源码分析——Hashtable

本文主要分析了Hashtable的源码,对比HashMap,讨论了它们的继承体系、成员变量、构造器和核心方法。尽管Hashtable已较少使用,但作为面试常考点,了解其工作原理仍很重要。Hashtable的扩容机制与HashMap不同,它采用位与运算来确保正数哈希值,并在哈希冲突时使用拉链法。此外,Hashtable不允许重复键和空值,而HashMap则允许。当需要线程安全的Map时,通常使用Collections的synchronizedMap()方法。
摘要由CSDN通过智能技术生成

对于Hashtable,其实实际中用的不多,但是作为一个面试常考点,还是试着来研究一下这个集合类。

首先研究Hashtable,就要将Hashtable和HashMap放在一起比较,因为两者的功能类似且相近。Hashtable产生于JDK1.1,而HashMap产生于JDK1.2,HashMap产生的时间要比Hashtable晚。接下来我们来看一看Hashtable的继承体系:

其实通过对比我们可以发现,Hashtable并不像HashMap之类的map集合类一样都属于AbstractCollection继承体系,它自成一个体系,继承自Dictonary抽象类,我们再来看看Dictonary类:

 

上图可以看出Dictonary和AbstractMap抽象类有些方法类似,只是AbstractMap抽象类的方法比Dictonary抽象类的方法更加全面,而且Dictionary类实际上已经是一个已经被废弃的类。接下来开始看其源码:

public class  Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serialzable

可以发现Hashtable和HashMap除了继承的类不一样之外,其他实现的接口基本是一致的,此处就不再对这些接口的用途一一赘述了,毕竟都已经是老熟人了。下面继续看其成员变量:

private transient Entry<?,?>[] table;
//私有成员变量,不可序列化,哈希表
	
private transient int count;
//私有成员变量,不可序列化,键值对数量
	
private int threshold;
//私有成员变量,阈值
	
private float loadFactor;
//加载因子
	
private transient int modCount = 0;
//修改次数
	
private static final long serialVersionUID = 1421746759512286392L;
//序列化标志,用于反序列化

private transient volatile Set<K> keySet;
//私有成员变量,不可序列化,同步,用于获得key集合
	
private transient volatile Set<Map.Entry<K,V>> entrySet;
//私有成员变量,不可序列化,同步,用于获得Entry集合

private transient volatile Collection<V> values;
//私有成员变量,不可序列化,同步,用于获得value集合

从上可以发现其成员变量也和HashMap类似,都有哈希表table,count键值对数量,threshold阈值,loadFactor加载因子以及modCount修改次数,当然有些HashMap中存在的成员变量Hashtable中并没有出现,如最大容量,最大建树阈值之类的。让我们继续向下看:

