본문 바로가기

IT Book Summary/ModernJavaInAction

Chapter5 스트림 활용

데이터 컬렉션 외부반복 코드를 filter와 collect 연산을 이용해 내부 반복으로 처리

 

List<Dish> vegetarianDishes = new ArrayList<>();
for(Dish d: menu) {
    if(d.isVegetarian()){
        vegetarianDishes.add(d);
    }
}

//filter 메서드에 필터링 연산을 인수로 넘겨줌
import static java.util.stream.Collectors.toList;
List<Dish> vegerarianDishes = 
    menu.stream()
        .filter(Dish::isVegetarian)
        .collect(toList());

 

이 장에서는 스트림 API 가 지원하는 다양한 연산을 알아볼것이다.


5.1 필터링

 

프레디케이트로 필터링

filter 메서드는 프레디케이트를 인수로 받아서 일치하는 모든 요소를 포함하는 스트림을 반환

 

List vegerarianDishes = menu.stream()
                                               .filter(Dish::isVegetarian) // 채식요리인지 확인하는 메서드 참조
                                               .collect(toList());

 

고유요소 필터링

고유요소로 이루어진 스프림을 반환하는 distinct 메서드도 지원

(고유 여부는 스트림에서 만든 객에의 hashCode, equals로 결정)

 

List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);

number.stream()

            .filter(i-> i%2 == 0 )

            .distinct() // 중복을 필터링

            .forEach(System.out::println);

 

5.2 스트림 슬라이싱

 

프레디케이트를 이용한 슬라이싱

자바9 은 스트림 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두가지 메서드 지원

 

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칼로리 이하의 요리 선택
    //이미 칼로리 순대로 정렬되어있다면 filter를 이용해 전체를 탐색하는대신
    // takeWhile을 이용해 모든 스트림에 프레디케이트 적용해 슬라이스. 반복작업 중단
List<Dish> sliceMenu1 = specislMenu.stream()
                                .takeWhile(dish->getCalories() < 320)
                                .collect(toList()); //fruit,prawns

// 프레디케이트가 처음 거짓이 되는 지점까지 발견된 요소를 버림
List<Dish> sliceMenu2 = specialMenu.stream()
                                .dropWhile(dish->dish.getCalories() < 320)
                                .collect(toList()); //rice,chicken,french fries

 

스트림축소

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

 

List<Dish> vegerarianDishes = menu.stream()
                                               .filter(Dish::isVegetarian)

                                               .limit(3)
                                               .collect(toList());

 

요소 건너뛰기

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

List<Dish> dishes = menu.stream()
                                           .filter(d -> d.getCalories() > 300)

                                           .skip(2)
                                           .collect(toList());


5.3  매핑

스트림 API 의 map과 flatMap메서드는 특정 데이터를 선택하는 기능을 제공

 

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

함수를 인수로 받는 map 메서드. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑

List<String> words = Arrays.asList("Modern", "Java", "In", "Action");

List<Integer> wordLengths = menu.stream()
                                           .map(String::length)

                                           .collect(toList());

스트림 평면화

["Hello", "World"] 리스트에서 ["H", "e", "l", "o", "W", "r", "d"] 반환하려면?

 

map과 Arrays.stream 활용

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

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

 

flatMap 사용

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

각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능 수행.

 

List<String> uniqueCharacters = words.stream()
                                    .map(word-> word.split("")) //개별문자 배열 변환
                                    .faltMap(Arrays::stream) //하나의 스트림으로 평면화
                                    .distinct()
                                    .collect(toList());

 


5.4 검색과 매칭

 

특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리, 다양한 유틸리티 메서드 제공

 

프레디케이트가 적어도 한 요소와 일치하는지 확인

anyMatch 메서드 이용

if(menu.stream().anyMatch(Dish::isVegetarian)){

    System.out.println("This menu is (somewhat) vegetarian friendly!!");

}

 

프레디케이트가 모든 요소와 일치하는지 검사

allMatch 메서드. 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사

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

 

NoneMatch

주어진 프레디케이트와 일치하는 요소가 없는지 확인.

boolean is Healthy = menu.stream().noneMatch(dish->dish.getCalories() >= 1000);

스트림 쇼트서킷 기법, 즉 자바의 &&, || 과 같은 연산을 활용

 

요소검색

FindAny 메서드는 현재 스트림에서 임의의 요소를 반환

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

 

Optional

 

첫번째 요소 찾기

