Guava Multiset简介
1. 概述
在本教程中,我们将探索Guava集合之一 —— Multiset像java.util.Set一样,它允许在没有保证顺序的情况下有效地存储和检索项目。
但是,与Set不同的是,它通过跟踪其包含的每个唯一元素的计数来允许同一元素多次出现。
2. Maven依赖
首先,让我们添加guava依赖项 :
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
3. 使用Multiset
让我们考虑一个书店,它有多个不同书籍的副本。我们可能想要执行诸如添加副本、获取副本数量以及在售出时移除一个副本等操作。由于Set不允许同一元素多次出现,因此它无法处理此要求。
让我们从添加书名的副本开始。Multiset应该返回标题存在并为我们提供正确的计数:
Multiset<String> bookStore = HashMultiset.create();
bookStore.add("Potter");
bookStore.add("Potter");
bookStore.add("Potter");
assertThat(bookStore.contains("Potter")).isTrue();
assertThat(bookStore.count("Potter")).isEqualTo(3);
现在让我们删除一个副本。我们希望计数会相应更新:
bookStore.remove("Potter");
assertThat(bookStore.contains("Potter")).isTrue();
assertThat(bookStore.count("Potter")).isEqualTo(2);
实际上,我们可以只设置计数,而不是执行各种加法操作:
bookStore.setCount("Potter", 50);
assertThat(bookStore.count("Potter")).isEqualTo(50);
Multiset验证计数值。如果我们将其设置为负数,则会抛出 IllegalArgumentException:
assertThatThrownBy(() -> bookStore.setCount("Potter", -1))
.isInstanceOf(IllegalArgumentException.class);
4. 与Map比较
在没有访问Multiset的情况下,我们可以通过使用java.util.Map实现我们自己的逻辑来实现上述所有操作:
Map<String, Integer> bookStore = new HashMap<>();
// adding 3 copies
bookStore.put("Potter", 3);
assertThat(bookStore.containsKey("Potter")).isTrue();
assertThat(bookStore.get("Potter")).isEqualTo(3);
// removing 1 copy
bookStore.put("Potter", 2);
assertThat(bookStore.get("Potter")).isEqualTo(2);
当我们想使用Map添加或删除副本时,我们需要记住当前计数并相应地调整它。我们也需要每次都在调用代码中实现这个逻辑,或者为此构建我们自己的库。我们的代码还需要控制value参数。如果我们不小心,即使两个值都无效,我们也可以轻松地将值设置为null或负数:
bookStore.put("Potter", null);
assertThat(bookStore.containsKey("Potter")).isTrue();
bookStore.put("Potter", -1);
assertThat(bookStore.containsKey("Potter")).isTrue();
正如我们所见,使用Multiset代替Map更方便。
5. 并发
当我们想在并发环境中使用Multiset时,我们可以使用ConcurrentHashMultiset,这是一个线程安全的Multiset实现。
我们应该注意到,线程安全并不能保证一致性。在多线程环境中使用add或remove方法会很好,但是如果有多个线程调用setCount方法呢?
如果我们使用setCount方法,最终结果将取决于跨线程的执行顺序,这不一定是可以预测的。add和 remove方法是增量的,并且 *ConcurrentHashMultiset *能够保护它们的行为。直接设置计数不是递增的,因此在同时使用时可能会导致意外结果。
但是,setCount方法还有另一种风格,它仅在当前值与传递的参数匹配时才更新计数。如果操作成功,该方法返回 true,这是一种乐观锁定:
Multiset<String> bookStore = HashMultiset.create();
// updates the count to 2 if current count is 0
assertThat(bookStore.setCount("Potter", 0, 2)).isTrue();
// updates the count to 5 if the current value is 50
assertThat(bookStore.setCount("Potter", 50, 5)).isFalse();
如果我们想在并发代码中使用setCount方法,我们应该使用上面的版本来保证一致性。如果更改计数失败,多线程客户端可以执行重试。