zoukankan      html  css  js  c++  java
  • 【Java】浅谈HashMap

    HashMap是常用的集合类,以Key-Value形式存储值。下面一起从代码层面理解它的实现。

    构造方法

    它有好几个构造方法,但几乎都是调此构造方法:

        public HashMap(int initialCapacity, float loadFactor) { // 初始容量,过载因子
            if (initialCapacity < 0) // 初始容量<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);
    
            // Find a power of 2 >= initialCapacity
            int capacity = 1;
            while (capacity < initialCapacity) // 按初始容量找到最近的2的n次方值,为真实的容量。为什么?个人认为因计算下标用&元素效率较高
                capacity <<= 1;
    
            this.loadFactor = loadFactor;
            threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 计算扩容阀值,容量 * 过载因子
            table = new Entry[capacity]; // 实例化容量的数组
            useAltHashing = sun.misc.VM.isBooted() &&
                    (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
            init(); // HashMap构造完毕,还没有插入任何元素的回调方法
        }
    

    放入元素,put(K key, V value)

    实际的逻辑在putVal方法:

        public V put(K key, V value) {
            if (key == null)
                return putForNullKey(value); // 存储在table[0]
            int hash = hash(key); // 计算hash
            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))) { // 首先判断hash值是否相等(不同hash有可能映射到同一下标),再判断引用是否相等或equal方法相等
                    V oldValue = e.value; // 暂存旧值
                    e.value = value; // 赋予新值
                    e.recordAccess(this); // 调用覆盖值回调方法
                    return oldValue; // 返回旧值
                }
            }
    
            modCount++; // 递增变更次数
            addEntry(hash, key, value, i); // 构造Entry,添加在i下标的链表中
            return null;
        }
    

    通过hash和数组长度计算数组下标,indexFor(int h, int length)

        static int indexFor(int h, int length) {
            return h & (length-1); // hash和数组长度-1做与运算,得到下标
        }
    

    Value被覆盖回调方法,当put(k,v)覆盖原值时调用,recordAccess()

            /**
             * This method is invoked whenever the value in an entry is
             * overwritten by an invocation of put(k,v) for a key k that's already
             * in the HashMap.
             */
            void recordAccess(HashMap<K,V> m) {
            }
    

    结构变更次数,modCount

    此字段记录HashMap结构变更次数,如添加新元素、rehash、删除元素。此字段用于迭代器的快速失败机制。

        /**
         * The number of times this HashMap has been structurally modified
         * Structural modifications are those that change the number of mappings in
         * the HashMap or otherwise modify its internal structure (e.g.,
         * rehash).  This field is used to make iterators on Collection-views of
         * the HashMap fail-fast.  (See ConcurrentModificationException).
         */
        transient int modCount;
    

    添加元素,addEntry()

    此方法包含数组是否扩容的判断,如需扩容,会调用扩容方法:

        /**
         * Adds a new entry with the specified key, value and hash code to
         * the specified bucket.  It is the responsibility of this
         * method to resize the table if appropriate.
         *
         * Subclass overrides this to alter the behavior of put method.
         */
        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);
        }
    

    实际的创建元素,createEntry()

        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++; // 递增容量
        }
    

    数组扩容,resize()

        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]; // 实例化新容量的数组
            boolean oldAltHashing = useAltHashing;
            useAltHashing |= sun.misc.VM.isBooted() &&
                    (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
            boolean rehash = oldAltHashing ^ useAltHashing; // 是否重新hash
            transfer(newTable, rehash); // 转移所有元素到新数组
            table = newTable; // 正式使用新数组
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); // 重新计算阀值
        }
    

    转移所有元素到新数组

    逐个遍历,映射到新数组的链表中:

        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); // 重新hash
                    }
                    int i = indexFor(e.hash, newCapacity); // 重新计算下标
                    e.next = newTable[i]; // 当前节点的下一节点指向链表首元素(在链表前插入)
                    newTable[i] = e; // 链表首元素指向当前节点
                    e = next;
                }
            }
        }
    

    删除元素,remove()

    删除元素的入口如下,其实质调用removeEntryForKey方法:

        public V remove(Object key) {
            Entry<K,V> e = removeEntryForKey(key);
            return (e == null ? null : e.value);
        }
    

    真实的删除元素,removeEntryForKey()

        final Entry<K,V> removeEntryForKey(Object key) {
            int hash = (key == null) ? 0 : hash(key); // 计算hash值
            int i = indexFor(hash, table.length); // 计算下标
            Entry<K,V> prev = table[i]; // 该下标的链表首元素
            Entry<K,V> e = prev;
    
            while (e != null) {
                Entry<K,V> next = e.next;
                Object k;
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    modCount++; // 删除元素,也属于结构变化
                    size--; // 容量减一
                    if (prev == e) // 如果当前元素是链表首元素
                        table[i] = next; // 链表首元素指向当前节点的下一节点
                    else
                        prev.next = next; // 当前节点的前一节点的next指向当前节点的下一节点(删除当前节点,即跳过当前节点)
                    e.recordRemoval(this); // 删除后的回调方法
                    return e;
                }
                prev = e;
                e = next;
            }
    
            return e;
        }
    

    获取元素,get()

        public V get(Object key) {
            if (key == null)
                return getForNullKey(); // 在table[0]的下标寻找
            Entry<K,V> entry = getEntry(key); // 计算下标、遍历链表对比(与之前的put、remove方法找元素类似)
    
            return null == entry ? null : entry.getValue();
        }
    

    小疑问

    计算最接近的2的n次方,roundUpToPowerOf2(int number)

    这个方法是计算number最接近的2的N次方数。
    其中Integer.highestOneBit()是取最高位1对应的数,如果是正数,返回的是最接近的比它小的2的N次方;如果是负数,返回的是-2147483648,即Integer的最小值。
    那为什么要先减1,再求highestOneBit()?
    举几个数的二进制就知道了:
    00001111 = 15 -> 00011110 = 30 -> highestOneBit(30) = 16
    00010000 = 16 -> 00100000 = 32 -> highestOneBit(32) = 32
    所以,为了获取number最接近的2的N次方数,就先减一。

    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    

    计算映射到指定范围的下标,indexFor(int h, int length)

    将h映射到length的范围里,效果就像求模。

    return h & (length-1);
    

    将h和length - 1和操作就可以了。
    比如length为16,那么:
    16 = 00010000
    15 = 00001111

    为什么hash数组的长度要弄成2的N次方?

    要将散列值映射到一定范围内,一般来说有2种方法,一是求模,二是与2的N次方值作&运算。而现代CPU对除法、求模运算的效率不算高,所以用第二种方法会效率比较高,所以数组被设计为2的N次方。

    拓展:LinkedHashMap

    见此类的声明可知其继承自HashMap,而实际的存储逻辑也是由HashMap提供:

    public class LinkedHashMap<K,V>
        extends HashMap<K,V>
        implements Map<K,V>
    

    链表的维护顺序

    而LinkedHashMap中维护了遍历的顺序,是通过另外的双向链表维护的,比如,链表首元素:

        /**
         * The head of the doubly linked list.
         */
        private transient Entry<K,V> header;
    

    元素之间的指向:

            // These fields comprise the doubly linked list used for iteration.
            Entry<K,V> before, after;
    

    用此字段表示链表维护的顺序,true表示访问顺序,false表示插入顺序:

        private final boolean accessOrder;
    

    放入元素

    覆盖了HashMap的addEntry和createEntry方法:

        /**
         * This override alters behavior of superclass put method. It causes newly
         * allocated entry to get inserted at the end of the linked list and
         * removes the eldest entry if appropriate.
         */
        void addEntry(int hash, K key, V value, int bucketIndex) {
            super.addEntry(hash, key, value, bucketIndex); // 沿用HashMap的逻辑
    
            // Remove eldest entry if instructed
            Entry<K,V> eldest = header.after;
            if (removeEldestEntry(eldest)) { // 是否删除最老元素(LRU原则)
                removeEntryForKey(eldest.key); // 删除最老元素
            }
        }
    
        /**
         * This override differs from addEntry in that it doesn't resize the
         * table or remove the eldest entry.
         */
        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[bucketIndex] = e;
            e.addBefore(header); // 插入到Header节点前
            size++;
        }
    
            /**
             * Inserts this entry before the specified existing entry in the list.
             */
            private void addBefore(Entry<K,V> existingEntry) {
                after  = existingEntry; // 指定节点的后节点
                before = existingEntry.before; // 指定节点的前节点
                before.after = this; // 将当前节点赋予前节点的后节点赋值
                after.before = this; // 将当前节点赋予后节点的前节点赋值
            }
    

    获取元素

        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;
        }
    
            void recordAccess(HashMap<K,V> m) {
                LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
                if (lm.accessOrder) { // 如果按访问顺序记录
                    lm.modCount++;
                    remove(); // 删除当前节点
                    addBefore(lm.header); // 将当前节点加入到列表头
                }
            }
    
            /**
             * Removes this entry from the linked list.
             */
            private void remove() {
                before.after = after; // 将“当前节点的后节点”赋予“当前节点的前节点的后节点”
                after.before = before; // 将“当前节点的前节点”赋予“当前节点的后节点的前节点”
            }
    
  • 相关阅读:
    ligerui_ligerTree_007_ligerTree动态加载节点
    ligerui_ligerTree_006_ligerui事件支持
    ligerui_ligerTree_005_动态增加“树”节点
    ligerui_ligerTree_004_对"ligerTree"节点操作
    ligerui_ligerTree_003_配置url参数,加载“树”
    ligerui_ligerTree_002_利用JavaScript代码配置ligerTree节点
    ligerui_ligerTree_001_第一个“树”效果
    ligerui_实际项目_003:form中添加数据,表格(grid)里面显示,最后将表格(grid)里的数据提交到servlet
    ligerui+json_002_Grid用法、属性总结
    ligerui+json_001_实现表格(grid)的后台数据显示、分页
  • 原文地址:https://www.cnblogs.com/nick-huang/p/7405015.html
Copyright © 2011-2022 走看看