안녕하세요, 흐린날이지만 기분은 차분해져서 좋은 Dorothy입니다. 아유.. 오늘은 왠지 정신이 하나도 없네요. 바로 이어서 공부해보도록 하겠습니다. 오늘은 중첩클래스에 대해서 배워볼게요. 시작합니당!
1. Nested Classes
자바 프로그래밍 언어에서는 한 클래스 내에 다른 클래스를 정의할 수 있습니다. 이러한 클래스를 Nested Class(중첩 클래스)라고 하며, 여기서 그 예를 보여줍니다.
class OuterClass {
...
class NestedClass {
...
}
}
용어: 중첩 클래스는 두 가지 범주로 분류됩니다:
non-static 과 static.
Non-static 중첩 클래스는 inner 클래스라고 합니다.
static으로 선언된 중첩 클래스들은 static nested 클래스라고 합니다.
외부 클래스 : 내부 클래스를 정의한, 감싸고 있는 클래스.
class OuterClass {
...
class InnerClass {
...
}
static class StaticNestedClass {
...
}
}
중첩 클래스는 중첩 클래스를 포함하고 있는 클래스의 멤버입니다.
- 비정적[Non-static] 중첩 클래스(내부 클래스)는 이 클래스를 포함하고 있는 외부 클래스의 다른 멤버에 접근할 수 있으며, 그 멤버들이 private으로 선언되어 있더라도 마찬가지입니다.
- 반면에, 정적[Static] 중첩 클래스는, 이 클래스를 포함하고 있는 외부 클래스의 다른 멤버에 접근할 수 없습니다.
- OuterClass의 멤버로서, 중첩 클래스는 private, public, protected, 또는 package-private[패키지 전용]으로 선언될 수 있습니다. (외부 클래스는 public 또는 package-private으로만 선언될 수 있다는 점을 상기해주세요.)
1) Why Use Nested Classes?
중첩 클래스를 사용하는데는 다음과 같은 설득력 있는 이유들이 있습니다:
- 한 곳에서만 사용되는 클래스들을 논리적으로 그룹화하는 방법: 만약 어떤 클래스가 다른 클래스 하나에만 유용하다면, 그 클래스를 해당 클래스 내부에 내장시키고 두 클래스를 함께 유지하는 것이 논리적입니다. 이러한 helper 클래스들을 중첩시키면 패키지가 더욱 간결해집니다.
- 캡슐화 증가: 두 개의 최상위 클래스 A와 B가 있다고 가정해 보세요. 이때 클래스 B는 클래스 A의 멤버에 접근해야 하지만, 이 멤버들은 원래 private으로 선언되어야 한다고 가정합니다. 클래스 B를 클래스 A 내부에 숨김으로써, A의 멤버들을 private으로 선언할 수 있고 B는 이 멤버들에 접근할 수 있습니다. 또한, B 자체도 외부 세계로부터 숨겨질 수 있습니다.
// 최상위 클래스 A
public class A {
private int secretData;
public A(int data) {
this.secretData = data;
}
// A의 private 멤버에 접근할 수 있는 메서드 제공
public int getSecretData() {
return secretData;
}
}
// 최상위 클래스 B
public class B {
private A a;
public B(A a) {
this.a = a;
}
public void showSecretData() {
// B는 A의 private 멤버에 직접 접근할 수 없고
// A의 Getter 메서드를 통해서만 가능
System.out.println(a.getSecretData());
}
}
위 코드에서, B 클래스는 A 클래스의 secretData 필드에 접근할 수 없습니다. secretData 필드는 private으로 선언되어 외부 클래스에서는 접근할 수 없기 때문에 A 클래스의 Getter 메서드를 호출해서 secretData에 액세스가 가능합니다.
이 문제를 해결하기 위해 클래스 B를 클래스 A 내부에 숨길 수 있습니다.
public class A {
private int secretData;
public A(int data) {
this.secretData = data;
}
// A의 private 멤버에 접근할 수 있는 메서드 제공
public int getSecretData() {
return secretData;
}
// B 클래스를 A 클래스 내부로 숨김
public class B {
public void showSecretData() {
// B 클래스는 A 클래스의 private 멤버에 직접 접근할 수 있음
System.out.println(secretData);
}
}
public B createBInstance() {
return new B();
}
}
public class Main {
public static void main(String[] args) {
A a = new A(42);
A.B b = a.createBInstance();
b.showSecretData(); // 출력: 42
}
}
아래 코드의 import 구문을 확인하세요
package com.intheeast.java;
import com.intheeast.java.A.B;
public class Main {
public static void main(String ... args) {
A a = new A(3);
B b = a.new B();
b.showSecretData();
}
}
- 더 읽기 쉽고 유지보수하기 쉬운 코드로 이어질 수 있음: 최상위 클래스 내부에 작은 클래스들을 중첩시키면, 코드를 사용하는 곳에 더 가깝게 배치할 수 있습니다.
Inner Classes
인스턴스 메소드와 변수와 마찬가지로, 내부 클래스는 이 클래스를 포함하는 외부 클래스의 인스턴스와 연관되어 있으며, 내부 클래스는 외부 클래스 객체의 메소드와 필드에 직접 접근할 수 있습니다. 또한, 내부 클래스는 외부 클래스 인스턴스와 연관되어 있기 때문에, 스스로 어떤 정적 멤버도 정의할 수 없습니다.
※ 내부 클래스는 외부 클래스의 인스턴스에 종속되어 있습니다. 즉, 내부 클래스의 인스턴스는 반드시 외부 클래스의 인스턴스와 연관되어 생성됩니다. 이 때문에 내부 클래스는 외부 클래스의 메서드와 필드에 직접 접근할 수 있습니다.
내부 클래스의 인스턴스 객체들은 외부 클래스의 인스턴스 내부에 존재합니다. 다음 클래스들을 고려해보세요:
package com.intheeast.java;
public class OuterClass {
private int outerField;
private InnerClass innerClassInstance; // InnerClass 객체 참조 변수
public OuterClass(int outerField) {
this.outerField = outerField;
this.innerClassInstance = new InnerClass(); // 기본 생성자로 초기화
}
// 외부 클래스의 인스턴스 메서드
public void outerMethod() {
System.out.println("Outer class method.");
}
// 비정적 내부 클래스
public class InnerClass {
private int innerField;
// 디폴트 생성자
public InnerClass() {
this.innerField = 0;
}
// 파라미터를 받는 생성자
public InnerClass(int innerField) {
this.innerField = innerField;
}
// 내부 클래스의 메서드
public void innerMethod() {
// 내부 클래스는 외부 클래스의 필드와 메서드에 접근할 수 있음
System.out.println("Outer field: " + outerField);
outerMethod();
// 내부 클래스의 자체 필드에 접근
System.out.println("Inner field: " + innerField);
// this 키워드 사용
System.out.println("Inner class this: " + this);
System.out.println("Outer class this: " + OuterClass.this);
}
// 내부 클래스에서 외부 클래스의 메서드를 호출하는 메서드
public void callOuterMethod() {
OuterClass.this.outerMethod();
}
// 외부 클래스의 필드를 수정하는 메서드
public void modifyOuterField(int newValue) {
OuterClass.this.outerField = newValue;
System.out.println("Outer field modified to: " + outerField);
}
}
// OuterClass에서 InnerClass의 메서드 호출
public void callInnerMethod() {
innerClassInstance.innerMethod();
}
// OuterClass에서 InnerClass 인스턴스를 변경하는 메서드
public void setInnerClassInstance(int innerField) {
this.innerClassInstance = new InnerClass(innerField);
}
// OuterClass에서 InnerClass 인스턴스를 생성하는 메서드
public InnerClass createInnerClassInstance(int innerField) {
return new InnerClass(innerField);
}
// OuterClass의 main 메서드
public static void main(String[] args) {
OuterClass outer = new OuterClass(10);
// 기본 생성자를 사용하여 비정적 내부 클래스의 인스턴스 생성
InnerClass inner1 = outer.new InnerClass();
inner1.innerMethod();
// 매개변수를 받는 생성자를 사용하여 비정적 내부 클래스의 인스턴스 생성
InnerClass inner2 = outer.new InnerClass(20);
inner2.innerMethod();
// 외부 클래스의 메서드를 내부 클래스에서 호출
inner2.callOuterMethod();
// 내부 클래스에서 외부 클래스의 필드를 수정
inner2.modifyOuterField(100);
// 외부 클래스에서 내부 클래스의 메서드 호출
outer.callInnerMethod();
// 외부 클래스에서 내부 클래스의 인스턴스를 변경
outer.setInnerClassInstance(50);
outer.callInnerMethod();
// 외부 클래스에서 내부 클래스의 인스턴스를 생성하는 메서드 사용
InnerClass inner3 = outer.createInnerClassInstance(30);
inner3.innerMethod();
}
}
package com.intheeast.java;
public class TestClass {
public static void main(String[] args) {
// OuterClass 인스턴스 생성
OuterClass outer = new OuterClass(10);
// 기본 생성자를 사용하여 비정적 내부 클래스의 인스턴스 생성
OuterClass.InnerClass inner1 = outer.new InnerClass();
inner1.innerMethod();
// 매개변수를 받는 생성자를 사용하여 비정적 내부 클래스의 인스턴스 생성
OuterClass.InnerClass inner2 = outer.new InnerClass(20);
inner2.innerMethod();
// 외부 클래스의 메서드를 내부 클래스에서 호출
inner2.callOuterMethod();
// 내부 클래스에서 외부 클래스의 필드를 수정
inner2.modifyOuterField(100);
// 외부 클래스에서 내부 클래스의 메서드 호출
outer.callInnerMethod();
// 외부 클래스에서 내부 클래스의 인스턴스를 변경
outer.setInnerClassInstance(50);
outer.callInnerMethod();
// 외부 클래스에서 내부 클래스의 인스턴스를 생성하는 메서드 사용
OuterClass.InnerClass inner3 = outer.createInnerClassInstance(30);
inner3.innerMethod();
}
}
InnerClass의 인스턴스는 OuterClass의 인스턴스 내부에서만 존재할 수 있으며, InnerClass의 인스턴스를 포함하는 OuterClass 인스턴스의 메소드와 필드에 직접 접근할 수 있습니다.
내부 클래스를 인스턴스화하려면, 먼저 외부 클래스를 인스턴스화해야 합니다. 그런 다음, 이 구문을 사용하여 외부 객체 내부에 내부 객체를 생성합니다:
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
두 가지의 특별한 inner 클래스들이 있습니다: local class 와 anonymous class
Static Nested Classes
클래스 메소드와 필드처럼, 정적 중첩 클래스는 정적 중첩 클래스의 외부 클래스와 연관되어 있습니다. 그리고 정적 클래스 메소드처럼, 정적 중첩 클래스는 직접적으로 정적 중첩 클래스를 포함하는 외부 클래스에서 정의된 인스턴스 필드나 메소드를 참조할 수 없습니다: 오직 객체 참조를 통해서만 사용할 수 있습니다. Inner Class and Nested Static Class Example 에서 이를 보여줍니다.
참고: 정적 중첩 클래스는 다른 최상위 클래스들처럼 정적 중첩 클래스를 포함하는 외부 클래스의 인스턴스 멤버들과 상호작용합니다. 실제로, 정적 중첩 클래스는 행동적으로는 최상위 클래스이지만, 패키징의 편의를 위해 다른 최상위 클래스에 중첩된 클래스입니다. Inner Class and Nested Static Class Example 도 이를 보여줍니다.
정적 중첩 클래스를 인스턴스화하는 방법은 최상위 클래스를 인스턴스화하는 방법과 동일합니다:
StaticNestedClass staticNestedObject = new StaticNestedClass();
Inner Class and Nested Static Class Example
다음 예제인 OuterClass와 TopLevelClass는 외부 클래스(OuterClass)의 멤버 중 어떤 것들이 내부 클래스(InnerClass), 중첩 정적 클래스(StaticNestedClass), 그리고 최상위 클래스(TopLevelClass)에서 접근할 수 있는지를 보여줍니다:
package com.intheeast.java;
public class OuterClass {
String outerField = "Outer field";
static String staticOuterField = "Static outer field";
// StaticNestedClass 객체 참조 변수
StaticNestedClass staticNestedInstance;
public OuterClass() {
// StaticNestedClass 인스턴스를 생성하여 필드에 저장
staticNestedInstance = new StaticNestedClass();
}
class InnerClass {
void accessMembers() {
System.out.println(outerField);
System.out.println(staticOuterField);
}
}
static class StaticNestedClass {
// 외부 클래스의 필드는 오직 외부 클래스 객체 참조를 통해서만 사용할 수 있습니다
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field outerField
// System.out.println(outerField);
System.out.println(outer.outerField);
System.out.println(staticOuterField);
}
}
// StaticNestedClass의 메서드를 호출하는 메서드
public void callStaticNestedMethod() {
staticNestedInstance.accessMembers(this);
}
public static void main(String[] args) {
System.out.println("Inner class:");
System.out.println("------------");
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
innerObject.accessMembers();
System.out.println("\nStatic nested class:");
System.out.println("--------------------");
StaticNestedClass staticNestedObject = new StaticNestedClass();
staticNestedObject.accessMembers(outerObject);
System.out.println("\nStatic nested class accessed from OuterClass:");
System.out.println("---------------------------------------------");
outerObject.callStaticNestedMethod();
System.out.println("\nTop-level class:");
System.out.println("----------------");
TopLevelClass topLevelObject = new TopLevelClass();
topLevelObject.accessMembers(outerObject);
}
}
TopLevelClass.java
public class TopLevelClass {
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field OuterClass.outerField
// System.out.println(OuterClass.outerField);
System.out.println(outer.outerField);
System.out.println(OuterClass.staticOuterField);
}
}
This example prints the following output:
Inner class:
------------
Outer field
Static outer field
Static nested class:
--------------------
Outer field
Static outer field
Top-level class:
--------------------
Outer field
Static outer field
정적 중첩 클래스는 다른 최상위 클래스처럼 외부 클래스의 인스턴스 멤버와 상호 작용합니다. 정적 중첩 클래스인 StaticNestedClass는 외부 클래스인 OuterClass의 인스턴스 필드인 outerField에 직접 접근할 수 없습니다. 자바 컴파일러는 강조된 문장에서 오류를 생성합니다:
static class StaticNestedClass {
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field outerField
System.out.println(outerField); // 에러가 발생하는 부분
}
}
이 오류를 수정하려면 객체 참조를 통해 outerField에 접근하세요:
System.out.println(outer.outerField);
마찬가지로, 최상위 클래스인 TopLevelClass도 outerField에 직접 접근할 수 없습니다.
Shadowing
특정 범위(예: 내부 클래스 또는 메소드 정의) 내에서 타입(멤버 변수 또는 파라미터 이름과 같은)의 선언이 바깥쪽 범위 내의 다른 선언과 동일한 이름을 가지면, 해당 선언은 바깥쪽 범위의 선언을 가립니다(shadow). 그림자진(shadowed) 선언을 단독으로 그 이름으로 참조할 수는 없습니다. 다음 예제인 ShadowTest는 이를 보여줍니다:
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
}
}
public static void main(String... args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
다음은 이 예제의 출력입니다.
x = 23
this.x = 1
ShadowTest.this.x = 0
이 예제에서는 이름이 x인 세 개의 변수를 정의합니다: 클래스 ShadowTest의 멤버 변수, 내부 클래스 FirstLevel의 멤버 변수, 그리고 메소드 methodInFirstLevel의 파라미터. 메소드 methodInFirstLevel의 파라미터로 정의된 변수 x는 내부 클래스 FirstLevel의 변수를 가립니다. 따라서 methodInFirstLevel 메소드에서 변수 x를 사용하면, 그것은 메소드의 파라미터를 참조합니다. 내부 클래스 FirstLevel의 멤버 변수를 참조하려면, 포함 범위를 나타내는 키워드 this를 사용하세요:
System.out.println("this.x = " + this.x);
더 큰 범위를 포함하는 멤버 변수는 소속된 클래스 이름을 사용하여 참조합니다. 예를 들어, 다음 문장은 methodInFirstLevel 메소드에서 클래스 ShadowTest의 멤버 변수에 접근합니다:
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
Serialization
내부 클래스(local 클래스 및 anonymous 클래스 포함)의 직렬화는 강력하게 권장되지 않습니다. 자바 컴파일러가 내부 클래스와 같은 특정 구조를 컴파일할 때, 소스 코드에 해당 구조가 없는 합성 구조를 생성합니다. 합성 구조에는 클래스, 메서드, 필드 및 기타 구조가 포함됩니다. 합성 구조는 JVM의 변경 없이 새로운 자바 언어 기능을 구현할 수 있도록 자바 컴파일러가 사용하는 것입니다. 그러나 합성 구조는 서로 다른 자바 컴파일러 구현 간에 차이가 있을 수 있습니다. 즉, .class 파일이 서로 다른 구현 간에 다를 수 있습니다. 따라서 내부 클래스를 직렬화한 후 다른 JRE 구현으로 역직렬화할 경우 호환성 문제가 발생할 수 있습니다. 내부 클래스가 컴파일될 때 생성되는 합성 구조에 대한 자세한 내용은 "Obtaining Names of Method Parameters" 섹션의 "Implicit and Synthetic Parameters" 부분을 참조하십시오.
Inner Class Example
내부 클래스가 사용되는 것을 보기 위해, 먼저 배열을 고려해보겠습니다. 다음 예제에서, 배열을 생성하고, 정수 값으로 채운 다음, 오름차순으로 배열의 짝수 인덱스의 값만을 출력합니다.
이어지는 DataStructure.java 예제는 다음으로 구성됩니다:
- DataStructure 외부 클래스는 연속된 정수 값(0, 1, 2, 3 등)으로 채워진 배열을 포함하는 DataStructure의 인스턴스를 생성하기 위한 생성자와 짝수 인덱스 값을 가진 배열 요소들을 출력하는 메소드를 포함합니다.
- EvenIterator 내부 클래스는 DataStructureIterator 인터페이스를 구현하며, 이 인터페이스는 Iterator<Integer> 인터페이스를 확장합니다. 이터레이터는 데이터 구조를 순회하는 데 사용되며, 일반적으로 마지막 요소를 테스트하는 메소드, 현재 요소를 검색하는 메소드, 다음 요소로 이동하는 메소드를 가지고 있습니다.
- main 메소드는 DataStructure 객체(ds)를 인스턴스화한 다음, printEven 메소드를 호출하여 짝수 인덱스 값을 가진 arrayOfInts 배열의 요소들을 출력합니다.
public class DataStructure {
// Create an array
private final static int SIZE = 15;
private int[] arrayOfInts = new int[SIZE];
public DataStructure() {
// fill the array with ascending integer values
for (int i = 0; i < SIZE; i++) {
arrayOfInts[i] = i;
}
}
public void printEven() {
// Print out values of even indices of the array
DataStructureIterator iterator = this.new EvenIterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
}
interface DataStructureIterator extends java.util.Iterator<Integer> { }
// Inner class implements the DataStructureIterator interface,
// which extends the Iterator<Integer> interface
private class EvenIterator implements DataStructureIterator {
// Start stepping through the array from the beginning
private int nextIndex = 0;
public boolean hasNext() {
// Check if the current element is the last in the array
return (nextIndex <= SIZE - 1);
}
public Integer next() {
// Record a value of an even index of the array
Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);
// Get the next even element
nextIndex += 2;
return retValue;
}
}
public static void main(String s[]) {
// Fill the array with integer values and print out only
// values of even indices
DataStructure ds = new DataStructure();
ds.printEven();
}
}
출력은:
0 2 4 6 8 10 12 14
EvenIterator 클래스는 DataStructure 객체의 arrayOfInts 인스턴스 변수를 직접 참조한다는 점에 유의하세요.
이 예제에서 보여주는 것처럼, 내부 클래스를 사용하여 도우미 클래스와 같은 것들을 구현할 수 있습니다. 사용자 인터페이스 이벤트를 처리하기 위해서는 내부 클래스 사용 방법을 알아야 합니다. 왜냐하면 이벤트 처리 메커니즘은 내부 클래스를 광범위하게 사용하기 때문입니다.
Local and Anonymous Classes
내부 클래스에는 두 가지 추가적인 유형이 있습니다. 메소드의 본문 내에 내부 클래스를 선언할 수 있습니다. 이러한 클래스들은 로컬 클래스로 알려져 있습니다. 또한, 클래스의 이름을 지정하지 않고 메소드 본문 내에 내부 클래스를 선언할 수도 있습니다. 이러한 클래스들은 익명 클래스로 알려져 있습니다.
Modifiers
내부 클래스에는 외부 클래스의 다른 멤버들에 사용하는 것과 동일한 수정자(modifiers)를 사용할 수 있습니다. 예를 들어, 다른 클래스 멤버들의 접근을 제한하는 데 사용하는 것처럼, 액세 지정자인 private, public, 및 protected를 사용하여 내부 클래스에 대한 접근을 제한할 수 있습니다.
Local Classes
로컬 클래스는 균형 잡힌 중괄호 사이에 있는 하나 이상의 statement 그룹인 블록 내에 정의된 클래스입니다. 일반적으로 메소드 본문 내에서 로컬 클래스를 정의하는 것을 볼 수 있습니다.
Declaring Local Classes
어떤 블록 내에서도 로컬 클래스를 정의할 수 있습니다(자세한 내용은 Expressions, Statements, and Blocks 을 참조하세요). 예를 들어, 메소드 본문, for 루프, if 절에서 로컬 클래스를 정의할 수 있습니다.
다음 예제인 LocalClassExample은 두 개의 전화번호를 검증합니다. 이는 validatePhoneNumber 메소드 내에 PhoneNumber라는 로컬 클래스를 정의합니다:
public class LocalClassExample {
static String regularExpression = "[^0-9]";
public static void validatePhoneNumber(
String phoneNumber1, String phoneNumber2) {
final int numberLength = 10;
// Valid in JDK 8 and later:
// int numberLength = 10;
class PhoneNumber {
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber){
// numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
public String getNumber() {
return formattedPhoneNumber;
}
// Valid in JDK 8 and later:
// public void printOriginalNumbers() {
// System.out.println("Original numbers are " + phoneNumber1 +
// " and " + phoneNumber2);
// }
}
PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
// Valid in JDK 8 and later:
// myNumber1.printOriginalNumbers();
if (myNumber1.getNumber() == null)
System.out.println("First number is invalid");
else
System.out.println("First number is " + myNumber1.getNumber());
if (myNumber2.getNumber() == null)
System.out.println("Second number is invalid");
else
System.out.println("Second number is " + myNumber2.getNumber());
}
public static void main(String... args) {
validatePhoneNumber("123-456-7890", "456-7890");
}
}
이 예제는 먼저 전화번호에서 0부터 9까지의 숫자를 제외한 모든 문자를 제거함으로써 전화번호를 검증합니다. 그 후에, 전화번호가 북미 지역의 전화번호 길이인 정확히 10자리를 포함하고 있는지 확인합니다. 이 예제는 다음과 같은 결과를 출력합니다:
First number is 1234567890
Second number is invalid
Accessing Members of an Enclosing Class
로컬 클래스는 이 클래스를 포함하는 클래스의 멤버에 접근할 수 있습니다. 이전 예제에서, PhoneNumber 생성자는 LocalClassExample.regularExpression 멤버에 접근합니다.
또한, 로컬 클래스는 로컬 변수에도 접근할 수 있습니다. 그러나 로컬 클래스는 final로 선언된 로컬 변수에만 접근할 수 있습니다. 로컬 클래스가 이 클래스를 포함하는 블록의 로컬 변수나 파라미터에 접근할 때, 그 변수나 파라미터를 캡처합니다. 예를 들어, PhoneNumber 생성자는 final로 선언된 numberLength 로컬 변수에 접근할 수 있으며, numberLength는 캡처된 변수입니다.
그러나 Java SE 8부터 로컬 클래스는 포함 블록의 final 또는 실질적으로 final인 로컬 변수와 파라미터에 접근할 수 있습니다. 초기화된 후에 결코 변경되지 않는 변수나 파라미터는 실질적으로 final입니다. 예를 들어, numberLength 변수가 final로 선언되지 않았다고 가정하고, PhoneNumber 생성자에 하이라이트된 할당 문을 추가하여 유효한 전화번호의 길이를 7자리로 변경한다면:
PhoneNumber(String phoneNumber) {
numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
이 할당 문으로 인해, 변수 numberLength는 더 이상 실질적으로 final이 아닙니다. 결과적으로, 내부 클래스 PhoneNumber가 numberLength 변수에 접근하려고 할 때, 자바 컴파일러는 "내부 클래스에서 참조되는 로컬 변수는 final이거나 실질적으로 final이어야 합니다"와 유사한 오류 메시지를 생성합니다:
if (currentNumber.length() == numberLength)
Java SE 8부터 메소드 내에서 로컬 클래스를 선언하면, 해당 클래스는 메소드의 파라미터에 접근할 수 있습니다. 예를 들어, PhoneNumber 로컬 클래스 내에 다음과 같은 메소드를 정의할 수 있습니다:
public void printOriginalNumbers() {
System.out.println("Original numbers are " + phoneNumber1 +
" and " + phoneNumber2);
}
메소드 printOriginalNumbers는 validatePhoneNumber 메소드의 파라미터인 phoneNumber1과 phoneNumber2에 접근합니다.
Shadowing and Local Classes
로컬 클래스에서 타입(예: 변수)의 선언은 같은 이름을 가진 포함 범위의 선언을 가립니다(shadow). 자세한 내용은 Shadowing을 참조하십시오.
Local Classes Are Similar To Inner Classes
로컬 클래스는 어떠한 정적 멤버도 정의하거나 선언할 수 없기 때문에 inner 클래스와 유사합니다. static 메소드 내의 로컬 클래스, 예를 들어 static 메소드 validatePhoneNumber 내에 정의된 PhoneNumber 클래스는 PhoneNumber 클래스 를 포함하는 클래스의 static 멤버에만 참조할 수 있습니다. 예를 들어, 멤버 변수 regularExpression을 static으로 정의하지 않으면, 자바 컴파일러는 "static context에서 non-static 변수 regularExpression을 참조할 수 없습니다"와 유사한 오류를 생성합니다.
로컬 클래스는 로컬 클래스를 포함하는 블록의 인스턴스 멤버에 접근할 수 있기 때문에 non-static입니다. 따라서 대부분의 종류의 static 선언을 포함할 수 없습니다.
블록 내에서 인터페이스를 선언할 수 없습니다; 인터페이스는 본질적으로 static입니다. 예를 들어, 다음 코드 조각은 greetInEnglish 메소드 본문 내에 인터페이스 HelloThere가 정의되어 있기 때문에 컴파일되지 않습니다:
public void greetInEnglish() {
interface HelloThere {
public void greet();
}
class EnglishHelloThere implements HelloThere {
public void greet() {
System.out.println("Hello " + name);
}
}
HelloThere myGreeting = new EnglishHelloThere();
myGreeting.greet();
}
로컬 클래스 내에서는 static 초기화 블록이나 멤버 인터페이스를 선언할 수 없습니다.
다음 코드 조각은 EnglishGoodbye.sayGoodbye 메소드가 static으로 선언되어 있기 때문에 컴파일되지 않습니다.
컴파일러는 이 메소드 정의를 만나면 "수정자 'static'은 상수 변수 선언에서만 허용됩니다"와 유사한 오류를 생성합니다:
public void sayGoodbyeInEnglish() {
class EnglishGoodbye {
public static void sayGoodbye() {
System.out.println("Bye bye");
}
}
EnglishGoodbye.sayGoodbye();
}
로컬 클래스는 상수 변수인 경우에 한해 정적 멤버를 가질 수 있습니다. (상수 변수는 기본 타입이거나 String 타입으로, final로 선언되고 컴파일 타임 상수 표현식으로 초기화된 변수입니다. 컴파일 타임 상수 표현식은 일반적으로 컴파일 시간에 평가될 수 있는 문자열이나 산술 표현식입니다. 자세한 내용은 Understanding Class Members 를 참조하세요.) 다음 코드 조각은 정적 멤버 EnglishGoodbye.farewell이 상수 변수이기 때문에 컴파일됩니다:
public void sayGoodbyeInEnglish() {
class EnglishGoodbye {
public static final String farewell = "Bye bye";
public void sayGoodbye() {
System.out.println(farewell);
}
}
EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
myEnglishGoodbye.sayGoodbye();
}
Anonymous Classes
익명 클래스는 코드를 더 간결하게 만들 수 있습니다. 익명 클래스는 클래스의 선언과 인스턴스화를 동시에 할 수 있게 합니다. 이름이 없는 것을 제외하면 지역 클래스와 비슷합니다. 지역 클래스를 한 번만 사용해야 할 경우 익명 클래스를 사용하세요.
이 섹션에서는 다음 주제를 다룹니다:
- 익명 클래스 선언
- 익명 클래스의 문법
- 감싸는 범위의 지역 변수에 접근하기 및 익명 클래스의 멤버 선언과 접근
- 익명 클래스의 예제
Declaring Anonymous Classes
지역 클래스가 클래스 선언인 반면, 익명 클래스는 expression입니다. 이는 다른 expression 내에서 클래스를 정의한다는 것을 의미합니다. 다음 예제인 HelloWorldAnonymousClasses는 지역 변수 frenchGreeting과 spanishGreeting의 초기화 문에서 익명 클래스를 사용하고, 변수 englishGreeting의 초기화에는 지역 클래스를 사용합니다.
public class HelloWorldAnonymousClasses {
interface HelloWorld {
public void greet();
public void greetSomeone(String someone);
}
public void sayHello() {
class EnglishGreeting implements HelloWorld {
String name = "world";
public void greet() {
greetSomeone("world");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hello " + name);
}
}
HelloWorld englishGreeting = new EnglishGreeting();
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Salut " + name);
}
};
HelloWorld spanishGreeting = new HelloWorld() {
String name = "mundo";
public void greet() {
greetSomeone("mundo");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hola, " + name);
}
};
englishGreeting.greet();
frenchGreeting.greetSomeone("Fred");
spanishGreeting.greet();
}
public static void main(String... args) {
HelloWorldAnonymousClasses myApp = new HelloWorldAnonymousClasses();
myApp.sayHello();
}
}
이 예제에서 EnglishGreeting 클래스는 지역 클래스입니다. 반면에 frenchGreeting과 spanishGreeting은 익명 클래스를 사용하여 초기화됩니다. 익명 클래스는 이름이 없으며 클래스 선언과 동시에 인스턴스화됩니다. 이는 특정 클래스가 한 번만 사용될 때 유용합니다.
englishGreeting, frenchGreeting, spanishGreeting의 각 클래스는 HelloWorld 인터페이스를 구현하며, 각 언어로 인사를 출력하는 메서드를 포함합니다. main 메서드는 HelloWorldAnonymousClasses의 인스턴스를 생성하고 sayHello 메서드를 호출하여 각 인사 메시지를 출력합니다.
Syntax of Anonymous Classes
앞서 언급했듯이 익명 클래스는 expression입니다. 익명 클래스 extension의 syntax는 코드 블록에 클래스 정의가 포함되어 있다는 점을 제외하면 생성자의 호출과 유사합니다.
frenchGreeting 객체의 인스턴스화를 고려해보세요.
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Salut " + name);
}
};
익명 클래스 expression은 다음으로 구성됩니다.
- new 연산자
- 구현할 인터페이스 또는 확장할 클래스의 이름입니다. 이 예에서 익명 클래스는 HelloWorld 인터페이스를 구현합니다.
- 일반 클래스 인스턴스 생성 expression과 마찬가지로 생성자에 대한 아규먼트를 포함하는 괄호입니다. 참고: 인터페이스를 구현할 때는 생성자가 없으므로 이 예제와 같이 빈 괄호 쌍을 사용합니다.
- 클래스 선언 본문을 의미하는 본문입니다. 보다 구체적으로 말하면 본문에서는 메서드 선언이 허용되지만 statement은 허용되지 않습니다.
public class MyClass {
// 잘못된 문: 클래스 본문 안에 직접 문(statement)이 올 수 없습니다.
int number = 5;
System.out.println("Hello, World!"); // 컴파일 오류
// 생성자 선언
public MyClass() {
// 올바른 문: 생성자 본문 안에서는 문(statement)이 허용됩니다.
number = 10;
System.out.println("Hello, Constructor!");
}
// 메서드 선언
public void myMethod() {
// 올바른 문: 메서드 본문 안에서는 문(statement)이 허용됩니다.
System.out.println("Hello, Method!");
}
}
익명 클래스 정의는 expression이므로 statement의 일부여야 합니다. 이 예에서 익명 클래스 expression은 FrenchGreeting 객체를 인스턴스화하는 statement의 일부입니다. (이것은 닫는 중괄호 뒤에 세미콜론이 있는 이유를 설명합니다.)
Accessing Local Variables of the Enclosing Scope, and Declaring and Accessing Members of the Anonymous Class
로컬 클래스와 마찬가지로 익명 클래스도 변수를 캡처할 수 있습니다. 그들은 둘러싸는 범위의 지역 변수에 대해 동일한 액세스 권한을 갖습니다.
- 익명 클래스는 자신을 둘러싸는 클래스의 멤버에 액세스할 수 있습니다.
- 익명 클래스는 final 또는 사실상 final으로 선언되지 않은 바깥쪽 범위의 지역 변수에 액세스할 수 없습니다.
- 중첩 클래스와 마찬가지로 익명 클래스의 유형 선언(예: 변수)은 바깥쪽 범위에서 동일한 이름을 가진 다른 모든 선언을 숨깁니다. 자세한 내용은 섀도잉을 참조하세요.
※ 익명 클래스는 final 또는 사실상 final으로 선언되지 않은 바깥쪽 범위의 지역 변수에 액세스할 수 없습니다.의 예
컴파일 오류 발생
public class OuterClass {
public void myMethod() {
int number = 10; // final 또는 사실상 final이 아님
// 익명 클래스 선언
Runnable runnable = new Runnable() {
@Override
public void run() {
// number 변수에 접근하려고 하면 컴파일 오류 발생
System.out.println(number);
}
};
runnable.run();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.myMethod();
}
}
컴파일 오류 없음
public class OuterClass {
public void myMethod() {
final int number = 10; // final로 선언됨
// 익명 클래스 선언
Runnable runnable = new Runnable() {
@Override
public void run() {
// number 변수에 접근 가능
System.out.println(number);
}
};
runnable.run();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.myMethod();
}
}
사실상 final
public class OuterClass {
public void myMethod() {
int number = 10; // 사실상 final
// 익명 클래스 선언
Runnable runnable = new Runnable() {
@Override
public void run() {
// number 변수에 접근 가능
System.out.println(number);
}
};
runnable.run();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.myMethod();
}
}
익명 클래스에는 멤버와 관련하여 로컬 클래스와 동일한 제한 사항이 있습니다.
- 익명 클래스에서는 정적 초기화 프로그램이나 멤버 인터페이스를 선언할 수 없습니다.
- 익명 클래스는 상수 변수인 경우 정적 멤버를 가질 수 있습니다.
익명 클래스에서는 다음을 선언할 수 있습니다.
- Fields
- 추가 메소드(상위 유형의 메소드를 구현하지 않더라도)
- 인스턴스 초기화
- 로컬 클래스
그러나 익명 클래스에서는 생성자를 선언할 수 없습니다.
Examples of Anonymous Classes
익명 클래스는 GUI(그래픽 사용자 인터페이스) 애플리케이션에서 자주 사용됩니다.
JavaFX 예제 HelloWorld.java를 고려하십시오( Hello World, JavaFX Style from Getting Started with JavaFX의 섹션에서 참조). 이 샘플은 'Hello World' 버튼이 포함된 프레임을 만듭니다. 익명 클래스 expression이 강조표시됩니다.
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
이 예에서 메소드 호출 btn.setOnAction은 'Hello World' 버튼을 선택할 때 발생하는 상황을 지정합니다. 이 메서드에는 EventHandler<ActionEvent> 유형의 개체가 필요합니다. EventHandler<ActionEvent> 인터페이스에는 핸들이라는 하나의 메서드만 포함되어 있습니다. 이 예제에서는 새 클래스로 이 메서드를 구현하는 대신 익명 클래스 식을 사용합니다. 이 표현식은 btn.setOnAction 메소드에 전달된 인수입니다.
EventHandler<ActionEvent> 인터페이스에는 메서드가 하나만 포함되어 있으므로 익명 클래스 expression 대신 람다 expression을 사용할 수 있습니다. 자세한 내용은 람다 experssion 섹션을 참조하세요.
익명 클래스는 두 개 이상의 메서드가 포함된 인터페이스를 구현하는 데 이상적입니다. 다음 JavaFX 예제는 UI 컨트롤 사용자 정의 섹션에서 가져온 것입니다. 강조 표시된 코드는 숫자 값만 허용하는 텍스트 필드를 만듭니다. TextInputControl 클래스에서 상속된 교체 텍스트 및 교체 선택 메서드를 재정의하여 익명 클래스로 TextField 클래스의 기본 구현을 재정의합니다.
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class CustomTextFieldSample extends Application {
final static Label label = new Label();
@Override
public void start(Stage stage) {
Group root = new Group();
Scene scene = new Scene(root, 300, 150);
stage.setScene(scene);
stage.setTitle("Text Field Sample");
GridPane grid = new GridPane();
grid.setPadding(new Insets(10, 10, 10, 10));
grid.setVgap(5);
grid.setHgap(5);
scene.setRoot(grid);
final Label dollar = new Label("$");
GridPane.setConstraints(dollar, 0, 0);
grid.getChildren().add(dollar);
final TextField sum = new TextField() {
@Override
public void replaceText(int start, int end, String text) {
if (!text.matches("[a-z, A-Z]")) {
super.replaceText(start, end, text);
}
label.setText("Enter a numeric value");
}
@Override
public void replaceSelection(String text) {
if (!text.matches("[a-z, A-Z]")) {
super.replaceSelection(text);
}
}
};
sum.setPromptText("Enter the total");
sum.setPrefColumnCount(10);
GridPane.setConstraints(sum, 1, 0);
grid.getChildren().add(sum);
Button submit = new Button("Submit");
GridPane.setConstraints(submit, 2, 0);
grid.getChildren().add(submit);
submit.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent e) {
label.setText(null);
}
});
GridPane.setConstraints(label, 0, 1);
GridPane.setColumnSpan(label, 3);
grid.getChildren().add(label);
scene.setRoot(grid);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
이상입니다!
'2. Java Tutorials' 카테고리의 다른 글
[Java Tutorials] #10 Lesson: Interfaces and Inheritance[Interfaces] (38) | 2024.08.23 |
---|---|
[Java Tutorials] #9 Lesson: Classes and Objects 4[Lambda, Enum] (28) | 2024.08.22 |
[JAVA Tutorials] #7 Lesson: Classes and Objects 2 (98) | 2024.08.20 |
[JAVA Tutorials] #6 Lesson: Classes and Objects 1 (88) | 2024.08.19 |
[JAVA Tutorials] #5 Lesson: Control Flow Statements (106) | 2024.08.15 |