【Guava】BiMapMultimapMultiset

news/2025/4/2 17:46:16/文章来源:https://www.cnblogs.com/seven97-top/p/18803292

BiMap

Map 可以实现 key -> value 的映射,如果想要 value -> key 的映射,就需要定义两个 Map,并且同步更新,很不优雅。Guava 提供了 BiMap 支持支持双向的映射关系,常用实现有HashMap, EnumBiMap, EnumHashBiMap...

而它对key和value严格的保证唯一性。如果使用put方法添加相同的value值或key值则会抛出异常:java.lang.IllegalArgumentException,如果使用forcePut方法添加则会覆盖掉原来的value值。

BiMap<String, Integer> biMap = HashBiMap.create();
biMap.put("A", 100);// 删除已存在的 KV,重新添加 KV
biMap.forcePut("A", 200);// 获取反向映射
BiMap<Integer, String> inverse = biMap.inverse();
log.debug("{}", inverse.get(100));

这里主要使用HashBiMap 进行分析

成员变量

private static final double LOAD_FACTOR = 1.0D;
// BiEntry是HashBiMap中为的Map.Entry接口的实现类,这里定义了两个BiEntry,一个是实现使用Key找到value的,另一个是实现使用value找到key的
private transient HashBiMap.BiEntry<K, V>[] hashTableKToV;
private transient HashBiMap.BiEntry<K, V>[] hashTableVToK;
private transient int size;
private transient int mask;
private transient int modCount;
private transient BiMap<V, K> inverse;

HashMap做的是唯一key值对应的value可以不唯一,而Bimap做的是唯一key值,value值也要唯一,方便从key找到value,从value找到key

private static final class BiEntry<K, V> extends ImmutableEntry<K, V> {//key的hash值final int keyHash;//value的hash值final int valueHash;@Nullable//为key链表做的指向下一个节点的变量HashBiMap.BiEntry<K, V> nextInKToVBucket;@Nullable//为value链表做的指向下一个节点的变量HashBiMap.BiEntry<K, V> nextInVToKBucket;BiEntry(K key, int keyHash, V value, int valueHash) {super(key, value);this.keyHash = keyHash;this.valueHash = valueHash;}
}

对比一下HashMap的Node源码:

static class Node<K,V> implements Map.Entry<K,V> {//因为HashMap实现的功能只需要key找到value,所以这里的hash值默认就是key的hash值final int hash;final K key;V value;//在HashMap中的链表只做key的链表就好,所以只需要一个指向下一个节点的变量Node<K,V> next;
}

构造方法

//传入期望容器长度
private HashBiMap(int expectedSize) {this.init(expectedSize);
}

可以看到构造方法是私有的,所以在类中一定会有静态方法构造器会用到这个私有的构造方法。

这个构造方法调用了init方法,可以看一下init方法的源码:

private void init(int expectedSize) {CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");//经过closedTableSize方法运算达到期望的实际值int tableSize = Hashing.closedTableSize(expectedSize, 1.0D);//初始化key和value存储链表的数组this.hashTableKToV = this.createTable(tableSize);this.hashTableVToK = this.createTable(tableSize);//初始化mask为数组最大小标值this.mask = tableSize - 1;//初始化modCount值为0this.modCount = 0;//初始化size值为0this.size = 0;
}

静态方法构造器

public static <K, V> HashBiMap<K, V> create() {//调用另一个create构造器,期望长度为16return create(16);
}
public static <K, V> HashBiMap<K, V> create(int expectedSize) {//直接创建一个长度为expectedSize的HashBiMapreturn new HashBiMap(expectedSize);
}
public static <K, V> HashBiMap<K, V> create(Map<? extends K, ? extends V> map) {//创建一个与传入map相同长度的biMapHashBiMap bimap = create(map.size());//然后将传入map的值全部赋值给新的BiMapbimap.putAll(map);return bimap;
}

添加功能

添加功能有两种,一个是put方法,一个是forcePut方法:

