您的位置:首页 > 理论基础 > 数据结构算法

再思考Java里的数据结构容器——hash容器:hashset hashmap hashtable

2009-12-04 09:45 661 查看
hash容器的结构

Hashtable跟hashmap差不多,不过HashTable强制同步,是线程安全的。

HashSet<E>是通过内置的HashMap<E,object>来实现的,HashSet<E>中指定的元素类型E,本身就是key,每个元素的value是一个常量object,HashSet仅作一个集合管理,只有add contains remove等接口,没有get接口。

重点讨论下HashMap的结构。

HashMap维护了capacity个数的hash bucket(hash桶),这个是用数组实现的,即table[capacity],每个非空的hash bucket其实是一个映射链表的表头,即list head of entry,链表里面的每个entry负责一个具体key到value的映射。

每个hash bucket里元素,他们key值的hashcode可能不一样,即hash散列时是容许冲突的,但一样的是hashcode&(capacity-1),这个hashcode&(capacity-1)即他们所在的hash bucket在table[capacity]里的index,其实这就是hash散列的过程,顺便说下这个hashcode是key的hashcode经过变换之后得到的,这个index就是所谓的hash bucket index。

当然capacity也可以认为是元素的容量,因为理想的hash散列是一对一的,即没有散列冲突,一个hash bucket固定存储一个key到value的映射,后面可以看到容器维护的时候也是这么努力的,是否扩充容量是由元素的个数是否到了负载极限来决定的,而不是已填充的hash bucket的个数,这是表现之一。

有了这个概念之后再看hashmap的常见操作:

为一个元素在table[bucketIndex]里添加一个映射entry,如果超过threshold(Capacity * loadFactor),则扩展一倍,默认的容量是16,loderfactor是0.75,所以当元素个数达到12的时候,table容量扩充为32,依次64,128等

void addEntry(int hash, K key, V value, int bucketIndex) {

Entry<K,V> e = table[bucketIndex]; // 获取hash bucket里的entry链表表头
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// new Entry<K,V>(hash, key, value, e) 构造里是这么写的:this.next = e,可见是前插法填充链表的。
if (size++ >= threshold)
resize(2 * table.length);
}

查找是否包含某个key的映射:比如 hm.add("ab");则hm.containsKey("ab")则true

public boolean containsKey(Object key) {
//空元素null特殊对待,貌似专门用一个object常量
Object k = maskNull(key);
//把k的hashcode再转换
int hash = hash(k);
//hashcode&(capacity-1)换的index
int i = indexFor(hash, table.length);
Entry e = table[i];
// 在entry链表里找key
while (e != null) {
//先验证key的hashcode,然后就是key的equals值
if (e.hash == hash && eq(k, e.key))
return true;
e = e.next;
}
return false;
}

按照这个过程分析hm.containsKey("ab"),过程就是通过"ab"的hashcode值找到hashbucket的entry链表表头,然后在里面逐个与"ab"比较equals,找到的话则返回true,否则false。其他的查找操作与这个过程大同小异,比如get(key);put(key,value)其实也有个类似的查找映射enty的操作。

public V put(K key, V value) {
K k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length);

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
if (e.hash == hash && eq(k, e.key)) {
//put另一个值得注意的地方:同一个key下增加,会把原来的value给覆盖掉
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, k, value, i);
return null;
}

同样如果是查找某个value,得依次遍历所有的hash bucket,并逐个与给定值进行equals,这个源码略去。

原创,转载请标注http://hi.baidu.com/heelenyc 欢迎交流指正。

再看hashtable。
大致上和hashmap是一样的,但可以肯定他们两不是一个人写的,实现细节有差别,包括hash散列函数,空值处理、代码风格等。
最重要的区别是在关键操作方法前多了一个synchronized同步的,这是出于线程安全而考虑的。下面以put函数为例

public synchronized V put(K key, V value) {
// Make sure the value is not null
//从这可以看出 hashtable不欢迎value==null的元素,hashmap对这个问题专门对待

if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
//此处如果key为null,抛出异常
int hash = key.hashCode();
//跟hashmap的散列函数有区别
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)) {
V old = e.value;
e.value = value;
return old;
}
}

总结:
1、HashSet,HashMap,Hashtable 之所以"hash", 个人认为体现在:
首先、通过key的hashcde找到table[capability]里的hash bucket的过程,这是hash散列的过程,
其次、散列过程显然无法避免冲突,在元素个数不止一个的hash bucket通过链表查找key的过程,其实就是hash再探测的过程,显然java是通过链表来解决hash冲突的。

2、key的hashcode、key的equals方法和value的equals方法是hash容器操作的关键,所以当我们自定义hash容器的元素类的时候,特别要注意这两个方法的重写。用到的地方包括:
查找key是用key的hashcode找hash bucket,用key的equals找对应的entry;查找value是使用了value的equals来匹配的。

3、Hashtable和Hashmap在hash处理上大同小异,列举几点
区别一:hashtable不允许null的value,也没有考虑null的key,即在获取key的hashcode会抛出异常,hashmap碰到null的key都替换成一个常量object,它也可以容忍null的value
区别二:把key的hashcode散列到hash bucket的时候有区别
区别三:Hashtable强制同步,保证线程安全。

原创,转载请标注http://hi.baidu.com/heelenyc 欢迎交流指正。

待续,下次看下容器里的泛型编程和设计模式。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: