모던 자바 인 액션 5장: 스트림 활용

dev_314·2023년 1월 8일
0

모던 자바 인 액션

목록 보기
5/6

모던 자바 인 액션 5장을 학습하고 정리한 내용입니다.

5장 스트림 활용

데이터를 어떻게 처리할지는 스트림 API가 알아서 관리, 최적화한다.
스트림 API는 내부 반복 뿐 아니라, 코드를 병렬로 실행하지 여부도 결정할 수 있다.

5.1 필터링

5.1.1 Predicate로 필터링

스트림 인터페이스는 filter메서드를 제공한다.
필터 메서드는 Predicate를 파라미터로 받아서, 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

List<Dish> vegeDishes = dishes.stream()
								.filter(Dish::isVegetable)
                                .collect(toList());

5.1.2 고유 요소 필터링 (distinct)

스트림은 고유 요소로 이루어진 스트림을 반환하는 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]

5.2 스트림 슬라이싱

5.2.1 Predicate를 이용한 슬라이싱 (takeWhile, dropWhile)

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

takeWhile

이미 칼로리 기준으로 정렬된 음식 목록에서, 칼로리가 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

dropwhiletakeWhile과 반대로 작동한다.
고로 dropWhile을 사용하면, 320칼로리 초과의 음식만 선택한다.

~하는 동안(while) 버리자(drop)

5.2.2 스트림 축소 (limit)

List<Dish> filtered = dishes.stream()
								.filter((dish) -> dish.getCalories() < 320)
                                .limit(3)
                                .collect(Collection.toList());

조건에 일치하는 요소를, 앞에서부터 일치하는 대로 개수를 제한해서 불러온다.

5.2.3 요소 건너뛰기 (skip)

List<Dish> filtered = dishes.stream()
								.filter((dish) -> dish.getCalories() < 320)
                                .skip(3) // 조건에 일치하는 요소가 3개가 될 때 까지 생략, 그 후에 일치하는 요소들만 필터링
                                .collect(Collection.toList());

조건에 일치하는 요소를, 앞에서부터 일치하는 대로 생략한 뒤 진행함.

5.3 매핑

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

List<Dish> names = dishes.stream()
							.map(Dish::getName)
                            .collect(toList());

5.3.2 스트림 평면화 (flatmap)

정확히 이해한 다음에 다시 작성하기로....

5.4 검색과 매칭

5.4.1 Predicate가 적어도 한 요소와 일치하는지 확인 (anyMatch)

Boolean hasAnyVegetarianDish = dishes.stream().anyMatch(Dish::isVegetarian);

5.4.2 Predicate가 모든 요소와 일치하는지 검사 (allMatch, noneMatch)

// 모든 요리가 채식주의자용인가
Boolean isAllVegetarianDish = dishes.stream().allMatch(Dish::isVegetarian);
// 모든 요리가 채식주의자용이 아닌가
Boolean isAllNotVegetarianDish = dishes.stream().noneMatch(Dish::isVegetarian);

5.4.3 요소 검색 (findAny, ifPresent)

// 스트림에서 무작위 요소를 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()));
// 값이 존재하지 않으면 아무일도 일어나지 않는다.

5.4.4 첫 번째 요소 찾기 (findFirst)

// 스트림은 생성할 때 사용한 리스트, 또는 정렬된 데이터의 순서를 따른다.
// 그런 상황에서 첫 번째 요소를 찾으려면 findFirst를 사용하자
List<Integer> numbers = List.of(1,4,9,12,25);
Optional<Integer> 3의배수 = numbers.stream()
									.filter(num -> num % 3 == 0)
                                    .findFirst(); // 12를 찾는다.

5.4.a 특징

anyMatch, allMatch, noneMath, findAny, findFirst등은 Short curcuit를 사용한다.
즉, 스트림을 처리하다 조건을 만족하지 않는 요소를 발견하면 그 즉시 실행을 종료한다.

5.4.b findAny vs findFirst

병렬 실행에서는 첫 번째 요소를 찾기 어렵다. (병렬적으로 처리하다보니, 뭐가 처음인지 알 수 없다)
그러므로 반환하는 요소의 순서가 중요하지 않은 경우에는 findAny를 사용하자

5.5 리듀싱

5.5.1 요소의 합 (reduce)

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는 내부적으로 다음과 같이 동작한다.

ab스트림 숫자
01
1
12
3
33
6
64
10

초기값을 사용하지 않는 reduce도 존재한다.

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

스트림이 비어있는데, 초깃값까지 없으면 결과를 반환할 수 없다. 그러므로 Optional<T>를 반환한다.

5.5.2 최댓값과 최솟값

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는 내부적으로 다음과 같이 동작한다.

ab스트림 숫자
x1
Optional(1)
Optional(1)2
Optional(2)
Optional(2)3
Optional(3)
Optional(3)4
Optional(4)

5.5.a 중간 연산, 최종 연산 정리

5.7 숫자형 스트림

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

이렇게 하면 sum을 구할 수 있는데, 문제는 Integer::sum을 사용할 때 boxing 비용이 발생한다.
기본형 특화 스트림 (primitive stream specialization)을 사용하면 효율적으로 처리할 수 있다.

5.7.1 기본형 특화 스트림

스트림 API는 boxing비용을 피할 수 있는 IntStream, DoubleStream, LongStream을 제공한다.

숫자 스트림으로 매핑: mapToInt

스트림을 특화 스트림으로 변환할 때 mapToInt, mapToDouble, mapToLong 메서드를 주로 사용한다.

int calories = dishes.stream()
					.mapToInt(Dish::getCalories) // IntStrea을 반환
                    .sum();  // IntStream이 제공하는 메서드

스트림이 비어있으면 기본값 0을 반환한다.

객체 스트림으로 복원하기: boxed

특화 스트림을 다시 기본 스트림으로 변환할 수도 있다.

IntStream intStream = dishes.stream().mapToInt(Dish::getCalories); // 특화 스트림으로 변환
Stream<Integer> stream = intStream.boxed(); // 기본 스트림으로 변환

기본값: OptionalInt + orElse

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(); // 값 꺼내오기

5.7.2 숫자 범위 (range, rangeClosed)

특정 범위의 숫자를 이용해야하는 경우에는 기본형 특화 스트림의 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개

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

생략

5.8 스트림 만들기

컬렉션에서, 범위의 숫자에서 스트림을 만드는 것을 확인했다. 이뿐만 아니라 다양한 방식으로 스트림을 만들 수 있다.

5.8.1 값으로 스트림 만들기 (Stream.of)

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

Stream<String> stream = Stream.of("a", "b", "c");
Stream<String> emptyStream = Stream.empty(); // 비어있는 스트림 생성

5.8.2 null이 될 수 있는 객체로 스트림 만들기 (Stream.ofNullable)

객체가 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);

5.8.3 배열로 스트림 만들기 (Arrays.stream)

Integer[] numbers = List.of(1,2,3,4,5);
Stream<Integer> stream = Arrays.stream(numbers);

5.8.4 파일로 스트림 만들기

생략

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

크기가 고정되지 않은 스트림을 만들 수 있다.

iterate 메서드로 무한 스트림 만들기

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

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

gnerate 메서드로 무한 스트림 만들기

추후 진행

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글