본문 바로가기

자바

JAVA Lambda, Functional Interface

Lambda란

우리는 이전에 Stream API를 살펴봤었다.

그 포스트에서 알 수 있었듯이, Stream 연산들은 매개변수로 람다식을 사용했다.

그 이유는 Stream 연산들은 함수형 인터페이스를 받도록 되어있고 람다식은 함수형 인터페이스를 반환하기 때문이다.

우리는 Stream API를 확실하게 사용하기 위해서는 람다(Lambda)와 함수형 인터페이스(Functional Interface)에 대해서 알아야하기 때문에 중요하다고 할 수 있다.

람다식은 JDK 1.8부터 추가되었다.

람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 프로그래밍 언어가 되었다.

(람다식과 함수형 인터페이스의 이해를 위해 함수형 프로그래밍 언어에 대해서 알아볼 필요가 있다.[함수형 프로그래밍 링크] )

우리는 이를 통해 함수형 언어에서의 장점을 객체 지향의 장점과 함께 누릴 수 있게 되었다.

람다식은 간단히 말해서 메서드를 하나의 식(expression)으로 표현한 것이다.

메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 익명 함수(anonymous function)라고도 한다.

또한, 람다식은 1급 객체이기 때문에 파라미터로 전달이 가능하고 리턴도 가능하다.

간단한 예를 통해 람다식에 대해 자세히 알아보자.

우리는 기존에 람다식을 이용하지 않고 함수를 선언할 때는 다음과 같았다.

public int max(int a, int b) {
    return a > b ? a : b;
}

하지만, 우리가 메서드 max를 람다식으로 변환함으로써 다음과 같이 간단하게 표현할 수 있다.

(int a, int b) -> {return a > b ? a : b;}

여기서, 반환값이 있는 경우 식의 연산결과가 자동적으로 반환값이 된다.

위의 경우 반환값이 true, false로 존재하므로 return이 생략가능하다.

(int a, int b) -> a > b ? a : b

그리고 람다식의 매개변수의 타입이 추론 가능한 경우에 생략 가능하다.

(a, b) -> a > b ? a : b

이렇게 람다식이 등장함으로써 우리는 불필요한 코드를 줄여 코드를 간결하게 만들고 가독성을 높일 수 있었다. 추가적으로 람다는 지연연산을 수행하여 불필요한 연산을 최소화할뿐 아니라 멀티쓰레드를 활용하여 병렬처리를 할 수 있다.

하지만, 이렇게 람다를 사용하면서 만든 익명함수는 재사용이 불가능하다. 그에 따라, 비슷한 함수 코드가 중복되어 오히려 코드의 가독성을 해칠 수 있다.

함수형 인터페이스

람다식은 함수형 인터페이스의 인스턴스를 생성하여 함수를 변수처럼 선언하기 때문에 함수형 인터페이스를 요구하는 Stream API를 사용할 수 있다고 언급했다.

그렇다면, 람다식은 함수형 인터페이스를 반환한다는 의미일텐데 함수형 인터페이스는 무엇일까?

함수형 인터페이스는 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다.

우리는 함수형 인터페이스를 이용하여 람다식을 1급 객체처럼 변수처럼 선언할 수 있다.

이를 함수형 인터페이스를 활용하여 구현해보면 다음과 같다.

@FunctionalInterface
interface LambdaFuntion {
    int max(int a, int b);
}

public class Lambda {
    public static void main(String[] args) {
        LambdaFuntion lambdaFuntion = (int a, int b) -> a > b ? a : b;
        System.out.println(lambdaFuntion.max(1, 2));
    }
}
  • interface를 선언하고 그 위에 @FunctionalInterface를 선언한다.
  • 선언된 람다식이 익명 함수이며 람다식의 매개변수의 타입과 개수 그리고 반환값이 함수형 인터페이스가 일치하여 다음과 같이 대체할 수 있게 된다.
  • 단, 오직 1:1로만 연결이 된다.

자바에서 제공하는 함수형 인터페이스

자바에서는 java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해놓았다.

우리는 가급적이면 이를 활용하여 사용하는 것이 좋다.


Runnable

