JANGUN


Java 8 in Action


지음 : 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트
옮김 :우정은



Contents

Part I 기초
Chapter 1 자바8을 눈여겨봐야 하는 이유
Chapter 2 동작 파라미터화 코드
Chapter 3 람다 표현식
Part II 함수형 데이터 처리
Chapter 4 스트림 소개
Chapter 5 스트림 활용
Chapter 6 스트림으로 데이터 수집
Chapter 7 병렬 데이터 처리와 성능
Part III 효과적인 자바 8 프로그래밍
Chapter 8 리팩토링, 테스팅, 디버깅
Chapter 9 디폴트 메서드
Chapter 10 null 대신 Optional
Chapter 11 CompletableFuture: 조합할 수 있는 비동기 프로그래밍
Chapter 12 새로운 날짜와 시간 API
Part IV 자바 8의 한계를 넘어서
Chapter 13 함수형 관점으로 생각하기
Chapter 14 함수형 프로그래밍 기법
Chapter 15 OOP와 FP의 조화: 자바 8과 스칼라 비교
Chapter 16 결론 그리고 자바의 미래


Chapter 1. 자바 8을 눈여겨봐야 하는 이유

Java 8 : 2014년 3월 발표
- 스트림 API
- 메서드에 코드를 전달하는 기법: 동작 파라미터화
- 인터페이스의 디폴트 메서드

스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 이론적으로 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있다. 스트림 API의 핵심은, 기존에는 한 번에 한 항목을 처리했지만 이제는 우리가 하려는 작업을 (데이터베이스 질의처럼) 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다. 또한 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다.

동작 파라미터화는 메서드를 다른 메서드의 인수로 넘겨주는 기능이다.

공유되지 않은 가변 데이터, 메서드와 함수 코드를 다른 메서드로 전달하는 두 가지 기능은 함수형 프로그래밍 패러다임의 핵심적인 사항이다. 반면 명령형 프로그래밍 패러다임에서는 일련의 가변 상태로 프로그램을 정의한다. 공유되지 않은 가변 데이터 요구사항을 준수하는 메서드는 인수를 결과로 변환하는 동작만 수행한다. 즉, 수학적인 함수처럼 정해진 기능만 수행하며 다른 부작용은 일으키지 않는다.

프로그래밍 언어에서 함수라는 용어는 메서드 특히 정적 메서드와 같은 의미로 사용된다. 자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.

자바 8의 메서드 레퍼런스 (‘::’ - 이 메서드를 값으로 사용하라는 의미)를 이용해서 메서드를 인수로 전달할 수 있다.
자바 8에서는 람다(익명 함수)를 포함하여 함수도 값으로 취급할 수 있다.

컬렉션에서는 반복 과정을 직접 처리해야 했다. 즉, for-each 루프를 이용해서 각 요소를 반복하면서 작업을 수행했다. 이런 방식의 반복을 외부반복이라고 한다. 반면 스트림 API를 이용하면 루프를 신경 쓸 필요가 없다. 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다. 이와 같은 반복을 내부 반복이라고 한다. 컬렉션은 어떻게 데이터를 저장하고 접근할 지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다.

자바 8에서는 라이브러리 설계자가 더 쉽게 변화할 수 있는 인터페이스를 만들 수 있도록 디폴트 메서드를 추가했다. 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스가 포함할 수 있는 기능을 제공한다.
자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다.



Chapter 2 동작 파라미터화 코드 전달하기

어떤 상황에서 일을 하든 소비자 요구사항은 항상 바뀐다. 변화하는 요구사항은 소프트웨어 엔지니어링에서 피할 수 없는 문제다. 동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다. 예를 들어 나중에 실행될 메서드의 인수로 코드 블록을 전달할 수 있다. 결과적으로 코드 블록에 따라 메서드의 동작이 파라미터화된다. (파라미터 = 변수로 전달)

프레디케이트(predicate) – 선택 조건을 어떤 속성에 기초해서 불린 값을 반환하는 동작
사전)
1. 술부(John went home에서 went home처럼, 문장 속에서 주어에 대해 진술하는 동사 이하 부분)
2. (특정 신조・생각・원칙에 …의) 근거를 두다, 입각하다
3. (사실이라고) 단정하다

전략 디자인 패턴은 각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 한 메서드가 다른 동작을 수행하도록 재활용할 수 있다.
익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있다.
동작 파라미터화 패턴은 동작을 (한 조각의 코드로) 캡슐화한 다음에 메서드로 전달해서 메서드의 동작을 파라미터화한다. 더 유연하고 재상용할 수 있는 코드를 만들 수 있다.
자바에서는 Runnable 인터페이스를 이용해서 실행할 코드 블록을 지정할 수 있다.



Chapter 3 람다 표현식

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.
- 익명 – 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드에 대한 걱정거리가 줄어든다.
- 함수 – 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
- 전달 – 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결성 – 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.

람다를 이용해서 간결한 방식으로 코드를 전달할 수 있다. 람다가 기술적으로 자바 8 이전의 자바로 할 수 없었던 일을 제공하는 것은 아니다. 다만 동작 파라미터를 이용할 때 익명 클래스 등 판에 박힌 코드를 구현할 필요가 없다.

람다는 세 부분으로 이루어진다.
(parameter) -> expression or (parameter) -> { statements; }
- 파라미터 리스트
- 화살표 (->) – 람다의 파라미터 리스트와 바디를 구분한다.
- 람다의 바디 – 람다의 반환 값에 해당하는 표현식이다.
(예제)
() -> { }
() -> “Raoul”
() -> { return “Mario”; }
(Integer I ) -> return “Alan”+i; // 틀림, { }로 감싸야 함 (Integer i) -> {return “Alan”+i; }
(String s) -> { “Iron Man”;} // 틀림, (String s) -> “Iron Man” 또는 (String s) -> {return “Iron Man”; }
(List<String> list) -> list.isEmpty() // 불린 표현식
() -> new Apple(10) // 객체 생성
(Apple a) -> { System.out.println(a.getWeight() ); } // 객체에서 소비
(String s ) -> s.length() // 객체에서 선택, 추출
(int a, int b) -> a*b // 두 값을 조합
(Apple a1, Apple a2) -> a1.getWeight().compareTo( a2.getWeight() ) // 두 객체 비교
함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다. 예로, Predicate<T>가 함수형 인터페이스이다. 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다.
public interface Predicate<T> {
Boolean test( T t );
}
public interface Runnable {
void run();
}
public interface Comparator<T> {
int compare(T o1, T o2);
}

