JSE - 代号为 Tiger - 计划在 年年底发布我一直都热衷于尽可能多地收集有关即将推出的新技术的预告信息因此我将撰写一系列的文章讨论可从 V 中获得的新的和经过重组的特性本文是第一篇我特别想谈谈泛型类型并重点讲述在 Tiger 中为了支持它们而进行的更改和调整
在许多方面Tiger 肯定是迄今为止在 Java 编程方面(包括对源语言语法的重大扩展)所取得的最大进步Tiger 中计划进行的最显着的变化是添加泛型类型正如在 JSR 原型编译器中所预先展示的那样(您可以立即免费下载该编译器请参阅参考资料)
让我们从介绍泛型类型是什么以及添加了什么特性来支持它们开始吧
数据类型转换和错误
为理解泛型类型为何如此有用我们要将注意力转向 Java 语言中最容易引发错误的因素之一 - 需要不断地将表达式向下类型转换(downcast)为比其静态类型更为具体的数据类型(请参阅参考资料中的The Double Descent bug pattern以了解进行数据类型转换时可能会碰到的麻烦的某些方面)
程序中的每个向下类型转换对于 ClassCastException 而言都是潜在的危险应当尽量避免它们但是在 Java 语言中它们通常是无法避免的即便在设计优良的程序中也是如此
在 Java 语言中进行向下类型转换最常见的原因在于经常以专用的方式来使用类这限制了方法调用所返回的参数可能的运行时类型例如假定往 Hashtable 中添加元素并从中检索元素那么在给定的程序中被用作键的元素类型和存储在散列表中的值类型将不能是任意对象通常所有的键都是某一特定类型的实例同样地存储的值将共同具有比 Object 更具体的公共类型
但是在目前现有的 Java 语言版本中不可能将散列表的特定键和元素声明为比 Object 更具体的类型在散列表上执行插入和检索操作的类型特征符告诉我们只能插入和删除任意对象例如put 和 get 操作的说明如下所示
清单 插入/检索类型说明表明只能是任意对象
class Hashtable {
Object put(Object key Object value) {}
Object get(Object key) {}
}
因此当我们从类 Hashtable 的实例检索元素时比如即使我们知道在 Hashtable 中只放了 String而类型系统也只知道所检索的值是 Object 类型在对检索到的值进行任何特定于 String 的操作之前必须将它强制转换为 String即使是将检索到的元素添加到同一代码块中也是如此!
清单 将检索到的值强制转换成 String
import javautilHashtable;
class Test {
public static void main(String[] args) {
Hashtable h = new Hashtable();
hput(new Integer() value);
String s = (String)hget(new Integer());
Systemoutprintln(s);
}
}
请注意 main 方法主体部分的第三行中需要进行的数据类型转换因为 Java 类型系统相当薄弱因此代码会因象上面那样的数据类型转换而漏洞百出这些数据类型转换不仅使 Java 代码变得更加拖沓冗长而且它们还降低了静态类型检查的价值(因为每个数据类型转换都是一个选择忽略静态类型检查的伪指令)我们该如何扩展该类型系统从而不必回避它呢?
用泛型类型来解决问题!
要消除如上所述的数据类型转换有一种普遍的方法就是用泛型类型来增大 Java 类型系统可以将泛型类型看作是类型函数它们通过类型变量进行参数化这些类型变量可以根据上下文用各种类型参数进行实例化
例如与简单地定义类 Hashtable 不同我们可以定义泛型类 Hashtable< Key, Value>其中 Key 和 Value 是类型参数除了类名后跟着尖括号括起来的一系列类型参数声明之外在 Tiger 中定义这样的泛型类的语法和用于定义普通类的语法很相似例如可以按照如下所示的那样定义自己的泛型 Hashtable 类
清单 定义泛型 Hashtable 类
class Hashtable< Key, Value> { }
然后可以引用这些类型参数就像我们在类定义主体内引用普通类型那样如下所示
清单 像引用普通类型那样引用类型参数 class Hashtable< Key, Value> {
Value put(Key k Value v) {}
Value get(Key k) {}
}
类型参数的作用域就是相应类定义的主体部分(除了静态成员之外)(在下一篇文章中我们将讨论为何 Tiger 实现中有这样的怪习即必须对静态成员进行此项限制请留意!)
创建一个新的 Hashtable 实例时必须传递类型参数以指定 Key 和 Value 的类型传递类型参数的方式取决于我们打算如何使用 Hashtable在上面的示例中我们真正想要做的是创建 Hashtable 实例它只将 Integer 映射为 String可以用新的 Hashtable 类来完成这件事
清单 创建将 Integer 映射为 String 的实例
import javautilHashtable;
class Test {
public static void main(String[] args) {
Hashtable< Integer, String> h = new Hashtable< Integer, String>();
hput(new Integer() value);
}
}
现在不再需要数据类型转换了请注意用来实例化泛型类 Hashtable 的语法就像泛型类的类型参数用尖括号括起来那样泛型类型应用程序的参数也是用尖括号括起来的
清单 除去不必要的数据类型转换
String s = hget(key);
Systemoutprintln(s);
当然程序员若只是为了能使用泛型类型而必须重新定义所有的标准实用程序类(比如 Hashtable 和 List)的话则可能会是一项浩大的工程幸好Tiger 为用户提供了所有 Java 集合类的泛型版本因此我们不必自己动手来重新定义它们了此外这些类能与旧代码和新的泛型代码一起无缝工作(下个月我们会说明如何做到这一点)
Tiger 的基本类型限制
Tiger 中类型变量的限制之一就是它们必须用引用类型进行实例化 - 基本类型不起作用因此在上面这个示例中无法完成创建从 int 映射到 String 的 Hashtable
这很遗憾因为这意味着只要您想把基本类型用作泛型类型的参数您就必须把它们组装为对象另一方面当前的这种情况是最糟的您不能将 int 作为键传递给 Hashtable因为所有的键都必须是 Object 类型
我们真正想看到的是基本类型可以自动进行包装(boxing)和解包装(unboxing)类似于用 C# 所进行的操作(或者比后者更好)遗憾的是Tiger 不打算包括基本类型的自动包装(但是人们可以一直期待 Java 中出现该功能!)
受限泛型
有时我们想限制可能出现的泛型类的类型实例化在上面这个示例中类 Hashtable 的类型参数可以用我们想用的任何类型参数进行实例化但是对于其它某些类我们或许想将可能的类型参数集限定为给定类型范围内的子类型
例如我们可能想定义泛型 ScrollPane 类它引用普通的带有滚动条功能的 Pane被包含的 Pane 的运行时类型通常会是类 Pane 的子类型但是静态类型就只是 Pane
有时我们想用 getter 检索被包含的 Pane但是希望 getter 的返回类型尽可能具体些我们可能想将类型参数 MyPane 添加到 ScrollPane 中该类型参数可以用 Pane 的任何子类进行实例化然后可以用这种形式的子句extends Bound 来说明 MyPane 的声明从而来设定 MyPane 的范围
清单 用 extends 子句来说明 MyPane 声明
class ScrollPane< MyPane extends Pane> { }
当然我们可以完全不使用显式的范围只要能确保没有用不适当的类型来实例化类型参数
为什么要自找麻烦在类型参数上设定范围呢?这里有两个原因首先范围使我们增加了静态类型检查功能有了静态类型检查就能保证泛型类型的每次实例化都符合所设定的范围
其次因为我们知道类型参数的每次实例化都是这个范围之内的子类所以可以放心地调用类型参数实例出现在这个范围之内的任何方法如果没有对参数设定显式的范围那么缺省情况下范围是 Object这意味着我们不能调用范围实例在 Object 中未曾出现的任何方法
多态方法
除了用类型参数对类进行参数化之外用类型参数对方法进行参数化往往也同样很有用泛型 Java 编程用语中用类型进行参数化的方法被称为多态方法(Polymorphic method)
多态方法之所以有用是因为有时候在一些我们想执行的操作中参数与返回值之间的类型相关性原本就是泛型的但是这个泛型性质不依赖于任何类级的类型信息而且对于各个方法调用都不相同
例如假定想将 factory 方法添加到 List 类中这个静态方法只带一个参数也将是 List 唯一的元素(直到添加了其它元素)因为我们希望 List 成为其所包含的元素类型的泛型所以希望静态 factory 方法带有类型变量 T 这一参数并返回 List< T> 的实例
但是我们确实希望该类型变量 T 能在方法级别上进行声明因为它会随每次单独的方法调用而发生改变(而且正如我在下一篇文章中将讨论的那样Tiger 设计的怪习规定静态成员不在类级类型参数的范畴之内)