Chapter3 - 람다 표현식


익명 클래스로 다양한 동작을 구현할 수 있지만 만족할 만큼 코드가 깔끔하지 않음.

> 깔끔하지 않은 코드는 동작 파라미터화를 적용하는 것을 막는 요소.

람다 표현식은 익명 클래스처럼 이름이 없는 함수면서 메서드를 인수로 전달할 수 있음.

> 람다에 대해 더 알아보기 전에는 익명 클래스와 비슷한 것이라 인지하면 좋음.

더 간결하고 유연한 코드를 구현하는 방법을 단계적으로 설명.


1. 람다란 무엇인가?

- 메서드로 전달할 수 있는 익명 함수를 단순화 한 것.

- 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외리스트는 가질 수 있음.

> 익명

* 보통의 메서드와 달리 이름이 없으므로 익명이라 표현.

> 함수

* 메서드처럼 특정 클래스에 종속되지 않으므로 함수라 표현.

* 하지만 메서드 처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함.

> 전달

* 메서드 인수로 전달하거나 변수로 저장 가능.

> 간결성

* 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없음.

- 람다(lambda)라는 용어는 람다 미적분학 학계에서 개발한 시스템에서 유래

- 람다 표현식이 중요한 이유는?

> 간결한 방식으로 코드 전달이 가능하기 때문.

- 람다는 세 부분으로 구성됨.

> 파라미터 리스트

* Comparator의 compare 메서드의 파라미터(두 개의 사과) - 예제임.

> 화살표

* 람다의 파라미터 리스트와 바디를 구분.

> 람다의 바디

* 람다의 반환값에 해당하는 표현식.


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

- 함수형 인터페이스라는 문맥에서 람다 표현식 사용 가능.


2.1 함수형 인터페이스

- 정확히 하나의 추상 메서드를 지정하는 인터페이스

> 자바 API의 함수형 인터페이스로 Comparator, Runnalbe등이 있다.

- 인터페이스는 디폴트 메서드를 포함할 수 있음. 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스.

* 디폴트 메서드: 인터페이스의 메서드를 구현하지 않는 클래스를 고려해서 기본 구현을 제공하는 바디를 포함하는 메서드.


2.2 함수 디스크립터

- 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가르킴.

- 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라 부름.

> ex) Runnable 인터페이스의 추상 메서드 run은 인수와 반환값이 없음.(void반환)

* Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있음.

- () -> void 라는 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미.

- 람다 표현식은 변수에 할당하거나 함수형 인터페이스를 인수로 받는 메서드로 전달 가능.

- 함수형 인터페이스의 추상 메서드와 같은 시그니처를 갖음.


@FunctionalInterface란?

- 함수형 인터페이스를 가리키는 어노테이션

- 실제로 함수형 인터페이스가 아닐 경우 컴파일러에서 에러 발생.

- 한 개 이상의 추상 메서드가 존재할 경우 "Multiple nonoverriding abstract method found in interface xxx" 발생할 수 있음.


2.3 함수형 인터페이스 사용

- 다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요.

- 아래 자바 8 라이브러리인 Predicate, Consumer, Function 인터페이스를 설명 진행.

2.3.1 Predicate

- java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의.

- test는 제네릭 형식 T의 객체를 인수로 받아 boolean 반환.

- T 형식의 객체를 사용하는 boolean 표현식이 필요한 상황에서 Predicate를 사용할 수 있음.


2.3.2 Consumer

- java.util.function.Consumer<T> 인터페이스는 accept라는 추상 메서드를 정의.

- accept는 제네릭 형식 T의 객체를 인수로 받아 어떤 동작을 수행하고 싶을때 사용.

- Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있음.


2.3.3 Function

- java.util.function.Function<T, R> 인터페이스는 apply라는 추상 메서드를 정의.

- apply는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환.

- 입력을 출력으로 매핑하는 람다를 정의할때 Function 인터페이스를 활용할 수 있음.


- 기본형 특화

> 자바의 모든 형식은 참조형(Byte, Integer, Object, List..) 아니면 기본형 (int, double, byte, char...)에 해당.

