Map接口是保存一元偶对象的最大接口:
**区别:**TreeMap和HashMap(都是有序的Map集合):
LinkedHashMap是HashMap的子类,有序Map,序指的是插入顺序,元素的添加顺序;
而TreeMap有序Map,序指的是Comparator或Compareable
常用方法?
put和get
3.Map集合遍历
Map—>Set?
Set接口与Map接口的关系:
Set接口是穿了马甲的Map接口,本质上Set接口的子类都是使用Map来存储元素的,都是讲元素存储到Mapkey值而已,value都是用共同的一个空Object对象。
4.常见子类的解析:
##HashMap:
1.成员变量:树化和数据结构:
初始化容量:(桶的数量)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16
static final int MAXIMUM_CAPACITY = 1 << 30;
负载因子:(扩容时用到的)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
树化的阈值(门限值)
static final int TREEIFY_THRESHOLD = 8;
树化的最少元素个数 默认64
static final int MIN_TREEIFY_CAPACITY = 64;
解树化,返回链表的阈值:
static final int UNTREEIFY_THRESHOLD = 6;
真正存储元素的Hash表
transient Node<K,V>[] table;
树化逻辑:当一个桶中链表元素个数>=8并且哈希表中所有元素个数加起来超过64,此时会将此桶中链表转为红黑数结构。
将链表变为哈希数的原因是,为了提高查找效率,由于链表过长而导致查找太慢,由原来的O(n)变成Olog(n),最主要的是减少Hash碰撞(安全性问题)
若只是链表个数大于8,哈希表元素不超过64,此时只是简单的resize而已,并不会树化
2.构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap的无参构造只是初始化负载因子而已(不在对象产生是初始化哈希表)。HashMap同样采用lazy—load策略
HashMap可以传入初始化容量和负载因子。
3.put与get流程
put():
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;
//取得当前元素的哈希表
//如果当前hash表还未初始化
if ((tab = table) == null || (n = tab.length) == 0)
//resize()完成哈希表的初始化操作
n = (tab = resize()).length;
//哈希表的(长度-1)取哈希,根据key值hash后的桶得到桶下标,并且此时桶中元素个数为空
if ((p = tab[i = (n - 1) & hash]) == null)
//将要保存的结点放置到此桶的第一个元素
tab[i] = newNode(hash, key, value, null);
else {//Hash表已经初始化
Node<K,V> e; K k;
//否则当key值相等时,替换value值
//p.hash是当前要放置元素的hash。
//要插入的结点是当前桶,要插入的k与
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//hash(key)桶中元素不为空,判断此桶是否已经树化
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;
//判断添加元素之后整个哈希表是否超过threshold = 容量(默认16)*赋载因子
if (++size > threshold)
//若超过,调用resize()方法扩容
resize();
afterNodeInsertion(evict);
return null;
}
1.若hash还未初始化,先进性哈希表的初始化操作(默认初始化为16个桶)。
2.对传入的key值做hash得出要存放该元素的桶编号。
a.若无发生碰撞,即头结点为空,将该节点直接存放到桶中,作为头结点
b.若发生碰撞,
此桶中的链表已经树化,将结点构造成树节点后加入红黑树
链表还为树化,将结点作为链表最后一个元素插入链表
3.若hash表中存在key值相同的元素,替换最新的value值。
4.若桶满了 ,判断resize++是否大于Threshold,调用resize()方法扩容hash表
threshold=容量(默认16)*赋载因子
4.哈希算法,扩容,性能
HashMap中的hash方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.调用key值的hashCode():但是因为数值太大,如果作为桶下标,所以无法碰撞,将高位的16移到低位。hash表就和普通数组没有区别。(每个元素Hash出来都在不同的桶中);
2.为何取出h>>>16?
为何取出key值的高16位右移16位参与hash运算?
因为hash基本上是在高16位进行hash运算。
3.为何HashMap容量均为2n(如果传入的容量不知2n,将其扩容成最近的2^n)?
要用与运算代替取模运算提高效率。
(n-1)&hash:当n为2^n,此时的位运算就相当于hash%(n-1);
get()
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode(int hash, Object key)
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 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//当前节点key值刚好是第一个节点,变量桶的其他节点,返回其value
return first;
//遍历桶的其他节点找到指定key返回其value
if ((e = first.next) != null) {
//树的遍历
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//链表的遍历
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
1.当表还未初始化,或者key值为null,返回空值
2.表已经初始化并且key不为null
a.key值刚好是桶的头结点,直接返回
b.遍历桶的其他节点
·若已经树化,调用树的变量方式找到key对应的结点Node返回
·调用链表的遍历方式找到指定key对应的Node返回
重点
HashMap的resize()方法:
扩容桶的个数,扩容为原Hash表的二倍,
原来桶中的元素会进行一个rehash的过程,要么在原桶,要么在其二倍的桶中。
final Node<K,V>[] resize() {
//当前hash表
Node<K,V>[] oldTab = table;
//如果当前表为空,容量为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//当前Hash表已经到大最大值,返回最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容原hash表的二倍
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;
//对原hash表的元素进行rehash
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;
}
1、赋值哈希表的初始化操作
2、当表中元素个数到达阈值:容量*负载因子后进行扩容操作
3.扩容后,原来元素进行rehash:要么元素还呆在原桶中,要么呆在double桶中
性能问题
1.在多线程线程下:
在竞争激烈的场景下使用HashMap会造成CPU彪到100%,解决:
使用:ConcurrenHashMap来代替HashMap
2.性能的主要开销:
在resize()方法后的rehash过程,并且开销越来越大。
解决:在能预估存放元素个数的前提下传入适当的初始化参数来尽量避免resize()过程
补充:
在resize过程中若发现桶下的红黑树结点,小于<=UNTREEIFY_THRESHOLD,会将红黑树解除树化还原为链表结构。
HashTable
单纯的Hash表实现,用的是synchronized;
在put、get、remove等方法上使用方法级别的内建锁,锁的是Hashtable对象,即整个Hash表。
问什么效率低:两个线程共同访问Hashtable,但是访问是不同的桶,这时线程1,拿到锁,即便线程2,要进入的桶与线程1不冲突,还是无法访问桶。
如何此性能问题优化?
JDK8之前ConcurrentHashMap思路:(JDK7)
通过锁细粒度化,将整表拆分为多个锁进行优化。
实现思路:
将原先的16个桶设计为16个Segment,每个Segment都有独立的一把锁,拆分后的每个Segment都有相当于原先的一个HashMap(double—hash设计,并且Segment在初始化后无法扩容,每个Segment对应的哈希表可以扩容,扩容Segment下的哈希表)。
线程安全:使用ReentranLock保证相应Segment下的线程安全
JDK8下的ConcurrentHashMap
整体结构与HashMap别无二致,都是使用哈希表+红黑数结构
线程安全:使用内建锁synchronized+CAS锁每个桶的头结点,使得锁进一步细粒度化。
ConcurrentHashMap不允许键值对为空。
JDK7和JDK8 ConcurrentHashMap的变化:
1.结构上的变化:取消原来的Segment设计,取而代之的是使用与HashMap同样的数据结构,
即哈希表+红黑数,并引入了懒加载。
2.线程安全上变化:
锁粒度更细:由原来的锁一篇区域到锁桶的头结点,
由原先的RenntrantLock替换为Synchronize+CAS:现版本的synchronized已经进过了不断优化性能上与ReentrantLock基本没有差异,并且相对于ReentrantLock,节省了大量内存空间,这是问什么替换的原因。