- 写在前面的话
- 引言
- 第 1 章 对象入门
- 第 2 章 一切都是对象
- 第 3 章 控制程序流程
- 第 4 章 初始化和清除
- 第 5 章 隐藏实施过程
- 第 6 章 类再生
- 第 7 章 多形性
- 第 8 章 对象的容纳
- 第 9 章 违例差错控制
- 第 10 章 Java IO 系统
- 第 11 章 运行期类型鉴定
- 第 12 章 传递和返回对象
- 第 十三 章 创建窗口和程序片
- 第 14 章 多线程
- 第 15 章 网络编程
- 第 16 章 设计范式
- 第 17 章 项目
- 附录 A 使用非 JAVA 代码
- 附录 B 对比 C++和 Java
- 附录 C Java 编程规则
- 附录 D 性能
- 附录 E 关于垃圾收集的一些话
- 附录 F 推荐读物
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 不允许程序员覆盖其他任何运算符——注释④)。
④: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 提供了更高的效率,但会产生比我们希望的多得多的对象。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论