集合框架--HashTable

24 阅读9分钟

1. 简介

Hashtable 是基于哈希表来实现的,每个元素是一个 key-value 形式,内部是数组+链表的数据结构,通过单链表解决 hash 冲突的问题,容量不足的时候也会自动进行扩容操作,存储的数据是无序的;Hashtable 的 JDK1.0 引入的类,基本已经被废弃使用;Hashtable 的重要方法使用 synchronized 关键字修饰,是线程安全的;key 和 value 都不可以为 null,否则将会抛出 NullPointerException 异常。

与 HashMap 相似,底层采用数组+链表的数据结构,根据 key 找到相应的桶,相同的 key 通过链表维护,当数组桶达到阈值后也会进行动态的扩容,但是 Hashtable 不会转换为红黑树。默认大小为 11

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {}
  • 实现了 Map 接口,提供了键值对的增删改查等基础操作
  • 继承了 Directory 字典类
  • 实现了 Cloneable 接口,支持拷贝
  • 实现了 Serializable 接口,支持序列化和反序列化

Hashtable 和 HashMap 不同点:

  1. 线程安全方面:Hashtable 线程安全、HashMap 线程不安全
  2. 底层数据结构:Hashtable 数组 + 链表、HashMap 数组 + 链表 + 红黑树
  3. 默认初始容量:Hashtable 是 11、HashMap 是 16(2 的次幂)
  4. 扩容大小:Hashtable 原来大小的2倍+1、HashMap 是原来大小的 1.5 倍数
  5. Hashtable 不允许 key 和 value 为空、HashMap 可以允许一个 key 为null,任意 value 为 null

2. 成员变量

/**
 * 使用 Entry 数组存储键值对对象,Entry 实际为单向链表的表头
 */
private transient Entry<?,?>[] table;
/**
 * The total number of entries in the hash table. 
    数组元素个数
 */
private transient int count;
/**
 * The table is rehashed when its size exceeds this threshold.  (The
 * value of this field is (int)(capacity * loadFactor).)
 * 扩容阈值,当超过这个值后会进行扩容操作,计算方式:数组容量 * 加载因子
 * @serial
 */
private int threshold;
/**
 * The load factor for the hashtable.
 * 加载因子
 * @serial
 */
private float loadFactor;
/**
 * Hashtable bucket collision list entry
 */
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;
    }
    ...
}

3. 构造方法

这里的构造方法不像 HashMap 中的懒加载方式,而是在构造对象的时候就先创建一个 hash 表放在那里

/**
 * Constructs a new, empty hashtable with the specified initial
 * capacity and the specified load factor.
 *
 * @param      initialCapacity   the initial capacity of the hashtable.
 * @param      loadFactor        the load factor of the hashtable.
 * @exception  IllegalArgumentException  if the initial capacity is less
 *             than zero, or if the load factor is nonpositive.
 */
// 用指定的容量大小和加载因子构造一个新的哈希表
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);
    // 初始容量
    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    // 初始化 table 对象,创建大小为 initialCapacity 的数组
    table = new Entry<?,?>[initialCapacity];
    // 计算阈值
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
/**
 * Constructs a new, empty hashtable with the specified initial capacity
 * and default load factor (0.75).
 *
 * @param     initialCapacity   the initial capacity of the hashtable.
 * @exception IllegalArgumentException if the initial capacity is less
 *              than zero.
 */
// 指定初始容量,使用默认加载因子构造一个空的哈希表
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
/**
 * Constructs a new, empty hashtable with a default initial capacity (11)
 * and load factor (0.75).
    无参构造方法,容量为 11 ,加载因子为 0.75
 */
public Hashtable() {
    this(11, 0.75f);
}
/**
 * Constructs a new hashtable with the same mappings as the given
 * Map.  The hashtable is created with an initial capacity sufficient to
 * hold the mappings in the given Map and a default load factor (0.75).
 *
 * @param t the map whose mappings are to be placed in this map.
 * @throws NullPointerException if the specified map is null.
 * @since   1.2
 */
