哈希表(HashTable,散列表)是根据key-value进行访问的数据结构,他是通过把key映射到表中的一个位置来访问value,加快查找的速度,其中映射的函数叫做散列函数,存放value的数组叫做散列表,哈希表的主干是数组。
存在的问题就是不同的值在经过hash函数之后可能会映射到相同的位置上,当插入一个元素时,发现该位置已经被占用,这时候就会产生冲突,也就是所谓的哈希冲突,因此哈希函数的设计就至关重要,一个好的哈希函数希望尽可能的保证计算方法简单,但是元素能够均匀的分布在数组中,但是数组是一块连续的且是固定长度的内存空间,不管一个哈希函数设计的多好,都无法避免得到的地址不会发生冲突,因此就需要对哈希冲突进行解决。
(1)开放定址法:当插入一个元素时,发生冲突,继续检查散列表的其他项,直到找到一个位置来放置这个元素,至于检查的顺序可以自定义;
(2)再散列法:使用多个hash函数,如果一个发生冲突,使用下一个hash函数,直到找到一个位置,这种方法增加了计算的时间;
(3)链地址法:在数组的位置使用链表,将同一个hashCode的元素放在链表中,HashMap就是使用的这种方法,数组+链表的结构。
哈希桶(buckets):在 HashMap 的注释里使用哈希桶来形象的表示数组中每个地址位置。注意这里并不是数组本身,数组是装哈希桶的,他可以被称为哈希表。
初始容量(initial capacity) : 这个很容易理解,就是哈希表中哈希桶初始的数量。如果我们没有通过构造方法修改这个容量值默认为DEFAULT_INITIAL_CAPACITY = 1<<4 即16。值得注意的是为了保证 HashMap 添加和查找的高效性,HashMap 的容量总是 2^n 的形式。
加载因子(load factor):加载因子是哈希表(散列表)在其容量自动增加之前被允许获得的最大数量的度量。当哈希表中的条目数量超过负载因子和当前容量的乘积时,散列表就会被重新映射(即重建内部数据结构),重新创建的散列表容量大约是之前散列表哈系统桶数量的两倍。默认加载因子(0.75)在时间和空间成本之间提供了良好的折衷。加载因子过大会导致很容易链表过长,加载因子很小又容易导致频繁的扩容。所以不要轻易试着去改变这个默认值。
扩容阈值(threshold):其实在说加载因子的时候已经提到了扩容阈值了,扩容阈值 = 哈希表容量 * 加载因子。哈希表的键值对总数 = 所有哈希桶中所有链表节点数的加和,扩容阈值比较的是是键值对的个数而不是哈希表的数组中有多少个位置被占了。
树化阀值(TREEIFY_THRESHOLD) :这个参数概念是在 JDK1.8后加入的,它的含义代表一个哈希桶中的节点个数大于该值(默认为8)的时候将会被转为红黑树行存储结构。
非树化阀值(UNTREEIFY_THRESHOLD): 与树化阈值相对应,表示当一个已经转化为数形存储结构的哈希桶中节点数量小于该值(默认为 6)的时候将再次改为单链表的格式存储。导致这种操作的原因可能有删除节点或者扩容。
最小树化容量(MIN_TREEIFY_CAPACITY): 经过上边的介绍我们只知道,当链表的节点数超过8的时候就会转化为树化存储,其实对于转化还有一个要求就是哈希表的数量超过最小树化容量的要求(默认要求是 64),且为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD);在达到该有求之前优先选择扩容。扩容因为因为容量的变化可能会使单链表的长度改变。
红黑树是一种自平衡二叉查找树,可以在 O (log (n)) 时间内完成查找、插入和删除。红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees),它具有以下性质:
1.每个节点要么是红色,要么是黑色。
2.根节点是黑色。
3.每个叶节点(NIL节点,空节点)是黑色。
4.如果一个节点是红色,那么它的两个子节点都是黑色。
5.对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数量的黑色节点。
当红黑树的某个节点的左子树高度大于右子树高度的两倍时,需要进行旋转操作。旋转操作分为左旋和右旋两种情况。左旋操作是将左子树的根节点移到当前节点的右子树,然后对当前节点进行右旋操作;右旋操作是将右子树的根节点移到当前节点的左子树,然后对当前节点进行左旋操作。
插入操作的过程如下:
将新节点插入到红黑树的正确位置上,即左子树或右子树。
如果新节点是红色,它的父节点也是红色,这违反了红黑树的性质4,需要进行颜色调整,即将父节点和兄弟节点都设为黑色,将父节点的父节点设为红色,然后进行左旋或右旋操作。
如果新节点的兄弟节点是红色,这违反了红黑树的性质4,需要进行颜色调整,即将兄弟节点设为黑色,将新节点的父节点设为红色,然后进行左旋或右旋操作。
如果新节点的兄弟节点是黑色,但新节点的父节点是红色,这违反了红黑树的性质5,需要进行旋转操作。如果新节点的兄弟节点的另一个子节点也是黑色,需要进行右旋操作;否则需要进行左旋操作。
删除操作的过程如下:
找到要删除的节点及其前驱节点(pre)和后继节点(succ)。
如果要删除的节点有两个子节点,则将前驱节点的键值替换到要删除的节点,然后删除前驱节点;否则将后继节点的键值替换到要删除的节点,然后删除后继节点。
如果删除的节点是红色,它的兄弟节点也是红色,这违反了红黑树的性质4,需要进行颜色调整,即将要删除的节点和兄弟节点都设为黑色,将兄弟节点的另一个子节点设为红色,然后进行右旋或左旋操作。
如果删除的节点的兄弟节点是黑色,但兄弟节点的另一个子节点是红色,这违反了红黑树的性质5,需要进行旋转操作。如果兄弟节点的另一个子节点是左子节点,则进行右旋操作;否则进行左旋操作。
通过以上插入和删除操作的过程,红黑树可以在保持平衡的同时进行高效的插入和删除操作。
数组+链表+红黑树(JDK1.8之前)
在JDK1.8之前,HashMap的内部实现主要基于数组+链表+红黑树的数据结构。具体来说,HashMap内部有一个Entry类,用于存储键值对。每个Entry对象包含一个指向Node对象的引用,Node对象又包含一个指向下一个节点的引用和一个表示节点颜色的字段(红色或黑色)。当链表的长度超过一定阈值时,链表会转换为红黑树以提高查询效率。
数组+链表+红黑树+跳表(JDK1.8之后)
从JDK1.8开始,HashMap的内部实现采用了数组+链表+红黑树+跳表的数据结构。与之前的版本相比,这个版本的HashMap在处理哈希冲突时使用了链表和红黑树的组合,而不是仅使用链表。此外,为了进一步提高查询效率,还在红黑树的基础上引入了跳表。跳表是一种可以在O(logN)时间内完成查找、插入和删除操作的数据结构,它可以在红黑树的基础上进行优化,提高HashMap的性能。
总结:Java中的HashMap是一个基于数组+链表+红黑树(JDK1.8之前)或数组+链表+红黑树+跳表(JDK1.8之后)的数据结构实现的哈希表。它通过哈希函数将键映射到数组的一个位置,然后在该位置上创建一个链表或红黑树来存储具有相同哈希值的键值对。当链表的长度超过一定阈值时,链表会转换为红黑树以提高查询效率。从JDK1.8开始,HashMap还引入了跳表,以进一步提高查询效率。
table:这是一个Entry数组,存储了所有的key-value对,其中每个Entry对象都包含key、value、链表下一个Entry的引用以及两个整型变量,分别代表hash值和链表长度。用于存储键值对的数组,其长度为2的幂次方。当元素数量超过容量时,会进行扩容操作,将数组长度扩大至原来的两倍,并将原来的元素重新哈希到新的数组中。
链表头节点head:指向数组的第一个元素,即链表的头节点。当链表长度超过一定阈值时,链表会转换为红黑树以提高查询效率。
红黑树root:当链表长度超过一定阈值时,链表会转换为红黑树,该红黑树的根节点即为root。红黑树是一种自平衡二叉查找树,可以在O(log(n))时间内完成查找、插入和删除操作。
size:当前存储在HashMap中的元素个数。记录HashMap中键值对的数量,初始值为0。
threshold:当前HashMap的容量极限,当元素个数达到这个值的时候,HashMap会自动扩容。
loadFactor:负载因子,它表示了底层数组的利用率。负载因子越小,扩容时增加的元素越多,查询效率越高,但会增加内存开销;负载因子越大,扩容时增加的元素越少,查询效率越低,但可以减少内存开销。表示在扩容之前,HashMap允许的最大填充程度。默认值为0.75,即当元素数量达到容量的0.75倍时,会触发扩容操作。
modCount:用来记录对HashMap结构进行了多少次的修改操作(包括put、remove、clear等),每次修改操作都会增加modCount的值。
keySet、entrySet:这两个变量分别存储了HashMap中所有的键和所有的键值对。它们是两个不可变的集合视图,可以用来对外提供Map接口的方法。
哈希函数hashCode()和equals():用于将键映射到数组的索引位置。
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 加载因子
final float loadFactor;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 指向下一个节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash; // 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 重写hashCode()方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写 equals() 方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
其中key值定义的为final,因此在定义之后就无法进行修改,key和value就是在调用map时对应的键值对,next存储的是链表中的下一个节点,他是一个单链表,hash是对key的hashcode再次进行哈希运算之后得到的值,存储起来是为了避免重复计算。
/**
*使用默认的容量及装载因子构造一个空的HashMap
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/**
* 根据给定的初始容量和装载因子创建一个空的HashMap
* 初始容量小于0或装载因子小于等于0将报异常
*/
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;
//这个方法就是把容量控制在2的倍数
this.threshold = tableSizeFor(initialCapacity);
}
/**
*根据指定容量创建一个空的HashMap
*/
public HashMap(int initialCapacity) {
//调用上面的构造方法,容量为指定的容量,装载因子是默认值
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//通过传入的map创建一个HashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HashMap提供了四种构造方法:
(1)使用默认的容量及装载因子构造一个空的HashMap;
(2)根据给定的初始容量和装载因子创建一个空的HashMap;
(3)根据指定容量创建一个空的HashMap;
(4)通过传入的map创建一个HashMap。
第三种构造方法会调用第二种构造方法,而第四种构造方法将会调用putMapEntries方法将元素添加到HashMap中去。
putMapEntries方法是一个final方法,不可以被修改,该方法实现了将另一个Map的所有元素加入表中,参数evict初始化时为false,其他情况为true,我们来看看这个方法吧
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) {
//根据m的元素数量和当前表的加载因子,计算出阈值
float ft = ((float)s / loadFactor) + 1.0F;
//修正阈值的边界 不能超过MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);
//如果新的阈值大于当前阈值
if (t > threshold)
//返回一个>=新的阈值的 满足2的n次方的阈值
threshold = tableSizeFor(t);
}
//如果当前元素表不是空的,但是 m的元素数量大于阈值,说明一定要扩容。
else if (s > threshold)
resize();
//遍历 m 依次将元素加入当前表中。
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);
}
}
}
从中可以看出,它这个涉及了2个操作,一个是计算新的阈值,另一个是扩容方法
如果新的阈值大于当前阈值,需要返回一个>=新的阈值的 满足2的n次方的阈值,这涉及到了tableSizeFor:
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;
}
如果当前元素表不是空的,但是 m的元素数量大于阈值,说明一定要扩容。这涉及到了扩容方法resize。最复杂的方法之一
final Node<K,V>[] resize() {
//oldTab 为当前表的哈希桶
Node<K,V>[] oldTab = table;
//当前哈希桶的容量 length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//当前的阈值
int oldThr = threshold;
//初始化新的容量和阈值为0
int newCap, newThr = 0;
//如果当前容量大于0
if (oldCap > 0) {
//如果当前容量已经到达上限
if (oldCap >= MAXIMUM_CAPACITY) {
//则设置阈值是2的31次方-1
threshold = Integer.MAX_VALUE;
//同时返回当前的哈希桶,不再扩容
return oldTab;
}//否则新的容量为旧的容量的两倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果旧的容量大于等于默认初始容量16
//那么新的阈值也等于旧的阈值的两倍
newThr = oldThr << 1; // double threshold
}
//如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
else if (oldThr > 0)
newCap = oldThr;//那么新表的容量就等于旧的阈值
else {
//如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况
newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16
//新的阈值为默认容量16 * 默认加载因子0.75f = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//如果新的阈值是0,对应的是 当前表是空的,但是有阈值的情况
float ft = (float)newCap * loadFactor;//根据新表容量 和 加载因子 求出新的阈值
//进行越界修复
newThr = (newCap < MAXIMUM_CAPACITY && ft <(float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
//更新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//根据新的容量 构建新的哈希桶
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//更新哈希桶引用
table = newTab;
//如果以前的哈希桶中有元素
//下面开始将当前哈希桶中的所有节点转移到新的哈希桶中
if (oldTab != null) {
//遍历老的哈希桶
for (int j = 0; j < oldCap; ++j) {
//取出当前的节点 e
Node<K,V> e;
//如果当前桶中有元素,则将链表赋值给e
if ((e = oldTab[j]) != null) {
//将原哈希桶置空以便GC
oldTab[j] = null;
//如果当前链表中就一个元素,(没有发生哈希碰撞)
if (e.next == null)
//直接将这个元素放置在新的哈希桶里。
//注意这里取下标 是用 哈希值 与 桶的长度-1 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
newTab[e.hash & (newCap - 1)] = e;
//如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
else {
//因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位,或者扩容后的下标,即high位。high位=low位+原哈希桶容量
//低位链表的头结点、尾节点
Node<K,V> loHead = null, loTail = null;
//高位链表的头节点、尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;//临时节点 存放e的下一个节点
do {
next = e.next;
//利用位运算代替常规运算:利用哈希值与旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位
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);
//将低位链表存放在原index处
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将高位链表存放在新index处
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize的操作主要涉及以下几步操作:
Java中的HashMap使用了一种优化后的hash算法,以降低冲突的概率。该算法的核心思想是使用一个扰动函数和一个非线性组合方案,使得不同的key可以产生出不同的hash值,从而降低冲突的可能性。
具体来说,HashMap的hash算法步骤如下:
1、对key的hashCode进行高16位与低16位异或运算,得到一个扰动值。
2、将扰动值与HashMap的capacity-1进行按位异或运算,得到一个桶的索引。
3、将桶中的链表长度设置为桶内最大长度(即HashMap的loadFactor倍),如果链表长度已经达到最大长度,则进行扩容操作。
4、通过这种优化后的hash算法,HashMap可以更好地分布键值对,降低冲突的概率,从而提高查询效率。
TimSort排序数组的哈希算法首先将键值对转换为字符串,然后将字符串转换为字符数组。接着,对于每个字符数组,计算其哈希值,并将其映射到哈希表的相应位置上。如果发生哈希冲突,则使用链表或红黑树来存储具有相同哈希值的键值对。
//向哈希表中添加元素
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//tab存放当前的哈希桶,p用作临时链表节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前哈希表是空的,代表是初始化
if ((tab = table) == null || (n = tab.length) == 0)
//那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n
n = (tab = resize()).length;
//如果当前index的节点是空的,表示没有发生哈希碰撞。直接构建一个新节点Node,挂载在index处即可。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//否则 发生了哈希冲突。
Node<K,V> e; K k;
//如果哈希值相等,key也相等,则是覆盖value操作
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;//将当前节点引用赋值给e
else if (p instance of TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//不是覆盖操作,则插入一个普通链表节点
//遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//遍历到尾部,追加新节点到尾部
p.next = newNode(hash, key, value, null);
//如果追加节点后,链表数量>=8,则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到了要覆盖的节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不是null,说明有需要覆盖的节点,
if (e != null) { // existing mapping for key
//则覆盖节点值,并返回原oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeAccess(e);
return oldValue;
}
}
//如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。
++modCount;
//更新size,并判断是否需要扩容。
if (++size > threshold)
resize();
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeInsertion(evict);
return null;
}
HashMap的put操作原理如下:
1、根据key的hashCode计算出hash值。
2、使用hash寻址算法计算出存储位置的索引。
3、将key-value对存储在存储位置的桶中。
4、如果桶中已经存在元素,则覆盖原来的元素。
详细步骤
key.hashCode()是Key自带的hashCode()方法,返回一个int类型的散列值。我们大家知道,32位带符号的int表值范围从-2147483648到2147483648。这样只要hash函数松散的话,一般是很难发生碰撞的,因为HashMap的初始容量只有16。但是这样的散列值我们是不能直接拿来用的。用之前需要对数组的长度取模运算。得到余数才是索引值。
hash寻址算法的核心思想是通过key的hashCode对HashMap的capacity-1进行按位异或运算,得到一个桶的索引。这样可以使得不同的key能够均匀地分布在桶中,降低冲突的概率。如果两个key的hashCode相同,即hash碰撞,HashMap会使用链表或红黑树来处理冲突。当桶中的元素数量大于等于一定的阈值(即loadFactor倍)时,会将链表转换为红黑树,这样可以进一步提高查询效率。
public V get(Object key) {
Node<K,V> e;
//传入扰动后的哈希值 和 key 找到目标节点Node
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
HashMap向用户分开放的get方法是调用的getNode方法来实现的
//传入扰动后的哈希值 和 key 找到目标节点Node
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//查找过程,找到返回节点,否则返回null
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((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;
}
简单讲讲查询过程,还是比较简单的
HashMap没有提供判断元素是否存在的方法,只提供了判断Key是否存在及Value是否存在的方法,分别是containsKey(Object key)、containsValue(Object value)。
containsKey(Object key)方法很简单,只是判断getNode (key)的结果是否为null,是则返回false,否返回true。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
//遍历哈希桶上的每一个链表
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
//如果找到value一致的返回true
if ((v = e.value) == value || (value != null && value.equals(v)))
return true;
}
}
}
return false;
}
判断一个value是否存在比判断key是否存在还要简单,就是遍历所有元素判断是否有相等的值。这里分为两种情况处理,value为null何不为null的情况,但内容差不多,只是判断相等的方式不同。这个判断是否存在必须遍历所有元素,是一个双重循环的过程,因此是比较耗时的操作。
HashMap中“删除”相关的操作,有remove(Object key)和clear()两个方法。
其中向用户开放的remove方法调用的是removeNode方法,,removeNode (key)的返回结果应该是被移除的元素,如果不存在这个元素则返回为null。remove方法根据removeEntryKey返回的结果e是否为null返回null或e.value。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
// p 是待删除节点的前置节点
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果哈希表不为空,则根据hash值算出的index下 有节点的话。
if ((tab = table) != null && (n = tab.length) > 0&&(p = tab[index = (n - 1) & hash]) != null) {
//node是待删除节点
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;//将待删除节点引用赋给node
else if ((e = p.next) != null) {//否则循环遍历 找到待删除节点,赋值给node
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);
}
}
//如果有待删除节点node, 且 matchValue为false,或者值也相等
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 == p,说明是链表头是待删除节点
tab[index] = node.next;
else//否则待删除节点在表中间
p.next = node.next;
++modCount;//修改modCount
--size;//修改size
afterNodeRemoval(node);//LinkedHashMap回调函数
return node;
}
}
return null;
}
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
删除HashMap中所有的元素,这里就不用一个个删除节点了,而是直接将table数组内容都置空,这样所有的链表都已经无法访问,Java的垃圾回收机制会去处理这些链表。table数组置空后修改size为0。
Java中的HashMap在处理hash冲突时会使用链表。当两个或多个key的hashCode相同,即hash碰撞时,它们会被存储在一个链表中。这个链表是存储在对应的桶中的,每个链表节点都包含一个key-value对。
当进行get操作时,根据key的hashCode计算出存储位置的索引,然后在对应的链表中查找该key。如果找到了,就返回对应的value;如果没找到,就返回null。
当进行put操作时,如果对应的桶中没有元素,就直接将key-value对存储在对应的链表头部;如果对应的桶中已经有元素,就先遍历链表,找到要插入的位置并插入。
如果链表长度大于一定的阈值(即HashMap的loadFactor倍),HashMap会将链表转换为红黑树。这样可以进一步提高查询效率。
Java中的HashMap通过红黑树来解决hash冲突。当HashMap的负载因子大于等于阈值(loadFactor)时,会将链表转换为红黑树。
红黑树是一种自平衡的二叉查找树,它满足以下五个性质:
1、每个节点要么是红色,要么是黑色。
2、根节点是黑色。
3、每个叶节点(NIL节点,空节点)是黑色。
4、如果一个节点是红色,那么它的两个子节点都是黑色。
5、对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数量的黑色节点。
当发生hash冲突时,HashMap会将冲突的元素存储在链表中。当链表长度超过一定阈值时,会将链表转换为红黑树。这个过程涉及到一些细节,包括节点的颜色调整和树的旋转操作。
总之,Java中的HashMap通过红黑树来解决hash冲突,以提高查询效率。
Java中的HashMap是一种常用的数据结构,它基于数组实现。当HashMap的元素数量达到一定的阈值时,需要进行扩容操作。
HashMap的扩容原理如下:
创建一个新的Entry数组,其容量是原数组容量的两倍加1。
遍历原数组的每个元素,重新计算每个元素的存储位置,并将元素插入到新数组中。
将原数组引用新的Entry数组,完成扩容操作。
在扩容过程中,HashMap仍然支持插入和查询操作。为了保证操作的正确性,需要使用一些额外的技巧,例如使用一个“dummy”元素作为新数组的头部,以及使用一个“shadow”指针来记录原数组的头部元素在新数组中的位置。
总之,Java中的HashMap通过基于数组的扩容原理来实现容量的扩展,以适应更多的元素存储需求。
Java中的HashMap在JDK1.8中引入了一种高性能的rehash算法,以提高其性能和扩容效率。
该算法的核心思想是利用原数组的容量,重新计算每个元素的存储位置,并将元素插入到新的Entry数组中。与传统的扩容方式不同,该算法在扩容时不再需要遍历整个原数组,而是通过一些优化技巧,减少了扩容所需的时间和空间开销。
具体来说,该算法的优化技巧包括:
使用“dummy”元素作为新数组的头部,避免了在新数组头部插入元素时进行复杂的扩容操作。
使用一个“shadow”指针来记录原数组的头部元素在新数组中的位置,避免了遍历整个原数组寻找头部元素。
将扩容操作与查询操作合并,即在重新计算元素的存储位置时,同时检查是否需要将元素移动到新的Entry数组中。
使用二次哈希函数来计算元素的存储位置,以减少哈希冲突的概率。
Java中的HashMap是一种基于哈希表的数据结构,它实现了Map接口。HashMap中的元素是通过键值对的形式存储的,其中键是唯一的,而值可以重复。
与有序的Map数据结构(如TreeMap)不同,HashMap不保证元素的顺序。它根据键的hashCode值存储元素,因此元素在HashMap中的位置与键的顺序无关。
如果需要使用有序的Map数据结构,可以考虑使用TreeMap或者LinkedHashMap。TreeMap根据键的自然顺序或者自定义的比较器来排序元素;而LinkedHashMap则根据插入顺序或访问顺序来维护元素的顺序。