[Java] Chapter 3. 람다표현식

SeungWoo Cha·2025년 9월 25일

[Java] 자바 인 액션

목록 보기
2/2

Chapter 3. 람다표현식

3.0. 서론

  • 이번 장에서는 람다 표현식을 어떻게 만들고, 사용하는지, 그리고 어떻게 코드를 간결하게 만들 수 있는지를 설명한다.
  • 더불어 자바 8 API에 추가된 중요한 인터페이스와 형식 추론 기능을 확인하고, 메서드 참조와 같은 기법도 다룬다.
  • 목표는 간결하고 유연한 코드를 작성하는 방법을 단계적으로 이해하는 것이다.

3.1. 람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이다.

3.1.1. 람다 특징

  1. 익명성 : 메서드와 달리 이름이 없다.
  2. 함수적 성격 : 클래스에 종속되지 않으며, 파라미터 리스트·바디·반환 형식·예외 리스트를 포함한다.
  3. 전달 가능 : 메서드 인수로 전달하거나 변수에 저장할 수 있다.
  4. 간결성 : 익명 클래스를 구현할 때보다 코드가 훨씬 간단하다.

3.1.2. 람다 구성 요소

  1. 파라미터 리스트 : 입력 값 정의
  2. 화살표(->) : 파라미터와 바디를 구분
  3. 람다 바디 : 실행할 동작, 또는 반환 값

3.1.3. 람다 예제

() -> {}
() -> "Real"
() -> { return "Mario"; }
  • return은 단일 표현식일 경우 생략 가능.
  • 블록 {}을 사용할 경우 반드시 return을 명시해야 한다.

3.2. 어디에, 어떻게 람다를 사용할까?

람다는 함수형 인터페이스라는 문맥에서만 사용할 수 있다.

3.2.1. 함수형 인터페이스

  • 추상 메서드가 단 하나만 있는 인터페이스를 말한다.
  • 디폴트 메서드가 여러 개 있어도 상관없다.
  • @FunctionalInterface 어노테이션을 붙이면 함수형 인터페이스임을 명시할 수 있으며, 규칙 위반 시 컴파일 오류가 발생한다.

3.2.2. 함수 디스크립터와 시그니처

  • 함수 시그니처 : 자바 코드 차원, "메서드 이름 + 매개변수 타입 목록"
  • 함수 디스크립터 : JVM 바이트코드 차원, "매개변수 타입 + 반환 타입"
  • 함수형 인터페이스의 추상 메서드 시그니처는 곧 람다 표현식의 시그니처이다.

3.3. 실행 어라운드 패턴

3.3.0. 개요

자원 처리(예: 파일 입출력, DB 연결)는

  • 자원 열기 → 작업 수행 → 자원 닫기
    구조를 반복한다.
    이러한 구조를 실행 어라운드 패턴이라고 한다.

3.3.1. 단계별 전개

  1. 1단계 – 동작 파라미터화 기억

    • 공통되는 준비와 정리 코드를 재사용하고, 실제 작업 코드만 파라미터화.
  2. 2단계 – 함수형 인터페이스 활용

    • 추상 메서드 시그니처에 맞는 인터페이스 정의 후 람다 전달.
  3. 3단계 – 동작 실행

    • 전달받은 람다를 실행. 원래는 익명 클래스를 통해 구현 가능.
  4. 4단계 – 람다 전달

    • 익명 클래스 대신 람다로 작성해 코드 간소화.

3.4. 함수형 인터페이스 사용

자바 8은 java.util.function 패키지에 다양한 함수형 인터페이스를 제공한다.

3.4.1. Predicate

  • 입력 T를 받아 불리언을 반환.
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) { ... }

3.4.2. Consumer

  • 입력 T를 받아 소비하고 반환 값 없음.
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c) { ... }

3.4.3. Function<T,R>

  • 입력 T를 받아 R을 반환.
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
public <T,R> List<R> map(List<T> list, Function<T,R> f) { ... }

3.4.4. 기본형 특화

  • 제네릭은 참조형만 사용 가능 → 기본형은 박싱/언박싱 발생.
  • 박싱된 값은 힙 메모리에 저장 → 비용 발생.
  • 자바는 IntPredicate, DoubleFunction기본형 특화 인터페이스 제공.

3.5. 형식 검사, 형식 추론, 제약

  • 컴파일러는 람다의 타입을 추론한다.
  • 단, 외부 변수를 사용할 때는 final 혹은 사실상 final이어야 한다.
  • 이는 메모리 안전성 때문. (지역 변수는 스택에, 람다는 힙에 저장되므로 수명 차이 발생)

