Java 8 Functional Interface

망7H·2021년 5월 8일
2

이번 포스팅에서는 자주 쓰이는 함수형 인터페이스인
Predicate, Consumer, Function, Supplier에 대해 알아보려 합니다.

0. 기본 개념

1) 함수형 인터페이스

디폴트 메서드가 있더라도 추상 메서드가 하나인 인터페이스를 함수형 인터페이스라고 한다.

2) Lambda expression (람다 표현식)

메서드로 전달할 수 있는 익명 함수를 단순화 한 것.

/*
 * 일반적인 자바 코드
 * Comparator 는 인터페이스인데,
 * 객체를 생성하기 위해 Comparator를 구현한 익명 클래스를 추가해주었음.
 * Comparator에 하나 존재하는 compare 함수 오버라이딩.
 */
Comparator<Apple> byWeight = new Comparator<Apple>() {
  @Override
  public int compare(Apple a1, Apple a2) {
    return a1.getWeight().compareTo(a2.getWeight());
  }
};

// 람다 표현식을 적용한 자바 코드
Comparator<Apple> byWeight =
  (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

1. Predicate

1) Predicate Interface

java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며
test는 T 타입의 객체를 인수로 받아 boolean을 반환하는 추상 메서드이다.

// Predicate 인터페이스
@FunctionalInterface
public interface Predicate<T> {
  boolean test(T t);
}

2) Predicate 구현체 생성

Predicate 인터페이스를 구현할 때, test라는 추상 메서드 하나를 구현하면 된다.
비어있지 않은 string인지 확인하는 Predicate 구현체를 만들어 보자.

(1) Lambda 이전 방식으로 구현

Predicate<String> nonEmptyStringPredicate = new Predicate<String>() {
  @Override
  public boolean test(String s) {
    return !s.isEmpty();
  }
};

람다 이전 방식으로 구현할 때는 익명 클래스를 활용하여
Predicate 인터페이스를 구현하는 익명 클래스의 인스턴스를 생성하는 방식을 사용한다.

(2) Lambda를 사용한 구현

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

람다를 활용한 방식에서는
Predicate 인터페이스를 람다로 표현할 때, 시스템에서 Predicate 타입의 객체인 경우
반드시 있어야 한다고 판단하는 것들은 대부분 생략할 수 있다.
예를 들자면, Predicate의 인스턴스이니 new 연산자 이후의 Predicate 까지.
Predicate 타입이니 반드시 구현되어야 하는 test 메서드에 대한 생략.

다만, 파라미터나 반환 값들에 대해서는 커스텀이 이루어져야 하므로 생략할 수 없고 위와 같이 람다 표현식으로 표현할 수 있다.

3) Predicate 구현체의 사용

Springboot로 프로젝트 생성 후 /test/java/ 경로 아래에 PredicateTest.java를 생성하여 테스트 해보았다.

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

public class PredicateTest {

    // 문자열 List 생성
    private final List<String> listOfStrings =
            new ArrayList<String>(Arrays.asList("Black", "", "", "Red", "White", "Blue", ""));

    @Test
    @DisplayName("익명클래스를 사용하여 Predicate를 구현하고 정상 동작 확인")
    void predicateTest() {
        // 비어있지 않은 문자열인지 확인하는 Predicate 타입 객체
        Predicate<String> nonEmptyStringPredicate = new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return !s.isEmpty();
            }
        };

        // 비어있지 않은 문자열을 제거하는 filter 적용
        List<String> nonEmptyStringList = filter(listOfStrings, nonEmptyStringPredicate);

        System.out.println("nonEmptyStringList = " + nonEmptyStringList);

        // 공백을 포함하지 않는지 검증
        Assertions.assertThat(nonEmptyStringList).doesNotContain("");
    }

    @Test
    @DisplayName("Lambda를 사용하여 Predicate를 구현하고 정상 동작 확인")
    void predicateTestWithLambda() {
        // 비어있지 않은 문자열인지 확인하는 Predicate 타입 객체
        Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

        // 비어있지 않은 문자열을 제거하는 filter 적용
        List<String> nonEmptyStringList = filter(listOfStrings, nonEmptyStringPredicate);

        System.out.println("nonEmptyStringList = " + nonEmptyStringList);

        // 공백을 포함하지 않는지 검증
        Assertions.assertThat(nonEmptyStringList).doesNotContain("");
    }

    /**
     * 비어있지 않은 문자열만 남기는 메서드
     *
     * @param list
     * @param predicate
     * @param <T>
     * @return
     */
    public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
        List<T> result = new ArrayList<T>();

        for (T string : list) {
            if (predicate.test(string)) {
                result.add(string);
            }
        }

        return result;
    }
}

