46 java程式設計思想——傳遞和返回物件 只讀類
46.java程式設計思想——傳遞和返回物件 只讀類
儘管在一些特定的場合,由clone()產生的本地副本能夠獲得我們希望的結果,但程式設計師(方法的作者)不得不親自禁止別名處理的副作用。假如想製作一個庫,令其具有常規用途,但卻不能擔保它肯定能在正確的類中得以克隆,這時又該怎麼辦呢?更有可能的一種情況是,假如我們想讓別名發揮積極的作用——禁止不必要的物件複製——但卻不希望看到由此造成的副作用,那麼又該如何處理呢?
一個辦法是建立“不變物件”,令其從屬於只讀類。可定義一個特殊的類,使其中沒有任何方法能造成物件內部狀態的改變。在這樣的一個類中,別名處理是沒有問題的。因為我們只能讀取內部狀態,所以當多處程式碼都讀取相同的物件時,不會出現任何副作用。
作為“不變物件”一個簡單例子,Java 的標準庫包含了“封裝器”(wrapper)類,可用於所有基本資料型別。大家可能已發現了這一點,如果想在一個象Vector(只採用Object 控制代碼)這樣的集合裡儲存一個int數值,可以將這個int 封裝到標準庫的Integer 類內部。如下所示:
import java.util.*;
public class ImmutableInteger {
public staticvoidmain(String[] args){
Vector
for (int i = 0; i < 10; i++)
v.addElement(new Integer(i));
// But how do you change the int
// inside the Integer?
}
} /// :~
Integer 類(以及基本的“封裝器”類)用簡單的形式實現了“不變性”:它們沒有提供可以修改物件的方法。
若確實需要一個容納了基本資料型別的物件,並想對基本資料型別進行修改,就必須親自建立它們。幸運的是,操作非常簡單:
import java.util.*;
class IntValue {
int n;
IntValue(int x) {
n = x;
}
public String toString() {
return Integer.toString(n);
}
}
public class MutableInteger {
public staticvoidmain(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);
}
} /// :~
輸出:
[0,1, 2, 3, 4, 5, 6, 7, 8, 9]
[1,2, 3, 4, 5, 6, 7, 8, 9, 10]
注意n 在這裡簡化了我們的編碼。
若預設的初始化為零已經足夠(便不需要構建器),而且不用考慮把它打印出來(便不需要toString ),那麼IntValue 甚至還能更加簡單。如下所示:
class IntValue { int n; }
將元素取出來,再對其進行造型,這多少顯得有些笨拙,但那是Vector 的問題,不是IntValue 的錯。
1 建立只讀類
1.1 程式碼
public class Immutable1 {
private intdata;
public Immutable1(int initVal){
data = initVal;
}
public int read() {
returndata;
}
public booleannonzero() {
return data != 0;
}
public Immutable1 quadruple() {
return new Immutable1(data * 4);
}
static voidf(Immutable1 i1){
Immutable1 quad = i1.quadruple();
System.out.println("i1 = " + i1.read());
System.out.println("quad = " + quad.read());
}
public staticvoidmain(String[] args){
Immutable1 x = new Immutable1(47);
System.out.println("x = " + x.read());
f(x);
System.out.println("x = " + x.read());
}
} /// :~
1.2 執行
x= 47
i1= 47
quad= 188
x= 47
所有資料都設為private,可以看到沒有任何public 方法對資料作出修改。事實上,確實需要修改一個物件的方法是quadruple(),但它的作用是新建一個Immutable1 物件,初始物件則是原封未動的。方法f()需要取得一個Immutable1 物件,並對其採取不同的操作,而main()的輸出顯示出沒有對x 作任何修改。因此,x 物件可別名處理許多次,不會造成任何傷害,因為根據Immutable1 類的設計,它能保證物件不被改動。
2 “一成不變”的弊端
從表面看,不變類的建立似乎是一個好方案。但是,一旦真的需要那種新型別的一個修改的物件,就必須辛苦地進行新物件的建立工作,同時還有可能涉及更頻繁的垃圾收集。對有些類來說,這個問題並不是很大。
但對其他類來說(比如String 類),這一方案的代價顯得太高了。
為解決這個問題,我們可以建立一個“同志”類,並使其能夠修改。以後只要涉及大量的修改工作,就可換為使用能修改的同志類。完事以後,再切換回不可變的類。
可改成下面這個樣子:
2.1 程式碼
class Mutable {
private intdata;
public Mutable(intinitVal){
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 intdata;
public Immutable2(int initVal){
data = initVal;
}
public intread() {
return data;
}
public booleannonzero() {
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 staticImmutable2 modify1(Immutable2 y) {
Immutable2 val = y.add(12);
val = val.multiply(3);
val = val.add(11);
val = val.multiply(2);
return val;
}
// Thisproduces the same result:
public staticImmutable2 modify2(Immutable2 y) {
Mutable m = y.makeMutable();
m.add(12).multiply(3).add(11).multiply(2);
return m.makeImmutable2();
}
public staticvoidmain(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());
}
} /// :~
2.2 執行
i2= 47
r1= 376
r2= 376
和往常一樣,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) 建立新的不變物件代價太高
2.3 程式碼2-不變字串
public class Stringer {
static String upcase(String s) {
return s.toUpperCase();
}
public staticvoidmain(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
}
} /// :~
2.4 執行
howdy
HOWDY
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 物件應用覆蓋過的運算子+和+=時。
2.5 程式碼3
public class ImmutableStrings {
public staticvoidmain(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);
}
} /// :~
2.6 執行
abcfoodef47
abcfoodef47
建立字串s 時,編譯器做的工作大致等價於後面使用sb 的程式碼——建立一個StringBuffer,並用append()將新字元直接加入StringBuffer 物件(而不是每次都產生新物件)。儘管這樣做更有效,但不值得每次都建立象"abc"和"def"這樣的引號字串,編譯器會把它們都轉換成String 物件。所以儘管StringBuffer 提供了更高的效率,但會產生比我們希望的多得多的物件。
3 S t r i n g 和S t r i n g B u f f e r 類
這裡總結一下同時適用於String 和StringBuffer 的方法,以便對它們相互間的溝通方式有一個印象。這些表格並未把每個單獨的方法都包括進去,而是包含了與本次討論有重要關係的方法。那些已被覆蓋的方法用單獨一行總結。
3.1 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的一個控制代碼。這樣做可以節省儲存空間和系統開銷。
3.2 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()方法採用類似的形式。這兩個方法都能對緩衝區進行重要的操作,不需要另建新物件。
4 字串的特殊性
String 類並非僅僅是Java 提供的另一個類。String 裡含有大量特殊的類。通過編譯器和
特殊的覆蓋或過載運算子+和+=,可將引號字串轉換成一個String。用同志StringBuffer 精心構造的“不可變”能力,以及編譯器中出現的一些有趣現象。
5 總結
由於Java 中的所有東西都是控制代碼,而且由於每個物件都是在記憶體堆中建立的——只有不再需要的時候,才會當作垃圾收集掉,所以物件的操作方式發生了變化,特別是在傳遞和返回物件的時候。舉個例子來說,在C和C++中,如果想在一個方法裡初始化一些儲存空間,可能需要請求使用者將那片儲存區域的地址傳遞進入方法。否則就必須考慮由誰負責清除那片區域。因此,這些方法的介面和對它們的理解就顯得要複雜一些。但在Java 中,根本不必關心由誰負責清除,也不必關心在需要一個物件的時候它是否仍然存在。因為系統會照料一切。我們的程式可在需要的時候建立一個物件。而且更進一步地,根本不必擔心那個物件的傳輸機制的細節:只需簡單地傳遞控制代碼即可。有些時候,這種簡化非常有價值,但另一些時候卻顯得有些多餘。
可從兩個方面認識這一機制的缺點:
(1) 肯定要為額外的記憶體管理付出效率上的損失(儘管損失不大),而且對於執行所需的時間,總是存在一絲不確定的因素(因為在記憶體不夠時,垃圾收集器可能會被強制採取行動)。對大多數應用來說,優點顯得比缺點重要,而且部分對時間要求非常苛刻的段落可以用native 方法寫成。
(2) 別名處理:有時會不慎獲得指向同一個物件的兩個控制代碼。只有在這兩個控制代碼都假定指向一個“明確”的物件時,才有可能產生問題。對這個問題,必須加以足夠的重視。而且應該儘可能地“克隆”一個物件,以防止另一個控制代碼被不希望的改動影響。除此以外,可考慮建立“不可變”物件,使它的操作能返回同種型別或不同種類型的一個新物件,從而提高程式的執行效率。但千萬不要改變原始物件,使對那個物件別名的其他任何方面都感覺不出變化。
有些人認為Java 的克隆是一個笨拙的傢伙,所以他們實現了自己的克隆方案,永遠杜絕呼叫Object.clone()方法,從而消除了實現Cloneable 和捕獲CloneNotSupportException 違例的需要。這一做法是合理的,而且由於clone()在Java 標準庫中很少得以支援,所以這顯然也是一種“安全”的方法。只要不呼叫Object.clone(),就不必實現Cloneable 或者捕獲違例,所以那看起來也是能夠接受的。Doug Lea 特別重視這個問題,他說只需為每個類都建立一個名為duplicate()的函式即可。
Java 中一個有趣的關鍵字是byvalue(按值),它屬於那些“保留但未實現”的關鍵字之一。在理解了別名和克隆問題以後,大家可以想象byvalue 最終有一天會在Java 中用於實現一種自動化的本地副本。這樣做可以解決更多複雜的克隆問題,並使這種情況下的編寫的程式碼變得更加簡單和健壯。
再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智慧的隊伍中來!https://www.cnblogs.com/captainbed