• HashMap原理,源码分析


    转载请注明出处  http://www.cnblogs.com/justenjoy/p/8920702.html

    本文主要结合自己的实际应用和源码的解析,来分析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()

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

    好了,就到这了。

  • 相关阅读:
    div里面的内容超出自身高度时,显示省略号
    CSS文本超出2行就隐藏并且显示省略号
    CSS中可以和不可以继承的属性
    return false
    CSS position: absolute、relative定位问题详解
    逆FizzBuzz问题求最短序列
    HTTP协议篇(一):多路复用、数据流
    PHP正则式PCRE
    Docker笔记三:基于LVS DR模式构建WEB服务集群
    架构设计之防止或缓解雪崩效应
  • 原文地址:https://www.cnblogs.com/justenjoy/p/8920702.html
Copyright © 2020-2023  润新知