본문 바로가기
JAVA

Java 8 - Function Interface

by 봄석 2019. 10. 13.

Java 8 - Function Interface

Java SDK 8의 java.util.function 패키지에는 수많은 Functional Interface들이 등록되어 있습니다.

이 패키지에 등록되어 있는 모든 인터페이스들은 @FunctionalInterface로 지정되어 있으며

API 문서에는 다음과 같은 설명이 추가되어 있습니다.

 

This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.

이것은 Functional Interface이며 그러므로 람다식이나 메서드 레퍼런스를 위한 할당 대상으로 사용될 수 있습니다.

 

Functional interface 에대하여 자세히 알아보고

 java.util.function 패키지의 대표적인 인터페이스에 대해 알아본 뒤

이 인터페이스들을 이용하는 java.util.stream.Stream 인터페이스에 람다식을 적용하는 방법에 대해서도 알아보도록 하겠습니다.

 

 

Functional Interface

Functional Interface란 함수를 **일급 객체로 사용할 수 없는 자바 언어의 단점을 보완하기 위해 도입되었습니다.

위 덕분에 자바는 전보다 간결한 표현이 가능해졌으며, 가독성이 높아지게 되었습니다.

 

Functional Interface는  일반적으로 

`구현해야 할 추상 메서드가 하나만 정의된 인터페이스`를 가리킵니다.

 

"Java Language Specification의 설명"

functional interface is an interface that has just one abstract method (aside from the methods of Object), and thus represents a single function contract.

Functional Interface는 (Object 클래스의 메서드를 제외하고) 단 하나의 추상 메서드만을 가진 인터페이스를 의미하며, 그런 이유로 단 하나의 기능적 계약을 표상하게 된다.

 

 Funcional Interface Ex

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

Java Specification에서는Object클래스를 제외하고라는 단어가 붙어있는데 이는 왜일까요??

자바언어에서 사용되는 모든 객체들이 Object 객체를 상속하고 있다는 것을 생각하면 쉽게 알 수 있습니다.

Interface의 구현체들이 Object객체의 메서드들을 모두 갖고 있기 때문에  이 메서드들은 funcional Interface의 대상이 되지 않는 것입니다.

 

 Funcional Interface Ex2

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

 

 

**일급 객체란?

