zoukankan      html  css  js  c++  java
  • hashMap底层的实现原理

    1.hashMap底层实现原理

     可以访问这篇文档   --->传送门

    2.hashMap是怎样取值和设置

    HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象

    当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

    3.HashMap和Hashtable的区别

    HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。

    主要的区别有线程安全性同步(synchronization),以及速度

    HashMap非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。

    • HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
    • 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
    • HashMap不能保证随着时间的推移Map中的元素次序是不变的。

    4.HashMap和HashSet的区别

    1.什么是HashSet

    HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。

    public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。

    2.什么是HashMap

    HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现HashMap和TreeMapTreeMap保存了对象的排列次序,而HashMap则不能HashMap允许键和值为null。HashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。

    public Object put(Object Key,Object value)方法用来将元素添加到map中

    5.HashMap基础

    HashMap继承了AbstractMap类,实现了Map,Cloneable,Serializable接口

    HashMap的容量,默认是16

    /**
    * The default initial capacity - MUST be a power of two.
    */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    HashMap的加载因子,默认是0.75
    
    /**
    * The load factor used when none specified in constructor.
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    当HashMap中元素数超过容量*加载因子时,HashMap会进行扩容。

    5.1HashMap实现原理

    Node和Node链
    首先来了解一下HashMap中的元素类型

    HashMap类中的元素是Node类,翻译过来就是节点,是定义在HashMap中的一个内部类,实现了Map.Entry接口

    Node类的定义如下

    /**
    * Basic hash bin node, used for most entries. (See below for
    * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
    */
    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;
    }
    }

    可以看到,Node类的基本属性有:

    hash:key的哈希值

    key:节点的key,类型和定义HashMap时的key相同

    value:节点的value,类型和定义HashMap时的value相同

    next:该节点的下一节点

    值得注意的是其中的next属性记录的是下一个节点本身,也是一个Node节点,这个Node节点也有next属性,记录了下一个节点,于是,只要不断的调用Node.next.next.next……,就可以得到:

     Node-->下个Node-->下下个Node……-->null

    这样的一个链表结构,而对于一个HashMap来说,只要明确记录每个链表的第一个节点,就能顺序遍历链表上的所有节点

    拉链法
    HashMap使用拉链法管理其中的每个节点。

    由Node节点组成链表之后,HashMap定义了一个Node数组:

    transient Node<K,V>[] table;

    这个数组记录了每个链表的第一个节点,于是最终形成了HashMap下面这样的数据结构:

                                                       

    这种数组+链表的数据结构,使得HashMap可以较为高效的管理每一个节点。

    关于Node数组 table
    对于table的理解,对后面关于扩容的理解很有帮助。

    table在第一次往HashMap中put元素的时候初始化

    如果HashMap初始化的时候没有指定容量,那么初始化table的时候会使用默认的DEFAULT_INITIAL_CAPACITY参数,也就是16,作为table初始化时的长度

    如果HashMap初始化的时候指定了容量,HashMap会把这个容量修改为2的倍数,然后创建对应长度的table

    table在HashMap扩容的时候,长度会翻倍。

    所以table的长度肯定是2的倍数

    修改容量的方法是这样的:

        /**
         * Returns a power of two size for the given target capacity.
         */
        static final int tableSizeFor(int cap) {
            int n = cap - 1;
            n |= n >>> 1;
            n |= n >>> 2;
            n |= n >>> 4;
            n |= n >>> 8;
            n |= n >>> 16;
            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
        }


    所以要注意,如果要往HashMap中放1000个元素,又不想让HashMap不停的扩容,最好一开始就把容量设为2048,设为1024不行,因为元素添加到七百多的时候还是会扩容。

    散列算法
    当调用HashMap.put()方法时,经历了以下步骤:

    1,对key进行hash值计算

        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    2,hash值和table.length取模

    取模的方法是(table.length - 1) & hash,算法直接舍弃了二进制hash值在table.length以上的位,因为那些位都代表table.length的2的n次方倍数。

    取模的结果就是Node将要放入table的下标。

    比如,一个Node的hash值是5,table长度是4,那么取余的结果是1,也就是说,这个Node将被放入table[1]所代表的链表(table[1]本身指向的是链表的第一个节点)。

    3,添加元素

    如果此时table的对应位置没有任何元素,也就是table[i]=null,那么就直接把Node放入table[i]的位置,并且这个Node的next==null。

    果此时table对应位置是一个Node,说明对应的位置已经保存了一个Node链表,则需要遍历链表,如果发现相同hash值则替换Node节点,如果没有相同hash值,则把新的Node插入链表的末端,作为之前末端Node的next,同时新Node的next==null。

    如果此时table对应位置是一个TreeNode,说明链表被转换成了红黑树,则根据hash值向红黑树中添加或替换TreeNode。(JDK1.8)

    4,如果添加元素之后,Node链表的节点数超过了8个,则该链表会考虑转为红黑树。(JDK1.8)

    5,如果添加元素之后,HashMap总节点数超过了阈值,则HashMap会进行扩容。

    相关代码是这样的:

      

      public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)                       //注释1
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))   //注释2
                    e = p;
                else if (p instanceof TreeNode)                        //注释3
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    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);               //注释4
                            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;
            if (++size > threshold)                 //注释5
                resize();
            afterNodeInsertion(evict);
            return null;
        }


    代码解析:

    1,注释1,table对应位置无节点,则创建新的Node节点放入对应位置。

    2,注释2,table对应位置有节点,如果hash值匹配,则替换。

    3,注释3,table对应位置有节点,如果table对应位置已经是一个TreeNode,不再是Node,也就说,table对应位置是TreeNode,表示已经从链表转换成了红黑树,则执行插入红黑树节点的逻辑。

    4,注释4,table对应位置有节点,且节点是Node(链表状态,不是红黑树),链表中节点数量大于TREEIFY_THRESHOLD,则考虑变为红黑树。实际上不一定真的立刻就变,table短的时候扩容一下也能解决问题,后面的代码会提到。

    5,注释5,HashMap中节点个数大于threshold,会进行扩容。

    HashMap扩容机制 -----resize()

    什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值(念yu值四声)---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

    扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

    先看一下什么时候,resize();

    /** 
     * HashMap 添加节点 
     * 
     * @param hash        当前key生成的hashcode 
     * @param key         要添加到 HashMap 的key 
     * @param value       要添加到 HashMap 的value 
     * @param bucketIndex 桶,也就是这个要添加 HashMap 里的这个数据对应到数组的位置下标 
     */  
    void addEntry(int hash, K key, V value, int bucketIndex) {  
        //size:The number of key-value mappings contained in this map.  
        //threshold:The next size value at which to resize (capacity * load factor)  
        //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值  
        //             2.底层数组的bucketIndex坐标处不等于null  
        if ((size >= threshold) && (null != table[bucketIndex])) {  
            resize(2 * table.length);//扩容之后,数组长度变了  
            hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢?  
            bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。  
        }  
        createEntry(hash, key, value, bucketIndex);  
    }  
      
    /** 
     * 这地方就是链表出现的地方,有2种情况 
     * 1,原来的桶bucketIndex处是没值的,那么就不会有链表出来啦 
     * 2,原来这地方有值,那么根据Entry的构造函数,把新传进来的key-value mapping放在数组上,原来的就挂在这个新来的next属性上了 
     */  
    void createEntry(int hash, K key, V value, int bucketIndex) {  
        HashMap.Entry<K, V> e = table[bucketIndex];  
        table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);  
        size++;  
    }

    我们分析下resize的源码,鉴于JDK1.8融入了红黑树,较复杂,为了便于理解我们仍然使用JDK1.7的代码,好理解一些,本质上区别不大,具体区别后文再说

    void resize(int newCapacity) {   //传入新的容量
            Entry[] oldTable = table;    //引用扩容前的Entry数组
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了
                threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
                return;
            }
     
            Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
            transfer(newTable);                         //!!将数据转移到新的Entry数组里
            table = newTable;                           //HashMap的table属性引用新的Entry数组
            threshold = (int) (newCapacity * loadFactor);//修改阈值
        }

    代码中可以看到,如果原有table长度已经达到了上限,就不再扩容了。

    果还未达到上限,则创建一个新的table,并调用transfer方法:

    这里就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。

    void transfer(Entry[] newTable) {
            Entry[] src = table;                   //src引用了旧的Entry数组
            int newCapacity = newTable.length;
            for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
                Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素
                if (e != null) {
                    src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
                    do {
                        Entry<K, V> next = e.next;             //注释1
                        int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置    //注释2
                        e.next = newTable[i]; //标记[1]            //注释3
                        newTable[i] = e;      //将元素放在数组上        //注释4
                        e = next;             //访问下一个Entry链上的元素   //注释5
                    } while (e != null);
                }
            }
        }
    static int indexFor(int h, int length) {
            return h & (length - 1);
        }

    transfer方法的作用是把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点。

    其中的while循环描述了头插法的过程,这个逻辑有点绕,下面举个例子来解析一下这段代码。

    假设原有table记录的某个链表,比如table[1]=3,链表为3-->5-->7,那么处理流程为:

    1,注释1:记录e.next的值。开始时e是table[1],所以e==3,e.next==5,那么此时next==5。

     

     

    2,注释2,计算e在newTable中的节点。为了展示头插法的倒序结果,这里假设e再次散列到了newTable[1]的链表中。

    3,注释3,把newTable [1]赋值给e.next。因为newTable是新建的,所以newTable[1]==null,所以此时3.next==null。

     

     

    4,注释4,e赋值给newTable[1]。此时newTable[1]=3。

     

     

    5,注释5,next赋值给e。此时e==5。

     

     

    此时newTable[1]中添加了第一个Node节点3,下面进入第二次循环,第二次循环开始时e==5。


    原文链接:

  • 相关阅读:
    sourceTree和eclipse 的使用
    oracle习题练习
    oracle详解
    单例模式
    反射详解
    Oracle 存储过程判断语句正确写法和时间查询方法
    MVC4 Jqgrid设计与实现
    遇到不支持的 Oracle 数据类型 USERDEFINED
    ArcGIS Server10.1 动态图层服务
    VS2010连接Oracle配置
  • 原文地址:https://www.cnblogs.com/JonaLin/p/12662226.html
Copyright © 2011-2022 走看看