본문 바로가기
Programming Language/Java

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

by 곰민 2023. 1. 8.

Java - Lambda Expression에 관하여 블로그 글을 작성하려고 보니 함수형 인터페이스(functional interface)도 긴밀하게 엮여있어
하나의 글로 작성을 하기엔 너무 길고 가독성도 떨어질 것 같아
lambda 이전에 functional interface에 관한 글을 선행하여 작성하려고 합니다.
Java8에서 추가된 Functional Interface에 대해서 알아봅시다 🧐예제를 최대한 많이 넣으려고 했습니다!.

 

 

Functional Interface란?


Functional Interface란
Object Class의 메서드를 제외하고 '구현해야 할 추상 메서드가 하나만 정의된 인터페이스'를 의미합니다.

 

예시 코드

//Functional Interface인 경우: 메서드가 하나만 있음
public interface Functional {
    public boolean test(Condition condition);
}
  
//java.lang.Runnable도 결과적으로 Functional Interface임
public interface Runnable {
    public void run();
}
  
//구현해야 할 메서드가 하나 이상 있는 경우는 Functional Interface가 아님
public interface NonFunctional {
    public void actionA();
    public void actionB();
}
 
🤔 Object Class를 제외하는 이유는?
자바에서 사용되는 모든 객체들이 Object 객체를 상속하고 있기 때문에, 인터페이스 구현체들이 Object 객체의 메서드를 굳이 재정의하지 않아도 그 메서드들을 소유할 수 있으므로 Object Class와 관련된 메서드들은 Functional Interface의 대상이 되지 않는다.

 

예시 코드

//Object 객체의 메서드만 인터페이스에 선언되어 있는 경우는 Functional Interface가 아님
public interface NotFunctional {
    public boolean equals(Object obj);
}
  
//Object 객체의 메서드를 제외하고 하나의 추상 메서드만 선언되어 있는 경우는 Functional Interface임
public interface Functional {
    public boolean equals(Object obj);
    public void execute();
}
  
//Object객체의 clone 메서드는 public 메서드가 아니기 때문에 Functional Interface의 대상이 됨
public interface Functional {
    public Object clone();
}
public interface NotFunctional {
    public Object clone();
    public void execute();
}
 
🤔 Java8에서 Functional Interface를 도입한 이유는 무엇일까?
함수형 프로그래밍을 지원하기 위해서 도입되었다.
간략하게 함수형 프로그래밍이란 변경 가능한 상태를 최대한 제거하려고 노력하고, 내부 상태를 갖지 않아 같은 입력에 대해서 항상 같은 출력이 보장되는 함수인 순수 함수를 지향하며 불변성을 추구한다.

 

  1. 프로그램 검증이 쉽고
  2. 계산한 값을 캐싱 하는 등의 최적화가 가능하며
  3. 스레드가 프로그램 상태를 공유하기 때문에 생기는 동시성 문제로부터 자유로운 편이며
  4. 함수를 재사용 할 수 있는 등의 장점들이 존재한다.

자세한 내용은 이후 포스팅에서 다루도록 하겠습니다.

@FunctionalInterface Annotation


Java SDK 8에서는 @FunctionalInterface라고 하는 어노테이션을 제공하여 작성한 인터페이스가 Functional Interface 인지 확인할 수 있도록 하고 있다.
java에서 기본으로 제공하는 FuntionalInterface가 아닌 직접 만든 Functional Interface에는 항상@FunctionalInterface 애너테이션을 사용해야 한다.
(Effective Java Item 44 표준 함수형 인터페이스를 사용하라)

 

  1. 인터페이스가 람다용으로 설계된 것임을 알려주며
  2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일이 되게 해주며
  3. 그 결과 유지 보수 과정에서 누가 실수로 메서드를 추가하지 못하게 막아주거나 함수형이어야 하는 인터페이스가 다른 인터페이스를 상속한 경우 미리 확인할 수 있다.
@FunctionalInterface
public interface NotFunctional {
    public boolean equals(Object obj);
}
 

Java.util.functional에서 제공하는 interface 종류


🚀 Operator 연산 관련 함수형 인터페이스