일급 객체(영어: first-class object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.

보통 함수에 매개변수로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다.

출처 - https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B8%89_%EA%B0%9D%EC%B2%B4

 

 

@FunctionalInterface Annotation

Java SDK 8에서는 @FunctionalInterface이라는 어노테이션을 제공하여

작성한 인터페이스가  Fucntional Interface인지 확인할 수 있도록 하고 있습니다.

 

다시 Java Specification을 봐보도록 하겠습니다.

"Java Language Specification의 설명"

The annotation type FunctionalInterface is used to indicate that an interface is meant to be a functional interface (§9.8). It facilitates early detection of inappropriate method declarations appearing in or inherited by an interface that is meant to be functional.

It is a compile-time error if an interface declaration is annotated with @FunctionalInterface but is not, in fact, a functional interface.

Because some interfaces are functional incidentally, it is not necessary or desirable that all declarations of functional interfaces be annotated with @FunctionalInterface.

어노테이션 타입 FunctionalInterface는 어떤 인터페이스가 Functional Interface라는 것을 나타내기 위해 사용된다. 이것을 이용하면 부적절한 메서드 선언이 포함되어 있거나 함수형이어야 하는 인터페이스가 다른 인터페이스를 상속한 경우 미리 확인할 수 있다.

@FunctionalInterface로 지정되어 있으면서 실제로는 Functional Interface가 아닌 인터페이스를 선언한 경우 컴파일 타임 에러가 발생한다.

어떤 인터페이스들은 우연히 함수형으로 정의될 수도 있기 때문에, Functional Interface들이 모두 @FunctionalInterface 어노테이션으로 선언될 필요도 없고 그렇게 하는 것이 바람직하지도 않다.

 

즉, @FucntionalInterface어노테이션은  인터페이스가 Fucntional  Interface인지 아닌지 확실히 하기를 원할 때 사용하면 됩니다.

 

 

Lambda와 Fucntional Interface

우선 Java 8에서 함께 추가된  람다에 대하여 알아보겠습니다.

"Java Language Specification의 설명"

A lambda expression is like a method: it provides a list of formal parameters and a body - an expression or block - expressed in terms of those parameters.

(중간 생략)

Evaluation of a lambda expression produces an instance of a functional interface (§9.8). Lambda expression evaluation does not cause the execution of the expression's body; instead, this may occur at a later time when an appropriate method of the functional interface is invoked.

람다식은 메서드와 비슷하다: 규격을 갖춘 파라미터의 목록과 그 파라미터를 이용해서 표현된 몸통(표현식이든 블록이든)을 제공한다.

(중간 생략)

람다식이 평가(evaluation)되면 그 결과 Functional Interface의 인스턴스를 생성한다. 람다식의 처리 결과는 표현식 몸통을 실행하는 것이 아니다; 대신 나중에 이 Functional Interface의 적절한 메서드가 실제 호출(invoke)될 때 이것(표현식 몸통의 실행)이 일어난다.

 

 지금 중요하게 봐야 할 부분은

람다식의 평가 결과가 Fucntional Interface의 인스턴스라는 것입니다.

 

 

람다식은 보통 아래와 같이 표기하고 사용합니다.

() -> {}                // No parameters; result is void
() -> 42                // No parameters, expression body
() -> null              // No parameters, expression body
() -> { return 42; }    // No parameters, block body with return
() -> { System.gc(); }  // No parameters, void block body
 
() -> {                 // Complex block body with returns
  if (true) return 12;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                         
 
(int x) -> x+1              // Single declared-type parameter
(int x) -> { return x+1; }  // Single declared-type parameter
(x) -> x+1                  // Single inferred-type parameter
x -> x+1                    // Parentheses optional for single inferred-type parameter
 
(String s) -> s.length()      // Single declared-type parameter
(Thread t) -> { t.start(); }  // Single declared-type parameter
s -> s.length()               // Single inferred-type parameter
t -> { t.start(); }           // Single inferred-type parameter
 
(int x, int y) -> x+y  // Multiple declared-type parameters
(x, y) -> x+y          // Multiple inferred-type parameters
(x, int y) -> x+y    // Illegal: can't mix inferred and declared types
(x, final y) -> x+y  // Illegal: no modifiers with inferred types 

또 다른 예를 봐보겠습니다. 사칙 연산자를 표상하는  Functional Interface인 ArithmeticOperator와 같이 사용하는 예제입니다.

@FunctionalInterface
public interface ArithmeticOperator {
    public int operate(int a, int b);
}
@FunctionalInterface
public class ArithmeticCalculator {
    /**
     * 실제 계산은 ArithmeticCalculator에 위임한다.
     */
    public static int calculate(int a, int b, ArithmeticOperator operator){
        return operator.operate(a, b);
    }
} 
public class ArithmeticCalculatorTest {
    @Test
    public void testPlus{
        int firstNumber = 5;
        int secondNumber = 94;
  
        int result = ArithmeticCalculator.calculate(firstNumber, secondNumber, (a, b) -> a + b);
        Assert.assertEquals(firstNumber + secondNumber, result);
    }
} 

ArithmeticCalculatorTest를 보게 되면 , ArithmeticOperator의 구현체가 예상되는 곳(result의 ArithmeticCalculator.calculate의 3번째 인자)에 람다식이 입력되어 있습니다.

 

위에서 이야기했던 것처럼 lambda 식은 functional Interface의 인스턴스를 생성합니다.

즉, 3번째 인자인 람다식은 ArithmeticOperator의 인스턴스를 생성하게 됩니다.

  • (a, b) : ArithmeticOperator.operate 메서드의 두 개의 파라미터를 의미함 , 둘 다 int Type을 가질 것으로 추론(inffered 될 수 있다.
  • a + b : 람다식의 몸통, 중괄호로 묶여있지 않으므로 , 위 처리 값이 리턴된다. 

 

위와 같이 람다식은  Functional  Interface의 계약에 근거한 "추론"을 통해 인스턴스를 생성합니다.

위 추론이 가능한 이유는 Functional Interface 가 1개의 메서드만을 갖기로 전제했기 때문입니다.

 

람다는 새로운 클래스를 만들어야만 인스턴스 생성이 가능했던 자바 언어의 한계를 넘게 해 주었습니다.

 

 

람다식과 ArithmeticOperator 인터페이스를 사용한 예제 소스

// 자체 생성한 인터페이스
@FunctionalInterface
public interface ArithmeticOperator {
    public int operate(int a, int b);
}
  
// 자체 생성한 ArithmeticOperator 인터페이스를 사용하는 경우
public void testArithmeticOperator() {
    ArithmeticOperator plusOperator = (a, b) -> a + b;
    ArithmeticOperator minusOperator = (a, b) -> a - b;
    ArithmeticOperator multiplyOperator = (a, b) -> a * b;
    ArithmeticOperator divideOperator = (a, b) -> {
        if (b == 0) {
            b = 1;
        }
        return a / b;
    };
    ArithmeticOperator spareOperator = (a, b) -> {
        if (b == 0) {
            b = 1;
        }
        return a % b;
    };
    int a = new Random().nextInt(10000);
    int b = new Random().nextInt(10000);
    int plus = plusOperator.operate(a, b);
    int minus = minusOperator.operate(a, b);
    int multiply = multiplyOperator.operate(a, b);
    int divide = divideOperator.operate(a, b);
    int spare = spareOperator.operate(a, b);
 
    Assert.assertEquals(a + b, plus);
    Assert.assertEquals(a - b, minus);
    Assert.assertEquals(a * b, multiply);
    Assert.assertEquals(a / b, divide);
    Assert.assertEquals(a % b, spare);
}

람다식과 Function인터페이스를 사용한 예제 소스

// java.util.function.Function 인터페이스의 apply 메소드가 하나의 파라미터만 받을 수 있기 때문에 정의한 클래스
public class TwoNumbers {
    private int first;
    private int second;
    public TwoNumbers(int first, int second) {
        this.first = first;
        this.second = second;
    }
    public int getFirst() {
        return first;
    }
    public void setFirst(int first) {
        this.first = first;
    }
    public int getSecond() {
        return second;
    }
    public void setSecond(int second) {
        this.second = second;
    }
}
  
// 별도의 인터페이스나 메소드 없이 java.util.function.Function 인터페이스를 직접사용
@Test
private void testFunction() {
    Function<TwoNumbers, Integer> plusOperator = n -> n.getFirst() + n.getSecond();
    Function<TwoNumbers, Integer> minusOperator = n -> n.getFirst() - n.getSecond();
    Function<TwoNumbers, Integer> multiplyOperator = n -> n.getFirst() * n.getSecond();
    Function<TwoNumbers, Integer> divideOperator = n -> {
        if (n.getSecond() == 0) {
            return 0;
        }
        return n.getFirst() / n.getSecond();
    };
    Function<TwoNumbers, Integer> spareOperator = n -> {
        if (n.getSecond() == 0) {
            return 0;
        }
        return n.getFirst() % n.getSecond();
    };
    TwoNumbers numbers = new TwoNumbers(new Random().nextInt(10000), new Random().nextInt(10000));
    int plus = plusOperator.apply(numbers);
    int minus = minusOperator.apply(numbers);
    int multiply = multiplyOperator.apply(numbers);
    int divide = divideOperator.apply(numbers);
    int spare = spareOperator.apply(numbers);
 
    Assert.assertEquals(numbers.getFirst() + numbers.getSecond(), plus);
    Assert.assertEquals(numbers.getFirst() - numbers.getSecond(), minus);
    Assert.assertEquals(numbers.getFirst() * numbers.getSecond(), multiply);
    Assert.assertEquals(numbers.getFirst() / numbers.getSecond(), divide);
    Assert.assertEquals(numbers.getFirst() % numbers.getSecond(), spare);
}

 

 

 

그리고 Fucntion Package에 대하여 알아보도록 하겠습니다.

 

Funcion Package의 대표적인 Interface

 

Consumer<T>

void accept(T) 메서드가 선언되어 있는 인터페이스. 입력된 T type 데이터에 대해 어떤 작업을 수행하고자 할 때 사용합니다.

리턴 타입이 void이므로 처리 결과를 리턴해야 하는 경우에는 Function 인터페이스를 사용해야 합니다.
혹은 call by reference를 이용하여 입력된 데이터의 내부 프러퍼티를 변경할 수도 있습니다.

Function<T, R>

R apply(T) 메서드가 선언되어 있는 인터페이스. 입력된 T type 데이터에 대해 일련의 작업을 수행하고 R type 데이터를 리턴할 때 사용합니다.

입력된 데이터를 변환할 때 사용할 수 있습니다.

Predicate<T> boolean test(T) 메소드가 선언되어 있는 인터페이스. 입력된 T type 데이터가 특정 조건에 부합되는지 확인하여 boolean 결과를 리턴합니다.
Supplier

T get() 메서드가 선언되어있는 인터페이스입니다.

매개변수를 받지 않고 특정 타입의 결과를 리턴합니다.

 

Consumer <T>

Consumner Interface원형

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

사용 예시

public class ConsumerTest {
    @Test
    public void test() {
        andThen();
        accept();
    }

    private void andThen() {
        final Consumer<String> consumer1 = (i) -> System.out.println("consumer1 "+i);
        final Consumer<String> consumer2 =(i) -> System.out.println("consumer2 "+i);

        consumer1.andThen(consumer2).accept("is Consume!!");
    }

    private void accept() {
        final Consumer<String> greetings = value -> System.out.println("Hello " + value);
        greetings.accept("World");  // Hello World
    }
}

andThen

Consumer 인터페이스는 처리 결과를 리턴하지 않기 때문에 andThen() 디폴트 메서드는 함수적 인터페이스의 호출 순서만 정합니다.

 

Function <T , R>

Function Interface원형

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

 

사용 예시

public class FunctionTest {
    @Test
    public void test() {
        compose();
        andThen();
        identity();
    }

    private void compose() {
        Function<Integer, String> intToString = Objects::toString;
        Function<String, String> quote = s -> "'" + s + "'";

        Function<Integer, String> quoteIntToString = quote.compose(intToString);
        System.out.println(quoteIntToString.apply(5)); // '5'
    }

    private void andThen() {
        Function<String, String> upperCase = v -> v.toUpperCase();

        String result = upperCase.andThen(s -> s + "abc").apply("a");
        System.out.println(result); // Aabc
    }

    private void identity() {
        final Function<Integer, String> intToStr = value -> value.toString();
        System.out.println(intToStr.apply(10));  // 10

        final Function<Integer, Integer> identity = Function.identity();
        System.out.println(identity.apply(100));    // 100
    }
}

 

compose

before 메서드 실행 후 결과를 받아서 현재 메소드 실행합니다.
제네릭 타입은 처음 input과 마지막 output의 타입입니다.

즉 ,메서드를 순서대로 실행시키기 위한 함수입니다.

 

andThen

compose와 반대로

현재 메소드를 실행 후 매게 변수로 받은 람다를 실행합니다.

 

identity

자신의 값을 그대로 리턴하는 스테틱 메서드입니다.

 

compose , andThen 차이점

andThen()과 compose()의 차이점은 어떤 함수적 인터페이스부터 먼저 처리하느냐에 따라 다릅니다.

인터페이스A.compose(인터페이스B);
B실행 -> A실행

인터페이스A.andThen(인터페이스B);
A실행 -> B실행

 

 

Predicate <T>

Predicate Interface의 원형

@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);
  }
}
public class PredicateTest {
    @Test
    public void test() {
        tests();
        and_or_negate();
    }

