3. 集計操作
注: このセクションの概念をより深く理解するには、「ラムダ式とメソッドのリファレンス」を参照してください。
コレクションは何に使用しますか? オブジェクトを単にコレクションに保存し、そこに残しておくことはできません。ほとんどの場合、コレクションに格納されている項目を取得するには、コレクションを使用します。
ラムダ式のセクションで説明したシナリオをもう一度考えてみましょう。ソーシャル ネットワーキング アプリケーションを作成しているとします。特定の基準を満たすソーシャル ネットワーキング アプリケーションのメンバーに対して、管理者がメッセージの送信などのあらゆる種類のアクションを実行できる機能を作成したいと考えています。
前と同様に、このソーシャル ネットワーキング アプリケーションのメンバーは次のPersonクラスで表されると仮定します。
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
// ...
public int getAge() {
// ...
}
public String getName() {
// ...
}
}
次の例では、for-each
ループを使用してroster
、コレクションに含まれるすべてのメンバーの名前を出力します。
for (Person p : roster) {
System.out.println(p.getName());
}
次の例では、roster
コレクションに含まれるすべてのメンバーを出力しますが、集約操作を使用しますforEach
。
roster
.stream()
.forEach(e -> System.out.println(e.getName());
この例では、集計操作を使用するバージョンはfor-each
ループを使用するバージョンよりも長くなりますが、より複雑なタスクの場合は、バルク データ操作を使用するバージョンの方がコンパクトであることがわかります。
次のトピックを取り上げます。
- パイプラインとストリーム
- 集計演算とイテレータの違い (集計演算とイテレータの違い)
このセクションで説明されているコードの抜粋は、BulkDataOperationsExamplesの例で見つけてください。
パイプと小川
パイプライン ( pipeline
) は、一連の集計操作です。filter
次の例では、集計操作とforEach
で構成されるパイプラインを使用して、コレクションroster
に含まれる男性メンバーを出力します。
roster
.stream()
.filter(e -> e.getGender() == Person.Sex.MALE)
.forEach(e -> System.out.println(e.getName()));
この例を、for-each
ループを使用してroster
コレクションに含まれる男性メンバーを出力する次の例と比較してください。
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
パイプラインには次のコンポーネントが含まれています。
- source(
source
) :コレクション、配列、ジェネレーター関数、または I/O チャネルにすることができます。この場合、ソースはコレクションですroster
。 - 0 個以上の中間操作 (
intermediate operations
)。中間操作 (例filter
: ) は新しいストリームを生成します。
ストリームは要素のシーケンスです。セットとは異なり、要素を格納するデータ構造ではありません。代わりに、ストリームはパイプを介してソースから値を運びます。この例では、メソッドを呼び出してstream
コレクション ブックからroster
ストリームを作成します。
filter(filter
) オペレーションは、その述語 (オペレーションの引数) に一致する要素を含む新しいストリームを返します。この場合、述語はラムダ式ですe -> e.getGender() == Person.Sex.MALE
。オブジェクトe
のgender
フィールドのPerson.Sex.MALE
値が の場合、ブール値を返しますtrue
。したがって、この例のフィルター操作は、roster
コレクションのすべての男性メンバーを含むストリームを返します。 - 最終操作 (
terminal operation
)。最後の操作 (例: ) は、プリミティブ値 (double など)、コレクション、または値がまったくない場合など、forEach
非ストリーミング結果を生成しますforEach
。この場合、 forEach オペレーションの引数は、オブジェクトのメソッドe -> System.out.println(e.getName())
を呼び出すラムダ式です(Java ランタイムとコンパイラはオブジェクトの型を推論します)。e
getName
e
Person
次の例では、集計操作 と から構成されるパイプラインを使用してfilter
、セット フラワー デッキのすべての男性メンバーの平均年齢を計算しますmapToInt
。average
double average = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
mapToInt
この操作は、IntStream
次のタイプの新しいストリームを返します (整数値のみを含むストリームです)。この操作は、仮パラメータで指定された関数を、指定されたストリーム内の各要素に適用します。この場合、関数は ですPerson::getAge
。これはメンバーの年齢を返すメソッド参照です。(あるいは、ラムダ式を使用することもできますe -> e.getAge()
。) したがって、この例の操作は、コレクション ブックの男性メンバー全員の年齢をmapToInt
含むストリームを返します。roster
average
この操作では、IntStream
タイプ ストリームに含まれる要素の平均が計算されます。OptionalDouble
型のオブジェクトを返します。ストリームに要素が含まれていない場合、average
オペレーションは空のOptionalDouble
インスタンスを返し、呼び出したgetAsDouble
メソッドは をスローしますNoSuchElementException
。average
JDKには、ストリームの内容を結合して値を返すなど、多くの最終操作が含まれています。これらの操作はリダクション操作と呼ばれます。詳細については、「リダクション」セクションを参照してください。
集計演算とイテレータの違い
このような集計操作はforEach
イテレータと同じように見えます。ただし、これらにはいくつかの基本的な違いがあります。
- これらは内部反復を使用します
next
。集計操作には、コレクションの次の要素を処理するように指示するこのようなメソッドは含まれません。内部 delegate(internal delegation
) を使用すると、アプリケーションがどのコレクションを反復するかを決定しますが、JDK はコレクションを反復する方法を決定します。外部反復では、アプリケーションはどのコレクションを反復するか、およびコレクションを反復する方法を決定します。ただし、外側の反復ではコレクションの要素を順番に走査することしかできません。内部反復にはこの制限はありません。これにより、問題をサブ問題に分割し、それらを同時に解決し、サブ問題に対する解の結果を組み合わせるという並列コンピューティングを利用することが容易になります。詳細については、並列処理に関するセクションを参照してください。 - ストリームからの要素を処理します。集約操作は、コレクションから直接ではなく、ストリームから要素を処理します。したがって、これらはストリーム操作 (
stream operations
) とも呼ばれます。 - これらはパラメータとしての動作をサポートしています。ほとんどの集計操作のパラメータとしてラムダ式を指定できます。これにより、特定の集計操作の動作をカスタマイズできます。
3.1 誘導
「集計操作」セクションでは、コレクション内のすべての男性メンバーの平均年齢を計算する、次の操作のパイプラインについて説明します。roster
double average = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
JDK には、ストリームの内容を結合して値を返す多数の最終操作 ( average、sum、min、max、countなど) が含まれています。これらの演算は帰納的演算 ( reduction operations
) と呼ばれます。JDK には、単一の値の代わりにコレクションを返す帰納的操作も含まれています。多くの帰納的操作は、値の平均を求めたり、要素をカテゴリにグループ化したりするなど、特定のタスクを実行します。ただし、JDK では、このセクションで詳細に説明する一般的なリダクション操作 redundantおよびcollectが提供されます。
このセクションでは次のトピックについて説明します。
- Stream.reduceメソッド_
- Stream.collectメソッド_
このセクションで説明されているコードの抜粋は、Example ReductionExamplesにあります。
Stream.reduce 方法
このStream.reduceメソッドは一般的なリダクション操作です。roster
コレクション内の男性メンバーの年齢の合計を計算する次のパイプラインについて考えてみましょう。Stream.sum
帰納的演算を使用します。
Integer totalAge = roster
.stream()
.mapToInt(Person::getAge)
.sum();
これをStream
以下で使用するパイプラインと比較してください。同じ値を計算する操作を減らします。
Integer totalAgeReduce = roster
.stream()
.map(Person::getAge)
.reduce(
0,
(a, b) -> a + b);
この例のアクションはreduce
2 つのパラメーターを受け入れます。
- identity :identity 要素は、誘導の初期値であると同時に、ストリームに要素がない場合のデフォルトの結果でもあります。この例では、unit 要素は です。
0
これは年齢の合計の初期値、roster
またはセット内にメンバーが存在しない場合のデフォルト値です。 - accumulator :
accumulator
この関数は 2 つの引数を受け入れます。帰納法の部分的な結果 (この場合は、これまでに処理されたすべての整数の合計) と、ストリームの次の要素 (この場合は整数) です。新しい部分的な結果を返します。この例では、アキュムレータ関数は 2 つの整数値を加算し、整数値を返すラムダ式です。(a, b) -> a + b
reduce
操作は常に新しい値を返します。ただし、この関数は、ストリーム内の要素が処理されるたびにaccumulator
新しい値も返します。ストリームの要素をコレクションなどのより複雑なオブジェクトに一般化したいとします。これはアプリケーションのパフォーマンスに影響を与える可能性があります。操作にコレクションへの要素の追加が含まれる場合、関数が要素を処理するreduce
たびに、その要素を含む新しいコレクションが作成されるため、非効率的です。accumulator
既存のコレクションを更新する方が効率的です。これは、次のセクションで説明するStream.collectメソッドを使用して実行できます。
Stream.collect 方法
reduce
要素の処理時に常に新しい値を作成するメソッドとは異なり、collectメソッドは既存の値を変更または変更します。
ストリーム内の値の平均を見つける方法を考えてみましょう。値の合計数とそれらの値の合計という 2 つのデータが必要です。ただし、reduce
メソッドや他のすべての単純化されたメソッドと同様、collect
メソッドは 1 つの値のみを返します。以下のクラスAveragerなど、値の総数とそれらの値の合計を追跡するメンバー変数を使用して新しいデータ型を作成できます。
class Averager implements IntConsumer
{
private int total = 0;
private int count = 0;
public double average() {
return count > 0 ? ((double) total)/count : 0;
}
public void accept(int i) {
total += i; count++; }
public void combine(Averager other) {
total += other.total;
count += other.count;
}
}
次のパイプラインは、Averager
クラスとcollect
メソッドを使用して、すべての男性メンバーの平均年齢を計算します。
Averager averageCollect = roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(Person::getAge)
.collect(Averager::new, Averager::accept, Averager::combine);
System.out.println("Average age of male members: " +
averageCollect.average());
この例のアクションはcollect
3 つのパラメータを受け入れます。
- supplier :
supplier
はファクトリ関数であり、新しいインスタンスを構築します。操作の場合はcollect
、結果コンテナーのインスタンスを作成します。この場合、それはAverager
クラスの新しいインスタンスです。 - accumulator :
accumulator
ストリーム要素を結果コンテナーにマージする関数。この場合、count
変数を追加し1
、男性メンバーの年齢を表す整数であるストリーム要素の値をメンバー変数に追加することによって、結果コンテナーtotal
を変更します。Averager
- combiner :
combiner
この関数は 2 つの結果コンテナーを受け取り、その内容を結合します。この場合、count
変数を別のAverager
インスタンスのメンバー変数に追加し、他のインスタンスのメンバー変数count
の値をメンバー変数に追加することによって、結果コンテナーを変更します。Averager
total
total
Averager
次の点に注意してください。
supplie
はラムダ式 (またはメソッド参照) であり、reduce
アクション内の ID 要素のような値ではありません。- accumulator(
accumulator
) および combiner(combiner
) 関数は値を返しません。 - 並列ストリームで操作を使用できます
collect
。詳細については、並列処理に関するセクションを参照してください。(並列ストリームで収集メソッドを実行する場合、combiner
この例のような関数が新しいオブジェクトを作成するたびに、JDK は新しいスレッドを作成します。Averager
そのため、同期について心配する必要はありません。)
collect
操作はコレクションで最も効果的に機能します。次の例では、collect
操作を使用して男性メンバーの名前をコレクションに追加します。
List<String> namesOfMaleMembersCollect = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());
このバージョンのcollect
操作はCollector
型パラメータを受け入れます。このクラスは、collect
3 つのパラメーター (サプライヤー関数、アキュムレーター関数、結合関数) を必要とする操作でパラメーターとして使用される関数をカプセル化します。
コレクションクラスには、要素をコレクションに蓄積したり、さまざまな基準に基づいて要素を要約したりするなど、多くの便利な帰納的操作が含まれています。これらの帰納的操作はクラスのインスタンスを返すCollector
ため、それらを操作の引数として使用できますcollect
。
この例では、 Collectors.toListオペレーションを使用し、ストリーム要素を のList
新しいインスタンスに蓄積します。Collectors
クラスに対するほとんどの操作と同様、toList
演算子はCollector
コレクションではなくインスタンスを返します。
コレクションのメンバーを性別ごとにroster
グループ化する例を次に示します。
Map<Person.Sex, List<Person>> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));
groupingBy
操作は、引数として指定されたラムダ式 (分類関数と呼ばれる) を適用したclassification function
結果の値をキーとするマップを返します。この例では、返されたマップには 2 つのキーPerson.Sex.MALE
と が含まれていますPerson.Sex.FEMALE
。キーに対応する値は、List
分類関数によって処理されるときに、キー値に対応するストリーム要素を含むインスタンスです。たとえば、Person.Sex.MALE
キーの値は、List
すべての男性メンバーを含むインスタンスです。
次の例では、roster
コレクション内の各メンバーの名前を取得し、性別ごとにグループ化します。
Map<Person.Sex, List<String>> namesByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
この例のgroupingByオペレーションは、分類関数とインスタンスという 2 つの引数を受け取りますCollector
。Collector
このパラメータはダウンストリーム コレクタ ( downstream collector
) と呼ばれます。これは、あるコレクターが Java ランタイムによって別のコレクターに適用された結果です。したがって、このgroupingBy
操作により、groupingBy
演算子によって作成されたList
値にメソッドを適用できるようになりますcollect
。この例では、ストリームの各要素にマッピング関数を適用するコレクターマッピングを適用します。Person::getName
したがって、結果のストリームはメンバーの名前のみで構成されます。1 つ以上の下流コレクタを含むパイプライン (この例のような) は、マルチレベル誘導 ( multilevel reduction
) と呼ばれます。
次の例では、各性別のメンバーの合計年齢を取得します。
Map<Person.Sex, Integer> totalAgeByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.reducing(
0,
Person::getAge,
Integer::sum)));
リダクション操作では、次の 3 つのパラメータを受け取ります。
- identity :
Stream.reduce
操作と同様に、identity 要素は誘導の初期値であり、ストリームに要素がない場合のデフォルトの結果でもあります。この例では、unit 要素は 0 です。これは年齢の合計の初期値、またはメンバーが存在しない場合のデフォルト値です。 - mapper :
reducing
この操作は、このマッパー関数をすべてのストリーム要素に適用します。この例では、マッパーは各メンバーの年齢を取得します。 - Operation :
operation
マップされた値を一般化するために使用される関数。この例では、アクション関数がInteger
値を追加します。
次の例では、性別ごとのメンバーの平均年齢を取得します。
Map<Person.Sex, Double> averageAgeByGender = roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.averagingInt(Person::getAge)));
3.2 並列化
并行计算
これは、問題をサブ問題に分割し、それらを同時に (各サブ問題を別のスレッドで実行して並行して) 解決し、サブ問題の解決策を結合することで構成されます。Java SE は、アプリケーションに並列コンピューティングをより簡単に実装できるフォーク/結合フレームワークを提供します。ただし、このフレームワークでは、問題をどのように細分化するか (パーティション) を指定する必要があります。集約操作を使用すると、Java ランタイムはそのようなソリューションの分割と結合を実行します。
コレクションを使用するアプリケーションで並列処理を実現する際の難点の 1 つは、コレクションがスレッドセーフではないことです。つまり、複数のスレッドはスレッド干渉やメモリ整合性エラーを引き起こすことなくコレクションを操作できません。コレクション フレームワークは、任意のコレクションに自動同期を追加してスレッド セーフにする同期ラッパーを提供します。ただし、同期によりスレッドの競合が発生します。スレッドの並列実行が妨げられるため、スレッドの競合を回避する必要があります。集計操作と並列ストリームを使用すると、操作中にコレクションが変更されない限り、非スレッドセーフなコレクションで並列処理を実現できます。
十分なデータとプロセッサ コアがある場合は、操作を並列で実行するほうが高速になる可能性がありますが、操作を並列で実行する方が必ずしも直列で実行するより速いとは限らないことに注意してください。集約操作を使用すると並列処理をより簡単に実現できますが、アプリケーションが並列処理に適しているかどうかを判断するのは依然としてユーザーの責任です。
このセクションでは次のトピックについて説明します。
- ストリームを並列実行する
- 同時誘導
- 分類する
- 副作用:
- 怠惰
- 干渉
- ステートフルラムダ式
このセクションで説明されているコードの抜粋は、ParallelismExamplesの例にあります。
3.2.1 ストリームの並列実行
ストリームはシリアルまたはパラレルに実行できます。ストリームが並列実行されると、Java ランタイムはストリームを複数のサブフローに分割します。集計操作は、これらのサブストリームを横断して並列処理し、結果を結合します。
ストリームを作成する場合、特に指定しない限り、ストリームは常にシリアル ストリームになります。並列ストリームを作成するには、Collection.ParallelStreamオペレーションを呼び出します。あるいは、BaseStream.Parallelオペレーションを呼び出します。たとえば、次のステートメントは、すべての男性メンバーの平均年齢を並行して計算します。
double average = roster
.parallelStream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
3.2.2 同時誘導
メンバーを性別別にグループ化する次の例をもう一度考えてみましょう ( 「削減」セクションで説明)。次の例では、収集するcollect
アクションを呼び出します。roster
Map
Map<Person.Sex, List<Person>> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));
同時に同等のものは次のとおりです。
ConcurrentMap<Person.Sex, List<Person>> byGender =
roster
.parallelStream()
.collect(
Collectors.groupingByConcurrent(Person::getGender));
これは同時誘導( )として知られていますconcurrent reduction
。操作を含む特定のパイプラインについてcollect
、次の条件がすべて満たされる場合、Java ランタイムは同時誘導を実行します。
- ストリームは平行です。
collect
操作のパラメータ には、collector
特性Collector.Characteristics.CONCURRENTがあります。コレクターの特性を判断するには、Collector.characteristics
メソッドを呼び出します。- ストリームが順序付けされていない、またはコレクターにCollector.Characteristics.UNORDERED特性があるかのいずれかです。ストリームが順序付けされていないことを確認するには、BaseStream.unownedオペレーションを呼び出します。
注: この例では、 Map の代わりにConcurrentMapのインスタンスを返し、代わりにgroupingByConcurrentgroupingBy
オペレーションを呼び出します。( ConcurrentMap の詳細については、 「同時コレクション」のセクションを参照してください。)groupingByConcurrent
操作とは異なり、操作はgroupingBy
並列ストリームではパフォーマンスが低下します。(これは、キーごとに 2 つのマップをマージすることで動作し、計算コストがかかるためです。)同様に、並列ストリームを処理する場合、操作Collectors.toConcurrentMapは操作Collectors.toMapよりもパフォーマンスが高くなります。
3.2.3 並べ替え
パイプラインがストリーム要素を処理する順序は、ストリームがシリアルに実行されるか並列に実行されるか、ストリームのソース、および中間操作によって異なります。たとえば、forEach
演算子を使用してArrayList
インスタンスの要素を複数回出力する次の例を考えてみましょう。
Integer[] intArray = {
1, 2, 3, 4, 5, 6, 7, 8 };
List<Integer> listOfIntegers =
new ArrayList<>(Arrays.asList(intArray));
System.out.println("listOfIntegers:");
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("listOfIntegers sorted in reverse order:");
Comparator<Integer> normal = Integer::compare;
Comparator<Integer> reversed = normal.reversed();
Collections.sort(listOfIntegers, reversed);
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Parallel stream");
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Another parallel stream:");
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("With forEachOrdered:");
listOfIntegers
.parallelStream()
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
この例は 5 つのパイプラインで構成されています。次のようなものが出力されます。
listOfIntegers:
1 2 3 4 5 6 7 8
listOfIntegers sorted in reverse order:
8 7 6 5 4 3 2 1
Parallel stream:
3 4 1 6 2 5 7 8
Another parallel stream:
6 3 1 5 7 8 4 2
With forEachOrdered:
8 7 6 5 4 3 2 1
この例では次のことを行います。
- 最初のパイプは、
listOfIntegers
リストの要素をリストに追加された順序で出力します。 - 2 番目のパイプラインは、Collections.sortメソッドによって
listOfIntegers
要素が並べ替えられた後に要素を出力します。 - 3 番目と 4 番目のパイプは、リストの要素を明らかにランダムな順序で出力します。ストリーム操作では、ストリームの要素を処理するときに内部反復が使用されることに注意してください。したがって、ストリームが並列実行される場合、ストリーム操作で別途指定されていない限り、Java コンパイラとランタイムは、並列コンピューティングの利点を最大化するためにストリーム要素が処理される順序を決定します。
- 5 番目のパイプラインは、 forEachOrderedメソッドを使用します。このメソッドは、ストリームをシリアルに実行するか並列に実行するかに関係なく、ソースによって指定された順序でストリームの要素を処理します。このような操作を並列ストリームで使用すると、並列処理
forEachOrdered
の利点が失われる可能性があることに注意してください。
3.2.4 副作用
メソッドまたは式には、値を返したり生成したりするだけでなく、計算の状態を変更する場合にも副作用があります。例には、可変リダクション (collect
操作を使用する操作、詳細についてはリダクションに関するセクションを参照)、およびSystem.out.println
デバッグ用のメソッドの呼び出しが含まれます。JDK は、パイプライン内のいくつかの副作用をうまく処理します。特に、collect
メソッドは、最も一般的な副作用のあるストリーム操作を並列安全な方法で実行するように設計されています。やforEach
などの操作はpeek
副作用、つまりSystem.out.println
呼び出し式など、void を返すラムダ式を考慮して設計されています。副作用以外何もできません。