java泛型文档

本文详细介绍了Java泛型的使用方法、原理及注意事项,包括泛型接口、子类继承、通配符、泛型方法等内容。通过实例演示了如何在实际编程中灵活运用泛型,提高代码的安全性与可读性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摘要和关键字... 1

1.介绍... 1

2.定义简单的泛型... 2

3.泛型和子类继承... 3

4.通配符(wildcards). 4

4.1.有限制的通配符(bounded wildcards). 5

5.泛型方法... 6

6.与旧代码交互... 9

6.1.在泛型代码中使用老代码... 9

6.2.擦除和翻译(erasure and translation). 10

6.3.在老代码中使用泛型代码... 11

7.要点(the fine print). 12

7.1.一个泛型类被其所有调用共享... 12

7.2.转型和instanceof 13

7.3.数组arrays. 13

8.class literals as run-time type tokens. 14

9.more fun with *. 16

9.1.通配符匹配(wildcard capture). 18

10.泛型化老代码... 18

11.致谢... 20

1.介绍

jdk1.5中引入了对java语言的多种扩展,泛型(generics)即其中之一。

这个教程的目标是向您介绍java的泛型(generic)。你可能熟悉其他语言的泛型,最著名的是c++的模板(templates)。如果这样,你很快就会看到两者的相似之处和重要差异。如果你不熟悉相似的语法结构,那么更好,你可以从头开始而不需要忘记误解。

generics允许对类型进行抽象(abstract over types)。最常见的例子是集合类型(container types),collection的类树中任意一个即是。

下面是那种典型用法:

list myintlist = new linkedlist();// 1

myintlist.add(new integer(0));// 2

integer x = (integer) myintlist.iterator().next();// 3

第3行的类型转换有些烦人。通常情况下,程序员知道一个特定的list里边放的是什么类型的数据。但是,这个类型转换是必须的 (essential)。编译器只能保证iterator返回的是object类型。为了保证对integer类型变量赋值的类型安全,必须进行类型转换。

当然,这个类型转换不仅仅带来了混乱,它还可能产生一个运行时错误(run time error),因为程序员可能会犯错。

程序员如何才能明确表示他们的意图,把一个list中的内容限制为一个特定的数据类型呢?这是generics背后的核心思想。这是上面程序片断的一个泛型版本:

list myintlist = new linkedlist(); // 1

myintlist.add(new integer(0)); // 2

integer x = myintlist.iterator().next(); // 3

注意变量myintlist的类型声明。它指定这不是一个任意的list,而是一个integer的list,写作:list。我们说list是一个带一个类型参数的泛型接口(a generic interface that takes a type parameter),本例中,类型参数是integer。我们在创建这个list对象的时候也指定了一个类型参数。

另一个需要注意的是第3行没了类型转换。

现在,你可能认为我们已经成功地去掉了程序里的混乱。我们用第1行的类型参数取代了第3行的类型转换。然而,这里还有个很大的不同。编译器现在能够在编译时检查程序的正确性。当我们说myintlist被声明为list类型,这告诉我们无论何时何地使用 myintlist变量,编译器保证其中的元素的正确的类型。与之相反,一个类型转换说明程序员认为在那个代码点上它应该是那种类型。

实际结果是,这可以增加可读性和稳定性(robustness),尤其在大型的程序中。

2.定义简单的泛型

下面是从java.util包中的list接口和iterator接口的定义中摘录的片断:

public interface list {

void add(e x);

iterator iterator();

}

public interface iterator {

e next();

boolean hasnext();

}

这些都应该是很熟悉的,除了尖括号中的部分,那是接口list和iterator中的形式类型参数的声明(the declarations of the formal type parameters of the interfaces list and iterator)。

类型参数在整个类的声明中可用,几乎是所有可是使用其他普通类型的地方(但是有些重要的限制,请参考第7部分)。

(原文:type parameters can be used throughout the generic declaration, pretty much where you would use ordinary types (though there are some important restrictions; see section 7))

在介绍那一节我们看到了对泛型类型声明list(the generic type declaration list)的调用,如list。在这个调用中(通常称作一个参数化类型a parameterized type),所有出现形式类型参数(formal type parameter,这里是e)都被替换成实体类型参数(actual type argument)(这里是integer)。

