[JAVA] 함수형 인터페이스 - Predicate
1. 함수형 인터페이스와 Predicate
먼저 함수형 인터페이스란 추상 메소드를 1개를 가지는 인터페이스를 의미하며 Predicate는 1개의 제네릭 타입 인자를 받아서 boolean 을 리턴하는 추상 메소드를 가지고 있습니다.
아래는 JDK 17 기준에서 Predicate 인터페이스입니다. 추상 메소드는 test이고 그 외에도 and, negate, or, isEqual 등 여러 개의 메소드가 구현되어 있는 것을 확인할 수 있습니다. 여러 개의 메소드가 있어서 함수형 인터페이스가 아니라고 생각할 수 있지만 추상 메소드가 1개 이기 때문에 Predicate는 함수형 인터페이스가 맞습니다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
2. 왜 사용하는지
그럼 이 Predicate와 같은 함수형 인터페이스는 왜 사용하는 것 일까요? 그 이유는 자바에서 람다식을 사용하기 위해서는 함수형 인터페이스를 사용해야 하기 때문입니다.
람다식이란 메소드를 하나의 식으로 표현한 것을 말합니다. 간단한 기능을 수행하거나 일회성의 메소드를 작성하더라도 메소드 명과, 리턴 값 등 고민해야 할 것이 꽤 있습니다. 람다식(= 익명함수)을 사용하게 되면 메소드 명과, 리턴 값을 고민하지 않아도 되기 때문에 개발에 편리한 부분이 있습니다. 람다식에 대해서는 다음 기회에 더 자세히 공부해보도록 하겠습니다.
아래의 예시처럼 1부터 10까지 각각 판별해야 하는 메소드를 만들어야 한다고 가정해봅시다. 이와 같이 특별한 기능도 없고 일회성의 메소드를 메소드 이름을 고민하고, 코드양을 늘려야 할 필요가 있을까요? 이와 같은 경우에 Predicate를 사용하면 훨씬 가독성있게 구현할 수 있습니다.
(물론 아래와 같은 경우는 1개의 메소드로 구현할 수 있겠지요.. 예를 위해 극단적으로 설명해보았습니다..)
boolean isNumberOneCheck(int num) {
return num == 1;
}
boolean isNumberTwoCheck(int num) {
return num == 2;
}
boolean isNumberThreeCheck(int num) {
return num == 3;
}
// 4..
// 5..
// 6..
3. Predicate 사용법
test()
test 메소드는 T 타입의 인자를 받아서 boolean 타입의 결과를 리턴하는 추상 메소드입니다. 아래의 예시에서 "isNumberOneCheck"는 정수 타입의 T 인자를 받아서 해당 정수가 1인지 판별하는 함수형 인터페이스를 만들었습니다. test 메소드를 통해 각각의 인자가 1인지 판별하고 boolean 값을 리턴하고 있습니다.
@Test
@DisplayName("인자가 숫자1인지 판별")
void isNumberOneCheck() {
Predicate<Integer> isNumberOneCheck = (num) -> num == 1;
Assertions.assertEquals(true, isNumberOneCheck.test(1)); ;
Assertions.assertEquals(false, isNumberOneCheck.test(2)); ;
Assertions.assertEquals(false, isNumberOneCheck.test(3)); ;
}
negate()
negate 메소드는 test 결과를 반전시키는 Predicate를 리턴하고 있기 때문에 test로 정의한 메소드와 반대의 결과를 만들 수 있습니다. 아래 예시에서 숫자가 1이면 true, 1이 아니면 false를 리턴하는 Predicate가 정의되어 있지만 negate 메소드를 이용하여 그 결과를 반전시키고 있습니다.
@Test
@DisplayName("인자가 숫자1이면 false, 1이 아니면 true")
void isNumberOneCheck2() {
Predicate<Integer> isNumberOneCheck = (num) -> num == 1;
Assertions.assertEquals(false, isNumberOneCheck.negate().test(1)); ;
Assertions.assertEquals(true, isNumberOneCheck.negate().test(2)); ;
Assertions.assertEquals(true, isNumberOneCheck.negate().test(3)); ;
}
or()
여러 개의 Predicate 를 or 메소드로 연결할 수 있고 연결된 Predicate 중 하나라도 만족하는 Predicate가 있다면 true 리턴하고, 만족하는 Predicate가 하나도 없다면 false를 리턴하게 됩니다. 아래의 예시에서 인자로 넘어온 숫자가 1인지, 2인지, 3인지 판별하는 Predicate가 3개 있습니다. 그리고 각각의 Predicate를 or로 연결하여 인자로 받은 숫자가 1 또는 2 또는 3인지 판별할 수 있습니다.
@Test
@DisplayName("인자가 숫자 1 또는 2 또는 3인지 판별")
void isNumberOneOrTwoOrThree() {
Predicate<Integer> isNumberOneCheck = (num) -> num == 1;
Predicate<Integer> isNumberTwoCheck = (num) -> num == 2;
Predicate<Integer> isNumberThreeCheck = (num) -> num == 3;
Assertions.assertEquals(true, isNumberOneCheck
.or(isNumberTwoCheck)
.or(isNumberThreeCheck)
.test(1));
Assertions.assertEquals(true, isNumberOneCheck
.or(isNumberTwoCheck)
.or(isNumberThreeCheck)
.test(2));
Assertions.assertEquals(true, isNumberOneCheck
.or(isNumberTwoCheck)
.or(isNumberThreeCheck)
.test(3));
Assertions.assertEquals(false, isNumberOneCheck
.or(isNumberTwoCheck)
.or(isNumberThreeCheck)
.test(4));
}
and()
여러 개의 Predicate를 and 로 연결할 수 있고 인자로 전달된 값이 모든 Predicate를 만족하는 경우 true를 반환하고, 그렇지 못한 경우 false를 반환합니다.아래의 예시에서 2의 배수인지 확인하는 Predicate와 3의 배수인지 확인하는 Predicate가 있습니다. 이를 and로 연결한 후 test 메소드를 호출한다면 2와 3의 공배수인 인자만 최종 값으로 true를 반환하게 됩니다.
@Test
@DisplayName("인자가 2와 3의 공배수인지 확인")
void isNumberTwoAndThreeMultiples() {
Predicate<Integer> isTwoMultiples = (num) -> num % 2 == 0;
Predicate<Integer> isThreeMultiples = (num) -> num % 3 == 0;
Assertions.assertEquals(true, isTwoMultiples
.and(isThreeMultiples)
.test(6));
Assertions.assertEquals(true, isTwoMultiples
.and(isThreeMultiples)
.test(12));
Assertions.assertEquals(false, isTwoMultiples
.and(isThreeMultiples)
.test(4));
Assertions.assertEquals(false, isTwoMultiples
.and(isThreeMultiples)
.test(9));
}
isEquals() & not()
Predicate 에서는 static 메소드인 isEqauls와 not 메소드를 지원하고 있습니다. isEquals 메소드는 인자와 같은지 비교하는 Predicate를 생성하여 반환하고, not 메소드는 인자로 받은 Predicate를 반전하는 Predicate를 반환합니다.
아래의 예시에서는 isEquals 메소드를 통해 1과 같은지 비교하는 Predicate를 만들고 test 메소드를 통해 인자가 1인지 판별하고 있습니다.
@Test
@DisplayName("1인지 비교하는 Predicate 생성")
void isEqualPredicate() {
Assertions.assertEquals(true, Predicate.isEqual(1).test(1));
Assertions.assertEquals(false, Predicate.isEqual(1).test(2));
}
반면 아래의 예시는 isEquals 메소드를 통해 1과 같은지 비교하는 Predicate를 만들고 not 메소드를 통해 1과 다른지 비교하는 Predicate를 만들고 1과 다른지 판별하고 있습니다.
@Test
@DisplayName("1과 다른지 비교하는 Predicate 생성")
void isNotPredicate() {
Predicate<Integer> isNumberOneCheck = Predicate.isEqual(1);
Predicate<Integer> isNumberNotOneCheck = Predicate.not(isNumberOneCheck);
Assertions.assertEquals(false, isNumberNotOneCheck.test(1));
Assertions.assertEquals(true, isNumberNotOneCheck.test(2));
}