이번 아이템부터는 제네릭에 대해 다룬다.
제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때 마다 형변환을 해야했다. 제네릭을 사용함으로써 타입을 사용하는 부분이 많이 편리해지고, 오류를 찾기도 쉬워졌다.
하지만 코드가 복잡해진다는 단점이 있었다.
이번 아이템부터는 제네릭의 이점은 살리면서 단점은 최소화하는 방법들을 다룬다.
로(raw) 타입은 사용하지 말아라
제네릭 타입을 정의하면 그에 딸려서 로 타입(raw Type)이 함께 정의된다.
로 타입이란 뭘까?
로 타입이란?
제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 의미한다.
1
List<E> 의 로 타입은 List 이다.
- 로 타입이 존재하는 이유는 제네릭이 나타나기 전의 코드와 호환되도록 하기 위함이다.
1
2
private final Collection stamps = ...;
stamps.add(new Coin()); //"unchecked call" 경고를 낼 뿐 컴파일러가 잡지 않는다.
- 제네릭 등장 전의 코드이다.
- Collection 이라는 로 타입을 사용했다. 아무런 타입이 없다.
- 이 변수를 선언한 이유는 Stamp 클래스를 저장하기 위함일 것이다. 하지만 해당 Collection에 Coin을 넣더라도 컴파일러는 오류를 내지 않을 것이다.
1
2
3
for (Object o : stamps) {
Stamp stamp = (Stamp) o; //Coin 꺼낼 때 ClassCastException 발생
}
- 컬렉션에서 인스턴스를 꺼낼 때 오류가 발생한다.
- 이 경우 오류가 발생한 곳과, 오류의 원인이 생긴 부분이 동떨어져 있다는 것을 알 수 있다.
- 오류를 찾기 위해 코드 전체를 훑어야 할 수 있다.
제네릭을 사용한다면?
제네릭을 활용하면 당연히 위와 같은 문제는 해결된다.
1
2
Collection<Stamp> stamps = ...
stamps.add(new Coin()); // 컴파일러에서 오류
- Stamp가 아닌 인스턴스를 넣을 때 바로 컴파일 오류가 발생해 무엇이 잘못된건지 확인 가능하다.
로 타입을 사용하고 싶은 경우?
로 타입을 사용하는 것을 언어 차원에서 막아놓지는 않았지만 절대로 써서는 안된다.
로 타입을 사용하면 제네릭이 안겨주는 안정성 및 표현력을 모두 잃게 된다.
로 타입은 제네릭 이전의 코드들과 호환성 때문에 존재하는 것일 뿐이다.
그럼에도 내가 어떤 타입을 사용할 지 모를 때 로 타입을 사용하고 싶을 수 있다.
1
2
3
4
5
6
7
static int method(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1) {
if (s2.contains(o1)) result++;
}
return result;
}
- 위 메서드도 동작은 한다. 하지만 로 타입을 사용했기 때문에 안전하지 않다.
- 이런 경우 비한정적 와일드카드 타입을 쓰면 된다.
비한정적 와일드카드 타입
?를 사용하는 방법이다.
1
static int method(Set<?> s1, Set<?> s2) {...}
- ?를 사용하는 것과 아닌 것의 차이가 무엇인지를 알아야 한다. 결과적으로 차이가 없어보인다.
- 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다.
- 로 타입 컬렉션에는 아무 원소나 넣을 수 있다. 반면 와일드 카드 타입은 null을 제외한 어떤 원소도 넣을 수 없다.
로 타입 컬렉션에는 아무 원소나 넣을 수 있고, 와일드 카드 타입에는 null을 제외한 어떤 원소도 넣을 수 있다는 말이 무슨 말일까?
1
2
3
List<?> list = new ArrayList<>();
// list.add("Hello"); // 컴파일 에러: 원소 추가 불가
// list.add(10);
- 말 그대로 어떤 타입의 원소도 추가할 수 없다.
- 와일드 카드 타입은 다양한 제네릭 타입을 다루기 위한 것으로, 특정 타입의 제한 없이 처리하기 위한 것이다.
- 어떤 타입인지 몰라도 받을 수 있지만 사용 시에는 타입 체크가 되어 안전하다.
로 타입을 쓰는 예외적인 경우
위의 예시도 결국 와일드 카드 타입으로 대체하라는 것이지, 결국 로 타입을 사용하지 말라고 했다.
그럼에도 로 타입을 사용해도 좋은 부분이 있다.
- class 리터럴에는 로 타입을 사용해야 한다.
1
2
3
4
5
6
final Map<Class<?>, Object> map = new HashMap<>();
map.put(String.class, "abc");
map.put(Integer.class, 1);
map.put(String[].class, strings);
map.put(List<Integer>.class, new ArrayList<>()); //컴파일 오류
class 리터럴은 String.class와 같은 것을 말한다.
위 예시에서 보면 자바 언어에서 배열과 기본 타입은 매개변수화 타입을 허용하지만, 컬렉션은 로 타입만을 허용한다.
- instanceof 연산자의 경우
1
2
3
list instanceof List<?>;
list instanceof List;
런타임에는 제네릭 타입 정보가 지워져 instanceof 연산자는 로 타입이든 와일드카드 타입이든 동일하게 동작한다.
오히려 와일드 카드 타입은 가독성을 떨어뜨리므로 로 타입을 사용하는 것이 좋다.
정리
로 타입은 웬만해서는 사용하면 안된다. 제네릭 등장 이전 코드와의 호환성을 위해 제공되는 것일 뿐 안정성이 떨어진다.
어떤 타입인지 모를 때 처리는 와일드 카드 타입을 사용하면 된다.
로 타입을 사용하는 경우는 instanceof 연산자를 사용할 때나 class 리터럴을 사용할 때 둘 뿐이다.