들어가기 전

이번 포스팅에서는 자바에서 지원해 주는 함수형 인터페이스에 대해서 알아보겠습니다.

 

 

함수형 인터페이스란?

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

추상 메서드 외의 default, static 메서드가 존재하더라도 추상 메서드가 하나만 존재하면 함수형 인터페이스로 선언할 수 있습니다.

만약 추상 메서드가 존재하지 않거나 하나를 초과할 경우에는 함수형 인터페이스가 될 수 없습니다.

함수형 인터페이스를 생성하는 방법은 @FunctionalInterface 어노테이션을 선언하면 됩니다.

 

@FunctionalInterface
public interface FunctionalInterfaceEx {
    void call();
}

 

 

위와 같이 추상 메서드가 하나만 존재하면 정상적으로 함수형 인터페이스를 만들 수 있습니다.

하지만 아래와 같이 @FunctionalInterface를 선언한 인터페이스에 대해서 추상메서드가 존재하지 않거나 두 개 이상의 추상 메서드가 존재하면 컴파일 에러가 발생하게 됩니다.

 

 

 

 

지금까지 함수형 인터페이스가 무엇인지에 대해서 알아보았습니다.
이제 자바에서 제공하는 함수형 인터페이스가 무엇이 있는지 알아보겠습니다.

 

 

자바에서 제공하는 함수형 인터페이스 종류

  • Consumer <T>
  • Supplier <T>
  • Function <T, R>
  • Comparator <T>
  • Predicate <T>

 

Consumer <T>

Consumer는 반환타입이 void인 accept 추상 메서드 하나를 가지고 있는 함수형 인터페이스입니다.

 

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

 

 

매개변수를 받지만 반환 값이 없습니다.

Consumer 함수형 인터페이스는 아래와 같이 활용할 수 있습니다.

 

import java.util.function.Consumer;

public class ConsumerEx {

    public static void main(String[] args) {
        Consumer<String> consumer = (content) -> System.out.println(content);
        consumer.accept("함수형 인터페이스 - Consumer");
    }
}

 

 

위와 같이 Consumer를 활용할 수 있습니다.

그리고 우리는 개발을 하면서 우리도 모르게 Consumer 함수형 인터페이스를 많이 활용하고 있습니다.

아래와 같이 forEach문을 한 번이라도 활용해신분은 Consumer 함수형 인터페이스를 활용한 것입니다.

 

import java.util.List;

public class ConsumerEx {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4);
        numbers.forEach(number -> System.out.println(number));
    }
}

 

 

forEach 내부를 들어가면 아래와 같이 매개변수로 Consumer를 받는 것을 확인할 수 있습니다.

 

 

 

Supplier <T>

 

Supplier는 Consumer와 달리 매개변수를 받지 않지만 반환 타입을 가진 get 추상 메서드 하나를 가지고 있는 함수형 인터페이스입니다.

 

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

 

 

Supplier는 아래와 같이 활용할 수 있습니다.

 

import java.util.function.Supplier;

public class SupplierEx {

    public static void main(String[] args) {
        Supplier<String> supplier = () -> {
            int randomNum = (int) (Math.random() + 1);
            if(randomNum % 2 == 0) {
                return "짝수";
            }
            return "홀수";
        };
        String result = supplier.get();
        System.out.println("result = " + result);
    }
}

 

import java.util.function.Supplier;

public class SupplierEx {

    public static void main(String[] args) {
        String result1 = useMethodSupplierArg(() -> "메서드 매개변수를 활용하여 처리 - 1");
        String result2 = useMethodSupplierArg(() -> "메서드 매개변수를 활용하여 처리 - 2");
    }

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

 

 

위와 같이 메서드 매개변수로 Supplier를 활용하면 여러 방면에서 재사용할 수 있게 할 수 있습니다. 

Consumer처럼 Supplier는 개발을 하면서 많이 활용되고 있습니다.

아래와 같이 Optional을 활용하면서 값이 없을 경우 orElseGet을 통해서 결괏값을 추출할 수 있습니다.

 

public class SupplierEx {

