HashMap学习笔记

关于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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值