Effective Java #43 NULL 대신 빈 배열이나 컬렉션을 반환하라.
아래와 같이 정의된 메서드는 어렵지 않게 만나볼 수 있다. 아래에서 cheese가 없을 경우에는 배열을 반환하는 것이 아니라 NUll을 반환하고 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public final class Test { private final List<cheese> cheesesInStock = ...; public Cheese[] getCheeses() { if(cheesesInStocks.size() == 0) { return null; } } public static void main(String[] args) { //NULL을 반환할 경우 if(cheese != null && Arrays.asList(cheeses).contains(Cheese.STILTON)) //NULL을 반환하지 않을 경우 if(Arrays.asList(cheeses).contains(Cheese.STILTON)) } } |
사용하는 입장에서 NULL을 반환될 때를 대비한 코드를 만들어야 한다.
이런 메서드는 오류를 쉽게 유발한다. 클라이언트가 NULL처리를 잊어버릴 수 있기 때문이다.
배열 할당 비용이 있으니 NULL을 반환해야 바람직한 것 아니냐는 주장도 있으나 이 주장은 2가지 측면에서 틀렸다.
1. 프로파일링 결과로 해당 메서드가 성능저하의 주범이라는 것이밝혀지지 않는 한 그런 수준까지 성능 걱정을 하는 것은 바람직하지 않다.
2. 길이가 0인 배열은 변경이불가능하므로 아무 제약 없이 재사용할 수 있다는 것이다.
컬렉션에서 배열을 만들어 반환하는 올바른 방법
1 2 3 4 5 6 7 8 | private final List<Cheese> cheeseInStock = ...; private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; /** * @return 재고가 남은 모든 치즈 목록을 배열로 만들어 반환 **/ public Cheese[] getCheese() { return cheeseInStock.toArray(EMPTY_CHEESE_ARRAY); } |
위 코드에서 toArray()에 전달되는 빈 배열 상수는 반환값의 자료형을 명시하는 구실을 한다. 보통 toArray()는 반환되는 원소가 담길 배열을 스스로 할당하는데, 컬렉션이 비어 있는 경우에는 인자로 주어진 빈 배열을 쓴다. 그리고 인자로 주어진 배열이 컬렉션의 모든 원소를 담을 정도로 큰 경우에는 해당 배열을 반환값으로 사용한다. 따라서 위의 숙어대로 하면 빈 배열은 절대로 자동 할당되지 않는다.(이 부분 이해가 안된다면 List에서 toArray() 메서드를 확인하기 바란다.)
마찬가지로 컬렉션을 반환하는 메서드도 빈 컬렉션을 반환해야 할 때마다 동일한 변경 불가능한 빈 컬렉션 객체를 반환하도록 구현할 수 있다. Collections.emptySet emptyList, emptyMap 메서드가 그런 용도로 사용된다.
컬렉션 복사본을 반환하는 올바른 방법
1 2 3 4 5 6 7 | public List<Cheese> getCheeseList() { if (cheeseInSrock.isEmpty()) { return Collections.emptyList(); // 언제나 같은 리스트 반환 } else { return new ArrayList<Cheese>(cheeseInStock); } } |
요약하자면 NULL 대신에 빈 배열이나 빈 컬렉션을 반환하라는 것이다. NULL을 반환하는 것은 C 언어에서 전해진 관습으로, C에서는 배열의 길이가 배열과 따로 반환된다. C에서는 길이 0인 배열을 할당해서 반환하더라도 아무 이득이 없다.
추가 설명
아래 코드가 이해가 안갔다. 컬렉션이 비어 있는 경우에는 인자로 주어진 빈 배열을 쓴다. 아래 return 처리대로 하면 빈 배열은 절대로 자동 할당되지 않는다.
1 2 3 4 5 6 7 8 | private final List<Cheese> cheeseInStock = ...; private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; /** * @return 재고가 남은 모든 치즈 목록을 배열로 만들어 반환 **/ public Cheese[] getCheese() { return cheeseInStock.toArray(EMPTY_CHEESE_ARRAY); } |
무슨 의미인지 확인해 보기 위해 아래와 같이 코드를 작성해보았다. 별도의 Cheese클래스를 구현해보았고, 객체를 생성하여 t1.getCheese() == t1.EMPTY_CHEESE_ARRAY를 구현 비교해 보았더니 true가 나왔다.
이 규칙에서 하는 의도를 다시 적어보자면 null 처리는 대비하지 못한다면 프로그램상의 오류가 있다.
아래 소스와 같이 빈 배열을 전달해 준다면 null처리를 하지 못한 실수를 줄일 수 있고, 똑같은 빈 배열이 할당되기 때문에 자동할당되지 않아 성능저하를 일으키지 않는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import java.util.ArrayList; import java.util.List; class Cheese { String info; public Cheese(String info) { this.info = info; } @Override public String toString() { // TODO Auto-generated method stub return this.info; } } public final class Test { private final List<Cheese> cheesesInStock = new ArrayList<Cheese>(); public static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; public Cheese[] getCheeses() { if(cheesesInStock.size() == 0) { return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY); } return (Cheese[]) cheesesInStock.toArray(); } public static void main(String[] args) { Test t1 = new Test(); System.out.println(t1.getCheeses()); //[Lcom.ktko.init.Cheese;@15db9742 System.out.println(t1.EMPTY_CHEESE_ARRAY); //[Lcom.ktko.init.Cheese;@15db9742 System.out.println(t1.getCheeses() == t1.EMPTY_CHEESE_ARRAY); //true } } |