Java集合框架源码学习笔记

对集合类的分析从以下几点入手

1 底层数据结构
2 增删改查方式
3 初始容量,扩容方式,扩容时机。
4 线程安全与否
5 是否允许空,是否允许重复,是否有序

ArrayList

数据结构:动态数组
初始容量

  • 使用无参构造器时,默认数组大小为10;
  • 使用指定容量大小initialCapacity的构造器时,初始化容量为initialCapacity的数组

扩容时机:要添加一个元素前判断(oldsize+1)是否大于数组容量,大於则进行1.5*oldsize+1扩容
扩容方式:新建一个大小为原数组1.5倍的数组,将原数组元素复制到新数组,原数组引用指向新数组。
增加和删除元素:都会调用System.arraycopy()方法将旧数组元素复制到新数组中
其他:非线程安全,元素可以为空,允许重复,插入有序

Vector(线程安全的,可以指定增长因子的ArrayList)

数据结构:动态数组
初始容量

  • 使用无参构造器时,默认数组大小为10;
  • 使用指定容量大小initialCapacity的构造器时,初始化容量为initialCapacity的数组

扩容时机:要添加一个元素前判断(oldsize+1)是否大于数组容量,大於则进行扩容
扩容方式:如果在创建Vector时,指定了capacityIncrement的大小;则,每次当Vector中动态数组容量增加时>,增加的大小都是capacityIncrement。如果容量的增量小于等于零,则每次需要增大容量时,向量的容量将增大一倍。
增加和删除元素:都会调用System.arraycopy()方法将旧数组元素复制到新数组中
其他:线程安全,元素可以为空,允许重复,插入有序

LinkedList

用循环双向链表实现,初始化时header结点的前后指针都指向自己
非线程安全,元素可以为空,允许重复,插入有序
增删改查没什么好说的。

ArrayDeque

数据结构:循环数组
初始容量

  • 使用无参构造器时,默认容量为16;
  • 使用指定容量大小numElements的构造器时,numElements小于8,则初始化容量为8的数组;numElements大于8,则初始化>=numElements的最小的2的幂次方大小的数组。

扩容时机:当head下标和tail下标重合时,说明数组已满,扩充为原来的2倍大小。
扩容方式:新建一个大小为原数组两倍的数组,将原数组元素复制到新数组,原数组引用指向新数组。
元素下标:通过和(数组长度-1)进行‘&’操作来保证下标始终不越界,构成了循环数组。
其他:非线程安全,元素不能为空,允许重复,插入有序

PriorityQueue

数据结构:使用数组来实现堆的结构
初始容量:11
扩容时机:添加元素前判断数组是否已满,如果满了需要扩容
扩容方式:旧容量n<64则 变为2n+2;否则变为1.5n
添加元素:先将元素添加到最后的叶子结点,再自下而上调整
删除堆顶元素:将最后的叶子结点元素放在堆顶位置,再自上而下调整
删除非堆顶元素:将最后一个叶子结点元素放在被删除元素的位置i,从i先自上而下调整,若调整后发现被删除元素位置i的元素没有发生变化,再从i自下而上调整
其他:非线程安全,为了确保线程安全,使用 java.util.concurrent.PriorityBlockingQueue;元素不能为空,允许重复,插入无序

HashMap(jdk 1.7)

数据结构:数组+链表
初始容量:使用无参构造器时数组大小默认为16,负载因子0.75
扩容阈值:容量*负载因子
允许键为空,但是只允许一个这样的键值对存在,存放在table[0]中
不允许重复,插入无序,非线程安全
扩容时机:插入元素前判断元素个数是否>=扩容阈值,满足则进行2倍扩容

Hash算法

  1. 计算key的hashCode,然后对hashCode进行9次扰动处理:4次移位+5次异或
  2. index=hash&(table.length-1);

put(key,value)操作:

  1. 若哈希表没有初始化,即table为空,则利用构造函数设置的扩容阈值(16)对table数组进行初始化 [真正的初始化存储数组table是在第一次进行put操作时]
  2. 判断key是否为null
    2.1 key为空(hash==0),则将键值对存放在table[0]。该桶中最多只有一个键值对,新value会覆盖旧value
    2.2 若不为空,由key的hash得到数组索引,在该桶下遍历
      2.2.1在链表中找到相同的key,则新value覆盖旧value并返回旧value
      2.2.2 在链表中没有找到相同的key,将先判断数组是否需要扩容,若需要扩容,则新容量为旧容量的2倍,再将新的Entry插入到链表的头部,并返回null

