JAVA

모던 자바 인 액션 - CH05. 스트림 활용

사랑박 2023. 8. 22. 14:58

5.1 필터링

스트림의 요소를 선택하는 방법, 즉 프레디케이트 필터링 방법과 고유 요소만 필터링하는 방법을 배운다.

 

5.1.1 프레디케이트로 필터링

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

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

 

5.1.2 고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다. 고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다.

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,123,41,5,1,2,3,4,5,5,6,7,8)
        .stream()
        .filter(number->number%2==0)
        .distinct()
        .collect(Collectors.toList());

5.2 스트림 슬라이싱

스트림의 요소를 선택하거나 스킵하는 다양한 방법을 설명한다.

 

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

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

 

THAKEWHILE 활용

리스트가 이미 정렬되어 있다는 사실을 이용해 320 칼로리보다 크거나 같은 요리가 나왔을 때 반복 작업을 중단할 수 있다.

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

DROPWHILE 활용

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

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

 

 

5.2.2 스트림 축소

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

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

스트림이 정렬되지 않아도 limit을 사용할 수 있다. -> limit의 결과도 정렬되지 않은 상태로 반환된다.

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

 

5.2.3 요소 건너뛰기

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

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

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

  List<Dish> dishes = menu.stream()
                            .filter(dish -> dish.getCalories() < 300)
                            .skip(2) // 2개 건너뛰기
                            .collect(toList());

 

5.3 매핑

특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리과정에서 자주 수행되는 연산이다. 스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다.

 

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

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

    // 요리명을 추출
    List<String> dishNames = menu.stream()
                            .map(Dish::getName)
                            .collect(toList());
                            
                            
    // 단어의 길이 추출
    List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
    List<Integer> wordLengths = words.stream()
          .map(String::length)
          .collect(toList());


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

 

5.3.2 스트림 평면화

리스트에서 고유 문자로 이루어진 리스트를 반환해보자.

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

 

flatMap 사용

flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, map (Arrays::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다.

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

5.4 검색과 매칭

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

 

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

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

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

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

 

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

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

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

 

NONEMATCH

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

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

 

5.4.3 요소 검색

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

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

 Optional<Dish> dish = 
 	menu.stream()
    	    .filter(Dish::isVegetarian)
            .findAny();	//findAny는 Optional 객체를 반환

Optional이란?

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

이전 예제에서 findAny는 아무 요소도 반환하지 않을 수 있다. null은 쉽게 에러를 일으킬 수 있으므로 자바 8 라이브러리 설계자는 Optional<T>를 만들었다.

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

  • isPresent()는 Optional이 값을 포함하면 참(true)를 반환하고, 값을 포함하지 않으면 거짓(false)를 반환한다.
  • ifPresent(Consumer<T> block)은 값이 있으면 주어진 블록을 실행한다.
  • T get()은 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킨다.
  • T orElse(T other) 는 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환한다.
 	menu.stream()
    	    .filter(Dish::isVegetarian)
            .findAny();	// Optional<Dish>반환
            .ifPresent(dish-> System.out.println(dish.getName()); // 값이 있으면 출력되고 값이 없으면 아무 일도 일어나지 않는다.

 

5.4.4 첫 번째 요소 찾기

리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야 할까?

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

 

5.5 리듀싱

이 절에서 리듀스(reduce) 연산을 이용해서 `메뉴의 모든 칼로리의 합계를 구하시오.`, `메뉴에서 칼로리가 가장 높은 요리는?` 같이 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 방법을 설명한다.

이러한 질의를 수행하려면 Integer 같은 결과가 나올 때까지 스트림의 모든 요소를 반복적으로 처리해야 한다. 이런 질의를 리듀싱 연산(모든 스트림 요소를 처리해서 값으로 도출하는)이라고 한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종리(우리의 스트림)를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 부른다.

 

5.5.1 요소의 합

reduce를 이용해서 다음처럼 스트림의 모든 요소를 더할 수 있다.

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

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

  • 초깃값 0
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>. 예제에서는 람다 표현식 (a,b) -> a+b를 사용했다.

reduce로 다른 람다, 즉(a+b) -> a*b를 넘겨주면 모든 요소에 곱셈을 적용할 수 있다.

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

메서드 참조를 이용해서 이 코드를 좀 더 간결하게 만들 수 있다. 자바 8에서는 Integer 클래스에 두 숫자를 더하는 정적 sum 메서드를 제공한다. 따라서 직접 람다 코드를 구현할 필요가 없다.

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

 

초깃값 없음

초깃값을 받지 않도록 오버로드된 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환한다.

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

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

 

5.5.2 최댓값과 최솟값

최댓값과 최솟값을 찾을 때 reduce를 활용할 수 있다.

// 최댓값
Optional<Integer> max = numbers.stream().reduce(Integer::max);
// 최솟값
Optional<Integer> min = numbers.stream().reduce(Integer::min);
// 람다 표현식 사용
Optional<Integer> min = numbers.stream().reduce((x, y) -> x < y ? x:y);

 

5.7 숫자형 스트림

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

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

위 코드는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다. 다음 코드처럼 직접 sum 메서드를 호출할 수 있다면 더 좋지 않을까?

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

하지만 위 코드처럼 sum 메서드를 직접 호출할 수 없다. map 메서드가 Stream<T>를 생성하기 때문이다. 스트림 요소 형식은 Integer지만 인터페이스에는 sum 메서드가 없다.

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

 

5.7.1 기본형 특화 스트림

자바 8에서는 세 가지 기본형 특화 스트림을 제공한다. 스트림 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();

IntStream의 sum 메서드는 스트림이 비어있으면 기본값 0을 반환한다.

IntStream은 max, min, average 등 다양한 유틸리티 메서드도 지원한다.

 

객체 스트림으로 복원하기

boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다.

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


기본값 : OptionalInt

Optional을 Integer, String 등의 참조 형식으로 파라미터화할 수 있다. 또한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다.

다음처럼 OptionalInt를 이용해서 IntStream의 최댓값요소를 찾고 최댓값이 없는 상황에 사용할 기본값을 명시적으로 정의할 수 있다.

OptionalInt maxCalories = menu.stream()
			      .maptoInt(Dish::getCalories)
			      .max();

int max = maxCalories.orElse(1);

 

5.7.2 숫자 범위

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

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

rang 메서드는 시작값과 종료값이 결과에 포함되지 않는다.

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

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

 

 

여기서 rangeClosed 대신에 IntStream.range(1, 100)을 사용하면 1과 100을 포함하지 않으므로 짝수 49개를 반환한다.

 

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

`피타고라스 수` 스트림을 만들어 보자!

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)})
                         );

