深入理解 Hashtable

什么是 Hashtable ?

Hashtable 是一个散列表,它是以键值对来存储的,这点与 HashMap 一样

Hashtable 的继承结构

在这里插入图片描述
Hashtable 继承了 Dictionary,并且实现了 Map、Cloneable、Serializable 接口

其中 Dictionary 是一个抽象类,是任何可将映射到相应值的类的抽象父类,每个键和每个值都是一个对象。在任何一个 Dictionary 对象中,每个键最多只能与一个值相关联

Hashtable 几个重要的成员变量

在 Hashtable 中是通过 “拉链法” 来实现的。它包括几个重要的成员变量,如下:

// 单向链表,Hashtable 中的 key-value 键值对都是存储在这个 Entry 数组中的
private transient Entry<?,?>[] table;

// Hashtable 中存储的键值对的数量
private transient int count;

// Hashtable 的阈值,用于判断是否需要调整 Hashtable 的容量
private int threshold;

// 加载因子
private float loadFactor;

// 修改值,用来实现 fail-fast 机制用的
private transient int modCount = 0;

Hashtable 的构造函数

Hashtable(int initialCapacity, float loadFactor)

传入 initialCapacity(初始容量)和 loadFactor(加载因子)构建 Hashtable

// 设置初始大小和加载因子
public Hashtable(int initialCapacity, float loadFactor) {
	// 如果初始化的容量 < 0 的话抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    // 如果加载因子小于 0  或者加载因子不是 float 类型的话抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

	// 如果初始化容量为 0,则设置初始化容量为 1
    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;

	// 初始化 table,设置 initialCapacity 大小的 table 数组
    table = new Entry<?,?>[initialCapacity];

	// 计算阈值
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

Hashtable(int initialCapacity)

传入指定初始容量构建一个新的 Hashtable

public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}

Hashtable()

默认的构造函数,初始容量大小为 11,加载因子为 0.75 f

public Hashtable() {
    this(11, 0.75f);
}

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

构建一个与给定 Map 具有相同映射关系的新的 Hashtable

public Hashtable(Map<? extends K, ? extends V> t) {
	// 设置 table 容器大小
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}

Hashtable 的主要方法

put(K key, V value)

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    // 确保 value 不能为空,否则会抛出异常
    if (value == null) {
        throw new NullPointerException();
    }

    // 确保 key 在 table 中是不可重复的,唯一的
    Entry<?,?> tab[] = table;
    // 计算 key 的 hash 值
    int hash = key.hashCode();
    // 将 key 的 hash 值进行与远算再除以数组的长度得到索引位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    // 迭代遍历,寻找该 key,进行替换
    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;
}

addEntry(int hash, K key, V value, int index)

private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    // 如果容器中的元素数量已经达到阈值,则进行扩容
    if (count >= threshold) {
        // 扩容方法
        rehash();

		// 移动到新的 Entry 数组
        tab = table;
        // 重新计算 hashCode
        hash = key.hashCode();
        // 重新计算索引位置
        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++;
}

rehash()

扩容方法

protected void rehash() {
	// 获取数组的长度
    int oldCapacity = table.length;
    // 获取数组的元素
    Entry<?,?>[] oldMap = table;

    // 新的容量 = 旧的容量 * 2 + 1
    int newCapacity = (oldCapacity << 1) + 1;
	
	// 如果新的容量 - Integer.MAX_VALUE - 8 大于 0,也就是溢出了
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
    	// 如果旧的容量 == Integer.MAX_VALUE - 8
        if (oldCapacity == MAX_ARRAY_SIZE)
            return;
        // 继续使用 Integer.MAX_VALUE - 8 作为新的容量
        newCapacity = MAX_ARRAY_SIZE;
    }
	
	// 新建一个容量大小为 Integer.MAX_VALUE - 8 的 Entry 数组
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    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 是如何确定存储位置的?

那么 Hashtable 是如何确定存储位置的呢,也就是索引的位置。在上述代码中也重复出现了计算索引的代码,我们将这段代码复制修改如下:

/**
  * 计算索引位置
  * @param hashCode 传入 key 的hashCode 值
  * @return 返回索引位置
  */
public static int getIndex(int hashCode) {
    int index = (hashCode & 0x7FFFFFFF) % 11;
    return index;
}

OK,就是这么一段核心的代码,我们测试一下:

import java.util.Hashtable;

/**
 * @author Woo_home
 * @create by 2020/7/14  12:29
 */
public class Demo {

    /**
     * 计算索引位置
     * @param hashCode 传入 key 的hashCode 值
     * @return 返回索引位置
     */
    public static int getIndex(int hashCode) {
        int index = (hashCode & 0x7FFFFFFF) % 11;
        return index;
    }

    public static void main(String[] args) {
        Hashtable<String, String> hashtable = new Hashtable<>();
        hashtable.put("name", "John");
        hashtable.put("age", "18");
        System.out.println(hashtable);
        // 计算存储的索引位置
        System.out.println("nameIndex : " + getIndex("name".hashCode()));
        System.out.println("ageIndex : " + getIndex("age".hashCode()));
    }
}

输出:
在这里插入图片描述
从输出结果可以发现,name 和 age 存储的索引位置分别是 7 和 8

get(Object key)

Hashtable 的 get() 方法,这个方法还是比较简单的

public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    // 首先还是先计算 key 的hashCode 值
    int hash = key.hashCode();
    // 然后获得 table 数组中的索引位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    // 然后迭代链表
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    	// 如果找到相对应的 key 的 value,则返回
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    // 如果没有找到则返回 null
    return null;
}

Hashtable 与 HashMap 有什么区别?

1、是否线程安全

HashMap 是线程不安全的,Hashtable 是线程安全的,在 Hashtable 中内部的方法基本都是经过 synchronized 修饰的
在这里插入图片描述

2、效率

因为 Hashtable 是线程安全的,所以效率上比 HashMap 要低。另外,HashTable 基本要被淘汰了

3、键值能否为空?

在 HashMap 中,null 可以作为键,也可以作为值,但是只能有一个键为 null,可以有多个值为 null。而 HashTable 是不允许键值为 null 的,不然会抛出空指针异常
在这里插入图片描述

4、默认初始容量大小:

HashTable 在创建时如果不指定容量的初始值时,默认初始容量大小为 11。HashMap 默认的初始容量大小为 16

5、每次扩容大小

HashTable 每次扩容,容量会变成原来的 2n+1 倍,而 HashMap 每次扩容,容量会变成原来的 2 倍。如果创建时给定了容量的初始值,那么 HashTable 会直接使用给定的值,而 HashMap 会将其扩充为 2 的幂次方大小

6、底层数据结构

在 JDK 1.8 中的 HashMap 使用的是数组 + 链表 + 红黑树的数据结构,在解决哈希冲突时有了较大的变化,当链表长度大于阈值的的时候(默认为 8 时),将链表转换为红黑树,以减少搜索的时间。HashTable 没有这样的机制

相关文章

深入理解 HashMap

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值