HashMap源码随笔

源码第一块:
概述:
Map 接口的基于哈希表的实现。此实现提供所有可选的映射操作,并允许 null 值和 null 键。(HashMap 类大致等同于 Hashtable,只不过它是不同步的,并且允许 null。此类不保证地图的顺序;特别是,它不保证订单会随着时间的推移保持不变。此实现为基本操作(get 和 put)提供恒定时间性能,假设哈希函数将元素正确地分散在存储桶中。对集合视图进行迭代所需的时间与 HashMap 实例的“容量”(存储桶数量)及其大小(键值映射数量)成正比。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载系数太低),这一点非常重要。HashMap 实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中的存储桶数量,初始容量只是创建哈希表时的容量。负载因子是衡量哈希表在容量自动增加之前允许的满量。当哈希表中的条目数超过负载因子和当前容量的乘积时,将重新哈希表(即重新构建内部数据结构),以便哈希表的桶数大约是其两倍。作为一般规则,默认负载系数 (0.75) 在时间和空间成本之间提供了良好的权衡。较高的值会减少空间开销,但会增加查找成本(反映在 HashMap 类的大多数操作中,包括 get 和 put)。在设置映射的初始容量时,应考虑映射中的预期条目数及其负载系数,以最大程度地减少重新散列操作的次数。如果初始容量大于最大条目数除以负载因子,则不会发生重新散列操作。如果要在 HashMap 实例中存储许多映射,则创建具有足够大容量的映射将允许更有效地存储映射,而不是让它根据需要执行自动重新散列以增加表。请注意,使用具有相同 hashCode() 的多个键肯定会降低任何哈希表的性能。为了改善影响,当键具有可比性时,此类可以使用键之间的比较顺序来帮助打破联系。请注意,此实现不是同步的。如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部同步该映射。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。这通常是通过在自然封装地图的某个对象上进行同步来实现的。如果不存在此类对象,则应使用 Collections.synchronizedMap 方法“包装”映射。这最好在创建时完成,以防止意外的不同步访问地图: Map m = Collections.synchronizedMap(new HashMap(...));此类的所有“集合视图方法”返回的迭代器都是快速失败的:如果在创建迭代器后的任何时间对映射进行结构修改,则迭代器将抛出 ConcurrentModificationException。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是冒着在未来不确定的时间出现任意的、非确定性行为的风险。请注意,无法保证迭代器的快速失效行为,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬性保证。快速失败迭代器会尽力而为地抛出 ConcurrentModificationException。因此,编写一个依赖于此异常的程序的正确性是错误的:迭代器的快速失败行为应该只用于检测错误。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {}’

