Modern Java in Action #6


ストリームからのデータ収集


Collection:Javaが提供するデータ構造
Collector:フロー要素を抽出する方法を定義したインタフェース
collect:ストリームの最終演算の1つ(収集して返す方法)

コレクターとは?


以前の例では、toList()のようにコレクタインタフェースの実装体である.このようなCollectorインタフェースのインプリメンテーションは、ストリーム内の要素をエクスポートする方法を指定します.
Java 8以前のコマンドコードでは、パケットなどのデータ収集に複数のループや条件文を追加すると可読性や保守性が低下するが、宣言プログラミングでは必要なコレクタを追加することで可読性や保守性を向上させることができる.
// 통화별로 트랜잭션을 그룹화하는 코드

//Java8 이전의 코드
Map<Currency, List<Treansaction>> transactionsByCurrencies = new HashMap<>();

for(Transaction transaction : transactions) {
	Currency currency = transaction.getCurrency();
    
    List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
    
    if(transactionsForCurrency == null) {
    	transactionsForCurrency = new ArrayList<>();
        transactionsByCurrencies.put(currency, transactionsForCurrency);
        
        transactionsForCurrency.add(transaction);
    }
}

//Java8 이후의 코드
Map<Currency, List<Transaction>> transactionsByCurrencies = 
	transactions.stream().collect(groupingBy(Transaction::getCurrency));

高度な再生機能を備えたコレクタ


ストリームにcollectメソッドを呼び出すと、コレクタはタスクを処理し、パラメータ化された再生操作をコレクタ対流の要素として実行して、ストリームの各要素にアクセスします.
//toList는 스트림의 모든 요소를 리스트로 수집한다.
List<Transaction> transactions = transactionStream.collect(Collectors.toList());

事前定義コレクタ


