[Java] 59. 함수형 인터페이스

2025. 7. 4. 23:50·Java/Modern Java(8~)

 

함수형 인터페이스

#Java/adv3


함수형 인터페이스와 제네릭1

함수형 인터페이스에서 제네릭이 필요한 이유


방법1: 함수형 인터페이스 따로 선언

@FunctionalInterface
interface StringFunction {
    String apply(String s);
}

@FunctionalInterface
interface NumberFunction {
    Integer apply(Integer s);
}

StringFunction upperCase = s -> s.toUpperCase();
NumberFunction square = n -> n * n;
  • 두 인터페이스의 apply 메서드는 하나의 인자를 입력받고 결과를 반환한다.
  • 타입이 다르기 때문에 두 개의 인터페이스를 따로 만들어서 람다를 할당해야 한다.
  • 타입이 다를때마다 함수형 인터페이스를 계속 만들어야 할까?

방법2: Object로 합치기

ObjectFunction upperCase = s -> ((String)s).toUpperCase();
String result1 = (String) upperCase.apply("hello");

ObjectFunction square = n -> (Integer) n * (Integer) n;
Integer result2 = (Integer) square.apply(3);

@FunctionalInterface
interface ObjectFunction {
    Object apply(Object s);
}
  • 함수형 인터페이스 2개를 1개로 합쳤지만, Object를 사용하기 때문에 복잡하고 안전하지 않은 캐스팅 과정이 필요하다.
  • 코드의 중복을 제거하고 재사용성을 늘렸지만, Object를 사용하므로 다운 캐스팅을 해야 하고, 결과적으로 타입 안전성 문제가 발생한다.

정리

  • 방법1: 코드 재사용X, 타입 안정성O
  • 방법2: 코드 재사용O, 타입 안정성X
  • 이거 어디서 많이 본 내용…? 제네릭의 도입

함수형 인터페이스와 제네릭2

함수형 인터페이스에 제네릭을 도입해서 코드 재사용과 타입 안정성 두 마리 토끼를 모두 챙겨보자.

@FunctionalInterface
interface GenericFunction<T, R> {
    R apply(T s);
}

GenericFunction<String, String> upperCase = s -> s.toUpperCase();
GenericFunction<Integer, Integer> square = n -> n * n;
GenericFunction<Integer, Integer> square = x -> x * x;
GenericFunction<Integer, Boolean> isEven = num -> num % 2 == 0;
  • GenericFunction은 매개변수가 1개이고, 반환값이 있는 모든 람다에 사용할 수 있다.
  • 제네릭을 도입함으로써 코드 재사용성을 높임과 동시에 타입 안정성까지 챙겼다.
    • 코드의 중복을 줄이고 유지 보수성을 높일 수 있다.
  • 동일한 구조의 함수형 인터페이스를 다양한 타입에 재사용할 수 있다.
  • T는 입력 타입을, R은 반환 타입을 나타내며, 실제 사용할 때 구체적인 타입을 지정하면 된다.

람다와 타겟 타입

직접 만든 GenericFunction의 문제점

  • 문제 1. 모든 개발자들이 비슷한 함수형 인터페이스를 개발해야 한다.
    • 비슷한 GenericFunction같은 함수형 인터페이스들이 수없이 만들어진다.
  • 문제 2. 개발자가 만든 함수형 인터페이스와 개발자가 만든 함수형 인터페이스는 서로 호환되지 않는다.
    • FunctionA functionA = i -> "value = " + i;
    • FunctionB functionB = i -> "value = " + i;
    • 컴파일 에러: FunctionB targetB = functionA; // 컴파일 에러!
      • 람다 자체는 동일한 모양이지만, 자바 타입 시스템상 전혀 다른 인터페이스이므로 서로 호환되지 않는다. 이는 람다가 타켓 타입에 의해 타입이 결정되기 때문이다. 자세히 알아보자.
  • 람다와 타켓 타입
    • 람다는 그 자체만으로는 구체적인 타입이 정해져 있지 않고, 타겟 타입(target type)이라고 불리는 맥락(대입되는 참조형)에 의해 타입이 결정된다.
    • FunctionA functionA = i -> "value = " + i;
      • 이 코드에서 i -> "value = " + i 라는 람다는 FunctionA 라는 타겟 타입을 만나서 비로소 FunctionA 타입으로 결정된다.
    • FunctionB functionB = i -> "value = " + i;
      • 동일한 람다라도 이런 코드가 있었다면, 똑같은 람다가 이번에는 FunctionB 타입으로 타겟팅되어 유효하게 컴파일된다.
    • 즉, 람다는 대입되는 함수형 인터페이스(타겟 타입)에 의해 비로소 타입이 결정된다.
    • 자바 컴파일러는 다른 타입으로 간주하기 때문에 상호 대입이 불가능하다.
    • 대입되는 함수형 인터페이스(타겟 타입)에 의해 비로소 타입이 결정된다.
  • /자바가 기본으로 제공하는 함수형 인터페이스
    • 자바는 이런 문제들을 해결하기 위해 필요한 함수형 인터페이스 대부분을 기본으로 제공한다.
    • 이제 비슷한 함수형 인터페이스를 불필요하게 만들 필요도 없고, 호환성 문제도 해결할 수 있다.
    package java.util.function;
    
    @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
        // ...
    }
    
    • Function<String, String> upperCase = s -> s.toUpperCase();
    • Function<Integer, Integer> square = n -> n * n;
    • Function<Integer, String> functionA = i -> "value = " + i;
    • 대입 가능: Function<Integer, String> functionB = functionA;
    • 따라서 자바가 기본으로 제공하는 함수형 인터페이스를 사용하자.

