아이템 26. 로 타입은 사용하지 말라

문법식·2022년 8월 9일
0

Effective Java 3/E

목록 보기
26/52

클래스와 인터페이스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다. 예를 들어 List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다. 그래서 이 인터페이스의 완전한 이름은 List<E>이다. 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 한다.

각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다. 먼저 클래스(혹은 인터페이스) 이름이 나오고, 이어서 꺽쇠 갈호 안에 실제 타입 매개변수들을 나열한다. List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다. 여기서 String이 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수다.

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


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

// 컬렉션 로 타입
private final Collections stamps=...;

이 코드를 사용하면 실수로 도장(Stamp) 대신 동전(Coin)을 넣어도 아무 오류 없이 컴파일되고 실행된다. 컬렉션에서 이 동정은 다시 꺼내기 전에는 오류를 알아채지 못한다.
이 책 전반에서 많이 얘기하고 강조했듯이, 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋다. 이 예에서는 오류가 발생하고 한참 뒤인 런타임에야 알아챌 수 있는데, 이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공하는 코드가 물리적으로 상당히 떨어져 있을 가능성이 높아진다. 그래서 stamps에 동전을 넣은 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있다.

제네릭을 활용하면 아래와 같다.

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

위와 같이 선언하면 컴파일러는 stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다. 따라서 아무런 경고 없이 컴파일된다면 의도대로 동작할 것임을 보장한다. 또한 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장한다


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

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

위 코드와 같이 원소의 타입 구분 없이 사용하고자 로 타입을 사용하고 싶을 수 있다. 하지만 안전하지 않은 행동이다. 따라서 비한정적 와일드카드 타입을 대신 사용하는 것이 좋다. 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표를 사용하자. 예를 들어 Set<E>의 비한정적 와일드카드 타입은 Set<?>다. 이것이 어떤 타입이라도 담으 수 있는 가장 범용적인 매개변수화 Set타입이다. 다음과 같이 사용하면 된다.

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

비한정적 와일드카드 타입인 Set<?>와 로 타입인 Set의 차이에 의문을 가질 수 있다. 간단히 말하자면 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다. 로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다. 반면에 Collcetion<?>에는 (null 외에는) 어떤 원소도 넣을 수 없다. 다른 원소를 넣으려 하면 컴파일 오류가 발생한다.


로 타입을 쓰지 말라는 규칙에도 예외가 몇 개 있다. class 리터럴에는 로 타입을 써야 한다. 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.(배열과 기본 타입은 허용한다.) 예를 들어 List.class, String[].class, int.class는 허용하고 List<String>.classList<?>.class는 허용하지 않는다.
두 번째 예외는 instanceof 연산자와 관련이 있다. 런타임에는 제너릭 타임 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. 그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다. 비한정적 와일드카드 타입의 꺾쇠괄호와 물음표는 아무런 역할 없이 코드를 지저분하게 만드므로, 차라리 로 타입을 쓰는 편이 깔끔하다. 다음은 제네릭 타입에 instanceof를 사용하는 올바른 예다.

if (o instanceof Set){
	Set<?> s=(Set<?>) o;
    ...
}
profile
백엔드

0개의 댓글