Home Item 26 - 로 타입은 사용하지 말라
Post
Cancel

Item 26 - 로 타입은 사용하지 말라

1. 제너릭이란? (Generic type)

클래스 내부에서 사용할 데이터 타입을 나중에 인스턴스를 생성할 때 확정하는 것.
클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제너릭 클래스 혹은 제너릭 인터페이스라 한다.

ex) List 인터페이스

1
2
3
public interface List<E> extends Collection<E> {
    ...
}    

원소의 타입을 나타내는 타입 매개변수 E를 받는다. 따라서 이 인터페이스의 완전한 이름은 List<E>지만 짧게 그냥 List라고 자주 쓴다. 제너릭 클래스 + 제너릭 인터페이스를 통틀어 제너릭 타입이라 한다.

제너릭 타입은 매개변수화 타입을 정의한다.

클래스 이름(혹은 인터페이스)<실제 타입 매개변수들 나열>

  • List
    • 원소의 타입이 String인 List를 뜻하는 매개변수화 타입
    • String: 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수

제너릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.

로타입: 제너릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때
ex) List<E>의 로타입은 List

  • 로타입은 타입선언에서 제너릭 타입 정보가 전부 지워진 것 처럼 동작한다.
  • 제너릭이 도래하기 전 코드와 호환되도록 하기 위해 생겼다.

제너릭을 지원하기 전 raw type의 사용

1
2
3
4
5
6
7
8
9
10
11
//Stamp 인스턴스만 취급하는 Collection 로타입 선언
private final Collection stamps = ...;

//실수로 동전을 넣은 경우 "Unchecked call" 경고만 내뱉고 오류없이 컴파일된다.
stamps.add(new Coin(...));

//이렇게 동전을 꺼내기 전에는 오류를 알아채지 못한다.
for(Iterator i = stamps.iterator(); i.hasNext();){
    Stamp stamp = (Stamp)i.next(); //ClassCastException을 던진다.
    stamp.cancel();
}
  • 단점
    • 오류가 런타임시에 발견된다. 좋은 코드는 오류가 발생하기 전 컴파일 즉시 발견할 수 있는 코드다.
      • 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 떨어져있을 가능성이 커진다.
    • 로타입을 쓰면 제너릭이 안겨주는 안전성(typesafe)과 표현력을 모두 잃는다. 절대로 써서는 안된다.

잘못된 예

  • 모르는 타입의 원소도 받는 로타입 사용
    1
    2
    3
    4
    5
    6
    7
    8
    
    //2개의 집합(Set)을 받아 공통 원소의 수를 반환하는 메서드
    static int numElementsInCommon(Set s1,Set s2){
      int result = 0;
      for (Object o1 : s1)
          if(s2.contains(o1))
              result++;
      return result;
    }
    

    위 소스는 정상 작동하나 로 타입을 사용해 안전하지 않다.
    비한정적 와일드카드 타입(unbounded wildcard type)을 사용하자!

제너릭을 활용하면 매개변수화 타입을 주석이 아닌 타입 선언 자체에 담을 수 있다.

매개변수화 타입: stamps에는 Stamp만 들어간다는 정보

1
2
3
4
5
6
7
8
9
10
//매개변수화된 컬렉션 타입 - 타입 안전성 확보!
private final Collection<Stamp> stamps = ...;

//컴파일 오류 발생!
stampsC.add(new Coin(...));

for(Iterator i = stamps.iterator(); i.hasNext();){
    Stamp stamp = (Stamp)i.next(); //여기서 에러나지 않음을 컴파일러가 보장한다.
    stamp.cancel();
}

List 같은 로타입은 사용하면 안되지만 List<Object>사용은 된다.

  • 차이점
    • List
      • 제너릭 타입에서 완전히 발을 뺀 것
    • List<Object>
      • 모든 타입의 객체를 허용한다는 의사를 컴파일러에 전달

제너릭의 하위 타입 규칙

매개변수 List를 받는 메서드에 List<String>을 넘길 수 있다.
그러나 List<Object>를 받는 메서드에 List<String>를 넘길 수 없다.
List<String>: 로타입 List의 하위타입, List<Object>의 하위타입은 아님

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();

    addList(strings, Integer.valueOf(42)); //매개변수 List를 받는 메서드에 `List<String>`을 넘길 수 있다.
    safeAddList(strings, Integer.valueOf(42)); //컴파일 에러! List<String>은 List<Object>의 하위타입이 아니다.

    String s = strings.get(0);//class java.lang.Integer cannot be cast to class java.lang.String
}

