관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[자바] 자바 함수형 인터페이스(Functional Interface) 총 정리!!! 본문

자바(java)

[자바] 자바 함수형 인터페이스(Functional Interface) 총 정리!!!

막무가내막내 2021. 1. 14. 00:18
728x90

 

[2021.01.14 블로그 포스팅 스터디 일곱 번째 글]

 

먼저 함수형 인터페이스란 1개의 추상 메소드를 갖고 있는 인터페이스를 말합니다. 

그래서 이를 강제하기 위해 밑과 같은 @FunctionalInteface 같은 어노테이션도 존재합니다.

@FunctionalInterface  // 추상메소드 한개만 갖도록 강제한다!!
interface Game{
	public abstract void play();
}

그럼 추상 메소드 한개만 갖고있는 이러한 함수형 인터페이스는 왜 사용할까요?

바로 자바8에서 함수형 프로그래밍을 위한 람다식(Lambda)을 사용하기 위해서 입니다. 람다식은 인터페이스의 추상 메소드가 2개 이상인 경우 사용할 수 없습니다. 

 

위와 같이 함수형 인터페이스를 만드면 익명클래스를 다음과 같이 람다식으로 구현할 수 있는 예시를 들어봤습니다. 2개 이상이면 익명 클래스로 밖에 생성을 못하겠지요 ㅠ

		//1. 익명 클래스
		Game game = new Game() {
			@Override
			public void play() {
				System.out.println("익명 클래스");
			}
		};
		game.play();
		
		//2. 람다 표현식
		Game game2 = ()->{System.out.println("람다 표현식 1");};
		game2.play();
		
		//2. 람다 표현식-> 단일 문장인 경우에는  {}  생략가능
		Game game3 = ()->System.out.println("람다 표현식 2");
		game3.play();

 

 

위 함수형 인터페이스에서는 파라미터변수 없고 리턴값 없는 경우를 다뤘습니다. 

 

파라미터 변수와 리턴값 여부로 함수형 인터페이스의 종류를 따지자면 크게 몇 종류가 있을까요?

총 다음과 같이 4가지가 있을 겁니다. 

파라미터변수 X 리턴값 X
파라미터변수 O 리턴값 X
파라미터변수 X 리턴값 O
파라미터변수 O 리턴값 O

 

이렇게 종류가 크게 4가지로 분류가 되고 사람들이 매번 함수형 인터페이스를 매번 각각의 인터페이스 이름과 함수의 타입 및 이름 등을 직접 정의하고 생성해주는 불편함을 막아주기 위해서 자바에서 편의성을 위한 라이브러리 패키지를 제공해줍니다. 

 

Java  1.8 부터 지원되며 함수적 인터페이스 API는 java.util.function 패키지에 포함되고 모두 Interface로 구현되어 있고 다음과 같이 기본적으로 크게 여섯 종류가 있습니다.

제네릭으로 타입도 정하고 기본 제공하는 함수를 사용하므로 일관성있고 편리한 함수형 프로그래밍이 가능해집니다. ( BiConsumer(파라미터 2개받아서 처리), DoubleConsumer(double 타입으로 파라미터 받아서 처리) 등 파생된건 제외했습니다. ㅎㅎ )

 

1. Runnable -> 파라미터 않고 리턴값도 없는 경우 / run()

2. Consumer<T> -> 파라미터 있고 리턴값 없는 경우 / accept(T t)

3. Supplier<T> -> 파라미터 없고 리턴값 있는 경우 / get() 

4. Function<T,R> -> 파라미터 있고 리턴값 있는 경우 / apply(T t) / 데이터 맵핑용, int받고 String반환같은

5. Operator<T> -> 파라미터 있고 리턴값 있는 경우 / 연산용도, 임의의 타입 전달하고 임의의 타입 반환
6. Predicate<T> -> 파라미터 있고 리턴값(boolean) 있는 경우 / test(T t) / 임의의 타입 받고 boolean형 반환

 

안드로이드를 해보신분이면 아시겠지만 RxJava의 함수들도 이 인터페이스들 구현되어 함수형 프로그래밍을 구현합니다.

 

 

