HashMap(1)- 定义,静态变量和内部类

1. 什么是HashMap

我们先来看源码中HashMap类的描述

一个基于哈希表的Map接口的实现。

这个实现提供了map所有的可选的操作,允许键和值为null。(HashMap类除了线程不安全和允许null之外大体上和HashTable类是相同的)这个类不对map的顺序做保证;特别是,它不能保证顺序会随着时间的推移保持不变。

假设哈希方法能把元素正确的分散到各个桶里,这个实现就可以提供常数时间性能(constant-time performance)的基础操作(getput)。遍历一个HashMap实例所需的时间和它的“容量”(桶的数量)加上它的大小(键值的数量)成正比。因此,如果对遍历的性能有重要要求的话,就不要把初始容量设置的过高(或者把负载系数load factor设置的过低)。

有两个参数对于一个HashMap的实例的性能有影响:初始容量initial capacity)和负载系数load factor)。容量就是哈希表中桶的数量,而初始容量也就是哈希表创建时的容量。负载系数是哈希表容量在自动增加之前允许达到的最大的容量的系数。当哈希表的键值对数量超过了当前容量和负载系数计算得出的数量时,哈希表进行rehashed(重新建立内部的数据结构),哈希表中桶的数量大概会变成之前的两倍。

一般来说,默认的负载系数(0.75)是权衡了时间消耗和空间消耗得出的值。更高的值能降低空间开销但是会提高查询的消耗(在HashMap类的大部分操作中都会有所体现,包括getput)。在设置初始容量时,应该考虑预期的键值对数量和负载因子的值,从而尽量减少rehash的次数。如果初始容量比键值对数量除以负载因子大的话,就根本不会发生rehash了。

如果一个HashMap实例中想要存储很多映射关系,创建的时候指定一个足够大的容量会比让它在需要的时候自动rehash增长哈希表来的更有效。请记住,使用具有相同hashCode()返回值的key一定会减慢任何一个哈希表的性能。为了改善这种影响,当key实现了Comparable接口时,这个类可能会把key进行比较,来解决问题。

注意,这个实现时线程不安全的。如果多个线程要同时访问一个HashMap,而且至少一个线程要修改map的结构的话,那么必须在外部进行同步。(所谓修改map的结构是指任何新增或删除了映射关系的操作,仅仅修改已经存在的某个键对应的值的话,不算修改结构。)这通常是由封装了这个map的对象来实现的。如果不存在这样的对象,map应该使用Collections.synchrinizedMap方法来包装。为了防止意外的不同步访问列表,最好在创建的时候来完成:

Map m = Collections.synchronizedMap(new HashMap(...));

类的所有“集合视图方法”返回的iterator都是快速失败fail-fast)的:如果在iterator创建后的任何时间对映射进行结构修改,除了通过迭代器自己的 remove方法外,迭代器将抛出 ConcurrentModificationException。因此,面对并发修改,迭代器将快速而干净地失败,而不是在未来去冒不确定的时间发生不确定行为的风险。

注意,不能保证迭代器的快速失败fail-fast)行为,因为一般来说,在存在非同步并发修改的情况下不可能做出任何硬性保证。 快速失败fail-fast)的迭代器会尽最大努力抛出 ConcurrentModificationException。 因此,编写一个依赖这个异常来确保其正确性的程序是错误的:迭代器的快速失败行为应该仅用于检测错误

简单总结一下,主要是这么几点:

  1. HashMap允许键和值为null
  2. HashMap是无序的
  3. HashMap是线程不安全的
  4. HashMap的性能受到初始容量initial capacity)和负载系数load factor)这两个参数的影响,如果使用正确的话,HashMap可以提供常数时间性能(constant-time performance)的基础操作(getput)。

前面三点都很好理解,我们着重来看一下第四点

2. HashMap的几个静态变量

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

默认的初始容量initial capacity),要求必须是2的n次方,值为1<<4 也就是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;

