Contents
The Java8(람다 표현식)
   Jul 15, 2022     8 min read

람다 표현식

람다식(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만 가능하기 때문이다.

참고 : 백기선 - The Java8