Перейти к основному содержимому

Руководство по мультинабору гуавы

· 3 мин. чтения

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 должен вернуть, что заголовок существует, и предоставить нам правильный счет :

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. Сравнение с картой

Без доступа к 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 , нам нужно запомнить текущий счетчик и соответствующим образом настроить его. Нам также необходимо каждый раз реализовывать эту логику в нашем вызывающем коде или создавать для этой цели собственную библиотеку. Наш код также должен управлять аргументом значения . Если мы не будем осторожны, мы можем легко установить значение 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 .

Однако следует отметить, что потокобезопасность не гарантирует согласованности. Использование методов добавления или удаления будет хорошо работать в многопоточной среде, но что, если несколько потоков вызывают метод setCount ? ``

Если мы используем метод setCount , окончательный результат будет зависеть от порядка выполнения в потоках , который не всегда можно предсказать. Методы добавления и удаления являются добавочными, и 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 в параллельном коде, мы должны использовать приведенную выше версию, чтобы гарантировать согласованность. Многопоточный клиент может выполнить повторную попытку, если изменение счетчика не удалось.

6. Заключение

В этом кратком руководстве мы обсудили, когда и как использовать Multiset, сравнили его со стандартной картой и рассмотрели, как лучше всего использовать его в параллельном приложении.

Как всегда, исходный код примеров можно найти на GitHub .