자바가 람다를 지원하면서 API 작성 스타일이 변경되었습니다.
- 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴보다
- 함수 객체를 받는 정적 팩토리나 생성자를 제공하는 스타일을 권장
이에 따라 저자는 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다고 주장합니다.
LinkedHashMap
Collections 라이브러리에서 제공하는 LinkedHashMap를 보면 노드를 삽입한 후 removeEldestEntry() 메서드를 호출해 캐시에서 가장 오래된 데이터를 지울 것인지에 대한 여부를 확인합니다.

removeEldestEntry()는 protected 메서드로 사용하는 쪽에서 재정의해줘야 합니다.

1. removeEldestEntry() 메서드를 재정의한 코드
removeEldestEntry를 다음과 같이 재정의하면 맵에 원소가 2개가 될 때까지 넣기만 하다가, 그 이상이 되면 새로운 키가 더해질 때마다 가장 오래된 원소를 하나씩 제거합니다.
public class MyLinkedHashMap extends LinkedHashMap<Integer, String> { | |
private final int size; | |
MyLinkedHashMap(int size) { | |
this.size = size; | |
} | |
@Override | |
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) { | |
return size() > size; | |
} | |
public static void main(String[] args) { | |
MyLinkedHashMap map = new MyLinkedHashMap(2); | |
map.put(1, "하나"); | |
System.out.println(map); | |
map.put(2, "둘"); | |
System.out.println(map); | |
map.put(3, "셋"); | |
System.out.println(map); | |
} | |
} |

2. 직접 정의한 함수형 인터페이스를 통해 removeEldestEntry() 메서드를 재정의한 코드
1번 코드도 잘 동작하지만 람다를 사용하면 훨씬 잘 해낼 수 있습니다.
다음은 직접 정의한 함수형 인터페이스를 적용한 코드입니다.
public interface EldestEntryRemovalFunction<K, V> { | |
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest); | |
} | |
public class InterfaceLinkedHashMap extends LinkedHashMap<Integer, String> { | |
private final EldestEntryRemovalFunction<Integer, String> removalFunction; | |
InterfaceLinkedHashMap(EldestEntryRemovalFunction<Integer, String> removalFunction) { | |
this.removalFunction = removalFunction; | |
} | |
@Override | |
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) { | |
return removalFunction.remove(this, eldest); | |
} | |
public static void main(String[] args) { | |
InterfaceLinkedHashMap map = new InterfaceLinkedHashMap( | |
(m, eldest) -> m.size() > 2 | |
); | |
map.put(1, "하나"); | |
System.out.println(map); | |
map.put(2, "둘"); | |
System.out.println(map); | |
map.put(3, "셋"); | |
System.out.println(map); | |
} | |
} |

코드 부연 설명
- removeEldestEntry는 size()를 호출해 맵 안의 원소 수를 알아내는데, removeEldestEntry가 인스턴스 메서드라 가능한 방법
- 반면, 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니기 때문에
- 팩토리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않아 맵은 자기 자신도 함수 객체에 건네줘야 함
- this는 현재 객체, 즉 InterfaceLinkedHashMap 객체를 가리킴
- removeEldestEntry 메서드는 LinkedHashMap에서 상속받은 메서드를 오버라이드한 것이므로, 해당 메서드가 속한 객체가 바로 InterfaceLinkedHashMap
- removeEldestEntry 메서드에서 removalFunction.remove(this, eldest)를 호출할 때 this를 넘기는 이유는 함수 객체가 현재 맵의 상태에 접근하고 조작할 수 있도록 하기 위함
- remove 메서드는 인터페이스에서 정의되었으며, 이를 구현하는 구체적인 클래스에서는 실제로 어떤 일이 일어날지를 결정
- 따라서 remove 메서드에게 현재 맵에 대한 참조를 전달함으로써 함수 객체가 필요한 작업을 수행
3. 자바 표준 라이브러리에서 제공하는 함수형 인터페이스를 통해 removeEldestEntry() 메서드를 재정의한 코드
2번 코드도 잘 동작하지만 자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있기 때문에 필요한 용도에 맞는 표준 인터페이스가 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하는 것을 권장합니다.
class BiPredicateLinkedHashMap extends LinkedHashMap<Integer, String> { | |
private final BiPredicate<Map<Integer, String>, Map.Entry<Integer, String>> removalBiPredicate; | |
BiPredicateLinkedHashMap(BiPredicate<Map<Integer, String>, Map.Entry<Integer, String>> removalBiPredicate) { | |
this.removalBiPredicate = removalBiPredicate; | |
} | |
@Override | |
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) { | |
return removalBiPredicate.test(this, eldest); | |
} | |
public static void main(String[] args) { | |
BiPredicateLinkedHashMap map = new BiPredicateLinkedHashMap( | |
(m, eldest) -> m.size() > 2 | |
); | |
map.put(1, "하나"); | |
System.out.println(map); | |
map.put(2, "둘"); | |
System.out.println(map); | |
map.put(3, "셋"); | |
System.out.println(map); | |
} | |
} |

