前言:
Chapter 5. Generics
- 26. 不要使用原始类型
- 27. 消除非检查警告
- 28. 列表优于数组(泛型场景时)
- 29. 优先考虑泛型
- 30. 优先使用泛型方法
26. 不要使用原始类型
相关概念:
- 泛型类或泛型接口:一个类或接口,它的声明有一个或多个类型参数(type parameters ),被称之为泛型类或泛型接口[JLS,8.1.2,9.1.2]。
- 泛型类型(generic types):泛型类和接口统称为泛型类型(generic types);
- 原始类型(raw type):每个泛型定义了一个原始类型(raw type),它是没有任何类型参数的泛型类型的名称[JLS,4.8]。 例如,对应于 List<E> 的原始类型是 List。
- 原始类型的行为:就像所有的泛型类型信息都从类型声明中被清除一样。 它们的存在主要是为了与没有泛型之前的代码相兼容。
不使用原始类型的原因:
- 使用原始类型(没有类型参数的泛型)是合法的(Java为了兼容),但是你不应该这样做。
- 如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势。
原始类型的正确替代方案:
- 安全替代方式是使用无限制通配符类型(unbounded wildcard types)。 如果要使用泛型类型,但不知道或关心实际类型参数是什么,则可以使用问号来代替。
- 无限制通配符 Set<?> 与原始类型 Set 之间有什么区别? --通配符类型是安全的,原始类型不是(无限制通配符 Set<?> ≠ 原始类型 Set )。
另外,原始类型的可用场景:
- 你必须在类字面值(class literals)中使用原始类型。 规范中不允许使用参数化类型(尽管它允许数组类型和基本类型)[JLS,15.8.2]。 换句话说,List.class,String[].class 和 int.class 都是合法的,但 List<String>.class 和 List<?>.class 都是不合法的。
- 因为泛型类型信息在运行时被擦除,所以在无限制通配符类型以外的参数化类型上使用 instanceof 运算符是非法的。
总结:
- 使用原始类型可能导致运行时异常,所以不要使用它们。
- 原始类型只是为了与引入泛型机制之前的遗留代码进行兼容和互用而提供的。
27. 消除非检查警告
使用泛型编程时,会看到许多编译器警告:
- 未经检查的强制转换警告;
- 未经检查的方法调用警告;
- 未经检查的参数化可变长度类型警告;
- 以及未经检查的转换警告。
泛型编译警告处理方法:
- 1、尽可能地消除每一个未经检查的警告。
- 2、如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用 @SuppressWarnings("unchecked") 注解来抑制警告。
- 3、每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的。 这将有助于他人理解代码,更重要的是,这将减少有人修改代码的可能性,从而使计算不安全。
总结:
- 1、未经检查的警告是重要的。 不要忽视他们。
- 2、每个未经检查的警告代表在运行时出现 ClassCastException 异常的可能性。
- 3、尽你所能消除这些警告。
- 4、如果无法消除未经检查的警告,并证明是安全类型的,则在尽可能小的范围内使用 @SuppressWarnings(“unchecked”) 注解来禁止警告。并说明你决定在注释中抑制此警告的理由。
28. 列表优于数组(泛型场景)
数组和泛型不能很好地在一起混合使用。因为数组在两个重要方面与泛型不同:
- 1、数组是协变的(covariant),泛型是不变的(invariant)。Item类型错误时,List编译时即会发现错误,Array到运行时才能发现错误。
- 2、数组被具体化了(reified)。 这意味着数组在运行时知道并强制执行它们的元素类型。 如前所述,如果尝试将一个 String 放入 Long 数组中,得到一个 ArrayStoreException 异常。 相反,泛型通过擦除(erasure)来实现。 这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)它们的元素类型信息。
泛型数组的错误语法举例:
// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
总结:
- 1、数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。
- 2、因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,对于泛型则是相反。
- 3、一般来说,数组和泛型不能很好地混合工作。
- 4、如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。
29. 优先考虑泛型
参数化声明并使用 JDK 提供的泛型类型和方法通常不会太困难。 但编写自己的泛型类型有点困难,但值得努力学习。
堆栈容器(具体的数据类型改为泛型)的代码示例:
// Object-based collection - a prime candidate for generics
public class Stack {
private Object[] 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(Object e) {
ensureCapacity();
elements[size++] = e;
}
// Appropriate suppression of unchecked warning
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;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
总结:
- 1、泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用。
- 2、当你设计新的类型时,确保它们可以在没有这种强制转换的情况下使用。 这通常意味着使类型泛型化。
- 3、如果你有任何现有的类型,应该是泛型的但实际上却不是,那么把它们泛型化。
- 4、这使这些类型的新用户的使用更容易,而不会破坏现有的客户端(条目 26)。
30. 优先使用泛型方法
正如类可以是泛型的,方法也可以是泛型的。 对参数化类型进行操作的静态工具方法通常都是泛型的。 集合中的所有“算法”方法(如 binarySearch 和 sort)都是泛型的。
泛型方法示例:
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
泛型单例工厂:
因为泛型是通过擦除来实现的(详见第 28 条),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。 这种被称为泛型单例工厂(generic singleton factory)的模式,用于函数对象(function objects)(详见第 42 条),比如 Collections.reverseOrder 方法,偶尔也用于 Collections.emptySet 之类的集合。
自限定的类型:
<E extends Comparable <E >> 可以理解为「任何可以与自己比较的类型 E」,这或多或少精确地对应于相互可比性的概念。
// Returns max value in a collection - uses recursive type bound
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("Empty collection");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
总结:
- 1、像泛型类型一样,泛型方法比需要客户端对输入参数和返回值进行显式强制转换的方法更安全,更易于使用。
- 2、像类型一样,你应该确保你的方法可以不用强制转换,这通常意味着它们是泛型的。
- 3、应该泛型化现有的方法,其使用需要强制转换。 这使得新用户的使用更容易,而不会破坏现有的客户端(详见第 26 条)。