从Java源码的角度来分析HashMap与HashTable的区别

Posted a_woxinfeiyang_a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Java源码的角度来分析HashMap与HashTable的区别相关的知识,希望对你有一定的参考价值。

        由于HashMap与HashTable都是用来存储Key-Value的键值对,所以经常拿来对比二者的区别,下面就从源码的角度来分析一下HashMap与HashTable的区别,

        首先介绍一下两者的区别,然后再从源码分析。

HahMap与HahTable两者主要区别:

1、继承的父类不同

<span style="font-size:18px;">public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable</span>
<span style="font-size:18px;"><pre name="code" class="java">public class Hashtable<K, V> extends Dictionary<K, V> implements Map<K, V>, Cloneable, Serializable</span>
HashTable是一个古老的Map实现类,从JDK1.0就开始出现,继承自Dictionary,当时JDK还有没有出现Map这个接口;HashMap继承自AbstractMap这个抽象类,而AbstractMap这个抽象类实现了Map接口。

 
        2、HashTable中的方法是同步的,在多线程程序中无需程序员自己去实现同步处理;而HashMap中的方法是非同步的,需要程序员自己去实现同步处理,常用的方法是使用Collections工具类中的synchronizedXxx()方法吧集合类包装成线程同步的集合。 

        3、HashTable中Key与Value都不允许出现NULL值,而HashMap中Key可以为

NULL,由于HashMap是Map中实现类,Map集合中要求每个Key唯一,所以HashMap

中之多只能出现一个Key为NULL的情况,但HashMap中可以出现多个Value为NULL

的情况。

        4、遍历方式不同

        Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了

Enumeration的方式 。

        然后接下来看看HashMap的源码。

        HashMap中一些属性Field如下:        

<span style="font-size:18px;">    /**
     * Min capacity (other than zero) for a HashMap. Must be a power of two
     * greater than 1 (and less than 1 << 30).
     * HashMap的最小容量
     */
    private static final int MINIMUM_CAPACITY = 4;

    /**
     * Max capacity for a HashMap. Must be a power of two >= MINIMUM_CAPACITY.
     * HashMap的最大容量
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * An empty table shared by all zero-capacity maps (typically from default
     * constructor). It is never written to, and replaced on first put. Its size
     * is set to half the minimum, so that the first resize will create a
     * minimum-sized table.
     * 
     * HashMapEntry数组,HashMapEntry条目数组,HashMapEntry数组大小为MINIMUM_CAPACITY/2
     */
    private static final Entry[] EMPTY_TABLE
            = new HashMapEntry[MINIMUM_CAPACITY >>> 1];

    /**
     * The default load factor. Note that this implementation ignores the
     * load factor, but cannot do away with it entirely because it's
     * mentioned in the API.
     *
     * <p>Note that this constant has no impact on the behavior of the program,
     * but it is emitted as part of the serialized form. The load factor of
     * .75 is hardwired into the program, which uses cheap shifts in place of
     * expensive division.
     * 默认的负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = .75F;

    /**
     * The hash table. If this hash map contains a mapping for null, it is
     * not represented this hash table.
     * HashMap容器数组,用于存放HashMapEntry对象的数组
     */
    transient HashMapEntry<K, V>[] table;

    /**
     * The entry representing the null key, or null if there's no such mapping.
     * 空键值对应的HashMapEntry条目对象
     */
    transient HashMapEntry<K, V> entryForNullKey;

    /**
     * The number of mappings in this hash map.
     * HashMap中元素的数量(大小)
     */
    transient int size;

    /**
     * Incremented by "structural modifications" to allow (best effort)
     * detection of concurrent modification.
     * 用于确保使用迭代器的时候,HashMap并未进行更改
     */
    transient int modCount;

    /**
     * The table is rehashed when its size exceeds this threshold.
     * The value of this field is generally .75 * capacity, except when
     * the capacity is zero, as described in the EMPTY_TABLE declaration
     * above.
     * 负载因子(阈值)
     */
    private transient int threshold;

    // Views - lazily initialized
    /**由K键组成的Set集合*/
    private transient Set<K> keySet;
    /**由K-Value组成的Set集合,Set集合中存放了Entry<K, V>元素*/
    private transient Set<Entry<K, V>> entrySet;
    
    private transient Collection<V> values;</span>
        HashMap中是用HashMapEntry数组来存储Key-Value键值对的,HashMapEntry是HashMap中一个静态的内部类,实现了java.util.Map.Entry接口。在HashMap的属性Field中定义用于存放Key-Value键值对的HashMapEntry数组长度的默认的最小值和最大值。一个空的HashMapEntry数组EMPTY_TABLE以及HashMap容器数组tab,用于存放HashMapEntry对象的数组。默认的负载因子DEFAULT_LOAD_FACTOR,将其定义为0.75(3/4)。

