├── 01장(자바8,9,10,11).md ├── 02장(동작 파라미터화).md ├── 03장(람다 표현식).md ├── 04장(스트림 소개).md ├── 05장(스트림 활용).md ├── 06장(스트림으로 데이터 수집).md ├── 07장(병렬 데이터 처리와 성능).md ├── 08장(컬렉션 API 개선).md ├── 09장(리팩터링, 테스팅, 디버깅).md ├── 10장(람다를 이용한 도메인 전용 언어).md ├── 11장(null 대신 Optional 클래스).md ├── 12장(새로운 날짜와 시간 API).md ├── 13장(디폴트 메서드).md ├── 14장(자바 모듈 시스템).md ├── Collector 인터페이스.md ├── Collectors 클래스의 정적 팩토리 메서드.md ├── README.md ├── Sample.java ├── flatMap을 사용한 스트림 평준화.md ├── streams_sample ├── .idea │ └── README.md ├── README.md ├── src │ └── com │ │ └── bjh │ │ ├── Main.java │ │ └── chapter4 │ │ └── dish │ │ ├── DishVo.java │ │ ├── README.md │ │ └── Type.java └── streams_sample.iml ├── 도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유.md ├── 자바 벤치마크(JHM library)를 사용한 스트림 성능 측정.md ├── 정렬(Sorted).md ├── 중복된 키값제거.md ├── 포크조인프레임워크.md └── 함수형 인터페이스(Functional Interface).md /01장(자바8,9,10,11).md: -------------------------------------------------------------------------------- 1 | # 자바 8, 9, 10, 11 무슨 일이 일어나고 있는가? 2 | 3 | 자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다. 4 | 5 | - 자바 8에서 제공하는 새로운 기술 6 | - 스트림 API 7 | - 메서드에 코드를 전달하는 방법 8 | - 인터페이스의 디폴트 메서드 9 | 10 | 스트림을 이용하면 에러를 자주 일으키며, 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다. 자바 8에 11 | 추가된 스트림 API 덕분에 다른 두 가지 기능, 즉 `메서드에 코드를 전달하는 기법(메서드 참조와 람다)`과 인터페이스의 `디폴트 메서드`가 존재 할 수 12 | 있음을 알 수 있다. 13 | 14 | 하지만 스트림 API 때문에 메서드에 코드를 전달하는 기법이 생긴것은 아니다. 15 | 16 | 메서드에 코드를 전달하는 기법을 사용하면 `동작 파라미터화(behavior parameterization)`를 구현할 수 있다. 메서드에 코드를 전달(뿐만 아니라 결과를 17 | 반환하고 다른 자료구조로 전달할 수 도 있음)하는 자바 8 기법은 `함수형 프로그래밍(functional-style programming)`에서 위력을 발휘한다. 18 | 19 | 자바 8에서 함수형 프로그래밍을 도입하면서 객체지향 프로그래밍과, 함수형 프로그래밍의 장점을 누릴 수 있게 되었다. 20 | 21 | ## 자바 8 설계의 밑바탕을 이루는 세가지 프로그래밍 개념 22 | 23 | ### 스트림 처리(stream processing) 24 | 25 | 첫 번째 프로그래밍 개념은 스트림 처리다. 스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임. 이론적으로 프로그램은 입력 스트림에서 26 | 데이터를 한 개씩 읽어 들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 27 | 수 있다. 28 | 29 | 자바 8에는 `java.util.stream` 패키지에 스트림 API가 추가 되었다. 스트림 패키지에 정의된 Stream는 T형식으로 구성된 일련의 항목을 의미한다. 30 | 31 | > 스트림 API는 파이프라인을 만드는 데 필요한 많은 메서드를 제공한다. 스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만, 이제 자바 8에서는 32 | 우리가 하려는 작업을 데이터베이스 질의 처럼 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다. 또한 파이프라인을 이용해서 입력 33 | 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다. 스레드라는 복잡한 작업을 사용하지 않으면서도 `공짜`로 병렬성을 얻을 수 있다. 34 | 35 | ### 동작 파라미터화(behavior parameteriation)로 메서드에 코드 전달하기 36 | 37 | 자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다. 기존에 자바는 메서드를 다른 메서드의 파라미터로 전달 할 수 없었다. 38 | 물론 정렬기능을 위해서 익명함수 형태로 Comparator를 구현하는 방법도 있지만 복잡하다. 자바 8에서는 메서드를 다른 메서드의 파라미터로 전달 할 수 있다. 39 | 이러한 기능을 이론적으로 `동작 파라미터화`라고 부른다. 동작 파라미터화가 중요한 이유는 `스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 40 | 사상에 기초하기 때문이다.` 41 | 42 | ### 병렬성과 공유 가변 데이터 43 | 44 | 세 번째 프로그래밍의 개념은 `병렬성을 공짜로 얻을 수 있다`라는 말에서 시작된다. 병렬성을 공짜로 얻기 위해서는 다른 한가지를 포기해야하는데, 45 | 스트림 메서드로 전달하는 코드의 동작 방식을 조금 바꿔야 한다. 처음에는 불편하지만 나중에는 편하게 느껴질 것이다. 46 | 47 | 스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수있어야 한다. 48 | 49 | > 보통 다른 코드와 동시에 실행 하더라도 `안전하게 실행`할 수 있는 코드를 만들려면 `가변 데이터(shared mutable data)`에 접근하지 않아야 한다. 50 | 이러한 함수를 `순수(pure) 함수`, `부작용 없는 함수(side-effect-free)`, `상태 없는(stateless) 함수` 라고 부른다. 51 | 52 | ## 자바 함수 53 | 54 | ### 일급 시민과 이급 시민 55 | 56 | 프로그래밍 언어의 핵심은 값을 바꾸는 것이다. 역사적으로 그리고 전통적으로 프로그래밍 언어에서는 이 값을 `일급(first-class) 값 또는 시민(citizens)` 57 | 이라고 부른다. 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값을 구조를 표현하는데 도움이 될 수 있다. 하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다. 이렇게 전달할 수 없는 구조체는 이급 시민이다. 58 | 59 | 자바 8에서는 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다. 이미 스몰토크, 자바스크립트 같은 다양한 언어에서 일급 시민으로 가득찬 세계를 60 | 성공적으로 만들어 가고있다. 61 | 62 | ### 메서드와 람다를 일급 시민으로 63 | 64 | #### 메서드 참조(method reference) `::` 65 | 66 | - 디렉터리에서 모든 숨겨진 파일을 필터링 하는 코드 67 | 68 | ```java 69 | File[] hiddenFiles = new File(".").listFiles(new FileFilter() { 70 | public boolean accept(File file) { 71 | return file.isHidden(); // 숨겨진 파일 필터링 72 | } 73 | }); 74 | ``` 75 | 76 | 하지만 코드가 마음에 들지 않는다. 단 세 행의 코드지만 각 행이 무슨 작업을 하는지 투명하지 않다. 자바 8에서는 위 코드를 아래처럼 구현할 수 있다. 77 | 78 | ```java 79 | File[] hiddenFiles = new File(".").listFiles(File::isHidden); 80 | ``` 81 | 82 | 자바 메서드 참조 `::` (이 메서드를 값으로 사용하라는 의미)를 이용해서 listFiles에 직접 전달할 수 있다. 83 | 84 | ### 람다 : 익명함수 85 | 86 | 자바 8에서는 메서드를 일급 값으로 취급할 뿐 아니라 람다(또는 익명함수 anonymous functions)를 포함하여 함수도 값으로 취급할 수 있다. 87 | 88 | ## 코드 넘겨주기 : 예제 89 | 90 | Apple 클래스와 getColor 메서드가 있고 Apples 리스트를 포함하는 변수 inventory가 있다고 가정하자. 이때 모든 녹색 사과를 선택해서 리스트를 91 | 반환하는 프로그램을 구현해보자. 92 | 93 | - 자바 8 이전 방식 94 | 95 | ```java 96 | public static List filterGreenApples(List inventory) { 97 | List result = new ArrayList<>(); 98 | for(Apple apple : inventory) { 99 | if(GREEN.equals(apple.getColor())) { 100 | result.add(apple); 101 | } 102 | } 103 | return result; 104 | } 105 | ``` 106 | 107 | 하지만 누군가는 사과를 무게 150그람 이상으로 필터링 하고 싶을 수 있다. 그러면 우리는 다음처럼 코드를 구현할 수 있을 것이다. 108 | 109 | ```java 110 | public static List filterHeavyApples(List inventory) { 111 | List result = new ArrayList<>(); 112 | for(Apple apple : inventory) { 113 | if(apple.getWeight() > 150) { 114 | result.add(apple); 115 | } 116 | } 117 | return result; 118 | } 119 | ``` 120 | 121 | 소프트웨어공학적인면에서 복붙의 단점은 어떤 코드에 버그가 있다면 복붙한 모든 코드를 고쳐야 한다. 122 | 123 | 자바 8을 기준으로는 아래처럼 고칠 수 있다. 124 | 125 | ```java 126 | public static boolean isGreenApple(Apple apple) { 127 | return GREEN.equals(apple.getColor()); 128 | } 129 | 130 | public static boolean isHeavyApple(Apple apple) { 131 | return apple.getWeight() > 150; 132 | } 133 | 134 | // 명확히 하기위해 적어놓음 135 | // 보통은 java.util.function에서 임포트함 136 | public interface Predicate { 137 | boolean test(T t); 138 | } 139 | 140 | // 메서드가 p라는 이름의 프레디케이트 파라미터로 전달됨 141 | static List filterApples(List inventory, Predicate p { 142 | List result = new ArrayList<>(); 143 | for(Apple apple : inventory) { 144 | if(p.test(apple)) { 145 | result.add(apple); 146 | } 147 | } 148 | return result; 149 | } 150 | 151 | // 아래처럼 메서드를 호출할 수 있다. 152 | filterApples(inventory, Apple:isGreenApple); 153 | filterApples(inventory, Apple:isHeavyApple); 154 | ``` 155 | 156 | ### 프레디케이트(Predicate) 란? 157 | 158 | `수학에서는 인수로 값을 받아 true나 false를 반환하는 함수를 프레디케이트라고 한다.` 나중에 설명하겠지만 자바 8에서도 Function 같이 코드를 구현할 수 있만 Predicat을 사용하는 것이 더 표준적인 방식이다.(또한 boolean을 Boolean으로 변환하는 과정이 없으므로 더 효율적이다.) 159 | 160 | ### 메서드 전달에서 람다로 161 | 162 | 메서드를 값으로 전달하는 것은 분명 유용한 기능이다. 하지만 isHeavyApple, isGreenApple 처럼 한 두 번만 사용할 메서드를 매번 정의하는 것은 163 | 귀찮은 일이다. 자바 8에서는 이 문제도 간단히 해겨랄 수 있다. 바로 람다를 이용하면 된다. 164 | 165 | filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor())); 166 | 167 | filterApples(inventory, (Apple a) -> a.getWieght() > 150); 168 | 169 | `즉, 한 번만 사용할 메서드는 따로 정의를 구현할 필요가 없다. 하지만 람다가 몇 줄 이상으로 길어진다면 익명 람다 보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직 하다. 코드의 명확성이 우선시 되어야 한다.` 170 | 171 | ## 스트림 172 | 173 | 거의 모든 자바 애플리켕션은 컬렉션을 만들고 활용한다. 하지만 컬렉션으로 모든 문제가 해결되는 것은 아니다. 174 | 175 | 예를 들어 고가의 트랜잭션(transaction)(거래) 만 필터링한 다음에 통화로 결과를 그룹화 해야 한다고 가정하자. 아래와 같은 많은 기본 코드를 구현해야한다. 176 | 177 | ```java 178 | Map> transactionByCurrencies = new HashMap<>(); // 그룹화된 트랜잭션을 더할 Map 생성 179 | for(Transaction transaction : transactions) { 180 | if(transaction.getPrice() > 1000) { 181 | Curreny curreny = transacation.getCurrency(); // 트랜잭션의 통화를 추출 182 | List transactionsForCurrency = transactionsByCurrencies.get(currency); 183 | if(transactionsForCurrency == null) { 184 | transactionsForCurrency = new ArrayList<>(); 185 | transactionsByCurrencies.put(currenc, transcationsForCurrency); 186 | } 187 | transactionsForCurrency.add(transacation); 188 | } 189 | } 190 | ``` 191 | 192 | 위의 예제는 중첩된 제어 흐름 문장이 많아서 코드를 한 번에 이해하기 어렵다. 193 | 194 | 스트림 API를 이용하면 다음처럼 문제를 해결할 수 있다. 195 | 196 | ```java 197 | import static java.util.stream.Collectors.groupingBy; 198 | Map> transactionsByCurrencies = transactions.stream() 199 | .filter((Transcations t) -> t.getPrice() > 1000); // 고가의 트랜잭션 필터링 200 | .collect(groupingBy(Transcation::geturrency)); // 통화로 그룹화 201 | ``` 202 | 203 | ## 내부 반복과 외부 반복 204 | 205 | 외부 반복(external iteration)은 for-each루프를 이용해서 각 요소를 반 작업을 수행하는 것들을 말한다. 반면 내부 반복(internal iteration)은 스트림 API와 같이 루프를 신경 쓸 필요 없이, 스트림 API라는 라이브러리 내부에서 모든 데이터가 처리되는 것을 말한다. 206 | 207 | ## 멀티 스레딩은 어렵다 208 | 209 | 자바 8은 스트림(API, java.util.stream)로 컬렉션을 처리하면서 발생하는 `모호함과 반복적인 코드 문제`와 `멀티코어 활용 어려움`이라는 두 가지 문제를 모두 해결했다. 컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다. 210 | 211 | ## 포킹단계(forking step) 212 | 213 | 예를들어 두 CPU를 가진 환경에서 리스트를 필터링할 때 한 CPU는 앞 부분을 처리하고, 다른 CPU는 리스트의 뒷 부분을 처리하도록 요청할 수 있는데 이 과정을 포킹 단계라고 한다. 각각의 cpu는 자신이 맡은 절반의 리스트를 처리하고, 마지막으로 하나의 cpu가 두 결과를 정리한다. 214 | 215 | ## 자바 8에서 제공하는 두 가지 요술방망이 216 | 217 | 흔히 사람들은 자바의 병렬성은 어렵고 synchronized는 쉽게 에러를 일으킨다고 생각한다. 자바8은 어떤 요술방망이를 제공할까? 218 | 219 | 자바 8은 두 가지 요술 방망이를 제공한다. 우선 라이브러리에서 분할을 처리한다. 즉, 큰 스트림을 병렬로 처리할 수 있도록 작은 스트림으로 분할한다. 220 | 또한 filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용을 하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있다. 221 | 상호작용을 하지 않는다는 제약은 프로그래머 입장에서 상당히 자연스러운 일이다. 함수형 프로그래밍에서 함수란 `함수를 일급값으로 사용한다`라는 의미도 있지만, 부가적으로 `프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다`라는 의미도 포함한다. 222 | 223 | ## 디폴트 메서드와 자바 모듈 224 | 225 | 자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다. 226 | 또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다. 227 | 228 | `디폴트 메서드는 특정 프로그램을 구현하는 데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능`이다. 229 | 230 | 어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까라는 딜레마를 디폴트 메서드가 해소시켜준다. 231 | 232 | 기존에는 인터페이스에 메서드가 하나 추가되면 인터페이스를 사용하는 모든 곳에서 메서드를 추가해야하지만, 디폴트 메서드는 `구현하지 않아도 되는 메서드`이다. 메서드 본문(bodies)은 클래스 구현이 아니라 인터페이스 일부로 포함된다.(그래서 이를 디폴트 메서드라고 한다.) 233 | 234 | ## 함수형 프로그래밍에서 가져온 다른 유용한 아이디어 235 | 236 | 1965년에 널 참조를 발명했던 일을 회상하며 그 결정은 정말 뼈아픈 실수였다고 반성하고 있다.. 단지 구현이 편리하단 이유로 널 참조를 만들어야 겠다는 237 | 유혹을 뿌리치지 못했다. -> 토니 호아레(Tony Hoare)는 2009년 QCon London의 프레젠테이션에서 위 와같은 말을 했다. 238 | 239 | 자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 `Optional` 클래스를 제공한다. Optional는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체이다. 240 | -------------------------------------------------------------------------------- /02장(동작 파라미터화).md: -------------------------------------------------------------------------------- 1 | # 동작 파라미터화 코드 전달하기 2 | 3 | 소비자의 요구사항은 항상 바뀌기 마련이다. 이런 변화하는 요구사항에 대해 효과적으로 대응하기 위해서 `동작 파라미터화(behavior parameterization)` 4 | 를 이용하면다. 5 | 6 | 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다. 7 | 8 | ## EXAMPLE 9 | 10 | > 기존의 농장 재고목록 애플리케이션에 리스트에서 녹색(green) 사과만 필터링하는 기능을 추가한다고 가정하자. 11 | 12 | ### 첫 번째 시도 : 녹색사과 필터링 13 | 14 | 사과 색을 정의하는 ENUM 클래스 15 | 16 | ```java 17 | enum Color { RED, GREEN } 18 | ``` 19 | 20 | 첫 번째 시도 21 | 22 | ```java 23 | public static List filterGreenApples(List result = new ArrayList<>(); 25 | for(Apple apple : inventory) { 26 | if(GREEN.equals(apple.getColor)) { 27 | result.add(apple); 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | 만약 농부가 빨간 사과도 필터링 하고 싶다고하면 어떻게 할까? 메서드를 하나 더만들 수 도 있지만 만약 또 농부가 노란색, 어두운 빨간색등 요구사항이 34 | 점차 늘어난다면 메서드가 많이 생길 것이다. 이런경우 다음과 같은 좋은 규칙이 있다. 35 | 36 | > 거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화 한다. 37 | 38 | ### 두 번째 시도 : 색을 파라미터화 39 | 40 | ```java 41 | public static List filterApplesByColor(List inventory, Color color) { 42 | List result = new ArrayList<>(); 43 | for(Apple apple : inventory) { 44 | if(apple.getColor().equals(color)) { 45 | result.add(apple); 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | 이제 농부가 어떤 색을 원하든 해당 메서드만 호출하면, 색으로 필터링 할 수 있다. 하지만 농부가 색 이외에도 무거운 사과와 가벼운 사과, 무게로 52 | 사과를 필터링하고 싶다고하면 어떻게 해야 할까? 53 | 54 | `색을 파라미터화` 한 것처럼 `무게를 파라미터화` 하면 된다. 55 | 56 | ```java 57 | public static List filterApplesByWeight(List inventory, int weight) { 58 | List result = new ArrayList<>(); 59 | for(Apple apple : inventory) { 60 | if(apple.getWeight() > weight) { 61 | result.add(apple); 62 | } 63 | } 64 | return result; 65 | } 66 | ``` 67 | 68 | 위 코드도 좋은 해결책이지만, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다. 69 | 70 | 이는 소프트웨어 공학의 `DRY(Don't Repeat yourself)` 같은 것을 반복하지말 것 원칙을 어기는 것이다. 이를 해결하기 위해서 색과 무게를 filter라는 71 | 메서드로 합치는 방법도 있다. 따라서 색이나 무게 중 어던 것을 기준으로 필터링할 지 가리키는 플래그를 추가할 수 있다. 72 | `(하지만 실전에서는 절대 이 방법을 사용하지 말아야 한다.)` 73 | 74 | ### 절대 사용하지 말아야 하는 방법 75 | 76 | - `절대 사용하지 말아야 하는 방법` 77 | 78 | ```java 79 | public static List filterApples(List inventory, Color color, int weight, boolean flag) { 80 | List result = new ArrayList<>(); 81 | for(Apple apple : inventory) { 82 | if((flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)) { 83 | result.add(apple); 84 | } 85 | } 86 | return result; 87 | } 88 | ``` 89 | 90 | 위 메서드를 아래처럼 호출 할 수 있다. 91 | 92 | ```java 93 | List greenApples = filterApples(inventory, GREEN, 0, true); 94 | List heavyApples = filterApples(inventory, null, 150, false); 95 | ``` 96 | 97 | 하지만 정말 마음에 들지 않는 코드이다. 대체 true와 false는 무엇을 의미하는 것일까?... 게대가 앞으로 요구사항이 바뀌었을때 유연하게 대처할 수 없다. 98 | 에를 들어 사과의 크기, 모양, 출하지 등으로 사과를 필터링 하고싶어지는 경우 어떻게 해야할까? 99 | 100 | 아래에서 `동작 파라미터화`를 이용해서 유연성을 얻는 방법에 대해 설명한다. 101 | 102 | ### 동작 파라미터화 103 | 104 | 사과의 어떤 속성에 기초해서 불리언 값을 반환(예를 들어 사과가 녹색인가? 150그램 이상인가?). 참 또는 거짓을 반환하는 함수를 `프레디케이트` 라고 한다. 105 | `선택 조건을 결정하는 인터페이스`를 정하자. 106 | 107 | ```java 108 | public interface ApplePredicate { 109 | boolean test (Apple apple); 110 | } 111 | ``` 112 | 113 | 다음 예제 처럼 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다. 114 | 115 | ```java 116 | public class AppleHeavyWeightPredicate implements ApplePredicate { 117 | public boolean test(Apple apple) { 118 | return apple.getWeight() > 150; 119 | } 120 | } 121 | 122 | public class AppleGreenColorPredicate implements ApplePredicate { 123 | public boolean test(Apple apple) { 124 | return GREEN.equals(apple.getColor()); 125 | } 126 | } 127 | ``` 128 | 129 | 위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 `전략 디자인패턴(strategy design patter)`이라고 부른다. 130 | 131 | 전략 디자인 패턴은 각 알고리즘(전략이라 부르는)을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다. 132 | 133 | 여기서는 ApplePredicate가 알고리즘 패밀리이며, 이를 구현한 클래스들이 전략이다. 134 | 135 | ### 네 번째 시도 : 추상적 조건으로 필터링 136 | 137 | ```java 138 | public static List filterApples(List inventory, ApplePredicate p) { 139 | List result = new ArrayList<>(); 140 | for(Apple apple : inventory) { 141 | if(p.test(apple)) { 142 | result.add(apple); 143 | } 144 | } 145 | return result; 146 | } 147 | ``` 148 | 149 | 첫 번째 코드에 비해 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌을 뿐 아니라 사용하기도 쉬워 졌다. 150 | 151 | 예를 들어 농부가 150그램이 넘는 빨간 사과를 검색해달라고 부탁하면 우리는 ApplePredicate를 적절하게 구현하는 클래스만 만들면 된다. 152 | 153 | ```java 154 | public class AppleRedAndHeavyPredicate implements ApplePredicate { 155 | public boolean test(Apple apple) { 156 | return RED.equals(apple.getColor()) && apple.getWeight() > 150; 157 | } 158 | } 159 | ``` 160 | 161 | 우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다. 즉, 우리는 filterApples 메서드의 동작을 파라미터화 한 것이다. 162 | 163 | > 즉, 우리는 전략 디자인 패턴(Strategy Design Pattern)과 동작 파라미터화를 통해서 필터 메서드에 전략(Strategy)을 전달 함으로써 더 유연한 코드를 164 | 만들었다. 165 | 166 | ### 한 개의 파라미터, 다양한 동작 167 | 168 | 지금까지 살펴본 것첢 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 169 | 170 | ## QUIZ 171 | 172 | 사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성(커스터마이즈된 다양한 toString) 메서드와 같이) 할 수 있도록 파라미터화된 prettyPrintApple 메서드를 구현하시오. 예를 들어 prettyPrintApple 메서드가 각각의 사과 무게를 출력하도록 지시할 수 있다. 혹은 각각의 사과가 무거운지, 가벼운지 173 | 출력하도록 지시할 수 있다. 174 | 175 | - interface 만들기 176 | 177 | 참 또는 거짓을 반환하는 함수를 `프레디케이트` 라고 했습니다. 위 문제에서 String 타입을 반환하라고 했으니 interface 네이밍을 xxxPredicate보다는 178 | Formatter라든지 이 외의 다른 좋은 네이밍으로 정하는게 좋습니다. 179 | 180 | ```java 181 | public interface AppleFormatter { 182 | String accept(Apple a); 183 | } 184 | 185 | public class AppleFancyFormatter implements AppleFormatter implements AppleFormatter { 186 | public String accept(Apple apple) { 187 | String characteristic = apple.getWeight() > 150 ? "heavy" : "light"; 188 | return "A " + characteristic + " " + apple.getColor() + " apple"; 189 | } 190 | } 191 | 192 | public class AppleSimpleFormatter implemetns AppleFormatter { 193 | public String accept(Apple apple) { 194 | return "An apple of " + apple.getWeight() + "g"; 195 | } 196 | } 197 | 198 | public static void prettyPrintApple(List inventory, AppleFormatter foramtter) { 199 | for(Apple apple : inventory) { 200 | String output = formatter.accept(apple); 201 | System.out.println(output); 202 | } 203 | } 204 | ``` 205 | 206 | ## 복잡한 과정 간소화 207 | 208 | 위에서 보여준 예시는(Predicate 인터페이스를 선언하고, 이를 구현하여 인스턴스화 하여 사용하는 것) 상당히 번거로운 작업이며, 시간 낭비이다. 209 | 210 | 이를 개선 하기 위해서 자바는 `클래스의 선언과 인스턴스화`를 동시에 수행할 수 있도록 `익명 클래스`라는 기법을 제공한다. 211 | 212 | `익명 클래스`는 자바의 지역 클래스와 비슷한 개념이다. 213 | 214 | ## 다섯 번째 시도 : 익명 클래스 사용 215 | 216 | ```java 217 | List redApples = filterApples(inventory, new ApplePredicate() { 218 | public boolean test(Apple apple) { 219 | return RED.equals(apple.getColor()); 220 | } 221 | } 222 | ``` 223 | 224 | 익명 클래스의 단점은 아직도 코드의 줄이 길다는 것이며, 많은 개발자들이 익명 클래스 사용에 익숙하지 않는다는 점 이다. 225 | 226 | ## 여섯 번째 시도 : 람다 표현식 사용 227 | 228 | ```java 229 | List result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor())); 230 | ``` 231 | 232 | ## 일곱 번째 시도 : 리스트 형식으로 추상화 233 | 234 | ```java 235 | public interface Predicate { 236 | boolean test(T t); 237 | } 238 | 239 | public static List filter(List list, Predicate p) { 240 | List result = new ArrayList<>(); 241 | for(T e : list) { 242 | if(p.test(e)) { 243 | result.add(e); 244 | } 245 | } 246 | return result; 247 | } 248 | ``` 249 | 250 | ```java 251 | List redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor())); 252 | List evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0); 253 | ``` 254 | 255 | ## Comparator로 정렬하기 256 | 257 | java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화 할 수 있다. 258 | 259 | ```java 260 | public interface Comparator { 261 | int compare(T o1, T o2); 262 | } 263 | ``` 264 | 265 | 무게가 적은 순서로 목록에서 사과를 정렬 266 | 267 | ```java 268 | inventory.sort(new Comparator() { 269 | public int comapre(Apple a1, Apple a2) { 270 | return a1.getWeight().compareTo(a2.getWeight()); 271 | } 272 | } 273 | ``` 274 | 275 | ```java 276 | inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); 277 | ``` 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /03장(람다 표현식).md: -------------------------------------------------------------------------------- 1 | # 람다 표현식 2 | 3 | ## 람다란? 4 | 5 | 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화 한 것. 6 | 7 | 람다 표현식은 `파라미터 + 화살표 + 바디`로 이루어진다. 8 | 9 | ```java 10 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 11 | ``` 12 | 13 | ## 자바8에서 지원하는 다섯 가지 람다 표현식 예제 14 | 15 | ```java 16 | (String s) -> s.length() // String 형식의 파라미터를 받으며, int 반환 람다 표현식에는 return 문이 함축되어있어서 명시하지 않아도 된다. 17 | (Apple a) -> a.getWeight() > 150 // Apple 형식의 파라미터를 받으며, boolean 반환 18 | (int x, int y) -> { 19 | System.out.println("Result : "); 20 | System.out.println(x+y); 21 | } 22 | // int 형식의 파라미터 두 개를 가지며 리턴 값이 없다. 23 | 24 | () -> 42 // 파라미터가 없으며 42를 반환한다. 25 | 26 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) 27 | ``` 28 | 29 | - 람다의 기본 문법 30 | 31 | `(parameteres) -> expression` 32 | 33 | `(parameteres) -> { statements; }` 34 | 35 | 36 | 람다에서 표현식이 하나인데 return문을 사용하는 경우 블록으로 감싸야 한다. 37 | 38 | ```java 39 | // x 40 | (String s) -> return "Alan" + i; 41 | // o 42 | (String s) -> {return "Alan" + i;} 43 | ``` 44 | 45 | ## 함수형 인터페이스 46 | 47 | 함수형 인터페이스는 `오직 하나의 추상메서드만 지정하는 인터페이스` 이다. 예를들어 `Predicate`, Comparator, Runnable 등이 있다. 48 | 49 | ```java 50 | public interface Predicate { 51 | boolean Test (T t); 52 | } 53 | 54 | public interface Comparator { 55 | int compare(T o1, T o2); 56 | } 57 | 58 | public interface Runnable { 59 | void run(); 60 | } 61 | 62 | public interface ActionListener extends EventListener { 63 | void actionPerformed(ActionEvent e); 64 | } 65 | 66 | public interface Callable { 67 | V call() throws Exception; 68 | } 69 | 70 | public interface PrivilegedAction { 71 | T run(); 72 | } 73 | ``` 74 | 75 | 람다에서 함수형 인터페이스로 뭘 할 수 있을 까? 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 `전체 표현식을 76 | 함수형 인터페이스의 인스턴스로 취급(기술적으로 따지면 함수형 인터페이스를 `구현한` 클래스의 인스턴스)` 할 수 있다. 77 | 78 | ## 함수 디스크립터(function descriptor) 79 | 80 | 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 `함수 디스크립터`라고 부른다. 81 | 예를들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로 void 반환 `() -> void`로 표기할 수 있다. 82 | 83 | 람다 표현식은 `함수형 인터페이스`를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있다. 84 | 85 | 그리고 void 반환이 한 개 인 경우에는 중괄호로 감쌀 필요가 없다. 86 | 87 | ## @FunctionallInterface 88 | 89 | @FunctionallInterface는 함수형 인터페이스에 붙는 어노테이션이다. (즉, 함수형 인터페이스를 가리키는 어노테이션) 90 | 91 | @FunctionallInterface로 선언했지만 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다. 예를 들어 추상 메서드가 한 개 이상이라면 92 | 93 | `Multiple nonoverriding abstract methods found in interface Foo` 같은 에러가 발생할 수 있다. 94 | 95 | ## 람다 활용 : 실행 어라운드 패턴 96 | 97 | 자원 처리(예를 들면 데이터베이스의 파일 처리)에 사용하는 `순환 패턴(recurrent pattern)`은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어 진다. 설정(setup)과 정리(cleanup) 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다. 98 | 99 | - `실행 어라운드 패턴(execute around pattern)` 100 | 101 | ```java 102 | public String processFile() throws IOException { 103 | try ( BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { 104 | return br.readLine(); 105 | } 106 | } 107 | ``` 108 | 109 | try-with-resources 구문을 사용하여 자원을 닫지 않아도 된다. 110 | 111 | ### 1단계 : 동작(메서드) 파라미터화를 기억하라 112 | 113 | 현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다. 만약 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까? 114 | 115 | 바로 processFile()을 동작 파라미터화 시키는 것이다. 116 | 117 | - 한 번에 두 줄 출력 118 | 119 | ```java 120 | String result = processFile((BufferedReader br) -> br.readLine() + br.readLine()); 121 | ``` 122 | 123 | ### 2단계 : 함수형 인터페이스를 이용해서 동작 전달 124 | 125 | 함수형 인터페이스 자리에 람다를 사용할 수 있다. 따라서 BufferedReader -> String 과 IOException을 던질(throw) 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다. 이 인터페이스를 BufferedReaderProcessor라고 정의하자. 126 | 127 | ```java 128 | @FunctionalInterface 129 | public interface BufferedReaderProcessor { 130 | String process(BufferedReader b) throws IOException; 131 | } 132 | ``` 133 | 134 | ```java 135 | public String processFile(BufferedReaderProcessor p) throws IOException { 136 | // ... 137 | } 138 | ``` 139 | 140 | ### 3단계 : 동작 실행 141 | 142 | 이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader -> String)와 일치하는 람다를 전달할 수 있다. 143 | 144 | 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으며, 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리한다. 145 | 146 | ```java 147 | public String processFile(BufferedReaderProcessor p) throws IOException { 148 | try(BufferedREader br = new BufferedReader(new FileReader("data.txt"))) { 149 | return p.process(br); 150 | } 151 | } 152 | ``` 153 | 154 | ### 4단계 : 람다 전달 155 | 156 | 이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다. 157 | 158 | ```java 159 | String oneLine = processFile((BufferedReaer br) -> br.readLine()); 160 | ``` 161 | 162 | ## 함수형 인터페이스 사용 163 | 164 | 자바8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다. 165 | 166 | ### Predicate 167 | 168 | java.util.function.Predicate 인터페이스는 test 라는 추상 메서드를 정의히ㅏ며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언으로 반환한다. 169 | 따로 정의할 필요없이 바로 사용할 수 있다. 170 | 171 | 아래 예제처럼 String 객체를 인수로 받는 람다를 정의할 수 있다. 172 | 173 | ```java 174 | @FunctionalInterface 175 | public interface Predicat { 176 | boolean test(T t); 177 | } 178 | public List filter(List list, Predicate p) { 179 | List results = new ArrayList<>(); 180 | for(T t : list) { 181 | if(p.test(t)) { 182 | results.add(t); 183 | } 184 | } 185 | return results; 186 | } 187 | Predicat nonEmptyStrinPredicate = (String s) -> !s.isEmpty(); 188 | List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate); 189 | ``` 190 | 191 | Predicate 인터페이스의 자바독 명세(Javadoc specification)를 보면 and나 or 같은 메서드도 있음을 알 수 있다. 192 | 193 | ### Consumer 194 | 195 | java.util.function.Consumer 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반화하는 accept라는 추상메서드를 정의한다. 196 | 197 | `T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.` 예를 들어 Integer 리스트를 인수로 받아서 198 | 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있다. 199 | 200 | ```java 201 | @FunctionalInterface 202 | public interface Consumer { 203 | void accept(T t); 204 | } 205 | 206 | public void forEach(List list, Consumer c) { 207 | for(T t : list) { 208 | c.accept(t); 209 | } 210 | } 211 | 212 | forEach( 213 | Arrays.asList(1,2,3,4,5), 214 | (Integer i) -> System.out.println(i) 215 | ); 216 | ``` 217 | 218 | ### Function 219 | 220 | java.util.function.Function 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반화하는 추상 메서드 apply를 정의한다. 221 | 222 | 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.(예를 들면 사과의 무게 정보를 추출하거나 문자열을 길이와 매핑). 223 | 224 | 다음은 String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 변환하는 map 메서드를 정의하는 예제이다. 225 | 226 | ```java 227 | @FunctionalInterface 228 | public interface Function { 229 | R apply(T t); 230 | } 231 | 232 | public List map(List list, Function f) { 233 | List result = new ArrayList<>(); 234 | for(T t: list) { 235 | result.add(f.apply(t)); 236 | } 237 | return result; 238 | } 239 | 240 | // [7,2,6] 241 | List l = map(Arrays.asList("lambdas", "in", "action"), (Strin s) -> s.length()); // Function의 apply 메서드를 구현하는 람다 242 | ``` 243 | 244 | #### 구현방식 245 | 246 | 1. 함수형 인터페이스를 선언한다.(사실 java.util.Function.* 에 가보면 Predicate, Consumer, Function 등 함수형 인터페이스가 있다.) 247 | 2. 이 함수형 인터페이스를 동작(전략) 파라미터화 시켜서(즉, 함수형 인터페이스를 매개변수로 받는) 자신이 원하는 로직을 처리하는 메서드를 만든다. 248 | 3. 해당 메서드를 호출할때 파라미터로 람다를 넘길 수 있다.(여기서 람다는, 함수형 인터페이스의 메서드를 구현하는 역할) 249 | 250 | ## 기본형 특화 251 | 252 | 자바의 모든 형식은 참조형(reference type) (Byte, Integer, Object, List) 아니면 기본형(primitive type)에 해당한다. 자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 `박싱(boxing)`이라고한다. 반대로 참조형을 기본형으로 변환하는 동작을 `언박싱(unboxing)`이라고 한다. 253 | 그리고 이러한 동작이 자동으로 이루어지는 `오토박싱(autoboxing)`이라는 기능도 제공한다. 254 | 255 | - autoboxing 256 | 257 | ```java 258 | List list = new ArrayList<>(); 259 | for(int k=0; k<500; k++) { 260 | list.add(k); // int > Integer 261 | } 262 | ``` 263 | 264 | 하지만 이런 변환 과정은 비용이 소모된다. 박싱한 값은 기본형을 감싸는 래퍼이며 힙에 저장된다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 265 | 가져올 때도 메모리를 탐색하는 과정이 필요하다. 266 | 267 | ## 형식 검사, 형식 추론, 제약 268 | 269 | 람다 표현식을 처음 설명할 때 람다로 함수형 인터페이스의 인스턴스를 마들 수 있다고 언급했다. 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 270 | 구현하는지의 정보가 포함되어 있지 않다. 따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다. 271 | 272 | ### 형식 검사 273 | 274 | 람다가 사용되는 `콘텍스트(context)`를 이용해서 람다의 `형식(type)`을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 `대상 형식(target type)`이라고 한다. 275 | 276 | 277 | ### 형식 추론 278 | 279 | 자바 컴파일러는 람다 표현식이 사용된 콘테긋트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 280 | 281 | ``` 282 | 대상형식 = () -> ...; 283 | ``` 284 | 285 | - Comparator 객체를 만드는 코드 286 | 287 | ```java 288 | // 형식 추론 하지 않음 289 | Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 290 | 291 | // 형식 추론 함 292 | Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); 293 | ``` 294 | 295 | ## 자유변수(free variable) 와 람다 캡처링(capturing lambda) 296 | 297 | 지금까지 살펴본 모든 람다 표현식은 인수를 자신의 바디 안에서만 사용했다. 하지만 람다 표현식에서는 익명 함수가 하는 것처럼 `자유 변수`(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다. 이와 같은 동작을 `람다 캡처링`이라고 한다. 298 | 299 | 다음은 portNumber 변수를 캡처하는 람다 예제이다. 300 | 301 | ```java 302 | int portNumber = 1337; 303 | Runnable r = () -> System.out.println(portNumber); 304 | ``` 305 | 306 | 하지만 자유 변수에도 제약이 있다. 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록) 할 수 있다. 307 | 308 | 하지만 그러러면 `지역 변수는 명시적으로 final로 선언되어 있어야 하거나, 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다.` 309 | 310 | 아래는 portNumber에 값을 두 번 할당하므로 컴파일 할 수 없는 코드이다. 311 | 312 | ```java 313 | int portNumber = 1337; 314 | Runnable r = () -> System.out.println(portNumber); 315 | portNumber = 31337; 316 | ``` 317 | 318 | ## 클로저(Clojure) 319 | 320 | 원칙적으로 클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다. 예를 들어 클로저를 다른 함수의 인수로 전달 할 수 있다. 321 | 클로저는 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있다. 자바 8의 람다와 익명 클래스는 클로저와 비슷한 도작을 수행한다. 322 | 람다와 익명 클래스 모두 메서드의 인수로 전달 될 수 있으며, 자신의 외부 영역의 변수에 접근할 수 있다. 다만 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없다. 람다가 정의된 메서드의 지역 변숫값은 final 변수여야 한다. 덕분에 람다는 벼누가 아닌 값에 국한되어 어떤 동작을 수행한다는 사실이 명확해진다. 이전에도 설명한 것 처럼 지역 변숫값은 스택에 존재하므로 자신을 정의한 스레드와 생존을 같이 해야 하며 따라서 지역 변수는 323 | final 이어야 한다. 가변 지역 변수를 새로운 스레드에서 캡처할 수 있다면, 안전하지 않은 동작을 수행할 가능성이 생긴다.(인스턴스 변수는 스레드가 공유하는 힙에 존재하므로 특별한 제약이 없다.) 324 | 325 | ## 메서드 참조 326 | 327 | 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 때로는 람다 표현식 보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다. 328 | 329 | 다음은 기존 코드다. 330 | 331 | ```java 332 | inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); 333 | ``` 334 | 335 | 다음은 메서드 참조와 java.util.Comparaptor.comparing을 활용한 코드다. 336 | 337 | ```java 338 | inventory.sort(comparing(Apple::getWeight)); 339 | ``` 340 | 341 | 메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다. 메서드 명을 참조하기 때문에 `가독성을 높일 수 있다.` 메서드를 참조하는 342 | 방법은 메서드명 앞에 구분자(::)를 붙이면 된다. 343 | 344 | - Example 345 | 346 | ```java 347 | // (Apple apple) -> apple.getWeight() 348 | Apple::getWeight 349 | 350 | // () -> Thread.currentThread().dumpStack() 351 | Thread.currentThread()::dumpStack() 352 | 353 | // (str, i) -> str.substring(i) 354 | String::substring 355 | 356 | // (String s) -> System.out.println(s) 357 | System.out::println 358 | 359 | // (String s) -> this.isValidName(s) 360 | this::isValidName 361 | ``` 362 | 363 | ### 메서드 참조의 세 가지 유형 364 | 365 | 메서드 참조는 세 가지 유형으로 구분할 수 있다. 366 | 367 | - 정적 메서드 참조 368 | - 예를 들어 Integer의 parseInt 메서드는 `Integer::parseInt`로 표현할 수 있다. 369 | - 다양한 형식의 인스턴스 메서드 참조 370 | - 예를 들어 Strin의 length 메서드는 `String::length`로 표현할 수 있다. 371 | - 기존 객체의 인스턴스 메서드 참조 372 | - 예를 들어 Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transacation 객체에는 getValue 메서드가 있다면 373 | - 이를, `expensiveTransaction::getValue`로 표현할 수 있다. 374 | 375 | - `세 가지 종류의 람다 표현식을 메서드 참조로 바꾸는 방법` 376 | - (args) -> ClassName.staticMethod(args) 377 | - ClassName::staticMethod 378 | - (arg0, rest) -> arg0.instanceMethod(rest) ... arg0은 className 형식 379 | - ClassName::instanceMethod 380 | - (args) -> expr.oinstanceMethod(args) 381 | - expr::instanceMethod 382 | 383 | 위에서 설명한 기법을 이용하면 람다 표현식을 메서드 참조를 사용해서 다음 처럼 줄일 수 있다. 384 | 385 | ```java 386 | List str = Arrays.asList("a", "b", "A", "B"); 387 | str.sort(String::compareToIgnoreCae); 388 | ``` 389 | 390 | 즉, 메서드 참조는 콘텍스트의 형식과 일치해야 한다. 391 | 392 | ### 생성자 참조 393 | 394 | `ClassName::new` 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 395 | 396 | ```java 397 | // Supplier c1 = () -> new Apple(); 398 | Supplier c1 = Apple::new; 399 | Apple a1 = c1.get(); 400 | ``` 401 | 402 | - Quiz 403 | 404 | Color(int, int, int) 처럼 인수가 세 개인 생성자의 생성자 참조를 사용하려면 어떻게 해야할까? 405 | 406 | - 정답 407 | 408 | 생성자 참조 문법은 ClassName::new 이므로 Color 생성자의 참조는 Color::new가 된다. 409 | 410 | 하지만 이를 이용하려면 `생성자 참조와 일치하는 시그처를 갖는 함수형 인터페이스`가 필요하다. 411 | 412 | 아래와 같이 만들 수 있다. 413 | 414 | ```java 415 | public interface TriFunction { 416 | R apply(T t, U u, V v); 417 | } 418 | ``` 419 | 420 | 아래 처럼 생성자 참조를 사용할 수 있다. 421 | 422 | ```java 423 | TriFunction colorFactory = Color::new; 424 | ``` 425 | 426 | ## 람다, 메서드 참조 활용하기 427 | 428 | ### 람다 표현식을 조합할 수 있는 유용한 메서드 429 | 430 | Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공한다. 431 | 이 메서드가 바로 `디폴트 메서드(default method)`이다. 432 | 433 | - `Comparator 조합` 434 | 435 | - 역정렬 436 | 437 | 사과의 무게를 내림차순으로 정렬하고 싶다면 어덯게 해야 할까? Comparator 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 438 | 디폴트 메서드를 제공한다. 439 | 440 | ```java 441 | inventory.sort(comparing(Apple::getWeight).reversed()); 442 | ``` 443 | 444 | 만약 무게가 같은 사과가 존재한다면 어떻게 해야 할까? 무게가 같은경우 국가별로 정렬할 수 도 있다. 445 | 446 | ```java 447 | inventory.sort(comapring(Apple::getWeight) 448 | .reversed() 449 | .thenComparing(Apple::getCountry)); 450 | ``` 451 | 452 | - `Predicate 조합` 453 | 454 | Predicate 인터페이슨느 복잡한 프레디케이트를 만들 수 있도록 `negate, and, or` 세 가지 메서드를 제공한다. 455 | 456 | - 빨간색이 아닌 사과 457 | 458 | ```java 459 | Predicate notRedApple = redApple.negate(); 460 | ``` 461 | 462 | - and 를 사용하여 빨간색이면서 무거운 사과 선택 463 | 464 | ```java 465 | Predicate redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150); 466 | ``` 467 | 468 | - or 를 사용하여 빨간색이면서 무거운 사과 또는 그냥 녹색 사과 469 | 470 | ```java 471 | Predicate readAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150) 472 | .or(apple -> GREEN.equals(a.getColor())); 473 | ``` 474 | 475 | - `Function 조합` 476 | 477 | Function 인터페이스는 `andThen`과 `compose` 두 가지 디폴트 메서드를 제공한다. 478 | 479 | 예를 들어 숫자를 증가 시키는 x -> x + 1 이라는 f 함수가 있고, 숫자에 2를 곱하는 g 함수가 있다고 가정하자. 480 | 481 | - andThen 482 | 483 | ```java 484 | Function f = x -> x + 1; 485 | Function g = x -> x * 2; 486 | Function h = f.andThen(g); // g(f(x)) 487 | int result = h.apply(1); // 4를 반환 488 | ``` 489 | 490 | - compose 491 | 492 | ```java 493 | Function h = f.compose(g); // f(g(x)) 494 | ``` 495 | 496 | ## 정리 497 | 498 | - 람다는 `파라미터 -> (화살표) 바디`로 이루어진다. 499 | - (parameters) -> expression; 500 | - (parameters) -> { statements; } 501 | 502 | - 많은 디폴트 메서드가 존재하더라도 `추상 메서드`가 하나이면 함수형 인터페이스이다. 503 | - 람다 표현식의 시그니처를 서술하는 메서드를 `함수 디스크립터` 라고 한다. 504 | -------------------------------------------------------------------------------- /04장(스트림 소개).md: -------------------------------------------------------------------------------- 1 | ## 스트림(Streams) 2 | 3 | 스트림은 자바 8 API에 새로 추가된 기능이다. 스트림을 이용하면 `선언형(즉, 데이터를 처리하는 임시 구현 코드 대신 질의로 표현 할 수 있다.)`으로 4 | 컬렉션 데이터를 처리할 수 있다. 5 | 6 | > https://tecoble.techcourse.co.kr/post/2020-05-14-foreach-vs-forloop/ 7 | 8 | - __Effetive Java 3 item - 46__ 9 | - 스트림은 함수형 프로그래밍에 기초한 패러다임이다. 10 | - 순수 함수여야 한다. 11 | - 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다. 12 | - forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고 계산하는 데는 쓰지 말자. 13 | 14 | > js 의 foreach 는 for 문을 `선언형`으로 표현할 뿐 stream 의 foreach 와는 다른 패러다임이다. 15 | 16 | `스트림은 데이터 컬렉션 반복을 멋지게 처리하는 기능` 17 | 18 | 또한 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 `투명하게` 병렬로 처리할 수 있다. 19 | 20 | - 기존 코드 21 | 22 | ```java 23 | List(); 24 | for(Dish dish : menu) { 25 | if(dish.getCalories() < 400) { 26 | lowCaloricishes.add(dish); 27 | } 28 | } 29 | 30 | Collections.sort(lowCaloricDishes, new Comparator() { 31 | public int compare(Dish dish1, Dish dish2) { 32 | reutnr Integer.compare(dish1.getCalories(), dish2.getCalories()); 33 | }); 34 | 35 | List(); 36 | for(Dish dish : lowCaloricDishes) { 37 | lowCaloricDishesName.add(dish.getName()); 38 | } 39 | ``` 40 | 41 | - 스트림 사용(선언형 코드) 42 | 43 | ```java 44 | import static java.util.Comparator.comparing; 45 | import static java.util.stream.Collectors.toList; 46 | 47 | List lowCaloricDishesName = menu.stream() 48 | .filter(d -> d.getCalories() < 400) // 400 칼로리 이하의 요리 선택 49 | .sorted(comparing(Dish::getCalories)) // 칼로리로 요리 정렬 50 | .map(Dish::getName) // 요리명 추출 51 | .collect(toList()); // 모든 요리명을 리스트에 저장 52 | ``` 53 | 54 | stream()을 parallelStream()으로 바꾸면 이 코드를 멀티코어 아키텍처에서 병렬로 실행할 수 있다. 55 | 56 | filter(map, sorted, collect) `fmsc` 같은 연산은 `고수준 빌딩 블록(high-level building block)`으로 이루어져 있어서 57 | 특정 스레딩 모델에 제한되지 자유롭게 어떤 상황에서든 사용할수 있다. 또한 이들은 내부적으로 단일 스레드 모델에 사용할 수 있지만 58 | 멀티 코어 아키텍처를 최대한 투명하게 활용할 수 있게 구현되어있다. 결과적으로 우리는 데이터 처리 과정을 병렬화하면서 스레드와 락을 걱정할 필요가 59 | 없다. 60 | 61 | ## 스트림 시작하기 62 | 63 | 스트림 이란 `데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소(Sequence of elements)`로 정의할 수 있다. 64 | 65 | - 연속된 요소 66 | - 소스 67 | - 데이터 처리 68 | 69 | 컬렉션의 주제는 데이터고, 스트림의 주제는 계산이다. 70 | 71 | 스트림은 filter, map, reduce, find, match, sort 등으로 데이터를 조작할 수 있다. 그리고 순차적으로 또는 병렬로 실행할 수 있다. 72 | 73 | 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다. 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다. 74 | 즉, 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지한다. 75 | 76 | ### 스트림의 중요한 2가지 특징 77 | 78 | #### 파이프라이닝(Pipelining) 79 | 80 | 대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프 라인을 구성할 수 있도록 `스트림 자신을 반환`한다. 그 덕분에 `게으름(laziness), 쇼트서킷(short-cricuiting)` 같은 최적화도 얻을 수 있다. 연산 파이프라인은 데이터 솟에 적용하는 데이터베이스 질의와 비슷하다. 81 | 82 | #### 내부 반복 83 | 84 | 컬렉션은 반복자를 이용해서 명시적으로 반복하지만, 스트림은 내부 반복을 지원한다. 85 | 86 | 87 | ```java 88 | public class Main { 89 | 90 | public static void main(String[] args) { 91 | /** 92 | * 데이터 소스 : 요리 메뉴 93 | * 연속된 요소 : 내부 리스트 객체 94 | * */ 95 | List menu = Arrays.asList( 96 | new DishVo("pork", false, 800, Type.MEAT), 97 | new DishVo("beef", false, 700, Type.MEAT), 98 | new DishVo("chicken", false, 400, Type.MEAT), 99 | new DishVo("fries", true, 500, Type.OTHER), 100 | new DishVo("rice", true, 350, Type.OTHER), 101 | new DishVo("fruit", true, 120, Type.OTHER), 102 | new DishVo("pizza", false, 600, Type.OTHER), 103 | new DishVo("prawns", false, 300, Type.FISH), 104 | new DishVo("salmon", false, 450, Type.FISH) 105 | ); 106 | 107 | /** 108 | * 스트림으로 데이터 처리 109 | * filter, map, limit, collect 데이터 처리 연산 110 | * - pipeline 111 | * collect 를 제외한 처리 연산들 112 | * filter, map, limit은 스트림을 반환하지만, collect는 리스트를 반환한다. 113 | */ 114 | List threeHighCaloricDishNames = 115 | menu.stream() // 메뉴에서 스트림을 얻는다. 116 | // 파이프라인 연산 만들기 117 | .filter(dishVo -> dishVo.getCalories() > 300) // 고칼로리 요리 필터링, Stream // 중간 연산 118 | .map(DishVo::getName) // 요리명 추출 Stream // 중간 연산 119 | .limit(3) // 선착순 3개만 선택 Stream // 중간 연산 120 | .collect(toList()); // 결과를 다른 리스트로 저장 List 121 | 122 | System.out.println(threeHighCaloricDishNames); 123 | } 124 | } 125 | ``` 126 | 127 | ## 스트림 연산 128 | 129 | - 중간 연산(intermediate opertiaon) 130 | - 연결할 수 있는 스트림 연산 131 | - 최종 연산(terminal operation) 132 | - 스트림을 닫는 연산 133 | 134 | ### 중간 연산 135 | 136 | 중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 것. 즉, `게으르다(lazy)`는 것이다. 137 | 138 | 스트림의 게으른 특성 덕분에 몇 가지 최적화 효과를 얻을 수 있었다. limit과 쇼트 서킷(short circuit) 기법 덕분에 여러개의 요리중에서 3개만 선택되며 139 | filter와 map은 서로 다른 연산이지만 한 과정으로 병합되었다.(이 기법을 루프 퓨전(loop fusion) 이라고 한다.) 140 | 141 | ### 최종 연산 142 | 143 | 최종 연산은 스트림 파이프라인에서 결과를 도출한다. 144 | 145 | ## 스트림과 컬렉션 146 | 147 | 자바의 기존 컬렉션과 새로운 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다. 여기서 `연속된(sequenced)`이라는 표현은 148 | 순서와 상관이 아무 값에나 접속하는 것이 아니라 순차적으로 값에 접근한다는 것을 의미한다. 149 | 150 | - 스트림과 컬렉션의 차이 151 | - 스트림 152 | - 내부 반복(internal iteration) 153 | - 딱 한 번만 소비 154 | - 게으른 생성(필요할 때만 값을 계산) 155 | - 컬렉션 156 | - 외부 반복(external iteration) 157 | - 적극적 생성(값이 다 계산 될 때 까지 기다림) 158 | 159 | 160 | > 스트림은 생산자(producer)와 소비자(consumer)의 관계를 형성한다. 또한 스트림은 게으르게 만들어지는 컬렉션과 같다. 즉, 사용자가 데이터를 161 | 요청할 때만 값을 계산한다.(경영학에서는 이를 요청 중심 제조(demand-driven manufacturing) 또는 즉석 제조(just-in-time manufacturing) 라고 부른다.) 반면 컬렉션은 적극적으로 생성된다.(생산자 중심(supplier-driven)) 162 | 163 | - `적극적 생성` 이란 모든 값을 계산 할 때 까지 기다린다는 의미이다. 164 | - `게으른 생성` 이란 필요할 때만 값을 계산 한다는 의미이다. 165 | 166 | 스트림도 반복자와 같이 `딱 한 번만 탐색` 할 수 있다. 즉, 탐색된 스트림의 요소는 소비된다. 167 | 168 | - 외부 반복 169 | - 컬렉션 인터페이스를 사용하기 위해서 for-each 등으로 사용자가 직접 요소를 반복해야함. 170 | - 병렬성을 스스로 관리해야함(synchronized 등) 171 | - 내부 반복 172 | - 함수에 어떤 작업을 수행할지만 지정하면 모든 것이 알아서 처리된다. 173 | - 병렬성 구현을 자동으로 선택함. 174 | 175 | ```java 176 | List names = new ArrayList<>(); 177 | for(Dish dish: menu) { 178 | names.add(dish.getName()); 179 | } 180 | 181 | List names = menu.stream() 182 | .map(Dish::getName) 183 | .collect(toList()); 184 | ``` 185 | 186 | ## map, filter 187 | 188 | - map : 특정 값을 추출해서 Stream 형식으로 저장 189 | - filter : 어떠한 조건으로 필터링해서 Stream 형식으로 저장 190 | 191 | ## 스트림의 이용 과정 3가지 192 | 193 | - 질의를 수행할 컬렉션과 같은 데이터 소스 194 | - 스트림 파이프라인을 구성할 중간 연산 연결 195 | - 스트림 파이프라인을 실행하고 결과를 만들 최종 연산 196 | 197 | 스트림 파이프라인의 개념은 `빌더 패턴(builder pattern)`과 비슷하다. builder 패턴은 호출을 연결해서 설정을 만들고 마지막 build() 메서드를 호출해서 198 | 닫는다. 199 | -------------------------------------------------------------------------------- /05장(스트림 활용).md: -------------------------------------------------------------------------------- 1 | # 스트림 활용 2 | 3 | ## 필터링 4 | 5 | ### 프레이케이트로 필터링 6 | 7 | filter 메서드는 `프레디케이트(불리언을 반환하는 함수)`를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다. 8 | 9 | ```java 10 | List numbers = Arrays.asList<1, 2, 1, 3, 3, 2, 4); 23 | numbers.stream() 24 | .filter(i -> i % 2 == 0) 25 | .distinct() 26 | .forEach(System.out::println); 27 | ``` 28 | 29 | ## 스트림 슬라이싱(자바 9) 30 | 31 | 스트림 슬라이싱은 자바 9에서 도입된 새 기술이며, 스트림 슬라이싱을 이용하면 아래와 같은 이점을 누릴 수 있다. 32 | 33 | - 프레디케이트를 이용하는 방법 34 | - 스트림의 요소를 선택하거나 스킵하는 다양한 방법 35 | - 스트림의 처음 몇 개의 요소를 무시하는 방법 36 | - 특정 크기로 스트림을 줄이는 방법 37 | 38 | ### 프레디케이트를 이용한 슬라이싱 39 | 40 | 자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 `takeWhile, dropWhile` 두 가지 새로운 메서드를 지원한다. 41 | 42 | ```java 43 | List specialMenu = Arrray.asList( 44 | new Dish("seasonal fruit", true, 12, Dish.Type.OTHER), 45 | new Dish("prawns", false, 300, Dish.Type.FISH), 46 | new Dish("rice", true, 350, Dish.Type.OTHER), 47 | ... 48 | ); 49 | ``` 50 | 51 | 위 리스트는 이미 칼로리 순으로 정렬되어 있다. 52 | 53 | 만약 320 칼로리 이하의 요리를 선택하려면 filter가 떠오를 것이다. 54 | 55 | 따라서 320 칼로리보다 크거나 같은 요리가 나왔을 때 반복작업을 중단 할 수 있는데, `takeWhile` 을 사용하면된다. 56 | 57 | 아주 많은 요소를 포함하는 큰 스트림에서는 이 차이가 상당히 커질 수 있다. 58 | 59 | ```java 60 | List slicedMenu1 61 | = specialMenu.stream() 62 | .takeWhile(dish -> dish.getCalories() < 320) 63 | .collect(toList()); 64 | ``` 65 | 66 | 만약 나머지 요소(320 칼로리보다 큰 것들)를 선택하려면 `dropWhile`을 이용하면 된다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 67 | 발견된 요소를 버린다. 68 | 69 | ```java 70 | List slicedMenu2 71 | = specialMenu.stream() 72 | .dropWhile(dish -> dish.getCalories() < 320) 73 | .collect(toList()); 74 | ``` 75 | 76 | ### 스트림 축소 77 | 78 | 스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 `limit(n)` 메서드를 지원한다. 79 | 80 | ### 요소 건너뛰기 81 | 82 | 스트림은 `처음 n개 요소를 제외한 스트림을 반환`하는 `skip(n)` 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 83 | 스트림이 반환된다. 84 | 85 | ## 매핑 86 | 87 | 스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다. (`특정 객체에서 특정 데이터를 선택하는 작업`) 88 | 89 | > map과 flatMap은 변환에 가까운 매핑 90 | 91 | ### 스트림의 각 요소에 함수 적용하기 92 | 93 | 스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다. 94 | (이 과정은 기존 값을 고친다(modify) 라는 개념보다는 `새로운 버전을 만든다` 개념에 가까우므로 `변환(transforming)에 가까운 매핑(mapping)`이라는 95 | 단어를 사용한다.) 96 | 97 | - 요리명 추출 98 | 99 | ```java 100 | List dishNames = menu.stream() 101 | .map(Dish::getName) 102 | .collect(toList()); 103 | ``` 104 | 105 | ### flatMap 106 | 107 | flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, `하나의 평면화된 스트림을 반환`한다. 108 | 109 | ```java 110 | List uniqueCharacters = 111 | words.stream() 112 | .map(word -> word.splict("")) // 각 단어를 개별 문자를 포함하는 배열로 반환 113 | .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화 114 | .distinct() 115 | .collect(toList()); 116 | ``` 117 | 118 | - 숫자 1 2 3 4 5가 제공되면 해당 숫자의 제곱근을 반환 1 4 9 16 25 119 | 120 | ```java 121 | List numbers = Arrays.asList(1,2,3,4,5); 122 | List squars = numbers.stream() 123 | .map(n -> n * n) 124 | .collect(toList()); 125 | ``` 126 | 127 | - 두 개의 숫자 리스트 1,2,3과 3,4 가 있을때 모든 숫자 쌍의 리스트를 반환 하시오 (1,3) (1,4) (2,3) ... 128 | 129 | ```java 130 | List numbers1 = Arrays.asList(1, 2, 3); 131 | List numbers2 = Arrays.asList(3, 4); 132 | List pairs = numbers1.stream() 133 | .flatMap(i -> numbers2.stream().map(j -> new int[]{i, j})) 134 | .collect(toList()); 135 | ``` 136 | 137 | ## 검색과 매칭 138 | 139 | ### 프레디케이트가 적어도 한 요소와 일치하는지 확인 140 | 141 | - anyMatch 메서드 사용 142 | 143 | ```java 144 | if(menu.stream().anyMatch(Dish::isVegetarian)) { 145 | System.out.println("The menu is somewhat vegetarian friendly!!"); 146 | } 147 | ``` 148 | 149 | ### 프레디케이트가 모든 요소와 일치하는지 검사 150 | 151 | - allMatch 메서드 사용 152 | 153 | ```java 154 | boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000); 155 | ``` 156 | 157 | ### 프레디케이트가 모든 요소와 일치하지 않는 경우를 검사 158 | 159 | - noneMatch 메서드 사용 160 | 161 | ```java 162 | boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000); 163 | ``` 164 | 165 | > anyMatch, allMatch, noneMatch 세 메서드는 스트림 `쇼트서킷` 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다. 166 | 167 | ## 쇼트서킷(short circuit) 168 | 169 | 예를 들어 여러 and 연산으로 연결된 커다란 불리언 표현식을 평가한다고 가정하자. 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 170 | 상관없이 전체 결과도 거짓이 된다. 이러한 상황을 쇼트서킷이라고 부른다. 스트림의 allMatch, noneMatch, findFirst, findAny, limit 등의 연산은 171 | 모든 스트림의 요소를 처리하지 않고도 반환할 수 있다. 즉, 원하는 요소를찾았으면 즉시 결과를 반환할 수 있다. 172 | 173 | ### 요소검색 174 | 175 | - findAny 176 | - 스트림에서 임의의 요소를 반환한다. 177 | 178 | ```java 179 | Optional dish = menu.stream() 180 | .filter(Dish::isVegetarian) 181 | .findAny(); 182 | ``` 183 | 184 | `Optional` 클래스는 값으 존재나 부재 여부를 표현하는 컨테이너 클래스이다. 만약 findAny()가 null을 반환한다면 쉽게 에러를 일으킬 수 있다. 185 | 이때 Optional에서 제공하는 `ifPresent(Consumer block)`을 사용할 수 있다. 186 | 187 | ```java 188 | Optional dish = menu.stream() 189 | .filter(Dish::isVegetarian) 190 | .findAny() 191 | .ifPresent(dish -> System.out.println(dish.gtName()); // 값이 있으면 반환하고 없으면 아무일도 일어나지 않는다. 192 | ``` 193 | 194 | ### 첫 번째 요소 찾기 195 | 196 | ```java 197 | List someNumbers = Arrays.asList(1,2,3,4,5); 198 | Optional firstSquareDivisibleByThree = someNumbers.stream() 199 | .map(n -> n * n) 200 | .filter(n -> n % 3 == 0) 201 | .findFirst(); //9 202 | ``` 203 | 204 | ### findFirst와 findAny는 언제 사용할까 ? 205 | 206 | findFirst와 findAny의 메서드가 모두 필요한 이유는 `병렬성` 때문이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 207 | 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다. 208 | 209 | ## 리듀싱 210 | 211 | 리듀싱 연산이란 모든 스트림 요소를 처리해서 값으로 도출하는 연산을 의미한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 212 | 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 `폴드(fold)`라고 부른다. 213 | 214 | - for-each 루프를 사용해서 리듀싱하기 215 | 216 | ```java 217 | int sum = 0; 218 | for(int x : numbers) { 219 | sum += x; 220 | } 221 | ``` 222 | 223 | 리스트에서 숫자가 하나 남을 때까지 reduce 과정을 반복한다. 224 | 225 | - reduce 사용하기 226 | 227 | ```java 228 | int sum = numbers.stream().reduce(0, (a,b) -> a+b); 229 | ``` 230 | 231 | reduce는 두 개의 인수를 갖는다. 232 | 233 | - 초기값 0 234 | - 두 요소를 조합해서 새로운 값을 만드는 `BinaryOperator` 235 | 236 | ### reduce 진행 과정 237 | 238 | ```java 239 | // 숫자리스트 : 8,4,1,9,7,5 240 | reduce(0, (a, b) -> a + b) 241 | ``` 242 | 243 | - 위 처럼 되어있을 경우 초기값 0이 a의 자리에 들어가고, b에는 8이 들어간다. 244 | - 누적값이 8이 되었다. 245 | - 누적값 8이 a에 들어가고, b에는 4가 들어간다. 246 | - 누적값이 12가 되었다. 247 | - 누적값 12가 a에 들어가고, b에는 1이 들어간다. 248 | - 누적값이 13이 되었다. 249 | - 반복... 250 | 251 | 메서드 참조로 간결하게 코드를 수정하면 아래와 같다. 252 | 253 | ```java 254 | int sum = numbers.stream().reduce(0, Integer::sum); 255 | ``` 256 | 257 | ### 초기값이 없는 경우 258 | 259 | 초기값을 받지 않도록 오버로드된 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환한다. 260 | 261 | ```java 262 | Optional sum = numbers.stream().reduce((a, b) -> (a + b)); 263 | ``` 264 | 265 | Optional을 반환하는 이유는 스트림에 아무 요소도 없는 경우 reduce가 합계를 반환할 수 없기 때문이다. 266 | 267 | ## 맵 리듀스 패턴(map-reduce pattern) 268 | 269 | map과 reduce를 연결하는 기법을 `맵 리듀스 패턴` 이라고 한다. 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다. 270 | 271 | ```java 272 | int count = menu.stream() 273 | .map(d -> 1) 274 | .reduce(0, (a,b) -> a + b); 275 | ``` 276 | 277 | ## 맵, 필터 278 | 279 | - 추출 : map 280 | - 선택 : filter 281 | 282 | ## 숫자형 스트림 283 | 284 | - reduce를 이용한 스트림 요소의 합 구하기 285 | 286 | ```java 287 | int calories = menu.stream() 288 | .map(Dish::getCalories) 289 | .reduce(0, Integer::sum); 290 | ``` 291 | 292 | 위 과정에서는 내부적으로 합계를 계산하기 위해서 Integer가 int형으로 언박싱 된다. 따라서 박싱 비용이 소모된다. 293 | 294 | ### 기본형 특화 스트림(primitive stream specialization) 295 | 296 | 자바 8에서는 세 가지 기본형 특화 스트림을 제공한다. 스트림 API는 박싱 비용을 피할 수 있도록 int 요소에 특화된 `IntStream`, double 요소에 특화된 297 | `DoubleStream`, long 요소에 특화된 `LongStream`을 제공한다. 298 | 299 | 각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다. 300 | 301 | #### 숫자 스트림으로 매핑 302 | 303 | 스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세가지 메서드를 가장 많이 사용한다. 304 | 305 | ```java 306 | int calories = menu.stream() 307 | .mapToInt(Dish::getCalories) 308 | .sum(); 309 | ``` 310 | 311 | #### 객체 스트림으로 복원하기 312 | 313 | ```java 314 | IntStream intStream = menu.strema().mapToInt(Dish::getCalories); // 스트림을 숫자 스트림으로 변환 315 | Stream stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환 316 | ``` 317 | 318 | #### 숫자 범위 319 | 320 | 자바 8의 IntStream과 LongSteam에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다. 321 | 322 | ```java 323 | IntSream evenNumbers = IntStream.rangeClosed(1, 100) 324 | .filter(n -> n % 2 == 0); 325 | System.out.println(evenNumbers.count()); 326 | ``` 327 | 328 | rangeClose(1, 100)은 1과 100을 포함하며 range(1, 100)은 1과 100을 제외한다. 329 | 330 | ## 스트림 만들기 331 | 332 | ### 값으로 스트림 만들기 333 | 334 | 임의의 수를 인수로 받는 정적 메서드 `Stream.of`를 이용해서 스트림을 만들 수 있다. 335 | 336 | - Stream.of로 문자열 스트림을 만드는 예제 337 | 338 | ```java 339 | Stream stream = Stream.of("Mordern", "Java", "In", "Action"); 340 | stream.map(String::toUpperCase).forEach(System.out.println); 341 | ``` 342 | 343 | ### 배열로 스트림 만들기 344 | 345 | `Arrays.stream`을 이용해서 배열을 인수로 받아 스트림을 만들 수 있다. 346 | 347 | ```java 348 | int[] numbers = {2, 3, 5, 7, 11, 13}; 349 | int sum = Arrays.stream(numbers).sum(); 350 | ``` 351 | 352 | ### 함수로 무한 스트림 만들기 353 | 354 | 스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용해서 `무한 스트림(infinite stream)` 즉, `언바운드 스트림(unbounded stream)`을 만들 수 있다. 355 | 356 | - 피보나치수열 357 | 358 | 0, 1, 1, 2, 3, 5, 8 ... 359 | 360 | ```java 361 | Stream.iterate(new int[]{0, 1}, t-> new int[] {t[1], t[0] + t[1]}) 362 | .limit(10) 363 | .map(t -> t[0) 364 | .forEach(System.out::println); 365 | ``` 366 | 367 | - generate 메서드 368 | 369 | generate는 iterate와 달리 생산된 각 값을 연속적으로 계산하지는 않는다. 370 | 371 | ```java 372 | Stream.generate(Math::random) 373 | .limit(5) 374 | .forEach(System.out::println) 375 | ``` 376 | -------------------------------------------------------------------------------- /06장(스트림으로 데이터 수집).md: -------------------------------------------------------------------------------- 1 | ## 스트림으로 데이터 수집 2 | 3 | - Collectors 클래스로 컬렉션 만들고 사용하기 4 | - 하나의 값으로 데이터 스트림 리듀스 하기 5 | - 특별한 리듀싱 요약 연산 6 | - 데이터 그룹화와 분할 7 | - 자신만의 커스텀 컬렉터 개발 8 | 9 | > 컬렉션(Collection), 컬렉터(Collector), collect는 서로 다르다. 10 | 11 | ## 컬렉터란 무엇인가? 12 | 13 | Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다. 14 | 15 | > 스트림에 collect를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산이 수행된다. 즉, 내부적으로 `리듀싱 연산`이 일어난다. 16 | 17 | 18 | Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분할 수 있다. 19 | 20 | - 스트림 요소를 하나의 값으로 리듀스하고 요약 21 | - 요소 그룹화 22 | - 요소 분할 23 | 24 | ## reducing 25 | 26 | 범용 Collectors.reducing을 사용 27 | 28 | ```java 29 | int totalCalories = menu.stream() 30 | .collect(reducing(0, Dish::getCaloreis, (i, j) -> i + j)); 31 | ``` 32 | 33 | reducing은 세 개의 인수를 받는다. (초기값, 합계 함수, 변환 함수) 34 | 35 | - 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다.(숫자 합계에서는 인수가 없을 때 반환하므로 0이 적합하다.) 36 | - 두 번째 인수는 함수를 받는다. 37 | - 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator이다. 38 | 39 | ### 한 개의 인수를 갖는 reducing 40 | 41 | 가장 칼로리가 높은 요리 찾는 방법 42 | 43 | ```java 44 | Optional mostCaloireDish = menu.stream().collect(reducing((d1, d2) -> d1.getCaloreis() > d2.getCalories() ? d1 : d2)); 45 | ``` 46 | 47 | 한 개의 인수를 갖는 reducing 팩터리 메서드는 세 개의 인수를 갖는 reducing 메서드에서 첫 번째 인수를 받고, 두 번째 인수에서 48 | `자기 자신을 그대로 반환하는 항등함수(identity function)`를 두 번째 인수로 받는 상황에 해당한다. 49 | 50 | 따라서 한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌으래 시작값이 설정되지 않아 null을 반환할 수 있으므로 51 | Optional 객체로 만들어 사용해야 한다. 52 | 53 | ### 컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다. 54 | 55 | ```java 56 | int totalCaloires = menu.stream().collect(reducing(0, Dish::getCaloires, Integer::sum)); 57 | ``` 58 | 59 | ## 그룹화 60 | 61 | 자바8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현할 수 있다. 62 | 63 | ```java 64 | /** 65 | * 그룹화 groupingBy 66 | * 생선, 고기 그 밖의 것들로 그룹화 67 | */ 68 | Map> dishesByType = menu.stream().collect(groupingBy(Dish::getType)); 69 | ``` 70 | 71 | groupingBy를 `분류 함수(classification function)` 이라고 한다. 72 | 73 | ```java 74 | /** 75 | * 칼로리별로 그룹화 76 | */ 77 | 78 | Map> dishesByCaloricLevel = menu.stream().collect( 79 | groupingBy(dish -> { 80 | if(dish.getCalories() <= 400) return CaloricLevel.DIET; 81 | else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL; 82 | else return CaloricLevel.FAT; 83 | })); 84 | ``` 85 | 86 | ```java 87 | // 500칼로리가 넘는 요리만 타입과 종류로 그룹화 88 | Map> caloricDishesByType = menu.stream().filter(dish -> dish.getCalroies() > 500) 89 | .collect(groupingBy(Dish::getType)); 90 | /** 91 | * 결과 92 | * {OTHER=[french fries, pizza], MEAT=[pork, beef]} 93 | * 위 코드의 단점은 위 filter 프레디케이트를 만족하는 값이 없을 경우 키값 자체가 제외되서 맵에 담지 못한다. 94 | * 해결책 : Collectors 클래스의 정적 팩터리 메서드인 filtering 사용 95 | */ 96 | 97 | // 해결 98 | Map> caloricDishesByType = menu.stream().collect(groupingBy 99 | (Dish::getType, filtering(dish -> dish.getCalories() > 500, toList()))); 100 | // 결과 : {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]} 101 | 102 | /** mapping 사용 */ 103 | Map> dishNamesByType = menu.stream(). 104 | collect(groupingBy(Dish::getType, mapping(Dish::getName, toList()))); 105 | 106 | /** flatMapping 사용 107 | * flatMap은 두 수준의 리스트를 한 수준으로 평면화 할 수 있음 108 | */ 109 | Map> dishNamesByType = 110 | menu.stream() 111 | .collect(groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet()))); 112 | ``` 113 | 114 | ### groupingBy 115 | 116 | groupingBy(x)는 사실 groupingBy(x, toList())의 축약형이다. 117 | 118 | - 요리의 종류를 분류하는 컬렉터로 메뉴에서 가장 높은 칼로리를 가진 요리를 찾는 프로그램 119 | 120 | ```java 121 | Map> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCaloires)))); 122 | ``` 123 | 124 | ### collectingAndThen 125 | 126 | - 팩토리 메서드 collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다. 127 | 128 | ```java 129 | Map mostCaloricByType = 130 | menu.stream() 131 | .collect(groupingBy(Dish::getType, collectingAndThen(maxBy(comparingInt(Dish::getCaloreis)), Optioanl::get)); 132 | ``` 133 | 134 | - 각 요리 형식에 존재하는 모든 CaloricLevel 값을 알고 싶은 경우(groupingBy와 mapping 사용) 135 | 136 | ```java 137 | Map> caloricLevelByType = 138 | menu.stream().collect( 139 | groupingBy(Dish::getType, mapping(dish -> { 140 | if(dish.getCalories() <= 400) return CaloricLevel.DIET; 141 | else if(dish.getCalories() <= 700 return CaloricLevel.NORMAL; 142 | else return CaloricLevel.FAT; }, 143 | toSet() ))); 144 | ``` 145 | 146 | ## 분할(partitioningBy) 147 | 148 | 분할은 `분할 함수(partitioning function)`이라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불리언을 149 | 반환하므로 맵의 키 형식은 `Boolean`이다. 150 | 151 | 분할의 장점은 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다. 152 | 153 | - 모든 요리를 채식과 아닌 요리로 분류 154 | 155 | ```java 156 | Map> partitionedMenu = 157 | menu.stream().collect(partitioningBy(Dish::isVegetarian)); 158 | 159 | List vegetarianDishes = partitionedMenu.get(true); 160 | 161 | List vegetarianDishes2 = menu.stream().filter(Dish::isVegetarian).collct(toList()); 162 | ``` 163 | 164 | - 채식과 채식이 아닌 요리에서 가장 칼로리가 높은 음식 찾기 165 | 166 | ```java 167 | Map mostCaloricPartitioneByVegetarian = 168 | menu.stream().collect( 169 | partitioningBy(Dish::isVegetarain, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get))); 170 | ``` 171 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /07장(병렬 데이터 처리와 성능).md: -------------------------------------------------------------------------------- 1 | # 병렬 데이터 처리와 성능 2 | 3 | 컬렉션에 `parallelStream`을 호출하면 `병렬 스트림(parallel stream)`이 생성된다. 4 | 5 | > 병렬 스트림이란 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다. 따라서 병렬 스트림을 이용하면 모든 멀티코어 6 | 프로세서가 각각의 청크를 처리하도록 할당할 수 있다. 7 | 8 | - EX) 숫자 n을 인수로 받아서 1부터 n까지의 모든 숫자의합계를 반환하는 메서드를 구현한다고 가정하자. 9 | 10 | ```java 11 | public long sequentialSum(long n) { 12 | return Stream.iterate(1L, i->i+1) 13 | .limit(n) 14 | .reduce(0L, Long::sum); 15 | } 16 | 17 | // 반복문으로 구현 18 | 19 | public long iterativeSum(long n) { 20 | long result = 0; 21 | for(long i = 1L; i <= n; i++) { 22 | result += i; 23 | } 24 | return result; 25 | } 26 | ``` 27 | 28 | 여기서 n이 커진다면 이 연산을 병렬로 처리하는게 좋다. 결과 변수는 어떻게 동기화 할까?, 몇 개의 스레드를 사용할 까? 숫자는 어떻게 생성할까? 등의 29 | 걱정을 병렬 스트림을 이용하면 없앨 수 있다. 30 | 31 | ## 순차 스트림을 병렬 스트림으로 변환하기 32 | 33 | 순차 스트림에 `parallel()` 메서드를 호출하면 기존의 함수형 리듀싱 연산(숫자 합계 계산)이 병렬로 처리된다. 반면에 `sequential()` 메서드를 호출하면 34 | 순차 스트림으로 바꿀 수 있다. 35 | 36 | > parallel()을 호출하면 내부적으로 병렬로 수행해야 함을 의미하는 불리언 플래그가 설정된다. 37 | 38 | ```java 39 | public long parallelSum(long n) { 40 | return Stream.iterate(1L, i->i+1) 41 | .limit(n) 42 | .parallel() 43 | .reduce(0L, Long::sum); 44 | } 45 | ``` 46 | 47 | 병렬 스트림을 이용하면 스트림이 여러 청크로 쪼개져서 연산을 수행한다. 48 | 49 | ## 병렬 스트림에서 사용하는 스레드 풀 설정 50 | 51 | 병렬 스트림은 내부적으로 `ForkJoinPool`을 사용한다 52 | -------------------------------------------------------------------------------- /08장(컬렉션 API 개선).md: -------------------------------------------------------------------------------- 1 | # 컬렉션 API 개선 2 | 3 | ## 오버로딩 vs 가변인수 4 | 5 | List나 Set, Map 인터페이스를 살펴보면 List.of와 같은 다양한 오버로드 버전이 존재한다. 6 | 7 | ```java 8 | static List of(E e1, E e2, E e3, E e4) 9 | static List of(E e1, E e2, E e3, E e4, E e5) 10 | ``` 11 | 12 | 왜 다중요소를 받을 수 있도록 자바 API를 만들지 않은 것인지 궁금할 것이다. 13 | 14 | ```java 15 | static List of(E... elements) 16 | ``` 17 | 18 | 내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다. 따라서 배열을 할당하고 초기화 하며 나중에 가비지 컬렉션을 사용하는 비용을 지불하게 19 | 된다. 고정된 숫자의 요소(최대 10개까지)를 API로 정의하므로 이런 비용을 제거할 수 있다. List.of로 열 개 이상의 요소를 가진 리스트를 만들 수도 있지만 20 | 이 때는 가변 인수를 이용하는 메서드가 사용된다. 21 | 22 | ## 바꿀 수 없는 리스트, 집합, 맵 23 | 24 | ```java 25 | List friends = List.of("abc", "efg", "hij"); // 변수에 추가 삭제 못함 26 | Set friends2 = Set.of("abc", "efg", "hij"); 27 | ``` 28 | 29 | 맵은 2가지 방식으로 나뉜다. 30 | 31 | 1. 10개 이하의 키와 값 쌍을 가진 작은 맵을 만드는 경우 32 | - 일반적인 Map.of 사용 33 | 2. 10개 이상의 데이터를 가지는 맵을 만드는 경우 34 | - Map.Entry 객체를 인수로 받으며 가변 인수로 구현된 Map.ofEntries 팩터리 메서드를 이용하는 것이 좋다. 35 | 36 | ```java 37 | // 1번 38 | Map ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26); 39 | 40 | // 2번 41 | import static java.util.Map.entry; 42 | Map ageOfFriends2 = Map.ofEntries(entry("Raphael", 30), entry("Olivia", 25), entry("Thibaut", 26)); 43 | ``` 44 | 45 | Map.entry는 Map.Entry 객체를 만드는 새로운 팩터리 메서드이다. 46 | 47 | ## Object.equals(E1, E2) 48 | -------------------------------------------------------------------------------- /09장(리팩터링, 테스팅, 디버깅).md: -------------------------------------------------------------------------------- 1 | # 리팩터링, 테스팅, 디버깅 2 | 3 | ## 익명클래스와 람다 4 | 5 | - 익명 클래스에서 사용한 this는 자신을 가리키지만, 람다에서 this는 람다를 감싸는 클래스를 가리킨다. 6 | - 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다.(섀도 변수, shadow variable) 7 | 8 | ```java 9 | int a = 10; 10 | Runnable r1 = () -> {int a = 2; // 컴파일 에러 System.out.println(a);}; 11 | 12 | Runnable r2 = () new Runnable() { 13 | public void run() { 14 | int a = 2; // 모든 것이 잘 작동한다. 15 | System.out.println(a); 16 | } 17 | } 18 | ``` 19 | 20 | ## 람다 표현식을 메서드 참조로 리팩터링하기 21 | 22 | ```java 23 | Map> dishesByCaloricLevel = 24 | menu.stream() 25 | .collect(groupingBy(dish -> { 26 | if(dish.getCalories() <= 400) return CaloricLevel.DIET; 27 | else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL; 28 | else return CaloricLevel.FAT; 29 | })); 30 | ``` 31 | 32 | 위 코드에서 람다 표현식을 메서드로 따로 뺀 후 메서드 참조를 이용할 수 있다. 33 | 34 | ```java 35 | Map> dishesByCaloricLevel = 36 | menu.stream() 37 | .collect(groupingBy(Dish::getCaloricLevel)); 38 | 39 | public Dish { 40 | ... 41 | public CaloricLevel getCaloricLevel() { 42 | if(... 43 | else if (... 44 | else ... 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /10장(람다를 이용한 도메인 전용 언어).md: -------------------------------------------------------------------------------- 1 | # 람다를 이용한 도메인 전용 언어(domain-specific languages, DSL) 2 | 3 | DSL은 특정 비즈니스 도메인의 문제를 해결하려고 만든 언어이다. DSL은 특정 비즈니스 도메인을 인터페이스로 만든 API라고 생각할 수 있다. 4 | 5 | > DSL의 핵심은 우리의 코드의 의도가 명확히 전달되어야 하며, 동료가 쉽게 이해할 수 있도록 코드를 구현해야 한다. 6 | 7 | - 장점 8 | - 간결함 9 | - 가독성 10 | - 유지보수의 편리성 11 | - 높은 수준의 추상화 12 | - 집중 13 | - 관심사 분리(Sparation of concerns) 14 | - 단점 15 | - DSL 설계의 어려움 16 | - 개발 비용 17 | - 추가 우회 계층 18 | - 새로 배워야 하는 언어 19 | - 호스팅 언어 한계 20 | -------------------------------------------------------------------------------- /11장(null 대신 Optional 클래스).md: -------------------------------------------------------------------------------- 1 | # null 대신 Optional 클래스 2 | 3 | ```java 4 | public String getCarInsuranceName(Person person) { 5 | return person.getCar().getInsurance().getName(); 6 | } 7 | ``` 8 | 9 | 위 코드의 문제는 NullPointerException이 발생할 수 있다는 것이다. 10 | 11 | NullPointerException을 피하려면 필요한 곳에 다양한 null 확인 코드를 추가할 것인데, 아래처럼 반복 패턴 코드를 `깊은 의심(deep doubt)`이라고 부른다. 12 | 13 | ```java 14 | public String getCarInsuranceName(Person person) { 15 | if(person != null) { 16 | Car car = person.getCar(); 17 | if(car != null) } 18 | Insurance insuracne = car.getInsurance(); 19 | if(insurance != null) { 20 | return insurance.getName(); 21 | } 22 | } 23 | } 24 | return "Unknown"; 25 | } 26 | ``` 27 | 28 | ## null 때문에 발생하는 문제 29 | 30 | 1. 에러의 근원이다. 31 | 2. 코드를 어지럽힌다. 32 | 3. 아무 의미가 없다 33 | 4. 자바 철학에 위배된다. 34 | 5. 형식 시스템에 구멍을 만든다. 35 | 36 | ## Optional 클래스 소개 37 | 38 | `java.util.Optional` 라는 클래스를 제공, Optional은 값이 있다면 값을 감싸고, 없으면 Optional.empty 메서드로 Optional을 반환한다. 39 | 40 | Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드이다. 41 | 42 | ```java 43 | public class Person { 44 | private Optional car; // 사람이 차를 소유했을 수도 소유하지 않았을 수도 있다. 45 | public Optional getCar() { 46 | return car; 47 | } 48 | } 49 | ``` 50 | NPE(NullPointerException)을 피하기 위해서 모든 객체에 Optional을 써도 되지 않을까? 라고 생각이 들수 도 있다. 51 | 하지만 어떤 변수나 객체가 무조건 null이 아니어야 하는 객체를 Optional로 감싸게되면 고쳐야하는 문제를 감추는 꼴이 되어버리기 때문에 52 | semeantic이 명확하지 않아진다. 53 | 54 | ### null이 아닌 값으로 Optional 만들기 55 | 56 | 정적 팩터리 메서드 `Optional.of`로 null이 아닌 값을 포함하는 Optiaonl을 만들 수 있다. 57 | 58 | ```java 59 | Optional optCar = Optional.of(car); 60 | ``` 61 | 62 | car가 null이라면 NPE가 발생한다. 만약 Optional을 사용하지 않았다면, Car 프로퍼티에 접근하려 할 때 에러가 발생했을 것이다. 63 | 64 | ### null값으로 Optional 만들기 65 | 66 | 정적 팩터리 메서드 Optional.ofNullable로 null 저장할 수 있는 Optional을 만들 수 있다. 67 | 68 | ```java 69 | Optioanl optCar = Optional.ofNullable(car); 70 | ``` 71 | 72 | ### 맵으로 Optional 값을 추출하고 변환하기 73 | 74 | ```java 75 | String name = null; 76 | if(insurance != null) { 77 | name = insurance.getName(); 78 | } 79 | ``` 80 | 81 | 위와 같은 유형의 패턴에서는 Optional은 map을 지원한다. 82 | 83 | ```java 84 | Optional optInsurace = Optioanl.ofNullable(insuracne); 85 | Optional name = opInsurance.map(Insurance::getName); 86 | ``` 87 | 88 | 만약 아래와 같이 여러 메서드를 호출할 때는 어떻게 변환해야 할까? 89 | 90 | ```java 91 | public String getCarInsuranceName(Person person) { 92 | return person.getCar().getInsuracne.getName(); 93 | } 94 | ``` 95 | 96 | ```java 97 | public String getCarInsuarnceName(Optional person) { 98 | return person.flatMap(Person::getCar) 99 | .flatMap(Car::getInsurance) 100 | .map(Insurance::getName) 101 | .orElse("Unknown"); // optional이 비어있으면 기본값 사용 102 | } 103 | ``` 104 | 105 | ### orElse와 isPresent 106 | 107 | orElse는 값이 없는 경우 디폴트 값을 반환할 수 있으며, isPresent는 Optional이 값을 포함하는지 여부를 알려준다. 108 | 109 | ## 자바9 Optional 스트림 조작 110 | 111 | 자바 9에서는 Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional에 stream() 메서드를 추가했다. 112 | 113 | ## 예외와 Optional 클래스 114 | 115 | ```java 116 | public static Optional stringToInt(String s) { 117 | try { 118 | return Optional.of(Integer.parseInt(s)); 119 | } catch(NumberFormatException e) { 120 | return Optional.empty(); 121 | } 122 | } 123 | ``` 124 | 125 | ## Optional Code 126 | 127 | ```java 128 | private Product findProductById(Long productId) { 129 | return Optional.ofNullable(productRepository.findOne(productId) 130 | .orElseThrow(() -> new RuntimeException("해당 제품 ID 는 없는 ID 입니다."); 131 | } 132 | ``` 133 | 134 | ```java 135 | Optional objectOptional = Optional.empty(); 136 | 137 | Object object1 = objectOptional.orElse(new Object()); 138 | Object object2 = objectOptional.orElseGet(() -> new Object()); 139 | Object object3 = objectOptional.orElseGet(Object::new); 140 | ``` 141 | 142 | ```java 143 | /** 144 | * of 를 사용하는 경우는 null 이 올 수 없다는 것을 명시하는 것이기 때문에 145 | * get 으로 꺼내쓰면된다. 146 | */ 147 | public User findUsers() { 148 | Optional userOpt = Optional.of(userRepository.findUsers()); 149 | return userOpt.get(); 150 | } 151 | ``` 152 | 153 | ```java 154 | Optional userVo = Optional.ofNullable(findUserById(employeeVo)); 155 | Employee employee = new Employee(); 156 | employee.setName(userVo.map(UserVo::getName).orElseGet(() -> "")); 157 | // employee.setName(userVo.map(UserVo::getName) 158 | .orElseThrow(() -> new RuntimeException("ID 에 해당하는 유저가 존재하지 않습니다."))); 159 | ``` 160 | 161 | ### ifPresent 162 | 163 | ```java 164 | Member member = memberRepository.findById(id); 165 | if (member != null) { 166 | if (member.isAdmin()) { 167 | member.addAdminPermissions(); 168 | } else { 169 | member.addDefaultPermissions(); 170 | } 171 | } 172 | ``` 173 | 174 | null 체크 하는 로직을 optional 을 통해 없애보자. 175 | 176 | 177 | ```java 178 | Optional memberOptional = memberRepository.findById(id); 179 | memberOptional.ifPresent(member -> { 180 | if (member.isAdmin()) { 181 | member.addAdminPermissions(); 182 | } else { 183 | member.addDefaultPermissions(); 184 | } 185 | }); 186 | ``` 187 | 188 | null을 확인하던 if 문 대신에 ifPresent 함수를 호출하면서 그 안에 함수를 제공했다. 값이 존재하는 경우에 그 안에 있는 내용을 실행한다고 읽을 수 있으니 null 을 확인하는 if 문을 사용했던 첫번째 예제에 비해 코드량도 조금 줄어들고 가독성도 좋아졌다. 189 | 190 | ## References. 191 | 192 | > http://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/ 193 | -------------------------------------------------------------------------------- /12장(새로운 날짜와 시간 API).md: -------------------------------------------------------------------------------- 1 | # 새로운 날짜와 시간 API 2 | 3 | ## LocalDate와 LocalTime 4 | 5 | ```java 6 | LocalDate date = LocalDate.of(2020, 2, 11); 7 | int year = date.getYear(); // 2020 8 | Month month = date.getMonth9); // SEPTEMBER 9 | DayOfWeek dow = date.getDayOfWeek(); // THURSDAY 10 | int len = date.lengthOfMonth(); // 31 11 | boolean leap = date.isLeapYear(); // false 12 | ``` 13 | 14 | ```java 15 | LocalTime time = LocalTime.of(13, 45, 20); 16 | int hour = time.getHour(); 17 | int minute time.getMinute(); 18 | int second = time.getSecond(); 19 | ``` 20 | 21 | ## LocalDateTime 22 | 23 | LocalDateTime은 LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스이다. 24 | 25 | ## 날짜 조정, 파싱, 포매팅 26 | 27 | - 절대적 방식 28 | 29 | ```java 30 | LocalDate date1 = LocalDate.of(2020, 2, 11); 31 | LocalDate date2 = date1.withYear(2019); // 2019-02-11 32 | ``` 33 | 34 | - 상대적 방식 35 | 36 | ```java 37 | LocalDate date1 = LocalDate.of(2020, 2, 11); 38 | LocalDate date2 = date1.plusWeeks(1); // 2020-02-18 39 | ``` 40 | -------------------------------------------------------------------------------- /13장(디폴트 메서드).md: -------------------------------------------------------------------------------- 1 | # 디폴트 메서드 2 | 3 | 자바8은 기본 구현을 포함하는 인터페이스를 정의하는 두 가지 방법을 제공한다. 4 | 5 | - 정적 메서드(static method) 6 | - 인터페이스 내부에 정적 메서드 사용 7 | - 디폴트 메서드(default method) 8 | - 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드 사용 9 | 10 | 즉, 자바8 에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있다. 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 11 | 메서드의 디폴트 메서드를 상속받게 된다. 이렇게 하면 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다. 12 | 13 | > 디폴트 메서드는 `다중 상속 동작` 이라는 유연성을 제공하면서 프로그램 구성에도 도움을 준다. 14 | 15 | - 특징 16 | - 디폴트 메서드는 `default` 키워드로 시작한다. 17 | - 메서드 바디를 포함한다 `{}` 18 | 19 | ```java 20 | public interface Sized { 21 | int size(); 22 | default boolean isEmpty() { 23 | return size() == 0; 24 | } 25 | } 26 | ``` 27 | 28 | ## 동작 다중상속 29 | 30 | - 클래스는 한 개만 상속 받을 수 있지만, 인터페이스는 여러 개 구현할 수 있다. 31 | 32 | ```java 33 | public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable {} 34 | ``` 35 | -------------------------------------------------------------------------------- /14장(자바 모듈 시스템).md: -------------------------------------------------------------------------------- 1 | # 자바 모듈 시스템 2 | 3 | 자바 모듈 시스템은 자바 9의 새로운 기능이다. 4 | 5 | ## 관심사분리(SoC, Separation of concerns) 6 | 7 | 관심사분리는 컴퓨터 프로그램을 고유의 기능으로 나누는 동작을 권장하는 원칙이다. 8 | -------------------------------------------------------------------------------- /Collector 인터페이스.md: -------------------------------------------------------------------------------- 1 | ## Collector 인터페이스 2 | 3 | ```java 4 | public interface Collector { 5 | Supplier supplier(); 6 | BiConsumer accumulator(); 7 | Function finisher(); 8 | BinaryOperator combiner(); 9 | Set characteristics(); 10 | } 11 | ``` 12 | 13 | - T는 수집될 스트림 항목의 제네릭 형식 14 | - A는 누적자, 즉 수집과정에서 중간 결과를 누적하는 객체의 형식 15 | - R은 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 보통 컬렉션 형식이다.) 16 | 17 | 18 | Collector 인터페이스를 이용해서 `커스텀 컬렉터`를 개발할 수 있다. (자세한건 책 232p 참고) 19 | 20 | - 장점 : 성능이 뛰어남(약 32퍼센트가량 상승) 21 | - 단점 : 가독성과 재사용성이 22 | -------------------------------------------------------------------------------- /Collectors 클래스의 정적 팩토리 메서드.md: -------------------------------------------------------------------------------- 1 | ## Collectors 클래스의 정적 팩토리 메서드 2 | 3 | > collect(Collectors.메서드명)과 같이 사용 4 | 5 | - `toList()` 6 | - return List 7 | - 스트림의 모든 항목을 리스트로 수집 8 | - List dishes = menu.stream().collect(toList()); 9 | 10 | - `toSet()` 11 | - return Set 12 | - 스트림의 모든 항목을 중복이 없는 집합으로 수집 13 | - Set dishes = menu.stream().collect(toSet()); 14 | 15 | - `toCollection()` 16 | - return Collection 17 | - 스트림의 모든 항목을 발행자가 제공하는 컬렉션으로 수집 18 | - Collection dishes = menu.stream().collect(toCollection(), ArrayList::new); 19 | 20 | - `counting()` 21 | - return Long 22 | - 스트림의 항목 수 계산 23 | - long howManyDishes = menu.stream().collect(counting()); 24 | 25 | - `summingInt()` 26 | - return Integer 27 | - 스트림의 항목에서 정수 프로퍼티 값을 더함 28 | - int totalCalories = menu.stream().collect(summingInt(Dish::getCalories)); 29 | 30 | - `averagingInt()` 31 | - return Double 32 | - 스트림 항목의 정수 프로퍼티의 평균값 계산 33 | - double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories)); 34 | 35 | - `summarizingInt()` 36 | - return IntSummaryStatistics 37 | - 스트림 내 항목의 최댓값, 최솟값, 합계, 평균 등의 정수 정보 통계 수집 38 | - IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories)); 39 | 40 | - `joining()` 41 | - return String 42 | - 스트림의 각 항목에 toString() 메서드를 호출한 결과 문자열 연결 43 | - String shortMenu = menuStream.map(Dish::getName).collect(joining(", ")); 44 | 45 | - `maxBy(), minBy()` 46 | - return Optional 47 | - 주어진 비교자를 이용해서 스트림의 최댓값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty() 반환 48 | - Optional fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories))); 49 | 50 | - `reducing()` 51 | - 누적자를 초기값으로 설정한 다음에 BinaryOperator로 스트림의 각 요소를 반복적으로 누적자와 합쳐 스트림을 하나의 값으로 리듀싱 52 | - int totalCalories = menuStream.collect(reducing(0, Dish::getCalories, Integer::sum)); 53 | 54 | - `collectingAndThen()` 55 | - 다른 컬렉터를 감싸고 그 결과에 변환 함수 적용 56 | - int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size)); 57 | 58 | - `groupingBy()` 59 | - return Map> 60 | - 하나의 프로퍼티 값을 기준으로 스트림의 항목을 그룹화 하며 기준 프로퍼티 값을 맵의 키로 사용 61 | - Map> dishesByType = menuStream.collect(groupingBy(Dish::getType)); 62 | 63 | - `partitioningBy()` 64 | - return Map> 65 | - 프레디케이트를 스트림의 각 항목에 적용한 결과로 항목 분할 66 | - Map> vegetarianDishes = menu.stream().collect(partitioningBy(Dish::isVegetarain)); 67 | 68 | 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 모던 자바 인 액션 2 | 3 | 모던 자바 인 액션(람다, 스트림, 함수형, 리액티브 프로그래밍으로 새로워진 자바 마스터하기) 4 | 5 | - 출판사 : 한빛미디어 6 | - 지은이 : 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트 7 | - 옮긴이 : 우정은 8 | - 시작일 : 2019-12-10 9 | - 종료일 : 2020-02-28 10 | -------------------------------------------------------------------------------- /Sample.java: -------------------------------------------------------------------------------- 1 | package com.bjh; 2 | 3 | import com.bjh.chapter4.dish.DishVo; 4 | import com.bjh.chapter4.dish.Type; 5 | import com.bjh.chapter5.Trader; 6 | import com.bjh.chapter5.Transaction; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.stream.IntStream; 12 | import java.util.stream.Stream; 13 | 14 | import static java.util.Comparator.comparing; 15 | import static java.util.stream.Collectors.*; 16 | 17 | public class Main { 18 | 19 | public static void main(String[] args) { 20 | 21 | /** 22 | * 필터 : filter 23 | * 변형 : map 24 | * 변환 : collect, reduce 25 | * 버킷(bucket) : groupingBy 26 | */ 27 | 28 | /** 29 | * 데이터 소스 : 요리 메뉴 30 | * 연속된 요소 : 내부 리스트 객체 31 | * */ 32 | List menu = Arrays.asList( 33 | new DishVo("pork", false, 800, Type.MEAT), 34 | new DishVo("beef", false, 700, Type.MEAT), 35 | new DishVo("chicken", false, 400, Type.MEAT), 36 | new DishVo("fries", true, 500, Type.OTHER), 37 | new DishVo("rice", true, 350, Type.OTHER), 38 | new DishVo("fruit", true, 120, Type.OTHER), 39 | new DishVo("pizza", false, 600, Type.OTHER), 40 | new DishVo("prawns", false, 300, Type.FISH), 41 | new DishVo("salmon", false, 450, Type.FISH) 42 | ); 43 | 44 | /** 45 | * 매핑(Mapping) 46 | * map 은 함수를 적용한 결과가 새로운 버전을 만든다. 47 | */ 48 | List dishNames = menu.stream() 49 | .map(DishVo::getName) 50 | .collect(toList()); 51 | 52 | /** 53 | * 스트림으로 데이터 처리 54 | * filter, map, limit, collect 데이터 처리 연산 55 | * - pipeline 56 | * collect 를 제외한 처리 연산들 57 | * filter, map, limit은 스트림을 반환하지만, collect는 리스트를 반환한다. 58 | */ 59 | List threeHighCaloricDishNames = 60 | menu.stream() // 메뉴에서 스트림을 얻는다. 61 | // 파이프라인 연산 만들기 62 | .filter(dishVo -> dishVo.getCalories() > 300) // 고칼로리 요리 필터링 63 | .map(DishVo::getName) // 요리명 추출 64 | .limit(3) // 선착순 3개만 선택 65 | .collect(toList()); // 결과를 다른 리스트로 저장 66 | 67 | System.out.println(threeHighCaloricDishNames); 68 | 69 | Trader raoul = new Trader("Raoul", "Cambridge"); 70 | Trader mario = new Trader("Mario", "Milan"); 71 | Trader alan = new Trader("Alan", "Cambridge"); 72 | Trader brian = new Trader("Brian", "Cambridge"); 73 | 74 | List transactions = Arrays.asList( 75 | new Transaction(brian, 2011, 300), 76 | new Transaction(raoul, 2012, 1000), 77 | new Transaction(raoul, 2011, 400), 78 | new Transaction(mario, 2012, 710), 79 | new Transaction(mario, 2012, 700), 80 | new Transaction(alan, 2012, 950) 81 | ); 82 | 83 | /** 2011년에 일어난 모든 트랜잭션을 찾아 값을 오름차순으로 정리하시오. */ 84 | List transaction2011s = transactions.stream() 85 | .filter(transaction -> transaction.getYear() == 2011) 86 | .sorted(comparing(Transaction::getValue)) 87 | .collect(toList()); 88 | 89 | /** 거래자가 근무하는 모든 도시를 중복 없이 나열하시오. */ 90 | List workingCitys = transactions.stream() 91 | .map(transaction -> transaction.getTrader().getCity()) 92 | .distinct() 93 | .collect(toList()); 94 | 95 | /** 케임브리지에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오. */ 96 | List workers = transactions.stream() 97 | .map(Transaction::getTrader) 98 | .filter(trader -> trader.getCity().equals("Cambridge")) 99 | .distinct() 100 | .sorted(comparing(Trader::getName)) 101 | .collect(toList()); 102 | 103 | /** 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오. */ 104 | String names = transactions.stream() 105 | .map(transaction -> transaction.getTrader().getName()) 106 | .distinct() 107 | .sorted() 108 | .collect(joining()); 109 | 110 | /** 밀라노에 거래자가 있는가? */ 111 | boolean isMilanBased = transactions.stream() 112 | .anyMatch(transaction -> transaction.getTrader() 113 | .getCity() 114 | .equals("Milan")); 115 | 116 | /** 케임브리지에 거주하는 거래자의 모든 트랜잭션값 추출 */ 117 | transactions.stream() 118 | .filter(t -> "Cambridge".equals(t.getTrader().getCity())) 119 | .map(Transaction::getValue) 120 | .forEach(System.out::println); 121 | 122 | /** 전체 트랜잭션중 최댓값은? */ 123 | Optional highestValue = transactions.stream() 124 | .map(Transaction::getValue) 125 | .reduce(Integer::max); 126 | 127 | /** 리스트의 모든 짝수를 선택하고 중복을 필터링 */ 128 | List numbers = Arrays.asList(1,2,1,3,3,2,4); 129 | numbers.stream() 130 | .filter(i -> i % 2 == 0) 131 | .distinct() 132 | .forEach(System.out::println); 133 | 134 | /** 135 | * 스트림 슬라이싱(Stream slicing) 136 | * takeWhile과 dropWhile로 조건에 벗어나면 반복 작업 중단 137 | * filter의 경우는 조건을 찾아도 끝까지 반복한다. 138 | * 자바 9 이상 사용 가능 139 | */ 140 | List specialMenu = Arrays.asList( 141 | new DishVo("seasonal fruit", true, 12, Type.OTHER), 142 | new DishVo("prawns", false, 300, Type.FISH), 143 | new DishVo("rice", true, 350, Type.OTHER) 144 | ); 145 | List sliceMenu1 = specialMenu.stream() 146 | .takeWhile(dish -> dish.getCalories() < 320) // 320보다 작은 것들 선택 147 | .collect(toList()); 148 | List sliceMenu2 = specialMenu.stream() 149 | .dropWhile(dish -> dish.getCalories() < 320) // 320보다 큰 것들 선택 150 | .collect(toList()); 151 | 152 | /** 153 | * flatMap 154 | * flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, 하나의 평면화된 스트림을 반환한다. 155 | */ 156 | List uniqueCharacters = 157 | words.stream() 158 | .map(word -> word.splict("")) // 각 단어를 개별 문자를 포함하는 배열로 반환 159 | .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화 160 | .distinct() 161 | .collect(toList()); 162 | 163 | /** 두 개의 숫자 리스트 1,2,3 과 3,4 가 있을때 모든 숫자 쌍의 리스트를 반환 하시오 (1,3) (1,4) (2,3) ... */ 164 | List numbers1 = Arrays.asList(1, 2, 3); 165 | List numbers2 = Arrays.asList(3, 4); 166 | List pairs = numbers1.stream() 167 | .flatMap(i -> numbers2.stream().map(j -> new int[]{i, j})) 168 | .collect(toList()); 169 | 170 | 171 | /** 172 | * 검색과 매칭 173 | * anyMach : 프레디케이트가 적어도 한 요소와 일치하는지 확인 174 | * allMach : 프레디케이트가 모든 요소와 일치하는지 검사 175 | * noneMatch : 프레디케이트가 모든 요소와 일치하지 않는 경우 검사 176 | */ 177 | if(menu.stream().anyMatch(DishVo::isVegetarian)) { 178 | System.out.println("The menu is somewhat vegetarian friendly!!"); 179 | } 180 | 181 | boolean isHealthy1 = menu.stream().allMatch(dish -> dish.getCalories() < 1000); 182 | boolean isHealthy2 = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000); 183 | 184 | /** 185 | * 쇼트 서킷(short circuit) 186 | * 예를 들어 여러 and 연산으로 연결된 커다란 불리언 표현식을 평가한다고 가정하자. 187 | * 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이 전체 결과도 거짓이 된다. 188 | * 이러한 상황을 쇼트서킷이라고 부른다. 189 | * allMatch, noneMatch, findFirst, findAny, limit 등의 연산은 스트림의 모든 요소를 처리하지 않고 반환할 수 있다. 190 | * 즉, 원하는 요소를 찾고 즉시 결과를 반환할 수 있다. 191 | */ 192 | Optional dish = menu.stream() 193 | .filter(DishVo::isVegetarian) 194 | .findAny(); // 임의의 요소 반환 195 | 196 | List someNumbers = Arrays.asList(1,2,3,4,5); 197 | Optional firstSquareDivisibleByThree = someNumbers.stream() 198 | .map(n -> n * n) 199 | .filter(n -> n % 3 == 0) 200 | .findFirst(); // 첫 번째 요소 찾기 201 | 202 | /** 203 | * 리듀싱 연산(fold) 204 | * reduce는 두 개의 인수를 갖는다. 205 | * 1. 초기값 206 | * 2. 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator 207 | */ 208 | int sum = numbers.stream().reduce(0, (a,b) -> a+b); 209 | 210 | /** 211 | * 숫자리스트 : 8,4,1,9,7,5 212 | * 위 처럼 되어있을 경우 초기값 0이 a의 자리에 들어가고, b에는 8이 들어간다. 213 | * 누적값이 8이 되었다. 214 | * 누적값 8이 a에 들어가고, b에는 4가 들어간다. 215 | * 누적값이 12가 되었다. 216 | * 누적값 12가 a에 들어가고, b에는 1이 들어간다. 217 | * 누적값이 13이 되었다. 218 | * 반복... 219 | */ 220 | int sum2 = numbers.stream().reduce(0, (a, b) -> a + b); 221 | 222 | /** 223 | * 초기값을 받지 않도록 오버로드된 reduce도 있다. 224 | * 이 reduce는 Optional 객체를 반환한다. 225 | */ 226 | Optional sum3 = numbers.stream().reduce((a, b) -> (a + b)); 227 | 228 | /** 229 | * 맵 리듀스 패턴(map-reduce pattern) 230 | * 구글이 웹 검색 엔진에 적용한 사례 231 | */ 232 | int count = menu.stream() 233 | .map(d -> 1) 234 | .reduce(0, (a,b) -> a + b); 235 | 236 | /** 237 | * 기본형 특화 스트림(primitive stream specialization) 238 | * Integer::sum 과 같은 경우 박싱 비용이 든다. 239 | * 박싱 비용을 피하기 위해서 스트림 API는 기본형 특화 스트림을 제공한다. 240 | * IntStream, DoubleStream, LongStream 241 | * mapToInt, mapToDouble, mapToLong 242 | */ 243 | int calories = menu.stream() 244 | .mapToInt(DishVo::getCalories) 245 | .sum(); 246 | 247 | /** 248 | * 객체 스트림으로 복원 249 | */ 250 | IntStream intStream = menu.stream().mapToInt(DishVo::getCalories); // 스트림을 숫자 스트림으로 변환 251 | Stream stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환 252 | 253 | /** 254 | * rangeClose(1, 100)은 1과 100을 포함 255 | * range(1, 100)은 1과 100을 제외 256 | */ 257 | IntStream evenNumbers = IntStream.rangeClosed(1, 100) 258 | .filter(n -> n % 2 == 0); 259 | System.out.println(evenNumbers.count()); 260 | 261 | /** 262 | * 피보나치수열 263 | * 0, 1, 1, 2, 3, 5, 8 ... 264 | */ 265 | Stream.iterate(new int[]{0, 1}, t-> new int[] {t[1], t[0] + t[1]}) 266 | .limit(10) 267 | .map(t -> t[0]) 268 | .forEach(System.out::println); 269 | 270 | /** 271 | * Collectors.reducing 272 | * reducing은 세 개의 인수를 받는다. 273 | * 1. 초기값 274 | * 2. 합계 함수 275 | * 3. 변환 함수 (세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator이다.) 276 | * */ 277 | 278 | /** 279 | * 한 개의 인수를 갖는 reducing 280 | * 두 번째 인수에서 자기 자신을 그대로 반환하는 항등함수(identity function)을 받는다. 281 | */ 282 | Optional mostCaloireDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2)); 283 | 284 | /** 각 요리를 요리명으로 변환 후 요리명 연결 joining */ 285 | String joiningMenu = menu.stream() 286 | .map(DishVo::getName) 287 | .collect(joining()); 288 | 289 | /** 각 요리를 요리명으로 변환 후 문자열을 누적자로 사용하여 요리명 연결 reducing */ 290 | String reducingMenu = menu.stream() 291 | .map(DishVo::getName) 292 | .collect(reducing((s1, s2) -> s1 + s2)) 293 | .get(); 294 | 295 | String reducingMenu2 = menu.stream() 296 | .collect(reducing("", DishVo::getName, (s1, s2) -> s1 + s2)); 297 | 298 | List treeCategories = categoryRepository.findTreeCategories(parent).stream() 299 | .filter(category -> categorySeq.equals(category.getCategorySeq())) 300 | .map(category -> category.getDepthFullName().split(">")) 301 | .collect(toList()); 302 | 303 | /** 304 | * 그룹화 groupingBy 305 | * 생선, 고기 그 밖의 것들로 그룹화 306 | */ 307 | Map> dishesByType = menu.stream().collect(groupingBy(Dish::getType)); 308 | 309 | /** 310 | * 칼로리별로 그룹화 311 | */ 312 | 313 | Map> dishesByCaloricLevel = menu.stream().collect( 314 | groupingBy(dish -> { 315 | if(dish.getCalories() <= 400) return CaloricLevel.DIET; 316 | else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL; 317 | else return CaloricLevel.FAT; 318 | })); 319 | 320 | // 500칼로리가 넘는 요리만 타입과 종류로 그룹화 321 | Map> caloricDishesByType = menu.stream().filter(dish -> dish.getCalroies() > 500) 322 | .collect(groupingBy(Dish::getType)); 323 | /** 324 | * 결과 325 | * {OTHER=[french fries, pizza], MEAT=[pork, beef]} 326 | * 위 코드의 단점은 위 filter 프레디케이트를 만족하는 값이 없을 경우 키값 자체가 제외되서 맵에 담지 못한다. 327 | * 해결책 : Collectors 클래스의 정적 팩터리 메서드인 filtering 사용 328 | */ 329 | 330 | // 해결 331 | Map> caloricDishesByType = menu.stream().collect(groupingBy 332 | (Dish::getType, filtering(dish -> dish.getCalories() > 500, toList()))); 333 | // 결과 : {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]} 334 | 335 | /** mapping 사용 */ 336 | Map> dishNamesByType = menu.stream(). 337 | collect(groupingBy(Dish::getType, mapping(Dish::getName, toList()))); 338 | 339 | /** flatMapping 사용 340 | * flatMap은 두 수준의 리스트를 한 수준으로 평면화 할 수 있음 341 | */ 342 | Map> dishNamesByType = 343 | menu.stream() 344 | .collect(groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet()))); 345 | 346 | /** 347 | * 다수준 그룹화 groupingBy 두 번 348 | */ 349 | Map>> dishesByTypeCaloricLevel = 350 | menu.stream().collect( 351 | groupingBy(Dish::getType, 352 | groupingBy(dish -> 353 | if(dish.getCalories() <= 400) 354 | return CaloricLevel.DIET; 355 | else if(dish.getCalories() <= 700) 356 | return CaloricLevel.NORMAL; 357 | else return CaloricLevel.FAT; 358 | }) 359 | ) 360 | ); 361 | /** 362 | * 결과 363 | * {MEAT={DIET=[chicken], MORMAL=[beef], FAT=[port]}, ... 364 | */ 365 | } 366 | 367 | /** 368 | * 회사 프로세스 369 | * 어떤 이름 목록에서, 한 글자로 된 이름을 제외한 모든 이름을 대문자화 해서 쉼표로 연결한 문자열 구현 370 | */ 371 | public String cleanNames(List names) { 372 | if(names == null) return ""; 373 | return names 374 | .stream() 375 | .filter(name -> name.length() > 1) 376 | .map(name -> capitalize(name)) 377 | .collect(joining(",")); 378 | } 379 | 380 | private String capitalize(String e) { 381 | return e.substring(0,1).toUpperCase() + e.substring(1, e.length()); 382 | } 383 | 384 | 385 | /** 386 | * grouping 1개 387 | */ 388 | List