본문 바로가기
Java

[Java] - @FunctionalInterface 함수형 인터페이스

by 주발2 2021. 2. 11.
반응형

 안녕하세요~ 이전에 운영하던 블로그GitHub, 공부 내용을 정리하는 Study-GitHub 가 있습니다!

 네이버 블로그

 GitHub

Study-GitHub

 🐔


✔ 함수형 인터페이스 - @FunctionalInterface

안녕하세요~ 이번에 정리할 내용은 함수형 인터페이스 입니다.

 

함수형 인터페이스를 얘기하기 전에 일급 객체(First Class Citizon)에 대해 간단히 알아보겠습니다.

 

 

First Class Citizon

 First Class Citizon 은 아래의 속성들을 모주 만족해야 합니다.

     변수에 값을 할당할 수 있어야 합니다.

     함수의 파라미터로 넘겨줄 수 있어야 합니다.

     함수의 반환값이 될 수 있어야 합니다.

Java에 메서드는 위 조건의 모두를 만족하지 않으므로 일급객체가 아니고,

따라서 Java는 함수형 프로그래밍 언어가 아닙니다.

하지만, Java8에서는 함수를 일급객체처럼 다룰 수 있게 함수형 인터페이스를 제공합니다.

 

 

@FunctionalInterface 어노테이션은 자바8에서 추가된 어노테이션입니다.

함수형 인터페이스(Functional Interface)추상 메서드가 딱 하나만 존재하는 인터페이스를 말합니다.

그리고 람다식은 이러한 함수형 인터페이스를 기반으로만 작성이 될 수 있습니다.

즉, 함수형 인터페이스를 사용하는 이유람다식은 함수형 인터페이스로만 접근이 가능하기 때문에 사용합니다!

간단한 예시를 통해 추상 메서드가 하나만 존재하는 인터페이스란 어떤것인지 살펴보겠습니다.

 

• 매개변수가 있고 반환하지 않는 람다식

위 예제에서 보듯 여러가지의 표현이 가능합니다.

 

(String s) -> { System.out.println(s); }

줄임이 없는 람다식은 위와 같습니다. 매개변수 정보에 소괄호를 하고 메소드 몸체에는 중괄호를 합니다.

 

(String s) -> System.out.println(s); 

(s) -> System.out.println(s);

중괄호를 생략할때는 해당 문장의 끝에 위치한 세미콜론도 함께 생략해야 합니다.

 

그리고 위 코드에서 매개변수의 정보에 대해 s가 String형 임은 컴파일러의 입장에서 유추가 가능합니다.

따라서 두 번째 방식처럼, 매개변수의 자료형 정보(String)도 생략이 가능합니다.

 

s -> System.out.println(s)

그리고 매개변수가 위와 같이 하나일 경우에는 다음과 같이 소괄호도 생략할 수 있습니다.

;

System.out::println

마지막 표현에서 ::메소드 참조(Method References)를 의미합니다.

 

 

✔ 메소드 참조(Method References)란? 

람다식도 결국 메소드의 정의이므로 다음과 같이 생각해 볼 수 있습니다.

"이미 정의되어 있는 메소드가 있다면, 이 메소드의 정의가 람다식을 대신할 수 있지 않을까?"

실제 메소드 정의는 람다식을 대신할 수 있는데, 이를 "메소드 참조(Method References)"라고 합니다.
코드의 양을 줄이고, 가독성도 개선된다는 점에서 '메소드 참조'는 람다식으로 줄어든 코드의 양을 조금 더 줄일 수 있습니다.
ClassName::MethodName

출처: 윤성우의 열혈 Java 프로그래밍
Consumer<List<Integer>> c = l -> Collections.reverse(l); // 저장 순서 뒤집기
Consumer<List<Integer>> c = Collections::reverse();      // 위와 동일한 결과

Function<char[], String> f = ar -> { return new String(ar); };
Function<char[], String> f = String::new;  // 생성자 참조



 

• 매개변수가 있고 반환하는 람다식

위의 예제에서 등장한 람다식은 다음과 같습니다.

(a, b) -> { return a+b; };

메서드 몸체에 해당하는 내용이 return 문일경우, 그 문장이 하나이더라도 중괄호의 생략이 불가능합니다.

하지만 위의 람다식은 다음과 같은 표현이 가능합니다.

 

 

