함수형 인터페이스(Functional Interface)
리액티브 프로그래밍을 잘 사용하기 위해서 기본적으로 함수형 프로그래밍 기법을 알아야 한다.
✅ 함수형 인터페이스
단 하나의 추상 메서드만 정의되어 있는 인터페이스.
함수를 일급 시민으로 취급하여, 함수 자체를 파라미터로 전달할 수 있다.
ex) Comparator
public class Example4_1 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
Collections.sort(cryptoCurrencies, new Comparator<CryptoCurrency>() {
@Override
public int compare(CryptoCurrency cc1, CryptoCurrency cc2) {
return cc1.getUnit().name().compareTo(cc2.getUnit().name());
}
});
for(CryptoCurrency cryptoCurrency : cryptoCurrencies)
System.out.println("암호 화폐명: " + cryptoCurrency.getName() +
", 가격: " + cryptoCurrency.getUnit());
}
}
-> sort 메서드 안에 Comparator 인터페이스를 익명 구현 객체로 전달한다.
Comparator 인터페이스의 내부 모습은 다음과 같다.
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
하나의 추상 메서드만 정의되어 있고, @FunctionalInterface라는 애노테이션으로 함수형 인터페이스임을 명확하게 했다.
사실 익명 구현 객체, 즉 객체를 파라미터로 전달하는 방식은 이미 Java에서 많이 쓰이고,
익명 구현 객체를 전달하는 방식은 코드가 길어 지저분하게 보이기도 한다.
-> 그래서 Java 8부터 람다 표현식을 통해 익명 구현 객체를 전달하던 방식을 조금 더 함수형 프로그래밍에 맞게 표현할 수 있게 됐다.
람다 표현식(Lambda Expression)
✅ 람다 표현식
람다 표현식은 Java에서 함수를 값으로 취급하기 위해 생겨난 간결한 형태의 표현식이다.
람다 표현식은 함수형 인터페이스를 구현한 클래스의 메서드 구현을 단순화한 표현식이다.
(람다 파라미터) -> { 람다 몸체 }
람다 파라미터: 함수형 인터페이스에 정의된 추상 메서드의 파라미터.
람다 몸체: 추상 메서드에서 구현되는 메서드 몸체.
앞서 봤던 Comparaotr의 예시를 람다 표현식으로 표현해본다.
public class Example4_4 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
Collections.sort(cryptoCurrencies,
(cc1, cc2) -> cc1.getUnit().name().compareTo(cc2.getUnit().name()));
for(CryptoCurrency cryptoCurrency : cryptoCurrencies)
System.out.println("암호 화폐명: " + cryptoCurrency.getName() +
", 가격: " + cryptoCurrency.getUnit());
}
}
-> 훨씬 간결해진 것을 볼 수 있다.
람다 파라미터인 cc1, cc2는 파라미터 타입인 CryptoCurrency가 생략된 표현인데, 람다 표현식에서는 메서드 파라미터의 타입이 내부적으로 추론되어 생략이 가능해진다.
여기서 혼동하지 않아야 할 것은, 람다 표현식도 결국 함수형 인터페이스를 구현한 클래스의 인스턴스를 람다 표현식으로 작성해서 전달한다는 것이다.
✅ 람다 캡처링
람다 표현식 외부에서 정의된 변수(자유 변수)를 사용하는 것.
public class Example4_5 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
String korBTC = "비트코인";
// korBTC = "빗코인";
cryptoCurrencies.stream()
.filter(cc -> cc.getUnit() == CurrencyUnit.BTC)
.map(cc -> cc.getName() + "(" + korBTC + ")" )
.forEach(cc -> System.out.println(cc));
}
}
map() 메서드의 파라미터인 람다 표현식에서 외부에서 정의된 korBTC 변수를 사용하고 있다.
주의할 점으로, 만약 예제의 주석을 해제하면 오류가 발생하는데, 그 이유는 람다 표현식에서 사용되는 자유 변수는 final 또는 final과 같은 효력을 지녀야 하기 때문이다.
메서드 레퍼런스(Method Reference)
✅ 메서드 레퍼런스
람다 표현식을 더 간결하게 작성할 수 있는 방법.
✅ ClassName::static method
public class Example4_6 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
cryptoCurrencies.stream()
.map(cc -> cc.getName())
// .map(name -> StringUtils.upperCase(name))
.map(StringUtils::upperCase)
.forEach(name -> System.out.println(name));
}
}
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
✅ ClassName::intance method
public class Example4_7 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
cryptoCurrencies.stream()
.map(cc -> cc.getName())
// .map(name -> name.toUpperCase())
.map(String::toUpperCase)
.forEach(name -> System.out.println(name));
}
}
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
✅ object::intance method
public class Example4_8 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
int amount = 2;
PaymentCalculator calculator = new PaymentCalculator();
cryptoCurrencies.stream()
.filter(cc -> cc.getUnit() == CurrencyUnit.BTC)
.map(cc -> new ImmutablePair(cc.getPrice(), amount))
// .map(pair -> calculator.getTotalPayment(pair))
.map(calculator::getTotalPayment)
.forEach(System.out::println);
}
}
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
✅ ClassName::new
public class Example4_9 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
int amount = 2;
Optional<PaymentCalculator> optional =
cryptoCurrencies.stream()
.filter(cc -> cc.getUnit() == CurrencyUnit.BTC)
.map(cc -> new ImmutablePair(cc.getPrice(), amount))
// .map(pair -> new PaymentCalculator(pair))
.map(PaymentCalculator::new)
.findFirst();
System.out.println(optional.get().getTotalPayment());
}
}
생성자도 메서드 레퍼런스로 사용할 수 있다.
주석 처리된 라인과 그 아래 라인은 같은 표현식이다.
함수 디스크립터(Function Descriptor)
✅ 함수 디스크립터
일반화된 람다 표현식을 통해서 함수형 인터페이스가 어떤 파라미터를 가지고, 어떤 값을 리턴하는지 설명해 주는 역할.
함수형 인터페이스 | 함수 디스크럽터 |
Predicate<T> | T -> boolean |
Consumer<T> | T -> void |
Function<T, R> | T -> R |
Supplier<T> | () -> T |
BiPredicate<L, R> | (L, R) -> boolean |
BiConsumer<T, U> | (T, U) -> void |
BiFunction<T, U, R> | (T, U) -> R |
✅ Predicate
추상 메서드가 하나의 파라미터를 가지고, 리턴 값으로 boolean 타입을 반환하는 함수형 인터페이스.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Predicate를 가장 흔하게 볼 수 있는 코드는 filter 메서드다.
filter 메서드는 파라미터로 Predicate를 가지며 test 메서드의 리턴 값이 true인 데이터만 필터링하게 된다.
다음은 filter 메서드를 직접 코드로 구현한 예제다.
public class Example4_11 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
List<CryptoCurrency> result = filter(cryptoCurrencies, cc -> cc.getPrice() > 500_000);
for (CryptoCurrency cc : result) {
System.out.println(cc.getName());
}
}
private static List<CryptoCurrency> filter(List<CryptoCurrency> cryptoCurrencies,
Predicate<CryptoCurrency> p){
List<CryptoCurrency> result = new ArrayList<>();
for (CryptoCurrency cc : cryptoCurrencies) {
if (p.test(cc)) {
result.add(cc);
}
}
return result;
}
}
✅ Consumer
추상 메서드가 하나의 파라미터를 가지고, 리턴 값이 없는 함수형 인터페이스.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Consumer의 대표적인 예로, 특정 작업을 수행한 후, 결과 값을 리턴할 필요가 없는 배치 처리를 들 수 있다.
public class Example4_13 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
addBookmark(cryptoCurrencies, cc -> saveBookmark(cc));
}
private static void addBookmark(List<CryptoCurrency> cryptoCurrencies,
Consumer<CryptoCurrency> consumer) {
for (CryptoCurrency cc : cryptoCurrencies) {
consumer.accept(cc);
}
}
private static void saveBookmark(CryptoCurrency cryptoCurrency) {
System.out.println("# Save " + cryptoCurrency.getUnit());
}
}
addBookmark() 메서드의 파리미터로 리턴 값이 없는 saveBookmark가 추상 메서드인 Consumer 인터페이스를 전달한다.
✅ Function
추상 메서드가 하나의 T 타입 파라미터를 가지고, 리턴 값으로 R 타입을 반환하는 함수형 인터페이스.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
어떤 input에 대해 어떤 처리 과정을 거친 후 어떤 output을 반환하는 전형적인 함수 역할을 하기 때문에, Function이라는 이름을 가진다.
public class Example4_15 {
public static void main(String[] args) {
List<CryptoCurrency> cryptoCurrencies = SampleData.cryptoCurrencies;
int totalPayment = calculatePayment(cryptoCurrencies, cc -> cc.getPrice() * 2);
System.out.println("# 구매 비용: " + totalPayment);
}
private static int calculatePayment(List<CryptoCurrency> cryptoCurrencies,
Function<CryptoCurrency, Integer> f) {
int totalPayment = 0;
for (CryptoCurrency cc : cryptoCurrencies) {
totalPayment += f.apply(cc);
}
Supplier s = () -> "";
return totalPayment;
}
}
✅ Supplier
추상 메서드가 파라미터를 갖기 않고, 리턴 값을 반환하는 함수형 인터페이스.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier 인터페이스는 어떤 값이 필요할 때 데이터를 제공하는 용도로 사용할 수 있다.
public class Example4_17 {
public static void main(String[] args) {
String mnemonic = createMnemonic();
System.out.println(mnemonic);
}
private static String createMnemonic() {
return Stream
.generate(() -> getMnemonic())
.limit(12)
.collect(Collectors.joining(" "));
}
private static String getMnemonic() {
List<String> mnemonic = Arrays.asList(
"alpha", "bravo", "charlie",
"delta", "echo", "foxtrot",
"golf", "hotel", "india",
"juliet", "kilo", "lima",
"mike", "november", "oscar",
"papa", "quebec", "romeo",
"sierra", "tango", "uniform",
"victor", "whiskey", "xray",
"yankee", "zulu"
);
Collections.shuffle(mnemonic);
return mnemonic.get(0);
}
}
Stream.generate(Supplier).limit(int)는 limit 만큼의 데이터를 생성한다.
✅ Bixxxxx
BiPredicate, BiConsumer, BiFunction과 같이 Bi로 시작하는 함수형 인터페이스는 추상 메서드에 전달해야 할 파라미터가 하나 더 추가되어, 두개의 파리미터를 갖는 함수형 인터페이스다.
-> 기본 함수형 인터페이스의 확장형이라고 보면 된다.
참고자료
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
'book > 스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
[리액티브 프로그래밍] 마블 다이어그램(Marble Diagram) (0) | 2024.03.11 |
---|---|
[리액티브 프로그래밍] Reactor 개요 (0) | 2024.03.11 |
[리액티브 프로그래밍] Blocking I/O와 Non-Blocking I/O (2) | 2024.02.26 |
[리액티브 프로그래밍] 리액티브 스트림즈(Reactive Streams) (0) | 2024.02.19 |
[리액티브 프로그래밍] 리액티브 시스템과 리액티브 프로그래밍 (0) | 2024.02.19 |