public V put(@Nullable K key, @Nullable V value) {return this.put(key, value, false);
}
public V forcePut(@Nullable K key, @Nullable V value) {return this.put(key, value, true);
}

可以看到,这两个方法同时调用了本类的put方法,只不过是这两个方法的第三个参数不同,一个为ture,一个为false,看一下put的源码,看看第三个参数有什么用

private V put(@Nullable K key, @Nullable V value, boolean force) {//获取传入key的hash值int keyHash = hash(key);//获取传入value的hash值int valueHash = hash(value);//根据key的值和它的hash值查找是否存在这个节点,seekByKey方法就是遍历了这个keyhash所映射的下标上的链表进行查找。HashBiMap.BiEntry oldEntryForKey = this.seekByKey(key, keyHash);if(oldEntryForKey != null && valueHash == oldEntryForKey.valueHash && Objects.equal(value, oldEntryForKey.value)) {//如果这个key值存在,并且value也相等,则返回这个valuereturn value;} else {//使用seekByValue查找这个value是否存在HashBiMap.BiEntry oldEntryForValue = this.seekByValue(value, valueHash);if(oldEntryForValue != null) {//如果存在,则判断force(第三个参数)是否为falseif(!force) {//Value已经存在了,因此要判断是否允许强制插入//如果force(第三个参数)为false//则直接抛出异常String newEntry1 = String.valueOf(String.valueOf(value));throw new IllegalArgumentException((new StringBuilder(23 + newEntry1.length())).append("value already present: ").append(newEntry1).toString());}//如果force(第三个参数)为true,则删除这个节点,这个方法是双向删除this.delete(oldEntryForValue);}//如果key存在,则删除这个节点if(oldEntryForKey != null) {this.delete(oldEntryForKey);}//根据key,value,keyHash,valueHash创建一个BiEntryHashBiMap.BiEntry newEntry = new HashBiMap.BiEntry(key, keyHash, value, valueHash);//插入这个节点。this.insert(newEntry);//插入完成,刷新一下,看看是否需要扩容this.rehashIfNecessary();return oldEntryForKey == null?null:oldEntryForKey.value;}
}
private void insert(HashBiMap.BiEntry<K, V> entry) {//计算出这个节点在key容器中的下标位置int keyBucket = entry.keyHash & this.mask;//使当前节点的keynext指向当前下标位置上entry.nextInKToVBucket = this.hashTableKToV[keyBucket];//将当前节点赋值给这个下标位置this.hashTableKToV[keyBucket] = entry;//value如key一样int valueBucket = entry.valueHash & this.mask;entry.nextInVToKBucket = this.hashTableVToK[valueBucket];this.hashTableVToK[valueBucket] = entry;//size加1++this.size;++this.modCount;
}

Multimap

支持将 key 映射到多个 value 的方式,而不用定义Map<K, List<V>> 或 Map<K, Set<V>>这样的形式。实现类包括ArrayListMultimap, HashMultimap, LinkedListMultimap, TreeMultimap...

// 列表实现
ListMultimap<String, Integer> listMultimap = MultimapBuilder.hashKeys().arrayListValues().build();
// 集合实现
SetMultimap<String, Integer> setMultimap = MultimapBuilder.treeKeys().hashSetValues().build();listMultimap.put("A", 1);
listMultimap.put("A", 2);
listMultimap.put("B", 1);
// {A=[1, 2], B=[1, 2]}
log.debug("{}", listMultimap);
// [1, 2],不存在则返回一个空集合
log.debug("{}", listMultimap.get("A"));
// [1, 2] 移除 key 关联的所有 value
List<Integer> valList = listMultimap.removeAll("A");// 返回普通 map 的视图,仅支持 remove,不能 put,且会更新原始的 listMultimap
Map<String, Collection<Integer>> map = listMultimap.asMap();

HashMultimap构造器

因为他的构造方法是私有的,所有他会拥有静态方法构造器:

