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

gang_shik·2022년 5월 8일
0

Effective Java 5장

목록 보기
1/6
  • 클래스와 인터페이스 선언타입 매개변수가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 함

  • 제네릭 클래스와 제네릭 인터페이스를 통틀어서 제네릭 타입이라고 함

  • 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의함

    • 먼저 클래스(혹은 인터페이스) 이름이 나오고 이어서 꺾쇠괄호 안에 실제 타입 매개변수들을 나열함

    • List<String>

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

  • 제네릭 타입 정의시 그에 딸린 로 타입(raw type)도 함께 정의됨

  • 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말함 List<E> 에서 List 가 로 타입임

  • 로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 도래하기 전 코드와 호환되도록 하기 위한 궁여지책임

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

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

// 실수로 Stamp 대신 Coin을 넣음
stamps.add(new Coint(...)); // "unchecked call" 경고를 내뱉음

for (Iterator i = stamps.iterator(); i.hasNext(); ) {
		Stamp stamp = (Stamp) i.next(); // ClassCastException을 던짐
		stamp.cancel();
}
  • 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋음, 위의 예에선 런타임에야 알아채는데 이러면 전체를 훑어서 처리해야하고 주석 설명도 의미가 없어짐

  • 제네릭 활용시 이 정보가 주석이 아닌 타입 선언 자체에 녹아듬

// 매개변수화된 컬렉션 타입 - 타입 안전성 확보
private final Collection<Stamp> stamps = ...;
  • 위와 같이 쓰면 stamps 에는 Stamp 의 인스턴스만 넣어야 함을 컴파일러가 인지함, 엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생하고 무엇이 잘못됐는지를 정확히 알려줌

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

  • 로 타입(타입 매개변수가 없는 제네릭 타입)을 쓰는 걸 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안됨

  • 로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게됨, 이 로 타입은 호환성때문에 생긴것임

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

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

  • 역기서 List 를 받는 메서드에 List<String> 을 넘길 수 있지만, List<Object> 를 받는 메서드에는 넘길 수 없음, 제네릭의 하위 타입 규칙 때문에

  • List<String> 은 로 타입인 List 의 하위 타입이지만, List<Object> 의 하위 타입은 아님

  • 그래서 List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안전성을 잃게됨

// 런타임에 실패하는 케이스 메서드에 로 타입을 사용
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);
}
  • 위를 그대로 실행하면 strings.get(0) 의 결과를 형변환하려 할 때 예외를 던짐, 하지만 이 형변환은 컴파일러가 자동으로 만들어준 것이라 보통은 실패하지 않음

  • 하지만 여기서 List<Object> 로 바꿔서 컴파일을 하면 컴파일조차 안됨

// 잘못된 예 - 모르는 타입의 원소도 받는 로 타입을 사용함
static int numElementsCommon(Set s1, Set s2) {
		int result = 0;
		for (Object o1 : s1)
				if (s2.contains(o1))
						result++;
		return result;
}
  • 위 메서드는 동작은 하지만 로 타입을 사용해 안전하지 않음, 이때 비한정적 와일드카드 타입을 대신 사용하는게 좋음

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

  • 그러면 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화가 됨, 아래와 같이 사용

// 비한정적 와일드카드 타입 사용, 타입 안전하며 유연함
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
  • 와일드카드 타입은 안전하고 로 타입은 안전하지 않음, 로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉬움

  • 반면 Collection<?> 에는 (null외에는) 어떤 원소도 넣을 수 없음, 다른 원소를 넣으면 컴파일 할 때 오류가 나옴

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

추가 예외

  • class 리터럴에는 로 타입을 써야함, class 리터럴에 매개변수화 타입을 사용하지 못하게 함

  • List.class String[].class int.class는 허용하고 List<String>.classList<?>.class 는 허용하지 않음

  • 두 번째로 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없음

  • 그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof 는 완전히 똑같이 동작함, 여기선 차라리 로 타입을 쓰는게 나음

// 로 타입 써도 좋은 예
if (o instanceof Set) { // 로 타입
		Set<?> s = (Set<?>) o; // 와일드카드 타입
		...
}
profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글