    public static void main(String[] args) {
        String result = Optional.of("Supplier 함수형 인터페이스").orElseGet(() -> "기본값");
    }
}

 

 

Optional의 orElseGet 메서드를 살펴보면 아래 이미지처럼 Suppler을 매개변수로 받는 것을 확인할 수 있습니다.

 

 

 

Function <T, R>

Function 함수형 인터페이스는 타입 변수 T를 매개변수로 받고 R 타입으로 반환하는 apply 추상 메서드가 있습니다.

 

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

 

 

Function은 아래와 같이 활용할 수 있습니다.

 

import java.util.function.Function;

public class FunctionEx {


    public static <T,R> R convert(Function<T,R> function, T t) {
        return function.apply(t);
    }

    public static void main(String[] args) {
        Function<String, Integer> stringToInteger = (str) -> {
            try{
                return Integer.parseInt(str);
            }catch (NumberFormatException e) {
                throw new IllegalStateException("숫자로 변환할 수 없는 문자열입니다.");
            }
        };
        Integer apply = stringToInteger.apply("51");
        System.out.println("apply = " + apply);

        String convert = convert((str) -> {
            return "Function Interface Ex".concat(str);
        }, "- concat");
        System.out.println("convert = " + convert);

        Integer stringToIntegerConvert = convert((str) -> {
            try {
                return Integer.parseInt(str);
            } catch (NumberFormatException e) {
                throw new IllegalStateException("숫자로 변환할 수 없는 문자열입니다.");
            }
        }, "-51");
        System.out.println("convert = " + stringToIntegerConvert);
    }
}

 

위와 같이 Function을 활용할 수 있습니다.

백엔드 개발자라면 적어도 한 번은 아래 코드를 본 적 있거나 작성해 보셨을 거라고 생각합니다.

 

public class FunctionEx {
    public static void main(String[] args) {
        List<String> strings = List.of("1", "2", "3", "4", "5");
        List<Integer> integers = strings.stream()
                                         .map(str -> Integer.parseInt(str))
                                         .toList();
    }
}

 

위 코드는 String 클래스 타입을 Integer로 형변환 하는 코드입니다.

바로 위에서 설명한 Function과 똑같이 동작합니다.

이렇게 동작할 수 있는 것은 stream의 중간 연산인 "map"이 매개변수로 Function을 받기 때문입니다.

 

 

 

Comparator <T>

Comparator 함수형 인터페이스는 타입을 T로 정의하고 두 개의 T 타입을 매개 변수로 받고 두 개의 매개변수 크기를 비교하는 compare 추상 메서드가 있습니다.

 

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

 

Comparator의 compare 메서드는 첫 번째 인자가 두 번째 인자보다 작을 경우 음수를 반환하고 클 경우에는 양수를 반환합니다.

두 개의 인자의 값이 동일하면 0을 반환합니다.

 

아래와 같이 활용할 수 있습니다.

import java.util.Comparator;

public class CompareEx {

    public static void main(String[] args) {
        Comparator<Integer> cm = (num1, num2) -> num1.compareTo(num2);
        int negativeInteger = cm.compare(1, 2);
        int zero = cm.compare(2, 2);
        int positiveInteger = cm.compare(2, 1);
        System.out.println("negativeInteger = " + negativeInteger);
        System.out.println("zero = " + zero);
        System.out.println("positiveInteger = " + positiveInteger);
    }
}

 

 

Predicate <T>

Predicate 함수형 인터페이스는 T 타입 변수를 매개변수로 받아서 boolean 타입으로 반환하는 test 추상 메서드가 있습니다.

 

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

 

 

Predicate는 아래와 같이 활용할 수 있습니다.

 

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateEx {

    public static List<Integer> getResult(Predicate<Integer> condition) {
        List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
        return numbers.stream()
            .filter(condition)
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        Predicate<Integer> predicate = (num) -> num > 0;
        boolean isGrater = predicate.test(1);
        List<Integer> result = getResult((num) -> num % 2 == 0);
        for (Integer i : result) {
            System.out.println("i = " + i);
        }
    }
}

 

 

위 코드를 보면 stream의 중간 연산자인 filter에 Predicate의 값을 이용하는 것을 확인할 수 있습니다.

filter는 아래와 같이 매개변수로 Predicate를 받고 있습니다.