友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第62部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
必须对这个问题加以高度重视。第一个要注意的是基于组件的程序设计。在这种环境下,我们用“快速应用
开发”(RAD)模型来构建程序项目。RAD 一般是在应用程序构建工具中内建的。这是编制程序的一种可视途
径(在屏幕上以窗体的形式出现)。可将代表不同组件的图标拖曳到窗体中。随后,通过设定这些组件的属
性或者值,进行正确的配置。设计期间的配置要求任何组件都是可以“例示”的(即可以自由获得它们的实
例)。这些组件也要揭示出自己的一部分内容,允许程序员读取和设置各种值。此外,用于控制 GUI 事件的
343
…………………………………………………………Page 345……………………………………………………………
组件必须揭示出与相应的方法有关的信息,以便RAD 环境帮助程序员用自己的代码覆盖这些由事件驱动的方
法。“反射”提供了一种特殊的机制,可以侦测可用的方法,并产生方法名。通过 Java Beans (第13章将
详细介绍),Java 1。1 为这种基于组件的程序设计提供了一个基础结构。
在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调
用”(RMI),它允许Java 程序(版本 1。1 以上)使用由多台机器发布或分布的对象。这种对象的分布可能
是由多方面的原因引起的:可能要做一件计算密集型的工作,想对它进行分割,让处于空闲状态的其他机器
分担部分工作,从而加快处理进度。某些情况下,可能需要将用于控制特定类型任务(比如多层客户/服务
器架构中的“运作规则”)的代码放置在一台特殊的机器上,使这台机器成为对那些行动进行描述的一个通
用储藏所。而且可以方便地修改这个场所,使其对系统内的所有方面产生影响(这是一种特别有用的设计思
路,因为机器是独立存在的,所以能轻易修改软件!)。分布式计算也能更充分地发挥某些专用硬件的作
用,它们特别擅长执行一些特定的任务——例如矩阵逆转——但对常规编程来说却显得太夸张或者太昂贵
了。
在Java 1。1 中,Class 类(本章前面已有详细论述)得到了扩展,可以支持“反射”的概念。针对 Field,
Method 以及Constructor 类(每个都实现了 Memberinterface——成员接口),它们都新增了一个库:
java。lang。reflect。这些类型的对象都是 JVM 在运行期创建的,用于代表未知类里对应的成员。这样便可用
构建器创建新对象,用 get()和 set()方法读取和修改与 Field 对象关联的字段,以及用 invoke()方法调用
与Method 对象关联的方法。此外,我们可调用方法 getFields(),getMethods(),getConstructors(),分
别返回用于表示字段、方法以及构建器的对象数组(在联机文档中,还可找到与Class 类有关的更多的资
料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。
大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道
时,JVM 只是简单地检查那个对象,并调查它从属于哪个特定的类(就象以前的RTTI 那样)。但在这之后,
在我们做其他任何事情之前,Class 对象必须载入。因此,用于那种特定类型的。class文件必须能由 JVM 调
用(要么在本地机器内,要么可以通过网络取得)。所以 RTTI 和“反射”之间唯一的区别就是对 RTTI 来
说,编译器会在编译期打开和检查。class文件。换句话说,我们可以用“普通”方式调用一个对象的所有方
法;但对“反射”来说,。class 文件在编译期间是不可使用的,而是由运行期环境打开和检查。
11。3。1 一个类方法提取器
很少需要直接使用反射工具;之所以在语言中提供它们,仅仅是为了支持其他Java 特性,比如对象序列化
(第10章介绍)、Java Beans 以及RMI (本章后面介绍)。但是,我们许多时候仍然需要动态提取与一个类
有关的资料。其中特别有用的工具便是一个类方法提取器。正如前面指出的那样,若检视类定义源码或者联
机文档,只能看到在那个类定义中被定义或覆盖的方法,基础类那里还有大量资料拿不到。幸运的是,“反
射”做到了这一点,可用它写一个简单的工具,令其自动展示整个接口。下面便是具体的程序:
//: ShowMethods。java
// Using Java 1。1 reflection to show all the
// methods of a class; even if the methods are
// defined in the base class。
import java。lang。reflect。*;
public class ShowMethods {
static final String usage =
〃usage: n〃 +
〃ShowMethods qualified。class。namen〃 +
〃To show all methods in class or: n〃 +
〃ShowMethods qualified。class。name wordn〃 +
〃To search for methods involving 'word'〃;
public static void main(String'' args) {
if(args。length 《 1) {
System。out。println(usage);
System。exit(0);
}
try {
344
…………………………………………………………Page 346……………………………………………………………
Class c = Class。forName(args'0');
Method'' m = c。getMethods();
Constructor'' ctor = c。getConstructors();
if(args。length == 1) {
for (int i = 0; i 《 m。length; i++)
System。out。println(m'i'。toString());
for (int i = 0; i 《 ctor。length; i++)
System。out。println(ctor'i'。toString());
}
else {
for (int i = 0; i 《 m。length; i++)
if(m'i'。toString()
。indexOf(args'1')!= …1)
System。out。println(m'i'。toString());
for (int i = 0; i 《 ctor。length; i++)
if(ctor'i'。toString()
。indexOf(args'1')!= …1)
System。out。println(ctor'i'。toString());
}
} catch (ClassNotFoundException e) {
System。out。println(〃No such class: 〃 + e);
}
}
} ///:~
Class 方法getMethods()和 getConstructors()可以分别返回Method 和Constructor 的一个数组。每个类都
提供了进一步的方法,可解析出它们所代表的方法的名字、参数以及返回值。但也可以象这样一样只使用
toString(),生成一个含有完整方法签名的字串。代码剩余的部分只是用于提取命令行信息,判断特定的签
名是否与我们的目标字串相符(使用 indexOf()),并打印出结果。
这里便用到了“反射”技术,因为由Class。forName()产生的结果不能在编译期间获知,所以所有方法签名
信息都会在运行期间提取。若研究一下联机文档中关于“反射”(Reflection)的那部分文字,就会发现它
已提供了足够多的支持,可对一个编译期完全未知的对象进行实际的设置以及发出方法调用。同样地,这也
属于几乎完全不用我们操心的一个步骤——Java 自己会利用这种支持,所以程序设计环境能够控制Java
Beans——但它无论如何都是非常有趣的。
一个有趣的试验是运行 java ShowMehods ShowMethods。这样做可得到一个列表,其中包括一个public 默认
构建器,尽管我们在代码中看见并没有定义一个构建器。我们看到的是由编译器自动合成的那一个构建器。
如果随之将 ShowMethods 设为一个非 public 类(即换成“友好”类),合成的默认构建器便不会在输出结果
中出现。合成的默认构建器会自动获得与类一样的访问权限。
ShowMethods 的输出仍然有些“不爽”。例如,下面是通过调用 java ShowMethods java。lang。String得到
的输出结果的一部分:
public boolean
java。lang。String。startsWith(java。lang。String;int)
public boolean
java。lang。String。startsWith(java。lang。String)
public boolean
java。lang。String。endsWith(java。lang。String)
若能去掉象 java。lang 这样的限定词,结果显然会更令人满意。有鉴于此,可引入上一章介绍的
StreamTokenizer 类,解决这个问题:
//: ShowMethodsClean。java
345
…………………………………………………………Page 347……………………………………………………………
// ShowMethods with the qualifiers stripped
// to make the results easier to read
import java。lang。reflect。*;
import java。io。*;
public class ShowMethodsClean {
static final String usage =
〃usage: n〃 +
〃ShowMethodsClean qualified。class。namen〃 +
〃To show all methods in class or: n〃 +
〃ShowMethodsClean qualif。class。name wordn〃 +
〃To search for methods involving 'word'〃;
public static void main(String'' args) {
if(args。length 《 1) {
System。out。println(usage);
System。exit(0);
}
try {
Class c = Class。forName(args'0');
Method'' m = c。getMethods();
Constructor'' ctor = c。getConstructors();
// Convert to an array of cleaned Strings:
String'' n =
new String'm。length + ctor。length';
for(int i = 0; i 《 m。length; i++) {
String s = m'i'。toString();
n'i' = StripQualifiers。strip(s);
}
for(int i = 0; i 《 ctor。length; i++) {
String s = ctor'i'。toString();
n'i + m。length' =
StripQualifiers。strip(s);
}
if(args。length == 1)
for (int i = 0; i 《 n。length; i++)
System。out。println(n'i');
else
for (int i = 0; i 《 n。length; i++)
if(n'i'。indexOf(args'1')!= …1)
System。out。println(n'i');
} catch (ClassNotFoundException e) {
System。out。println(〃No such class: 〃 + e);
}
}
}
class StripQualifiers {
private StreamTokenizer st;
public StripQualifiers(String qualified) {
st = new StreamTokenizer(
new StringReader(qualified));
st。ordinaryChar(' '); // Keep the spaces
346
…………………………………………………………Page 348……………………………………………………………
}
public String getNext() {
String s = null;
try {
if(st。nextToken() !=
StreamTokenizer。TT_EOF) {
switch(st。ttype) {
case StreamTokenizer。TT_EOL:
s = null;
break;
case StreamTokenizer。TT_NUMBER:
s = Double。toString(st。nval);
break;
case StreamTokenizer。TT_WORD:
s = new String(st。sval);
break;
default: // single character in ttype
s = String。valueOf( (char)st。ttype);
}
}
} catch(IOException e) {
System。out。println(e);
}
return s;
}
public static String strip(String qualified) {
StripQualifiers sq =
new StripQualifiers(qualified);
String s = 〃〃; si;
while((si = sq。getNext()) != null) {
int lastDot = si。lastIndexOf('。');
if(lastDot != …1)
si = si。substring(lastDot + 1);
s += si;
}
return s;
}
} ///:~
ShowMethodsClean 方法非常接近前一个ShowMethods,只是它取得了Method 和Constructor 数组,并将它们
转换成单个 String 数组。随后,每个这样的 String 对象都在 StripQualifiers。Strip()里“过”一遍,删
除所有方法限定词。正如大家看到的那样,此时用到了StreamTokenizer 和String 来完成这个工作。
假如记不得一个类是否有一个特定的方法,而且不想在联机文档里逐步检查类结构,或者不知道那个类是否
能对某个对象(如Color 对象)做某件事情,该工具便可节省大量编程时间。
第 17 章提供了这个程序的一个GUI 版本,可在自己写代码的时候运行它,以便快速查找需要的东西。
11。4 总结
利用RTTI 可根据一个匿名的基础类句柄调查出类型信息。但正是由于这个原因,新手们极易误用它,因为有
些时候多形性方法便足够了。对那些以前习惯程序化编程的人来说,极易将他们的程序组织成一系列switch
语句。他们可能用 RTTI 做到这一点,从而在代码开发和维护中损失多形性技术的重要价值。Java 的要求是
让我们尽可能地采用多形性,只有在极特别的情况下才使用RTTI 。
但为了利用多形性,要求我们拥有对基础类定义的控制权,因为有些时候在程序范围之内,可能发现基础类
347
…………………………………………………………Page 349……………………………………………………………
并未包括我们想要的方法。若基础类来自一个库,或者由别的什么东西控制着,RTTI 便是一种很好的解决方
案:可继承一个新类型,然后添加自己的额外方法。在代码的其他地方,可以侦测自己的特定类型,并调用
那个特殊的方法。这样做不会破坏多形性以及程序的扩展能力,因为新类型的添加不要求查找程序中的
switch语句。但在需要新特性的主体中添加新代码时,就必须用 RTTI 侦测自己特定的类型。
从某个特定类的利益的角度出发,在基础类里加入一个特性后,可能意味着从那个基础类衍生的其他所有类
都必须获得一些无意义的“鸡肋”。这使得接口变得含义模糊。若有人从那个基础类继承,且必须覆盖抽象
方法,这一现象便会使他们陷入困扰。比如现在用一个类结构来表示乐器(Instrument)。假定我们想清洁
管弦乐队中所有适当乐器的通气音栓(Spit Valve),此时的一个办法是在基础类Instrument 中置入一个
ClearSpitValve()方法。但这样做会造成一个误区,因为它暗示着打击乐器和电子乐器中也有音栓。针对这
种情况,RTTI 提供了一个更合理的解决方案,可将方法置入特定的类中(此时是Wind ,即“通气口”)——
这样做是可行的。但事实上一种更合理的方案是将 prepareInstrument()置入基础类中。初学者刚开始时往
往看不到这一点,一般会认定自己必须使用RTTI 。
最后,RTTI 有时能解决效率问题。若代码大量运用了多形性,但其中的一个对象在执行效率上很有问题,便
可用RTTI 找出那个类型,然后写一段适当的代码,改进其效率。
11。5 练习
(1) 写一个方法,向它传递一个对象,循环打印出对象层次结构中的所有类。
(2) 在ToyTest。java 中,将Toy 的默认构建器标记成注释信息,解释随之发生的事情。
(3) 新建一种类型的集合,令其使用一个Vector。捕获置入其中的第一个对象的类型,然后从那时起只允许
用户插入那种类型的对象。
(4) 写一个程序,判断一个 Char 数组属于基本数据类型,还是一个真正的对象。
(5) 根据本章的说明,实现 clearSpitValve()。
(6) 实现本章介绍的rotate(Shape)方法,令其检查是否已经旋转了一个圆(若已旋转,就不再执行旋转操
作)。
348
…………………………………………………………Page 350……………………………………………………………
第 12 章 传递和返回对象
到目前为止,读者应对对象的“传递”有了一个较为深刻的认识,记住实际传递的只是一个句柄。
在许多程序设计语言中,我们可用语言的“普通”方式到处传递对象,而且大多数时候都不会遇到问题。但
有些时候却不得不采取一些非常做法,使得情况突然变得稍微复杂起来(在C++中则是变得非常复杂)。
Java 亦不例外,我们十分有必要准确认识在对象传递和赋值时所发生的一切。这正是本章的宗旨。
若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java 有指针吗?”有些人认为指
针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于Java 有如此好的口碑,所以应
该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java 是
有指针的!事实上,Java 中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受
到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说,
Java 有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“句柄”,但你可以把它想像成“安全指
针”。和预备学校为学生提供的安全剪刀类似——除非特别有意,否则不会伤着自己,只不过有时要慢慢
来,要习惯一些沉闷的工作。
12。1 传递句柄
将句柄传递进入一个方法时,指向的仍然是相同的对象。一个简单的实验可以证明这一点(若执行这个程序
时有麻烦,请参考第3 章3。1。2 小节“赋值”):
//: PassHandles。java
// Passing handles around
package c12;
public class PassHandles {
static void f(PassHandles h) {
System。out。println(〃h inside f(): 〃 + h);
}
public static void main(String'' args) {
PassHandles p = new PassHandles();
System。out。println(〃p inside main(): 〃 + p);
f(p);
}
} ///:~
toString 方法会在打印语句里自动调用,而 PassHandles 直接从 Object 继承,没有 toString 的重新定义。
因此,这里会采用toString 的Object 版本,打印出对象的类,接着是那个对象所在的位置(不是句柄,而
是对象的实际存储位置�
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!