'김영한의 실전 자바 - 중급 1편' 강의를 들으면서 복습할만한 내용을 정리하였다.

8. 지역 클래스, 익명 클래스

8.1 지역 클래스

  • 지역 클래스(Local Class)는 내부 클래스의 특별한 종류의 하나이다. 따라서 내부 클래스의 특징을 그대로 가진다. 예를 들어서 지역 클래스도 내부 클래스이므로 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.

  • 지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의된다.

public class LocalOuterV1 {

    private int outInstanceValue = 3;

    public void process(int paramVar) {
        int localVar = 1;

        class LocalPrinter {
            int value = 0;

            public void printData() {
                System.out.println("value=" + value);
                System.out.println("localVar = " + localVar);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outInstanceValue = " + outInstanceValue);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        printer.printData();
    }

    public static void main(String[] args) {
        LocalOuterV1 localOuter = new LocalOuterV1();
        localOuter.process(2);
    }
}
  • 자신의 인스턴스 변수(value)에 접근할 수 있다.
  • 자신이 속한 코드 블럭의 지역 변수(localVar)에 접근할 수 있다.
  • 자신이 속한 코드 블럭의 매개 변수(paramVar)에 접근할 수 있다.
  • 바깥 클래스의 인스턴스 멤버(outInstanceValue)에 접근할 수 있다.

8.2 지역 클래스 - 지역 변수 캡처

변수의 생명 주기

  • 클래스 변수 : 프로그램 종료까지, 가장 길다(메서드 영역)

    • 클래스 변수(static 변수)는 메서드 영역에 존재하고, 자바가 클래스 정보를 읽어 들이는 순간터 프로그램 종료까지 존재한다.
  • 인스턴스 변수 : 인스턴스의 생존 기간(힙 영역)

    • 인스턴스 변수는 본인이 소속된 인스턴스가 GC 되기 전까지 존재한다. 생존 주기가 긴 편이다.
  • 지역 변수 : 메서드 호출이 끝나면 사라짐(스택 영역)

    • 지역 변수는 스택 영역의 스택 프레임 안에 존재한다. 따라서 메서드가 호출되면 생성되고, 메서드 호출이 종료되면 스택 프레임이 제거되면서 그 안에 있는 지역 변수도 모두 제거된다. 생존 주기가 아주 짧다. 참고로 매겨변수도 지역 변수의 한 종류이다.

지역 클래스 예제

public class LocalOuterV3 {

    private int outInstanceValue = 3;

    public Printer process(int paramVar) {
        int localVar = 1; // 지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.

        class LocalPrinter implements Printer{
            int value = 0;
            @Override
            public void print() {
                System.out.println("value=" + value);

                // 인스턴스는 지역 변수보다 오래 살아 남는다.
                System.out.println("localVar = " + localVar);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outInstanceValue = " + outInstanceValue);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        // printer.print(); 를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.
        return printer;
    }

    public static void main(String[] args) {
        LocalOuterV3 localOuter = new LocalOuterV3();
        Printer printer = localOuter.process(2);
        // Prnter.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
        printer.print();
    }
}

실행 결과

 value=0
 localVar=1
 paramVar=2
 outInstanceVar=3
  • process()Printer 타입을 반환한다. 여기서는 LocalPrinter 인스턴스를 반환한다.

  • process() 메서드가 종료된 이후에 printer.print() 메서드를 실행한다.

LocalPrinter 인스턴스 생성 직후 메모리 그림

지역 클래스 인스턴스의 생존 범위

  • 지역 클래스로 만든 객체도 인스턴스이기 때문에 힙 영역에 존재한다. 따라서 GC 전까지 생존한다.

    • LocalPrinter 인스턴스는 porcess() 메서드 안에서 생성된다. 그리고 process() 에서 main() 으로 생성한 LocalPrinter 인스턴스를 반환하고 printer 변수에 LocalPrinter 인스턴스의 참조값을 보관한다. 따라서 LocalPrinter 인스턴스는 main() 이 종료될 때까지 생존한다.
  • paramVar, localVar 와 같은 지역 변수는 process() 메서드를 실행하는 동안에만 스택 영역에서 생존한다. process() 메서드가 종료되면 process() 스택 프레임이 스택 영역에서 제거 되면서 함께 제거된다.

LocalPrinter.print() 접근 메모리 그림

  • LocalPrinter 인스턴스는 print() 메서드를 통해 힙 영역에 존재하는 바깥 인스턴스의 변수인 outInstanceVar 에 접근한다. 이 부분은 인스턴스의 필드를 참조하는 것이기 때문에 특별한 문제가 없다.

  • LocalPrinter 인스턴스는 print() 메서드를 통해 스택 영역에 존재하는 지역 변수도 접근하는 것처럼 보인다. 하지만 스택 영역에 존재하는 지역 변수를 힙 영역에 있는 인스턴스가 접근하는 것은 생걱처럼 단순하지 않다.

process() 메서드의 종료

  • 지역 변수의 생명주기는 매우 짧다. 반면에 인스턴스의 생명주기는 GC 전까지 생존할 수 있다.

  • 지역 변수인 paramVarl, localVarporcess() 메서드가 실행되는 동안에만 생존할 수 있다. process() 메서드가 종료되면 process() 의 스택 프레임이 제거되면서 두 지역 변수도 함께 제거된다.

  • 여기서 문제는 process() 메서드가 종료되어도 LocalPrinter 인스턴스는 계속 생존할 수 있다는 점이다.

process() 메서드가 종료된 이후에 지역 변수 접근

  • process() 메서드가 종료된 이후에 main() 메서드 안에서 LocalPrinter.print() 메서드를 호출한다.

  • LocalPrinter 인스턴스에 있는 print() 메서드는 지역 변수인 paramVar, localVar 에 접근해야 한다. 하지만 process() 메서드가 이미 종료되었으므로 해당 지역 변수들도 이미 제거된 상태이다.

하지만 실행 결과를 보면 모두 정상적으로 출력되는 것을 확인할 수 있다.

지역 변수 캡처

자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이런 과정을 변수 캡처(Capture)라 한다. 접근이 필요한 지역 변수만 캡처한다.

지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정1

  • 1. LocalPrinter 인스턴스 생성 시도 : 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인한다.

    • LocalPrinter 클래스는 paramVar, localVar 지역 변수에 접근한다.
  • 2. 사용하는 지역 변수 복사 : 지역 클래스가 사용하는 지역 변수를 복사한다. (매개변수도 지역 변수의 한 종류이다.)

    • 여기서는 paramVar, localVar 지역 변수를 복사한다.

지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정2

  • 3. 지역 변수 복사 완료 : 복사한 지역 변수를 인스턴스에 포함한다.

  • 4. 인스턴스 생성 완료 : 복사한 지역 변수를 포함해서 인스턴스 생성이 완료된다. 이제 복사한 지역 변수를 인스턴스를 통해 접근할 수 있다.

캡처 변수 접근

  • LocalPrinter 인스턴스에서 print() 메서드를 통해 paramVar, localVar 에 접근하면 사실은 스택 영역에 있는 지역 변수에 접근하는 것이 아니다. 대신에 인스턴스에 있는 캡처한 변수에 접근한다.

  • 캡처한 paramVar, localVar 의 생명주기는 LocalPrinter 인스턴스의 생명주기와 같다. 따라서 LocalPrinter 인스턴스는 지역 변수의 생명주기와 무관하게 언제든지 paramVar, localVar 캡처 변수에 접근할 수 있다.

  • 이렇게 해서 지역 변수와 지역 클래스를 통해 생성한 인스턴스의 생명주기가 달라서 생기는 문제를 해결한다.

코드로 캡처 변수 확인

public static void main(String[] args) {
	LocalOuterV3 localOuter = new LocalOuterV3();
    Printer printer = localOuter.process(2);
    // Prnter.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
    printer.print();

	//추가
	System.out.println("필드 확인");
    Field[] fields = printer.getClass().getDeclaredFields();
    for (Field field : fields) {
    	System.out.println(field);
	}
}

실행 결과

필드 확인
//인스턴스 변수
field = int nested.local.LocalOuterV3$1LocalPrinter.value

//캡처 변수
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$localVar
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$paramVar

//바깥 클래스 참조
field = final nested.local.LocalOuterV3 
nested.local.LocalOuterV3$1LocalPrinter.this$0

정리

지역 클래스는 인스턴스를 생성할 때 필요한 지역 변수를 먼저 캡처해서 인스턴스에 보관한다. 그리고 지역 클래스의 인스턴스를 통해 지역 변수에 접근하면, 실제로는 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 캡처 변수에 접근한다.

지역 클래스가 접근하는 지역 변수는 final

지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다. 따라서 final 로 선언하거나 또는 사실상 final 이어야 한다. 이것은 자바 문법이고 규칙이다.

사실상 final

영어로 effectively final이라 한다. 사실상 final 지역 변수는 지역 변수에 final 키워드를 사용하지는 않았지만, 값을 변경하지 않는 지역 변수를 뜻한다. final 키워드를 넣지 않았을 뿐이지, 실제로는 final 키워드를 넣은 것처럼 중간에 값을 변경하지 않은 지역 변수이다. 따라서 사실상 final 지역 변수는 final 키워드를 넣어도 동일하게 작동해야 한다.

중간에 값이 변하면 안되는 이유

  • 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야 한다.

  • 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 한다.

  • 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티 쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다.

8.3 익명 클래스

익명 클래스(anonymous class)는 지역 클래스의 특별한 종류의 하나이다.

익명 클래스는 지역 클래스인데, 클래스의 이름이 없다는 특징이 있다.

익명 클래스 - 지역 클래스의 선언과 생성을 한번에

Printer printer = new Printer(){
	//body
}

new Printer() { body }

익명 클래스는 클래스의 본문(body)을 정의하면서 동시에 생성한다. new 다음에 바로 상속 받으면서 구현할 부모 타입을 입력하면 된다.

이 코드는 마치 인터페이스 Printer 를 생성하는 것처럼 보인다. 하지만 자바에서 인터페이스를 생성하는 것은 불가능하다. 이 코드는 인터페이스를 생성하는 것이 아니고, Printer 라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것이다. {body} 부분에 Printer 인터페이스를 구현한 코드를 작성하면 된다. 이 부분이 바로 익명 클래스의 본문이 된다. 쉽게 이야기해서 Printer 를 상속(구현)하면서 바로 생성하는 것이다.

익명 클래스 특징

  • 익명 클래스는 이름 없는 지역 클래스를 선언하면서 동시에 생서한다.

  • 익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 구현해야 한다. 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요하다.

  • 익명 클래스는 말 그대로 이름이 없다. 이름을 가지지 않으므로, 생성자를 가질 수 없다.(기본 생성자만 사용됨)

  • 익명 클래스는 바깥 클래스 이름 $ 숫자 로 정의된다. 익명 클래스가 여러개면 숫자가 증가하면서 구분된다.

익명 클래스의 장점

익명 클래스를 사용하면 클래스르 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 더 간결해진다.

익명 클래스 활용

어떻게 외부에서 코드 조각을 전달할 수 있을까?

코드 조각은 보통 메서드(함수)에 정의한다. 따라서 코드 조각을 전달하기 위해서는 메서드가 필요하다. 대신에 인스턴스를 전달하고, 인스턴스에 있는 메서드를 호출하면 된다.

Process 인터페이스

public interface Process {
    void run();
}

인스턴스를 구현하여 익명 클래스를 정의하고 그 안에 전달하고 싶은 메서드를 구현하여 넘겨준다.

public class Ex1RefMainV3 {

    public static void main(String[] args) {

        hello(new Process() {
            @Override
            public void run() {
                int randomValue = new Random().nextInt(6) + 1;
                System.out.println("주사위 = " + randomValue);
            }
        });

        hello(new Process() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        });
    }

    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        process.run();
        System.out.println("프로그램 종료");
    }
}

람다(lamda)

자바8에 들어서면서 큰 변화가 있었는데 바로 메서드(더 정확히는 함수)를 인수로 전달할 수 있게 되었다.

public class Ex1RefMainV4 {

    public static void main(String[] args) {

        hello(() -> {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        });

        hello(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }

    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        process.run();
        System.out.println("프로그램 종료");
    }
}

profile
가오리의 개발 이야기

0개의 댓글