ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Stream
    언어/Java 2021. 2. 27. 18:38

    Stream

    자바 8에서 추가된 스트림은 람다를 활용할 수 있는 기술 중 하나로 배열과 컬렉션을 함수형으로 처리할 수 있습니다.

    스트림 생성

    배열

    String[] arr = new String[]{"1", "2", "3"};
    Stream<String> stream = Arrays.stream(arr);

    컬렉션

    List<String> list = Arrays.asList("1", "2", "3");
    Stream<String> stream = list.stream();

    stream.of

    stream.of() 메소드를 사용하면 스트림 객체를 바로 생성할 수 있습니다.

    Stream<String> stream = Stream.of("1", "2", "3");
    // [1, 2, 3]

    빈 리스트

    Stream<String> stream  = Stream.empty();

    builder

    builder를 사용하면 사용자가 원하는 값을 입력할 수 있습니다. builder 사용 후, 마지막에 build() 메소드로 스트림을 리턴합니다.

    Stream<String> builderStream = Stream.<String>builder()
            .add("1")
            .add("2")
            .add("3")
            .build();
    // [1, 2, 3]

    generate

    generate를 사용하면 파라미터에 람다를 입력해 람다에서 리턴하는 값으로 스트림을 구성합니다. 생성되는 스트림의 크기는 무한이기 때문에 사이즈를 제한하는 것이 필요합니다.

    Stream<String> generatedStream = Stream.generate(() -> "1").limit(3);
    // [1, 1, 1]

    iterate

    iterate 메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만듭니다. 다음 예제에서는 30이 초기값이고 값이 2씩 증가하는 값들이 들어가게 됩니다. 즉 요소가 다음 요소의 인풋으로 들어갑니다. 이 방법도 스트림의 사이즈가 무한하기 때문에 특정 사이즈로 제한해야 합니다.

    iterate를 사용하면 초기값, 해당 값을 다루는 람다를 사용해 스트림에 들어갈 요소를 만들어 냅니다. 1, 3, 5, 7, 9, ... 와 같은 형식을 만들 수 있습니다.

    Stream<Integer> iteratedStream = Stream.iterate(1, n -> n + 2).limit(10);
    // [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

    기본 타입

    제네릭을 사용하지 않고 직접 해당 타입의 스트림을 다룰 수 있습니다.

    IntStream intStream = IntStream.range(1, 5); 
    // [1, 2, 3, 4]

    제네릭을 사용하지 않기 때문에 오토박싱이 발생하지 않습니다. 만약 필요하다면 boxed() 메소드를 사용할 수 있습니다.

    Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();

    스트림 가공

    스트림은 내가 원하는 값을 추출하거나 타입을 변형하는 등의 가공을 할 수 있는데 이러한 작업은 스트림을 반환하기 때문에 여러 작업을 붙여서 사용할 수 있습니다.

    filtering

    Stream<String> stream = Stream
            .of("1", "2", "3", "123")
            .filter(name -> name.contains("1"));
    // [1, 123]

    mapping

    map은 스트림 내 요소들을 하나씩 특정 값으로 변환합니다. 이때 값을 변환하기 위한 람다를 인자로 받습니다. 스트림에 들어가 있는 값이 인자가 되어 사용자가 원하는 변환을 거친 후, 새로운 스트림으로 반환합니다.

    Stream<String> stream = Stream
            .of("a", "b", "c", "d")
            .map(String::toUpperCase);
    // [A, B, C, D]

    sorting

    인자가 없을 경우, 오름차순으로 정렬합니다.

    Stream<Integer> stream = Stream
            .of(1, 5, 3, 2, 4)
            .sorted();
    // [1, 2, 3, 4, 5]

    peek

    스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드로는 peek 이 있습니다. ‘peek’ 은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer 를 인자로 받습니다.

    라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않습니다. 다음처럼 작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있습니다.

    스트림 내 요소들 각각 대상으로 특정 연산을 수행합니다. 정의된 작업을 수행만 하고 결과에는 영향을 주지 않습니다.

    int sum = IntStream.of(1, 2, 3, 4, 5)
        .peek(System.out::println)
    
    //        1
    //        2
    //        3
    //        4
    //        5

    스트림 결과처리

    count

    스트림 내의 요소 개수를 반환합니다. 스트림이 비어있는 경우 0을 반환합니다.

    long count = IntStream.of(1, 3, 5, 7, 9).count();
    // 5
    long count1 = Stream.of("a", "v").count();
    // 2

    sum

    count()와 마찬가지로 스트림이 비어있는 경우 0을 반환합니다.

    long sum = IntStream.of(1, 3, 5, 7, 9).sum();
    // 25

    min, max

    min과 max는 스트림이 비어있는 경우 값을 표현할 수 없기 때문에 Optional을 이용해 반환합니다.

    OptionalInt min = IntStream.of(1, 2, 3, 4, 5).min();
    // OptionalInt[1]
    OptionalInt max = IntStream.of(1, 2, 3, 4, 5).max();
    // OptionalInt[5]
    OptionalInt empty = IntStream.of().min();
    // OptionalInt.empty

    ifPresent

    스트림에서 ifPresent() 메소드를 사용해 Optional을 바로 표현할 수 있습니다.

    IntStream
            .of()
            .min()
            .ifPresent(System.out::println);
    // 출력안됨
    IntStream
            .of(1, 2, 3, 4, 5)
            .min()
            .ifPresent(System.out::println);
    // 1

    reduce

    reduce() 메소드는 총 3가지의 파라미터를 받습니다.

    • accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
    • identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
    • combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.

    마지막 세 번째 파라미터는 병렬스트림이 아니면 실행되지 않습니다.

    1개의 파라미터만 주었을 때입니다.

    OptionalInt reduced = IntStream.range(1, 4)
            .reduce((a, b) -> { 
                return Integer.sum(a, b); 
            });
    // OptionalInt[6]
    
    // 위 값은 아래와 같이 간소화할 수 있음
    OptionalInt reduced = IntStream.range(1, 4)
            .reduce(Integer::sum);
    // OptionalInt[6]

    다음은 2개의 파라미터를 주었을 때입니다.

    int reduced = IntStream.range(1, 4)
            .reduce(10, Integer::sum);
    // 16

    마지막으로 3개의 파라미터를 모두 주었을 때입니다. 이때, parellelStream() 메소드로 선언해주어야 동작합니다.

    Integer reduced = Arrays.asList(1, 2, 3)
            .parallelStream()
            .reduce(10,
                    Integer::sum,
                    (a, b) -> {
                        System.out.println(a+b+" call ");
                        return a + b;
                    });
    // 25 call 
    // 36 call 
    // 36

    세 번째 인자의 람다식은 각자의 스레드에서 실행한 결과를 마지막에 합치는 단계입니다. 동작 방식은 다음과 같습니다.

    1. 초기값 10에 각 스트립을 더한 세개의 값 (10+1, 10+2, 10+3)을 계산
    2. combiner는 identity 와 accumulator를 가지고 여러 스레드로 나눠 계산한 결과를 합침
      1. 12 + 13 진행
      2. a의 결과인 25 + 11 진행
      3. 결과 36 반환

    연산량이 많은 경우, 해당 방식을 사용해야 효율이 좋습니다.

    matching

    매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다.

    • 하나라도 조건을 만족하는 요소가 있는지(anyMatch)
    • 모두 조건을 만족하는지(allMatch)
    • 모두 조건을 만족하지 않는지(noneMatch)

    조건식 람다인 Predicate를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 반환합니다.

    • anyMatch: 1개라도 조건을 만족하는지
    • allMatch: 전부 만족하는지
    • noneMatch: 모든 조건이 만족하지 않은지
    List<String> names = Arrays.asList("1", "2", "3", "4", "5");
    
    boolean anyMatch = names.stream()
            .anyMatch(num -> num.contains("1"));
    boolean allMatch = names.stream()
            .allMatch(num -> !num.isBlank());
    boolean noneMatch = names.stream()
            .noneMatch(name -> name.equals("10"));
    
    System.out.println(anyMatch);
    // true
    System.out.println(allMatch);
    // true
    System.out.println(noneMatch);
    // true

    collecting

    Collector 타입의 인자를 받아서 처리하며 보통 자주 사용하는 작업은 Collectors 객체에서 제공합니다.

    Collectors.toList()

    스트림의 작업결과를 리스트로 반환합니다.

    Stream<String> stream = Stream
            .of("1", "2", "3", "123")
            .filter(name -> name.contains("1"));
    
    System.out.println(stream);
    // java.util.stream.ReferencePipeline$2@56cbfb61
    
    System.out.println(stream.collect(Collectors.toList()));
    // [1, 123]

    Collectors.joining()

    스트림의 작업 결과를 하나의 스트링으로 반환합니다.

    String stream = Stream
            .of("a", "b", "c", "d")
            .collect(Collectors.joining());
    
    System.out.println(stream);
    // abcd

    Collectors.averageInt

    Double average = Stream
            .of("1", "2", "3", "4", "5")
            .collect(Collectors.averagingInt(Integer::parseInt));
    
    System.out.println(average);
    // 3.0

    Collectors.summarizingInt()

    평균, 합계, 개수 등등을 한번에 구할 때 사용합니다.

    IntSummaryStatistics info = Stream
            .of("1", "2", "3", "4", "5")
            .collect(Collectors.summarizingInt(Integer::parseInt));
    
    System.out.println(info);
    // IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}

    Collectors.groupingBy

    특정 조건으로 요소들을 그룹화 할 수 있습니다.

    class Info {
        private final int id;
        private final String name;
    
        public Info(int id, String name) {
            this.id = id;
            this.name = name;
        }
    
        public int getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
    List<Info> infoList = Arrays.asList(
            new Info(1, "aa"),
            new Info(2, "bb"),
            new Info(3, "cc"),
            new Info(2, "dd"),
            new Info(5, "ee")
    );
    
    Map<Integer, List<Info>> result = infoList
            .stream()
            .collect(Collectors.groupingBy(Info::getId));
    
    System.out.println(result);
    // {1=[com.company.Info@1d251891], 2=[com.company.Info@48140564, com.company.Info@58ceff1], 3=[com.company.Info@7c30a502], 5=[com.company.Info@49e4cb85]}
    

    결과는 Map 타입으로 반환되고 groupingBy의 조건이 같으면 해당 값들을 리스트로 묶어 반환합니다.

    Collectors.partitioningBy

    groupingBy가 함수형 인터페이스인 Function을 이용해서 특정 값을 기준으로 스트림 내 요소를 묶었다면 partitioningBy는 함수형 인터페이스인 Predicate를 받습니다. Predicate는 인자를 받아 boolean 타입의 값을 반환합니다.

    Map<Boolean, List<Info>> result = infoList
            .stream()
            .collect(Collectors.partitioningBy(i -> i.getId() > 3));
    
    System.out.println(result);
    // {false=[com.company.Info@4c203ea1, com.company.Info@27f674d, com.company.Info@1d251891, com.company.Info@48140564], true=[com.company.Info@58ceff1]}

    댓글