[2026] Java 람다와 함수형 인터페이스 | Lambda Expression

[2026] Java 람다와 함수형 인터페이스 | Lambda Expression

이 글의 핵심

Java 람다와 함수형 인터페이스: Lambda Expression. 람다 표현식 (Lambda Expression)·함수형 인터페이스.

들어가며

람다는 메서드를 값처럼 넘기는 문법입니다. Stream API와 함께 쓰면 컨베이어 벨트 위에서 조건·변환을 짧게 연결하기 좋습니다.

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

1. 람다 표현식 (Lambda Expression)

기존 방식 vs 람다

람다는 익명 클래스를 훨씬 간결하게 표현합니다: 다음은 java를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import java.util.*;
// 기존 방식: 익명 클래스 (Java 7 이전)
Runnable r1 = new Runnable() {
    // Runnable 인터페이스를 구현하는 익명 클래스 생성
    @Override
    public void run() {
        // run() 메서드 구현
        System.out.println("Hello");
    }
};
// 문제점:
// - 코드가 장황함 (8줄)
// - 의도가 명확하지 않음 (보일러플레이트 코드가 많음)
// 람다 방식 (Java 8+)
Runnable r2 = () -> System.out.println("Hello");
// () : 매개변수 없음
// -> : 람다 연산자 (화살표)
// System.out.println("Hello") : 실행할 코드
// 
// 장점:
// - 간결함 (1줄)
// - 의도가 명확함 ("Hello를 출력하는 작업")
// 실행
r1.run();  // Hello
r2.run();  // Hello
// Comparator 예제: 문자열 정렬
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// 기존 방식: 익명 클래스
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        // compareTo: 사전순 비교
        // a < b → 음수, a == b → 0, a > b → 양수
        return a.compareTo(b);
    }
});
// 코드: 7줄, 의도: "이름순 정렬"
// 람다 방식
Collections.sort(names, (a, b) -> a.compareTo(b));
// (a, b) : 두 개의 매개변수 (타입 추론)
// -> a.compareTo(b) : 비교 로직
// 코드: 1줄, 의도: 명확
// 더 간결하게: 메서드 참조
names.sort(String::compareTo);
// String::compareTo : "String의 compareTo 메서드를 사용"
// 람다보다 더 간결하고 가독성 좋음
System.out.println(names);  // [Alice, Bob, Charlie]

람다의 장점:

  1. 간결성: 익명 클래스 대비 코드 양 90% 감소
  2. 가독성: 핵심 로직에 집중
  3. 함수형 프로그래밍: Stream API와 함께 사용
  4. 타입 추론: 컴파일러가 타입 자동 추론 언제 사용하나:
  • Stream API (filter, map, reduce)
  • 컬렉션 정렬 (sort)
  • 이벤트 핸들러
  • 비동기 작업 (CompletableFuture)

람다 문법

람다 표현식의 다양한 형태입니다: 다음은 java를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 1. 매개변수 없음
() -> System.out.println("Hello")
// () : 빈 괄호 필수 (매개변수 없음을 명시)
// -> : 람다 연산자
// System.out.println("Hello") : 실행할 표현식
// 2. 매개변수 1개
x -> x * x
// x : 매개변수 (타입 추론)
// 괄호 생략 가능 (매개변수가 1개일 때만)
// x * x : 반환값 (return 키워드 생략)
// 매개변수 1개 - 괄호 사용 (권장)
(x) -> x * x
// 가독성을 위해 괄호를 쓰는 것이 좋음
// 3. 매개변수 여러 개
(a, b) -> a + b
// (a, b) : 두 개의 매개변수 (괄호 필수)
// a + b : 반환값 (단일 표현식이면 return 생략)
// 4. 타입 명시 (타입 추론이 안 될 때)
(int a, int b) -> a + b
// int a, int b : 명시적 타입 선언
// 모든 매개변수의 타입을 명시하거나 모두 생략해야 함
// (int a, b) -> ....// ❌ 에러: 일부만 명시 불가
// 5. 여러 줄 (블록)
(a, b) -> {
    // 중괄호 {} 사용
    int sum = a + b;
    // 여러 문장 실행 가능
    System.out.println("합계: " + sum);
    // return 키워드 필수
    return sum * 2;
}
// 주의: 중괄호를 쓰면 return 키워드 필수
// 실전 예제
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 단일 표현식 (return 생략)
numbers.forEach(n -> System.out.println(n));
// 여러 줄 (return 필수)
numbers.stream()
    .map(n -> {
        int squared = n * n;
        System.out.println(n + "의 제곱: " + squared);
        return squared;
    })
    .collect(Collectors.toList());

