[Java] 김영한의 실전 자바 - 중급편 섹션8 중첩 클래스,내부 클래스2
○ 지역 클래스
지역 클래스는 내부클래스의 한 종류로, 지역 변수와 같이 코드 블럭 안에서 정의된다.
또한 내부,중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나, 부모 클래스를 상속할 수 있다.
class Outer {
public void process() {
//지역 변수
int localVar = 0;
//지역 클래스
class Local {...}
Local local = new Local();
}
}
○ 지역 클래스 - 지역 변수 캡처
🟢변수 생명 주기
- 클래스 변수: 프로그램 종료 까지, 가장 길다(메서드 영역)
- 인스턴스 변수: 인스턴스 생존 기간(힙 영역)
- 지역 변수: 메서드 호출이 끝나면 사라짐 (스택영역)
위를 염두하고 아래를 보자. LocalPrinter.print() 메서드를 process() 메서드가 종료된 이후에 main() 메서드에서 실행한다.
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);
}
} // LocalPrinter class 끝
Printer printer = new LocalPrinter();
return printer; // Printer 인스턴스를 반환한다.
}
public static void main(String[] args) {
LocalOuterV3 localOuter = new LocalOuterV3();
Printer printer = localOuter.process(2);
//printer.print() 를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
printer.print();
}
}
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
지역 변수의 생명주기는 매우 짧기때문에, 인스턴스 생명 주기는 GC 전까지 생존할 수 있다.
지역 변수인 paramVar,localVar는 process()메서드가 실행되는 동안에만 생존할 수 있다.
또한 process() 메서드가 종료되어도 LocalPrinter 인스턴스는 계속 생존할 수 있다.
그런데 어떻게 process() 메서드가 종료되어도 제거된 지역 변수들에 접근할 수 있는 걸까?
지역변수 생명 주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명 주기는 길다.
따라서 인스턴스가 지역변수에 접근하려 할때 지역 변수는 이미 제거된 상태일 수 있다.
🟢지역 변수 캡처
위 문제를 해결하기 위해, 인스턴스를 생성할 때 필요한 지역 변수를 복사해서 보관해 둔다.
(모든 지역 변수를 캡처하는 것이 아니라 접근이 필요한 지역변수만 캡처한다.)
LocalPrinter 인스턴스를 생성할때, 지역 클래스가 사용하는 지역 변수를 복사(paramVar , localVar)하고, 복사한 지역 변수를 인스턴스에 포함한다.
그럼 이제 복사한 지역 변수를 인스턴스를 통해 접근할 수 있다.
LocalPrinter 인스턴스에서 print() 메서드를 통해 paramVar , localVar 에 접근하면 사실은 스택 영역에 있는 지역 변수에 접근하는 것이 아니다. 대신에 인스턴스에 있는 캡처한 변수에 접근하는것이다.
캡처한 paramVar , localVar 의 생명주기는 LocalPrinter 인스턴스의 생명주기와 같다.
이렇게 해서 지역 변수와 지역 클래스를 통해 생성한 인스턴스의 생명주기가 다른 문제를 해결한다.
실제 클래스 필드를 찍어보면 바깥 클래스 참조 필드와, 캡처 변수를 확인할 수 있다.
이런 필드들은 자바가 내부에서 만들어 사용하는 필드들이다.
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 inner2.local.LocalOuterV3$1LocalPrinter.value // 인스턴스 변수
field = final int inner2.local.LocalOuterV3$1LocalPrinter.val$localVar // 캡처 변수
field = final int inner2.local.LocalOuterV3$1LocalPrinter.val$paramVar
field = final inner2.local.LocalOuterV3 inner2.local.LocalOuterV3$1LocalPrinter.this$0 // 바깥 클래스 참조
지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다.
따라서 final 로 선언하거나 또는 사실상 final 이어야 한다. 이것은 자바 문법이고 규칙이다.
🔶동기화 문제 : 만약 값 변경시, 스택영역에 존재하는 지역변수와 인스턴스에 캡처한 캡처 변수 값이 서로 달라지는 문제가 발생
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);
}
} // LocalPrinter class 끝
Printer printer = new LocalPrinter();
// 만약 localVar의 값을 변경한다면? 다시 캡처해야 하나??
// localVar = 10; // 컴파일 오류
// paramVar = 20; // 컴파일 오류
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);
}
}
}
🟢캡처 변수 값 변경을 못하는 이유
- 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야 한다.
- 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 한다.
- 개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경될 수 있다. 이는 디버깅을 어렵게 한다.
- 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티쓰레드 상황에서 이런 동기화
는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다. (?)
○ 익명 클래스
지역클래스를 사용하기 위해 원래는 선언과 생성 2가지 단계를 거쳤다.
//선언
class LocalPrinter implements Printer{
//body
}
//생성
Printer printer = new LocalPrinter();
public class LocalOuterV2 {
private int outInstanceVar = 3;
public void process(int paramVar) {
int localVar = 1;
// 지역클래스를 LocalPrinter()로 선언.Printer인터페이스도 함께 구현
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);
}
}
아래처럼 익명 클래스를 사용해볼수 있다.
public class LocalOuterV2 {
private int outInstanceVar = 3;
public void process(int paramVar) {
int localVar = 1;
//Printer 라는 이름의 인터페이스를 구현한 익명 클래스를 생성
Printer printer = new Printer() { //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
printer.class=class nested.anonymous.AnonymousOuter$1
🟢익명 클래스 특징
- 익명클래스는 이름이 없는 지역 클래스
- 특정 부모 클래스(인터페이스)를 상속 받고 바로 생성하는 경우 사용
- new다음에 바로 상속받으면서 구현할 타입을 입력, {body} 부분에 인터페이스를 구현한 코드를 작성
- 익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 구현해야 한다. 익명 클래스를 사용할 때는 상위 클
래스나 인터페이스가 필요하다. - 익명 클래스는 말 그대로 이름이 없다. 이름을 가지지 않으므로, 생성자를 가질 수 없다. (기본 생성자만 사용됨)
- 익명 클래스는 AnonymousOuter$1 과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의된다. 익명 클
래스가 여러개면 $1 , $2 , $3 으로 숫자가 증가하면서 구분된다
🟢익명 클래스 장점
- 인터페이스나 추상 클래스를 즉석 구현할 수 있어 코드가 더 간결해진다.
- 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용한다.
○ 익명 클래스 활용
프로그래밍에서 중복을 제거하고, 좋은 코드를 유지하는 핵심 중 하나는변하는 부분과 변하지 않는 부분을 분리하는 것이다.
이를 염두하고 아래를 리팩토링해보자. 아래 중복제거를 위해서는 코드 조각을 전달해야 한다.
import java.util.Random;
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();
}
}
코드 조각을 전달해야하는데, 코드 조각은 보통 메서드(함수)에 정의한다.하지만 메서드를 전달할 수 있는방이 없기에, 인스턴스를 전달하고 인스턴스에 있는 메서드를 호출하면 된다.
이를 위해 인터페이스를 정의하고 구현 클래스를 만들었다.이를통해 다형성을 활용해 필요한 코드 조각을 실행할수있다.
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{
//..
}
class Sum implements Process{
//..
}
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) {
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 static void main(String[] args) {
System.out.println("Hello 실행");
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부터 메서드를 인수로 전달할 수 있게되었는데,이를 람다(lambda)라고 한다.
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);
}
});
}
}
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 중급편 섹션10 예외처리2 - 실습 (0) | 2024.05.23 |
---|---|
[Java] 김영한의 실전 자바 - 중급편 섹션9 예외처리1 - 이론 (0) | 2024.05.22 |
[Java] 김영한의 실전 자바 - 중급편 섹션5 열거형 - ENUM (0) | 2024.05.09 |
[Java] 김영한의 실전 자바 - 중급편 섹션3,4 String ,래퍼,Class 클래스 (0) | 2024.05.09 |
[Java] 김영한의 실전 자바 - 중급편 섹션2 불변 객체 (0) | 2024.05.05 |