管中窥虎
在学习
java 1.5
的过程中,我使用了
sun
公布的
tutorial
,这份文档写的比较详尽易明,但是对于想快速了解
tiger
而且具有较好
java
基础的人来说,大篇幅的英文文档是比较耗时间和非必需的,所以我将会归纳这份文档的主要内容,在保证理解的底线上,尽力减少阅读者需要的时间。
在以下地址可以进入各新增语言特色介绍以及下载相关文档(若有)。
http://java.sun.com/j2se/1.5.0/docs/relnotes/features.html
这一篇是接着上文继续的,在这里补充说明,虽然我希望以双语写作,但是把英文文档翻译过来后再翻译回去,似乎是件好傻的事情。。。所以这些翻译并精简的文章算是个例外吧。
第一道虎纹:
generic
-泛型
/
类属(二)
泛型方法
假设我们想把一个数组的元素都放到一个容器类里,下面是第一次尝试:
static
void
fromArrayToCollection(Object[] a, Collection
<
?
>
c)
{
for
(Object o : a)
{
c.add(o);
//
compile time error
}
}
现在你应该学会了不去犯初学者的错误,用
Collection
<
Object
>
来作为参数,你也许也发现了用
Collection
<
?
>
也不成,不知道就是不知道,不能放东西进去。
好了,主角登场:泛型方法
static
<
T
>
void
fromArrayToCollection(T[] a, Collection
<
T
>
c)
{
for
(T o : a)
{
c.add(o);
//
correct
}
}
方法的声明加入了类型参数,在上面这个方法里,如果
c
的元素类型是
a
的元素类型的父类,就能成功执行方法。下面这些代码可以帮助你了解一下:
Object[] oa
=
new
Object[
100
];
Collection
<
Object
>
co
=
new
ArrayList
<
Object
>
();
fromArrayToCollection(oa, co);
//
T inferred to be Object
String[] sa
=
new
String[
100
];
Collection
<
String
>
cs
=
new
ArrayList
<
String
>
();
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
<
Number
>
cn
=
new
ArrayList
<
Number
>
();
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
注意到我们并没有真正的传入一个类型实参,而是由编译器以方法的实际参数对象来推断,它推断出使得这次方法调用成立的类型,并尽可能地特化这个类型。比如说如果
T
推断为
Number
依然成立的时候,就不会推断为
Object
。
现在看来,泛型方法和通配符有些共通的地方,使得类属有一定的灵活性。那么什么时候用泛型方法,什么时候用通配符?看看下面的例子:
interface Collection < E > {
public boolean containsAll(Collection < ? > c);
public boolean addAll(Collection < ? extends E > c);
}
以及:
interface Collection < E > {
public < T > boolean containsAll(Collection < T > c);
public < T extends E > boolean addAll(Collection < T > c);
// 注意类型变量也可以有上限哦~
}
这两种方式都达成了同样的目的,使得方法有了多态性。然而注意到在每个方法的声明中,
T
只出现了一次,这种情况下就应该用通配符,通配符的主要目的就是提供弹性的泛化,而多态方法则用于表达两个或多个参数间的依赖关系,你回过头去看多态方法的第一个例子,是不是这个情况?如果不存在依赖关系需要表达,就不应该用多态方法,因为从可读性上来说,通配符更清晰。
而且有趣的是,它们并非水火不容,反而可以精妙配合,如下:
class Collections {
public static < T > void copy(List < T > dest, List < ? extends T > src) { }
}
这个合作使得
dest
与
src
的依赖关系得以表达,同时让
src
的接纳范畴扩大了。假如我们只用泛型方法来实现:
class Collections {
public static < T, S extends T > void copy(List < T > dest, List < S > src) { }
}
那么
S
的存在就显得有些不必要,有些不优雅。总的来说,通配符更简洁清晰,只要情况允许就应该首选。
下面是给几何图形家族添加了一个有记忆功能的绘图方法,展示了泛型方法的使用,有兴趣就看看,略过也不影响下一步的学习。
static
List
<
List
<
?
extends
Shape
>>
history
=
new
ArrayList
<
List
<
?
extends
Shape
>>
();
public
void
drawAll(List
<
?
extends
Shape
>
shapes)
{
history.addLast(shapes);
for (Shape s: shapes) {
s.draw( this );
}
}
这里又再谈谈命名规范的事情,用
T
来表示类型(
type
)就是个很好的选择,假如已经没有更多的背景信息的话,而在泛型方法里就是这样子,我们只是想表达一个类型。那么如果有多个参数出现,那么用
T
的街坊邻里就不错,
S
啊,什么的。如果方法出现在一个泛型类里,就主要避免类的泛型变量和方法的泛型变量同名混淆,同样的,泛型类的嵌套泛型类也应该注意。
和遗老们打交道
很显然的,这个星球上存在的
java
代码里,没有引入泛型的还是多数,它们也不会一夜走进新社会,怎么把它们转换为泛型的会在晚些再谈及,现在我们谈谈和它们打交道的事情。这包括两方面:在引入泛型的代码里使用老代码,在老代码上使用引入了泛型的代码。
首先看看前者,看例子:
public
interface
Part
{ }
public
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();
//
Returns a collection of Parts
}
public
class
Blade
implements
Part
{}
public
class
Guillotine
implements
Part
{}
public
class
Main
{
public static void main(String[] args) {
Collection < Part > c = new ArrayList < Part > ();
c.add(new Guillotine()) ;
c.add(new Blade());
Inventory.addAssembly(”thingee”, c);
Collection < Part > k = Inventory.getAssembly(”thingee”).getParts();// 琢磨一下这里?
}
}
上面的代码有没有问题?如果前面的内容里你没打瞌睡,你应该发现注解处的那个语句很有问题,类型不安全问题。我们称Collection这种不带类型参数的使用叫做原始类型,编译器无法保证这样子的容器类放了什么东西,但是编译器会以不那么严格的标准去要求这个旧社会的人,否则,老代码将完全不能在1.5里使用,编译器会发出一个unchecked warning,怎么处理由你来决定。这样的设计是符合实际的,否则就是和已有代码彻底决裂。
虽然这样的调用会有错误的风险,但总比你完全不用泛型机制好,因为至少你保证了在你的这一端的类型安全,而且总有一天,英特那雄耐尔一定会实现。。。。
J
严肃地回到我们的话题,既然有风险,那么当你得到了这样的警告时,小心检查则是目前最好的对策。
但是,如果你不理会一个这样的警告,而且事实上你真的犯了一个类型安全的错误,会发生什么呢?
消除与翻译
public String loophole(Integer x) { List < String > ys = new LinkedList < String > ();
List xs = ys;
xs.add(x); // compile-time unchecked warning
return ys.iterator().next();
}
这个警告所在的地方确实是有问题的,一个
Integer
被放入了一个
List
中,这个
List
只是原始类型,所以编译器只能给个警告,但事实上它又是指向了
ys
指向的对象,一个只放
String
的
List
,在最后一句里,会发生什么事情?实际运行起来,这段代码就等同于:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return (String) ys.iterator().next(); // run time error
}
它们会得到同样的错误:一个
ClassCastException
。
为什么呢?因为泛型在
java
编译器里是以一种称为“消除”的前端转换实现的,你几乎,我说几乎,可以认为是一种代码到代码的翻译,象上面这样,把带泛型的版本翻译成不带泛型的版本,接下来,当代码的执行交到
JVM
的手里时,它可不管你是哪朝代的人,有错就是有错,类型安全的基本政策不动摇,即使你手里拽着
unchecked warnings
的证明。
基本上,消除机制就是把类型信息都扔掉了,
List<String>
变成了
List
,上限通常都变成了
Object
,而且,当转换后的代码不符合泛型里的类型限制时,就添加一个类型转换,就是上例中那个
String
的转换会出现的原因。
消除机制的细节不是这里讨论的内容,这里简单的让你了解一些需要了解的情况而已。
假如你的代码更新为:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return (String) ys.iterator().next(); // run time error
}
而调用这个代码的老客户代码是:
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();
}
也就是在老代码里调用泛型化了的代码。
如注解
1
所示,会有警告出现,原因你也已经了解了。你可以在编译时选择用
1.4
的环境,那么就不会有警告出现,但同样你也失去了
1.5
带来的各种新特色了。