[이펙티브 자바] 아이템 23-28

diveintoo·2022년 5월 13일
0

이펙티브 자바

목록 보기
4/6

아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

태그 달린 클래스

  • 두 가지 이상의 의미를 표현할 수 있으며, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스

태그 달린 클래스의 단점

  • 열거 타입 선언, 태그 필드 등 쓸데없는 코드가 많다.
  • 여러 구현이 한 클래스에 혼합돼 있어서 가독성이 나쁘다.
  • 다른 의미를 위한 코드도 항상 함께하니 메모리도 많이 사용한다.
  • 또 다른 의미를 추가하려면 코드를 수정해야 한다.

<태그 달린 클래스는 장황하고 오류를 내기 쉽고 비효율적이다.>

태그 달린 클래스를 클래스 계층구조로 바꾸는 방법

1. 계층구조의 루트가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다.

2. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다.

3. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.

// 1번
abstract class Figure {
    abstract double area();
}

// 3번
class Circle extends Figure {
    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

// 3번
class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width;
    }
}

클래스 계층구조의 장점

  • 간결하고 명확하며, 쓸데없는 코드가 사라졌다.
  • 루트 클래스의 코드를 건드리지 않고도 다른 프로그래머들이 독립적으로 계층구조를 확장하고 함께 사용할 수 있다.
  • 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다.

아이템 24. 멤버 클래스는 되도록 static으로 만들라

중첩 클래스

  • 다른 클래스 안에 정의된 클래스
  • 중첩 클래스는 자신을 감싼 바깥쪽 클래스에서만 사용되어야 한다.
  • 중첩 클래스의 종류는 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스가 있다.

정적 멤버 클래스

  • 정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근 가능하다.
    • 그 외에는 일반 클래스와 똑같다.
  • 정적 멤버 클래스와 비정적 멤버 클래스는 코드 상에서 static의 유무만 보일 수 있으나 의미상의 차이는 더 크다.

비정적 멤버 클래스

  • 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.
    • 바깥 인스턴스로의 숨은 외부 참조를 갖게 되는데, 이 참조를 저장하려면 시간과 공간이 소비된다.
    • 더 심각한 문제로 가비지 컬렉션이 바깥 클래스의 인스턴스를 정리하지 못하는 메모리 누수가 발생할 수 있다.
  • 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 통해 바깥 인스턴스의 메서드를 호출한다거나 바깥 인스턴스를 참조할 수 있다.
    • 정규화된 this란 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법을 말한다.

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

익명 클래스

  • 익명 클래스는 이름이 없으며 바깥 클래스의 멤버도 아니다.
  • 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다.
  • 코드의 어디에서든 만들 수 있다.

제약

  • 비정적 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다.
  • 정적 문맥에서라도 상수 변수 이외에 정적 멤버는 가질 수 없다.
  • instanceof 연산자를 통한 타입 검사가 불가능하다.
  • 여러 인터페이스를 구현할 수 없고, 인터페이스 구현과 동시에 다른 클래스를 상속할 수도 없다.

지역 클래스

  • 지역 클래스는 지역변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있고, 유효 범위도 지역변수와 같다.
  • 다른 중첩 클래스와 마찬가지로 이름이 있으며 반복해서 사용할 수 있다.
  • 비정적 문맥에서만 바깥 인스턴스를 참조할 수 있으며, 정적 멤버는 가질 수 없고 가독성을 위해 짧게 작성되어야 한다.

아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라

소스 파일 하나에 톱레벨 클래스를 여러 개 선언할 때의 문제점

  • 소스 파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 불평하지 않는다.
  • 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느 것을 사용할지는 어느 소스 파일을 먼저 컴파일하냐에 따라 달라진다.

<컴파일러에 어느 소스 파일을 먼저 건네느냐에 따라 동작이 달라진다.>

해결책

톱레벨 클래스들을 서로 다른 소스 파일로 분리한다.

굳이 여러 톱레벨 클래스를 한 파일에 담고 싶다면 정적 멤버 클래스를 사용하는 방법을 고민해볼 수 있다.

  • 다른 클래스에 딸린 부차적인 클래스라면 정적 멤버 클래스로 만드는 쪽이 일반적으로 더 나을 것이다.
  • 읽기 좋고, private으로 선언하면 접근 범위도 최소로 관리할 수 있기 때문이다.
public class Main {
  public static void main(String[] args) {
    System.out.println(Utensi.NAME + Dessert.NAME); 
  } 
  
  private static class Utensil { 
    static final String NAME = "pan"; 
  } 
  
  private static class Dessert {
    static final String NAME = "cake"; 
  } 
}

아이템 26. 로 타입은 사용하지 말라

제너릭 용어 정리

제네릭 클래스, 제네릭 인터페이스

  • 클래스와 인터페이스 선언에 타입 매개변수가 쓰인 경우
  • ex) List<E>
  • 통틀어서 제너릭 타입이라고 한다.

매개변수화 타입

  • ex) List<String>
  • 여기에서 String이 타입 매개변수 E에 해당하는 실제 타입 매개변수이다.

로 타입

  • 제너릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때
  • ex) List<E>의 로 타입은 List이다.

로 타입을 쓰면?

// Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;
  • 만약 도장을 담는 컬렉션에 동전을 넣어도 컴파일되고 실행된다.
  • 한참 뒤 런타임에야 오류가 발생할 수 있다.
    • add한 Coin 객체를 꺼내서 Stamp 변수에 할당하면 ClassCastException이 발생한다.

로 타입은 제너릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.

제너릭을 사용하자.

제너릭을 활용하면 위 예제의 'Stamp 인스턴스만 취급한다.'는 정보가 주석이 아닌 타입 선언 자체에 녹아든다.

private final Collection<Stamp> stamps = ...;
  • 컴파일러는 stamps에 Stamp의 인스턴스만 넣어야함을 인지하게 된다.
  • 따라서 타입 안정성을 확보할 수 있다.

List<Object>는 사용해도 괜찮다.

List<Object>는 모든 타입을 허용한다는 매개변수화 타입이다.

  • 매개 변수로 List를 받는 메서드에 List<String>을 넘길 수 있다.
  • 하지만 List<Object>를 받는 메서드에는 넘길 수 없다.
    • 제너릭의 하위 타입 규칙 때문이다.

List<String>은 로 타입인 List의 하위 타입이지만, List<Object>의 하위 타입은 아니다.

원소의 타입을 신경쓰고싶지 않다면?

비한정적 와일드카드 타입을 사용하자!

  • 제너릭 타입인 Set<E>의 비한정적 와일드카드 타입은 Set<?>이다.
  • 로 타입 컬렉션에는 아무 원소나 넣을 수 있어서 타입 불변식을 훼손하기 쉽다.
  • 하지만 Collection<?>에는 null 외에는 어떤 원소도 넣을 수 없다.

아이템 27. 비검사 경고를 제거하라

제네릭을 사용하기 시작하면 수많은 컴파일러 경고들을 마주치게 된다.

할 수 있는 한 모든 비검사 경고를 제거하라.

대부분의 비검사 경고는 쉽게 제거할 수 있다.

예시: 컴파일러 오류가 난다.

Set<Car> cars = new HashSet();

Venery.java:4: warning: [unchecked] unchecked conversion
                Set<Car> cars = new HashSet();
                                ^
  required: Set<Car>
  found:    HashSet

-> Set<Car> cars = new HashSet<>();

컴파일러가 알려준 대로 수정하면 경고가 사라진다.

경고를 제거할 수 없지만 타입 안전하다고 확신할 수 있으면, @SuppressWarnings(“unchecked”)를 달아 경고를 숨기자.

  • @SuppressWarnings은 지역변수 선언부터 클래스 전체까지 선언할 수 있지만, 가능한 가장 좁은 범위에 적용하자.
    • 절대로 클래스 전체에 적용해서는 안 된다.
  • 한 줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 애너테이션은 지역변수 선언 쪽으로 옮기자.

예시: 지역변수를 추가해 @SuppressWarnings의 범위를 좁힌다.

public <T> T[] toArray(T[] a) {
   if (a.length < size) {
      // 수정 전
      return (T[]) Arrays.copyOf(elements, size, a.getClass());
      
      // 수정 후
      // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환입니다 
      @SuppressWarnings("unchecked") T[] result = 
         (T[]) Arrays.copyOf(elements, size, a.getClass());
      return result; 
   } 
   System.arraycopy(elements, 0, a, 0, size);
   if (a.length > size)
      a[size] = null;
   return a; 
}

애너테이션은 선언에만 달 수 있기 때문에 return 문에는 @SuppressWarning을 다는 게 불가능하다.

@SuppressWarnings(“unchecked”) 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.


아이템 28. 배열보다는 리스트를 사용하라

배열과 제너릭 타입의 중요한 차이

1. 공변

배열은 공변이다.

  • Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.

반면 제네릭은 불공변이다.

  • List<Object>List<String>의 하위 타입도 아니고 상위 타입도 되지 않는다.

문제는 배열에 있다!

아래의 코드처럼 배열에서는 오류를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일 시점에 오류를 바로 확인할 수 있다.

예제: 배열 - 런타임에 실패한다.

Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException 발생

예제: 제너릭 - 컴파일되지 않는다.

List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이므로 컴파일부터 막힌다.
ol.add("타입이 달라 넣을 수 없다.");

2. 실체화

배열은 실체화된다.

  • 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
  • 그래서 위의 예제에서 ArrayStoreException가 발생한 것이다.

제너릭은 실체화되지 않는다.

  • 타입 정보가 런타임에는 소거된다.
  • 원소 타입을 컴파일타임에만 검사하며 런타임 시점에는 알 수조차 없다.
  • 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있도록 해준다.

이러한 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.

  • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
    • new List<E>[], new List<String>[], new E[] // 오류 발생~!
    • 타입 안전성이 보장되지 않기 때문에 제너릭 배열 생성을 막는 것이다.

배열과 제너릭을 섞어 쓰다가 오류를 만난다면

배열을 리스트로 대체하자!

배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신에 컬렉션인 List<E>를 사용하면 해결된다.

예제: 형변환 오류가 날 수 있다.

public class Chooser {
    private final Object[] choiceArray;
    
    public Chooser(Collection choices) {
        this.choiceArray = choices.toArray();
    }
    
    // 이 메서드를 호출할 때마다 반환된 Object를 형변환해야 한다.
    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

예제: 배열 대신 리스트를 사용한다.

class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

런타임에 형변환 오류를 발생시키지 않는다.

0개의 댓글