본문 바로가기
카테고리 없음

Java 메소드 참조와 final 제약 이해하기

by silvertogold100 2025. 8. 3.
반응형

Java 메소드 참조와 final 제약 이해하기

1단계: 기본 개념 이해

메소드 참조란?

// 람다식
list.forEach(item -> System.out.println(item));

// 메소드 참조로 변환
list.forEach(System.out::println);

메소드 참조는 기존 메소드를 함수형 인터페이스의 구현체로 사용하는 간편한 문법입니다.

2단계: 인스턴스 메소드 참조의 종류

1) 특정 인스턴스의 메소드 참조

String str = "Hello";
Supplier<Integer> lengthSupplier = str::length;  // str 인스턴스의 length() 메소드

2) 임의 인스턴스의 메소드 참조

Function<String, Integer> lengthFunction = String::length;  // 임의의 String 인스턴스의 length()

문제가 되는 것은 1번 케이스입니다!

3단계: 왜 final/effectively final이어야 할까?

문제 상황 예시

public void problematicExample() {
    StringBuilder sb = new StringBuilder("Hello");
    
    // 이 시점에서 sb::toString 메소드 참조가 생성됨
    Supplier<String> supplier = sb::toString;
    
    // 만약 sb가 재할당된다면?
    sb = new StringBuilder("World");  // 컴파일 에러!
    
    // supplier.get()은 어떤 StringBuilder를 참조해야 할까?
    System.out.println(supplier.get()); // "Hello"? "World"?
}

핵심 문제점들

1) 참조 일관성 문제

메소드 참조가 생성될 때 특정 인스턴스를 "캡처"합니다. 만약 변수가 다른 객체를 가리키게 되면, 메소드 참조는 여전히 원래 객체를 참조하게 되어 혼란이 발생합니다.

2) 예측 불가능한 동작

public Supplier<String> createSupplier() {
    StringBuilder sb = new StringBuilder("Initial");
    Supplier<String> supplier = sb::toString;
    
    sb.append(" Modified");  // 원본 객체 변경 - 이건 OK
    // sb = new StringBuilder("New");  // 재할당 - 이건 컴파일 에러
    
    return supplier;
}

4단계: 올바른 사용 예시

1) final로 선언

public void correctExample1() {
    final StringBuilder sb = new StringBuilder("Hello");
    Supplier<String> supplier = sb::toString;  // OK!
    
    sb.append(" World");  // 객체 내용 변경은 가능
    System.out.println(supplier.get());  // "Hello World"
}

2) effectively final (사실상 final)

public void correctExample2() {
    StringBuilder sb = new StringBuilder("Hello");  // final 키워드 없음
    Supplier<String> supplier = sb::toString;  // OK! (effectively final)
    
    // sb = new StringBuilder("World");  // 이 줄이 있으면 컴파일 에러
    
    System.out.println(supplier.get());
}

5단계: 다른 해결 방법들

1) 지역 변수 대신 필드 사용

class MyClass {
    private StringBuilder sb = new StringBuilder("Hello");
    
    public Supplier<String> getSupplier() {
        return sb::toString;  // 필드는 재할당 가능
    }
    
    public void changeField() {
        sb = new StringBuilder("World");  // OK!
    }
}

2) 람다식 사용

public void alternativeWithLambda() {
    StringBuilder sb = new StringBuilder("Hello");
    
    // 메소드 참조 대신 람다식 사용
    Supplier<String> supplier = () -> sb.toString();
    
    sb = new StringBuilder("World");  // 여전히 컴파일 에러
    // 람다도 동일한 제약이 있음!
}

6단계: 핵심 정리

Java가 이런 제약을 두는 이유:

  1. 명확성: 메소드 참조가 어떤 객체를 참조하는지 명확해야 함
  2. 일관성: 참조 생성 시점과 실행 시점의 동작이 일관되어야 함
  3. 안전성: 예상치 못한 동작을 방지
  4. 람다와의 일관성: 람다식도 동일한 제약을 가지므로 일관된 규칙

기억할 점:

  • 객체 내용 변경: OK (sb.append() 같은)
  • 변수 재할당: NO (sb = new StringBuilder() 같은)
  • 필드는 예외: 클래스 필드는 재할당 가능
  • effectively final: 실제로 재할당하지 않으면 final 키워드 없어도 OK

이런 제약이 있어야 메소드 참조의 동작을 예측할 수 있고, 코드의 안전성을 보장할 수 있습니다!

반응형