Modern Java In Action - Chapter 3 : Lambda (2 / 2)

청주는사과아님·2024년 7월 31일
0
post-thumbnail

저번 글에서는 람다식과 함수형 인터페이스의 관계, Function Descriptor 를 알아보았다.

Modern Java In Action - Chapter 3 : Lambda (1 / 2)

이번 글에서는 메서드 참조와 람다식 조합에 대해 알아보자.


A. 람다식 VS 메서드 참조

이전까지는 함수형 인터페이스와 람다식에 집중하였다.

람다식을 이용해 함수형 인터페이스 내 추상 메서드의 행동 (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)) 처럼, 람다식이 "어느 메서드를 호출시키기만" 하는 경우가 존재한다.

이러한 경우 람다식보다 메서드 참조를 이용하는 것이 더욱 간결하고 명확한 코드를 만들어 낼 수 있다.


B. 메서드 참조란?

메서드 참조는 “이미 존재하는 메서드를 어느 람다식으로 참조시키는 행위” 라 할 수 있다. 말 그대로 람다식에 존재하는 메서드를 연결시켜, "이 메서드를 직접 사용해!" 라 말하는 것과 같다.

때문에 사실 메서드 참조 그 자체는 람다식에게 메서드를 지칭해주는 "약어" 역할일 뿐, "참조하는 메서드 그 자체" 는 아니다.

Oracle 의 Java 튜토리얼에 의하면, 메서드 참조는 다음 4 가지 종류로 나눌 수 있다. [1]

KindLambda ExpressionSyntaxExample
Reference to a static method(args) -> ClassName.staticMethod(args)ClassName::staticMethodInteger::intValue, List::of
Reference to an instance method of an arbitrary object of a particular type(arg0, rest) -> arg0.instanceMethod(rest)ClassNameOfArg0::instanceMethodString::compareToIgnoreCase, String::concat
Reference to an instance method of a particular object(args) -> obj.instanceMethd(args)obj::instanceMethodobj::hashCode, exampleObj::getValue
Reference to a constructor(args) -> new OBJ(args)ClassName::newObject::new, Thread::new

메서드 참조는 (참조 타겟)::(메서드 이름) 과 같은 형태로 표현한다.

이 때 (당연하지만) 메서드 호출이 아니라 메서드 참조이기 때문에, (메서드 이름) 뒤에 () 가 붙지 않는다.


C. 익명 클래스 → 람다식 → 메서드 참조

메서드 참조에 대한 규칙은 단순하지 않다. 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 내 존재하는 원소 s1compareTo 메서드를 호출한다.
    —> (정적 메서드, 생성자 레시피 X)

  • 또한 s1 은 람다 바깥에 존재하는 특정 객체가 아니라 List 내에 존재하는 객체이다.
    —> (현재 존재하는 객체 (particular object) 의 메서드 X)

    • (교재에는 instance method of a particular objectinstance method of of an existing object or expression 이라 칭한다)
  • 더군다나 s1, s2List 에 존재하는 임의의 객체 를 지칭하므로, 결국 해당 람다식은 "임의 유형의 인스턴스 메서드 참조" 로 바뀔 수 있다.
    —> (임의 유형의 인스턴스 메서드 O)

그래서 결국 위 코드는 다음처럼 바뀔 수 있다.

List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort(String::compareTo);

System.out.println(str);
[A, B, a, b]

D. 생성자 참조의 좋은 예시

메서드 참조는 코드의 가독성과 복잡도를 낮출 수 있는 휼륭한 도구이다. 교재에서 이에 관한 좋은 예제가 있어 첨부한다.

과일 공장

다음과 같은 과일 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)

E. 람다식 조합하기

지금까지 함수형 인터페이스와 람다식, 그리고 메서드 참조에 대해 알아보았다. 특히 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]

Reference

profile
나 같은게... 취준?!

0개의 댓글