[item 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라

김동훈·2023년 8월 6일
1

Effective-java

목록 보기
12/14
post-thumbnail

가변인수 메서드와 힙 오염

item 29에서 힙 오염에 대해 잠깐 언급했었다. 힙 오염은 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 발생한다.

가변인수 메서드는 다음처럼 생겼다.

    static <T> T[] toArray(T... args) {
        
        return args;
    }

여기에 제네릭을 적용해보면 힙 오염이 발생할 수도 있다.

public static void method(List<String>... strs) {
        List<Integer> intList = List.of(43);
        Object[] objects = strs;
        objects[0] = intList; // 힙 오염 발생
        String s = strs[0].get(0); // ClassCastException
    }

그럼 가변인수 메서드와 힙 오염이 어떻게 연관되어 있을까?

가변인수 메서드의 파라미터를 보면 List<String>...strs 의 List<string>을 담기 배열이 만들어진다. 앞서, 실체화 불가 타입(제네릭, 매개변수화 타입 등)은 타입 소거로 인해 런타임에 타입 정보가 없다고 배웠다. 그래서 런타임에 생성되는 이 배열은 List[] 이다! 런타임에는 타입 정보가 없으므로 어떤 타입도 우선 들어올 수가 있어진다.
이 메서드를 컴파일하면 비검사 경고를 보내는데 Possible heap pollution from ~ 라고 나온다.

위의 가변인수 메서드 method를 보며 생각해보자.
런타임에는 타입 정보가 없다는 것을 이용하면, 컴파일만 통과하면 된다. Object[]로 strs가변인수 배열에 List<Integer>를 담을 수 있게 된다. 그러면 objects[0] = intList;에서 힙오염이 발생하는데, 좀 더 자세히 말하면 strs가 힙 오염된다고 본다.

@SafeVarargs

이러한 비검사 경고를 없애기 위해서는 자바 7 이전에는 호출하는 곳마다 @SuppressWarnings("unchecked
")를 사용했다. 자바 7에서 @SafeVarargs가 추가된 이후로 번거로움을 없앨 수 있었다.

하지만, 경고를 없애는 행위이기 때문에 그 메서드가 타입 안전함을 보장한 뒤에 사용해야 한다.
2가지 기준으로 타입 안전함을 보장할 수 있다.

  • 가변인수를 담는 제네릭 배열에 아무것도 저장하지 않는다.
  • 이 배열의 참조가 밖으로 노출되지 않는다.
    즉, varargs 매개변수 배열이 순수하게 인수들을 전달하는 일만 하면 안전하다.

varargs매개변수 배열에 아무것도 저장하지 않고 타입 안정성을 깰 수 있는데 확인해보자.

varargs 매개변수 배열의 참조 노출

static <T> T[] toArray(T... args) {
       
        return args;
    }

처음 보여주었던 가변인수 메서드이다. 이 메서드가 반환하는 배열의 타입은 컴파일타임에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 없어 타입을 잘 못 판단 할 수 있다. 이 내용은 아래 예제를 통해 좀 더 말해보겠다. 이 메서드를 보면 args를 반환함으로써 배열의 참조를 노출 하고 있다.

그럼 구체적으로 어떻게 문제가 되는지 보자.

	public static void main(String[] args) {
    	String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // ClassCastException
        
    }
    
    static <T> T[] pickTwo(T a, T b, T c) {
        switch (ThreadLocalRandom.current().nextInt(3)) {
            case 0:
                return toArray(a, b);
            case 1:
                return toArray(a, c);
            case 2:
                return toArray(b, c);
        }
        throw new AssertionError();
    }

이 상황에서 pickTwo메서드의 반환타입은 String[] 일까?

아니다. Object[] 이다. toArray메서드의 반환값을 그대로 반환하고 있으니, toArray를 보자.
컴파일 시점에서 toArray에 넘어오는 인수들은 T a, T b, T c로 이 타입을 String이 아닌 매개변수 타입 T이다. 아직은 컴파일러가 무슨 타입인지 정확히 모른다. 그러니까 toArray메서드 입장에서는 Object[]로 반환할 수 밖에 없다.

그럼 구체적으로 명시하면 올바른 타입(String[]) 으로 반환할까?

String[] array = toArray("좋은", "빠른", "저렴한");
        System.out.println("array = " + array[0]);

이렇게 직접 toArray메서드 정확한 타입으로 넘겨주면 올바르게 동작한다. 컴파일러가 이제는 충분한 정보(String)을 받았기 때문이다.

그래서 pickTwo메서드의 반환 타입은 항상 Object[] 타입 배열이다. 그러니 main메서드에서 Object[] 을 String[]로 형변환하게 되니 ClassCastException이 발생하며 실패한다.

이러한 상황이 바로 varargs매개변수 배열을 올바르게 사용했지만 문제가 발생한 예이다.

profile
董訓은 영어로 mentor

0개의 댓글