람다 표현식이란, 메서드로 전달할 수 있는 익명 함수를 단순화한 것이다.
이를 통해, 기존 문법에 대한 syntactic sugar를 제공한다.
람다 표현식은 함수형 인터페이스로 선언된 자리에만 사용할 수 있습니다. 함수형 인터페이스란, 추상 메서드를 하나만 갖는 인터페이스를 의미합니다. 람다 표현식을 함수형 인터페이스 자리에 사용할 수 있는 이유는, 람다식을 함수형 인터페이스를 구현한 클래스의 인스턴스로 취급하기 때문입니다.
Runnable r1 = () -> Sysmtem.out.println("Hello World 1"); // 아래와 동일한 코드. 결국 람다 표현식은 우리에게 syntatic sugar를 제공하는 것
Runnable r2 = new Runnable() { //보면 Runnable 인터페이스로부터 인스턴스를 생성함을 확인할 수 있는데, '익명 클래스'는 인터페이스로부터도 인스턴스를 생성할 수 있게 해준다고 한다.
public void run() {
System.out.println("Hello World 2");
}
}
해당 애노테이션을 인터페이스에 붙여주면, 대상 인터페이스가 실제로 함수형 인터페이스가 아닌 경우에 컴파일러에서 에러를 발생시킨다고 한다.
@FunctionalInterface
public interface Function<T, R> {
...
}
실제 작업에 대한 코드를 설정, 정리 코드가 둘러싸고 있는 형태의 패턴을 실행 어라운드 패턴이라고 한다. 해당 패턴의 경우 설정, 정리가 반복되기 때문에 작업에 대한 코드를 파라미터화해서 람다식을 통해 전달해주는 방법을 사용할 수 있다.
// 함수형 인터페이스, 람다식을 통한 구현
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { // 해당 라인을 이해하기 위해서 Try With Resources (JAVA7 spec)를 찾아보는 것이 좋다.
return p.process(br);
}
}
// 람다식을 processFile 메서드의 매개변수로 넘겨줌으로써 반복되는 코드에 대해 재사용성을 늘려줄 수 있다.
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있게 해준다고 한다. 실제로 메서드를 호출하는 것은 아니고 람다 표현식에 대한 syntactic sugar를 제공해준다고 생각하면 될 듯하다.
메서드 참조는 그때의 컨텍스트와 일치해야 한다. (자세히 알기 위해서는 '자바 람다의 형식 검사, 추론' 등을 검색해보자)
e.g. Apple::getWeight === (Apple a) -> a.getWeight()
메서드 참조를 만드는 방법에는 3가지가 존재한다.
1. 정적메서드 참조 - ClassNeme::staticMethod
2. 인스턴스 메서드 참조 - ClassName::instanceMethod
3. 기존 객체의 인스턴스 메서드 참조 - expr::instanceMethod
생성자에 대한 참조를 만들 수도 있다. - ClassName:new
// 아래 두개는 같다.
Supplier<Apple> c1 = Apple:: new; //Apple() constructor 참조
Apple a1 = c1.get();
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();
// 아래 두개도 같다
Function<Integer, Apple> c2 = Apple::new; // Apple(int weight) constructor 참조 -> 같은 시그니처를 갖는 함수형 인터페이스에 맵핑된다.
Apple a2 = c2.apply(110);
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);
아래의 코드는 배운 내용을 바탕으로 고도화 시키는 작업을 보여준다.
//1단계 - 코드 전달
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
//2단계 - 익명 클래스 사용
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
//3단계 - 람다 표현식 사용
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//3단계 - 람다 표현식 + 타입 추론 활용
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
//3단계 - 람다 표현식 + 타입 추론 활용 + comparing 메서드 활용
//comparing 정적 메서드는 Comparable 키를 추출해서 Comparator 객체로 만드는 Funtion 함수를 인수로 받는 메서드라고 한다..
import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
//4단계 - 메서드 참조 사용
inventory.sort(comparing(Apple::getWeight));
기본적으로 제공하는 함수형 인터페이스는 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있는 유틸리티 메서드(디폴트 메서드)를 제공한다.
디폴트 메서드는 추상 메서드가 아니기 때문에, 함수형 인터페이스의 정의를 벗어나지 않는다고 한다.
간단히 아래의 예제들을 살펴보자.
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
// 역정렬
inventory.sort(comparing(Apple::getWeight).reversed());
// 여러 개의 Comparator 사용 (메서드 체이닝 활용)
// 비슷한 예시 : controller에 대한 mock test code (찾아서 보여주자.)
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry)); //두 사과의 무게가 같으면 국가별로 정렬
Predicate<Apple> notRedApple = redApple.negate(); //결과 반전
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150); //빨간색 + 무게 150 초과
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor())); //(빨간색 and 무게 150초과) or 녹색사과 (우선순위 왼 -> 오른)
잘 보고 있습니다. 다음 장도 기대되네요!