[윤성우의 열혈 Java 프로그래밍] Chapter 30 - 스트림 2

Update:     Updated:

카테고리:

태그:

30-1. 스트림의 생성과 연결

- 스트림의 생성: 스트림 생성에 필요한 데이터를 직접 전달

스트림 생성과 관련하여 Stream<T> 인터페이스에 정의되어 있는 static 메소드가 있다.

import java.util.List;
import java.util.Arrays;
import java.util.stream.Stream;

class StreamOfStream {
    public static void main(String[] args) {
        // ex 1
        Stream.of(11, 22, 33, 44)
                .forEach(n -> System.out.print(n + "\t"));
        System.out.println();

        // ex 2
        Stream.of("So Simple")
                .forEach(s -> System.out.print(s + "\t"));
        System.out.println();

        // ex 3
        List<String> sl = Arrays.asList("Toy", "Robot", "Box");
        Stream.of(sl)
                .forEach(w -> System.out.print(w + "\t"));
        System.out.println();

        // ex 4
        String[] sr = {"Toy", "Robot", "Box"};
        Stream.of(sr)
                .forEach(i -> System.out.print(i + "\t"));
        System.out.println();
    }
}

image

실행결과를 보면 알겠지만 전달한 인스턴스를 대상으로 스트림이 생성된다.

(추가)
4번 예시와 같이 Wrapper 클래스 배열이 인자로 전달될 경우에는 하나의 배열로 이뤄진 스트림이 생성되는 것이 아니라, 배열에 저장된 요소로 이뤄진 스트림이 생성된다.

- 기본 자료형을 지원하는 스트림

오토박싱을 생략하기 위해 기본 자료형을 지원하는 스트림 인터페이스가 있다.

import java.util.Arrays;
import java.util.stream.IntStream;

class CreateIntStream {
    public static void showIntStream(IntStream is) {
        is.forEach(n -> System.out.print(n + "\t"));
        System.out.println();
    }

    public static void main(String[] args) {
        // 인자로 전달하는 값을 스트림으로
        IntStream is3 = IntStream.of(7, 5, 3); 
        showIntStream(is3);

        // 숫자 5 부터 8 이전까지 스트림으로
        IntStream is4 = IntStream.range(5, 8); 
        showIntStream(is4);
     }
}

- 병렬 스트림으로 변경

이미 스트림을 생성한 상태에서 이를 기반으로 병렬 스트림을 생성할 수도 있다.

import java.util.List;
import java.util.Arrays;
import java.util.stream.Stream;
import java.util.function.BinaryOperator;

class ToParallelStream {
    public static void main(String[] args) {
        List<String> ls = Arrays.asList("Box", "Simple", "Complex", "Robot");
        Stream<String> ss = ls.stream();
        
        BinaryOperator<String> lc = 
            (s1, s2) -> { 
               if(s1.length() > s2.length())
                   return s1;
               else 
                   return s2;                   
            };
        
        String str = ss.parallel()
                      .reduce("", lc);
      
        System.out.println(str);
    }
}

- 스트림의 연결

두 개의 스트림을 연결하여 하나의 스트림을 생성할 수도 있다.

import java.util.Arrays;
import java.util.stream.Stream;

class ConcateStringStream {
    public static void main(String[] args) {
        Stream<String> ss1 = Stream.of("Cake", "Milk");
        Stream<String> ss2 = Stream.of("Lemon", "Jelly");
        
        // 스트림을 하나로 묶은 후 출력
        Stream.concat(ss1, ss2).forEach(s -> System.out.println(s));
     }
}

30-2. 스트림의 중간 연산

- 맵핑(Mapping)에 대한 추가 정리

Stream<T>의 map 메소드와 flatMap 메소드의 차이는 1:1 매핑이냐 1:* 매핑이나에 있다.

// map
<R> Stream<R> map(Function<T, R> mapper)

// flatMap (stream을 반환한다.)
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class FlatMapStream {
    public static void main(String[] args) {
        String ss = Stream.of("MY_AGE", "YOUR_LIFE")
                .flatMap(s -> Arrays.stream(s.split("_")))
                .collect(Collectors.joining("/"));

        System.out.println(ss);
    }
}

image

위의 예제보다 좀 더 현실적인 예제를 풀어보자.

import java.util.Arrays;
import java.util.stream.IntStream;

