🍧 Peach

蜜桃学代码

🦋 HashMap - 原理

- 目录
HashMap HashMap 原理
HashMap 源码

注:本文源码版本均为:JDK 8


一. 原理

(1)HashMap怎么存取

%%{
    init: {
        'themeVariables': {
            'fontSize': '13px'
        }
    }
}%%
graph TD

T(("HashMap
怎么存取值
")):::p T --> |"数据以键值对形式存储"| A["key — value"] A --> |"包装为Node数组对象"| B["Node { key, value }"] B --> |"通过node数组下标保存对应数据"| C["node[0] = new Node { key, value }
node[1] = new Node { key, value }
node[2] = new Node { key, value }
...
"] C -.-> |"[ 保存 ]
遍历数组,对应key和value"| D["for (Node node : nodes)"] C --> |"[ 获取 ]
通过 数组索引 获取(key的hash值)
"|E["node[ hash(key) ] = value"]:::lb classDef p fill:#ddaebd style E fill:#d9dfeb, stroke:#657eae,stroke-width:2px,stroke-dasharray: 3 3
  • 使用Node数组保存、通过Node数组索引获取。

(2)Hash算法怎么写

%%{
    init: {
        'themeVariables': {
        'fontSize': '13px',
        'clusterBorder': 'none',
        'clusterBkg': '#f0f2f7'
    }
}
}%%
graph TD
T(("Hash算法
怎么写?
")):::p T --> |"如何计算出数组索引?hash(key)"| A["* 因为Node数组有固定的初始化默认长度(length)
* 索引值需限定在数组长度范围内(0~length-1)
* 索引值还要尽可能平衡、分布均匀"] A --> B(["取模运算:hash % length"]) B ==> |"(更好的方案
使用位运算效率更高)"| C(["位运算:hash & (length - 1)"]) C -.- c("length必须是2的n次幂"):::b subgraph "前置条件" c -.- c2["假如 key = 20, length = 16, hash & (length-1):
0001 0100 // 20
0000 1111 // 15
0000 0100 // =4"]:::c2 end classDef p fill:#ddaebd classDef b fill:#d2d9e7, stroke-width: 0px style c2 color: #86868b, fill:#ebedf3, stroke-dasharray: 3 3 %% 取模运算 style B fill: #f4e4e9 %% "位运算" style C fill:#b8c3d9, stroke:#657eae,stroke-width:2px
  • 为了解决取模的效率问题,采用了位运算的方法:index = hash值 & (length -1)
    此时需要一个前置条件:length的长度必须是2的n次幂
    当length的长度是2n时,有以下公式成立: num%2n = num&(2n-1)

    🦋 因为就像十进制取余数一样,若除以10或10的整次幂数,余数刚好是取低位上的数字。同理,二进制取余数只要除以2的n次幂数,低位上的数字就是我们需要的余数。而(2n - 1)的二进制有效位都是1,和n位的1作与运算相当于取低n位的值。

  • length的长度刚好是2的n次幂,扩容也是按原来的2倍容量进行扩容,可以加快hash计算、减少hash冲突。

    🦋 加快hash计算

    1. 两倍扩容机制使容量一直保持在2的n次幂数,就可以使用位运算代替取模运算,提升hash计算效率。
    2. 当容量保持为2的n次幂时,每次扩容时位运算计算得出的余数不变,数据存放的索引位置也保持不变。而使用%计算时,结果会因为容量的变化而改变(模运算会产生小数点),每次扩容时数据在数组中存放的位置会发生改变(数据漂移),影响性能。

    🦋 减少hash冲突

    • 将容量保持为偶数进行hash计算时,经过(length-1)后,计算出的索引值为奇数和偶数的概率相同(取决于随机生成的hash值);
    • 而使用奇数容量进行hash计算时,经过(length-1)后,最终结果均为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这浪费了近一半的空间。
    • 因此2的n次幂容量、双倍扩容机制可以使容量保持在偶数值,可以使添加的元素在HashMap的数组中均匀分布,减少hash碰撞。

(3)Hash冲突怎么办

%%{
    init: {
    'themeVariables': {
        'fontSize': '13px'
        %%'edgeLabelBackground': '#ddebe6'
    }
}
}%%
graph TD

T(("1. Hash冲突
怎么优化
")):::p --> T1("hash表里可以存储元素的位置
被称为“桶(bucket)”") T1 --> |"通常情况"| A("单个“桶”里存储一个元素,
此时性能最佳"):::g A -.-> |"O(1)"| a["hash算法可以根据hashCode值
计算出“桶”的存储位置,
接着从“桶”中取出元素。"] T1 --> |"哈希冲突的情况"| B("单个桶会存储多个元素(hash值相同)"):::g B -.-> |"O(n)"|b(["多个元素以 链表 形式存储
"]):::B b --> 2T(("2. Hash冲突
很大怎么优化
")):::p subgraph "(链表必须按顺序搜索,存取性能差)" 2T --> C(["将链表结构转为红黑树"]):::B C -.- |"O(logn)"|c["以树的高度为最差查询性能,永远不会出现O(n)的情况。"]:::info end classDef p fill:#ddaebd classDef g fill:#f4e4e9 classDef B fill:#d9dfeb, stroke:#657eae,stroke-width:2px classDef info fill:#f6f6f7,color:#737379, stroke-width: 2px, stroke-dasharray: 3 3
  • Hash冲突示意图

        %%{
          init: {
              'themeVariables': {
                  'clusterBorder': 'none',
                  'clusterBkg': '#ececee'
              }
          }
      }%%
      %% 链表超过8时树化成红黑树
      graph TD
    
      subgraph "Node[]"
      Entry0["Node0
    Node<K,V>"] -.- Node1 -.- Node2 -.- Node3 -.- Node4 -.- Node5 -.- Node6 end subgraph 链表 Node1 --> A[Node] --> B[Node] --> C[Node] --> D[Node] --> E[Node] --> F[Node] end subgraph 红黑树 Node4 --> a[Node] a[Node] --> b[Node] b --> b1[Node] b --> b2[Node] a --> c[Node] c --> c1[Node] c --> c2[Node] b1 --> d[Node] b1 --> d2[Node] end style 链表 fill:#ddebe6 style 红黑树 fill:#f4e4e9

(4)内存结构示意图

  • 创建对象,内存结构示意图:

    Map<String, Integer> countMap = new HashMap<>();
    countMap.put("hello", 1);
    countMap.put("world", 3);
    countMap.put("position", 4);
    内存结构图1
  • 保存键值对

    “hello”的hash值为96207088,模16的结果为0,所以插入table[0]指向的链表头部,内存结构变为:

    内存结构图2

    “world”的hash值为111207038,模16结果为14,所以保存完“world”后,内存结构:

    内存结构图3

    “position”的hash值为771782464,模16结果也为0, table[0]已经有节点了,新节点会插到链表头部,内存结构变为如图:

    内存结构图4



二. 使用

(1)常用方法

// 返回值            方法                         说明
// --------------------------------------------------------------------------------------------
V put(K key, V value) // 将键(key)/ 值(value)映射存放到 Map 集合中,向 HashMap 中添加元素
V get(Object key) // 返回指定键所映射的值,没有该 key 对应的值则返回 null
int size() // 返回 Map 集合中数据数量
void clear() // 清空 Map 集合
boolean isEmpty() // 判断 Map 集合中是否有数据,如果没有则返回 true,否则返回 false
V remove(Object key) // 删除 Map 集合中键为 key 的数据并返回其所对应 value 值。
Collection<V> values() // 返回 Map 集合中所有 value 组成的以 Collection 数据类型格式数据。
boolean containsKey(Object key) // 判断集合中是否包含指定键,包含返回 true,否则返回 false
boolean containsValue(Object value) // 判断集合中是否包含指定值,包含返回 true,否则返回 false
Set<K> keySet() // 返回 Map 集合中所有 key 组成的 Set 集合
Set<Map.Entry<K,V>> entrySet() /* 将 Map 集合每个 key-value 转换为一个 Entry 对象
并返回由所有的 Entry 对象组成的 Set 集合 */
// ------------------------------------------------------------------------------------------
Printer.printHashMapStructures(map); // 打印树结构
getMapFromStringParam();

(2)如何遍历

graph LR
T(("遍历HashMap
的5种方法
")):::p T --> A(["entrySet"]):::lp T --> B(["keySet"]):::lp T --> C(["forEach"]):::lp T --> |"流操作"|D(["StreamApi.forEach"]):::lp T -.-> E(["values"]) A -.-> a("(1)entrySet") A -.-> a2("(2)entrySet + 迭代器") B -.-> |"(效率低)"| b("(3)keySet + get()") C -.-> c("(4)forEach + Lambda") D -.-> d("(5)Stream().forEach(单线程)") D -.-> d2("(6)Stream().forEach(多线程)") E -.-> e("(7)仅遍历value") classDef p fill:#ddaebd classDef lp fill:#f4e4e9 classDef B fill:#d9dfeb, stroke:#657eae,stroke-width:2px
  • 遍历HashMap代码

    package com.equne;

    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;

    public class Main {

    /** (1)entrySet */
    public static void traverse1(Map<String, String> map) {
    for (Map.Entry<String, String> entry : map.entrySet()){
    System.out.println(entry.getKey() + ", " + entry.getValue());
    }
    }

    /** (2)迭代器(entrySet) */
    public static void traverse2(Map<String, String> map) {
    Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
    while (iterator.hasNext()){
    Map.Entry<String, String> entry = iterator.next();
    System.out.println(entry.getKey() + ", " + entry.getValue());
    }
    }

    /** (3)keySet + get() */
    public static void traverse3(Map<String, String> map) {
    for (String key : map.keySet()){
    System.out.println(key + ", " + map.get(key));
    }
    }

    /** (4)forEach + Lambda */
    public static void traverse4(Map<String, String> map) {
    map.forEach((key, value) -> {
    System.out.println(key + ", " + value);
    });
    }

    /** (5)StreamApi(单线程) */
    public static void traverse5(Map<String, String> map) {
    map.entrySet().stream().forEach((entry) -> {
    System.out.println(entry.getKey() + ", " +entry.getValue());
    });
    }

    /** (6)StreamApi(多线程) */
    public static void traverse6(Map<String, String> map) {
    map.entrySet().stream().parallel().forEach((entry) -> {
    System.out.println(entry.getKey() + ", " +entry.getValue());
    });
    }

    /** (7)仅遍历value */
    public static void traverse7(Map<String, String> map) {
    for (String v : map.values()){
    System.out.println(v);
    }
    }

    public static void main(String[] args) {

    Map<String, String> map = new HashMap<>();
    map.put("key1", "value1");
    map.put("key2", "value2");
    traverse1(map);
    traverse2(map);
    traverse3(map);
    traverse4(map);
    traverse5(map);
    traverse6(map);
    traverse7(map);
    }
    }
  • entrySet 和 keySet

    • Map.entrySet 迭代器会生成 EntryIterator,其返回的实例是一个包含 key/value 键值对的对象。
    • 而 keySet 中迭代器返回的只是 key 对象,还需要到 map 中二次取值(多一个get方法的调用)。故 entrySet 要比 keySet 快一倍左右。(keySet适合仅需要操作key时使用)
      🔗 摘自:《Java修炼指南:高频源码解析》
  • foreach + Lambda 表达式(Java8 的新特性)

    • forEach():用于遍历动态数组中每一个元素并执行特定操作。
    • Lambda 表达式:箭头符号 ->,其两边连接着输入参数函数体
  • Stream()

    • 其实用Iterable本身的forEach方法即可,没有必要用流式操作。StreamApi适合在对集合进行其他流式操作之后使用。
    • 补充:用流获取映射值的极端案例:
      Optional<String> value = map
      .entrySet()
      .stream()
      .filter(entry -> entry.getKey().equals(key))
      .map(Entry::getValue) // 双冒号:引用方法
      .findFirst();
      🔗 摘自:《好代码,坏代码》




- end -


参考: