3주차 Unit 10.1 — 람다 표현식

Psj·2026년 5월 20일

F-lab

목록 보기
117/230

Unit 10.1 — 람다 표현식

F-LAB JAVA · 3주차 · Phase 10 · 함수형 프로그래밍
🚀 Phase 10 시작 — 3주차 마지막 Phase 진입


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 람다 표현식 의 정의와 등장 배경은?
  • 함수형 인터페이스 의 정의와 @FunctionalInterface 의 역할은?
  • java.util.function 의 4가지 핵심 (Function, Consumer, Supplier, Predicate) 은?
  • 메서드 참조 의 4가지 종류는?
  • 람다 vs 익명 클래스 의 차이는?
  • this 키워드 의 의미 (람다 vs 익명 클래스)는?
  • 변수 캡처effectively final 의 의미는?
  • 람다의 내부 구현 (invokedynamic) 은?
  • 람다의 성능 은?

🎯 핵심 한 문장

람다 표현식 (Lambda Expression) 은 Java 8+ 의 핵심 기능으로, 함수형 인터페이스 (단 하나의 추상 메서드를 가진 인터페이스) 의 구현을 간결한 표현식 으로 작성하는 방법이다.
(매개변수) -> { 본문 } 형식으로, 익명 클래스의 보일러플레이트를 제거 하고 함수를 일급 시민 처럼 다룰 수 있게 한다.
java.util.function 패키지의 4가지 핵심 (Function, Consumer, Supplier, Predicate) 이 대부분의 시나리오를 커버.
메서드 참조 (String::length) 는 람다의 더 간결한 표현, 4가지 종류 (정적/인스턴스/특정/생성자).
변수 캡처effectively final (실질적 final) 만 가능, this 는 람다 외부의 this (익명 클래스와 정반대).
내부적으로 invokedynamic 으로 구현되어 익명 클래스보다 가볍고 빠름.

비유 — 위임장

익명 클래스 (옛):
  "이 일을 처리할 사람" 을 매번 만들기
  - 회사 이름 + 부서 + 직급 + 이름 + 일 설명
  - 매우 형식적
  - 길고 복잡

람다 (Java 8+):
  단순 위임장: "이 일을 해주세요"
  - 일 설명만
  - 간결
  - 핵심에 집중

함수형 인터페이스:
  "1가지 일만 위임" 의 약속
  - SAM (Single Abstract Method)
  - 1가지 일 = 람다로 표현

메서드 참조:
  이미 있는 사람 가리키기
  - "저기 김 부장한테 부탁"
  - 새로 만들 필요 X

→ 람다 = 함수형 인터페이스 + 간결.


🧭 9개 섹션 로드맵

1. 람다 표현식의 정의와 문법
2. 함수형 인터페이스
3. java.util.function 의 4가지 핵심
4. 메서드 참조의 4가지 종류
5. 람다 vs 익명 클래스
6. this 키워드의 의미
7. 변수 캡처와 effectively final
8. 람다의 내부 구현과 성능
9. 면접 + 자기 점검

1️⃣ 람다 표현식의 정의와 문법

1.1 람다의 정의

람다 표현식 (Lambda Expression):

  익명 함수의 간결한 표현.
  함수형 인터페이스의 구현을 표현식으로.

Java 8+ (2014) 도입.

문법:
  (매개변수) -> { 본문 }

3가지 부분:
  1. 매개변수 (파라미터)
  2. 화살표 (->)
  3. 본문 (식 또는 블록)

1.2 등장 배경

람다 등장 전 (Java 7 이하):

// 익명 클래스로 인터페이스 구현
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// 문제:
// - 너무 길다
// - 보일러플레이트 (의식적 코드)
// - 핵심은 "Hello 출력" 뿐
// - 익명 클래스 = "객체 만들기" 강조
// - 의도는 "동작 전달"

람다 등장 후 (Java 8+):

Runnable r = () -> System.out.println("Hello");
// 간결
// 핵심에 집중
// 동작 전달의 의도 명확

1.3 다양한 람다 문법

// 매개변수 0개
Runnable r1 = () -> System.out.println("Hello");

// 매개변수 1개 (괄호 생략 가능)
Consumer<String> c1 = s -> System.out.println(s);
Consumer<String> c2 = (s) -> System.out.println(s);   // 같음

// 매개변수 여러 개
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// 본문 1개 식 (return 생략)
Function<Integer, Integer> square = x -> x * x;
// 같은 표현:
Function<Integer, Integer> square2 = x -> { return x * x; };

// 본문 여러 줄 (중괄호 + return 필요)
Function<Integer, Integer> complex = x -> {
    int y = x * 2;
    int z = y + 1;
    return z * z;
};

// 매개변수 타입 명시 (필요시)
BiFunction<Integer, Integer, Integer> add2 = (Integer a, Integer b) -> a + b;
// 보통 추론 가능 → 생략

1.4 문법 규칙

괄호 규칙:
  매개변수 0개 → () 필수
  매개변수 1개 + 타입 생략 → () 생략 가능
  매개변수 1개 + 타입 명시 → () 필수
  매개변수 여러 개 → () 필수

중괄호 규칙:
  본문 1개 식 → {} 생략 가능 (return 도 생략)
  본문 여러 줄 → {} 필수 (return 도 명시)

타입 추론:
  - 매개변수 타입 보통 생략 가능
  - 컨텍스트에서 추론

1.5 시각화

변환 과정:

옛 (익명 클래스):
  Comparator<String> comp = new Comparator<String>() {
      @Override
      public int compare(String a, String b) {
          return a.length() - b.length();
      }
  };

→ 익명 클래스 골격 제거:
  Comparator<String> comp = (String a, String b) -> {
      return a.length() - b.length();
  };

→ 타입 추론:
  Comparator<String> comp = (a, b) -> {
      return a.length() - b.length();
  };

→ 본문 단순화:
  Comparator<String> comp = (a, b) -> a.length() - b.length();

같은 동작, 80% 짧음

1.6 활용 예

// 1. 리스트 정렬
List<String> names = new ArrayList<>(List.of("Bob", "Alice", "Charlie"));

names.sort((a, b) -> a.compareTo(b));
// 또는 메서드 참조
names.sort(String::compareTo);

// 2. forEach
names.forEach(name -> System.out.println(name));
names.forEach(System.out::println);

// 3. Thread
new Thread(() -> {
    System.out.println("Running");
}).start();

// 4. Stream
List<Integer> doubled = numbers.stream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

// 5. 함수형 인터페이스
Function<Integer, String> intToStr = i -> "Value: " + i;

1.7 ILIC 활용

public class ShipmentExamples {
    
    private final List<Shipment> shipments = new ArrayList<>();
    
    // 1. 정렬
    public void sortByWeight() {
        shipments.sort((a, b) -> a.getWeight().compareTo(b.getWeight()));
        // 또는
        shipments.sort(Comparator.comparing(Shipment::getWeight));
    }
    
    // 2. 필터링
    public List<Shipment> findUrgent() {
        return shipments.stream()
            .filter(s -> s.isUrgent())
            .toList();
    }
    
