JAVA HashMap原理

HashMap概述

Hash,又称散列。哈希表是一种以键-值(key-value) 存储数据的,和数组、链表、二叉树等同样典型的一种数据结构。Java中用HashMap来实现了哈希表这种数据结构。

内部实现

前言 - hashCode()和equals(obj)方法
java.lang.Object中的方法定义

/** JNI,调用底层其它语言实现 */  
public native int hashCode();  
  
/** 默认同==,直接比较对象 */  
public boolean equals(Object obj) {  
    return (this == obj);  
}  

hashCode是Object类中的方法,因此所有Java对象都有hashCode方法。当类的对象用作HashMap这类哈希结构的key值时,它的返回值用来支撑Hash算法的计算。其它时候,hashCode并没有什么作用。所以很多情况下我们都不需要重写hashCode方法,而Object类中将它定义为native方法。

equals也是Object类中的方法,默认情况下equals比较的是两个对象的引用是否相同,如果要将类的对象用作HashMap的key值,我们一般会重写equals方法。Integer, String等基本类型都已经重写了equals方法,所以我们可以很方便的将它们用作hash的key。

  • 底层结构
transient Entry<K,V>[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

HashMap底层是以数组+链表+红黑树(jdk1.8增加了红黑树,这里暂时讨论1.7以前版本)来存储K/V数据的。Entry[] 就是一个K/V键值对数组,通常也叫bucket(散列桶)数组,数组中的每一个Entry又是一个链表,next用于存储链表中的下一个条目。

HashMap底层结构.png
  • 存储k/v操作 put(key,value)

1、判断key是否是null,如果是,hash值直接置为0,散列位置为bucket数组中第一个位置,index=0。直接到步骤3。
2、如果key不为null,根据key的hashcode值计算hash值(h = hash(k.hashcode)),根据哈希值h找到key被散列到bucket数组中的位置index( index = h&(length-1) )。
3、找到bucket数组对应位置 table[index] 的链表。如果链表为空,那么新建一个entry,k/v/hash值存储于entry中,next指向null,table[index]=entry。 如果链表非空,遍历,判断当前key是否和链表中某个entry的key值equals,如果equals,用value替换掉之前旧的value,然后方法立即返回。如果遍历完没有找到,那么创建一个新的entry,将新的entry置于链头,next指向之前的链头entry。 添加entry之前判断是否需要扩容,如果需要,以2的倍数扩容

HashMap put图解.png

观察元素put的过程,我们发现在根据key寻找存储地址时,先比较了key的hashCode,如果hashCode相同,再比较了equals,那么两个key equals的前提是hashCode相等。所以就可以理解我们在初学java时,都熟记的一条原则:重写equals方法,必须重写hashCode方法。equals相等,hashCode一定相同。hashCode相同,不一定equals。

  • 根据k值获取v操作 get(key)

1、判断key是否是null,如果是,hash值直接置为0,散列地址为bucket数组中第一个位置,index=0。找到bucket数组对应位置 table[0] 的链表。如果链表为空,那么返回null;如果不为空,遍历找到key=null的entry,返回entry的value值。
2、如果key不为null,根据key的hashcode值计算hash值(h = hash(k.hashcode)),根据哈希值h找到key被散列到bucket数组中的位置index( index = h&(length-1) )。
3、找到bucket数组对应位置 table[index] 的链表。如果链表为空,那么返回null;如果不为空,遍历entry,判断当前k的hash值等于entry的hash值,且key值和entry的key值equals的entry条目,返回entry的value值。

进阶分析

  1. hash碰撞
    [什么是hash碰撞?]
    对不同的key可能得到同一散列地址,即key1≠key2,而hash(key1)=hash(key2),这种现象称碰撞。比如上面的例子中“张三”和“张三的弟弟”两个key在进行hash的时候,得到的hash值都为8,index计算都为0,那么就产生了hash碰撞。

[为什么会有hash碰撞?]
产生上述hash碰撞的原因是由于我们的hashCode方法实现不合理,两个同姓不同名的person,我们在定义Person类的时候不能简单地用“姓氏”一个属性来计算hashCode,应该综合姓氏、姓名、年龄等所有属性计算hashCode。通常我们在Eclipse等IDE自动生成hashCode方法时,编译器会默认帮我们生成合理的hashCode算法就是这个道理。

[hash碰撞会带来什么问题?]
我们知道,数组的优势是随机存取速度快,链表的优势是插入删除速度快。假设所有存入HashMap的entry的key都不会产生hash碰撞,那么所有bucketIndex位置就只会存储一个entry,整个hash表就类似是一个entry数组,存取速度会非常快。反之,如果key的hash碰撞概率非常高的话,那么有可能产生某个bucketIndex位置存储的entry非常之多,链表非常长。极端情况下就是整个entry数组,只有某个index位置有数据存储,整个hash表几乎就变成了一个链表,那么这个hash表的存取速度会非常慢。

[如何避免hash碰撞?]
hash值是根据对象的hashCode计算而来的,如果我们的hashCode算法比较优秀,可以保证重复率低,那么hash碰撞的概率就会降低。但是想做到完全避免,是非常困难的。而且,就算hashCode计算结果不一样,在计算bucketIndex的时候,也可能得到相同的结果。比如,“张三”的hashCode=8,“张三的弟弟”的hashCode=16,bucket数组长度为8,那么 index = h & (length-1),二者得到的结果都是0,仍然会发生碰撞。
那么可能大家会有这样的想法,我们把bucket数组长度调大,翻倍变成16,二者index计算的结果就不会相同了,就没有碰撞了。但是,我们很难合理设计数组的长度,如果设计很长固然可以一定程度上减少hash碰撞,提高存取效率,但是同时也牺牲了内存空间,所以在考虑平衡空间和时间的情况下,我们只能在初始情况下定义一个较小的数组长度,当发现哈希表中存储的数据较多,达到一定阈值时,再对数组长度进行扩容。

  1. resize扩容
    [什么是扩容?如何扩容?]
    hashmap的初始容量为16,即table数组的长度为16。默认加载因子为0.75,即阈值为16*0.75=12。当hash表中存储的entry数量达到12时,hashmap会进行扩容。扩容就是table数组长度翻倍变成32,当达到下一次阈值时,继续扩容长度达到64,依此类推,hashmap每次扩容后容量大小都是2的指数。

[为什么要扩容?]
前面提到,如果数组长度比较小,就会很容易产生hash碰撞,导致entry以链表的形式集中存储在某一个或多个bucketIndex上,降低存取效率。所以为了尽量保证hashmap的存取效率,需要在适当的时候进行扩容。

[扩容会带来什么问题?]
扩容后,会创建一个新的entry数组,将旧的entry数组数据拷贝到新的数组中。并且,这个拷贝不是简单的范围拷贝。扩容后,因为hash的算法和数组length相关联,最后一步是 h & (length - 1),当length发生变化时,entry的bucketIndex可能发生改变。即以前同时存储在index=0位置上的“张三”和“张三的弟弟”可能需要分散到index=0,index=8的2个不同位置上。所以,扩容会带来rehash,整个hash表中的entry的存储位置需要重新计算,这个操作是很影响效率的。

[如何避免rehash?]
为了减少初始时内存空间的占用,我们只能定义容量较小的hash表。所以rehash肯定会产生,除非我们在创建hashmap之前,提前预知存储entry所需要的容量,然后根据可传入capacity的构造方法构造一个hashmap。

多线程下的使用

HashMap是非线程安全的,在多线程环境下,我们可以使用concurrent包下的ConcurrentHashMap。(Hashtable虽然可以替代HashMap,并且是线程安全的,但是是通过在方法上加synchrionize实现,效率没有ConcurrentHashMap的分段锁高)

总结

  1. HashMap底层是以数组+链表的结构存储键值对。
  2. 当某一个类的对象想用作HashMap的key值时,需要重写hashCode和equals方法。hashCode的实现要降低重复概率,推荐使用IDE默认的hashCode实现。
  3. HashMap在给key寻找存储位置时,先比较hashCode,再比较equals。
  4. HashMap扩容导致rehash会造成性能问题,大批量数据存储应尽量在构造hashmap之前设置好容量,避免递增式的rehash。
  5. HashMap非线程安全,多线程下推荐使用ConcurrentHashMap。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容