매개변수도 없고, 반환값도 없는 인터페이스이다.

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

Runnable을 사용한 간단한 예입니다.

public class FunctionalInterface {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Runnable!");
        runnable.run(); // Runnable!
    }
}
  • 반환값이 존재하지 않으므로 print문을 진행했다.

Supplier

매개변수는 없고, 반환값만 있는 인터페이스이다.

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

Supplier를 사용한 간단한 예입니다.

public class FunctionalInterface {
    public static void main(String[] args) {
        Supplier<String> stringSupplier = () -> "Supplier!";
        System.out.println(stringSupplier.get()); // Supplier!
    }
}
  • 반환값을 String으로 하였다.
  • get을 통해 결과값을 리턴했다.

Consumer

매개변수만 있고, 반환값이 없는 인터페이스이다.

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); };
    }
}

Consumer를 사용한 간단한 예입니다.

public class FunctionalInterface {
    public static void main(String[] args) {
        Consumer<String> stringConsumer = (String text) -> System.out.println(text);
        stringConsumer.accept("Consumer!"); // Consumer!
    }
}
  • accept를 통해 파라미터를 주입한다.
public class FunctionalInterface {
    public static void main(String[] args) {
        Consumer<String> stringConsumer1 = (String text) -> System.out.println("first : " + text);
        Consumer<String> stringConsumer2 = (String text) -> System.out.println("second : " + text);
        stringConsumer1.andThen(stringConsumer2).accept("Consumer!");
    }
}

// first : Consumer!
// second : Consumer!
  • andThen을 통해 연이어 Consumer를 적용시킬 수 있다.

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;
    }
}

Funtion<T, R>을 사용한 간단한 예입니다.

public class FunctionalInterface {
    public static void main(String[] args) {
        Function<String, String> stringFunction = (String text) -> text.substring(0, 3);
        System.out.println(stringFunction.apply("GilSSang")); // Gil
    }
}
  • apply를 통해 파라미터를 주입하고 결과값을 반환받았다.
public class FunctionalInterface {
    public static void main(String[] args) {
        Function<String, String> stringFunction = (String text) -> text.substring(0, 3);
        Function<String, String> stringStringFunction = (String text) -> text.toUpperCase();
        Function<String, String> compose = stringFunction.compose(stringStringFunction);
        System.out.println(compose.apply("GilSSang")); // GIL
    }
}
  • compose를 통해 연이어 Function을 적용시킬 수 있다.

Predictable

매개변수는 하나, 반환 타입은 Boolean인 인터페이스이다.

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;
    }
}

Predictable을 사용한 간단한 예입니다.

public class FunctionalInterface {
    public static void main(String[] args) {
        Predicate<Integer> integerPredicate = (num) -> num > 3;
        System.out.println(integerPredicate.test(5)); // true
        System.out.println(integerPredicate.test(1)); // false
    }
}
  • test()를 통해 파라미터를 주입한다.
public class FunctionalInterface {
    public static void main(String[] args) {
        Predicate<Integer> predicate1 = (num) -> num > 3;
        Predicate<Integer> predicate2 = (num) -> num < 6;
        // and
        System.out.println(predicate1.and(predicate2).test(5)); // true
        System.out.println(predicate1.and(predicate2).test(7)); // false

        // or
        System.out.println(predicate1.or(predicate2).test(5)); // true
        System.out.println(predicate1.or(predicate2).test(7)); // true
    }
}
  • and를 통해 실제 두 Predictable에 대한 and 연산을 진행한다.
  • or를 통해 실제 두 Predictable에 대한 or 연산을 진행한다.

우리는 함수형 프로그래밍의 장점을 살리기 위해 Stream API를 적극 활용하여야한다.

그러기 위해서는 람다식과 함수형 인터페이스의 이해를 기반으로 좋은 프로그래밍이 가능하기에 이를 학습해보았다.

'자바' 카테고리의 다른 글

JAVA Generic  (0) 2021.10.16
JAVA Collection  (0) 2021.10.15
JAVA Exception  (0) 2021.10.13
Java Optional  (1) 2021.10.04
Java Stream  (1) 2021.10.03