함수형 인터페이스와 제네릭
1. 문자, 숫자 타입을 각각 처리하는 두 개의 함수형 인터페이스
package lambda.lambda3;
public class GnenericMain1 {
public static void main(String[] args) {
StringFunction upperCase = s -> s.toUpperCase();
String result1 = upperCase.apply("hello");
System.out.println(result1); // HELLO 출력
NumberFunction squere = n -> n * n;
System.out.println(squere.apply(3)); // 9 출력
}
@FunctionalInterface
interface StringFunction {
String apply(String s);
}
@FunctionalInterface
interface NumberFunction {
Integer apply(Integer s);
}
}
2. Object를 사용해 다형성으로 함수를 하나로 합침
(+) 앞에서 만든 함수형 인터페이스 2개를 1개로 합칠 수 있음
(-) 캐스팅 과정 필요
package lambda.lambda3;
public class GnenericMain2 {
public static void main(String[] args) {
ObjectFunction upperCase = s -> ((String) s).toUpperCase();
String result1 = (String) upperCase.apply("hello");
System.out.println(result1); // HELLO 출력
ObjectFunction squere = n -> (Integer)n * (Integer)n;
System.out.println(squere.apply(3)); // 9 출력
}
@FunctionalInterface
interface ObjectFunction {
Object apply(Object obj);
}
}
3. 제네릭 도입
(+) 코드 재사용과 타입 안정성이 높아짐
package lambda.lambda3;
public class GnenericMain6 {
public static void main(String[] args) {
GenericFunction<String, String> upperCase = s -> s.toUpperCase();
GenericFunction<String, Integer> stringLength = str -> str.length();
System.out.println(upperCase.apply("hello")); // HELLO 출력
System.out.println(stringLength.apply("hello")); // 5 출력
}
// T : 매개변수 타입 & R : 반환 타입
@FunctionalInterface
interface GenericFunction<T, R> {
R apply(T s);
}
}
-> GenericFunction은 매개변수가 1개이고, 반환값이 있는 모든 람다에 사용할 수 있다.
-> 제네릭을 활용하면 타입 안정성을 보장하면서도 유연한 코드를 작성할 수 있다.
-> 컴파일 시점에 타입 체크가 이뤄지므로 런타임 에러를 방지할 수 있다.
람다와 타겟 타입
앞에서 만든 GenericFunction은 코드 중복을 줄이고 유지보수성을 높여주지만 2가지 문제가 있다.
1. 모든 개발자들이 비슷한 함수형 인터페이스를 개발해야 한다.
2. 개발자 A가 만든 함수형 인터페이스와 개발자 B가 만든 함수형 인터페이스는 서로 호환되지 않는다.

-> functionA와 functionB는 자바 타입 시스템상 전혀 다른 인터페이스이므로 서로 호환되지 않는다.
-> 두 인터페이스가 시그니처가 같고 같은 모양의 함수형 인터페이스지만, 타입 자체는 별개이므로 상호 대입은 허용되지 않는다.
정리
1. 람다는 익명 함수로서 특정 함수를 가지지 않고, 대입되는 참조 변수가 어떤 함수형 인터페이스를 가리키냐에 따라 타입이 결정된다.
2. 이미 대입된 변수(functionA)는 엄연히 FunctionA 타입의 객체가 되었으므로 이를 FunctionB 참조 변수에 그대로 대입할 수 없다.
자바가 기본으로 제공하는 함수형 인터페이스
자바는 이런 문제를 해결하기 위해 필요한 함수형 인터페이스 대부분을 기본으로 제공한다.

기본 함수형 인터페이스
자바가 제공하는 대표적인 함수형 인터페이스
> Function : 입력 O, 반환 O
> Consumer : 입력 O, 반환 X
> Supplier : 입력 X, 반환 O
> Runnable : 입력 X, 반환 X
* 함수형 인터페이스들은 대부분 제네릭을 활용하므로 종류가 많을 필요는 없다.
* 대부분 java.util.function 패키지에 위치해있다.
1. Function
: 하나의 매개변수를 받고, 결과를 반환하는 함수형 인터페이스
: 입력값 T를 받아 다른 타입(+ 같은 타입)의 출력값 R을 반환하는 연산을 표현할 때 사용
package lambda.lambda4;
import java.util.function.Function;
public class FunctionMain {
public static void main(String[] args) {
// 익명 클래스
Function<String, Integer> function1 = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
};
System.out.println(function1.apply("hello"));
// 람다
Function<String, Integer> function2 = s -> s.length();
System.out.println(function2.apply("hello"));
}
}
2. Consumer
: 입력 값 T만 받고 결과를 반환하지 않는(void) 연산을 수행하는 함수형 인터페이스
: 입력받은 데이터를 기반으로 내부적으로 처리만 하는 경우 유용
package lambda.lambda4;
import java.util.function.Consumer;
public class ConsumerMain {
public static void main(String[] args) {
// 익명 클래스
Consumer<String> consumer1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer1.accept("hello consumer");
// 람다
Consumer<String> consumer2 = s -> System.out.println(s);
consumer2.accept("hello consumer");
}
}
3. Supplier
: 입력을 받지 않고 어떤 데이터를 공급해주는 함수형 인터페이스
: 객체나 값 생성, 지연 초기화 등에 주로 사용
package lambda.lambda4;
import java.util.Random;
import java.util.function.Supplier;
public class SupplierMain {
public static void main(String[] args) {
// 익명 클래스
Supplier<Integer> supplier1 = new Supplier<Integer>() {
@Override
public Integer get() {
return new Random().nextInt(10);
}
};
System.out.println(supplier1.get());
// 람다
Supplier<Integer> supplier2 = () -> new Random().nextInt(10);
System.out.println(supplier2.get());
}
}
4. Runnable
: 입력값도 없고 반환값도 없는 함수형 인터페이스
: 원래는 스레드 실행을 위한 인터페이스로 쓰였지만, 자바8 이후로 람다식으로도 많이 표현된다.
: java.lang 패키지에 있으며(인터페이스 하위 호환을 위해 유지함) 주로 멀티스레딩에서 스레드 작업을 정의할 때 사용한다.
package lambda.lambda4;
public class RunnableMain {
public static void main(String[] args) {
// 익명 클래스
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("hello Runnable");
}
};
runnable1.run();
// 람다
Runnable runnable2 = () -> System.out.println("hello Runnable");
runnable2.run();
}
}
특화 함수형 인터페이스
특화 함수형 인터페이스는 의도를 명확하게 만든 조금 특별한 함수형 인터페이스이다.
> Predicate : 입력 O, 반환 boolean
> Operator(UnaryOperator, BinaryOperator) : 입력 O, 반환 O
1. Preddicate
: 입력 값(T)을 받아 true 또는 false로 판단하는 함수형 인터페이스
: 조건 검사, 필터링 등의 용도로 많이 사용됨 -> 뒤에서 설명할 스트림 API에서 필터 조건을 지정할 때 자주 등장
package lambda.lambda4;
import java.util.function.Predicate;
public class PredicateMain {
public static void main(String[] args) {
Predicate<Integer> predicate1 = new Predicate<Integer>() {
@Override
public boolean test(Integer value) {
return value % 2 == 0;
}
};
System.out.println(predicate1.test(10)); // true 반환
Predicate<Integer> predicate2 = i -> i % 2 == 0;
System.out.println(predicate2.test(10));
}
}
-> Function<T, Boolean>으로 충분히 구현이 가능하지만 별도로 Predicate<T>를 둠으로 의미가 명확해지고, 가독성 및 유지보수성이 높아진다.
2. Operator
UnaryOperator, BinaryOperator 2가지 종류가 제공된다.
1) UnaryOperator(단항 연산)
: 하나의 피연산자에 대해 연산을 수행하는 것을 말한다.
ex) -x, !x
: 입력과 결과가 동일한 타입인 연산을 수행할 때 사용한다.
: Function<T, T>를 상속받는데, 입력과 반환을 모두 같은 T로 고정한다. -> 입력과 반환 타입이 반드시 같아야 한다.
2) BinaryOperator(이항 연산)
: 두 개의 피연산자에 대해 연산을 수행하는 것을 말한다.
ex) x + y, x * y
: 같은 타입의 두 입력을 받아, 같은 타입의 결과를 반환할 때 사용한다.
: BiFunction<T, T, T>를 상속받는 방식으로 구현되어 있는데, 입력값 2개와 반환을 모두 같은 T로 고정한다.
package lambda.lambda4;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class OperatorMain {
public static void main(String[] args) {
Function<Integer, Integer> squere1 = x -> x*x;
UnaryOperator<Integer> squere2 = x -> x*x;
System.out.println(squere1.apply(5));
System.out.println(squere2.apply(5));
BiFunction<Integer, Integer, Integer> addition1 = (a, b) -> a*b;
BinaryOperator<Integer> addition2 = (a, b) -> a*b;
System.out.println(addition1.apply(2, 3));
System.out.println(addition2.apply(2, 3));
}
}
-> 이 역시 Function<T, R>과 BiFunction<T, U, R>로 구현할 수 있지만 의도와 가독성, 유지보수성을 위해 제공된다.
-> 단항 연산이고 타입이 동일하다면 UnaryOperator<T>를 사용하고,
-> 이항 연산이고 타입이 동일하다면 BinaryOperator<T>를 사용하는 것이 좋다.
기타 함수형 인터페이스
입력 값이 2개 이상인 경우, Bixxx 시리즈를 사용하면 된다.
"Bi"는 Binary(이항)의 줄임말이다.
package lambda.lambda4;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
public class BiMain {
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(5, 10)); // 15 출력
BiConsumer<String, Integer> repeat = (s, n) -> {
for(int i=0; i<n; i++) {
System.out.print(s);
}
};
repeat.accept("*", 5); // ***** 출력
System.out.println();
BiPredicate<Integer, Integer> isGreater = (a, b) -> a > b;
System.out.println(isGreater.test(10, 5)); // true 반환
}
}
입력값이 3개라면, 다음과 같이 직접 만들어 사용하면 된다.
package lambda.lambda4;
public class TriMain {
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
}
public static void main(String[] args) {
TriFunction<Integer, Integer, Integer, Integer> triFunction
= (a, b, c) -> a + b+ c;
System.out.println(triFunction.apply(1, 2, 3)); // 6 출력
}
}
정리 - 함수형 인터페이스 종류
기본 함수형 인터페이스

특화 함수형 인터페이스

'인프런 > 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍' 카테고리의 다른 글
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 7. 메서드 참조 (0) | 2025.05.28 |
|---|---|
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 6. 람다 vs 익명 클래스 (0) | 2025.05.28 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 5. 람다 활용 (0) | 2025.05.22 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 3. 람다 (0) | 2025.05.12 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 2. 람다가 필요한 이유 (2) | 2025.05.12 |