위의 예제 코드를 실행해보면 테스트 출력 결과가 정상임을 확인할 수 있다.

2. Consumer

1) Consumer Interface

java.util.function.Consumer 인터페이스는 T 타입의 인스턴스를 인수로 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.

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

2) Consumer 구현체 생성

Consumer 인터페이스를 구현할 때, apply라는 추상 메서드 하나를 구현하면 된다.
List에 있는 Integer 값들을 출력하는 Consumer 구현체를 만들어보자.

(1) Lambda 이전 방식으로 구현

Consumer<Integer> printInteger = new Consumer<Integer>() {
  @Override
  public void accept(Integer i) {
    System.out.println(i);
  }
};

람다 이전 방식으로 구현할 때는 익명 클래스를 활용하여
Consumer 인터페이스를 구현하는 익명 클래스의 인스턴스를 생성하는 방식을 사용한다.

(2) Lambda를 사용한 구현

Consumer<Integer> printInteger = (Integer i) -> System.out.println(i);

3) Consumer 구현체의 사용

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

public class ConsumerTest {

    private final List<Integer> listOfInteger =
            Arrays.asList(3, 5, 1, 7, 4, 6, 2, 8, 9);

    @Test
    @DisplayName("익명클래스를 사용하여 Consumer를 구현하고 정상 동작 확인")
    void consumerTest() {
        Consumer<Integer> printInteger = new Consumer<Integer>() {
            @Override
            public void accept(Integer i) {
                System.out.println("integer = " + i);
            }
        };

        forEach(listOfInteger, printInteger);
    }

    @Test
    @DisplayName("Lambda를 사용하여 Consumer를 구현하고 정상 동작 확인")
    void consumerTestWithLambda() {
        Consumer<Integer> printInteger = (Integer i) -> System.out.println("integer = " + i);

        forEach(listOfInteger, printInteger);
    }

    /**
     * 리스트의 각 요소를 출력처리
     *
     * @param list
     * @param c
     * @param <T>
     */
    public static <T> void forEach(List<T> list, Consumer<T> c) {
        for (T integer : list) {
            c.accept(integer);
        }
    }
}

위의 예제 코드를 실행해보면 테스트 출력 결과가 배열에 집어넣은 정수값이 순서대로 나온 것을 확인할 수 있다.

3. Function

1) Function Interface

java.util.function.Function<T, R> 인터페이스는 T 타입의 인스턴스를 인수로 받아서 R 타입의 인스턴스로 반환하는 apply라는 추상 메서드를 정의한다.
즉, T 타입의 인스턴스를 받아서 R 타입의 인스턴스를 반환하는 경우.

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

2) Function 구현체 생성

(1) Lambda 이전 방식으로 구현

Function<String, Integer> getLength = new Function<String, Integer>() {
  @Override
  public int apply(String s) {
    return s.length();
  }
};

람다 이전 방식으로 구현할 때는 익명 클래스를 활용하여
Function 인터페이스를 구현하는 익명 클래스의 인스턴스를 생성하는 방식을 사용한다.

(2) Lamdda를 사용한 구현

Function<String, Integer> getLength = (String s) -> s.length();

람다를 활용한 방식에서는
Function 인터페이스를 람다로 표현할 때, 시스템에서 Function 타입인 경우
반드시 있어야 한다고 판단하는 것들은 대부분 생략할 수 있다.

