[자바/Java] 스트림 Stream - 최종 연산
[자바/Java] 스트림 Stream - 중간 연산
https://codingralro.tistory.com/entry/%EC%9E%90%EB%B0%94Java-%EC%8A%A4%ED%8A%B8%EB%A6%BC-Stream-%EC%83%9D%EC%84%B1 [자바/Java] 스트림 Stream - 생성 스트림 (Stream) 이란 자바8에서 등장한 스트림은 람다를 활용할 수 있는 개념
codingralro.tistory.com
저번 글에서는 중간 연산에 대해 공부하였다. 이번 글에서는 스트림의 가장 마지막 단계인 최종 연산에 대해서 알아보겠다.
최종 연산 - 결과 도출
중간 연산을 통해 가공된 데이터 스트림을 최종적으로 사용할 결과값으로 만드는 단계이다. 그렇기 때문에 스트림을 끝내는 최종 연산이라고 한다.
Calculating
최대, 최소, 합 평균 등 계산과 관련된 최종 연산을 수행 후 기본형 타입으로 결과를 만들어 낸다.
public class Main {
public static void main(String[] args) {
//합 - sum() : long을 반환
long sum = IntStream.of(14, 11, 20, 39, 23).sum();
//갯수 - count() : long을 반환
long cnt = IntStream.of(14, 11, 20, 39, 23).count();
//최소, 최대 - min(), max()
//비어 있는 경우, null로 표현 -> Optional 객체를 리턴
//Optional 메소드를 chaining할 수 있음
OptioanlInt min = IntStream.of(14, 11, 20, 39, 23).min();
OptioanlInt max = IntStream.of(14, 11, 20, 39, 23).max();
IntStream.of(14, 11, 20, 39, 23).max().ifPresent(System.out::println); //39
}
}
위의 예제에도 나와있듯이, 각각의 메소드가 어떠한 객체를 반환하는지 알고 있는 것이 스트림에서는 매우 중요하다!!
Reduction
스트림은 reduce라는 메소드를 이용하여 결과를 만들어 낼 수 있다. reduece 메소드는 스트림에서 주요한 메소드 중 하나로 익숙해진다면, 여러 작업을 할 수 있는 메소드이다.
reduce는 총 3가지의 파라미터를 받을 수 있다.
- accumulator : 각 요소를 처리하는 계산 로직, 각 요소가 올 때마다 중간 결과를 생성하는 로직
- identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴
- combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작을 하는 로직
reduce의 동작 방식은 계산 로직을 통해 나온 결과를 다음 계산 로직의 한 인자로서 넘겨주는 방식으로 동작을 한다.
// 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);
public class Main {
public static void main(String[] args) {
//인자가 하나만 있는 경우
OptionalInt reduced = IntStream.range(1, 5) //[1, 2, 3, 4]
.redece((a, b) -> Integer.sum(a, b)); // = .reduce(Integer::sum);
//과정
//1 + 2 = 3 -> 다음 연산식으로 넘김
//3 + 3 = 6 -> 다음 연산식으로 넘김
//6 + 4 = 10
//인자가 두개 있는 경우
OptionalInt reducedTwoParams = IntStream.range(1, 5) //[1, 2, 3, 4]
.redece(10, Integer::sum);
//과정
//10 + 1 = 11 -> 다음 연산식으로 넘김
//11 + 2 = 13 -> 다음 연산식으로 넘김
//13 + 3 = 16 -> 다음 연산식으로 넘김
//16 + 4 = 20
//인자가 세개 있는 겨우
//parallelStream에서만 사용 가능
Integer reduceParallel = Arrays.asList(1, 2, 3, 4)
.parallelStream()
.reduce(10,
Integer::sum,
(a, b) -> {
System.out.println("combine was called");
return a + b;
});
//과정
//10 + 1 , 10 + 2, 10 + 3, 10 + 4 : 동시 진행
//combine was called
//11 + 12 = 23 -> 다음 combiner에게 넘김
//combine was called
//23 + 13 = 36 -> 다음 combiner에게 넘김
//combine was called
//36 + 14 = 50
}
}
병렬 스트림이 무조건 시퀀셜보다 좋은 것이 아니다. 간단한 경우에는 오히려 부가처리(combiner)로 인해 더 느린 경우가 있다.
Collecting
collect 메소드는 또 다른 종료 작업이다. Collector 타입의 인자를 받아서 처리를 하는데, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있다.
이번 예제에서는 아래와 같은 간단한 리스트를 사용한다. Product 객체는 수량과 이름을 가지고 있다.
class Product {
public String name;
public int amount;
public Product(String name, int amount) {
this.name = name;
this.amoount = amount;
}
public String getName() { return this.name; }
public int getAmount() { return this.amount; }
}
public class Main {
public static void main(String[] args) {
List<Product> productList =
Arrays.asList(new Product(23, "potatos"),
Arrays.asList(new Product(14, "orange"),
Arrays.asList(new Product(13, "lemon"),
Arrays.asList(new Product(23, "bread"),
Arrays.asList(new Product(13, "sugar"));
//Collectors.toList()
//스트림에서 작업한 결과를 담은 리스트로 반환
//각 상품의 이름을 담은 리스트를 반환하는 코드
List<String> nameList =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
//[potatos, orange, lemon, bread, sugar]
//Collectors.joining
//스트림에서 작업한 결과를 하나의 문자열로 합칠 수 있다.
//세 개의 인자를 받을 수 있음
//delimeter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
//prefix : 결과 맨 앞에 붙는 문자
//suffix : 결과 맨 뒤에 붙는 문자
String str =
productList.stream()
.map(Product::getName)
.collect(Collectors.joining(", ", "<", "]"));
// <potatos, orange, lemon, bread, sugar]
//Collectors.averageingInt()
//숫자값의 평균을 낸다.
Double avg =
ProductList.stream()
.collect(Collectors.averagingInt(Product::getAmount));
//17.2
//Collectors.summingInt()
//숫자값의 합을 낸다.
Integer sum =
ProdunctList.stream()
.collect(Collectors.summingInt(Product::getAmout));
//mapToInt 메소드 사용시 더 간단하게 표현 가능
Integer sum =
ProdunctList.stream()
.mapToInt(Product::getAmount)
.sum();
//86
//Collectors.summarizingInt()
//합계, 평균 등 여러 정보를 담은 객체를 리턴
IntSummaryStatistics statics =
ProductList.stream()
.collect(Collectors.summarizingInt(Product::getAmount));
//statics { count = 5, sum = 86, min=13, average=17.200000, max=23}
//getCount(), getSum(), getAverage(), getMin(), getMax()
//Collectors.groupingBy()
//특정 조건으로 요소들을 그룹지을 수 있다.
//인자는 함수형 인터페이스인 Function<T, R>이다.
//수량을 기준으로 그룹핑하는 코드
Map<Integer, List<Product>> map =
productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
//{23=[Product{amount=23, name='potatoes'},
// Product{amount=23, name='bread'}],
// 13=[Product{amount=13, name='lemon'},
// Product{amount=13, name='sugar'}],
// 14=[Product{amount=14, name='orange'}]}
//Collectors.partitioningBy()
//위의 groupingBy 하수형 인터페이스 Function을 이용하여,
//특정 값을 기준으로 스트림 내 요소들을 묶었다면,
//partitioningBy는 함수형 인터페이스 Predicate를 인자로 받는다.
Map<Boolean, List<Product>> partition =
productList.stream()
.collect(Collectiors.partiotioningBy(el -> el.getAmount() > 15));
//{false=[Product{amount=14, name='orange'},
// Product{amount=13, name='lemon'},
// Product{amount=13, name='sugar'}],
// true=[Product{amount=23, name='potatoes'},
// Product{amount=23, name='bread'}]}
//Collectors.collectiongAndThen()
//특정 타입으로 결과를 collect한 이후에 추가 작업이 필요한 경우 사용
public static<T, A, R, RR> Collectior<T, A, RR> collectiongAndThen(
Collector<T, A, R> downStream,
Fuction<R, RR> finisher) { ... }
//finisher는 collect를 한 이후에 실행할 작업을 의미
//Product를 Set에 저장하고 해당 Set을 수정 불가능한 Set으로 만들어라
Set<Product> set =
productList.stream()
.collect(Collectiors.collectingAndThen(Collectors.toSet(),
Collections::unmodifiableSet);
//Collectors.of()
//직접 collector를 만들 수 있다.
//accumulator와 combiner는 reduce와 동일
public static <T, R> Collector<T, R, R> of(
Supplier<R> supplier, //new Collector 생성
BiConsumer<R, T> accumulator, //두 값을 가지고 계산
BinaryOperator<R> combiner, //계산한 결과를 수집하는 함수
Characteristics... characteristics) { ... }
//예제
//collector를 생성
//supplier -> LinkedList의 생성자
//accumulator -> 리스트에 추가하는 add 메소드
//combiner -> 생성된 리스트들을 하나의 리스트로 합치기
Collector<Product, ?, LinkedList<Product>> toLinkedList =
Collector.of(LinkedList::new.
LinkedList::add,
(first, second) -> {
first.addAll(second);
return first;
});
LinkedList<Product> list =
produnctList.stream()
.collect(toLinkedList);
}
}
Matching
매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴한다. 다음과 같은 세 가지 메소드가 있다.
- anyMatch : 하나라도 조건을 만족하는 요소가 있는지
- allMatch : 모두 조건을 만족하는지
- noneMatch : 모두 조건을 만족하지 않는지
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Eric", "Elena", "Java");
boolean anyMath = names.stream()
.anyMatch(name -> name.contains("a"));//true
boolean allMatch = names.stream()
.allMatch(name -> name.length() > 4); //false : Elena만 만족
boolean noneMatch = names.stream()
.noneMatch(name -> name.endsWith("s"));//true
}
}
Iterating
forEach는 요소를 돌면서 실행되는 최종 연산이다. 보통 System.out.println 메소드를 통해 결과를 출력할 때 주로 사용된다. peek 메소드는 중간 연산에 해당되고 forEach는 최종 연산에 해당되는 것이 둘의 차이점이다.
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Eric", "Elena", "Java");
names.stream().forEach(System.out::println);
//Eric
//Elena
//Java
}
}