1.ArrayList源码分析 1.1ArrayList概述 ArrayList
的底层是一个基于数组实现的动态数组,其能够根据扩容机制能够实现容量的动态增长。
1.2类声明 ArrayList类的声明如下:
public class ArrayList < E > extends AbstractList < E >
implements List < E > , RandomAccess , Cloneable , java. io. Serializable
{
. . . .
}
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
RandomAccess
是一个标志接口(Marker),支持快速随机访问(通过元素序号快速获取元素对象 —— get(int index)
)
Cloneable
:实现浅克隆(clone()
)
java.io.Serializable
:ArrayList 支持序列化
继承树结构如下:
1.3属性 ArrayList属性如下:
public class ArrayList < E > extends AbstractList < E >
implements List < E > , RandomAccess , Cloneable , java. io. Serializable
{
private static final long serialVersionUID = 8683452581122892189L ;
private static final int DEFAULT_CAPACITY = 10 ;
private static final Object [ ] EMPTY_ELEMENTDATA = { } ;
private static final Object [ ] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = { } ;
transient Object [ ] elementData;
private int size;
private static final int MAX_ARRAY_SIZE = Integer . MAX_VALUE - 8 ;
1.4构造函数 ArrayList 提供了 1 个无参构造和 2 个带参构造来初始化 ArrayList ,我们在创建 ArrayList 时,经常使用无参构造的方式,其本质就是初始化了一个空数组,直到向数组内真的添加元素的时候才会真的去分配容量。例如:向数组中添加第一个元素,数组容量扩充为 10。这样做可以延迟数组的创建,节省内存空间。
补充:JDK1.7 无参构造初始化 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组elementData。
含有int类型参数的构造函数
如果传入参数也就是初始容量大于0,则新建一个初始容量大小的数组
如果传入的参数初始容量等于0,将 EMPTY_ELEMENTDATA
赋值给 elementData
如果传入的参数初始容量小于0,将抛出异常
public ArrayList ( int initialCapacity) {
if ( initialCapacity > 0 ) {
this . elementData = new Object [ initialCapacity] ;
} else if ( initialCapacity == 0 ) {
this . elementData = EMPTY_ELEMENTDATA ;
} else {
throw new IllegalArgumentException ( "Illegal Capacity: " +
initialCapacity) ;
}
}
无参构造
如果没有传入参数,则使用默认无参构建方法创建 ArrayList 对象。elementData 是一个大小为 0 的空数组
public ArrayList ( ) {
this . elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA ;
}
含有Collection<? extends E>
类型的参数的构造函数
表示传入的集合中的元素只能是 E
及其子类的对象
将参数中的集合转换为数组,赋值给 elementData
给 size 进行赋值,size 代表集合元素数量。判断参数是否为空,如果数组的大小不等于 0,则进一步判断是否转化为 Object 类型的数组,如果不是,则进行复制,并赋值给elementData
否则,设置元素数组为空数组
public ArrayList ( Collection < ? extends E > c) {
elementData = c. toArray ( ) ;
if ( ( size = elementData. length) != 0 ) {
if ( elementData. getClass ( ) != Object [ ] . class )
elementData = Arrays . copyOf ( elementData, size, Object [ ] . class ) ;
} else {
this . elementData = EMPTY_ELEMENTDATA ;
}
}
1.5最小实例化容量方法
将 ArrayList
实例的容量调整为列表的当前大小,作用是减少 ArrayList
实例占用的内存空间,使其仅占用与当前元素数量相当的空间。
public void trimToSize ( ) {
modCount++ ;
if ( size < elementData. length) {
elementData = ( size == 0 )
? EMPTY_ELEMENTDATA
: Arrays . copyOf ( elementData, size) ;
}
}
1.6常用方法解析
public int size ( ) {
return size;
}
public boolean isEmpty ( ) {
return size == 0 ;
}
public boolean contains ( Object o) {
return indexOf ( o) >= 0 ;
}
public int indexOf ( Object o) {
if ( o == null ) {
for ( int i = 0 ; i < size; i++ )
if ( elementData[ i] == null )
return i;
} else {
for ( int i = 0 ; i < size; i++ )
if ( o. equals ( elementData[ i] ) )
return i;
}
return - 1 ;
}
public int lastIndexOf ( Object o) {
if ( o == null ) {
for ( int i = size- 1 ; i >= 0 ; i-- )
if ( elementData[ i] == null )
return i;
} else {
for ( int i = size- 1 ; i >= 0 ; i-- )
if ( o. equals ( elementData[ i] ) )
return i;
}
return - 1 ;
}
public Object clone ( ) {
try {
ArrayList < ? > v = ( ArrayList < ? > ) super . clone ( ) ;
v. elementData = Arrays . copyOf ( elementData, size) ;
v. modCount = 0 ;
return v;
} catch ( CloneNotSupportedException e) {
throw new InternalError ( e) ;
}
}
public Object [ ] toArray ( ) {
return Arrays . copyOf ( elementData, size) ;
}
@SuppressWarnings ( "unchecked" )
public < T > T [ ] toArray ( T [ ] a) {
if ( a. length < size)
return ( T [ ] ) Arrays . copyOf ( elementData, size, a. getClass ( ) ) ;
System . arraycopy ( elementData, 0 , a, 0 , size) ;
if ( a. length > size)
a[ size] = null ;
return a;
}
@SuppressWarnings ( "unchecked" )
E elementData ( int index) {
return ( E ) elementData[ index] ;
}
public E get ( int index) {
rangeCheck ( index) ;
return elementData ( index) ;
}
public E set ( int index, E element) {
rangeCheck ( index) ;
E oldValue = elementData ( index) ;
elementData[ index] = element;
return oldValue;
}
public boolean add ( E e) {
ensureCapacityInternal ( size + 1 ) ;
elementData[ size++ ] = e;
return true ;
}
public void add ( int index, E element) {
rangeCheckForAdd ( index) ;
ensureCapacityInternal ( size + 1 ) ;
System . arraycopy ( elementData, index, elementData, index + 1 ,
size - index) ;
elementData[ index] = element;
size++ ;
}
public E remove ( int index) {
rangeCheck ( index) ;
modCount++ ;
E oldValue = elementData ( index) ;
int numMoved = size - index - 1 ;
if ( numMoved > 0 )
System . arraycopy ( elementData, index+ 1 , elementData, index,
numMoved) ;
elementData[ -- size] = null ;
return oldValue;
}
public boolean remove ( Object o) {
if ( o == null ) {
for ( int index = 0 ; index < size; index++ )
if ( elementData[ index] == null ) {
fastRemove ( index) ;
return true ;
}
} else {
for ( int index = 0 ; index < size; index++ )
if ( o. equals ( elementData[ index] ) ) {
fastRemove ( index) ;
return true ;
}
}
return false ;
}
private void fastRemove ( int index) {
modCount++ ;
int numMoved = size - index - 1 ;
if ( numMoved > 0 )
System . arraycopy ( elementData, index+ 1 , elementData, index,
numMoved) ;
elementData[ -- size] = null ;
}
public void clear ( ) {
modCount++ ;
for ( int i = 0 ; i < size; i++ )
elementData[ i] = null ;
size = 0 ;
}
public boolean addAll ( Collection < ? extends E > c) {
Object [ ] a = c. toArray ( ) ;
int numNew = a. length;
ensureCapacityInternal ( size + numNew) ;
System . arraycopy ( a, 0 , elementData, size, numNew) ;
size += numNew;
return numNew != 0 ;
}
public boolean addAll ( int index, Collection < ? extends E > c) {
rangeCheckForAdd ( index) ;
Object [ ] a = c. toArray ( ) ;
int numNew = a. length;
ensureCapacityInternal ( size + numNew) ;
int numMoved = size - index;
if ( numMoved > 0 )
System . arraycopy ( elementData, index, elementData, index + numNew,
numMoved) ;
System . arraycopy ( a, 0 , elementData, index, numNew) ;
size += numNew;
return numNew != 0 ;
}
protected void removeRange ( int fromIndex, int toIndex) {
modCount++ ;
int numMoved = size - toIndex;
System . arraycopy ( elementData, toIndex, elementData, fromIndex,
numMoved) ;
int newSize = size - ( toIndex- fromIndex) ;
for ( int i = newSize; i < size; i++ ) {
elementData[ i] = null ;
}
size = newSize;
}
private void rangeCheck ( int index) {
if ( index >= size)
throw new IndexOutOfBoundsException ( outOfBoundsMsg ( index) ) ;
}
private void rangeCheckForAdd ( int index) {
if ( index > size || index < 0 )
throw new IndexOutOfBoundsException ( outOfBoundsMsg ( index) ) ;
}
private String outOfBoundsMsg ( int index) {
return "Index: " + index+ ", Size: " + size;
}
public boolean removeAll ( Collection < ? > c) {
Objects . requireNonNull ( c) ;
return batchRemove ( c, false ) ;
}
public boolean retainAll ( Collection < ? > c) {
Objects . requireNonNull ( c) ;
return batchRemove ( c, true ) ;
}
private boolean batchRemove ( Collection < ? > c, boolean complement) {
final Object [ ] elementData = this . elementData;
int r = 0 , w = 0 ;
boolean modified = false ;
try {
for ( ; r < size; r++ )
if ( c. contains ( elementData[ r] ) == complement)
elementData[ w++ ] = elementData[ r] ;
} finally {
if ( r != size) {
System . arraycopy ( elementData, r,
elementData, w,
size - r) ;
w += size - r;
}
if ( w != size) {
for ( int i = w; i < size; i++ )
elementData[ i] = null ;
modCount += size - w;
size = w;
modified = true ;
}
}
return modified;
}
public ListIterator < E > listIterator ( int index) {
if ( index < 0 || index > size)
throw new IndexOutOfBoundsException ( "Index: " + index) ;
return new ListItr ( index) ;
}
public ListIterator < E > listIterator ( ) {
return new ListItr ( 0 ) ;
}
public Iterator < E > iterator ( ) {
return new Itr ( ) ;
}
private class Itr implements Iterator < E > {
int cursor;
int lastRet = - 1 ;
int expectedModCount = modCount;
Itr ( ) { }
public boolean hasNext ( ) {
return cursor != size;
}
@SuppressWarnings ( "unchecked" )
public E next ( ) {
checkForComodification ( ) ;
int i = cursor;
if ( i >= size)
throw new NoSuchElementException ( ) ;
Object [ ] elementData = ArrayList . this . elementData;
if ( i >= elementData. length)
throw new ConcurrentModificationException ( ) ;
cursor = i + 1 ;
return ( E ) elementData[ lastRet = i] ;
}
public void remove ( ) {
if ( lastRet < 0 )
throw new IllegalStateException ( ) ;
checkForComodification ( ) ;
try {
ArrayList . this . remove ( lastRet) ;
cursor = lastRet;
lastRet = - 1 ;
expectedModCount = modCount;
} catch ( IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException ( ) ;
}
}
final void checkForComodification ( ) {
if ( modCount != expectedModCount)
throw new ConcurrentModificationException ( ) ;
}
}
1.7扩容原理 按照以下代码进行分析:
public class ArrrayListDemo {
public static void main ( String [ ] args) {
ArrayList < Integer > list = new ArrayList < > ( ) ;
list. add ( 1 ) ;
list. add ( 2 ) ;
list. add ( 3 ) ;
list. add ( 4 ) ;
list. add ( 5 ) ;
}
}
首先创建ArrayList对象实例,进入构造器方法
查看常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,即默认创建的是一个空数组。elementData
就是Object类型的数组。
public ArrayList ( ) {
this . elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA ;
}
private static final Object [ ] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = { } ;
跳出构造器方法,执行add()
方法
首先调用ensureCapacityInternal()
方法用于确保内部容量。
public boolean add ( E e) {
ensureCapacityInternal ( size + 1 ) ;
elementData[ size++ ] = e;
return true ;
}
调用ensureCapacityInternal()
方法
调用calculateCapacity(elementData, minCapacity)
方法
参数minCapacity
此时为1
private void ensureCapacityInternal ( int minCapacity) {
ensureExplicitCapacity ( calculateCapacity ( elementData, minCapacity) ) ;
}
调用calculateCapacity()
方法,求得最小容量
传递的参数为elementData
(此时为空数组)和minCapacity
(此时为1)。
此时elementData
就是等于空数组,同时DEFAULT_CAPACITY(10)
>minCapacity(1)
,所以返回10。
private static int calculateCapacity ( Object [ ] elementData, int minCapacity) {
if ( elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ) {
return Math . max ( DEFAULT_CAPACITY , minCapacity) ;
}
return minCapacity;
}
再次回到ensureCapacityInternal
方法
private void ensureCapacityInternal ( int minCapacity) {
ensureExplicitCapacity ( calculateCapacity ( elementData, minCapacity) ) ;
}
调用ensureExplicitCapacity
方法,判断添加元素的时候是否需要对当前数组进行扩容
此时minCapacity
为10,minCapacity - elementData.length
= 10
private void ensureExplicitCapacity ( int minCapacity) {
modCount++ ;
if ( minCapacity - elementData. length > 0 )
grow ( minCapacity) ;
}
此方法的核心就是 if 判断这个数组需不需要扩容,可以分为三种情况
add 第 1 个元素时:此时数组还只是一个被初始化过的空数组,minCapacity 经过 calculateCapacity
计算会返回 DEFAULT_CAPACITY 的默认值 10,而 elementData.length 也自然是 0,所以 minCapacity - elementData.length > 0 是成立的,直接进入 grow(minCapacity);
开始扩容。
add 第 2 到 10 个元素的时候(以 2 举例):此时 minCapacity = size + 1 = 1 + 1 = 2 ,而 elementData.length 已经在添加第 1 个元素后等于 10 了。所以 minCapacity - elementData.length > 0 就不成立了,所以不会进入 grow(minCapacity);
,也不会扩容
add 第 11 个元素的时候,minCapacity 变成了 11,比 10 还要大,所以又一次进去扩容了
调用grow()
方法进行扩容
进行扩容
如果扩容后的长度依然小于所需最小容量,则直接将所需最小容量当作新容量
如果新容量大于了所规定的最大容量,调用hugeCapacity()
方法
最后调用 Arrays.copyOf() 方法,用来复制原数组,将 elementData 赋值为得到的新数组
private void grow ( int minCapacity) {
int oldCapacity = elementData. length;
int newCapacity = oldCapacity + ( oldCapacity >> 1 ) ;
if ( newCapacity - minCapacity < 0 )
newCapacity = minCapacity;
if ( newCapacity - MAX_ARRAY_SIZE > 0 )
newCapacity = hugeCapacity ( minCapacity) ;
elementData = Arrays . copyOf ( elementData, newCapacity) ;
}
hugeCapacity(int minCapacity) 方法,用于处理 minCapacity
超出 MAX_ARRAY_SIZE
的情况
private static int hugeCapacity ( int minCapacity) {
if ( minCapacity < 0 )
throw new OutOfMemoryError ( ) ;
return ( minCapacity > MAX_ARRAY_SIZE ) ?
Integer . MAX_VALUE :
MAX_ARRAY_SIZE ;
}
System.arraycopy()
和 Arrays.copyOf()
方法
阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)
、toArray()
等方法中都用到了该方法!
System.arraycopy()
方法
源码:
public static native void arraycopy ( Object src, int srcPos,
Object dest, int destPos,
int length) ;
场景:
public void add ( int index, E element) {
rangeCheckForAdd ( index) ;
ensureCapacityInternal ( size + 1 ) ;
System . arraycopy ( elementData, index, elementData, index + 1 , size - index) ;
elementData[ index] = element;
size++ ;
}
Arrays.copyOf()
方法
源码:
public static int [ ] copyOf ( int [ ] original, int newLength) {
int [ ] copy = new int [ newLength] ;
System . arraycopy ( original, 0 , copy, 0 ,
Math . min ( original. length, newLength) ) ;
return copy;
}
场景:
public Object [ ] toArray ( ) {
return Arrays . copyOf ( elementData, size) ;
}
回到add()
方法
此时elementData
就是一个长度为10的空数组。
elementData[size++] = e;
将元素存储在索引为0的位置,并将size
加1。其中size是用于记录元素个数。
public boolean add ( E e) {
ensureCapacityInternal ( size + 1 ) ;
elementData[ size++ ] = e;
return true ;
}
2.LinkList源码分析 2.1LinkedList概述 LinkedList
底层是基于双向链表 实现的。其中的元素在逻辑上是连续的,但是在物理结构上不一定连续。
在 LinkedList
的内部,有一个名为 Node
的内部类用来表示链表中的节点。每个节点都包含一个元素和两个指向前驱节点 和后继节点 的指针。当我们创建一个 LinkedList
对象时,它会初始化一个空的链表。
当我们向 LinkedList
中添加元素时,它会创建一个新的节点,并将其插入到链表中。由于链表是动态分配内存的,因此不需要进行扩容操作。
2.2类声明 public class LinkedList < E >
extends AbstractSequentialList < E >
implements List < E > , Deque < E > , Cloneable , java. io. Serializable
{
. . .
}
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
Deque
:继承自 Queue
接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。
Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
结构树如下:
2.3属性 public class LinkedList < E >
extends AbstractSequentialList < E >
implements List < E > , Deque < E > , Cloneable , java. io. Serializable
{
transient int size = 0 ;
transient Node < E > first;
transient Node < E > last;
}
从以上源码可以看出,LinkedList
的属性比较少,分别是:
size :双向链表的节点个数
first :双向链表指向头节点的指针
last : 双向链表指向尾节点的指针
注意:first 和 last 是由引用类型 Node 连接的,这是它的一个内部类。
2.4内部类Node解析 LinkedList 是通过双向链表实现的,而双向链表就是通过 Node 类来实现的。
Node 类中通过 item 变量存储当前元素
通过 next 变量指向当前节点的下一个节点
通过 prev 变量指向当前节点的上一个节点
private static class Node < E > {
E item;
Node < E > next;
Node < E > prev;
Node ( Node < E > prev, E element, Node < E > next) {
this . item = element;
this . next = next;
this . prev = prev;
}
}
2.5构造方法 LinkedList
中有一个无参构造函数和一个有参构造函数。
public LinkedList ( ) {
}
public LinkedList ( Collection < ? extends E > c) {
this ( ) ;
addAll ( c) ;
}
2.6常用方法解析 2.6.1插入节点 LinkedList
除了实现了 List
接口相关方法,还实现了 Deque
接口的很多方法,所以我们有很多种方式插入元素。
在 LinkedList
中,当我们向链表中添加一个元素时,它会创建一个新的节点来存储这个元素。这个过程是通过调用 LinkedList
类中的 linkLast()
方法或 linkBefore()
方法实现的。
例如,当我们调用 add()
方法在链表末尾添加一个元素时,它会调用 linkLast()
方法。在这个方法中,首先会创建一个新的节点,并将其元素设置为要添加的元素。然后,它会将新节点的前驱节点设置为原链表的最后一个节点,并将原链表最后一个节点的后继节点设置为新节点。最后,它会更新链表的大小和修改次数。
add(E e)
:用于在 LinkedList
的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)。
add(int index, E element)
:用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
案例如下:
add(E e) 方法,将指定的元素追加到此列表的末尾
public boolean add ( E e) {
linkLast ( e) ;
return true ;
}
其中,调用的 linkLast() 方法,设置元素 e 为最后一个元素:
获取链表的最后一个节点
创建一个新节点
使新的一个节点为最后一个节点
如果最后一个节点为 null,则表示链表为空,则将 newNode 赋值给 first 节点
否则尾节点的 last 指向 newNode
void linkLast ( E e) {
final Node < E > l = last;
final Node < E > newNode = new Node < > ( l, e, null ) ;
last = newNode;
if ( l == null )
first = newNode;
else
l. next = newNode;
size++ ;
modCount++ ;
}
public void add ( int index, E element) {
checkPositionIndex ( index) ;
if ( index == size)
linkLast ( element) ;
else
linkBefore ( element, node ( index) ) ;
}
调用checkPositionIndex
方法判断是否是现有元素的索引
private void checkPositionIndex ( int index) {
if ( ! isPositionIndex ( index) )
throw new IndexOutOfBoundsException ( outOfBoundsMsg ( index) ) ;
}
private boolean isPositionIndex ( int index) {
return index >= 0 && index <= size;
}
如果是在非null节点之前节点,则调用linkBefore()
方法,在非 null 节点 succ 之前插入元素 e
void linkBefore ( E e, Node < E > succ) {
final Node < E > pred = succ. prev;
final Node < E > newNode = new Node < > ( pred, e, succ) ;
succ. prev = newNode;
if ( pred == null )
first = newNode;
else
pred. next = newNode;
size++ ;
modCount++ ;
}
2.6.2删除节点 LinkedList
删除元素相关的方法一共有 5 个:
removeFirst()
:删除并返回链表的第一个元素。
removeLast()
:删除并返回链表的最后一个元素。
remove(E e)
:删除链表中首次出现的指定元素,如果不存在该元素则返回 false。
remove(int index)
:删除指定索引处的元素,并返回该元素的值。
void clear()
:移除此链表中的所有元素。
remove()
方法,删除头节点
在remove()方法中调用的是removeFirst
方法,即删除first节点
public E remove ( ) {
return removeFirst ( ) ;
}
获取到头节点first,如果头节点为空则抛出异常,否则调用unlinkFirst()
方法
public E removeFirst ( ) {
final Node < E > f = first;
if ( f == null )
throw new NoSuchElementException ( ) ;
return unlinkFirst ( f) ;
}
在unlinkFirst()
方法中接收first节点作为参数
private E unlinkFirst ( Node < E > f) {
final E element = f. item;
final Node < E > next = f. next;
f. item = null ;
f. next = null ;
first = next;
if ( next == null )
last = null ;
else
next. prev = null ;
size-- ;
modCount++ ;
return element;
}
remove(int index)
方法,删除指定位置的元素
检查索引 index 的位置,调用 node 方法获取节点,接着调用 unlink(E e) 方法:
public E remove ( int index) {
checkElementIndex ( index) ;
return unlink ( node ( index) ) ;
}
nulink(Node x)方法
E unlink ( Node < E > x) {
final E element = x. item;
final Node < E > next = x. next;
final Node < E > prev = x. prev;
if ( prev == null ) {
first = next;
} else {
prev. next = next;
x. prev = null ;
}
if ( next == null ) {
last = prev;
} else {
next. prev = prev;
x. next = null ;
}
x. item = null ;
size-- ;
modCount++ ;
return element;
}
unlink()
方法的逻辑如下:
首先获取待删除节点 x 的前驱和后继节点;
判断待删除节点是否为头节点或尾节点:
如果 x 是头节点,则将 first 指向 x 的后继节点 next。
如果 x 是尾节点,则将 last 指向 x 的前驱节点 prev。
如果 x 不是头节点也不是尾节点,执行下一步操作
将删除节点的前驱节点的next指向删除节点的后继节点
将删除元素的后继节点的pre指向删除节点的前驱节点
2.6.3修改节点 set(int index, E element)
方法,将指定下标处的元素修改成指定值
public E set ( int index, E element) {
checkElementIndex ( index) ;
Node < E > x = node ( index) ;
E oldVal = x. item;
x. item = element;
return oldVal;
}
2.6.4获取节点 LinkedList
获取元素相关的方法一共有 3 个:
getFirst()
:获取链表的第一个元素。
getLast()
:获取链表的最后一个元素。
get(int index)
:获取链表指定位置的元素。
get(int index)
返回此列表中指定位置的元素
public E get ( int index) {
checkElementIndex ( index) ;
return node ( index) . item;
}
在此调用了node() 方法:
该方法通过比较索引值与链表 size 的一半大小来确定从链表头还是尾开始遍历。如果索引值小于 size 的一半,就从链表头开始遍历,反之从链表尾开始遍历。这样可以在较短的时间内找到目标节点,充分利用了双向链表的特性来提高效率。
Node < E > node ( int index) {
if ( index < ( size >> 1 ) ) {
Node < E > x = first;
for ( int i = 0 ; i < index; i++ )
x = x. next;
return x;
} else {
Node < E > x = last;
for ( int i = size - 1 ; i > index; i-- )
x = x. prev;
return x;
}
}
2.6.5遍历链表 遍历链表在2.8章节详细说明
2.7双端队列操作方法的源码剖析
offerFirst(E e)
方法,将元素添加到首部
public boolean offerFirst ( E e) {
addFirst ( e) ;
return true ;
}
offerLast(E e)
方法,将元素添加到尾部
public boolean offerLast ( E e) {
addLast ( e) ;
return true ;
}
peekFirst()
方法,获取此集合列表的第一个元素值
public E peekFirst ( ) {
final Node < E > f = first;
return ( f == null ) ? null : f. item;
}
peekLast()
方法,获取此集合列表的最后一个元素值
public E peekLast ( ) {
final Node < E > l = last;
return ( l == null ) ? null : l. item;
}
pollFirst()
方法,删除此集合列表的第一个元素,如果为 null,则会返回 null
public E pollFirst ( ) {
final Node < E > f = first;
return ( f == null ) ? null : unlinkFirst ( f) ;
}
pollLast()
方法 ,删除此集合列表的最后一个元素,如果为 null 会返回 null
public E pollLast ( ) {
final Node < E > l = last;
return ( l == null ) ? null : unlinkLast ( l) ;
}
push(E e)
方法,将元素添加到此集合列表的首部
public void push ( E e) {
addFirst ( e) ;
}
pop()
方法,删除并返回此集合列表的第一个元素,如果为null会抛出异常
public E pop ( ) {
return removeFirst ( ) ;
}
removeFirstOccurrence(Object o)
方法,删除集合中元素值等于o的第一个元素值
public boolean removeFirstOccurrence ( Object o) {
return remove ( o) ;
}
注 :removeFirstOccurrence() 和 remove 方法是一样的,它的内部调用了 remove 方法
removeLastOccurrence(Object o)
方法,删除集合中元素值等于o的最后一个元素值
public boolean removeLastOccurrence ( Object o) {
if ( o == null ) {
for ( Node < E > x = last; x != null ; x = x. prev) {
if ( x. item == null ) {
unlink ( x) ;
return true ;
}
}
} else {
for ( Node < E > x = last; x != null ; x = x. prev) {
if ( o. equals ( x. item) ) {
unlink ( x) ;
return true ;
}
}
}
return false ;
}
2.8LinkedList迭代器 推荐使用for-each
(增强for循环) 循环来遍历 LinkedList
中的元素, for-each
循环最终会转换成迭代器形式。
LinkedList < String > list = new LinkedList < > ( ) ;
list. add ( "apple" ) ;
list. add ( "banana" ) ;
list. add ( "pear" ) ;
for ( String fruit : list) {
System . out. println ( fruit) ;
}
LinkedList
的遍历的核心就是它的迭代器的实现。
private class ListItr implements ListIterator < E > {
private Node < E > lastReturned;
private Node < E > next;
private int nextIndex;
private int expectedModCount = modCount;
. . . .
}
下面我们对迭代器 ListItr
中的核心方法进行详细介绍。
我们先来看下从头到尾方向的迭代:
public boolean hasNext ( ) {
return nextIndex < size;
}
public E next ( ) {
checkForComodification ( ) ;
if ( ! hasNext ( ) )
throw new NoSuchElementException ( ) ;
lastReturned = next;
next = next. next;
nextIndex++ ;
return lastReturned. item;
}
再来看一下从尾到头方向的迭代:
public boolean hasPrevious ( ) {
return nextIndex > 0 ;
}
public E previous ( ) {
checkForComodification ( ) ;
if ( ! hasPrevious ( ) )
throw new NoSuchElementException ( ) ;
lastReturned = next = ( next == null ) ? last : next. prev;
nextIndex-- ;
return lastReturned. item;
}
3.Vector源码分析 3.1Vector概述 Vector
是一个基于数组实现的动态数组,它和ArrayList
一样能够根据扩容机制能够实现容量的动态增长。
3.2类声明 Vector类声明如下 :
public class Vector < E >
extends AbstractList < E >
implements List < E > , RandomAccess , Cloneable , java. io. Serializable
{
. . . .
}
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
RandomAccess
是一个标志接口(Marker),支持快速随机访问(通过元素序号快速获取元素对象 —— get(int index)
)
Cloneable
:实现浅克隆(clone()
)
java.io.Serializable
:ArrayList 支持序列化
结构树如下:
3.3构造方法
调用无参构造
在无参构造中又调用本类当中的有参构造Vector(int initialCapacity)
默认初始化容量为10
在构造函数Vector(int initialCapacity)
又调用Vector(int initialCapacity, int capacityIncrement)
方法,capacityIncrement默认为0
public Vector ( int initialCapacity, int capacityIncrement) {
super ( ) ;
if ( initialCapacity < 0 )
throw new IllegalArgumentException ( "Illegal Capacity: " +
initialCapacity) ;
this . elementData = new Object [ initialCapacity] ;
this . capacityIncrement = capacityIncrement;
}
public Vector ( int initialCapacity) {
this ( initialCapacity, 0 ) ;
}
public Vector ( ) {
this ( 10 ) ;
}
调用有参构造Vector(int initialCapacity, int capacityIncrement)
需要传入两个int类型的参数,分别是数组的初始化长度和扩容的增量
总结:
Vector默认的扩容机制是按两倍扩容。这意味着,如果创建Vector对象时没有指定capacityIncrement
,则每次扩容时,新容量等于原容量的两倍。
如果创建Vector对象时指定了capacityIncrement
,则每次扩容时,新容量等于原容量加上capacityIncrement
。
3.4扩容原理
调用add()
方法
public synchronized boolean add ( E e) {
modCount++ ;
ensureCapacityHelper ( elementCount + 1 ) ;
elementData[ elementCount++ ] = e;
return true ;
}
进入ensureCapacityHelper
方法
判断内容容量,首先添加元素时minCapacity
= 1,elementData.length
= 10。所以不用扩容
private void ensureCapacityHelper ( int minCapacity) {
if ( minCapacity - elementData. length > 0 )
grow ( minCapacity) ;
}
查看扩容方法grow()
假设此时elementData
为10,oldCapacity
就为10,capacityIncrement
为0,则newCapacity
就为原来的oldCapacity
两倍。
private void grow ( int minCapacity) {
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) ;
}
4.HashSet源码分析 4.1HashSet简介 HashSet 是 Java 集合 Set 的一个实现类,Set 是一个接口,其实现类除 HashSet 之外,还有 TreeSet,并继承了 Collection。
4.2类声明 public class HashSet < E >
extends AbstractSet < E >
implements Set < E > , Cloneable , java. io. Serializable
{
. . .
}
实现了 Serializable 接口,表明它支持序列化。
实现了 Cloneable 接口,表明它支持克隆,可以调用超类的 clone()方法进行浅拷贝。
继承了 AbstractSet 抽象类,和 ArrayList 和 LinkedList 一样,在他们的抽象父类中,都提供了 equals() 方法和 hashCode() 方法。它们自身并不实现这两个方法
实现了 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持,不能保证元素的顺序。
1.3属性
private transient HashMap < E , Object > map;
private static final Object PRESENT = new Object ( ) ;
1.4构造函数
public HashSet ( ) {
map = new HashMap < > ( ) ;
}
public HashSet ( Collection < ? extends E > c) {
map = new HashMap < > ( Math . max ( ( int ) ( c. size ( ) / .75f ) + 1 , 16 ) ) ;
addAll ( c) ;
}
public HashSet ( int initialCapacity, float loadFactor) {
map = new HashMap < > ( initialCapacity, loadFactor) ;
}
public HashSet ( int initialCapacity) {
map = new HashMap < > ( initialCapacity) ;
}
HashSet ( int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap < > ( initialCapacity, loadFactor) ;
}
通过构造函数,不难发现,HashSet 的底层是采用 HashMap 实现的。
1.5常用方法解析
添加方法
public boolean add ( E e) {
return map. put ( e, PRESENT ) == null ;
}
往 HashSet 中添加元素,实际是往 Map 成员变量里面添加对应的 key 和 value;Map 中的 key 实际就是要添加的元素,value 是一个对象类型的静态常量;
private static final Object PRESENT = new Object ( ) ;
当第一次往 Map 中添加 key 时,添加成功返回 null,所以当第一次往 HashSet 中添加元素时,会返回 true;由于 HashMap 中的 key 不能重复,所以 HashSet 不能存储重复元素。
删除方法
public boolean remove ( Object o) {
return map. remove ( o) == PRESENT ;
}
当 HashSet 删除一个元素时,实际是操作 Map 删除对应的元素;当删除 Map 中一个不存在的对象是,会返回 null,所以这里当返回 PERSENT 时,说明之前 HashSet 往 Map 中添加过对应的元素,因此,当 remove(o) 返回 true 时,说明之前已经存在该元素,并且成功删除;当返回 false 时,说明之前并没有添加过该对象。
iterator() 方法
public Iterator < E > iterator ( ) {
return map. keySet ( ) . iterator ( ) ;
}
Hashset 获取迭代器实际是获取 Map 的 keySet 的 iterator。
size() 方法
public int size ( ) {
return map. size ( ) ;
}
size() 方法实际是调用 Map 集合中的 size() 方法。
总结:
我们发现 HashSet 的底层源码特别少,主要是因为 HashSet 的方法基本都是借助 HashMap 的方法来实现的。
HashSet 存储的元素对应 HashMap 的 key,因为 HashMap 不能存储重复的 key,所以 HashSet 不能存放重复元素;由于HashMap 的 key 是基于 hashCode 存储对象的,所以 HashSet 中存放的对象也是无序的;HashSet 也没有提供 get 方法,可以通过 Iterator 迭代器获取数据。
5.HashMap源码分析 5.1HashMap概述 HashMap
在jdk1.8之前的存储结构是数组+链表
,到了jdk1.8变成了数组+链表+红黑树
。
在 JDK1.6 和 JDK1.7 中,HashMap 采用数组 + 链表实现,即使用拉链法处理哈希冲突, 同一 hash 值的链表都存储在一个链表里。但是当位于一个链表中的元素较多时,通过 key 值依次查找的效率较低。而 JDK1.8 中,HashMap 采用数组 + 链表 + 红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树 ,在性能上进一步得到提升。
注意:不是说变成了红黑树效率就一定提高了,只有在链表的长度大于8,而且数组的长度大于64的时候才会将链表转化为红黑树,
问题一:什么是红黑树呢?
红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。红黑树的查找效率很高。
问题二:为什么不一下子把整个链表变为红黑树呢?
这个问题的意思是这样的,就是说我们为什么非要等到链表的长度大于等于8的时候,才转变成红黑树?在这里可以从两方面来解释
(1)构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。
(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。
JDK1.7时的数据结构:
JDK1.8时的数据结构:
5.2属性 public class HashMap < K , V > extends AbstractMap < K , V > implements Map < K , V > , Cloneable , Serializable {
private static final long serialVersionUID = 362498820763181265L ;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;
static final int MAXIMUM_CAPACITY = 1 << 30 ;
static final float DEFAULT_LOAD_FACTOR = 0.75f ;
static final int TREEIFY_THRESHOLD = 8 ;
static final int UNTREEIFY_THRESHOLD = 6 ;
static final int MIN_TREEIFY_CAPACITY = 64 ;
transient Node < k, v> [ ] table;
transient Set < map. entry< k, v> > entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
}
table :当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor :
负载因子,默认为 0.75。loadFactor 负载因子是控制数组存放数据的疏密程度 ,loadFactor 越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密。loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75,是官方给出的一个比较好的临界值。给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
threshold :扩容的阈值。threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要对数组扩容。
5.3构造函数 public HashMap ( int initialCapacity, float loadFactor) {
if ( initialCapacity < 0 )
throw new IllegalArgumentException ( "Illegal initial capacity: " +
initialCapacity) ;
if ( initialCapacity > MAXIMUM_CAPACITY )
initialCapacity = MAXIMUM_CAPACITY ;
if ( loadFactor <= 0 || Float . isNaN ( loadFactor) )
throw new IllegalArgumentException ( "Illegal load factor: " +
loadFactor) ;
this . loadFactor = loadFactor;
this . threshold = tableSizeFor ( initialCapacity) ;
}
public HashMap ( int initialCapacity) {
this ( initialCapacity, DEFAULT_LOAD_FACTOR ) ;
}
public HashMap ( ) {
this . loadFactor = DEFAULT_LOAD_FACTOR ;
}
public HashMap ( Map < ? extends K , ? extends V > m) {
this . loadFactor = DEFAULT_LOAD_FACTOR ;
putMapEntries ( m, false ) ;
}
上面 4 个构造方法中
无参构造HashMap()
初始化负载因子
HashMap(int initialCapacity)
可以指定初始容量大小,并调用有参构造HashMap(int initialCapacity, float loadFactor)
HashMap(int initialCapacity, float loadFactor)
:可以指定初始化容量和负载因子,并根据指定的初始容量调用tableSizeFor()
方法计算得阈值threshold
。
即阈值就是大于等于指定初始化容量的最小2次幂
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
* 但是发现并没将`initialCapacity`参数没有被保存下来,那么它怎么参与数组的初始化过程呢?这会在扩容机制方法`resize()`中出现。
* `HashMap(Map<? extends K, ? extends V> m)`:将一个几个列表作为参数传入
## 5.4Node节点
`Node`类是一个静态内部类。这个`Node`类表示哈希表中的一个节点,它既可以是数组中的元素,也可以是链表结构中的元素。
```java
// 继承自 Map.Entry<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
final K key;//键
V value;//值
// 指向下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 重写hashCode()方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写 equals() 方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
5.5树节点TreeNode TreeNode
类是一个静态内部类。表示红黑树中的一个节点,它包含了键值对的信息。
static final class TreeNode < K , V > extends LinkedHashMap. Entry < K , V > {
TreeNode < K , V > parent;
TreeNode < K , V > left;
TreeNode < K , V > right;
TreeNode < K , V > prev;
boolean red;
TreeNode ( int hash, K key, V val, Node < K , V > next) {
super ( hash, key, val, next) ;
}
final TreeNode < K , V > root ( ) {
for ( TreeNode < K , V > r = this , p; ; ) {
if ( ( p = r. parent) == null )
return r;
r = p;
}
}
}
5.6添加元素源码分析 我们在存储一个元素的时候,大多是使用下面的这种方式。
public class Test {
public static void main ( String [ ] args) {
HashMap < String , Integer > map= new HashMap < > ( ) ;
map. put ( "张三" , 20 ) ;
}
}
存储的时候只需要调用put方法即可。根据HashMap
的数据结构,可以画出以下流程图:
put()方法
public V put ( K key, V value) {
return putVal ( hash ( key) , key, value, false , true ) ;
}
put方法其实调用的是putVal
方法。putVal方法有5个参数:
(1)第一个参数hash:调用了hash方法(散列函数)计算hash值(散列值)
static final int hash ( Object key) {
int h;
return ( key == null ) ? 0 : ( h = key. hashCode ( ) ) ^ ( h >>> 16 ) ;
}
散列值的计算 :调用hashCode()函数计算出key的散列值
,并和散列值的无符号右移16位后的结果进行异或运算。
(2)第二个参数key:就是我们传入的key值,也就是例子中的张三
(3)第三个参数value:就是我们传入的value值,也就是例子中的20
(4)第四个参数onlyIfAbsent:如果为true,则仅在键不存在时才插入值
(5)第五个参数evict :如果为false,那么数组就处于创建模式中,所以一般为true。
调用putVal()
方法
下面方法的步骤可以总结为:
首先,检查数组是否为空,如果为空,则调用resize()
方法进行扩容。
然后,根据哈希值计算键值对应该存储在数组的索引位置。
计算方式为(n - 1) & hash
,即将每个元素的key的hash值,与最大索引值进行与操作,如果对应的索引处为空,则直接添加。
为什么HashMap的容量要为2的幂次方呢?
因为在putVal()方法中,计算key对应的散列值是根据当前的HashMap容量减1和hash值做与操作,由于HashMap的容量都是2的幂次方,减1后转换为二进制全为1,由于2进制做与运算时,同为1则为1,所以通过这种算法能够将哈希值均匀地映射到散列表中,使元素分布更加均匀,较少碰撞。
如果该位置为空,则直接在该位置创建一个新节点。
如果该位置不为空,则检查该位置的节点是否与要插入的key相同。如果相同,则更新节点的值。
如果该位置的节点不是要插入的键值对,并且该节点是一个树节点,则调用putTreeVal
方法将键值对插入到红黑树中。如果存在相同的key,就会进行替换。
如果该位置的节点不是要插入的键值对,并且该节点不是一个树节点,则遍历链表,查找是否有与要插入的键值对相同的节点。如果找到了,则更新节点的值;否则,在链表尾部插入一个新节点。如果链表长度超过了阈值,则调用treeifyBin()方法转换为红黑树结构,但是在转换为红黑树之前,会判断当前的HashMap容量是否大于64,如果大于64才会转换为红黑树。
最后,更新哈希表的大小,并判断哈希表中的节点数量是否大于阈值,如果大于阈值,就进行扩容。
final V putVal ( int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node < K , V > [ ] tab; Node < K , V > p; int n, i;
if ( ( tab = table) == null || ( n = tab. length) == 0 )
n = ( tab = resize ( ) ) . length;
if ( ( p = tab[ i = ( n - 1 ) & hash] ) == null )
tab[ i] = newNode ( hash, key, value, null ) ;
else {
Node < K , V > e; K k;
if ( p. hash == hash &&
( ( k = p. key) == key || ( key != null && key. equals ( k) ) ) )
e = p;
else if ( p instanceof TreeNode )
e = ( ( TreeNode < K , V > ) p) . putTreeVal ( this , tab, hash, key, value) ;
else {
for ( int binCount = 0 ; ; ++ binCount) {
if ( ( e = p. next) == null ) {
p. next = newNode ( hash, key, value, null ) ;
if ( binCount >= TREEIFY_THRESHOLD - 1 )
treeifyBin ( tab, hash) ;
break ;
}
if ( e. hash == hash &&
( ( k = e. key) == key || ( key != null && key. equals ( k) ) ) )
break ;
p = e;
}
}
if ( e != null ) {
V oldValue = e. value;
if ( ! onlyIfAbsent || oldValue == null )
e. value = value;
afterNodeAccess ( e) ;
return oldValue;
}
}
++ modCount;
if ( ++ size > threshold)
resize ( ) ;
afterNodeInsertion ( evict) ;
return null ;
}
5.7扩容机制 在 HashMap 中,数组的长度均是 2 的幂次方,阈值大小为数组长度与负载因子的乘积。当 HashMap 中的键值对数量(size
)超过阈值(threshold
)时,进行扩容。
HashMap 的扩容机制与其他变长集合的不太一样,HashMap 按当前数组长度的 2 倍进行扩容,阈值也变为原来的 2 倍(如果计算过程中,阈值溢出归零,则按阈值公式重新计算)。扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去。
流程图如下:
resize
就是进行扩容的方法
下面方法的步骤可以总结为:
首先,获取旧哈希表的信息,包括旧哈希表、旧容量和旧阈值。 这些信息分别存储在oldTab
、oldCap
和oldThr
变量中。
如果容量大于0并且容量大于所规定的最大容量,则将Integer类型的最大值赋值给阈值threshold,如果容量大于0但是不大于所规定的最大值,则将容量和阈值都扩大为原来的两倍。
如果阈值大于0,则将阈值赋值给最新容量。
如果容量和阈值都不大于0,则初始化默认容量,并将默认容量和负载因子的乘积作为阈值。
如果新阈值仍然为0,则根据新容量和负载因子计算新阈值。 具体计算方法是将新容量乘以负载因子,然后取整。
接下来,根据新的容量创建一个新的哈希表,并将旧哈希表中的元素复制到新哈希表中。
如果旧哈希表中的元素是一个单独的节点,则直接将其复制到新哈希表中;
如果旧哈希表中的元素是一个树节点,则调用split
方法将其分裂到新哈希表中;
否则,遍历链表,将链表中的节点分别复制到新哈希表中。
在复制链表中的节点时,需要保持节点在链表中的相对顺序不变 。具体做法是遍历旧链表,将每个节点分别插入到两个新链表中。其中一个新链表用于存储索引位置不变的节点,另一个新链表用于存储索引位置改变的节点。最后,将两个新链表分别插入到新哈希表中。这样做可以保证在扩容后,链表中的元素顺序不变。
最后,返回新哈希表。`
这里把oldThr > 0
情况单独拿出来说一下。在这种情况下,会将 oldThr 赋值给 newCap,等价于newCap = threshold = tableSizeFor(initialCapacity)
。我们在初始化时传入的 initialCapacity 参数经过 threshold 中转最终赋值给了 newCap。这也就解答了前面提的一个疑问:initialCapacity 参数没有被保存下来,那么它怎么参与哈希表的初始化过程的呢?
final Node < K , V > [ ] resize ( ) {
Node < K , V > [ ] oldTab = table;
int oldCap = ( oldTab == null ) ? 0 : oldTab. length;
int oldThr = threshold;
int newCap, newThr = 0 ;
if ( oldCap > 0 ) {
if ( oldCap >= MAXIMUM_CAPACITY ) {
threshold = Integer . MAX_VALUE ;
return oldTab;
}
else if ( ( newCap = oldCap << 1 ) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY )
newThr = oldThr << 1 ;
}
else if ( oldThr > 0 )
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY ;
newThr = ( int ) ( DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY ) ;
}
if ( newThr == 0 ) {
float ft = ( float ) newCap * loadFactor;
newThr = ( newCap < MAXIMUM_CAPACITY && ft < ( float ) MAXIMUM_CAPACITY ?
( int ) ft : Integer . MAX_VALUE ) ;
}
threshold = newThr;
@SuppressWarnings ( { "rawtypes" , "unchecked" } )
Node < K , V > [ ] newTab = ( Node < K , V > [ ] ) new Node [ newCap] ;
table = newTab;
if ( oldTab != null ) {
for ( int j = 0 ; j < oldCap; ++ j) {
Node < K , V > e;
if ( ( e = oldTab[ j] ) != null ) {
oldTab[ j] = null ;
if ( e. next == null )
newTab[ e. hash & ( newCap - 1 ) ] = e;
else if ( e instanceof TreeNode )
( ( TreeNode < K , V > ) e) . split ( this , newTab, j, oldCap) ;
else {
Node < K , V > loHead = null , loTail = null ;
Node < K , V > hiHead = null , hiTail = null ;
Node < K , V > next;
do {
next = e. next;
if ( ( e. hash & oldCap) == 0 ) {
if ( loTail == null )
loHead = e;
else
loTail. next = e;
loTail = e;
}
else {
if ( hiTail == null )
hiHead = e;
else
hiTail. next = e;
hiTail = e;
}
} while ( ( e = next) != null ) ;
if ( loTail != null ) {
loTail. next = null ;
newTab[ j] = loHead;
}
if ( hiTail != null ) {
hiTail. next = null ;
newTab[ j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5.8树化和链化 在 JDK 1.8 及以后的版本中,HashMap 引入了红黑树结构。当桶中链表元素个数大于等于 8 时,链表会转换成树结构;若桶中链表元素个数小于等于 6 时,树结构还原成链表。
中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
为什么要在8时转换为红黑树:
选择8和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。当为8时,hash碰撞发生8次的概率已经降低到了6×10⁻⁸。如果真的碰撞发生了8次,此时的链表性能已经已经很差了,操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。所以,将链表转换为红黑树来提升性能。
5.8.1链表树化 当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()
方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 resize()
方法对数组扩容。
static final int TREEIFY_THRESHOLD = 8 ;
static final int MIN_TREEIFY_CAPACITY = 64 ;
static final class TreeNode < K , V > extends LinkedHashMap. Entry < K , V > {
TreeNode < K , V > parent;
TreeNode < K , V > left;
TreeNode < K , V > right;
TreeNode < K , V > prev;
boolean red;
TreeNode ( int hash, K key, V val, Node < K , V > next) {
super ( hash, key, val, next) ;
}
}
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) ;
}
}
TreeNode < K , V > replacementTreeNode ( Node < K , V > p, Node < K , V > next) {
return new TreeNode < > ( p. hash, p. key, p. value, next) ;
}
树化要满足两个条件:
链表的长度大于了阈值(默认为8)。
哈希表的数组长度大于等于64。
只有当这两个条件都满足时,才会进行树化。如果哈希表的容量小于64,则会通过扩容来减少哈希冲突。
当桶数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。毕竟高碰撞率是因为桶数组容量较小引起的,这个是主因。容量小时,优先扩容可以避免一些列的不必要的树化过程。同时,桶容量较小时,扩容会比较频繁,扩容时需要拆分红黑树并重新映射。所以在桶容量比较小的情况下,将长链表转成红黑树是一件吃力不讨好的事。
回到上面的源码中,我们继续看一下 treeifyBin 方法。该方法主要的作用是将普通链表转成为由 TreeNode 型节点组成的链表,并在最后调用 treeify 是将该链表转为红黑树。TreeNode 继承自 Node 类,所以 TreeNode 仍然包含 next 引用,原链表的节点顺序最终通过 next 引用被保存下来。我们假设树化前,链表结构如下:
HashMap 在设计之初,并没有考虑到以后会引入红黑树进行优化。所以并没有像 TreeMap 那样,要求键类实现 comparable 接口或提供相应的比较器。但由于树化过程需要比较两个键对象的大小,在键类没有实现 comparable 接口的情况下,怎么比较键与键之间的大小了就成了一个棘手的问题。为了解决这个问题,HashMap 是做了三步处理,确保可以比较出两个键的大小,如下:
比较键与键之间 hash 的大小,如果 hash 相同,继续往下比较
检测键类是否实现了 Comparable 接口,如果实现调用 compareTo 方法进行比较
如果仍未比较出大小,就需要进行仲裁了,仲裁方法为 tieBreakOrder
通过上面三次比较,最终就可以比较出孰大孰小。比较出大小后就可以构造红黑树了,最终构造出的红黑树如下:
可以看出,链表转成红黑树后,原链表的顺序仍然会被引用仍被保留了(红黑树的根节点会被移动到链表的第一位),我们仍然可以按遍历链表的方式去遍历上面的红黑树。
5.8.2红黑树拆分 扩容后,普通节点需要重新映射,红黑树节点也不例外。按照一般的思路,我们可以先把红黑树转成链表,之后再重新映射链表即可。这种处理方式是大家比较容易想到的,但这样做会损失一定的效率。不同于上面的处理方式,HashMap 实现的思路则是在将普通链表转成红黑树时,HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。 这样就避免了将红黑树转成链表后再进行映射,无形中提高了效率。
以上就是红黑树拆分的逻辑,下面看一下具体实现吧:
static final int UNTREEIFY_THRESHOLD = 6 ;
final void split ( HashMap < K , V > map, Node < K , V > [ ] tab, int index, int bit) {
TreeNode < K , V > b = this ;
TreeNode < K , V > loHead = null , loTail = null ;
TreeNode < K , V > hiHead = null , hiTail = null ;
int lc = 0 , hc = 0 ;
for ( TreeNode < K , V > e = b, next; e != null ; e = next) {
next = ( TreeNode < K , V > ) e. next;
e. next = null ;
if ( ( e. hash & bit) == 0 ) {
if ( ( e. prev = loTail) == null )
loHead = e;
else
loTail. next = e;
loTail = e;
++ lc;
}
else {
if ( ( e. prev = hiTail) == null )
hiHead = e;
else
hiTail. next = e;
hiTail = e;
++ hc;
}
}
if ( loHead != null ) {
if ( lc <= UNTREEIFY_THRESHOLD )
tab[ index] = loHead. untreeify ( map) ;
else {
tab[ index] = loHead;
if ( hiHead != null )
loHead. treeify ( tab) ;
}
}
if ( hiHead != null ) {
if ( hc <= UNTREEIFY_THRESHOLD )
tab[ index + bit] = hiHead. untreeify ( map) ;
else {
tab[ index + bit] = hiHead;
if ( loHead != null )
hiHead. treeify ( tab) ;
}
}
}
从源码上可以看得出,重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。举个例子说明一下,假设扩容后,重新映射上图的红黑树,映射结果如下:
5.8.3红黑树链化 前面说过,红黑树中仍然保留了原链表节点顺序。有了这个前提,再将红黑树转成链表就简单多了,仅需将 TreeNode 链表转成 Node 类型的链表即可。相关代码如下:
final Node < K , V > untreeify ( HashMap < K , V > map) {
Node < K , V > hd = null , tl = null ;
for ( Node < K , V > q = this ; q != null ; q = q. next) {
Node < K , V > p = map. replacementNode ( q, null ) ;
if ( tl == null )
hd = p;
else
tl. next = p;
tl = p;
}
return hd;
}
Node < K , V > replacementNode ( Node < K , V > p, Node < K , V > next) {
return new Node < > ( p. hash, p. key, p. value, next) ;
}
5.9HashMap产生死循环问题 HashMap的死循环问题会在JDK1.7中出现,主要原因是因为HashMap本身的工作机制和并发操作从而导致死循环的出现。
在JDK1.7当中,HashMap使用的数据结构是数据+链表,同时在插入元素时,采用的头插法插入。在并发场景下对HashMap进行扩容时,假设有两个线程都指向了链表的头节点,当一个线程 A 在扩容还未完成的时候,不巧的是失去了 CPU 时间片,不能获得执行机会,另外一个线程 B 抢先扩容完毕了,并且链表变成了逆序,而线程 A 还在按照顺序操作,造成指针混乱,于是出现死循环。
这个问题在 JDK1.8 中得到了解决。JDK1.8 中的 HashMap 使用了一种新的数据结构——红黑树,它能够有效地避免死循环问题的出现。此外,在 JDK1.8 中,HashMap 在插入元素时采用尾插法插入,这也有助于避免死循环问题的出现。但是在多线程环境建议使用 ConcurrentHashMap 来代替 HashMap,以确保线程安全。
5.10HashMap为什么线程不安全 有之前说的死循环问题,下面说一下数据覆盖问题 :
假设两个线程A、B都在进行put操作,插入的key的散列值是相同的,当线程A计算出散列值后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了。
6.LinkedHashMap源码分析 6.1LinkedHashMap概述 LinkedHashMap
是 Java 提供的一个集合类,它继承自 HashMap
,并在 HashMap
基础上维护一条双向链表,使得具备如下特性:
支持遍历时会按照插入顺序有序进行迭代。
支持按照元素访问顺序排序,适用于封装 LRU(L east R ecently U sed,最近最少使用)缓存工具。
因为内部使用双向链表维护各个节点,所以遍历时的效率和元素个数成正比,相较于和容量成正比的 HashMap 来说,迭代效率会高很多。
6.2类声明 public class LinkedHashMap < K , V >
extends HashMap < K , V >
implements Map < K , V >
{
. . .
}
可以看到,LinkedHashMap就时直接继承了HashMap,其双向链表结构就是节点内部类Entry
当中定义的before
和 after
指针使节点具备双向链表的特性。
6.2LinkedHashMap使用演示 6.2.1插入顺序遍历 如下所示,我们按照顺序往 LinkedHashMap
添加元素然后进行遍历。
HashMap < String , String > map = new LinkedHashMap < > ( ) ;
map. put ( "a" , "2" ) ;
map. put ( "g" , "3" ) ;
map. put ( "r" , "1" ) ;
map. put ( "e" , "23" ) ;
for ( Map. Entry < String , String > entry: map. entrySet ( ) ) {
System . out. println ( entry. getKey ( ) + ":" + entry. getValue ( ) ) ;
}
输出:
可以看出,LinkedHashMap
的迭代顺序是和插入顺序一致的,这一点是 HashMap
所不具备的。
6.2.2访问顺序遍历 LinkedHashMap
定义了排序模式 accessOrder
(boolean 类型,默认为 false),访问顺序则为 true,插入顺序则为 false。
为了实现访问顺序遍历,我们可以使用传入 accessOrder
属性的 LinkedHashMap
构造方法,并将 accessOrder
设置为 true,表示其具备访问有序性。
LinkedHashMap < Integer , String > map = new LinkedHashMap < > ( 16 , 0.75f , true ) ;
map. put ( 1 , "one" ) ;
map. put ( 2 , "two" ) ;
map. put ( 3 , "three" ) ;
map. put ( 4 , "four" ) ;
map. put ( 5 , "five" ) ;
map. get ( 2 ) ;
map. get ( 3 ) ;
for ( Map. Entry < Integer , String > entry : map. entrySet ( ) ) {
System . out. println ( entry. getKey ( ) + " : " + entry. getValue ( ) ) ;
}
输出:
1 : one
4 : four
5 : five
2 : two
3 : three
可以看出,LinkedHashMap
的迭代顺序是和访问顺序一致的。
6.2.3LRU 缓存 从上一个我们可以了解到通过 LinkedHashMap
我们可以封装一个简易版的 LRU(L east R ecently U sed,最近最少使用) 缓存,确保当存放的元素超过容器容量时,将最近最少访问的元素移除。
具体实现思路如下:
继承 LinkedHashMap
;
构造方法中指定 accessOrder
为 true ,这样在访问元素时就会把该元素移动到链表尾部,链表首元素就是最近最少被访问的元素;
重写removeEldestEntry
方法,该方法会返回一个 boolean 值,告知 LinkedHashMap
是否需要移除链表首元素(缓存容量有限)。
public class LRUCache < K , V > extends LinkedHashMap < K , V > {
private final int capacity;
public LRUCache ( int capacity) {
super ( capacity, 0.75f , true ) ;
this . capacity = capacity;
}
@Override
protected boolean removeEldestEntry ( Map. Entry < K , V > eldest) {
return size ( ) > capacity;
}
}
测试代码如下,初始化缓存容量为 2,然后按照次序先后添加 4 个元素。
LRUCache < Integer , String > cache = new LRUCache < > ( 2 ) ;
cache. put ( 1 , "one" ) ;
cache. put ( 2 , "two" ) ;
cache. put ( 3 , "three" ) ;
for ( int i = 0 ; i < 4 ; i++ ) {
System . out. println ( cache. get ( i) ) ;
}
输出:
从输出结果来看,由于缓存容量为 2 ,因此,添加第 3 个元素时,第 1 个元素会被删除。添加第 4 个元素时,第 2 个元素会被删除。
6.3LinkedHashMap的内部类Entry LinkedHashMap
是在 HashMap
的基础上为 bucket 上的每一个节点建立一条双向链表,这就使得转为红黑树的树节点也需要具备双向链表节点的特性,即每一个树节点都需要拥有两个引用存储前驱节点和后继节点的地址,所以对于树节点类 TreeNode
(因为LinkedHashMap
继承了HashMap
,所以LinkedHashMap
当中使用的TreeNode
就是父类HashMap
当中的,所以TreeNode
需要具有双向链表的特点 ) 的设计就是一个比较棘手的问题。
对此我们不妨来看看两者之间节点类的类图,可以看到:
Entry接口时Map接口当中的一个内部接口,HashMap的节点内部类Node
实现了该接口。而LinkedHashMap
的节点内部类 Entry
继承了HashMap当中的节点内部类Node
,并在Node
的基础上添加了before
和 after
指针使节点具备双向链表的特性。
因为LinkedHashMap
的节点内部类 Entry
具有了双向链表的特点,所以HashMap
的树节点 TreeNode
继承了具备双向链表特性的 LinkedHashMap
的 Entry
。
为什么 HashMap
的树节点 TreeNode
要通过 LinkedHashMap
获取双向链表的特性呢?为什么不直接在 Node
上实现前驱和后继指针呢?
第一个问题,我们都知道 LinkedHashMap
是在 HashMap
基础上对节点增加双向指针实现双向链表的特性,所以 LinkedHashMap
内部链表转红黑树时,对应的节点会转为树节点 TreeNode
,为了保证使用 LinkedHashMap
时树节点具备双向链表的特性,所以树节点 TreeNode
需要继承 LinkedHashMap
的 Entry
。
第二个问题,我们直接在 HashMap
的节点 Node
上直接实现前驱和后继指针,然后 TreeNode
直接继承 Node
获取双向链表的特性为什么不行呢?其实这样做也是可以的。只不过这种做法会使得使用 HashMap
时存储键值对的节点类 Node
多了两个没有必要的引用,占用没必要的内存空间。
所以,为了保证 HashMap
底层的节点类 Node
没有多余的引用,又要保证 LinkedHashMap
的节点类 Entry
拥有存储链表的引用,设计者就让 LinkedHashMap
的节点 Entry
去继承 Node 并增加存储前驱后继节点的引用 before
、after
,让需要用到链表特性的节点去实现需要的逻辑。然后树节点 TreeNode
再通过继承 Entry
获取 before
、after
两个指针。
static class Entry < K , V > extends HashMap. Node < K , V > {
Entry < K , V > before, after;
Entry ( int hash, K key, V value, Node < K , V > next) {
super ( hash, key, value, next) ;
}
}
但是这样做,不也使得使用 HashMap
时的 TreeNode
多了两个没有必要的引用吗?这不也是一种空间的浪费吗?
static final class TreeNode < K , V > extends LinkedHashMap. Entry < K , V > {
}
对于这个问题,作者们认为在良好的 hashCode
算法时,HashMap
转红黑树的概率不大。就算转为红黑树变为树节点,也可能会因为移除或者扩容将 TreeNode
变为 Node
,所以 TreeNode
的使用概率不算很大,对于这一点资源空间的浪费是可以接受的。
6.4构造函数 LinkedHashMap
构造方法有 4 个实现也比较简单,直接调用父类即 HashMap
的构造方法完成初始化。
public LinkedHashMap ( ) {
super ( ) ;
accessOrder = false ;
}
public LinkedHashMap ( int initialCapacity) {
super ( initialCapacity) ;
accessOrder = false ;
}
public LinkedHashMap ( int initialCapacity, float loadFactor) {
super ( initialCapacity, loadFactor) ;
accessOrder = false ;
}
public LinkedHashMap ( int initialCapacity,
float loadFactor,
boolean accessOrder) {
super ( initialCapacity, loadFactor) ;
this . accessOrder = accessOrder;
}
我们上面也提到了,默认情况下 accessOrder
为 false,如果我们要让 LinkedHashMap
实现键值对按照访问顺序排序
(即将最近未访问的元素排在链表首部、最近访问的元素移动到链表尾部),需要调用第 4 个构造方法将 accessOrder
设置为 true。
6.5get方法 get
方法是 LinkedHashMap
增删改查操作中唯一一个重写的方法, accessOrder
为 true 的情况下, 它会在元素查询完成之后,将当前访问的元素移到链表的末尾。
public V get ( Object key) {
Node < K , V > e;
if ( ( e = getNode ( hash ( key) , key) ) == null )
return null ;
if ( accessOrder)
afterNodeAccess ( e) ;
return e. value;
}
从源码可以看出,get
的执行步骤非常简单:
调用父类即 HashMap
的 getNode
获取键值对,若为空则直接返回。
判断 accessOrder
是否为 true,若为 true 则说明需要保证 LinkedHashMap
的链表访问有序性,执行步骤 3。
调用 LinkedHashMap
重写的 afterNodeAccess
将当前元素添加到链表末尾。
关键点在于 afterNodeAccess
方法的实现,这个方法负责将元素移动到链表末尾。
void afterNodeAccess ( Node < K , V > e) {
LinkedHashMap. Entry < K , V > last;
if ( accessOrder && ( last = tail) != e) {
LinkedHashMap. Entry < K , V > p =
( LinkedHashMap. Entry < K , V > ) e, b = p. before, a = p. after;
p. after = null ;
if ( b == null )
head = a;
else
b. after = a;
if ( a != null )
a. before = b;
else
last = b;
if ( last == null )
head = p;
else {
p. before = last;
last. after = p;
}
tail = p;
++ modCount;
}
}
从源码可以看出, afterNodeAccess
方法完成了下面这些操作:
如果 accessOrder
为 true 且链表尾部不为当前节点 p,我们则需要将当前节点移到链表尾部。
获取当前节点 p、以及它的前驱节点 b 和后继节点 a。
将当前节点 p 的后继指针设置为 null,使其和后继节点 p 断开联系。
尝试将前驱节点指向后继节点,若前驱节点为空,则说明当前节点 p 就是链表首节点,故直接将后继节点 a 设置为首节点,随后我们再将 p 追加到 a 的末尾。
再尝试让后继节点 a 指向前驱节点 b。
上述操作让前驱节点和后继节点完成关联,并将当前节点 p 独立出来,这一步则是将当前节点 p 追加到链表末端,如果链表末端为空,则说明当前链表只有一个节点 p,所以直接让 head 指向 p 即可。
上述操作已经将 p 成功到达链表末端,最后我们将 tail 指针即指向链表末端的指针指向 p 即可。
7.CopyOnWriteArrayList 源码分析 7.1CopyOnWriteArrayList 简介 在 JDK1.5 之前,如果想要使用并发安全的 List
只能选择 Vector
。而 Vector
是一种老旧的集合,已经被淘汰。Vector
对于增删改查等方法基本都加了 synchronized
,这种方式虽然能够保证同步,但这相当于对整个 Vector
加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。
JDK1.5 引入了 Java.util.concurrent
(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 List
实现就是 CopyOnWriteArrayList
。
7.2Copy-On-Write 思想 CopyOnWriteArrayList
名字中的“Copy-On-Write”即写时复制,简称 COW。
写入时复制(英语:Copy-on-write,简称 COW) 是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
这里再以 CopyOnWriteArrayList
为例介绍:当需要修改( add
,set
、remove
等操作) CopyOnWriteArrayList
的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
可以看出,写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。
不过,写时复制机制依然存在一些缺点,下面列举几点:
内存占用 :每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。
写操作开销 :每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。
数据一致性问题 :修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。
7.3CopyOnWriteArrayList 源码分析 这里以 JDK1.8 为例,分析一下 CopyOnWriteArrayList
的底层核心源码。
CopyOnWriteArrayList
的类定义如下:
public class CopyOnWriteArrayList < E >
extends Object
implements List < E > , RandomAccess , Cloneable , Serializable
{
}
CopyOnWriteArrayList
实现了以下接口:
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
RandomAccess
:这是一个标志接口,表明实现这个接口的 List
集合是支持 快速随机访问 的。
Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
7.3.1构造函数 CopyOnWriteArrayList
中有一个无参构造函数和两个有参构造函数。
public CopyOnWriteArrayList ( ) {
setArray ( new Object [ 0 ] ) ;
}
public CopyOnWriteArrayList ( Collection < ? extends E > c) {
Object [ ] elements;
if ( c. getClass ( ) == CopyOnWriteArrayList . class )
elements = ( ( CopyOnWriteArrayList < ? > ) c) . getArray ( ) ;
else {
elements = c. toArray ( ) ;
if ( elements. getClass ( ) != Object [ ] . class )
elements = Arrays . copyOf ( elements, elements. length, Object [ ] . class ) ;
}
setArray ( elements) ;
}
public CopyOnWriteArrayList ( E [ ] toCopyIn) {
setArray ( Arrays . copyOf ( toCopyIn, toCopyIn. length, Object [ ] . class ) ) ;
}
7.3.2插入元素 CopyOnWriteArrayList
的 add()
方法有三个版本:
add(E e)
:在 CopyOnWriteArrayList
的尾部插入元素。
add(int index, E element)
:在 CopyOnWriteArrayList
的指定位置插入元素。
addIfAbsent(E e)
:如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true。
这里以add(E e)
为例进行介绍:
public boolean add ( E e) {
final ReentrantLock lock = this . lock;
lock. lock ( ) ;
try {
Object [ ] elements = getArray ( ) ;
int len = elements. length;
Object [ ] newElements = Arrays . copyOf ( elements, len + 1 ) ;
newElements[ len] = e;
setArray ( newElements) ;
return true ;
} finally {
lock. unlock ( ) ;
}
}
从上面的源码可以看出:
add
方法内部用到了 ReentrantLock
加锁,保证了同步,避免了多线程写的时候会复制出多个副本出来。锁被修饰保证了锁的内存地址肯定不会被修改,并且,释放锁的逻辑放在 finally
中,可以保证锁能被释放。
CopyOnWriteArrayList
通过复制底层数组的方式实现写操作,即先创建一个新的数组来容纳新添加的元素,然后在新数组中进行写操作,最后将新数组赋值给底层数组的引用,替换掉旧的数组。这也就证明了我们前面说的:CopyOnWriteArrayList
线程安全的核心在于其采用了 写时复制(Copy-On-Write) 的策略。
每次写操作都需要通过 Arrays.copyOf
复制底层数组,时间复杂度是 O(n) 的,且会占用额外的内存空间。因此,CopyOnWriteArrayList
适用于读多写少的场景,在写操作不频繁且内存资源充足的情况下,可以提升系统的性能表现。
CopyOnWriteArrayList
中并没有类似于 ArrayList
的 grow()
方法扩容的操作。由于每次修改都会创建一个新的内部数组副本,因此不需要像 ArrayList
那样进行扩容操作。相反,每次修改都会根据需要创建一个足够大的新数组来容纳所有元素。
7.3.3 读取元素 CopyOnWriteArrayList
的读取操作是基于内部数组 array
并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。
private transient volatile Object [ ] array;
public E get ( int index) {
return get ( getArray ( ) , index) ;
}
final Object [ ] getArray ( ) {
return array;
}
private E get ( Object [ ] a, int index) {
return ( E ) a[ index] ;
}
不过,get
方法是弱一致性的,在某些情况下可能读到旧的元素值。
get(int index)
方法是分两步进行的:
通过getArray()
获取当前数组的引用;
直接从数组中获取下标为 index 的元素。
这个过程并没有加锁,所以在并发环境下可能出现如下情况:
线程 1 调用get(int index)
方法获取值,内部通过getArray()
方法获取到了 array 属性值;
线程 2 调用CopyOnWriteArrayList
的add
、set
、remove
等修改方法时,内部通过setArray
方法修改了array
属性的值;
线程 1 还是从旧的 array
数组中取值。
7.3.4获取列表中元素的个数 public int size ( ) {
return getArray ( ) . length;
}
CopyOnWriteArrayList
中的array
数组每次复制都刚好能够容纳下所有元素,并不像ArrayList
那样会预留一定的空间。因此,CopyOnWriteArrayList
中并没有size
属性CopyOnWriteArrayList
的底层数组的长度就是元素个数,因此size()
方法只要返回数组长度就可以了。
7.3.5删除元素 CopyOnWriteArrayList
删除元素相关的方法一共有 4 个:
remove(int index)
:移除此列表中指定位置上的元素。将任何后续元素向左移动(从它们的索引中减去 1)。
boolean remove(Object o)
:删除此列表中首次出现的指定元素,如果不存在该元素则返回 false。
boolean removeAll(Collection<?> c)
:从此列表中删除指定集合中包含的所有元素。
void clear()
:移除此列表中的所有元素。
这里以remove(int index)
为例进行介绍:
public E remove ( int index) {
final ReentrantLock lock = this . lock;
lock. lock ( ) ;
try {
Object [ ] elements = getArray ( ) ;
int len = elements. length;
E oldValue = get ( elements, index) ;
int numMoved = len - index - 1 ;
if ( numMoved == 0 )
setArray ( Arrays . copyOf ( elements, len - 1 ) ) ;
else {
Object [ ] newElements = new Object [ len - 1 ] ;
System . arraycopy ( elements, 0 , newElements, 0 , index) ;
System . arraycopy ( elements, index + 1 , newElements, index,
numMoved) ;
setArray ( newElements) ;
}
return oldValue;
} finally {
lock. unlock ( ) ;
}
}
7.3.6判断元素是否存在 CopyOnWriteArrayList
提供了两个用于判断指定元素是否在列表中的方法:
contains(Object o)
:判断是否包含指定元素。
containsAll(Collection<?> c)
:判断是否保证指定集合的全部元素。
public boolean contains ( Object o) {
Object [ ] elements = getArray ( ) ;
return indexOf ( o, elements, 0 , elements. length) >= 0 ;
}
public boolean containsAll ( Collection < ? > c) {
Object [ ] elements = getArray ( ) ;
int len = elements. length;
for ( Object e : c) {
if ( indexOf ( e, elements, 0 , len) < 0 )
return false ;
}
return true ;
}
8.ConcurrentHashMap源码分析 8.1概述 ConcurrentHashMap
是 HashMap
的线程安全版本,其内部和 HashMap
一样,也是采用了数组 + 链表 + 红黑树的方式来实现。
如何实现线程的安全性?加锁。但是这个锁应该怎么加呢?在 HashTable
中,是直接在 put
和 get
方法上加上了 synchronized
,但是这么做锁的粒度太大,会非常影响并发性能,所以在 ConcurrentHashMap
中并没有采用这么直接简单粗暴的方法,其内部采用了非常精妙的设计,大大减少了锁的竞争,提升了并发性能。
8.2ConcurrentHashMap数据结构 在JDK1.7中,ConcurrentHashMap
的数据结构是由 Segment
数组(分段锁
)和 HashEntry
数组组成的。每个 Segment
都包含一个 HashEntry
数组,其中每个 HashEntry
都是一个链表。Segment
本身继承自 ReentrantLock
,因此它充当了分段锁的角色。这种设计允许多个线程同时对不同的 Segment
进行写操作,从而提高了并发性能。
Segment
的个数一旦初始化就不能改变 ,默认 Segment
的个数是 16 个,你也可以认为 ConcurrentHashMap
默认支持最多 16 个线程并发。
可以发现 Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,不再是之前的 Segment 数组 + HashEntry 数组 + 链表 ,而是 Node 数组 + 链表 / 红黑树 。当冲突链表达到一定长度时,链表会转换成红黑树。JDK1.8中的 ConcurrentHashMap
使用了更多的 CAS 操作和无锁技术来实现线程安全,而不再依赖分段锁。
8.3源码和问题分析 8.3.1Concurrenthashmap和HashTable实现线程安全的方式 ConcurrentHashMap
和 HashTable
都是线程安全的哈希表实现,但它们实现线程安全的方式不同。
HashTable
使用 synchronized
关键字来保证线程安全。这意味着每次对 HashTable
进行修改操作时,都需要获取一个全局锁。这种方式可以确保线程安全,但在高并发情况下,锁竞争激烈会导致性能下降。
相比之下,ConcurrentHashMap
采用了更加高效的方式来实现线程安全。在 JDK1.7 中,ConcurrentHashMap
采用分段锁技术,在多线程场景下,锁的竞争更加细粒度,可以提供更好的并发性能。在JDK1.8中,ConcurrentHashMap
不再使用 Segment
数组,而是直接使用一个 Node
数组。每个 Node
都是一个链表,当链表长度超过一定阈值时,会转换为红黑树以提高查找效率。JDK1.8中的 ConcurrentHashMap
使用了更多的 CAS 操作和无锁技术来实现线程安全。
CAS(Compare and Swap)是一种无锁算法,它通过比较内存中的值与预期值是否相等来决定是否执行更新操作。这种方式可以避免使用显式锁,从而减少锁竞争带来的开销。
在JDK1.8中的 ConcurrentHashMap
中,CAS 操作被用于实现无锁的读操作和部分写操作。例如,在扩容时,新建一个更大的数组并将旧数组中的元素复制到新数组中时,就使用了 CAS 操作来保证线程安全。
此外,JDK1.8中的 ConcurrentHashMap
还使用了无锁技术来实现部分写操作。例如,在向哈希表中添加元素时,如果目标桶为空,则可以使用无锁技术直接将新元素插入到桶中。
8.3.2如何用 CAS 保证数组初始化的安全 下面就是初始化的方法:
private final Node < K , V > [ ] initTable ( ) {
Node < K , V > [ ] tab; int sc;
while ( ( tab = table) == null || tab. length == 0 ) {
if ( ( sc = sizeCtl) < 0 )
Thread . yield ( ) ;
else if ( U . compareAndSwapInt ( this , SIZECTL , sc, - 1 ) ) {
try {
if ( ( tab = table) == null || tab. length == 0 ) {
int n = ( sc > 0 ) ? sc : DEFAULT_CAPACITY ;
@SuppressWarnings ( "unchecked" )
Node < K , V > [ ] nt = ( Node < K , V > [ ] ) new Node < ? , ? > [ n] ;
table = tab = nt;
sc = n - ( n >>> 2 ) ;
}
} finally {
sizeCtl = sc;
}
break ;
}
}
return tab;
}
这里面有一个非常重要的变量 sizeCtl
,这个变量对理解整个 ConcurrentHashMap
的原理非常重要。
sizeCtl
有四个含义:
sizeCtl<-1
表示有 N-1
个线程正在执行扩容操作,如 -2
就表示有 2-1
个线程正在扩容。
sizeCtl=-1
占位符,表示当前正在初始化数组。
sizeCtl=0
默认状态,表示数组还没有被初始化。
sizeCtl>0
记录下一次需要扩容的大小。
知道了这个变量的含义,上面的方法就好理解了,第二个分支采用了 CAS
操作,因为 SIZECTL
默认为 0
,所以这里如果可以替换成功,则当前线程可以执行初始化操作,CAS
失败,说明其他线程抢先一步把 sizeCtl
改为了 -1
。扩容成功之后会把下一次扩容的阈值赋值给 sc
,即 sizeClt
。
8.3.3put 操作如何保证数组元素的可见性 ConcurrentHashMap
中存储数据采用的 Node
数组是采用了 volatile
来修饰的,但是这只能保证数组的引用在不同线程之间是可用的,并不能保证数组内部的元素在各个线程之间也是可见的,所以这里我们判定某一个下标是否有元素,并不能直接通过下标来访问,那么应该如何访问呢?源码给你答案:
根据 key 计算出 hashcode 。
判断是否需要进行初始化。
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
如果当前位置的 hashcode == MOVED == -1
,则需要进行扩容。
如果都不满足,则利用 synchronized 锁写入数据。
如果数量大于 TREEIFY_THRESHOLD
则要执行树化方法,在 treeifyBin
中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树。
可以看到,这里是通过 tabAt
方法来获取元素,而 tableAt
方法实际上就是一个 CAS
操作。
如果发现当前节点元素为空,也是通过 CAS
操作(casTabAt
)来存储当前元素。
如果当前节点元素不为空,则会使用 synchronized
关键字锁住当前节点,并进行对应的设值操作。
public V put ( K key, V value) {
return putVal ( key, value, false ) ;
}
final V putVal ( K key, V value, boolean onlyIfAbsent) {
if ( key == null || value == null ) throw new NullPointerException ( ) ;
int hash = spread ( key. hashCode ( ) ) ;
int binCount = 0 ;
for ( Node < K , V > [ ] tab = table; ; ) {
Node < K , V > f; int n, i, fh;
if ( tab == null || ( n = tab. length) == 0 )
tab = initTable ( ) ;
else if ( ( f = tabAt ( tab, i = ( n - 1 ) & hash) ) == null ) {
if ( casTabAt ( tab, i, null , new Node < K , V > ( hash, key, value, null ) ) )
break ;
}
else if ( ( fh = f. hash) == MOVED )
tab = helpTransfer ( tab, f) ;
else {
V oldVal = null ;
synchronized ( f) {
if ( tabAt ( tab, i) == f) {
if ( fh >= 0 ) {
binCount = 1 ;
for ( Node < K , V > e = f; ; ++ binCount) {
K ek;
if ( e. hash == hash &&
( ( ek = e. key) == key ||
( ek != null && key. equals ( ek) ) ) ) {
oldVal = e. val;
if ( ! onlyIfAbsent)
e. val = value;
break ;
}
Node < K , V > pred = e;
if ( ( e = e. next) == null ) {
pred. next = new Node < K , V > ( hash, key,
value, null ) ;
break ;
}
}
}
else if ( f instanceof TreeBin ) {
Node < K , V > p;
binCount = 2 ;
if ( ( p = ( ( TreeBin < K , V > ) f) . putTreeVal ( hash, key,
value) ) != null ) {
oldVal = p. val;
if ( ! onlyIfAbsent)
p. val = value;
}
}
}
}
if ( binCount != 0 ) {
if ( binCount >= TREEIFY_THRESHOLD )
treeifyBin ( tab, i) ;
if ( oldVal != null )
return oldVal;
break ;
}
}
}
addCount ( 1L , binCount) ;
return null ;
}
8.3.4为什么ConcurrentHashMap不允许插入空值 在向ConcurrentHashMap当中添加元素时,调用put方法:
public V put ( K key, V value) {
return putVal ( key, value, false ) ;
}
final V putVal ( K key, V value, boolean onlyIfAbsent) {
if ( key == null || value == null ) throw new NullPointerException ( ) ;
. . .
}
即当插入的键值对数据的key或者是value为空时,则直接抛出NPE异常。
为什么不允许插入null值呢?
原因是因为当在ConcurrentHashMap当中插入null值得时候会引起歧义。当在ConcurrentHashMap当中插入null值,那么取值的时候就会产生两个结果:
值没有在集合中,所以返回null
值就是null,所以返回的就是他原本的null值
这样就产生了歧义问题。
为什么HashMap不担心产生起义问题?
因为HashMap的设计是给单线程使用的,如果取到null值,就可以调用HashMap的containsKey()方法来区分null值到底是插入的null值,还是不存在当前值。
而在ConcurrentHashMap是多线程情况下使用的,情况更加复杂,如一个线程调用ConcurrentHashMap的containsKey(key)方法,期望返回的值是false,但是另一个线程调用ConcurrentHashMap的put()方法,插入了一个key,并且存入的是null。则第一个线程返回的就是true。则上述产生的歧义不能够被证伪。
总结:ConcurrentHashMap在源码中加入不允许插入null值的设计,主要目的是为了防止并发场景下的歧义问题。