Effective Java - 泛型

泛型

声明中具有一个或多个类型参数的类或者接口,称之为泛型(generic)类或接口。泛型类和泛型接口统称为泛型(generic type)

每个泛型都有一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。例如:List<E>对应的原生态类型是List。

在类进行编译时编译器自动进行类型转换,并且在编译时告知是否插入类型错误的对象,并在运行时擦除它们的元素类型。

泛型相关术语

术语 实例 所在条目
参数化的类型 List<String> 第23条
实际类型参数 String 第23条
泛型 List<E> 第23、26条
形式类型参数 E 第23条
无限制通配符类型 List<?> 第23条
原生态类型 List 第23条
有限制类型参数 <E extends Number> 第26条
递归类型参数 <T extends Comparable<T>> 第27条
有限制通配符类型 List<? extends Number> 第28条
泛型方法 static <E> List<E> asList(E[] a) 第27条
类型令牌 String.class 第29条

第23条 不要再新代码中使用原生态类型

Java1.5发行版中增加了泛型。在没增加之前,都使用原生态类型。在此之后,泛型使程序更具有安全性和更好的表述性。为了保证移植兼容性,Java平台依然允许使用原生态类型,但在新代码中不要使用它们。

  • 泛型使得错误的发现提前,编译时就发现,降低了系统异常的风险
  • set<Object>参数化类型,表示可以包含任何对象类的一个集合;
  • set<?>无限制通配符类型,表示可以包含某种未知对象类型的一个集合;
  • set原生态类型,脱离了泛型系统不安全

第24条 消除非受检的警告

使用泛型编程时会经常遇到很多非受检的警告。大多数警告很容易消除,有些比较难消除,在确保警告的代码是类型安全时可以通过@SuppressWarnning("unchecked")注解消除警告。

  • 尽可能消除每一个非受检警告,确保代码类型安全。这样程序就不会抛出ClassCastException异常。
  • @SuppressWarnning("unchecked")注解尽可能写在影响范围小的区域,最好写在警告代码所在行上方。
  • 每当使用@SuppressWarnning("unchecked")注解消除警告时都要添加一条注解说明为什么是安全的。

第25条 泛型列表优先于泛型数组

  • 协变的(covariant)解释

如果类Sub是类Super的子类,那么数组类型Sub[]就是Super[]的子类型,因此数组是协变的。但泛型不是协变的,因此List<Sub>不是List<Super>的子类型,后者也不是前者的父类型。看似泛型有劣势,实际上这确是它的优势所在。

  • 具体化的(reified)解释

具体化的——运行时表示法包含的信息和编译时表示法包含相同的类型信息。数组能够在运行时知道并检查元素类型的约束,如果你把一个String对象存入Long数组中就会得到ArrayStoreException异常。反之,泛型只是在编译期对它的类型信息进行强化,运行期会擦除类型信息以兼容原生态类型,运行时类型信息比编译时少,因此泛型是不可具体化的(non-reifiable)。

1
2
3
4
5
6
static <E> E reduce(List<E> list) {
//此处编译时有警告,运行时会擦除泛型,协变成其他子类型的数组
E[] xx = (E[]) list.toArray();
E result = xx[0];
return result;
}

数组是协变的且可具体化的,因此泛型数组E[]会在运行时擦除泛型参数E,可能协变为String[]、Integer[]、Long[]等等,使用时造成类型转换异常。而泛型使不可协变的,因此编译时是安全的。如果发现泛型数组编译警告就应当用列表替代数组。

例外

并不可能总是在泛型中使用列表,Java不是天生支持列表,因此有些泛型如:ArrayList必须在数组上实现。为了提升性能,泛型HashMap也在数组上实现。

第26条 优先考虑泛型

使用JDK或其他类库提供的泛型比较简单,但是自己编写泛型就不那么简单了。

类泛型化的步骤

  1. 类声明中添加一个或者多个类型参数,这个参数的名称通常是E(原因详见第44条)
  2. 用相应的类型参数替换类中所有的Object类型

泛型化的优势

泛型化的类中进行类型转换比客户端自己进行类型转换更安全、更方便。

第27条 优先考虑泛型方法

静态工具方法尤其适合泛型化。例如,Collections中所有的算法方法都已经泛型化。

