【Java源码解读】Hashtable

源码来源:OpenJDK 10

 

简述

Hashtable实现了Map接口;Key和Value都不允许是null

Hashtable的public方法都是用synchronized修饰,以达到线程安全的效果。

一般较少用Hashtable。

当没有线程安全的需求时,推荐使用HashMap;

当需要线程安全时,推荐使用并发性能更好的ConcurrentHashMap

 

其它:Hashtable用的是近乎标准的拉链法解决冲突。其代码逻辑与HashMap相比要浅显得多。可作为哈希表原理初学者阅读模仿的对象。

 

关于性能

与HashMap相同,initial capacity(初始容量)和load factor(负载因子)这2个参数会对Hashtable产生较大影响。

“容量”表示Hashtable中哈希桶的数量。

哈希值相同的键值对(哈希冲突)会被放到同一个哈希桶中(单向链表)。

查找这写哈希值相同的键值对时会按链表顺序逐个比对。

负载因子表示哈希桶数量占容量的最大比值;

当超过该阀值时Hashtable将进行扩容。

负载因子的默认值是0.75。这是一个平衡查找查找速度和空间占用比较好的值。

负载因子更高时,Hashtable的空间占用会下降,但键值对查找速度会增加(get和put操作的耗时都会增加)。

通过“初始容量”参数合理设置初始容量,可以减少Hashtable后续自动扩容开销,从而提高性能。

 

构造方法

Hashtable(int initialCapacity, float loadFactor)

创建一个空的Hashtable,并指定初始容量和负载因子

与HashMap不同的是,Hashtable不要求容量是2的幂,且会在该构造方法中初始化内部数组,而不是等到添加第一个键值对时再初始化

 

if (initialCapacity==0)
    initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
// 这是扩容临界值的计算方法。当键值对数量达到该值,且需要新增键值对时,就需要扩容
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);

 

Hashtable(int initialCapacity)

创建一个空Hashtable,并指定初始容量

该方法会指定负载因子为0.75

 

Hashtable()

创建一个空Hashtable

初始化容量为11,负载因子为0.75

 

Hashtable(Map<? extends K, ? extends V> t)

创建一个Hashtable,并将指定map中的键值对添加到该table中

初始容量为max(2*t.size, 11),负载因子为0.75

该方法会通过putAll方法添加指定map中的键值对

 

关键方法

addEntry(int hash, K key, V value, int index)

添加一个键值对

如果当前键值对数量达到了扩容的临界值,则先调用rehash()方法进行扩容。

添加方法就是用指定值创建一个键值对节点,将目标哈希桶处原节点作为该新节点的后继节点,最后将新节点存到原节点索引处

 

rehash()

对Hashtable进行扩容

具体实现方法:

先计算新容量,并创建一个新数组用于存放键值对;该新数组的容量就是计算得到的新容量;

然后遍历原数组中各哈希桶内的各节点,将其加入到新数组中。

添加方法与addEntry内部实现机制完全相同,也是将新节点直接放到数组目标索引处,并将原节点作为新节点后继节点

新容量计算方法:

 

// overflow-conscious code
// 在原容量基础上翻倍,再加1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
    if (oldCapacity == MAX_ARRAY_SIZE)
        // Keep running with MAX_ARRAY_SIZE buckets
        return;
    newCapacity = MAX_ARRAY_SIZE;
}

 

其它方法

size()

获取键值对数量

 

isEmpty()

判断Hashtable是否为空

内部就是判断size是否等于0

 

keys()

获得一个所有key的迭代器

 

elements()

获得一个所有value的迭代器

keys()和elements()所返回迭代器的实现方法很有意思。

它们是同一套代码。

迭代器类内部用一个字段type表示该迭代器是对哪部分数据的迭代(key,value或整个键值对)。

当需要对外提供数据时,根据type返回相应的数据。

 

return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);

 

contains(Object value)

检查Hashtable是否包含有指定的value

