Study/이펙티브 자바 / / 2023. 11. 1. 20:57

[Effective Java 3E] 로 타입은 사용하지 말라

💥 개요

한글 용어 영문 용어 예시
매개변수화 타입 parameterized type List<String>
실제 타입 매개변수 actual type parameter String
제네릭 타입 generic type List<E>
정규 타입 매개변수 formal type parameter E
비한정적 와일드카드 타입 unbounded wildcard typ List<?>
로 타입 raw type List
한정적 타입 매개변수 bounded type parameter <E extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable<T>>
한정적 와일드카드 타입 bounded wildcard type List<? extends Number>
제네릭 메서드 generic method static <E> List<E> asList(E[] a)
타입 토큰 type token String.class

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

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

클래스(혹은 인터페이스)의 이름<실제 타입 매개변수들>의 형식으로 나열한다.

List<String>에서 원소 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.

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

 

 

⚠️ 문제

제네릭 타입을 하나 정의하면 그에 딸린 로(raw) 타입은 타입 선언에서 제네릭이 전부 지워진 것 처럼 동작하는데, 제네릭이 나오기 전의 코드와 호환하기 위해서다. 제네릭은 지원하기 전에는 다음과 같이 컬렉션을 선언했다.

//Stamp 인스턴스만 취급
private final Collection stamps = ...;

//실수로 동전을 넣는다
stamps.add(new Coin(...));

 위처럼 이전에는 실수로 동적을 넣으면 런타임 시점 전까지 오류를 알아차리지 못한다.

 

 

👍 해결 방법

//매개변수화된 컬렉션 타입 - 타입 안정성 확보
private final Collection<Stamp> stamps = ...;

이렇게 선언하면 컴파일 시점에 오류가 발생하게 되고, 경고가 미리 나타난다.

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

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

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

 

🔍 List와 List<Object>의 차이

List는 제네릭 타입에서 완전히 발을 뺀 것이고, List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.

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를 사용해 경고가 발생하고, 형변환시 ClassCastException을 던진다. Integer를 String으로 변환하려다 실패한다. 이 형변환은 컴파일러가 자동으로 만들어준 것이라 보통은 실패하지 않는다. 하지만 이경우에는 경고를 무시하여 대가를 치르게 된다.

 

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

이제 List를 매개변수화 타입인 List<Object>로 변경하면 오류 메시지가 발생하여 컴파일 시점에 알아차릴 수 있다.

 

 

🎈 타입을 몰라도 되는 로 타입

그럼 원소의 타입을 몰라도 되는 로 타입을 쓰려면 어떻게 할까?

//잘못된 예 - 모르는 타입의 원소도 받는 로 타입 사용
statc int numElementsInCommon(Set s1, Set s2) {
	int result = 0;
    for (Object o1 : s1)
    	if (s2.contains(o1))
        	result++;
    
    return result;
}

이 메소드는 동작하지만 로 타입을 사용해 안전하지 않다.

 

따라서 비한정적 와일드카드 타입을 대신 쓰는게 좋다.

//비한정적 와일드카드 타입을 사용하라. - 타입에 안전하며 유연하다.
statc int numElementsInCommon(Set<?> s1, Set<?> s2) {

와일드 카드 타입은 안전하고, 로 타입은 안전하지 않다. 로 타입 컬렉션은 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.

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

다른 원소를 넣으려 하면 컴파일 시점에 오류가 발생한다. 

@Test
void 와일드카드타입_컴파일_오류_발생() {
    Collection<?> wildCollection = new ArrayList<String>();
    wildCollection.add("Hello");  // 오류: add(capture<?>)에서 String을 인수로 사용할 수 없음
    wildCollection.add(null);     // 작동함
}

컴파일도 안되고 오류가 발생

 

🛠️ 로 타입을 쓰지 말라 - 예외

1.class 리터럴에서는 로 타입을 써야한다. 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못함

  •  배열과 기본 타입은 허용한다.
  •  [허용] List.class, String[].class int.class
  •  [비허용] List<String>.class, List<?>.class

2. instanceof 연산자는 비한정적 와일드타입 이외의 매개변수화 타입에 적용할 수 없다.

 - 로타입이든 비한정적 와일드카드 타입이든 isntanceof는 완전히 똑같이 동작한다.

 - 꺾쇠괄호와 물음표는 아무 역할없이 더럽게 만든다.

 - 차라리 로타입이 깔끔

//로 타입을 써도 좋은 예 -instaceof 연산자
if (o instanceof Set) {		//로타입
	Set<?> s = (Set<?>) o;	//와일드카드 타입
    ...
}

 

로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안된다. 로 타입은 제네릭이 도입되기 전의 코드와 호환성을 위해 제공될 뿐이다. 빠르게 훑어보자면, Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다. 그리고 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다. Set<Object>와 Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유