这句话有点拗口,声明方法的参数的 形式类型参数 列表,位于方法修饰符和返回类型之间。如下代码所示。

1
2
//方法修饰符 private 返回类型E  参数List<E>  形式类型列表 <E>
private static <E> E reduce(List<E> list){...}

通过某个包含该类型参数本身的表达式来限制类型参数,称之为递归类型限制 。递归类型限制最普遍的应用在于定义类型的自然顺序的Comparable接口。

1
public static <T extends Comparable<T>> T max(List<T> list){...}

泛型方法就像泛型一样,泛型化方法使用起来比要求客户端转换输入参数更安全、更容易。

第28条 利用有限制通配符来提升API的灵活性

  1. 泛型参数化类型是不可协变的,意味着我们不能使用参数化类型逻辑上的子类型

例如,Stack<Number>理应可以使用子类型的Stack<Integer>的intVal,但实际就会因为参数化类型不可协变而编译报错。但有时我们需要更大的灵活性,幸好Java提供了一种特殊的参数化类型——有限制的通配符 ,可以处理类似的情况。上述例子即可采用Statck<? extends Number&> 解决。这里的extends并不单指继承,而且包含被“继承”的自身——Number。

反之,如果要将Stack<Number>的一个元素取出放入Stack<Integer>中,就会因为泛型是不可协变的(即使互为父子关系的形式类型参数)出现上述例子中的错误。

同样的Java也提供了一种特殊的应对办法,即Stack<? super Integer>。这里的super也不单指父类,也包含自身

结论 :为了获得最大限度的灵活性,要在生产者(写入)和消费者(读取)的读入参数上使用通配符类型。使用的基本原则是PECS。如果既是P又是C就不要用通配符,因为要严格匹配类型。
助记符 PECS 表示 producer-extends , consumer-super

  1. 不要再返回值类型里使用通配符类型,它会强制用户在客户端代码中使用通配符类型

不使用通配符类型,进行类型推导十分麻烦。但是不加编译会报错incompatible types,幸运的是可以通过显式的类型参数来告诉它要使用哪种类型。

例如:

1
2
3
4
5
6
7
8
9
10
//返回通配符类型的方法
private static <E> Set<? extends E> union(Set<? extends E> s1, Set<? extends E> s2){...}
//两个参数
Set<Integer> integers = ...;
Set<Double> doubles = ...;
//类型推导不知道结果该为Number呢、还是Number的子类们呢
//所以类型推导编译会报错 incompatible types
Set<Number> numbers= union(integers,doubles);
//显式类型参数指定,虽然不优雅,但这样就不会报错了
Set<Number> number = Union.<Number>union(integers,doubles);
  1. 类型参数和通配符具有双重性,许多方法都可选其一进行声明

在公共API中最好使用通配符代替类型参数,无限制类型参数<E>用无限制通配符<?>代替。有限制通类型参数<E extends Number>用有限制通配符代替。因为这样能保持API简洁,复杂的方法声明中类型名称的增多会降低方法的可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface Box<T> {
public T get();
public void put(T element);
}
public void rebox(Box<?> box) {...} //优先使用
public void <E> rebox(Box<E> box){...}//被替换掉
//优先使用的看似没问题,但是当进行put时会编译异常
public void rebox(Box<?> box) {
box.put(box.get());
}

//Rebox.java:8: put(capture#337 of ?) in Box<capture#337 of ?> cannot be applied
// to (java.lang.Object)
// box.put(box.get());
// ^
//1 error


// “capture#337 of ?” 与 Object 不兼容的错误消息
//通配符意味着某个T,虽然不知道是哪个具体类型,可以通过占位符指代具体类型。占位符被称为这个特殊通配符的捕获(capture),每个通配符都会获得一个不同的捕获。
//因此box.get() 和box.put()两个捕获,第一个捕获get()类型是object,因为"?"实际是"? extends Object",put()的捕获是"capture#337 of ?",编译器不能静态地检验对由占位符 “capture#337 of ?” 所识别的类型而言 Object 是否是一个可接受的值。


