聊聊java中的哪些Map:(五)HashTable与HashMap的区别

时间:2022-07-23
本文章向大家介绍聊聊java中的哪些Map:(五)HashTable与HashMap的区别,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

既然聊到了HashMap,那么HashTable、ConcurrentHashMap等这都是绕不开的话题。做为ConcurrentHashMap在并发场景下高效性能的一个反例,HashTable究竟是怎么实现的呢,本章将对HashTable的源码进行分析。

1.类结构及其成员变量

1.1 类的基本结构

HashTable与HashMap不太一样,由于HashTable产生得比较早,而在java升级的过程中,其功能逐渐被ConcurrentHashMap取代,因此HashTable逐渐显得有些过时。其继承了一个过时的抽象类Dictionary。在java后续发展的过程中,Dictionary的作用逐渐被Map取代了。虽然HashTable在jdk1.2之后也实现了map接口,但是可以看到在1.7、1.8中除了保持原状之外逐渐没有更新。 其类结构如下:

我们可以看到,继承了Dictionary抽象类,另外实现了Map、Cloneable、Serializable接口。

/**
 * This class implements a hash table, which maps keys to values. Any
 * non-<code>null</code> object can be used as a key or as a value. <p>
 *
 * To successfully store and retrieve objects from a hashtable, the
 * objects used as keys must implement the <code>hashCode</code>
 * method and the <code>equals</code> method. <p>
 *
 * An instance of <code>Hashtable</code> has two parameters that affect its
 * performance: <i>initial capacity</i> and <i>load factor</i>.  The
 * <i>capacity</i> is the number of <i>buckets</i> in the hash table, and the
 * <i>initial capacity</i> is simply the capacity at the time the hash table
 * is created.  Note that the hash table is <i>open</i>: in the case of a "hash
 * collision", a single bucket stores multiple entries, which must be searched
 * sequentially.  The <i>load factor</i> is a measure of how full the hash
 * table is allowed to get before its capacity is automatically increased.
 * The initial capacity and load factor parameters are merely hints to
 * the implementation.  The exact details as to when and whether the rehash
 * method is invoked are implementation-dependent.<p>
 *
 * Generally, the default load factor (.75) offers a good tradeoff between
 * time and space costs.  Higher values decrease the space overhead but
 * increase the time cost to look up an entry (which is reflected in most
 * <tt>Hashtable</tt> operations, including <tt>get</tt> and <tt>put</tt>).<p>
 *
 * The initial capacity controls a tradeoff between wasted space and the
 * need for <code>rehash</code> operations, which are time-consuming.
 * No <code>rehash</code> operations will <i>ever</i> occur if the initial
 * capacity is greater than the maximum number of entries the
 * <tt>Hashtable</tt> will contain divided by its load factor.  However,
 * setting the initial capacity too high can waste space.<p>
 *
 * If many entries are to be made into a <code>Hashtable</code>,
 * creating it with a sufficiently large capacity may allow the
 * entries to be inserted more efficiently than letting it perform
 * automatic rehashing as needed to grow the table. <p>
 *
 * This example creates a hashtable of numbers. It uses the names of
 * the numbers as keys:
 * <pre>   {@code
 *   Hashtable<String, Integer> numbers
 *     = new Hashtable<String, Integer>();
 *   numbers.put("one", 1);
 *   numbers.put("two", 2);
 *   numbers.put("three", 3);}</pre>
 *
 * <p>To retrieve a number, use the following code:
 * <pre>   {@code
 *   Integer n = numbers.get("two");
 *   if (n != null) {
 *     System.out.println("two = " + n);
 *   }}</pre>
 *
 * <p>The iterators returned by the <tt>iterator</tt> method of the collections
 * returned by all of this class's "collection view methods" are
 * <em>fail-fast</em>: if the Hashtable is structurally modified at any time
 * after the iterator is created, in any way except through the iterator's own
 * <tt>remove</tt> method, the iterator will throw a {@link
 * ConcurrentModificationException}.  Thus, in the face of concurrent
 * modification, the iterator fails quickly and cleanly, rather than risking
 * arbitrary, non-deterministic behavior at an undetermined time in the future.
 * The Enumerations returned by Hashtable's keys and elements methods are
 * <em>not</em> fail-fast.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw <tt>ConcurrentModificationException</tt> on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness: <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>
 *
 * <p>As of the Java 2 platform v1.2, this class was retrofitted to
 * implement the {@link Map} interface, making it a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 *
 * Java Collections Framework</a>.  Unlike the new collection
 * implementations, {@code Hashtable} is synchronized.  If a
 * thread-safe implementation is not needed, it is recommended to use
 * {@link HashMap} in place of {@code Hashtable}.  If a thread-safe
 * highly-concurrent implementation is desired, then it is recommended
 * to use {@link java.util.concurrent.ConcurrentHashMap} in place of
 * {@code Hashtable}.
 *
 * @author  Arthur van Hoff
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Object#equals(java.lang.Object)
 * @see     Object#hashCode()
 * @see     Hashtable#rehash()
 * @see     Collection
 * @see     Map
 * @see     HashMap
 * @see     TreeMap
 * @since JDK1.0
 */
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    
}

