Stream의 객체를 구성하고자 할 때 "Stream 생성 → 중간 연산→ 최종 연산"의 세 단계의 과정을 통해서 Stream의 구성이 이루어집니다.
이번 포스팅에서는 Stream 생성 후 생성된 스트림을 필터링하거나 원하는 형태에 알맞게 가공하는 연산을 하는 과정인
Stream 중간 연산을 통해 변환된 Stream의 각 요소를 소모하여 결과 Stream을 생성하는 최종 연산에 대해 알아보겠습니다.
Stream 최종 연산이란❓
Stream 최종 연산은 지연 평가(Lasy Evaluation)되었던 중개 연산들이 최종 연산에 모두 수행됩니다.
최종연산을 하고 나면 해당 Stream은 닫히게 되고 다시 사용할 수 없습니다. 즉 재사용 불가.
Stream API에서 사용할 수 있는 대표적인 최종연산 메서드는 다음과 같습니다.
1. 요소의 출력 (Iterating) ➡️ forEach()
2. 요소의 소모 (Reduction) ➡️ reduce()
3. 요소의 검색 (Searching) ➡️ findFirst(), findAny()
4. 요소의 검사 (Matching) ➡️ anyMatch(), allMatch(), noneMatch()
5. 요소의 연산 (Calculating) ➡️ sum(), average(), count(), min(), max(),
6. 요소의 수집 (Collecting) ➡️ collect()
Stream 요소의 출력
forEach()
void forEach(Consumer<? super T> action);
forEach() 메서드는 배열 혹은 리스트를 순회하면서 실행되는 최종연산입니다.
반환 타입이 void로 보통 모든 요소를 출력하거나 값을 새로운 형태로 변환하여 구성하기 위한 목적으로 사용됩니다.
요소 출력
Stream<String> stringStream = Stream.of("a","b","c");
stringStream.forEach(System.out::println);
// a
// b
// c
Array
int[] forEachIntArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
List<Integer> forEachIntList = new ArrayList<>();
Arrays.stream(forEachIntArr).forEach(item -> {
if (item % 2) {
forEachIntList.add(item);
}
});
System.out.println(forEachIntList.toString()); // 결과 : [2, 4, 6, 8, 10]
Stream 요소의 소모
reduce()
reduce() 메소드는 Stream의 요소를 소모하여 줄여나가면서 연산을 수행하고 최종 결과를 한 개를 반환합니다.
총 3 개의 파라미터를 받을 수 있으며 각 파라미터는 다음과 같습니다.
// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);
// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);
// 3개 (combiner)
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
- accmulator: 누적 함수를 사용하여 스트림의 각 요소를 처리하여 하나의 결과로 축소하는 계산로직.
- combiner : 병렬 스트림에서 나눠 계산한 결과를 하나로 합치는 로직.
- identity : 요악되는 값의 초기값
인자가 하나만 있는 경우
OptionalInt reduced =
IntStream.range(1, 5) // [1, 2, 3, 4]
.reduce((a, b) -> {
return Integer.sum(a, b); // 10
});
reduce() 메서드의 인자가 하나라면, 주어진 람다식을 수행한 결과를 반환합니다.
주어진 스트림이 비어있을 수 도 있기 때문에 Optional객체로 리턴을 합니다.
인자가 두 개 있는 경우
List<Integer> num = Arrays.asList(1, 2, 3, 4, 5, 6, -1);
Integer sum = num
.stream()
.reduce((x,y -> x + y)
.get();
System.out.println(sum); // 20
reduce() 메서드의 인자가 두 개라면, 맨 앞에 있는 인자는 초기 값(Identity)입니다.
주어진 스트림이 만약에 비어있더라도 초기 값을 설정했기 때문에 기본 타입으로 반환합니다.
인자가 세 개있는 경우
Integer reducedParams = Stream.of(1, 2, 3)
.reduce(10, // identity
Integer::sum, // accumulator
(a, b) -> {
System.out.println("combiner was called");
return a + b;
});
파라미터 combiner은 병렬 처리 시 각각의 Thread에서 실행한 결과를 하나로 합치는 로직이라고 했습니다.
그렇기 때문에 병렬 스트림에서만 동작하며 위와같은 코드에서는 병렬스트림이 아니기 때문에 Combiner 관련 출력이 찍히지 않아 다음과 같이 결과가 나옵니다.
16
병렬적으로 처리하기위해 Parallel() 메소드를 사용하면 다음과 같습니다.
int reduced = Stream.of(1, 2, 3)
.parallel()
.reduce(10, Integer::sum, (a, b) -> {
System.out.println("combiner was called");
return a + b;
});
parallel() 메서드를 추가하여 해당 코드를 실행하면 Combiner는 2번 호출되고, 총합의 결과 reduced는 36이 됩니다.
왜냐면 reduce 로직이 parallel() 메서드로 인해 병렬로 실행되는데, 초기값 10 역시 병렬로 갖게 되기 때문입니다.
병렬로 더해진 각각의 값은 11(10+1), 12(10+2), 13(10+3) 이 되고, Combiner가 이를 합치게 된다. Combiner는 역순으로 12+13 = 25를 먼저 더하고, 그다음 25+11 = 36을 더하여 총 2번 호출되며 최종적으로 36을 반환하게 됩니다.
int reduced = 10 + Stream.of(1, 2, 3)
.parallel()
.reduce(0, Integer::sum, (a, b) -> {
System.out.println("combiner was called");
return a + b;
});
그렇기 때문에 위와같이 전체 연산에서 한 번만 초기값이 필요한 경우에는 연산된 결과에 10을 더해줘야 합니다.
Stream 요소의 검색
Stream 요소에 지정된 요소가 조건에 맞는지 체크하는 검색 메소드는 findFirst(), findAny()가 있습니다.
findFirst()
Stream<Integer> num = Stream.of(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> first = num.filter((item) -> item > 4).findFirst();
System.out.println(fist.get()) // 결과 : 5
findFirst()는 스트림 내에서 조건에 일치하는 요소중에서 가장 앞(First)에 있는 요소를 참조하여 Optional 객체를 리턴해주는 함수입니다.
findAny()
Stream<Integer> num = Stream.of(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> any = num.filter((item) -> item > 4).findAny();
System.out.println("any = " + any.get()); // 결과 : 5
findAny()는 해당 스트림에서 가장 먼저 탐색되는 아무 요소나 참조하여 Optional 객체를 리턴해주는 함수입니다.
병렬 스트림인 경우에는 findAny() 메서드를 사용해야 정확하며, 병렬 스트림이 아닌 경우에는 findFirst() 메서드처럼 스트림의 첫 번째 요소를 리턴할 가능성이 높지만, 이에 대한 보장은 없습니다.
findFirst()와 findAny() 모두 비어있는 스트림의 경우, 비어있는 Optional 객체를 리턴합니다.
findFirst() vs findAny()
두 메소드 모두 직렬로 처리할 때는 대부분 조건을 만족하는 첫 번째 요소를 리턴 하지만 병렬로 처리할 때는 차이가 있습니다.
findFirst()는 여러 요소가 조건에 부합해도 Stream의 순서를 고려하여 가장 앞에 있는 요소를 리턴합니다.
findAny()는 Multi thread에서 Stream을 처리할 때 가장 먼저 찾은 요소를 리턴합니다. 따라서 Stream의 뒤쪽에 있는 element가 리턴될 수 있습니다.
List<String> elements = Arrays.asList("a", "a1", "b", "b1", "c", "c1");
Optional<String> firstElement = elements.stream().parallel()
.filter(s -> s.startsWith("b")).findFirst();
System.out.println("findFirst: " + firstElement.get());
위 코드는 Stream을 병렬로 처리하는 findFirst() 코드입니다. 이 때 findFirst()는 항상 조건을 만족하는 첫 번째 요소인 b를 리턴합니다.
List<String> elements = Arrays.asList("a", "a1", "b", "b1", "c", "c1");
Optional<String> anyElement = elements.stream().parallel()
.filter(s -> s.startsWith("b")).findAny();
System.out.println("findAny: " + anyElement.get());
위 코드는 Stream을 병렬로 처리하는 findAny() 코드입니다.
이 때 findAny()는 실행할 때마다 리턴값이 달라지며 b1, 또는 b를 리턴합니다.
이는 아래의 각 Stream의 특징에 이유가 있습니다.
- 순차 처리 스트림(sequential)
-
parallel() 메서드를 이용하지 않은방식으로 싱글 쓰레드내에서 순차적으로 스트림 내의 처리를 수행합니다. - 해당 처리 방식은 싱글 스레드이기 때문에 처리속도는 상대적으로 느리지만 순서를 보장한다는 점이 있습니다.
-
- 병렬 처리 스트림 (Parallel)
- parallel() 메서드를 사용하여 멀티 쓰레드 내에서 작업을 처리를 수행합니다.
- 이는 병렬로 처리를 하게 되면 여러 스레드가 있기에 빠르게 처리를 하지만 순서를 보장하지 않는 점이 있습니다.
Stream 요소의 검사
Stream 요소의 검사 (Matching)은 람다식 Predicate를 받아 특정한 조건을 충족하는지 검사하고 결과를 boolean 타입으로 리턴합니다.
anyMatch()
Stream<Integer> integerStream2 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8);
boolean isAnyMatch = integerStream2.anyMatch((i) -> i <= 1);
System.out.println("isAnyMatch = " + isAnyMatch); // 결과 true
anyMatch() 메서드는 스트림의 일부 요소가 특정 조건을 만족하면 true값을 반환합니다.
allMatch()
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8);
boolean isAllMatch = integerStream.allMatch((item) -> item < 8);
System.out.println("isAllMatch = " + isAllMatch); // 결과 : false
allMatch() 메서드는 스트림의 모든 요소가 특정 조건을 만족하면 true값을 반환합니다.
noneMatch()
Stream<Integer> integerStream3 = Stream.of(1, 2, 3, 4, 5);
boolean isNoneMatch = integerStream3.noneMatch((item) -> i > 5);
System.out.println("isNoneMatch = " + isNoneMatch); // 결과 : true
noneMatch() 메서드는 스트림의 모든 요소가 특정 조건을 만족하지 않을 때 true값을 반환합니다.
Stream 요소의 통계
sum()
int sum = IntStream.of(1,2,3,4,5,6,7,8,9,10) ;
System.out.println(sum); // 결과 : 55
sum() 메서드는 스트림 내의 요소의 모든 요소를 더한 총합을 int 타입으로 리턴하는 함수입니다.
count()
long cnt = DoubleStream.of(1.2, 1.3, 1.4).count();
System.out.println(cnt); // 결과 : 3
count() 메서드는 스트림 내의 요소의 총개수를 long 타입으로 리턴하는 함수입니다.
sum() 메서드와 count() 메서드는 Stream이 비어있든 없든 상관없기 때문에Optional 객체가 아닌기본 타입으로 결과를 리턴합니다.
min()
int[] arr = {1000, 2000, 3000, 5000};
int result = Arrays.stream(arr).min().getAsInt();
System.out.println(result); // 결과 : 1000
min() 메서드는 스트림 내의 각 요소 중 최소 값을 참조하는 Optional객체를 반환하는 함수입니다.
max()
int[] arr = {1000, 2000, 3000, 5000};
int maxResult = Arrays.stream(arr).max().getAsInt(); // 결과 5000
max() 메서드는 스트림 내의 각 요소 중 최댓값을 참조하는 Optional객체를 반환하는 함수입니다.
average()
int[] arr = {1000, 2000, 3000, 5000};
double arrAvg = Arrays.stream(arr).average().getAsDouble();
System.out.println(arrAvg); // 결과 : 2750.0
List<String> arrStr = new ArrayList<>(Arrays.asList("kim", "lee", "park", "lee", "jung", "jin"));
double strLengthAvg = arrStr.stream().mapToInt(String::length).average().getAsDouble();
System.out.println(strLengthAvg); // 결과 : 3.6666666666666665
average() 메서드는 스트림 내의 요소들의 평균값을 참조하는 Optional객체를 반환하는 함수입니다.
max(), min(), average() 이 3개의 메소드는 모두 Stream이 비어있을 결과를 만들 수 없으므로 기본적으로 Optional 객체로 리턴합니다.
값을 얻고 싶다면 getAsXXX() 메서드를 사용해야 합니다.
ex) getAsInt(), getAsDouble()
그게 아니라면 ifPresent() 메서드를 사용하여 바로 Optional 객체를 처리할 수 도 있습니다.
Collect() 메서드는 양이 많아 다음 포스팅에서 알아보겠습니다.
'Language > Java' 카테고리의 다른 글
[JAVA] Stream API에 대해 알아보기 _ Stream 최종 연산 Collect()(집계) (5/5) (1) | 2024.05.22 |
---|---|
[JAVA] Optional 클래스에 대해 알아보기 (0) | 2024.05.19 |
[JAVA] Stream API에 대해 알아보기 _ Stream 중간 연산(가공) (3/5) (0) | 2024.05.17 |
[JAVA] Stream API에 대해 알아보기 _ Stream 생성 (2/5) (0) | 2024.05.16 |
[JAVA] Stream API에 대해 알아보기 (1/5) (0) | 2024.05.15 |