//通过辅助方法这一技巧, 泛型方法引入了额外的类型参数(位于返回类型之前的尖括号中),这些参数用于表示参数和/或方法的返回值之间的类型约束。get()不再是object而是v,并将v传给put()
public void rebox(Box<?> box) {
reboxHelper(box);
}

private<V> void reboxHelper(Box<V> box) {
box.put(box.get());
}

参考资料 Java 理论与实践: 使用通配符简化泛型使用

Brian Goetz (brian.goetz@sun.com), 高级工程师, Sun Microsystems

总之,如果编写广泛使用的类库一定要适当利用通配符类型。记住基本原则:PECS。所有的comparable和comparator都是消费者。

第29条 优先考虑类型安全的异构容器

通常情况下,泛型运用于集合和单元素容器,限制了固定数目的类型参数,符合我们的需要。但是有时候,我们需要更多的灵活性。比如,数据库行可以任意多的列,如果能以类型安全的方式访问所有列就好了。

幸运的是,有种方法可以非常容易的实现,即把键进行参数化而不是容易参数化。然后将参数化的键提交给容器,来插入或获取值。用泛型系统来确保值类型和键相符。

异构的(heterogeneous)解释

容器的键可以是不同类型的,则把这个容器称之为类型安全的异构容器

通过一个简单的Favorites类来说明异构过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Favorites{
private Map<Class<?>,Object> favorites = new HashMap<Class<?>,Object>();
public <T> void putFavorite(Class<T> type,T instance){
//type.cast(instance) 利用类的类型Class的cast方法动态转换成Class对象所表示的类型
favorites.put(type,type.cast(instance));
}
public <T> T getFavorite(Class<T> type){
//我们需要返回T而不是Object所以也需要动态转换类型
return type.cast(favorites.get(type));
}

public static void main(String[] args) {
Favorites f = new Favorites();
//参数化的键String.class被称为类型令牌,本质是String类类型的实例
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
//参数化的键返回类类型Class的类型令牌Class.class Favorites.class 为类类型为Favorites的类类型实例 Favorites.class
//每个类都有一个类类型实例,在类被加载后JVM自动构造,由JVM管理。
//Class类类型Class 是所有类的共有信息抽象
f.putFavorite(Class.class, Favorites.class);

String string = f.getFavorite(String.class);
Integer integer = f.getFavorite(Integer.class);
Class<?> clazz = f.getFavorite(Class.class);
//printf c语言风格的输出 %x 十六进制输出 %n 换行
System.out.printf("%s %x %s%n", string, integer, clazz.getName());
}
//输出结果
//Java cafebabe Favorites
}

Favorites类put和get方法使用的类型令牌是无限制的,可以接受任何Class对象(类类型实例),有时需要限制传给方法的类型。可以通过有限制类型参数和有限制通配符来限制可以表示的类型。

注解API广泛的使用了有限制的类型令牌。例如接口AnnotatedElement的getAnnotation方法:

1
public <T extends Annotation> T getAnnotation(Class<T> annotationType);

如果有一个Class<?>对象需要传入有限制的令牌类型,直接传入会编译报错。

捕获capture<?> 意味着某个T,与Annotation及其子类型不相符

可以把Class<?>改成Class<? extends T>,但这样会报编译未受检的警告(见24条)。

1
2
3
4
5
6
7
8
9
10
11
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<? extends Annotation> annotationType = null;
try {
//未受检警告
//Unchecked cast: 'java.lang.Class<capture<?>>' to 'java.lang.Class<? extends java.lang.annotation.Annotation>'
annotationType = (Class<? extends Annotation>) Class.forName(annotationTypeName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
return element.getAnnotation(annotationType);
}

幸好,类Class提供了安全的进行转换的实例方法asSubClass,它将调用它的Class对象转换成用起参数表示的类的一个子类。如果转换成功返回参数;如果失败报ClassCastException异常。

1
2
3
4
5
6
7
8
9
10
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
//annotationType类型某个T,安全转换为annotationType的子类
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}

总之,集合API是泛型的一般用法,限制每个容器只有固定的类型参数。通过键类型参数化可以避开这一限制,使得其成为安全异构的容器。可以用Class对象(类类型的实例)来作为键。

也可以定制键类型,例如,用一个DatabaseRow类型表示一个数据库行的容器,用泛型Column<T>作为它的键。