HOME > java > java8

Java8 - 함수형 인터페이스(Functional Interface) 정리

JSFollow04 Oct 2019

함수형 인터페이스(Functional interface)는 1개의 추상 메소드를 갖고 있는 인터페이스를 말합니다. Single Abstract Method(SAM)라고 불리기도 합니다.

예를들어, 아래와 같은 인터페이스를 함수형 인터페이스라고 합니다.

public interface FunctionalInterface {
    public abstract void doSomething(String text);
}

함수형 인터페이스를 사용하는 이유?

함수형 인터페이스를 사용하는 이유는 자바의 람다식은 함수형 인터페이스로만 접근이 되기 때문입니다.

예를 들어, 아래 코드에서 변수 func는 람다식으로 생성한 객체를 가리키고 있습니다. doSomething()에 인자로 문자열을 전달하면 람다식에 정의된 것처럼 로그로 출력을 합니다.

public interface FunctionalInterface {
     public abstract void doSomething(String text);
}

FunctionalInterface func = text -> System.out.println(text);
func.doSomething("do something");
// 실행 결과
// do something

지금까지 개발을 하면서 익명 클래스로 객체를 만든적이 있었을텐데요. 아래 코드는 익명 클래스를 사용하여 리팩토링한 코드입니다. 함수형 인터페이스와 람다식으로 익명 클래식을 간단하게 표현했다고 생각할 수 있습니다.

FunctionalInterface func = new FunctionalInterface() {
    @Override
    public void doSomething(String text) {
        System.out.println(text);
    }
};
func.doSomething("do something");

정리하면, 함수형 인터페이스를 사용하는 것은 람다식으로 만든 객체에 접근하기 위해서 입니다. 위의 예제처럼 람다식을 사용할 때마다 함수형 인터페이스를 매번 정의하기에는 불편하기 때문에 자바에서 라이브러리로 제공하는 것들이 있습니다.

기본 함수형 인터페이스

자바에서 기본적으로 제공하는 함수형 인터페이스는 다음과 같은 것들이 있습니다.

  • Runnable
  • Supplier
  • Consumer
  • Function<T, R>
  • Predicate

이 외에도 다양한 것들이 있습니다. 자바의 java.util.function 패키지에 정의되어있으니 더 많은 것을 확인하고 싶으시면 이곳을 확인해주세요.

Runnable

Runnable은 인자를 받지 않고 리턴값도 없는 인터페이스입니다.

public interface Runnable {
  public abstract void run();
}

아래 코드처럼 사용할 수 있습니다.

Runnable runnable = () -> System.out.println("run anything!");
runnable.run();
// 결과
// run anything!

Runnable은 run()을 호출해야 합니다. 함수형 인터페이스마다 run()과 같은 실행 메소드 이름이 다릅니다. 인터페이스 종류마다 만들어진 목적이 다르고, 그 목적에 맞는 이름을 실행 메소드 이름으로 정하였기 때문입니다.

Supplier

Supplier<T>는 인자를 받지 않고 T 타입의 객체를 리턴합니다.

public interface Supplier<T> {
    T get();
}

아래 코드처럼 사용할 수 있습니다. get() 메소드를 호출해야 합니다.

Supplier<String> getString = () -> "Happy new year!";
String str = getString.get();
System.out.println(str);
// 결과
// Happy new year!

Consumer

Consumer<T>는 T 타입의 객체를 인자로 받고 리턴 값은 없습니다.

public interface Consumer<T> {
    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

아래 코드처럼 사용할 수 있습니다. accept()메소드를 사용합니다.

Consumer<String> printString = text -> System.out.println("Miss " + text + "?");
printString.accept("me");
// 결과
// Miss me?

또한, andThen()을 사용하면 두개 이상의 Consumer를 연속적으로 실행할 수 있습니다.

Consumer<String> printString = text -> System.out.println("Miss " + text + "?");
Consumer<String> printString2 = text -> System.out.println("--> Yes");
printString.andThen(printString2).accept("me");
// 결과
// Miss me?
// --> Yes

Function

Function<T, R>는 T타입의 인자를 받고, R타입의 객체를 리턴합니다.

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

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

다음과 같이 사용할 수 있습니다. apply()메소드를 사용합니다.

Function<Integer, Integer> multiply = (value) -> value * 2;
Integer result = multiply.apply(3);
System.out.println(result);
// 결과
// 6

compose()는 두개의 Function을 조합하여 새로운 Function 객체를 만들어주는 메소드입니다. 주의할 점은 andThen()과는 실행 순서가 반대입니다. compose()에 인자로 전달되는 Function이 먼저 수행되고 그 이후에 호출하는 객체의 Function이 수행됩니다.

예를들어, 다음과 같이 compose를 사용하여 새로운 Function을 만들 수 있습니다. apply를 호출하면 add 먼저 수행되고 그 이후에 multiply가 수행됩니다.

Function<Integer, Integer> multiply = (value) -> value * 2;
Function<Integer, Integer> add      = (value) -> value + 3;

Function<Integer, Integer> addThenMultiply = multiply.compose(add);

Integer result1 = addThenMultiply.apply(3);
System.out.println(result1);
// 결과
// 12

Predicate

Predicate<T>는 T타입 인자를 받고 결과로 boolean을 리턴합니다.

public interface Predicate<T> {
    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

다음과 같이 사용할 수 있습니다. test()메소드를 사용합니다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
System.out.println("10 is bigger than 5? -> " + isBiggerThanFive.test(10));
// 결과
// 10 is bigger than 5? -> true

and()or()는 다른 Predicate와 함께 사용됩니다. 직관적으로 and()는 두개의 Predicate가 true일 때 true를 리턴하며 or()는 두개 중에 하나만 true이면 true를 리턴합니다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
Predicate<Integer> isLowerThanSix = num -> num < 6;
System.out.println(isBiggerThanFive.and(isLowerThanSix).test(10));
System.out.println(isBiggerThanFive.or(isLowerThanSix).test(10));
// 결과
// false
// true

isEqual()은 static 메소드로, 인자로 전달되는 객체와 같은지 체크하는 Predicate 객체를 만들어 줍니다. 다음과 같이 사용할 수 있습니다.

Predicate<String> isEquals = Predicate.isEqual("Google");
isEquals.test("Google");
// 결과
// true

정리

자바에서 기본적으로 제공하는 함수형 인터페이스에 대해서 알아보았습니다. 인터페이스마다 인자와 리턴타입이 다르고 이름이 다릅니다. 사용하는 목적에 맞게 이름을 지었습니다. 그렇기 때문에 실행하는 메소드 이름도 다릅니다.

참고