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

Update:     Updated:

카테고리:

태그:

29-1. 스트림의 이해와 스트림의 생성

- 스트림(Stream)의 이해

배열 또는 컬렉션 인스턴스에 저장된 데이터를 꺼내서 파이프에 흘려보낸다.

파이프에 흘려보내는 데이터의 흐름을 가리켜 ‘스트림’이라 한다.
그리고 데이터를 흘려보낼 파이프(연산)의 종류는 다음 두 가지로 나뉜다.

  • 중간 연산(Intermediate Operation): 마지막이 아닌 위치에서 진행 되어야 하는 연산
  • 최종 연산(Terminal Operation): 마지막에 진행이 되어야 하는 연산

- 스트림(Stream)의 첫 번째 예제

import java.util.*;
import java.util.strea.*;

class MyFirstStream {
    public static void main(String[] args) {
        int[] ar = {1, 2, 3, 4, 5};

        int sum = Arrays.stream(ar) // 스트림 생성(IntStream)
                        .filter(i -> i % 2 == 1)    // 중간 연산 진행(IntStream)
                        .sum(); // 최종 연산 진행(int)
        
        System.out.println(sum);
    }
}

- 스트림(Stream)의 특성

위의 작성한 코드를 부분 부분 살펴보자!

int sum = Arrays.stream(ar) // 스트림 생성하고,
                .filter(i -> i % 2 == 1)    // filter 통과시키고,
                .sum(); // sum을 통과 시켜 그 결과 반환

스트림의 연산은 효율과 성능을 고려하여 ‘지연(Lazy) 처리’ 방식으로 동작한다.
즉 최종 연산인 sum이 호출되어야 filter의 호출 결과가 스트림에 반영되고, 이어서 sum의 호출 결과가 스트림에 반영된다.

- 스트림 생성하기: 배열

Arrays 클래스의 static 메소드인 Arrays.stream()에 배열 참조값을 전달하면 된다.

public static <T> Stream<T> stream(T[] array)

다음의 예시를 보자.

import java.util.*;
import java.util.stream.*;

class StringStream {
    /*
    forEach(Consumer<? super T> action)
    Consumer<T> voice accept(T t)
    */

    public static void main(String[] args) {
        String[] names = {"YOON", "LEE", "PARK"};

        Stream<String> stm = Arrays.stream(names);  // 스트림 생성
        stm.forEach(s -> System.out.println(s));    // 최종 연산 진행
        // stm.forEach(System.out::println);    // 메소드 참조 방식
    }
}

대상 배열의 특정 부분만 뽑아내서 스트림으로 생성하는 방법도 있다.

import java.util.*;

class IntStream {
    public static void main(String[] args) {
        int[] intArray = {1,2,3,4,5};

        IntStream ism = Arrays.stream(insArray, 1, 3);  // 인덱스 1부터 3 이전 까지
    }
}

- 스트림 생성하기: 컬렉션 인스턴스

컬렉션 인스턴스를 대상으로 인스턴스 메소드 stream()을 호출

import java.util.*;

class ListStream {
    public static void main(String[] args) {
        List<String> ls = Arrays.asList("Toy", "Robot", "Box");
        Stream<String> stm = ls.stream();
    }
}

29-2. 필터링(Filtering)과 맵핑(Mapping)

- 필터링(Filtering)

// Predicate<T>  boolean test(T t)
Stream<T> filter(Predicate<? super T> predicate)

filter 메소드는 내부적으로 스트림의 데이터를 하나씩 인자로 전달하면서 test를 호출한다.
그리고 그 결과가 true가 반환되면 해당 데이터는 스트림에 남긴다.

- 맵핑(Mapping)

// Function<T, R>  R apply(T t)
<R> Stream<R> map(Function<? super T, ? extends R> mapper)

map은 내부적으로 스트림의 데이터를 하나씩 인자로 전달하며 apply 메소드를 호출한다.
그리고 그 결과로 반환되는 값을 모아 새로운 스트림을 생성한다.

추가)
map의 인자로 apply 함수에 대한 람다식이 전달되기 때문에 반환과정에서 오토 박싱이 진행된다.
따라서 기본 자료형의 값을 반환하는 경우를 고려한 메소드도 있다.

