[첫번째 프로젝트] 11. Predicate, Function

DAEILLIM·2024년 1월 23일
0

첫번째 프로젝트

목록 보기
11/17
post-thumbnail

Predicate, Function

QueryDSL 의 QuerydslPredicateExecutor 부터 학습하면서 Predicate, Function 인터페이스를 만나게 됩니다. 그리고 이전에 정리한 람다와 메소드 레퍼런스도 함께 사용되기 때문에 [첫번째 프로젝트] 3. 람다부터 읽는 것을 권장합니다.

1. Predicate

1.1 Predicate

  • Predicate는 함수형 프로그래밍을 지원하기 위한 인터페이스
  • java.util.function 패키지에 포함
  • Predicate는 주어진 입력에 대해 참 또는 거짓을 반환하는 메소드 test를 가지고 있음

여기서 주의해야 할 점은, Predicate는 단순히 '참' 또는 '거짓'을 반환하는 것뿐만 아니라, 입력 값에 대한 어떠한 조건을 표현할 수 있는 함수로도 사용됩니다.

1.2 Predicate 특징

  1. 단일 메소드 인터페이스(SAM - Single Abstract Method): Predicate는 단일 추상 메소드인 test를 가지고 있습니다. 이 메소드는 제네릭 타입의 매개변수를 받아서 Boolean 값을 반환합니다. 이러한 특성으로 인해 Predicate는 함수형 인터페이스로 간주되며, 람다 표현식이나 메소드 참조를 통해 간결하게 구현할 수 있습니다.

    @FunctionalInterface
    public interface Predicate<T> {
        boolean test(T t);
    }
  2. 논리 연산 가능: Predicate는 여러 조건을 결합하거나 부정할 수 있는 메소드를 제공합니다. and, or, negate와 같은 메소드를 사용하여 Predicate 인스턴스들을 조합할 수 있습니다.

    Predicate<Integer> isEven = x -> x % 2 == 0;
    Predicate<Integer> isPositive = x -> x > 0;
    
    Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
    Predicate<Integer> isEvenOrPositive = isEven.or(isPositive);
    Predicate<Integer> isNotEven = isEven.negate();
  3. 스트림 API와의 통합: Predicate는 자바의 스트림 API에서 주로 사용되며, 컬렉션을 필터링하거나 조건에 따른 처리를 수행할 때 함께 활용됩니다. 예를 들어, filter 메소드의 매개변수로 Predicate를 전달하여 조건에 맞는 요소를 선택할 수 있습니다.

    List<String> words = Arrays.asList("apple", "banana", "orange", "kiwi", "grape");
    
    Predicate<String> isLongWord = s -> s.length() >= 5;
    List<String> longWords = words.stream()
                                 .filter(isLongWord)
                                 .collect(Collectors.toList());
  4. 함수형 프로그래밍 지원: Predicate는 자바 8부터 도입된 함수형 프로그래밍의 핵심 요소 중 하나입니다. 함수형 인터페이스로서 람다 표현식을 통해 간결하게 코드를 작성할 수 있으며, 이는 가독성을 높이고 유지보수를 쉽게 만들어줍니다.

    Predicate<Integer> isPositive = x -> x > 0;

1.3 Predicate 메서드

  1. test(T t) 메소드:

    • 주어진 인자에 대한 조건을 평가하고, 그 결과를 불리언 값으로 반환합니다.

    • 예를 들어, 문자열의 길이가 5보다 작은지 여부를 확인하는 Predicate:

      Predicate<String> isShort = s -> s.length() < 5;
      boolean result = isShort.test("apple");  // false
  2. and(Predicate<? super T> other) 메소드:

    • 현재 Predicate와 다른 Predicate를 논리적 AND로 결합합니다. 두 조건이 모두 참일 때만 새로운 Predicate도 참이 됩니다.

      Predicate<Integer> isEven = x -> x % 2 == 0;
      Predicate<Integer> isPositive = x -> x > 0;
      
      Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
  3. or(Predicate<? super T> other) 메소드:

    • 현재 Predicate와 다른 Predicate를 논리적 OR로 결합합니다. 두 조건 중 하나만 참이어도 새로운 Predicate는 참이 됩니다.

      Predicate<Integer> isEven = x -> x % 2 == 0;
      Predicate<Integer> isPositive = x -> x > 0;
      
      Predicate<Integer> isEvenOrPositive = isEven.or(isPositive);
  4. negate() 메소드:

    • 현재 Predicate의 결과를 부정합니다. 즉, 현재가 참이면 부정된 Predicate는 거짓이 되고, 거짓이면 참이 됩니다.

      Predicate<Integer> isEven = x -> x % 2 == 0;
      Predicate<Integer> isNotEven = isEven.negate();
  5. isEqual(Object targetRef) 메소드:

    • 주어진 객체와 동등한지 여부를 검사하는 Predicate를 생성합니다. equals 메소드를 사용하여 동등성을 판단합니다.

      Predicate<String> isEqualToApple = Predicate.isEqual("apple");
      boolean result = isEqualToApple.test("banana");  // false