(a, b) -> a + b;

위의 경우 메서드 몸체에 연산이 등장하는데, 이 연산이 진행되면 그 결과로 값이 남게됩니다.

그러면 이 값은 별도로 명시하지 않아도 반환의 대상이 됩니다.

따라서 return문이 메서드 몸체를 이루는 유일한 문장이라면 위와 같이 생략해서 작성할 수 있습니다.

 

 

 

 

간단하게 람다식을 통해 함수형 인터페이스의 예시에 대해 알아보았는데요,

함수형 인터페이스를 다시 정의하자면 다음과 같습니다.

함수형 인터페이스(Functional Interface)추상 메서드가 단 하나만 존재하는 인터페이스를 말합니다.
그리고 람다식은 이러한 함수형 인터페이스를 기반으로만 작성이 될 수 있습니다.

다음은 함수형 인터페이스와 해당 어노테이션이 붙어 있는 인터페이스의 예시입니다.

@FunctionalInterface
interface Calculate {
    int cal(int a, int b);
}

 

 

@FunctionalInterface 어노테이션은 함수형 인터페이스 부합하는지를 확인하기 위한 어노테이션 타입입니다.

만약 위 인터페이스에서 둘 이상의 추상 메서드가 존재한다면, 이는 함수형 인터페이스가 아니기 때문에 컴파일 오류를 발생합니다.

Invalid '@FunctionalInterface' annotation; Calculate is not a functional interface

@FunctionalInterface 어노테이션이 타당하지 않고, Calculate 는 함수형 인터페이스가 아니라고 말합니다.

하지만, static이나 default 선언이 붙은 메서드의 경우 함수형 인터페이스에는 아무런 영향을 미치지 않습니다.

따라서 다음 인터페이스도 함수형 인터페이스라고 할 수 있습니다.

 

 

인터페이스는 제네릭으로 정의하는 것이 가능합니다. 이러한 방식은 자바 8 에서부터는 아주 흔한 일이 되었습니다.

왜 인터페이스를 제네릭으로 정의하는것에 대해 설명을 하느냐?

-> 이후에 나올 네 종류의 함수형 인터페이스에서 사용되기 때문입니다!

위의 세 문장의 람다식은 동일하지만, 참조변수의 자료형이 모두 다르므로 전혀 다른 인스턴스의 생성으로 이어집니다.

정수형 덧셈을 하는 인스턴스, 문자열, 실수형을 참조하는 각 인스턴스가 생성됩니다.

 

 

 

자바에 정의되어 있는 함수형 인터페이스

  • Function<T, R>

위 사진은 Function<T, R> 의 내부 코드중 일부 입니다.

위 인터페이스에는 다음과 같은 추상 메서드가 존재합니다.

R apply(T t);   // 전달 인자와 반환 값이 모두 존재할 때

apply 라는 메서드가 존재하고, 인자로는 어떤 타입의 T(제네릭) 을 받고 R(제네릭) 을 리턴합니다.

 

 

✔ Function 예제

package other;

import java.util.function.Function;

public class FunctionalInterfaceExamples {

    public static void main(String[] args) {
        Function<String, Integer> toInt = new Function<String, Integer>(){

            @Override
            public Integer apply(String t) {
                return Integer.parseInt(t);
            }
        };
        
        // Function<String, Integer> toInt = Integer::parseInt;

        Integer number = toInt.apply("100");
        System.out.println("number: " + number);
    }
}

함수형 인터페이스인 Function<T, R>을 사용해 내부의 apply() 메서드를 구현한 코드입니다.

Function<String, Integer> toInt = new Function<String, Integer>() {
    @Override
    public Integer apply(String t) {
        return Integer.parseInt(t);
    }
};

위 예제를 리팩토링 해서 람다 표현식으로 변경해보도록 하겠습니다.

 

 

먼저 new 부터 메서드 이름(apply)까지 모두 제거하고, 마지막 중괄호를 제거합니다.

Function<String, Integer> toInt = (String t) {
    return Integer.parseInt(t);
};

 

그 후 매개변수와 중괄호 사이에 ->(람다표현식) 를 추가합니다.

Function<String, Integer> toInt = (String t) -> { 
    return Integer.parseInt(t);
}

 

 

