在面试的过程中经常会被问到一个问题,HashMap和HashTable有什么不同?我就大概的整理了一些,与大家分享。
一、相同点
- 都实现了Map接口,底层都是采用的哈希表(数组 + 单向链表,在JDK1.8以后又加入了红黑树。即当链表长度大于8时,单向链表转换成红黑树--提高查询速度)。
- 都属于双列集合,存储的都是key-value(键值对)形式的数据。
3.双列集合的存储结构(哈希表)
双列集合存储的都是键值对形式的数据,每个键值对都是一个Entry类型的(Map接口中的内部接口Entry<K,V>),实际上Map集合存储的都是Entry类型的数据。
public interface Map<K,V> {
interface Entry<K,V> {
// 内容省略
}
}
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
}
根据源码可以知道Entry的实现类Node(存储的键值对)有四个属性:
-
K key:存储的key。
-
V value:存储的value。
-
int hash:key的哈希值。
-
Entry entry:指向当前Entry对象在链表中的下一个Entry对象,可以为null,null表示当前Entry对象在链表的尾部。
哈希表存储过程
- 创建Map集合:集合内部初始化一个Entry类型的数组(HashMap初始长度为16,HashTable初始长度为11) ---> 实际上存储的是Node类型。
- 存储数据:存入哈希结构的集合的元素必须重写equals()和hashCode()方法,否则无法保证存入数据的不可重复性。
- 根据存入key的哈希值计算当前Entry对象的在数组中的存储索引。
- 判断计算出来的索引上是否存在元素。
- 如果不存在元素,那么将当前Entry对象存入数组中。
- 如果存在元素,那么使用equals()方法判断当前Entry对象与原始的Entry对象是否相同。
- 如果相同,放弃存储(Map集合的key不可重复)。
- 如果不同,判断当前索引位置是红黑树还是链表。
- 红黑树:插入红黑树中存储。
- 链表:判断插入Entry对象后当前链表的长度是否达到了指定长度(7)。
- 达到了指定长度:转化成红黑树存储。
- 没有达到指定长度:插入链表的尾部。
补充:
- 由于对象的哈希值可能会发生重复(哈希冲突),所以为了解决哈希冲突的问题,在数组的基础之上又引入了链表。将哈希值相同但不是重复的元素存储在同一个数据索引上的链表中。
- 由于链表没有索引,同时在内存中的地址值不是连续的,链表的这种特性导致其查询速率比较低。当哈希值相同的非重复元素较多时,就会导致链表上的查询速度大大降低。为了提高查询速度,当链表的长度大于指定长度后会将链表转换成查询速度更高效的红黑树结构。
二、不同点
- 继承体系不同
- 键值对的null值问题
- 数组扩容机制不同
- 线程安全问题
- 迭代器不同
- 继承体系不同
根据两者的继承体系可以知道,两者实现的接口都相同,却别就在于继承的类不同。HahsMap继承了AbstractMap类,HashTable继承了Dictionary类(已经不再使用)。继承体系的不同导致两者之间的方法有少许差别,HashTable比HashMap多了两个方法:一个是来自Dictionary类的elements()方法,另一个是contains()方法
2、键值对的null值问题
(1)、HashMap的key和value都可以为null
HashMap的key为null是由于对获取哈希值的步骤进行了特殊处理,当key为null时,哈希值返回0,不为null时再去调用hashCode()方法获取哈希值。value可以为null,是由于在put()方法内,直接将value进行了存储,没有进行判断。
// put方法
public V put(K key, V value) {
// 存储数据时,使用key获取哈希值时做了处理
return putVal(hash(key), key, value, false, true);
}
// hash方法
static final int hash(Object key) {
int h;
// 值null的key认为其哈希值为0,即存储在数组的第一个元素上。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
(2)、HashTable的key和value都不能为null
HashTable的key不能为null是由于需要使用key调用hashCode()方法获取哈希值,如果key为null就会抛出空指针异常。value不能为null是由于在put()方法内,针对value做了个是否为null值的判断。
// get方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 直接使用key调用hashCode()方法获取哈希值,如果key为null的话肯定会抛出空指针异常。
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// ...省略...
}
// put方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
// 对存入的value做了是否为null的判断,如果为null就抛出空指针异常
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
// 直接使用key调用hashCode()方法获取哈希值,如果key为null的话肯定会抛出空指针异常。
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// ...省略...
}
3、数组扩容机制不同
-
HashMap:初始长度为16,每次扩容为原来的2倍(2n)
-
HashTable:初始长度为11,每次扩容为原来的2倍再加一(2n + 1)
// HashMap的哈希表默认初始大小为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// HashMap每次扩充为原来的2倍
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
}
// HashTable的哈希表默认初始大小为11
public Hashtable() {
this(11, 0.75f);
}
// HashTable每次扩容为原来的2倍再加一
protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldMap = table;
// 每次扩容为原来的2n+1
int newCapacity = (oldCapacity << 1) + 1;
// ...
}
4、线程安全问题
HashTable是线程安全的,HashMap是线程不安全的。也就是在多线程并发访问时,HashTable不会发生线程安全问题(不需要添加额外的同步机制),而HashMap会发生线程安全问题(需要添加同步机制)。
HashTable之所以是线程安全的,是因为其在存储和获取元素的方法上使用了synchronized关键字进行修饰,在遍历视图的方法中使用了Collections.synchronizedXXX进行了封装。
// get方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
// get方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// .....省略
}
// 遍历集合的方法
public Set<K> keySet() {
if (keySet == null)
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
5、迭代器不同
-
HashMap的迭代器是Iterator类型的,该类型的迭代器特点是如果在迭代过程中改变的集合的内容(增删改)的话,就会抛出ConcurrentModificationException(并发修改异常)异常。
-
HashTable的迭代器是Enumerator类型的