1. 제너릭이란? (Generic type)
클래스 내부에서 사용할 데이터 타입을 나중에 인스턴스를 생성할 때 확정하는 것.
클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제너릭 클래스 혹은 제너릭 인터페이스라 한다.
ex) List 인터페이스
1
2
3
public interface List<E> extends Collection<E> {
...
}
원소의 타입을 나타내는 타입 매개변수 E를 받는다. 따라서 이 인터페이스의 완전한 이름은 List<E>
지만 짧게 그냥 List라고 자주 쓴다. 제너릭 클래스 + 제너릭 인터페이스를 통틀어 제너릭 타입이라 한다.
제너릭 타입은 매개변수화 타입을 정의한다.
클래스 이름(혹은 인터페이스)<실제 타입 매개변수들 나열>
- List
- 원소의 타입이 String인 List를 뜻하는 매개변수화 타입
- String: 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수
제너릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.
로타입: 제너릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때
ex) List<E>
의 로타입은 List
- 로타입은 타입선언에서 제너릭 타입 정보가 전부 지워진 것 처럼 동작한다.
- 제너릭이 도래하기 전 코드와 호환되도록 하기 위해 생겼다.
제너릭을 지원하기 전 raw type의 사용
1
2
3
4
5
6
7
8
9
10
11
//Stamp 인스턴스만 취급하는 Collection 로타입 선언
private final Collection stamps = ...;
//실수로 동전을 넣은 경우 "Unchecked call" 경고만 내뱉고 오류없이 컴파일된다.
stamps.add(new Coin(...));
//이렇게 동전을 꺼내기 전에는 오류를 알아채지 못한다.
for(Iterator i = stamps.iterator(); i.hasNext();){
Stamp stamp = (Stamp)i.next(); //ClassCastException을 던진다.
stamp.cancel();
}
- 단점
- 오류가 런타임시에 발견된다. 좋은 코드는 오류가 발생하기 전 컴파일 즉시 발견할 수 있는 코드다.
- 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 떨어져있을 가능성이 커진다.
- 로타입을 쓰면 제너릭이 안겨주는 안전성(typesafe)과 표현력을 모두 잃는다. 절대로 써서는 안된다.
- 오류가 런타임시에 발견된다. 좋은 코드는 오류가 발생하기 전 컴파일 즉시 발견할 수 있는 코드다.
잘못된 예
- 모르는 타입의 원소도 받는 로타입 사용
1 2 3 4 5 6 7 8
//2개의 집합(Set)을 받아 공통 원소의 수를 반환하는 메서드 static int numElementsInCommon(Set s1,Set s2){ int result = 0; for (Object o1 : s1) if(s2.contains(o1)) result++; return result; }
위 소스는 정상 작동하나 로 타입을 사용해 안전하지 않다.
→ 비한정적 와일드카드 타입(unbounded wildcard type)을 사용하자!
제너릭을 활용하면 매개변수화 타입을 주석이 아닌 타입 선언 자체에 담을 수 있다.
매개변수화 타입: stamps에는 Stamp만 들어간다는 정보
1
2
3
4
5
6
7
8
9
10
//매개변수화된 컬렉션 타입 - 타입 안전성 확보!
private final Collection<Stamp> stamps = ...;
//컴파일 오류 발생!
stampsC.add(new Coin(...));
for(Iterator i = stamps.iterator(); i.hasNext();){
Stamp stamp = (Stamp)i.next(); //여기서 에러나지 않음을 컴파일러가 보장한다.
stamp.cancel();
}
List 같은 로타입은 사용하면 안되지만 List<Object>
사용은 된다.
- 차이점
- List
- 제너릭 타입에서 완전히 발을 뺀 것
List<Object>
- 모든 타입의 객체를 허용한다는 의사를 컴파일러에 전달
- List
제너릭의 하위 타입 규칙
매개변수 List를 받는 메서드에 List<String>
을 넘길 수 있다.
그러나 List<Object>
를 받는 메서드에 List<String>
를 넘길 수 없다.
→ List<String>
: 로타입 List의 하위타입, List<Object>
의 하위타입은 아님
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
addList(strings, Integer.valueOf(42)); //매개변수 List를 받는 메서드에 `List<String>`을 넘길 수 있다.
safeAddList(strings, Integer.valueOf(42)); //컴파일 에러! List<String>은 List<Object>의 하위타입이 아니다.
String s = strings.get(0);//class java.lang.Integer cannot be cast to class java.lang.String
}
private static void addList(List list, Object o){
list.add(o);//컴파일러 경고 발생: 매개변수화된 클래스 List의 raw type 사용
}
private static void safeAddList(List<Object> list, Object o){
list.add(o);
}
2. 비한정적 와일드카드 타입(unbounded wildcard type)
제너릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때 로타입이 아닌 물음표(?)를 사용하자.
?는 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set타입이다.
1
2
3
4
//좋은 예: 비한정적 와일드카드 타입(unbounded wildcard type) 사용
static int numElementsInCommon(Set<?> s1,Set<?> s2){
...
}
Set VS Set<?>
- 로 타입
Set
- 불안전함 - 아무 원소나 넣을 수 있어 타입 불변식을 훼손하기 쉽다.
- 비한정적 와일드카드 타입(Unbounded wildcard type)
Set<?>
Set<E>
의 비한정적 와일드카드 타입은Set<?>
이다.- 안전함 - Collection<?>에는 null제외 어떤 원소도 넣을 수 없다.
- 다른 원소를 add하려 하면 컴파일타임에 오류메시지를 볼 수 있다.
- 컬렉션의 타입 불변식을 훼손하지 못하게 막고, 컬렉션에서 꺼낼 수 있는 객체의 타입도 알 수 없게 한다.
3. 로타입 규칙 예외
1) class 리터럴에는 로타입을 사용한다.
자바 명세가 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.
- 허용: List.class, String[].class, int.class
- 불가:
List<String>.class
,List<?>.class
- 클래스 리터럴(class literal)
- 클래스 리터럴(Class Literal)은
String.class
,Integer.class
등을 말하며,String.class
의 타입은Class<String>
,Integer.class
의 타입은Class<Integer>
다. - 타입 토큰(Type Token)은 쉽게 말해 타입을 나타내는 토큰이며, 클래스 리터럴이 타입 토큰으로서 사용된다.
myMethod(Class<?> clazz)
와 같은 메서드는 타입 토큰을 인자로 받는 메서드이며,method(String.class)
로 호출하면,String.class
라는 클래스 리터럴을 타입 토큰 파라미터로 myMethod에 전달한다.
- 클래스 리터럴(Class Literal)은
2) instance of 연산자는 로타입을 사용한다.
런타임에는 제너릭 타입 정보가 지워진다.
instance of 연산자는 비한정적 와일드카드 타입(Set<?>
) 이외의 매개변수화 타입(Set<String>
)에는 적용할 수 없다.
로타입과 비한정적 와일드 카드 타입의 instance of는 완전히 똑같이 동작한다. <?>는 코드만 지저분해지므로 차라리 로타입을 쓰자.
1
2
3
if(o instance of Set){ //로 타입
Set<?> s = (Set<?>)o; //와일드카드 타입
}
o의 타입이 Set임을 확인한 다음 와일드 카드인 Set<?>
로 형변환 해야 한다.
이는 검사형변환(checked cast)이므로 컴파일러 경고가 뜨지 않는다.
결론
- 로 타입을 사용하면 런타임에 예외가 일어날 수 있으므로 사용하지 마라
- 로 타입은 제너릭이 도입되기 이전 코드와의 호환성을 위해 제공된 것이다.
Set<Obejct>
: 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입. 안전함.Set<?>
: 어떤 종류의 타입 객체만 저장할 수 있는 와일드카드 타입. 안전함.- Set: 로타입. 제너릭 타입 시스템에 속하지 않음. 안전하지 않음.
제너릭 관련 용어정리
1
2
3
public interface List<E> extends Collection<E> {
private E member;
}
한글용어(영문용어) | 설명 | 예 |
---|---|---|
매개변수화 타입(parameterized type) | 각각의 제네릭 타입은 parameterizedType을 선언함 | List<String> |
실제 타입 매개변수(actual type parameter) | String | |
제네릭 타입(Generic Type) | 제네릭 클래스와 제네릭 인터페이스를 통틀어 이르는 말 | List<E> |
정규 타입 매개변수(formal Type parameter) | 제네릭 선언에 사용된 매개변수 | E |
비한정적 와일드카드 타입(unbounded wildcard type) | 어떤 종류의 타입 객체만 저장할 수 있는 와일드카드 타입 | List<?> |
로타입(raw type) | 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않았을 때 | List |
한정적 타입 매개변수(bounded Type parameter) | <E extends Number> | |
재귀적 타입 한정(recursive Type parameter) | <T extends Comparable<T>> | |
한정적 와일드카드 타입(bounded wildcard type) | List<? extends Number> | |
제너릭 메서드(generic method) | static <E> List<E> asList(E[] a) | |
타입 토큰(type token) | String.class |