HashTable与HashMap区别与联系

HashTable与HashMap的区别于联系是在面试中经常遇到的问题,最近被这个问题困扰,看了几篇blog,总结一下。

1.出现的时间

出现的时间来说,HashTable产生于JDK 1.1,而HashMap产生于JDK 1.2。从时间的维度上来看,HashMap要比HashTable出现得晚一些。其实现在HashTable已经废弃了,如果现在写代码,都不用HashTable了,至于为什么要一直更新,因为前面有很多代码用到,现在必须对其升级维护。

 

2.继承的类与接口以及对外接口(API)

从上面的图可以看到,虽然都实现了Map、Cloneable、Serializable三个接口。但是HashMap继承自抽象类AbstractMap,而HashTable继承自抽象类Dictionary。其中Dictionary类是一个已经被废弃的类不过他们的底层实现基本是一致的,都是基于数组(Entry类型)加链表这种数据结构实现的,称为“拉链法”实现哈希表,如下图所示

同时我们看到HashTable比HashMap多了两个公开方法。一个是elements,这来自于抽象类Dictionary,鉴于该类已经废弃,所以这个方法也就没什么用处了。另一个多出来的方法是contains,这个多出来的方法也没什么用,因为它跟containsValue方法功能是一样的。

以下代码及注释来自java.util.HashTable

 public synchronized boolean contains(Object value) {
     if (value == null) {
         throw new NullPointerException();
     }

     Entry tab[] = table;
     for (int i = tab.length ; i-- > 0 ;) {
         for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {
             if (e.value.equals(value)) {
                 return true;
             }
         }
     }
     return false;
 }

 public boolean containsValue(Object value) {
     return contains(value);
 }

所以从公开的方法上来看,这两个类提供的,是一样的功能。都提供键值映射的服务,可以增、删、查、改键值对,可以对建、值、键值对提供遍历视图。支持浅拷贝,支持序列化。

3.两者的构造方法

HashTable和HashMap的构造方法:两者的构造方法都是一样的,以下4种(以HashMap为例)。

HashMap()
构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。
HashMap(int initialCapacity)
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
HashMap(int initialCapacity, float loadFactor)
构造一个空的 HashMap具有指定的初始容量和负载因子。
HashMap(Map<? extends K,? extends V> m)
构造一个新的 HashMap与指定的相同的映射 Map 。

HashMap的一个实例有两个影响其性能的参数: 初始容量(initialCapacity)和负载因子(loadFactor) 。 容量是哈希表中的桶(bucket)数,初始容量只是创建哈希表时的容量。 负载因子是在容量自动增加之前允许哈希表得到满足的度量。 当在散列表中的条目的数量超过了负载因数和初始容量的乘积,哈希表被重新散列 (即内部数据结构被重建),使得哈希表具有桶的大约两倍。

作为一般规则,默认负载因子(0.75)提供了时间和空间成本之间的良好折中。 更高的值会降低空间开销,但会增加查找成本(反映在HashMap类的大部分操作中,包括get和put )。 在设置其初始容量时,应考虑地图中预期的条目数及其负载因子,以便最小化重新散列操作的次数。 如果初始容量大于最大条目数除以负载因子,则不会发生重新排列操作。

4、HashTable与HashMap主要方法的实现

HashTable和HashMap的API中提供了很多的方法,其中主要两个方法是put()和get():

put()方法:

思路:

<1>根据key计算可以key的hashCode,然后计算出hashCode在数组中对应的bucket的位置,如果该位置的key为null,那么在这个位置创建一个新的Entry类型的Node:

if ((p = tab[i = (n - 1) & hash]) == null)
   tab[i] = newNode(hash, key, value, null);

<2>如果这个位置的key不为null,那么就要判断待添加的元素和该位置的元素是否相同,如果相同的话,就把key对应的旧节点e替换为新的节点:

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;

<3>如果该节点是一个树节点的实例,那说明HashMap的底层已经转为数组加红黑树的结构,那么把该节点添加到红黑树中:

else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

<4>如果以上都不是,那么开始对该位置指向的链表遍历,对每一个节点,如果他们的hashCode相同且key相同,那么退出遍历,该节点就是要被替换成新节点的节点e,对于这个节点,做如下处理:将该节点的value换成新的value,并返回旧的value

if (e != null) 
{ // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}

<5>如果遍历完该位置的链表都没有找到重复的节点(hashCode一致且key相同),那么在链表尾部添加新节点,并且如果添加新节点后,链表长度大于TREEIFY_THRESHOLD那么将链表转换为红黑树的结构:

if ((e = p.next) == null) {
    p.next = newNode(hash, key, value, null);
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        treeifyBin(tab, hash);
    break;
 }

在JDK1.8.0_102中,TREEIFY_THRESHOLD等于8,也就是说,当链表长度大于8时,将链表转换为红黑树的结构,这样当该位置(bucket)的节点数很多时,访问效率会提高很多。

<6>最后,modcount要加1,因为modcount是用来记录HashMap的修改操作的次数的,用于检测该HashMap是否被多线程修改,如果多线程的modcount不一致,说明该HashMap被多线程修改,那么立刻抛出异常。然后,如果size(节点数/键值对数)大于threshold,那么就要进行扩容resize()。

HashMap源码:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashTable源码:因为HashTable的key和value都不允许为null,并且HashTable不会转换为红黑树的结构,因此要简单很多

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        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];
        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);
        return null;
    }

