《大話Java性能優化》面向對象及基礎類型相關部分

3.1 面向對象及基礎類型

3.1.1 採用Clone()方式創建對象

Java語言裏面的所有類都默認繼承自java.lang.Object類,在java.lang.Object類裏面有一個clone()方法,JDK API的說明文檔裏面解釋了這個方法會返回Object對象的一個拷貝。我們需要說明兩點:一是拷貝對象返回的是一個新對象,而不是一個對象的引用地址;二是拷貝對象與用new關鍵字操作符返回的新對象的區別是,這個拷貝已經包含了一些原來對象的信息,而不是對象的初始信息,即每次拷貝動作不是一個針對全新對象的創建。

當我們使用new關鍵字創建類的一個實例時,構造函數中的所有構造函數都會被自動調用。但如果一個對象實現了Cloneable接口,那麼我們可以通過調用它的clone()方法,注意,clone()方法不會調用任何構造函數。

代碼3-1所示是工廠模式的一個典型實現,工廠模式是採用工廠方法代替new操作的一種模式,所以工廠模式的作用就相當於創建實例對象的new操作符。

代碼清單3-1 創建新對象

public static CreditgetNewCredit()

{

    return new Credit();//創建一個新的Credit對象

}

如果我們採用clone()方法的方式創建對象,那麼原有的信息可以被保留,因此創建速度會加快。如清單3-2所示,改進後的代碼使用了clone()方法。

代碼清單3-2 使用了clone()方法

private static CreditBaseCredit = new Credit();

public static CreditgetNewCredit()

{

    return (Credit)BaseCredit.clone();

}

 

3.1.2 避免對boolean判斷

Java裏的boolean數據類型被定義爲存儲8位(1個字節)的數值形式,但只能是true或是false。

有些時候我們出於寫代碼的習慣,經常容易導致習慣性思維,這裏指的習慣性思維是想要對生成的數據進行判別,這樣感覺可以在該變量進入業務邏輯之前有一層檢查、判定。對於大多數的數據類型來說,這是正確的做法,但是對於boolean變量,我們應該儘量避免不必要的等於判定。如果嘗試去掉boolean與true的比較判斷代碼,大體上來說,我們會有2個好處。

n  代碼執行的更快(生成的字節碼少了5個字節);

n  代碼整體顯得更加乾淨。

例如代碼清單3-3和3-4所示,我們針對這個判定進行了代碼解釋,這兩個類只有一個差距,即是否調用了等號表達式進行了一致性判定,如代碼string.endswith ("a") == true。

代碼清單3-3 boolean示例1

boolean method (stringstring) {

    return string.endswith ("a") ==true;//判斷是否以a結尾

}

代碼清單3-4 boolean示例2

boolean method (stringstring) {

  return string.endswith ("a");

}

 

3.1.3 多用條件操作符

我們在編寫代碼的過程中很喜歡使用if-else用於判定,這種思維來源於C語言學習的經歷。大多數中國學生都是從譚老師的C語言書籍[1]瞭解計算機領域知識的,我們在高級語言程序設計過程中,如果有可能,儘量使用條件操作符"if (cond) return; else return;"這樣的順序判斷結構,主要原因還是因爲條件操作符更加簡捷,代碼看起來會少一點。其實JVM會幫助我們優化代碼,但是個人感覺能省就省吧,代碼過多讓人看着不爽。代碼清單3-5和3-6所示是示例代碼,對比了兩者的區別。

代碼清單3-5 if示例1

//採用if-else的方式

public intmethod(boolean isdone){

        if (isdone) {

            return 0;

        } else {

            return 1;

        }

}

代碼清單3-6 if示例

public intmethod(boolean isdone) {

   return (isdone ? 0 : 1);

}

上面兩個例子,我們可以看到有一定差距,代碼行數縮短了50%。其實現代JVM已經在編譯時做了類似的處理,但是從代碼整潔度考慮,作者覺得還是推薦多采用代碼清單3-6的方式實現。

3.1.4 靜態方法替代實例方法

在Java中,使用static關鍵字描述的方法是靜態方法。與靜態方法相比,實例方法的調用需要消耗更多的系統資源,這是因爲實例方法需要維護一張類似虛擬函數導向表的結構,這樣可以方便地實現對多態的支持。對於一些常用的工具類方法,我們沒有必要對其進行重載,那麼我們可以嘗試將它們聲明爲static,即靜態方法,這樣有利於加速方法的調用。

如代碼清單3-7所示,我們分別定義了兩個方法,一個是靜態方法,一個是實例方法,然後在main函數進程裏分別調用10億次兩個方法,計算兩個方法的調用總計時間。

代碼清單3-7 靜態方法示例

public static voidstaticMethod(){

}

//實例方法

public voidinstanceMethod(){

 

}

 

@Test

public static voidmain(String[] args){

  long start = System.currentTimeMillis();

   //循環10億次,創建靜態方法

for(inti=0;i<1000000000;i++){

 staticVSinstance.staticMethod();

}

System.out.println(System.currentTimeMillis()- start);

 

 start = System.currentTimeMillis();

staticVSinstance si1 =new staticVSinstance();

        //循環10億次,創建實例方法

for(intj=0;j<1000000000;j++){

si1.instanceMethod();

 }

System.out.println(System.currentTimeMillis()- start);

}

清單3-7代碼中申明瞭一個靜態方法staticMethod()和一個實例方法instanceMethod(),運行程序,統計了兩個方法調用若干次後的耗時,程序輸出如下,單位是毫秒,方法內部沒有實現任何代碼。請讀者注意,由於機器差別,所以運行的結果可能也會有所不同。

代碼清單3-8 程序運行輸出

733   764

總的來說,靜態方法和實例方法的區別主要體現在兩個方面:

n 在外部調用靜態方法時,可以使用"類名.方法名"的方式,也可以使用"對象名.方法名"的方式。而實例方法只有後面這種方式。也就是說,調用靜態方法可以無需創建對象。

n 靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變量和靜態方法),而不允許訪問實例成員變量和實例方法;實例方法則無此限制。

從上面的例子我們可以這麼總結,如果你沒有必要去訪問對象的外部,那麼就讓你的方法成爲靜態方法。靜態方法會被更快地調用,因爲它不需要一個虛擬函數導向表,該表用來告訴你如何區分方法的性質,調用這個方法不會改變對象的狀態。

