HashMap详解

最近在重温java源码,一边看一边总结下,并且分享下自己的心得,共同学习,欢迎指点。这一篇说下HashMap的源码中的一些注意点,争取把原理讲透彻。

问题

开篇提出几个问题,带着问题往下看:
1:用过hashmap?ok用过的话,hashmap的put和get工作原理?
2:key和value在hashmap中的存储结构?
3:hashmap对key的选择?
4:hashcode是什么?
5:hashmap如何解决冲撞?
6:如果理解了get原理,那么当hashcode相同的时候如何取到值对象?
7:hashmap的扩容策略?
8:扩容时会带来什么问题?

字段:

1:table,hashMap中存储数据的变量
transient Entry[] table;

至于为什么设置为transient详见:【java源码系列】之ArrayList详解中有提到

2:threshold,loadFactor
这两个变量要一起说:hashMap默认table的大小是16;loadFactor是负载因子,用来控制hashMap的扩容时机;threshold是table容量*负载因子的结果。当用hashMap.put的时候,会有size++>threshold的判断,来选择hashmap是否是需要扩容的。

3:modCount 记录hashMap的修改次数

当在迭代器中修改这个线程不安全的对象的时候,会抛出ConcurrentModificationException异常,这就是fail-fast策略。实现原理就是用modCount,判断modCount的值和期望的count值,如果不相等,则说明在iterator期间有对hashMap进行修改。


方法以及问题探究

1.hashCode()和equals()
hashCode和equals对HashMap起到至关重要的作用,散列和判断都有用到。所以特意做了一篇关于hashCode和equals的分析,请先看完这篇 【java源码系列】之hashCode和equals详解再继续往下看。

2.key和value在hashmap中存储结构,解决冲撞问题

在hashmap中,数据的存储是通过Map.Entry<K,V>这个结构。K和V用的泛型,分别对key和value添加存取器,并且重写了hashCode和equals。

Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

table可以抽象为:

如上图所示,当hashmap进行hash散表发现冲撞,会采用链表来解决问题,在统一个bucket上创建一个链来存储hash相同的元素。

3.put和get方法原理以及扩容问题

get和put方法是主要详解的,hashmap中大部分核心内容都在这里,通过分析这两个方法,会解决上面大部分问题,按照平时使用顺序来分析,首先分析put方法。

首先hashmap会对提供的key的hashCode做一个额外的hash,以防止质量很差的hashCode算法导致hashMap频繁冲撞,降低效率。

static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
 }


在hashMap中,散表的方法是:

static int indexFor(int h, int length) {
        return h & (length-1);
    }

其中h为经过额外hash之后的值。由于length永远为2的n次方,所以length-1的二进制的值都为1。由此可以推断出hashMap散表的方法就是取h二进制(length二进制位数)个低位,将其转为10进制就是这个key所在的桶的位置。所以hashMap在对hashCode做二次hash的时候尤其会低hashCode值的低位进行特殊处理。在hashMap中,length永远为2的n次方,这是在内部实现里强制约定的,它会强制把length转为大于你给定值的最小符合约定的大小。
 // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

这是在hashmap构造函数中的一段代码。

通过以上的额外针对hashCode的hash和散表,可以拿到key在桶中对应的位置,在这里分两种情况,插入和修改。修改很简单,找到对应的key,进行覆盖。注意:这里用的hash和equals进行判断key值的,因为,hash值是可以相同的,但是key的地址判断和equals方法对不同的对象是不同的;而插入,是每一次新的对象都会放到table里面,将next指向上一个table里对象。插入的时候有这么一段代码:

if (size++ >= threshold)
            resize(2 * table.length);

其中threshold为table的capcity*负载因子。也就是说,当table桶的使用大于threshold的时候,要进行resize(2*table.length)方式进行扩容。resize方法也没有什么神秘的:判断size是否是int所能承受的最大值,如果不是,那么创建一个新的容量的table,并将老的数据按照新的table的容量进行hash。重新插入。如果是,则将threshold设为int最大值,等于负载因子为1。

了解了put的原理就会发现两个问题:一个是扩容时候的效率问题,另一个是扩容时候多线程的问题。为了尽量避免这些问题,用hashmap的时候不要存储庞大数量的数量。面对多线程并发有很多解决办法,如在使用hashMap程序断上加synchronized;创建hashmap用Collections.synchronizedMap来创建;或者使用ConcurrentHashMap来代替hashmap等等。这种选择应该根据具体情况而定。

get的方法很简单了,但是要注意一点,在判断key的问题上。上面刚才有提到用hash,地址,equals三个条件进行判断。

ok,hashMap这一篇就到这里,通过分析,开始提出的问题也都有了答案,使用hashMap注意点也就这么多了。如果还有什么问题,欢迎提出。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值