728x90
3.1 네 가지 함수형 인터페이스
- Function: 인수를 받고 결과를 반환합니다.
- Consumer: 인수만 받고 결과를 반환하지 않습니다.
- Supplier: 인수를 받지 않고 결과를 반환합니다.
- Predicate: 인수를 받아서 표현식에 대해 테스트하고 boolean 값을 결과로 반환합니다.
Function
- 하나의 입력과 출력을 가진 전통적인 함수
- Function은 하나의 입력값을 받아 하나의 결괏값을 반환한다.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
public class FunctionExample {
public static void main(String[] args) {
Function<Integer, String> intToString = num -> "숫자: " + num;
String result = intToString.apply(5);
System.out.println(result);
}
}
- 입력타입 T와 출력 타입 R은 동일할 수도 있다.
Consumer
- Consumer(소비)는 입력 파라미터를 소비하지만 아무것도 반환하지 않는다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public class ConsumerExample {
public static void main(String[] args) {
Consumer<String> printMessage = System.out::println;
printMessage.accept("안녕, 세상!");
}
}
- 값의 소비만이 '순수한'함수형 개념에 완벽하게 부합하지 않을 수 있지만, 자바에서 함수형 코딩 스타일을 적용하는 데 중요한 구성 요소.
Supplier
- 다양한 Supplier 변형들은 어떠한 입력 파라미터도 받지 않지만, T 타입의 단일값을 반환합니다.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
public class SupplierExample {
public static void main(String[] args) {
Supplier<Double> randomValue = Math::random;
System.out.println("랜덤: " + randomValue.get());
}
}
- Supplier는 종종 지연 실행(deferred execution)에 사용됩니다. 예를 들어 비용이 많이 드는 작업을 Supplier로 감싸고 필요할 때에만 get을 호출하는 경우가 있습니다.
Predicate
- Predicate는 단일 인수를 받아 그것을 로직에 따라 테스트하고 true 또는 false를 반환하는 함수입니다.
Predicate<T>
의 단일 추상 메서드는 test로, T 타입의 인수를 받아 boolean 타입을 반환합니다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public class PredicateExample {
public static void main(String[] args) {
Predicate<Integer> isEven = number -> number % 2 == 0;
System.out.println("4는 짝수인가? " + isEven.test(4));
System.out.println("5는 짝수인가? " + isEven.test(5));
}
}
3.2 함수형 인터페이스 변형이 많은 이유
- 다양한 타입은 자바가 람다를 도입하는 과정에서 하위 호환성을 유지하기 위해 필수적이다.
함수 아리티
- 함수의 인수 개수를 나타내는 아리티(arity)는 함수가 받아들이는 피연산자의 수를 의미.
- 자바 메서드의 인수 개수와 마찬가지로, SAM에는 각 아리티에 대한 명시적인 함수형 인터페이스가 있어야 한다.
인수가 1개인 경우 | 인수가 2개인 경우 |
---|---|
Function<T, R> | BiFunction<T, U, R> |
Consumer | BiConsumer<T, U> |
Predicate | BiPredicate<T, U> |
- 자바의 기본적인 인터페이스들은 인수를 최대 2개까지 지원하며, 자바의 함수형 API와 사용 사례를 살펴보면 아리티가 1개 또는 2개인 것이 가장 일반적이다.
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
- 위 방식은 권장하지 않는다. 함수형 인터페이스는 정적 및 기본메서드를 통해 추가 기능을 많이 제공하므로, 이러한 기능을 사용하는 것이 최상의 호환성과 이해하기 쉬운 사용 패턴을 보장한다.
@FunctionalInterface
interface BinaryOperator<T> extends BiFunction<T, T, T>{
// }
- 공통된 슈퍼 인터페이스 구현을 통해 더 의미 있는 유형으로 간결한 코드를 작성할 수 있다.
아라티 | 연산자 | 슈퍼 인터페이스 |
---|---|---|
1 | UnaryOperator | Function<T, T> |
2 | BinaryOperator | BiFunction<T, T, T> |
- 하지만 연산자 타입과 해당 상위 인터페이스는 상호 교환할 수 없다는 점을 염두에 두어야 한다. (API 설계할 때 중요.)
public class ArityExample {
public static void main(String[] args) {
// Function
Function<Integer, String> intToString = num -> "Number: " + num;
String result = intToString.apply(10);
System.out.println(result);
// BiFunction
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
int resultBiFunction = add.apply(5, 3);
System.out.println(resultBiFunction);
// BinaryOperator
BinaryOperator<Integer> multiply = (a, b) -> a * b;
int resultBinaryOperator = multiply.apply(5, 3);
System.out.println(resultBinaryOperator);
}
}
원시 타입
- 원시 타입은 제네릭 타입으로 사용될 수 없으므로 원시 타입에 대해 특화된 함수형 인터페이스가 존재.
- boolean 원시 타입은 BooleanSupplier라는 하나의 특수화된 함수형 인터페이스만 갖는다.
public class PrimitiveExample {
public static void main(String[] args) {
// int
IntFunction<String> intToString = num -> "숫자: " + num;
String result = intToString.apply(5);
System.out.println(result);
// double
DoubleSupplier randomSupplier = Math::random;
System.out.println("랜덤: " + randomSupplier.getAsDouble());
// boolean
BooleanSupplier isTrue = () -> true;
System.out.println("참: " + isTrue.getAsBoolean());
}
}
원시타입을 사용하는 이유
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
...
}
@FunctionalInterface
public interface IntFunction<R> {
R apply(int value);
}
- Function<T, R> 부분이 Function<Integer, R>이 되었을 때, 인수로 받는 값을 Object -> Integer(T)로 오토박싱을 해주어야 하므로, 오버헤드가 발생할 수 있다.
- Function<Integer, String>을 사용할 때는 오토박싱이 발생하여 Integer 객체로 변환되며, 해당 과정에서 추가적인 메모리와 시간이 소요될 수 있다.
- IntFunction<String>을 사용하면 원시 타입 int를 직접 처리하기 때문에 이러한 오버헤드가 발생하지 않는다.
함수형 인터페이스 브리징
- 함수형 인터페이스는 인터페이스이며 람다 표현식은 이러한 인터페이스들의 구체적인 구현체다.
- 타입 추론으로 인해 종종 이들을 서로 교환하거나 관련 없는 인터페이스 사이에서 간단히 형 변환할 수 있는 것을 잊어버릴 수도 있다.
public class BridgingExample {
public static void main(String[] args) {
// Functional
Function<Integer, Boolean> isEven = number -> number % 2 == 0;
Predicate<Integer> isEvenPredicate = isEven::apply;
System.out.println("4는 짝수인가? " + isEvenPredicate.test(4));
System.out.println("5는 짝수인가? " + isEvenPredicate.test(5));
// Supplier
Supplier<Double> randomSupplier = Math::random;
Function<Void, Double> randomFunction = v -> randomSupplier.get();
System.out.println("랜덤: " + randomFunction.apply(null));
}
}
3.3 함수 합성
- 함수 합성은 작은 함수들을 결합하여 더 크고 복잡한 작업을 처리하는 함수형 프로그래밍의 주요 접근 방식
- 새로운 키워드를 도입하거나 언어의 의미를 변경하는 대신 자바의 글루 메서드를 사용
- 글루 메서드는 기본적으로 함수형 인터페이스 자체에 직접 구현되며, 이를 통해 네 가지 카테고리의 함수형 인터페이스를 쉽게 합성할 수 있따.
<V> Function<T, R> compose (Function<? super V, ? extends T> before)
<V> Function<T, V> andThen (Function<? super R, ? extends V> after)
- 두 메서드는 인수 이름과 반환되는 Function, 그리고 제네릭 타입으로 나타나는 함수의 합성 방식에 있다.
- compose는 before 인수를 입력하고 결과를 함수에 적용하여 합성된 함수를 만든다.
- andThen은 함수를 실행한 후에 이전 결과에 after를 적용한다
함수 합성을 지원하는 네 가지 주요 인터페이스
Function<T, R>
Function<T, R>
및UnaryOperator<T>
와 같은 특수화된 아리티는 양방향으로 합성을 지원. -Bi 변형들은 andThen만 지원
Predicate<T>
- Predicate는 일반적인 연산을 수행하는 새로운 Predicate를 합성하기 위한 다양한 메서드(and, or, negate)를 제공
Consumer<T>
- andThen만 지원하며 두 개의 Consumer를 순차적으로 합성하여 값을 받아들임.
특수화된 기본형 함수형 인터페이스
- 기본형을 위한 특수화된 함수형 인터페이스들 사이의 합성 지원은 일관성이 없으며, 기본형 간에도 지원 수준이 다르다.
public class CompositionExample {
public static void main(String[] args) {
// Function
Function<Integer, Integer> multiplyByTwo = x -> x * 2;
Function<Integer, Integer> addThree = x -> x + 3;
// compose: addThree -> multiplyByTwo
Function<Integer, Integer> composedFunction = multiplyByTwo.compose(addThree);
System.out.println(composedFunction.apply(5));
// andThen: multiplyByTwo -> addThree
Function<Integer, Integer> andThenFunction = multiplyByTwo.andThen(addThree);
System.out.println(andThenFunction.apply(5));
// Predicate
Predicate<Integer> isEven = x -> x % 2 == 0;
Predicate<Integer> isGreaterThanTen = x -> x > 10;
// and
Predicate<Integer> isEvenAndGreaterThanTen = isEven.and(isGreaterThanTen);
System.out.println(isEvenAndGreaterThanTen.test(12));
System.out.println(isEvenAndGreaterThanTen.test(8));
// or
Predicate<Integer> isEvenOrGreaterThanTen = isEven.or(isGreaterThanTen);
System.out.println(isEvenOrGreaterThanTen.test(8));
System.out.println(isEvenOrGreaterThanTen.test(11));
// negate
Predicate<Integer> isOdd = isEven.negate();
System.out.println(isOdd.test(8));
System.out.println(isOdd.test(11));
// Consumer
Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase());
Consumer<String> printLength = s -> System.out.println(s.length());
// andThen: 순차적으로 실행
Consumer<String> combinedConsumer = printUpperCase.andThen(printLength);
combinedConsumer.accept("함수형 자바 프로그래밍 with 자바");
}
}
3.4 함수형 지원 확장
- JDK의 타입을 변경할 수 없지만 JDK에서 사용하는 세가지 접근 방식과 마찬가지로 여전히 직접 만든 타입을 더 함수적으로 만들 수 있다.
- 기존 타입을 더 함수적으로 만들기 위해 인터페이스에 기본 메서드를 추가
- 함수형 인터페이스를 명시적으로 구현
- 공통 함수형 작업을 제공하기 위해 정적 헬퍼 생성
기본 메서드 추가
- 기본 메서드를 사용하여 '상식적인' 구현 제공 가능.
- 코드의 하위 호환성 유지 가능.
- 기본 메서드의 계층 구조를 통해 인터페이스에 새로운 기능을 추가하면 기존의 구현을 깨뜨리지 않고 새로운 메서드의 상식적인 변형을 제공할 수 있다.
함수형 인터페이스 명시적으로 구현하기
- 함수형 인터페이스를 직접 구현하는 것은 이전에 '비함수형'이었던 타입을 함수형 코드에서 쉽게 사용할 수 있도록 합니다.
- 구체적인 명령 클래스들은 필요한 인수를 받지만 실행된 명령은 단순히 업데이트된 편집기 내용만을 반환합니다.
- 함수형 인터페이스간의 논리적 동등성만으로는 호환성을 만들 수 없으나,
Supplier<>
로 확장함으로써 기본 메서드를 통해 이것들 사이의 격차를 메꿀 수 있습니다. - 인터페이스는 다중 상속을 허용하기 때문에 함수형 인터페이스를 추가하는 것은 문제가 되지 않는다.
public class Command implements Supplier<String> {
private String action;
public Command(String action) {
this.action = action;
}
@Override
public String get() {
return "실행: " + action;
}
public static void main(String[] args) {
Command command = new Command("저장");
System.out.println(command.get());
}
}
정적 헬퍼 생성하기
- JDK 자체에서 제공하는 함수형 인터페이스와 같이 타입을 직접 제어할 수 없는 경우 정적 메서드를 모으는 헬퍼 타입을 ㅁ나들 수 있다.
- Supplier는 인수를 받지 않기 때문에
Function<T, R>
의 결과를 연산할 수 없으므로 두 가지 조합만 가능합니다. - 반대로 Supplier의 경우에도 동일한 이유가 적용된다.
- 직접적으로
Function<T, R>
인터페이스를 확장할 수 없어서 간접적인 컴포지터가 필요하며 정적 헬퍼 형태로 제공된다.Supplier<R> compose(Supplier<T> before, Function<T, R> fn)
Consumer<T> compose(Function<T, R> fn, Consumer<R> after
public class FunctionalHelpers {
public static <T, R>Supplier<R> compose(Supplier<T> supplier, Function<T, R> function) {
return () -> function.apply(supplier.get());
}
public static <T, R> Function<T, R> compose(Function<T, R> function, Function<R, R> after) {
return t -> after.apply(function.apply(t));
}
public static void main(String[] args) {
Supplier<Integer> supplier = () -> 5;
Function<Integer, String> function = num -> "숫자 : " + num;
Supplier<String> composedSupplier = compose(supplier, function);
System.out.println(composedSupplier.get());
}
}
반응형
'스터디 > 함수형 프로그래밍 with 자바' 카테고리의 다른 글
Chapter 06. 스트림 (Stream) (1) | 2024.06.06 |
---|---|
Chapter 05. 레코드 (Record) (0) | 2024.05.30 |
Chapter 04. 불변성 (0) | 2024.05.21 |
Chapter 02. 자바 람다 (0) | 2024.05.08 |
Chapter 01. 함수형 프로그래밍 소개 (1) | 2024.05.02 |