class ReportCard {
    private int kor;
    private int eng;
    private int math;

    public ReportCard(int kor, int eng, int math) {
        this.kor = kor;
        this.eng = eng;
        this.math = math;
    }

    public int getKor() {
        return kor;
    }

    public int getEng() {
        return eng;
    }

    public int getMath() {
        return math;
    }
}

public class GradeAverage {
    public static void main(String[] args) {
        ReportCard[] cards = {
                new ReportCard(70, 80, 90),
                new ReportCard(90, 80, 70),
                new ReportCard(80, 80, 80)
        };

        // 학생들의 전과목 평균 구하기
        double avg = Arrays.stream(cards)
                .flatMapToInt(c -> IntStream.of(c.getKor(), c.getEng(), c.getMath()))   // 점수들이 담긴 스트림들
                .average()  // IntStream, LongStream, DoubleStream에만 존재하는 메소드, OptionalDouble을 반환
                .getAsDouble();

        System.out.println("avg. " + avg);
    }
}

- 정렬

정렬 기능을 제공하는 중간 연산자 메소드들도 있다.

  • Comparable<T>
    • compareTo
  • Comparator<T>
    • compare

정렬을 위해서는 스트림을 구성하는 인스턴스가 Comparable<T>의 compareTo를 구현하고 있어야한다.
또, 일시적으로 정렬의 기준을 마련해주어야할 때 Comparator<T>의 compare 메소드 구현에 해당하는 람다식을 전달해야 한다.

// 인스턴스 대상 정렬
import java.util.stream.Stream;

class InstSortedStream {
    public static void main(String[] args) {
        Stream.of("Box", "Apple", "Robot")
            .sorted()
            .forEach(s -> System.out.print(s + '\t'));
        System.out.println();
        
        Stream.of("Box", "Apple", "Rabbit")
            .sorted((s1, s2) -> s1.length() - s2.length())
            .forEach(s -> System.out.print(s + '\t'));
        System.out.println();
    }
}
// 기본 타입 대상 정렬
import java.util.stream.IntStream;
import java.util.stream.DoubleStream;

class PrimitiveSortedStream {
    public static void main(String[] args) {
        IntStream.of(3, 9, 4, 2)
            .sorted()
            .forEach(d -> System.out.print(d + "\t"));
        System.out.println();

        DoubleStream.of(3.3, 6.2, 1.5, 8.3)
            .sorted()
            .forEach(d -> System.out.print(d + "\t"));
        System.out.println();
    }
}

- 루핑(Looping)

루핑: 스트림을 이루는 모든 데이터 각각을 대상으로 특정 연산을 진행하는 행위

대표적으로는 forEach가 있다.
이는 최종 연산이지만, 중간 연산에도 루핑을 위한 메소드가 있다.

Stream<T> peek(Consumer<? super T> action)
import java.util.stream.IntStream;

class LazyOpStream {
    public static void main(String[] args) {
        // 최종 연산이 생략된 스트림의 파이프라인
        IntStream.of(1, 3, 5)
            .peek(d -> System.out.print(d + "\t"));
        System.out.println();
  
        // 최종 연산이 존재하는 스트림의 파이프라인
        IntStream.of(5, 3, 1)
            .peek(d -> System.out.print(d + "\t"))
            .sum();            
        System.out.println();
    }
}

image
실행 결과를 통해 스트림의 지연 처리 특성을 볼 수 있고, peek 연산이 중간 연산임을 알 수 있다.

30-3. 스트림의 최종 연산

- sum(), count(), average(), min(), max()

메소드의 이름만 봐도 알 수 있듯이 이들은 수에 의미 있는 연산이다.
따라서 IntStream, LongStream, DoubleStream형 참조변수가 참조하는 스트림을 대상으로만 이 연산들이 가능하다.

// IntStream의 메소드들
int sum()
long count()
OptionalDouble average()
OptionalInt min()
OptionalInt max()
import java.util.stream.IntStream;

