SummaryBook/[모던 자바 인 액션]

[Chap 5] 스트림 활용

seung_soos 2023. 7. 11. 21:48

5.1 필터링

5.1.1 프레디케이트로 필터링

스트림 인터페이스는 filter  메서드를 지원한다. filter 메서드는 프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

	/**
         * 5.1.1 프레디케이트로 필터링
         */
        List<Dish> menu = new ArrayList<>();
        List<Dish> vegetarianMenu = menu.stream()
                    .filter(Dish::isVegetarian) // 채식 요리인지 확인하는 메서드참조
                    .collect(Collectors.toList());

5.2 스트림 슬라이싱

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

TAKEWHILE 활용

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

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

위 리스트는 이미 칼로리 순으로 정렬되어 있다. 리스트가 이미 정렬되어 있다는 사실을 이용해서 320칼로리보다 크거나 같은 요리가 나왔을때 반복 작업을 중단 할 수 있다. 작은 리스트에서는 별거 아닌 것처럼 보일 수 있지만, 아주 많은 요소를 포함하는 큰 스트림에서는 상당한 차이가 될 수 있다.

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

        // takeWhile 적용
	List<Dish> slicedMenu1 = specialMenu.stream()
                .takeWhile(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());

DROPWHILE 활용

320 칼로리보다 큰 요소를 찾는방법으로는  dropWhile을 활용

	// dropWhile 적용
        List<Dish> slicedMenu2 = specialMenu.stream()
                .dropWhile(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());

dropWhile은 takeWhile과 정반대의 작업을 수행한다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다.프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다. dropWhile은 무한한 남은 요소를 가진 무한 스트림에서도 동작한다.

5.2.2 스트림 축소

스틀미은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 스트림이 정렬되어 있으면 최대 요소 n개를 반환 할 수 있다. 

5.2.3 요소 건너뛰기

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈스트림이 반환된다.

        /**
         * 5.2.3 요소 건너뛰기
         */
        List<Dish> menu = new ArrayList<>();
        List<Dish> dishes = menu.stream()
                .filter(d -> d.getCalories() > 300)
                .skip(2)
                .collect(Collectors.toList());

5.3 매핑

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

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

        /**
         * 5.3.1 스트림의 각 요소에 함수 적용하기
         */
        List<String> dishNames = menu.stream()
                .map(Dish::getName)
                .collect(Collectors.toList());

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

요리명의 문자길이를 알고 싶다면 map메서드를 연결 할 수 있다.

	// 요리명의 길이 추가
        List<Integer> dishNameLengths = menu.stream()
                .map(Dish::getName)
                .map(String::length)
                .collect(Collectors.toList());

5.3.2 스트림 평면화

리스트에서 고유문자로 이루어진 리스트를 반환한다. 중복을 제거하기위해 Distinct를 사용하여 중복을 제거할 수 있을거라 생각하지만, 

결과는 다음과같다.

        /**
         * 5.3.2 스트림 평면화
         */
        List<String> list = Arrays.asList("Hello", "World");

        List<String[]> result = list.stream()
                .map(word -> word.split(""))
                .distinct().collect(Collectors.toList());

        result.stream().forEach(x -> System.out.println(Arrays.toString(x)));

결과  : [H, e, l, l, o], [W, o, r, l, d]

map과 Arrays.stream 활용

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

        /**
         * map과 Arrays.stream 활용
         */
        List<String> words = null;
        String[] arraysOfWord = {"Goodbye", "World"};
        Stream<String> streamOfWords = Arrays.stream(arraysOfWord);
        words.stream()
                .map(word -> word.split("")) // 각 단어를 개별 문자열로 반환 
                .map(Arrays::stream) // 각 배열을 별도의 스트림으로 생성
                .distinct()
                .collect(Collectors.toList());

결국 스트림 리스트가 만들어지면서  문제가 해결되지는 않는다. 문제를 해결하려면 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 다음에 각 배열을 별도의 스트림으로 만들어야한다.