    // 3. 변환
    public List<String> extractBlNos() {
        return shipments.stream()
            .map(s -> s.getBlNo())
            .toList();
    }
    
    // 4. 콜백
    public void processAsync(Runnable callback) {
        // 비동기 처리 후
        callback.run();
    }
    
    public void useCallback() {
        processAsync(() -> {
            System.out.println("Done");
        });
    }
    
    // 5. 이벤트
    public void registerListener() {
        shipmentRepository.onSave(s -> {
            log.info("Saved: {}", s.getId());
        });
    }
}

1.8 자기 점검 답변

람다 표현식의 정의와 문법은?

:
1. 정의:

  • 익명 함수의 간결한 표현
  • 함수형 인터페이스 구현
  • Java 8+
  1. 문법:

    • (매개변수) -> { 본문 }
    • 매개변수 / 화살표 / 본문
  2. 문법 규칙:

    • 매개변수 1개 + 타입 생략: () 생략 가능
    • 본문 1개 식: {} return 생략 가능
    • 타입: 보통 추론 가능
  3. 목적:

    • 보일러플레이트 제거
    • 함수형 프로그래밍
    • 가독성
  4. 활용:

    • 정렬, 필터링
    • forEach, Stream
    • 콜백, 이벤트

2️⃣ 함수형 인터페이스

2.1 함수형 인터페이스의 정의

함수형 인터페이스 (Functional Interface):

  단 하나의 추상 메서드 (SAM) 를 가진 인터페이스.
  
  SAM (Single Abstract Method)

조건:
  - 추상 메서드 정확히 1개
  - default 메서드는 여러 개 OK
  - static 메서드는 여러 개 OK
  - Object 의 메서드 (equals, hashCode 등) 는 제외

Java 8+ 도입.

2.2 @FunctionalInterface 어노테이션

@FunctionalInterface
public interface MyFunction {
    void execute();   // 유일한 추상 메서드
    
    // default 메서드 OK
    default void log() {
        System.out.println("Executed");
    }
    
    // static 메서드 OK
    static MyFunction nothing() {
        return () -> {};
    }
}

// @FunctionalInterface 의 역할:
// 1. 의도 명시 ("함수형 인터페이스")
// 2. 컴파일러 검증 (SAM 위반 시 에러)
// 3. 문서화

2.3 자주 만들어진 함수형 인터페이스

// Java 표준 함수형 인터페이스 (Java 1.0+ 부터)
public interface Runnable {
    void run();
}

public interface Comparable<T> {
    int compareTo(T o);
}

public interface Comparator<T> {
    int compare(T o1, T o2);
}

public interface Callable<V> {
    V call() throws Exception;
}

public interface ActionListener {
    void actionPerformed(ActionEvent e);
}

// 모두 SAM
// 람다로 구현 가능

2.4 람다와 함수형 인터페이스

// 람다는 함수형 인터페이스의 인스턴스
Runnable r = () -> System.out.println("Hello");
// r 의 타입: Runnable

// 메서드 매개변수
public void runAsync(Runnable task) {
    new Thread(task).start();
}

runAsync(() -> System.out.println("Async"));

// 반환
public Runnable createTask(String msg) {
    return () -> System.out.println(msg);
}

Runnable t = createTask("Hello");
t.run();   // "Hello"

2.5 람다의 타입

// 같은 람다, 다른 인터페이스 타입
Runnable r = () -> System.out.println("Hello");
// 타입: Runnable

ActionListener al = () -> System.out.println("Hello");
// ❌ ActionListener.actionPerformed 는 매개변수 1개

// 람다의 타입은 컨텍스트로 결정
Object obj = () -> System.out.println("Hello");
// ❌ Object 는 함수형 인터페이스 X
// "target type X" 컴파일 에러

// 명시적 캐스팅
Object obj = (Runnable) () -> System.out.println("Hello");
// OK

2.6 함수형 인터페이스 만들기

// 사용자 정의 함수형 인터페이스
@FunctionalInterface
public interface ShipmentValidator {
    boolean isValid(Shipment shipment);
    
    // default 메서드 추가 OK
    default ShipmentValidator and(ShipmentValidator other) {
        return s -> this.isValid(s) && other.isValid(s);
    }
}

// 활용
ShipmentValidator hasBlNo = s -> s.getBlNo() != null;
ShipmentValidator hasWeight = s -> s.getWeight() != null;

ShipmentValidator complete = hasBlNo.and(hasWeight);

if (complete.isValid(shipment)) {
    // 처리
}

2.7 표준 라이브러리 (Java 8+)

java.util.function 패키지:

기본:
  - Function<T, R>
  - Consumer<T>
  - Supplier<T>
  - Predicate<T>

다중 매개변수:
  - BiFunction<T, U, R>
  - BiConsumer<T, U>
  - BiPredicate<T, U>

기본 타입 (boxing 방지):
  - IntFunction, IntConsumer 등
  - LongFunction 등
  - DoubleFunction 등

특수:
  - UnaryOperator<T> = Function<T, T>
  - BinaryOperator<T> = BiFunction<T, T, T>

다음 섹션에서 정밀.

2.8 ILIC 활용

// 1. 사용자 정의 함수형 인터페이스
@FunctionalInterface
public interface ShipmentPriceCalculator {
    BigDecimal calculate(Shipment shipment);
    
    default ShipmentPriceCalculator withDiscount(BigDecimal rate) {
        return s -> this.calculate(s).multiply(BigDecimal.ONE.subtract(rate));
    }
}

// 활용
ShipmentPriceCalculator basePrice = s -> 
    s.getWeight().multiply(BigDecimal.valueOf(1000));

ShipmentPriceCalculator vipPrice = basePrice.withDiscount(BigDecimal.valueOf(0.1));

BigDecimal price = vipPrice.calculate(shipment);

// 2. 콜백
@FunctionalInterface
public interface ShipmentEventListener {
    void onEvent(ShipmentEvent event);
}

public void registerListener(ShipmentEventListener listener) {
    listeners.add(listener);
}

// 사용
registerListener(event -> log.info("Event: {}", event));

2.9 자기 점검 답변

함수형 인터페이스의 정의는?

:
1. 정의:

  • SAM (Single Abstract Method)
  • 추상 메서드 정확히 1개
  • default/static 은 여러 개 OK
  1. @FunctionalInterface:

    • 의도 명시
    • 컴파일러 검증
    • 선택 (없어도 함수형 가능)
  2. 자주 만들어진:

    • Runnable, Comparable, Comparator
    • Callable
    • 모두 SAM
  3. 람다와 관계:

    • 람다는 함수형 인터페이스 인스턴스
    • 컨텍스트로 타입 결정
    • Object 는 X (SAM 아님)
  4. 표준 (java.util.function):

    • Function, Consumer, Supplier, Predicate
    • 다음 섹션

3️⃣ java.util.function 의 4가지 핵심

3.1 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) { ... }
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { ... }
    static <T> Function<T, T> identity() { ... }
}

// 활용
Function<String, Integer> length = s -> s.length();
length.apply("Hello");   // 5

