编码Java时的10个微妙的最佳实践
這是10個最佳實踐的列表,這些最佳實踐比您的平均Josh Bloch有效Java規則要微妙得多。 盡管Josh Bloch的列表很容易學習,并且涉及日常情況,但此處的列表包含了涉及API / SPI設計的較不常見的情況,但可能會產生很大的影響。
我在編寫和維護jOOQ時遇到了這些問題, jOOQ是Java中的內部DSL建模SQL。 作為內部DSL,jOOQ最大限度地挑戰了Java編譯器和泛型, 將泛型,可變參數和重載組合在一起,這是Josh Bloch可能不推薦使用的“平均API”。
讓我與您分享編碼Java時的10個微妙的最佳實踐:
1.記住C ++析構函數
還記得C ++析構函數嗎? 沒有? 然后,您可能會很幸運,因為您無需再調試任何代碼,因為刪除對象后沒有釋放分配的內存,因此不會留下內存泄漏。 感謝Sun / Oracle實現垃圾回收!
但是,盡管如此,破壞者還是有一個有趣的特征。 通常以相反的順序釋放內存是有意義的。 在使用類似析構函數的語義進行操作時,也要在Java中記住這一點:
- 當使用@Before和@After JUnit批注時
- 分配時,釋放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 codesuper.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 {// Badvoid message(String message); }如果還需要消息ID和消息源怎么辦? API的發展將阻止您輕松地將該參數添加到上述類型。 使用Java 8,您可以添加防御者方法來“捍衛”您糟糕的早期設計決策:
interface EventListener {// Baddefault void message(String message) {message(message, null, null);}// Better?void message(String message,Integer id,MessageSource source); }請注意,不幸的是,防御者方法不能設為final 。
但是,比使用數十種方法污染SPI更好的方法是,僅為此目的使用上下文對象(或參數對象) 。
interface MessageContext {String message();Integer id();MessageSource source(); }interface EventListener {// Awesome!void message(MessageContext context); }與EventListener SPI相比,您可以更輕松地開發MessageContext API,因為實施該應用程序的用戶將更少。
規則 :無論何時指定SPI,都應考慮使用上下文/參數對象,而不要編寫帶有固定數量參數的方法。
備注 :通常也可以通過專用的MessageResult類型(可以通過構建器API構造)來傳遞結果,這是一個好主意。 這將為您的SPI增加更多的SPI演進靈活性。
3.避免返回匿名,本地或內部類
Swing程序員可能有幾個鍵盤快捷鍵可以為其數百個匿名類生成代碼。 在許多情況下,創建它們很不錯,因為您可以本地遵守接口,而無需經歷思考完整SPI子類型生命周期的“麻煩”。
但是,您不應該過于頻繁地使用匿名類,局部類或內部類,原因很簡單:它們保留對外部實例的引用。 并且,如果您不小心,它們會將外部實例拖到任何地方,例如,拖到本地類之外的某個范圍。 這可能是內存泄漏的主要來源,因為整個對象圖會突然以微妙的方式糾纏在一起。
規則 :每當編寫匿名,本地或內部類時,請檢查是否可以使其成為靜態類,甚至是常規頂級類。 避免將匿名,本地或內部類實例從方法返回到外部作用域。
備注 :對于簡單對象實例化,圍繞雙花括號有一些聰明的做法:
new HashMap<String, String>() {{put("1", "a");put("2", "b"); }}這利用了JLS§8.6中指定的 Java實例初始化程序 。 看起來不錯(也許有點奇怪),但確實是個壞主意。 原來是完全獨立的HashMap實例現在將保留對外部實例的引用,無論發生什么情況。 此外,您將創建一個其他類供類加載器管理。
4.立即開始編寫SAM!
Java 8正在敲門。 隨Java 8一起提供lambda ,無論您是否喜歡。 不過,您的API使用者可能會喜歡它們,因此您最好確保他們可以盡可能多地使用它們。 因此,除非您的API接受簡單的“標量”類型(例如int , long , String , Date ,否則您的API應盡可能多地接受SAM。
什么是SAM? SAM是單一抽象方法[Type]。 也稱為功能接口 ,很快將使用@FunctionalInterface注釋進行注釋 。 這與規則2配合得很好,其中EventListener實際上是SAM。 最好的SAM是具有單個參數的SAM,因為它們將進一步簡化lambda的編寫。 想象寫作
listeners.add(c -> System.out.println(c.message()));代替
listeners.add(new EventListener() {@Overridepublic void message(MessageContext c) {System.out.println(c.message()));} });想象一下通過jOOX進行的 XML處理,它具有幾個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)))規則 :與您的API使用者友好, 現在已經編寫SAM /功能接口。
備注 :有關Java 8 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.避免從API方法返回null
我曾經寫過一兩次關于Java的NULL的博客。 我也寫了關于Java 8對Optional的介紹的博客。 從學術和實踐的角度來看,這些都是有趣的話題。
盡管NULL和NullPointerExceptions在Java中可能會持續一段時間,但是您仍然可以通過設計API來避免用戶遇到任何問題。 盡可能避免從API方法返回null。 您的API使用者應能夠在適用的情況下鏈接方法:
initialise(someArgument).calculate(data).dispatch();在上面的代碼段中,所有方法都不應該返回null。 實際上,通常使用null的語義(缺少值)應該是非常例外的。 在諸如jQuery (或jOOX ,其Java端口)之類的庫中,由于始終對可迭代對象進行操作 ,因此完全避免了null。 是否匹配某項與下一個方法調用無關。
由于延遲初始化,通常還會出現空值。 在許多情況下,也可以避免延遲初始化,而不會對性能產生重大影響。 實際上,僅應謹慎使用惰性初始化。 如果涉及大型數據結構。
規則 :盡可能避免從方法返回null。 僅將空值用于“未初始化”或“不存在”的語義。
6.切勿從API方法返回空數組或列表
雖然在某些情況下從方法返回null可以,但是絕對沒有用過返回null數組或null集合的用例! 讓我們考慮一下丑陋的java.io.File.list()方法。 它返回:
在此抽象路徑名表示的目錄中命名文件和目錄的字符串數組。 如果目錄為空,則數組為空。 如果此抽象路徑名不表示目錄,或者發生I / O錯誤,則返回null。
因此,處理此方法的正確方法是
File directory = // ...if (directory.isDirectory()) {String[] list = directory.list();if (list != null) {for (String file : list) {// ...}} }空檢查真的必要嗎? 大多數I / O操作都會產生IOException,但是此操作返回null。 Null無法保存任何指示為什么發生I / O錯誤的錯誤消息。 因此,這在三種方式上是錯誤的:
- 空無助于發現錯誤
- Null不允許將I / O錯誤與不是目錄的File實例區分開
- 每個人都會忘記空值
在集合上下文中,“空缺”的概念最好通過空數組或集合來實現。 除了再一次進行延遲初始化外,幾乎沒有有用的數組或集合。
規則 :數組或集合絕不能為空。
7.避免狀態,發揮作用
HTTP的優點在于它是無狀態的。 所有相關狀態都在每個請求和每個響應中傳遞。 這對于REST的命名至關重要: 代表性狀態轉移 。 當用Java完成時,這也很棒。 當方法接收有狀態參數對象時,可以根據規則2來考慮它。 如果狀態在此類對象中傳遞,而不是從外部進行操縱,則事情會變得更加簡單。 以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。 每個對象都是難以置信的有狀態且難以操縱。 具體來說,有兩個主要問題:
- 在多線程環境中正確處理有狀態的API非常困難
- 很難使有狀態資源在全球范圍內可用,因為沒有記錄狀態
阿甘正傳的戲劇海報, 派拉蒙影業 ( Paramount Pictures)版權所有?1994。 版權所有。 可以相信上述用法滿足了所謂的合理使用
規則 :實施更多的功能樣式。 通過方法參數傳遞狀態。 操作較少的對象狀態。
8.短路equals()
這是一個低落的果實。 在大型對象圖中,如果所有對象的equals()方法首先比較便宜地比較身份,則可以顯著提高性能:
@Override public boolean equals(Object other) {if (this == other) return true;// Rest of equality logic... }請注意,其他短路檢查可能涉及空檢查,該檢查也應該存在:
@Override public boolean equals(Object other) {if (this == other) return true;if (other == null) return false;// Rest of equality logic... }規則 :短路所有equals()方法以獲得性能。
9.嘗試使方法默認為final
有些人對此持不同意見,因為默認情況下使事情最終完成與Java開發人員所習慣的相反。 但是,如果您完全控制所有源代碼,則默認情況下將方法設為final絕對沒有問題,因為:
- 如果確實需要重寫方法(確實嗎?),仍然可以刪除final關鍵字
- 您再也不會意外覆蓋任何方法
這特別適用于靜態方法,在這些方法中“覆蓋”(實際上是陰影)幾乎沒有任何意義。 最近,我在Apache Tika上遇到了一個非常糟糕的陰影靜態方法示例。 考慮:
- TaggedInputStream.get(InputStream)
- TikaInputStream.get(InputStream)
TikaInputStream擴展了TaggedInputStream并使用完全不同的實現來隱藏其靜態get()方法。
與常規方法不同,靜態方法不會互相覆蓋,因為調用站點在編譯時綁定了靜態方法調用。 如果您不走運,您可能會偶然得到錯誤的方法。
規則 :如果您完全控制自己的API,請嘗試在默認情況下盡可能多地使用final方法。
10.避免方法(T…)簽名
偶爾接受一個Object...參數的“ accept-all” varargs方法沒有任何問題:
void acceptAll(Object... all);編寫這樣的方法給Java生態系統帶來一點JavaScript的感覺。 當然,您可能希望將實際類型限制為在實際情況下更受限的內容,例如String... 而且由于您不想限制太多,您可能會認為用通用T代替Object是一個好主意:
void acceptAll(T... all);但事實并非如此。 T總是可以推斷為Object。 實際上,您最好不要將泛型與上述方法一起使用。 更重要的是,您可能認為可以重載上述方法,但是您不能:
void acceptAll(T... all); void acceptAll(String message, T... all);看起來您可以選擇將String消息傳遞給該方法。 但是這里的電話怎么辦?
acceptAll("Message", 123, "abc");編譯器會推斷<? extends Serializable & Comparable<?>> 為T <? extends Serializable & Comparable<?>> ,這使調用變得模棱兩可!
因此,每當您擁有“所有人都接受”的簽名(即使它是通用的)時,您將永遠無法再次安全地重載它。 API使用者可能只是幸運地“偶然地”選擇了編譯器選擇“正確的”最具體的方法。 但是他們也可能被欺騙使用“ accept-all”方法,或者根本無法調用任何方法。
規則 :如果可以,請避免“全部接受”簽名。 如果不能,則不要重載這種方法。
結論
Java是野獸。 與其他更高級的語言不同,它已經發展到今天。 那可能是一件好事,因為在Java的發展速度下,已經有數百個警告,這些警告只能通過多年的經驗來掌握。
翻譯自: https://www.javacodegeeks.com/2013/08/10-subtle-best-practices-when-coding-java.html
總結
以上是生活随笔為你收集整理的编码Java时的10个微妙的最佳实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Guava MapSplitters
- 下一篇: 房价备案价格还可以还价吗(房价备案价格)