Effective Java - 제네릭(1)

SeungHyuk Shin·2021년 10월 6일
0

Effective Java

목록 보기
14/26
post-thumbnail

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


로타입은 List list = new ArrayList(); 형태로 사용하는 방식이다.
이번장에서 왜 로타입을 사용하면 안되는지, 그렇다면 로타입을 사용하고 싶을땐 어떤 방법으로 제네릭을 사용해야하는지 알아보자.
클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.

List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다. 그래서 이 인터페이스의 완전한 이름은 List 지만, 짧게 그냥 List라고도 자주 쓰인다. 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입(generic type)이라 한다.

각각의 제네릭 타입은 일련의 매개변수화 타입(parameterized type)을 정의한다. 먼저 클래스 이름이 나오고, 이어서 꺾쇠괄호 안에 실제 타입 매개변수들을 나열한다.

마지막으로, 제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다. 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다. 로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 도래하기 전 코드와 호환되도록 하기 위한 억지로 짜낸 방법이라 할 수 있다.

제네릭을 지원하기 전에는 컬렉션을 다음과 같이 선언했다. 자바 9에서도 여전히 동작하지만 좋은 예라고 볼 수는 없다.

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

이 코드를 사용하면 실수로 도장(Stamp) 대신 동전(Coin)을 넣어도 아무 오류없이 컴파일되고 실행된다(컴파일러가 모호한 경고 메시지를 보여주긴 할 것이다).

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

이 책 전반에서 이야기하듯, 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋다. 이 오류는 런타임에야 알아챌 수 있는데, 이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 상당히 떨어져 있을 가능성이 커진다.

ClassCastException이 발생하면 stamps에 동전을 넣은 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있다. 주석은 컴파일러가 이해하지 못하니 별 도움이 되지 못한다. 제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아든다.

이런 경우도 있을 수 있다. 매개형식타입으로 List stringList로 잘 선언했지만, 로타입 리스트와 오브젝트를 받아 add시켜주는 함수를 이용해 add한다면 컴파일은 되지만 get을 할때 ClassCastException이 떨어질 것이다.

제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않다면 비한정적 와일드카드 타입(unbounded wildcard type)인 물음표('?')를 대신 사용하는게 좋다. 예컨대 제네릭 타입인 Set의 비한정적 와일드카드 타입은 Set<?>다. 이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.

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

제너릭 vs 와일드 카드?

제네릭 : 지금은 이 타입을 모르지만, 이 타입이 정해지면 그 타입 특성에 맞게 사용하겠다!
와일드 카드 : 지금도 이 타입을 모르고, 앞으로도 모를 것이다!

List<?> list;
// 1. 원소를 꺼내 와서는 Object에 정의되어 있는 기능만 사용하겠다. equals(), toString(), hashCode()…
// 2. List에 타입이 뭐가 오든 상관 없다.
 // 나는 List 인터페이스에 정의되어 있는 기능만 사용하겠다. size(), clear().. 단,  add(), addAll()과 같은타입 파라미터와 결부된 기능은 사용하지 않겠다!
List<T> list;
// 1. 원소를 꺼내 와서는 Object에 정의되어 있는 기능만 사용하겠다. equals(), toString(), hashCode()…
// 2. List에 타입이 뭐가 오든 상관 없다. 
// 나는 List 인터페이스에 정의되어 있는 기능만 사용을 하고, 타입 파라미터와 결부된 기능도 사용하겠다.

로 타입을 쓰지 말라는 규칙에도 몇몇 예외가 있다.

  1. class 리터럴에는 로 타입을 써야 한다.

    자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다. 예를 들어 List.class, String[].class, int.class는 허용하고 List.class와 List<?>.class는 허용하지 않는다.

  2. instanceof 연산자.

    런타임에는 제네릭 타입정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. 그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다. 비한정적 와일드카드 타입의 꺾쇠괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만드므로, 차라리 로 타입을 쓰는 편이 깔끔하다.

0개의 댓글