내부 클래스(inner class)는 사실 클래스 내에 선언된다는 점을 제외하고는 일반적인 클래스와 다르지 않다.
내부 클래스에는 인스턴스 클래스, 스태틱 클래스, 지역 클래스, 익명 클래스의 종류로 나눌 수 있다.
목차
1. 내부 클래스란?
2. 내부 클래스의 종류와 특징
3. 내부 클래스의 선언
4. 내부 클래스의 제어와 접근성
5. 익명 클래스(anonymous class)
1. 내부 클래스란?
내부 클래스는 클래스 내에 선언된 클래스입니다. 한 클래스를 다른 클래스의 내부 클래스로 선언하면 두 클래스의 멤버들 간에 서로 쉽게 접근할 수 있다는 장점과 외부에는 불필요한 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점을 얻을 수 있습니다.
내부 클래스의 장점
- 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
- 코드의 복잡성을 줄일 수 있다(캡슐화).
현재 A와 B 두개의 독립적인 클래스를 가지고 있는 왼쪽 이미지를 오른쪽과 같이 바꾸면 B는 A의 내부 클래스(inner class)가 되고 A는 B를 감싸고 있는 외부 클래스(outer class)가 됩니다.
이때 내부 클래스인 B는 외부 클래스인 A를 제외하고는 다른 클래스에서 잘 사용되지 않아야 합니다.
2. 내부 클래스의 종류와 특징
내부 클래스의 종류는 변수의 선언 위치에 따른 종류와 같습니다. 내부 클래스는 마치 변수를 선언하는 것과 같은 위치에 선언할 수 있으며, 변수의 선언 위치에 따라 인스턴스 변수, 클래스 변수(static 변수), 지역변수로 구분되는 것과 같이 내부 클래스도 선언 위치에 따라 다음과 같이 구분되어집니다.
내부 클래스 | 특 징 |
인스턴스 클래스(instance class) | 외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스멤버들과 관련된 작업에 사용될 목적으로 선언된다. |
스태틱 클래스(static class) | 외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 static멤버처럼 다루어진다. 주로 외부 클래스의 static멤버, 특히 static메서드에서 사용될 목적으로 선언된다. |
지역 클래스(local class) | 외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다. |
익명 클래스(anonymous class) | 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용) |
3. 내부 클래스의 선언
위 오른쪽 코드에는 외부 클래스(Outer)에 3개의 서로 다른 종류의 내부 클래스가 선언되어 있습니다. 양쪽의 코드를 비교해 보면 내부 클래스의 선언 위치가 변수의 선언 위치와 동일한 것을 볼 수 있습니다.
변수가 선언된 위치에 따라 인스턴스 변수, 클래스 변수(static변수), 지역변수로 나뉘듯이 내부 클래스도 이와 마찬가지로 선언된 위치에 따라 나뉩니다. 그리고, 각 내부 클래스의 선언 위치에 따라 같은 선언 위치의 변수와 동일한 유효 범위(scope)와 접근성(accessibility)을 갖습니다.
4. 내부 클래스의 제어자와 접근성
위 코드에서 인스턴스 클래스(instance class)와 스태틱 클래스(static inner)는 외부 클래스(Outer)의 멤버 변수(인스턴스 변수와 클래스 변수)와 같은 위치에 선언되며, 또한 멤버 변수와 같은 성질을 갖습니다. 따라서 내부 클래스가 외부 클래스의 멤버와 같이 간주되고, 인스턴스 멤버와 static멤버 간의 규칙이 내부 클래스에도 똑같이 적용됩니다.
그리고 내부 클래스도 클래스이기 때문에 abstract나 final과 같은 제어자를 사용할 수 있을 뿐만 아니라, 멤버 변수들처럼 private, protected와 접근제어자도 사용이 가능합니다.
예제 1)
class InnerEx1 {
class InstanceInner {
int iv = 100;
// static int cv = 100; // 에러! static변수를 선언할 수 없다.
final static int CONST = 100; // final static은 상수이므로 허용한다.
}
static class StaticInner {
int iv = 200;
static int cv = 200; // static클래스만 static멤버를 정의할 수 있다.
}
void myMethod() {
class LocalInner {
int iv = 300;
// static int cv = 300; // 에러! static변수를 선언할 수 없다.
final static int CONST = 300; // final static은 상수이므로 허용
}
}
public static void main(String args[]) {
System.out.println(InstanceInner.CONST); //100
System.out.println(StaticInner.cv); //200
}
}
위 코드를 보면 내부 클래스 중에서 스태틱 클래스(static inner)만 static 멤버를 가질 수 있습니다. 드문 경우지만 내부 클래스에 static변수를 선언해야 한다면 스태틱 클래스(static class)로 선언해야 합니다.
다만 final과 static이 동시에 붙은 상수(constant)는 모든 내부 클래스에서 정의가 가능합니다.
예제 2)
class InnerEx2 {
class InstanceInner {}
static class StaticInner {}
// 인스턴스멤버 간에는 서로 직접 접근이 가능하다.
InstanceInner iv = new InstanceInner();
// static 멤버 간에는 서로 직접 접근이 가능하다.
static StaticInner cv = new StaticInner();
static void staticMethod() {
// static멤버는 인스턴스멤버에 직접 접근할 수 없다.
// InstanceInner obj1 = new InstanceInner();
StaticInner obj2 = new StaticInner();
// 굳이 접근하려면 아래와 같이 객체를 생성해야 한다.
// 인스턴스클래스는 외부 클래스를 먼저 생성해야만 생성할 수 있다.
InnerEx2 outer = new InnerEx2();
InstanceInner obj1 = outer.new InstanceInner();
}
void instanceMethod() {
// 인스턴스메서드에서는 인스턴스멤버와 static멤버 모두 접근 가능하다.
InstanceInner obj1 = new InstanceInner();
StaticInner obj2 = new StaticInner();
// 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
// LocalInner lv = new LocalInner();
}
void myMethod() {
class LocalInner {}
LocalInner lv = new LocalInner();
}
}
위 코드를 정리하면 아래와 같습니다.
- 인스턴스 멤버는 같은 클래스에 있는 인스턴스 멤버와 static 멤버 모두 직접 호출이 가능하지만, static 멤버는 인스턴스 멤버를 직접 호출할 수 없습니다.
- 또한 인스턴스 클래스는 외부 클래스의 인스턴스 멤버를 객체 생성 없이 바로 사용할 수 있지만, 스태틱 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 사용할 수 없습니다.
- 마찬가지로 인스턴스 클래스는 스태틱 클래스의 멤버들을 객체 생성 없이 사용할 수 있지만, 스태틱 클래스에서는 인스턴스 클래스의 멤버들을 객체생성 없이 사용할 수 없습니다.
예제 3)
class InnerEx3 {
private int outerIv = 0;
static int outerCv = 0;
class InstanceInner {
int iiv = outerIv; // 외부 클래스의 private멤버도 접근가능하다.
int iiv2 = outerCv;
}
static class StaticInner {
// 스태틱 클래스는 외부 클래스의 인스턴스멤버에 접근할 수 없다.
// int siv = outerIv;
static int scv = outerCv;
}
void myMethod() {
int lv = 0;
final int LV = 0; // JDK1.8부터 final 생략 가능
class LocalInner {
int liv = outerIv;
int liv2 = outerCv;
// 외부 클래스의 지역변수는 final이 붙은 변수(상수)만 접근가능하다.
// int liv3 = lv; // 에러!!!(JDK1.8부터 에러 아님)
int liv4 = LV; // OK
}
}
}
내부 클래스에서 외부 클래스의 변수들에 대한 접근성을 보여 주는 예제입니다. 인스턴스 클래스(instance class)는 외부 클래스(InnerEx3)의 인스턴스 멤버이기 때문에 인스턴스 변수 outerIv와 static 변수 outerCv를 모두 사용할 수 있습니다. 심지어는 outerIv의 접근 제어자가 private일지라도 사용 가능합니다.
스태틱 클래스(static inner)는 외부 클래스(InnerEx3)의 static멤버이기 때문에 외부 클래스의 인스턴스 멤버인 outerIv와 instanceInner를 사용할 수 없습니다. 단지 static 멤버인 iouterCv만을 사용할 수 있습니다.
지역 클래스(LocalInner)는 외부 클래스의 인스턴스 멤버와 static 멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있습니다. 단 final이 붙은 지역 변수만 접근이 가능한데, 그 이유는 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문입니다.
JDK1.8부터 지역 클래스에서 접근하는 지역 변수 앞에 final을 생략할 수 있게 바뀌었습니다. 컴파일러가 자동으로 final을 붙여주며, final이기 때문에 해당 변수의 값이 바뀌는 문장이 있으면 컴파일 에러가 발생합니다.
5. 익명 클래스(anonymous class)
이제 젤 처음 알고 싶었던 익명 클래스에 대해 알아보겠습니다.
익명 클래스는 특이하게도 다른 내부 클래스들과는 달리 이름이 없습니다. 즉, 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한 번만 사용될 수 있고 오직 단 하나의 객체만을 생성할 수 있는 일회용 클래스입니다.
이름이 없기 때문에 생성자도 가질 수 없으며, 조상 클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없습니다.
오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있습니다.
class InnerEx6 {
Object iv = new Object(){ void method(){} }; // 익명클래스
static Object cv = new Object(){ void method(){} }; // 익명클래스
void myMethod() {
Object lv = new Object(){ void method(){} }; // 익명클래스
}
}
위의 예제는 단순히 익명 클래스의 사용 예를 보여 준 것입니다. 이제 이 예제를 컴파일하면 다음과 같이 4개의 클래스 파일이 생성됩니다.
익명 클래스는 이름이 없기 때문에 '외부 클래스명$숫자.class'의 형식으로 클래스 파일명이 결정됩니다.
class Child extends Parent {
}
Parent라는 부모 클래스와, 이를 상속받은 Child라는 자식 클래스가 있다고 가정해보겠습니다.
Parent field = new Child();
이를 위와 같이 사용할 수 있습니다. 이때 자식 클래스가 재사용되지 않고, 오로지 초기값으로만 사용하는 경우 익명 자식 객체를 생성해서 사용하는 것이 좋은 방법입니다.
익명 자식 객체 생성 방법은 다음과 같습니다.
Parent field = new Parent() {
int childField;
void childMethod() {}
@Override
void parentMethod() {
childField = 3;
childMethod();
}
}
즉, child 같은 완전한 클래스를 생성하는 것 대신 처음 지정한 값으로만 사용할 익명 객체를 만드는 것입니다.
- 외부에서는 이 익명 객체의 필드와 메서드에 접근할 수 없다.
- 위의 예제 코드의 경우, childMethod()는 해당 자식 객체 내부에서만 접근이 가능하다.
- parentMethod()는 접근 가능하고 override 한 결과가 반영된다.
- Parent가 class일 수도 Interface일 수도 있다.
매개변수로 부모 클래스를 받아오는 경우도 다음과 같이 사용할 수 있습니다.
class A {
void method1(Parent p) {}
void method2(int param){
int var2 =3;
//method1() 호출
method1(
new Parent(){
@Override
void parentMethod() {}
}
)
}
}
익명 객체의 로컬 변수 사용
- 익명 객체 내부에서는 외부 클래스의 필드나 메서드를 제한 없이 사용할 수 있다.
- 하지만 메서드의 매개 변수(param)나 로컬 변수(var2)를 익명 객체에서 사용할 때 문제가 생길 수 있다.
- 메서드 내에서 생성된 익명 객체는 메서드 실행이 끝나도 힙 메모리에 존재해서 계속 사용할 수 있다.
- 하지만 매개 변수나 로컬 변수는 메서드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 사용할 수 없게 되므로 문제가 발생한다.
- 이러한 문제를 해결하기 위해서 Java 8 이후부터는, 익명 객체 내부에서 메서드 매개 변수나 로컬 변수를 사용할 경우, 자동으로 이 변수들은 final 특성을 가지게 된다.
'공부 STUDY > JAVA' 카테고리의 다른 글
[JAVA] 람다식이란? 람다식 사용 방법에대해 알아보자! (1) | 2023.01.23 |
---|---|
[JAVA] 스트림(Stream)이란 무엇일까? (0) | 2023.01.23 |
[자바/ 자료구조] 컬렉션 프레임워크 공부 전, 자료구조를 간단히 정리해보자! (0) | 2023.01.21 |
[JAVA] Optional이란? | Optional 개념 및 사용법 (0) | 2023.01.17 |
로깅(Logging)이란? (0) | 2023.01.17 |