Function<Integer, Integer> square = x -> x * x;
square.apply(5);   // 25

// 합성
Function<String, Integer> lengthSquared = length.andThen(square);
lengthSquared.apply("Hello");   // 25 (length 5 → square 25)

// identity
Function<String, String> id = Function.identity();
id.apply("Hello");   // "Hello"

3.2 Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) { ... }
}

// 활용
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello");

Consumer<Integer> doubler = x -> System.out.println(x * 2);

// 합성
Consumer<String> both = printer.andThen(s -> System.out.println("done"));
both.accept("Hello");
// 출력:
// Hello
// done

// forEach 에서 활용
List.of("A", "B", "C").forEach(printer);
// A
// B
// C

3.3 Supplier

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

// 활용
Supplier<String> hello = () -> "Hello";
hello.get();   // "Hello"

Supplier<List<String>> emptyList = () -> new ArrayList<>();
List<String> list = emptyList.get();

// 지연 평가
public void log(Supplier<String> messageSupplier) {
    if (isDebugEnabled()) {
        System.out.println(messageSupplier.get());
        // 실제 호출 시점에만 메시지 생성
    }
}

log(() -> "Debug: " + expensiveOperation());
// expensiveOperation 은 isDebugEnabled 가 true 일 때만

3.4 Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    
    default Predicate<T> and(Predicate<? super T> other) { ... }
    default Predicate<T> or(Predicate<? super T> other) { ... }
    default Predicate<T> negate() { ... }
    static <T> Predicate<T> isEqual(Object target) { ... }
    static <T> Predicate<T> not(Predicate<? super T> target) { ... }
}

// 활용
Predicate<String> isEmpty = s -> s.isEmpty();
isEmpty.test("");   // true
isEmpty.test("Hello");   // false

Predicate<Integer> isPositive = x -> x > 0;
Predicate<Integer> isEven = x -> x % 2 == 0;

// 결합
Predicate<Integer> isPositiveEven = isPositive.and(isEven);
isPositiveEven.test(4);   // true
isPositiveEven.test(-4);   // false (음수)

Predicate<Integer> isNotPositive = isPositive.negate();
isNotPositive.test(-5);   // true

// Stream 에서 활용
List<Integer> positives = numbers.stream()
    .filter(isPositive)
    .toList();

3.5 다중 매개변수

// BiFunction<T, U, R>
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
add.apply(2, 3);   // 5

// BiConsumer<T, U>
BiConsumer<String, Integer> printer = (name, age) -> 
    System.out.println(name + ": " + age);
printer.accept("Alice", 30);

// BiPredicate<T, U>
BiPredicate<String, String> startsWith = (s, prefix) -> s.startsWith(prefix);
startsWith.test("Hello", "He");   // true

3.6 기본 타입 특수화

// Boxing 회피를 위한 특수화

// IntFunction<R>
IntFunction<String> intToStr = i -> "Number: " + i;
intToStr.apply(42);

// IntConsumer
IntConsumer printer = i -> System.out.println(i);
printer.accept(42);

// IntSupplier
IntSupplier random = () -> ThreadLocalRandom.current().nextInt();
random.getAsInt();

// IntPredicate
IntPredicate isPositive = i -> i > 0;
isPositive.test(5);

// 비슷한 Long, Double 도 있음

// 장점:
// - 기본 타입 직접 (boxing X)
// - 성능 ↑
// - Stream API 에서 활용 (IntStream 등)

3.7 4가지 핵심 비교

인터페이스입력출력메서드의미
Function<T,R>TRapply변환
ConsumerTvoidaccept소비
Supplier없음Tget생성
PredicateTbooleantest판단

3.8 UnaryOperator, BinaryOperator

// UnaryOperator<T> extends Function<T, T>
UnaryOperator<Integer> doubler = x -> x * 2;
doubler.apply(5);   // 10

// BinaryOperator<T> extends BiFunction<T, T, T>
BinaryOperator<Integer> add = (a, b) -> a + b;
add.apply(2, 3);   // 5

// 활용
List.of(1, 2, 3).stream().reduce(0, add);   // 6

// 같은 타입 일 때 더 명확

3.9 ILIC 활용

public class ShipmentFunctional {
    
    private final List<Shipment> shipments = new ArrayList<>();
    
    // 1. Function — 변환
    Function<Shipment, String> toBlNo = s -> s.getBlNo();
    Function<Shipment, BigDecimal> toWeight = s -> s.getWeight();
    
    public List<String> getBlNos() {
        return shipments.stream().map(toBlNo).toList();
    }
    
    // 2. Consumer — 처리
    Consumer<Shipment> logger = s -> log.info("Shipment: {}", s.getId());
    Consumer<Shipment> notifier = s -> notificationService.send(s);
    
    public void processShipment(Shipment s) {
        logger.andThen(notifier).accept(s);
    }
    
    // 3. Supplier — 생성
    Supplier<Shipment> emptyShipment = () -> Shipment.builder().build();
    Supplier<LocalDateTime> now = LocalDateTime::now;   // 메서드 참조
    
    public Shipment createDefault() {
        return emptyShipment.get();
    }
    
    // 4. Predicate — 필터
    Predicate<Shipment> isUrgent = s -> s.isUrgent();
    Predicate<Shipment> isHeavy = s -> s.getWeight().compareTo(BigDecimal.valueOf(1000)) > 0;
    Predicate<Shipment> isUrgentAndHeavy = isUrgent.and(isHeavy);
    
    public List<Shipment> findUrgentHeavy() {
        return shipments.stream()
            .filter(isUrgentAndHeavy)
            .toList();
    }
    
    // 5. BiFunction — 두 객체 비교
    BiFunction<Shipment, Shipment, Integer> compareByWeight = 
        (a, b) -> a.getWeight().compareTo(b.getWeight());
    
    public void sortByWeight() {
        shipments.sort(compareByWeight::apply);
        // Comparator 와 BiFunction 비슷
    }
}

3.10 자기 점검 답변

java.util.function 의 4가지 핵심은?

:
1. Function<T, R>:

  • apply(T) → R
  • 변환
  1. Consumer:

    • accept(T) → void
    • 소비
  2. Supplier:

    • get() → T
    • 생성
  3. Predicate:

    • test(T) → boolean
    • 판단

확장:

  • BiFunction, BiConsumer, BiPredicate
  • IntXxx, LongXxx, DoubleXxx (특수화)
  • UnaryOperator, BinaryOperator

4️⃣ 메서드 참조의 4가지 종류

4.1 메서드 참조의 정의

메서드 참조 (Method Reference):

  이미 존재하는 메서드를 람다로 사용하는 방법.
  더 간결한 표현.

문법: ClassName::methodName

4가지 종류:
  1. 정적 메서드 참조
  2. 인스턴스 메서드 참조 (특정 객체)
  3. 인스턴스 메서드 참조 (임의 객체)
  4. 생성자 참조

4.2 정적 메서드 참조

// 람다:
Function<Integer, String> intToStr = i -> Integer.toString(i);

