이 글은 이것이 자바다의 내용을 참고하여 작성되었다.
학습 동기
우테코 미션 중, 특히 Stream 을 사용하면서 매개변수에 낯선 타입을 전달 받는 메소드를 많이 마주치게 되었다. 추후 알아보니 대부분이 자바 8의 표준 API 에서 제공하는 함수형 인터페이스들이었다. 공부해보니 정말 별것이 아니었다는 것을 알게되었다. 그 내용을 정리해본다.
자바 8이 제공하는 함수형 인터페이스
자바 8버전부터 빈번하게 사용되는 함수형 인터페이스를 java.util.function 표준 API 패키지로 제공한다고 한다. 제공되는 함수형 인터페이스는 크게 5가지로 Consumer, Supplier, Function, Operator, Predicate 이다. 각 인터페이스는 또 여러개의 인터페이스로 나뉜다.
두개의 매개변수를 받는 인터페이스라면 Bi 라는 접두사, 정수 타입을 매개변수로 전달받는 인터페이스라면 Int 라는 접두사가 혹은 실수 타입을 반환하는 인터페이스라면 AsDouble 과 같은 접미사가 달려있는 등 일정한 네이밍 규칙이 존재한다. 각 인터페이스의 특징과 세부 종류를 알아보도록 하자.
Consumer 계열
매개값은 있고, 반환값은 없다. 매개값을 전달받아 사용하고 아무것도 반환하지 않을 때 사용된다. 이를 소비 (Consume) 한다고 표현한다. accept 추상 메소드를 가지고 있다.
종류
| 인터페이스 이름 | 설명 | 추상 메소드 |
|---|---|---|
| Consumer |
객체 T를 받아 소비한다. | void accept(T t) |
| BiConsumer<T, U> | 객체 T와 U 두가지를 받아 소비한다. | void accept(T t, U u) |
| DoubleConsumer | double 값을 받아 소비한다. | void accept(double value) |
| IntConsumer | int 값을 받아 소비한다. | void accept(int value) |
| LongConsumer | long 값을 받아 소비한다. | void accept(long value) |
| ObjDoubleConsumer |
객체 T와 double 을 받아 소비한다. | void accept(T t, double value) |
| ObjIntConsumer |
객체 T와 int 를 받아 소비한다. | void accept(T t, int value) |
| ObjLongConsumer |
객체 T와 long 을 받아 소비한다. | void accept(T t, long value) |
용례
대표적으로 Stream 의 forEach 메소드의 매개변수 타입이 Consumer 이다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
numbers.stream().forEach(number -> System.out.println(number));
// Consumer 전달됨매개값으로 number 를 받고 람다 표현식 내부에서 사용되기만 할 뿐 아무것도 반환하지 않는 것을 확인할 수 있다.
또한 Map 의 forEach 메소드는 BiConsumer 타입을 매개변수로 받는다.
Map<String, Integer> map = Map.of("hudi", 25, "baby", 1);
map.forEach((name, number) -> System.out.println(name + "는 " + number + "살"));
// BiConsumer 전달됨위와 같이 첫번째 매개변수는 Map 의 key 를, 두번째 매개변수는 Map 의 value 를 전달받는다.
Supplier 계열
매개값은 없고, 반환값은 있다. 실행 후 호출한 곳으로 데이터를 공급 (Supply) 한다. getXXX 추상 메소드를 가지고 있다.
종류
| 인터페이스 이름 | 설명 | 추상 메소드 |
|---|---|---|
| Supplier |
T 객체를 반환한다. | T get() |
| BooleanSupplier | boolean 값을 반환한다. | boolean getAsBoolean() |
| DoubleSupplier | double 값을 반환한다. | double getAsDouble() |
| IntSupplier | int 값을 반환한다. | int getAsInt() |
| LongSupplier | long 값을 반환한다. | long getAsLong() |
용례
Stream 의 generate 는 매개변수로 Supplier 타입을 받아 해당 get 메소드의 반환값으로 무한한 Stream 을 생성한다.
Stream.generate(() -> "Infinite Stream!") // Supplier 전달됨
.limit(5)
.forEach(System.out::println);Function 계열
매개값도 있고, 리턴값도 있다. 주로 매개값을 반환값으로 매핑할 때 즉, 타입 변환이 목적일 때 사용한다. applyXXX 추상 메소드를 갖고 있다.
종류
| 인터페이스 이름 | 설명 | 추상 메소드 |
|---|---|---|
| Function<T, R> | 객체 T를 객체 R로 매핑한다. | R apply(T t) |
| BiFunction<T, U, R> | 객체 T와 U를 객체 R로 매핑한다. | R apply(T t, U u) |
| DoubleFunction |
double 값을 객체 R로 매핑한다. | R apply(double value) |
| IntFunction |
int 값을 객체 R로 매핑한다. | R apply(int value) |
| IntToDoubleFunction | int 값을 double 값으로 매핑한다. | double applyAsDouble(int value) |
| IntToLongFunction | int 값을 long 값으로 매핑한다. | long applyAsLong(int value) |
| LongToDoubleFunction | long 값을 double 값으로 매핑한다. | double applyAsDouble(long value) |
| LongToIntFunction | long 값을 int 값으로 매핑한다. | int applyAsInt(long value) |
| ToDoubleBiFunction<T, U> | 객체 T와 U를 double 값으로 매핑한다. | double applyAsDouble(T t, U u) |
| ToDoubleFunction |
객체 T를 double 값으로 매핑한다. | double applyAsDouble(T t) |
| ToIntBiFunction<T, U> | 객체 T와 U를 int 값으로 매핑한다. | int applyAsInt(T t, U u) |
| ToIntFunction |
객체 T를 int 값으로 매핑한다. | int applyAsInt(T t) |
| ToLongBiFunction<T, U> | 객체 T와 U를 long 값으로 매핑한다. | long applyAsLong(T t, U u) |
| ToLongFunction |
객체 T를 long 값으로 매핑한다. | long applyAsLong(T t) |
용례
IntStream 의 mapToObj 는 정수를 객체로 매핑하는 메소드이다. 이 메소드는 인자로 IntFunction 타입을 전달받는다.
List<Number> numbers = IntStream.rangeClosed(0, 10)
.mapToObj(number -> new Number(number)) // IntFunction 전달됨
.collect(Collectors.toList());Operator 계열
Function 과 마찬가지로, 매개값도 있고, 반환값도 있다. 주로 매개값을 연산 (Operation) 하여 결과를 반환할 때 사용된다. Function 과 마찬가지로 applyXXX 추상 메소드를 가지고 있다.
종류
| 인터페이스 이름 | 설명 | 추상 메소드 |
|---|---|---|
| BinaryOperator |
객체 T와 T를 연산 후 객체 T를 반환한다. | T apply(T t, T t) |
| UnaryOperator |
객체 T를 연산 후 T를 반환한다. | T apply(T t) |
| DoubleBinaryOperator | 두개의 double 을 연산 후 double 을 반환한다. | double applyAsDouble(double, double) |
| DoubleUnaryOperator | 한개의 double 을 연산 후 double 을 반환한다. | double applysDouble(double, double) |
| IntBinaryOperator | 두개의 int 를 연산 후 int 을 반환한다. | int applyAsInt(int, int) |
| IntUnaryOperator | 한개의 int 를 연산 후 int 을 반환한다. | int applyAsInt(int) |
| LongBinaryOperator | 두개의 long 을 연산 후 long 을 반환한다. | long applyAsLong(long, long) |
| LongUnaryOperator | 한개의 long 을 연산 후 long 을 반환한다. | long applyAsLong(long) |
용례
Stream 의 여러 오버로드된 reduce 메소드 중 하나는 매개변수로 BinaryOperator 를 전달받는다. 아래는 BinaryOperator 를 활용하여 컬렉션의 모든 수를 더하는 예시이다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
Integer sum = numbers.stream()
.reduce((acc, cur) -> acc + cur) // BinaryOperator 전달됨
.get();Predicate 계열
매개값은 있고, 반환 타입은 boolean 이다. 매개값을 받아 검사하고 true 혹은 false 를 반환할 때 사용된다. test 추상 메소드를 가지고 있다.
종류
| 인터페이스 이름 | 설명 | 추상 메소드 |
|---|---|---|
| Predicate |
객체 T를 조사 후 boolean 값을 반환한다. | boolean test(T t) |
| BiPredicate<T, U> | 객체 T와 U를 비교 조사 후 boolean 값을 반환한다. | boolean test(T t, U u) |
| DoublePredicate | double 값을 조사 후 boolean 값을 반환한다. | boolean test(double value) |
| IntPredicate | int 값을 조사 후 boolean 값을 반환한다. | boolean test(int value) |
| LongPredicate | long 값을 조사 후 boolean 값을 반환한다. | boolean test(long value) |
용례
Stream 의 allMatch 메소드는 매개변수로 Predicate 타입을 전달받아, 컬렉션의 모든 요소가 주어진 조건에 모두 일치하면 true 를 반환한다.
List<Integer> numbers = List.of(10, 20, 25, 15, 30, 35);
boolean allMatched = numbers.stream()
.allMatch(number -> number > 5);마치며
Consumer, Function, Operator 계열은 andThen 과 compose 라는 디폴트 메소드를 가지고 있다고 한다. 또한 Predicate 계열은 and, or, negate 라는 디폴트 메소드, 그리고 isEqual 이라는 정적 메소드를 가지고 있다고 한다. 이 부분은 아직 공부하지 않았으므로 추후 공부하게 되면 추가 포스팅을 작성하도록 한다.