자바

Java - 람다 표현식(lambda expression) - 4개의 주요 functional interface

알쓸개잡 2023. 9. 15.

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

댓글

💲 추천 글