본문 바로가기
Java

Java - 람다란 무엇인가?(Lambda)

by 오늘부터개발시작 2022. 8. 21.

 

 

 

 

 

 

 

Lambda의 출현

Java에서는 기존의 @FunctionalInterface를 사용하기 위해서는 새로운 객체를 만들어서 동작 파라미터화를 했었다. 객체를 만들어서 넘기기 때문에 코드 이해가 쉽지 않고 불필요한 코드들이 많이 들어갔다. 

 

Java8이 등장하면서 람다표현식을 지원하기 시작했고 동작 파라미터화를 할 때 익명 클래스를 만들어서 객체를 넘기지 않고 람다식을 넘길 수 있게 되면서 코드가 확 줄어들었고 이해하기도 쉽게 되었다. 아래 예시가 람다식을 사용했을 때와 아닐 때의 비교 코드이다.

 

 

 

Lambda의 특징

Lambda식은 Parameter List, Body, Return Type, Exception List로 구성되어 있다. 메서드를 전달 할 수 있도록 익명 함수를 단순화시킨 구조이다. 람다의 특징은 다음과 같다.

 

1. 익명

람다는 이름이 없고 () -> {} 형식으로 구현된다. 

 

2. 함수

람다는 메서드처럼 클래스에 속해있는게 아니고 아무곳에서나 사용할 수 있다. 그래서 메서드 보다는 함수라고 여겨진다.

 

3. 전달

람다는 1급 시민으로 파라미터로 전달되거나 변수로도 저장이 가능하다.

 

4. 간결성

위의 코드 예시를 보는 것처럼 이해하기 쉽고 간결하게 표현이 가능하다.

 

Lambda와 Functional Interface

Javascript 같은 언어에서는 다음과 같이 () => {} 라는 익명함수를 변수에 넣을 수 있다.

const a = () => "hi";

 

그런데 Java에서의 익명 함수는 제약이 있다. Java의 익명 함수는 Functional Interface하고만 치환이 가능하다. 예를 들면 Javascript 같이 아래 처럼 쓰는 것은 불가능하다. IDE에서 Target Type이 반드시 interface이어야 한다고 에러가 뜬다.

 

아래에서 사용 된 Supplier는 @Functional Interface인데 익명 함수에 에러 없이 적용된다.

이처럼 람다식과 Functional Interface는 바늘과 실 같은 존재라고 볼 수 있다. 

 

Lambda의 작동 방식

아래 코드처럼 동작 파라미터화할 때 Lambda와 new를 사용한 객체의 구조 자체가 다른데 어떻게 작동하는지에 대해 매우 궁금증이 생긴다. 위 코드를 기준으로 컴파일러가 람다식을 만나면 형식 검사를 통해 처리하게 되는데 순서는 다음과 같다.

 

형식 검사 순서

0. Lambda를 만났다.

1. filterFish 메서드의 파라미터의 형식을 확인한다.

2. filterFish 메서드는 Predicate<Fish> 형식을 기대한다.

3. Predicate<Fish>는 test라는 메서드를 정의하는 Functional Interface이다.

4. test 메서드는 Fish를 입력 받아서 boolean을 반환한다.

5. filterFish 메서드로 전달된 Lambda 표현식이 위의 조건과 일치 한다 -> Lambda를 Accept

 

** 예외

Functional Interface의 추상 메소드가 void 형식일 경우일 때 lambda의 바디에 일반 표현식이 있으면 Lambda를 Accept 한다. 아래의 예시가 이와 같은 예외 케이스이다. list.add는 boolean을 return하지만 유효하다.

Consumer<String> b = (String s) -> list.add(s);

 

형식 추론

Java에서는 기존에는 Type을 반드시 정의해줘야만 했다. 그런데 Lambda식에서는 형식 추론 기능을 제공해준다. 이는 Functional Interface에 Type이 정해져 있기 때문에 가능하다. 예를 들어 Predicate<Fish>라고 하면 Predicate의 test 메소드에는 반드시 Fish Type의 파라미터가 넘어갈 것이기 때문에 아래와 같이 Lambda식에 Type을 지정하지 않아도 된다.

Predicate<Fish> p = f -> f.getWeight() > 10;

지역변수 사용

String s = "hi";

Runnable r = () -> System.out.println(s);

위의 예시 코드처럼 Lambda 내부에서는 외부의 변수도 사용할 수 있다. 인스턴스 변수와 지역변수를 모두 사용할 수 있는데 지역변수를 사용할 때는 final로 선언된 변수와 동일하게 값이 더이상 바뀌지 않을 변수를 사용할 수 있다.

 

그 이유는 인스턴스 변수는 힙에 저장 되어서 값이 변해도 참조가 가능하지만 지역변수는 스택에 위치하기 때문에 만약 Lambda가 스레드에서 실행이 된다면 변수를 할당한 스레드가 사라지면 변수에 접근할 수 없기 때문에 지역 변수의 복사본을 만들어서 사용한다. 그렇기 때문

에 값이 바뀌면 적용이 되지 않아서 final인 변수가 아니면 컴파일 에러가 나게 막혀있다. 반대로 Lambda 내부에서 외부 변수의 값도 변경할 수 없게 되어있다.

 

추가적으로 병렬화를 방해하는 외부 변수를 변화시키는 명령형 프로그래밍 패턴을 방지할 수 있다.