Java8 In Action

[Part2] Java8 In action - Chapter5 - 2

신나게개발썰 2022. 7. 29. 13:35
반응형

해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.

좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!

5.3 검색과 매칭

Entity

Dish.java

package Part2.Chapter5.Chapter5_5_3.entity;

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {    
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return this.name;
    }

    public int getCalories() {
        return this.calories;
    }

    public Type getType() {
        return this.type;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public enum Type {
        MEAT, FISH, OTHER
    }
}

Main

package Part2.Chapter5.Chapter5_5_3;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import Part2.Chapter5.Chapter5_5_1.entity.Dish;

/*
 * 5.3 검색과 매칭
 * 
 * 특정 속성이 데이터 집하에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.
 * 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.
 */
public class Main_5_3 {

    public static void main(String[] args) {
        List<Dish> menu = Arrays.asList(
            new Dish("pork", false, 800, Dish.Type.MEAT)    
            , new Dish("beef", false, 700, Dish.Type.MEAT)                
            , new Dish("chicken", false, 400, Dish.Type.MEAT)
            , new Dish("french fries", true, 530, Dish.Type.OTHER)
            , new Dish("rice", true, 350, Dish.Type.OTHER)
            , new Dish("season fruit", true, 120, Dish.Type.OTHER)
            , new Dish("pizza", true, 550, Dish.Type.OTHER)
            , new Dish("prawns", false, 300, Dish.Type.FISH)
            , new Dish("salmon", false, 450, Dish.Type.FISH));

        /*
         * 5.3.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
         * 
         * 프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때
         * anyMatch 메서드를 이용한다.
         * 다음 코드는 menu에 채식요리가 있는지 확인하는 예제이다. 
         */
        if(menu.stream().anyMatch(Dish::isVegetarian)) {
            System.out.println("The menu is (somewhat) vegetarian friendly!!");
        }

        /*
         * 5.3.2 프레디케이트가 모든 요소와 일치하는지 검사
         * 
         * allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지
         * 검사한다.
         * 다음 코드는 건강식(모든 요리가 1000칼로리 이하면 건강식으로 간주)인지 확인하는 코드다.
         */
        if(menu.stream().allMatch(d -> d.getCalories() < 1000)) {
            System.out.println("모든 식단이 건강식단 입니다!");
        }

        /*
         * noneMatch
         * noneMatch는 allMatch와 반대 연산을 수행한다.
         * 즉, noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다. 
         */
        if(menu.stream().noneMatch(d -> d.getCalories() >= 1000)) {
            System.out.println("건강 식단이라서 먹을게 없네요.");
        }        
        /*
         * anyMatch, allMatch, noneMatch 세 가지 메서드는 쇼트서킷 기법.
         * 즉, 자바의 &&, ||와 같은 연산을 활용한다.
         */

        /*
         * 쇼트서킷 평가
         * 때로는 전체 스트림을 처리하지 않았더라도 결과를 반환할 수 있다.
         * 예를 들어 여러 and 연산으로 연결된 커다란 불린 표현식을 평가한다고 가정하자.
         * 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이
         * 전체 결과도 거짓이 된다.
         * 이러한 상황을 "쇼트서킷"이라고 한다.
         * 
         * allMatch, noneMatch, findFirst, findAny 등의 연산은 모든 스트림의 요소를 처리하지
         * 않고도 결과를 반환할 수 있다. 
         * 이는 원하는 요소를 찾으면 즉시 결과를 반환할 수 있기 때문이다.
         * 마찬가지로 스트림의 모든 요소를 처리할 필요 없이 주어진 크기의 스트림을 생성하는
         * limit도 쇼트서킷 연산이다.
         */

        /*
         * 5.3.3 요소 검색
         * 
         * findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.
         * findAny 메서드를 다른 스트림 연산과 연결해서 사용할 수 있다.
         * 다음 코드는 filter와 findAny를 이용해서 채식요리를 선택한다.
         * 
         * 스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다.
         * 즉, 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다.
         */
        Optional<Dish> dish = menu.stream()
            .filter(Dish::isVegetarian)
            .findAny();

        /*
         * Optional이란?
         * Optional<T>클래스(java.util.Optional)는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다.
         * 이전 예제에서 findAny 메서드는 아무 요소도 반환하지 않을 수 있다.
         * null은 쉽게 에러를 일으킬 수 있으므로 자바 8 라이브러리 설계자는 Optional<T>라는 기능을 만들었다.
         *        - isPresend()는 Optional이 값을 포함하면 참(true)을 반환하고, 값을 포함하지 않으면
         *         거짓(false)을 반환한다.
         * 
         *      - ifPresend(Consumer<T> block)은 값이 있으면 주어진 블럭을 실행한다.
         *      Consumer 함수형 인터페이스에선 T형식의 인수를 받으며 void를 반환하는 람다를 전달할 수 있다.
         *  
         *      - T get()은 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킨다.
         *  
         *      - T orElse(T other)는 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환한다.     
         */

        // 아래 코드 Optional<Dish>에서는 요리명이 null인지 검사할 필요가 없다.
        dish.ifPresent(d -> System.out.println("채식 요리를 선택한 사람 : " + d.getName() ));

        /*
         * 5.3.4 첫 번째 요소 찾기
         * 
         * 리스트 또는 정렬된 데이터로부터 생성된 스트림은 논리적인 아이템 순서가 정해져 있을 수 있다.
         * 이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야 할까?
         * 아래 코드는 숫자 리스트에서 3으로 나누어떨어지는 첫 번째 제곱값을 구하는 예이다.
         */
        System.out.println("5.3.4 첫 번째 요소 찾기 : " + Arrays.asList(1, 2, 3, 4, 5).stream()
                .map(x -> x * x)
                .filter(x -> x % 3 == 0)
                .findFirst()
                .get() );

        /*
         * findfirst와 findAny는 언제 사용하나?
         * 두 가지 메서드가 필요한 이유는 바로 병렬성 때문이다.
         * 병렬실행에서 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면
         * 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
         */
    }
}

