HashMap原理,源码分析

本文主要结合自己的实际应用和源码的解析,来分析HashMap的实现原理。
主要分析内容

  HashMap的散列分布逻辑,hash碰撞的处理,扩容的触发,树化等内容。
行文顺序

  1.结合源码,对基本的属性,内容分析
  2.结合实例对照源码

下面首先讲一下基本的属性和原理:

//默认容量 16
//当首次put()操作时,会调用resize(),初始化table数组,(table)数组
//下面会有介绍。数组的长度就是这个默认参数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//这是一个系数,只要知道触发扩容的条件是:
//使用容量 > 当前总容量 * DEFAULT_LOAD_FACTOR 
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//触发树化的条件,起初当存在相同hash阵列到同一个table数组的位置i上
//那么在这个位置上以链表的形式储存,当大于这个长度,则转成树
static final int TREEIFY_THRESHOLD = 8;
//这既是HashMap的真身,其实就是一个数组,元素是Node类。
//Node是一个内部类,下面会有介绍
transient Node<K,V>[] table;


//Node<K,V>类
//这个类其实就是HashMap元素储存的基本形式
//而且Node可以形成链表,以链表的形式存储在table的第i个位置
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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

然后我们再来看一下put()操作的逻辑

当put()操作时要处理hash碰撞,扩容,树化等可能出现的情景。也是本文分析的核心。

//put 操作
public V put(K key, V value) {
       //调用putValue 方法      
        return putVal(hash(key), key, value, false, true);
}

//hash方法,可以忽略
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//核心方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //一般来说,首次put会进入这个if条件,然后调用resize方法
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 根据hash 散列到table数组上,如果发现数组的该位置上并没有
        // 填充对象,则直接新建Node,填充 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
         // 这里就要处理,当hash值相同,散列到table数组的同一位置时的
         // 处理逻辑。
            Node<K,V> e; K k;
         //好吧,就算你hash值一样,也不一定要你插在我的后面
         // 除非既不 == 也不 equals。不然只能把当前节点替换掉
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果在table的第i位置出,延伸出去的链表已经转成树了。
            // 则执行树的插入操作
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
              // 然后,这一块主要是往table的第i位置,延伸出的链表最后
              // 插入新的节点的。通过for循环,达到链表末尾
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表的长度已经达到树化的阀值,则树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
       // 如果以储存的容量  >  threshold 的话,则触发扩容
       // threshold = 当前容量 * DEFAULT_LOAD_FACTOR (上面参数介绍过)
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  

上面一些基本的属相和方法已经介绍了。

然后介绍一下HashMap的储存结构:

首先上面已经介绍了Node<K,V>类是HashMap储存的基本结构类型。

HashMap其实就是一个Node类型的数组。

储存过程:

1.先分配容量

2.根据hash值阵列

  2.1.当hash阵列计算到数组的位置 n 时,如果 table[n]初未储存对象( table[n] == null),则直接new Node<K,V>储存

  2.2.如果 table[n]储存对象(table[n] != null)。这种情况下要根绝 K 的 hashCode 和equals方法,判断是跟新当前节点,还是在最后插入。

    如果出入的情况,判断是否达到树化阀值。如果达到则树化。如果未达到,则在链表最后出入。

3.每次插入后判断容量,是否进行扩容

然后具体的简单实例:

通过重写hashCode 和 equals 方法,来产生hash碰撞的情况,然后触发树化

package temp;

import java.util.HashMap;
import java.util.Map;

public class HashMapDemo {
    public static void main(String[] args) {
        Map<MyValue,Object> hashMap = new HashMap<MyValue, Object>();
        hashMap.put(new MyValue("one"),"first");
        hashMap.put(new MyValue("two"),"secend");
        for(int i=0;i<18;i++){
            hashMap.put(new MyValue("dump"),"dump");
        }
    }

    private static class MyValue{
        String value;
        public MyValue(String value){
            this.value = value;
        }
        // 重写,hash散列到 table[n]上
        public int hashCode(){
            return value.hashCode();
        }
        // 还记的putVal的判断方法么,只要散列到同一个位置
        // 而且通过equals 返回true,才会去追加
        public boolean equals(MyValue v){
            return false;
        }

        public String toString(){
            return value;
        }
    }
}

这里只是介绍了一些基本的核心和原理。
当然还有扩容方法 resize(), 树化方法  treeifyBin(),移除节点方法 removeNode()

可以参照源码看一下具体的实现逻辑。

好了,就到这了。

猜你喜欢

转载自www.cnblogs.com/justenjoy/p/8920702.html