자바가 람다를 지원하면서 API 작성 스타일이 변경되었습니다.
- 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴보다
- 함수 객체를 받는 정적 팩토리나 생성자를 제공하는 스타일을 권장
이에 따라 저자는 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다고 주장합니다.
LinkedHashMap
Collections 라이브러리에서 제공하는 LinkedHashMap를 보면 노드를 삽입한 후 removeEldestEntry() 메서드를 호출해 캐시에서 가장 오래된 데이터를 지울 것인지에 대한 여부를 확인합니다.
removeEldestEntry()는 protected 메서드로 사용하는 쪽에서 재정의해줘야 합니다.
1. removeEldestEntry() 메서드를 재정의한 코드
removeEldestEntry를 다음과 같이 재정의하면 맵에 원소가 2개가 될 때까지 넣기만 하다가, 그 이상이 되면 새로운 키가 더해질 때마다 가장 오래된 원소를 하나씩 제거합니다.
2. 직접 정의한 함수형 인터페이스를 통해 removeEldestEntry() 메서드를 재정의한 코드
1번 코드도 잘 동작하지만 람다를 사용하면 훨씬 잘 해낼 수 있습니다.
다음은 직접 정의한 함수형 인터페이스를 적용한 코드입니다.
코드 부연 설명
- removeEldestEntry는 size()를 호출해 맵 안의 원소 수를 알아내는데, removeEldestEntry가 인스턴스 메서드라 가능한 방법
- 반면, 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니기 때문에
- 팩토리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않아 맵은 자기 자신도 함수 객체에 건네줘야 함
- this는 현재 객체, 즉 InterfaceLinkedHashMap 객체를 가리킴
- removeEldestEntry 메서드는 LinkedHashMap에서 상속받은 메서드를 오버라이드한 것이므로, 해당 메서드가 속한 객체가 바로 InterfaceLinkedHashMap
- removeEldestEntry 메서드에서 removalFunction.remove(this, eldest)를 호출할 때 this를 넘기는 이유는 함수 객체가 현재 맵의 상태에 접근하고 조작할 수 있도록 하기 위함
- remove 메서드는 인터페이스에서 정의되었으며, 이를 구현하는 구체적인 클래스에서는 실제로 어떤 일이 일어날지를 결정
- 따라서 remove 메서드에게 현재 맵에 대한 참조를 전달함으로써 함수 객체가 필요한 작업을 수행
3. 자바 표준 라이브러리에서 제공하는 함수형 인터페이스를 통해 removeEldestEntry() 메서드를 재정의한 코드
2번 코드도 잘 동작하지만 자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있기 때문에 필요한 용도에 맞는 표준 인터페이스가 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하는 것을 권장합니다.
표준 함수형 인터페이스
표준 함수형 인터페이스는 총 43개이며, 기본 인터페이스 6개만 숙지하면 나머지 인터페이스들을 유추할 수 있는 시스템입니다.숙지해야 하는 여섯 가지 인터페이스는 다음과 같습니다.
- UnaryOperator
- BinaryOperator
- Predicate
- Function
- Supplier
- Consumer
1. UnaryOperator
- 매개변수 타입, 반환 타입이 같음
- 매개변수가 한 개
인터페이스 | 메서드 |
UnaryOperator | T apply(T t) |
IntUnaryOperator | int applyAsInt(int value) |
LongUnaryOperator | long applyAsLong(long value) |
DoubleUnaryOperator | double applyAsDouble(double value) |
2. BinaryOperator
- 매개변수 타입, 반환 타입이 같음
- 매개변수가 두 개
인터페이스 | 메서드 |
BinaryOperator | T apply(T t1, T t2) |
IntBinaryOperator | int applyAsInt(int value1, int value2) |
LongBinaryOperator | long applyAsLong(long value1, long value2) |
DoubleBinaryOperator | double applyAsDouble(double value1, double value2) |
3. Predicate
- 매개변수가 한 개이며, boolean 반환
인터페이스 | 메서드 |
Predicate | boolean test(T t) |
BiPredicate<T, U> | boolean test(T t, U u) |
IntPredicate | boolean test(int value) |
LongPredicate | boolean test(long value) |
DoublePredicate | boolean test(double value) |
4. Function
- 매개변수 타입과 반환 타입이 다름
인터페이스 | 메서드 |
Function<T, R> | R apply(T t) |
BiConsumer<T, U, R> | R apply(T t, U u) |
IntFunction | int apply(int value) |
LongFunction | long apply(long value) |
DoubleFunction | double apply(double value) |
IntToLongFunction | long applyAsLong(int value) |
IntToDoubleFunction | double applyAsDouble(int value) |
LongToIntFunction | int applyAsInt(long value) |
LongToDoubleFunction | double applyAsDouble(long value) |
DoubleToIntFunction | int applyAsInt(double value) |
DoubleToLongFunction | long applyAsLong(double value) |
ToIntFunction | int applyAsInt(T value) |
ToLongFunction | long applyAsLong(T value) |
ToDoubleFunction | double applyAsDouble(T value) |
ToPIntFunction<T, U> | int applyAsInt(T t, U u) |
ToPLongFunction<T, U> | long applyAsLong(T t, U u) |
ToPDoubleFunction<T, U> | double applyAsDouble(T t, U u) |
5. Supplier
- 매개변수가 필요하지 않고, 반환값만 존재
인터페이스 | 메서드 |
Supplier | T get() |
BooleanSupplier | boolean getAsBoolean() |
IntSupplier | int getAsInt() |
LongSupplier | long getAsLong() |
DoubleSupplier | double getAsDouble() |
6. Consumer
- 매개변수는 필요하지만 반환값이 없음
인터페이스 | 메서드 |
Consumer | void accept(T t) |
BiConsumer<T, U> | void accept(T t, U u) |
IntConsumer | void accept(int value) |
LongConsumer | void accept(long value) |
DoubleConsumer | void accept(double value) |
ObjIntConsumer | void accept(T t, int value) |
ObjLongConsumer | void accept(T t, long value) |
ObjDoubleConsumer | void accept(T t, double value) |
* 주의: 표준 함수형 인터페이스 대부분은 기본 타입만 지원하는데 Wrapper 타입을 건넬 경우 오토박싱에 의해 성능 저하를 유발할 수 있음
직접 정의한 커스텀 함수형 인터페이스
대부분 상황에서는 직접 작성하는 것보다 표준 함수형 인터페이스를 사용하는 편이 낫지만 다음과 같은 케이스에는 직접 정의해야 합니다.
- 표준 인터페이스 중 필요한 용도에 맞는 것이 없을 때
- 구조적으로 똑같은 표준 함수형 인터페이스가 있지만 다음 조건을 따르는 경우
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명
- 반드시 따라야 하는 규약 존재
- 유용한 디폴트 메서드를 제공
1. 표준 인터페이스 중 필요한 용도에 맞는 것이 없을 때
2. 구조적으로 똑같은 표준 함수형 인터페이스가 있지만 특정 조건을 따르는 경우
Comparator가 해당 조건에 부합합니다.
- 정렬을 위한 비교는 자주 쓰이며 Comparator 이름 자체가 비교를 위한 클래스임이 자명함
- 첫 번째 매개변수가 두 번째 매개변수가 작을 경우 음수, 같을 경우 0, 클 경우 양수를 반환하는 규약 존재
- reverse()와 같은 유용한 디폴트 메서드 제공
@FunctionalInterface
직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 어노테이션을 적용해야 합니다.
- 해당 클래스의 코드나 설명 문서를 읽을 개발자들에게 해당 인터페이스가 람다용으로 설계된 것임을 알림
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게끔 강제
- 유지보수 과정에서 다른 개발자가 실수로 메서드를 추가하지 못하도록 막음
참고
이펙티브 자바
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 46] 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2024.03.03 |
---|---|
[아이템 45] 스트림은 주의해서 사용하라 (0) | 2024.03.02 |
[아이템 43] 람다보다는 메서드 참조를 사용하라 (0) | 2024.03.02 |
[아이템 42] 익명 클래스보다는 람다를 사용하라 (1) | 2024.03.02 |
[아이템 41] 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2024.02.29 |