[JAVA]자바 Stream 사용방법 3편(최종연산)
스트림 관련글
JAVA[자바] Stream 사용방법 1편(생성하기)
JAVA[자바] Stream 사용방법 2편(중간연산)
스트림 최종연산
스트림을 생성하고, 중간연산에서 어느정도 원하는 데이터로 바꾸고 최종적으로 결과값을 반환해주는 연산 이다. 중간연산과 다른점이 있다면 최종연산은 마지막 단 한번만 사용할 수 있다.
아래 List 와 Class 를 기준으로 최종연산 예제를 작성해 보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
List<String> names = Arrays.asList("Eric", "Elena", "Java");
List<Product> productList =
Arrays.asList(new Product(23, "potatoes"),
new Product(14, "orange"),
new Product(13, "lemon"),
new Product(23, "bread"),
new Product(13, "sugar"));
public class Product {
int amount;
String name;
public Product(int amount, String name) {
this.amount = amount;
this.name = name;
}
public int getAmount() {
return amount;
}
public String getName() {
return name;
}
}
Calculating
컬렉션형태가 아닌 기본형타입(IntStream, LongStream 등)에는 여러가지 연산함수들이 있다. 대부분 Long
반환인것을 주의하자. Long
반환인 이유는 간단하다. Int
형이라도 값을 계속더하면 오버플로우가 날 수 있으니 Long
자료형으로 반환해주는게 기본이다.
1
2
long count = IntStream.of(1, 3, 5, 7, 9).count(); // 5
long sum = LongStream.of(1, 3, 5, 7, 9).sum(); // 25
평균, 최소, 최대의 경우 빈 스트림에서는 null
값을 반환해야하는데, 이것을 처리해주기위해 Optional
로 리턴해준다. Optional
은 null
관련 문제를 해결때 자주 사용되는 라이브러리다. 스트림 최종연산에서 null
을 반환할 가능성이 있는 함수는 대부분 Optional
로 반환해준다고 보면 된다.
1
2
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
또는 ifPresent
를 이용해 Optional
로 값을 안받고 바로 처리해 줄 수 있다. ifPresent
는 Optional
에서 지원하는 함수로, null
값이 아닐때만 실행해준다.
1
2
3
DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
.average()
.ifPresent(System.out::println); // 3.3
Reduction
기본타입이 아닌 컬렉션타입인 경우 reduce
이용해 값을 만들 수 있다.
1
2
3
4
5
6
7
8
9
10
// 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);
- accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
- identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
- combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
1개의 인자만 넘겨올경우 accumulator
만 사용하는데, 자료형이 BinaryOperator<T>
이다. 해당 함수형 인터페이스는 같은 자료형인 2개의 매개변수를 받아 하나의 값으로 반환해 준다.
accumulator
1
2
3
4
Optional reduced = names.stream()
.reduce((a, b) -> {
return a+b;
}); //Optional[EricElenaJava]
참고로 반환값은 Optional
인 점을 유의하자. 빈스트림의 경우 reduce
써도 null
을 반환하기때문에 Optional
로 감싸서 반환해주기 때문이다.
람다식으로 조금 더 짧게 표현이 가능하지만 (a,b) -> a+b
이해가 더 잘되도록 길게 표현해봤다.
1
2
Optional reduced = names.stream()
.reduce((a, b) -> a+b); //Optional[EricElenaJava]
람다식을 활용해면 이렇게 짧게 표현이 가능하다.
identity
두 개의 인자를 넘겨주면 초기값 설정할 수 있다.
1
2
3
4
5
String reduced =names.stream()
.reduce("Start",
(a, b) -> {
return a+b;
}); //StartEricElenaJava
그리고 초기값을 넘겨주면 빈스트림이어도 값이 무조건 존재하기때문에 null
값을 반환할일이 없다. 그래서 Optional
로 감싸주지 않고 identity
에서 사용했던 자료형으로 반환을 해준다.
combiner
마지막 3개의 인자를 넘겨줄 경우 각자 다른 쓰레드에서 연산을 수행 후 마지막에 합쳐준다. 그래서 병렬스트림에서만 동작을 한다.
1
2
3
4
5
6
7
8
9
String paralleReduced = names
.parallelStream()
.reduce("Start",
(a, b) -> {
return a + b;
},
(a, b) -> {
return a + b;
}); // StartEricStartElenaStartJava
병렬로 Start+Eric, Start+Elena, Start+Java 로 각각 수행후 마지막으로 StartEric + StartElena + StartJava 연산을 수행했다.
Collecting
Collectors.toList()
스트림에서 가장 많이 사용하는 함수이다. 최종 자료형을 리스트로 받을 수 있다.
1
2
3
4
5
List<String> collectorCollection =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]
Collectors.joining()
문자들을 한줄로 붙여서 반환받을 수 있다.
1
2
3
4
String listToString = productList.stream()
.map(Product::getName)
.collect(Collectors.joining());
// potatoesorangelemonbreadsugar
1개의 인자를 넘겨준다면 해당 인자를 문자 사이에 붙여서 만들 수 있다.
1
2
3
4
String listToString = productList.stream()
.map(Product::getName)
.collect(Collectors.joining("|"));
// potatoes|orange|lemon|bread|sugar
3개의 인자를 넘겨준다면 prefix
(맨처음), sufix
(맨뒤) 에 붙여줄 문자를 넣어줄 수 있다.
1
2
3
4
String listToString = productList.stream()
.map(Product::getName)
.collect(Collectors.joining("|", "<", ">"));
// <potatoes|orange|lemon|bread|sugar>
Collectors.averagingInt()
값의 평균을 구해주는 함수, 반환값은 Double
이다. 끝에 Int
가 붙은 이유는 Int
로 데이터를 맵핑하기 때문이다. Double
로 맵핑을 원한다면 averagingDouble()
을 사용하면 된다.
1
2
Double averageAmount = productList.stream()
.collect(Collectors.averagingInt(Product::getAmount)); // 17.2
Collectors.summingInt()
스트림 값들의 합을 구해주는 함수이다.
1
2
Integer summingAmount = productList.stream()
.collect(Collectors.summingInt(Product::getAmount)); // 86
mapToInt
를 사용하면 IntStream
으로 바뀌어 기본타입 함수도 이용할 수 있다.
1
2
3
Integer summingAmount = productList.stream()
.mapToInt(Product::getAmount)
.sum(); // 86
Collectors.summarizingInt()
값들에 대해 개수, 합, 최소, 평균, 최대값을 담고있는 자료형을 반환해준다.
1
2
3
4
IntSummaryStatistics statistics = productList.stream()
.collect(Collectors.summarizingInt(Product::getAmount))
// IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
Collectors.groupingBy()
그룹핑은 매개변수로 받은 값을 기준으로 Map
자료형으로 반환해준다.
1
2
3
4
5
6
7
8
9
10
11
12
Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
/**
* amount 를 기준으로 값을 그룹핑(같은 리스트로 묶어서) 맵 자료형으로 반환해준다.
* 23, 13, 14 값을 키값으로 객체를 Value로 분류해주었다.
*/
{
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.partitioning()
그룹핑과는 다르게 매개변수로 받은 함수에대해 만족하는지(true
) 아닌지(false
) 로 구분해서 Map
자료형으로 반환해준다.
1
2
3
4
5
6
7
8
9
10
Map<Boolean, List<Product>> mapPartitioned = productList.stream()
.collect(Collectors.partitioningBy(el -> el.getAmount() > 15));
/**
* Amount 가 15 를 넘으면 true, 못넘으면 false 로 분류해준다. true, false 가 키 값이 된다.
*/
{
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.collectingAndThen()
Collectors
작업을 한뒤 추가로 하고싶은 작업이있다면 collectingAndThen()
을 이용해 추가해줄 수 있다.
1
2
3
4
5
6
/**
* 먼저 리스트 자료형을 Set 자료형 으로 바꾸고, 수정불가능한 Set 으로 변경해주었다.
*/
Set<Product> unmodifiableSet = productList.stream()
.collect(Collectors.collectingAndThen(Collectors.toSet(),
Collections::unmodifiableSet));
Collector.of()
만족스로운 Collector 함수가 없다면 직접 만들 수 있다.
1
2
3
4
5
public static<T, R> Collector<T, R, R> of(
Supplier<R> supplier, // new collector 생성
BiConsumer<R, T> accumulator, // 두 값을 가지고 계산
BinaryOperator<R> combiner, // 계산한 결과를 수집하는 함수.
Characteristics... characteristics) { ... }
1
2
3
4
5
6
7
Collector<Product, ?, LinkedList<Product>> toLinkedList =
Collector.of(LinkedList::new, // Collector 를 생성하기위해 LinkedList 생성자를 넘겨준다
LinkedList::add, // 생성된 리스트에 요소를 추가한다. 병렬로 진행되기 때문에 5개의 연결리스트가 만들어진다.
(first, second) -> { // 마지막연산으로 각각의 연결리스트를 합쳐 하나의 연결리스트가 만들어진다.
first.addAll(second);
return first;
});
1
2
3
4
5
6
7
8
9
LinkedList<Product> linkedListOfPersons = productList.stream()
.collect(toLinkedList);
// 결과적으로 하나의 연결리스트가 반환된다.
Product{amount=23, name="potatoes"}
Product{amount=14, name="orange"}
Product{amount=13, name="lemon"}
Product{amount=23, name="bread"}
Product{amount=13, name="sugar"}
Matching
1
2
3
4
5
6
7
8
// 하나라도 만족하면 true
boolean anyMatch(Predicate<? super T> predicate);
// 모두 만족해야 true
boolean allMatch(Predicate<? super T> predicate);
// 모두 만족하지 않아야 true
boolean noneMatch(Predicate<? super T> predicate);
1
2
3
4
5
6
7
8
List<String> names = Arrays.asList("Eric", "Elena", "Java");
boolean anyMatch = names.stream()
.anyMatch(name -> name.contains("a")); // true
boolean allMatch = names.stream()
.allMatch(name -> name.length() > 3); // true
boolean noneMatch = names.stream()
.noneMatch(name -> name.endsWith("s")); // true
Iterating
스트림 내부를 순회하면서 forEach()
의 매개변수로 받은 함수를 실행해 준다.
1
2
3
4
names.stream().forEach(System.out::println);
// Eric
// Elena
// Java
Find
맨처음 탐색된 값을 반환한ㄷ. 원하는값을 찾고 싶다면, 중간연산을 이용해 필터링을 거친 뒤 find
를 활용하면 된다.
1
2
3
4
5
6
7
8
9
// findFirst 는 첫번째 요소를 반환한다.
Optional find1 = names.stream()
.filter(name -> name.contains("E"))
.findFirst(); // Eric
// findAny 는 병렬로 작업 처리시 먼저 탐색되는것을 반환하기때문에 결과값이 바뀔 수 있다.
Optional find2 = names.stream().parallel()
.filter(name -> name.contains("E"))
.findAny(); // Eric 또는 Elena 가 출력된다.