Hashtable
Hashtable的應用非常廣泛,HashMap是新框架中用來代替HashTable的類,至於這二者的區別,從二者關係圖中可見一般,更多的區別,來分析二者的實現源碼吧!
先來看一下二者都實現了的接口Map的定義:
package java.util;
public interface Map<K,V> {
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value;
V get(Object key);
V put(K key, V value);
V remove(Object key);
void putAll(Map<? extends K, ? extends V> t);
void clear();
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
boolean equals(Object o);
int hashCode();
}
Map接口定義了實現該接口的容器存儲的值爲鍵值對。比較吸引我眼球的是Map接口中又定義了一個接口Entry<K,V>,
從後面的實現代碼分析中可以看出具體容器都是實現了該接口作爲容器的內部數據結構的。
下面是Hashtable內部的數據結構:
private static class Entry<K,V> implements Map.Entry<K,V> {
int hash;//該節點的hash值
K key; //鍵
V value;//值
Entry<K,V> next;//如果產生Hash衝突,該節點指向衝突鏈的下一個節點
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
protected Object clone() {
return new Entry<K,V>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ (value==null ? 0 : value.hashCode());
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
這是一個靜態內部類,很明顯它實現了接口Map中的Entry接口:
Hashtable的內部數據結構便是一個Entry數組。
private transient Entry[] table;//Hashtable內部數據結構聲明
下面通過分析Hashtable的put()方法來一窺Hash類容器的特點:
public synchronized V put(K key, V value) {
//確定value非空,若爲空,則拋出NullPointerException
if (value == null) {
throw new NullPointerException();
}
// 接下來判斷傳入的key是否已經存在hashtable中,而這需要判斷hashCode是否相等以及equal()是否爲true
Entry tab[] = table;
//得到當前key的hash值,如果Key爲Null,也將拋出NullPointerException
int hash = key.hashCode();
//根據hash值算出索引
int index = (hash & 0x7FFFFFFF) % tab.length;
//判斷table[index]處是否已經非空
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
//如果table[index]非空
//判斷table[index]處key的hash值與傳入的key的hash值是否相等
// 如果二者hash值相等,則判斷equals()是否爲真
if ((e.hash == hash) && e.key.equals(key)) {
//如果通過了判斷,則說明table[index]處的key和傳入的key是相同的,用傳入的value替換table[index]的value
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
/*
以下這個if判斷元素個數是否超過閾值,如果超過則需要申請更大的空間
執行到此並沒有出現count增長的情況,這是對上一次執行put方法之後的cout進行的判斷,這樣不好,不好!!
如果本次操作超過了閾值,那麼需要到下次調用put方法才能重新申請空間。HashMap中則是size++之後才進行這個判斷,
應該是更好的一種做法。
*/
if (count >= threshold) {
//如果table的元素個數超過了閾值(即裝填因子),重新申請空間,默認爲擴大一倍
rehash();
tab = table;
index = (hash & 0x7FFFFFFF) % tab.length;
}
//將值插入到tab[index]
Entry<K,V> e = tab[index];
//tab[index]不爲空,則會採用頭插法插入到tab[index]處的鏈表
tab[index] = new Entry<K,V>(hash, key, value, e);
count++;
return null;
}
//這是Entry的構造方法,注意該方法使用的是頭插法
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
從這個方法中可以看出:
1、Hashtable是不容許null key 和 null value的。
2、根據hashCode得到索引的算法,主要體現在下面這句代碼上:
int index = (hash & 0x7FFFFFFF) % tab.length;
(hash & 0x7FFFFFFF):鍵的hash值可能爲負數(比如new Integer(-1)的hashCode便是-1),
整句通過一個“與”操作,先將hash值都轉化正數,然後將得到的hash值與Hashtable的長度求餘。
這個由Hash值得到索引的方法還是非常簡單的!
3、put()操作的時間複雜度在不產生衝突的情況下爲常數複雜度,產生衝突的情況下爲線性複雜度
雖然不同對象有不同的hashcode,但不同的hashCode經過與長度的取餘,就很可能產生相同的index。這邊會產生hash衝突。
從實現源碼中可以看出,Hashtable處理衝突的方法是在衝突產生的索引處建立一個鏈表,經所有索引相同的對象存儲在鏈表中。
先看一段測試代碼:
Map map = new Hashtable();
map.put(0,"h");//
map.put(1,"h");
map.put(3,"h");
map.put(5,"h");
map.put(12,"h");// 12%11=1
map.put(11,"h");// 11%11 = 0
map.put(22,"h"); // 22%11 = 0
執行這段代碼將得到這樣的Hashtable(默認應該有11個節點,這裏只畫出了5個,容器中存儲的是對象引用,“h”爲與常量池中,
所有節點的value都是指向“h”的引用,注意採用衝突鏈表採用的是頭插法):
再來看看這兩個方法:
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry tab[] = table;
//遍歷整個hash表
for (int i = tab.length ; i-- > 0 ;) {
//遍歷hash衝突產生的鏈表
for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
public synchronized boolean containsKey(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
//直接得到索引
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return true;
}
}
return false;
}
contains(Object value)判斷hashtable中是否存在指定的值;
containsKey(Object key)判斷hashtable中是否存在指定的鍵;
想說的是這兩個函數的效率,contains(Object value)需要遍歷整個hash表,而containsKey(Object key)可以直接通過hashCode()得到索引,二者的效率可見一般。
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
這個方法通過key得到value,如果沒有產生hash衝突,其時間複雜度爲O(1),如果產生了hash衝突,其複雜度爲O(n).
在java中,存取數據的性能,一般來說當然是首推數組,但是在數據量稍大的容器選擇中,
Hashtable將有比數組性能更高的查詢速度.具體原因從以上分析便知。
可以通過這兩個方法來分別得到鍵和值,Hashtable不支持Iterator遍歷。
//以Enumeration的形式返回所有的鍵
public synchronized Enumeration<K> keys() {
return this.<K>getEnumeration(KEYS);
}
//以Enumeration的形式返回所有的值
public synchronized Enumeration<V> elements() {
return this.<V>getEnumeration(VALUES);
}