표준 함수형 인터페이스
표준 함수형 인터페이스는 총 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) |
@Test | |
void UnaryOperatorTest() { | |
UnaryOperator<String> unaryOperator = String::toLowerCase; | |
assertThat(unaryOperator.apply("JAIMEMIN")).isEqualTo("jaimemin"); | |
} |
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) |
@Test | |
void BinaryOperatorTest() { | |
BinaryOperator<BigInteger> binaryOperator = BigInteger::add; | |
assertThat(binaryOperator.apply(BigInteger.ZERO, BigInteger.ZERO)).isEqualTo(BigInteger.valueOf(0L)); | |
} |
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) |
@Test | |
void PredicateTest() { | |
Predicate<Collection> predicate = Collection::isEmpty; | |
assertThat(predicate.test(new HashSet())).isTrue(); | |
} |
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) |
@Test | |
void functionTest() { | |
Function<int[], List<Integer>> function = arr -> | |
IntStream.of(arr) | |
.boxed() | |
.collect(Collectors.toList()); | |
int[] array = {1, 2, 3, 4, 5}; | |
assertThat(function.apply(array).size()).isEqualTo(5); | |
} |
5. Supplier
- 매개변수가 필요하지 않고, 반환값만 존재
인터페이스 | 메서드 |
Supplier | T get() |
BooleanSupplier | boolean getAsBoolean() |
IntSupplier | int getAsInt() |
LongSupplier | long getAsLong() |
DoubleSupplier | double getAsDouble() |
@Test | |
void SupplierTest() { | |
Instant past = Instant.now().minusMillis(1); | |
Supplier<Instant> supplier = Instant::now; | |
assertThat(supplier.get().isAfter(past)).isTrue(); | |
} |
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) |
@Test | |
void ConsumerTest() { | |
OutputStream output = new ByteArrayOutputStream(); | |
System.setOut(new PrintStream(output)); | |
Consumer consumer = System.out::print; | |
consumer.accept("jaimemin"); | |
assertThat(output.toString()).isEqualTo("jaimemin"); | |
} |
* 주의: 표준 함수형 인터페이스 대부분은 기본 타입만 지원하는데 Wrapper 타입을 건넬 경우 오토박싱에 의해 성능 저하를 유발할 수 있음
직접 정의한 커스텀 함수형 인터페이스
대부분 상황에서는 직접 작성하는 것보다 표준 함수형 인터페이스를 사용하는 편이 낫지만 다음과 같은 케이스에는 직접 정의해야 합니다.
- 표준 인터페이스 중 필요한 용도에 맞는 것이 없을 때
- 구조적으로 똑같은 표준 함수형 인터페이스가 있지만 다음 조건을 따르는 경우
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명
- 반드시 따라야 하는 규약 존재
- 유용한 디폴트 메서드를 제공
1. 표준 인터페이스 중 필요한 용도에 맞는 것이 없을 때
@FunctionalInterface | |
public interface TriPredicate<T, U, V> { | |
boolean test(T t, U u, V v); | |
} |
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 |