Post

[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 로 리턴해준다. Optionalnull 관련 문제를 해결때 자주 사용되는 라이브러리다. 스트림 최종연산에서 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 로 값을 안받고 바로 처리해 줄 수 있다. ifPresentOptional 에서 지원하는 함수로, 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 가 출력된다.

Reference

https://futurecreator.github.io/2018/08/26/java-8-streams/

This post is licensed under CC BY 4.0 by the author.