본문 바로가기

연재작/프로그래밍 언어

Java - Stream 뽀개기 (2)

 

Stream은 크게 3가지로 구성되었고 중간 연산 메소드까지 소개했었다.

 

3. 최종 연산 - Optional을 반환하는 경우(Optional로 가로채는)도 있으니 주의

Stream은 데이터들의 다양한 연산을 도와주는데,

그 중에는 연산을 했지만 값이 나오지 않거나 에러가 뜨는 연산도 존재할 수 있다.

이럴 가능성이 있는 메소드들은 Stream이 아니라 Optional로 반환을 한다.

 

종류를 설명하기 전에 최종 연산에 대해 간단히 설명하자면,

  • stream을 소비하고 연산을 종료한다.
  • 최종 연산이 호출되면 스트림을 더이상 사용할 수 없다.

해당 색깔의 method는 전부 다 Optional을 반환한다.

findFirst : 첫 번째 요소를 반환.  stream이 비어있는 경우도 있기에 Optional을 반환한다.

Optional<T> findFirst()
Stream<String> stream = Stream.of("a", "b", "c");
Optional<String> first = stream.findFirst();  // Optional["a"] 반환

 

findAny : 임의의 요소를 반환. 병렬 스트림(parallelStream)에서 유용하다는데

Optional<T> findAny()
Stream<String> stream = Stream.of("a", "b", "c");
Optional<String> any = stream.findAny();  // Optional["a"] (또는 다른 임의의 요소) 반환

 

 

더보기

병렬 스트림(Parallel Stream)은 Java에서 대량의 데이터를 보다 효율적으로 처리하기 위해 여러 개의 쓰레드에서 작업을 분할하여 동시에 수행할 수 있는 스트림입니다. 병렬 스트림을 사용하면 데이터를 여러 개의 쓰레드로 나눠 병렬로 처리하여 성능을 향상시킬 수 있습니다. 특히, 데이터 양이 많거나 계산이 복잡할 때 병렬 스트림을 사용하면 멀티코어 CPU를 활용하여 실행 시간을 줄일 수 있습니다.

병렬 스트림의 주요 특징:

  • 자동으로 쓰레드 분배: 병렬 스트림은 내부적으로 작업을 여러 쓰레드로 나누어 처리합니다. 개발자가 직접 쓰레드를 관리할 필요 없이, Java의 ForkJoinPool이 작업을 자동으로 병렬 처리합니다.
  • 비결정성: 병렬 스트림은 여러 쓰레드가 동시에 작업을 수행하므로, 작업 순서가 보장되지 않습니다. 따라서 순서에 민감한 작업에는 병렬 스트림을 사용하지 않는 것이 좋습니다.
  • 데이터의 병렬 처리: 스트림의 데이터 요소들을 분할하여 동시에 여러 쓰레드에서 작업을 처리하므로, 성능 향상이 가능합니다.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.parallelStream()
    .forEach(System.out::println); 
list.stream()
    .parallel()
    .forEach(System.out::println); // 이 두 가지 방법으로 parallel을 사용한다.

 

min, max : 말 그대로 min, max 값을 반환 한다.

Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);

Optional<Integer> min = Stream.of(1, 2, 3).min(Integer::compare);
Optional<Integer> max = Stream.of(1, 2, 3).max(Integer::compare);

 

reduce : stream의 요소들을 결합해서 하나의 결과를 생성.

   T reduce(T identity, BinaryOperator<T> accumulator);
   Optional<T> reduce(BinaryOperator<T> accumulator);
   
   // BinaryOperator란 BiFunction<T,T,T>를 말한다. 즉 두 개의 T파라미터를 받아 T를 반환하는 함수.
   
   Optional<Integer> reduced = Stream.of(1, 2, 3).reduce((a, b) -> a + b);

주의해야 할 것은 여기서 reduce는 overload된 메서드이므로 두 방식을 잘 알아두어야 한다.

첫 번째 방식은 T identity는 default값이므로 이 메서드는 무조건 T를 반환한다

두 번째 방식은 default값이 없기 때문에 Optional의 사용이 필요하다.

 

count : 스트림의 요소 개수를 반환

long count()
Stream<String> stream = Stream.of("a", "b", "c");
long count = stream.count();  // 3 반환

마찬가지로 주의해야할게, long을 반환하기 때문에 Integer와 같은 primitive type의 wrapper 클래스로 받는 것이 좋다.

 

forEach : 스트림의 각 요소에 대해 주어진 작업 수행 ➡️ void, 유일하게 반환 값이 없다. 

void forEach(Consumer<? super T> action)
// 함수형 인터페이스 Consumer는 한 개의 파라미터를 받아 아무것도 반환하지 않는 인터페이스이다.
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println);  // "a", "b", "c" 출력

 

collect : 스트림의 요소를 수집해 리스트, 집합 등의 컬렉션으로 만든다. 두 가지 방법이 있다.

<R> R collect(Supplier<R> supplier,
			  BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);
// BiConsumer는 위의 consumer와 같다. 반환되는게 없는 것
// Supplier는 파라미터가 없는데 값을 반환하는 인터페이스이다. 
// getter나 필드가 없는 생성자를 생각하면 좋다.

List<String> asList = stringStream
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

 

공식문서에 나와있는 예시로 설명을 해보자면

  • Supplier<R> supplier : stream의 값을 받을 새로운 R 타입의 collection을 공급 (new 생성자)
  • BiConsumer<R, ? super T> accumulator : stream의 원소 T를 R에 집어넣는 적재 행위 (add method)
  • BiConsumer<R, R> combiner : 병렬 스트림에서 사용하기 위한 메서드를 집어넣어야한다.
    병렬스트림에서는 스레드별로 각각 새로운 collection를 만들기 때문에,
    이렇게 만들어진 각각의 collection을 합치기 위한 방식을 제공해야 한다.
    예를 들어 ArrayList라면 addAll을, StringBuilder라면 append를 제공해야 한다.
<R, A> R collect(Collector<? super T, A, R> collector);
// A는 수집 중간에 사용되는 누적기. ArrayList와 같은 리스트가 누적기로 사용된다.
List<String> list = Stream.of("apple", "banana", "cherry")
            .collect(Collectors.toList()); // list로 수집
Set<String> set = Stream.of("apple", "banana", "cherry")
            .collect(Collectors.toSet());  // 스트림을 집합으로 수집
Map<String, Integer> map = Stream.of("apple", "banana", "cherry")
            .collect(Collectors.toMap(
                s -> s,            // 키: 문자열 자체를 키로 사용
                s -> s.length()     // 값: 문자열의 길이를 값으로 사용
            )); map으로도 가능하다.

 

 

참고로, Java 16부터 간편하게 list, set을 만들 수 있게

toList, toSet, toArray의 메서드들은 collect를 쓰지 않고도 바로 사용할 수 있다.

 

allMatch,  anyMatch, noneMatch

스트림의 요소중 위의 제목 순서대로 (전부 만족함/아무거나 만족함/하나도 만족 못함 )일 때 true를 반환한다.

boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)

Stream<String> stream = Stream.of("banana", "cherry");
boolean noneStartWithA = stream.noneMatch(s -> s.startsWith("a"));  // true 반환