目录
1、概述
2、原理(图解)
3、源码分析
4、知识延伸
一、概述
- Hashtable也称为散列表,它存储的内容是键值对(key-value)映射,是根据关键字值(key value)直接进行访问的数据结构。也就是说,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度。这个映射函数称为哈希函数(也称为散列函数),映射过程称为哈希化,存放记录的数组叫做散列表。
- Hashtable继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
- Hashtable的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null,Hashtable中的映射不是有序的。
- Hashtable的实例有两个参数影响其性能: ①初始容量(initialCapacity) :哈希表中桶 的数量,初始容量 就是哈希表创建时的容量; ②负载因子(load Factor):负载因子哈希表是0.1 到 1.0 范围内的数字,当容量自动增加(扩容rehash)之前允许哈希表得到满足的度量。初始容量和负载因子这两个参数只是对该实现的提示。通常,默认负载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。负载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间。
二、原理(图解)
三、源码分析
1、相关参数定义:
/**
* 为一个Entry[]数组类型,Entry代表了“拉链”的节点,每一个Entry代表了一个键值对,哈希表的"key-
value键值对"都是存储在Entry数组中的。
*/
private transient Entry<?,?>[] table;
/**
* 哈希表中条目的总数
*/
private transient int count;
/**
* table数组扩容的节点数阈值,以此来判断是否达到扩容标准
*/
private int threshold;
/**
* 负载因子默认0.75f
*/
private float loadFactor;
/**
* Hashtable被修改次数,用来实现“fail-fast”机制的(也就是快速失败)。
*/
private transient int modCount = 0;
2、构造函数:
// 初始化默认构造函数。
public Hashtable() {
//默认容量为11 负载因子为0.75
this(11, 0.75f);
}
// 初始化指定“容量大小”的构造函数
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
// 初始化指定“容量大小”和“加载因子”的构造函数
public Hashtable(int initialCapacity, float loadFactor) {
//传入容量不能<0,否则会报异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
//传入的加载因子范围在0——1之间,默认0.75 建议设置在0.7——0.75之间,否则也会报异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//如果传入的容量为0,那么会默认把容量初始为1,可看出容量默认为11,最小为1
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//创建容量为initialCapacity的Entry数组table
table = new Entry<?,?>[initialCapacity];
//计算出数组的阀值,也算一个临界值吧(阀值表示当table的长度达到这个阀值(临界值)之后就会触
//发扩容机制(rehash))
//计算阀值的公式:阀值=容量*负载因子 与 当前系统数组最大长度+1 的最小值
//MAX_ARRAY_SIZE =Integer.MAX_VALUE - 8
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 初始化包含“子Map”的构造函数
public Hashtable(Map<? extends K, ? extends V> t) {
// 初始容器取map最大尺寸*2和11取大的,也就是最小也是11,负载因子0.75
this(Math.max(2*t.size(), 11), 0.75f);
//将传入的map放入数组
putAll(t);
}
//将传入的map放入数组
public synchronized void putAll(Map<? extends K, ? extends V> t) {
//遍历循环传入的map,调用put放入数组
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
//放入
put(e.getKey(), e.getValue());
}
接下来主要分析常用的几个方法(get / put / remove / rehash) ,在这个之前我们先看下Entry这个类
3、Entry类分析
//Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
private static class Entry<K,V> implements Map.Entry<K,V> {
//定义一个int类型的hash字段
final int hash;
//定义key字段
final K key;
//定义value字段
V value;
//存储下一个Entry数组对象
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;
}
//克隆
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// 获取Entry中的key
public K getKey() {
return key;
}
//获取Entry中的value值
public V getValue() {
return value;
}
//设置Entry中的value值 可以看出hashtable的key和value都不能为null,否则会抛异常
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
//比较equals
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()));
}
//获取hashCode
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
4、get方法
//根据key从哈希表中获取对应的value值
//获取方法同步锁synchronized
public synchronized V get(Object key) {
//创建Entry[]数组类型
Entry<?,?> tab[] = table;
//获取key的hash值
int hash = key.hashCode();
//再根据hash值和table的长度计算出在table数组中的索引位置
//扩展:hash值为int类型 4个字节 32bit.
// 为了在hash为负值的情况下,去掉起符号位,所以和0x7FFFFFFF进行&操作
// 0x7FFFFFFF 二进制 0111 1111 1111 1111 1111 1111 1111 1111
// 负数与其进行&操作将产生一个正整数
int index = (hash & 0x7FFFFFFF) % tab.length;
//遍历对应位置的链表
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//在链表中查找hash值和key值都相等的元素
if ((e.hash == hash) && e.key.equals(key)) {
//返回节点的值
return (V)e.value;
}
}
//没有找到则返回null
return null;
}
5、put放入哈希表
//添加键值对
public synchronized V put(K key, V value) {
//判断value是否为空
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
//先获取key的hash值
int hash = key.hashCode();
//再根据hash值和table的长度计算出在table数组中的索引位置
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)) {
//若添加的key在Hashtable已经存在,则用新value覆盖原有的value
V old = entry.value;
entry.value = value;
return old;
}
}
//添加链表节点
addEntry(hash, key, value, index);
return null;
}
//添加链表节点
private void addEntry(int hash, K key, V value, int index) {
//更改次数加1
modCount++;
Entry<?,?> tab[] = table;
//当哈希表实际容量>=哈希表的阀值(临界值),触发扩容操作
if (count >= threshold) {
//进行扩容操作
rehash();
//将扩容后的table赋值给新创建的Entry数组tab[]
tab = table;
//先获取key的hash值
hash = key.hashCode();
//再根据hash值和table的长度计算出在table数组中的索引位置
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 创建一个新的Entry数组
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
//哈希表哈希表实际容量+1
count++;
}
6、rehash 扩容解析
//扩容操作
protected void rehash() {
//获得扩容前数组容量
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
//计算得出新的数组容量,新数组容量=旧数组容量*2+1(<<1表示右移一位 <<1=2的一次方=2)
int newCapacity = (oldCapacity << 1) + 1;
//如果新数组容量>数组规定的最大容量限制,则使用最大限制容器值 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//如果旧的数组容量=MAX_ARRAY_SIZE则
if (oldCapacity == MAX_ARRAY_SIZE)
return;
newCapacity = MAX_ARRAY_SIZE;
}
//创建一个容量为newCapacity的新的数组对象
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//容器更改次数+1
modCount++;
//获取新的数组阀值(临界值)
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//将新的数组赋值给table参数
table = newMap;
//依次循环将原有元素复制到新的Hashtable中
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;
}
}
}
四、Hashtable知识延伸
1)Java中==,equals和hashCode有什么区别和联系?
2)为什么加载因子要用0.75?
3)为什么获取数组下标时要 key.hashCode() & 0x7fffffff ?
4)为什么扩容是2N+1?
5)负载因子值的大小,对HashMap有什么影响?
6) Hashtable的复杂度为什么是O(1)?
7)Hashtable最大容量为什么是2^31-8?