1.4 Predicate 예제1

  1. 컬렉션 필터링: 주어진 조건에 따라 컬렉션에서 원하는 요소만 선택할 때 Predicate가 유용합니다. 스트림 API와 함께 사용하면 코드를 간결하게 작성할 수 있습니다.

    List<String> words = Arrays.asList("apple", "banana", "orange", "kiwi", "grape");
    
    // Predicate를 사용하여 길이가 5 이상인 단어만 필터링
    Predicate<String> isLongWord = s -> s.length() >= 5;
    List<String> longWords = words.stream()
                                 .filter(isLongWord)
                                 .collect(Collectors.toList());
  2. 조건부 동작: Predicate는 주어진 조건에 따라 동작을 다르게 구현할 수 있도록 도와줍니다. 예를 들어, 어떤 조건이 참일 때만 특정 동작을 수행하고 그 외의 경우에는 다른 동작을 수행하도록 할 수 있습니다.

    Predicate<Integer> isPositive = x -> x > 0;
    
    // 조건에 따라 다른 동작 수행
    if (isPositive.test(someValue)) {
        // 양수일 때의 동작
    } else {
        // 음수 또는 0일 때의 동작
    }
  3. 검증 및 유효성 검사: Predicate는 입력 값이 특정 조건을 충족하는지 검사하는 데에도 사용됩니다. 예를 들어, 사용자로부터 받은 입력이 유효한지를 검증하는 데에 Predicate를 활용할 수 있습니다.

    Predicate<String> isValidEmail = email -> email.matches("^[a-zA-Z0-9_]+@[a-zA-Z]+\\.[a-zA-Z]+$");
    
    // 이메일 유효성 검사
    if (isValidEmail.test(userInput)) {
        // 유효한 이메일 주소
    } else {
        // 유효하지 않은 이메일 주소
    }

1.5 Predicate 정리

  • 역할 : 매개값을 받고 true / false 리턴
  • 실행 메서드 : test()
  • 매개값을 받아 참/거짓을 단정(predicate) 한다고 생각하면 된다.
인터페이스 형태내용
Predicate<T>T 를 받아 boolean 리턴
BiPredicate<T, U>T, U를 받아 boolean 리턴
XXXPredicateXXX를 받아 boolean 리턴

Predicate 종류

인터페이스 명추상 메소드설명
PredicateBoolean test(T t)객체 T를 조사
BiPredicate<T, U>Boolean test(T t, U u)객체 T와 U를 비교 조사
DoublePredicateBoolean test(double value)double 값을 조사
IntPredicateBoolean test(int value)int 값을 조사
LongPredicateBoolean test(long value)long 값을 조사
class Student {
    String name;
    int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}
public static void main(String[] args) {

    List<Student> list = List.of(
            new Student("홍길동", 99),
            new Student("임꺽정", 76),
            new Student("고담덕", 36),
            new Student("김좌진", 77)
    );

    // int형 매개값을 받아 특정 점수 이상이면 true 아니면 false 를 반환하는 함수 정의
    IntPredicate scoring = (t) -> {
        return t >= 60;
    };

    for (Student student : list) {
        String name = student.name;
        int score = student.score;
		
        // 함수 실행하여 참 / 거짓 값 얻기
        boolean pass = scoring.test(score);
        
        if(pass) {
            System.out.println(name + "님 " + score + "점은 국어 합격입니다.");
        } else {
            System.out.println(name + "님 " + score + "점은 국어 불합격입니다.");
        }
    }
}
Predicate-interface

