본문 바로가기
Develop/Java

[Java] Predicate란?

by 연로그 2022. 3. 4.
반응형

🤔 서론

우테코 오늘자 강의에서 BiPredicate라는 개념을 처음 들어보았다.

Predicate란 무엇인지, 언제 사용하는 것인지 그리고 내 코드에 적용하는 과정까지를 담아본다.

생각보다 어렵지 않다. 설명 안읽고 예제 코드만 봐도 바로 이해할 수 있을듯

 

📚 Predicate란?

  • argument를 받아 boolean 값을 반환하는 함수형 인터페이스
  • functional method: test()

 

🔻 함수형 인터페이스란?

더보기
  • = SAM interface; Single Abstract Method Interface

  • : 1개의 추상 메소드를 갖고 있는 인터페이스
    ➕ default나 static 메소드의 제한 X

  • @FunctionalInterface 어노테이션 사용
    ➕ 없어도 동작하지만 함수형 인터페이스 조건에 부합되는지 검사해주므로 사용하는 것이 좋다.

  • Java8부터 지원된 람다는 함수형 인터페이스로 접근 가능

  • Predicate 외에도 Consumer, Supplier, Function, Comparator 등이 있다.

 

아래 예제에서 Square이 함수형 인터페이스이다.

@FunctionalInterface
interface Square {
    int calculate(int x);
}
  
class Test {
    public static void main(String args[]) {
        Square s = (int x) -> x * x;
        System.out.println(s.calculate(5));
    }
}

 

📑 구성 메소드

Predicate의 코드를 가져왔는데 내부 동작은 특별한 것이 없다.

바로 예제 코드를 봐도 무방하다.

@FunctionalInterface
public interface Predicate<T> {
    // 주어진 arguments를 검증
    boolean test(T t);

    // 다른 Predicate와 연결하는 역할 &&
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    // test()의 반대 결과 반환 (ex: true -> false)
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    // 다른 Predicate와 연결하는 역할 ||
    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);
    }

    @SuppressWarnings("unchecked")
    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>)target.negate();
    }
}

 

📕 예제1 - test()

  • 전달한 argument가 충족되면 true
@Test
public void test() {
    Predicate<Integer> predicate = (num) -> num < 10;
    assertThat(predicate.test(5)).isTrue();
}

성공~!

 

📙 예제2 - and()

  • 두 Predicate를 잇는 역할 ( && )
@Test
public void test() {
    Predicate<Integer> predicate1 = (num) -> num < 10;
    Predicate<Integer> predicate2 = (num) -> num > 5;

    assertThat(predicate1.and(predicate2).test(7)).isTrue();
}

 

📒 예제3 - negate()

  • Predicate.test()의 결과와 반대로 return하는 Predicate 생성
@Test
public void test() {
    Predicate<Integer> originPredicate = (num) -> num < 10;
    Predicate<Integer> negatePredicate = originPredicate.negate();

    assertThat(negatePredicate.test(5)).isFalse();
}

 

📗 예제4 - or()

  • 두 Predicate를 잇는 역할 ( || )
@Test
public void test() {
    Predicate<Integer> predicate1 = (num) -> num < 10;
    Predicate<Integer> predicate2 = (num) -> num > 5;

    assertThat(predicate1.or(predicate2).test(3)).isTrue();	// predicate1만 충족
    assertThat(predicate1.or(predicate2).test(12)).isTrue();	// predicate2만 충족
}

 

📘 예제5 - isEqual()

  • 두 객체가 동일한지 판단
  • Stream에서 사용될 수 있음
@Test
public void test1() {
    Predicate<Integer> predicate = Predicate.isEqual(5);
    assertThat(predicate.test(5)).isTrue();
    assertThat(predicate.test(6)).isFalse();
}

@Test
public void test2() {
    Stream<Integer> stream = IntStream.range(1, 10).boxed();
    stream.filter(Predicate.isEqual(5))
            .forEach(System.out::println);
}

