Study/이펙티브 자바 / / 2023. 10. 26. 20:53

[Effective Java 3E] 멤버 클래스는 되도록 static으로 만들라

💥 개요

중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다.

중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.

 

🔍 중첩 클래스의 종류

  • 정적 멤버 클래스
  • 멤버 클래스(비정적)
  • 익명 클래스
  • 지역 클래스

 

정적 멤버 클래스

다른 클래스 안에 선언, 바깥 클래스의 private 멤버에도 접근이 가능하다.

정적 멤버 클래스는 정적 멤버와 똑같은 접근 규칙을 적용 받는다.(private이면 바깥 클래스에서만 사용 가능)

흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다.

public class Calculator {
	//static을 명시하지 않아도 nested enum은 static으로 선언됨
    public enum Operation {
        PLUS, MINUS;
    }
}

class Main {

    public static void main(String[] args) {
        Calculator.Operation operation = Calculator.Operation.PLUS; // 바로 선언이 가능
    }
}

 

비정적 멤버 클래스

비정적 멤버 클래스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.

그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 통해서(바깥클래스.this) 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.

바깥 클래스의 인스턴스를 참조할 수 있기 때문에 메모리 누수가 발생할 가능성도 있다. 이는 쉽게 찾을 수 없어 큰 문제가 발생한다.

그리고 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스를 생성 후 해당 참조를 이용해 생성해줘야 한다.

메모리를 추가적으로 사용, 생성시간도 오래 걸린다.

따라서 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적이면 정적 멤버 클래스로 만들어야 한다.

비정적 멤버 클래스는 어댑터( 어떤 구현체를 특정 인터페이스에 맞게 감싸주는 중간객체)를 정의할 때 자주 쓰인다.

클래스의 인스턴스를 감싸 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용

예컨대 Map 인터페이스의 구현체는 보통 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다. 

public class OuterClass {
    private String outerField = "Outer Field";

    // 비정적 멤버 클래스
    public class NonStaticInnerClass {
        public void display() {
            System.out.println("Non-static inner class: " + OuterClass.this.outerField);
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        NonStaticInnerClass inner = outer.new NonStaticInnerClass();
        inner.display();
    }
}

 

익명 클래스

이름이 없다. 그리고 바깥 클래스의 멤버도 아니다.

멤버와 달리 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다. 코드의 어디서든 만들 수 있다.

오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다. 정적 문맥에서도 상수변수 이외의 정적 멤버는 가질 수 없고(초기화된 final 기본타입과 문자열 필드만) 응용하는데 제약이 많다.

선언한 지점에서 인스턴스를 만들 수 있는데 instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.

또 하나의 인터페이스만 상속이 가능하고 동시에 다른 클래스의 상속도 불가능하다. 

익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다. 익명 클래스 표현식이 존재해서 짧지 않으면 가독성이 떨어진다.(10줄 이내)

람다가 나오기전에는 즉석에서 작은 함수 객체나 처리 객체를 만드는데 주로 썼다. 또 다른 주 쓰임은 정적 팩터리 메서드를 구현할 때다.

public class AnonymousClassExample {
    interface Greeting {
        void sayHello();
    }

    public static void main(String[] args) {
        // 익명 클래스를 사용하여 Greeting 인터페이스의 구현체를 생성
        Greeting greeting = new Greeting() {
            @Override
            public void sayHello() {
                System.out.println("Hello from anonymous class!");
            }
        };

        greeting.sayHello();
    }
}

 

지역 클래스

네 가지 중첩 클래스 중 가장 드물게 사용한다.

지역변수를 선언할 수 있는 어디든 선언이 가능하고 유효 범위도 지역변수와 같다.

멤버 클래스처럼 이름이 있고 반복해서 사용이 가능하다.

익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며, 정적 멤버는 가질 수 없으며 가독성을 위해 짧게 작성해야한다.

public class LocalClassExample {

    private String outerField = "Outer Field";

    public void demonstrateLocalClass() {
        String localVariable = "Local Variable";

        // 지역 클래스
        class LocalClass {
            public void display() {
                System.out.println("Outer field: " + outerField);
                System.out.println("Local variable: " + localVariable);
            }
        }

        LocalClass localClass = new LocalClass();
        localClass.display();
    }

    public static void main(String[] args) {
        LocalClassExample example = new LocalClassExample();
        example.demonstrateLocalClass();
    }
}

 

 

중첩 클래스에는 네 가지가 있으며, 각각의 쓰임새가 다르다. 메서드 밖에서도 사용해야 하거나 메서드 안에 정의 하기엔 너무 길다면 멤버 클래스로 만든다. 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들자. 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.