3.1.5 有條件地使用final關鍵字

在Java中,final關鍵字可以被用來修飾類、方法和變量(包括成員變量和局部變量)。我們在使用匿名內部類的時候可能會經常用到final關鍵字,例如Java中的String類就是一個final類。

如代碼清單3-9所示,由於final關鍵字會告訴編譯器,這個方法不會被重載,所以我們可以讓訪問實例內變量的getter/setter方法變成“final”。

代碼清單3-9 非final類

public void setsize(int size) {

  _size = size;

}

private int _size;

代碼清單3-10 final類

//告訴編譯器該方法不會被重載

final public voidsetsize (int size) {

    _size = size;

}

private int _size;

總的來說,使用final方法的原因有兩個[2]。第一個原因是把方法鎖定,以防任何繼承類修改它的含義。第二個原因是提高效率。在早期的Java實現版本中,會將final方法轉爲內嵌調用。但是如果方法過於龐大,可能看不到內嵌調用帶來的任何性能提升。JDK6以後的Java版本已經不再需要使用final方法進行這些優化了。

3.1.6 避免不需要的instanceof操作

instanceof關鍵字是Java的一個二元操作符,和==、>、<是屬於同一類表達式。由於instanceof是由字母組成的,所以它也是Java的保留關鍵字。instanceof的作用是測試它左邊的對象是否是它右邊的類的實例,返回boolean類型的數據,即如果左邊的對象的靜態類型等於右邊的,我們使用的instanceof表達式的返回值會返回true。

代碼清單3-11 instanceof示例

void method (dog dog,faClass faclass) {

    dog d = dog;

   if (d instanceof faClass) // 這裏永遠都返回true.

      system.out.println("dog is a faClass");

    faClass faclass = faclass;

    if (faclass instanceof object) // alwaystrue.

      system.out.println("uiso is anobject");

}

上述代碼裏面對dog類型的變量都做了判定,由於已經確定類繼承自基類,所以我們可以刪除不需要的instanceof操作。當然,這樣的操作修改還是需要基於實際的業務邏輯,有些時候爲了保證數據準確性、安全性,還是需要層層檢查的。如代碼清單3-12所示,代碼可以被精簡成這樣。

代碼清單3-12 instanceof示例

void method () {

   dog d;

    system.out.println ("dog is an faclass");

    system.out.println ("uiso is an faclass");

}

另外,絕大多數情況下都不推薦使用instanceof方法,還是好好利用多態特性吧,這是面向對象的基本功能。

3.1.7 避免子類中存在父類轉換

我們知道在Java語言裏所有的類都是直接或者間接繼承自Object類。我們可以說,Object類是所有Java類的祖先,因此每個類都使用Object作爲超類,所有對象(包括數組)都實現這個類的方法。在不明確是否提供了超類的情況下,Java會自動把Object作爲被定義類的超類。

我們可以使用類型爲Object的變量指向任意類型的對象。同樣,所有的子類也都隱含的“等於”其父類。那麼,程序代碼中就沒有必要再把子類對象轉換爲它的父類了。

代碼清單3-13 避免父類轉換示例

class oriClass {

  string _id = "unc";

}

class dog extendsoriClass{

    void method () {

     dog dog = new dog();

     oriClass animal = (oriClass)dog;  //已經確定繼承自oriClass類了,因此沒有必要再轉對象類型

     object o = (object)dog;

  }

}

代碼清單3-14 避免父類轉換示例

class dog extendsoriClass {

  //去掉了轉換父類操作

  void method () {

     dog dog = new dog();

     unc animal = dog;

    object o = dog;

}

}

 

3.1.8 建議多使用局部變量

調用方法時傳遞的參數以及在調用中創建的臨時變量都被保存在棧(Stack)裏面,因此讀寫速度較快。其他變量,例如靜態變量、實例變量,它們都在堆(heap)中被創建,也被保留在那裏,所以讀寫相對於保存在棧裏面的數據來說,它的速度較慢。

Java類的成員變量有兩種,一種是被static關鍵字修飾的變量,叫類變量或者靜態變量,另一種沒有static修飾,稱爲實例變量。在語法定義上的區別,靜態變量前要加static關鍵字,而實例變量前則不加。

靜態變量(類變量)被所有對象共有,如果其中一個對象將它的值改變,那麼其他對象得到的就是改變後的結果。實例變量屬於對象私有,如果某一個對象將其值改變,也不會影響到其他對象。此外,靜態變量和實例變量都屬全局變量。

程序運行過程當中,實例變量屬於某個對象的屬性,必須創建實例對象,其中的實例變量纔會被分配空間,才能使用這個實例變量。靜態變量不屬於某個實例對象,而是屬於類,所以也稱爲類變量,只要程序加載了類的字節碼,不用創建任何實例對象,靜態變量就會被分配空間,靜態變量就可以被使用了。總之,實例變量必須創建對象後纔可以通過這個對象來使用,靜態變量則可以直接使用類名來引用。

代碼3-15演示了分別創建100萬次實例變量和靜態變量所消耗的時間,從測試結果來看,使用局部變量和靜態變量的操作時間對比較爲明顯。

代碼清單3-15 局部變量和靜態變量之間的對比測試

public classvariableCompare {

public static int b =0;

public static voidmain(String[] args){

int a = 0;

long starttime =System.currentTimeMillis();

for(inti=0;i<1000000;i++){

    a++;//在函數體內定義局部變量

}

System.out.println(System.currentTimeMillis()- starttime);

starttime =System.currentTimeMillis();

for(inti=0;i<1000000;i++){

    b++;//在函數體內定義局部變量

}

System.out.println(System.currentTimeMillis()- starttime);

}

}

以上兩段代碼的運行時間分別爲0和15,單位是毫秒。由此結果可見,局部變量的訪問速度遠遠高於類的成員變量。

3.1.9 運算效率最高的方式——位運算

在Java語言中的所有運算中,位運算是最爲高效的。位運算表達式由操作數和位運算符組成,實現對整數類型的二進制數進行位運算。位運算符可以分爲邏輯運算符(包括~、&、|和^)及移位運算符(包括>>、<<和>>>)。因此,可以嘗試使用位運算方式代替部分算術運算,來提高系統的運行速度。最典型示例的就是對於整數的乘除運算優化。

