본문 바로가기
Language/Effective java

제네릭

by y.j 2022. 6. 26.
728x90

제네릭과 가변인수를 함께 쓸 때는 신중하라.

제네릭과 가변인수는 생각보다 잘 어우러지지 않는다. 아래코드를 매개변수화 타입을 적용한다면 어떻게 될까? 제네릭은 실체화 불가 타입이기 때문에 타입 관련 정보를 적게 담고 있어 컴파일 타임에 에러를 못 낼 수 있다. 이처럼 타입 안전성이 깨지니 제네릭 varargs배열 매개변수에 저장하는 것은 안전하지 않다.

static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    objects[0] = intList;                   // 힘오염 발생
    String s = stringLists[0].get(0);       // ClassCastException
}

 

제네릭 가변인수 만들어보기

아래 코드는 메서드에 인수를 넘기는 컴파일 타입에 결정되는데 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다. 

static <T> T[] toArray(T... args) {
    return args;
}
public static void main(String[] args) {
    String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}

위 코드는 에러가 난다. 반환 타입은 Object[]이지만, T...는 컴파일러에서 자동으로 String[]으로 형변환하기 때문이다. Object[]는 String[]의 하위타입이 아니기 때문에 에러가 난다.

 

안정적으로 제네릭 가변인수 사용하는 방법

@SageVarargs로 된 varargs 메서드에 쓰면 컴파일러 경고를 없앨 수 있다. 하지만 기본적으로 안전하지 않기 때문에 쓰는 것이므로 varargs를 안 쓰는 것이 좋다.

@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    List<T> result = new ArrayList<>();
    for(List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

점검 사항

  • vargargs 매개변수 배열에 아무것도 저장하지 않는다.
  • 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.

 

이 배열 내용의 일부 함수를 호출만 하는 (vargargs가 아닌) 일반 메서드에 넘기는 것이 안전하다. 

static <T> List<T> flatten(List<List<? extends T>> lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

 

타입 안전 이종 컨테이너를 고려하라

Map<K,V>는 키와 값의 타입을 뜻하는 2개만 필요한 식이다. 데이터 베이스의 경우에는 행과 임의의 개수의 열을 가질 수 있는데 모두 열을 타입 안전하게 이용 할 수 있는 방법이 필요하다. 방법은 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 시스템이 값의 타입이 키와 같음을 보장해줄 것이다. 이것은 타입 안전 이종 컨네이터 패턴이라 한다. 

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();
    
    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

 

Map<Class<?>, Object>

Class<?>는 비한정적 와일드 카드라서 아무것도 넣을 수 없는 것처럼 보이지만 실제로는 키가 비한정적 와일드 카드이다. 따라서 중첩된 와일드카드 타입이므로 모든 키가 서로 다른 매개변수화 타입 일 수 있다는 뜻이다.

 

getFavorite 메서드

getFavorite은 값이 Object이므로 타입 캐스팅을 해주어야 한다. cast는 함수는 동적으로 형변환 해준다.  

 

Favorties 클래스의 제약

첫 번째, putFavorite함수를 좀 더 안정적으로 바꿔야 한다. instance를 Class객체가 아닌 로타입으로 넘기면 안정성이 깨진다. put할때도 형변환을 시켜주자.

public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

 

java.util.Collections에는 checkedSet, checkedList, checkedMap같은 메서드가 있는데, 바로 이 방식을 적용한 컬렉션 래퍼들이다. 비슷한 방식으로 type에 대한 체크를 하고 있다.

E typeCheck(Object o) {
    if (o != null && !type.isInstance(o))
        throw new ClassCastException(badElementMsg(o));
    return (E) o;
}

 

두 번째, 제약은 실체화 불가 타입에는 사용 할 수 없다. List<String>용 Class 객체를 얻을 수 없어 허용되지 않는다. 

 

한정하는 방법

static Annotation getAnnotation(AnnotationElement element,
                                String annotationTypeName) {
    Class<?> annotationType = null;
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (Exception ex) {
        throw new IllegalArgumentException(ex);
    }
    return element.getAnnotation(
            annotationType.asSubclass(Annotation.class)
    );
}

단순히 <? extends T>로 형변환을 할 수도 있겠지만 비검사이므로 컴파일하는 도중 경고가 나올것이다. 하지만 asSubClass를 사용한다면 오류나 경고 없이 컴파일이 된다.

@SuppressWarnings("unchecked")
public <U> Class<? extends U> asSubclass(Class<U> clazz) {
    if (clazz.isAssignableFrom(this))
        return (Class<? extends U>) this;
    else
        throw new ClassCastException(this.toString());
}

 

728x90

'Language > Effective java' 카테고리의 다른 글

열거 타입과 애너테이션  (0) 2022.07.18
열거 타입과 애너테이션  (0) 2022.07.13
제네릭  (0) 2022.06.19
제네릭  (0) 2022.06.18
클래스와 인터페이스  (0) 2022.06.17

댓글