Java8부터 람다를 지원하면서 함께 등장한 함수형 인터페이스다
이전의 자바에서는 인터페이스를 정의할 때 다수의 추상 메서드를 가질 수 있고, 람다식을 사용할 수 없었다.
하지만 함수형 인터페이스가 등장하면서 단 하나의 추상 메서드를 가지면서 람다식을 사용할 수 있는 간결하면서 표현력이 높은 코드를 작성할 수 있게되었다.
예를 들어 기존 인터페이스를 선언해보자
interface Calculator {
int calculate(int a, int b);
}
@FunctionalInterface
interface FunctionCalculator {
int calculate(int a, int b);
}
위에가 일반 인터페이스, 밑에가 함수형 인터페이스다.
사실 이렇게 보면 @FunctionalInterface
를 쓴거 외에는 달라진게 없다.
맞다 사실이다. @FunctionalInterface
은 이 인터페이스가 함수형 인터페이스라는것을 알리는 용도이고, 없어도 함수형 인터페이스를 만들 수 있다.
하지만 무조건 하나만 들어가는 것이 아닌 default method
, static method
는 같이 들어갈 수 있다.
일반 메서드만 하나가 들어가야 함수형 인터페이스를 구현 가능하고 default method
, static method
는 들어가도 상관이 없다. 이미 내용이 정의되어있기때문이다
이제 위에 나온 인터페이스를 구현하면 다음과 같다
public static void main(String[] args) {
//기존 인터페이스
Calculator calculator = new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
};
int result = calculator.calculate(1, 2);
//3
//함수형 인터페이스
FunctionCalculator functionCalculator = (n1, n2) -> n1 + n2;
int functionResult = functionCalculator.calculate(1, 2);
//3
}
기존 인터페이스에 있는 메서드를 구현하려면 Override
넣고 다시 메서드를 작성해서 코드가 길고 읽기도 불편했다.
하지만 함수형 인터페이스는 람다식을 사용하여 아주 간편하고 깔끔하게 구현이 가능하다.
그리고 역시 결과는 똑같다.
이러한 큰 장점때문에 함수형 인터페이스를 쓴다고 봐도 된다
그리고 자바에서 우리를 위해서 기본적인 인터페이스를 몇개 구현해두었다.
java.util.function
에 있는 인터페이스들인데 대표적으로 다음과 같다
1. Function<T,R>
2. Consumer<T>
3. Supplier<T>
4. Predicate<T>
5. UnaryOperator<T>
6. BinaryOperator<T>
등이 있다. 이외에도 BiConsumer
, BiFunction
, BiPredicate
와 같은 두개의 인자를 처리하는 인터페이스가 존재한다
위의 인터페이스를 알아보기 전 두 메서드를 알아보자
이 메서드 역시 java8에 추가된 기능인데
기존 인터페이스에서 추상 메서드만 선언이 가능했던것과 달리 이 메서드들은 안에 로직을 넣을 수 있다
@FunctionalInterface
interface FunctionCalculator {
int calculate(int a, int b);
default int multi(int a, int b) {
return a*b;
}
static int square(int a) {
return a*a;
}
}
public class Test implements FunctionCalculator{
//default 메서드는 Override를 해도 되고 안해도 된다
@Override
public int multi(int a, int b) {
return a*a*b;
}
}
public class Test {
public static void main(String[] args) {
FunctionCalculator functionCalculator = (n1, n2) -> n1 + n2;
int functionResult = functionCalculator.calculate(1, 2);
//default, static
int defaultMethod = functionCalculator.multi(1,2); //Override를 안해도 사용 가능
int staticMethod = fufunctionCalculator.square(2); //Override를 사용할 수 없다
}
}
Function<T,R>
T를 입력으로 받아서 R로 내보낸다.
아주 간단한 인터페이스이다.
메서드가 apply이기때문에 apply로 실행한다
Function<String, Integer> parsing = (str) -> Integer.parseInt(str);
parsing.apply("1"); //1
위의 코드는 string을 입력받아 람다식으로 해당 string을 int로 변환하는 로직을 작성한 코드이다
위의 코드를 출력하기 위해서 parsing.apply()
를 사용했다
andThen이라는 디폴트 메서드도 존재한다.
해당 메서드는 다른 Function인터페이스와 연결하는 메서드로 인자에 Function인터페이스로 구현한 함수를 넣어주면 첫번째 Function의 결과를 두번째 Function의 입력으로 전달하고 해당 결과를 반환하는 Function을 생성한다.
Function<String, Integer> parsing = (str) -> Integer.parseInt(str);
Function<Integer, String> intToString = (num) -> Integer.toString(num);
Function<String, String> chainedFunction = parsing.andThen(intToString);
chainedFunction.apply("1");
아까 코드에서 intToString이라는 메서드를 새로 만들었다. 이 메서드는 int를 string으로 바꿔주는 메서드이다.
chainedFunction은 두 메서드를 연결시킨 메서드이다
chainedFunction.apply("1")
을 통해 구현하면
먼저 문자열 "1"이 parsing.apply를 통해 숫자형 1로 바뀌게 되고, 해당 결과값을 다시 입력으로 intToString.apply에서 넣게되어 다시 문자열 "1"로 나오게 된다.
결국 chainedFunction.apply("1")
의 리턴값은 "1"이 된다
andThen과 반대이다
두번째 Function의 결과가 첫번째 Function의 입력으로 전달된다
BiFunction<T,U,R>
Function이 하나의 입력과 출력이라면 BiFunction은 두 개의 입력과 출력을 나타낸다
T,U를 입력, R을 리턴으로 받는다
BiFunction<Integer,String,Integer> parsingPlus = (n,s) -> n + Integer.parseInt(s);
parsingPlus.apply(1,"10");
//11
Consumer<T>
어떠한 결과를 내보내는 Function과는 다르게 Consumer는 결과를 내보내지 않는다
즉 리턴이 없는 void라고 할 수 있다.
T에 해당하는 입력을 받고 그에 맞는 결과를 수행한다. 하지만 void이기때문에 리턴은 없다
Consumer<String> printer = (s) -> System.out.println(s);
printer.accept("hi");
//hi
Function과 마찬가지로 두 개의 Consumer를 이어주는 메서드이다
바깥메서드가 먼저 실행되고 그 결과를 안쪽 메서드에 전달을 시켜서 안쪽 메서드가 실행된다
Consumer<String> printUpperCase = (str) -> System.out.println(str.toUpperCase());
Consumer<String> printLowerCase = (str) -> System.out.println(str.toLowerCase());
Consumer<String> printBoth = printUpperCase.andThen(printLowerCase);
printBoth.accept("Hello");
//Hello hello
Supplier<T>
이 인터페이스는 입력이 존재하지 않고 출력만 존재하는 인터페이스다.
그렇기때문에 T를 넣어두면 그 T를 출력해주는 인터페이스이다
저장된 값을 꺼내오는 get과 아주 유사하고 실제로 메서드도 get이다
Supplier<Integer> randomNumberSupplier = () -> (int) (Math.random() * 100);
int randomNumber = randomNumberSupplier.get();
// 0부터 99 사이의 랜덤한 숫자얻음
Predicate<T>
이름에서부터 알 수 있듯이 boolean을 리턴하는 인터페이스다
T를 받은다음 연산 후 true, false중 하나로 리턴을 한다
조건에 맞는 결과를 출력하여준다
Predicate<Integer> predicate = (n) -> n % 2 == 0;
predicate.test(4);
//true
predicate.test(3);
//false
논리식에 나오는 and, or, not과 같은 뜻이다
test에 들어가는 하나의 객체를 가지고 두개의 연산을 실행한 후 and, or에 따라 결과를 출력한다
Predicate<Integer> predicate = (n) -> n % 2 == 0;
Predicate<Integer> bigger = (n) -> n > 2;
predicate.and(bigger).test(4);
//true
predicate.or(bigger).test(2);
//true
predicate.negate().test(4);
//false
UnaryOperator<T>
T를 받은다음 T를 리턴으로 내보낸다.
이렇게 보면 Function<T,T>
를 한 것과 무슨 차이냐고 느껴질텐데,
Function을 상속받았기때문에 차이 없는게 맞다
Function과 같은 형식이다
T에 대한 연산을 수행한 후 T를 리턴한다
UnaryOperator<Integer> unaryOperator = (n) -> n+1;
unaryOperator.apply(1);
//2
Function을 상속받았기때문에 andThen, compose역시 사용이 가능하다
BinaryOperator<T>
BinaryOperator도 UnaryOperator와 마찬가지로 모두 다 같은 T를 가지고 연산을 수행하고 리턴을 한다.
하지만 BinaryOperator는 입력을 T,T로 두 개 받는다.
이 역시 BiFunction을 상속받았다.
BinaryOperator<Integer> binaryOperator = (n1, n2) -> n1 * n2;
binaryOperator.apply(2,4);
//8