본문 바로가기

Programming/Java

[Java] 스트림(Stream) 활용

필터링

filter

filter 메소드는 Predicate를 인수로 받아 true를 반환하는 요소만을 포함하는 스트림을 반환한다

 

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
    .filter(i -> i % 2 == 0)
    .forEach(System.out::println);

// 2
// 4 

 

 

distinct

중복을 제거한 요소를 반환한다. 중복 여부는 객체의 hashCode(), equals() 메소드로 판별한다.

 

List<Integer> numbers = Arrays.asList(1, 1, 2, 2, 3);
numbers.stream()
    .distinct()
    .forEach(System.out::println);

// 1
// 2
// 3

 

슬라이싱

Predicate를 활용

자바 9이전에는 filter 메소드를 이용해 전체 스트림을 반복하면서 모든 요소에 Predicate를 적용해본다.

List<Integer> numbers = Arrays.asList(12, 17, 29, 35, 41, 44, 50, 66, 72, 80);
List<Integer> filteredNumbers = numbers.stream()
                                        .filter(i -> i < 50)
                                        .collect(toList());

 

하지만 이미 정렬되어 있다면 false가 등장하는 위치부터 반복을 중단할 수 있기 때문에 크기가 큰 스트림의 경우 많은 시간을 절약할 수 있다

takeWhile

List<Integer> takeWhileList = numbers.stream()
                                        .takeWhile(i -> i < 50)
                                        .collect(toList()); // 12, 17, 29, 35, 41, 44

takeWhile을 사용하면 무한 스트림을 포함한 모든 스트림에 Predicate를 적용해 슬라이스가 가능하다

dropWhile

만약 50보다 큰 나머지 요소를 선택하고 싶다면 dropWhile을 사용하면 된다

List<Integer> dropWhileList = numbers.stream()
                                        .dropWhile(i -> i < 50)
                                        .collect(toList()); // 50, 66, 72, 80

dropWhiletakeWhile과 정반대의 작업으로 처음으로 false가 등장하는 시점까지의 요소를 모두 버리고 남은 요소를 반환한다. takeWhile과 동일하게 무한 스트림에서도 동작한다.

 

limit

주어진 값 이하의 크기를 갖는 limit(int n) 메소드를 지원. 최대 n개의 요소를 반환한다

List<Integer> limitList = numbers.stream()
                                    .limit(5)
                                    .collect(toList()); // 12, 17, 29, 35, 41

 

skip

처음 n개의 요소를 제외한 스트림을 반환하며, limit()과 상호 보완적 연산을 수행

n개 이하의 요소를 가진 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다

List<Integer> skipList = numbers.stream()
                                  .skip(5)
                                  .collect(toList()); // 44, 50, 66, 72, 80

 

 

매핑

map

함수를 인자로 받아 각 요소에 적용되고, 적용된 결과가 새로운 요소로 매핑된다

 

List<String> words = Arrays.asList("hi", "hello", "bye");
List<Integer> lengths = words.stream()
                                .map(String::length)
                                .collect(toList()); // 2, 5, 3

 

 

flatMap

flatMap은 스트림의 각 값을 다른 스트림으로 만든 다음 모든 스트림을 하나의 스트림으로 연결한다.

 

List<String> words = Arrays.asList("hi", "hello", "bye");
List<Integer> lengths = words.stream()
                              .map(w -> w.split("")) // Stream<String[]>
                              .flatMap(Arrays::stream) // Stream<String>
                              .distint() // Stream<String>
                              .collect(toList()); // List<String> : h, i, e, l, o, b, y

 

 

정렬

중간 단계에서 요소들을 정렬하는데 사용한다.

클래스를 구현할 때 Comparable을 구현하지 않았다면 ClassCastException이 발생하게 된다. 파라미터를 주지 않으면 Comparable을 구현한 대로 정렬되고, 만약 구현하지 않았거나 다른 정렬방법을 사용하고 싶다면 Comparator를 구현해 주면된다.

 

List<Integer> numbers = Arrays.asList(4, 30, 19, 22, 50);
List<Integer> sortedNumbers = numbers.stream()
                                      .sorted((o1, o2) -> Integer.compare(o2, o1))
                                      //.sorted(Comparator.reverseOrder()) 과 동일
                                      .collect(toList());

 

 

