為什么android API 中有很多對象的創(chuàng)建都是使用new關鍵字?
比起工廠方法、builder模式,java 中不提倡直接使用構造方法創(chuàng)建對象(new),為什么android API 中還是有很多對象的創(chuàng)建都使用構造方法 ?
這只是個草稿
首先,謝邀。
其次,是怎么找到我知乎賬號的,我隱藏的這么深(臉紅了)
最后,加入了自己的總結概括,讓然也可以當成讀書筆記來看。
我會很認真,很認真地回答問題噠,畢竟這是第一次回答專業(yè)相關的提問 : )
最近在溫習《Effective Java》這本書,真的是每一次都有新的收獲和認識。從第二章《創(chuàng)建和銷毀對象》開始,就涉及了“靜態(tài)工廠方法”,“構造器創(chuàng)建對象”等概念,篇幅不長,但實用性極強,且概括性極強,可謂句句精辟。
那么回到問題本身,其實在Java中,并不是不提倡直接使用構造函數來創(chuàng)建對象,而是在某些情況下,很難區(qū)分究竟調用哪個構造函數來初始化對象,或者說當函數簽名類似時,一不小心就使用了錯誤的構造函數,從而埋下難以發(fā)現的隱患,最后付出程序崩潰的代價,等等一系列“眼一花,手一滑”所導致的后果,或多或少給人們帶來“使用new關鍵字直接創(chuàng)建對象不靠譜”的錯覺,其實這種結論有些片面了,為什么呢?因為所有的用例都有一個場景約束,一旦脫離適用場景,強制使用總是很牽強的。OK,讓我們來再來細致的了解一下,或者說回顧一下。
考慮使用靜態(tài)工廠方法代理構造函數
假設你已經知道了這里的“靜態(tài)工廠”與設計模式中的“工廠模式”是兩碼事。
靜態(tài)工廠方法可以有突出的名稱
我們不能通過給類的構造函數定義特殊的名稱來創(chuàng)建具備指定初始化功能的對象,也就是說我們必須通過參數列表來找到合適的構造函數,即便文檔健全但仍很煩人,而且一旦使用了錯誤的構造函數,假如編譯期不報錯,一旦運行時奔潰,那就說明我們已經離錯誤發(fā)生的地方很遠了,而且錯誤的對象已經被創(chuàng)建了,不過謝天謝地,它崩潰了,如果不崩潰,我們將更難找到問題所在。所以,這個時候我們就需要使用“靜態(tài)工廠方法”了,因為有突出的名稱,因此它很直觀,易讀,能夠幫助我們避免這種低級錯誤的發(fā)生。當然,它的適用場景是存在多個構造函數,如果你只有一個構造函數,且希望被繼承,則完全可以使用new來創(chuàng)建對象。
靜態(tài)工廠方法可以使用對象池,避免對象的重復創(chuàng)建
反正這也應該是細節(jié)隱藏的,因此我們可以在“靜態(tài)工廠方法”的背景下,在類的內部維護一個對象緩存池。這使得不可變類可以使用預先構件好的實例,或者將構建好的實例緩存起來,重復利用,從而避免創(chuàng)建不必要的對象。
可以像Boolean.valueOf(boolean)
那樣,使用預先創(chuàng)建好的實例。
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);
}
它從不創(chuàng)建新的對象,而且Boolean
自身的不變性,因此能夠很好的使用預先創(chuàng)建好的實例。
或者像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<POOL_SIZE; i++) {
p = pool[i];
if (p != null) {
pool[i] = null;
if (DEBUG_RECYCLE) {
p.mStack = new RuntimeException();
}
return p;
}
}
}
return new Parcel(0);
}
也可以像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();
}
需要注意的是,為這些對象添加一個正確的回收邏輯。
在這些場景下,我們能夠輕松的控制究竟使用緩存實例,還是創(chuàng)建新的對象,或者設計成單例,它完全是可控的,屬于“實例受控類”的范疇。相反地,如果你在設計類的時候考慮到,既不需要緩存,也不可能成為單例,那么你同樣可以,以直接new的方式來創(chuàng)建對象。
使用靜態(tài)工廠方法可以返回“原返回”類型的任何子類型
這樣,我們在選擇返回對象的類時就有了更大的靈活性。
這種靈活性的一種場景是,API可以返回對象,同時又不會使對象的所對應的類變成共有的。以這種方式隱藏實現類會使API變得非常簡潔。如Collections.unmodifiableList(list)
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
static class UnmodifiableRandomAccessList<E> extends UnmodifiableList<E>
implements RandomAccess{
UnmodifiableRandomAccessList(List<? extends E> list) {
super(list);
}
...
}
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list;
}
...
}
就像描述中的一樣,由于訪問域的限制,我們“永遠”無法在Collections
類的外部直接初始化UnmodifiableRandomAccessList
或UnmodifiableList
實例。
不過這也有個限制,我們只能通過接口"List"來引用被返回的對象,而不是通過它的實現類來引用,值得一提的是,通過接口或者抽象來引用被返回的對象,理應成為一種良好的習慣。
靜態(tài)工廠方法在創(chuàng)建參數化類型實例的時候,它們使代碼變得更加簡潔。
在調用參數化構造器時,即使類型參數很明顯,也必須指明。這通常需要連續(xù)兩次提供類型參數
Map<String, List<String>> map = new HashMap<String, List<String>>();
/*使用靜態(tài)工廠方法,編譯器會通過“類型推導”,找到正確的類型參數*/
Map<String, List<String>> map1 = newInstance();
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
不過現在編譯器或者說IDE已經足夠智能,上面第一個例子完全允許寫成:
Map<String, List<String>> map = new HashMap<>();
不必連續(xù)兩次提供類型參數。
上面提到的大都是使用“靜態(tài)工廠方法”相較于其他(創(chuàng)建對象方式)的優(yōu)勢,那么我們再來看看它有什么限制。
靜態(tài)工廠方法,類如果不含共有的或者受保護的構造器,就不能子類化
因為子類需要在構造函數中隱式調用父類的無參構造函數或者顯式調用有參構造函數,這和把類修飾成final
所表達的效果一致。而一旦類中存在公有構造函數,也就是說客戶端可直接通過構造函數創(chuàng)建對象,也就弱化了靜態(tài)工廠方法約束性。
靜態(tài)工廠方法,它和其他靜態(tài)方法實際上沒有任何區(qū)別
一旦考慮使用“靜態(tài)工廠方法”,就必須考慮簡單,直觀,完善的命名,這的確是個頭疼的事 : (
遇到多個構造器參數時考慮使用構建器
其實,靜態(tài)工廠方法和構造函數都有局限性:“他們都不能很好的擴展到大量的可選參數”。
在《Effective Java》舉了這樣一個經典的例子:
考慮用一個類表示包裝食品外面顯示的營養(yǎng)成分標簽。這些標簽中有幾個域是必需的:每份含量,每罐的含量以及每份的卡路里,還有超過20個可選域:總脂肪量、飽和脂肪量、轉化脂肪、膽固醇,鈉等等。
如果這種情況下依然堅持使用構造函數或者靜態(tài)工廠方法,那么要編寫很多重疊構造函數,而且對于那么多的可選域而言,這些重疊函數簡直就是噩夢!
避免代碼難寫,難看,難以閱讀,有兩種辦法可以解決。
JavaBeans模式
使用JavaBeans模式,把必需域作為構造函數的參數,可選域則通過setter
方法注入。
我們都知道JavaBeans模式自身存在著嚴重的缺陷。因為構造過程可能被分到幾個調用中,在構造過程中JavaBean可能處于不一致狀態(tài)。類無法通過檢驗構造參數的有效性來保證一致性。而試圖使用處于不一致狀態(tài)的對象,將會導致失敗,這種失敗與包含錯誤代碼大相徑庭,因此調試起來十分困難。與此相關的另一點不足在于,JavaBeans模式阻止了了把類做成不可變的可能,這就需要程序員付出額外的努力來確保它的線程安全。
Builder模式
幸運地是,Builder模式既能保證像重疊模式那樣的安全性,也能保證JavaBeans模式那么好的可讀性。而且也能夠對參數進行及時的校驗,一旦傳入無效參數或者違反約束條件就應該立即拋出IllegalStateException
異常,而不是等著build
的調用,從而創(chuàng)建錯誤的對象。
那么我們真的需要把創(chuàng)建對象的方式更改為Builder嗎?
答案是,否定的。
我們可以在可選域多樣化的條件下,考慮使用這種模式,而且我們應該注意:不要過度設計API。
其實看完這些總結和經驗,我想你心里一定有明確的答案了,那就讓我們再來一句總結:
如果你的類足夠簡單,那么完全可以使用new來直接創(chuàng)建!切記過猶不及的API設計