내부 클래스
내부 클래스는 이름 그대로 클래스 안의 클래스이고, 말 그대로 클래스 안에 클래스를 선언하는 것과 같다. 우리는 지금까지 클래스끼리는 격리 시켜서 생성했는데 내부에도 클래스를 생성할 수 있다는 것이다. 다음과 같이 말이다.
// 기존의 방식
class A {
}
class B {
}
=============
class AA {
class BB {
}
}
위처럼 구성이 되어있을 때 BB를 AA 클래스의 내부 클래스, AA를 BB 클래스의 외부 클래스라고 부른다.
그럼 위와 같은 내부 클래스를 사용하는 이유는 무엇일까? 결론부터 얘기하자면, 캡슐화 및 코드의 복잡성을 줄이는 것이다. 예를 들어서 BB라는 클래스는 AA의 클래스만 사용을 한다고 가정하자. 그럼 BB 클래스는 AA 클래스의 객체를 생성하고 해당 AA 인스턴스에 변수들이 캡슐화 되어 있다고 할 때, 메서드를 통해서 접근을 해줘야 한다. 다음과 같이 말이다.
class A {
private int i = 100;
public int getI(){
return i;
}
}
class B {
void method() {
A a = new A();
System.out.print(a.getI());
}
}
그러나 내부 클래스를 사용하면 이러한 번거로움을 제거할 수 있다.
class A {
private int i = 100;
B b = new B();
public int getI() {
return i;
}
class B {
void method() {
// 다음처럼 외부 클래스의 private 멤버에도 접근이 가능하다.
System.out.print(i);
}
}
}
public class Main {
public static void main(String[] args) {
A a = new A();
a.b.method();
}
}
Composition을 사용하여 B 클래스를 A 클래스에서 생성되도록 하고 A 클래스를 통해 B 클래스에 접근하였다. 이렇게 했을 때 다른 클래스에서는 B 클래스에 접근이 불가능 할 것이다.
내부 클래스의 종류와 유효 범위는 변수와 동일하다. 다음과 같이 구분 할 수 있다.
class A {
int iv = 0;
static int cv = 0;
void methodA() {
int lv = 0;
}
}
class B {
class InstanceB {}
static class ClassB {}
void methodB() {
class LocalB {}
}
}
위에 얘기했던 것처럼 유효 범위는 변수와 동일하다. 변수와 비슷한 것처럼 내부클래스에는 default, public 말고도 다른 접근제어자도 사용할 수 있다. 내부 클래스에는 위 처럼 3가지 말고 익명 클래스라는 것이 하나 더 존재한다. 익명 클래스는 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스이다. 람다 혹은 Event 에서 많이 사용되는 방식인데, 익명 클래스의 생성 방식을 알아야 람다를 이해하고, 람다를 이해해야 스트림을 이해할 수 있다.
내부 클래스는 예시를 확인하면서 보는게 제일 인 것 같다. 왜냐하면 이미 우리는 변수를 배우면서 접근 제어자와 유효범위가 어떻게 구성되어 있는지 알고 있기 때문이다. 이미 배웠지만 간단하게 확인해보고 가자.
class A {
class InstanceInner {
int iv = 100;
static int cv = 100; // 1️⃣
final static int CONST = 100;
}
static class StaticInner {
int iv = 200;
static int cv = 200; // 2️⃣
static class StaticInner2 {}
}
void method() {
class LocalInner {
int iv = 300;
static int cv = 300;
final static int CONST = 300; // 3️⃣
}
System.out.println(LocalInner.CONST); // 가능
}
}
public class Main {
public static void main(String[] args) {
System.out.println(StaticInner.cv); // 가능
System.out.println(InstanceInner.CONST); // 가능
System.out.println(LocalInner.CONST); // 불가능, 지역 내부 클래스는 메서드 내에서만 가능하다.
}
}
먼저 1번부터 살펴보자. InstanceInner 클래스는 A 클래스의 내부 클래스이지만 인스턴스 멤버에 속한다. 기존에 클래스에서 인스턴스 메서드에서 클래스 변수를 생성할 수 없었던 것처럼, 동일하게 인스턴스 내부 클래스에서는 클래스 변수를 생성할 수 없다.
2번을 보면 static 클래스가 생성되어 있는 것을 볼 수 있다. 이도 당연하게 우리는 기존에 static 멤버에 접근할 때 인스턴스를 생성하지 않고 클래스 이름을 사용하여 접근할 수 있었다. 또한 클래스 멤버는 애플리케이션 시작 시점에 메모리에 올라가기 때문에, 내부에서 static 멤버를 또 선언을 해줘도 문제가 없는 것이다.
3번은 예외 상황이다. 예전에 잠깐 얼핏 얘기했지만 상수같은 경우는 Constant Pool 이라는 곳에서 상수를 따로 관리한다. 따라서 어디서든 생성이 가능하다.
참고) String을 대입연산자를 이용해서 생성할 경우에도 동일하게 저장된다.
JDK 1.8 이후 인스턴스 변수 및, 지역 변수에서 변수를 선언하고 변수의 값이 변하지 않으면 상수로 취급한다.
자주 사용하지는 않겠지만 혹시 내부 클래스를 생성해야 한다면 다음과 같이 생성해야 한다.
A a = new A();
a.InstanceInner i = a.new InstanceInner();
기존 문법에서 크게 벗어나지 않는다. 그저 참조변수를 통해서 해당 클래스에 접근해서 생성자로 인스턴스를 생성한다고 생각하면 된다. 단, 스태틱 클래스는 기존의 클래스 변수와 같이 인스턴스를 생성하지 않고도 사용할 수 있으므로, 다음과 같이 사용하면 된다.
System.out.println(A.StaticInner.cv);
// 단 스태틱 클래스의 인스턴스 변수에 접근하기 위해서는 객체를 생성해주어야 하는데, 이 때, 외부 클래스를 생성하지 않아도 된다.
A.StaticInner si = new A.StaticInner();
System.out.println(si.iv);
실제로 위의 클래스를 getClass()를 통해 나오는 결과를 확인해보면 다음과 같이 나온다.
class A
class A$InstanceInner
class A$StaticInner
class A$1LocalInner
class A$StaticInner$StaticInner2
$는 내부 클래스라는 의미이다. 프록시 패턴을 사용하게 되면 볼 수 있는 부분이였는데, 프록시 패턴의 구현방식도 비슷한걸까? 라는 생각이 든다. 그리고 자세히 보면 LocalInner 좌측에 숫자 1이 있는 걸 볼 수 있는데, 지역 멤버는 스택에 호출되고 사라질 수 있는 스택에만 고유하게 존재할 수 있는 영역이기에 지역 변수의 범위에서 클래스 이름이 겹쳐도 상관없기 때문에, 번호가 있는 것이다.
익명 클래스
익명 클래스는 위에서 얘기했듯이 이름이 없는 일회용 클래스이다. 즉, 정의와 생성을 동시에 하는 것인데, 익명 클래스는 만드는 방법만 알면 그렇게 어려운 것은 아니다. 주로 awt 혹은 Comparator, Comparable 같은 정렬 인터페이스를 구현 할 때 많이 사용한다.
new 조상클래스() {
}
new 구현할 인터페이스이름() {
}
다음과 같이 작성하면 된다. 익명이기 때문에 본인 클래스 이름이 없어서 조상클래스로 생성하는 것이다. 상속을 받아야 할 클래스라던가 혹은 사용할 것 같지 않지만, 기본 객체라면 Object를 상속받아서 익명 클래스를 생성하는 것이 가능하다. awt를 만드는 예시를 한 번 살펴보자.
public class Main {
public static void main(String[] args) {
Button b = new Button("Start");
b.addActionListener(new EventHandler());
}
}
class EventHandler implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("ActionEvent occurred!!!");
}
}
다음과 같이 구성이 되어있다고 하자. b 라는 버튼에 이벤트를 주기 위하여 이벤트 핸들러를 생성해서 ActionListener 라는 인터페이스의 참조 객체를 인자로 넘겨준 것을 볼 수 있는데, Event Handler는 보통 한 번 사용되고 사용되지 않는다. 이런 상황에 익명 클래스를 사용할 수 있는데 사용 방법을 차근 차근 살펴보자.
// b.addActionListener(new 조상 클래스 or 인터페이스) {}
b.addActionListener(new ActionListener() {}); // 1. 익명 클래스 생성
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("ActionEvent occurred!!!");
}
});
// 내부 구현
기존에 구현했었던 메서드를 그대로 가져온 것이다. 이렇게 생성하는 것이 그렇게 어렵지 않고 간단하다. 단, 익명클래스가 이렇게 생성되는구나 를 이해해야 함수형 프로그래밍인 람다를 이해할 수 있고, 람다를 이해해야 스트림사용이 가능하다.
'Language > Java' 카테고리의 다른 글
JVM (0) | 2023.04.21 |
---|---|
enum의 Enum 상수 객체의 변수 사용과 생성 (0) | 2023.04.12 |
12. Generic 제너릭 (0) | 2023.03.29 |
4-2 자바 반복문 (0) | 2023.03.23 |
4-1 자바 조건문 (0) | 2023.03.23 |