자바가 람다를 지원하면서 API를 작성하는 모범 사례가 크게 바뀌었다. 특히 함수 객체를 사용할 수 있게 되어서, 템플릿 패턴 대신 함수 객체를 매개변수로 넘기는 방법이 선호된다.
템플릿 메소드를 사용하는 예시로 LinkedHashMap의 removeEldestEntry가 있다. LinkedHashMap에 키를 새로 추가하는 put 메소드는 내부적으로 afterNodeInsertion 템플릿 메소드를 호출하고, 이는 같은 인스턴스에 위치하는 removeEldestEntry()를 호출한다. removeEldestEntry()는 put에서 아이템을 추가한 후 기존 원소의 제거 유무를 결정하는데 사용된다. removeEldestEntry를 재정의하면 캐시로 활용할 수 있다.
// HashMap
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evic) {
// 노드 추가..
afterNodeInsertion(evict);
}
// LinkedHashMap
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
// 캐시로 활용하는 경우 - 최신 아이템 100개 유지
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100; // size는 Map의 인스턴스 메소드
}
템플릿 메소드 패턴을 사용하는 대신 removeEldestEntry와 동일한 역할을 하는 함수 객체를 정의해서 afterNodeInsertion에 매개변수로 넘겨줄 수 있다. 이때 필요한 함수 객체의 인터페이스를 정의해야하는데, boolean을 반환해야한다. Map의 size 정보가 필요하므로 매개변수로 Map.Entry<K,V>와 Map<K, V>가 필요하다. 다음과 같은 함수형 인터페이스를 선언한다.
이 함수는 매개변수를 두개 받아 boolean을 반환한다. 이러한 함수 객체는 자바가 미리 정의해둔 표준 함수형 인터페이스인 BiPredicate를 사용해도 된다. 표준 함수형 인터페이스를 사용하면 사용자 정의 함수 인터페이스를 직접 읽어보지 않아도 되기 때문에 가독성과 이해의 측면에서 더 좋다.
표준 함수형 인터페이스 종류
총 43개의 표준 함수형 인터페이스가 있다. 큰 분류는 다음과 같다.
기본 : Operator, Predicate, Function, Supplier, Consumer
매개변수 개수 : 1개(기본), 2개(Bi~~~)
기본타입 변형 : Int~~, Long~~, Double~~
기본 함수형 인터페이스
인터페이스
함수 시그니처
예
설명
UnaryOperator
T apply(T t)
String::toLowerCase
T를 인수로 받아 T를 반환
Predicate
boolean test(T t)
Collection::isEmpty
T를 인수로 받아 boolean을 반환
Function<T,R>
R apply(T t)
Arrays::asList
T를 인수로 받아 R을 반환
Supplier
T get()
Instant::now
인수를 받지 않고 T를 반환
Consumer
void accept(T t)
System.out::println
T를 인수로 받아 값을 반환하지 않음
public class JavaBiConsumer {
static <T> void addTwo(T a1, T a2, BiConsumer<T, T> c) {
c.accept(a1, a2);
}
public static void main(String[] args) {
addTwo(1, 2, (x, y) -> System.out.println(x + y)); // 3
addTwo("Node", ".js", (x, y) -> System.out.println(x + y)); // Node.js
}
}
기본 타입 변형
기본 타입인 int, long, double로 변형이 생김
Operator
T apply(T t1, T t2)
구분 : 기본 타입(int, long, double), 매개변수 개수(Unary, Binary)
종류 : UnaryOperator, BinaryOperator, DoubleUnaryOperator, DoubleBinaryOperator, IntUnaryOperator, IntBinaryOperator, LongUnaryOperator, LongBinaryOperator
Predicate
boolean test(T t)
구분 : 기본 타입(int, long, double)
종류 : Predicate, BiPredicate, IntPredicate, LongPredicate, DoublePredicate
Function
R apply(T t)
구분 : 입력 기본 타입, 입력 반환 타입(INPUTTYPEToRESULTTYPE), 반환타입(ToTYPE)
종류
Function, BiFunction
입력 타입 : IntFunction, LongFunction, DoubleFunction
입력 반환 타입 : IntToLongFunction, IntToDoubleFunction, LongToIntFunction, LongToDoubleFunction, DoubleToIntFunction, DoubleToLongFunction
반환 타입 : ToIntFunction, ToIntBiFunction, ToLongFunction, ToLongBiFunction, ToDoubleFunction, ToDoubleBiFunction
Consumer
void accept(T t)
구분 : 입력 기본 타입(int, long, double), 입력 객체 타입(Object)
종류
Consumer, BiConsumer
입력 하나 : IntConsumer, LongConsumer, DoubleConsumer
입력 둘(with 객체 참조) : ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer
Supplier
T get()
구분 : 반환 기본 타입(int, long, double) + boolean
종류 : Supplier, IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier
새로운 함수형 인터페이스 정의
새로운 함수형 인터페이스를 정의해야하는 경우
자주 사용되며, 이름 자체가 용도를 명확히 설명하는 경우
반드시 따라야하는 규약이 있음
유용한 디폴트 메소드를 제공함
주의사항 : @FunctionalInterface 애너테이션을 사용하자
@Override을 사용하는 것과 같은 맥락
인터페이스의 사용자에게 해당 인터페이스가 람다용으로 설계된 것임을 알리기 위해
인터페이스 수정자가 추상 메소드를 추가하지 않도록 컴파일 단계에서 방지
새로 정의한 함수형 인터페이스 예시 : Comparator<T>
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
형태는 두개의 T 타입 매개변수를 받아 int를 반환하는 ToIntBiFunction과 같다.
그럼에도 불구하고 Comparator을 독자적인 인터페이스로 분리한 이유는 다음과 같다.
Comparator은 API에서 매우 자주 사용되므로 표준 인터페이스의 형태로부터 분리된 고유의 맥락 필요
구현하는 쪽에서 지켜야하는 규약 명시 : 매개변수로 넘기는 타입이 동일해야함
비교자를 변환하고 조합하는 유용한 디폴트 메소드
Comparator 사용 예시
class Student {
int rollno;
// ..
}
// main()
ArrayList<Student> ar = new ArrayList<Student>();
ar.add(new Student(111, "Mayank", "london"));
ar.add(new Student(131, "Anshul", "nyc"));
ar.add(new Student(121, "Solanki", "jaipur"));
ar.add(new Student(101, "Aggarwal", "Hongkong"));
// Collections.sort(List<T> list, Comparator<? super T> c)
Collections.sort(ar, (o1, o2) -> o1.rollno - o2.rollno);
함수형 인터페이스의 활용 예시
자바가 람다를 지원하면서 API를 작성하는 모범 사례가 크게 바뀌었다. 특히 함수 객체를 사용할 수 있게 되어서, 템플릿 패턴 대신 함수 객체를 매개변수로 넘기는 방법이 선호된다.
템플릿 메소드를 사용하는 예시로 LinkedHashMap의 removeEldestEntry가 있다. LinkedHashMap에 키를 새로 추가하는
put
메소드는 내부적으로 afterNodeInsertion 템플릿 메소드를 호출하고, 이는 같은 인스턴스에 위치하는 removeEldestEntry()를 호출한다. removeEldestEntry()는 put에서 아이템을 추가한 후 기존 원소의 제거 유무를 결정하는데 사용된다. removeEldestEntry를 재정의하면 캐시로 활용할 수 있다.템플릿 메소드 패턴을 사용하는 대신 removeEldestEntry와 동일한 역할을 하는 함수 객체를 정의해서 afterNodeInsertion에 매개변수로 넘겨줄 수 있다. 이때 필요한 함수 객체의 인터페이스를 정의해야하는데, boolean을 반환해야한다. Map의 size 정보가 필요하므로 매개변수로 Map.Entry<K,V>와 Map<K, V>가 필요하다. 다음과 같은 함수형 인터페이스를 선언한다.
이 함수는 매개변수를 두개 받아 boolean을 반환한다. 이러한 함수 객체는 자바가 미리 정의해둔 표준 함수형 인터페이스인 BiPredicate를 사용해도 된다. 표준 함수형 인터페이스를 사용하면 사용자 정의 함수 인터페이스를 직접 읽어보지 않아도 되기 때문에 가독성과 이해의 측면에서 더 좋다.
표준 함수형 인터페이스 종류
총 43개의 표준 함수형 인터페이스가 있다. 큰 분류는 다음과 같다.
기본 함수형 인터페이스
기본 타입 변형
기본 타입인 int, long, double로 변형이 생김
Operator
T apply(T t1, T t2)
Predicate
boolean test(T t)
Function
R apply(T t)
INPUTTYPE
ToRESULTTYPE
), 반환타입(ToTYPE
)Consumer
void accept(T t)
Supplier
T get()
새로운 함수형 인터페이스 정의
새로운 함수형 인터페이스를 정의해야하는 경우
유용한 디폴트 메소드를 제공함
주의사항 :
@FunctionalInterface
애너테이션을 사용하자새로 정의한 함수형 인터페이스 예시 :
Comparator<T>
Comparator 사용 예시