자바가 람다를 지원하면서 API를 작성하는 모범 사례도 크게 바뀌었다.
상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 매서드 패턴의 매력이 줄었다.
⇒ 함수 객체를 받는 정적 팩터리나 생성자를 제공
LinkedHashMap을 생각해보자. LinkedHashMap의 protected 메서드인 removeEldestEntry 를 재정의하면 캐시로 LinkedHashMap을 사용할 수 있다.
맵에 새로운 키를 추가하는 put 메서드는 이 메서드를 호출하여 true가 반환되면 맵에서 가장 오래된 원소를 제거하게 로직을 추가하면 된다.
public class MyLinkedHashMap<K,V> extends LinkedHashMap<K,V> {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100;
}
}
이렇게 로직을 구현하게 되면, Map 에 원소를 넣을 때 100개의 원소가 넘으면 가장 오래된 원소를 하나씩 지우며 원소 100개를 유지하게 된다.
이것을 람다를 이용해서 구현해보자.
LinkedHashMap을 오늘날 다시 구현한다면 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것이다.
removeEldestEntry 에서는 size()를 호출하여 원소의 수를 알아내는데, removeEldestEntry가 인스턴스 메서드라 가능한 방식이다. 하지만 우리가 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니기 때문에 맵은 자기 자신도 함수 객체에 건네줘야 한다.
@FunctionalInterface
public interface EldestEntryRemovalFunction<K, V> {
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}
이 인터페이스는 잘 동작하긴 하지만, 굳이 사용할 이유가 없다. 왜냐하면 java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인퍼테이스가 담겨있기 때문이다.
⇒ 필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하는 것이 좋다.
EldestEntryRemovalFunction 는 표준 인터페이스인 BiPredicate<Map<K,V>, Map.Entry<K,V>>를 사용할 수 있다.
java.util.function 패키지에 있는 기본 인터페이스 6개
- Operator : 반환값과 인수의 타입이 같은 함수를 뜻한다.
=> UnaryOperator : 인수가 1개
=> BinaryOperator : 인수가 2개 - Predicate : 인수 하나를 받아 boolean을 반환하는 함수를 뜻한다.
- Function : 인수와 반환 타입이 다른 함수를 뜻한다.
- Supplier : 인수를 받지 않고 반환하는 함수를 뜻한다.
- Consumer : 인수를 하나 받고 반환값은 없는 함수를 뜻한다.
인터페이스 | 함수 시그니처 | 예 |
UnaryOperator<T> | T apply(T t) | String::toLowerCase |
BinaryOperator<T> | T apply(T t1, T t2) | BigInteger::add |
Predicate<T> | boolean test(T t) | Collection::isEmpty |
Function<T, R> | R apply(T t) | Array::asList |
Supplier<T> | T get() | Instant::now |
Consumer<T> | void accept(T t) | System.out::println |
표준 함수형 인터페이스는 대부분 기본 타입만 지원하지만, 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하는 것은 좋지 않다. (특히 계산량이 많을 때는 성능이 처참히 느려진다.)
직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용해라
- 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
- 그 결과 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
번외..
LinkedHashMap을 상속받는 MyLinkedHashMap을 구현해보자..?
public class MyLinkedHashMap<K,V> extends LinkedHashMap<K,V> {
private EldestEntryRemovalFunction<K, V> eldestEntryRemovalFunction; // 표준 함수형 인터페이스인 BiPredicate 변환하는 것이 좋다.
public MyLinkedHashMap(EldestEntryRemovalFunction<K, V> function){
this.eldestEntryRemovalFunction = function;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return eldestEntryRemovalFunction.remove(this, eldest);
// return size() > 100;
}
}
MyLinkedHashMap<String, String> linkedHashMap = new MyLinkedHashMap<>((map, eldest)-> map.size() > 100);
이런식으로 구현하면 될듯하다?
BiPredicate로 변환하면 이렇게 하면 될런가? 흠흠
public class MyLinkedHashMap<K,V> extends LinkedHashMap<K,V> {
private BiPredicate<Map, Map.Entry> predicate;
public MyLinkedHashMap(BiPredicate<Map, Map.Entry> predicate){
this.predicate = predicate;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return predicate.test(this, eldest);
// return size() > 100;
}
}
'프로그래밍 노트 > Effective 시리즈' 카테고리의 다른 글
null이 아닌, 빈 컬렉션이나 배열을 반환해야 한다. (2) | 2020.01.07 |
---|---|
스트림은 주의해서 사용해야한다. (0) | 2020.01.06 |
람다보다는 메서드 참조를 사용하라. (0) | 2019.12.26 |
익명 클래스보다는 람다를 사용하라. (0) | 2019.12.26 |
규칙4. 객체 생성을 막을 때는 private 생성자를 사용하라 (0) | 2018.11.29 |