타입 경계와 와일드카드
모든 타입 인자를 허용하는 것보다 특정 상위 타입의 기능이 필요할 때 경계를 선언할 수 있습니다.
<T extends Comparable<T>>는 T가 비교 동작을 제공한다는 사실을 메서드 안에서 보장합니다.
와일드카드는 정확한 타입을 이름 붙이지 않고 제네릭 타입의 범위를 표현합니다.
? extends와 ? super는 읽기와 쓰기 가능성이 서로 다릅니다.
이 절에서는 필요한 연산만큼만 타입 제약을 줍니다.
경계는 “받을 수 있는 타입의 목록”만 정하지 않습니다. 메서드 본문에서 어떤 연산을 안전하게 할 수 있는지도 함께 정합니다. extends에서 읽기 타입이 넓어지고 쓰기가 막히는 이유, super에서 쓰기는 가능하지만 읽기가 Object로 넓어지는 이유를 실제 목록의 정체를 모른다는 관점에서 따라가 봅니다.
List와 구현별 비용은 다음 장에서 정리합니다. 여기서는 목록을 자료구조로 선택하는 문제가 아니라 List<Integer>, List<Number> 사이에서 컴파일러가 허용하는 값의 이동 방향만 다룹니다.
상한 경계가 사용할 수 있는 동작을 보장한다
T extends Number라면 Number 메서드를 호출할 수 있습니다.
여러 경계는 클래스 하나를 먼저 쓰고 인터페이스를 &로 잇습니다.
제네릭 타입은 기본적으로 불공변이다
Integer가 Number의 하위 타입이어도 List<Integer>는 List<Number>의 하위 타입이 아닙니다.
그렇지 않으면 Double을 Integer 목록에 넣을 수 있기 때문입니다.
GenericBounds.java는 숫자를 읽어 합산하는 생산자와 정수를 받아들이는 소비자를 별도 메서드로 둡니다.
import java.util.List;
public class GenericBounds {
static double total(List<? extends Number> values) {
double sum = 0;
for (Number value : values) sum += value.doubleValue();
return sum;
}
static void addDefaults(List<? super Integer> target) {
target.add(10);
target.add(20);
}
public static void main(String[] args) {
System.out.println(total(List.of(1, 2, 3)));
System.out.println(total(List.of(1.5, 2.5)));
List<Number> target = new java.util.ArrayList<>();
addDefaults(target);
System.out.println(target);
}
}total은 Integer 목록과 Double 목록을 모두 Number 생산자로 읽습니다. 반면 addDefaults는 Integer를 받아들일 수 있는 Number 목록에만 값을 추가합니다. 두 메서드는 같은 와일드카드를 반대로 쓴 것이 아니라 데이터가 흐르는 방향이 다릅니다.
6.0
4.0
[10, 20]extends 목록에서는 Number로 읽고 super 목록에는 Integer를 쓸 수 있지만 반대 방향은 제한됩니다.
시그니처를 읽을 때 목록 이름 옆에 데이터 화살표를 그려 보면 좋습니다. total은 목록에서 Number를 꺼내므로 화살표가 메서드 쪽으로 향하고 extends가 맞습니다. addDefaults는 Integer를 목록으로 보내므로 화살표가 매개변수 쪽으로 향하고 super가 맞습니다.
extends 와일드카드는 생산자를 읽는다
List<? extends Number>에서 원소를 Number로 읽을 수 있지만 정확한 원소 타입을 몰라 null 외에는 추가할 수 없습니다.
producer의 실제 타입은 List<Integer>일 수도 List<Double>일 수도 있으므로 3을 안전하게 추가할 근거가 없습니다. consumer에서는 Integer를 추가할 수 있지만 실제 목록이 List<Object>일 수도 있어 읽은 값을 Integer라고 단정할 수 없습니다.
List<Integer> integers = List.of(1, 2);
// List<Number> numbers = integers; // 컴파일 오류: 제네릭은 불공변List<? extends Number> producer = integers;
Number read = producer.getFirst();
// producer.add(3); // 컴파일 오류: 실제 원소 타입을 모름List<? super Integer> consumer = new java.util.ArrayList<Number>();
consumer.add(3);
Object value = consumer.getFirst();super 와일드카드는 소비자에게 값을 쓴다
List<? super Integer>에는 Integer를 추가할 수 있지만 읽은 값의 정적 타입은 Object입니다.
실제 목록이 Integer, Number, Object일 수 있습니다.
무제한 와일드카드는 Object 목록과 다르다
List<?>는 원소 타입을 모르는 어떤 List도 받을 수 있지만 null 외의 값을 추가할 수 없습니다.
List<Object>는 실제 원소 타입이 Object인 목록만 받습니다.
addDefaults의 대상을 ArrayList<Object>로 바꿔도 컴파일되고 같은 두 정수가 들어가는지 확인하세요. 이어서 List<Double>을 전달하면 왜 거부되는지 설명해 보세요. Double 목록은 Number를 읽는 생산자일 수는 있어도 Integer를 받는 소비자는 될 수 없습니다.
매개변수가 읽기와 쓰기를 모두 해야 한다면 와일드카드 하나로 억지로 표현하지 않습니다. 정확한 List<T>나 메서드 타입 변수로 두 연산이 같은 원소 타입을 공유한다는 관계를 드러내는 편이 호출자와 구현자 모두에게 읽기 쉽습니다.
경계와 와일드카드는 타입을 복잡하게 만드는 장치가 아니라 메서드가 읽고 쓸 수 있는 범위를 정확히 제한하는 장치입니다.