歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Java避免創建不必要的對象

Java避免創建不必要的對象

日期:2017/3/1 9:08:50   编辑:Linux編程

最近看到了《Effective Java》這本書,這本書包含的內容非常豐富,這本書我就不多介紹了,只能默默的說一句,作為一名java開發錯過了這本書難免會成為一個小遺憾,所以還是建議有時間的小伙伴能夠去看看這本書,時間擠擠總還是有的。這本書介紹的很多東西我現在也還看不太明白,很多東西我們在平時的開發中也不見得會用上,所以我不會每個東西都拿來詳細解釋一遍,只會從中抽取我們平時開發中比較實用的,以及小Alan這個小菜鳥能夠看懂的部分,至於一些不實用的以及比較高深的部分那就只能隨著小Alan的工作經歷和深入理解再慢慢的整理出來給自己也給部分覺得有用的朋友理清思路。

《Effective Java中文版 第2版》.(Joshua Bloch)(高清pdf)+英文版+源代碼 http://www.linuxidc.com/Linux/2016-11/137370.htm

《Effective Java 》第5條:避免創建不必要的對象

我們把原文拆分成幾部分來理解,實現一個一個的小目標,最後來完全理解這一塊的內容。

第一部分:一般來說,最好能重用對象而不是在每次需要的時候就創建一個相同功能的新對象。重用方式既快速,又流行。如果對象是不可變的,它就始終可以被重用。

反面例子:

String s = new String("啪啪啪");  //Don't do this!

該語句每次被執行的時候都創建一個新的String實例,但是這些創建對象的動作全都是不必要的。傳遞給String構造器的參數("啪啪啪")本身就是一個String實例,功能方面等同於構造器創建的所有對象。如果這種用法是在一個循環中,或是在一個被頻繁調用的方法中,就會創建成千上萬不必要的String實例。

改進版本:

String s = "啪啪啪";

這個版本只用了一個String實例,而不是每次執行的時候都創建一個新的String實例。而且,它可以保證,對於所有在同一台虛擬機中運行的代碼,只要它們包含相同的字符串字面常量,該對象就會被重用。

擴展思路:①在Java1.7中運行,Java會在方法區運行時常量池中記錄首次出現的實例,也就是說會在常量池中保存"啪啪啪",那麼當你下次調用String s = "啪啪啪";的時候,Java會直接返回這個對象的引用,而不會去重新創建一個新的對象,這樣就節省了內存的開銷,也可以放心的在循環中去使用,也不怕在方法中被頻繁的調用。String s = new String("啪啪啪");實際上創建了兩個對象,一個存放在堆中,一個就是保存在常量池中的"啪啪啪",s只是對象的引用保存在棧中,而String s = "啪啪啪";只會創建一個對象保存在常量池中,然後保存一個對象的引用在棧中就ok了(對Java虛擬機理解不是很深入,理解有誤請指出,萬分感謝)。

第二部分:對於同時提供了靜態工廠方法和構造器的不可變類,通常可以使用靜態工廠方法而不是構造器,以避免創建不必要的對象。例如,靜態工廠方法Boolean.valueOf(String)幾乎總是優先於構造器Boolean(String)。構造器在每次被調用的時候都會創建一個新的對象,而靜態工廠方法則從來不要求這樣做,實際上也不會這樣做。

擴展思路:

 1 package com.czgo.effective;
 2 
 3 /**
 4  * 用valueOf()靜態工廠方法代替構造器
 5  * @author AlanLee
 6  * @version 2016/12/01
 7  *
 8  */
 9 public class Test {
10 
11     public static void main(String[] args) {
12         // 使用帶參構造器
13         Integer a1 = new Integer("1");
14         Integer a2 = new Integer("1");
15         
16         //使用valueOf()靜態工廠方法
17         Integer a3 = Integer.valueOf("1");
18         Integer a4 = Integer.valueOf("1");
19         
20         //結果為false,因為創建了不同的對象
21         System.out.println(a1 == a2);
22         
23         //結果為true,因為不會新建對象
24         System.out.println(a3 == a4);
25     }
26 
27 }

可見,使用靜態工廠方法valueOf不會新建一個對象,避免大量不必要的對象被創建,實際上很多類默認的valueOf方法都不會返回一個新的實例,比如原文提到的Boolean類型,不僅僅是Java提供的這些類型,我們在平時的開發中如果也有類似的需求不妨模仿Java給我們提供的靜態工廠方法,給我們自己的類也定義這樣的靜態工廠方法來實現對象的獲取,避免對象的重復創建,但是也不要過度迷信使用靜態工廠方法的方式,這種方式也有它的弊端(有關靜態工廠方法的知識可以看看《Effective Java》第一條),個人很少使用這種方式,平時的類多創建個對象也不會有太大的影響,只要稍微注意下用法就ok了。

第三部分:除了重用不可變的對象之外,也可以重用那些已知不會修改的可變對象。書上寫的例子讓人非常難以理解,我也沒花時間去看了,我給大家想出來一個類似的例子,也不知道是否是這個意思,多多指教!

反面例子:

 1 package com.czgo.effective;
 2 
 3 import java.sql.Connection;
 4 import java.sql.DriverManager;
 5 import java.sql.SQLException;
 6 
 7 public class DBUtilBad {
 8     private static final String URL = "jdbc:mysql://127.0.0.1:3306/imooc";
 9     private static final String UNAME = "root";
10     private static final String PWD = "root";
11 
12     public static Connection getConnection() {
13         Connection conn = null;
14         try {
15             // 1.加載驅動程序
16             Class.forName("com.mysql.jdbc.Driver");
17             // 2.獲得數據庫的連接
18             conn = DriverManager.getConnection(URL, UNAME, PWD);
19         } catch (ClassNotFoundException e) {
20             e.printStackTrace();
21         } catch (SQLException e) {
22             e.printStackTrace();
23         }
24         return conn;
25     }
26 }

該類提供的getConnection方法獲取JDBC數據庫連接對象,每次調用該方法都會新建一個conn實例,而我們知道在平時的開發中數據庫連接對象往往只需要一個,也不會總是去修改它,沒必要每次都去新創建一個連接對象,每次都去創建一個實例不知道程序會不會出現什麼意外情況,這個我不知道,但有一點是肯定的,這種方式影響程序的運行性能,增加了Java虛擬機垃圾回收器的負擔。我們可以對它進行改進。

改進版本:

 1 package com.czgo.effective;
 2 
 3 import java.sql.Connection;
 4 import java.sql.DriverManager;
 5 import java.sql.SQLException;
 6 
 7 public class DBUtil {
 8     private static final String URL = "jdbc:mysql://127.0.0.1:3306/imooc";
 9     private static final String UNAME = "root";
10     private static final String PWD = "root";
11 
12     private static Connection conn = null;
13 
14     static {
15         try {
16             // 1.加載驅動程序
17             Class.forName("com.mysql.jdbc.Driver");
18             // 2.獲得數據庫的連接
19             conn = DriverManager.getConnection(URL, UNAME, PWD);
20         } catch (ClassNotFoundException e) {
21             e.printStackTrace();
22         } catch (SQLException e) {
23             e.printStackTrace();
24         }
25     }
26 
27     public static Connection getConnection() {
28         return conn;
29     }
30 }

我們使用了靜態代碼塊來創建conn實例,改進後只有在類加載初始化的時候創建了conn實例一次,而不是在每次調用getConnection方法的時候都去創建conn實例。如果getConnection方法被頻繁的調用和使用,這種方式將會顯著的提高我們程序的性能。除了提高性能之外,代碼的含義也更加的清晰了,使得代碼更易於理解。