기본 함수형 인터페이스

자바가 제공하는 대표적인 함수형 인터페이스

  • Function : 입력O, 반환O
  • Consumer : 입력O, 반환X
  • Supplier : 입력X, 반환O
  • Runnable : 입력X, 반환X

Function: 입력O, 반환O

package java.util.function;

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    // ...
}

Function<String, Integer> function2 = s -> s.length();
System.out.println("function2 = " + function2.apply("hello"));
  • 하나의 매개변수를 받고, 결과를 반환하는 함수형 인터페이스이다.
  • 입력값(T)을 받아서 다른 타입의 출력값(R)을 반환하는 연산을 표현할 때 사용한다. 물론 같은 타입의 출력 값도 가능하다.
  • 어떠한 값 t에 함수를 apply(적용)해서 결과를 얻는다.

Consumer: 입력O, 반환X

package java.util.function;

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Consumer<String> consumer2 = (s) -> System.out.println(s);
consumer2.accept("hello consumer");
  • 입력 값(T)만 받고, 결과를 반환하지 않는(void) 연산을 수행하는 함수형 인터페이스이다.
  • 입력값(T)을 받아서 처리하지만 결과를 반환하지 않는 연산을 표현할 때 사용한다.
  • 입력 받은 데이터를 기반으로 내부적으로 처리만 하는 경우에 유용하다.
    • 예) 컬렉션에 값 추가, 콘솔 출력, 로그 작성, DB 저장 등
  • Consumer: 소비자 라는 의미에 맞게, 데이터를 accept 하여 consume(소비)만 하고 아무것도 반환하지 않는다.

Supplier: 입력X, 반환O

package java.util.function;

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier<Integer> supplier2 = () -> new Random().nextInt(10);
System.out.println("supplier2.get() = " + supplier2.get());
  • 입력을 받지 않고(()) 어떤 데이터를 공급(supply)해주는 함수형 인터페이스이다.
  • 객체나 값 생성, 지연 초기화 등에 주로 사용된다.
  • Supplier는 "공급자"라는 의미로, 요청할 때마다 값을 공급해주는 역할을 한다.
  • get()을 통해 Supplier 로 부터 값을 Get 한다.

Runnable: 입력X, 반환X

package java.lang;

@FunctionalInterface
public interface Runnable {
    void run();
}

Runnable runnable2 = () -> System.out.println("Hello Runnable");
  • 입력값도 없고 반환값도 없는 함수형 인터페이스이다. 자바에서는 원래부터 스레드 실행을 위한 인터페이스로 쓰였지만, 자바 8 이후에는 람다식으로도 많이 표현된다. 자바8로 업데이트 되면서 @FunctionalInterface 애노테이션도 붙었다.
  • java.lang 패키지에 있다. 자바의 경우 원래부터 있던 인터페이스는 하위 호환을 위해 그대로 유지한다.
  • 주로 멀티스레딩에서 스레드의 작업을 정의할 때 사용한다.
  • 입력값도 없고, 반환값도 없는 함수형 인터페이스가 필요할 때 사용한다.

특화 함수형 인터페이스

  • 의도를 명확하게 만든 조금 특별한 함수형 인터페이스다.
  • Predicate : 입력O, 반환 boolean
    • 조건 검사, 필터링 용도
  • Operator (UnaryOperator , BinaryOperator): 입력O, 반환O
    • 동일한 타입의 연산 수행, 입력과 같은 타입을 반환하는 연산 용도
  • 기본 함수형 인터페이스로도 대체할 수 있지만, 의도를 명확하게 드러내기 위해 특화 함수형 인터페이스가 따로 존재한다.

