람다 표현식
은 메서드로 전달할 수 있는 익명 함수를 단순화한 것
람다 표현식은 파라미터, 화살표, 바디로 이루어진다.
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
필터 메서드에도 람다를 활용할 수 있다.
List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));
Predicate가 함수형 인터페이스인데, 이는 오직 하나의 추상 메서드만 지정하기 때문이다.
public interface Predicate<T> {
boolean test (T t);
}
함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스
다.
자바 API의 함수형 인터페이스로 Comparator, Runnable등이 있다.
함수형 인터페이스의 추상 메서드 시그니처
는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터
라고 부른다.
() → void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다. (Runnable이 해당)
자원 처리에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다.
실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine(); // 실제 필요한 작업을 하는 행
}
}
processFile의 동작을 파라미터화. 람다를 이용해서 동작을 전달할 수 있다.
다음은 BufferedReader에서 두 행을 출력하는 코드다.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
함수형 인터페이스 자리에 람다를 사용할 수 있다. BufferedReader → String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
...
}
BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader → String)와 일치하는 람다를 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOExpection {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br); // BufferedReader 객체 처리
}
}
람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.
한 행을 처리하는 코드
String oneLine = processFile((BufferedReader br) -> br.readLine());
두 행을 처리하는 코드
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터
라고 한다.
java.util.function.Predicate<T>
인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.
java.util.function.Consumer<T>
인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.
java.util.function.FunctionL<T, R>
인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.
자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 박싱
이라고 하며 참조형을 기본형으로 변환하는 반대 동작을 언박싱
이라고 한다.
박싱과 언박싱이 자동으로 이루어지는 오토박싱
이라는 기능도 제공한다.
람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식
이라고 부른다.
List<Apple> heavierThan150g =
filter(inventory, (Apple apple) -> apple.getWeight() > 150);
대상 형식
이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 사용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다, ⇒ 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.
람다 형식에서는 익명 함수가 하는 것처럼 자유 변수 를 활용할 수 있다.
람다 캡처링
이라고 한다.
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 람다 표현식보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다.
메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다. 이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.
메서드 참조는 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있다.
Apple::getWeight는 Apple 클래스에 정의된 getWeight의 메서드 참조다.
메서드 참조는 세 가지 유형으로 구분할 수 있다.
메서드 참조는 콘텍스트의 형식과 일치해야 한다.
ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이것은 정적 메서드의 참조를 만드는 방법과 비슷하다.
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get(); // Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.
(추후 보충 예정)
사과 리스트 정렬 문제를 해결하면서 동작 파라미터화, 익명 클래스, 람다 표현식, 메서드 참조 등을 총동원한다.
sort메서드에 어떻게 정렬 전략을 전달할 수 있을까?
void sort(Comparator<? super E> C)
Comparator 객체를 인수로 받아 두 사과를 비교한다. 객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있다. 이제 sort의 동작은 파라미터화 되었다.
라고 말할 수 있다. 즉, sort에 전달된 정렬 전략에 따라 sort의 동작이 달라질 것이다.
한 번만 사용할 Comparator를 익명 클래스를 이용하는 것이 좋다.
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
함수형 인터페이스를 기대하는 곳 어디에서나 람다 표현식을 사용할 수 있음을 배웠다. Comparator의 함수 디스크립터는 (T, T) → int다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight())
메서드 참조를 이용해서 코드를 조금 더 간소화할 수 있다.
inventory.sort(comparing(Apple::getWeight));
정적 메서드 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환할 수 있다.
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
역정렬
inventory.sort(comparing(Apple::getWeight).reversed());
Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.
특정 프레디케이트를 반전시킬때는 negate메서드를 사용할 수 있으며, and 메서드를 이용해서 두 람다를 조합할 수 있다. 뿐만 아니라 or 메서드를 이용해서 다양한 조건을 만들 수 있다.
Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.
andThen → 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
compose → 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.