findFirst() 리스트 또는 정렬된 연속 데이터로부터 생성된 스트림에서 첫번째 요소 찾아 반환.

findFirst 와 findAny 메서드가 필요한 이유는 병렬성 때문

병렬실행에서는 첫번째 요소를 찾기 어렵기 때문에 . 요소의 반환순서가 상관없다면 제약이 적은 FindAny 사용

 


5.5 리듀싱

리듀스 연산을 통해 '메뉴의 모든 칼로리의 합계를 구하시오' 같은 더 복잡한 질의를 표현하는 방법을 설명한다.

Integer같은 결과가 나올때까지 스트림의 모든 요소를 반복적으로 처리.

리듀싱 연산 (모든 스트림 요소를 처리해서 값으로 도출)

함수형 프로그래밍 언어 용어로는 폴드 (마치 종이를 작은 조각이 될때까지 반복해서 접는것과 비슷)

 

요소의 합

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

 

reduce는 두개의 인수를 갖는다 

- 초깃값 0

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

 

스트림이 하나의 값으로 줄어들때 까지 람다는 각 요소를 반복해서 조합.

메서드 참조를 이용해 더 간결하게 표현.

 

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

 

초기값 없음

이 reduce 객체는 Optional 객체를 반환.

Optional<Integer> sum = number.stream().reduce( (a,b) -> (a+b) );

스트림에 아무 값이 없을경우 합계를 반환할 수 없으므로 Optional 객체로 감싼 결과를 반환

 

최대값과 최소값

 

다음처럼 스트림의 최대값을 찾을수 있음

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

최소값

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


5.7 숫자형 스트림

 

다음처럼 칼로리 합계를 계산할 수 있다.

int calories = menu.stream(). map(Dish::getCalories).reduce(0, Integer::sum);

여기에는 박싱 비용이 숨어 있다.

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

 

int calories = menu.stream(). map(Dish::getCalories).sum();

sum메서드를 직접 호출할 수는 없다.

스트림 요소 형식은 Integer이지만 인터페이스에는 sum 메서드가 없다.

다행히 숫자스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.

 

기본형 특화 스트림

기본형 특화 스트림 IntStream, DoubleStream, LongStream 과 숫자관련 리듀싱 연산 수행 메서드를 제공한다.

 

숫자 스트림으로 매핑

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

map과 같은 기능을 하면서 Stream<T> 대신 특화된 스트림을 반환.

 

int calories = menu.stream().mapToInt(Dish::getCalories).sum();

스트림이 비어있으면 sum 은 기본값 0을 반환

IntStream은 max, min, average등 다양한 유틸리티 메서드 제공

 

객체 스트림으로 복원하기

숫자 스트림을 만든 다음에 원상태인 특화되지 않은 스트림으로 복원할 수 있을까?

 

IntStream intStream = menu.stream().mapToInt(Dish::getCalories); // 스트림을 숫자스트림으로 변환

Stream<Integer> stream = intStream.boxed(); // 숫자스트림을 스트림으로 변환

 

기본값: OptionalInt

스트림에 요소가 없는 상황과 실제최대값이 0인 상황을 어떻게 구별할 수 있을까?

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

OptionalInt maxCalories = menu.stream()

                                                   .mapToInt(Dish::getCalories)

                                                   .max();

OptionalInt를 이용

int max = maxCalories.orElse(1); // 값이 없을 때 기본 최댓값을 명시적으로 설정

 

숫자 범위

자바8의 IntStream과 LongStream에서는

range(시작값과 종료값이 결과에 포함)와 rangeClosed(시작값과 종료값이 결과에 포함 안됨) 두 정적 메서드를 제공

 

IntStream evenNumbers = IntStream.rangeClosed(1,100)
                                 .filter(m -> n % 2 == 0); // 짝수스트림
System.out.println(evenNumbers.count());

 

숫자 스트림 활용: 피타고라스 수

 

피타고라스 수

a*a + b*b = c*c

 

세 수 표현하기

세 요소를 갖는 int 배열을 사용하자.

예를 들어 new int[]{3, 4, 5}

 

좋은 필터링 조합

두 수가 피타고라스 수의 일부가 될 수 있는 조합인지 어떻게 확인할 수 있을까?

a*a + b*b 의 제곱근이 정수인지 확인할 수 있다

Math.sqrt(a*a + b*b) % 1 == 0;

 

이때 x가 부동 소숫점 수라면 x % 1.0 이라는 자바코드로 소숫점 이하 부분을 얻을수 있음.