第四部分:Map接口的keySet方法返回該Map對象的Set視圖,其中包含該Map中所有的鍵(key)。粗看起來,好像每次調用keySet都應該創建一個新的Set實例,但是,對於一個給定的Map對象,實際上每次調用keySet都返回同樣的Set實例。雖然被返回的Set實例一般是可改變的,但是所有返回的對象在功能上是等同的:當其中一個返回對象發生變化的時候,所有其他返回對象也要發生變化,因為它們是由同一個Map實例支撐的。雖然創建keySet視圖對象的多個實例並無害處,卻也是沒有必要的。這一部分內容我不是特別的理解,貼一段代碼給大家分析,如果有對這部分比較理解的朋友,請留下你寶貴的經驗,萬分感謝!

 1 package com.czgo.effective;
 2 
 3 import java.util.HashMap;
 4 import java.util.Iterator;
 5 import java.util.Map;
 6 import java.util.Set;
 7 
 8 public class TestKeySet {
 9 
10     public static void main(String[] args) {
11         
12         Map<String,Object> map = new HashMap<String,Object>();
13         map.put("A", "A");
14         map.put("B", "B");
15         map.put("C", "C");
16         
17         Set<String> set = map.keySet();
18         Iterator<String> it = set.iterator();
19         while(it.hasNext()){
20             System.out.println(it.next()+"①");
21         }
22         
23         System.out.println("---------------");
24         
25         map.put("D", "D");
26         set = map.keySet();
27         it = set.iterator();
28         while(it.hasNext()){
29             System.out.println(it.next()+"②");
30         }
31         
32     }
33 
34 }

第五部分:有一種創建多余對象的新方法,稱作自動裝箱(autoboxing),它允許程序員將基本類型和裝箱基本類型(Boxed Primitive Type<引用類型>)混用,按需要自動裝箱和拆箱。自動裝箱使得基本類型和引用類型之間的差別變得模糊起來,但是並沒有完全消除。它們在語義上還有著微妙的差別,在性能上也有著比較明顯的差別。考慮下面的程序,它計算所有int正值的總和。為此,程序必須使用long變量,因為int不夠大,無法容納所有int正值的總和:

 1 package com.czgo.effective;
 2 
 3 public class TestLonglong {
 4 
 5     public static void main(String[] args) {
 6         Long sum = 0L;
 7         for(long i = 0; i < Integer.MAX_VALUE; i++){
 8             sum += i;
 9         }
10         System.out.println(sum);
11     }
12     
13 }

這段程序算出的結果是正確的,但是比實際情況要慢的多,只因為打錯了一個字符。變量sum被聲明成Long而不是long,意味著程序構造了大約2的31次方個多余的Long實例(大約每次往Long sum中增加long時構造一個實例)。將sum的聲明從Long改成long,速度快了不是一點半點。結論很明顯:要優先使用基本類型而不是引用類型,要當心無意識的自動裝箱。

最後,不要錯誤地認為"創建對象的代價非常昂貴,我們應該盡可能地避免創建對象"。相反,由於小對象的構造器只做很少量的顯示工作,所以小對象的創建和回收動作是非常廉價的,特別是在現代的JVM實現上更是如此。通過創建附加的對象,提升程序的清晰性、簡潔性和功能性,這通常是件好事。

反之,通過維護自己的對象池(Object pool)來避免創建對象並不是一種好的做法,除非池中的對象是非常重量級的。真正正確使用對象池的典型對象示例就是數據庫連接池。建立數據庫連接的代價是非常昂貴的,因此重用這些對象非常有意義。而如今的JVM(Java虛擬機)具有高度優化的垃圾回收器,如果是輕量的對象池可能還不如垃圾回收器的性能。

這裡我們說到“當你應該重用現有對象的時候,請不要創建新的對象”,反之我們也應該考慮一個問題“當你應該創建新對象的時候,請不要重用現有的對象”。有時候重用對象要付出的代價要遠遠大於因創建重復對象而付出的代價。必要時,如果沒能創建新的對象實例將會導致潛在的錯誤和安全漏洞;而不必要地創建對象則只會影響程序的風格和性能。

Copyright © Linux教程網 All Rights Reserved