반응형
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가 이런 제약을 두는 이유:
- 명확성: 메소드 참조가 어떤 객체를 참조하는지 명확해야 함
- 일관성: 참조 생성 시점과 실행 시점의 동작이 일관되어야 함
- 안전성: 예상치 못한 동작을 방지
- 람다와의 일관성: 람다식도 동일한 제약을 가지므로 일관된 규칙
기억할 점:
- 객체 내용 변경: OK (sb.append() 같은)
- 변수 재할당: NO (sb = new StringBuilder() 같은)
- 필드는 예외: 클래스 필드는 재할당 가능
- effectively final: 실제로 재할당하지 않으면 final 키워드 없어도 OK
이런 제약이 있어야 메소드 참조의 동작을 예측할 수 있고, 코드의 안전성을 보장할 수 있습니다!
반응형