1. 概要

このチュートリアルでは、Javaで単語カウンターを実装するさまざまな方法を紹介します。

2. カウンターの実装

この配列内の単語の単語数を単純に計算することから始めましょう。

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

巨大なファイルを処理したい場合は、ここで説明されている他のオプションを選択する必要があります。

2.1. マップ整数

最も簡単な解決策の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の便利なcomputeメソッドを使用しました。このメソッドは、カウンターをインクリメントするか、キーが存在しない場合は1で初期化します。

ただし、 Integerは不変であるため、このカウンター作成方法は効率的ではありません。したがって、カウンターをインクリメントするたびに、新しいIntegerオブジェクトを作成します。

2.2. ストリームAPI

それでは、Java 8 Stream API、並列 Streams 、および 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. マップ整数配列

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

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

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

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

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

このメソッドは、ラッパーの実装よりも優れています– 作成するオブジェクトが少ないため、

2.4. Map With MutableInteger

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

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

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

カウンターを作成するこの方法は、以前の方法よりも優れています– 同じMutableIntegerを再利用しているため、作成するオブジェクトが少なくなります。

これは、Apache Collections HashMultiSet が、MutableIntegerとして値を持つHashMapを内部に埋め込む場合の動作です。

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