본 게시글은 혼자 공부하는 자바 (저자 : 신용권)의 책과 유튜브 영상을 참고하였고, 개인적으로 정리하는 글임을 알립니다.


※java version "17.0.5" 2022-10-18 LTS 기준 문법임

중첩 클래스

객체 지향 프로그래밍에서 클래스들은 서로 긴밀한 관계를 맺고 상호작용을 한다. 어떤 클래스는 여러 클래스와 관계를 맺지만 어떤 클래스는 특정 클래스와 관계를 맺는다. 클래스가 여러 클래스와 관계를 맺는 경우에는 독립적으로 선언하는 것이 좋으나, 특정 클래스와 관계를 맺을 경우에는 클래스 내부에 선언하는 것이 좋다.

 

중첩 클래스란 클래스 내부에 선언한 클래스를 말한다. 중첩 클래스를 사용하면 두 클래스의 멤버들을 서로 쉽게 접근할 수 있고, 외부에는 불필요한 관계 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점이 있다.

class Outter{
	class Inner{ //중첩 클래스
    }
}

 

 

중첩 클래스는 선언되는 위치에 따라서 두 가지로 분류된다.

  • 멤버 클래스 : 클래스의 멤버로서 선언되는 중첩 클래스
  • 로컬 클래스 : 생성자 또는 메소드 내부에서 선언되는 중첩 클래스

멤버 클래스는 인스턴스 멤버 클래스와 정적 멤버 클래스로 분류된다.

 

인스턴스 멤버 클래스

인스턴스 멤버 클래스는 바깥 클래스의 객체가 생성되어야 인스턴스 멤버 클래스 객체의 멤버를 사용할 수 있다.

->바깥 클래스의 객체가 생성되어야 안쪽 클래스 객체의 멤버를 사용할 수 있다.

class A {
	A() { System.out.println("A 객체가 생성됨"); }
	
	public class B { //인스턴스 멤버 클래스
		B() { System.out.println("B 객체가 생성됨"); }
		int field1; //인스턴스 필드
		static int field2;
		void method1() {System.out.println("B method1");}
		static void method2() {System.out.println("B method2");}
	}
}
public class Test {
	public static void main(String[] args) {
		A a = new A();
		
		//인스턴스 멤버 클래스 객체 생성
		System.out.println("인스턴스 멤버 클래스 -----------");
		A.B b = a.new B();
		b.field1 = 3;
		System.out.println("b.field1 : " + b.field1);
		b.field2 = 4;
		System.out.println("b.field2 : " + b.field2);
		b.method1();
		b.method2();
	}
}
/*
A 객체가 생성됨
인스턴스 멤버 클래스 -----------
B 객체가 생성됨
b.field1 : 3
b.field2 : 4
B method1
B method2
*/

바깥 클래스의 객체를 생성하지 않으면 오류가 발생하는 것을 볼 수 있다.

 

public class A {
	//인스턴스 멤버 클래스
	class B {
		//인스턴스 필드
		int field1 = 1;

		//정적 필드(Java 17부터 허용)
		static int field2 = 2;

		//생성자
		B() {
			System.out.println("B-생성자 실행");
		}

		//인스턴스 메소드
		void method1() {
			System.out.println("B-method1 실행");
		}

		//정적 메소드(Java 17부터 허용)
		static void method2() {
			System.out.println("B-method2 실행");
		}
	}

	//인스턴스 메소드
	void useB() {
		//B 객체 생성 및 인스턴스 필드 및 메소드 사용
		B b = new B();
		System.out.println(b.field1);
		b.method1();

		//B 클래스의 정적 필드 및 메소드 사용
		System.out.println(B.field2);
		B.method2();
	}
}
public class AExample {
	public static void main(String[] args) {
		//A 객체 생성
		A a = new A();

		//A 인스턴스 메소드 호출
		a.useB();
	}
}
/*
B-생성자 실행
1
B-method1 실행
2
B-method2 실행
*/

 

정적 멤버 클래스

정적 멤버 클래스는 바깥 클래스의 객체가 생성되지 않아도 정적 멤버 클래스 객체의 멤버를 사용할 수 있다.

->바깥 클래스의 객체가 생성되지 않아도 안쪽 클래스 객체의 멤버를 사용할 수 있다.

class A {
	A() { System.out.println("A 객체가 생성됨"); }
	
	static class C { //정적 멤버 클래스
		C() { System.out.println("C 객체가 생성됨"); }
		int field1;
		static int field2;
		void method1() {System.out.println("C method1");}
		static void method2() {System.out.println("C method2");}
	}
}
public class Test {
	public static void main(String[] args) {
		A a = new A();

		
		//정적 멤버 클래스 객체 생성
		System.out.println("정적 멤버 클래스 -----------");
		A.C c = new A.C();
		c.field1 = 3;
		System.out.println("c.field1 : " + c.field1);
		c.field2 = 4;
		System.out.println("c.field2 : " + c.field2);
		c.method1();
		c.method2();
		
		A.C.field2 = 5;
		System.out.println("A.C.field2 : " + A.C.field2);
		A.C.method2();
	}
}
/*
A 객체가 생성됨
정적 멤버 클래스 -----------
C 객체가 생성됨
c.field1 : 3
c.field2 : 4
C method1
C method2
A.C.field2 : 5
C method2
*/

