Hashtable
![](https://img-my.csdn.net/uploads/201204/18/1334764652_9494.jpg)
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);
}