美文网首页
深度剖析HashMap(一)——基于JDK7

深度剖析HashMap(一)——基于JDK7

作者: Wangheguan | 来源:发表于2018-05-02 21:32 被阅读0次

    基于JDK7

    HashMap是每个Java/Android程序员必须掌握的一种容器。在这个专题下将分若干篇文章对其进行深度剖析。由于JDK版本的不同,HashMap的底层实现也有些许差别。本文先对基于JDK7的HashMap进行分析,之后会奉上JDK8中对HashMap实现改动的分析。

    一、HashMap结构概览

    在JDK7中,HashMap说白了就是用到两种数据结构——数组与链表。
    数组:
    需要连续的内存空间,寻址较快,增删元素较为复杂;
    链表:
    不需要连续的空间,寻址较慢,增删元素较为简便;
    HashMap:
    是数组和链表的复合型数据结构,对两者的特性进行了折中,形成了一种独特的数据存储风格。

    HashMap数据结构图
    左侧纵向的数组Entry[]可以看成是基底,每个数组索引称之为一个桶(Bucket),每个桶对应一个哈希值,具有相同哈希值的元素在同一个桶上发生冲突,散列成一个单链表。如果相同哈希值的元素太过,意味着链表过长,在查找元素时效率降低,HashMap将退化成链表。
    HashMap主要做的工作提供合理的计算哈希值的函数,且尽可能减少哈希冲突概率。这样就能使元素均匀散列在不同的bucket上,最大化体现复合型数据结构的优势。

    二、HashMap变量声明及构造函数

    ——主要变量声明

    //默认的初始容量为16,必须为2的n次幂
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    //最大容量,如果使用有参构造指定具体的容量大于最大容量,则使用最大容量。且必须是2的n次幂。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //键值对表,可根据需要调整大小,但长度必须为2的n次幂
    transient Entry<K,V>[] table;
    //此映射中包含的键值映射的数量
    transient int size;
    //容量阈值(一旦超过阈值就要扩容),等于 容量*负载因子
    int threshold;
    //记录修改次数
    transient int modCount
    //表示在对字符串键(即key为String类型)的HashMap应用替代哈希函数时HashMap的条目数量的默认阈值。替代哈希函数的使用可以减少由于对字符串键进行弱哈希码计算时的碰撞概率
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
    //如果useAltHashing为true,执行String类型键的替代哈希以减少由于哈希码计算较弱导致的冲突发生率。
     transient boolean useAltHashing;
    //与实例关联的随机值,用于哈希代码中,以减少冲突
    transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
    

    ——构造函数1 HashMap(int initialCapacity, float loadFactor)

    public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            // Find a power of 2 >= initialCapacity
            int capacity = 1;
            while (capacity < initialCapacity)
                capacity <<= 1;
            this.loadFactor = loadFactor;
            threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            table = new Entry[capacity];
            useAltHashing = sun.misc.VM.isBooted() &&
                    (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
            init();
        }
    

    解读分析:

    • 构造函数数量两个参数,分别是初始容量initialCapacity和负载因子loadFactor,若初始容量为负,或负载因子为负,或负载因子不是个数字将抛出异常;若初始容量大于最大容量,则初始容量等于最大容量;
    • 构造器内声明了一个中间变量capacity并初始化为1,若capacity小于自定义的初始容量,则会进入循环体执行左移一位的操作(即乘以2)直至capacity大于等于初始容量。这么做的意义在于,防止用户输入的初始容量不是2的n次幂
    • 容量阈值threshold取min(容量*负载因子,最大容量值+1)。
    • 初始化数组Entry[](即我们所说的bucket——桶),大小为capacity,表内元素为键值对Entry(Entry实现了Map.Entry接口),有如下4个属性:
      final K key;
      V value;
      Entry<K,V> next;
      int hash。
    • useAltHashing=.....计算是否对字符串键的HashMap使用备选哈希函数,其用法可见构造函数1。对于替代哈希函数下文会提及。
      说明:所谓的备选哈希函数是jdk7中对键值类型为String类型采用的内部hash算法。
    • 调用初始化方法init(),默认情况下什么也没做,因为默认的init()是一个空函数。
      ——构造函数2 HashMap(int initialCapacity)
    public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    

    ——构造函数3 HashMap()

    public HashMap() {
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
        }
    

    构造器2,3均调用构造器1,比较好理解。

    三、HashMap的私有静态内部类Holder与hash值计算的分析

    JDK解释中:class Holder用来容纳那些只有在虚拟机引导之后才会被初始化的值。
    这个类就是用来持有 ALTERNATIVE_HASHING_THRESHOLD的,影响String在极端情况下的hash值的计算,不影响HashMap基本的实现。
    先放源码看看吧。

    private static class Holder{
    // Unsafe mechanics
            /**
             * Unsafe utilities
             */
            static final sun.misc.Unsafe UNSAFE;
            /**
             * Offset of "final" hashSeed field we must set in readObject() method.
             */
            static final long HASHSEED_OFFSET;
            /**
             * Table capacity above which to switch to use alternative hashing.
             */
            static final int ALTERNATIVE_HASHING_THRESHOLD;
            static {
                String altThreshold = java.security.AccessController.doPrivileged(
                    new sun.security.action.GetPropertyAction(
                        "jdk.map.althashing.threshold"));
                int threshold;
                try {
                    threshold = (null != altThreshold)
                            ? Integer.parseInt(altThreshold)
                            : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
                    // disable alternative hashing if -1
                    if (threshold == -1) {
                        threshold = Integer.MAX_VALUE;
                    }
                    if (threshold < 0) {
                        throw new IllegalArgumentException("value must be positive integer.");
                    }
                } catch(IllegalArgumentException failed) {
                    throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
                }
                ALTERNATIVE_HASHING_THRESHOLD = threshold;
                try {
                    UNSAFE = sun.misc.Unsafe.getUnsafe();
                    HASHSEED_OFFSET = UNSAFE.objectFieldOffset(
                        HashMap.class.getDeclaredField("hashSeed"));
                } catch (NoSuchFieldException | SecurityException e) {
                    throw new Error("Failed to record hashSeed offset", e);
                }
            }
    }
    

    ——final int hash(Object k)

    final int hash(Object k) {
            int h = 0;
            if (useAltHashing) {
                if (k instanceof String) {
                    return sun.misc.Hashing.stringHash32((String) k);
                }
                h = hashSeed;
            }
    
            h ^= k.hashCode();
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
    

    (一)、解读JDK7对减少碰撞概率的一些优化

    我们都知道,要想往HashMap中添加元素首先要知道放到哪个位置,这个位置由哈希值决定。而哈希值的计算最重要的一点就是减少冲突,让元素尽可能的分散在不同的bucket中。

    在JDK7中,做了一点减少冲突概率的优化。我们之前提到一个布尔型变量——useAltHashing,这个变量很重要,在计算hash的过程中起到了一个判别的作用,让我们照着源码来看看。

    • 首先,useAltHashing=sun.misc.VM.isBooted() &&(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD),什么意思呢?若虚拟机开始激励引导,且当前容量capacity满足大于Holder.ALTERNATIVE_HASHING_THRESHOLD,那么useAltHashing为true,反之为false。
    • 再来,ALTERNATIVE_HASHING_THRESHOLD是一个保存Holder.threshold值的一个静态变量。threshold值与altThreshold有关,至于altThreshold 的由来笔者也不太清楚,不过不要紧,这部分不太需要深究。
    • Holder类对类内部的threshold做了个开关设置。若threshold == -1,那么threshold = Integer.MAX_VALUE,这就意味着ALTERNATIVE_HASHING_THRESHOLD=Integer.MAX_VALUE,因为capacity基本上是小于Integer.MAX_VALUE,所以变量useAltHashing 必定为false。
    • 若useAltHashing为true,对于String类型键,对其进行内部哈希运算,采用内部hash算法,也就是sun.misc.Hashing.stringHash32的 hash 算法。
      对于非String类型键,将用随机hashSeed和键值的hashCode()进行异或运算,也可以一定程度上减少碰撞的概率。
    • 若useAltHashing为false,便不做String类型键的优化,且hashSeed为0,理论上来讲,这时候的碰撞概率会高于useAltHashing为true的情况,不过正常情况下的配置已经够用了,JDK7增加的这部分优化只是针对极端情况。

    补充:之所以说capacity基本上是小于Integer.MAX_VALUE是因为Integer.MAX_VALUE=2的32次方-1,capacity最大值为2的32次方。

    (二)、关于hash(Object k)中位运算的解读

    在hash(Object k)中有这么一段位运算的代码:

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

    看起来既简单又深奥的样子,让我们来看看这段代码隐藏的东西吧。

    k.hashCode()函数调用的是key键值类型自带的哈希函数,由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。

    接下来的一串与运算和异或运算,称之为“扰动函数”,扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,增加其值的不确定性,从而降低冲突的概率。不同的版本实现的方式不一样,但其根本思想是一致的。
    这里的具体实现方式是如何保证的呢?笔者功力浅薄,暂时还没有答案,如果有朋友知道的话可以交流。但是,“扰动函数”的核心思想一定要明白。


    今天就先到这吧,HashMap需要深究的东西还是挺多的哈,明天继续撸。

    相关文章

      网友评论

          本文标题:深度剖析HashMap(一)——基于JDK7

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