1.HashMap的结构
HashMap的结构框图如下:
如上图所示,HashMap是数组加链表的存储结构,存储的是键值对Entry<K,V>。横向以数组的结构存储,数组的长度一般为2的整数次幂,默认数组的初始值为1<<4,即16.怎样存储一个键值对Entry<K,V>?通过key值的hashcode值与上数组长度减1来获取要存入元素在数组table中的index值,查看在index位置的是否有元素,如果没有则存入键值对Entry<K,V> entry1, 如果index位置有键值对,则比较key的hashcode值与key值是否都相等(即hashcode与equals方法),如果都相等,则更新key值对应的value值,如果不都相等,则在数组的index对应的链表头部再插入新的键值对Entry<K,V> entry2;怎样获取一个键值对Entry<K,V>?首先根据键值对的key值用index=HashCode(key)&(HashMap.length-1)获取到index值,再用keys.equals(),查找到对应的key值(用hashcode与equals方法),根据对应的key值获取到value值。
index=HashCode(key)&(HashMap.length-1)
2.HashMap存储元素put(K,V)与获取元素get(K)
jdk1.7中hashMap对应的put的源码如下:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//如果数组都为空,则初始化数组
inflateTable(threshold);
}
if (key == null)
//如果键为空,则在键值为空的键值对中放入value,此处说明HashMap允许key和value为null
return putForNullKey(value);
//key值求hashcode
int hash = hash(key);
//获取key值在数组中的索引
int i = indexFor(hash, table.length);
//遍历数组索引i位置的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//比较hashcode值是否相同并且key的地址或这是内容相同,则表明key值存在,覆盖原来key值对应的值,返回旧的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//如果key值在链表中没有存在过,则计数器加1
modCount++;
//在索引i处,hash值为hash,插入新的键值对entry<K,V>
addEntry(hash, key, value, i);
return null;
}
//key为null,在数组的索引为0,在key为null的entry<K,V>对中放入值
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断hashMap的容量是否超过门限值并且在相应的索引位置没有元素
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容数组容量为以前的2倍
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];
//在数组bucketIndex位置插入键值对
table[bucketIndex] = new Entry<>(hash, key, value, e);
//HashMap的容量加1
size++;
}
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//key值得hashcode值按位与数组长度减1
return h & (length-1);
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//获取数组的长度为2的整数次幂
int capacity = roundUpToPowerOf2(toSize);
//元素的门限值为数组的长度*负载因子
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化数组的长度为2的整数次幂
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
jdk1.7中hashMap获取元素get(K)的源码如下:
public V get(Object key) {
//key值为空的时候返回key值为空对应的value
if (key == null)
return getForNullKey();
//根据key值获取到键值对
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
//hashMap的大小为0,则没有元素,返回null
if (size == 0) {
return null;
}
//hashMap的大小不为0,有元素,则遍历数组的index为0的链表
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//获取key值为null对应的value,此处也说明hashMap的key与value允许为空
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//返回key值对应的hash值
int hash = (key == null) ? 0 : hash(key);
//遍历key值对应的数组索引index下的链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//与put时相同,同样比较hash、==、equals
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//返回键值对
return e;
}
return null;
}
3.技术核心点
1.为什么数组的长度是2的整数次幂?
因为数组的长度是2的整数次幂可以让元素key值hashcode散列更均匀。例如:key1的hashcode值为
,那么当数组的长度分别为16和10的时候,获取到数组的index值如下:
1011100011101011101001&1111=1001,index=9
1011100011101011101001&1001=1001,index=9
数组长度为16,只有低四位1001&1111才会等于1001,index为9;数组长度为10,1001&1001、1011&1001、1101&1001、1111&1001都等于1001,从而导致多个不同key的hashcode值不同却发生碰撞都落在index为9的索引上,所以采用2的指数次幂减1,数组长度-1,所有位数都是1,从而获取到hashcode最后几位而获取index的方式,会使key值hashcode的散列更均匀,分布在数组的不同的index上 ,减少碰撞的次数。 当然用hash(key)%M,M为数组长度,也可以获取索引,但是用求余的方法的效率要比按位与index=HashCode(key)&(HashMap.length-1)的效率低很多。
2.HashMap在put元素的时候比较key值为什么用 e.hash == hash && ((k = e.key) == key || key.equals(k))?
首先,因为元素在数组table中的index相同,但是key的hash值不一定相同,index=HashCode(key)&(HashMap.length-1),index=HashCode(key)&(HashMap.length-1)中不同key的HashCode(key)的低(HashMap.length-1)相同,而高位不一定相同,所以index相同,但是hash不一定相同。因为equals方法效率低,所以首先判断e.hash==hash,首先缩小比较范围,提高比较速度。
其次,==比较的是两个变量在栈内存中存放的内存地址,equals比较的是对象的内容。因为String类型重写了Object的hashcode和equals方法,hashcode中的算法会导致hashcode一样,对象值却不一样,所以如果两个对象的hashcode相同,但是两个对象不一定相同。进而导致在hash值相同的情况下,还要比较key的内存地址或者是内容是一样的,才认为key值是一样的。另外,如果两个对象相同,那么它们的hashcode一定相同。 e.hash == hash && ((k = e.key) == key || key.equals(k))是为了提高比较的效率。注意:Object的equals和==是一样的,但是String类型==与equals不一样,==的速度要比equals的速度快。
当用HashMap的时候,一定要重写hashCode方法和equals方法。hashCode重写规则:
当equals相同的时候,hashCode一定要相同
//String类重写了hashcode方法
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
//String类重写了equals方法
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
//object对象获取hashcode方法
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
//object对象equals方法
public boolean equals(Object obj) {
return (this == obj);
}
String s1,s2,s3 = "abc", s4 ="abc" ;
s1 = new String("abc");
s2 = new String("abc");
s1==s2 //false:两个变量的内存地址不一样,也就是说它们指向的对象不一样,
s1.equals(s2) //true:两个变量的所包含的内容是abc,故相等。
s3==s4 //true:s3和s4是两个字符串常量所生成的变量,"所存放的内存地址"是相等的,即使没有s3=s4赋值语句
3.hashMap怎样扩容?
当hashMap中所有元素的总个数大于数组长度*负载因子的时候,数组大小扩展为以前的2倍,即bucket的数量扩容为原来的二倍。扩容的源码如下:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果hashMap原来的容量已经为最大容量,则不再扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//Entry[]数组的长度设置为新的长度
Entry[] newTable = new Entry[newCapacity];
// 把所有的键值对由当前table转入扩容后的table
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//获取新的门限值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 把所有的键值对由当前table转入扩容后的table
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//每个元素重新计算索引值
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断hashMap的容量是否超过门限值并且在相应的索引位置没有元素
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容数组容量为以前的2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//重新获取扩容后的索引
bucketIndex = indexFor(hash, table.length);
}
//在索引位置插入键值对
createEntry(hash, key, value, bucketIndex);
}
负载因子默认的大小为0.75f, 也可以设置为0.1、0.7、2、3等,负载因子越小,bucket桶的数目越多,即数组的长度越长,元 素碰撞的可能性越小,读写的复杂度越低。
4.影响hashMap的性能因素是什么?
1.负载因子 :当负载因子小的时候,bucket数目多,元素发生碰撞的机会小,这是读写的复杂度是数组的读写复杂度O(1)
当负载因子大的时候,bucket数目少,元素发生碰撞的机会大,每个bucket都是一个链表,读写的复杂度是链表的读写复杂度O(n)
在JDK1.8中,当链表的长度大于8的时候,由链表转为红黑树,时间复杂度为O(lgn)
2. 哈希值
哈希值决定hashMap的key值散列是否均匀。key重写hashcode散列的越均匀,hashMap碰撞的机会越小。
5.hashMap为什么非线程安全?
hashMap的非线程安全是因为在resize扩容的时候,链表元素会从链表头取出,插入新的index下的链表的时候是从头插入,这样导致在原来链表元素的顺序和扩容后链表中元素的顺序是相反的。在这个过程中会出现死循环,最终导致hashMap的非线程安全。
例如:hashMap的size为2,有三个元素Entry<3,1>,Entry<5,2>, Entry<7,3> , 按照取模获取index的算法,hashMap的存储结构如下:
单线程情况下,当resize为4的时候,hashMap的存储结构如下:
在多线程的情况下。当resize为4的时候,resize的新table和旧table的transfer代码如下:
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,1], next = [7,3]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,3]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,1]迁移到新的数组上,然后再处理[7,3],而[7,3被链接到了[3,1]的后面,处理完[7,3]之后,就需要处理[7,3]的next了啊,而通过thread2的resize之后,[7,3]的next变为了[3,1],此时,[3,1]和[7,3]形成了环形链表,在get的时候,如果get的key的桶索引和[3,1和[7,3]一样,那么就会陷入死循环。
thread1:
thread2:
最终多线程结果:
6.作为HashMap的key需要满足什么条件?
- 必须重写hashCode和equals方法,1.hashCode让相同的对象,hashCode必须相同;2.不同的对象,hashCode可以相同也可以不同,但是必须通过==或euqals方法判断出相同的key,当然不同对象的hashCode不同的机会越大,那么hashMap的性能越好。
- key值是不可变的,如果key值可变,在put和get的时候的hashCode不一样,那么hashMap就不能正常工作了。
- String类重新实现了hashCode和equals方法,而且String是final不可变量,一般String作为key很合适。
7.HashMap非线程安全,那么可以用什么类代替HashMap实现多线程操作?
- HashTable是线程安全的,但是多线程情况下,效率低。因为HashTable是通过synchronized保证线程安全,当一个线程访问HashTable同步方法的时候,另外一个线程也访问HashTable的同步方法,会进入轮询或者是阻塞状态。所以线程安全但是效率低下。
- ConcurrectHashMap是线程安全的而且是高效的。因为ConcurrectHashMap采用分段锁技术,把数据分成一段一段存储,而且每一段数据都配一把锁,不同的数据段锁不同,当一个线程占用锁访问其中一个数据段的时候,其他数据段也能被其他线程访问。
- Map map=Collections.synchronizedMap(new HashMap<>()),synchronizedMap()返回由指定映射支持的同步(线程安全的)映射。实际上该方法只是一个工具方法, 将传入Map的实现方法加一个同步(synchronized)锁代理,内部还是调用实现的对应方法.
8.JDK1.8中HashMap做了哪些优化?
-
用Node<K,V>代替了Entry<K,V>static class Node<K,V> implements Map.Entry<K,V>
- JDK1.8中没有indexFor函数,直接用 tab[(n - 1) & hash]获取索引。
3. JDK1.8中在链表长度超过默认值8时,链表转为红黑树。红黑树增删改查的时间复杂度为O(lgn),提高了HashMap的性能。
检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转 换,先去扩充数组。当HashMap的数组的长度大于等于64的时候,才会把链表转为红黑树。
static final int TREEIFY_THRESHOLD = 8; // 当链表的长度大于8-1时,链表转为红黑树
//链表转为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
static final int UNTREEIFY_THRESHOLD = 6; //当链表的长度小于等于6时,红黑树转为链表
//红黑树转为链表
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
4.hashMap的扩容算法优化
JDK1.7需要把所有的元素都重新计算index并重新放入到新的index下的链表中。而JDK1.8通过算法
if ((e.hash & oldCap) == 0) 索引不变
if ((e.hash & oldCap) == 1) 索引+oldCap
例如:原HashMap的size为16,hash(key1)=1111 1111 1111 1111 0000 1111 1110 0101,hash(key2)=1111 1111 1111 1111 0000 1111 1111 0101;
扩容前索引 index1=hash(key1)&(16-1)=0101=5,
index2=hash(key2)&(16-1)=0101=5
扩容后索引 index1=hash(key1)&(32-1)=00101=5,
index2=hash(key2)&(32-1)=10101=16+5=21
所以只需要判断 hash(key1)&16=0, hash(key2)&16=1,为0则索引不变,为1则现索引=原索引+原来数组大小 .
JDK1.8put及resize的源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}