자바

[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> 

 

 

출처

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