理解HashMap的几个关键点

什么是HashMap?

HashMap是一个散列表,存储的内容为键值对的映射(key-value),由于key存放在Set集合中,意味着key值不允许重复,但是key和value都允许为null。HashMap继承AbstractMap抽象类,实现了Map、Cloneable、Serializable接口,允许克隆和序列化。另外,HashMap是非线性安全的,键值对的映射也不是有序的。

HashMap的工作原理?

我们在使用HashMap的时候,最常用的就是get(key)和put(key,value)方法,分别是插入或者取出键值对。而这两个方法的实现并不简单。首先我们需要知道,在HashMap类中,有一个静态内部类Entry,这个类才是真正存放key和value的地方,HashMap有一个成员属性Entry<K,V>[] table 就是用于存放Entry。get和put方法就是通过hashCode()方法求出key值的hashCode,再根据hashCode将键值对存放到table中。我们再深入看看Entry类的成员属性,会发现有一个Entry<K,V> next属性,这就意味着Entry实际上是一个单链表!为什么需要设计为单链表?我们再看下文分析。

当两个对象的hashCode相同怎么办?

好了,现在就可以解决为什么Entry要设计为单链表,就是用于解决hashCode冲突问题!这里,我先提出一个关键点:HashMap是通过hashCode()方法找出我们bucket(也就是table数组中的位置),而equals()是用于从Entry链中找到我们的key值。很多时候都会混淆这两个方法,误以为就是通过hashCode()确定我们存放的key-value。如果我们将两个key的hashCode值相同的对象放进HashMap时,这时候,就先确定hashCode对应的table下标,然后调用equals()方法对两个hashCode相同的key值进行比较(也就是比较它们的内存地址是否相等),如果相等,用新的value替换旧的value,如果不相等,则将新的key-value插到Entry链的头部,之所以插到头部,是为了避免插到尾部而遍历链表导致额外的o(n)时间复杂度。取出key-value的原理也是差不多,确认hashCode后,从Entry链的头部遍历到尾部,逐个用equals()比较,找到对应的key-value。

如何有效地减少冲突?

通常,我们建议使用String和其他包装类作为key更好。原因是String类声明为final,是不可变的,同时,String类重写了hashCode()方法和equals()方法,包装类也类似。如果存放的key对象经常改变,就会增大冲突的概率,又或者我们设计的hashCode()方法和equals()方法不合理,也会导致冲突概率增加。

额外提醒一下Java中对于hashCode和equals()是这样定义的:如果两个对象满足x.equals(y)==true,那么它们的hashCode也应该相等。同时对于equals()方法的设计应该遵循必须满足自反性(x.equals(x)必须返回true)、对称性(x.equals(y)返回true时,y.equals(x)也必须返回true)、传递性(x.equals(y)和y.equals(z)都返回true时,x.equals(z)也必须返回true)和一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值),而且对于任何非null值的引用x,x.equals(null)必须返回false。

如何设计HashMap的容量

关于HashMap的容量有几个关键的属性:①负载因子(loadFactor):默认值为0.75,也就是当容量达到75%时,和其他集合类一样需要进行扩容,HashMap每次扩容一倍。②初始容量(initialCapacity):创建HashMap的初始容量大小,默认值为16。③阈值(threshold):当容量大小达到该值即进行扩容,其中threshold=loadFactor*initialCapacity。当HashMap大小超过负载因子的大小,就会调用rehash()方法,对所有Entry重新计算hashCode值,这个代价是有非常高的!所以,合理地设计负载因子和初始容量很关键,一般来说,当负载因子较大时,rehash的可能性就会降低,占用的内存空间也会减少,但是每一条Entry链的大小就会增加,导致搜索时间变长,以时间换空间;反之,即以空间换时间,具体还需要根据实际情况而设定。

为什么说HashMap是非线性安全的?

我们对HashMap进行rehash(),原Entry链逆序,1->2->3就会变成3->2->1,当两个或以上的线程同时对HashMap实例进行rehash时,就有可能出现1->2->3->4死锁情况。详细可以阅读这篇博客


以上的分析其实都是基于JDK 1.7之前的版本,在JDK 1.8之后,HashMap的变化还是挺大的,这里简单分析一下两个版本的HashMap有哪些不一样,具体可自行Baidu,Google。

我们知道JDK 1.7中,HashMap是采用数组+链表的方式存储键值对,当冲突比较多的时候,由于链表的遍历时间复杂度为o(n),势必会大大影响get()方法的性能,所以,JDK 1.8采用数组+链表+红黑树来存储键值对。具体为,当某一条链大小超过8,将会转换为红黑树,红黑树的查找时间复杂度是o(logn),得到较大的优化。而且,Entry类改名为Node,也就是改为树节点了。

这里写图片描述

关于HashMap与HashTable、ConcurrentHashMap的区别(简单分析)

①HashMap与HashTable
相比于HashMap,HashTable是线程安全的,使用了Synchronize关键字进行控制,同时HashTable不仅实现了Map接口,还继承了Dictionary类,而Dictionary依赖于Enumeration接口,使得HashTable不仅可以使用迭代器遍历,也可以通过Enumeration遍历。HashTable不允许key=null和value=null;
②HashMap与ConcurrentHashMap
为了保证线程安全,而又不至于像HashTable使用重量锁Synchronize,ConcurrentHashMap采用了分段锁技术。ConcurrentHashMap实际上是一个Segment数组,每一个Segment通过继承ReentrantLock进行加锁,这样就保证了在多线程下,每个线程只对自己访问的Segment加锁,而不影响其他线程对其他Segment的操作,相比HashTable性能更优越。每个Segment其实和HashMap的结构有点相似,JDK 1.7之前采用数组+链表,JDK1.8采用数组+链表+红黑树,与HashMap有所不同的是,为了保证并发安全,Node<K,V>中对于V和next都声明为volatile。详细可阅读Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
这里写图片描述

资料参考
http://www.importnew.com/7099.html
https://segmentfault.com/q/1010000005602326
http://www.importnew.com/22011.html
http://www.cnblogs.com/skywang12345/p/3310835.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值