文章目录
Map简介
Map是一种键值对集合,每一个元素都包含一个键对象和一个值对象。其中键对象是不允许重复的。
Map接口与Collection接口是不同的,Map接口有两个主要实现类HashMap类
、TreeMap类
,HashMap类按哈希算法来存取键值对象,而TreeMap类可以对键值对象进行排序。
方法签名 | 说明 |
---|---|
V get(Object key) | 返回Map结合中指定键值对所对应的值 |
V put(K key, V value) | 向Map集合中添加键-值对,返回key以前对应的value,如果没有,则返回null |
V remove(Object key) | 从Map集合中删除key对应的键值对,返回key对应的value,如果没有则返回null |
Set entrySet() | 返回Map集合中所有键值对的Set集合,类型为Map内部类,Map.Entry |
Set keySet() | 返回Map集合中所有键对象的Set集合 |
在java1.8以后采用数组+链表+红黑树的形势来进行存储,通过散列映射来存储键值对
-
对key的hashcode进行一个取模操作得到数组下标
-
数组存储的是一个单链表
-
数组下标相同将会被放在同一个链表中进行存储
-
元素是无序排列的
-
链表超过一定长度(
TREEIFY_THRESHOLD=8
)会转化为红黑树 -
红黑树在满足一定条件会再次退回链表
HashMap
HashMap在我们Map接口的实现类中的使用中占有很高的频率,在初始化时将会给定默认容量为16
,扩容的加载因子为0.75
,当实例内的结点数量>16*0.75
就会进行扩容,每次扩容都是1<<2也就是2倍
。
Hashmap不能用于多线程场景中,多线程下推荐使用concurrentHashmap!
原因在于扩容机制!扩容机制!扩容机制! 下文会进行仔细剖析
那么接下来怎们结合使用代码,对HashMap的数据结构进行深入剖析。
Map hashMap=new HashMap(3);
hashMap.put("小林",666);
hashMap.put("小马",777);
那么在咱们HashMap中数据结构就应该只存了一个Node
其中这个Node[]数组的index
是通过key对象的hash值取模得出的,源码公式采用位运算(length-1)&hash
取模然后放置到Node数组的Node[(length-1)&hash]
。
当然如果小林和小马的hash值可能相同(发生哈希碰撞
),则将会在小林的出追加一个结点,形成了链表。
在追加节点时成为链表,那么这里就会分为头插法和尾插法了。
头插法
首先我们声明下单向链表结点
class ListNode {
//结点上存储的数据
int val;
//单向链表指针
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
public void setVal(int val) {
this.val = val;
}
public ListNode getNext() {
return next;
}
public void setNext(ListNode next) {
this.next = next;
}
}
接下来使用头插法进行数据存储
class List {
// 头结点
private ListNode headNode = new ListNode();
/**
* 头插法
*/
public boolean headInsert(int val) {
// 实例化新节点,也就是待插入结点
ListNode node = new ListNode();
// 先放入value对象
node.setVal(val);
// 待插入结点next指针指向头结点next指针域
node.setNext(headNode.getNext());
// 头结点指针指向待插入结点
headNode.setNext(node);
// 判断是否指向成功
if (headNode.getNext()==node){
return true;
}
return false;
}
}
头插法单向链表的插入动作如下图
尾插法
还是以上方头插法的结点数据结构为基础,咱们来简单表示下尾插法
class List {
// 尾结点
private ListNode tailNode = new ListNode();
/**
* 尾插法
*/
public boolean tailInsert(int val) {
// 实例化新节点,也就是待插入结点
ListNode node = new ListNode();
// 先放入value对象
node.setVal(val);
// 尾结点指针指向待插入结点
tailNode.setNext(node);
// 尾结点直接改变为当前插入的节点
tailNode=node;
// 判断尾结点是否是当前节点
if (tailNode.getNext()==null){
return true;
}
return false;
}
}
尾插法动作如下图
为什么Java8要使用尾插法呢
在Java7中Map的链表新增还是头插法,而在Java8中使用的就是尾插法了,其原因有两个:
- 头插法会造成死链
- Java7考虑是热点数据可能会更早的用到,如果发生链表迁移,头插法还是会扰乱插入的顺序
内部重要属性
/**
* 默认初始化容量,值为16,必须是2的n次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 默认加载因子
* 当size>capacity*DEFAULT_LOAD_FACTOR时则进行resize(扩容)
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表需要转红黑树时的长度。
* 此时未必会做换红黑树的操作,需要结合MIN_TREEIFY_CAPACITY,即链表长度达到8且容量达到64时,才会做红黑树的转换;
* 否则,进行扩容操作。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树转链表时的元素个数
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 链表树形化,最小数组容量阈值
* 数组容量超过这个,链表将会树形化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
不同类型结点结构
无论是LinkedMap
还是HashMap
内部都离不开Node
节点、这是我们数据存储的最基本结构,在Java7叫Entry
,在Java8叫Node
,其本质都是Map.Entry
的实现,但其内部结构却不太一样,可能是HashMap中是链表,LinkedMap中是双向链表,在TreeMap中可以排序,又可以是红黑树。
/**
* 链表结点,实现Map.Entry接口
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ...
}
/**
* LinkedHashMap中元素的结点类型
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
/**
* 红黑树结点,继承了LinkedHashMap.Entry,间接继承了HashMap.Node,所以也具有链表的性质。
* 实际上该结点类型既可以作为红黑树结点,又可以作为双向链表结点。
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
// ...
}
HashMap中的重要操作
在HashMap中,我们常用到get
、set
这些操作,但除了这些操作还有一些其他的链表转红黑树
、红黑树转链表
等一系列扩容或者是迭代的其他操作,通过这些操作影响着HashMap的内部结构,同时这也是HashMap之所以线程不安全、性能比较高的原因。
HashMap实例化
/**
* 可以通过构造参数,指定当前新Map的初始化容量
* @param initialCapacity 初始化容量
* @param loadFactor 影响扩容的加载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
put
/**
*
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash 键对象的hash值
* @param key 键对象
* @param value 键值对中的值对象,主要就是用于存储这个对象
* @param onlyIfAbsent 如果同样的键对象是否替换当前已存在的值对象
* @param evict if false, the table is in creation mode.
* @return 如果是替换的话则返回oldValue,否则返回null
*/
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;
}
treeifyBin树形化链表
/**
* 当达到两个阈值后,替换所有数组上的链表节点
*/
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);
}
}
resize扩容
Node数组的容量是优先的,当数据多次插入达到一定数量就会进行扩容resize()
那什么时候进行resize呢,有两个因素
- Capacity:HashMap当前的容量
- LoadFactor:负载因子
当Map的长度大于capacity * load factor
,如容量为16时,当Map中节点数量超过12则需要进行扩容,那么怎么扩容呢?
-
判断旧数组的容量是否大于等于最大容量
1<<30
,将新数组容量和阈值参数扩大2倍Node<K,V>[] oldTab = table; //判断当前数组的长度,如果是0则新建数组,容量和负载因子都是默认值16*0.75 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //判断当前数组是否有容量,且不超过最大容量1<<30 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //若有容量,并且新扩容的容量值没有超过最大容量值,则将阈值和容量扩大二倍,也就是容量总是2的幂次方 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 //如果阈值为0则使用默认值 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;
-
根据新阈值创建新数组
//根据新容量参数,创建一个新数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //Node数组引用指向新数组 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)// 表示j下标处只有一个结点 //根据新数组的容量值,取模计算在新数组中的位置 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 将红黑树分解为两个双向链表,如果双向链表长度大于6,则分解出来的双向链表转换为红黑树。 // 然后将双向链表或红黑树放入新table的相应下标处 ((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; // 将新的链表放入新table的相应下标处 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab;
-
get获取Value对象
返回key关联的value值;如果key不存在,返回null。
- 计算key的hash值,根据hash值计算key在table数组中的位置
-
如果该位置不为null,则比较key和其hash值是否相等
-
如果相等,则返回该结点
-
否则
- 如果结点是红黑树,则从红黑树中查找该key对应的结点
- 否则,就是从链表中查找该key对应的结点
- 否则,返回null
- 根据1获取结点的value值,或者返回null
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 实现了Map接口的get方法
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
//哈希值取模后在数组中存在至少一个节点,在该index位置有一个Node存在
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果链表的头结点不是目标元素,则判断是不是树形化的节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历这个链表,一直到找到key和hash值都相同的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove删除key对象
删除key,返回key关联的value;如果key不存在,返回null
-
查找key对应的结点
-
从链表或者红黑树删除该结点
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 实现了Map接口中remove方法
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
Conclusion
什么情况下会进行扩容?
- size属性 > 数组容量 * 加载因子时
- 为解决hash冲突而形成的链表长度 >= TREEIFY_THRESHOLD(8),但是table数组长度 <= MIN_TREEIFY_CAPACITY(64)时
什么情况下会进行转红黑树操作?
- 为解决hash冲突而形成的链表长度 >= TREEIFY_THRESHOLD(8),并且table数组长度 > MIN_TREEIFY_CAPACITY(64)时
- 在扩容时,如果从原红黑树拆解出的新的链表长度 >= TREEIFY_THRESHOLD(8)
为什么是非线程安全的?
为了性能及安全。在多线程环境下,请使用ConcurrentHashMap
- 如果多线程写HashMap实例,会有什么问题?
-
多线程put(不考虑resize)
如果hash冲突,后一个线程覆盖前一个线程的key-value,如在一系列线程中出现hash冲突的键值对个数为N,则最多可能丢失N-1个键值对,即只有一个线程的键值对最终被加入到table内的链表或红黑树中
-
多线程resize
A、B线程执行顺序如下
A线程
- oldTab = table; // 赋值旧数组的引用
- Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建扩容后的数组
- table = newTab; // 将新数组赋值给旧数组
- 将oldTab中的全部元素放入newTab // A线程还未执行到这里
B线程
- oldTab = table; // B线程拿到的table实际上是A线程刚创建的没有任何元素的newTab
- Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建扩容后的数组
- table = newTab; // 将新数组赋值给旧数组,这里会覆盖掉A线程创建的newTable
- 将oldTab中的全部元素放入newTab // B线程先于A线程执行完这一步骤,oldTab中没有任何元素,所以newTab中也不会有任何元素,即table中也不会有任何元素。这样,元素就全部丢失了
以上是全部丢失的情况,如果在A线程执行步骤4的过程中,B线程开始执行上述逻辑,那么有可能不会丢失全部元素,而是丢失A线程还未放入newTable的部分元素。
-
多线程remove