본문 바로가기
JAVA/Effective Java

Item24. 멤버 클래스는 되도록 static으로 만들라

by 민휘 2023. 6. 29.
중첩 클래스는 자신을 감싼 바깥 클래스에서만 사용된다면 적절하다. 중첩 클래스에는 네 가지가 있으며, 쓰임이 각각 다르다. 멤버 클래스 인스턴스가 바깥 인스턴스를 참조한다면 non static으로, 인스턴스와 상관없이 독립적으로 존재한다면 static으로 만든다. 재사용되지 않는다면 익명 클래스로 만든다. 로컬 클래스는 거의 사용하지 않는다.

 

중첩 클래스 vs 톱 레벨 클래스

중첩 클래스는 자신을 감싼 바깥 클래스에서만 사용되어야 한다. 그 외의 쓰임새가 있다면 톱 레벨 클래스를 사용한다.

중첩 클래스는 외부 클래스를 통해서만 접근 가능하므로, 논리적인 의미로 클래스를 그룹핑해야할 때 사용할 수 있다.

public class OuterClass {
    
    public static class NestedClass {
        // 중첩 클래스
    }

}
public class OuterClass {
    TopLevelClass class;
}

public class TopLevelClass {
	// 외부로 분리한 톱 레벨 클래스
}

 

static 내부 클래스 vs non static 내부 클래스

 

둘의 차이점은 바깥 클래스 인스턴스의 참조 가능 여부이다. static은 인스턴스 참조가 불가능하고, non static은 암묵적으로 참조가 가능하다. 바깥 인스턴스 참조가 필요하지 않다면 static으로 쓰는 것이 좋다.

 

static 내부 클래스

바깥 인스턴스와 독립적으로 존재할 수 있으며, 바깥 클래스와 함께 쓰일 때만 유용한 클래스라면 static 내부 클래스로 정의한다. 예를 들어 Operator 열거 타입을 Calculator 클래스 내부에서 정적 내부 클래스로 정의할 수 있다. inner enum은 static으로 정의되는 클래스이므로 static을 생략할 수 있다.

package org.example.item24.inner;

public class Calculator {

    enum Operation {
        PLUS("+") {
            public double apply(double x, double y) {
                return x + y;
            }
        },
        MINUS("-") {
            public double apply(double x, double y) {
                return x - y;
            }
        },
        TIMES("*") {
            public double apply(double x, double y) {
                return x * y;
            }
        },
        DIVIDE("/") {
            public double apply(double x, double y) {
                return x / y;
            }
        };

        private final String symbol;

        Operation(String symbol) {
            this.symbol = symbol;
        }

        public abstract double apply(double x, double y);

    }

    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Calculator.Operation op : Calculator.Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }

}

 

non static 내부 클래스

비정적 내부 클래스의 인스턴스는 바깥 클래스의 인스턴스 참조를 암묵적으로 가진다. 코드 상으로 참조하는 부분이 없더라도 바이트 코드로 찍어보면 참조가 존재한다.

 

비정적 내부 클래스에서는 정규화된 this로 바깥 인스턴스를 참조한다.

public class OutterClass {

    private int number = 10;

    private class InnerClass {
        void doSomething() {
//            정규화된 this
            int innerNumber = OutterClass.this.number;
            System.out.println(innerNumber);
        }
    }

}

 

비정적 내부 클래스 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더 이상 변경될 수 없다. 비정적 멤버 클래스는 바깥 인스턴스 없이 생성될 수 없다.

 

보통 바깥 클래스의 인스턴스 메서드에서 내부 클래스의 생성자를 호출하여 관계가 만들어진다.

public class OutterClass {

    private int number = 10;

    void printNumber() {
        InnerClass innerClass = new InnerClass();
        innerClass.doSomething();
    }

    private class InnerClass {
        void doSomething() {
            System.out.println(number);
        }
    }

}

 

드물게는 바깥 클래스.new 클래스()로 만들기도 한다.

