1. Обзор
В этом руководстве мы покажем различные способы реализации счетчика слов в Java.
2. Реализации счетчиков
Давайте начнем с простого подсчета количества слов в этом массиве:
static String[] COUNTRY_NAMES
= { "China", "Australia", "India", "USA", "USSR", "UK", "China",
"France", "Poland", "Austria", "India", "USA", "Egypt", "China" };
Если мы хотим обрабатывать огромные файлы, нам нужно использовать другие варианты, описанные здесь .
2.1. Карта
с целыми числами
Одним из самых простых решений было бы создать Map
, хранить слова в качестве ключей и количество вхождений в качестве значений:
Map<String, Integer> counterMap = new HashMap<>();
for (String country : COUNTRY_NAMES) {
counterMap.compute(country, (k, v) -> v == null ? 1 : v + 1);
}
assertEquals(3, counterMap.get("China").intValue());
assertEquals(2, counterMap.get("India").intValue());
Мы просто использовали удобный метод вычисления
Map
, который увеличивает счетчик или инициализирует его значением 1, если ключ отсутствует. ``
Однако этот метод создания счетчика неэффективен, так как Integer
неизменяем, поэтому каждый раз, когда мы увеличиваем счетчик, мы создаем новый объект Integer .
2.2. Потоковое API
Теперь давайте воспользуемся Java 8 Stream API, параллельными потоками
и сборщиком groupingBy
() :
@Test
public void whenMapWithLambdaAndWrapperCounter_runsSuccessfully() {
Map<String, Long> counterMap = new HashMap<>();
Stream.of(COUNTRY_NAMES)
.collect(Collectors.groupingBy(k -> k, ()-> counterMap,
Collectors.counting());
assertEquals(3, counterMap.get("China").intValue());
assertEquals(2, counterMap.get("India").intValue());
}
Точно так же мы могли бы использовать parallelStream
:
@Test
public void whenMapWithLambdaAndWrapperCounter_runsSuccessfully() {
Map<String, Long> counterMap = new HashMap<>();
Stream.of(COUNTRY_NAMES).parallel()
.collect(Collectors.groupingBy(k -> k, ()-> counterMap,
Collectors.counting());
assertEquals(3, counterMap.get("China").intValue());
assertEquals(2, counterMap.get("India").intValue());
}
2.3. Карта
с целочисленным
массивом
Далее давайте воспользуемся Map
, которая заключает счетчик в массив Integer
, используемый в качестве значения:
@Test
public void whenMapWithPrimitiveArrayCounter_runsSuccessfully() {
Map<String, int[]> counterMap = new HashMap<>();
counterWithPrimitiveArray(counterMap);
assertEquals(3, counterMap.get("China")[0]);
assertEquals(2, counterMap.get("India")[0]);
}
private void counterWithPrimitiveArray(Map<String, int[]> counterMap) {
for (String country : COUNTRY_NAMES) {
counterMap.compute(country, (k, v) -> v == null ?
new int[] { 0 } : v)[0]++;
}
}
Обратите внимание, как мы создали простой HashMap
с массивами int
в качестве значений.
В методе counterWithPrimitiveArray
при переборе каждого значения массива мы:
- вызвать
get
наcounterMap
, передав название страны в качестве ключа - проверить, был ли уже ключ или нет. Если запись уже существует, мы создаем новый экземпляр примитивного целочисленного массива с одной «1». Если запись отсутствует, мы увеличиваем значение счетчика, присутствующее в массиве.
Этот метод лучше, чем реализация-обертка , так как он создает меньше объектов.
2.4. Карта
с изменяемым целым числом
Затем давайте создадим объект-оболочку, который встраивает примитивный целочисленный счетчик, как показано ниже:
private static class MutableInteger {
int count = 1;
public void increment() {
this.count++;
}
// getter and setter
}
Давайте посмотрим, как мы можем использовать класс выше в качестве счетчика:
@Test
public void whenMapWithMutableIntegerCounter_runsSuccessfully() {
Map<String, MutableInteger> counterMap = new HashMap<>();
mapWithMutableInteger(counterMap);
assertEquals(3, counterMap.get("China").getCount());
assertEquals(2, counterMap.get("India").getCount());
}
private void counterWithMutableInteger(
Map<String, MutableInteger> counterMap) {
for (String country : COUNTRY_NAMES) {
counterMap.compute(country, (k, v) -> v == null
? new MutableInteger(0) : v).increment();
}
}
В методе mapWithMutableInteger
при переборе каждой страны в массиве COUNTRY_NAMES
мы:
- вызвать get на
counterMap
, передав название страны в качестве ключа - проверьте, присутствует ли уже ключ или нет. Если запись отсутствует, мы создаем экземпляр
MutableInteger
, который устанавливает значение счетчика равным 1. Мы увеличиваем значение счетчика, присутствующее вMutableInteger
, если страна присутствует на карте.
Этот метод создания счетчика лучше предыдущего, поскольку мы повторно используем один и тот же MutableInteger
и тем самым создаем меньше объектов.
Вот как работает Apache Collections HashMultiSet
, где он встраивает HashMap
со значением MutableInteger
внутри.
3. Анализ производительности
Вот диаграмма, в которой сравнивается производительность каждого метода, перечисленного выше.
Диаграмма выше создана с использованием JMH, и вот код, который создал приведенную выше статистику:
Map<String, Integer> counterMap = new HashMap<>();
Map<String, MutableInteger> counterMutableIntMap = new HashMap<>();
Map<String, int[]> counterWithIntArrayMap = new HashMap<>();
Map<String, Long> counterWithLongWrapperMap = new HashMap<>();
@Benchmark
public void wrapperAsCounter() {
counterWithWrapperObject(counterMap);
}
@Benchmark
public void lambdaExpressionWithWrapper() {
counterWithLambdaAndWrapper(counterWithLongWrapperMap );
}
@Benchmark
public void parallelStreamWithWrapper() {
counterWithParallelStreamAndWrapper(counterWithLongWrapperStreamMap);
}
@Benchmark
public void mutableIntegerAsCounter() {
counterWithMutableInteger(counterMutableIntMap);
}
@Benchmark
public void mapWithPrimitiveArray() {
counterWithPrimitiveArray(counterWithIntArrayMap);
}
4. Вывод
В этой быстрой статье мы проиллюстрировали различные способы создания счетчиков слов с помощью Java.
Реализацию этих примеров можно найти в проекте GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.