람다와 함수형 인터페이스

임준영·2021년 4월 23일
0

람다와 함수형 인터페이스

함수형 인터페이스는 추상 메서드가 하나만 정의된 인터페이스를 의미합니다.

함수형 인터페이스를 작성할 때는 인터페이스 이름 위에 @FunctionalInterface라고 명시해주면 메서드를 추가로 정의할 경우 Java 컴파일러가 에러를 발생시킵니다.

Java에서 제공해주는 대표적인 함수형 인터페이스인 Function, Predicate, Consumer, Supplier 인터페이스를 람다 표현식으로 구현하는 예제 코드들을 살펴보겠습니다.

1. Function 인터페이스 예제 코드

Function 인터페이스는 주로 비즈니스 로직에 대한 리턴 값이 반드시 필요할 경우 이용하면 좋습니다.
가장 대표적인 함수형 인터페이스라서 이름 자체가 함수입니다.

주의할점은 Function 인터페이스는 리턴타입이 반드시 특정 클래스여야 합니다. 물론 기본타입으로 리턴할 경우 오토 박싱이 적용되어 객체로 리턴됩니다.

import java.util.function.Function;

public class FunctionExample {

    public static int executeFunction(String context, Function<String , Integer> function) {
        return function.apply(context);
    }

    /**
     * 주로 비즈니스 로직에 대한 리턴 값이 반드시 필요할 경우 Function 인터페이스를 이용하면 좋습니다. 가장 대표적인 함수형 인터페이스라서 이름 자체가 함수임.
     * 주의할점은 Function 인터페이스는 리턴타입이 반드시 특정 클래스여야 합니다. 물론 기본타입으로 리턴할 경우 오토 박싱이 적용되어 객체로 리턴됩니다.
     */
    // Function 인터페이스 실행 예제
    public static void main(String[] args) {
        int result = FunctionExample.executeFunction("Hello! Welcome to Java World!", (context) -> context.length());
        System.out.println(result);
    }
}

2. Prediate 인터페이스 예제 코드

Predicate 함수형 인터페이스는 리턴타입이 참 / 거짓 중 하나를 선택하는 불 타입을 필요할 때 사용할 수 있는 인터페이스 입니다.
예언 혹은 예측이라는 뜻을 가지고 있어서 우리나라 개발자들이 이름으로 추론해서 사용하기 어려운 인터페이스 입니다.

import java.util.function.Predicate;

public class PredicateExample {

    public static boolean executePredicate(String name, Predicate<String> predicate) {
        return predicate.test(name);
    }

    public static void main(String[] args) {
        boolean isEmpty = PredicateExample.executePredicate("junyoung", name -> !name.isEmpty());
        System.out.println(isEmpty);
    }
}

3. Consumer 인터페이스 예제 코드

Consumer의 accept() 메서드는 전달받는 파라미터만 있고, 리턴타입은 제공하지 않는 함수형 인터페이스 입니다. 입력받는 파라미터를 소비만 한다고 해서 이름이 Consumer입니다.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class ConsumerExample {

    public static void executeConsumer(List<String> nameList, Consumer<String> consumer) {

        for (String name: nameList) {
            consumer.accept(name);
        }
    }

    public static Consumer<String> getExpression() {
        return (name) -> System.out.println(name);
    }

    public static void main(String[] args) {

        List<String> nameList = new ArrayList<>();

        nameList.add("임준영");
        nameList.add("배성탑");
        nameList.add("김진");
        nameList.add("진민완");

        executeConsumer(nameList, getExpression());
    }
}

4. Supplier 인터페이스 예제 코드

Supplier는 공급자라는 의미로 해석합니다. 파라미터는 없고 리턴 타입만 존재하는 함수형 인터페이스 입니다.

입력 없이 출력만 있어서 공급자라는 이름을 사용하였고, 메서드 명도 직관적으로 이해할 수록 get을 사용하고 있습니다.

파라미터 없이 리턴 타입만 있는 메서드는 주로 지정된 정보를 확인하거나 조회할 때 유용합니다.

import java.util.function.Supplier;

public class SupplierExample {

    public static String executeSupplier(Supplier<String> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        String version = "Java upgrade book, version 0.1 BETA";
        String result = SupplierExample.executeSupplier(() -> { return version; });
        System.out.println(result);
    }
}

5. 기본 데이터를 위한 인터페이스

Java에서의 데이터 타입은 기본형과 객체형으로 구분됩니다. 기본형 데이터를 객체형으로 변환하는 것을 박싱이라고 하고 반대로 객체형을 기본형으로 변경하는 것을 언박싱이라고 합니다. Java에서는 기본적으로 컴파일러가 자동으로 오토 박싱/언박싱을 해주지만 Java 가상머신 입장에서는 굉장히 비용이 많이 드는 작업으로, 소프트웨어의 성능에 악영향을 줍니다. 함수형 인터페이스에서도 마찬가지로 오토 박싱과 언박싱이 발생하는데 Function 인터페이스나 Supplier 인터페이스처럼 리턴 타입이 객체형인 경우 주의해야 합니다.

