[Java] 자바 중급(7) 중첩 클래스, 내부 클래스2

wony·2024년 3월 31일

Java

목록 보기
14/30

0.개요

주제 : 김영한님의 자바 중급 1편 총 정리
내용 : 중첩 클래스, 내부 클래스2에 대해 공부

1.지역 클래스

1) 지역 클래스

  • 지역 클래스는 내부 클래스의 특별한 종류의 하나이다. 따라서, 내부 클래스의 특징을 그대로 가진다.
  • 지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의한다.

지역 클래스 예

class Outer {
	public void process() {

	//지역 변수
	int localVar = 0;

	//지역 클래스
	class Local {...}

	Local local = new Local();
	}
}

지역 클래스 예제 1

public class LocalOuterV1 {

    private int outInstanceVar = 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("outInstanceVar=" + outInstanceVar);
            }
        }

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

    public static void main(String[] args) {
        LocalOuterV1 localOuter = new LocalOuterV1();
        localOuter.process(2);
    }
}

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3

지역 클래스의 접근 범위

  • 자신의 인스턴스 변수인 value 에 접근할 수 있다.
  • 자신이 속한 코드 블럭의 지역 변수인 localVar 에 접근할 수 있다.
  • 자신이 속한 코드 블럭의 매개변수인 paramVar 에 접근할 수 있다. 참고로 매개변수도 지역 변수의 한 종류이다.
  • 바깥 클래스의 인스턴스 멤버인 outInstanceVar 에 접근할 수 있다. (지역 클래스도 내부 클래스의 한 종류이다.)
  • 지역 클래스는 지역 변수 처럼 접근 제어자를 사용할 수 없다.

지역 클래스 예제2
내부 클래스를 포함한 중첩 클래스도 일반 클래스처럼 인스페이스를 구현하거나, 부모 클래스를 상속할 수 있다.

public interface Printer {
    void print();
}
public class LocalOuterV2 {

    private int outInstanceVar = 3;

    public void 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("outInstanceVar=" + outInstanceVar);
            }
        }

        Printer printer = new LocalPrinter();
        printer.print();
    }

    public static void main(String[] args) {
        LocalOuterV2 localOuter = new LocalOuterV2();
        localOuter.process(2);
    }
}

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3

2) 지역 클래스 - 지역 변수 캡처1

지역 클래스를 더 자세히 알아려보려면 변수와 생명 주기에 대해 정리해야 한다.

  • 클래스 변수 : 프로그램 종료까지, 가장 길다(메서드 영역)
  • 인스턴스 변수 : 인스턴스의 생존 기간(힙 영역)
  • 지역 변수 : 메서드 호출이 끝나면 사라짐, 매우 짧다.(스택영역)
    • 지역 변수는 매우 짧은데 어떻게 localVar, paramVar에 접근할 수 있을까?

지역 클래스 예제3

public class LocalOuterV3 {

    private int outInstanceVar = 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("outInstanceVar=" + outInstanceVar);
            }
        }
        Printer printer = new LocalPrinter();

        //printer.print()를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.
        return printer;
    }
    public static void main(String[] args) {
        LocalOuterV3 localOuter = new LocalOuterV3();
        Printer printer = localOuter.process(2);

        //printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
        printer.print();
    }
}
  • process()Printer 타입을 반환한다. 여기서는 LocalPrinter 인스턴스를 반환한다.
  • 여기서는 LocalPrinter.print() 메서드를 process() 안에서 실행하는 것이 아니라 process() 메서드가 종료된 이후에 main() 메서드에서 실행한다.

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3
  • 이 예제를 실행하면서 뭔가 이상한 느낌이 들었다
  • process() 메서드가 종료된 이후에 지역 변수에 접근하는데 어떻게 모두 정상적으로 출력이 되는걸까? -> 지역 변수 캡처

3) 지역 클래스 - 지역 변수 캡처2

  • 지역 클래스는 지역 변수에 접근할 수 있다.
  • 그런데, 앞서 본 것 처럼 지역 변수의 생명주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명 주기는 길다.
  • 지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명 주기가 다르기 때문에 인스턴스는 살아있지만, 지역 변수는 이미 제거된 상태일 수 있다.

지역 변수 캡처

  • 자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이런 과정을 변수 캡처(Capture)라 한다.
  • 캡처라는 단어는 스크린 캡처를 떠올려 보면 바로 이해가 될 것이다. 인스턴스를 생성할 때 필요한 지역 변수를 복사해서 보관해 두는 것이다.
    • 물론, 모든 지역 변수를 캡처하는 것이 아니라 접근이 필요한 지역 변수만 캡처한다.

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

  • 1. LocalPrinter 인스턴스 생성 시도: 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인한다.
    • LocalPrinter 클래스는 paramVar , localVar 지역 변수에 접근한다.
  • 2. 사용하는 지역 변수 복사: 지역 클래스가 사용하는 지역 변수를 복사한다. (매개변수도 지역 변수의 한 종류이다)
    • 여기서는 paramVar, localVar 지역 변수를 복사한다.

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

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

  • LocalPrinter 인스턴스에서 print() 메서드를 통해 paramVar , localVar 에 접근하면 사실은 스택영역에 있는 지역 변수에 접근하는 것이 아니다. 대신에 인스턴스에 있는 캡처한 변수에 접근한다.
  • 캡처한 paramVar , localVar 의 생명주기는 LocalPrinter 인스턴스의 생명주기와 같다. 따라서LocalPrinter 인스턴스는 지역 변수의 생명주기와 무관하게 언제든지 paramVar , localVar 캡처 변수에 접근할 수 있다.
  • 이렇게 해서 지역 변수와 지역 클래스를 통해 생성한 인스턴스의 생명주기가 다른 문제를 해결한다.

