# 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 推薦讀物