谈 JAVA容器之 HashMap

花了点时间来研究HashMap的数据结构。看了源码之后不得不为设计者感到震惊!

下面讲讲有意思的方面:

一、关于key=null。把null作为key的话,我认为存取速度是最快的!因为在put和get之前都会去判断key是否为null,如果为null则会直接去取key为null的值,而且key=null的话,在容器Entry数组里面是存的0下标,直接可以取出,对象在Entry数组所存储下标是根据一个hash值和数字的长度减一相与而得来的,只要key是null 那么数组下标肯定是0,及时HashMap自己在做长度调整重新转储时也一样!记住快的原因不是因为下标为0哦~而是因为put和get之前都会做判断!不过有一个极端的情况就是Entry[0]这个链非常的长,那么速度也就不那么的快了。

 

二、HashMap容量变化。当HashMap容量“不够”时,容量会自动增长,增长容量的方式上一次数组长度*2,HashMap容量不手动设置的话初始值是16,并且会有一个loadfactor(默认初始值为0.75f)来决定是否现在进行容量增长,并不是等16用完才增长,是当容量>=loadfactor*1 6的时候就开始增长。至于默认这个因子为什么是0.75f,可能是由实践得出的结论吧,所有默认HashMap在放入12个对象后,下次再放对象则会重新改变容量大小为32,而下一次判断是否扩容时就是用32*loadfactor来决定了。

 

三、put普通key,普通对象的过程。在我们存放一个key!=null的情况下,对象的存储过程又是怎样呢?我们常常使用String来作为key,是为了方便识别,其实key也可以是一个普通的Object对象,或者是自己初始化的对象、甚至可以是**.class,在jvm看来它依然是一个Object而已,既然是Object那么就会有hashcode,如果你没有覆写hashcode方法,自己实现的对象hashcode会直接返回jvm对 对象在内存中的一个索引值,这也就是hashcode本身存在的意义---对象的快速寻址。

    好了,不扯远了!继续put的过程,

先介绍HashMap里面的存储原子Entry,他是Map接口里面的一个内部接口,HashMap实现了它,有四个属性:Key、value、next、hash。key就是我们用的键值,value就是被存储的对象。next其实也是一个Entry对象,你可以认为这是一个链式结构,他指向下一个Entry对象,在Entry的具体对象里面可能存储了一个很长的链式结构,最后一个末端的对象的next是为null的。hash这个东西暂时认为是一个一个用于计算Entry数组下标的依据。你也可以认为是一个简单的索引。

HashMap在put对象之前会先要获得一个hash,这个hash是后来用于做Entry数组下标的一个依据之一,其实另外一个依据是数组的长度!后面再说。

    获得hash这个值得过程很纠结,看代码

 

int hash = hash(key.hashCode());
...
// 
static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

 先去key的hashcode,然后再进行了一番恶心的无符号移位与运算,至于为什么移位是20 、12、7、4 这个就是一个疑问了,就像是String的hashcode为什么移位是31(我的博客里面有讨论)....可能是处于性能方面的考虑,需要大神解释。

好了,这里就计算出了hash这个值!在HashMap存储一个对象之前,因为底层是一个数组,所以他要先计算出该存在哪个数组下标下面呢?于是有了下面的计算:

int i = indexFor(hash, table.length);
...//省略
//计算数组下标
static int indexFor(int h, int length) {
        return h & (length-1);
    }

 有人可能会问,为什么要length-1,其实很简单,为了数组不越界,相与的结果做大也只能是length-1

好了,要存入的数组下标也已经计算出来了,现在就是要放入对象了....

 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
           //这里为什么要比较e.hash==hash呢?看下面解答
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
               //新老值替换
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

 看看代码可以看出,在放入对象的之后直接定位的table[i],里面的i就是之前indexFor出来的值。直接来这个Entry链来进行比较有木有相同的key,注意注释里面写: e.hash==hash 是为了提升查找速度(我们常常覆写hashcode和equals方法,如果hashcode不相等可以立马判决这不是同一个对象,如果hashcode相等再进行equals对比!普通的没有覆写Object的equals方法的对象,其实是去对比内存地址是否相等的),直接数字的对比要比key的对比快,如果发现hash值不相等,那么就可以立马执行下一次循环对比!找到则进行新老值替换,如果没有找到就立马新加一个Entry对象到该Entry链中,注意下面一个细节:

//这里的i就是之前计算出来的
// int hash = hash(key.hashCode());
//int i = indexFor(hash, table.length);
addEntry(hash, key, value, i);
.....
//
void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);//扩容
    }

 这样,一个对象就被放入了!

 

四、关于get方法。

public V get(Object key) {
        if (key == null)
            return getForNullKey();//key为null的情况
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

 看看上面的代码,可以知道1个问题:

      hash这个值是那么的重要,能够帮HashMap快速定位到具体哪个Entry原子,现在很多公司在使用Mysql的时候,为了解决单表数据量过多导致操作表速度变慢的一个解决方法就是分库分表,其实也是一个道理,分库分表也相当于这里的一个Entry原子而已,hash看起来就像是一个索引,帮我快速定位!

 

五、关于entrySet方法。

    为了方便我们遍历Map,map提供了一个entrySet方法,使其转化为一个Set的子类。由各个子类去自己实现其遍历的方法。对于HashMap而言,遍历从Entry[0]开始,先把Entry[0]这个链表遍历完毕,然后再遍历Entry[1]....,类似遍历二维数组,或者说..类似深度遍历!

    其中还有一个keySet方法,也是和entryset方法类似,只是在内部把key单独的从entry剥离出来。

 

最后、关于putAll方法,即把被put的map里面的内容copy或者覆盖到当前map里面,在之前会进行一些容量计算或者扩容的操作,最后循环调用put方法。

 

 

=======================================

 

好了,看了看都是Java的基础,小弟不才...只是回过头来看看这些设计思想也挺有意思。上面内容有很多属于个人的YY,有不同意见不吝赐教,望高人指点。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值