중첩 클래스 종류
- 정적 중첩 클래스 : 정적 중첩 클래스는 바깥 클래스의 안에 있지만 바깥 클래스와 관계 없는 전혀 다른 클래스
- 내부 클래스 : 바깥 클래스의 내부에 있으면서 바깥 클래스의 인스턴스에 소속되는 클래스
- 내부 클래스 : 바깥 클래스의 인스턴스 멤버에 접근
- 지역 클래스 : 내부 클래스 특징 + 지역 변수에 접근
- 익명 클래스 : 지역 클래스 특징 + 클래스 이름이 없음
사용하는 이유
- 논리적 그룹화 : 특정 클래스가 다른 클래스 안에서만 사용되는 경우(다른 곳에서 사용될 필요가 없을 때)
- 캡슐화 : 중첩 클래스는 바깥 클래스의 private 멤버에 접근이 가능, 바깥 클래스와의 긴밀하게 연결하고 불필요한 public 삭제
정적 중첩 클래스
public class NestedOuter {
private static String outClassValue = "외부 static 변수";
private String outInstanceValue = "외부 클래스 변수";
static class Nested {
private String nestedInstanceValue = "내부 클래스 변수";
public void print() {
System.out.println(nestedInstanceValue + " (자신의 멤버 호출)");
System.out.println(new NestedOuter().outInstanceValue
+" (외부 클래스의 인스턴스도 같이 호출해야 외부멤버 호출가능)");
System.out.println(NestedOuter.outClassValue
+ " (외부 static 멤버 호출)");
}
}
public static void main(String[] args) {
NestedOuter.Nested nested = new NestedOuter.Nested();
nested.print();
// NestedOuter 클래스와 Nested 클래스는 실질적으로 아무런 관련이 없어
// 정적 중첩 클래스의 인스턴스만 따로 생성하여 메서드를 호출한다.
System.out.println("Nested 클래스 위치 = " + nested.getClass());
}
}
결과
콘솔 결과 | |
내부 클래스 변수 (자신의 멤버 호출) | |
외부 클래스 변수 (외부 클래스의 인스턴스도 같이 호출해야 외부멤버 호출가능) | ▶ Nested 클래스 영역은 메서드 영역에 보관 OuterNested 클래스 영역은 힙 영역에 보관 보관되는 영역이 다르기 때문에 서로 중첩되어있지만, 서로 다른 클래스로 보아야 한다 |
외부 static 변수 (외부 static 멤버 호출) | |
Nested 클래스 위치 = class NestedOuter$Nested | NestedOuter 클래스 안에 공간만 빌린($=static 표시) Nested 클래스 |
결론 : 정적 중첩 클래스는 그냥 중첩 해둔 것, 둘은 아무 관련이 없으며, 그냥 클래스 2개를 따로 만든 것과 같다.
단, 차이가 있다면 같은 클래스 공간에 있으니, 바깥에 있는 private 멤버 변수에 접근할 수 있다는 정도이다.
내부 클래스
public class InnerOuter {
private static String outClassValue = "바깥 static 변수";
private String outInstanceValue = "바깥 멤버 변수";
class Inner {
private String innerInstanceValue = "안 멤버 변수";
public void print() {
System.out.println(innerInstanceValue);
System.out.println(outInstanceValue);
System.out.println(outClassValue);
}
}
public static void main(String[] args) {
InnerOuter innerOuter = new InnerOuter();
Inner inner = innerOuter.new Inner();
inner.print();
}
}
결과
안 멤버 변수 바깥 멤버 변수 바깥 static 변수 |
※ 만약, 내부 클래스의 멤버변수와 외부 클래스의 멤버변수 이름이 같다면, 클래스의 this가 기준이 된다. (더 가깝거나, 구체적인것이 우선권) 이렇게 외부 클래스의 멤버변수는 가려져 보이지 않게 되는데 이를 Shadowing이라고 한다.
Inner 클래스는 InnerOuter 인스턴스에 속하므로, 바깥 클래스의 멤버변수, 클래스 멤버에 접근할 수 있다.
(실제로 바깥 인스턴스 안에 생성되는게 아니라, 내부 인스턴스는 바깥 인스턴스의 참조를 보관한다 이를 이용해 바깥 인스턴스의 멤버에 접근 할 수 있는 것)
또한, 호출시에도 바깥 클래스의 인스턴스를 경로로 불러와야 한다
내부 클래스는 `바깥클래스의 인스턴스 참조.new 내부클래스()` 로 생성할 수 있다. |
지역 클래스
public class LocalOuterV1 {
private String outInstanceVar = "바깥 멤버 변수";
public void process(String paramVar) {
String localVar = "지역 변수";
class LocalPrinter {
String value = "지역 클래스 멤버 변수";
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 localOuterV1 = new LocalOuterV1();
localOuterV1.process("파라미터");
}
}
결과
value = 지역 클래스 멤버 변수 localVar = 지역 변수 paramVar = 파라미터 outInstanceVar = 바깥 멤버 변수 |
지역 클래스 사용시 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다.
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();
return printer;
}
public static void main(String[] args) {
LocalOuterV3 localOuter = new LocalOuterV3();
// 힙 영역에 localOuter 인스턴스 생성 ( ~ GC 될때 까지 생존)
Printer printer = localOuter.process(2);
// 힙 영역에 printer 인스턴스 생성 ( ~ GC 될때 까지 생존 )
// & 스택 영역에 process 메서드 생성 (메서드 호출이 끝나면 스택 프레임이 사라짐)
// == 지역변수 생존 주기(스택 영역)이 힙 영역보다 짧다.
printer.print(); //printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
//
}
}
스택 영역 생존 주기 : 메서드가 호출이 끝나면 스택 프레임에서 사라진다
힙 영역 생존 주기 : GC 되기전까지 살아있다.
process() 메서드가 호출되고 스택 프레임이 없어 진 뒤,
printer 인스턴스의 print() 메서드가 process()의 지역변수를 참조하려고 한다.
자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사하여
생성한 인스턴스에 함께 넣어둔다 (이런 과정을 Capture라고 한다)
이런 Capture로 인하여 사라진 지역변수도 같이 참조 할 수 있는 것
결과
value=0 localVar=1 paramVar=2 outInstanceVar=3 |
지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다.
따라서 `final` 로 선언하거나 또는 사실상 `final`(effectively final) 이어야 한다.
사실상 final 인 localVar와 paramVar을 메서드가 끝난 뒤 값을 수정하려고 하면, 캡쳐한 변수의 값과 달라지므로 '동기화 문제'가 생긴다. 이는 멀티 스레드 상황에서 동기화가 매우 어려워진다.
자바 문법에서는 캡쳐한 지역변수를 변경하려고 하면 컴파일 오류가 나도록 설계되어있다.
익명 클래스
지역 클래스 사용 시, (1) 지역 클래스 선언 후 (2) 지역 클래스 선언을 한다.
public class LocalOuterV2 {
private int outInstanceVar = 3;
public void process(int paramVar) {
int localVar = 1;
class LocalPrinter implements Printer { // 지역 클래스 선언
int value = 0;
....
}
}
Printer printer = new LocalPrinter(); // 지역 클래스 생성
printer.print();
}
public static void main(String[] args) {
....
}
}
익명 클래스는 위 클래스 선언과, 생성을 한번에 처리한다.
public class AnonymousOuter {
private int outInstanceVar = 3;
public void process(int paramVar) {
int localVar = 1;
Printer printer = new Printer() { // 지역 클래스 생성+선언
int value = 0;
@Override
public...
}
printer.print();
}
public static void main(String[] args) {
....
}
}
※ printer.getClass() = printer.class=class AnonymousOuter$1
new 구현할타입() {body} |
익명 클래스는 클래스 본문(body)을 정의하면서 동시에 생성한다.
new 다음에 바로 상속받을 타입을 입력하면 되고, body부에 @override를 하면 된다.
(그러므로, 익명 클래스를 사용하려면 상속할 클래스나 인터페이스가 필요하다)
특징으로는
- 이름이 없으므로, 생성자를 가질 수 없다(기본생성자만 있음)
- 익명 클래스의 위치는 바깥클래스네임 + $ + 숫자 로 정의된다.
- 지역 클래스가 일회성으로 사용되거나, 간단한 구현을 제공할 때 사용된다.
<익명 클래스 활용 -> 람다>
(1) 지역클래스
public class Ex1Main {
public static void helloRun(Printer2 pri) {
System.out.println("프로그램 시작");
pri.run();
System.out.println("프로그램 종료");
}
public static class helloDice implements Printer2 {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
}
public static class helloSum implements Printer2 {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
}
public static void main(String[] args) {
Printer2 helloDice = new helloDice();
helloSum helloSum = new helloSum();
helloRun(helloDice);
helloRun(helloSum);
}
}
(2) 익명 클래스로 리팩토링
public class Ex2Main {
public static void helloRun(Printer2 pri) {
System.out.println("프로그램 시작");
pri.run();
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
Printer2 dice = new Printer2() {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
};
Printer2 sum = new Printer2() {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
};
helloRun(dice);
helloRun(sum);
}
}
(3) 참조값 직접 전달하도록 익명 클래스 리팩토링
public class Ex3Main {
public static void helloRun(Printer2 pri) {
System.out.println("프로그램 시작");
pri.run();
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
helloRun(new Printer2() {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
});
helloRun( new Printer2() {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
});
}
}
(4) 람다 리팩토링
public class Ex4Main {
public static void helloRun(Printer2 pri) {
System.out.println("프로그램 시작");
pri.run();
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
helloRun(() -> {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
});
helloRun(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
});
}
}
김영한의 실전 자바 - 중급 1편 강의 | 김영한 - 인프런
김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을
www.inflearn.com
'JAVA' 카테고리의 다른 글
Jakarta Mail(JavaMail) API (0) | 2025.02.11 |
---|---|
Map.Entry와 Map.getKey 차이 (0) | 2024.12.29 |
동시성 문제와 동기화(synchronized) (0) | 2024.11.24 |
멀티스레드 메모리 접근 방식/메모리 가시성/happens-before (0) | 2024.11.21 |
스레드와 Yield (0) | 2024.11.18 |