Android学习之路(七)---- Hashtable
Hashtable
在这里主要参考Java8的代码,其他版本几乎没怎么改动。
主要看的流程是创建,添加,获取,其他方法如果需要自行查看源码
1. 数据结构
private static class Entry<K,V> implements Map.Entry<K,V> {
//注意这里使用final修饰了,HashMap并没有
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;
}
2. 构造函数
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);
}
/**
* Constructs a new, empty hashtable with the specified initial capacity
* and default load factor (0.75).
*
* @param initialCapacity the initial capacity of the hashtable.
* @exception IllegalArgumentException if the initial capacity is less
* than zero.
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
*/
public Hashtable() {
this(11, 0.75f);
}
- 可以看出所有的构造函数都调用
public Hashtable(int initialCapacity, float loadFactor)
,在这个了创建了哈希表(默认11,加载因子为0.75),与HashMap有所不同的是HasHMap哈希表初始化操作在put中,在构造函数中只是进行一些赋值。(上面所有探讨都是排除传入Map对象这一个构造函数)
2
3. put(K key, V value)
方法
可以看出put使用了
synchronized
,上锁的实例是本类,所有同一个实例进行操作时同步,有线程安全,但是Hashtable中操作哈希表的操作都加上了同一锁(这里解释锁有点错误,可能需要了解过synchronized),会影响性能。
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();
//就将原始的 hash 值转换为一个有效的、非负的数组索引。
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;
}
4. addEntry(int hash, K key, V value, int index)
方法
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++;
}
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;
}
}
}
6. put(Object key)
方法
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;
}
7. 总结
- Hashtable在new的时候就创建了,哈希表,而且数据结构为数组加链表,这与HasHMap有些不同。
- Hashtable的put方法,首先通过hash值取到位置,然后通过头插法插入链表中,这与Java7的HasHMap有些相似。添加节点addEntry()方法中,首先会判断填充是否达到阀值来确定是否扩充。如果扩充需要重新计算填入节点的位置。最后返回值是旧值。
- rehash()方法是哈希表的扩充方法。大概内容是扩充一倍加一(如果没有达到最大值
Integer.MAX_VALUE - 8
),然后创建一个新的哈希表来重新填充数据。 - Hashtable的get方法,比较简单,通过hash值取到哈希表位置,通过遍历链表获取值。
- Hashtable是线程安全,这是由于暴露的操作哈希表的方法都加上了
synchronized
- 关于键不能为 null 的原因包括:
- 哈希算法: Hashtable 使用键的哈希码来确定存储位置。如果键为 null,那么它的哈希码就需要为 0。由于哈希表的实现使用取模运算,0 对于任何正整数 n 都会产生 0。这就会导致所有键为 null 的元素都会映射到哈希表的相同位置,降低了哈希表的效率。
- equals() 方法: Hashtable 在查找键时使用 equals() 方法来比较键的相等性。如果键为 null,在调用 equals() 方法时就会导致NullPointerException 异常。
一些优秀的算法
1. (hash & 0x7FFFFFFF) % tab.length;
在 Hashtable 中,(hash & 0x7FFFFFFF) % tab.length; 这行代码的目的是将传入的 hash 值转换为一个正数索引,并将其限制在 Hashtable 的数组 tab 的长度范围内。
hash & 0x7FFFFFFF:这是一个位与操作,它确保 hash 的最高位(符号位)被清零。0x7FFFFFFF 是一个32位整数,其二进制表示是31个1后跟一个0。因此,当 hash 与 0x7FFFFFFF 进行位与操作时,hash 的符号位会被清零,从而确保结果是一个非负整数。这是因为在Java中,整数是有符号的,而哈希表的索引通常是无符号的或正数。
% tab.length:取模操作确保得到的索引值在 tab.length 的范围内。这是非常重要的,因为数组索引不能超出数组的实际长度。
将这两个操作组合起来,(hash & 0x7FFFFFFF) % tab.length; 就将原始的 hash 值转换为一个有效的、非负的数组索引。