(Java 8) 자바 8에 추가된 함수형 인터페이스(Function, Predicate 등)

2022. 11. 20. 20:52Java

반응형

Java는 각 버전마다 새로운 기능이 추가되고 있지만 가장 큰 변곡점은 Java 8 버전이라고 할 수 있습니다.

 

Java 8버전에는 함수형 인터페이스, 람다 표현식, LocalDatetime, stream, Optional 등이 추가되면서

더욱 코드를 간결하면서 효율적이게 작성할 수 있게 되었습니다.

 

 

이 문서에서는 '함수형 인터페이스와 람다 표현식'에 대해서 알아보려고 합니다.

 

'함수형'이 무엇인지 궁금한 분들은 아래 문서를 참고 바랍니다.

 

(함수형 코딩) 1장/ 함수형 사고란 무엇인가?

책을 펴고 1장을 펴면 다음과 같은 챕터에 대한 설명을 볼 수 있다. 이 장에서는 함수형 사고가 무엇인지, 왜 함수형 사고가 더 좋은 소프트웨어를 만들려는 개발자에게 도움이 되는지 설명합니

mirrorofcode.tistory.com

 

🧐함수형 인터페이스


자바에서 함수형 인터페이스의 조건은 인터페이스에 추상 메서드가 하나만 있으면 함수형 인터페이스입니다..

 

(두 개, 세 개면 함수형 인터페이스가 아니다!!!)

 

public interface Foo {
     int add10(int number);
}

 

이런 인터페이스가 바로 함수형 인터페이스입니다.

 

이 인터페이스를 좀 더 강력하게 함수형 인터페이스로 정의하고 싶다면 자바 기본 패키지(java.lang)에서 제공하는

@FunctionalInterface 어노테이션을 함께 사용하면 훨씬 안전하게 사용할 수 있습니다.

 

 

@FunctionalInterface
public interface Foo {
    int add10(int number);
}

 

FunctionalInterface 어노테이션 사용하면 메서드가 두 개 이상 생성되었을 때,

컴파일러가 이를 감지하고 컴파일 에러를 발생시킵니다.

 

 

그럼 이를 사용할 때는 어떻게 사용할까요?

 

첫 번째 방법은 익명 내부 클래스를 사용해 구현하는 방식입니다.

public class Main {
    public static void main(String[] args) {
       Foo foo = new Foo() {
           @Override
           public int add10(int number) {
               return number + 10;
           }
       };
    }
}

 

익명 내부 클래스를 사용한다면 @Override메서드를 사용해서 메서드 내부를 구현하면 됩니다.

 

 

두 번째 방법은 람다 표현식을 사용해서 구현하는 방법입니다.

 

 public static void main(String[] args) {
       Foo foo = number -> number + 10;
 }

 

람다 표현식을 사용하면 훨씬 깔끔하게 코드를 작성할 수 있습니다.

 

 

문서 마지막에 람다 표현식에 대해서 자세하게 살펴볼 것입니다만, 람다 표현식을 사용할 때, 인자가 하나라면 소괄호()를 생략해서 사용할 수 있습니다. 그리고 한 줄로 짧게 표현 가능한 경우, 오른쪽 바디 부분을 중괄호와 리턴 문을 제거할 수도  있습니다.

 

 

 

여기까지 함수형 인터페이스의 기본에 대해서 알아봤습니다.

여기까지는 사용자가 인터페이스를 직접 정의해서 사용하는 경우였고, 이제는 자바에서 기본으로 제공하는 함수형 인터페이스를 사용해서 구현해보겠습니다.

 

 

🧐기본 함수형 인터페이스, Function


자바 기본 함수형 패키지는 Java.util.function에 정의되어 있습니다.

(그리고 모둔 함수형 패키지는 순수 함수임을 가정합니다. 순수 함수에 대해 모르시는 분들의 위의 함수형 사고 링크를 통해서...)

 

이 문서에서 살펴볼 대표적인 함수형 인터페이스는 Function, Comsumer, Supplier 등입니다.

 

먼저 Function을 살펴봅시다.

 

Function의 정의를 살펴보면 Function <T, R>로 정의되어 있습니다.

(T = the type of the input to the function, R = the type of the result of the function)

 

그렇기에 파라미터의 자료형과, 결괏값의 자료형을 필요로 합니다.

 

코드는 아래와 같습니다.

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> function = number -> number + 10;
    }
}

 

 

 

 

Function인터페이스로 순수 함수를 구현할 수 있습니다.

 

실제로 Foo 인터페이스를 정의해서 사용하는 방법과 완전히 동일합니다만, 사용방법에서 차이가 있습니다.

 

앞선 코드에서는  

Foo foo = number -> number + 10;

과 같이 정의와 동시에 인스턴스가 생성되었습니다.

 

하지만 Function과 같은 자바에서 제공하는 함수형 인터페이스의 경우 선언만으로는 인터페이스가 생성되지 않고, 

메서드를 실행할 때 인스턴스가 생성됩니다.

 

 

그래서 위 코드는 이렇게 사용할 수 있습니다.

 

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> function = number -> number + 10;
        System.out.println(function.apply(10));
        // 20 출력
    }
}

 

 

