ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Part2] Java8 In action - Chapter5 - 2
    Java8 In Action 2022. 7. 29. 13:35
    반응형

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

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

    5.3 검색과 매칭

    Entity

    Dish.java

    package Part2.Chapter5.Chapter5_5_3.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.Chapter5.Chapter5_5_3;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.Optional;
    
    import Part2.Chapter5.Chapter5_5_1.entity.Dish;
    
    /*
     * 5.3 검색과 매칭
     * 
     * 특정 속성이 데이터 집하에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.
     * 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.
     */
    public class Main_5_3 {
    
        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));
    
            /*
             * 5.3.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
             * 
             * 프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때
             * anyMatch 메서드를 이용한다.
             * 다음 코드는 menu에 채식요리가 있는지 확인하는 예제이다. 
             */
            if(menu.stream().anyMatch(Dish::isVegetarian)) {
                System.out.println("The menu is (somewhat) vegetarian friendly!!");
            }
    
            /*
             * 5.3.2 프레디케이트가 모든 요소와 일치하는지 검사
             * 
             * allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지
             * 검사한다.
             * 다음 코드는 건강식(모든 요리가 1000칼로리 이하면 건강식으로 간주)인지 확인하는 코드다.
             */
            if(menu.stream().allMatch(d -> d.getCalories() < 1000)) {
                System.out.println("모든 식단이 건강식단 입니다!");
            }
    
            /*
             * noneMatch
             * noneMatch는 allMatch와 반대 연산을 수행한다.
             * 즉, noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다. 
             */
            if(menu.stream().noneMatch(d -> d.getCalories() >= 1000)) {
                System.out.println("건강 식단이라서 먹을게 없네요.");
            }        
            /*
             * anyMatch, allMatch, noneMatch 세 가지 메서드는 쇼트서킷 기법.
             * 즉, 자바의 &&, ||와 같은 연산을 활용한다.
             */
    
            /*
             * 쇼트서킷 평가
             * 때로는 전체 스트림을 처리하지 않았더라도 결과를 반환할 수 있다.
             * 예를 들어 여러 and 연산으로 연결된 커다란 불린 표현식을 평가한다고 가정하자.
             * 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이
             * 전체 결과도 거짓이 된다.
             * 이러한 상황을 "쇼트서킷"이라고 한다.
             * 
             * allMatch, noneMatch, findFirst, findAny 등의 연산은 모든 스트림의 요소를 처리하지
             * 않고도 결과를 반환할 수 있다. 
             * 이는 원하는 요소를 찾으면 즉시 결과를 반환할 수 있기 때문이다.
             * 마찬가지로 스트림의 모든 요소를 처리할 필요 없이 주어진 크기의 스트림을 생성하는
             * limit도 쇼트서킷 연산이다.
             */
    
            /*
             * 5.3.3 요소 검색
             * 
             * findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.
             * findAny 메서드를 다른 스트림 연산과 연결해서 사용할 수 있다.
             * 다음 코드는 filter와 findAny를 이용해서 채식요리를 선택한다.
             * 
             * 스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다.
             * 즉, 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다.
             */
            Optional<Dish> dish = menu.stream()
                .filter(Dish::isVegetarian)
                .findAny();
    
            /*
             * Optional이란?
             * Optional<T>클래스(java.util.Optional)는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다.
             * 이전 예제에서 findAny 메서드는 아무 요소도 반환하지 않을 수 있다.
             * null은 쉽게 에러를 일으킬 수 있으므로 자바 8 라이브러리 설계자는 Optional<T>라는 기능을 만들었다.
             *        - isPresend()는 Optional이 값을 포함하면 참(true)을 반환하고, 값을 포함하지 않으면
             *         거짓(false)을 반환한다.
             * 
             *      - ifPresend(Consumer<T> block)은 값이 있으면 주어진 블럭을 실행한다.
             *      Consumer 함수형 인터페이스에선 T형식의 인수를 받으며 void를 반환하는 람다를 전달할 수 있다.
             *  
             *      - T get()은 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킨다.
             *  
             *      - T orElse(T other)는 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환한다.     
             */
    
            // 아래 코드 Optional<Dish>에서는 요리명이 null인지 검사할 필요가 없다.
            dish.ifPresent(d -> System.out.println("채식 요리를 선택한 사람 : " + d.getName() ));
    
            /*
             * 5.3.4 첫 번째 요소 찾기
             * 
             * 리스트 또는 정렬된 데이터로부터 생성된 스트림은 논리적인 아이템 순서가 정해져 있을 수 있다.
             * 이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야 할까?
             * 아래 코드는 숫자 리스트에서 3으로 나누어떨어지는 첫 번째 제곱값을 구하는 예이다.
             */
            System.out.println("5.3.4 첫 번째 요소 찾기 : " + Arrays.asList(1, 2, 3, 4, 5).stream()
                    .map(x -> x * x)
                    .filter(x -> x % 3 == 0)
                    .findFirst()
                    .get() );
    
            /*
             * findfirst와 findAny는 언제 사용하나?
             * 두 가지 메서드가 필요한 이유는 바로 병렬성 때문이다.
             * 병렬실행에서 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면
             * 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
             */
        }
    }

    5.4 리듀싱

    Entity

    Dish.java

    package Part2.Chapter5.Chapter5_5_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.Chapter5.Chapter5_5_4;
    
    import java.util.Arrays;
    import java.util.List;
    
    /*
     * 5.4 리듀싱
     * 
     * 스트림의 모든 요소를 반복적으로 처리하는 행위에 대한 질의를 리듀싱 연산이라고 한다.
     * (모든 스트림 요소를 처리해서 값으로 도출하는)
     * 
     * 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이(우리는 스트림)를 작은 조각이
     * 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 부른다.
     */
    public class Main_5_4 {
    
        static public void main(String[] args) {
            List<Integer> numbers = Arrays.asList(4, 5, 3, 9);
    
            /*
             * 5.4.1 요소의 합
             * 
             * reduce 메서드를 보기 전에 for-each를 이용해서 리스트의 숫자 요소를 더하는 코드를 보자.
             *         int num = 0;
             *         for(int x : numbers) {
             *             sum += x;
             *         }
             * numbers의 각 요소는 결과에 반복적으로 더해진다. 리스트에서 하나의 숫자가 남을 때까지
             * 이 과정은 반복한다. 코드에는 두 개의 파라미터가 사용되었다.
             *         - sum 변수의 초기값 0
             *         - 리스트의 모든 요소를 조합하는 연산(+) 
             */
    
            /*
             * reduce는 두 개의 인수를 갖는다.
             *         - 초기값 0
             *         - 두 요소를 조합해서 새로운 값을 만드는 BinaryOprator<T>
             * 
             * 아래 코드 (a, b) -> a + b 관련해서 reduce 메서드가 처리하는 흐름을 설명 하자면
             * (처음은 reduce 메서드의 첫 번쨰 인자로 받은 값을 더하면서 시작한다.)
             *         1. 0 + 4 = (0, 4) -> 0 + 4;    
             *         2. 4 + 5 = (4, 5) -> 4 + 5
             *         3. 9 + 3 = (9, 3) -> 9 + 3;
             *         4. 12 + 9 = (12, 9) -> 12 + 9;
             * 흐름과 같는다. 즉,  
             *         ("람다 표현식에서 반환된 값", "요소") -> "람다 표현식에 반환된 값" + "요소";
             * 같은 형식으로 인수가 전달되게 된다. 
             */        
            System.out.println("5.4.1 요소의 합 - 람다 표현식 : " + numbers.stream().reduce(0, (a, b) -> a + b));
            System.out.println("5.4.1 요소의 합 - 메서드 레퍼런스 : " + numbers.stream().reduce(0, Integer::sum));
    
            /*
             * 초기값 없음
             * 
             * 초기값이 없는 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환한다.
             * 스트림에 아무 요소도 없는 상황을 생각해보자. 이런 상황이라면 초기값이 없으므로 reduce는
             * 합계를 반환할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록 Optional 객체로 감싼 결과를 반환한다.
             */
            System.out.println("초기값 없는 reduce 메서드 : " + numbers.stream().reduce((a, b) -> a + b).get());
    
            /*
             * 5.4.2 최대값과 최소값
             *  
             * reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행한다.
             * 아래는 최대값과 최소값을 구하는 코드들이다.
             */
            // 최대값 구하기
            System.out.println("5.4.2 최대값 - 람다 표현식 : " + numbers.stream().reduce((a, b) -> a < b ? b : a).get());        
            System.out.println("5.4.2 최대값 - 메서드 레퍼런스 : " + numbers.stream().reduce(Integer::max).get());
    
            // 최소값 구하기
            System.out.println("5.4.2 최소값 - 람다 표현식 : " + numbers.stream().reduce((a, b) -> a < b ? a : b).get());        
            System.out.println("5.4.2 최소값 - 메서드 레퍼런스 : " + numbers.stream().reduce(Integer::min).get());
    
            /*
             * reduce 메서드의 장점과 병렬화
             * 
             * reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.
             * 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵다.
             * 강제적으로 동기화시킨다 하더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에
             * 상쇄되어 버리기 때문이다. 
             */
    
            /*
             * 아래 코드는 parallelStream 메서드를 이용해서 병렬처리를 수행한다. 하지만 병렬로 실행하려면 대가를 지불해야 한다.
             * 즉, reduce에 넘겨준 람다의 상태(인스턴스 변수 같은)가 바뀌지 말아야 하며, 연산이 어떤 순서로 실행되더라도
             * 결과가 바뀌지 않는 구조여야 한다.
             */
            System.out.println("reduce 메서드의 장점과 병렬화 : " + numbers.parallelStream().reduce(0, Integer::sum));
    
            /*
             * 스트림 연산 : 상태 없음과 상태 있음
             * 
             * map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
             * 따라서(사용자가 제공한 람다나 메서드 레퍼런스가 내부적인 가변 상태를 갖지 않는다는 가정 하에)
             * 이들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산(stateless operation)이다.
             * 
             * 하지만 sorted나 distinct는 filter나 map과는 다르다.
             * 스트림의 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 한다.
             * 즉, 어떤 요소를 출력 스트림으로 추가하려면 "모든 요소가 버퍼에 추가되어 있어야 한다."
             * 연산을 수행하는데 필요한 저장소 크기는 정해져있지 않기 때문에 데이터 스트림의 크기가 크거나
             * 무한이라면 문제가 생길수 있다. 따라서 이러한 연산을 내부 상태를 갖는 연산(stateful operation)으로
             * 간주할 수 있다.
             */
    
            /*
             * 중간 연산과 최종 연산
             * 
             * 연산 : filter
             * 형식 : 중간 연산
             * 반환 형식 : Stream<T>
             * 사용된 함수형 인터페이스 형식 : Predicate<T> 
             * 함수 디스크립터 : T -> boolean
             * 
             * 연산 : distinct
             * 형식 : 중간 연산(상태 있는 언바운드)
             * 반환 형식 : Stream<T>
             * 사용된 함수형 인터페이스 형식 :  
             * 함수 디스크립터 :
             * 
             * 연산 : skip
             * 형식 : 중간 연산(상태 있는 바운드)
             * 반환 형식 : Stream<T> 
             * 사용된 함수형 인터페이스 형식 : Long 
             * 함수 디스크립터 : 
             * 
             * 연산 : limit
             * 형식 : 중간 연산(상태 있는 바운드)
             * 반환 형식 : Stream<T>
             * 사용된 함수형 인터페이스 형식 : Long 
             * 함수 디스크립터 : 
             * 
             * 연산 : map
             * 형식 : 중간 연산
             * 반환 형식 : Stream<R>
             * 사용된 함수형 인터페이스 형식 : Function<T, R> 
             * 함수 디스크립터 : T -> R
             * 
             * 연산 : flatMap
             * 형식 : 중간 연산
             * 반환 형식 : Stream<R>
             * 사용된 함수형 인터페이스 형식 : Function<T, Stream<R>> 
             * 함수 디스크립터 : T -> Stream<R>
             * 
             * 연산 : sorted
             * 형식 : 중간 연산(상태 있는 언바운드)
             * 반환 형식 : Stream<T> 
             * 사용된 함수형 인터페이스 형식 : Comparator<T> 
             * 함수 디스크립터 : (T, T) -> int
             * 
             * 연산 : anyMatch
             * 형식 : 최종 연산
             * 반환 형식 : boolean
             * 사용된 함수형 인터페이스 형식 : Predicate<T> 
             * 함수 디스크립터 : T -> boolean
             * 
             * 연산 : noneMath
             * 형식 : 최종 연산
             * 반환 형식 : boolean
             * 사용된 함수형 인터페이스 형식 : Predicate<T> 
             * 함수 디스크립터 : T -> boolean
             * 
             * 연산 : allMatch
             * 형식 : 최종 연산
             * 반환 형식 : boolean
             * 사용된 함수형 인터페이스 형식 : Predicate<T> 
             * 함수 디스크립터 : T ->boolean
             * 
             * 연산 : findAny
             * 형식 : 최종 연산
             * 반환 형식 : Optional<T> 
             * 사용된 함수형 인터페이스 형식 : 
             * 함수 디스크립터 : 
             * 
             * 연산 : findFirst
             * 형식 : 최종 연산
             * 반환 형식 : Optional<T>
             * 사용된 함수형 인터페이스 형식 : 
             * 함수 디스크립터 : 
             * 
             * 연산 : forEach
             * 형식 : 최종 연산
             * 반환 형식 : void
             * 사용된 함수형 인터페이스 형식 : Consumer<T> 
             * 함수 디스크립터 : T -> void
             * 
             * 연산 : collect
             * 형식 : 최종 연산
             * 반환 형식 : R
             * 사용된 함수형 인터페이스 형식 : Collector<T, A, R> 
             * 함수 디스크립터 : 
             * 
             * 연산 : reduce
             * 형식 : 최종 연산(상태 있는 바운드)
             * 반환 형식 : Optional<T>
             * 사용된 함수형 인터페이스 형식 : BinaryOperator<T> 
             * 함수 디스크립터 : (T, T) -> T
             * 
             * 연산 : count
             * 형식 : 최종 연산
             * 반환 형식 : long
             * 사용된 함수형 인터페이스 형식 : 
             * 함수 디스크립터 : 
             */
        }
    
    }

    스트림 활용

    • 데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다.
    • 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다.
    • 내부 반복뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 이러한 일은 순차적인 반복을 단일 스레드로 구현했기 때문에 외부 반복으로는 불가능하다.

    요약

    • 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
    • filter, distinct, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
    • map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
    • findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다. allMatch, noneMatch, anyMatch 메서드를 이용해서 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색할 수 있다.
    • 이들 메서드는 쇼트서킷(short-circuit), 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
    • reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다. 예를 들어 reduce로 스트림의 최대값이나 모든 요소의 합계를 계산할 수 있다.
    • filter, map 등은 상태를 저장하지 않는 "상태 없는 연산(stateless operation)"이다 reduce 같은 연산은 값을 계산하는 데 필요한 상태를 저장한다. sorted, distinct 등의 메서드는 새로운 스트림을 반환하기 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 "상태 있는 연산(stateful operation)"이라고 부른다.
    • IntStream, DoubleStream, LongStream은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
    • 컬렉션뿐 아니라, 값, 배열, 파일, iterate와 generate 같은 메서드로도 스트림을 만들 수 있다.
    • 크기가 정해지지 않는 스트림을 무한 스트림이라고 한다.
    반응형

    댓글

Designed by Tistory.