    private void and_or_negate() {
        Predicate<Integer> predicateA = a -> a % 2 == 0;

        Predicate<Integer> predicateB = b -> b % 3 == 0;

        boolean result;
        result = predicateA.and(predicateB).test(9);
        System.out.println("9는 2와 3의 배수입니까? " + result);


        result = predicateA.or(predicateB).test(9);
        System.out.println("9는 2또는 3의 배수입니까? " + result);

        result = predicateA.negate().test(9);
        System.out.println("9는 홀수입니까? " + result);
    }

    private void tests() {
        Predicate<Integer> isZero = (i) -> i == 0;

        System.out.println(isZero.test(0));
        ;
    }
}

and

인자로 받는 다른 Predicate의 람다 수행 내용과 , 자기 자신의 람다 수행 내용을 &&연산합니다.

 

or

인자로받는 다른 Predicate의 람다 수행내용과 , 자기자신의 람다 수행내용을 ||연산합니다.

 

negate

람다 수행 내용을! 연산합니다.

 

 

 Supplier <T>

Supplier Inteface원형

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

Supplier를 활용하면 값을 그냥 전하는 것이 아니라, 중간에 로직을 추가해서 전달할 수 있습니다.

public class SupplierTest {
    @Test
    public void test(){
        Supplier<Integer> intSupplier = () -> {
            int num = (int) (Math.random() * 6) + 1;

            return num;
        };

        int num = intSupplier.get();
        System.out.println("눈의 수 : " + num);

    }
}

