Java 8のコレクターガイド

Java 8のコレクターのガイド

1. 概要

このチュートリアルでは、Streamを処理する最終ステップで使用されるJava8のコレクターについて説明します。

Stream API自体について詳しく知りたい場合は、this articleを確認してください。

2. Stream.collect()メソッド

Stream.collect()は、Java 8のStream APIのターミナルメソッドの1つです。 Streamインスタンスに保持されているデータ要素に対して、変更可能なフォールド操作(要素を一部のデータ構造に再パッケージ化し、追加のロジックを適用し、それらを連結するなど)を実行できます。

この操作の戦略は、Collectorインターフェースの実装を介して提供されます。

3. Collectors

事前定義されたすべての実装は、Collectorsクラスにあります。 読みやすさの向上を活用するために、次の静的インポートを使用するのが一般的な方法です。

import static java.util.stream.Collectors.*;

または、選択した単一のインポートコレクター:

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

次の例では、次のリストを再利用します。

List givenList = Arrays.asList("a", "bb", "ccc", "dd");

3.1. Collectors.toList()

ToListコレクターは、すべてのStream要素をListインスタンスに収集するために使用できます。 覚えておくべき重要なことは、このメソッドでは特定のListの実装を想定できないという事実です。 これをより細かく制御したい場合は、代わりにtoCollectionを使用してください。

要素のシーケンスを表すStreamインスタンスを作成し、それらをListインスタンスに収集してみましょう。

List result = givenList.stream()
  .collect(toList());

3.2. Collectors.toSet()

ToSetコレクターは、すべてのStream要素をSetインスタンスに収集するために使用できます。 覚えておくべき重要なことは、このメソッドでは特定のSetの実装を想定できないという事実です。 これをさらに制御したい場合は、代わりにtoCollectionを使用できます。

要素のシーケンスを表すStreamインスタンスを作成し、それらをSetインスタンスに収集してみましょう。

Set result = givenList.stream()
  .collect(toSet());

Setには重複する要素が含まれていません。 コレクションに互いに等しい要素が含まれている場合、それらは結果のSetに1回だけ表示されます。

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
Set result = listWithDuplicates.stream().collect(toSet());
assertThat(result).hasSize(4);

3.3. Collectors.toCollection()

すでにお気づきかもしれませんが、toSet and toListコレクターを使用する場合、それらの実装を想定することはできません。 カスタム実装を使用する場合は、提供されている任意のコレクションでtoCollectionコレクターを使用する必要があります。

要素のシーケンスを表すStreamインスタンスを作成し、それらをLinkedListインスタンスに収集してみましょう。

List result = givenList.stream()
  .collect(toCollection(LinkedList::new))

これは、不変のコレクションでは機能しないことに注意してください。 このような場合、カスタムCollector実装を作成するか、collectingAndThenを使用する必要があります。

3.4. CollectorstoMap()

ToMapコレクターを使用して、Stream要素をMapインスタンスに収集できます。 これを行うには、2つの機能を提供する必要があります。

  • keyMapper

  • valueMapper

keyMapperは、Stream要素からMapキーを抽出するために使用され、valueMapperは、特定のキーに関連付けられた値を抽出するために使用されます。

これらの要素をMapに集めて、文字列をキーとして、長さを値として格納しましょう。

Map result = givenList.stream()
  .collect(toMap(Function.identity(), String::length))

Function.identity()は、同じ値を受け入れて返す関数を定義するための単なるショートカットです。

コレクションに重複した要素が含まれている場合はどうなりますか? toSetとは異なり、toMapは重複をサイレントにフィルタリングしません。 理解できます。このキーにどの値を選択するかをどのように判断する必要がありますか?

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
assertThatThrownBy(() -> {
    listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);

toMapは、値も等しいかどうかさえ評価しないことに注意してください。 キーが重複している場合は、すぐにIllegalStateExceptionをスローします。

キーの衝突があるこのような場合、別の署名でtoMapを使用する必要があります。

Map result = givenList.stream()
  .collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));

