您的位置:首页 > 编程语言 > Java开发

集合源码学习(十):HashTable(Java8)与HashMap比较

2017-10-20 19:02 686 查看

什么是HashTable

一句话介绍,一个里面方法大部分都是线程安全的集合,类似于HashMap。也是通过一个数组,利用hash函数,如果冲突就用链表进行连接。

具体如下图:



当然这只是一句话简短介绍,面试的时候,经常会被问到,

HashTable和HashMap有什么区别?

经常的回答就是,

HashTable是线程安全,里面方法大部分是synchronized,而HashMap不是;

HashMap里面key和value可以为null,而HashTable中不允许为null;

在没细致分析集合源码之前,我也是这样认为的,在面试中也是这样回答的(逃

看完两个源码后,发现自己了解的真实太浅了。

有兴趣可以看我前一篇分析HashMap的文章:

集合源码学习(七):HashMap(Java8)

首先看HashTable的定义:

public class HashTable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable


继承自Dictionary,Dictionary是一个抽象父类,功能和Map一样,但已经是个过时的类了,官方推荐用实现Map接口来取代。

并且实现了Map接口,以及Cloneable,Serializable接口。

接下来的文章,我就以和HashMap比较为主进行讲解。

null值问题

HashTable键(key)和值(value)均不能为null。

先看put方法:

/**
* 将key和value加入到map中,明显标明,
* value不能为null。如果key为null,则会包nullPointer
*/
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
//很直接的利用hashcode去除table.length,然后取长度。
int index = (hash & 0x7FFFFFFF) % tab.length;

@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//链表后面有数据
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
//hash相同且equals,那么就连在后面,是用链表的方式。
V old = entry.value;
entry.value = value;
return old;
}
}
//第一个,链表后面没有数据。
addEntry(hash, key, value, index);
return null;
}


关于value,明显有if判断,不能为null,

如果key为null,则也直接在计算hashCode的时候就会报空指针。

计算table数组索引值方法

相信大家应该还记得,java8中HashMap更改了hash()方法:

HashMap中:

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


接着计算数组索引:

tab[i = (n - 1) & hash]


而在HashTable中:

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;


很直接的利用hashcode去除table.length,然后取长度。

HashTable是线程安全

因为HashTable中大部分方法都是加了synchronized关键字,所以同一时刻,只能有一个线程进入其方法故是线程安全的,这里就不多说。

initialCapacity和loadFactor

有些读者可能还不懂这两个参数的意思,initialCapacity是初始容量,而loadFactor是加载因子,当当前已用大小size=table.length*loadFactory时,就会进行扩容操作。具体可以看我的HashMap分析篇。

说重点,没有指定初始容量时

HashMap中:initialCapacity=16,loadFactory=0.75

HashTable中:initialCapacity=11,loadFactory=0.75

只有链表方式解决冲突

Java8,HashMap中,当出现冲突时,

如果冲突数量小于8,则是以链表方式解决冲突。

而当冲突大于等于8时,就会将冲突的Entry转换为红黑树进行存储。

而又当数量小于6时,则又转化为链表存储。

而在HashTable中,

则都是以链表方式存储。

扩容的额度

Java8中,

HashMap:

HashMap一旦扩容,都是扩展到2的倍数,因为这样有利于计算数组索引值,即和计算数组索引结合起来。就算指定大小,也是会选择一个大于大小的2的倍数。

HashTable:

一次性扩展为oldCapacity*2+1。

先看代码:

/**
* 一次扩展是,old*2+1
*/
@SuppressWarnings("unchecked")
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;

// overflow-conscious code
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;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

modCount++;
//新的threshold值。取newCapacity*loadFactor的小值。
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;

for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}


先扩展,再把旧数组里面元素一个一个加到新的里面。

注意,这里取hash不是e.hash,而仍然是key.hashCode计算保留下来的值。

如果哪里分析有问题或者哪些地方没有分析到的,可以在下方留言。

另附上:

集合源码学习(七):HashMap(Java8)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: