용어 정리
한글 용어 | 영문 용어 | 예 | 아이템 |
매개변수화 타입 | parameterized type | List<String> | 아이템 26 |
실제 타입 매개변수 | actual type parameter | String | 아이템 26 |
제네릭 타입 | generic type | List<E> | 아이템 26, 29 |
정규 타입 매개변수 | formal type parameter | E | 아이템 26 |
비한정적 와일드카드 타입 | unbounded wildcard type | List<?> | 아이템 26 |
로 타입 | raw type | List | 아이템 26 |
한정적 타입 매개변수 | bounded type parameter | <E extends Number> | 아이템 29 |
재귀적 타입 한정 | recursive type bound | <T extends Comparable<T>> | 아이템 30 |
한정적 와일드카드 타입 | bounded wildcard type | List<? extends Number> | 아이템 31 |
제네릭 메서드 | generic method | static <E> List<E> asList(E[] a) | 아이템 30 |
타입 토큰 | type token | String.class | 아이템 33 |
Raw Type
class와 interface 선언에 타입 매개 변수가 사용되면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.
제네릭 타입에서 타입 매개변수를 생략한 것을 Raw Type이라고 한다. 예를 들어 List<String>을 List로 선언하는 것이 Raw Type이다.
제네릭은 JDK 5부터 사용할 수 있다. 그 이전 버전 하위 호환성 문제로 인해 Raw Type을 아직까지도 허용하고 있다. 또한, Raw Type이 없어도 구현하는 데에 딱히 문제되지 않는다.
결론적으로, Raw Type은 사용하면 안 된다. 이유는 다음과 같다.
1. 코드의 가독성 문제
2. 타입 안정성(type safety) 문제
3. 컴파일 시점에 오류가 발생하지 않고 런타임 시점에 발생하는 문제
이에 대해 자세히 살펴보자.
컴파일 단계가 아닌 런타임 단계에 에러를 인지한다.
Raw Type 예시 1
class Main {
static class Stamp {}
static class Coin {}
private final static Collection stamps = new ArrayList(); // Stamp 클래스만 담기 위해 선언함
public static void main(String[] args) {
stamps.add(new Coin()); // 오류가 발생하지 않음 !
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // ClassCastException 발생 !
}
}
}
위 코드에서 Stamp가 아닌 Coin을 해당 컬렉션에 넣어도 오류 없이 컴파일되고 실행된다.
그 다음, Coin이 담겨져 있는 stamps에서 원소를 꺼내오고, Stamp로 형변환을 할 때, ClassCastException이 발생한다.
즉, 컴파일 단계가 아닌 런타임 단계에 에러를 인지하게 된다.
타입 매개변수 지정 예시 1
class Main {
static class Stamp {}
static class Coin {}
private final static Collection<Stamp> stamps = new ArrayList();
public static void main(String[] args) {
stamps.add(new Coin());
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next();
}
}
}
위 코드처럼 타입 매개변수를 지정하면, 컴파일 단계에서 에러가 발생한다.
Raw Type 예시 2
class Main {
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0); // ClassCastException 발생 !
}
}
Raw Type인 List를 매개변수로 받는 메서드에 List<String>을 넘길 수는 있다. 왜냐하면 List<String>은 Raw Type인 List의 하위 타입이기 때문이다.
마찬가지로, 컴파일 단계에서는 오류가 발생하지 않지만 strings.get(0)에서 Integer를 String으로 형변환할 때, ClassCastException이 발생한다.
타입 매개변수 지정 예시 2
class Main {
private static void unsafeAdd(List<Object> list, Object o) {
list.add(o);
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
}
위 코드처럼, 타입 매개변수를 지정하면 컴파일 단계에서 에러가 발생한다. List<String>은 List<Object>의 하위 타입이 아니기 때문이다.(아이템 28)
즉, Raw Type인 List는 모든 타입의 원소를 넘겨받을 수 있지만, 매개변수화 타입인 List<Object>는 List<Object>로만 파라미터를 받을 수 있다.
비한정적 와일드카드 타입
Raw Type 예시 3
private static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1) {
if(s2.contains(o1)) {
result ++;
}
}
return result;
}
위 코드는 동작은 하지만(마찬가지로 다른 Type이 들어왔을 때 ClassCastException) Type Safety하지 않다. 이런 경우, 비한정적 와일드카드 타입(unbounded wildcard type)을 사용하는 것이 좋다.
비한정적 와일드카드 타입(unbounded wildcard type) 예시 1
private static int numElementsInCommon(Set<?> s1, Set<?> s2) {...}
위 코드에서 Set<?>이 비한정적 와일드카드 타입이다. 처음에 넣은 타입 외에는 다른 원소를 넣을 수 없다. 즉, Raw Type보다 Type Safe하다.
로 타입을 허용하는 예외
1. class 리터럴에는 Raw Type을 써야 한다.
클래스 리터럴(Class Literal)은 클래스나 인터페이스의 정보를 담고 있는 객체이다. 이 객체는 해당 클래스나 인터페이스의 메타 정보를 가져오기 위해 사용된다.
에를 들어, String.class는 String 클래스의 메타 정보를 담고 있는 Class<String> 객체를 반환하며, Integer.class는 Integer 클래스의 메타 정보를 담고 있는 Class<Integer> 객체를 반환한다.
자바 명세는 매개변수화 타입을 사용하는 클래스 리터럴을 허용하지 않는다. (배열과 기본 타입은 허용한다.)
예를 들어 List.class, String[].class, int.class는 허용하고 List<String>.class, List<?>.class는 허용하지 않는다.
이는 제네릭 타입에 대한 클래스 리터럴을 지원하지 않으며, 제네릭 타입은 컴파일 시점에서 타입 소거에 의해 제거되기 때문이다.
따라서, 제네릭 타입을 사용하는 클래스나 인터페이스를 다룰 때는, 리터럴 대신에 타입 토큰(Type Token)을 사용하는 것이 좋다. 타입 토큰은 클래스나 인터페이스의 정보를 객체로 표현하는 것으로, 클래스 리터럴을 대체한다. 이를 사용하면, 컴파일러가 타입 정보를 가지고 있어, 런타임 시에도 타입 정보를 확인할 수 있다.
2. instanceof 연산자
instanceof 연산자는 런타임에 객체의 타입을 체크한다. 그러나 제네릭에서는 컴파일 타임에만 타입 정보가 유지되며, 런타임에는 타입 소거(type erasure)로 타입 정보가 지워지기 때문에, instanceof 연산자로 제네릭 타입 정보를 체크할 수 없다.
따라서 instanceof 연산자를 사용하는 경우에는 비한정적 와일드카드 타입 이외에는 매개변수화 타입을 사용할 수 없다.
핵심 요약
제네릭은 JDK 5부터 도입된 개념으로, 클래스, 인터페이스, 메서드에서 타입 매개변수를 사용하여 타입 안정성을 확보하고 코드의 유연성을 높일 수 있다. 반면, Raw Type을 사용하면 타입 안정성이 떨어지며, 클라이언트 입장에서 어떤 타입이 들어와야 할지 모호해진다. 이로 인해 비정상적인 인스턴스를 활용해도 컴파일 단계에서 알 수 없고, 실제 동작을 해보고 오류가 발생해야만 확인할 수 있다.
따라서, Raw Type이 아닌 제네릭 타입을 사용하는 것이 좋다. 제네릭 타입을 사용할 때는 반드시 매개변수 타입을 지정하여 사용해야 한다. 그러나 타입을 명확하게 지정할 수 없는 경우에는 비한정적 와일드카드 제네릭을 활용하여 최대한 안정적으로 활용할 수 있다.
제네릭이 나오기 전에는 컴파일러가 잘못된 타입을 체크할 수 없었기 때문에 불필요한 캐스팅이나 강제하는 캐스팅 등이 계속해서 발생하였다. 그러나 제네릭이 도입되면서 컴파일러가 체크할 수 있는 기반이 마련되어, 코드의 안정성과 가독성을 높일 수 있게 되었다.
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 28. 배열보다는 리스트를 사용하라. (0) | 2023.05.09 |
---|---|
[effective java] 아이템 27. Unchecked Warning을 제거하라. (0) | 2023.05.09 |
[effective java] 아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라. (0) | 2023.05.09 |
[effective java] 아이템 17. 변경 가능성을 최소화하라. (0) | 2023.03.25 |
[effective java] 아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라. (0) | 2023.03.25 |