最大的容量,如果两个带参数的构造方法隐式的指定了更高的值,就用这个值。要求必须是2的n次方,值为1<<30 也就是1 073 741 824

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

默认的负载系数load factor),如果构造方法没有指定,就使用这个值。值为0.75f

也就是说,我们平时最常用的无参数的构造方法,是创建了一个初始容量为16,负载系数为0.75的HashMap

/**
 * 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;

使用树而不是列表的桶计数阈值。当向桶里添加新的元素,而且一个桶里有至少这么多的节点时,要把这个桶转化为树。这个值必须大于2,并且应该最少是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;

resize操作时将树转化为列表的阈值,应该比TREEIFY_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;

可以把列表转化为树的最小哈希表容量(否则如果一个桶里有太多的节点,表就会被resize)。最少应该是4 * TREEIFY_THRESHOLD,来避免重新分配大小和转化为树的阀值的冲突。

3. HashMap的几个内部类

接下来我们来看一下HashMap的几个主要的内部类,也就是如何实现Map接口定义的几个内部接口的

  1. Node
    Nodemap中的一个实体(一个键值对),提供了getKey()getValue()setValue()几个方法。包括hashkeyvaluenext几个属性,而final修饰符表明一个Nodekeyhash一旦被设定,是不允许进行修改的。而通过next可以看出Node类实质上是一个单向链表。
/**
  * Basic hash bin node, used for most entries.  (See below for
  * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
  */

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 哈希值,final类型表明初始化后不会再被修改
    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;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}
  1. KeySet、Values、EntrySet
    KeySet需要结合HashMap本身的KeySet()方法来看,这个方法返回了这个map中包含的所有的key,由于KeySet是基于这个map的,因此修改了map就会反映在这个set里,而且反之亦然。如果在遍历set时修改了map(除了使用迭代器本身提供的remove方法),遍历的结果就会不确定。你可以通过Iterator.removeSet.removeremoveAllretainAllclear 这些操作来移除set中的元素,被移除的元素会相应的反映到map里。它不支持addaddAll操作。
    对应的,Values类同样需要结合HashMap本身的values()方法来看,它返回了这个map中所有value的集合,其它的特性和KeySet一致。
    类比KeySetvaluesMap.entrySet方法返回的是这个map中所有Entry的集合,其它的特性也是一样的。
public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}
final class KeySet extends AbstractSet<K> {
    //...
}

public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}
final class Values extends AbstractCollection<V> {
    //...
}

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    //...
}
  1. TreeNode
    TreeNode可以说是HashMap最重要的内部类了,它继承自LinkedHashMap.Entry,而LinkedHashMap.Entry反过来又继承了HashMap.Node,这样它就可以用作常规或者链式节点的扩展了。
    TreeNode的参数和几个关键方法不难看出,它的本质是一棵红黑树。关于红黑树的定义和具体实现,我们这里不再细说,主要来看几个和Node相关的方法。treeify:生成链接到该节点节点树,和untreeify:返回链接到该节点节点树转化成的非树列表。通过这两个方法,可以实现NodeTreeNode的相互转换。可以猜测,HashMap的节点可能是Node,也有可能是TreeNode,而且这两者在某种条件下会相互转换。而这个转换的条件,通过静态变量的文档描述,应该是和TREEIFY_THRESHOLDUNTREEIFY_THRESHOLDMIN_TREEIFY_CAPACITY这三个静态变量有关系。
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);
    }
    //...

    /**
     * Forms tree of the nodes linked from this node.
     */
    final void treeify(Node<K,V>[] tab) {
        //...
    }

    /**
     * Returns a list of non-TreeNodes replacing those linked from
     * this node.
     */
    final Node<K,V> untreeify(HashMap<K,V> map) {
        //...
    }

    //...
}

看到这里,我基本了解了HashMap内部的数据结构,下一篇再来看HashMap是如何实现putgetremove等基本操作的。