개발자로 후회없는 삶 살기

[문법] 인터페이스의 익명 객체 람다 본문

[백엔드]/[Java | 학습기록]

[문법] 인터페이스의 익명 객체 람다

몽이장쥰 2024. 5. 25. 17:05

서론

왜 람다를 사용하는지 특징과 목적을 알아보겠습니다.

 

본론

자바에서는 이렇게 코드 블록을 어딘가에 전달하는 경우가 있고 그러면 해당 코드가 어딘가에서 사용됩니다. 근데 이렇게 어딘가에 코드 블록을 전달하는 일이 쉽지 않습니다. 자바는 객체지향이기 때문에 원하는 코드가 있는 메서드를 포함하는 클래스를 생성해야 합니다. Comparator를 구현한 Length 클래스는 compare 코드 블록을 어딘가에 전달하기 위해 구현되고 생성되었습니다. 현재는 sort에 전달되어 호출됩니다.

 

- 람다 표현식 문법

앞에서 본 예제에서는 한 문자열이 다른 문자열보다 짧은지 여부를 검사하는(compare) 코드를 전달합니다. 이를 람다로 표현해보면 (파라미터, ->, 원하는 코드) 형태를 가집니다.

 

람다 표현식의 파라미터 타입을 추정할 수 있는 경우에는 타입을 생략할 수 있습니다. 여기서는 람다 표현식을 문자열 비교자에 대입하기 때문에 컴파일러는 first와 seceond가 String이라고 추정할 수 있습니다.

 

맨처음 예제에서 new Length로 생성하여 sort에 넣은 것을 comp 레퍼런스를 넣는 것으로 단순화 할 수 있고 comp 레퍼런스에 대입하는 것도 생략하고 바로 람다 표현식을 sort 메서드에 전달하는 것이 가능합니다. 람다가 인터페이스와 호환이 되어서 람다로 표현하면 바로 구현체가 되는 것입니다.

 

만약 원하는 코드가 한 줄로 표현되지 않으면 메서드를 작성할 때처럼 {}와 return을 사용하면 됩니다. 한 줄일 경우 반환이 있을 수도 없을 수도 있는 데 위에선 Integer.compare() 결과가 반환된다. 메서드에서 이름과 반환형을 생략한 게 람다 표현 방식이다.

람다 표현식이 파라미터를 받지 않으면, 파라미터 없는 빈 괄호를 파라미터로 받으면 됩니다.

파라미터를 한 개만 받으면 괄호를 생략할 수 있습니다.

 

(int x) -> {if(x >= 0) return 1; }

람다 표현식이 어떤 경우에는 값을 리턴하고 다른 경우에는 리턴하지 않게 하는 것은 규칙에 어긋납니다.

 

- 함수형 인터페이스

지금까지 보인 것처럼, 자바에는 Runnable, Comparator 등 코드 블록을 캡슐화하는 수많은 기존 인터페이스가 있습니다. 람다가 이러한 인터와 호환합니다.

Arrays.sort 메서드를 보면 이 메서드의 두 번째 파라미터는 단일 메서드를 갖춘 Comparator 함수형 인터의 인스턴스를 요구합니다.

 

여기에 단순히 람다를 전달해보면 내부적으로 sort 메서드는 Comparator를 구현하는 객체를 받고 전달받은 객체의 compare 메서드를 호출하면 람다 표현식의 몸체를 실행합니다. 람다가 함수형 인터페이스와 호환한다고 했으니 지금 Comparator 인터와 호환하여 단일 추상메서드와 연결된 것입니다.

 

@FunctionalInterface
public interface CustomInterface<T> {
    T myCall();
}

함수형 인터페이스란 1개의 추상 메서드를 갖는 인터페이스입니다. 자바 8부터 인터페이스는 기본 구현체를 포함한 디폴트 메서드를 포함할 수 있습니다. 추상 메서드가 하나라는 뜻은 default, static 메서드는 여러개 존재해도 상관 없다는 뜻입니다. @FunctionalInterface 어노테이션을 사용하는데 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 컴파일 레벨에서 검사해줍니다.

 

✅ Arrays.sort()의 인자 Comparator

위에서 Arrays.sort()의 인자로 Comparator를 대신해, 람다식을 넣어주었는데, 그렇다면 Comparator는 함수형 인터페이스인지 알아봅니다.

 

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);

    boolean equals(Object obj);
    
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        Objects.requireNonNull(other);
        return (Comparator<T> & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }

    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
        return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
    }
    
    /*그 외 default, static 메서드*/
}

Comparator의 내부를 살펴보면, compare 추상 메서드 외 모든 메서드가 default나 static으로 함수형 인터페이스의 조건을 만족합니다. equals는 내부가 obj == this로 구현된 default 메서드입니다.

 

-> 실제 사용

@Test
void lambdaTest() {
    CustomInterface<String> customInterface = () -> "Hello Custom";

    String s = customInterface.myCall();
    System.out.println(s);
}

// 결과
Hello Custom

함수형 인터페이스라서 람다식으로 표현할 수 있습니다. String 타입을 래핑했기 때문에 myCall()은 String 타입을 리턴합니다. 람다로 표현하면 인터와 호환하여 구현체를 만든 것이고 구현체가 역할 메서드를 호출한 상황이 됩니다.

 

✅ 람다와 함수형 인터페이스 강화

sort 예제와 위 예제를 보면 함수형 인터페이스를 선언하고 인스턴스를 람다로 전달하고 함수형 인터페이스의 단 1개의 추상 메서드를 호출하면 람다 표현식의 몸체를 실행합니다. myCall은 제가 직접 호출했지만 compare은 sort 내부에서 호출될 것입니다.

 

