目录
4.1 为什么直接采用hashCode() 处理的hashcode,作为存储数组table的下标位置
4.2 为什么采用哈希码与运算(数组长度-1) 计算数组下标
4.3 为什么在计算数组下标前,需要对哈希码进行二次处理:扰动处理
大纲:
1.数据结构
2.基础知识
3.源码
4.存在的问题
HashMap结构,根据键的hashCode值存储数据,大多数情况能够直接定位到它的值,
1.数据结构
jdk1.7中 hashmap 采用的数据结构是数组+单链表;这种结构方式成为拉链法,是用来处理哈希冲突的方法之一:哈希冲突就是,存储两个不同的元素,这两个元素通过哈希函数得出的实际存储地址相同,导致插入的时候,发现数组的位置已经被占用;链地址的解决方案就是:hash值相同的元素,以链表的方式存储在数组中;数组是HashMap的主体,链表则是为了解决哈希冲突而存在的
HashMap的主干是一个Entry类型的数组。Entry也是HashMap的基本组成单元,每一个Entry都包含一个key-value键值对,
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next; //存储指向下一个Entry的引用,形成了解决hash冲突的单链表
int hash; //对key的hashcode值进行hash运算后得到的值,存储在Entry,防止重复计算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
根据这个结构,当插入或者查找的时候,如果数组上不包含链表:即(table[i]==null || table[i].next==null);那么HashMap的插入和删除的复杂度都是O(1);而如果数组上存在链表的话,则可能需要遍历链表,复杂度就是O(n);
2.基本知识
以后补充
3.源码
3.1属性
HashMap的几个属性
public class HashMap<K,V>extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
{
//默认初始容量(16)-必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量,如果HashMap的构造函数隐士的指定了更高的容量,则使用该容量,同样必须是2的幂
static final int MAXIMUM_CAPACITY = 1 << 30;
// 构造函数未指定时使用的负载系数:(负载系数与HashMap的扩容有关;当当前容量size>(loadFactory*capacity)进行扩容)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//共享的空表实例,用来判断当前hashmap实例是否需要初始化;
static final Entry<?,?>[] EMPTY_TABLE = {};
// 数组table;根据需要调整大小,长度必须始终是2的幂;
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//hashmap的大小:(键值对映射数=数组长度中的键值对+链表中的键值对)
transient int size;
//阈值:当size达到这个阈值时,hashmap会进行扩容;
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75;假设初始桶大小是16,负载因子是0.75;那么当hashmap的size到达了 16*0.75=12时,就会进行扩容,扩容是扩容到当前容量的2倍; resize(2 * table.length);
final float loadFactor;
// hashmap被改变的次数,由于HashMap的非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生了变化,就会抛出异常(ConcurrentModificationException)
transient int modCount;
//映射容量的默认阈值,超过该阈值时,可以使用替代哈希用于字符串键。由于字符串键的哈希代码计算能力较弱,替代哈希减少了冲突的发生率。可以通过定义系统属性{@code jdk.map.althashing.threshold}来覆盖该值。属性值{@code-1}强制始终使用替代哈希,而{@code-1}值确保从不使用替代哈希。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
3.2 构造函数
构造函数有四个;可以发现在四个构造函数中,除了最后一个指定了map的构造器之外,其余均没有为数组table分配内存空间;(在执行put操作的时候才真正构建table数组)
1.仅接受初始化容量大小(capacity)、加载因子(Load factor),但仍无真正初始化哈希表table,
2、真正初始化哈希表(初始化存储数组table)是在第一次添加键值对时,即第一次调用put()时;
//创建一个空的hashMap,(指定初始容量以及负载因子)
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//HashMap的最大初始容量只能是MAXIMUM_CAPACITY,哪怕传入的初始容量>MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init(); //init在Hashmap中没有实际实现,其子类LinkedHashMap中有对应实现
}
//构造一个空的HashMap(指定初始化容量,默认负载因子0.75)
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//创建一个空的hashMap,使用默认的初始化容量16,以及默认的负载因子0.75;
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//通过一个给定Map,创建一个同样映射的新的HashMap,这个hashmap,通过默认负载因子和初始化容量(Max(map/0.75),默认初始化容量);
public HashMap(Map<? extends K, ? extends V> m) {
//设置容量大小& 加载因子=默认
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//该方法用于初始化数组&阈值;
inflateTable(threshold);
//将传入的map全部元素逐个添加到HashMap中
putAllForCreate(m);
}
3.3 PUT
public V put(K key, V value) {
// 前面提过,构造函数中,并没有为哈希表(数组table)进行真正初始化,为table分配实际内存空间;第一次初始化时,threshold为initialCapacity,在构造函数中指定过
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//处理key为null的情况
if (key == null)
return putForNullKey(value);
//通过key计算hash值,同时根据计算后的hash值确定该key在table里的索引i
int hash = hash(key);
int i = indexFor(hash, table.length);
//遍历table[i]处的链表,确定链表中是否有与key相等的键值对;如果有,将链表中的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;
}
}
//hashmap结构被改变次数加1
modCount++;
//走到这里,就意味着要么table[i]处链表为空,要么,就是遍历完之后没有找到对应key值,需要在table[i]上添加新的节点
addEntry(hash, key, value, i);
return null;
}
下面将逐一分析初始化函数inflateTable(threshold)
;处理空键函数putForNullkey(value)
;hash和索引计算函数hash(key) indexFor(hash,table.length)
、添加新的节点函数 addEntry(hash,key,value,i)
以及后面的扩容函数resize(2*tableLength)
3.3.1 初始化inflateTable(threshold)
private void inflateTable(int toSize) {
// 保证capacity一定是2的次幂;
int capacity = roundUpToPowerOf2(toSize);
//设置阈值,保证阈值不会超过最大容量
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
3.3.2 处理空键函数putForNullKey(value)
// 当key==null时,将该key-value的存储位置规定在数组table中的第一个位置,即table[0]
private V putForNullKey(V 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++;
addEntry(0, null, value, 0);
return null;
}
3.3.3 hash和索引计算
1.hash
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
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).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
2.indexFor
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
3.3.4 添加新的节点函数 addEntry(hash,key,value,i)
//在这里是线判断是否需要扩容,再创建节点
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //扩容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建节点
createEntry(hash, key, value, bucketIndex);
}
//注意这里:1.7里面使用的是头插法;先用一个节点e指向table[bucketIndex],也就是链表原头节点。然后再把新节点插入链表头节点;最后再把新节点的next指向e;
void createEntry(int hash, K key, V value, int bucketIndex) {
//保存table原位置的节点
Entry<K,V> e = table[bucketIndex];
//在table中bucketIndex的位置,新建一个Entry,并把key,value放在这个entry中,同时把entry.next指向table原位置的节点e
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
3.3.5 扩容 resize(2 * table.length)
void resize(int newCapacity) {
//保存旧数组
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值。退出,不进行后续扩容。
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建新table:容量为 2*oldCapacity;
Entry[] newTable = new Entry[newCapacity];
//将旧数组上的数据(键值对) 转移到新table中,从而完成扩容 ❤❤
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//把新table引用到hashmap的table属性上;
table = newTable;
//设置新的阈值;不能超过系统默认最大容量
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//把为扩容前的旧table上的数据迁移到扩容后的新table;
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历未扩容前的旧table:
for (Entry<K,V> e : table) {
//处理旧table上的每一个索引处的链表;
while(null != e) {
//因为链表是单链表,为了防止后续节点丢失,使用next保存当前节点的下一个节点;
Entry<K,V> next = e.next;
//如果要 重新计算哈希值,计算哈希值
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 重新计算每个元素的存储位置;
int i = indexFor(e.hash, newCapacity);
//下面两步就是1.7中hashmap扩容后可能出现逆序的原因;新表中的头节点之前是旧表的节点e的上一个节点;即
// oldtableNode.next=e
//旧表的节点e的next指向 新表中的头节点;
e.next = newTable[i];
//新表i处存储旧表的节点e
newTable[i] = e;
//继续遍历旧表的节点e的下一个节点;
e = next;
}
}
}
3.4 GET
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key); // 通过key来获得entry
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key); //计算hash值
for (Entry<K,V> e = table[indexFor(hash, table.length)]; //通过hash值得到key在table中的下标;遍历所在链表
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;//匹配到直接返回
}
return null;
}
3.5 remove
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//得到hash值以及下标;
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e) //如果第一次遍历正好遍历到了,这个时候e恰好还是=prev;因此直接让prev=e.next=next;
table[i] = next;
else
prev.next = next; //不是第一次遍历了,这个时候prev.next=e; 直接prev.next=e.next=next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
4.几个问题
4.1 为什么直接采用hashCode() 处理的hashcode,作为存储数组table的下标位置
答:hashcode算出来不一定在数据的大小范围内;计算下标通过 hash&(n-1) 确保映射到数组的范围内;
4.2 为什么采用哈希码与运算(数组长度-1) 计算数组下标
通过与运算,可以保证计算出来的数组下标一定位于 数组的范围内;同时由于数组的长度始终是2的次幂大小;这样可以保证length-1保证二进制时,低位全部为1.可以使得数组的索引index更加均匀
4.3 为什么在计算数组下标前,需要对哈希码进行二次处理:扰动处理
答:加大哈希码低位的随机性,使得分布更加均匀,从而提高对应数组存储下标位置的随机性&均匀性。最终减少hash冲突;
4.4 与jdk1.8中有什么区别
1.数据结构
版本 | JDK1.7 | JDK1.8 |
---|---|---|
数据结构 | 数组+链表 | 数组+链表+红黑树 |
数组&链表节点的实现类 | Entry类 | Node类 |
红黑树的实现类 | / | TreeNode类 |
核心参数: 主要参数相同:容量、加载因子、扩容阈值 JDK1.8中增加了红黑树的相关参数: 1.桶的树化阈值: 链表转成红黑树的阈值,在存储数据时,当链表长度>该值时,则将链表转换成红黑树。 2、桶的列表还原阈值:红黑树转为链表的阈值; 3、最小树形化容量阈值:当哈希表中的容量>该值时,才允许树形化链表;否则,若桶内元素太多,则直接扩容,而不是树形化
2.获取数据时
版本 | JDK1.7 | JDK1.8 |
---|---|---|
初始化方式 | 单独函数infalteTable() | 直接集成在扩容函数resize()中 |
hash值计算方式 | 1.hashCode; 2扰动处理=9次扰动=4次位运算=5次异或运算 | 1.hashCode; 2. 扰动处理 =2次扰动=1次位运算+1次异或运算 |
存放数据的规则 | 数组+链表 1.无冲突时,存放数组 2.有冲突时,存放链表 | 数组+链表+红黑树 1.无冲突时,存放数组 2.冲突&链表长度<8;存放到单链表 3.冲突&链表长度>8;存放到红黑树 |
插入数据的方式 | 头插法 (先将原位置的数据移到后1位,再插入数据到该位置) | 尾插法 (直接插入到链表尾部) |
3.扩容机制
版本 | JDK1.7 | JDK.18 |
---|---|---|
扩容后存储位置的计算方式 | 全部按照原来方法进行计算 (hashCode()->扰动处理->(h&length-1)) | 按照扩容后的规律计算 (即扩容后的位置=原位置or原位置+旧容量) |
转移数据的方式 | 头插法 | 尾插法 |
需插入数据的插入时机&位置重计算时机 | 扩容后插入,单独计算 | 扩容前插入、转移数据时同一计算 |