07.Java 集合 - HashTable

基本概念

1.结构

首先来看它的继承结构:

这里写图片描述

再来看看它的结构图,HashTable 是基于哈希表(hash table)实现的map。而哈希表的组成是一个数组,而数组的元素是则单向链表的首节点。

这里写图片描述


2.特点

线程安全,并且不允许 key 或 value 为 null 。

与 HasMap 的底层结构相同,不同的是:

  • HashMap 允许 key,value 为 null;

  • HashMap 的初始容量必须为 2 的倍数,而 HashTable 只要求不为 0 即可;

  • 关于数组索引位置的计算公式不同。


3.初始容量 和加载因子

Hashtable 的实例有两个参数影响其性能:初始容量 和加载因子

  • 容量,是哈希表中桶(bucket)的数量,初始容量 就是哈希表创建时的容量。在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。

  • 初始容量,主要控制空间消耗与执行 rehash 操作所需要的时间损耗之间的平衡。如果初始容量大于 Hashtable 所包含的最大条目数除以加载因子,则永远 不会发生 rehash 操作。但是,将初始容量设置太高可能会浪费空间。

  • 加载因子,是对哈希表在其容量自动增加之前可以达到多满的一个尺度。

  • 默认加载因子(.75),在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。


源码分析

1.节点

HashTable 中存放的元素,也称节点,由 Entry 构成。结构如图所示:

这里写图片描述

观察它的构造函数,是由 h(哈希值),key(键),value(值),Entry(下一个节点)这几个参数组成。

  • 通过 key-value 构成了 map 的 映射关系

  • 通过 Entry (即 next)连接它的下一节点,构成了单向链表

private static class Entry<K, V> implements Map.Entry<K, V> {
    int hash; 
    K key;
    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;
    }

    // 计算哈希码值
    public int hashCode() {
        return hash ^ (value == null ? 0 : value.hashCode());
    }

    //....省略剩余方法
}

2.构造函数

观察代码构造函数 ①~③ 都调用了构造函数 ④ 。

而该方法主要完成了参数的初始化(loadFactor,threshold)以及数组(table [])的创建工作。

注意:在 HashMap 中,初始化容量(initialCapacity) 必须是 2 的倍数,而 HashTable 只要求不为 0 即可。

// 内部数组
private transient Entry[] table;

// 加载因子
private float loadFactor;

// 临界值
private int threshold;

// 实际存放元素个数
private transient int count;

// 修改次数
private transient int modCount = 0;

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

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

// ③
public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2 * t.size(), 11), 0.75f);

    // 添加 map 的所有映射(在添加操作会介绍)
    putAll(t);
}

// ④ --> 负责具体实现
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);
    }

    // 在 HashMap 初始化容量必须为 2的倍数 
    if (initialCapacity == 0) {
        initialCapacity = 1;
    }

    // 初始化参数,并创建数组
    this.loadFactor = loadFactor;
    table = new Entry[initialCapacity];
    threshold = (int) (initialCapacity * loadFactor);
}

3.添加操作(putAll,put)

这里提供了两种了添加方式, putAll 会去遍历指定 map 的所有 key-value,然后再调用 put 将其逐个添加进哈希表。因此重点来看 put 的工作流程,如下图所示:

这里写图片描述

  • 判断 value 是否为空,为空抛出。与 HashMap 不同, 在 HashTable 中不允许空值,而 HashMap 则允许

  • 计算 key-value 在哈希表中的位置。同样,这里的计算公式也与 HashMap 有差别

  • 找到该位置的节点(table 数组中存放的都是单链表的首节点),遍历操作。

  • 比较 key,存在则替换 key;

  • 不存在,先扩充容量。再添加 key-value 作用新的首节点。

//添加指定的 map 
public synchronized void putAll(Map<? extends K, ? extends V> t) {
    // 遍历 Map 的映射关系
    for (Map.Entry<? extends K, ? extends V> e : t.entrySet()) {
        // 添加一个映射关系
        put(e.getKey(), e.getValue());
    }
}


