클래스와 인터페이스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 함
제네릭 클래스와 제네릭 인터페이스를 통틀어서 제네릭 타입이라고 함
각각의 제네릭 타입은 일련의 매개변수화 타입을 정의함
먼저 클래스(혹은 인터페이스) 이름이 나오고 이어서 꺾쇠괄호 안에 실제 타입 매개변수들을 나열함
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>.class
와 List<?>.class
는 허용하지 않음
두 번째로 런타임에는 제네릭 타입 정보가 지워지므로 instanceof
연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없음
그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof
는 완전히 똑같이 동작함, 여기선 차라리 로 타입을 쓰는게 나음
// 로 타입 써도 좋은 예
if (o instanceof Set) { // 로 타입
Set<?> s = (Set<?>) o; // 와일드카드 타입
...
}