zoukankan      html  css  js  c++  java
  • Java基础知识强化之集合框架笔记79:HashMap的实现原理

    1. HashMap的实现原理之 HashMap数据结构

    HashMap是对数据结构中哈希表(Hash Table)的实现, Hash表又叫散列表。Hash表是根据关键码Key来访问其对应的值Value的数据结构。

    它通过一个映射函数把关键码Key映射到Hash表中一个位置来访问该位置的值Value,从而加快查找的速度。这个映射函数叫做Hash函数存放记录的数组叫做Hash表

    在Java中,HashMap的内部实现结合了链表和数组的优势,链接节点的数据结构是Entry<k,v>,每个Entry对象的内部又含有指向下一个Entry类型对象的引用,如以下代码所示:

    1 static class Entry<K,V> implements Map.Entry<K,V> {  
    2       final K key;  
    3       V value;  
    4       Entry<K,V> next; //Entry类型内部有一个自己类型的引用,指向下一个Entry  
    5       final int hash;   
    6       ...
    7 }  

    哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法--- 拉链法,我们可以理解为"链表的数组" ,如图:

    2. HashMap的实现原理之 HashMap的存取实现

     既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现:

    1 // 存储时:
    2 int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值
    3 int index = hash % Entry[].length;
    4 Entry[index] = value;
    5 
    6 // 取值时:
    7 int hash = key.hashCode();
    8 int index = hash % Entry[].length;
    9 return Entry[index];

    (1)put

    疑问:如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?

      这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素,HashMap同一index下使用头插法(每次插入数据,从链头部插入)

    到这里为止,HashMap的大致实现,我们应该已经清楚了。

     public V put(K key, V value) {
            if (key == null)
                return putForNullKey(value); //null总是放在数组的第一个链表中
            int hash = hash(key.hashCode());
            int i = indexFor(hash, table.length);
            //遍历链表
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                //如果key在链表中已存在,则替换为新value
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            modCount++;
            addEntry(hash, key, value, i);
            return null;
        }
    
     
    
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next
    //如果size超过threshold,则扩充table大小。再散列
        if (size++ >= threshold)
                resize(2 * table.length);
    }

      当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,会不会影响性能?

    回答:会影响性能,HashMap里面设置一个因子,随着map的size越来越大,Entry[](对应index的链表,每个元素都是Entry)会以一定的规则加长长度。

    (2)get

     public V get(Object key) {
            if (key == null)
                return getForNullKey();
            int hash = hash(key.hashCode());
            //先定位到数组元素,再遍历该元素处的链表
            for (Entry<K,V> e = table[indexFor(hash, table.length)];
                 e != null;
                 e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                    return e.value;
            }
            return null;
    }

    (3)null key 的存取

    null key总是存放在Entry[]数组的第一个元素

      private V putForNullKey(V value) {
            for (Entry<K,V> e = table[0]; e != null; e = e.next) {
                if (e.key == null) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            modCount++;
            addEntry(0, null, value, 0);
            return null;
        }
     
        private V getForNullKey() {
            for (Entry<K,V> e = table[0]; e != null; e = e.next) {
                if (e.key == null)
                    return e.value;
            }
            return null;
        }

    (4)确定数组的index:hashcode % table.length取模

    HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:

     /**
         * Returns index for hash code h.
         */
        static int indexFor(int h, int length) {
            return h & (length-1);
        }
    按位取并,作用上相当于取模mod或者取余%。
    这意味着数组下标相同并不表示hashCode相同
     
    (5)table(哈希表)初始大小
    public HashMap(int initialCapacity, float loadFactor) {
            .....
            // Find a power of 2 >= initialCapacity
            int capacity = 1;
            while (capacity < initialCapacity)
                capacity <<= 1;
    this.loadFactor = loadFactor;
            threshold = (int)(capacity * loadFactor);
            table = new Entry[capacity];
            init();
        }

    注意table初始大小并不是构造函数中的initialCapacity!!

    而是 >= initialCapacity的2的n次幂!!!!

    3. HashMap的实现原理之 解决hash冲突的办法

    1. 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
    2. 再哈希法
    3. 链地址法
    4. 建立一个公共溢出区

    Java中hashmap的解决办法就是采用的链地址法

    4. HashMap的实现原理之 哈希表rehash过程(扩容机制)

    当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

    当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。

    HashMap 类中包含3个和扩容相关的常量:

    DEFAULT_INITIAL_CAPACITY 是初始容量,默认是 2^4 = 16;

    MAXIMUM_CAPACITY是最大容量,默认是 2^30;

    DEFAULT_LOAD_FACTOR是增长因子,当占用率超过这个值时,就会触发扩容操作。

    DEFAULT_INITIAL_CAPACITY是table数组的容量,DEFAULT_LOAD_FACTOR则是为了最大程度避免哈希冲突,提高HashMap效率而设置的一个影响因子,将DEFAULT_LOAD_FACTOR乘以DEFAULT_INITIAL_CAPACITY就得到了一个阈值threshold,当HashMap的容量达到threshold时就需要进行扩容,这个时候就要进行ReHash操作了,可以看到下面addEntry函数的实现,当size达到threshold时会调用resize()函数进行扩容

    HashMap的默认扩容机制,是存储的key超过容量的75%时,容量翻番。其实,这些和有序无序没关系。

    比如:当前大小是16,当占用超过16*0.75=12时,就把容量扩充到16*2=32

    resize()方法的源码如下:

     1   /**
     2      * Rehashes the contents of this map into a new array with a
     3      * larger capacity.  This method is called automatically when the
     4      * number of keys in this map reaches its threshold.
     5      *
     6      * If current capacity is MAXIMUM_CAPACITY, this method does not
     7      * resize the map, but sets threshold to Integer.MAX_VALUE.
     8      * This has the effect of preventing future calls.
     9      *
    10      * @param newCapacity the new capacity, MUST be a power of two;
    11      *        must be greater than current capacity unless current
    12      *        capacity is MAXIMUM_CAPACITY (in which case value
    13      *        is irrelevant).
    14      */
    15     void resize(int newCapacity) {
    16         Entry[] oldTable = table;
    17         int oldCapacity = oldTable.length;
    18         if (oldCapacity == MAXIMUM_CAPACITY) {
    19             threshold = Integer.MAX_VALUE;
    20             return;
    21         }
    22         Entry[] newTable = new Entry[newCapacity];
    23         transfer(newTable);
    24         table = newTable;
    25         threshold = (int)(newCapacity * loadFactor);
    26     }
    27 
    28  
    29 
    30     /**
    31      * Transfers all entries from current table to newTable.
    32      */
    33     void transfer(Entry[] newTable) {
    34         Entry[] src = table;
    35         int newCapacity = newTable.length;
    36         for (int j = 0; j < src.length; j++) {
    37             Entry<K,V> e = src[j];
    38             if (e != null) {
    39                 src[j] = null;
    40                 do {
    41                     Entry<K,V> next = e.next;
    42                     //重新计算index
    43                     int i = indexFor(e.hash, newCapacity);
    44                     e.next = newTable[i];
    45                     newTable[i] = e;
    46                     e = next;
    47                 } while (e != null);
    48             }
    49         }
    50     }

    在扩容的过程中需要进行ReHash操作,而这是非常耗时的,在实际中应该尽量避免

  • 相关阅读:
    ps命令
    关于typedef的用法总结
    C#中正则表达式的使用
    调试与编译
    大端和小端
    64位程序内存之我看
    C/C++内存泄漏及检测
    内核中的 likely() 与 unlikely()
    do/while(0) c4127
    django+xadmin在线教育平台(六)
  • 原文地址:https://www.cnblogs.com/hebao0514/p/5704641.html
Copyright © 2011-2022 走看看