인터페이스의 장점 중 하나는 무언가를 구현하는걸 강제하고 동일한 이름의 메소드를 사용하는 것을 강제하기 때문에 일관성 있는 프로그래밍이 가능하다는 점인데 자바에서 제공해주는 기본 함수형 인터페이스들을 사용한다면 편의성은 물론이고 일관성도 더 좋아집니다.

직접 클래스로 구현하지 말고 상위의 함수적 인터페이스를 사용합시다.!!

 

차례대로 예제를 살펴보겠습니다.

 


 

1. Runnable - run()

-> 파라미터 않고 리턴값도 없는 경우

 

(인터페이스)

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

 

(사용)

 Runnable runnable = () -> System.out.println("Runnable run");
 runnable.run();

 

 


 

 

2. Consumer - accpet(T t)

 -> 파라미터 있고 리턴값 없는 경우, accept()라는 추상메소드 이름으로 각각의 기본 함수형 인터페이스들은 자기의 역할에 맞는 인터페이스 이름과 추상메소드 명을 갖고 있습니다. (accept -> 파라미터만 받겠다)

 

(인터페이스)

여기서 andThen()은 두 개의 Comsumer 로직을 체이닝시켜줄떄 사용합니다. 

참고사이트 : stackoverflow.com/questions/47449580/java-8-usage-of-consumers-andthen

 

 * @param <T> the type of the input to the operation
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

 

(사용)

Consumer<String> con = text -> System.out.println("파리미터 받은 것 -> " + text);
con.accept("막내의 막무가내 프로그래밍!!");

 

(사용2 - andThen() 체이닝)

Consumer<String> con = text -> System.out.println("con -> " + text);
Consumer<String> con2 = text -> System.out.println("con2 -> " + text);
con2.andThen(con).accept("me");

 

 

 

 

 

 

Consumer에서 다음과 같이 파생된 몇 가지 인터페이스도 좀만 더 살펴보고 가겠습니다. (다른 기본형 함수 인터페이스들도 똑같이 갖고 있으므로 뒤에선 생략하겠습니다.)

BiConsumer =>  파라미터를 2개 받아서 처리
DoubleConsumer ==> double타입으로 파라미터를 받아서 처리
IntConsumer ==> int타입으로 파라미터를 받아서 처리
LongConsumer ==> long타입으로 파라미터를 받아서 처리
ObjDoubleConsumer ==> 두개의 파라미터중에 하나는 obj 다른하나는 double
ObjIntConsumer ==> 두개의 파라미터중에 하나는 obj 다른하나는 int
ObjLongConsumer ==> 두개의 파라미터중에 하나는 obj 다른하나는 long

 

 

먼저 BiConsumer<T, U> 입니다.

이것은 파라미터를 2개 받을 수 있는 Consumer라고 보시면 됩니다.

 

(인터페이스)

@FunctionalInterface
public interface BiConsumer<T, U> {

    /**
     * Performs this operation on the given arguments.
     *
     * @param t the first input argument
     * @param u the second input argument
     */
    void accept(T t, U u);

    /**
     * Returns a composed {@code BiConsumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code BiConsumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);

        return (l, r) -> {
            accept(l, r);
            after.accept(l, r);
        };
    }
}

 

(사용 accept )

BiConsumer<String, Integer> bi = (text, num) -> System.out.println("파리미터 2개 -> " + text + " + " + num);
bi.accept("프로그래밍!!", 1234);

 

 

 

다음은 DoubleConsumer 입니다. Double 형 파라미터를 받는 함수형 인터페이스 입니다. 그래서 처음부터 타입이 정해져 있으므로 제네릭을 따로 설정해줄 필요가 없습니다. 이름을 보시면 아시겠지만 IntConsumber~, Long~ ,Boolean~ 등도 있다고 예측가능하시겠죠?? ㅋㅋ 

 

(인터페이스)

@FunctionalInterface
public interface DoubleConsumer {

    /**
     * Performs this operation on the given argument.
     *
     * @param value the input argument
     */
    void accept(double value);

