当前位置: 首页 > 科技观察

你所不知道的Map家族那些冷门容器一览

时间:2023-03-17 11:33:11 科技观察

本文主要讲解Map家族中三个比较冷门的容器,分别是WeakHashMap、EnumMap和IdentityHashMap。想必,你在日常工作中很少用到它们,如果你根本不知道它们的特点和适用场景,本文就带你一探究竟。WeakHashMap介绍WeakHashMap全称为弱三列映射,实现了Map接口,具有以下特点:WeakHashMap中的entry是弱引用,当key除了自身对key的引用外没有其他引用时,mapGC之后会删除这个值,自动丢弃。非线程安全可以存储null演示案例publicstaticvoidmain(String[]args){Stringa=newString("a");字符串b=新字符串("b");地图weakmap=newWeakHashMap();weakmap.put(a,"aaa");weakmap.put(b,"bbb");一个=空;b=空;//执行gcSystem.gc();迭代器j=weakmap.entrySet().iterator();while(j.hasNext()){Map.Entryen=(Map.Entry)j.next();System.out.println("weakmap:"+en.getKey()+":"+en.getValue());}}运行结果:已经被gc回收了。原理实现从这里我们可以看出,Entry内部继承了WeakReference,即弱引用,所以具有弱引用的特性。弱引用的特点是当垃圾回收线程扫描其管辖的内存区域时,一旦发现只有弱引用的对象,无论当前内存空间是否足够,都会回收其内存。然而,由于垃圾收集器是一个非常低优先级的线程,只有弱引用的对象可能无法快速找到。WeakReference中有一个成员变量ReferenceQueue。它的作用是GC会清理对象后,将引用对象放入ReferenceQueue中,然后遍历队列删除Entry。WeakHashMap内部有一个expungeStaleEntries函数,它通过移除其中未使用的条目来实现自动释放内存的目的。所以我们每次访问WeakHashMap的时候,都会调用这个expungeStaleEntries函数来进行清理。使用场景在并发泛滥的今天,缓存应该大家都用过。缓存是存放在内存中的,而内存几乎是计算机中最宝贵、最稀缺的资源,所以需要谨慎使用,否则会非常吃力。很容易出现OOM。缓存的主要作用是更快的处理业务,减轻服务器的压力,所以需要保证缓存的命中率。这里假设整个缓存是key-value结构(以key-value对缓存为例),使用HashMap作为强引用。在不主动删除key的情况下,对象不会被JVM回收,这样HashMap中的对象就会堆积,直到出现OOM错误;那么如何在不占用那么多内存的情况下实现高缓存命中率,这里可以使用WeakHashMap。当然不会有HashMap的100%命中率(假设内存足够),但是这种缓存方案在保证程序正常的前提下更好的实现。EnumMap为枚举类型键引入了专门的Map实现。枚举映射中的所有键必须来自创建映射时显式或隐式指定的单个枚举类型。与HashMap中的枚举作为key相比,EnumMap内部使用了一个非常紧凑的数组来存储value,直接根据enum类型的key定位到内部数组的索引,不需要计算hashCode(),这样不仅效率最高,而且没有额外的空间浪费。它不是线程安全的,可以存储空值。演示案例publicstaticvoidmain(String[]args){//构造函数输入类型Mapmap=newEnumMap<>(DayOfWeek.class);map.put(DayOfWeek.MONDAY,"星期一");map.put(DayOfWeek.TUESDAY,"星期二");map.put(DayOfWeek.WEDNESDAY,"星期三");map.put(DayOfWeek.THURSDAY,"星期四");map.put(DayOfWeek.FRIDAY,"星期五");map.put(DayOfWeek.SATURDAY,"星期六");map.put(DayOfWeek.SUNDAY,"星期日");System.out.println(地图);System.out.println(map.get(DayOfWeek.MONDAY));}enumDayOfWeek{MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY}原理put方法源码如下:publicVput(Kkey,Vvalue){//查看枚举类型看是否key与构造函数typeCheck(key)传入的类类型一致;//枚举的顺序intindex=key.ordinal();//原始位置的值ObjectoldValue=vals[index];//设置值vals[index]=maskNull(value);如果(oldValue==null)大小++;返回unmaskNull(旧值);}通过put源码发现存储是通过一个数组实现的,不需要扩展使用场景。如果在项目中遇到以枚举为key的映射容器,可以先选择EnumMap。IdentityHashMap引入了一个使用哈希表实现Map接口的类,在比较键(和值)时使用引用相等而不是对象相等。换句话说,在IdentityHashMap中,当且仅当(k1==k2)时,两个键k1和k2才被认为是相等的。(在普通的Map实现(如HashMap)中,当且仅当(k1==null?k2==null:k1.equals(k2))时,两个键k1和k2才被认为是相等的)。非线程安全的无序键不能为空Democasepublicstaticvoidmain(String[]args){//hashMapMaphashMap=newHashMap<>();//identityHashMapMapidentityHashMap=newIdentityHashMap<>();hashMap.put(newInteger(200),"a");hashMap.put(newInteger(200),"b");identityHashMap.put(newInteger(200),"a");identityHashMap.put(newInteger(200),"b");//遍历hashmapSystem.out.println("hashmapresult:");hashMap.forEach((key,value)->{System.out.println("key="+key+",value="+value);});//遍历hashmapSystem.out.println("identityHashMapresult:");identityHashMap.forEach((key,value)->{System.out.println("key="+key+",value="+value);});}运行结果:原理实现IdentityHashMap底层数据结构是一个数组,我们注意put方法:调用hash方法,获取key在表中的位置索引,然后进行赋值操作,也是分为三种情况:1.item==k,找到对应的key和valuee有key右边相邻的位置,更新tab[i+1],返回原值;2.item==null,说明表中没有对应的key值,跳出for循环,执行tab[i]=k和tab[i+1]=value插入新的key。个人感觉这里扩容的时机不是很好。终于找到更新位置了。因为展开完全没有了,只好重新计算一遍。和HashMap一样,更新后可以扩容。3.item!=null&&item!=key,说明发生hash冲突,处理完冲突后调用nextKeyIndex获取索引位置,然后重复上述过程。再来看hash方法:System.identityHashCode方法,用于获取IdentityHashMap中的hash值。在不重写Object.hashCode方法的情况下,System.identityHashCode和Object.hashCode返回的值相同,相当于对象的唯一HashCode。System.identityHashCode(null)始终返回0,无论Object.hashCode是否改写,都不会影响System.identityHashCode的执行结果。使用场景当我们必须用地址相等来判断值是否相等,并且我们确定只要他们的地址不相等时,他们的equals方法的结果也一定是不相等的。小结本文主要讲解集合中不常用的Maps。当然,我们也需要了解它们的特点,有时候也会用到。