해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.



 새 코드에는 무인자 제네릭 자료형을 사용하지 마라.

: 무인자 자료형은 실 형인자 없이 사용되는 제네릭 자료형이다. 예로 List<E>의 무인자 자료형은 List다. 다음과 같이 무인자 컬렉션이 선언 되었다고 가정하자. 해당 stamps 컬렉션 객체는 Stamps 객체만 보관한다.

private final Collection stamps = ....;

이 컬렉션에 다음과 같이 다른 자료형의 객체를 넣어도 문제가 없다. 실인자를 명시하지 않았기 때문이다.

stamps.add(new Comin(..));

하지만 객체를 꺼내고 형변환하는 과정에서 잘못 삽입된 객체를 만나게 되면 실행 도중 오류가 발생한다.

for(Iterator i = stamps.iterator(); i.hasNext() ; ){
   Stamp s = (Stamp)i.next(); //ClassCastException 예외 발생
}

만약 무인자 자료형 대신 실인자를 명시한 자료형을 코딩했다면 위와 같은 다른 자료형의 객체를 넣는 상황이 발생했을 때 실행 도중에 오류를 만나는 게 아니라 컴파일할 때 오류가 발생해 빠르게 오류를 발견할 수 수 있다. 또한 위의 상황의 경우 오류의 원인이 된 stamps.add 구문에서 오류가 발생한 지 컴파일러가 알아 차릴 수 없기 때문에 프로그래머가 직접 오류를 찾아야 하는 번거로움도 있다. 다음은 위 예에서의 무인자 자료형을 실인자 자료형으로 바꾼 코드이다.

private final Collection<Stamp> stamps = ....;

또한 형인자 자료형을 사용하면 컬렉션에서 원소를 꺼낼 때 컴파일러가 알아서 형변환을 해줘 위 예제처럼 형변환 코드를 작성 안해도 된다. 


만약 컬렉션에 들어간 원소들의 자료형을 모를 시 무인자 자료형을 사용하고 싶을 것이다. 이럴 때는 Collection<?>를 사용하면 된다. 무인자 자료형 컬렉션의 경우 컬렉션의 자료형 불변식이 쉽게 깨지지만 비한정적 와일드 카드 인 <?>를 사용하면 안전하다. 그러나 Collection<?> 객체에는 null을 제외한 어떤 원소도 넣을 수 없다는 단점이 있다. 


새로 만든 코드에 무인자 제네릭 자료형을 사용하지 않는게 좋지만 무인자 자료형을 써야할 때가 있다. 하나는 클래스 리터럴이다. 예로 List<String>.class나 List<?>.class는 사용할 수 없다. 두 번째는 instanceof 연산자이다. 제네릭 자료형 정보는 프로그램이 실행될 때 지워지기 때문에 instanceof 연산자에 실인자를 적용할 수 없다. 따라서 제네릭 자료형에 대해서 다음과 같이 사용하는 게 좋다.

if(o instanceof Set){
   Set<?> m = (Set<?>) o;
}



무점검 경고(unchecked warning)을 제거하라.

: 제네릭 클래스를 사용하게 되면 많은 무점검 경고를 만날 수 있다. 무점검 경고를 최대한 제거해야 한다. 무점검 경고는 프로그램 실행 도중이 ClassCastException이 발생할 가능성을 나타낸다. 경고를 제거할 수 없으나 작성한 코드가 형 안전성을 확실히 보장할 때는 @SupressWarnings("unchecked") 어노테이션을 사용해 억제하면 좋다. 하지만 어노테이션은 가능한 한 작은 범위에 적용해야 한다. 변수 선언인, 아주 짧은 메서드, 생성자에 붙여야 한다. 클래스 전체에 절대로 붙이면 안 된다. 그리고 @SupressWarnings("unchecked") 어노테이션을 사용했다면 왜 형 안전성을 위반하지 않는 지에 대해서 주석을 명시하면 좋다.



 배열과 제네릭을 혼용해서 사용하지 마라. 배열을 리스트로 바꿔라.