코드로 캡처 변수 확인
LocalOuterV3 - 추가

public class LocalOuterV3 {

    private int outInstanceVar = 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("outInstanceVar=" + outInstanceVar);
            }
        }
        Printer printer = new LocalPrinter();

        //printer.print()를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.
        return printer;
    }
    public static void main(String[] args) {
        LocalOuterV3 localOuter = new LocalOuterV3();
        Printer printer = localOuter.process(2);

        //printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
        printer.print();

        //추가
        System.out.println("필드 확인");
        Field[] fields = printer.getClass().getDeclaredFields();
        for (Field field : fields) {
            System.out.println("field = " + 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
  • 실행 결과를 통해 LocalPrinter 클래스의 캡처 변수를 확인할 수 있다.
  • 추가로 바깥 클래스를 참조하기 위한 필드도 확인할 수 있다. 참고로 이런 필드들은 자바가 내부에서 만들어 사용하는 필드들이다.

정리

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

4) 지역 클래스 - 지역 변수 캡처3 (중요!)

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

public class LocalOuterV4 {
    private int outInstanceVar = 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("outInstanceVar=" + outInstanceVar);
            }
        }
        Printer printer = new LocalPrinter();
        // 만약 localVar 의 값을 변경한다면? 다시 캡처해야 하나??
        // localVar = 10; // 컴파일 오류
        // paramVar = 20; // 컴파일 오류

        return printer;
    }
    public static void main(String[] args) {
        LocalOuterV4 localOuter = new LocalOuterV4();
        Printer printer = localOuter.process(2);
        printer.print();
    }
}

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3

Printer printer = new LocalPrinter();

  • LocalPrinter 를 생성하는 시점에 지역 변수인 localVar , paramVar 를 캡처한다.

그런데 이후에 캡처한 지역 변수의 값을 다음과 같이 변경하면 어떻게 될까?

Printer printer = new LocalPrinter()
// 만약 localVar의 값을 변경한다면? 다시 캡처해야 하나??
localVar = 10; // 컴파일 오류
paramVar = 20; // 컴파일 오류
  • 이렇게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생한다. 이것을 동기화 문제라 한다.(중요!)

2. 익명 클래스 - 시작

  • 익명 클래스는 지역 클래스의 특별한 종류 중 하나이다.
  • 익명 클래스는 지역 클래스인데, 클래스의 이름이 없다는 특징이 있다.
  • 앞서 지역 클래스 예제 코드인 LocalOuterV2 코드를 살펴보자.
  • 여기서는 지역 클래스를 사용하기 위해 선언과 생성이라는 2가지 단계를 거친다.
    • 선언 : 지역 클래스를 LocalPrinter라는 이름으로 선언한다. 이때 Printer 인터페이스도 함께 구현한다.
    • 생성: new LocalPrinter() 를 사용해서 앞서 선언한 지역 클래스의 인스턴스를 생성한다.

지역 클래스의 선언과 생성

//선언
class LocalPrinter implements Printer{
	//body
}
//생성
Printer printer = new LocalPrinter();

익명 클래스 사용하면 클래스의 이름을 생략하고, 클래스의 선언과 생성 한번에 처리할 수 있다.

익명 클래스

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

익명 클래스 예시

public class AnonymousOuter {

    private int outInstanceVar = 3;

    public void process(int paramVar) {

        int localVar = 1;

        Printer printer = new 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("outInstanceVar=" + outInstanceVar);
            }
        };
        printer.print();
        System.out.println("printer.class=" + printer.getClass());

    }
    public static void main(String[] args) {
        AnonymousOuter main = new AnonymousOuter();
        main.process(2);
    }
}

실행 결과

value=0
localVar=1
paramVar=2
outInstanceVar=3
printer.class=class nested.anonymous.AnonymousOuter$1

new Printer() {body}

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

익명 클래스 특징
익명 클래스는 이름 없는 지역 클래스를 선언하면서 동시에 생성한다.
익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 구현해야 한다.

익명 클래스를 사용할 수 없을 때

  • 익명 클래스는 단 한 번만 인스턴스를 생성할 수 있다.
  • 다음과 같이 여러 번 생성이 필요하다면 익명 클래스를 사용할 수 없다.
    대신에 지역 클래스를 선언하고 사용하면 된다.
