HashMap
HashMap的特点:
HashMap是key value键值对,并且是线程不安全的,key的值唯一,且允许一个null键和null值,是无序的,因为是通过hash和数组长度进行判定位置的。当存储相同key值时会进行一个替换操作。
HashMap源码1.7
//默认容量为16,1>>4 ; 扩容因子0.75
// 使用的是懒加载 再put的时候才进行一个new操作
//static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap(int initialCapacity, float loadFactor) {
//判断初始容量不小于0 否则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//判断是否大于1>>30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断扩容因子是否小于等于0 以及是否是不是数字得数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//赋值因子
this.loadFactor = loadFactor;
//赋值初始阈值
threshold = initialCapacity;
//初始化..查看为空
init();
}
跟踪一下put方法
public V put(K key, V value) {
//判断是否为空表
if (table == EMPTY_TABLE) {
//进行表初始化 -->1 调用无参构造的初始化容量值
inflateTable(threshold);
}
//判断key是否为null
if (key == null)
//-->4 对null进行一个赋值,默认数组下标为0 且hash值也为0
return putForNullKey(value);
//对key进行hash换算 -->5
int hash = hash(key);
//获取存储的数组下标 --> 6
int i = indexFor(hash, table.length);
//对数组下标为i的进行遍历,判断key是否存在,如存在则进行替换value 并且返回旧的value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断hash和(key地址或者key值)相等 则判定为相同的key
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
//返回旧值
return oldValue;
}
}
//计数自增
modCount++;
//进行一个添加keyvalue对象 (hash值,key,value,数组下标) -->7
addEntry(hash, key, value, i);
//返回null
return null;
}
//1
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize -->2
int capacity = roundUpToPowerOf2(toSize);
//将阈值更新,这个设置达到多少阈值既可达到扩容阈值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建一个初始容量的Entry
table = new Entry[capacity];
//根据需要初始化哈希种子
initHashSeedAsNeeded(capacity);
}
//2
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY //-->3
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//3 进行或运算和右移运算 返回向下最近的一个2的幂次方
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
//4 key值为null时
private V putForNullKey(V value) {
//判断数组下标为0的区域是否有值,如有则进行判断key是否为null,如存在 则进行value的替换
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//计数自增
modCount++;
//添加Entry hash为0 key为null value 数组下标为0
addEntry(0, null, value, 0);
//返回null
return null;
}
//5 获取key
final int hash(Object k) {
//假设为0
int h = hashSeed;
//为0 所以不进入此方法
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//h为0 k.hashCode值不变
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
//提高hash的一个散列性,让高位的值也能参与运算
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//6 计算存储的数组下标
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//对hash值和数组长度-1进行一个与运算
//如果容量不是2的幂次放的话,这里的运算可能会出现问题
return h & (length-1);
}
//7 添加新元素
void addEntry(int hash, K key, V value, int bucketIndex) {
//进行判断 当前数组数量是否大于等于扩容阈值并且数组下标不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
//进行2倍扩容 还是2的幂次方 -->10
resize(2 * table.length);
//处理新元素的hash,判断是否为null,是则设置为0
hash = (null != key) ? hash(key) : 0;
//重新设置了数组下标
bucketIndex = indexFor(hash, table.length);
}
//进行创建 --> 8
createEntry(hash, key, value, bucketIndex);
}
//8 创建元素
void createEntry(int hash, K key, V value, int bucketIndex) {
//获取数组下标entry对象
Entry<K,V> e = table[bucketIndex];
//进行创建entry对象, --> 9 e为数组的头对象,设置为下一个entry对象
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
//9 新增对象 并且对变量进行赋值
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//10 扩容数组
void resize(int newCapacity) {
//获取一个旧的数组
Entry[] oldTable = table;
//获取旧数组长度
int oldCapacity = oldTable.length;
//进行判断是否为最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个新的数组并且设置新的容量大小
Entry[] newTable = new Entry[newCapacity];
//-->11 调用了一个扩容并转移数据的方法 , -->12 设置散列种子 判断是否进行rehash -->13
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//将新容量的数组进行赋值
table = newTable;
//再次设置新容量数组的一个阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//11
void transfer(Entry[] newTable, boolean rehash) {
//获取新容量数组的长度
int newCapacity = newTable.length;
//遍历旧数组的元素
for (Entry<K,V> e : table) {
//循环遍历链表中的entry
while(null != e) {
//获取下一个entry
Entry<K,V> next = e.next;
//判断是否重新hash,基本不会进入到该方法
if (rehash) {
//判断key是否为null 为null返回0 不为null重新进行hash
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新获取下标
int i = indexFor(e.hash, newCapacity);
//设置该数组下标的元素为下一个元素
e.next = newTable[i];
//进行头插
newTable[i] = e;
//将下一个对象放入e进行下一次的循环
e = next;
}
}
}
//12
final boolean initHashSeedAsNeeded(int capacity) {
//hashSeed默认为0 所以为false
boolean currentAltHashing = hashSeed != 0;
//capacity>= Holder.ALTERNATIVE_HASHING_THRESHOLD
//ALTERNATIVE_HASHING_THRESHOLD这个值再加载时使用了静态代码块进行赋值 最大值 2147483647
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//一个false 一个false 所以结果为false
boolean switching = currentAltHashing ^ useAltHashing;
//false
if (switching) {
hashSeed = useAltHashing
//进行了一个随机散列种子的操作
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
//返回false
return switching;
}
//13 所以一开始ALTERNATIVE_HASHING_THRESHOLD就是一个integer的最大值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
//注意这里
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
//所有这个值是很大的
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
1.7的方法还是简单很多的,相比1.8
1.7是数组+链表, 1.8是数组+链表/红黑树
1.7的HashMap是一个头插法,在多线程下会出现一个死链的情况,所以在1.8使用了一个尾插。
HashMap容量
HashMap的容量无论初始给多少,(无参构造的初始容量是1>>4 得到16)它都会去进行一个换算成大于等于该值的一个2的幂次方,后面在计算存储数组的一个下标值时,会进行减1操作,这时二进制都为1,和hash进行一个与运算。能减少一个哈希冲突,让node节点的存储更散一些。
1.7的扩容是在put新节点时进行的,每新增加key,value成功一次,就会让size++,扩容的条件需要size>=阈值时(阈值等于数组长度*加载因子 0.75) 并且 需要新增的节点的存储 node[]下标不为null, 才会进行一个扩容,这样算的话,理想状态默认容量16的话,size某个链表为7,然后其余都为1, size可以达到26 ,之后才必须扩容,这是理想状态下的。
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
里面用了或运算和右移 得到2的幂次方,因为int是4字节的 所以占用的是32位,这里1 2 4 8 16 就是一个32 ,这里设计的很巧妙,进行一个或运算右移把最高位和最低位运算都为1, 最后进行一个减去右移一位的结果 得到一个2的幂次。
HashMap1.8
1.8加了一个红黑树的操作,红黑树提高了查询速度,当然不是一创建就是红黑树,需要等到链表达到一个红黑树的阈值,并且node数组容量大于64,才会进行红黑树的替换。
红黑树相比链表 查询速度更快,但是内存占用更多。
1.8的table创建也是一个懒加载,在初次调用put的时候才进行的创建,默认容量是16, 扩容条件是size > 阈值 时进行扩容
1.7的初始化方法和扩容方法时分开的,
但1.8初始化方法和扩容方法是放到了一起
put的一个执行流程
当初始容量有进行赋值时会进入该方法
public HashMap(int initialCapacity, float loadFactor) {
//判断容量是否小于0 如果是 则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//判断初始容量设置是否大于2的30幂次 如果大于,则等于2的30幂次方
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断扩容因子小于等于0 判断是否是正确数字 如果不是则报错
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//设置扩容因子 0.75f
this.loadFactor = loadFactor;
//默认的话这个阈值为0,不为0的话会进行换算为大于该值的一个2的幂次方赋值给这个阈值,进行初始化时会用到
this.threshold = tableSizeFor(initialCapacity);
}
这里面的tableSizeFor()方法会把你传进来的初始容量参数进行运算,这里按位或和右移运算的是将传过去的值运算成一个大于等于该值的最近的一个2的幂次方。
static final int tableSizeFor(int cap) {
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;
}
无参会进入这个方法:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
只是对初始的一个加载因子进行赋值,0.75f;
table的一个创建是在put,是一个懒加载的模式。
当调用put的方法
有点长,这里都是自己的一些理解,可能会有一些错误。
红黑树没有进行一个深入
public V put(K key, V value) {
//进行一个hash运算,主要是计算key的哈希值,并且进行一个运算
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
//获取key的hash和hash右移16位进行异或运算得到一个哈希值,这里是为了让高位也能参与运算
//否则只有低位的参与运算会导致过多的哈希冲突
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
会先进行一个hash算法,得到一个hash值,再调用putVal方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //node数组
Node<K,V> p; //node对象
int n, i; //n i 变量
//将table赋值给新创建的tab,并且判断是否为null,或tab.length是否为空
if ((tab = table) == null || (n = tab.length) == 0)
//进行resize初始化 -->1 并且将n进行赋值
n = (tab = resize()).length;
//进行运算出当前新添加node对象的一个数组下标 然后进行一个p的赋值
if ((p = tab[i = (n - 1) & hash]) == null)
//因为该下标中没有node节点,所以该设置该值为首节点
tab[i] = newNode(hash, key, value, null);
else {
//设置一个e 和一个k的变量
Node<K,V> e; K k;
//判断该首节点下标的hash是否和该新增node节点的hash相同,并且key的地址相等或key不等于null并且key的值相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将p赋值给e
e = p;
//判断是否此节点为红黑树对象
else if (p instanceof TreeNode)
//进入红黑树的新增方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//进入普通添加的方法,进行for循环,没有设置结束条件,需进入方法break结束
for (int binCount = 0; ; ++binCount) {
//前面p进行了赋值,这里将p的下一个节点赋值给了e
//再判断p的下一个节点是否为null
//如果为null则说明p就是尾节点了 这时null会赋值给e
//所以e为null旧不会进入下面一个替换的判断了
if ((e = p.next) == null) {
//直接将准备添加的node节点设置p的next节点
p.next = newNode(hash, key, value, null);
//并且进行一个判断,该节点的链表是否大于等于7,binCount为循环次数
//TREEIFY_THRESHOLD是红黑树的一个阈值默认为8
//当循环满足8 则进入红黑树转换的方法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//红黑树替换链表的方法
treeifyBin(tab, hash);
//退出循环
break;
}
//判断是否当前节点hash与新添加的节点hash相同并且key的值也相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//如果相同 则退出循环
break;
//将当前对象赋值给p
p = e;
}
}
//如果没有相同的hash和key相同的节点 则不会进入该判断,该判断为替换
if (e != null) { // existing mapping for key
//获取旧的value值
V oldValue = e.value;
//判断基本为true
if (!onlyIfAbsent || oldValue == null)
//将value赋值替换旧的value
e.value = value;
//翻译是 (回调,允许LinkedHashMap的后操作)不细究
afterNodeAccess(e);
//返回旧的value值
return oldValue;
}
}
//计数自增
++modCount;
//++size 判断是否大于阈值,如果大于则进入扩容方法
if (++size > threshold)
//扩容方法
resize();
//翻译是 (回调,允许LinkedHashMap的后操作)不细究
afterNodeInsertion(evict);
//正常新增 返回null 运行结束 hashSet在添加新元素时也利用了这个null 去判断值是否存在
return null;
}
//1 进行一个初始化操作 返回一个初始化的Node数组
final Node<K,V>[] resize() {
//获取旧的node数组
Node<K,V>[] oldTab = table;
//得到旧的node数组的长度,判断是否为null,null则赋值0 不为则赋值长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取阈值(初始值是16)
int oldThr = threshold;
//设置新的长度和阈值大小
int newCap, newThr = 0;
//判断旧的node数组长度是否大于0
if (oldCap > 0) { //初始化不会进入该方法
//判断是否大于最大值,如是则赋值最大值并返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//判断旧的长度左移一位是否小于最大值并且赋值给新的长度 和旧的长度是否大于等于一个初始容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//对旧阈值进行一个左移一位 (16 << 1 = 32)
newThr = oldThr << 1; // double threshold阈值 待会会进行计算 也2幂次进行增加
}
//判断旧阈值是否大于0,若自定义大小的话
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);
}
//判断新的阈值是否等于0,如果走的是默认容量大小就不会进入这个方法了 走自定义参数时会进入这个方法
if (newThr == 0) {
//进行运算 得到一个阈值
float ft = (float)newCap * loadFactor;
//赋值newThr
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];
//对全局node数组进行重新赋值
table = newTab;
//判断旧数组不为null, 初始化时不进入该方法,接下去就是扩容的方法了
if (oldTab != null) {
//对旧长度进行一个循环 ++j 和 j++ 是一样的 我还疑惑了一下。。
for (int j = 0; j < oldCap; ++j) {
//设置一个e,为当前数组下标对象
Node<K,V> e;
//尝试获取j下标的数组,对e进行赋值,不为null才进入 否则进入下一次循环
if ((e = oldTab[j]) != null) {
//将旧数组下标为j的设为null,因为已经赋值给了e 方便GC
oldTab[j] = null;
//判断链表的下一个对象是否为null,为null 则说明该node为最后一个
if (e.next == null)
//获得e的hash和新容量大小-1进行与运算得到新的数组下标,将e进行赋值
newTab[e.hash & (newCap - 1)] = e;
//判断该数组下标的节点是否为红黑树结构
else if (e instanceof TreeNode)
//进行强转为红黑树,进入红黑树的扩容方法
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//设置hash和旧数组的长度与运算为null的首节点,为null的尾节点
Node<K,V> loHead = null, loTail = null;
//设置hash和旧数组的长度与运算不为null的首节点,不为null的尾节点
Node<K,V> hiHead = null, hiTail = null;
//next节点
Node<K,V> next;
do {
//e获取下一个节点并且赋值
next = e.next;
//判断e的hash和旧数组长度进行按位与运算,结果要么为0要么有值
//为0则添加到新数组同样的位置
//不为0则添加到原来的位置加旧数组的一个长度的位置
if ((e.hash & oldCap) == 0) {
//首次进入会为null
if (loTail == null)
//e赋值给首节点
loHead = e;
else
//尾节点不为null,则设置到尾节点的next
//1.7是用了一个头插法,1.8这里是用了一个尾插法
//将节点添加到最后
loTail.next = e;
//e设置为尾节点
loTail = e;
}
else {
//进这里说明与运算的结果不为null
//判断尾节点是否尾null
//首次为null
if (hiTail == null)
//对首节点进行赋值
hiHead = e;
else
//设置e为尾节点的next,是尾插法
hiTail.next = e;
//将e赋值给尾节点
hiTail = e;
}
//当next == null的时候退出循环
} while ((e = next) != null);
//进行判断尾不为null
if (loTail != null) {
//设置尾节点的next为null操作
loTail.next = null;
//将首节点添加到j 该j为旧数组下标相同的位置
newTab[j] = loHead;
}
//进行判断尾不为null
if (hiTail != null) {
//设置尾节点的next为null操作
hiTail.next = null;
//将首节点添加到j加上旧数组长度 的新数组下标
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新的node[]数组
return newTab;
}