在开头有大段的注释,在java中,尤其是源码中有大段注释的地方是特别需要我们注意的。上述部分其大意为: 这个类实现了一个hash表,它将key映射到值。任何非空的对象都可以做为key或者value使用。 要成功地从hash表中存储和检索对象,则做为key的对象必须实现hashCode方法和equals方法。 HashTable的一个示例有两个参数影响它的性能,初始容量,负载因子。容量capacity是哈希表中bucket的数量,而初始的容量则是在创建hash表的时候同时创建。需要注意的是,哈希表是开放状态的,在hash冲突的情况下,一个bucket存储了多个条目,那么必须按顺序对这些条目进行搜索。负载因子load factor 是一个度量哈希表在其容量自动增加之前可以达到的完整程度。初始容量和负载因子只是对实现的提示,有关何时及是否调用rehash方法确切的细节取决于实现。 通常情况下,负载因子默认为0.75,这是在时间和空间成本上的一个平衡。如果这个值太高固然会降低空间的开销,但是检索的时候会增加时间成本。这在大多数哈希表的操作中,表现在get和put方法上。 初始容量是空间浪费和rehash操作的折衷。rehash操作非常耗时。如果初始容量大于hashtable将包含的最大条目和负载因子之商,那么将不会有任何rehash操作。然而,这将造成空间浪费。 如果许多条目被创建到hashtable中,创建一个足够大的容量允许这些条目插入,比让这些条目插入之后触发rehash操作更有效率。 如下这个例子创建了一个数字的hash表,它使用名称做为数字的key。

 Hashtable<String, Integer> numbers
     = new Hashtable<String, Integer>();
    numbers.put("one", 1);
    numbers.put("two", 2);
    numbers.put("three", 3);

检索一个号码,用如下代码:

Integer n = numbers.get("two");
    if (n != null) {
      System.out.println("two = " + n);
    }

类的所有集合视图方法返回的集合的iterator方法返回的集合是符合fail-fast机制的。在hashTable被创建后的任何时候,以任何方式(除迭代器自己的remove方法之外)修改hashtable,迭代器将抛出ConcurrentModificationException异常。因此,在并发修改的情况下,迭代器会迅速的返回失败,而不是在将来某个不确定的时间,冒着任意的、不确定的行为风险。HashTable的keys和elements方法返回Enumerations类型不是fail-fast。 需要注意的是,迭代器的fail-fast行为不能得到保证,因为一般来说,在存在不同步的并发修改的时候,不可能做出任何硬件担保。Fail-fast迭代器尽最大努力抛出ConcurrentModificationException,因此,编写任何一个依赖于这个异常来确保正确性的程序是错误的,迭代器的fail-fast机制只用于检测错误。 在java1.2中,这个类实现了map接口,确保其是java集合框架的成员之一。 在java集合框架中,与其他集合不同的是HashTable是同步的,如果线程安全不是必须的,请使用HashMap来替换HashTable。如果需要一个线程安全的实现,推荐使用ConcurrentHashMap。 通过上述注释可以看除,实际上HashTable在java集合框架中,越来越尴尬,不需要并发的情况下直接使用HashMap效率会高很多。在并发场景下则推荐使用ConcurrentHashMap。

1.2 核心内部类Entry

H与HashMap类似,HashTable内部也采用内部类来实现这些复杂的结构。

private static class Entry<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Entry<K,V> next;

    protected Entry(int hash, K key, V value, Entry<K,V> next) {
        this.hash = hash;
        this.key =  key;
        this.value = value;
        this.next = next;
    }

    @SuppressWarnings("unchecked")
    protected Object clone() {
        return new Entry<>(hash, key, value,
                              (next==null ? null : (Entry<K,V>) next.clone()));
    }

    // Map.Entry Ops

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

    public V setValue(V value) {
        if (value == null)
            throw new NullPointerException();

        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
           (value==null ? e.getValue()==null : value.equals(e.getValue()));
    }

    public int hashCode() {
        return hash ^ Objects.hashCode(value);
    }

    public String toString() {
        return key.toString()+"="+value.toString();
    }
}

