이펙티브 자바 완벽 공략 2부
아이템 26. 로 타입은 사용하지 말라.
아이템 26. 로 타입은 사용하지 말라
- 아이템 26. 로 타입은 사용하지 말라
- 아이템 26. 핵심 정리 - 제네릭 용어 정리
- 아이템 26. 로 타입은 사용하지 말라
- 제네릭이 없던 코드부터 살펴보자
- Raw Type에서는 사실상 타입 안전성을 잃는다
- 버그가 발생한 위치와 발견한 위치가 달라진다
- 제네릭을 적용해보자
- 런타임보다 컴파일 타임에 실패하는 것이 좋다
- 형변환도 필요 없어진다
- 제네릭 용어를 이해해보자
- Generic Type
- 타입 매개변수 Type Parameter
- 반드시 E라는 이름을 써야 하는 것은 아니다
- 실제 타입 인수 Type Argument
- Box
는 매개변수화 타입이다 - List를 기준으로 다시 보면
- 타입 매개변수가 실제 타입으로 결정되면
- 그래서 잘못된 타입을 넣을 수 없다
- 한정적 타입 매개변수
- Integer는 사용할 수 있다
- String은 사용할 수 없다
- extends는 클래스 상속에만 사용하는 것이 아니다
- 와일드카드 타입
- 비한정적 와일드카드
- 왜 Box<?>가 필요한가?
- Raw Type과 Box<?>는 다르다
- 와일드카드에서는 아무 값이나 넣을 수 없다
- 와일드카드는 읽는 용도에서 유용하다
- 특정 타입의 Box만 받고 싶다면
- Box
는 Box - 왜 Box
를 Box - 이 특성을 불공변이라고 이해할 수 있다
- 아무 Box나 받고 싶다면 Object가 아니라 ?를 사용한다
- Box
- 한정적 와일드카드
- extends를 사용한 와일드카드
- 타입 매개변수와 와일드카드를 구분하자
- 제네릭 용어를 한 번에 정리해보자
- 표로 정리하면
- Raw Type과 Object 타입을 혼동하면 안 된다
- Raw Type은 타입 시스템을 우회한다
- 제네릭은 코드의 의도를 타입으로 표현한다
- 제네릭을 사용하는 이유를 한 흐름으로 정리하면
- 제네릭에서 가장 먼저 기억해야 할 세 가지
- 핵심 정리
- 한 줄 정리
- 아이템 26. 핵심 정리 - 제네릭 용어 정리
아이템 26. 핵심 정리 - 제네릭 용어 정리
아이템 26. 로 타입은 사용하지 말라
Java 5부터 제네릭(Generic)이 도입되었다.
제네릭은 처음 접하면 문법이 복잡하게 느껴질 수 있다.
List<String>
Map<String, Integer>
Box<? extends Number>
List<? super Integer>
<T>, <?>, extends, super 같은 문법이 한꺼번에 등장하기 때문이다.
하지만 제네릭이 Java에 도입된 가장 중요한 목적은 오히려 개발자가 더 편리하고 더 안전하게 코드를 작성하도록 돕는 것이다.
특히 제네릭을 사용하면 다음 두 가지 장점을 얻을 수 있다.
잘못된 타입의 데이터가 들어오는 것을
컴파일 시점에 차단할 수 있다.
+
객체를 꺼낼 때
불필요한 형변환을 줄일 수 있다.
아이템 26의 핵심은 이 장점을 포기하게 만드는 로 타입(Raw Type)을 사용하지 말라는 것이다.
제네릭을 제대로 이해하기 위해서는 먼저 제네릭에서 사용하는 용어들을 정리할 필요가 있다.
제네릭이 없던 코드부터 살펴보자
다음 리스트가 있다고 하자.
List numbers = new ArrayList();
타입 매개변수를 지정하지 않았다.
이것이 바로 로 타입(Raw Type) 을 사용한 형태다.
List 자체는 제네릭 타입이다.
원래 선언을 보면 개념적으로 다음과 같다.
public interface List<E> {
...
}
E라는 타입 매개변수를 받을 수 있도록 만들어져 있다.
그런데 사용하는 쪽에서
List numbers
처럼 타입을 지정하지 않았다.
이것이 Raw Type이다.
Raw Type에서는 사실상 타입 안전성을 잃는다
다음 코드를 보자.
List numbers = new ArrayList();
numbers.add(10);
numbers.add(20);
numbers.add("30");
컴파일러는 이를 막지 못한다.
Integer도 객체이고 String도 객체이기 때문에 서로 다른 타입의 값이 같은 컬렉션 안에 들어갈 수 있다.
결과적으로 리스트의 상태가 다음과 같이 될 수 있다.
[
Integer,
Integer,
String
]
문제는 값을 꺼낼 때 발생한다.
개발자는 이 리스트에 Integer만 들어 있다고 생각할 수 있다.
Integer number =
(Integer) numbers.get(2);
하지만 실제 세 번째 값은 "30"이라는 String이다.
따라서 실행 시점에 문제가 발생한다.
Runtime
↓
ClassCastException
즉 잘못된 코드는 이미 다음 순간에 만들어졌다.
numbers.add("30");
하지만 실제 문제를 발견하는 시점은 훨씬 뒤다.
(Integer) numbers.get(2);
버그가 발생한 위치와 발견한 위치가 달라진다
이것이 Raw Type의 큰 문제 중 하나다.
버그 발생
numbers.add("30");
↓
여러 코드 실행
↓
문제 발견
(Integer) numbers.get(...)
실제 프로젝트에서는 두 코드가 서로 멀리 떨어져 있을 수도 있다.
Service A
→ 잘못된 값 삽입
Repository
→ 전달
Service B
→ 조회
Controller
→ 형변환
Runtime Error
문제가 늦게 발견될수록 원인을 추적하기 어려워진다.
제네릭을 적용해보자
이 리스트가 Integer만 다룬다는 사실을 선언한다.
List<Integer> numbers =
new ArrayList<>();
이제 다음 코드는 정상이다.
numbers.add(10);
numbers.add(20);
하지만 다음 코드는 컴파일되지 않는다.
numbers.add("30");
컴파일러는 이미 numbers가
List<Integer>
라는 것을 알고 있기 때문이다.
따라서 add() 역시 개념적으로 다음 타입을 요구한다.
add(Integer value)
String을 전달하면 즉시 오류가 발생한다.
Compile Time Error
런타임보다 컴파일 타임에 실패하는 것이 좋다
두 상황을 비교해보자.
Raw Type
잘못된 데이터 입력
↓
컴파일 성공
↓
프로그램 실행
↓
특정 코드 실행
↓
ClassCastException
Generic
잘못된 데이터 입력
↓
컴파일 실패
↓
즉시 수정
가능하다면 문제는 일찍 발견되는 것이 좋다.
제네릭은 타입과 관련된 많은 실수를 실행 시점에서 컴파일 시점으로 앞당겨준다.
형변환도 필요 없어진다
제네릭이 없으면 값을 꺼낼 때 타입 정보를 알 수 없다.
List numbers =
new ArrayList();
numbers.add(10);
get() 결과는 타입 안전하게 Integer라는 보장이 없으므로 형변환이 필요해진다.
Integer number =
(Integer) numbers.get(0);
반면 제네릭을 사용하면
List<Integer> numbers =
new ArrayList<>();
numbers.add(10);
컴파일러가 리스트의 요소 타입을 알고 있다.
따라서 바로 사용할 수 있다.
Integer number =
numbers.get(0);
정리하면 제네릭의 대표적인 장점은 다음과 같다.
타입 안전성 향상
+
불필요한 형변환 제거
제네릭 용어를 이해해보자
제네릭을 공부하다 보면 비슷해 보이는 용어가 많이 등장한다.
대표적으로 다음과 같다.
Generic Type
Type Parameter
Type Argument
Parameterized Type
Bounded Type Parameter
Wildcard Type
Bounded Wildcard Type
Raw Type
처음에는 모두 비슷하게 들리지만 타입을 정의하는 쪽과 사용하는 쪽을 나누어 생각하면 이해하기 쉽다.
Generic Type
다음 클래스를 만들어보자.
public class Box<E> {
private E value;
public void set(E value) {
this.value = value;
}
public E get() {
return value;
}
}
이 클래스는 특정 타입 하나에 고정되어 있지 않다.
Integer Box
String Box
Member Box
Order Box
등 다양한 타입으로 사용할 수 있다.
이처럼 타입 매개변수를 선언한 클래스나 인터페이스를 제네릭 타입(Generic Type) 이라고 한다.
클래스라면 좀 더 구체적으로 제네릭 클래스(Generic Class) 라고 부를 수 있다.
class Box<E>
인터페이스라면 다음과 같은 형태다.
interface Repository<E>
타입 매개변수 Type Parameter
다음 코드에서
public class Box<E> {
}
E가 타입 매개변수(Type Parameter) 다.
E라는 실제 클래스가 존재하는 것은 아니다.
이것은 일종의 자리 표시자다.
"아직 정확한 타입은 모르겠지만
그 타입을 여기에서는 E라고 부르겠다."
라는 의미다.
따라서 다음과 같이 사용할 수 있다.
private E value;
public void set(E value) {
this.value = value;
}
public E get() {
return value;
}
E가 어떤 타입인지 클래스 정의 시점에는 결정되어 있지 않다.
반드시 E라는 이름을 써야 하는 것은 아니다
다음 코드도 문법적으로 가능하다.
public class Box<D> {
private D value;
}
심지어
public class Box<ABC> {
}
도 가능하다.
하지만 제네릭에는 일반적으로 사용하는 관례가 있다.
대표적으로 다음 이름을 자주 사용한다.
| 이름 | 일반적인 의미 |
|---|---|
T | Type |
E | Element |
K | Key |
V | Value |
N | Number |
R | Return/Result |
예를 들어 List는 요소를 다루기 때문에 흔히 E를 사용한다.
List<E>
Map은 Key와 Value를 사용한다.
Map<K, V>
이런 관례를 따르면 코드를 이해하기가 훨씬 쉽다.
실제 타입 인수 Type Argument
Box<E>를 실제로 사용해보자.
Box<Integer> box =
new Box<>();
여기에서
Integer
가 실제로 전달된 타입이다.
이를 타입 인수(Type Argument) 라고 한다.
즉 다음 관계다.
정의할 때
Box<E>
↑
Type Parameter
사용할 때
Box<Integer>
↑
Type Argument
E라는 자리에 실제 Integer 타입이 들어갔다고 이해하면 쉽다.
Box는 매개변수화 타입이다
다음 전체 타입은
Box<Integer>
매개변수화 타입(Parameterized Type) 이라고 한다.
즉 Box라는 제네릭 타입이 Integer라는 실제 타입 인수로 매개변수화된 것이다.
정리하면 다음과 같다.
Box<E>
→ Generic Type 선언
E
→ Type Parameter
Integer
→ Type Argument
Box<Integer>
→ Parameterized Type
List를 기준으로 다시 보면
우리가 매일 사용하는 List에도 동일하게 적용된다.
List<String>
이를 분해하면 다음과 같다.
List<E>
→ Generic Type
E
→ Type Parameter
String
→ Type Argument
List<String>
→ Parameterized Type
타입 매개변수가 실제 타입으로 결정되면
다음 제네릭 클래스를 생각해보자.
public class Box<E> {
private E value;
public void set(E value) {
this.value = value;
}
public E get() {
return value;
}
}
그리고 다음과 같이 사용한다.
Box<Integer> box =
new Box<>();
개념적으로는 E가 Integer로 결정된 것처럼 생각할 수 있다.
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
실제 Java의 제네릭 구현 방식은 타입 소거 등의 내용과 연결되므로 내부적으로 정말 별도의 Box<Integer> 클래스가 만들어진다는 뜻은 아니다.
하지만 소스 코드를 이해하는 단계에서는
E 자리에 Integer가 들어간다.
라고 생각하면 편하다.
그래서 잘못된 타입을 넣을 수 없다
Box<Integer> box =
new Box<>();
이제 다음은 가능하다.
box.set(100);
하지만 다음은 불가능하다.
box.set("hello");
Box<Integer>에서 set()이 요구하는 타입은 Integer이기 때문이다.
한정적 타입 매개변수
지금까지의 E에는 사실 거의 모든 참조 타입을 사용할 수 있다.
Box<String>
Box<Integer>
Box<Member>
Box<Object>
그런데 때로는 사용할 수 있는 타입을 제한하고 싶을 수 있다.
예를 들어 숫자 타입만 받고 싶다고 생각해보자.
이럴 때 한정적 타입 매개변수(Bounded Type Parameter) 를 사용할 수 있다.
public class NumberBox<E extends Number> {
private E value;
public void set(E value) {
this.value = value;
}
public E get() {
return value;
}
}
핵심은 다음 부분이다.
E extends Number
의미는 다음과 같다.
E는 Number 또는
Number의 하위 타입이어야 한다.
Integer는 사용할 수 있다
Integer는 Number의 하위 클래스다.
따라서 다음은 가능하다.
NumberBox<Integer> box =
new NumberBox<>();
Double도 가능하다.
NumberBox<Double> box =
new NumberBox<>();
대략적인 관계는 다음과 같다.
Number
/ \
Integer Double
String은 사용할 수 없다
String은 Number 계층과 관계가 없다.
따라서 다음 코드는 컴파일되지 않는다.
NumberBox<String> box =
new NumberBox<>();
즉 타입 매개변수의 범위를 제한한 것이다.
<E>
↓
거의 모든 참조 타입 허용
<E extends Number>
↓
Number 계층으로 제한
이것이 한정적 타입 매개변수다.
extends는 클래스 상속에만 사용하는 것이 아니다
제네릭 문법에서
<T extends SomeType>
의 extends는 조금 넓은 의미를 가진다.
SomeType이 클래스일 수도 있고 인터페이스일 수도 있다.
예를 들어 다음처럼 사용할 수 있다.
<T extends Comparable<T>>
따라서 제네릭 문맥에서는
extends
=
상위 경계(upper bound)를 지정한다.
정도로 이해하는 것이 좋다.
와일드카드 타입
이번에는 사용하는 쪽에서 다음과 같은 타입을 볼 수 있다.
Box<?>
?를 와일드카드(Wildcard) 라고 한다.
다음 의미로 생각할 수 있다.
정확히 어떤 타입인지는 모르겠지만
어떤 타입으로 매개변수화된 Box다.
즉
Box<Integer>
도 받을 수 있고
Box<String>
도 받을 수 있고
Box<Member>
도 받을 수 있다.
비한정적 와일드카드
다음처럼 extends나 super 없이 ?만 사용하는 것을 비한정적 와일드카드 타입(Unbounded Wildcard Type) 이라고 한다.
Box<?>
예를 들어 다음 메서드를 작성할 수 있다.
public static void printBox(
Box<?> box
) {
System.out.println(box.get());
}
이 메서드는 다양한 Box를 받을 수 있다.
Box<Integer> integerBox =
new Box<>();
Box<String> stringBox =
new Box<>();
printBox(integerBox);
printBox(stringBox);
왜 Box<?>가 필요한가?
메서드 입장에서 다음과 같은 요구가 있다고 하자.
"Box이기만 하면 된다.
안에 Integer가 들었든
String이 들었든 상관없다."
이런 요구를 표현하는 타입이
Box<?>
다.
Raw Type과 Box<?>는 다르다
다음 두 코드는 비슷해 보인다.
Box box
Box<?> box
하지만 의미가 다르다.
첫 번째는 Raw Type이다.
Box box
제네릭 타입 정보를 포기한 형태다.
반면 두 번째는
Box<?> box
제네릭 타입 시스템 안에서
"정확한 타입은 모르겠다."
라고 표현한 것이다.
따라서 아무 종류의 Box를 받고 싶다면 Raw Type 대신 다음을 사용하는 것이 더 안전하다.
Box<?>
와일드카드에서는 아무 값이나 넣을 수 없다
다음 코드를 생각해보자.
Box<?> box =
new Box<Integer>();
box가 실제로 Box<Integer>라는 사실은 오른쪽에서 확인할 수 있다.
하지만 변수의 정적 타입은
Box<?>
다.
컴파일러 입장에서는 ?가 정확히 어떤 타입인지 알 수 없다.
다음일 수도 있다.
Integer
String
Member
Order
따라서 다음처럼 값을 넣게 허용하면 위험하다.
box.set("hello");
만약 실제 객체가 Box<Integer>라면 타입 안전성이 깨진다.
그래서 Box<?>에는 일반적인 구체 타입의 값을 마음대로 넣을 수 없다.
다만 정확히 말하면 null은 참조 타입의 공통 값이므로 별개의 예외적인 경우가 될 수 있다. 핵심은 구체적인 타입의 객체를 안전하게 넣을 수 없다는 것이다.
와일드카드는 읽는 용도에서 유용하다
Box<?>에서 값을 꺼낸다고 해보자.
Object value =
box.get();
정확한 타입은 모르지만 적어도 모든 참조 타입은 Object이므로 Object로 읽어낼 수 있다.
Box<?>
↓
정확한 타입은 모름
↓
Object 수준에서는 읽을 수 있음
그래서 단순히 내용을 확인하거나 출력하는 메서드라면 유용하다.
public static void printBox(
Box<?> box
) {
System.out.println(box.get());
}
특정 타입의 Box만 받고 싶다면
이번에는 String만 담는 Box를 받는다고 해보자.
public static void printStringBox(
Box<String> box
) {
System.out.println(box.get());
}
다음 호출은 가능하다.
Box<String> stringBox =
new Box<>();
printStringBox(stringBox);
하지만 다음은 불가능하다.
Box<Integer> integerBox =
new Box<>();
printStringBox(integerBox);
메서드가 요구하는 것은
Box<String>
이지만 전달한 타입은
Box<Integer>
이기 때문이다.
Box는 Box
제네릭을 처음 공부할 때 가장 헷갈리는 부분 중 하나다.
Integer는 Object의 하위 타입이다.
Object
↑
Integer
그렇다면 다음 관계도 성립할 것처럼 보인다.
Box<Object>
↑
Box<Integer>
하지만 그렇지 않다.
Box<Integer>
≠ subtype of
Box<Object>
두 타입은 서로 다른 매개변수화 타입이다.
왜 Box를 Box
만약 이것이 가능하다고 가정해보자.
Box<Integer> integerBox =
new Box<>();
Box<Object> objectBox =
integerBox;
objectBox는 Box<Object>이므로 다음 코드가 허용되어야 한다.
objectBox.set("hello");
하지만 실제 객체는
Box<Integer>
다.
결과적으로 Integer만 담아야 하는 Box에 String이 들어간다.
Box<Integer>
↓
"hello" 삽입
제네릭 타입 안전성이 깨진다.
그래서 Java는 처음부터 다음 대입을 허용하지 않는다.
Box<Object> objectBox =
integerBox;
이 특성을 불공변이라고 이해할 수 있다
Java 제네릭의 일반적인 매개변수화 타입은 이러한 관계에서 불공변(invariant) 하다.
즉
Integer <: Object
라고 해서
Box<Integer> <: Box<Object>
가 되는 것은 아니다.
이 특성을 이해하지 못하면 와일드카드를 공부할 때 혼란스러워진다.
아무 Box나 받고 싶다면 Object가 아니라 ?를 사용한다
다음 메서드를 보자.
public static void printBox(
Box<Object> box
) {
}
이 메서드는 이름만 보면 아무 객체를 담은 Box를 받을 수 있을 것처럼 보인다.
하지만 실제 의미는
정확히 Object 타입으로 매개변수화된 Box
다.
따라서
Box<Integer>
를 받을 수 없다.
Box<String>
도 받을 수 없다.
아무 타입의 Box를 받고 싶다면 다음과 같이 표현해야 한다.
public static void printBox(
Box<?> box
) {
}
Box
| 타입 | 의미 |
|---|---|
Box<Object> | Object를 요소 타입으로 사용하는 Box |
Box<?> | 요소 타입이 무엇인지 알 수 없는 임의의 Box |
Box | 타입 정보를 버린 Raw Type |
예를 들어 다음 Box들이 있다고 하자.
Box<Integer>
Box<String>
Box<Member>
Box<?>는 모두 받을 수 있다.
Box<Integer> ─┐
Box<String> ─┼→ Box<?>로 참조 가능
Box<Member> ─┘
하지만 Box<Object>는 다르다.
Box<Integer> → Box<Object> X
Box<String> → Box<Object> X
한정적 와일드카드
와일드카드에도 범위를 지정할 수 있다.
예를 들어 숫자 계열의 Box만 받고 싶다고 하자.
Box<? extends Number>
의미는 다음과 같다.
Number 또는
Number의 어떤 하위 타입을 사용하는 Box
따라서 다음은 가능하다.
Box<Integer>
Box<Double>
Box<Long>
하지만 다음은 허용되지 않는다.
Box<String>
extends를 사용한 와일드카드
Box<? extends Number>
를 한정적 와일드카드 타입(Bounded Wildcard Type) 이라고 할 수 있다.
다음과 같이 사용할 수 있다.
public static void printNumberBox(
Box<? extends Number> box
) {
Number number = box.get();
System.out.println(number);
}
Integer, Double 등 무엇이 들어 있는지는 정확히 모르지만 적어도 Number 계열이라는 사실은 알고 있다.
그래서 값을 Number로 읽을 수 있다.
타입 매개변수와 와일드카드를 구분하자
다음 두 문법을 혼동하기 쉽다.
<T>
<?>
둘 다 아직 정확한 타입을 모르고 있는 것처럼 보인다.
하지만 역할이 다르다.
타입 매개변수
<T>
타입에 이름을 붙인다.
예를 들어
public static <T> T getFirst(
List<T> list
) {
return list.get(0);
}
여기서는 T라는 타입을 여러 위치에서 다시 사용할 수 있다.
입력 List<T>
↓
출력 T
타입 사이의 관계를 표현할 수 있다.
와일드카드
<?>
타입 이름 자체가 중요하지 않을 때 사용할 수 있다.
public static int size(
List<?> list
) {
return list.size();
}
여기서는 리스트 안에 무엇이 들어 있는지 알 필요가 없다.
"어떤 타입이든 List면 된다."
라는 의미다.
제네릭 용어를 한 번에 정리해보자
다음 클래스를 기준으로 보자.
public class Box<E> {
private E value;
}
제네릭 타입
Box<E>
타입 매개변수를 선언하고 있는 클래스다.
타입 매개변수
E
정의할 때 사용하는 타입 변수다.
타입 인수
Integer
다음 코드에서 실제로 전달한 타입이다.
Box<Integer>
매개변수화 타입
Box<Integer>
실제 타입 인수가 적용된 타입이다.
한정적 타입 매개변수
<E extends Number>
타입 매개변수로 사용할 수 있는 범위를 제한한다.
비한정적 와일드카드 타입
Box<?>
정확한 타입을 특정하지 않은 임의의 Box다.
한정적 와일드카드 타입
Box<? extends Number>
특정 범위 안에서 임의의 타입을 나타낸다.
Raw Type
Box
제네릭 타입인데 타입 인수를 사용하지 않은 형태다.
표로 정리하면
| 용어 | 예제 | 의미 |
|---|---|---|
| Generic Type | Box<E> | 타입 매개변수를 선언한 타입 |
| Type Parameter | E | 타입 정의에서 사용하는 타입 변수 |
| Type Argument | Integer | 실제 사용 시 전달하는 타입 |
| Parameterized Type | Box<Integer> | 실제 타입이 지정된 제네릭 타입 |
| Bounded Type Parameter | <E extends Number> | 타입 매개변수 범위 제한 |
| Unbounded Wildcard | Box<?> | 임의의 타입을 가진 Box |
| Bounded Wildcard | Box<? extends Number> | 범위가 제한된 임의 타입 |
| Raw Type | Box | 타입 인수를 생략한 제네릭 타입 |
Raw Type과 Object 타입을 혼동하면 안 된다
다음 두 코드는 완전히 같은 의미가 아니다.
List list
List<Object> list
첫 번째는 Raw Type이다.
List list
타입 검사 기능을 상당 부분 포기한다.
반면
List<Object> list
는 Object를 요소 타입으로 사용하는 정상적인 매개변수화 타입이다.
예를 들어 List<Object>에는 여러 객체를 넣을 수 있다.
List<Object> list =
new ArrayList<>();
list.add("hello");
list.add(10);
list.add(new Member());
이것은 제네릭 타입 시스템 안에서 의도적으로
"나는 Object를 담겠다."
라고 선언한 것이다.
Raw Type과는 의미가 다르다.
Raw Type은 타입 시스템을 우회한다
다음 코드를 보자.
List<String> strings =
new ArrayList<>();
List rawList =
strings;
Raw Type을 중간에 끼우면 다음과 같은 위험한 코드가 가능해질 수 있다.
rawList.add(100);
원래 strings는 String만 담아야 한다.
그런데 Raw Type을 거치면서 타입 검사가 약해졌다.
List<String>
↓
Raw List
↓
Integer 삽입
그리고 나중에
String value =
strings.get(0);
같은 코드에서 문제가 터질 수 있다.
이것이 아이템 26에서 Raw Type을 피하라고 강조하는 근본적인 이유다.
제네릭은 코드의 의도를 타입으로 표현한다
다음 코드를 비교해보자.
List values =
new ArrayList();
이 코드만 보면 무엇을 저장할지 알 수 없다.
반면
List<Order> orders =
new ArrayList<>();
는 의도가 명확하다.
"이 리스트는 Order만 다룬다."
컴파일러뿐만 아니라 코드를 읽는 개발자에게도 정보를 제공한다.
즉 제네릭은 단순한 문법 기능이 아니라 API와 코드의 의도를 타입에 기록하는 도구이기도 하다.
제네릭을 사용하는 이유를 한 흐름으로 정리하면
Raw Type에서는 다음 문제가 존재한다.
어떤 타입이 들어갈지 알 수 없음
↓
아무 객체나 삽입 가능
↓
꺼낼 때 형변환 필요
↓
잘못된 타입이라면 Runtime Error
↓
문제 발견이 늦음
제네릭을 적용하면 다음처럼 바뀐다.
요소 타입 명시
↓
잘못된 타입 삽입 차단
↓
Compile Time Error
↓
꺼낼 때 명시적 형변환 불필요
↓
타입 안전성 향상
제네릭에서 가장 먼저 기억해야 할 세 가지
처음 제네릭을 학습한다면 복잡한 와일드카드 규칙보다 먼저 다음 세 가지를 확실하게 이해하는 것이 좋다.
타입을 명시한다
List<String>
Raw Type을 사용하지 않는다.
List
제네릭 타입끼리는 일반적인 상속 관계가 그대로 전파되지 않는다
Integer extends Object
하지만
List<Integer>
does not extend
List<Object>
정확한 타입이 중요하지 않다면 Raw Type 대신 와일드카드를 사용한다
List<?>
즉
List
가 아니라
List<?>
를 고려한다.
이 세 가지를 이해하면 이후 제네릭 아이템들을 훨씬 쉽게 이해할 수 있다.
핵심 정리
- Java의 제네릭은 Java 5에서 도입되었으며 타입 안전성과 코드 편의성을 높이기 위한 기능이다.
- 제네릭을 사용하면 잘못된 타입의 값을 컬렉션 등에 넣는 실수를 컴파일 시점에 발견할 수 있다.
- Raw Type은 제네릭 타입이 제공하는 타입 매개변수를 지정하지 않고 사용하는 형태다.
List는 Raw Type이고List<Integer>는 매개변수화 타입이다.- Raw Type에서는 서로 다른 타입의 객체가 컬렉션에 들어갈 수 있고 문제를 실행 시점까지 발견하지 못할 수 있다.
- 제네릭을 사용하면 잘못된 타입이 들어오는 순간 컴파일 오류가 발생하므로 문제를 더 빨리 발견할 수 있다.
- 제네릭을 사용하면 컬렉션에서 값을 꺼낼 때 불필요한 명시적 형변환도 줄일 수 있다.
Box<E>처럼 타입 매개변수를 선언한 타입을 제네릭 타입이라고 한다.E는 타입을 정의하는 곳에서 사용하는 타입 매개변수다.Box<Integer>에서Integer는 실제로 전달한 타입 인수다.Box<Integer>전체는 매개변수화 타입이다.<E extends Number>처럼 사용할 수 있는 타입 범위를 제한한 것을 한정적 타입 매개변수라고 한다.Box<?>는 정확한 타입은 모르지만 어떤 타입으로 매개변수화된 Box라는 의미의 비한정적 와일드카드 타입이다.Box<? extends Number>처럼 와일드카드에도 범위를 지정할 수 있다.Box<?>에는 구체적인 타입의 값을 안전하게 추가할 수 없으며 주로 정확한 요소 타입을 알 필요 없는 API에서 유용하다.Box<Object>와Box<?>는 전혀 다른 의미다.Integer가Object의 하위 타입이라고 해서Box<Integer>가Box<Object>의 하위 타입이 되는 것은 아니다.- 아무 타입의 Box를 받고 싶은 경우
Box<Object>가 아니라Box<?>가 적절하다. - Raw Type인
Box와 와일드카드 타입인Box<?>도 다르다. - Raw Type은 제네릭 타입 정보를 포기하지만
Box<?>는 제네릭 타입 시스템 안에서 알 수 없는 타입을 안전하게 표현한다. - 제네릭은 단순히 형변환을 줄이는 문법이 아니라 코드가 어떤 타입을 다루는지 명확하게 표현하고 잘못된 타입 사용을 컴파일 단계에서 차단하는 타입 안전성 도구다.
한 줄 정리
제네릭은
List<Integer>처럼 코드가 다루는 타입을 컴파일러에게 명확하게 알려 잘못된 타입 사용을 실행 전에 차단하고 불필요한 형변환을 없애는 기능이며, 타입 정보를 포기하는List같은 Raw Type 대신 매개변수화 타입이나List<?>같은 와일드카드 타입을 사용하는 것이 제네릭의 타입 안전성을 제대로 활용하는 방법이다.