// 메서드 참조:
Function<Integer, String> intToStr2 = Integer::toString;
// 같은 동작, 더 간결

// 호출:
intToStr2.apply(42);   // "42"

// 활용:
List<Integer> nums = List.of(1, 2, 3);
List<String> strs = nums.stream()
    .map(Integer::toString)
    .toList();

4.3 인스턴스 메서드 참조 (특정 객체)

// 람다:
String s = "Hello";
Supplier<Integer> getLength = () -> s.length();

// 메서드 참조:
Supplier<Integer> getLength2 = s::length;
// s 의 length 메서드 참조

// 호출:
getLength2.get();   // 5

// 활용:
PrintStream out = System.out;
Consumer<String> printer = out::println;
// out.println 의 참조

List.of("A", "B").forEach(printer);
// 출력:
// A
// B

4.4 인스턴스 메서드 참조 (임의 객체)

// 람다:
Function<String, Integer> length = s -> s.length();

// 메서드 참조:
Function<String, Integer> length2 = String::length;
// String 클래스의 length 메서드
// 받은 객체에서 호출

// 호출:
length2.apply("Hello");   // 5

// 차이:
// - 특정 객체: s::length (s 의 length)
// - 임의 객체: String::length (받은 String 의 length)

// 활용:
List<String> names = List.of("Alice", "Bob", "Charlie");
names.stream()
    .map(String::length)   // 임의 String 의 length
    .forEach(System.out::println);

4.5 생성자 참조

// 람다:
Supplier<List<String>> newList = () -> new ArrayList<>();

// 생성자 참조:
Supplier<List<String>> newList2 = ArrayList::new;
// ArrayList 의 생성자 참조

// 호출:
List<String> list = newList2.get();

// 매개변수가 있는 생성자
Function<Integer, ArrayList<String>> newListSize = ArrayList::new;
List<String> list2 = newListSize.apply(100);
// new ArrayList<>(100)

// 활용:
Stream.of("A", "B", "C")
    .collect(Collectors.toCollection(ArrayList::new));
// 새 ArrayList 생성하여 수집

4.6 4가지 종류 비교

종류문법람다 등가활용
정적 메서드ClassName::staticMethod(args) -> ClassName.staticMethod(args)Integer::parseInt
인스턴스 (특정)instance::method(args) -> instance.method(args)System.out::println
인스턴스 (임의)ClassName::instanceMethod(obj, args) -> obj.instanceMethod(args)String::length
생성자ClassName::new(args) -> new ClassName(args)ArrayList::new

4.7 활용 예

// 1. Stream 정렬
List<Person> people = ...;
people.sort(Comparator.comparing(Person::getAge));
// vs
people.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()));

// 2. Stream 변환
List<String> names = people.stream()
    .map(Person::getName)   // 임의 객체의 메서드
    .toList();

// 3. forEach
list.forEach(System.out::println);   // 특정 객체

// 4. 컬렉터
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::getCity));

// 5. 생성자
Stream.generate(ArrayList::new)
    .limit(10)
    .toList();

// 6. 정적 메서드
List<Integer> nums = strs.stream()
    .map(Integer::parseInt)
    .toList();

4.8 람다 vs 메서드 참조 선택

람다 권장:
  ✓ 추가 로직 있음
  ✓ 메서드 호출 외 작업
  ✓ 짧은 인라인 코드

메서드 참조 권장:
  ✓ 단순 메서드 호출
  ✓ 의미 명확
  ✓ 더 간결

예:
  x -> x.toString()              → Object::toString
  s -> s.toUpperCase().trim()    → 람다 (체이닝)
  x -> System.out.println(x)     → System.out::println
  (a, b) -> a + b                → 람다 (연산자, 메서드 X)
  Integer::sum                    → 메서드 참조 (Integer 에 sum 정적 메서드)

4.9 ILIC 활용

public class ShipmentMethodRefs {
    
    private final List<Shipment> shipments = new ArrayList<>();
    
    // 1. 정적 메서드
    public List<Long> parseIds(List<String> strIds) {
        return strIds.stream()
            .map(Long::parseLong)   // Long.parseLong
            .toList();
    }
    
    // 2. 인스턴스 (특정 객체)
    public void logAll() {
        Logger logger = LoggerFactory.getLogger(getClass());
        shipments.forEach(logger::info);   // logger 의 info
        // 또는
        shipments.stream()
            .map(Shipment::toString)
            .forEach(System.out::println);   // System.out 의 println
    }
    
    // 3. 인스턴스 (임의 객체)
    public List<String> getBlNos() {
        return shipments.stream()
            .map(Shipment::getBlNo)   // 임의 Shipment 의 getBlNo
            .toList();
    }
    
    public List<Shipment> sortByDate() {
        return shipments.stream()
            .sorted(Comparator.comparing(Shipment::getCreatedAt))
            .toList();
    }
    
    // 4. 생성자
    public List<Shipment> createBatch(List<String> blNos) {
        return blNos.stream()
            .map(blNo -> {
                Shipment s = new Shipment();
                s.setBlNo(blNo);
                return s;
            })
            .toList();
    }
    
    // 5. 컬렉터 + 생성자
    public TreeSet<Shipment> getUniqueShipments() {
        return shipments.stream()
            .collect(Collectors.toCollection(TreeSet::new));
        // TreeSet::new 가 생성자 참조
    }
}

4.10 자기 점검 답변

메서드 참조의 4가지 종류는?

:
1. 정적 메서드 참조:

  • ClassName::staticMethod
  • 예: Integer::parseInt
  1. 인스턴스 메서드 (특정 객체):

    • instance::method
    • 예: System.out::println
  2. 인스턴스 메서드 (임의 객체):

    • ClassName::instanceMethod
    • 예: String::length
  3. 생성자 참조:

    • ClassName::new
    • 예: ArrayList::new

선택:

  • 단순 메서드 호출 → 메서드 참조
  • 추가 로직 → 람다

5️⃣ 람다 vs 익명 클래스

5.1 표면적 차이

// 익명 클래스
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// 람다
Runnable r2 = () -> System.out.println("Hello");

// 같은 동작?
// → 표면적으로는 그렇게 보이지만 내부는 다름

5.2 차이 1 — this 의 의미

public class Demo {
    private String name = "Demo";
    
    public void test() {
        // 익명 클래스의 this — 익명 클래스 자신
        Runnable r1 = new Runnable() {
            String name = "Anonymous";
            
            @Override
            public void run() {
                System.out.println(this.name);   // "Anonymous"
                System.out.println(Demo.this.name);   // "Demo"
            }
        };
        
        // 람다의 this — 람다 외부의 this
        Runnable r2 = () -> {
            System.out.println(this.name);   // "Demo"
            // 람다는 자신만의 this 없음
        };
    }
}

5.3 차이 2 — 변수 그림자

public void test() {
    int x = 10;
    
    // 익명 클래스 — 자체 변수 가능
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            int x = 20;   // OK (그림자)
            System.out.println(x);   // 20
        }
    };
    
    // 람다 — 외부 변수와 같은 이름 X
    Runnable r2 = () -> {
        // int x = 20;   // ❌ 컴파일 에러
        System.out.println(x);   // 10 (외부)
    };
}