예제 코드에서 나오는 () → lambda expression 람다 표현식과
:: 메서드 참조는 추후 포스팅할 예정 😅
최대한 메서드 참조를 활용하였고 메서드 참조 표현식 :: 은 특정 클래스에 해당 메서드를
참조하는데 () -> 람다 식을 사용할시 적어야 하는 매개변수를 생략할 수 있도록 해줍니다.

 

🚀 UnaryOperator(단항)


UnaryOperator - Function<T, R>
메서드 시그니처 - T apply (T t)
또 다른 기본형 인터페이스인 Function<T,T>를 상속받으며 T apply(T t)를 호출합니다.
Type T의 인자를 하나 받고 동일한 Type T 객체를 리턴하는 함수형 인터페이스.

 

  • UnaryOperator
  • package java.util.function;
    @FunctionalInterface
    public interface UnaryOperator<T> extends Function<T, T> {
    		//..
        static <T> UnaryOperator<T> identity() {
            return t -> t;
        }
    }
    
     
    • identity()
      • input 값을 항상 그대로 리턴하는 unaryoperator를 리턴합니다.
    • Function<T, T>
  • @FunctionalInterface
    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;
        }
    }
    
 

🛠️ 사용 예시


String::toLowerCase 예시

public static void main(String[] args) {
        String testString1 = "convert to capital letter";
        UnaryOperator<String> convertToUpper = String::toUpperCase;
        System.out.println(convertToUpper.apply(testString1));
        UnaryOperator<String> convertToIdentity = UnaryOperator.identity();
        System.out.println(convertToIdentity.apply(testString1));
    }
//출력
//CONVERT TO CAPITAL LETTER
//convert to capital letter

Stirng에 toUpperCase 메서드를 참조한 뒤 apply 호출시 testString1 인자를 전달하면
String 클래스에서 참조한 toUppercCase 메서드 참조 구현 내용이 수행되고 인자와 같은 타입의 객체가 리턴됩니다.
identity의 경우 그대로 출력합니다.

 

🚀 BinaryOperator(이항)


BinaryOperator
메서드 시그니처 - T apply (T t1, T t2)
BiFunction<T,T,T>를 상속받으며 T apply(T tq, T t2)를 호출함
Type T의 인자를 두 개 받고 동일한 Type T 객체를 리턴하는 함수형 인터페이스.

 

또다른 기본형 인터페이스인 BiFunction<T,R>를 상속받으며 Function 의 메서드 시그니처를 따른다.

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
 
    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

 

BinaryOperator.maxBy()

  • Comparator를 이용하여 두 개의 객체를 비교하고 큰 값을 리턴하는 BinaryOperator 생성합니다.

 BinaryOperator.minBy()

  • Comparator를 이용하여 두 개의 객체를 비교하고 작은 값을 리턴하는 BinaryOperator 생성합니다.

BiFunction<T, T, T>

@FunctionalInterface
public interface BiFunction<T, U, R> {

    R apply(T t, U u);

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

🛠️ 사용 예시


BigInteger::add 예시

public class FItest {

    public static void main(String[] args) {
        BigInteger Bone = new BigInteger("11");
        BigInteger Btwo = new BigInteger("22");
        BinaryOperator<BigInteger> maxOperator = BinaryOperator.maxBy(Comparator.comparing(BigInteger::intValue));
        BinaryOperator<BigInteger> minOperator = BinaryOperator.minBy(Comparator.comparing(BigInteger::intValue));
        BinaryOperator<BigInteger> bigOperator = BigInteger::add;
        System.out.println("maxOperator = " + maxOperator.apply(Bone, Btwo));
        System.out.println("minOperator = " + minOperator.apply(Bone, Btwo));
        System.out.println("minOperator = " + bigOperator.apply(Bone, Btwo));
    }

}
//출력
//22
//11
//33
  • maxBy / minBy는 comparator를 활용하기 때문에 Comparator에 BigInteger의 IntValue를 참조해서 전달합니다.
  • BigInteger::add로 BigInteger type 두 가지를 받아서 참조한 add메서드 구현 내용을 수행한 뒤 BigInteger type으로 반환합니다.
 

🚀 Predicate 판단 관련 함수형 인터페이스


Predicate
메서드 시그니처 - boolean test(T t);
type T를 인자로 받고 Boolean을 리턴하는 함수형 인터페이스.

@FunctionalInterface
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);
    }
}
  • and는 Predicate를 연결하는 Chain 메서드 들임 Predicate들의 결과들을 AND 연산하고 그결과를 리턴합니다.
  • or은 and처럼 Predicate를 연결하는 메서드이며 Predicate들의 OR 연산 결과를 리턴합니다.
  • isEqual은 인자로 전달된 Object 와 같은지를 비교하는 Predicate를 리턴합니다.
  • negate()는 Predicate가 리턴하는 Boolean 값과 반대되는 값을 리턴하는 Predicate를 리턴함
    즉 논리연산에서의 NOT이 Predicate 앞에 붙는다고 생각할 수 있습니다.

