ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Part2] Java8 In action - Chapter4 - 1
    Java8 In Action 2022. 7. 28. 09:22
    반응형

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

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

    4.1 스트림이란 무엇인가

    Entity

    Dish.java

    package Part2.Chapter4.Chapter4_4_1.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_1;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import Part2.Chapter4.Chapter4_4_1.entity.Dish;
    
    /*
     * 4.1 스트림이란 무엇인가?
     * 
     * 스트림은 자바 API에 새로 추가된 기능으로, 스트림을 이용하면 선언형(즉, 데이터를 처리하는 임시 구현 코드 대신 질의로 표현할 수 있다.)
     * 으로 컬렉션 데이터를 처리할 수 있다. 
     * 일단 스트림이 데이터 컬렉션 반복을 멋지게 처리하는 기능이라고 생각하자. 또한 스트림을 이용하면 멀티 스레드 코드를 구현하지 않아도
     * 데이터를 투명하게 병렬처리할 수 있다. 
     */
    public class Main_4_1 {
    
        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));
    
            /*
             * 스트림이 어떤 유용한 기능을 제공하는지 알기 위해
             * "저칼로리의 요리명을 반환하고, 칼로리를 기준으로 요리를 정렬"하는 코드를
             * 자바 7과 자바 8로 나눠서 구현해보겠다.
             */
    
            /*
             * 자바 7인 경우.
             */
            // 저칼로리(400 미만)를 구하는 구현코드.
            List<Dish> lowCaloriDishes = new ArrayList<>();
            for(Dish d : menu) {
                if(d.getCalories() < 400) {
                    lowCaloriDishes.add(d);
                }
            }
    
            // 추출된 저칼로리를 칼로리 기준으로 정렬하는 구현코드.
            Collections.sort(lowCaloriDishes, new Comparator<Dish>() {
                @Override
                public int compare(Dish o1, Dish o2) {
                    return Integer.compare(o1.getCalories(), o2.getCalories());
                }
            });
    
            // 정렬된 리스트를 처리하면서 요리명 선택.
            List<String> lowCaloriDishesName = new ArrayList<>();
            for(Dish d : lowCaloriDishes) {
                lowCaloriDishesName.add(d.getName());
            }
    
            /*
             * 위 코드에서는 lowCaloriDishes라는 '가비지 변수'가 사용되었다.
             * 즉, lowCaloriDishes는 컨테이너 역할만하는 중간 변수다.
             */
            lowCaloriDishesName.stream().forEach((String name) -> System.out.println("Java7 Dish Name : " + name));
    
            /*
             * 자바 8인 경우.
             */
            lowCaloriDishesName = menu 
                .stream()
                // 위 stream 메서드 대신 아래 parallelStream 메서드를 사용하게 되면 멀티코어 아키텍처에서 병렬로 실행할 수 있다.
                //.parallelStream()
                // 400 칼로리 이하의 요리 선택.
                .filter((Dish d) -> d.getCalories() < 400)
                // 칼로리로 요리 정렬.
                .sorted((d1, d2) -> Integer.compare(d1.getCalories(), d2.getCalories()))
                // 요리명 추출.
                .map(Dish::getName)
                // 모든 요리명을 리스트에 저장.
                .collect(Collectors.toList());
    
            lowCaloriDishesName.stream().forEach((String name) -> System.out.println("Java8 Dish Name : " + name));
    
            /*
             * 스트림의 새로운 기능이 소프트웨어공학적으로 다양한 이득을 준다.
             *         1. 선언형으로 코드를 구현할 수 있다.
             *         루프와 if 조건문 등의 제어 블록을 사용해서 어떻게 동작을 구현할지 지정할 필요가 없다.
             *         선언형 코드와 동작 파라미터화를 활용하면 변하는 요구사항에 쉽게 대응할 수 있다.
             *         즉, 기존 코드를 복사하여 붙여 넣는 방식을 사용하지 않는다.
             * 
             *         2. filter, sorted, map, collect 같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다.
             *         여러 연산을 파이프라인으로 연결해도 여전히 가독성과 명확성이 유지된다.
             *         filter 메서드의 결과는 sorted 메서드로, 다시 sorted의 결과는 map 메서드로, map 메서드의 결과는 collect로 연결된다.
             * 
             * filter(또는 sorted, map, collect) 같은 연산은 고수준 빌딩 블록(high-level building block)으로 이루어져 있으므로
             * 특정 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서든 사용할 수 있다.
             * (또한 이들은 내부적으로 멀티코어 아키텍처를 최대한 투명하게 활용할 수 있도록 구현되어 있다.)
             * 결과적으로 우리는 데이터 처리 과정을 병렬화하면서 스레드와 락을 걱정할 필요가 없다.
             * 
             * 자바 8의 스트림 API의 특징을 다음처럼 요약할 수 있다.
             * - 선언형
             *         더 간결하고 가독성이 좋아진다.
             * - 조립할 수 있음
             *         유연성이 좋아진다.
             * - 병렬화
             *         성능이 좋아진다.
             */        
        }
    }

    4.2 스트림 시작하기

    Entity

    Dish.java

    package Part2.Chapter4.Chapter4_4_2.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_2;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import Part2.Chapter4.Chapter4_4_1.entity.Dish;
    
    /*
     * 4.2 스트림 시작하기
     * 
     * 스트림이란 "데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소"로 정의할 수 있다.
     * 
     * 연속된 요소
     * 컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다.
     * 컬렉션은 자료구조이므로 컬렉션에서는 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이룬다.
     * 반면 스트림은 filter, sorted, map처럼 표현 계산식이 주를 이룬다.
     * 즉, 컬렉션의 주제는 데이터고 스트림의 주제는 계산이다.
     * 
     * 소스
     * 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비(consume)한다.
     * 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다.
     * 즉 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지한다.
     * 
     * 데이터 처리 연산
     * 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원한다.
     * 예를 들어 filter, map, reduce, find, match, sort 등으로 데이터를 조작할 수 있다.
     * 스트림 연산은 순차적으로 또는 병렬로 실행할 수 있다. 
     */
    public class Main_4_2 {
    
        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));
    
            /* 
             * 또한 스트림은 다음과 같은 두 가지 중요한 특징을 갖는다.     
             * 
             * 파이프라이닝
             * 대부분 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다.
             * 그 덕분에 게으름(laziness), 쇼트서킷(short-circuiting - 단선)같은 최적화도 얻을 수 있다.
             * 연산 파이프라인은 데이터 소스에 적용하는 데이터베이스 질의와 비슷하다.
             * 
             * 내부 반복
             * 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다.
             */
            System.out.println("내부 반복 : " + menu.stream()
                    .filter((d) -> d.getCalories() > 300)
                    .map(Dish::getName)
                    .limit(3)
                    .collect(Collectors.toList() ));
            /*
             * menu에 stream 메서드를 호출해서 요리 리스트로부터 스트림을 얻었다.
             *         1. 여기서 "데이터 소스"는 요리 리스트(menu 변수)다.
             *         2. 데이터 소스는 "연속된 요소"를 스트림에 제공한다.
             *         3. 스트림에 filter, map, limit, collect로 이어지는 일련의 "데이터 처리 연산"을 적용한다.
             *         4. collect를 제외한 모든 연산은 서로 "파이프라인"을 형성할 수 있도록 스트림을 반환한다.
             * 
             * 파이프라인은 소스에 적용하는 질의 같은 존재다.
             * 마지막으로 collect 연산으로 파이프라인을 처리해서 결과를 반환한다.
             * (collect는 스트림이 아니라 List를 반환한다.)
             * 마지막에 collect를 호출하기 전까지는 menu에서 아무것도 선택되지 않으며 출력 결과도 없다.
             * 즉, collect가 호출되기 전까지 메서드 호출이 저장되는 효과가 있다.
             * 
             * 아래는 일련의 스트림 연산이 적용되는 모습을 보여준다. filter, map, limit, collect는 각각 다음 작업을 수행한다.
             * (아래 순서는 위 코드 기준으로 한다.) 
             * filter
             *         람다를 인수로 받아 스트림에서 특정 요소를 제외시킨다.
             * map
             *         람다를 이용해서 요소를 다른 요소로 변환하거나 정보를 추출한다.
             * limit
             *         정해진 개수 이상의 요소가 스트림에 저장되지 못하게 스트림 크기를 축소(truncate)한다.
             * collect
             *         스트림을 다른 형식으로 변환한다.
             * 
             * 위 코드 "3개의 고칼로리 요리명을 찾아라"처럼 좀 더 선언형으로 데이터를 처리할 수 있다.
             * 스트림 라이브러리에서 필터링(filter), 추출(map), 축소(limit)기능을 제공하므로 직접 이 기능을
             * 구현할 필요가 없다.
             * 결과적으로 스트림 API는 파이프라인을 더 최적화할 수 있는 유연성을 제공한다.
             */
        }
    }

    요약

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

    댓글

Designed by Tistory.