代碼清單3-16實現了位運算與算術運算的對比,兩個運算方式輸出的結果是一樣的,但是耗時差距達到了8倍。

代碼清單3-16 位運算與算術運算對比試驗

public classyunsuanClass {

public static voidmain(String args[]){

long start =System.currentTimeMillis();

long a=1000;

    //執行1000萬次算術運算

for(inti=0;i<10000000;i++){

a*=2;

a/=2;

}

System.out.println(a);

  System.out.println(System.currentTimeMillis() - start);

start =System.currentTimeMillis();

   //執行1000萬次位運算

for(inti=0;i<10000000;i++){

a<<=1;

a>>=1;

}

System.out.println(a);

System.out.println(System.currentTimeMillis()- start);

}

}

兩段代碼執行了完全相同的功能,在每次循環中,整數1000乘以2,然後除以2。第一個循環耗時546,第二個循環耗時63,單位是毫秒(ms)。

我們的程序內部進行位運算時,需要注意以下幾點。

n >>>和>>的區別是:在執行運算時,>>>運算符的操作數高位補0,而>>運算符的操作數高位移入原來高位的值。

n 右移一位相當於除以2,左移一位(在不溢出的情況下)相當於乘以2;移位運算速度高於乘除運算。

n 若進行位邏輯運算的兩個操作數的數據長度不相同,則返回值應該是數據長度較長的數據類型。

n 按位異或可以不使用臨時變量完成兩個值的交換,也可以使某個整型數的特定位的值翻轉。

n 按位與運算可以用來屏蔽特定的位,也可以用來取某個數型數中某些特定的位。

n 按位或運算可以用來對某個整型數的特定位的值置l。

個人建議,由於移位操作需要一定的底層編程技術能力,所以對於剛開始接觸程序設計的讀者來說,除非是在一個非常大的循環內,性能因素至關重要,而且你很清楚你自己在做什麼,才建議使用這種方法,否則提高性能所帶來的程序易讀性的降低就不划算了。

3.1.10 用一維數組代替二維數組

JDK很多類庫是採用數組方式實現的數據存儲,比如ArrayList、Vector等,數組的優點是隨機訪問性能非常好。

一維數組和二維數組的訪問速度不一樣,二維數組的訪問速度要優於一維數組,但是,二維數組比一維數組佔用更多的內存空間,大概是10倍左右。在性能敏感的系統中要使用二維數組,如果內存不足,儘量將二維數組轉化爲一維數組再進行處理,以節省內存空間。

如代碼清單3-17所示,我們演示了一維數組和二維數組比較的示例程序。

代碼清單3-17 一維數組和二維數組對比

public class arrayTest{

     public static void main(String[] args){

         long start =System.currentTimeMillis();

         int[] arraySingle = new int[1000000];

         int chk = 0;

        //構建1億個數組元素,並賦值

         for(int i=0;i<100;i++){

             for(intj=0;j<arraySingle.length;j++){

                 arraySingle[j] = j;

             }

         }

       //遍歷1億個數組元素,並賦值給局部變量

         for(int i=0;i<100;i++){

             for(intj=0;j<arraySingle.length;j++){

                 chk = arraySingle[j];

             }

         }

        System.out.println(System.currentTimeMillis() - start);

 

         start = System.currentTimeMillis();

         int[][] arrayDouble = newint[1000][1000];

         chk = 0;

        //構建對應於1億個一維數組的二維數組

         for(int i=0;i<100;i++){

             for(intj=0;j<arrayDouble.length;j++){

                 for(intk=0;k<arrayDouble[0].length;k++){

                     arrayDouble[i][j]=j;

                 }

             }

         }

      //遍歷這些二維數組

         for(int i=0;i<100;i++){

            for(int j=0;j<arrayDouble.length;j++){

                 for(intk=0;k<arrayDouble[0].length;k++){

                     chk = arrayDouble[i][j];

                 }

             }

         }

        System.out.println(System.currentTimeMillis() - start);

 

         start = System.currentTimeMillis();

         arraySingle = new int[1000000];

         int arraySingleSize =arraySingle.length;

         chk = 0;

      //遍歷一維數組

         for(int i=0;i<100;i++){

             for(intj=0;j<arraySingleSize;j++){

                 arraySingle[j] = j;

             }

        }

         for(int i=0;i<100;i++){

             for(intj=0;j<arraySingleSize;j++){

                 chk = arraySingle[j];

             }

         }

         System.out.println(System.currentTimeMillis()- start);

 

         start = System.currentTimeMillis();

         arrayDouble = new int[1000][1000];

         int arrayDoubleSize =arrayDouble.length;

         int firstSize = arrayDouble[0].length;

         chk = 0;

        //遍歷二維數組

         for(int i=0;i<100;i++){

             for(intj=0;j<arrayDoubleSize;j++){

                 for(intk=0;k<firstSize;k++){

                     arrayDouble[i][j]=j;

                 }

             }

         }

         for(int i=0;i<100;i++){

             for(intj=0;j<arrayDoubleSize;j++){

                 for(intk=0;k<firstSize;k++){

                     chk = arrayDouble[i][j];

                 }

             }

         }

        System.out.println(System.currentTimeMillis() - start);

     }

}

第一段代碼操作的是一維數組的賦值、取值過程,第二段代碼操作的是二維數組的賦值、取值過程,第三段代碼是一維數組遍歷、賦值過程,第四段代碼是二維數組的遍歷、賦值過程。輸出時間分別是374、312、297、266毫秒。從3-17所示代碼的運行結果來看,二維數組的速度有一定優勢,但是請注意,這是JVM犧牲了內存空間換取的性能,請讀者自己做出選擇。

3.1.11 布爾運算代替位運算