5.4 차이 3 — 컴파일 결과

익명 클래스:
  - 별도 .class 파일 생성
  - Demo$1.class, Demo$2.class 등
  - 각 익명 클래스마다 하나
  - 디스크/메모리 사용 ↑

람다:
  - .class 파일 추가 X
  - invokedynamic 명령어
  - 런타임에 동적 처리
  - 더 가볍고 빠름

5.5 차이 4 — 인스턴스 생성

// 익명 클래스 — 매번 새 인스턴스
for (int i = 0; i < 1000; i++) {
    Runnable r = new Runnable() {
        @Override
        public void run() { }
    };
    // 매 반복마다 새 인스턴스
}

// 람다 — 캡처 변수 없으면 같은 인스턴스 재사용
for (int i = 0; i < 1000; i++) {
    Runnable r = () -> System.out.println("Hello");
    // JVM 이 같은 인스턴스 재사용 (보통)
}

// 캡처 변수 있는 람다:
for (int i = 0; i < 1000; i++) {
    int x = i;
    Runnable r = () -> System.out.println(x);
    // 매번 새 인스턴스 (x 가 다르니)
}

5.6 차이 5 — 함수형 인터페이스 강제

// 익명 클래스 — SAM 아닌 인터페이스도 가능
ListIterator<String> iter = new ListIterator<String>() {
    @Override public boolean hasNext() { return false; }
    @Override public String next() { return null; }
    @Override public boolean hasPrevious() { return false; }
    @Override public String previous() { return null; }
    // ... 여러 메서드
};

// 람다 — SAM 만
Runnable r = () -> {};   // OK (SAM)
// ListIterator<String> iter = () -> {};   // ❌ SAM 아님

5.7 비교 표

항목익명 클래스람다
등장Java 1.1Java 8
문법길음간결
this자신외부
변수 그림자가능X
.class 파일추가됨X
구현일반 객체invokedynamic
인스턴스 재사용X가능 (캡처 X 시)
SAM 강제XO
성능

5.8 언제 어느 것을?

람다 권장:
  ✓ 함수형 인터페이스 (SAM)
  ✓ 간결함 우선
  ✓ 거의 모든 경우

익명 클래스 권장:
  ✓ 여러 메서드 구현 (SAM 아님)
  ✓ 상태 (필드) 필요
  ✓ 자신의 this 필요
  ✓ 옛 인터페이스 (변경 안 됨)

예시:
  Runnable, Comparator, Consumer 등 → 람다
  ListIterator, MouseListener (여러 메서드) → 익명 클래스
  복잡한 상태 + 메서드 → 익명 클래스

5.9 ILIC 활용

public class CallbackExamples {
    
    // 1. 단순 콜백 — 람다
    public void onShipmentSaved(Shipment s, Consumer<Shipment> callback) {
        repository.save(s);
        callback.accept(s);
    }
    
    public void usage1() {
        onShipmentSaved(shipment, s -> log.info("Saved: {}", s.getId()));
    }
    
    // 2. 복잡한 상태 — 익명 클래스
    public void registerComplexListener() {
        repository.onSave(new ShipmentListener() {
            private int count = 0;
            
            @Override
            public void onSave(Shipment s) {
                count++;
                if (count > 100) {
                    log.warn("Too many saves: {}", count);
                }
            }
            
            @Override
            public void onError(Exception e) {
                log.error("Error", e);
                count = 0;
            }
        });
    }
    
    // 3. SAM 콜백 — 람다 또는 메서드 참조
    public void registerSimple() {
        eventBus.register(event -> log.info("Event: {}", event));
        // 또는
        eventBus.register(this::handleEvent);
    }
    
    private void handleEvent(ShipmentEvent event) {
        log.info("Event: {}", event);
    }
}

interface ShipmentListener {
    void onSave(Shipment s);
    void onError(Exception e);
    // SAM 아님 → 람다 X
}

5.10 자기 점검 답변

람다 vs 익명 클래스의 차이는?

:
1. this:

  • 익명: 자신
  • 람다: 외부
  1. 컴파일:

    • 익명: .class 파일 추가
    • 람다: invokedynamic
  2. 인스턴스:

    • 익명: 매번 새
    • 람다: 재사용 가능
  3. 변수:

    • 익명: 그림자 가능
    • 람다: 외부와 같은 이름 X
  4. SAM:

    • 익명: 아무 인터페이스
    • 람다: SAM 만
  5. 권장:

    • 람다: SAM, 간결
    • 익명: 여러 메서드, 상태 필요

6️⃣ this 키워드의 의미

6.1 익명 클래스의 this

public class Demo {
    private String name = "Demo";
    
    public void test() {
        Runnable r = new Runnable() {
            String name = "Anonymous";
            
            @Override
            public void run() {
                // 익명 클래스의 this — 자기 자신
                System.out.println(this.name);   // "Anonymous"
                System.out.println(this.getClass().getName());   // Demo$1
                
                // 외부 this 는 OuterClass.this
                System.out.println(Demo.this.name);   // "Demo"
            }
        };
        r.run();
    }
}

6.2 람다의 this

public class Demo {
    private String name = "Demo";
    
    public void test() {
        Runnable r = () -> {
            // 람다의 this — 외부 (Demo)
            System.out.println(this.name);   // "Demo"
            System.out.println(this.getClass().getName());   // Demo (Demo$1 아님)
            
            // 람다는 자체 this 없음
            // 람다는 익명 객체 X (개념적으로)
        };
        r.run();
    }
}

6.3 시각화

익명 클래스의 this:

  Demo 객체
    ├── name: "Demo"
    └── 메서드 안에서 익명 클래스 생성
        ├── Demo$1 객체 (별도)
        │   ├── name: "Anonymous"
        │   └── this → Demo$1
        └── Demo.this → Demo

람다의 this:

  Demo 객체
    ├── name: "Demo"
    └── 메서드 안에서 람다
        └── 람다 (별도 객체 X 같은 개념)
            └── this → Demo (외부)

6.4 활용 — 콜백 안에서 외부 메서드 호출

public class Service {
    
    private String prefix = "[Service] ";
    
    public void doSomething() {
        // 익명 클래스
        executor.submit(new Runnable() {
            @Override
            public void run() {
                log("익명 클래스");   // ❌ Service.this.log() 필요
                // 또는
                Service.this.log("익명 클래스");
            }
        });
        
        // 람다
        executor.submit(() -> {
            log("람다");   // ✓ 외부 메서드 그대로
            // this 가 Service 이니
            // 또는
            this.log("람다");
        });
    }
    
    private void log(String msg) {
        System.out.println(prefix + msg);
    }
}

6.5 람다 안에서 익명 클래스

public class Demo {
    String name = "Demo";
    
    public void test() {
        Runnable r = () -> {
            // 람다 안의 익명 클래스
            Runnable inner = new Runnable() {
                @Override
                public void run() {
                    // 익명 클래스의 this
                    System.out.println(this.getClass().getName());   // Demo$1
                    System.out.println(Demo.this.name);   // "Demo"
                }
            };
            inner.run();
            
            // 람다의 this
            System.out.println(this.name);   // "Demo"
        };
        r.run();
    }
}