바깥 클래스의 객체를 생성하지 않아도 정적 멤버 클래스 객체의 멤버를 사용할 수 있다.

 

public class A {
	//인스턴스 멤버 클래스
	static class B {}

	//인스턴스 필드 값으로 B 객체 대입
	B field1 = new B();

	//정적 필드 값으로 B 객체 대입
	static B field2 = new B();

	//생성자
	A() {
		B b = new B();
	}

	//인스턴스 메소드
	void method1() {
		B b = new B();
	}

	//정적 메소드
	static void method2() {
		B b = new B();
	}
}
public class AExample {
	public static void main(String[] args) {
		//B 객체 생성
		A.B b = new A.B();
	}
}

 

로컬 클래스

메소드 내에 선언된 클래스를 로컬 클래스라고 하며, 로컬 클래스는 접근 제한자(public, private) 및 static을 붙일 수 없다.

로컬 클래스는 메소드 내부에만 사용되므로 접근을 제한할 필요가 없기 때문이다.

로컬 클래스는 바깥 클래스의 객체가 생성되어야 로컬 클래스 객체의 멤버를 사용할 수 있다. 또한 메소드가 종료되면 객체가 소멸되므로 객체 멤버를 사용할 수 없다. 로컬 클래스는 메소드가 실행될 때 메소드 내에서 객체를 생성하고 사용해야 한다.

->바깥 클래스의 객체가 생성되어야 안쪽 클래스 객체의 멤버를 사용할 수 있다. 메소드가 종료되면 객체가 소멸된다.

class A {
	A() { System.out.println("A 객체가 생성됨"); }
    
	void method() {
		class D { //로컬 클래스
			D() { System.out.println("D 객체가 생성됨"); }
			int field1;
			static int field2;
			void method1() {System.out.println("D method1");}
			static void method2() {System.out.println("D method2");}
		}
		D d = new D();
		d.field1 = 3;
		System.out.println("d.field1 : " + d.field1);
		d.field2 = 4;
		System.out.println("d.field2 : " + d.field2);
		d.method1();
		d.method2();
		//A.D.field2 = 5; 불가능
	}
}
package TestPackage;
public class Test {
	public static void main(String[] args) {
		A a = new A();
		//로컬 클래스 객체 생성을 위한 메소드 호출
		System.out.println("로컬 클래스 -----------");
		a.method();
	}
}
/*
A 객체가 생성됨
로컬 클래스 -----------
D 객체가 생성됨
d.field1 : 3
d.field2 : 4
D method1
D method2
*/

바깥 클래스의 객체를 생성하지 않으면 오류가 발생하는 것을 볼 수 있다.

 

 

 

 

중첩 클래스의 접근 제한

멤버 클래스 내부에서 바깥 클래스의 필드와 메소드에 접근할 때는 제한이 따른다. 또한 메소드의 매개 변수나 로컬 변수를 로컬 클래스에서 사용할 때도 제한이 따른다.

바깥 필드와 메소드에서 사용 제한

바깥 클래스에서 인스턴스 멤버 클래스를 사용할 때 제한이 있다.

class A {

	class B {} //인스턴스 멤버 클래스
	
	static class C {} //정적 멤버 클래스
    
	//인스턴스 필드
	B field1 = new B();               
	C field2 = new C();
	
	void method1() { //인스턴스 메소드
		B var1 = new B();
		C var2 = new C();
	}
	
	//정적 필드 초기화
	//static B field3 = new B(); 컴파일 에러
	static C field4 = new C();
	
	static void method2() { //정적 메소드
		//B var1 = new B(); 컴파일 에러
		C var2 = new C();
	}
}

인스턴스 멤버 클래스(B)는 인스턴스 필드의 초기값이나 인스턴스 메소드에서 객체를 생성할 수 있으나, 정적필드의 초기값이나 정적 메소드에서는 객체를 생성할 수 없다.

반면 정적 멤버 클래스(C)는 모든 필드의 초기값이나 모든 메소드에서 객체를 생성할 수 있다.

static B field3 = new B();가 컴파일 오류가 발생하는 이유는 B객체는 A객체가 생성되어야 생성되는데, static으로 선언하려고 하니까 나는 오류이다.

B var1 = new B();가 컴파일 오류가 발생하는 이유는 B객체는 A객체가 생성되어야 생성되는데 static으로 선언된 메소드가 B와 관련된 행동을 하려고 하기 때문이다.

static(정적)
static(정적) 멤버는 클래스의 객체가 생성되지 않아도 접근할 수 있는 멤버를 뜻한다.

 

