这篇文章,是笔者学习hash源码的笔记,写作的过程,利于知识梳理,找到盲区。将会分三篇文章。
这是第一篇,讲解hashtable扩容。虽然hashtable已经被ConcurrentHashMap取代了,但是源码简单,利于我们理解hash的实现方式。
先看hashtable的结构图
hashtable表结构。数组+元素节点是单链表。
默认数组长度是11,负载因子是0.75
/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
*/
public Hashtable() {
this(11, 0.75f);
}
可以手动指定。为什么hashMap的初始化是16,hashtable是11?原因是hashmap做了优化,后面hashMap篇讲解。
HashTable扩容,元素数量达到阈值。阈值的计算。数组长度 * 负载因子
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
默认的hashtable,当插入第9个元素时候,会扩容。测试代码如下
public class HashTableTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Hashtable<String, Integer> table = new Hashtable<>();
table.put("0", 112);
table.put("1", 1);
table.put("2", 1);
table.put("3", 1);
table.put("4", 1);
table.put("5", 1);
table.put("6", 1);
table.put("7", 1);
// table.put("8", 1);
log.info("" + ((Map.Entry[])(getValue(Hashtable.class, table, "table"))).length);
int c = (int)Math.min(11f * 0.75f, 100.0f);
log.info("" + c);
}
private static Object getValue(Class c1, Object o, String field) throws NoSuchFieldException, IllegalAccessException {
Field field1 = c1.getDeclaredField(field);
field1.setAccessible(true);
return field1.get(o);
}
}
18:00:55.288 [main] INFO com.austin.daily.hashMap.HashTableTest - 11
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Hashtable<String, Integer> table = new Hashtable<>();
table.put("0", 112);
table.put("1", 1);
table.put("2", 1);
table.put("3", 1);
table.put("4", 1);
table.put("5", 1);
table.put("6", 1);
table.put("7", 1);
table.put("8", 1);
log.info("" + ((Map.Entry[])(getValue(Hashtable.class, table, "table"))).length);
}
INFO com.austin.daily.hashMap.HashTableTest - 23
扩容规则是什么呢?
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
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++;
}
添加Entry前,先判断达到threshold,核心代码在rehash()
protected void rehash() {
//记录原有hash表的长度
int oldCapacity = table.length;
//追踪引用原有hash表对象
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//计算新数组长度,old乘以2 +1. 所以 11 * 2 +1 = 23
int newCapacity = (oldCapacity << 1) + 1;
//防止超过JVM规定的数组最大长度
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
//创建一个新hash数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//与并发操作锁有关系,待后续了解
modCount++;
//计算新的阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//替换为新的数组
table = newMap;
//核心中的核心 循环old hash表的所有element(element是单链表的head)
for (int i = oldCapacity ; i-- > 0 ;) {
//利用单链表的特性,循环到链尾
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
//记录当前element
Entry<K,V> e = old;
//下一次循环用当前元素的next
old = old.next;
//计算element的hashcode,将正负位转为0,保证是正数,再取模,得到新hash表的索引位置
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//两行要理清楚。当前element的next指向该索引的值。目的是当前元素成为该索引下单链表的head。经过循环,当前element都会作为head插入到单链表中。头插法
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
每行代码都有注释。至此。已经解析完扩容机制。
我注意到。put()方法是头插法,rehash()也是头插法。会导致rehash后,插入顺序会逆转。下面是代码验证
@Slf4j
public class HashTableTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// Hashtable<String, Integer> table = new Hashtable<>();
// table.put("0", 112);
// table.put("1", 1);
// table.put("2", 1);
// table.put("3", 1);
// table.put("4", 1);
// table.put("5", 1);
// table.put("6", 1);
// table.put("7", 1);
// table.put("8", 1);
// log.info("" + ((Map.Entry[])(getValue(Hashtable.class, table, "table"))).length);
// int c = (int)Math.min(11f * 0.75f, 100.0f);
// log.info("" + c);
Hashtable<Kk, Integer> table = new Hashtable<>();
table.put(new Kk(), 0);
table.put(new Kk(), 1);
table.put(new Kk(), 2);
table.put(new Kk(), 3);
table.put(new Kk(), 4);
table.put(new Kk(), 5);
table.put(new Kk(), 6);
table.put(new Kk(), 7);
// table.put(new Kk(), 8);
Map.Entry[] entries = (Map.Entry[])getValue(Hashtable.class, table, "table");
Map.Entry entry = entries[1];
while (entry != null) {
System.out.print(getValue(entry.getClass(), entry, "value") + ",");
entry = (Map.Entry)getValue(entry.getClass(), entry, "next");
}
System.out.println("");
System.out.println("扩容后");
//扩容后
table.put(new Kk(), 8);
entries = (Map.Entry[])getValue(Hashtable.class, table, "table");
entry = entries[1];
while (entry != null) {
System.out.print(getValue(entry.getClass(), entry, "value") + ",");
entry = (Map.Entry)getValue(entry.getClass(), entry, "next");
}
}
private static Object getValue(Class c1, Object o, String field) throws NoSuchFieldException, IllegalAccessException {
Field field1 = c1.getDeclaredField(field);
field1.setAccessible(true);
return field1.get(o);
}
}
class Kk {
@Override
public int hashCode() {
return 1;
}
}
7,6,5,4,3,2,1,0,
扩容后
8,0,1,2,3,4,5,6,7,
Process finished with exit code 0
创建类Kk,覆盖hashCode,使得element一定会在数组索引1。打印的是table[1]的单链表元素顺序。
观察扩容前,头插法,所以在前面;
扩容中,验证后,果然是和倒置插入顺序。8在最前面的原因是,会先处理原有element,再使用头插法。