test2()에서 5가 출력됨

 

 

🔻 다양한 종류의 Predicate

더보기

Predicate와 유사한 함수형 인터페이스가 많이 존재한다.

BiPredicate, DoublePredicate, ... 등이 있는데 간단하게 살펴보겠다.

 

우선 BiPredicate와 Predicate는 크게 다르지 않다.

  •  Predicate : Type T 인자를 하나만 받아 boolean을 return
  •  BiPredicate : T, U 인자 두개를 받아 boolean을 return

 

그 외에는 다음과 같다.

  • double 값을 조사하는 DoublePredicate
  • int 값을 조사하는 IntPredicate
  • long 값을 조사하는 LongPredicate

 

내부 구현 코드도 대부분 비슷하다.

모두 test()가 functional method라는 점을 기억하자.

 

💡 리팩토링

이제 BiPredicate를 직접 코드에 적용해보려고 한다.

 

상금, 개수, 보너스 공 일치 여부를 저장하는 Ranking이라는 Enum이 있다.

findRanking() 메소드는 일치하는 개수 공 일치 여부를 파라미터로 보내면 해당하는 Ranking을 반환한다.

요 부분을 BiPredicate를 활용해 리팩토링 해보도록 하자.

 

❗ AS-IS

public enum Ranking {
    FIRST(2000_000_000, 6, false),
    SECOND(30_000_000, 5, true),
    THIRD(1_500_000, 5, false),
    FOURTH(50_000, 4, false),
    FIFTH(5_000, 3, false),
    NONE(0, 0, false);

    private final int prize;
    private final int count;
    private final boolean hasBonusNumber;

    Ranking(int prize, int count, boolean hasBonusNumber) {
        this.prize = prize;
        this.count = count;
        this.hasBonusNumber = hasBonusNumber;
    }

    public static Ranking findRanking(int cnt, boolean hasBonusNumber) {
        return Arrays.stream(Ranking.values())
                .filter(ranking -> ranking.count == cnt && ranking.hasBonusNumber == hasBonusNumber)
                .findAny()
                .orElse(NONE);
    }
}
  • 생성 시 prize, count, hasBonusNumber 초기화
  • findRanking()에서 count와 hasBonusNumber가 일치하는지 체크

 

❗ TO-BE

public enum Ranking {
    FIRST(2000_000_000, (count, bonus) -> count == 6),
    SECOND(30_000_000, (count, bonus) -> count == 5 && bonus),
    THIRD(1_500_000, (count, bonus) -> count == 5 && !bonus),
    FOURTH(50_000, (count, bonus) -> count == 4),
    FIFTH(5_000, (count, bonus) -> count == 3),
    NONE(0, (count, bonus) -> count < 3);

    private final int prize;
    private final BiPredicate<Integer, Boolean> condition;

    Ranking(int prize, BiPredicate<Integer, Boolean> condition) {
        this.prize = prize;
        this.condition = condition;
    }

    public static Ranking findRanking(int cnt, boolean hasBonusNumber) {
        return Arrays.stream(Ranking.values())
                .filter(ranking -> ranking.condition.test(cnt, hasBonusNumber))
                .findAny()
                .orElse(NONE);
    }
}
  • 생성 시 prize와 BiPredicate형인 condition 초기화
  • findRanking()에서 조건을 검사하지 않고 condition의 test 메소드를 통해 일치하는지 체크

 

위와 같이 Enum 안에 속해있는 상수마다 조건이 다른 경우에 활용하면 좋을 것 같다.

 

하지만 나는 최종적으로 AS-IS 코드를 선택했다.

View에서 결과 출력할 String을 만드는 과정에서 count와 hasBonusNumber을 가져왔기 때문에..

위 예제로선 상태를 따로 저장하고 있는 것이 더 좋다고 생각했다.


참고

반응형