Printer printer1 = new LocalPrinter();
printer1.print();
Printer printer2 = new LocalPrinter();
printer2.print();

정리

  • 익명 클래스는 이름이 없는 지역 클래스이다.
  • 특정 부모 클래스를 상속 받고 생성하는 경우 사용한다.
  • 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용한다.

1) 익명 클래스 활용(2)

리팩토링 전

public class Ex1Main {
    public static void helloDice() {
        System.out.println("프로그램 시작");
        //코드 조각 시작
        int randomValue = new Random().nextInt(6) + 1;
        System.out.println("주사위 = " + randomValue);
        //코드 조각 종료
        System.out.println("프로그램 종료");
    }
    public static void helloSum() {
        System.out.println("프로그램 시작");
        //코드 조각 시작
        for (int i = 1; i <= 3; i++) {
            System.out.println("i = " + i);
        }
        //코드 조각 종료
        System.out.println("프로그램 종료");
    }
    public static void main(String[] args) {
        helloDice();
        helloSum();
    }
}

실행 결과

프로그램 시작
주사위 = 5 //랜덤
프로그램 종료
프로그램 시작
i = 1
i = 2
i = 3
프로그램 종료

리팩토링 후

  • Dice , Sum 각각의 클래스는 Process 인터페이스를 구현하고 run() 메서드에 필요한 코드 조각을 구현한다.
  • Process process 매개변수를 통해 인스턴스를 전달할 수 있다. 이 인스턴스의 run() 메서드를 실행하면 필요한 코드 조각을 실행할 수 있다.
  • 이때 다형성을 활용해서 외부에서 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행된다.
public interface Process {
    void run();
}
public class Ex1RefMainV1 {

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

    static class Dice implements Process {

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

    static class Sum implements Process {

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

    public static void main(String[] args) {

        Process dice = new Dice();
        Process sum = new Sum();

        System.out.println("Hello 실행");
        hello(dice);
        hello(sum);
    }
}

2) 익명 클래스 활용3

2-1) 지역 클래스 사용

public class Ex1RefMainV2 {

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

        //코드 조각 시작
        process.run();

        //코드 조각 종료
        System.out.println("프로그램 종료");
    }
    public static void main(String[] args) {
        class Dice implements Process {
            @Override
            public void run() {
                int randomValue = new Random().nextInt(6) + 1;
                System.out.println("주사위 = " + randomValue);
            }
        }
        class Sum implements Process {
            @Override
            public void run() {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        }
        Process dice = new Dice();
        Process sum = new Sum();
        System.out.println("Hello 실행");
        hello(dice);
        hello(sum);
    }
}

2-2) 익명 클래스 사용1

앞의 지역 클래스는 간단히 한번만 생성해서 사용한다.
이런 경우 익명 클래스로 변경할 수 있다.

public class Ex1RefMainV3 {
    public static void hello(Process process) {
        System.out.println("프로그램 시작");

        //코드 조각 시작
        process.run();

        //코드 조각 종료
        System.out.println("프로그램 종료");
    }
    public static void main(String[] args) {
        Process dice = new Process() {
            @Override
            public void run() {
                int randomValue = new Random().nextInt(6) + 1;
                System.out.println("주사위 = " + randomValue);
            }
        };
        Process sum = new Process() {   // 여기 익명 클래스
            @Override
            public void run() {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        };
        System.out.println("Hello 실행");
        hello(dice);
        hello(sum);
    }
}

익명 클래스 사용2 - 참조값 직접 전달

이 경우 익명 클래스의 참조값을 변수에 담아 둘 필요 없이 바로 전달 가능

public class Ex1RefMainV4 {

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

        //코드 조각 시작
        process.run();

        //코드 조각 종료
        System.out.println("프로그램 종료");
    }
    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 = 1; i <= 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        });
    }
}

3) 람다

자바8 이전까지 메서드에 인수로 전달할 수 있는 것은 크게 2가지였다.

  • int , double 과 같은 기본형 타입
  • Process Member 와 같은 참조형 타입(인스턴스)
    결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나, 인스턴스의 참조이다.
  • 지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스를 꼭 생성해서 전달해야 할까?
  • 생각해보면 클래스나 인스턴스와 관계 없이 다음과 같이 메서드만 전달할 수 있다면 더 간단하지 않을까?
  • 자바8에 들어서면서 큰 변화가 있었는데 바로 메서드(더 정확히는 함수)를 인수로 전달할 수 있게 되었다. 이것을 간단히 람다( Lambda )라 한다.
public class Ex1RefMainV5 {
    public static void hello(Process process) {
        System.out.println("프로그램 시작");

        //코드 조각 시작
        process.run();

        //코드 조각 종료
        System.out.println("프로그램 종료");
    }
    public static void main(String[] args) {
        hello(() -> {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        });
        hello(() -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }
}
profile
안녕하세요. wony입니다.

0개의 댓글