람다 표현식은 어디에, 어떻게 사용할까?
-> 함수형 인터페이스라는 문맥에서 사용할 수 있다.
함수형 인터페이스
람다식은 익명 클래스의 객체와 동등하다. 그렇다면 람다식으로 정의된 익명 객체의 메서드는 어떻게 호출할 것인가?
타입 f = (int a, int b) -> a>b?a:b;
먼저 참조 변수 f에 익명 객체를 저장해 보았다.
그렇다면 참조변수 f의 타입은 어떤 것이 와야 할까?
일단 참조 변수이기 때문에 클래스 또는 인터페이스가 가능하며 람다식과 동등한 메서드가 정의되어있어야 한다.
예를 들어 아래와 같이 정의된 인터페이스가 있다고 가정하자.
interface MyFunction{
public abstract int max(int a, int b);
}
MyFunction 인터페이스를 구현한 익명 클래스의 객체는 다음과 같다.
MyFunction f = new MyFuntion(){
public int max(int a, int b){
return a>b?a:b;
}
}
int big = f.max(5,3); //익명 객체 메서드 호출
위에 생성한 익명 객체는 아래의 람다식으로 대체할 수 있다.
MyFunction f =(int a, int b)-> a>b?a:b
int big = f.max(5,3);
MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체할 수 있는 이유는 람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수 타입과 개수 그리고 반환 값이 일치하기 때문이다.
하나의 추상 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바 규칙을 어기지 않으면서도 자연스럽기 때문에 인터페이스를 통해서 람다식을 다루기로 결정되었다.
즉 람다식을 다루기 위한 인터페이스를 ‘함수형 인터페이스’라고 부르기로 하였다.
또한 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문에 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어있어야 한다. 하지만 static, default 메서드의 개수는 제약이 없다.
자바 API 함수형 인터페이스로는 Comparator, Runnable 등이 있다.
public interface Comparator<T>{
int compare(T o1, T o2);
}
public interface Runnable<T>{
void run();
}
함수 디스크립터
함수 디스크립터란 람다 표현식의 시그니처를 서술하는 메서드를 말한다.
boolean test(T t)가 메서드 시그니처라면 함수 디스크립터로 표현할 시 T → boolean으로 표현할 수 있다.
Runnable의 메서드와 같은 경우는 () → void로 표현한다.
함수형 인터페이스를 람다식으로 표현하려면 인터페이스의 시그니처와 함수 디스크립터가 일치해야 한다.
interface MyFunction{
public int max(int a, int b){
return a>b?a:b;
}
}
MyFunction f = (a,b) -> return "ab";
위의 코드는 유효하지 않는 람다 표현식이다.
(a, b) -> return "ab"의 시그니처는 (int a, int b) → String 이기 때문에 MyFunction의 max 메서드 시그니처(a,b) -> Integer 와 일치하지 않는다.
@FunctionalInterface
: 함수형 인터페이스를 가리키는 어노테이션이다. 함수형 인터페이스가 아닌 곳에 선언하면 컴파일 에러를 발생시킨다.
실행 어라운드 패턴(Excute Around Pattern)
람다 표현식을 적용할 수 있는 대표적인 패턴으로 실행 어라운드 패턴이 있다.
실행 어라운드 패턴이란 공통된 준비코드와 정리 코드가 작업을 감싸고 있는 형태를 말한다.
public String processFile() throws IOException(){
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
return br.readLine();
}
}
파일에서 한 행을 읽는 코드이다. 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다. 위의 예제는 try-with-resource 구문을 사용함으로써 자원을 명시적으로 닫을 필요가 없으므로 간결한 형태를 갖는다.
1. 동작 파라미터화
하지만 위에서 정의한 코드는 파일을 한 줄만 읽을 수 있다. 한 번에 두 줄을 읽거나 혹은 자주 사용되는 단어를 변환하려면? 기존의 설정, 정의 과정은 재사용하되 processFile의 메서드만 변경해주면 된다.
이는 즉 processFile 로 동작을 전달해서 processFile 메서드가 다른 동작을 수행하게끔 할 수 있다.
이러한 동작을 람다로 전달하기 위해서 코드를 변경해보자
먼저 BufferedReader를 인수로 받아서 String 결과를 리턴하는 람다가 필요하다.
String result = processFile((BufferedReader br)-> br.readLine() + br.readLine());
2. 함수형 인터페이스를 이용해서 동작 전달
processFile 메서드로 람다식으로 표현한 동작을 전달하기 위해서는 BufferedReader → String과 IOException을 throw 할 수 있는 시그니처를 가진 함수형 인터페이스를 만들어야 한다.
@FunctionalInterface
public interface BufferedReaderProcesser{
String process(BufferedReader br) throws IOException;
}
정의한 인터페이스를 processFile 의 인수로 전달한다.
public String processFile(BufferedReaderProcesser p) throws IOException(){
...
}
3. 동작 실행
public String processFile(BufferedReaderProcesser p) throws IOException(){
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
return p.process(br);
}
}
processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.
4. 람다 전달
람다를 이용해서 다양한 동작을 processFile로 전달할 수 있다.
String oneLine = processFile((BufferedReader br)->br.readLine());
String twoLines = processFile((BufferedReader br)->br.readLine()+br.readLine());
참조 : 모던 자바 인 액션, 자바의 정석
'Dev > Java' 카테고리의 다른 글
[Design Pattern] Observer Pattern (0) | 2022.07.10 |
---|---|
Serialize & Deserialize in Java (0) | 2022.06.26 |
[모던자바인액션] 스트림API가 지원하는 다양한 연산 (0) | 2022.02.19 |
[모던자바인액션] 컬렉션과 스트림의 차이점(Stream vs Collection) (0) | 2022.01.28 |
댓글