위 구조만으로도 람다 표현식이지만, 좀 더 간결하게 나타내 보겠습니다.

    • Type 자료형을 제거합니다.

     return 문도 제거합니다.

     필요없는 중괄호를 제거합니다.

Function<String, Integer> toInt = t -> Integer.pasreInt(t);

 

 

위 람다식을 메소드 참조를 사용하면 최종적으로 다음과 같습니다.

Function<String, Integer> toInt = Integer::parseInt;

 

 

 

 

  • Consumer<T>

위 사진은 Consumer<T> 의 내부 코드중 일부 입니다.

위 인터페이스에는 다음과 같은 추상 메서드가 존재합니다.

void accept(T t); // 전달된 인자 기반으로 '반환' 이외의 다른 결과를 보일 때

Consumer 소비한다 라는 단어만 봐도 유추할 수 있듯이, 인자는 전달받지만 반환은 하지 않습니다.

 

 

✔ Consumer 예제

package other;

import java.util.function.Consumer;

public class FunctionalInterfaceExamples {

    public static void main(String[] args) {

        Consumer<String> print = new Consumer<String>() {
        
            @Override
            public void accept(String t) {
                System.out.println(t);
            }
        };

        //Consumer<String> print = System.out::println;
        print.accept("Hello World");
    }
}

함수형 인터페이스인 Consumer<T>을 사용해 내부의 accept() 메서드를 구현한 코드입니다.

​(람다 표편식은 주석 처리를 해놓은 부분이며, 람다 방식으로의 과정은 생략하도록 하겠습니다!)

 

 

 

 

  • Predicate<T>

위 사진은 Predicate<T> 의 내부 코드중 일부 입니다.

위 인터페이스에는 다음과 같은 추상 메서드가 존재합니다.

boolean test(T t); // 전달된 인자를 대상으로 True, False 를 판단할 때

따라서 Predicate<T> 인터페이스는 전달된 인자를 판단하여 true 또는 false를 반환해야 하는 상황에서 유용합니다.

 

 

✔ Predicate 예제

package other;

import java.util.function.Predicate;

public class FunctionalInterfaceExamples {

    public static void main(String[] args) {

        Predicate<Integer> isPositive = i -> i > 0;

        System.out.println(isPositive.test(-1));
        System.out.println(isPositive.test(0));
        System.out.println(isPositive.test(1));
    }
}

 

 

package other;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class FunctionalInterfaceExamples {

    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5);
        List<Integer> positiveNumbers = new ArrayList<>();

        /* 양수만 저장 */
        for (Integer num : numbers) {
            if (num > 0) {
                positiveNumbers.add(num);
            }
        }

        System.out.println("Positive Integers: " + positiveNumbers);

        Predicate<Integer> lessThan3 = i -> i < 3;
        List<Integer> numbersLessThan3 = new ArrayList<>();

        /* 양수만 저장 */
        for (Integer num : numbers) {
            if (lessThan3.test(num)) {
                numbersLessThan3.add(num);
            }
        }
        System.out.println("numbersLessThan3: " + numbersLessThan3);
    }
}

위 코드는 List를 생성하고, for문을 순회하며 조건문을 통해 필터링하는 동일한 코드가 중복되는

보일러 플레이트 코드가 많습니다.

 

함수를 만들어서 빼낸 뒤 중복 코드를 제거해보도록 하겠습니다.

 

package other;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class FunctionalInterfaceExamples {

    private static <T> List<T> filter(List<T> list, Predicate<T> filters) {
        List<T> result = new ArrayList<>();
        for (T input : list) {
            if (filters.test(input)) {    // Predicate의 조건에 따라 분류
                result.add(input);
            }
        }
        return result;
    }

    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5);

        Predicate<Integer> isPositive = i -> i > 0;

        List<Integer> positiveNumbers = new ArrayList<>();

        /* 양수만 저장 */
        for (Integer num : numbers) {
            if (num > 0) {
                positiveNumbers.add(num);
            }
        }

        System.out.println("Positive Integers: " + positiveNumbers);

        Predicate<Integer> lessThan3 = i -> i < 3;
        List<Integer> numbersLessThan3 = new ArrayList<>();

        for (Integer num : numbers) {
            if (lessThan3.test(num)) {
                numbersLessThan3.add(num);
            }
        }

        System.out.println("numbersLessThan3: " + numbersLessThan3);
        System.out.println("Positive Integers: " + filter(numbers, isPositive));
        //System.out.println("Positive Integers: " + filter(numbers, i -> i > 0));
        System.out.println("numbersLessThan3: " + filter(numbers, lessThan3));
    }

}

 

 

