HashMap
1、HashMap的主要参数都有哪些?
初始化桶容量,默认16, 负载系数,默认0.75 最大容量,2的30次方 扩容阈值,默认容量*负载系数,当元素个数达到阈值,再put进来元素,就会触发扩容 table数组,
2、HashMap 的数据结构?
1.7是数组 + 链表,实体的数据结构是k-v,hash,next引用 1.8是数组+链表/红黑树
3、hash的计算规则?
JDK8中使用扰动函数,32位hashCode中,高位不变,然后将高16位和低16位进行异或运算的结果作为低16位,最后的结果就是hash值,hash对长度减一做取余操作定位对应的桶; JDK7中稍微有点不一样
4、hash碰撞和解决方法?
hash碰撞就是两个key通过hash值计算桶的位置的index,得到相同的index,这就是hash碰撞 解决hash碰撞,采用链地址法,jdk8中链的元素大于等于8个会转换为红黑树,小于等于6个则转换为链表。另外hash值计算时的扰动函数也可以减少hash碰撞
5、关于扩容
5.1 为什么扩容是以2的幂次?
为了便于hash计算后定位到桶,hash值对2的幂次取余在计算上是对该数-1进行按位与运算,效率高。(hash % len = hash | len-1 ) 另外2倍扩容,可以减少resize的数据重分配。部分元素不需要移动,比如原来16,位于hash为21的在5的位置,扩容之后就在21的位置了,如果hash37原本也是在5的位置,扩容之后还是在5的位置,这样有助于分散处于一个桶中的多个元素,
5.2 HashMap的扩容时机,什么时候会进行rehash?
都是在插入元素的时候。不过稍微有点不一样,代码中1.8是在插入完成之后会检查扩容,1.7则是扩容之后,再头插法新增节点。扩容的思路都类似,就是扩大二倍,如果达到了最大值则不会扩容了,扩容的条件是元素个数达到阈值;
6、存取
6.1 HashMap put的过程
定位到桶,如果桶为空则直接插入,如果不为空,1.7 以前是在链表进行头插法,在1.8是进行尾插法,1.8还会进行链表和红黑树转换的阈值判断,检查是否需要转换为红黑树。 另外在1.8中增加了putOnlyIfAbsent 的功能,在1.7中没有
final V putVal ( int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node< K, V> [ ] tab; Node< K, V> p; int n, i;
if ( ( tab = table) == null || ( n = tab. length) == 0 )
n = ( tab = resize ( ) ) . length;
if ( ( p = tab[ i = ( n - 1 ) & hash] ) == null)
tab[ i] = newNode ( hash, key, value, null) ;
else {
Node< K, V> e; K k;
if ( p. hash == hash && ( ( k = p. key) == key || ( key != null && key. equals ( k) ) ) )
e = p;
else if ( p instanceof TreeNode )
e = ( ( TreeNode< K, V> ) p) . putTreeVal ( this , tab, hash, key, value) ;
else {
for ( int binCount = 0 ; ; ++ binCount) {
if ( ( e = p. next) == null) {
p. next = newNode ( hash, key, value, null) ;
if ( binCount >= TREEIFY_THRESHOLD - 1 )
treeifyBin ( tab, hash) ;
break ;
}
if ( e. hash == hash &&
( ( k = e. key) == key || ( key != null && key. equals ( k) ) ) )
break ;
p = e;
}
}
if ( e != null) {
V oldValue = e. value;
if ( ! onlyIfAbsent || oldValue == null)
e. value = value;
afterNodeAccess ( e) ;
return oldValue;
}
}
++ modCount;
if ( ++ size > threshold)
resize ( ) ;
afterNodeInsertion ( evict) ;
return null;
}
public V put ( K key, V value) {
if ( table == EMPTY_TABLE) {
inflateTable ( threshold) ;
}
if ( key == null)
return putForNullKey ( value) ;
int hash = hash ( key) ;
int i = indexFor ( hash, table. length) ;
for ( Entry< K, V> e = table[ i] ; e != null; e = e. next) {
Object k;
if ( e. hash == hash && ( ( k = e. key) == key || key. equals ( k) ) ) {
V oldValue = e. value;
e. value = value;
e. recordAccess ( this ) ;
return oldValue;
}
}
modCount++ ;
addEntry ( hash, key, value, i) ;
return null;
}
void addEntry ( int hash, K key, V value, int bucketIndex) {
if ( ( size >= threshold) && ( null != table[ bucketIndex] ) ) {
resize ( 2 * table. length) ;
hash = ( null != key) ? hash ( key) : 0 ;
bucketIndex = indexFor ( hash, table. length) ;
}
createEntry ( hash, key, value, bucketIndex) ;
}
void createEntry ( int hash, K key, V value, int bucketIndex) {
Entry< K, V> e = table[ bucketIndex] ;
table[ bucketIndex] = new Entry < > ( hash, key, value, e) ;
size++ ;
}
6.2 HashMap的get过程
先得到key的hash值,再把这个hash值与length-1按位与(取余),得到table数组的下标。取出这个下标值的key,与传入的key比较,如果相同那就是这个了。如果不同呢,那就沿着这个单向链表向后找,直到找到或找到结束也找不到,找不到返回null。
7、HashMap初始化传入的容量参数的值就是HashMap实际分配的空间么?
不是,空间是2的幂次,不管传入的数字是什么,最后会转成一个大于该数字的2的幂次。且构造之后不会分配空间,会在第一次put元素的时候存入
static final int tableSizeFor ( int cap) {
int n = cap - 1 ;
n |= n >>> 1 ;
n |= n >>> 2 ;
n |= n >>> 4 ;
n |= n >>> 8 ;
n |= n >>> 16 ;
return ( n < 0 ) ? 1 : ( n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ;
}
8、为什么String, Interger这样的wrapper类适合作为键?
一个是因为不可变,另一个是因为已经重写了hashcode和equals方法 String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
9、自定义对象做key什么要重写hashcode方法和equals方法?如果都不重写会怎么样?如果值重写一个会怎么样?
假设我们使用自定义对象作为key,new了两个一模一样的对象A和B,先用map.put(A,“123”),再使用map.get(B),肯定不能获取到“123”,因为这两个对象的hash值不等且equals也是返回false。但是我们肯定期望能够获取到123,因此我们需要重写这两个方法,让他们在属性一致的时候,hash值一致且equals为true,这样才能满足需求。 如果重写了hash,equals不一样,那么在get的时候,因为二者的equals为false,因此也是get不到的, 如果重写了equals,没有重写hash,那么AB的hash值不一样,那么也是get不到的,从源码我们可以看到不管是put还是get,判断key一致的条件是hash值首先要一样,然后要么equals为true,要么是一个对象(==wei true)
if ( p. hash == hash && ( ( k = p. key) == key || ( key != null && key. equals ( k) ) ) )
结合HashMap的原理这里我们也可以看出我们我们不但要重写hashcode方法,还要尽量降低hashcode方法的冲突
10、HashMap的key是否可以为null
11、HashMap的复杂度
HashMap整体上性能都非常不错,但是不稳定,为O(N/Buckets),N就是以数组中没有发生碰撞的元素。 新增查找获取的复杂度都是O(N/Buckets),设计良好的话接近一O(1) 如果某个桶中的链表记录过大的话(大于等于8),就会把这个链动态变成红黑二叉树,使查询最差复杂度由O(N)变成了O(logN)
12、HashMap在JDK7和8的区别
https://my.oschina.net/hosee/blog/618953 https://blog.youkuaiyun.com/qq_36520235/article/details/82417949
对比维度 1.7 1.8 数据结构 数组 + 链表 数组 + 链表/红黑树 插入 链表头插法 尾插法 扩容 计算hash 不计算hash要么不动要么往后移动扩容的大小(通过和旧容量与运算得到)。 hash计算 9次扰动处理(4次位运算 + 5次异或) 2次扰动处理( 1次位运算 + 1次异或)
final int hash ( Object k) {
int h = 0 ;
if ( useAltHashing) {
if ( k instanceof String ) {
return sun. misc. Hashing. stringHash32 ( ( String) k) ;
}
h = hashSeed;
}
h ^= k. hashCode ( ) ;
h ^= ( h >>> 20 ) ^ ( h >>> 12 ) ;
return h ^ ( h >>> 7 ) ^ ( h >>> 4 ) ;
}
return ( key == null) ? 0 : ( h = key. hashCode ( ) ) ^ ( h >>> 16 ) ;
13、我们能否让HashMap同步?
Map m = Collections.synchronizedMap(map);思想很简单,返回的是一个SynchronizedMap,该类实现了Map接口,因此可以直接调用Map接口的方法,它内部持有真正的非线程安全的Map和一把锁(其实就是一个Object对象),然后调用的时候都加锁,再调用内部Map的方法,让所有方法变成了同步方法,方式其实并不是很可取,推荐使用CocurrentHashMap。
14、HashMap 和 HashTable有何不同?
HashTable的相关方法都是synchronized同步的get方法也是,比如get、put,putAll,remove、size、isEmpty、keys、contains、containsKey、clear、forEach等,甚至toString和equals都是同步方法,我们看到很多只读方法也是同步的,其实读取方法有时候没有必要同步,这样让它性能很低,比如单线程写多线程读取的时候我可以用HashMap,但是用HashTable多线程get都要排队,性能低。 HashTable无论key或value都不能为null,HashMap只能允许一个key为null,可以运行多个value为null。而且HashTable是线程安全的,HashMap是线程不安全的。
15、JDK1.8中对 HashMap做了哪些性能优化?
主要是:链表和红黑树的转换,阈值分表为6和8 旧版本的HashMap存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(TREEIFY_THRESHOLD默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能(O(logn))。当长度小于(UNTREEIFY_THRESHOLD默认为6),就会退化成链表。 扩容时,1.7需要重新计算hash,1.8不需要,并且要么元素不动,要么元素移动一个2的N次方 参考
16、高并发下HashMap为什么会线程不安全?
JDK 1.7 下死循环(头插法导致) JDK 1.8 下值覆盖(尾插法规避了死循环,但是会出现值覆盖),两个线程对同一个位置进行put操作,对应桶为null且hash冲突时,线程A put完之后,线程B解挂后再操作时,因B之前已经判断过hash,就不判断直接写入
参考:HashMap 线程不安全的体现
下面是我给出的一个简单的1.8下值覆盖的例子,hashmap保存的键值对中键和值都是一样的,理论上线程安全就会有10万个键值对,由于线程不安全,少于10万个,并且打印出键值不等的键值对
public class HashMapTest {
public static final CountDownLatch latch = new CountDownLatch ( 5 ) ;
public static void main ( String[ ] args) throws InterruptedException {
HashMapThread thread1 = new HashMapThread ( "HashMap-TestThread-1" ) ;
HashMapThread thread2 = new HashMapThread ( "HashMap-TestThread-2" ) ;
HashMapThread thread3 = new HashMapThread ( "HashMap-TestThread-3" ) ;
HashMapThread thread4 = new HashMapThread ( "HashMap-TestThread-4" ) ;
HashMapThread thread5 = new HashMapThread ( "HashMap-TestThread-5" ) ;
thread1. start ( ) ;
thread2. start ( ) ;
thread3. start ( ) ;
thread4. start ( ) ;
thread5. start ( ) ;
latch. await ( ) ;
System. out. println ( "size: " + HashMapThread. map. size ( ) ) ;
for ( Integer i : HashMapThread. map. keySet ( ) ) {
if ( ! HashMapThread. map. get ( i) . equals ( i) ) {
System. out. println ( i + " 》》 " + HashMapThread. map. get ( i) ) ;
}
}
}
}
public class HashMapThread extends Thread {
private static AtomicInteger count = new AtomicInteger ( ) ;
public static Map< Integer, Integer> map = new HashMap < > ( ) ;
public HashMapThread ( String name) {
super ( name) ;
}
@Override
public void run ( ) {
while ( count. get ( ) < 1000000 ) {
map. put ( count. get ( ) , count. get ( ) ) ;
count. incrementAndGet ( ) ;
}
HashMapTest. latch. countDown ( ) ;
}
}
部分打印如下,可以看到只有不到6万个键值对,每次都不一样,而且被覆盖的值和原本正确的值相差只有一两个数,因为原子变量是递增的,这体现出了HashMap的值覆盖问题
size: 597274
786435 》》 786437
786495 》》 786496
786504 》》 786506
786505 》》 786506
786516 》》 786517
786514 》》 786515
786532 》》 786533
786555 》》 786556