private static void addList(List list, Object o){
    list.add(o);//컴파일러 경고 발생: 매개변수화된 클래스 List의 raw type 사용
}

private static void safeAddList(List<Object> list, Object o){
    list.add(o);
}

2. 비한정적 와일드카드 타입(unbounded wildcard type)

제너릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때 로타입이 아닌 물음표(?)를 사용하자.
?는 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set타입이다.

1
2
3
4
//좋은 예: 비한정적 와일드카드 타입(unbounded wildcard type) 사용
static int numElementsInCommon(Set<?> s1,Set<?> s2){
    ...
}

Set VS Set<?>

  • 로 타입 Set
    • 불안전함 - 아무 원소나 넣을 수 있어 타입 불변식을 훼손하기 쉽다.
  • 비한정적 와일드카드 타입(Unbounded wildcard type) Set<?>
    • Set<E>의 비한정적 와일드카드 타입은 Set<?>이다.
    • 안전함 - Collection<?>에는 null제외 어떤 원소도 넣을 수 없다.
      • 다른 원소를 add하려 하면 컴파일타임에 오류메시지를 볼 수 있다.
      • 컬렉션의 타입 불변식을 훼손하지 못하게 막고, 컬렉션에서 꺼낼 수 있는 객체의 타입도 알 수 없게 한다.

3. 로타입 규칙 예외

1) class 리터럴에는 로타입을 사용한다.

자바 명세가 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.

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

  • 클래스 리터럴(class literal)
    • 클래스 리터럴(Class Literal)은 String.class, Integer.class 등을 말하며, String.class의 타입은 Class<String>, Integer.class의 타입은 Class<Integer>다.
    • 타입 토큰(Type Token)은 쉽게 말해 타입을 나타내는 토큰이며, 클래스 리터럴이 타입 토큰으로서 사용된다.
    • myMethod(Class<?> clazz)와 같은 메서드는 타입 토큰을 인자로 받는 메서드이며, method(String.class)로 호출하면, String.class라는 클래스 리터럴을 타입 토큰 파라미터로 myMethod에 전달한다.

2) instance of 연산자는 로타입을 사용한다.

런타임에는 제너릭 타입 정보가 지워진다.
instance of 연산자는 비한정적 와일드카드 타입(Set<?>) 이외의 매개변수화 타입(Set<String>)에는 적용할 수 없다.
로타입과 비한정적 와일드 카드 타입의 instance of는 완전히 똑같이 동작한다. <?>는 코드만 지저분해지므로 차라리 로타입을 쓰자.

1
2
3
if(o instance of Set){    //로 타입
    Set<?> s = (Set<?>)o; //와일드카드 타입
}

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

결론

  • 로 타입을 사용하면 런타임에 예외가 일어날 수 있으므로 사용하지 마라
    • 로 타입은 제너릭이 도입되기 이전 코드와의 호환성을 위해 제공된 것이다.
  • Set<Obejct>: 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입. 안전함.
  • Set<?>: 어떤 종류의 타입 객체만 저장할 수 있는 와일드카드 타입. 안전함.
  • Set: 로타입. 제너릭 타입 시스템에 속하지 않음. 안전하지 않음.

제너릭 관련 용어정리

1
2
3
public interface List<E> extends Collection<E> {
     private E member; 
}
한글용어(영문용어)설명
매개변수화 타입(parameterized type)각각의 제네릭 타입은 parameterizedType을 선언함List<String>
실제 타입 매개변수(actual type parameter) String
제네릭 타입(Generic Type)제네릭 클래스와 제네릭 인터페이스를 통틀어 이르는 말List<E>
정규 타입 매개변수(formal Type parameter)제네릭 선언에 사용된 매개변수E
비한정적 와일드카드 타입(unbounded wildcard type)어떤 종류의 타입 객체만 저장할 수 있는 와일드카드 타입List<?>
로타입(raw type)제네릭 타입에서 타입 매개변수를 전혀 사용하지 않았을 때List
한정적 타입 매개변수(bounded Type parameter) <E extends Number>
재귀적 타입 한정(recursive Type parameter) <T extends Comparable<T>>
한정적 와일드카드 타입(bounded wildcard type) List<? extends Number>
제너릭 메서드(generic method) static <E> List<E> asList(E[] a)
타입 토큰(type token) String.class

참고

This post is written by PRO.