//添加单个映射关系
public synchronized V put(K key, V value) {

    // 与 HashMap 不同,不允许 value 为空
    if (value == null) {
        throw new NullPointerException();
    }

    Entry tab[] = table;

    // 与 HashMap 计算方式不同,计算得到数组的索引位置
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;

    // 遍历该位置的单链表
    for (Entry<K, V> e = tab[index]; e != null; e = e.next) {

        // 存在相同的 key,替换 value,并返回旧值
        if ((e.hash == hash) && e.key.equals(key)) {
            V old = e.value;
            e.value = value;
            return old;
        }
    }

    // 若不存在相同的 key,扩充完容量,并重新计算索引值
    modCount++;
    if (count >= threshold) {
        // 扩充容量
        rehash();
        tab = table;

        // 重新计算索引位置
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // 添加新节点为首节点
    Entry<K, V> e = tab[index];
    tab[index] = new Entry<K, V>(hash, key, value, e);

    count++;
    return null;
}


// 调整 HashTable 的容量,=(旧容量*2+1)
protected void rehash() {
    int oldCapacity = table.length;
    Entry[] oldMap = table;
    // 扩充容量
    int newCapacity = oldCapacity * 2 + 1;
    // 创建新数组
    Entry[] newMap = new Entry[newCapacity];
    modCount++;
    threshold = (int) (newCapacity * loadFactor);
    table = newMap;
    // 复制元素到新数组
    for (int i = oldCapacity; i-- > 0;) {
        for (Entry<K, V> old = oldMap[i]; old != null;) {
            Entry<K, V> e = old;
            old = old.next;
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = newMap[index];
            newMap[index] = e;
        }
    }
}

4.删除操作(remove,clear)

同样有两种删除操作。重点来看下 remove 。观察代码,发现与 put 的流程相似。不同的是 put 是添加/修改,而它是删除

这里来分析删除操作,即从单链表中移除节点的具体流程:

  • 首先要判断是不是首节点

  • 若是首节点的话,则将它的下一节点设为数组元素(因为在数组中存在的都是单链表的首节点)。并将该节点置空,等待 gc 回收。

  • 若不是,则需要修改该节点前置节点的指针,将其指向该节点的下一节点。如下图所示:

这里写图片描述

// 移除指定的映射关系
public synchronized V remove(Object key) {

    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;

    for (Entry<K, V> e = tab[index], 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;
}


// 清空操作
public synchronized void clear() {
    Entry tab[] = table;
    modCount++;
    for (int index = tab.length; --index >= 0;) {
        tab[index] = null;
    }
    count = 0;
}

5.查询操作(get,containsKey,contains)

观察 getcontainsKey 的代码,发现二者的代码基本相同。甚至与 remove,put 也相差不大。

都遵循了[计算哈希值 -> 计算索引位置 -> 遍历单链表 -> 判断 key 是否相同 -> …] 这几个步骤。

因此这里重点来看下 contains 方法,它与 get,containsKey 不同。由于不能通过 value 得到数组的索引位置,只能遍历整个哈希表的元素(节点)。

因此它的步骤是 [判断是否为空 -> 遍历数组 -> 遍历单链表 -> 比较 value是否相同 -> …]

// 根据 key 找到对应的 value
public synchronized V get(Object key) {
    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;

    for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return e.value;
        }
    }
    return null;
}


// 判断是否包含指定的 key
public synchronized boolean containsKey(Object key) {
    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;

    for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return true;
        }
    }
    return false;
}


// 判断是否包含指定的 value
public synchronized boolean contains(Object value) {
    // 不允许 value 为空
    if (value == null) {
        throw new NullPointerException();
    }
    Entry tab[] = table;

    // 从后向前遍历数组(因为不能从 value 计算中节点在哈希表中的位置 )
    for (int i = tab.length; i-- > 0;) {
        for (Entry<K, V> e = tab[i]; e != null; e = e.next) {
            // 与 key 不同,只比较值
            if (e.value.equals(value)) {
                return true;
            }
        }
    }
    return false;
}

6.遍历操作(entrySet,keySet,values)

分别实现了对 Entry(节点),key,value 的遍历操作。

以 entrySet 为例,该方法通过 Collections 的同步方法创建了一个 Hashtable.EntrySet(内部类) 的实例;

相应的 keySet ,values 分别创建了 Hashtable.KeySet,Hashtable.AbstractCollection 的实例。

