Java集合(七)--WeakHashMap简析

WeakHashMap是一个带有弱键的Map,即当某个键不再正常使用的时候,这个键就会被移除,它所对应的键值对也就被移除了。该类支持空键和空值,具有与HashMap相似的性能特征,有与初始容量和负载因子。WeakHashMap是非同步的。

WeakHashMap的定义及说明

定义如下:

public class WeakHashMap<K,V>extends AbstractMap<K,V> implements Map<K,V> {}

WeakHashMap的定义还是比较简单的,就是继承了AbstractMap,实现了Map。即拥有Map基本的属性和方法。

构造方法

构造方法如下:

//最大的容量,如果具有参数的任一构造函数隐式指定较高值,则使用最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;

//默认的初始容量,必须是2的次方
private static final int DEFAULT_INITIAL_CAPACITY = 16;

//默认的加载因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;

//数组类型,实际是一个单链表,数组长度必须是2的次方
Entry<K,V>[] table;

//阈值(容量*loadFaactor)
private int threshold;

//加载因子
private final float loadFactor;

//WeakHashMap中包含的键值的数量
private int size;

//使用给定的初始容量和给定的加载因子构造一个新的空WeakHashMap。
public WeakHashMap(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);
        //计算初始容量(找到大于initialCapacity的最小的2的次方)
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        //新建一个空数组
        table = newTable(capacity);
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
    }

//使用给定的初始容量和默认加载因子(0.75)构造一个新的空WeakHashMap。
public WeakHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//使用默认初始容量(16)和加载因子(0.75)构造一个新的空WeakHashMap
public WeakHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

//使用与指定映射相同的映射构造一个新的WeakHashMap。 使用默认加载因子(0.75)创建WeakHashMap,且初始容量要足以保存指定映射中的映射。
public WeakHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY),
             DEFAULT_LOAD_FACTOR);
        putAll(m);
    }

有没有一种熟悉的感觉?不错,就是HashMap。可以看到它与HashMap相类似,都有容量、加载因子、阈值以及都是单链表结构。怎么看出是一个单链表?看一下Entry就了然了:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        //值
        V value;
        //hash值
        final int hash;
        //下个节点
        Entry<K,V> next;

        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
             //通过WeakReference调用Reference的方法,使key注册到queue中,并且,key为弱引用
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }

}

其中ReferenceQueue是一个引用队列,在检测到可到达性更改后,垃圾回收器将已注册的引用对象添加到队列中,主要是用于监听Reference所指向的对象是否已经被垃圾回收。通过"super(key, queue)"方法及其调用方法,使key成为一个弱引用对象,并将其注册到queue中,以便在key被GC的时候可以添加到queue中去。

源码简析

我们还是从put()方法入手,分析一下它的实现原理:

//已清除的Entries的引用队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

public V put(K key, V value) {
        //判断key是否为空
        Object k = maskNull(key);
        //获取key经过处理后的hashCode
        int h = hash(k);
        //获取当前(清除了被GC了的key所对应的映射) 的表
        Entry<K,V>[] tab = getTable();
        //返回哈希码h在数组中对应的索引
        int i = indexFor(h, tab.length);
        //获取i对应的索引位置的链表的首节点并遍历链表
        for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
            //判断hash值是否相等
            if (h == e.hash && eq(k, e.get())) {
                //获取当前位置的旧值
                V oldValue = e.value;
                //新旧值不相等,则新值覆盖旧值
                if (value != oldValue)
                    e.value = value;
                //返回旧值,结束后面的操作
                return oldValue;
            }
        }
       
        //修改次数加1
        modCount++;
        //将新元素添加到数组中
        Entry<K,V> e = tab[i];
        tab[i] = new Entry<>(k, value, queue, h, e);
        //调整数组大小
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }

//对key的hashCode做处理
final int hash(Object k) {
        //获取k的hashCode
        int h = k.hashCode();
        //hashCode混淆
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

//首次删除过时条目后返回表
private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return table;
    }

