모던 자바 인 액션 5장을 학습하고 정리한 내용입니다.
데이터를 어떻게 처리할지는 스트림 API가 알아서 관리, 최적화한다.
스트림 API는 내부 반복 뿐 아니라, 코드를 병렬로 실행하지 여부도 결정할 수 있다.
스트림 인터페이스는 filter
메서드를 제공한다.
필터 메서드는 Predicate
를 파라미터로 받아서, 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
List<Dish> vegeDishes = dishes.stream()
.filter(Dish::isVegetable)
.collect(toList());
스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct
메서드를 제공한다.
고유 여부는 stream에서 만든 객체의 hashcode
, equals
로 결정된다.
List<Integer> numbers = List.of(1,2,2,3,4,4,4);
List<Integer> uniques = numbers.stream() // [1, 3]
.distinct()
.collect(toList());
Arrays.stream("hello".split("")).distinct().collect(Collectors.toList()); // [h, e, l, o]
자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile
, dropWhile
두 가지 메서드를 제공한다.
이미 칼로리 기준으로 정렬된 음식 목록에서, 칼로리가 320 이하인 음식들만 뽑아내고 싶다.
List<Dish> filtered = dishes.stream()
.filter(dish -> dish.getCalories() <= 320)
.collect(toList());
그런데 이미 칼로리 기준으로 정렬되어
있으므로, 최초로 320을 넘기는 음식 이전까지만 확인하면 된다.
이때 takeWhile
을 사용한다.
List<Dish> filtered = dishes.stream()
.takeWhile(dish -> dish.getCalories() <= 320)
.collect(toList());
~하는 동안(while) 취하자(take)
dropwhile
은 takeWhile
과 반대로 작동한다.
고로 dropWhile을 사용하면, 320칼로리 초과의 음식만 선택한다.
~하는 동안(while) 버리자(drop)
List<Dish> filtered = dishes.stream()
.filter((dish) -> dish.getCalories() < 320)
.limit(3)
.collect(Collection.toList());
조건에 일치하는 요소를, 앞에서부터 일치하는 대로 개수를 제한해서 불러온다.
List<Dish> filtered = dishes.stream()
.filter((dish) -> dish.getCalories() < 320)
.skip(3) // 조건에 일치하는 요소가 3개가 될 때 까지 생략, 그 후에 일치하는 요소들만 필터링
.collect(Collection.toList());
조건에 일치하는 요소를, 앞에서부터 일치하는 대로 생략한 뒤 진행함.
List<Dish> names = dishes.stream()
.map(Dish::getName)
.collect(toList());
정확히 이해한 다음에 다시 작성하기로....
Boolean hasAnyVegetarianDish = dishes.stream().anyMatch(Dish::isVegetarian);
// 모든 요리가 채식주의자용인가
Boolean isAllVegetarianDish = dishes.stream().allMatch(Dish::isVegetarian);
// 모든 요리가 채식주의자용이 아닌가
Boolean isAllNotVegetarianDish = dishes.stream().noneMatch(Dish::isVegetarian);
// 스트림에서 무작위 요소를 Optional로 감싸서 반환
Optional<Dish> dish = dishes.stream()
.filter(Dish::isVegetarian)
.findAny();
if (dish.isPresent()) {
sout(...);
}
// ifPresent로 합치기
dishes.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(dish -> sout(dish.getName()));
// 값이 존재하지 않으면 아무일도 일어나지 않는다.
// 스트림은 생성할 때 사용한 리스트, 또는 정렬된 데이터의 순서를 따른다.
// 그런 상황에서 첫 번째 요소를 찾으려면 findFirst를 사용하자
List<Integer> numbers = List.of(1,4,9,12,25);
Optional<Integer> 3의배수 = numbers.stream()
.filter(num -> num % 3 == 0)
.findFirst(); // 12를 찾는다.
anyMatch, allMatch, noneMath, findAny, findFirst
등은 Short curcuit를 사용한다.
즉, 스트림을 처리하다 조건을 만족하지 않는 요소를 발견하면 그 즉시 실행을 종료한다.
병렬 실행에서는 첫 번째 요소를 찾기 어렵다. (병렬적으로 처리하다보니, 뭐가 처음인지 알 수 없다)
그러므로 반환하는 요소의 순서가 중요하지 않은 경우에는 findAny
를 사용하자
List<Integer> numbers = List.of(1,2,3,4);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
int product = numbers.stream()
.reduce(0, (a, b) -> a * b);
// 메서드 참조를 사용한 개선
int sum2 = numbers.stream()
.reduce(0, Integer::sum);
reduce
는 두 개의 파라미터를 사용한다.
초깃값 (위 경우에는 0)
두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>
sum의 reduce
는 내부적으로 다음과 같이 동작한다.
a | b | 스트림 숫자 |
---|---|---|
0 | 1 | |
1 | ||
1 | 2 | |
3 | ||
3 | 3 | |
6 | ||
6 | 4 | |
10 |
초기값을 사용하지 않는 reduce
도 존재한다.
Optiona<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
스트림이 비어있는데, 초깃값까지 없으면 결과를 반환할 수 없다. 그러므로 Optional<T>
를 반환한다.
reduce
를 활용해서 최댓값, 최솟값을 구할 수 있다.
Optional<Integer> max = numbers.stream()
.reduce((a, b) -> a > b ? a : b);
Optional<Integer> min = numbers.stream()
.reduce((a, b) -> a < b ? a : b);
// 메서드 참조를 통한 개선
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
Optional<Integer> min = numbers.stream()
.reduce(Integer::min);
max의 reduce
는 내부적으로 다음과 같이 동작한다.
a | b | 스트림 숫자 |
---|---|---|
x | 1 | |
Optional(1) | ||
Optional(1) | 2 | |
Optional(2) | ||
Optional(2) | 3 | |
Optional(3) | ||
Optional(3) | 4 | |
Optional(4) |
int sum = dishes.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
이렇게 하면 sum을 구할 수 있는데, 문제는 Integer::sum
을 사용할 때 boxing 비용이 발생한다.
기본형 특화 스트림 (primitive stream specialization)
을 사용하면 효율적으로 처리할 수 있다.
스트림 API는 boxing비용을 피할 수 있는 IntStream
, DoubleStream
, LongStream
을 제공한다.
스트림을 특화 스트림으로 변환할 때 mapToInt
, mapToDouble
, mapToLong
메서드를 주로 사용한다.
int calories = dishes.stream()
.mapToInt(Dish::getCalories) // IntStrea을 반환
.sum(); // IntStream이 제공하는 메서드
스트림이 비어있으면 기본값 0
을 반환한다.
특화 스트림을 다시 기본 스트림으로 변환할 수도 있다.
IntStream intStream = dishes.stream().mapToInt(Dish::getCalories); // 특화 스트림으로 변환
Stream<Integer> stream = intStream.boxed(); // 기본 스트림으로 변환
int calories = dishes.stream()
.mapToInt(Dish::getCalories) // IntStrea을 반환
.sum(); // IntStream이 제공하는 메서드
위의 코드는 스트림이 비어있는 경우 기본값 0을 제공한다. 그런데 sum이 아니고 max를 구하는 상황에서는 기본값 0이 사용되면 곤란하다. OptionalInt
, OptionalDouble
, OptionalLong
을 사용해서 해결할 수 있다.
OptionalInt optional = dishes.stream()
.mapToInt(Dish::getWeight)
.max();
int max = optional.orElse(1); // optional이 empty인 경우 기본값 1을 사용
int max2 = optional.getAsInt(); // 값 꺼내오기
특정 범위의 숫자를 이용해야하는 경우에는 기본형 특화 스트림의 range
, rangeClosed
라는 static 메서드를 사용하면 된다.
// range: (n, m)
// rangeClosed: [n, m]
IntStream evenNumbers = IntStream.rangeClosed(1, 100) // 1 이상 100 이하의 값
.filter(n -> n % 2 ==0);
int count = evenNumbers.count() // 짝수 50개
생략
컬렉션에서, 범위의 숫자에서 스트림을 만드는 것을 확인했다. 이뿐만 아니라 다양한 방식으로 스트림을 만들 수 있다.
임의의 수를 인수로 받는 정적 메서드 Stream.of
를 이용해서 스트림을 만들 수 있다.
Stream<String> stream = Stream.of("a", "b", "c");
Stream<String> emptyStream = Stream.empty(); // 비어있는 스트림 생성
객체가 null이면 비어있는 스트림을 만들 수 있다.
Address address = user.getAddress() // address가 null일 수도 있는 상황
Stream<Address> stream = address == null ? Stream.empty() : Stream.of(address);
// Stream.ofNullable을 통해 개선 가능
Stream<Address> stream = Stream.ofNullable(address);
Integer[] numbers = List.of(1,2,3,4,5);
Stream<Integer> stream = Arrays.stream(numbers);
생략
크기가 고정되지 않은 스트림을 만들 수 있다.
Stream.iterate(0, n -> n + 2)
.limit(5);
.Collect(toList()); // {0, 2, 4, 8, 10}
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1); // laziness 특성 때문에 아직은 무한히 돌지 않음
List<Integer> infiniteNumbes = infiniteStream.collect(Collectors.toList()); // 스트림을 사용하는 순간 무한 루프 (java.lang.OutOfMemoryError)
iterate
는 초기값(0)과 UnaryOperator<T>
를 사용해서 새로운 값을 끊임없이 생성한다.
보통 limit
를 통해 개수를 제한한다.
이러한 무한 스트림을 unbound stream
이라고 한다.
자바 9의 iterate는 Predicate를 지원한다.
IntStream.iterate(0, n -> n < 100, n -> n + 4)
.forEach(System.out::println)// 100보다 작을 때 까지 n + 4를 출력 (0, 4, 8, 12, ...)
// 다른 표현 방법
IntStream.iterate(0, n -> n + 4)
.takeWhile(n -> n < 100)
.forEach(System.out::println);
추후 진행