🛠️ 사용 예시


Collection::isEmpty 예시

public class FItest {

    private static boolean isOverThenFive(int num) {
        return num > 5;
    }

    public static void main(String[] args) {
		Predicate<Integer> isOverPredicate = FItest::isOverThenFive;
        Predicate<Integer> isUnderPredicate = FItest::isUnderThenFive;
        Predicate<List<Integer>> cpredicates = List::isEmpty;
        Predicate<String> equalPredicate  = Predicate.isEqual("test_string");
        System.out.println("cpredicates = " + cpredicates.test(new ArrayList<Integer>(Arrays.asList(1,2,3))));
        System.out.println("isEquals = " + equalPredicate.test("not_same"));
				//test값이 "test_string" 이여야 true 반환
        System.out.println("predicate = " + isOverPredicate.test(3));
        System.out.println("Negate_predicate = " + isOverPredicate.negate().test(3));
        System.out.println("Or_predicate = " + isOverPredicate.or(isUnderPredicate).test(3));
        System.out.println("And_predicate = " + isOverPredicate.and(isUnderPredicate).test(3));

    }
}

// 출력
//cpredicates = false
//isEquals = false
//predicate = false
//Negate_predicate = true
//Or_predicate = true
//And_predicate = false
 

🚀 Function 인수와 반환타입이 다른 함수형 인터페이스


Function으로도 인수와 반환 타입이 같도록 사용할 수 있지만
해당 경우에는 Unary를 사용하는 것이 더 낫습니다.

 

Function
메서드 시그니처 - R apply(T t)
1개의 Type T 인자를 받고, 1개의 객체 Type R을 리턴하는 함수형 인터페이스.

 

@FunctionalInterface
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;
    }
}
  • andThen()은 인자로 Function 객체를 받으며 다수의 Function들을 순차적으로 실행합니다.
  • compose()는 인자로 Function 객체를 받으며 두 개의 Function을 합쳐진 하나의 Function 객체로 리턴합니다.
    합성되는 두 개의 Function은 인자 타입과 리턴 타입이 서로 맞아야 합니다.
  • identity()는 input 값을 항상 그대로 리턴하는 function을 리턴합니다.
 

🛠️ 사용 예시


Array::asList 예시

public class FItest {

    private static Integer half(Integer x) {
        return x / 2;
    }

    private static Double multiply(Integer x) {
        return x * 2.0;
    }

    public static void main(String[] args) {
        Function<Integer, Integer> half = FItest::half;
        Function<Integer, Double> multiply = FItest::multiply;
        Function<Integer, Integer> identity = Function.identity();
        String[] alpabetList = new String[] {"A", "B", "C", "D"};
        Function<String[], List<String>> list = Arrays::asList;
        System.out.println("half = " + half.apply(2));
        System.out.println("multiply = " + multiply.apply(2));
        System.out.println("andThen = " + half.andThen(multiply).apply(6));
        System.out.println("compose = " + half.compose(identity).apply(6));
        System.out.println("identity = " + identity.apply(6));
        System.out.println("list = " + list.apply(alpabetList));
    }

}
//출력
//half = 1
//multiply = 4.0
//andThen = 6.0
//compose = 3
//identity = 6
//list = [A, B, C, D]
 

🚀 Supplier 인수를 받지 않고 값을 반환하는 함수형 인터페이스


Supplier
메서드 시그니처 - T get()
Supplier는 인자를 받지 않고 Type T 객체를 리턴하는 함수형 인터페이스입니다.

 

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

🛠️ 사용 예시


Instant::now 예시

public class FItest {

    private static String getString() {
        return "stringTest";
    }

