隨筆 - 0, 文章 - 75, 評論 - 0, 引用 - 0

          導航

          <2025年7月>
          293012345
          6789101112
          13141516171819
          20212223242526
          272829303112
          3456789

          留言簿

          文章檔案

          搜索

          •  

          最新評論

          初始化和清除1

          “隨著計算機的進步,‘不安全’的程序設計已成為造成編程代價高昂的罪魁禍首之一。”

          “初始化”和“清除”是這些安全問題的其中兩個。許多C程序的錯誤都是由于程序員忘記初始化一個變量造成的。對于現成的庫,若用戶不知道如何初始化庫的一個組件,就往往會出現這一類的錯誤。清除是另一個特殊的問題,因為用完一個元素后,由于不再關心,所以很容易把它忘記。這樣一來,那個元素占用的資源會一直保留下去,極易產生資源(主要是內存)用盡的后果。
          C++為我們引入了“構建器”的概念。這是一種特殊的方法,在一個對象創建之后自動調用。Java也沿用了這個概念,但新增了自己的“垃圾收集器”,能在資源不再需要的時候自動釋放它們。本章將討論初始化和清除的問題,以及Java如何提供它們的支持。

          4.1
          用構建器自動初始化
          對于方法的創建,可將其想象成為自己寫的每個類都調用一次initialize()。這個名字提醒我們在使用對象之前,應首先進行這樣的調用。但不幸的是,這也意味著用戶必須記住調用方法。在Java中,由于提供了名為“構建器”的一種特殊方法,所以類的設計者可擔保每個對象都會得到正確的初始化。若某個類有一個構建器,那么在創建對象時,Java會自動調用那個構建器——甚至在用戶毫不知覺的情況下。所以說這是可以擔保的!
          接著的一個問題是如何命名這個方法。存在兩方面的問題。第一個是我們使用的任何名字都可能與打算為某個類成員使用的名字沖突。第二是由于編譯器的責任是調用構建器,所以它必須知道要調用是哪個方法。C++采取的方案看來是最簡單的,且更有邏輯性,所以也在Java里得到了應用:構建器的名字與類名相同。這樣一來,可保證象這樣的一個方法會在初始化期間自動調用。
          下面是帶有構建器的一個簡單的類(若執行這個程序有問題,請參考第3章的“賦值”小節)。


          //: SimpleConstructor.java // Demonstration of a simple constructor package c04;  
          class Rock { Rock() { // This is the constructor System.out.println("Creating Rock"); } } public class SimpleConstructor { public static void main(String[] args) { for(int i = 0; i < 10; i++) new Rock(); } } ///:~ 

          現在,一旦創建一個對象:
          new
          Rock();
          就會分配相應的存儲空間,并調用構建器。這樣可保證在我們經手之前,對象得到正確的初始化。
          請注意所有方法首字母小寫的編碼規則并不適用于構建器。這是由于構建器的名字必須與類名完全相同!
          和其他任何方法一樣,構建器也能使用自變量,以便我們指定對象的具體創建方式??煞浅7奖愕馗膭由鲜隼?,以便構建器使用自己的自變量。如下所示:


          class Rock { Rock(int i) { System.out.println( "Creating Rock number " + i); } } public class SimpleConstructor { public static void main(String[] args) { for(int i = 0; i < 10; i++) new Rock(i); } } 

          利用構建器的自變量,我們可為一個對象的初始化設定相應的參數。舉個例子來說,假設類Tree有一個構建器,它用一個整數自變量標記樹的高度,那么就可以象下面這樣創建一個Tree對象:

          tree
          t = new Tree(12); //
          12英尺高的樹

          若Tree(int)是我們唯一的構建器,那么編譯器不會允許我們以其他任何方式創建一個Tree對象。
          構建器有助于消除大量涉及類的問題,并使代碼更易閱讀。例如在前述的代碼段中,我們并未看到對initialize()方法的明確調用——那些方法在概念上獨立于定義內容。在Java中,定義和初始化屬于統一的概念——兩者缺一不可。
          構建器屬于一種較特殊的方法類型,因為它沒有返回值。這與void返回值存在著明顯的區別。對于void返回值,盡管方法本身不會自動返回什么,但仍然可以讓它返回另一些東西。構建器則不同,它不僅什么也不會自動返回,而且根本不能有任何選擇。若存在一個返回值,而且假設我們可以自行選擇返回內容,那么編譯器多少要知道如何對那個返回值作什么樣的處理。

          4.2
          方法過載
          在任何程序設計語言中,一項重要的特性就是名字的運用。我們創建一個對象時,會分配到一個保存區域的名字。方法名代表的是一種具體的行動。通過用名字描述自己的系統,可使自己的程序更易人們理解和修改。它非常象寫散文——目的是與讀者溝通。
          我們用名字引用或描述所有對象與方法。若名字選得好,可使自己及其他人更易理解自己的代碼。
          將人類語言中存在細致差別的概念“映射”到一種程序設計語言中時,會出現一些特殊的問題。在日常生活中,我們用相同的詞表達多種不同的含義——即詞的“過載”。我們說“洗襯衫”、“洗車”以及“洗狗”。但若強制象下面這樣說,就顯得很愚蠢:“襯衫洗襯衫”、“車洗車”以及“狗洗狗”。這是由于聽眾根本不需要對執行的行動作任何明確的區分。人類的大多數語言都具有很強的“冗余”性,所以即使漏掉了幾個詞,仍然可以推斷出含義。我們不需要獨一無二的標識符——可從具體的語境中推論出含義。
          大多數程序設計語言(特別是C)要求我們為每個函數都設定一個獨一無二的標識符。所以絕對不能用一個名為print()的函數來顯示整數,再用另一個print()顯示浮點數——每個函數都要求具備唯一的名字。
          在Java里,另一項因素強迫方法名出現過載情況:構建器。由于構建器的名字由類名決定,所以只能有一個構建器名稱。但假若我們想用多種方式創建一個對象呢?例如,假設我們想創建一個類,令其用標準方式進行初始化,另外從文件里讀取信息來初始化。此時,我們需要兩個構建器,一個沒有自變量(默認構建器),另一個將字串作為自變量——用于初始化對象的那個文件的名字。由于都是構建器,所以它們必須有相同的名字,亦即類名。所以為了讓相同的方法名伴隨不同的自變量類型使用,“方法過載”是非常關鍵的一項措施。同時,盡管方法過載是構建器必需的,但它亦可應用于其他任何方法,且用法非常方便。
          在下面這個例子里,我們向大家同時展示了過載構建器和過載的原始方法:


          //: Overloading.java // Demonstration of both constructor // and ordinary method overloading. import java.util.*; class Tree { int height; Tree() { prt("Planting a seedling"); height = 0; } Tree(int i) { prt("Creating new Tree that is " + i + " feet tall"); height = i; } void info() { prt("Tree is " + height + " feet tall"); } void info(String s) { prt(s + ": Tree is " + height + " feet tall"); } static void prt(String s) { System.out.println(s); } } public class Overloading { public static void main(String[] args) { for(int i = 0; i < 5; i++) { Tree t = new Tree(i); t.info(); t.info("overloaded method"); } // Overloaded constructor: new Tree(); } } ///:~ 

          Tree既可創建成一顆種子,不含任何自變量;亦可創建成生長在苗圃中的植物。為支持這種創建,共使用了兩個構建器,一個沒有自變量(我們把沒有自變量的構建器稱作“默認構建器”,注釋①),另一個采用現成的高度。

          ①:在Sun公司出版的一些Java資料中,用簡陋但很說明問題的詞語稱呼這類構建器——“無參數構建器”(no-arg
          constructors)。但“默認構建器”這個稱呼已使用了許多年,所以我選擇了它。

          我們也有可能希望通過多種途徑調用info()方法。例如,假設我們有一條額外的消息想顯示出來,就使用String自變量;而假設沒有其他話可說,就不使用。由于為顯然相同的概念賦予了兩個獨立的名字,所以看起來可能有些古怪。幸運的是,方法過載允許我們為兩者使用相同的名字。

          4.2.1
          區分過載方法
          若方法有同樣的名字,Java怎樣知道我們指的哪一個方法呢?這里有一個簡單的規則:每個過載的方法都必須采取獨一無二的自變量類型列表。
          若稍微思考幾秒鐘,就會想到這樣一個問題:除根據自變量的類型,程序員如何區分兩個同名方法的差異呢?
          即使自變量的順序也足夠我們區分兩個方法(盡管我們通常不愿意采用這種方法,因為它會產生難以維護的代碼):


          //: OverloadingOrder.java // Overloading based on the order of // the arguments. public class OverloadingOrder { static void print(String s, int i) { System.out.println( "String: " + s + ", int: " + i); } static void print(int i, String s) { System.out.println( "int: " + i + ", String: " + s); } public static void main(String[] args) { print("String first", 11); print(99, "Int first"); } } ///:~ 

          兩個print()方法有完全一致的自變量,但順序不同,可據此區分它們。

          4.2.2
          主類型的過載
          主(數據)類型能從一個“較小”的類型自動轉變成一個“較大”的類型。涉及過載問題時,這會稍微造成一些混亂。下面這個例子揭示了將主類型傳遞給過載的方法時發生的情況:


          //: PrimitiveOverloading.java // Promotion of primitives and overloading public class PrimitiveOverloading { // boolean can't be automatically converted static void prt(String s) { System.out.println(s); } void f1(char x) { prt("f1(char)"); } void f1(byte x) { prt("f1(byte)"); } void f1(short x) { prt("f1(short)"); } void f1(int x) { prt("f1(int)"); } void f1(long x) { prt("f1(long)"); } void f1(float x) { prt("f1(float)"); } void f1(double x) { prt("f1(double)"); } void f2(byte x) { prt("f2(byte)"); } void f2(short x) { prt("f2(short)"); } void f2(int x) { prt("f2(int)"); } void f2(long x) { prt("f2(long)"); } void f2(float x) { prt("f2(float)"); } void f2(double x) { prt("f2(double)"); } void f3(short x) { prt("f3(short)"); } void f3(int x) { prt("f3(int)"); } void f3(long x) { prt("f3(long)"); } void f3(float x) { prt("f3(float)"); } void f3(double x) { prt("f3(double)"); } void f4(int x) { prt("f4(int)"); } void f4(long x) { prt("f4(long)"); } void f4(float x) { prt("f4(float)"); } void f4(double x) { prt("f4(double)"); } void f5(long x) { prt("f5(long)"); } void f5(float x) { prt("f5(float)"); } void f5(double x) { prt("f5(double)"); } void f6(float x) { prt("f6(float)"); } void f6(double x) { prt("f6(double)"); } void f7(double x) { prt("f7(double)"); } void testConstVal() { prt("Testing with 5"); f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5); } void testChar() { char x = 'x'; prt("char argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testByte() { byte x = 0; prt("byte argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testShort() { short x = 0; prt("short argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testInt() { int x = 0; prt("int argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testLong() { long x = 0; prt("long argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testFloat() { float x = 0; prt("float argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testDouble() { double x = 0; prt("double argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } public static void main(String[] args) { PrimitiveOverloading p = new PrimitiveOverloading(); p.testConstVal(); p.testChar(); p.testByte(); p.testShort(); p.testInt(); p.testLong(); p.testFloat(); p.testDouble(); } } ///:~ 

          若觀察這個程序的輸出,就會發現常數值5被當作一個int值處理。所以假若可以使用一個過載的方法,就能獲取它使用的int值。在其他所有情況下,若我們的數據類型“小于”方法中使用的自變量,就會對那種數據類型進行“轉型”處理。char獲得的效果稍有些不同,這是由于假期它沒有發現一個準確的char匹配,就會轉型為int。
          若我們的自變量“大于”過載方法期望的自變量,這時又會出現什么情況呢?對前述程序的一個修改揭示出了答案:


          //: Demotion.java // Demotion of primitives and overloading public class Demotion { static void prt(String s) { System.out.println(s); } void f1(char x) { prt("f1(char)"); } void f1(byte x) { prt("f1(byte)"); } void f1(short x) { prt("f1(short)"); } void f1(int x) { prt("f1(int)"); } void f1(long x) { prt("f1(long)"); } void f1(float x) { prt("f1(float)"); } void f1(double x) { prt("f1(double)"); } void f2(char x) { prt("f2(char)"); } void f2(byte x) { prt("f2(byte)"); } void f2(short x) { prt("f2(short)"); } void f2(int x) { prt("f2(int)"); } void f2(long x) { prt("f2(long)"); } void f2(float x) { prt("f2(float)"); } void f3(char x) { prt("f3(char)"); } void f3(byte x) { prt("f3(byte)"); } void f3(short x) { prt("f3(short)"); } void f3(int x) { prt("f3(int)"); } void f3(long x) { prt("f3(long)"); } void f4(char x) { prt("f4(char)"); } void f4(byte x) { prt("f4(byte)"); } void f4(short x) { prt("f4(short)"); } void f4(int x) { prt("f4(int)"); } void f5(char x) { prt("f5(char)"); } void f5(byte x) { prt("f5(byte)"); } void f5(short x) { prt("f5(short)"); } void f6(char x) { prt("f6(char)"); } void f6(byte x) { prt("f6(byte)"); } void f7(char x) { prt("f7(char)"); } void testDouble() { double x = 0; prt("double argument:"); f1(x);f2((float)x);f3((long)x);f4((int)x); f5((short)x);f6((byte)x);f7((char)x); } public static void main(String[] args) { Demotion p = new Demotion(); p.testDouble(); } } ///:~ 

          在這里,方法采用了容量更小、范圍更窄的主類型值。若我們的自變量范圍比它寬,就必須用括號中的類型名將其轉為適當的類型。如果不這樣做,編譯器會報告出錯。
          大家可注意到這是一種“縮小轉換”。也就是說,在造型或轉型過程中可能丟失一些信息。這正是編譯器強迫我們明確定義的原因——我們需明確表達想要轉型的愿望。

          4.2.3
          返回值過載
          我們很易對下面這些問題感到迷惑:為什么只有類名和方法自變量列出?為什么不根據返回值對方法加以區分?比如對下面這兩個方法來說,雖然它們有同樣的名字和自變量,但其實是很容易區分的:
          void
          f() {}
          int f() {}
          若編譯器可根據上下文(語境)明確判斷出含義,比如在int
          x=f()中,那么這樣做完全沒有問題。然而,我們也可能調用一個方法,同時忽略返回值;我們通常把這稱為“為它的副作用去調用一個方法”,因為我們關心的不是返回值,而是方法調用的其他效果。所以假如我們象下面這樣調用方法:
          f();
          Java怎樣判斷f()的具體調用方式呢?而且別人如何識別并理解代碼呢?由于存在這一類的問題,所以不能根據返回值類型來區分過載的方法。

          4.2.4
          默認構建器
          正如早先指出的那樣,默認構建器是沒有自變量的。它們的作用是創建一個“空對象”。若創建一個沒有構建器的類,則編譯程序會幫我們自動創建一個默認構建器。例如:


          //: DefaultConstructor.java class Bird { int i; } public class DefaultConstructor { public static void main(String[] args) { Bird nc = new Bird(); // default! } } ///:~ 

          對于下面這一行:
          new
          Bird();
          它的作用是新建一個對象,并調用默認構建器——即使尚未明確定義一個象這樣的構建器。若沒有它,就沒有方法可以調用,無法構建我們的對象。然而,如果已經定義了一個構建器(無論是否有自變量),編譯程序都不會幫我們自動合成一個:

          class
          Bush {
          Bush(int i) {}
          Bush(double d) {}
          }

          現在,假若使用下述代碼:
          new
          Bush();
          編譯程序就會報告自己找不到一個相符的構建器。就好象我們沒有設置任何構建器,編譯程序會說:“你看來似乎需要一個構建器,所以讓我們給你制造一個吧。”但假如我們寫了一個構建器,編譯程序就會說:“啊,你已寫了一個構建器,所以我知道你想干什么;如果你不放置一個默認的,是由于你打算省略它。”

          4.2.5
          this關鍵字
          如果有兩個同類型的對象,分別叫作a和b,那么您也許不知道如何為這兩個對象同時調用一個f()方法:

          class Banana
          { void f(int i) { } }
          Banana a = new Banana(), b = new
          Banana();
          a.f(1);
          b.f(2);

          若只有一個名叫f()的方法,它怎樣才能知道自己是為a還是為b調用的呢?
          為了能用簡便的、面向對象的語法來書寫代碼——亦即“將消息發給對象”,編譯器為我們完成了一些幕后工作。其中的秘密就是第一個自變量傳遞給方法f(),而且那個自變量是準備操作的那個對象的句柄。所以前述的兩個方法調用就變成了下面這樣的形式:

          Banana.f(a,1);
          Banana.f(b,2);

          這是內部的表達形式,我們并不能這樣書寫表達式,并試圖讓編譯器接受它。但是,通過它可理解幕后到底發生了什么事情。
          假定我們在一個方法的內部,并希望獲得當前對象的句柄。由于那個句柄是由編譯器“秘密”傳遞的,所以沒有標識符可用。然而,針對這一目的有個專用的關鍵字:this。this關鍵字(注意只能在方法內部使用)可為已調用了其方法的那個對象生成相應的句柄。可象對待其他任何對象句柄一樣對待這個句柄。但要注意,假若準備從自己某個類的另一個方法內部調用一個類方法,就不必使用this。只需簡單地調用那個方法即可。當前的this句柄會自動應用于其他方法。所以我們能使用下面這樣的代碼:

          class
          Apricot {
          void pick() { }
          void pit() { pick();
          }
          }

          在pit()內部,我們可以說this.pick(),但事實上無此必要。編譯器能幫我們自動完成。this關鍵字只能用于那些特殊的類——需明確使用當前對象的句柄。例如,假若您希望將句柄返回給當前對象,那么它經常在return語句中使用。


          //: Leaf.java // Simple use of the "this" keyword public class Leaf { private int i = 0; Leaf increment() { i++; return this; } void print() { System.out.println("i = " + i); } public static void main(String[] args) { Leaf x = new Leaf(); x.increment().increment().increment().print(); } } ///:~ 

          由于increment()通過this關鍵字返回當前對象的句柄,所以可以方便地對同一個對象執行多項操作。

          1.
          在構建器里調用構建器
          若為一個類寫了多個構建器,那么經常都需要在一個構建器里調用另一個構建器,以避免寫重復的代碼。可用this關鍵字做到這一點。
          通常,當我們說this的時候,都是指“這個對象”或者“當前對象”。而且它本身會產生當前對象的一個句柄。在一個構建器中,若為其賦予一個自變量列表,那么this關鍵字會具有不同的含義:它會對與那個自變量列表相符的構建器進行明確的調用。這樣一來,我們就可通過一條直接的途徑來調用其他構建器。如下所示:


          //: Flower.java // Calling constructors with "this" public class Flower { private int petalCount = 0; private String s = new String("null"); Flower(int petals) { petalCount = petals; System.out.println( "Constructor w/ int arg only, petalCount= " + petalCount); } Flower(String ss) { System.out.println( "Constructor w/ String arg only, s=" + ss); s = ss; } Flower(String s, int petals) { this(petals); //! this(s); // Can't call two! this.s = s; // Another use of "this" System.out.println("String & int args"); } Flower() { this("hi", 47); System.out.println( "default constructor (no args)"); } void print() { //! this(11); // Not inside non-constructor! System.out.println( "petalCount = " + petalCount + " s = "+ s); } public static void main(String[] args) { Flower x = new Flower(); x.print(); } } ///:~ 

          其中,構建器Flower(String
          s,int
          petals)向我們揭示出這樣一個問題:盡管可用this調用一個構建器,但不可調用兩個。除此以外,構建器調用必須是我們做的第一件事情,否則會收到編譯程序的報錯信息。
          這個例子也向大家展示了this的另一項用途。由于自變量s的名字以及成員數據s的名字是相同的,所以會出現混淆。為解決這個問題,可用this.s來引用成員數據。經常都會在Java代碼里看到這種形式的應用,本書的大量地方也采用了這種做法。
          在print()中,我們發現編譯器不讓我們從除了一個構建器之外的其他任何方法內部調用一個構建器。
          2.
          static的含義
          理解了this關鍵字后,我們可更完整地理解static(靜態)方法的含義。它意味著一個特定的方法沒有this。我們不可從一個static方法內部發出對非static方法的調用(注釋②),盡管反過來說是可以的。而且在沒有任何對象的前提下,我們可針對類本身發出對一個static方法的調用。事實上,那正是static方法最基本的意義。它就好象我們創建一個全局函數的等價物(在C語言中)。除了全局函數不允許在Java中使用以外,若將一個static方法置入一個類的內部,它就可以訪問其他static方法以及static字段。

          ②:有可能發出這類調用的一種情況是我們將一個對象句柄傳到static方法內部。隨后,通過句柄(此時實際是this),我們可調用非static方法,并訪問非static字段。但一般地,如果真的想要這樣做,只要制作一個普通的、非static方法即可。

          有些人抱怨static方法并不是“面向對象”的,因為它們具有全局函數的某些特點;利用static方法,我們不必向對象發送一條消息,因為不存在this。這可能是一個清楚的自變量,若您發現自己使用了大量靜態方法,就應重新思考自己的策略。然而,static的概念是非常實用的,許多時候都需要用到它。所以至于它們是否真的“面向對象”,應該留給理論家去討論。事實上,即使Smalltalk在自己的“類方法”里也有類似于static的東西。

          posted on 2012-04-22 15:29 hantai 閱讀(121) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 鄂伦春自治旗| 任丘市| 磐安县| 区。| 北辰区| 墨玉县| 西华县| 綦江县| 绥阳县| 迁安市| 沂南县| 扶绥县| 乐亭县| 乡宁县| 大兴区| 屏东县| 德令哈市| 恭城| 铜川市| 淄博市| 东城区| 万盛区| 滨州市| 湖州市| 房产| 涟源市| 玉田县| 苍山县| 军事| 横峰县| 锡林浩特市| 农安县| 洱源县| 扬中市| 成武县| 西盟| 平定县| 保康县| 习水县| 万山特区| 乌拉特中旗|