Predicate

package java.util.function;

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Predicate<Integer> predicate2 = value -> value % 2 == 0;
System.out.println("predicate2.test(10) = " + predicate2.test(10));
  • 입력 값(T)을 받아서 true 또는 false 로 구분(판단)하는 함수형 인터페이스이다.
  • 조건 검사, 필터링 등의 용도로 많이 사용된다(뒤에서 설명할 스트림 API에서 필터 조건을 지정할 때 자주 등장한다).
  • Predicate: 술어. 조건을 만족하는지 test 하여 참/거짓을 판단한다.
  • Function<T, Boolean> 으로 대체할 수 있지만, Predicate<T> 는 "입력 값을 받아 true/false 로 결과를 판단한다"라는 의도를 명시적으로 드러내기 위해 정의된 함수형 인터페이스이다.
    • Function<T, Boolean>을 사용하면 계산을 하여 Boolean을 반환한다고 보일 수 있지만, Predicate<T> 를 사용하면 "이 함수는 조건을 검사하거나 필터링 용도로 쓰인다"라는 의도가 더 분명해진다.
    • 여러 사람과 협업하는 프로젝트에서, "조건을 판단하는 함수"는 Predicate<T> 라는 패턴을 사용함으로써 의미 전달이 명확해진다.
  • boolean 판단 로직이 들어가는 부분에서 Predicate<T> 를 사용하면 코드 가독성과 유지보수성이 향상된다.
    • 이름도 명시적이고, 제네릭에 <Boolean> 을 적지 않아도 된다.

Operator

  • Operator는 UnaryOperator , BinaryOperator 2가지 종류가 제공된다.
  • 수학에서 사용되는 Operator 개념에서 왔다.
    • 수학에서 Operator(연산자)는 보통 같은 타입의 값들을 받아서 동일한 타입의 결과를 반환한다.
    • 덧셈 연산자(+): 숫자 + 숫자 → 숫자
    • 곱셈 연산자(*): 숫자 * 숫자 → 숫자
    • 논리 연산자(AND): boolean AND boolean → boolean
  • 자바에서는 수학처럼 숫자의 연산에만 사용된다기 보다는 입력과 반환이 동일한 타입의 연산에 사용할 수 있다.

UnaryOperator(단항 연산)

package java.util.function;

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    T apply(T t);
    // 실제 코드가 있지는 않음
}
  • 단항 연산은 하나의 피연산자(operand)에 대해 연산을 수행하는 것을 말한다.
    • 예) 숫자의 부호 연산( -x ), 논리 부정 연산( !x ) 등
  • 입력(피연산자)과 결과(연산 결과)가 동일한 타입인 연산을 수행할 때 사용한다.
    • 예) 숫자 5를 입력하고 그 수를 제곱한 결과를 반환한다.
    • 예) String 을 입력받아 다시 String 을 반환하면서, 내부적으로 문자열을 대문자로 바꾼다든지, 앞뒤에 추가 문자열을 붙이는 작업을 할 수 있다.
  • Function<T, T> 를 상속받는데, 입력과 반환을 모두 같은 T 로 고정한다. 따라서 UnaryOperator 는 입력과 반환 타입이 반드시 같아야 한다.

BinaryOperator(이항 연산)

package java.util.function;

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
    T apply(T t1, T t2);
    // 실제 코드가 있지는 않음
}
  • 이항 연산은 두 개의 피연산자(operand)에 대해 연산을 수행하는 것을 말한다.
    • 예: 두 수의 덧셈( x + y ), 곱셈( x * y ) 등
  • 같은 타입의 두 입력을 받아, 같은 타입의 결과를 반환할 때 사용된다.
    • 예) Integer 두 개를 받아서 더한 값을 반환
    • 예) Integer 두 개를 받아서 둘 중에 더 큰 값을 반환
  • BiFunction<T,T,T> 를 상속받는 방식으로 구현되어 있는데, 입력값 2개와 반환을 모두 같은 T로 고정한다. 따라서 BinaryOperator 는 모든 입력값과 반환 타입이 반드시 같아야 한다.
    • BiFunction 은 입력 매개변수가 2개인 Function 이다.

