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

从源码的角度分析Hashtable和HashMap的区别

2016-11-15 23:57 501 查看

引言

面试中我们经常被问到这样的问题:”请说说Hashtable和HashMap的区别?”。

通过搜索引擎,我们能轻易找到 许多答案。这些答案详细比较了两者的不同。但是往往停留在”知其然“的阶段,只是用文字列出了两者的不同。因此,等过一些日子我们再来回顾这个问题 时,似乎一切又归零了(至少对于记忆不好的我来说是这样的)。今天,我打算从源码的角度来分析分析它们的区别,做到不仅”知其然“,更能”知其所以然“。 有兴趣的话,不妨随我一道,来看看Hashtable和HashMap的世界是什么样的。

注意:以下源码来自Oracle JDK1.8。如果您在Android
SdK中查看源码发现与文中所列源码不一致,请不要惊慌。因为android SDK中JDK源码采用Apache的开源项目Harmony。

数据结构

为了便于比较,我们将两者放在一起:首先,我们各实例化一个Hashtable和HashMap:

HashMap hashMap = new HashMap();

Hashtable hashtable = new Hashtable();接着,进入它们的构造方法,让我们来看看,里面都有什么:
//hashMap构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//DEFAULT_LOAD_FACTOR的定义
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//hashmap中table申明
transient Node<K,V>[] table;

//hashtable构造函数
public Hashtable() {
this(11, 0.75f);
}
//进入this方法
public Hashtable(int initialCapacity, float loadFactor) {
//省略异常判断代码...
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}这里有几个变量解释下:

loadFactor:加载因子。两种数据结构中都有这个变量,默认都为0.75f;

threshold:阀值。当Hashtable扩容时,先判断当前长度是否超过这个threshold值,来决定是否扩容,由此可见,创建一个Hashtable对象时,初始化了loadFactor,table长度11,阀值为11*0.75 = 8(取整)。而HashMap扩容时是否也有阀值呢?答案是肯定的,只不过它的初始化并不在这里,我们下面会介绍到;

当然你也可以在创建对象的时候指定它们的加载因子和阀值,如;
Hashtable table = new Hashtable(20,0.8f);
HashMap map = new HashMap(20,0.8f);

table:HashMap和Hashtable中都有一个数组table,HashMap中table类型为Node泛型。Hashtable中table类型为Entry泛型。虽然它们名称不同,但都有相同的数据结构,并且都实现了Map.Entry接口,它们内部都有四个属性,分别是,hash,key,value,next:
//HashMap-Node属性
final int hash; //用于判断检验的key是否相同
final K key; //存入的key
V value; //存入的value
Node<K,V> next; //指针由此可见,Node为HashMap中最基本数据结构,Entry为Hashtable中最基本数据结构。我们调用put方法时添加的键值对都是存在table数组中的。

put方法

我们再来看看它们在put方法上的区别:

HashMap的put方法

首先来分析HashMap的put方法

//HashMap put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

接着进入putVal方法,代码如下:
//HashMap
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//...
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

从第5行代码中可知,putVal方法先判断table是否为null或者长度是否为0,当我们使用构造函数创建HashMap对象时,table并没有初始化,所以table为空,条件成立(也就是第一次调用put方法)进入resize()方法:
//HashMap resize()方法
//resize方法的描述:Initializes or doubles table size.
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//...
threshold = newThr;
//.fangfa..
return newTab;
}从描述中,我们得到信息,resize方法承担了两个功能:

1. 初始化

2. 2倍扩容

现在我们就来仔细看看这个实现过程:  
- 当第一次调用put方法时,table为null,相应的oldtab为null。根据第5行代码得到oldCap=0;随即,代码进入第20行,对newCap和newThr进行初始化:DEFAULT_INITIAL_CAPACITY=16;阀值newThr为16*0.75=12

- 如果table不为空,则进入第9行代码执行,先判断table数组长度是否达到了上限值,如果达到了,则将原table返回,也就是此次put的数据并不会添加到集合中去。如果符合扩容条件则执行:newCap = oldCap << 1进行扩容,也就是扩容为原来的两倍。相应的阀值也扩为原来两倍:newThr = oldThr << 1

Hashtable的put方法:

public synchronized V put(K key, V value) {
// table的value值不可以为null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
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)) {
V old = entry.value;
entry.value = value;
return old;
}
}

addEntry(hash, key, value, index);
return null;
}


很明显得看到Hashtable的put方法是由synchronized修饰的,也就是它是线程安全的。这恐怕是它与HashMap的put方法最大的区别了。

在for循环中根据hash值查找key在原来集合中是否已经存在,如果存在,则替换value值。如果不存在则使用addEntry()方法将新的键值对加入。

在插入新值前,先做长度校验,判断长度是否溢出(大于阀值)。如果溢出,则进行扩容,扩容机制:int newCapacity = (oldCapacity << 1) + 1。

总结

创建对象时,Hashtable初始化了加载因子(0.75f)、数组长度(11)、阀值、;而HashMap只初始化了加载因子(0.75f),它在第一次put时初始化数组长度(16)
HashMap内部的存储结构是Node,而Hashtable内部的存储结构Entry。虽然名称不同,但它们有相同的数据结构。并且它们都实现了Map.Entry接口;
Hashtable的put方法是线程安全的,而HashMap的put方法不是;
扩容时Hashtable长度变为原来2倍+1;而HashMap长度为原来2倍;
使用put方法时table的value值不可以为null,Map可以。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java hashmap hashtable