hashmap单向链表、红黑树、扩容

为了方便了解hashmap结构以及结构的转变过程抄了一段代码

class MapKey{
    private static final String REG = "[0-9]+";
    private String key;
    public MapKey(String key) {
        this.key = key;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        MapKey mapKey = (MapKey) o;
 
        return !(key != null ? !key.equals(mapKey.key) : mapKey.key != null);
 
    }
    /*
     * 确保每次key的hashCode都相同
     */
    @Override
    public int hashCode() {
        if (key == null)
            return 0;
        Pattern pattern = Pattern.compile(REG);
        if (pattern.matcher(key).matches())
            return 1;
        else
            return 2;
    }
    @Override
    public String toString() {
        return key;
    }
}

这个类重写了hashcode方法保证规则:数字的hash值是1,else为2,null的hash为0。实现hash碰撞。


 

观察hashmap的结构转变。

public static void main(String[] args){
            Map<MapKey,String> map = new HashMap<MapKey, String>();
            //第一阶段  
            for (int i = 0; i < 6; i++) {
                map.put(new MapKey(String.valueOf(i)), "A");
            }
            //第二阶段
            for (int i = 0; i < 10; i++) {
                map.put(new MapKey(String.valueOf(i)), "A");
            }
            //第三阶段
            for (int i = 0; i < 50; i++) {
                map.put(new MapKey(String.valueOf(i)), "A");
            }
            //第四阶段
            map.put(new MapKey("Z"), "B");
            map.put(new MapKey("J"), "B");
            map.put(new MapKey("F"), "B");
            System.out.println(map);
        }

每个阶段的hashmap结构在下面详细分析。




 

HashMap在jdk1.8数据结构为 数组+单向链表/红黑树。学习一下。

数组是 Node<K,V>[] 实际上是Entry数组table。

看源码、看结构。

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

上面代码初始化完成之后,构造方法初始化了加载因子。table数组还是空的。之后put值进去。。

上面是for循环运行第一次之后的变化,modeCount表示hashMap更改了一次,对应 i=0,时的数据key=0,value=A。size也变为了1,给定了默认的容量16。阈值threshold为12,具体 threshold = loadFactor*容量。而且线性表table也有值,默认容量为16。

put第一个值进去时,看一下HashMap put 方法源码(jdk1.8)。

put方法针对key进行了hash运算(hashmap中hash方法对其进行了高位运算,暂时先不看)调用了putVal方法,继续往下走,看putVal方法在第一次调用put进值。

putVal方法分了三种情况:

判断table是否为空,为则扩容;

②根据length对key的hash取模,计算当前key的桶中是否有为空,为空则创建;

③table不为空,当前key的桶不为空,为链表;

 

 

目前是put第一个值进入HashMap中,table为空,符合第一个,创建存储v的node。继续走进入resize方法。

resize() 方法有总的分为两部分:

①扩容;

②数据迁移;

扩容和数据迁移都需要进行相应条件的判断,遇到细看。目前(put进去第一个值)table是空的,除了加载因子其他所有属性都是空的。

当第一次初始化调用resize()方法时:(判断容量和阈值)

扩容:在resize方法中会给容量、阈值赋以默认值16、16*0.75=12。

数据迁移:容量为0不需要数据迁移。

回到putval方法中,根据容量判断当前key的hash桶(index)中是否为空,为空则创建。

 

 

上面是将key,value放到了table数组的第i个元素中。这里是通过(n-1)&hash运算确定下标的位置的,类似取模运算,n的值是2的次幂。完成put值。

继续往下执行

 

 

modCount自增1,modCount字段主要用来记录HashMap内部结构发生变化的次数,put新键值对,但是某个key对应的value值被覆盖不属于结构变化。在putval源码中就可以看到:在对key对应的value进行覆盖时,直接return不会到++modCount这一步。

 

 

我们知道java.util.HashMap不是线程安全的,因此在使用迭代器Iterator的过程中,如果有其他线程修改了map,将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现就是通过modCount,它记录修改次数,在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。所以遍历那些非线程安全的数据结构时,尽量使用迭代器Iterator。

根据size大小判断是否需要扩容,resize()方法。第一次put时不需要扩容直接返回。第一次put值进HashMap结束。


 

put第二个值进去时,看一下HashMap put 方法源码

此时在table[1]的位置有了Node<k,v>类型的值,容量是16阈值时12。进入hashmap源码看put方法:

①判断table为空,此时不为空跳过;

②判断当前key对应table[index]有值,不用创建新的Node跳过;

③进入else,判断当前key在HashMap中是否有重复的,有则覆盖。这里判断key值用的是hash 和 equals方法进行判断。此时没有重复的key跳过;

④判断是不是红黑树,此时不是跳过;

⑥确定是链表结构,for循环遍历:末尾插入,大于阈值则红黑树化,key重复则覆盖。

此时末尾插入,通过循环找到next指向为空的节点,将值插入到为空的next节点中,同时判断长度是否超过阈值,超过则树化。

 

 

 

 

table数组容量为16,以key的hash值为下标的。第一次for循环put进去的key是数字,上面重写hashcode的方法对数字返回的hash值为1。

table中存储的是Node,而Node中还又存了个Node类型的next。通过next指向下一个Node。所以table数组中存了类似于套娃的Node的单向链表。

看源码和结构样子。

static class Node<K,V> implements Map.Entry<K,V> {
        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;
        }

现在只有一个Node,指向下一个Node的next为空。

再执行一次for循环put值进去,下图

第一个node的next现在已经有值,指向第二个Node对象,而第二个Node的next没有指向任何Node。开始了套娃。

这样构成链表结构,结构下图:参考博客

 

套娃的单向链表就是这样。

HashMap中对于单向链表的套娃行为加以了限制,TREEIFY_THRESHOLD & MIN_TREEIFY_CAPACITY稍后细看。

随着put值的进行,学习一下HashMap的自动扩容,树化。扩容过程是通过resize方法进行的,但是现在还没看懂先看看过程。

阶段一for循环put进去了6个值,此时HashMap的结构还是数组加单向链表的套娃结构。如下图

等单向链表长度达到TREEIFY_THRESHOLD=8时,put值进去时并没有进行红黑树化,还是链表套娃结构。这是因为HashMap的容量没达到最小容量 MIN_TREEIFY_CAPACITY= 64。如下图

 

此时HashMap开始扩容(虽然容量没有达到阈值threshold,但是还是进行了扩容。可能是为了整治套娃行为(单向链表),努力达到条件 -->TREEIFY_THRESHOLD & MIN_TREEIFY_CAPACITY),直到容量达到64之前,一直都是单向链表的套娃结构。如图

等到容量达到64时再put进值时就会转为红黑树(此时链表长度早已超过8)。如图

此时扩容将会等达到阈值时才会扩容。如图

 

TODO红黑树还没懂

 

posted @ 2021-01-08 18:27  wyzbky  阅读(140)  评论(0)    收藏  举报