본문 바로가기

지역 클래스와 변수 캡처

캡처된 지역 변수 변경의 컴파일 실패를 재현하고 스택·힙 수명 차이, 사실상 final 규칙, 바깥 필드 접근을 게시글 작업으로 이해합니다.

지역 클래스는 메서드 블록 안에 선언되며 그 범위의 매개변수와 지역 값을 사용할 수 있습니다.

하지만 메서드가 끝나면 스택 프레임의 지역 변수는 사라지고 지역 객체는 힙에서 더 오래 살아남을 수 있습니다.

자바는 필요한 지역 값을 객체에 복사해 캡처하고, 원본과 복사본이 어긋나지 않도록 값 변경을 제한합니다.


캡처 변수의 재대입 제한

prefix를 지역 클래스가 사용한 뒤 다른 문자열로 바꾸려 합니다.

캡처 대상은 final이거나 사실상 final이어야 하므로 실패합니다.

lab/CapturedLocalMutationFailure.java
public final class CapturedLocalMutationFailure {
    public static void main(String[] args) {
        String prefix = "board";

        class Printer {
            void print() {
                System.out.println(prefix);
            }
        }

        prefix = "changed";
        new Printer().print();
    }
}
컴파일 실패 관찰
error: local variables referenced from an inner class must be final or effectively final

final을 명시하지 않아도 초기화 뒤 한 번도 재대입하지 않으면 사실상 final입니다.

객체 참조를 캡처한 경우 참조 재대입은 금지되지만 그 객체의 가변 메서드 호출까지 자동으로 막지는 않습니다.


지역 객체와 캡처 값의 수명

팩터리 메서드는 Printer를 반환하고 종료됩니다.

나중에 print를 호출해도 prefix와 viewCount가 출력됩니다.

실행 중 원래 스택 변수를 다시 찾는 것이 아니라 생성 시 객체에 저장한 값을 사용합니다.

src/CapturedPostPrinter.java
public final class CapturedPostPrinter {
    public static void main(String[] args) {
        Printer printer = createPrinter("daily", 40);
        System.out.println("factory-finished");
        printer.print("local class");
    }

    private static Printer createPrinter(String prefix, int viewCount) {
        String separator = ":";

        class LocalPrinter implements Printer {
            @Override
            public void print(String title) {
                System.out.println(prefix + separator + title + "=" + viewCount);
            }
        }

        return new LocalPrinter();
    }

    private interface Printer {
        void print(String title);
    }
}
factory-finished
daily:local class=40

prefix와 viewCount의 생명 주기가 LocalPrinter와 함께 연장된 것처럼 보입니다.

실제 구현 세부 필드 이름에 의존하지 않고 “필요한 값을 캡처한다”는 언어 계약으로 이해합니다.

팩터리 반환 뒤에도 사용하는 캡처 값

팩터리 생성→반환→factory-finished→나중 print의 호출 순서가 실제 예제입니다. 메서드 종료 뒤 캡처한 값이 사용되는 시간 차이를 장면으로 따라가며 캡처 값의 사용 시점을 확인할 수 있습니다.

팩터리 반환 뒤에도 사용하는 캡처 값

CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다.

지역 클래스 · 생성 뒤 호출

팩터리 반환 뒤에도 사용하는 캡처 값

팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다.

팩터리 반환과 이후 Printer 호출의 순서 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. 생성 → 반환 → main의 다음 문장 → 나중 호출 팩터리 호출 createPrinter("daily", 40) 반환된 Printer prefix=daily / viewCount=40 separator=":" main의 다음 문장 factory-finished 출력 나중 print 호출 title="local class" 호출 결과 daily:local class=40 팩터리는 이미 반환 · 받은 참조를 나중에 호출 · 물리 캡처 필드 배치는 가정하지 않음
팩터리 반환과 이후 Printer 호출의 순서 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. 생성 → 반환 뒤 호출 팩터리 호출 createPrinter "daily", 40 반환된 Printer prefix=daily viewCount=40 separator=":" main의 다음 문장 factory-finished 출력 나중 print 호출 title="local class" 호출 결과 daily:local class=40 팩터리는 이미 반환했습니다 앞서 받은 참조를 다시 호출함 상자는 논리적 값과 실행 단계 물리 필드 배치를 가정하지 않음
  1. 팩터리 생성·반환

    사용하는 값과 반환
    createPrinter("daily", 40)에서 LocalPrinter를 생성하고 참조를 반환합니다. prefix는 "daily", viewCount는 40입니다.
    실제 출력
    이 단계의 출력 없음
  2. 반환 후 main

    사용하는 값과 반환
    반환된 참조를 printer에 보관한 뒤 다음 문장을 실행합니다.
    실제 출력
    factory-finished
  3. 나중 print 호출

    사용하는 값과 반환
    printer.print("local class")는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다.
    실제 출력
    daily:local class=40