ここでの3番目の引数はBinaryOperatorで、衝突の処理方法を指定できます。 この場合、同じ文字列の長さも常に同じであることがわかっているため、これら2つの衝突する値のいずれかを選択します。

3.5. Collectors.c_ollectingAndThen()_

CollectingAndThenは、収集が終了した直後に結果に対して別のアクションを実行できるようにする特別なコレクターです。

Stream要素をListインスタンスに収集し、その結果をImmutableListインスタンスに変換してみましょう。

List result = givenList.stream()
  .collect(collectingAndThen(toList(), ImmutableList::copyOf))

3.6. Collectors.j_oining()_

Joiningコレクターは、Stream<String>要素の結合に使用できます。

以下を実行することで、それらを結合できます。

String result = givenList.stream()
  .collect(joining());

これは次のようになります。

"abbcccdd"

カスタムセパレーター、プレフィックス、ポストフィックスを指定することもできます。

String result = givenList.stream()
  .collect(joining(" "));

これは次のようになります。

"a bb ccc dd"

またはあなたは書くことができます:

String result = givenList.stream()
  .collect(joining(" ", "PRE-", "-POST"));

これは次のようになります。

"PRE-a bb ccc dd-POST"

3.7. Collectors.c_ounting()_

Countingは、すべてのStream要素を単純にカウントできる単純なコレクターです。

今書くことができます:

Long result = givenList.stream()
  .collect(counting());

3.8. Collectors.s_ummarizingDouble/Long/Int()_

SummarizingDouble/Long/Intは、抽出された要素のStream内の数値データに関する統計情報を含む特別なクラスを返すコレクターです。

以下を実行することで、文字列の長さに関する情報を取得できます。

DoubleSummaryStatistics result = givenList.stream()
  .collect(summarizingDouble(String::length));

この場合、次のことが当てはまります。

assertThat(result.getAverage()).isEqualTo(2);
assertThat(result.getCount()).isEqualTo(4);
assertThat(result.getMax()).isEqualTo(3);
assertThat(result.getMin()).isEqualTo(1);
assertThat(result.getSum()).isEqualTo(8);

3.9. Collectors.averagingDouble/Long/Int()

AveragingDouble/Long/Intは、抽出された要素の平均を返すだけのコレクターです。

以下を実行することで、平均文字列長を取得できます。

Double result = givenList.stream()
  .collect(averagingDouble(String::length));

3.10. Collectors.s_ummingDouble/Long/Int()_

SummingDouble/Long/Intは、抽出された要素の合計を返すだけのコレクターです。

以下を実行することで、すべての文字列の長さの合計を取得できます。

Double result = givenList.stream()
  .collect(summingDouble(String::length));

3.11. Collectors.maxBy()/minBy()

MaxBy/MinBy collectors return the biggest/the smallest element of a Stream according to a provided Comparator instance.

以下を行うことで最大の要素を選択できます。

Optional result = givenList.stream()
  .collect(maxBy(Comparator.naturalOrder()));

戻り値がOptionalインスタンスにラップされていることに注意してください。 これにより、ユーザーは空のコレクションコーナーケースを再考する必要があります。

3.12. CollectorsgroupingBy()

GroupingByコレクターは、オブジェクトをいくつかのプロパティでグループ化し、結果をMapインスタンスに格納するために使用されます。

それらを文字列の長さでグループ化し、グループ化の結果をSetインスタンスに格納できます。

Map> result = givenList.stream()
  .collect(groupingBy(String::length, toSet()));

これにより、以下が真になります。

assertThat(result)
  .containsEntry(1, newHashSet("a"))
  .containsEntry(2, newHashSet("bb", "dd"))
  .containsEntry(3, newHashSet("ccc"));

groupingByメソッドの2番目の引数はCollectorであり、任意のCollectorを自由に使用できることに注意してください。

3.13. Collectors.partitioningBy()

PartitioningByは、Predicateインスタンスを受け入れ、Boolean値をキーとして格納するMapインスタンスにStream要素を収集するgroupingByの特殊なケースです。値としてのコレクション。 「true」キーの下に、指定されたPredicateに一致する要素のコレクションがあり、「false」キーの下に、指定されたPredicateに一致しない要素のコレクションがあります。