Predicate 결합

Predicate 함수형 인터페이스는 참 / 거짓 값을 리턴하는 함수를 다룬다. 즉, true / false 조건식에 대하여 이들을 결합하여 and 연산, or 연산을 행한다고 보면 된다.

인터페이스 메소드설명
default Predicate<T> and (Predicate<? super T> other) and 연산
default Predicate<T> or (Predicate<? super T> other)or 연산
default Predicate<T> negate()역 부정
static <T> Predicate<T> isEqual(Object targetRef)객체 비교
Predicate 결합

람다 결합 사용 예시

조건문을 구성할때 if(x > 10 && x < 20) 이런식으로 조건 연산자를 이용해 범위를 구성해본적이 있을 것이다. 이것을 람다 함수로 표현한 것이라고 보면 된다.

public static void main(String[] args) {

    Predicate<Integer> greater = x -> x > 10;
    Predicate<Integer> less = x -> x < 20;

    // x > 10 && x < 20
    Predicate<Integer> between = greater.and(less); 
    System.out.println(between.test(15)); // true

    // x > 10 || x < 20
    Predicate<Integer> all = greater.or(less); 
    System.out.println(all.test(5)); // true

    // x <= 10
    Predicate<Integer> negate = greater.negate(); 
    System.out.println(negate.test(50)); // false
}

isEqual() 정적 인터페이스 메소드는 입력값으로 받은 객체와 같은지 판단해주는 메소드 이다. 그냥 equals() 를 람다 함수로 표현한 것이라고 보면 된다.

public static void main(String[] args) {
    // 함수의 인자로 들어온 문자열이 "홍길동" 인지 판별해주는 함수
    Predicate<String> checkMyName = Predicate.isEqual("홍길동");

    System.out.println(checkMyName.test("임꺽정")); // false
    System.out.println(checkMyName.test("홍길동")); // true
}

2. Function

2.1 Function

  • Function은 자바에서 함수형 프로그래밍을 지원하기 위한 인터페이스
  • java.util.function 패키지에 포함
  • 주로 입력 값을 받아서 출력 값을 생성하는 함수를 표현
  • Function은 하나의 입력을 받아서 하나의 출력을 생성하는 메소드 apply를 가지고 있음

2.2 Function 특징

Function 인터페이스는 다음과 같이 정의되어 있습니다:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

여기서 T는 입력 타입, R은 출력 타입을 나타냅니다.

  1. 입력과 출력 타입 지정:

    • Function은 제네릭으로 정의되어 있어 입력(T)과 출력(R)의 타입을 명시할 수 있습니다. 이를 통해 재사용성이 높은 함수를 만들 수 있습니다.
    Function<String, Integer> strToInt = s -> Integer.parseInt(s);
  2. 단일 추상 메소드:

    • Function은 단일 추상 메소드 apply를 가지고 있습니다. 이 메소드는 주어진 입력에 대해 함수를 적용하고 결과를 반환합니다.
    R apply(T t);
  3. 람다 표현식 사용:

    • 람다 표현식을 통해 간단하게 Function 인터페이스를 구현할 수 있습니다. 이로써 익명 클래스나 별도의 구현 클래스를 작성할 필요가 없어집니다.
    Function<String, Integer> strToInt = s -> Integer.parseInt(s);
  4. 체이닝 (Chaining):

    • andThen 메소드를 사용하여 두 개의 Function을 연결할 수 있습니다. 첫 번째 함수의 결과가 두 번째 함수의 입력으로 전달됩니다.
    Function<String, Integer> strToInt = s -> Integer.parseInt(s);
    Function<Integer, Integer> multiplyByTwo = n -> n * 2;
    
    // 체이닝
    Function<String, Integer> strToIntAndMultiply = strToInt.andThen(multiplyByTwo);
  5. 함수 합성 (Composition):

    • compose 메소드를 사용하여 두 개의 Function을 합성할 수 있습니다. 먼저 적용된 함수의 결과가 나중에 적용되는 함수의 입력으로 사용됩니다.
    Function<String, Integer> strToInt = s -> Integer.parseInt(s);
    Function<Integer, Integer> square = n -> n * n;
    
    // 합성
    Function<String, Integer> strToSquare = square.compose(strToInt);
  6. 함수 항등원 (Identity):

    • identity 메소드를 사용하여 항등 함수(identity function)를 생성할 수 있습니다. 항등 함수는 입력을 그대로 반환하는 함수입니다.
    Function<String, String> identityFunction = Function.identity();

