Link
Today
Total
10-17 00:15
Archives
관리 메뉴

초보개발자 긍.응.성

(모던 자바 인 액션) Chapter 3 람다 표현식 본문

책 정리/모던 자바 인 액션

(모던 자바 인 액션) Chapter 3 람다 표현식

긍.응.성 2020. 12. 13. 23:22
반응형

이 장에서는 람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 설명한다. 또한 자바 8 API에 추가된 중요한 인터페이스와 형식 추론 등의 기능을 확인한다.

람다의 특징

  • 익명 - 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구형해야 할 코드에 대한 걱정거리가 줄어든다
  • 함수 - 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
  • 전달 - 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성 - 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.

람다의 구성

/* 람다 파라미터  | 화살표 |             람다 바디               */
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • 파라미터 리스트 - 람다 바디에서 사용할 메서드 파라미터를 명시한다.
  • 화살표 - 람다의 파라미터 리스트와 바디를 구분한다.
  • 람다 바디 - 람다의 반환 값에 해당하는 표현식이다.

람다의 사용

함수형 인터페이스

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다. 아무리 많은 디폴트 메서드가 존재하더라도 추상 메서드가 오직 하나면 함수형 인터페이스이다. 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 인터페이스의 인스턴스로 취급할 수 있다.

함수형 인터페이스에는 @FuntionalInterface 애노테이션을 함께 붙여주자. @FuntionalInterface는 함수형 인터페이스임을 가리키는 애노테이션이다. 만약 애노테이션을 선언했지만 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다.

함수 디스크립터

함수형 인터페이스의 추상 메서드를 람다로 구현하였다면, 둘의 표현식 시그니처는 같다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.

예를 들어 함수형 인터페이스 Comparator의 compare 메서드의 함수 디스크립터는 (T, T) → int이다.

실행 어라운드 패턴

고정된 설정과 정리 과정을 두고 실제 자원을 처리하는 코드를 설정과 과정리 두 과정이 둘러싸는 형태의 패턴을 실행 어라운드 패턴(execute around pattern)이라고 부른다. 이때 실제 자원을 처리하는 코드를 파라미터화 하고 람다를 통해 동작을 전달할 수 있다.

함수형 인테페이스 사용

Predicate : (T) → boolean

@FuncationalInterface
public interface Predicate<T> {
	boolean test(T t);
}

Consumer : (T) → void

@FuncationalInterface
public interface Consumer<T> {
	void accept(T t);
}

Supplier : () → T

@FunctionalInterface
public interface Supplier<T> {
	T get();
}

Function : (T) → R

@FuncionalInterface
public interface Function<T, R> {
	R apply(T t);
}

기본형 특화

제네릭은 참조형 타입만 지정할 수 있다. Integer와 Long 같이 기본 타입에 대하여 박싱 된 타입을 통해 제네릭을 이용할 수 있지만, 오토 박싱으로 인해 변환 비용이 소모된다. 자바 8에 추가된 함수형 인터페이스는 기본형을 입출력으로 사용하는 상황에서 오토 박싱 동작을 피할 수 있도록 기본형에 특화된 버전의 함수형 인터페이스를 제공한다.

앞서 보았던 함수형 인터페이스의 이름 앞에 사용하는 기본형 타입의 이름을 합친 이름으로 제공된다. 예로 Predicate이나 파라미터로 int를 받는 Predicate을 처리할 수 있게 IntPredicate을 제공한다.

@FunctionalInterface
public interface IntPredicate {
	boolean test(int value);
}

Predicate, Consumer, Function을 기반으로 다양한 타입에 대하여 지원하는 형태의 함수형 인터페이스를 포함하여 파라미터 개수, 파라미터 타입과 반환 타입의 관계, 파라미터 타입들의 관계에 따라 지원되는 함수형 인터페이스가 존재하니 이를 잘 숙지하길 바란다.

  • Unary - 파라미터 타입과 반환 타입이 같은 경우

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

}
  • Bi - 파라미터 인자가 두 개인 경우

@FunctionalInterface
public interface BiFunction<T, U, R> {
	R apply(T t, U u);
}

함수형 인터페이스와 예외

java.util.function의 함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야한다.

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

람다 표현식이 어떻게 동작하게 되는지 실제 형식을 파악하자

형식 검사

람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식(type)을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 **대상 형식(target type)**이라고 부른다. 형식검사는 다음과 같은 과정으로 진행된다.

  1. 람다가 사용된 메서드의 선언을 확인한다.
  2. 람다가 사용된 메서드의 파라미터로 대상 형식을 기대한다.
  3. 기대하는 파라미터의 함수형 인터페이스를 파악한다.
  4. 그 함수형 인터페이스의 함수 디스크립터를 묘사한다.
  5. 전달받은 인수의 람다가 그 요구사항을 만족해야 한다.

형식 추론

제네릭을 사용할 때 선언부에 타입 매개변수를 명시하면 생성자에서는 빈 다이아몬드 연산자로 남겨두어도 자바 컴파일러는 생성 객체의 타입을 추론할 수 있다. 람다 표현식도 마찬가지이다. 자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.

// 형식 추론을 하지 않음
Comparator<Apple> c =
	(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 형식을 추론함
Comparator<Apple> c = 
	(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

지역 변수 사용(제약)

람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다. 이를 람다 캡처링(lamda capturing)이라 부른다. 하지만 그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다(이후 재 할당 불가).

인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 지역 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.

메서드 참조

명시적으로 메서드 명을 참조함으로써 가독성을 높일 수 있다. 메서드 참조는 메서드명 앞에 구분자(::)를 붙이는 방식으로 사용할 수 있다. Class::method 형식을 취한다. 메서드 참조는 세 가지 유형으로 구분할 수 있다.

  1. 정적 메서드 참조 (Integer::parseInt)
  2. 다양한 형식의 인스턴스 메서드 참조 (String::length)
  3. 기존 객체의 인스턴스 메서드 참조 (Apple::getWeight)

또한 ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이는 정적 메서드의 참조를 만드는 방식과 비슷하다.

람다 표현식을 조합할 수 있는 유용한 메서드

함수형 인터페이스에서는 다양한 유틸리티 메서드를 지원한다. Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공하며, 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다. 이 유틸리티 메서드는 디폴트 메서드로 제공되어 함수형 인터페이스의 정의를 해치지 않으며 여러 조합을 가능케 하는 유틸리티를 제공한다.

Comparator

  • comparing - 비교에 사용할 Function 기반의 키 지정
  • reversed - 역정렬
  • thenComparing - 동일한 조건에 대하여 추가적인 비교

Predicate

  • and - and 연산
  • or - or 연산
  • negate - not 연산

Function

  • andThen - 이후에 처리할 function 추가
  • compose - 이전에 처리되어야 할 function 추가
반응형
Comments