public class OutterClass {

    private int number = 10;

    private class InnerClass {
        void doSomething() {
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        InnerClass innerClass = new OutterClass().new InnerClass();
        innerClass.doSomething();
    }

}

 

비정적 내부 클래스는 참조를 저장하기 위해 시간과 공간을 더 소비한다. 그리고 암묵적 참조를 사용하므로 GC가 바깥 인스턴스를 수거하지 못하기도 한다. 그러므로 바깥 참조가 필요하지 않다면 static 내부 클래스를 사용하는 것이 더 유리하다.

 

 

어댑터 패턴

비정적 내부 클래스를 자주 사용하는 패턴으로 어댑터가 있다.

어댑터는 어떤 클래스의 인스턴스를 감싸서 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다. 클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드가 해당 인터페이스로 보이도록 연결해서 클라이언트가 인터페이스를 통해 기존 코드를 사용할 수 있게 하는 방법이다.

 

Set과 List 같은 컬렉션 인터페이스의 구현은 클라이언트가 자신의 반복자를 Interator 타입으로 사용하도록 제공한다. 자신의 반복자를 구현하고 나서 Iterator처럼 보이도록 Iterator 타입으로 반환한다. (위의 그림과 구현 모습은 다르지만 뷰를 제공한다는 점에서 어댑터 역할을 수행한다고 볼 수 있다)

public class MySet<E> extends AbstractSet<E> {
    @Override
    public Iterator<E> iterator() {
        return new MyIterator();
    }

    @Override
    public int size() {
        return 0;
    }

    private class MyIterator implements Iterator<E> {

        @Override
        public boolean hasNext() {
            return false;
        }

        @Override
        public E next() {
            return null;
        }

    }
}

 

 

익명 내부 클래스

 

클래스 정의와 인스턴스화를 동시에 수행하는 방법이다. 익명 클래스는 주로 인터페이스나 추상 클래스의 구현체로 구현되며, 재사용성이 없다고 판단되는 경우 클래스 정의를 줄이기 위해 사용한다.

public class AnonymousInnerClassExample {
    public static void main(String[] args) {
        // 익명 내부 클래스로 Runnable 구현
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello, World!");
            }
        };
        
        // 익명 내부 클래스로 생성한 객체를 스레드로 실행
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

 

정적 팩터리 메소드가 반환하는 인스턴스는 클래스 내에서 재사용되지 않으므로 익명 클래스로 선언하기 적절하다.

public class IntArrays {
    static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);

        return new AbstractList<>() {
            @Override public Integer get(int i) {
                return a[i];  // Autoboxing (Item 6)
            }

            @Override public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val;     // Auto-unboxing
                return oldVal;  // Autoboxing
            }

            @Override public int size() {
                return a.length;
            }
        };
    }

}

 

익명 내부 클래스는 제약이 많다. 하지만 일회용 클래스를 선언하는데 있어 이보다 더 좋은 문법은 없다.

 

이름이 없으므로 참조 필드도 없다. 참조 필드가 없으므로 클래스의 필드가 아니다. 그래서 instanceof 처럼 클래스 이름이 필요한 작업은 수행할 수 없다.

non static 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다. static 문맥에서 사용될 때도 final이나 문자열처럼 상수 변수 외의 정적 멤버는 가질 수 없다.

익명 내부 클래스는 표현식 중간에 등장하므로 내용이 짧지 않다면 가독성이 떨어진다. 자바8부터 람다를 지원면서부터 함수 객체나 처리 객체를 더 간결한 문법으로 구현할 수 있게 되었다.

 

로컬 내부 클래스

 

안 쓴다.

지역 변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있고, 유효 범위도 지역 변수와 같다.

public class MyClass {

    private int number = 10;
    
    void doSomething() {
        class LocalClass {
            private void printNumber() {
                System.out.println(number);
            }
        }
        LocalClass localClass = new LocalClass();
        localClass.printNumber();
    }

}