雖然位運算的速度遠遠高於算術運算,但是在條件判斷時,使用位運算替代布爾運算是非常非常錯誤的選擇。在條件判斷時,Java會對布爾運算做相當充分的優化。假設有表達式a、b、c進行布爾運算“a&&b&&c”,根據邏輯與的特點,只要在整個布爾表達式中有一項返回false,整個表達式就返回false,因此,當表達式a爲false時,該表達式將立即返回false,而不會再去計算表達式b和c。若此時,表達式a、b、c需要消耗大量的系統資源,這種處理方式可以節省這些計算資源。同理,當計算表達式“a||b||c”時,只要a、b或c,3個表達式其中任意一個計算結果爲true時,整體表達式立即返回true,而不去計算剩餘表達式。簡單地說,在布爾表達式的計算中,只要表達式的值可以確定,就會立即返回,而跳過剩餘子表達式的計算。如果使用位運算(按位與、按位或)代替邏輯與和邏輯或,雖然位運算本身沒有性能問題,但是位運算總是要將所有的子表達式全部計算完成後,再給出最終結果。因此,從這個角度看,使用位運算替代布爾運算會使系統進行很多無效計算。代碼清單3-18演示了位運算與布爾運算的對比實驗。

代碼清單3-18 位運算與布爾運算的比較

public classOperationCompare {

     public static void booleanOperate(){

         long start =System.currentTimeMillis();

         boolean a = false;

         boolean b = true;

         int c = 0;

         //下面循環開始進行位運算,表達式裏面的所有計算因子都會被用來計算

         for(int i=0;i<1000000;i++){

            if(a&b&"Test_123".contains("123")){

                 c = 1;

             }

         }

        System.out.println(System.currentTimeMillis() - start);

     }

 

     public static void bitOperate(){

        long start =System.currentTimeMillis();

         boolean a = false;

         boolean b = true;

         int c = 0;

         //下面循環開始進行布爾運算,只計算表達式a即可滿足條件

         for(int i=0;i<1000000;i++){

            if(a&&b&&"Test_123".contains("123")){

                 c = 1;

             }

         }

         System.out.println(System.currentTimeMillis()- start);

    }

 

     public static void main(String[] args){

         OperationCompare.booleanOperate();

         OperationCompare.bitOperate();

     }

}

上面的示例代碼運行結果顯示布爾計算大大優於位運算(位運算用了63毫秒,布爾運算幾乎沒有耗時),但是,這個結果不能說明位運算比邏輯運算慢,因爲在所有的邏輯與運算中,都省略了表達式“"Test_123".contains("123")”的計算,而所有的位運算都沒能省略這部分系統開銷。

3.1.12 提取表達式優化

在大部分情況下,由於計算機運算單元(CPU)的不斷髮展,我們現在用的CPU大多是多核的,有些可能還會附帶GPU,這樣我們可以把圖像計算、數據挖掘算法這類需要消耗大量計算資源的程序放到GPU上運行,這是題外話了,不多展開。這樣,我們知道在程序高速運行過程當中,少量的重複代碼並不會對性能構成太大的威脅,但是如果你希望將系統性能發揮到極致,還是有很多地方可以優化的。比如代碼清單3-19所示的代碼,我們通過採用局部變量的方式,避免了重複的計算,雖然計算量相對於CPU來說很微小,但是總是還是可以節省一點時間的。

代碼清單3-19 提取表達式實驗

public classduplicatedCode {

     public static void beforeTuning(){

         long start =System.currentTimeMillis();

         double a1 = Math.random();

         double a2 = Math.random();

         double a3 = Math.random();

         double a4 = Math.random();

         double b1,b2;

     //開始循環運算

         for(int i=0;i<10000000;i++){

             b1 = a1*a2*a4/3*4*a3*a4;

             b2 = a1*a2*a3/3*4*a3*a4;

         }

        System.out.println(System.currentTimeMillis() - start);

     }

 

     public static void afterTuning(){

         long start =System.currentTimeMillis();

         double a1 = Math.random();

         double a2 = Math.random();

         double a3 = Math.random();

         double a4 = Math.random();

         double combine,b1,b2;

     //計算公式被移到了外面

         for(int i=0;i<10000000;i++){

             combine = a1*a2/3*4*a3*a4;

             b1 = combine*a4;

             b2 = combine*a3;

         }

        System.out.println(System.currentTimeMillis() - start);

     }

 

     public static void main(String[] args){

         duplicatedCode.beforeTuning();

         duplicatedCode.afterTuning();

     }

}

兩段代碼的差別是提取了重複的攻勢,使得這個公式的每次循環計算只執行一次。分別耗時202ms和110ms,可見,提取複雜的重複操作是相當具有意義的。這個例子告訴我們,在循環體內,如果能夠提取到循環體外的計算公式,最好提取出來,儘可能讓程序少做重複的計算。

3.1.13 不要總是使用取反操作符(!)

取反操作符(!)表示異或操作,使用起來很方便,但是也要注意的是,它降低了程序的可讀性,所以建議不要經常使用。

代碼清單3-20 取反操作符示例

boolean method (booleana, boolean b) {

if (!a)

return !a;

else

return !b;

}

 

3.1.14 不要重複初始化變量

默認情況下,調用類的構造函數時,Java會把變量初始化爲一個確定的值,例如,所有的對象被設置成Null,整數變量設置成0,float和double變量設置成0.0,邏輯值設置成false。當一個類從另一個類派生時,這一點尤其應該注意,因爲用new關鍵字創建一個對象時,構造函數鏈中的所有構造函數都會被自動調用。

這裏需要注意,當我們給成員變量設置初始值,又需要調用其他方法的時候,最好放在一個方法裏面。比如initXXX()中,因爲直接調用某方法賦值可能會因爲類尚未初始化而拋空指針異常。

此外,如果不初始化變量,那麼當我們直接調用變量的時候,系統會給對象或變量隨機賦一個值,這樣容易產生不必要的錯誤。

3.1.15 變量初始化過程思考

由於智能化的GUI工具,例如Eclipse、IntelliIDEA這樣的工具存在,所以程序員很少會去主動思考Java程序對於成員變量的聲明及初始化順序,一般都是按照自己的習慣方式進行編碼,如果出錯了GUI工具也會自動提醒,點一下左側的修復按鈕就會自動修復代碼。

我們思考一個問題,爲什麼抽象類不能用final關鍵字聲明?如果讀者看過閆宏博士的《Java與模式》書,那麼可能已經知道了答案,因爲一個類一旦被修飾成了final,那麼意味着這個類是不能被繼承的,並且Java中定義抽象類不能被實例化。如果一個抽象類可以是final類型的,那麼這個類就不能被繼承,也不能被實例化,那麼它也就沒有存在的意義了。即從語言的角度來講,一個類既然是抽象類,那麼它就是爲了被其他類繼承,所以給它標識爲final是沒有意義的。