此映射通常充当 bined(桶)哈希表,但是当 bin 变得太大时,它们会转换为 TreeNodes 的 bin,每个 bin 的结构与 java.util.TreeMap 中的结构类似。大多数方法尝试使用普通的 bin,但在适用时中继到 TreeNode 方法(只需检查节点的实例)。TreeNodes 的 bin 可以像任何其他 bin 一样被遍历和使用,但在过度填充时还支持更快的查找。但是,由于正常使用的绝大多数箱不会过度填充,因此在表方法的过程中可能会延迟检查树箱是否存在。树 bin(即元素都是 TreeNodes 的 bin)主要由 hashCode 排序,但在平局的情况下,如果两个元素属于相同的“类 C 实现 Comparable”,则使用它们的 compareTo 方法进行排序。(我们保守地通过反射检查泛型类型来验证这一点——参见 comparableClassFor 方法)。当键具有不同的哈希值或可排序时,树箱的增加复杂性在提供最坏情况下的 O(log n) 操作是值得的,因此,在意外或恶意使用下,性能会优雅地下降,其中 hashCode() 方法返回分布不良的值,以及许多键共享 hashCode 的值,只要它们也具有可比性。(如果这些都不适用,与不采取预防措施相比,我们可能会在时间和空间上浪费大约两倍。但唯一已知的情况源于糟糕的用户编程实践,这些实践已经非常缓慢,以至于这几乎没有区别。由于 TreeNodes 的大小大约是常规节点的两倍,因此我们仅在 bin 包含足够多的节点来保证使用时才使用它们(参见 TREEIFY_THRESHOLD)。当它们变得太小时(由于移除或调整大小),它们会被转换回普通垃圾箱。在具有良好分布的用户 hashCode 的用法中,很少使用树 bin。理想情况下,在随机哈希码下,bin 中节点的频率服从泊松分布 (http:en.wikipedia.orgwikiPoisson_distribution),对于默认调整大小阈值 0.75,参数平均约为 0.5,尽管由于调整大小粒度而存在很大差异。忽略方差,列表大小 k 的预期出现次数为 (exp(-0.5) pow(0.5, k) factorial(k))。树箱的根通常是它的第一个节点。但是,有时(目前仅在 Iterator.remove 上),根目录可能在其他地方,但可以通过父链接(方法 TreeNode.root())恢复)。所有适用的内部方法都接受哈希码作为参数(通常由公共方法提供),允许它们在不重新计算用户哈希码的情况下相互调用。大多数内部方法还接受“tab”参数,该参数通常是当前表,但在调整大小或转换时可能是新表或旧表。当 bin 列表被树化、拆分或未树化时,我们将它们保持在相同的相对访问遍历顺序(即字段 Node.next)中,以更好地保留局部性,并略微简化对调用 iterator.remove 的拆分和遍历的处理。在插入时使用比较器时,为了在重新平衡之间保持总排序(或此处要求的接近),我们将类和 identityHashCodes 作为决胜局进行比较。由于子类 LinkedHashMap 的存在,普通模式与树模式之间的使用和转换变得复杂。请参阅下面的钩子方法,这些方法定义为在插入、删除和访问时调用,这些方法允许 LinkedHashMap 内部以其他方式保持独立于这些机制。(这还要求将映射实例传递给一些可能创建新节点的实用工具方法。类似并发编程的基于 SSA 的编码风格有助于避免在所有曲折的指针操作中出现锯齿错误。

`/**
* The default initial capacity - MUST be a power of two.
* 默认容量大小为16,其必须为2的整数次幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16`
`/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*最大容量,如果使用参数的任一构造函数隐式指定了较高的值,则使用该容量。必须是 2 的幂<= 1<<30。
* 规定了HashMap的最大容量大小为2的29次方,如果手动输入了其他值,则采用改值,但是该值必须为2的幂``
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 负载因子,没什么好说的,网络上的文章已经写的很明白了
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 树化的阈值为8,当HashMap的容量大于64并且在一个bin上发生冲突,链的长度为8的时候,进行树化
* 该值必须大于 2,并且应至少为 8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
* 解除树化的阈值为6,与8之间空着一个7,防止单个元素增减的时候频繁进行树化与解除
* 该值必须小于树化阈值,并且最多为6
*
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
* 树化前置容量条件,当HashMap中的容量小于这个值的时候,达到了树化阈值,那么将会进行一次检定,如果容量
* 大于这个值,则进行树化,否则就进行扩容来减少Hash冲突
* 该值至少应该是4倍的树化阈值
*/
static final int MIN_TREEIFY_CAPACITY = 64;`

接下来是一个内部类Node,是HashMap进行元素存储的基本单位之一,这里就不过多赘述。

`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;
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; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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.hashCode() 并将 (XOR) 较高的哈希位传播到更低的位。由于该表使用 2 的幂掩码,因此仅以高于当前掩码的位数变化的哈希集将始终发生冲突。
* (在已知的例子中,有一组浮点键,它们在小表中保存连续的整数。因此,我们应用了一个转换
* ,将较高位的影响向下分散。在速度、实用性和比特扩展质量之间需要权衡。由于许多常
* 见的哈希集已经合理分布(因此不会从扩展中受益),并且由于我们使用树来处理 bin 中的大量冲突,因此我们只是以最便宜的方式对一些移位进行异或,
* 以减少系统损失,并合并最高位的影响,否则由于表边界,这些位永远不会用于索引计算。
* 这里计算hash是对key的hashCode进行高16位与低16位的异或,从而使得hash值同时具有高16位与低16位的特征,从而减少Hash冲突
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* Returns x's Class if it is of the form "class C implements
* Comparable<C>", else null.
* 返回x的Class对象,当且仅当 X 是 X implement Comparable<X>,否则返回nul
* 详细可以查看这篇博客 https://blog.csdn.net/qpzkobe/article/details/79533237
*/
static Class<?> comparableClassFor(Object x) {
// 首先 x 必须是 Comparable的实现类
if (x instanceof Comparable) {
// 初始化 Class对象,type对象,参数化类型对象
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
//返回实现接口信息的type数组 包含泛型信息
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
//遍历数组,筛选出其中的参数化类型
if (((t = ts[i]) instanceof ParameterizedType) &&
// 获取其中去掉参数化类型的对象等于Comparable
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
// 以参数化类型返回其中的参数化列表,与getRawType刚好相反
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
/**
* Returns k.compareTo(x) if x matches kc (k's screened comparable
* class), else 0.
* 如果 x 与 kc 匹配(k 筛选的可比类),则返回 k.compareTo(x),否则为 0。
*/
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
/**
* Returns a power of two size for the given target capacity.
* 返回大于 入参cap最小的2的整数次幂
*
*/
static final int tableSizeFor(int cap) {
// 首先就是让cap减去1,否则如果cap本身就是2的整数次幂将得到一个错误的值
int n = cap - 1;
//原理: int具有32bit,
// 假设现在最高位的1是这样的:00100...将其与自身的无符号右移1位后的数字进行或运算,得到00110...
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 将其进行无符号右边移动1+2+4+8+16,会发生什么? 会发现其总共右边移动了31位,而且是无符号右移动
// 也就意味着在符号位不参与运算的情况下,该算法使得‘最高位的1右边的0全变成了1’
// 最后返回值再进行+1,就完成了这一次最近size的运算
// 当然返回值是有最大容量校验的
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
posted @   不要在云上云云宝  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
点击右上角即可分享
微信分享提示