ListPredicate 를 매개변수로 받는 제네릭 메서드 filter() 를 만들었습니다.

filter() 함수는 매개변수의 List 원소들에 대해 반복문을 통해 Predicate<T> 인터페이스의 test 메서드를 통해 true인 경우 리턴할 List인 result 에 저장합니다. 말이 길어졌는데요, 요약하면 다음과 같습니다.

List , Predicate를 인자로 받고, List의 원소들이 특정 조건을 만족할 경우(test) 다른 리스트(result)에 저장합니다.

 

 

 

마지막으로 Predicate<T> 인터페이스를 통해 홀수, 짝수의 합을 구하는 예제를 보겠습니다.

package other;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class PredicateExample {

    public static int filterSum(Predicate<Integer> filter, List<Integer> list) {
        int sum = 0;
        for (Integer num : list) {
            if (filter.test(num)) {
                sum += num;
            }
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        int sum;
        sum = filterSum(i -> i % 2 == 0, list);
        System.out.println("짝수 합: " + sum);

        sum = filterSum(i -> i % 2 == 1, list);
        System.out.println("홀수 합: " + sum);
    }

}

 

 

 

 

  • Supplier<T>

위 사진은 Supplier<T> 의 내부 코드 입니다.

인터페이스는 다음과 같은 추상메서드가 존재합니다.

T get(); // 매개변수가 없고, 단순히 무엇인가를 반환할 때

다른 함수형 인터페이스들의 추상메서드는 모두 매개변수를 받았는데,

Supplier<T> 는 특이하게 매개변수를 받지 않고 단순히 무엇인가를 반환하는 추상메서드가 존재합니다.

package other;

import java.util.function.Supplier;

public class SupplierExample {

    public static void main(String[] args) {
        Supplier<String> helloSupplier = () -> "Hello ";

        System.out.println(helloSupplier.get() + "World");
    }
}

위 코드에서 Supplier<T>의 제네릭 타입은 String 이므로, helloSupplier.get() 을 통해 "Hello " 를 받아올 수 있습니다.

Supplier<T> 인터페이스는 추상 메서드 get()을 통해 Lazy Evaluation 이 가능합니다.

 

Lazy Evaluation
직역은 "게으른 연산"으로, 불필요한 연산을 피하기 위해 연산을 지연시키는 것을 말합니다.
즉, 불필요한 연산을 피한다 는 의미입니다.

 

Lazy Evaluation을 통해 효율적인 동작을 이끌어 낼 수 있는 코드를 알아보겠습니다.

getVeryExpensiveValue() 메서드는 3초간 sleep 후 "JuHyun" 을 리턴하는 함수이고,

printIfValidIndex(int number, String value) 메서드는 조건에따라 출력을 달리하는 함수입니다.

위 함수에서 number의 값에 관계 없이 메서드가 총 3번 호출이 되기 때문에 9초가 경과하게 됩니다.

 

이제 위 코드를 Supplier<T> 인터페이스를 통해 Lazy Evaluation 불필요한 연산을 줄여보도록 하겠습니다.

위의 경우에는 printInfValidIndex() 메서드의 매개변수로 메서드의 반환값이 아닌, 함수가 전달되었기 때문에 실제적으로 valueSupplier.get() 을 호출할 때만 getVeryExpensiveValue() 메서드를 호출하게 됩니다.

위 코드의 결과를 보면 실제적으로 불필요한 연산을 제거하고, 결과가 3초로 나오는것을 확인할 수 있습니다.

 

이상으로 Java8에 추가된 대표적인 함수형 인터페이스를 살펴보았습니다.

 

 

 

✔ References

https://codechacha.com/ko/java8-functional-interface/

https://velog.io/@litien/Modern-Java-Java-8-%ED%95%A8%EC%88%98%ED%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4

https://youtu.be/mu9XfJofm8U

http://m.yes24.com/goods/detail/43755519

https://docs.oracle.com/javase/8/docs/api/

 

 

 

반응형

댓글0