: 함수형 인터페이스에 정의된 추상 메서드는 람다 표현식의 시그니처를 정의한다. 그리고 이 시그니처를 함수 디스크립터라고 한다.
: 다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다.
⇒ 뭔가 포괄적으로 사용하는 마스터키같은 느낌보다는 화장실 관련 문을 연다거나 창고 관련 문을 여는 키가 필요하다는 느낌같다..
: 자바 8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다. 이 절에는 Predicate, Consumer, Function 인터페이스를 설명한다.
1. Predicate
: Predicate<T> 인터페이스는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환하는 test라는 추상 메서드를 정의한다.
: 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;
}
Predicat<String> nonEmptyStrinPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Java
복사
2. Consumer
: Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.
: 제네릭 형식 T의 객체를 인수로 받아 어떠한 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.
: 다음은 Integer 리스트를 인수로 받아 각 항목에 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)
);
Java
복사
3. 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;
}
// [7,2,6]
List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), (Strin s) -> s.length());
Java
복사
기본형 특화
: 지금까지 세 개의 제네릭 형식을 인수로 받아 처리하는 함수형 인터페이스를 살펴봤다. 하지만 특화된 형식의 함수형 인터페이스도 있다.
: 자바의 모든 형식은 참조형 아니면 기본형에 해당하는데, 제네릭 파라미터를 인수로 받게되면 참조형만 사용할 수 있다. 그렇다면..? 기본형은 못쓰는 걸까?
: 자바에서는 기본형을 참조형으로 변환하는 기능을 제공하는데, 이를 박싱이라고 한다. 참조형을 기본형으로 반환하는 반대 동작을 언박싱이라고 한다.
: 또한 프로그래머가 편리하게 코드를 구현할 수 있게 박싱과 언박싱이 자동으로 이루어지는 오토박싱이라는 기능도 제공한다. 예를 들어 다음은 유효한 코드이다.
List<Integer> list = new ArrayList<>();
for(int k=0; k<500; k++) {
list.add(k); // int가 Integer로 언박싱됨
}
Java
복사
: 하지만 우린 알고 있다. 대가없는 쾌락은 없다는 것을.. 이 작업은 비용이 소모된다. 박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다. 따라서 박싱한 값은 메모리를 참조형보다 더 소모하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.
: 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.
: 예를 들어 아래 예제에서 IntPredicate는 1000이라는 값을 박싱하지 않지만 Predicate<Integer>는 1000이라는 값을 Integer 객체로 박싱한다.
public interface IntPredicate {
boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); // 참(박싱 없음)
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000); // 거짓(박싱)
Java
복사
: 일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 DoublePredicate, IntConsumer와 같이 형식명이 붙는다.
: 다음은 함수형 인터페이스와 람다를 요약한 것이다.
사용 사례 | 람다 예제 | 대응하는 함수형 인터페이스 |
불리언 표현 | (List<String> list) → list.isEmpty() | Predicate<List<String>> |
객체 생성 | () → new Apple(10) | Supplier<Apple> |
객체에서 소비 | (Apple a) → System.out.println(a.getWeight()) | Consumer<Apple> |
객체에서 선택/추출 | (String s) → s.length() | Function<String,Integer> 또는 ToIntFunction<String> |
두 값 조합 | (int a, int b) → a * b | IntBinaryOperator |
두 값 비교 | (Apple a1, Apple a2) → a1.getWeight().compareTo(a2.getWeight()) | Comparator<Apple> 또는 BiFunction<Apple, Apple, Integer> 또는 ToIntBiFunction<Apple, Apple> |