3.6. 람다 캡처링과 클로저

3.6.1. 람다 캡처링

람다는 바깥 스코프에 정의된 변수를 자유 변수(free variable)로 참조할 수 있다.
이 과정을 람다 캡처링이라고 한다.

하지만, 자바에서는 지역 변수 캡처에 제약이 있다.

  • 지역 변수는 final 또는 사실상 final(값이 변하지 않는)이어야 한다.

  • 이는 메모리 안전성 때문이다.

    • 지역 변수는 메서드가 종료되면 스택 프레임과 함께 사라지지만,
    • 람다는 힙에 저장될 수 있어 수명이 길다.
    • 따라서 값이 바뀌면 동기화 문제나 예기치 못한 오류가 발생할 수 있다.

예시 1: final 또는 사실상 final 지역 변수

public class LambdaCaptureExample {
    public static void main(String[] args) {
        String greeting = "Hello"; // 사실상 final (값을 변경하지 않음)

        Runnable r = () -> System.out.println(greeting + ", Lambda!");
        r.run(); // 출력: Hello, Lambda!
    }
}
  • greeting은 한 번만 초기화되고 이후 변경되지 않으므로 사실상 final이다.
  • 따라서 람다에서 안전하게 캡처 가능하다.

예시 2: 지역 변수 변경 시 컴파일 오류

public class LambdaCaptureExample {
    public static void main(String[] args) {
        String message = "Hi";

        Runnable r = () -> System.out.println(message);

        // message = "Hello"; // 컴파일 오류: 변수는 final 또는 사실상 final이어야 함

        r.run();
    }
}
  • message를 람다에서 사용한 후 값을 바꾸려고 하면 컴파일 오류가 발생한다.

예시 3: 클래스 필드와 정적 변수는 변경 가능

public class LambdaCaptureExample {
    private String instanceVar = "Instance";
    private static String staticVar = "Static";

    public void test() {
        Runnable r = () -> {
            System.out.println(instanceVar); // 인스턴스 변수 캡처 가능
            System.out.println(staticVar);   // 정적 변수도 캡처 가능
        };

        r.run();
    }

    public static void main(String[] args) {
        new LambdaCaptureExample().test();
    }
}
  • 클래스 필드(instanceVar)와 정적 변수(staticVar)는 힙/메서드 영역에 저장되므로 자유롭게 변경 가능하다.
  • 따라서 지역 변수와 달리 final 제약이 없다.

3.6.2. 클로저

클로저(Closure)란 함수가 선언될 당시의 환경(변수 값 포함)을 함께 저장하는 개념이다.
람다는 클로저의 성격을 가지며, 실행 시점에도 선언 당시의 변수를 사용할 수 있다.

예시 4: 클로저 개념 확인

import java.util.function.Function;

public class ClosureExample {
    public static void main(String[] args) {
        int factor = 2;

        Function<Integer, Integer> multiplier = (x) -> x * factor;

        System.out.println(multiplier.apply(5)); // 출력: 10
    }
}
  • factor는 람다 외부에서 정의된 지역 변수지만, 람다 안에서 참조 가능하다.
  • 이 경우 multiplierfactor를 캡처하여 클로저 역할을 한다.

예시 5: 클로저와 지역 변수 변경 문제

import java.util.function.Supplier;

public class ClosureExample {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Hello");

        Supplier<String> supplier = () -> sb.toString();

        sb.append(" World"); // 변경 가능 (참조형 객체)

        System.out.println(supplier.get()); // 출력: Hello World
    }
}
  • 객체 참조(StringBuilder)는 final로 선언된 것이 아니라 객체 내부 상태를 바꾼 것이므로 허용된다.
  • 즉, "변수 참조"는 final이어야 하지만, "객체 내부 값"은 변경 가능하다.

요약

  1. 람다는 바깥 스코프의 변수를 자유롭게 참조할 수 있으며, 이를 람다 캡처링이라 한다.
  2. 지역 변수는 final 혹은 사실상 final이어야 한다.
  3. 인스턴스 변수와 정적 변수는 자유롭게 변경 가능하다.
  4. 람다는 선언 당시의 환경을 저장하여 실행 시점까지 활용할 수 있는데, 이를 클로저라고 한다.
  5. 참조형 객체의 내부 상태는 변경 가능하므로 주의해야 한다.
profile
한 발자국씩

0개의 댓글