Map--HashTable
上一篇文章中,我们分析了HashMap的源码,这一篇文章我们学习Map接口的另一个实现类---HashTable,在学习之前,不熟悉hashMap的可以先看我的上一篇文章Map--HashMap,我们需要先了解下它和HashMap有哪些异同点。
不同点 | HashMap | HashTable |
---|---|---|
继承的父类 | Dictionary类 | AbstractMap |
线程安全性 | 线程不安全 | 线程安全 |
key和value | 允许为null | 不允许为null |
遍历方式 | Iterator | Iterator和Enumeration |
hash值计算 | 重新计算key的hash值 | 直接使用key的hashCode() |
初始化 | 默认容量为16 | 默认容量为11 |
扩容方式 | 原容量*2 | 原容量*2+1 |
数据结构 | 数组+链表+红黑树 | 数组+链表 |
Iterator遍历数组的顺序 | 索引从小到大 | 索引从大到小 |
确认key在数组中的索引 | i=(n-1)&hash | index=(hash&0x7FFFFFFF)%tab.length |
底层数组容量为2的整数幂 | 一定要为2的整数幂 | 不要求 |
NOTE:HashMap和HashTable最大的不同体现在线程安全、key和value是否为null,HashTable是个过时的集合类,如果使用场景不需要线程安全,可以直接使用hashMap来代替;如果需要在线程安全的场景中使用,可以使用ConcurrentHashMap替换,看起来HashTable好像没什么用,但是面试经常问啊,所以我们还是需要了解下。
因为HashMap和HashTable在存储结构和实现方式上很相似,所以这篇文章主要讲解HashTable与HashMap不同的知识点。
我们先看一下HashTable的底层数据结构:
HashTable的数据结构
1、成员属性
transient Entry[] table:Entry[ ]数组类型,每个Entry代表一个键值对
transient int count:HashTable内键值对的数量,不是容器的大小
int threshold:调整hashTable容量的阈值
float loadFactor:加载因子
transient int modCount:标记HashTable修改的次数
//Entry[ ]数组类型,每个Entry代表一个键值对
private transient Entry<?,?>[] table;
//HashTable内键值对的数量,不是容器的大小
private transient int count;
//调整hashTable容量的阈值
private int threshold;
//加载因子
private float loadFactor;
//标记HashTable修改的次数
private transient int modCount = 0;
2、构造函数
HashTable有四个构造函数,我们来逐一分析
-
HashTable(int initialCapacity, float loadFactor)
public Hashtable(int initialCapacity, float loadFactor) {
//初始容量<0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//负载因子为非负整数,否则抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//初始化hashTable中的参数、loadFactor、table和threshold
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
//选用initialCapacity*loadFactor和Max_ARRAY_Size+1最小的作为阈值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
MAX_ARRAY_SIZE表示为给数组(Table)分配的最大容量,
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
有同学可能会对MAX_ARRAY_SIZE取值有疑问,为什么是Integer.MAX_VALUE-8,这是因为数组作为一个对象,需要一块内存来存储对象头信息,对象头信息的最大占用内存不能超过8个字节,所以需要减去这个头信息才是分配给Table的最大容量。
-
HashTable (int initialCapacity)
以给定的初始容量和么默认加载因子(0.75f)构造hashtable
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
-
HashTable()
以默认的初始容量(11)和加载因子(0.75f)构造HashTable
public Hashtable() {
this(11, 0.75f);
}
-
HashTable(Map<? extends K, ? extends V> t)
使用给定的键值对集合t来构造HashTable
public Hashtable(Map<? extends K, ? extends V> t) {
//初始化HashTable
this(Math.max(2*t.size(), 11), 0.75f);
//将t中的键值对插入到HashTable中
putAll(t);
}
我们看一下putAll方法,它内部使用了增强for循环来遍历,内部调用了put方法,我们在核心方法讲解put方法
public synchronized void putAll(Map<? extends K, ? extends V> t) {
//使用增强for循环来进行遍历
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
//调用put方法
put(e.getKey(), e.getValue());
}
3、核心方法
HashTable内部提供了很多方法,我们在这篇文章主要讲解HashTable中比较重要的方法,put、get和remove
3.1、put方法
put方法时是将指定的键值添加到hashTable中,其添加步骤可以概括为①判断value不为null,为null时,则抛出异常②计算key的hash值并找到key的索引,获取key所在位置的entry③遍历entry,判断key是否存在④如果key存在,则将用新的值替换旧值⑤如果指定的位置key不存在,直接添加,并返回null
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;
//直接使用hashCode作为hash值
int hash = key.hashCode();
//找到key的索引位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//获取指定下标的entry
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍历链表
for(; entry != null ; entry = entry.next) {
//判断链表中是否有与entry的hash和key相等的对象,如果有,则让新值覆盖旧值,并返回旧值
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//如果在Table没有对应key值,则新添加一个
addEntry(hash, key, value, index);
return null;
}
在这个程序中,有的同学可能会有疑问,比如key的索引位置是怎么计算的,添加的流程是什么,
-
index的计算
hash&0x7FFFFFFF操作是为了使得hashCode的值为正数,(计算后的值)%tab.length表示对数组长度取模,从概率上讲,采用取模计算可以保证结点在数组上的分配比价均匀,这只是减少哈希冲突一种策略。
如果在Table中对没有对应的key值,则需要新添加一个,我们具体看一下添加流程
-
addEntry(hash, key, value, index)
addEntry方法的作用是将指定的值的放入到指定坐标下,其步骤如下①判断entry中的个数是否大于阈值②大于阈值时需要扩容并重新计算值的位置③不大于阈值时需要将指定的值放入到index位置下,原有的index位置的元素向后移,可以看出数据的插入是前插
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判断entry的个数是否大于阈值(阈值默认是11*0.75f)
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")
//将原下标为index的元素赋值给e
Entry<K,V> e = (Entry<K,V>) tab[index];
//将新的结点放在坐标为index的位置,并将新结点的next设置为e
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
我们对上面程序中扩容方法rehash还没有讲解,我们来讲解下rehash的源码
-
rehash()
rehash方法是对扩容后的数组重新计算下标值并放入的数组中,其步骤如下①将新的数组的容量扩展为(原容量)*2+1,②判断新数组的容量是否超出了最大容量的限制③超出了容量限制,就将最大容量赋值给新的数组容量④遍历原有数组中的元素并重新计算新的索引,将值存入到新的数组中
protected void rehash() {
int oldCapacity = table.length;
//将原来的数组赋值给oldMap
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//新的容量扩容为:(原有容量)*2+1
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;
}
}
}
3.2、get方法
返回指定key的value,如果不存在,则返回null
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//计算索引值
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//根据索引找到数组的位置,根据key找到指定的值,并返回,如果没有则返回null
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
3.3、remove方法
删除指定key的键值对,其流程如下①根据key找到数组中的索引,获取key所在的entry②遍历entry,判断key是否存在③如果key存在,删除指定的键值对,并返回Value值④如果不存在,返回null
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
//计算key在hashtabke中的索引
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//根据索引得到头结点键值对
Entry<K,V> e = (Entry<K,V>)tab[index];
//遍历entry,找到key的键值对并删除,返回value值
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
//如果不存在,则返回null
return null;
}
参考文献
[1]https://blog.csdn.net/panweiwei1994/article/details/77427010
[2]https://blog.csdn.net/panweiwei1994/article/details/77428710
[3]jdk开发文档
[4]马克.艾伦.维斯.数据结构与算法分析