这个内部类实现了Map.Entry接口。 内部核心的字段有,hash、key、value,另外还有一个next的指针指向下一个元素。也就是说,实际上hashtable在hash碰撞之后由单向链表组成。这实际上与HashMap中的Node节点一致。 需要注意的是 hashCode方法与HashMap中的实现不太一样: 如下是HashMap中Node的hashCode的方法。是将key和value的hashCode进行异或运算。

Objects.hashCode(key) ^ Objects.hashCode(value);

而在此处,则是Entry中的hash和value的hashcode进行异或运算。

return hash ^ Objects.hashCode(value);

这个代码的结果实际上没有区别,Hashtable中的hash实际上是key的hashcode:

hash = key.hashCode();

但是hashMap中的hash属性已经被修改了,需要用高位部分混淆。

 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

也就是说hashMap和HashTable中元素的hash属性的计算是有区别的,这也是我们在面试过程中需要注意的。hashMap为什么要重写hash方法,实际上是因为hashmap计算bucket的方法不同。这个在所有Map都分析完之后进行统一总结。

1.3 成员变量 重要的一些成员变量如下:

/**
 * The hash table data.
 */
private transient Entry<?,?>[] table;

/**
 * The total number of entries in the hash table.
 */
private transient int count;

/**
 * The table is rehashed when its size exceeds this threshold.  (The
 * value of this field is (int)(capacity * loadFactor).)
 *
 * @serial
 */
private int threshold;

/**
 * The load factor for the hashtable.
 *
 * @serial
 */
private float loadFactor;

成员变量有transient修饰的Entry数组 table。以及哈希表元素的数量count。另外还有threshold和loadFactor两个控制变量。这个与HashMap不同的是,loadfactor并没有在类中定义一个常量。HashTable的这些初始化参数直接在构造函数中。 需要注意的是,HashMap以及HashTable相关存储元素的数组等属性都是transient修饰,在序列化的时候不会被序列化,而是类自己实现了序列化和反序列华的缺省方法。因为EffecitveJava一书中说过,这是因为在不同的虚拟机或者不同操作系统上,hashcode的算法可能会造成相同的值经过hash之后其hashcode不一致。这样就容易造成实际上反序列化之后的HashTable与之前的HashTable不同。

另外有个常量是我们需要注意的:

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

这里对为啥MAX_ARRAY_SIZE不是Integer.MAX_VALUE进行了解释。再某些jvm中,数组是需要一些头信息的,因此如果直接将int数组分配为Integer.MAX_VALUE,则会造成OOM。所以这里的长度是Integer.MAX_VALUE - 8;

2.HashTable的基本原理

学过HashMap之后再来看HashTable就非常简单了。其内部构成就是通过拉链法实现的单向链表。

这就是HashTable的组成。 实际上相对HashMap和LinkedHashMap要简单得多。

3.HashTable的构造函数

3.1 Hashtable(int initialCapacity, float loadFactor)

这是一个最基本的构造方法,判断initialCapacity和loadFactor是否合法。当initialCapacity为0的时候,会改为1。

public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    table = new Entry<?,?>[initialCapacity];
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

在调用构造函数的时候,会创建这个table数组,这一点与HashMap也不同,HashMap的数组为了控制其大小为2的幂,是在resize的方法中才会创建。而HashTable没有这个限制。因此可以在构造函数中创建。

3.2 Hashtable(int initialCapacity)

实际上是调用的3.1中的构造函数:

public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}

在这指定了loadFactor为0.75。

3.3 Hashtable()

也是通过this调用前面的构造函数。

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

这里可以看到,HashTable的默认大小为11,负载因子为0.75。HashTable的bucket可以为任意大小。

3.4 Hashtable(Map<? extends K, ? extends V> t)

同理,也会存在一个根据原有的Map产生一个新Map的构造方法。

public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}

判断大小,通过构造函数创建的HashTable默认为11。之后调用putAll方法。

public synchronized void putAll(Map<? extends K, ? extends V> t) {
    for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
        put(e.getKey(), e.getValue());
}

putAll方法是一个同步方法。其没部通过遍历,之后对put方法进行调用。

4.重要的方法

在HashTable中,还有一些非常重要的方法。

4.1 rehash

这个方法是Hashtable的核心方法,扩容就是用此方法进行实现,我们来看看其源码:

/**
 * Increases the capacity of and internally reorganizes this
 * hashtable, in order to accommodate and access its entries more
 * efficiently.  This method is called automatically when the
 * number of keys in the hashtable exceeds this hashtable's capacity
 * and load factor.
 */
