HashMap学习笔记

来源:互联网 发布:三国杀 知乎 编辑:程序博客网 时间:2024/06/06 21:40

关于HashMap的工作原理与实现的好文章不少,推荐几个:

http://blog.csdn.net/u010887744/article/details/50346257

http://yikun.github.io/2015/04/01/Java-HashMap工作原理及实现/

http://blog.csdn.net/vking_wang/article/details/14166593

对着HashMap的源码来看理解的会更好,JDK的版本不同具体的实现原理也有所不同,新版本都是在有所改进,减少碰撞,同时更少的rehash,这么做都是为了更高的效率和更少的bug。我是用jdk1.7学习的,jdk版本不是重点,重点对其中的原理理解即可。

1.实现

HashMap是采用数组+单向链表的方式实现的。这样做结合了数组寻址容易和链表插入删除快的有点。HashMap也可以理解为链表的数组,也是哈希表的一种实现方法-拉链法。在JDK8里,新增默认为8的閥值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。

2.原理

2.1 put,即HashMap.put(K,V)方法。

首先需要确认该数据元素在数组中位置。数组的默认长度是16(assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";).

先用key计算h,然后用h和length-1按位与所得的值即为该数据数组在数组中的index(位置)。

k即为key

h ^= k.hashCode();h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
return h & (length-1);//index,在数组中的位置

然后将该数据放在单向链表中。

Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);//此e为上一个单向链表的第一个元素

这句可以看出新元素是单向链表的第一个元素,并且指向上一个保存在该单向链表的第一个元素,这一过程称为碰撞(我的理解)。

resize

当多个key向HashMap中put的时候,碰撞就可能会变得频繁,可能某个单向链表会变得很长,get元素的时候查找效率就会降低。JDK中是这样解决的:

if ((size >= threshold) && (null != table[bucketIndex])) {            resize(2 * table.length);            hash = (null != key) ? hash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }

threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

loadFactor初始值是0.75

threshold是阈值。初始值16*0.75=8.

当使用的size不小于threshold,对数组进行扩容,扩展为原来的两倍(2 * table.length)。

rehash

resize的过程中需要重新计算元素的位置,这个成本还是比较高的,在jdk1.7中就是对元素的key重新计算一遍hash,然后分配。但是在JDK1.8中做了重大改变。
JDK1.8中resize的时候不需要再重新计算hash:
因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
假设我们length从16resize到32(以下仅写出8位,实际32位),hash(key)是不变的。
n-1:     0000 1111-----》0001 1111【高位全0,&不影响】
hash1:    0000 0101-----》0000 0101  
index1:  0000 0101-----》0000 0101【index不变(31&5)
hash2:    0001 0101-----》0001 0101 
index2:  0000 0101-----》0001 0101【新index=5+16即原index+oldCap(31&21)

 新bucket下标:(2n - 1) & hash(key),由于是&操作,同1为1;hash(key)不变;2n-1在原来n-1的基础上仅最高位0变1;
HashMap在Resize时,只需看(新)n-1最高位对应的hash(key)位是0还是1即可,0则位置不变,1则位置变为原位置+oldCap
这个新设计很有创意,需要重点理解。

可以直白的这样理解:
有这样一个hash1:1111111001110010000110001010000(2134445136)
size是16时,index是:2134445136&(16-1)=0
resize的时候,从16扩容到32。
size是32,index是:2134445136&(32-1)=16
31=11111,最高位对应hash1的数字是1

再有一个hash2:1111111001110010000111110101011(2134445995)
size是16时,index是:2134445995&(16-1)=11
resize的时候,从16扩容到32。
size是32,index是:2134445995&(32-1)=11
31=11111,最高位对应hash1的数字是0

由此可以得出结论:(新)n-1最高位对应的hash(key)位是0还是1,0则位置不变,1则位置变为原位置+oldCap


2.2 get HashMap.get(K)方法
JDK1.8之前,是通过hash计算key在数组中的位置然后在遍历单向链表用equals(key)确认所查找元素。
由于1.8中当新增了红黑树,所以get(K)时先判断元素类型,如果是红黑树则遍历红黑树逐层查找。

小结
HashMap很值得研究,对于学习数据结构有很大帮助。

HashMap遍历:
方法1(效率高,推荐)
Map map = new HashMap();  Iterator iter = map.entrySet().iterator();  while (iter.hasNext()) {     Map.Entry entry = (Map.Entry) iter.next();     Object key = entry.getKey();     Object val = entry.getValue();   }

方法2(效率低,不推荐)

Iterator iter = map.keySet().iterator();  while (iter.hasNext()) {  Object key = iter.next();  Object val = map.get(key);  }



对于keySet其实是遍历了2次,一次是转为iterator,一次是从hashmap中取出key所对于的value。而entryset只是遍历了第一次,他把key和value都放到了entry中,所以速度快。


Map关注数据的键和值的唯一性。HashMap主要用来存储键值对。







0 0
原创粉丝点击