    /**
     * Returns a composed {@code DoubleConsumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code DoubleConsumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default DoubleConsumer andThen(DoubleConsumer after) {
        Objects.requireNonNull(after);
        return (double t) -> { accept(t); after.accept(t); };
    }
}

 

 

(사용)

DoubleConsumer doubleConsumer = doubleNum -> System.out.println("Double형 나와랏  -> " + doubleNum);
doubleConsumer.accept(1234.123);

 

 

 

 

 


 

 

 

3. Supplier  - get()

-> 파라미터 없고 리턴값 있는 경우

 

(인터페이스)

 *
 * @param <T> the type of results supplied by this supplier
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

 

 

(사용 get )

Supplier<String> supplier = () -> "리턴값이 있지요!!";
System.out.println(supplier.get());

 

파생 인터페이스

BooleanSupplier :  boolean 타입으로 반환
DoubleSupplier  :  double 타입으로 반환
IntSupplier  :  int 타입으로 반환
LongSupplier  :  long 타입으로 반환

 

 

 

 

 


 

 

4. Function - apply(T t)

-> 파라미터 있고 리턴값 있는 경우

 

 

(Fucntion 인터페이스)

 *
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

    /**
     * Returns a composed function that first applies the {@code before}
     * function to its input, and then applies this function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of input to the {@code before} function, and to the
     *           composed function
     * @param before the function to apply before this function is applied
     * @return a composed function that first applies the {@code before}
     * function and then applies this function
     * @throws NullPointerException if before is null
     *
     * @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));
    }

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     *
     * @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));
    }

    /**
     * Returns a function that always returns its input argument.
     *
     * @param <T> the type of the input and output objects to the function
     * @return a function that always returns its input argument
     */
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

 

 

(사용 apply )

        Function<Integer, Integer> sumFunction = (value) -> value + 5;
        Integer result = sumFunction.apply(10);
        System.out.println(result);

(추가예제)

BiFunction<T, U, R> :  T와 U 전달하고 R타입으로 반환

 * @param <T> the type of the first argument to the function
 * @param <U> the type of the second argument to the function
 * @param <R> the type of the result of the function
 *
 * @see Function
 * @since 1.8
 */
@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     */
    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}
        BiFunction<String, String, Integer> f = (t, u) -> t.length() + u.length();
        int num = f.apply("QWQWQ", "BCBCB");
        System.out.println(num); // 5 + 5 = 10

 

 

(사용2 compose - 이것도 위에서 다룬 Consumber andThen() 같이 체이닝을 지원해줍니다. compose()는 두개의 Function을 조합하여 새로운 Function 객체를 만들어주는 메소드입니다.  주의 할점은 compose()부터 역순으로 실행된다는 점입니다. 이건 예제를 보면 이해가 가실 것 입니다.)

        Function<Integer, Integer> sumFunction = (value) -> value + 5;
        Function<Integer, Integer> multiplyFunction = (value) -> value * 10;
        //실행순서 : multiplyFunction -> sumFunction
        Function<Integer, Integer> composeFunction = sumFunction.compose(multiplyFunction);
        Integer result = composeFunction.apply(1);
        System.out.println(result);

 

 

 

추가로 이런 파생된 Function 클래스입니다.

BiFunction<T,U,R> : T와 U 전달하고 R타입으로 반환
DoubleFunction<R> : double로 전달하고 R타입으로 반환
IntFunction<R> : int로 전달하고 R타입으로 반환
IntToDoubleFunction : int로 전달하고 double타입으로 반환
IntToLongFunction : int로 전달하고 long타입으로 반환
LongToDoubleFunction : long로 전달하고 double타입으로 반환
LongToIntFunction : long로 전달하고 int타입으로 반환

 

 

 

 


 

 

 

5. Operator<T> 

-> 파라미터 있고 리턴값 있는 경우 (주의할 점: Operator라는 인터페이스는 없습니다. 앞에 수식어가 붙습니다.) 내부적으로 Function을 상속받고 전달하는 파라미터를 연산해서 임의의 값으로 리턴하는 용도로 사용됩니다.

 

먼저 인터페이스 종류입니다.

    BinaryOperator<T> : BiFunction<T,U,R>  상속
                      : 두개의 파라미터 전달하고 반환 ( 모두 데이터타입 동일 )		
	UnaryOperator<T> : Function<T,R> 상속
	                  : 하나의 파라미터 전달하고 반환 ( 모두 데이터타입 동일 )	
	                  
    DoubleBinaryOperator : 두개의 파라미터 double 전달하고  double 반환 ( 모두 데이터타입 동일 )
    DoubleUnaryOperator	  :	: 하나의 파라미터 double 전달하고 double반환  
    IntBinaryOperator : 두개의 파라미터 int 전달하고  int 반환 ( 모두 데이터타입 동일 )
    IntUnaryOperator	  :	: 하나의 파라미터 int 전달하고 int반환  
    LongBinaryOperator : 두개의 파라미터 long 전달하고  long 반환 ( 모두 데이터타입 동일 )
    LongUnaryOperator	  :	: 하나의 파라미터 long 전달하고 long반환  

 

