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. MapIntegers

最も簡単な解決策の1つは、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());

単にMapの便利なcomputeメソッドを使用しました。このメソッドは、カウンターをインクリメントするか、キーが存在しない場合は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. ストリーム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. Integer配列を持つMap

次に、値として使用されるInteger配列内のカウンターをラップするMapを使用しましょう。

@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]++;
    }
}

int arraysを値として使用して単純なHashMapを作成した方法に注意してください。

counterWithPrimitiveArrayメソッドでは、配列の各値を反復処理しながら、次のことを行います。

  • 国名をキーとして渡して、counterMapgetを呼び出します

  • キーがすでに存在していたかどうかを確認します。 エントリが既に存在する場合、単一の「1」を持つプリミティブ整数配列の新しいインスタンスを作成します。 エントリが存在しない場合、配列に存在するカウンタ値をインクリメントします

このメソッドは、ラッパーの実装よりも優れています–as it creates fewer objects.

2.4. MapMutableInteger

次に、次のようにプリミティブ整数カウンターを埋め込むラッパーオブジェクトを作成しましょう。

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配列内の各国を反復処理しながら、次のことを行います。

  • 国名をキーとして渡して、counterMapでgetを呼び出します

  • キーが既に存在するかどうかを確認します。 エントリがない場合は、カウンタ値を1に設定するMutableIntegerのインスタンスを作成します。 国がマップに存在する場合、MutableIntegerに存在するカウンター値をインクリメントします

カウンターを作成するこの方法は、前の方法よりも優れています–as we’re reusing the same MutableInteger and thereby creating fewer objects.

これは、ApacheコレクションHashMultiSetが、MutableIntegerとして値を持つHashMapを内部に埋め込む場合の動作方法です。

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ベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。