이를 filter를 이용해서 좋은 조합을 갖는 a, b 를 선택할 수 있음.

filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)

 

집합 생성

마지막 세번째 수를 찾자.

map을 이용해 각 요소를 피타고라스 수로 변환

stream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)

           .map(b -> new int[]{a, b, (int) Math.sqrt(a*a + b*b)});

 

b 값 생성

Stream.rangeClosed로 주어진 범위의 b 값을 생성

 

IntStream.rangeClosed(1, 100)
          .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
          .boxed()
          .map(b -> new int[]{a, b, (int)Math.sqrt(a*a + b*b)});

IntStream을 boxed를 이용해서 Stream<Integer>로 복원했다

map 은 스트림의 각요소를 int 배열로 변환하기 때문에.

개체값 스트림을 반환하는 IntStream의 mapToObj 메서드를 이용해서 이 코드를 재구현 할수 있다.

 

IntStream.rangeClosed(1, 100)
         .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
         .mapToObj(b -> new int[]{a,b, (int)Math.sqrt(a*a + b*b)});

 

 

a 값 생성

b와 비슷한 방법으로 a값을 생성하는 코드를 추가해 피타고라스 수를 생성하는 스트림을 완성

 

Stream<int[]> pythagoreanTriples = 
    IntStream.rangeClosed(1, 100).boxed()
             .flatMap(a ->
                 IntStream.rangeClosed(a, 100)
                          .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
                          .mapToObj(b ->
                              new int[]{a, b, (int)Math.sqrt(a*a + b*b)})
                      );

 

코드 실행

limit를 이용해 얼마나 많은 세수를 포함하는 스트림을 만들지만 결정

 

pythagoreanTriples.limit(5)
                  .forEach(t ->
                      System.out.println(t[0] + ", "+ t[1] + ", " + t[2]));

결과

3, 4, 5

5, 12, 13

6, 8, 10

7, 24, 25,

8, 15, 17

 

개선할 점?

위 코드에서는 제곱근을 두번 계산한다.

따라서 (a*a, b*b, a*a* + b*b)형식을 만족하는 세 수를 만든후 조건에 맞추어 필터링하도록 최적화 하자.

 

Stream<double[]> pythagoreanTriples2 = 
    IntStream.rangeClosed(1, 100).boxed
             .flatMap(a -> IntStream.rangeClosed(a, 100)
             .mapToObj(
                 b -> new double[]{a, b, Math.sqrt(a*a + b*b)}) // 만들어진 세 수
             .filter(t -> t[2] % 1 == 0)); //세번째 요소는 반드시 정수

 


5.8 스트림 만들기

 

이번엔 일련의 값, 배열, 파일, 심지어 함수를 이용한 무한 스트림 만들기 등 다양한 방식을 소개한다.

 

 

값으로 스트림 만들기

임의의 수를 인수로 받는 정적메서드 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();

 

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

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

(객체가 null 이라면 빈 스트림)

 

예를들어 System.getProperty는 제공된 키에 대응하는 속성이 없으면 null 반환

이런 메서드를 스트림에 활용하려면 null을 명시적으로 확인해야했음.

 

String homeValue = System.getProperty();
Stream<String> homeValueStream = 
                  homeValue == null? Stream.empty() : Stream.of(value);

//Stream.ofNullable 이용해 구현
Stream<String> homeValueStream
     = Stream.ofNullable(System.getProperty("home"));

//null이 될수있는 객체를 포함하는 스트림값을 flatMap과 함께 사용하는 상황
Stream<String> values = Stream.of("config", "home", "user")
                              .flatMap(key->Stream.ofNullable(System.getProperty(key)));

 

배열로 스트림 만들기

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

기본형 int 로 이루어진 배열을 IntStream 으로 변환

 

int[] numbers = {2, 3, 5, 7, 11, 13};

int sum = Arrays.stream(numbers).sum(); // 합계 41

 

파일로 스트림 만들기

자바의 NIO API(비블록 I/O)도 스트림을 활용할 수 있게 업데이트 됨.

java.nio.file.Files의 많은 정적 메서드가 스트림을 반환.

 

ex) 파일에서 고유한 단어수를 찾는 프로그램

//파일에서 고유한 단어수를 찾는 프로그램
//스트림은 자원을 자동으로 해제할수있는 AutoCloseable 인터페이스 구현, try-finally 필요없음.
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), 
                                       Charset.defaultCharset())){
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) //고유단어수계산
                       .distict() // 중복제거
                       .count(); //단어스트림 생성
}catch(IOException e){
}

