為什麼android API 中有很多對象的創建都是使用new關鍵字?

比起工廠方法、builder模式,java 中不提倡直接使用構造方法創建對象(new),為什麼android API 中還是有很多對象的創建都使用構造方法 ?


首先,謝邀。

其次,是怎麼找到我知乎賬號的,我隱藏的這麼深(臉紅了)

最後,加入了自己的總結概括,讓然也可以當成讀書筆記來看。

我會很認真,很認真地回答問題噠,畢竟這是第一次回答專業相關的提問 : )

最近在溫習《Effective Java》這本書,真的是每一次都有新的收穫和認識。從第二章《創建和銷毀對象》開始,就涉及了「靜態工廠方法」,「構造器創建對象」等概念,篇幅不長,但實用性極強,且概括性極強,可謂句句精闢。

那麼回到問題本身,其實在Java中,並不是不提倡直接使用構造函數來創建對象,而是在某些情況下,很難區分究竟調用哪個構造函數來初始化對象,或者說當函數簽名類似時,一不小心就使用了錯誤的構造函數,從而埋下難以發現的隱患,最後付出程序崩潰的代價,等等一系列「眼一花,手一滑」所導致的後果,或多或少給人們帶來「使用new關鍵字直接創建對象不靠譜」的錯覺,其實這種結論有些片面了,為什麼呢?因為所有的用例都有一個場景約束,一旦脫離適用場景,強制使用總是很牽強的。OK,讓我們來再來細緻的了解一下,或者說回顧一下。

考慮使用靜態工廠方法代理構造函數

假設你已經知道了這裡的「靜態工廠」與設計模式中的「工廠模式」是兩碼事。

靜態工廠方法可以有突出的名稱

我們不能通過給類的構造函數定義特殊的名稱來創建具備指定初始化功能的對象,也就是說我們必須通過參數列表來找到合適的構造函數,即便文檔健全但仍很煩人,而且一旦使用了錯誤的構造函數,假如編譯期不報錯,一旦運行時奔潰,那就說明我們已經離錯誤發生的地方很遠了,而且錯誤的對象已經被創建了,不過謝天謝地,它崩潰了,如果不崩潰,我們將更難找到問題所在。所以,這個時候我們就需要使用「靜態工廠方法」了,因為有突出的名稱,因此它很直觀,易讀,能夠幫助我們避免這種低級錯誤的發生。當然,它的適用場景是存在多個構造函數,如果你只有一個構造函數,且希望被繼承,則完全可以使用new來創建對象。

靜態工廠方法可以使用對象池,避免對象的重複創建

反正這也應該是細節隱藏的,因此我們可以在「靜態工廠方法」的背景下,在類的內部維護一個對象緩存池。這使得不可變類可以使用預先構件好的實例,或者將構建好的實例緩存起來,重複利用,從而避免創建不必要的對象。

可以像Boolean.valueOf(boolean)那樣,使用預先創建好的實例。

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

它從不創建新的對象,而且Boolean自身的不變性,因此能夠很好的使用預先創建好的實例。

或者像Parcel.obtain()那樣,在類的內部維護一個數組結構的緩存池:

private static final int POOL_SIZE = 6;
private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];

