Java核心技术--集合篇

本文深入解析Java集合框架,包括Collection、Set、List、Map的主要实现类及其特点,探讨了ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等的数据结构与应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 集合

集合与数组的区别:

集合数组
长度可变固定
存储的数据类型对象的引用基本数据类型

整个集合类的体系结构(简略):

在这里插入图片描述

1.1 Collection

Collection接口的常用方法:

返回值类型方法含义
booleanadd(E e)将指定的对象添加到集合中
booleanremove(Obiect o)将指定的对象从集合中删除
intsize()返回此集合中的元素数
booleanisEmpty()判断集合是否为空
Iteratoriterator()返回此集合中的元素的迭代器,可用于遍历集合中的元素
booleancontains(Object o)判断集合是否包含指定的元素
Object[]toArray()将集合转换成数组
1.1.1 List

List接口存储一组不唯一(元素可以重复),有序(顺序与插入顺序一致)的对象

相对于Collection接口增加的方法:

返回值类型方法含义
Eget(int index)返回集合中指定索引处的元素
voidadd(int index,E e)将指定的元素添加到该集合中的指定位置上
intindexOf(Object o)返回集合中指定元素第一次出现的索引,如果元素不存在,则返回-1
intlastIndexOf(Object o)返回集合中指定元素最后一次出现的索引,元素不存在,则返回-1
Eremove(int index)移除列表中指定位置的元素, 返回的是被移除的元素
Eset(int index , E e)用指定元素替换集合中指定位置的元素,返回更新前的元素
1.1.1.1 ArrayList

ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问(根据索引位置)。当当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

1.1.1.2 LinkedList

LinkedList的底层是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

1.1.1.3 Vector

Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。

三者之间的区别:

ArrayListLinkedListVector
底层实现数组循环双向链表数组
是否线程安全不安全不安全安全
优缺点查询速度快,插入删除慢插入删除快速度慢
1.1.2 Set

Set接口存储一组唯一无序的对象

它注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象hashCode值(java是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖Object的hashCode方法和equals方法。

1.1.2.1 HashSet

HashSet的底层是HashMap(哈希表)支持的,他是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    //每一个HashSet的构造函数里面都实例了HashMap对象
    public HashSet() {
        map = new HashMap<>();
    }

    //省略一万字
    .............
}

HashSet存储数据的原理:

JDK1.8之前,哈希表的底层是采用数组+链表的方式实现的,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而在JDK1.8以后,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值 8 时,将链表转换为红黑树,这样大大减少了查找时间。

在这里插入图片描述

综上:HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equls结果为true ,HashSet就视为同一个元素,舍弃其中一个。如果equals 为 false 就不是同一个元素,就会在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中),也就是哈希值一样的存一列,如果个数超过了8 ,链表结构 就会转化为红黑树结构。

思考:如果要存放自定义类型的对象 ,则该怎么存储 ?
解决办法:必须重写hashCode方法和equals方法

HashSet存放自定义类型的对象 :

public class Student implements Serializable {

    private static final long serialVersionUID = 6030711162869997903L;

    private Integer id;
    private String username;

    public Student(Integer id, String username) {
        this.id = id;
        this.username = username;
    }

    //重写hashCode方法和equals方法

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(id, student.id) &&
                Objects.equals(username, student.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username);
    }
    
    //省略get/set/toString方法
}

public static void main(String[] args) {

        Set<Student> students = new HashSet<>();
        students.add(new Student(1,"张三"));
        students.add(new Student(1,"张三"));
        students.add(new Student(2,"李四"));

        Iterator<Student> iterator = students.iterator();
        while (iterator.hasNext()){
            Student student = iterator.next();
            System.out.println(student);
        }
    }

没有重写hashCode和equals方法时,打印结果如下:Set集合里存放了重复的元素
在这里插入图片描述

重写hashCode和equals方法时,打印结果如下:
在这里插入图片描述

1.1.2.2 LinkedHashSet

HashSet里面存放的元素是无序的,但是他有一个子类:LinkedHashSet可以保证数据的有序存放。通过查看底层源码可知:它继承HashSet、又基于LinkedHashMap来实现的。LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承HashSet,其所有的方法操作上又与HashSet相同,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2851667679971038690L;

    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }
    //点进上面的super方法后,源码如下:
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    //由此可知,LinkedHashSet是基于HashSet和LinkedHashMap来实现的


    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }

 
    public LinkedHashSet() {
        super(16, .75f, true);
    }

 
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }

   
    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
    }
}
1.1.2.3 TreeSet

TreeSet是一个有序的集合,它的作用是提供有序的Set集合,但是读取数据速度慢。它继承了AbstractSet抽象类,实现了NavigableSet,Cloneable,Serializable接口,而NavigableSet实现了SortedSet接口。TreeSet的元素支持2种排序方式:自然排序或者根据提供的Comparator进行排序

TreeSet相对于Set接口增加的方法:

返回值方法含义
Efirst()返回此集合中第一个(最低)元素
Elast()返回此集合中最后一个(最高)元素
Comparatorcomparator()返回用于对该集合中的元素进行排序的比较器
SortedSetheadSet(E e)返回e之前(不包含e)的所有元素
SortedSetsubSet(E e2,E e2)返回e1到e2之间的所有元素,含e1不含e2
SortedSettailSet(E e)返回集合中大于或等于e的所以元素

注:凡是涉及到取中间元素的方法基本都是含左不含右

TreeSet是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。Integer和String对象都可以进行默认的TreeSet排序,而自定义类的对象是不可以的,自己定义的类必须实现Comparable接口,并且覆写相应的compareTo()函数,才可以正常使用。所以在使用TreeSet集合时,一定要保证存放的对象实现了Comparable接口并且实现了compareTo方法

