数据结构之LinkedHashMap

    Map:Map是一个接口,它定义了一些规则,即get和put操作。Map用于保存具有映射关系的数据,因此Map集合中存的是键值对,并且key不能重复
    HashMap:HashMap是Map接口的一个实现类。HashMap提供所有可选的映射操作,并且允许存null键和null值,它不保证映射的顺序,特别是不保证该顺序永远不发生改变。HashMap的迭代所需的时间和HashMap实例的“容量”(桶的数量)以及大小(里面键值对的数量)成比例。
    影响HashMap性能的两个参数:初始容量和加载因子。容量就是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,加载因子是哈希表在其容量自动增加之前达到多满的一种程度,比如默认的加载因子是0.75,初始容量是100,那么当哈希表中的条目数量达到0.75 * 100 = 75时,则要对哈希表进行rehash操作(即重建内部结构),从而将哈希表的桶数翻倍。
    HashMap是不同步的,所以在多线程访问同一个哈希表的时候,需要在外部进行同步,比如
        Map map = Collections.synchronizedMap(new HashMap(...));
    HashMap的迭代:由所有此类的"Collection视图方法"所返回的迭代器都是快速失败的:在迭代器创建之后,如果要对HashMap结构进行修改,建议通过迭代器本身的remove()和add()
   HashMap的遍历:可以通过获取到HashMap的keySet,然后通过keySet里面的key去取value

