본문 바로가기
Language/Java

[Java 8] Stream - 2 (기본 연산)

by ocwokocw 2021. 2. 11.

- 출처: https://www.oracle.com/technical-resources/articles/java/ma14-java-se-8-streams.html

- Stream 연산

java.util.stream.Stream 에 정의되어 있는 Stream 인터페이스는 많은 연산들을 정의하고 있는데, 크게 2 부분으로 나눌 수 있다. 

  • filter, sorted, map 와 같이 파이프라인으로 서로 연결될 수 있는 형태
  • 파이프라인을 종결시키고, 결과를 반환하는 collect

연결될 수 있는 Stream 연산자들을 "중간 연산자" 라고 칭한다. 해당 연산자들은 반환형이 Stream 형이기 때문에 서로 연결될 수 있다. 

 

Stream 파이프라인을 종결시키는 연산자들은 "종결 연산자" 라고 칭한다. 종결 연산자는 파이프라인으로 부터 List, Integer, 심지어 void 와 같은 결과를 생산한다.

 

"중간 연산자" 들은 stream 파이프라인에서 "종결 연산자"들이 수행되기까지 어떤 처리도 하지 않는다. 이것을 게으른("lazy") 연산이라고 한다. 이렇게 뒤늦은 처리를 하는 이유는 대게 "중간 연산자"들은 "종결 연산자"에 의해서 1개로 "병합" 되며 처리 되기 때문이다.

 

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> twoEvenSquares = 
    numbers.stream()
           .filter(n -> {
                    System.out.println("filtering " + n); 
                    return n % 2 == 0;
                  })
           .map(n -> {
                    System.out.println("mapping " + n);
                    return n * n;
                  })
           .limit(2)
           .collect(toList());

 

위의 예제는 주어진 List 에서 짝수 2개를 제곱한 계산 결과를 반환한다. System.out.println 결과는 아래와 같다.

 

filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4

 

이런 결과가 나오는 이유는 limit(2) 구문에서 short-circuiting (알고리즘 기법에서는 "가지치기" 와 같은 기법)을 사용하기 때문이다. 결과를 얻기 위해서 list 전체가 아니라 stream 의 일부분 처리만 필요하다. 마치 and 연산자로 연결된 대규모 Boolean 연산에서 1개의 표현식만 false를 반환하면, 나머지를 계산할 필요 없이 전체 결과를 false로 치환하는것과 같은 이치이다. 위에 예제에서는 limit 연산자의 크기를 2로 지정한것이 그러한 역할을 한다.

 

일반적인 stream의 동작을 3가지로 요약해보면 아래와 같다.

  • 질의를 수행하기 위한 데이터소스(ex - collection)
  • stream 파이프라인 형태를 취하는 중간 연산자 체인
  • stream 파이프라인을 수행하고 결과를 생산하는 종결 연산자

- Stream 연산예제

 

- Filtering: stream 으로 부터의 원소들을 거르는데 사용되는 연산자들

  • filter(Predicate): 인자로서 predicate(java.util.function.Predicate)를 취하고, 주어진 predicate 를 만족하는 모든 원소들을 포함하는 stream을 반환한다.
  • distinct: 유일한 원소들을 반환한다.
  • limit(n): 주어진 n개의 사이즈만큼만 반환한다.
  • skip(n): 처음 n개의 원소들을 버린다.

- Finding and matching: 일반적인 데이터 처리 패턴은 몇개의 원소들이 주어진 속성에 매칭되는지를 결정하는것이다. 이런 작업을 수행하기 위해 anyMatch, allMatch, nonMatch 와 같은 연산자들을 사용할 수 있다. 해당 연산자들은 모두 predicate를 인자로 취하고 boolean 을 결과로 반환한다. 예를 들면 transactions 의 stream 에서 모든 원소들이 100보다 큰 값을 가지는지를확인하기 위해 allMatch를 사용할 수 있다.

 

boolean expensive =
    transactions.stream()
                .allMatch(t -> t.getValue() > 100);

 

또한 Stream 인터페이스는 stream 으로 부터 임의의 원소들을 반환하는 findFirst 와 findAny 연산자들도 제공한다. 이 연산자들은 filter와 같이 다른 연산자들과 함께 사용된다. findFirst 나 findAny 모두 아래처럼 Optional 객체를 반환한다.

 

Optional<Transaction> = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .findAny();

 

