람다식(Lambda Expression)이란?
함수(메소드)를 간단한 '식(expression)'으로 표현하는 방법
메소드를 람다식으로 표현하면 이름과 반환값이 없어지므로, 람다식을 익명함수(anonymous function)이라고도 한다.
람다식 사용법
1. 람다식은 '익명 함수'이므로 이름과 반환타입을 제거하고 매개변수 선언부와 블록 사이에 '->'를 추가한다.
2. 반환값이 있는 경우엔 식이나 값만 적고 return문을 생략할 수 있다. 이때는 '문장(statement)'가 아닌 '식'이므로 세미콜론을 생략한다.
3. 매개변수의 타입이 추론 가능한 경우엔 생략할 수 있다. 대부분의 경우엔 생략이 가능하다.
4. 매개변수가 하나인 경우엔 괄호를 생략할 수 있다. 단, 매개변수의 타입이 있다면 생략할 수 없다.
5. 블록 안의 문장이 하나뿐인 경우엔 괄호를 생략할 수 있다. 2번과 동일하게 세미콜론을 생략한다. 단, 괄호 안의 문장이 return문인 경우엔 괄호를 생략할 수 없다.
다음과 같이 메소드를 람다식으로 바꿔보자. 이때, 람다식은 선언과 생성을 동시에 하는 익명 객체이기 때문에 작성한 람다식을 사용하기 위해선 별도의 참조변수가 필요하다. 아래의 함수형 인터페이스에서 마저 살펴보자.
public class LambdaEx {
int max(int a, int b) {
return a > b ? a : b;
}
// (a, b) -> a > b ? a : b
void printVar(String name, int i) {
System.out.println(name+"="+i);
}
// name, i -> System.out.println(name+"="+i)
int square(int x) {
return x * x;
}
// x -> x * x
int roll() {
return (int)(Math.random()*6);
}
// () -> (int)(Math.random()*6)
}
함수형 인터페이스
앞서 람다식이 익명 객체라고 언급했다. 지금까지는 람다식이 메소드와 동일한 것처럼 설명했지만, 사실 람다식은 아래의 그림과 같이 익명 클래스의 객체와 동등하다고 할 수 있다.
그렇다면 람다식으로 정의된 익명 객체의 메소드를 어떻게 호출할 수 있을까? 당연하게도 참조변수가 있어야 객체의 메소드를 호출할 수 있으므로 이 익명 객체의 주소를 f라는 참조변수에 저장해보자.
타입 f = (int a, int b) -> a > b ? a : b;
여기서 참조변수 f의 타입은 다음의 조건을 충족해야 한다.
- 참조형이므로 클래스 또는 인터페이스의 타입을 취해야 한다.
- 람다식과 동등한 메소드가 정의되어 있는 것이어야 한다.
예를 들어 max()라는 메소드가 정의된 MyFunction인터페이스가 정의되어 있다고 가정하자.
interface MyFunction {
public abstract int max(int a, int b);
}
이때, 이 인터페이스를 구현한 익명 클래스의 객체는 다음과 같이 생성할 수 있다.
MyFunction f = new MyFunction() {
@Override
public int max(int a, int b) {
return a > b ? a : b;
}
};
여기서 MyFunction인터페이스에 정의된 max()는 위에서 작성한 람다식 '(int a, int b) -> a > b ? a : b'과 메소드의 선언부가 일치한다. 그래서 바로 위 코드의 익명 객체를 람다식으로 아래와 같이 대체할 수 있다.
MyFunction f = (int a, int b) -> a > b ? a : b; // 익명 객체를 람다식으로 대체
이처럼 MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는 다음과 같다.
- 람다식도 실제로는 익명 객체이다.
- MyFunction인터페이스를 구현한 익명 객체의 메소드 max()와 람다식의 매개변수의 타입과 개수, 그리고 반환값이 일치하기 때문이다.
지금까지 살펴본 것처럼 하나의 메소드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다. 그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(functional interface)'라고 부르기로 했다. 참고로 함수형 인터페이스를 사용할 땐 '@FunctionalInterface'라는 어노테이션을 붙일 수 있다.
@FunctionalInterface
interface MyFunction { // 함수형 인터페이스 MyFunction을 정의
public abstract int max(int a, int b);
}
단, 함수형 인터페이스에는 오직 하나의 추상 메소드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메소드가 1:1로 연결될 수 있기 때문이다. 반면에 static메소드와 default메소드의 개수에는 제약이 없다.
결론적으로 함수형 인터페이스를 한 마디로 정의하자면 '람다식을 다루기 위해 단 하나의 추상 메소드만 선언된 인터페이스'라고 할 수 있다.
람다 캡쳐링
람다식의 선언부에 정의된 매개변수가 아니라 외부에서 정의된 변수를 자유 변수(Free Variable)라고 한다. 람다식의 내부에서 이러한 자유 변수를 참조하는 행위를 람다 캡쳐링(Lambda Capturing)이라고 한다.
// https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/
public class LambdaCapturing {
private int a = 12;
public void test() {
int b = 123;
final Runnable r = () -> System.out.println(a);
final Runnable r2 = () -> System.out.println(b);
}
}
이렇듯 람다 캡쳐링을 할 땐 두 가지 제약조건이 있다.
- 참조하는 변수는 final로 선언되어있어야 한다.
- 그렇지 않다면 final처럼 동작해야 한다. 즉, 값의 재할당이 일어나지 않아야 한다(effective final).
// https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/
public class LambdaCapturing {
private int a = 12;
public void test() {
final int b = 123;
int c = 123;
int d = 123;
final Runnable r = () -> {
// 인스턴스 변수 a는 final로 선언돼있을 필요도, final처럼 재할당하면 안된다는 제약조건도 적용되지 않는다.
a = 123;
System.out.println(a);
};
// 지역변수 b는 final로 선언돼있기 때문에 OK
final Runnable r2 = () -> System.out.println(b);
// 지역변수 c는 final로 선언돼있지 않지만 final을 선언한 것과 같이 변수에 값을 재할당하지 않았으므로 OK
final Runnable r3 = () -> System.out.println(c);
// 지역변수 d는 final로 선언돼있지도 않고, 값의 재할당이 일어났으므로 final처럼 동작하지 않기 때문에 X
d = 12;
final Runnable r4 = () -> System.out.println(d);
}
}
JVM에서 지역 변수는 스택 영역에 생성되며, 쓰레드는 각자의 스택 영역을 가진다. 따라서 지역 변수는 쓰레드끼리 공유가 되지 않는다. 반면, 인스턴스 변수는 힙 영역에 생성된다. 쓰레드는 힙 영역을 공유하기 때문에 인스턴스 변수를 공유할 수 있다.
람다식은 별도의 쓰레드에서 실행이 가능하다. 이는 곧 별도의 스택 영역을 가진다는 의미이기 때문에 람다식 내에서 다른 쓰레드의 지역 변수를 참조하는 것은 다소 이상해보인다.
예컨대 람다식이 이미 실행이 종료된 쓰레드의 지역 변수를 참조한다면 분명 에러가 날 것이다. 하지만, 사실은 그렇지 않다. 이는 람다식에서 지역 변수에 직접적으로 접근하는 게 아니라 해당 변수를 자신이 속한 쓰레드의 스택 영역에 복사(Variable Capture)하기 때문에 가능한 일이다.
이렇게 복사된 변수의 값은 공유가 되어 있지 않기 때문에 변경되지 않아야 부수효과를 예방할 수 있다. 이러한 이유에서 람다식 내부에서 사용되는 지역 변수가 final하거나 effective final(값의 재할당이 불가능)해야 한다는 조건이 생긴 것이다.
참고
자바의 정석(남궁성 저)
https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/
'프로그래밍 언어 > Java + Kotlin' 카테고리의 다른 글
[Java] 스터디 17주차: 자동차 경주 게임 만들기 (0) | 2021.10.04 |
---|---|
[Java] 스터디 16주차: 문자열 계산기 만들어보기 (0) | 2021.09.28 |
[Java] 스터디 14주차: 제네릭 (0) | 2021.09.07 |
[Java] 스터디 13주차: I/O (0) | 2021.09.02 |
[Java] 스터디 12주차: 애노테이션 (0) | 2021.08.19 |