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 注解的使用方法。

posted @ 2012-04-06 14:18  Charles Zhang  阅读(1405)  评论(0编辑  收藏  举报