그리고 함수형 인터페이스는 다른 함수형 인터페이스와 조합해서 사용 가능한 메서드가 있습니다.

 

  • compose
  • andThen

먼저 compose 메서드 한 마디로 '선 처리' 메서드입니다.

 

compose로 들어온 입력값을 먼저 처리한 후, 본 함수를 처리합니다.

 

코드를 보시죠.

 

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> add = number -> number + 10;
        Function<Integer,Integer> multiply = number -> number * 2;
        
        System.out.println(add.compose(multiply).apply(5));
    }
}

 

실제 함수 실행 순서는

 

multiply가 먼저 실행된 후, add 함수를 실행합니다.

그래서 결괏값으로 5 * 2 + 10이라는 식을 거쳐 결국 20이 출력되게 됩니다.

 

 

두 번째로 andThen 메서드는 compose와는 반대로 '후 처리' 메서드입니다.

 

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> add = number -> number + 10;
        Function<Integer,Integer> multiply = number -> number * 2;

        System.out.println(add.andThen(multiply).apply(5));
    }
}

 

andThen은 '후 처리' 메서드이기 때문에 add 함수를 실행한 후 multiply 함수를 실행합니다.

그렇기 때문에 결국 (5 + 10) * 2 이기에 30이 출력되게 됩니다.

 

 

🧐기본 함수형 인터페이스, Consumer


Comsumer는 Function과는 다르게 리턴 값이 존재하지 않습니다.

void형 메서드와 같다고 볼 수 있습니다.

 

인터페이스를 정의하고 문자열을 출력하는 코드를 작성해봅시다.

 

@FunctionalInterface
public interface Foo {
    void hello(String name);
}


public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo() {
            @Override
            public void hello(String name) {
                System.out.println("hi " + name);
            }
        };

        foo.hello("mike");
    }
}

 

이런 코드를 익명 내부 클래스를 사용해서 foo 인터페이스를 구현했습니다.

 

이름 파라미터를 전달하면 "hi" + name을 출력하는 메서드를 구현했습니다.

 

 

그리고 이를 람다 표현식으로 사용하면 다음과 같이 사용할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Foo foo = name -> System.out.println("hi " + name);

        foo.hello("mike");
    }
}

 

 

이 람다 표현식, 혹은 익명 내부 클래스를 Consumer를 사용해서 함수형 인터페이스로 바꿔보겠습니다.

 

import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        Consumer<String> consumer = name -> System.out.println("hi " + name);
        consumer.accept("mike");
    }
}

 

Consumer는 사용할 때 accept 메서드를 사용합니다.

 

Function과 마찬가지로 andThen을 사용할 수 있지만, compose를 사용할 수 없는데요,

 

그 이유는 당연하게도 리턴 값이 없기 때문입니다.

 

Function의 내부에서 Compose를 살펴보면,

 

    /**
     * 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*라고 명시되어 있습니다.

 

그렇기에 리턴을 하지 않는 Consumer에서는 사용할 수 없겠죠.

 

 

 

🧐기본 함수형 인터페이스, Supplier


소개할 기본 함수형 인터페이스는 Supplier입니다.

 

Supplier는 어떠한 인자 값도 받지 않고, 오직 값만을 리턴해주는 함수형 인터페이스입니다.

 

아래 코드를 살펴보시죠.

 

import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Supplier<Integer> supplier = () -> 100;
        System.out.println(supplier.get());
    }
}

 

get을 사용하면 결괏값을 리턴 받을 수 있습니다.

 

 

앞에서 언급하지 않았지만, 이전에 언급한 함수형 인터페이스와 Supplier 역시 커스텀 클래스를 인자로 사용할 수 있는데요,

 

 

public class Car {
    int price;

    public int getPrice() {
        return price;
    }

    public Car() {
        this.price = 2000;
    }
}



import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Supplier<Car> carSupplier = Car::new;

        Car car = carSupplier.get();
    }
}

 

이런 식으로 사용 가능합니다.

 

여기서 주의할 점은 carSupplier를 선언한다고 해서 인스턴스가 생성되는 것이 아니라는 것입니다.

 

반드시 get을 호출해야 인스턴스가 생성됩니다.

 

 

🧐기본 함수형 인터페이스, Predicate


마지막으로 알아볼 함수형 인터페이스는 Predicate입니다.

 

Predicate는 어떤 인자를 받아서 true, false 즉, 불린 값을 리턴합니다.

 

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isPrime = (number) -> {
            for (int i = 2; i <= number; i++) {
                if (number % 2 == 0) {
                    return false;
                }
            }
            return true;
        };

        System.out.println(isPrime.test(11));
    }
}

 

이렇게 코드를 작성할 수 있습니다.

 

Predicate는 test 메서드뿐만 아니라, and, or, negate 등의 논리 연산자를 가지고 있습니다.

 

 

 

여기까지 기본 함수형 인터페이스에 대해서 알아봤습니다.

지금까지 소개한 것 외에도 다양한 함수형 인터페이스들을 ava.lang 패키지에서 확인하실 수 있습니다.

 

 

BiFunction이나, UnaryOperator, BinaryOperator 등 재미있는 함수형 인터페이스가 많으니 살펴보면 좋을 것 같습니다.

 

 

반응형