ArrayList
是否保证线程安全
ArrayList不是线程安全的,其内部的方法均没有使用synchronized修饰,而且在ArrayList类源码注释的39行也明确说明它不是同步的。
39 - This class is roughly equivalent to {@code Vector}, except that it is unsynchronized.
扩容机制
初始容量是10。
ArrayList底层是使用Object数组来存储元素,刚初始化ArrayList时是一个空的Object数组,只有在添加第一个元素时才会扩充其初始容量为10。
ArrayList每次都是在调用add方法添加元素之前判断是否需要扩容,除了第一次扩容外,其余都是扩容为原来的1.5倍。
每次添加元素都是使用尾插法。
// ArrayList类
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 用于初始化ArrayList的空Object数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// ArrayList中用于存储元素的Object数组,初始化时将EMPTY_ELEMENTDATA赋值给它
transient Object[] elementData;
// elemntData数组中元素个数
private int size;
// 添加元素的方法
private void add(E e, Object[] elementData, int s) {
// ArrayList每次都是在添加元素之前判断是否需要扩容,其不像Map中有扩容因子,它是在当数组被填满而无法添加元素时才扩容
if (s == elementData.length)
elementData = grow();
// 添加元素
elementData[s] = e;
size = s + 1;
}
// 会调用上面的add方法
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
// 扩容的方法
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
// 判断是否为第一次扩容
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, // 最小增幅
oldCapacity >> 1// 右移运算符,每次扩容的增幅是原来容量的一半
);
// 扩容后的数据迁移操作
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
// 这一步就是ArrayList的第一次扩容,会扩容为一个10容量的Object数组
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
// 会调用上面的grow方法,并且传入比当前数组容量大1的最小容量大小
private Object[] grow() {
return grow(size + 1);
}
//ArraySupport类
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
//扩容为原来容量的1.5倍 -> 新容量 = (旧容量 >> 1) + 旧容量
int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
//判断当前扩容是否会超过最大限制,未超过就返回新长度
if (newLength - MAX_ARRAY_LENGTH <= 0) {
return newLength;
}
return hugeLength(oldLength, minGrowth);
}
Vector
是否保证线程安全
Vector是线程安全的,其内部的方法均使用synchronized修饰,而且在Vector类源码注释的79行也明确说明它是同步的。
如果不需要线程安全建议使用ArrayList代替Vector。
79 - Unlike the new collection implementations, {@code Vector} is synchronized.
扩容机制
初始容量是10。
Vector底层是使用Object数组来存储元素,刚初始化Vector时就会创建一个长度为10的Object数组。
Vector每次都是在调用add方法添加元素之前判断是否需要扩容,在Vector中有一个capacityIncrement变量,默认为0。在Vector类源码注释的118~120行已经说明了其扩容机制,在扩容时如果capacityIncrement变量的值小于或等于0,则Vector每次扩容为原来的2倍,否则扩容为原有容量 + capacityIncrement。这个变量可以在Vector构造方法中指定。
每次添加元素都是使用尾插法。
Vector虽然是线程安全的,但是也有Fast-Fail机制和modCount变量。
// 每次扩容的增量
protected int capacityIncrement;
// 扩容方法
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity,
// 如果capacityIncrement小于或等于0,则扩容为原来的2倍
capacityIncrement > 0 ? capacityIncrement : oldCapacity
);
return elementData = Arrays.copyOf(elementData, newCapacity);
}
LinkedList
是否是线程安全
LinkedList不是线程安全的,其内部的方法均没有使用synchronized修饰,而且在LinkedList类源码注释的39行也明确说明它是非同步的。
39 - Note that this implementation is not synchronized.
添加元素
LinkedList底层使用的是双向链表的数据结构来存储数据,元素存储在其内部的一个自定义的内部类Node中,该类中拥有指向上一个和下一个元素的指针。
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;
}
}
获取元素
LinkedList因为底层使用的是双向链表的数据结构,所以其随机读取的效率比ArrayList低的多。
在node方法中,会先将传入的索引(index)与链表长度一半(size >> 1)进行比较,如果是索引更小,那么从链表头部开始顺序循环,直到找到为止。如果索引更大,那么从链表尾部倒序循环,直到找到为止。也就是说越靠近中间的元素,循环遍历的次数就越多,效率也就越低,而且随着链表越来越长,get方法的执行性能也会成指数级降低。
// 随机获取元素
public E get(int index) {
// 检查元素索引的合法性
checkElementIndex(index);
return node(index).item;
}
// get方法的真正实现
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;
}
}
所以只能说LinkedList的插入和删除效率高是相对的,因为它省去了ArrayList中插入数据可能的数组扩容和数据元素移动造成的开销,但是LinkedList随机读取时遍历链表的开销也不小,同时ArrayList数据扩容和数据元素移动却并不是时时刻刻都在发生的。
modCount变量
这里均以ArrayList举例,在ArrayList中modCount变量是定义其父类AbstractList中的,该变量的初始值为0。
modCount变量的作用是为了防止在迭代过程中通过非迭代器的方法修改了原有的集合的结构(添加元素、删除元素、扩容、缩容等操作),modCount和迭代器的Fast-Fail机制紧密相关。
ArrayList中的add、addAll、remove、fastRemove、removeRange、clear、trimToSize、ensureCapacity方法都会修改modCount变量。
Fast-Fail机制
在ArrayList类源码注释的75~93行中对Fast-Fail机制有介绍,
Fast-Fail又叫快速失败机制,即在创建了迭代器后的任何时间内,如果集合发生了结构上的改变(添加元素、删除元素、扩容、缩容等操作),除非使用迭代器自己的add和remove方法,否则都会抛出ConcurrentModificationException异常。因此,在面对并发修改时,迭代器会快速、干净地"失败",而不是去冒险迎接可能将会到来的错误。Fast-Fail机制会尽可能地抛出ConcurrentModificationException异常。
注:结构上的修改仅仅是指添加或者删除任何数量的元素,设置元素的值不属于结构性修改。在LinkedList类源码注释的42~44行已经解释了。
42 - A structural modification is any operation
43 - that adds or deletes one or more elements; merely setting the value of
44 - an element is not a structural modification.
Fast-Fail机制并不能用来保证集合在并发修改时的正确性,它只能用来检测错误。
Iterator和ListIterator迭代器中的方法都具有Fast-Fail机制,迭代器内部有一个checkForComodification方法,这个方法在迭代器方法执行前会对modCount变量值进行检测,如果发现与expectedModCount值不一致就会抛出异常。
在Iterator和ListIterator内部有一个expectedModCount变量,它默认情况下是等于modCount变量的值。其实迭代器内部的remove方法也是调用的集合的remove方法,但是它会在最后同步expectedModCount与modCount的值。
// 迭代器中的remove方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 迭代器内部也是调用集合的remove方法
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 最后会同步二者的值
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
ArrayList与LinkedList的区别
是否保证线程安全
ArrayList和LinkedList都是不保证线程安全。
底层数据结构
Arraylist底层使用的是Object数组。
LinkedList底层使用的是双向循环链表。
插入和删除是否受元素位置的影响
ArrayList采用数组存储元素,所以插入和删除元素操作的时间复杂度受元素位置的影响。
执行add方法时, ArrayList会默认将指定的元素追加到数组的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置"i"插入和删除元素的话时间复杂度就时O(n - i)。因为在进行上述操作的时候集合中第i个元素之后的(n - i)个元素都要执行向后或者向前移一位的操作。
LinkedList采用双向链表存储,所以插入和删除元素的时间复杂度不受元素位置的影响,都是近似O(1)的,而数组则近似O(n)。
是否支持快速随机访问
LinkedList不支持快速随机元素访问。
ArrayList支持快速随机元素访问,其实现了RandmoAccess接口。
内存空间占用
ArrayList的空间浪费主要体现在数组的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继、直接前驱以及数据)。
为什么ArrayList查询比LinkedList更快?
因为ArrayList底层使用数组存储元素,而数组是支持随机访问的,所以数组在内存中是开辟了一块连续的、大小相同的空间(虚拟内存是连续的,物理内存不是),所以只要得到首地址和偏移量就能快速找到某个位置的元素。而LinkedList底层使用链表,链表只能轮询查找元素,效率较低。
HashMap
是否是线程安全
HashMap不是线程安全的,其内部的方法均没有使用synchronized修饰,而且在HashMap类源码注释的32~34行也明确说明它不是同步的。
32 - (The <tt>HashMap</tt>
33 - class is roughly equivalent to <tt>Hashtable</tt>, except that it is
34 - unsynchronized and permits nulls.)
拉链法
HashMap基本上就是用拉链法实现的。拉链法的实现比较简单,将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK 7
初始化容量是16,加载因子默认是0.75,加载因子可自行修改。
HashMap底层使用Entry<K, V>数组来存储元素,Entry<K, V>是HashMap的一个内部类,其中含有下一个元素的指针。HashMap在刚初始化时是一个空的Entry<K, V>数组,只有在添加第一个元素时才会扩充其初始容量为16。
HashMap中提供了一个可以自定义数组容量的构造方法,但是容量必须为2的整数次幂,如果不是则会在roundUpToPowerOf2方法中强制将其改为2的整数次幂或1。
roundUpToPowerOf2这个方法是将你希望的数组容量输入,经过计算返回一个合适的数组容量。
-
如果超过了最大值,即230,将设置成最大值。
-
如果这个数是零,那么就返回1。
-
如果这个数就是2的整数次幂,那么将返回此数。
-
如果这个数不是2的整数次幂,那么返回的数是该数二进制下只取最高位为1剩下全部变成0的数最后再乘以2的结果。
roundUpToPowerOf2方法里面有一个Integer类的highestOneBit方法,该方法是将一个数的二进制下的最高位变为1,其余位全部为0。比如13的二进制是1101B,经过highestOneBit方法处理后变为1000B,就是8。该方法相当于将一个数变为小于或等于其自身的一个2的整数次幂数。而在roundUpToPowerOf2方法中将highestOneBit方法处理的结果又乘了2,所以最终的数组容量就是一个大于或等于当前容量的一个2的整数次幂数。比如当前容量是9就会变为16,当前容量是23就会变成32。
//初始化容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//存储元素的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static final Entry<?,?>[] EMPTY_TABLE = {};
//添加元素的方法
public V put(K key, V value) {
//在添加元素前判断数组是否为空,如果为空就对其进行初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
//计算元素位置索引
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果元素Key相同就替换旧Value,同时将其返回
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);
//扩容阈值变为(数组容量 * 加载因子)
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
//将容量变为2的整数次幂或1
private static int roundUpToPowerOf2(int number) {
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
HashMap中的Key和Value都可以为null,空Key的哈希值永远都是0,因此存储的数组索引也是0。
HashMap中规定了数组容量必须为2的整数次幂,是为了减少哈希碰撞几率,这一定在put方法中已经体现。indexFor方法中将Key的哈希值与数组容量减1的值进行按位与计算,原因是当数组容量为2的整数次幂时,该值的二进制最高位为1其余都为0,再减去1就变为一个全是1的二进制数,这样与Key的哈希值按位运算的结果就完全依赖于哈希值,不会出现某个位数永远无法被计算到。所以这样计算出的索引分布会更加均匀,减少了哈希碰撞的概率。
比如容量为16,其二进制是10000,再减1就是1111,将其与任何哈希值按位与运算结果都等于哈希值的后四位。假如容量是14,其二进制是1110,再减1就是1101,哈希值与其按位与运算结果的倒数第二位永远都是0。
put方法在存储元素时会判断是否有相同Key的元素,如果有就对其进行替换并将旧值返回。for循环遍历比对是因为可能是链表,直到遍历至最后一个元素,如果均不相等就通过addEntry方法进行添加元素操作。元素比对是先对Key的哈希值进行比较,然后才是Key值是否相等。因为相同的Key值其哈希值一定相同,但不同Key值其哈希值也有可能相同,如果Key的哈希值都不相同就不用再对Key值进行比较了。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//表明Key值是可以为null的,有专门一个方法来处理空Key
if (key == null)
return putForNullKey(value);
//HashMap中是使用了自定义的一个哈希规则,然后对Key进行哈希计算
int hash = hash(key);
//使用Key的哈希值与数组容量来计算元素位置索引
int i = indexFor(hash, table.length);
//判断是否有相同Key的元素,因为可能该索引位置上是一个链表,所以对其进行遍历查找
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//对Key的哈希值和Key值进行比较
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//对相同Key值的元素进行替换
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加元素
addEntry(hash, key, value, i);
//未有相同Key元素时返回null
return null;
}
//该方法确定了元素的数组索引
static int indexFor(int h, int length) {
return h & (length - 1);
}
addEntry方法负责添加新元素和扩容,在添加元素前会先进行扩容判断,条件是当前元素数量达到阈值并且当前索引位置不为空。这里要说明一下,扩容的目的是为了避免形成太长的链表,如果元素数量已经达到了阈值,但是当前索引位置是空的,就不会形成链表,也就没必要扩容了。
resize方法是扩容方法,这里可以看出HashMap扩容是变为原有容量的2倍,而且扩容完毕后要重新计算元素的索引位置(又叫重rehash),因为索引位置是根据数组容量计算的。
在createEntry方法中,每次都是将原有的链表或元素接到新元素的尾部,所以JDK 7下HashMap使用的是头插法,但是头插法会造成环链的问题,所以在JDK 8中改为了尾插法。
//添加元素
void addEntry(int hash, K key, V value, int bucketIndex) {
//添加元素前的扩容判断,先判断元素数量是否已达到扩容阈值,再判断当前索引位置是否为null
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容方法,新容量是原有的2倍
resize(2 * table.length);
//因为数组容量变了,所以扩容后要重新计算元素的索引位置(又叫rehash)
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//真正将元素插入到桶中的方法
void createEntry(int hash, K key, V value, int bucketIndex) {
//这里使用的是头插法
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
transfer方法用于在扩容后重新计算数组中所有元素的索引位置(又叫rehash),假如在并发情况下,当线程A执行完Entry<K,V> next = e.next;
语句后被挂起了,这时线程B进入并执行完所有代码,然后线程A被唤醒执行完所有代码(三轮遍历后)就会形成环链。
两个线程中共享的资源是table,而e、next和newTable都不属于共享资源,真正导致环链的原因是线程1修改了元素中下一个元素的指针。
[产生环链原理解析1](https:// www.toutiao.com/i6545790064104833539/)
[产生环链原理解析2](https:// blog.csdn.net/qq_42524262/article/details/100636429)
哈希桶中形成环链后问题还没有直接产生,当使用get方法查找一个不存在的元素时,而这个元素的索引位置又恰好等于环链的索引位置,程序将会进入查找的死循环!
//扩容方法
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//扩容
Entry[] newTable = new Entry[newCapacity];
//扩容后进行元素迁移
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//元素迁移方法
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历数组中的每个元素
for (Entry<K,V> e : table) {
//遍历每个链表
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
JDK 8
这里主要讲讲JDK 8中HashMap的一些变化或改进,其余相同的就不多介绍。
在JDK 8中HashMap将存储元素的容器Entry改名为了Node,同样也是泛型的。
static class Node<K,V> implements Map.Entry<K,V> {
//Key的哈希值
final int 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;
}
//其余方法省略...
}
在JDK 8中HashMap使用数组、链表、红黑树三者来存储元素,在未达到转变条件之前和JDK 7中一样是数组与链表进行存储,当某个链表变换条件时会将该链表转换为红黑树,同时在特定条件下红黑树也会转换回链表。
链表到红黑树的转换条件(需同时满足):
- 该链表的长度大于或等于8。
- 数组容量大于或等于64。
红黑树到链表的转换条件(需同时满足):
- 该链表的长度小于或等于6。
在变换中使用7作为过渡,从而避免红黑树与链表之间的频繁转换,因为这种转换也是很消耗性能的。如果链表长度大于或等于8,但是数组容量小于64,则只进行扩容。
在JDK 8中初始化数组容量(即第一次扩容)是在添加元素之前,而其余的扩容都是在添加完元素之后,判断是否需要扩容。
//链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转换为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//链表转换为红黑树的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
//添加方法
public V put(K key, V value) {
//这里计算了元素Key的哈希值
return putVal(hash(key), key, value, false, true);
}
/*
* 真正添加元素的操作
* onlyIfAbsent:如果为true,不会替换已经存在元素的Value
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//数组
Node<K,V>[] tab;
//要添加的元素在数组中所在位置上的元素
Node<K,V> p;
/*
* n:数组容量
* i:要添加的元素的数组位置索引
*/
int n, i;
//当数组为空时,初始化数组容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*
* 通过元素Key的哈希值(在put方法中计算了)与数组容量计算出元素在数组中的位置索引
* 判断该索引位置上是否为空,如果为空就直接插入元素
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//索引位置非空情况
Node<K,V> e;
K k;
//如果元素的Key相同就直接替换
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 {
/*
* 遍历链表中的每个元素,一直遍历到链表最后一个元素,然后将元素插入到链表的末尾(尾插法)
* binCount:哈希桶中的元素个数
*/
for (int binCount = 0; ; ++binCount) {
//判断是否遍历到末尾元素
if ((e = p.next) == null) {
//将元素插入到链表末尾
p.next = newNode(hash, key, value, null);
//判断当前链表长度是否大于或等于8
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//判断链表中是否有与其Key相等的元素
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//找到Key相等的元素时,将旧的Value返回,同时根据onlyIfAbsent参数判断是否替换旧Value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//替换旧Value
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//每次添加元素之后会判断是否需要扩容
if (++size > threshold)
resize();
//插入元素后的回调方法,默认为空方法,可以在子类中重写该方法
afterNodeInsertion(evict);
return null;
}
//链表转换为红黑树的方法
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);
}
}
在JDK 8的HashMap中,底层数组为空不在使用"{ }"来表示,而就是null。
当我们初始化HashMap时,我们可以通过构造方法指定数组的初始容量initialCapacity,但是在构造方法中并不会直接创建数组,而是将initialCapacity赋值给了threshold,当添加元素时才通过resize方法创建指定容量的数组。
因为每次扩容的增幅都等于原数组容量,所以元素的位置要么保持不变,要么在原位置上偏移原数组容量大小。
从上图可以看到,扩大2倍,相当于n左移一位,那么n - 1在最高位就多出了一个1,此时与原哈希值进行位与运算,就多参与了一位,并且这个比特位要么是0要么是1。
- 如果是0的话,索引不变。
- 如果是1的话,新索引 = 原索引 + oldCap。
在重新计算链表中元素位置时,只可能得到两个子链表,一个是索引不变的元素链表,另一个是具有相同偏移量(偏移量就是原数组容量)的元素链表。在构造子链表的过程中,使用头节点(loHead、hiHead)和尾节点(loHead、hiHead),保证了拆分后的有序性。
假设原数组容量为n,因为是2倍扩容,新数组容量就是2n,可以把扩容后的数组想象为一个高位数组(n + 1 ~ 2n)和一个低位数组(0 ~ n)的组合。
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;
}
//新容量扩容至原来的2倍
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];
//rehash
table = newTab;
if (oldTab != null) {
//遍历数组中的每个元素
for (int j = 0; j < oldCap; ++j) {
//备份数组中每个位置上的元素
Node<K,V> e;
//遍历哈希桶中的每个元素,哈希桶里面可能为null、可能是链表还可能是红黑树
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 {
/*
* loHead:索引不变的元素链表的头元素
* loTail:索引不变的元素链表的尾元素
*/
Node<K,V> loHead = null, loTail = null;
/*
* hiHead:索引不变的元素链表的头元素
* hiTail:索引不变的元素链表的尾元素
*/
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) {
//将低位链末尾元素的next设为null,因为e的下一个结点有可能被分到高位元素
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//将高位链末尾元素的next设为null,因为e的下一个结点有可能被分到低位元素
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
为什么是当链表长度大于等于8时变为红黑树?
在HashMap的源码注释180~198行中已经解释了为什么红黑树转换阈值是8:在理想情况下,如果使用随机的哈希值,元素分布在哈希桶(链表)中的概率遵循泊松分布,按照泊松分布的计算公式可以计算出哈希桶中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小,链表中超过8个元素的概率少于一千万分之一,再多概率就更少了。所以选择链表元素个数时选择了8,是根据概率统计而选择的。
180 - Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
198 - more: less than 1 in ten million //链表中超过8个元素的概率少于一千万分之一
HashTable
是否保证线程安全
HashTable是线程安全的,其内部的方法均使用synchronized修饰,而且在HashTable类源码注释的112行也明确说明它是同步的。
112 - Unlike the new collection implementations, {@code Hashtable} is synchronized.
扩容机制
初始化容量是11,加载因子默认是0.75,加载因子可自行修改,刚初始化HashTable时就会创建一个长度为10的Entry数组。
HashTable底层使用Entry<K, V>数组来存储元素,Entry<K, V>是HashTable的一个内部类,其中含有下一个元素的指针。
HashTable中提供了一个可以自定义数组容量的构造方法,并且容量无须时2的整数次幂,因为在计算元素索引时是直接用Key的哈希值与数组容量取余。
在HashTable的put方法中,是不允许Key和Value为null的,否则就会抛出NullPointerException异常。
addEntry方法是真正的添加方法,在添加元素前会判断是否需要扩容,扩容详情看rehash方法。HashTable插入元素使用的是头插法。
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//当容量为0时
if (initialCapacity == 0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//在初始化时就创建了容量为11的数组
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
//空参构造方法
public Hashtable() {
//默认容量为11,加载因子为0.75
this(11, 0.75f);
}
//添加方法
public synchronized V put(K key, V value) {
//检测value是否为null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
//HashTable使用的是默认的计算哈希值方法
int hash = key.hashCode();
//计算数组位置索引
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍历哈希桶
for(; entry != null ; entry = entry.next) {
//如果元素的Key值相同就直接替换Value,并且返回旧Value
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
//真正添加元素方法
private void addEntry(int hash, K key, V value, int index) {
Entry<?,?> tab[] = table;
//当哈希表中元素总数大于或等于阈值时进行扩容
if (count >= threshold) {
//扩容方法
rehash();
//扩容后重新计算元素索引
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//取出该索引上的原有元素
Entry<K,V> e = (Entry<K,V>) tab[index];
//将元素插入在链表头部(头插法)
tab[index] = new Entry<>(hash, key, value, e);
count++;
modCount++;
}
HashTable每次都是扩容为原有容量的2倍再加1,即NewCapacity = OldCapacity * 2 + 1。
//扩容方法
protected void rehash() {
int oldCapacity = table.length;
//备份原有数组
Entry<?,?>[] oldMap = table;
//扩容为原有容量2倍再加1
int newCapacity = (oldCapacity << 1) + 1;
//判断新容量是否超过最大值
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//如果原有数组的容量已经达到了最大值则不进行扩容,否则新容量直接取最大值
if (oldCapacity == MAX_ARRAY_SIZE)
return;
newCapacity = MAX_ARRAY_SIZE;
}
//创建新数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//遍历原有数组中的每个元素
for (int i = oldCapacity ; i-- > 0 ;) {
//遍历链表中元素
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
//获取当前元素的下一个元素
old = old.next;
//重新计算新数组索引
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//将原有元素插入到新位置链表的头部(头插法)
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
HashMap与HashTable的区别
是否保证线程安全?
HashMap不是线程安全的。
HashTable是线程安全的。
Key和Value能否为null?
HashMap的Key和Value都能取null值。
HashTable的Key和Value均不能取null值,会抛出空指针异常。
何时创建数组?
HashMap在初始化时只是创建了一个空数组"{ }",只有当添加第一个元素时才又创建了一个容量为16的数组。
HashTable在初始化时就创建了一个容量为11的数组。
ConcurrentHashMap
虽然已经有一个线程安全的HashTable,但是HashTable容器使用synchronized(他的get和put方法的实现代码如下)来保证线程安全,在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,访问其他同步方法的线程就可能会进入阻塞状态或轮询状态。如"线程1"使用put进行添加元素,"线程2"不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
是否是线程安全
ConcurrentHashMap是线程安全的,实现线程安全的方式与HashTable不同,HashTable使用的是synchronized关键字,而ConcurrentHashMap使用的是分段锁和CAS来实现线程安全。
JDK 7
ConcurrentHashMap中定义了两个静态内部类Segment<K, V>和HashEntry<K, V>,Segment就是ConcurrentHashMap特有的"段"概念,在Segment类内部有一个HashEntry[]数组,分段锁可以锁住Segment中的所有HashEntry,而HashEntry<K, V>是用来存储元素的。可以不Segment<K, V>类理解为HashMap,把HashEntry<K, V>理解为HashMap中的Node<K, V>类。
ConcurrentHashMap中有Segment数组和HashEntry数组两个数组,初始化容量都是16,加载因子默认是0.75,加载因子可自行修改。Segment数组在刚初始化时就会创建,而HashEntry数组
ConcurrentHashMap中提供了一个可以自定义Segment数组容量的构造方法,在构造方法中,ssize变量就是Segment数组的容量,它是由形参concurrencyLevel确定的,ssize一定是大于或等于concurrencyLevel的最小2的整数次幂。
//初始容量为16,即HashEntry数量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//HashEntry的扩容因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认并发级别,用来确定Segment数组的容量
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量,即最大HashEntry数量
static final int MAXIMUM_CAPACITY = 1 << 30;
//扫描重试的次数
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
//段表中的最小容量,即每个Segment中的最小HashEntry数量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//最大段数,即最多Segment数量
static final int MAX_SEGMENTS = 1 << 16;
//空参构造方法
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
//构造方法
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
//形参的非空判断
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//检验Segment数组是否超过最大容量
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
//Segment的右移保留量
int sshift = 0;
//Segment数组的容量
int ssize = 1;
/**
* 根据concurrencyLevel初始化ssize的值
* ssize一定是大于或等于concurrencyLevel的最小2的整数次幂
* 默认情况下concurrencyLevel是DEFAULT_CONCURRENCY_LEVEL
*/
while (ssize < concurrencyLevel) {
//每次ssize乘2就将sshift加1,所以sshift正好等于ssize的幂
++sshift;
ssize <<= 1;
}
//Segment的右移量
this.segmentShift = 32 - sshift;
//segmentMask用于确定段中位置索引
this.segmentMask = ssize - 1;
//检验HashEntry数组是否超过最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
/**
* 通过Segment数组的容量和ConcurrentHashMap的初始化容量来计算每个Segment中HashEntry数组的初始化容量
* 变量c是向上取整的
*/
int c = initialCapacity / ssize;
//这个操作相当于向上取整
if (c * ssize < initialCapacity) {
++c;
}
//每个Segment中HashEntry数组的容量,并且默认最小容量为2
int cap = MIN_SEGMENT_TABLE_CAPACITY;
//HashEntry数组的容量也一定是2的整数次幂
while (cap < c) {
cap <<= 1;
}
//创建一个Segment实例作为Segment模板
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int) (cap * loadFactor), (HashEntry<K,V>[]) new HashEntry[cap]);
//创建Segment数组
Segment<K,V>[] ss = (Segment<K,V>[]) new Segment[ssize];
//将该Segment模板实例放在Segment数组的第一个位置
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
第一个put方法是ConcurrentHashMap的添加元素方法,不允许Key和Value为空,并且添加成功会将该元素的Value返回。首先是通过元素Key计算其在Segment数组中的位置索引,然后通过ensureSegmetn方法获取该索引上的Segment实例(如果该索引位置上为空,则会根据Segment模板创建一个Segment实例),最后调用该Segment中的put方法将元素放入指定HashEntry中。
第二个put方法是Segment的添加元素方法,onlyIfAbsent形参用来判断当含有相同元素时是否替换旧值。因为Segment类继承了ReentrantLock类,所以lock或tryLock方法锁住的是当前Segment,从而实现了分段锁效果,大大提高了效率。方法开始时会尝试获取锁,在
注:计算元素在Segment数组中的索引时使用的是其Key哈希值的高位截取,而随后计算元素在HashEntry数组中的索引时使用的是其Key哈希值的低位截取。
//添加元素,ConcurrentHashMap类中的put方法
public V put(K key, V value) {
//元素所在段
Segment<K,V> s;
//值的非空判断
if (value == null)
throw new NullPointerException();
//计算元素Key的哈希值,使用的是ConcurrentHashMap特有的hash方法
int hash = hash(key);
/**
* 计算该元素在Segment数组中的位置索引
* 这里计算索引使用的是高位的Key哈希值
*/
int j = (hash >>> segmentShift) & segmentMask;
//获取该索引位置上的Segment,如果为空就通过ensureSegment方法创建一个新的Segment
if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
//将元素放入Segment的指定HashEntry中,同时返回元素的Value
return s.put(key, hash, value, false);
}
//返回一个Segment,如果不存在就根据模板创建一个
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE;
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
/**
* 从Segment数组0索引位获取模板
* 根据该模板创建其他Segment以及Segment中的HashEntry
*/
Segment<K,V> proto = ss[0];
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int) (cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[]) new HashEntry[cap];
if ((seg = (Segment<K,V>) UNSAFE.getObjectVolatile(ss, u)) == null) {
//根据模板创建的Segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>) UNSAFE.getObjectVolatile(ss, u)) == null) {
//将新创建的Segment放到指定索引位置
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
//添加元素,Segemnt类中的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
/**
* 先创建一个用于存储元素的HashEntry,同时还会进行对当前Segment进行加锁
* 当能立即获取到锁时HashEntry会被设置为null,如果不能立即获取到锁(有其他线程占用锁)则会进入scanAndLockForPut方法
*/
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
/**
* 计算该元素在Segment数组中的位置索引
* 这里计算索引使用的是高位的Key哈希值
*/
int index = (tab.length - 1) & hash;
//获取指定索引位置上的头元素
HashEntry<K,V> first = entryAt(tab, index);
/**
* 遍历寻找是否有相同元素
* 如果没有就添加元素,反之则替换旧元素(当onlyIfAbsent为false时)
*/
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
//通过Key值判断元素是否相同
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
//这里是创建一个存有新元素的HashEntry,并且将指定索引位置上的旧值接到该HashEntry后面(头插法)
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
//元素数量加1
int c = count + 1;
//扩容判断,如果容量够就直接将该HashEntry放入指定索引位置
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//扩容方法
rehash(node);
else
//将HashEntry设置到指定索引位置
setEntryAt(tab, index, node);
++modCount;
count = c;
//非旧值替换,方法则返回null
oldValue = null;
break;
}
}
} finally {
//对当前Segment进行解锁
unlock();
}
return oldValue;
}
//该方法中实际是使用了自旋锁
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//获取指定索引位置上的头元素
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
//重试次数
int retries = -1;
/**
* 这个循环会不断尝试获取锁(自旋锁),如果拿到锁就顺势结束方法
* 但是也有一个尝试次数MAX_SCAN_RETRIES,当是多核CPU时该值为64,当是单核CPU时该值为1
* 根据方法注释中说明,while中的代码其实是为了减慢每次循环的执行速度
*/
while (!tryLock()) {
HashEntry<K,V> f;
/**
* 在刚开始自旋时会遍历一次该索引位置上的所有元素
* 如果找到相同元素就无需创建新的HashEntry,反之则会创建
*/
if (retries < 0) {
if (e == null) {
if (node == null)
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
//当重试次数到达上限时就不再使用自旋锁,而是转而使用互斥锁
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
/**
* 每两次自旋(当retries为偶数时)会进行一次检查
* 检查是否有别的线程添加了新元素,正因为使用的是头插法,所以只需比较头元素是否发生变化即可
* 如果添加了新元素则又要遍历一遍该索引位置上的所有元素
*/
else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
return node;
}
扩容操作是在添加元素过程中,当创建完新的HashEntry后,拿新的元素数量count与阈值threshold进行比较,如果没超过阈值就将该HashEntry放入HashEntry数组中,反之则调用rehash方法进行扩容。ConcurrentHashMap中是扩容为原有容量的两倍,并且只是扩容当前Segment中的HashEntry数组,Segment数组在初始化容量就不会再变动了,扩容后会重新计算每个元素的位置索引。其中在移动元素时有一个优化,会把链表中最末尾的具有相同索引的并且连续的元素序列整体移动到新数组的指定索引位置上(事实上只移动序列的头元素即可),最后再依次移动链表剩余元素。
//扩容方法
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
//扩容为原来的两倍
int newCapacity = oldCapacity << 1;
//计算新阈值
threshold = (int) (newCapacity * loadFactor);
//创建一个扩容后的新HashEntry数组
HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
//遍历旧HashEntry数组中的每个元素
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
//计算元素的新位置索引
int idx = e.hash & sizeMask;
/**
* 如果当前索引位置上只有一个元素,则直接将该元素放到新位置
* 如果不止一个元素就表示是一个链表,需要遍历该链表
*/
if (next == null)
//单元素
newTable[idx] = e;
else {
//链表
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
/**
* 取链表中最末尾的位置索引相同的连续的元素序列,因为序列中元素的索引相同,所以只需要移动序列的头元素即可,并且因为是链表最末尾的序列,也无需担心会影响到其他元素
* lastIdx:相同索引序列的元素对应的索引
* lastRun:相同索引序列的头元素
*/
for (HashEntry<K,V> last = next; last != null; last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//直接将序列的头元素移动至新位置
newTable[lastIdx] = lastRun;
//将该索引位置上剩余的元素依次重新计算位置
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
//同时也需要对新添加的HashEntry进行rehash
int nodeIndex = node.hash & sizeMask;
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}