友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第90部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
//: 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 一个布尔结果,指出自变量是不是一个后缀
indexOf();lastIndexOf()
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!