Contents

Guava Multiset简介

1. 概述

在本教程中,我们将探索Guava集合之一 —— Multisetjava.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实现。

我们应该注意到,线程安全并不能保证一致性。在多线程环境中使用addremove方法会很好,但是如果有多个线程调用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方法,我们应该使用上面的版本来保证一致性。如果更改计数失败,多线程客户端可以执行重试。