원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":"도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다.

이 예의 조건

  • 논리적 캡처값이며 물리 필드 배치를 가정하지 않는다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다.
원 도표의 설명과 주석
구분내용
설명팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다.
전체 읽기 정보팩터리 반환 뒤에도 사용하는 캡처 값 팩터리 반환 뒤에도 사용하는 캡처 값 CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다. 지역 클래스 · 생성 뒤 호출 팩터리 반환 뒤에도 사용하는 캡처 값 팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다. 팩터리 생성·반환 사용하는 값과 반환 createPrinter("daily", 40) 에서 LocalPrinter 를 생성하고 참조를 반환합니다. prefix 는 "daily" , viewCount 는 40 입니다. 실제 출력 이 단계의 출력 없음 반환 후 main 사용하는 값과 반환 반환된 참조를 printer 에 보관한 뒤 다음 문장을 실행합니다. 실제 출력 factory-finished 나중 print 호출 사용하는 값과 반환 printer.print("local class") 는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다. 실제 출력 daily:local class=40 원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":" 도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
팩터리 호출

daily/40을 받은 Printer를 만듭니다.

  • 입력: createPrinter(daily,40)
  • 보관 의미: prefix daily, viewCount 40, separator :
  • 나중 행동이 사용할 값을 생성 시 정한다.
팩터리 반환

main이 Printer 참조를 받고 다음 문장을 실행합니다.

  • 출력: factory-finished
  • 팩터리 프레임: 종료됨
  • 생성 호출이 끝난 뒤에도 반환 객체를 사용할 수 있다.
나중 행동

print(local class)가 앞선 값을 사용합니다.

  • 현재 제목: local class
  • 출력: daily:local class=40
  • 호출 시 제목과 생성 시 캡처값이 결합한다.

팩터리 반환 뒤에도 사용하는 캡처 값

팩터리 생성→반환→factory-finished→나중 print의 호출 순서가 실제 예제입니다. 메서드 종료 뒤 캡처한 값이 사용되는 시간 차이를 장면으로 따라가며 캡처 값의 사용 시점을 확인할 수 있습니다.

팩터리 반환 뒤에도 사용하는 캡처 값

CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다.

지역 클래스 · 생성 뒤 호출

팩터리 반환 뒤에도 사용하는 캡처 값

팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다.

팩터리 반환과 이후 Printer 호출의 순서 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. 생성 → 반환 → main의 다음 문장 → 나중 호출 팩터리 호출 createPrinter("daily", 40) 반환된 Printer prefix=daily / viewCount=40 separator=":" main의 다음 문장 factory-finished 출력 나중 print 호출 title="local class" 호출 결과 daily:local class=40 팩터리는 이미 반환 · 받은 참조를 나중에 호출 · 물리 캡처 필드 배치는 가정하지 않음
팩터리 반환과 이후 Printer 호출의 순서 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. 생성 → 반환 뒤 호출 팩터리 호출 createPrinter "daily", 40 반환된 Printer prefix=daily viewCount=40 separator=":" main의 다음 문장 factory-finished 출력 나중 print 호출 title="local class" 호출 결과 daily:local class=40 팩터리는 이미 반환했습니다 앞서 받은 참조를 다시 호출함 상자는 논리적 값과 실행 단계 물리 필드 배치를 가정하지 않음
  1. 팩터리 생성·반환

    사용하는 값과 반환
    createPrinter("daily", 40)에서 LocalPrinter를 생성하고 참조를 반환합니다. prefix는 "daily", viewCount는 40입니다.
    실제 출력
    이 단계의 출력 없음
  2. 반환 후 main

    사용하는 값과 반환
    반환된 참조를 printer에 보관한 뒤 다음 문장을 실행합니다.
    실제 출력
    factory-finished
  3. 나중 print 호출

    사용하는 값과 반환
    printer.print("local class")는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다.
    실제 출력
    daily:local class=40

원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":"도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다.

이 예의 조건

  • 논리적 캡처값이며 물리 필드 배치를 가정하지 않는다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다.
원 도표의 설명과 주석
구분내용
설명팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다.
전체 읽기 정보팩터리 반환 뒤에도 사용하는 캡처 값 팩터리 반환 뒤에도 사용하는 캡처 값 CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다. 지역 클래스 · 생성 뒤 호출 팩터리 반환 뒤에도 사용하는 캡처 값 팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다. 팩터리 생성·반환 사용하는 값과 반환 createPrinter("daily", 40) 에서 LocalPrinter 를 생성하고 참조를 반환합니다. prefix 는 "daily" , viewCount 는 40 입니다. 실제 출력 이 단계의 출력 없음 반환 후 main 사용하는 값과 반환 반환된 참조를 printer 에 보관한 뒤 다음 문장을 실행합니다. 실제 출력 factory-finished 나중 print 호출 사용하는 값과 반환 printer.print("local class") 는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다. 실제 출력 daily:local class=40 원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":" 도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
팩터리 호출