함수형 인터페이스로 뭘 할 수 있을까? 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. (기술적으로 따지면 함수형 인터페이스를 concrete 구현한 클래스의 인스턴스) 함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있다.
Runnable r1 = () -> System.out.println(“Hello World 1”); // 람다 사용
Runnable r2 = new Runnable() { // 익명 클래스 사용
public void run() {
System.out.println(“Hello World 2”);
}
};
public static void process(Runnable r) {
r.run();
}
process( r1 ); // ‘Hello World 1’ 출력
process( r2 ); // ‘Hello World 2’ 출력
process( () -> System.out.println(“Hello World 3”) ); // 직접 전달된 람다 ‘Hello World 3’ 출력

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그너처를 가리킨다. 람다 표현식의 시그너처를 서술하는 메서드를 함수 디스크립터라고 부른다. 일단 람다 표현식은 변수에 할당하거나 함수형 인터페이스를 인수로 받는 메서드로 전달할 수 있으며, 함수형 인터페이스의 추상메서드와 같은 시그너처를 갖는다는 사실을 기억하자!
왜 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까?

람다 활용: 실행 어라운드 패턴
람다와 동작 파라미터화로 유연하고 간결한 코드를 구현하는 데 도움을 주는 실용적인 예제를 살펴보자.
(자바 7에 새로 추가된 try-with-resource 구문: 자원을 명시적으로 닫을 필요가 없으므로 간결한 코드 가능)
1단계: 동작 파라미터화를 기억하라 (processFile 메서드를 파라미터화)
2단계: 함수형 인터페이스를 이용해서 동작 전달
3단계: 동작 실행! (함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리)
4단계: 람다 전달
(OLD) public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(“data.txt”) ) ) {
return br.readLine(); // 실제로 필요한 작업을 하는 행 (한번에 한 줄만 읽음)
}
}
(NEW)
import java.io.*;
public class ExecuteAround {
public static void main(String ...args) throws IOException{
// method we want to refactor to make more flexible
String result = processFileLimited();
System.out.println(result);
System.out.println("---");
String oneLine = processFile((BufferedReader b) -> b.readLine());
System.out.println(oneLine);
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
System.out.println(twoLines);
}
public static String processFileLimited() throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("lambdasinaction/chap3/data.txt"))) {
return br.readLine();
}
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("lambdasinaction/chap3/data.txt"))){
return p.process(br);
}
}
public interface BufferedReaderProcessor{
public String process(BufferedReader b) throws IOException;
}
}

함수형 인터페이스는 오직 하나의 추상 메서드를 지정한다. 함수형 인터페이스의 추상 메서드는 람다 표현식의 시그너처를 묘사한다. 함수형 인터페이스의 추상 메서드 시그너처를 함수 디스크립처라고 한다.
java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불린을 반환한다. T 형식의 객체를 사용하는 불린 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있다.
java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다. 예를 들어 Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있다.
java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 apply라는 추상 메서드를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다. 예를 들면 사과의 무게 정보를 추출하거나 문자열을 길이와 매핑.

제네릭 파라미터에는 참조형만 사용할 수 있다. 자바에서는 기본형을 참조형으로 변환할 수 있는 박싱 기능을 제공한다. 참조형을 기본형으로 반환하는 언박싱이라고 한다. 박싱과 언박싱이 자동으로 이루어지는 오토 박식이라는 기능도 제공한다. 하지만 이런 변환 과정은 비용이 소모된다.
함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야 한다.
람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있다. 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있다.
람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처할 수 있다. 하지만 그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다. 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.
메서드 레퍼런스는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.
Inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
- Inventory.sort( comparing(Apple::getWeight) );
() -> Thread.currentThread().dumpStack() - Thread.currentThread()::dumpStack
(str, i) -> str.substring( i ) - String::substring
(String s) -> System.out.println(s) - System.out::println



Chapter 4 스트림 소개

모든 자바 애플리케이션은 컬렉션을 만들고 처리하는 과정을 포함한다.
스트림은 자바 API에 새로 추가된 기능으로, 스트림을 이용하면 선언형(즉, 데이터를 처리하는 임시 구현 코드 대신 질의로 표현할 수 있다)으로 컬렉션 데이터를 처리할 수 있다. 일단 스트림이 데이터 컬렉션 반복을 멋지게 처리하는 기능이라고 생각하자. 또한 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.
예제) 저칼로리의 요리명을 반환하고, 칼로리를 기준으로 요리를 정렬 – 자바7
public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes){
List<Dish> lowCaloricDishes = new ArrayList<>(); // 가비지 변수
for(Dish d: dishes){ // 누적자로 요소 필터링
if(d.getCalories() > 400){
lowCaloricDishes.add(d);
}
}
List<String> lowCaloricDishesName = new ArrayList<>();
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){ // 익명 클래스로 요리 정렬
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
for(Dish d: lowCaloricDishes){ // 정렬된 리스트를 처리, 요리 이름 선택
lowCaloricDishesName.add(d.getName());
}
return lowCaloricDishesName;
}

자바 8 코드)
public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes){
return dishes.stream()
.filter(d -> d.getCalories() > 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());
}

스트림의 기능
- 선언형으로 코드를 구현할 수 있다. 즉, 루프와 if 조건문 들의 제어 블록을 사용해서 어떻게 동작을 구현할 지 지정할 필요 없이, ‘저칼로리의 요리만 선택하라’ 같은 동작의 수행을 지정할 수 있다. 선언형 코드와 동작 파라미터화를 활용하면 변하는 요구사항에 쉽게 대응할 수 있다. 즉, 기존 코드를 복사하여 붙여 넣는 방식을 사용하지 않고 람다 표현식을 이용해서 저칼로리 대신 고칼로리의 요리만 필터링하는 코드도 쉽게 구현할 수 있다.
- Filter, sorted, map, collect 같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다.


자바 8의 스트림 API의 특징
- 선언형: 더 간결하고 가독성이 좋아진다
- 조립할 수 있음: 유연성이 좋아진다
- 병렬화: 성능이 좋아진다.

스트림이란 ‘데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소’로 정의할 수 있다. 자바 8의 컬렉션에서는 스트림을 반환하는 stream이라는 메서드가 추가되었다.
컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다. 컬렉션은 자료구조이므로 컬렉션에서는 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이룬다. 반면 스트림은 filter, sorted, map 처럼 표현 계산식이 주를 이룬다. 즉, 컬렉션의 주제는 데이터고 스트림의 주제는 계산이다.
스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다. 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그래도 유지된다. 즉, 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지한다. 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원한다. 예를 들어, filter, map, reduce, find, match, sort, collect 등으로 데이터를 조작할 수 있다.

대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. 그 덕분에 게으름, 쇼트서킷 같은 최적화도 얻을 수 있다. 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다. 파이프라인은 소스에 적용하는 질의 같은 존재다.
- filter: d -> d.getCalories() > 300 // 특정 요소를 제외시킴
- map: Dish::getName (= d -> d.getName()) // 한 요소를 다른 요소로 변환하거나 정보를 추출한다.
- Limit: 스트림 크기를 축소한다.
- collect: 스트림을 다른 형식으로 변환한다.
Import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishNames =
menu.stream() // 메뉴(요리 리스트)에서 스트림을 얻는다.
.filter( d -> d.getCalories() > 300) // 파이프라인 연산. 고칼로리 요리를 필터링
.map(Dish::getName) // 요리명 추출
.limit(3) // 선착순 세 개만 선택
.collect( toList() ); // 결과를 다른 리스트로 저장
System.out.println( threeHighCaloricDishNames );

자바의 기존 컬렉션과 새로운 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다. 데이터를 언제 계산하느냐가 컬렉션과 스트림의 가장 큰 차이라고 할 수 있다. 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조다. 즉, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다. 컬렉션은 적극적으로 생성된다. (생산자 중심: 팔기도 전에 창고를 가득 채움). 반면 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다. 결과적으로 스트림은 생산자와 소비자 관계를 형성한다. 또한 스트림은 게으르게 만들어지는 컬렉션과 같다. 즉, 사용자가 데이터를 요청할 때만 값을 계산한다. (요청 중심: 데이터를 요청할 때만 값을 계산한다. 음악이나 영화의 스트리밍 서비스처럼 다 받기 전에 재생 가능)
스트림은 단 한 번만 소비할 수 있다는 점을 명심하자.
컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 한다. (외부반복) 컬렉션은 명시적으로 컬렉션항목을 하나씩 가져와서 처리한다. 스트림 라이브러리는 (반복을 알아서 처리하고 결과 스트림 값을 어딘가에 저장해주는) 내부 반복을 사용한다. 내부 반복을 이용하면 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리할 수 있다.
List<String> names = new ArrayList<>();
for( Dish d: menu) { // 메뉴 리스트를 명시적으로 순차 반복한다.
names.add( d.getName() ); // 이름을 추출해서 리스트에 추가한다.
}

List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
While( iterator.hasNext() ) { // 명시적 반복
Dish d = iterator.next();
names.add( d.getName() );
}

List<String> names = menu.stream()
.map(Dish::getName) // map 메서드를 getName 메서드로 파라미터화해서 요리명을 추출
.collect(toList()); // 파이프라인을 실행. 반복자는 필요 없다.

스트림 연산: filter, map, limit는 서로 연결되어 파이프라인을 형성하고, collect로 파이프라인을 실행한 다음에 닫는다. 연결할 수 있는 스트림 연산을 중간 연산이라고 하면, 스트림을 닫는 연산을 최종 연산이라고 한다.
중간 연산은 다른 스트림을 반환한다. 따라서 여러 중간 연산을 연결해서 질의를 만들 수 있다. 중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 것, 즉 게으르다는 것이다. 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한 번에 처리하기 때문이다.
최종 연산은 스트림 파이프라인에서 결과를 도출한다. 보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과가 반환된다.

스트림 이용 과정은 다음과 같이 세 가지로 요약할 수 있다.
- 질의를 수행할 (컬렉션 같은) 데이터 소스
- 스트림 파이프라인을 구성할 중간 연산 연결
- 스트림 파이프라인을 실행하고 결과를 만들 최종 연산
중간 연산


최종 연산



Chapter 5 스트림 활용

스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다. 스트림 API는 내부 반복 뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 스트림 API가 지원하는 연산을 이용해서 필터링, 슬라이팅, 매핑, 검색, 매칭, 리듀싱 등 다양한 데이터 처리 질의를 표현할 수 있다.
스트림 인터페이스는 filter 메서드를 지원한다. filter 메서드는 프레디케이트(불린을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 요소를 포함하는 스트림을 반환한다.
스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct라는 메서드도 지원한다. (고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다)
스트림은 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다.
스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다.

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다. (값을 고치는 것이 아닌 새로운 버전을 만드는 변환에 가까운 개념)
특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다. 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.
메뉴의 모든 칼로리 합계를 구하시오와 같은 질의를 리듀싱 연산(모든 스트림 요소를 처리해서 값으로 도출하는)이라고 한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 ‘폴드’라고 부른다. reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다. 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵다.
요소의 합: int sum = numbers.stream().reduce(0, (a,b) -> a+b);
최대/최소 값: Optional<Integer> max = numbers.stream().reduce(Integer::max);

표) 중간 연산과 최종 연산


