美文网首页
25-HashMap

25-HashMap

作者: 鹏程1995 | 来源:发表于2020-02-04 15:31 被阅读0次

类介绍

类定位

HashMap是一个使用哈希方法存储元素实例的Map。在大多数情况下,HashMap有着比TreeMap更加优秀的效率,所以在我们的编程中,如果要用到MapMap<XX,XX> temp = new HashMap<>()几乎是下意识的代码。这篇文章主要介绍HashMap的工作原理,由于HashMap的原理较复杂,源码介绍无法一个方法一个方法的来,我们这次主要介绍HashMap的实现思想和类的架构,并对关键代码进行粘贴和注释。

类继承关系分析

1.png

可见,和TreeMap不同的是,HashMap没有实现SortedMapNabvigateMap的一些方法,而是专心的实现Map中预定义的方法。还有就是一些通用的东西:

  • 实现了Serialable,可以进行序列化和反序列化
  • 实现了Cloneable,可以进行浅拷贝

注意事项

在Java8 的HashMap源码中依然使用了红黑树的算法和思想,我们接下来会介绍。红黑树的要求和之前的TreeMap是一样的,只是实现略有不同,这里就不再详细解释了,如果想看可以去刷一下HashMapTreeNode源码。

算法介绍

本节主要通过流程图和结构图的方法对HashMap的工作原理进行介绍,以期读者能对HashMap的工作原理和演进有大概的理解。方便接下来对源码的理解。

哈希算法分析

散列函数(英语:Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。与指纹一样,散列算法就是一种以较短的信息来保证文件唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。因此,当原有文件发生改变时,其标志值也会发生改变,从而告诉文件使用者当前的文件已经不是你所需求的文件。

优秀hash算法的特点

一个优秀的 hash 算法,将能实现:

  • 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
  • 逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
  • 输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。
  • 冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。

常见的简单hash方法思路

哈希值计算方法,常见的方法有:

  • 加法hash
    • 所谓的加法Hash就是把输入元素一个一个的加起来构成最后的结果
    • 此处可以参考Collection系列类的哈希值,他就是把每个元素的哈希值加起来
  • 位运算hash
    • 这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素
    • HashMap使用的是位运算的方法计算哈希值,用数学的话描述是除数组长度取余
  • 乘法hash
    • 这样的类型的哈希函数利用了乘法的不相关性.乘法哈希里最有名的就是adler32,reactJS的checksum校验就是使用的adler32的改良版。
  • 除法hash
    • 和乘法一样用了不相关性,但性能不好。
  • 查表hash
  • 混合hash
    • 混合Hash算法利用了以上各种方式。

流行的hash算法

MD5,SHA 等算法

HahsMap实现结构及演进分析

我们以HashMap的插入为引入介绍哈希算法:

2.png

从流程图上容易看出:一个是哈希值计算方法、一个是解决位置冲突问题的方法。对HashMap的实现有着较大的影响。哈希值计算方法我们上面已经介绍过了。

  • 解决位置冲突的方法,常见的方法有:

  • 开放定址法【从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。】

    • 线性探查法【从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止】
  • 平方探查法【平方探查法即是发生冲突时,用发生冲突的单元d[i], 加上 1²、 2²等。即d[i] + 1²,d[i] + 2², d[i] + 3²...直到找到空闲单元】

    • 双散列函数探查法【这种方法使用一个双散列函数,有兴趣自己去搜】
  • 拉链法【链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。】【java7 就使用此方法,java8在7的基础上进行了改造

  • 再哈希法【就是同时构造多个不同的哈希函数,当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。】

  • 建立公共溢出区【将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区】

java7的HashMap

话不多说,先上图:

3.png

注意:图只是示例图,不要纠结阈值还有扩充数组长度的问题,这个我们后面会专门介绍。

java7 获得初始位置的方法

java7计算hash值的方法是元素hash值除以数组长度取余,也就是说假设数组长度是y,新来元素的hash值是x,则计算出的初始位置是X%Y。在源码中是使用位运算实现的:

4.png

通过图片我们对比可知,X%Y十进制取余和二进制X\&(Y-1)效果一样。

java7 解决冲突的方法

通过图片我们可以看出,java 7的HashMap解决冲突的方法是拉链法

java7 其他要注意的地方

在扩展数组空间后,同一个位置的链可能会有一部分被分到新的位置,java7的算法源码如下:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    // 新的数组
    Entry[] newTable = new Entry[newCapacity];
    // 将原来数组中的值迁移到新的更大的数组中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable){
        Entry[] src=table;
        int newCapacity=newTable.length;
        for(int j=0;j<src.length;j++){
            Entry<K, V> e=src[j];
            if(e!=null){
                src[j]=null;
                do{
                    Entry<K, V> next=e.next;//保存下一次循环的Entry
                    
                    //在新的table 中求得适合插入的位置
                    int i=indexFor(e.hash, newCapacity);
                    e.next=newTable[i];//  如果I位置原来没有值,则直接插入;有值,采用链头插入法
                    newTable[i]=e;
                    
                    //轮替,下一次循环
                    e=next;
                }while(e!=null);
            }
        }
    }