루핑

요소 전체를 반복할 때 사용한다

peek

전체 요소를 루핑하며 추가적인 작업을 하기 위해 사용된다.

중간 처리 메소드로 처리 후 Stream을 반환한다. 중간 처리 메소드이기 때문에 마지막에 호출하면 스트림이 작동하지 않는다.

 

forEach

요소를 루핑하며 처리하는 것은 동일하지만 최종처리 메소드로 호출하면 스트림이 종료된다. 따라서, 이후 다른 메소드를 추가적으로 호출할 수 없다.

 

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

numbers.stream().forEach(System.out::println);
// 출력
1
2
3
4

 

매칭

anyMatch

적어도 한 요소가 주어진 Predicate와 일치하는지 확인한다.

 

allMatch

모든 요소가 Predicate와 일치하는지 확인한다.

 

noneMatch

allMatch()와 반대로 모든 요소가 일치하지 않는지 확인한다.

 

List<Integer> numbers = Arrays.asList(12, 17, 29, 35, 41, 44, 50, 66, 72, 80);

boolean anyMatch = numbers.stream()
                            .anyMatch(i -> i % 2 == 0); // true
boolean allMatch = numbers.stream()
                            .allMatch(i -> i % 2 == 0); // false
boolean noneMatch = numbers.stream()
                            .noneMatch(i -> i % 2 == 0); // false

 

match 메소드들은 스트림 쇼트서킷 기법(자바의 &&, ||와 같은 연산)을 활용한다.

 

 

💡 쇼트서킷 평가

스트림의 전체를 보지 않아도 결과를 확인할 수 있다. and 연산으로 모든 요소를 연결하면 하나라도 거짓이 나올 경우 나머지와 관계없이 항상 거짓이 된다. 이러한 상황을 쇼트서킷이라고 한다.
모든 요소를 처리할 필요없이 원하는 요소를 찾게 되면 즉시 결과를 반환이 가능하다.

 

검색

findAny

현재 스트림에서 임의의 요소를 반환

스트림 파이프라인은 단일 과정으로 실행할 수 있도록 최적화되기 때문에 결과를 찾는 즉시 종료한다.

 

List<Integer> numbers = Arrays.asList(12, 17, 29, 35, 41, 44, 50, 66, 72, 80);
Optional<Integer> number = numbers.stream()
                                    .filter(i -> i > 50)
                                    .findAny(); // Optional[66]

 

 

💡 Optional
java.util.Optional<T>

값의 존재 여부를 표현하는 컨테이너 클래스
아무 요소도 반환하지 않을 경우 null을 반환하게 되고, null은 에러의 원인이 되기때문에 이를 처리하도록 강제하는 역할을 한다.

 

findFirst

생성된 스트림에서 논리적인 아이템의 순서가 정해져 있을 때, 첫 번째 요소를 찾기 위해 사용된다

 

List<Integer> numbers = Arrays.asList(12, 17, 29, 35, 41, 44, 50, 66, 72, 80);
Optional<Integer> number = numbers.stream()
                                    .filter(i -> i % 5 == 0)
                                    .findFirst(); // Optional[35]

 

 

❓ findAny, findFirst는 언제 사용할까

왜 두 메소드가 모두 필요할까? 병렬성!
병렬 실행에서는 첫 번째 요소를 찾기 어렵기 때문에 요소의 반환 순서가 중요하지 않다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

 

리듀싱

요소를 처리해 값으로 도출하는 연산. 과정이 종이를 접어 작게 만드는 것과 유사하다고 해 폴드(fold)라고 불림

대량의 데이터를 가공해 축소하는 리덕션(Reduction)

 

reduce

reduce([초기값], Operator)

다양한 집계 결과물을 만드는 메소드이다. Operator를 통해 모든 요소를 하나씩 소비하면서 최종 결과물을 반환한다.

 

초기값은 생략할 수 있지만, 생략한다면 결과값은 Optional로 반환된다.

  • why? 만약 빈 stream이 들어올 경우 아무런 연산을 할 수 없기 때문에 가리킬 값이 없게 된다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

