공공부하자개발 · 영어 학습 노트
자바
고급모던 자바와 성능0/10 완료
  • 01제네릭과 와일드카드
  • 02멀티스레드와 동기화
  • 03람다식과 함수형 인터페이스
  • 04Stream API와 병렬 처리
  • 05Optional로 NPE 방지
  • 06메서드 활용 패턴 (고급)
  • 07Java 21 모던 문법
  • 08어노테이션·리플렉션·동적 프록시
  • 09CompletableFuture 심화와 가상 스레드 실전
  • 10JVM 메모리·GC·OOM 진단
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 고급 › 03 / 10

람다식과 함수형 인터페이스

섹션 7진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

예제 1: 전략 교체 — 결제 수단별 수수료 계산

if-else 사다리를 Map<Type, Function> 으로 바꿉니다. 새 결제 수단은 map 에 한 줄 추가면 끝입니다.

java
import java.util.*;
import java.util.function.*;

public class FeeStrategy {
    enum PayType { CARD, BANK, POINT, KAKAO }

    static final Map<PayType, LongUnaryOperator> FEE = new EnumMap<>(Map.<PayType, LongUnaryOperator>of(
        PayType.CARD,  amt -> amt * 3 / 100,                 // 3%
        PayType.BANK,  amt -> Math.min(500, amt / 100),      // 1%, 최대 500
        PayType.POINT, amt -> 0,                             // 무료
        PayType.KAKAO, amt -> amt * 25 / 1000 + 100          // 2.5% + 100원
    ));

    static long fee(PayType type, long amount) {
        return FEE.getOrDefault(type, amt -> { throw new IllegalArgumentException("unsupported"); })
                  .applyAsLong(amount);
    }

    public static void main(String[] args) {
        for (PayType t : PayType.values())
            System.out.printf("%-5s 10,000원 → 수수료 %d원%n", t, fee(t, 10_000));
    }
}
// 출력:
// CARD  10,000원 → 수수료 300원
// BANK  10,000원 → 수수료 100원
// POINT 10,000원 → 수수료 0원
// KAKAO 10,000원 → 수수료 350원

예제 2: 검증 체인 — Predicate 합성과 실패 사유 수집

주문 검증 규칙을 Predicate<Order> 와 메시지의 쌍으로 두고, and 로 합성하거나 하나씩 돌려 실패 사유를 모읍니다.

java
import java.util.*;
import java.util.function.Predicate;
import static java.util.function.Predicate.not;

public class ValidationChain {
    record Order(String id, long amount, String email, List<String> items) {}
    record Rule(String message, Predicate<Order> check) {}

    static final List<Rule> RULES = List.of(
        new Rule("금액은 양수",           o -> o.amount() > 0),
        new Rule("이메일 형식",           o -> o.email() != null && o.email().contains("@")),
        new Rule("상품 1개 이상",         not(o -> o.items().isEmpty())),
        new Rule("주문번호는 ORD- 로 시작", o -> o.id().startsWith("ORD-"))
    );

    static List<String> violations(Order o) {
        List<String> out = new ArrayList<>();
        for (Rule r : RULES) if (!r.check().test(o)) out.add(r.message());
        return out;
    }

    // 모든 규칙을 하나의 Predicate 로 합성
    static final Predicate<Order> ALL_VALID =
        RULES.stream().map(Rule::check).reduce(o -> true, Predicate::and);

    public static void main(String[] args) {
        Order good = new Order("ORD-1", 5000, "[email protected]", List.of("pen"));
        Order bad  = new Order("X-1", -10, "nomail", List.of());
        System.out.println(ALL_VALID.test(good) + " " + violations(good));
        System.out.println(ALL_VALID.test(bad) + " " + violations(bad));
    }
}
// 출력:
// true []
// false [금액은 양수, 이메일 형식, 상품 1개 이상, 주문번호는 ORD- 로 시작]

예제 3: 캡처 규칙과 this 차이 실험

effectively final 위반이 어디서 나는지, 람다와 익명 클래스의 this 가 무엇을 가리키는지 확인합니다.

java
import java.util.function.*;

public class CaptureDemo {
    String name = "outer";

    void run() {
        int base = 10;                                     // effectively final
        IntUnaryOperator addBase = x -> x + base;
        // base = 20;   // 주석 해제 시 위 람다에서 컴파일 에러

        int[] counter = {0};                               // 배열 요소 변경은 허용 (변수 재대입이 아님)
        Runnable inc = () -> counter[0]++;
        inc.run(); inc.run();

        Supplier<String> lambdaThis = () -> this.name;     // this = CaptureDemo 인스턴스
        Supplier<String> anonThis = new Supplier<>() {
            String name = "anon";
            public String get() { return this.name; }      // this = 익명 클래스 인스턴스
        };

        System.out.println(addBase.applyAsInt(5));
        System.out.println(counter[0]);
        System.out.println(lambdaThis.get() + " / " + anonThis.get());

        // 캡처 없는 람다는 같은 인스턴스가 재사용될 수 있다 (구현 세부, 의존 금지)
        Supplier<Integer> a = () -> 1, b = () -> 1;
        System.out.println(a == b);                        // 서로 다른 람다 식이라 false
        Runnable r1 = CaptureDemo::noop, r2 = CaptureDemo::noop;
        System.out.println(r1.getClass() == r2.getClass());// 같은 식이 아니어도 클래스는 다를 수 있음
    }
    static void noop() {}