我們來看一系列的示例代碼,通過這些代碼可以幫助讀者理解成員變量的初始化過程。

如代碼清單3-21所示,我們首先定義局部變量variableA的值爲1,然後再申明變量,由於main主函數裏面會實例化類varClass,所以variableA會被賦值爲1。

代碼清單3-21 普通局部變量

public class varClass {

    {

        variableA = 1;

    }

 

    private int variableA;

 

public static voidmain(String[] args){ 

3-24Class test1 = new3-24Class (); 

System.out.println(test1.variableA); 

}

}

程序運行輸出爲1,如果我們想要打印出variableA的值,我們必須等到成員變量被申明後才能這麼做,代碼清單3-22所示的代碼,GUI工具會提醒我們,“cannot reference a fieldbefore it is defined”,也就是無法在申明之前調用它的引用地址。

代碼清單3-22 普通局部變量嘗試輸出

public class errorClass

{

variableA = 1;

System.out.println(variableA);//這一行會拋出錯誤,必須註釋後才能在GUI工具裏面運行這個類

 

private final intvariableA;

 

public static voidmain(String[] args){ 

errorClass test1 = new errorClass(); 

       System.out.println(test1.variableA); 

}

}

如果把variableA申明爲final呢?如代碼清單3-23所示。

代碼清單3-23 final局部變量

public class finalClass{

    {

        variableA = 1;

    }

 

    private final int variableA;

 

    public static void main(String[]args){ 

        finalClass test1 = newfinalClass(); 

       System.out.println(test1.variableA); 

    }

}

輸出依然是1,但是如果我們嘗試對final關鍵字申明的variableA變量賦值,那麼又會產生錯誤“The final fieldClass1.variableA cannot be assigned”,即既然是不可變的變量,又怎麼允許在申明之後的代碼塊裏面繼續變更變量值呢。

如果我們對3-22代碼稍作修改,定義privateint variable=2,程序的輸出會變成2,這是因爲代碼的分析、執行是從上至下的,實質上我們可以看出,JVM將變量variable的申明和賦值分爲了兩個步驟,即先執行申明操作,然後順序地執行第一步賦值操作,即賦值爲1,然後執行第二步賦值操作,賦值爲2,這樣最終類實例會打印出第二個值,即2。

如果我們依然相打印出1呢?我們可以通過申明變量爲靜態變量的方式來達到這樣的目的,如代碼清單3-23所示。

代碼清單3-23 static變量

public classstaticClass {

//static

    {

        variableA = 1;

    }

 

    private static int variableA = 2;

 

    public static void main(String[]args){ 

        staticClass test1 = newstaticClass(); 

        System.out.println(test1.variableA); 

    }

}

如果希望輸出爲2,那麼可以釋放3-23代碼中註釋的針對代碼塊的static關鍵字申明,這樣可以確保程序先執行靜態代碼塊,也就確保了賦值2的定義被後續執行。

這一個小章節講述的是成員變量、代碼塊的申明過程、執行順序,我們在很多場景下可能不會去思考這類問題,但是作者覺得還是有必要思考的,這樣可以幫助程序員理清思路,也許,下一個語言是由你創造的呢?

3.1.16 對象的創建、訪問過程

n 對象的創建

創建一個對象通常是需要new關鍵字,當虛擬機遇到一條new指令時,首先檢查這個指令的參數是否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果那麼執行相應的類加載過程。

類加載檢查通過後,虛擬機將爲新生對象分配內存。爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。分配的方式有兩種,一種叫指針碰撞,假設Java堆中內存是絕對規整的,用過的和空閒的內存各在一邊,中間放着一個指針作爲分界點的指示器,分配內存就是把那個指針向空閒空間的那邊挪動一段與對象大小相等的距離。另一種叫空閒列表,如果Java堆中的內存不是規整的,虛擬機就需要維護一個列表,記錄哪個內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。採用哪種分配方式是由Java堆是否規整決定的,而Java堆是否規整是由所採用的垃圾收集器是否帶有壓縮整理功能決定的。另外一個需要考慮的問題就是對象創建時的線程安全問題,有兩種解決方案:一是對分配內存空間的動作進行同步處理;另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存(TLAB),哪個線程要分配內存就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時才需要同步鎖定。

內存分配完成後,虛擬機需要將分配到的內存空間初始化爲零值。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就可以直接使用。接下來虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息等,這些信息存放在對象的對象頭中。

上面的工作都完成以後,從虛擬機的角度來看一個新的對象已經產生了。但是從Java程序的角度,還需要執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。

n 對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局可分爲三個部分,即對象頭、實例數據和對齊填充。

對象頭包括兩個部分:第一部分用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、線程所持有的鎖等。官方稱之爲“Mark Word”。第二個部分爲是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

實例數據是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內容。

對齊填充並不是必然存在的,僅僅起着佔位符的作用。Hotpot VM要求對象起始地址必須是8字節的整數倍,對象頭部分正好是8字節的倍數,所以當實例數據部分沒有對齊時,需要通過對齊填充來對齊。

n 對象的訪問定位

Java程序通過棧上的reference數據來操作堆上的具體對象。主要的訪問方式有使用句柄和直接指針兩種:

n 句柄:Java堆將會劃出一塊內存來作爲句柄池,引用中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

n 直接指針:Java堆對象的佈局要考慮如何放置訪問類型數據的相關信息,引用中存儲的就是對象地址。

兩個方式各有優點,使用句柄最大的好處是引用中存儲的是穩定的句柄地址,對象被移動時只會改變句柄中實例的地址,引用不需要修改、使用直接指針訪問的好處是速度更快,它節省了一次指針定位的時間開銷。

3.1.17 在switch語句中使用字符串

對於switch語句,開發人員並不陌生。大部分編程語言中都有類似的語法結構,用來根據某個表達式的值選擇要執行的語句塊。對於switch語句中的條件表達式類型,不同編程語言所提供的支持是不一樣的。對於Java語言來說,在Java 7之前,switch語句中的條件表達式的類型只能是與整數類型兼容的類型,包括基本類型char、byte、short和int,與這些基本類型對應的封裝類Character、Byte、Short和Integer,還有枚舉類型。這樣的限制降低了語言的靈活性,使開發人員在需要根據其他類型的表達式來進行條件選擇時,不得不增加額外的代碼來繞過這個限制。爲此,Java 7放寬了這個限制,額外增加了一種可以在switch語句中使用的表達式類型,那就是很常見的字符串,即String類型。

