함수형 인터페이스(Functional Interface)는 하나의 추상 메서드만을 가지며, 람다식 또는 메서드 참조를 이용해 함수 객체를 생성할 수 있는 인터페이스입니다.

Java는 Java 8부터 함수형 인터페이스를 통해 함수형 프로그래밍을 지원합니다.

함수형 인터페이스 생성

함수형 인터페이스는 아래와 같이 1개의 추상 메소드가 있어야 합니다.

  • @FunctionalInterface 어노테이션은 이 인터페이스가 함수형 인터페이스라는 것을 표시
  • int calculateLength(String str) : 인자 1개를 받고 리턴 값이 있는 추상 메소드
@FunctionalInterface
interface StringLengthCalculator {
    int calculateLength(String str);
}

함수형 인터페이스 사용 방법

위에서 만든 함수형 인터페이스 StringLengthCalculator를 람다 표현식으로 구현하였습니다.

  • 구현된 객체는 calculator.calculateLength("Hello, World!")처럼 호출할 수 있고, 결과를 받음
  • 람다 표현식과 함께 사용하여 간결하고 가독성 있는 코드를 작성할 수 있음
public class Example {

    @FunctionalInterface
    interface StringLengthCalculator {
        int calculateLength(String str);
    }

    public static void main(String[] args) {

        StringLengthCalculator calculator = (str) -> str.length();

        int length = calculator.calculateLength("Hello, World!");
        System.out.println("Length of the string: " + length);
    }
}

Output:

Length of the string: 13

함수형 인터페이스를 익명 클래스로 구현

아래 예제는 람다 대신에 익명 클래스를 이용하여 인터페이스를 구현하였습니다. 이렇게 구현하면 람다보다 코드 양이 많아집니다.

  • 람다와 익명 클래스 구현의 동작은 동일
  • 람다로 구현하는게 간결하고 가독성 있는 프로그램을 만들 수 있음
public class Example {

    @FunctionalInterface
    interface StringLengthCalculator {
        int calculateLength(String str);
    }

    public static void main(String[] args) {

        StringLengthCalculator calculator = new StringLengthCalculator() {
            @Override
            public int calculateLength(String str) {
                return str.length();
            }
        };

        int length = calculator.calculateLength("Hello, World!");
        System.out.println("Length of the string: " + length);
    }
}

Output:

Length of the string: 13

Java의 기본 함수형 인터페이스

함수형 인터페이스를 사용할 때마다 인터페이스를 직접 구현해야 한다면, 매우 번거로울 수 있습니다.

이 때문에 다음과 같이 자바에서 기본적으로 제공하는 함수형 인터페이스가 있습니다. 개발하는데 사용되는 함수형 인터페이스는 대부분 있기 때문에, 새로운 함수형 인터페이스를 만들어야 하는 경우는 많지 않을 것 같습니다.

각 인터페이스에 대한 자세한 내용 및 예제는 아래에서 소개하겠습니다.

  • Runnable: () -> void
  • Supplier: () -> T
  • Consumer: T -> void
  • Function: T -> R
  • Predicate: T -> boolean
  • BiFunction: (T, U) -> R
  • UnaryOperator: T -> T
  • BinaryOperator: (T, T) -> T

Runnable: () -> void

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

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

아래 예제는 Runnable 인터페이스를 구현하여 스레드에서 실행할 코드를 구현하고, 이를 스레드에서 실행합니다.

  • Runnable은 run()으로 함수 호출
  • Thread는 Runnable 인터페이스를 인자로 받음, 쓰레드 내부에서 run() 호출
  • 인자가 필요하지 않고, 리턴할 필요도 없는 경우 Runnable을 사용하면 됨
public class Example {

