Java集合框架:从底层原理到高阶应用的全方位深度解析

150 阅读9分钟

集合

1. 集合和数组

我们知道,数组是长度固定,内存连续的。他存放的是相同类型的元素。

而集合相当于优化了数组的一些缺点:

  1. 集合中可以存储不同数据类型的元素,当然也可以指定泛型存储相同的数据类型。
  2. 集合的长度是可变的,当存储的元素达到一个阈值时就会自动扩容。
  3. 集合有更加丰富的数据结构,使用方式可以根据不同应用场景来切换不同的集合类型

2. 泛型

2.1 基本使用

泛型用于强制规定集合统一数据类型的方式,如果不加泛型add()和get()获取的都是Object,需要强转才能正常使用。如果添加了泛型add()和get()获取的就是泛型所规定的类型(省略强转)。

  • 泛型方法

    • 非静态方法:内部的泛型会根据类的泛型去匹配(创建对象时)

    • 静态方法:必须声明出自己独立的泛型,在方法调用时确定

       public static <E> void print(E[] e) {
           for (E e1 : e) {
               System.out.print(e1 + " ");
           }
       }
      
  • 泛型接口

    • 实现接口的时候指定具体的类型

    • 让接口的泛型跟着类的泛型去匹配

       public class ArrayList<E> extends AbstractList<E> implements List<E>
      
  • 通配符

    • ?:可以是任意类型

    • ? extend E:E或是其子类

       public static void test1(ArrayList<? extends B> list)
      
    • ? super E:E或其父类

       public static void test2(ArrayList<? super B> list)
      
2.2 泛型擦除

在jdk1.5之前,java是没有泛型这个概念的,在很多需要使用集合的地方都要进行强制类型转化,很容易出错 ,因此引入了泛型。但是java的泛型是一种“假泛型”,java到编译器会进行泛型擦除,在jvm层面自始至终都没有泛型这个概念,底层一直是在做强转。反射的时候也无法获取到集合泛型的类型

 ArrayList<Integer> l1 = new ArrayList<>();
 ArrayList<String> l2 = new ArrayList<>();
 System.out.println(l1.getClass() == l2.getClass());//true

3. 集合体系结构

3.1 集合的分类

单列集合

  • Collection单列集合父类接口 提供迭代器

    • List接口

      • ArrayList
      • LinkedList
      • Vector:数组初始长度10,存满扩容,扩到原数组2倍。(早期java版本的一部分,今不推荐使用)
    • Set接口

      • HashSet ->LinkedHashSet
      • TreeSet
    • Queue

      • LinkedList

双列集合

  • Map双列集合父类接口

    • HashMap ->LinkedHashMap
    • TreeMap
    • HashTable
    • ConcurrentHashMsp

其中,List存储的元素是 有序可重复,有索引的。set存储的元素是 无顺序不重复,无索引的。Map比较特殊,是基于key-value存储的,key是唯一的 不能重复,value是可以重复的,而且每组key和value都是无序的。

3.2 List和Set和数组 相互转化
  • 数组转list或set

    注意:Arrays.asList()返回的是Arrays内部实现的固定大小的列表,不能增删,修改数组会影响到列表,反之亦然。与subList()类似。如果要修改外边再套一层ArrayList

     Integer[] nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
     List<Integer> list = Arrays.asList(nums);
     HashSet<Integer> set = new HashSet<>(Arrays.asList(nums));
     ​
     int[] a = {1, 2, 3, 4, 5};
     List<Integer> list = Arrays.stream(a).boxed().collect(Collectors.toList());
    
  • list或set转数组

     Object[] objects = list.toArray();
     Integer[] nums1 = list.toArray(new Integer[0]);
     Integer[] nums2 = set.toArray(new Integer[0]);
      
    
  • list和set相互转化

     ArrayList<Integer> list = new ArrayList<>(set);
     HashSet<Integer> s = new HashSet<>(list);
    
3.3 工具类

