함수형 인터페이스란?
람다식을 다루기 위한 인터페이스를 '함수형 인터페이스'라고 한다.
interface MyFunction {
int sum(int a, int b);
}
위와 같은 인터페이스가 정의되어있다고 하자. 그러면 이 인터페이스를 구현한 익명 클래스의 객체는 다음과 같이 생성할 수 있다.
MyFunction f = new MyFunction() {
public int sum(int a, inb b) {
return a+b;
}
};
f.sum(1,2);
위와같이 사용 할수 있으며 MyFunction인터페이스에 정의된 메서드 sum()은 람다식 '(a,b) -> a+b' 과 일치 하므로 아래와 같이 수정할 수 있다.
MyFunction f = (a, b) -> a + b;
f.sum(1,2);
위와 같이 MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로는 익명 객체이고, MyFunction인터페이스를 구현한 익명 객체의 메서드(sum()과 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다.
단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다. 반면에 static메서드와 default메서드의 개수에는 제약이 없다.
참고
@FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해주므로, 꼭 붙이도록 하자.
자주 사용하는 함수형 인터페이스
java에서는 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다.
매번 새로운 함수형 인터페이스를 정의하지 말고, 가능하면 java.util.function패키지의 인터페이스를 활용하자!
그래야 함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서도 좋다.
아래는 자주 쓰이는 가장 기본적인 함수형 인터페이스이다.

매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의되어 있고, Function의 변형으로 Predicate가 있는데, 반환값이 boolean이라는 것만 제외하면 Function과 동일하다. Predicate는 조건식을 함수로 표한하는데 사용된다.
참고
'T' 는 'Type'을, 'R' 은 'Return Type'을 의미한다.
Predicate
매개변수 하나를 받고, 반환 값이 존재하며 반환타입은 boolean 이다.
static final Predicate<Integer> even = i -> i%2==0;
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3, 4, 5);
evenNum(even, list);
}
public static <T> void evenNum(Predicate<T> predicate, List<T> list) {
for (T i : list) {
if (predicate.test(i)) {
System.out.print(i + " ");
}
}
}
위의 코드는 list에서 짝수만 출력하는 예제 이다.
predicate.test(i)를 하게 되면 람다식인 i%2==0 이 수행된다.
결과
2 4
Supplier
매개변수를 받지않고, 반환 값만 있다.
public class FunctionalInterfaceTest {
static final Supplier<Integer> random = () -> (int)(Math.random() * 100)+1;
public static void main(String[] args) {
List<Integer> list = newRandomList(random);
...
}
public static <T> List<T> newRandomList(Supplier<T> supplier) {
List<T> list = new ArrayList<>();
for (int i = 0 ; i < 10 ; i++) {
list.add(supplier.get());
}
return list;
}
}
위의 코드는 newRandomList 호출시 Supplier를 넘기게 되어있으며 supplier.get()시 supplier에 저장된 람다식 () -> (int)(Math.random() * 100) + 1 에 따라 1~100까지의 랜덤한 정수가 반환되어 list에 저장된다.
결과
4 95 34 58 39 22 84 23 8 10
Consumer
매개변수를 하나 받고, 반환 값은 없다.
public class FunctionalInterfaceTest {
static final Consumer<Integer> print = i -> System.out.print(i + " ");
public static void main(String[] args) {
List<Integer> list = newRandomList(random);
print(print, list);
}
public static <T> void print(Consumer<T> consumer, List<T> list) {
for (T e : list) {
consumer.accept(e);
}
}
}
위의 코드는 print 호출시 consumer를 넘기게 되어있으며 consumer.accept()시 comsumer에 저장된 람다식 i -> System.out.print(i + " ")를 수행하게 된다.
결과
4 95 34 58 39 22 84 23 8 10
Function
매개변수를 하나 받고, 반환 값이 존재한다.
public class FunctionalInterfaceTest {
static final Function<Integer, Integer> compute = i -> i/10*10;
public static void main(String[] args) {
List<Integer> list = newRandomList(random);
...
List<Integer> newList = newList(compute, list);
}
public static <T> List<T> newList(Function<T, T> function, List<T> list) {
ArrayList<T> newList = new ArrayList<>();
for (T i : list) {
newList.add(function.apply(i));
}
return newList;
}
}
위의 코드는 newList 호출시 function을 넘기게 되어있으며 function.apply()시 function에 저장된 람다식 i -> i/10*10 을 수행하게 된다.
매개변수가 2개인 인터페이스

