zoukankan      html  css  js  c++  java
  • java中HashTable、HashMap、LinkedHashMap

      前面写了list下arrlist和linkedlist的区别也就没有下文了,抽空总结一下map下的一些类。

      纯手码,转载著名出处哦。

    一、概述

      首先说一下三个map的介绍(treemap比较特殊,暂时忽略)

      1、hashtable:数组+单链表结构、线程安全(操作加锁)、无序、

      2、hashmap:数组+单链表结构、线程不安全、无序、

      3、linkedhashmap:继承了hashmap、数组+单链表结构、线程不安全、有序(1、插入顺序 2、lru:最少最近访问顺序 [采用双向链表存储顺序])

      上面就是三个map的大体特性,那么这些特性是怎么实现的呢,我们可以从几个方面切入源码分析三个map不同之处:构造函数、put方法、get方法、

    二、底层结构

      1、HashTable:存储一个值时,先根据key算出所存入table数组的下角标i,如果table[i]为空,直接存入生成的entry,如果不为空则从该位置的entry头遍历,如果key相同,则覆盖value,如果没有相同的key的entry,则将新生成的entry放入该数组下角标的头部;

      /**     
        * hashtable用来存储数据的数组,数组中的元素是Entry,下面说明.
    */ private transient Entry<K,V>[] table; /** * hashtable数组中的元素为Entry,是一个key和value的结构(类中方法省略) */ private static class Entry<K,V> implements Map.Entry<K,V> {
         //当前entry的hash值
    int hash;
        //当前entry的key值
    final K key;
        //当前enry的value值 V value;
        //当前entry的下一个entry(这里可以看出是一个单链表) Entry
    <K,V> next;
    }

      2、hashmap:跟hashtable完全相同的底层结构

        /**
         * 一个空entry数组
         */
        static final Entry<?,?>[] EMPTY_TABLE = {};
        /**
         * 跟hashtable一样,table数组,存放entry对象,唯一区别就是直接先初始化为空数组
         */
        transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    
    
       //entry对象跟entry完全相同 
        static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            int hash;
        }

      3、LinkedHashMap:下面的代码可以看到,linkedhashmap继承了hashmap,在hashmap的基础上加了hear双向链表和entry中的前后标记

    public class LinkedHashMap<K,V>
        extends HashMap<K,V>
        implements Map<K,V>
    {
          /**
         * 类中看出继承了hashmap,所以底层的数据格直接使用了hashmap的table.
         这个hear是一个链表,head为链表的头,后面涉及到
    */ private transient Entry<K,V> header; /** * accessorder属性:用来标记hear双向链表是根绝lru排序,还是插入顺序排序 */ private final boolean accessOrder; }
      /**
         * linkedhashmap中的entry继承了hashmap中的enry
         */
        private static class Entry<K,V> extends HashMap.Entry<K,V> {
            // 多了两个属性:entry前 和 entry后的记录,这是给head用的
            Entry<K,V> before, after;
        }

    三、构造函数(只说明大流程)

      1、hashtable

         * 最常用的构造函数,这里调用了this构造函数,传入两个参数11和0.75,下面说明参数意义
         */
        public Hashtable() {
            this(11, 0.75f);
        }
    
        /**
         * 可以看到,第一个参数为初始化的容量小,这里默认设置为11,第二个参数是扩容因子,默认当容器中有百分之75使用的时候进行扩容*/
        public Hashtable(int initialCapacity, float loadFactor) {
          //check容量设置大小
    if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
          //check加载因子大小
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor;
         //根据初始容量大小,初始化entry数组(还记得上面hashtable是定义属性的时候就初始化了么) table
    = new Entry[initialCapacity];
          //计算扩容的临界值:总容量*加载因子 threshold
    = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
          //计算hashseed参数,用来计算元素key的hash值 initHashSeedAsNeeded(initialCapacity); }

      可以看到:

        1、hashtable默认初始化大小为11,默认扩容加载因子为0.75    

        2、hashtable的数组容器在初始化时完成  

      2、hashmap

        /**
         * 这里进行了代码优化,使用常量,传入两个参数为:16和0.75
         */
        public HashMap() {
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
        }
    
    
        /**
         * 初始容量跟hashtable不一样,这里是16,加载因子都为0.75
         * capacity and load factor.*/
        public HashMap(int initialCapacity, float loadFactor) {
          //check初始容量
    if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
          //设置加载因子 和 初始容量值
    this.loadFactor = loadFactor; threshold = initialCapacity;
          //这个方法为空,可以看到hashmap只是将初始值设置,并没有跟hashtable一样进行计算,到目前位置table数组还是一个空数组
          //(其实hashmap是在第一次put进元素的时候,才对hashmap的容量等信息进行初始化的,下面会看到) init(); }

      这里可以看到hashmap构造函数做的内容:

        1、hashmap容器,默认容量16,扩容加载因子0.75

        2、构造函数只进行了参数check和参数设置,并没有将table设置为真正的大小,也没有计算扩容的容量临界值

      3、linkedhashmap

        /**
         * 完全复用的hashmap的构造函数。accessorder属性默认设置为false:就是hear链表按照插入顺序排序
         */
        public LinkedHashMap() {
            super();
            accessOrder = false;
        }

      linkedhashmap没什么好说的,完全跟hashmap一样,多了一个head链表的排序设置。

    四、put方法

      1、hashtable

        /**
       * 1、可以看到这是一个同步的方法,所以hashtable的同步就是根据关键字实现的。
    * 2、存储一个值时,先根据key算出所存入table数组的下角标i,如果table[i]为空,直接存入生成的entry,
       * 如果不为空则从该位置的entry头遍历,如果key相同,则覆盖value,如果没有相同的key的entry,则将新生成的entry放入该数组下角标的头部;
    */ public synchronized V put(K key, V value) { // 检查value不为空,这里可以看出hashtable中的值不能为空 if (value == null) { throw new NullPointerException(); } Entry tab[] = table;
          //计算出key的hash值
    int hash = hash(key);
          //根据hash值,计算出key在数组中的下角标(可能两个key计算出的下角标相同,所以数组中提供了entry的方式,一个位置可以放置多个元素)
    int index = (hash & 0x7FFFFFFF) % tab.length;

          //下面的循环:如果当前数组index中有值,一个一个遍历
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            //如果发现其中的hash值相同,并且key相同,则覆盖该元素的value值
    if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } }
          //如果走到这一步,说明整个hashtable中都没有该元素,需要新增,modcount就是记录hashtable中元素个数的 modCount
    ++; if (count >= threshold) {       //如果元素个数大于扩容的临界值,进行扩容,并且需要将里面所有的元素,从新hash计算,从新放置位置 rehash(); tab = table; hash = hash(key); index = (hash & 0x7FFFFFFF) % tab.length; } // 创建新的entry,并且放置到table中对应index的第一位 Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; return null; }

      2、hashmap

        /**
         * 1、hashmap允许其中一个key为空,单独做了key为空的放置处理,也允许value为空,没有对value进行空check
         * 2、hashmap容器大小初始化是在put第一个元素的时候进行的
         * 3、hashmap没有加同步,这也是跟hashtable的一个重要区别*/
        public V put(K key, V value) {
          //如果table是默认的空,也就是没有put过则进行容器大小初始化(容器初始化大小必须为2的n次),并且计算扩容临界值(可以自己进去看源码)
    if (table == EMPTY_TABLE) { inflateTable(threshold); }
          //针对key为空的值,进行针对处理,放在数组的第一位
    if (key == null) return putForNullKey(value);
          //根据key计算出hash值,这里的计算方法跟hashtable有区别,可以自己研究一下
    int hash = hash(key);
          //根据hash值计算出在table中的index位置
    int i = indexFor(hash, table.length);
          //同hashtable,遍历index位置值,看是否存在该key,存在则做替换操作
    for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
          //这里跟hashtable不一样,新增entry封装了一个新增的方法,可以到下面看里面的操作 modCount
    ++; addEntry(hash, key, value, i); return null; } /** * 如果新增的key在原来元素中不存在,则进行新增操作 */ void addEntry(int hash, K key, V value, int bucketIndex) {
          //进行扩容操作check
    if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); }       //添加entry,过程跟hashtable基本一样 createEntry(hash, key, value, bucketIndex); }

      可以看到hashtable跟hashmap还是有一些区别的:

        1、hashtable中key和value都不能为空,hashmap对空key进行了专门处理,可以允许一个key为空,而对于value为空则没有check

        2、hashtable的put方法加了同步,而hashmap没有添加

        3、hashtable的容量根据设置大小设置,而hashmap的初始容量回根据设置的大小,向上找到大于设置大小的最小2的n次方的值

        4、hashmap在put第一个元素的时候,设置table的实际大小

      3、linkedhashmap

        /**
         * linkedhasmap在调用put的时候,直接调用的hashmap的put,重写了put中调用的addEntry方法如下
         */
        void addEntry(int hash, K key, V value, int bucketIndex) {

          //调用了hashmap中的addentry,并且在下面重写了addentry中调用的createentry方法(下面的方法)
        
    super.addEntry(hash, key, value, bucketIndex); // 如果要使用lru算法,则要重写下面的removeEldestEntry方法,当前默认是false,不执行,如果重写返回true则删除最老的元素 Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } } /** * 重写的hashmap中的createentry方法 */ void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old);
         //设置当前的值到table中index的第一位 table[bucketIndex]
    = e;
          //这里多了一个操作,将当前的entry添加到hear双向链表的尾部(可以看出按顺序插入添加的时候回维护一个hear链表来标示) e.addBefore(header); size
    ++; }

      这里最大的一个区别就是每次新增一个entry的时候,都会插入header双向链表的最后一位。

    五、get方法

      1、hashtable

        

        /**
         * hashtable根据key获取value的值
         **/
        public synchronized V get(Object key) {
            Entry tab[] = table;
          //计算出key对应的hash值
    int hash = hash(key);
          //计算出hash值对应在数组中index的位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
          //遍历当前位置,找出对应的entry
    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; }

      很简单,

        1、根据put时候的规则,再将值找出来

        2、get方法也是同步的,线程安全

      2、hashmap

        /**
         * 根据key值获取hashmap中对应value的值*/
        public V get(Object key) {
          //key为空的时候,特殊处理
    if (key == null) return getForNullKey();
          //不为空的情况调用下面的getentry方法 Entry
    <K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) {
          //空map直接放弃寻找
    if (size == 0) { return null; }       //这里又做了一次空key的判断,然后计算hash值,计算index值,遍历找到返回 int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }

      也是很简单的方法,跟hashtable的区别是:

        1、对key为null的获取做了特定的处理

        2、方法不同步,线程不安全

      3、linkedhashmap

        /**
         * 跟hashmap类似,getentry也是调用了hashmap中的方法,区别是再后面对recordaccess进行操作,如果按照lru,则将当前读取的放入header链表中的最后一位*/
        public V get(Object key) {
            Entry<K,V> e = (Entry<K,V>)getEntry(key);
            if (e == null)
                return null;
            e.recordAccess(this);
            return e.value;
        }

      跟hashmap完全类似,唯一是再linkedhashmap设置了lru顺序的时候,将当前读取的元素位置进行调整

    六、关于linkedhashmap的遍历

        private abstract class LinkedHashIterator<T> implements Iterator<T> {
            Entry<K,V> nextEntry    = header.after;
            Entry<K,V> lastReturned = null;
    
            /**
             * The modCount value that the iterator believes that the backing
             * List should have.  If this expectation is violated, the iterator
             * has detected concurrent modification.
             */
            int expectedModCount = modCount;
    
            public boolean hasNext() {
                return nextEntry != header;
            }
    
            public void remove() {
                if (lastReturned == null)
                    throw new IllegalStateException();
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
    
                LinkedHashMap.this.remove(lastReturned.key);
                lastReturned = null;
                expectedModCount = modCount;
            }
    
            Entry<K,V> nextEntry() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                if (nextEntry == header)
                    throw new NoSuchElementException();
    
                Entry<K,V> e = lastReturned = nextEntry;
                nextEntry = e.after;
                return e;
            }
        }

    看上面的代码可以发现,linkedhashmap其实是遍历的header双向链表进行遍历的,所以跟hashmap和hashtable的无序性不同,遍历顺序是基于其中维护的header双向链表来维护的。

      

        

        

  • 相关阅读:
    MySQL中TIMESTAMP和DATETIME区别
    图片标签的alt与title区别
    DEDE自带的采集功能,标题太短的解决方法
    Modernizr——为HTML5和CSS3而生!
    InnoDB,MyISAM,Memory区别
    Innodb,MyIsam,聚集索引和非聚集索引
    聚集索引与非聚集索引的总结
    程序kill -9与kill -15的区别,以及回调函数的作用
    linux 信号 SIGINT SIGTERM SIGKILL区别
    oracle mysql sqlserver 查看当前所有数据库及数据库基本操作命令
  • 原文地址:https://www.cnblogs.com/guoliangxie/p/7506659.html
Copyright © 2011-2022 走看看