• [源码解析]HashMap和HashTable的区别(源码分析解读)


    [源码解析]HashMap和HashTable的区别(源码分析解读)

     

    前言: 
    又是一个大好的周末, 可惜今天起来有点晚, 扒开HashMap和HashTable, 看看他们到底有什么区别吧.

    先来一段比较拗口的定义:

    复制代码
    Hashtable 的实例有两个参数影响其性能:初始容量 和 加载因子。容量 是哈希表中桶 的数量,初始容量 就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。

      而HashTable是 基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适   当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
      

    复制代码

    一, 实例举证

    复制代码
     1     
     2     public static void main(String[] args) {
     3         Map<String, String> map = new HashMap<String, String>();
     4         map.put("a", "aaa");
     5         map.put("b", "bbb");
     6         map.put("c", "ccc");
     7         map.put("d", "ddd"); 
     8         Iterator<String> iterator = map.keySet().iterator();
     9         while (iterator.hasNext()) {
    10             Object key = iterator.next();
    11             System.out.println("map.get(key) is :" + map.get(key));
    12         }
    13 
    14         Hashtable<String, String> tab = new Hashtable<String, String>();
    15         tab.put("a", "aaa");
    16         tab.put("b", "bbb");
    17         tab.put("c", "ccc");
    18         tab.put("d", "ddd");  
    19         Iterator<String> iterator_1 = tab.keySet().iterator();
    20         while (iterator_1.hasNext()) {
    21             Object key = iterator_1.next();
    22             System.out.println("tab.get(key) is :" + tab.get(key));
    23         }
    24     }
    25 }
    复制代码

    首先上面有这么一段代码, 那么它的输出是什么呢? 

    可以看到, HashMap按照正常顺序输出, 而HashTable输出的顺序却有些诡异.

    2, 源码分析
    看到上面的结果, 那么我们就分别来看下HashMap和HashTable的源码吧.

    首先我要来灌输一些思想, 然后再根据这些定义的规则(前人总结出来的) 再去源码中一探究竟.

    1)HashTable是同步的,HashMap是非同步的
    HashTable中put和get方法:

    复制代码
     1 public synchronized V put(K key, V value) {
     2         // Make sure the value is not null
     3         if (value == null) {
     4             throw new NullPointerException();
     5         }
     6 
     7         // Makes sure the key is not already in the hashtable.
     8         Entry<?,?> tab[] = table;
     9         int hash = key.hashCode();
    10         int index = (hash & 0x7FFFFFFF) % tab.length;
    11         @SuppressWarnings("unchecked")
    12         Entry<K,V> entry = (Entry<K,V>)tab[index];
    13         for(; entry != null ; entry = entry.next) {
    14             if ((entry.hash == hash) && entry.key.equals(key)) {
    15                 V old = entry.value;
    16                 entry.value = value;
    17                 return old;
    18             }
    19         }
    20 
    21         addEntry(hash, key, value, index);
    22         return null;
    23     }
    复制代码
    复制代码
     1 public synchronized V get(Object key) {
     2         Entry<?,?> tab[] = table;
     3         int hash = key.hashCode();
     4         int index = (hash & 0x7FFFFFFF) % tab.length;
     5         for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
     6             if ((e.hash == hash) && e.key.equals(key)) {
     7                 return (V)e.value;
     8             }
     9         }
    10         return null;
    11     }
    复制代码

    HashMap中put和get方法:

    1 public V put(K key, V value) {
    2       return putVal(hash(key), key, value, false, true);
    3 }
    1 public V get(Object key) {
    2         Node<K,V> e;
    3         return (e = getNode(hash(key), key)) == null ? null : e.value;
    4 }

    从以上代码中就能显而易见的看到HashTable中的put和get方法是被synchronized修饰的, 这种做的区别呢? 
    由于非线程安全,效率上可能高于Hashtable. 如果当多个线程访问时, 我们可以使用HashTable或者通过Collections.synchronizedMap来同步HashMap。


    2)HashTable与HashMap实现的接口一致,但HashTable继承自Dictionary,而HashMap继承自AbstractMap;
    HashTable:

     HashMap:
     

    3)HashTable不允许null值(key和value都不可以) ,HashMap允许null值(key和value都可以)。

     在1中我们可以看到HashTable如果value为null就会直接抛出: throw new NullPointerException();
     那么再看看HashMap put value 具体做了什么?

    复制代码
    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;
    }
    复制代码

    由此可见, 并没有value值进行强制的nullCheck.

    4)HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。
    这里我们可以直接对比HashMap和HashTable有关Contains的方法:

    HashTable中的contains方法在HashMap中就被取消了, 那么我们来具体看下HashTable中的contains方法的作用: 

    复制代码
     1 public synchronized boolean contains(Object value) {
     2         if (value == null) {
     3             throw new NullPointerException();
     4         }
     5 
     6         Entry<?,?> tab[] = table;
     7         for (int i = tab.length ; i-- > 0 ;) {
     8             for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
     9                 if (e.value.equals(value)) {
    10                     return true;
    11                 }
    12             }
    13         }
    14         return false;
    15 }
    复制代码

    然后再看下HashTable中的containsValue方法:

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

    这里就很明显了, contains方法其实做的事情就是containsValue, 里面将value值使用equals进行对比, 所以在HashTable中直接取消了contains方法而是使用containsValue代替.

    5)HashTable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。


    首先是HashTable中:

     View Code

    然后是HashMap中:

     View Code

    废弃的接口:Enumeration 
    Enumeration接口是JDK1.0时推出的,是最好的迭代输出接口,最早使用Vector(现在推荐使用ArrayList)时就是使用Enumeration接口进行输出。虽然Enumeration是一个旧的类,但是在JDK1.5之后为Enumeration类进行了扩充,增加了泛型的操作应用。

    Enumeration接口常用的方法有hasMoreElements()(判断是否有下一个值)和 nextElement()(取出当前元素),这些方法的功能跟Iterator类似,只是Iterator中存在删除数据的方法,而此接口不存在删除操作。

    为什么还要继续使用Enumeration接口
    Enumeration和Iterator接口功能相似,而且Iterator的功能还比Enumeration多,那么为什么还要使用Enumeration?这是因为java的发展经历了很长时间,一些比较古老的系统或者类库中的方法还在使用Enumeration接口,因此为了兼容,还是需要使用Enumeration。

    下面给出HashTable和HashMap的几种遍历方式:

     Person.java
     Test.java

    6)HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

    HashMap:

    1 /**
    2      * The default initial capacity - MUST be a power of two.
    3      */
    4     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    HashTable:通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。

    1  // 默认构造函数。
    2 public Hashtable() {
    3     // 默认构造函数,指定的容量大小是11;加载因子是0.75
    4     this(11, 0.75f);
    5 }

    7)哈希值的使用不同
    HashTable:,HashTable直接使用对象的hashCode

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

    HashMap:HashMap重新计算hash值,而且用与代替求模:

    复制代码
    1 int hash = hash(k);
    2 int i = indexFor(hash, table.length);
    3 static int hash(Object x) {
    4 h ^= (h >>> 20) ^ (h >>> 12);
    5      return h ^ (h >>> 7) ^ (h >>> 4);
    6 }
    7 static int indexFor(int h, int length) {
    8 return h & (length-1);
    9 }
    复制代码

    3,其他关联
    3.1HashMap与HashSet的关系

    a、HashSet底层是采用HashMap实现的:

    1 public HashSet() {
    2     map = new HashMap<E,Object>();
    3 }

    b、调用HashSet的add方法时,实际上是向HashMap中增加了一行(key-value对),该行的key就是向HashSet增加的那个对象,该行的value就是一个Object类型的常量。

    1 private static final Object PRESENT = new Object(); public boolean add(E e) { 
    2     return map.put(e, PRESENT)==null; 
    3 } 
    4 public boolean remove(Object o) { 
    5     return map.remove(o)==PRESENT; 
    6 }

    3.2 HashMap 和 ConcurrentHashMap 的关系

    关于这部分内容建议自己去翻翻源码,ConcurrentHashMap 也是一种线程安全的集合类,他和HashTable也是有区别的,主要区别就是加锁的粒度以及如何加锁,ConcurrentHashMap 的加锁粒度要比HashTable更细一点。将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

    更多请参考: http://www.hollischuang.com/archives/82


    4. HashTable源码奉上

     View Code
  • 相关阅读:
    Redis键是如何过期删除的
    使用EventBus + Redis发布订阅模式提升业务执行性能(下)
    使用EventBus + Redis发布订阅模式提升业务执行性能
    使用C#的计时器加观察者模式完成报警推送需求
    Go 语言入门教程:变量
    Go 语言入门教程:安装
    Docker 入门:Dockerfile
    Docker 入门:容器
    Docker 入门:什么是 Docker ?
    Windows 系统如何安装 Docker
  • 原文地址:https://www.cnblogs.com/du-0210/p/8426600.html
Copyright © 2020-2023  润新知