Collections是集合的工具类,提供了很多集合操作的方法 比如:sort,shuffle,addAll,binarySearch,max,min,swap(交换指定元素的位置)

4. 单列集合

4.1 List接口
4.1.1 实现类

List接口下主要有ArrayList和LinkedList两个实现类。他们两个的区别和底层实现如下:

  • ArrayList:底层是数组实现的;查询快,增删慢线程是不安全的,执行效率高

    • 查询快的原因:内存是连续的,可以通过下标快速查找元素

    • 增删慢的原因:数组的长度是固定的,新增和删除元素的时候长度改变了,需要定义长度更长的数组进行元素的前移/后移

    • 扩容原理:

      • 利用空参构造创建的集合,会在底层创建一个默认长度为0的数组
      • 添加第一个元素时会创建一个新的长度为10的数组
      • 当长度达到数组最大值的时候扩容1.5倍
      • 如果一次性添加多个元素,1.5倍还放不下,则新的数组长度会以实际长度为准。
  • LinkedList:底层是双向链表实现的,查询慢,增删快;线程不安全,执行效率高;链表是靠结点指向下一个结点的,内存是不连续的,理论上是没有长度限制的

    • 查询慢原因:链表结构,值通过结点指向下一个结点的,而且内存不是连续的,只能通过前一个结点才能找到下一个结点,所以只能从头结点遍历尾结点,效率低下。但是查询头结点/尾结点效率很高。
    • 增删快原因:链表结构,想增删只需要修改指向下一个结点的地址 (换成其他结点)这样其他结点元素不受影响,不需要前移后移,效率比较高。(问到了再答)
4.1.2 集合运算
  • 交集:获取两个集合相同的元素

     list1.retainAll(list2);//list1就会保存相同的元素
    
  • 并集:两个集合的元素合并保存(不去重)

     list1.addAll(list2);//2.获取两个集合合并的结果
    
  • 差集:获取两个集合不同的元素

     list1.removeAll(list2);//3.获取两个集合不同的元素
    
4.1.3 集合排序

List 的排序方式

  • 使用 Collections.sort 默认正序,可以传第二个参数自定义排序
  • 自定义bean实现 Comparable 接口。
  • 实现Comparator接口自定义比较器
4.2.Set接口

主要实现类有HashSet、LinkedHashSet和TreeSet

  • HashSet:底层就是HashMap。HashMap底层是哈希表数据结构,jdk1.8开始哈希表等于数组+链表+红黑树

    • 数组默认长度是16,并且有一个加载因子(0.75) 当存储元素超过了长度的75% 才开始扩容 扩容2倍

    • 存储元素(put方法)的原理:

      • 先根据存储的key值调用hashCode()方法来计算哈希码。这个哈希码会经过再次计算得出最终的哈希码
      • 根据哈希码做位运算,求出数组下标 这样就知道了元素在该存储数组的什么位置中
      • 如果数组存储下标对应的空间没有值,就直接存入
      • 如果有再去调用equals()方法比较数组中的元素是否相等。如果相等说明是重复的元素 则进行替换(保证元素唯一) ,如果不相等就是哈希冲突 就通过next指向下一个结点来保存所有hashCode相同而值不同的元素(属于链地址法)
      • 链表长度超过8数组元素长度超过64 会影响性能 所以会把链表转化成红黑树 。如果红黑树高度小于6 就会在转化成链表

      ps: 先获取原始的哈希值,再将原始哈希值向右移动16位做哈希扰动(希望高位的数据也能参与运算,可一定程度减少链表的挂载数量),再和原始哈希异或做二次哈希。得到哈希值后再和数组长度-1做与运算(相当于取模) 算出应存入的索引位置

    HashMap的扩容机制:

    • 扩容数组

      • 当数组中元素个数达到了数组长度*加载因子(0.75) 扩容原数组2倍的大小
      • 链表长度超过8个且数组长度没有超过64
    • 链表转红黑树

      • 链表挂载元素超过8个且数组长度达到了64
  • TreeSet:底层实现TreeMap,数据结构是红黑树,支持自然排序(对象排序需要实现比较器对象)

    红黑树:是一个特殊自我平衡的二叉树。普通二叉树是根据高度不超过1来控制平衡,而红黑树是通过修改结点成红色和黑色达到颜色的自我平衡

    • 根节点必须是黑色的
    • 每个存储null 的叶子结点也是黑色的
    • 如果有一个节点是红色的 那么他的子节点 只能是黑色的。因为他不影响出现两个连续的红节点存在
    • 保证从根节点开始到达任意一个叶子节点 他们黑色的数量是一样的 从而达到了颜色自我平衡(否则要实现左旋和右旋 比较复杂)
