高度な Java -- コレクション (2)

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オブジェクトegenderフィールドのPerson.Sex.MALE値が の場合、ブール値を返しますtrueしたがって、この例のフィルター操作は、rosterコレクションのすべての男性メンバーを含むストリームを返します。
  • 最終操作 ( terminal operation)最後の操作 (例: ) は、プリミティブ値 (double など)、コレクション、または値がまったくない場合など、forEach非ストリーミング結果を生成しますforEachこの場合、 forEach オペレーションの引数は、オブジェクトのメソッドe -> System.out.println(e.getName())を呼び出すラムダ式です(Java ランタイムとコンパイラはオブジェクトの型を推論します)。egetNameePerson

次の例では、集計操作 と から構成されるパイプラインを使用してfilterセット フラワー デッキのすべての男性メンバーの平均年齢を計算しますmapToIntaverage

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メソッドは をスローしますNoSuchElementExceptionaverageJDKには、ストリームの内容を結合して値を返すなど、多くの最終操作が含まれています。これらの操作はリダクション操作と呼ばれます。詳細については、「リダクション」セクションを参照してください。

集計演算とイテレータの違い

このような集計操作は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 には、ストリームの内容を結合して値を返す多数の最終操作 ( averagesumminmaxcountなど) が含まれています。これらの演算は帰納的演算 ( reduction operations) と呼ばれます。JDK には、単一の値の代わりにコレクションを返す帰納的操作も含まれています多くの帰納的操作は、値の平均を求めたり、要素をカテゴリにグループ化したりするなど、特定のタスクを実行しますただし、JDK では、このセクションで詳細に説明する一般的なリダクション操作 redundantおよび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);

この例のアクションはreduce2 つのパラメーターを受け入れます。

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

この例のアクションはcollect3 つのパラメータを受け入れます。

  • supplier :supplierはファクトリ関数であり、新しいインスタンスを構築します。操作の場合はcollect 、結果コンテナーのインスタンスを作成します。この場合、それはAveragerクラスの新しいインスタンスです。
  • accumulator :accumulatorストリーム要素を結果コンテナーにマージする関数。この場合、count変数を追加し1、男性メンバーの年齢を表す整数であるストリーム要素の値をメンバー変数に追加することによって、結果コンテナーtotalを変更します。Averager
  • combiner :combinerこの関数は 2 つの結果コンテナーを受け取り、その内容を結合します。この場合、count変数を別のAveragerインスタンスのメンバー変数に追加し、他のインスタンスのメンバー変数countの値をメンバー変数に追加することによって、結果コンテナーを変更しますAveragertotaltotalAverager

次の点に注意してください。

  • 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型パラメータを受け入れます。このクラスは、collect3 つのパラメーター (サプライヤー関数、アキュムレーター関数、結合関数) を必要とする操作でパラメーターとして使用される関数をカプセル化します。

コレクションクラスには、要素をコレクションに蓄積したり、さまざまな基準に基づいて要素を要約したりするなど、多くの便利な帰納的操作が含まれています。これらの帰納的操作はクラスのインスタンスを返す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 つの引数を受け取りますCollectorCollectorこのパラメータはダウンストリーム コレクタ ( 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アクションを呼び出しますrosterMap

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 を返すラムダ式を考慮して設計されています。副作用以外何もできません。

おすすめ

転載: blog.csdn.net/chinusyan/article/details/130948879