HashMap源码分析-基于JDK1.8
基本结构
1)、初始变量
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1073741824;
//默认扩展因子,达到容量的一定系数就开始扩容
static final float DEFAULT_LOAD_FACTOR = 0.75F;
//转为红黑树结构的
static final int TREEIFY_THRESHOLD = 8;
//当桶(链表)节点数大于这个值时会转为红黑树
static final int UNTREEIFY_THRESHOLD = 6;
//桶结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组
transient HashMap.Node<K, V>[] table;
//存放具体元素的值
transient Set<Entry<K, V>> entrySet;
//存放元素的个数
transient int size;
//每次扩容修改结构map结构的计数器
transient int modCount;
//临界值,当实际大小超过临界值会扩容
int threshold;
//加载因子
final float loadFactor;
}
2)、链表结构
存放的是键值对。
static class Node<K, V> implements Entry<K, V> {
final int hash; //hash值
final K key;
V value;
HashMap.Node<K, V> next; //下一个节点
//.......省略......
public final String toString() {
return this.key + "=" + this.value;
}
//key的hash值与value的hash值的异或结果
public final int hashCode() {
return Objects.hashCode(this.key) ^ Objects.hashCode(this.value);
}
.......省略........
//判断链表值是否相等,key相等且value相等
public final boolean equals(Object var1) {
if (var1 == this) {
return true;
} else {
if (var1 instanceof Entry) {
Entry var2 = (Entry)var1;
if (Objects.equals(this.key, var2.getKey()) && Objects.equals(this.value, var2.getValue())) {
return true;
}
}
return false;
}
}
}
3)、红黑树结构
继承于LinkedHashMap,提高查找效率。
static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
HashMap.TreeNode<K, V> parent; //父节点
HashMap.TreeNode<K, V> left; //左节点
HashMap.TreeNode<K, V> right;
HashMap.TreeNode<K, V> prev;
boolean red; //颜色标志
TreeNode(int var1, K var2, V var3, HashMap.Node<K, V> var4) {
super(var1, var2, var3, var4);
}
//返回根节点
final HashMap.TreeNode<K, V> root() {
HashMap.TreeNode var1 = this;
while(true) {
HashMap.TreeNode var2 = var1.parent;
if (var1.parent == null) {
return var1;
}
var1 = var2;
}
}
......省略.......
}
hash算法
- 计算key的hashcode —h
- 对h无符号向右位移 16位 i
- h和i做异或运算。使得高位也可以参与hash,更大程度上减少了碰撞率。
static final int hash(Object var0) {
int var1;
return var0 == null ? 0 : (var1 = var0.hashCode()) ^ var1 >>> 16;
}
重要方法分析
1)、存放数据putVal方法
对外方法
//计算key的hash值,调用putval方法
public V put(K var1, V var2) {
return this.putVal(hash(var1), var1, var2, false, true);
}
final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
HashMap.Node[] var6 = this.table;
int var8;
//数组是否存在,或长度是否为0
if (this.table == null || (var8 = var6.length) == 0) {
//是->进行扩容
var8 = (var6 = this.resize()).length;
}
Object var7;
int var9;
//计算当前索引,判断table[i]是否存在
if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
//不存在--->新建节点
var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
} else {
Object var10;
label79: {
Object var11;
//存在---判断key值是否在数组中存在
if (((HashMap.Node)var7).hash == var1) {
var11 = ((HashMap.Node)var7).key;
//判断key值是否相等
if (((HashMap.Node)var7).key == var2 || var2 != null && var2.equals(var11)) { //是 --->替换旧值
var10 = var7;
break label79;
}
}
//table[i] 是否是红黑树结构
if (var7 instanceof HashMap.TreeNode) {
//是--加入红黑树结构
var10 = ((HashMap.TreeNode)var7).putTreeVal(this, var6, var1, var2, var3);
} else {
//否--->遍历链表
int var12 = 0;
while(true) {
//是否有下一个节点
var10 = ((HashMap.Node)var7).next;
if (((HashMap.Node)var7).next == null) {
//无,创建节点
((HashMap.Node)var7).next = this.newNode(var1, var2, var3, (HashMap.Node)null);
//节点数是否 >= 7
if (var12 >= 7) {
//节点大于8 ,转为红黑树结构
this.treeifyBin(var6, var1);
}
break;
}
//hash判断key是否存在
if (((HashMap.Node)var10).hash == var1) {
var11 = ((HashMap.Node)var10).key;
if (((HashMap.Node)var10).key == var2 || var2 != null && var2.equals(var11)) {
break;
}
}
//存在--->替换旧值
var7 = var10;
//节点数的计数器
++var12;
}
}
}
if (var10 != null) {
Object var13 = ((HashMap.Node)var10).value;
if (!var4 || var13 == null) {
((HashMap.Node)var10).value = var3;
}
this.afterNodeAccess((HashMap.Node)var10);
//返回旧值
return var13;
}
}
++this.modCount;
//数组长度是否大于临界值
if (++this.size > this.threshold) {
//扩容
this.resize();
}
this.afterNodeInsertion(var5);
return null;
}
putval调用流程图如下:
HashMap存储原理:
- 根据key计算key.hashcode = (h = hash(key) ^ h>>>16).
- 根据key.hash 计算得到桶的索引 i。
- 如果该索引位置无数据 table[i],则用该数据生成一个新的节点
- 如果该索引有数据且是一个红黑树,在红黑树中执行更新或新增操作
- 如果该索引有数据且是一个链表,遍历链表,在链表结尾创建节点,如果节点数超过7 ,转为红黑树,判断key的hash值是否相等,key及value相等跳出循环。
2)、get方法
public V get(Object var1) {
HashMap.Node var2;
return (var2 = this.getNode(hash(var1), var1)) == null ? null : var2.value;
}
//计算key的hash值,根据key.hash取值
final HashMap.Node<K, V> getNode(int var1, Object var2) {
HashMap.Node[] var3 = this.table;
HashMap.Node var4;
int var6;
//数组是否存在,长度 ,根据keyhash计算的索引对应的数据 table[i]
if (this.table != null && (var6 = var3.length) > 0 && (var4 = var3[var6 - 1 & var1]) != null) {
Object var7;
//根据keyhash相同找位置 ----其实查找的是第一项数据
if (var4.hash == var1) {
var7 = var4.key;
//equals方法判断key是否一致
if (var4.key == var2 || var2 != null && var2.equals(var7)) {
return var4;
}
}
HashMap.Node var5 = var4.next;
//存在链表
if (var4.next != null) {
//链表是红黑树--从红黑树中取
if (var4 instanceof HashMap.TreeNode) {
return ((HashMap.TreeNode)var4).getTreeNode(var1, var2);
}
//遍历链表--获取value
do {
if (var5.hash == var1) {
var7 = var5.key;
if (var5.key == var2 || var2 != null && var2.equals(var7)) {
return var5;
}
}
} while((var5 = var5.next) != null);
}
}
return null;
}
HashMap取数据原理
- 计算key的hash值
- 判断table及根据key.hash计算的索引的table[i]值是否!=null.
- 开始查找数据
- 查找第一个元素,是-则返回
- 是否是红黑树,是—则在红黑树中查找
- 遍历链表查找keyhash相等且使用equals方法相等的。
3)、resize()方法
- 当数组大小达到临界值或者在初始化的时候,则开始进行扩容
- 每次扩容都是原来容量的2倍
- 扩展后Node对象的位置要么不变,要么是原来位置的两倍。
- 扩容的原理:重新创建一个
new HashMap.Node[var4]
,将老的数据复制到新的Node,并将老的置空。
区别
原理:
HashMap实现原理:JDK1.8以前是数组+链表结构,jdk1.8 改为数组+链表+红黑树结构。当链表的节点大于等于8的时候就会使用红黑树结构。
Hashtable实现原理:数组+链表结构。
**HashSet实现原理:**是简单类型的HashMap,值是固定的,只存储key值。
主干table是元素为链表的数组,链表是为了解决hash冲突的,当使用key值计算的hash地址一致时,在该下标下增加链表,存放及查找时需要遍历数组先查下标,如果该下标有链表还需要遍历链表,通过key值的equals方法判断取值或存值。
1、初始化
- HashMap:初始化容量为16(必须是2的倍数)
- Hashtable:初始化容量为11
两者的加载因子都是0.75.
2、线程是否安全
- HashMap:线程不安全,如果是单线程操作,效率较高。如果要使HashMap线程安全,可以使用Collection.synchronizedMap(hashmap)进行同步。
- Hashtable:线程安全,内部方法都使用了
synchronized
保证线程安全,因此单线程环境下他比HashMap慢。
3、空键值
- HashMap:键值都可以为null ,key为null 时放在下标为1的位置。
- Hashtable:键值不可以为null
4、扩容
- HashMap:扩容大小是原大小的2倍
- Hashtable:扩容大小是*2+1。
HashSet是存储了一个固定值的的HashMap,结构及初始化基本相同,具体实现上有区别,set实现的是Set接口,map实现的是map接口。两者hashcode的算法不一样,HashMap是使用key计算hash值,hashset使用成员对象计算hash值,并使用对象的equals方法判断对象的想等性。
参考文案链接