小Alan最近看到了《Effective Java》這本書,這本書包含的內容非常豐富,這本書我就不多介紹了,只能默默的說一句,作為一名java開發錯過了這本書難免會成為一個小遺憾,所以還是建議有時間的朋友能夠去看看這本書,時間擠擠總還是有的。這本書介紹的很多東西我現在也還看不太明白,很多東西我們在平時的開發中也看不見得會用上,所以我不會每個東西都拿來詳細解釋一遍,只會從中抽取我們平時開發中比較實用的,以及小Alan這個小菜鳥能夠看懂的部分,至於一些不實用的以及比較高深的部分那隻能隨著小Alan的工作經歷和深入理解再慢慢的整理出來給自己也給部分覺得有用的朋友理清思緒。
《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)。構造器在每次被呼叫的時候都會創建一個新的對象,而靜態工廠方法則從來不要求這樣做,實際上也不會這樣做。
擴展思路:
package com.czgo.effective; /** * 用valueOf()静态工厂方法代替构造器 * @author AlanLee * @version 2016/12/01 * */ public class Test { public static void main(String[] args) { // 使用带参构造器 Integer a1 = new Integer("1"); Integer a2 = new Integer("1"); //使用valueOf()静态工厂方法 Integer a3 = Integer.valueOf("1"); Integer a4 = Integer.valueOf("1"); //结果为false,因为创建了不同的对象 System.out.println(a1 == a2); //结果为true,因为不会新建对象 System.out.println(a3 == a4); } }
可見,使用靜態工廠方法valueOf不會新建一個對象,避免大量不必要的對像被創建,實際上很多類默認的valueOf方法都不會返回一個新的實例,比如原文提到的Boolean類型,不只是Java提供的這些類型,我們在平時的開發中如果也有類似的需求不妨模仿Java給我們提供的靜態工廠方法,給我們自己的類別也定義這樣的靜態工廠方法來實現物件的獲取,避免物件的重複創建,但是也不要過度迷信使用靜態工廠方法的方式,這種方式也有它的弊端(有關靜態工廠方法的知識可以看看《Effective Java》第一條),個人很少使用這種方式,平常的類別多建立個物件也不會有太大的影響,只要稍微注意下用法就ok了。
第三部分:除了重複使用不可變的物件之外,也可以重複使用那些已知不會修改的可變物件。書上寫的例子讓人非常難以理解,我也沒花時間去看了,我給大家想出來一個類似的例子,也不知道是否是這個意思,多多指教!
反面範例:
package com.czgo.effective; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtilBad { private static final String URL = "jdbc:mysql://127.0.0.1:3306/imooc"; private static final String UNAME = "root"; private static final String PWD = "root"; public static Connection getConnection() { Connection conn = null; try { // 1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); // 2.获得数据库的连接 conn = DriverManager.getConnection(URL, UNAME, PWD); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } return conn; } }
该类提供的getConnection方法获取JDBC数据库连接对象,每次调用该方法都会新建一个conn实例,而我们知道在平时的开发中数据库连接对象往往只需要一个,也不会总是去修改它,没必要每次都去新创建一个连接对象,每次都去创建一个实例不知道程序会不会出现什么意外情况,这个我不知道,但有一点是肯定的,这种方式影响程序的运行性能,增加了Java虚拟机垃圾回收器的负担。我们可以对它进行改进。
改进版本:
package com.czgo.effective; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://127.0.0.1:3306/imooc"; private static final String UNAME = "root"; private static final String PWD = "root"; private static Connection conn = null; static { try { // 1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); // 2.获得数据库的连接 conn = DriverManager.getConnection(URL, UNAME, PWD); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } public static Connection getConnection() { return conn; } }
我们使用了静态代码块来创建conn实例,改进后只有在类加载初始化的时候创建了conn实例一次,而不是在每次调用getConnection方法的时候都去创建conn实例。如果getConnection方法被频繁的调用和使用,这种方式将会显著的提高我们程序的性能。除了提高性能之外,代码的含义也更加的清晰了,使得代码更易于理解。
第四部分:Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(key)。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其他返回对象也要发生变化,因为它们是由同一个Map实例支撑的。虽然创建keySet视图对象的多个实例并无害处,却也是没有必要的。
package com.czgo.effective; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class TestKeySet { public static void main(String[] args) { Map<String,Object> map = new HashMap<String,Object>(); map.put("A", "A"); map.put("B", "B"); map.put("C", "C"); Set<String> set = map.keySet(); Iterator<String> it = set.iterator(); while(it.hasNext()){ System.out.println(it.next()+"①"); } System.out.println("---------------"); map.put("D", "D"); set = map.keySet(); it = set.iterator(); while(it.hasNext()){ System.out.println(it.next()+"②"); } } }
第五部分:有一种创建多余对象的新方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type<引用类型>)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和引用类型之间的差别变得模糊起来,但是并没有完全消除。它们在语义上还有着微妙的差别,在性能上也有着比较明显的差别。考虑下面的程序,它计算所有int正值的总和。为此,程序必须使用long变量,因为int不够大,无法容纳所有int正值的总和:
package com.czgo.effective; public class TestLonglong { public static void main(String[] args) { Long sum = 0L; for(long i = 0; i < Integer.MAX_VALUE; i++){ sum += i; } System.out.println(sum); } }
段程序算出的结果是正确的,但是比实际情况要慢的多,只因为打错了一个字符。变量sum被声明成Long而不是long,意味着程序构造了大约2的31次方个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。将sum的声明从Long改成long,速度快了不是一点半点。结论很明显:要优先使用基本类型而不是引用类型,要当心无意识的自动装箱。
最后,不要错误地认为"创建对象的代价非常昂贵,我们应该尽可能地避免创建对象"。相反,由于小对象的构造器只做很少量的显示工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
反之,通过维护自己的对象池(Object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而如今的JVM(Java虚拟机)具有高度优化的垃圾回收器,如果是轻量的对象池可能还不如垃圾回收器的性能。
这里我们说到“当你应该重用现有对象的时候,请不要创建新的对象”,反之我们也应该考虑一个问题“当你应该创建新对象的时候,请不要重用现有的对象”。有时候重用对象要付出的代价要远远大于因创建重复对象而付出的代价。必要时,如果没能创建新的对象实例将会导致潜在的错误和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。