람다식Lambda Expression
은 메서드를 하나의 식으로 표현한 것이에요. 그리고 이로써 자바도 '메서드'가 아닌 '함수'를 구현할 수 있게 되었어요. 객체지향개념에서 '메서드'는 일종의 객체의 행위를 뜻하는 의미로, 기존 자바에선 클래스 내에서 '메서드'를 구현해야 하기 때문에 클래스에 종속적이에요. 이에 비해 람다는 객체에 종속되지 않고 독립적인 행위, 즉 함수function
를 정의하기 때문에 자바는 메서드 뿐만 아니라 함수도 구현할 수 있게 되었어요.
단계적으로 진행해볼게요. 람다식은 자바의 '익명 함수'와 동일해요. 이 익명 함수를 람다식으로 아래와 같이 표현할 수 있어요.
// 예시를 위한 곱셈 함수
int pow2(int a) {
return a * a;
}
// 람다식으로 변환, '->'를 붙이고 메서드 명과 반환 타입을 생략할 수 있어요.
(int a) -> {
return a * a;
}
그리고 반환 값이 있는 함수의 경우, return
을 생략하고 식의 연산 결과가 자동으로 반환 값이 되요. 이땐 ;
를 붙이지 않아요.
(int a) -> a * a
또한 매개변수 타입이 추론할 수 있는 경우에 생략도 가능해요. 대부분의 경우에 생략이 가능해서 웬만하면 다 생략이 가능해요.
(a) -> a * a
그리고 선언된 매개변수가 하나라면 ()
도 생략할 수 있어요. 이 때 매개변수 타입은 존재하면 안되요.
a -> a * a // 이건 되요
//int a -> a * a // 이건 안되요
윗처럼 문장이 하나라면 {}
생략이 가능해요. 이 때는 문장 끝에 ;
가 붙으면 안되요.
// if문처럼 한 문장에 한해서
// {}를 생략하는 것과 똑같이 생각하시면 되요
// a -> a * a; a *= 2;
람다식을 함수형 인터페이스를 통해 타입 클래스를 지정할 수 있어요. 함수형 인터페이스란 추상 메서드가 하나만 존재하는 인터페이스에요. 그래서 함수형 인터페이스를 작성하여 아래와 같이 타입 선언이 가능해요.
@FunctionalInterface
interface MyFunc {
public abstract int func(int a, int b);
}
MyFunc run = (int a, int b) -> { return a * b; };
위 코드에서 선언된 func
추상 메서드는 임의로 붙인 것이라 의미는 없어요. 어쨋든 람다식은 함수형 인터페이스에서 구현한 메서드 func
와 매개변수, 타입 그리고 반환 값이 일치하기 때문에 타입 지정이 가능해요. 그렇게 람다식을 변수로 활용할 수 있게 되요. 이땐 함수형 인터페이스에 작성된 메서드를 호출하면 되요.
System.out.println(run.func(4, 2));
자바에서 제공하는 디폴트 함수형 인터페이스가 존재해요. 자바에서 제공하는 라이브러리 중에 함수형 인터페이스를 매개변수로 받아 처리하는 것이 존재하기 때문이에요. 해당 인터페이스들은 java.lang.function
패키지에 구현되어 있어요.
Collection
클래스의 removeif
가 대표적인 예 중 하나에요.
자바 인터페이스 중 하나인 Predicate
는 boolean
타입을 반환하는 하나의 매개변수를 가진 메서드를 가져요. 이렇게 설계되었기 때문에 아래처럼 간단한 조건문을 작성해서 코드를 작성할 수 있어요.
List<Integer> list = new ArrayList<Integer>();
// 원소가 2의 배수인 경우 제거해요.
list.removeIf((a) -> a % 2 == 0);
자주 쓰이는 인터페이스를 나열해 봤어요.
인터페이스 | 메서드 | 설명 |
---|---|---|
java.lang.Runnable | void run() | 매개변수 X, 반환 X |
Supplier<T> | T get() | 매개변수 X, 반환 O |
Consumer<T> | void accept(T t) | 매개변수 O, 반환 X |
Function<T, R> | R apply(T t) | 매개변수 O, 반환 O |
Predicate<T> | boolean test(T t) | 매개변수 O, boolean 반환 |
그리고 매개변수가 2개인 인터페이스도 있어요. 위 함수 앞에 Bi
라는 접두어가 붙어요.
인터페이스 | 메서드 | 설명 |
---|---|---|
BiConsumer<T, U> | void accept(T t, U u) | 매개변수 O, 반환 X |
BiFunction<T, U, R> | R apply(T t, U u) | 매개변수 O, 반환 O |
BiPredicate<T, U> | boolean test(T t, U u) | 매개변수 O, boolean 반환 |
3개 이상부턴 직접 제작해야 해요자바 라이브러리에선 안쓰는가 봅니다 ㅎㅎ. 구현 방식은 아래와 같겠죠?
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
public abstract R apply(T t, U u, V v);
}
Function
인터페이스의 변형으로 UnaryOperator
와 BinaryOperator
가 있어요. 매개변수의 타입과 반환타입이 모두 일치한다는 차이가 있어요.
인터페이스 | 메서드 |
---|---|
UnaryOperator<T> | T apply(T t) |
BinaryOperator<T> | T apply(T t1, T t2) |
위에 있는 인터페이스는 지네릭 타입을 활용한 인터페이스들이에요. 그래서 기본형 타입을 사용할 수 있는 인터페이스도 존재해요.
인터페이스 | 메서드 | 설명 |
---|---|---|
DoubleToIntFunction | int applyAsInt(double d) | A ToB Function, A 타입을 B 타입으로 변환해줘요 |
ToIntFunction<T> | int applyAsInt(T t) | ToA Function, 모든 타입 매개변수를 A 로 변환해줘요 |
IntFunction<R> | R apply(T t, U u) | A Function, 입력은 A 타입, 출력은 R 타입 |
ObjIntConsumer<T> | void accept(T t, U u) | ObjA Function, 입력은 A 타입, 출력은 없어요 |
함수형 인터페이스는 추상 메서드가 오직 하나만 있어야 하지만, static
이나 default
메서드는 제약이 없어요. 이 점을 활용해 Predicate
는 일부 메서드를 구현해 Predicate
간에 결합이 가능해요. 기본적으로 and
, or
, negate(not)
가 구현되어 있어요.
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 2;
Predicate<Integer> notP = p.negate();
Predicate<Integer> lessThan200AndMultFor2 = p.and(r);
람다식은 극한으로 요약을 할 수 있어서 클래스의 메서드를 활용할 수 있어요. 먼저 문자열을 정수형으로 변환하는 람다식을 작성해봐요.
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
람다식은 거추장스러운 래핑wrapping
작업 없이 직접 메서드를 호출하는 것이 가능해요.
// 이렇게 매개변수를 지정하지 않아도 되요. '.'대신 '::'를 써야해요
Function<String, Integer> f = Integer::parseInt;
BiFunction<Integer, Integer, Boolean> f = Integer::equals;
이를 정리하면 아래와 같아요.
종류 | 람다 | 메서드 참조 |
---|---|---|
클래스 메서드 | (x) -> {class}.method(x) | {class}::method |
멤버 메서드 | (x) -> {class}.method(x) | {class}::method |
인스턴스 메서드 | (x) -> {obj}.method(x) | {obj}::method |