Files.lines로 파일의 각 행 요소를 반환하는 스트림을 얻을 수 있음.

 

 

함수로 무한 스트림 만들기

함수에서 스트림을 만들 수 있는 두 정적메서드 Stream.iterate 와 Stream.generate 지원

두 연산을 이용해 무한스트림, 즉 크기가 고정되지 않은 스트림을 만들 수 있음.

요청할 때마다 주어진 함수를 이용해 무제한으로 값을 계산할수 있지만 보통 limit(n) 함수와 연결해 사용.

 

iterate 메서드

Stream.iterate(0, n -> n+2)

           .limit(10)

           .forEach(System.out::println);

초기값과 람다를 인수로 받아 새로운값을 끊임없이 생산.

무한스트림을 만듦. 언바운드 스트림 이라고도 함.

 

자바9의 iterate 메서드는 프레디케이트 지원.

 

// 0에서 시작해 100보다 크면 숫자생성 중단
IntStream.iterate(0, n -> n<100, n -> n+4)
         .forEach(System.out::println);

// filter로는 작업 중단지점을 알수 없기에 적용 불가
// 대신 스트림 쇼트서킷을 지원하는 takeWhile 이용
IntStream.iterate(0, n -> n+4)
         .takeWhile(n -> n<100)
         .forEach(System.out::println);

 

generate 메서드

generate는 Supplier<T> 를 인수로 받아 새로운 값을 생산

Stream.generate(Math::random)

           .limit(5)

           .forEach(System.out::println);

 

ex) 피보나치 수열 구현

 

//IntStream의 generate 메서드는 Supplier<T> 대신 IntSupplier를 인수로 받음
IntStream ones = IntStream.generate(() -> 1);

//getAsInt를 구현하는 객체를 명시적으로 전달할수도 있음
//getAsInt 메서드 연산을 커스터마이징할수있는 상태필드를 정의 가능. 부작용.
IntStream twos = IntStream.generate(new IntSupplier() {
    public int getAsInt(){
        return 2;
    }
});

//수열 상태를 저장하고 getAsInt로 다음 요소를 계산하도록 IntSupplier 만듬.
IntSupplier fib = new IntSupplier() {
    private int previous = 0;
    private int current = 1;
    public int getAsInt() {
        int oldPrevious = this.previous;
        int nestValue = this.previous + this.current;
        this.previous = this.current;
        this.current = nextValue;
        return oldPrevious;
    }
};
IntStream.generate(fib).limit(10).forEach(System.out::println);

 

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

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

iterate를 사용했을때 새로운 값을 생성하면서도 기존상태를 바꾸지 않는 불변상태와는 다름.

 

-> 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변상태 기법을 고수해야함 (Chapter 7에서 다룰것임)

 

 

무한한 크기를 가진 스트림을 처리하므로 limit를 이용해 크기를 제한하지 않으면,

최종연산을 수행했을때 아무 결과도 계산되지 않는다.

또한 무한스트림요소는 무한으로 계산이 반복되므로 정렬하거나 리듀스 할 수 없다.


  • 스트림 API 를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
  • filter, distinc, takeWhile, dropWhile, skip, limit 메서드로 스트림을 필터링하거나 자를수있다.
  • 소스가 정렬되어 있다는 사실을 알때 takeWhile, dropWhile 메소드를 효과적으로 사용 가능
  • map, flatMap 메서드로 스트림의 요소를 추출하거나 변환 가능
  • findFirst, findAny 메서드로 스트림의 요소를 검색. allMatch, noneMatch, anyMatch 메서드 이용해 스트림 검색
  • 이들 메서드는 쇼트서킷, 즉 찾는 즉시 반환. 전체 스트림을 처리하지 않음.
  • reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출
  • filter, map 등은 상태를 저장하지 않는 연산.
  • reduce 같은 연산은 값을 계산하는데 필요한 상태를 저장
  • sorted, distinct 등의 메서드는 스트림의 모든 요소를 버퍼에 저장. 상태 있는 연산
  • IntStream, DoubleStream, LongStream 은 기본 특화 스트림.
  • 컬렉션 뿐 아니라 값, 배열, 파일, iterate와 generate 같은 메서드로도 스트림을 만들 수 있다.
  • 무한한 개수의 요소를 가진 스트림을 무한 스트림 이라 함.