2.3 Function 메서드

  1. apply(T t) 메소드:

    • Function의 주요 메소드로, 주어진 입력에 대해 함수를 적용하고 결과를 반환합니다.

    • 예를 들어, 문자열을 정수로 변환하는 함수:

      Function<String, Integer> strToInt = s -> Integer.parseInt(s);
      Integer result = strToInt.apply("123");
  2. andThen(Function<? super R, ? extends V> after) 메소드:

    • 현재의 Function을 다른 Function과 조합하여 두 함수를 순차적으로 실행하는 새로운 Function을 생성합니다. 현재 함수를 먼저 적용하고, 그 결과를 다른 함수의 입력으로 전달합니다.

      Function<String, Integer> strToInt = s -> Integer.parseInt(s);
      Function<Integer, Integer> multiplyByTwo = n -> n * 2;
      
      // 체이닝
      Function<String, Integer> strToIntAndMultiply = strToInt.andThen(multiplyByTwo);
  3. compose(Function<? super V, ? extends T> before) 메소드:

    • 현재의 Function을 다른 Function과 합성하여 두 함수를 역순으로 실행하는 새로운 Function을 생성합니다. 먼저 적용되는 함수의 결과를 현재 함수의 입력으로 전달합니다.

      Function<String, Integer> strToInt = s -> Integer.parseInt(s);
      Function<Integer, Integer> square = n -> n * n;
      
      // 합성
      Function<String, Integer> strToSquare = square.compose(strToInt);
  4. identity() 메소드:

    • 정적 메소드로, 항등 함수(identity function)를 생성합니다. 항등 함수는 입력을 그대로 반환하는 함수입니다.

      Function<String, String> identityFunction = Function.identity();

2.4 Function 예제

  1. 문자열을 정수로 변환하는 함수:

    import java.util.function.Function;
    
    public class FunctionExample {
        public static void main(String[] args) {
            Function<String, Integer> strToInt = s -> Integer.parseInt(s);
    
            Integer result = strToInt.apply("123");
            System.out.println(result);  // 출력: 123
        }
    }
  2. 체이닝을 이용한 함수 조합:

    import java.util.function.Function;
    
    public class FunctionExample {
        public static void main(String[] args) {
            Function<String, Integer> strToInt = s -> Integer.parseInt(s);
            Function<Integer, Integer> multiplyByTwo = n -> n * 2;
    
            // 체이닝
            Function<String, Integer> strToIntAndMultiply = strToInt.andThen(multiplyByTwo);
    
            Integer result = strToIntAndMultiply.apply("5");
            System.out.println(result);  // 출력: 10
        }
    }
  3. 합성을 이용한 함수 조합:

    import java.util.function.Function;
    
    public class FunctionExample {
        public static void main(String[] args) {
            Function<String, Integer> strToInt = s -> Integer.parseInt(s);
            Function<Integer, Integer> square = n -> n * n;
    
            // 합성
            Function<String, Integer> strToSquare = square.compose(strToInt);
    
            Integer result = strToSquare.apply("3");
            System.out.println(result);  // 출력: 9
        }
    }
  4. 항등 함수(identity function) 사용:

    import java.util.function.Function;
    
    public class FunctionExample {
        public static void main(String[] args) {
            // 항등 함수
            Function<String, String> identityFunction = Function.identity();
    
            String result = identityFunction.apply("Hello, World!");
            System.out.println(result);  // 출력: Hello, World!
        }
    }

2.5 Function 정리

