HashMap与HashTable的区别


1.前言

        HashMap与HashTable的区别是面试过程中最容易被问到的问题,如果直接背,很容易就忘记,所以自己通过对源码的了解来总结出HashMap与HashTable的不同,如果与别的文章有雷同,纯属巧合。
        JDK版本:1.8

2.HashMap与HashTable的区别

2.1继承的父类不同

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

        看上面的源码,HashTable继承的是Dictionary类,而HashMap继承的是AbstractMap类。不过两者都实现了Map、Clonable和Serializable接口。

2.2线程安全性不同

        HashTable是线程安全的,内部的方法基本都经过synchronized关键字进行修饰,而HashMap是线程不安全的。正是因为加了锁的原因,HashTable的效率低于HashMap。另外,HashTable 基本被淘汰,不要在代码中使用它;

2.3初始化和扩容机制不同

HashTable的初始化:

  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);
    }
    public Hashtable() {
        this(11, 0.75f);
    }

        看上面HashTable的源码,当我们初始化一个HashTable对象时,如果不指定初始化大小和加载因子,那么默认初始化大小为11,加载因子为0.75,如上面第11、12和16行代码。
HashMap的初始化:

 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

        如上面的源码,当创建一个HashMap实例对象时,如果不指定初始容量和加载因子时,默认的加载因子为0.75,这时并没有创建table。当用户向HashMap中插入数据时,才会创建table。

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

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

        当用户向HashMap中插入数据时会调用put方法,而put方法会调用putVal方法实现插入数据。看putVal方法中的第3行代码和第4行代码,一开始时table是为null或者table的长度为0时,就会调用resize方法。putVal方法中倒数第四行代码,此时是HashMap需要扩容时调用的代码。即resize是HashMap初始化和扩容的具体实现方法。

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

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        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
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        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);
        }

        一开始的时候table是空的,即resize方法中oldTab为空,那么oldCap为0,即一开始调用resize方法时会执行倒数第九行代码处,而DEFAULT_INITIAL_CAPACITY的值刚好就是16,所以HashMap初始化的大小就是16个。
HashTable的扩容机制:

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

        HashTable的扩容机制是通过rehash方法实现的,每次扩容时先获取HashTable的长度,然后向左移一位并加1,即扩容到原来的两倍加1
HashMap的扩容机制:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        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的扩容依然调用resize函数实现的,每次需要扩容时,先获取HashMap原来的长度,然后再向左移动一位,即扩容为原来的两倍。如上面倒数第三行代码。

2.4对null值支持不同

        在HashTable中,key和value都不能为null值;在HashMap中,key可以为null,但只能允许存在一个,而value不仅可以为null,还可以存在多个。

2.5对外的接口不同

        HashMap中没有contains方法,但有containsValue方法和containsKey方法;HashTable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。除此之外,HashTable还多提供了elments() 方法,该方法继承自HashTable的父类Dictionnary。elements() 方法用于返回此HashTable中的value的枚举。

2.6底层数据结构不同

        在jdk1.7版本中,HashTable与HashMap底层都是数组+链表。到了JDK8版本中,HashMap引入了红黑树的结构。如果entry数组中某个位置链表的冲突数据数量小于8时,则以链表的形式存在;如果冲突数据的数量大于8时,则将链表转换成红黑树存储,当冲突数量小于6时,又转为链表存储。

2.7支持的遍历种类不同

        HashMap只支持Iterator遍历,而HashTable支持Iterator和Enumeration 两种方式遍历。
        HashMap的迭代器(Iterator)是fail-fast迭代器,而HashTable的Enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常,而HashTable 则不会。

2.8计算hash值的方式不同

HashTable的哈希值计算:

@SuppressWarnings("unchecked")
    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;
    }

        HashTable的hash值直接使用的key的hash值,不过需要去掉符号位(即& 0x7FFFFFFF),获取数据的下标时只需要对HashTable的长度进行取模。
HashMap的哈希值计算:

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

 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;

HashMap的哈希值计算则是先判断key是否为空,如果为空,哈希值直接为0,否则先获取key的哈希值,然后在异或上右移16位的key的哈希值(即h >>> 16)。而HashMap的下标计算公式为:(table.length-1)&hash。

3.总结

两者的不同点:

  • 继承的父类不同。HashTable继承的是Dictionary类,而HashMap继承的是AbstractMap类。
  • 线程安全性不同。HashTable是线程安全的,而HashMap是线程不安全的。HashTable在方法上面加上了synchronized关键字。也因如此,HashTable的效率不如HashMap。
  • 初始容量和扩容机制不同。HashTable默认初始化大小为11,加载因子为0.75,每次扩容时扩大为原来的2倍+1;HashMap默认初始化大小为16,加载因子为0.75,每次扩容时扩大为原来的2倍。
  • 对null值支持不同。HashTable的key和value都不能为null;HashMap的key可以为null,但只能存在一个,而可以有多个value为null。
  • 对外的接口不同。HashTable比HashMap多了elements和contains方法,contains方法与containValue方法一致。
  • 底层数据结构不同。在JDK1.7时,两者底层都是采用数组+链表的形式。到了JDK1.8,HashMap引入了红黑树的结构。当链表长度大于8时,链表转换为红黑树存储,数据量小于6时才会转回链表。
  • 支持的遍历种类不同。HashMap只支持Iterator遍历,而HashTable支持Iterator和Enumeration 两种方式遍历。
  • 计算hash值的方式不同。HashTable的hash值使用的是key的hash值(去掉符号位),而HashMap则是通过计算生成(key.hash^(key.hash>>>16))。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值