타입으로 견고하게 다형성으로 유연하게 - 두 다형성의 만남
| 절 |
묻는 것 |
| 제네릭 클래스와 상속 |
물려받을 때 타입 인자를 전달할 것인가 고정할 것인가 |
| 타입 매개변수 제한 |
T에 대해 최소한 무엇을 알아야 하는가 |
| 가변성 |
Integer <: Number일 때 Cell<Integer>와 Cell<Number>는 무슨 관계인가 |
class Chest<T> extends Box<T> // 전달 - 하위 타입도 제네릭으로 남는다
class StringBox extends Box<String> // 고정 - 타입 파라미터가 사라진다
-
타입 파라미터를 그대로 전달하거나, 특정 타입으로 고정하거나. 그리고 고정하는 쪽에서는 컴파일러가 몰래 메서드를 하나 만든다.
- 바운드는 능력을 주는 장치가 아니라 요구를 적는 장치다.
class NumberBox<T extends Number> // T가 Number라는 것만 안다
-
<T>만 적었다면 원소로 할 수 있는 일이 없다. 아무것도 모르는 타입에는 부를 메서드가 없으니까, T extends Number라고 적는 순간 doubleValue()를 부를 수 있게 된다.
- 그래도
T가 Integer인지 Double인지는 여전히 모른다.
|
형태 |
쓰는 이유 |
| 상한 |
<T extends Number> |
T의 메서드를 부르려면 필요하다 |
| 다중 바운드 |
<T extends Number & Comparable<T>> |
두 능력이 동시에 필요할 때 |
| 재귀 바운드 |
<SELF extends Request<SELF>> |
상속해도 구체 타입을 유지하려고 |
- Java에 union 타입은 없지만 intersection 타입은 바운드 위치에서 쓸 수 있다.
- 제약을 늘리는 만큼 받아들일 수 있는 타입이 줄어든다.
- 상속과 메서드 체이닝은 사이가 나쁘다.
- 상위 클래스의
path()가 Request를 돌려주면 체이닝이 거기서 상위 타입으로 주저앉고, 그 뒤에 하위 타입 메서드를 못 부른다.
new JsonRequest().path("/users").body("{}")
// ^^^^ path()가 Request를 돌려줬다면 여기서 끊긴다
- 그래서 "내 하위 타입이 무엇인지"를 타입 파라미터로 받는다.
abstract class Request<SELF extends Request<SELF>>
-
SELF의 바운드 안에 자기 자신이 다시 나오기 때문에 재귀적이다.
- 대가가 있다.
self()를 하위 타입이 구현해야 하고, 컴파일러는 SELF가 정말 this의 타입인지 검증하지 못한다.
class Bound<T super Integer> { } // error: > expected
-
Integer <: Number일 때 Cell<Integer>와 Cell<Number>는 무슨 관계인가. 답은 타입 파라미터가 어디에 나타나는가로 갈린다.
| 타입 |
T가 나타나는 곳 |
결론 |
Source<T> |
반환 위치에만 |
공변 — ? extends
|
Sink<T> |
파라미터 위치에만 |
반공변 — ? super
|
Cell<T> |
양쪽 모두 |
불변 |
- 생산자는 위로 넓히고, 소비자는 아래로 넓힌다.
-
List<?>는 "원소 타입이 무엇인지는 모르지만 하나로 정해져 있다"는 뜻이다.
- 문제는
?가 나올 때마다 검사기가 같은 타입이라고 보장하지 못한다는 것이다.
static void swap(List<?> list, int i, int j) {
list.set(i, list.get(j)); // error: Object cannot be converted to CAP#1
}