在put()方法中,有两个地方需要注意

<1>在put()一个键值对时,先计算hashCode,如果hashCode相同再判断key和已有的key是否相同,在判断是否相同时,有两种情况,一个是添加的key和原有的key是同一个对象,或者不是同一个对象但是equals()返回true,这两种情况都是判断相同的。此外,从源代码中可以看出HashMap的key可以为null:

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;

但是HashTable的key是不能为null的,因为HashTable中直接调用key的hashCode()

方法,如果key为null会抛出异常:

if (value == null) {
    throw new NullPointerException();
}

<2>扩容:HashTable使用rehash(),HashMap使用resize(),当键值对的数量超过threshold=initialCapacity*loadFactor 的时候,就会自动调用rehash()或者resize()对table进行扩容,并重新组织table,以便减少hash冲突,使HashTable或HashMap更有效率。

HashMap源码:

因为要判断oldTab是否为null或者长度为0,所以HashMap的新容量的计算相对麻烦一点。HashMap的新容量=旧容量*2,新threshold = 旧threshold*2。然后在oldTab中从头向后逐个取出节点,根据节点key的hashCode计算节点在newTab中的位置,并把oldTab中的节点指向null。

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //判断oldTab是否为null,然后返回oldTab的长度
        int oldThr = threshold;
        int newCap, newThr = 0;
        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; // double threshold ,HashMap的新容量=旧容量*2,新threshold = 旧threshold*2
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;   //如果旧容量==0, 且就threshold大于0,那么新容量等于旧threshold
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;   //否则设置为默认值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  //根据新容量创建newTab
        table = newTab;
       //从oldTab中从前向后逐个取出节点,计算节点key的hashCode在newTab中的位置
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

HashTable源码:

HashTable扩容后 新容量是原容量的2倍+1,新threshold为新容量*loadFactor。然后在oldMap中从后向前取出节点,重新计算节点在newMap中的hashCode,放进newMap中,并把oldMap中的节点指向null。

/**
     * 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() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;
 
        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
 
        modCount++;  //扩容也是修改操作,modcount加1
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
        //把oldMap的节点重新计算hashCode放进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;
            }
        }
    }

get()方法:

HashMap源码:根据给出的key计算hashCode,然后计算在该HashMap中对应的bucket位置,如果找到fail位置,并且key.equals()返回true,那么返回该可以映射的value,如果没有找到符合条件的key,那么返回null,但是这里要说明,返回null也不一定是没有找到符合条件的null,也可能是这个key映射的value本来就是null,那么这个时候就需要用containsKey()这个方法来区别这两种情况。

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
 
    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    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;
                //如果first不是符合条件的key,那么对后面的bucket,如果是树,则查询树的节点key是否
                //符合条件,如果是链表,则遍历链表中的节点,查看是否有符合条件的key
                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; //如果没有查找到则返回null
    }

HashTable源码:因为HashTable的key和value都不允许为null,并且HashTable不会转换为红黑树的结构,因此要简单很多,只要遍历每个bucket的链表,查询是否有符合条件的key即可。

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;
    }

5、总结

<1>继承的类不同:HashTable继承Dictionary类,HashMap继承AbstractMap类;

<2>线程安全级别不同:HashTable是线程安全的类,每个方法都有Synchronized修饰,HashMap不是线程安全的,如果想要用HashMap创建线程安全的对象,那么可以用Collections接口的SynchrinizedMap()方法封装一个HashMap的对象,使之成为线程安全的对象。由于HashMap非线程安全,在只有一个线程访问的情况下,效率要高于HashTable。

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

<3>是否允许null的要求不同

HashTable:key不允许为null,value不允许为null

HashMap:key允许为null,value允许为null

<4>在JDK1.8以后,HashMap在节点数大于TREEIFY_THRESHOLD(等于8)时,底层实现从数组加链表转换为数组加红黑树;但是HashTable没有这种操作。

<5>HashMap把HashTable的contains方法去掉了,改成containsvalue和containsKey,因为contains方法容易让人引起误解。而Hashtable有contains方法、containsvalue方法和containsKey方法,其中contains方法和containsvalue方法是一样的。
<6>两者通过hash值散列到hash表的算法不一样:

HashTbale是古老的除留余数法,直接使用hashcode

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

而HashMap是强制容量为2的幂,重新根据hashcode计算hash值,在使用hash 位与 (hash表长度 – 1),也等价取模,但更加高效,取得的位置更加分散,偶数、奇数保证了都会分散到。前者就不能保证。

int hash = hash(k);  
int i = indexFor(hash, table.length);  
  
static int hash(Object x) {  
  int h = x.hashCode();  
  
  h += ~(h << 9);  
  h ^= (h >>> 14);  
  h += (h << 4);  
  h ^= (h >>> 10);  
  return h;  
}  
static int indexFor(int h,int length) {  
  return h & (length-1); 
}

<7>HashMap的迭代器(Iterator)是fail-fast迭代器,而HashTable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。
*关于fail-fast机制:fail-fast是通过modcount参数实现的,在HashMap中每当有会修改HashMap结构的操作被执行,那么modcount加1,modcount是volatile的,因此是线程可见的,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。而HashTable并没有这个参数,因此没有fail-fast机制。

 

 

参考博客

http://zhaox.github.io/2016/07/05/hashmap-vs-hashtable

https://blog.csdn.net/tongdanping/article/details/79511898

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值