ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • CHAP5. 스트림 활용
    book/모던 자바 인 액션 2020. 8. 23. 18:41

    5.1 필터링

    5.1.1 프레디케이트로 필터링

    스트림 인터페이스는 filter 메서드를 지원한다.

    filter 메서드는 Predicate(boolean을 반환하는 함수)를 인수로 받아서 Predicate와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

    List<Dish> vegetarianMenu = menu.stream()
                                    .filter(Dish::isVegetarian)
                                    .collect(toList());

    위와 같이 채식 요리인지 확인하는 메서드 참조를 filter 메서드에서 사용할 수 있다.

     

    5.1.2 고유 요소 필터링

    스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다.

    고유의 여부는 hashCode와 equals에 의해 결정된다.

    List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
    numbers.stream()
           .filter(i -> i % 2 == 0) // 모든 짝수를 선택하고
           .distinct() // 중복을 제거
           .forEach(System.out::println);

    위는 리스트의 모든 짝수를 선택하고 중복을 필터링하는 예제이다.

    위 코드의 결과는 [2, 4]가 된다.

     

    5.2 스트림 슬라이싱

    5.2.1 Predicate를 이용한 슬라이싱

    자바9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지로 새로운 메서드를 지원한다.

     

    • TAKEWHILE 활용

    다음과 같이 특별한 요리 목록을 가지고 있다고 가정해보자

    List<Dish> specialMenu = Arrays.asList(
        new Dish("seasonal fruit", true, 120, Dish.Type.OTHER ),
        new Dish("prawns", false, 300, Dish.Type.FISH ),
        new Dish("rice", true, 350, Dish.Type.OTHER ),
        new Dish("chicken", false, 400, Dish.Type.MEAT ),
        new Dish("french fries", true, 530, Dish.Type.OTHER ));     

    320 칼로리 이하의 요리를 선택하는 방법은 아래와 같다.

    List<Dish> filteredMenu
        = specialMenu.stream()
                     .filter(dish -> dish.getCalories() < 320)
                     .collect(toList());

    그런데 요리의 목록을 보면 칼로리가 오름차순으로 정렬되어 있다는 사실을 알 수 있다.

    그렇다면 320 칼로리보다 크거나 같은 요리가 나왔을 때 반복 작업을 중단할 수도 있지 않을까?

    takeWhile 연산을 이용하면 이를 간단하게 처리할 수 있다.

    List<Dish> sliceMenu1
        = specialMenu.stream()
                     .takeWhile(dish -> dish.getCalories() < 320)
                     .collect(toList());

    takeWhile을 이용하면 무한 스트림을 포함한 모든 스트림에 Predicate를 적용해 스트림을 슬라이스할 수 있다.

     

    • DROPWHILE 활용

    나머지 요소를 선택하기 위해선 어떻게 해야할까?

    즉 320 칼로리보다 큰 요소는 어떻게 탐색할까?

    dropWhile을 이용하면 이 작업을 할 수 있다.

    List<Dish> sliceMenu2
        = specialMenu.stream()
                     .dropWhile(dish -> dish.getCalories() < 320)
                     .collect(toList());

    dropWhile은 Predicate가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다.

    Predicate가 거짓이 되면 그 지점에서 작업을 중단하고 남은 요소를 반환한다.

    dropWhile 역시 무한한 남은 요소를 가진 무한 스트림에서 동작한다.

     

    5.2.2 스트림 축소

    스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다.

    스트림이 정렬되어 있으면 최대 요소 n개를 반환할 수 있다.

    List<Dish> dishes = specialMenu.stream()
                                   .filter(dish -> dish.getCalories() > 300)
                                   .limit(3)
                                   .collect(toList());

    위의 코드는 300칼로리 이상의 요리 세 가지를 선택해서 리스트로 만드는 예제이다.

    limit는 정렬되지 않은 스트림(Set) 에도 사용할 수 있다. 소스가 정렬되어 있지 않다면 limit의 결과도 정렬되지 않은 상태로 반환한다.

     

    5.2.3 요소 건너뛰기

    소트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다.

    n개 이사의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.

    limit(n)과 skip(n)은 상호 보완적인 연산을 수행한다.

    List<Dish> dishes = specialMenu.stream()
                                   .filter(d -> d.getCalories() > 300)
                                   .skip(2)
                                   .collect(toList());

    위의 코드는 300칼로리 이상의 처음 두 요리를 건너뛴 다음에 300 칼로리가 넘는 나머지 요리를 반환하는 예제이다.

     

    5.3 매핑

    5.3.1 스트림의 각 요소에 함수 적용하기

    스트림은 함수를 인수로 받는 map 메서드를 적용한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다. ( 값을 고친다라기 보다는 새로운 버전을 만든다에 아까움 )

    예를 들어 아래는 Dish::getName을 map 메서드로 전달해서 스트림의 요리명을 추출하는 코드다.

    List<String> dishNames = specialMenu.stream()
                                        .map(Dish::getName)
                                        .collect(toList());

    getName 메서드는 문자열을 반환하므로 map 메서드의 출력 스트림은 Stream<String>형식을 갖는다.

     

    만약 단어 리스트가 주어졌을 때 각 단어가 포함하는 글자 수의 리스트를 반환한다고 가정하자.

    이 경우 리스트의 각 요소에 함수를 적용해야 한다.

    아래처럼 코드를 작성할 수 있다.

    List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
    List<Integer> wordLengths = words.stream()
                                     .map(String::length)
                                     .collect(toList());

     

    아까의 요리명을 추출하는 예제에서 요리명의 길이를 알고 싶다면 어떻게 해야 할까?

    List<Integer> dishNameLengths = specialMenu.stream()
                                               .map(Dish::getName)
                                               .map(String::length)
                                               .collect(toList());

     

    5.3.2 스트림 평면화

    위에서 확인한 메서드 map을 이용해서 리스트의 각 단어의 길이를 반환하는 방식을 응용해서 리스트에서 고유 문자로 이루어진 리스트를 반환해보자

    예를 들어 ["Hello", "World"] 리스트가 있다면 ["H", "e", "l", "o", "W", "r", "d"]를 포함하는 리스트가 반환되어야 한다.

    words.stream()
         .map(word -> word.split(""))
         .distinct()
         .collect(toList());

    위의 예제처럼 코드를 짤 수도 있을 것이다. 하지만 이는 잘못됐다.

    위의 코드의 map 메서드는 Stream<String[]>을 반환한다.

    우리가 원하는 결과는 Stream<String>이었다.

    이 문제는 flatMap이라는 메서드를 통해서 해결할 수 있다.

     

    • map과 Arrays.stream의 활용

    우선 배열 스트림 대신에 문자열 스트림이 필요하다.

    아래 코드에서 보여주는 것처럼 문자열을 받아 스트림을 만드는 Arrays.stream() 메서드가 있다.

    String[] arrayOfWords = {"Goodbye", "World"};
    Stream<String> streamOfWords = Arrays.stream(arrayOfWords);

    위 예제의 파이프라인에 Arrays.stream()을 적용하게 되면

    words.stream()
         .map(word -> word.split("")) // 각 단어를 개별 문자열 배열로 변환
         .map(Arrays::stream) // 각 배열을 별도의 스트림으로 생성
         .distinct()
         .collect(toList());

    결국 스트림의 리스트가 생성이 되게 되면서 문제가 해결되지는 않았다.

    문제를 해결하기 위해서는 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 다음에 각 배열을 별도의 스트림으로 만들어야 한다.

     

    • flatMap 사용

    flatMap을 사용하면 아래처럼 문제를 해결할 수 있다.

    List<String> uniqueCharacters = 
        words.stream()
             .map(word -> word.split("")) // 각 단어를 개별 문자를 포함하는 배열로 전환
             .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
             .distinct
             .collect(toList());

    flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다.

    즉, map과 달리 flatMap은 하나의 평면화된 스트림을 반환한다.

     

    요약하면 flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

     

    5.4 검색과 매칭

    5.4.1 Predicate가 적어도 한 요소와 일치하는지 확인

    Predicate가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 이용한다.

    아래는 menu에 채식 요리가 있는지 확인하는 예제다.

    if(menu.stream().anyMatch(Dish::isVegetarian)){
        System.out.println("The menu is (somewhat) vegetarian friendly!!");
    }

    anyMatch는 불리언을 반환하므로 최종 연산이다.

     

    5.4.2 Predicate가 모든 요소와 일치하는지 검사

    allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 Predicate와 일치하는지 검사한다.

    예를 들어 메뉴가 1000칼로리 이하인지 확인하는 경우를 보면

    boolean isHealthy = menu.stream()
                            .allMatch(dish -> dish.getCalories() < 1000);

    위와 같이 작성할 수 있다.

     

    • NONEMATCH

    noneMatch는 allMatch와 반대 연산을 수행한다.

    즉, noneMatch는 주어진 Predicate와 일치하는 요소가 없는지 확인한다.

    boolean isHealthy = menu.stream()
                            .noneMatch(dish -> dish.getCalories() < 1000);

    위와 같이 noneMatch로 구현할 수 있다.

     

    anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, || 와 같은 연산을 활용한다.

     

    쇼트서킷

    여러 and 연산으로 연결된 커다란 boolean 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과에 상관없이 전체 결과도 거짓이 된다. 이러한 상황을 쇼트서킷이라고 한다.

     

    5.4.3 요소 검색

    findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.

    findAny 메서드를 다른 스트림 연산과 연결해서 사용할 수 있다.

    Optional<Dish> dish = 
        menu.stream()
            .filter(Dish::isVegetarian)
            .findAny();

    위 코드는 findAny를 이용해서 채식 요리를 선택하는 예제이다.

    스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다.

    즉 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다.

     

    Optional이란?

    Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다.

    바로 위의 예제에서 findAny()는 아무 요소도 반환하지 않을 수 있다.

    null은 쉽게 에러를 일으킬 수 있으므로 Java 8 라이브러리 설계자는 Optional<T>를 만들었다.

    Optional은 값이 존재하는지 확인하고 값이 없을 때 어떻게 처리할지 강제하는 기능을 제공한다.

     

    5.4.4 첫 번째 요소 찾기

    findAny는 임의로 한 개를 반환하지만 첫 번째 요소가 필요한 경우가 있다.

    이때는 findFirst라는 메서드를 이용한다.

    이는 스트림의 첫 번째 요소를 반환하며 findAny와 같이 Optional로 감싼 반환값을 반환한다.

    List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
            .map(n -> n * n)
            .filter(n -> n % 3 == 0)
            .findFirst();

     

    5.5 리듀싱

    모든 스트림 요소를 처리해서 값으로 도출하는 것을 리듀싱 연산이라고 한다.

     

    5.5.1 요소의 합

    for-each 루프를 이용해서 리스트의 숫자 요소를 더하는 코드는 아래와 같다.

    int sum = 0;
    for(int number : numbers){
        sum += number;
    }

    위의 코드는 파라미터 두 개를 사용했다.

    1. sum 변수의 초깃값 0

    2. 리스트의 모든 요소를 조합하는 연산 (+)

     

    위 코드를 복사, 붙여 넣기를 하지 않고 모든 숫자를 곱하는 연산을 구할 수 있다면 좋지 않을까?

    위의 코드에 reduce를 사용하면 반복된 패턴을 추상화할 수 있다.

    int sum = numbers.stream().reduce(0, (a,b) -> a+b);

    reduce는 두 개의 인수를 갖는다.

    1. 초깃값 0

    2. 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>

     

    reduce로 다른 람다를 넘겨주면 모든 요소에 곱셈을 적용시킬 수 있다.

    int sum = numbers.stream().reduce(1, (a,b) -> a*b);

     

    Integer 클래스의 정적 메서드인 sum 메서드를 사용하여 메서드 참조를 통해 더 간결하게 만들 수도 있다.

    int sum = numbers.stream().reduce(0, Integer::sum);

     

    초기 값을 받지 않고 오버로드된 reduce도 있다. 하지만 이 reduce는 Optional 객체를 반환한다.

    스트림이 비어있다면 초깃값이 없으므로 reduce는 합계를 반환할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록 Optional 객체로 감싼 결과를 반환한다.

     

    5.2.2 최댓값과 최솟값

    reduce는 앞서 언급했던 것처럼 두 인수를 받는다.

    1. 초깃값

    2. 스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다

     

    reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행하면서 최댓값을 생산한다.

    Optional<Integer> max = numbers.stream().reduce(0, Integer::max);
    Optional<Integer> min = numbers.stream().reduce(0, Integer::min);

     

    스트림 연산을 하다 보면 map,filter처럼 이전 스트림의 값을 저장하지 않아도 되는 메서드가 있다.

    이를 상태를 갖지 않는 스트림 연산이라고 하는데 sum, max, sorted같이 이전 스트림의 값을 알아야만 처리가 가능한 메서드도 있다.

    이를 상태를 갖는 스트림 연산이라고 한다.

    상태를 갖는 연산은 내부 버퍼에 값을 저장해놓고 사용한다.

     

    5.6 숫자형 스트림

    앞서 사용했던 reduce 메서드로 스트림 요소의 합을 구하게 되면 박싱 비용이 발생하게 된다.

    내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야한다.

    스트림에서는 이를 처리하기 위해 기본형 특화 스트림을 제공한다.

     

    5.6.1 기본형 특화 스트림

    Java8에서는 세 가지 기본형 특화 스트림을 제공한다.

    스트림 API는 박싱 비용을 피할 수 있도록 int 요소에 특화된 IntStream, double 요소에 특화된 DoubleStream, long 요소에 특화된 LongStream을 제공한다.

    각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최대값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.

    또한 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.

     

    • 숫자 스트림으로 매핑

    스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다.

    이들 메서드는 map과 정확히 같은 기능을 수행하지만 Stream<T> 대신 특화된 스트림을 반환한다.

    int calories = menu.stream() //Stream<Dish> 반환
                       .mapToInt(Dish::getCalories) // IntStream 반환
                       .sum();

    mapToInt 메서드는 각 요리에서 모든 칼로리를 Integer 형식으로 추출한 다임에 IntStream을 반환한다.

     

    • 객체 스트림으로 복원하기

    숫자 스트림을 만든 후에 원상태인 특화되지 않은 스트림으로 복원하기 위해 boxed 메서드를 제공한다.

    IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
    Stream<Integer> stream = intStream.boxed();

     

    • 기본값 : OptionalInt

    Optional을 Integer, String 등의 참조 형식으로 파라미터화할 수 있다.

    또한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림도 제공한다.

    OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
    int max = maxCalories.orElse(1); // 값이 없을 때 기본 최댓값을 명시적으로 설정

     

    5.6.2 숫자 범위

    프로그램에서는 특정 범위의 숫자를 이용해야 하는 상황이 자주 발생한다.

    for문을 통해 i=1부터 100까지 반복문을 돌리는 연산을 예로 들 수 있다.

    Java8의 IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.

    두 메서드 모두 첫 번째 인수로 시작 값을, 두 번째 인수로 종료 값을 갖는다.

    range 메서드는 시작 값과 종료 값이 결과에 포함되지 않는 반면

    rangeClosed는 시작값과 종료값이 결과에 포함된다.

    IntStream evenNumbers = IntStream.rangeClosed(1, 100) // [1,100]의 범위를 나타낸다.
                                     .filter(n -> n % 2 == 0); // 1부터 100까지의 짝수 스트림
                                     
    System.out.println(evenNumbers.count()); // 1부터 100까지에는 50개의 짝수가 있다.

     

    5.7 스트림 만들기

    컬렉션뿐만 아니라 일련의 값, 배열, 파일, 심지어 함수에서도 스트림 생성이 가능하다.

     

    5.7.1 값으로 스트림 만들기

    임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.

    Stream<String> stream = Stream.of("Modern ", "Java ", "In ", "Action");
    stream.map(String::toUpperCase).forEach(System.out::println);

    empty 메서드를 이용해서 스트림을 비울 수도 있다.

    Stream<String> emptyStream = Stream.empty();

     

    5.7.2 null이 될 수 있는 객체로 스트림 만들기

    Java9에서는 null이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 메서드가 추가되었다.

    때로는 null이 될 수 있는 객체를 스트림으로 만들어야 할 수도 있다. ( 객체가 null이라면 빈 스트림 )

    이 경우 ofNullable 메서드를 사용하면 된다.

    Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));

     

    5.7.3 배열로 스트림 만들기

    배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림으로 만들 수 있다.

    int[] numbers  = {1, 2, 3, 4 ,5};
    int sum = Arrays.stream(numbers).sum();

     

    5.7.4 파일을 스트림 만들기

    파일 처리의 I/O연산에 사용하는 java.nio.file도 스트림 API를 사용할 수 있도록 업데이트되었다.

    long uniqueWords = 0;
    try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
        uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                .distinct()
                .count();
    } catch (IOException e) {
        e.printStackTrace();
    }

     

    5.7.5 함수로 무한 스트림 만들기

    스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드

    Stream.iterate와 Stream.generate를 제공한다.

     두 연산을 이용해서 무한 스트림, 즉 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과는 다르게

    크기가 고정되지 않은 스트림을 만들 수 있다.

    하지만 보통 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결해서 사용한다.

     

    • iterate 메서드

    iterate 메서드는 초깃값과 람다를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다.

    이러한 스트림을 인바운드 스트림이라고 표현한다.

    Stream.iterate(0, n -> n + 2)
           .limit(10)
           .forEach(System.out::println);

    위 예제는 이전 결과에 2를 더한 값을 반환한다. 

    결과적으로 iterate 메서드는 짝수 스트림을 생성한다.

     

    Java9의 iterate 메서드는 Predicate를 지원한다.

    예를 들어 0부터 시작해서 4씩 증가하며 100보다 크면 숫자 생성을 중단하는 코드를 아래와 같이 구현할 수 있다.

    IntStream.iterate(0, n -> n < 100, n -> n + 4)
             .forEach(System.out::println);

     

    • generate 메서드

    iterate와 비슷하게 generate도 요구할 때 값을 계산하는 무한 스트림으로 만들 수 있다.

    하지만 iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않는다.

    generate는 Supplier<T>를 인수로 받아서 새로운 값을 생성한다.

    Stream.generate(Math::random)
          .limit(5)
          .forEach(System.out::println);

    위 코드는 0에서 1사이에 임의의 더들 숫자 다섯 개를 만든다.

     

    우리가 사용한 Supplier(메서드 참조 Math::random)는 상태가 없는 메서드, 즉 나중에 계산에 사용할 어떤 값도 저장해 두지 않는다.

    하지만 Supplier가 꼭 상태가 없어야 하는 것은 아니다.

    Supplier가 상태를 저장한 다음에 스트림의 다음 값을 만들 때 상태를 고칠 수도 있다.

     

    우리는 이전 예제에서 IntStream을 이용하면 박싱 연산 문제를 피할 수 있었다.

    IntStream의 generate 메서드는 Supplier<T> 대신에 IntSupplier를 인수로 받는다.

    IntStream ones = IntStream.generate(() -> 1);

     

    아래는 generate 메서드가 주어진 Supplier를 통해 2를 반환하는 getAsIt 메서드를 반복적으로 호출하는 예제다.

    IntStream twos = IntStream.generate(new IntSupplier(){
        public int getAsInt(){
            return 2;
        }
    }

    여기서 사용한 익명 클래스와 람다는 비슷한 연산을 수행하지만 익명 클래스는 getAsInt 메서드의 연산을 커스터마이즈할 수 있는 상태 필드를 정의할 수 있다는 점이 다르다. 람다는 상태를 바꾸지 않는다.

    이렇게 익명 클래스를 사용하게 되면 부작용이 발생할 수도 있다.

    IntSupplier fib = new IntSupplier() {
        private int previous = 0;
        private int current = 1;
    
        public int getAsInt() {
            int oldPrevious = this.previous;
            int nextValue = this.previous + this.current;
            this.previous = this.current;
            this.current = nextValue;
            return oldPrevious;
        }
    };
    
    IntStream.generate(fib).limit(10).forEach(System.out::println);

     위 코드에서는 IntSupplier 인스턴스를 만들었다.

    만들어진 객체는 기존 피보나치 요소와 두 인스턴스 변수에 어떤 피보나치 요소가 들어있는지 추적하므로 가변 상태 객체다.

    getAsInt를 호출하면 객체의 상태가 바꾸며 새로운 값을 생산한다.

    iterate를 사용했을 땐 기존의 상태를 바꾸지 않는 순수한 불변 상태를 유지했다.

Designed by Tistory.