람다식 (Lambda Expression)
함수형 인터페이스
추상 메소드가 하나만 존재하는 인터페이스이다.
default 메소드나 static 메소드가 있어도 상관 없고 다만, 추상 메소드는 꼭 하나만 존재해야 한다.
함수형 인터페이스에는 @FunctionalInterface
어노테이션을 달아준다.
1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
interface Functional {
void print(); // public abstract 키워드는 자동 생략됨
default void printDefault() {
System.out.println("default");
}
static void printStatic() {
System.out.println("static");
}
}
람다식 (Lambda Expression)
아래와 같은 함수형 인터페이스가 있다고 하자.
1
2
3
4
@FunctionalInterface
interface Calculable {
int calculate(int a, int b);
}
이 인터페이스의 add 메소드를 사용하기 위해서는 객체를 생성해야 한다.
하지만, 우리는 인터페이스로 객체를 만들 수 없다는 사실을 이미 알고 있다.
그래서 클래스를 정의해야 하지만 한 번만 사용할 예정이기 때문에, 익명 클래스로 정의해서 바로 객체로 만들어보겠다.
1
2
3
4
5
6
Calculable c1 = new Calculable() {
@Override
public int calculate(int a, int b) {
return a + b;
}
};
위 코드를 한 번 보자.
Calculable 함수형 인터페이스를 구현한 익명 클래스이기 때문에 calculate 메소드의 접근 제어자, 반환형, 메소드 이름, 파라미터의 타입 등 너무 당연한 것들이 눈에 거슬린다.
그래서 친절한 자바에서는 람다 표현식이라는 대안을 줬고, 우리는 아래와 같이 표현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
Calculable c2 = (a, b) -> a + b;
a -> a * 2; // 파라미터가 1개인 경우는 괄호도 생략 가능
() -> 1; // 파라미터가 없는 경우는 괄호 필수 표기
// 내부가 두 줄 이상으로 구성해야 하는 경우는 중괄호 필수 표기
(a, b) -> {
System.out.println("input values: " + a + " " + b);
return a + b;
};
1. 일급 객체 (First-Class Object)
일급 객체란 다른 객체들에 적용 가능한 연산을 모두 지원하는 객체를 말한다.
즉, 일반적인 객체를 사용하는 것과 동일하게 사용할 수 있는 객체를 말한다.
일급 객체의 충족 요건
- 변수에 할당할 수 있다.
- 함수의 파라미터로 전달할 수 있다.
- 함수의 반환 값으로 사용할 수 있다.
자바스크립트나 파이썬에서는 함수 자체가 일급 객체이기 때문에 함수를 객체처럼 변수에 할당할 수 있다.
자바에서 일반적인 함수는 일급 객체가 아니지만, 람다는 일급 객체라고 할 수 있다.
2. 람다식의 활용
람다를 변수에 할당하거나, 람다를 파라미터로 받는 메소드를 정의하는 등 일급 객체인 람다를 활용하기 위해서는 위에서 확인한 것처럼 함수형 인터페이스가 필요하다.
하지만, 매번 우리가 람다를 사용할 때마다 함수형 인터페이스를 정의하기에는 너무 불편하다.
이를 위해 자바에서 제공하는 여러 함수형 인터페이스가 있는데, 그중 몇 개만 살펴 보자.
아래에서 소개할 함수형 인터페이스에는 추상 메소드 외에도 몇몇 static 메소드나 default 메소드가 존재하지만, 가장 중요한 추상 메소드만 살펴보겠다.
2-1. Predicate
파라미터를 입력받아, 해당 파라미터의 조건 만족 여부를 반환하는 추상 메소드를 가지고 있는 인터페이스이다.
보통 필터링 작업을 할 때 주로 사용된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
⁝
}
// 변수에 할당
Predicate<String> isEnd = s -> s.equals("end");
public int countLargerThan(List<Integer> numbers, int standard) {
// 필터링과 같은 작업에 사용
int count = (int) numbers.stream().filter(i -> i > standard).count();
return count;
}
아래와 같이 스트림 인터페이스에 정의된 filter 메소드를 보면, 파라미터로 Predicate를 받는 것을 볼 수 있다.
1
Stream<T> filter(Predicate<? super T> predicate);
2-2. Consumer
파라미터를 입력받아, 작업을 수행 후 아무것도 반환하지 않는 추상 메소드를 가지고 있는 인터페이스이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
⁝
}
// 변수에 할당
Consumer<String> print = s -> System.out.println(s);
// 메소드의 파라미터로 사용
public void exec(List<Integer> numbers, Consumer<Integer> consumer) {
numbers.forEach(consumer);
}
아래와 같이 Iterable 인터페이스에 정의된 forEach 메소드를 보면, 파라미터로 Consumer를 받는 것을 볼 수 있다.
1
2
3
4
5
6
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
2-3. Supplier
어떠한 파라미터도 입력받지 않고, 작업을 수행 후 결과를 반환하는 추상 메소드를 가지고 있는 인터페이스이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FunctionalInterface
public interface Supplier<T> {
T get();
⁝
}
// 변수에 할당
Supplier<Double> randomSupplier = () -> Math.random() * 5;
// 메소드의 파라미터로 사용
public void print(Supplier<String> supplier) {
for (int i = 0; i < 5; i++) {
String word = supplier.get();
System.out.println(word);
}
}
아래와 같이 Optional 클래스에 정의된 orElseGet 메소드를 보면, 파라미터로 Supplier를 받는 것을 볼 수 있다.
1
2
3
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}
2-4. Funtion
파라미터를 입력받아, 작업을 수행 후 결과를 반환하는 추상 메소드를 가지고 있는 인터페이스이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
⁝
}
// 변수에 할당
Function<String, Integer> lengthFunction = s -> s.length();
// 메소드의 파라미터로 사용
public static int getTotalLength(List<String> words, Function<String, Integer> function) {
int totalLength = words.stream().mapToInt(word -> function.apply(word)).sum();
return totalLength;
}
아래와 같이 Stream 인터페이스에 정의된 map 메소드를 보면, 파라미터로 Function을 받는 것을 볼 수 있다.
1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
3. 메소드 참조 (Method Reference)
메소드 레퍼런스란 람다식에서 입력되는 값을 변경 없이 특정 메소드의 매개변수로 넘겨줄 때, 해당 메소드를 참조해서 불필요한 부분을 생략하는 것을 말한다.
1
2
3
4
(a, b) -> Math.max(a, b);
// 위 람다식을 다음과 같이 표현 가능
Math::max;
위 코드에서 첫 번째 줄의 람다식에서 가져온 a, b를 그대로 Math.max 메소드의 매개변수로 넘겨주는데, 이럴 때 이중 콜론 연산자 (double colon operator) ::
를 사용해서 메소드 레퍼런스 방식으로 간결하게 표현 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
// static 메소드 참조
// s -> Integer.parseInt(s);
Integer::parseInt;
// 인스턴스 메소드 참조
List<Number> numbers = new ArrayList<>();
// n -> numbers.add(n);
numbers::add;
// 생성자 참조
// (x, y) -> new Point(x, y);
Point::new;
위 코드와 같이 다양하게 메소드 레퍼런스를 사용할 수 있다.