您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“十個精妙的Java編碼是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!
1. 牢記C++的析構(gòu)函數(shù)
記得C++的析構(gòu)函數(shù)?不記得了?那么你真的很幸運,因為你不必去調(diào)試那些由于對象刪除后分配的內(nèi)存沒有被釋放而導(dǎo)致內(nèi)存泄露的代碼。感謝Sun/Oracle實現(xiàn)的垃圾回收機制吧!
盡管如此,析構(gòu)函數(shù)仍提供了一個有趣的特征。它理解逆分配順序釋放內(nèi)存。記住在Java中也是這樣的,當(dāng)你操作類析構(gòu)函數(shù)語法:
使用JUnit的@Before和@After注釋
分配,釋放JDBC資源
調(diào)用super方法
還有其他各種用例。這里有一個具體的例子,說明如何實現(xiàn)一些事件偵聽器的SPI:
@Override
public void beforeEvent(EventContext e) {
super.beforeEvent(e);
// Super code before my code
}
@Override
public void afterEvent(EventContext e) {
// Super code after my code
super.afterEvent(e);
}
臭名昭著的哲學(xué)家就餐問題是另一個說明它為什么重要的好例子。 關(guān)于哲學(xué)家用餐的問題,請查看鏈接:
http://adit.io/posts/2013-05-11-The-Dining-Philosophers-Problem-With-Ron-Swanson.html
規(guī)則:無論何時使用before/after, allocate/free, take/return語義實現(xiàn)邏輯時,考慮是否逆序執(zhí)行after/free/return操作。
2. 不要相信你早期的SPI演進判斷
向客戶提供SPI可以使他們輕松的向你的庫/代碼中注入自定義行為的方法。當(dāng)心你的SPI演進判斷可能會迷惑你,使你認為你 (不)打算需要附加參數(shù)。 當(dāng)然,不應(yīng)當(dāng)過早增加功能。但一旦你發(fā)布了你的SPI,一旦你決定遵循語義版本控制,當(dāng)你意識到在某種情況下你可能需要另外一個參數(shù)時,你會真的后悔在SPI中增加一個愚蠢的單參數(shù)的方法:
interface EventListener {
// Bad
void message(String message);
}
如果你也需要消息ID和消息源,怎么辦?API演進將會阻止你向上面的類型添加參數(shù)。當(dāng)然,有了Java8,你可以添加一個defender方法,“防御”你早期糟糕的設(shè)計決策:
interface EventListener {
// Bad
default void message(String message) {
message(message, null, null);
}
// Better?
void message(
String message,
Integer id,
MessageSource source
);
}
注意,不幸的是,defender方法不能使用final修飾符。
但是比起使用許多方法污染你的SPI,使用上下文對象(或者參數(shù)對象)會好很多。
interface MessageContext {
String message();
Integer id();
MessageSource source();
}
interface EventListener {
// Awesome!
void message(MessageContext context);
}
比起EventListner SPI你可以更容易演進MessageContext API,因為很少用戶會實現(xiàn)它。
規(guī)則: 無論何時指定SPI時,考慮使用上下文/參數(shù)對象,而不是寫帶有固定參數(shù)的方法。
備注: 通過專用的MessageResult類型交換結(jié)果也是一個好主意,該類型可以使用建設(shè)者API構(gòu)造它。這樣將大大增加SPI進化的靈活性。
3. 避免返回匿名,本地或者內(nèi)部類
Swing程序員通 常只要按幾下快捷鍵即可生成成百上千的匿名類。在多數(shù)情況下,只要遵循接口、不違反SPI子類型的生命周期(SPI subtype lifecycle),這樣做也無妨。 但是不要因為一個簡單的原因——它們會保存對外部類的引用,就頻繁的使用匿名、局部或者內(nèi)部類。因為無論它們走到哪,外部類就得跟到哪。例如,在局部類的 域外操作不當(dāng)?shù)脑?,那么整個對象圖就會發(fā)生微妙的變化從而可能引起內(nèi)存泄露。
規(guī)則:在編寫匿名、局部或內(nèi)部類前請三思能否將它轉(zhuǎn)化為靜態(tài)的或普通的***類,從而避免方法將它們的對象返回到更外層的域中。
注意:使用雙層花括號來初始化簡單對象:
new HashMap<String, String>() {{
put("1", "a");
put("2", "b");
}}
這個方法利用了 JLS §8.6規(guī)范里描述的實例初始化方法(initializer)。表面上看起來不錯,但實際上不提倡這種做法。因為要是使用完全獨立的HashMap對象,那么實例就不會一直保存著外部對象的引用。此外,這也會讓類加載器管理更多的類。
4. 現(xiàn)在就開始編寫SAM!
Java8的腳步近了。伴隨著Java8帶來了lambda表達式,無論你是否喜歡。盡管你的API用戶可能會喜歡,但是你***確保他們可以盡可能 經(jīng)常的使用。因此除非你的API接收簡單的“標量”類型,比如int、long、String 、Date,否則讓你的API盡可能經(jīng)常的接收SAM。
什么是SAM?SAM是單一抽象方法[類型]。也稱為函數(shù)接口,不久會被注釋為@FunctionalInterface。這與規(guī)則2很 配,EventListener實際上就是一個SAM。***的SAM只有一個參數(shù),因為這將會進一步簡化lambda表達式的編寫。設(shè)想編寫
listeners.add(c -> System.out.println(c.message()));
來替代
listeners.add(new EventListener() {
@Override
public void message(MessageContext c) {
System.out.println(c.message()));
}
});
設(shè)想以JOOX的方式來處理XML。JOOX就包含很多的SAM:
$(document)
// Find elements with an ID
.find(c -> $(c).id() != null)
// Find their child elements
.children(c -> $(c).tag().equals("order"))
// Print all matches
.each(c -> System.out.println($(c)))
規(guī)則:對你的API用戶好一點兒,從現(xiàn)在開始編寫SAM/函數(shù)接口。
備注:有許多關(guān)于Java8 lambda表達式和改善的Collections API的有趣的博客:
http://blog.informatech.cr/2013/04/10/java-optional-objects/
http://blog.informatech.cr/2013/03/25/java-streams-api-preview/
http://blog.informatech.cr/2013/03/24/java-streams-preview-vs-net-linq/
http://blog.informatech.cr/2013/03/11/java-infinite-streams/
5.避免讓方法返回null
我曾寫過1、2篇關(guān)于java NULLs的文章,也講解過Java8中引入新的Optional類。從學(xué)術(shù)或?qū)嵱玫慕嵌葋砜?,這些話題還是比較有趣的。
盡管現(xiàn)階段Null和NullPointerException依然是Java的硬傷,但是你仍可以設(shè)計出不會出現(xiàn)任何問題的API。在設(shè)計API時,應(yīng)當(dāng)盡可能的避免讓方法返回null,因為你的用戶可能會鏈式調(diào)用方法:
initialise(someArgument).calculate(data).dispatch();
從上面代碼中可看出,任何一個方法都不應(yīng)返回null。實際上,在通常情況下使用null會被認為相當(dāng)?shù)漠愵?。?jQuery或 jOOX這樣的庫在可迭代的對象上已完全的摒棄了null。
Null通常用在延遲初始化中。在許多情況下,在不嚴重影響性能的條件下,延遲初始化也應(yīng)該被避免。實際上,如果涉及的數(shù)據(jù)結(jié)構(gòu)過于龐大,那么就要慎用延遲初始化。
規(guī)則:無論何時方法都應(yīng)避免返回null。null僅用來表示“未初始化”或“不存在”的語義。
6.設(shè)計API時永遠不要返回空(null)數(shù)組或List
盡管在一些情況下方法返回值為null是可以的,但是絕不要返回空數(shù)組或空集合!請看 java.io.File.list()方法,它是這樣設(shè)計的:
此方法會返回一個指定目錄下所有文件或目錄的字符串?dāng)?shù)組。如果目錄為空(empty)那么返回的數(shù)組也為空(empty)。如果指定的路徑不存在或發(fā)生I/O錯誤,則返回null。
因此,這個方法通常要這樣使用:
File directory = // ...
if (directory.isDirectory()) {
String[] list = directory.list();
if (list != null) {
for (String file : list) {
// ...
}
}
}
大家覺得null檢查有必要嗎?大多數(shù)I/O操作會產(chǎn)生IOExceptions,但這個方法卻只返回了null。Null是無法存放I/O錯誤信息的。因此這樣的設(shè)計,有以下3方面的不足:
Null無助于發(fā)現(xiàn)錯誤
Null無法表明I/O錯誤是由File實例所對應(yīng)的路徑不正確引起的
每個人都可能會忘記判斷null情況
以集合的思維來看待問題的話,那么空的(empty)的數(shù)組或集合就是對“不存在”的***實現(xiàn)。返回空(null)數(shù)組或集合幾乎是無任何實際意義的,除非用于延遲初始化。
規(guī)則:返回的數(shù)組或集合不應(yīng)為null。
7. 避免狀態(tài),使用函數(shù)
HTTP的好處是無狀態(tài)。所有相關(guān)的狀態(tài)在每次請求和響應(yīng)中轉(zhuǎn)移。這是REST命名的本質(zhì):含狀態(tài)傳輸(Representational state transfer)。在Java中這樣做也很贊。當(dāng)方法接收狀態(tài)參數(shù)對象的時候從規(guī)則2的角度想想這件事。如果狀態(tài)通過這種對象傳輸,而不是從外邊操作狀 態(tài),那么事情將會更簡單。以JDBC為例。下述例子從一個存儲的程序中讀取一個光標。
CallableStatement s =
connection.prepareCall("{ ? = ... }");
// Verbose manipulation of statement state:
s.registerOutParameter(1, cursor);
s.setString(2, "abc");
s.execute();
ResultSet rs = s.getObject(1);
// Verbose manipulation of result set state:
rs.next();
rs.next();
這使得JDBC API如此的古怪。每個對象都是有狀態(tài)的,難以操作。具體的說,有兩個主要的問題:
在多線程環(huán)境很難正確的處理有狀態(tài)的API
很難讓有狀態(tài)的資源全局可用,因為狀態(tài)沒有被描述
規(guī)則:更多的以函數(shù)風(fēng)格實現(xiàn)。通過方法參數(shù)轉(zhuǎn)移狀態(tài)。極少操作對象狀態(tài)。
8. 短路式 equals()
這是一個比較容易操作的方法。在比較復(fù)雜的對象系統(tǒng)中,你可以獲得顯著的性能提升,只要你在所有對象的equals()方法中首先進行相等判斷:
@Override
public boolean equals(Object other) {
if (this == other) return true;
// 其它相等判斷邏輯...
}
注意,其它短路式檢查可能涉及到null值檢查,所以也應(yīng)當(dāng)加進去:
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null) return false;
// Rest of equality logic...
}
規(guī)則: 在你所有的equals()方法中使用短路來提升性能。
9. 盡量使方法默認為final
有些人可能不同意這一條,因為使方法默認為final與Java開發(fā)者的習(xí)慣相違背。但是如果你對代碼有完全的掌控,那么使方法默認為final是肯定沒錯的:
如果你確實需要覆蓋(override)一個方法(你真的需要?),你仍然可以移除final關(guān)鍵字
你將永遠不會意外地覆蓋(override)任何方法
這特別適用于靜態(tài)方法,在這種情況下“覆蓋”(實際上是遮蔽)幾乎不起作用。我最近在Apache Tika中遇到了一個很糟糕的遮蔽靜態(tài)方法的例子。看一下:
TaggedInputStream.get(InputStream)
TikaInputStream.get(InputStream)
TikaInputStream擴展了TaggedInputStream,以一種相對不同的實現(xiàn)遮蔽了它的靜態(tài)get()方法。
與常規(guī)方法不同,靜態(tài)方法不能互相覆蓋,因為調(diào)用的地方在編譯時就綁定了靜態(tài)方法調(diào)用。如果你不走運,你可能會意外獲得錯誤的方法。
規(guī)則:如果你完全掌控你的API,那么使盡可能多的方法默認為final。
10. 避免方法(T…)簽名
在特殊場合下使用“accept-all”變量參數(shù)方法接收一個Object…參數(shù)就沒有錯的:
void acceptAll(Object... all);
編寫這樣的方法為Java生態(tài)系統(tǒng)帶來一點兒JavaScript的感覺。當(dāng)然你可能想要根據(jù)真實的情形限制實際的類型,比如String…。因為你不想要限制太多,你可能會認為用泛型T取代Object是一個好想法:
void acceptAll(T... all);
但是不是。T總是會被推斷為Object。實際上你可能僅僅認為上述方法中不能使用泛型。更重要的是你可能認為你可以重載上述方法,但是你不能:
void acceptAll(T... all);
void acceptAll(String message, T... all);
這看起來好像你可以可選地傳遞一個String消息到方法。但是這個調(diào)用會發(fā)生什么呢?
acceptAll("Message", 123, "abc");
編譯器將T推斷為<? extends Serializable & Comparable<?>>,這將會使調(diào)用不明確!
所以無論何時你有一個“accept-all”簽名(即使是泛型),你將永遠不能類型安全地重載它。API使用者可能僅僅在走運的時候才會讓編譯器“偶然地”選擇“正確的”方法。但是也可能使用accept-all方法或者無法調(diào)用任何方法。
規(guī)則: 如果可能,避免“accept-all”簽名。如果不能,不要重載這樣的方法。
“十個精妙的Java編碼是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。