<기존 코드>
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
<람다 이용한 코드>
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
(Apple a1, Apple a2)
: 람다 파라미터->
: 화살표a1.getWeight().compareTo(a2.getWeight());
: 람다 바디<자바 8의 유용한 다섯 가지 람다 표현식 예제>
(String s) -> s.length();
(Apple a) -> a.getWeight() > 150;
(int x, int y) -> {
System.out.println("Result: ");
System.out.println(x + y);
}
() -> 42;
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
함수형 인터페이스란?
정확히 하나의 추상 메서드를 지정하는 인터페이스
Predicate<T>
인터페이스로 필터 메서드를 파리미터화할 수 있었음Predicate<T>
가 함수형 인터페이스Predicate<T>
는 오직 하나의 추상 메서드만 지정하기 때문!람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.
(정확히는 함수형 인터페이스를 구현한 클래스의 인스턴스)
<예시>
public interface Predicate<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
@FunctionalInterface
- 함수형 인터페이스임을 가리키는 어노테이션
@FunctionalInterface
선언했지만 실제로 함수형 인터페이스가 아니라면 컴파일 에러 발생
Runnable
인터페이스의 유일한 추상 메서드 run
() -> void
참고!
왜 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까?
- 언어를 더 복잡하게 만들지 않는 현재 방법을 선택
- 대부분의 자바 프로그래머가 하나의 추상 메서드를 갖는 인터페이스(예를 들면 이벤트 처리 인터페이스)에 이미 익숙
<그림 3-2 실행 어라운드 패턴>
(T) → boolean
@FuncationalInterface
public interface Predicate<T> {
boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for (T t: list) {
if (p.test(t)) {
results.add(t);
}
}
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
//List<String> nonEmpty2 = filter(listOfStrings, (String s) -> !s.isEmpty());
(T) -> void
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c) {
for (T t: list) {
c.accept(t);
}
}
forEach(
Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i) // Consumer의 accpet를 구현
);
(T) -> R
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
public <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for (T t: list) {
result.add(f.apply(t));
}
return result;
}
List<Integer> l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length(); // Function의 apply 메서드를 구현하는 람다
);
Consumer<T> 의 T
)에는 참조형만 가능Integer
와 Double
은 변환 없이 바로 제네릭에 바로 사용이 가능하지만 int
나 double
같은 형태는 오토 박싱 과정에서 비용이 발생한다.IntPredicate
와 같은 특별한 인터페이스를 제공한다.public interface IntPredicate {
boolean test(int t);
}
DoublePredicate
, IntConsumer
등 처럼 형식명이 붙는다.참고!
Bi
가 앞에 붙는다면 인수를 2개 받을 수 있다.
BiPredicate<L, R>
: (T, U) -> booleanBiConsumer<T, U>
: (T, U) -> voidBiFucntion<T, U, R>
: (T, U) -> R
<filter 함수 람다 표현식 적용>
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
<람다 표현식의 형식 검사 과정 예시>
filter
메서드의 선언을 확인filter(List<Apple> inventory, Predicate<Apple> p)
Predicate<Apple>
형식(대상 형식)을 기대 Predicate<Apple>
test
라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다!test
Apple
을 받아 boolean
을 반환하는 함수다!특별한 void 호환 규칙
- 람다의 바디에 일반 표현식이 있으면
void
를 반환하는 함수 디스크립터와 호환된다.
예)List
의add
메서드는Consumer
콘텍스트(T -> void)지만boolean
을 반환해도 호환
<예시>
// Predicate는 불리언 반환값을 갖는다.
Predicate<String> p = s -> list.add(s);
// Consumer은 void 반환값을 갖지만 호환 가능
Consumer<String> b = s -> list.add(s);
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); // 형식을 추론 X
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); // 형식을 추론 O
람다 캡처링
람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
지역 변수 제약
final
로 선언되어 있거나 한 번만 할당할 수 있는 지역 변수만을 사용할 수 있다.Why❓
- 인스턴스 변수 -> 힙에 저장
- 지역 변수 -> 스택에 저장
- 지역 변수가 스레드에서 실행되다가 해당 스레드가 해제되어도 람다를 실행하는 스레드에서 해당 변수에 접근하려 할 수 있음
- 원래 변수에 접근을 허용하는 것(사라질 수도 있기 때문)이 아니라 자유 지역 변수의 복사본을 제공
➜ 따라서 복사본의 값이 바뀌지 않아야 한다.
<기존 코드>
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
<메서드 참조 사용 - java.util.Comparator.comparing 활용>
inventory.sort(comparing(Apple::getWeight));
장점
사용 방법
Apple::getWeight
Apple
클래스에 정의된 getWeight
의 메서드 참조<람다와 메서드 참조 단축 표현 예제>
람다 | 메서드 참조 단축 표현 |
---|---|
(Apple apple) -> apple.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 |
(String s) -> this.isValidName(s) | this::isValidName |
메서드 참조 만드는 방법 3가지
(String s) -> Integer.parseInt(s)
➜ Integer::parseInt
(String s) -> s.length()
➜ String::length
Transaction
객체를 할당받은 expensiveTransaction
지역 변수가 있고, Transaction
객체에는 getValue
메서드가 있으면 ➜ expensiveTransaction::getValue
방법 | 람다 | 메서드 참조 단축 표현 |
---|---|---|
방법 1 | ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s); | Function<String, Integer> stringToInteger = Integer::parseInt; |
방법 2 | BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element); | BiPredicate<List<String>, String> contains = List::contains; |
방법 3 | Predicate<String> startsWithNumber = (String string) -> this.startsWithNumber(string); | Predicate<String> startsWithNumber = this::startsWithNumber; |
ClassName::new
처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.<기본 생성자>
Supplier
의 () -> Apple 과 같은 시그니처를 갖는 생성자가 있다고 가정// 원래 형태
// Supplier<Apple> c1 = () -> new Apple();
Supplier<Apple> c1 = Apple::new;
Apple apple = c1.get();
<인자 1개 - 무게>
// 원래 형태
// Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Function<Integer, Apple> c2 = Apple::new;
Apple apple c2.apply(110);
<인자 2개 - 색상, 무게>
// 원래 형태
// BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
BiFunction<Color, Integer, Apple> c2 = Apple::new;
Apple apple c3.apply(GREEN, 110);
<생성자를 전달받는 함수(map) 예시>
List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);
public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
List<Apple> result = new ArrayList<>();
for (Integer i: list) {
result.add(f.apply(i));
}
return result;
}
Function
함수를 인수로 받아 Comparable
키를 추출해서 Comparator
객체로 만듦Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
inventory.sort(Comparator.comparing(Apple::getWeight).reversed());
Comparator
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
negate
, and
, or
세 가지 메서드를 제공// 기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만듦
Predicate<Apple> notRedApple = redApple.negate();
// 두 프레디케이트를 연결해서 새로운 프레디케이트 객체를 만듦
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
andThen
compose
두 가지 디폴트 메서드를 제공andThen
compose