当前位置:文档之家› Java泛型编程指南

Java泛型编程指南

Java泛型编程指南
Java泛型编程指南

此系列文章译自SUN的泛型编程指南, 看不懂译文的请看原文

https://www.doczj.com/doc/af14923222.html,/j2se/1.5/pdf/generics-tutorial.pdf

Java泛型编程指南

一、绪言

JDK1.5对JAVA语言进行了做了几个扩展,其中一个就是泛型。

本指南旨在介绍泛型。如果你熟悉其它语言的构造类似的东西,特别是C++的模

板(template),你会很快发现它们之间的相同点及重要的不同点;如果你在其他

地方没看到过类似的东西,那反而更好,那样你就可以开始全新的学习,用不着去忘

掉那些(对JAVA泛型)容易产生误解的东西。

泛型允许你对类型进行抽象。最常见的例子是容器类型,比如那些在Collection

层次下的类型。

下面是那类例子的典型用法:

List myIntList = new LinkedList();//1

myIntList.add(new Integer(0));//2

Integer x = (Integer) myIntList.iterator().next();//3

第3行里的强制类型转换有点烦人,程序通常都知道一个特定的链表(list)里

存放的是何种类型的数据,但却一定要进行类型转换。编译器只能保证迭代器返回的是一个对象,要保证对Integer类型变量的赋值是类型安全的话,必须进行类型转换。

类型转换不但会引起程序的混乱,还可能会导致运行时错误,因为程序员可能会

犯错误。

如果程序员可以如实地表达他们的意图,即标记一个只能包含特定数据类型的链

表,那会怎么样呢?这就是泛型背后的核心思想。下面是前面代码的泛型写法:

List myIntList = new LinkedList();//1'

myIntList.add(new Integer(0));//2'

Integer x = myIntList.iterator().next();//3'

请注意变量myIntList的类型声明,它指明了这不仅仅是一个任意的List,还

是一个Integer类型的List,写作List。我们说List是一个接受类型(在

这个例子是Integer)参数的泛华的接口,在创建链表对象的时候,我们也指定了一个

类型参数。

另外要注意的是在第3'行的类型转换已经不见了。

现在你可能会想,我们所做的全部都是为了把混乱消除。我们没有在第3行把类

型转换为Integer,而是在第1'行加了Integer类型参数;非也非也,这里面差别很

大,编译器现在能够在编译期间检测程序的类型正确性。当我们把myIntList声明为

类型List的后,就意味着变量myIntList在何时何地的使用都是正确的,

编译器保证了这一点。相反,类型转换只是告诉我们程序员认为它在程序的某个地方

是正确的。

实际的结果是,程序(特别是大型的程序)的可读性和健壮性得到了提高。

免费linux公开课,,现在报名!

二、定义简单的泛型

下面是java.util包里的List和Iterator接口定义的一个小小的引用:

public interface List{

void add(E x);

Iterator iterator();

}

public interface Iterator{

E next();

boolean hasNext();

}

除了尖括号里的东西,这里所有的都应该很熟悉了。那是List和Iterator接口

的规范类型参数的声明。

类型参数可以用在任何的泛型声明中,就像使用普通的类型一样(虽然有一些很

重要的限制;看第7部分)。

在绪言中,我们看到了List泛型声明的调用,比如List。在调用里面

(通常称为参数化类型),所有出现规范类型参数(这里是E)的全部都用实际的类型

参数(这里是Integer)所代替。

你可以想象成List代表所有E都用Integer代替了的List:

public interface IntegerList{

void add(Integer x)

Iterator iterator();

}

这种想法是有所帮助的,但也会造成误解。

它是有所帮助的,是因为参数化类型List有看起来像这种扩展的方法。

它会造成误解,是因为泛型的声明实际上不会像那样去扩展;在源代码中、二进制

文件中、硬盘和内在里,都没有代码的多个拷贝。如果你是一个C++程序员,你会明白

这跟C++的模板(template)很不同。

泛型声明是一次编译,永远使用,它会变成一个单独的class文件,就像一个普通

的类或接口声明。

类型参数跟用在方法或构造函数里的普通的参数类似,就像一个方法具有描述它运

算用到的值的类型的规范值参一样,泛化声明具有规范类型参数。当一个方法被调用的时候,实际的参数将会被规范参数所代替而对方法求值。当一个泛化声明被调用的时候,实际类型参数将会代替规范类型参数。

命名惯例要注意的一个地方。我们建议你用一些简炼(如果可以的话只用一个字

符)但却映眼的名字作为规范类型参数名。在那些名字中最后避免小写字母,这样可

以很容易把规范类型参数和普通的类或接口区分开来。就像前面的例子一样,很多容

器类型使用E。我们将会在后面的例子里看到其他的惯例。

免费linux公开课,,现在报名!

三、泛型和子类化

https://www.doczj.com/doc/af14923222.html,

我们来测试一下对泛型的理解,下面的代码是否正确呢?

List ls = new ArrayList();//1

List lo = ls;//2

第1行肯定是正确的,问题的难点在于第2行;这样就归结为这个问题:一个字符

串(String)链表(List)是不是一个对象链表?大部分人的直觉是:“肯定了!”

那好,看一下下面这两行:

