[Java 기초] 람다식 - Lambda Expression
Updated:
람다식의 도입으로 인해 자바는 객체지향 언어이면서 함수형 언어가 되었다.
이번 포스트에서는 자바의 람다식에 대해서 알아보도록 하겠다. 👨🏫
📋 목차
- 람다식 사용법
- 함수형 인터페이스
- 메서드, 생성자 래퍼런스
- Variable Capture
람다식 사용법
메서드 : 특정 클래스에 반드시 속해야 한다. 객체의 행위나 동작을 의미.
함수 : 클래스에 종속되지 않고 독립적인 기능을 수행하는 메서드.
람다식 (Lambda expression)
: 메서드의 이름과 반환값을 생략하여 하나의 식으로 표현한 것.
- 익명 함수라고 불린다.
- 람다식 자체로 메서드의 역할을 수행. $($클래스를 만들고 객체 생성하여 메서드 호출하는 과정 필요없음.)
- 메서드의 매개변수로 전달되어지는 것이 가능.
- 메서드의 결과값으로 반환된는 것이 가능.
- 익명객체이기 때문에 람다식을 다루기 위해서는 참조변수가 필요. $($참조변수의 타입은 함수형 인터페이스로 해야한다.)
람다식 작성방법
반환값은 식으로 대신할 수 있으며 끝에 ; 를 붙이지 않는다.
연산결과가 자동적으로 반환값이 된다.
메서드 | 람다식 |
---|---|
int roll() { return (int) (Math.random() * 6); } |
() -> {return (int) (Math.random() * 6);} — () -> (int) (Math.random() * 6) |
그렇다면 람다식을 다룰 참조변수의 타입은 어떻게 해야할까? 🧐
함수형 인터페이스
함수형 인터페이스 : 단 하나의 추상 메서드만 선언된 인터페이스. 람다식을 다루기 위해 사용.
@FunctionalInterface // 함수형 인터페이스를 올바르게 정의하였는지 검사
interface MyFunction {
public abstract int max(int a, int b); // 추상 메서드
}
// 익명 클래스
MyFunction f = new MyFunction() { // 클래스의 선언, 객체 생성 동시 진행
public int max(int a, int b) {
return a > b ? a : b;
}
}
int value = f.max(3, 5);
함수형 인터페이스 타입의 참조변수 $($MyFunction f)로 람다식을 참조할 수 있다.
단, 함수형 인터페이스의 메서드와 람다식의 매개변수 개수와 반환 타입이 일치해야 한다.
MyFunction f = (a, b) -> a > b ? a : b
int value = f.max(3, 5); // 실제로는 람다식이 호출된다.
즉, 함수형 인터페이스를 선언하고, 함수형 인터페이스 타입의 참조변수를 사용해서 람다식을 다루게 된다.
class Example {
public static void main(String[] args) {
// Object obj = (a, b) -> a > b ? a : b; // 람다식. 익명객체
// MyFunction f = new MyFunction() {
// public int max(int a, int b) { // 오버라이딩 - 접근제어자는 좁게 못바꾼다.
// return a > b ? a : b;
// }
// };
// 람다식(익명 객체)을 다루기 위한 참조변수의 타입은 함수형 인터페이스로 한다.
MyFunction f = (a, b) -> a > b ? a : b;
int value = f.max(3, 5);
System.out.println("value = " + value);
}
}
@FunctionalInterface // 함수형 인터페이스는 단 하나의 추상 메서드만 가져야 함.
interface MyFunction {
// public abstract int max(int a, int b);
int max(int a, int b); // 람다식은 이름을 생략하였기 때문에 호출시 부를 이름이 존재하지 않는다. 때문에 함수형 인터페이스에서 이름을 정의해 준다.
}
메서드를 변수처럼 주고받는것 또한 가능하다.
함수형 인터페이스를 이용해서 메서드의 매개변수로 람다식을 받을수 있다
// void aMethod(MyFunction f) {
// f.myMethod(); // 매개변수를 받아서 MyFuction에 정의된 메서드를 호출
// }
//
// MyFuction f = () -> System.out.println("myMethod()");
// aMethod(f);
aMethod(() -> System.out.println("myMethod()"));
함수형 인터페이스 타입의 반환타입 지정이 가능하다
// MyFuction myMethod() {
// MyFuction f = () -> { };
// return f;
// }
MyFuction myMethod() { return () -> { }; }
예제
@FunctionalInterface
interface MyFunction {
void run(); // public abstract void run();
}
class Example {
static void execute(MyFunction f) { // 매개변수의 타입이 MyFunction인 메서드
f.run();
}
static MyFunction getMyFunction() { // 반환타입이 함수형 인터페이스 MyFunction인 메서드
// MyFunction f = () -> System.out.println("f3.run()");
// return f;
return () -> System.out.println("f3.run()");
}
public static void main(String[] args) {
// 람다식으로 MyFunction의 run()을 구현
MyFunction f1 = () -> System.out.println("f1.run()");
// 함수형 인터페이스를 직접 구현한 경우
MyFunction f2 = new MyFunction() { // 익명클래스로 run()을 구현
public void run() { // 오버라이딩 - 범위는 더 좁게 될 수 없다.
System.out.println("f2.run()");
}
};
MyFunction f3 = getMyFunction();
f1.run();
f2.run();
f3.run();
execute(f1);
execute( () -> System.out.println("run()"));
}
}
java.util.fuction
출처 - 자바의 정석-기초편 유튜브 강의
Supplier<T>
- 공급자
Supplier<Integer> f = () -> (int) (Math.random() * 100) + 1;
Consumer<T>
- 소비자
Consumer<Integer> f = i -> System.out.print(i + ", ");
Function<T, R>
- 함수
Function<Integer, Integer> f = i -> i / 10 * 10;
Predicate<T>
- 조건식
Predicate<Integer> f = i -> i % 2 == 0;
메서드 참조 $($method reference)
하나의 메서드만 호출하는 람다식은 메서드 참조
로 간단히 할 수 있다.
클래스이름::메서드이름
종류 | 람다 | 메서드 참조 |
---|---|---|
static 메서드 참조 | (x) -> ClassName.method(x) |
ClassName::method |
인스턴스메서드 참조 | (obj.x) -> obj.method(x) |
ClassName::method |
static 메서드 참조 예제
출처 - 자바의 정석-기초편 유튜브 강의
다음과 같이 람다식을 메서드 참조로 만들수 있다.
Function<입력, 출력>
에서 입력이 String
타입인 것을 알 수 있기 때문에
f = (String s) -> Integer.parseInt(s)
에서 String s를 생략 할 수 있다.
import java.util.function.Function;
class Example {
public static void main(String[] args) {
// Function<String, Integer> f = (String s) -> Integer.parseInt(s);
// Function<String, Integer> f = 클래스이름::메서드이름;
Function<String, Integer> f = Integer::parseInt; // 메서드 참조
System.out.println(f.apply("100") + 200);
}
}
생성자 참조
클래스이름::new
// Function<Integer, MyClass> s = (i) -> new MyClass(i);
Function<Integer, MyClass> s = MyClass::new;
배열과 메서드 참조
// Integer 길이에 해당하는 int배열을 생성하는 함수식
Function<Integer, int[]> f = x -> new int[x]; // 람다식
Function<Integer, int[]> f = int[x]::new; // 메서드 참조
예제
import java.util.function.Function;
import java.util.function.Supplier;
class Example {
public static void main(String[] args) {
// Supplier는 입력X,
//// Supplier<MyClass> s = () -> new MyClass();
// Supplier<MyClass> s = MyClass::new;
// Function<Integer, MyClass> f = (i) -> new MyClass(i);
Function<Integer, MyClass> f = MyClass::new;
// System.out.println(s.get());
MyClass mc = f.apply(100);
System.out.println(mc.iv);
System.out.println(f.apply(200).iv);
// Function<Integer, int[]> f2 = (i) -> new int[i];
Function<Integer, int[]> f2 = int[]::new;
int[] arr = f2.apply(100);
System.out.println("arr.length=" + arr.length);
}
}
class MyClass {
int iv;
MyClass(int iv) {
this.iv = iv;
}
}
출력
100
200
arr.length=100
변수캡쳐 - Variable Capture
자유 변수(Free Variable)
: 람다식의 매개변수가 아닌 외부에 정의된 변수.
변수 캡처링 (Variable Capture)
: 람다 바디에서 자유변수를 참조하는 행위.
변수 캡쳐링의 제약조건
-
람다식과 같은
Scope
의 지역변수는final
로 선언되야 한다. -
만약 지역변수가 같은
Scope
에서 final로 선언되지 않았다면 final처럼 동작해야한다.(Effective final)
-
람다는 람다를 감싸고 있는 메서드와 같은
Scope
이다.- 람다식의 몸통에 있는 변수와 람다식이 사용되고 있는 클래스내의 변수들은 같은
Scope
이다.
- 람다식의 몸통에 있는 변수와 람다식이 사용되고 있는 클래스내의 변수들은 같은
출처 - https://www.notion.so/758e363f9fb04872a604999f8af6a1ae
쉐도윙 : 가려지는것, 덮어지는것
위의 이미지에서 익명클래스, 로컬클래스는 run 메서드
내에 새로운 Scope
을 만들어서 쉐도윙한다.
반면 람다식
의 경우 run 메서드
와 같은 Scope
에 속하며 쉐도윙하지 않는다.
public class VariableCapturing {
private int a = 12;
public void run() {
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; // 지역변수 d의 값을 재할당.
final Runnable r4 = () -> System.out.println(d); // 에러!!
}
}
변수캡쳐 제약조건 등장 배경
지역변수에는 변수캡쳐 제약조건이 붙는 반면 인스턴스 변수에는 조건이 붙지 않는다. 그 이유는 무엇일까?
- 쓰레드는 각각 고유의 스택을 갖는다.
JVM에서 지역변수는 스택에 저장되고 스택은 쓰레드끼리 공유가 불가능하다.
반면 인스턴스 변수는 힙 영역에 저장되고 쓰레드는 힙 영역을 공유한다.
- 람다는 별도의 쓰레드에서 실행이 가능하다.
람다식이 참조하던 지역변수가 있는 쓰레드가 사라졌는데도 불구하고 람다가 실행 중인 쓰레드는 살아있을 수 있다.
이러한 경우 사라진 지역변수를 참조하기 때문에 에러가 발생할 것이라 생각하지만 그렇지 않다. 또한 람다가 실행되는 쓰레드는 고유의 스택영역을 가질테고 다른 쓰레드의 스택에 존재하는 지역변수를 어떻게 참조할 수 있을까?
- 다른 쓰레드의 스택 영역에 있는 지역변수를 람다가 수행되는 쓰레드의 스택에 복사하여 저장
이러한 이유 때문에 다른 쓰레드의 스택영역에 있는 지역변수를 람다가 수행되는 쓰레드에서 참조하는것이 가능하고 지역변수가 선언되어있는 쓰레드가 사라져도 계속 수행할 수 있는것이다.
이렇게 변수를 복사해서 사용하기 때문에 변수의 값이 변경된다면 복사본의 신뢰성이 깨지게 된다.
따라서 지역 변수는 final 혹은 Effective final로 선언되어야 한다는 제약 조건이 붙게된 것이다.
참조
Java의 정석 $($남궁 성)
whiteship 자바 라이브 스터디
오늘도 끄적끄적 - 람다 캡처링과 final 제약조건
SSON Dev Study Blog
Leave a comment