스트림 API 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.
Int calories = menu.stream() // Stream<Dish> 반환
.mapToInt(Dish::getCalrories) // IntStream 반환, 각 요리에서 모든 칼로리 추출
.sum;
IntStream intStream = menu.stream().mapToInt(Dish::getCalrories); // 스트림을 숫자 스트림으로 변환
Stream<Integer> stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환
Optional을 Integer, String 등의 레퍼런스 형식으로 파라미터화 할 수 있다. 또한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다.
OptionalInt maxCalrories = menu.stream().mapToInt(Dish::getCalrories).max();
Int max = maxCalrories.orElse(1); // 값이 없을 때 기본 최대값을 명시적으로 설정
range 메서드는 시작값과 종료값이 결과에 포함되지 않는 반면 rangeClosed는 시작값과 종료값이 결과에 포함된다는 점이 다르다.
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter( n -> n%2==0);
// 1부터 100까지의 짝수 스트림
피타고라스의 수 (예, 3, 4, 5 -> 3*3+4*4=5*5 만족하는 정수) 찾기
Stream<int[]> pythagoreanTriples = IntStream.rangeClosed(1, 100).boxed()
.flatMap( a-> IntStream.rangeClosed(a, 100)
.filter(b->Math.sqrt(a*a+b*b)%1==0)
.mapToObj(b-> new int[] {a, b, (int) Math.sqrt(a*a+b*b)}));
임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.
Int[] numbers = {2, 3, 5, 7, 11, 13};
Int sum = Arrays.stream(numbers).sum(); // 합계는 41

Files.lines는 주어진 파일의 행 스트림을 문자열로 반환한다.
스트림 API는 함수에서 스트림을 만들 수 있는 두 개의 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용해서 무한 스트림, 즉 고정된 컬렉션에서 고정된 크기의 스트림을 만들었던 것과는 달리 크기가 고저오디지 않은 스트림을 만들 수 있다.
Stream.iterate(0, n->n+2).limit(10).forEach(System.out::println);
Iterate 메서드는 초깃값(예제는 0)과 람다(예제에서는 UnaryOperator<T> 사용)를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다. 즉 iterate는 요청할 때 마다 무한 스트림을 만든다. 이러한 스트림을 언바운드 스트림이라고 표현한다.

피보나치 수열 집합
Stream.iterate(new int[]{0, 1}, t->new int[]{t[1], t[0]+t[1]})
.limit(20).forEach( t->System.out.println(“(“+t[0]+”,”+t[1]+”)”)); // (0,1),(1,1,),(1,2),(2,3) (3,5), …
Stream.iterate(new int[]{0, 1}, t-> new int[]{t[1], t[0]+t[1]})
.limit(10).map(t -> t[0]).forEach(System.out::println); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …
Generate 메서드는 Supplier<T>를 인수로 받아서 새로운 값을 생산한다.
Stream.generate(Math::random).limit(5).forEach(System.out::println); // 다섯개의 난수 생성
Limit가 없다면 스트림은 언바운드 상태가 된다. 병렬 코드에서는 공급자에 상태가 있으면 안전하지 않다. 따라서 상태를 갖는 공급자는 단지 설명에 필요한 예제일 뿐 실제로는 피해야 한다. 무한 스트림의 요소는 무한적으로 계산이 반복되므로 정렬하거나 리듀스할 수 없다.



Chapter 6 스트림을 데이터 수집

자바 8의 스트림이란 데이터 집합을 멋지게 처리하는 게으른 반복자라고 생각할 수 있다. 스트림의 연산은 filter 또는 map 같은 중간 연산과 count, findFirst, forEach, reduce 등의 최종 연산으로 구분할 수 있다. 중간 연산은 한 스트림을 다른 스트림으로 변환하는 연산으로서, 여러 연산을 연결할 수 있다. 중간 연산은 스트림 파이프라인을 구성하며, 스트림의 요소를 소비하지 않는다. 반면 최종 연산은 스트림의 요소를 소비해서 최종 결과를 도출한다. 최종 연산은 스트림 파이프라인을 최적화하면서 계산 과정을 짧게 생략하기도 한다. 다양한 요소 누적 방식은 Collector 인터페이스에 정의되어 있다. 지금부터 컬렉션, 컬렉터, collect를 헷갈리지 않도록 주의하자.
통화별로 트랜잭션을 그룹화한 코드 (명령형 버전)
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>(); // 그룹화한 트랜잭션 저장
For (Transaction transaction : transactions) { // 트랜잭션 리스트를 반복
Currency currency = transaction.getCurrency(); // 트랜잭션의 통화를 추출
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
if(transactionsForCurrency == null) { // 현재 통화를 그룹화하는 맵에 항목이 없으면 항목 만듬
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction); // 같은 통화를 가진 트랜잭션 리스트에 현재 탐색중인
} // 트랜잭션을 추가한다.
(함수형 버전)
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream().collect(groupingBy(Transaction::getCurrency));

컬렉터란 무엇인가? 함수형 프로그래밍에서는 ‘무엇’을 원하는지 직접 명시할 수 있어서 어떤 방법으로 이를 얻을지는 신경 쓸 필요가 없다. 다수준으로 그룹화를 수행할 때 명령형 프로그래밍과 함수형 프로그래밍의 차이점이 더욱 두드러진다. 명령형 코드에서는 문제를 해결하는 과정에서 다중 루프와 조건문을 추가하며 가독성과 유지보수성이 크게 떨어진다. 함수형 프로그래밍에서는 필요한 컬렉터를 쉽게 추가할 수 있다.
훌륭하게 설계된 함수형 API의 또 다른 장점으로 높은 수준의 조합성과 재사용성을 꼽을 수 있다. collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이 컬렉터의 최대 강점이다. 구체적으로 설명해서 스트림에 collect를 호출하면 스트림의 요소에 (컬렉터로 파라미터화된) 리듀싱 연산이 수행된다. collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다. 보통 함수를 요소로 변환할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적한다.
미리 정의된 컬렉터, 즉 groupingBy 같이 Collectors 클래스에서 제공하는 팩토리 메서드의 기능을 설명한다. Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분할 수 있다.
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할

