zoukankan      html  css  js  c++  java
  • 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的,毕竟容量已经变了。

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

  • 相关阅读:
    Spring学习(一)初识Spring
    搜索引擎学习(七)解析查询
    搜索引擎学习(六)Query的子类查询
    Oracle学习(十四)分表分区
    Oracle学习(十三)优化专题
    Python学习————流程控制之while循环
    Python学习————深浅copy
    Python学习————while循环作业
    Python学习————运算符
    Python学习————与用户交互
  • 原文地址:https://www.cnblogs.com/yxym2016/p/14520616.html
Copyright © 2011-2022 走看看