从Java源码的角度来分析HashMap与HashTable的区别
Posted a_woxinfeiyang_a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Java源码的角度来分析HashMap与HashTable的区别相关的知识,希望对你有一定的参考价值。
由于HashMap与HashTable都是用来存储Key-Value的键值对,所以经常拿来对比二者的区别,下面就从源码的角度来分析一下HashMap与HashTable的区别,
首先介绍一下两者的区别,然后再从源码分析。
HahMap与HahTable两者主要区别:
1、继承的父类不同
<span style="font-size:18px;">public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable</span>
<span style="font-size:18px;"><pre name="code" class="java">public class Hashtable<K, V> extends Dictionary<K, V> implements Map<K, V>, Cloneable, Serializable</span>
HashTable是一个古老的Map实现类,从JDK1.0就开始出现,继承自Dictionary,当时JDK还有没有出现Map这个接口;HashMap继承自AbstractMap这个抽象类,而AbstractMap这个抽象类实现了Map接口。
2、HashTable中的方法是同步的,在多线程程序中无需程序员自己去实现同步处理;而HashMap中的方法是非同步的,需要程序员自己去实现同步处理,常用的方法是使用Collections工具类中的synchronizedXxx()方法吧集合类包装成线程同步的集合。
3、HashTable中Key与Value都不允许出现NULL值,而HashMap中Key可以为
NULL,由于HashMap是Map中实现类,Map集合中要求每个Key唯一,所以HashMap
中之多只能出现一个Key为NULL的情况,但HashMap中可以出现多个Value为NULL
的情况。
4、遍历方式不同
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了
Enumeration的方式 。
然后接下来看看HashMap的源码。
HashMap中一些属性Field如下:
<span style="font-size:18px;"> /**
* Min capacity (other than zero) for a HashMap. Must be a power of two
* greater than 1 (and less than 1 << 30).
* HashMap的最小容量
*/
private static final int MINIMUM_CAPACITY = 4;
/**
* Max capacity for a HashMap. Must be a power of two >= MINIMUM_CAPACITY.
* HashMap的最大容量
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* An empty table shared by all zero-capacity maps (typically from default
* constructor). It is never written to, and replaced on first put. Its size
* is set to half the minimum, so that the first resize will create a
* minimum-sized table.
*
* HashMapEntry数组,HashMapEntry条目数组,HashMapEntry数组大小为MINIMUM_CAPACITY/2
*/
private static final Entry[] EMPTY_TABLE
= new HashMapEntry[MINIMUM_CAPACITY >>> 1];
/**
* The default load factor. Note that this implementation ignores the
* load factor, but cannot do away with it entirely because it's
* mentioned in the API.
*
* <p>Note that this constant has no impact on the behavior of the program,
* but it is emitted as part of the serialized form. The load factor of
* .75 is hardwired into the program, which uses cheap shifts in place of
* expensive division.
* 默认的负载因子
*/
static final float DEFAULT_LOAD_FACTOR = .75F;
/**
* The hash table. If this hash map contains a mapping for null, it is
* not represented this hash table.
* HashMap容器数组,用于存放HashMapEntry对象的数组
*/
transient HashMapEntry<K, V>[] table;
/**
* The entry representing the null key, or null if there's no such mapping.
* 空键值对应的HashMapEntry条目对象
*/
transient HashMapEntry<K, V> entryForNullKey;
/**
* The number of mappings in this hash map.
* HashMap中元素的数量(大小)
*/
transient int size;
/**
* Incremented by "structural modifications" to allow (best effort)
* detection of concurrent modification.
* 用于确保使用迭代器的时候,HashMap并未进行更改
*/
transient int modCount;
/**
* The table is rehashed when its size exceeds this threshold.
* The value of this field is generally .75 * capacity, except when
* the capacity is zero, as described in the EMPTY_TABLE declaration
* above.
* 负载因子(阈值)
*/
private transient int threshold;
// Views - lazily initialized
/**由K键组成的Set集合*/
private transient Set<K> keySet;
/**由K-Value组成的Set集合,Set集合中存放了Entry<K, V>元素*/
private transient Set<Entry<K, V>> entrySet;
private transient Collection<V> values;</span>
HashMap中是用HashMapEntry数组来存储Key-Value键值对的,HashMapEntry是HashMap中一个静态的内部类,实现了java.util.Map.Entry接口。在HashMap的属性Field中定义用于存放Key-Value键值对的HashMapEntry数组长度的默认的最小值和最大值。一个空的HashMapEntry数组EMPTY_TABLE以及HashMap容器数组tab,用于存放HashMapEntry对象的数组。默认的负载因子DEFAULT_LOAD_FACTOR,将其定义为0.75(3/4)。
HashMap的构造函数主要有以下几个:
HashMap() Constructs a new empty HashMap instance. |
HashMap(int capacity) Constructs a new HashMap instance with the specified capacity. |
HashMap(int capacity, float loadFactor) Constructs a new HashMap instance with the specified capacity and load factor. |
HashMap(Map<? extendsK,? extends V> map) Constructs a new HashMap instance containing the mappings from the specified map. |
<span style="font-size:18px;"> /**
* Constructs a new {@code HashMap} instance with the specified capacity.
*
* @param capacity
* the initial capacity of this hash map.
* @throws IllegalArgumentException
* when the capacity is less than zero.
*通过一个指定的容量来构造一个HashMap实例对象
*@param capacity:HashMap初始化容量
*
*/
public HashMap(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity: " + capacity);
}
if (capacity == 0) {
@SuppressWarnings("unchecked")
HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
table = tab;
threshold = -1; // Forces first put() to replace EMPTY_TABLE
return;
}
if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity > MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
capacity = Collections.roundUpToPowerOfTwo(capacity);
}
makeTable(capacity);
}</span>
该函数通过一个指定的容量值capacity来创建一个HashMapEntry数组对象,当capacity等于0时将空的HashMapEntry数组对象EMPTY_TABLE赋值给tab对象;如果capacity不为0时,当capacity小于容量最小值时将capacity调整为容量最小值,同理当capacity大于容量最大值时将capacity调整为容量最大值。最后调用makeTable来初始化创建tab数组。下面是makeTable的具体实现
<span style="font-size:18px;"> /**
* Allocate a table of the given capacity and set the threshold accordingly.
* 根据指定容量来分配table数组的大小,并设置负载因子(阈值)
* @param newCapacity must be a power of two
*
*/
private HashMapEntry<K, V>[] makeTable(int newCapacity) {
@SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
= (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
table = newTable;
threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
return newTable;
}</span>
可以发现在makeTable中是通过创建一个按指定容量大小新的HashMapEntry数组newTable,并将其newTable数组赋值给HashMap的属性File变量table,因此在HashMap中table数组才是真正的存放HashMapEntry的容器,即存放Key-Value条目的容器。在构造函数中将负载因子threshold设置为容量的3/4,当table数组中存放的HashMapEntry元素个数大于负载因子threshold时table数组将扩容。通过查看java.util.HashMap.put(K key, V value)源码即可发现这点:
<span style="font-size:18px;"> /**
* Maps the specified key to the specified value.
*
* @param key
* the key.
* @param value
* the value.
* @return the value of any previous mapping with the specified key or
* {@code null} if there was no such mapping.
*/
@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}</span>
HashMap.put方法用于向HashMap中插入HashMapEntry元素(即插入Key-Value键值对),当key为空的时候调用putValueForNullKey方法将value加入到key为NULL的HashMapEntry对象中。然后再检测当前传入的key是否在table数组中已经存在,如果存在就将新的value值替换掉key对应旧的value值oldValue并将oldValue返回;如果key在table数组中不存在则调用addNewEntry(K key, V value, int hash, int index) 方法将Value对象添加到table数组中。不过在加入到table数组之前要先进行判断,如果table数组元素数量size大于负载因子threshold则调用doubleCapacity来给table数组扩容。doubleCapacity方法源码如下:
<span style="font-size:18px;"> /**
* Doubles the capacity of the hash table. Existing entries are placed in
* the correct bucket on the enlarged table. If the current capacity is,
* MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
* will be new unless we were already at MAXIMUM_CAPACITY.
* 将数组容量扩大
*/
private HashMapEntry<K, V>[] doubleCapacity() {
/**旧的table数组*/
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
int newCapacity = oldCapacity * 2;
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
for (int j = 0; j < oldCapacity; j++) {
/*
* Rehash the bucket using the minimum number of field writes.
* This is the most subtle and delicate code in the class.
*/
HashMapEntry<K, V> e = oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash & oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j | nextHighBit] = n;
else
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}</span>
下面再看看java.util.HashMap.get(Object key)方法源码:
<span style="font-size:18px;"> /**
* Returns the value of the mapping with the specified key.
*
* @param key
* the key.
* @return the value of the mapping with the specified key, or {@code null}
* if no mapping for the specified key is found.
*/
public V get(Object key) {
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
return e == null ? null : e.value;
}
// Doug Lea's supplemental secondaryHash function (inlined).
// Replace with Collections.secondaryHash when the VM is fast enough (http://b/8290590).
int hash = key.hashCode();
hash ^= (hash >>> 20) ^ (hash >>> 12);
hash ^= (hash >>> 7) ^ (hash >>> 4);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}</span>
在HashMap.get方法可以发现HashMap在查找Key的过程中,如果是自定义类作为Key对象的话,通常要求重写equals方法和hashCode方法,并且equals方法和hashCode方法判断标准必须保持一致,否则将出现冲突。即在使用自定义类作为HashMap中Key对象,HashMap判断两个Key相等的标准是:两个Key通过equals()方法比较返回true,两个Key的hashCode值也必须相等。
与此同时,我们再看看HashTable的源码,可以发现HashTable与HashMap有一部分方式的底层实现差不多,在此就不详述,主要看下两者在底层实现中的一些区别。
java.util.Hashtable.put(K key, V value)源码:
<span style="font-size:18px;"> /**
* Associate the specified value with the specified key in this
* {@code Hashtable}. If the key already exists, the old value is replaced.
* The key and value cannot be null.
*
* @param key
* the key to add.
* @param value
* the value to add.
* @return the old value associated with the specified key, or {@code null}
* if the key did not exist.
* @see #elements
* @see #get
* @see #keys
* @see java.lang.Object#equals
*/
public synchronized V put(K key, V value) {
if (key == null) {
throw new NullPointerException("key == null");
} else if (value == null) {
throw new NullPointerException("value == null");
}
int hash = Collections.secondaryHash(key);
HashtableEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
HashtableEntry<K, V> first = tab[index];
for (HashtableEntry<K, V> e = first; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for key is present; create one
modCount++;
if (size++ > threshold) {
rehash(); // Does nothing!!
tab = doubleCapacity();
index = hash & (tab.length - 1);
first = tab[index];
}
tab[index] = new HashtableEntry<K, V>(key, value, hash, first);
return null;
}</span>
HashTable.put方法与HashMap.put方法基本实现逻辑相同,主要区别是HashTable.put方法被synchronized关键字修饰,因此HashTable是线程安全的。同时如果Key为NULL的话,在HashTable.put方法中将会报异常,所以在HashTable中Key不能为NULL,而我们在前面的分析中可以发现HashMap.put方法中Key是可以为NULL的。
下面给出java.util.Hashtable.get(Object key)源码,亦可以发现相似结果,故就不具体分析。
<span style="font-size:18px;"> /**
* Returns the value associated with the specified key in this
* {@code Hashtable}.
*
* @param key
* the key of the value returned.
* @return the value associated with the specified key, or {@code null} if
* the specified key does not exist.
* @see #put
*/
public synchronized V get(Object key) {
int hash = Collections.secondaryHash(key);
HashtableEntry<K, V>[] tab = table;
for (HashtableEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}</span>
HashMap源码(JDK1.7,含注释) HashTable源码(JDK1.7,含注释)
以上是关于从Java源码的角度来分析HashMap与HashTable的区别的主要内容,如果未能解决你的问题,请参考以下文章
Android Gems — Java源码分析之HashMap和SparseArray
Android Gems — Java源码分析之HashMap和SparseArray