/**
* Retrieve a new Parcel object from the pool.
*/
public static Parcel obtain() {
final Parcel[] pool = sOwnedPool;
synchronized (pool) {
Parcel p;
for (int i=0; i&

也可以像Message.obtain()那樣,使用一個鏈表結構的緩存池:

private static Message sPool;

/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}

需要注意的是,為這些對象添加一個正確的回收邏輯。

在這些場景下,我們能夠輕鬆的控制究竟使用緩存實例,還是創建新的對象,或者設計成單例,它完全是可控的,屬於「實例受控類」的範疇。相反地,如果你在設計類的時候考慮到,既不需要緩存,也不可能成為單例,那麼你同樣可以,以直接new的方式來創建對象。

使用靜態工廠方法可以返回「原返回」類型的任何子類型

這樣,我們在選擇返回對象的類時就有了更大的靈活性。

這種靈活性的一種場景是,API可以返回對象,同時又不會使對象的所對應的類變成共有的。以這種方式隱藏實現類會使API變得非常簡潔。如Collections.unmodifiableList(list)

public static & List& unmodifiableList(List& list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList&<&>(list) :
new UnmodifiableList&<&>(list));
}

static class UnmodifiableRandomAccessList& extends UnmodifiableList&
implements RandomAccess{

UnmodifiableRandomAccessList(List& list) {
super(list);
}
...
}

static class UnmodifiableList& extends UnmodifiableCollection&
implements List& {

final List& list;

UnmodifiableList(List& list) {
super(list);
this.list = list;
}
...
}

就像描述中的一樣,由於訪問域的限制,我們「永遠」無法在Collections類的外部直接初始化UnmodifiableRandomAccessList或UnmodifiableList實例。

不過這也有個限制,我們只能通過介面"List"來引用被返回的對象,而不是通過它的實現類來引用,值得一提的是,通過介面或者抽象來引用被返回的對象,理應成為一種良好的習慣。

靜態工廠方法在創建參數化類型實例的時候,它們使代碼變得更加簡潔。

在調用參數化構造器時,即使類型參數很明顯,也必須指明。這通常需要連續兩次提供類型參數

Map&&> map = new HashMap&&>();

/*使用靜態工廠方法,編譯器會通過「類型推導」,找到正確的類型參數*/
Map&&> map1 = newInstance();
public static & HashMap& newInstance() {
return new HashMap&();
}

不過現在編譯器或者說IDE已經足夠智能,上面第一個例子完全允許寫成:

Map&&> map = new HashMap&<&>();

不必連續兩次提供類型參數。

上面提到的大都是使用「靜態工廠方法」相較於其他(創建對象方式)的優勢,那麼我們再來看看它有什麼限制。

靜態工廠方法,類如果不含共有的或者受保護的構造器,就不能子類化

因為子類需要在構造函數中隱式調用父類的無參構造函數或者顯式調用有參構造函數,這和把類修飾成final所表達的效果一致。而一旦類中存在公有構造函數,也就是說客戶端可直接通過構造函數創建對象,也就弱化了靜態工廠方法約束性。

靜態工廠方法,它和其他靜態方法實際上沒有任何區別

一旦考慮使用「靜態工廠方法」,就必須考慮簡單,直觀,完善的命名,這的確是個頭疼的事 : (

遇到多個構造器參數時考慮使用構建器

其實,靜態工廠方法和構造函數都有局限性:「他們都不能很好的擴展到大量的可選參數」。

在《Effective Java》舉了這樣一個經典的例子:

考慮用一個類表示包裝食品外面顯示的營養成分標籤。這些標籤中有幾個域是必需的:每份含量,每罐的含量以及每份的卡路里,還有超過20個可選域:總脂肪量、飽和脂肪量、轉化脂肪、膽固醇,鈉等等。

如果這種情況下依然堅持使用構造函數或者靜態工廠方法,那麼要編寫很多重疊構造函數,而且對於那麼多的可選域而言,這些重疊函數簡直就是噩夢!

避免代碼難寫,難看,難以閱讀,有兩種辦法可以解決。

JavaBeans模式

使用JavaBeans模式,把必需域作為構造函數的參數,可選域則通過setter方法注入。

我們都知道JavaBeans模式自身存在著嚴重的缺陷。因為構造過程可能被分到幾個調用中,在構造過程中JavaBean可能處於不一致狀態。類無法通過檢驗構造參數的有效性來保證一致性。而試圖使用處於不一致狀態的對象,將會導致失敗,這種失敗與包含錯誤代碼大相徑庭,因此調試起來十分困難。與此相關的另一點不足在於,JavaBeans模式阻止了了把類做成不可變的可能,這就需要程序員付出額外的努力來確保它的線程安全。

Builder模式

幸運地是,Builder模式既能保證像重疊模式那樣的安全性,也能保證JavaBeans模式那麼好的可讀性。而且也能夠對參數進行及時的校驗,一旦傳入無效參數或者違反約束條件就應該立即拋出IllegalStateException異常,而不是等著build的調用,從而創建錯誤的對象。

那麼我們真的需要把創建對象的方式更改為Builder嗎?

答案是,否定的。

我們可以在可選域多樣化的條件下,考慮使用這種模式,而且我們應該注意:不要過度設計API。

其實看完這些總結和經驗,我想你心裡一定有明確的答案了,那就讓我們再來一句總結:

如果你的類足夠簡單,那麼完全可以使用new來直接創建!切記過猶不及的API設計


Factory這種模式我一般在當構造函數參數過多或者需要參數來生產不同功能的組件時候使用,用來簡化對象創建和增加代碼可讀性。我比較常用的場景就是創建Fragment的時候,因為用Bundle傳參數是比較麻煩和不直觀的。還有就是創建一些功能複雜的自定義View。

Builder一般只有構建的對象功能複雜和需要高靈活性的情況下才會用。我只有在寫複雜組件的時候用過。

Android的API中許多類都簡單沒必要用上這兩個模式,像複雜和可選項多的AlertDialog就用了Builder來構建。需要的話自己封裝一些靜態工廠方法來構建就行了。


誰告訴你「Java中不提倡直接使用構造方法創建對象」?


JAVA這種喝口水還要先洗手的做法是要不得的,不要把這些壞習慣當真理


呵呵。

還有什麼關鍵字?


不要把簡單的事情搞複雜。


因為簡單啊。有什麼問題。。這個類拿來用還要想那麼多幹嘛,又沒那麼多要求。難道我寫個adapter還要寫成多高大上的東西嗎?而且迄今為止沒出過構造出錯的情況。


推薦閱讀:

Android 4.4 和 Android 7.0 的區別是什麼?
如何評價新版知乎 2.0 for Android?
一加手機為什麼比鎚子手機銷量高那麼多?
你為什麼用 Android 手機而非 iPhone?
有哪些適合日常使用的理財、記賬軟體?

TAG:Java | 設計模式 | Android |