이번 장에서는 스트림 API가 지원하는 다양한 연산들을 살펴본다. 스트림을 어떻게 활용하는지 중점적으로 보자.
스트림 인터페이스는 filter 메서드를 지원한다.
아래 예시는 filter 메서드를 활용한 문장이다.
List<Dish> vegetarianMenu = menu.stream() .filter(Dish::isVegetarian) .collect(toList());
boolean을 반환하는 isVegetarian에 부합하는 모든 스트림을 반환한다.
distinct는 고유 요소로 이루어진 스트림을 반환한다.(중복 제거)
아래는 리스트의 모든 짝수를 선택하고 중복을 필터링한다.
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
칼로리가 320이하인 음식만 선택하는 예시
List<Dish> slicedMenu1 = specialMenu.stream() .takeWhile(dish -> dish.getCalories() < 320) //Predicate을 각 요소에 적용해 일치 하지 않는 것이 나오면 멈춤 .collect(toList());
위 takeWhile과 정반대의 작업 예시
List<Dish> slicedMenu2 = specialMenu.stream() .dropWhile(dish -> dish.getCalories() < 320) //Predicate을 각 요소에 적용해 거짓이 될 시, 나머지 요소 반환 .collect(toList());
300칼로리 이상 3개의 요리를 선택하는 예시
List<Dish> dishes = specialMenu.stream() .filter(dish -> dish.getCalories() > 300) .limit(3) //스트림의 크기를 3으로 축소, 일치하는 처음 3개의 요소 선택 .collect(toList());
처음 n개 요소 제외 스트림 반환
- n개 이하 요소 포함하는 스트림에 skip(n) 적용 시 빈 요소 반환
300칼로리 이상 처음 두 요리를 건너뛰고 300칼로리가 넘는 나머지 요리 반환하는 예시
List<Dish> dishes = menu.stream() .filter(d -> d.getCalories() > 300) .skip(2) //처음 두개 건너뛰기 .collect(toList());
요리명을 추출하는 예시
List<String> dishNames = menu.stream() .map(Dish::getName) //함수를 인자로 받아 함수를 적용한 결과가 새로운 요소로 반환 .collect(toList());
이해가 잘 되지 않으니 예시를 보자.
예를 들어, ["Hello", "World"] 리스트가 있고, 이를 고유 문자로 이루어진 리스트로 반환해보자. 결과는 ["H", "e", "l", "o", "W", "r", "d"]가 나와야 한다.
만약 map을 사용할 시,
words.stream() .map(word -> word.split("") .distinct() .collect(toList());
위 코드의 문제점이 무엇일까?
- map으로 전달된 람다는 각 단어의 String[](문자열 배열)을 반환한다.
- map 메소드가 반환한 스트림의 형식은 Stream<String[]>이다.
- 우리가 원하는 것은 Stream<String>이다.
- 즉, "Hello", "World" 각각 Stream[]이 반환된다.
따라서 flatMap을 사용하여 문제를 해결해보자.
List<string> uniqueCharacters = words.stream() .map(word -> word.split("")) //각 단어를 개별 문자를 포함하는 배열로 변환 .flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화 .distinct() .collect(toList());
특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.
채식 요리가 있는지 확인하는 예시
if(menu.stream().anyMatch(Dish::isVegetarian)) { System.out.println("this menu is vegetarian"); }
위 세개의 메서드는 쇼트서킷 기법이다.
현재 스트림에서 임의의 요소를 반환한다.
쇼트서킷을 이용해 결과를 찾는 즉시 실행 종료
채식 요리 선택 예시
Optional<Dish> dish = menu.stream() .filter(Dish::isVegetarian) .findAny(); //최종연산 findAny()
위 예시에서 사용된 Optional은 무엇일까?
findAny는 아무 요소도 반환하지 않을 수도 있다.(null)
null은 쉽게 에러를 일으키므로 Optional은 값이 존재하는지 확인하고 값이 없을때 어떻게 처리할지 강제하는 기능을 제공한다.
Optional 클래스는 뒤에서 자세하게 다룬다고 한다
지금까지 살펴본 최종 연산은 boolean, void, Optional 등을 반환했다. 또한 collect로 모든 스트림 요소를 리스트로 모으는 방법도 살펴 보았다.
하지만 '메뉴의 모든 칼로리의 합계를 구하시오' 같이 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 것은 어떻게 할까?
이러한 질의를 수행하려면 Integer 같은 결과가 나올때까지 스트림의 요소를 반복적으로 처리해야한다. 이런 질의를 리듀싱 연산이라고한다.
리듀싱을 살펴보기 전에 for-each 루프를 이용해 숫자 요소를 더하는 코드를 보자
int sum = 0; for (int x : numbers) { sum += x; }
위 코드를 리듀싱 연산으로 간단히 할 수 있다.
int sum = numbers.stream().reduce(0, (a,b) -> a+b);
초기값 0부터 값이 누적되어 더해진다.
reduce는 두개의 파라미터를 가진다.
이렇듯 스트림이 하나의 값으로 줄어들 때까지 람다는 각 요소를 반복해서 조합한다.
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
reduce는 이와 같이 어떤 람다를 넘겨주는지에 따라 최대값, 최소값등 다양하게 값을 구할 수 있다.
중요한 것은 reduce 연산은 새로운 값을 이용해 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행한다는 것이다.
map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다. 따라서 이들은 내부 결과를 저장할 필요가 없으므로 내부 상태를 가지지 않는 연산이다.
하지만 reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다. 스트림에서 처리하는 요소 수와 관계 없이 내부 상태의 크기는 한정적이다.
sorted나 distinct도 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 한다. 예를 들어 어떤 요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 따라서 이들은 내부 상태를 갖는 연산이다.
앞에서 reduce 메서드로 요소의 합을 구하는 예제를 보았다. 예를 들어 다음처럼 메뉴의 칼로리 합계를 구할 수 있다.
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
//reduce 이용, 초기값 0 부터 합계 구하기(메서드 참조)
사실 위 코드에는 박싱 비용이 숨겨져 있다.
내부적으로 합계를 계산하기 전 Integer를 기본형으로 언박싱 해야한다.
다음처럼 sum 메서드를 직접 호출할 수 있다면 좋겠지만 map의 반환값이 Stream이기 때문에 그럴 수 없다.
int calories = menu.stream()
.map(Dish::getCalories)
.sum();
//map 반환이 stream이라 직접 호출 불가능
따라서 스트림 API는 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.
자바 8에서는 박싱 비용을 피할 수 있도록 3가지 기본형 특화 스트림을 제공한다.
각각의 인터페이스는 다음과 같은 메서드도 지원한다.
주의할점은 특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련이 있고 따로 추가 기능을 제공하진 않는다는 것이다.
스트림을 특화 스트림으로 변환할 때 사용하는 함수들은 다음과 같다.
이들은 map과 정확히 같은 기능을 수행하지만, Stream 대신 특화된 스트림을 반환한다.
int calories = menu.stream() //Stream<Dish> 반환
.mapToInt(Dish::getCalories)
//IntStream 반환
.sum(); //IntStream이라 sum 사용 가능
숫자 스트림을 만든 다음, 원상태인 특화되지 않은 스트림으로 복원할 수 있다.
예를 들어, IntStream은 기본형의 정수값만 만들 수 있다. 따라서 int를 인수로 받아 int를 반환하는 람다를 인수로 받는다.
하지만 int가 아닌 Dish 같은 다른 값을 반환하고 싶을때 boxed 메소드를 사용한다.
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); //숫자 스트림 변환
Stream<Integer> stream = intStream.boxed(); //숫자 스트림을 스트림으로 변환
합계에서는 0이라는 기본값이 있었으므로 별 문제가 없었다. 하지만 최대값을 찾을때는 0이라는 기본값 때문에 잘못된 결과가 도출될 수 있다.
이런 상황에서 OptionalInt를 사용해 최대값이 없는 상황에서 사용할 기본값을 명시적으로 정의할 수 있다.(OptionalDouble, OptionalLong 도 지원)
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(1); //orElse 메소드 이용해 기본값 지정
특정 범위의 숫자를 이용해야 하는 상황에서 IntStream, LongStream에서는 range와 rangeClosed라는 두가지 정적 메서드를 제공한다.
두 함수 모두 첫번째 인수로 시작값, 두번째 인수로 종료값을 갖는다.
rangeClosed는 시작값, 종료값이 결과에 포함되고
range는 시작값, 종료값이 결과에 표함되지 않는다.
IntStream evenNumbers = IntStream.rangeClosed(1, 100) //[1,100]
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count()); // 50개의 짝수
IntStream evenNumbers = IntStream.range(1, 100) //(1,100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count()); // 49개의 짝수
임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해 스트림을 만들 수 있다.
Stream<String> stream = Stream.of("Morder", "java", "In", "action");
//Stream.of를 이용해 스트림 생성
stream.map(String::toUpperCase)
.forEach(System.out::println);
또한 다음과 같이 스트림을 비울 수 있다.
Stream<String> emptyStream = Stream.empty();
때로는 null이 될 수 있는 객체를 스트림(객체가 null이라면 빈 스트림)으로 만들어야 할 수도 있다.
이럴때는 Stream.ofNullable을 이용한다.
String homeValue = System.getProperty("home");
//getProperty는 키에 대응하는 속성이 없으면 null 반환
Stream<String> homeValueStream
= homeValue == null ? Stream.empty() : Stream.of(value);
//위 코드를 아래와 같이 구현 가능하다.
Stream<String> homeValueStream
= Stream.ofNullable(System.getProperty("home"));
//값이 없다면 빈스트림으로 만듦
배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해 스트림을 만들 수 있다.
예시로 다음처럼 기본형 int로 이루어진 배열을 IntStream으로 변환할 수 있다.
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
//IntStream 반환
먼저 iterate를 사용하는 방법을 살펴보자
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
또한 Java 9 iterate 메서드는 Predicate를 지원한다. Predicate을 이용하여 언제까지 작업을 수행할 것인지 지정할 수 있다.
IntStream.iterate(0, n -> n < 100, n -> n + 4).forEach(System.out::println);
//Predicate 이용해서 언제까지 작업할 것인지 지정
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
generate는 상태가 없는 메서드, 즉 나중에 계산에 사용할 어떤 값도 저장해두지 않는다.
물론 상태를 저장하게 할 수 있지만 병렬성 문제와 더불어 권장되지 않는다.
무한한 크기를 가진 스트림을 처리하므로 limit을 이용해 명시적으로 스트림의 크기를 제한해야 한다.
제한하지 않을 시, 최종 연산을 수행하였을 때 아무 결과도 계산되지 않는다.
무한 스트림의 요소는 계산이 반복되므로 정렬하거나 reduce 할 수 없다.