HashMap和LinkedHashMap 3

HashMap
    基于哈希表(散列表)的Map接口的实现,允许使用null键和null值,HashMap是非线程安全的,数据元素存取迭代是无序,顺便提一下HashTable,HashTable是线程安全的,除了线程安全和null键值的区别,HashMap和HashTable大致相同。

    上图是哈希表的结构图,这种表结构查找效率高,如果我们把0-15的顺序线表的每个地址看成一个"桶",而下标是"桶"的编号,我们通过计算key的hash值“与”上线性表的长度得到得到0-15的值,然后将value放入对应的"桶"内,这样我们查找的时候不用遍历所有的元素,直接去对应的桶里面查找就可以了。

    下面我们一起看HashMap的源码,我是用Android studio查看的api25的源码,不同api版本源码会有不同,首先来看看成员变量。

    //最少容量
    static final int DEFAULT_INITIAL_CAPACITY = 4;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //扩容因子(当容量到75%的时候就要开始扩容了)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空表
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
    //键值对的数组
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
    //非空元素的长度
    transient int size;
    //容量
    int threshold;
    //扩容因子相关
    final float loadFactor = DEFAULT_LOAD_FACTOR
    //操作计数器
    transient int modCount;
    我们再来看看put方法:

public V put(K key, V value) {
    //存储key为null的值,如果之前有值就覆盖
        if (key == null)
            return putForNullKey(value);
    //获取key的hash值
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
    //通过hash&(length-1)得到元素长度内的值,也就是"桶"的下标,(&运算不熟悉的自行百度)
        int i = indexFor(hash, table.length);
    //通过桶的下标,遍历"桶"里面的元素(需要看一下HashMapEntry的实现),如果hash值相同,key相等,那就覆盖value,返回旧的value
        for (HashMapEntry<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++;
    //如果没有相同的key,就添加新元素
        addEntry(hash, key, value, i);
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
    //如果元素个数大于等于扩容因子容量(目前容量的75%),那就扩容原来的一倍
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
    //添加数据元素(内容简单,代码就不贴了)
        createEntry(hash, key, value, bucketIndex);
    }
    void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
    //如果达到最大了就不扩容了
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
    //通过遍历,把旧数组中的元素添加到新数组中
        transfer(newTable);
        table = newTable;
    //计算扩容因子容量,loadFactor=0.75f
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
  void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
    //双层循环遍历把元素添加到新数组里面,因为indexFor()方法中的leng变了,
    //"桶"的下标就有可能变,所以需要遍历全部元素,通过key的hash值重新计算桶的下标
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    上面是put流程的部分源码,还需要自己对照源码看一看。如果能把put看明白,get就很简单了。下面我们看一下remove方法源码:

public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.getValue());
    } 
    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
    //通过key的hash值获取"桶"的下标
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        HashMapEntry<K,V> prev = table[i];
        HashMapEntry<K,V> e = prev;
    //遍历这个"桶"内的元素
        while (e != null) {
            HashMapEntry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
        //只有第一个元素就是要删除元素的时候prev才会等于e
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
 
        return e;
    }
    一些逻辑还需要自己认真屡一下。HashMap核心内容也就这些。下面我们一起看看LinkedHashMap。
LinkedHashMap
    LinkedHashMap是HashMap的子类,具有HashMap所有特性,只是额外维护一个双向循环链表来保持迭代顺序,当然,是牺牲了一定的性能。LinkedHashMap支持LRU算法,LruCache就是基于LinkedHashMap来实现的。

下面一起看看源码,看一下仅有的两个成员变量。

    //头结点
    private transient LinkedHashMapEntry<K,V> header;
    //如果是true通过访问排序,如果是false通过插入排序,默认为false
    private final boolean accessOrder;
    如果你去看一下Lrucache的构造方法,你会发现它传入的就是true。LinkedHashMap里面并没有put的方法,只是重写了addEntry()和createEntry()方法,HashMap的put方法最后会调用addEntry()和createEntry()方法。

 void addEntry(int hash, K key, V value, int bucketIndex) {
    //双向循环链表头节点(header后的节点)如果是按插入排序就是最先插入的元素,
    //如果是按访问排序就是访问最少的元素,把访问最少或最老的元素赋值给eldest,
        LinkedHashMapEntry<K,V> eldest = header.after;
        if (eldest != header) {
            boolean removeEldest;
            size++;
            try {
        //removeEldestEntry()方法交给子类实现,通过判断返回值来删除eldest元素
        //默认返回false,所以不删除
                removeEldest = removeEldestEntry(eldest);
            } finally {
                size--;
            }
        //删除最少或最老的元素
            if (removeEldest) {
                removeEntryForKey(eldest.key);
            }
        }
 
        super.addEntry(hash, key, value, bucketIndex);
    }
 
    void createEntry(int hash, K key, V value, int bucketIndex) {
    //取出数组中存的节点
        HashMapEntry<K,V> old = table[bucketIndex];
    //创建新节点
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
    //将新节点放在数组中
        table[bucketIndex] = e;
    //将新元素节点添加到双向循环链表,为什么传header呢,因为要把元素插入在header前面
    //header前面是尾节点,header后面是头节点,新添加的元素需要添加到尾节点
        e.addBefore(header);
        size++;
    }
    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
        //将新元素添加到header前面的尾节点,也就是header和之前的尾节点之间插入新元素节点
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
    上面就是LinkedHashMap添加新元素的核心代码。配合结构图能更好的理解。每次新添加的元素都添加到header的前面,也就是尾节点,而header的后面的节点就是头结点,所以在Lru算法中,无论是访问排序还是插入排序,需要删除元素时,都是删除头结点。

  public V get(Object key) {
    //调用父类的获取元素方法
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<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();
        //把自己添加到header前面的尾节点
                addBefore(lm.header);
            }
        }
    上面试get()的核心代码,如果是插入排序,只需要遍历查找就可以了,如果是访问排序,就需要把访问的元素放在尾节点。

   public Map.Entry<K, V> eldest() {
        Entry<K, V> eldest = header.after;
        return eldest != header ? eldest : null;
    }
    eldest()方法是返回头节点。以上就是LinkedHashMap比较核心的源码了。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值