...
함수형 인터페이스 표준 API
함수형 인터페이스(functional interface)는 추상메서드가 1개만 정의된 인터페이스를 통칭하여 일컫는다. 이 인터페이스 형태의 목적은 자바에서 람다 표현식(Lambda Expression)을 이용해 함수형 프로그래밍을 구현하기 위해서 이다.
// @FunctionalInterface 어노테이션을 인터페이스에 붙여주면,
// 두 개 이상의 메소드 선언 시 컴파일 오류를 발생시켜 개발자의 실수를 줄일 수 있다.
@FunctionalInterface
public interface Animal {
public void method();
}
그런데 곰곰히 생각해보면 함수의 형태(Signature)는 다양하다. 함수의 리턴 값이 있을수도 없을수도 있고 매개변수 갯수가 1개 혹은 2개일 수도 있다. 이러한 형태의 함수를 일일히 정의해서 사용하기엔 너무 양이 많으니, 자바 개발진들이 미리 함수형 인터페이스를 만들어 제공하는 것이 바로 이번 시간에 배울 함수형 인터페이스 표준 API 이다.
자바에서 자료구조를 컬렉션 프레임워크로 미리 만들어 제공하듯이, 자주 사용할 것 같은 람다 함수 형태를 함수형 인터페이스 표준 API로 미리 만들어 제공해준다라고 이해하면 된다.
표준 API 종류
함수적 인터페이스 표준 API는 java.util.function 패키지로 제공된다. 이 패키지 안에 있는 함수적 인터페이스 종류로는 Consumer, Supplier, Function, Operator, Predicate 가 있다. 이 인터페이스명은 각 함수의 형태와 목적에 따라 조금 재미있게 붙여진 이름인데 이에 대해서는 뒤에서 자세히 다룬다.
import java.util.function.*;
함수형 인터페이스 | 메서드 형태 | API 활용 | 매개변수 | 반환값 |
Runnable | void run() | 매개 변수를 사용 안하고 리턴을 하지 않는 함수 형태로 이용 대표적으로 쓰레드의 매개 변수로 이용 |
X | X |
Consumer<T> | void accept(T t) | 매개 변수를 사용만 하고 리턴을 하지 않는 함수 형태로 이용 | O | X |
Supplier<T> | T get() | 매개 변수를 사용 안하고 리턴만 하는 함수 형태로 이용 | X | O |
Function<T, R> | R apply(T t) | 매개값을 매핑(=타입변환)해서 리턴하기 | O | O |
Predicate<T> | boolean test(T t) | 매개값이 조건에 맞는지 단정해서 boolean 리턴 | O | O |
Operator | R applyAs(T t) | 매개값을 연산해서 결과 리턴하기 | O | O |
(T: 데이터 타입, R: 리턴 타입)
이 처럼 다양한 함수적 형태가 될수 있는 것들을 추려 모아 정리하여 API로 제공함으로써, 우리 같은 개발자들은 굳이 추상 메소드 하나 가진 인터페이스를 일일히 정의할 필요없이 API 만 가져다 쓰면 되는 것이다.
표준 API는 타입 제공 용도이다
자바의 람다식을 변수나 매개변수에 할당하기 위해서는 그에 해당하는 인터페이스 타입이 필요하다.
예를 들어 자바스크립트 같은경우 약타입 언어이기 때문에 다음과 같이 변수에 함수를 대입만하면 알아서 타입 추론이 된다.
let a = () => {};
하지만 자바는 강타입 언어이기 때문에 어떠한 정수면 정수에 맞는 타입을, 함수면 함수에 맞는 타입을 지정해 주어야 한다. 그래서 람다 함수를 담을 수 있는 대표할 타입을 인터페이스 타입으로 정한 것이라고 이해하면 된다. (정확히는 인터페이스 익명 구현 객체를 간략화 한 것이다)
@FunctionalInterface
public interface Animal {
public void run();
}
// run() 이라는 메서드를 인터페이스 타입으로 선언하여 정의
Animal a = () -> {};
그런데 함수형 인터페이스라고 하지만 사실상 인터페이스명이 딱히 정해져 있지 않기 때문에 문제가 발생한다. 그냥 추상 메소드 한개만 가지고 있으면 모든 인터페이스는 함수적 인터페이스로 취급될 수 있기 때문이다.
이것이 왜 문제냐면 메소드를 설계할때 애매해진다. 만일 파라미터로 람다 함수를 받는 어떠한 메소드를 설계한다고 하면, 이 매개변수의 인터페이스 타입명을 지정하는데 애로사항이 생긴다. 라이브러리 사용자가 인터페이스 타입명을 어찌 지을지 어떻게 알고 강타입 언어인 자바에서 설계를 하느냐인 문제이다.
따라서 자바 개발진들이 미리 함수형 인터페이스 이름들을 정해 제공하는 것이 위의 표에서 본 Runnable, Consumer, Supplier, Function, Operator, Predicate 인터페이스인 것이다.
Runnable 인터페이스
함수형 인터페이스와 람다 함수를 이해하는데 있어 가장 좋은 것이 바로 Runnable 인터페이스이다. 실제 자바 패키지에 정의 되어있는 Runnalbe 인터페이스 선언 코드를 보면 다음과 같이 되어 있다.
뭘 달린다 혹은 실행한다라는 의미인것 같은데 당최 이러한 인터페이스는 왜 만들었는지 어디에 사용되는지 애매모호하다.
앞서 말했듯이 이러한 인터페이스들을 만든 이유는 람다 함수의 타입명을 미리 지정하기 위해서 라고 했다. 즉, 매개변수를 받지도 않고 리턴값이 없고 그냥 실행만 하는 람다 함수 형태를 받는 메서드를 설계할때 람다 매개변수의 인터페이스 타입을 Runnable로 설정하면 되는 것이다. 그리고 그 메소드가 바로 우리가 잘 아는 Thread 클래스의 메서드(생성자)이다.
Thread thread = new Thread( () -> {
for(int i = 0; i < 10; i++) {
System.out.println(i);
}
} );
실제 Thread 클래스 정의문에 가서 생성자 형태를 보면 다음과 같이 파라미터로 Runnalbe 타입의 target 변수를 받아 사용한 다는 것을 볼 수 있다.
이것이 함수형 인터페이스 표준 API의 존재 이유다. Runnalbe외에도 여러가지 자바 메소드에서 미리 파라미터 타입으로 정의된 여러 종류의 함수형 인터페이스 표준 API에 정의된 타입을 받도록 설계하였기 때문에 자유롭게 람다식을 할당 할 수 있는 것이다.
함수형 인터페이스 API 사용법
이제부터 함수적 인터페이스 API의 구체적인 사용법에 대해 상세히 알아볼 시간을 가질텐데, 엄청난 양의 표준 API를 보고 절대 겁먹지 말자. 절대로 일일히 다 외우려 하지말고 String 이나 List 클래스의 메서드 처럼 필요할때마다 찾아서 사용하면 된다.
이 표준 API들은 사실상 알고보면 별거 아닌 놈들이다. 단어만 그럴싸하게 함수형 인터페이스 API 라고 하지, 그냥 람다식을 이용해 프로그래밍할때 자주 사용될것 같은 함수 모양, 형태를 미리 만들어놓고 인터페이스 이름만 아주 그럴싸하게 전문가 처럼 지어놓은 것 뿐이다.
예를들어 바로 다음에 배울 Consumer 인터페이스 API도 그냥 뭔가 대단한 자료형 마냥 보이겠지만, 그냥 매개변수만 받고 반환값이 없는 형태의 함수를 표현하기 위해 '소비' 라는 단어를 골라 넣은 것 뿐이다. 그리고 자바에는 타입이 여러개이니 각 타입들에 맞춰 Consumer 를 여러개로 구성한 것이다.
이 점을 유의해서 인터페이스 표준 API를 살펴보면 공부에 대한 거부감이나 압박감 없이 스무스하게 학습 할 수 있을 것이다.
Consumer 인터페이스
- 역할 : 매개값만 받고 처리 (리턴값 X)
- 실행 메서드 :
accept() - 소비(consume)한다는 말은 사용만 할 뿐 리턴값이 없다는 뜻으로 보면 된다.
인터페이스 형태 | 내용 |
Consumer<T> | T 형태의 인자값을 받는다 |
BiConsumer<T, U> | T, U 형태의 인자값 2개를 받는다 |
XXXConsumer | XXX 형태의 인자값을 받는다 |
ObjXXXConsumer<T> | T, XXX 형태의 인자값 2개를 받는다 |
BiConsumer의 Bi 뜻은, 라틴어에서 파생된 영어 접두사 bi- 로 "둘"을 의미한다. 따라서 매개변수를 두개 받는다로 이해하며 암기하면 된다.
Consumer 종류
인터페이스 명 | 추상 메소드 | 설명 |
Consumer<T> | void accept(T t) | 객체 T를 받아 소비 |
BiConsumer<T, U> | void accept(T t, U u) | 객체 T와 U를 받아 소비 |
DoubleConsumer | void accept(double value) | double 값을 받아 소비 |
IntConsumer | void accept(int value) | int 값을 받아 소비 |
LongConsumer | void accept(long value) | long 값을 받아 소비 |
ObjDoubleConsumer<T> | void accept(T t, double value) | 객체 T와 double 값을 받아 소비 |
ObjIntConsumer<T> | void accept(T t, int value) | 객체 T와 int 값을 받아 소비 |
ObjLongConsumer<T> | void accept(T t, long value) | 객체 T와 long 값을 받아 소비 |
public static void main(String[] args) {
// 객체 T를 받아 출력하는 함수 정의
Consumer<String> c1 = t -> System.out.println("입력값 : "+ t);
c1.accept("홍길동");
// 객체 T와 U를 받아 출력하는 함수 정의
BiConsumer<String, Integer> c2 = (a, b) -> System.out.println("입력값1 : "+ a+ ", 입력값2 : "+ b);
c2.accept("홍길동", 100);
// int 값을 받아 출력하는 함수 정의
IntConsumer c3 = a -> System.out.println("입력값 : "+ a);
c3.accept(100);
// double 값을 받아 출력하는 함수 정의
DoubleConsumer c4 = a -> System.out.println("입력값 : "+ a);
c4.accept(100.01);
// long 값을 받아 출력하는 함수 정의
LongConsumer c5 = a -> System.out.println("입력값 : "+ a);
c5.accept(2100000000);
}
만일 3개 이상의 매개변수를 받는 람다 함수를 만들고 싶다면, 직접 인터페이스를 정의해서 사용하여야 한다.
interface TripleConsumer<T, U, K> {
void accept(T t, U u, K k);
}
public class Main {
public static void main(String[] args) {
TripleConsumer<String, Double, Integer> c1 = (t, u, k) -> {
System.out.printf("%s + %f + %d", t, u, k);
};
c1.accept("안녕", 101.23, 999);
}
}
Supplier 인터페이스
- 역할 : 아무 매개값 없이 리턴값만을 반환
- 실행 메서드 :
getXXX() - 생산(supply)한다는 말은 데이터를 반환(공급) 한다는 뜻으로 보면 된다.
인터페이스 형태 | 내용 |
Supplier<T> | T형 반환 |
XXXSupplier | XXX형 반환 |
Supplier 종류
인터페이스 명 | 추상 메소드 | 설명 |
Supplier<T> | T get() | T 객체를 리턴 |
BooleanSupplier | Boolean getAsBoolean() | Boolean 값을 리턴 |
DoubleSupplier | double getAsDouble() | double 값을 리턴 |
IntSupplier | int getAsInt() | int 값을 리턴 |
LongSupplier | long getAsLong() | long 값을 리턴 |
public static void main(String[] args) {
// T 객체를 리턴하는 함수 정의
Supplier<Object> supplier = () -> new Object();
System.out.println(supplier.get());
// Boolean 값을 리턴하는 함수 정의
BooleanSupplier booleanSup = () -> true;
System.out.println(booleanSup.getAsBoolean());
// int 값을 리턴하는 함수 정의
IntSupplier intSup = () -> {
int num = (int) (Math.random() * 6) + 1;
return num;
};
System.out.println("주사위 랜덤 숫자 : " + intSup.getAsInt());
// double 값을 리턴하는 함수 정의
DoubleSupplier doubleSup = () -> 1.0;
System.out.println(doubleSup.getAsDouble());
// long 값을 리턴하는 함수 정의
LongSupplier longSup = () -> 1L;
System.out.println(longSup.getAsLong());
}
Function 인터페이스
- 역할 : 매핑(타입 변환)하기
- 실행 메서드 :
applyXXX() - 매핑 한다는 말은, 예를들어 여러 데이터 항목들이 들은 객체에서 특정 타입 값을 추출하거나 혹은 다른 타입으로 변환하는 작업에 사용한다고 보면 된다.
인터페이스 형태 | 내용 |
Function<T, R> | T 받아서 R 리턴 |
BiFunction<T, U, R> | T, U 받아서 R 리턴 |
XXXFunction<T> | XXX 받아서 T 리턴 |
XXXtoYYYFunction | XXX 받아서 YYY 리턴 |
toXXXFunction<T> | T 받아서 XXX 리턴 |
toXXXBiFunction<T, U> | T, U 받아서 XXX 리턴 |
T와 U는 매개변수 타입이고 R을 리턴(return) 타입 기호하고 보면 된다.
Function 종류
인터페이스 명 | 추상 메소드 | 설명 |
Function<T, R> | R apply(T t) | 객체 T를 객체 R로 매핑 |
BiFunction<T, U, R> | R apply(T t, U u) | 객체 T와 U를 객체 R로 매핑 |
DoubleFunction<R> | R apply(double value) | double을 객체 R로 매핑 |
IntFunction<R> | R apply(int value) | int를 객체 R로 매핑 |
IntToDoubleFunction | double applyAsDouble(int value) | int를 double로 매핑 |
IntToLongFunction | long applyAsLong(int value) | int를 long으로 매핑 |
LongToDoubleFunction | double applyAsDouble(long value) | long을 double로 매핑 |
LongToIntFunction | int applyAsInt(long value) | long을 int로 매핑 |
ToDoubleBiFunction<T, U> | double applyAsDouble(T t, U u) | 객체 T와 U를 double로 매핑 |
ToDoubleFunction<T> | double applyAsDouble(T value) | 객체 T를 double로 매핑 |
ToIntBiFunction<T, U> | int applyAsInt(T t, U u) | 객체 T와 U를 int로 매핑 |
ToIntFunction<T> | int applyAsInt(T t) | 객체 T를 int로 매핑 |
ToLongBiFunction<T, U> | long applyAsLong(T t, U u) | 객체 T와 U를 long으로 매핑 |
ToLongFunction<T> | long applyAsLong(T t) | 객체 T를 long으로 매핑 |
public static void main(String[] args) {
// int형을 String으로 타입 변환하는 함수 정의
Function<Integer, String> intToStr = t -> String.valueOf(t);
String str = intToStr.apply(100);
// String을 int형으로 타입 변환하는 함수 정의
ToIntFunction<String> strToInt = t -> Integer.parseInt(t);
int num = strToInt.applyAsInt("100");
// int형을 double형으로 타입 변환하는 함수 정의
IntToDoubleFunction intToDouble = t -> (double) t;
double d = intToDouble.applyAsDouble(100);
}
class Student {
private String name;
int korean_score;
int english_score;
int math_score;
Student(String name, int korean_score, int english_score, int math_score) {
this.name = name;
this.korean_score = korean_score;
this.english_score = english_score;
this.math_score = math_score;
}
String getName() {
return name;
}
}
public static void main(String[] args) {
List<Student> list = List.of(
new Student("홍길동", 99, 12, 45),
new Student("임꺽정", 76, 20, 8),
new Student("고담덕", 36, 50, 33),
new Student("김좌진", 77, 89, 91)
);
// 각 학생의 이름 가져오는 함수 정의 (매개타입 : Student 객체, 리턴타입 : String형)
Function<Student, String> getNameFunc = (s) -> s.getName();
// 각 학생의 교과목 평균 계산하고 가져오는 함수 정의 (매개타입 : Student 객체, 리턴타입 : int형)
ToDoubleFunction<Student> getScoreFunc = (s) -> {
int sum = s.korean_score + s.english_score + s.korean_score;
double avg = sum / 3.0;
return avg;
};
for (Student student : list) {
String name = getNameFunc.apply(student);
double avg = getScoreFunc.applyAsDouble(student);
System.out.printf("%s 평균 점수 : %f\n", name, avg);
}
}
Operator 인터페이스
- 역할 : 매개값 계산해서 동일한 타입으로 리턴하기
- 실행 메서드 :
applyXXX() - Function과 비슷하지만, 매개값을 리턴값으로 매핑(타입 변환)하는 역할보다는 매개값을 이용해서 연산을 수행한 후 동일한 타입으로 리턴값을 제공하는 역할에 초점이 가있다.
인터페이스 형태 | 내용 |
UnaryOprater<T> | T타입 연산하고 리턴 |
BinaryOperator<T> | T타입 연산하고 리턴 |
XXXUnaryOperator | XXX 타입 1개 연산 |
XXXBinaryOperator | XXX 타입 2개 연산 |
Unary는 단항(연산이 1개) 이고, Binary는 이항(연산이 2개) 이라는 뜻이다.
Operator 종류
인터페이스 명 | 추상 메소드 | 설명 |
UnaryOperator<T> | T apply(T t) Function<T, R>의 하위 인터페이스 |
T를 연산한 후 R 리턴 |
BinaryOperator<T> | T apply(T t, T u) BiFunction<T, U, R>의 하위 인터페이스 |
T와 U를 연산 후 R 리턴 |
DoubleUnaryOperator | double applyAsDouble(double) | 한 개의 double을 연산 |
DoubleBinaryOperator | double applyAsDouble(double, double) | 두 개의 double을 연산 |
IntUnaryOperator | int applyAsInt(int) | 한 개의 int를 연산 |
IntBinaryOperator | int applyAsInt(int, int) | 두 개의 int를 연산 |
LongUnarayOperator | long applyAsLong(long) | 한 개의 long을 연산 |
LongBinaryOperator | long applyAsLong(long, long) | 두 개의 long을 연산 |
class Operation {
static int calculate(int[] arr, IntBinaryOperator o) {
int result = arr[0];
for (int i = 1; i < arr.length; i++) {
result = o.applyAsInt(result, arr[i]);
}
return result;
}
}
public static void main(String[] args) {
int[] numbers = {3, 1, 7, 6, 5};
// 배열 요소의 모든 합 구하기
int sum = Operation.calculate(numbers, (x, y) -> {
return x + y;
});
System.out.println(sum);
// 배열 요소의 모든 곱 구하기
int mul = Operation.calculate(numbers, (x, y) -> {
return x * y;
});
System.out.println(mul);
// 배열 요소중 가장 큰 수 구하기
int max = Operation.calculate(numbers, (x, y) -> {
int tmp;
if(x > y)
tmp = x;
else
tmp = y;
return tmp;
});
System.out.println(max);
// 배열 요소중 가장 작은 수 구하기
int min = Operation.calculate(numbers, (x, y) -> {
int tmp;
if(x < y)
tmp = x;
else
tmp = y;
return tmp;
});
System.out.println(min);
}
Predicate 인터페이스
- 역할 : 매개값을 받고 true / false 리턴
- 실행 메서드 :
test() - 매개값을 받아 참/거짓을 단정(predicate) 한다고 생각하면 된다.
인터페이스 형태 | 내용 |
Predicate<T> | T 를 받아 boolean 리턴 |
BiPredicate<T, U> | T, U를 받아 boolean 리턴 |
XXXPredicate | XXX를 받아 boolean 리턴 |
Predicate 종류
인터페이스 명 | 추상 메소드 | 설명 |
Predicate<T> | Boolean test(T t) | 객체 T를 조사 |
BiPredicate<T, U> | Boolean test(T t, U u) | 객체 T와 U를 비교 조사 |
DoublePredicate | Boolean test(double value) | double 값을 조사 |
IntPredicate | Boolean test(int value) | int 값을 조사 |
LongPredicate | Boolean test(long value) | long 값을 조사 |
class Student {
String name;
int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
}
public static void main(String[] args) {
List<Student> list = List.of(
new Student("홍길동", 99),
new Student("임꺽정", 76),
new Student("고담덕", 36),
new Student("김좌진", 77)
);
// int형 매개값을 받아 특정 점수 이상이면 true 아니면 false 를 반환하는 함수 정의
IntPredicate scoring = (t) -> {
return t >= 60;
};
for (Student student : list) {
String name = student.name;
int score = student.score;
// 함수 실행하여 참 / 거짓 값 얻기
boolean pass = scoring.test(score);
if(pass) {
System.out.println(name + "님 " + score + "점은 국어 합격입니다.");
} else {
System.out.println(name + "님 " + score + "점은 국어 불합격입니다.");
}
}
}
함수형 인터페이스 디폴트 메서드
Function 합성
중고등학생때 수학 시간에 f(x) 함수와 g(x) 함수가 있을 때, 이 두 함수를 합성하여 f(g(x)) 라는 합성 함수를 다뤄본 기억이 어렴풋이나마 있을 것이다. f(g(x)) 는 g(x)의 결과를 다시 f(x) 함수의 인자로 넣어준 것이다.
이처럼 두 람다 함수를 연결하여 합성시킬 수 있는데, 이 합성 시키는 메서드를 자바에서 함수형 인터페이스의 디폴트 메서드로서 제공한다.
인터페이스 메서드 | 설명 |
default <V> Function <T, V> andThen (Function <? super R, ? extends V> after); | f(g(x)) 합성함수 |
default <V> Function <V, R> compose(Function <? super V, ? extends T> before); | g(f(x)) 합성함수 (andThen의 반대) |
static <T> Function<T, T> identity(); | 항등함수 (자기 자신 반환) |
합성 함수는 Function 인터페이스 뿐만 아니라 Consumer 이나 Operator 인터페이스도 존재한다.
람다 합성 사용 예시 1
간단한 수학 함수를 코드로 표현해 보았다. 숫자를 받으면 4를 빼는 함수 f(x) 와 숫자를 받으면 두배 곱해주는 함수 g(x) 를 람다표현식으로 선언하였다. 그리고 이 둘을 andThen 과 compose로 합성하여 사용하면 다음과 같다.
public static void main(String[] args) {
Function<Integer, Integer> f = num -> (num - 4); // f(x)
Function<Integer, Integer> g = num -> (num * 2); // g(x)
// f(g(x))
int a = f.andThen(g).apply(10);
System.out.println(a); // (10 - 4) * 2 = 12
// g(f(x)) - andThen을 반대로 해석하면 된다
int b = f.compose(g).apply(10);
System.out.println(b); // 10 * 2 - 4 = 16
}
andThen 과 compose 의 차이는 간단하다.
f.andThen(g) 를 수행하면 f 함수를 실행한 결과 값을 다시 g 함수의 인자로 전달하여 결과를 얻게 된다. 단, f 함수의 리턴 타입이 g 함수의 매개변수 타입과 호환되어야 한다.
f.compose(g) 를 수행하면 g 함수를 실행한 결과 값을 다시 f 함수의 인자로 전달하여 결과를 얻게 된다. 즉, andThen의 반대 버전이라고 보면 된다.
즉,x.andThen(y)는y.compose(x)와 동일하다고 보면 된다.
람다 합성 사용 예시 2
다음 Member 클래스와 Address 클래스가 있고, Member 클래스에서 Address 객체를 합성(composition) 하여 가지고 있다. 이 객체끼리 합성된 관계를 합성 함수를 통해 멤버의 도시 주소값을 불러오는 간단한 예제이다.
class Member {
private String name;
private Address address; // Address 객체를 합성(composition)
public Member(String name, Address address) {
this.name = name;
this.address = address;
}
public String getName() { return name; }
public Address getAddress() { return address; }
}
class Address {
private String country;
private String city;
public Address(String country, String city) {
this.country = country;
this.city = city;
}
public String getCountry() { return country; }
public String getCity() { return city; }
}
public static void main(String[] args) {
Member member = new Member("홍길동", new Address("조선", "한양"));
// Member 매개타입과 Address 리턴타입
Function<Member, Address> f = x -> x.getAddress(); // Address 객체를 얻기
// Address 매개타입과 String 리턴타입
Function<Address, String> g = x -> x.getCity(); // city 문자열 얻기
// f(g(x))
Function<Member, String> fg = f.andThen(g);
String city = fg.apply(member); // Address 객체를 얻고(f 실행), Address 객체에서 city 필드값을 얻기(g 실행)
System.out.println("거주 도시 : " + city);
fg = g.compose(f);
city = fg.apply(member);
System.out.println("거주 도시 : " + city);
}
Predicate 결합
Predicate 함수형 인터페이스는 참 / 거짓 값을 리턴하는 함수를 다룬다. 즉, true / false 조건식에 대하여 이들을 결합하여 and 연산, or 연산을 행한다고 보면 된다.
인터페이스 메소드 | 설명 |
default Predicate<T> and (Predicate<? super T> other) | and 연산 |
default Predicate<T> or (Predicate<? super T> other) | or 연산 |
default Predicate<T> negate() | 역 부정 |
static <T> Predicate<T> isEqual(Object targetRef) | 객체 비교 |
람다 결합 사용 예시
조건문을 구성할때 if(x > 10 && x < 20) 이런식으로 조건 연산자를 이용해 범위를 구성해본적이 있을 것이다. 이것을 람다 함수로 표현한 것이라고 보면 된다.
public static void main(String[] args) {
Predicate<Integer> greater = x -> x > 10;
Predicate<Integer> less = x -> x < 20;
// x > 10 && x < 20
Predicate<Integer> between = greater.and(less);
System.out.println(between.test(15)); // true
// x > 10 || x < 20
Predicate<Integer> all = greater.or(less);
System.out.println(all.test(5)); // true
// x <= 10
Predicate<Integer> negate = greater.negate();
System.out.println(negate.test(50)); // false
}
isEqual() 정적 인터페이스 메소드는 입력값으로 받은 객체와 같은지 판단해주는 메소드 이다. 그냥 equals() 를 람다 함수로 표현한 것이라고 보면 된다.
public static void main(String[] args) {
// 함수의 인자로 들어온 문자열이 "홍길동" 인지 판별해주는 함수
Predicate<String> checkMyName = Predicate.isEqual("홍길동");
System.out.println(checkMyName.test("임꺽정")); // false
System.out.println(checkMyName.test("홍길동")); // true
}
자바 컬렉션의 함수형 인터페이스
마지막으로 자바의 컬렉션 프레임워크에 함수형 인터페이스와 함께 사용할 수 있는 메서드도 별도로 지원한다.
인터페이스 | 메서드 | 설명 |
Collection | boolean removeIf(Predicate<E> filter); | 조건에 맞는 엘리먼트를 삭제 |
List | void replaceAll(UnaryOperator<E> operator); | 모든 엘리먼트에 operator를 적용하여 대체(replace) |
Iterable | void forEach(Consumer<T> action); | 모든 엘리먼트에 action 수행 |
인터페이스 | 메서드 | 설명 |
Map | V compute(K key, BiFunction<K, V, V> f); | 지정된 키에 해당하는 값에 f를 수행 |
Map | V computeIfAbsent(K key, Function<K, V> f); | 지정된 키가 없으면 f 수행후 추가 |
Map | V cumputeIfPresent(K key, BiFunction<K, V, V> f) | 지정된 키가 있을 때, f 수행 |
Map | V merge(K key, V value, BiFunction<V, V, V> f); | 모든 엘리먼트에 Merge 작업 수행, 키에 해당하는 값이 있으면 f 수행해서 병합후 할당 |
Map | void forEach(BiConsumer<K, V> action); | 모든 엘리먼트에 action 수행 |
Map | void replaceAll(BiFunction<K, V, V> f); | 모든 엘리먼트에 f 수행후 대체 |
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 각 요소에 10을 곱함
list.replaceAll( (x) -> x * 10 );
// 리스트는 Iteratble을 상속하니까 사용이 가능
list.forEach( (x) -> System.out.println(x) );
}
함수형 인터페이스 API 사용 정리표
보다시피 함수형 인터페이스 종류는 결코 적은게 아니라 하나하나 기억하기 애매하기도 하고, 언제 어느때 인터페이스 타입을 골라 사용할지 바로 떠올리기도 힘들다. 그래서 한눈에 들어올 수 있게 잘 정리된 도식을 가져와 보았다.
왼쪽 세로 부분은 매개변수 타입을 나타내는 것이고, 각 표의 위쪽 부분은 함수 반환 타입을 나타내는 것이다. 만일 double 형 매개변수를 받아 void 로 행하는 함수형 인터페이스가 무엇인지 찾으려면, 왼쪽에서 (double) → 을 찾고 표 상단의 → void 를 매치해보면 바로 DoubleConsumer 인터페이스를 사용한다는 것을 알 수 있게 된다.
# 참고자료
https://hbase.tistory.com/78
https://palpit.tistory.com/673
http://blog.orfjackal.net/2014/07/java-8-functional-interface-naming-guide.html
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.