泛型
声明中具有一个或多个类型参数的类或者接口,称之为泛型(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 | static <E> E reduce(List<E> list) { |
数组是协变的且可具体化的,因此泛型数组E[]会在运行时擦除泛型参数E,可能协变为String[]、Integer[]、Long[]等等,使用时造成类型转换异常。而泛型使不可协变的,因此编译时是安全的。如果发现泛型数组编译警告就应当用列表替代数组。
例外
并不可能总是在泛型中使用列表,Java
不是天生支持列表,因此有些泛型如:ArrayList
必须在数组上实现。为了提升性能,泛型HashMap
也在数组上实现。
第26条 优先考虑泛型
使用JDK或其他类库提供的泛型比较简单,但是自己编写泛型就不那么简单了。
类泛型化的步骤
- 类声明中添加一个或者多个类型参数,这个参数的名称通常是
E
(原因详见第44条) - 用相应的类型参数替换类中所有的Object类型
泛型化的优势
泛型化的类中进行类型转换比客户端自己进行类型转换更安全、更方便。
第27条 优先考虑泛型方法
静态工具方法尤其适合泛型化。例如,Collections中所有的算法方法都已经泛型化。
这句话有点拗口,声明方法的参数的 形式类型参数 列表,位于方法修饰符和返回类型之间。如下代码所示。
1 | //方法修饰符 private 返回类型E 参数List<E> 形式类型列表 <E> |
通过某个包含该类型参数本身的表达式来限制类型参数,称之为递归类型限制 。递归类型限制最普遍的应用在于定义类型的自然顺序的Comparable接口。
1 | public static <T extends Comparable<T>> T max(List<T> list){...} |
泛型方法就像泛型一样,泛型化方法使用起来比要求客户端转换输入参数更安全、更容易。
第28条 利用有限制通配符来提升API的灵活性
- 泛型参数化类型是不可协变的,意味着我们不能使用参数化类型逻辑上的子类型
例如,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
- 不要再返回值类型里使用通配符类型,它会强制用户在客户端代码中使用通配符类型
不使用通配符类型,进行类型推导十分麻烦。但是不加编译会报错incompatible types
,幸运的是可以通过显式的类型参数来告诉它要使用哪种类型。
例如:
1 | //返回通配符类型的方法 |
- 类型参数和通配符具有双重性,许多方法都可选其一进行声明
在公共API中最好使用通配符代替类型参数,无限制类型参数<E>用无限制通配符<?>代替。有限制通类型参数<E extends Number>用有限制通配符代替。因为这样能保持API简洁,复杂的方法声明中类型名称的增多会降低方法的可读性。
1 | public interface Box<T> { |
参考资料 Java 理论与实践: 使用通配符简化泛型使用
Brian Goetz (brian.goetz@sun.com), 高级工程师, Sun Microsystems
总之,如果编写广泛使用的类库一定要适当利用通配符类型。记住基本原则:PECS。所有的comparable和comparator都是消费者。
第29条 优先考虑类型安全的异构容器
通常情况下,泛型运用于集合和单元素容器,限制了固定数目的类型参数,符合我们的需要。但是有时候,我们需要更多的灵活性。比如,数据库行可以任意多的列,如果能以类型安全的方式访问所有列就好了。
幸运的是,有种方法可以非常容易的实现,即把键进行参数化而不是容易参数化。然后将参数化的键提交给容器,来插入或获取值。用泛型系统来确保值类型和键相符。
异构的(heterogeneous)解释
容器的键可以是不同类型的,则把这个容器称之为类型安全的异构容器 。
通过一个简单的Favorites类来说明异构过程。
1 | public class 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 | static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) { |
幸好,类Class提供了安全的进行转换的实例方法asSubClass,它将调用它的Class对象转换成用起参数表示的类的一个子类。如果转换成功返回参数;如果失败报ClassCastException异常。
1 | static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) { |
总之,集合API是泛型的一般用法,限制每个容器只有固定的类型参数。通过键类型参数化可以避开这一限制,使得其成为安全异构的容器。可以用Class对象(类类型的实例)来作为键。
也可以定制键类型,例如,用一个DatabaseRow类型表示一个数据库行的容器,用泛型Column<T>作为它的键。