Map--Hashtable

Map

Map集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。Java自带了各种Map类,主要分为以下三类:

1、通用Map,用于在应用程序中管理映射,通常放置在java.util包中:HashMap、Hashtable、Properties、LinkedHashMap、IdentityHashMap、TreeMap、WeakHashMap、ConcurrentHashMap

2、专用Map,通常我们不必亲自创建此类Map,而是通过某些其他类对其进行访问:java.util.jar.Attributes、javax.print.attribute.standard.PrinterStateReasons、java.security.Provider、java.awt.RenderingHints、javax.swing.UIDefaults

3、一个用于帮助我们实现自己的Map类的抽象类AbstractMap

Map的常用方法

1 void clear( ) 从此映射中移除所有映射关系(可选操作)。
2 boolean containsKey(Object k) 如果此映射包含指定键的映射关系,则返回 true。
3 boolean containsValue(Object v) 如果此映射将一个或多个键映射到指定值,则返回 true。
4 Set entrySet( ) 返回此映射中包含的映射关系的 Set 视图。
5 boolean equals(Object obj) 比较指定的对象与此映射是否相等。
6 Object get(Object k) 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
7 int hashCode( ) 返回此映射的哈希码值。
8 boolean isEmpty( ) 如果此映射未包含键-值映射关系,则返回 true。
9 Set keySet( ) 返回此映射中包含的键的Set视图。
10 Object put(Object k, Object v) 将指定的值与此映射中的指定键关联(可选操作)。
11 void putAll(Map m) 从指定映射中将所有映射关系复制到此映射中(可选操作)。
12 Object remove(Object k) 如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。
13 int size( ) 返回此映射中的键-值映射关系数。
14 Collection values( ) 返回此映射中包含的值的 Collection 视图。

Hashtable

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

​ 实现了Map接口,继承了Dictionary类,Dictionary类是任何可以将键映射到相应值的类的父类。每个键和每个值都是一个对象。在任何一个Dictionary对象中,每个键至多与一个值相关联。

重要参数

​ HashTable采用"拉链法"实现哈希表,它定义了几个重要的参数:table、count、threshold、loadFactor、modCount。

// 为一个Entry[]数组类型,Entry代表了“拉链”的节点,每一个Entry代表了一个键值对,哈希表的"key-value键值对"都是存储在Entry数组中的。
private transient Entry<?,?>[] table;

// HashTable的大小,这个大小并不是HashTable的容器大小,而是它所包含Entry键值对的数量。
private transient int count;

// Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
private int threshold;
// 加载因子
private float loadFactor;
// 用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你
private transient int modCount = 0;

构造方法

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

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

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

​ 默认构造函数,容量为11,加载因子为0.75,所以初始化时,HashTable的初始容量为11。也可以在初始化,只指定初始容量,此时默认的加载因子依旧是0.75。

​ 也可以直接传入一个Map集合,此时的容量会设置为11与传入Map集合大小的两倍之间的最大值。

​ 这三个构造方法调用的都是下面这个构造方法:

    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 = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

Hashtable是线程安全的,因为其内部的公有方法基本上都被synchronized修饰了,所以是线程安全的。

image-20210311113455648

存数据流程

put方法是往Hashtable中存入数据的方式,下面看一下put方法的源码:

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;  //确认该key的索引位置  
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
         //迭代,寻找该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;
    }

1、首先要确保存入的键值对的value不为null,如果为null,直接抛出异常

2、然后就是要确保集合中的key是不重复的,处理过程是:

​ (1)计算key的hash值,确认在table[]中的索引位置

​ (2)迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值。

3、确认要添加的key不是重复的以后,就将这个新的键值对添加到Entry数组中,即执行addEntry方法,顺带传入键值对以及哈希值,还有这个键值对在Entry数组中的索引值。

    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++;
    }

4、添加之前要确认容器中的元素数量是否达到阈值,如果大于等于阈值,则进行扩容,扩容的机制在下面有讲解。扩容之后,会重新为要添加的键值对计算哈希值和索引值。

5、依据对象的哈希值,确定键值对在数组中的位置,然后在这个位置上插入一个新的节点。

hash值确定键值对在Entry数组中的位置,index值确定键值对在这个位置的链表中位于链表的哪个具体位置。

总结一下

​ put方法的整个处理流程是:计算key的hash值,根据hash值获得key在table数组中的索引位置,然后迭代该key处的Entry链表(我们暂且理解为链表),若该链表中存在一个这个的key对象,那么就直接替换其value值即可,否则将该key-value节点插入该index索引位置处。

​ 当程序试图将一个key-value对放入Hashtable中时,程序首先根据该对象的key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个Entry的key的hashCode() 返回值相同,那它们的存储位置相同。如果这两个Entr的 key通过equals比较返回true,新添加 Entry的value将覆盖集合中原有Entry的value,但key不会覆盖。如果这两个 Entry的key通过equals比较返回false,新添加的Entry将与集合中原有Entry形成Entry链,而且新添加的Entry位于 Entry链的头部。

扩容机制

​ 在put方法中,如果需要向table[]中添加Entry元素,会首先进行容量校验,如果容量已经达到了阀值,HashTable就会进行扩容处理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;
        }
    }
}

​ 从源码可以看出HashTable的扩容操作是:新容量=旧容量 * 2 + 1。同时需要将原来HashTable中的元素一一复制到新的HashTable中,这个过程是比较消耗时间的,同时还需要重新计算hashSeed的,毕竟容量已经变了。

​ 其中还需要注意的是,扩容之后,阈值发送了改变。

posted @ 2021-03-11 21:18  有心有梦  阅读(94)  评论(0编辑  收藏  举报