public static <K, V> HashMultimap<K, V> create() {//new一个HashMultimap,不传入任何值return new HashMultimap();
}
public static <K, V> HashMultimap<K, V> create(int expectedKeys, int expectedValuesPerKey) {//new一个HashHultimap,传入两个值,一个是期望key的长度,另一个是期望value的长度return new HashMultimap(expectedKeys, expectedValuesPerKey);
}
public static <K, V> HashMultimap<K, V> create(Multimap<? extends K, ? extends V> multimap) {//传入一个Multimap值return new HashMultimap(multimap);
}

三个构造方法都调用了私有的构造器,私有构造器的源码如下:

private HashMultimap() {//new一个新的map然后交给父类处理super(new HashMap());
}
private HashMultimap(int expectedKeys, int expectedValuesPerKey) {//获取一个expectedKeys 的map然后交给父类处理super(Maps.newHashMapWithExpectedSize(expectedKeys));Preconditions.checkArgument(expectedValuesPerKey >= 0);this.expectedValuesPerKey = expectedValuesPerKey;
}
private HashMultimap(Multimap<? extends K, ? extends V> multimap) {//获取一个multimap的长度的map交给父类处理super(Maps.newHashMapWithExpectedSize(multimap.keySet().size()));this.putAll(multimap);
}

三个私有构造方法都调用了父类的构造方法,接下来看看父类的构造器源码,发现最后的Multimap的数据结构也体现在AbstractMapBasedMultimap这个类中,所以看一下这个类的构造器个变量:

//底层数据结构是一个key为一个Object类,value为一个容器
private transient Map<K, Collection<V>> map;
//Multimap总长度
private transient int totalSize;
protected AbstractMapBasedMultimap(Map<K, Collection<V>> map) {Preconditions.checkArgument(map.isEmpty());this.map = map;
}

put方法的实现

