1. 程式人生 > >46 java程式設計思想——傳遞和返回物件 只讀類

46 java程式設計思想——傳遞和返回物件 只讀類

46.java程式設計思想——傳遞和返回物件 只讀類

儘管在一些特定的場合,由clone()產生的本地副本能夠獲得我們希望的結果,但程式設計師(方法的作者)不得不親自禁止別名處理的副作用。假如想製作一個庫,令其具有常規用途,但卻不能擔保它肯定能在正確的類中得以克隆,這時又該怎麼辦呢?更有可能的一種情況是,假如我們想讓別名發揮積極的作用——禁止不必要的物件複製——但卻不希望看到由此造成的副作用,那麼又該如何處理呢?

一個辦法是建立“不變物件”,令其從屬於只讀類。可定義一個特殊的類,使其中沒有任何方法能造成物件內部狀態的改變。在這樣的一個類中,別名處理是沒有問題的。因為我們只能讀取內部狀態,所以當多處程式碼都讀取相同的物件時,不會出現任何副作用。

作為“不變物件”一個簡單例子,Java 的標準庫包含了“封裝器”(wrapper)類,可用於所有基本資料型別。大家可能已發現了這一點,如果想在一個象Vector(只採用Object 控制代碼)這樣的集合裡儲存一個int數值,可以將這個int 封裝到標準庫的Integer 類內部。如下所示:

import java.util.*;

public class ImmutableInteger {

    public staticvoidmain(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 類(以及基本的“封裝器”類)用簡單的形式實現了“不變性”:它們沒有提供可以修改物件的方法。

若確實需要一個容納了基本資料型別的物件,並想對基本資料型別進行修改,就必須親自建立它們。幸運的是,操作非常簡單:

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