컬렉터(Stream.collect 메서드의 인수)로 스트림의 항목을 컬렉션으로 재구성할 수 있다. 좀 더 일반적으로 말해 컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠 수 있다. 첫 번째 예제로 counting()이라는 팩토리 메서드가 반환하는 컬렉터로 메뉴에서 요리 수를 계산한다.
long howManyDishes = menu.stream().collect( Collectors.counting() );
- long howManyDishes = menu.stream().count();
counting 컬렉터는 다른 컬렉터와 함께 사용할 때 위력을 발휘한다.

Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드를 제공한다. summingInt는 객체를 int로 매핑하는 함수를 인수로 받는다. summingInt의 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다. 다음은 메뉴 리스트의 총 칼로리를 계산하는 코드다.
int totalCalories = menu.stream().collect( summingInt( Dish::getColrories) );
컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다. 다음은 메뉴의 모든 요리명을 연결하는 코드다. joining 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
Dish 클래스가 요리명을 반환하는 toString 메서드를 포함하고 있다면 다음 코드에서 보여주는 것처럼 map으로 각 요리의 이름을 추출하는 과정을 생략할 수 있다.
String shortMenu = menu.stream().collect(joining()); // 당근, 위의 결과와 같다.
연결된 두 요소 사이에 구분 문자열을 넣을 수 있도록 오버로드된 joining 팩토리 메서드도 있다.
String shortMenu = menu.stream().map(Dish::getName).collect( joining(“, ”) );

모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있다. 즉, 범용 Collectors.reducing으로도 구현할 수 있다. 다음 코드처럼 reducing 메서드로 만들어진 컬렉터로도 메뉴의 모든 칼로리 합계를 계산할 수 있다.
int totalCalrories = menu.stream().collect( reducing(0, Dish::getCalrories, (i, j) -> i+j) );
reducing 메서드는 세 개의 인수를 받는다.
- 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다. (숫자 합계에서는 인수가 없을 때 반환값으로 0이 적당하다)
- 두 번째 인수는 요리를 칼로리 정수로 변환할 때 사용한 변환 함수다
- 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다. 예제에서는 두 개의 int가 사용되었다.

다음은 한 개의 인수를 가진 reducing 버전을 이용해서 가장 칼로리가 높은 요리를 찾는 방법이다
Optional<Dish> mostCalrorieDish = menu.stream().collect( reducing
(d1, d2) -> d1.getCalrories() > d2.getCalrories ? d1 : d2 ) );
collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드인 반면 reduce는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적인 문제가 일어난다.

컬렉션 프레임워크 유연성: 같은 연산도 다양한 방식으로 수행할 수 있다.
int totalCalrories = menu.stream().collect( reducing(0, Dish:getCalrories, Integer::sum); (초기값, 변환함수, 합계함수)
= int totalCalrories = menu.stream().map(Dish::getCalrories).reduce(Integer::sum).get();
= int totalCalrories = menu.stream().mapToInt(Dish::getCalrories).sum();
스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 컬렉터를 이용하는 코드가 더 복잡하다는 사실을 볼 수 있다. 코드가 좀 더 복잡한 대신 재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화와 일반화를 얻을 수 있다. 실무에서는 joining을 사용하는 것이 가독성과 성능에 더 좋다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
= String shortMenu = menu.stream().map(Dish::getName).collect( reducing( s1,s2) -> s1+s2) ).get();
= String shortMenu = menu.stream().collect( reducing( “”, Dish::getName, (s1, s2) -> s1+s2) );
≠ String shortMenu = menu.stream().collect( reducing( (d1, d2) -> d1.getName()+d2.getName() ) ).get();
// 컴파일 에러

데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다. 팩토리 메서드 Collectors.groupingBy를 이요해서 쉽게 메뉴를 그룹화할 수 있다. 이를 분류 함수라고 부른다. 단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 레퍼런스를 분류 함수로 사용할 수 없다.
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect( groupingBy(Dish::getType) );
두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해서 항목을 다수준으로 그룹화할 수 있다. Collectors.groupingBy는 일반적인 분류 함수와 컬렉터를 인수로 받는다. 보통 groupingBy의 연산을 ‘버킷’ 개념으로 생각하면 쉽다.
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
menu.stream().collect( groupingBy(Dish::getType, // 첫 번째 수준의 분류함수
groupingBy( dish -> { // 두 번째 수준의 분류함수
if( dish.getCalories() <= 400 ) return CaloricLevel.DIET;
else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
})
) );

서브 그룹으로 데이터 수집, 메뉴에서 요리의 수를 종류별로 계산할 수 있다.
Map<Dish.Type, Long> typeCount = menu.stream().collect( groupingBy(Dish::getType, counting() ));
컬렉터 결과를 다른 형식에 적용하기: 마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을 삭제할 수 있다. 즉, 다음처럼 패토리 메서드 Collectors.collectingAndThen으로 컬렉터가 반환한 결과를 다른 형식으로 활용할 수 있다. 이 팩토리 메서드는 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다. 리듀싱 컬렉터는 절대 Optional.empty()를 반환하지 않으므로 안전한 코드다.
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(gropuingBy(Dish::getType, // 분류함수
collectingAndThen(maxBy(comparingInt(Dish::getCalories)), // 감싸인 컬렉터
Optional::get))); // 변환 함수