flatMap 사용

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

map(Arrays::stream)과 달리 flatMap(Arrays::stream)은 하나의 평면화된 스트림을 반환한다.

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

5.4 검색과 매칭

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

	/**
         * 5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
         */
        if (menu.stream().anyMatch(Dish::isVegetarian)) {
            System.out.println("vegetarian food");
        }

anyMatch는 Boolean을 반환하므로 최종 연산이다.

5.4.2 프레디케이트가 모든욧고와 일치하는지 검사

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

        /**
         * 5.4.2 프레디케이트가 모든욧고와 일치하는지 검사
         */
        boolean isHealthy1 = menu.stream()
                                .allMatch(dish -> dish.getCalories() < 1000);

noneMatch

noneMatch는 allMatch와 반대연산을 수행한다. 즉, 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.

        /**
         * noneMatch
         */
        boolean isHealthy2 = menu.stream()
                                .noneMatch(dish -> dish.getCalories() >= 1000);

5.4.3 요소 검색

findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny 메서드를 다른 스트림연산과 연결해서 사용 할 수 있다.

        /**
         * 5.4.3 요소 검색
         */
        Optional<Dish> dish = menu.stream()
                                .filter(Dish::isVegetarian)
                                .findAny();

스트림 파이프라인은 내부적으로 단일 과정으로 실행 할 수 있도록 최적화된다. 결과를 찾는 즉시 실행을 종료한다.

5.4.4 첫번째 요소 찾기

        /**
         * 5.4.4 첫번째 요소 찾기
         */
        List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
        Optional<Integer> first = someNumbers.stream()
                                            .map(x -> x * x)
                                            .filter(x -> x % 3 == 0)
                                            .findFirst();

※ 병렬 실행에서는 첫번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다. 

5.5 리듀싱

리듀싱연산은 모든 스트림 요소를 처리해서 값으로 도출한다. 

5.5.1 요소의 합

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

        /**
         * 5.5.1 요소의 합
         */
        List<Integer> numbers = Arrays.asList(4, 5, 3, 9);

        Integer reduce = numbers.stream().reduce(0, (a, b) -> a + b);
        System.out.println(reduce);

초깃값 0, 두요소를 조합해서 새로운 값을 만드는 BinaryOperator를 사용

reduce의 내부 동작 : 초깃값 0 + 4 -> 4 + 5 -> 9 + 3 ->  12 + 9 -> 21  

메서드 참조를 이용한 코드의 간결화 적용

        /**
         * 메서드 참조 사용
         */
        Integer reduce1 = numbers.stream().reduce(0, Integer::sum);

초깃값 없음

스트림에 아무 요소도 없는 상황이라면  초깃값이 없으므로 reduce는 합계를 반환 할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록

Optional로 반환한다.

	/**
         * 초깃값 없음
         */
        Optional<Integer> reduce2 = numbers.stream().reduce((a, b) -> (a + b));

5.5.2 최댓값과 최솟값

        /**
         * 5.5.2 최댓값과 최솟값
         */
        Optional<Integer> reduceMax = numbers.stream().reduce(Integer::max);
        Optional<Integer> reduceMin = numbers.stream().reduce(Integer::min);

5.7 숫자형 스트림

메뉴의 칼로리 합계 계산

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

위코드에는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야한다.

5.7.1 기본형 특화 스트림

Java 8에서는 세가지 기본형 특화 스트림을 제공한다. 스트림 API는 박싱 비용을 피할 수 있도록 IntStream, DoubleStream, LongStream을 제공한다. 특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련이 있으며, 스트림에 추가 기능을 제공하지는 않는다.

숫자 스트림으로 매핑

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

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

	// 숫자 스트림으로 매핑
        int sum = menu.stream()
                .mapToInt(Dish::getCalories)
                .sum();