4.3 遍历方式
  • 标准for(List集合才能用)

  • 增强for(底层就是迭代器实现)

  • 迭代器遍历

    • java的迭代器是Collection接口提供的。通过游标(下标)跟踪和一种快速失败的机制(Fail-Fast) 来实现的,他提供了一种安全且统一的集合遍历方式。可以使集合的遍历和集合的实现过程解耦,同时还可以防止并发修改导致的数据不一致问题,(list和set才能直接使用迭代器变量,map需要间接使用)。

    • 底层原理:

      1. 游标跟踪:迭代器通过一个类似于下标 跟踪 遍历集合元素的位置,每遍历一次下标+1,并且每次变量元素的时候都会现做一个快速失败机制验证,为了防止并发问题
      2. 快速失败机制(Fail-Fast):集合内部会维护一个modCount(修改计数器)每次集合结构发生变化都会递增。而迭代器每次创建时都会记录当前的modCount,是为了以后每次调用next()或者remove()会去验证两者是否一致 如果不一致则抛出并发修改异常

5. 双列集合

5.1 实现类

Map接口下主要有HashMap、HashTable、TreeMap、LinkedHashMap、ConcurrentHashMap

  • HashMap:基于哈希表实现的,允许key和value为null。默认长度16,加载因子0.75 扩容2倍。线程不安全,执行效率高。
  • HashTable:基于哈希表实现的,不允许key和value为null。 默认长度11,加载因子0.75,扩容2倍+1。线程安全,执行效率低。
  • TreeMap:是TreeSet的底层实现,可以根据key值做自然排序。不能针对value排序
  • LinkedHashMap:相比HashMap是有序的 (使用较少)
  • ConcurrentHashMap:相当于线程安全的HaspMap。底层不仅保证了安全,执行效率还比一些传统的线程安全的集合(HashTable)效率高一些。底层实现是通过分段锁来保证线程安全(会把一些数据分成很多数据段,每个数据段都加了一把子锁),当一个线程获取到了一把子锁,其他线程对其他数据操作是不影响的
5.2 遍历方式
 for (String key : map.keySet()) {
     System.out.println(key + "=" + map.get(key));
 }
 for (Map.Entry<String, Integer> e : map.entrySet()) {
     System.out.println(e.getKey() + "=" + e.getValue());
 }
 //迭代器
 Iterator<String> it = map.keySet().iterator();
 while (it.hasNext()) {
     String key = it.next();
     System.out.println(key + "=" + map.get(key));
 }
 Iterator<Map.Entry<String, Integer>> it2 = map.entrySet().itera
 while (it2.hasNext()) {
     Map.Entry<String, Integer> e = it2.next();
     System.out.println(e.getKey() + "=" + e.getValue());
 }
 map.forEach((k, v) -> System.out.println(k + "=" + v));
 //forEach

6. 集合面试常见面试题

  1. Collection和Collections区别?
  2. list,set,map的区别?
  3. ArrayList和LinkedList的区别?
  4. ArrayList扩容原理
  5. HashSet 或 HaspMap底层原理?
  6. HashMap和HashTable的区别?
  7. ConcurrentHashMap如何保证线程安全?
  8. 怎么获取Map的k和v?