6.4 분할
분할은 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불린을 반환하므로 맵의 키 형식은 Boolean이다.
Map<Boolean, List<Dish>> partitionedMenu
= menu.stream().collect(partitioningBy(Dish::isVegetarian)); // 분할 함수
분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점이다.

6.5 Collector 인터페이스
Collector 인터페이스는 리듀싱 연산(즉, 컬렉터)을 어떻게 구혀할 지 제공하는 메서드 집합으로 구성된다. 즉, Collector 인터페이스를 직접 구현해서 더 효율적으로 문제를 해결하는 컬렉터를 만드는 방법을 살펴본다.
- supplier 메서드: 새로운 결과 컨테이너 만들기
- accumulator 메서드: 결과 컨테이너에 요소 추가하기
- finisher 메서드: 최종 변환값을 결과 컨테이너로 적용하기
- combiner 메서드: 두 결과 컨테이너 병합
- Charactoeristics 메서드: 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.
Stream은 세 함수(supplier, accumulator, combiner)를 인수로 받는 collect 메서드를 오버로드하며 각각의 메서드는 Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 수행한다.
List<Dish> dishes = menuStream.collect( ArrayList::new, List::add, List::addAll);
// supplier accumulator combiner
6.6 커스텀 컬렉터를 구현해서 성능 개선하기
1단계: Collector 클래스 시그너처 정의
2단계: 리듀싱 연산 구현
3단계: 병렬 실행할 수 있는 컬렉터 만들기 (가능하다면)
4단계: finisher 메서드와 컬렉터의 characteristics 메서드



Chapter 7 병렬 데이터 처리와 성능

컴퓨터의 멀티코어를 활용해서 파이프라인 연산을 실행할 수 있다는 점이 가장 중요한 특징이다.

7.1 병렬 스트림
컬렉션에 parallelStream을 호출하면 병렬 스트림이 생성된다. 병렬 스트림이란 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다. 따라서 병렬 스트림을 이용하면 모든 멀티코어 프로세서가 각각의 청크를 처리하도록 할당할 수 있다.
public static long parallelSum(long n) {
return Stream.iterate(1L, i->i+1).limit(n).parallel().reduce(0L, Long::sum);
} // 스트림을 병렬 스트림으로 변환
사실 순차 스트림에 parallet을 호출해도 스트림 자체에는 아무 변화도 일어나지 않는다. 내부적으로는 parallel을 호출하면 이후 연산이 병렬로 수행해야 함을 의미하는 불린 플래그가 설정된다. 반대로 sequential로 병렬 스트림을 순차 스트림으로 바꿀 수 있다.
마법 같은 parallel 메서드를 호출했을 때 내부적으로 어떤 일이 일어나는지 꼭 이해해야 한다. 그렇지 않으면, 스레드를 할당하는 오버헤드만 증가하게 된다. 특화하지 않은 스트림을 처리할 때는 오토박싱, 언박싱 등의 오버헤드를 순반한다. 상황에 따라서는 어떤 알고리즘을 병렬화하는 것보다 적절한 자료구조를 선택하는 것이 더 중요하다.

병렬화가 완전 공짜는 아니라는 사실을 기억하자. 병렬화를 이용하려면 스트림을 재귀적으로 분할해야 하고, 각 서브스트림을 서로 다른 스레드의 리듀싱 연산으로 할당하고, 이들 결과를 하나의 값으로 합쳐야 한다. 멀티코어 간의 데이터 이동은 우리가 생각하는 것보다 비싸다. 따라서 코어 간에 데이터 전송 시간보다 훨씬 오래 걸리는 작업만 병렬로 다른 코어에서 수행하는 것이 바람직하다.
병렬 스트림을 잘못 사용하면서 발생하는 많은 문제는 공유된 상태를 바꾸는 알고리즘을 사용하기 때문에 일어난다. 특히 total을 접근할 때마다 (다수의 스레드에서 동시에 데이터에 접근하는) 데이터 레이스 문제가 일어난다. 동기화로 문제를 해결하다보면 결국 병렬화라는 특성이 없어져 버릴 것이다. 즉, 상태 공유에 따른 부작용을 피해야 한다.

7.2 포크/조인 프레임워크
포크/조인 프레임워크는 병렬화할 수 있는 작업을 재귀적으로 작은 작업으로 분할한 다음에 서브태스크 각각의 결과를 합쳐서 전체 결과를 만들도록 설계되었다. 포크/조인 프레임워크에서는 서브태스크를 스레드 풀의 작업자 스레드에 분산 할당하는 ExecutorService 인터페이스를 구현한다. – 분할 정복 알고리즘의 병렬화 버전

7.3 Spliterator
자바8에서는 Spliterator라는 새로운 인터페이스를 제공한다. Spliterator는 ‘분할할 수 있는 반복자’라는 의미다. Iterator처럼 Spliterator는 소스의 요소 탐색 기능을 제공한다는 점은 같지만, Spliterator는 병렬 작업에 특화되어 있다.



Chapter 8 리팩토링, 데스팅, 디버깅

이 자에서는 람다 표현식을 이용해서 가독성과 유연성을 높이려면 기존 코드를 어떻게 리팩토링해야 하는지 설명할 것이다. 또한 람다 표현식으로 전략, 템플릿 메서드, 옵저버, 의무 체인, 팩토리 등의 객체지향 디자인 패턴을 어떻게 간소화할 수 있는지도 살펴본다.

8.1 가독성과 유연성을 개선하는 리팩토링
람다 표현식을 이용한 코드는 다양한 요구사항 변화에 대응할 수 있도록 동작을 파라미터화한다.
코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있게 만드는 것을 의미한다.
- 익명 클래스를 람다 표현식으로 리팩토링하기 : 콘텍스트 오버로딩에 따른 모호함이 초래될 수 있다.
Runnable r1 = new Runnable() {
public void run() {
System.out.println(“Hello”);
}
}; // 익명 클래스
- Runnable r2 = () -> System.out.println(“Hello”); // 람다 표현식
- 람다 표현식을 메서드 레퍼런스로 리팩토링하기
- 명령형 데이터 처리를 스트림으로 리팩토링하기

