ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Part2] Java8 In action - Chapter4 - 2
    Java8 In Action 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처럼 스트림 파이프라인을 처리해서 스트림이 아닌 결과를 반환하는 연산을 최종 연산이라고 한다.
    • 스트림의 요소는 요청할 때만 계산된다.
    반응형

    댓글

Designed by Tistory.