diff --git a/notes/Java 容器.md b/notes/Java 容器.md index cdc3012d..e91a5a84 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -266,7 +266,7 @@ private void writeObject(java.io.ObjectOutputStream s) [HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) -### 1. 基本数据结构 +### 1. 存储结构 使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶。 @@ -278,6 +278,50 @@ transient Entry[] table;

+Java 8 使用 Node 类型存储一个键值对,它依然继承自 Entry,因此可以按照上面的存储结构来理解。 + +```java +static class Node implements Map.Entry { + final int hash; + final K key; + V value; + Node next; + + Node(int hash, K key, V value, Node 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; + } +} +``` + ### 2. 拉链法的工作原理 ```java @@ -294,7 +338,15 @@ map.put("vaibhav", 20); 当进行查找时,需要分成两步进行,第一步是先根据 hashcode 计算出所在的桶,第二步是在链表上顺序查找。由于 table 是数组形式的,具有随机读取的特性,因此第一步的时间复杂度为 O(1),而第二步需要在链表上顺序查找,时间复杂度显然和链表的长度成正比。 -### 3. 扩容 +### 3. 链表转红黑树 + +应该注意到,从 Java 8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。 + +

+ +### 4. 扩容 + +因为从 Java 8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 Java 7 的内容。 设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的数量级为 O(N/M)。 @@ -374,7 +426,32 @@ void transfer(Entry[] newTable) { } ``` -### 4. capacity 保证为 2 的幂次方 +### 5. 确定桶下标 + +需要三步操作:计算 Key 的 hashCode、高位运算、除留余数法取模。 + +

+ +**(一)hashcode()** + +```java +public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); +} +``` + +**(二)高位运算** + +通过 hashCode() 的高 16 位异或低 16 位,使得数组比较小时,也能保证高低位都参与到了哈希计算中。 + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +**(三)除留余数** 令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: @@ -403,13 +480,15 @@ y%x : 00000010 拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 +以下操作在 Java 8 中没有,但是原理上相同。 + ```java static int indexFor(int h, int length) { return h & (length-1); } ``` -### 5. null 值 +### 6. null 值 get() 操作需要分成两种情况,key 为 null 和不为 null,从中可以看出 HashMap 允许插入 null 作为键。 @@ -467,7 +546,7 @@ private V putForNullKey(V value) { } ``` -### 6. 与 HashTable 的区别 +### 7. 与 HashTable 的区别 - HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap,它是 HashTable 的替代,而且比 HashTable 可扩展性更好。 - HashMap 可以插入键为 null 的 Entry。 diff --git a/pics/061c29ce-e2ed-425a-911e-56fbba1efce3.jpg b/pics/061c29ce-e2ed-425a-911e-56fbba1efce3.jpg new file mode 100644 index 00000000..29d32e72 Binary files /dev/null and b/pics/061c29ce-e2ed-425a-911e-56fbba1efce3.jpg differ diff --git a/pics/hashMap_u54C8_u5E0C_u7B97_u6CD5_u4F8B_u56FE.png b/pics/hashMap_u54C8_u5E0C_u7B97_u6CD5_u4F8B_u56FE.png new file mode 100644 index 00000000..f19ceb66 Binary files /dev/null and b/pics/hashMap_u54C8_u5E0C_u7B97_u6CD5_u4F8B_u56FE.png differ