먼저 제네릭 클래스, 제네릭 인터페이스란, 클래스와 인터페이스 선언에 타입 매개변수를 의미한다. 우리가 흔히 사용하는 List<E>는 List라고 사용하며, 이러한 제네릭 클래스와 제네릭 인터페이스를 제네릭 타입이라고 한다.
List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다. (여기서 정규 타입 매개변수 E 가 String인 실제 타입 매개변수이다)
여기서 제네릭타입을 하나 정의하면 로 타입도 함께 정의한다. List<E>의 로 타입은 List다.
private final Collection stamps = ...;
Stamp를 넣기위한 stamps를 정의하였지만, stamps를 이렇게 정의하면 Coin을 넣으도 아무 오류 없이 컴파일되고 실행된다.
stamps.add(new Coin(...)); //"unchecked call" 경고를 내밷는다.
이렇게 넣을 때는 아무 문제 없지만, 꺼낼때서야 오류를 알 것이다.
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다.
stamp.cancel();
}
이렇게 넣을때 확인하지 못하고 꺼낼 때서야 잘못된 것을 알게 된다면, 좋지 않은 코드이며, 이상적으로는 컴파일할 때 발겨하는 것이 좋다.
따라서,
private final Collection<Stamp> stamps = ...;
이렇게 Stamp만 넣을 수 있음을 선언해야 컴파일러가 인지하게 된다. <Stamp>가 없어도 동작은 한다. 이 이유는 호환성 때문이다. 이미 기존에 만들어진 코드들이 동작을 하기 위해서는 로 타입의 코드도 동작을 해야하기 때문이다. 그러나 앞으로의 코드에서 이렇게 로 타입으로 소스를 작성한다면, 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다.
List같은 로 타입은 안되나, List<Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.
이 둘의 차이는
List : 전혀 제네릭 타입과 관련 X
List<Object> : 모든 타입을 허용한다는 의미를 정확히 전달하는 제네릭 타입
List<String> : 정확히 String을 받는 제네릭 타입.
List를 받는 메서드에 넘길 수 있음.
List<Object>를 받는 메서드에는 넘길 수 없음. -> 제네릭의 하위타입 규칙 때문
=> List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List같은 로 타입을 사용하면 타입 안정성을 잃게 됨.
public staic 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);
}
이 코드의 문제는 마지막에 있다. 컴파일은 된다. 그러나 list.add(o)에서 list가 받은 매개변수가 로타입이기 때문이다. unsafeAdd를 거치면서 LIst list가 제네릭 타입이 아닌 로타입이기 때문에 strings.get(0)을 하면서 Integer를 String으로 변환하려 시도하지만 이 경우에는 실패한다. list.add(o)에서 컴파일 경고를 처리하지 않았기 때문이다.
그러나 List를 List<Object>로 바꾼다면 아예 컴파일조차 되지 않는다.
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1)
if (s2.contains(o1))
result++;
return result;
}
위의 소스는 안전하지 않다. 그 이유는 로 타입을 사용했기 때문이다. 하지만 제네릭 타입으로 바꾸고 싶어도 실제 타입 매개변수가 무엇일지 알지 못한다면, ? 를 사용하자. ?는 비한정 와일드카드 타입( unbounded wildcard type)이다.
예를들은 Set<E>의 비한정 와일드 카드 타입은 Set<?> 이다.
staic int numElementsInCommon(Set<?> s1, Set<?> s2) {...}
이렇게 된다면, 로타입으로 인한 타입 불변식의 훼손이 일어나는 것을 막을 수 있다. Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없다.
몇가지 예외는 있다.
1. class 리터럴에는 로 타입을 써야한다. 자바 명세는 class리터럴에 매개변수화 타입을 사용하지 못하게 했다. (ex, List.class, String[].class, int.class는 허용 / List<String>.class, List<?>.class는 허용 x)
2. instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. 런타임에는 제네릭 타입정보가 지워지기 때문이다. 또한 로타입, 비한정적 와일드카드 타입, instanceof는 모두 똑같이 동작! 따라서 차라리 그냥 로 타입 쓰는게 좋다.
if (o instanceof Set) { //로 타입
Set<?> s = (Set<?>) o; // 와일드카드 타입
...
}
이렇게 Set<?>으로 형변환해야 검사 형변환이므로 컴파일러 경고가 뜨지 않는다.
※ 참고
+ 스터디 이후 추가 정리
위 글에서 매개변수에서 List list 대신 List<E> list 와 List <?> list를 쓸 수 있는지에 대한 궁금증이 생겼다.
일단, 결론적으로 여기서 List <E> list 를 쓸 수가 없다! E가 무엇인지 알 수 었기 때문이다.
대체로 E는 클래스를 생성할 때와 같은 경우에서 사용한다. 여기서처럼 E를 알지 못할때에는 쓸 수 없다.
private static void unsafeAdd(List<?> list, Object o) {
list.add(o);
}
만약 List <E> list를 쓰고자 한다면,
//1.
pulic <E extends Temp> void tmp (List<E> list, Object obj){
}
//2.
public static void tmp (List <E extends Temp> list, Object obj){
}
처럼 한정지어질 수 있을 경우 사용이 가능하다. 타입이 정해지지 않은 경우라면 ?를 사용해야한다.
두번째로, List<Objec> list가 List<String> list 를 받지 못하는 경우는 당연하다.
예를들어 TempChild라는 Temp 클래스의 자식 클래스가 있을 경우에도 TempChild 클래스는 Temp클래스로 넘길 수 없다.
제네릭에서 타입을 명시하는 이유 자체가 타입을 정하기 위해 생긴 것이기 때문이다!
'자바' 카테고리의 다른 글
(이펙티브 자바) 아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2020.09.29 |
---|---|
(이펙티브 자바) 아이템 27. 비검사 경고를 제거하라 (0) | 2020.09.08 |
(이펙티브 자바) 아이템 24.멤버 클래스는 되도록 static으로 만들어라. (0) | 2020.09.07 |
(이펙티브 자바) 아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2020.08.31 |
(이펙티브 자바) 아이템 20. 추상클래스보다는 인터페이스를 우선하라 (1) | 2020.08.31 |