4 泛型
泛型中的术语:
术语 | 例子 |
---|---|
参数化类型(Parameterized type) | List<String> |
实际类型参数(Actual type parameter) | String |
泛型(Generic type) | List<E> |
形式类型参数(Formal type parameter) | E |
无界通配符类型(Unbounded wildcard type) | List<?> |
原类型(Raw type) | List |
有界类型参数(Bounded type parameter) | <E extends Number> |
递归类型限制(Recursive type bound) | <T extends Comparable<T>> |
有界通配符类型(Bounded wildcard type) | List<? extends Number> |
泛型方法(Generic method) | static <E> List<E> asList(E[] a) |
类型标记(Type token) | String.class |
Item 26:不要使用泛型的原类型
总结:使用原类型会失去泛型的安全性和表达性,可能在运行时报错,还需要自己转型。
声明中有一个或多个类型参数(type parameter)的类或接口,被称为泛型类和泛型接口,统称为泛型(generic types)。List<String>
读作“List of String”,是一个参数化类型,代表一个元素类型为 String 的 List。
每个泛型都定义了一个原类型(raw type),就是这个泛型没有伴随的类型参数时的名字。比如,List<String>
的原类型为 List。原类型主要是为了兼容引入泛型之前的代码。
不指定类型参数,只使用原类型时,如 List list = new ArrayList();
,编译器会给出警告,但是能编译通过。但是这种方式失去了泛型的优点:
- 编译时类型检查(插入操作)
- 自动转型(查询操作)
List<String>
是 List 的子类型,但不是 List<Object>
的子类型。虽然都可以存放 Object 对象,但是 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);
}
如果真的不确定集合中会存放什么类型的话,你应该使用无界通配符类型(unbounded wildcard type),如 Set<?>
,它代表一个 Set 仅仅可以容纳某种未知类型的对象。Set<?>
和 Set 的区别在于,前者是类型安全且灵活的,后者不安全;前者不能插入任何非 null 对象,后者能插入任何对象。
注意,有一些需要使用原类型的场景:
-
在类的字面量中必须使用原类型,
List.class
、String[].class
和int.class
是正确的,但List<?>.class
List<String>.class
是错误的。 -
instanceof 操作符中使用原类型就够了,因为泛型信息会在运行时擦除,在 instanceof 操作符中只能使用无界通配符类型或原类型,而且两者并没有区别,推荐后者。
// instanceof 操作符的推荐使用方式 if (o instanceof Set) { Set<?> s = (Set<?>) o; // 这种转型时有检查的,不会有编译时警告 }
Item 27:消除未检查警告
总结:未检查警告很重要,不要忽略,每个未检查警告都意味着可能在运行时抛出 ClassCastException 异常。尽最大的能力消除它们。当不能消除且确信代码是类型安全时,在最小的作用域中使用 SuppressWarnings 注解,并在注释中记录你抑制此警告的理由。
使用泛型时,编译器可能给出很多种未检查警告:
- unchecked cast warning
- unchecked method invocation warning
- unchecked parameterized vararg type warning
- unchecked conversion warning
如果你已经尽可能消除警告,并确信自己的代码没有错误,可以使用 SuppressWarnings 注解(可以作用于类,方法,变量声明),但是注意,一定要最小化注解的作用范围,而且要添加注释说明为什么这是安全的。
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
ArrayList 中的 toArray 方法的注解可以修改为:
public <T> T[] toArray(T[] a) {
if (a.length < size) {
// This cast is correct because the array we're creating
// is of the same type as the one passed in, which is T[].
@SuppressWarnings("unchecked") T[] result =
(T[]) Arrays.copyOf(elementData, size, a.getClass());
return result;
}
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
Item 28:用 List 代替数组
综述:数组是协变的、reified,泛型是不变的、擦除式的。因此数组不能提供编译时安全。数组和泛型不能混和定义,如果因为两者的转换而有编译时错误或者警告,你的第一反应应该是用 list 代替数组。
数组和泛型类型有两个不同:
-
数组是协变的(covariant),如果 Sub 是 Super 的子类,那么 Sub[] 是 Super[] 的子类;泛型是不变的(invariant),任意两个类型 T1 和 T2,
List<T1>
和List<T2>
不存在父子关系。因此,数组有更多的风险:Object[] objectArray = new Long[1]; objectArray[0] = "I don't fit in"; // 运行时抛异常 ArrayStoreException,编译时有警告但能通过 List<Object> ol = new ArrayList<Long>(); // 编译失败,Incompatible types
-
数组是 reified,它们会在运行时知道并强制元素类型;泛型是 erasure,它们会在编译时强制类型约束,并在运行时擦除元素类型。
泛型和数组不能混合使用,new List<E>[]
、new List<String>[]
、new E[]
都是非法的语句。
为什么泛型数组是非法的?因为它不是类型安全的,如果它是合法的,那么编译时生成的自动类型转换会在运行时抛出 ClassCastException,这违反了泛型类型体系对类型安全的基本保证。
举个例子:
List[] lists = new List[1];
List<Integer> intList = new ArrayList<>();
intList.add(43);
Object[] objects = lists;
objects[0] = intList;
System.out.println(lists[0].get(0));
上面的代码使用了原类型数组,这是合法的,因为 List<Integer>
是 List 的子类,原类型数组的安全隐患是出于原类型本身就存在类型安全隐患(Item 26),虽有隐患但合法。但如果改写成下面的代码片段:
List<String>[] lists = new List<>[1]; // 假设是合法的
List<Integer> intList = new ArrayList<>();
intList.add(43);
Object[] objects = lists;
objects[0] = intList;
System.out.println(lists[0].get(0));
除了第 1 行外,其它行的代码都是合法的,假设现在允许泛型数组,那么,在上面的第 5 行代码处,将 List<Integer>
实例存储在一个声明为只存储 List<String>
实例元素的数组中,在第 6 行取元素时,编译器会自动将 Integer 元素类型转换为
String,因此在运行时抛出 ClassCastException,这违反了泛型类型体系对类型安全的基本保证。
使用 List<E>
代替数组,可以免除很多集合转数组的限制,虽然牺牲简洁性和性能,但得到类型安全和兼容性。比如要实现一个返回集合中一个随机元素的类,有如下三种方案。
方案一:对象数组
static class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
choiceArray = choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
缺点:获取元素需要自动转型,万一转错了呢?
方案二:泛型转数组
static class Chooser<T> {
private final T[] choiceArray;
// The type-safe is guaranteed by generic type.
@SuppressWarnings("unchecked")
public Chooser(Collection<T> choices) {
choiceArray = (T[]) choices.toArray();
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
这是类似 ArrayList 中 toArray 方法。缺点是,有未检查警告。
方案三:全部用泛型,不用数组
static class Chooser<T> {
private final List<T> choiceList;
// The type-safe is guaranteed by generic type.
@SuppressWarnings("unchecked")
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceArray.size()));
}
}
缺点:慢一些
需要补充的是:
- Java 中的 List 并不是原生类型,很多集合类(如 ArrayList)本身就是用数组实现,而一些泛型类(如 HashMap)基于性能的考虑也使用数组来实现。
- 另外由于泛型不支持原始类型,所以要么使用对应装箱类的泛型,要么使用原始类型数组。
Item 29:使用泛型类
综述:由于不需要在客户代码中进行类型转换,泛型类安全和简洁。因此,在设计泛型类时,也要确保在使用时不需要类型转换。
自定义泛型类:
方案一:创建数组时类型转换
static class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[].
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, size * 2 + 1);
}
}
}
可读性好(E[]
字段),简洁,只有一次类型转换。但是会造成无害的“堆污染”(Item 32):数组的运行时类型和编译时类型不一致。在运行时,E 会被擦除为 Object,elements 数组就是 Object[]
类型,编译时实际上是利用泛型自动进行了类型转换。pop 方法返回 Object 对象会在客户代码中被转换为 E 类型,因为push 方法插入的对象确实是 E 类型,所以这种强转是类型安全的。**注意:**如果我们添加一个获取 elements 数组的 API,就会出现问题,因为 Object[]
不能强转为 E[]
。
public E[] getElements(){
return elements;
}
方案二:获取元素时类型转换(Java 集合类中的做法)
static class Stack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
// push requires elements to be of type E, so cast is correct
@SuppressWarnings("unchecked") E result = (E) elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, size * 2 + 1);
}
}
}
每次获取元素都要类型转换,但没有“堆污染”。
Item 30:使用泛型方法
综述:泛型方法就像泛型类一样,使用安全、容易,不需要对参数和返回值进行类型转换。你可以方便地将需要类型转换的旧方法泛型化,这不会影响旧的客户。
类型参数列表(type parameter list),在方法的修饰符和返回类型之间,声明方法中的所有类型参数,如下面代码块中的 <E>
。
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
泛型单例工厂:创建一个 immutable 对象,适用于多种不同类型的操作。
// Collections#reverseOrder()
@SuppressWarnings("unchecked")
public static <T> Comparator<T> reverseOrder() {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}
ReverseComparator.REVERSE_ORDER
实际上是 Comparator<Comparable<Objec>>
类型,虽然不是对于每个 T,Comparator<Comparable<Object>>
都是 Comparator<T>
类型,但是由于它返回的是一个 immutable 对象,此对象调用 Comparable<Object>
的 comparaTo 方法来进行排序,只要 T 实现了 Comparable 接口,这样就是类型安全的。
public int compare(Comparable<Object> c1, Comparable<Object> c2) {
return c2.compareTo(c1);
}
递归类型限制(recursive type bound):类型参数被包含类型参数自身的表达式限制。一个常见的使用与 Comparable 接口有关。
public interface Comparable<T> {
public int compareTo(T o);
}
在实际使用中,几乎每个类型只和自身类型进行比较,所以 String 实现 Comparable<String>
接口,Integer 实现 Comparable<Integer>
接口。让集合中的元素实现 Comparable 接口,就可以对集合进行排序、搜索、找出最值等。这需要集合中的每个元素可以和集合中的其它元素进行比较,即 mutually comparable。在方法中描述为:
// 使用递归类型限制来表达相互可比性
public static <T extends Comparable<T>> T max(Collection<T> c);
<T extends Comparable<T>>
可读作“可以和自身比较的任何类型 T”。
Item 31:使用有限制通配符来增加API灵活性
综述:在 API 中使用通配符,虽然稍显麻烦,但增加灵活性。记住基本规则:PECS。记住 comparable 和 comparator 都是消费者。
Item 28 中我们说过,参数化类型是不变的,虽然 Integer 是 Number 的子类,但 List<Integer>
不是 List<Number>
的子类。
现在有一个如下的栈的 API:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
下列操作是有效的:
Stack<Number> stack = new Stack<>();
stack.push(new Integer(1)); // Integer 是 Number 的子类,可以向上转型为 Number
但是现在,我们先添加一个新的 API:
// 不灵活
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
下列操作会报错:
Stack<Number> stack = new Stack<>();
Iterable<Integer> integers = ...;
stack.pushAll(integers); // error: incompatible types
因为Iterable<Integer>
不是Iterable<Number>
的子类,所以不能向上转型,类型不兼容。
幸运的是,我们可以使用有限制通配符类型(bounded wildcard type)来解决这种问题。Iterable<? extends E>
读作“Iterable of some subtype of E”,这里的子类(subtype)一词可能有些误导,但是注意,每一个类型也是自身的子类。
// 通配符作为 E 的生产者
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
现在我们又想要加入一个 API:
// 不灵活
public void popAll(Collection<E> dst) {
while(!isEmpty())
dst.add(pop());
}
但是下列操作会出现类似的问题:
Stack<Number> stack = new Stack<>();
Collection<Object> objects = ...;
stack.popAll(objects); // error: incompatible types
因为 Collection<Object>
不是 Collection<Number>
的父类,所以类型不兼容。
这里使用另一种有限制通配符类型 Collection<? super E>
,读作“collection of some supertype of E”,注意,一个类型也是自身的父类。
// 通配符作为 E 的消费者
public void popAll(Collection<? super E> dst) {
while(!isEmpty())
dst.add(pop());
}
在上面 Stack 的例子中,pushAll 方法的参数提供了 Stack 需要的元素,是生产者,使用 extends;popAll 方法的参数使用 Stack 中的元素,是消费者,使用 super。总结起来就是统配符类型的 PECS(producer-extends, consumer-super)规则。
另外有一个关于通配符和类型参数的选择的原则:若类型参数仅在方法声明中出现一次,就改用通配符。
我们来看一下交换 List 中元素的两种实现 API:
修改前:类型参数仅在方法声明中出现一次
public static <E> void swap(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
修改后:改用通配符
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
虽然我们使用更曲折的方法来实现了 swap,但是这样做是值得的,因为它提供了一个更简洁的 API。
Item 32:不定参数中谨慎使用泛型
综述:不定参数不能与泛型很好地合作,是因为不定参数机制是 leaky abstraction 的数组,它是合法的,但有类型安全问题。如果你要定义泛型不定参数方法,要保证方法是类型安全的,并添加 SafeVarargs 注解。
Item 28 中提过,non-reifiable 类型是指运行时表示提供的信息少于编译时表示的类型,几乎所有泛型和参数化类型都是 non-reifiable。
不定参数(Item 53)的特点是允许客户提供长度不定的同类参数给一个方法,实际上,在方法中,创建了一个数组来存储这些参数,考虑到数组和泛型的混合定义问题,有类型安全风险。
// 危险案例一:修改泛型参数数组的值
static void dangerous(List<String>... stringLists) {
List<Integer> intList = new ArrayList<>();
intList.add(43);
Object[] objects = lists;
objects[0] = intList; // Heap pollution
System.out.println(lists[0].get(0)); // ClassCastException
}
lists 引用的是一个 List<String>[]
对象,由泛型的特点,调用 lists[0].get(0) 时,会把实际类型 Integer 类型转换为 String。
// 危险案例二:暴露了不定参数生成的泛型参数数组
static <T> T[] toArray(T... args) {
return args;
}
static <T> T[] pickTwo(T a, T b, T c) {
switch (ThreadLocalRandom.current().nextInt(3)) {
case 0:
return toArray(a, b);
case 1:
return toArray(a, c);
case 2:
return toArray(b, c);
default:
throw new AssertionError();
}
}
指定具体类型再使用 toArray 方法时,不定参数生成指定类型的参数数组,如 String[]
。在泛型方法 pickTwo 中使用 toArray 时,不定参数不确定泛型的具体类型,生成的是 Object[]
。因此最终 Object[]
转换为 T[]
时会抛“ [Ljava.lang.Object; cannot be cast to [Ljava.lang.String; ”异常。
为了让用户知道不定参数方法的安全性,对于每一个使用泛型类型或参数化类型的不定参数方法使用 SageVarargs 注解。但是你要确保此方法确实在使用中是安全的:
- 不往不定参数数组中存储元素
- 不让不定参数数组及其克隆对不信任代码可见
考虑安全性,SafeVarargs 注解的方法不能被重写,Java 8 中只能用于 static 和 final 方法,Java 9 中还可以用于 private 非静态方法。
// 类型安全的泛型不定参数
@SafeVarargs
static <T> List<T> flatten(List<? extends T>.. list) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
Item 33:类型标记和异构容器
综述:泛型的一般使用中,如集合类 API 中示范的那样,使每个容器中类型参数的数量是固定的。要避开这个限制,你可以在容器的键中使用类型参数,而不是在容器中使用类型参数。可以使用 Class 对象作为这种类型安全的异构容器的键,这样的 Class 对象被称为类型标记(type token)。比如,你可以用 DatabaseRow 类型代表数据库的一行(作为异构容器),使用泛型类 Column<T>
作为异构容器的键。
一个异构容器的例子:
static class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type)); // 无未检查警告的转型
}
}
上述例子中类型标记使用了无限制通配符类型,Java 注解类 API 中大量使用了有限制的类型标记:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
// 使用 asSubclass 方法,安全地由无限制转化为有限制类型标记
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null; // 无限制类型标记
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
thorow new IllegalArgumentException(ex);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
// Class#asSubclass()
@SuppressWarnings("unchecked")
public <U> Class<? extends U> asSubclass(Class<U> clazz) {
if (clazz.isAssignableFrom(this))
return (Class<? extends U>) this;
else
throw new ClassCastException(this.toString());
}