    public static void main(String[] args) {

        Runnable runnable = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Hello, World!");
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Output:

Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!

Supplier: () -> T

인자는 받지 않고 T 타입의 객체를 리턴하는 함수형 인터페이스입니다.

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

아래 예제는 Supplier 인터페이스를 사용하여 특정 값을 제공합니다.

  • 예제의 Supplier의 Integer 타입 42를 항상 리턴하도록 구현됨
  • Supplier는 get()으로 함수 호출
import java.util.function.Supplier;

public class Example {

    public static void main(String[] args) {

        Supplier<Integer> supplier = () -> 42;
        int result = supplier.get();
        System.out.println("Result: " + result);
    }
}

Output:

Result: 42

Consumer: T -> void

T 타입의 객체를 인자로 받으며, 리턴 값은 없는 함수형 인터페이스입니다.

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

아래 예제에서 Consumer는 인자로 받은 String을 메시지로 출력합니다.

  • Consumer는 accept()로 함수 수행
  • 화면에 문자열만 출력하고 리턴 값은 없음
import java.util.function.Consumer;

public class Example {

    public static void main(String[] args) {

        Consumer<String> consumer = (message) -> System.out.println("Message: " + message);
        consumer.accept("Hello, Consumer!");
    }
}

Output:

Message: Hello, Consumer!

Function: T -> R

T 타입의 객체를 인자로 받으며, R 타입 객체를 리턴하는 함수형 인터페이스입니다.

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

아래 예제는 Integer 값을 인자로 받아서 문자열로 만들고 String 값을 리턴합니다.

  • Function은 apply()로 함수 호출
import java.util.function.Function;

public class Example {

    public static void main(String[] args) {

        Function<Integer, String> function = (num) -> "Number: " + num;
        String result = function.apply(123);
        System.out.println(result);
    }
}

Output:

Number: 123

Predicate: T -> boolean

T 타입의 객체를 인자로 받으며, boolean을 리턴하는 함수형 인터페이스입니다.

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

아래 예제는 인자로 전달된 Integer가 짝수인지 확인 후 결과를 boolean으로 리턴합니다.

  • Predicate는 test()로 함수 호출
import java.util.function.Predicate;

public class Example {

    public static void main(String[] args) {

        Predicate<Integer> isEven = (num) -> num % 2 == 0;
        boolean result = isEven.test(10);
        System.out.println("Is 10 even? " + result);
    }
}

Output:

Is 10 even? true

BiFunction: (T, U) -> R

T, U 타입의 객체 2개를 인자로 받으며, R 타입 객체를 리턴하는 함수형 인터페이스입니다.

public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

아래 예제는 Integer 2개를 인자로 전달하면 두개의 합을 Integer로 리턴합니다.

  • BiFunction은 apply()로 함수 호출
import java.util.function.BiFunction;

public class Example {

    public static void main(String[] args) {

        BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
        int result = add.apply(5, 3);
        System.out.println("5 + 3 = " + result);
    }
}

Output:

5 + 3 = 8

UnaryOperator: T -> T

T 타입 객체를 인자로 받으며, T 타입 객체를 리턴하는 함수형 인터페이스입니다.

public interface UnaryOperator<T> extends Function<T, T> {
    // Function<T, T> 를 상속
}

아래 예제는 Integer를 인자로 받고, 제곱을 계산하여 리턴합니다.

  • UnaryOperator는 apply()로 함수 호출
  • UnaryOperator<T>Function<T, T>는 동일 인자, 동일 리턴 타입
import java.util.function.UnaryOperator;

public class Example {

    public static void main(String[] args) {

        UnaryOperator<Integer> square = (num) -> num * num;
        int result = square.apply(4);
        System.out.println("Square of 4: " + result);
    }
}

Output:

Square of 4: 16

BinaryOperator: (T, T) -> T

T 타입 객체 두개를 인자로 받고, T 타입 객체를 리턴하는 함수형 인터페이스입니다.

public interface BinaryOperator<T> extends BiFunction<T,T,T> {
    // BiFunction<T,T,T>를 상속
}

아래 예제는 두개의 Integer를 받고, 두 숫자의 곱을 Integer로 리턴합니다.

  • BinaryOperator는 apply()로 함수 호출
  • BinaryOperator<Integer>BiFunction<Integer, Integer, Integer>는 동일 인자, 동일 리턴 타입
import java.util.function.BinaryOperator;

public class Example {

    public static void main(String[] args) {

        BinaryOperator<Integer> multiply = (a, b) -> a * b;
        int result = multiply.apply(6, 7);
        System.out.println("6 * 7 = " + result);
    }
}

Output:

6 * 7 = 42