HashMap底层实现原理

171 阅读6分钟

HashMap

image.png

1. JDK1.7 HashMap底层数据结构分析

1.1. JDK1.7中,HashMap底层采用哈希表结构(数组+链表)实现,结合了数组和链表的各自优点,数组中的每个元素都是链表,HashMap通过 put&get 方法存储和获取

- 数组的优点

数组是顺序存储结构,通过数组下标可以快速实现对数组元素的访问,效率极高;

- 数组的缺点

插入或删除元素效率较低,因为可能需要数组扩容、移动元素;

- 链表的优点

链表是一种链式存储结构,插入或删除元素不需要移动元素,只需要修改指向下一个节点的指针域,效率较高;

- 链表的缺点

链表访问元素需要从头到尾逐个遍历,效率较低;

1.2. 单向链表由一个 Entry 内部类表示

Entry 包含四个属性:key,value,hash值和用于单向链表的 next(指向下一个entry节点)

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

1.3. capacity:当前数组容量,默认是16,始终保持 2n2^n,可以扩容,扩容后数组大小为当前的2倍;

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

1.4. loadFactor:负载因子(或叫加载因子),配合下面的扩容阈值一起进行使用,默认值为0.75;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

1.5. threshold:扩容的阈值,意思就是当我们的数组中达到多少个元素的时候开始进行扩容,等于 capacity*loadFactor ;

1.6. 数组

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

2. JDK1.8中的HashMap

JDK1.8 对 JDK1.7 中的HashMap进行了一些修改,最大的不同就是新增了红黑树,所以JDK1.8中HashMap由 数组+链表+红黑树 构成;

在 JDK1.7 HashMap 中查找的时候,根据hash值能够快速定位到数组的具体下标,但如果是链表的话,则需要顺着链表一个个比较下去才能找到需要的元素,时间复杂度取决于链表的长度为 O(n)O(n);

为了降低这部分的开销,在 JDK1.8 中,当链表中的元素大于等于8个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)O(logN)

2.1 HashMap中的红黑树

当单向链表中节点的个数大于等于8时,链表变为平衡二叉树,也就是红黑树,引入红黑树是为了解决单向链表深度过大的问题,优化链表的数据查找性能

红黑树的特点

  • 树中每个节点最多有两个子树;
  • 每个子树又是一个二叉树;
  • 左子树的节点都小于等于根节点;
  • 右子树的节点都大于等于根节点;
  • HashMap根据节点的hash值生成二叉树;

JDK1.7 中使用 Entry 来代表每个 HashMap 中的数据节点,JDK1.8 中使用 Node 代表每个 HashMap 中的数据节点,只是换了个名字,没有其它区别

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

都是 key,value,hash和next这四个属性,不过 Node 只能用于链表的情况,红黑树的情况使用 TreeNode;

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

根据数组元素中第一个节点的数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树

3. HashMap中put操作

HashMap<String, String> map = new HashMap<>();
map.put("key1", "1");
map.put("key2", "2");

进入看 put 源码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

我们先看 hash(key) 操作

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

将传入的 key 进行 hash 操作:

  • 如果 key 为 null,则返回0;
  • h = key.hashCode() :先取 key 的 hashCode 值赋值给变量h;
  • h >>> 16 :然后将 h 右移16位
  • ^ :最后进行 异或运算 hashCode()的高16位异或低16位计算,主要是从性能、hash碰撞来考虑的,减少系统的开销,降低hash碰撞的概率

然后我们再看调用的 putVal() 方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K, V>[] tab;
    Node<K, V> p;
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        // resize() 创建数组
        n = (tab = resize()).length;
    // (n - 1) & hash 计算数组下表,并判断数组当前位置是否已经有值
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 没有值的话,将值放入到上面计算的数组下标位置上
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果有值的话,则进入到这里
        Node<K, V> e;
        K k;
        // 先判断key的hash值是否与原来的相等,相等则说明当前传入的key以前设置过了
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            // 那么直接将原来的值替换成刚传入的值
            e = p;
        else if (p instanceof TreeNode)
            // 判断是否为红黑树节点
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        else {
            // 最后这里则表示是普通的链表节点
            for (int binCount = 0; ; ++binCount) {
                // 判断下一个节点是否为null
                if ((e = p.next) == null) {
                    // 为null,则再创建一个节点(newNode)
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 将链表转换为红黑树,TREEIFY_THRESHOLD值默认为8
                        treeifyBin(tab, hash);
                    break;
                }
                // 这个判断和最开始的if判断一样
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // key 已经存在了,则直接将原来的值替换掉
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        // 如果当前数组长度大于threshold(默认是12),则进行扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}

简单总结一下 HashMap 的 put 操作

  1. 调用哈希函数获取key对应的hash值,再计算其数组下标;
  2. 如果没有出现哈希冲突,则直接放入数组,如果出现哈希冲突,则以链表的方式放在链表后面;
  3. 如果链表长度超过阈值(TREEIFY_THRESHOLD = 8),就把链表转换成红黑树;
  4. 如果节点的key已经存在,则替换掉value值即可;
  5. 如果长度大于threshold(12),则进行扩容。

4. HashMap中get操作

HashMap<String, String> map = new HashMap<>();
map.put("key1", "1");

String key1 = map.get("key1");

get方法源码

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode()方法取值,如果为null,则返回null,否则返回value

getNode()方法源码

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 这里先判断数组不为空并且数组长度大于0
    // (n - 1) & hash 这个还是计算下标,并且判断该数组下标位置的值不为null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 判断是否为红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
            // 使用 do while 循环去取值
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

HashMap 的 get 操作

  1. 先根据key的hash值计算数组的下标;
  2. 根据计算得到的数组下标访问数组元素,如果数组元素为null则返回null;
  3. 如果数组元素不为null,则遍历该数组元素单向链表的每个节点,如果某个节点的key与当前key相等,则把该节点的值返回;
  4. 如果某个节点的key与当前所有key都不相等,则返回null;
  5. 如果长度大于threshold(12),则进行扩容。