예전 반복문을 배울 때 for문 안에 for문을 중첩하는 것을 배웠던 적이 있다.
for(){
for((){
~~~~~
}
}
이런 for문을 2중 for문 또는 중첩(Nested) for문이라고 하였었다.
이러한 중첩은 클래스에서도 적용이 될 수 있는데, 이것을 중첩 클래스(Nested Class)라고 한다.
중첩 클래스는 클래스를 정의하는 위치에 따라 다음과 같이 분류한다.
중첩 클래스는 총 4가지이고, 크게 2가지로 분류할 수 있다.
중첩 클래스 : 정적 중첩 클래스 + 내부 클래스 종류 포함
1. 정적 중첩 클래스
2. 내부 클래스 : 내부 클래스, 지역 클래스, 익명 클래스
중첩 클래스를 정의하는 위치는 변수의 선언 위치와 같다.
1. 정적 중첩 클래스 -> 정적 변수와 같은 위치
2. 내부 클래스 -> 인스턴스 변수와 같은 위치
3. 지역 클래스 -> 지역 변수와 같은 위치 -> 메서드 같은 코드 블럭 안에 선언한다.
4. 익명 클래스 -> 지역 클래스의 특별한 버전이다.
정적 중첩 클래스는 정적 변수처럼 앞에 static이 붙어있다.
내부 클래스는 인스턴스 변수와 같이 앞에 static이 붙어있지 않다
여기서 정척 중첩 클래스와 내부 클래스로 분류하는 것을 확인할 수 있다.
중첩이라는 단어와 내부라는 단어는 무슨 차이가 있는 걸까?
중첩(Nested):"어떤 다른 것"이 내부에 위치하거나 포함되는 구조적인 관계
내부 : 나의 내부에 있는 "나"를 구성하는 요소
중첩은 나의 안에 있지만 내 것이 아닌 것을 말한다. 단순히 위치만 안에 있는 것이다.
반면에 여기서 의미하는 내부는 나의 내부에서 나를 구성하는 요소를 말한다.
여기서 의미하는 중첩(Nested)와 내부(Inner)를 분류하는 핵심은 바로 "바깥 클래스 입장에서 볼 때 안에 있는 클래스가 나의 인스턴스에 소속이 되는가 되지 않는가"의 차이로 구분지어진다.
정적 중첩 클래스는 바깥 클래스와 전혀 다른 클래스이기에 바깥 클래스의 인스턴스에 소속되지 않는다.
내부 클래스는 바깥 클래스를 구성하는 요소이다. 따라서 바깥 클래스의 인스턴스에 소속된다.
정리하자면
정적 중첩 클래스 : static이 붙는다, 바깥 클래스의 인스턴스에 소속되지 않는다.
내부 클래스 : static이 붙지 않는다. 바깥 클래스의 인스턴스에 소속된다.
내부 클래스의 종류
내부 클래스(inner class) : 바깥 클래스의 인스턴스 멤버에 접근
지역 클래스(local class) : 내부 클래스의 특징 + 지역 변수에 접근
익명 클래스(anonymous class) : 지역 클래스의 특징 + 클래스의 이름이 없는 특별한 클래스
**실무에서는 내부라는 단어를 명확히 구분하지 않고, 중첩 클래스 또는 내부 클래스라고 이야기한다. 클래스안에 클래스가 있으면 무조건 중첩 클래스이기 때문이다.
+ 클래스 내부에 있다고 해서 정적 중첩 클래스를 내부 클래스라고 말하면 안된다. 하지만 대부분의 개발자들이 둘을 구분해서 말하지 않기 때문에 내부 또는 중첩 클래스라고 하면 상황과 문맥에 따라서 이해하면 된다.
중첩 클래스를 사용해야 할 때 : 내부 클래스를 포함한 모든 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용해야 한다.
중첩 클래스를 사용해야 하는 이유
논리적 그룹화 : 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 그룹화 된다.
캡슐화 : 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있다.
다음은 정적 중첩 클래스에 대한 예제이다.
public class NestedOuter {
private static int outClassValue = 3;
private int outInstanceValue = 2;
static class Nested{
private int nestedInstanceValue = 1;
public void print(){
System.out.println(nestedInstanceValue);
//바깥 클래스의 인스턴스 멤버 접근은 할 수 없다.
// System.out.println(outInstanceValue);
//바깥 클래스의 클래스 멤버에는 접근할 수 있다. private도 접근 가능
System.out.println(NestedOuter.outClassValue);
}
}
}
1.정적 중첩 클래스는 앞에 static이 붙는다.
2. 자신의 멤버에는 당연히 접근할 수 있다.
3. 바깥 클래스의 "인스턴스 멤버"에는 접근할 수 없다.
4. 바깥 클래스의 "클래스 멤버"에는 접근할 수 있다.
private 접근 제어자는 같은 클래스 안에 있을 때만 접근할 수 있다.
중첩 클래스도 바깥 클래스와 같은 클래스 안에 있다. 따라서 중첩 클래스는 바깥 클래스의 private 접근 제어자에 접근할 수 있다.
public static void main(String[] args) {
NestedOuter.Nested nested = new NestedOuter.Nested();
nested.print();
System.out.println("nestedClass" + nested.getClass());
}
정적 중첩 클래스는 바깥 클래스의 정적 필드에는 접근할 수 있다. 하지만 바깥 클래스가 만든 인스턴스 필드에는 바로 접근할 수 없다. 바깥 인스턴스의 참조가 없기 때문이다.
정적 중첩 클래스는 사실 다른 클래스를 그냥 중첩해 둔 것 뿐이다. 아무 관계 없다.
유일한 차이는 private static에 접근할 수 있다는 점이다.
그럼 정적 중첩 클래스가 왜 필요할까?
다음 예제를 통해 이해할 수 있다.
public class NetworkMessage {
private String content;
public NetworkMessage(String content) {
this.content = content;
}
public void print(){
System.out.println(content);
}
}
public class Network {
public void sendMessage(String text){
NetworkMessage networkMessage = new NetworkMessage(text);
networkMessage.print();
}
}
public class NetworkMain {
public static void main(String[] args) {
Network network = new Network();
network.sendMessage("hello java");
}
}
이 코드를 보면 다음 두 클래스가 보인다.
Network, NetworkMessage
Network 관련 라이브러리를 사용하기 위해 이 클래스를 열어본 개발자는 이 클래스 두개를 전부 다 사용해야 하나? 라고 생각할 것이다.
두 클래스의 코드를 모두 확인하고 나서야 network클래스만 사용하는 것을 깨닫게 될 것이다.
그럴 때 정적 중첩 클래스를 활용하여 다음과 같이 만들 수 있다.
public class Network {
public void sendMessage(String text){
NetworkMessage networkMessage = new NetworkMessage(text);
networkMessage.print();
}
private static class NetworkMessage {
private String content;
public NetworkMessage(String content) {
this.content = content;
}
public void print(){
System.out.println(content);
}
}
}
NetworkMessage 클래스를 Network 클래스 안에 중첩해서 만들었다.
NetworkMessage의 접근 제어자를 private 설정했다. 따라서 외부에서 NetworkMessage에 접근할 수 없다.
NetworkMessage가 중첩 클래스에 private 접근 제어자로 설정되어 있는 것을 보고, Network 내부에서만 단독으로 사용하는 클래스라고 바로 인지할 수 있다.
내부 클래스
앞서 본 정적 중첩 클래스는 바깥 클래스와 서로 관계가 없다. 하지만 내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소가 된다. 쉽게 이야기 해서 "내부 클래스는 바깥 클래스의 인스턴스에 소속"된다.
public class InnerOuter {
private static int outClassvalue = 3;
private int outInstanceValue = 2;
class Inner{
private int innerInstanceValue = 1;
public void print(){
//자기 자신에 접근
System.out.println("innerInstanceValue = " + innerInstanceValue);
//외부 클래스의 인스턴스 멤버에 접근 가능, private도 접근 가능
System.out.println("outInstanceValue = " + outInstanceValue);
//외부 클래스의 클래스 멤버에 접근 가능, private도 접근 가능
System.out.println("outClassvalue = " + outClassvalue);
}
}
}
public static void main(String[] args) {
InnerOuter outer = new InnerOuter();
InnerOuter.Inner inner = outer.new Inner();
inner.print();
System.out.println("inner.getClass() = " + inner.getClass());
}
내부 클래스 특징
1. static이 붙지 않는다.
2. 바깥 클래스의 인스턴스에 소속된다
3. 자신의 멤버에는 당연히 접근할 수 있다.
4. 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
5. 바깥 클래스의 클래스 멤버에 접근할 수 있다.
내부 클래스는 바깥 클래스의 인스턴스에 소속된다. 따라서 바깥 클래스의 인스턴스 정보를 알아야 생성할 수 있다.
내부 클래스는 "new 바깥클래스의 인스턴스 참조. 내부클래스()"로 인스턴스를 생성할 수 있다.
내부클래스는 바깥 클래스의 인스턴스에 소속되어야 하기 때문에 생성 시 바깥 클래스의 인스턴스 참조가 필요한 것이다.
outer.new Inner()로 생성한 내부 클래스는 개념상 바깥 클래스의 인스턴스 내부에 생성된다.
다음은 내부클래스의 활용이다.
public class Car {
private String model;
private int chargeLevel;
private Engine engine;
public Car(String model, int chargeLevel) {
this.model = model;
this.chargeLevel = chargeLevel;
this.engine = new Engine();
}
public void start(){
engine.start();
System.out.println(model + "시작 완료");
}
private class Engine {
public void start(){
System.out.println("충전 레벨 확인: " + chargeLevel);
System.out.println(model+"의 엔진을 구동화");
}
}
}
public static void main(String[] args) {
Car myCar = new Car("Model Y", 100);
myCar.start();
}
바깥 클래스에서 내부 클래스의 인스턴스를 생성할 때는 바깥 클래스 이름을 생략할 수 있다.
ex) new Engine()
바깥 클래스에서 내부 클래스의 인스턴스를 생성할 때 내부 클래스의 인스턴스는 자신을 생성한 바깥 클래스의 인스턴스를 자동으로 참조한다. 여기서 new Engine()으로 생성된 Engine 인스턴스는 자신을 생성한 바깥의 Car 인스턴스를 자동으로 참조한다.
중첩 클래스
중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴미랗게 연결되어 있는 특별한 경우에만 사용해야 한다. 외부 여러곳에서 특정 클래스를 사용한다면 중첩 클래스로 사용하면 안된다.
중첩 클래스를 사용하는 이유
논리적 그룹화 : 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화가 된다.
캡슐화 : 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있다.
같은 이름의 바깥 변수 접근
바깥 클래스의 인스턴스 변수 이름과 내부 클래스의 인스턴스 변수 이름이 같으면 어떻게 될까?
public class ShadowingMain {
public int value = 1;
class Inner{
public int value = 2;
void go(){
int value = 3;
System.out.println("value = " + value);
System.out.println("this.value = " + this.value);
System.out.println("ShadowingMain.this.value = " + ShadowingMain.this.value);
}
}
public static void main(String[] args) {
ShadowingMain main = new ShadowingMain();
Inner inner = main.new Inner();
inner.go();
}
}
변수의 이름이 같기 때문에 어떤 변수를 먼저 사용할지 우선순위가 필요하다.
프로그래밍에서 우선순위는 대부분 더 가깝거나, 더 구체적인 것이 우선권을 가진다. 쉽게 이야기해서 사람이 직관적으로 이해하기 쉬운 방향으로 우선순위를 설계한다.
메서드 go()의 경우 지역변수인 value가 가장 가깝다. 따라서 우선순위가 가장 높다.
이렇게 다른 변수들을 가려서 보이지 않게 하는 것을 섀도잉(shadowing)이라 한다.
다른 변수를 가리더라도 인스턴스의 참조를 사용하면 외부의 변수에 접근할 수 있다.
this.value는 내부 클래스의 인스턴스에 접근하고, 바깥클래스이름.this는 바깥클래스의 인스턴스에 접근할 수 있다.
프로그래밍에서 가장 중요한 것은 명확성이다. 이렇게 이름이 같은 경우 처음부터 이름을 서로 다르게 지어서 명확하게 구분하는 것이 더 나은 방법이다.
'공부 > Java' 카테고리의 다른 글
자바 익명 클래스 (1) | 2024.09.14 |
---|---|
자바 지역 클래스 (0) | 2024.09.14 |
자바 날짜와 시간 (1) | 2024.09.12 |
자바 열거형-ENUM (3) | 2024.09.11 |
자바 래퍼 클래스(wrapper class) (0) | 2024.09.10 |