lo.add(new Object());//3

String s = ls.get(0);//4:企图把一个对象赋值给字符串!

在这里我们把ls和lo搞混淆了。我们通过别名lo来访问字符串链表ls,插入不

确定对象;结果就是ls不再存储字符串,当我们尝试从里面取出数据的时候就会出错。Java编译器当然不允许这样的事情发生了,所以第2行肯定会编译出错。

一般来说,如果Foo是Bar的子类型(子类或子接口),而G又是某个泛型声明的

话,G并不是G的子类型。这可能是学习泛型的时候最难的地方,因为它

与我们的深层直觉相违背。

直觉出错的问题在于它把集合里的东西假想为不会改变的,我们的本能把这些东

西看作是不变的。

举个例子,假设汽车公司为人口调查局提供一份驾驶员的列表,这看上去挺合理。

假设Driver是Person的一个子类,则我们认为List是一个List。而实际上提交的是一份驾驶员登记表的一个副本。否则的话,人口调查局将可以驾驶员的人加入到那份列表中去,汽车公司的纪录受到破坏。

为了解决这类问题,我们需要考虑一些更灵活的泛型,到现在为止碰到的规则太

受约束了。

四、通配符

考虑一下写一个程序来打印一个集合对象(collection)里的所有元素。

在旧版的语言里面,你可以会像下面那样写:

void printCollection(Collection c){

Iterator i = c.iterator();

for (k = 0; k < c.size(); k++){

System.out.println(i.next());

}

}

下面尝试着用泛型(和新的for循环语法)来写:

void printCollection(Collection c){

for (Object e : c) {

System.out.println(e);

}

}

这样的问题是新版本的代码还没旧版本的代码好用。就像我们刚示范的一样,

Collection并不是所有类型的集合的父类型,所以它只能接受

Collection

对象,而旧版的代码却可以把任何类型的集合对象作为参数来调用。

那么,什么才是所有集合类型的父类型呢?这个东西写作Collection(读

作“未知集合”),就是元素类型可以为任何类型的集合。这就是它为什么被称为“通

配符类型”的原因。我们可以这样写:

void printCollection(Collection c){

for (Object e : c) {

System.out.println(e);

}

}

现在,我们就可以以任何类型的集合对象作为参数来调用了。注意,在printCollection() 方法里面,我们仍然可以从c对象中读取元素并赋予Object类型;因为无论集合里

实际包含了什么类型,它肯定是对象,所以是类型安全的。但对它插入任意的对象

的话则是不安全的:

Collection c = new ArrayList();

c.add(new Object());//编译错误

由于我们并不知道c的元素类型是什么,因此我们不能对其插入对象。add()方法

接受类型E,即集合的元素类型的参数。当实际的类型参数是?的时候,就代表是某未

知类型。任何传递给add方法的参数,其类型必须是该未知类型的子类型。因为我们并

不知道那是什么类型,所以我们传递不了任何参数。唯一的例外就是null,因为它是任

何(对象)类型的成员。

另外,假设有一个List,我们可以调用get()方法并使用其返回结果。结果

类型是一个未知类型,但我们都知道它是一个对象。因此把get()方法的返回结果赋

值给对象类型,或者把它作为一个对象参数传递都是类型安全的。

四、1-有界通配符

考虑一个简单的画图程序,它可以画长方形和圆等形状。为了表示这些形状,

你可能会定义这样的一个类层次结构:

public abstract class Shape{

public abstract void draw(Canvas c);

}