HashMap的构造函数主要有以下几个:

HashMap()
          Constructs a new empty HashMap instance.
HashMap(int capacity)
          Constructs a new HashMap instance with the specified capacity.
HashMap(int capacity, float loadFactor)
          Constructs a new HashMap instance with the specified capacity and load factor.
HashMap(Map<? extendsK,? extends V> map)
          Constructs a new HashMap instance containing the mappings from the specified map.
       我们来看下HashMap(int capacity)这个构造函数的源码

<span style="font-size:18px;">    /**
     * Constructs a new {@code HashMap} instance with the specified capacity.
     *
     * @param capacity 
     *            the initial capacity of this hash map.
     * @throws IllegalArgumentException
     *                when the capacity is less than zero.
     *通过一个指定的容量来构造一个HashMap实例对象
     *@param    capacity:HashMap初始化容量
     *             
     */
    public HashMap(int capacity) {
        if (capacity < 0) {
            throw new IllegalArgumentException("Capacity: " + capacity);
        }

        if (capacity == 0) {
            @SuppressWarnings("unchecked")
            HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
            table = tab;
            threshold = -1; // Forces first put() to replace EMPTY_TABLE
            return;
        }

        if (capacity < MINIMUM_CAPACITY) {
            capacity = MINIMUM_CAPACITY;
        } else if (capacity > MAXIMUM_CAPACITY) {
            capacity = MAXIMUM_CAPACITY;
        } else {
            capacity = Collections.roundUpToPowerOfTwo(capacity);
        }
        makeTable(capacity);
    }</span>
       该函数通过一个指定的容量值capacity来创建一个HashMapEntry数组对象,当capacity等于0时将空的HashMapEntry数组对象EMPTY_TABLE赋值给tab对象;如果capacity不为0时,当capacity小于容量最小值时将capacity调整为容量最小值,同理当capacity大于容量最大值时将capacity调整为容量最大值。最后调用makeTable来初始化创建tab数组。下面是makeTable的具体实现

<span style="font-size:18px;">    /**
     * Allocate a table of the given capacity and set the threshold accordingly.
     * 根据指定容量来分配table数组的大小,并设置负载因子(阈值)
     * @param newCapacity must be a power of two
     * 
     */
    private HashMapEntry<K, V>[] makeTable(int newCapacity) {
        @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
                = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
        table = newTable;
        threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
        return newTable;
    }</span>
可以发现在makeTable中是通过创建一个按指定容量大小新的HashMapEntry数组newTable,并将其newTable数组赋值给HashMap的属性File变量table,因此在HashMap中table数组才是真正的存放HashMapEntry的容器,即存放Key-Value条目的容器。在构造函数中将负载因子threshold设置为容量的3/4,当table数组中存放的HashMapEntry元素个数大于负载因子threshold时table数组将扩容。通过查看java.util.HashMap.put(K key, V value)源码即可发现这点:

<span style="font-size:18px;">    /**
     * Maps the specified key to the specified value.
     *
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the value of any previous mapping with the specified key or
     *         {@code null} if there was no such mapping.
     */
    @Override public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }</span>
HashMap.put方法用于向HashMap中插入HashMapEntry元素(即插入Key-Value键值对),当key为空的时候调用putValueForNullKey方法将value加入到key为NULL的HashMapEntry对象中。然后再检测当前传入的key是否在table数组中已经存在,如果存在就将新的value值替换掉key对应旧的value值oldValue并将oldValue返回;如果key在table数组中不存在则调用addNewEntry(K key, V value, int hash, int index) 方法将Value对象添加到table数组中。不过在加入到table数组之前要先进行判断,如果table数组元素数量size大于负载因子threshold则调用doubleCapacity来给table数组扩容。doubleCapacity方法源码如下:

