HashMap 和 Hashtable 区别

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_38495686/article/details/79965050

摘要:HashMap 和 Hashtable 在 Java 面试中经常容易被问到,甚至成了集合框架面试题中最被常考的问题。这篇文章首先简单介绍一下 Hashtable,然后对比 HashMap 和 Hashtable,分析了它们的主要区别。如果想要了解 HashMap, 可以看这篇文章《 深入理解 HashMap 》

Hashtable 概述

与 HashMap 相同,Hashtable 也是基于哈希表实现的,每个元素是一个 键值对(key-value),其内部也是通过单链表解决冲突问题,当容量超过其阈值时,就会自动扩容。

相比 HashMap,Hashtable 是线程安全的,可以用于多线程环境中。

Hashtable 同样实现了 Serializable 接口,支持序列化,实现了 Cloneable 接口,能被克隆。

由于 HashMap 和 Hashtable 底层大多数实现相同,这里就不再赘述。这里主要来看一下它们的不同。

HashMap 和 Hashtable 的区别

1.继承关系

直接看源码:

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

可以看到,HashMap 和 Hashtable 都实现了 Map

//AbstractMap 是基于 Map<K, V> 接口的骨干实现,内部有具体实现方法,极大地减少实现解耦所需的工作
public abstract class AbstractMap<K,V> implements Map<K,V> {
    protected AbstractMap() { }
    public int size() {
        return entrySet().size();
    }
    public boolean isEmpty() {
        return size() == 0;
    }
    public static class SimpleEntry<K,V>
        implements Entry<K,V>, java.io.Serializable
    {
        ·····
    }
    ······
}
//而 Dictionary 是任何可操作“键值对”函数接口的抽象父类,只有一些声明
public abstract
class Dictionary<K,V> {
    public Dictionary() { }
    abstract public int size();
    abstract public V get(Object key);
    ······
}
2.线程安全性

Hashtable 中的方法是 Synchronized 的,在多线程并发环境下可直接使用,而 HashMap 中的方法在缺省情况下是非 Synchronized 的,如果要用于多线程环境下,需要自己增加同步处理。一般可以使用 Collections.synchronizedMap 方法来包装该映射。例如

Map m = Collections.synchronizedMap(new HashMap(···));

Hashtable 的线程安全也很好理解,因为每个方法都加入了 Synchronize。至于 HashMap 为什么是不安全的? 在 《深入理解 HashMap 》 那篇文章中有说过一个死循环问题,还有可能会出现数据错误。

3.key 和 value 是否为 null 值

先来看一下源码:

public synchronized V put(K key, V value) {
    // 如果值为空,则抛出确保键对应的值非空
    if (value == null) {
        throw new NullPointerException();
    }

    // 确保键在 Hashtable 中不存在
    Entry<?,?> tab[] = table;
    //如果 key 为空,计算 hashCode 就会抛出异常
    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) {
        //如果已存在键为 key 的键值对,就会覆盖 value
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

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

可以看到,这里对 value 进行了判断,如果为 null 就抛出 NullPointerException 异常。而对 key 虽然没有判断,但计算 hashCode 时,如果 key 为 null 也会抛出 NullPointerException 异常。从而不允许 key 和 value 为空。

而在 HashMap 中,这里对 key 进行了判断:

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

如果 key 为 null时,会返回 0

 index = (n - 1) & lenth;

在计算下标后,所有 key 为 null 的值都会放在第一个桶中,并且之后 key 为 null 的 value 也会覆盖之前的 value。

所以在 HashMap 中,当 get() 方法返回 null 值时,可能是 map 中没有该键,也可能是该键对应值为 null。因此在 HashMap 中不能通过 get 方法判断是否存在某个键

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
4.遍历方式不同

HashMap 仅支持 Iterator 的遍历方式

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;
    public final boolean hasNext(){
        return next != null;
    }
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }
    ······
final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

final class ValueIterator extends HashIterator
    implements Iterator<V> {
    public final V next() { return nextNode().value; }
}

final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

而 Hashtable 支持 Iterator 和 Enumeration 两种遍历方式

private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
    Entry<?,?>[] table = Hashtable.this.table;
    int index = table.length;
    Entry<?,?> entry;
    Entry<?,?> lastReturned;
    int type;
    public boolean hasNext() {
        return hasMoreElements();
    }
    public T next() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        return nextElement();
    }
    ······
}

HashMap 的迭代器(Iterator) 是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fast-fail 的。所以在 HashMap 中,当有其他线程改变了它的结构时(增加或删除元素),将会抛出 ConcurrentModificationException。但迭代器本身的 remove 方法则不会抛出。这也是 Enumerator 和 Iterator 的区别

5.内部实现使用数组和扩容方式不同

Hashtable 在不指定容量时的默认容量为 11,不要求低层数组容量为 2 的次方幂

public Hashtable() {
    this(11, 0.75f);
}

而 HashMap 默认容量为 16,并且要求数组容量为 2 的次方幂

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

Hashtable 扩容时将容量变为原来的 2 倍加 1

protected void rehash() {
    ······
    // 2 * oldCap + 1
    int newCapacity = (oldCapacity << 1) + 1;
}

而 HashMap 扩容时,将容量变为原来的 2 倍

final Node<K,V>[] resize() {
    ······
    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; // 每次扩容时为原来容量的 2 倍
    }
}
6.hash 值使用

在得到 hashCode 后,在 Hashtable 中,采用取模运算得到索引位置

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

而 HashMap 采用先高位参与运算. 然后和 (table.length - 1) 与运算

static final int hash(Object key) {
    int h;
    //h = key.hashCode() 第一步 取 HashCode 值
    // h ^ (n >>> 16) 第二步 高位参与运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

index =  h & (length - 1);   //第三步 等价于取模运算,

这样做等价于取模,但更加高效,而且得到的位置也更加均匀

小结

  • Hashtable 继承自 Dictionary 的抽象类,而 HashMap 是 AbstarctMap 接口的一个实现
  • HashMap 是非线性安全的,HashMap 是线程安全的。如果使用 java5 或以上,可以使用 ConcurrentHashMap
  • HashMap 允许 key 和 value 都为null,而 Hashtable 都不能为 null
  • Hashtable中数组默认初始容量为 11,扩容方式是 oldCap * 2 + 1。而 HashMap 中数组的默认初始大小是 16,每次扩容为原来的 2 倍
  • 两者计算索引算法不同,得到 hashCode 后,Hashtable 是取模运算,而后者是高位参与运算计算 hash 值,再和(table.length – 1)进行与运算,也等价于取模,但更加高效,取得的位置更加均匀
  • HashMap 的迭代器 (Iterator) 是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的

参考文章:

HashTable和HashMap的区别详解:https://blog.csdn.net/fujiakai/article/details/51585767
HashTable的使用和原理:https://blog.csdn.net/jinhuoxingkong/article/details/52022999
HashMap和Hashtable的区别:http://www.importnew.com/7010.html
[Java集合源码剖析]Hashtable源码剖析:https://blog.csdn.net/ns_code/article/details/36191279
深入JDK源码之Hashtable:https://blog.csdn.net/alone_rojer/article/details/60331988

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页