Java Collection集合源码详解 —— Collection
作者:shiwyang
Java版本:1.8
- 深入了解集合源码与数据结构的关系,了解集合内部不同子类的共性与区别
- 一共分为三个部分:Collection Map ConcurrentHashMap
- 这是第一个部分 Collection 集合部分,主要分为 List 和 Set 两个模块
集合概述
- 集合主要是两组(单列,K-V)
Collection接口由两个重要的子接口ListSet,他们的实现子类都是单列集合Map接口的实现子类 是双列集合,存放K-V的
List 接口
List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
List中主要使用的时ArrayList Vector LinkedList 三个类,通过学习三个类的作用以及内部实现源码,我们可以了解List接口的共性和三个类之间的特性。
ArrayList
-
ArrayList的底层是一个Object[]数组用于存储元素,当数组内元素已满之后,要再添加元素的时候就会触发ArrayList的扩容机制。
-
ArrayList 是一个线程不安全的,在多线程的时候不要使用这个类。
-
ArrayList 可以添加任意元素。
ArrayList扩容机制
**说明:**ArrayList有一个自动扩容机制,通过阅读源码,来理解这个扩容机制是怎么实现的。
构造函数:ArrayList 有两个构造函数,分别是无参构造,和int 对象的参数构造。ArrayList的初始长度就是参数携带值的长度,如果是无参的话,就默认为10。
**扩容机制:**当ArrayList内需要添加新元素,但是List满了的话,就会触发这个扩容机制。每次扩容为ArrayList当前数组的1.5倍长度。
源码解读:
ArrayList的底层实现是通过一个elementData的Object[]数组来存储元素的,提供了一个默认的长度值:10。
注意:transient字段是指在 序列化过程中需要屏蔽这个值。
// 默认长度大小 : 10
private static final int DEFAULT_CAPACITY = 10;
// ArrayList 存储元素的Object[]数组
transient Object[] elementData; // non-private to simplify nested class access
**第一步:**ArrayList提供了两个构造函数,规定了ArrayList构造的时候可以放入一个 >= 0的int参数,作为ArrayList的初始长度。
这里使用不同的两个构造函数会出现两种情况:
- 使用无参的构造函数,会赋予一个默认的值,但是Object[]的长度还是0,要直到放入第一个元素的时候才会将Object[]数组扩容到10
- 使用有参的构造函数,就会直接把Object[]数组创建为对应的数值。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//创建一个新的Object[]数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
第二步: 执行add方法时,判断数组是否需要扩容。
确认容量函数中,假如数组为空,就附默认值minCapacity = 10对应无参构造函数第一次添加元素的情况。
public boolean add(E e) {
// 确认容量函数
ensureCapacityInternal(size + 1); // Increments modCount!!
//size++ 的意思是先把e元素放进elementData[size] 之后,size++
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {‘
// 无参构造时第一次添加元素的情况。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
**第三步:**通过执行ensureExplicitCapacity() 函数,记录ArrayList的操作数和扩容判断。
private void ensureExplicitCapacity(int minCapacity) {
// 记录操作数
modCount++;
// 扩容判断
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 扩容
grow(minCapacity);
}
第四步: 扩容的具体实现。
-
第一个if对应的是当无参构造时,第一次添加元素的时候elementData.length = 0;minCapacity = 10;则该数组只需要扩容到默认值10。
-
第二个if,是当扩容的后的数值大于int最大值时的具体处理。
-
扩容使用的函数是Arrays里面的拷贝函数
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
通过以上四步,可以实现对ArrayList的扩容,这样就可以在每次容量不够的时候都能够实现自动扩容了。
Vector
-
Vector的底层也是一个Object[]数组,和ArrayList一样,也有一个自动扩容机制。 -
Vector是线程同步的,即线程安全。
Vector 扩容机制
**说明:**Vector 有一个自动扩容机制,通过阅读源码,来理解这个扩容机制是怎么实现的。
构造函数:Vector 有两个构造函数,分别是无参构造,和int 对象的参数构造。Vector 的初始长度就是参数携带值的长度,如果是无参的话,就默认为10。
扩容机制:当Vector 内需要添加新元素,但是List满了的话,就会触发这个扩容机制。每次扩容为Vector 当前数组的2倍长度,Vector还可以在构造函数里面设置默认扩容的大小参数。
当使用add函数的时候,和ArrayList 的处理方式基本一样,关键就是grow函数不同。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
Grow函数:
函数中有一个三元判断式
newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
当capacityIncrement 大于零时 取capacityIncrement 否则取oldCapacity
通过这个三元判断式子,就可以实现默认扩容两倍和自定扩容大小的功能。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
LinkedList
LinkedList的底层结构是一个双向链表。- 可以添加任何元素
- 线程不安全
LinkedList 源码解析
**构造函数:**默认的无参构造函数,什么都没做,因为没有元素,不需要维护链表。
public LinkedList() {
}
**add函数:**将元素链接到链表末尾。
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
// 创建一个新的结点,pre结点指向原链表的last,next指向空
final Node<E> newNode = new Node<>(l, e, null);
// 将last改为最新的元素
last = newNode;
// 当原先的last为空时,说明原链表为空,因此头尾都应该是新加入的结点。
if (l == null)
first = newNode;
else
l.next = newNode;
// 修改链表的规模和操作数
size++;
modCount++;
}
remove函数
remove函数有三个重载:
- 无参 删除头
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
- 删除指定的元素e,需要遍历数组,找到相符合的元素删除即可
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
- 删除指定的序号index,判断原链表是否有该index,如果有就删除对应的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
poll函数:poll函数的功能就是获取并删除最前面的元素,当头结点为空时,获取空元素。
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
pop函数:pop和remove()的功能一样,和poll函数功能相近,跟poll不同的是,poll是基于队列实现的,当队列头为空时,取空,pop是基于栈实现的,当栈为空时抛出异常。
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
push函数:将元素推出栈顶,基于栈思想实现。
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
// 当原链表的头结点为空时,说明链表为空,所以该元素加入后,原链表的尾结点也应该是新加入的元素
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
unlinkFirst函数:
这个函数有一个重点就是在清楚一个函数结点的时候,需要把该节点链接取消,使得jvm的GC能正常运行。
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
Set接口
-
Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。 -
Set 主要使用的实现有HashSet LinkedHashSet
HashSet
- 实现了Set 接口
- HashSet 实际上是HashMap
- 可以存放null,但只能有一个null
- 不保证元素是有序的,取决于hash之后,再确定索引的结果
- 不能有重复的对象
HashSet(HashMap) 介绍
- HashSet 就是一个HashMap底层是一个数组 + 链表 + 红黑树 的结构。
- 添加一个元素时,会先得到hash值,再转化为索引值
- 找到存储数据表table ,看索引位置是否已经存放元素
- 如果没有直接加入
- 如果有,调用equals比较,相同就放弃添加,不同就添加到最后
- 在Java8中,如果一个链表的元素个数超过某个值(默认为8),并且table的大小超过某个值(默认为64),就会发生树化(红黑树)
HashSet为什么能保证数据唯一性: 因为HashSet底层就是一个HashMap,K对应的是需要保存的对象,在HashMap中,K保证是唯一的(利用HashCode()计算hash值),所以可以保证存入的元素永远是唯一的。
HashSet 源码解析
构造函数: HashSet的构造函数是构造了一个HashMap。
public HashSet() {
map = new HashMap<>();
}
add方法: add方法,调用的是HashMap的put方法,put方法调用的是putVal方法。
putVal方法的几个参数:
- hash:key 的哈希值 -> 有一个哈希值计算函数 hash(key)
- key: key值,对应的是需要放置的元素e
- value:存放的是HashSet定义的默认PRESENT对象 Object(),只用于占位
- onlyIfAbsent :假如为真,不改变Value
- evict: 假如为假,
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
获取hash值函数: 利用Object携带的hashCode方法。当key不为空的时候,计算出key的hash值
hashCode是native方法,底层使用是c实现
右移16位是防止冲突,至于为什么能防止冲突,我也不太清楚
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal 函数(重点)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table是HashMap自带的存放node结点的数组
// 当表为空时,初始化表,16个空间
if ((tab = table) == null || (n = tab.length) == 0)
// resize()的作用是初始化或加倍表的大小。如果为空,则按照字段阈值中持有的初始容量目标分配。
n = (tab = resize()).length;
// 根据hash值取计算Key 应该存放到table表的哪个索引位置i
// 并把这个这个对象赋给p
// 判断p 如果为null 表示还没有存放过元素,就创建一个Node 放在该位置,相当于数组[p]链表头
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k; // 辅助变量
// 如果当前索引位置对应的链表的第一个元素和准备添加的元素的hash值相同。
// 并且满足 下面两个条件之一,就不能加入hashMap中
// ((k = p.key) == key 是相同的对象
// (key != null && key.equals(k) key不等于空,且equals方法为true
// 这就可以解释,为什么存放相同的 new String("tom") 放不进去 但是new 自己建立的对象,放得进去,因为String类里面重写了Object的equals方法,可以使得文字内容相同时equals返回true
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 再判断p 是不是红黑树,如果是,就使用putTreeVal()。涉及了很多红黑树的算法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 否则,遍历该数组元素的链表,查看该链表上,有没有相同的元素,如果有,就不加入,如果没有,就加到最后一个。
else {
// 死循环,使用break; 跳出
for (int binCount = 0; ; ++binCount) {
// 假如链表的下一个为空,说明链表遍历结束了,把新的元素添加到链表尾部,退出循环
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 当链表内元素大于TREEIFY_THRESHOLD(8)了,触发树化函数treeifyBin()
// 这个树化函数有一个机制,就是要链表的长度大于8,且数组的长度大于64,如果数组的长度小于64,就进行扩容*2,不进行树化。
// 数组扩容之后可能会导致原来的元素在数组上的位置发生改变
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 假如找到链表上的元素和需要添加的元素相同,就退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 对退出循环之后的临时变量e进行判断,假如e非空,说明这个元素在原来的hashMap上已经存在,所以,返回这个元素。
// 为什么e == null 说明链表中没有相同的元素呢? 因为在上面循环中 e一直等于p.next 假如p.next == null 说明链表已经遍历到最后了,也就是说前面的所有元素都跟要添加的元素不同,如果e!=null ,也就是说p.next != null 说明e这个元素触发了第二个if 退出了循环。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 在HashSet中,使用的都是同一个Value对象,所以在HashSet里面这一句没意义
// 在HashMap中,这也成为了当key值相同时,为什么Value会替换成最新的value的原因
e.value = value;
afterNodeAccess(e);
// 表示添加的这个元素已经存在,并且返回改元素。
return oldValue;
}
}
++modCount;
// threshold 是在resize()函数里面设置过的,就是0.75的hash数组的空间,一旦超过这个空间就对Hash数组扩容,防止多个线程大量范围的时候,发生阻塞
// 每加入一个结点size就会++ ,也就是说,不需要占满12个数组位置,只需要填入12个元素,就会触发数组扩容。
if (++size > threshold)
resize();
// 在这个类里,这个方法是一个空方法
afterNodeInsertion(evict);
// 返回空代表添加的时候没有这个对象
return null;
}
treeifyBin:函数
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果容量不够,扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 如果容量足够,将链表树化
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
注意: 在自定义类的方法需要判断的是否相同的时候,需要同时重写hashCode和equals方法.
- hashCode 方法保证,元素相同时会计算出同一个hash值
- equals方法,保证元素相同时返true,默认是对比元素的指针地址。
LinkedHashSet
- LinkedHashSet是HashSet的子类,LinkedHashSet本质是HashSet的增强子类,目的就是为了记录元素的存储次序。
- LinkedHashSet的底层是一个LinkedHashMap,底层维护了一个
数组+双向链表(散列链表) - LinkedHashSet 根据元素的HashCode值来决定元素的存储位置,同时用链表维护元素的次序,使得元素看起来是以插入顺序保存的
- 不允许添加重复的元素(Set 共性)
LinkedHashSet和HashSet的异同
- 相同点:
- 都是数组 + 链表的存储结构
- 都有Set 的特性
- 查找的时候都利用HashCode 快速定位。
- 不同点:
- LinkedHashSet 的链表顺序是按照元素插入顺序,而HashSet是按照同一个数组位置的插入顺序
- LinkedHashSet读数据是按照元素插入顺序来读取,HashSet不是
- HashSet存放的是Node[]类型的。LinkedHashSet 存放的是 Entry类型的,继承了Node,因为双向链表需要保存一个before after。