public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}

4. put 方法

// 线程安全
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    // 确保 value 不为空,如果为 null 抛出空指针异常
    if (value == null) {
        throw new NullPointerException();
    }
    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    // 得到下标,先对 hash 值取整然后取余   hash&0x7FFFFFF 为了保证结果为正数
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    // 如果存在相同的 key 则遍历链表把那个 key 对应的 value 换成新的 value
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            // 迭代 index 索引位置,如果该位置处的链表中存在一样的key,则替换 value 返回旧值
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    // 如果上面循环结束了还是没有在表中找打相同 hash 和 key 的值,那就调用该方法进行插入
    addEntry(hash, key, value, index);
    return null;
}
​
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")
    // 直接构建结点,放到table中,作为链表的头节点
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

小总结:

  1. 对 value 做非空判断,hashtable 不允许 key、value 为空
  2. 根据 key 的 hashCode 找到对应的索引
  3. 如果存在相同的 key 则遍历链表把那个 key 对应的 value 换成新的 value
  4. 判断表大小是否超出阈值,如果是调用 rehash 方法扩容
  5. 直接将键值对插入到 table数组中,作为链表的头节点

5. rehash 方法

/**
 * Increases the capacity of and internally reorganizes this
 * hashtable, in order to accommodate and access its entries more
 * efficiently.  This method is called automatically when the
 * number of keys in the hashtable exceeds this hashtable's capacity
 * and load factor.
    增加哈希表的容量并在内部重组该哈希表,以便更有效地容纳和访问其条目。 
    当哈希表中的键数超过此哈希表的容量和负载因子时,将自动调用此方法。
 */
@SuppressWarnings("unchecked")
protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;
    // overflow-conscious code
    // 新容量 = 旧容量的 * 2 + 1
    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;
    }
    // 初始化一个新的 hash表,size 为 newCapacity
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    modCount++;
    // 计算新的阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    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;
        }
    }
}

小总结

在向集合中添加元素的时候调用 addEntry 首先会进行容量检验判断,查看当前数组大小是否超过阈值(count >= threshold)如果是就会调用 rehash 方法进行扩容。

  1. 通过对就数组值左移1位+1 得到新的数组的容量,即(旧容量 * 2 + 1)
  2. 判断是否达到最大容量
  3. 创建一个新容量大小的数组
  4. 通过 数组容量 * 负载因子得到新的阈值
  5. 循环遍历旧的表,对每一个元素的key重新 hash 然后放入新表的相应位置

6. get 方法

/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 *
 * <p>More formally, if this map contains a mapping from a key
 * {@code k} to a value {@code v} such that {@code (key.equals(k))},
 * then this method returns {@code v}; otherwise it returns
 * {@code null}.  (There can be at most one such mapping.)
 *
 * @param key the key whose associated value is to be returned
 * @return the value to which the specified key is mapped, or
 *         {@code null} if this map contains no mapping for the key
 * @throws NullPointerException if the specified key is null
 * @see     #put(Object, Object)
 */
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    // 计算 key 的 hash 值
    int hash = key.hashCode();
    // 得到下标位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    // 遍历找到并返回
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

小总结:

  1. 计算 key 的 hash 值
  2. 计算下标位置
  3. 迭代链表,匹配找到相应 key 的 value并返回,未找到则返回 null

7. remove 方法

/**
 * Removes the key (and its corresponding value) from this
 * hashtable. This method does nothing if the key is not in the hashtable.
 *  从此哈希表中删除键(及其对应的值)。 如果键不在哈希表中,则此方法不执行任何操作。
 * @param   key   the key that needs to be removed
 * @return  the value to which the key had been mapped in this hashtable,
 *          or <code>null</code> if the key did not have a mapping
 * @throws  NullPointerException  if the key is <code>null</code>
 */
