Hashtable扩容,源码阅读

这篇文章,是笔者学习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,再使用头插法。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值