stream().filter()에 넣는 람다가 Predicate이고, map()에 넣는 게 Function이라는 건 알겠는데, 이걸 직접 변수에 담아서 조합하면 뭘 할 수 있을까요?

이게 뭔가요?

함수형 인터페이스 는 추상 메서드가 하나뿐인 인터페이스입니다. 람다 표현식의 타겟 타입으로 사용되며, java.util.function 패키지에 43개가 정의되어 있습니다. 그 중 핵심 4종을 다룹니다.

핵심 4종

인터페이스메서드입력출력역할
Predicate<T>test(T)Oboolean조건 판단
Function<T,R>apply(T)OO변환
Consumer<T>accept(T)OX소비 (부수효과)
Supplier<T>get()XO생성

Predicate — 조건 판단

JAVA
Predicate<String> isNotEmpty = s -> !s.isEmpty();
Predicate<String> isLong = s -> s.length() > 10;

// 조합
Predicate<String> isNotEmptyAndLong = isNotEmpty.and(isLong);
Predicate<String> isShortOrEmpty = isNotEmpty.negate().or(isLong.negate());

// Stream에서 활용
List<String> filtered = names.stream()
    .filter(isNotEmpty.and(isLong))
    .toList();

유효성 검증에 활용:

JAVA
public class Validator<T> {
    private final List<Predicate<T>> rules = new ArrayList<>();

    public Validator<T> addRule(Predicate<T> rule) {
        rules.add(rule);
        return this;
    }

    public boolean validate(T target) {
        return rules.stream().allMatch(rule -> rule.test(target));
    }
}

// 사용
Validator<String> passwordValidator = new Validator<String>()
    .addRule(pw -> pw.length() >= 8)
    .addRule(pw -> pw.matches(".*[A-Z].*"))
    .addRule(pw -> pw.matches(".*\\d.*"));

boolean valid = passwordValidator.validate("MyP@ss1"); // true

Function — 변환

JAVA
Function<String, Integer> toLength = String::length;
Function<Integer, String> toStars = n -> "*".repeat(n);

// andThen: 순차 실행 (A → B → C)
Function<String, String> toLengthStars = toLength.andThen(toStars);
toLengthStars.apply("hello"); // "*****"

// compose: 역순 실행 (C → B → A 순으로 적용)
Function<String, String> composed = toStars.compose(toLength);
composed.apply("hello"); // "*****" (같은 결과)

변환 파이프라인:

JAVA
Function<User, UserDto> toDto = user -> new UserDto(
    user.getName(), user.getEmail());
Function<UserDto, String> toJson = dto -> objectMapper.writeValueAsString(dto);

Function<User, String> userToJson = toDto.andThen(toJson);

Consumer — 소비 (부수효과)

JAVA
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info("처리: {}", s);

// andThen: 순차 실행
Consumer<String> printAndLog = print.andThen(log);

// forEach에서 활용
names.forEach(printAndLog);

이벤트 핸들러 패턴:

JAVA
public class EventBus<T> {
    private final List<Consumer<T>> handlers = new ArrayList<>();

    public void subscribe(Consumer<T> handler) {
        handlers.add(handler);
    }

    public void publish(T event) {
        handlers.forEach(handler -> handler.accept(event));
    }
}

Supplier — 생성

JAVA
Supplier<LocalDateTime> now = LocalDateTime::now;
Supplier<UUID> randomId = UUID::randomUUID;
Supplier<List<String>> emptyList = ArrayList::new;

// 지연 초기화
public class Lazy<T> {
    private final Supplier<T> supplier;
    private T value;
    private boolean initialized = false;

    public Lazy(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T get() {
        if (!initialized) {
            value = supplier.get();
            initialized = true;
        }
        return value;
    }
}

// 무거운 객체를 실제 사용 시점에 생성
Lazy<DatabaseConnection> conn =
    new Lazy<>(() -> new DatabaseConnection("url"));

특화 인터페이스

오토박싱을 피하기 위한 기본형 특화 버전:

JAVA
IntPredicate isPositive = n -> n > 0;       // int → boolean
IntFunction<String> intToString = Integer::toString; // int → R
IntConsumer printInt = System.out::println;  // int → void
IntSupplier randomInt = () -> new Random().nextInt(); // () → int

// Bi- 버전 (두 개의 입력)
BiFunction<String, String, Integer> compareLength =
    (a, b) -> a.length() - b.length();
BiConsumer<String, Integer> printEntry =
    (key, value) -> System.out.println(key + "=" + value);
BiPredicate<String, Integer> isLongerThan =
    (str, len) -> str.length() > len;

UnaryOperator, BinaryOperator

입력과 출력 타입이 같을 때 사용합니다.

JAVA
UnaryOperator<String> toUpper = String::toUpperCase;
// Function<String, String>과 동일하지만 더 명확

BinaryOperator<Integer> add = Integer::sum;
// BiFunction<Integer, Integer, Integer>과 동일

// List.replaceAll은 UnaryOperator를 받음
List<String> names = new ArrayList<>(List.of("alice", "bob"));
names.replaceAll(String::toUpperCase); // ["ALICE", "BOB"]

자주 헷갈리는 포인트

  • Predicate vs Function<T, Boolean>: 기능은 같지만 Predicateand(), or(), negate() 조합 메서드를 제공합니다. 조건 판단에는 Predicate를 쓰세요.
  • 체크 예외: 함수형 인터페이스의 추상 메서드는 체크 예외를 선언하지 않습니다. 체크 예외를 던져야 하면 커스텀 함수형 인터페이스를 만들거나 try-catch로 감싸야 합니다.
  • null 반환: Function이 null을 반환하면 andThen()의 다음 함수에서 NPE가 날 수 있습니다. Optional로 감싸는 것을 고려하세요.

정리

인터페이스용도조합 메서드
Predicate조건 판단and, or, negate
Function변환andThen, compose
Consumer소비 (부수효과)andThen
Supplier생성 (지연 초기화)
UnaryOperator같은 타입 변환andThen, compose
BinaryOperator두 값 → 같은 타입andThen

References

댓글 로딩 중...