java.util.function.Predicate<T> 사용법을 상품 필터링 예제로 정리한 프로젝트입니다.
src/predicate/
├── Product.java # 상품 record (이름, 카테고리, 가격, 재고)
├── ProductPredicates.java # 방식 1: 유틸 클래스에 Predicate<Product> 팩토리 모음
├── ProductCondition.java # 방식 2: Predicate<Product>를 상속한 도메인 인터페이스
└── Main.java # 예제 실행
Predicate<T>는 Java 8에서 추가된 함수형 인터페이스입니다.
값 하나를 받아 조건을 검사하고 boolean을 반환합니다. 즉, "조건"을 객체로 다룰 수 있게 해 줍니다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}추상 메서드가 test() 하나뿐이므로 람다나 메서드 참조로 구현할 수 있습니다.
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> isEmptyRef = String::isEmpty;
isEmpty.test(""); // true
isEmpty.test("abc"); // false추상 메서드가 정확히 1개인 인터페이스를 함수형 인터페이스라고 하며, 람다는 이 추상 메서드 하나를 구현하는 코드가 됩니다.
@FunctionalInterface는 이 조건을 컴파일러가 검사하게 하는 애너테이션입니다.
JDK의 Predicate도 이 형태로 구현되어 있습니다. 추상 메서드는 test() 하나이고, 나머지는 default/static 메서드입니다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); // 추상 메서드 (람다가 구현하는 대상)
default Predicate<T> and(Predicate<? super T> other) // default 메서드
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()
static <T> Predicate<T> isEqual(Object targetRef) // static 메서드
static <T> Predicate<T> not(Predicate<? super T> target)
}| 항목 | 설명 |
|---|---|
| 개수에 포함되는 것 | 추상 메서드 |
| 개수에 포함되지 않는 것 | default 메서드, static 메서드, Object의 public 메서드(equals 등) 재선언 |
| 필수 여부 | 아님. 추상 메서드가 1개이면 애너테이션이 없어도 람다를 쓸 수 있음 |
| 붙이는 이유 | 람다용 인터페이스라는 의도를 드러내고, 추상 메서드가 추가되면 그 자리에서 컴파일 에러가 나게 함 |
@FunctionalInterface
public interface ProductCondition extends Predicate<Product> {
boolean isValid(); // ❌ 컴파일 에러: 추상 메서드가 test()와 isValid() 2개가 됨
}이 프로젝트의 ProductCondition도 추상 메서드를 직접 선언하지 않고 Predicate에게서 test(Product)를 물려받으므로, 추상 메서드가 1개인 함수형 인터페이스입니다.
Function, Consumer, Supplier, Runnable, Comparator 같은 JDK 함수형 인터페이스에도 모두 이 애너테이션이 붙어 있습니다.
람다는 자체 타입이 없고, 놓인 자리에서 기대하는 타입(대입 대상, 파라미터, 반환 타입)을 보고 어떤 인터페이스를 구현하는지 정해집니다.
public static Predicate<Product> nameContains(String keyword) {
return p -> p.name().contains(keyword);
// 반환 타입이 Predicate<Product>이므로
// p는 Product, 람다 본문은 boolean을 반환해야 함
}| 형태 | 가능 여부 |
|---|---|
p -> p.price() > 0 |
O (식 형태) |
p -> { return p.price() > 0; } |
O (블록 형태, return 필요) |
p -> {} |
X (boolean을 반환하지 않으므로 컴파일 에러, 이 형태는 Consumer용) |
람다가 바깥 변수(keyword)를 사용하면 그 값을 캡처합니다.
캡처하는 변수는 effectively final이어야 합니다.
| 메서드 | 의미 | 예시 |
|---|---|---|
and(other) |
AND (단락 평가) | isElectronics.and(hasStock) |
or(other) |
OR (단락 평가) | isBook.or(isCheap) |
negate() |
NOT | hasStock.negate() |
| 메서드 | 의미 | 예시 |
|---|---|---|
Predicate.not(p) (Java 11+) |
부정. 메서드 참조와 함께 쓰기 좋음 | filter(Predicate.not(String::isBlank)) |
Predicate.isEqual(v) |
Objects.equals(v, x) 조건 |
Predicate.isEqual("도서") |
| 인터페이스 | 시그니처 | 용도 |
|---|---|---|
BiPredicate<T, U> |
boolean test(T t, U u) |
인자 2개 |
IntPredicate / LongPredicate / DoublePredicate |
boolean test(int v) 등 |
기본형 전용 (박싱 없음) |
// Stream 필터링
products.stream().filter(nameContains("자바")).toList();
// 컬렉션에서 조건에 맞는 요소 제거
cart.removeIf(inStock().negate());
// Optional 필터링
Optional.of(product).filter(inStock());검색 조건처럼 선택적인 필터가 여러 개일 때, 항상 참인 조건에서 시작해 입력된 조건만 and()로 이어 붙이면 if문을 중첩하지 않아도 됩니다.
Predicate<Product> condition = p -> true;
if (keyword != null) condition = condition.and(nameContains(keyword));
if (onlyInStock) condition = condition.and(inStock());Predicate<Product>는 JDK 인터페이스 Predicate에 타입 인자를 끼운 것일 뿐이라, 여기에 직접 메서드를 추가할 수는 없습니다.
(Java에는 Kotlin/C#의 확장 함수 같은 기능이 없습니다.)
그래서 아래 두 방식 중 하나를 씁니다.
Collections, Paths처럼 정적 팩토리 메서드를 별도 클래스에 모읍니다.
public final class ProductPredicates {
public static Predicate<Product> inStock() {
return p -> p.stock() > 0;
}
}
products.stream().filter(nameContains("자바").and(inStock()));Predicate<Product>를 상속해 "상품 조건"이라는 도메인 타입을 만듭니다.
@FunctionalInterface
public interface ProductCondition extends Predicate<Product> {
static ProductCondition inStock() {
return p -> p.stock() > 0;
}
// 공변 반환 타입으로 재정의해서 조합 후에도 ProductCondition 유지
@Override
default ProductCondition and(Predicate<? super Product> other) {
return p -> test(p) && other.test(p);
}
// 도메인 전용 메서드 추가 가능
default ProductCondition available() {
return and(inStock());
}
}
ProductCondition purchasable = ProductCondition.inCategory("도서")
.or(ProductCondition.inCategory("전자기기"))
.available();and/or/negate를 재정의하지 않으면 반환 타입이 Predicate<Product>로 바뀌어,
그 뒤에는 available() 같은 도메인 메서드를 이어서 호출할 수 없습니다.
| 유틸 클래스 | 상속 인터페이스 | |
|---|---|---|
| 구현 | 단순함 | and/or/negate 재정의 필요 |
| 도메인 의미 | "Product용 조건 모음" | "상품 조건"이라는 도메인 타입 |
| 호환성 | 표준 Predicate 그대로 사용 |
Predicate<Product>가 필요한 곳에 그대로 전달 가능 |
| 확장 | static 메서드만 추가 | 도메인 전용 default 메서드 추가 가능 |
정리: 단순한 조건 모음이면 유틸 클래스로 충분합니다. 조건이 도메인에서 중요한 개념이고 전용 조합 메서드가 필요하면 상속 인터페이스(Specification 패턴과 비슷한 형태)를 고려하세요.
예제마다 메서드를 하나씩 두었고, main()에서 차례로 호출합니다.
| 번호 | 메서드 | 내용 |
|---|---|---|
| 1 | test_example() |
기본 test() |
| 2 | and_example() |
and() |
| 3 | or_example() |
or() |
| 4 | negate_example() |
negate() |
| 5 | not_example() |
Predicate.not() |
| 6 | isEqual_example() |
Predicate.isEqual() |
| 7 | dynamicSearch_example() |
동적 검색 조건 조립 |
| 8 | removeIf_example() |
removeIf()로 품절 상품 제거 |
| 9 | biPredicate_example() |
BiPredicate |
| 10 | intPredicate_example() |
IntPredicate |
| 11 | productCondition_example() |
ProductCondition (상속 인터페이스 방식) |