[교재 EffectiveJava] 아이템 26. 로 타입은 사용하지 말라

반응형
728x90
반응형

제네릭 타입

클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제네릭 클래스 혹인 제네릭 인터페이스라고 한다. 이를 통틀어 제네릭 타입(generic type)이라고 한다.

 

List<E>

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

 

각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다. List<String> 은 원소의 타입이 String 인 리스트를 뜻하는 매개변수화 타입이다. 여기서 String 이 정규(formal) 타입 매개변수 E 에 해당하는 실제(actual) 타입 매개변수다.

 

 

로 타입 (Raw Type)

제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다. 로 타입이란, 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다. List<E> 의 로 타입은 List 이다. 로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 도래하기 전 코드와 호환되도록 하기 위한 궁여지책이라 할 수 있다. 

private final Collection stamps = ...;
private final List list = ...;

stamps.add(new Coin(...)); // Stamp 타입이 아닌 Coin 객체도 들어간다.

 

해당 코드 사용시, 어떤 타입의 객체를 넣어도 아무 오류 없이 컴파일 되고 실행된다. 모호한 경고 메시지는 보여주지만 실행은 되어 오류를 알아채지 못하게된다. 오류는 컴파일 시점에 발견하는 것이 좋다.

 

 

타입 안정성 확보

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

 

이렇게 선언하면 컴파일러는 stamps 에는 Stamp 의 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다. 따라서 아무런 경고 없이 컴파일된다면 의도대로 동작할 것임을 보장한다. 

stamps.add(new Coin(...)); // Stamp 타입이 아닌 Coin 객체도 들어간다. -> 컴파일 오류 발생

 

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

 

 

로 타입(raw type) 쓰지않기

로 타입(타입 매개변수가 없는 제네릭 타입)은 절대로 써서는 안된다. 로 타입을 쓰면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게된다. 절대로 써서는 안되는 로 타입이 사용이 가능한 이유는 호환성 때문이다. 자바가 제네릭을 받아들이기까지 거의 10년이 걸려, 제네릭 없이 짠 코드가 이미 세상을 뒤덮어버렸다. 그래서 기존 코드를 모두 수용하면서 로 타입의 사용이 가능해야했다. 

 

List, List<Object>

이 둘의 차이는 무엇일까?

List 는 제네릭 타입에서 완전히 발을 뺀 것이고, List<Object> 는 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달한 것이다. 매개변수로 List를 받는 메서드에 List<String>을 넘길수 있지만, List<Object>를 q받는 메서드에 List<String>는 넘길수 없다. 이는 제네릭의 하위타입 규칙 때문이다. 즉, List<String>은 로 타입인 List 의 하위 타입이지만 List<Object>의 하위타입은 아니다.  그 결과 List<Object> 같은 매개변수화 타입을 사용할때와 달리 List 같은 로 타입을 사용하면 타입 안정성을 잃게된다.

package com.java.effective.item26;

import java.util.ArrayList;
import java.util.List;

public class Main {
    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);
    }
}

 

해당 코드는 ClassCastException 에러가 발생한다. Integer -> String 변환하려 시도한 것이다.

 

경고발생
Unchecked call to 'add(E)' as a member of raw type 'java.util.List'

 

이 형변환은 컴파일러가 자동으로 만들어준거라 보통은 실패하지 않는다. 하지만 이 경우엔 컴파일러의 경고를 무시하여 그 대가를 치른 것이다.

import java.util.ArrayList;
import java.util.List;

public class Main {
    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<String> list, Object o) {
        list.add(o);
    }
}

 

로 타입인 List -> List<Object> 로 바꾼 다음엔 오류 메시지가 출력되며 컴파일 조차 되지 않는다.

java: incompatible types: java.lang.Object cannot be converted to java.lang.String

 

 

비한정적 와일드카드 타입 

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

 

와일드카드 타입은 안전하다. Collection<?>에는 null 외에는 어떤 원소도 넣을 수 없다. 

public static int numElementInCommon(Set<?> s1, Set<?> s2) {
    s1.add(1); // 불가
    s1.add(null); // 가능
}

컬렉션의 타입 불변식을 훼손하지 못하게 막았다. 구체적으로는, 어떤 원소도 Collection<?>에 넣지 못하게 했으며 컬렉션에서 꺼낼 수 있는 객체의 타입도 전혀 알수 없게 했다.

 

 

로 타입의 사용 예외

로 타입의 사용을 절대 하지 말라고 했지만 예외의 경우도 있다.

 

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

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

허용 : List.class, String[].class 
불가 : List<String>.class, List<?>.class

 

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

로 타입이든 비한정적 와일드카드 타입이든 instanceof 는 똑같이 작동되므로, 로 타입을 쓰는 것이 코드가 더 깔끔하다.

if (o instanceof Set) { // 로 타입
    Set<?> s = (Set<?>) o; // 와일드카드 타입
    ...
}

 

o 타입이 Set임을 확인한 다음 와일드카드 타입인 Set<?>로 형변환 해야한다. 이는 검사 형변환(checked cast) 이므로 컴파일러 경고가 뜨지 않는다. 

 

 

반응형

Designed by JB FACTORY