Operator를 제공하는 이유

  • Function<T, R> 와 BiFunction<T, U, R> 만으로도 사실상 거의 모든 함수형 연산을 구현할 수 있지만, 의도(목적)의 명시성, “가독성과 유지 보수성”을 위해 UnaryOperator<T> 와 BinaryOperator<T> 를 별도로 제공한다.
    • UnaryOperator<T> 입력과 출력 타입이 같고 단항 연산을 하는구나~
    • BinaryOperator<T> 입력과 출력 타입이 같고, 같은 타입 두 개를 입력받아 이항 연산을 수행하는 구나~
    • Function<T, T> 요렇게 타입 파라미터를 두 번 안적고 UnaryOperator<T> 요렇게 한 번만 적어도 된다.

기타 함수형 인터페이스

  • 매개 변수가 2개 필요한 경우에는 BiXxx 시리즈를 사용하면 된다. Bi는 Binary(이항, 둘)의 줄임말이다.
    • 예) BiFunction, BiConsumer, BiPredicate
    • BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
      • 물론 이 경우에는 BinaryOperator를 사용하는 것이 더 나은 선택이다.
    • BiPredicate<Integer, Integer> isGreater = (a, b) -> a > b;
    • Supplier 는 매개변수가 없으므로 BiSupplier 는 존재하지 않는다.
  • /입력값이 3개라면?
    • 입력값이 3개라면 TriXxx 가 있으면 좋겠지만, 이런 함수형 인터페이스는 기본으로 제공하지 않는다.
    • 만약 입력값이 3개일 경우라면 다음과 같이 직접 만들어서 사용하면 된다.
@FunctionalInterface
interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);
}

TriFunction<Integer, Integer, Integer, Integer> triFunction = (a, b, c) -> a + b + c;

  • /기본 함수형 인터페이스: 다음과 같이 기본형(primitive type)을 지원하는 함수형 인터페이스.
package java.util.function;

@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}
  • 기본형 지원 함수형 인터페이스가 존재하는 이유
    • 오토박싱/언박싱(auto-boxing/unboxing)으로 인한 성능 비용을 줄이기 위해
    • 자바 제네릭의 한계(제네릭은 primitive 타입을 직접 다룰 수 없음)를 극복하기 위해
      • 자바의 제네릭은 기본형(primitive) 타입을 직접 다룰 수 없어서, Function<int, R> 같은 식으로는 선언할 수 없다.

기본형 매개변수를 받는 인터페이스, 기본형을 반환하는 인터페이스, 기본형을 받고 반환하는 인터페이스 + 기본형 Operator 등이 존재한다.

// 기본형 매개변수, IntFunction, LongFunction, DoubleFunction
IntFunction<String> function = x -> "숫자: " + x;
System.out.println("function.apply(100) = " + function.apply(100));

// 기본형 반환, ToIntFunction, ToLongFunction, ToDoubleFunction
ToIntFunction<String> toIntFunction = s -> s.length();
System.out.println("toIntFunction = " +
        toIntFunction.applyAsInt("hello"));

// 기본형 매개변수, 기본형 반환
IntToLongFunction intToLongFunction = x -> x * 100L;
System.out.println("intToLongFunction = " +
        intToLongFunction.applyAsLong(10));

// IntUnaryOperator: int -> int
IntUnaryOperator intUnaryOperator = x -> x * 100;
System.out.println("intUnaryOperator = " +
        intUnaryOperator.applyAsInt(10));

// 기타, IntConsumer, IntSupplier, IntPredicate
  • 참고로 IntToIntFunction 은 없는데, IntUnaryOperator 를 사용하면 된다
  • IntConsumer, IntSupplier, IntPredicate와 같은 인터페이스도 존재한다.

기본, 특화 2개 알아두고, 나머지는 To를 붙이든 Bi를 붙이든 해가지고 연결해서 쓰면 된다.


Ref) 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 강의 | 김영한 - 인프런

'Java > Modern Java(8~)' 카테고리의 다른 글

[Java] 62. 메서드 참조  (0) 2025.07.04
[Java] 61. 람다 vs 익명 클래스  (0) 2025.07.04
[Java] 60. 람다 활용  (0) 2025.07.04
[Java] 58. 람다  (0) 2025.07.04
[Java] 57. 람다가 필요한 이유  (0) 2025.07.04
'Java/Modern Java(8~)' 카테고리의 다른 글
  • [Java] 61. 람다 vs 익명 클래스
  • [Java] 60. 람다 활용
  • [Java] 58. 람다
  • [Java] 57. 람다가 필요한 이유
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (457)
      • Software Development (27)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Test (8)
        • Code Style, Convetion (0)
        • DDD (0)
        • Software Engineering (18)
      • Java (71)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (12)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (1)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
        • Clean Code (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 59. 함수형 인터페이스
상단으로

티스토리툴바