Java8부터 도입된 람다 표현식과 관련하여 새로운 패키지인 java.util.function 이 도입되었다. 해당 패키지 안에는 대부분 함수형 인터페이스가 정의되어 있으며 특히 컬렉션 프레임워크와 스트림 API와 함께 많이 쓰인다. 그 말은 컬렉션 프레임워크와 스트림 API와 함께 람다 표현식도 많이 쓰인다는 것과 동일하겠다. JDK8부터 기본적으로 제공되는 java.util.function 패키지의 기본 함수형 인터페이스의 Supplier, Consumer, Function, Predicate에 대해서 기록한다.
Supplier<T>
Supplier<T> 인터페이스는 파라미터가 없는 T 타입의 객체를 리턴하는 get() 추상 메소드를 가지는 함수형 인터페이스이다.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
사용 방법은 아래와 같다.
Supplier<String> supplier = () -> "hello world!!";
supplier.get()을 호출할 때마다 'hello world!!' String 객체를 반환한다.
위 내용은 아래와 같다.
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "hello world!!";
}
};
특화된 타입 supplier
Supplier<T> 이외에도 IntSupplier, BooleanSupplier, LongSupplier, DoubleSupplier를 제공한다. 각 함수형 인터페이스 정의를 보면 알 수 있겠지만 Generic Type <T>가 없다. 이 말은 인터페이스 이름에서도 알 수 있듯이 각 타입에 특화되어 정의가 되었다는 의미다.
함수형 인터페이스 | 추상 메소드 |
BooleanSupplier | boolean getAsBoolean() |
IntSupplier | int getAsInt() |
LongSupplier | long getAsLong() |
DoubleSupplier | double getAsDouble() |
primitive 타입을 반환하는 Supplier <T>를 사용하는 경우에는 <T>가 객체이기 때문에 get() 추상 메소드를 정의하는 람다 표현식 내에서 primitive 타입에 대한 boxing 처리가 발생할 수 있는데 타입에 특화된 Supplier를 사용하는 경우에는 primitive 타입에 대한 boxing 처리를 하지 않기 때문에 조금이나마 성능적으로 좋을뿐더러 직관적이기도 하다.
Consumer<T>
Consumer<T> 인터페이스는 T 타입의 한 개의 파라미터를 가지고 리턴값이 없는 accept() 추상 메소드를 가지는 함수형 인터페이스이다. 인터페이스 정의는 아래와 같다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
// default methods removed
}
기본적인 사용 방법은 아래와 같다.
Consumer<String> printer = s -> System.out.println(s);
String 타입의 s를 출력하는 람다 표현식이다. 위 코드는 결국 accept(Strign s)를 구현한다.
위 코드는 아래와 같다.
Consumer<String> printer = new Consumer<>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
아래와 같이 사용할 수 있다.
List<String> list = Arrays.asList("string1", "string2", "string3");
for (String s : list) {
printer.accept(s);
}
위 코드는 아래와 같이 사용할 수도 있다.
list.forEach(printer);
Iterable 인터페이스의 default 메소드로 정의되어 있는 forEach 코드는 아래와 같다.
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
내부적으로 for loop 내에서 Consumer<? super T> 타입의 action 객체의 accept()를 호출한다.
andThen(Consumer<? super T> after) 디폴트 메소드
default 메소드로 정의된 andThen은 메소드 이름에서 알 수 있듯이 또 다른 Consumer<T> 함수형 인터페이스를 파라미터로 전달하여 연속적인 실행을 위해서 사용될 수 있다.
andThen 디폴트 메소드는 아래와 같이 정의되어 있다.
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
accept() 메소드를 먼저 실행 후에 파라미터로 넘어온 또 다른 Consumer의 accept()를 실행한다.
사용 방법은 아래와 같다.
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printerAfter = s -> System.out.println(s + "suffix");
Consumer<String> afterThen = printer.andThen(printerAfter);
List<String> list = Arrays.asList("string1", "string2", "string3");
for (String s : list) {
afterThen.accept(s);
}
----------------------------------
결과
string1
string1suffix
string2
string2suffix
string3
string3suffix
특화된 타입 Consumer
Supplier와 마찬가지로 Consumer 역시 특화된 타입의 함수형 인터페이스를 제공한다.
함수형 인터페이스 | 추상 메소드 |
DoubleConsumer | void accept(double value) |
IntConsumer | void accept(int value) |
LongConsumer | void accept(long value) |
BiConsumer<T, U> | void accept(T t, U u) |
ObjDoubleConsumer<T> | void accept(T t, double value) |
ObjIntConsumer<T> | void accept(T t, int value) |
ObjLongConsumer<T> | void accept(T t, long value) |
BiConsumer<T, U>는 특화된 타입은 아니지만 ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T>는 BiConsumer<T, U>의 일부 특화된 타입이라고 볼 수 있다. 각 함수형 인터페이스의 andThen() 메소드를 보면 Consumer<T>의 andThen() 메소드와 다를 바 없다.
Predicate<T>
Predicate 인터페이스는 T 타입의 한 개의 파라미터를 가지고 boolean 타입을 리턴하는 test() 추상 메소드를 가지는 함수형 인터페이스이다. 인터페이스 정의는 아래와 같다.
@FunctionalInterface
public interface Predicate<T> {
/**
* 주어진 argument를 평가한다.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
/**
* 또 다른 predicate의 test() 호출에 대해서 && 연산을 수행하는 Predicate<T>를 리턴한다.
* 두 개의 predicate의 && 연산을 조합한다.
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/**
* test() 호출의 not 연산을 수행하는 Predicate<T>를 리턴한다.
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/**
* 또 다른 predicate의 test() 호출에 대해서 || 연산을 수행하는 Predicate<T>를 리턴한다.
* 두 개의 predicate의 || 연산을 조합한다.
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
/**
* 파라미터로 전달된 targetRef객체와 T타입의 객체의 동등성을 체크하는 Predicate<T>를 리턴한다.
*/
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
/**
* 파라미터로 전달된 target predicate의 not연산을 수행하는 predicate를 리턴한다.
* @since 11 - JDK 11부터 지원된다.
*/
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
기본적인 사용법은 아래와 같다.
Predicate<Integer> isGreaterThan10 = i -> i > 10;
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 10, 15, 20);
for (Integer i : list) {
if (isGreaterThan10.test(i)) {
System.out.println(i + " greater than 10");
}
}
--------------------------------------------------------------
결과
15 greater than 10
20 greater than 10
and(Predicate<? super T>) 디폴트 메소드
/**
* 또 다른 predicate의 test() 호출에 대해서 && 연산을 수행하는 Predicate<T>를 리턴한다.
* 두 개의 predicate의 && 연산을 조합한다.
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
and() 디폴트 메소드는 두개의 predicate에 대해서 && 연산을 수행한다.
Predicate<Integer> isLessThan10 = i -> i < 10;
Predicate<Integer> isGreaterThan2 = i -> i > 2;
//두개의 predicate를 조합하여 &&연산을 수행하는 predicate
Predicate<Integer> predicate = isLessThan10.and(isGreaterThan2);
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 10, 15, 20);
for (Integer i : list) {
if (predicate.test(i)) {
System.out.println(i + " greater than 2 and less than 10");
}
}
-----------------------------------------------------------
결과
3 greater than 2 and less than 10
4 greater than 2 and less than 10
5 greater than 2 and less than 10
negate() 디폴트 메소드
/**
* test() 호출의 not 연산을 수행하는 Predicate<T>를 리턴한다.
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
negate() 디폴트 메소드는 predicate에 대한 not 연산을 수행하는 새로운 Predicate를 리턴한다.
Predicate<Integer> isGreaterThan10 = i -> i > 10;
Predicate<Integer> isLessThanEqual10 = isGreaterThan10.negate();
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 10, 15, 20);
for (Integer i : list) {
if (isLessThanEqual10.test(i)) {
System.out.println(i + " less than equal 10");
}
}
---------------------------------------------------------
결과
1 less than equal 10
2 less than equal 10
3 less than equal 10
4 less than equal 10
5 less than equal 10
10 less than equal 10
or(Predicate<? super T> other) 디폴트 메소드
/**
* 또 다른 predicate의 test() 호출에 대해서 || 연산을 수행하는 Predicate<T>를 리턴한다.
* 두 개의 predicate의 || 연산을 조합한다.
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
or() 디폴트 메소드는 두 개의 predicate에 대해서 || 연산을 수행한다.
Predicate<Integer> isLessThan5 = i -> i < 5;
Predicate<Integer> isGreaterThan10 = i -> i > 10;
Predicate<Integer> predicate = isLessThan5.or(isGreaterThan10);
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 10, 15, 20);
for (Integer i : list) {
if (predicate.test(i)) {
System.out.println(i + " less than 5 or greater than 10");
}
}
---------------------------------------------------------
결과
1 less than 5 or greater than 10
2 less than 5 or greater than 10
3 less than 5 or greater than 10
4 less than 5 or greater than 10
15 less than 5 or greater than 10
20 less than 5 or greater than 10
isEqual(Object targetRef) 스태틱 메소드
/**
* 파라미터로 전달된 targetRef객체와 T타입의 객체의 동등성을 체크하는 Predicate<T>를 리턴한다.
*/
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
Predicate의 T 타입의 객체와 targetRef 객체의 동등성을 비교하는 Predicate를 리턴한다.
String test = "hello world!";
Predicate<String> equalPredicate = Predicate.isEqual(test);
List<String> list = List.of("hello", "world", "!", "hello world!");
for (String str : list) {
if (equalPredicate.test(str)) {
System.out.println(str + " equals " + test);
}
}
------------------------------------------------------------
결과
hello world! equals hello world!
not(Predicate<? super T> target) 스태틱 메소드
/**
* 파라미터로 전달된 target predicate의 not연산을 수행하는 predicate를 리턴한다.
* @since 11 - JDK 11부터 지원된다.
*/
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
파라미터로 전달되는 target predicate의 negate()를 호출하는 Predicate를 리턴한다.
Predicate<Integer> isGreaterThan10 = i -> i > 10;
Predicate<Integer> isLessThanEqual10 = Predicate.not(isGreaterThan10);
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 10, 15, 20);
for (Integer i : list) {
if (isLessThanEqual10.test(i)) {
System.out.println(i + " less than equal 10");
}
}
-------------------------------------------------------------
결과
1 less than equal 10
2 less than equal 10
3 less than equal 10
4 less than equal 10
5 less than equal 10
10 less than equal 10
특화된 타입 Predicate
함수형 인터페이스 | 추상 메소드 |
DoublePredicate | boolean test(double value) |
IntPredicate | boolean test(int value) |
LongPredicate | boolean test(long value) |
이름에서 알 수 있듯이 파라미터의 타입이 primitive 타입으로 특화하여 불필요한 boxing 처리가 되지 않도록 지원한다.
Function<T, R>
Function 인터페이스는 T 타입의 한 개의 파라미터를 가지고 R 타입을 리턴하는 test() 추상 메소드를 가지는 함수형 인터페이스이다. 인터페이스 정의는 아래와 같다.
@FunctionalInterface
public interface Function<T, R> {
/**
* T 타입의 인자를 받고 R 타입을 리턴하는 메소드
*/
R apply(T t);
/**
* T 타입을 리턴하는 Function 인터페이스 before.apply 호출의 결과 T를 현재 Function
* 인터페이스의 apply 메소드의 파라미터로 전달하여 R타입을 리턴하는 Function 인터페이스를
* 리턴한다.
* 파라미터 객체의 이름에서 알 수 있듯이 before.apply() 의 실행 결과는 현재 apply()의 입력
* 으로 전달되어 체이닝을 구성한다.
* andThen의 역순.
*
* @see #andThen(Function)
*/
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
/**
* apply()의 리턴 타입 R이 after.apply()의 파라미터로 전달된다.
* 파라미터 객체의 이름에서 알 수 있듯이 after.apply()는 현재 apply()의 실행 결과를 입력으로
* 받아서 수행되는 체이닝을 구성한다.
* compose의 역순
*
* @see #compose(Function)
*/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
/**
* input과 output이 동일한 Function 함수형 인터페이스를 리턴한다.
*/
static <T> Function<T, T> identity() {
return t -> t;
}
}
기본적인 사용 방법은 아래와 같다.
Function<Integer, Integer> multipleFive = value -> value * 5;
List<Integer> list = List.of(1, 2, 3, 4, 5);
for( Integer i : list) {
System.out.println(multipleFive.apply(i));
}
-----------------------------------------------------------------
결과
5
10
15
20
25
list에 담긴 각 숫자들에 5를 곱하는 Function 인터페이스에 대한 예시 코드다.
compose(Function<? super V, ? extends T> after) 디폴트 메소드
/**
* T 타입을 리턴하는 Function 인터페이스 before.apply 호출의 결과 T를 현재 Function
* 인터페이스의 apply 메소드의 파라미터로 전달하여 R타입을 리턴하는 Function 인터페이스를
* 리턴한다.
* 파라미터 객체의 이름에서 알 수 있듯이 before.apply() 의 실행 결과는 현재 apply()의 입력
* 으로 전달되어 체이닝을 구성한다.
* andThen의 역순.
*
* @see #andThen(Function)
*/
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
파라미터로 전달되는 before 객체의 before.apply()의 결과는 apply()의 입력 파라미터로 전달되어 체이닝을 구성한다.
/*
5개의 숫자 모두에 -1 연산 후 *5를 수행한다.
*/
Function<Integer, Integer> beforeMinusOne = value -> value -1;
Function<Integer, Integer> multipleFive = value -> value * 5;
Function<Integer, Integer> compose = multipleFive.compose(beforeMinusOne);
List<Integer> list = List.of(1, 2, 3, 4, 5);
for( Integer i : list) {
System.out.println(compose.apply(i));
}
-------------------------------------------------------------
결과
0
5
10
15
20
andThen(Function<? super R, ? extends V> after) 디폴트 메소드
/**
* apply()의 리턴 타입 R이 after.apply()의 파라미터로 전달된다.
* 파라미터 객체의 이름에서 알 수 있듯이 after.apply()는 현재 apply()의 실행 결과를 입력으로
* 받아서 수행되는 체이닝을 구성한다.
* compose의 역순
*
* @see #compose(Function)
*/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
apply()의 수행 결과가 파라미터로 전달되는 after 객체의 after.apply()의 입력으로 전달되어 새로운 타입(혹은 동일 타입)의 결과를 만드는 체이닝을 구성한다.
Function<Integer, String> afterMinusOne = value -> {
value = value -1;
return String.format("string value is %s", value);
};
Function<Integer, Integer> multipleFive = value -> value * 5;
Function<Integer, String> afterThen = multipleFive.andThen(afterMinusOne);
List<Integer> list = List.of(1, 2, 3, 4, 5);
for( Integer i : list) {
System.out.println(afterThen.apply(i));
}
----------------------------------------------------------
결과
string value is 4
string value is 9
string value is 14
string value is 19
string value is 24
identity() 스태틱 메소드
입력과 출력이 동일한 Function 인터페이스를 리턴한다.
/**
* input과 output이 동일한 Function 함수형 인터페이스를 리턴한다.
*/
static <T> Function<T, T> identity() {
return t -> t;
}
Function<Integer, Integer> identity = Function.identity();
Function<Integer, Integer> multipleFive = value -> value * 5;
List<Integer> list = List.of(1, 2, 3, 4, 5);
for( Integer i : list) {
System.out.println(identity.apply(i));
}
-------------------------------------------------------------
결과
1
2
3
4
5
개인적으로 List 타입의 객체를 stream을 이용하여 Map으로 변환하는 경우에 혹은 특수하게 stream의 reduce를 사용하는 경우에 주로 사용했던 것 같다.
특화된 타입 Function
함수형 인터페이스 | 추상 메소드 |
DoubleFunction<R> | R apply(double value) - 입력 타입 특수화 |
DoubleToIntFunction | int applyAsInt(double value) - 입출력 타입 특수화 |
DoubleToLongFunction | long applyAsLong(double value) - 입출력 타입 특수화 |
IntFunction<R> | R apply(int value) - 입력 타입 특수화 |
IntToDoubleFunction | double applyAsDouble(int value) - 입출력 타입 특수화 |
IntToLongFunction | long applyAsLong(int value) - 입출력 타입 특수화 |
LongFunction<R> | R apply(long value) - 입력 타입 특수화 |
LongToDoubleFunction | double applyAsDouble(long value) - 입출력 타입 특수화 |
LongToIntFunction | int applyAsInt(long value) - 입출력 타입 특수화 |
ToIntFunction<T> | int applyAsInt(T value) - 출력 타입 특수화 |
ToLongFunction<T> | long applyAsLong(T value) - 출력 타입 특수화 |
ToDoubleFunction<T> | double applyAsDouble(T value) - 출력 타입 특수화 |
BiFunction<T, U, R> | R apply(T t, U u) - 입력을 2개 받는 Function 인터페이스 |
ToDoubleBiFunction<T, U> | double applyAsDouble(T t, U u) - 출력 타입 특수화 |
ToIntBiFunction<T, U> | int applyAsInt(T t, U u) - 출력 타입 특수화 |
ToLongBiFunction<T, U> | long applyAsLong(T t, U u) - 출력 타입 특수화 |
가짓수가 상당히 많아 보이는데 구성만 파악하면 간단하다.
Function<T, R>을 확장하여 구현한 XXXOperator 함수형 인터페이스도 있는데 Function 인터페이스 기반으로 기능을 확장한 함수형 인터페이스이다. java.util.function 에서 확인해 볼 수 있다.
위 4가지 기본 함수형 인터페이스는 상당히 많은 부분에서 사용되고 있다. Java8에 새롭게 도입된 Stream<T> 인터페이스는 거의 대부분 메소드의 파라미터로 위 4가지의 함수형 인터페이스로 구성되어 있다.
'자바' 카테고리의 다른 글
java generic class (0) | 2023.09.24 |
---|---|
java time convert (시간 변환) (0) | 2023.09.17 |
Java - 람다 표현식 (lambda expression) 개요 (0) | 2023.09.10 |
jdk pattern matching for switch (0) | 2023.08.26 |
jdk pattern matching for instanceof (0) | 2023.08.26 |
댓글