: 배열은 컴파일 시간에 형 안전성을 보장하지 못한다. 그래서 배열의 자료형과 다른 객체가 저장되더라도 실행이 되고 실행 시간에 해당 사항에 대해서 오류를 낸다. 하지만 제네릭의 경우 컴파일 시간에 형 안전성이 보장된다. 자료형에 관련된 조건들이 컴파일 시간에만 활용되고 프로그램이 실행될 대는 자료형에 관한 정보들이 사라진다. 따라서 제네릭을 사용하면 더 안전한 코드를 작성할 수 있다. 이렇게 배열과 제네릭은 다르기 때문에 혼용해서 사용하기 어렵다. 혼용해서 사용하려면 배열을 리스트로 바꿔야 한다. new List<E>[], new E[] 와 같은 코드를 사용한다면 컬렉션 배열에 E 자료형과 맞지 않은 객체가 저장되 컴파일 시간에 형 안전성을 보장 못한다. 또한, 컴파일 오류가 발생한다. 하지만 new List<E>(list); 와 같은 코드를 사용하면 형 안정성이 보장되게 된다. 따라서 배열과 제네릭을 혼용할 때 배열을 리스트로 바꿔라.


 가능하면 제네릭 자료형을 만들어라.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
 
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
 
    public void push(Object e) {...}
 
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; 
        return result;
    }
}

위 코드는 간단한 Stack의 예이다. 위의 코드를 사용하면 스택에서 꺼낸 객체를 사용하기 전에 형변환을 해야 하는 데 실패할 가능성이 있다. 하지만 이를 제네릭으로 바꾸면 이런 위험은 사라진다. 다음은 Stack 에서 elements 자료형을 제네릭으로 바꾼 예이다.

public class Stack<E> { private E[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) {...} public Object pop() { if (size == 0) throw new EmptyStackException(); E result = elements[--size]; elements[size] = null; return result; } }

elements의 자료형을 E[]로 바꾸었다. 하지만 new E[DEFA...]; 부분에서 에러가 발생한다. 컬렉션을 배열로 만들 수 없기 때문이다. 그래서 "new E[DEFA...];"를 (E[]) new Object[DEFA...];로 바꾸어 에러를 해결할 수 있다. 이런 방법은 형 안전성을 보장할 수 없기 때문에 경고 메시지가 출력된다. 경고가 뜨는 elements 필드는 private이고 클라이언트에 반환되지도 않아서 형안전성이 보장된다. 따라서 경고를 억제하는 어노테이션을 추가할 수 있다. 어노테이션을 적용할 범위를 최소한으로 해야 하기 때문에 Stack 생성자에 어노테이션을 추가한다. 그러면 다음과 같은 코드가 된다.
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    }

"new E[DEFA...]" 오류를 다른 방법으로 해결할 수 있는 방법이 있다. elements의 자료형을 E[]에서 Object[]로 바꾸는 것이다. 그러면 pop 메서드의 "E result = elements[--size];" 구문을 "@SuppressWarnings("unchecked") E result = (E)elements[--size];"로 바꿔야 한다. 이렇게 오류를 해결할 수 있는 방법이 두 가지가 있는 데 후자의 경우는 제네릭 내부 객체에 접근하는 모든 코드에 어노테이션을 붙어야 하고 형변환을 해줘야 되 번거롭다. 따라서 전자의 경우를 많이 사용한다. 추가로 Stack<E>로 구현하면 E에 기본 자료형 int나 double이 들어갈 경우 컴파일 오류가 발생하는 단점이 있다.



 가능하면 제네릭 메서드를 만들어라.

제네릭 자료형과 마찬가지로 제네릭 메서드도 클라이언트가 코드상에서 직접 형변환을 안 해줘도 되 편리하다. 또한 제네릭 정적 팩터리 메서드를 통해 복잡한 객체 생성을 간결하게 할 수 있다. 예로, "Map<String, List<String>> a = newHashMap();" 과 같이 제네릭 정적 팩터리 메서드 newHashMap을 사용해 "new Map<String, List<String>>을 안 하고 간결하게 코딩할 수 있게 해준다.  제네릭 메서드에 관한 예제로 제네릭 싱글턴 팩터리 패턴을 보겠다. 제네릭 싱글턴 팩터리 패턴은 변경이 불가능하지만 많은 자료형에 적용 가능한 객체를 만들 때 유용하다.
public interface UnaryFunction<T> {  
    T apply(T arg);
}


