07.Java 集合 - HashTable

来源:互联网 发布:ubuntu下安装eclipse 编辑:程序博客网 时间:2024/06/06 14:04

基本概念

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 找到对应的 valuepublic 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;}// 判断是否包含指定的 keypublic 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;}// 判断是否包含指定的 valuepublic 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;}
0 0
原创粉丝点击