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 .