Java容器——HashMap(Java8)源码解析(一)

一 概述

HashMap是最常用的Java数据结构之一,是一个具有常数级别的存取速率的高效容器。相对于List,Set等,结构相对复杂,本篇我们先对HashMap的做一个基本说明,对组成元素和构造方法进行介绍。

二 继承关系

首先看HashMap的继承关系,比较简单,实现了Map和序列化等。

                                                                       图1 HashMap继承关系图

HashMap继承自Map,Map作为一个重要的接口,很有必要需要介绍一下。

                                                                       图2 Map接口

 Map接口定义了一些通用方法,包括插入,删除,替换,遍历元素等常规集合方法。这里有必要重要关注的有:

1 Entry接口:

Entry<K,V>是Map元素的组成形式,它是一个键值对,Key是Map的索引,Value是存储的元素。由于Key是为了便于快速查找,并且能够唯一标识,所以推荐使用不变类,如String来做键,若是自己实现的类,则必须重写hashcode和equals方法。Entry是Map元素的组成形式,可以并且只能通过iterator来遍历。

2 forEach方法:

    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch (IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

注意着是一个接口中的default方法。Java自1.8以后支持在接口中实现默认方法,不同于抽象方法,子类必须重新实现,default方法是开箱即用。这里可以看到,forEach方法是对Entry进行遍历并且执行指定操作。

三 组成元素

3.1 类实例变量

    /**
     * Entry数组
     */
    transient Node<K,V>[] table;

    /**
     * Entry集合
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * Entry的数量
     */
    transient int size;

    /**
     * HashMap被修改的次数
     */
    transient int modCount;

    /**
     * 扩容阈值
     *
     */
    int threshold;

    /**
     * HashMap的装载因子
     *
     */
    final float loadFactor;

可以看到,这其中的变量和HashTable中基本一致,事实上,HashMap就是HashTable的去同步锁以及提升单节点效率的优化版。为何需要提生效率,一方面同步操作没有必要使用synchronisd这种重量级锁,另一方面,HashTable的设计方式可能会发生性能机具下降。

需要注意的变量关系是capability * loadFactor = threshold。翻译一下就是HashMap的扩容阈值是当前容量乘以承载因子。这个阈值不是table中的下标数量,而是整个HashMap已经装载的元素。

                                                                图2 HashTable出现极端Key碰撞

当HashTable的Key碰撞了以后,会在单一Node节点处形成单向链表。所以假设Key选取的不是很合适,冲突很多,HashTable就退化成LinkedList了,查找效率和插入效率都剧烈下降,这也背离了设计的初衷。                         

 HashMap要如何解决这个问题呢,可以从下面定义的变量中一窥一二。

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

为了避免出现HashMap变链表,HashMap引入了红黑树。红黑树是一种尽量保持平衡的搜索二叉树,简单而言对红黑树的增删改查都可以再O(lgn)时间内完成,较链表的O(n)有了巨大的提升,详细了解见红黑树维基百科

这些变量也说明了链表与红黑树相互转化的条件:

1 当链表长度超过TREEIFY_THRESHOLD时,同时满足capacity大于MIN_TREEIFY_CAPACITY时,链表转化为树;

            2 当树节点少于UNTREEIFY_THRESHOLD时,从树转化为链表。

四 构造函数

HashMap的初始化构造方法有四个,都是围绕initialCapacity和loadFactor这两个变量展开的,由此可见这两个变量的重要性,最后一个是根据已有的Map集合初始化新的Map。

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

到这儿了,基本把HashMap的关键元素介绍完了,接下来就是HashMap的具体实现了。那么HashMap究竟有哪些关键操作,并且是如何实现的,请看HashMap(Java8)源码解析(二)

猜你喜欢

转载自blog.csdn.net/qisong3/article/details/84348434