> 제네릭 파라미터에는 참조형만 가능.

> 자바에는 기본형 <-> 참조형 변환 기능을 제공.

* 기본형 -> 참조형: 박싱, 기본형 <- 참조형: 언박싱, 자동으로 이루어지는 방식: 오토박싱

* 형 변환시 비용이 소모.

> 자바8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공.

> 일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 Double... Int..., LongBinary...처럼 형식명이 붙음.

> ToIntFunction<T>, IntToDoubleFunction등 다양한 출력 형식 인터페이스 제공.

- 자바 8의 대표적 함수형 인터페이스 목록

함수형 인터페이스

함수 디스크립터

기본형 특화

Predicate<T>

T -> boolean

IntPredicate, LongPredicate, DoublePredicate

Consumer<T>

T -> void

IntConsumer, LongConsumer, DoubleConsumer

Function<T, R>

T -> R

IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>,

ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>

Supplier<T>

() -> T

BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier

UnaryOperator<T>

T -> T

IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator

BinaryOperartor<T>

(T, T) -> T

IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator

BiPredicate<L, R>

(L, R) -> boolean


BiConsumer<T, U>

(T, U) -> void

ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>

BeFunction<T, U, R>

(T, U) -> R

ToIntBiFunction<T, U>, ToLongBiFunction<T, U>, ToDoubleBiFunction<T, U>


- 람다와 함수형 인터페이스 예제

사용 사례

람다 예제

대응하는 함수형 인터페이스

불린 표현

(List<String> list) -> list.isEmpty()

Predicate<List<String>>

객체 생성

() -> new Apple(10)

Supplier<Apple>

객체에서 소비

(Apple a) -> System.out.println(a,getWeight())

Consumer<Apple>

객체에서 선택/추출

(String s) -> s.length()

Function<String, Integer> 또는 ToIntFunction<String>

조합

(Int a, int b) -> a * b

IntBinaryOperator

객체 비교

(Apple a1, Apple a2) -> a.getWeight().compareTo(a2.getWeight())

Comparator<Apple> 또는 BiFunction<Apple, Apple, Integer> 또는 ToIntBiFunction<Apple, Apple>


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

- 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지 정보가 포함되어 있지 않음.

- 람다 표현식을 제대로 이해하려면 람다의 실제 형식 파악이 중요.


2.4.1 형식 검사

- 람다가 사용되는 컨텍스트를 이용해서 람다의 형식 추론 가능.

> 어떤 컨텍스트에서 기대되는 람다 표현식이 형식을 대상 형식이라 부름.

@ 형식 검사 과정

1. filter 메서드의 선언 확인

2. filter 메서드는 두번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대.

3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스.

4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사.

5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야함.


2.4.2 같은 람다, 다른 함수형 인터페이스

- 대상 형식이라는 특정 때문에 같은 람다 표현식이라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용 가능.

2.4.3 형식 추론

- 자바 컴파일러는 람다 표현식이 사용된 컨텍스트를 이용해 람다 표현식와 관련된 함수형 인터페이스를 추론.

> 즉, 대상 형식을 이용해 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론 가능.

> 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략 가능.

> 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상 시킬 때도 있음.


2.4.4 지역 변수 사용

- 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용 할 수 있음.

> 이를 람다 캡처링이라고 부름.

- 자유 변수에도 약간의 제약은 존재.

> 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록)할 수 있음.

> 지역변수는 명시적으로 final선언 또는 실질적으로 final로 선언된 변수와 똑같이 사용되어야 함.

* 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡쳐할 수 있음.

- 지역 변수의 제약

> 내부적으로 인스턴스 변수와 지역 변수는 태생이 다름.

> 인스턴스 변수는 힙에 저장, 지역 변수는 스택에 위치.

> 람다에서 지역 변수에 바로 접근할 수 있다는 가정 하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서

 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음.

> 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공.

> 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 항당해야 하는 제약이 생긴 것.

> 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴에 제동을 걸 수 있음.


2.5 메서드 레퍼런스