8.2 람다로 객체지향 디자인 패턴 리팩토링하기
디자인 패턴은 공통적인 소프트웨어 문제를 설계할 때 재사용할 수 있는, 검증된 청사진을 제공한다.
전략, 템플릿 메섣, 옵저버, 의무 체인, 팩토리 디자인 패턴을 람다 표현식으로…

8.3 람다 테스팅
좋은 소프트웨어 공학자라면 프로그램이 의도대로 동작하는지 확인할 수 있는 단위 테스팅을 진행한다. 우리는 소스 코드의 일부가 예상된 결과를 도출할 것이라 단언하는 테스트 케이스를 구현한다.

8.4 디버깅
* 스택 트레이스
* 로깅 



Chapter 9 디폴트 메서드

전통적인 자바에서 인터페이스와 관련 메서드는 한 몸처럼 구성된다. 인터페이스를 구현하는 클래스는 인터페이스에서 정의하는 모든 메서드 구현을 제공하거나 아니면 슈퍼클래스의 구현을 상속받아야 한다. 자바 8에서는 기본 구현을 포함하는 인터페이스를 정의하는 두 가지 방법을 제공한다. 첫 번째는 인터페이스 내부에 정적 메서드를 사용하는 것이다. 두 번째는 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드라는 기능을 사용하는 것이다. 즉 자바 8에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있다. 결과적으로 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다. 이렇게 하면 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다.
9.1 변화하는 API

9.2 디폴트 메서드란 무엇인가?
default 라는 키워드로 시작.

9.3 디폴트 메서드 활용 패턴
선택형 메서드
동작 다중 상속

9.4 해석 규칙
1. 클래스가 항상 이긴다.
2. 1번 규칙 이외의 상황에서는 서브 인터페이스가 이긴다.
3. 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.



Chapter 10 null 대신 Optional

10.1 값이 없는 상황을 어떻게 처리할까?
NullPointerException

10.2 Optional 클래스 소개
java.util.Optional<T>
값이 있으면 Optional 클래스는 값을 감싼다. 반면 값이 없으면 Optional.empty 메서드로 Optional을 반환한다. Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드다.

10.3 Optional 적용 패턴
Optional<Car> optCar = Optional.empty();
Optional은 map 매서드를 지원한다.
Optional method: empty, filter, flatMap, get, ifPresent, isPresent, map, of, ofNullable, orElse, orElseGet, orElseThrow

10.4 Optional을 사용한 실용 예제



Chapter 11 CompletableFuture: 조합할 수 있는 비동기 프로그래밍

최근에 소프트웨어 구현 방법에 큰 변화를 불러온 두 가지 추세가 있다. 하나는 애플리케이션을 실행하는 하드웨어와 관련된 변화고 다른 하나는 애플리케이션 구조 특히 애플리케이션끼리 어떻게 상호작용하는가와 관련된 변화다

11.1 Future
자바 5부터는 미래의 어느 시점에 결과를 얻는 모델에 활용할 수 있도록 Future 인터페이스를 제공하고 있다. 비동기 계산을 모델링하는 데 Future를 이용할 수 있으며, Future는 계산이 끝났을 때 결과에 접근할 수 있는 레퍼런스를 제공한다.
비동기 API에서는 메서드가 즉시 반환되며 끝내지 못한 나머지 작업을 호출자 스레드와 동기적으로 실행될 수 있도록 다른 스레드에 할당한다. 이와 같은 비동기 API를 사용하는 상황을 비블록 출이라고 한다.

11.2 비동기 API 구현

11.3 비블록 코드 만들기

11.4 비동기 작업 파이프라인 만들기

11.5 CompletableFuture의 종료에 대응하는 방법



Chapter 12 새로운 날짜와 시간 API

12.1 LocalDate, LocalTime, Instant, Duration, Period

12.2 날짜 조정, 파싱, 포매팅

12.3 다양한 시간대와 캘린더 활용 방법



Chapter 13 함수형 관점에서 생각하기

13.1 시스템 구현과 유지보수
자바 8의스트림을 이용하려면 상태 없는 동작이어야 한다는 조건을 만족해야 한다. (즉, 스트림 처리 파이프라인의 함수는 다른 누군가가 변수의 값을 바꿀 수 있는 상태에 있는 변수를 사용하지 않는다.)

변수가 예상하지 못한 값을 갖는 이유는 결국 우리가 유지보수하는 시스템의 여러 메서드에서 공유된 가변 데이터 구조를 읽고 갱신하기 때문이다. 자신을 포함하는 클래스의 상태 그리고 다른 객체의 상태를 바꾸지 않으며 return 문을 통해서만 자신의 결과를 반환하는 메서드를 순수 메서드 또는 부작용 없는 메서드라고 부른다. 불변 객체는 인스턴스화한 다음에는 객체의 상태를 바꿀 수 없는 객체이므로 함수 동작에 영향을 받지 않는다. 즉, 인스턴스화한 불변 객체의 상태는 결코 예상하지 못한 상태로 바뀌지 않는다. 따라서 불변 객체는 복사하지 않고 공유할 수 있으며, 객체의 상태를 바꿀 수 없으므로 스레드 안전성을 제공한다.

‘어떻게’에 집중하는 프로그래밍 형식은 고전의 객체지향 프로그래밍에서 이용하는 방식이다. 때로는 이를 명령형 프로그래밍이라고 부르기도 한다.
‘무엇을’로 접근하는 방식을 선언형 프로그래밍이라고 부르기도 한다. 선언형 프로그래밍에서는 우리가 원하는 것이 무엇이고 시스템이 어떻게 그 목표를 달성할 것인지 등의 규칙을 정한다. 문제 자체가 코드로 명확하게 드러난다는 점이 선언형 프로그래밍의 강점이다.

