Эффективный калькулятор частоты слов в Java

Эффективный калькулятор частоты слов в Java

1. обзор

В этом руководстве мы покажем различные способы реализации счетчика слов в Java.

2. Реализации счетчиков

Начнем с простого подсчета количества слов в этом массиве:

static String[] COUNTRY_NAMES
  = { "China", "Australia", "India", "USA", "USSR", "UK", "China",
  "France", "Poland", "Austria", "India", "USA", "Egypt", "China" };

Если мы хотим обрабатывать огромные файлы, нам нужно использовать другие параметры, описанные вhere.

2.1. Map сIntegers

Одним из самых простых решений было бы создатьMap, хранить слова как ключи и количество вхождений как значения:

Map 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());

Мы просто использовали удобный методcomputeMap, который увеличивает счетчик или инициализирует его с помощью 1, если ключ отсутствует.

Однакоthis method of creating counter isn’t efficient as Integer is immutable, so every time when we increment the counter, we create a new Integer object.

2.2. Stream API

Теперь давайте воспользуемся Java 8 Stream API, параллельнымStreams и сборщикомgroupingBy():

@Test
public void whenMapWithLambdaAndWrapperCounter_runsSuccessfully() {
    Map 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 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

Затем давайте воспользуемсяMap, который заключает счетчик в массивInteger, используемый в качестве значения:

@Test
public void whenMapWithPrimitiveArrayCounter_runsSuccessfully() {
    Map counterMap = new HashMap<>();

    counterWithPrimitiveArray(counterMap);

    assertEquals(3, counterMap.get("China")[0]);
    assertEquals(2, counterMap.get("India")[0]);
}

private void counterWithPrimitiveArray(Map counterMap) {
    for (String country : COUNTRY_NAMES) {
        counterMap.compute(country, (k, v) -> v == null ?
          new int[] { 0 } : v)[0]++;
    }
}

Обратите внимание, как мы создали простойHashMap со значениямиint arrays.

В методеcounterWithPrimitiveArray, перебирая каждое значение массива, мы:

  • вызватьget наcounterMap, передав название страны в качестве ключа

  • проверьте, присутствовал ли ключ или нет. Если запись уже присутствует, мы создаем новый экземпляр примитивного целочисленного массива с одним «1». Если запись отсутствует, мы увеличиваем значение счетчика в массиве

Этот метод лучше реализации обертки -as it creates fewer objects.

2.4. Map сMutableInteger

Затем давайте создадим объект-оболочку, который включает примитивный целочисленный счетчик, как показано ниже:

private static class MutableInteger {
    int count = 1;

    public void increment() {
        this.count++;
    }

    // getter and setter
}

Давайте посмотрим, как мы можем использовать вышеуказанный класс в качестве счетчика:

@Test
public void whenMapWithMutableIntegerCounter_runsSuccessfully() {
    Map counterMap = new HashMap<>();

    mapWithMutableInteger(counterMap);

    assertEquals(3, counterMap.get("China").getCount());
    assertEquals(2, counterMap.get("India").getCount());
}
private void counterWithMutableInteger(
  Map 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, если страна присутствует на карте

Этот способ создания счетчика лучше предыдущего -as we’re reusing the same MutableInteger and thereby creating fewer objects.

Вот как работает Apache CollectionsHashMultiSet, где он внутренне встраиваетHashMap со значениемMutableInteger.

3. Анализ производительности

Вот диаграмма, в которой сравнивается эффективность всех перечисленных выше методов. imageс

Вышеуказанная диаграмма создана с использованием JMH, а вот код, который создал приведенную выше статистику:

Map counterMap = new HashMap<>();
Map counterMutableIntMap = new HashMap<>();
Map counterWithIntArrayMap = new HashMap<>();
Map 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.

Реализацию этих примеров можно найти вthe GitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.