zoukankan      html  css  js  c++  java
  • HashMap 1.7

    1、关键属性与内部类

    1.1属性

      使用拉链法解决hash冲突的hash表,hash表里存了多少个元素、数组有多长以及当什么情况下需要扩容是重要的参数。

    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    transient int size;
    int threshold;
    final float loadFactor;
    • table,用来实现拉链法的数组
    • size,该数组里一共有多少个节点包括拉链法链表里的节点
    • threshold,临界值,当size大于threhold的时候HashMap会扩容。threshold=Capacity*loadFactory
    • loadFactory,负载因子,用来衡量HashMap的满的程度
    • Capacity,数组的长度

    1.2内部类

     static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            int hash;

      存储的值、指向下一个node的指针、hash值。HashMap拉链法使用的是单链表。

    2、构造器

    2.1 无参构造器

      一般的集合类都会提供一个无参的构造器,无参构造器使用默认的参数传入有参构造器。

        public HashMap() {
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
        }

    2.2 有参构造器

    • initialCapacity。用于初始化threshold

    • loadFactory。初始化loadFactory

      有参构造器在两个地方有点违反我的理解:

    • 我知道HashMap内部维护一个数组,但是HashMap的构造器并没有新建该数组
    • 数组的长度用Capacity变量存储,该构造器有一个initialCapacity的参数,但这个参数没有赋值给Capacity而是threshold。init方法里也没有执行新建数组的操作。结合之前阅读源码的经验:数组的初始化一定发生在第一次put的时候。
        public HashMap(int initialCapacity, float loadFactor) {
            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;
            init();
        }

    4、put  

      果然table的初始化在put里,这可以可以理解成HashMap的懒加载机制,只有用户在Put一个值的时候说明HashMap才真正被使用,也只有在这个时候才需要初始化数组。

      整个put方法可以分为三个部分

    • 检查是否是第一次使用HashMap,如果是就初始化table
    • 如果传入的KV中的K是Null放到一个固定的地方
    • 找到K的Index并对可能出现的哈希冲突使用拉链发解决
    • 如果到了扩容条件就扩容
     public V put(K key, V value) {
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);
            }
            if (key == null)
                return putForNullKey(value);
            int hash = hash(key);
            int i = indexFor(hash, table.length);
            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;
                }
            }
    
            modCount++;
            addEntry(hash, key, value, i);
            return null;
        }

    4.1 第一次扩容

      在Put方法的开始检查table是否为EMPTY_TABLE。EMPTY_TABLE是static修饰的数组,在HashMap类被加载的时候一同加载。当新建一个HashMap对象且没有使用的时候,table指向EMPTY_TABLE。所以检查table是否指向EMPTY_TABLE数组就是检查HashMap是否已经初始化过,如果没有就需要初始化。

        /**
         * An empty table instance to share when the table is not inflated.
         */
        static final Entry<?,?>[] EMPTY_TABLE = {};
    
        /**
         * The table, resized as necessary. Length MUST Always be a power of two.
         */
        transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

      新建数组传入的参数是threshold,但最终table的大小并不是threshold。在inflaatTable中:首先计算出比threshold大的最小的2的幂次作为Capacity,其次根据Capacity和loadFactory计算出threshold替换原有的threshold。

    这是一个很别扭的过程:threshold--->Capacity-->threshold。也就是说传入的threshold并不是最终的threshold。 

        private void inflateTable(int toSize) {
            // Find a power of 2 >= toSize
            int capacity = roundUpToPowerOf2(toSize);
    
            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            table = new Entry[capacity];
            initHashSeedAsNeeded(capacity);
        }

    4.2 如果key为Null

      HashMap支持放入key为null的节点,key为Null的节点会同一放到table[0]的位置,即在table[0]的位置形成一条链。

    4.3 解决可能哈希冲突

      在key不等于null的情况下,首先计算出key的哈希值,然后根据key的哈希值计算出key在table里的位置i。接下来就是解决在位置i处可能存在的哈希冲突了,遍历table[i]处的链表,如果在该链表上找到一个节点的key和待插入的节点的key相同,用新的value替换掉旧的value并返回旧的value。如果没有找到就执行addEntry方法。

    4.4 插!

      当走到addEntry方法的时候有两种可能:1、table[i]是一个空的链表,即for循环一次没有执行 2、table[i]的链表里没有找到和key重复的节点 ,总而言之需要在table[i]处插入一个节点。在插入节点之前首先要检查是否需要resize一次,然后才执行createEntry插入节点。

     void addEntry(int hash, K key, V value, int bucketIndex) {
        //检查是否需要扩容
    if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); }     //插入节点 createEntry(hash, key, value, bucketIndex); }

      典型的头插法插入链表,插入链表之后size++

        void createEntry(int hash, K key, V value, int bucketIndex) {
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
        }

    5、resize

     void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    
            createEntry(hash, key, value, bucketIndex);
        }

      当两个条件同时达到时才可以扩容:

    • size>threshold
    • table[i]处有节点

      总结起来就是容量不太够的情况下又发生了哈希冲突,所以此时需要扩容,扩容的大小是原来旧数组大小的两倍。

     void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
        }

       扩容是新建数组再把旧数组拷贝过去,关键是这个拷贝的过程需要重新计算哈希transfer,其实就是执行了一次插入过程,也是头插法。

     void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }

    4、get

      和put方法相比get方法平平无奇,当key不为Null的时候计算出key的索引,然后遍历该索引上的链表直至找到需要的值

        public V get(Object key) {
            if (key == null)
                return getForNullKey();
            Entry<K,V> entry = getEntry(key);
    
            return null == entry ? null : entry.getValue();
        }
        final Entry<K,V> getEntry(Object key) {
            if (size == 0) {
                return null;
            }
    
            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;
        }
  • 相关阅读:
    5、Hystrix服务熔断(服务端)与服务降级(客户端)
    迷宫回溯问题
    4、负载均衡:Ribbon、Feign
    PHP-删除排序数组中的重复项
    MYSQL-连续出现的数字
    PHP
    MYSQL分数排名
    MYSQL查询第二高的薪水
    PHP算法之有效的括号
    PHP算法之电话号码的字母组合
  • 原文地址:https://www.cnblogs.com/AshOfTime/p/10611591.html
Copyright © 2011-2022 走看看