ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JAVA] 람다(Lambda)와 함수형 인터페이스(FunctionalInterface)
    자바 2022. 10. 12. 21:57

    1. 람다(Lambda) 무엇일까

    1-1 람다란?

    람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.

    람다 표현식에는 이름은 없지만, 파라미터, 함수 본문, 반환 타입, 발생할 수 있는 예외 리스트는 가질 수 있다.

    (int a, int b)  ->  a + b

    • 람다 파라미터 : 함수나 메서드의 파라미터에 해당한다.
    • 화살표 : 화살표(->)는 람다의 파라미터와 바디를 구분한다.
    • 람다 바디 : 함수나 메서드의 바디에 해당한다.

     


     

    1-2 람다의 특징

    • 익명 : 보통 메서드와 달리 이름이 없으므로 익명이라 표현한다.
    • 함수 : 람다는 함수처럼 특정 클래스에 종속되지 않는다.
    • 전달 : 람다 표현식을 메서드 파라미터에 전달하거나 변수에 저장할 수 있다.
    • 간결성 : 익명 클래스처럼 자질구레한 코드를 구현할 필요가 없다.

     

     


     

    1-3 람다 표현식 예제들

    (String s) -> s.length();

    String 형식의 파라미터 하나를 가지며 int를 반환한다. 람다 표현식에는 return이 함축되어 있으므로

    return 문을 명시적으로 사용하지 않아도 된다.

     

    (Apple a) -> a.getWeigth() > 150

    Apple 형식의 파리미터 하나를 가지며 boolean값을 반환한다.

     

    (int x, int y) -> {
    	System.out.println("Result:");
        System.out.println(x + y);
    }

    int 형식의 파라미터 두 개를 가지며 리턴값이 없다. (void 리턴)

    이 예제에서 볼 수 있듯이 람다 표현식은 여러 행의 문장을 포함할 수 있다.

     

    () -> 42

    파라미터가 없으며 int 42를 반환한다.

     

     

     

    람다는 표현식 스타일과, 블럭 스타일로 나누어진다.
    (parameters) -> expression //표현식 스타일
    (parameters) -> { statements; } //블럭 스타일

     


     

    1-4 람다 잘못된 예제

    (Integer i) -> return "Alan" + i;

    return은 흐름 제어문이다. (Integer i) -> {return "Alen" + i;} 처럼 되어야 올바른 람다 표현식이다.

     

    (String s) -> {"Iron Man";}

    "Iron Man"은 구문이 아니라 표현식이다. (String s) -> "Iron Man" 처럼 되어야 올바른 람다 표현식이다.

    또는 (String s) -> {return "Iron Man";} 처럼 명시적으로 return문을 사용해야 한다.

     


     

    1-5 어디에, 어떻게 람다를 사용할까

    변수에 저장

    Runnable thread = () -> System.out.println("thread start");

     

    메서드 파라미터에 전달

    button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));

     

    위 예처럼 람다 표현식은 함수형 인터페이스의 추상 메서드에  직접 전달할 수가 있다.

    함수형 인터페이스는 아래에서 자세히 설명되어있다.

     

     


     

    2. 함수형 인터페이스(Functional Interface)

    2-1. 함수형 인터페이스란?

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

    함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스이다.

    대표적인 자바 API의 함수형 인터페이스로는 Comparator, Runnable 등이 있다.

     

        public interface Comparator<T> {
            int compare(T o1, T o2);
        }
        public interface Runnable {
            void run();
        }
        
        public interface ActionListener extends EventListener {
            void actionPerformed(ActionEvent e);
        }
        
        public interface Callable<V> {
            V call() throws Exception;
        }
        
        public interface PrivilegedAction<T> {
            T run();
        }
    참고 디폴트 메서드가 아무리 많아도 추상 메서드가 오직 하나면 함수형 인터페이스다.

     


     

    2-2. 함수형 인터페이스로 무엇을 할 수 있을까?

    람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로

    전체 표현식을 함수형 인터페이스의 인스턴스로 취급(기술적으로 따지면 함수형 인터페이스를 구현한 클래스의 인스턴스)할 수 있다. 물론 함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있다.

     

    다음 예제는 Runnable이 오직 하나의 추상 메서드 run을 정의하는 함수형 인터페이스이므로 올바른 코드다.

    Runnable r1 = () -> System.out.println("Hello World 1"); //람다 사용
    
    Runnable r2 = new Runnable() { //익명클래스 사용
    	public void run() {
        	System.out.println("Hello World 2");
        }
    };

     

     


     

    2-3 @FunctionalInterface는 무엇인가

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

     

    새로운 자바 API를 살퍼보면 함수형 인터페이스에 @FunctionalInterface 애노테이션이 추가되어 있다.

    @FunctionalInterface는 함수형 인터페이스임을 가리키는 애노테이션이다. @FunctionalInterface로

    인터페이스를 선언했지만 실제로 함수형 인터페이스가 아니면 컴파일러 에러를 발생시킨다.

     

    예를 들어 추상 메서드가 한 개 이상이라면 "Multiple nonoverriding abstract methods found in interface Foo" 같은

    에러가 발생할 수 있다.

     


     

    2.4 자바 표준 API 함수형 인터페이스 종류

    함수형 인터페이스는 오직 하나의 추상 메서드를 지정한다. 함수형 인터페이스의 추상 메서드는 람다 표현식

    의 시그니처를 묘사한다.

     

    함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있다. 그렇다면 람다 표현식을

    사용하려면 매번 함수형 인터페이스를 만들어야할까? 그렇지 않다

    자바 8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.

    아래 예제에서 Predicate, Consumer, Function 인터페이스를 설명한다.

     

    2.4.1. Predicate

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

    java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를

    인수로 받아 불리언을 반환한다. T 형식의 객체를 사용하는 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있다.

     

    public <T> List<T> filter(List<T> list, Predicate<T> p) {
        ArrayList<T> results = new ArrayList<>();
    
        for (T t : list) {
            if (p.test(t)) {
                results.add(t);
            }
        }
        return  results;
    }
    
    Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
    List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

     

     

    2.4.2. Consumer

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

    java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는

    accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때

    Consumer 인터페이스를 사용할 수 있다. 예를 들어 Integer 리스트를 인수로 받아서 각 항목에

    어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있다.

     

    다음은 forEach와 람다를 이용해서 리스트의 모든 항목을 출력하는 예제다.

    public <T> void forEach(List<T> list, Consumer<T> c) {
        for (T t : list) {
            c.accept(t);
        }
    }
    
    forEach(
            Arrays.asList(1, 2, 3, 4, 5),
            (Integer i) -> System.out.println(i)
    );

     

    2.4.3. Function

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

    java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를

    반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때

    Function 인터페이스를 활용할 수 있다. (예를 들어 사과의 무게 정보를 추출하거나 문자열을 길이와 매핑)

     

    다음은 String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 변환하는 map 메서드를 정의한다.

    public <T, R> List<R> map(List<T> list, Function<T, R> f) {
        ArrayList<Object> results = new ArrayList<>();
        for (T t : list) {
            results.add(f.apply(t));
        }
        return results;
    }
    
    List<Integer> l = map(
            Arrays.asList("lambda", "in", "action"),
            (String s) -> s.length()
    );

     


     

    2.5 기본 자료형(primitive type)에 특화된 함수형 인터페이스

    자바의 모든 형식은 참조형(reference type - Byte, Integer, Object, List) 아니면 기본형(primitive type - int, double, byte, char)에 해당한다. 하지만 제네릭 파라미터(예를 들어 Consumer<T>의 T)에는 참조형만 사용할 수 있다. 제네릭의 내부 구현 때문에 어쩔 수 없는 일이다.

     

    자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 박싱(boxing)이라고 한다.

    참조형을 기본형으로 변환하는 반대 동작을 언박싱(unboxing)이라고 한다. 또한 프로그래머가 편리하게 코드를 구현할 수 있도록 박싱과 언박싱이 자동으로 이루어지는 오토박싱(autoboxing)이라는 기능을 제공한다. 예를 들어 다음은 유효한 코드이다. (int가 Integer로 박싱됨)

    List<Integer> list = new ArrayList<>();
    for (int i = 300; i < 400; i++) {
    	list.add(i);
    }

     

    하지만 오토박싱은 비용이 든다. 기본 자료형을 래퍼 클래스에 감싸서 객체를 생성하면

    당연하게도 메모리 힙에 저장이되고 메모리를 더 소비하게 된다.

     

    자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다. 예를 들어 아래 예제에서 InePredicate는 1000이라는 값을 박싱하지 않지만, Predicate<Integer>는 1000이라는 값을 Integer 객체로 박싱한다.

     

    @FunctionalInterface
    public interface IntPredicate {
        boolean test(int value);
    }
    
    IntPredicate evenNumbers = (int i) -> i % 2 == 0;
    evenNumbers.test(1000); //박싱 없음
    
    Predicate<Integer> oddNumbers = (int i) -> i % 2 != 0;
    oddNumbers.test(1000); //박싱

     

    일반적으로 LongPredicate, DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction<R>처럼 형식명이 붙는다.

    Function 인터페이스는 ToIntFunction<T>, IntToDoubleFunction 등의 다양한 출력 형식 파라미터를 제공한다.

     

     


     

    2.6 자바 8에 추가된 함수형 인터페이스 종류들

    함수형 인터페이스 함수 디스크립터 기본 자료형 특화
    Predicate<T> T -> boolean IntPredicate, LongPredicate, DoublePredicate
    Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
    Function<T, R> T -> R IntFucnton<R>, IntToDoubleFucnton, IntToLongFucnton
    LongFucnton<R>, LongToDoubleFucnton, LongToIntFucnton
    DoubleFunction<R>, DoubleToIntFunction, DoubleToLongFunction
    ToIntFuction<T>, ToDoubleFuction<T>, ToLongFuction<T>
    Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
    UnaryOperator<T> T -> T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
    BinaryOperator<T> (T, T) -> T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
    BiPredicate<L, R> (T, U) -> boolean  
    BiConsumer<T, U> (T, U) -> void ObjIntConsumer<T>,ObjLongConsumer, ObjDoubleConsumer
    BiFucntion<T, U, R> (T, U) -> R ToIntBiFunction<T, U>, ToLongBiFunction<T, U>, ToDoubleBiFunction<T, U> 

     

     

    출처

    모던 자바 인 액션(라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트 지음)

Designed by Tistory.