get(key)操作:

1.判断key是否为空,如果为空则从table[0]中查询有没有key为null的键值对
2.如果不为空,则利用key的hashCode得到数组索引,在该桶中遍历Entry去查询对应的key是否存在

resize(newCapcity)操作:

1.先判断旧容量是否以及达到2^30,如果达到,则将扩容阈值直接设置为整数的最大值,返回。
2.否则,对容器中的每个元素重新计算hash值,重新计算对应的新数组索引位置,然后采用链表头插法:将旧数组中各桶中链表的元素逆序存放在新的数组中,即扩容前 = 1->2->3,扩容后 = 3->2->1

HashMap(jdk 1.8)

数据结构:数组+链表+红黑树
初始容量:使用无参构造器时数组大小默认为16
负载因子:0.75
扩容阈值:容量负载 * 因子
桶的树化阈值: 默认为8
桶的链表还原阈值: 默认为6
最小树形化阈值:默认64

最小树形化阈值,即当数组长度>=该值时,才允许树形化链表,否则,哈希表元素过多时选择进行扩容而不是树形化。
最小树形化阈值不能小于:4 * 桶的树形化阈值

允许键为空,但是只允许一个这样的键值对存在,存放在table[0]中
不允许重复,插入无序
非线程安全

扩容时机:插入元素后判断元素个数是否>=扩容阈值,满足则进行2倍扩容

Hash算法

  1. 计算key的hashCode,然后对hashCode进行2次扰动处理:1次移位+1次异或。相当于hashCode的高16位不变,低16位与高16位做异或处理
  2. index=hash&(table.length-1);

put(key,value)操作:

  1. 若哈希表没有初始化,即table为空,则利用构造函数设置的扩容阈值(16)对table数组进行初始化 [真正的初始化存储数组table是在第一次进行put操作时]
  2. 由hash值定位到的桶为空,则直接插入新结点
  3. 桶不为空,说明hash冲突
    3.1 首先判断待插入结点的key与桶中的第一个结点key是否相同
      相等则直接新value替换旧value
    3.2 不相同则由该桶中的第一个结点类型判断该桶中的结点类型是否为红黑树的结点
      是红黑树的结点,在红黑树中插入或者更新结点
    3.3 不是红黑树结点则说明桶中节点为链表的结点
      在链表中插入或者更新结点,注意是尾插法;插入后再判断链表长度是否大于8,大於则进行链表的树化。在树化方法中会首先判断数组长度是否小于最小树形化阈值,小於则进行扩容处理而不是红黑树化;大於则将链表转化为红黑树。
  1. 插入新的结点成功后,再判断键值对个数是否大于扩容阈值,进行扩容处理。

get(key)操作:

  1. 由key的hash定位到桶的索引
  2. 当桶非空时,总是先检查桶中的第一个元素
  3. 若桶中的第一个元素不是我们要找的key,那么判断结点是不是树的结点,是则从红黑树中查找元素。
  4. 不是树结点,则说明为链表结点,依次遍历

resize()操作:

  1. 先判断旧容量是否以及达到2^30,如果达到,则将扩容阈值直接设置为整数的最大值,返回。
  2. 若旧容量大于默认的初始容量16,并且扩为旧容量2倍的新容量小于容量允许最大值2^30,则将阈值变为原来的2倍
  3. 对原数组链表中的每个键值对进行重新定位,
    (1)hash值新增参与运算的位为0的结点以原来的相对顺序构成链表,其在新桶集中的索引与旧桶集中的桶索引相同,即元素扩容后的桶索引=原始索引
    (2) hash值新增参与运算的位为1的结点以原来的相对顺序构成链表,保留在新增容量的高位的桶中,即元素扩容后的桶索引=原始位置+扩容前的旧容量

HashTable

数据结构:数组+链表
初始容量:使用无参构造器时数组大小默认为11,负载因子0.75
扩容阈值:容量*负载因子
扩容时机:插入元素前判断元素个数是否>=扩容阈值,满足则进行(2倍+1)扩容
键和值都不能为空
不允许重复,插入无序
线程安全(synchronized修饰)

Hash算法

  1. hash=hashCode & 0x7fffffff (&0x7FFFFFFF的目的是为了将负的hash值转化为正值,只改变hashCode的符号位)
  2. index=hash % table.length;

