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 |