함수형 인터페이스란?
java는 기본적으로 객체지향 언어이기 때문에 순수 함수와 일반 함수를 다르게 취급하고 있으며, Java에서는 이를 구분하기 위해 함수형 인터페이스가 등장했다.
람다를 사용하려면 함수가 하나인 인터페이스를 사용해야 하고, 이를 함수형 인터페이스라고 한다.
예시를 통해 살펴보자.
public interface MyLambdaFunction {
int max(int a, int b);
}
a, b 중 더 큰 값을 고르는 인터페이스가 있다.
이 코드를 구현하면 기본적으로 익명 클래스를 통해 구현할 수 있다. 코드는 다음과 같다
public static void main(String[] args) {
MyLambdaFunction myLambdaFunction = new MyLambdaFunction() {
@Override
public int max(int a, int b) {
return Math.max(a, b);
}
};
System.out.println(myLambdaFunction.max(1, 100));
}
해당 코드는 람다로 리팩토링을 진행할 수 있다. intellij를 이용하면 바로 람다식으로 바꿔주기도 한다.
수정된 코드를 살펴보자.
public static void main(String[] args) {
MyLambdaFunction myLambdaFunction = (a, b) -> Math.max(a, b);
System.out.println(myLambdaFunction.max(1, 100));
}
코드가 매우 간단해졌다.
람다는 기본적으로 불필요한 코드를 줄이고 가독성을 높여줄 수 있다. 또한 컴파일러가 타입 추론을 해주어 메서드명과 타입 또한 필요 없다.
그렇다면 항상 람다를 쓸 수 있을까? 위에서 말했지만, 람다를 사용하려면 함수가 하나인 인터페이스를 사용해야 하고, 이를 함수형 인터페이스라고 한다. 즉, 함수형 인터페이스일 경우, 람다로 리팩토링을 진행할 수 있는것이다.
함수가 2개일 경우 사용 못 할까? 코드를 통해 살펴보자.
기존에 있던 max() 메서드 이외에 min() 메서드를 추가했다.
public interface MyLambdaFunction {
int max(int a, int b);
int min(int a, int b);
}
이후 해당 함수를 익명 클래스를 통해 구현했다. 코드는 다음과 같다.
public static void main(String[] args) {
MyLambdaFunction myLambdaFunction = new MyLambdaFunction() {
@Override
public int max(int a, int b) {
return Math.max(a, b);
}
@Override
public int min(int a, int b) {
return Math.min(a, b);
}
};
System.out.println(myLambdaFunction.max(1, 100));
System.out.println(myLambdaFunction.min(1, 100));
}
이 코드를 람다로 리팩토링 할 수 있을까? 못 한다.
MyLamdaFunction Interface에서 2개의 메서드를 지니고 있기에 함수형 인터페이스가 아니다. 그렇기에 위와 같은 상황에서는 익명 클래스를 사용해야 한다.
@FuntionalInterface
함수형 인터페이스를 보면 위와 같은 어노테이션을 본 적이 있을 거다. 없어도 람다식 잘 쓸 수 있었는데 무슨 기능을 하나... 실험해본 결과,
해당 어노테이션이 붙으면 함수가 하나가 아닐 경우 에러가 발생한다.
없어도 작동은 잘 하는데, 해당 어노테이션을 붙이면 함수형 인터페이스라는걸 조금 더 명확하게 알 수 있으니 사용하는거 아닐까? 생각한다.
자바에서 제공하는 기본적인 함수형 인터페이스
1. Supplier
- 매개변수 없이 반환값 만을 갖는 함수형 인터페이스
@FunctionalInterface
public interface Supplier<T> {
T get();
}
예시코드
1) 익명 클래스
public static void main(String[] args) {
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "hello world";
}
};
System.out.println(supplier.get());
// 결과 : hello world
}
2) 람다 리팩토링
public static void main(String[] args) {
Supplier<String> supplier = () -> "hello world";
System.out.println(supplier.get());
}
// 결과 : hello world
2. Consumer
- 객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스
@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); };
}
}
예시코드
1) 익명 클래스
public static void main(String[] args) {
Consumer<String> printConsumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("익명 클래스 : " + s);
}
};
printConsumer.accept("Hello world");
//익명 클래스 : Hello world
}
2) 람다 리팩토링
public static void main(String[] args) {
Consumer<String> printConsumer = s -> System.out.println("익명 클래스 : " + s);
printConsumer.accept("Hello world");
//익명 클래스 : Hello world
}
3. Function
- 객체 T를 매개변수로 받아서 처리한 후 R로 반환
@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;
}
}
1) 익명 클래스
public static void main(String[] args) {
Function<String, Integer> stringLengthFunction = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
};
System.out.println(stringLengthFunction.apply("AAABBccc"));
// 8
}
2) 람다로 리팩토링
public static void main(String[] args) {
Function<String, Integer> stringLengthFunction = str -> str.length();
System.out.println(stringLengthFunction.apply("AAABBccc"));
// 8
}
4. Predicate
- 객체 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);
}
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
1) 익명 클래스
public static void main(String[] args) {
Predicate<String> isPredicate = new Predicate<String>() {
@Override
public boolean test(String s) {
if (s.length() == 1) {
return true;
}
return false;
}
};
System.out.println(isPredicate.test("s")); // true
System.out.println(isPredicate.test("ssssss")); // false
}
2) 람다로 리팩토링
public static void main(String[] args) {
Predicate<String> isPredicate = str -> str.length() == 1;
System.out.println(isPredicate.test("s")); // true
System.out.println(isPredicate.test("ssssss")); // false
}
의문점 : 함수형 인터페이스는 하나의 함수만 존재해야 하는것으로 알고 있는데, 위의 메서드들 ex) Predicate, Consumer, Function...
에 하나의 메서드가 아닌 여러개의 메서드가 존재한다. 그런데 왜 함수형 인터페이스라고 하는걸까?
- 함수형 인터페이스는 하나의 추상 메서드만을 가지고 있어야 한다.
- default 메서드와 정적 메서드는 인터페이스에 추가할 수 있다.
함수형 인터페이스 실습
사실 필자는 Consumer, Function, Predicate 등등... 어떤 역할을 하는지는 알겠는데, 이런걸 어디에 써먹어! 라는 생각을 가지고 있었다.
머리속에 납득이 되지 않으면 대충 공부하는 성향이 있기에, 간단한 실습을 통해 함수형 인터페이스의 위력을 알아보자.
@Test
@DisplayName("기존 함수의 중복을 람다를 활용해 중복을 제거한다")
void 기존_함수의_중복을_람다를_활용해_중복을_제거한다() {
class Calculator {
// TODO: 람다를 활용하여 sum 메서드를 통해 중복을 제거하세요.
static int sumAll(final List<Integer> numbers) {
var total = 0;
for (final var number : numbers) {
total += number;
}
return total;
}
// TODO: 람다를 활용하여 sum 메서드를 통해 중복을 제거하세요.
static int sumAllEven(final List<Integer> numbers) {
var total = 0;
for (final var number : numbers) {
if (number % 2 == 0) {
total += number;
}
}
return total;
}
// TODO: 람다를 활용하여 sum 메서드를 통해 중복을 제거하세요.
static int sumAllOverThree(final List<Integer> numbers) {
var total = 0;
for (final var number : numbers) {
if (number > 3) {
total += number;
}
}
return total;
}
private static int sum(
final List<Integer> numbers,
final Predicate<Integer> condition
) {
// TODO: 조건에 맞게 필터링하여 합계를 구하는 기능을 구현하세요.
return 0;
}
}
final List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
assertAll(
() -> assertThat(Calculator.sumAll(numbers)).isEqualTo(45),
() -> assertThat(Calculator.sumAllEven(numbers)).isEqualTo(20),
() -> assertThat(Calculator.sumAllOverThree(numbers)).isEqualTo(39)
);
}
Predicate를 통해 중복되는 메서드를 제거할 수 있었다. 수정된 코드는 다음과 같다.
@Test
@DisplayName("기존 함수의 중복을 람다를 활용해 중복을 제거한다")
void 기존_함수의_중복을_람다를_활용해_중복을_제거한다() {
class Calculator {
static int sumAll(final List<Integer> numbers) {
return sum(numbers, number -> true);
}
static int sumAllEven(final List<Integer> numbers) {
return sum(numbers, number -> number % 2 ==0);
}
static int sumAllOverThree(final List<Integer> numbers) {
return sum(numbers, number -> number > 3);
}
private static int sum(
final List<Integer> numbers,
final Predicate<Integer> condition
) {
int sum = 0;
for (Integer number : numbers) {
if (condition.test(number)) {
sum += number;
}
}
return sum;
}
}
코드의 양이 상당히 줄어들었고, 가독성 또한 더 명확해졌다고 생각한다.
함수형 인터페이스를 왜 사용해야하는지, 각각의 함수형 인터페이스들을 어떻게 써먹어야하는지는 해당 예시를 통해 어느정도 감을 잡을 수 있을 것 같다.
아직 함수형 인터페이스가 어색하다. 머릿속에서 이 부분은 함수형 인터페이스로 추출해서 코드의 가독성을 향상시킬 수 있겠군! 하는 내 모습이 상상이 안 된다. 어색하다면, 의식적으로 함수형 인터페이스를 사용해보며 함수형 프로그래밍에 익숙해지자!
참고글
https://mangkyu.tistory.com/113
'java' 카테고리의 다른 글
Java Custom Exception (0) | 2025.03.04 |
---|---|
Stream (0) | 2025.02.25 |
Thread(3) - volatile, synchronize, lock (8) | 2025.01.19 |
Thread(2) - 스레드의 상태 (6) | 2025.01.15 |
Thread(1) - java의 메모리 구조, 프로세스의 메모리 구조 (4) | 2025.01.14 |