あなたは書ける:

Map> result = givenList.stream()
  .collect(partitioningBy(s -> s.length() > 2))

これにより、マップには次のものが含まれます。

{false=["a", "bb", "dd"], true=["ccc"]}

3.14. Collectors.teeing()

これまでに学習したコレクターを使用して、特定のStreamから最大数と最小数を見つけましょう。

List numbers = Arrays.asList(42, 4, 2, 24);
Optional min = numbers.stream().collect(minBy(Integer::compareTo));
Optional max = numbers.stream().collect(maxBy(Integer::compareTo));
// do something useful with min and max

ここでは、2つの異なるコレクターを使用し、それら2つの結果を組み合わせて意味のあるものを作成しています。 Java 12より前は、このようなユースケースをカバーするために、指定されたStreamを2回操作し、中間結果を一時変数に格納してから、それらの結果を結合する必要がありました。

幸いなことに、Java 12にはこれらの手順を代行する組み込みコレクターが用意されています。2つのコレクターと結合機能を提供するだけです。

この新しいコレクターは、指定されたストリームを2つの異なる方向に向けてteesするため、teeing:と呼ばれます。

numbers.stream().collect(teeing(
  minBy(Integer::compareTo), // The first collector
  maxBy(Integer::compareTo), // The second collector
  (min, max) -> // Receives the result from those collectors and combines them
));

この例は、GitHubのcore-java-12プロジェクトで利用できます。

4. カスタムコレクター

Collector実装を作成する場合は、Collectorインターフェイスを実装し、その3つの汎用パラメーターを指定する必要があります。

public interface Collector {...}
  1. T –収集に使用できるオブジェクトのタイプ。

  2. A –可変アキュムレータオブジェクトのタイプ。

  3. R –最終結果のタイプ。

要素をImmutableSetインスタンスに収集するためのコレクターの例を書いてみましょう。 適切なタイプを指定することから始めます。

private class ImmutableSetCollector
  implements Collector, ImmutableSet> {...}

内部コレクション操作の処理には可変コレクションが必要なため、これにImmutableSetを使用することはできません。他の可変コレクションまたはオブジェクトを一時的に蓄積する可能性のある他のクラスを使用する必要があります。 この場合、ImmutableSet.Builderを続行し、5つのメソッドを実装する必要があります。

  • サプライヤー>supplier()

  • BiConsumer 、T>accumulator()

  • BinaryOperator >combiner()

  • Function 、ImmutableSet >finisher()

  • セット<特性>characteristics()

The supplier()メソッドは、空のアキュムレータインスタンスを生成するSupplierインスタンスを返すため、この場合、次のように記述できます。

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

The accumulator()メソッドは、既存のaccumulatorオブジェクトに新しい要素を追加するために使用される関数を返すため、Builderaddメソッドを使用してみましょう。

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

The combiner()メソッドは、2つのアキュムレータをマージするために使用される関数を返します。

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

The finisher()メソッドは、アキュムレータを最終結果タイプに変換するために使用される関数を返すため、この場合は、Builderbuildメソッドを使用します。

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

The characteristics()メソッドは、内部最適化に使用されるいくつかの追加情報をStreamに提供するために使用されます。 この場合、Setの要素の順序には注意を払わないため、Characteristics.UNORDEREDを使用します。 この主題に関する詳細情報を入手するには、CharacteristicsのJavaDocを確認してください。

@Override public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

完全な実装と使用方法を以下に示します。

public class ImmutableSetCollector
  implements Collector, ImmutableSet> {

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

@Override
public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

public static  ImmutableSetCollector toImmutableSet() {
    return new ImmutableSetCollector<>();
}

そしてここで実際に:

List givenList = Arrays.asList("a", "bb", "ccc", "dddd");

ImmutableSet result = givenList.stream()
  .collect(toImmutableSet());

5. 結論

この記事では、Java 8のCollectorsを詳細に調査し、その実装方法を示しました。 必ずcheck one of my projects which enhances the capabilities of parallel processing in Java.にする

すべてのコード例はGitHubで利用できます。 もっと興味深い記事on my siteを読むことができます。