주제 : 김영한님의 자바 중급 1편 총 정리
내용 : 중첩 클래스, 내부 클래스2에 대해 공부
지역 클래스 예
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
지역 클래스를 더 자세히 알아려보려면 변수와 생명 주기에 대해 정리해야 한다.
- 클래스 변수 : 프로그램 종료까지, 가장 길다(메서드 영역)
- 인스턴스 변수 : 인스턴스의 생존 기간(힙 영역)
- 지역 변수 : 메서드 호출이 끝나면 사라짐, 매우 짧다.(스택영역)
- 지역 변수는 매우 짧은데 어떻게
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()메서드가 종료된 이후에 지역 변수에 접근하는데 어떻게 모두 정상적으로 출력이 되는걸까? -> 지역 변수 캡처
- 지역 클래스는 지역 변수에 접근할 수 있다.
- 그런데, 앞서 본 것 처럼 지역 변수의 생명주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명 주기는 길다.
- 지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명 주기가 다르기 때문에 인스턴스는 살아있지만, 지역 변수는 이미 제거된 상태일 수 있다.
지역 변수 캡처
- 자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이런 과정을 변수 캡처(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클래스의 캡처 변수를 확인할 수 있다.- 추가로 바깥 클래스를 참조하기 위한 필드도 확인할 수 있다. 참고로 이런 필드들은 자바가 내부에서 만들어 사용하는 필드들이다.
정리
- 지역 클래스는 인스턴스를 생성할 때 필요한 지역 변수를 먼저 캡처해서 인스턴스에 보관한다.
- 그리고 지역 클래스의 인스턴스를 통해 지역 변수에 접근하면, 실제로는 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 캡처 변수에 접근한다.
지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다.
따라서 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; // 컴파일 오류
- 이렇게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생한다. 이것을 동기화 문제라 한다.(중요!)
- 익명 클래스는 지역 클래스의 특별한 종류 중 하나이다.
- 익명 클래스는 지역 클래스인데, 클래스의 이름이 없다는 특징이 있다.
- 앞서 지역 클래스 예제 코드인
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();
정리
리팩토링 전
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); } }
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); } }
앞의 지역 클래스는 간단히 한번만 생성해서 사용한다.
이런 경우 익명 클래스로 변경할 수 있다.
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); } }
이 경우 익명 클래스의 참조값을 변수에 담아 둘 필요 없이 바로 전달 가능
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); } } }); } }
자바8 이전까지 메서드에 인수로 전달할 수 있는 것은 크게 2가지였다.
int,double과 같은 기본형 타입ProcessMember와 같은 참조형 타입(인스턴스)
결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나, 인스턴스의 참조이다.
- 지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스를 꼭 생성해서 전달해야 할까?
- 생각해보면 클래스나 인스턴스와 관계 없이 다음과 같이 메서드만 전달할 수 있다면 더 간단하지 않을까?
- 자바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); } }); } }