daily/40을 받은 Printer를 만듭니다.

  • 입력: createPrinter(daily,40)
  • 보관 의미: prefix daily, viewCount 40, separator :
  • 나중 행동이 사용할 값을 생성 시 정한다.
팩터리 반환

main이 Printer 참조를 받고 다음 문장을 실행합니다.

  • 출력: factory-finished
  • 팩터리 프레임: 종료됨
  • 생성 호출이 끝난 뒤에도 반환 객체를 사용할 수 있다.
나중 행동

print(local class)가 앞선 값을 사용합니다.

  • 현재 제목: local class
  • 출력: daily:local class=40
  • 호출 시 제목과 생성 시 캡처값이 결합한다.

스택과 힙의 수명 차이

메서드 매개변수와 지역 변수는 호출 스택 프레임에 있고 메서드 종료와 함께 사라집니다.

new로 만든 LocalPrinter는 힙에 있으며 참조가 남아 있으면 계속 살아 있습니다.

지역 객체가 사라진 스택 값에 직접 접근할 수 없으므로 생성 시 값을 자기 상태로 복사합니다.

만약 캡처 뒤 원본 지역 변수를 바꿀 수 있다면 어느 값을 출력해야 하는지 문제가 생깁니다.

스택 값과 객체 안 복사본을 자동 동기화하려면 별도 공유 상자가 필요하고 동시성·수명 규칙이 복잡해집니다.

사실상 final 제약은 값 스냅숏 의미를 단순하게 보존합니다.

src/CaptureSnapshotFactory.java
import java.util.ArrayList;
import java.util.List;

public final class CaptureSnapshotFactory {
    public static void main(String[] args) {
        List<Labeler> labelers = new ArrayList<>();
        for (int index = 0; index < 3; index++) {
            int capturedIndex = index;
            class IndexedLabeler implements Labeler {
                public String label(String title) {
                    return capturedIndex + "=" + title;
                }
            }
            labelers.add(new IndexedLabeler());
        }

        for (Labeler labeler : labelers) {
            System.out.println(labeler.label("capture"));
        }
    }

    private interface Labeler {
        String label(String title);
    }
}
0=capture
1=capture
2=capture

루프의 index는 매 반복 증가하므로 캡처할 수 없습니다.

반복마다 새로 초기화되는 capturedIndex는 그 반복 안에서 사실상 final이고 각 객체가 다른 값 스냅숏을 가집니다.


바깥 인스턴스 필드의 접근

이 예제처럼 인스턴스 메서드에 선언한 지역 클래스는 바깥 인스턴스와 연결됩니다. 반면 static 메서드 등 정적 문맥에 선언한 지역·익명 클래스에는 그 바깥 인스턴스 연결이 없습니다.

지역 값 복사본이 아니라 힙에 있는 같은 바깥 객체 필드를 읽으므로 필드가 바뀌면 최신 값이 보입니다.

src/OuterFieldAndLocalCapture.java
public final class OuterFieldAndLocalCapture {
    private int publishedCount = 10;

    public static void main(String[] args) {
        OuterFieldAndLocalCapture tracker = new OuterFieldAndLocalCapture();
        Reporter reporter = tracker.createReporter(100);

        tracker.publishedCount = 40;
        reporter.print();
    }

    private Reporter createReporter(int targetCount) {
        class ProgressReporter implements Reporter {
            public void print() {
                System.out.println("progress=" + publishedCount + "/" + targetCount);
            }
        }
        return new ProgressReporter();
    }

    private interface Reporter { void print(); }
}
progress=40/100

targetCount는 생성 시 캡처한 100이고 publishedCount는 바깥 객체의 현재 40입니다.

한 출력에서 스냅숏과 최신 상태를 섞는 것이 요구에 맞는지 확인해야 합니다.

모두 한 시점이어야 하면 불변 Progress 값을 먼저 만들어 전달합니다.

반복별 캡처 값과 바깥 객체의 현재 필드

반복별 캡처값과 별도 Reporter의 생성 시 targetCount/호출 시 publishedCount를 추적합니다. 각 독립 프로그램을 유지한 채 생성→바깥변경→나중호출 시점을 분리해 값의 출처를 확인할 수 있습니다.

반복별 캡처 값과 바깥 객체의 현재 필드

CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다.

캡처와 필드 · 읽는 시점

반복별 캡처 값과 바깥 객체의 현재 필드

반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다.