Optional<T> (java.util.Optional) 클래스는 값의 존재여부는 나타내기 위한 컨테이너 클래스이다. 위의 예제에서 findAny 가 grocery 타입의 어떠한 트랜잭션을 찾지 못했을 수도 있다. Optional 클래스는 원소의 존재를 확인하기 위한 몇가지 메소드를 포함하고 있다. 예를 들어 만약 트랜잭션이 존재하면 optional 객체의 연산 중 ifPresent 메소드를 사용할 수 있다.

 

transactions.stream()
              .filter(t -> t.getType() == Transaction.GROCERY)
              .findAny()
              .ifPresent(System.out::println);

 

- Mapping: Streams 는 function(java.util.function.Function) 을 인자로 취하는 map 메소드를 지원하는데, stream 의 원소를 다른 형태로 투영시키기 위해 사용한다. 인자로 취하는 function 에서는 각 원소를 취하여 새로운 원소로 "맵핑"한다.

 

만약에 stream 의 각 원소들로 부터 특정 정보를 추출하고 싶을 때가 있다. 아래 코드는 list 로 부터 각 단어의 길이의 목록을 반환한다.(Reducing) 지금까지는 boolean, void, Optional 객체를 반환하는 종결 연산자를 살펴보았다. collect를 이용하면 Stream 의 모든 원소를 결합하여 List 를 반환할 수 있다.

 

List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
 List<Integer> wordLengths = 
    words.stream()
         .map(String::length)
         .collect(toList());

 

또 예를 들면 "가장 큰 ID를 가지는 거래는 무엇인가?" 라든가 "모든 거래의 값의 합을 계산하라"와 같이 더 복잡한 질의를 계산하기 위해 stream 의 모든 원소들을 결합할 수도 있다. 이런 계산을 하기 위해서는 streams 에서 reduce 연산자를 이용해야 하는데, 이 연산자는 결과를 반환하기 까지 각 원소에 반복해서 연산을 적용한다. 

이 연산자를 함수형 프로그래밍 에서는 fold 연산자(접는 연산자) 라고도 하는데, 마치 종이의 큰 조각을 반복하여 접어서 하나의 작은 사각형을 만드는 것 처럼 연산이 진행된다하여 fold 연산자라고 부르기도 한다.

for 문에서 합을 구할때 보통 아래처럼 작성한다.

 

int sum = 0;
for (int x : numbers) {
    sum += x; 
}

 

numbers의 list 각 원소가 더하기 연산자로 반복적으로 결합되어 결과를 생산한다. 본질적으로 numbers의 list가 1개의 숫자로 축소되었다.("reduced")

streams 에서 reduce 메소드를 이용하면 아래처럼 stream의 모든 원소들의 합계를 구할 수 있다. reduce 메소드는 2 개의 인자를 취한다.

 

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

 

위의 코드에서 초기값은 0 이며, BinaryOperator<T> 는 2개의 원소를 결합하여 1개의 새로운값을 생성한다.

 

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {

.....

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);

 

BinaryOperator를 살펴보면 BiFunction을 extends 하고 있고 minBy와 maxBy라는 static 함수가 정의되어있다. @FunctionalInferface는 추상메소드가 1개인 인터페이스라고 했는데 왜 추상메소드가 없는것일까? BiFunction 의 정의를 보면 안에 해답이 있다. apply 라는 메소드는 2개의 인자를 받아 1개를 반환한다.

reduce 메소드는 본질적으로 반복되는 어플리케이션의 패턴을 추상화한것이다. 곱셈이나 Max 값을 구하는데도 사용할 수 있다.

 

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
	
int sum = numbers.stream().reduce(0, (a, b) -> a+b);
int product = numbers.stream().reduce(1, (a, b) -> a*b);
int maximum = numbers.stream().reduce(Integer.MIN_VALUE, Integer::max);
	
System.out.println("sum: " + sum + 
		", product: " + product +
		", maximum: " + maximum);

 

sum: 15, product: 120, maximum: 5

'Language > Java' 카테고리의 다른 글

[Java 8] Stream - 4 (flatMap, collect)  (0) 2021.02.11
[Java 8] Stream - 3 (Numeric, Stream 생성)  (1) 2021.02.11
[Java 8] Stream - 1 (Stream 개요)  (0) 2021.02.11
[Java 8] 람다(Lambda) - 3  (0) 2021.02.11
[Java 8] 람다(Lambda) - 2  (0) 2021.02.10

댓글