HashMap的数据结构:
哈希表>数组+链表(jdk1.8之前)数组的默认长度为16,jdk1.7之前需全部rehashing,jdk1.8通过 hashCode() 的高 16 位异或低 16 位实现:(h = k.hashCode()) ^ (h >>> 16),
主要从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起碰撞。
链表转换为红黑树-作用:解决了二叉查找树的线性问题;进行平衡性-变色-左旋-右旋(指针的变化)。
思考1:若是线性查找,链表的时间复杂度为O(n)。转化为红黑树时间复杂度为O(lgn),又为何不直接使用红黑树?
从源码中注释可知,在节点较多,红黑树占空间这一劣势不太明显时舍弃链表,使用红黑树。
思考2:jdk1.8引入红黑树-当链表长度超过 8 时,开始由链表转化为红黑树,那为什么是8?6 or 7?负载因子0.75?
从源码中注释可知,为了配合使用分布良好的hashcode,受随机分布的影响,链表中节点遵循泊松分布,根据统计链表中节点数为8的概率接近千分之一,此时链表性能很差,而链表转化为红黑树也消耗性能,
为了挽回性能使用红黑树。理想的均匀分布,节点数不到8就自动扩容。
负载因子过大,空间利用率提高,时间效率降低。负载因子较小,填充的元素少,hash冲突也会减少,底层的链表长度或红黑树的高度会降低,查询效率提高,时间效率提高,空间利用率降低。
从源码中注释可知, 负载因子为0.75的时候,即当前容量为16,则16*0.75=12,当容量达到12进行扩容,避免较多的hash冲突,使底层的链表长度或红黑树的高度会降低,提升了空间效率,空间利用率比较高。
数组对于数据的访问如查找和读取十分便捷,而链表对于数据插入非常方便,链表可以解决hash值冲突(不同的key值可能会得到相同的hashcode值)。
其中数组里每个元素存储的是一个链表的头结点。而组成链表的结点其实就是hashmap内部定义的一个类:Entity,如图-Entity包含三个元素:key,value和指向下一个Entity的next。 即数组中的元素,它持有一个指向下一个元素的引用,构成链表。
数组里面的元素都是Entry,Entry这个类有个成员变量就是Entry本身的引用,引用中又有自身的引用,构成链。
上述图示底层实现程序在此不作详细赘述,有兴趣的童鞋可以艾特博主?yxd179哟!
重点切入:
i.当put元素时>>>
a.首先根据put元素的key获取hashcode值,然后根据hashcode算出数组的下标位置,若下标位置没有元素,直接放入元素即可。
b.若该下标位置有元素(即根据put元素的key算出的hashcode值一样即重复),则需要已有元素和put元素的key对象比较equals方法,如果equals不一样,则说明可以放入进map中。由于hashcode一样,故得出的数组下标位置相同,会在该数组位置创建一个链表,后put进入的元素到放链表头,而原来的元素向后移动。
ii.当get元素时>>>
a.根据元素的key获取hashcode值,然后根据hashcode值获取数组下标位置,若只有一个元素则直接取出。若该位置一个链表,则需调用equals方法遍历链表中的所有元素与当前的元素比较equals,得到真正需要的对象。
综:HashMap在hashing的基础上,分别通过put()和get()方法储存和获取对象。当将键值对传递给put()方法时,它会调用键对象的hash()方法来计算hash值并储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回所获取的值对象。遇到碰撞问题如何解,当发生碰撞了怎么办,链表大大这时候就要来插足HashMap的前后今生啦,所找的对象将会储存在链表的下一个节点中,当声明作final(使用不可变的)的对象,并采用合适的equals()和hash()方法,将会大大减少碰撞的发生,从而提高效率(使用String,Interger这样的包装类作为键)。当然,若是根据hashcdoe算出的数组位置尽量的均匀分布,则可以避免遍历链表的情况,提高性能。
若HashMap的大小超过了0.75(默认的负载因子),这时候就需要扩容啦,当重新调整HashMap的大小时,若俩个线程都发现需要rehashing,当通过get()方法获取值对象的时候,部分会出现null,So线程安全问题也就成了HashMap前后今生的第三者。
当put的时候导致的多线程数据不一致:
举例:有线程A和B,A插入一个key-value键值对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来桶的索引一致,则当线程B成功插入之后,线程A再次被调度运行时,依然持有过期的链表头,覆盖线程B插入的记录,这样线程B插入的记录就凭空消失-数据不一致。
当在扩容的时候:jdk1.8之前是采用头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,采用头插法新链表与旧链表的顺序是反的,在1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。
那为什么要在多线程的环境下使用HashMap,又如何尽量避免or解决,更多?可以一起探讨哟!(collection框架提供方法能保证HashMap synchronized).
综上:童鞋们,JDK1.8对HashMap的优化想必早已get到啦!
附Aop:引出下篇博文论题:Spring的代理Jdk VS Cglib,默认选择的是Jdk动态代理还是Cglib代理?它们分别是如何实现的,反射or字节码ASM?Jdk动态代理又Why只能是接口??