Java 7新特性並沒有改變switch的語法含義,只是多了一種開發人員可以選擇的條件判斷的數據類型。但是這個簡單的新特性卻帶來了重大的影響,因爲根據字符串進行條件判斷在開發中是很常見的。

考慮這樣一個應用場景,在程序中需要根據用戶的性別來生成合適的稱謂。判斷條件的類型可以是字符串,不過這在Java 7之前的switch語句中是行不通的,之前只能添加額外的代碼先將字符串轉換成整數類型。而在Java7種就可以根據字符串進行條件判斷,代碼如清單3-24所示。

代碼清單3-24 Switch新特性

public class Title {

   public String generate(String name,Stringgender){

        String title = "";

        switch(gender) {

            case "男":

                title = name + "先生";

                break;

            case "女":

                title = name + "女士";

                break;

            default:

                title = name;

        }

        return title;

    }

}

在switch語句中,表達式的值不能是null,否則會在運行時拋出NullPointException。在case子句中也不能使用Null,否則會出現編譯錯誤。

根據siwtch語句的語法要求,其case子句的值是不能重複的。這個要求對字符串類型的條件表達式同樣適用。不過對於字符串來說,這種重複值得檢查還有一個特殊之處,那就是Java代碼中的字符串可以包含Unicode轉義字符。重複值的檢查是在Java編譯器對Java源代碼進行相關的詞法轉換之後才進行的。這個詞法轉換過程中包括了對Unicode轉義字符的處理。也就是說,有些case子句的值雖然在源代碼中看起來是不同的,但是經詞法轉換後是一樣的,這就會造成編譯錯誤。如下面的代碼是無法通過編譯的。這是因爲其中的switch語句中的兩個case字句所使用的值在經過詞法轉換之後會變成一樣的。

代碼清單3-25 錯誤的switch

public class Title {

    public String generate(String name,Stringgender){

        String title = "";

        switch(gender) {

            case "男":

                title = name + "先生";

                break;

            case "\u7537":

                title = name + "女士";

                break;

            default:

                title = name;

        }

        return title;

    }

}

代碼清單3-25所示代碼,eclipse會提示錯誤:Duplicate case。

通過以上代碼的演示,大家應該清楚了新特性的作用。實際上,這個新特性是在編譯器這個層次上實現的。而在Java虛擬機和字節代碼這個層次上,還是隻支持在switch語句中使用與整數類型兼容的類型。這麼做的目的是爲了減少這個特性所影響的範圍,以降低實現的代價。在編譯器層次實現的含義是,雖然開發人員在Java源代碼的switch語句中使用了字符串類型,但是在編譯的過程中,編譯器會根據源代碼的含義來進行轉換,將字符串類型轉換成與整數類型兼容的格式。不同的Java編譯器可能採用不同的方式來完成這個轉換,並採用不同的優化策略。舉個例子,如果switch語句中只包含一個case子句,那麼可以簡單地將其轉換成一個if語句。如果switch語句中包含一個case子句和一個default子句,那麼可以將其轉換成一個if-else語句。而對於複雜的情況,即switch語句中包含多個case子句的情況,也可以轉換成Java 7之前的switch語句,只不過使用字符串的哈希值作爲switch語句的表達式的值。

爲了探究OpenJDK中的Java編譯器使用的是什麼樣的轉換方式,需要一個名爲JAD的工具。這個工具可以把Java的類文件反編譯成Java的源代碼。在對編譯生成Title類的class文件使用了JAD之後,所得到的內容如清單3-26所示。

代碼清單3-26 編譯後的類代碼

public class Title

{

public Stringgenerate(String name,String gender)

{

    String title = “”;

    String s = gender;

    byte byte0 = -1;

    switch(s.hashCode())

    {

       case 30007:

            if(s.equals(“\u7537”))

byte0 = 0;

  break;

       case 22899:

          if(s.equals(“\u5973”))

             byte0 = 0;

break;

}

switch(byte0)

 {

 case 0://’\0’

title = (new StringBuilder()).append(name).append(“\u5148\u751F”).toString();

          break;

case 1://’\001’

title = (new StringBuilder()).append(name).append(“\u5973\u58EB”).toString();

         break;

       default:

         title = name;

         break;

}

return title;

}

}

從上面的代碼可以看出,原來用在switch語句中的字符串被替換成了對應的哈希值,而case子句的值也被換成了原來字符串常量的哈希值。經過這樣的轉換,Java虛擬機所看到的仍然是與整數類型兼容的類型。在這裏值得注意的是,在case子句對應的語句塊中仍然需要使用String的equals方法來進行字符串比較。這是因爲哈希函數在映射的時候可能存在衝突,多個字符串的哈希值可能是一樣的。進行字符串比較是爲了保證轉換之後的代碼邏輯與之前完全一樣。

Java 7引入的這個新特性雖然爲開發人員提供了方便,但是比較容易被誤用,造成代碼的可維護性差的問題。提到這一點就必須要說一下Java SE 5.0中引入的枚舉類型。switch語句的一個典型的應用就是在多個枚舉值之間進行選擇。在Java SE 5.0之前,一般的做法是使用一個整數來爲這些枚舉值編號,比如0表示“男”,1表示“女”。在switch語句中使用這個整數編碼來進行判斷。這種做法的弊端有很多,比如不是類型安全的、沒有名稱空間、可維護性差和不夠直觀等。Joshua Bloch最早在他的“Effective Java”一書中提出了一種類型安全的枚舉類型的實現方式。這種方式在J2SE 5.0中被引入到標準庫,就是現在的enum關鍵字。

Java語言中的枚舉類型的最大優勢在於它是一個完整的Java類,除了定義其中包含的枚舉值之外,還可以包含任意的方法和域,以及實現任意的接口。這使得枚舉類型可以很好地與其他Java類進行交互。在涉及多個枚舉值的情況下,都應該優先使用枚舉類型。

