1 创建集合对象
HashMap<String,Integer> map = new HashMap<>();
1.1 JDK1.8之前
构造方法创建一个长度为16的Entry[] table
数组用来存储键值对数据
1.2 JDK1.8之后
不是在HashMap的构造方法的时候创建数组,而是在第一次调用put()
方法时创建数组Node[] table
用来存储键值对数据
2 存储数据
HashMap中的hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
情况一:向hash表中存入(柳岩-18)数据时,调用String类中重写后的
hashCode()
方法再结合数组的长度计算出Node节点的索引(例如是3),如果此节点空间中没有数据,则将Node节点中的值存入数组中
情况二:向hash表中存入(刘德华-40)数据时,调用String类中重写后的
hashCode()
方法再结合数组的长度计算出Node节点的索引(3),此节点的数组空间不为null,此时便会调用HashMap中的hash()
方法进行二次hash后比较hash值是否相等,如果不相等,则在此空间上创建一个新的节点来储存该键值对中的信息(这时就形成了链表结构
)
情况三:在情况二的基础下,在调用了HashMap中的
hash()
方法进行了二次计算后的hash值仍然相等,这时便会调用(柳岩所属类)中的equals()
方法比较两个的内容是否相等
1.相等:现添加的value会覆盖原来的value
2.不相等:继续向下和其他数据的key进行比较,如果都不相等,则创建一个新的节点进行储存
注
:内容不同计算的hash值也有可能相同
eg:
System.out.println("重地".hashCode());//1179395
System.out.println("通话".hashCode());//1179395
3 HashMap继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap继承AbstractMap这个抽象类,AbstractMap又实现了Map接口
为什么还要去实现Map接口?
这是一个失误,错误的写法
HashMap实现了Cloneable和Serializable接口
Cloneable:HashMap可以对自己的对象进行克隆
Serializable:序列化接口。可以对对象进行序列化和反序列化
4 HashMap集合类的成员变量
序列化版本号
private static final long serialVersionUID = 362498820763181265L;
4.1 集合的初始化容量
<<阿里巴巴Java开发者手册>>建议
初始化容量值
尽量设为(需要储存的元素个数/负载因子)+1
,尽量减少扩容的几率
(必须是2的n次幂)默认的初始容量为16,1<<4 == 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
构造方法可以指定集合的初始化容量的大小
HashMap(int initialCapacity)
//构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
4.1.2 源码解析
假设传入的初始化容量是10,由于HashMap的Capacity必须都是2的n次幂,
因此会用该方法找到大于等于传入参数的最小的2的n次幂的数
public HashMap(int initialCapacity) {
//传入指定的初始化容量和默认的负载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
调用tableSizeFor()
方法对传入的容量进行处理
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;
//调用tableSizeFor()方法对传入的容量进行处理
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor()
方法
static final int tableSizeFor(int cap) {
//此处若不减一,传入的如果是2的n次幂的值时,返回的容量会扩大两倍
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
4.2 默认的负载因子
默认为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.3 集合的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
4.4 树化阈值
static final int TREEIFY_THRESHOLD = 8;
4.4.1树化阈值为何是8?
Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use(see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. Inusages with well-distributed user hashCodes, tree bins arerarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with aparameter of about 0.5 on average for the default resizingthreshold of 0.75, although with a large variance because ofresizing granularity. Ignoring variance, the expectedoccurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are
0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
由于树节点的大小约为常规节点的两倍,因此我们仅在容器包含足够的节点以保证使用时才使用它们(请参见树节点阈值)。当它们变得太小时(由于移除或调整大小),它们会被转换回普通垃圾箱。在具有分布良好的用户哈希代码的消息中,很少使用树仓。理想情况下,在随机哈希码下,箱中节点的频率遵循泊松分布(http://en.wikipedia.org/wiki/Poisson_distribution)默认大小阈值为0.75时,平均参数约为0.5,但由于大小调整粒度,差异较大。忽略方差,列表大小k的预期发生率为(exp(-0.5)*pow(0.5,k)/阶乘(k))。
4.5 红黑树退化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
4.6 链表转化为红黑树对应数组长度的最小值
static final int MIN_TREEIFY_CAPACITY = 64;
4.7 (重点)
table是用来初始化(必须是2的n次幂)
存储元素的数组
transient Node<K,V>[] table;
实现了Map.Entry<>接口,用来存放键值对数据的
static class Node<K,V> implements Map.Entry<K,V>
4.8 用来存放缓存
存放具体元素的集合
transient Set<Map.Entry<K,V>> entrySet;
4.9 (重点)
HashMap中存放元素的个数
存放元素的个数,注意这个不等于数组的长度
size为HashMap中键值对的实时数量,不是table的长度
transient int size;
4.9 用来记录HashMap的修改次数
transient int modCount;
4.10 (重点)
临界值
临界值,当实际大小超过临界值时,会进行扩容
int threshold;
4.11 (重点)
哈希表的加载因子
final float loadFactor;
计算HashMap实时的加载因子的方法为 size/capacity
扩容非常消耗性能,应在创建集合对象时指定较好的数组容量,减少扩容次数
5 HashMap构造方法
1.构造一个空的 HashMap,默认初始容量(16)和负载因子为(0.75)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
2.构造一个具有指定初始容量和默认负载因子(0.75)的HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3.构造一个具有指定初始容量和负载因子的HashMap
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);
}
4.包含另一个Map的构造函数
//构造一个映射关系与指定Map相同的新HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
putMapEntries()
方法
为什么要+1.0F
?float ft = ((float)s / loadFactor) + 1.0F;
答:初始容量指定较大,尽量减少扩容(resize)次数
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
6 (重点)
HashMap的成员方法
6.1 增加方法(put&putValue)
6.1.1 put方法
put()
方法实现大致如下
①HashMap是惰性创建数组的,首次使用也就是首次调用put方法时,才会创建数组
②计算索引(桶下标)
③如果桶下标还没人占用,创建Node占位返回
④如果桶下标已有人占用
④.1已经是TreeNode走红黑树的添加或更新逻辑
④.2是普通Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
⑤返回前检查容量是否超过阈值,一旦超过进行扩容
具体的方法如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
真正往数组中插入元素的是putVal()
方法,hash()
方法计算key的hash值
算出key的hashCode,然后再将算出来的值与原来算出的hashCode的值进行按位异或运算得到
hash()
方法运算出来的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
接下来就调用putValue()
方法
6.1.2 putValue方法
onlyIfAbsent:为true表示不更改现有的值
evict:如果为false表示table为创建状态
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//调用putValue方法时才会创建数组
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断计算出的索引下标对应数组中的位置的节点是否为null,同时判断计算出当前数组长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//n为数组长度
//[i = (n - 1) & hash],计算索引值,替换了 [hash % n]
//判断计算出的索引下标对应数组中的位置的节点是否为null
if ((p = tab[i = (n - 1) & hash]) == null)
//为null则将存入的数据存在数组对应的节点空间中,且当前节点node.next为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) {
//p为当前节点,e为p的下一个结点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断链表长度是否大于树化阈值
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断当前size是否达到了扩容的临界值
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
6.2 转化为红黑树的方法(treeifyBin&treeify)
//判断链表长度是否大于树化阈值
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
treeifyBin()
方法如下
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//判断数组的长度是否大于MIN_TREEIFY_CAPACITY(64)
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();//不大于64先进行扩容,改变putValue()方法中的TREEIFY_THRESHOLD,再进行判断,直到数组长度等于64时,才进行下一步else if中的代码执行
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;//hd为头节点,tl为尾结点
do {//执行循环直到e.next不为null,索引节点的内容赋给头节点,然后头节点不变,依次移动tl(尾)节点、p节点、e节点,实际上相当于一个双向链表
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)
//这里的treeify()方法经过自旋平衡等一系列操作将其变为红黑树
hd.treeify(tab);
}
}
replacementTreeNode()
方法就是创建了一个新的树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
6.3 扩容方法(reSize)
①如果元素个数大于临界值就需要进行扩容
②在树化之前也会进行判断,容量没达到64时,就会先扩容,不会先进行树化
reSize()
方法源码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原来的临界值
int oldThr = threshold;
//定义新的容量和新的临界值
int newCap, newThr = 0;
if (oldCap > 0) {
//在旧容量的基础上判断其是否大于MAXIMUM_CAPACITY(1<<30)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//不大于MAXIMUM_CAPACITY(1<<30),新容量等于旧容量的两倍
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
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
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个新容量为原容量两倍的新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//传进来的数组若不为空
//for循环进行遍历,就是将旧数组的值赋给新数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//用e节点来存储旧数组的内容
Node<K,V> e;
//将旧数组当前j索引位置的值赋给e
if ((e = oldTab[j]) != null) {
//将旧旧数组所占用的空间释放
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//判断e节点是否为树节点,若为树节点就需要将其拆分进行存储
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//下个节点有数据且不是红黑树(就是链表)
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//判断hash值 & 旧容量是否为0,若为0则放在原位置;不为0,新位置索引为 [原位置索引+旧容量]
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;
}
6.4 移除方法(remove&removeNode)
remove()
方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
removeNode()
方法
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;
//判断要删除的元素和传递的key值是否相等
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)
//将node的下一个节点赋给数组索引位置
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
6.5 查找方法(get&getNode)
get()
方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode()
方法
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))))
return first;
//不是第一个元素的齐纳提下判断是否为链表
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;
}
若为树结构,查找方法名为find()
,查找之前树已经是有序的了,故此处使用折半查找效率更高
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
7 遍历HashMap的四种方式
7.1 jdk1.8后使用Map接口的默认方法
/**
* default void forEach(BiConsumer<? super K,? super V> action)
* 对此映射中的每个条目执行给定的操作,直到所有条目都被处理或操作引发异常。
* 参数:
* BiConsumer 消费接口
* 抽象方法:void accept(T t, U u) 对给定的参数执行此操作。
* 参数:t :key
* u :value
*
* @param map
*/
private static void method3(HashMap<String, Integer> map) {
map.forEach((key,value) -> {
System.out.println(key+":"+value);
});
}
7.2 Iterator迭代器方式
private static void method1(HashMap<String, Integer> map) {
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Integer> next = iterator.next();
System.out.println(next.getKey()+":"+next.getValue());
}
}
7.3 遍历键和遍历值
private static void method(HashMap<String, Integer> map) {
for (String key : map.keySet()) {
System.out.println(key);
}
for (Integer value : map.values()) {
System.out.println(value);
}
}
7.4 get方法(不推荐)
private static void method2(HashMap<String, Integer> map) {
Set<String> keySet = map.keySet();
for (String s : keySet) {
System.out.println(s+":"+map.get(s));
}
}