Java程式設計細節之十個最佳實踐
本文講述的十個最佳實踐,這十個最佳實踐要比通常Josh Bloch Effective Java規範更加細緻。Josh Bloch的清單很容易學習,考慮的多是日常的情形,而本文則包括了不常見的情形例如API或SPI設計,儘管不常見,他們卻可能有著大的影響。
譯註:Java SPI (Service Provider Interface)是針對廠商或者外掛提供的介面,提供類似“Callback”的功能,實現對API的定製。關於SPI的詳細資訊可以參見java.util.ServiceLoader文件。
我在開發和維護JOOQ的過程中遇見過這類事情,JOOQ是一個用Java模擬SQL的 internal DSL(Domain Specific Language). 作為一個internal DSL, JOOQ挑戰了Java編譯器和泛型的極限,它結合泛型,可變引數和過載的方式恐怕是Josh Bloch在“average API”中不做推薦的。
讓我來告訴你這10個Java程式設計細節的最佳實踐。
1、記住C++的解構函式
要記住C++的解構函式(C++ destructors)?不想這樣做?那你得很幸運以至於從來不需要除錯那些由於分配記憶體而沒有在物件移除後釋放記憶體而導致記憶體洩露的程式碼。感謝Sun/Oracle實現了垃圾回收機制。
不過儘管如此,解構函式有一個有趣的特徵。通常按照分配的逆序來釋放記憶體是有道理的。在Java中也記著這一點,在你操作類似於解構函式的語義時
- JUnit 註釋中使用@Before和@After時
- 在分配和釋放JDBC資源時
- 在呼叫父類方法時
有多種例項。下面是一個具體的例子,它展示瞭如何實現事件監聽器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); }
另一個好的例子展示了為什麼它對於臭名昭著的哲學家就餐問題十分重要。更多關於哲學家就餐問題請參看這篇好文:http://adit.io/posts/2013-05-11-The-Dining-Philosophers-Problem-With-Ron-Swanson.html
規則:在任何你要使用before/after, allocate/free, take/return這樣的語義來實現邏輯時,想一想after/free/return操作是否應該以逆序的方式執行裡面的內容。
2、不要信任你早期SPI演化的結論
給客戶提供SPI是一個簡單的方式讓他們在庫或者程式碼中注入自定義的行為。但是請注意,你的SPI演化結論可能會欺騙你,讓你覺得你可能(不再)需要那個引數。是的,任何功能都不應過早的加入。但是一旦你釋出了你的SPI並且你決定遵循語義的版本控制,你就會後悔自己在SPI中加了一個愚蠢的,只有一個引數的方法,你意識到在某些情況下你還需要另一個引數:
interface EventListener { // Bad void message(String message); }
這裡如果你還需要一個message ID和一個message source怎麼辦?API演化可以阻止你輕易的給上面這個類新增引數。誠然,在java8中,你可以新增一個defender方法,來’defend’你早期的壞的設計決定。
interface EventListener { // Bad default void message(String message) { message(message, null, null); } // Better? void message( String message, Integer id, MessageSource source ); }
請注意,不幸的是defender方法不能定義為final。
不過這跟用一堆方法汙染你的api相比,已經很不錯了,在這裡可以使用一個上下文物件或者引數物件:
interface MessageContext { String message(); Integer id(); MessageSource source(); } interface EventListener { // Awesome! void message(MessageContext context); }
相對於EventListener SPI而言,你可以更容易地擴充套件MessageContext API,很少有人會實現它(指EventListener)。
規則:無論何時當你要指定一個SPI,考慮使用上下文物件或者引數物件,而不要寫有固定數量引數的方法。
備註:同時,通過特定的MessageRsult類來傳遞結果通常是一個好主意,這種類可以使用builder API來建立。這會給你的SPI增添更多的可擴充套件性。
3、避免返回匿名類,區域性類或者內部類
Swing開發者大概有很多快捷鍵來為他們成百上千個匿名類建立程式碼。在很多情況下,建立這些程式碼並不難因為你可以將他們依附於介面,而不用自找麻煩來思考整個SPI子類的生命週期。
但是你不應該太頻繁的使用匿名類,區域性類或內部類,原因很簡單,他們給每一個外部例項保留一個引用。他們無論走到哪裡都會帶著這個外部例項,例如如果你不注意,他們會帶到區域性類以外的範圍。這可能會成為一個主要的記憶體洩露點,因為你的整個物件圖譜將會突然以一種不被發覺的方式變得混亂起來。
規則:當你需要寫匿名類,區域性類,或者內部類時,看看能不能把他設為靜態甚至是常規的頂層類。避免從方法中向外層返回匿名類,區域性類和內部類。
備註:有很多明智的關於雙花括號給簡單物件例項化的實踐:
new HashMap<String, String>() {{ put("1", "a"); put("2", "b"); }}
這裡利用了JLS8.6中提到的Java例項初始化器。看起來不錯(或許有點怪),但實際上是非常差的主意。不然怎麼會有這個完全獨立的HashMap例項,這個為外層例項儲存一個引用的例項,只是碰巧也無所謂。而且,你還得建立額外的類給類的裝載器來管理。
4、開始寫SAMs吧(single abstract method)單個抽象方法
Java8已經在敲門了。無論你喜歡與否,Java8會帶來lambdas表示式。但你的API使用者可能會喜歡他們,你最好是確認他們可以儘量多的使用這些。因此,如果你的API不接收簡單的“標量”型別類如int, long, String, Date, 那麼就讓你的API儘量多地接收SAMs吧。
什麼是SAM? SAM是Single Abstract Method[Type], 即單一抽象方法,也稱作函式式介面(Functional Interface)。它即將使用@FunctionalInterface註解。這與規則2並無出入,因為EventListener實際上就是一個SAM. 最好的SAM應該只有一個引數,進而可以簡化為一個lambda表示式,比如我們這樣寫:
listeners.add(c -> System.out.println(c.message()));
而不是這樣寫
listeners.add(new EventListener() { @Override public void message(MessageContext c) { System.out.println(c.message())); } });
想象通過JOOX處理XML,這裡會有一些SAMs特色:
$(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)))
規則:好好對待你的使用者,現在就開始寫SAMs/Functional 介面吧。
備註:這裡有一些關於Java8 Lambda和改善的Collection 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、避免在API方法中返回null
我已經寫過幾次關於Java NULL的部落格了。我也寫過關於Java8中引入Optional的文章。這些有趣的話題都是從學術和實踐兩方面而來的。
雖然Nulls和NullPointerException在一段時期內還會是Java的一大頭痛難題,但你仍可以設計一種API使得你的使用者不會遇到任何問題。儘量試著避免在API方法中返回null。你的API使用者應該無論合適都可以串聯方法:
initialise(someArgument).calculate(data).dispatch();
在上面這段程式碼中,任何方法都不應該返回null。實際上,總的來講,使用null這種語法應當是例外情況。在像JQuery(或者JOOX,Java中的一個埠),null是被完全避免的。因為你總是在操作可迭代的物件。無論你有匹配的內容與否,都與下一個方法的呼叫無關。
Null通常在延遲初始化時出現。很多情況下,延遲初始化是可以避免的,並且對效能的影響不大。事實上,延遲初始化只應該被很小心的使用,特別是在有大型資料結構參與的時候。
規則:儘可能地避免方法返回nulls。只在非初始化或者預設的情況下使用null。
6、API方法永遠不要返回null陣列或null連結串列
其實在一些情形下方法返回null值是可以的,但是任何情況想都絕不要返回null陣列或者null collection!我們來看一個可怕的java.io.File.list()方法。它返回:
字串陣列,這些字串制定此抽象路徑名錶示的目錄中的檔案和目錄。如果目錄為空,則陣列為空。如果這個抽象路徑並不表示一個目錄,或者發生I/O錯誤,則返回null。
因此,對待這個方法正確方式是:
File directory = // ... if (directory.isDirectory()) { String[] list = directory.list(); if (list != null) { for (String file : list) { // ... } } }
- Null無助於找到錯誤。
-
Null並不能區分是I/
<wbr>O錯誤還是檔案的例項不能表示一個目錄。 - 在這裡,任何人都記不住這個null。
規則:陣列或集合永遠不應該是null。
7、避免使用狀態(state),使用函數語言程式設計(functional)
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。
- 在多執行緒環境下很難正確處理有狀態的API。
- 很難使得有狀態的資源全域性可用,因為狀態沒有被備份。
人們相信上面的使用滿足公平使用原則。
規則:實現更多功能風格的東西。通過方法引數傳遞狀態。
這是一個簡單易用的東西。在大型物件圖表中,
@Override public boolean equals(Object other) { if (this == other) return true; // Rest of equality logic... }
注意,其它捷徑檢查包括檢查null值,他也應該在那裡:
@Override public boolean equals(Object other) { if (this == other) return true; if (other == null) return false; // Rest of equality logic... }
規則:為所有的equals()尋找捷徑,從而提高效能。
有些人並不同意這一點,
-
如果你需要重寫一個方法(真的需要麼?),
<wbr>你仍可以刪掉final關鍵詞。 - 你永遠不會意外地重寫某個方法。
TikaInputStream 繼承TaggedInputStream並且以一種不同的實現方
與正常的方法不同,靜態方法不能相互重寫,
規則:如果你對你的API有完全的控制,
10、避免 method(T…)
偶爾使用”accept-all”可變引數方法並沒什麼錯,
void acceptAll(Object... all);
寫這樣的方法給Java生態系統帶來了一絲JavaScript
void acceptAll(T... all);
但這並不是一個好主意。T可能總會被認為是一個Object。
void acceptAll(T... all); void acceptAll(String message, T... all);
雖然這個看起來像是你也可以傳遞一個String給這個方法。但是如果執行這行會發生什麼?
編譯器會把<?extends Serializable & Comparable<?>>當做T,這樣以來這個呼叫就有了歧義。
因此,無論何時你使用“accept-all”(即使它是泛型),你永遠不能在保證類安全的前提下過載它。API使用者可能會很幸運的讓編譯器恰恰偶然選擇了那個正確的方法。但是他們也可能被騙而去使用”accept-all”方法,或者他們壓根不能呼叫任何方法。
規則:如果可以,避免“accept-all”特性。如果不可以,永遠不要過載這樣的方法。
總結
Java是一個野獸,與其他花哨的語言不同,
原文連結: dzone 翻譯: ImportNew.com - 湯米貓
相關文章
- Java程式設計師的八個最佳實踐Java程式設計師
- 資料庫設計的十個最佳實踐資料庫
- 使用GitHub的十個最佳實踐Github
- Python程式設計規範+最佳實踐Python程式設計
- Laravel最佳實踐–事件驅動程式設計Laravel事件程式設計
- 函數語言程式設計最佳實踐函數程式設計
- Laravel 最佳實踐 -- 事件驅動程式設計Laravel事件程式設計
- Laravel最佳實踐 -- 事件驅動程式設計Laravel事件程式設計
- 13 個設計 REST API 的最佳實踐RESTAPI
- Java併發程式設計實踐Java程式設計
- Spring Boot中五個設計模式最佳實踐Spring Boot設計模式
- MaxCompute表設計最佳實踐
- [01] C#網路程式設計的最佳實踐C#程式設計
- 領域驅動設計最佳實踐--程式碼篇
- TypeScript 資料模型層程式設計的最佳實踐TypeScript模型程式設計
- Java最佳實踐Java
- Java併發程式設計 - 第十一章 Java併發程式設計實踐Java程式設計
- Java併發程式設計實踐-this溢位Java程式設計
- JAVA併發程式設計實踐 下載Java程式設計
- react 設計模式與最佳實踐React設計模式
- 設計微服務的最佳實踐微服務
- Apache Airflow十條最佳實踐ApacheAI
- Github 十大最佳實踐Github
- dart系列之:dart程式碼最佳實踐Dart
- Java程式設計細節-重構-為什麼 if-else 不是好程式碼Java程式設計
- [分享]2021 年對 React 前端程式設計師的 10 個程式碼最佳實踐建議React前端程式設計師
- 這些Java程式碼最佳化細節,你需要注意!Java
- 【譯】Celeste 手感的 10 個設計細節
- Java null最佳實踐JavaNull
- 面向介面程式設計實踐之aspnetcoreapi的抽象程式設計NetCoreAPI抽象
- [04] C# Alloc Free程式設計之實踐C#程式設計
- 好程式設計師Java分享Javamain十個面試題程式設計師JavaAI面試題
- vSAN 設計、部署、運維最佳實踐運維
- Go程式設計實踐Go程式設計
- Java設計模式之(十)——組合模式Java設計模式
- 資料庫設計中的6個最佳實踐步驟資料庫
- Java的API設計實踐JavaAPI
- Mybatis之介面程式設計--JAVA動態代理的最佳展現MyBatis程式設計Java
- 處理Java異常的10個最佳實踐Java