중첩 클래스(Nested classes)

중첩 클래스는 말 그대로 클래스 내에 정의된 클래스를 말합니다. 어떤 클래스가 한 곳에서만 쓰인다면 아래와 같이 해당 클래스를 중첩시키고 두 클래스를 한꺼번에 관리하는 것이 적절합니다.

class OuterClass { // 외부 클래스
    // ...
    class NestedClass { // 중첩 클래스
        // ...
    }
}

중첩 클래스는 다시 static으로 선언되지 않은 중첩 클래스인 내부 클래스(inner class)와 static으로 선언된 중첩 클래스인 정적 클래스(static class)로 나뉩니다. 여기서는 두 용어를 구분하도록 하겠습니다.

class OuterClass { // 외부 클래스
    // ...
    class InnerClass { // 내부 클래스
        // ...
    }
    static class StaticNestedClass { // 정적 클래스
        // ...
    }
}

내부 클래스(Inner class)

내부 클래스는 자신을 둘러싸는 외부 클래스의 인스턴스 변수와 인스턴스 메서드에 접근할 수 있습니다. 내부 클래스도 외부 클래스에 안에 있는 것이므로 아래와 같이 외부 클래스의 private 멤버에 접근할 수 있습니다.

class OuterClass {
	private int a = 10;
	
    class InnerClass {
    	public void print() {
    		System.out.println("OuterClass.a: " + a);
    	}
    }
}

그리고 내부 클래스를 인스턴스화하려면 먼저 외부 클래스를 인스턴스화해야 합니다.

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

아래와 같이 작성하면 에러가 발생하므로 주의하세요. 먼저 내부 클래스를 둘러싸는 외부 클래스를 인스턴스화한 후에 내부 클래스를 인스턴스화할 수 있습니다.

OuterClass.InnerClass innerObject = new OuterClass.InnerClass();

내부 클래스는 외부 클래스의 멤버이기도 하므로 접근 제한자(private, public, protected, default)를 사용할 수 있습니다.

class OuterClass {
	private class InnerClass {
		// ...
	}
}

섀도잉(Shadowing)

외부 클래스의 필드나 메서드와 동일한 이름으로 내부 클래스의 필드나 메서드를 선언할 경우, 외부 클래스의 필드나 메서드가 그림자처럼 가려지는 것을 말합니다.

class OuterClass {
	private int a = 10;
	
    class InnerClass {
    	private int a = 20;
    	
    	public void print() {
    		System.out.println(a); // 20
    	}
    }
}

만약에 외부 클래스의 멤버에 접근하고 싶다면 아래와 같이 명시적으로 나타내야 합니다.

System.out.println(OuterClass.this.a);

정적 멤버 선언

내부 클래스에서는 정적 멤버를 정의하거나 선언할 수 없습니다. 선언하려하면 아래와 같은 에러가 발생합니다.

class Foo {
    public void doSomething() {
        class Bar {
            // 에러: 필드 x는 상수 식(constant expression)으로 초기화되지 않은 경우
            // 내부 클래스에서 static으로 선언할 수 없습니다.
            static int x = 10;
            // 에러: 메서드 doSomething()은 static으로 선언할 수 없습니다.
            public static void doSomething() {
                // ...
            }
        }
    }
}

다만 정적 필드는 아래와 같이 final을 달면 선언할 수는 있습니다.

class Foo {
    public void doSomething() {
        class Bar {
            static final int x = 10;
            // ...
        }
    }
}

하지만 자바의 버전이 올라가면서 설계자들이 이런 제한을 제거할 필요성을 느끼면서, 자바 16부터는 내부 클래스에서 정적 멤버를 선언할 수 있게 되었습니다.

로컬 클래스(Local classes)

로컬 클래스는 블록 내에 정의된 내부 클래스입니다. 예를 들면, 메서드 내부, 반복문 내부, if문 내부에서 로컬 클래스를 정의할 수 있습니다. 로컬 클래스는 자신을 둘러싸는 블록에서만 접근할 수 있습니다.

class OuterClass {
    public void doSomething() {
    	class LocalClass { // 로컬 클래스
    		public void doSomething() {
    			// ...
    		}
    	}
    	
    	LocalClass obj = new LocalClass();
    	obj.doSomething();
    }
}

