티스토리 뷰
클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제네릭 클래스 혹은 제네릭 인터페이스라 합니다.
예컨대 List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받습니다.
그래서 이 인터페이스의 완전한 이름은 List<E>지만 짧게 그냥 List라고도 자주 씁니다.
제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 합니다.
각각의 제네릭 타입은 일련의 매개변수화 타입을 정의합니다.
먼저 클래스(인터페이스) 이름이 나오고, 이어서 꺾쇠괄호 안에 실제 타입 매개변수들을 나열합니다.
예컨대 List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입입니다.
여기서 String이 정규(formal) 타입 매개변수 E에 해당하는 실제(actual) 타입 매개변수입니다.
마지막으로, 제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의됩니다.
로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말합니다.
예컨대 List<E>의 로 타입은 List입니다.
로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 도래하기 전 코드와 호환되도록 하기 위한 궁여지책이라 할 수 있습니다.
제네릭을 지원하기 전에는 컬렉션을 다음과 같이 선언했습니다.
자바 9에서도 여전히 동작하지만 좋은 예라고 볼 순 없습니다.
// Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;
이 코드를 사용하면 실수로 Stamp 대신 Coin을 넣어도 아무 오류 없이 컴파일되고 실행됩니다.
// 실수로 동전을 넣는다.
stamps.add(new Coin(...)); // "unchecked call" 경고를 던진다.
컬렉션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못합니다.
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다.
stamp.cancel();
}
줄기차게 이야기하듯, 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋습니다.
이 예에서는 오류가 발생하고 한참 뒤인 런타임에야 알아챌 수 있는데, 이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 상당히 떨어져 있을 가능성이 커집니다.
ClassCastException이 발생하면 stamps에 동전을 넣는 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있습니다.
제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아듭니다.
private final Collection<Stamp> stamps = ...;
이렇게 선언하면 컴파일러는 stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 됩니다.
따라서 아무런 경고 없이 컴파일된다면 의도대로 동작할 것임을 보장합니다.
물론 컴파일러 경고를 숨기지 않았어야 합니다.(아이템 27)
이제 stamps에 엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생하며 무엇이 잘못됐는지를 정확히 알려줍니다.
Test.java:9 : error : incompatible types: Coin cannot be converted
to Stamp
stamps.add(new Coin());
^
컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장합니다.
(이번에도 컴파일러 경고가 나지 않았고 경고를 숨기지도 않았다고 가정했습니다.)
Stamp용 컬렉션에 Coin을 넣는다는 예가 억지스러워 보이겠지만, 현업에서도 종종 일어나는 일입니다.
예컨대 BigDecimal용 컬렉션에 BigInteger를 넣는 실수는 그리 억지 같지 않을 것입니다.
앞에서도 얘기했듯, 로 타입(타입 매개변수가 없는 제네릭 타입)을 쓰는 걸 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안 됩니다.
로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 됩니다.
🤔
그렇다면 절대 써서는 안 되는 로 타입을 애초에 왜 만들어놓은 걸까요?
바로 호환성 때문입니다.
자바가 제네릭을 받아들이기까지 거의 10년이 걸린 탓에 제네릭 없이 짠 코드가 이미 세상을 뒤덮어 버렸습니다.
그래서 기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야만 했습니다.
로 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 (물론 그 반대도) 동작해야만 했던 것입니다.
이 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거(erasure: 아이템 28) 방식을 사용하기로 했습니다.
List 같은 로 타입은 사용해서는 안 되나, List<Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮습니다.
🤔
로 타입인 List와 매개변수화 타입인 List<Object>의 차이는 무엇일까요?
간단히 이야기하자면, List는 제네릭 타입에서 완전히 발을 뺀 것이고, List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것입니다.
매개변수로 List를 받는 메서드에 List<String>을 넘길 수 있지만, List<Object>를 받는 메서드에는 넘길 수 없습니다. 이는 제네릭의 하위 타입 규칙 때문입니다.
즉, List<String>은 로 타입인 List의 하위 타입이지만, List<Object>의 하위 타입은 아닙니다.(아이템 28)
그 결과, 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);
}
이 코드는 컴파일은 되지만 로 타입인 List를 사용하여 다음과 같은 경고가 발생합니다.
Test.java:10 : warning : [unchecked] unchecked call to add(E) as a
member of the raw type List
list.add(o);
^
이 프로그램을 이대로 실행하면 strings.get(0)의 결과를 형변환하려 할 때 ClassCastException을 던집니다.
Integer를 String으로 변환하려 시도한 것입니다.
이 형변환은 컴파일러가 자동으로 만들어준 것이라 보통은 실패하지 않습니다.
하지만 이 경우엔 컴파일러의 경고를 무시하여 그 대가를 치른 것이죠.
이제 로 타입인 List를 매개변수화 타입인 List<Object>로 바꾼 다음 다시 컴파일해봅시다.
이번에는 다음 오류 메시지가 출력되며 컴파일조차 되지 않습니다.
Test.java:5 : error : incompatible types: List<String> cannot be
converted to List<Object>
unsafeAdd(strings, Integer.valueOf(42));
^
이쯤 되면 원소의 타입을 몰라도 되는 로 타입을 쓰고 싶어질 수 있습니다.
예컨대 2개의 집합(set)을 받아 공통 원소를 반환하는 메서드를 작성한다고 해봅시다.
다음은 제네릭을 처음 접하는 사람이 작성할만한 코드입니다.
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1) {
if (s2.contains(o1)) result++;
}
return result;
}
이 메서드는 동작은 하지만 로 타입을 사용해 안전하지 않습니다.
따라서 비한정적 와일드카드 타입을 대신 사용하는 게 좋습니다.
제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용합시다.
예컨대 제네릭 타입인 Set<E>의 비한정적 와일드카드 타입은 Set<?>입니다.
이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입입니다.
다음은 비한정적 와일드카드 타입을 사용해 numElementsInCommon을 다시 선언한 모습이빈다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
...
}
🤔
비한정적 와일드카드 타입인 Set<?>와 로 타입인 Set의 차이는 무엇일까요?
특징을 간단히 말하자면 와일드카드 타입은 안전하고, 로 타입은 안전하지 않습니다.
로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽습니다.
반면, Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없습니다.
다른 원소를 넣으려 하면 컴파일할 때 다음의 오류 메시지를 보게 될 것입니다.
WildCard.java:13: error: imcompatible types: String cannot be
converted to CAP#1
c.add("verboten");
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
보충설명이 필요한 메시지이긴 하지만, 어쨌든 컴파일러는 제 역할을 한 것입니다.
즉, 컬렉션의 타입 불변식을 훼손하지 못하게 막았습니다.
구체적으로는, (null 외의) 어떤 원소도 Collection<?>에 넣지 못하게 했으며 컬렉션에서 꺼낼 수 있는 객체의 타입도 전혀 알 수 없게 했습니다.
이러한 제약을 받아들일 수 없다면 제네릭 메서드(아이템 30)나 한정적 와일드카드 타입(아이템 31)을 사용하면 됩니다.
로 타입을 써야 하는 예외적인 상황들
로 타입을 쓰지 말라는 규칙에도 소소한 예외가 몇 개 있습니다.
class 리터럴에는 로 타입을 써야 합니다.
자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했습니다. (배열과 기본 타입은 허용한다.)
예를 들어 List, class, String[].class, int.class는 허용하고 List<String>.class, List<?>.class는 허용하지 않습니다.
두 번째 예외는 instanceof 연산자와 관련이 있습니다.
런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없습니다.
그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작합니다.
비한정적 와일드카드 타입의 꺾쇠괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만드므로, 차라리 로 타입을 쓰는 편이 깔끔합니다.
다음은 제네릭 타입에 instanceof을 사용하는 올바른 예입니다.
if (o instanceof Set) { // 로 타입
Set<?> s = (Set<?>) o; // 와일드카드 타입
...
}
o의 타입이 Set임을 확인한 다음 와일드카드 타입인 Set<?>로 형변환해야 합니다. (로 타입인 Set이 아닙니다.)
이는 검사 형변환이므로 컴파일러 경고가 뜨지 않습니다.
정리
- 로 타입을 사용하면 런타입에 예외가 일어날 수 있으니 사용하면 안 된다.
- 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
- Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종으 ㅣ 타입 객체만 저장할 수 있는 와일드카드 타입이다.
- 그리고 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다.
- Set<Object>와 Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.
다음 표에서는 이번 아이템에서 사용한 용어를 정리했습니다.
4장 전반에 사용할 용어도 미리 소개합니다.
낯선 용어가 난무해 헷갈릴 때 참고하면 됩니다.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
아이템[28]. 배열보다는 리스트를 사용하라 (0) | 2022.04.13 |
---|---|
아이템[27]. 비검사 경고를 제거하라 (0) | 2022.04.13 |
아이템[25]. 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2022.04.10 |
아이템[24]. 멤버 클래스는 되도록 static으로 만들라 (0) | 2022.04.10 |
아이템[23]. 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2022.04.06 |
- Total
- Today
- Yesterday
- MSA
- 정규표현식
- 백준
- 프로그래머스
- 객체지향
- Algorithm
- Java
- 이팩티브 자바
- 클린 아키텍처
- C++
- kkoon9
- 테라폼
- Spring Boot
- BAEKJOON
- Kotlin
- BOJ
- Olympiad
- JPA
- AWS
- 디자인 패턴
- Effective Java
- 디자인패턴
- node.js
- kotest
- 알고리즘
- 클린 코드
- programmers
- Spring
- 코테
- 이펙티브 자바
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |