Java8 In Action

[Part2] Java8 In action - Chapter4 - 2

신나게개발썰 2022. 7. 28. 09:24
반응형

해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.

좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!

4.3 스트림과 컬렉션

Entity

Dish.java

package Part2.Chapter4.Chapter4_4_3.entiity;

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {    
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return this.name;
    }

    public int getCalories() {
        return this.calories;
    }

    public Type getType() {
        return this.type;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public enum Type {
        MEAT, FISH, OTHER
    }
}

Main

package Part2.Chapter4.Chapter4_4_3;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import Part2.Chapter4.Chapter4_4_3.entiity.Dish;

/*
 * 4.3 스트림과 컬렉션
 * 
 *  자바의 기존 컬렉션과 새로운 스트림은 모두 연속된 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.
 *  여기서 "연속된(sequenced)"이라는 표현은 순서와 상관없이 아무 값에나 접속하는 것이 아니라
 *  순차적으로 값에 접근한다는 것을 의미한다.
 *  
 *  스트림과 컬렉션의 큰 차이는 데이터를 언제 계산하느냐이다.
 *  컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조다.
 *  즉, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.
 *  (요소를 추가 혹은 삭제할 수 있다. 이런 연산을 수행할 때마다 컬렉션의 모든 요소를 메모리에 저장해야 하며
 *  컬렉션에 추가하려는 요소는 미리 계산되어야 한다.)
 *  
 *  스트림은 이론적으로 "요청할 때만 요소를 계산"하는 고정된 자료구조다.
 *  (스트림에 요소를 추가하거나 요소를 제거할 수 없다.)
 *  결과적으로 스트림은 생산자(producer)와 소비자(consumer)관계를 형성한다.
 *  또한 스트림은 게으르게 만들어지는 컬렉션과 같다.
 *  즉, 사용자가 데이터를 요청할 때만 값을 계산한다
 *  (경영학에서는 이를 요청 중심 제조[demand-driven manufacturing]) 또는 즉석 제조[just-in-time manufacturing]라고 부른다.)
 *  반면 컬렉션은 적극적으로 생성된다.
 *  (생산자 중심[supplier-driven:팔기도 전에 창고를 가득 채움])
 *  
 *  컬렉션은 DVD에 비유할 수 있고, 스트림은 인터넷 스트리밍에 비유할 수 있다.
 *  DVD로 비디오를 보기 위해서는 DVD에 모든 파일들이 로드 될 때까지 기달려야 하지만
 *  인터넷 스트리밍은 현재 받은 바이너리로 비디오를 볼 수 있다.  
 */
public class Main_4_3 {

    static public void main(String[] args) {
        List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT)    
                , new Dish("beef", false, 700, Dish.Type.MEAT)                
                , new Dish("chicken", false, 400, Dish.Type.MEAT)
                , new Dish("french fries", true, 530, Dish.Type.OTHER)
                , new Dish("rice", true, 350, Dish.Type.OTHER)
                , new Dish("season fruit", true, 120, Dish.Type.OTHER)
                , new Dish("pizza", true, 550, Dish.Type.OTHER)
                , new Dish("prawns", false, 300, Dish.Type.FISH)
                , new Dish("salmon", false, 450, Dish.Type.FISH));

        /*
         * 4.3.1 딱 한 번만 탐색할 수 있다!
         * 
         * 반복자와 마찬가지로 스트림도 한 번만 탐색할 수 있다.
         * 즉, 탐색된 스트림의 요소는 소비(consume)된다.
         * 한 번 탐색한 요소를 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다.
         * (그러려면 컬렉션처럼 반복 사용할 수 있는 데이터 소스여야 한다.
         * 만일 데이터 소스가 I/O 채널이라면 소스를 반복 사용할 수 없으므로 새로운 스트림을 만들 수 없다.)
         */
        List<String> title = Arrays.asList("Java8", "In", "Action");
        Stream<String> s = title.stream();
        s.forEach(System.out::println);
        // 스트림이 이미 소비되었거나 닫혔기 때문에 에러가 발생한다.
        // s.forEach(System.out::println);

        /*
         * 4.3.2 외부 반복과 내부 반복
         * 
         * 컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 한다.(예를 들어 for-each등을 사용)
         * 이를 외부 반복(external iteration)이라고 한다.
         * 반면 스트림 라이브러리는 내부 반복(internal iteration)을 사용한다.
         * (반복을 알아서 처리하고 결과 스트림값을 어딘가에 저장한다.) 
         */
        // 컬렉션 : for-each 루프를 이용하는 외부 반복
        List<String> names = new ArrayList<>();
        for(Dish d : menu) {
            names.add(d.getName());
        }
        names.stream().forEach((name) -> System.out.println("컬렉션 : for-each 루프를 이용하는 외부 반복 : " + name));

        /*
         * for-each 구문은 반복자를 사용하는 불편함을 어느 정도 해결해준다.
         * for-each를 이용하면 Iteratorr 객체를 이용하는 것보다 더 쉽게 컬렉션을 반복할 수 있다. 
         */

        // 컬렉션 : 내부적으로 숨겨졌던 반복자를 사용한 외부 반복(Iteratorr 이용)
        names = new ArrayList<>();
        Iterator<Dish> iterator = menu.iterator();

        while(iterator.hasNext()) {
            Dish d = iterator.next();
            names.add(d.getName());
        }
        names.stream().forEach((name) -> System.out.println("컬렉션 : 내부적으로 숨겨졌던 반복자를 사용한 외부 반복 : " + name));

        // 스트림 : 내부 반복
        names = menu.stream()
            // map 메서드를 getName 메서드로 파라미터화해서 요리명을 추출.
            .map(Dish::getName)
            // 파이프라인을 실행한다. 반복자는 필요 없다.
            .collect(Collectors.toList());
        names.stream().forEach((name) -> System.out.println("스트림 : 내부 반복 : " + name));

        /*
         * 컬렉션은 "외부적으로" 반복, 즉 명시적으로 컬렉션의 항목을 하나씩 가져와서 처리한다.
         * 그러나 스트림의 내부 반복을 이용하면 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리할 수 있다.
         * 스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택한다.
         * 반면 for-each를 이용하는 외부 반복에서는 병렬성을 스스로 관리해야 한다.
         * (병렬성을 "스스로 관리"한다는 것은 병렬성을 포기하던가 synchronized 사용함을 뜻한다.) 
         */
    }
}

4.4 스트림 연산

Entity

Dish.java

package Part2.Chapter4.Chapter4_4_4.entity;

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {    
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return this.name;
    }

    public int getCalories() {
        return this.calories;
    }

    public Type getType() {
        return this.type;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public enum Type {
        MEAT, FISH, OTHER
    }
}

Main

package Part2.Chapter4.Chapter4_4_4;

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

import Part2.Chapter4.Chapter4_4_4.entity.Dish;

/*
 * 4.4 스트림 연산
 * 
 * 스트림 인터페이스의 연산을 크게 두 가지로 구분할 수 있다.
 */
public class Main_4_4 {

    public static void main(String[] args) {
        List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT)    
                , new Dish("beef", false, 700, Dish.Type.MEAT)                
                , new Dish("chicken", false, 400, Dish.Type.MEAT)
                , new Dish("french fries", true, 530, Dish.Type.OTHER)
                , new Dish("rice", true, 350, Dish.Type.OTHER)
                , new Dish("season fruit", true, 120, Dish.Type.OTHER)
                , new Dish("pizza", true, 550, Dish.Type.OTHER)
                , new Dish("prawns", false, 300, Dish.Type.FISH)
                , new Dish("salmon", false, 450, Dish.Type.FISH));

        /*
         * 아래 스트림은 연산을 두 그룹으로 구분할 수 있다.
         * filter, map, limit는 서로 연결되어 파이프라인을 형성한다.
         * collect로 파이프라인을 실행한 다음에 닫는다.
         * 
         * 연결할 수 있는 스트림 연산을 중간 연산(intermediate operation)이라고 하며,
         * 스트림을 닫는 연산을 최종 연산(terminal operation)이라고 한다.
         */
        System.out.println(menu
            // 요리 리스트에서 스트림 얻기
            .stream()
            // 중간 연산
            .filter((d) -> d.getCalories() > 300)
            // 중간 연산
            .map(Dish::getName)
            // 중간 연산
            .limit(3)
            // 최종 연산(스트림을 리스트로 변환)
            .collect(Collectors.toList() ));

        /*
         * 4.4.1 중간 연산
         * 
         * filter나 sorted 같은 중간 연산은 다른 스트림을 반환한다.
         * 따라서 여러 중간 연산을 연결해서 질의를 만들 수 있다.
         * 중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는
         * 아무 연산도 수행하지 않는다는 것, 즉 게으르다(lazy)는 것이다.
         * 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한 번에 처리하기 때문이다.
         * 
         * 스트림의 게으른 특성 덕분에 몇 가지 최적화 효과를 얻을 수 있었다.
         *         1. 300칼로리가 넘는 요리는 여러 개지만 오직 처음 3개만 선택되었다.
         *         이는 limit 연산 그리고 "쇼트서킷"이라 불리는 기법 덕분이다.
         *         2. filter와 map은 서로 다른 연산이지만 한 과정으로 병합되었다.
         *         이 기법을 루프 퓨전(loop fusion)이라고 한다. 
         */
        System.out.println(menu.stream()
            .filter((d) -> {
                System.out.println("filtering : " + d.getName());
                return d.getCalories() > 300;    
            })
            .map((d) -> {
                System.out.println("mapping : " + d.getName());
                return d.getName();
            })
            .limit(3)
            .collect(Collectors.toList() ));

        /*
         * 4.4.2 최종 연산
         * 
         * 최종 연산은 스트림 파이프라인에서 결과를 도출한다. 보통 최종 연산에 의해
         * List, Integer, void 등 스트림 이외의 결과가 반환된다.
         */
        // 파이프라인에서 forEach는 소스의 각 요리에 람다를 적용한 다음에 void를 반환하는 최종 연산이다.
        menu.stream().forEach(System.out::println);

        /*
         * 4.4.3 스트림 이용하기
         * 
         * - 질의를 수행할(컬렉션 같은) "데이터 소스"
         * - 스트림 파이프라인을 구성할 "중간 연산" 연결
         * - 스트림 파이프라인을 실행하고 결과를 만들 "최종 연산"
         * 
         * 스트림 파이프라인의 개념은 빌더 패턴(builder pattern)과 비슷한다.
         * 빌드 패턴에서는 호출을 연결해 설정을 만든다(중간 연산을 연결하는 것과 같다.)
         * 그리고 준비된 설정에 build 메서드를 호출한다.(최종 연산에 해당함)
         * 
         * 중간연산
         * 연산 : filter
         * 형식 : 중간 연산
         * 반환 형식 : Stream<T>
         * 연산의 인수 : Prdeicate<T>
         * 함수 디스크립터 : T -> boolean
         * 
         * 연산 : map
         * 형식 : 중간 연산
         * 반환 형식 : Stream<T> 
         * 연산의 인수 : Function<T,R> 
         * 함수 디스크립터 :T -> R
         *
          * 연산 : limit
         * 형식 : 중간 연산
         * 반환 형식 : Stream<T> 
         * 연산의 인수 :  
         * 함수 디스크립터 :
         * 
         * 연산 : sorted
         * 형식 : 중간 연산
         * 반환 형식 : Stream<T> 
         * 연산의 인수 : Comparator<T> 
         * 함수 디스크립터 : (T, T) -> int 
         * 
         * 연산 : distinct
         * 형식 : 중간 연산
         * 반환 형식 : Stream<T> 
         * 연산의 인수 :  
         * 함수 디스크립터 : 
         * 
         * 최종 연산
         * 연산 : forEach 
         * 형식 : 최종 연산
         * 목적 : 스트림의 각 요소를 소비하면서 람다를 적용한다. void 반환.
         * 
         * 연산 : count 
         * 형식 : 최종 연산
         * 목적 : 스트림의 요소 개수를 반환한다. long을 반환.
         * 
         * 연산 : collect 
         * 형식 : 최종 연산
         * 목적 : 스트림을 리듀스해서 리스트, 맵, 정수 형식의 컬렉션을 만든다.
         */

    }

}

요약

  • 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다.
  • 스트림은 내부 반복을 지원한다. 내부 반복은 filter, map, sorted 등의 연산으로 반복을 추상화한다.
  • 스트림에는 중간 연산과 최종 연산이 있다.
  • filter와 map처럼 스트림을 반환하면서 다른 연산과 연결될 수 있는 연산을 중간 연산이라고 한다. 중간 연산을 이용해서 파이프라인을 구성할 수 있지만 중간 연산으로는 어떤 결과도 생성할 수 없다.
  • forEach나 count처럼 스트림 파이프라인을 처리해서 스트림이 아닌 결과를 반환하는 연산을 최종 연산이라고 한다.
  • 스트림의 요소는 요청할 때만 계산된다.
반응형