• HashMap源码分析


    一、HashMap介绍

    集合中的Map集合有两个实现类分别是HashMap和TreeMap,这里先说HashMap,TreeMap在以后的文章讲。

    HashMap,既然前面带了Hash,那么他一定用到了hash算法,HashMap的结构就是一个hash表,注意在HashMap中可以添加一个键为null和值为null的元素,在jdk源码中有向集合中添加null的方法,只不过添加键为null的元素没有意义。对于底层用HashMap实现的HashSet集合也是可以添加值为null的元素。

    前面说了HashMap基于哈希表实现,这个哈希表还配合着链表完美的解决了哈希冲突(具体解决哈希冲突的方法详见:介绍哈希函数及解决冲突的方法 ),这里用到了HashMap中的静态内部类Entry。哈希表的查询速度很快,他是根据key(键值)计算出hash值,然后根据这个hash值放入准备好的哈希表,在之后查询的时候直接计算hash值,在哈希表上直接就可以找到。下面这个图出自我的另一篇文章,简单的介绍了解决哈希冲突的一种方式。


    二、源码分析

    先看HashMap的构造方法,默认的构造方法调用其它带两个参数的构造方法,这两个值分别为哈希表的默认长度,和负载因子,这个负载因子的作用就是保持哈希表的存储的元素永远保持在一个合适的容量,负载因子越大表中添加的元素越多,空间利用率越高,适当的负载因子可以减少哈希冲突的次数,这样可以减少比较的次数。当这个负载因子很小的时候表中添加的元素就会很少,元素之间也更加稀疏,浪费了空间。在性能和空间上我们可能用更大的空间资源要求更高的性能。比如默认16个长度的哈希表,当存储到16*0.75=12的时候,这个哈希表就会扩容。

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


    先看HashMap的put方法就会知道向这个哈希表上添加元素的原理,在向集合中添加元素的时候,会先进行判断键是否为null,如果为空调用putForNullKey()方法,把这个元素放在哈希表的第一个位置。如果不为空计算key的hash值,利用indexFor()方法寻找hash值对应的哈希表中的索引位置,进入循环如果对应的键值已经存在,用新的value代替原来的value值(注意下面所说的哈希表就是Entry数组)

    public V put(K key, V value) {
        //判断键是否为null,是放在哈希表第一个位置
        if (key == null)
            return putForNullKey(value);
        //计算键key的哈希值
        int hash = hash(key);
        //查找对应哈希值在哈希表中的索引
        int i = indexFor(hash, table.length);
        //循环遍历哈希表,若键值存在,用新的value值代替原来的value
        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;
    }

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

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

    HashMap中的计算hash值的方法:

    final int hash(Object k) {
            int h = 0;
            if (useAltHashing) {
                if (k instanceof String) {
                    return sun.misc.Hashing.stringHash32((String) k);
                }
                h = hashSeed;
            }
    
            h ^= k.hashCode();
    
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }

    在添加元素的时候,总是在创建Entry对象,这个Entry是HashMap的一个静态内部类,在初始化哈希表的时候也是利用了Entry这个类,只是创建了一个线性数组。其中有key,value,next三个属性组成了HashMap集合。可以说HashMap就是一个Entry数组,在向这个数组(哈希表)中添加元素的时候发生了哈希冲突,就要用到Entry的next属性,对next属性进行赋值,形成一个链表,这样可以很好的解决哈希冲突,这就是链地址法解决哈希冲突。

    Entry源码实现:

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
    
        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    
        public final K getKey() {
            return key;
        }
    
        public final V getValue() {
            return value;
        }
    
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
    
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
    
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
    
        public final String toString() {
            return getKey() + "=" + getValue();
        }
    
        /**
         * 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) {
        }
    
        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }
    我们通过get方法追踪到调用的getEntry()方法,该方法为获取集合中元素的方法:每个键值都会计算出一个hash值,在向集合中添加元素的时候计算出hash值后根据这个值添加,当取元素的时候,同样计算出给出键值的hash值,直接通过hash值找,好比在一个数组中执行查找操作,效率还是很高的。

    final Entry<K,V> getEntry(Object key) {
            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;
        }

  • 相关阅读:
    中医手诊原理
    半月痕
    0020 教您新手修车的五种实用技巧
    下面说说我开车12年来的一些心得
    创建电子邮件信纸
    交通事故责任划分2011版(图解)
    育儿语录
    汽车中控台那些按钮是什么用的?
    我的书中的部分函数
    纠结的书名
  • 原文地址:https://www.cnblogs.com/duzhentong/p/7816553.html
Copyright © 2020-2023  润新知