6.6 정리 — this 비교

위치this 의 의미
일반 메서드이 객체
익명 클래스 (안)익명 클래스 객체
익명 클래스 (안) + Outer.this외부 객체
람다 (안)외부 메서드의 this (외부 객체)
람다 (안) + Outer.this같은 효과 (외부)
static 메서드this 사용 X

6.7 활용 — 메모리 누수 방지

// 옛 익명 클래스의 위험
public class MemoryLeakRisk {
    
    private byte[] hugeData = new byte[100_000_000];   // 100MB
    
    public Runnable getTask() {
        return new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello");
                // hugeData 사용 X
                // 하지만 익명 클래스가 외부 인스턴스 참조
                // → MemoryLeakRisk 인스턴스 GC X
                // → 100MB 메모리 누수 가능
            }
        };
    }
    
    // 람다도 비슷한 문제
    public Runnable getTaskLambda() {
        return () -> {
            System.out.println("Hello");
            // 외부 변수 캡처 시 this 도 캡처
            // hugeData 참조하면 큰 메모리 잡고 있음
        };
    }
    
    // 해결:
    public Runnable getTaskStatic() {
        // 정적 메서드 또는 별도 클래스
        return MemoryLeakRisk::staticTask;
    }
    
    private static void staticTask() {
        System.out.println("Hello");
    }
}

6.8 ILIC 활용

@Component
public class ShipmentProcessor {
    
    private final String workerName = "shipment-worker";
    private int processedCount = 0;
    
    // 1. 람다 안에서 외부 메서드/필드 접근
    public void processAll(List<Shipment> shipments) {
        shipments.forEach(s -> {
            processSingle(s);   // ✓ this.processSingle (this = ShipmentProcessor)
            processedCount++;   // ✓ this.processedCount
            log("Processed " + s.getId());
        });
    }
    
    private void processSingle(Shipment s) {
        // 처리
    }
    
    private void log(String msg) {
        System.out.println(workerName + ": " + msg);
    }
    
    // 2. 익명 클래스 — Outer.this 필요
    public void processAllAnonymous(List<Shipment> shipments) {
        for (Shipment s : shipments) {
            new Runnable() {
                @Override
                public void run() {
                    ShipmentProcessor.this.processSingle(s);
                    ShipmentProcessor.this.processedCount++;
                }
            }.run();
        }
    }
    
    // 3. 콜백 — this 활용
    public CompletableFuture<Void> processAsync(Shipment s) {
        return CompletableFuture.runAsync(() -> {
            this.processSingle(s);
            this.processedCount++;
        });
    }
}

6.9 자기 점검 답변

람다와 익명 클래스의 this 차이는?

:
1. 익명 클래스:

  • this = 익명 클래스 자신
  • 외부: OuterClass.this
  1. 람다:

    • this = 외부 메서드의 this
    • 람다는 자체 this 없음
  2. 이유:

    • 익명 클래스 = 새 객체
    • 람다 = 외부의 일부 (개념적)
  3. 활용:

    • 람다는 외부 메서드 호출 더 자연스러움
    • 익명 클래스는 명시적 Outer.this
  4. 주의:

    • 람다도 외부 this 캡처
    • 큰 객체 참조 시 메모리 주의

7️⃣ 변수 캡처와 effectively final

7.1 변수 캡처의 정의

변수 캡처 (Variable Capture):

  람다 (또는 익명 클래스) 가 외부의 변수를 사용하는 것.

종류:
  1. 지역 변수 (메서드 내 변수)
  2. 인스턴스 변수 (필드)
  3. static 변수 (클래스 변수)

규칙:
  - 인스턴스/static 변수: 자유롭게 (변경 가능)
  - 지역 변수: effectively final 만

7.2 effectively final 이란

effectively final:

  실질적으로 final 인 변수.
  - final 키워드 없어도
  - 선언 후 값 변경 X
  - 컴파일러가 "사실상 final" 로 판단

Java 8+ 부터 람다가 지역 변수 캡처 시 요구.

7.3 시각화

public void test() {
    int x = 10;
    // 1. final 명시 (옛 방식)
    final int y = 20;
    
    // 2. effectively final (Java 8+)
    int z = 30;
    // z 를 어디서도 변경 X
    // → effectively final
    
    Runnable r = () -> {
        System.out.println(x);   // OK (effectively final)
        System.out.println(y);   // OK (final)
        System.out.println(z);   // OK (effectively final)
    };
    
    // ❌ z 변경 시 캡처 X
    // z = 40;
    // → "Variable used in lambda should be effectively final"
}

7.4 왜 effectively final?

이유:

1. 동시성 안전
   - 람다가 다른 스레드에서 실행 가능
   - 지역 변수가 변경되면 race condition

2. 람다의 본질
   - 람다 = 함수
   - 함수는 호출 시점의 값으로 동작
   - 호출 후 변경 무의미

3. JVM 구현
   - 람다는 캡처된 값을 자신의 필드로
   - "복사" 의 의미
   - 원본 변경해도 람다 안의 값 변경 X

4. 가독성
   - 람다 안의 변수 = 외부와 같음 보장
   - 디버깅 용이

7.5 함정과 해결

// 함정 1: 반복문에서 변수 변경
List<Runnable> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    // ❌ i 가 변경됨 → effectively final X
    tasks.add(() -> System.out.println(i));
}

// 해결: 새 변수
for (int i = 0; i < 10; i++) {
    int captured = i;   // ★ 새 변수 (effectively final)
    tasks.add(() -> System.out.println(captured));
}

// 또는 for-each
for (int i : numbers) {
    // i 는 each loop 마다 새 변수
    tasks.add(() -> System.out.println(i));   // OK
}

// 또는 IntStream
IntStream.range(0, 10).forEach(i -> 
    tasks.add(() -> System.out.println(i)));

7.6 인스턴스 변수는 자유

public class Counter {
    private int count = 0;   // 인스턴스 변수
    
    public Runnable getIncrementer() {
        return () -> {
            count++;   // ✓ 인스턴스 변수는 OK
            // 외부 this 의 count
        };
    }
    
    public void test() {
        Runnable r = getIncrementer();
        r.run();   // count = 1
        r.run();   // count = 2
        r.run();   // count = 3
    }
}

// 이유:
// - 인스턴스 변수는 this 의 필드
// - 람다가 this 캡처
// - this 의 필드는 변경 가능 (this 자체는 final)

7.7 함정 — 가변 컬렉션 캡처

// 지역 변수가 컬렉션이면?
public void test() {
    List<Integer> nums = new ArrayList<>();
    
    Runnable r = () -> {
        nums.add(42);   // ✓ 가능!
        // nums 변수 자체는 변경 X (effectively final)
        // nums 가 가리키는 객체는 변경 가능
    };
    
    r.run();
    System.out.println(nums);   // [42]
}

