이번 포스팅에서는 자주 쓰이는 함수형 인터페이스인
Predicate
, Consumer
, Function
, Supplier
에 대해 알아보려 합니다.
디폴트 메서드가 있더라도 추상 메서드가 하나인 인터페이스를 함수형 인터페이스라고 한다.
메서드로 전달할 수 있는 익명 함수를 단순화 한 것.
/*
* 일반적인 자바 코드
* 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());
java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며
test는 T 타입의 객체를 인수로 받아 boolean을 반환하는 추상 메서드이다.
// Predicate 인터페이스
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Predicate 인터페이스를 구현할 때, test라는 추상 메서드 하나를 구현하면 된다.
비어있지 않은 string인지 확인하는 Predicate 구현체를 만들어 보자.
Predicate<String> nonEmptyStringPredicate = new Predicate<String>() {
@Override
public boolean test(String s) {
return !s.isEmpty();
}
};
람다 이전 방식으로 구현할 때는 익명 클래스를 활용하여
Predicate 인터페이스를 구현하는 익명 클래스의 인스턴스를 생성하는 방식을 사용한다.
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
람다를 활용한 방식에서는
Predicate 인터페이스를 람다로 표현할 때, 시스템에서 Predicate 타입의 객체인 경우
반드시 있어야 한다고 판단하는 것들은 대부분 생략할 수 있다.
예를 들자면, Predicate의 인스턴스이니 new 연산자 이후의 Predicate 까지.
Predicate 타입이니 반드시 구현되어야 하는 test 메서드에 대한 생략.
다만, 파라미터나 반환 값들에 대해서는 커스텀이 이루어져야 하므로 생략할 수 없고 위와 같이 람다 표현식으로 표현할 수 있다.
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;
}
}
위의 예제 코드를 실행해보면 테스트 출력 결과가 정상임을 확인할 수 있다.
java.util.function.Consumer 인터페이스는 T 타입의 인스턴스를 인수로 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Consumer 인터페이스를 구현할 때, apply라는 추상 메서드 하나를 구현하면 된다.
List에 있는 Integer 값들을 출력하는 Consumer 구현체를 만들어보자.
Consumer<Integer> printInteger = new Consumer<Integer>() {
@Override
public void accept(Integer i) {
System.out.println(i);
}
};
람다 이전 방식으로 구현할 때는 익명 클래스를 활용하여
Consumer 인터페이스를 구현하는 익명 클래스의 인스턴스를 생성하는 방식을 사용한다.
Consumer<Integer> printInteger = (Integer i) -> System.out.println(i);
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);
}
}
}
위의 예제 코드를 실행해보면 테스트 출력 결과가 배열에 집어넣은 정수값이 순서대로 나온 것을 확인할 수 있다.
java.util.function.Function<T, R> 인터페이스는 T 타입의 인스턴스를 인수로 받아서 R 타입의 인스턴스로 반환하는 apply라는 추상 메서드를 정의한다.
즉, T 타입의 인스턴스를 받아서 R 타입의 인스턴스를 반환하는 경우.
@FunctionalInterface
public interface Function<T, R> {
R apply (T t);
}
Function<String, Integer> getLength = new Function<String, Integer>() {
@Override
public int apply(String s) {
return s.length();
}
};
람다 이전 방식으로 구현할 때는 익명 클래스를 활용하여
Function 인터페이스를 구현하는 익명 클래스의 인스턴스를 생성하는 방식을 사용한다.
Function<String, Integer> getLength = (String s) -> s.length();
람다를 활용한 방식에서는
Function 인터페이스를 람다로 표현할 때, 시스템에서 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]
java.util.function.Supplier 인터페이스는 인수를 받지 않고 T 타입의 인스턴스로 반환하는 get 추상 메서드를 정의한다.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier 인터페이스를 구현할 때, get이라는 추상 메서드 하나를 구현하면 된다.
Supplier 구현체 호출 시, Hello라는 문자열을 출력하도록 만들어보자.
Supplier<String> supplyHello = new Supplier<String>() {
@Override
public String get() {
return "Hello";
}
};
Supplier<String> supplyHello = () -> "Hello";
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");
}
}
위의 예제 코드를 실행해보면 테스트 케이스가 정상으로 나오는 것을 확인할 수 있다.
함수형 인터페이스 | 함수 디스크립터 | 기본 외 특수형태 |
---|---|---|
Predicate<T> | T → boolean | IntPredicate LongPredicate DoublePredicate |
Consumer<T> | T → void | IntConsumer LongConsumer DoubleConsumer |
Function<T, R> | T → R | IntFunction<R>, IntToDobuleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
Supplier<T> | ( ) → T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
이 외에도 함수형 인터페이스들이 있지만
Stream에서 자주 쓰이는 인터페이스인 Predicate, Consumer, Function, Supplier와 그 구현체의 선언 및 사용법에 대해 알아보았습니다.