본문 바로가기
BackEnd/Java

람다 표현식

by 규난 2023. 8. 16.
728x90

람다 표현식

함수형 프로그래밍을 구성하기 위한 함수식이며, 자바의 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있습니다.

람다 표현식이 중요한 이유는 익명 클래스처럼 코드를 전달하는 과정에서 자질구레한 코드가 많이 생기지 않고 간결한 방식으로 코드를 전달할 수 있어 결과적으로 코드가 간결하고 유연해지기 때문입니다.

 

//    (1)      (2)          (3)
(Apple apple) -> a.getWeight() > 80;
  1. 람다 파라미터 리스트
  2. 파라미터 리스트와 바디를 구분
  3. 람다의 반환값에 해당하는 표현식. 바디가 한 줄일 경우 중괄호와 return 생략 가능.

 

람다의 특징

  • 익명
    • 보통의 메서드와 달리 이름이 없으므로 익명이라 표현되며 메서드를 미리 만들어서 재사용하는 것이 아니라 관리해야 할 코드가 줄어듭니다.
  • 함수
    • 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부릅니다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 예외 리스트를 포함할 수 있습니다.
  • 전달
    • 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있습니다.
  • 간결성
    • 익명 클래스처럼 많은 코드를 구현할 필요가 없습니다.

익명 클래스 vs 람다 코드 비교

// 익명 클래스를 메서드의 파라미터로 전달하는 방식
List<Apple> heavyApples = Apple.filterApples(apples, new Predicate<Apple>() {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
});

// 람다를 메서드의 파라미터로 전달하는 방식
List<Apple> heavyApples = Apple.filterApples(apples, (Apple a) -> a.getWeight() > 80);

위 코드를 보시면 딱 봐도 익명 클래스를 메서드의 파라미터로 넘기는 방식보다 람다를 사용한 방식이 코드가 간결한 것을 보실 수 있습니다.

 

함수형 인터페이스

위에서 언급한 람다 표현식은 함수형 인터페이스라는 문맥에서 사용할 수 있습니다.

함수형 인터페이스란 정확히 하나의 추상 메서드가 선언된 인터페이스를 말합니다. 즉 인터페이스 안에 디폴트 메서드의 개수와는 상관없이 오로지 추상 메서드가 1개인 인터페이스를 함수형 인터페이스라 합니다.

람다 표현식으로 함수형 인터페이스의 구현체를 전달할 수 있으므로 람다 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있습니다.

 

@FunctionallInterface

함수형 인터페이스임을 가르키는 애노테이션입니다. 이 애노테이션은 함수형 인터페이스 형식에 맞지 않으면 컴파일러가 오류를 발생시킵니다. 밑의 사진은 추상 메서드가 2개인 인터페이스에 @FunctionallInterface 애노테이션을 사용한 경우 컴파일 오류가 발생하는 코드를 보여주는 사진입니다.

타입 검사와 타입 추론

자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해 대상 형식을 찾고 대상 형식을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론하여 함수 디스크립터와 람다의 시그니처를 알 수 있습니다. 이 말은 결과적으로 컴파일러가 람다 표현식의 파라미터 리스트에 접근할 수 있다는 뜻이며 컴파일러는 타입 추론이 가능하기 때문에 파라미터의 타입을 생략하여 람다 표현식을 더 간결할 수 있다는 뜻입니다.

 

List<Apple> heavyApples = Apple.filterApples(apples, (Apple a) -> a.getWeight() > 80);

컴파일러는 Apple.filterApples의 메서드의 두 번째 파라미터로 대상 형식을 찾습니다. 

List<Apple> heavyApples = Apple.filterApples(apples, Predicate<Apple> p);

컴파일러는 람다 표현식으로 전달된 (Apple a) -> a.getWeight() > 80의 대상 형식은 Predicate<Apple>로 기대합니다.

이때 Predicate 인터페이스의 추상 메서드는 boolean test(T t)이고 함수 디스크립터는 T -> boolean이지만 기대한 대상 형식에 따라 boolean test(Apple apple)이 되며 함수 디스크립터는 Apple -> boolean으로 묘사됩니다.

마지막으로 함수 디스크립터와 람다의 시그니처인 Apple -> boolean을 비교해서 일치하는지 확인 후 타입 검사를 마치게 됩니다.

 

이러한 과정이 있기 때문에 파라미터의 타입을 생략해도 람다 시그니처를 타입 추론을 통해 예외 없이 간결한 람다 표현식을 사용할 수 있게 됩니다. 하지만 이 부분은 오히려 가독성을 떨어트릴 수 있기 때문에 어떤 코드가 더 가독성이 있는지 판단하고 사용하는 게 좋다고 생각합니다.

// (Apple a)에서 (a)로 타입 생략
List<Apple> heavyApples = Apple.filterApples(apples, (a) -> a.getWeight() > 80);

 

함수 디스크립터와 람다 시그니처

함수 디스크립터

함수형 인터페이스 추상 메서드의 리턴 타입과 파라미터 타입을 나타내는 시그니처입니다.

  • 예제 
    • Predicate의 boolean test(T t) 메서드의 함수 디스크립터 => T -> boolean
    • Function의 R apply(T t) 메서드의 함수 디스크립터 => T -> R
    • Apple.filterApples(apples, (a) -> a.getWeight() > 80); 이 코드에 대상 형식은 Predicate<Apple> 이며 함수 디스크립터 => Apple -> boolean

 

람다 시그니처

람다 표현식의 리턴 타입과 파라미티 타입을 나타내는 시그니처 입니다.

  • 예제
    • Apple.filterApples(apples, (a) -> a.getWeight() > 80); 이 코드에 람다 표현식의 람다 시그니처 => Apple -> boolean

 

람다 표현식에서 지역 변수 사용

람다 표현식에서는 람다 캡처링을 통해 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수(자유 변수)를 활용할 수 있습니다.

람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처가 가능하지만 몇 가지 제약 사항이 있습니다.

  1. 변수를 final로 선언해야 합니다.
  2. final로 선언하지 않은 변수는 final 변수로 선언된 변수와 똑같이 사용되어야 합니다. 즉 한 번 초기화가 이루어진 변수에 대해 값을 변경하면 안 됩니다. (밑 사진 지역 변수 사용 2 참고)

지역 변수 사용 1. 변수를 fianl로 선언한 경우의 예시
지역 변수 사용 2. 변수를 final로 선언하지 않은 경우의 예시

 

728x90

'BackEnd > Java' 카테고리의 다른 글

자바 Enum 타입 속 모든 비밀: 정의, 컴파일러, 싱글톤  (0) 2023.10.25
BigDecimal 사용 이유  (0) 2023.10.19
Java Virtual Machine  (2) 2023.10.14
equals() 메서드와 hashCode() 메서드  (0) 2023.09.16
Stream  (0) 2023.08.20