3) Function 구현체의 사용

/test/java/ 경로 아래에 FunctionTest.java를 생성하여 테스트 해보았다.

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class FunctionTest {

    private final List<String> listOfStrings =
            Arrays.asList("Black", "Blue", "", "Red", "White", "Pink", "Purple");

    @Test
    @DisplayName("익명클래스를 사용하여 Function을 구현하고 정상 동작 확인")
    void functionTest() {
        // 문자열의 길이를 반환하는 Function 타입 객체
        Function<String, Integer> getLength = new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                return s.length();
            }
        };

        List<Integer> list = map(listOfStrings, getLength);

        System.out.println("list = " + list);
    }

    @Test
    @DisplayName("Lambda를 사용하여 Function을 구현하고 정상 동작 확인")
    void functionTestWithLambda() {
        Function<String, Integer> getLength = (String s) -> s.length();

        List<Integer> list = map(listOfStrings, getLength);

        System.out.println("list = " + list);
    }

    /**
     * 리스트의 각 요소의 문자열의 길이를 계산해서 반환
     *
     * @param list
     * @param f
     * @param <T>
     * @param <R>
     * @return
     */
    public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
        List<R> result = new ArrayList<>();
        for (T string : list) {
            result.add(f.apply(string));
        }

        return result;
    }
}

위의 예제 코드를 실행해보면 출력 결과가 아래와 같이 나오는 것을 확인할 수 있다.

// 출력 결과
list = [5, 4, 0, 3, 5, 4, 6]

4. Supplier

1) Supplier Interface

java.util.function.Supplier 인터페이스는 인수를 받지 않고 T 타입의 인스턴스로 반환하는 get 추상 메서드를 정의한다.

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

2) Supplier 구현체 생성

Supplier 인터페이스를 구현할 때, get이라는 추상 메서드 하나를 구현하면 된다.
Supplier 구현체 호출 시, Hello라는 문자열을 출력하도록 만들어보자.

(1) Lambda 이전 방식으로 구현

Supplier<String> supplyHello = new Supplier<String>() {
  @Override
  public String get() {
    return "Hello";
  }
};

(2) Lambda를 사용한 구현

Supplier<String> supplyHello = () -> "Hello";

3) Supplier 구현체의 사용

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.function.Supplier;

public class SupplierTest {
    @Test
    @DisplayName("익명클래스를 사용하여 Supplier를 구현하고 정상 동작 확인")
    void supplierTest() {
        Supplier<String> supplyHello = new Supplier<String>() {
            @Override
            public String get() {
                return "Hello";
            }
        };

        Assertions.assertThat(supplyHello.get()).isEqualTo("Hello");
    }

    @Test
    @DisplayName("Lambda를 사용하여 Supplier를 구현하고 정상 동작 확인")
    void supplierTestWithLambda() {
        Supplier<String> supplyHello = () -> "Hello";

        Assertions.assertThat(supplyHello.get()).isEqualTo("Hello");
    }
}

위의 예제 코드를 실행해보면 테스트 케이스가 정상으로 나오는 것을 확인할 수 있다.

5. 정리

함수형 인터페이스함수 디스크립터기본 외 특수형태
Predicate<T>T → booleanIntPredicate
LongPredicate
DoublePredicate
Consumer<T>T → voidIntConsumer
LongConsumer
DoubleConsumer
Function<T, R>T → RIntFunction<R>, IntToDobuleFunction, IntToLongFunction,
LongFunction<R>, LongToDoubleFunction, LongToIntFunction,
DoubleFunction<R>,
ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>
Supplier<T>( ) → TBooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier


이 외에도 함수형 인터페이스들이 있지만
Stream에서 자주 쓰이는 인터페이스인 Predicate, Consumer, Function, Supplier와 그 구현체의 선언 및 사용법에 대해 알아보았습니다.

해당 글 작성에 참고한 링크

[도서] java 8 in action

profile
망한 개발자의 개발 기록입니다. 저를 타산지석으로 삼으시고 공부하세요.

0개의 댓글