🤔 서론
우테코 오늘자 강의에서 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);
}
🔻 다양한 종류의 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을 가져왔기 때문에..
위 예제로선 상태를 따로 저장하고 있는 것이 더 좋다고 생각했다.
참고
'Develop > Java' 카테고리의 다른 글
[Java] 중첩 클래스의 종류 (feat. 멤버 클래스는 static으로!) (0) | 2022.03.14 |
---|---|
[Java] 접근 권한을 최소화해야 하는 이유 (0) | 2022.03.06 |
[JUnit5] 중복되는 테스트 코드 줄이기 (0) | 2022.03.02 |
[Java] 인스턴스화 방지를 위해 private 생성자 이용하기 (3) | 2022.02.26 |
[Java] EnumMap이란? (4) | 2022.02.25 |