你可能想象,list代表一个e被全部替换成integer的版本:

public interface integerlist {

void add(integer x)

iterator iterator();

}

这种直觉可能有帮助,但是也可能导致误解。

它有帮助,因为list的声明确实有类似这种替换的方法。

它可能导致误解,因为泛型声明绝不会实际的被这样替换。没有代码的多个拷贝,源码中没有、二进制代码中也没有;磁盘中没有,内存中也没有。如果你是一个c++程序员,你会理解这是和c++模板的很大的区别。

一个泛型类型的声明只被编译一次,并且得到一个class文件,就像普通的class或者interface的声明一样。

类型参数就跟在方法或构造函数中普通的参数一样。就像一个方法有形式参数(formal value parameters)来描述它操作的参数的种类一样,一个泛型声明也有形式类型参数(formal type parameters)。当一个方法被调用,实参(actual arguments)替换形参,方法体被执行。当一个泛型声明被调用,实际类型参数(actual type arguments)取代形式类型参数。

一个命名的习惯:我们推荐你用简练的名字作为形式类型参数的名字(如果可能,单个字符)。最好避免小写字母,这使它和其他的普通的形式参数很容易被区分开来。许多容器类型使用e作为其中元素的类型,就像上面举的例子。在后面的例子中还会有一些其他的命名习惯。

3.泛型和子类继承

让我们测试一下我们对泛型的理解。下面的代码片断合法么?

list ls = new arraylist(); //1

list lo = ls; //2

第1行当然合法,但是这个问题的狡猾之处在于第2行。

这产生一个问题:

一个string的list是一个object的list么?大多数人的直觉是回答:“当然!”。

好,在看下面的几行:

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

string s = ls.get(0); // 4: 试图把object赋值给string

这里,我们使用lo指向ls。我们通过lo来访问ls,一个string的list。我们可以插入任意对象进去。结果是ls中保存的不再是string。当我们试图从中取出元素的时候,会得到意外的结果。

java编译器当然会阻止这种情况的发生。第2行会导致一个编译错误。

总之,如果foo是bar的一个子类型(子类或者子接口),而g是某种泛型声明,那么g是g的子类型并不成立!!

这可能是你学习泛型中最难理解的部分,因为它和你的直觉相反。

这种直觉的问题在于它假定这个集合不改变。我们的直觉认为这些东西都不可改变。

举例来说,如果一个交通部(dmv)提供一个驾驶员里表给人口普查局,这似乎很合理。我们想,一个list是一个 list

,假定driver是person的子类型。实际上,我们传递的是一个驾驶员注册的拷贝。然而,人口普查局可能往驾驶员list中加入其他人,这破坏了交通部的记录。

为了处理这种情况,考虑一些更灵活的泛型类型很有用。到现在为止我们看到的规则限制比较大。

4.通配符(wildcards)

考虑写一个例程来打印一个集合(collection)中的所有元素。下面是在老的语言中你可能写的代码:

