-
[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> 출처
모던 자바 인 액션(라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트 지음)