Chapter3: 람다
이 장에서는 다음 항목을 설명한다.
- 람다 표현식을 어떻게 만드는가
- 어떻게 사용하는가
- 어떻게 코드를 간결하게 만들 수 있는가
- 자바 8 API에서 추가된 중요한 인터페이스와 형식 추론 등의 기능
- 람다 표현식과 함께 위력을 발휘하는 메서드 참조
1. 람다란 무엇인가?
람다 표현식 은 메서드로 전달할 수 있는 익명 함수를 단순화한 것
람다의 특징
- 익명 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다.
- 함수 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 한다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
- 전달 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결성 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
람다 표현식
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
- 파라미터 리스트: 메서드 파라미터
- 화살표: 람다의 파라미터 리스트와 바디를 구분
- 람다 바디: 람다의 반환값에 해당하는 표현식
람다의 기본 문법은 (parameters) -> expression, (parameters) -> { statements; } 로 표현한다.
2. 어디에 어떻게 람다를 사용할까?
함수형 인터페이스
함수형 인터페이스 는 정확히 하나의 추상 메서드를 지정하는 인터페이스
ex) 함수형 인터페이스의 예
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
public interface ActionListner extends EventListner {
void actionPerformed(ActionEvent e);
}
public interface interface Callable<V> {
V call() throws Exception;
}
...
추상 메서드가 오직 하나라면 많은 디폴트 메서드를 포함할 수 있다.
람다 표현식은 함수형 인터페이스의 추상 메서드를 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급 할 수 있다.
함수 디스크립터
- 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다.
- 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스트립터 라고 부른다.
- 람다가 함수형 인터페이스의 시그니처와 같은 시그니처를 같는 다는 것을 기억하자.
@FunctionalInterface 어노테이션
해당 어노테이션이 붙은 인터페이스는 함수형 인터페이스임을 가리킨다. 만약 @FunctionalInterface로 선언했는데 실제로 함수형 인터페이스를 만족하지 않으면 컴파일러는 에러를 발생시킨다.
오직 추상 메서드가 하나인 인터페이스에 적용가능하다.
3. 람다 활용: 실행 어라운드 패턴
람다와 동작 파라미터화 코드로 유연하고 간결한 코드를 구현하는 데 도움을 주는 실용적인 예제를 살펴본다.
실행 어라운드 패턴
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine(); // <- 실제 필요한 작업을 하는 행
}
}
- 실제 자원을 처리하는 코드를 설정과 정리 과정이 둘러싸는 형태를 갖는다.
- try-with-resources 구문을 사용하여 자원을 명시적으로 닫지 않아도 된다.
1단계: 동작 파라미터화를 기억하라
기존의 설정, 정리과정은 재사용하고 processFile 메서드의 동작을 다르게 수행할 수 있으면 어떻게 할까?
-> 동작을 파라미터화 한다.
한번에 두 행을 출력하는 코드 - BufferedReader를 인수로 받아 String을 반환하는 람다가 필요하다.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
2단계: 함수형 인터페이스를 이용해서 동작 전달
BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
...
}
3단계: 동작 실행
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접전달할 수 있으며 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리한다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
4단계: 람다 전달
람다를 이용해서 다양한 동작을 processFile 메서드에 전달할 수 있다.
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
4. 함수형 인터페이스 사용
다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다.
자바 8 라이브러리에서 java.util.function 패키지는 여러 가지 새로운 표준 함수형 인터페이스를 제공한다.
1) Predicate
java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며, 제네릭 T를 인수로 받아 불리언을 반환한다. T 형식의 객체를 사용하는 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있다.
String이 비어있지 않으면 리스트에 담아 반환하는 예제
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T t: list) {
if(p.test(T)){
results.add(t);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
2) Consumer
java.util.function.Consume<T> 인터페이스는 제네릭 T를 인수로 받아 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을때 Consumer 인터페이스를 사용할 수 있다.
forEach와 람다를 이용해서 리스트의 모든 항목을 출력하는 예제
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c) {
for(T t: list) {
c.accept(t);
}
}
forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));
3) Function
java.util.function.Function<T, R> 인터페이스는 제네릭 T를 인수로 받아 제니릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.
String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 변환하는 map 메서드를 정의하는 예제
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
public <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T t: list) {
result,add(f.apply(t));
}
return result;
}
List<Integer> l = map(ArrayList("lambdas", "in", "action"),
(String s) -> s.length());
기본형 특화
자바의 모든 형식은 참조형아니면 기본형에 해당한다. 하지만 제네릭 파라미터에는 참조형만 사용할 수 있다. 그래서 자바에서는 기본형을 참조형으로 변환하는 기능을 제공하며, 이 기능은 기본형->참조형으로 변환하는 박싱, 참조형->기본형으로 변환하는 언박싱이다. 이 과정은 자동으로도 이루어지며 이를 오토박싱 이라고 한다.
하지만 박싱, 언박싱 과정은 비용이 소모되고 이에 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.
일반적으로 특정 형식을 받는 함수형 인터페이스의 이름 앞에는 DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction 처럼 형식명이 붙는다.
5. 형식 검사, 형식 추론, 제약
형식 검사
- 람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있다.
- 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식(target type) 이라고 부른다.
코드 형식 확인 과정
List<Apple> heavierThan150g =
filter(inventory, (Apple apple) -> apple.getWeight() > 150);
- filter 메서드의 선언을 확인한다.
- filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대한다.
- Predicate<Apple> 은 test 라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.
- test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
- filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.
같은 람다, 다른 함수형 인터페이스
대상 형식이라는 특징 때문에 같은 람다 표현식이라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> () -> 42;
형식 추론
컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 형식을 생략할 수 있다.
// 형식을 추론하지 않음
Comparator<Apple> c =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 형식을 추론함
Comparator<Apple> c =
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
- 상황에 따라 형식을 명시하거나 생략할 수도 있다.
- 개발자 스스로가 어떤 코드가 가독성을 향상시키는 지 결정해야 한다.
지역 변수 사용
람다 캡처링: 람다 표현식에서는 익명 함수가 하는 것 처럼 자유 변수(free variable) 를 활용할 수 있다.
람다에서 사용하는 자유 변수는 final로 선언되었거나 fianl 처럼 사용되는 변수만 가능하다.
에러 - 컴파일 할 수 없는 코드
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337; // 변수에 값을 두 번 할당하므로 컴파일할 수 없다.
지역 변수의 제약
- 람다가 지역 변수에서 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되어도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.
- 직접 접근을 허용하는 것이 아니라 복사본을 제공해야 한다.
- 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생겼다.
- 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴에 제동을 걸 수 있다.
6. 메서드 참조
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
// 기존 코드
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 메서드 참조
inventory.sort(comparing(Apple::getWeight));
요약
- 메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.
- 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.
- 활용: 클래스::메서드 방식으로 메서드 참조를 활용할 수 있다.
메서드 참조를 만드는 방법
-
정적 메서드 참조
- ex) Integer::parseInt
-
다양한 형식의 인스턴스 메서드 참조
- ex) String::length
-
기존 객체의 인스턴스 메서드 참조
- ex) Transaction 객체를 할당받은 expensiveTransaction의 getValue 메서드 expensiveTransaction::getValue
세 가지 종류의 람다 표현식을 메서드 참조로 바꾸는 방법
-
(args) -> ClassName.staticMethod(args)
ClassName::staticMethod
-
(args, rest) -> arg0.instanceMethod(rest)
ClassName::instanceMethod
-
(args) -> expr.instanceMethod(args)
expr::instanceMethod
메서드 참조는 콘텍스트의 형식과 일치해야 한다.
생성자 참조
ClassName::new 처럼 클래스명과 new 키워드를 이용해0서 기존 생성자의 참조를 만들 수 있다.
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);
BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple a3 = c3.apply(GREEN, 110);
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
Apple a3 = c3.apply(GREEN, 110);
7. 람다 메서드 참조 활용하기
처음의 사과 리스트를 다양한 정렬하는 기법에서 동작 파라미터화, 익명 클래스, 람다 표현식, 메서드 참조 등을 총동원한다.
1단계: 코드 전달
sort 메서드의 시그니처 void sort(Comparator<? super E> c)
‘sort의 동작은 파라미터화 되었다.’
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
2단계: 익명 클래스 사용
한 번만 사용할 Comparator는 익명 클래스를 이용하는 것이 좋다.
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()));
Comparator의 Function 정적 메서드 comparing 사용
import static java.util.Comprator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
4단게: 메서드 참조 사용
코드를 더 간소화할 수 있다.
inventory.sort(comparing(Apple::getWeight()));
8. 람다 표현식을 조합할 수 있는 유용한 메서드
-
자바 8 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함한다.
- 간단한 여러 개의 람다 표현식을 조합해 복잡한 람다 표현식을 만들 수 있다.
- 디폴트 메서드라는 것으로 가능하다.
Comparator 조합
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
역정렬
주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공한다.
// 무게를 내림차순으로 정렬
inventory.sort(comparing(Apple::getWeight).reversed());
Comparator 연결
무게가 같을 때 또 다른 기준으로 정렬하고 싶을 때
thenComparing 메서드로 두 번째 비교자를 만든다.
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry)
);
Predicate 조합
Predicate 인터페이스는 복잡한 프레디케이트를 위해 negate, and, or 세 가지 메서드를 제공한다.
// 기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만든다.
Predicate<Apple> notRedApple = redApple.negate();
// 두 프레디케이트를 연결해서 새로운 프레디케이트 객체를 만든다.
Predicate<Apple> redAndHeavyApple =
redApple.and(apple -> apple.getWeight() > 150);
// or을 이용해서 다양한 조건을 만든다.
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));
이것이 대단한 일인 이유는 단순한 람다 표현식을 조합해 복잡한 람다 표현식을 만들 수 있고, 표현식을 조합해도 코드 자체가 문제를 잘 설명한다.
Function 조합
Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.
-
andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.andThen(g); int result = h.apply(1); // 4를 반환
-
compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.
Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.compose(g); // f(g(x)) int result = h.apply(1); // 3을 반환
9. 비슷한 수학적 개념
수학적 관심이 있으면 책을 살펴보자. pass…
10. 마치며
- 람다 표현식은 익명 함수의 일종이다. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.
- 람다 표현식으로 간결한 코드를 구현할 수 있다.
- 함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스다.
- 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.
- 람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉석으로 제공할 수 있으며 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
- java.util.function 패키지는 자주 사용하는 다양한 함수형 인터페이스를 제공한다.
- 자바 8은 제네릭 함수형 인터페이스와 관련한 박싱 동작을 피할 수 있는 기본형 특화 인터페이스도 제공한다.
- 실행 어라운드 패턴을 람다와 활용하면 유연성과 재사용성을 추가로 얻을 수 있다.
- 람다 표현식의 기대 형식을 대상 형식이라고 한다.
- 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.
- Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공한다.