import java.util.*;

class MapToInt {
    public static void main(String[] args) {
        List<String> ls = Arrays.asList("Box", "Robot", "Simple");

        // 오토 박싱 진행
        ls.stream()
            .map(s -> s.length())
            .forEach(i -> System.out.print(i + " "));

        // 오토 박싱 생략
        ls.stream()
            .mapToInt(s -> s.length())
            .forEach(i -> System.out.print(i + " "));
    }
}

- 종합 예제

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

class ToyPriceInfo {
    private String model;
    private int price;

    public ToyPriceInfo(String model, int price) {
        this.model = model;
        this.price = price;
    }

    public int getPrice() {
        return price;
    }
}

public class ToyStream {
    public static void main(String[] args) {
        List<ToyPriceInfo> ls = new ArrayList<>();
        ls.add(new ToyPriceInfo("GUN", 200));
        ls.add(new ToyPriceInfo("TEDDY", 350));
        ls.add(new ToyPriceInfo("CAR", 550));

        // 정가 500원 미만인 장난감 가격의 총 합
        int sum = ls.stream()
                .filter(t -> t.getPrice() < 500)
                .mapToInt(t -> t.getPrice())
                .sum();

        System.out.println(sum);
    }
}

29-3. 리덕션(Reduction), 병렬 스트림(Parallel Streams)

- 리덕션과 reduce 메소드

리덕션(Reduction): 데이터를 축소하는 연산

앞서 보인 sum도 리덕션 연산에 해당한다.
다른 리덕션 연산의 경우 연산의 내용이 이미 정해진 상태지만
다음 메소드를 통해 전달하는 람다식에 의해 연산의 내용을 결정지을 수도 있다.

// BinaryOperator<T>  T apply(T t1, T t2)
T reduce(T identity, BinaryOperator<T> accumulator)

reduce는 내부적으로 apply 메소드를 호출하면서 스트림에 저장된 데이터를 다음과 같은 방식으로 줄여 나간다.
image

import java.util.*;
import java.util.function.*;

class ReductionStream {
    public static void main(String[] args) {
        List<String> ls = Arrays.asList("Box", "Simple", "Complex", "Robot");

        // 길이가 더 긴 문자열을 반환하는 람다식
        BinaryOperator<String> lc = (s1, s2) -> {
            if (s1.length() > s2.length()) {
                return s1;
            }
            return s2;
        };

        String str = ls.stream()
                        .reduce("", lc);
        
        System.out.println(str);
    }
}

reduce 메소드는 ‘첫 번째 인자로 전달된 값’을 스트림이 빈 경우에 반환을 한다.
뿐만 아니라 스트림이 비어 있지 않은 경우에는 이를 스트림의 첫 번째 데이터로 간주하고 리덕션을 진행한다.

- 병렬 스트림(Parallel Streams)

하나의 작업을 둘 이상의 작업으로 나누어서 동시에 진행하는 것을 가리켜 ‘병렬 처리’라 한다.

import java.util.*;
import java.util.function.*;

class ReductionStream {
    public static void main(String[] args) {
        List<String> ls = Arrays.asList("Box", "Simple", "Complex", "Robot");

        // 길이가 더 긴 문자열을 반환하는 람다식
        BinaryOperator<String> lc = (s1, s2) -> {
            if (s1.length() > s2.length()) {
                return s1;
            }
            return s2;
        };

        String str = ls.parallelStream()    // 병렬 처리를 위한 스트림 생성
                        .reduce("", lc);
        
        System.out.println(str);
    }
}

병렬 스트림을 생성하면 이어지는 연산들은 CPU의 코어 수를 고려하여 적절하게 병렬로 처리된다.

image

이렇게 병렬처리의 핵심은 연산의 횟수를 줄이는데 있지 않고 연산의 단계를 줄이는데 있다.
다만, 작업을 어떻게 나눌 것인지, 몇 개로 나눌 것인지 등에 따라 효율이 달라지기 때문에 무조건적으로 병렬처리가 좋다고 할 수 없음에 주의하자.

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

댓글 남기기