    public static void main(String[] args) {
        Supplier<String> stringSupplier = FItest::getString;
        Supplier<Instant> timeSupplier = Instant::now;
        System.out.println("stringSupplier = " + stringSupplier.get());
        System.out.println("timeSupplier = " + timeSupplier.get());
   }
}
//출력
//stringSupplier = stringTest
//timeSupplier = 2022-12-16T12:19:58.171838Z
 

🚀 Consumer 인수를 받고 소비하며 반환값은 없는 함수형 인터페이스


Consumer
메서드 시그니처 - void accept(T t)
Consumer 1개의 Type T 인자를 받고 리턴 값이 없는 함수형 인터페이스입니다.

 

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

- andThen() Consumer들을 연결해 주는 Chaining 메서드.
 

🛠️ 사용 예시


System.out:println 예시

public class FItest {

    private static String getString() {
        return "stringTest";
    }

    public static void main(String[] args) {
        Consumer<String> consumer = System.out::printf;
        consumer.accept("consumer test");
   }
}
//출력
//consumer test
 

🛠️ Java.util.function 내의 43개의 Interface


Java.util.function 패키지에는 43개의 interface가 존재한다.
기본 interface 6가지 기억하면 나머지는 유추가 가능하다.

  1. UnaryOperator
  2. BinaryOperator
  3. Predicate
  4. Function<T, R>
  5. Supplier
  6. Consumer
  • 6개의 기본 interface는 기본 타입(int, long, double)으로 3개씩 접두어로 붙으면서 변형이 들어간다. (6x3 =18개)
    • ex)IntPredicate, IntBinaryOperator, IntFunction …
  • Function 인터페이스에는 기본 타입을 반환하는 변형이 총 9가지 있다.
    • SrcToResult 변형 - 입력과 결과 모두 기본 타입.
      • ex) LongToIntFunction , IntToLongFunction … (6가지)
    • ToResult 변형 - 입력은 객체 참조 결과는 기본 타입
      • ex)ToLongFunction<int []> → int[] 인수를 받아 long으로 반환 (3가지)
  • 인수를 2개씩 받는 변형이 9가지 있다
    • BiPredicate<T, U> → ToLongBiPredicate / ToIntBiPredicate / ToDoubleBiPredicate
    • BiFunction<T,U,R> → Long / Int / Double
    • BiConsumer<T, U> → Long / Int / Double
  • Boolean을 반환하는 BooleanSupplier는 Supplier의 변형 (1가지)
  • 18 + 18+ 1 = 37(변형) + 6(기본) = 제공하는 43가지 인터페이스

 

표준 함수형 인터페이스를 사용하라


Effective Java Item 44에서는 필요한 용도에 알맞은 위에 43가지 표준 함수형 인터페이스가 존재한다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하는 것을 권장한다.
불필요한 중복을 피할 수 있으며, andThen(), compose() 와 같은 유용한 디폴트 메서드를 많이 제공하기 때문에 다른 코드와의 상호 운용성도 좋아진다.
 

표준 함수형 인터페이스가 아니라 직접 작성하는 것이 좋은 경우


Comparator 인터페이스를 떠올려보면 알 수 있다.

 

	// Comparator
	@FunctionInterface
	public interface Comparator<T> {
	    int compare(T o1, T o2);
	}
	// ToIntBiFunction
	@FunctionalInterface
	public interface ToIntBiFunction<T, U> {
	    int applyAsInt(T t, U u);
	}
  • Comparator 인터페이스는 구조적으로 ToIntBiFunctiuon<T, U> 와 동일하다.
  • Comparator기 독자적 인터페이스로 살아남아야 하는 이유가 몇가지 존재한다.
  1. 자주 쓰이며, 이름 자체가 용도를 명확히 설명해주는 경우
  2. 반드시 따라야하는 규약이 있는 경우
  3. 유용한 디폴트 메서드를 제공할 수 있는 경우
  • 위와 같이 3가지중 하나이상을 만족한다면 전용 함수형 인터페이스를 구현해야 하는 건 아닌지 진중히 고민해야 한다.
  • 만약 구현한다면 위에 언급햇듯 @FunctionalInterface 애너테이션을 꼭 붙여줘야 한다.

참조


effective java

백기선 The java 8

codechacha.com

https://engineering.linecorp.com/ko/blog/functional-programing-language-and-line-game-cloud/

반응형

댓글