<span style="font-size:18px;">    /**
     * Doubles the capacity of the hash table. Existing entries are placed in
     * the correct bucket on the enlarged table. If the current capacity is,
     * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
     * will be new unless we were already at MAXIMUM_CAPACITY.
     * 将数组容量扩大
     */
    private HashMapEntry<K, V>[] doubleCapacity() {
    	/**旧的table数组*/
        HashMapEntry<K, V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        int newCapacity = oldCapacity * 2;
        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
        if (size == 0) {
            return newTable;
        }

        for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;
            
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }
        return newTable;
    }</span>
下面再看看java.util.HashMap.get(Object key)方法源码:

<span style="font-size:18px;">    /**
     * Returns the value of the mapping with the specified key.
     *
     * @param key
     *            the key.
     * @return the value of the mapping with the specified key, or {@code null}
     *         if no mapping for the specified key is found.
     */
    public V get(Object key) {
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            return e == null ? null : e.value;
        }

        // Doug Lea's supplemental secondaryHash function (inlined).
        // Replace with Collections.secondaryHash when the VM is fast enough (http://b/8290590).
        int hash = key.hashCode();
        hash ^= (hash >>> 20) ^ (hash >>> 12);
        hash ^= (hash >>> 7) ^ (hash >>> 4);

        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }</span>
在HashMap.get方法可以发现HashMap在查找Key的过程中,如果是自定义类作为Key对象的话,通常要求重写equals方法和hashCode方法,并且equals方法和hashCode方法判断标准必须保持一致,否则将出现冲突。即在使用自定义类作为HashMap中Key对象,HashMap判断两个Key相等的标准是:两个Key通过equals()方法比较返回true,两个Key的hashCode值也必须相等。

与此同时,我们再看看HashTable的源码,可以发现HashTable与HashMap有一部分方式的底层实现差不多,在此就不详述,主要看下两者在底层实现中的一些区别。

 java.util.Hashtable.put(K key, V value)源码:

<span style="font-size:18px;">    /**
     * Associate the specified value with the specified key in this
     * {@code Hashtable}. If the key already exists, the old value is replaced.
     * The key and value cannot be null.
     *
     * @param key
     *            the key to add.
     * @param value
     *            the value to add.
     * @return the old value associated with the specified key, or {@code null}
     *         if the key did not exist.
     * @see #elements
     * @see #get
     * @see #keys
     * @see java.lang.Object#equals
     */
    public synchronized V put(K key, V value) {
        if (key == null) {
            throw new NullPointerException("key == null");
        } else if (value == null) {
            throw new NullPointerException("value == null");
        }
        int hash = Collections.secondaryHash(key);
        HashtableEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashtableEntry<K, V> first = tab[index];
        for (HashtableEntry<K, V> e = first; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for key is present; create one
        modCount++;
        if (size++ > threshold) {
            rehash();  // Does nothing!!
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
            first = tab[index];
        }
        tab[index] = new HashtableEntry<K, V>(key, value, hash, first);
        return null;
    }</span>
HashTable.put方法与HashMap.put方法基本实现逻辑相同,主要区别是HashTable.put方法被synchronized关键字修饰,因此HashTable是线程安全的。同时如果Key为NULL的话,在HashTable.put方法中将会报异常,所以在HashTable中Key不能为NULL,而我们在前面的分析中可以发现HashMap.put方法中Key是可以为NULL的。

下面给出java.util.Hashtable.get(Object key)源码,亦可以发现相似结果,故就不具体分析。

<span style="font-size:18px;">    /**
     * Returns the value associated with the specified key in this
     * {@code Hashtable}.
     *
     * @param key
     *            the key of the value returned.
     * @return the value associated with the specified key, or {@code null} if
     *         the specified key does not exist.
     * @see #put
     */
    public synchronized V get(Object key) {
        int hash = Collections.secondaryHash(key);
        HashtableEntry<K, V>[] tab = table;
        for (HashtableEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }</span>
      HashMap源码(JDK1.7,含注释)      HashTable源码(JDK1.7,含注释)




以上是关于从Java源码的角度来分析HashMap与HashTable的区别的主要内容,如果未能解决你的问题,请参考以下文章

Android Gems — Java源码分析之HashMap和SparseArray

Android Gems — Java源码分析之HashMap和SparseArray

Java BAT大型公司面试必考技能视频-1.HashMap源码分析与实现

HashMap源码分析与实现

3 手写Java HashMap核心源码

手写Java HashMap核心源码