BinaryOperator<T> 예시 하나만 들어보겠습니다.

 

(인터페이스) Function 인터페이스를 상속받으므로 apply()를 사용하는것을 볼 수 있습니다.

 *
 * @param <T> the type of the operands and result of the operator
 *
 * @see BiFunction
 * @see UnaryOperator
 * @since 1.8
 */
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
    /**
     * Returns a {@link BinaryOperator} which returns the lesser of two elements
     * according to the specified {@code Comparator}.
     *
     * @param <T> the type of the input arguments of the comparator
     * @param comparator a {@code Comparator} for comparing the two values
     * @return a {@code BinaryOperator} which returns the lesser of its operands,
     *         according to the supplied {@code Comparator}
     * @throws NullPointerException if the argument is null
     */
    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    /**
     * Returns a {@link BinaryOperator} which returns the greater of two elements
     * according to the specified {@code Comparator}.
     *
     * @param <T> the type of the input arguments of the comparator
     * @param comparator a {@code Comparator} for comparing the two values
     * @return a {@code BinaryOperator} which returns the greater of its operands,
     *         according to the supplied {@code Comparator}
     * @throws NullPointerException if the argument is null
     */
    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

 

 

(사용 - apply)

        BinaryOperator<String> binaryOperator = (t, u) -> t + u + " 이어주는 연산예시닷";
        String result = binaryOperator.apply("막내의", "막무가내 프로그래밍");
        System.out.println(result);

 

 

 

 

 


 

 

 

6. Predicate<T> - test(T t)

-> 파라미터 있고 리턴값(boolean) 있는 경우

 

(인터페이스)

 *
 * @param <T> the type of the input to the predicate
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * AND of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code false}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ANDed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * AND of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    /**
     * Returns a predicate that represents the logical negation of this
     * predicate.
     *
     * @return a predicate that represents the logical negation of this
     * predicate
     */
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * OR of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code true}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ORed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * OR of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    /**
     * Returns a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}.
     *
     * @param <T> the type of arguments to the predicate
     * @param targetRef the object reference with which to compare for equality,
     *               which may be {@code null}
     * @return a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}
     */
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

 

(사용 - test, and, or, isEqual)

예제와 메소드명만 봐도 어떤 동작을 하는지 예측하실 수 있을겁니다. ㅎㅎ

        //test()
        Predicate<String> isNameString = str -> str.equals("name");
        System.out.println("is String Name? test() -> " + isNameString.test("age"));
        System.out.println("is String Name? test() -> " + isNameString.test("name"));

        //and(), or()
        Predicate<Integer> isLowerThanTen = num -> num < 10;
        Predicate<Integer> isBiggerThanFive = num -> num > 5;
        System.out.println("and() -> " + isBiggerThanFive.and(isLowerThanTen).test(15)); //하나는 true, 하나는 false
        System.out.println("or() -> " + isBiggerThanFive.or(isLowerThanTen).test(15));

        //isEqual()
        Predicate<String> isStringEquals = Predicate.isEqual("name");
        System.out.println(isStringEquals.test("Google"));
        Predicate<Integer> isNumEquals = Predicate.isEqual(100);
        System.out.println(isNumEquals.test(100));

negate() 도 있는데 ! 를 나타낸다고 보면 됩니다.

 


참고 및 출처:

codechacha.com/ko/java8-functional-interface/

 

Java8 - 함수형 인터페이스(Functional Interface) 이해하기

함수형 인터페이스는 1개의 추상 메소드를 갖고 있는 인터페이스를 말합니다. Single Abstract Method(SAM)라고 불리기도 합니다. 함수형 인터페이스를 사용하는 이유는 자바의 람다식은 함수형 인터페

codechacha.com

 

 

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!

728x90
Comments