Java8에서 새롭게 추가된 기능 중 대표적인 기능은 람다 표현식입니다.
람다 표현식은 함수형 프로그래밍을 위한 필수 요소입니다. 람다 표현식에 대해 알아보고 적용해보는 시간을 가져봅시다‼️
람다 표현식(Lambda Expression)은 메서드로 전달할 수 있는 익명 함수를 단순화한 것입니다. 람다에서의 핵심은 지울 수 있는 것은 모두 지우는 것입니다. 즉, 자바 컴파일러의 추론에 의지하고 코드로 표현하는 건 다 없애버려 코드를 간결하네 만드는 것입니다.
1) 익명성
보통의 메서드와 달리 이름이 없는게 람다의 특징입니다. "이름이 없다"라는 것은 익명이라고 표현합니다.
2) 함수
람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부릅니다. 하지만 메서드처럼 파라미터, 바디, 반환 형식, 가능한 예외를 포함하게 됩니다.
3) 전달
람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있습니다.
4) 간결성
익명 클래스처럼 불필요한 코드(컴파일러의 추론으로 해결가능한 코드)를 구현할 필요가 없습니다.
A) 장점
B) 단점
1) 싱글 파라미터 : 괄호가 필요 없습니다.
2) 중괄호 선택 : 한문장일 경우 중괄호가 필요 없습니다.
3) return 키워드 선택 : 한문장일 경우 생략이 가능합니다. 다만 중괄호를 포함한 경우 무조건 return 키워드를 포함해야합니다.
4) 매개변수 화살표 (→) : 매개변수 화살표를 통해 함수 몸체를 가리킬 수 있습니다.
1️⃣ 싱글 파라미터 예시
(param) -> param+1
// 괄호 생략 가능
param -> param+1
2️⃣ 중괄호 선택 예시
param -> { param+1 }
// 중괄호 생략 가능
param -> param+1
3️⃣ return 키워드 선택 예시
param -> { return param+1; }
// return 키워드 생략 가능
param -> param+1
4️⃣ 매개변수 화살표 (→)
// 함수 몸체를 가리키는 (->) 화살표 사용
param -> param+1
람다 표현식을 이용하려면 오버라이드 할 메서드가 포함된 함수형 인터페이스가 필요합니다.
함수형 인터페이스란?
함수형 인터페이스(Functional Interface)는 함수를 하나만 가지는 인터페이스를 의미합니다. 함수형 인터페이스는@FunctionalInterface
어노테이션을 붙여 표현합니다.
0) 함수형 인터페이스 생성
@FunctionalInterface
public interface UserPredicate{
boolean test(User user);
}
1) 클래스 생성
public class EmailPredicate implements UserPredicate{
private String email;
public EmailPredicate(String email){
this.email = email;
}
@Override
public boolean test(User user){
return email.equals(user.getEmail());
}
public String getEmail() {
return email;
}
}
2) 익명 클래스
UserPredicate userPredicate = new EmailPredicate("dia0312@naver.com") {
@Override
public boolean test(User user) {
return this.getEmail().equals(user.getEmail());
}
};
3) 람다식 사용
UserPredicate userPredicate = (user) -> "dia0312@naver.com".equals(user.getEmail());
람다의 바디(구현부)에는 파라미터를 제외하고도 바디 외부에 있는 변수를 참조할 수 있습니다. 이렇게 람다 시그니처의 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 자유 변수(Free Variable)이라고 부릅니다.
이런 자유 변수를 참조하는 행위를 람다 캡쳐링(Lambda Capturing)이라고 합니다.
⚡람다 캡쳐링의 제약 조건
1️⃣ "지역 변수는 final로 선언되어 있어야 한다" 예시 코드
final String email = "dia0312@naver.com";
int age4 = getAge(users, user -> email.equals(user.getEmail()));
2️⃣ "final로 선언되지 않은 지역변수는 final처럼 동작해야 한다." 예시 코드
**// 값이 한번 초기화되고 재할당 되지 않음 -> final 처럼 동작함.**
String email = "dia0312@naver.com";
int age4 = getAge(users, user -> email.equals(user.getEmail()));
❌ "final로 선언되지 않은 지역변수는 final처럼 동작해야 한다." 예시 실패 코드
String email = "dia0312@naver.com";
**// 값을 재할당하였기 때문에 -> final 처럼 동작하지 않음.**
email = "sonny@naver.com";
int age4 = getAge(users, user -> email.equals(user.getEmail()));
다음과 같이 "Variable used in lambda expression should be final or effectively final"
에러 메시지를 확인할 수도 있습니다.
람다 캡쳐링에서 지역 변수에 대한 제약조건에 대해서 생각해볼 필요가 있습니다.
1) final 변수이거나, final 역할을 해야한다는 제약조건이 생겨나게 되었을까요 ❓
JVM에서 지역변수는 스택이라는 영역에 생성됩니다.
실제 메모리와는 달리 JVM에서 스택 영역은 스레드마다 별도의 스택이 생성됩니다. 따라서 지역 변수는 스레드끼리 공유가 안됩니다.
람다는 별도의 스레드에서 실행이 가능합니다. 그렇다면 람다, 지역변수는 별도의 스레드에서 실행되는데 어떻게 람다에서 지역변수를 참조할 수 있을까요?
그 이유는 람다에서는 지역변수에 직접적으로 접근하는 것이 아닌 지역 변수를 자신의 스레드의 스택에 복사하기 때문입니다.
그렇기 때문에 별도의 스레드의 스택에 있는 지역변수와 동일한 값을 참조할 수 있는 것입니다.
하지만 복사하는 값을 계속 바뀐다면 람다 스레드에서 해당 값에 대한 불신이 생길 것 입니다.
그렇기 때문에 람다 스레드에 신뢰를 부여하기 위해 "final 변수이거나, final 역할을 해야한다는 제약조건"이 생기게 된것입니다.
2) 또 왜 인스턴스 변수에는 왜 이런 제약조건이 없는 걸까요 ❓
JVM에서 인스턴스 변수는 힙 영역에 생성됩니다. 인스턴스 변수는 스레드끼리 공유가 가능합니다.
인스턴스 변수는 힙에 존재하고, 스레드끼리 공유도 가능하기 때문에 별도로 복사할 필요도 없고, 직접 힙에 접근해서 사용하면 되기 때문입니다.