【1】HashMap和HashTable异同

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。

主要的区别有:线程安全性–同步(synchronization),null,类继承,初始化及扩容、迭代器以及速度。

① 线程安全

HashMap是非synchronized,而Hashtable是synchronized。Hashtable 所有的元素操作都是 synchronized 修饰的,而 HashMap 并没有。

这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable。而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

② NULL

Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。

HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键(key)和值(value),而Hashtable则不行)。

为什么 Hashtable 是不允许 KEY 和 VALUE 为 null, 而 HashMap 则可以?

Hashtable put 方法逻辑:

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();
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;
}

HashMap hash 方法逻辑:

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

可以看出 Hashtable中如果key/ value 为 null 会直接抛出空指针异常,而 HashMap 的逻辑对 null 作了特殊处理。


③ 类继承

Hashtable如下:

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

HashMap如下:

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

可以看出两者继承的类不一样,Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。

Dictionary 是 JDK 1.0 添加的,很古老的一个字典类。其javadoc如下所示:

Dictionary是键值对映射类的抽象父类,例如Hashtable。

每一个键和值都是一个object。每个键最多关联一个值。

给定一个Dictionary对象和一个键,关联的元素可以被找到。

任何非 null 对象都可以用作键和值。

通常,这个类的实现应该使用equals方法来确定两个键是否相同

注意:这个类已经过时。新的实现应该实现Map接口,而不是继承这个类

④ 初始化和容量扩容

HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。

Hashtable构造函数如下:

/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
*/
public Hashtable() {
this(11, 0.75f);
}
/**
* Constructs a new hashtable with the same mappings as the given
* Map. The hashtable is created with an initial capacity sufficient to
* hold the mappings in the given Map and a default load factor (0.75).
*
* @param t the map whose mappings are to be placed in this map.
* @throws NullPointerException if the specified map is null.
* @since 1.2
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}

HashMap 初始化容量和负载因子:

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。


⑤ 迭代器

另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的Enumerator迭代器不是fail-fast的。

所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM的实现。这同样也是Enumeration和Iterator的区别。

参考博文:浅谈从fail-fast机制到CopyOnWriteArrayList使用

⑥ 速度与性能

由于Hashtable是线程安全的也是synchronized,所以通常它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。

如果要线程安全又要保证性能,建议使用 JUC 包下的 ConcurrentHashMap。


【2】相关重要术语

① sychronized意味着在一次仅有一个线程能够更改Hashtable。

就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。HashTable将底层整个hash表都锁了起来,故而在多线程的环境下,使用HashTable虽然安全但是性能十分低下。

另外需要注意的是HashTable并非绝对安全,比如在复合操作的情况下,如下所示:

if(!table.contains(XX)){
//如果在这中间,被其他线程拿到了锁呢?
table.put(XX);
}

② fail-fast机制

快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。

结构上的更改指的是类似于删除或者插入一个元素,这样会影响到map的结构。


③ 我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步:

Map m = Collections.synchronizeMap(hashMap);
// 或者直接使用ConcurrentHashMap(JDK1.5);