java集合类源码剖析
hashmap
底层实现
HashMap.Entry数组,数组+拉链
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
}
- Entry对象代表了HashMap中的一个元素(键值对)
- hashCode相同(碰撞)的元素将分配到Entry数组的同一个桶中
- 同一个桶中的Entry对象由next域串联起来,构成一个单向链式结构
初始化
- 默认容量
DEFAULT_INITIAL_CAPACITY
是16 - 默认负载因子
DEFAULT_LOAD_FACTOR
是0.75 - 若指定的初始容量
initialCapacity
大于MAXIMUM_CAPACITY
,会重置为MAXIMUM_CAPACITY
- HashMap实际的容量大于等于指定的容量
initialCapacity
,因为hashmap的容量是2的幂,如果initialCapacity
不是2的幂,实际容量将设置为比initialCapacity
大的最小的2的幂值int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;
- 如果使用另一个map进行初始化,负载因子会使用
DEFAULT_LOAD_FACTOR
,容量会在(int) (m.size() / DEFAULT_LOAD_FACTOR) + 1
和DEFAULT_INITIAL_CAPACITY
中取大者 - 调用
init()
方法(空实现,LinkedHashMap会重写这个方法)
put
- 如果键已经存在,用新值覆盖旧值并返回旧值
- 如果键已经存在,会调用
recordAccess
方法,凡是访问了HashMap中元素的方法,都会调用recordAccess
方法,在HashMap中是空实现,在LinkedHashMap会重写这个方法 - 如果键不存在,会调用
addEntry
方法,创建一个新的Entry对象,放到Entry数组对应的桶中,成为拉链的第一个元素 - 如果键不存在,添加了新元素之后,会判断是否需要扩容。如果
size>=threshold
,则进行2倍扩容(注意,如果容量已经等于最大容量MAXIMUM_CAPACITY
,只把threshold改为Integer.MAX_VALUE
,而不扩容)。创建新的Entry数组,并将当前数组中的元素迁移到新的数组中(重新计算每个元素在新数组中的索引) - 如果键不存在,
modCount++
,size++
,modCount是用来记录结构性修改次数的,凡是修改了底层结构的操作都会令modCount
加1,modCount
主要用于迭代器的快速失败,迭代器将根据modCount
的值是否发生了变化来判断是否有其他进程对HashMap做了修改
几点注意事项
- null的hashCode定义为0,如果key==null,将设置到table[0]中
- hash算法(h是传入的key的hashCode)
h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);
- 根据hash定位数组索引的算法
h & (length-1)
- 判断key是否存在的方法
e.hash == hash && ((k = e.key) == key || key.equals(k))
remove
- 计算key在Entry数组中的索引,在桶中查找键相等的元素(
equals
) - 如果找到了,将前一节点的next指向当前节点的下一个节点(单链表的删除操作)并返回当前节点
- 如果找到了,
modCount++
,size--
,调用recordRemoval
方法,凡是删除了HashMap中元素的方法都会调用recordRemoval
,在HashMap中是空实现,在LinkedHashMap会重写这个方法 - 最终将返回节点(Entry对象)的value值
get
- 计算key在Entry数组中的索引(如果key==null,索引是0)
- 遍历当前桶中的元素,如果找到key相等的元素(
equals
),返回元素的值
keySet
- 返回
keySet
对象 keySet
是HashMap中的一个内部类,继承了AbstractSet
,KeySet
的remove
和clear
方法可以直接修改HashMap本身的内容。因为,KeySet只不过是HashMap的一个视图而已private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } }
keySet
的iterator
方法调用了外部类(HashMap)的newKeyIterator
方法,newKeyIterator
将返回一个KeyIterator
对象,KeyIterator
继承了HashIterator
。LinkedHashMap重写了newKeyIterator
方法。HashIterator
中的域,expectedModCount
初始化为modCount
,index
初始化为第一个不为空的桶的索引+1,next
初始化为第一个不为空的桶中拉链的第一个元素。private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry } HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } }
next
方法。迭代的方案是,按照索引从小到大(大于0,小于table.length
)遍历每一个桶,如果桶不为空,则遍历这个桶中拉链的每个元素。每次被遍历到的元素将赋值给current
,current
对于下一次操作而言,保存了上一个返回的元素。while (index < t.length && (next = t[index++]) == null); //风骚的代码
remove
方法。如果current==null
,将抛出异常,这说明在调用remove
方法之前必须先调用next
,删除current.key
对应的元素,最后expectedModCount = modCount
,这是因为删除操作会使得modCount++
,而这次的删除操作并不是并发操作引起的,因此重置expectedModCount
,以免触发快速失败。- 在调用
next
和remove
时,有个快速失败(fast-fail)机制if (modCount != expectedModCount) throw new ConcurrentModificationException();
containsKey
- 计算key在数组中的索引,遍历桶中的所有元素,如果存在key,则返回true,否则返回false
containsValue
- 遍历所有的桶中的所有元素,如果找到值equals的,则返回true,否则返回true,否则返回false
- 这个方法效率蛮低的
ArrayList
底层实现
Object数组
初始化
- 如果没有指定容量,使用默认容量10
add
- 检查容量,同时
modCount++
,如果size+1 > elementData.length
,则需要先进行扩容,默认1.5倍扩容,如果扩容后容量还是还是比size+1
小,则新的容量等于size+1
,扩容使用Arrays.copyOf
静态方法int newCapacity = (oldCapacity * 3)/2 + 1;
elementData = Arrays.copyOf(elementData, newCapacity);
- 添加元素
elementData[size++] = e;
get
- 检查参数,
index
必须小于size
- 返回元素,做了类型转换
return (E) elementData[index];
set
- 检查参数,
index
必须小于size
- 取出就元素,设置新元素,返回旧元素
clear
- 将数组中索引小于
size
的所有元素置为null(释放引用,让垃圾回收机制工作) size=0
remove(int index)
- 检查参数,
index
必须小于size
modCount++
- 计算需要移动的元素数量
numMoved
int numMoved = size - index - 1;
- 如果
numMoved > 0
,调用System.arraycopy
将从index+1
开始的numMoved
个元素向前移动一位,System.arraycopy
是本地方法System.arraycopy(elementData, index+1, elementData, index, numMoved);
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
- 为了释放引用,让垃圾回收机制工作,将移动前最后一个元素的位置设置为null
- 返回被删除的元素
- remove方法效率低是因为需要将被删除元素后的所有元素向前移动一个位置
remove(Object o)
- 循环遍历整个数组,如果找到
equals
的元素,就删除这个元素,删除的方式见remove(int index)
- 因为在循环过程中,每找到一个符合条件的元素,都会移动一次元素,因此这个方法效率更低
add(int index, E element)
- 检查参数,
index
必须大于0,小于size
- 检查容量,同时
modCount++
,如果size+1 > elementData.length
,则需要先进行扩容,默认1.5倍扩容,如果扩容后容量还是还是比size+1
小,则新的容量等于size+1
,扩容使用Arrays.copyOf
静态方法int newCapacity = (oldCapacity * 3)/2 + 1;
elementData = Arrays.copyOf(elementData, newCapacity);
- 将从
index
开始的元素向后移动一位,将新元素设置到index
的位置 size++
- 同样需要在数组中移动元素,效率低
lastIndexOf(Object o)
- 反向遍历整个数组,如果找到
equals
的元素,则返回对应的索引,否则,返回-1
HashSet
底层实现
HashMap
常用操作
- HashSet内部封装了HashMap,其功能都是通过HashMap实现的,HashSet中的元素存储在HashMap的key中,Value使用一个静态哑变量
PRESENT
占位private static final Object PRESENT = new Object();
- HashSet实现举例
public Iterator<E> iterator() { return map.keySet().iterator(); }
public boolean contains(Object o) { return map.containsKey(o); }
public boolean add(E e) { return map.put(e, PRESENT)==null; }
public boolean remove(Object o) { return map.remove(o)==PRESENT; }
LinkedHashMap
底层实现
LinkedHashMap.Entry数组:数组+拉链+双向循环链表
- LinkedHashMap继承了HashMap
- LinkedHashMap的内部类Entry继承了HashMap.Entry,多了before和after两个域,用于实现双向循环链表,而原有的next域是用于桶中拉链的单向链表的,不要搞混了。
private static class Entry<K,V> extends HashMap.Entry<K,V> { // These fields comprise the doubly linked list used for iteration. Entry<K,V> before, after; }
初始化
- LinkedHashMap中多了一个
accessOrder
域,表示排序的方式,如果accessOrder
是true,按照访问顺序排序,否则,按照插入顺序排序 - 调用父类(HashMap)的构造方法,
accessOrder
默认为false
- 重载的构造方法可以传入
accessOrder
参数 - 调用
init()
方法,LinkedHashMap重载了HashMap的init()
方法,用于初始化双向循环链表头节点header
void init() { header = new Entry<K,V>(-1, null, null, null); header.before = header.after = header; }
put
- 如果键已经存在,用新值覆盖旧值并返回旧值
- 如果键已经存在,会调用
recordAccess
方法,凡是访问了HashMap中元素的方法,都会调用recordAccess
方法,在HashMap中是空实现,LinkedHashMap重写了这个方法,如果accessOrder
等于true
,modCount++
,从双向循环链表中删除当前节点,并把当前节点插入到header
之前void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } }
- 如果键不存在,会调用
addEntry
方法,LinkedHashMap重写了HashMap的addEntry
方法,创建一个新的Entry对象,放到Entry数组对应的桶中,成为拉链的第一个元素,并在双向循环链表中把新节点插入到header
之前void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<K,V>(hash, key, value, old); table[bucketIndex] = e; e.addBefore(header); size++; }
- 如果键不存在,添加了新元素之后,会判断是否需要扩容。如果
size>=threshold
,则进行2倍扩容(注意,如果容量已经等于最大容量MAXIMUM_CAPACITY
,只把threshold改为Integer.MAX_VALUE
,而不扩容)。创建新的Entry数组,并将当前数组中的元素迁移到新的数组中(重新计算每个元素在新数组中的索引) - 如果键不存在,
modCount++
,size++
,modCount是用来记录结构性修改次数的,凡是修改了底层结构的操作都会令modCount
加1,modCount
主要用于迭代器的快速失败,迭代器将根据modCount
的值是否发生了变化来判断是否有其他进程对HashMap做了修改
get
- 调用HashMap的get方法
- 计算key在Entry数组中的索引(如果key==null,索引是0)
- 遍历当前桶中的元素,如果找到key相等的元素(
equals
),返回元素的值
- 如果返回是
null
,则返回null
- 否则在返回值之前调用
recordAccess
方法,LinkedHashMap重写了这个方法,如果accessOrder
等于true
,modCount++
,从双向循环链表中删除当前节点,并把当前节点插入到header
之前void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } }
remove
- 计算key在Entry数组中的索引,在桶中查找键相等的元素(
equals
) - 如果找到了,将前一节点的next指向当前节点的下一个节点(单链表的删除操作)并返回当前节点
- 如果找到了,
modCount++
,size--
,调用recordRemoval
方法,凡是删除了HashMap中元素的方法都会调用recordRemoval
,在HashMap中是空实现,在LinkedHashMap会重写这个方法,将元素从双向循环链表中移除void recordRemoval(HashMap<K,V> m) { remove(); }
- 最终将返回节点(Entry对象)的value值
keySet
- 返回
keySet
对象 keySet
是HashMap中的一个内部类,继承了AbstractSet
,KeySet
的remove
和clear
方法可以直接修改HashMap本身的内容。因为,KeySet只不过是HashMap的一个视图而已private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } }
keySet
的iterator
方法调用了外部类(HashMap)的newKeyIterator
方法,LinkedHashMap重写了newKeyIterator
方法,newKeyIterator
将返回一个KeyIterator
对象,KeyIterator
继承了LinkedHashIterator
。LinkedHashIterator
中的域,expectedModCount
初始化为modCount
,lastReturned
保存最近一次迭代返回的元素,初始化为null,nextEntry
保存下一次迭代返回的元素,初始化为header
后的第一个元素,即第一个插入或者访问的元素。private abstract class LinkedHashIterator<T> implements Iterator<T> { Entry<K,V> nextEntry = header.after; Entry<K,V> lastReturned = null; int expectedModCount = modCount;
next
方法。迭代的方案是,从header的下一个元素开始,按照双向循环链表的正向遍历。将本次返回的元素赋值给lastReturned
,对于下一次操作而言,lastReturned
保存了最近一次迭代返回的元素remove
方法。如果lastReturned==null
,将抛出异常,这说明在调用remove
方法之前必须先调用next
,删除lastReturned.key
对应的元素,最后expectedModCount = modCount
,这是因为删除操作会使得modCount++
,而这次的删除操作并不是并发操作引起的,因此重置expectedModCount
,以免触发快速失败。- 在调用
next
和remove
时,有个快速失败(fast-fail)机制if (modCount != expectedModCount) throw new ConcurrentModificationException();
ConcurrentHashMap
底层实现
ConcurrentHashMap.Segment数组 + ConcurrentHashMap.HashEntry数组
- ConcurrentHashMap将整个map分为若干段(Segment)
- 读写时,仅对当前段加锁,通过这种方式来减少对整个map加锁造成的效率损失
- 每个段(Segment)都拥有自己的HashEntry数组,相当于每个段都是一个HashMap
- 定位元素时,先根据
hashCode
定位所在的段,再根据hashCode
定位所在的桶,最后再在拉链中定位元素(equals
)。 - Segment,注意
count
、table
是volatile的transient volatile int count; transient int modCount; transient int threshold; transient volatile HashEntry<K,V>[] table; final float loadFactor;
- HashEntry,注意
key
、hash
、next
是final的,value
是volatile的static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; HashEntry(K key, int hash, HashEntry<K,V> next, V value) { this.key = key; this.hash = hash; this.next = next; this.value = value; } }
初始化
- 默认容量
DEFAULT_INITIAL_CAPACITY
是16 - 默认负载因子
DEFAULT_LOAD_FACTOR
是0.75 - 默认段数组容量
DEFAULT_CONCURRENCY_LEVEL
是16 - 初始化段数组(
Segment<K,V>[] segments
)。this.segments = Segment.newArray(ssize);
- 计算段数组容量
ssize
。若指定的容量concurrencyLevel
大于MAX_SEGMENTS
,会重置为MAX_SEGMENTS
,实际的容量大于等于concurrencyLevel
,因为容量是2的幂,如果concurrencyLevel
不是2的幂,实际容量将置为比concurrencyLevel
大的最小的2的幂值int ssize = 1; while (ssize < concurrencyLevel) { //... ssize <<= 1; }
- 计算
segmentMask
- 计算
segmentShift
- 计算段数组容量
- 为每个段初始化HashEntry数组
for (int i = 0; i < this.segments.length; ++i) this.segments[i] = new Segment<K,V>(cap, loadFactor);
- 计算HashEntry数组的容量
cap
。若指定的容量initialCapacity
大于MAXIMUM_CAPACITY
,会重置为MAXIMUM_CAPACITY
,根据initialCapacity / ssize
计算每个段中的容量并向上取为2的幂int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = 1; while (cap < c) cap <<= 1;
- 计算HashEntry数组的容量
put
- value不允许为null,事实上,key也不能为null,因为计算hash值时会调用
key.hashCode()
,如果key为null的话会抛出空指针异常。HashMap允许key和value为null,这里不一样。 - 计算hash值
- 根据hash值计算在段数组中的索引
final Segment<K,V> segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; }
- 调用对应段对象的put方法
Segment的put
- 判断是否需要扩容。计算
count+1
是否大于threshold
,如果大于,进行2倍扩容(如果容量已经大于等于最大容量MAXIMUM_CAPACITY
,直接返回,不扩容)。创建新的Entry数组,并将当前数组中的元素迁移到新的数组中(重新计算每个元素在新数组中的索引,具体算法太风骚,没有看懂,稍后再看) - 如果键已经存在,用新值覆盖旧值并返回旧值
- 如果键不存在,创建一个新的HashEntry对象,放到HashEntry数组对应的桶中,成为拉链的第一个元素,并返回null
- 如果键不存在,
modCount++
,size++
,modCount是用来记录结构性修改次数的,凡是修改了底层结构的操作都会令modCount
加1,modCount
主要用于迭代器的快速失败,迭代器将根据modCount
的值是否发生了变化来判断是否有其他进程对HashMap做了修改
几点注意事项
- hash算法(h是传入的key的hashCode)
h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16);
- 根据hash定位数组索引的算法
int index = hash & (tab.length - 1);
- put,remove等写方法会对当前的段加锁,这是保障并发访问的关键所在,get方法不会加锁,除非需要value等于null的特殊情况(HashEntry中只有value不是final的,因此有可能被重排序到构造函数之外进行初始化)
lock(); try { //... } finally { unlock(); }
get
- key不能为null,因为计算hash值时会调用
key.hashCode()
,如果key为null的话会抛出空指针异常。HashMap允许key为null,这里不一样。 - 计算hash值
- 根据hash值计算在段数组中的索引
- 调用对应段对象的get方法
Segment的get
- 如果
count==0
,直接返回null - 计算key所在的索引,并取桶中的第一个节点开始遍历,如果找到key相等的元素(
equals
),将返回对应的value。 - 找到key相等的元素后,会检查value是否为null,如果不为null直接返回,如果为null,将通过加锁的方式取得value并返回。正常情况下value的值不应该为null,为null说明了某些编译器对HashEntry的初始化做了重排序,通过加锁的方式取得value,将会等到新增HashEntry的操作全部结束后才去试图获得value的值。
- get之所以不用加锁,是因为
HashEntry
的value
是volatile的
remove
- key不能为null,因为计算hash值时会调用
key.hashCode()
,如果key为null的话会抛出空指针异常。HashMap允许key为null,这里不一样。 - 计算hash值
- 根据hash值计算在段数组中的索引
- 调用对应段对象的remove方法
Segment的remove
- 计算key在HashEntry数组中的索引,在桶中查找键相等的元素(
equals
) - 如果找到了,链表中该节点之后的节点不用动,该节点之前的每个元素拷贝一份。为什么不直接将节点从链表中摘除呢?这是因为HashEntry中的
key
,next
,hash
都是final的,一经初始化,不能修改,因此需要把节点之前的元素全部拷贝一份新的。(那么问题来了,为什么设计成final的呢?参见JMM-final)HashEntry<K,V> newFirst = e.next; for (HashEntry<K,V> p = first; p != e; p = p.next) newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst;
1 -> 2 -> 3 -> 4 -> 5 - > 6 移除4后将变为 3 -> 2 -> 1 -> 5 -> 6
- 如果找到了,
modCount++
,count--
- 返回被删除节点(HashEntry对象)的value值
size
- 不加锁的计算size,第一次循环,对每个段的
count
求和sum
,并把每个段的modCount
保存下来。第二次循环,再次对每个段的count
求和check
,并检查这次的modCount
是否和上次的一样。如果每个段的modCount
都没有变,且check=sum
,则返回sum,否则,重试。(最多进行RETRIES_BEFORE_LOCK
次) - 对所有段加锁,循环,对每个段的
count
求和sum
,对所有段解锁,返回sumsum = 0; for (int i = 0; i < segments.length; ++i) segments[i].lock(); for (int i = 0; i < segments.length; ++i) sum += segments[i].count; for (int i = 0; i < segments.length; ++i) segments[i].unlock();
keySet
- 返回
keySet
对象 keySet
是ConcurrentHashMap中的一个内部类,继承了AbstractSet
,KeySet
的remove
和clear
方法可以直接修改HashMap本身的内容。因为,KeySet只不过是HashMap的一个视图而已final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return new KeyIterator(); } public int size() { return ConcurrentHashMap.this.size(); } public boolean contains(Object o) { return ConcurrentHashMap.this.containsKey(o); } public boolean remove(Object o) { return ConcurrentHashMap.this.remove(o) != null; } public void clear() { ConcurrentHashMap.this.clear(); } }
keySet
的iterator
方法返回一个KeyIterator
对象,KeyIterator
继承了HashIterator
。- 遍历的顺序,按照Segment数组从后向前,HashEntry数组从后向前,拉链从头向为的顺序遍历
- ConcurrentHashMap的迭代器没有使用快速失败机制,在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。(参见 弱一致性迭代器)
思考
- 为什么key和value不允许为null?
HashTable
与HashMap对比
- 继承不同
public class Hashtable extends Dictionary implements Map public class HashMap extends AbstractMap implements Map
- Hashtable 中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。
- Hashtable中,key和value都不允许出现null值。在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,既可以表示HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。
- 两个遍历方式的内部实现上不同。Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
- Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
与ConcurrentHashMap对比
- HashTable锁定整个map,而ConcurrentHashMap仅锁定所在的段,ConcurrentHashMap并发的效率高
- HashTable提供强一致性迭代器,而ConcurrentHashMap提供弱一致性迭代器,HashTable迭代时的一致性好
时间: 2024-10-06 01:57:33