// 주의:
// - nums = new ArrayList<>();   ❌ 변수 재할당 X
// - nums.add(...);   ✓ 객체 변경 OK

7.8 캡처의 비용

// 캡처 X — 가벼움
Runnable r1 = () -> System.out.println("Hello");
// 캡처할 변수 없음
// JVM 이 같은 인스턴스 재사용 가능

// 캡처 O — 약간 비용
String msg = "Hello";
Runnable r2 = () -> System.out.println(msg);
// msg 가 람다의 필드로
// 매번 새 인스턴스 (보통)

// 많은 변수 캡처
String prefix = "...";
int count = 10;
double price = 99.9;
Runnable r3 = () -> {
    System.out.println(prefix + count + price);
};
// 3개 변수 모두 캡처
// 람다 객체에 3개 필드

// 성능 영향:
// - 일반적으로 무시할 수준
// - 빈번한 람다 생성 시만 주의

7.9 ILIC 활용

public class ShipmentProcessor {
    
    private int totalProcessed = 0;
    
    // 1. 지역 변수 캡처
    public void process(List<Shipment> shipments) {
        LocalDateTime start = LocalDateTime.now();   // effectively final
        
        shipments.forEach(s -> {
            log.info("Processing at {}: {}", start, s.getId());
        });
    }
    
    // 2. 반복문 — 새 변수
    public List<Runnable> createTasks(List<Shipment> shipments) {
        List<Runnable> tasks = new ArrayList<>();
        for (int i = 0; i < shipments.size(); i++) {
            int index = i;   // ★ 새 변수
            Shipment s = shipments.get(i);   // 새 변수 (반복마다)
            
            tasks.add(() -> {
                log.info("Task {}: {}", index, s.getId());
            });
        }
        return tasks;
    }
    
    // 3. 인스턴스 변수
    public Runnable getCounter() {
        return () -> {
            totalProcessed++;   // ✓ 인스턴스 변수 자유
            log.info("Total: {}", totalProcessed);
        };
    }
    
    // 4. 컬렉션 활용
    public Function<Shipment, Void> getAccumulator() {
        List<Shipment> processed = new ArrayList<>();
        
        return s -> {
            processed.add(s);   // ✓ 컬렉션 변경 OK (변수 재할당 X)
            return null;
        };
    }
}

7.10 자기 점검 답변

변수 캡처와 effectively final 의 의미는?

:
1. effectively final:

  • final 없이도 변경 X 변수
  • 컴파일러 판단
  • Java 8+ 부터 람다 캡처 조건
  1. 지역 변수:

    • effectively final 만 가능
    • 재할당 시 캡처 X
  2. 인스턴스 변수:

    • 자유롭게 (final 무관)
    • this 캡처로 접근
  3. 이유:

    • 동시성 안전
    • 람다의 함수적 본질
    • JVM 구현
  4. 흔한 함정:

    • for 루프의 i
    • 해결: 새 변수 또는 for-each

8️⃣ 람다의 내부 구현과 성능

8.1 invokedynamic

람다의 구현:

Java 7+ 부터 도입된 invokedynamic 명령어 활용.

람다 컴파일 결과:
  - .class 파일 추가 X
  - invokedynamic 명령어 + LambdaMetafactory
  - 런타임에 동적으로 클래스 생성

장점:
  - 가벼움
  - JIT 최적화 용이
  - JVM 구현 자유 (변경 가능)

8.2 익명 클래스와 비교

익명 클래스의 컴파일:
  - Demo$1.class 같은 별도 클래스 생성
  - 디스크/메모리 사용 ↑
  - 클래스 로딩 비용

람다의 컴파일:
  - 클래스 파일 추가 X
  - LambdaMetafactory.metafactory 호출
  - 런타임에 lambda 인스턴스 생성
  - 더 가벼움

성능:
  - 람다가 약간 빠름
  - 작은 차이 (대부분 경우 무관)

8.3 람다의 클래스

// 람다의 런타임 클래스 확인
Runnable r = () -> System.out.println("Hello");
System.out.println(r.getClass());
// 예: class Demo$$Lambda$1/0x000000080003d440

// 익명 클래스의 클래스
Runnable r2 = new Runnable() {
    @Override
    public void run() { }
};
System.out.println(r2.getClass());
// 예: class Demo$1

// 차이:
// - 람다: 동적 생성된 이름 (런타임마다 다를 수 있음)
// - 익명: 컴파일 시 결정된 이름

8.4 인스턴스 재사용

// 캡처 변수 없는 람다 — 인스턴스 재사용
for (int i = 0; i < 1000; i++) {
    Runnable r = () -> System.out.println("Hello");
    // JVM 이 보통 같은 인스턴스 반환
}

// 캡처 변수 있는 람다 — 매번 새 인스턴스
for (int i = 0; i < 1000; i++) {
    int x = i;
    Runnable r = () -> System.out.println(x);
    // 매번 새 인스턴스 (x 가 다르니)
}

// 익명 클래스 — 항상 새 인스턴스
for (int i = 0; i < 1000; i++) {
    Runnable r = new Runnable() {
        @Override
        public void run() { System.out.println("Hello"); }
    };
    // 매 반복마다 새 인스턴스 (캡처 무관)
}

8.5 성능 벤치마크

public class LambdaBenchmark {
    
    @Benchmark
    public Runnable lambdaNoCapture() {
        return () -> {};
    }
    
    @Benchmark
    public Runnable lambdaWithCapture() {
        int x = 10;
        return () -> System.out.println(x);
    }
    
    @Benchmark
    public Runnable anonymousClass() {
        return new Runnable() {
            @Override
            public void run() { }
        };
    }
}

// 예상 결과 (대략):
// lambdaNoCapture:     ~3 ns/op (인스턴스 재사용)
// lambdaWithCapture:   ~10 ns/op
// anonymousClass:      ~15 ns/op

// 차이는 매우 작음
// 일반적 코드는 무관
// 빈번한 람다 생성 시만 영향

8.6 메서드 참조의 성능

// 람다
Function<String, Integer> length1 = s -> s.length();

// 메서드 참조
Function<String, Integer> length2 = String::length;

// 성능:
// - 둘이 거의 같음
// - 메서드 참조가 약간 더 효율 (JIT 인라인)
// - 차이는 무시할 수준

8.7 Stream API 의 람다 성능

// 일반 for-loop
long sum = 0;
for (int i = 0; i < list.size(); i++) {
    sum += list.get(i);
}

// Stream + 람다
long sum2 = list.stream().mapToInt(Integer::intValue).sum();

// 성능:
// - Stream: 약간 느림 (객체 생성 등)
// - 큰 데이터: 차이 작음
// - 작은 데이터: 차이 큼 (오버헤드)
// - 가독성: Stream 승

// 권장:
// - 일반적: Stream
// - 핫 루프 (성능 우선): for
// - 병렬: parallelStream

8.8 함정 — 람다 안의 박싱

// 박싱 비용
Function<Integer, Integer> doubler = x -> x * 2;
// Integer (객체) ↔ int (기본 타입) 변환 = boxing

doubler.apply(5);   // 5 박싱 → Integer 반환 (boxing)

