람다 표현식
람다 표현식은 Java 8부터 추가된 기능이다. 람다 표현식은 이름이 없는 함수면서 메서드 인수로 전달할 수 있는 표현식이다. 익명 클래스를 좀 더 간단하게 줄인 형태라고도 생각할 수 있다.
람다 표현식에 이름은 없지만 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다.
람다 표현식은 파라미터, 화살표, 바디로 이루어진다.
람다 표현식의 기본 문법은 위와 같다.
자바 컴파일러는 람다 표현식이 상요된 콘텍스트를 이용해서 람다 표현식과 관련되 함수형 인터페이스를 추론하므로 함수 디스크립터를 알 수 있고 람다의 시그니처도 추론할 수 있다. 따라서 람다 표현식의 파라미터에도 접근할 수 있으므로 파라미터에서 타입을 생략할 수 있다.
또 파라미터가 한개라면 왼쪽의 소괄호도 생략 가능하다.
(String arg) -> {System.out.println(arg);};
arg -> {System.out.println(arg);};
중괄호 내에 실행문이 하나라면 중괄호도 생략 가능하다.
arg -> {System.out.println(arg);};
arg -> System.out.println(arg)
값을 리턴해야할 때도 있는데 중괄호 안에 리턴문만 있을경우 중괄호와 리턴문 모두 생략 가능하다.
(x , y) -> {return x + y;};
(x, y) -> x + y;
함수형 인터페이스
함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스이다.
추가적으로 인터페이스에 디폴트,스태틱 메서드가 많이 있더라도 서로 다른 파라미터를 가지는 추상 메서드가 하나뿐이라면 함수형 인터페이스라고 할 수 있다.
또 인터페이스가 java.lang.Object의 공개 메소드 중 하나를 대체하는 추상 메소드를 선언하는 경우 추상 메서드 수에 포함되지 않는다.
함수형 인터페이스에는 @FunctionalInterface 어노테이션을 붙여둔다. @FunctionalInterface 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는 데 문제는 없지만, 인터페이스 검증과 유지보수를 위해 붙여주는게 좋다.
함수형 인터페이스의 대표적인 예로 java.util.Comparator<T> 가 있다. java.util.Comparator<T> 는 Arrays.sort(), Collections.sort(), list.sort() 등을 사용할 때 정렬 기준을 정해주는 기능을 할 수 있다.
함수형 인터페이스 사용
함수형 인터페이스의 추상 메서드는 람다 표현식의 시그니처를 묘사한다. 함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터(function descriptor)라고한다.
다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다. 자바8 라이브러리 설계자들을 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.
Predicate
java.util.function.Predicate<T> 인터페이스는 test 라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다. T 형식의 객체를 사용하는 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용한다.
<T> -> boolean
다음과같이 String 객체를 인수로 받는 람다를 정의할 수 있다.
package org.example;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> listOfStrings = new ArrayList<>(List.of("a", "bb", "", "c"));
List<String> list = filter(listOfStrings, nonEmptyStringPredicate);
// list = {"a", "bb", "c"}
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T t : list) {
if(p.test(t)) {
results.add(t);
}
}
return results;
}
}
위 예제에서는 nonEmptyStringPredicate를 따로 정의해서 filter로 넘겨주었지만 filter의 인자에 람다 표현식을 바로 사용할 수 있다.
Counsumer
java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다. T형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.
<T> -> void
예를들어 Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있다.
package org.example;
import java.util.List;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
forEach(List.of(1, 2, 3, 4, 5), (Integer i) -> System.out.printf("%d ", i));
// 출력값 : 1 2 3 4 5
}
public static <T> void forEach(List<T> list, Consumer<T> c) {
// for(T t : list) {
// c.accept(t);
// }
list.forEach(c);
}
}
function
java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.
<T, R> -> R
스트링 리스트에서 글자수 리스트로 바꾸는 map함수를 작성할 수 있다.
package org.example;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
List<Integer> list = map(Arrays.asList("Lambdas", "in", "action"),
(String s) -> s.length());
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T t : list) {
result.add(f.apply(t));
}
return result;
}
}
그 외
java.util.function 패키지는 위에 살펴본 세 가지 함수형 인터페이스 외에 여러 추가 함수형 인터페이스들을 지원한다.
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/function/package-summary.html
자세한 정보는 자바 문서를 통해 확인해볼 수 있다.