get

제네릭으로 전달받은 타입으로 값을 전달해줍니다.

 

 

쉽게 정리하면

  • Consumer -  어떤 데이터를 소비

  • Function - 어떤 기능을 수행

  • Predicate - 근거를 확인

  • Supplier - 특정 타입의 데이터를 공급

위의 인터페이스들을 그럼 어디에 쓸까요?? 

Java 8에서 같이 추가된 Stream 인터페이스와 결합하여 아주 강력한 기능을 제공합니다.

 

 

 

 

Function Interface Example with Stream

발췌 - https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#approach3

 

Lambda Expressions (The Java™ Tutorials > Learning the Java Language > Classes and Objects)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See JDK Release Notes for information about new fe

docs.oracle.com

Person이라는 데이터 목록(List)으로부터 나이가 일정 범위에 들어있는 데이터에 대해 출력하는 메서드를 작성하고자 합니다.

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
} 
  
// 사용하기
printPersonsWithinAgeRange(roster, 10, 20);

위의 코드의 안 좋은 점은  Person 목록에 대하여 "나이가 일정 범위에 있는 Person에 대하여 printPerson 한다"의 기능밖에 수행하지 못합니다.

특정 이름을 포함하는 사람을 출력하거나 , 특정 성별에 대하여 등등.. 다른 조건이 필요할 때는 그때마다 새로운 메서드를 정의해주어아합니다.

 

 