private transient volatile Set<Map.Entry<K, V>> entrySet = null; 

public Set<Map.Entry<K, V>> entrySet() {
    if (entrySet == null) {
        entrySet = Collections.synchronizedSet(new EntrySet(), this);
    }
    return entrySet;
}

以 Hashtable.EntrySet 为例子,重点来看它的 iterator 方法。

private static final int KEYS = 0;
private static final int VALUES = 1;
private static final int ENTRIES = 2;

private class EntrySet extends AbstractSet<Map.Entry<K, V>> {

    // 迭代器
    public Iterator<Map.Entry<K, V>> iterator() {
        return getIterator(ENTRIES);
    }

    //...省略部分代码
}

在三个内部类的 iterator 方法中都调用 getIterator 方法来创建迭代器,通过传入的 type 区分类型。

在 getIterator 中,首先会判断哈希表的元素个数。若为 0,返回 HshTable.EmptyIterator;不为 0,返回 HshTable.Enumerator

private static Iterator emptyIterator = new EmptyIterator();

private <T> Iterator<T> getIterator(int type) {
    if (count == 0) {
        return (Iterator<T>) emptyIterator;
    } else {
        return new Enumerator<T>(type, true);
    }
}

下面来分析下 Enumerator 的源码,当它表示迭代器时可以操作 next,hasNext ,remove方法;当它表示枚举类时,不能操作 remove 方法。

private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
    Entry[] table = Hashtable.this.table;
    int index = table.length;
    Entry<K, V> entry = null;
    Entry<K, V> lastReturned = null;
    int type;

    // Enumerator是 “迭代器(Iterator)” 还是 “枚举类(Enumeration)”的标志
    // 为true,表示它是迭代器;否则,是枚举类
    boolean iterator;

    // 在将Enumerator当作迭代器使用时会用到,用来实现fail-fast机制。
    protected int expectedModCount = modCount;

    // 构造函数
    Enumerator(int type, boolean iterator) {
        this.type = type;
        this.iterator = iterator;
    }

    // 关键--> 判断迭代器是否还有数据
    public boolean hasNext() {
        return hasMoreElements();
    }

    public boolean hasMoreElements() {
        Entry<K, V> e = entry;
        int i = index;
        Entry[] t = table;

        // 从后向前遍历数组元素
        while (e == null && i > 0) {
            e = t[--i];
        }
        entry = e;
        index = i;
        return e != null;
    }

    // 关键 --> 取得下一个节点
    public T next() {
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        return nextElement();
    }

    public T nextElement() {
        Entry<K, V> et = entry;
        int i = index;
        Entry[] t = table;
        /* Use locals for faster loop iteration */
        while (et == null && i > 0) {
            et = t[--i];
        }
        entry = et;
        index = i;
        if (et != null) {
            Entry<K, V> e = lastReturned = entry;
            entry = e.next;
            return type == KEYS ? (T) e.key : (type == VALUES ? (T) e.value : (T) e);
        }
        throw new NoSuchElementException("Hashtable Enumerator");
    }

    // 移除当前迭代的节点
    public void remove() {
        // 当该类表示迭代器时才能调用该方法
        if (!iterator) {
            throw new UnsupportedOperationException();
        }

        if (lastReturned == null) {
            throw new IllegalStateException("Hashtable Enumerator");
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

        synchronized (Hashtable.this) {
            Entry[] tab = Hashtable.this.table;
            int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;

            // 计算索引位置,遍历单链表
            for (Entry<K, V> e = tab[index], prev = null; e != null; prev = e, e = e.next) {
                // 判断是不是当前迭代的节点
                if (e == lastReturned) {
                    modCount++;
                    expectedModCount++;
                    if (prev == null) {
                        tab[index] = e.next;
                    } else {
                        prev.next = e.next;
                    }
                    count--;
                    lastReturned = null;
                    return;
                }
            }
            throw new ConcurrentModificationException();
        }
    }
}

7.工具类方法

public synchronized int size() {
    return count;
}

public synchronized boolean isEmpty() {
    return count == 0;
}
【6层】一字型框架办公楼(含建筑结构图、计算书) 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
1、资源项目源码均已通过严格测试验证,保证能够正常运行;、 2项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oxf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值