매개변수가 두 개인 함수형 인터페이스는 이름 앞에 접두사 'Bi'가 붙는다.
참고
Supplier는 매개변수는 없고 반환값만 존재하는데, 메서드는 두개의 값을 반환할 수 없으므로 BiSupplier가 없다.
아래의 코드는 예제 코드이다.
BiConsumer
public class BiFunctionalInterfaceTest {
public static final BiConsumer<String, Integer> biPrint = (name, age) -> System.out.println("name : " + name + " age : " + age);
public static void main(String[] args) {
Map<String, Integer> dateMap = Map.of("Kim", 10, "Choi", 20, "Lee", 30);
print(biPrint, dateMap);
}
public static void print(BiConsumer<String, Integer> biConsumer, Map<String,Integer> dataMap) {
for (String key : dataMap.keySet()) {
Integer integer = dataMap.get(key);
biConsumer.accept(key, integer);
}
}
}
결과
name : Kim age : 10
name : Choi age : 20
name : Lee age : 30
BiPredicate
public class BiFunctionalInterfaceTest {
public static final BiPredicate<Integer, Integer> biEqual = (num1, num2) -> num1.equals(num2);
// public static final BiPredicate<Integer, Integer> biEqual = Integer::equals; // 위 코드와 같다.
public static void main(String[] args) {
List<Integer> integers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
find(biEqual, integers, 5);
}
public static void find(BiPredicate<Integer, Integer> biPredicate, List<Integer> list, int findNum) {
for (int i : list) {
if (biPredicate.test(i, findNum)) {
System.out.println("Find! ==> " + i);
}
}
}
}
결과
Find! ==> 5
BiFunction
public class BiFunctionalInterfaceTest {
public static final BiFunction<Integer, Integer, Integer> biMax = (i , j) -> i > j ? i : j;
public static void main(String[] args) {
max(biMax, 20, 10);
}
public static void max(BiFunction<Integer, Integer, Integer> biFunction, int num1,int num2) {
Integer apply = biFunction.apply(num1, num2);
System.out.println(num1 + " , " + num2 + " max ==> " + apply);
}
}
결과
20 , 10 max ==> 20
매개변수 타입과 반환타입이 일치한 함수형 인터페이스

Function의 또 다른 변형으로 UnaryOperator와 BinaryOperator가 있는데, 매개변수의 타입과 반환타입의 타입이 모두 일치하다는 점만 제외하고는 Function과 같다.
참고
UnaryOperator와 BinaryOperator의 조상은 각각 Function과 BiFunction이다.
Function의 합성과 Predicate의 결합
앞서 소개한 Funcation, Predicate, Supplier, Consumer .. 함수형 인터페이스에는 추상메서드 외에도 디폴트 메서드와 static 메서드가 정의되어 있다. 우리는 Function과 Predicate에 정의된 메서드에 대해서만 살펴볼 것이다. 그 이유는 다른 함수형 인터페이스의 메서드도 유사하기 때문이다.
Funcation
@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;
}
수학에서 두 함수를 합성해서 하나의 새로운 함수를 만들어낼 수 있다는 것처럼, 두 람다식을 합성해서 새로운 람다식을 만들 수 있다. 이미 알고 있는 것처럼, 두 함수의 합성은 어느 함수를 먼저 적용하느냐에 따라 달라진다.
compose
함수 f,g 가 있을때 f.compose(g)는 함수 g를 먼저 적용하고 후에 f를 적용한다.
andThen
함수 f,g 가 있을때 f.andThen(g)는 함수 f를 먼저 적용하고 후에 g를 적용한다. (compose와 반대 순서)
compose, andThen의 예제코드를 봐보자
public static void main(String[] args) {
Function<String, String> a = i -> i + "A";
Function<String, String> b = i -> i + "B";
String apply = a.andThen(b).apply("1");
System.out.println("andThen = " + apply);
String apply1 = a.compose(b).apply("1");
System.out.println("compose = " + apply1);
}
결과
andThen = 1AB
compose = 1BA
위와같이 andThen과 compose는 적용 순서에 따라서 결과가 달라진다.
identity
함수를 적용하기 이전과 이후가 동일한 '항등 함수'가 필요할 때 사용한다. 이 함수를 람다식으로 표현하면 'x -> x' 이다.
public class AdvanceFunctionTest {
public static void main(String[] args) {
Function<String, String> f = x -> x;
Function<String, String> f2 = Function.identity(); // 위 코드와 같다.
String stringApply = f.apply("1234");
System.out.println("stringApply = " + stringApply);
String stringApply2 = f2.apply("1234");
System.out.println("stringApply2 = " + stringApply2);
}
}
결과
stringApply = 1234
stringApply2 = 1234
위와 같이 apply("1234")가 그대로 출력된다.
항등 함수는 잘 사용되지 않는 편이며, map()으로 변환작업할 때, 변환없이 처리하고자 할 때 사용된다.
Predicate
여러 조건식을 논리 연산자인 and (&&), or (||), not (!)으로 연결해서 하나의 식을 구성할 수 있는 것처럼, 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.
public class AdvancePredicateTest {
public static void main(String[] args) {
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i%2==0;
Predicate<Integer> notP = p.negate();
Predicate<Integer> test1 = notP.and(q.or(r));
Predicate<Integer> test2 = p.and(q.and(r));
System.out.println("test1.test(150) = " + test1.test(150));
System.out.println("test1.test(50) = " + test1.test(50));
System.out.println("test2.test(50) = " + test2.test(50));
System.out.println("test2.test(150) = " + test2.test(150));
}
}
위 코드에서 Predicate<Integer> test1과 , test2를 분석해보자.
test1 = (i > 100) && (i < 200) || (i%2 ==0) 과 같다. 즉, i > 100 이고 i < 200 이거나 i%2 ==0 이어야 한다.
test2 = (i < 100) && (i < 200) && (i%2 ==0) 과 같다. 죽, I < 100이고 i < 200 이고 i%2 ==0 이어야 한다.
결과
test1.test(150) = true
test1.test(50) = false
test2.test(50) = true
test2.test(150) = false
References
Java의 정석, 남궁 성 지음
'Java' 카테고리의 다른 글
| Stream - 중간연산 (0) | 2023.09.20 |
|---|---|
| Stream (0) | 2023.09.20 |
| 람다식 (0) | 2023.09.12 |
| 날짜와 시간 - parse (0) | 2023.09.11 |
| 날짜와 시간 - DateTimeFormatter (0) | 2023.09.11 |