@SuppressWarnings("unchecked")
protected void rehash() {
    //原始的table容量及table
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //注意其扩容的方式,左移1位+1。
    int newCapacity = (oldCapacity << 1) + 1;
    //如果其容量大于最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        //如果旧的容量就为最大值,那么说明没必要扩容,直接return
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        //反之扩容到最大值
        newCapacity = MAX_ARRAY_SIZE;
    }
    //创建新的数组
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    //增加modCount
    modCount++;
    //重新计算门槛阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
    //遍历数组
    for (int i = oldCapacity ; i-- > 0 ;) {
        //遍历数组上的链表
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;
            //注意这个计算槽位的方法
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

我们可以看到,rehash方法,其扩容的方式与HashMap中resize的方式不一样。

newCapacity = (oldCapacity << 1) + 1;

另外计算bucket槽位的时候:

int index = (e.hash & 0x7FFFFFFF) % newCapacity;

先采用& 0x7FFFFFFF。这是因为 0x7FFFFFFF为2^32-1。这样一来,由于采用的是&运算,这样一来任何大于0x7FFFFFFF都将只保留0x7FFFFFFF覆盖的低位部分。高位部分会舍去。由于Hashtable的长度不一定满足2的幂,因此其计算槽位不能用位运算。这里直接用%。显然其效率要低于Hashmap中的位运算操作。 由于在Hashtable中,rehash并没提供给外部访问,而调用rehash的位置只有addEntry方法。因此这个方法没有加同步关键字。 另外HashTable并没提供缩容机制,也不存在HashMap中红黑树和链表互相转换的问题。因此其逻辑要简单得多。

4.2 addEntry

这是HashTable中添加元素的底层方法:

private void addEntry(int hash, K key, V value, int index) {
    //修改modecount
    modCount++;
    //tab指向table
    Entry<?,?> tab[] = table;
    //判断是否需要扩容
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        //扩容
        rehash();

        tab = table;
        hash = key.hashCode();
        //重新计算索引
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    //在索引处创建元素 将这个元素添加到链表的末尾
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

将元素添加到Hashtable的时候,最近增加的元素会放到table指针上,而将之前的元素前移。

4.3 put

这是同步方法,将元素put到HashTable中

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    //不支持空的value
    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];
    //遍历 判断其key是否已经存在于链表中,如果存在直接返回
    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);
    添加成功返回null否则返回之前的old元素
    return null;
}

4.4 remove

删除hashTable中的元素,这个方法也是同步方法:

public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    //计算index
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    //遍历
    for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
    //用hash和key是否都相等来判断是否存在于HashTable中
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            if (prev != null) {
            //改变链表
                prev.next = e.next;
            } else {
            //如果在链表末尾 tab中,则改变tab指针
                tab[index] = e.next;
            }
            count--;
            V oldValue = e.value;
            e.value = null;
            //返回被删除的value
            return oldValue;
        }
    }
    //如果不存在则返回null
    return null;
}

4.5 get

获取元素,此方法也是同步的。

public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    //计算index
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //遍历
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        //如果hash相同且key相等说明即为所查找的元素
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

4.6 clear

清空HashTable,这个方法也是同步的。

public synchronized void clear() {
    Entry<?,?> tab[] = table;
    modCount++;
    //遍历table,将其都置为null
    for (int index = tab.length; --index >= 0; )
        tab[index] = null;
    count = 0;
}

4.7 clone

需要注意的是,Hashtable提供的clone是一个浅拷贝。

/**
 * Creates a shallow copy of this hashtable. All the structure of the
 * hashtable itself is copied, but the keys and values are not cloned.
 * This is a relatively expensive operation.
 *
 * @return  a clone of the hashtable
 */