public Hashtable(int initialCapacity, float loadFactor){
//构造器1,传入值为默认容量与加载因子
	if(initalCapacity < 0)
		throw new IllegalArgumentException("Illegal Capacity:" + initalCapacity);
	if(initalCapacity == 0)
		initalCapacity = 1;
	//判断initalCapacity是否合法,如果传入initalCapacity为0,则设置其为1
	this.loadFactor = loadFactor;
	table = new Entry<?,?>[initalCapacity];
	//创建一个initalCapacity大小的数组赋给table,此处和HashMap不一样
	threshold = (int)Math.min(initalCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
	//设置阈值
}
	
public Hashtable(int initalCapacity){
	//构造器2,传入值为默认容量,则设置加载因子为0.75,并调用构造器1
	this(initalCapacity, 0.75f);
}
	
public Hashtable(){
//构造器3,空参,则设置默认容量为11,设置加载因子为0.75,此处初始默认容量和HashMap不一样
	this(11,0.75f);
}
	
public Hashtable(Map<? extends K, ? extends V> t){
//构造器4,传入的值为map集合
	this(Math.max(2 * t.size(),11),0.75f);
	putAll(t);
}

Hashtable有四个构造器,当传入值为初始容量与加载因子时,其会先判断传进来的值是否合法,然后直接创建一个初始容量大小的table数组,这与HashMap不同,HashMap是无论你传多大的初始容量进去,它会根据大小来创建相应的2次幂大小的table数组。当传入值为初始容量时,则先设置默认加载因子为0.75,再调用构造器1进行创建;当传入空参时,它设置初始容量为11,默认加载因子为0.75.这又和HashMap不一样,HashMap的默认初始容量是16。当传入的参数为map集合时,先取该集合的大小的两倍与默认初始容量11的最大值作为初始容量,并和默认加载因子0.75一起调用构造器1进行构造,再将map集合中的键值对依次放入Hashtable中。

那么为什么有些地方和HashMap不一样呢?我们来研究一下它的核心方法:扩容

@SuppressWarnings("unchecked")
protected void rehash(){
	//哈希冲突的解决方式
	int oldCapacity = table.length;
	//获得当前的哈希表长度作为旧容量
	Entry<?,?>[] oldMap = table;
	//获取当前的table作为旧数组
	int newCapacity = (oldCapacity << 1) + 1;
	//定义新容量为旧容量的两倍加一
	if(newCapacity - MAX_ARRAY_SIZE > 0){
		//如果新容量比数组大小最大值还要大
		if(oldCapacity == MAX_ARRAY_SIZE)
			//如果旧容量就是最大值
			return;
			//不进行扩容,直接返回
		newCapacity = MAX_ARRAY_SIZE;
		//否则就将新容量定义为最大值
	}
	Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
	//创建一个新的数组,数组的大小为新容量
	modCount++;
	//修改次数加一
	threshold = (int)Math.min(newCapacity * loadFactor,MAX_ARRAY_SIZE + 1);
	//设置阈值
	table = newMap;
	//将newMap赋给table
	
	for(int i = oldCapacity; i-- > 0; ){
		//遍历数组槽
		for(Entry<K,V> old = (Entry<K,V>)oldMap[i]; old != null; ){
			//遍历每一个数组槽内的链表
			Entry<K,V> e = old;
			//先取出当前结点作为e
			old = old.next;
			//再将指针向后移动一位
			
			int index = (e.hash & 0x7FFFFFFF) % newCapacity;
			//重新定位
			e.next = (Entry<K,V>)newMap[index];
			//将开始存在与newMap[index]处的结点作为e的下一个结点
			newMap[index] = e;
			//再将e放入newMap[index]中,即如果在newMap[index]处的是null
			//则将e直接放入,且下一个结点为null,如果不是,则将e插入到链表的头部
		}
	}
}

我们可以发现,Hashtable中的rehash方法与HashMap中的最大的不同除了不需要考虑红黑树之外,就是定位方法不同了,它的hash定位方法是:

int index = (e.hash & 0x7FFFFFFF) % newCapacity;

首先 0x7FFFFFFF以二进制位数表示为:

0111 1111 1111 1111 1111 1111 1111 1111

而位与运算的计算方法是1 & 1为1,其实为0.因此将结点的hash值与0x7FFFFFFF进行与运算的目的就是将最高位全部转换为0,也就是无论正数负数都转换为正数。再和newCapacity进行求余运算我们就很容易理解了,因为我们在介绍HashMap中的hash定位时使用的第一个散列函数就是取余运算。因为是采用取余运算来通过hash值来计算在散列表中的位置,因此对于定位计算时二进制每一位是多少就没有那么苛刻的要求,所以你可以选择任何大小(合法的)作为table的大小都没有问题。而默认初始容量也不必设置成16,而由于Hashtable产生于HashMap之前,它当时采用的是以11位table的默认初始大小的。但是每一次扩容时都是原容量乘以2再加1,也就是尽量时散列表大小为奇数,这样可以尽量将插入的元素在散列表中均匀分布,减少哈希冲突。

那么Hashtable是如何处理哈希冲突的呢?

1.put

public synchronized V put(K key, V value){
	//增加键值对,对外方法
	if(value == null){
		//首先判断value是否为空,Hashtable中不支持value值为空
		throw new NullPointerException();
	}
	
	Entry<?,?> tab[] = table;
	//获取table
	int hash = key.hashCode();
	//获取key的哈希值
	int index = (hash & 0x7FFFFFFF) % tab.length;
	//获取应该插入的位置
	@SuppressWarnings("unchecked")
	Entry<K,V> entry = (Entry<K,V>)tab[index];
	//获取该位置当前的键值对
	for(; entry != null; entry = entry.next){
		//如果该位置上有结点,则遍历结点,寻找是否有和传入进来key一样
		//的key,如果存在,则直接用传进来的value将该key对应的value覆盖
		//并且返回原来的key,因此从此处可以看出,Hashtable不允许key重复
		//如果插入的键值对有重复,则将Hashtable中的value进行覆盖
		if((entry.hash == hash) && entry.key.equals(key)){
			V old = entry.value;
			entry.value = value;
			return old;
		}
	}
	addEntry(hash,key,value,index);
	//否则作为链表的头结点放入数组槽内
	return null;
	//返回null代表插入成功
}

从上述增加键值对的方法就可以看出其是如何处理hash冲突了,当通过hash值定位到散列表的某一个槽内后,发现槽内已经有一个键值对存在了,则将新插入的键值对的后置指针指向槽内的键值对,并且将新键值对放入槽内,也就是拉链法。但是只止步于拉链法,并没有像HashMap中那样再建红黑树。除此之外,我们还可以发现Hashtable中不允许存在相同的key值并且不允许value值为空。

接下来继续看常用方法:

2.remove

public synchronized V remove(Object key){
	//通过key值删除Hashtable中的某一个键值对
	Entry<?,?> tab[] = table;
	//获得table
	int hash = key.hashCode();
	//获得key的hash值
	int index = (hash & 0x7FFFFFFF) & tab.length;
	//获得位置
	@SuppressWarnings("unchecked")
	Entry<K,V> e = (Entry<K,V>)tab[index];
	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;//置空,方便GC回收
			return oldValue;//返回被删除结点的value
		}
	}
	return null;//如果返回null,则说明删除失败
}

同样,remove方法也和HashMap中的方法类似,当删除成功时则返回删除元素的value值,否则返回Null

3.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值