void printcollection(collection c) {

iterator i = c.iterator();

for (int k = 0; kc) {

for (object e : c) {

system.out.println(e);

}

}

问题是新版本的用处比老版本小多了。老版本的代码可以使用任何类型的collection作为参数,而新版本则只能使用collection,我们刚才阐述了,它不是所有类型的collections的父类。

那么什么是各种collections的父类呢?它写作: collection(发音为:"collection of unknown"),就是,一个集合,它的元素类型可以匹配任何类型。显然,它被称为通配符。我们可以写:

void printcollection(collection c) {

for (object e : c) {

system.out.println(e);

}

}

现在,我们可以使用任何类型的collection来调用它。注意,我们仍然可以读取c中的元素,其类型是object。这永远是安全的,因为不管collection的真实类型是什么,它包含的都是objects。但是将任意元素加入到其中不是类型安全的:

collection c = new arraylist();

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

因为我们不知道c的元素类型,我们不能向其中添加对象。

add方法有类型参数e作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。唯一的例外是null,它是所有类型的成员。

另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个object,因此把get的返回值赋值给一个object类型的对象或者放在任何希望是object类型的地方是安全的。

4.1.有限制的通配符(bounded wildcards)

考虑一个简单的画图程序,它可以用来画各种形状,比如矩形和圆形。

为了在程序中表示这些形状,你可以定义下面的类继承结构:

public abstract class shape {

public abstract void draw(canvas c);

}

public class circle extends shape {

private intx, y, radius;

public void draw(canvas c) { // ...

}

}

public class rectangle extends shape {

private intx, y, width, height;

public void draw(canvas c) {

// ...

}

}

这些类可以在一个画布(canvas)上被画出来:

public class canvas {

public void draw(shape s) {

s.draw(this);

}

}

所有的图形通常都有很多个形状。假定它们用一个list来表示,canvas里有一个方法来画出所有的形状会比较方便:

public void drawall(list shapes) {

for (shape s : shapes) {

s.draw(this);

}

}

现在,类型规则导致drawall()只能使用shape的list来调用。它不能,比如说对list来调用。这很不幸,因为这个方法所作的只是从这个list读取shape,因此它应该也能对list调用。我们真正要的是这个方法能够接受一个任意种类的shape:

public void drawall(list shapes) { //..}

这里有一处很小但是很重要的不同:我们把类型 list 替换成了 list。现在drawall()可以接受任何shape的子类的list,所以我们可以对list进行调用。

list是有限制通配符的一个例子。这里?代表一个未知的类型,就像我们前面看到的通配符一样。但是,在这里,我们知道这个未知的类型实际上是 shape的一个子类(它可以是shape本身或者shape的子类而不必是extends自shape)。我们说shape是这个通配符的上限 (upper bound)。

像平常一样,要得到使用通配符的灵活性有些代价。这个代价是,现在像shapes中写入是非法的。比如下面的代码是不允许的:

public void addrectangle(list shapes) {

shapes.add(0, new rectangle()); // compile-time error!

}

你应该能够指出为什么上面的代码是不允许的。因为shapes.add的第二个参数类型是? extends shape ——一个shape未知的子类。因此我们不知道这个类型是什么,我们不知道它是不是rectangle的父类;它可能是也可能不是一个父类,所以这里传递一个rectangle不安全。

有限制的通配符正是我们解决dmv给人口普查局传送名单的例子所需要的。我们的例子假定数据用一个姓名(string)到people(用 person或其子类来表示,比如driver)。map是一个有两个类型参数的泛型类型的例子,表示map的键key和值 value。

再一次,注意形式类型参数的命名习惯——k代表keys,v代表vlaues。

public class census {

public static voidaddregistry(map registry) { ...}

}...

map alldrivers = ...;

census.addregistry(alldrivers);

5.泛型方法

考虑写一个方法,它用一个object的数组和一个collection作为参数,完成把数组中所有object放入collection中的功能。

下面是第一次尝试:

static void fromarraytocollection(object[] a, collection c) {

for (object o : a) {

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

}

}

现在,你应该能够学会避免初学者试图使用collection作为集合参数类型的错误了。或许你已经意识到使用 collection也不能工作。会议一下,你不能把对象放进一个未知类型的集合中去。

解决这个问题的办法是使用generic methods。就像类型声明,方法的声明也可以被泛型化——就是说,带有一个或者多个类型参数。

staticvoid fromarraytocollection(t[] a, collection c){

for (t o : a) {

c.add(o); // correct

}

}

我们可以使用任意集合来调用这个方法,只要其元素的类型是数组的元素类型的父类。

object[] oa = new object[100];

collection co = new arraylist();

fromarraytocollection(oa, co);// t 指object

string[] sa = new string[100];

collection cs = new arraylist();

fromarraytocollection(sa, cs);// t inferred to be string

fromarraytocollection(sa, co);// t inferred to be object

integer[] ia = new integer[100];

float[] fa = new float[100];

number[] na = new number[100];

collection cn = new arraylist();

fromarraytocollection(ia, cn);// t inferred to be number

fromarraytocollection(fa, cn);// t inferred to be number

fromarraytocollection(na, cn);// t inferred to be number

fromarraytocollection(na, co);// t inferred to be object

fromarraytocollection(na, cs);// compile-time error

注意,我们并没有传送真实类型参数(actual type argument)给一个泛型方法。编译器根据实参为我们推断类型?%8


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值