우선 a에 사용할 1부터 100까지의 숫자를 만든다. 그리고 주어진 a를 이용해서 세 수의 스트림을 만든다. 스트림 a의 값을 매핑하면 스트림의 스트림이 만들어진다.

따라서 flatMap 메서드는 생성된 각각의 스트림을 하나의 평준화된 스트림으로 만들어준다. 결과적으로 세 수로 이루어진 스트림을 얻을 수 있다.

 

개선할 점?

위 코드에서는 제곱근을 두 번 계산한다. 따라서 (a*a, b*b, a*a+b*b) 형식을 만족하는 세 수를 만든 다음에 우리가 원하는 조건에 맞는 결과만 필터링하는 것이 더 최적화된 방법이다.

Stream<double[]> pythagoreanTriples =
                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 스트림 만들기

이 절에서는 일련이 값, 배열, 파일, 심지어 함수를 이용한 무한 스트림 만들기 등 다양한 방식으로 스트림을 만드는 방법을 설명한다.

 

5.8.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.8.2  null이 될 수 있는 객체로 스트림 만들기

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

예를 들어 System.getProperty는 제공된 키에 대응하는 속성이 없으면 null을 반환한다. 이런 메소드를 스트림에 활용하려면 다음처럼 null을 명시적으로 확인해야했다.

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

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)));

 

5.8.3 배열로 스트림 만들기

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

int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();

 

5.8.4 파일로 스트림 만들기

파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(비블록 I/O)도 스트림 API를 활용할 수 있도록 업데이트 되었다.

long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Path.get("data.txt"), Charset.defaultCharset())) { // 스트림은 자원을 자동으로 해제할 수 있는 AutoCloseable이므로 try-finally가 필요 없다.
		uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) // 고유 단어 수 계산
				   .distinct() // 중복 제거
                	       	   .count(); // 단어 스트림 생성
}
catch (IOException e) {
}

 

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

스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다.

두 연산을 이용해서 무한 스트림(infinite stream), 즉 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과 달리 크기가 고정되지 않은 스트림을 만들 수 있다.

iterate와 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);

 

generate 메서드

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

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

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