目录
1.简述JAVA的HashMap
JDK8
之前底层实现是数组
+
链表,
JDK8
改为数组
+
链表
/
红黑树。主要成员变量包括存储数据的
table
数组、元素数量
size
、加载因子
loadFactor
。
HashMap
中数据以键值对的形式存在,键对应的
hash
值用来计算数组下标,如果两个元素
key
的
hash
值一样,就会发生哈希冲突,被放到同一个链表上。
table
数组记录
HashMap
的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry
节点包含四个成员变量:
key
、
value
、
next
指针和
hash
值。在
JDK8
后链表超过
8
会转 化为红黑树。
若当前数据
/
总数据容量
>
负载因子,
Hashmap
将执行扩容操作。
默认初始化容量为
16
,扩容容量必须是
2
的幂次方、最大容量为
1<< 30
、默认加载因子为
0.75
。
2.HashMap的底层结构和扩容机制
1)底层结构:
HashMap
内部使用数组存储数据,数组中的每个元素类型为
Node<K,V>
:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Node 包含了四个字段: hash 、 key 、 value 、 next ,其中 next 表示链表的下一个节点。
2)扩容机制:
1.执行 put() , 该方法会执行 hash(key) 得到 key 对应的 hash 值 算法
h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2.执行putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table 就是 HashMap 的一个数组,类型是 Node[]
//if 语句表示如果当前 table 是 null, 或者 大小=0
//就是第一次扩容,到 16 个空间.
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(1)根据 key,得到 hash 去计算该 key 应该存放到 table 表的哪个索引位置
//并把这个位置的对象,赋给 p
//(2)判断 p 是否为 null
//(2.1) 如果 p 为 null, 表示还没有存放元素, 就创建一个 Node(key="java",value=PRESENT)
//(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//(1) 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象
//(2) p 指向的 Node 结点的 key 的 equals() 和准备加入的 key 比较后相同
//就不能加入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断 p 是不是一颗红黑树,
//如果是一颗红黑树,就调用 putTreeVal , 来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果 table 对应索引位置,已经是一个链表, 就使用 for 循环比较
//(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
// 注意在把元素添加到链表后,立即判断 该链表是否已经达到 8 个结点
// , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
// 注意,在转成红黑树时,要进行判断, 判断条件
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
// resize();
// 如果上面条件成立,先 table 扩容.
// 只有上面条件不成立时,才进行转成红黑树
//(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break
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;
//size 就是我们每加入一个结点 Node(k,v,h,next), size++
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put 操作过程总结:1. 判断 HashMap 数组是否为空,是的话初始化数组(由此可见,在创建 HashMap 对象的时候并不会直接初始化数组);2. 通过 (n - 1) & hash 计算 key 在数组中的存放索引;3. 目标索引位置为空的话,直接创建 Node 存储;4. 目标索引位置不为空的话,分下面三种情况:4.1. key 相同,覆盖旧值;4.2. 该节点类型是红黑树的话,执行红黑树插入操作;4.3. 该节点类型是链表的话,遍历到最后一个元素尾插入,如果期间有遇到 key 相同的,则直接覆盖。如果链表长度大于等于 TREEIFY_THRESHOLD ,并且数组容量大于等于MIN_TREEIFY_CAPACITY ,则将链表转换为红黑树结构;5. 判断 HashMap 元素个数是否大于等于 threshold ,是的话,进行扩容操作。
3.HashMap和Hashtable的区别
1. HashMap
是
Hashtable
的轻量级实现,
HashMap
允许
key
和
value
为
null
,但最多允许一条记录的
key
为
null.
而
HashTable
不允许。
2. HashTable
中的方法是线程安全的,而
HashMap
不是。在多线程访问
HashMap
需要提供额外的同步
机制。
3. Hashtable
使用
Enumeration
进行遍历,
HashMap
使用
Iterator
进行遍历。
4.如何决定使用HashMap还是TreeMap?
如果对
Map
进行插入、删除或定位一个元素的操作更频繁,
HashMap
是更好的选择。如果需要对
key
集
合进行有序的遍历,
TreeMap
是更好的选择。
5.HashMap和ConcurrentHashMap区别
HashMap虽然效率高,但是线程不安全的,所有我们在开发中,为了解决线程安全使用ConcurrentHashMap。它运用局部加锁的方式,提高并发下的完成效率。(Hashtable是全员加上synchronized方法进行加锁,效率慢,不推荐使用。)