由于transfer是头插,所以插入后会出现元素顺序相反的情况:比如原来的链表中A在B前面,移动到新位置后A到B后面了。这种结构会有极端情况发生:

当多线程对HashMap进行操作时【当然这是不对的】,有可能会出现死循环而不是fail-fast机制抛出的异常。【具体分析自行搜索,java7的HashMap不是重点,不再做分析】

java8的HashMap

先上图:

5.png

java8中的HashMap主要对java7中的HashMap做了一些算法上的优化。优化点罗列如下:

  1. 极端情况下,所有的元素都挤到一个位置,查找的效率会从O(1)下降至O(n),所以java8在链较短时仍然使用链表,在链表较长时使用红黑树实现

  2. java7在拆分链表进行扩容时存在新链表元素逆序的情况,所以java8所有新生成的链表要求元素仍然保持原链表中的顺序

  3. java7在扩容时要重新计算元素的新位置,实现思路是仍然调用根据hash求下标的方法,java8找到其中的规律,简化了计算

    由于每次数组扩容都是变为二倍,元素要不是在原位置,要不是增加一个原数组的长度Y,我们依然从二进制的角度来介绍这个情况:

    6.png

    可见,数组扩容后其实就是(Y-1)的二进制位多了一个1,联想到网络的子网掩码也就是掩码长度多了1,所以决定元素在扩容后数组的位置是原长度的最高位对应的hash位是0还是1

  4. java8修改了根据hash求初始位置的方法,支持了Object作为key,同时修改了位运算的方法:

java8认为如果只看低n位会增加碰撞,所以引入高16位对低16位的影响

// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作  = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode(); 
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
 }

// JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode() 
// 2. 高位参与低位的运算:h ^ (h >>> 16)  
static final int hash(Object key) {
     int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// a. 当key = null时,hash值 = 0,所以HashMap的key 可为null      
// 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}

7.png

8.png

其实对HashMap来说,添加操作就是put(),它的意思是如果你传入的key已经有了,就是覆盖它,然后返回原值;如果你传入的keyMap中不存在或者对应的valuenull,就新建节点或者覆盖null,并返回null

当然也有putIfAbsent(),只有在没有该节点不存在时才添加,不覆盖。

数据结构都知道了,没啥说的,无非是hash查找、树查找、链表遍历结合起来而已。

源码介绍

源码介绍主要分成两种,一种是HashMap独有的主要的功能实现方法,包括某些内部类和增删方法;一种是常用的套路方法,包括惯有的一堆Iterator,Set,Collection之类的内部类,还有一些常见的get(),put()方法。

我们对第二种方法直接忽略,主要看HashMap的实现。同时,第一种中的红黑树相关的算法确实是有点复杂,因为这里的红黑树不仅要维持红黑树的相关性质,还要维持链表的next链接顺序,不能随意旋转。所以,我们不做过多介绍,毕竟红黑树掌握原理即可,不专门研究没必要全弄通。

内部类介绍

TreeNode

继承关系

9.png

至于这个奇妙的依赖关系,我也是醉了,这个应该是最开始设计时就有的问题:

  1. 父类的实现依赖子类中的内部类,这不符合java编程中的单向依赖的规范。
  2. 经查验,LinkedHashMap.Entry只增加了两个变量,方便双向查找;HashMap.TreeNode也用到了这个特点来方便进行树调整。LinkedHashMap.Entry代码及其简单,而且没有明显体现出LinkedHashMap的特点,将其调整至HashMap也并无大错,毕竟HashMap也依赖链表数据结构。

以上为个人观点,仅记录于此。

功能及角色

TreeNode中实现了红黑树的基本方法,包括红黑树的增、删、改、查、HashMap扩容时的分裂、链表改树、树改链表等操作。

代码太多太杂,这里不做粘贴解释。

增/改

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 计算出的地址已经元素,且此元素的key是和传入key相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                // 此位置的元素是一个树的,进行树的增/改
                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);
                        break;
                    }
                    // 在链表中找到了key和传入key相同的元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 在HashMap中查到了相同key,根据入参判断是否覆盖
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 没有相同key,HashMap中新增了元素,修改Count++,同时判断是否需要扩容
        ++modCount;
        if (++size > threshold)
            resize();
        // 可以理解为预留的插入完成的钩子,用户可以根据自己的需求进行复写,
        // 比如插入后要打印一条日志啥的,就在这个函数里编码实现
        afterNodeInsertion(evict);
        return null;
    }

    /**
     * Implements Map.remove and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        // 兴许有的删
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            // 找到了要删的元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                // 从树里面找要删的元素
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    // 从链表里面找要删的元素
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // 找到了要删的元素
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 在树里就用树的方法删
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 数组中hash计算出来的那个就是要删的元素
                else if (node == p)
                    tab[index] = node.next;
                // 从链表中找到了要删的元素
                else
                    p.next = node.next;
                // 删完做相应的善后
                ++modCount;
                --size;
                // 和增/删的那个一样,也是个钩子
                afterNodeRemoval(node);
                return node;
            }
        }
        // 没得删
        return null;
    }

不说了,太简单。

扩容


    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 现在的数组已经是最大的了,直接把阈值调整到MAX,以后不会再触发阈值,也不会再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 数组大小合法,可以扩充,扩充数组和阈值
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // 旧数组为空,但是阈值有初始化,用预设阈值为新数组长度进行初始化
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 啥都没设,用默认的数组、阈值初始化
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 旧数组为空,但是阈值有初始化;旧数组太小或者新数组太大
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 接下来进行空间申请和数据迁移
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // 旧位置置空
                    oldTab[j] = null;
                    // 单元素,直接转到新数组中
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 是树,用树的方法进行拆分和转移
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 是链表,根据每个元素的hash在保证链表元素顺序的前提下进行数据迁移
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

常量分析

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
  • 默认初始化数组长度:DEFAULT_INITIAL_CAPACITY
  • 最大数组长度:MAXIMUM_CAPACITY
  • 默认阈值占比:DEFAULT_LOAD_FACTOR
  • 链表转树长度:TREEIFY_THRESHOLD
  • 树降级成链表长度:UNTREEIFY_THRESHOLD
  • 链表转树前置条件。数组长度限制MIN_TREEIFY_CAPACITY

使HashMap工作效率更高效化的几个建议

  1. 合理的设置初始化空间和阈值比例

    • 初始化空间太大浪费内存、太小刚开始就得扩充Map
    • 阈值比例太大,HashMap碰撞严重,会影响搜索效率;阈值太小不仅会造成存储的浪费,频繁的扩容导致HashMap不停搬运数据,会占用很多的资源
  2. 如有需要,可以自己扩展HashMap,自己复写常用的钩子来进行定制化操作

参考文献

  1. https://zh.wikipedia.org/wiki/%E6%95%A3%E5%88%97%E5%87%BD%E6%95%B8
  2. https://blog.csdn.net/carson_ho/article/details/79373026
  3. https://blog.csdn.net/yangyingbofx/article/details/51347477
  4. https://blog.csdn.net/q291611265/article/details/46797557
  5. http://www.importnew.com/20386.html
  6. https://www.jianshu.com/p/bf1d7eee28d0

相关文章

  • 25-HashMap

    类介绍 类定位 HashMap是一个使用哈希方法存储元素实例的Map。在大多数情况下,HashMap有着比Tree...

网友评论

      本文标题:25-HashMap

      本文链接:https://www.haomeiwen.com/subject/vlirxhtx.html