QueryDSL 의
QuerydslPredicateExecutor
부터 학습하면서Predicate, Function
인터페이스를 만나게 됩니다. 그리고 이전에 정리한 람다와 메소드 레퍼런스도 함께 사용되기 때문에 [첫번째 프로젝트] 3. 람다부터 읽는 것을 권장합니다.
Predicate
는 함수형 프로그래밍을 지원하기 위한 인터페이스java.util.function
패키지에 포함Predicate
는 주어진 입력에 대해 참 또는 거짓을 반환하는 메소드 test
를 가지고 있음여기서 주의해야 할 점은, Predicate
는 단순히 '참' 또는 '거짓'을 반환하는 것뿐만 아니라, 입력 값에 대한 어떠한 조건을 표현할 수 있는 함수로도 사용됩니다.
단일 메소드 인터페이스(SAM - Single Abstract Method): Predicate
는 단일 추상 메소드인 test
를 가지고 있습니다. 이 메소드는 제네릭 타입의 매개변수를 받아서 Boolean
값을 반환합니다. 이러한 특성으로 인해 Predicate
는 함수형 인터페이스로 간주되며, 람다 표현식이나 메소드 참조를 통해 간결하게 구현할 수 있습니다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
논리 연산 가능: 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();
스트림 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());
함수형 프로그래밍 지원: Predicate
는 자바 8부터 도입된 함수형 프로그래밍의 핵심 요소 중 하나입니다. 함수형 인터페이스로서 람다 표현식을 통해 간결하게 코드를 작성할 수 있으며, 이는 가독성을 높이고 유지보수를 쉽게 만들어줍니다.
Predicate<Integer> isPositive = x -> x > 0;
test(T t)
메소드:
주어진 인자에 대한 조건을 평가하고, 그 결과를 불리언 값으로 반환합니다.
예를 들어, 문자열의 길이가 5보다 작은지 여부를 확인하는 Predicate
:
Predicate<String> isShort = s -> s.length() < 5;
boolean result = isShort.test("apple"); // false
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);
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);
negate()
메소드:
현재 Predicate
의 결과를 부정합니다. 즉, 현재가 참이면 부정된 Predicate
는 거짓이 되고, 거짓이면 참이 됩니다.
Predicate<Integer> isEven = x -> x % 2 == 0;
Predicate<Integer> isNotEven = isEven.negate();
isEqual(Object targetRef)
메소드:
주어진 객체와 동등한지 여부를 검사하는 Predicate
를 생성합니다. equals
메소드를 사용하여 동등성을 판단합니다.
Predicate<String> isEqualToApple = Predicate.isEqual("apple");
boolean result = isEqualToApple.test("banana"); // false
컬렉션 필터링: 주어진 조건에 따라 컬렉션에서 원하는 요소만 선택할 때 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());
조건부 동작: Predicate
는 주어진 조건에 따라 동작을 다르게 구현할 수 있도록 도와줍니다. 예를 들어, 어떤 조건이 참일 때만 특정 동작을 수행하고 그 외의 경우에는 다른 동작을 수행하도록 할 수 있습니다.
Predicate<Integer> isPositive = x -> x > 0;
// 조건에 따라 다른 동작 수행
if (isPositive.test(someValue)) {
// 양수일 때의 동작
} else {
// 음수 또는 0일 때의 동작
}
검증 및 유효성 검사: Predicate
는 입력 값이 특정 조건을 충족하는지 검사하는 데에도 사용됩니다. 예를 들어, 사용자로부터 받은 입력이 유효한지를 검증하는 데에 Predicate
를 활용할 수 있습니다.
Predicate<String> isValidEmail = email -> email.matches("^[a-zA-Z0-9_]+@[a-zA-Z]+\\.[a-zA-Z]+$");
// 이메일 유효성 검사
if (isValidEmail.test(userInput)) {
// 유효한 이메일 주소
} else {
// 유효하지 않은 이메일 주소
}
true / false
리턴test()
predicate
) 한다고 생각하면 된다.인터페이스 형태 | 내용 |
---|---|
Predicate<T> | T 를 받아 boolean 리턴 |
BiPredicate<T, U> | T, U 를 받아 boolean 리턴 |
XXXPredicate | XXX 를 받아 boolean 리턴 |
Predicate 종류
인터페이스 명 | 추상 메소드 | 설명 |
---|---|---|
Predicate | Boolean test(T t) | 객체 T를 조사 |
BiPredicate<T, U> | Boolean test(T t, U u) | 객체 T와 U를 비교 조사 |
DoublePredicate | Boolean test(double value) | double 값을 조사 |
IntPredicate | Boolean test(int value) | int 값을 조사 |
LongPredicate | Boolean 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 결합
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) | 객체 비교 |
람다 결합 사용 예시
조건문을 구성할때 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
}
Function
은 자바에서 함수형 프로그래밍을 지원하기 위한 인터페이스java.util.function
패키지에 포함Function
은 하나의 입력을 받아서 하나의 출력을 생성하는 메소드 apply
를 가지고 있음Function
인터페이스는 다음과 같이 정의되어 있습니다:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
여기서 T
는 입력 타입, R
은 출력 타입을 나타냅니다.
입력과 출력 타입 지정:
Function
은 제네릭으로 정의되어 있어 입력(T
)과 출력(R
)의 타입을 명시할 수 있습니다. 이를 통해 재사용성이 높은 함수를 만들 수 있습니다.Function<String, Integer> strToInt = s -> Integer.parseInt(s);
단일 추상 메소드:
Function
은 단일 추상 메소드 apply
를 가지고 있습니다. 이 메소드는 주어진 입력에 대해 함수를 적용하고 결과를 반환합니다.R apply(T t);
람다 표현식 사용:
Function
인터페이스를 구현할 수 있습니다. 이로써 익명 클래스나 별도의 구현 클래스를 작성할 필요가 없어집니다.Function<String, Integer> strToInt = s -> Integer.parseInt(s);
체이닝 (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);
함수 합성 (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);
함수 항등원 (Identity):
identity
메소드를 사용하여 항등 함수(identity function)를 생성할 수 있습니다. 항등 함수는 입력을 그대로 반환하는 함수입니다.Function<String, String> identityFunction = Function.identity();
apply(T t)
메소드:
Function
의 주요 메소드로, 주어진 입력에 대해 함수를 적용하고 결과를 반환합니다.
예를 들어, 문자열을 정수로 변환하는 함수:
Function<String, Integer> strToInt = s -> Integer.parseInt(s);
Integer result = strToInt.apply("123");
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);
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);
identity()
메소드:
정적 메소드로, 항등 함수(identity function)를 생성합니다. 항등 함수는 입력을 그대로 반환하는 함수입니다.
Function<String, String> identityFunction = Function.identity();
문자열을 정수로 변환하는 함수:
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
}
}
체이닝을 이용한 함수 조합:
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
}
}
합성을 이용한 함수 조합:
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
}
}
항등 함수(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!
}
}
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) 를 람다표현식으로 선언하였다. 그리고 이 둘을 andThen
과 compose
로 합성하여 사용하면 다음과 같다.
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
}
andThen
과 compose
의 차이는 간단하다.
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);
}