HashMap 和 Hashtable 是Java中用于存储键值对的集合类,但它们之间有一些关键区别。
-
线程安全性:
- HashMap:不是线程安全的。如果多个线程同时访问一个
HashMap
而没有正确的同步,会导致数据不一致。在源码中,HashMap的主要方法(如put
,get
)都没有使用synchronized
关键字。 - Hashtable:是线程安全的,所有的关键方法都使用了
synchronized
关键字来确保线程安全。这可以在源码中看到每个方法都是同步的。多个线程可以安全地共享一个Hashtable
实例,但由于使用了同步机制,其性能比HashMap
差。
- HashMap:不是线程安全的。如果多个线程同时访问一个
-
性能:
- HashMap:由于不需要同步,其性能通常比
Hashtable
好。 - Hashtable:因为每个方法都是同步的,所以开销较大,性能相对较低。
- HashMap:由于不需要同步,其性能通常比
-
允许null值:
- HashMap:允许一个
null
键和多个null
值。在源码中,put
方法会单独处理key
为null
的情况。 - Hashtable:不允许
null
键或null
值。在源码中会直接通过Objects.requireNonNull
方法进行校验,抛出NullPointerException
。
- HashMap:允许一个
-
继承关系:
- HashMap:继承自
AbstractMap
。 - Hashtable:继承自
Dictionary
,历史上较久。
- HashMap:继承自
-
初始容量和增长因子:
- HashMap:可以指定初始容量和加载因子。HashMap 的默认初始容量是16,加载因子是0.75。扩容时,容量会翻倍。
- Hashtable:默认初始容量是
11
,加载因子是0.75
。扩容是直接扩大一倍加一。
-
数据结构:
- HashMap 和 Hashtable 都是基于哈希表的数据结构实现的。两者内部使用数组加链表的方式来处理哈希冲突。
源码分析:
HashMap 中的 put
方法(简化版):
public V put(K key, V value) {
// 处理key为null的情况
if (key == null)
return putForNullKey(value);
// 计算key的hash值,确定存储位置
int hash = hash(key);
int i = indexFor(hash, table.length);
// 遍历链表,检查是否存在相同的key
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// 如果不存在相同的key,则新增一个节点
addEntry(hash, key, value, i);
return null;
}
在 HashMap
中,putForNullKey
方法专门用于处理键为 null
的情况。由于 HashMap
允许使用 null
作为键,它需要特别处理这种情况,以避免在哈希计算和索引时出现问题。
以下是 putForNullKey
方法的基本实现逻辑(源码简化):
private V putForNullKey(V value) {
// 遍历链表,寻找key为null的节点
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
// 如果找到,则更新值
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// 如果没有找到,则创建新节点,并将其放在链表的头部
addEntry(0, null, value, 0);
return null;
}
说明:
-
存储位置:
- 当键为
null
时,HashMap
将其总是存储在数组的第一个位置,即table[0]
。这是因为null
键的哈希值固定为0。
- 当键为
-
遍历链表:
putForNullKey
会遍历位于table[0]
的链表,检查是否已有节点的键为null
。
-
更新或添加:
- 如果找到已有的键为
null
的节点,它将更新该节点的值。 - 如果没有找到,则调用
addEntry
方法在table[0]
位置添加一个新的节点。
- 如果找到已有的键为
小结:
这个方法说明了 HashMap
对 null
键的特殊处理机制。通过将 null
键的所有条目存储在数组的第一个位置,HashMap
简化了对 null
键的索引和管理。
Hashtable 中的 put
方法(简化版):
public synchronized V put(K key, V value) {
// 不允许null键
if (key == null || value == null) {
throw new NullPointerException();
}
// 计算key的hash值,确定存储位置
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % table.length;
// 遍历链表,检查是否存在相同的key
for (Entry<K,V> e = table[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
// 如果不存在相同的key,则新增一个节点
addEntry(hash, key, value, index);
return null;
}
结论:
- 在源码中可以看到,
HashMap
的关键方法没有使用同步机制,而Hashtable
中几乎每个方法都有synchronized
修饰。这是两者性能差异的根本原因。 - 由于
Hashtable
使用了更粗粒度的同步机制,推荐在需要并发操作时使用ConcurrentHashMap
代替。
代码说明:
HashMap应用场景: 适用于单线程环境或不要求线程安全的场景。例如,缓存数据在单线程应用中的使用。
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
System.out.println("HashMap: " + map);
// 允许null键和值
map.put(null, "nullValue");
System.out.println("HashMap with null key: " + map);
}
}
Hashtable应用场景: 适用于简单的多线程环境,或者不介意使用较老旧类的遗留系统。
import java.util.Hashtable;
public class HashtableExample {
public static void main(String[] args) {
Hashtable<String, String> table = new Hashtable<>();
table.put("keyA", "valueA");
table.put("keyB", "valueB");
table.put("keyC", "valueC");
System.out.println("Hashtable: " + table);
// 不允许null键和值,下面的代码会抛出NullPointerException
// table.put(null, "nullValue");
}
}
如果你在开发一个现代的Java应用,通常会选择使用HashMap,因为它性能更好并且更灵活。如果需要线程安全的Map,建议使用ConcurrentHashMap
而不是Hashtable
,因为ConcurrentHashMap
提供更好的并发性能。