- 기존의 메서드 정의를 재활용하여 람다처럼 전달 가능.

- 때로는 람다 표현식보다 메서드 레퍼런스를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있음.


2.5.1 요약

- 메서드 레퍼런스는 특정 메서드만 호출하는 람다의 축약형이라고 생각할 수 있음.

- 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 레퍼런스 사용 가능.

- 실제로 메서드를 호출하는 것이 아니므로 ()는 필요 없음.

  - 람다와 메서드 레퍼런스 단축표현 예제

람다

메서드 레퍼런스 단축 표현

(Apple a) -> a.getWeight()

Apple::getWeight

() -> Thread.currentThread().dumpStack()

Thread.currentThread()::dumpStack

(str, i) -> str.subString(i)

String::subString

(String s) -> System.out.println(s)

System.out::println


- 메서드 레퍼런스를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 간주 가능.

- 메서드 레퍼런스 만드는 방법 (굵고 색상 적용하기)

> 메서드 레퍼런스는 세가지 유형으로 구분 가능

* 정적 메서드 레퍼런스

@ Integer의 parseInt 메서드는 Integer::parseInt로 표현 가능.

* 다양한 형식의 인터페이스 메서드 레퍼런스

@ String의 length 메서드는 String::length로 표현 가능.

* 기존 객체의 인스턴스 메서드 레퍼런스

@ Transaction 객체를 할당 받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 존재.

 expensiveTransaction::getValue로 표현 가능.


2.5.2 생성자 레퍼런스

- ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 레퍼런스를 만들수 있음.

- 정적 메서드의 레퍼런스를 만드는 방법과 비슷.


2.6 람다, 메서드 레퍼런스 활용하기

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

- Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공.

- 해당 의미는, 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다는 것.


2.7.1 Comparator 조합

- 정적 메서드 Comparator.comparing을 이용하여 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환 가능.

- 역정렬

> 역정렬을 하기 위해서 다른 Comparator 인스턴스를 생성할 필요가 없음.

> 인터페이스 자체에서 주어진 비교자의 순서를 바꾸는 reverse라는 디폴트 메서드를 제공함.

- Comparator 연결

> 정렬 시 동일한 조건이 결과가 존재한다면 두 번째 Comparator를 생성할 수 있음.

> thenComparing메서드로 두 번째 비교자를 만들수 있음.

> thenComparing은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객채가 같다고 판단되면 두 번째 비교자에 객체를 전달.


2.7.2 Predicate 조합

- Predicate 인터페이스는 복잡한 Predicate를 만들수 있도로 negate, and, or 세 가지 메서드를 제공.

2.7.3 Function 조합

- andThen, compose 두 가지 디폴트 메서드를 제공.

2.8 요약

- 람다 표현식은 익명 함수의 일종.

> 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지고 있으며 예외를 던질 수 있음.

- 람다 표현식으로 간결한 코드 구현 가능.

- 함수형 인터페이스는 하나의 추상 메서드만 정의하는 인터페이스.

- 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있음.

- 람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉석으로 제공 가능하여 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급.

- java.util.function 패키지는 Predicate<T>, Function<T, R>, Supplier<T>, Consumer<T>, BianryOperator<T> 등을 포함해서

 자주 사용하는 다양한 함수형 인터페이스를 제공.

- Predicate<T>와 Function<T, R> 같은 제네릭 함수형 인터페이스와 관련한 박싱 동작을 피할 수 있도록 IntPredicate, IntToLongFunction

 등과 같은 기본형 특화 인터페이스도 제공.

- 람다 표현식의 기대 형식을 대상 형식이라 함.

- 메서드 레퍼런스를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있음.

- Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공.


'Java8 > Java 8 in Action' 카테고리의 다른 글

스트림 활용  (0) 2017.06.26
스트림 소개  (0) 2017.06.16
동작 파라미터화 예제 소스  (0) 2017.06.13
자바8 동작 파라미터화 코드 전달하기  (0) 2017.06.13
자바8을 눈여겨봐야 하는 이유  (0) 2017.06.13

+ Recent posts