람다식은 단일 메서드를 가진 클래스의 인스턴스를 익명클래스를 사용하는 것 보다 간결하게 표현할 수 있도록 해줍니다.
보다 일반적인 람다식의 의미는 메서드를 하나의 식으로 표현한 것으로, 함수를 간략하고도 명확한 식으로 표현할 수 있도록 합니다. 메서드를 람다식으로 표현하면, 메서드의 이름과 반환값이 없어지므로 람다식을 익명 함수(anonymous function)이라고도 부릅니다.
람다식이 가져다 주는 장점은 또 있는데, 메서드를 선언하려면 자바에서는 클래스 혹은 인터페이스를 선언해야 하며, 인스턴스를 생성해야 메서드를 호출할 수 있습니다. 이를 간단히 한 것이 바로 람다식입니다.
람다식은 람다식 자체로 메서드의 매개변수가 될 수 있고, 메서드의 결과로 반환될 수 있다는 것이 핵심이며, 이를 first-class function이라고 부릅니다. 이는 함수형 프로그래밍에 필수적입니다.
물론 자바는 함수형 패러다임을 설계단계에서부터 고려한 다른 언어들과는 다르게 순수한 객체지향 언어로 설계되었으며, 함수형 패러다임의 이점을 잘 가져오기 위해 많은 노력이 들어간 결과물이 바로 자바의 람다식입니다.
람다식은 메서드에서 이름과 반환 타입을 제거하고, 매개변수 선언부와 메서드의 몸통 사이에 ->
를 추가합니다.
// 메서드
반환타입 메서드이름(매개변수 목록) {
코드
}
// 람다식
(매개변수 목록) -> {
문장들
}
e.g.)
// 메서드
int max(int a, int b) {
return a > b ? a : b;
}
// 람다식
(int a, int b) -> { return a > b ? a : b; }
참고로 예제의 람다식은 반환 값이 있는 메서드이므로, body가 return이 포함된 한 문장일 경우 식 자체로 대체가 가능합니다. 즉, 식의 결과가 자동으로 반환 값이 됩니다. 이 때에는 문장이 아니라, 식이므로 ;
는 생략합니다.
(int a, int b) -> a > b ? a : b
그리고 람다식에 선언된 매개변수의 타입은 추론 가능한 경우 생략이 가능한데, 대부분의 경우 생략이 가능합니다. 따라서 다음과 같이 고칠 수 있습니다.
주의할 점으로 둘 중 하나만 생략하는 것은 언어에서 지원하지 않습니다. (int a, b)
와 같은 경우는 허용되지 않습니다.
(a, b) -> a > b ? a : b
매개변수가 하나인 경우에는 괄호(()
)를 생략할 수 있습니다. 단 타입이 명시되어 있는 경우는 불가능합니다.
(a) -> a * a
// 생략
a -> a * a
또한, body를 감싸는 괄호({}
)가 하나일 때에는 생략할 수 있고, ;
또한 생략해야합니다. 단, return
문은 괄호를 생략핧 수 없습니다.
(String name, int i) -> System.out.println(name + "=" + i);
자바는 객체지향적인 특성을 아주 강하게 가지고 있어서, 모든 메서드는 클래스 내에 포함되어야 합니다. 그렇다면, 람다식은 어떤 클래스에 포함되어야 할까요?
보통 람다식은 추상 메서드를 구현하는 형태로 표현됩니다. 또, 따로 상태를 가질 필요가 없으므로 인터페이스가 적합합니다.
사실 람다식은 익명 객체이고, 이를 사용하기 위해서는 인터페이스에 정의된 매개변수 목록과 반환 값이 일치하도록 작성해주면 됩니다.
그래서 인터페이스를 통해서 람다식을 다루기로 정했고, 이런 람다식을 다루기 위한 인터페이스를 함수형 인터페이스라고 부릅니다.
이 때, 이 인터페이스가 함수형 인터페이스임을 컴파일러에게 알려주기 위해 @FunctionalInterface
애너테이션을 붙여줄 수 있습니다. 이는 함수형 인터페이스를 정의할 것이라면 @Override
처럼 꼭 붙여주도록 합니다.
이렇게 정의한 함수형 인터페이스 타입을 인자로 받는다면, 해당 메서드는 람다식을 사용해서 매개변수를 받을 수 있다는 의미로 해석될 수 있습니다. 또한, 반환 타입이 함수형 인터페이스 타입이라면 람다식을 반환해줄 수도 있습니다.
@FunctionalInterface
interface MyFunction {
void myMethod();
}
void method(MyFunction f) {
f.myMethod();
}
aMethod(() -> System.out.println("hello"))
MyFunction method(MyFunction f) {
return f;
}
람다식을 메서드의 매개변수, 반환 타입으로 다룰 수 있다는 것은, 함수가 곧 일급 시민과 같다는 것입니다. 결과적으로 자바에서 크게 달라지는 점은 없지만, 읽을 때 코드가 간결하게 읽힐 수 있다는 장점을 얻었습니다.
Java의 람다식은 특정 상황에서 람다 함수 body 외부에 선언된 변수에 접근할 수 있습니다.
Java의 람다식은 다음 유형의 변수에 접근할 수 있습니다.
Java의 람다식은 함수 본문 외부에서 선언된 지역 변수의 값에 접근할 수 있습니다. 단, 이 변수는 사실상 변경되지 않는 것이 확인된 변수만 접근해서 람다식에서 값을 조작할 수 있습니다. 개발자는 컴파일러가 확실하게 final
임을 알 수 있게끔 키워드를 입력해주는 것도 좋습니다.
String myString = "Test";
(chars) -> {
return myString + ":" + new String(chars);
}
위와같이 접근할 수 있습니다.
Java의 람다식은 인스턴스 변수에도 접근할 수 있습니다.
public class EventConsumerImpl {
private String name = "MyConsumer";
public void attach(MyEventProducer eventProducer) {
eventProducer.listen(e -> {
System.out.println(this.name);
});
}
}
이곳의 인스턴스 변수(this.name
)가 바뀌면, 람다식이 참조하는 변수도 함께 바뀝니다. 이것이 인스턴스 캡쳐입니다.
이는 익명 클래스와 다른점인데 익명 클래스는 자체적인 인스턴스 변수를 가질 수 있기 때문에 다릅니다.
Java의 람다식은 클래스 변수에도 접근할 수 있습니다. 이는 접근할 수 있는 모든 영역에서 접근 가능하므로 그다지 신기한 기능은 아닙니다.
앞으로 설명할 메소드, 생성자 레퍼런스를 이용하면, 람다식을 더욱더 간결하게 표현할 수 있습니다.
람다식이 하나의 메서드만 호출하는 경우, 메서드 레퍼런스를 이용할 수 있습니다.
Function<String, Integer> f = s -> Integer.parseInt(s);
Function<String, Integer> f2 = Integer::parseInt;
이것이 가능한 이유는 생략된 부분을 컴파일러가 타입 추론을 통해서 알아 낼 수 있기 때문입니다.
이렇게 클래스 이름을 사용하는 대신 인스턴스의 참조변수를 적어줄 수 있는 경우도 있습니다. 이는 이미 생성된 객체의 메서드를 람다식에서 사용하는 경우, 외부에 존재하는 인스턴스의 참조변수를 통해서 메서드 레퍼런스를 사용할 수 있습니다.
MyClasss obj = new MyClass();
Function<String, Boolean> f = (x) -> obj.equals(x);
Function<String, Boolean> f2 = obj::equals;
즉 메서드 레퍼런스가 가능한 경우는
입니다.
생성자 또한 레퍼런스를 이용할 수 있습니다.
Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s2 = MyClass::new;
BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);
BiFunction<Integer, String, MyClass> bf2 = MyClass::new;
매개변수가 여러개라면 함수형 인터페이스를 새로 정의해서 사용할 수도 있습니다.
배열을 생성하는 방법은 다음과 같습니다.
Function<Integer, int[]> f = x -> new int[x];
Function<Integer, int[]> f2 = int[]::new;