반복별 캡처와 현재 바깥 필드를 따로 추적 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount100과 호출 때 읽는 같은 tracker의 publishedCount40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. 반복별 독립 객체 · 루프가 끝난 뒤 호출 반복 0의 지역 값 capturedIndex=0 각 라벨러 객체 label("capture") 0=capture 반복 1의 지역 값 capturedIndex=1 각 라벨러 객체 label("capture") 1=capture 반복 2의 지역 값 capturedIndex=2 각 라벨러 객체 label("capture") 2=capture 별도 진행률 프로그램 · 생성 뒤 바깥 필드만 10→40으로 변경 캡처한 인수 targetCount=100 createReporter 때 고정 같은 tracker의 필드 publishedCount:10→40 print 때 현재40 읽음 같은 Reporter progress=40/100 현재40 + 캡처100 점선은 출력에 사용되는 값 · 반복 캡처와 진행률은 서로 다른 프로그램
반복별 캡처와 현재 바깥 필드를 따로 추적 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount100과 호출 때 읽는 같은 tracker의 publishedCount40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. 반복별 독립 객체 반복 0의 지역 값 capturedIndex=0 각 라벨러 객체 label("capture") 0=capture 반복 1의 지역 값 capturedIndex=1 각 라벨러 객체 label("capture") 1=capture 반복 2의 지역 값 capturedIndex=2 각 라벨러 객체 label("capture") 2=capture 별도 진행률 프로그램 캡처한 인수 targetCount=100 createReporter 때 고정 같은 tracker의 필드 publishedCount:10→40 print 때 현재40 읽음 같은 Reporter progress=40/100 현재40 + 캡처100 점선은 출력에 사용되는 값 두 영역은 서로 다른 프로그램
반복별 캡처 값과 바깥 객체의 현재 필드
읽는 값시간 기준원문 값·결과
반복별 capturedIndex반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다.
0=capture
1=capture
2=capture
진행률의 targetCountcreateReporter(100) 호출 때 캡처한 매개변수 값입니다.100
진행률의 publishedCount같은 tracker의 필드입니다. 객체 생성 때의 10이 아니라 reporter.print() 때의 현재 값을 읽습니다.40
반복별 capturedIndex
시간 기준
반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다.
원문 값·결과
0=capture
1=capture
2=capture
진행률의 targetCount
시간 기준
createReporter(100) 호출 때 캡처한 매개변수 값입니다.
원문 값·결과
100
진행률의 publishedCount
시간 기준
같은 tracker의 필드입니다. 객체 생성 때의 10이 아니라 reporter.print() 때의 현재 값을 읽습니다.
원문 값·결과
40

진행률 예제는 두 값을 합쳐 progress=40/100을 출력합니다. 바뀌는 루프 index는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다.

이 예의 조건

  • 반복 라벨러와 Reporter는 독립 프로그램이다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다.
25print 때 현재 40 읽음
28현재 40 + 캡처 100
원 도표의 설명과 주석
구분내용
설명위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다.
전체 읽기 정보반복별 캡처 값과 바깥 객체의 현재 필드 반복별 캡처 값과 바깥 객체의 현재 필드 CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다. 캡처와 필드 · 읽는 시점 반복별 캡처 값과 바깥 객체의 현재 필드 반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다. 반복별 캡처 값과 바깥 객체의 현재 필드 읽는 값 시간 기준 원문 값·결과 반복별 capturedIndex 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 0=capture 1=capture 2=capture 진행률의 targetCount createReporter(100) 호출 때 캡처한 매개변수 값입니다. 100 진행률의 publishedCount 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 40 반복별 capturedIndex 시간 기준 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 원문 값·결과 0=capture 1=capture 2=capture 진행률의 targetCount 시간 기준 createReporter(100) 호출 때 캡처한 매개변수 값입니다. 원문 값·결과 100 진행률의 publishedCount 시간 기준 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 원문 값·결과 40 진행률 예제는 두 값을 합쳐 progress=40/100 을 출력합니다. 바뀌는 루프 index 는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex 는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
생성 시 값

반복마다 index를 캡처하고 Reporter는 목표 100을 받습니다.

  • 라벨러: capturedIndex 0,1,2
  • Reporter 목표: 100
  • tracker 최초: 10
  • 생성 때 확정한 지역값과 이후 읽을 객체 필드를 구분한다.
생성 후 변경

tracker 필드만 10→40으로 바꿉니다.

  • publishedCount: 40
  • targetCount: 100 유지
  • 라벨러 index: 각 0,1,2 유지
  • 같은 바깥 객체의 현재 필드는 나중 호출에 보인다.
나중 호출

각 객체가 자신의 캡처값 또는 현재 필드를 사용합니다.

  • 라벨 결과: 0=capture,1=capture,2=capture
  • Reporter: progress=40/100
  • 생성 시 snapshot과 호출 시 현재 값이 다른 출처로 결합한다.

반복별 캡처 값과 바깥 객체의 현재 필드

반복별 캡처값과 별도 Reporter의 생성 시 targetCount/호출 시 publishedCount를 추적합니다. 각 독립 프로그램을 유지한 채 생성→바깥변경→나중호출 시점을 분리해 값의 출처를 확인할 수 있습니다.

