Hashtable源码分析(基于jdk1.8,推荐)

Hashtable也算是集合系列中一个比较重要的集合类了,不过在介绍Hashtable的时候,老是不可避免的谈到HashMap,在面试的时候Hashtable每每也会结合HashMap一块来问。这篇文章就来好好地分析一下Hashtablejava

1、认识Hashtable面试

一、继承关系数组

为了能好好的理解Hashtable,咱们先看一下他在整个集合体系中的位置:安全

从上面的图咱们会发现,Hashtable和HashMap师出同门,不过这张图太宏观,咱们仍是放小了看:架构

这张图已经很清晰了,继承了Dictionary,实现了Map接口。并发

二、与HashMap的区别源码分析

若是你以前看过我写的那篇HashMap文章的话,在这里对他们俩的区别必定有了解,如今咱们对其进行一个整理(这里只看区别):this

(1)HashMap容许key和value为空,可是Hashtable不容许。spa

(2)Hashtable是线程安全的,HashMap不是线程安全。线程

(3)ashtable继承自Dictionary,HashMap继承自AbstractMap。

(4)迭代器不一样,Hashtable是enumerator迭代器,HashMap是Iterator迭代器。

三、Hashtable基本使用

public class HashtableTest {
    public static void main(String [] args){
    	//新建方式
        Hashtable<String, String> table=new Hashtable<>();
        Hashtable<String, String> table1=new Hashtable<>(16);
        Hashtable<String, String> table2=new Hashtable<>(16, 0.75f);
        HashMap<String,String>  map=new HashMap<>();
        Hashtable<String,String> table3=new Hashtable<>(map);
        table.put("张三", "1");
        table.put("李四", "2");
        //这种方式会出现空指针异常,由于Hashtable的key不能为空
        table.put(null, "3");
        System.out.println(table.toString());
    }
}
复制代码

下面咱们就经过源码来分析一下Hashtable。

2、源码分析

对于集合类的源码分析,通常都是从参数、构造方法、还有增删改查的基础上进行分析,而后就是增长元素,增多了怎么处理。删除元素,删多了怎么办等等。下面咱们就按照这个思路一步一步分析:

一、参数

HashTable的底层采用"拉链法"哈希表,而且提供了5个主要的参数:

(1)table:为一个Entry[]数组类型,Entry表明了“拉链”的节点,每个Entry表明了一个键值对。

(2)count:容器中包含Entry键值对的数量。

(3)threshold:阈值,大于这个阈值时须要调整容器容量。值="容量*加载因子"。

(4)loadFactor:加载因子。这个比较重要。

(5)modCount:用来实现“fail-fast”机制的。对容器任何增删改操做都会修改modCount。若是出错当即抛出ConcurrentModificationException异常。

private transient Entry<?,?>[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0;
复制代码

上面的是源码,你会发现table、count、modCount还都是transient修饰的,这也就意味着这三个参数是不能被系列化的。

二、构造方法

下面咱们看看其构造方法,源码中一共提供了4个构造方法:

(1)构造方法1

//构造一个空的Hashtable
//默认容量是11,加载因子是0.75
public Hashtable() {
    this(11, 0.75f);
} 
复制代码

(2)构造方法2

//在构造Hashtable的时候,指定容量
//加载因子仍是0.75
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
复制代码

(3)构造方法3

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;
    //新建一个table
    table = new Entry<?,?>[initialCapacity];
    //设置阈值:initialCapacity * loadFactor和MAX_ARRAY_SIZE + 1的最小者
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
复制代码

这个构造方法首先排除掉一些异常状况,而后新建一个table数组来装数据。

(4)构造方法4

//构造给定的 Map 具备相同映射关系的新哈希表:也就是下面这种
//HashMap<String,String> map=new HashMap<>();
//Hashtable<String,String> table3=new Hashtable<>(map);
public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}
复制代码

这个构造方法咱们可就要稍微注意了,真正实现这个操做的是putAll(t)。想要弄清楚咱们不妨跟进去看看。

//把map经过for循环一个一个存放在另一个map中
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());
}
复制代码

