[Java8] Stream의 특징, 대표적인 중간 연산, 종료 연산 함수 사용법과 분석

    Stream은 Java8에서 처음 도입 되었으며, stream은 연속된 데이터들을 처리할 수 있는 기능의 모음이다. 

     

    stream 작업은 아래 그림과 같이 stream 파이프라인(pipeline)으로 구성되는데 1개의 소스(Array, Collection 등)과 0개이상의 중간 작업과 마지막 1개의 종료 작업으로 끝이 난다.

     

    Strem 특징

    1. 스트림 소스 생성 종료 작업은 1개만, 중간연산은 여러개 가능하다.

    스트림 소스 생성과 종료 작업은 1개만 있을수 있어서 중간 작업과 같이 여러개 있을 수 없다. 만약 한개의 스트림 파이프라인에서 종료 작업을 여러번 하면 에러가 난다. 그리고 중간 작업을 여러번 한다고 해서 원본 소스 데이터를 변경하지 않는다.

     

    2. 지연 연산(LAZY Evaluation)

    또한 종료 작업이 있기 전까지는 Stream은 작업은 하지 않는다. 이것을 LAZY Evaluation 이라고 한다.

    names.stream() //생성 작업
            .filter(s -> !s.startsWith("toby"))
            .map(s -> {
                System.out.println(s);
                return s.toUpperCase();
            }); //중간 작업

    위 코드에서 map 구현부 안에 출력하는 코드를 넣어도 실행시켠 아무것도 출력되지 않는다. 왜냐하면 종료 작업을 하기 전에는 그냥 stream을 선언만 해놓은 것이기 때문이다.

     

    아래 그림을 보면 쉽게 이해 된다. 종료 작업이 있어야 스트림이 동작한다.

     

    3. 병렬처리가 가능하다.

    stream() 으로 생성 되신에 parallelStream() 함수를 사용하여 병렬 처리가 가능하지만 사용한다고 해서 무조건 적으로 빠른 처리가 되는 것은 아니다. 스레드를 만드는 비용과 스레드간의 컨텍스트 스위칭 비용 때문인데 데이터가 정말 방대할 때 사용해야 한다. 

    병렬처리를 사용하기 전에는 성능 측정을 꼭 해봐서 사용해야 한다.

     

    Stream 사용 예제

    public class App {
        public static void main(String[] args) {
            List<String> names1 = new ArrayList<>();
            names1.add("chovy");
            names1.add("viper");
    
            names1.stream()
                    .map(s -> s.toUpperCase())
                    .forEach(System.out::println);
        }
    }

     

    [출력]

     

    위 예제를 보면 list에는 소문자 이름이 여러개 들어갔다. Collections인 List객체에서 stream() 함수를 통해 stream을 생성하여 스트림 파이프라인이 생성되었고 중간 연산인 map() 함수에서 대문자로 변경 그리고 종료 함수인 forEach() 함수를 실행하면서 출력되고 stream 파이프라인을 완성 시켰다.

     

    Stream 중간 연산 함수 분석

    중간연산은 Stream 인터페이스에서 반환값이 Stream이다. 만약 반환값이 Stream이 아니라면 종료 연산자라고 보면 된다.

     

    map() 함수

    중간 연산 작업인 map() 함수를 보면 아래와 같다. 반환값이 Stream이고 함수형 인터페이스 Function이 매개변수로 받는다.

     

    Function 함수는 apply 함수를 구현해야 하고 특징은 T타입을 받아서 R타입을 반환한다. 따라서 같은 타입을 반환해도 되고 안해도 된다.

     

    mapToInt() 함수

    그러면 mapToInt() 함수는 어떨까? 매개변수로 ToIntFunction 함수형 인터페이스가 있다.

     

    ToIntFunction 함수는 T 타입 변수를 받아서 int 타입을 반환시킨다.

     

    그래서 아래처럼 toUpperCase() 함수로 String 타입을 반환 받을수 없으며 length() 함수 같은 숫자값을 반환받아야 한다.

     

    filter() 함수

    그리고 많이 사용되는 filter() 함수로 리스트를 걸러낼 수 있다. 아래 소스를 보면 앞글자가 toby 로 시작한것만 추출한걸 볼 수 있다.

    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names1.add("chovy");
        names1.add("viper");
        names1.add("toby");
        names1.add("toby2");
        names1.add("toby3");
        names1.add("gumayusi");
    	names1.add("toby4");
        
        names1.stream() //생성 작업
                .filter(s -> s.startsWith("toby"))
                .map(s -> s.toUpperCase()) //중간 작업
                .forEach(System.out::println); //종료 작업
    }

     

    [출력]

     

    filter는 Predicate 함수형 인터페이스를 매개변수로 받고 있다.

     

    그리고 Predicate은 T 타입변수를 받고 boolean 타입을 반환한다. 즉 true 반환값이 true인것만 추출하는 기능이다.

     

     

    concat()  함수

    concat() 함수로 2개의 스트림을 한개로 합칠수 있다.

    List<String> names1 = new ArrayList<>();
    names1.add("chovy");
    names1.add("viper");
    
    List<String> names2 = new ArrayList<>();
    names2.add("gumayusi");
    names2.add("jeus");
    
    //2개 stream 합치는법
    Stream.concat(names1.stream(), names2.stream())
            .forEach(System.out::println);

     

    flatMap() 함수

    flatMap() 함수는 리스트안의 리스트 Stream으로 풀어낼 수 있다. 만약에 리스트 3개 이상을 한 스트림에 넣고 싶다면 각각의 리스트를 한곳에 담아서 그 리스트의 stream 함수 중에 flatMap() 이 있다. 이것은 리스트 안에 있는 리스트들을 풀어서 하나의 stream 으로 만들어 준다.

    List<String> names1 = new ArrayList<>();
        names1.add("chovy");
        names1.add("viper");
    
        List<String> names2 = new ArrayList<>();
        names2.add("gumayusi");
        names2.add("jeus");
    
        List<String> names3 = new ArrayList<>();
        names3.add("faker");
        names3.add("deft");
    
        List<String> names4 = new ArrayList<>();
        names4.add("zeka");
        names4.add("marin");
    
        List<List<String>> lists = new ArrayList<>();
        lists.add(names1);
        lists.add(names2);
        lists.add(names3);
        lists.add(names4);
    
        lists.stream()
                .flatMap(list -> list.stream())
                .map(s -> s.toUpperCase())
                .forEach(System.out::println);
    }

     


     

    Stream() 대표 종료 함수

    종료 함수의 특징으로는 Stream 인터페이스에서 반환값이 Stream이 아니다.

     

    forEach() 함수

    위에서 사용된 forEach() 함수는 반환값이 없고 Consumer 함수형 인터페이스가 매개변수 이다.

     

    Consumer 인터페이스는 타입 인수 T를 받아서 반환값이 없는 함수이다. 선언부만 실행하고 아무 값도 반환 받을 필요가 없을때 사용된다.

    위에서 계속 forEach() 함수를 사용한 이유는 Stream 데이터들을 출력하고 아무값도 반환 받을 필요가 없기 때문이다.

     

     

    Collect() 함수

    만약에 Strem작업이 완성되고 새로운 Collection 으로 반환 받으려면 어떻게 해야할까 종료 함수로 collect() 함수를 쓰면 된다.

    List<String> names2 = new ArrayList<>();
    names2.add("gumayusi");
    names2.add("jeus");
    
    List<String> list = names2.stream()
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList());

     

    사용법은 API Note를 참고 하였다. List 타입을 반환 받고 싶으면 Collectors.toList() 함수를 사용 하면 된다.

     

     

    anyMatch() 함수

     anyMatch() 함수를 사용하여 boolean 값을 반환 받을 수 있다.

    List<String> names3 = new ArrayList<>();
    names3.add("faker");
    names3.add("deft");
    
    boolean is2 = names2.stream()
            .map(s -> s.toUpperCase())
            .anyMatch(s -> s.contains("FAKER"));

     

    또 여러가지 집계 종료 함수도 제공하기도 한다. 아래는 sum을 사용하여 리스트의 각 글자 길이를 합한 수를 구하였다.

    List<String> names4 = new ArrayList<>();
    names4.add("zeka");
    names4.add("marin");
    
    int cnt = names4.stream()
            .mapToInt(s -> s.length())
            .sum();

     

     

    참고

     - 이것이 자바다 / 신용권 / 한빛미디어

     - 더 자바, Java8 강의 / 백기선 /인프런

    댓글

    Designed by JB FACTORY