前言
这是我写的java容器的第四篇,也是到目前为止最重要最难的一篇,没错,正是HashMap。HashMap在jdk1.8修改部分还是比较大的,也是做了比较多的优化,本文就是基于jdk1.8去进行分析的,当然,可能还会对比一下和jdk1.7的一些区别
文章目录
正文
一:存储结构
HashMap内部包含了一个 Entry 类型的数组 table
Entry 存储着键值对 ,Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry,
HashMap在jdk7是数组+链表,而在jdk1.8引进了红黑树,就是基于数组+链表+红黑树,带来的好处是效率上的提升,比如基于jdk1.7的情况下的查询时间从O(1)到O(n)(最坏的情况)
而在jdk1.8中,查询时间最坏情况下也是O(log n)。
二:核心成员变量和常量
没什么好讲的,看源码旁边的注释就完事了,不过要记住,因为这些在源码其他地方都经常用得到
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16 默认容量,必须是2的幂
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//在构造函数中未指定时使用的负载因子
static final int TREEIFY_THRESHOLD = 8;//阈值;当put一个元素时,其链表长度达到8时将链表转换为红黑树
static final int UNTREEIFY_THRESHOLD = 6;//链表长度小于6时,解散红黑树
static final int MIN_TREEIFY_CAPACITY = 64;//默认的最小的扩容量64,为避免重新扩容冲突,至少为4 * TREEIFY_THRESHOLD=32,即默认初始容量的2倍
transient Node<K,V>[] table; //HashMap的哈希桶数组,非常重要的存储结构,用于存放表示键值对数据的Node元素。
transient Set<Map.Entry<K,V>> entrySet; //HashMap将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
transient int size; //HashMap中实际存在的Node数量,注意这个数量不等于table的长度,甚至可能大于它,因为在table的每个节点上是一个链表(或RBT)结构,可能不止有一个Node元素存在。
transient int modCount;
//HashMap的数据被修改的次数,这个变量用于迭代过程中的Fail-Fast机制,其存在的意义在于保证发生了线程安全问题时,能及时的发现(操作前备份的count和当前modCount不相等)并抛出异常终止操作。
int threshold; //HashMap的扩容阈值,在HashMap中存储的Node键值对超过这个数量时,自动扩容容量为原来的二倍。
final float loadFactor; //HashMap的负载因子,可计算出当前table长度下的扩容阈值:threshold = loadFactor * table.length。
三:链表和红黑树的结构
链表部分其实也很简单,只不过这是一个单向链表而已;红黑树部分这里也比极简单,但是具体实现上很难
//单向链表Node<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//hash值
final K key;//插入时给的key值
V value;//插入时对应的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; }
//这里内部重写了hashCode方法,所以对于基于hashmap这样的,hashcode相同并不代表equals后的结果就位true
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 比较两个node是否相等,如果和自身比较相等,或者比较key和value都相等,则返回true
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;
}
}
//红黑树
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
TreeNode<k,v> parent; // 父节点
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);
}
//返回当前节点的根节点
final TreeNode<k,v> root() {
for (TreeNode<k,v> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
四:插入操作:put(K key, V value)和putval()
先上源码:
//put方法,具体实现在putval方法中
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//真正的实现方法;版本:1.8
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断哈希桶数组table是否为null或者长度是否为0,如果为null或者长度以为0,则调用resize()方法并把返回值给n;这里可以看出,第一次put操作的时候,就会发生扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据计算得到的hash值去计算得到下标i,判断table[i]处是否为null;如果是,则直接插入,不是则执行else后的操作
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//这里其实判断是否是键为key的键值对,如果是,则把这个node传给e;后面根据情况去更新原来的值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断table[i]是否为红黑树,如果是红黑树,则直接在树中插入键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果上面两种都不符合,则进入下面的判断
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
如果当前node.next为null,则说明是最后一个元素,插入元素即可。可以看到,这里才用的是尾插法;插入完成之后,判断链表长度是否大于8,大于则转为红黑树
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//遍历过程中,如果不是链尾元素,则会进入下面判断:判断是否是键为key的键值对,如果是,直接退出循环,后面根据情况看是否更改
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为null,说明已经存在键为key的键值对,如果onlyIfAbsent为false或者oldValue为null;则将旧值更新为新的传入的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断目前键值对数是否到了需要扩容的程度,如果到了就扩容(这里是++size)
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//这是jdk1.7的put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 键为 null 单独处理
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 确定桶下标
int i = indexFor(hash, table.length);
// 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 插入新键值对,这里做真正的链表插入,可以看到,这里才用的是头插法。
addEntry(hash, key, value, i);
return null;
}
重点:对比1.7和1.8可以发现有区别的是,对于链表的插入,1.7的是采用头插法,1.8的是尾插法
而且每次插入操作之后,会在putval方法最后面去判断需不需要扩容了。
五:重点:计算数组下标(table长度为什么要限制成2的n次幂?)
上源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
i = (n - 1) & hash
主要分为三个阶段:计算hashcode、高位运算与取模运算
这里通过key.hashCode()计算出key的哈希值,而计算得到的值是一个int类型的32位数,如果直接把这个当做是hash值,那么数组的长度就得是2的32次幂,那么直接GG,所以需要对这个做高位运算;高位运算其实就是把求得的哈希值h右移16位,再与原来的h做异或运算,而将高16位和低16位的信息"融合"到一起,也称为"扰动函数"。这样能保证hash值所有位的数值特征都保存下来而没有遗漏,从而使映射结果尽可能的松散。
最后,根据 n-1 做与操作的取模运算。
这里可以看出为什么HashMap要限制table的长度为2的n次幂,因为这样,n-1可以保证二进制展示形式是最后n位全为1,其它全为0 ,比如16,n-1取得的值是15:0000 0000 0000 0000 0000 0000 0000 1111。而最后的取模运算实际上就是进行"与"操作,在这种情况下就等同于截取hash二进制值得后四位数据作为下标,这样运算速度会更快。实际上这里还隐含一点就是,最后的取模运算原理上是对数组的length记性取余运算,只有在容量为2的次幂的时候h & (length - 1) == h % length,也就是说,这样能做到效果一样但是速度更快。
其实,table长度为什么要限制成2的n次幂,还有一个原因:
上面讲到,如果长度为2的n次幂,那么n-1可以保证二进制展示形式是最后n位全为1,其它全为1;这样还有另一个好处就是就少碰撞几率;
打个比方,假如目前数组长度为16,如果这个时候有两个node的hashcode分别是8和9,在最后的取模运算的时候,结果分别是1001和1000,这样不会发生碰撞,但是假如数组长度为15,hashcode还是8和9,在最后的取模运算的时候,它们是和1110去进行与操作,结果都是1000,这就产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,最后的取模运算由于n-1最后4位是1110,那么取模运算出来的结果最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,这样会造成数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率;
从这里也可以看出"扰动函数"的重要性了,如果高位不参与运算,那么高16位的hash特征几乎永远得不到展现,发生hash碰撞的几率就会增大,也会影响性能。
六:构造函数
原谅我这里才来讲构造函数,因为我觉得上面讲完这里再讲会比较合适一点,至少你可以知道为什么后面要进行位运算把threshold赋值为传入参数最接近的2的n次幂
//无参构造,将loadFactor负载因子赋值为默认的0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//声明初始容量构造,将负载因子赋值为默认的0.75,转而调用下面的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//声明初始容量以及负载因子构造,会去做一些判断
public HashMap(int initialCapacity, float loadFactor) {
//容量值不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//容量值不能大于最大容量值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//设置扩容阈值,这里其实只是把传入的容量值修改为最接近的2的n次幂,扩容时会修改为真正的扩容阈值,也就是数组长度*负载因子
this.threshold = tableSizeFor(initialCapacity);
}
//进行位运算,得到传入的参数最接近的2的N次幂的值
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//利用map来进行初始化
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
这里可以看到的一个点是,在上面我们讲了数组的长度必须是2的n次幂的原因,也知道了这个长度很重要,那么在进行初始化的时候,我们可以指定数组的长度,而为了避免我们指定的长度不是2的n次幂,HahsMap底层运用了tableSizeFor()方法进行强制修改,假如你指定长度是8,那么数组长度就是8,如果是9,那么进行位运算之后,实际上的数组长度会是16。顺便说一下tableSizeFor()方法,实际上它是把你传入的参数-1转化为2进制,然后把从左往右数第一个1后面的所有值都变成了1,最后再加1,比如传入是9(1001),那么减一就是8(1000),把从左往右数第一个1后面的所有值都变成了1也就是(1111),再加1就是(10000)也就是16。看不懂的话可以去了解一下位运算。
七:扩容方法
HashMap的扩容可以说是最重要的几个点之一了,先看源码:
//扩容方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//获取旧数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取旧扩容阈值
int oldThr = threshold;
//新长度,新扩容阈值
int newCap, newThr = 0;
//旧数组长度大于0的情况,也就是不是空数组插入的情况
if (oldCap > 0) {
//超过了最大容量,另threshold= Integer.MAX_VALUE;
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新容量为一定会扩容为旧容量2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//满足两个条件的话新扩容阈值为旧扩容阈值2倍,不满足则为0;
//关键在于 oldCap >= DEFAULT_INITIAL_CAPACITY);也就是如果旧的长度大于等于16的话,那么在扩容的时候
//只要进行旧阈值进行扩大两倍即可,不用去做乘法运算(capacity * load factor)
newThr = oldThr << 1; // double threshold
}
//如果旧数组长度小于等于0,并且旧扩容阈值大于0情况下,新的长度等于旧的扩容阈值,也就是带参数构造第一次put的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果旧数组长度小于等于0,并且旧扩容阈值大于0情况,也就是无参构造第一次put的情况
else { // zero initial threshold signifies using defaults
//新的长度为默认的16,新的扩容阈值为默认的16*0.75
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新扩容阈值为0情况下,也就是新的桶数组长度超过最大长度或者新的长度还没大于16的情况下
if (newThr == 0) {
// 确定好新的扩容阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 确定好新的扩容阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//扩容,复制
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//旧数组不为null,进行复制
if (oldTab != null) {
//遍历数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//设置为null,方便回收
oldTab[j] = null;
//不是链表,直接赋值,要重新计算桶下标
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//红黑树,转红黑树遍历处理
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//链表情况下:
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//遍历链表
do {
//next指向下一个node
next = e.next;
//位运算,作用是算出hash值的核心位置是0还是1,为0,后面桶下标不变
if ((e.hash & oldCap) == 0) {
//赋值链表第一个node,表头
if (loTail == null)
loHead = e;
//赋值后面的node
else
loTail.next = e;
//调整尾部元素
loTail = e;
}
//hash值的核心位置是1,新的桶下标为旧的桶下标加oldCap
else {
//赋值链表第一个node,表头
if (hiTail == null)
hiHead = e;
//赋值后面的node
else
hiTail.next = e;
//调整尾部元素
hiTail = e;
}
} while ((e = next) != null);
//根据loTail判断桶下标要不要修改
if (loTail != null) {
//最后指向null
loTail.next = null;
//直接把loHead放到与旧桶数组相同桶下标的新的桶数组位置处
newTab[j] = loHead;
}
//根据hiTail判断桶下标要不要修改
if (hiTail != null) {
//最后指向null
hiTail.next = null;
//修改桶下标,赋值
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上面注释中已经详细把各种情况下怎么扩容怎么赋值解释清楚了,唯一难点在于赋值的时候要根据位运算去确定新的桶下标,可以自己去做一下e.hash & oldCap这个运算,HashMap 使用了这样一个特殊的机制,可以提高重新计算桶下标的操作的效率。
举个例子:
假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:
capacity : 00010000
new capacity : 00100000
对于一个 Key,
它的哈希值如果在第 5 位上为 0,那么取模得到的结果和之前一样;
如果为 1,那么得到的结果为原来的结果 +16。
而e.hash & oldCap便是用来确定第五位的值是0还是1;
八:删除remove()方法
//元素删除
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//matchValue的作用是在其为true的情况下,仅在值相等的时候才删除;默认为flase
//movable的作用是在其为false的情况下,删除时不移动其他节点,默认为true
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//根据hash值找到数组对应桶下标的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//待删除元素是桶中首元素的情况:比较hash值和key是否相等,是则赋值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//待删除元素在桶中,但不是桶中首元素情况:
else if ((e = p.next) != null) {
//红黑树情况
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//链表情况下:
else {
//遍历链表
do {
//比较hash值和key是否相等,是则赋值
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
//p保存了待删除节点的上一个节点
p = e;
} while ((e = e.next) != null);
}
}
//matchValue为false,不用管值相不相等;直接删
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//走红黑树删除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//待删元素是链首元素,把子节点放进桶位
else if (node == p)
tab[index] = node.next;
//待删元素在链表中间,p是待删除元素的上一节点,直接指向待删除元素的下一节点
else
p.next = node.next;
++modCount;
//size-1
--size;
afterNodeRemoval(node);
return node;
}
}
//不存在待删除元素,返回null
return null;
}
其实从插入就可以猜得出它的删除和获取是借助key去计算hash值,然后确定下标,接着分情况比较再删除
九:containsValue() 和containsKey()
HashMap还提供了这两个判断存不存在某个key或者某个valus的方法,实现上其实和删除一个道理;当然,你也可以自己去遍历也可以实现
//判断是否存在某个value值
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
//遍历整个HashMap
for (int i = 0; i < tab.length; ++i) {
//遍历某个桶位所有元素
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
//判断
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
//判断是否存在某个key值,实现方法时getNode方法;实际上就是先通过hash值找到桶下标,然后去比较key值,详情看下面
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
十:get方法
HashMap提供通过Key获取对应valus的方法,源码如下:
//通过key获取value
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 实际上就是先通过hash值找到桶下标,然后去比较key值
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//通过hash值找到对应桶下标的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//比较第一个node的key和传入的key是否相等,是则直接返回
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);
}
}
//都没有则返回null
return null;
}
十一:关于红黑树
从上面往下看,可以知道我上面讲到关于红黑树的都是没有详细去讲解的,因为关于红黑树部分的实现太过于复杂,而如果直接这里继续去写的话没有先讲解红黑树的一些原理直接讲代码估计也很难理解;后续我应该会写一篇关于红黑树的详细的博客,围绕原理和实现去讲;如果现在想了解这部分的朋友可以先去看红黑树的原理,比如各种情况下的插入等等,这里可以给两边文章,我觉得写得很详细的(侵权请告知):
https://blog.csdn.net/v_JULY_v/article/details/6105630
然后再去看具体的代码实现,这里也推荐两篇博客:
【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析
不看的朋友可以先记住的一些点是:
1·会在插入的时候去判断要不要转换成红黑树;
2.在冲突的节点数已经达到8个的时候,会看是否需要改变冲突节点的存储结构,treeifyBin首先判断当前hashMap的长度,如果不足64,只进行resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树
3.当冲突节点小于6的时候,会解散红黑树
大概就这些
十二:加载因子
附上一张我偷来的图吧(侵权请告知):
十三:总结
1.查询时间最坏情况下是O(log n)
2.HashMap最多只允许一条记录的键为null,允许多条记录的值为null
3.HashMap非线程安全,如果需要满足线程安全,可以一个Collections的synchronizedMap方法使HashMap具有线程安全能力,或者使用ConcurrentHashMap。
顺便说一下Hashtable,Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
4.再进行初始化时,如果事先确定容量大小,那么指定集合初始值大小,避免多次扩容。