[20210530] Lambda in Java

익명 클래스로 다양한 동작을 구현할 수 있지만 만족할 만큼 코드가 깔끔하지 않다.
하지만, 람다식을 사용한다면 더욱 간결한 코드를 작성할 수 있다.

람다란?

  • 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라 할 수 있다.
  • 람다(lambda)라는 용어는 람다 미적분학 학계에서 개발한 시스템에서 유래.

람다의 특징

  1. 익명
  2. 함수(<->특정 클래스에 종속되는 것을 메서드라 함)
  3. 전달(메서드 인수로 전달하거나 변수로 저장할 수 있다.)
  4. 간결성(익명 클래스보다 간결)

람다의 표현

람다 표현식은 파라미터, 화살표, 바디로 이루어진다. (Apple a1, Apple a2) -> a.getWeight().compareTo(a2.getWeight());

람다를 어디에, 어떻게 사용할까?

함수형 인터페이스라는 문맥에서 람다표현식을 사용할 수 있다.

(1) 함수형 인터페이스

  • 정확히 하나의 추상 메서드를 지정하는 인터페이스
  • 인터페이스에 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스다.

(2) 함수 디스크립터

  • 람다 표현식의 시그니처를 서술하는 메서드(함수형 인터페이스의 추상 메서드)를 함수 디스크립터라 부른다.

(3) @FunctionalInterface 함수형 인터페이스에 @FunctionalInterface 어노테이션을 추가할 수 있다. 만약 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다.

람다 활용 : 실행 어라운드 패턴

실행 어라운드 패턴 : 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태(ex. 파일 처리, DB 처리 등)

1단계: 동작 파라미터화를 기억하라.

  • 자원을 열고, 자원을 닫는 것은 중복되는 동작이다.
  • 자원을 처리하는 동작을 람다를 이용해서 전달한다.

2단계: 함수형 인터페이스를 이용해서 동작 전달

  • 자원을 처리하는 동작을 정의할 함수형 인터페이스를 작성한다.

3단계:동작 실행

  • 정의된 함수형 인터페이스의 시그니처와 일치하는 람다를 전달하면 된다.

4단계: 람다 전달

  • 람다를 이용해서 다양한 동작을 메서드로 전달할 수 있다.

(java8에 추가된)함수형 인터페이스 사용

(1) Predicate : (T) -> boolean (2) Consumer : (T) -> void (3) Function : (T) -> R (T, R은 제네릭 형식)

자바에서 변수는 참조형과 기본형이 존재하는데, 제네릭 형식은 참조형만 지원한다. 그래서 제네릭 형식을 사용하기 위해서는 기본형을 참조형으로 변환 해주어야 한다.(boxing) 명시적으로 박싱과 언박싱을 하지 않더라도 오토박싱 기능을 제공하기 때문에 신경 쓰지않고 사용할 수 있다. 하지만, 박싱과 언박싱은 변환 과정에서 비용이 든다. 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.(int를 처리하는 Predicate -> IntPredicate …)

형식 검사, 형식 추론, 제약

람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않는데, 어떻게 구분하는가?

(1) 형식 검사 람다가 사용되는 context를 이용해서 람다의 형식을 추론할 수 있다. 어떤 context(예를 들면 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현식의 형식을 대상 형식이라 부른다.

(2) 같은 람다, 다른 함수형 인터페이스 대상 형식이라는 특징 때문에 같은 람다 표현식이라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다. 즉, 하나의 람다 표현식을 다양한 함수형 인터페이스에 사용할 수 있다.(함수 디스크립터와 람다의 시그니처가 일치해야함) 자바7의 다이아몬드 연산자(<>)도 context에 따른 제네릭 형식을 추론한 것이다.

(3) 형식 추론 자바 컴파일러는 람다 표현식이 사용된 컨텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.

<형식을 추론하지="" 않음=""> Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); <형식을 추론함=""> Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); (4) 지역 변수 사용 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다. 이를 람다 캡처링(capturing lambda)라 부른다. 사용하는 지역변수는 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정 하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다. ### 메서드 레퍼런스 - 메서드 레퍼런스를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. - 때로는 람다 표현식보다 메서드 레퍼런스를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다. - 메서드명 앞에 구분자(::)를 붙이는 방식으로 활용 가능(Apple::getWeight는 Apple 클래스에 정의된 getWeight의 메서드 레퍼런스다.) - 이때, 메서드 레퍼런스와 함수형 인터페이스의 시그니처가 같아야만 한다. ### 람다 표현식의 조합 - 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다. - 하지만, 여러 람다 표현식을 조합하기 위해서는 함수형 인터페이스에서는 관련 메서드를 제공해야 하는데, 함수형 인터페이스는 단, 하나만의 추상 메서드를 가지고 있어야 하기때문에 설계에서 벗어나느것이 아닌가? - 여기서 디폴트 메서드라는 개념을 사용하여 설계에 벗어나지 않으면서 추가적인 메서드를 제공해준다. - 람다 표현식의 조합을 활용해서 복잡한 기준으로 정렬, Predicate, Function 등을 사용할 수 있다. 일명, 파이프라인을 구축한다고 보면 된다. --- ### 출처 Java 8 in Action by Alan Mycroft and Mario Fusco 참고