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

基于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需要深究的东西还是挺多的哈,明天继续撸。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • HashMap 是 Java 面试必考的知识点,面试官从这个小知识点就可以了解我们对 Java 基础的掌握程度。网...
    野狗子嗷嗷嗷阅读 6,650评论 9 107
  • 一、取整 二、取绝对值 三、取余 四、求最大值和最小值 五、示例
    KnowWhy阅读 2,579评论 0 2
  • 作者:余华 我在情感上的愚钝就像是门窗紧闭的屋子,虽然爱情的脚步在屋前走过去又走过来,我也听到了,可是我觉得那是路...
    我的糖果屋阅读 1,790评论 0 0
  • 宁夏,有塞上江南的美誉,近些年,在全国也小有些名气。枸杞,羊肉,沙坡头,六盘山,七十二连湖……全面打造适宜创业,适...
    Life_53a9阅读 239评论 7 3
  • 今天下午4点,新巅峰教育的李娜老师和于淼妈妈来家访,看望了儿子和老婆,李娜老师主要是要了解一下儿子参加完《...
    原野_c00f阅读 365评论 0 2