Java/함수형 인터페이스

[Java]함수형 인터페이스(Functional Interface)

DevStory 2022. 5. 10.

함수형 인터페이스(Functional Interface)란?

Java 8에 도입된 함수형 인터페이스는 인터페이스가 함수처럼 동작하기 때문에 함수형 인터페이스라고 합니다. 함수형 인터페이스는 하나의 추상 메서드를 가지고 있어서 SAM 인터페이스(Single Abstract Method Interface)라고도 합니다.

함수형 인터페이스는 하나의 추상 메서드 외에도 정적 메서드, 기본 메서드, Object 클래스의 메서드를 가질 수 있습니다.


함수형 인터페이스 구문

함수형 인터페이스의 기본 구문은 다음과 같습니다.

public interface MyFunctionInterface {
  public void myMethod();
}

위 예제는 구현되지 않은 단일 메서드만 포함되어 있으므로 함수형 인터페이스입니다.

위에서 언급했듯이 함수형 인터페이스에는 기본 메서드와 정적 메서드가 포함될 수 있습니다.

public interface MyFunctionInterface {
  public void myMethod();
  public default void defaultMethod() {}
  public static void staticMethod() {}
}

위 예제는 기본 메서드와 정적 메서드가 존재하지만, myMethod()라는 단일 메서드가 존재하므로 함수형 인터페이스입니다.


인터페이스 구현

람다식으로 함수형 인터페이스 구현 방법을 설명하기 전에 클래스로 구현하는 방법부터 알아봅시다.

다음은 두 개의 매개변수의 합산을 반환하는 구현 클래스를 만들기 위해 제네릭 함수형 인터페이스를 생성합니다.

public interface MyFunctionInterface<T, R> {
  public R sum(T x, T y);
}

클래스에서 인터페이스를 구현합니다.

인터페이스에 작성된 sum() 메서드를 Override 합니다.

// 함수형 인터페이스 구현
public class FunctionalInterfaceClass 
        implements MyFunctionInterface<Integer, Integer>{
  @Override
  public Integer sum(Integer x, Integer y) {
    return x + y;
  }
}

public class Main {
  public static void main(String args[]) {
    FunctionalInterfaceClass obj = new FunctionalInterfaceClass();
    System.out.println(obj.sum(10, 20));
  }
}

[실행 결과]

30

람다식으로 인터페이스 구현

클래스로 구현하는 방법의 단점은 함수형 인터페이스가 제네릭인 경우 타입 개수만큼 클래스를 생성해야 한다는 점입니다.

public class IntegerClass
        implements MyFunctionInterface<Integer, Integer> {
  @Override
  public Boolean sum(Integer x, Integer y) {
    return x + y;
  }
}

public class DoubleClass
        implements MyFunctionInterface<Double, Double> {
  @Override
  public Double sum(Double x, Double y) {
    return x + y;
  }
}

public class StringClass
        implements MyFunctionInterface<String, String> {
  @Override
  public String sum(String x, String y) {
    return x + y;
  }
}

타입 개수만큼 클래스를 생성해야 하므로 클래스가 얼마나 많아질지 예상하기 어렵습니다. 아래에서 소개할 람다식을 사용하면 클래스 없이 함수형 인터페이스를 구현할 수 있습니다.

다음 예제는 람다식을 사용하여 함수형 인터페이스를 구현합니다.

public interface MyFunctionInterface<T> {
  public void myMethod();
}

public class Main {
  public static void main(String args[]) {
    MyFunctionInterface<Integer> myFunctionInterface = () -> {
      System.out.println("실행");
    };
  }
}


int형 매개변수 두 개의 합산을 String 타입으로 반환하는 경우 추상 메서드의 반환 타입과 매개변수를 설정합니다.

public interface MyFunctionInterface<T> {
  public String myMethod(int a, int b);
}

public class Main {
  public static void main(String args[]) {
    MyFunctionInterface<Integer> myFunctionInterface = (x, y) -> {
      return Integer.toString(x + y);
    };
  }
}


함수형 인터페이스에 기본 메서드, 정적 메서드, Object 클래스의 메서드가 존재하더라도 단 하나의 추상 메서드를 가지고 있어서 아래 코드는 정상적으로 컴파일됩니다.

public interface MyFunctionInterface<T> {
  // 추상 메서드
  public void myMethod();

  // Object 클래스의 메서드
  @Override
  public boolean equals(Object object);

  // 기본 메서드
  public default void defaultMethod() {}

  // 정적 메서드
  public static void staticMethod() {}
}

public class Main {
  public static void main(String args[]) {
    MyFunctionInterface<Integer> myFunctionInterface = () -> {
      System.out.println("실행");
    };
    
    myFunctionInterface.myMethod();
  }
}

[실행 결과]

실행

람다식을 사용하면 코드가 훨씬 간결해지고 클래스를 구현할 필요가 없습니다.

반응형

FunctionalInterface 어노테이션

Java 8에 도입된 @FunctionalInterface 어노테이션을 사용하여 함수형 인터페이스를 선언할 수 있습니다. 이 어노테이션은 함수형 인터페이스의 규칙을 위반하는 경우 컴파일러에 의해 오류가 발생합니다.

예를 들어 함수형 인터페이스에 둘 이상의 추상 메서드가 존재하는 경우 컴파일 오류가 발생합니다. 다음 예제는 두 개의 추상 메서드가 존재하는 함수형 인터페이스입니다.

