아이템 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라. - ksw6169/effective-java GitHub Wiki

예제 - 컬렉션이 비어 있으면 null을 반환하는 메소드

다음 코드는 주변에서 심심치 않게 볼 수 있는 메소드다.

private final List<Cheese> cheesesInStock = ...;

/**
 * @return 매장 안의 모든 치즈 목록을 반환한다.
 *          단, 재고가 하나도 없다면 null을 반환한다.
 */
public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}

컬렉션이 비어 있으면 null을 반환하는 메소드의 문제점

컬렉션이나 배열 같은 컨테이너가 비었을 때 null을 반환하는 메소드를 사용하면 클라이언트는 항상 NPE를 방지하기 위한 방어 코드를 넣어줘야 한다.

List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
    System.out.println("좋았어, 바로 그거야.");

클라이언트에서 방어 코드를 빼먹으면 오류가 발생할 수 있다. 실제로 객체가 0개일 가능성이 거의 없는 상황에서는 수년 뒤에야 오류가 발생하기도 한다. 한편, null을 반환하려면 반환하는 쪽에서도 이 상황을 특별히 취급해줘야해서 양쪽으로 코드가 더 복잡해진다.


빈 컨테이너보다는 null을 반환하는 것이 낫다?

때로는 빈 컨테이너를 할당하는 데도 비용이 드니 null을 반환하는 쪽이 낫다는 주장도 있다. 하지만 이는 두 가지 면에서 틀린 주장이다.

  • 이 할당이 성능 저하의 주범이라고 확인되지 않는 한 이 정도의 성능 차이는 신경 쓸 수준이 못된다.
  • 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.

null 대신 빈 컬렉션을 반환하라.

다음은 빈 컬렉션을 반환하는 올바른 예시다.

/**
 * cheesesInStock list의 요소가 있다면 리스트의 내부 필드에 이를 복사하여 반환하고, 
 * 없다면 빈 리스트를 반환한다.
 */
public List<Cheese> getCheeses() {
    return new ArrayList<>(cheesesInStock);
}

희박하지만 빈 컬렉션 할당이 성능을 크게 저하시킬 수도 있다.

가능성은 적지만 빈 컬렉션 할당이 성능을 눈에 띄게 떨어뜨릴 수도 있다. 다행히 이 경우에는 매번 똑같은 빈 '불변' 컬렉션을 반환하게 하면 해결할 수 있다. (단, 이 역시 최적화에 해당하니 꼭 필요할 때만 사용하자.)

  • 리스트가 필요하면 Collections.emptyList() 를 사용하라.
  • 집합이 필요하면 Collections.emptySet() 를 사용하라.
  • 맵이 필요하면 Collections.emptyMap() 을 사용하라.
public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? Collections.emptyList()
        : new ArrayList<>(cheesesInStock);
}

Collections.emptyList()

이 메소드를 사용하면 미리 생성해놓은 빈 불변 리스트(EmptyList 객체)를 반환한다. add() 메소드를 사용할 수 없어 요소 추가가 불가하다.

public static final List EMPTY_LIST = new EmptyList<>();

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}

배열도 null 대신 길이가 0인 배열을 반환하라.

배열도 마찬가지로 절대 null을 반환하지 말고 길이가 0인 배열을 반환하라. 여기서 toArray() 에 전달한 길이 0짜리 배열은 우리가 원하는 반환 타입(Cheese[]) 을 알려주는 역할을 한다.

public Cheese[] getCheeses() {
    return cheesesInStock.toArray(new Cheese[0]);
}

만약 위 방식이 성능을 떨어뜨릴 것 같다면 길이 0짜리 배열을 미리 선언해두고 매번 그 배열을 반환하면 된다. 길이 0인 배열은 모두 불변이기 때문이다.

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheeses() {
    // new Cheese[0]처럼 빈 배열을 매번 새로 할당하지 않는다.
    return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}

길이가 0인 배열은 모두 불변이다.

길이가 0인 배열에 첫 번째 요소를 할당하려고 하면 ArrayIndexOutOfBoundsException이 발생한다. 따라서 길이가 0인 배열은 모두 불변이라 할 수 있다.

String[] arr = new String[0];

// ArrayIndexOutOfBoundsException 발생
arr[0] = "A";

<T> T[] ArrayList.toArray(T[] a) 메소드

주어진 배열 a가 List의 크기보다 크면 a 안에 원소를 담아 반환하고, 그렇지 않으면 T[] 타입의 배열을 새로 만들어 그 안에 원소를 담아 반환한다. 따라서 원소가 하나라도 있다면 Cheese[] 타입의 배열을 새로 생성해 반환하고, 원소가 0개면 EMPTY_CHEESE_ARRAY를 반환한다.

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

toArray에 넘기는 배열을 미리 할당하지 말자.

  • 단순히 성능을 개선할 목적이라면 toArray에 넘기는 배열을 미리 할당하는 건 추천하지 않는다.
  • 오히려 성능이 떨어진다는 연구 결과도 있기 때문이다.
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);

핵심 정리

  • null이 아닌 빈 배열이나 컬렉션을 반환하라.
  • null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은 것도 아니다.

참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️