간결하며 이해하기 쉬운 코드가 각광받음에 따라 객체지향 언어인 자바에서도 8버전(1.8버전)부터 화살표 함수, 즉 람다식을 지원하기 시작했다.
아래 코드는 id로 DB에서 값을 조회하여 만약 존재한다면 해당 객체의 이름을 출력하는 함수를 두 가지 방식으로 구현한 것이다. (다소 비현실적인 형태이지만 양해바람...)
public class Test {
class Student {
private Long id;
private String name;
public String getName() {
return this.name;
}
}
Optional<Student> findById(Long id) {
// DB의 Student 객체가 담긴 테이블에서 id로 검색 후 검색된 녀석을 Student 객체로 변환하여 반납
// 못찾으면 예외를 발생시키므로 이 함수를 사용하는 곳에서 예외처리를 해주어야 함
// 구현은 생략
}
void DB에서_Student조회후_name출력_1(Long id) {
Student student = findById(id).못찾으면이거반납(null);
if (student != null) {
System.out.println(student.getName());
}
}
void DB에서_Student조회후_name출력_2(Long id) {
findById(id).ifPresent(찾은거 -> System.out.println(찾은거.getName()));
}
}
첫 번째 함수도 못 알아볼 정도는 아니지만 두 번째 함수가 더 마음에 든다. (아님 말구)
이제 두 번째 함수와 같은 코드를 쓸 수 있게 해주는 함수형 인터페이스에 대해 알아보자.
함수형 인터페이스는 추상메소드를 단 하나만 가지고 있는 인터페이스를 말한다.
그런데 이 정의만 가지고는 함수형 인터페이스가 도대체 뭘 하기 위한 녀석인지 감이 오지 않는다.
왜 추상메소드를 단 하나만 가지고 있는 인터페이스를 특정하여 '함수형 인터페이스'라는 이름까지 붙여주었는지 이해하려면 아무래도 추가 설명이 필요하다.
하고 싶은 일
- 함수의 파라미터로 람다식을 전달했을 때 자동으로 익명 객체 만들기
해결방안
- 난 함수A의 파라미터 자리에 람다식 하나만 줄테니 자바 네가 알아서 익명 객체 만들어서 대신 집어넣어.
대신 함수A는 파라미터로 추상메소드가 하나뿐인 인터페이스를 받으라고 할게.
거기에는 추상메소드가 하나뿐이니까 그 자리에 내가 준 람다식을 넣어서 익명 객체를 만들면 되겠지?
위 내용을 통해 대략적인 감을 잡고 더 구체적인 내용으로 넘어가보자.
자바는 객체지향 언어(매우매우 형식적인 언어)이다.
따라서 언어를 사용함에 있어 꽤 많은 제약이 따르는데, 여기서 주목할 것은 함수의 파라미터로 '일급 객체' 만 받을 수 있다는 사실이다.
'일급 객체'가 무엇이냐 물으면 필자도 잘 모르니 넘어가고, 지금 중요한 사실은 자바에서 함수, 즉 메소드는 일급 객체가 아니라는 사실이다. (다른 언어에서는 메소드가 일급 객체일 수도 있다)
그러면 위 코드의
~~~.ifPresent(찾은거 -> System.out.println(찾은거.getName()));
에서, ifPresent
가 파라미터로 받은 람다식은 마치 메소드처럼 생겼지만 메소드가 아니라는 말이 된다.
실제로 ifPresent
가 어떻게 구현되어 있는지 살펴보면 아래와 같다.
public final class Optional<T> {
// Optional에 들어있을 것이라고 여겨지는 값
// 위 예제처럼 DB에서 무엇인가를 조회했다면 그 값이 value로 전달. 만약 찾지 못했다면 null이 채워짐
// 아래 두 생성자 참고
private final T value;
private Optional() { this.value = null; }
private Optional() { this.value = Objects.requireNonNull(value); }
public void ifPresent(Consumer<? super T> action) {
if (value != null) {
action.accept(value);
}
}
// 여러가지 함수들...
}
위 코드에서 볼 수 있듯이 ifPresent
는 파라미터로 Consumer
라는 녀석을 받고 있다.
Consumer
는 함수형 인터페이스로, 구현되지 않은 단 하나의 추상 메소드 accept
를 가진다.
자바는 컴파일 시(정확하지 않음... 원문에서는 '람다식이 평가될 때') ifPresent
의 파라미터로 전달된 람다식을 accept
자리에 넣어 Consumer
를 구현한 익명 객체를 만든 후, 그 객체를 ifPresent
의 파라미터 자리에 집어넣는다.
위 코드에서 ifPresent
는 파라미터로 받은 Consumer
타입의 action
이라는 객체에서 accept
메소드를 꺼내 쓰고 있다는 것을 확인할 수 있다.
다시 처음 코드로 돌아가보자.
// findById(id)는 Optional<Student> 형
findById(id).ifPresent(찾은거 -> System.out.println(찾은거.getName()));
자바는 ifPresent
의 파라미터로 전달한
찾은거 → System.out.println(찾은거.getName())
를 이용하여 Consumer<? super Student>
형의 익명 객체를 만든다.
우리가 전달한 람다식은 이 객체의 accept
라는 메소드가 되어 ifPresent
내부에서 호출된다.
이제 Consumer
의 내부를 살펴보자.
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
(맨 위 @FunctionalInterface
어노테이션은 휴먼에러를 방지하기 위한 어노테이션으로, 이 어노테이션이 달린 인터페이스가 가진 추상메소드가 한 개가 아닐 경우 컴파일 에러가 발생된다)
(default
메소드는 여기서는 설명하지 않는다)
Consumer
는 '소비하는 자'라는 이름답게 추상메소드 accept
가 파라미터를 받지만 반환형은 void
이다.
만약 파라미터를 받아서 무언가를 반환하고 싶거나, 파라미터를 받지 않고 싶다면 Consumer
가 아닌 다른 함수형 인터페이스를 사용해야 한다.
다음은 자바에서 지원하는 대표적 함수형 인터페이스들이다.
입력 → 출력 | 추상메소드 | |
---|---|---|
Consumer | T → void | void accept(T t) |
Function | T → R | R apply(T t) |
Predicate | T → boolean | boolean test(T t) |
Supplier | ( ) → T | T get() |
Runnable | ( ) → void | void run() |
Comparator | (T, T) → int | int compare(T o1, T o2) |
이외에도 자바가 제공하는 함수형 인터페이스들이 더 있고 필요에 따라 사용자가 직접 만들어 사용할 수도 있다.
다만 위 목록에 있는 녀석들 만으로도 웬만한 일은 전부 해결할 수 있다.
끄읕_!
1.
자바 스트림의 map
, filter
메소드는 각각 파라미터로 위 목록에 있는 함수형 인터페이스중 하나를 받는다. 각 함수가 파라미터로 어떤 함수형 인터페이스를 받을지 추론하여 말하고 그 이유를 설명하시오.
힌트 _ map
, filter
사용 예시
// 각각의 student를 student의 name으로 변경
~~~.map(student -> student.getName());
// student 중 점수가 80 미만인 친구들만 남기기
~~~.filter(student -> student.getScore() < 80);
2.
다음을 읽고 ${ㅁㅁㅁ}, ${ㅇㅇㅇ} 안에 들어갈 코드를 작성하시오.
class Robot {
private String name;
private String nickname;
private int weight;
private int price;
public Robot(String name, String nickname, int weight, int price) {
this.name = name;
this.nickname = nickname;
this.weight = weight;
this.price = price;
}
}
${ㅁㅁㅁ}<Robot> makeRobot = () -> new Robot(랜덤문자열, 랜덤문자열, 랜덤양의정수, 랜덤양의정수);
List<Robot> robots = new ArrayList<>();
for (int i = 0 ; i < 100 ; i++) {
robots.add(makeRobot.${ㅇㅇㅇ});
}