위 문제를 해결하기 위해서  Java Colleciton API는 Stream이라는 인터페이스를 새롭게 선보였습니다.

Stream과 람다식을 이용하여 매우 간단하게 위의 기능을 처리할 수 있습니다.

// 메소드나 클래스 생성 없이 바로 사용. roster 객체는 List 타입의 인스턴스
roster
    .stream()
    .filter( p -> p.getAge() >= 10 && p.getAge() <= 20 )
    .forEach( p -> p.printPerson() ); 

위 Stream코드를 보게 되면 

roster의 stream() 메서드를 호출하여 Stream 타입의 인스턴스를 얻습니다.

그리고 Stream.filter(Predicate)와 Stream.forEach(Consumer) 메서드를 

체인 형식으로 호출하고 있습니다.

 

즉, Stream Interface의 메서드들이 맨 위에서 봤던 Function Interface를 사용하고 있는 것입니다.

//Stream.filter
Stream<T> filter(Predicate<? super T> predicate)	

//Stream.map
<R> Stream<R> map(Function<? super T, ? extends R> mapper)

//Stream.forEach
void forEach(Consumer<? super T> action) 

 

 

 

정리

  1. Functional Interface는 Object 클래스의 메서드를 제외하고 단 하나의 메서드만 가지고 있는 인터페이스를 의미합니다
  2. 람다식은 기본적으로 "파라미 터부 -> {몸통부}"의 형태를 띠며 평가 결과로 Functional Interface의 인스턴스를 생성할 수 있습니다,
  3. Stream 인터페이스는 람다식과 결합하여 List를 일괄적으로 처리할 수 있도록 도와준다. 대표적인 메서드로 filter, map, forEach 등이 있다. 
    이 API를 이용하면 클래스나 메서드를 만들지 않고도 효과적이고 가독성 높은 코드를 작성할 수 있습니다.

 

'JAVA' 카테고리의 다른 글

Java - Comparable, Comparator  (1) 2019.10.15
Java 8 - More Functional Interface!!  (0) 2019.10.13
Java 8 - Interface바뀐점을 알아보기  (0) 2019.10.11
equals,hashCode 알아보기  (2) 2019.10.01
Lombok 알아보기  (4) 2019.09.15

댓글