HashMap hashMap = new HashMap

    HashMap设计思路
        HashMap假定哈希函数将元素正确分布在各桶之间,可为get和put提供稳定的性能。迭代视图所需的时间与HashMap实例的“容量”及其大小成比例

    HashMap重写的方法:因为HashMap是基于HashCode的,在Object类中有一个HashCode方法,这个方法返回的HashCode对应于当前的地址,也就是说不同的对象,即使它们的内容完全一样所得到的哈希值也会不一样,所以就跟复写equals方法一样,要重新定义hashCode的实现
      重写HashCode的原则:1.不唯一原则:不必对每个不同的对象都产生一个唯一的hashcode,只要设计的hashCode方法能get到put进去的内容就可以了;

                                            2.分散原则:hashCode算法生成的值要分散一些,不要很多的hashcode都集中在一个范围,这样有利于提高HashMap的性能
       对HashMap的分析
            1.HashMap是实现了Map Cloneable Serializable并继承了AbstractMap的类,最重要的是里面有一个实现了Map.Entry的静态内部类HashMapEntry,里面包含了key value next hash四个属性

    static class HashMapEntry implements Entry {
        final K key;
        V value;
        final int hash;
        HashMapEntry next;

        HashMapEntry(K key, V value, int hash, HashMapEntry next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        @Override public final boolean equals(Object o) {
            if (!(o instanceof Entry)) {
                return false;
            }
            Entry e = (Entry) o;
            return Objects.equal(e.getKey(), key)
                    && Objects.equal(e.getValue(), value);
        }

        @Override public final int hashCode() {
            return (key == null ? 0 : key.hashCode()) ^
                    (value == null ? 0 : value.hashCode());
        }

        @Override public final String toString() {
            return key + "=" + value;
        }
    }

                put源码如下

 public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        //生成哈希值
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //通过该方法查找hash值对象的元素索引
        int i = indexFor(hash, table.length);
        for (HashMapEntry 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;//保存oldValue用于返回
                e.value = value;//赋值新的value
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;//结构更改的次数
        addEntry(hash, key, value, i);//添加新元素
        return null;//没有相同的键值返回null
    }
        
    private V putForNullKey(V value) {
        for (HashMapEntry e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

        从上面可以看出HashMap其实内部还是用到的table,如果key为null的话,会调用putForNullKey方法存入null键条目,这也就是为什么HashMap允许存null键的原因。因为哈希算法可能导致不同的键值有相同的hash值并有相同的table索引,假设shadow 和 walker这两个key的hash值一样,那么通过indexFor方法得到的table里面的索引肯定一样,这样再new的时候这个Entry的next就指向原来的这个table[i],再有下一个也如此,因此会在table[i]处形成一个链表(PS:因为HashMapEntry里面只有一个next指针指向下一个,所以每次相同hash值的键值对会存放在第1个,然后它的next指向原来的第一个元素)
数据结构之LinkedHashMap_第1张图片
 

if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }

        从上面可以看出put的时候如果达到threshold那么直接会将HashMap的桶量翻倍,然后获取索引的时候用到的是hash & (tab.lenth -1);HashMap底层数组长度总是2的n次方,在构造函数中存在capacity=1<<4这样就能保证HashMap的底层数组长度总是2的n次方。当length为2的n次方时,hash & (tab.length -1)就相当于对length取模,而且速度比直接取模快得多,而且这样能够保证table数据均匀分布和充分利用空间
         PS:x mod 2^n = x & (2^n - 1) 分析:取模运算,因为2^n二进制是1000...0这种形式,与2^n取模就是取的后面(n-1)位,而与运算遇0则0遇1保持,2^(n-1)全是1,所以 x mod 2^n = x & (2^n - 1)

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

     如果传入的初始容量不是2的幂,内部仍然会自行调整为2的幂

   /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

  假设length为16和15,hash为5,6,7

数据结构之LinkedHashMap_第2张图片数据结构之LinkedHashMap_第3张图片

        length为15的时候比如6和7结果一样,10和11结果一样,出现很多次hash碰撞,这就导致table多个地方没有存放数据,而且多个地方存的是一个链表,这样就导致查询很慢。所以说当length = 2^n时,hash碰撞的概率较小,查询较快

 

    LinkedHashMap: HashMap是无序的,也就是说迭代HashMap所得到的元素的顺序并不是它们最初放进去的顺序。LinkedHashMap内部维护的是一个双向链表,它可以指定迭代顺序,这个迭代顺序可以是插入顺序也可以是访问顺序,很适合做LRU Cache

    LinkedHashMap中,所有put进来的Entry都保存在如下第一个图表示的哈希表中,但由于它又定义了一个以head为头结点的双向链表如图二所示,因此对于每次put进来的Entry,除了将其保存到哈希表中的对应的位置上外,还会将其插入到双向链表的尾部

 

数据结构之LinkedHashMap_第4张图片
 

数据结构之LinkedHashMap_第5张图片

    HashMap和双向链表的配合使用造就了LinkedHashMap,next用于维护HashMap各个桶中的Entry链,before、after用于维护LinkedHashMap中的双向链表,虽然作用的对象都是Entry,但是各自分离,是不同的

 

数据结构之LinkedHashMap_第6张图片


数据结构之LinkedHashMap_第7张图片
    使用LinkedHashMap做LRU Cache的使用需要将accessOrder设置为true,设置为true就是制定迭代的顺序为访问顺序,accessOrder为true的时候每次put或者get一个Entry的时候都会将它移到双向链表的尾部,这样最近访问的自然元素自然会在双向链表的尾部。如果要在Cache满的时候移除最老的元素,则可以重写removeEldestEntry方法,该方法默认返回false,只需要重写返回true即可在双向链表满的情况下往里面插入元素时移除最老的元素

void recordAccess(HashMap m) {
            LinkedHashMap lm = (LinkedHashMap)m;
            if (lm.accessOrder) {//如果要实现LRUCache 则需要将accessOrder设置为true
                lm.modCount++;
                remove();//移除该元素
                addBefore(lm.header);//将该元素插入到双线链表的尾部
            }
        }

public V get(Object key) {
        LinkedHashMapEntry e = (LinkedHashMapEntry)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }
//重写了HashMap的createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry old = table[bucketIndex];
        LinkedHashMapEntry e = new LinkedHashMapEntry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);//每次插入的时候都会将新的Entry放到双链表的尾部,这样就可以按照插入顺序迭代
        size++;
    }

    从如上图所示的get方法还有createEntry方法可以看出,在插入顺序层面,新的Entry插入到双向链表的尾部可以实现按照插入的先后顺序来迭代Entry,而在访问顺序的层面,新put进去的Entry又是最近访问的Entry,所以每次都应该移到双向链表的尾部

    /**
     * Evicts eldest entry if instructed, creates a new entry and links it in
     * as head of linked list. This method should call constructorNewEntry
     * (instead of duplicating code) if the performance of your VM permits.
     *
     * 

It may seem strange that this method is tasked with adding the entry * to the hash table (which is properly the province of our superclass). * The alternative of passing the "next" link in to this method and * returning the newly created element does not work! If we remove an * (eldest) entry that happens to be the first entry in the same bucket * as the newly created entry, the "next" link would become invalid, and * the resulting hash table corrupt. */ @Override void addNewEntry(K key, V value, int hash, int index) { LinkedEntry header = this.header; // Remove eldest entry if instructed to do so. LinkedEntry eldest = header.nxt; if (eldest != header && removeEldestEntry(eldest)) { remove(eldest.key); } // Create new entry, link it on to list, and put it into table LinkedEntry oldTail = header.prv;//获取之前的队尾元素 // 创建一个新的元素,并让元素nxt指向header,prv指向之前的队尾元素 LinkedEntry newTail = new LinkedEntry( key, value, hash, table[index], header, oldTail); //1.让header的prv指向新的元素 //2.让以前的队尾nxt指向新的元素 //3.将新的元素放进table table[index] = oldTail.nxt = header.prv = newTail; }


参考资料:https://www.cnblogs.com/chenssy/p/3521565.html
                 https://baike.baidu.com/item/Hashmap/1167707?fr=aladdin120

你可能感兴趣的:(Android技术点,数据结构)