提示:这篇文章由于我懒癌犯了因此有很多内容都是不全的,文章的最后有参考文章部分,可以结合着看。
最后更新于2021年3月3日08:58:49
目录
HashMap
HashMap是基于哈希表的Map接口的实现,允许使用null值和null键。HashMap是非线程安全的,也就是说在多个线程同时对HashMap中的某个元素进行增删改操作的时候,是不能保证数据的一致性的。
HashMap的底层基于数组和链表实现。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。
HashMap中计算Hash值:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
//若key=null则键值对存入索引为0的桶
//否则计算key的hashcode并将其右移16位做异或运算得到索引值(即hash值)。
//右移是为了减少碰撞防止hash冲突。
//异或运算能够尽量保留各部分特征
}
HashMap底层通过链表解决hash冲突,如图
hashmap内部结构
HashMap以键值对形式储存数据,每个键值对都存储在Entry<K,V>这样的对象中。
static class Entry<K,V> implements Map.Entry<K,V>
{
final K key;
V value;
Entry<K,V> next;
int hash;
//Some methods are defined here
}
hashmap关键属性
transient Entry[] table;//存储元素的实体数组
transient int size;//存放元素的个数
int threshold; //临界值 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
final float loadFactor; //加载因子
transient int modCount;//被修改的次数
若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
hashmap的put
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
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(hash, key, value, i);
return null;
}
步骤:
1.空key放到table[0]位置
2.若不是空key,计算键对应的哈希值
3.indexFor()方法返回特定键值对在table中的索引
4.查找该索引下的链表中是否有相同的key,是的话,用心值取代旧值;不是的话,加入该键值对到链表末尾。
hashmap的扩容(这个被问到了,看一下)
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];
transfer(newTable);//用来将原先table的元素全部移到newTable里面
table = newTable; //再将newTable赋值给table
threshold = (int)(newCapacity * loadFactor);//重新计算临界值
}
新建了一个HashMap的底层数组,上面代码中第10行为调用transfer方法,将HashMap的全部元素添加到新的HashMap中,并重新计算元素在新的数组中的索引位置。
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
当HashMap中的元素个数超过数组大小乘以loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过 16乘0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
hashmap扩容涉及到元素迁移:
JAVA8中的元素迁移:
这是java8的resize函数注释:Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table. 初始化或翻倍表格大小,如果表格为空,根据字段阈值中的初始容量目标分配大小。否则,由于我们根据二的幂次扩张,每个bin中的元素要么留在原index,要么在新表中做一个二的幂次的偏移。
hashmap的get
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K , V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
1.空key?是的话调用getForNullKey()方法。
2.不是空key,计算对应的hash值。
3.indexFor()方法找到对应索引。
4.对应索引上找相同的key并返回value,无相同key则返回null。
jdk1.8新增
链表转化为红黑树
链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树
put方法
1.若发生了hash冲突,还要判断此时的数据结构是什么
2.若此时的数据结构是红黑树,那就直接插入红黑树中
3.若此时的数据结构是链表,判断插入之后是否大于等于8
4.插入之后大于8了,就要先调整为红黑树,再插入
5.插入之后不大于8,那么就直接插入到链表尾部即可。
小知识
异或运算:将十进制转成二进制后比较每一位,相同为0,不同为1。例如15^2=13.
红黑树:红黑树是一个自平衡的二叉查找树,查找效率会从链表的o(n)降低为o(logn)。
参考:
你能谈谈HashMap怎样解决hash冲突吗
How HashMap Works Internally In Java?
idea版Java就业班
HashMap源码分析(jdk1.8,保证你能看懂)