在Java 7之前,也就是switch語句還不支持使用字符串表達式類型時,如果要枚舉的值本身都是字符串,使用枚舉類型是唯一的選擇。而在Java 7中,由於switch語句增加了對字符串條件表達式的支持,一些開發人員會選擇放棄枚舉類型而直接在case子句中用字符串常量來列出各個枚舉值。這種方式雖然簡單和直接,但是會帶來維護上的麻煩,尤其是這樣的switch語句在程序的多個地方出現的時候,在程序中多次出現字符串常量總是一個不好的現象,而使用枚舉類型就可以避免這種情況。

3.1.18 數值字面量的改進

在編程語言中,字面量(literal)指的是在源代碼中直接表示的一個固定的值。絕大部分編程語言都支持在源代碼中使用基本類型字面量,包括整數、浮點數、字符串和布爾值等。少數編程語言支持複雜類型的字面量,如數組和對象等。Java語言只支持基本類型的字面量。Java7中對數值類型字面量進行了增強,包括對整數和浮點數字面量的增強。

在Java源代碼中使用整數字面量的時候,可以指定所使用的進制。在Java 7之前,所支持的進制包括十進制、八進制和十六進制。十進制是默認使用的進制。八進制是用在整數字面量之前添加“0”來表示的,而十六進制則是用在整數字面量之前添加“0x”或“0X”來表示的。Java 7中增加了一種可以在字面量中使用的進制,即二進制。二進制整數字面量是通過在數字前面添加“0b”或“0B”來表示的。

代碼清單3-27 二進制示例1

import staticjava.lang.System.out;

public classBinaryIntegralLiteral {

    public void display(){

        out.println(0b001001);//輸出9

        out.println(0B001110);//輸出14

    }

    public static void main(String[] args){

        BinaryIntegralLiteral b = newBinaryIntegralLiteral();

        b.display();

    }

}

這種新的二進制字面量的表示方式使得在源代碼中使用二進制數據變得更加簡單,不再需要先手動將數據轉換成對應的八/十/十六進制的數值。

如果Java源代碼中有一個很長的數值字面量,開發人員在閱讀這段代碼時需要很費力地分辨數字的位數,以知道其所代表的數值大小。在現實生活中,當遇到很長的數字的時候,我們採取的是分段分隔的方式。比如數字500000,我們通常會寫成500,000,即每三位數字用逗號分隔。利用這種方式就可以很快知道數值的大小。這種做法的理念被加入到了Java 7中,不過用的不是逗號,而是下畫線“_”。

在Java 7中,數值字面量,不管是整數還是浮點數,都允許在數字之間插入任意多個下畫線。這些下畫線不會對字面量的數值產生影響,其目的主要是方便閱讀。

代碼清單3-28 二進制示例2

import staticjava.lang.System.out;

public classBinaryIntegralLiteral {

    public void display(){

        out.println(0b001001);//輸出9

        out.println(0B001110);//輸出14

        out.println(1_500_500);//輸出1500500

        out.println(5_6.3_4);//輸出56.34

        out.println(89_3___1);//輸出8931

}

 

public static voidmain(String[] args){

        BinaryIntegralLiteral b = newBinaryIntegralLiteral();

        b.display();

}

}

雖然下畫線在數值字面量中的應用非常靈活,但有些情況是不允許出現的。最基本的原則是下畫線只能出現在數字中間,也就是說前後都必須是數字。所以“_100”、“120_”、“ob_101”、“0x_da0”這樣的使用方式都是非法的,無法通過編譯。這樣限制的動機在於降低實現的複雜度。有了這個限制之後,Java編譯器只需要在掃描源代碼的時候,將所發現的數字中間的下畫線直接刪除就可以了。這樣就和沒有使用下畫線的形式是相同的。如果不添加這個限制,那麼編譯器就需要進行語法分析才能做出判斷。比如“_100”可能是一個整數字面量100,也可能是一個變量名稱。這就要求編譯器的實現做出更加複雜的改動。

3.1.19 優化變長參數的方法調用

J2SE 5.0中引入的一個新特性就是允許在方法聲明中使用可變長度的參數。一個方法的最後一個形式參數可以被指定爲代表任意多個相同類型的參數。在調用的時候,這些參數是以數組的形式來傳遞的。在方法體中也可以按照數組的方式來引用這些參數。如下代碼中可以對多個整數進行求和,可以用類似sum(1,2,3)這樣的形式來調用此方法。

代碼清單3-29 變長參數示例

public intsum(int…args){

int result = 0;

for(int value:args){

  result += value;

}

return result;

}

可變長度的參數在實際開發中可以簡化方法的調用方式。但是在Java 7之前,如果可變長度的參數與泛型一起使用後會遇到一個麻煩,就是便一起產生的警告過多,比如下面代碼。

代碼清單3-30 Java 6變長參數示例

public static<T>T useVarargs(T…args){

    return args.length > 0?args[0]:null;

}

如果參數傳遞的是不可具體化(non-reifiable)的類型,如List<String>這樣的泛型類型,會產生警告信息。每一次調用該方法,都會產生警告信息。比如在Java 7之前的編譯器上編譯代碼VarargsWarninguseVarargs(new ArrayList<String>()),編譯器會給出警告信息。如果希望禁止這個警告信息,需要使用@SuppressWarning(“unchecked”)註解來聲明。這其中的原因是可變長度的方法參數的實際值是通過數組來傳遞的,而數組中存儲的是不可具體化的泛型類對象,自身存在類型安全問題。因此編譯器會給出相應的警告信息。這樣的警告信息在使用Java標準類庫中的java.util.Arrays類的asList和java.util.Collections類的addAll方法中也會遇到。建議開發人員每次使用方法時都抑制編譯器的警告信息,這個不是一個好主意。

爲了解決這個問題,Java 7引入了一個新的註解@SafeVarargs。如果開發人員確信某個使用了可變長度參數的方法,在與泛型類一起使用時不會出現類型安全問題,就可以用這個註解進行聲明。在使用了這個註解之後,編譯器遇到類似的問題,就不會再給出相關的警告信息。

@SafeVarargs註解只能用在參數長度可變的方法或構造方法上,且方法必須聲明爲static或final,否則會出現編譯錯誤。一個方法是用@SafeVarargs註解的前提是,開發人員必須確保這個方法的實現中對泛型類型參數的處理不會引發類型安全問題。

3.1.20 針對基本數據類型的優化