이러한 문제를 위해서 기본형 데이터를 이용해서 람다 표현식을 화용하고자 한다면 아래와 같은 인터페이스를 이용하는 것을 권장합니다.

IntPredicate 예제 코드

IntPredicate 함수형 인터페이스의 test() 메서드는 입력 파라미터를 int 타입, 즉 기본형 데이터로 받아서 처리합니다.

import java.util.function.IntPredicate;

public class IntPredicateExample {

    public static boolean isPositive(int i, IntPredicate intPredicate) {
        return intPredicate.test(i);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            boolean isPositive = IntPredicateExample.isPositive(i, n -> n > 0);
            System.out.println(isPositive);
        }

    }
}

BiConsumer 예제 코드

Bi가 접두사로 붙어있는 함수형 인터페이스는 입력 파라미터가 두 개라는 의미입니다. 4개의 기본 인터페이스 중 Supplier는 Bi가 붙은 인터페이스가 없습니다.

import java.util.function.BiConsumer;

public class BiConsumerExample {

    public static void executeBiConsumer(BiConsumer<String, String> biConsumer) {
        String param1 = "Hello";
        String param2 = "Junyoung";
        biConsumer.accept(param1, param2);
    }

    public static void getExpression(String param1, String param2) {
        System.out.println(param1);
        System.out.println(param2);
    }

    public static void main(String[] args) {
        String param1 = "My name is";
        String param2 = "Junyoung";

        BiConsumerExample.executeBiConsumer(BiConsumerExample::getExpression);
    }
}

6. 메서드 참조

메서드 참조는 람다 표현식의 재사용성과 가독성을 높여주는 역할을 합니다. 뿐만 아니라 람다 표현식이 아니더라도 기존에 작성한 메서드들을 표현식이나 익명 클래스에서 대체해서 사용할 수 있습니다.

클래스와 메서드를 구분하는 키워드 ::를 이용하여 참조할 내용을 정의합니다.

실제 메서드를 호출하는 것이 아니라 이름만 참조하는 것이기 때문에 메서드 뒤에 괄호와 입력파라미터는 생략합니다.

메서드 참조 역시 코드 자체를 전달하는 것이지 실행 결과를 전달하는 것은 아닙니다.

package chapter03;

import java.util.ArrayList;
import java.util.List;

public class MethodReferenceExample {

    public static MethodReferenceExample of() {
        return new MethodReferenceExample();
    }

    public static void executeMethod(String entity) {
        if (!entity.isEmpty()) {
            System.out.println("Contents: " + entity);
            System.out.println("Length: " + entity.length());
        }
    }

    // 대문자로 변경하는 코드
    public void toUpperCase(String entity) {
        System.out.println(entity.toUpperCase());
    }

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("d");
        list.add("c");

        list.stream().sorted(String::compareTo).forEach(MethodReferenceExample::executeMethod);

        // 정적 메서드 참조
        list.stream().forEach(MethodReferenceExample::executeMethod);

        // 한정적 메서드 참조
        list.stream().forEach(MethodReferenceExample.of()::toUpperCase);

        // 비한정적 메서드 참조
        list.stream().map(String::toUpperCase).forEach(System.out::println);
    }
}
list.stream().sorted(String::compareTo);

위의 코드 중 sorted 메서드에 있는 람다 표현식을 메서드 참조 형태로 변경하면 다음과 같습니다.

list.stream().sorted((String a, String b) -> a.compareTo(b));

메서드 참조에 의해 정렬하는 위의 코드는 람다 표현식에 비해 짧게 작성할 수 있는 장점이 있찌만, 코드를 이해하고 해석하는데 어려움이 있씁니다. 파라미터를 너무 많이 생략해서 3개의 데이터(입력 데이터 2개, 리턴 데이터 1개)를 하나의 메서드 참조로 다루어야 하기 때문에 쉽게 읽히지 않고 멈칫거릴 수 밖에 없습니다.

메서드 참조를 사용할 경우 아래와 같은 조건에서 이용하는 것이 좋습니다.

  • 한 라인으로 작성 가능한 람다 표현식은 람다 표현식을 그대로 사용합니다. 만일 여러 라인으로 작성해야 한다면 메서드로 분리한 후 메서드 참조를 이용합니다.
  • 비록 한 라인으로 작성할 수 있어도 다른 곳에서 재활용할 가능성이 높으면 메서드 참조를 이용합니다.
  • Integer의 parseInt와 같이 너무나 익숙하고 자주 사용하는 메서드라면 입력 파라미터와 결과 타입, 그리고 메서드 유형까지 잘 인지하고 있기 때문에 메서드 참조를 이용해서 코드를 읽는데 어려움이 없습니다. 이런 경우에도 메서드 참조를 사용합니다.

0개의 댓글