Java 集合学习笔记:HashMap
Posted 笑虾
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合学习笔记:HashMap相关的知识,希望对你有一定的参考价值。
Java 集合学习笔记:HashMap
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 | 16 | 默认的初始容量-必须是2 的幂。 |
MAXIMUM_CAPACITY | 230 | 最大容量,必须小于等于该值。且必须是2 的幂。 |
DEFAULT_LOAD_FACTOR | 0.75f | 默认加载因子 |
TREEIFY_THRESHOLD | 8 | 树化阈值。当向bin (桶)中添加元素时,如果 binCount >= TREEIFY_THRESHOLD - 1 则,bin 将(由列表)转换为树 。取值范围(2, 8] 。 |
UNTREEIFY_THRESHOLD | 6 | 在调整大小操作期间取消树化的阈值。应小于 TREEIFY_THRESHOLD ,且最多为6 。 |
MIN_TREEIFY_CAPACITY | 64 | 可以对容器进行树化的最小表容量。(否则,如果一个bin 中有太多的节点,则会调整表的大小。)应该至少是4 * TREEIFY_THRESHOLD ,以避免调整大小和树化阈值之间的冲突。 |
2.成员属性
默认都是 null
属性 | 说明 |
---|---|
transient Node<K,V>[] table | 表,在第一次使用时初始化,并根据需要调整大小。分配时,长度总是2 的幂。(我们还允许某些操作的长度为0 ,以允许当前不需要的引导机制。) |
transient Set<Map.Entry<K,V>> entrySet | 保存缓存的 entrySet() 。注意,AbstractMap 字段用于 keySet() 和 values() 。 |
transient int size | 此 Map 中包含的键-值 对的数量。 |
transient int modCount | 结构修改是指改变 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新哈希)的修改。该字段用于使 HashMap 集合视图上的迭代器快速失败。(见ConcurrentModificationException)。 |
int threshold | 要调整大小的下一个大小值( 容量 * 加载因子 )。 |
final float loadFactor | 哈希表的加载因子。 |
——————————————————— |
静态内部类
class Node<K,V>
基本的哈希 bin
(桶)节点,用于大多数键值对。(参见下面的TreeNode子类,以及LinkedHashMap的Entry子类。)
static class Node<K,V> implements Map.Entry<K,V>
final int hash; // 散列值。通过静态方法 hash(Object key) 计算 key 生成的
final K key; // 没错就是用我算出的 hash
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;
// 1. 如果地址相等,直接 true
// 2. 如果 o 是 Map.Entry(键值对实体)的实例,且 key、value 都一样则 true
// 3. 否则不相等 false
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;
静态工具方法
访问修饰符&返回类型 | 方法 | 描述 |
---|---|---|
static final int | hash(Object key) | 获取 key 的 hash 值。为了尽量避免碰撞,使用 异或 和 位移 。是出于性能考虑。 |
static Class<?> | comparableClassFor(Object x) | 如果x 的形式是Class C implements Comparable<C> 返回它的类的类型 。否则为空。 |
static int | compareComparables(Class<?> kc, Object k, Object x) | 如果 x的类型 是 kc 就返回 k.compareTo(x) 的结果,否则返回 0 。 |
static final int | tableSizeFor(int cap) | 返回大于 cap (给定目标容量)的最小 2 次幂数。 |
——————— | —————————————— |
hash(Object key)
获取 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)
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)
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
然后才使用。
@SuppressWarnings("rawtypes","unchecked") // 压制强转警告
static int compareComparables(Class<?> kc, Object k, Object x)
// 以下情况中假设 k、x 的类型都是 String
// 1. x 为 null 直接返回 0 (表示比个毛)
// 2. kc 是从 k 上获取的比较器(Comparable<String>)的参数的类型(String.class)。
// 如果 k 没有实现 Comparable<String> 则 kc 为 null,否则 kc 为 String.class
// 3. x.getClass() != kc 意思是:如果 k 没有实现 Comparable<String> 比较器,就没法比,直接返回 0
// 换句话说只有 k 实现了 Comparable<X> 才会执行到 ((Comparable)k).compareTo(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
从下而定个例子也可以看出,当有 6
位时,第三次移动时,就已经得到全是1
的效果了。
// ========================= 6位 =========================
100000
0100000 // 第1次
------------------
110000
00110000 // 第2次
------------------
111100
0000111100 // 第3次
------------------
111111 // 6位第次就搞定了
// ========================= 再来个全的32位 =========================
10000000000000000000000000000000
01000000000000000000000000000000 // >>> 1
------------------------------------
11000000000000000000000000000000 // |
00110000000000000000000000000000 // >>> 2
------------------------------------
11110000000000000000000000000000 // |
00001111000000000000000000000000 // >>> 4
------------------------------------
11111111000000000000000000000000 // |
00000000111111110000000000000000 // >>> 8
------------------------------------
11111111111111110000000000000000 // |
00000000000000001111111111111111 // >>> 16
------------------------------------
11111111111111111111111111111111
cap:目标容量传进来前确保 >= 0
static final int tableSizeFor(int cap) // cap = 50
int n = cap - 1; // 此处 -1 确保当正好是2的二次幂时,最后的 +1 能还原此数。
// 这一通 >>> 与 | 配合下来,能得到原最高位后所有位都变成1.
// 如: 100000 to 111111; 101010 to 111111;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 小于0直接返回 1
// 如果大于最大值直接返回最大值,否则当前值 +1 返回。
// +1 能保存是 2的二次幂。因为最高位后所有都是1时,再+1,肯定是一个2的倍数。
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
构造方法
访问修饰符&返回类型 | 方法 | 描述 |
---|
HashMap(int initialCapacity, float loadFactor)
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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;
this.threshold = tableSizeFor(initialCapacity);
HashMap(int initialCapacity)
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity)
this(initialCapacity, DEFAULT_LOAD_FACTOR);
HashMap()
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap()
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
HashMap(Map<? extends K, ? extends V> m)
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m)
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
- putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
/**
* Implements Map.putAll and Map constructor
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
int s = m.size();
if (s > 0)
if (table == null以上是关于Java 集合学习笔记:HashMap的主要内容,如果未能解决你的问题,请参考以下文章