소스코드
https://github.com/mjung1798/Jyami-Java-Lab/tree/master/java8-in-action
동작 파라미터화 : 변화하는 요구사항에 효과적으로 대응할 수 있는 코드
- 익명 클래스 : 다양한 동작 구현 가능 -> 코드가 깔끔하지 않다
- 람다 표현식 : 더 깔끔한 코드로 동작을 구현할 수 있다.
1. 람다란 무엇인가
a. 람다의 특징
- 익명 : 보통 메서드와 다르게 이름이 없다
- 함수 : 메서드처럼 특정 클래스에 종속되지 않는다. 하지만 메서드처럼 파라미터 리스트 바디, 반환 형식, 가능한 예외 클래스 포함
- 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
b. 람다의 구성
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight);
- 파라미터 리스트 : Comparator의 compare 메서드의 파라미터 (두 개의 사과)
- 화살표 : 람다의 파라미터 리스트와 바디를 구분
- 람다의 바디 : 두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식
c. 람다 예제
사용 사례 | 람다 예제 |
---|---|
불린 표현식 | (List<String> list) -> list.isEmpty() |
객체 생성 | ()->new Apple(10) |
객체에서 소비 | (Apple a)->{System.out.println(a.getWeight)} |
객체에서 선택/추출 | (String s)-> s.length() |
두 값을 조합 | (int a, int b)-> a * b |
두 객체 비교 | (Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight); |
2. 람다를 사용하는 곳
함수형 인터페이스 문맥에서 사용
a. 함수형 인터페이스
- 정확히 하나의 추상메서드를 지정하는 인터페이스
- 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스
ex ) Comprator
, Runnable
람다는 함수형 인터페이스의 추상 메서드 구현을 직접 전달 할 수 있어 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.
@FunationalInterface
: 함수형 인터페이스임을 가리키는 어노테이션, 이를 선언하고 실제로 함수형 인터페이스가 아니면 컴파일 에러 발생
b. 함수 디스크립터(function descriptor)
람다 표현식의 시그너처를 서술하는 메서드
() -> void
: 파라미터 리스트가 없으며 void 를 반환하는 함수 : Runnable
(Apple, Apple) -> int
: 두개의 Apple을 인수로 받아 int를 반환하는 함수
c. 사용법
- 함수형 인터페이스를 인수로 받는 메서드로 전달
- 변수에 할당
3. 람다 활용 : 실행 어라운드 패턴
실행 어라운드 패턴(execute around pattern) : 자원을 열고 -> 처리한 다음(변동) -> 자원을 닫는다
// 자원 열고 닫는 로직이 중복적으로 앞뒤에 있다.
public static String processFile2(BufferedReaderProcessor p) throws IOException {
InputStream fileResourceAsStream = FileProcessor.class.getClassLoader().getResourceAsStream("data.txt");
try(BufferedReader br = new BufferedReader(new InputStreamReader(fileResourceAsStream))){
return p.process(br);
}
}
//처리 로직에 대한 동작을 함수형 인터페이스를 이용해 파라미터화 하였다.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
// 처리 로직에 대한 동작을 선언하여, 실행 어라운드 패턴안에서 해당 동작을 수행한다.
@Test
@DisplayName("실행 어라운드 패턴 개선1")
void name2() throws IOException {
String s = FileProcessor.processFile2(br -> br.readLine());
assertThat(s).isEqualTo("line1");
}
4. 함수형 인터페이스 사용
함수 디스크립터(function descriptor) : 함수형 인터페이스의 추상 메서드 시그너처
공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합 : Comparable, Runnable, Callable 등 자바에서 포함하고 있다
a. Predicate
Predicate<String> lengthOnePredicate = (String s) -> s.length() == 1;
List<String> filter = PredicateProcess.filter(list, lengthOnePredicate);
- Input : 제네릭 형식 T 객체
- Output : Boolean 반환
b. Consumer
Consumer<Integer> printConsumer = i -> System.out.println(i);
ConsumerProcess.forEach(list, printConsumer);
- Input : 제네릭 형식 T 객체
- Output : void 반환
c. Function
Function<String, Integer> parseIntegerFunction = s -> Integer.parseInt(s);
List<Integer> map = FunctionProcess.map(list, parseIntegerFunction);
- Input : 제네릭 형식 T 객체
- Output : 제네릭 형식 R 객체
d. 기본형 특화
- 박싱(boxing) : 기본형 -> 참조형 변환
- 언박싱(unboxing) : 참조형 -> 기본형 변환
- 오토박싱(autoboxing) : 박싱과 언박싱이 자동으로 이루어짐
기본형 특화 함수형 인터페이스 : 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있게 함
함수형 인터페이스 | 함수 디스크립터 | 기본형 특화 |
---|---|---|
Predicate<T> |
T -> boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer<T> |
T -> void | IntConsumer, LongConsumer, DoubleConsumer |
Function<T,R> |
T -> R | IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction |
Supplier<T> |
() -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator<T> |
T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator<T> |
(T, T) -> T | IntBinaryOprator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate<L,R> |
(L, R) -> boolean | |
BiConsumer<T,U> |
(T, U) -> void | ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer |
BiFunction<T,U,R> |
() | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
5. 형식 검사, 형식 추론, 제약
a. 형식검사
람다가 사용되는 컨텍스트를 이용해서 람다의 형식을 추론할 수 있다.
대상형식 (target type) : 어떤 컨텍스트에서 기대되는 람다 표현식의 형식
filter(inventory, (Apple a)->a.getWeight > 150);
1. 람다가 사용된 컨텍스트확인 : filter의 정의 확인
2. 두 번째 파라미터로 Predicate<Apple>
형식(대상 형식)을 기대한다.
3. Predicate<Apple>
의 추상메서드를 확인한다.
4. Apple을 인수로 받아 boolean을 반환하는 test 메서드다 (Apple->boolean)
5. 함수 디스크립터와, 람다의 시그너처가 일치한다! : 형식검사 완료
b. 같은 람다 다른 함수형 인터페이스
대상 형식이라는 특징덕분에 같은 람다표현식이더라도 호환되는 추상메서드를 가진 다른 함수형 인터페이스 사용이 가능하다.
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
하나의 람다표현식을 다양한 함수형 인터페이스에 사용할 수 있다.
c. 형식추론
자바 컴파일러
- 람다 표현식이 사용된 컨텍스트(대상 형식) 를 이용해 람다 표현식과 관련된 함수형 인터페이스 추론
- 대상 형식을 이용해서 함수 디스크립터를 알 수 있다.
- 컴파일러는 람다의 시그너처도 추론할 수 있다.
상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있다.
// 형식 추론을 하지 않는다
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 형식 추론을 한다
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
d. 지역변수 사용
- 자유변수(free variable) : 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수 => 람다캡처링(capturing lambda)
- 제약 : final로 선언되어있거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야한다.
람다 표현식은 한번만 할당할 수 있는 지역변수를 캡쳐한다.
int portNumber = 1337;
Runnable r = ()->System.out.println(portNumber);
portNumber = 31337;
제약이 있는 이유
- 인스턴스 변수는 힙에 저장, 지역변수는 스택에 저장된다.
- 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서 해당 변수에 접근하려 할 수 있다.
- 원래 변수에 접근을 허용하지 않고 자유 지역 변수의 복사본을 제공한다
- 복사본의 값이 바뀌지 않아야한다 == 지역 변수에는 한 번만 값을 할당해야 한다.
e. 클로저
클로저 : 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스
설명! | 클로저 | 람다 |
---|---|---|
다른함수의 인수로 전달할 수 있다. | O | O |
자신의 외부 영역의 변수에 접근할 수 있다. | O | O |
람다가 정의된 메서드의 지역변수 값을 바꿀 수 있다. | O | X |
6. 메서드 레퍼런스
특정 메서드만을 호출하는 람다의 축약형이다.
명시적으로 메서드명을 참조하여 가독성을 높일 수 있다! 메서드 명 앞에 구분자(::)를 붙이는 방식으로 활용한다.
Apple 클래스에 정의된 getWeight 메서드 레퍼런스
Apple::getWeight
(Apple a)-> a.getWeight()
a. 메서드 레퍼런스를 만드는 방법
1. 정적 메서드 레퍼런스
Function<String, Integer> stringIntegerFunctionLambda = (String str) -> Integer.parseInt(str);
Function<String, Integer> stringIntegerFunction = Integer::parseInt;
2. 다양한 형식의 인스턴스 메서드 레퍼런스
Function<String, Integer> stringIntegerFunctionLambda = (String arg0) -> arg0.length();
Function<String, Integer> stringIntegerFunction = String::length;
3. 기존 객체의 인스턴스 메서드 레퍼런스
람다 표현식에 현존하는 외부 객체의 메서드를 호출 할 때 사용
Apple apple = new Apple("red", 100);
Supplier<Integer> supplierLambda = () -> apple.getWeight();
Supplier<Integer> supplier = apple::getWeight;
메서드 레퍼런스는 컨텍스트의 형식과 일치하는지 확인한다.
b. 생성자 레퍼런스
ClassName::new 처럼 클래스와 new 키워드를 사용해 기존 생성자의 레퍼런스를 만들 수 있다.
@Getter
public class Apple {
private String color;
private Integer weight;
public Apple() {
}
public Apple(String color) {
this.color = color;
}
public Apple(Integer weight) {
this.weight = weight;
}
public Apple(String color, Integer weight) {
this.color = color;
this.weight = weight;
}
}
// 기본 생성자 레퍼런스
Supplier<Apple> appleSupplier = Apple::new;
//인수 1개 생성자 레퍼런스 : 무게
Function<Integer, Apple> appleFunction = Apple::new;
// 인수 1개 생성자 레퍼런스 : 색깔
Function<String, Apple> appleFunction = Apple::new;
// 인수 2개 생성자 레퍼런스
BiFunction<String, Integer, Apple> appleBiFunction = Apple::new;
시그너처를 대응시켜서 생성자에 접근이 가능하다.
7. 람다, 메서드 레퍼런스 정리
- 코드 전달 : 함수형 인터페이스를 구현하여 사용
- 익명 클래스 사용 : 클래스를 구현하지 않고 바로 인스턴스 화 할 수 있으나 코드가 지저분하다.
- 람다 표현식 사용 : 추상 메서드의 시그너처(함수 디스크립터)가 람다 표현식의 시그너처를 정의한다 = 형식추론에 이용
- 메서드 레퍼런스 활용 : 람다 표현식의 인수를 더 깔끔하게 전달할 수 있다.
8. 람다 표현식을 조합할 수 있는 메서드
람다 표현식을 조합할 수 있는 유틸리티 메서드 : 디폴트메서드를 사용한다
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // 추상메서드
default Comparator<T> reversed() {...}
default Comparator<T> thenComparing(Comparator<? super T> other) {...}
}
실제로 위와 같이 선언이 되어있다.
단순한 람다 표현식을 조합해서 더 복잡한 람다 표현식을 만들 수 있다.
a. Comparator 조합
inventory.sort(
Comparator.comparing(Apple::getWeight)
.reversed() // 역정렬
.thenComparing(Apple::getColor)); // 두번째 비교자를 만들 수 있다 (두 사과 비교 후 같을 때 정렬 법)
b. Predicate 조합
Predicate<Apple> redApple = a -> "red".equals(a.getWeight());
// 기존 Predicate를 반전
Predicate<Apple> notRedApple = redApple.negate();
// 기존 Predicate에 and를 이용해서 빨강이면서 무거운 사과로 조합
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
// 기존 Predicate에 or를 이용해서 빨강이면서 무거운 사과 또는 그냥 녹색사과로 조합
Predicate<Apple> redAndHeavyAppleOrGreen = redApple
.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
c. Function 조합
andThen : 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환
compose : 인수로 주어진 함수를 먼저 실행 한 후에 그 결과를 외부 함수의 인수로 제공
@Test
@DisplayName("Function 연결")
void name3() {
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // g(f(x))
int result = h.apply(1);
assertThat(result).isEqualTo(4);
}
@Test
@DisplayName("Function 연결")
void name4() {
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); // f(g(x))
int result = h.apply(1);
assertThat(result).isEqualTo(3);
}
'Dev Book Review > Java8 in Action' 카테고리의 다른 글
[자바8인액션] Chap17. 리액티브 프로그래밍 (1) | 2021.01.10 |
---|---|
[자바8인액션] Chap15. CompletableFuture와 리액티브 프로그래밍 컨셉의 기초 (2) | 2021.01.10 |
[자바8인액션] Chap.2 동작 파라미터화 코드 전달하기 (0) | 2020.07.30 |
[자바8인액션] Chap.1 자바 8을 눈여겨 봐야하는 이유 (0) | 2020.07.29 |