특정 클래스끼리만의 관계를 맺는 경우에는 중첩(nested) 클래스로 선언하는 것이 유지보수에 도움이 되는 경우가 많다.
중첩 클래스(nested) 클래스란 클래스 내부에 선언한 클래스를 말하는데, 중첩 클래스를 사용하면 클래스의 맴버를 쉽게 사용할 수 있고 외부에는 중첩 관계 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점이 있다.
중첩 클래스는 선언하는 위치에 따라 두 가지로 분류된다. 클래스의 맴버로서 선언되는 중첩 클래스를 맴버 클래스라고 하고, 메서드 내부에서 선언되는 중첩 클래스를 로컬 클래스라고 한다.
class A {
class B {...}
}
A 객체를 생성해야만, B 객체를 생성할 수 있다.
class A {
static class B {...}
}
A 객체를 생성하지 않아도, B 객체를 생성할 수 있다.
class A {
void method() {
class B { ... }
}
}
method가 실행할 B 객체를 생성할 수 있다.
중첩 클래스도 하나의 클래스이기 때문에 컴파일하면 바이트코드 파일(.class)이 별도로 생성된다.
A $ B .class
A $1 B .class
인스턴스 맴버 클래스는 다음과 같이 A 클래스 맴버로 선언된 B 클래스를 말한다.
[public] class A {
[public | private] class B {
} // 인스턴스 클래스
}
접근 제한자에 따른 인스턴스 맴버 클래스의 접근 범위는 다음과 같다.
구분 | 접근 범위 |
---|---|
public class B() | 다른 패키지에서 B 클래스를 사용할 수 있다. |
class B () | 같은 패키지에서만 B 클래스를 사용할 수 있다. |
private class B() | A 클래스 내부에서만 B 클래스를 사용할 수 있다. |
인스턴스 맴버 클래스는 주로 private
접근자를 사용하는 것이 일반적이다. 이는 중첩 클래스 정의 자체가 특정 클래스에 종속되었기 때문이다.
public class A {
// 인스턴스 맴버 클래스
class B {}
// 인스턴스 필드
B field = new B();
// 생성자
A() {
B b = new B();
}
// 인스턴스 메서드
void method() {
B b = new B();
}
}
B객체를 A클래스 외부에 생성하려면 default
또는 public
접근 제한자를 가져야 하고, A객체를 먼저 생성한 다음 B
객체를 생성해야 한다.
A a = new A();
A.B b = a.new B();
인스턴스 맴버 클래스 B내부에는 일반 클래스와 같이 필드, 생성자 메서드 선언이 올 수 있다. 정적 필드와 정적 메서드는 Java17
부터 선언이 가능하다.
public class A {
// 인스턴스 맴버 클래스
class B {
// 인스턴스 필드
int field1 = 1;
// 정적 필드(Java17부터 허용)
static int field2 = 2;
// 생성자
B() {
System.out.println("B생성자 실행");
}
//인스턴스 메서드
void method1() {
System.out.println("Bmethod 실행");
}
// 정적 메서드(Java17부터 허용)
static void method2() {
System.out.println("Bmethod2 실행");
}
}
void useB() {
// B 객체 생성 및 인스턴스 필드 및 메서드 사용
B b = new B();
System.out.println(b.field1);
b.method1();
// 정적 필드, 메서드 사용
System.out.println(B.field2);
B.method2();
}
}
정적 맴버 클래스는 다음과 같이 static
키워드와 함께 A클래스의 맴버로 선언된 B클래스를 말한다.
[public] class A {
// 정적 맴버 클래스
[public | private] static class B {
}
}
접근 제한자에 따른 정적 맴버 클래스의 접근 범위는 다음과 같다.
구분 | 접근 범위 |
---|---|
public static class B() | 다른 패키지에서 B 클래스를 사용할 수 있다. |
static class B() | 같은 패키지에서만 B 클래스를 사용할 수 있다. |
private static class B() | A 클래스 내부에서만 B 클래스를 사용할 수 있다. |
정적 맴버 클래스 B는 A 클래스 내부에서 사용되기도 하지만, A 클래스 외부에서 A와 함께 사용되는 경우가 많기 때문에 주로 default
, private
접근 제한을 가진다. B 객체는 A 클래스 내부 어디든 객체를 생성할 수 있다.
public class A {
// 정적 맴버 클래스
static class B {
// 인스턴스 필드
int field1 = 1;
// 정적 필드(Java17부터 사용)
static int field2 = 2;
// 생성자
B() {
System.out.println("B 생성자 실행");
}
// 인스턴스 메서드
void method1() {
System.out.println("B method1 실행");
}
// 정적 메서드(Java17부터 허용)
static void method2() {
System.out.println("B method2 실행");
}
}
}
다음과 같이 실행이 가능하다.
public class Main {
public static void main(String[] args) {
//B 객체 생성 및 인스턴스 필드 및 메서드 사용
A.B b = new A.B();
System.out.println(b.field1);
b.method1();
//B 클래스의 정적 필드 및 메서드 사용
System.out.println(A.B.field2);
A.B.method2();
}
}
결과는 다음과 같다.
B 생성자 실행
1
B method1 실행
2
B method2 실행
생성자 또는 메서드 내부에서 다음과 같이 선언된 클래스를 로컬 클래스라고 한다.
[public] class A {
// 생성자
public A() {
class B {}
}
//메서드
public void method() {
class B { }
}
}
생성자와 method안에 정의된 class B
가 바로 로컬 클래스이다.
아래와 같이 로컬 클래스의 객체를 생성할 수 있다.
public class A {
// 생성자
A() {
// 로컬 클래스
class B {
void method2() {
System.out.println("method2");
}
}
B b = new B();
b.method2();
}
void method() {
class B {
void method3() {
System.out.println("method3");
}
}
B b = new B();
b.method3();
}
}
로컬 클래스 B 내부에는 일반 클래스와 같이 field, 생성자, method 선언이 올 수 있다. 정적 필드와 정적 메서드는 Java17부터 선언이 가능하다.
중첩 클래스는 바깥 클래스와 긴밀한 관계를 맺으면서 바깥 클래스의 맴버(필드, 메서드)에 접근할 수 있다. 하지만 중첩 클래스가 어떻게 선언되었느냐에 따라 접근 제한이 있을 수 있따.
구분 | 바깥 클래스의 사용 가능한 맴버 |
---|---|
인스턴스 맴버 클래스 | 바깥 클래스의 모든 필드와 메서드 |
정적 맴버 클래스 | 바깥 클래스의 정적 필드와 정적 메서드 |
public class A {
// A의 인스턴스 필드와 메서드
int field1;
void method1() {}
//A의 정적 필드와 메서드
static int field2;
static void method2() {}
class B {
void method() {
field1 = 10;
method1();
field2 = 10;
method2();
}
}
static class C {
void method() {
// field1 접근 불가
// method1() 접근 불가
field2 = 10;
method2();
}
}
}
정적 중첩 클래스 C는 바깥 클래스인 A의 정적 필드, 메서드만 접근할 수 있다.
중첩 클래스 내부에서 this
는 해당 중첩 클래스의 객체를 말한다. 만약 중첩 클래스 내부에서 바깥 클래스 객체를 얻으려면 바깥 클래스 이름에 this
를 붙여주면 된다.
// 중첩 클래스 내부
바깥클래스이름.this -----> 바깥 객체
다음은 중첩 클래스와 바깥 클래스가 동일한 이름의 인스턴스 필드와 메서드를 가지고 있을 경우, 바깥 객체 소속의 필드와 메서드를 사용하는 방법을 보여준다.
public class A {
// A 인스턴스 필드
String field = "A-field";
void method() {
System.out.println("A-method");
}
class B {
// B 인스턴스 필드
String field = "B-field";
// B 인스턴스 메서드
void method() {
System.out.println("B-method");
}
void print() {
System.out.println(this.field);
this.method();
System.out.println(A.this.field);
A.this.method();
}
}
// A의 인스턴스 메서드
void useB() {
B b = new B();
b.print();
}
}
중첩 클래스인 B 클래스에서 바깥 클래스인 A 클래스를 접근하는 방법은 A.this
를 통해서 가능한 것이다.
public class Main {
public static void main(String[] args) {
A a = new A();
a.useB();
}
}
결과는 다음과 같다.
B-field
B-method
A-field
A-method
중첩 인터페이스는 클래스의 맴버로 선언된 인터페이스를 말한다. 인터페이스를 클래스 내부에 선언하는 이유는 해당 클래스와 긴밀한 관계를 맺는 구현 객체를 만들기 위해서이다. 중첩 인터페이스는 다음과 같이 선언된다.
class A {
[public | private] [static] interface B {
//상수 필드
//추상 메서드
//디폴트 메서드
//정적 메서드
}
}
외부 접근을 막지 않으려면 public을 붙이고, A클래스 내에서만 사용하려면 private
를 붙인다. 접근 제한자를 붙이지 않으면 default
로 같은 패키지 안에서만 접근이 가능하다.
중첩 인터페이스는 보통 바깥 클래스에 실행에 있어서 callback으로 실행될 method를 정의해야할 때 필요하다.
가령, Button
class는 Button
을 눌렀을 때 사용자가 원하는 이벤트가 실행되어야 한다. 즉, Button
을 누르면 하나의 메서드가 이벤트로 실행되는 것이다. 이 메서드를 client에게 정의하게 하기 위해서 중첩 인터페이스를 사용하는 것이다.
public class Button {
// 정적 맴버 인터페이스
public static interface ClickListener {
// 추상 메서드
void onClick();
}
// 필드
private ClickListener clickListener;
// 메서드
public void setClickListener(ClickListener clickListener) {
this.clickListener = clickListener;
}
public void click() {
this.clickListener.onClick();
}
}
ClickListener
인터페이스에 대한 구현체를 client가 정의하여 넘겨주어야 하는 것이다.
public class Main {
public static void main(String[] args) {
Button btn = new Button();
class Listener implements Button.ClickListener {
@Override
public void onClick() {
System.out.println("Ok button을 출력");
}
}
btn.setClickListener(new Listener());
btn.click(); // Ok button을 출력
}
}
이렇게 만들면 Button
에 강하게 중속된 인터페이스를 굳이 다른 파일에 만들에 만들 필요가 없다.
익명 객체는 이름이 없는 객체를 말한다. 익명 객체는 클래스를 상속하거나, 인터페이스를 구현해야만 생성할 수 있다. 클래스를 상속해서 만들 경우 '익명 자식 객체'라고 하고, 인터페이스를 구현해서 만들 경우 '익명 구현 객체'라고 한다.
'익명 자식 객체'는 부모 클래스를 상속받아 다음과 같이 생성된다. 이렇게 생성된 객체는 부모 타입의 필드, 로컬 변수, 매개변수의 값으로 대입할 수 있다.
new 부모생성자(매개값, ...) {
//field
//method
}
다음은 Tire
클래스의 익명 자식 객체를 생성해서 필드, 로컬 변수, 매개변수의 값으로 사용하는 방법을 보여준다. Tire
클래스는 roll()
메서드를 가지고 있지만, 익명 자식 객체는 roll
을 오버라이드해 실행 내용을 변경한다.
public class Tire {
public void roll() {
System.out.println("일반 타이어");
}
}
public class Car {
//field에 Tire 객체 대입
private Tire tire1 = new Tire();
private Tire tire2 = new Tire() {
@Override
public void roll() {
System.out.println("익명 자식 Tire 객체1이 굴러간다.");
}
};
// method(field 이용)
public void run1() {
tire1.roll();
tire2.roll();
}
// method(로컬 변수 이용)
public void run2() {
Tire tire = new Tire() {
@Override
public void roll() {
System.out.println("익명 자식 Tire 객체2가 굴러간다.");
}
};
tire.roll();
}
public void run3(Tire tire) {
tire.roll();
}
}
new Tire() {}
은 익명 자식 객체로 Tire
를 부모로 상속한 익명 자식 객체를 만드는 것이다. 이를 통해서 우리는 Tire
의 메서드를 다시 정의할 수 있는 것이다.
실행을 시켜보도록 하자.
public class Main {
public static void main(String[] args) {
Car car = new Car();
//익명 자식 객체가 대입된 필드 사용
car.run1();
//익명 자식 객체가 대입된 로컬변수 사용
car.run2();
//익명 자식 객체가 대입된 매개변수 사용
car.run3(new Tire() {
@Override
public void roll() {
System.out.println("익명 자식 Tire 객체 3이 굴러간다.");
}
});
}
}
결과는 다음과 같다.
일반 타이어
익명 자식 Tire 객체1이 굴러간다.
익명 자식 Tire 객체2가 굴러간다.
익명 자식 Tire 객체 3이 굴러간다.
'익명 자식 객체'에 의해서 부모 객체인 Tire
를 상속하여 roll
메서드를 오버라이딩한 결과가 나온 것을 볼 수 있다.
'익명 구현 객체'는 인터페이스를 구현해서 다음과 같이 생성된다. 이렇게 생성된 객체는 인터페이스 타입의 필드, 로컬변수, 매개변수의 값으로 대입할 수 있다. '익명 구현 객체'는 안드로이드에서 많이 사용된다.
new 인터페이스() {
//field
//method
}
다음 예제는 RemoteControl
인터페이스의 '익명 구현 객체'를 사용해서 필드, 로컬 변수, 매개변수 값으로 사용하는 방법을 보여준다. '익명 구현 객체'는 roll()
메서드를 오버라이딩해서 실행 내용을 가지고 있다.
public interface RemoteControl {
//추상 메서드
void turnOn();
void turnOff();
}
public class Home {
// 필드에 익명 구현 객체 대입
private RemoteControl rc = new RemoteControl() {
@Override
public void turnOn() {
System.out.println("TV를 켠다.");
}
@Override
public void turnOff() {
System.out.println("TV를 끈다.");
}
};
// 메서드(필드 이름)
public void use1() {
rc.turnOn();
rc.turnOff();
}
// 메서드(로컬 변수 이용)
public void use2() {
//로컬 변수에 '익명 구현 객체' 대입
RemoteControl rc = new RemoteControl() {
@Override
public void turnOn() {
System.out.println("에어컨을 켠다");
}
@Override
public void turnOff() {
System.out.println("에어컨을 끈다.");
}
};
rc.turnOn();
rc.turnOff();
}
// 메서드 (매개변수 이용)
public void use3(RemoteControl rc) {
rc.turnOn();
rc.turnOff();
}
}
public class Main {
public static void main(String[] args) {
Home home = new Home();
home.use1();
home.use2();
home.use3(new RemoteControl() {
@Override
public void turnOn() {
System.out.println("난방을 켠다");
}
@Override
public void turnOff() {
System.out.println("난방을 끈다");
}
});
}
}
결과는 다음과 같다.
TV를 켠다.
TV를 끈다.
에어컨을 켠다
에어컨을 끈다.
난방을 켠다
난방을 끈다
다음은 실제로 쓰이는 예제들로, button 이벤트 처리에 필요한 객체를 '익명 구현 객체'로 쓰는 것이다.
public class Main {
public static void main(String[] args) {
Button btn = new Button();
btn.setClickListener(new Button.ClickListener() {
@Override
public void onClick() {
System.out.println("Ok 버튼 클릭");
}
});
btn.click(); // Ok 버튼 클릭
}
}
new Button.ClickListener()
로 '익명 구현 객체'를 만들어 넘겨준 것이다.