저번 글에서는 람다식과 함수형 인터페이스의 관계, Function Descriptor 를 알아보았다.
이번 글에서는 메서드 참조와 람다식 조합에 대해 알아보자.
이전까지는 함수형 인터페이스와 람다식에 집중하였다.
람다식을 이용해 함수형 인터페이스 내 추상 메서드의 행동 (behavior) 을 서술하여 더욱 짧고 간결한 코드로 대체할 수 있었다.
하지만 이보다 훨씬 더 간결하게 행동을 서술하는 방법이 존재한다. 바로 메서드 참조이다.
List<String> list = new ArrayList<>(
List.of("d", "e", "c", "g",
"a", "b", "f", "h", "i")
);
list.sort((s1, s2) -> s1.compareTo(s2)); // sort via lambda expression
list.sort(String::compareTo); // sort via method reference
System.out.println(list);
[a, b, c, d, e, f, g, h, i]
람다식은 결국 익명 메서드를 생성해, 이를 추상 메서드와 연결시킨다. 하지만 종종 위의 list.sort((s1, s2) -> s1.compareTo(s2))
처럼, 람다식이 "어느 메서드를 호출시키기만"
하는 경우가 존재한다.
이러한 경우 람다식보다 메서드 참조를 이용하는 것이 더욱 간결하고 명확한 코드를 만들어 낼 수 있다.
메서드 참조는 “이미 존재하는 메서드를 어느 람다식으로 참조시키는 행위”
라 할 수 있다. 말 그대로 람다식에 존재하는 메서드를 연결시켜, "이 메서드를 직접 사용해!"
라 말하는 것과 같다.
때문에 사실 메서드 참조 그 자체는 람다식에게 메서드를 지칭해주는 "약어"
역할일 뿐, "참조하는 메서드 그 자체"
는 아니다.
Oracle 의 Java 튜토리얼에 의하면, 메서드 참조는 다음 4 가지 종류로 나눌 수 있다. [1]
Kind | Lambda Expression | Syntax | Example |
---|---|---|---|
Reference to a static method | (args) -> ClassName.staticMethod(args) | ClassName::staticMethod | Integer::intValue , List::of |
Reference to an instance method of an arbitrary object of a particular type | (arg0, rest) -> arg0.instanceMethod(rest) | ClassNameOfArg0::instanceMethod | String::compareToIgnoreCase , String::concat |
Reference to an instance method of a particular object | (args) -> obj.instanceMethd(args) | obj::instanceMethod | obj::hashCode , exampleObj::getValue |
Reference to a constructor | (args) -> new OBJ(args) | ClassName::new | Object::new , Thread::new |
메서드 참조는 (참조 타겟)::(메서드 이름)
과 같은 형태로 표현한다.
이 때 (당연하지만) 메서드 호출이 아니라 메서드 참조이기 때문에, (메서드 이름)
뒤에 ()
가 붙지 않는다.
메서드 참조에 대한 규칙은 단순하지 않다. JLS - 15.13
[2]
에는 메서드 참조식에 대한 규칙이 적혀있는데, 이를 직접 번역하고 싶었으나 (너무 어려워서) 포기하였다.
이 때문인지 교재에서도 정확한 규칙을 설명하기 보단 차례차례 위 참조 레시피
를 이용해 익명 클래스 -> 람다식 -> 메서드 참조
순으로 바꾸는 식으로 설명한다.
아래 예시를 통해 우리도 동일한 과정을 따라가 보자.
List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
System.out.println(str);
[A, B, a, b]
위 코드의 str.sort(...)
를 람다식으로 바꿔보자. str.sort(...)
의 ...
는 Comparator<String>
이 필요하고 Comparator<T>
는 int compare(T o1, T o2)
추상 메서드가 존재하므로, 우리는 결국 (String, String) -> int
형태를 가진 람다식이 필요하다.
이를 적용하면 다음과 같다.
str.sort(
(s1, s2) -> s1.compareTo(s2)
);
이제 해당 람다식을 메서드 참조로 바꾸자.
위 람다는 List
내 존재하는 원소 s1
에 compareTo
메서드를 호출한다.
—> (정적 메서드, 생성자 레시피 X)
또한 s1
은 람다 바깥에 존재하는 특정 객체가 아니라 List
내에 존재하는 객체이다.
—> (현재 존재하는 객체 (particular object) 의 메서드 X)
instance method of a particular object
를 instance method of of an existing object or expression
이라 칭한다)더군다나 s1
, s2
는 List
에 존재하는 임의의 객체
를 지칭하므로, 결국 해당 람다식은 "임의 유형의 인스턴스 메서드 참조"
로 바뀔 수 있다.
—> (임의 유형의 인스턴스 메서드 O)
그래서 결국 위 코드는 다음처럼 바뀔 수 있다.
List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort(String::compareTo);
System.out.println(str);
[A, B, a, b]
메서드 참조는 코드의 가독성과 복잡도를 낮출 수 있는 휼륭한 도구이다. 교재에서 이에 관한 좋은 예제가 있어 첨부한다.
과일 공장다음과 같은 과일 class 들이 존재한다 하자.
abstract class Fruit {
private int weight;
public Fruit(int weight) {
this.weight = weight;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "(" + weight + ")";
}
}
class Apple extends Fruit {
public Apple(int weight) {
super(weight);
}
}
class Banana extends Fruit {
public Banana(int weight) {
super(weight);
}
}
이 때 위 과일들을 올바르게 생성해주는 "공장"
을 만들 수 있다.
class FruitFactory {
private static Map<Class, Function<Integer, Fruit>> constructors
= new HashMap<>();
static {
// Function<Integer, Fruit> : int -> Fruit
// Fruit::new : int -> Fruit
constructors.put(Apple.class, Apple::new);
constructors.put(Banana.class, Banana::new);
}
public static Object getInstance(Class clazz, int arg) {
return constructors.get(clazz)
.apply(arg);
}
}
이를 이를 이용하면 다음처럼 원하는 과일을 공장을 이용해 생성할 수 있다.
Apple a1 = (Apple) FruitFactory.getInstance(Apple.class, 100);
Apple a2 = (Apple) FruitFactory.getInstance(Apple.class, 200);
Banana b1 = (Banana) FruitFactory.getInstance(Banana.class, 300);
Banana b2 = (Banana) FruitFactory.getInstance(Banana.class, 400);
for (Fruit fruit : new Fruit[] {a1, a2, b1, b2})
System.out.println(fruit);
Apple(100)
Apple(200)
Banana(300)
Banana(400)
지금까지 함수형 인터페이스와 람다식, 그리고 메서드 참조에 대해 알아보았다. 특히 Java 8 부터 추가된 java.util.function
패키지의 인터페이스는 이들은 쉽게 이용할 수 있게 도와준다.
하지만 이에 그치지 않고 더 많은 기능을 제공하는데, 바로 람다식 조합
기능이다.
Prediate<T>
, Function<T, R>
등의 함수형 인터페이스를 보면, 다음과 같은 정적 메서드 혹은 기본 메서드가 존재함을 알 수 있다.
@FunctionalInterface
public interface Predicate<T> {
/* ... */
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
@FunctionalInterface
public interface Function<T, R> {
/* ... */
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
}
이들은 여러개의 람다식 (또는 메서드 참조) 를 조합하기 위한 메서드로, 이들을 chaining
하여 (비교적) 적은 코드로 복잡한 람다식을 구현할 수 있다.
Function<String, String> addHeader = s -> "This is HEADER\n\n" + s;
Function<String, String> addDate = s -> s + "\n" + LocalDate.now().toString();
Function<String, String> addFooter = s -> s + "\n\nThis is FOOTER";
String contextOrigin = "Hello World!\n" +
"How are you?\n" +
"I'm fine thank you.";
System.out.println(contextOrigin);
System.out.println("\n--------------\n");
String wholeContext = addHeader.andThen(addFooter)
.andThen(addDate)
.apply(contextOrigin);
System.out.println(wholeContext);
Hello World!
How are you?
I'm fine thank you.
--------------
This is HEADER
Hello World!
How are you?
I'm fine thank you.
This is FOOTER
2024-07-30
static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t : list)
if (predicate.test(t))
result.add(t);
return result;
}
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 20; i++) numbers.add(i);
Predicate<Integer> odds = num -> num % 2 != 0;
Predicate<Integer> greaterThan15 = num -> num > 15;
List<Integer> filtered1 = filter(
numbers, odds.and(greaterThan15.negate())
);
List<Integer> filtered2 = filter(
numbers, odds.negate().or(greaterThan15)
);
System.out.println(filtered1); // (less or equals then 15) and odd numbers
System.out.println(filtered2); // (greater than 15) or even numbers
[1, 3, 5, 7, 9, 11, 13, 15]
[2, 4, 6, 8, 10, 12, 14, 16, 17, 18, 19, 20]