반복별 캡처 값과 바깥 객체의 현재 필드

CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다.

캡처와 필드 · 읽는 시점

반복별 캡처 값과 바깥 객체의 현재 필드

반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다.

반복별 캡처와 현재 바깥 필드를 따로 추적 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount100과 호출 때 읽는 같은 tracker의 publishedCount40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. 반복별 독립 객체 · 루프가 끝난 뒤 호출 반복 0의 지역 값 capturedIndex=0 각 라벨러 객체 label("capture") 0=capture 반복 1의 지역 값 capturedIndex=1 각 라벨러 객체 label("capture") 1=capture 반복 2의 지역 값 capturedIndex=2 각 라벨러 객체 label("capture") 2=capture 별도 진행률 프로그램 · 생성 뒤 바깥 필드만 10→40으로 변경 캡처한 인수 targetCount=100 createReporter 때 고정 같은 tracker의 필드 publishedCount:10→40 print 때 현재40 읽음 같은 Reporter progress=40/100 현재40 + 캡처100 점선은 출력에 사용되는 값 · 반복 캡처와 진행률은 서로 다른 프로그램
반복별 캡처와 현재 바깥 필드를 따로 추적 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount100과 호출 때 읽는 같은 tracker의 publishedCount40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. 반복별 독립 객체 반복 0의 지역 값 capturedIndex=0 각 라벨러 객체 label("capture") 0=capture 반복 1의 지역 값 capturedIndex=1 각 라벨러 객체 label("capture") 1=capture 반복 2의 지역 값 capturedIndex=2 각 라벨러 객체 label("capture") 2=capture 별도 진행률 프로그램 캡처한 인수 targetCount=100 createReporter 때 고정 같은 tracker의 필드 publishedCount:10→40 print 때 현재40 읽음 같은 Reporter progress=40/100 현재40 + 캡처100 점선은 출력에 사용되는 값 두 영역은 서로 다른 프로그램
반복별 캡처 값과 바깥 객체의 현재 필드
읽는 값시간 기준원문 값·결과
반복별 capturedIndex반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다.
0=capture
1=capture
2=capture
진행률의 targetCountcreateReporter(100) 호출 때 캡처한 매개변수 값입니다.100
진행률의 publishedCount같은 tracker의 필드입니다. 객체 생성 때의 10이 아니라 reporter.print() 때의 현재 값을 읽습니다.40
반복별 capturedIndex
시간 기준
반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다.
원문 값·결과
0=capture
1=capture
2=capture
진행률의 targetCount
시간 기준
createReporter(100) 호출 때 캡처한 매개변수 값입니다.
원문 값·결과
100
진행률의 publishedCount
시간 기준
같은 tracker의 필드입니다. 객체 생성 때의 10이 아니라 reporter.print() 때의 현재 값을 읽습니다.
원문 값·결과
40

진행률 예제는 두 값을 합쳐 progress=40/100을 출력합니다. 바뀌는 루프 index는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다.

이 예의 조건

  • 반복 라벨러와 Reporter는 독립 프로그램이다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다.
25print 때 현재 40 읽음
28현재 40 + 캡처 100
원 도표의 설명과 주석
구분내용
설명위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다.
전체 읽기 정보반복별 캡처 값과 바깥 객체의 현재 필드 반복별 캡처 값과 바깥 객체의 현재 필드 CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다. 캡처와 필드 · 읽는 시점 반복별 캡처 값과 바깥 객체의 현재 필드 반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다. 반복별 캡처 값과 바깥 객체의 현재 필드 읽는 값 시간 기준 원문 값·결과 반복별 capturedIndex 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 0=capture 1=capture 2=capture 진행률의 targetCount createReporter(100) 호출 때 캡처한 매개변수 값입니다. 100 진행률의 publishedCount 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 40 반복별 capturedIndex 시간 기준 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 원문 값·결과 0=capture 1=capture 2=capture 진행률의 targetCount 시간 기준 createReporter(100) 호출 때 캡처한 매개변수 값입니다. 원문 값·결과 100 진행률의 publishedCount 시간 기준 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 원문 값·결과 40 진행률 예제는 두 값을 합쳐 progress=40/100 을 출력합니다. 바뀌는 루프 index 는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex 는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
생성 시 값

반복마다 index를 캡처하고 Reporter는 목표 100을 받습니다.

  • 라벨러: capturedIndex 0,1,2
  • Reporter 목표: 100
  • tracker 최초: 10
  • 생성 때 확정한 지역값과 이후 읽을 객체 필드를 구분한다.
생성 후 변경

tracker 필드만 10→40으로 바꿉니다.

  • publishedCount: 40
  • targetCount: 100 유지
  • 라벨러 index: 각 0,1,2 유지
  • 같은 바깥 객체의 현재 필드는 나중 호출에 보인다.