@FunctionalInterface
public interface MyFunctionInterface<T> {
  public void myMethod();
  public void myMethod2();
}

[에러 내용]

여러 개의 추상 메서드가 발견되었다는 에러가 표시됩니다.

FunctionalInterface 어노테이션 사용은 개발자 선택이지만, 함수형 인터페이스라는 것을 직접적으로 명시함으로써 팀원 또는 동료들이 코드를 쉽게 이해할 수 있습니다.


Java에서 사용되는 함수형 인터페이스

Java에서 기본적으로 제공하는 함수형 인터페이스에 대해 알아봅시다.


Consumer

이름에서 알 수 있듯이 Consumer 함수형 인터페이스는 데이터만 소비하고 아무것도 생성하지 않습니다. T 타입의 인자에 대해 모든 작업을 수행할 수 있으며, 반환 타입은 void입니다. Consumer 함수형 인터페이스의 구조는 다음과 같습니다.

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

Consumer 함수형 인터페이스의 accpet() 메서드는 제네릭 객체 t를 매개변수로 받아 t에 대한 작업을 수행합니다.

다음 예제는 매개변수의 값을 출력합니다.

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

public class Main {
  public static void main(String args[]) {
    Consumer<Integer> consumer = (Integer integer) -> {
      System.out.println(integer);
    };
    
    consumer.accept(20);
  }
}

[실행 결과]

20

Supplier

Supplier 함수형 인터페이스는 매개변수가 존재하지 않으며, T 타입의 객체를 반환합니다. Supplier 함수형 인터페이스 구조는 다음과 같습니다.

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

Supplier 함수형 인터페이스의 get() 메서드는 제네릭 객체를 반환합니다.

다음 예제는 특정 문자열 값을 반환합니다.

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

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

[실행 결과]

Hello

Function

Function 함수형 인터페이스는 T 타입의 인자를 받아 특정 작업을 수행 후 R 타입의 객체를 반환합니다. Consumer와 Supplier의 조합입니다. Function 함수형 인터페이스의 구조는 다음과 같습니다.

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

T와 R 타입이 존재하므로 Function 함수형 인터페이스는 인자의 타입과 반환 타입이 같을 수도 있고 다를 수도 있습니다.

다음 예제는 Integer 타입의 인자를 문자열과 조합 후 String으로 반환합니다.

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

public class Main {
  public static void main(String args[]) {
    Function<Integer, String> function = (Integer integer) -> {
      return "인자는 " + integer + "입니다.";
    };
    
    System.out.println(function.apply(100));
  }
}

[실행 결과]

인자는 100입니다.

Predicate

Predicate 함수형 인터페이스는 T 타입의 인자를 받아 boolean 값을 반환합니다. 구조는 다음과 같습니다.

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

다음 예제는 정수가 10보다 큰 경우 true, 그렇지 않으면 false를 반환합니다.

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

public class Main {
  public static void main(String args[]) {
    Predicate<Integer> predicate = (Integer integer) -> {
      if(integer > 10) return true;
      else return false;
    };

    System.out.println(predicate.test(100));
  }
}

[실행 결과]

true

Runnable

Runnable 함수형 인터페이스는 매개변수와 반환 타입이 존재하지 않습니다. run() 메서드에 정의된 코드만 실행합니다. Runnable 함수형 인터페이스의 구조는 다음과 같습니다.

@FunctionalInterface 
public interface Runnable { 
    void run(); 
}

다음 에제는 run() 메서드에 선언된 문자열을 출력합니다.

@FunctionalInterface
public interface Runnable {
  void run();
}

public class Main {
  public static void main(String args[]) {
    Runnable runnable = () -> {
      String str = "run run run";
      System.out.println(str);
    };

    runnable.run();
  }
}

[실행 결과]

run run run

BiConsumer

BiConsumer 함수형 인터페이스는 T 타입의 인자와 U 타입의 인자로 두 개의 인자를 가집니다. 반환 타입은 없으며, Consumer 함수형 인터페이스와 유사하게 동작합니다. BiConsumer 함수형 인터페이스의 구조는 다음과 같습니다.

@FunctionalInterface 
public interface BiConsumer<T, U> { 
  void accept(T t, U u); 
}

다음 예제는 List를 전달받아 특정 값이 존재하는지 출력합니다.

@FunctionalInterface
public interface BiConsumer<T, U> {
  void accpet(T t, U u);
}

public class Main {
  public static void main(String args[]) {
    BiConsumer<Integer, List<Integer>> biConsumer =
            (Integer integer, List<Integer> list) -> {
      if(list.contains(integer)) {
        System.out.println("존재한다.");
      } else {
        System.out.println("존재하지 않는다.");
      }
    };
    
    biConsumer.accpet(10, Arrays.asList(1, 3, 7, 10));
  }
}

[실행 결과]

존재한다.

정리

  • 함수형 인터페이스는 하나의 추상 메서드를 가지고 있습니다.
  • 함수형 인터페이스에는 정적 메서드, 기본 메서드, Object 클래스의 메서드가 존재할 수 있습니다.
  • 람다식을 사용하면 클래스를 구현하지 않아도 됩니다.
  • FunctionalInterface 어노테이션을 사용하여 함수형 인터페이스라는 것을 명시할 수 있습니다.
  • FunctionalInterface 어노테이션이 선언된 인터페이스에서 두 개 이상의 추상 메서드가 존재하거나 추상 메서드가 존재하지 않으면 컴파일 오류가 발생합니다.
반응형

댓글