44. 표준 함수형 인터페이스를 사용하라
📌 표준 함수형 인터페이스를 사용하면 코드가 더 간결하고 이해하기 쉬워지며 재사용성도 높아진다.
📝 핵심 요약 (Overview)
Java 8부터는 람다 표현식과 메서드 참조를 지원하여, 함수를 값처럼 다룰 수 있습니다. 이때 직접 함수형 인터페이스를 정의하기보단, 이미 자바가 제공하는 표준 함수형 인터페이스(java.util.function) 를 사용하는 것이 권장됩니다.
표준 함수형 인터페이스의 중요한 점
코드 가독성 향상: 표준 함수형 인터페이스는 이미 많은 개발자들에게 익숙하므로, 코드를 더 쉽게 이해할 수 있도록 도와줍니다. 예를 들어,
Predicate<T>
를 보면 'T 타입의 객체를 받아서 boolean 값을 반환하는 함수'임을 직관적으로 알 수 있습니다.API 일관성 유지: 자바 API 및 많은 라이브러리에서 표준 함수형 인터페이스를 광범위하게 사용하고 있으므로, 여러분의 코드에서도 이를 활용하면 API의 일관성을 유지하는 데 도움이 됩니다.
불필요한 코드 감소: 필요한 용도에 맞는 표준 함수형 인터페이스가 이미 존재한다면, 굳이 똑같은 기능을 하는 자신만의 함수형 인터페이스를 만들 필요가 없어 코드 양을 줄일 수 있습니다.
Comparator
의 예외:Comparator
인터페이스는 API에서 매우 자주 사용되고, 구현 시 지켜야 할 규약이 명확하며, 비교자를 조합하고 변환하는 유용한 디폴트 메서드들을 포함하고 있기 때문에 독자적인 인터페이스로 유지되는 것이 타당합니다.커스텀 함수형 인터페이스 작성 시: 만약 표준 함수형 인터페이스 중에서 원하는 기능을 제공하는 것이 없다면, 직접 함수형 인터페이스를 작성해야 합니다. 이때는
@FunctionalInterface
애너테이션을 사용하여 해당 인터페이스가 함수형 인터페이스임을 명시하고, 실수로 추상 메서드가 여러 개 추가되는 것을 방지하는 것이 좋습니다.
📚 미리 알아야 할 배경지식 (Prerequisites)
함수형 인터페이스(Functional Interface)는 정확히 하나의 추상 메서드를 가진 인터페이스입니다.
자바의 표준 함수형 인터페이스는 주로 다음과 같습니다.
공급
Supplier
매개변수 없이 값을 제공
T get()
소비
Consumer
값을 소비하고 반환 없음
void accept(T t)
변환
Function<T,R>
값을 다른 값으로 변환
R apply(T t)
조건
Predicate
조건을 검사하여 boolean 반환
boolean test(T t)
단항 연산
UnaryOperator
같은 타입의 입력·출력 연산
T apply(T t)
이항 연산
BinaryOperator
두 개의 입력값을 받아 같은 타입으로 반환
T apply(T t1, T t2)
⚠️ 주의사항 (Cautions)
기본 타입 사용 권장 박싱된 타입(Integer, Double 등)을 사용하면 성능 저하 가능성 존재(아이템 61 참조)
오버로딩 지양 같은 위치의 매개변수로 서로 다른 함수형 인터페이스를 다중 정의하면, 사용자가 형변환을 강요받을 수 있음(아이템 52 참조)
사용자 정의 시 @FunctionalInterface 애너테이션 사용 필수 직접 함수형 인터페이스를 정의할 경우 실수를 방지하고 명시적으로 표현하는 것이 좋음
❌ 잘못된 예제
// 사용자 정의 함수형 인터페이스 (java.util.function.Predicate와 동일한 기능)
interface MyCondition<T> {
boolean check(T t);
}
public static <T> List<T> filter(List<T> list, MyCondition<T> condition) {
List<T> result = new ArrayList<>();
for (T item : list) {
if (condition.check(item)) {
result.add(item);
}
}
return result;
}
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 굳이 MyCondition을 사용할 필요 없이 Predicate를 사용하는 것이 더 좋습니다.
// List<Integer> evenNumbers = filter(numbers, n -> n % 2 == 0);
✅ 좋은 예시 코드
import java.util.List;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;
public class StandardFunctionalInterfaceExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Predicate: 조건을 검사하여 boolean 값을 반환
Predicate<Integer> isEven = number -> number % 2 == 0;
List<Integer> evenNumbers = numbers.stream().filter(isEven).collect(Collectors.toList());
System.out.println("짝수: " + evenNumbers); // 출력:
// UnaryOperator: 하나의 인자를 받아서 같은 타입의 결과를 반환
UnaryOperator<Integer> increment = number -> number + 1;
List<Integer> incrementedNumbers = numbers.stream().map(increment).collect(Collectors.toList());
System.out.println("각 숫자에 1 더하기: " + incrementedNumbers); // 출력:
// BinaryOperator: 두 개의 같은 타입 인자를 받아서 같은 타입의 결과를 반환
BinaryOperator<Integer> sum = (a, b) -> a + b;
int totalSum = numbers.stream().reduce(0, sum);
System.out.println("숫자들의 합: " + totalSum); // 출력: 15
}
}
🔖 기억해야 할 중요한 점 (Important Tips)
이미 존재하는 표준 함수형 인터페이스를 우선적으로 활용
직접 구현 시
@FunctionalInterface
반드시 명시박싱된 타입 사용 자제하고, 기본 타입 사용 고려
불필요한 오버로딩(다중 정의) 피하기
🚧 어려웠던 점과 해결법 (Challenges & Solutions)
다양한 인터페이스의 역할이 헷갈린다? → 인터페이스의 이름을 통해 기억:
Supplier: "공급자"
Consumer: "소비자"
Predicate: "판정자(조건 검사)"
Function: "변환기(입력→출력 변환)"
Operator: "연산자(동일 타입 연산)"
메서드 참조의 개념이 어려웠다면? → "클래스이름::메서드이름" 또는 "객체이름::메서드이름"의 형태로 간단히 생각합시다.
🗣️ 느낀점 (Reflection)
표준 함수형 인터페이스는 처음엔 낯설지만, 익숙해지면 코드를 매우 간결하고 효율적으로 만들어줍니다. 특히 라이브러리와의 호환성이 높아지고, API의 재사용성을 크게 증가시켜주는 것이 가장 큰 장점입니다. 앞으로 적극적으로 활용할 예정입니다.
Last updated