나중 호출

각 객체가 자신의 캡처값 또는 현재 필드를 사용합니다.

  • 라벨 결과: 0=capture,1=capture,2=capture
  • Reporter: progress=40/100
  • 생성 시 snapshot과 호출 시 현재 값이 다른 출처로 결합한다.

가변 객체 참조 캡처의 위험

List<String> tags 참조를 재대입하지 않으면 사실상 final이지만 tags.add(...)는 가능합니다.

지역 객체가 나중에 목록을 읽으면 생성 이후 추가된 값까지 봅니다.

캡처 시점 스냅숏이 필요하면 List.copyOf(tags)를 별도 지역 값으로 만들어 캡처합니다.

목록을 복사했다는 말이 요소의 내용까지 고정했다는 뜻은 아닙니다. List.copyOf의 공식 계약은 목록의 추가·삭제·교체를 막지만 가변 요소까지 불변으로 만들지는 않습니다.

예시

다음 독립 파일에서 StringBuilder는 문자열 내용을 바꿀 수 있는 객체입니다. snapshot은 원소 구성을 고정한 목록이고, fixedText는 생성 시점의 불변 String 값입니다.

src/MutableTagCaptureBoundary.java
import java.util.ArrayList;
import java.util.List;

public final class MutableTagCaptureBoundary {
    public static void main(String[] args) {
        StringBuilder tag = new StringBuilder("java");
        List<StringBuilder> source = new ArrayList<>(List.of(tag));
        Formatter formatter = createFormatter(source);
        tag.append("-changed");
        source.add(new StringBuilder("nested"));
        System.out.println(formatter.format());
        System.out.println("source-size=" + source.size());
    }

    private static Formatter createFormatter(List<StringBuilder> tags) {
        List<StringBuilder> snapshot = List.copyOf(tags);
        String fixedText = snapshot.getFirst().toString();
        class TagFormatter implements Formatter {
            public String format() {
                return "members=" + snapshot.size()
                        + ", live=" + snapshot.getFirst()
                        + ", fixed=" + fixedText;
            }
        }
        return new TagFormatter();
    }

    private interface Formatter { String format(); }
}
members=1, live=java-changed, fixed=java
source-size=2

추가한 nested는 snapshot에 들어오지 않지만, 첫 요소는 같은 StringBuilder 참조라 변경된 내용을 읽습니다. fixedText는 toString을 호출한 시점의 String 값이므로 java를 유지합니다. 캡처한 지역 참조를 재대입하지 않은 것과 그 참조가 가리키는 객체를 바꾸지 않은 것은 별개의 조건입니다.

지역 클래스를 사용할 때 다음을 결정합니다.

  • 캡처 값은 생성 시점 스냅숏인가, 공유 가변 상태인가?
  • 객체가 메서드 범위를 벗어나 반환되는가?
  • 큰 객체를 캡처해 불필요하게 수명을 늘리지 않는가?
  • 같은 구현을 여러 메서드가 필요로 하지 않는가?
  • 단일 메서드 함수라면 람다가 더 간단하지 않은가?

이름 있는 지역 클래스는 한 메서드 안에서 여러 메서드나 상태를 가진 작은 구현이 필요할 때 유용합니다.


연습 문제

가변 태그 목록을 받은 팩터리에서 불변 복사본을 만들고, 지역 클래스 Formatter가 그 복사본으로 문장을 만들게 하세요.

팩터리 반환 후 원본 목록을 바꿔도 출력은 유지되어야 합니다.

원본 초깃값은 ["java", "nested"]이고, 팩터리에서 Formatter를 만든 뒤 원본에 "changed"를 추가합니다. format의 title 입력은 "capture"이며 출력 형식은 title=쉼표로 이은 태그입니다. 태그 목록과 각 요소는 null이 아닌 것으로 정합니다.

독립 연습에서는 초기 목록을 ["java"]로 바꾼 뒤 같은 추가를 합니다. Formatter가 보는 요소 수와 format("capture")의 결과를 예상하세요.

해설 보기
src/LocalTagSnapshotExercise.java
import java.util.ArrayList;
import java.util.List;

public final class LocalTagSnapshotExercise {
    public static void main(String[] args) {
        List<String> source = new ArrayList<>(List.of("java", "nested"));
        Formatter formatter = createFormatter(source);
        source.add("changed");

        System.out.println(formatter.format("capture"));
        System.out.println("source=" + source);
    }

    private static Formatter createFormatter(List<String> tags) {
        List<String> snapshot = List.copyOf(tags);
        class TagFormatter implements Formatter {
            public String format(String title) {
                return title + "=" + String.join(",", snapshot);
            }
        }
        return new TagFormatter();
    }

    private interface Formatter { String format(String title); }
}
capture=java,nested
source=[java, nested, changed]

