[Effective Java] 5장 제네릭 (1부)


정리

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

아이템 27. 비검사 경고는 무시하지 말자. 모든 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있는 잠재적 가능성을 뜻하니 최대한 제거하자. 경고를 없앨 방법을 찾기 못하면 최대한 좁은 범위에 @SupperessWarnings("unchecked") 애너테이션으로 경고를 숨겨라. 그런 다음 경고를 숨기기로 한 근거를 주석으로 남겨라
아이템 28. 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입이 안전하지 않지만 컴파일타임에는 그렇지 않다. 제네릭은 반대다 그래서 둘을 섞어 쓰기 쉽지 않다. 이로 인해 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용하자
아이템 29. 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 안전하고 쓰기 쉽다. 이 방식으로 설계하기 위해선 제네릭 타입으로 만들어야 할 경우가 많다. 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자. 기존 클라이언트에는 아무 영향을 주지 않고, 새로운 사용자가 사용하기 쉬워질 것이다.

내용
아이템 26. 로 타입(Raw Type)은 사용하지 말라
● 클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다. 각각의 제네릭 타입은 일련의 매개변수화 타입(parameterized)을 정의한다.

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

● private final Collection stamps = ...; 와 같이 사용하지 말자, 이 코드를 사용하게 되면 실수로 다른 타입의 값을 넣어도 아무 오류가 생기지 않고 나중에 사용하려고 할때 오류가 발생한다.
이렇게 되면 런타임 문제를 겪는 코드와 원인을 제공하는 코드가 물리적으로 상당히 떨어져 있어서 코드 전체를 확인해야할 수 있다.
private final Collection stamps = ...; 을  private final Collection<Stamp> stamps = ...; 으로 변경하면 예방할 수 있다.  

● raw 타입은 제네릭으로 인해 생기는 안전성과 표현력을 모두 잃게 된다. 제네릭을 쓰는 이유는 호환성 때문이다.

● raw 타입을 사용하면 안전하지 않을 때 비한정적 와일드카드 타입(unbounded wildcard type)을 대신 사용하는 게 좋다. 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용하면 된다. 그 이유는 컴파일러가 제 역할을 할 수 있기 때문입니다.

● class 리터럴에는 raw 타입을 써야한다.

1. 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.
2. instanceof 연산자와 관련이 있다. 비한정적 와일드카드 타입 이외의 매개벼수화 타입에는 적용할 수 없다. 그리고 raw 타입이건 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.
아래의 예는 제네릭 타입에 instanceof를 사용하는 올바른 방법이다.
if (o instanceof Set) {
Set<?> s = (Set<?>) o;
...
}

아이템 27. 비검사 경고를 제거하라
● 제네릭에 익숙해질수록 마주치는 경고 수는 줄겠지만 새로 작성한 코드가 한번에 깨끗하게 컴파일 되라 기대하지 말아야한다. 그럼 컴파일러가 무엇이 잘못됐는지 설명할 것이다.(javac 명령줄 인수에 -Xlint:uncheck 옵션을 추가)

● 모든 비검사 경고를 제거하면 타입 안전성이 보장된다.(ClassCastException이 발생하지 않는다)

● 자바 7부터는 지원하는 다이아몬드 연산자(<>)만으로 해결할 수 있다. (Set<String> s = new HashSet<>();)

● 경고를 제거할 수 없지만 타입 안전하다고 확신할 수 있다면 @SupperessWarnings("unchecked") 애너테이션을 달아 경고를 숨기자. @SupperessWarnings은 클래스 전체에 할 수 있지만 최대한 적은 범위내에 사용하는 것이 좋다. 또한 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야한다.

아이템 28. 배열보다는 리스트를 사용하라
● 배열과 제네릭 타입에는 중요한 두가지 차이가 있다.
1. 배열은 공변(covariant)이다. (공변 : 함께 변한다는 의미) Sub Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다. 반면 제네릭은 불공변이다. 그레서 배열은 런타임에 실수를 잡을 수 있지만 리스트는 컴파일시 바로 알 수 있다.
2. 배열의 실체화(reify)이다. 실체화(reify), 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 그래서 다른 타입을 넣으면 ArrayStoreException이 발생한다. 반면 제네릭의 경우 타입정보가 런타임시 소거된다.
● 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 제네릭 배열을 못 만드는 이유는 타입이 안전하지 않기 때문이다. 즉 컴파일이 되지 않는다. 서로 다른 타입을 넣고 반환 과정에서 형변환을 하게 되는데, 이때 ClassCastException이 발생하기 때문이다.
● E, List<E>, List<String> 같은 타입을 실체화 불가 타입(non-reifiable type)이라 한다. 실체화되지 않아서 런타임에는 컴파일타입보다 타입 정보를 적게 가지는 타입이다.
● 실체화 불가 타입은 배열을 만들 수 없다.
● 소거 매커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?> Map<?,?> 같은 비한정적 와일드카드 타입뿐이다.
● 제네릭 컬렉션에서는 자신의 원소 타입을 담을 배열을 반환하는 게 보통은 불가능하다. 또한 제네릭 타입과 가변인수 메서드(varargs method)를 함께 쓰면 경고 메시지를 받게 된다.
이유는 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하기 때문이다. (@SafeVarargs 으로 대체할 수 있다)
● 제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없음을 기억하자

아이템 29. 이왕이면 제네릭 타입으로 만들라
● 일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다. 스택이 담을 원소의 타입 하나만 추가하면 된다. (이때 타입 이름으로 보통 E를 사용한다)
● 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음은 다음 기준에 부합해야 한다. 문제의 배열 elements private필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 전혀 없고 항상 E 타입만 들어온다면 안전한 것이다. (heap pollution을 일으킬 수 있음)
● elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것이다. 하지만 배열이 반환한 원소를 E로 형변환하면 오류 대신 경고가 뜬다. E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 알 수 없기 때문이다. 하지만 위와 같은 기준에 부합이 된다면 메서드 전체에 경고를 숨기지 말고 비검사 형변환을 수행하는 부분만 숨기는 것이 좋다.
● 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다. 단 기본 타입은 사용할 수 없다. 이는 자바 제네릭 타입 시스템의 근본적인 문제이나, 박싱된 기본 타입을 사용해 우회할 수 있다.
한정적 타입 매개변수(bounded type parameter), 타입 매개변수에 제약을 두는 제네릭 타입도 있다. java.util.concurrent.DelayQueue<E extends Delayed> java.util.concurrent.Delayed의 하위 타입만 받는다라는 뜻이다. 이 방식을 사용하면 형변환 없이 바로 Delayed 클래스의 메서드를 호출할 수 있다.


용어정리
● java.util.concurrent.DelayQueue : 지연 시간이 기준인 BlockingQueue이다. 가장 오래 지연된 원소가 먼저 서비스를 받는다.



5장 제네릭에서는 raw 타입의 사용하는 것을 강력하게 사용하지 말라고 이야기하고 있다. 이유는 안전성과 표현력을 잃게 되기 때문이다. 런타임 문제를 겪는 코드와 원인을 제공하는 코드가 물리적으로 상당히 떨어져 있어서 코드 전체를 확인해야하는 불상사가 생길 수 도 있다. 따라서 제네릭이나 비한정적 와일드카드 타입을 사용하는 것이 좋다.

댓글

가장 많이 본 글