람다 문법 규칙:

  1. 매개변수 0개: () 필수
  2. 매개변수 1개: () 생략 가능
  3. 매개변수 2개 이상: () 필수
  4. 단일 표현식: {}return 생략 가능
  5. 여러 문장: {}return 필수

2. 함수형 인터페이스

커스텀 함수형 인터페이스

다음은 java를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}
public class Main {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;
        Calculator subtract = (a, b) -> a - b;
        Calculator multiply = (a, b) -> a * b;
        Calculator divide = (a, b) -> a / b;
        
        System.out.println(add.calculate(10, 20));       // 30
        System.out.println(subtract.calculate(10, 20));  // -10
        System.out.println(multiply.calculate(10, 20));  // 200
        System.out.println(divide.calculate(10, 20));    // 0
    }
}

표준 함수형 인터페이스

Java는 자주 사용하는 함수형 인터페이스를 java.util.function 패키지에서 제공합니다: 다음은 java를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import java.util.function.*;
// 1. Predicate<T>: T -> boolean (조건 판별)
// test() 메서드: 입력을 받아 boolean 반환
Predicate<Integer> isEven = n -> n % 2 == 0;
// n % 2 == 0: 짝수 판별 (나머지가 0이면 짝수)
System.out.println(isEven.test(4));   // true (4는 짝수)
System.out.println(isEven.test(5));   // false (5는 홀수)
// 실전 사용: Stream filter
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evens = numbers.stream()
    .filter(isEven)  // Predicate를 filter에 전달
    .collect(Collectors.toList());
System.out.println(evens);  // [2, 4]
// 2. Function<T, R>: T -> R (변환)
// apply() 메서드: 입력 T를 받아 출력 R 반환
Function<String, Integer> length = s -> s.length();
// 문자열을 받아 길이(정수)를 반환
System.out.println(length.apply("Hello"));    // 5
System.out.println(length.apply("World"));    // 5
// 실전 사용: Stream map
List<String> words = Arrays.asList("a", "bb", "ccc");
List<Integer> lengths = words.stream()
    .map(length)  // Function을 map에 전달
    .collect(Collectors.toList());
System.out.println(lengths);  // [1, 2, 3]
// 3. Consumer<T>: T -> void (소비)
// accept() 메서드: 입력을 받아 처리 (반환값 없음)
Consumer<String> print = s -> System.out.println(s);
print.accept("Hello");  // Hello 출력
print.accept("World");  // World 출력
// 실전 사용: forEach
List<String> names = Arrays.asList("홍길동", "김철수");
names.forEach(print);  // 각 이름 출력
// 4. Supplier<T>: () -> T (공급)
// get() 메서드: 매개변수 없이 값을 생성하여 반환
Supplier<Double> random = () -> Math.random();
// 호출할 때마다 새 난수 생성
System.out.println(random.get());  // 0.123456...
System.out.println(random.get());  // 0.789012....(다른 값)
// 실전 사용: 지연 초기화
Supplier<List<String>> listSupplier = () -> new ArrayList<>();
List<String> list = listSupplier.get();  // 필요할 때 생성
// 5. BiFunction<T, U, R>: (T, U) -> R (두 입력 → 한 출력)
// apply() 메서드: 두 입력을 받아 하나의 출력 반환
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(10, 20));  // 30
System.out.println(add.apply(5, 15));   // 20
// 실전 사용: reduce 초기값과 함께
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
int sum = nums.stream()
    .reduce(0, add)  // BiFunction을 reduce에 전달
    .intValue();
