友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第32部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
多只需要导入一个包(这对于继承和合并都是成立的)。
大家要记住这样一个重点:程序开发是一个不断递增或者累积的过程,就象人们学习知识一样。当然可根据
要求进行尽可能多的分析,但在一个项目的设计之初,谁都不可能提前获知所有的答案。如果能将自己的项
目看作一个有机的、能不断进步的生物,从而不断地发展和改进它,就有望获得更大的成功以及更直接的反
馈。
尽管继承是一种非常有用的技术,但在某些情况下,特别是在项目稳定下来以后,仍然需要从新的角度考察
自己的类结构,将其收缩成一个更灵活的结构。请记住,继承是对一种特殊关系的表达,意味着“这个新类
属于那个旧类的一种类型”。我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对
象,用它们表达出来自“问题空间”的一个模型。
6。7 上溯造型
继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基础类之间的关系的一种表达。可这样
总结该关系:“新类属于现有类的一种类型”。
这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。作为一个例子,大家可考虑
一个名为 Instrument 的基础类,它用于表示乐器;另一个衍生类叫作Wind 。由于继承意味着基础类的所有
方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。若 Instrument 类有一个
play()方法,则 Wind 设备也会有这个方法。这意味着我们能肯定地认为一个Wind 对象也是 Instrument 的一
种类型。下面这个例子揭示出编译器如何提供对这一概念的支持:
//: Wind。java
// Inheritance & upcasting
import java。util。*;
class Instrument {
public void play() {}
static void tune(Instrument i) {
// 。。。
i。play();
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
public static void main(String'' args) {
Wind flute = new Wind();
Instrument。tune(flute); // Upcasting
151
…………………………………………………………Page 153……………………………………………………………
}
} ///:~
这个例子中最有趣的无疑是tune()方法,它能接受一个 Instrument句柄。但在 Wind。main()中,tune()方法
是通过为其赋予一个Wind 句柄来调用的。由于Java 对类型检查特别严格,所以大家可能会感到很奇怪,为
什么接收一种类型的方法也能接收另一种类型呢?但是,我们一定要认识到一个 Wind 对象也是一个
Instrument对象。而且对于不在 Wind 中的一个Instrument (乐器),没有方法可以由tune()调用。在
tune()中,代码适用于 Instrument 以及从 Instrument 衍生出来的任何东西。在这里,我们将从一个Wind 句
柄转换成一个 Instrument 句柄的行为叫作“上溯造型”。
6。7。1 何谓“上溯造型”?
之所以叫作这个名字,除了有一定的历史原因外,也是由于在传统意义上,类继承图的画法是根位于最顶
部,再逐渐向下扩展(当然,可根据自己的习惯用任何方法描绘这种图)。因素,Wind。java 的继承图就象
下面这个样子:
由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型”,即Upcasting。上溯造
型肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。换言之,衍生类是基础类的一个超
集。它可以包含比基础类更多的方法,但它至少包含了基础类的方法。进行上溯造型的时候,类接口可能出
现的唯一一个问题是它可能丢失方法,而不是赢得这些方法。这便是在没有任何明确的造型或者其他特殊标
注的情况下,编译器为什么允许上溯造型的原因所在。
也可以执行下溯造型,但这时会面临第 11章要详细讲述的一种困境。
1。 再论合成与继承
在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:将数据和方法统一封装到一个类里,
并且使用那个类的对象。有些时候,需通过“合成”技术用现成的类来构造新类。而继承是最少见的一种做
法。因此,尽管继承在学习OOP 的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相
反,使用它时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。为判断自己到
底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。若必须上溯,就
需要继承。但如果不需要上溯造型,就应提醒自己防止继承的滥用。在下一章里(多形性),会向大家介绍
必须进行上溯造型的一种场合。但只要记住经常问自己“我真的需要上溯造型吗”,对于合成还是继承的选
择就不应该是个太大的问题。
6。8 final 关键字
由于语境(应用环境)不同,final 关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明
“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇
有些区别,所以也许会造成final 关键字的误用。
在接下去的小节里,我们将讨论final 关键字的三种应用场合:数据、方法以及类。
6。8。1 final 数据
许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:
(1) 编译期常数,它永远不会改变
(2) 在运行期初始化的一个值,我们不希望它发生变化
对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期
间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型
(Primitives),而且要用final 关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个
152
…………………………………………………………Page 154……………………………………………………………
值。
无论 static还是 final字段,都只能存储一个数据,而且不得改变。
若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类
型,final 会将值变成一个常数;但对于对象句柄,final 会将句柄变成一个常数。进行声明时,必须将句柄
初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java
对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有
“常数”效果)。这一限制也适用于数组,它也属于对象。
下面是演示 final字段用法的一个例子:
//: FinalData。java
// The effect of final on fields
class Value {
int i = 1;
}
public class FinalData {
// Can be pile…time constants
final int i1 = 9;
static final int I2 = 99;
// Typical public constant:
public static final int I3 = 39;
// Cannot be pile…time constants:
final int i4 = (int)(Math。random()*20);
static final int i5 = (int)(Math。random()*20);
Value v1 = new Value();
final Value v2 = new Value();
static final Value v3 = new Value();
//! final Value v4; // Pre…Java 1。1 Error:
// no initializer
// Arrays:
final int'' a = { 1; 2; 3; 4; 5; 6 };
public void print(String id) {
System。out。println(
id + 〃: 〃 + 〃i4 = 〃 + i4 +
〃; i5 = 〃 + i5);
}
public static void main(String'' args) {
FinalData fd1 = new FinalData();
//! fd1。i1++; // Error: can't change value
fd1。v2。i++; // Object isn't constant!
fd1。v1 = new Value(); // OK …not final
for(int i = 0; i 《 fd1。a。length; i++)
fd1。a'i'++; // Object isn't constant!
//! fd1。v2 = new Value(); // Error: Can't
//! fd1。v3 = new Value(); // change handle
//! fd1。a = new int'3';
fd1。print(〃fd1〃);
System。out。println(〃Creating new FinalData〃);
153
…………………………………………………………Page 155……………………………………………………………
FinalData fd2 = new FinalData();
fd1。print(〃fd1〃);
fd2。print(〃fd2〃);
}
} ///:~
由于i1和 I2都是具有 final 属性的基本数据类型,并含有编译期的值,所以它们除了能作为编译期的常数
使用外,在任何导入方式中也不会出现任何不同。I3是我们体验此类常数定义时更典型的一种方式:public
表示它们可在包外使用;Static 强调它们只有一个;而 final 表明它是一个常数。注意对于含有固定初始化
值(即编译期常数)的 fianl static基本数据类型,它们的名字根据规则要全部采用大写。也要注意 i5 在
编译期间是未知的,所以它没有大写。
不能由于某样东西的属性是final,就认定它的值能在编译时期知道。i4 和 i5 向大家证明了这一点。它们在
运行期间使用随机生成的数字。例子的这一部分也向大家揭示出将final 值设为 static 和非 static 之间的
差异。只有当值在运行期间初始化的前提下,这种差异才会揭示出来。因为编译期间的值被编译器认为是相
同的。这种差异可从输出结果中看出:
fd1: i4 = 15; i5 = 9
Creating new FinalData
fd1: i4 = 15; i5 = 9
fd2: i4 = 10; i5 = 9
注意对于fd1 和 fd2 来说,i4 的值是唯一的,但 i5 的值不会由于创建了另一个FinalData 对象而发生改
变。那是因为它的属性是 static,而且在载入时初始化,而非每创建一个对象时初始化。
从v1 到v4 的变量向我们揭示出final 句柄的含义。正如大家在main()中看到的那样,并不能认为由于v2
属于final,所以就不能再改变它的值。然而,我们确实不能再将v2 绑定到一个新对象,因为它的属性是
final。这便是final 对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种
类型的句柄而已。将句柄变成 final 看起来似乎不如将基本数据类型变成 final那么有用。
2。 空白final
Java 1。1 允许我们创建“空白final”,它们属于一些特殊的字段。尽管被声明成 final,但却未得到一个
初始值。无论在哪种情况下,空白 final 都必须在实际使用前得到正确的初始化。而且编译器会主动保证这
一规定得以贯彻。然而,对于 final 关键字的各种应用,空白 final 具有最大的灵活性。举个例子来说,位
于类内部的一个final 字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。下面列出一
个例子:
//: BlankFinal。java
// 〃Blank〃 final data members
class Poppet { }
class BlankFinal {
final int i = 0; // Initialized final
final int j; // Blank final
final Poppet p; // Blank final handle
// Blank finals MUST be initialized
// in the constructor:
BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet();
}
BlankFinal(int x) {
j = x; // Initialize blank final
154
…………………………………………………………Page 156……………………………………………………………
p = new Poppet();
}
public static void main(String'' args) {
BlankFinal bf = new BlankFinal();
}
} ///:~
现在强行要求我们对final 进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构建器中。这
样就可以确保final 字段在使用前获得正确的初始化。
3。 final 自变量
Java 1。1 允许我们将自变量设成 final 属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一
个方法的内部,我们不能改变自变量句柄指向的东西。如下所示:
//: FinalArguments。java
// Using 〃final〃 with method arguments
class Gizmo {
public void spin() {}
}
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal …g is final
g。spin();
}
void without(Gizmo g) {
g = new Gizmo(); // OK …g not final
g。spin();
}
// void f(final int i) { i++; } // Can't change
// You can only read from a final primitive:
int g(final int i) { return i + 1; }
public static void main(String'' args) {
FinalArguments bf = new FinalArguments();
bf。without(null);
bf。with(null);
}
} ///:~
注意此时仍然能为 final 自变量分配一个null (空)句柄,同时编译器不会捕获它。这与我们对非final 自
变量采取的操作是一样的。
方法f()和 g()向我们展示出基本类型的自变量为 final 时会发生什么情况:我们只能读取自变量,不可改变
它。
6。8。2 final 方法
之所以要使用final 方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改
变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可
以采取这种做法。
采用 final 方法的第二个理由是程序执行的效率。将一个方法设成 final 后,编译器就可以把对那个方法的
所有调用都置入“嵌入”调用里。只要编译器发现一个final 方法调用,就会(根据它自己的判断)忽略为
执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除
155
…………………………………………………………Page 157……………………………………………………………
堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这
样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入
代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java 编译器能自动侦测这些情
况,并颇为“明智”地决定是否嵌入一个 final 方法。然而,最好还是不要完全相信编译器能正确地作出所
有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为
final。
类内所有private 方法都自动成为final。由于我们不能访问一个private 方法,所以它绝对不会被其他方
法覆盖(若强行这样做,编译器会给出错误提示)。可为一个 private 方法添加final 指示符,但却不能为
那个方法提供任何额外的含义。
6。8。3 final 类
如果说整个类都是 final (在它的定义前冠以final 关键字),就表明自己不希望从这个类继承,或者不允
许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出
于安全方面的理由,我们不希望进行子类化(子类处理)。
除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。
如下所示:
//: Jurassic。java
// Making an entire class final
class SmallBrain {}
final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}
//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
public static void main(String'' args) {
Dinosaur n = new Dinosaur();
n。f();
n。i = 40;
n。j++;
}
} ///:~
注意数据成员既可以是 final,也可以不是,取决于我们具体选择。应用于final 的规则同样适用于数据成
员,无论类是否被定义成final。将类定义成final 后,结果只是禁止进行继承——没有更多的限制。然
而,由于它禁止了继承,所以一个 final类中的所有方法都默认为 final。因为此时再也无法覆盖它们。所
以与我们将一个方法明确声明为final 一样,编译器此时有相同的效率选择。
可为final 类内的一个方法添加final 指示符,但这样做没有任何意义。
6。8。4 final 的注意事项
设计一个类时,往往需要考虑是否将一个方法设为 final。可能会觉得使用自己的类时执行效率非常重要,
没有人想覆盖自己的方法。这种想法在某些时候是正确的。
但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的
156
…………………………………………………………Page 158……………………………………………………………
类尤其如此。若将一个方法定义成 final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途
径,因为我们根本没有想到它会象那样使用。
标准Java 库是阐述这一观点的最好例子。其中特别常用的一个类是 Vector。如果我们考虑代码的执行效
率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和
覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首
先,Stack (堆栈)是从Vector 继承来的,亦即Stack “是”一个Vector,这种说法是不确切的。其次,对
于Vector 许多重要的方法,如addElement()以及elementAt()等,它们都变成了 synchronized (同步
的)。正如在第14章要讲到的那样,这会造成显著的性能开销,可能会把final 提供的性能改善抵销得一干
二净。因此,程序员�
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!