Skip to content

item 26 junghyunlyoo

JungHyunLyoo edited this page Aug 1, 2020 · 2 revisions

제네릭 관련 용어 정리

링크

클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.

예를 들어, List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다.

이 List 인터페이스의 완전한 이름은 List이다. 하지만 짧게 List 라고도 자주 쓴다.

제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입(generic type)이라 한다.

매개변수화 타입

각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.

먼저 클래스나 인터페이스 이름이 나오고 이어서 꺾쇠괄호 안에 실제 타입 매개변수들을 나열한다.

예를 들어 List은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.

여기서 String이 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수다.

로 타입

제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의된다.

로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.

예컨대 List의 로 타입은 List다.

로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작한다.

이는 제네릭이 도래하기 전의 코드와 호환되도록 하기 위한 궁여지책이다.

로 타입의 단점 :: ClassCastException

제네릭을 지원하기 전에는 컬렉션을 다음과 같이 선언했다.

자바 9에서도 여전히 동작하지만 좋은 예라고 볼 순 없다.

private final Collection stamps = ...;

이 코드를 사용하면 실수로 도장(stamp) 대신 동전(Coin)을 넣어도 아무 오류없이 컴파일되고 실행된다.

(컴파일러가 모호한 경고 메세지를 보여주긴 할 것이다)

stamps.add(new Coin());

이 동전을 컬렉션에서 다시 꺼내기 전에는 오류를 알 수 없다.

for (Iterator i = stamps.iterator(); i.hasNext(); ) {
    Stamp stamp = (Stamp) i.next(); //ClassCastException
}

오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋다.

이 예에서는 오류가 발생하고 한참 뒤인 런타임에야 알아챌 수 있다.

이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 상당히 떨어져 있을 가능성이 커진다.

ClassCastException이 발생하면 stamps에 동전을 넣은 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있다.


제네릭을 활용하면 이 오류를 방지할 수 있다.

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

이렇게 선언하면 컴파일러는 stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다.

따라서 아무런 경고 없이 컴파일된다면 의도대로 동작할 것임을 보장한다.

(물론 컴파일러 경고를 숨기지 않았어야 한다. item 27)

stamps에 Stamp 이외의 인스턴스를 넣으려 하면 컴파일 오류가 발생하며 무엇이 잘못됐는지를 정확히 알려준다.


컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장한다.

Stamp용 컬렉션에 Coin을 넣는다는 예가 억지스러워 보이겠지만 현업에서도 종종 일어나는 일이다.

(BigDecimal용 컬렉션에 BigInteger를 넣는 경우 등)

로 타입의 존재 이유 :: 호환성

로타입을 쓰는 걸 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안 된다.

로 타입을 쓰면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다.

그렇다면 로 타입은 왜 존재하는 걸까? 바로 호환성 때문이다.

자바가 제네릭을 받아들이기까지 거의 10년이 걸렸기 때문에 제네릭 없이 짠 코드가 이미 널리 사용되고 있다.

그래서 기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야만 했다.

로 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 (혹은 그 반대도) 정상적으로 동작해야만 했던 것이다.

이 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거(erasure; item 28) 방식을 사용하기로 했다.

List vs List

List같은 로 타입은 사용해서는 안 되나, List처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.

로 타입인 List와 매개변수화 타입인 List의 차이는 무엇일까?

List는 제네릭 타입에서 완전히 발을 뺀 것이고

List는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.

매개변수로 List를 받는 메서드에 List을 넘길 수 있지만

List를 받는 메서드에는 넘길 수 없다.

이는 제네릭의 하위 타입 규칙 때문이다.

즉, List은 로 타입인 List의 하위 타입이지만 List의 하위 타입은 아니다. (item 28)

그 결과, List 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안전성을 잃게 된다.

다음은 로 타입을 사용한, 런타임에 실패하는 코드이다.

public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0); // 컴파일러가 자동으로 형변환 코드를 넣어준다.
    }

    private static void unsafeAdd(List list, Object o) {
        list.add(o);

    }
}

이 코드는 컴파일은 된다. 하지만 로 타입인 List를 사용했기 때문에 경고 문구가 생긴다.

이 프로그램을 실행하면 strings.get(0)의 결과를 형변환하려 할 때 ClassCastException을 던진다.

Integer를 String으로 변환하려 시도한 것이다.

이 형 변환은 컴파일러가 자동으로 만들어준 것이라 보통은 실패하지 않는다.

하지만 이 경우엔 컴파일러 경고를 무시했기 때문에 런타임에 실패한다.

로 타입인 List를 매개변수화 타입인 List로 바꾼 다음 다시 컴파일해보자.

오류 메세지화 함께 컴파일 조차 되지 않을 것이다.

로 타입 vs 비한정적 와일드 카드 타입

제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 로 타입을 사용하지 말고 '?'를 사용하자.

로 타입을 사용하면 타입 관련 오류를 '런타임에' 마주칠 수 있음을 명심하자!

예컨대 제네릭 타입인 Set의 비한정적 와일드카드 타입은 Set<?>다.

이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.

다음은 비한정적 와일드카드 타입을 사용해 선언한 메소드이다.

static int numElementsInCommon(Set<?> s1, Set<?> s2){
    
}

비한정적 와일드카드 타입인 Set<?>와 로 타입인 Set의 차이는 무엇일까?

물음표가 무언가 멋진 일을 해주는 걸까?

특징을 간단히 말하자면 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다.

로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.

반면, Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없다.

심지어 꺼낼 수 있는 객체의 타입도 전혀 알 수 없다.

이러한 제약을 받아들일 수 없다면 제네릭 메서드(item 30)나 한정적 와일드카드 타입(item 31)을 사용하면 된다.

와일드카드에 왜 값을 넣을 수 없을까?

[와일드카드와 정규 타입 매개변수의 차이](https://github.com/JAVA-JIKIMI/EFFECTIVE-JAVA3/wiki/wildcard_vs_formalTypeParameter-JungHyunLyoo)

로 타입을 사용해도 되는 경우 1 :: class 리터럴

class 리터럴에는 로 타입을 써야 한다.

자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다. (배열과 기본 타입은 허용한다)

예를 들어 List.class, String[].class, int.class는 허용하고

List.class와 List<?>.class는 허용하지 않는다.

로 타입을 사용해도 되는 경우 2 :: instanceof

런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드 카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.

그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다

비한정적 와일드카드 타입의 꺾쇠괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만든다.

이런 경우엔 차라리 로 타입을 쓰는 편이 깔끔할 수 있다.

다음은 제네릭 타입에 instanceof를 사용하는 올바른 예다.

if (o instanceof Set) {
    Set<?> s = (Set<?>) o;
}
Clone this wiki locally