上面的代码使用了泛型,并且仍是泛型通配符,不过意思很明确,就是经过for循环一个一个转移到新的map中。

以上所述就是整个构造方法的机制。

三、增长一个元素

public synchronized V put(K key, V value) {
    //第一部分:确保value不为空
    if (value == null) {
        throw new NullPointerException();
    }
    //第二部分:确保table中没有当前的key
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    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;
}
复制代码

这些代码第一部分和第二部分都是为了没有异常,若是当前容器有这个key,那么直接以新值代替旧值便可,最主要的仍是第三部分,添加一个元素的核心addEntry方法。进去看看:

private void addEntry(int hash, K key, V value, int index) {
   //modCount是为了安全机制
   modCount++;
   Entry<?,?> tab[] = table;
   if (count >= threshold) {
        //若是大于阈值,就会从新hash扩容
        rehash();
        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
   }
   //没有异常状况,新建一个Entry根据hash值插入到指定位置便可
   @SuppressWarnings("unchecked")
   Entry<K,V> e = (Entry<K,V>) tab[index];
   tab[index] = new Entry<>(hash, key, value, e);
   count++;
}
复制代码

上面的这些代码的大体意思就是若是容器里面没有满,那就新建一个Entry根据hash值插入到指定位置。并且一开始还提供了modCount确保安全(快速失败机制)。如何去扩容呢?下面咱们接着讲。

二、扩容

扩容就是当容器中放满了,须要把容器扩大。咱们看看这个rehash是如何扩容的。

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;
    //新容量=旧容量 * 2 + 1:实现方式就是oldCapacity << 1
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)  return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    //根据新容量建一个新Map,并赋值给table
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    modCount++;
    //从新计算阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    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;
        }
    }
}
复制代码

上面代码的注释已经很清楚了,不过上面相信你会有一个疑问,不论是put一个元素仍是扩容,在计算hash的时候都出现了(e.hash & 0x7FFFFFFF) ,它的做用是什么呢?

你能够这样理解,hash值是int类型,并且必定是正数,和0x7FFFFFFF作与操做便是将负数变成正数,确保了获取到的index是正数。

三、删除一个元素

public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    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;
            return oldValue;
        }
    }
    return null;
}
复制代码

删除一个元素就比较简单,核心就是经过key计算在容器中的位置,而后把这个位置上的Entry删除便可。因为使用的链表删除起来会更简单。将前一个元素指针直接指向下一个元素,跳过当前元素e.next。

四、查询一个元素

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;
}
复制代码

这个就太简单了,经过key计算hash值找到位置,直接经过e.value获取值便可。

五、迭代器

与HashMap不一样的是,它的迭代器是Enumeration。在这里咱们不会讲解Enumeration,只是给出其基本的使用方法,由于Enumeration我会在专门的文章里面会介绍。这里就再也不重复了。

public static void main(String[] args) {
      Hashtable hashTable = new Hashtable();
      hashTable.put("张三", "1");
      hashTable.put("李四", "2");
      hashTable.put("java的架构师技术栈", "3");
      Enumeration e = hashTable.elements();
      while(e.hasMoreElements()){
         System.out.println(e.nextElement());
      }
}
复制代码

这就是一个最简单的使用方法,

六、其余方法

public synchronized boolean contains(Object value) {
    if (value == null) {
        throw new NullPointerException();
    }
    Entry<?,?> tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
            if (e.value.equals(value)) {
                return true;
            }
        }
    }
    return false;
}
复制代码

这个方法是判断这个容器中是否有value这个值,判断的方法特别死板,就是经过for循环一个一个比较。

3、总结

说实话这个Hashtable使用的场景仍是很局限的,因此通常状况下基本上不会用到,不论是效率仍是空间特性。由于上面的增删改查方法你会发现,全是一个一个比对的,对于数据量大的时候这是很是耗时的,并且存储空间也是采用的链表。

通常来讲非并发场景使用HashMap,并发场景可使用Hashtable,可是推荐使用ConcurrentHashMap,由于它锁粒度更低、效率更高。

Hashtable每每会结合者HashMap来出问题,但愿你们注意,最核心的仍是HashMap。

相关文章
相关标签/搜索