Function 합성

중고등학생때 수학 시간에 f(x) 함수와 g(x) 함수가 있을 때, 이 두 함수를 합성하여 f(g(x)) 라는 합성 함수를 다뤄본 기억이 어렴풋이나마 있을 것이다. f(g(x)) 는 g(x)의 결과를 다시 f(x) 함수의 인자로 넣어준 것이다.

이처럼 두 람다 함수를 연결하여 합성시킬 수 있는데, 이 합성 시키는 메서드를 자바에서 함수형 인터페이스의 디폴트 메서드로서 제공한다.

인터페이스 메서드설명
default <V> Function <T, V> andThen (Function <? super R, ? extends V> after);f(g(x)) 합성함수
default <V> Function <V, R> compose(Function <? super V, ? extends T> before);g(f(x)) 합성함수 (andThen의 반대)
static <T> Function<T, T> identity();항등함수 (자기 자신 반환)
자바-인터페이스-합성

합성 함수는 Function 인터페이스 뿐만 아니라 Consumer 이나 Operator 인터페이스도 존재한다.

자바-인터페이스-합성

람다 합성 사용 예시 1

간단한 수학 함수를 코드로 표현해 보았다. 숫자를 받으면 4를 빼는 함수 f(x) 와 숫자를 받으면 두배 곱해주는 함수 g(x) 를 람다표현식으로 선언하였다. 그리고 이 둘을 andThencompose로 합성하여 사용하면 다음과 같다.

public static void main(String[] args) {

    Function<Integer, Integer> f = num -> (num - 4); // f(x)
    Function<Integer, Integer> g = num -> (num * 2); // g(x)

    // f(g(x))
    int a = f.andThen(g).apply(10);
    System.out.println(a); // (10 - 4) * 2 = 12

    // g(f(x)) - andThen을 반대로 해석하면 된다
    int b = f.compose(g).apply(10);
    System.out.println(b); // 10 * 2 - 4 = 16
}

andThencompose 의 차이는 간단하다.

f.andThen(g) 를 수행하면 f 함수를 실행한 결과 값을 다시 g 함수의 인자로 전달하여 결과를 얻게 된다. 단, f 함수의 리턴 타입이 g 함수의 매개변수 타입과 호환되어야 한다.

f.compose(g) 를 수행하면 g 함수를 실행한 결과 값을 다시 f 함수의 인자로 전달하여 결과를 얻게 된다. 즉, andThen의 반대 버전이라고 보면 된다.

Tip

즉, x.andThen(y)y.compose(x) 와 동일하다고 보면 된다.

람다 합성 사용 예시 2

다음 Member 클래스와 Address 클래스가 있고, Member 클래스에서 Address 객체를 합성(composition) 하여 가지고 있다. 이 객체끼리 합성된 관계를 합성 함수를 통해 멤버의 도시 주소값을 불러오는 간단한 예제이다.

class Member {
    private String name;
    private Address address; // Address 객체를 합성(composition)

    public Member(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    public String getName() { return name; }

    public Address getAddress() { return address; }
}

class Address {
    private String country;
    private String city;

    public Address(String country, String city) {
        this.country = country;
        this.city = city;
    }

    public String getCountry() { return country; }

    public String getCity() { return city; }
}
public static void main(String[] args) {

    Member member = new Member("홍길동", new Address("조선", "한양"));

    // Member 매개타입과 Address 리턴타입
    Function<Member, Address> f = x -> x.getAddress(); // Address 객체를 얻기

    // Address 매개타입과 String 리턴타입
    Function<Address, String> g = x -> x.getCity(); // city 문자열 얻기

    // f(g(x))
    Function<Member, String> fg = f.andThen(g);
    String city = fg.apply(member); // Address 객체를 얻고(f 실행), Address 객체에서 city 필드값을 얻기(g 실행)
    System.out.println("거주 도시 : " + city);

    fg = g.compose(f);
    city = fg.apply(member);
    System.out.println("거주 도시 : " + city);
}
Function 합성
profile
필기하고, 타이핑하고, 말하면서 읽고, 코딩하고, 눈으로 읽고 오감으로 공부하기

0개의 댓글