Effective Java读书笔记三:创建和销毁对象
第1條:考慮用靜態(tài)工廠方法代替構造器
對于類而言,為了讓客服端獲得它的一個實例最常用的的一個方法就是提供一個公有的構造器。還有一種方法,類可以提供一個公有的靜態(tài)工廠方法(static factory method),它只是一個返回類實例的靜態(tài)方法。
通過靜態(tài)工廠方法構造對象的優(yōu)勢:
這種靈活性的一種應用:API可以返回對象,同時又不會使對象的類變成公有的。公有的靜態(tài)方法所返回的對象的類不僅可以是非公有的,而且該類還可以隨著每次調用而發(fā)生變化著取決于靜態(tài)工廠方法的參數(shù)值,只要是已聲明返回類型的子類型,都是允許的。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
靜態(tài)工廠方法的主要缺點在于:
常用的靜態(tài)工廠名稱:valueOf,of,getInstance,newInstance,getType,newType.
第2條:遇到多個構造參數(shù)時要考慮用構建器(Builder模式)
class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { //對象的必選參數(shù) private final int servingSize; private final int servings; //對象的可選參數(shù)的缺省值初始化 private int calories = 0; private int fat = 0; private int carbohydrate = 0; private int sodium = 0; //只用少數(shù)的必選參數(shù)作為構造器的函數(shù)參數(shù) public Builder(int servingSize,int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } } //使用方式 public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100) .sodium(35).carbohydrate(27).build(); System.out.println(cocaCola); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
對于Builder方式,可選參數(shù)的缺省值問題也將不再困擾著所有的使用者。這種方式還帶來了一個間接的好處是,不可變對象的初始化以及參數(shù)合法性的驗證等工作在構造函數(shù)中原子性的完成了。
第3條:用私有構造器或者枚舉類型強化Singleton屬性
1、將構造函數(shù)私有化,直接通過靜態(tài)公有的final域字段獲取單實例對象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public void leaveTheBuilding() { ... } }- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
這樣的方式主要優(yōu)勢在于簡潔高效,使用者很快就能判定當前類為單實例類,在調用時直接操作Elivs.INSTANCE即可,由于沒有函數(shù)的調用,因此效率也非常高效。然而事物是具有一定的雙面性的,這種設計方式在一個方向上走的過于極端了,因此他的缺點也會是非常明顯的。如果今后Elvis的使用代碼被遷移到多線程的應用環(huán)境下了,系統(tǒng)希望能夠做到每個線程使用同一個Elvis實例,不同線程之間則使用不同的對象實例。那么這種創(chuàng)建方式將無法實現(xiàn)該需求,因此需要修改接口以及接口的調用者代碼,這樣就帶來了更高的修改成本。
2、 通過公有域成員的方式返回單實例對象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
這種方法很好的彌補了第一種方式的缺陷,如果今后需要適應多線程環(huán)境的對象創(chuàng)建邏輯,僅需要修改Elvis的getInstance()方法內部即可,對用調用者而言則是不變的,這樣便極大的縮小了影響的范圍。至于效率問題,現(xiàn)今的JVM針對該種函數(shù)都做了很好的內聯(lián)優(yōu)化,因此不會產生因函數(shù)頻繁調用而帶來的開銷。
3、使用枚舉的方式(Java?SE5):
public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
就目前而言,這種方法在功能上和公有域方式相近,但是他更加簡潔更加清晰,擴展性更強也更加安全。
第4條:通過私有構造器強化不可實例化的能力
對于有些工具類如Java.lang.Math、java.util.Arrays等,其中只是包含了靜態(tài)方法和靜態(tài)域字段,因此對這樣的class實例化就顯得沒有任何意義了。然而在實際的使用中,如果不加任何特殊的處理,這樣的classes是可以像其他classes一樣被實例化的。這里介紹了一種方式,既將缺省構造函數(shù)設置為private,這樣類的外部將無法實例化該類,與此同時,在這個私有的構造函數(shù)的實現(xiàn)中直接拋出異常,從而也避免了類的內部方法調用該構造函數(shù)。
public class UtilityClass { //Suppress default constructor for noninstantiability. private UtilityClass() { throw new AssertionError(); } }- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
這樣定義之后,該類將不會再被外部實例化了,否則會產生編譯錯誤。然而這樣的定義帶來的最直接的負面影響是該類將不能再被子類化。
第5條:避免創(chuàng)建不必要的對象
一般來說,最好能重用對象而不是在每次需要的時候就創(chuàng)建一個相同功能的新對象。如果對象是不可變的,它就始終可以被重用。?
反例:
- 1
- 1
該語句每次被執(zhí)行的時候都創(chuàng)建一個新的String實例,但是這些創(chuàng)建對象的動作全都是不必要的,傳遞給String構造器的參數(shù)(“stringette”)本身就是一個String實例,功能方面等同于構造器創(chuàng)建的對象。如果這種用法是在一個循環(huán)中,或者是在一個被頻繁調用的方法中,就會創(chuàng)建出很多不必要的String實例。?
改進:
- 1
- 1
改進后,只用一個String實例,而不是每次執(zhí)行的時候都創(chuàng)建一個新的實例,而且,它可以保證,對于所有在同一虛擬機中運行的代碼,只要它們包含相同的字符串字面常量,該對象就會被重用。
對于同時提供了靜態(tài)工廠方法和構造器的不可變類,通常可以使用靜態(tài)工廠方法而不是構造器,以避免創(chuàng)建不必要的對象。?
例如,靜態(tài)工廠方法Boolean.valueOf(String)幾乎總是比構造器Boolean(String)好,構造器在每次被調用的時候都會創(chuàng)建一個新的對象,而靜態(tài)工廠方法則從來不要求這樣做,實際上也不會這樣做。
要優(yōu)先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱:
public static void main(String[] args) {Long sum = 0L;for (long i = 0; i < Integer.MAX_VALUE; i++) {sum += i;}System.out.println(sum); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
這段程序算出的答案是正確的,但是比實際情況要更慢一些,只因為打錯了一個字符。變量sum被聲明成Long而不是long,這就意味著程序構造了大約2^31個多的Long實例。
Java共有9中基本類型,同別的語言有重要區(qū)別的是這9中類型所占存儲空間大小與機器硬件架構無關,這使得Java程序有很強的可移植性,如下圖:
不要錯誤地認為“創(chuàng)建對象的代價非常昂貴,我們應該要盡可能地避免創(chuàng)建對象,而不是不創(chuàng)建對象”,相反,由于小對象的構造器只做很少量的工作,所以,小對象的創(chuàng)建和回收動作是非常廉價的,特別是在現(xiàn)代的JVM實現(xiàn)上更是如此。通過創(chuàng)建附加的對象,提升程序的清晰性、簡潔性和功能性,這通常也是件好事。
反之,通過維護自己的對象池來創(chuàng)建對象并不是一種好的做法,除非池中的對象是非常重量級的。真正正確使用對象池的典型對象示例就是數(shù)據(jù)庫連接池。建立數(shù)據(jù)庫連接的代價是非常昂貴的,因此重用這些對象非常有意義。
另外,在1.5版本里,對基本類型的整形包裝類型使用時,要使用形如 Byte.valueOf來創(chuàng)建包裝類型,因為-128~127的數(shù)會緩存起來,所以我們要從緩沖池中取,Short、Integer、Long也是這樣。
第6條:消除過期的對象引用
盡管Java不像C/C++那樣需要手工管理內存資源,而是通過更為方便、更為智能的垃圾回收機制來幫助開發(fā)者清理過期的資源。即便如此,內存泄露問題仍然會發(fā)生在你的程序中,只是和C/C++相比,Java中內存泄露更加隱匿,更加難以發(fā)現(xiàn),見如下代碼:
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if (elements.length == size) elements = Arrays.copys(elements,2*size+1); } }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
這段程序有一個“內存泄漏”問題,如果一個棧先是增長,然后再收縮,那么,從棧中彈出來的對象不會被當做垃圾回收,即使使用棧的程序不再引用這些對象,它們也不會被回收。這是因為,棧內部維護這對這些對象的過期使用(obsolete reference),過期引用指永遠也不會被解除的引用。?
修復的方法很簡單:一旦對象引用已經過期,只需要清空這些引用即可。對于上述例子中的Stack類而言,只要一個單元彈出棧,指向它的引用就過期了,就可以將它清空。
修改方式如下:
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; //手工將數(shù)組中的該對象置空 return result; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Stock為什么會有內存泄漏問題呢??
問題在于,Stock類自己管理內存。存儲池中包含了elements數(shù)組(對象引用單元,而不是對象本身)的元素。數(shù)組活動區(qū)域的元素是已分配的,而數(shù)組其余部分的元素是自由的。但是垃圾回收器并不知道這一點,就需要手動清空這些數(shù)組元素。?
一般而言,只要類是自己管理內存,就應該警惕內存泄漏問題。一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。
由于現(xiàn)有的Java垃圾收集器已經足夠只能和強大,因此沒有必要對所有不在需要的對象執(zhí)行obj = null的顯示置空操作,這樣反而會給程序代碼的閱讀帶來不必要的麻煩,該條目只是推薦在以下3中情形下需要考慮資源手工處理問題:
第7條:避免使用終結方法
Java的語言規(guī)范中并沒有保證終結方法會被及時的執(zhí)行,甚至都沒有保證一定會被執(zhí)行。即便開發(fā)者在code中手工調用了System.gc和System.runFinalization這兩個方法,這僅僅是提高了finalizer被執(zhí)行的幾率而已。還有一點需要注意的是,被重載的finalize()方法中如果拋出異常,其棧幀軌跡是不會被打印出來的。
《Effective Java中文版 第2版》PDF版下載:?
http://download.csdn.net/detail/xunzaosiyecao/9745699
作者:jiankunking 出處:http://blog.csdn.net/jiankunking
from:?http://blog.csdn.net/jiankunking/article/details/54862672
總結
以上是生活随笔為你收集整理的Effective Java读书笔记三:创建和销毁对象的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Effective Java读书笔记二:
- 下一篇: java美元兑换,(Java实现) 美元