로컬 클래스는 패키지나 클래스의 멤버가 아니므로 접근 제어자(private, public, protected)는 사용할 수 없습니다. 그리고 로컬 클래스는 자신을 둘러싸는 클래스의 멤버에 접근할 수 있습니다.

지역 변수로의 접근

메서드의 지역 변수에도 접근할 수 있지만 이 경우에는 해당 지역 변수가 final로 선언되어야 합니다. 하지만 자바 8부터는 사실상 final(effectively final)인 지역 변수에도 접근할 수 있게 되었습니다.

사실상 final(effectively final)

말 그대로 사실상 final입니다. final로 선언되지는 않았지만 초기화된 후에도 값이 변경되지 않는 변수나 매개변수는 사실상 final(effectively final)이라고 할 수 있습니다.

class Foo { 
	public void doSomething() {
		int x = 10; // effectively final
		class Bar {
			public void doSomething() {
				System.out.println(x);
			}
		}
		// ...
	}
}

하지만 아래와 같은 경우는 사실상 final이라고 할 수 없습니다. 초기화된 후에 값이 변경되었기 때문입니다.

class Foo { 
	public void doSomething() {
		int x = 10;
		// ...
		x = 20; // 중간에 값이 한 번 변경됨
		class Bar {
			public void doSomething() {
				// 지역 변수 x는 final이나 effectively final이어야 에러가 발생하지 않는다.
				System.out.println(x);
			}
		}
		// ...
	}
}

정적 클래스(Static classes)

중첩 클래스는 static으로 선언할 수 있습니다. 정적 클래스는 내부 클래스와는 다르게 외부 클래스의 인스턴스 변수나 인스턴스 메서드에 접근할 수 없습니다. 정적 클래스는 외부 클래스를 인스턴스화할 필요가 없기 때문입니다.

class OuterClass {
    static class StaticNestedClass {
    	public void doSomething() {
    		// ...
    	}
    }
}

class JavaTutorial25 {
	public static void main(String[] args) {
		OuterClass.StaticNestedClass obj = new OuterClass.StaticNestedClass();
		obj.doSomething();
	}
}

익명 클래스(Anonymous classes)

익명 클래스는 이름이 없는 로컬 클래스입니다. 이름이 없기 때문에 익명 클래스를 가지고 객체를 여러 번 생성할 수 없으며 생성자를 만들 수도 없습니다. 익명 클래스는 클래스가 한 번만 필요하고, 가독성을 해치지 않을 정도로 클래스의 본문이 짧다면 사용을 고려해볼 수 있습니다.

부모 클래스 상속

부모 클래스를 상속하는 익명 클래스는 아래와 같이 만들 수 있습니다.

new 부모클래스(생성자 매개변수) {
	// 클래스 몸체
}

예를 들어서 아래의 익명 클래스가 있다고 해봅시다.

class Foo {	/* ... */ }

class JavaTutorial25 {
	public static void main(String[] args) {
		new Foo() {
			public void doSomething() {
				// ...
			}
		}.doSomething();
	}
}

이는 다음과 같이 바꿀 수 있습니다.

class Foo {	/* ... */ }

class AnonymousClass extends Foo {
	public void doSomething() {
		// ...
	}
}

class JavaTutorial25 {
	public static void main(String[] args) {
		new AnonymousClass().doSomething();
	}
}

인터페이스 구현

인터페이스를 구현하는 익명 클래스는 아래와 같이 만들 수 있습니다.

new 인터페이스() {
	// 클래스 몸체
}

예를 들어서 아래의 익명 클래스가 있다고 해봅시다.

class JavaTutorial25 {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			public void run() {
				// ...
			}
		}).start();
	}
}

이는 다음과 같이 바꿀 수 있습니다.

class AnonymousClass implements Runnable {
	public void run() {
		// ...
	}
}

class JavaTutorial25 {
	public static void main(String[] args) {
		new Thread(new AnonymousClass()).start();
	}
}

'프로그래밍 관련 > 자바' 카테고리의 다른 글

34편. 애노테이션(Annotation)  (0) 2022.02.18
26편. 제네릭(Generic)  (0) 2022.02.06
29편. 스레드(Thread) (2)  (1) 2022.01.27
정리. JVM의 힙 영역 살펴보기  (0) 2022.01.22
14편. 문자열(String)  (2) 2022.01.15