Java 集合学习笔记:HashMap
Posted 笑虾
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合学习笔记:HashMap相关的知识,希望对你有一定的参考价值。
Java 集合学习笔记:HashMap
- UML
- 简介
- 阅读源码
- 总结
- 参考资料
UML
简介
基于
Hash table
的Map
接口实现。实现了Map
定义的所有方法,并允许key
和value
为null
。(除了非线程安全和允许 null 之外,HashMap
与Hashtable
大致相同)这个类不保证map
的顺序;尤其是,它不能保证顺序随时间的推移保持不变。
这个实现为基本操作(get
和put
)提供了恒定时间的性能,假设hash
函数将元素适当地分散到桶中。在集合视图上迭代所需的时间与HashMap
实例的容量(桶的数量)加上它的大小(键-值映射的数量)成正比。因此,如果迭代性能很重要,就不要将初始容量设置得太高(或负载因子设置得太低)。
HashMap
的实例有两个参数影响其性能:初始容量
和负载因子
。容量
是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。负载因子
是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了负载因子与当前容量的乘积时,则要对该哈希表进行rehash
操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认负载因子 (0.75
) 在时间和空间成本上寻求一种折衷。负载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数HashMap
类的操作中,包括get
和put
操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其负载因子,以便最大限度地减少rehash
操作次数。如果初始容量大于最大条目数除以负载因子,则不会发生rehash
操作。
如果很多映射关系要存储在HashMap
实例中,则相对于按需执行自动的rehash
操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(...));
由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出ConcurrentModificationException
。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
此类是 Java Collections Framework 的成员。
阅读源码
属性字段
1. 静态属性
属性 | 默认值 | 说明 |
---|---|---|
DEFAULT_INITIAL_CAPACITY | 1<<4 ==16 | 默认的初始容量 16 。必须是2 的幂。 |
MAXIMUM_CAPACITY | 1<<30 ==230 | table 的最大容量 ,必须小于等于该值。且必须是2 的幂。230= 01000000 00000000 00000000 00000000 |
DEFAULT_LOAD_FACTOR | 0.75f | 默认负载因子 |
TREEIFY_THRESHOLD | 8 | 树化阈值。当向哈希桶 中添加元素时,如果 结点数 >= TREEIFY_THRESHOLD - 1 则将链表 转换为树 。取值范围(2, 8] 。(还需要满足前提条件 MIN_TREEIFY_CAPACITY) |
UNTREEIFY_THRESHOLD | 6 | 取消树化阈值。在split 时如果发现桶中的结点数 <= 此阈值,则将红黑树 转为链表 。取值应小于 TREEIFY_THRESHOLD ,且最多为6 。 |
MIN_TREEIFY_CAPACITY | 64 | 触发树化的前提条件。 除了 哈希桶 中链表长度达到阈值 外还需要 哈希表.容量 >= 64 ,才能触发树化。(否则,如果一个 bin 中有太多的结点,则会调整表的大小。)应该至少是4 * TREEIFY_THRESHOLD ,以避免调整大小和树化阈值之间的冲突。 |
2. 成员属性
- 默认都是:null
- transient :被此关键字修饰的字段
不会序列化
属性 | 说明 |
---|---|
transient Node<K,V>[] table | 哈希表 本质是一个数组。HashMap 是由 数组+链表or红黑树 实现的,这就是那个数组 。它的每个索引位置是一个哈希桶 ,桶中存放的是链表的头结点 或树的根结点 。在刚 new 出来的新 HashMap 对象中table = null 。在第一次使用时通过 resize() 初始化,并根据需要调整 哈希表 大小。分配的长度必定是2 的幂。(We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed. 这句只知道他说允许长度为0,但他所指的“当前不需要的引导机制”不知道是指的啥。不会翻555) |
transient Set<Map.Entry> entrySet | 缓存entrySet() 的结果。作为一个当前 HashMap 所有键值对的视图。 |
transient int size | 此 Map 中实际包含的元素(键-值 )数量。 |
transient int modCount | 结构修改次数。结构修改是指改变 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新哈希)的操作。该字段用于 HashMap 迭代器的并发冲突检测。(见ConcurrentModificationException)。 |
int threshold | 扩容阈值。键值对(entry)的个数大于阈值时,会触发扩容。( threshold = 容量 * 负载因子 )。 |
final float loadFactor | 哈希表的负载因子。 |
——————————————————— |
HashMap 结构
静态工具方法
访问修饰符&返回类型 | 方法 | 描述 |
---|---|---|
static final int | hash(Object key) | 计算 key 的 hash 值。为了尽量避免碰撞,使用 异或 和 位移 。是出于性能考虑。 |
static Class<?> | comparableClassFor(Object x) | 如果x 的形式是Class C implements Comparable<C> 返回 C.class 。否则返回 null 。 |
static int | compareComparables(Class<?> kc, Object k, Object x) | 如果 x的类型 是 kc 就返回 k.compareTo(x) 的结果,否则返回 0 。 |
static final int | tableSizeFor(int cap) | 返回大于 cap 的最近的一个 2 的倍数。 |
——————— | —————————————— |
hash
计算 key
的 hash
值。为了尽量避免碰撞,使用 异或
和 位移
。是出于性能考虑。
- test
@Test
public void hashCodeTest()
int h = 0b11111111111111110000000000000000; // 0b开头表示二进制数
int i = h >>> 16; // 无符号右移16位(包括符号位一起移)
log.info("", Integer.toBinaryString( i )); // 00000000000000001111111111111111 原本高位的16个1都移到了左边,左边空出的位置补0
int hash = h ^ i; // 异或运算
log.info("", Integer.toBinaryString( hash )); // 11111111111111111111111111111111 i高16位没东西,直接照搬 h,低16位,不同为1,相同为 0
hash(Object key)
- 如果 key 为 null 返回 0
- 否则调用对象的
hashCode()
获取 hash值。 ^
+>>>
,把 hash 值搅拌一下,尽可能的减少不同key
出现hash
相同的情况。
static final int hash(Object key)
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- public native int hashCode(); Object 的原生方法:返回此对象的 hash 值。
【算法学习】 ^ 加 >>> 减少碰撞
comparableClassFor(Object x)
如果x
的形式是Class C implements Comparable<C>
返回其类的类型。否则为空。
- test
例如:当x
的类型 C 和比较器的参数类型Comparable<
C>
一样时就返回 C)
// 形式为 Class C implements Comparable<C>
Class C implements Comparable<C>;
C c = new C;
Class<?> clazz = comparableClassFor(c);
System.out.println(clazz.getName()); // C
// Class C implements Comparable<如果这里不是C> 返回 null
comparableClassFor(Object x)
- 检测:给定参数必需实现了
Comparable
否则直接返回null
- 如果给定参数是String,直接返回
String.class
,因为我们知道String实现了Comparable<String>
- 否则:遍历对象实现的所有接口,逐个判断:
3.1. 如果实现了Comparable
并且泛型参数不为空,则返回此参数类型。
static Class<?> comparableClassFor(Object x)
// 如果 x 是 Comparable 的实例。如果是继续,否则返回 null;
if (x instanceof Comparable)
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
// 如果 x 是个 String 直接返回类型。
if ((c = x.getClass()) == String.class) // bypass checks
return c;
// 获取 c 所实现的接口们(可能有多个所以放数组里)。如果不为 null 继续,否则返回 null;
if ((ts = c.getGenericInterfaces()) != null)
// 逐个遍历接口类型
for (int i = 0; i < ts.length; ++i)
if (
// 1. 如果此接口 t 是某种 ParameterizedType(参数化类型,如 Collection<String>)
((t = ts[i]) instanceof ParameterizedType)
// 2. 并且,接口 t 的类型正好是 Comparable(为了调用 getRawType() 获取类型,做了强转)
&& ((p = (ParameterizedType)t).getRawType() == Comparable.class)
// 3. 并且,获取 p 的参数类型数组。不为 null。(Comparable<T>就是这里替换 T 的实参)
&& (as = p.getActualTypeArguments()) != null
// 4. 并且,只有 1 个
&& as.length == 1
// 5. 并且,Comparable<参数> 中的 ‘参数’ == 给定的 x 的类型。
&& as[0] == c
)
return c;
return null;
compareComparables(Class<?> kc, Object k, Object x)
如果 x的类型
是 kc
就返回 k.compareTo(x)
的结果,否则返回 0
。
此方法是要配合上面的 comparableClassFor(Object x) 一起用的。
- test
@Test
public void compareComparablesTest()
String k = "jerry1";
String x = "jerry2";
Class<?> kc = comparableClassFor(k);
int i = compareComparables(kc, k, x);
log.info("", i); // -1
compareComparables(Class<?> kc, Object k, Object x)
k:就是 key
,比如类型是我们最见的“字符串”。String
就实现了 Comparable<String>
。
kc : 通过 comparableClassFor(k)
获取 k
实现的 Comparable<T>
中的实参。在 HashMap 的源码 find
、treeify
、putTreeVal
这些方法中能看到它的身影。kc
都有先判断 非null
然后才使用。
以下情况中假设 k、x 的类型都是 String
x
为null
直接返回0
(表示比个毛)kc
是从k
上获取的比较器Comparable<String>
的参数的类型String.class
。
如果k
没有实现Comparable<String>
则kc
为null
,否则kc
为String.class
x.getClass() != kc
表达的意思是:如果k
没有实现Comparable<String>
比较器,就没法比,直接返回0
换句话说只有k
实现了Comparable<X>
才会执行到((Comparable)k).compareTo(x)
这里。
@SuppressWarnings("rawtypes","unchecked") // 压制强转警告
static int compareComparables(Class<?> kc, Object k, Object x)
return (x == null
|| x.getClass() != kc ? 0 : ((Comparable)k).compareTo(x));
tableSizeFor(int cap)
返回大于 cap
的最近的一个 2
的倍数。
- test
以 cap = 50 为例:
@Test
public void tableSizeForTest()
int cap = 50;
int n = cap - 1; // n: 49。
int MAXIMUM_CAPACITY = 1 << 30; // 1_073_741_824
//int x,y;
//log.info("原值=; = | ; Binary: = | ", x=y=n, x |= x >>> 1, y, y>>>1,Integer.toBinaryString(x), Integer.toBinaryString(y), toBinary(y>>>1, 6));
n |= n >>> 1; // 原值=49; 57 = 49 | 24; Binary: 111001 = 110001 | 011000
n |= n >>> 2; // 原值=57; 61 = 57 | 28; Binary: 111101 = 111001 | 011100
n |= n >>> 4; // 原值=63; 63 = 63 | 31; Binary: 111111 = 111111 | 011111
n |= n >>> 8; // 原值=63; 63 = 63 | 31; Binary: 111111 = 111111 | 011111
n |= n >>> 16; // 原值=63; 63 = 63 | 31; Binary: 111111 = 111111 | 011111
n = (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
System.out.println(n); // 64
cap:目标容量传进来前已确保 >= 0
static final int tableSizeFor(int cap) // cap = 50
// 因为末尾需要 + 1 达到 0111 + 1 = 1000 效果。所以此处配合先 -1
// 最终效果:确保当 cap 正好是 2的n次方时,最终结果是 cap 本身。
int n = cap - 1;
// 这一通 >>> 与 | 配合下来,使得最高位的 1 不变,右边所有的 0 都变成 1
// ( 也就是找到最高位并将其右侧所有位都设置成 1 )
// 如: 1000 0000 变成 1111 1111;
// 0010 1010 变成 0011 1111;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 小于 0 直接返回 1 ( 2 的 0 次方 )
// 如果大于最大值直接返回最大值,否则当前值 +1 返回。
// +1 能保存是 2的二次幂。因为最高位后所有都是 1 时,再+1,肯定是一个 2 的幂。
// 例:0000 1111 + 1 = 0001 0000 = 16
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
【算法学习】32 位全置 1
10000000 00000000 00000000 00000000
01000000 00000000 00000000 00000000 // >>> 1
————————————————————————————————————// 或
11000000 00000000 00000000 00000000
00110000 00000000 00000000 00000000 // >>> 2
————————————————————————————————————// 或
11110000 00000000 00000000 00000000
00001111 00000000 00000000 00000000 // >>> 4
————————————————————————————————————// 或
11111111 00000000 00000000 00000000
00000000 11111111 00000000 00000000 // >>> 8
————————————————————————————————————// 或
11111111 11111111 00000000 00000000
00000000 00000000 11111111 11111111 // >>> 16
————————————————————————————————————// 或
11111111 11111111 11111111 11111111
构造方法
方法 | 描述 |
---|---|
HashMap(int initialCapacity, float loadFactor) | 指定初始容量 和负载因子 ,构造一个空的 HashMap 。 |
HashMap(int initialCapacity) | 指定初始容量 ,构造一个空的 HashMap 。负载因子 默认 0.75 。 |
HashMap() | 构造一个空的 HashMap【容量默认16 】,【负载因子默认0.75】 |
HashMap(Map<? extends K, ? extends V> m) | 用指定的 Map,构造一个新的 HashMap。【负载因子默认 0.75】,容量足够存放给定的 Map。 |
HashMap(int initialCapacity, float loadFactor)
指定初始容量
和负载因子
,构造一个空的 HashMap。
/**
* @param initialCapacity 初始容量
* @param loadFactor 负载因子
* @throws 如果【初始容量 < 0】,或【负载因子 <= 0】,抛 IllegalArgumentException
*/
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;
// 返回大于 initialCapacity 的最小 2 次幂数。
this.threshold = tableSizeFor(initialCapacity);
HashMap(int initialCapacity)
指定 初始容量
,构造一个空的 HashMap。负载因子
默认 0.75
。
/**
* @param initialCapacity 初始容量
* @throws 如果【初始容量 < 0】抛 IllegalArgumentException
*/
public HashMap(int initialCapacity)
this(initialCapacity, DEFAULT_LOAD_FACTOR); // DEFAULT_LOAD_FACTOR = 0.75f;
HashMap()
构造一个空的 HashMap 容量默认16
、负载因子默认0.75
public HashMap()
this.loadFactor = DEFAULT_LOAD_FACTOR; // DEFAULT_LOAD_FACTOR = 0.75f; 其它都是默认值。
HashMap(Map<? extends K, ? extends V> m)
用指定的 Map,构造一个新的 HashMap。负载因子
使用默认的0.75
,容量
等于给定的 Map 大小
。
/**
* @param m 用来创建 HashMap 的给定 map
* @throws 如果给定 map 为空,抛 NullPointerException
*/
public HashMap(Map<? extends K, ? extends V> m)
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
非 public 方法
putMapEntries 填充当前对象
用给定的 map 填充当前 HashMap。主要是实现 Map.putAll
和 Map 构造
的底层逻辑,供它们调用 .
- putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
- 如果给定的 map 不为空,继续处理,否则结束。
- 如果 哈希表表为 null 则先初始化,否则直接检测并按需扩容。
- 然后遍历给定的 map 填充当前 HashMap
/**
* @param m 用来创建 HashMap 的 map
* @param evict 在最初构造这个 Map 时为 false,否则为true(最终会传给 afternodeinsert 方法 )。
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
// 取出给定 map 的大小,好用来创建新的 HashMap
int s = m.size();
// 没有内容直接跳过
if (s > 0)
// 为 null 表示尚未初始化
if (table == null) // pre-size
// 计算所需的初始容量(向上取整尽量避免频繁扩容)
float ft = ((float)s / loadFactor) + 1.0F;
// 如果没超最大值,直接使用。否则用最大值。
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
// 如果大于扩容阈值,则计算出新的扩容阈值(大于 t 的最小 2 次幂)。
if (t > threshold) // 初始化时 threshold = 0 必触发
threshold = tableSizeFor(t);
// 给定的 map 比当前 HashMap 大。进行扩容。
else if (s > threshold)
resize();
// 遍历给定的 m 填充当前 HashMap
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
K key = e.getKey();
V value = e.getValue();
// 被构造函数调用时 evict = false
putVal(hash(key), key, value, false, evict);
【算法学习】 尽量避免新 Map 很快触发扩容
分析1:
((float)s / loadFactor) + 1.0F
通过+1.0F
努力避免刚出生没扑腾 put
几下就触发扩容的尴尬。
1. (int)( float + 1 )
的含义是向上取整。常用于小数部分不应舍弃的场景,比如1.5瓶饮料,需要2个瓶子装。
2. 以 s = 12
为例:如不 +1.0F
那么 threshold = (12 / 0.75) = 16
离下一次扩容近在咫尺。构造出来的新 map
几乎一抬脚就要触发一次扩容。
但如果有了 +1.0F
那么 (12 / 0.75) + 1 = 17
经过 tableSizeFor(17)
结果是 32
。新 map
可以开心的扑腾put
不用尴尬了。
分析2:
在网上搜了一下,看到有一种说法,如果不+1
,舍弃小数,会导致放不下(触发扩容浪费)。但是 size + 1 > size / 0.75
不可能成立,size > size / 0.75
更不可能。
更何况有tableSizeFor(t)
的存在,size
要正好是 2的n次方
,然后再满足上面的条件。
无论如何,实际PUT
的个数永远不可能放不下啊????这个疑惑困扰了我几天5555
分析3:
看了 anlian523-JDK8 HashMap源码 putMapEntries解析 发现:我把程序员们想得太好了,我认为他们会遵守基本的编码规范(如果你不知道为何 loadFactor 是 0.75 请别动它),但是如果有人不用 0.75
非要传一个奇怪的小数,就可能满足 16 + 1 > 16 / 0.95 == true
,所以我是不是可以理解为,这个 +1
不是用来防止容量不够,而是用来防刁民的
getNode 获取结点
按给定的 以上是关于Java 集合学习笔记:HashMap的主要内容,如果未能解决你的问题,请参考以下文章hash