建議26:提防包裝型別的null值
我們知道Java引入包裝型別(Wrapper Types)是為了解決基本型別的例項化問題,以便讓一個基本型別也能參與到物件導向的程式設計世界中。而在Java5中泛型更是對基本型別說了"不",如果把一個整型放入List中,就必須使用Integer包裝型別。我們看一段程式碼:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Client26 { 5 6 public static int testMethod(List<Integer> list) { 7 int count = 0; 8 for (int i : list) { 9 count += i; 10 } 11 return count; 12 } 13 14 public static void main(String[] args) { 15 List<Integer> list = new ArrayList<Integer>(); 16 list.add(1); 17 list.add(2); 18 list.add(null); 19 System.out.println(testMethod(list)); 20 } 21 }
testMethod接收一個元素是整型的List引數,計算所有元素之和,這在統計和專案中很常見,然後編寫一個測試testMethod,在main方法中把1、2和空值都放到List中,然後呼叫方法計算,現在思考一下會不會報錯。應該不會吧,基本型別和包裝型別都是可以通過自動裝箱(Autoboxing)和自動拆箱(AutoUnboxing)自由轉換的,null應該可以轉換為0吧,真的是這樣嗎?執行之後的結果是: Exception in thread "main" java.lang.NullPointerException 執行失敗,報空指標異常,我們稍稍思考一下很快就知道原因了:在程式for迴圈中,隱含了一個拆箱過程,在此過程中包裝型別轉換為了基本型別。我們知道拆箱過程是通過呼叫包裝物件的intValue方法來實現的,由於包裝型別為null,訪問其intValue方法報空指標異常就在所難免了。問題清楚了,修改也很簡單,加入null值檢查即可,程式碼如下:
public static int testMethod(List<Integer> list) { int count = 0; for (Integer i : list) { count += (i != null) ? i : 0; } return count; }
上面以Integer和int為例說明了拆箱問題,其它7個包裝物件的拆箱過程也存在著同樣的問題。包裝物件和拆箱物件可以自由轉換,這不假,但是要剔除null值,null值並不能轉換為基本型別。對於此問題,我們謹記一點:包裝型別參與運算時,要做null值校驗。
建議27:謹慎包裝型別的大小比較
基本型別是可以比較大小的,其所對應的包裝型別都實現了Comparable介面,也說明了此問題,那我們來比較一下兩個包裝型別的大小,程式碼如下:
1 public class Client27 { 2 public static void main(String[] args) { 3 Integer i = new Integer(100); 4 Integer j = new Integer(100); 5 compare(i, j); 6 } 7 8 public static void compare(Integer i, Integer j) { 9 System.out.println(i == j); 10 System.out.println(i > j); 11 System.out.println(i < j); 12 13 } 14 }
程式碼很簡單,產生了兩個Integer物件,然後比較兩個的大小關係,既然包裝型別和基本型別是可以自由轉換的,那上面的程式碼是不是就可以列印出兩個相等的值呢?讓事實說話,執行結果如下:
false false false
竟然是3個false,也就是說兩個值之間不相等,也沒大小關係,這個也太奇怪了吧。不奇怪,我們來一 一解釋:
- i==j:在java中"=="是用來判斷兩個運算元是否有相等關係的,如果是基本型別則判斷值是否相等,如果是物件則判斷是否是一個物件的兩個引用,也就是地址是否相等,這裡很明顯是兩個物件,兩個地址不可能相等。
- i>j 和 i<j:在Java中,">" 和 "<" 用來判斷兩個數字型別的大小關係,注意只能是數字型別的判斷,對於Integer包裝型別,是根據其intValue()方法的返回值(也就是其相應的基本型別)進行比較的(其它包裝型別是根據相應的value值比較的,如doubleValue,floatValue等),那很顯然,兩者不肯能有大小關係的。
問題清楚了,修改總是比較容易的,直接使用Integer的例項compareTo方法即可,但是這類問題的產生更應該說是習慣問題,只要是兩個物件之間的比較就應該採用相應的方法,而不是通過Java的預設機制來處理,除非你確定對此非常瞭解。
建議28:優先使用整型池
上一個建議我們解釋了包裝物件的比較問題,本建議將繼續深入討論相關問題,首先看看如下程式碼:
1 import java.util.Scanner; 2 3 public class Client28 { 4 public static void main(String[] args) { 5 Scanner input = new Scanner(System.in); 6 while (input.hasNextInt()) { 7 int tempInt = input.nextInt(); 8 System.out.println("\n=====" + tempInt + " 的相等判斷====="); 9 // 兩個通過new產生的物件 10 Integer i = new Integer(tempInt); 11 Integer j = new Integer(tempInt); 12 System.out.println(" new 產生的物件:" + (i == j)); 13 // 基本型別轉換為包裝型別後比較 14 i = tempInt; 15 j = tempInt; 16 System.out.println(" 基本型別轉換的物件:" + (i == j)); 17 // 通過靜態方法生成一個例項 18 i = Integer.valueOf(tempInt); 19 j = Integer.valueOf(tempInt); 20 System.out.println(" valueOf產生的物件:" + (i == j)); 21 } 22 } 23 }
輸入多個數字,然後按照3中不同的方式產生Integer物件,判斷其是否相等,注意這裡使用了"==",這說明判斷的不是同一個物件。我們輸入三個數字127、128、555,結果如下:
127
=====127 的相等判斷=====
new 產生的物件:false
基本型別轉換的物件:true
valueOf產生的物件:true
128
=====128 的相等判斷=====
new 產生的物件:false
基本型別轉換的物件:false
valueOf產生的物件:false
555
=====555 的相等判斷=====
new 產生的物件:false
基本型別轉換的物件:false
valueOf產生的物件:false
很不可思議呀,數字127的比較結果竟然和其它兩個數字不同,它的裝箱動作所產生的物件竟然是同一個物件,valueOf產生的也是同一個物件,但是大於127的數字和128和555的比較過程中產生的卻不是同一個物件,這是為什麼?我們來一個一個解釋。
(1)、new產生的Integer物件
new宣告的就是要生成一個新的物件,沒二話,這是兩個物件,地址肯定不等,比較結果為false。
(2)、裝箱生成的物件
對於這一點,首先要說明的是裝箱動作是通過valueOf方法實現的,也就是說後兩個演算法相同的,那結果肯定也是一樣的,現在問題是:valueOf是如何生成物件的呢?我們來閱讀以下Integer.valueOf的原始碼:
1 /** 2 * Returns an {@code Integer} instance representing the specified 3 * {@code int} value. If a new {@code Integer} instance is not 4 * required, this method should generally be used in preference to 5 * the constructor {@link #Integer(int)}, as this method is likely 6 * to yield significantly better space and time performance by 7 * caching frequently requested values. 8 * 9 * This method will always cache values in the range -128 to 127, 10 * inclusive, and may cache other values outside of this range. 11 * 12 * @param i an {@code int} value. 13 * @return an {@code Integer} instance representing {@code i}. 14 * @since 1.5 15 */ 16 public static Integer valueOf(int i) { 17 assert IntegerCache.high >= 127; 18 if (i >= IntegerCache.low && i <= IntegerCache.high) 19 return IntegerCache.cache[i + (-IntegerCache.low)]; 20 return new Integer(i); 21 }
這段程式碼的意思已經很明瞭了,如果是-128到127之間的int型別轉換為Integer物件,則直接從cache陣列中獲得,那cache陣列裡是什麼東西,JDK7的原始碼如下:
1 /** 2 * Cache to support the object identity semantics of autoboxing for values between 3 * -128 and 127 (inclusive) as required by JLS. 4 * 5 * The cache is initialized on first usage. The size of the cache 6 * may be controlled by the -XX:AutoBoxCacheMax=<size> option. 7 * During VM initialization, java.lang.Integer.IntegerCache.high property 8 * may be set and saved in the private system properties in the 9 * sun.misc.VM class. 10 */ 11 12 private static class IntegerCache { 13 static final int low = -128; 14 static final int high; 15 static final Integer cache[]; 16 17 static { 18 // high value may be configured by property 19 int h = 127; 20 String integerCacheHighPropValue = 21 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); 22 if (integerCacheHighPropValue != null) { 23 int i = parseInt(integerCacheHighPropValue); 24 i = Math.max(i, 127); 25 // Maximum array size is Integer.MAX_VALUE 26 h = Math.min(i, Integer.MAX_VALUE - (-low)); 27 } 28 high = h; 29 30 cache = new Integer[(high - low) + 1]; 31 int j = low; 32 for(int k = 0; k < cache.length; k++) 33 cache[k] = new Integer(j++); 34 } 35 36 private IntegerCache() {} 37 }
cache是IntegerCache內部類的一個靜態陣列,容納的是-128到127之間的Integer物件。通過valueOf產生包裝物件時,如果int引數在-128到127之間,則直接從整型池中獲得物件,不在該範圍內的int型別則通過new生成包裝物件。
明白了這一點,要理解上面的輸出結果就迎刃而解了,127的包裝物件是直接從整型池中獲得的,不管你輸入多少次127這個數字,獲得的物件都是同一個,那地址自然是相等的。而128、555超出了整型池範圍,是通過new產生一個新的物件,地址不同,當然也就不相等了。
以上的理解也是整型池的原理,整型池的存在不僅僅提高了系統效能,同時也節約了記憶體空間,這也是我們使用整型池的原因,也就是在宣告包裝物件的時候使用valueOf生成,而不是通過建構函式來生成的原因。順便提醒大家,在判斷物件是否相等的時候,最好使用equals方法,避免使用"=="產生非預期效果。
注意:通過包裝型別的valueOf生成的包裝例項可以顯著提高空間和時間效能。
建議29:優先選擇基本型別
包裝型別是一個類,它提供了諸如構造方法,型別轉換,比較等非常實用的功能,而且在Java5之後又實現了與基本型別的轉換,這使包裝型別如虎添翼,更是應用廣泛了,在開發中包裝型別已經隨處可見,但無論是從安全性、效能方面來說,還是從穩定性方面來說,基本型別都是首選方案。我們看一段程式碼:
1 public class Client29 { 2 public static void main(String[] args) { 3 Client29 c = new Client29(); 4 int i = 140; 5 // 分別傳遞int型別和Integer型別 6 c.testMethod(i); 7 c.testMethod(new Integer(i)); 8 } 9 10 public void testMethod(long a) { 11 System.out.println(" 基本型別的方法被呼叫"); 12 } 13 14 public void testMethod(Long a) { 15 System.out.println(" 包裝型別的方法被呼叫"); 16 } 17 }
在上面的程式中首先宣告瞭一個int變數i,然後加寬轉變成long型,再呼叫testMethod()方法,分別傳遞int和long的基本型別和包裝型別,諸位想想該程式是否能夠編譯?如果能編譯,輸出結果又是什麼呢?
首先,這段程式絕對是能夠編譯的。不過,說不能編譯的同學還是動了一番腦筋的,你可能猜測以下這些地方不能編譯:
(1)、testMethod方法過載問題。定義的兩個testMethod()方法實現了過載,一個形參是基本型別,一個形參是包裝型別,這類過載很正常。雖然基本型別和包裝型別有自動裝箱、自動拆箱功能,但並不影響它們的過載,自動拆箱(裝箱)只有在賦值時才會發生,和編譯過載沒有關係。
(2)、c.testMethod(i) 報錯。i 是int型別,傳遞到testMethod(long a)是沒有任何問題的,編譯器會自動把 i 的型別加寬,並將其轉變為long型,這是基本型別的轉換法則,也沒有任何問題。
(3)、c.testMethod(new Integer(i))報錯。程式碼中沒有testMethod(Integer i)方法,不可能接收一個Integer型別的引數,而且Integer和Long兩個包裝型別是兄弟關係,不是繼承關係,那就是說肯定編譯失敗了?不,編譯時成功的,稍後再解釋為什麼這裡編譯成功。
既然編譯通過了,我們看一下輸出:
基本型別的方法被呼叫
基本型別的方法被呼叫
c.testMethod(i)的輸出是正常的,我們已經解釋過了,那第二個輸出就讓人困惑了,為什麼會呼叫testMethod(long a)方法呢?這是因為自動裝箱有一個重要原則:基本型別可以先加寬,再轉變成寬型別的包裝型別,但不能直接轉變成寬型別的包裝型別。這句話比較拗口,簡單的說就是,int可以加寬轉變成long,然後再轉變成Long物件,但不能直接轉變成包裝型別,注意這裡指的都是自動轉換,不是通過建構函式生成,為了解釋這個原則,我們再來看一個例子:
1 public class Client29 { 2 public static void main(String[] args) { 3 Client29 c = new Client29(); 4 int i = 140; 5 c.testMethod(i); 6 } 7 8 public void testMethod(Long a) { 9 System.out.println(" 包裝型別的方法被呼叫"); 10 } 11 }
這段程式的編譯是不通過的,因為i是一個int型別,不能自動轉變為Long型,但是修改成以下程式碼就可以通過了:
int i = 140; long a =(long)i; c.testMethod(a);
這就是int先加寬轉變成為long型,然後自動轉換成Long型,規則說明了,我們繼續來看testMethod(Integer.valueOf(i))是如何呼叫的,Integer.valueOf(i)返回的是一個Integer物件,這沒錯,但是Integer和int是可以互相轉換的。沒有testMethod(Integer i)方法?沒關係,編譯器會嘗試轉換成int型別的實參呼叫,Ok,這次成功了,與testMethod(i)相同了,於是乎被加寬轉變成long型---結果也很明顯了。整個testMethod(Integer.valueOf(i))的執行過程是這樣的:
(1)、i 通過valueOf方法包裝成一個Integer物件
(2)、由於沒有testMethod(Integer i)方法,編譯器會"聰明"的把Integer物件轉換成int。
(3)、int自動拓寬為long,編譯結束
使用包裝型別確實有方便的方法,但是也引起一些不必要的困惑,比如我們這個例子,如果testMethod()的兩個過載方法使用的是基本型別,而且實參也是基本型別,就不會產生以上問題,而且程式的可讀性更強。自動裝箱(拆箱)雖然很方便,但引起的問題也非常嚴重,我們甚至都不知道執行的是哪個方法。
注意:重申,基本型別優先考慮。
建議30:不要隨便設定隨機種子
隨機數用的地方比較多,比如加密,混淆計算,我們使用隨機數期望獲得一個唯一的、不可仿造的數字,以避免產生相同的業務資料造成混亂。在Java專案中通常是通過Math.random方法和Random類來獲得隨機數的,我們來看一段程式碼:
1 import java.util.Random; 2 3 public class Client30 { 4 public static void main(String[] args) { 5 Random r = new Random(); 6 for(int i=1; i<=4; i++){ 7 System.out.println("第"+i+"次:"+r.nextInt()); 8 9 } 10 } 11 }
程式碼很簡單,我們一般都是這樣獲得隨機數的,執行此程式可知,三次列印,的隨機數都不相同,即使多次執行結果也不同,這也正是我們想要隨機數的原因,我們再來看看下面的程式:
1 public class Client30 { 2 public static void main(String[] args) { 3 Random r = new Random(1000); 4 for(int i=1; i<=4; i++){ 5 System.out.println("第"+i+"次:"+r.nextInt()); 6 7 } 8 } 9 }
上面使用了Random的有參構造,執行結果如下:
第1次:-1244746321
第2次:1060493871
第3次:-1826063944
第4次:1976922248
計算機不同輸出的隨機數也不同,但是有一點是相同的:在同一臺機器上,甭管執行多少次,所列印的隨機數都是相同的,也就是說第一次執行,會列印出這幾個隨機數,第二次執行還是列印出這三個隨機數,只要是在同一臺機器上,就永遠都會列印出相同的隨機數,似乎隨機數不隨機了,問題何在?
那是因為產生的隨機數的種子被固定了,在Java中,隨機數的產生取決於種子,隨機數和種子之間的關係遵從以下兩個原則:
- 種子不同,產生不同的隨機數
- 種子相同,即使例項不同也產生相同的隨機數
看完上面兩個規則,我們再來看這個例子,會發現問題就出在有參構造上,Random類的預設種子(無參構造)是System.nonoTime()的返回值(JDK1.5版本以前預設種子是System.currentTimeMillis()的返回值),注意這個值是距離某一個固定時間點的納秒數,不同的作業系統和硬體有不同的固定時間點,也就是說不同的作業系統其納秒值是不同的,而同一個作業系統納秒值也會不同,隨機數自然也就不同了.(順便說下,System.nonoTime不能用於計算日期,那是因為"固定"的時間是不確定的,納秒值甚至可能是負值,這點與System.currentTiemMillis不同)。
new Random(1000)顯示的設定了隨機種子為1000,執行多次,雖然例項不同,但都會獲得相同的四個隨機數,所以,除非必要,否則不要設定隨機種子。
順便提一下,在Java中有兩種方法可以獲得不同的隨機數:通過,java.util.Random類獲得隨機數的原理和Math.random方法相同,Math.random方法也是通過生成一個Random類的例項,然後委託nextDouble()方法的,兩者殊途同歸,沒有差別。
注意:若非必要,不要設定隨機數種子。