Java 함수형 인터페이스

박은빈·2023년 5월 19일
0

자바

목록 보기
24/25

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와 같은 두개의 인자를 처리하는 인터페이스가 존재한다

default method, static method

위의 인터페이스를 알아보기 전 두 메서드를 알아보자
이 메서드 역시 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>

apply

T를 입력으로 받아서 R로 내보낸다.
아주 간단한 인터페이스이다.
메서드가 apply이기때문에 apply로 실행한다

Function<String, Integer> parsing = (str) -> Integer.parseInt(str);

parsing.apply("1"); //1

위의 코드는 string을 입력받아 람다식으로 해당 string을 int로 변환하는 로직을 작성한 코드이다
위의 코드를 출력하기 위해서 parsing.apply()를 사용했다

andThen

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"이 된다

compose

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라고 할 수 있다.

accept

T에 해당하는 입력을 받고 그에 맞는 결과를 수행한다. 하지만 void이기때문에 리턴은 없다

Consumer<String> printer = (s) -> System.out.println(s);

printer.accept("hi");
//hi

andThen

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과 아주 유사하고 실제로 메서드도 get이다

Supplier<Integer> randomNumberSupplier = () -> (int) (Math.random() * 100);
int randomNumber = randomNumberSupplier.get();
// 0부터 99 사이의 랜덤한 숫자얻음

Predicate<T>

이름에서부터 알 수 있듯이 boolean을 리턴하는 인터페이스다
T를 받은다음 연산 후 true, false중 하나로 리턴을 한다

test

조건에 맞는 결과를 출력하여준다

Predicate<Integer> predicate = (n) -> n % 2 == 0;

predicate.test(4);
//true

predicate.test(3);
//false

and, or, negate

논리식에 나오는 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을 상속받았기때문에 차이 없는게 맞다

apply

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을 상속받았다.

apply

BinaryOperator<Integer> binaryOperator = (n1, n2) -> n1 * n2;

binaryOperator.apply(2,4);
//8
profile
안녕하세요

0개의 댓글