lambda에 관하여 블로그 글을 작성하려고 보니 functional interface도 긴밀하게 엮여있어
하나의 글로 작성을 하기엔 너무 길고 가독성도 떨어질것 같아.
lambda 이전에 functional interface 에 관한 글을 선행하여 작성하려고 합니다.
Java8에서 추가된 Functional Interface에 대해서 알아봅시다 🧐
예제를 최대한 많이 넣으려고 했습니다!.
Functional Interface는 Object Class의 메서드를 제외하고 '구현해야 할 추상 메서드가 하나만 정의된 인터페이스'를 의미합니다.
//Functional Interface인 경우: 메서드가 하나만 있음
public interface Functional {
public boolean test(Condition condition);
}
//java.lang.Runnable도 결과적으로 Functional Interface임
public interface Runnable {
public void run();
}
//구현해야 할 메서드가 하나 이상 있는 경우는 Functional Interface가 아님
public interface NonFunctional {
public void actionA();
public void actionB();
}
🤔 Object Class를 제외하는 이유는?
자바에서 사용되는 모든 객체들이 Object 객체를 상속하고 있기 때문에, 인터페이스 구현체들이 Object 객체의 메서드를 굳이 재정의하지 않아도 그 메서드들을 소유할 수 있으므로 Object Class와 관련된 메서드들은 Functional Interface의 대상이 되지 않는다.
//Object 객체의 메서드만 인터페이스에 선언되어 있는 경우는 Functional Interface가 아님
public interface NotFunctional {
public boolean equals(Object obj);
}
//Object 객체의 메서드를 제외하고 하나의 추상 메서드만 선언되어 있는 경우는 Functional Interface임
public interface Functional {
public boolean equals(Object obj);
public void execute();
}
//Object객체의 clone 메서드는 public 메서드가 아니기 때문에 Functional Interface의 대상이 됨
public interface Functional {
public Object clone();
}
public interface NotFunctional {
public Object clone();
public void execute();
}
🤔 Java8에서 Functional Interface를 도입한 이유는 무엇일까?
함수형 프로그래밍을 지원하기 위해서 도입되었다.
간략하게 함수형 프로그래밍이란 변경 가능한 상태를 최대한 제거하려고 노력하고, 내부 상태를 갖지 않아 같은 입력에 대해서 항상 같은 출력이 보장되는 함수인 순수 함수를 지향하며 불변성을 추구한다.
1. 프로그램 검증이 쉽고
2.계산한 값을 캐싱 하는 등의 최적화가 가능하며
3.스레드가 프로그램 상태를 공유하기 때문에 생기는 동시성 문제로부터 자유로운 편이며
4.함수를 재사용 할 수 있는 등의 장점들이 존재한다.
자세한 내용은 이후 포스팅에서 다루도록 하겠습니다.
Java SDK 8에서는 @FunctionalInterface라고 하는 어노테이션을 제공하여 작성한 인터페이스가 Functional Interface 인지 확인할 수 있도록 하고 있다.
java에서 기본으로 제공하는 FuntionalInterface가 아닌 직접 만든 Functional Interface에는 항상@FunctionalInterface 애너테이션을 사용해야 한다.
(Effective Java Item 44 표준 함수형 인터페이스를 사용하라)
1. 인터페이스가 람다용으로 설계된 것임을 알려주며
2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일이 되게 해주며
3. 그 결과 유지 보수 과정에서 누가 실수로 메서드를 추가하지 못하게 막아주거나 함수형이어야 하는 인터페이스가 다른 인터페이스를 상속한 경우 미리 확인할 수 있다.
@FunctionalInterface
public interface NotFunctional {
public boolean equals(Object obj);
}
예제 코드에서 나오는 () → lambda expression 람다 표현식과
:: 메서드 참조는 추후 포스팅할 예정 😅
최대한 메서드 참조를 활용하였고 메서드 참조 표현식 :: 은 특정 클래스에 해당 메서드를
참조하는데 () -> 람다 식을 사용할시 적어야 하는 매개변수를 생략할 수 있도록 해줍니다.
메서드 시그니처 - T apply (T t)
또 다른 기본형 인터페이스인 Function<T,T>를 상속받으며 T apply(T t)를 호출합니다.
Type T의 인자를 하나 받고 동일한 Type T 객체를 리턴하는 함수형 인터페이스.
UnaryOperator
package java.util.function;
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
//..
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
identity()
Function<T, T>
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
public static void main(String[] args) {
String testString1 = "convert to capital letter";
UnaryOperator<String> convertToUpper = String::toUpperCase;
System.out.println(convertToUpper.apply(testString1));
UnaryOperator<String> convertToIdentity = UnaryOperator.identity();
System.out.println(convertToIdentity.apply(testString1));
}
//출력
//CONVERT TO CAPITAL LETTER
//convert to capital letter
메서드 시그니처 - T apply (T t1, T t2)
BiFunction<T,T,T>를 상속받으며 T apply(T tq, T t2)를 호출함
Type T의 인자를 두 개 받고 동일한 Type T 객체를 리턴하는 함수형 인터페이스.
또다른 기본형 인터페이스인 BiFunction<T,R>를 상속받으며 Function 의 메서드 시그니처를 따른다.
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
BinaryOperator.maxBy()
BinaryOperator.minBy()
BiFunction<T, T, T>
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t, U u) -> after.apply(apply(t, u));
}
}
public class FItest {
public static void main(String[] args) {
BigInteger Bone = new BigInteger("11");
BigInteger Btwo = new BigInteger("22");
BinaryOperator<BigInteger> maxOperator = BinaryOperator.maxBy(Comparator.comparing(BigInteger::intValue));
BinaryOperator<BigInteger> minOperator = BinaryOperator.minBy(Comparator.comparing(BigInteger::intValue));
BinaryOperator<BigInteger> bigOperator = BigInteger::add;
System.out.println("maxOperator = " + maxOperator.apply(Bone, Btwo));
System.out.println("minOperator = " + minOperator.apply(Bone, Btwo));
System.out.println("minOperator = " + bigOperator.apply(Bone, Btwo));
}
}
//출력
//22
//11
//33
메서드 시그니처 - boolean test(T t);
type T를 인자로 받고 Boolean을 리턴하는 함수형 인터페이스.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
public class FItest {
private static boolean isOverThenFive(int num) {
return num > 5;
}
private static boolean isUnderThenFive(int num) {
return num < 5;
}
public static void main(String[] args) {
Predicate<Integer> isOverPredicate = FItest::isOverThenFive;
Predicate<Integer> isUnderPredicate = FItest::isUnderThenFive;
Predicate<List<Integer>> cpredicates = List::isEmpty;
Predicate<String> equalPredicate = Predicate.isEqual("test_string");
System.out.println("cpredicates = " + cpredicates.test(new ArrayList<Integer>(Arrays.asList(1,2,3))));
System.out.println("isEquals = " + equalPredicate.test("not_same"));
//test값이 "test_string" 이여야 true 반환
System.out.println("predicate = " + isOverPredicate.test(3));
System.out.println("Negate_predicate = " + isOverPredicate.negate().test(3));
System.out.println("Or_predicate = " + isOverPredicate.or(isUnderPredicate).test(3));
System.out.println("And_predicate = " + isOverPredicate.and(isUnderPredicate).test(3));
}
}
// 출력
//cpredicates = false
//isEquals = false
//predicate = false
//Negate_predicate = true
//Or_predicate = true
//And_predicate = false
Function으로도 인수와 반환 타입이 같도록 사용할 수 있지만
해당 경우에는 Unary를 사용하는 것이 더 낫습니다.
메서드 시그니처 - R apply(T t)
1개의 Type T 인자를 받고, 1개의 객체 Type R을 리턴하는 함수형 인터페이스.
```java
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
```
public class FItest {
private static Integer half(Integer x) {
return x / 2;
}
private static Double multiply(Integer x) {
return x * 2.0;
}
public static void main(String[] args) {
Function<Integer, Integer> half = FItest::half;
Function<Integer, Double> multiply = FItest::multiply;
Function<Integer, Integer> identity = Function.identity();
String[] alpabetList = new String[] {"A", "B", "C", "D"};
Function<String[], List<String>> list = Arrays::asList;
System.out.println("half = " + half.apply(2));
System.out.println("multiply = " + multiply.apply(2));
System.out.println("andThen = " + half.andThen(multiply).apply(6));
System.out.println("compose = " + half.compose(identity).apply(6));
System.out.println("identity = " + identity.apply(6));
System.out.println("list = " + list.apply(alpabetList));
}
}
//출력
//half = 1
//multiply = 4.0
//andThen = 6.0
//compose = 3
//identity = 6
//list = [A, B, C, D]
메서드 시그니처 - T get()
Supplier는 인자를 받지 않고 Type T 객체를 리턴하는 함수형 인터페이스입니다.
```java
@FunctionalInterface
public interface Supplier<T> {
T get();
}
```
public class FItest {
private static String getString() {
return "stringTest";
}
public static void main(String[] args) {
Supplier<String> stringSupplier = FItest::getString;
Supplier<Instant> timeSupplier = Instant::now;
System.out.println("stringSupplier = " + stringSupplier.get());
System.out.println("timeSupplier = " + timeSupplier.get());
}
}
//출력
//stringSupplier = stringTest
//timeSupplier = 2022-12-16T12:19:58.171838Z
메서드 시그니처 - void accept(T t)
Consumer 1개의 Type T 인자를 받고 리턴 값이 없는 함수형 인터페이스입니다.
```java
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
```
- andThen() Consumer들을 연결해 주는 Chaining 메서드.
public class FItest {
private static String getString() {
return "stringTest";
}
public static void main(String[] args) {
Consumer<String> consumer = System.out::printf;
consumer.accept("consumer test");
}
}
//출력
//consumer test
Java.util.function 패키지에는 43개의 interface가 존재한다.
1. UnaryOperator
2. BinaryOperator
3. Predicate
4. Function<T, R>
5. Supplier
6. Consumer
기본 interface 6가지 기억하면 나머지는 유추가 가능하다.
Effective Java Item 44에서는 필요한 용도에 알맞는 위에 43가지 표준 함수형인터페이스가 존재한다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하는 것을 권장한다.
불필요한 중복을 피할 수 있으며, andThen(), compose() 와 같은 유용한 디폴트 메서드를 많이 제공하기 때문에 다른 코드와의 상호 운용성도 좋아진다.
Comparator 인터페이스를 떠올려보면 알 수 있다.
// Comparator
@FunctionInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
// ToIntBiFunction
@FunctionalInterface
public interface ToIntBiFunction<T, U> {
int applyAsInt(T t, U u);
}
#참조