함수형 프로그래밍은 선언형 프로그래밍을 따르는 대표적인 방식이며, 이전에 설명한 것처럼 부작용이 없는 계산을 지향한다. 스트림으로는 여러 연산을 연결해서 복잡한 질의를 표현할 수 있다.

함수형 프로그래밍이란 무엇인가?
함수형이라는 말은 ‘수학의 함수처럼 부작용이 없는’을 의미한다. 함수나 메서드는 지역 변수만을 변경해야 함수형이라고 할 수 있다. 그리고 함수나 메서드에서 참조하는 객체가 있다면 그 객체는 불변 객체여야 한다. 즉, 객체의 모든 필드가 final이어야 하고 모든 참조 필드는 불변 객체를 직접 참조해야 한다. 예외적으로 메서드 내에서 생성한 객체의 필드는 갱신할 수 있다. 함수형이라면 함수나 메서드가 어떤 예외도 일으키지 않아야 한다.
‘부작용을 감춰야 한다’라는 제약은 참조 투명성 개념으로 귀결된다. 즉, 같은 인수로 함수를 호출했을 때 항상 같은 결과를 반환한다면 참조적으로 투명한 함수라고 표현한다.
순수 함수형 프로그래밍 언어에서는 while, for 같은 반복문을 포함하지 않는다. 왜 그럴까? 이러한 반복문 때문에 변화가 자연스럽게 코드에 스며들 수 있기 때문이다. 루프의 바디에서 함수형과 상충하는 부작용이 발생한다.

이론적으로 반복을 이용하는 모든 프로그램은 재귀로도 구현할 수 있는데 재귀를 이용하면 부작용이 일어나지 않는다. 재귀를 이용하면 루프 단계마다 갱신되는 반복 변수를 제거할 수 있다.



Chapter 14 함수형 프로그래밍 기법

고차원 함수, 커링, 영구 자료구조, 게으른 리스트, 패턴 매칭, 참조 투명성을 이용한 캐싱, 콤비네이터 등

함수형 언어 프로그래머는 함수형 프로그래밍이라는 용어를 좀 더 폭넓게 사용한다. 즉, 함수를 마치 일반값처럼 사용해서 인수로 전달하거나, 결과로 반환하거나, 자료 구조에 저장할 수 있음을 의미한다. 일반값처럼 취급할 수 있는 함수를 일급 함수라고 한다.

♠ 함수 : 하나 이상의 동작을 수행하는 함수 (하나 이상의 함수를 인수로 받음, 함수를 결과로 반환). 자바 8에서는 함수를 인수로 전달할 수 있을 뿐 아니라 결과로 반환하고, 지역 변수로 할당하거나, 구조체로 삽입할 수 있다. 고차원 함수나 메서드를 구현할 때 어떤 이눗가 전달될 지 알 수 없으므로 인수가 부작용을 포함할 가능성을 염두에 두어야 한다.
♠ 커링 : x와 y라는 두 인수를 받는 함수 f를 한 개의 인수를 받는 g라는 함수로 대체하는 기법이다. 이때 g라는 함수 역시 하나의 인수를 받는 함수를 반환한다. 함수 g와 원래 함수 f가 최종적으로 반환하는 값은 같다.
f(x, y) = (g(x))(y)
♠ 영속 자료구조 : 영속(persistent)는 저장된 값이 다른 누군가에 의해 영향을 받지 않는 상태를 의미한다. 결과 자료구조를 바꾸지 말라는 것이 자료구조를 사용하는 모든 사용자에게 요구하는 단 한 가지 조건이다.
♠ 스트림과 게으른 평가 : 스트림은 단 한 번만 소비할 수 있다는 제약이 있어서 스트림은 재귀적으로 정의할 수 없다. 자바 8의 스트림은 요청할 때만 값을 생성하는 블랙박스와 같다. 스트림에 일련의 연산을 적용하면 연산이 수행되지 않고 일단 저장된다. 스트림에 최종 연산을 적용해서 실제 계산을 해야 하는 상황에서만 실제 연산이 이루어진다. 게으른 특성 때문에 연산별로 스트림을 탐색할 필요 없이 한 번에 여러 연산을 처리할 수 있다.

♠ (구조적인) 패턴 매칭 : 자바는 패턴 매칭을 지원하지 않지만, 흉내 내기는 할 수 있다.
♠ 콤비네이터 : 함수형 프로그래밍에서는 두 함수를 인수로 받아 다른 함수를 반환하는 등 함수를 조합하는 고차원 함수를 많이 사용하게 된다. 이처럼 함수를 조합하는 기능을 콤비네이터라고 부른다. - 함수 조합



Chapter 15. OOP와 FP의 조화 : 자바 8과 스칼라 비교

스칼라는 복잡한 형식 시스템, 형식 추론, 패턴 매칭, 도메인 전용 언어를 단순하게 정의할 수 있는 구조 등을 제공한다. 스칼라 코드에서는 모든 자바 라이브러리를 사용할 수 있다.
스칼라에서는 모든 것이 객체다. 자바와 달리 기본형이 없다.
기본 자료구조 : 리스트, 집합, 맵, 튜플, 스트림, 옵션
스칼라의 함수는 어떤 작업을 수행하는 일련의 명령어 그룹이다.



Chapter 16. 결론 그리고 자바의 미래

자바 8의 기능 리뷰
♠ 동작 파라미터화 (람다와 메서드 레퍼런스)
♠ 스트림
♠ CompletableFuture
♠ Optional
♠ 디폴트 메서드

자바의 미래 : 자바 개발은 갑자기 ‘빅뱅’처럼 급격히 변화한 것이 아니라 오랜 시간에 걸쳐 진화하고 있다.