5.4 리듀싱

Entity

Dish.java

package Part2.Chapter5.Chapter5_5_4.entity;

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {    
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return this.name;
    }

    public int getCalories() {
        return this.calories;
    }

    public Type getType() {
        return this.type;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public enum Type {
        MEAT, FISH, OTHER
    }
}

Main

package Part2.Chapter5.Chapter5_5_4;

import java.util.Arrays;
import java.util.List;

/*
 * 5.4 리듀싱
 * 
 * 스트림의 모든 요소를 반복적으로 처리하는 행위에 대한 질의를 리듀싱 연산이라고 한다.
 * (모든 스트림 요소를 처리해서 값으로 도출하는)
 * 
 * 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이(우리는 스트림)를 작은 조각이
 * 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 부른다.
 */
public class Main_5_4 {

    static public void main(String[] args) {
        List<Integer> numbers = Arrays.asList(4, 5, 3, 9);

        /*
         * 5.4.1 요소의 합
         * 
         * reduce 메서드를 보기 전에 for-each를 이용해서 리스트의 숫자 요소를 더하는 코드를 보자.
         *         int num = 0;
         *         for(int x : numbers) {
         *             sum += x;
         *         }
         * numbers의 각 요소는 결과에 반복적으로 더해진다. 리스트에서 하나의 숫자가 남을 때까지
         * 이 과정은 반복한다. 코드에는 두 개의 파라미터가 사용되었다.
         *         - sum 변수의 초기값 0
         *         - 리스트의 모든 요소를 조합하는 연산(+) 
         */

        /*
         * reduce는 두 개의 인수를 갖는다.
         *         - 초기값 0
         *         - 두 요소를 조합해서 새로운 값을 만드는 BinaryOprator<T>
         * 
         * 아래 코드 (a, b) -> a + b 관련해서 reduce 메서드가 처리하는 흐름을 설명 하자면
         * (처음은 reduce 메서드의 첫 번쨰 인자로 받은 값을 더하면서 시작한다.)
         *         1. 0 + 4 = (0, 4) -> 0 + 4;    
         *         2. 4 + 5 = (4, 5) -> 4 + 5
         *         3. 9 + 3 = (9, 3) -> 9 + 3;
         *         4. 12 + 9 = (12, 9) -> 12 + 9;
         * 흐름과 같는다. 즉,  
         *         ("람다 표현식에서 반환된 값", "요소") -> "람다 표현식에 반환된 값" + "요소";
         * 같은 형식으로 인수가 전달되게 된다. 
         */        
        System.out.println("5.4.1 요소의 합 - 람다 표현식 : " + numbers.stream().reduce(0, (a, b) -> a + b));
        System.out.println("5.4.1 요소의 합 - 메서드 레퍼런스 : " + numbers.stream().reduce(0, Integer::sum));

        /*
         * 초기값 없음
         * 
         * 초기값이 없는 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환한다.
         * 스트림에 아무 요소도 없는 상황을 생각해보자. 이런 상황이라면 초기값이 없으므로 reduce는
         * 합계를 반환할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록 Optional 객체로 감싼 결과를 반환한다.
         */
        System.out.println("초기값 없는 reduce 메서드 : " + numbers.stream().reduce((a, b) -> a + b).get());

        /*
         * 5.4.2 최대값과 최소값
         *  
         * reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행한다.
         * 아래는 최대값과 최소값을 구하는 코드들이다.
         */
        // 최대값 구하기
        System.out.println("5.4.2 최대값 - 람다 표현식 : " + numbers.stream().reduce((a, b) -> a < b ? b : a).get());        
        System.out.println("5.4.2 최대값 - 메서드 레퍼런스 : " + numbers.stream().reduce(Integer::max).get());

        // 최소값 구하기
        System.out.println("5.4.2 최소값 - 람다 표현식 : " + numbers.stream().reduce((a, b) -> a < b ? a : b).get());        
        System.out.println("5.4.2 최소값 - 메서드 레퍼런스 : " + numbers.stream().reduce(Integer::min).get());

        /*
         * reduce 메서드의 장점과 병렬화
         * 
         * reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.
         * 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵다.
         * 강제적으로 동기화시킨다 하더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에
         * 상쇄되어 버리기 때문이다. 
         */

        /*
         * 아래 코드는 parallelStream 메서드를 이용해서 병렬처리를 수행한다. 하지만 병렬로 실행하려면 대가를 지불해야 한다.
         * 즉, reduce에 넘겨준 람다의 상태(인스턴스 변수 같은)가 바뀌지 말아야 하며, 연산이 어떤 순서로 실행되더라도
         * 결과가 바뀌지 않는 구조여야 한다.
         */
        System.out.println("reduce 메서드의 장점과 병렬화 : " + numbers.parallelStream().reduce(0, Integer::sum));

        /*
         * 스트림 연산 : 상태 없음과 상태 있음
         * 
         * map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
         * 따라서(사용자가 제공한 람다나 메서드 레퍼런스가 내부적인 가변 상태를 갖지 않는다는 가정 하에)
         * 이들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산(stateless operation)이다.
         * 
         * 하지만 sorted나 distinct는 filter나 map과는 다르다.
         * 스트림의 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 한다.
         * 즉, 어떤 요소를 출력 스트림으로 추가하려면 "모든 요소가 버퍼에 추가되어 있어야 한다."
         * 연산을 수행하는데 필요한 저장소 크기는 정해져있지 않기 때문에 데이터 스트림의 크기가 크거나
         * 무한이라면 문제가 생길수 있다. 따라서 이러한 연산을 내부 상태를 갖는 연산(stateful operation)으로
         * 간주할 수 있다.
         */

        /*
         * 중간 연산과 최종 연산
         * 
         * 연산 : filter
         * 형식 : 중간 연산
         * 반환 형식 : Stream<T>
         * 사용된 함수형 인터페이스 형식 : Predicate<T> 
         * 함수 디스크립터 : T -> boolean
         * 
         * 연산 : distinct
         * 형식 : 중간 연산(상태 있는 언바운드)
         * 반환 형식 : Stream<T>
         * 사용된 함수형 인터페이스 형식 :  
         * 함수 디스크립터 :
         * 
         * 연산 : skip
         * 형식 : 중간 연산(상태 있는 바운드)
         * 반환 형식 : Stream<T> 
         * 사용된 함수형 인터페이스 형식 : Long 
         * 함수 디스크립터 : 
         * 
         * 연산 : limit
         * 형식 : 중간 연산(상태 있는 바운드)
         * 반환 형식 : Stream<T>
         * 사용된 함수형 인터페이스 형식 : Long 
         * 함수 디스크립터 : 
         * 
         * 연산 : map
         * 형식 : 중간 연산
         * 반환 형식 : Stream<R>
         * 사용된 함수형 인터페이스 형식 : Function<T, R> 
         * 함수 디스크립터 : T -> R
         * 
         * 연산 : flatMap
         * 형식 : 중간 연산
         * 반환 형식 : Stream<R>
         * 사용된 함수형 인터페이스 형식 : Function<T, Stream<R>> 
         * 함수 디스크립터 : T -> Stream<R>
         * 
         * 연산 : sorted
         * 형식 : 중간 연산(상태 있는 언바운드)
         * 반환 형식 : Stream<T> 
         * 사용된 함수형 인터페이스 형식 : Comparator<T> 
         * 함수 디스크립터 : (T, T) -> int
         * 
         * 연산 : anyMatch
         * 형식 : 최종 연산
         * 반환 형식 : boolean
         * 사용된 함수형 인터페이스 형식 : Predicate<T> 
         * 함수 디스크립터 : T -> boolean
         * 
         * 연산 : noneMath
         * 형식 : 최종 연산
         * 반환 형식 : boolean
         * 사용된 함수형 인터페이스 형식 : Predicate<T> 
         * 함수 디스크립터 : T -> boolean
         * 
         * 연산 : allMatch
         * 형식 : 최종 연산
         * 반환 형식 : boolean
         * 사용된 함수형 인터페이스 형식 : Predicate<T> 
         * 함수 디스크립터 : T ->boolean
         * 
         * 연산 : findAny
         * 형식 : 최종 연산
         * 반환 형식 : Optional<T> 
         * 사용된 함수형 인터페이스 형식 : 
         * 함수 디스크립터 : 
         * 
         * 연산 : findFirst
         * 형식 : 최종 연산
         * 반환 형식 : Optional<T>
         * 사용된 함수형 인터페이스 형식 : 
         * 함수 디스크립터 : 
         * 
         * 연산 : forEach
         * 형식 : 최종 연산
         * 반환 형식 : void
         * 사용된 함수형 인터페이스 형식 : Consumer<T> 
         * 함수 디스크립터 : T -> void
         * 
         * 연산 : collect
         * 형식 : 최종 연산
         * 반환 형식 : R
         * 사용된 함수형 인터페이스 형식 : Collector<T, A, R> 
         * 함수 디스크립터 : 
         * 
         * 연산 : reduce
         * 형식 : 최종 연산(상태 있는 바운드)
         * 반환 형식 : Optional<T>
         * 사용된 함수형 인터페이스 형식 : BinaryOperator<T> 
         * 함수 디스크립터 : (T, T) -> T
         * 
         * 연산 : count
         * 형식 : 최종 연산
         * 반환 형식 : long
         * 사용된 함수형 인터페이스 형식 : 
         * 함수 디스크립터 : 
         */
    }

}

스트림 활용

  • 데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다.
  • 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다.
  • 내부 반복뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 이러한 일은 순차적인 반복을 단일 스레드로 구현했기 때문에 외부 반복으로는 불가능하다.

요약

  • 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
  • filter, distinct, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
  • map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
  • findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다. allMatch, noneMatch, anyMatch 메서드를 이용해서 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색할 수 있다.
  • 이들 메서드는 쇼트서킷(short-circuit), 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
  • reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다. 예를 들어 reduce로 스트림의 최대값이나 모든 요소의 합계를 계산할 수 있다.
  • filter, map 등은 상태를 저장하지 않는 "상태 없는 연산(stateless operation)"이다 reduce 같은 연산은 값을 계산하는 데 필요한 상태를 저장한다. sorted, distinct 등의 메서드는 새로운 스트림을 반환하기 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 "상태 있는 연산(stateful operation)"이라고 부른다.
  • IntStream, DoubleStream, LongStream은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
  • 컬렉션뿐 아니라, 값, 배열, 파일, iterate와 generate 같은 메서드로도 스트림을 만들 수 있다.
  • 크기가 정해지지 않는 스트림을 무한 스트림이라고 한다.
반응형