public synchronized Object clone() {
    try {
        //clone对象
        Hashtable<?,?> t = (Hashtable<?,?>)super.clone();
        //创建数组
        t.table = new Entry<?,?>[table.length];
        //如果某个索引不为空则clone
        for (int i = table.length ; i-- > 0 ; ) {
            t.table[i] = (table[i] != null)
                ? (Entry<?,?>) table[i].clone() : null;
        }
        //其他全部置空
        t.keySet = null;
        t.entrySet = null;
        t.values = null;
        t.modCount = 0;
        return t;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

这个clone方法是一个浅拷贝方法,其数组会新建一个数据,但是其中的Entry都是指向之前的对象。而且Keyset和entrySet以及values都为空。这也是hashtable面试的过程中需要注意的。

4.8 replace

HashTable提供了两个replace方法,分别是当key和Value都相等的情况下replace和只需要根据key来确定replace。

 @Override
public synchronized boolean replace(K key, V oldValue, V newValue) {
    Objects.requireNonNull(oldValue);
    Objects.requireNonNull(newValue);
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    //计算index
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    //for循环遍历
    for (; e != null; e = e.next) {
        //判断hash和key是否相等
        if ((e.hash == hash) && e.key.equals(key)) {
           //判断value是否相等
            if (e.value.equals(oldValue)) {
               //如果都相等则替换
                e.value = newValue;
                return true;
            } else {
                return false;
            }
        }
    }
    return false;
}

另外一个replace方法:

public synchronized V replace(K key, V value) {
    Objects.requireNonNull(value);
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    //计算index
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    //循环遍历
    for (; e != null; e = e.next) {
    //如果hash和key相等则替换
        if ((e.hash == hash) && e.key.equals(key)) {
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }
    return null;
}

5.视图类

与HashMap等类似,Hashtable也提供了视图类KeySet、Values、EntrySet的支持。其实现如下:

/**
 * Each of these fields are initialized to contain an instance of the
 * appropriate view the first time this view is requested.  The views are
 * stateless, so there's no reason to create more than one of each.
 */
private transient volatile Set<K> keySet;
private transient volatile Set<Map.Entry<K,V>> entrySet;
private transient volatile Collection<V> values;

由于涉及在并发情况下使用,因此这三个变量都是volatile修饰的,具有可见性。

5.1 KeySet

public Set<K> keySet() {
    if (keySet == null)
        keySet = Collections.synchronizedSet(new KeySet(), this);
    return keySet;
}

private class KeySet extends AbstractSet<K> {
    public Iterator<K> iterator() {
        return getIterator(KEYS);
    }
    public int size() {
        return count;
    }
    public boolean contains(Object o) {
        return containsKey(o);
    }
    public boolean remove(Object o) {
        return Hashtable.this.remove(o) != null;
    }
    public void clear() {
        Hashtable.this.clear();
    }
}

Keyset实际上就是要给视图类,但是此处采用 Collections.synchronizedSet进行同步保证。 其他Values和EntrySet与此原理类似。就不一一列举。

6.总结

本文对Hashtale源码进行了分析,虽然Hashtable类已经在现实情况中很少使用,但是我们仍然需要注意其与HashMap的区别。这也是面试的时候的高频面试题,HashMap与HashTable主要区别体现在:

  • 1.从根本上而言,HashMap和HashTable的结构不同,HashTable的简单的数据加链表组成。而HashMap显然要复杂得多。可以将链表在触发阈值之后转为红黑树。之后再元素少于阈值的时候转为链表。
  • 2.HashMap是不支持同步的,不能用于并发场景。而Hashtable的对外的方法都是synchronized的,这样HashTable就能在同步的情况下使用。
  • 3.HashMap支持key和Value为null的情况,而HashTable并不支持。这是因为,Hashtable产生得比较早,而其源码中都用到了key和value的hashcode,因此不能为null,如果不做非空约束的话就会产生NPE异常。而HashMap在其源代码中对null的情况做了特殊的支持。另外,由于put和get的时候,都需要返回value的值,如果并发容器返回了null,此时你不知道是插入了null还是本来就不存在。但是HashMap的时候由于是非并发场景,可以使用contains方法判断。所以这也是ConcurrentHashMap也不支持null的原因。
  • 4.HashMap的bucket的默认初始化大小为16,这是其空间和时间的权衡,hashMap其内部实现要求其table的大小为2的幂。而Hashtable内部table的大小没有这个限制。HashTable的初始化大小为11。
  • 5.二者的扩容方式也不同,HashMap的扩容,由于其table长度为2的幂,那么扩容的情况下,只需要左移一位即可。Hashtable则是左移1位之后+1。
  • 6.二者计算索引的方法不同,HashMap符合2的幂,因此计算的之后采用hashcode&(size-1),而HashTable中,则是(hashcode&0x7FFFFFFF)%size。显然hashMap的方式效率更高。
  • 7.在扩容的时候,HashMap可以利用其长度为2的幂的特性,直接可以将元素通过高低位计算,由hashcode&oldsize==0判断是否为低位,如果不是则放在高位,高位索引为 index+oldsize。但是Hashtable则不能这么做,只能遍历每个元素之后重新计算索引值。可见HashMap的扩容效率也要高不少。
  • 8.此外HashMap还提供了Spliterator等可以在java8的stream中并行迭代的迭代器。HashTable由于开发较早,而且逐渐不再被使用,因此没有提供并行迭代器。

以上是我对HashTable和HashMap源代码阅读之后的比较,如有不足,烦请补充。