람다 표현식
람다식(Lambda expression)
- 람다식은 자바8(JDK1.8) 부터 추가된 문법이다.
- 람다식의 도입으로 자바는 객체지향언어인 동시에 함수형 언어가 되었다.
- 람다식은 함수를 간략, 명확한 식으로 표현할 수 있게 만들어준다.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로 , 람다식을 ‘익명 함수(anonymous function)’이라고도 한다.
//람다식 전
반환타입 메서드이름 (매개변수 선언) {구현부
}
//람다식 후
반환타입 메서드이름(매개변수 선언) {구현부
}
람다
- (인자 리스트) → {바디}
인자 리스트
- 인자가 없을 때 : ( )
() ->
- 인자가 한 개일 때 : (one) 또는 one
(one) ->
- 인자가 여러개 일 때 : (one, two)
(one, two) ->
- 인자의 타입은 생략 가능, 컴파일러가 추론(infer)하지만 명시할 수도 있다. (Integer one, Integer two)
//인자타입 넣었을 때
(int a, int b) -> a * b;
//인자타입 생략
(a, b) -> a * b;
바디(Body)
- 화살표 오른쪽에 함수 본문을 정의한다
- 여러 줄인 경우 {}를 사용해서 묶는다.
- 한 줄인 경우에 생략 가능, return 생략이 가능
public class Main{
public static void main(String[] args){
UnaryOperator<Integer> plus10 = (i) -> i + 10; // 인자 (i), 바디 i + 10
UnaryOperator<Integer> multiply2 = (i) -> i * 2;
System.out.println(plus10.andThen(multiply2).apply(2));
}
}
- 인자가 없을 때 - Supplier
public class Main{
public static void main(String[] args){
Supplier<Integer> get10 = () -> {
return 10;
};
}
}
- 바디(Body)가 한 줄이면 생략이 가능
public class Main{
public static void main(String[] args){
Supplier<Integer> get10 = () -> 10;
}
}
- 인자
public class Main{
public static void main(String[] args){
BiFunction<Integer, Integer, Integer> sum = (Integer a, Integer b) -> {
a + b;
};
}
}
- BiFunction<T,U,R> 타입이 다 같을 경우 BinaryOperator로 바꿀 수 있다.
BinaryOperator는 BiFunction과는 다르게 타입을 한번만 써도되는 장점이 있다.
public class Main{
public static void main(String[] args){
BinaryOperator<Integer> sum = (Integer a, Integer b) -> {
a + b ;
};
}
}
- 인자의 타입을 Generic 타입을 가지고 컴파일러가 인자의 타입을 추론을 할 수 있다. 따라서 생략이 가능하다.
public class Main{
public static void main(String[] args){
BinaryOperator<Integer> sum = (a, b) -> {
a + b ;
};
}
}
변수 캡쳐(Variable Capture)
- 로컬 변수 캡쳐
- final이거나 effective final인 경우에만 참조할 수 있다.
- 그렇지 않을 경우 concurrency 문제가 생길 수 있어서 컴파일로 방지한다.
- effective final
- 이것도 역시 자바 8부터 지원하는 기능으로 “사실상” final인 변수.
- final 키워드 사용하지 않은 변수를 익명 클래스 구현체 또는 람다에서 참조할 수 있다.
- 익명 클래스 구현체와 달리 ‘Shadowing’하지 않는다.
- 익명 클래스는 새로 스코프을 만들지만, 람다는 람다를 감싸고 있는 스코프와 같다.
- IntConsumer
i 인자로 받아온 int를 출력하는 간단한 함수
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber);
};
안에서 어떤 변수(Local 에 있는 변수 baseNumber)를 참조한다고 가정하면
여기서 참조하고 있는 local variable( baseNumer)이 캡쳐가 될까?
- 자바 8 이전에는 final in baseNumber를 해야지 익명클래스, 내부 클래스에서 참조가 가능했다 ?
답 : 가능하다. 하지만 조건이… effective final인 경우에만 .. 가능
public class Main{
public static void main(String[] args){
Main T = new Main();
T.run();
}
public void run(){
final int baseNumber = 10; // 로컬 변수
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber); //baseNumber는 Local variable(baseNumber)를 참조 가능 !
};
printInt.accept(10);
}
}
- 전체 코드
public class Main{
public static void main(String[] args){
Main main = new Main();
Main.run();
}
//인스턴스 메소드
private void run(){
final int baseNumber = 10; // 자바 8 전에는 fianl을 넣어줘야한다.
//익명 클래스 - Local variable 참조
Consumer<Integer> integerConsumer = new Consumer<Integer>() {
@Override
public void accept(Integer integer){
System.out.println(baseNumber);
}
};
//로컬 클래스 - Local variable 참조
class LocalClass{
void printBaseNumber(){
System.out.println(baseNumber);
}
}
//람다 - Local variable 참조
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber);
};
printInt.accept(10);
}
}
익명 클래스 , 로컬클래스, 람다의 공통점과 차이점
공통점 : 메소드안에 있는 로컬변수를 참조할 수 있다. (단, final int 변수 → final 이 들어가야한다. )
하지만…
자바 8부터 final을 생략할 수 있는 경우가 생겼다.
- baseNumber가
사실상 final인 경우
, 즉 변경이 불가한 변수 이다.
여기서 사실상 final이라는 경우
는 final 이라는 키워드는 없지만 어떠한 곳에도 그 변수를 변경하지 않는 경우라고 한다. 이런 경우를 effective - final이라고 한다.
차이점 : shadowing되는것 로컬클래스, 익명클래스 → 람다는 안된다. 왜 람다는 안될까 ?
shadowing 이란 ?
특정 스코프(예: 내부 클래스 또는 메서드 정의)의 형식 선언(예: 멤버 변수 또는 매개 변수 이름)이 바깥쪽 범위의 다른 선언과 동일한 이름을 갖는 경우 선언은 선언을 숨깁니다. 이름만으로 숨겨진 선언을 참조할 수 없습니다. Shadwing이란 ?
다음 ShadowTest의 예시를 통해서 설명을 구체화 시켜보자.
- 익명클래스
public class ShadowingTest{
public static void main(String[] args){
ShadowingTest T = new ShadowingTest();
ShadowingTest.run();
}
private void run(){
int baseNumber = 10;
//익명 클래스
Consumber<Integer> integerConsumer = new Consumer<Integer>() {
@Override
public void accept(Integer baseNumber){ //더 이상 int baseNumber = 10을 참고하지 않는다.
//이렇게 변수가 바뀐걸 shadowing이라고 함
System.out.println(baseNumber);
}
};
printInt.accept(10);
}
}
- 로컬 클래스 에서 baseNumber를 재정의 하면
public class ShadowingTest{
public static void main(String[] args){
ShadowingTest T = new ShadowingTest();
ShadowingTest.run();
}
public void run(){
int baseNumber = 10;
//로컬 클래스
class LocalClass(){
void printBaseNumber(){
int baseNumber = 11; // baseNumber는 이제 11이다. run() 로컬에 있는 변수는 가려진다.
System.out.println(baseNumber); // output : 11
}
}
}
}
결론 : 로컬클래스안에 BaseNumber가 run() baseNumber를 가린것이다. 이것이 shadowing
- 람다는 쉐도잉이 안된다.
private void run(){
int baseNumber = 10;
IntConsumer printInt = (baseNumber) -> {
System.out.println(i + baseNumber);
};
printInt.accept(10);
}
private void run(){
int baseNumber = 10;
IntConsumer printInt = (baseNumber) -> {
System.out.println(i + baseNumber);
};
printInt.accept(10);
baseNumber++;
}
Variable used in lambda expression should be final or effectively final
라는 에러가 뜬다.
이유는 람다에 baseNumber랑 run()에 baseNumber랑 같은 스코프에 있기 때문이다. 즉 , int baseNumber 10 과 람다 안에서 같은 스코프를 쓴다.
Effective-final이 아닌거라고 알 수 있는방법 : 나중에 값이 바뀌었다는 것을 보면 알 수 있다.
그리고 값을 변경되었을 때 람다에서 baseNumber를 참조할 수 없다. 왜냐하면 값이 변경되었기 때문에 effective-final이 아니기 때문이다 .
baseNumber를 변경할 경우 에러가 뜬다. 이유는 baseNumber가 Effectively-final만 가능하기 때문이다.