람다 표현식이란?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.
람다의 특징
- 익명
보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드와 메서드명의 대한 걱정거리가 줄어든다.
- 함수
람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터, 바디, 반환, 예외를 포함 할 수 있다.
- 전달
람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결성
익명 클래스처럼 많은 코드를 구현하는게 아닌 축약된 코드를 사용한다.
람다 예제
불리언 표현식 - (List<String> list) -> list.isEmpty()
객체 생성 - () -> new Apple(10)
객체에서 소비 - (Apple a) -> { System.out.println(a.toString()); }
객체에서 선택/추출 - (String s) -> s.length()
두 값을 조합 - (int a, int b) -> a*b
두 객체 비교 - (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
어디에, 어떻게 람다를 사용할까?
람다는 함수형 인터페이스를 사용한다. 함수형 인터페이스는 하나의 추상 메서드를 가진 인터페이스이다. @FunctionalInterface 어노테이션을 붙여 주면 메서드가 2개 이상일 경우 컴파일 단계에서 에러를 발생 시킬 수 있다.
함수형 인터페이스는 많은 미폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스이다.
함수형 인터페이스
자바 API의 함수형 인터페이스로 Comparator, Runnable, Predicate, Consumer, Function, Supplier 등이 있다.
함수 디스크립터
함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 부른다. () -> void 표기는 파라미터가 없고 void를 반환하는 함수를 의미하고 (Apple,Apple) -> int 는 두 개의 Apple을 인수로 받아 int를 반환하는 함수를 가리킨다.
public void process(Runnable r) {
r.run();
}
process(() -> System.out.println("This is awesome !" ));
위에 코드를 실행하면 'This is awesome!!' 이 출력된다 이 표현식은 인수가 없으며 void를 반환하는 람다 표현식이다.
람다 활용 : 실행 어라운드 패턴
실행 어라운드 패턴이란 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는걸 말한다.
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("text.txt"))){
return br.readLine();
}
}
현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다. 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까?
기존의 설정, 정리 과정은 재사용하고 processFile메서드만 다른 동작을 수행하도록 명령 할 수 있게하면 된다. 떠오르는 방법으로는 processFile메서드 동작을 파라미터화 하는것이다. BufferedReader를 이용해서 다른 동작을 수행할 수 있도록 processFile메서드로 동작을 전달해야 한다.
@FunctionalInterface
public interface BufferedReaderProcess {
String process(BufferedReader b) throws IOException;
}
public class Main {
public static void main(String[] args) throws IOException {
System.out.println(processFile((BufferedReader br) -> br.readLine() + br.readLine()));
}
public static String processFile(BufferedReaderProcess p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("text.txt"))) {
return p.process(br);
}
}
}
람다를 이용해 다양한 동작을 processFile 메서드로 전달할 수 있게 됐고 더 유연한 메서드를 만들었다.
Predicate
java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다. 우리가 만들었던 인터페이스와 같은 형태지만 따로 정의할 필요없이 사용할 수 있게 되어있다.
public interface Predicate<T> {
boolean test(T t);
}
public static <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.isBlank();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.
T형식의 객체를 인수로받아서 어떤 동작을 수행하고 싶을 때 Consumer인터페이스를 사용할 수 있다.
public interface Consumer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list,Consumer<T> c){
for(T t : list){
c.accept(t);
}
}
forEach(listOfStrings,(s)-> System.out.println(s));
Function
java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R객체를 반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 Fnction 인터페이스를 활용할 수 있다.
@FunctionalInterface
public interface Function<T,R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> results = new ArrayList<>();
for (T t : list) {
results.add(f.apply(t));
}
return results;
}
List<Integer> results = map(listOfStrings, (x) -> x.length());
자바의 모든 형식은 참조형(Byte,Integer,Object) 아니면 기본형(int,long,char)에 해당한다. 하지만 제네릭 파라미터(<T>)에는 참조형만 사용할 수 있다. 제네릭의 내부 구현 때문에 어쩔 수 없는 일이다.(기본형은 Object로 변환 불가하므로)
형식 검사, 형식 추론, 제약
- 형식검사
람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있다. 어떤 콘텍스트(예를 들면 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현식의 형식을 대상 형식 이라고 부른다. 람다 표현식을 사용할 대 실제 어떤 일이 일어나는지 보여주는 예제를 보자
1. filter메서드의 선언을 확인한다.
2. filter메서드는 두 번째 파라미터로 Predicate<Apple> 형식을 기대한다.
3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.
4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크럽터를 묘사한다.
5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.
0 . List heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
1. filter의 정의를 확인하면 filter(List<Apple> inventory, Predicate<Apple> p)
2. 대상형식은 Predicate<Apple> 확인
3. Predicate<Apple>인터페이스의 추상메서드는 무엇인가? boolean test(Apple apple)
4. Apple을 인수로 받아 boolean을 반환하는 test메서드 확인!
5. 함수 디스크립터는 Apple -> boolean 이므로 람다의 시그니처와 일치 형식 검사 완료
형식 추론
코드를 좀 더 단순화할 수 있는 방법이 있다. 자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론 할 수 있다.
예를 들어,
Comparator<Apple> c = (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
위에 코드는 형식 추론을 하지 않는다. 하지만
Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
위에 코드는 형식을 추론해야 한다. 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있다. 어떤 방법이 좋은지 정해진 규칙은 없다.
메서드 참조
메서드참조는 특정 람다 표현식을 축약한 것이라고 생각하면 된다. 기존의 메서드를 재활용해서 람다처럼 전달할 수 있다. 때로는 람다 표현식보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다.
코드로 보면
inventory.sort ((Apple a1, Apple a2) -> a.getWeight().compareTo(a2.getWeight()));
inventory.sort(comparing(Apple::getWeight));
위에 코드를 아래 코드처럼 변환 할 수 있다. 처음보면 어색 할 수 있지만 경우에따라 가독성을 높일 수 있으니 알아보자.
결과적으로 메서드 참조는 람다 표현식 (Apple a) ->a.getWeight()를 축약한 것이다. 메서드 참조는 세 가지 유형으로 구분할 수 있다.
1. 정적 메서드 참조
Integer의 parseInt메서드는 Integer::parseInt 로 표현할 수 있다.
2. 다양한 형식의 인스턴스 메서드 참조
String의 leghth메서드는 String::lenghth로 표현할 수 있다.
3. 기존 객체의 인스턴스 메서드 참조
Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면, 이를 expensiveTransaction::getValue 라고 표현할 수 있다.
'Study > 모던 자바 인 액션' 카테고리의 다른 글
스트림 활용 -1 (0) | 2022.09.14 |
---|---|
2장 - 동작 파라미터화 코드 전달하기 (0) | 2022.07.06 |