再来看Hash Map实现

对HashMap的疑惑

通过培训接触Java,学习了基础的Java知识,然后投入了Java开发的工作;工作过程中对Hash Map的使用仅仅限于API阶段,其实是没有深入了解过HashMap的内部实现的;对于其实现更多的是在准备面试的阶段(希望你不是。。。);最开始接触的当然是JDK1.7版本的Hash Map,当时为了面试网上搜寻各种博客,文档去了解其实现:数组实现,链表结构,hash算法等等。再到后来JDK1.8中的树化,大体的概念是有所了解的,但是更深入的原理是没有深究过的。最近在学习网易云课堂的Java微专业,所以在此对自己的学习做一个总结或者算是一个记录,若是能对你有些许帮助,那更是荣幸之至!

HashMap概述

首先,简单来讲Hash Map就是一个存储key/value;放入元素的时候通过key确定value存放的位置;取出元素的时候通过key来快速锁定需要取的value。当然以上讲述过于白话,让我们通过官方文档来认识一下:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

基于哈希表的Map接口的实现。实现提供map操作的所有实现,并且允许null值和null键(HashMap和Hashtable基本相同,除了不是同步和允许null值。)它不保证存储元素的顺序尤其是不会保证存储元素的顺序是一直不变的(存储元素的顺序是根据key值的hash计算来确定的,而在进行扩容的时候执行rehash操作会重新计算hash值,所以顺序是无法保证的)。
下面我们通过HashMap的数据结构,存取元素来了解HashMap。

JDK1.7版本数据结构(数组+链表)

我们先来看一张图(图片来源于网络,并非原创):
在这里插入图片描述
HashMap内部维护了一个数组,每个数组元素对应的是一个链表,链表的头部是数组的元素,数组和链表的元素类型是Enty<K,V>,这通过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.)
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

Node是链表的元素类型;它继承了Entry<K,V>,Entry是map的内部类;

JDK1.8版本数据结构(数组+链表/红黑树)

在这里插入图片描述
1.8版本的JDK中可以看到,是链表和红黑树同时存在的,jdk引入红黑树的原因在于,在调用Map的put()方法存储元素时,如果元素的hash值相同的情况下会在一个数组元素下的链表上一直追加元素,导致链表过长,此时查找元素的效率会越来越低,因此引入平衡的红黑树提高hash冲突导致链表过长的问题。以下源码为红黑树的节点类型代码:

 /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {}

TreeNode继承了LinkedHashMap的Entry内部类,此时如果继续查看可以发现它同样继承了Map接口的Entry<K,V>.
HashMap使用链表法避免哈希冲突(相同hash值),当链表长度大于TREEIFY_THRESHOLD(默认为8)时,将链表转换为红黑树,当然小于UNTREEIFY_THRESHOLD(默认为6)时,又会转回链表以达到性能均衡。
上文中你会发现树化阈值和非树化阈值是不相同的,这是为了避免频繁的树化转换造成的性能消耗。

HashMap的API操作

首先介绍一下HashMap的灵魂算法Hash();

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

HashMap提供默认的hash值算法实现,好的Hash算法可以保证放入的元素均匀的分布在数组的各个元素上,同时也是让我们能够快速定位元素获取value的关键所在(可以发现如果存储元素是null,那么返回的值是0,说明HashMap可以存储null元素,但是只能存储一个,再次放入key值为null的元素只会覆盖value值)。

put(K,V)

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

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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)
            n = (tab = resize()).length;
        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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

以上代码为JDK1.8版本中的put方法,它跟JDK1.7中的put方法相比可读性不是很高,看起来甚至有点拒人千里之外的感觉;但是总体的思路是差不多的,下面概述一下put方法的逻辑:
首先计算key值的hash值,然后查看数组对应hash值位置是否有元素,如果没有元素那么直接new一个node元素放在当前位置,如果存在元素则调用equals()方法判断是否key值相等,如果相等则替换value的值,如果不相等则遍历当前元素下的链表或红黑树如果确实不存在key值相等的元素,则追加到链表末尾或者红黑树上。

关键点解析

  1. 初始化
    上述代码中在判断当前数组table为空或者table的大小为0的情况下会调用resize()方法对table数组进行初始化;因为在通常的使用当中我们通常是直接调用无参数的构造方法进行HashMap的创建的,如下
HashMap<K,V> map = new HashMap<K,V>();

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
static final float DEFAULT_LOAD_FACTOR = 0.75f;

无参构造只会给负载因子赋初始值0.75f,并不会初始化table数组。因此在第一次存放元素的时候才会初始化数组,默认大小16

newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  1. 树化
    当满足条件ibinCount >= TREEIFY_THRESHOLD - 1 时,会调用树化方法将链表转化为红黑树。此处红黑树的详细情况不做过多说明,后期如果有机会再补充红黑树部分。
/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {...}
  1. modCount
/**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

相信大家都见过以下异常:ConcurrentModificationException;当我们再遍历某个集合或者map的时候同时对元素进行remove操作的时候就会出现这个异常;modCount是用来记录当前map变更的次数的,java内部再遍历之前会记录modCount,在遍历过程中随时对modCount进行比对,如果发现发生了变化则代表当前遍历的预期结果可能会发生变化,也就是map已经和遍历前不一样了,就会出现上述异常;
同时HashMap中使用的是++操作,这并不是一个原子操作,所以说HashMap是不安全的也有这个原因。

get(K)

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

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    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) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

下面我们同样对get方法做一个概述:
首先计算key值的hash值,通过hash值快速定位数组中的位置,然后遍历链表和红黑树调用equals()方法查找相等key值的元素获取value值。

总结

上述文章中并没有对基础的构造方法和API做太多说明,可以说知识对自己学习过程的一个复习过程,后期可能会再对其中细节的地方进行补充,希望自己以后的学习过程可以逐渐养成写博客的习惯,及时将自己的知识点记录下来,以达到沉淀的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值