Java8 - 람다 표현식 (2): 메서드 레퍼런스, 람다 표현식과 메서드의 조합
이 포스트에서는 Java8 API 에 추가된 인터페이스와 형식 추론과 람다 표현식과 함께 사용하면 좋은 메서드 레퍼런스에 대해 알아본다.
소스는 github 에 있습니다.
목차
1. 메서드 레퍼런스
Java8 - Java8 이란? 의 2.1. 메서드 레퍼런스: ::
에서 메서드 레퍼런스에 대해 잠시 살펴보았다.
메서드 레퍼런스는 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 ::
를 사용한다.
메서드 레퍼런스를 사용하면 람다 표현식보다 가독성이 더 좋은 경우가 있다.
람다 표현식과 메서드 레퍼런스 활용 에서 좀 더 자세한 설명이 있습니다.
아래는 Java8 - 동작 파라메터화 의 3. Comparator 로 정렬 에 나왔던 예시이다.
람다 표현식으로 정렬
inventory.sort((Apple a1, Apple a2) -> a2.getWeight().compareTo(a1.getWeight()));
위 코드를 메서드 레퍼런스와 java.util.Comparator.comparing 을 활용하면 아래와 같다.
// 메서드 레퍼런스 사용
inventory.sort(comparing(Apple::getWeight));
Apple::getWeight 는 Apple 클래스에 정의된 getWeight 의 메서드 레퍼런스이다.
Apple::getWeight 는 (Apple a) -> a.getWeight() 람다 표현식과 동일하다.
람다 | 메서드 레퍼런스 |
---|---|
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
메서드 레퍼런스는 3 가지 유형으로 구분하여 표현할 수 있다.
static 메서드 레퍼런스
- class 내부에 존재하는 static 메서드
- 람다 표현식
(args) -> ClassName::staticMethod(args)
를 메서드 레퍼런스로 표현하면ClassName:staticMethod
- 예) 람다 표현식 Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s) 를 메서드 레퍼런스로 표현하면 Function<String, Integer> stringToInteger = Integer::parseInt
instance 메서드 레퍼런스
- class 내부에 존재하는 일반 함수
- 람다 표현식 (arg0, rest) -> arg0.instanceMethod(rest) 를 메서드 레퍼런스로 표현하면 ClassName::instanceMethod
- 예) 람다 표현식
BiPredicate<List<String>, String> stringList = (list, ele) -> list.contains(ele)
를 메서드 레퍼런스로 표현하면BiPredicate<List<String>, String> stringList2 = List::contains
기존 객체의 instance 메서드 레퍼런스
- 외부 객체의 메서드 호출 시 사용
- 람다 표현식
(args) -> expr.instanceMethod(args)
를 메서드 레퍼런스로 표현하면expr::instanceMethod
- 예) 람다 표현식 () -> testTransaction.getValue() 를 메서드 레퍼런스로 표현하면 testTransaction::getValue
아래는 List 의 문자열을 대소문자 구분없이 정렬 시 람다 표현식과 메서드 레퍼런스를 사용한 예시이다.
List<String> str = Arrays.asList("a", "b", "A", "B");
// List 의 sort 메서드는 인수로 Comparator 기대함
// Comparator 는 (T, T) -> int 라는 함수 디스크립터를 가짐
// 대소문자 구별을 람다 표현식으로
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2)); // String 클래스에 정의된 compareToIgnoreCase 메서드로 람다 표현식 정의
// [a, A, b, B]
System.out.println(str);
// 대소문자 구별을 메서드 레퍼런스로
str.sort(String::compareToIgnoreCase);
// [a, A, b, B]
System.out.println(str);
1.1. 생성자 레퍼런스
생성자 레퍼런스는 static 메서드 레퍼런스
를 만드는 방법과 비슷하게 ClassName::new
로 만들 수 있다.
1.1.1. 생성자 인수가 0개인 경우
인수가 없는 생성자는 Supplier 의 함수 디스크립터(=람다 표현식의 시그니처) 와 같은 () -> Apple
의 람다 시그니처를 갖는다.
Supplier 의 함수 디스크립터는
() -> T
Supplier 함수형 인터페이스의 좀 더 자세한 내용은 Java8 - 람다 표현식 (1): 함수형 인터페이스, 형식 검사 Supplier 계열 함수형 인터페이스 와 2.4. 기본형(primitive type) 특화 를 참고하세요.
// 생성자 인수가 0개인 경우 - 생성자 레퍼런스 사용
Supplier<Apple> c1 = Apple::new; // 디폴트 생성자 Apple() 의 생성자 레퍼런스
Apple a1 = c1.get(); // Supplier 의 get() 메서드롤 호출하여 새로운 Apple 객체 생성
// 생성자 인수가 0개인 경우 - 람다 표현식 사용
Supplier<Apple> c2 = () -> new Apple(); // 디폴트 생성자 Apple() 의 람다 표현식
Apple a2 = c2.get(); // Supplier 의 get() 메서드롤 호출하여 새로운 Apple 객체 생성
1.1.2. 생성자 인수가 1개인 경우
Apple(Integer weight) 시그니처를 갖는 생성자는 Function<T,R> 의 함수 디스크립터와 같은 Integer -> Apple
의 람다 시그니처를 갖는다.
Function<T,R> 의 함수 디스크립터는
T -> R
Function 함수형 인터페이스의 좀 더 자세한 내용은 Java8 - 람다 표현식 (1): 함수형 인터페이스, 형식 검사 2.3. Function<T,R>: R apply(T t) 와 2.4. 기본형(primitive type) 특화 를 참고하세요.
// 생성자 인수가 1개인 경우 - 생성자 레퍼런스 사용
Function<Integer, Apple> c3 = Apple::new; // Apple(Integer weight) 의 생성자 레퍼런스
Apple a3 = c3.apply(10); // Function 의 apply() 메서드롤 호출하여 새로운 Apple 객체 생성
// 생성자 인수가 1개인 경우 - 람다 표현식 사용
Function<Integer, Apple> c4 = (weight) -> new Apple(weight); // Apple(Integer weight) 의 람다 표현식
Apple a4 = c4.apply(10); // Function 의 apply() 메서드롤 호출하여 새로운 Apple 객체 생성
Java8 - 람다 표현식 (1): 함수형 인터페이스, 형식 검사 의 2.3. Function<T,R>: R apply(T t) 에서 만들었던 map() 을 이용하면 다양한 무게 리스트를 만들 수 있다.
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> results = new ArrayList<>();
for (T s: list) {
// 생성자 레퍼런스를 사용하면 객체의 생성이 delay 됨 (=lazy initialize 가능)
// 실제 객체는 get() 이나 apply() 같은 메서드가 호출될 때 생성됨
// factory method pattern 에 유용히 사용
results.add(f.apply(s));
}
return results;
}
// map() 을 이용하여 다양한 무게 리스트 만들기
List<Integer> weights = Arrays.asList(1,5,9,7);
List<Apple> apples = map(weights, Apple::new); // map() 메서드로 생성자 레퍼런스 전달
생성자 레퍼런스를 사용하면 객체의 생성이 delay 되기 때문에 lazy initialize 가 가능하다. 실제 객체는 get() 이나 apply() 같은 메서드가 호출될 때 생성된다. (=factory method pattern 에 유용히 사용 가능)
Factory Method Pattern (팩토리 메서드 패턴)
객체의 생성 코드를 별도의 클래스/메서드로 분리함으로써 객체 생성의 변화에 대비하는데 유용
Factory Method 패턴의 좀 더 상세한 내용은 Java8 - 리팩토링, 디자인 패턴 의 2.5. 팩토리 패턴 (Factory Pattern) 를 참고하세요.
1.1.3. 생성자 인수가 2개인 경우
Apple(Integer weight, String color) 시그니처를 갖는 생성자는 BiFunction<T,U,R> 의 함수 디스크립터와 같은 (Integer, String) -> Apple
의 람다 시그니처를 갖는다.
BiFunction<T,U,R> 의 함수 디스크립터는
(T,U) -> R
BiFunction 함수형 인터페이스의 좀 더 자세한 내용은 Java8 - 람다 표현식 (1): 함수형 인터페이스, 형식 검사 2.4. 기본형(primitive type) 특화 를 참고하세요.
// 생성자 인수가 2개인 경우 - 생성자 레퍼런스 사용
BiFunction<Integer, String, Apple> c5 = Apple::new; // Apple(Integer weight, String color) 의 생성자 레퍼런스
Apple a5 = c5.apply(10, "red"); // BiFunction 의 apply() 메서드롤 호출하여 새로운 Apple 객체 생성
// 생성자 인수가 2개인 경우 - 람다 표현식 사용
BiFunction<Integer, String, Apple> c6 = (weight, color) -> new Apple(weight, color); // Apple(Integer weight, String color) 의 람다 표현식
Apple a6 = c6.apply(10, "red"); // BiFunction 의 apply() 메서드롤 호출하여 새로운 Apple 객체 생성
객체를 인스턴스화하지 않고도 생성자에 접근할 수 있는 생성자 레퍼런스는 다양하게 활용되는데 아래처럼 Map 으로 생성자와 문자열을 연관시킬 수 있다.
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
map.put("apple", Apple::new);
map.put("mango", Mango::new);
}
public static Fruit getFruit(String fruit, Integer weight) {
return map.get(fruit.toLowerCase()) // map 에서 Function<Integer, Fruit> 얻음
.apply(weight); // Fruit 생성
}
Fruit f1 = getFruit("apple", 2);
Fruit f2 = getFruit("mango", 5);
System.out.println(f1); // Apple{weight=2, color='null'}
System.out.println(f2); // Mango{weight=5, color='null'}
2. 람다 표현식과 메서드 레퍼런스 활용
이제 정렬에 대해 코드가 개선되는 과정을 다시 한번 되짚어 본다.
2.1. 동작 파라메터화
Java8 - 동작 파라메터화 의 3. Comparator 로 정렬 에 과정이 있습니다.
List 의 sort() 메서드 시그니처는 아래와 같다.
void sort(Comparator<? super E> c)
Comparator 객체를 인수로 받기 때문에 객체 안에 동작을 포함시키는 방식으로 다양한 정렬 전략을 전달할 수 있다. (=sort() 의 동작이 파라메터화됨)
// 1 - 동작 파라메터화
public static class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
// 1 - 동작 파라메터화
inventory.sort(new AppleComparator());
// [Apple{weight=10, color='red'}, Apple{weight=100, color='red'}, Apple{weight=150, color='green'}]
System.out.println(inventory);
2.2. 익명 클래스 사용
자주 사용하지 않는 정렬이라면 클래스를 만드는 것보다 익명 클래스를 사용하는 것이 좋다.
// 2 - 익명 클래스
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
Java8 - 동작 파라메터화 의 2.2. 익명 클래스로 개선 과 함께 보면 도움이 됩니다.
2.3. 람다 표현식 사용
코드가 복잡하므로 람다 표현식으로 간략히 표현한다.
함수형 인터페이스를 기대하는 곳이면 람다 표현식을 사용할 수 있고, 함수형 인터페이스는 오직 하나의 추상 메서드를 정의하는 인터페이스이다.
추상 메서드의 시그니처(=함수 디스크립터) 는 람다 표현식의 시그니처를 정의한다.
Comparator<T>
함수형 인터페이스의 함수 디스크립터는 (T,T) -> int
이다.
int compare(T o1, T o2);
// 3 - 람다 표현식 사용
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// [Apple{weight=10, color='red'}, Apple{weight=100, color='red'}, Apple{weight=150, color='green'}]
System.out.println(inventory);
자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 활용하여 람다의 파라메터 형식을 추론하므로 아래처럼 더 간략히 할 수 있다.
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
// [Apple{weight=10, color='red'}, Apple{weight=100, color='red'}, Apple{weight=150, color='green'}]
System.out.println(inventory);
Comparator<T>
함수형 인터페이스는 Comparable 키를 추출해서 Comparator 객체를 만드는 Function 함수를 인수로 받는 static 메서드인 comparing 을 포함한다.
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
// Comparator<T> 의 comparing() static 메서드 사용
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
inventory.sort(c);
위 코드를 좀 더 간략히 표현하면 아래와 같다.
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
Java8 - 동작 파라메터화 의 2.3. 람다 표현식으로 개선 과 함께 보면 도움이 됩니다.
2.4. 메서드 레퍼런스 사용
이제 메서드 레퍼런스를 사용하여 인수를 좀 더 깔끔히 정리해보자.
// 4 - 메서드 레퍼런스
inventory.sort(comparing(Apple::getWeight));
// [Apple{weight=10, color='red'}, Apple{weight=100, color='red'}, Apple{weight=150, color='green'}]
System.out.println(inventory);
3. 람다 표현식을 조합할 수 있는 메서드
Java8 API 의 일부 함수형 인터페이스는 람다 표현식을 조합할 수 있는 유틸리티 메서드를 제공한다. (Comparator, Function, Predicate 등)
간단한 여러 개의 람다 표현식을 조합하여 복잡한 람다 표현식을 만드는 것을 의미하는데 예를 들면 2개의 Predicate 를 조합하여 두 Predicate 의 or 연산을 수행하는 Predicate 를 만들거나 한 함수의 결과가 다른 함수의 입력이 되도록 두 함수를 조합할 수 있다.
함수형 인터페이스의 정의는 오직 하나의 추상 메서드를 지정하는데 이렇게 유틸리티 메서드 제공이 가능한 이유는 바로 디폴트 메서드로 제공하기 때문이다.
디폴트 메서드의 간단한 내용은 Java8 - Java8 이란? 의 4. 디폴트 메서드:
default
를 참고해주세요.
디폴트 메서드에 대한 자세한 내용은 Java8 - 디폴트 메서드 를 참고하세요.
3.1. Comparator 와 조합
2.3. 람다 표현식 사용 에서 본 것과 같이 static 메서드인 Comparator.comparing 을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator 를 반환할 수 있다.
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
Comparator: reversed()
만일 역정렬을 하려면 다른 Comparator 인스턴스를 만드는 것이 아니라 Comparator<T>
의 디폴트 메서드인 reversed()
를 사용하면 된다.
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
inventory.sort(comparing(Apple::getWeight).reversed());
// [Apple{weight=150, color='green'}, Apple{weight=10, color='red'}, Apple{weight=10, color='blue'}]
System.out.println(inventory);
Comparator: thenComparing()
thenComparing()
를 이용하여 첫 번째 비교자가 같을 때 두 번째 비교자에 객체를 전달하여 비교할 수 있다.
default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor)
{
return thenComparing(comparing(keyExtractor));
}
// 첫 번째 비교자가 같을 경우 두 번째 비교자로 정렬
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getColor)); // 무게가 같으면 색깔로 내림차순 정렬
// [Apple{weight=150, color='green'}, Apple{weight=10, color='blue'}, Apple{weight=10, color='red'}]
System.out.println(inventory);
3.2. Predicate 와 조합
Predicate<T>
함수형 인터페이스는 negate()
, and()
, or()
세 가지 디폴트 메서드를 제공한다.
아래 분홍색인 사과를 선택하는 Predicate 가 있다.
public static <T> Apple filterApple(Apple apple, Predicate<Apple> p) {
if (p.test(apple)) {
return apple;
}
return null;
}
BiFunction<Integer, String, Apple> biFunction = Apple::new;
Apple pinkApple = biFunction.apply(100, "pink");
Apple yellowApple = biFunction.apply(100, "yellow");
Apple blueApple = biFunction.apply(100, "blue");
Predicate<Apple> pinkPredicate = (Apple a) -> "pink".equals(a.getColor());
Apple apple1 = filterApple(pinkApple, pinkPredicate);
Apple apple2 = filterApple(yellowApple, pinkPredicate);
System.out.println(apple1); // Apple{weight=100, color='pink'}
System.out.println(apple2); // null
Predicate: negate()
만일 분홍색이 아닌 사과처럼 특정 Predicate 를 반전시킬 때 negate()
를 사용한다.
Predicate<Apple> notPinkPredicate = pinkPredicate.negate();
Apple apple3 = filterApple(pinkApple, notPinkPredicate);
Apple apple4 = filterApple(yellowApple, notPinkPredicate);
System.out.println(apple3); // null
System.out.println(apple4); // Apple{weight=100, color='yellow'}
Predicate: and()
분홍색이면서 50 이상인 사과를 선택하도록 and()
를 통해 람다를 조합할 수 있다.
Predicate<Apple> pinkAndHeavyPredicate = pinkPredicate.and(a -> a.getWeight() > 50);
Apple apple5 = filterApple(pinkApple, pinkAndHeavyPredicate);
// Apple{weight=100, color='pink'}
System.out.println(apple5); // Apple{weight=100, color='pink'}
Predicate: or()
분홍색이면서 50 이상이거나 그냥 노란색 사과를 선택하도록 or()
를 통해 람다를 조합할 수 있다.
Predicate<Apple> pinkAndHeavyOrYellowPredicate =
pinkPredicate.and(a -> a.getWeight() > 50)
.or(a -> "yellow".equals(a.getColor()));
Apple apple6 = filterApple(pinkApple, pinkAndHeavyOrYellowPredicate);
Apple apple7 = filterApple(yellowApple, pinkAndHeavyOrYellowPredicate);
Apple apple8 = filterApple(blueApple, pinkAndHeavyOrYellowPredicate);
System.out.println(apple6); // Apple{weight=100, color='pink'}
System.out.println(apple7); // Apple{weight=100, color='yellow'}
System.out.println(apple8); // null
3.3. Function 과 조합
Function<T,R>
함수형 인터페이스는 compose()
, andThen()
디폴트 메서드를 제공한다.
Function: andThen()
andThen()
은 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
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);
System.out.println(result); // 4
Function: compose()
compose()
는 인수로 주어진 함수를 먼저 실행한 후 그 결과를 외부 함수의 인수로 제공한다.
f.andThen(g) 에서 andThen 대신 compose 를 사용하면 g(f(x)) 가 아닌 f(g(x)) 가 된다.
Function<Integer, Integer> f = x -> x+1;
Function<Integer, Integer> g = x -> x*2;
Function<Integer, Integer> z = f.compose(g); // f(g(x))
int result2 = z.apply(1);
System.out.println(result2); // 3
정리하며..
- 람다 표현식은 익명 함수의 일종임, 이름은 없지만 파라메터 리스트/바디/반환 형식을 가지며 예외를 던질 수 있음
- 함수형 인터페이스는 오직 하나의 추상 메서드만을 정의하는 인터페이스
- 함수형 인터페이스를 기대하는 곳에서만 람다 표현식 사용 가능
- java.util.function 패키지는
Predicate<T>
,Function<T,R>
,Supplier<T>
,Consumer<T>
,BinaryOperator<T>
등 자주 사용하는 함수형 인터페이스를 제공함 - Java8 은
Predicate<T>
같은 제네릭 함수형 인터페이스와 관련한 박싱 동작을 피할 수 있도록 IntPredicate 등의 기본형 특화 인터페이스를 제공함 - 실행 어라운드 패턴을 람다와 활용하면 유연성과 재사용성을 추가로 얻을 수 있음
- 메서드 레퍼런스를 이용하면 기존의 메서드 구현을 재사용하며, 직접 전달 가능
Comparator<T>
,Predicate<T>
,Function<T,R>
과 같은 함수형 인터페이스는 람다 표현식을 조랍할 수 있는 다양한 디폴트 메서드를 제공
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트 저자의 Java 8 in Action을 기반으로 스터디하며 정리한 내용들입니다.
- 자바 8 인 액션
- 책 예제 소스
- 함수와 메서드의 차이점
- 함수형 인터페이스
- 자바 8 표준 API의 함수형 인터페이스 (Consumer, Supplier, Function, Operator, Predicate)