Java 8ではjavautil.stream.Collectorsは工場の方法を提供します.提供する方法には、次の3つの機能があります.

  • ストリーム要素を値に再分割して要約する
  • 各種計算の実行

  • 要素グループ
  • マルチスタンダードパケット

  • 要素の分割
    -グループの特殊な演算(グループ関数としてプリセット値を使用)
  • 再設計とまとめ


    前述したように、Collectorストリーム内のアイテムをコレクションに再編成できます.再構築可能なフォーマットは、マルチレベルマップや単純な整数など、さまざまなフォーマットでエクスポートできます.
    //아래 코드는 메뉴에서 요리 개수를 반환합니다.
    long howManyDishes = menu.stream().collect(Collectors.counting());
    long howManyDishes = menu.stream().count();
    
    
    //아래 코드는 최댓값을 반환합니다
    Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
    Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
    
    
    //아래 코드는 메뉴 리스트의 총 칼로리를 반환합니다.
    int totalCalories = menu.stream().collect(summingInt(Dist::getCalories));
    
    
    //아래 코드는 평균 칼로리를 반환합니다
    double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
    
    
    //joing 팩토리 메서드를 이용해 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환할 수 있습니다.
    String shortMenu = menu.stream().map(Dish::getName).collect(joining());
    
    
    上記のすべてのコレクタは、工場を削減する方法として定義することもできる.それでも、私たちがそうしないのは、プログラミングの利便性が毒性を高めるからです.
    //아래 코드는 위의 칼로리 합계를 계산하는 코드를 reducing으로 변경한 코드입니다.
    int totalCalories = menu.stream().collect(reducing(0, Dish:getCalories, (i, j) -> i + j));
    

    StreamインタフェースのCollectとReduce


    collect法はコンテナを変更して結果を累積する方法であり,reduce法は不変式の演算であり,意味的には2つの値を1つの値に連結する.
    パラレル性と可変コンテナが関連していることを確認するには、collectメソッドを使用して再生操作を行うことをお勧めします.

    グループ化


    Java 8の関数形式では、1行の読み取り可能なコードを使用して量子化できます.
    Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
    グループ化Byメソッドに渡されるDish::getTypeに従ってグループ化されるので、分類関数と呼ばれる.
    次のコードでは、メソッドの順序を変更するだけで結果値の違いがわかります.collectメソッドを使用して、既存のすべてのグループマッピングキーを保持することをお勧めします.
    // collect를 먼저 호출한 경우
    menu.stream()
        .collect(groupingBy(
            Dish::getType,
            filtering(dish → dish.getCalories() > 500, toList()
        )));
    // 실행 결과: {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
     
     
    // 필터링을 먼저한 경우
    menu.stream()
        .filter(dish → dish.getCaloires() > 500)
        .collect(groupingBy(Dish::getType)); 
    // 실행 결과: {OTHER=[french fries, pizza], MEAT=[pork, beef]}  ( FISH 라는 키는 볼 수 없다.)
    次の点がわかります.
    パケットByコレクタは、ストリーム内の最初の要素が検出されると、不活性な方法でパケットマッピングに新しいキーを追加します.

    マルチスタンダードグループ


    groupingByをgroupingByのパラメータとして再伝達することで、複数のグループをグループ化することができる.
    menu.stream()
        .collect(groupingBy(
            Dish::getType, // 타입으로 1차 분류
            groupingBy(dish -> {
                if (dish.getCalories() <= 400) // 그 안에서 칼로리로 2차 분류
                    return CaloricLevel.DIET;
                else if (dish.getCalories() <= 700)
                    return CaloricLevel.NORMAL;
                else
                    return CaloricLevel.FAT;
            })
        ));
     
    // 실행 결과: {MEAT={DIET=[chicken], NORMAL=[beef], FAT=[prok]},
    //           FISH={DIET=[prawns], NORMAL=[salmon]},
    //           OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}

    コレクタ別に追加


    Collectors.CollectingAndThenを使用すると、コレクタから返された結果に対して他の操作を実行できます.
    menu.stream()
        .collect(groupingBy(
            Dish::getType, // 1. 일단 먼저 type 기준으로 서브스트림 생성 -> 각 요리 타입에 따라 그룹을 나눔. 각 그룹의 요리를 담은 스트림 생성
            collectingAndThen( // 2. 각 서브스트림(그룹)에 대해 아래 내용 실행
                maxBy(comparingInt(Dish::getCalories)), // 3. 칼로리를 구해서 칼로리 최댓값을 가진 음식을 찾고
                Optional::get // 4. Optional<Dish>로 리턴했던 결과를 그냥 Dish로 바꿈.
            )
        )
    );

    分割ぶんかつ


    分割は、分割関数と呼ばれるプリセットを分類関数として使用する特殊なパケット機能です.分類関数としてPredicateを使用するため、マッピングされたキーフォーマットはBooleanです.
    すなわち、「パケットマッピング」は、最大2つのグループに分けられ、値は「真」と「偽」です.
    filterメソッドの利点は、「真」要素のみが存在し、分割メソッド「パーティションBy」メソッドは、すべての「真」要素と「偽」要素を保持することである.
    Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
        .collect(partitioningBy(Dish::isVegetarian));
     
    // 결과
    { false=[pork, beef, chicken, prawns, salmon],
      true=[french fries, rice, season fruit, pizza]}
    分割後に、次のように分類することもできます.
    menu.stream()
        .collect(partitioningBy(
            Dish::isVegetarian, // 채식인지 아닌지로 한번 거르고
            groupingBy(Dish::getType))); // 그 안에서 타입별로 그룹화
     
    // 결과
    { false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]},
      true={OTHER=[french fries, rice, season fruit, pizza]}}

    Collectorインタフェース


    次のCollectorインタフェースを使用して、リプレイ操作を直接作成できます.
    /*
    T: 스트림 안에 들어있는 데이터 하나하나의 타입
    A: 수집 과정에서 중간 결과를 누적하는 객체 타입
    R: 수집 연산을 끝낸, 결과 객체의 타입
    */
    
    public interface Collector<T, A, R> {
        Supplier<A> supplier();
        BiConsumer<A, T> accumulator();
        Function<A, R> finisher();
        BinaryOperator<A> combiner();
        Set<Characteristics> characteristics();
    }