item 24) 멤버 클래스는 되도록 static으로 만들라

중첩 클래스

  • 다른 클래스 안에 정의된 클래스

  • 자신을 감싼 바깥 클래스(선언된 클래스)에서만 쓰여야 하며 그 외 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.

  • 종류: 정적 멤버 클래스, 멤버 클래스, 익명 클래스, 지역클래스

정적 멤버 클래스 (static member class)

  • 다른 클래스 안에 선언되고, 선언된 바깥 클래스의 private 멤버에 접근할 수 있다.

  • 다른 정적 멤버와 똑같은 접근 규칙 적용하므로, private으로 선언되면 바깥 클래스에서만 접근할 수 있다.

  • 바깥 클래스와 함께 쓰일 때에만 유용한 public 도우미 클래스로 사용된다.

    • Opration.PLUS, Operation.MINUS와 같이 enum 타입 Operation 클래스가 Calculator 클래스의 public 정적 멤버 클래스가 되면 클라이언트는 원하는 연산을 참조할 수 있다.

  • private 정적 멤버 클래스: 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 사용

멤버 클래스

  • 비정적 멤버 클래스

  • 바깥 클래스의 인스턴스와 암묵적으로 연결되어 인스턴스 메서드에서 정규화된 this를 사용해 바깥 클래스 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있다.

정규화된 this: 클래스명.this 형태로 선언된 클래스의 이름을 명시하는 용법

  • 바깥 클래스의 인스턴스 없이는 생성할 수 없으므로, 개념적으로 중첩 클래스와 바깥 클래스의 인스턴스가 각각 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.

  • 바깥 인스턴스와 멤버 클래스 인스턴스 간 관계는 멤버 클래스가 인스턴스화될 때 확립된다.

    • 멤버 클래스가 인스턴스화되는 경우는 다음 두가지가 있다.

    • 1) 바깥 클래스의 인스턴스 메서드에서 멤버 클래스의 생성자를 호출할 때

    • 2) 직접 바깥인스턴스 클래스에서 수동으로 new를 사용해 멤버 클래스의 인스턴스 만들 때

  • 어댑터 정의할 때 자주 사용

    • ex) Map, Set, List같은 컬렉션 인터페이스의 구현체들은 자신의 컬렉션 뷰를 구현할 때 멤버 클래스를 사용한다.

  • 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 정적 멤버 클래스로 만들 것

    • 바깥 인스턴스로의 숨은 외부 참조를 갖게 되어 메모리 공간을 차지하며 생성 시간이 걸림

    • 가비지 컬렉션이 바깥 인스턴스를 수거하지 못하는 메모리 누수 발생 가능

    • 참조가 눈에 보이지 않으므로 디버깅도 어려움

  • 예제

class A {
    int a = 10;

    public void run() {
        System.out.println("Run A");
        B.run();
        C c = new C();
        c.run();
    }

    // 정적 멤버 클래스
    public static class B {
        public static void run() {
            System.out.println("Run B");
        }
    }

    // 비정적 멤버 클래스
    public class C {
        public void run() {
            // 정규화된 this를 통해 참조 가능하다.
            // 정규화된 this란 클래스명.this 형태로 이름을 명시하는 용법을 말한다.
            System.out.println("Run C: " + A.this.a);
        }
    }
}
public class Example {
    public static void main(String[] args) {
        // A 클래스의 정적 멤버 클래스 B는 외부에서 접근 가능하다.
        A.B.run();
        A a = new A();
        a.run();

        // A 클래스의 멤버 클래스 C는 a애 대한 외부 참조를 가진다.
        A.C c = a.new C();
        c.run();
    }
}
  • 아래는 내부 클래스를 사용 시 참조가 있다는 점을 간과하고 코드를 짜면 발생하는 문제에 대한 예시이다. ButtonState는 Serializable 구현체이지만 Button의 내부 클래스이므로 Button에 대한 참조를 갖게 되고, Button은 Serializable이 아니게 되므로 ButtonState를 직렬화할 때 오류가 발생한다.

public class Button implements View {
    @Override
    public State getCurrentState() {
        return new ButtonState();
    }
    
    @Override
    public void restoreState() {
        // ...
    }
    
    public class ButtonState implements State { // State는 Serializable을 상속받는 인터페이스
        // ...
    }
}

private 정적 멤버 클래스

  • 바깥 클래스가 표현하는 객체의 구성요소를 나타낼 때 사용

  • Map 구현체의 Entry 객체는 내부 메서드(getKey, getValue, setValue)를 가지지만, Map 구현체에서는 사용하지 않는다.

  • Map 객체의 private 정적 멤버 클래스로 Entry 객체를 표현하면 바깥 맵으로의 참조를 갖지 않아 공간, 시간 절약 가능

  • 공개된 클래스의 public이나 protected 멤버 클래스라면 혹시 나중 릴리즈에서 static을 붙이면 하위 호환성이 깨지므로 static 여부가 중요하다.

익명클래스

  • 쓰이는 시점에 선언과 동시에 인스턴스를 생성하며, 코드의 어디서는 만들 수 있다.

  • static하지 않은 상황에서 사용될 때에만 바깥 클래스의 인스턴스 참조 가능

  • static한 상황에서 상수 변수 이외의 정적 멤버는 가질 수 없다.

  • 상수 표현을 위해 초기화된 final 기본타입과 문자열 필드만 가질 수 있다.

  • instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.

  • 여러 인터페이스를 구현할 수 없고, 인터페이스 구현하는 동시에 다른 클래스 상속 불가

  • 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출 불가

  • 익명 클래스가 짧지 않으면 가독성이 떨어진다.

  • 작은 함수 객체나 처리 객체를 만드는 역할을 했었으나, 현재는 람다를 사용한

  • 정적 팩토리 메서드 구현할 때 사용

지역클래스

  • 가장 드물게 사용되는 중첩 클래스

  • 지역변수를 선언할 수 있는 곳에서 선언 가능하며 유효 범위도 지역변수와 같다.

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

  • static하지 않은 상황에서 사용될 때에만 바깥 인스턴스 참조 가능

  • 정적 멤버를 가질 수 없으며 가독성을 위해 짧게 작성해야 한다.

중첩 클래스 사용법

  • 메서드 밖에서 사용해야 하거나 메서드 안에 정의하기 길면 -> 멤버 클래스

  • 멤버 클래스의 인스턴스 각각이 바깥 인스턴스 참조하면 -> 비정적 멤버 클래스, 참조하지 않으면 -> 정적 멤버 클래스

  • 중첩 클래스가 한 메서드에서만 쓰이고 인스턴스 생성하는 지점이 한곳이고 타입으로 사용하기에 적합한 클래스나 인터페이스가 있으면 -> 익명클래스

Last updated