編寫高質量代碼:改善Java程式的151個建議(第2章:基本類型___建議26~30)

来源:http://www.cnblogs.com/selene/archive/2016/09/12/5854300.html
-Advertisement-
Play Games

建議26:提防包裝類型的null值 我們知道Java引入包裝類型(Wrapper Types)是為瞭解決基本類型的實例化問題,以便讓一個基本類型也能參與到面向對象的編程世界中。而在Java5中泛型更是對基本類型說了"不",如果把一個整型放入List中,就必須使用Integer包裝類型。我們看一段代碼 ...


建議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,也就是說兩個值之間不相等,也沒大小關係,這個也太奇怪了吧。不奇怪,我們來一 一解釋:

  1. i==j:在java中"=="是用來判斷兩個操作數是否有相等關係的,如果是基本類型則判斷值是否相等,如果是對象則判斷是否是一個對象的兩個引用,也就是地址是否相等,這裡很明顯是兩個對象,兩個地址不可能相等。
  2. 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中,隨機數的產生取決於種子,隨機數和種子之間的關係遵從以下兩個原則:

  1. 種子不同,產生不同的隨機數
  2. 種子相同,即使實例不同也產生相同的隨機數

  看完上面兩個規則,我們再來看這個例子,會發現問題就出在有參構造上,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()方法的,兩者殊途同歸,沒有差別。

註意:若非必要,不要設置隨機數種子。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 以下是這段時間抽時間刷的前5題,都是自己想的解法,或許不是最優解,只是整理下,方便日後優化提升 1. Two Sum: 2. Add Two Numbers: 3. Longest Substring Without Repeating Characters: 4. Median of Two So ...
  • 1.Python 字典(Dictionary) 字典是另一種可變容器模型,且可存儲任意類型對象。 字典的每個鍵值(key=>value)對用冒號(:)分割,每個對之間用逗號(,)分割,整個字典包括在花括弧({})中 ,格式如下所示: 鍵必須是唯一的,但值則不必。 值可以取任何數據類型,但鍵必須是不可 ...
  • Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list. For example,Given 1->2 ...
  • 這篇文章來介紹一下spfa(Shortest Path Faster Algorithm)這種演算法 這是一種單源最短路的一種十分高效的的演算法。 我們需要用鄰接表來存儲一下圖,以及用隊列進行優化。 我們以1為起點,以n為終點來講一下(一共n個點) 用L數組記錄當前點的最短路 先把每一條邊的最短路賦成最 ...
  • 這題是在01背包問題的基礎上,擴充了重量,需要用時間換空間。要註意重量wi為a*2^b(a<=10,b<=30),很容易想到要按 b 分開做背包的DP。重點是怎麼從b-1繼承到b——仔細看題,發現只有一次詢問,那麼就可以在這個W上做文章:依W的大小進行所有的DP。由於是按b的大小進行DP,所以把數都 ...
  • 最近由於在做一個眾籌的項目,其中有一個查找項目支持數的介面,查找的方法定義的是一個long型的,我更新項目中的支持數的時候是int型的,所以需要在long型與int型之間轉化,下麵把轉轉化的詳細方法記錄一下,以便後面總結學習: 一.將long型轉化為int型,這裡的long型是基礎類型: long ...
  • 這個,不管是什麼書都會這樣說,因為常常我們並不需要繼承,而只是想把類進行一定的擴展,而我們想擴展的屬性或方法對應的類都有,這個時候如果兩者是is a的關係,這種關係是確實存在的,那麼就可以使用繼承,不然一般都是建議使用複合。 如果我們隊一個類進行繼承的時候,我們如果對其內部的邏輯並不十分瞭解的時候, ...
  • 獲取類的名稱 獲取該類的方法 獲取方法的返回值類型 獲取方法的名稱 獲取方法的參數的類型 返回結果 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...