멤버 클래스에서 사용 제한

멤버 클래스가 인스턴스 또는 정적으로 선언됨에 따라 멤버 클래스 내부에서 바깥 클래스의 필드와 메소드에 접근할 때에도 제한이 따른다.

class A {
	int field1;
	void method1() { }
	
	static int field2;
	static void method2() { }
	
	class B {
		void method() {
			field1 = 10;
			method1();

			field2 = 10;
			method2();
		}
	}
	
	static class C {
		void method() {
			//field1 = 10;
			//method1();

			field2 = 10;
			method2();
		}
	}	
}

인스턴스 멤버 클래스(B) 안에서는 바깥 클래스의 모든 필드와 모든 메소드에 접근할 수 있지만, 정적 멤버 클래스(C) 안에서는 바깥 클래스의 정적 필드와 정적 메소드에만 접근할 수 있고 인스턴스 필드와 인스턴스 메소드에는 접근할 수 없다.

C에서 field1 = 10; 을 사용할 수 없는 이유는 C는 정적 클래스인데  field1 = 10;은 A객체가 생성되어야 하기 때문이다

C에서 method1(); 을 사용할 수 없는 이유는 C는 정적 클래스인데 method1();은 A객체가 생성되어야 하기 때문이다.

 

로컬 클래스에서 사용 제한

메소드의 매개 변수나 로컬 변수를 로컬 클래스에서 사용할 때 제한이 있다.(로컬 변수를 로컬 클래스에서 사용하지 않으면 해당 X)

로컬 클래스의 객체는 메소드 실행이 종료되면 없어지는 것이 일반적이지만, 메소드가 종료되어도 계속 실행 상태로 존재할 수 있다. 예를 들어 로컬 스레드 객체를 사용할 때이다. 메소드를 실행하는 스레드와 다르므로 메소드가 종료된 후에도 로컬 스레드 객체는 실행 상태로 존재할 수 있다.

자바는 이 문제를 해결하기 위해 컴파일 시 로컬 클래스에서 사용하는 매개 변수나 로컬 변수의 값을 로컬 클래스 내부에 복사해두고 사용한다. 그리고 매개 변수나 로컬 변수가 수정되어 값이 변경되면 로컬 클래스에 복사해둔 값과 달라지므로 로컬 변수를 final로 선언할 것을 요구한다.

하지만 자바 8부터는 final 키워드 없이 선언된 매개 변수와 로컬 변수를 사용해도 컴파일 에러가 발생하지 않는다. final선언을 하지 않아도 값이 수정될 수 없도록 final의 특성을 부여하기 때문이다.

로컬 변수를 로컬 클래스에서 사용할 경우, 명시적으로 final 키워드를 붙이지 않아도 되지만 로컬 변수에 final 키워드를 추가해서 final 변수임을 명확히 할 수도 있다.

public class A {
	//메소드
	public void method1(int arg) {
		//로컬 변수 
		int var = 1;
		
		//로컬 클래스
		class B {
			//메소드
			void method2() {
				//로컬 변수를 로컬 클래스에서 사용하는 경우
				System.out.println("arg: " + arg); //(o)
				System.out.println("var: " + var); //(o)
				
				//로컬 변수 수정
                //로컬 변수를 로컬 클래스에서 사용하기 때문에 final 특성을 가짐
				//arg = 2; //(x)
				//var = 2; //(x)
			}
		}
		
		//로컬 객체 생성
		B b = new B();
		//로컬 객체 메소드 호출
		b.method2();
		
		//로컬 변수 수정
		//arg = 3; //(x)
		//var = 3; //(x)
	}
}

 

 

중첩 클래스에서 바깥 클래스 참조 얻기

클래스에서 this는 객체 자신의 참조이다. 내부 클래스에서 this 키워드를 사용하면 바깥 클래스의 객체 참조가 아니라, 내부 클래스 객체 참조가 된다. 따라서 내부 클래스에서 바깥 클래스의 객체 참조를 얻고 싶다면 '바깥클래스이름.this'를 사용하면 된다.

아래의 예제와 같이 내부 클래스(Nested)에서 바깥 클래스(Outter)의 멤버를 사용하려면 Outter.this.field 와 Outter.this.method() 처럼 사용하면 된다.

public class Outter {
	String field = "Outter-field";
	void method() {
		System.out.println("Outter-method");
	}
	
	class Nested {
		String field = "Nested-field";
		void method() {
			System.out.println("Nested-method");
		}
		void print() {
			System.out.println(this.field); //내부 객체 참조
			this.method(); //내부 객체 참조
			System.out.println(Outter.this.field); // 바깥 객체 참조
			Outter.this.method(); //바깥 객체 참조
		}
	}
}
public class Test {
	public static void main(String[] args) {
		Outter outter = new Outter();
		Outter.Nested nested = outter.new Nested();
		nested.print();
	}
}
/*
Nested-field
Nested-method
Outter-field
Outter-method
*/