public class Circle extends Shape{

private int x, y, radius;

public void draw(Canvas c) { ... }

public class Rectangle extends Shape {

private int x, y, width, height;

public void draw(Canvas c) { ... }

}

这些类可以在canvas上描画:

public class Canvas {

public void draw(Shape s) {

s.draw(this);

}

}

任何的描画通常都包括有几种形状,假设它们用一个链表来表示,那么如果在Canvas里面有一个方法来画出所有的形状的话,那将会很方便:

public void drawAll(List shapes) {

for (Shape s: shapes) {

s.draw(this);

}

}

但是现在,类型的规则说drawAll()方法只能对确切的Shape类型链表调用,

比如,它不能对List类型调用该方法。那真是不幸,因为这个方法所要

做的就是从链表中读取形状对象,从而对List类型对象进行调用。我们

真正所想的是要让这个方法能够接受一个任何形状的类型链表:

public void drawAll(List shapes) { ... }

这里有一个很小但很重要的不同点:我们把类型List替换为List

现在drawAll()方法可以接受任何Shape子类的链表,我们就可以如愿的对List 调用进行啦。

List是一个有界通配符的例子。? 表示一个未知类型,

就像我们之前所看到的通配符一样。但是,我们知道在这个例子里面这个未知类型

实际是Shape的子类型(注:它可以是Shape本身,或者是它的子类,无须在字面上

表明它是继承Shape类的)。我们说Shape是通配符的“上界”。

如往常一样,使用通配符带来的灵活性得要付出一定的代价;代码就是现在在

方法里面不能对Shape对象插入元素。例如,下面的写法是不允许的:

public void addRectangle(List shapes) {

shapes.add(0, new Rectangle()); //编译错误

}

你应该可以指出为什么上面的代码是不允许的。shapes.add()方法的第二个

参数的类型是 ? 继承Shape,也就是一个未知的Shape的子类型。既然我们不知道

类型是什么,那么我们就不知道它是否是Rectangle的父类型了;它可能是也可能

不是一个父类型,因此在那里传递一个Rectangle的对象是不安全的。

有界通配符正是需要用来处理汽车公司给人口调查局提交数据的例子方法。在

我们的例子里面,我们假设数据表示为姓名(用字符串表示)对人(表示为引用类

型,比如Person或它的子类型Driver等)的映射。Map是有两个类型参数的

一个泛型的例子,表示键值映射。

请再一次注意规范类型参数的命名惯例:K表示键,V表示值。

public class Census {

public static void

addRegistry(Map registry){ ... }

}

...

Map allDrivers = ...;

Census.addRegistry(allDrivers);

免费linux公开课,,现在报名!

五、泛型方法

考虑写这样一个方法,它接收一个数组和一个集合(collection)作为参数,

并把数组里的所有对象放到集合里面。

先试试这样:

static void fromArrayToCollection(Object[] a, Collection c){

for (Object o : a){

c.add(o);//编译错误

}

}

到现在,你应该学会了避免把Collection作为集合参数的类型这种初学

者的错误;你可能或可能没看出使用Collection也是不行的,回想一下,你是不能把对象硬塞进一个未知类型的集合里面的。

解决这类问题的方法是使用泛型方法。就像类型声明一样,方法也可以声明为泛型的,就是说,用一个或多个类型参数作为参数。

static void fromArrayToCollection(T[]a, Collection c){

for (T o : a){

c.add(o);//正确

}

}

对于集合元素的类型是数组类型的父类型,我们就可以调用这个方法。

Object[] oa = new Object[100];

Collection co = new ArrayList();

fromArrayToCollection(oa, co);// T是对象类型

String[] sa = new String[100];

Collection cs = new ArrayList();

fromArrayToCollection(sa, cs);// T是字符串类型(String)fromArrayToCollection(sa, co);// T对象类型

Integer[] ia = new Integer[100];

Float[] fa = new Float[100];

Number[] na = new Number[100];

Collection cn = new ArrayList();

fromArrayToCollection(ia, cn);// T是Number类型

fromArrayToCollection(fa, cn);// T是Number类型

fromArrayToCollection(na, cn);// T是Number类型

fromArrayToCollection(na, co);// T是Number类型

fromArrayToCollection(na, cs);// 编译错误

请注意,我们并没有把实际的类型实参传递给泛型方法,因为编译器会根据

实参的类型为我们推断出类型实参。一般地,编译器推断得到可以正确调用的最

接近的(the most specific)实参类型。

现在有一个问题:我应该什么时候使用泛型方法,什么时候使用通配符类型

呢?为了明白这个问题的答案,我们来看看Collection库里的几个方法:

interface Collection{

public boolean containsAll(Collection c);

public boolean addAll(Collection c);

}

在这里我们也可以用泛型方法:

interface Collection{

public boolean containsAll(Collection c);

public boolean addAll(Collection c);

//哈哈,类型变量也可以有界!

}

但是,类型参数T在containsAll和addAll两个方法里面都只是用了一次。返

回类型并不依赖于类型参数或其他传递给该方法的实参(这种是只有一个实参的简单情况)。这就告诉我们类型实参是用于多态的,它的作用只是对不同的调用可以有一系列的实际的实参类型。如果是那样的话,就应该使用通配符,通配符就是设计来支持灵活的子类型的,这也是我们这里所要表述的东西。

泛型方法允许类型参数用于表述一个或多个的实参类型对方法或及其返回类型的

依赖关系。如果没有那样的一个依赖关系的话,泛型方法就不应用使用。

也有可能是一前一后一起使用泛型方法和通配符的情况,下面是Collections.copy() 方法:

class Collections {

public static void copy(List dest, list< ? extends T> src) {...}

}

请注意这里两个参数类型的依赖关系,任何要从源链表src复制过来的对象都必

须是对目标链表dst元素可赋值的;所以我们可以不管src的元素类型是什么,只要它是T类型的子类型。copy方法的方法头表示了使用一个类型参数,但是用通配符来作为第二个参数的元素类型的依赖关系。

我们是可以用另外一种不用通配符来写这个方法头的办法。

class Collections {

public static

vod copy(List dest, List src) { ...}

}

没问题,但是当第一个类型参数用作dst的类型和批二个类型参数S的上界的

时候,S它本身在src类型里只能使用一次,没有其他的东西依赖于它。这就意味

着我们可以用一个通配符来代替S了。使用通配符比声明显式的类型参数要来得清

晰和简单,因此在可能的话都优先使用通配符。

当通配符用于方法头外部,作为成员变量、局部变量和数组的类型的时候,同

样也有优势。请看下面的例子。

看回我们之前画图的那个问题,现在我们想要保留一份画图请求的历史记录。

我们可以这样来维护这份历史记录,在Shape类里用一个静态的变量表示历史记录,然后在drawAll()方法里面把传递的实参储存到那历史记录变量里头。

static List> history =

new ArrayList>();

public void drawAll(List shapes){

history.addLast(shapes);

for (Shape s: shapes) {

s.draw(this);

}

}

最后,我们再次留意一下使用类型参数的命名惯例。当没有更精确的类型来

区分的时候,我们用T来表示类型,这是通常是在泛型方法里面的情况。如果有多个类型参数,我们可以用在字母表中与T相邻的字母来表示,比如S。如果一个泛型方法出现在一个泛型类里面,一个好的方法就是,应该避免对方法和类使用相同的类型参数以免发生混淆。这在嵌套泛型类里也一样。

免费linux公开课,,现在报名!

六、与遗留代码的交互

到现在为止,我们所有的例子都是在一个假想的理想世界里面的,就是所有的

人都在使用Java语言支持泛型的最新版本。

唉,不过在现实中情况却不是那样。千百万行的代码都是用早期版本的语言

来编写的,不可能把它们全部在一夜之间就转换过来。

在后面的第10部分,我们将会解决把遗留代码转为用泛型这个问题。在这部分

我们要看的是比较简单的问题:遗留代码与泛型代码如何交互?这个问题分为两个部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。

免费linux公开课,,现在报名!

六-1 在泛型代码中使用遗留代码

[url=https://www.doczj.com/doc/af14923222.html,][url]

当你在享受在代码中使用泛型带来的好处的时候,你怎么样使用遗留代码呢?

假设这样一个例子,你要使用com.Foodlibar.widgets这个包。https://www.doczj.com/doc/af14923222.html, 的人要销售一个库存控制系统,主要部分如下:

package com.Fooblibar.widgets;

public interface Part { ... }

public class Inventory {

/**

*Adds a new Assembly to the inventory databse.

*The assembly is given the name name, and consists of a set

*parts specified by parts. All elements of the collection parts

*must support the Part interface.

**/

public static void addAssembly(String name, Collection parts) {...}

public static Assembly getAssembly(String name) {...}

}

public interface Assembly{

Collection getParts();//Returns a collection of Parts

}

现在,你可以用上面的API来增加新的代码,它可以很好的保证你调用参数恰当

的addAssembly()方法,就是说传递的集合是一个Part类型的Collection对象,当

然,泛型是最适合做这个:

package com.mycompany.inventory;

import com.Fooblibar.widgets.*;

public class Blade implements Part{

...

}

public class Guillotine implements Part {

}

public class Main {

public static void main(Sring[] args) {

Collection c = new ArrayList();

c.add(new Guillotine());

c.add(new Blade());

Inventory.addAssembly("thingee", c);

Collection k = Inventory.getAssembly("thingee").getParts();

}

}

当我们调用addAssembly方法的时候,它想要的第二个参数是Collection类型的,

实参是Collection类型,但却可以,为什么呢?毕竟,大多数集合存储的都不是Part对象,所以总的来说,编译器不会知道Collection存储的是什么类型的集合。

在正规的泛型代码里面,Collection都带有类型参数。当一个像Collection这样

的泛型不带类型参数使用的时候,称之为原生类型。

很多人的第一直觉是Collection就是指Collection,但从我们先前所

看到的可以知道,当需要的对象是Collection,而传递的却是Collection 对象的时候,是类型不安全的。确切点的说法是Collection类型表示一个未知类型的

集合,就像Collection

稍等一下,那样做也是不正确的!考虑一下调用getParts()方法,它返回一个

Collection对象,然后赋值给k,而k是Collection类型的;如果调用的结果

是返回一个Collection的对象,这个赋值可能是错误的。

事实上,这个赋值是允许的,只是它会产生一个未检测警告。警告是需要的,因为

编译器不能保证赋值的正确性。我们没有办法通过检测遗留代码中的getAssembly()方法

来保证返回的集合的确是一个类型参数是Part的集合。程序里面的类型是Collection,

我们可以合法的对此集合插入任何对象。

所以,这不应该是错误的吗?理论上来说,答案是:是;但实际上如果是泛型代码

调用遗留代码的话,这又是允许的。对这个赋值是否可接受,得取决于程序员自己,在这个例子中赋值是安全的,因为getAssembly()方法约定是返回以Part作为类型参数的集合,尽管在类型标记中没有表明。

所以原生类型很像通配符类型,但它们没有那么严格的类型检测。这是有意设计成

这样的,从而可以允许泛型代码可以与之前已有的遗留代码交互。

在泛型代码中调用遗留代码固然是危险的,一旦把泛型代码和非泛型代码混合在一

起,泛型系统所提供的全部安全保证就都变得无效了。但这仍比根本不使用泛型要好,最起码你知道你的代码是一致的。

泛型代码出现的今天,仍然有很多非泛型代码,二者混合同时使用是不可避免的。

如果一定要把遗留代码与泛型代码混合使用,请小心留意那些未检测警告。仔细的

想想如何才能判定引发警告的代码是安全的。

如果仍然出错,代码引发的警告实际不是类型安全的,那又怎么样呢?我们会看

那样的情况,接下来,我们将会部分的观察编译器的工作方式。

免费linux公开课,,现在报名!

六-2 擦除和翻译

public String loophole(Integer x){

List ys = new LinkedList();

List xs = ys;

xs.add(x);//编译时未检测警告

return ys.iterator().next();

}

在这里我们定义了一个字符串类型的链表和一个一般的老式链表,我们先插入

一个Integer对象,然后试图取出一个String对象,很明显这是错误的。如果我们

忽略警告继续执行代码的话,程序将会在我们使用错误类型的地方出错。在运行时,

代码执行大致如下:

public String loophole(Integer x) {

List ys = new LinkedList;

List xs = ys;

xs.add(x);

return (String)ys.iterator().next();//运行时出错

}

当我们要从链表中取出一个元素,并把它当作是一个字符串对象而把它转换为

String类型的时候,我们将会得到一个ClassCastException类型转换异常。在

泛型版本的loophole()方法里面发生的就是这种情况。

出现这种情况的原因是,Java的泛型是通过一个前台转换“擦除”的编译器实现

的,你基本上可以认为它是一个源码对源码的翻译,这就是为何泛型版的loophole()

方法转变为非泛型版本的原因。

结果是,Java虚拟机的类型安全性和完整性永远不会有问题,就算出现未检测

的警告。

基本上,擦除会除去所有的泛型信息。尖括号里面的所有类型信息都会去掉,比如,参数化类型的List会转换为List。类型变量在之后使用时会被类型

变量的上界(通常是Object)所替换。当最后代码不是类型正确的时候,就会加入一个适当的类型转换,就像loophole()方法的最后一行。

对“擦除”的完整描述不是本指南的范围内的内容,但前面我们所给的简单描述

也差不多是那样了。了解这点很有好处,特别是当你想做诸如把现有API转为使用泛型(请看第10部分)这样复杂的东西,或者是想知道为什么它们会那样的时候。免费linux公开课,,现在报名!

六-3 在遗留代码中使用泛型

现在我们来看看相反的情况。假设https://www.doczj.com/doc/af14923222.html,把他们的API转换为泛型的,

但有些客户还没有转换。代码就会像下面的:

package com.Fooblibar.widgets;

public interface Part { ... }

publlic class Inventory {

/**

*Adds a new Assembly to the inventory database.

*The assembly is given the name name, and consists of a set

*parts specified by parts. All elements of the collection parts

*must support the Part interface.

**/

public static void addAssembly(String name, Collection parts) {...} public static Assembly getAssembly(String name){ ... }

}

public interface Assembly {

Collection getParts();//Return a collection of Parts

}

客户代码如下:

package com.mycompany.inventory;

import com.Fooblibar.widgets.*;

public class Blade implements Part {

...

}

public class Guillotine implements Part {

...

public class Main {

public static void main(String[] args){

Collection c = new ArrayList();

c.add(new Guillotine());

c.add(new Blade());

Inventory.addAssembly("thingee", c);//1: unchecked warning

Collection k = Inventory.getAssembly("thingee").getParts();

}

}

客户代码是在引进泛型之前写下的,但是它使用了com.Fooblibar.widgets包和集

合库,两个现在都是在用泛型的。在客户代码里面使用的泛型全部都是原生类型。

第1行产生一个未检测警告,因为把一个原生Collection传递给了一个需要Part类型的Collection的地方,编译器不能保证原生的Collection是一个Part类型的Collection。不这样做的话,你也可以在编译客户代码的时候使用source 1.4这个标记来保证不

会产生警告。但是这样的话你就不能使用所有JDK 1.5引入的新的语言特性。

免费linux公开课,,现在报名!

七、晦涩难懂的部分

七-1 泛型类为所有调用所共享

下面的代码段会打印出什么呢?

List l1 = new ArrayList();

List l2 = new ArrayList();

System.out.println(l1.getClass() == l2.getClass());

你可能会说是false,但是你错了,打印的是true,因为所有泛型类的实例它们

的运行时的类(run-time class)都是一样的,不管它们实际类型参数如何。

泛型类之所以为泛型的,是因为它对所有可能的类型参数都有相同的行为,相同

的类可以看作是有很多不同的类型。

结果就是,一个类的静态的变量和方法也共享于所有的实例中,这就是为什么不

允许在静态方法或初始化部分、或者在静态变量的声明或初始化中引用类型参数。

免费linux公开课,,现在报名!

七-2 强制类型转换和instanceof

泛型类在它所有的实例****享,就意味着判断一个实例是否是一个特别调用的泛

型的实例是毫无意义的:

Collection cs = new ArrayList();

if (cs instanceof Collection) {...}//非法

类似地,像这样的强制类型转换:

Collection cstr = (Collection) cs;//未检测警告

给出了一个未检测的警告,因为这里系统在运行时并不会检测。

对于类型变量也一样:

T BadCast(T t, Object o) {

return (T) o;//未检测警告

}

类型变量不存在于运行时,这就是说它们对时间或空间的性能不会造成影响。

但也因此而不能通过强制类型转换可靠地使用它们了。

免费linux公开课,,现在报名!

七-3 数组

数组对象的组件类型可能不是一个类型变量或一个参数化类型,除非它是一个

(无界的)通配符类型。你可以声明元素类型是类型变量和参数华类型的数组类型,但元素类型不能是数组对象。

这自然有点郁闷,但这个限制对避免下面的情况是必要的:

List[] lsa = new List[10];//实际上是不允许的

Object o = lsa;

Object[] oa = (Object[]) o;

List li = new ArrayList();

li.add(new Integer(8));

oa[1] = li;//不合理,但可以通过运行时的赋值检测

String s = lsa[1].get(0);//运行时出错:ClassCastException异常

如果参数化类型的数组允许的话,那么上面的例子编译时就不会有未检测的警告,但在运行时出错。对于泛型编程,我们的主要设计目标是类型安全,而特别的是这个语言的设计保证了如果使用了javac -source 1.5来编译整个程序而没有未检测的警告的话,它是类型安全的。

但是你仍然会使用通配符数组,这与上面的代码相比有两个变化。首先是不使用

数组对象或元素类型被参数化的数组类型,这样我们就需要在从数组中取出一个字符串的时候进行强制类型转换:

List[] lsa = new List[10];//没问题,无界通配符类型数组

Object o = lsa;

Object[] oa = (Object[]) o;

List li = new ArrayList();

li.add(new Integer(3));

oa[1] = li;//正确

String s = (String) lsa[1].get(0);//运行时错误,显式强制类型转换

第二个变化是,我们不创建元素类型被参数化的数组对象,但仍然使用参数化元素

类型的数组类型,这是允许的,但引起现未检测警告。这样的程序实际上是不安全的,甚至最终会出错。

List[] lsa = new List[10];//未检测警告-这是不安全的!

Object o = lsa;

Object[] oa = (Object[]) o;

List li = new ArrayList();

li.add(new Integer(3));

oa[1]=li;//正确

String s = lsa[1].get(0);//运行出错,但之前已经被警告

类似地,想创建一个元素类型是类型变量的数组对象的话,将会编译出错。

T[] makeArray(T t){

return new T[100];//错误

}

因为类型变量并不存在于运行时,所以没有办法知道实际的数组类型是什么。

要突破这类限制,我们可以用第8部分说到的用类名作为运行时标记的方法。

免费linux公开课,,现在报名!

八、把类名作为运行时的类型标记

JDK1.5中的一个变化是https://www.doczj.com/doc/af14923222.html,ng.Class是泛化的,一个有趣的例子是对

容器外的东西使用泛型。

现在Class类有一个类型参数T,你可能会问,T代表什么啊?它就代表Class

对象所表示的类型。

比如,String.class的类型是Class,Serializable.class的

类型是Class,这可以提高你的反射代码中的类型安全性。

特别地,由于现在Class类中的newInstance()方法返回一个T对象,因此

在通过反射创建对象的时候可以得到更精确的类型。

其中一个方法就是显式传入一个factory对象,代码如下:

interface Factory {T make();}

public Collection select(Factory factory, String statement){ Collection result = new ArrayList();

//用JDBC运行SQL查询

for(/*遍历JDBC结果*/){

T item = factory.make();

/*通过SQL结果用反射和设置数据项*/

result.add(item);

}

return result;

}

你可以这样调用:

select(new Factory(){ public EmpInfo make() {

return new EmpInfo();

}}

, "selection string");

或者声明一个EmpInfoFactory类来支持Factory接口:

class EmpInfoFactory implements Factory{

...

public EmpInfo make() { return new EmpInfo();}

}

然后这样调用:

select(getMyEmpInfoFactory(), "selection string");

这种解决办法需要下面的其中之一:

· 在调用的地方使用详细的匿名工厂类(verbose anonymous factory classes),或者· 为每个使用的类型声明一个工厂类,并把工厂实例传递给调用的地方,这样有点不自然。

使用类名作为一个工厂对象是非常自然的事,这样的话还可以为反射所用。现在

没有泛型的代码可能写作如下:

Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps"); ...

public static Collection select(Class c, String sqlStatement) {

Collection result = new ArrayList();

/*用JDBC执行SQL查询*/

for(/*遍历JDBC产生的结果*/){

Object item = c.newInstance();

/*通过SQL结果用反射和设置数据项*/

result.add(item);

}

return result;

}

但是,这样并不能得到我们所希望的更精确的集合类型,现在Class是泛化的,

我们可以这样写:

Collection emps =

sqlUtility.select(EmpInfo.class, "select * from emps");

...

public static Collection select(Class c, String sqlStatement) { Collection result = new ArrayList();

/*用JDBC执行SQL查询*/

for(/*遍历JDBC产生的结果*/){

T item = c.newInstance();

/*通过SQL结果用反射和设置数据项*/

result.add(item);

}

return result;

}

这样就通过类型安全的方法来得到了精确的集合类型了。

这种使用类名作为运行时类型标记的技术是一个很有用的技巧,是需要知道的。

在处理注释的新的API中也有很多类似的情况。

免费linux公开课,,现在报名!

九通配符的其他作用

(more fun with wildcards,不知道如何译才比较妥当,呵呵。)

在这部分,我们将会仔细看看通配符的几个较为深入的用途。我们已经从几个

有界通配符的例子中看到,它对从某一数据结构中读取数据是很有用的。现在来看看相反的情况,只对数据结构进行写操作。

下面的Sink接口就是这类情况的一个简单的例子:

interface Sink {

flush(T t);

}

我们可以想象在下面的示范的例子中使用它,writeAll()方法用于把coll集合

里的所有元素填充(flush)到Sink接口变量snk中,并返回最后一个填充的元素。

public static T writeAll(Collection coll, Sink snk){

T last;

for (T t: coll){

last = t;

snk.flush(last);

}

return last;

}

...

Sink s;

Collection cs;

String str = writeAll(cs, s);//非法调用

如注释所注,这里对writeAll()方法的调用是非法的,因为无有效的类型参数

可以引用;String和Object都不适合作为T的类型,因为Collection和Sink的元素必须是相同类型的。

我们可以通过使用通配符来改写writeAll()的方法头来处理,如下:

public static T writeAll(Collection, Sink) {...}

...

String str = writeAll(cs, s);//调用没问题,但返回类型错误

现在调用是合法的了,但由于T的类型跟元素类型是Object的s一样,因为返回的

类型也是Object,因此赋值是不正确的。

解决办法是使用我们之前从未见过的一种有界通配符形式:带下界的通配符。

语法 ? super T 表示了是未知的T的父类型,这与我们之前所使用的有界

(父类型:或者T类型本身,要记住的是,你类型关系是自反的)

通配符是对偶有界通配符,即用 ? extends T 表示未知的T的子类型。

public static T writeAll(Collection coll, Sink snk) {...} ...

String str = writeAll(cs, s);//正确!

使用这个语法的调用是合法的,指向的类型是所期望的String类型。

现在我们来看一个比较现实一点的例子,java.util.TreeSet表示元素类型

是E的树形数据结构里的元素是有序的,创建一个TreeSet对象的一个方法是使用参数是Comparator对象的构造函数,Comparator对象用于对TreeSet对象里的元素进行

所期望的排序进行分类。

TreeSet(Comparator c)

Comparator接口是必要的:

interface Comparator {

int compare(T fst, T snd);

}

假设我们想要创建一个TreeSet对象,并传入一下合适的Comparator

对象,我们传递的Comparator是能够比较字符串的。我们可以用Comparator,但Comparator也是可以的。但是,我们不能对Comparator对象

调用上面所给的构造函数,我们可以用一个下界通配符来得到我们想要的灵活性:TreeSet(Comparator c)

这样就可以使用适合的Comparator对象啦。

最后一个下界通配符的例子,我们来看看Collections.max()方法,这个方法

返回作为参数传递的Collection对象中最大的元素。

现在,为了max()方法能正常运行,传递的Collection对象中的所有元素都必

须是实现了Comparable接口的,还有就是,它们之间必须是可比较的。

先试一下泛化方法头的写法:

public static >

T max(Collection coll)

那样,方法就接受一个自身可比较的(comparable)某个T类型的Collection

对象,并返回T类型的一个元素。这样显得太束缚了。

来看看为什么,假设一个类型可以与合意的对象进行比较:

class Foo implements Comparable {...}

...

Collection cf = ...;

Collectins.max(cf);//应该可以正常运行

cf里的每个对象都可以和cf里的任意其他元素进行比较,因为每个元素都是Foo

的对象,而Foo对象可以与任意的对象进行比较,特别是同是Foo对象的。但是,使用上面的方法头,我们会发现这样的调用是不被接受的,指向的类型必须是Foo,但Foo 并没有实现Comparable

T对于自身的可比性不是必须的,需要的是T与其父类型是可比的,就像下面:

(实际的Collections.max()方法头在后面的第10部分将会讲得更多)

public static >

T max(Collection coll)

这样推理出来的结果基本上适用于想用Comparable来用于任意类型的用法:

就是你想这样用Comparable

总的来说,如果你有一个只能一个T类型参数作为实参的API的话,你就应该用

下界通配符类型(? suer T);相反,如果API只返回T对象,你就应该用上界通

配符类型(? extends T),以使得你的客户的代码有更大的灵活性。

JDK 5.0 中增加的泛型类型,是 Java 语言中类型安全的一次重要改进。但是,对于初次使用泛型类型的用户来说,泛型的某些方面看起来可能不容易明白,甚至非常奇怪。在本月的“Java 理论和实践”中,Brian Goetz 分析了束缚第一次使用泛型的用户的常见陷阱。您可以通过讨论论坛与作者和其他读者分享您对本文的看法。(也可以单击本文顶端或底端的讨论来访问这个论坛。)

表面上看起来,无论语法还是应用的环境(比如容器类),泛型类型(或者泛型)都类似于 C++ 中的模板。但是这种相似性仅限于表面,Java 语言中的泛型基本上完全在编译器中实现,由编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码。这种实现技术称为擦除(erasure)(编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除),这项技术有一些奇怪,并且有时会带来一些令人迷惑的后果。虽然范型是 Java 类走向类型安全的一大步,但是在学习使用泛型的过程中几乎肯定会遇到头痛(有时候让人无法忍受)的问题。

注意:本文假设您对 JDK 5.0 中的范型有基本的了解。

泛型不是协变的

虽然将集合看作是数组的抽象会有所帮助,但是数组还有一些集合不具备的特殊性质。Java 语言中的数组是协变的(covariant),也就是说,如果 Integer 扩展了 Number(事实也是如此),那么不仅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 Number 是 Integer 的超类型,那么 Number[] 也是 Integer[] 的超类型)。您也许认为这一原理同样适用于泛型类型—— List 是 List 的超类型,那么可以在需要 List 的地方传递 List。不幸的是,情况并非如此。

不允许这样做有一个很充分的理由:这样做将破坏要提供的类型安全泛型。如果能够将List 赋给 List。那么下面的代码就允许将非 Integer 的内容放入List

因为 ln 是 List,所以向其添加 Float 似乎是完全合法的。但是如果 ln 是 li 的别名,那么这就破坏了蕴含在 li 定义中的类型安全承诺——它是一个整数列表,这就是泛型类型不能协变的原因。

其他的协变问题

java泛型详解

java泛型详解 泛型(Generic type 或者generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。 可以在集合框架(Collection framework)中看到泛型的动机。例如,Map类允许您向一个Map添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如String)的对象。 因为Map.get()被定义为返回Object,所以一般必须将Map.get()的结果强制类型转换为期望的类型,如下面的代码所示: Map m = new HashMap(); m.put("key", "blarg"); String s = (String) m.get("key"); 要让程序通过编译,必须将get()的结果强制类型转换为String,并且希望结果真的是一个String。但是有可能某人已经在该映射中保存了不是String的东西,这样的话,上面的代码将会抛出ClassCastException。 理想情况下,您可能会得出这样一个观点,即m是一个Map,它将String键映射到String值。这可以让您消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。 泛型的好处 Java 语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。这带来了很多好处: · 类型安全。泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。 Java 程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,比如“Str ing列表”或者“String到String的映射”。通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误现在就可以在编译时被捕获了,而不是在运行时当作 ClassCastException展示出来。将类型检查从运行时挪到编译时有助于您更容易找到错误,并可提高程序的可靠性。

Java泛型详解

Java 泛型 1 什么是泛型 (2) 2 泛型类跟接口及泛型方法 (3) 2.1 泛型类跟接口及继承 (3) 2.1.1泛型类 (3) 2.1.2继承 (3) 2.1.3接口 (3) 2.2 泛型方法 (3) 2.2.1 方法 (3) 2.2.2 类型推断 (4) 3 泛型实现原理 (5) 4 泛型数组 (6) 5边界 (7) 6通配符 (8) 7 泛型的问题及建议 (9) 7.1问题 (9) 7.2 建议 (9)

1 什么是泛型 从jdk1.5开始,Java中开始支持泛型了。泛型是一个很有用的编程工具,给我们带来了极大的灵活性。在看了《java核心编程》之后,我小有收获,写出来与大家分享。 所谓泛型,我的感觉就是,不用考虑对象的具体类型,就可以对对象进行一定的操作,对任何对象都能进行同样的操作。这就是灵活性之所在。但是,正是因为没有考虑对象的具体类型,因此一般情况下不可以使用对象自带的接口函数,因为不同的对象所携带的接口函数不一样,你使用了对象A的接口函数,万一别人将一个对象B传给泛型,那么程序就会出现错误,这就是泛型的局限性。所以说,泛型的最佳用途,就是用于实现容器类,实现一个通用的容器。该容器可以存储对象,也可以取出对象,而不用考虑对象的具体类型。因此,在学习泛型的时候,一定要了解这一点,你不能指望泛型是万能的,要充分考虑到泛型的局限性。下面我们来探讨一下泛型的原理以及高级应用。首先给出一个泛型类: public class Pair { public Pair() { first = null; second = null; } public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } public void setFirst(T newValue) { first = newValue; } public void setSecond(T newValue) { second = newValue; } private T first; private T second; } 我们看到,上述Pair类是一个容器类(我会多次强调,泛型天生就是为了容器类的方便实现),容纳了2个数据,但这2个数据类型是不确定的,用泛型T来表示。关于泛型类如何使用,那是最基本的内容,在此就不讨论了。

java泛型详解

Java 泛型详解 泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java 泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。 泛型基础 泛型类 我们首先定义一个简单的Box类: public class Box { private String object; public void set(String object) { this.object = object; } public String get() { return object; }}这是最常见的做法,这样做的一个坏处是Box里面现在只能装入String类型的元素,今后如果我们需要装入Integer等其他类型的元素,还必须要另外重写一个Box,代码得不到复用,使用泛型可以很好的解决这个问题。 public class Box { // T stands for 'Type' private T t; public void set(T t) { this.t = t; } public T get() { return t; }} 这样我们的Box类便可以得到复用,我们可以将T替换成任何我们想要的类型: Box integerBox = new Box();Box doubleBox = new

Box();Box stringBox = new Box(); 泛型方法 看完了泛型类,接下来我们来了解一下泛型方法。声明一个泛型方法很简单,只要在返回类型前面加上一个类似的形式就行了: public class Util { public static boolean compare(Pair p1, Pair p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); }}public class Pair { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; }} 我们可以像下面这样去调用泛型方法: Pair p1 = new Pair(1, 'apple');Pair p2 = new Pair(2, 'pear');boolean same = https://www.doczj.com/doc/af14923222.html,pare(p1, p2); 或者在Java1.7/1.8利用type inference,让Java自动推导出相应的类型参数: Pair p1 = new Pair(1, 'apple');Pair p2 = new Pair(2, 'pear');boolean same = https://www.doczj.com/doc/af14923222.html,pare(p1, p2);

相关主题
文本预览