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

Java8 In Action Chapter2의 예제를 직접 타이핑.

- 실행에 문제 없는 소스.


Main 소스


Model 소스

- Apple.java


- ApplePredicate.java


- AppleGreenColorPredicate.java

- AppleHeavyWeightPredicate.java

- Predicate.java


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

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

2. Chapter2 - 동작 파라미터화 코드 전달하기


우리가 어떤 상황에서 일을 하던 소비자 요구사항은 항상 바뀜.

변화하는 요구사항은 소프트웨어 엔지니어링에서 피할 수 없는 문제.

새로 추가한 기능은 쉽게 구현할 수 있어야 하며 장기적인 관점에서 유지보수가 쉬어야 함.

동작 파라미터화(behavior parameterization)를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응 가능.

동작 파라미터화란 아직 어떻게 실행될 것인지 결정하지 않은 코드 블록을 의미.

이 코드 블록은 나중에 프로그램에서 호출.


1. 변화하는 요구사항에 대응하기.

- 비슷한 코드를 구현한 다음 추상화 하기.

- 소프트웨어 공학의 DRY(don't repeat yourself - 같은 것을 반복하지 말 것) 원칙 준수.

- 어떤 기준으로 필터링할지 가리키는 플래그를 추가하는 개발은 지양.

> 실전에서는 사용하지 말아야 하는 방법!!!

> 해당 기법은 동작 파라미터화로 충분히 해결이 가능함.


아래 예제를 통해서 유연한 코드를 만드는 모범 사례를 전달.

예제에서 사용 될 Apple Class.


첫번째 예제: 녹색 사과 필터링

- 사과 목록 중에서 녹색 사과만 선택.


두번째 예제: 색을 파라미터화

- 다양한 색의 사과를 필터링 해야하는 요구사항이 발생.

- 색이 아닌 무게 기준 필터링의 요구사항 발생.

- 색상 필터 조건이 무게 필터 조건으로만 변경됨.

- 비슷한 코드의 추상화 작업이 필요함.


세번째 예제: 가능한 모든 속성 필터링

- 색 또는 무게 중 어떤 것을 필터링할지 가리키는 플래그를 통한 필터링

- 앞으로 색 & 무게 이외에 다른 기준을 통한 필터링에 대해서는 대처가 불가능한 코드.

- 위와 같이 flag 값을 통해 개발하는 것은 지양하도록 해야함.


2. 동작 파라미터화

- 파라미터를(플래그)를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할수 있는 방법이 절실.

- 전략 디자인 패턴 적용

> 선택 조건을 결정하는 인터페이스 정의(프레디케이트)

> 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate 정의.

> 전략 디자인 패턴은 각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해준 다음

 런타임에 알고리즘을 선택하는 기법.


네번째: 추상적 조건으로 필터링

- ApplePredicate 인터페이스를 구현한 메서드를 통한 필터 처리.

- 첫번째 코드에 비해 더 유연한 코드로 재 탄생 되었음.

- 전달한 ApplePredicate 객체에 의해 메서드의 동작이 결정됨.

- 필요한 대로 ApplePredicate를 구현하여 메서드로 전달 가능.

- 한 개의 파라미터, 다양한 동작이 가능해짐.


3. 복잡한 과정 간소화

- 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공.

- 익명 클래스를 이용하면 코드의 양을 줄일 수 있음.

> 단, 익명 클래스가 모든 것을 해결하는 것은 아님.


3.1 익명 클래스

- 익명 클래스는 자바의 지역클래스(블록 내부에 선언된 클래스)와 비슷한 개념.

- 말 그대로 이름이 없는 클래스.

- 익명 클래스를 사용하면 클래스 선언과 인스턴스화를 동시에 할 수 있음.


다섯번째 예제: 익명 클래스 사용

- 익명 클래스 역시 많은 여러 Line을 차지함.

- 많은 프로그래머가 익명 클래스 사용에 익숙하지 않는 단점을 지님.


여섯번째 예제: 람다 표현식 사용

- 자바8 람다 표현식을 이용해 다섯번째 예제의 소스를 다음과 같아 간단하게 구현.


일곱번째 예제: 리스트 형식으로 추상화

- 사과 이외에 다양한 Item에 대해 필터링이 작동되도록 리스트 형식을 추상화.


4. 실전 예제

- 동작 파라미터화가 변화하는 요구사항에 쉽게 적응하는 유용한 패턴임을 확인함.

- 동작 파라미터화 패턴은 동작을 (한 조각의 코드로) 캡슐화한 다음 메서드로 전달하여 메서드의 동작을 파라미터화 함.

> 예를 들면 사과의 다양한 Predicate.

- 코드 전달 개념을 다기지 위한 Comparator로 정렬. Runnable로 코드 블록 실행하기 예제 작성.


예제: Comparator로 정렬

예제: Runnable 코드 블록 실행.

- 람다 표현식을 이용한 코드.


5. 요약

- 동작 파라미터화는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달.

- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할수 있는 코드 구현 가능.

> 엔지니어링 비용을 줄일 수 있음.

- 코드 전달 기법을 이용하여 동작을 메서드 인수로 전달 가능.

> 하지만 자바8 이전에는 코드를 지저분하게 구현해야 했음.

> 익명 클래스로도 어느 정도 코드를 깔끔하게 만들수 있으나 자바 8에서는 인터페이스를 상속받아

 여러 클래스를 구현해야하는 수고를 없앴수 있는 방법 제공.

- 자바 API이 많은 메서드는 정렬, 스레드, GUI 처리등을 포함한 다양한 동작으로 파라미터화 가능.



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

스트림 활용  (0) 2017.06.26
스트림 소개  (0) 2017.06.16
람다 표현식  (0) 2017.06.15
동작 파라미터화 예제 소스  (0) 2017.06.13
자바8을 눈여겨봐야 하는 이유  (0) 2017.06.13

+ Recent posts