List.copyOf로 만든 snapshot은 원소의 추가·삭제·교체를 허용하지 않고 원본 목록의 이후 변경을 반영하지 않습니다. 다만 요소 자체를 깊게 복사하지 않으며, 이 예제의 요소는 불변 String입니다.

독립 입력의 Formatter는 한 요소만 보며 capture=java입니다. 원본 목록은 [java, changed]가 됩니다. 요소는 String이므로 목록 구성뿐 아니라 각 태그의 문자 값도 생성 시점 값을 유지합니다.

캡처 대상의 시점 의미를 코드로 확정했습니다.

목록 멤버십의 복사와 요소 참조의 한계

List.copyOf 생성 뒤 source에 changed를 추가해 snapshot 멤버십은 유지되고 String 요소 참조는 공유됩니다. 복사 시점과 이후 추가를 분리하면 얕은 복사의 경계가 명확합니다.

목록 멤버십의 복사와 요소 참조의 한계

LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다.

목록 복사 · 원소 참조

목록 멤버십의 복사와 요소 참조의 한계

List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다.

목록 멤버십 분리와 같은 String 요소 참조 source는 java,nested에서 changed를 나중에 추가한 세 요소 목록이고 snapshot은 추가 전에 source를 인수 tags로 전달하고 List.copyOf(tags)로 받은 두 요소 목록입니다. 점선은 두 목록이 같은 String 요소를 참조함을 나타냅니다. snapshot은 changed를 담지 않습니다. 실제 String은 불변이며 가변 요소의 깊은 복사 보장은 아닙니다. source 나중 추가 뒤 요소3 java, nested, changed snapshot List.copyOf(tags) 추가 전 요소2 유지 같은 String 요소 String 요소 java String 요소 nested 나중 추가한 요소 changed 점선은 목록의 요소 참조 · 멤버십 복사로 이후 추가는 격리 · String 자체는 불변
목록 멤버십 분리와 같은 String 요소 참조 source는 java,nested에서 changed를 나중에 추가한 세 요소 목록이고 snapshot은 추가 전에 source를 인수 tags로 전달하고 List.copyOf(tags)로 받은 두 요소 목록입니다. 점선은 두 목록이 같은 String 요소를 참조함을 나타냅니다. snapshot은 changed를 담지 않습니다. 실제 String은 불변이며 가변 요소의 깊은 복사 보장은 아닙니다. source 나중 추가 뒤 요소3 java, nested, changed snapshot List.copyOf(tags) 추가 전 요소2 유지 같은 String 요소 String 요소 java String 요소 nested 나중 추가한 요소 changed 점선은 목록의 요소 참조 snapshot은 changed를 안 담음 String 자체는 불변입니다
목록 멤버십의 복사와 요소 참조의 한계
대상나중 변경과의 관계원문 결과·한계
원본 목록 sourcesource.add("changed")로 원본의 요소 수가 2에서 3이 됩니다.
source=[java, nested, changed]
캡처한 목록 snapshot추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다.
capture=java,nested
목록 안 요소복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다.가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다.
원본 목록 source
나중 변경과의 관계
source.add("changed")로 원본의 요소 수가 2에서 3이 됩니다.
원문 결과·한계
source=[java, nested, changed]
캡처한 목록 snapshot
나중 변경과의 관계
추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다.
원문 결과·한계
capture=java,nested
목록 안 요소
나중 변경과의 관계
복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다.
원문 결과·한계
가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다.

참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다.

이 예의 조건

  • String 요소는 불변이며 가변 요소의 깊은 복사를 보장하지 않는다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
4나중 추가 뒤 요소 3개
8추가 전 요소 2개 유지
원 도표의 설명과 주석
구분내용
전체 읽기 정보목록 멤버십의 복사와 요소 참조의 한계 목록 멤버십의 복사와 요소 참조의 한계 LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다. 목록 복사 · 원소 참조 목록 멤버십의 복사와 요소 참조의 한계 List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다. 목록 멤버십의 복사와 요소 참조의 한계 대상 나중 변경과의 관계 원문 결과·한계 원본 목록 source source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. source=[java, nested, changed] 캡처한 목록 snapshot 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. capture=java,nested 목록 안 요소 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 원본 목록 source 나중 변경과의 관계 source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. 원문 결과·한계 source=[java, nested, changed] 캡처한 목록 snapshot 나중 변경과의 관계 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. 원문 결과·한계 capture=java,nested 목록 안 요소 나중 변경과의 관계 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 원문 결과·한계 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
복사 전

source가 두 String 요소를 보유합니다.

  • source: java,nested
  • 요소 수: 2
  • 멤버십 snapshot의 기준 시점을 정한다.
snapshot 생성

List.copyOf(tags)로 두 요소를 보관합니다.

  • snapshot: java,nested
  • String 요소: 같은 참조 공유
  • 목록 구조 복사는 요소 객체 자체를 복제하지 않는다.