class OpIntStream {
    public static void main(String[] args) {
        // 합
        int sum = IntStream.of(1, 3, 5, 7, 9)
                          .sum();
        System.out.println("sum = " + sum);

        // 개수
        long cnt = IntStream.of(1, 3, 5, 7, 9)
                          .count();
        System.out.println("count = " + cnt);

        // 평균
        IntStream.of(1, 3, 5, 7, 9)
                .average()
                .ifPresent(av -> System.out.println("avg = " + av));

        // 최소
        IntStream.of(1, 3, 5, 7, 9)
                .min()
                .ifPresent(mn -> System.out.println("min = " + mn));

        // 최대
        IntStream.of(1, 3, 5, 7, 9)
                .max()
                .ifPresent(mx -> System.out.println("max = " + mx));
    }
}

- forEach

void forEach(Consumer<? super T> action)

스트림에 포함되어있는 데이터를 대상으로 action 메소드를 호출한다.
forEach와 peek은 각각 최종 연산과 중간 연산이라는 부분에서만 차이가 있다.

- allMatch, anyMatch, noneMatch

// 스트림의 모든 데이터가 조건을 만족하는가?
boolean allMatch(Predicate<? super T> predicate)

// 스트림의 데이터 중 조건을 만족하는 것이 하나라도 있는가?
boolean anyMatch(Predicate<? super T> predicate)

// 스트림의 모든 데이터가 조건을 만족하지 않는가?
boolean noneMatch(Predicate<? super T> predicate)
import java.util.stream.IntStream;

class MatchStream {
    public static void main(String[] args) {
        boolean b = IntStream.of(1, 2, 3, 4, 5)
                          .allMatch(n -> n%2 == 0);
        System.out.println("모두 짝수이다. " + b);

        b = IntStream.of(1, 2, 3, 4, 5)
                    .anyMatch(n -> n%2 == 0);
        System.out.println("짝수가 하나는 있다. " + b);

        b = IntStream.of(1, 2, 3, 4, 5)
                    .noneMatch(n -> n%2 == 0);
        System.out.println("짝수가 하나도 없다. " + b);
    }
}

- collect

최종으로 원하는 것이 reduction이 아닐 때 사용한다.

한 번 파이프라인을 통해서 가공되고 걸러진 데이터를 최종 연산 과정에서 별도로 저장하고 싶은 경우에 사용해준다.

<R> R collect(Supplier<R> supplier,
                BiConsumer<R, ? super T> accumulator,
                BiConsumer<R, R> combiner)
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CollectStringStream {
    public static void main(String[] args) {
        String[] words = {"Hello", "Box", "Robot", "Toy"};

        List<String> ls = Arrays.stream(words)
                .filter(s -> s.length() < 5)
                .collect(() -> new ArrayList<>(),   // 저장소
                        (c, s) -> c.add(s), // c: 저장소, s: 스트림에 있는 데이터
                        (lst1, lst2) -> lst1.addAll(lst2)); // 저장소 합치기

        System.out.println(ls);
    }
}

파이프를 통해 걸려진 데이터들을 저장소에 저장하는 코드이다.
마지막 인자는 순차 스트림일 경우에는 의미 없다.

- 병렬 스트림에서의 collect

만약 병렬 스트림을 대상으로 collect 메소드가 호출되면,
첫 번째 인자로 전달된 람다식을 기반으로 ‘다수의 저장소’가 생성되어,
두 번째 람다식을 기반으로 이 다수의 저장소에 데이터가 나뉘어 저장된다.

따라서, 저장이 끝난 뒤 이 다수의 저장소들을 하나로 묶는 과정을 거쳐야 하는데 이 때 사용되는 것이 세 번째 전달인자이다.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CollectParallelStringStream {
    public static void main(String[] args) {
        String[] words = {"Hello", "Box", "Robot", "Toy"};

        List<String> ls = Arrays.stream(words)
                .parallel()
                .filter(s -> s.length() < 5)
                .collect(() -> new ArrayList<>(),   // 저장소
                        (c, s) -> c.add(s), // c: 저장소, s: 스트림에 있는 데이터
                        (lst1, lst2) -> lst1.addAll(lst2)); // 저장소 합치기

        System.out.println(ls);
    }
}

앞선 포스트에서도 말했듯이 병렬 처리가 능사는 아니다.
처리해야 할 일에 비해 병렬 처리를 위한 전후 과정이 더 소모적인 경우에는 오히려 속도가 더 느려질 수도 있다.
따라서 병렬 처리를 결정했을 때는 테스트를 통해 병렬 처리의 적합성을 판단해야 한다.

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기