// 더 효율적: IntFunction
IntFunction<Integer> doubler2 = x -> x * 2;
// int → Integer 반환 (한 번만 boxing)

// 가장 효율적: IntUnaryOperator
IntUnaryOperator doubler3 = x -> x * 2;
// int → int (boxing X)

// 많은 호출 시 차이 큼
// Stream API 의 IntStream 활용 권장

8.9 ILIC 활용

public class ShipmentPerformance {
    
    // 1. 인스턴스 재사용 (캡처 X)
    private static final Predicate<Shipment> IS_URGENT = s -> s.isUrgent();
    // 한 번 생성, 재사용
    
    public List<Shipment> filterUrgent(List<Shipment> shipments) {
        return shipments.stream()
            .filter(IS_URGENT)   // 재사용
            .toList();
    }
    
    // 2. 빈번한 호출 시 IntStream
    public long totalWeight(List<Shipment> shipments) {
        return shipments.stream()
            .mapToLong(s -> s.getWeight().longValue())   // IntStream/LongStream
            .sum();
        // boxing 회피
    }
    
    // 3. 메서드 참조 (약간 더 효율)
    public List<String> extractBlNos(List<Shipment> shipments) {
        return shipments.stream()
            .map(Shipment::getBlNo)   // 메서드 참조
            .toList();
    }
    
    // 4. 정적 람다 상수 (재사용)
    public static final Comparator<Shipment> BY_WEIGHT = 
        Comparator.comparing(Shipment::getWeight);
    
    public List<Shipment> sortByWeight(List<Shipment> shipments) {
        return shipments.stream()
            .sorted(BY_WEIGHT)
            .toList();
    }
}

8.10 자기 점검 답변

람다의 내부 구현과 성능은?

:
1. invokedynamic:

  • Java 7+ 명령어
  • LambdaMetafactory
  • 런타임 동적 생성
  1. vs 익명 클래스:

    • .class 파일 X
    • 더 가벼움
    • 약간 빠름
  2. 인스턴스 재사용:

    • 캡처 X → 재사용 가능
    • 캡처 O → 매번 새
  3. 성능 차이:

    • 일반적 무시할 수준
    • 빈번한 생성/박싱 시 주의
  4. 권장:

    • 정적 람다 상수 (재사용)
    • IntStream/LongStream (boxing 회피)
    • 메서드 참조

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
람다 표현식?함수형 인터페이스의 간결한 구현
함수형 인터페이스?SAM (Single Abstract Method)
@FunctionalInterface?컴파일러 검증 + 의도 명시
Function/Consumer/Supplier/Predicate?apply/accept/get/test
메서드 참조 4가지?정적/특정 인스턴스/임의 인스턴스/생성자
람다 vs 익명 클래스 this?외부 vs 자신
람다의 컴파일?invokedynamic
effectively final?실질적으로 변경 X
지역 변수 캡처 조건?effectively final
인스턴스 변수?자유롭게 (this 캡처)
람다 성능?익명 클래스보다 약간 빠름
boxing 회피?IntStream, IntFunction 등

9.2 자기 점검 체크리스트

람다

  • 정의와 문법
  • 매개변수/화살표/본문
  • 다양한 문법 (괄호/중괄호)

함수형 인터페이스

  • SAM 의 정의
  • @FunctionalInterface
  • default/static 메서드

java.util.function

  • Function/Consumer/Supplier/Predicate
  • BiFunction 등
  • IntXxx 등 (boxing 회피)

메서드 참조

  • 4가지 종류
  • 람다와의 등가
  • 언제 어느 것?

this

  • 익명 클래스: 자신
  • 람다: 외부
  • 변수 그림자

변수 캡처

  • effectively final
  • 지역 vs 인스턴스 vs static
  • for 루프의 함정

성능

  • invokedynamic
  • 인스턴스 재사용
  • boxing
  • 정적 상수

9.3 추가 심화 질문

Q1: 람다가 동시성 안전한가?

답:

  • 람다 자체는 안전 X
  • 캡처 변수가 동시성 안전해야
  • effectively final 의 일부 이유
  • 인스턴스 변수 변경 시 별도 동기화

Q2: 람다 안에서 throw checked exception?

답:

// ❌ checked exception 던지면 컴파일 에러
Runnable r = () -> {
    Thread.sleep(1000);   // InterruptedException
    // → 컴파일 에러
};

// 해결:
// 1. try-catch
Runnable r2 = () -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
};

// 2. 사용자 정의 함수형 인터페이스
@FunctionalInterface
interface ThrowingRunnable {
    void run() throws Exception;
}

ThrowingRunnable r3 = () -> Thread.sleep(1000);   // OK

Q3: 람다가 lambda 의 일급 시민화?

답:

  • 일급 시민: 변수 할당, 매개변수, 반환 가능
  • 람다는 함수형 인터페이스 인스턴스
  • 일급 시민화 부분적
  • 완전 일급 시민 (Scala, Kotlin) 과는 차이
  • 자바는 인터페이스 wrapping

Q4: 람다 안의 메서드 참조 가능?

답:

// 람다 안에서 메서드 참조 사용
Function<List<String>, Stream<Integer>> fn = list -> 
    list.stream().map(String::length);
// OK

Q5: 람다와 클로저의 관계?

답:

  • 클로저 (Closure): 외부 변수 캡처 함수
  • 자바 람다는 부분적 클로저
  • effectively final 만 캡처
  • 다른 언어 (Kotlin, Scala) 는 변경 가능 변수도 캡처
  • 자바는 동시성/안전 우선

🎯 핵심 요약 — 3줄 정리

1. 람다 등장

  • Java 8+, 함수형 인터페이스의 간결 구현
  • (params) -> { body }
  • 익명 클래스 보일러플레이트 제거

2. 핵심 4가지

  • Function (변환), Consumer (소비)
  • Supplier (생성), Predicate (판단)
    • BiXxx, IntXxx (특수화)

3. 메서드 참조 + 캡처

  • 4가지 종류 (정적/특정/임의/생성자)
  • effectively final 만 캡처
  • this 는 외부 (익명 클래스와 반대)

📚 다음으로...

Unit 10.2 — Stream API

이번 Unit에서 람다를 봤다면, 다음은 Stream API.

  • Stream 의 정의
  • 중간 연산 (map, filter, sorted, ...)
  • 최종 연산 (collect, forEach, reduce, ...)
  • 병렬 Stream

Phase 10 진행 상황

🚀 Phase 10 — 함수형 프로그래밍
  ✅ Unit 10.1 람다 표현식 ← 여기
  ⏭ Unit 10.2 Stream API
  ⏭ Unit 10.3 Collectors와 reduce (★ 마스터)
  ⏭ Unit 10.4 Optional

3주차 누적 진행

✅ Phase 1 ~ 9 완주 (42 Unit)
🚀 Phase 10 — 함수형 프로그래밍 (1/4 진행)

총: 43/43 Unit (3주차 거의 완주)

🚀 Phase 10 시작 — 함수형 프로그래밍 진입

profile
Software Developer

0개의 댓글