본문 바로가기
책/effective java

[effective java] item 26. 로 타입은 사용하지 말라

by 2245 2023. 8. 28.

결론

로 타입은 런타임 에러를 발생시키므로 사용하면 안 됩니다.
매개변수화 타입인 Set<Object>나 비한정적 와일드 카드 타입인 Set<?>은 컴파일 에러를 발생시켜 더 안전하므로, 로 타입인 Set 대신 사용하는 것이 좋습니다. 

 

설명

용어 정리

제네릭 클래스 / 제네릭 인터페이스
클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제니릭 인터페이스라 합니다.
ex) List<E>
List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받습니다. 
그래서 이 인터페이스의 완전한 이름은 List<E>지만, 짧게 그냥 List라고도 자주 씁니다.
제네릭 타입(generic type)
제네릭 클래스와 제네릭 인터페이스를 통틀어 지칭하는 말입니다. 
매개변수화 타입(parameterized type)
각각의 제네릭 타입은 일련의 매개변수화 타입을 지정합니다. 
먼저 클래스의 이름이 나오고, 이어서 < > 안에 실제 타입 매개변수들을 나열합니다. 
ex) List<String>
List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입입니다. 
실제(actual) 타입 매개변수
List<String> 에서 String은 정규(formal) 타입 매개변수 E에 해당하는 실제(actual) 타입 매개변수입니다. 
로 타입(raw type)
로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말합니다.
ex) List<E>에서 로 타입은 List
로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작합니다.
즉, 제네릭이 도래하기 전 코드와 호환되도록 동일하게 동작합니다. 

 

 

제네릭 지원 전의 문제점

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

 

//실수로 동전을 넣는다.
stamps.add(new Coin(...));		//"unchecked call" 경고를 낸다.

 

//런타임 에러!!
for(Iterator i = stamps.iterator(); i.hasNext(); ) {
	Stamp stamp = (Stamp) i.next(); 		// ClassCastException 에러 발생
    stamp.cancel();
}
  • 해당 코드는 도장(Stamp) 대신 동전(Coin)을 넣어도, 아무 오류없이 컴파일되고 실행됩니다.
    (컴파일러가 모호한 경고 메시지를 보여주긴 합니다.)
  • 하지만, 컬렉션에서 해당 동전을 꺼낼 때 런타임 에러를 발생합니다.
    (참고: 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일 때 발견해야 합니다. 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 멀리 떨어져 있을 가능성이 높아 해결이 어려울수 있기 때문입니다.)

 

제네릭 사용

제네릭을 사용하면 //Stamp 인스턴스만 취급한다.는 정보를 주석이 아닌 타입 선언 자체에 녹일 수 있습니다. 

 

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

안전성 확보

  • stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 됩니다. 
  • 따라서, 아무런 경고 없이 컴파일이 된다는 것은 의도대로 동작할 것을 보장받게 됩니다. 

 

로 타입

로 타입을 쓰면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 되므로, 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안 됩니다.

로 타입(List) vs List<Object>

로 타입 (List) 매개변수화 타입 (List<Object>)
제네릭 타입 시스템에 속하지 않습니다.  모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달한 것입니다. 
List 매개변수에 List<String>을 넘길 수 있습니다.
(List<String>은 List의 하위타입입니다.)
List<Object> 매개변수에 List<String>을 넘길 수 없습니다. 
(List<String>은 List<Object>의 하위타입이 아닙니다.)
타입 안전성을 잃습니다.  타입 안전성을 잃지 않습니다. 

 

[예제] 로 타입(raw type) 사용 : 런타임 에러 

public static void main(String[] args) {
	List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0);		//ClassCastExeption 에러 발생
}

private static void unsafeAdd(List, list, Obejct o) {
	list.add(o);					//"unchecked call" 경고 발생
}
  • 컴파일은 가능합니다. 
  • 하지만, 이대로 실행하면 string.get(0)의 결과를 형변환하려 할 때 ClassCastException 에러가 발생합니다. 

 

매개변수화 타입(List<Object>)로 변경 : 컴파일 에러

List<String> cannot be converted to 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<Object>, list, Obejct o) {
	list.add(o);					
}

 

 

로 타입(Set) vs 한정적 와일드 카드 타입(Set<?>)

[예제] 모르는 타입 원소도 받는 로 타입 사용

static int numElementsInCommon(Set s1, Set s2) {
	int result = 0;
    for(Object o1 : s1) {
    	if(s2.contains(o1))
        	result++;
    return result;
}

이  메서드는 동작은 하지만, 로 타입을 사용해 안전하지 않습니다. 로 타입 컬렉션에는 아무 원소나 넣을 수 있어 타입 불변식을 훼손하기 쉽습니다. (ex) stamp 컬렉션에  coin 삽입)

따라서 비한정적 와일드카드 타입(unbounded wildcard type)을 대신 사용하는 것이 좋습니다. 

 

제네릭 타입을 쓰고는 싶지만, 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용합시다. 

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

 

비한정적 와일드카드 타입으로 변경 : 안정성 확보

static int numElementsInCommon(Set s1<?>, Set<?> s2) {
	int result = 0;
    for(Object o1 : s1) {
    	if(s2.contains(o1))
        	result++;
    return result;
}
  • Set<?>에는 같은 타입의 원소만 들어있음을 보장합니다. 
  • Collection<?>에는 null 외에는 어떤 원소도 넣을 수 없습니다. (다른 원소를 넣으려면 컴파일 오류가 발생합니다.)
  • 또한, 컬렉션에서 꺼낼 수 있는 객체의 타입도 전혀 알 수 없습니다.
  • 이러한 제약을 받아들일 수 없다면, 제네릭 메서드 한정적 와일드카드 타입을 사용하면 됩니다. 

 

예외: 로 타입을 써야 하는 경우

1. class 리터럴

- List.class (O)
- List<String>.class (X)
- List<?>.class (X)

참고로, 리터럴에는 배열과 기본 타입도 쓸 수 있습니다.

- String[].class (O)
- int.class (O)

 

2. instanceof 연산자

if (o instanceof Set) {
	Set<?> s = (Set<?>) o;		
    ...
}

//o의 타입이 Set임을 확인한 후, 와일드카드 타입인 Set<?>으로 형변환해야 합니다.
//로 타입인 Set이 아닙니다.
//이는 검사 형변환(checked cast)이므로 컴파일러 경고가 뜨지 않습니다.
  • 런 타임에는 제네릭 타입 정보가 지워지므로, instanceof 연산자는 매개변수화 타입에는 적용할 수 없습니다. 
    (비한정적 와일드카드 타입 제외)
  • 로 타입이든, 비한정적 와일드카드 타입이든 instanceof는 완전히 동작하므로, 지저분한 <> 를 없앤 로 타입을 쓰는 편이 깔끔합니다. 

 


출처

https://www.yes24.com/Product/Goods/65551284

 

이펙티브 자바 Effective Java 3/E - 예스24

자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브

www.yes24.com