[Java8] Functional Interface 알아보기 (+ 기본형 특화 인터페이스)
함수형 인터페이스(Functional Interface)
API 공식문서에 따르면 아래와 같이 정의할 수 있습니다.
- 오직 하나의 추상 메소드만 가져야 한다.
ㄴ 추상메소드 2개인 경우, Functional Interface X -> 컴파일 에러 발생
ㄴ 추상메소드 1 + 디폴트 메소드 1 인 경우에는, Functional Interface O
-> 디폴트 메소드는 직접 구현을 하기 때문에 추상 메소드에 포함되지 않음 - `@FunctinalInterface` 애노테이션 사용하여 함수형 인터페이스 조건에 맞는지 검사함
ㄴ애노테이션을 따로 붙이지 않아도 컴파일러가 알아서 식별함
ㄴ 가독성을 위해서 애노테이션을 붙이는 것을 권장함
예제
size라는 추상 메소드 하나와 출력을 위한 디폴트 메소드 하나를 가진 함수형 인터페이스를 만들었습니다.
그리고 가독성을 위해 `@FunctinalInterface`을 사용했습니다.
하지만 getColor라는 추상메소드를 하나 더 생성했더니 컴파일 에러가 발생했음을 확인할 수 있습니다.
함수형 인터페이스를 람다표현식으로 간결하게 처리!
Java8에서 제공하는 기능 중 하나인 `람다표현식`은 코드 여러줄을 한줄로 끝내는 파워풀한 기능입니다.
아래 코드로 확인해보겠습니다.
인터페이스를 익명클래스로 구현하는 방법은 보통 구현을 한번만 하는 경우에 별도의 클래스를 생성하지 않고 구현할 수 있습니다. 객체를 정의하고 생성함과 동시에 메소드를 구현할 수 있는 장점이 있습니다. 다만, 과도하게 남발하면 코드 가독성이 매우 떨어질 수 있습니다.
public class Apple {
public static void main(String[] args) {
Fruit apple = new Fruit() { // Fruit : interface
@Override
public void size() {
System.out.println(8);
}
};
apple.size();
}
}
위와 같은 함수형 프로그래밍은, Java8에서 도입된 람다표현식으로 코드를 간결하게 처리할 수 있습니다.
public class Apple {
public static void main(String[] args) {
Fruit apple = () -> System.out.println(8); // Lambda Expression
apple.size();
}
}
자바에서 제공하는 함수형 인터페이스에 대하여
개발자가 직접 함수형 인터페이스를 만드는 경우는 거의 없습니다. 왜냐하면 기본적으로 자바가 제공해주기 때문입니다. 그래서 자바에서 제공하는 기본적인 함수형 인터페이스에 대해 알아보고, 이를 람다표현식으로 처리해보겠습니다.
(API문서)
Main함수내에서 인터페이스를 직접 구현하는 방식으로 처리하겠습니다.
1. Function<T,R>
- T타입을 입력받아 R타입을 리턴합니다.
Function<Integer, Integer> addTwo = (i) -> i+2;
System.out.println(addTwo.apply(4));
- R apply(T t) 메소드를 람다표현식으로 +2 연산 처리
- 리턴값이 있기 때문에 print문 사용
2. Consumer<T>
- T타입을 입력받고, 리턴이 없음
Consumer<Integer> printConsumer = System.out::println;
printConsumer.accept(2);
- void accept(T t) 메소드를 람다표현식으로 print 처리
- 리턴값이 없어 print문 처리 X
3. Supplier<T>
- T타입을 결과값으로 리턴
Supplier<Integer> supplier = () -> 100;
System.out.println(supplier.get());
- T get() 메소드를 람다표현식으로 Integer타입의 100을 리턴
- 리턴값이 존재하기 때문에 print문 사용
4. Predicate<T>
- T타입을 입력받아 boolean을 리턴
Predicate<String> startWithJ = (s) -> s.startsWith("J");
System.out.println(startWithJ.test("Java8")); // True
System.out.println(startWithJ.test("Python")); // False
- boolean test(T t) 메소드를 람다표현식으로 String의 startsWith 메소드로 정의
5. UnaryOperator<T>
- Fuction<T, R>를 상속받으며, 입력변수와 결과값의 타입이 일치할때 사용 → 코드 간결
UnaryOperator<Integer> unaryOperatorAddTwo = (i) -> i + 2;
System.out.println(unaryOperatorAddTwo.apply(2));
- R apply(T t) 메소드를 람다표현식으로 +2 연산 처리
※ 첫 Function<T, R> 함수형 인터페이스와 비교해보면 이해 되실 겁니다.
만약 2개 이상의 입력변수를 받는다면? -> Bi 인터페이스
6. BiFunction<T,U,R>
- T, U 라는 두개의 입력변수를 받아 R을 리턴
BiFunction<Integer, Integer, Integer> biFunction = Integer::sum;
System.out.println(biFunction.apply(1, 2));;
7. BinaryOperator<T>
- Bifunction<T, T, T>를 상속받으며, 입력변수와 결과값의 타입이 일치할때 사용 → 코드 간결
BinaryOperator<Integer> binaryOperatorSum = Integer::sum;
System.out.println(binaryOperatorSum.apply(2,3));
[추가 학습] 기본형 특화 인터페이스 (출처)
Tip)
해당 내용은 블로그 정리하면서 다른 개발자분의 블로그를 참고했습니다.
위의 출처 참고 바라며, 왜 기본형 특화 인터페이스를 구분해놨는지에 대한 고찰이 인상깊었습니다.
지금까지 확인한 함수형 인터페이스는 `제네릭 함수형 인터페이스`입니다.
제네릭의 사전적인 의미는 한가지 타입보다는 여러 가지 타입에서 동작 가능하여 재사용성을 높인 프로그래밍 방식 중 하나 입니다.
자바의 모든 형식은 두가지로 구분됩니다.
- `기본형(Primitive)` : int, Long, double, char
- `참조형(Reference)` : Object, Integer, List
위에서 살펴본 함수형 인터페이스 중, Consumer<T>의 T 는 참조형만 사용 가능하며 기본형 타입을 입력해주면 컴파일 에러가 발생합니다.
그러나, 자바에서는 기본형과 참조형을 형변환해주는 기능을 제공합니다.
- 박싱(Boxing) : 기본형 → 참조형 (int → Integer)
- 언박싱(Boxing) : 참조형 → 기본형 (Integer → int)
추가로 자바에서 개발자를 위해 자동으로 형변환해주는 오토박싱(Autoboxing)이라는 기능도 제공합니다.
예를 들면, List<Integer> list 라는 변수를 생성하여 list.add(3)을 입력해도 정상 작동하는 이유가 바로 오토박싱 덕분입니다.
하지만 이러한 변환과정은 비용이 발생하기 때문에, 되도록이면 피하고 명확하게 기본형/참조형을 구분하여 사용하는게 바람직합니다.
그래서 함수형 인터페이스 중, 기본형 특화 인터페이스 를 따로 구분해 놓은 것 같네요!
사용하실때 해당 내용 참고 하시기 바랍니다.
- IntFunction<R>
- DoubleFunction<R>
- DoublePredicate
- LongConsumer
- 등등
그외 출처