public boolean put(@Nullable K key, @Nullable V value) {
//首先在map容器中查看是否有这个key值存在。
Collection collection = (Collection)this.map.get(key);
//如果collection为null,则说明这个key值在map容器中不存在
if(collection == null) {//根据这个key创建一个容器collection = this.createCollection(key);//然后将value放在这个容器中if(collection.add(value)) {++this.totalSize;this.map.put(key, collection);return true;} else {throw new AssertionError("New Collection violated the Collection spec");}//如果这个容器存在则直接放入value值} else if(collection.add(value)) {++this.totalSize;return true;} else {return false;}
}

get方法的实现

public Collection<V> get(@Nullable K key) {//首先在map容器中查看是否有这个key值存在。Collection collection = (Collection)this.map.get(key);//如果为null,则为其创建一个容器if(collection == null) {collection = this.createCollection(key);}//根据本类的wrapCollection方法找到并返回一个集合类return this.wrapCollection(key, collection);
}

Multiset

Multiset 是一个新的集合类型,可以多次添加相等的元素,既可以看成是无序的列表,也可以看成存储元素和对应数量的键值对映射[E1: cnt1; E2:cnt2]。常用实现包括 HashMultiset, TreeMultiset, LinkedHashMultiset...

Multiset<String> multiset = HashMultiset.create();
multiset.add("A");
multiset.add("A");
multiset.add("B");
// 输出:[A x 2, B]
log.debug("{}", multiset);// 元素总数
log.debug("{}", multiset.size());
// 不重复元素个数
log.debug("{}", multiset.elementSet().size());
// 设置元素计数
multiset.setCount("A", 3);
// 获取元素个数
log.debug("{}", multiset.count("A"));

接口源码

public interface Multiset<E> extends Collection<E> {//返回给定参数元素的个数int count(@Nullable Object var1);//向其中添加指定个数的元素int add(@Nullable E var1, int var2);//移除相应个数的元素int remove(@Nullable Object var1, int var2);//设定某一个元素的重复次数int setCount(E var1, int var2);//将符合原有重复个数的元素修改为新的重复次数boolean setCount(E var1, int var2, int var3);//将不同的元素放入一个Set中Set<E> elementSet();//类似与Map.entrySet 返回Set<Multiset.Entry>。包含的Entry支持使用getElement()和getCount()Set<Multiset.Entry<E>> entrySet();boolean equals(@Nullable Object var1);int hashCode();String toString();//返回迭代器Iterator<E> iterator();//判断是否存在某个元素boolean contains(@Nullable Object var1);//判断是否存在集合中所有元素boolean containsAll(Collection<?> var1);//添加元素boolean add(E var1);//删除某个元素boolean remove(@Nullable Object var1);//删除集合中所有元素boolean removeAll(Collection<?> var1);boolean retainAll(Collection<?> var1);public interface Entry<E> {E getElement();int getCount();boolean equals(Object var1);int hashCode();String toString();}
}

Multiset的接口中方法的实现在AbstractMapBasedMultiset抽象类中,下面针对AbstractMapBasedMultiset类的存储数据结构。add、remove、count和迭代器的实现进行分析

存储数据结构

//可以看到实际存储结构为一个Map,key为存储元素,Count类型存储是key这个元素的个数,看一下Count源码:
private transient Map<E, Count> backingMap;final class Count implements Serializable {//记录当前个数private int value;//构造方法,为变量赋值Count(int value) {this.value = value;}//获取当前个数public int get() {return this.value;}//加上指定个数,先加在返回加完后的值public int getAndAdd(int delta) {int result = this.value;this.value = result + delta;return result;}//加上指定个数,先返回,在进行相加public int addAndGet(int delta) {return this.value += delta;}//直接设置当前个数public void set(int newValue) {this.value = newValue;}//先设置新的值在返回这个值大小public int getAndSet(int newValue) {int result = this.value;this.value = newValue;return result;}public int hashCode() {return this.value;}public boolean equals(@Nullable Object obj) {return obj instanceof Count && ((Count)obj).value == this.value;}public String toString() {return Integer.toString(this.value);}
}

构造方法

protected AbstractMapBasedMultiset(Map<E, Count> backingMap) {//存储的Map可以为任意类型的Mapthis.backingMap = (Map)Preconditions.checkNotNull(backingMap);//获取当前Multiset长度this.size = (long)super.size();
}

add方法

public int add(@Nullable E element, int occurrences) {//如果想要添加的个数为0if(occurrences == 0) {//如果存在则返回这个元素的个数,否则返回0return this.count(element);} else {Preconditions.checkArgument(occurrences > 0, "occurrences cannot be negative: %s", new Object[]{Integer.valueOf(occurrences)});//根据想要插入的元素在map中找到CountCount frequency = (Count)this.backingMap.get(element);int oldCount;//如果key所对应的Count为nullif(frequency == null) {//设置原来数据为0oldCount = 0;//将这个元素和所对应的Count添加到Map中this.backingMap.put(element, new Count(occurrences));} else {//获取原来个数oldCount = frequency.get();//计算出新的个数long newCount = (long)oldCount + (long)occurrences;Preconditions.checkArgument(newCount <= 2147483647L, "too many occurrences: %s", new Object[]{Long.valueOf(newCount)});//为key所对应的Count添加occurrences个frequency.getAndAdd(occurrences);}//将当前的size加上occurrencesthis.size += (long)occurrences;//返回原来数据return oldCount;}
}

remove方法

public int remove(@Nullable Object element, int occurrences) {//如果想要删除0个if(occurrences == 0) {//返回当前这个元素的个数,如果不存在容器中返回0return this.count(element);} else {Preconditions.checkArgument(occurrences > 0, "occurrences cannot be negative: %s", new Object[]{Integer.valueOf(occurrences)});//根据要删除的值作为key获取到他的CountCount frequency = (Count)this.backingMap.get(element);//如果对应的Count为null,则返回0if(frequency == null) {return 0;} else {//获取当前个数int oldCount = frequency.get();int numberRemoved;//如果原来个数大于想要删除的个数if(oldCount > occurrences) {numberRemoved = occurrences;} else {//如果原来个数小于想要删除的个数numberRemoved = oldCount;//直接将这个元素在Map中删除this.backingMap.remove(element);}//设置这个元素对应的Countfrequency.addAndGet(-numberRemoved);this.size -= (long)numberRemoved;return oldCount;}}
}

Count方法

public int count(@Nullable Object element) {//以传入的作为key,在map容器中找到相对应的CountCount frequency = (Count)Maps.safeGet(this.backingMap, element);//如果这个Count为空,则返回0,否则返回Count中的值return frequency == null?0:frequency.get();
}

迭代器

public Iterator<E> iterator() {return new AbstractMapBasedMultiset.MapBasedMultisetIterator();
}

Multiset中有一个实现了Iterator接口的类:

private class MapBasedMultisetIterator implements Iterator<E> {final Iterator<java.util.Map.Entry<E, Count>> entryIterator;java.util.Map.Entry<E, Count> currentEntry;int occurrencesLeft;boolean canRemove;MapBasedMultisetIterator() {//获取当前map容器的迭代器this.entryIterator = AbstractMapBasedMultiset.this.backingMap.entrySet().iterator();}//根据当前迭代器判断是否还有元素public boolean hasNext() {return this.occurrencesLeft > 0 || this.entryIterator.hasNext();}public E next() {//如果occurrencesLeft为0,则说明现在处于刚开始,或上一个元素完成if(this.occurrencesLeft == 0) {//迭代器向下获取一个元素this.currentEntry = (java.util.Map.Entry)this.entryIterator.next();//获取到当前元素的个数this.occurrencesLeft = ((Count)this.currentEntry.getValue()).get();}//因为是获取一个元素,所以减去这一个--this.occurrencesLeft;this.canRemove = true;return this.currentEntry.getKey();}public void remove() {CollectPreconditions.checkRemove(this.canRemove);int frequency = ((Count)this.currentEntry.getValue()).get();if(frequency <= 0) {throw new ConcurrentModificationException();} else {if(((Count)this.currentEntry.getValue()).addAndGet(-1) == 0) {this.entryIterator.remove();}AbstractMapBasedMultiset.access$110(AbstractMapBasedMultiset.this);this.canRemove = false;}}
}

这个迭代器的好处是,存储多个相同的值,不会占用多个地方,只会占用1个位置。

往期推荐

  • 《SpringBoot》EasyExcel实现百万数据的导入导出
  • 《SpringBoot》史上最全SpringBoot相关注解介绍
  • Spring框架IoC核心详解
  • 万字长文带你窥探Spring中所有的扩展点
  • 如何实现一个通用的接口限流、防重、防抖机制
  • 万字长文带你深入Redis底层数据结构
  • volatile关键字最全原理剖析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/909351.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VMware ESXi 8.0U3d macOS Unlocker OEM BIOS 标准版和厂商定制版,已适配主流品牌服务器

VMware ESXi 8.0U3d macOS Unlocker & OEM BIOS 标准版和厂商定制版,已适配主流品牌服务器VMware ESXi 8.0U3d macOS Unlocker & OEM BIOS 标准版和厂商定制版 ESXi 8.0U3d 标准版,Dell (戴尔)、HPE (慧与)、Lenovo (联想)、Inspur (浪潮)、Cisco (思科)、Hitachi (日…

AMS1117-LDO(线性稳压器)稳压电路

AMS1117-LDO(线性稳压器)稳压电路 原理图引脚说明编号 名称 功能描述1 GND GND2 VOUT 输出3 VIN 输入4 VOUT 输出拓展C8和C7为输出滤波电容,用于抑制自激振荡。如果这两个电容不接,线性稳压器的输出通常会是一个振荡波形。 电容C5和C6是输入电容。对于交流电压整流输入,它…

Avalonia 界面效果 滚动的渐变矩形边框

本文将和大家介绍一个 Avalonia 界面效果,制作一个滚动的渐变矩形边框本文代码基于 Avalonia 11.2.x 版本实现,预期在其他 Avalonia 版本也能正常使用 本文效果由 晓嗔戈 提供,我只是记录此实现方法的工具人 界面效果如下图所示,录制的gif中颜色存在一些偏差,动画有些卡顿…

Avalonia 界面效果 三个圆实现模糊界面动效背景

本文将和大家介绍一个 Avalonia 动效界面效果,由三个圆带模糊效果实现的模糊界面动效背景,适合用在各种 AIGC 主题的应用里面本文代码基于 Avalonia 11.2.x 版本实现,预期在其他 Avalonia 版本也能正常使用 本文效果由 晓嗔戈 提供,我只是记录此实现方法的工具人 界面效果如…

儿子的画

昨天儿子在幼儿园学习了自制小册子,并在上面画画。 晚上睡觉前,心血来潮想要再展示一下他在学校是怎么弄的,于是又一顿操作起来,动作还算麻利,只是完成之后都已过十点了,非要我们帮他配上文字,我一开始不明就理,以为写个标题就好了..., 但最后终于搞懂他是要我帮忙下一…

团队协作管理:贝尔宾团队角色模型学习

“没有完美的个人,只有完美的团队。” 任何企业的领导者要想使自己的企业能够快速地发展和成长,就必须对团队建设的重要性有正确的认识。团队角色理论 团队角色理论是管理学中用于分析和优化团队协作的重要理论,可以帮助管理者认识人才、选拔人才,组建高效率合作团队。也可…

钉钉 + AI 网关给 DeepSeek 办入职

通过 Open-WebUI 在企业内部部署一套 DeepSeek 只是第一步,给 DeepSeek 办理入职,在钉钉等企业通讯工具上和 DeepSeek 对话才是真时尚。通过 Open-WebUI 在企业内部部署一套 DeepSeek 只是第一步,给 DeepSeek 办理入职,在钉钉等企业通讯工具上和 DeepSeek 对话才是真时尚。…

网络工程师修仙指北---STP(Spanning Tree Protocol)

网络工程师修仙指北---STP(Spanning Tree Protocol) Hello哇,欢迎来到《网络工程是修仙指北系列》,今天我们接着上一篇VLAN的内容,继续为大家介绍网络交换二层技术中另一个重要的内容---STP 一口小酒🍸,一首歌📻,阿轩带你修成仙! 上一篇中我们讲到,通过VLAN的技术…

「通义灵码+X」公开课开讲啦!和赛博同桌一起完成开发任务 有奖励

在AI技术重塑未来的今天,阿里云通义灵码团队携手高校开发者,推出「通义灵码+X系列公开课」暨赛博同桌计划,为编程学习注入全新活力!活动将于2025年3月12日至4月30日火热进行,无论你是技术小白还是代码达人,都能在这里找到与AI并肩学习的乐趣,赢取限定好礼!让AI成为你的…

云原生 Kafka 问卷调研启动,你的声音很重要!参与赢精美礼品!

Apache Kafka 作为高吞吐的分布式消息系统,支持实时数据采集、传输、存储及处理,广泛应用于日志收集、监控数据聚合、流式数据处理、在线和离线分析等场景,是大数据生态的核心组件。然而,随着云计算的快速发展,传统 Kafka 架构在云环境中的局限性日益凸显。Apache Kafka 作…

掌握FastAPI与Pydantic的跨字段验证技巧

title: 掌握FastAPI与Pydantic的跨字段验证技巧 date: 2025/04/01 00:32:07 updated: 2025/04/01 00:32:07 author: cmdragon excerpt: FastAPI中的Pydantic跨字段一致性验证用于处理用户注册、表单提交等场景中多个字段的联合验证需求。Pydantic通过验证器装饰器和根验证器实…

使用 Ollama 本地模型与 Spring AI Alibaba 的强强结合,打造下一代 RAG 应用

使用 Ollama 本地模型与 Spring AI Alibaba 的强强结合,打造下一代 RAG 应用作者:牧生 Spring AI Alibaba RAG Example 示例项目源码地址: https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-rag-example RAG 应用架构概述 1.1 核…