    public static void main(String[] args) { new CaptureDemo().run(); }
}
// 출력:
// 15
// 2
// outer / anon
// false
// false

예제 4: 콜백과 이벤트 핸들러 — 결제 완료 후 후속 처리

결제 서비스가 완료 콜백(Consumer)과 실패 콜백을 받고, 여러 핸들러를 andThen 으로 묶어 등록합니다.

java
import java.util.*;
import java.util.function.*;

public class EventHandlers {
    record Payment(String orderId, long amount) {}

    static class PaymentService {
        private Consumer<Payment> onSuccess = p -> {};       // 초기값: 아무것도 안 함
        private BiConsumer<Payment, Exception> onFailure = (p, e) -> {};

        void addSuccessHandler(Consumer<Payment> h) { onSuccess = onSuccess.andThen(h); }
        void addFailureHandler(BiConsumer<Payment, Exception> h) { onFailure = onFailure.andThen(h); }

        void pay(Payment p) {
            try {
                if (p.amount() <= 0) throw new IllegalArgumentException("금액 오류: " + p.amount());
                onSuccess.accept(p);
            } catch (Exception e) {
                onFailure.accept(p, e);
            }
        }
    }

    public static void main(String[] args) {
        List<String> audit = new ArrayList<>();
        PaymentService svc = new PaymentService();
        svc.addSuccessHandler(p -> System.out.println("영수증 발송: " + p.orderId()));
        svc.addSuccessHandler(p -> audit.add("PAID " + p.orderId()));
        svc.addSuccessHandler(p -> System.out.println("포인트 적립: " + p.amount() / 100));
        svc.addFailureHandler((p, e) -> audit.add("FAIL " + p.orderId() + " " + e.getMessage()));

        svc.pay(new Payment("ORD-1", 25_000));
        svc.pay(new Payment("ORD-2", 0));
        System.out.println(audit);
    }
}
// 출력:
// 영수증 발송: ORD-1
// 포인트 적립: 250
// [PAID ORD-1, FAIL ORD-2 금액 오류: 0]

예제 5: Comparator 체인과 Function 파이프라인

주문 목록을 다단계 정렬하고, 설정에 따라 켜지는 후처리 단계를 Function 리스트로 접어 하나의 파이프라인으로 만듭니다.

java
import java.util.*;
import java.util.function.*;
import static java.util.Comparator.*;

public class Pipelines {
    record Order(String id, String status, long amount, String customer) {}

    public static void main(String[] args) {
        List<Order> orders = new ArrayList<>(List.of(
            new Order("O3", "PAID", 3000, "kim"),
            new Order("O1", "PAID", 9000, "lee"),
            new Order("O2", "PENDING", 9000, "kim"),
            new Order("O4", "PAID", 9000, "kim")
        ));

        // 상태 오름차순 → 금액 내림차순 → id 오름차순
        orders.sort(comparing(Order::status)
                .thenComparing(Order::amount, reverseOrder())
                .thenComparing(Order::id));
        orders.forEach(o -> System.out.println(o.status() + " " + o.amount() + " " + o.id()));

        // 문자열 파이프라인: 단계 목록을 하나의 Function 으로 합성
        List<UnaryOperator<String>> steps = List.of(String::strip, String::toLowerCase, s -> s.replace(' ', '-'));
        Function<String, String> slugify = steps.stream()
                .map(s -> (Function<String, String>) s)
                .reduce(Function.identity(), Function::andThen);
        System.out.println(slugify.apply("  Hello Lambda World  "));

        // compose vs andThen
        Function<Integer, Integer> twice = x -> x * 2, plus3 = x -> x + 3;
        System.out.println(twice.andThen(plus3).apply(5) + " " + twice.compose(plus3).apply(5));
    }
}
// 출력:
// PAID 9000 O1
// PAID 9000 O4
// PAID 3000 O3
// PENDING 9000 O2
// hello-lambda-world
// 13 16

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\advanced\03_lambda
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: 전략 교체 — 결제 수단별 수수료 계산
  • 예제 2: 검증 체인 — Predicate 합성과 실패 사유 수집
  • 예제 3: 캡처 규칙과 this 차이 실험
  • 예제 4: 콜백과 이벤트 핸들러 — 결제 완료 후 후속 처리
  • 예제 5: Comparator 체인과 Function 파이프라인
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제