synchronized put(key,value)操作:

  1. 先判断value是否为空,为空则抛出异常
  2. 利用hash算法定位桶索引
    2.1 key和hash值相同,则更新value
    2.2 否则,链表头部插入新Entry。插入前判断是否需要扩容

synchronized get(key)操作:

1.利用hash算法定位桶索引
2.key和hash值相同,则返回value,否则返回null

resize()操作:

1.先设置新的数组长度为旧长度的2倍+1
2.判断数组新长度是否大于允许最大数组长度(Integer.MAX_VALUE - 8),大於则设置新的数组长度为允许最大数组长度。
3. 将旧数组链表的元素复制到新的数组链表,正序遍历原桶中的元素,然后采用头插法插入到新的桶中,所以相当于逆序复制。

总结: 相当于线程安全版本的jdk1.7HashMap,采用数组链表结构,键和值都不允许为空,hash算法比hashMap简单,不对hashCode做复杂的扰动处理。

LinkedHashMap(jdk1.6)【继承自HashMap】

数据结构:数组+链表+双向链表
初始容量:使用无参构造器时数组大小默认为16,负载因子0.75
扩容阈值:容量*负载因子
允许键为空,但是只允许一个这样的键值对存在,存放在table[0]中
不允许重复,非线程安全,有序:访问顺序和插入顺序
扩容时机插入元素后判断元素个数是否>=扩容阈值,满足则进行2倍扩容

对HashMap中的recordAccess方法进行了重写:
当AccessOrder为true时,双向链表的元素按照访问顺序排序,调用get方法时其中的recordAccess方法会将当前访问的元素添加到双向链表的末尾; 当AccessOrder为false时,双向链表元素按照插入顺序排序,recordAccess方法什么也不做,进行put操作时新结点默认是加到双向链表末尾的,满足插入顺序。

Hash算法

  1. 计算key的hashCode,然后对hashCode进行9次扰动处理:4次移位+5次异或
  2. index=hash&(table.length-1);

put(key,value)操作:

相比较HashMap的put方法
重写了其中的addEntry方法,新增了将新结点加入到双向链表的步骤。
重写了recordAccess方法,涉及到了LRU算法,即header后的第一个结点就是最近最少使用的结点。

get(key)操作:

对hashMap中的get方法中的recordAccess进行了重写,用于LRU算法。具体元素在双向链表中如何排序取决于AccessOrder标志位。

resize(newCapcity)操作:

1.先判断旧容量是否以及达到2^30,如果达到,则将扩容阈值直接设置为整数的最大值,返回。
2.否则,对容器中的每个元素重新计算hash值,重新计算对应的新数组索引位置【这里重写了transfer方法,思路是一样的,不过LinkedHashMap利用双向链表独特的结构进行键值对的遍历重哈希】,然后采用链表头插法将旧数组链表的元素存放在新的数组链表中。

总结:LinkedHashMap的Entry继承了HashMap.Entry,并且多了before和after两个指针,用于构建双向链表。通过双向链表,LinkedHashMap能够将键值按照元素插入排序和元素访问顺序排序,具体哪一种排序方式由AccessOrder这个标志位来决定。按照元素访问顺序排序的算法思想是LRU,将当前访问的entry移到双向链表的末尾,这样header后的第一个结点就是最近最少使用的结点,也就是最老的结点。

HashSet(jdk1.8)

由它的构造函数可以看出来HashSet的功能是通过HashMap实现的,HashSet中的元素集合就是HashMap的key集合,实现HashSet的HashMap中的所有value都相等,这就保证了HashSet元素的唯一性。

TreeMap(jdk1.8)

底层数据结构为红黑树,红黑树是一个自平衡的二叉查找树,查找、删除、增加元素的时间复杂度均为O(logn). 由于元素之间的比较是key类型数据之间的比较,所以key不能为空。

TreeSet

由它的构造函数可以看出来TreeSet的功能是通过TreeMap实现的,TreeSet中的元素集合就是TreeMap的key集合,实现TreeSet的HashMap中的所有value都相等,这就保证了TreeSet元素的唯一性。
同样key不能为空。

LinkedHashSet

由它的构造函数可以看出来LinkedHashSet的功能是通过LinkedHashMap实现的,LinkedHashSet中的元素集合就是LinkedHashMap的key集合。
同样key不能为空。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章