java, 중첩 클래스의 기본 - inner class와 nested class와 사용
중첩 클래스란?
- 클래스 내부에 선언하는 클래스를 중첩 클래스라 한다.
- 장점으로는,
- 두 클래스간 필드에 서로 쉽게 접근할 수 있으며,
- 외부에 불필요한 클래스를 감추며 코드의 복잡성을 줄인다.
- 중첩클래스는 클래스와 인터페이스 모두에서 사용할 수 있다.
- 중첩 클래스는 인스턴스 맴버클래스와 정적 맴버클래스가 있다. 그리고 메서드 안에서 선언되는 로컬 클래스가 있다.
인스턴스 맴버 클래스와 정적 맴버 클래스
- 인스턴스 맴버 클래스는 외부 클래스가 인스턴스로 선언 되었을 때 사용 가능한 클래스이다. inner class라고 부른다.
- 정적 맴버 클래스는 인스턴스와 관계 없이 접근 가능한 클래스이다. (static) nested class라고 부른다.
- 각 클래스는 일반적인 instance와 static의 특징을 가진다.
- 인스턴스 맴버클래스는 외부 클래스가 초기화 된 이후에 접근 가능하다.
- 정적 맴버클래스는 인스턴스 생성과 관계 없이 접근 가능하다.
public class SampleClass {
interface InnerInterface{
class InnerClassInInnerInterface {
}
}
static String staticField;
String instanceField;
// 비교 1. 필드에 접근
// 정적 중첩 클래스는 일반적인 static과 같이 인스턴스 필드에 접근 불가능하다.
static class StaticNestedClass {
void hello(){
System.out.println("hello!");
// System.out.println("instanceField = " + instanceField);
System.out.println("staticField = " + staticField);
}
}
// 인스턴스 중첩 클래스는 인스턴스이므로 인스턴스 필드에 접근 가능하다.
class InstanceInnerClass{
void goodbye(){
System.out.println("good bye~~");
System.out.println("instanceField = " + instanceField);
System.out.println("staticField = " + staticField);
}
}
// 비교2. 중첩 클래스의 초기화
// 인스턴스 메서드는 인스턴스 맴버 클래스와 정적 클래스를 선언하고 초기화함에 있어 제한이 없다.
static void staticMethod(){
// final InstanceInnerClass instanceInnerClass = new InstanceInnerClass(); // 컴파일 에러
final StaticNestedClass staticNestedClass = new StaticNestedClass();
}
// 정적 매서드는 인스턴스 매서드는 선언 및 초기화 불가능하다.
void instanceMethod(){
final InstanceInnerClass instanceInnerClass = new InstanceInnerClass();
final StaticNestedClass staticNestedClass = new StaticNestedClass();
}
}
- 위의 코드를 보면 중첩 클래스는 인스턴스가 사용에 자유롭고 더 좋아 보인다.
- 하지만 정적 클래스를 주로 사용하며 또한 권장된다.
- 외부에서 사용할 경우 정적 클래스가 더 직관적이다. 초기화 할때 더 단순하다.
- 정적 클래스가 인스턴스 클래스에 대비하여 성능 상 낫다. 외부 클래스을 인스턴스로 초기화할 때, 해당 힙에 인스턴스 클래스 정보가 추가적으로 할당된다.
public static void main(String[] args) {
SampleClass.StaticNestedClass staticNestedClass = new SampleClass.StaticNestedClass();
// SampleClass.InstanceInnerClass instanceInnerClass = new SampleClass.InstanceInnerClass();
SampleClass.InstanceInnerClass instanceInnerClass = new SampleClass().new InstanceInnerClass();
}
로컬 클래스
- 로컬클래스는 매서드 내부에서 작성되는 인스턴스 클래스이다. 익명 내부 클래스와 람다가 대표적이다.
- 로컬클래스는 메서드 블럭 내 지역 변수와 인자, 외부 클래스의 모든 필드에 제한 없이 접근 가능하다.
- 다만, 로컬클래스가 접근하는 모든 변수와 인자는 암묵적으로 final 속성을 가진다.
- 이는 할당된 메모리 간 차이를 해결하기 위한 방법이다.
- 로컬클래스는 힙에 저장된다. 클래스 내부에는 지역 변수 등 스택에 할당되는 변수가 존재한다. 메서드가 종료되면 스택에서 사라지고 이로 인하여 힙에서 참조할 데이터가 사라지게 된다.
- 이러한 패러다임의 차이를 해소하기 위하여 자바는 컴파일 시점에서 로컬 클래스가 접근하는 매개 변수와 로컬 변수는 final로 만들고 힙 메모리의 로컬 클래스 내부에 저장한다.
- 람다나 인스턴스 익명 구현 객체 내부의 코드블럭에서는 외부 메서드의 변수를 변경할 수 없었다. 그 이유가 궁금했는데, 이러한 메모리 공간의 차이로부터 발생한 문제였다.
public class SampleLocalClass {
String instanceField = "instanceField";
static String staticField = "staticField";
void sampleMethod(String arg){ // final String arg
String localVar = "localVar"; // final String localVar
class LocalClass{
void accessVars(){
instanceField = "hi!";
staticField = "hi!";
// localVar = "hi!"; // final이라서 수정불가
// arg = "hi!"; // final이라서 수정불가
}
}
}
}
익명 객체
- 익명 객체란 인터페이스나 클래스를 상속하여 구현하는 인스턴스 클래스이다.
- 필드가 되거나 메서드 내 변수가 될 수 있다. 그러니까 맴버 클래스와 로컬 클래스가 될 수 있다.
- 인터페이스의 구현 객체는 인터페이스의 특징을 가진다. 구현 과정에서 새롭게 정의한 필드나 메서드에는 접근할 수 없다.
public abstract class AbstractParent {
abstract void helloAbstract();
}
public interface InterfaceParent {
void helloInterface();
}
public class Main {
final AbstractParent abstractParent = new AbstractParent() { // 필드
@Override
void helloAbstract() {
System.out.println("hello abstract!");
}
};
public static void main(String[] args) {
final InterfaceParent interfaceParent = new InterfaceParent() { // 로컬 변수
@Override
public void helloInterface() {
System.out.println("hello interface!");
}
String newField ="newField";
void newMethod(){
System.out.println("new method");
}
};
interfaceParent.helloInterface();
// 부모의 스펙에 종속된다. 새롭게 선언된 메서드나 필드에는 접근할 수 없다.
// interfaceParent.newField;
// interfaceParent.newMethod();
}
static abstract class SampleClass{
void method1(InterfaceParent interfaceParent) {}
void method2() {
// 익명 객체가 인자로 바로 사용될 수 있다.
method1(new InterfaceParent(){
@Override
public void helloInterface() {
System.out.println("hihi!!");
}
});
}
}
}