함수형 인터페이스에 제네릭을 사용하면 다음과 같은 이점이 있다:
기존 방식인 Object 타입을 사용하는 ObjectFunction은 다양한 타입을 다룰 수 있지만, 매번 다운캐스팅이 필요하고, 이는 런타임에 에러 발생 가능성을 높인다.
반면 제네릭을 사용하면:
// 기존 방식 (다운캐스팅 필요)
interface ObjectFunction {
Object apply(Object input);
}
// 제네릭 방식 (타입 안전성 확보)
interface GenericFunction<T, R> {
R apply(T input);
}
람다는 자체로 타입이 없고, 대입되는 함수형 인터페이스(타겟 타입)에 따라 타입이 결정된다.
@FunctionalInterface
interface FunctionA {
int calculate(int x);
}
@FunctionalInterface
interface FunctionB {
int operate(int x);
}
FunctionA fa = x -> x + 1;
// FunctionB fb = fa; // 오류 발생, 타입 불일치
자바는 다음과 같은 주요 함수형 인터페이스를 기본으로 제공한다:
Function<T, R>: 하나의 매개변수를 받아 결과를 반환.Consumer<T>: 하나의 매개변수를 받아 처리만 하고 반환값은 없음.Supplier<T>: 매개변수 없이 값을 반환.Runnable: 매개변수와 반환값이 없는 작업 실행(주로 스레드 사용).예시 코드:
Function<Integer, String> intToString = Object::toString;
Consumer<String> printer = System.out::println;
Supplier<Double> randomSupplier = Math::random;
Runnable runnable = () -> System.out.println("Hello, Runnable!");
자바는 특정 목적에 적합한 추가적인 특화 인터페이스를 제공한다:
Predicate<T>: 입력값을 받아 조건 검사 후 boolean 반환.UnaryOperator<T>: 같은 타입의 단일 입력을 받아 같은 타입을 반환.BinaryOperator<T>: 같은 타입의 두 입력을 받아 같은 타입을 반환.추가로 2개의 매개변수를 다룰 수 있는 인터페이스들도 제공한다:
BiFunction<T, U, R>BiConsumer<T, U>BiPredicate<T, U>기본형(primitive) 타입을 위한 전용 인터페이스 (IntPredicate, DoubleFunction 등)도 제공한다.
예시 코드:
Predicate<Integer> isEven = x -> x % 2 == 0;
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
자주 사용되는 예제들(filter, map, reduce)에서 직접 정의한 함수형 인터페이스를 자바가 제공하는 인터페이스로 대체하는 예를 살펴보았다.
가장 중요한 것은 코드의 입력과 반환 구조, 그리고 그 의도를 명확히 드러낼 수 있는 인터페이스를 선택하는 것이다.
PredicateUnaryOperatorBinaryOperator람다와 함수형 인터페이스를 적극 활용하면 코드가 간결하고 가독성이 높아진다. 제네릭을 함께 사용하면 추가적으로 재사용성과 타입 안전성까지 얻을 수 있다. 또한 자바에서 제공하는 다양한 기본 함수형 인터페이스를 활용하면 불필요한 인터페이스 생성을 피하고, 호환성 문제를 해결할 수 있다.
무엇보다 코드의 "의도를 명확하게 드러내는" 함수형 인터페이스를 선택하는 것이 핵심이다. 이로써 코드의 목적이 분명해지고, 유지보수성이 향상된다.