具体实现就是遍历每个哈希桶中的每个键值对节点,调用其键值对value的equals方法检查其是否与指定的value相等,如果相等就返回true

 

containsValue(Object value)

与contains(Object value)完全相同

 

containsKey(Object key)

检查Hashtable是否包含指定的key

具体实现就是先根据指定key的哈希值计算出所属哈希桶的索引,再遍历该哈希桶中所有节点的key是否与指定key相等

与HashMap不同的是,键值对的哈希值(字段hash)就是key的hashCode()方法所返回的值;哈希桶索引的计算方式也不同。

注意:键值对的hash字段与其hashCode()方法返回的值不同;

hash字段就是key的hashCode()方法返回值,

但Entry.hashCode()方法返回的值是key的hash与value的hash异或运算(^)所得值

为了加快比对,会先检查节点哈希值是否与指定key的哈希值相等;

如果不相等,则直接认为两个key不相等;

否则会调用键值对key的equals方法进行比对

 

// 确定哈希桶位置

int hash = key.hashCode();
// 通过这个与运算可以将负数hash转换为正数(把首位强制置为0)
// 再通过取模运算(%)确定哈希桶位置
int index = (hash & 0x7FFFFFFF) % tab.length;
// 检查键值对的key是否与传入的key相等
if ((e.hash == hash) && e.key.equals(key)) {
    return true;
}

 

get(Object key)

获取指定key对应的value

具体实现类似containsKey(Object key),也是需要找到key相等的键值对。不同的是该方法最终返回键值对的value

 

put(K key, V value)

添加一个键值对。

如果已存在指定key的键值对,则原value被替换为新value,并返回原value;

如果不存在则添加新键值对后返回null

 

remove(Object key)

移除指定key对应的键值对

如果目标键值对存在,则返回其value,否则返回null

具体实现方法是先根据指定的key找到目标键值对(类似get方法);

再通过标准的单向链表操作将目标键值对节点从其所在哈希桶链表中移除;

如果目标键值对节点是所在链表的第一个节点,则直接将其后继节点存放到内部数组的相应索引处,以替代目标节点

 

putAll(Map<? extends K, ? extends V> t)

将指定map中的键值对添加到Hashtable中

具体实现就是通过指定map的entrySet()方法遍历所有键值对,并调用put方法将其添加到Hashtable中

 

clear()

清空Hashtable

具体实现是将内部数组的每个索引处引用置为null

 

clone()

(浅拷贝)克隆一个Hashtable

 

toString()

获得Hashtable实例的字符串表示形式

用‘key=value’的形式展示各键值对。

如果key或value是Hashtable实例本身,则用’(this Map)’表示key或value;

否则就用它们自身的toString方法返回结果表示。

这样的区别对待可防止陷入死循环

 

keySet()

获取key的Set视图

对该视图的修改会影响到Hashtable实例本身。如,KeySet.clear()方法内部就是调用Hashtable实例的clear()方法

虽然该方法本身并未用synchronized修饰,但它内部用Collections.synchronizedSet方法对KeySet进行了包装,且目标互斥元就是Hashtable实例本身。所以与synchronized修饰是等价的

 

entrySet()

获取键值对的Set视图

与keySet()方法原理相同,对该视图的修改也会影响到Hashtable实例本身。EntrySet.clear()方法内部也是调用Hashtable的clear()方法

该视图的同步实现机制也与keySet()相同

 

values()

获取value的Collection视图

该视图与keySet()和entrySet()实现原理相同。对该视图的修改也会影响到Hashtable实例本身。该视图的同步实现机制也类似,是通过Collections.synchronizedCollection方法进行包装。

此外,keySet()、entrySet()、values()这几个方法返回的视图的迭代器(Iterator)实现方式与keys()和elements()方法返回的迭代器(Enumeration)是相同的,

它们使用了同一份代码。

