最新JDK8HashMap实现过程源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/youngogo/article/details/81267773

小编道行也没那么深,就用最通俗易懂的方式,来解释hashmap实现原理。本文基于JDK8分析HashMap(),我们从源码出发将主要分析讨论如下的几个知识点:

  1. HashMap的特点是什么?以及它的使用场景
  2. HashMap的数据结构?
  3. HashMap的工作原理是什么?
  4. equals和hashCode都有什么作用?
  5. 重写equals()为什么一定要重写hashCode()?
  6. HashMap里面的table数组为什么是2的N次方?

1、感知HashMap

 我们首先进行如下操作:

HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("语文", 1);
map.put("数学", 2);
map.put("英语", 3);
map.put("历史", 4);
map.put("政治", 5);
map.put("地理", 6);
map.put("生物", 7);
map.put("化学", 8);
for(Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

接着利用debug模式,从数据结构上认知HashMap,循序渐进

以下是JDK8中HashMap的数据结构源码:

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
    表,在首次使用时初始化,并根据需要调整大小。当分配时,长度总是2的幂。
     */
    transient Node<K,V>[] table;

/**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
        基本的哈希bin节点,用于大多数条目(内部类)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        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;
        }
    }

2、HashMap的两个重要参数

 /**
     * The default initial capacity - MUST be a power of two.

        table的默认初始容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

  /**
     * The load factor used when none specified in constructor.(负载因子)
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

capacity就是初始化table时的数组容量,load factor指table的填充比例;当我们对迭代性能要求比较高时,我们首先不能把capacity设置的太大;同时load factor不要超过0.75,否则会明显增加冲突机率,降低HashMap性能

负载因子 * 容量 > 元素数量(put进去的元素个数)时,就需要调整容量(table的长度)为原来的2倍

3、HashMap的put(Key k,Value v)的原理

在分析源码之前,我们先看看大体思路:

 1)当在第一次put时,先对table初始化,通过hash计算得到存放位置table[i],存放。

 2)当再次put时,同样经过hash计算得到位置,则采用链表法解决冲突,存放在相同位置的next区域

3)在JDK8中设置了链表的默认阈值为8,如果超过这个值,则进行树化

4)如果节点已经存在就替换old value(保证key的唯一性)

5)如果bucket满了(超过load factor*current capacity),就要resize,变为原来2倍

面试题:解释HashMap的原理,数据量增大时,
     在数据量小的时候,HashMap是按照链表的模式存储的。当数据量变大之后,为了进行快速的查找,会将这个链表变成红黑树(均衡二叉树),用hash码作为数据的定位来进行保存。

具体实现代码如下:

public V put(K key, V value) {
    // 对key的hashCode()做hash
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // tab为空则创建
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 计算index得出存放的位置,并对null做处理
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 节点存在
        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) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //当链表长度超过默认阈值,进行树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                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;
    // 超过load factor*current capacity,resize()
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

4、get函数的实现

大致思路如下:

  1. bucket里的第一个节点,直接命中;
  2. 如果有冲突,则通过key.equals(k)去查找对应的entry
    若为树,则在树中通过key.equals(k)查找,O(logn);
    若为链表,则在链表中通过key.equals(k)查找,O(n)。
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    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) {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

为了避免篇幅过长,关于table的长度为什么必须是2的N次方,hash函数的实现以及对开始问题的回答,在下篇博客进行分析,请转最新JDK8HashMap实现原理(二)

欢迎大家留言交流,小编q:1298364867

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章