[Effective Java] item 24 : 멤버 클래스는 되도록 static으로 만들라

DEINGVELOP·2022년 12월 28일
0

Effective Java

목록 보기
8/19

Top Level Class

: 중첩되지 않은 클래스. 즉, 다른 클래스나 인터페이스 내부에 선언되지 않은 클래스

Nested Class

: 중첩 클래스. 다른 클래스 안에 정의된 클래스

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

  • 종류

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

Static Member Class

: 정적 멤버 클래스

  • 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다.
public class OuterClass {

  private int x = 10;

  private static class InnerClass {
    void test() {
      OuterClass outerClass = new OuterClass();
      //바깥 클래스에 private 멤버에 접근하는 중
      outerClass.x = 100;
    }
  }
}
  • 다른 정적 멤버와 똑같은 접근 규칙을 적용받는다. 예를 들어, private으로 선언하면 바깥 클래스에서만 접근할 수 있다.

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

    예시
    계산기가 지원하는 연산 종류를 정의하는 열거 타입 Operation = Calculator 클래스의 public 정적 멤버 클래스가 되어야 함

    활용

    • Calculator.Operator.PLUS
    • Calculator.Operator.MINUS

private 정적 멤버 클래스

  • 흔히 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 쓴다.

  • ex) Map (key-value를 매핑시킴) 인스턴스

    • 많은 Map 구현체는 각각의 Key-value 쌍을 표현하는 Entry 객체를 가지고 있음
    • 모든 엔트리가 맵과 연관되어 있지만 엔트리의 메서드들(getKey, getValue, setValue)은 맵을 직접 사용하지는 않음
      ➡ 엔트리를 비정적 멤버 클래스로 표현하는 것 = 낭비. private 정적 멤버 클래스가 가장 알맞음

    즉, 엔트리 선언 시 static을 빠뜨려도 맵은 동작하지만, 모든 엔트리가 바깥 맵으로의 참조를 갖게 되어 공간과 시간을 낭비한다.


(Non-static) Member Class

  • 정적 멤버 클래스와 비정적 멤버 클래스는 구문 상으로는 static의 유무지만, 의미상 차이는 크다.
public class TestClass {

    void x() {
    }
    
     class NestedClass {
        
        void x() {
            TestClass.this.x();
        }
        
    }
}
  • 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결됨

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

public class Main {

     public static void main(String[] args) {
         //관계가 확립되었음.
         new TestClass().new NestedClass();
     }
}

정규화된 this란?
: 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법

  • 어댑터를 정의할 때에 많이 쓰임
    즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 많이 쓰임

    • ex 1 : Map 인터페이스의 구현체들
      보통 keySet, entrySet, values 메서드가 반환하는 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용

    • ex 2 : SetList 같은 다른 컬렉션 인터페이스 구현들도 자신의 반복자를 구현할 때 비정적 멤버 클래스 주로 사용

    public class MySet<E> extends AbstractSet<E> {
    	... // 생략
      
      	@Override
      	public Iterator<E> iterator() {
      		return new MyIterator();
      	}
      
      	private class MyIterator implements Iterator<E> {
          	...
      	}
    }

정적/비정적 멤버 클래스 결정 기준

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

반대로, 멤버 클래스의 인스턴스가 바깥 인스턴스 없이는 생성할 수 없다면 비정적 멤버 클래스가 되어야 한다. (비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없음)

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

이 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는 게 보통이지만, 드물게는 직접 바깥 인스턴스의 클래스.new Member Class(args)를 호출해 수동으로 만들기도 한다.

이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.

멤버 클래스가 공개된 클래스의 public 혹은 protected 멤버라면 정적/비정적 여부는 두 배로 중요해진다. 멤버 클래스 역시 공개 API가 되니 혹시라도 향후 릴리스에서 static을 붙이면 하위 호환성이 깨진다.


익명 클래스

: 이름이 없는 클래스

public class TestClass {
    Integer intInstance = 10;
  
    void doX() {
        new SInterface() {
            @Override
            public void doSometing() {
                //바깥 인스턴스 참조
                System.out.println(intInstance);
            }
        };
    }
}
  
  
interface SInterface {
    void doSometing();
}
  • 바깥 클래스의 멤버가 아니다. 멤버와 달리, 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다.

  • 코드의 어디서든 만들 수 있다.

  • 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다. (정적 문맥에서라도 상수 변수 이외의 정적 멤버는 가질 수 없음)

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

  • 익명 클래스의 또 다른 주 쓰임은 정적 팩토리 메서드를 구현할 때임

익명 클래스의 제약

  • 선언한 지점에서만 인스턴스를 만들 수 있음

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

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

  • 익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다.

  • 익명 클래스는 표현식 중간에 등장하므로 짧지 않으면 (10줄 이하 권장) 가독성이 떨어진다.

Java 8 이전

  • Java가 람다를 지원하기 전에는 즉석에서 작은 함수 객체나 처리 객체를 만드는 데에 익명 클래스를 주로 사용했다. (이제는 람다에게 그 자리를 물려줌)

지역 클래스

: 4가지 중첩 클래스 중 가장 드물게 사용됨

public class TestClass {

    void x() {
        class LocalClass {
            void doPrint() {
                System.out.println("LocalClass");
            }
        }
    }
}
  • 지역변수를 선언할 수 있는 곳이면 실질적으로 어디든 선언 가능

  • 유효 범위도 지역변수와 같음

  • 다른 세 중첩 클래스와의 공통점이 하나씩 있다.

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

정리

중첩 클래스에는 네 가지가 있으며, 각각의 쓰임이 다르다.

  1. 멤버 클래스 : 메서드 밖에서도 사용해야 하
    거나 메서드 안에 정의하기엔 너무 길 경우 멤버 클래스로 만든다.

  2. 정적 멤버 클래스 : 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만든다.

static을 생략하면?

  • 바깥 인스턴스로의 숨은 외부 참조를 갖게 되며, 이 참조를 저장하려면 시간과 공간이 소비된다.
  • 더 심각한 문제는, 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점이다.
  1. 비정적 멤버 클래스 : 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조할 경우 static을 생략하여 비정적 멤버 클래스로 만든다.

  2. 익명 클래스 : 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있을 경우

  3. 지역 클래스 : 그렇지 않을 경우

0개의 댓글