추상 메서드가 단 하나뿐인 인터페이스. 무엇을 구현해야 하는지가 하나로 확정되므로, 클래스를 만들지 않고 표현식만 적어 넘길 수 있다.
하나로 확정되는 구현
인터페이스는 인스턴스를 만들 수 없어서 그 자리에서 구현하려면 익명 클래스가 필요했다. 추상 메서드가 하나뿐이면 거기 적은 코드가 그 하나를 구현한 것으로 확정되고, new나 @Override 같은 뻔한 부분을 걷어낸 람다 표현식이 성립한다. 익명 클래스가 람다로 줄어드는 과정은 그 항목에 적어두었다.
행위를 넘겨받는 컨테이너
이렇게 하면 기능을 쓰는 쪽이 그 기능을 직접 정의한다. 컨테이너가 함수를 매개변수로 받으면 무엇을 할지는 부르는 곳이 정하고, 컨테이너는 그것을 모른 채 원소를 넘겨주기만 하면 된다. 같은 컨테이너가 걸러내기도 하고 변환하기도 한다. 구현부를 하나도 건드리지 않고 호출하는 쪽에서 동작을 바꾸는 것이라 의존성 주입과 같은 발상이고, 객체 대신 행위를 주입하는 셈이다.
직접 만들 때
직접 만들 때는 추상 메서드를 하나만 두고 @FunctionalInterface를 붙인다.
@FunctionalInterface
public interface MyRunnable {
void run();
}컬렉션에 붙여보기
컬렉션에 붙여보면 감이 온다. 원소마다 무언가 하고 반환은 없는 foreach에는 Consumer가 맞는다.
public void foreach(Consumer<T> consumer) {
for (int i = 0; i < list.size(); i++) {
T data = list.get(i);
consumer.accept(data);
}
}원소를 다른 타입으로 바꾸는 map을 만들다 보면 타입 변수에서 걸린다.
public <U> MyCollection<U> map(Function<T, U> function) {
List<U> newList = new ArrayList<>();
foreach(data -> newList.add(function.apply(data)));
return new MyCollection<>(newList);
}MyCollection이 들고 있는 타입 변수는 <T> 하나뿐인데 <U>는 이 메서드에서만 쓴다. 클래스가 아니라 메서드에 선언해야 하고, 반환 타입 앞에 붙은 <U>가 그 선언이다. 빠뜨리면 컴파일 오류가 난다.
조건에 맞는 것만 남기는 filter는 boolean을 돌려주는 Predicate를 받는다.
public MyCollection<T> filter(Predicate<T> predicate) {
List<T> newList = new ArrayList<>();
foreach(data -> {
if (predicate.test(data)) newList.add(data);
});
return new MyCollection<>(newList);
}java.util.function에 있는 넷
자주 쓰는 형태는 java.util.function에 미리 만들어져 있고, 입력과 출력이 있느냐 없느냐로 갈린다.
| 인터페이스 | 메서드 | 입력 | 출력 |
|---|---|---|---|
Consumer<T> | accept(T t) | 있다 | 없다 |
Supplier<T> | get() | 없다 | 있다 |
Function<T,R> | apply(T t) | 있다 | 다른 타입으로 |
Predicate<T> | test(T t) | 있다 | boolean |
이 넷이 스트림의 연산에 그대로 대응된다. 원소마다 무언가 하고 반환이 없는 forEach가 Consumer를, 원소를 다른 것으로 바꾸는 map이 Function을, 조건에 맞는지 판단하는 filter가 Predicate를 받는다. 아무것도 받지 않고 값을 만들어내는 Stream.generate에는 Supplier가 들어간다.