int sum = numbers.stream()
                  .reduce(0, (a, b) -> a + b); // 10
Optional<Integer> OptinalSum = numbers.stream()
                                        .reduce((a, b) -> a + b); // Optional[10]

Optional<Integer> min = numbers.stream()
                                .reduce(Math::min); // Optional[1]

 

장점

반복문을 사용해 합계를 구하는 것보다 reduce를 사용해 합계를 구하게 되면 내부 반복이 추상화 되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.

일반적인 반복문은 sum 변수를 공유해야하기 때문에 병렬화가 어렵고, 동기화를 시키더라도 스레드간의 경쟁으로 인해 상쇄된다. 병렬화하기 위해서는 입력 분할, 분할된 값들의 합계, 합계된 값을 합치는 과정을 거쳐야 한다.

 

스트림은 pallelStream()을 사용하면 병렬로 쉽게 만들 수 있어 간편하다.

 

 

제공되는 메소드

기본적으로 스트림에서 간단한 집계를 처리해주는 메소드를 제공해준다.

sum(), count(), average(), max(), min() 등을 사용하면 쉽게 집계 값을 구할 수 있다.

 

int sum = IntStream.range(0, 5).sum(); // 10

 

 

 

기본형 특화 스트림

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

int sum = numbers.stream()
                  .reduce(0, Integer::sum);

위와 같이 합계를 구할 수 있지만, Integer 타입을 기본형으로 언박싱해 더해야하기 때문에 박싱비용이 추가된다.

스트림은 객체 타입을 사용하기 때문에 객체의 합을 계산할 수 없어 sum()메소드를 제공하지 않는다. 이를 사용하기 위해 숫자 스트림을 처리할 수 있는 기본형 특화 스트림(primitive stream specialization)을 제공한다

 

박싱 비용을 피할 수 있도록 int에는 IntStream, double은 DoubleStream, long은 LongStream을 제공한다. 오직 박싱 효율성만 관련이 있고, 숫자 리듀싱 연산 수행 메서드만을 제공하고 추가적인 기능은 제공하지 않는다.

변환

숫자 스트림으로 변환

mapToInt, mapToDouble, mapToLong을 사용해 기본형 특화 스트림으로 변환이 가능하다.

 

int sum = numbers.stream()
                  .mapToInt(Integer::parseInt)
                  .sum();

 

 

객체 스트림으로 복원

boxed()

숫자 스트림에서 다시 객체 스트림으로 변경하고 싶을 때 사용한다.

 

numbers.stream() // Stream<Integer>
    	.mapToInt(Integer::parseInt) // IntStream
    	.boxed(); // Stream<Integer>

 

 

무한 스트림

고정된 컬렉션에서 고정된 크기의 스트림을 만들었던 것과 달리 크기가 고정되지 않은 스트림을 만들 수 있다.

Stream.iterateStream.generate를 사용하면 요청할 때마다 주어진 함수를 이용해 무한하게 만들 수 있다. 때문에 일반적으로 limit()과 함께 사용한다.

 

iterate

Stream.iterate(초기값, Oprator) 형태로 사용 가능하다.

Oprator의 인자는 이전 결과이다. 기본적으로 기존 결과에 의존해 순차적으로 연산하고, 끝이 없기 때문에 언바운드 스트림(Unbound Stream)이라고 표현한다.

 

Stream.iterate(초기값, Predicate, Operator)를 사용하면 Predicate가 만족할 때까지만 수행하도록 제공할 수 있다.

또는 쇼트서킷을 제공하는 메소드와 함께 사용하면 만족하지 않을경우 종료되도록 사용이 가능하다.

 

Stream.iterate(0, n -> n <= 100, n -> n + 10); // 0, 10, 20, ..., 100

 

 

generate

Stream.generate(Supplier<T>)

요구할 때마다 값을 무한히 생산하지만 연속적인 값을 생성하지 않고 Supplier를 인자로 받아 새로운 값을 생산한다.

 

Intstream.generate(() -> 1); // 1, 1, 1, ...

 

 

 

 

 


📚 Reference

신용권, 『이것이 자바다』, 한빛미디어(2015)
라울-게이브리얼 우르마, 『모던 자바 인 액션』, 우정은, 한빛미디어(2018)