分析是基于jdk11,但与jdk1.8相差并不大
HashTable是一个安全散列集,它是以Key-Value对的形式存储,和HasMap一样它同样也存在哈希冲突的情况,因此HashTable采用了数组+单链表的方式来减少哈希冲突(简称拉链法),此外,它的所有涉及到并发的方法的被synchroniced关键字修饰(即在方法加了同步锁),所以在并发情况下是线程安全的,但又因为被sysnchronized修饰所以性能没有hashmap好(HashTable的性能还和初始化容量和加载因子有关,这影响着是否需要重新哈希)。
HashTable采用链表存储的数据结构,在进行添加修改的时候比较快,不需要移动元素,只需要修改指向(指针)即可,但在查找的时候就不能顺序查找了,只能通过遍历一次链表。
下图是大概的HashTable的数据结构图:
从图中就基本可以清楚知道HashTable是通过在数组和链表的结构来存储数据和减少哈希冲突的。下面开始分析源码:
-
首先看HasTable的存储结构
//这个变量是HashTable的存储链表的数组 private transient Entry<?,?>[] table; //该类是HashTable的内部静态类,它是一种链表结构,同时也是真正存储数据的类 private static class Entry<K,V> implements Map.Entry<K,V> { //每个存储元素key的hash值 final int hash; //k-v对 final K key; V value; //后继指针 Entry<K,V> next; //可以看到构建一个节点是采用头插法的,即把新元素在原来的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; } //中间省略了一下基本操作。。 //这个是修改值,是直接修改并返回旧的数值 public V setValue(V value) { if (value == null) throw new NullPointerException(); V oldValue = this.value; this.value = value; return oldValue; } //equeals是根据k-v是否相同来判断的 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())); } //这返回的是这个存储k-v这个结构的哈希码,计算是key的哈希值与value的哈希值进行异或就得到哈希码 public int hashCode() { return hash ^ Objects.hashCode(value); } }
-
HashTable的全局变量并分析相关作用
//基础了Dirctionary这个类和实现了map接口 public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { /** * 数组存储每一个链表头 */ private transient Entry<?,?>[] table; /* *计算元素的个数 */ private transient int count; /** *这是一个阈值,如果超过这个阈值就会进行rehash,性能会有所下降 *它的计算公式是 当前容量*加载因子 * @serial */ private int threshold; /** *这个是加载因子 * @serial */ private float loadFactor; /* *修改操作的次数 */ private transient int modCount = 0; private static final long serialVersionUID = 1421746759512286392L;
-
构造函数是如何初始化的
HashTable默认初始化容量是11,loadFactor=0.75
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); //如果传入的容量为0则默认初始化容量大小1, if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; //构建entry数组并赋值给table table = new Entry<?,?>[initialCapacity]; //计算阈值,容量*加载因子 与最大容量比较取小的作为阈值 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); } /** *带容量的构造方法默认初始化加载因子是0.75 */ public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } /** * 默认构建一个容量为11的entry数组,加载因子默认是0.75 */ public Hashtable() { this(11, 0.75f); } /** *带有map序列的构造方法 */ public Hashtable(Map<? extends K, ? extends V> t) { //内部通过上面的构造函数初始化,容量是当前序列大小的两倍与11比较取大的,加载因子默认0.75 this(Math.max(2*t.size(), 11), 0.75f); //初始化后进行元素添加//该方法在后面会分析 putAll(t); }
-
HashTable的put方法分析
大概实现过程:添加元素前先根据key的哈希码来求出 当前元素应该添加在那个条拉链(链表)上(即先计算除在数组那个位置),然后获取那个位置的链表头,对链表进行遍历看是否已经存在,如果存在则把值修改为新值,返回旧值,如果不存在则调用 addEntry方法,addEntry方法根据key和哈希码进行链表插入(头插法),然后再把原来的头节点修改为这个添加完元素的链表的头接节点(即头节点会该为新添加这个元素)
/* *批量添加只是通过遍历来添加 */ public synchronized void putAll(Map<? extends K, ? extends V> t) { for (Map.Entry<? extends K, ? extends V> e : t.entrySet()) put(e.getKey(), e.getValue()); } //加了同步锁的put方法 public synchronized V put(K key, V value) { //判空 if (value == null) { throw new NullPointerException(); } // 先把链表头数组拷贝一份 Entry<?,?> tab[] = table; //计算key的哈希码,计算String的哈希码,1.8的计算是 hash*31+val[i]循环遍历,即每次哈希码*31+每个字符的ascall码 int hash = key.hashCode(); //根据hash计算应该存储在那条链表上(散列) int index = (hash & 0x7FFFFFFF) % tab.length; //根据下标获取链表头 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; } //改方法采用的是链表的头插入法 private void addEntry(int hash, K key, V value, int index) { //拷贝一份数组 Entry<?,?> tab[] = table; //判断当前元素的个数是否超过了阈值,如果超过了阈值则进行重新哈希,具体的重新哈希放到后面分析 if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); //重新哈希后重新赋值给tab tab = table; //并重新获取key的hash hash = key.hashCode(); //重新计算散列,得到具体的数组位置 index = (hash & 0x7FFFFFFF) % tab.length; } // 获取对应位置的链表头 @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) tab[index]; //采用头插法构建一个节点 并存储新的头节点,具体可以看开头的链表结构分析 tab[index] = new Entry<>(hash, key, value, e); //修改个数 count++; //修改操作次数 modCount++; }
-
remove方法的分析
大致实现过程:计算key的哈希码,然后根据哈希码,进行散列计算在那条拉链(链表上),然后直接修改链表的指针即可
//注意每个修改方法都被synchronized标识了 public synchronized V remove(Object key) { Entry<?,?> tab[] = table; //计算key的哈希码 int hash = key.hashCode(); //进行散列 int index = (hash & 0x7FFFFFFF) % tab.length; //获取链表头 @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; //对e链表进行遍历寻址key和haxi都相同的 for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) { //如果是要找的元素 if ((e.hash == hash) && e.key.equals(key)) { //prev是e的前一个节点,判断prev不为空则让e的后继指向prev的后继节点,相当于跳个e这个节点的后继的节点指向prev的后继节点 if (prev != null) { prev.next = e.next; } else { //否则是头节点,只需要把tab[index]的链表头改为后继节点即可 tab[index] = e.next; } //常规操作 modCount++; count--; V oldValue = e.value; e.value = null; return oldValue; } } return null; }
-
get方法分析
由于和上述remve前面部分相同则不进行分析了
public synchronized V get(Object key) { //获取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; }
-
HashTable的rehash分析
//数组的最大容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * rehash方法是超过了阈值进行扩容并重新散列计算元素的位置 */ @SuppressWarnings("unchecked") protected void rehash() { //获取旧数组的容量 int oldCapacity = table.length; //拷贝一份旧数组 Entry<?,?>[] oldMap = table; //计算新的数组容量,但真正创建容量不一定是这个 //扩容的计算公式是 旧数组的容量*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指向新容量的数组 table = newMap; //遍历旧数组进行重新散列 for (int i = oldCapacity ; i-- > 0 ;) { //遍历每一条链表的元素 for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { //获取头节点元素,并把old指向后继节点 Entry<K,V> e = old; old = old.next; //重新计算散列,来确定在新数组的那个位置 int index = (e.hash & 0x7FFFFFFF) % newCapacity; //把新数组的链表头指向e的后继节点 e.next = (Entry<K,V>)newMap[index]; //更新新数组的链表头 newMap[index] = e; } } }
-
获取entrySet和keySet方法
//这几个方法就是获取一个set的集合或者key的集合或者vales就不分析了 public Set<Map.Entry<K,V>> entrySet() { if (entrySet==null) entrySet = Collections.synchronizedSet(new EntrySet(), this); return entrySet; } public Set<K> keySet() { if (keySet == null) keySet = Collections.synchronizedSet(new KeySet(), this); return keySet; }
-
还有一些其他方法都和上面大同小异就不进行分析了都是重复的
总结:HashTable是通过同步锁线程安全的(和vector一样),每次扩容都是旧数组大小的两倍,如果超出最大容量则使用最大容量来进行扩容,在添加,修改、删除方面速度比较快,在查询方面需要进行变量,此外,要尽量避免重新散列,因为重新散列会对性能有所影响。
HashTable源码分析
LinkedList源码分析
Vector源码分析
CopyOnWriteArrayList源码分析
SynchorincedList源码分析