区别是,Enumeration不支持移除元素,Iterator则支持。这是通过其内的一个标记字段区分的。如果调用Enumeration的remove方法,会检测到字段值不满足要求,然后抛出UnsupportedOperationException

 

equals(Object o)

检测Hashtable实例是否与指定的对象相等

检测步骤如下:

  1. 检查两者是否为同一个对象
  2. 检查指定对象是否为Map
  3. 检查两者的size(即键值对数量)是否相同
  4. 通过遍历Hashtable实例的entrySet()方法所返回的键值对,检查指定对象Map是否也包含相同的键值对(key和value都相等)。
    • 其中对value是否相等的判断与HashMap中惯用的方式相同,即如果Hashtable中的value为null,则直接判断对方的value是否等于null;否则,通过value的equals方法检查两个value是否相等

 

hashCode()

获得Hashtable实例的哈希码

该方法就是返回各键值对的hashCode()方法返回值的累加结果。

因为Entry.hashCode()方法内部会调用value的hashCode()方法,所以为了避免Hashtable自引用(Hashtable实例本身就是其中value之一)导致死循环,该方法会先将负载因子字段(loadFactor)置为相反数(成为负数),用以表示当前正在执行hashCode()方法。

此时如果有再次进入该方法,会检测到loadFactor为负数而直接返回。

当所有哈希码累加完成,再将loadFactor置为相反数(还原为正数)

 

getOrDefault(Object key, V defaultValue)

获取指定key对应的value;如果目标键值对不存在,则返回指定的默认值

具体实现就是直接调用get(Object key)方法获得value;如果value为null就返回指定的默认值,否则返回value。

因为Hashtable不允许value为null,所以可以用此方式判断是否存在键值对

put(K key, V value)方法会针对value为null的情况直接抛异常NullPointerException

 

forEach(BiConsumer<? super K, ? super V> action)

将每个键值对的key和value作为指定操作方法的参数执行一次

 

replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

将每个键值对的value替换为指定操作方法以键值对的key和value为参数的返回结果

如果指定操作方法返回的结果是null,将抛异常NullPointerException

 

putIfAbsent(K key, V value)

将指定的key和value加入到Hashtable中;

如果已存在该key对应的键值对,则返回原value,不会用新value替换;但是如果原value是null,还是会替换为新value;

如果key对应的键值对不存在,则新加键值对,并返回null

 

remove(Object key, Object value)

将Hashtable中key和value与指定值相等的键值对移除,并返回true;如果没有符合条件的键值对,则返回false

 

replace(K key, V oldValue, V newValue)

将Hashtable中key和value与指定值(key, oldValue)相等的键值对的value替换为指定的新value(newValue),并返回true;如果没有符合条件的键值对,则返回false

 

replace(K key, V value)

将Hashtable中key与指定值相等的键值对的value替换为指定的新value,并返回true;如果没有符合条件的键值对,则返回false

 

复杂又较少用到的方法

computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)

添加一个键值对,其中key就是参数指定的值,value是指定的mappingFunction以key为参数计算的结果,最终返回该计算所得的value;

如果已存在key相等的键值对,则直接返回该键值对的value

 

computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

将Hashtable中key与指定值相等的键值对的value替换为指定remappingFunction以key和原value为参数的计算结果;

如果新value为null,则移除原键值对,并返回null;

如果不存在符合条件的键值对,则直接返回null

 

compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

将Hashtable中key与指定值相等的键值对的value替换为指定remappingFunction以key和原value为参数的计算结果,并返回新value;

如果新value为null,则移除原键值对,并返回null;

如果不存在符合条件的键值对,且新value不为null,则添加键值对(key和新value)

 

merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)

将Hashtable中key与指定值相等的键值对的value替换为指定remappingFunction以原value和指定value为参数的计算结果,并返回新value;

如果新value为null,则移除原键值对,并返回null;

如果不存在符合条件的键值对,且指定的value不为null,则添加键值对(key和指定的value)

猜你喜欢

转载自pre.iteye.com/blog/2429868