HashMap(1)- 定义,静态变量和内部类
1. 什么是HashMap
我们先来看源码中HashMap
类的描述
一个基于哈希表的
Map
接口的实现。这个实现提供了map所有的可选的操作,允许键和值为
null
。(HashMap
类除了线程不安全和允许null
之外大体上和HashTable
类是相同的)这个类不对map的顺序做保证;特别是,它不能保证顺序会随着时间的推移保持不变。假设哈希方法能把元素正确的分散到各个桶里,这个实现就可以提供常数时间性能(constant-time performance)的基础操作(
get
和put
)。遍历一个HashMap
实例所需的时间和它的“容量”(桶的数量)加上它的大小(键值的数量)成正比。因此,如果对遍历的性能有重要要求的话,就不要把初始容量设置的过高(或者把负载系数load factor
设置的过低)。有两个参数对于一个
HashMap
的实例的性能有影响:初始容量(initial capacity
)和负载系数(load factor
)。容量就是哈希表中桶的数量,而初始容量也就是哈希表创建时的容量。负载系数是哈希表容量在自动增加之前允许达到的最大的容量的系数。当哈希表的键值对数量超过了当前容量和负载系数计算得出的数量时,哈希表进行rehashed
(重新建立内部的数据结构),哈希表中桶的数量大概会变成之前的两倍。一般来说,默认的负载系数(0.75)是权衡了时间消耗和空间消耗得出的值。更高的值能降低空间开销但是会提高查询的消耗(在
HashMap
类的大部分操作中都会有所体现,包括get
和put
)。在设置初始容量时,应该考虑预期的键值对数量和负载因子的值,从而尽量减少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
。 因此,编写一个依赖这个异常来确保其正确性的程序是错误的:迭代器的快速失败行为应该仅用于检测错误。
简单总结一下,主要是这么几点:
-
HashMap
允许键和值为null
-
HashMap
是无序的 -
HashMap
是线程不安全的 -
HashMap
的性能受到初始容量(initial capacity
)和负载系数(load factor
)这两个参数的影响,如果使用正确的话,HashMap
可以提供常数时间性能(constant-time performance)的基础操作(get
和put
)。
前面三点都很好理解,我们着重来看一下第四点
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
接口定义的几个内部接口的
- Node
Node
是map
中的一个实体(一个键值对),提供了getKey()
、getValue()
和setValue()
几个方法。包括hash
、key
、value
和next
几个属性,而final
修饰符表明一个Node
的key
和hash
一旦被设定,是不允许进行修改的。而通过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;
}
}
- KeySet、Values、EntrySet
KeySet
需要结合HashMap
本身的KeySet()
方法来看,这个方法返回了这个map
中包含的所有的key
,由于KeySet
是基于这个map
的,因此修改了map
就会反映在这个set
里,而且反之亦然。如果在遍历set
时修改了map
(除了使用迭代器本身提供的remove
方法),遍历的结果就会不确定。你可以通过Iterator.remove
Set.remove
removeAll
retainAll
clear
这些操作来移除set
中的元素,被移除的元素会相应的反映到map
里。它不支持add
或addAll
操作。
对应的,Values
类同样需要结合HashMap
本身的values()
方法来看,它返回了这个map
中所有value
的集合,其它的特性和KeySet
一致。
类比KeySet
和values
,Map.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>> {
//...
}
- TreeNode
TreeNode
可以说是HashMap
最重要的内部类了,它继承自LinkedHashMap.Entry
,而LinkedHashMap.Entry
反过来又继承了HashMap.Node
,这样它就可以用作常规或者链式节点的扩展了。
从TreeNode
的参数和几个关键方法不难看出,它的本质是一棵红黑树。关于红黑树的定义和具体实现,我们这里不再细说,主要来看几个和Node
相关的方法。treeify
:生成链接到该节点节点树,和untreeify
:返回链接到该节点节点树转化成的非树列表。通过这两个方法,可以实现Node
和TreeNode
的相互转换。可以猜测,HashMap
的节点可能是Node
,也有可能是TreeNode
,而且这两者在某种条件下会相互转换。而这个转换的条件,通过静态变量的文档描述,应该是和TREEIFY_THRESHOLD
,UNTREEIFY_THRESHOLD
,MIN_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
是如何实现put
、get
、remove
等基本操作的。