你可以通过指定extends 后接类型名称来提供通配符的上界。同样的,你可以通过指定super 后接类型名称来提供通配符的下界。这些限定限制了可以作为实际类型参数传入的类型。
在例子中,你可以把? extends String 理解为任何String或其子类的实际类型参数。同样的,你可以把? super String 理解为任何String或其父类的实际类型参数。因为String是final的,这意味着它不能被继承,只有源列表为String对象,目标列表为String或Object对象能够传入作为参数,这样用处不大。
你可以使用泛型方法来完全解决这个问题,它是一个有类型实现参数的类或接口方法。泛型方法支持下面的语法:
1 2 | <formalTypeParameterList> returnType
identifier(parameterList)
|
泛型方法的形参列表在它的返回类型之前。它包含类型参数和可选的上界。类型参数可以作为返回类型使用,并且可以出现在参数列表中。
清单9展示了怎么定义和调用泛型copy() 方法
Listing 9. GenDemo.java (version 5)
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 32 33 34 35 36 37 38 39 40 41 42 43 44 | import java.util.ArrayList;
import java.util.List;
public class GenDemo
{
public static void main(String[] args)
{
List<Integer> grades = new ArrayList<Integer>();
Integer[] gradeValues =
{
new Integer( 96 ),
new Integer( 95 ),
new Integer( 27 ),
new Integer( 100 ),
new Integer( 43 ),
new Integer( 68 )
};
for ( int i = 0 ; i < gradeValues.length; i++)
grades.add(gradeValues[i]);
List<Integer> failedGrades = new ArrayList<Integer>();
copy(grades, failedGrades, new Filter<Integer>()
{
public boolean accept(Integer grade)
{
return grade.intValue() <= 50 ;
}
});
for ( int i = 0 ; i < failedGrades.size(); i++)
System.out.println(failedGrades.get(i));
}
static <T> void copy(List<T> src, List<T> dest,
Filter<T> filter)
{
for ( int i = 0 ; i < src.size(); i++)
if (filter.accept(src.get(i)))
dest.add(src.get(i));
}
}
interface Filter<T>
{
boolean accept(T o);
}
|
清单9中我定义了一个<T> void copy(List<T> src, List<T> dest, Filter<T> filter) 泛型方法。编译器注意到src ,dest 和filter 参数的类型都包含类型参数T。这意味着在方法调用中必须传入同样的实际类型参数,而编译器会在调用中获取参数。
如果你编译清单9(javac GenDemo.java )并运行程序(java GenDemo ),你应该可以看到下面的输出:
Java语言中关于泛型最有争议的是什么?
虽然泛型本身并不具争议,但它在Java语言中的特殊实现却是。泛型是作为消除转换的语法糖的编译时特性来实现的。编译器会在编译源码后丢弃泛型类型或泛型的形参类型列表。这个“丢弃”行为称为擦除(erasure)。其他在泛型中关于擦除的例子包含:在代码类型不正确时,插入时可以自动转换为合适的类型;通过上界(例如Object )来替换类型参数。
更多关于泛型的讨论
泛型不只因为擦除而备受争议。看一下StackOverflow.com的“为什么我们抱怨Java关于泛型的实现很糟糕”主题的讨论,包含了通配符很难理解和事实上泛型并不直接值类型(例如,你不能指定List<int> )。
使用擦除会有下面的几个限制:
-
instanceof 并不能用于参数化类型,只有一种情况是例外的。这个例外就是无界的通配符。例如,你不能指定Set<Shape> shapes = null; if (shapes instanceof ArrayList<Shape>){} 。相反,你需要把对instanceof 表达式修改为shapes instanceof ArrayList<?> ,这种就是无界的通配符。或者,你可以指定shapes instanceof ArrayList ,这使用的是原生类型(通常也是推荐使用的做法)。
-
编译器把泛型代码转换为非泛型代码,并保存在class文件中。一些开发人员指出擦除会使得你不能通过反射取得泛型信息,因为它们并不保存在class文件中。开发人员Jakob Jenkov在“Java 反射:泛型”中指出一些泛型信息会被保存在class文件中的情况,并且这些信息可以通过反射来访问。
-
你不能在创建数组的表达式中使用类型参数;例如,elements = new E[size]; 。如果你这样做,编译器会报告泛型数组创建错误信息。
鉴于擦除的限制,你会奇怪为什么泛型要通过擦除来实现。原因很简单:Java编译器被重构来使用擦除,因此泛型代码可以跟那些非泛型的遗留代码进行交互。没有这个向后兼容性,遗留代码在支持泛型的Java编译器上编译时将会报错。
第一部分总结
Java语言已经添加了许多新特性。在这篇文章中,我展示了怎么使用断言来增强你在代码正确性上的信心,和如何使用泛型来消除ClassCastException 。通过使用断言和泛型,你可以编写更可靠的代码,并且使你的代码在运行时的错误降到最低,当然,也减少面对生气的客户时的头痛了。
Java 5 是Java平台历史上的一个重大发布,虽然泛型比其他特性都更具争议,但它却比其他都更加重要。我的下篇文章将会介绍另外7个在Java5时加入的必要的特性:类型安全的枚举,注解,自动装箱和拆箱,加强的循环,静态引入,可变参数,协变返回类型。在那之前,下载这篇文章的源代码,它包含了更多的关于断言和泛型的提示和例子。
Jeff Friesen是一个自由职业导师和侧重Java和Android的软件开发人员。除了为Apress写Java和Android书籍,Jeff为JavaWorld,informIT,Java.net,DevSource和SitePoint写了大量的关于Java和其他技术的文章。你可以通过他在TutorTutor.ca的网站联系到他
了解更多主题相关
下载文章的源代码
阅读Angelika Langer的Java Generics FAQs,那里有着大量的关于Java语言泛型的信息和观点。
对于想学习Java语言和它的备受争议的特性的,Langer的文章理解闭包的争论(2008.6 JavaWorld)对比了Java 7语言中的三个关于添加闭包和lambda表达式的初始提议。
可以查看“Java反射:泛型”(Jakob Jenkov, Jenkov.com)关于泛型反射和某些情况下可以在运行时获取泛型信息的讨论。
Java无痛并发编程,第一部(2013.6):介绍了Executor框架,同步类型和Java并发集合包。
Java无痛并发编程,第二部(2013.8):介绍了锁,原子变量和fork/join操作,还附加了Java8中关于java.util.concurrent 的修改概述。
跟上Java Date和Time API(2013.4):介绍了Java8的JSR310:Date 和Time API,并且展示了你最有可能使用的java.time 系列类的使用。
JavaWorld中更多关于Java集合框架的文章:
Java集合框架从零开始(1998.11 Dan Becker):这篇文章介绍了集合刚引入java时的历史。
Java集合中的省时习惯(2013.9 Java Q&A blog):展望未来,Jeff Friesen回答一些当前使用Java集合的常见问题。
|