Java7對基本類型的包裝類做了一些更新,以更好地滿足日常的開發需求。第一個更新是在基本類型的比較方面,Boolean、Byte、Short、Integer、Long和Character類都添加了一個比較兩個基本類型值的靜態compare方法,比如Long類的compare方法可以用來比較兩個Long類型的值。這個compare方法只能簡化進行基本類型數值比較時的代碼。在Java7之前,如果需要對兩個int數值x和y進行比較,一般的做法是使用代碼“Integer.value(x).compareTo(Integer.value(y))”,而在Java7直接使用“Integer.compare(x,y)”。

字符串內部化(string interning)技術可以提高字符串比較時的性能,是一種典型的空間換時間的做法。在Java中包含相同字符的字符串字面量引用的是相同的內部對象。String類也提供了intern方法來返回與當前字符串內容相同的但已經包含在內部緩存中的對象引用。在對被內部緩存的字符串進行比較時,可以直接使用“==”操作符,而不需要用更加耗時的equals方法。

Java7把這種內部化機制擴大到了-128到127之間的數字。根據Java語言規範,對於-128到127範圍內的short類型和int類型,以及\u0000到\u007f範圍內的char類型,它們對應的包裝類對象始終指向相同的對象,即通過“==”進行判斷時的結果爲true。爲了滿足這個要求,Byte、Short、Integer類的valueOf方法對於-128到127範圍內的值,以及Character類的valueOf方法對於0到127範圍內的值,都會返回內部緩存的對象。如果希望緩存更多的值,可以通過Java虛擬機啓動參數“java.lang.Integer.Integer-Cache.high”來進行設置。例如,使用“-Djava.lang.Integer.IntegerCache.high=256”之後,數值緩存的範圍就變成了-128到256。

3.1.21 空變量

顯式地賦空變量是否有助於程序的性能。賦空變量是指簡單地將null值顯式地賦值給這個變量,相對於讓該變量的引用失去其作用域。

代碼清單 3-31 局部作用域

public static StringscopingExample(String string) {

  StringBuffer sb = new StringBuffer();

  sb.append("hello ").append(string);

  sb.append(", nice to see you!");

  return sb.toString();

}

如清單3-31所示,當該方法執行時,運行時棧保留了一個對StringBuffer對象的引用,這個對象是在程序的第一行產生的。在這個方法的整個執行期間,棧保存的這個對象引用將會防止該對象被當作垃圾。當這個方法執行完畢,變量sb也就失去了它的作用域,相應地運行時棧就會刪除對該StringBuffer對象的引用。於是不再有對該StringBuffer對象的引用,現在它就可以被當作垃圾收集了。棧刪除引用的操作就等於在該方法結束時將null值賦給變量sb。

既然Java虛擬機可以執行等價於賦空的操作,那麼顯式地賦空變量還有什麼用呢?對於在正確的作用域中的變量來說,顯式地賦空變量的確沒用。但是讓我們來看看另外一個版本的 scopingExample方法,如代碼清單3-32所示,這一次我們將把變量sb放在一個錯誤的作用域中。

代碼清單 3-32 靜態作用域

static StringBuffer sb = new StringBuffer();

public static StringscopingExample(String string) {

  sb = new StringBuffer();

  sb.append("hello ").append(string);

  sb.append(", nice to see you!");

  return sb.toString();

}

現在sb是一個靜態變量,所以只要它所在的類還裝載在Java虛擬機中,它也將一直存在。該方法執行一次,一個新的StringBuffer將被創建並且被sb變量引用。在這種情況下,sb變量以前引用的StringBuffer對象將會死亡,成爲垃圾收集的對象。也就是說,這個死亡的StringBuffer對象被程序保留的時間比它實際需要保留的時間長得多,如果再也沒有對該scopingExample方法的調用,它將會永遠保留下去。

即使如此,顯式地賦空變量能夠提高性能嗎?我們會發現我們很難相信一個對象會或多或少對程序的性能產生很大影響,直到清單3-33所示,它包含了一個大型對象。

代碼清單 3-33 仍在靜態作用域中的對象

private static Object bigObject;

 

public static voidtest(int size) {

  long startTime = System.currentTimeMillis();

  long numObjects = 0;

  while (true) {

    //bigObject = null; //explicit nulling

    //SizableObject could simply be a largearray, e.g. byte[]

    //In the JavaGaming discussion it was aBufferedImage

    bigObject = new SizableObject(size);

    long endTime = System.currentTimeMillis();

    ++numObjects;

    // We print stats for every two seconds

    if (endTime - startTime >= 2000) {

      System.out.println("Objects createdper 2 seconds = " + numObjects);

      startTime = endTime;

      numObjects = 0;

    }

  }

}

這個例子有個簡單的循環,創建一個大型對象並且將它賦給同一個變量,每隔兩秒鐘報告一次所創建的對象個數。現在的Java虛擬機採用generational垃圾收集機制,新的對象創建之後放在一個內存空間(取名 Eden)內,然後將那些在第一次垃圾收集以後仍然保留的對象轉移到另外一個內存空間。在 Eden,即創建新對象時所在的新一代空間中,收集對象要比在“老一代”空間中快得多。但是如果 Eden 空間已經滿了,沒有空間可供分配,那麼就必須把 Eden 中的對象轉移到老一代空間中,騰出空間來給新創建的對象。如果沒有顯式地賦空變量,而且所創建的對象足夠大,那麼 Eden 就會填滿,並且垃圾收集器就不能收集當前所引用的這個大型對象。所產生的後果是,這個大型對象被轉移到“老一代空間”,並且要花更多的時間來收集它。

通過顯式地賦空變量,Eden 就能在新對象創建之前獲得自由空間,這樣垃圾收集就會更快。實際上,在顯式賦空的情況下,該循環在兩秒鐘內創建的對象個數是沒有顯式賦空時的5倍――但是僅當您選擇創建的對象要足夠大而可以填滿 Eden 時纔是如此, 在 Windows 環境、Java虛擬機 1.4 的默認配置下大概需要 500KB。那就是一行賦空操作產生的 5 倍的性能差距。但是請注意這個性能差別產生的原因是變量的作用域不正確,這正是賦空操作發揮作用的地方,並且是因爲所創建的對象非常大。



[1]    即譚浩強教授,他編著的《C程序設計》發行了1100萬冊。

[2]    這段話摘自《Java編程思想》第四版第143頁。

發佈了428 篇原創文章 · 獲贊 197 · 訪問量 82萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章