mapToInt 메서드는 각 요리에서 모든 칼로리를 추출한 다음에 IntStream을 반환한다. IntStream 인터페이스에서 제공하는 sum 메서드를 이용해서 칼로리 합계를 계산하였다.

객체 스트림으로 복원하기

 

        // 객체 스트림으로 복원하기
        IntStream intStream = menu.stream()
                .mapToInt(Dish::getCalories); // Stream -> IntStream 변환
        Stream<Integer> boxed = intStream.boxed(); // IntStream -> Stream 변환

기본값 : OptionalInt

Stream에서 요소가 없는 상황과, 실제 최댓값이 0인 상황을 어떻게 구별할까? Optional을 Integer, String 등의 참조형식으로 파라미터화 할 수 있다. 또한 OptionalInt, OptionalDouble, OptionalLong 세가지 기본형 특화 스트림도 제공한다.

        // 기본값 : OptionalInt
        OptionalInt max = menu.stream().mapToInt(Dish::getCalories).max();

5.7.2 숫자 범위

Java 8의 IntStream과 LongStream에서는 range와 rangeClosed 두 정적 메서드를 제공한다.

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

        IntStream intStream1 = IntStream.rangeClosed(1, 100) // 1부터 100까지
                .filter(x -> x % 2 == 0); // 짝수

5.8 스트림 만들기

5.8.1 값으로 스트림 만들기

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

        /**
         * 5.8.1 값으로 스트림 만들기
         */
        Stream<String> modern = Stream.of("Modern", "Java", "In", "Action");
        modern.map(String::toUpperCase).forEach(System.out::println);
        Stream<Object> empty = Stream.empty(); // empty메서드를 이용 Stream을 비울수 있다.

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

Java 9에서는 null이 될 수 있는 개체를 스트림으로 만들수 잇는 메서드가 추가되었다. 예를들어 System.getProperty는 제공된 키에 대응하는 속성이 없으면 null을 반환한다.

        /**
         * 5.8.2 null이 될 수 있는 객체로 스트림 만들기
         */
        String homeValue = System.getProperty("home");
        Stream<String> homeValueStream1 = homeValue == null ? Stream.empty() : Stream.of("value");

        // ofNullable 사용
        Stream<String> homeValueStream2 = Stream.ofNullable(System.getProperty("home"));

ofNullable의 내부구조는 다음과 같다.

5.8.3 배열로 스트림 만들기

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

        /**
         * 5.8.3 배열로 스트림 만들기
         */
        int[] numbers = {2, 3, 4, 5, 6, 7, 8};
        int sum = Arrays.stream(numbers).sum();

5.8.4 파일로 스트림 만들기

        /**
         * 5.8.4 파일로 스트림 만들기
         */
        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) {
            throw new RuntimeException(e);
        }

Stream은 AutoiCloseable 인터페이스를 구현한다. 따라서 try 블록 내의 자원은 자동으로 관리된다.

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

iterate 메서드

        // iterate 메서드
        Stream.iterate(0, n -> n+2)    // 초기값 0에서 +2 증가
                .limit(10)          // 10개
                .forEach(System.out::println);

limit을 주석처리시 무한으로 값이 증가한다.

        // 초깃값 0에서, 0보다 작을때가지, +4 증가
        IntStream.iterate(0, n -> n < 100, n -> n + 4)
                .forEach(System.out::println);
        // filter 메서드가 적용되지않음, 무한 증가
        IntStream.iterate(0, n -> n + 4)
                .filter(n -> n < 100)
                .forEach(System.out::println);
        // 해결방법 : takeWhiles
        IntStream.iterate(0, n -> n + 4)
                .takeWhile(n -> n < 100)
                .forEach(System.out::println);

generate 메서드

iterate와 비슷하게 generate도 무한 스틀미을 만들 수 있다.하지만 iterate와 달리 generate는 생산된 값을 연속적으로 계산하지 않고, 새로운 값을 생산한다.

        //generate 메서드
        Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);

마찬가지로 limit  주석시 무한 스트림이 된다.