-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Discussed in https://github.com/orgs/Study-2-Effective-Java/discussions/101
Originally posted by bunsung92 February 3, 2023
📝 구성
0. 들어가기에 앞서 🤔
자바가 람다를 지원하면서 API를 작성하는 모범 사례도 크게 바뀌었다.
상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴
에서 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 방식으로 바뀌게 되었다. 일반화 해보자면 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다는 말이다.
중요한 점은 매개변수의 함수 객체 타입
을 올바르게 선택해야 한다.
1. Version1 removeEldestEntry() 구현하기 - Basic
LinkedHashMap
을 이용하여 캐시
를 구현한다고 생각해보자.
캐시란? 🧐
값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 보다 빨리 처리될 수 있도록 하는 저장소이다.
removeEldestEntry()
를 @Override
하면 캐시로 사용할 수 있다.
- 해당 메서드가 캐시로 이용될 수 있는 이유는 Map 객체가 캐시로 사용될 때 이 메서드를 재정의하여, 새로운 엔트리가 추가될 때마다
가장 오래된 엔트리를 제거하여 Map 객체가 적절한 크기를 유지할 수 있기 때문이다.
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
직접 구현해 보자
먼저 MyCache 제네릭 클래스
import java.util.LinkedHashMap;
import java.util.Map.Entry;
public class MyCache<K, V> extends LinkedHashMap<K, V> {
private final int limitSize;
public MyCache(int limitSize) {
this.limitSize = limitSize;
}
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return size() > this.limitSize;
}
}
Main 클래스
public class Main {
public static void main(String[] args) {
MyCache<String, Integer> cache = new MyCache<>(3);
cache.put("1", 1);
cache.put("2", 2);
cache.put("3", 3);
cache.put("4", 4);
System.out.println(String.join(", ", cache.keySet()));
}
}
2. Version2 removeEldestEntry() 구현하기 - @FunctionalInterface
MyCache2 제네릭 클래스
import java.util.LinkedHashMap;
import java.util.Map.Entry;
public class MyCache2<K, V> extends LinkedHashMap<K, V> {
private final EldestEntryRemovalFunction<K, V> eldestEntryRemovalFunction;
public MyCache2(EldestEntryRemovalFunction<K, V> eldestEntryRemovalFunction) {
this.eldestEntryRemovalFunction = eldestEntryRemovalFunction;
}
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return eldestEntryRemovalFunction.remove(this, eldest);
}
}
EldestEntryRemovalFunction 인터페이스
import java.util.Map;
public interface EldestEntryRemovalFunction<K, V> {
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
main 메서드 - 달라진 부분만 기록
MyCache2<String, Integer> cache2 = new MyCache2<>(((map, eldest) -> map.size() > 3));
// 이하 동일
자 이렇게 함수형 인터페이스를 직접 정의해서 사용할 수 있다.
하지만 EldestEntryRemovalFunction<K, V>
인터페이스의 구조는 이미 표준 자바 함수형 인터페이스에서 정의하고 있다.
3. Version3 removeEldestEntry() 구현하기 - 표준 함수형 인터페이스 ✨
MyCache3 제네릭 클래스
public class MyCache3<K, V> extends LinkedHashMap<K, V> {
private final BiPredicate<Map<K, V>, Map.Entry<K, V>> biPredicate;
public Cache(BiPredicate<Map<K, V>, Map.Entry<K, V>> biPredicate) {
this.biPredicate = biPredicate;
}
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return biPredicate.test(this, eldest);
}
}
// 나머지 동일
- 자 다시 돌아와서 해당 메서드를 변경하면서 주제를 되짚어보자.
표준 함수형 인터페이스를 사용하라
- 자바에서 정의하고 있는 인터페이스는
java.util.function
패키지에 정의 되어있다. 해당 패키지는 43개의 인터페이스가 담겨 있다. (6개의 중요 인터페이스를 통해 종류를 정의 해보자)
3.1 표준 함수형 인터페이스의 종류
3.1.1 UnOperator<T>
: 인수 1개, 인수의 타입 == 반환 타입
- 함수 시그니처
T apply(T t)
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
/**
* Returns a unary operator that always returns its input argument.
*
* @param <T> the type of the input and output of the operator
* @return a unary operator that always returns its input argument
*/
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
- 사용 예
String::toLowerCase
- 변형
- 기본타입용
DoubleUnaryOperator
,IntUnaryOperator
,LongUnaryOperator
- 기본타입용
3.1.2 BinaryOperator<T>
: 인수 2개, 인수의 타입 == 반환 타입
- 함수 시그니처
T apply(T t1, T t2)
package java.util.function;
import java.util.Objects;
import java.util.Comparator;
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
/**
* Returns a {@link BinaryOperator} which returns the lesser of two elements
* according to the specified {@code Comparator}.
*
* @param <T> the type of the input arguments of the comparator
* @param comparator a {@code Comparator} for comparing the two values
* @return a {@code BinaryOperator} which returns the lesser of its operands,
* according to the supplied {@code Comparator}
* @throws NullPointerException if the argument is null
*/
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
/**
* Returns a {@link BinaryOperator} which returns the greater of two elements
* according to the specified {@code Comparator}.
*
* @param <T> the type of the input arguments of the comparator
* @param comparator a {@code Comparator} for comparing the two values
* @return a {@code BinaryOperator} which returns the greater of its operands,
* according to the supplied {@code Comparator}
* @throws NullPointerException if the argument is null
*/
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
- 사용 예
BigInteger::add
- 변형
- 기본타입용
DoubleBinaryOperator
,IntBinaryOperator
,LongBinaryOperator
- 기본타입용
3.1.3 Predicate<T>
: 인수 1개, 반환타입 == boolean
- 함수 시그니처
boolean test(T t)
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
// 다른 메서드 ...
}
- 사용 예
Collection::isEmpty
- 변형
- 기본 타입용
DoublePredicate
,IntPredicate
,LongPredicate
- 인수를 2개 받고 boolean 반환
BiPredicate<T, U>
- 기본 타입용
3.1.4 Function<T, R>
: 인수의 타입 != 반환 타입
- 함수 시그니처
R apply(T t)
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
- 사용 예
Arryas::asList
- 변형
- 입력은 기본타입, 출력은 R 타입:
DoubleFunction<R>
,IntFunction<R>
,LongFunction<R>
- 입력과 출력 모두 기본 타입(..To..Function) : LongToIntFunction, DoubleToLongFunction 등등
- 출력이 기본 타입:
ToDoubleFunciton<T>
,ToIntFunction<T>
,ToLongFunction<T>
- 인수를 2개 받고 R 타입 반환: BiFunction<T, U, R>
- 인수를 2개 받고 기본 타입 반환:
ToDoubleBiFunction<T, U>
,ToIntBiFunciton<T, U>
,ToLongBiFunction<T, U>
- 입력은 기본타입, 출력은 R 타입:
3.1.5 Consumer<T>
: 인수 1개, 반환 X
- 함수 시그니처
void accept(T t)
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
}
- 사용 예
System.out::println
- 변형
- 기본타입용
DoubleConsumer
,IntConsumer
,LongConsumer
- 인수 2개 받는
BiConsumer<T, U>
- T 타입, 기본타입 받는
ObjDoubleConsumer<T>
,ObjIntConsumer<T>
,ObjLongConsumer<T>
- 기본타입용
3.1.6 Supplier<T>
: 인수 X, 반환 O
- 함수 시그니처
T get()
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
- 사용 예
Instant::now
- 변형
- 기본 타입용
DoubleSupplier
,IntSupplier
,LongSupplier
,BooleanSupplier
- 기본 타입용
3.2 표준 함수형 인터페이스의 도입시 주의 사항 🚨
3.2.1 기본 함수형 인터페이스에 박싱된 기본 타입
을 넣어 사용하지 말자
- 계산량이 많을 때는 성능이 느려질 수 있다.
Funciton<Integer, Double> 보다는 IntDoubleFunction
Supplier<Long> 보다는 LongSupplier
등등
3.2.2 표준 함수형 인터페이스 대신 직접 구현해야 할 때도 있다.
Comparator<T>
인터페이스를 생각 해보자.
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
//이하 생략
}
구조적으로는 ToIntBiFunction<T, U>
와 동일하다. 근데 왜 따로 정의 해 두었을까? 🤔
- API에서 자주 사용되는데, 지금의 이름이 그 용도를 잘 설명 해준다.
구조적으로는 ToIntBiFunction<T,U>와 같지만 Comparator라는 이름이 훨씬 명확하다
- 구현하는 쪽에서 반드시 지켜야 할 규약을 담고있다.
compare()
는 따라야 하는 규약이 많다
- 비교자들을 변환하고 조합해주는 유용한
디폴트 메서드
를 담고있다.
reversed()
,thenComparing()
등등의 메서드를 제공한다
정리하자면 아래의 내용중 하나라도 만족하는 경우가 있다면 함수형 인터페이스를 구현할 것을 고민해 볼 수 있다.
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명 해준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공할 수 있다.
함수형 인터페이스도 인터페이스이다. 도입을 하기로 마음 먹었다면 #52 을 고려한 뒤에 도입해야 한다.
4. 직접 함수형 인터페이스를 만들 때 주의사항 🚨
4.1 @FunctionalInterface
를 붙여야 한다.
- 해당 인터페이스가 람다용으로 설계된 것임을 명확하게 알려준다.
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일이 되도록 한다.
- 누군가 실수로 메서드를 추가할 수 없게 막아준다.
4.2 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드를 오버로딩하면 안된다.
잘못 설계된 예는 자바에 있다.
public interface ExecutorService extends Executor {
// ...
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
// ...
}
- submit 메서드는
Callable<T>
를 받는 것과Runnable
을 받는 것을 다중 정의했다. - 올바른 메서드를 알려주기 위해 형변환해야 할 때가 생긴다.(아이템 52)
5. 핵심 정리 📚
- 자바도 람다를 지원한다. 그말은 API를 설계할 때 람다도 염두해 두자.
java.util.function
패키지의 표준 함수형 인터페이스에 정의 되어있다면 해당 패키지를 이용하자.혹시나
,그럴 경우가 드물겠지만
직접 새로운 함수형 인터페이스를 만들어 쓰는게 좋을 수 있다면 해당 글로 돌아와 주의사항을 체크 해 보자.
6. 회고 🧹
2023.02.06
- 오랜만에 함수형 인터페이스를 다시 읽어본 것 같다.
- 짧은 글로 정리 하기에는 객체지향을 고집하고 있는
Java
에서 함수형 코드를 도입하게 되었기 때문에 보다 심도 깊은 내용이 필요하다 생각했다. - 한번쯤은 왜 자바 진영에서 함수형 인터페이스를 도입하게 되었는지 고민 해 볼 필요가 있다고 생각한다.