System.out.println(sum);  // 15

표준 함수형 인터페이스 요약:

인터페이스메서드시그니처용도
Predicate<T>test()T -> boolean조건 판별 (filter)
Function<T,R>apply()T -> R변환 (map)
Consumer<T>accept()T -> void소비 (forEach)
Supplier<T>get()() -> T생성 (lazy init)
BiFunction<T,U,R>apply()(T,U) -> R두 입력 변환
변형 인터페이스:
  • BiPredicate<T, U>: 두 입력 → boolean
  • BiConsumer<T, U>: 두 입력 → void
  • UnaryOperator<T>: T → T (Function의 특수 케이스)
  • BinaryOperator<T>: (T, T) → T (BiFunction의 특수 케이스)

3. 메서드 참조 (Method Reference)

4가지 유형

다음은 java를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import java.util.*;
List<String> names = Arrays.asList("홍길동", "김철수", "이영희");
// 1. 정적 메서드 참조
Function<String, Integer> parseInt = Integer::parseInt;
System.out.println(parseInt.apply("123"));  // 123
// 2. 인스턴스 메서드 참조
String str = "Hello";
Supplier<String> upper = str::toUpperCase;
System.out.println(upper.get());  // HELLO
// 3. 특정 타입의 임의 객체 메서드 참조
Function<String, String> toUpper = String::toUpperCase;
System.out.println(toUpper.apply("hello"));  // HELLO
// 4. 생성자 참조
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
ArrayList<String> list = listSupplier.get();

실전 활용

아래 코드는 java를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

List<String> names = Arrays.asList("홍길동", "김철수", "이영희");
// 람다
names.forEach(name -> System.out.println(name));
// 메서드 참조 (더 간결)
names.forEach(System.out::println);
// 정렬
names.sort((a, b) -> a.compareTo(b));
names.sort(String::compareTo);

4. 실전 예제

예제: 사용자 필터링

다음은 java를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import java.util.*;
import java.util.function.*;
import java.util.stream.*;
class User {
    String name;
    int age;
    
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class Main {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("홍길동", 25),
            new User("김철수", 17),
            new User("이영희", 30)
        );
        
        // Predicate로 필터링
        Predicate<User> isAdult = u -> u.age >= 18;
        
        List<User> adults = users.stream()
            .filter(isAdult)
            .collect(Collectors.toList());
        
        // Function으로 변환
        Function<User, String> getName = u -> u.name;
        
        List<String> names = adults.stream()
            .map(getName)
            .collect(Collectors.toList());
        
        System.out.println(names);  // [홍길동, 이영희]
    }
}

예제: 계산기

다음은 java를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import java.util.function.BiFunction;
public class Calculator {
    public static void main(String[] args) {
        BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
        BiFunction<Integer, Integer, Integer> subtract = (a, b) -> a - b;
        BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
        BiFunction<Integer, Integer, Integer> divide = (a, b) -> a / b;
        
        int a = 10, b = 5;
        
        System.out.println("덧셈: " + calculate(a, b, add));
        System.out.println("뺄셈: " + calculate(a, b, subtract));
        System.out.println("곱셈: " + calculate(a, b, multiply));
        System.out.println("나눗셈: " + calculate(a, b, divide));
    }
    
    static int calculate(int a, int b, BiFunction<Integer, Integer, Integer> op) {
        return op.apply(a, b);
    }
}

정리

핵심 요약

  1. 람다: (매개변수) -> 표현식
  2. 함수형 인터페이스: 추상 메서드 1개, @FunctionalInterface
  3. 메서드 참조: ::로 간결하게
  4. 표준 인터페이스: Predicate, Function, Consumer, Supplier
  5. 활용: Stream API와 함께 사용

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3