JDK源码分析(10)之 Hashtable 相关
本文的目的并不是让你对hashtable
更加了解,然后灵活运用;因为hashtable
的一个历史遗留的类,目前并不建议使用,所以本文主要和hashmap
对比,感受同样功能的不同实现,知道什么是好的代码;所以在阅读本文之前最好先了解一下 hashmap
,可以参考 hashmap 相关;
一、 类定义
public class hashtable<k,v> extends dictionary<k,v> implements map<k,v>, cloneable, java.io.serializable
可以看到它和hashmap
虽然都是哈希表,但是结构完全不一样,他是继承于dictionary
;
/** * maps the specified <code>key</code> to the specified * <code>value</code> in this dictionary. neither the key nor the * value can be <code>null</code>. */ abstract public v put(k key, v value); abstract public enumeration<k> keys(); abstract public enumeration<v> elements(); public interface enumeration<e> { boolean hasmoreelements(); e nextelement(); }
同abstractmap
相比功能结构基本一样,但是有两点很重要的区别:
-
hashtable
要求 key 和 value,都不能为 null,也就意味着这每次 put 元素的时候都需要判空,真是想想都好痛苦; - 另外 keys 和 elements 返回的居然是
enumeration
,这也是一个比较古老的接口,用于枚举(一次获得一个)对象集合中的元素;但是目前大多已经被iterator
给取代了;
二、构造方法和成员变量
private transient entry<?,?>[] table; // 哈希槽 private int threshold; // 阈值 private float loadfactor; // 负载系数
以上三个应该就是 map 中最重要的成员变量了,阈值和负载系数控制扩容时机,同hashmap
的作用是一样的,哈希槽也是一样的,但是注意entry<?,?>[]
这里用的居然是通配符,而不是k v
,也就意味着在取 entry 的时候,还需要强转类型,这也是非常神奇的地方;
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(int initialcapacity) { this(initialcapacity, 0.75f); } public hashtable() { this(11, 0.75f); } public hashtable(map<? extends k, ? extends v> t) { this(math.max(2*t.size(), 11), 0.75f); putall(t); }
如代码所示四个构造函数,主要就是为了初始化以上三个成员变量,但是注意table
的容量;熟悉hashmap
的肯定知道,hashmap
的容量要求是2的幂,目的是为了使用hash % length = hash & (length-1)
,优化哈希槽的定位;但是如上面代码所示hashtable
的容量却不是,初始容量默认11,扩容是2倍加1;这样做的优缺点有什么呢:
- 缺点,不能利用“与”来优化哈希槽定位;
- 优点,减小了哈希冲突的几率(hashmap 的容量虽然是偶数,但是对哈希做了高位与低位,以及红黑树,使得即使hash冲突十分严重,性能也能得以保证),详情可以参考 ;
三、重要方法
1. 哈希槽定位
int index = (hash & 0x7fffffff) % tab.length;
哈希表中最重要的方法肯定是哈希槽定位,如上面的原因hashtable
寻址的时候并不能做优化,所以只是用的典型除留余数法,(hash & 0x7fffffff)
则是为了保证第一位符号位是0,也就是正数,保证最终的余数是正数;
2. get 方法
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
的所有方法都是synchronized
修饰的,所以hashtable
是线程安全的容器;
代码很简单,就是得到哈希,计算哈希桶,再一次遍历链表;但是需要注意:
-
int hash = key.hashcode();
,这里是直接取的 key 的 hashcode,所以这里不能避免极端哈希的情况; - 另外就是不能使用可变 key (所有容器都不能使用可变 key),例如:
private static class a { string name; public a(string name) {this.name = name;} @override public boolean equals(object o) { ... } @override public int hashcode() { ... } } private static void test01() { map<a, string> map = new hashtable<>(); a a1 = new a("a"); a a2 = new a("a"); map.put(a1, "a"); map.put(a2, "a"); system.out.println(map.get(a1)); a1.name = "b"; system.out.println(map.get(a1)); }
// 打印:
a
null
3. put 方法
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; }
hashtable
的put
方法和hashmap
相比,就显得十分清晰,先判空,在查找,找到就替换,找不到就插入新节点;但是在插入顺序(后面会讲到),红黑树性能保证等方面也就不能和hashmap
相比了;另外这里取出来的entry
也是进行了类型强制转换;
4. addentry 方法
private void addentry(int hash, k key, v value, int index) { modcount++; entry<?,?> tab[] = table; if (count >= threshold) { // rehash the table if the threshold is exceeded rehash(); tab = table; hash = key.hashcode(); index = (hash & 0x7fffffff) % tab.length; } // creates the new entry. @suppresswarnings("unchecked") entry<k,v> e = (entry<k,v>) tab[index]; tab[index] = new entry<>(hash, key, value, e); count++; } private static class entry<k,v> implements map.entry<k,v> { final int hash; final k key; v value; entry<k,v> next; protected entry(int hash, k key, v value, entry<k,v> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } ... }
这里添加元素的时候首先判断是否扩容,然后添加节点;值得注意的是添加的节点是直接放在哈希槽里的(tab[index] = new entry<>(hash, key, value, e);
)大部分的 map 实现都是将添加的节点放在链表尾部;所以hashtable
中节点的相对顺序是不断变化的;
5. rehash 方法
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++; threshold = (int)math.min(newcapacity * loadfactor, max_array_size + 1); table = 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; } } }
扩容的时候也是,先计算新容量,在得到一个新的哈希槽,然后将节点在依次放入;同添加节点一样是将节点直接放到哈希槽中,那么在扩容完毕之后,链表的相对顺序会反向;
总结
-
hashtable
的 key 和 value 都不能为 null,在使用的时候需要判空。。。。蛋疼 - 哈希值完全依赖 key 的
hashcode
方法,所以在使用的时候,需要额外注意 -
hashtable
的容量可以是任意值,默认是11,不能使用“与”来优化寻址 -
hashtable
的节点相对位置是不断变化的; -
hashtable
是线程安全的;
推荐阅读
-
JDK源码分析(10)之 Hashtable 相关
-
JDK源码分析之String、StringBuilder和StringBuffer
-
JDK源码分析之String、StringBuilder和StringBuffer
-
JDK源码分析(6)之 LinkedHashMap 相关
-
JDK源码分析(10)之 Hashtable 相关
-
源码分析--ConcurrentHashMap与HashTable(JDK1.8)
-
JDK源码分析(7)之 Reference 框架概览
-
JDK源码分析(9)之 WeakHashMap 相关
-
JDK源码分析(8)之 Reference 完全解读
-
JDK源码分析(12)之 ConcurrentHashMap 详解