Map:双列数据,存储key-value键值对的数据
HashMap的底层:数组+链表(HashSet底层用的就是HashMap) (jdk7及之前)
数组+链表+红黑树 (jdk 8),红黑树防止数组位置上链表太长降低检索效率
面试题:
HashMap的底层实现原理?
HashMap 和 Hashtable的异同?
两者底层结构非常相似,hashMap线程不安全,他的key和value都可以是空的
CurrentHashMap 与 Hashtable的异同?(暂时不讲)
CurrentHashMap 实现分段锁,一个Map分为几段,多个线程同时访问多段,同一时间,本来一个Map只能被一个线程访问
HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
Entry实体里面封装key和value
map.put(key1,value1)//...可能已经执行过多次put...
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
如果key1的哈希值和已经存在的==某一个==(有时候应该不止一个,应该比较完整个链表的hashcode)数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法----情况3
* 进行比较:
* 如果equals()返回false,此时key1-value1添加成功。
* 如果equals()返回true:使用value1替换value2。
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,Entry总数量当超出临界值(且要==存放的位置非空,即Entry[i]非空==)时,扩容。因为Entry[i]为空,存储进去不需要进行对比hashcode值,而且说明可能很多索引位置都还是空的,不必急着扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
jdk8 相较于jdk7在底层实现方面的不同:
1.new HashMap():底层没有创建一个长度为16的数组
2.jdk 8底层的数组是:Node[],而非Entry[]
3.首次调用put()方法时,底层创建长度为16的数组(类似ArrayList在JDK 7和JDK 8中区别)
4.jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
public HashMap() {
//调用有参构造器,初始默认容量16,默认加载因子0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
//初始容量小于0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量大于最大容量
//static final int MAXIMUM_CAPACITY = 1 << 30;1左移30位
//00.....1变成010000.....0
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity,找到一个2的倍数,是大于被传进构造器的初始容量,保证Entry数组容量是2的倍数
int capacity = 1;
while (capacity < initialCapacity)
//capacity=capacity<<1,m每次左移1位,从0001变为010,100,1000直到大于初始容量
capacity <<= 1;
//加载因子,存储上了到达容量百分之几会扩容
this.loadFactor = loadFactor;
//threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//用min方法防止溢出
// transient Entry[] table;存储数据的数组
table = new Entry[capacity];
//
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
public V put(K key, V value) {
if (key == null)
//--------------------------------------
//putForNullKey 一号被引用方法
//--------------------------------------
return putForNullKey(value);
//根据算法值hash
int hash = hash(key);
//根据hash,取得应该存放于哪个位置
//--------------------------------------
//indexFor 三号被引用方法
//--------------------------------------
int i = indexFor(hash, table.length);
//若table[i]已存值,在table[i]位置的链表遍历,当存在hash值相等的entry,先判断key,和e.key是否引用同一个对象(地址一样)或用equals()判断它们是否属于同一个对象,同一个对象的话更改value值就可以
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//--------------------------------------
//addEntry 四号被引用方法
//--------------------------------------
//table[i]的链表还没有存在该key值的Entry
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
//将key=null的Entry存放于table[0](Entry数组),检索table[0]和它连接的链表是否已有key=null的Entry,没有直接添加,有的话更改那个节点的value值,没有直接添加节点
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
//recordAccess HashMap这个方法为空,子类中实现了它--在下方
//--------------------------------------
//recordAccess 二号被引用方法
//--------------------------------------
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
//LinkedHashMap中实现了这个方法。
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//accessOrder为true时,即当前被修改节点使用的是最近最少使用的次序
if (lm.accessOrder) {
//modCount修改次数
lm.modCount++;
//删除当前被修改节点
remove();
//将当前被修改的节点移动到header节点之前,即链表的尾部。
addBefore(lm.header);
}
}
static int indexFor(int h, int length) {
//根据h值,即hash找到元素应该在Entry数组存放位置
//&与%完全没关系,但效率更高
//&按位与操作符 两个操作数中位都为1,结果才为1,否则结果为0,即保存下两者间二进制数位都是1的位置
//h为得出哈希值hash,length为数组长度
//假设h=88,length=16,h=01011000,length-1=00001111
//h&(length-1)=00001000=8
return h & (length-1);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//size(已存放数据,包括链表上节点)大于等于临界值,且要存放数据的位置table[bucketIndex]已有数据,才扩容,因为即使超过临界值,但是所在索引为空存放进去也不麻烦,没有遍历流程,没必要立即扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容为用来2倍
//扩容改变数组长度length改变,hash可能也改变了,所以调用indexFor方法时得到的值,即存放于数组的位置改变了,所以链表可能会被改变
resize(2 * table.length);
//更新hash值
hash = (null != key) ? hash(key) : 0;
//更新bucketIndex值
bucketIndex = indexFor(hash, table.length);
}
//--------------------------------------
//createEntry 五号被引用方法
//--------------------------------------
//创建Entry同时头插法插入table[bucketIndex]
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//保存第一个table[bucketIndex]
Entry<K,V> e = table[bucketIndex];
//头插法插入Entry节点,注意之前的头节点e传入创建Entry的构造方法,保存在了创建的next指针
table[bucketIndex] = new Entry<>(hash, key, value, e);
//说明了size是hashMap中所有Entry数量
size++;
}
与jdk7不同,初始化HashMap时并没有立即创建数组
public HashMap() {
//加载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
存储数据实体变为Node
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V>
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;
//创建HashMap数组后的首次添加才进入if
//注意---------当table非空,会检测 (n = tab.length) == 0)同时 给n赋值n=tab.length
//注意给tab赋值 tab = table
if ((tab = table) == null || (n = tab.length) == 0)
///
//调用resize() 一号被调用方法
///
//初始化Node数组
n = (tab = resize()).length;
//当插入的位置为空,直接插入
//注意 同时给p赋值要插入那个位置的Node,给i赋值 要插入数组的位置的索引
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//插入位置非空
else {
Node<K,V> e; K K;
//p(即table[i])的key等于传进来的传参进来的key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//e占存p节点
e = p;
//p是红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
//为什么判断key是否一样放后面,因为第一个节点已经在前面判断equals
//尾插法,节点插入结尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当链表的长度大于TREEIFY_THRESHOLD(8),执行 treeifyBin方法判断是否要变成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
//
//treeifyBin() 二号被调用方法
///
treeifyBin(tab, hash);
break;
}
//e的key与传参key相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//此时e!=null
break;
p = e;
//只有两者方式能退出循环
//第一种,遍历到了尽头,e=p.next=null,不进入下方if结果改变节点value值
//第二种,找到key相等节点,e!=null,进入下方if结构,改变value值
}
}
//节点e的key和要存储的key一样,所以更改evlaue就行
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数组
Node<K,V>[] oldTab = table;
//oldCap为Node数组改变前的大小,刚创建HashMap,第一次添加数据为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//临界值一开始为0
int oldThr = threshold;
int newCap, newThr = 0;
//旧Node数组容量大于零
if (oldCap > 0) {
//当扩容前数组大小已经为了默认最大值
if (oldCap >= MAXIMUM_CAPACITY) {
//更改临界值为数组最大值保证之后不会扩容
threshold = Integer.MAX_VALUE;
//无法扩容,直接返回
return oldTab;
}
//oldCap进行扩容,左移一位,变成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 {
//创建完HashMap第一次put数据才会进入分支
//新容量和新临界值
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数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//更改指向
table = newTab;
//当旧Node数组非空,得进行复制工作
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;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//MIN_TREEIFY_CAPACITY=64,当数组小于64或数组为空,即使链表长度大于8,也不要改变链表为红黑树,而是扩容,就是你链表太长原因可能是数组长度太小,导致节点都挤到链表上,所以直接扩容一下,增加数组大小,这样改变了节点的存放位置,就不会挤在一起
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);
}
}
LinkedHashMap的底层实现原理(了解)
源码中:
//存储数据的实体改变了,增加了两个指向前后指针,按照添加进Map的时间顺序指向节点,可以按添加时间遍历实体
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);
}
}
//增加头节点和尾节点
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;