• Java 源码分析之 HashTable


    概念

    散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

    • 注:本文中使用的 JDK 版本为 1.8.0_121。

    定义

    Java 中 Hashtable 的定义如下:

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

    从代码中可以看出 HashTable 继承了 Dictionary 类,实现了 Map<K,V>CloneableSerializable 三个接口。

    Dictionary 类是任何可将键映射到相应值的类(如 HashTable)的抽象父类。每个键和每个值都是一个对象。在任何一个 Dictionary 对象中,每个键至多与一个值相关联。
    Map 将键映射到值的对象。Map 中不能包含重复的键;每个键最多可以映射一个值。

    从 Hashtable 内部 Entry 的定义可以看出 Entry 实现了 Map 接口的 Entry,所以 HashTable 底层的数据结构是基于数组和单向链表。

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

    Hashtable 使用了拉链法解决哈希冲突,拉链法是解决哈希冲突的一种行之有效的方法,某些哈希地址可以被多个关键字值共享,这样可以针对每个哈希地址建立一个单链表。

    在拉链(单链表)的哈希表中搜索一个记录是容易的,首先计算哈希地址,然后搜索该地址的单链表。

    在插入时应保证表中不含有与该关键字值相同的记录,然后按在有序表中插入一个记录的方法进行。针对关键字值相同的情况,现行的处理方法是更新该关键字值中的内容。

    删除记录时,应先在该关键字值的哈希地址处的单链表中找到该记录,然后删除之。

    初始参数

    Hashtable 内部声明了几个重要的参数:

        // 定义存放键值对的 Entry[] 数组,每一个 Entry 代表了一个键值对。
        private transient Entry<?,?>[] table;
    
        // Hashtable 的大小,注意这个大小并不是 HashTable 的容器大小,而是他所包含 Entry 键值对的数量。
        private transient int count;
    
        // 阈值,用于判断是否需要调整 HashTable 的容量。threshold 的值= 容量 * 加载因子。
        private int threshold;
    
        // 加载因子。
        private float loadFactor;
        // 指的是 HashTable 被修改或者删除的次数总数。用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出 ConcurrentModificationException 异常,而不是等到迭代完成之后才告诉你(你已经出错了)。
        private transient int modCount = 0;
    
        // 为了序列化时保持版本的兼容性。
        private static final long serialVersionUID = 1421746759512286392L;
    

    构造函数

    Hashtable 中提供了四个构造函数(旧版本的 JDK 中有五个构造函数):

    // 使用默认初始容量(11)和加载因子(0.75)构造一个新的空 Hashtable。
    Hashtable()
    // 使用指定的初始容量和默认加载因子(0.75)构造一个新的空 Hashtable。
    Hashtable(int initialCapacity)
    // 使用指定的初始容量和指定的加载因子构造一个新的空 Hashtable。
    Hashtable(int initialCapacity, float loadFactor)
    // 使用指定的 Map 构造一个新的 Hashtable。
    Hashtable(Map<? extends K,? extends V> t)
    

    Hashtable 和 HashMap 的初始容量有所不同,HashMap 是 16,而 Hashtable 使用的是 11,扩容逻辑是乘 2+1,保证是素数。关于这个问题我去查了些资料,我理解的是 HashMap 对性能更高一些(参考:HashMap requires a better hashCode),所以在 JDK 1.4 以后做出了改进。知乎中也有大神对这个问题进行了解释【为什么 HashTable 的默认大小和 HashMap 不一样?】。

    其中 Hashtable() 和 Hashtable(int initialCapacity) 两个构造函数都重载了 Hashtable(int initialCapacity, float loadFactor) 。

        public Hashtable(int initialCapacity, float loadFactor) {
            // 如果初始容量小于 0 则抛出异常
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
            // 如果加载因子小于 0 或非浮点类型则抛出异常 
            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];
            // 初始化阈值大小,这里最大值是 Integer.MAX_VALUE + 8 - 1,默认是 8 
            threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        }
    

    最后一个构造函数 Hashtable(Map<? extends K,? extends V> t) 是使用指定的 Map 构造一个具有相同映射关系的新 Hashtable,然后调用了 putAll() 方法将 Map 中的数据逐一放入 table 中。

        public Hashtable(Map<? extends K, ? extends V> t) {
            // 调用 Hashtable(int initialCapacity, float loadFactor) 初始化,默认容器大小是指定 Map 容量大小 * 2
            this(Math.max(2*t.size(), 11), 0.75f);
            // 调用内部 putAll() 方法将 Map 中的数据放入 table
            putAll(t);
        }
    

    主要方法

    Hashtable 中比较常用的方法就是 putget 和 remove,下面分别来看一下每个方法的内部实现。

    put 方法
        public synchronized V put(K key, V value) {
            // 确保 value 不为 null,若为空则抛出异常
            if (value == null) {
                throw new NullPointerException();
            }
    
            // 确保 key 不在哈希表中
            Entry<?,?> tab[] = table;
            
            // 计算 key 的 hashCode
            int hash = key.hashCode();
            // 计算索引
            int index = (hash & 0x7FFFFFFF) % tab.length;
            @SuppressWarnings("unchecked")
            Entry<K,V> entry = (Entry<K,V>)tab[index];
            
            // 遍历 e 和 e 的下一个节点,寻找该 key
            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;
        }
    

    在 Java Docs 中描述如下:

    This class implements a hash table, which maps keys to values. Any non-null object can be used as a key or as a value.

    大概意思是:这个类实现了一个哈希表,它将键映射到值。任何非 null 对象都可以用作键或值。
    注意后面的说明了必须是非空的对象。
    如果向 Hashtable 中添加了一个空的 key。程序会抛出如下异常:

    java.lang.NullPointerException
    

     这个异常是 Hashtable 在计算 key 的 hashCode 时导致的。同样在插入时也对 value 进行了检查,同样会抛出上面的异常。

        private void addEntry(int hash, K key, V value, int index) {
            // 增加被修改或者删除的次数总数
            modCount++;
    
            Entry<?,?> tab[] = table;
            // 如果容器中的元素数量已经达到阀值,则进行扩容操作
            if (count >= threshold) {
                // 进行扩容操作
                rehash();
    
                tab = table;
                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);
            // 增加 Entry 数量
            count++;
        }
    
    get 方法

    Hashtable 的 get 方法中很多代码都与 put 方法相似,很好理解。

        public synchronized V get(Object key) {
            Entry<?,?> tab[] = table;
            int hash = key.hashCode();
            // 计算索引
            int index = (hash & 0x7FFFFFFF) % tab.length;
            // 遍历 e 和 e 的下一个节点,寻找该 key
            for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
                // 判断 hash 和 key 是否想等
                if ((e.hash == hash) && e.key.equals(key)) {
                    return (V)e.value;
                }
            }
            return null;
        }
    
    remove 方法
        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;
        }
    
    addEntry 方法
        private void addEntry(int hash, K key, V value, int index) {
            modCount++;
    
            Entry<?,?> tab[] = table;
            if (count >= threshold) {
                // Rehash the table if the threshold is exceeded
                rehash();
    
                tab = table;
                hash = key.hashCode();
                index = (hash & 0x7FFFFFFF) % tab.length;
            }
    
            // Creates the new entry.
            @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>) tab[index];
            tab[index] = new Entry<>(hash, key, value, e);
            count++;
        }
    
    rehash 方法
     protected void rehash() {
            int oldCapacity = table.length;
            Entry<?,?>[] oldMap = table;
    
            // overflow-conscious code
            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 = newMap;
    
            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;
                }
            }
        }
    

    其他

    SuppressWarnings 注解

    在源码中有很多地方使用了 @SuppressWarnings 注解,@SuppressWarnings 注解作用抑制编译器产生警告信息,unchecked 表示抑制没有进行类型检查操作的警告。在使用 @SuppressWarnings 来排除警告和 Java Docs 描述 有描述 @SuppressWarnings 注解的使用方法。

  • 相关阅读:
    死磕 java线程系列之自己动手写一个线程池(续)
    Spring Boot (十): Spring Boot Admin 监控 Spring Boot 应用
    opencv之为图像添加边界
    协作,才能更好的中断线程
    Java并发——线程池Executor框架
    神经网络dropout
    xgboost
    物体检测-毕业设计项目回顾
    计算机网络-TCP连接
    gbdt推导和代码
  • 原文地址:https://www.cnblogs.com/weisenz/p/2434615.html
Copyright © 2020-2023  润新知