source 추가

changed를 외부 목록에 넣습니다.

  • source: java,nested,changed / 3
  • snapshot: java,nested / 2
  • 이후 멤버십 변경이 기존 snapshot에 도달하지 않는다.

목록 멤버십의 복사와 요소 참조의 한계

List.copyOf 생성 뒤 source에 changed를 추가해 snapshot 멤버십은 유지되고 String 요소 참조는 공유됩니다. 복사 시점과 이후 추가를 분리하면 얕은 복사의 경계가 명확합니다.

목록 멤버십의 복사와 요소 참조의 한계

LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다.

목록 복사 · 원소 참조

목록 멤버십의 복사와 요소 참조의 한계

List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다.

목록 멤버십 분리와 같은 String 요소 참조 source는 java,nested에서 changed를 나중에 추가한 세 요소 목록이고 snapshot은 추가 전에 source를 인수 tags로 전달하고 List.copyOf(tags)로 받은 두 요소 목록입니다. 점선은 두 목록이 같은 String 요소를 참조함을 나타냅니다. snapshot은 changed를 담지 않습니다. 실제 String은 불변이며 가변 요소의 깊은 복사 보장은 아닙니다. source 나중 추가 뒤 요소3 java, nested, changed snapshot List.copyOf(tags) 추가 전 요소2 유지 같은 String 요소 String 요소 java String 요소 nested 나중 추가한 요소 changed 점선은 목록의 요소 참조 · 멤버십 복사로 이후 추가는 격리 · String 자체는 불변
목록 멤버십 분리와 같은 String 요소 참조 source는 java,nested에서 changed를 나중에 추가한 세 요소 목록이고 snapshot은 추가 전에 source를 인수 tags로 전달하고 List.copyOf(tags)로 받은 두 요소 목록입니다. 점선은 두 목록이 같은 String 요소를 참조함을 나타냅니다. snapshot은 changed를 담지 않습니다. 실제 String은 불변이며 가변 요소의 깊은 복사 보장은 아닙니다. source 나중 추가 뒤 요소3 java, nested, changed snapshot List.copyOf(tags) 추가 전 요소2 유지 같은 String 요소 String 요소 java String 요소 nested 나중 추가한 요소 changed 점선은 목록의 요소 참조 snapshot은 changed를 안 담음 String 자체는 불변입니다
목록 멤버십의 복사와 요소 참조의 한계
대상나중 변경과의 관계원문 결과·한계
원본 목록 sourcesource.add("changed")로 원본의 요소 수가 2에서 3이 됩니다.
source=[java, nested, changed]
캡처한 목록 snapshot추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다.
capture=java,nested
목록 안 요소복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다.가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다.
원본 목록 source
나중 변경과의 관계
source.add("changed")로 원본의 요소 수가 2에서 3이 됩니다.
원문 결과·한계
source=[java, nested, changed]
캡처한 목록 snapshot
나중 변경과의 관계
추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다.
원문 결과·한계
capture=java,nested
목록 안 요소
나중 변경과의 관계
복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다.
원문 결과·한계
가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다.

참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다.

이 예의 조건

  • String 요소는 불변이며 가변 요소의 깊은 복사를 보장하지 않는다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
4나중 추가 뒤 요소 3개
8추가 전 요소 2개 유지
원 도표의 설명과 주석
구분내용
전체 읽기 정보목록 멤버십의 복사와 요소 참조의 한계 목록 멤버십의 복사와 요소 참조의 한계 LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다. 목록 복사 · 원소 참조 목록 멤버십의 복사와 요소 참조의 한계 List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다. 목록 멤버십의 복사와 요소 참조의 한계 대상 나중 변경과의 관계 원문 결과·한계 원본 목록 source source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. source=[java, nested, changed] 캡처한 목록 snapshot 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. capture=java,nested 목록 안 요소 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 원본 목록 source 나중 변경과의 관계 source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. 원문 결과·한계 source=[java, nested, changed] 캡처한 목록 snapshot 나중 변경과의 관계 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. 원문 결과·한계 capture=java,nested 목록 안 요소 나중 변경과의 관계 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 원문 결과·한계 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
복사 전

source가 두 String 요소를 보유합니다.

  • source: java,nested
  • 요소 수: 2
  • 멤버십 snapshot의 기준 시점을 정한다.
snapshot 생성

List.copyOf(tags)로 두 요소를 보관합니다.

  • snapshot: java,nested
  • String 요소: 같은 참조 공유
  • 목록 구조 복사는 요소 객체 자체를 복제하지 않는다.
source 추가

changed를 외부 목록에 넣습니다.

  • source: java,nested,changed / 3
  • snapshot: java,nested / 2
  • 이후 멤버십 변경이 기존 snapshot에 도달하지 않는다.