注:在重写conpareTo方法时,返回值不同集合遍历输出的元素顺序就不同。如果返回值为正整数,就认为新插入的元素比上一个元素大,于是二叉树存储时,会存在根的右侧;如果返回值为0,认为是相同的元素;如果返回值为负整数,就认为新插入的元素比上一个元素小,于是二叉树存储时,会存在根的左侧。特别注意TreeSet遍历输出时默认采用的是中序遍历的方式

//Student.java
public class Student implements Comparable<Student>, Serializable {

    private static final long serialVersionUID = 7527642451143166932L;

    private String username;
    private Integer age;

    public Student(String username, Integer age) {
        this.username = username;
        this.age = age;
    }

    //省略get/set方法
    ...........

    //重写compareTo方法 按照年龄排序
    @Override
    public int compareTo(Student o) {
        //return this.age-o.age;   升序(递增) 可参照二叉树构造原理分析
        return o.age - this.age;    //逆序(递减)
    }

    @Override
    public String toString() {
        return "Student{" +
                "username='" + username + '\'' +
                ", age=" + age +
                '}';
    }
}

//测试类
 public static void main(String[] args) {

        /**
         * TreeSet集合中的元素必须实现Comparable接口,并实现compareTo方法
         * TreeSet最终是采用中序的遍历方式遍历输出集合中元素的
         */
        Set<Student1> students = new java.util.TreeSet<>();
        students.add(new Student1("张三",15));
        students.add(new Student1("李四",25));
        students.add(new Student1("王五",18));

        Iterator<Student1> iterator = students.iterator();
        while (iterator.hasNext()){
            Student1 stu = iterator.next();
            System.out.println(stu);
        }
    }

打印结果如下:

在这里插入图片描述

1.2 Map

Map接口提供的是key到value的映射。Map集合中不能包含相同的key,并且每个key只能映射一个value,且允许值为null。

Map接口中常用的方法:

返回值方法含义
Vput(K key,V value)把指定的键与指定的值添加到Map集合中
Vget(Object key)根据指定的键,在Map集合中获取对应的值
Vremove(Object key)移除Map集合中指定的元素
booleancontainsKey(Object key)判断Map集合中是否包含指定的键
booleancontainsValue(Object value)判断Map集合中是否包含指定的值
SetkeySet()获取Map集合中所有的键
Collectionvalues()获取Map集合中所有的值
Set<Map.Entry<K,value>>entrySet()获取到Map集合中所有的键值对对象的集合
1.2.1 HashMap

底层实现:HashMap的底层实现同HashSet一样,1.8之后采用 数组+链表+红黑树

HashMap根据键的hashCode值存储数据,当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须覆写对象的hashCode和equals方法。大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null(HashMap允许键和值为null)。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap

ConcurrentHashMap:ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全,而且访问速度也不慢。

JDK1.7 ConcurrentHashMap的结构

在这里插入图片描述

concurrencyLevel:并行级别、并发数、Segment 数。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

JDK1.8 ConcurrentHashMap的结构:在7的基础上加上了红黑树,并且舍弃了Segment+ReentrantLock,采用CAS+Synchronized来实现并发安全性。

在这里插入图片描述

CAS(Compare And Swap):比较并且替换,是乐观锁的一种实现方式,是一种轻量级锁。

1.2.2 LinkedHashMap

LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,使得map中存放的key和取出的顺序一致。底层实现采用的就是链表+哈希表

1.2.3 TreeMap

TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap时传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

1.2.4 HashTable

Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,且键和值不允许为空,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

1.3 Iterator

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口Iterator,Iterator接口也是Java集合中的一员,但他与Collection、Map接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。

主要方法:

返回值方法含义
booleanhashNext()判断集合中是否有元素,如果仍有元素可以迭代,则返回 true
Enext()返回迭代的下一个元素
1.4 Collections工具类

java.utils.Collections是集合工具类,用来对集合进行操作。

常用的方法:

返回值方法含义
booleanaddAll(Collection c, T… elements)往集合中添加一些元素
voidshuffle(List list)打乱集合中元素的顺序
voidsort(List list)将集合中元素按照默认规则排序(升序)
voidsort(List list,Comparator<? super T> )将集合中元素按照指定规则排序
Map<K,V>synchronizedMap(Map<K,V> m)返回一个线程安全的Map集合
public class CollectionsDemo {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        //原来写法
        //list.add(5);
        //list.add(222);
        //list.add(1);
        //list.add(2);
        //采用工具类 完成 往集合中添加元素  
        Collections.addAll(list, 5, 222, 12);
        System.out.println(list);
        //排序方法 
        Collections.sort(list);
        System.out.println(list);
    }
}
结果:
[5, 222, 1, 2]
[1, 2, 5, 222]

由上可知:sort方法默认的排序规则是升序,如果想实现降序,就需要用到Comparator比较器。

Comparator比较器:需要实现compare方法

public static void main(String[] args) {
        List<Person> persons = new ArrayList<>();
        persons.add(new Person("张三",21));
        persons.add(new Person("李四",17));
        persons.add(new Person("王五",19));

        //降序
        /**
          *  o1 - o2:升序
          *  o2 - o1:降序
          */
        Collections.sort(persons, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o2.getAge()-o1.getAge();
            }
        });

        Iterator<Person> iterator = persons.iterator();
        while(iterator.hasNext()){
            Person person = iterator.next();
            System.out.println(person);
        }


    }

Comparable和Comparator两个接口的区别:

Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator:强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值