반응형

ITEM 24 "멤버 클래스는 되도록 static 으로 만들어라"

 

클래스 내부에 변수처럼 중첩 클래스를 둘 수 있다. (nested class)

자신을 감싼 바깥 클래스에서만 쓸 수 있으며, 그게 아니라면 탑 레벨 클래스로 바꾸어야 한다. 이런 중첩 클래스는 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스 4가지로 분류할 수 있다.

 

정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다. 그 이외에는 일반 클래스와 동일하다. 주로 바깥 클래스와 함께 쓰일 때만 사용하는 public 도우미 클래스로 쓰인다. 예를 들어 Calculator 클래스의 Operation.PLUS 와 MINUS 같은 형태로 연산을 참조할 때 정적 클래스로 표현하면 좋다.

 

public class EffectiveJavaTest {
    private String name;
    static class StaticClass {
        void hello() {
            EffectiveJavaTest effectiveJavaTest = new EffectiveJavaTest();
            effectiveJavaTest.name = "gold-egg";
            System.out.println(effectiveJavaTest.name);
        }
    }
    public static void main(String[] args) {
        EffectiveJavaTest.StaticClass staticClass = new StaticClass();
        staticClass.hello();
    }
}

 

비정적 멤버 클래스는 구문 상으로 static 키워드가 빠진 내부 클래스를 말한다. 비정적 멤버 클래스의 인스턴스가 바깥 클래스의 인스턴스와 암묵적으로 연결되어 있어서 비정적 멤버 클래스에서 "클래스명.this" 형태로 바깥 클래스 필드를 참조할 수 있다. 다른 클래스지만, 매우 강한 결합으로 묶일 수가 있다.

또 비정적 멤버 클래스는 바깥 인스턴스 없이 생성할 수 없다는 단점이 있다. 멤버 클래스가 인스턴스화 될 때 관계가 확립되며, 더 이상 변경할 수 없다. 아래 코드와 같이 바깥 클래스에서 new 로 생성하는데 진짜 드물게 바깥 인스턴스의 클래스.new MemberClass(args) 형태로 호출해 수동으로 만들기도 한다고 한다.

 

public class EffectiveJavaTest {
    private final String name;
    public EffectiveJavaTest(String name) {
        this.name = name;
    }
    public String getName() {
        NonStaticClass nonStaticClass = new NonStaticClass("noneStatic-class : ");
        return nonStaticClass.getNameWithOuter();
    }

    private class NonStaticClass {
        private final String noneStaticName;

        public NonStaticClass(String noneStaticName) {
            this.noneStaticName = noneStaticName;
        }

        public String getNameWithOuter() {
            return noneStaticName + EffectiveJavaTest.this.name;
        }
    }
    public static void main(String[] args) {
        EffectiveJavaTest noneStaticClass = new EffectiveJavaTest("g-egg");
        System.out.println(noneStaticClass.getName());
    }
}

 

이런 비정적 멤버 클래스는 어댑터를 정의할 때 쓰인다. 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용할 때이다. Map 인터페이스의 keySet, entrySet, values 메서드가 반환하는 뷰들이 비정적 멤버 클래스를 사용한다고 한다. (열심히 찾아 보았지만 안 나온다... 하하)

 

장단점이 확실해서 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면, 무조건 static 을 붙여 정적 멤버 클래스로 만들라고 권고하고 있다. 앞서 이야기 했듯이, 비정적 멤버 클래스는 숨은 외부 참조가 생겨 이 참조를 저장하면서 시간과 공간이 소비된다. 더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못 할 수도 있다. 언제까지 참조가 살아있는지 체크해야 되는데 눈에 보이지 않기 때문이다.

 

익명 클래스는 말 그대로 이름이 없고 바깥 클래스의 멤버가 아닌 클래스를 말한다. 쓰이는 시점에 선언과 동시에 인스턴스가 만들어지며 어디서든 만들 수 있다. 비정적인 문맥에서만 사용될 때 바깥 클래스의 인스턴스 참조가 가능하다.

 

public class EffectiveJavaTest {
    private String name;
    public void hello() {
        HelloWorld helloBot = new HelloWorld() {
            @Override
            public void hello() {
                System.out.println("g-egg");
            }
        };
        helloBot.hello();
    }

    interface HelloWorld {
        void hello();
    }
}

 

선언한 지점에서만 인스턴스를 만들 수 있고, 여러 인터페이스를 구현할 수 없고 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수도 없다. instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수도 없다. 익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다. 가독성도 떨어진다.

 

자바가 람다를 지원하기 전에는 익명 클래스를 주로 사용했는데 이제는 이런 익명 클래스는 람다를 써도 된다. 이제는 정적 팩터리 메서드를 구현할 때 빼고 쓰지 않는다.

 

지역 클래스는 자주 쓰이지 않는다. 지역 변수를 선언할 수 있는 곳이라면 실질적으로 어디든 선언할 수 있다. 유효 범위도 지역 변수와 같다. 그런데, 지역 클래스를 쓰기 보다는 차라리 합성으로 객체를 변수로 사용하는 것이 좋지 않을까 생각한다.

 

public class EffectiveJavaTest {
    public void hello() {
        class LocalExample {
            private String name;

            public LocalExample(String name) {
                this.name = name;
            }

            public String getName() {
                return name;
            }
        }
        LocalExample localExample = new LocalExample("g-egg");
        System.out.println(localExample.getName());
    }

    public static void main(String[] args) {
        EffectiveJavaTest effectiveJavaTest = new EffectiveJavaTest();
        effectiveJavaTest.hello();
    }
}

 

"바깥 인스턴스를 참조하면, 비정적 클래스로 만들고 그렇지 않으면 정적 클래스로 만들자."

반응형

+ Recent posts