//从表中清除过时的条目
private void expungeStaleEntries() {
        //遍历queue中的key(已被GC)
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                //取出queue中的当前位置的元素    
                Entry<K,V> e = (Entry<K,V>) x;
                //获取该key在数组中的所对应的索引
                int i = indexFor(e.hash, table.length);
                //将当前key赋值与prev
                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                //遍历索引i对应的链表
                while (p != null) {
                    //获取p的下一个元素并赋值给next
                    Entry<K,V> next = p.next;
                    //判断p是否等于e(除了第一次轮询以外,其他情况下p = prev.next())
                    if (p == e) {
                        //判断prev是否等于e
                        if (prev == e)
                            //p和prev都等于e,则直接将数组索引为i的链表的首节点替换为next
                            table[i] = next;
                        else
                            //只有p等于e,则改变prev的下一个元素的指向,将其变为p的下一个元素,即删除p
                            prev.next = next;
                        //不得将e.next归零; 旧的条目可能被HashIterator使用
                        //置为空以方便GC回收
                        e.value = null;
                        //数组长度减一
                        size--;
                        break;
                    }
                    //重新赋值,以便再次循环
                    prev = p;
                    p = next;
                }
            }
        }
    }


//返回哈希码h对应的索引
private static int indexFor(int h, int length) {
        return h & (length-1);
    }

//调整数组大小
void resize(int newCapacity) {
        //获取当前table数组
        Entry<K,V>[] oldTable = getTable();
        //获取当前数组长度
        int oldCapacity = oldTable.length;
        //判断数组长度是否大于最大值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //将阈值置为最大值
            threshold = Integer.MAX_VALUE;
            return;
        }
        //使用新的数组长度新建一个数组
        Entry<K,V>[] newTable = newTable(newCapacity);
        //将旧数组的所有元素移入新数组
        transfer(oldTable, newTable);
        //将table指向新数组
        table = newTable;

        //判断数组长度是否大于等于阈值的1/2
        if (size >= threshold / 2) {
            //重新计算阈值
            threshold = (int)(newCapacity * loadFactor);
        } else {
            //从表中清除过时的条目
            expungeStaleEntries();
            //将新元素移入旧的数组
            transfer(newTable, oldTable);
            //将table指向旧数组
            table = oldTable;
        }
    }

//将所有条目从src传输到dest
private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
        for (int j = 0; j < src.length; ++j) {
            //获取src指定位置的元素
            Entry<K,V> e = src[j];
            src[j] = null;
            //遍历指定索引下的链表
            while (e != null) {
                Entry<K,V> next = e.next;
                //获取key
                Object key = e.get();
                if (key == null) {
                    //key为null则将其next和value置为空,且数组长度减一
                    e.next = null;
                    e.value = null;
                    size--;
                } else {
                    //获取e在dest数组中所对应的索引,并赋值
                    int i = indexFor(e.hash, dest.length);
                    e.next = dest[i];
                    dest[i] = e;
                }
                //赋值并重新开始循环
                e = next;
            }
        }
    }

从上面的分析中我们可以看出,WeakHashMap每次put一个元素,都会先删除table中不再正常使用的元素。然后再判断当前已有元素中的key是否有与要添加元素的key相同的,相同则覆盖其值,不同则将新元素添加如数组中。在这个过程中不再正常使用的键的原理为:当WeakHashMap中的键(弱引用)被GC回收时,该键会被添加到queue中。然后,在put过程中会执行expungeStaleEntries()方法,该方法会遍历queue中的key并删除WeakHashMap中与其key对应的键值对。

在expungeStaleEntries()方法中,有一个queue.poll()方法,它是ReferenceQueue类中的方法,具体如下:


//ReferenceQueue的队首
private Reference<? extends T> head = null;

//ReferenceQueue的队尾
private Reference<? extends T> tail = null;

//判断队列中是否为空,如果不为空,则取出链表中head位置的元素
public Reference<? extends T> poll() {
        synchronized (lock) {
            if (head == null)
                return null;
            return reallyPollLocked();
        }
    }

//Reference.queueNext将设置为sQueueNextUnenqueued,以指示何时将引用放入队列并从队列中删除。
private static final Reference sQueueNextUnenqueued = new PhantomReference(null, null);

private Reference<? extends T> reallyPollLocked() {
        
        if (head != null) {
            Reference<? extends T> r = head;
            if (head == tail) {
                tail = null;
                head = null;
            } else {
                head = head.queueNext;
            }
            //更新queueNext以指示引用已入队,但现在已从队列中删除。
            r.queueNext = sQueueNextUnenqueued;
            return r;
        }

        return null;
    }

作用就是取出queue中的key,以方便从数组中删除。

好了,本文到此结束。

感觉自己博客写的好乱!!!后期要整理一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342