원래는 인터페이스를 클래스로 구현하고 추상 메서드를 재정의하고 넣어줘야 했는데 훨씬 효율적입니다. 람다 표현식은 객체가 아니라 함수로 생각하고, 함수형 인터페이스에 전달할 수 있다고 인식하는 것이 좋습니다. 이렇게 인터페이스로 변환되는 점이 람다 표현식을 강력하게 만들어주는 요인입니다. 문법이 짧고 단순합니다. 사실 함수형 인터페이스로 변환이 자바에서 람다 표현식을 이용해 할 수 있는 유일한 일입니다.

 

@FunctionalInterface
public interface CustomInterface<T> {
    T myCall();
}


@Test
void customLambdaTest() {
    CustomInterface<String> customInterface = () -> "Hello Custom";

    String s = customInterface.myCall(); // 직접 myCall() 호출
    System.out.println(s);
}

@Test
void javaFunctionalInterfaceTest() {
    String[] words = {"b", "c", "a"};
    Comparator<String> comparator = 
            (first, second) -> Integer.compare(first.length(), second.length());
    Arrays.sort(words, comparator); // 내부에서 comparator.compare() 호출
}

CustomInterface의 메서드를 호출하면 String 타입을 래핑했기 때문에 myCall()은 String 타입을 리턴합니다. comparator도 compare 메서드를 호출하면 내부에 구현된 코드를 수행하고 결과를 리턴할 것입니다.

 

-> java에서 제공하는 함수형 인터페이스

매번 함수형 인터페이스를 직접 만들어서 사용하는 것은 번거로운 일입니다. 자바에서 함수형 인터페이스를 제공합니다.

이처럼 우리는 제공받은 함수형 인터페이스로 웬만한 람다식을 다 만들 수 있기 때문에 개발자가 직접 함수형 인터페이스를 만드는 일은 거의 없습니다.

 

- 메서드 레퍼런스

sout::println 표현식은 람다 표현식 x -> sout(x)에 해당하는 메서드 레퍼런스입니다. setOnAction 메서드에 println 메서드만 전달할 수 있습니다.

 

대소문자를 가리지 않고 문자열을 정렬하고 싶은 경우 이렇게 할 수 있습니다. sort의 인자로 들어오는 모든 비교 기준은 int 형을 반환합니다.

 

예에서 보듯이 :: 연산자는 [클래스 또는 객체] :: [메서드 이름]을 구분하며 메서드는 메서드에 파라미터를 제공하는 람다 표현식에 해당합니다. sout::println 은 event -> s.out.println(event)에 해당합니다. math::pow는 (x, y) -> math.pow(x, y)입니다. 전달하려는 코드를 :: 로 더 짧게 전달하려는 목적입니다.

 

- 생성자 레퍼런스

Button::new가 new Button()로 보면 메서드 이름이 new라는 점을 제외하면 메서드 레퍼런스와 유사합니다. Button::new가 new Button 버튼 생성자를 가리키는 레퍼런스입니다.

 

- 디폴트 메서드

함수형 표현식을 컬랙션 라이브러리와 통합하여 더 짧고 이해하기 쉬운 코드를 만들 수 있습니다. 근데 이 루프를 보면 Collection 인터페이스가 forEach같은 새로운 메서드를 얻은 모양이다. 인터페이스는 구현체를 정의 하기 전에는 동작하지 않습니다.

 

그래서 자바 8부터 인터페이스의 디폴트 메서드(내부에 코드를 가지고 있는 메서드)를 허용함으로써 이 문제를 해결합니다.

 

이 인터는 추상 메서드 getId와 디폴트 메서드 getName을 가지고 있습니다. 당연히 getId는 구현해야한데 getName은 그대로 두거나 오버라이드 중 선택할 수 있습니다. 이제는 인터페이스에서 바로 메서드를 사용할 수 있습니다.

 

- 인터페이스 정적 메서드

자바 8부터는 인터페이스에 정적 메서드도 추가할 수 있습니다. 상당히 많은 인터페이스에 정적메서드를 추가했고 예를들어 Comparator 인터페이스는 '키 추출' 함수를 받아서 추출된 키들을 비교하는 비교자를 돌려주는 아주 유용한 정적 Comparing 메서드를 제공합니다. person 객체를 이름으로 비교하려면 Comparator.comparing(Peson::name)을 사용하면 됩니다.

앞에서 람다 표현식을 이용해서 문자열 길이를 비교했습니다.

 

public static void main(String[] args) {
    String[] words = {"b", "c", "a"};
    Arrays.sort(words, Comparator.comparing(String::length));
}

하지만 정적 comparing 메서드를 이용하면 훨씬 간단하게 수행할 수 있고 Comparator.comparing(String::length)를 사용하면 됩니다.

 

🚨 함수형 프로그래밍

위에서 계속 언급한 함수형 인터페이스를 함수형이라고 하는 이유는 인터페이스의 로직을 하나의 함수로 구현하고 있기 때문으로 객체지향 프로그래밍이 객체를 단위로 로직을 구현한다면 함수형 프로그래밍은 함수를 단위로 로직을 구현하는 프로그래밍 패러다임입니다.

 

입력 인자에만 영향을 받는 순수 함수 사용
고차 함수를 쌓아서 로직 구현
해당 언어가 일급 객체

함수형 프로그래밍은 위와 같은 특징을 가지고 있고

 

순수 함수가 이처럼 인자 외에는 함수 로직에 영향을 끼치는 것이 없는 함수입니다. 자바는 최초에 객체지향을 사용했지만, 함수형 프로그래밍도 도입한 결과 람다식이 나왔습니다.

Comments