友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第66部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
最后,注意每个类都有一个副本构建器,而且每个副本构建器都必须关心为基础类和成员对象调用副本构建
器的问题,从而获得“深层复制”的效果。对副本构建器的测试是在 CopyConstructor 类内进行的。方法
ripen()需要获取一个Tomato 参数,并对其执行副本构建工作,以便复制对象:
t = new Tomato(t);
而 slice()需要获取一个更常规的 Fruit 对象,而且对它进行复制:
f = new Fruit(f);
它们都在main()中伴随不同种类的Fruit 进行测试。下面是输出结果:
In ripen; t is a Tomato
In slice; f is a Fruit
In ripen; t is a Tomato
In slice; f is a Fruit
从中可以看出一个问题。在slice()内部对Tomato 进行了副本构建工作以后,结果便不再是一个 Tomato 对
象,而只是一个Fruit。它已丢失了作为一个Tomato (西红柿)的所有特征。此外,如果采用一个
GreenZebra,ripen()和 slice()会把它分别转换成一个 Tomato 和一个 Fruit。所以非常不幸,假如想制作对
象的一个本地副本,Java 中的副本构建器便不是特别适合我们。
1。 为什么在C++的作用比在 Java 中大?
副本构建器是C++的一个基本构成部分,因为它能自动产生对象的一个本地副本。但前面的例子确实证明了
它不适合在 Java 中使用,为什么呢?在 Java 中,我们操控的一切东西都是句柄,而在C++中,却可以使用
类似于句柄的东西,也能直接传递对象。这时便要用到C++的副本构建器:只要想获得一个对象,并按值传
递它,就可以复制对象。所以它在 C++里能很好地工作,但应注意这套机制在Java 里是很不通的,所以不要
用它。
12。4 只读类
尽管在一些特定的场合,由clone()产生的本地副本能够获得我们希望的结果,但程序员(方法的作者)不
得不亲自禁止别名处理的副作用。假如想制作一个库,令其具有常规用途,但却不能担保它肯定能在正确的
类中得以克隆,这时又该怎么办呢?更有可能的一种情况是,假如我们想让别名发挥积极的作用——禁止不
必要的对象复制——但却不希望看到由此造成的副作用,那么又该如何处理呢?
一个办法是创建“不变对象”,令其从属于只读类。可定义一个特殊的类,使其中没有任何方法能造成对象
内部状态的改变。在这样的一个类中,别名处理是没有问题的。因为我们只能读取内部状态,所以当多处代
码都读取相同的对象时,不会出现任何副作用。
作为“不变对象”一个简单例子,Java 的标准库包含了“封装器”(wrapper )类,可用于所有基本数据类
型。大家可能已发现了这一点,如果想在一个象Vector (只采用Object 句柄)这样的集合里保存一个 int
数值,可以将这个 int 封装到标准库的 Integer类内部。如下所示:
//: ImmutableInteger。java
// The Integer class cannot be changed
import java。util。*;
public class ImmutableInteger {
public static void main(String'' args) {
Vector v = new Vector();
for(int i = 0; i 《 10; i++)
v。addElement(new Integer(i));
// But how do you change the int
369
…………………………………………………………Page 371……………………………………………………………
// inside the Integer?
}
} ///:~
Integer类(以及基本的“封装器”类)用简单的形式实现了“不变性”:它们没有提供可以修改对象的方
法。
若确实需要一个容纳了基本数据类型的对象,并想对基本数据类型进行修改,就必须亲自创建它们。幸运的
是,操作非常简单:
//: MutableInteger。java
// A changeable wrapper class
import java。util。*;
class IntValue {
int n;
IntValue(int x) { n = x; }
public String toString() {
return Integer。toString(n);
}
}
public class MutableInteger {
public static void main(String'' args) {
Vector v = new Vector();
for(int i = 0; i 《 10; i++)
v。addElement(new IntValue(i));
System。out。println(v);
for(int i = 0; i 《 v。size(); i++)
((IntValue)v。elementAt(i))。n++;
System。out。println(v);
}
} ///:~
注意n 在这里简化了我们的编码。
若默认的初始化为零已经足够(便不需要构建器),而且不用考虑把它打印出来(便不需要 toString ),那
么 IntValue 甚至还能更加简单。如下所示:
class IntValue { int n; }
将元素取出来,再对其进行造型,这多少显得有些笨拙,但那是Vector 的问题,不是IntValue 的错。
12。4。1 创建只读类
完全可以创建自己的只读类,下面是个简单的例子:
//: Immutable1。java
// Objects that cannot be modified
// are immune to aliasing。
public class Immutable1 {
private int data;
public Immutable1(int initVal) {
data = initVal;
}
public int read() { return data; }
370
…………………………………………………………Page 372……………………………………………………………
public boolean nonzero() { return data != 0; }
public Immutable1 quadruple() {
return new Immutable1(data * 4);
}
static void f(Immutable1 i1) {
Immutable1 quad = i1。quadruple();
System。out。println(〃i1 = 〃 + i1。read());
System。out。println(〃quad = 〃 + quad。read());
}
public static void main(String'' args) {
Immutable1 x = new Immutable1(47);
System。out。println(〃x = 〃 + x。read());
f(x);
System。out。println(〃x = 〃 + x。read());
}
} ///:~
所有数据都设为private,可以看到没有任何public 方法对数据作出修改。事实上,确实需要修改一个对象
的方法是quadruple(),但它的作用是新建一个Immutable1 对象,初始对象则是原封未动的。
方法 f()需要取得一个 Immutable1对象,并对其采取不同的操作,而 main()的输出显示出没有对x 作任何修
改。因此,x 对象可别名处理许多次,不会造成任何伤害,因为根据 Immutable1类的设计,它能保证对象不
被改动。
12。4。2 “一成不变”的弊端
从表面看,不变类的建立似乎是一个好方案。但是,一旦真的需要那种新类型的一个修改的对象,就必须辛
苦地进行新对象的创建工作,同时还有可能涉及更频繁的垃圾收集。对有些类来说,这个问题并不是很大。
但对其他类来说(比如 String 类),这一方案的代价显得太高了。
为解决这个问题,我们可以创建一个“同志”类,并使其能够修改。以后只要涉及大量的修改工作,就可换
为使用能修改的同志类。完事以后,再切换回不可变的类。
因此,上例可改成下面这个样子:
//: Immutable2。java
// A panion class for making changes
// to immutable objects。
class Mutable {
private int data;
public Mutable(int initVal) {
data = initVal;
}
public Mutable add(int x) {
data += x;
return this;
}
public Mutable multiply(int x) {
data *= x;
return this;
}
public Immutable2 makeImmutable2() {
return new Immutable2(data);
}
}
371
…………………………………………………………Page 373……………………………………………………………
public class Immutable2 {
private int data;
public Immutable2(int initVal) {
data = initVal;
}
public int read() { return data; }
public boolean nonzero() { return data != 0; }
public Immutable2 add(int x) {
return new Immutable2(data + x);
}
public Immutable2 multiply(int x) {
return new Immutable2(data * x);
}
public Mutable makeMutable() {
return new Mutable(data);
}
public static Immutable2 modify1(Immutable2 y){
Immutable2 val = y。add(12);
val = val。multiply(3);
val = val。add(11);
val = val。multiply(2);
return val;
}
// This produces the same result:
public static Immutable2 modify2(Immutable2 y){
Mutable m = y。makeMutable();
m。add(12)。multiply(3)。add(11)。multiply(2);
return m。makeImmutable2();
}
public static void main(String'' args) {
Immutable2 i2 = new Immutable2(47);
Immutable2 r1 = modify1(i2);
Immutable2 r2 = modify2(i2);
System。out。println(〃i2 = 〃 + i2。read());
System。out。println(〃r1 = 〃 + r1。read());
System。out。println(〃r2 = 〃 + r2。read());
}
} ///:~
和往常一样,Immutable2 包含的方法保留了对象不可变的特征,只要涉及修改,就创建新的对象。完成这些
操作的是add()和multiply()方法。同志类叫作 Mutable,它也含有 add()和 multiply()方法。但这些方法
能够修改Mutable 对象,而不是新建一个。除此以外,Mutable 的一个方法可用它的数据产生一个
Immutable2对象,反之亦然。
两个静态方法modify1()和 modify2()揭示出获得同样结果的两种不同方法。在 modify1()中,所有工作都是
在 Immutable2 类中完成的,我们可看到在进程中创建了四个新的 Immutable2 对象(而且每次重新分配了
val,前一个对象就成为垃圾)。
在方法modify2()中,可看到它的第一个行动是获取 Immutable2 y,然后从中生成一个Mutable (类似于前
面对 clone()的调用,但这一次创建了一个不同类型的对象)。随后,用Mutable 对象进行大量修改操作,
同时用不着新建许多对象。最后,它切换回Immutable2。在这里,我们只创建了两个新对象(Mutable 和
Immutable2 的结果),而不是四个。
这一方法特别适合在下述场合应用:
372
…………………………………………………………Page 374……………………………………………………………
(1) 需要不可变的对象,而且
(2) 经常需要进行大量修改,或者
(3) 创建新的不变对象代价太高
12。4。3 不变字串
请观察下述代码:
//: Stringer。java
public class Stringer {
static String upcase(String s) {
return s。toUpperCase();
}
public static void main(String'' args) {
String q = new String(〃howdy〃);
System。out。println(q); // howdy
String qq = upcase(q);
System。out。println(qq); // HOWDY
System。out。println(q); // howdy
}
} ///:~
q 传递进入 upcase()时,它实际是q 的句柄的一个副本。该句柄连接的对象实际只在一个统一的物理位置
处。句柄四处传递的时候,它的句柄会得到复制。
若观察对upcase() 的定义,会发现传递进入的句柄有一个名字 s,而且该名字只有在upcase()执行期间才会
存在。upcase()完成后,本地句柄 s 便会消失,而 upcase()返回结果——还是原来那个字串,只是所有字符
都变成了大写。当然,它返回的实际是结果的一个句柄。但它返回的句柄最终是为一个新对象的,同时原来
的q 并未发生变化。所有这些是如何发生的呢?
1。 隐式常数
若使用下述语句:
String s = 〃asdf〃;
String x = Stringer。upcase(s);
那么真的希望upcase()方法改变自变量或者参数吗?我们通常是不愿意的,因为作为提供给方法的一种信
息,自变量一般是拿给代码的读者看的,而不是让他们修改。这是一个相当重要的保证,因为它使代码更易
编写和理解。
为了在C++中实现这一保证,需要一个特殊关键字的帮助:const。利用这个关键字,程序员可以保证一个句
柄(C++叫“指针”或者“引用”)不会被用来修改原始的对象。但这样一来,C++程序员需要用心记住在所
有地方都使用const。这显然易使人混淆,也不容易记住。
2。 覆盖〃+〃和StringBuffer
利用前面提到的技术,String 类的对象被设计成 “不可变”。若查阅联机文档中关于String 类的内容(本
章稍后还要总结它),就会发现类中能够修改 String 的每个方法实际都创建和返回了一个崭新的String 对
象,新对象里包含了修改过的信息——原来的 String 是原封未动的。因此,Java 里没有与C++的const 对应
的特性可用来让编译器支持对象的不可变能力。若想获得这一能力,可以自行设置,就象String 那样。
由于String 对象是不可变的,所以能够根据情况对一个特定的 String 进行多次别名处理。因为它是只读
的,所以一个句柄不可能会改变一些会影响其他句柄的东西。因此,只读对象可以很好地解决别名问题。
通过修改产生对象的一个崭新版本,似乎可以解决修改对象时的所有问题,就象 String 那样。但对某些操作
来讲,这种方法的效率并不高。一个典型的例子便是为String 对象覆盖的运算符“+”。“覆盖”意味着在
与一个特定的类使用时,它的含义已发生了变化(用于String 的“+”和“+=”是Java 中能被覆盖的唯一运
算符,Java 不允许程序员覆盖其他任何运算符——注释④)。
373
…………………………………………………………Page 375……………………………………………………………
④:C++允许程序员随意覆盖运算符。由于这通常是一个复杂的过程(参见《Thinking in C++》,Prentice
Hall 于 1995 年出版),所以Java 的设计者认定它是一种“糟糕”的特性,决定不在 Java 中采用。但具有
讽剌意味的是,运算符的覆盖在Java 中要比在C++中容易得多。
针对String 对象使用时,“+”允许我们将不同的字串连接起来:
String s = 〃abc〃 + foo + 〃def〃 + Integer。toString(47);
可以想象出它“可能”是如何工作的:字串〃abc〃可以有一个方法append(),它新建了一个字串,其中包含
〃abc〃以及foo 的内容;这个新字串然后再创建另一个新字串,在其中添加〃def〃;以此类推。
这一设想是行得通的,但它要求创建大量字串对象。尽管最终的目的只是获得包含了所有内容的一个新字
串,但中间却要用到大量字串对象,而且要不断地进行垃圾收集。我怀疑 Java 的设计者是否先试过种方法
(这是软件开发的一个教训——除非自己试试代码,并让某些东西运行起来,否则不可能真正了解系统)。
我还怀疑他们是否早就发现这样做获得的性能是不能接受的。
解决的方法是象前面介绍的那样制作一个可变的同志类。对字串来说,这个同志类叫作StringBuffer,编译
器可以自动创建一个StringBuffer,以便计算特定的表达式,特别是面向String 对象应用覆盖过的运算符+
和+=时。下面这个例子可以解决这个问题:
//: ImmutableStrings。java
// Demonstrating StringBuffer
public class ImmutableStrings {
public static void main(String'' args) {
String foo = 〃foo〃;
String s = 〃abc〃 + foo +
〃def〃 + Integer。toString(47);
System。out。println(s);
// The 〃equivalent〃 using StringBuffer:
StringBuffer sb =
new StringBuffer(〃abc〃); // Creates String!
sb。append(foo);
sb。append(〃def〃); // Creates String!
sb。append(Integer。toString(47));
System。out。println(sb);
}
} ///:~
创建字串 s 时,编译器做的工作大致等价于后面使用 sb 的代码——创建一个StringBuffer,并用 append()
将新字符直接加入 StringBuffer 对象(而不是每次都产生新对象)。尽管这样做更有效,但不值得每次都创
建象〃abc〃和〃def〃这样的引号字串,编译器会把它们都转换成 String 对象。所以尽管StringBuffer 提供了
更高的效率,但会产生比我们希望的多得多的对象。
12。4。4 String 和 StringBuffer 类
这里总结一下同时适用于String 和StringBuffer 的方法,以便对它们相互间的沟通方式有一个印象。这些
表格并未把每个单独的方法都包括进去,而是包含了与本次讨论有重要关系的方法。那些已被覆盖的方法用
单独一行总结。
首先总结String 类的各种方法:
方法 自变量,覆盖 用途
构建器 已被覆盖:默认,String,StringBuffer,char 数组,byte 数组 创建String 对象
length() 无 String 中的字符数量
374
…………………………………………………………Page 376……………………………………………………………
charAt() int Index 位于String 内某个位置的char
getChars(),getBytes 开始复制的起点和终点,要向其中复制内容的数组,对目标数组的一个索引 将 char
或byte 复制到外部数组内部
toCharArray() 无 产生一个char'',其中包含了String 内部的字符
equals(),equalsIgnoreCase() 用于对比的一个 String 对两个字串的内容进行等价性检查
pareTo() 用于对比的一个String 结果为负、零或正,具体取决于String 和自变量的字典顺序。注意大
写和小写不是相等的!
regionMatches() 这个String 以及其他String 的位置偏移,以及要比较的区域长度。覆盖加入了“忽略大
小写”的特性 一个布尔结果,指出要对比的区域是否相同
startsWith() 可能以它开头的String。覆盖在自变量里加入了偏移 一个布尔结果,指出String 是否以那
个自变量开头
endsWith() 可能是这个 String 后缀的一个 String 一个布尔结果,指出自变量是不是一个
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!