💡 이 장의 주요 내용
- 람다란 무엇인가?
- 어디에, 어떻게 람다를 사용하는가?
- 실행 어라운드 패턴
- 함수형 인터페이스, 형식 추론
- 메서드 참조
- 람다 만들기
Comparator<Apple> beWeight = 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());
다음은 Java 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())
List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));
그래서 정확히 어디에서 람다를 사용할 수 있다는 건가?
→ 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.
Predicate<T>
를 기대하는 filter()
의 인수로 람다 표현식을 전달했다.Comparator
, Runnable
등이 있다.public interface Predicate<T> {
boolean test(T t);
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
Run()
을 정의하는 함수형 인터페이스이므로 가능한 코드들 /* 람다 사용 */
Runnable r1 = () -> System.out.println("Hello");
/* 익명 클래스 사용 */
Runnable r2 = new Runnable() {
public void run() {
System.out.println("Hello");
}
};
/* 직접 전달된 람다 표현식을 사용 */
public static void process(Runnable r) {
r.run();
}
process(r1); process(r2);
process(() -> System.out.println("Hello"));
run()
를 인수와 반환값이 없는 시그니처로 생각할 수 있다.왜 함수형 인터페이스를 인수로 받는 메서드에서만 람다 표현식을 사용할 수 있을까?
🌱
@FunctionalInterface
는 무엇인가?
- 함수형 인터페이스임을 가리키는 어노테이션이다.
try-with-resource
구문 활용public String processFile() throws IOException {
try (
BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
}
processFile()
메서드만 다른 동작을 수행하도록 해보자.processFile()
의 동작을 파라미터화하기!!BufferedReader
를 인수로 받아, String을 반환하는 람다String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
BufferedReader
→ String, IOException을 던질 수 있는 시그니처와 일치하는 인터페이스 만들기@FunctionalInteface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
//정의한 인터페이스를 processFile() 메서드의 인수로 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {}
BufferedReaderProcessor
에 정의된 process()
의 시그니처와 일치하는 람다를 전달하자.public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
람다를 이용해 다양한 동작을 processFile()
메서드로 전달할 수 있다.
String oneLine = processFile((BufferedReader br) -> br.readLine());
String oneLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
java.util.function
패키지로 여러 가지 새로운 인터페이스들을 제공한다.Predicate
, Consumer
, Function
인터페이스를 살펴보자.test()
라는 추상 메서드를 정의한다.boolean
타입을 반환한다.Predicate
예제@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
pubilc <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);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
accept()
라는 추상 메서드를 정의한다.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)
);
apply()
라는 추상 메서드를 가진다.//String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 변환하는 map()
@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;
}
//[7, 2, 6]
List<Integer> list = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length()
);
List<Integer> list = new ArrayList<>();
for (int i = 300; i < 400; i++) {
list.add(i);
}
DoublePredicate
, IntConsumer
, LongBinaryOperator
등등public interface IntPredicate {
boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); //참 : 박싱 없음
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000); //거짓 : 박싱
try-catch
블록으로 감싸야 한다.Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
} catch {
throw new RuntimeException(e);
}
};
List<Apple> heabierThan150g =
filter(inventory, (Apple apple) -> apple.getWeight() > 150);
filter()
메서드의 선언을 확인한다.filter()
는 두 번째 파라미터로 Predicate<Apple>
형식( = 대상 형식)을 기대한다.Predicate<Apple>
은 test()
라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.test()
는 Apple
을 받아 boolean
을 반환하는 함수 디스크립터를 묘사한다.filter()
로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
자바의 변화에 익숙한 독자라면 이미 자바 7에서도 다이아몬드 연산다(<>
) 로 콘텍스트에 따른 제네릭 형식을 추론할 수 있다는 사실을 기억할 것이다.
List<String> listOfStrings = new ArrayList<>();
List<Integer> listOfIntegers = new ArrayList<>();
람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립트와 호환된다.
→ 물론 파라미터 리스트도 호환되어야 한다.
예를 들어 예제에서 List의 add()
는 Consumer 콘텍스트(T → void)가 기대하는 void 대신 boolean을 반환하지만 유효한 코드다.
//Predicate는 불리언 반환값을 갖는다.
Predicate<String> p = s -> list.add(s);
//Consumer는 void 반환값을 갖는다.
Consumer<String> b = s -> list.add(s);
Comparator
객체를 만드는 코드/* 형식을 추론하지 않음 (= 형식을 명시적으로 포함) */
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
/* 형식을 추론 */
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getweight());
🌱 상황에 따라 명시적으로 형식을 포함한 것이 좋을때도 있고, 배제한 것이 가독성을 향상시킬 때도 있다.
어떤 방법이 좋은지 정해진 규칙은 없으니, 개발자는 가독성을 향상시키는 방법을 고민해보자!
int portNumber = 8080; //자유 변수
Runnable r = () -> System.out.println(portNumber);
final
로 선언되거나, 실질적으로 그래야 한다.final
지역 변수 this
를 캡처하는 것과 마찬가지다.int portNumber = 8080; //자유 변수
Runnable r = () -> System.out.println(portNumber);
portNumber = 3000; //에러 : 람다에서 참고하는 지역 변수는 final로 선언되거나 취급되어야 한다.
🌱 클로저(Closure)
클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다.
예를 들어, 클로저를 다른 함수의 인수로 전달할 수 있다.
클로저는 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있다.자바 8의 람다와 익명 클래스는 클로저와 비슷한 동작을 수행한다.
람다와 익명 클래스 모두 메서드의 인수로 전달될 수 있고, 자신의 외부 영역의 변수에 접근할 수 있다.단, 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없다.
→ 람다가 정의된 메서드의 지역 변숫값은final
변수여야 하기 떄문!
→ 덕분에 람다는 변수가 아닌 값에 국한되어 어떤 동작을 수행한다는 사실이 명확해진다.이전에도 설명했듯 지역 변수값은 스택에 존재하므로, 자신을 정의한 스레드와 생존을 같이 해야 하며, 따라서 지역 변수는
final
이어야 한다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
java.util.Comparator.comparing
을 활용한 코드inventory.sort(comparing(Apple::getWeight));
::
)를 붙이는 방식으로 메서드 참조를 활용할 수 있다.Integer::parseInt
String::length
expensiveTransaction::getValue
비공개 헬퍼 메서드 예시
private boolean isValidName(String string) {
return Character.isUpperCase(string.charAt(0));
}
이를 Predicate를 필요로 하는 적당한 상황에서 메서드 참조를 사용할 수 있다.
fileter(words, this::isVaildName)
sort()
는 인수로 Comparator를 기대한다.compareToIgnoreCase()
메서드로 정의하기List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort(String::compareToIgnoreCase);
💡 컴파일러는 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다.
ClassName::new
처럼 클래스명과 new
키워드를 이용해 기본 생성자의 참조를 만들 수 있다.Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);
map()
메서드를 이용해 Apple 생성자로 전달하는 코드이다.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 ArrayLst<>();
for (Integer i : list) {
result.add(f.apply(i));
}
return result;
}
Apple(String color, Integer weight)
처럼 두 인수를 갖는 생성자는 BiFunction 인터페이스와 같은 시그니처를 가지므로 아래와 같이 작성할 수 있다.BiFunction<Color, Integer, Apple> c3 = Apple::new;
//Apple(String color, Integer weight)을 참조
Apple a3 = c3.apply(GREEN, 110);
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
Apple a3 = c3.apply(GREEN, 110);
static Map<String, Function<Integer, Fruit>> map = new HashMap();
static {
map.put("apple", Apple::new);
map.put("orange", Orange::new);
// 등등
}
public static Fruit giveMeFruit(String fruit, Integer weight) {
return map.get(fruit.toLowerCase()) //map의 Fuction<Integer, Fruit>를 얻음
.apply(weight);
}
💡 메소드 참조와 생성자 참조 이해 유무 확인하기!
Color(int, int, int)
처럼 인수가 3개인 생성자의 생성자 참조를 이해하려면 어떻게 해야할까?→ 힌트 : 생성자 참조와 일치하는 시그니처를 갖는 함수형 인터페이스가 필요!
public interface TriFunction<T, U, V, R> { R apply(T t, U u, V v); }
→ 답안
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
sort()
메서드를 제공하므로 정렬을 구현할 필요는 없다.sort()
의 시그니처는 아래와 같다void sort(Comparator<? super E> c)
sort()
의 동작을 파라미터화하다!public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
(T, T) → int
이다.inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
comparing()
을 포함한다.//람다 표현식은 사과를 비교하는 데 사용할 키를 어떻게 추출할 것인지 지정하는 한 개의 인수만 포함
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
inventory.sort(comparing(Apple::getWeight));
Java 8 API의 몇몇 함수형 인터페이스들은 다양한 유틸리티 메서드를 포함한다.
예를 들어, 두 프리디케이트를 조합해서 두 프리디케이트의 OR
연산을 수행하는 프리디케이트를 만들 수 있다.
여기서 등장하는 것이 디폴트 메소드이다.
→ 추상 메서드가 아니므로 함수형 인터페이스의 정의를 벗어나지 않는다.
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
reverse()
디폴트 메소드를 이용해보자.inventory.sort(comparing(Apple::getWeight).reversed();
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry)); //원산지 국가별로 정렬
negate()
, and
, or
negate()
Predicate<Apple> notRedApple = redApple.negate();
and()
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
or()
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));
andThen()
, compose()
디폴트 메소드를 제공한다.andThen()
: 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1); //4
compose()
: 인수로 주어진 함수를 먼저 실행한 다음 그 결과를 외부 함수의 인수로 제공Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); //3