「源码解析」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 map = new HashMap(); 4 map.put("a", "aaa"); 5 map.put("b", "bbb"); 6 map.put("c", "ccc"); 7 map.put("d", "ddd");8 Iterator 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 tab = new Hashtable();15 tab.put("a", "aaa");16 tab.put("b", "bbb");17 tab.put("c", "ccc");18 tab.put("d", "ddd");19 Iterator 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 }首先上面有这么一段代码, 那么它的输出是什么呢?
https://p5.toutiaoimg.com/large/pgc-image/afdf58a037c34a8d90f9f6ea968c1c8c
可以看到, 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 } 67 // 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 entry = (Entry)tab;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 ; 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 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:
https://p26.toutiaoimg.com/large/pgc-image/2e45333c23d04b9ab5cc13789cc4e337
HashMap:
https://p6.toutiaoimg.com/large/pgc-image/1c896da332184f15bd63fc5d1a8b897b
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[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab) == null) tab = newNode(hash, key, value, null); else { Node 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)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的方法:
https://p5.toutiaoimg.com/large/pgc-image/27201db3807f451c9720c7d1d0e1f572
https://p26.toutiaoimg.com/large/pgc-image/fdda6131c1d24b8b85e46414db221d68
HashTable中的contains方法在HashMap中就被取消了, 那么我们来详细看下HashTable中的contains方法的作用:
1 public synchronized boolean contains(Object value) { 2 if (value == null) { 3 throw new NullPointerException(); 4 } 56 Entry tab[] = table; 7 for (int i = tab.length ; i-- > 0 ;) { 8 for (Entry e = tab ; 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 >> 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();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源码奉上
1 package java.util;2 3 import java.io.*;4 5 public class Hashtable extends Dictionary implements Map, Cloneable, java.io.Serializable {6 7 // Hashtable保存key-value的数组。8 // Hashtable是采用拉链法实现的,每一个Entry本质上是一个单向链表9 private transient Entry[] table; 1011 // Hashtable中元素的现实数量 12 private transient int count; 1314 // 阈值,用于判断是否需要调整Hashtable的容量(threshold = 容量*加载因子) 15 private int threshold; 1617 // 加载因子 18 private float loadFactor; 1920 // Hashtable被改变的次数 21 private transient int modCount = 0; 2223 // 序列版本号 24 private static final long serialVersionUID = 1421746759512286392L; 2526 // 指定“容量大小”和“加载因子”的构造函数 27 public Hashtable(int initialCapacity, float loadFactor) { 28 if (initialCapacity < 0) 29 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); 30 if (loadFactor
页:
[1]