HashMap了解吗?
-
HashMap底层数据结构:数组+链表+红黑树
-
允许put null值,HashMap在调用hash算法时,如果key为null,那么hash值为0,这一点和HashTable和ConcurrentHashMap有所不同。
- (key == null) ? 0:(h=key.hashCode()) ^ (h >>> 16);
-
loadFactor:负载因子,默认为0.75,时均衡了时间和空间损耗计算出来的,较高的值会减少空间的开销,扩容减少,数组大小增加速度变慢,但是查找成本增加,hash冲突增加,链表变长。
-
如果有很多需要存储在HashMap中的数据,要在一开始把它的容量设置为足够大,防止出现段的扩容。
-
可以通过Collections.synchronizedMap()来实现线程安全或者使用ConcurrentHashmap
-
put的过程
- 首先会判断数组有没有进行初始化,没有的话,先执行初始化操作,resize()方法
- (n-1) & hash用来定位到数组中具体的位置,如果数组中的位置为空,直接在该位置添加值
- 如果数组中该位值有值的话,会先判断该位置的key和hash值是否和要插入的key和hash值相等,若相等则直接覆盖。
- 如果要插入位置是链表,采用的是尾插法插入数据,并且当链表的长度大于等于8时,并且数组长度大于64,则会将链表转换为红黑树,否则,就将数组进行扩容。
- 如果插入位置是树节点,则进行红黑树的插入操作。
- 插入完成后,会判断size是否大于threshold,是否需要扩容,若扩容的话,数组大小为之前的2倍,扩容完成后,将原数组上的节点移动到新数组上。
-
为什么树化操作的阈值为8?
- 链表的查询时间复杂度为O(n),红黑树的查询时间复杂度为O(logn),在数据量不多的时候,使用链表比较快,只有当数据量比较大的时候,才会转换为红黑树,但是红黑树占用的空间大小是链表的2倍,考虑到时间和空间上的损耗,所以要设置边界值;链表的长度为8的概率很低,在HashMap注释中写了,出现的概率不足千分之一,红黑树只是为了在极端情况下来保证性能。
-
哈希冲突:如果两个不同的对象经过hash运算后得到的值相同,就会发生碰撞,这种现象就被称为hash冲突
- 有以下方式可以解决哈希冲突:
- 开放定址法
- 再哈希法
- 链地址法
- 建立公共溢出区
- 有以下方式可以解决哈希冲突:
-
hash算法
- (key == null) ? 0:(h=key.hashCode()) ^ (h >>> 16); 该方法只是得到要存入对象的hash值,具体获取要存入的数组的下标的计算方法是“(length-1)& hash”。
-
为什么数组大小始终为2的n次幂?
- 因为在确定某个值在数组位置的下标时,采用的是(数组大小-1)位与上hash值,而数组大小减一后,用2机制表示最后几位都是1,这样每位在位与运算之后,不是0就是1,如果我们hash值是均匀分布的话,那么得到的数组下标也是均匀分布的,而如果数组的容量不是2的n次幂,那就没有这个特性了。
- 为了能让HashMap存取高效,尽量减少碰撞,尽量把数据分配均匀。
- 比如数组长度为16时,length-1的二进制一定是“…00001111…”这种形式,即后面几位全部都是1,那么做与运算时,只会对后面的四位进行运算,肯定会落在0-15的范围内。
- 取余(%)的操作中如果除数时2的幂次方则等价于被除数和除数减一的与(&)操作,也就是说(hash% length == hash & ( length -1))前提时length是2的幂次方。并且采用二进制位操作,可以提高运算效率。
-
为什么数组的大小默认是16?
- 16是一个经验值,2,4,8太小了,会频繁扩容,32有些大了,会多占用空间。
-
为什么JDK1.8采用了尾插法?
- JDK1.7时采用头插法,在扩容后rehash,会使得链表的顺序颠倒,引用关系发生改变,那么在多线程的情况下,会出现链表成环进而出现死循环的问题,而尾插法又不会出现这种问题,rehash后链表的顺序不变,引用关系也不会发生改变。
-
HashMap和HashTable的区别?
- 实现方式不同:HashTable继承了Dictionary类,而HashMap继承的是AbstractMap类
- 初始容量不同:HashMap的初始容量为16,HashTable为11,负载因子都为0.75
- 扩容机制不同:HashMap每次扩容为原来的2倍,HashTable每次扩容为原来的2倍+1。
- 底层数据结构不同:HashMap底层待用数组+链表+红黑树的数据结构,而HashTable底层是数组+链表的数据结构。
- 线程是否安全:HashMap是线程不安全的,HashTable是线程安全的,因为Hash Table内部的方法都有synchronized关键字修饰,因此HashMap的效率要比HashTable高一些。为了保证线程安全,可以使用ConcurrentHashMap。
- HashMap可以存储null的key和null的value,但是null为键只能有一个。HashTable不允许存储null键和null值,否则会抛出异常。
- HashMap提供了可供应用迭代的键的集合,因此HashMap是快速失败的。
-
HashMap和TreeMap的区别
- HashMap
- 数组方式存储key/value,线程不安全,允许null作为key和value,key不可以重复,value允许重复,不保证元素迭代顺序,每次扩容会重新计算key的hash值,会消耗资源,key必须重写equals和hashCode方法。
- TreeMap
- 基于红黑树的NavigableMap实现,线程不安全,不允许null,key不可重复,value允许重复,存入TreeMap的元素应当实现Comparable接口,或者实现Comparator接口,默认按照key排序后的顺序迭代元素。
- 用于对存入元素的时候进行自动排序,可以进行自定义排序。
- HashMap