友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
富士康小说网 返回本书目录 加入书签 我的书架 我的书签 TXT全本下载 『收藏到我的浏览器』

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 一个布尔结果,指出自变量是不是一个
返回目录 上一页 下一页 回到顶部 10 9
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!