public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    // 根据 key 计算下标
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    // 判断 e != null,表示下标的位置是有元素的
    // 遍历链表,找到 key 相等的节点,该表前后链表关系,将 value 置为空等待 GC 回收
    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;
}

8. 常用方法

  1. boolean isEmpty()

    功能:测试此哈希表是否没有键映射到值

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            System.out.println(hashtable.isEmpty());
        }
    }
    
    运行结果:
        true
    
  2. Object put(Object key, Object value)

    功能:将指定 key 映射到此哈希表中的指定 value

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            System.out.println(hashtable.toString());
            //String toString():返回此 Hashtable 对象的字符串表示形式,
        }
    }
    
    运行结果:
        {1=aaa}
    
  3. int size()

    功能:返回此哈希表中的键的数量

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            System.out.println(hashtable.size());
        }
    }
    
    运行结果:
    	1
    
  4. Enumeration elements(),Enumeration keys()

    功能:Enumeration elements() 返回此哈希表中的值的枚举

    功能:Enumeration keys() 返回此哈希表中的键的枚举

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            hashtable.put(2,"bbb");
            Enumeration<Object> elements = hashtable.elements();
            while (elements.hasMoreElements()){
                System.out.println(elements.nextElement());
            }
        }
    }
    
    运行结果:
        bbb
        aaa
        
        
    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            hashtable.put(2,"bbb");
            hashtable.put(3,"ccc");
            Enumeration<Object> keys = hashtable.keys();
            while (keys.hasMoreElements()){
                System.out.println(keys.nextElement());
            }
        }
    }
    
    运行结果:
        3
        2
        1
    
  5. Object get(Object key)

    功能:返回指定键所映射到的值,如果此映射不包含此键的映射,则返回 null

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            hashtable.put(2,"bbb");
            System.out.println(hashtable.get(1));
            System.out.println(hashtable.get(3));
        }
    }
    
    运行结果:
        aaa
        null
    
  6. contains()

    • boolean contains(Object value):测试此映射表中是否存在与指定值关联的键
    • boolean containsKey(Object key):测试指定对象是否为哈希表中的值
    • boolean containsValue(Object value):如果此Hashtable 将一个或多个键映射到此值,则返回true
    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            hashtable.put(2,"bbb");
            hashtable.put(2,"aaa");
            System.out.println(hashtable.contains("aaa"));
            System.out.println(hashtable.containsKey(2));
            System.out.println(hashtable.containsValue("aaa"));
        }
    }
    
    运行结果:
        true
        true
        true
    
  7. Object remove(Object key)

    功能:从哈希表中移除该键及其相应的值

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            hashtable.put(2,"bbb");
            hashtable.put(3,"ccc");
            System.out.println(hashtable.toString());
            hashtable.remove(3);
            System.out.println(hashtable.toString());
        }
    }
    
    运行结果:
        {3=ccc, 2=bbb, 1=aaa}
    	{2=bbb, 1=aaa}
    
  8. Object clone()

    功能:创建此哈希表的副本

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            hashtable.put(2,"bbb");
            hashtable.put(3,"ccc");
            Object clone = hashtable.clone();
            System.out.println(hashtable.toString());
            System.out.println(clone.toString());
        }
    }
    
    运行结果:
        {3=ccc, 2=bbb, 1=aaa}
    	{3=ccc, 2=bbb, 1=aaa}
    
  9. void clear()

    功能:将此哈希表清空,使其不包含任何键

    public class test {
        public static void main(String[] args) {
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put(1,"aaa");
            hashtable.put(2,"bbb");
            hashtable.put(3,"ccc");
            System.out.println(hashtable.toString());
            hashtable.clear();
            System.out.println(hashtable.toString());
        }
    }
    
    运行结果:
        {3=ccc, 2=bbb, 1=aaa}
    	{}
    

参考文献:

juejin.cn/post/693690…

juejin.cn/post/684490…