Map是我们平时使用非常频繁的一种集合,因为Map 提供了一个更通用的元素存储方法。Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值,其主要的实现类有HashMap,Hashtable,ConcurrentHashMap(JDK1.8)
- HashMap
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
这里要先了解两个概念:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
//我们先看其构造方法
//默认的加在因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
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);
}
//hashMap提供了四种构造方法。包括可以自定义哈希因子,自定义初始容量。
//再看其添加的方法
transient Node<K,V>[] table;
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//先计算key的hashCode。这里进行了非空判断。所以key值可以为null。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//这里先判断 tab数组是不为null。如果为null则调用resize()。生成默认长度的node数组。
//如果数组不为null会将原来数组的数据复制进新的扩容数组。代码太长不贴了。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过hash值判断对象应该存储在哪个 数组位置。如果数组索引位置为null则直接放入。
//如果不为null。放入数组的链表结构中
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;
}
上面的存储过程看着很晕。结合别人的理解可以大致理解为。
- 校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
- 通过hash值计算索引位置,将该索引位置的头节点赋值给p节点,如果该索引位置节点为空则使用传入的参数新增一个节点并放在该索引位置
- 判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
- 如果p节点不是目标节点,则判断p节点是否为TreeNode,如果是则调用红黑树的putTreeVal方法查找目标节点
- 走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,并定义变量binCount来统计该链表的节点数
- 如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部,并校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点
- 如果遍历的e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
- 如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
- 如果插入节点后节点数超过阈值,则调用resize方法进行扩容
数据结构如下
这里再贴一个大神的扩容步骤总结
1.如果老表的容量大于0,判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表(此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大);如果容量 * 2小于最大容量并且不小于16,则将阈值设置为原来的两倍。
2.如果老表的容量为0,老表的阈值大于0,这种情况是传了容量的new方法创建的空表,将新表的容量设置为老表的阈值(这种情况发生在新创建的HashMap第一次put时,该HashMap初始化的时候传了初始容量,由于HashMap并没有capacity变量来存放容量值,因此传进来的初始容量是存放在threshold变量上(查看HashMap(int initialCapacity, float loadFactor)方法),因此此时老表的threshold的值就是我们要新创建的HashMap的capacity,所以将新表的容量设置为老表的阈值。
3.如果老表的容量为0,老表的阈值为0,这种情况是没有传容量的new方法创建的空表,将阈值和容量设置为默认值。
4.如果新表的阈值为空,则通过新的容量 * 负载因子获得阈值(这种情况是初始化的时候传了初始容量,跟第2点相同情况,也只有走到第2点才会走到该情况)。
将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将当前的表设置为新定义的表。
5.如果老表不为空,则需遍历所有节点,将节点赋值给新表。
将老表上索引为j的头结点赋值给e节点,并将老表上索引为j的节点设置为空。
如果e的next节点为空,则代表老表的该位置只有1个节点,通过hash值计算新表的索引位置,直接将该节点放在新表的该位置上。
6.如果e的next节点不为空,并且e为TreeNode,则调用split方法进行hash分布。
如果e的next节点不为空,并且e为普通的链表节点,则进行普通的hash分布。
如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为0,则说明e节点扩容后的索引位置跟老表的索引位置一样(见例子1),进行链表拼接操作:如果loTail为空,代表该节点为第一个节点,则将loHead赋值为该节点;否则将节点添加在loTail后面,并将loTail赋值为新增的节点。
7.如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为1,则说明e节点扩容后的索引位置为:老表的索引位置+oldCap(见例子1),进行链表拼接操作:如果hiTail为空,代表该节点为第一个节点,则将hiHead赋值为该节点;否则将节点添加在hiTail后面,并将hiTail赋值为新增的节点。
8.老表节点重新hash分布在新表结束后,如果loTail不为空(说明老表的数据有分布到新表上原索引位置的节点),则将最后一个节点的next设为空,并将新表上原索引位置的节点设置为对应的头结点;如果hiTail不为空(说明老表的数据有分布到新表上原索引+oldCap位置的节点),则将最后一个节点的next设为空,并将新表上索引位置为原索引+oldCap的节点设置为对应的头结点。
9.返回新表。
贴一个大神的详细分解讲解的很透彻:
https://blog.youkuaiyun.com/v123411739/article/details/78996181 - Hashtable
这个集合类在1.8中并无太大改变。主要是保证了线程的安全。这个我们在其方法中就可以看出。
先看构造方法
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
//可以看出他的构造方法和hashMap提供的差不多。不同在于初始容量为11.负载因子均为0.75
//还有一个区别就是hashtable直接初始化了数组。而hashMap是在放入第一个对象的时候调用扩容的方法进行数组创建
再看其添加元素的方法
//方法加了同步锁
public synchronized V put(K key, V value) {
// 所以value值不能为null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
//第一方法进行了同步锁定,
//第二他没有采用hashMap的数组+链表+红黑树的结构。它采用的是传统的数据+链表结构。
//先计算hash值。查出数组位置再进行链表循环判断看是否存在。如果存在且key值相等则覆盖。
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// 如果容量超出。则进行扩容操作,扩容会进行hash值从新计算。重新放置位置
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
//将新的对象放入头部。并将原来的头部entry变为其next;
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
//再看扩容代码
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//这个循环进行了链表的重新分配。
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
移除方法
//也是加入了同步锁
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//先定位数组位置
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
//循环遍历链表
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
//找到对映的节点。并且移除它的节点关联
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
- ConcurrentHashMap
主要解决了HashMap线程安全问题。虽然hashtable也是安全的但是。从其安全处理方式以及数据结构来看。性能都极大的被降低。所以不推荐使用。ConcurrentHashMap其数据结构和HashMap结构类似。我们通过源码分析主要查看其如何保证线程安全。
//被关键字volatile 修饰。保证了多线程之间的可见性
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile CounterCell[] counterCells;
//再看它的添加方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//所以key和value不能为null
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果数组为null则初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果hash对应的索引数组头部为null。则加入进去
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果哈希值计算不正确无法找到对应的数组所以。则进行调整。如果无法调整则无法加入进去
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//锁定这个索引数组存储的对象,这样不会影响其他链表的操作。提高了性能。保证了安全性
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//遍历链表查找hash对应的节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//如果节点大于8则进行转换成红黑树结构
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//如果容量超出进行扩容操作
addCount(1L, binCount);
return null;
}
//再看其移除方法
public V remove(Object key) {
return replaceNode(key, null, null);
}
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
//也是在确定数组索引位置后进行锁定。保证线程的安全性
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
这里再贴一个大神1.6版本的分析
https://blog.youkuaiyun.com/v123411739/article/details/65944448
相对以前的版本主要进行了2方面的改动
改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
总结
三种类型的map可以说在1.8版本差异化明显。hashMap无法保证线程安全。但是拥有良好的性能以及新的数据结构给予的快速查询速度。hashTable保证了线程安全。但是其锁定方式以及数据结构影响了整体性能,基本不用。而ConcurrentHashMap是在hashMap进行线程安全优化。保证了其线程的安全性。