public class GenericSingletonFactory {  
    //제너릭 싱글턴 패턴
    private static UnaryFunction<Object> IDENTIFY_FUNCTION =
            new UnaryFunction<Object>() {
                public Object apply(Object arg) {
                    return arg;
                }
            };
    //IDENTIFY_FUNCTION은인자를 수정없이 반환하므로 T가 무엇이건 UnaryFunction<T>인 것처럼 써도
    //형 안정성이 보장된다
    @SuppressWarnings("unchecked")
    public static <T> UnaryFunction<T> IdentityFunctoin() {
        return (UnaryFunction<T>) IDENTIFY_FUNCTION;
    }

    public static void main(String[] args) {
        String[] string = { "jute", "hemp", "nylon" };
        UnaryFunction<String> sameString = IdentityFunctoin();
        for (String s : string) {
            System.out.println(sameString.apply(s));
        }

        Number[] numbers = { 1, 2.0, 3L };
        UnaryFunction<Number> sameNumber = IdentityFunctoin();
        for(Number n : numbers) {
            System.out.println(sameNumber.apply(n));
        }
    }
}


 API에는 와일드 카드 자료형을 사용하라.

: List<Type1>과 List<Type2> 사이에는 어떤 상위-하위 자료형 관계를 성립할 수 없다. 다음과 같은 Stack public API 클래스가 있다고 가정하자.

public class Stack<E>{ public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); }

Stack 클래스에 대해서 다음의 메서드를 추가하고 싶다고 하자.

public void pushAll(Iterable<E> src){
    for(E e : src){
        push(e);
    }
}

위 메서드를 컴파일할 때는 아무 오류가 없다. 하지만 Stack<Number> 일 경우 pushAll의 매개변수로 Number의 하위 클래스인 Iterable<Integer>이 올 경우 컴파일 오류가 발생한다. Iterable<Stack>과 Iterable<Integer>사이에는 아무런 관계가 없기 때문이다. 이 문제를 해결하기 위해서는 한정적 와일드카드 자료형을 사용해야 한다. pushAll의 매개변수를 Interable<? extends E> src로 하면 오류가 발생하지 않는다. 다른 한정적 와일드 카드 자료형으로 <? super E>도 있다. 만약 인자가 E 생성자라면 <? extends E>를 E 소비자라면 <? super E>를 사용하면 된다. 위 예 pushAll은 E 생성자므로 <? extends E>를 사용했다. 만약 popAll 메서드를 만든다고 하면 popAll은 E 소비자 이므로 <? super E>를 사용하면 된다. 


그렇다면 다음과 같이 형인자와 와일드 카드 중 어떤 것을 사용하면 좋을까?  

public statc <E> void swap(List<E> list,int i, int j);
public static void swap(List<?> list, int i, int j);

만약 public API를 만든다면 와일드 카드를 사용하는 게 좋다. 간결하기 때문이다. 원칙은 형인자가 메서드 선언에 단 한군데 나타난다면 해당 인자를 와일드 카드로 쓰는 것이다. 하지만 와일드카드를 사용할 시 다음의 코드는 컴파일 되지 않는다.

public static void swap(List<?> list, int i, int j){
   list.set(i, list.set(j, list.get(i)));
}

Collection<?>는 null 이외의 어떤 값도 넣을 수 없기 때문이다. 형 안전성이 보장되지 않는 형변환이나 무인자 자료형을 쓰지 않고 이 문제를 해결할 수 있다. 다음 코드와 같이 private 도움 메서드를 이용하는 것이다.

public static void swap(List<?> list, int i, int j){
   swapHelper(list, i, j);
}

private static <E> void swapHelper(List<E> list, int i, int j){
   list.set(i, list.set(j, list.get(i)));
}

API에는 와일드카드 자료형을 사용하는 게 좋다. API의 유연성이 높아지기 때문이다.

+ Recent posts