# 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
// 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; }
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 companion 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);
}
}
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`的结果),而不是四个。
这一方法特别适合在下述场合应用:
(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不允许程序员重载其他任何运算符——注释④)。
④: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`中的字符数量 |
| `charAt()` | `int Index` | 位于`String`内某个位置的`char` |
| `getChars()`,`getBytes` | 开始复制的起点和终点,要向其中复制内容的数组,对目标数组的一个索引 | 将`char`或`byte`复制到外部数组内部 |
| `toCharArray()` | 无 | 产生一个`char[]`,其中包含了`String`内部的字符 |
| `equals()`,`equalsIgnoreCase()` | 用于对比的一个String | 对两个字符串的内容进行等价性检查 |
| `compareTo()` | 用于对比的一个`String` | 结果为负、零或正,具体取决于`String`和参数的字典顺序。注意大写和小写不是相等的! |
| `regionMatches()` | 这个`String`以及其他`String`的位置偏移,以及要比较的区域长度。重载加入了“忽略大小写”的特性 | 一个布尔结果,指出要对比的区域是否相同 |
| `startsWith()` | 可能以它开头的`String`。重载在参数里加入了偏移 | 一个布尔结果,指出`String`是否以那个参数开头 |
| `endsWith()` | 可能是这个`String`后缀的一个`String` | 一个布尔结果,指出参数是不是一个后缀 |
| `indexOf()`,`lastIndexOf()` | 已重载:`char`,`char`和起始索引,`String`,`String`和起始索引 | | 若参数未在这个`String`里找到,则返回-1;否则返回参数开始处的位置索引。`lastIndexOf()`可从终点开始回溯搜索 |
| `substring()` | 已重载:起始索引,起始索引和结束索引 | 返回一个新的`String`对象,其中包含了指定的字符子集 |
| `concat()` | 想连结的`String` | 返回一个新`String`对象,其中包含了原始`String`的字符,并在后面加上由参数提供的字符 |
| `relpace()` | 要查找的老字符,要用它替换的新字符 | 返回一个新`String`对象,其中已完成了替换工作。若没有找到相符的搜索项,就沿用老字符串 |
| `toLowerCase()`,`toUpperCase()` | 无 | 返回一个新`String`对象,其中所有字符的大小写形式都进行了统一。若不必修改,则沿用老字符串 |
| `trim()` | 无 | 返回一个新的`String`对象,头尾空白均已删除。若毋需改动,则沿用老字符串 |
| `valueOf()` | 已重载:`object`,`char[]`,`char[]`和偏移以及计数,`boolean`,`char`,`int`,`long`,`float`,`double ` |返回一个`String`,其中包含参数的一个字符表现形式 |
| `Intern()` | 无 | 为每个独一无二的字符顺序都产生一个(而且只有一个)`String`引用 |
可以看到,一旦有必要改变原来的内容,每个`String`方法都小心地返回了一个新的`String`对象。另外要注意的一个问题是,若内容不需要改变,则方法只返回指向原来那个`String`的一个引用。这样做可以节省存储空间和系统开销。
下面列出有关`StringBuffer`(字符串缓冲)类的方法:
| 方法 | 参数,重载 | 用途 |
| --- | --- | --- |
| 构造器 | 已重载:默认,要创建的缓冲区长度,要根据它创建的`String` | 新建一个`StringBuffer`对象 |
| `toString()` | 无 | 根据这个`StringBuffer`创建一个`String` |
| `length()` | 无 | `StringBuffer`中的字符数量 |
| `capacity()` | 无 | 返回目前分配的空间大小 |
| `ensureCapacity()` | 用于表示希望容量的一个整数 | 使`StringBuffer`容纳至少希望的空间大小 |
| `setLength()` | 用于指示缓冲区内字符串新长度的一个整数 | 缩短或扩充前一个字符串。如果是扩充,则用`null`值填充空隙 |
| `charAt()` | 表示目标元素所在位置的一个整数 | 返回位于缓冲区指定位置处的`char` |
| `setCharAt()` | 代表目标元素位置的一个整数以及元素的一个新`char`值 | 修改指定位置处的值 |
| `getChars()` | 复制的起点和终点,要在其中复制的数组以及目标数组的一个索引 | 将`char`复制到一个外部数组。和`String`不同,这里没有`getBytes()`可供使用 |
| `append()` | 已重载:`Object`,`String`,`char[]`,特定偏移和长度的`char[]`,`boolean`,`char`,`int`,`long`,`float`,`double` | 将参数转换成一个字符串,并将其追加到当前缓冲区的末尾。若有必要,同时增大缓冲区的长度 |
| `insert()` | 已重载,第一个参数代表开始插入的位置:`Object`,`String`,`char[]`,`boolean`,`char`,`int`,`long`,`float`,`double` | 第二个参数转换成一个字符串,并插入当前缓冲区。插入位置在偏移区域的起点处。若有必要,同时会增大缓冲区的长度 |
| `reverse()` | 无 | 反转缓冲内的字符顺序 |
最常用的一个方法是`append()`。在计算包含了`+`和`+=`运算符的`String`表达式时,编译器便会用到这个方法。`insert()`方法采用类似的形式。这两个方法都能对缓冲区进行重要的操作,不需要另建新对象。
## 12.4.5 字符串的特殊性
现在,大家已知道`String`类并非仅仅是Java提供的另一个类。`String`里含有大量特殊的类。通过编译器和特殊的重载或重载运算符`+`和`+=`,可将引号字符串转换成一个`String`。在本章中,大家已见识了剩下的一种特殊情况:用同志`StringBuffer`精心构造的“不可变”能力,以及编译器中出现的一些有趣现象。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物