万字长文|Hashtable源码深度解析以及与HashMap的区别

基于JDK1.8对Java中的Hashtable集合的源码进行了深度解析,包括各种方法、扩容机制、哈希算法、遍历方法等方法的底层实现,最后给出了Hashtable和HashMap的详细对比以及使用建议。

1 Hashtable的概述

public class Hashtable< K,V > extends Dictionary< K,V > implements Map< K,V >, Cloneable, Serializable

Hashtable是来自于JDK1.0时代的古老key-value形式的集合类。类当中所有的方法都是同步的,数据安全的,效率低。

JDK1.0的时候Hashtable是继承的抽象类Dictionary,JDK1.2集合框架诞生之后,又实现了Map 接口,成为了Java集合体系的一员。

实现了Cloneable、Serializable标志性接口,支持克隆、序列化操作。

由于Map不属于Collection集合体系,没有实现Iterable接口,因此不支持获取迭代器的方法iterator(),或者说Map的集合体系并没有真正的迭代器。但是它们有自己的遍历数据的方法。

Hashtable的底层实际上是采用“拉链法”实现了一个哈希表,即使用一个数组作为哈希表的骨架,每一个数组元素的位置称为“bucket”桶,桶里存放的就是哈希值相同的键值对,如果一个桶里面有多个键值对,那么说明出现了哈希冲突,Hashtable使用“拉链法”解决冲突,每个桶的大小即该位置链表节点数量。Hashtable的key 和 value 都不允许为null。

2 Hashtable的源码解析

2.1 主要类属性

/**
 * 内部Entry[]数组,用来作为哈希表的骨架,数组每一个Entry元素代表了一个链表的头节点,Hashtable内部的哈希表的key-value键值对都是存储在Entry节点中的。
 */
private transient Entry<?, ?>[] table;

/**
 * HashTable的大小,注意这个大小并不是HashTable的容器大小,而是他所包含Entry键值对的数量。
 */
private transient int count;

/**
 * Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。当count大于等于threshold时,需要调整容量(尝试扩容)。
 */
private int threshold;

/**
 * 加载因子,是可以大于1的。
 */
private float loadFactor;

/**
 * 用来实现"fail-fast"机制的(也就是快速失败)。
 */
private transient int modCount = 0;

扩容阈值是由出初始容量和加载因子共同决定的,通常threshold=table.length*loadFactor,初始容量和加载因子越大,那么就不需要频繁的“扩容”,初始容量过大可能会浪费更多空间,加载因子越大会增加哈希冲突的风险,导致查找数据的时间过长。默认容量(11)和加载因子(0.75)在时间和空间成本上寻求一种折衷。

关于modCount 的作用和fail-fast机制,早在ArrayLsit集合的源码文章中就已经讲解了,java.util包下的集合的fail-fast机制都是一样的,这里不再赘述,详情可以看这篇文章:Java集合—ArrayList的源码深度解析以及应用介绍。

2.2 Entry节点

Entry实际上就是Hashtable的一个内部类,作为内部存储key和value的容器,还保存key的hashCode值,同时由于Hashtable采用“拉链法”实现哈希表,每一个Entry还作为链表的一个节点,因此内部还有一个到下一个节点的引用属性。

实际上Entry实现了Map.Entry接口,因此Entry内部还实现了相关方法共外部调用。EntrySet()方法返回的set集合的元素May.Entry,实际上就是返回的这个Entry节点的实例,后面会详细讲解!

private static class Entry<K,V> implements Map.Entry<K,V> {
    //哈希值,存储起来方便后续使用,避免重复运算
    final int hash;
    //key
    final K key;
    //value
    V value;
    //下一个相同桶位的节点引用
    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()));
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

    public V setValue(V value) {
        if (value == null)
            throw new NullPointerException();

        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    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()));
    }

    public int hashCode() {
        return hash ^ Objects.hashCode(value);
    }

    public String toString() {
        return key.toString()+"="+value.toString();
    }
}

据此,我们能够画出Hashtable的大概数据结构图:

 

2.3 构造器与初始化参数

2.3.1 Hashtable()

public Hashtable()

构造一个新的,空的散列表,默认初始容量(11)和加载因子(0.75)。

public Hashtable() {
    //内部调用另外一个构造器,初始容量11,加载因子0.75
    this(11, 0.75f);
}

2.3.2 Hashtable(int initialCapacity)

public Hashtable(int initialCapacity)

用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。

public Hashtable(int initialCapacity) {
    //内部调用另外一个构造器,用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。
    this(initialCapacity, 0.75f);
}

2.3.3 Hashtable(int initialCapacity, float loadFactor)

public Hashtable(int initialCapacity,float loadFactor)

用指定初始容量和指定加载因子构造一个新的空哈希表。加载因子可以大于1,但是很明显,加载因子越大,发生哈希冲突的概率也越大!

这里的initialCapacity也没有要求是2的幂次方,但是HashMap 中初始化容量大小必须是 2 的幂次方。

/**
 * 建议数组最大容量,因为某些VM实现可能需要部分长度用来存放数组头部信息
 * 但是在HotSopt的虚拟机中,数组长度是可以超过这个限制的,可以达到Integer.MAX_VALUE – 2的长度
 * 并且在上面的源码中能够看到,我们分配的initialCapacity完全可以大于MAX_ARRAY_SIZE
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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 = new Entry<?, ?>[initialCapacity];
    //计算扩容阈值,取initialCapacity * loadFactor和MAX_ARRAY_SIZE + 1的最小值
    threshold = (int) Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

2.3.4 Hashtable(Map<? extends K,? extends V> t)

public Hashtable(Map<? extends K,? extends V> t)

构造一个与给定的 Map 具有相同映射关系的新哈希表。该哈希表是用足以容纳给定 Map 中映射关系的初始容量和默认的加载因子(0.75)创建的。

public Hashtable(Map<? extends K, ? extends V> t) {
    //首先初始化hashtable
    this(Math.max(2*t.size(), 11), 0.75f);
    //底层调用putAll方法
    putAll(t);
}

2.4 put方法与

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值