10 個 Java 編碼中微妙的最佳實踐

jopen 11年前發布 | 24K 次閱讀 Struts Java開發

這是10個最佳實踐的列表,比你平時在Josh Bloch的《effective java》中看到的規則更加精妙。和Josh Bloch列出的非常容易學習的、和日常情況息息相關的實踐相比,這個列表中提到了一些關于設計API/SPI的實踐,雖然不常見,但是存在很大的效率問題。

我在編寫和維護jOOQ一種內部DSL,在java中將SQL模塊化)時,碰到了這些問題。作為內部DSL,jOOQ最大限度的挑戰了java編譯器和泛型,把泛型,變量和重載結合到了一起。這種太寬泛的API是Josh Bloch相當不推薦的。

讓我來和你分享這10個java編碼中微妙的最佳實踐:

1.牢記C++的析構函數

還記得C++中的析構函數嗎?不記得了?或許你真的很幸運,因為你再也不必為刪除對象后,沒有及時釋放內存而造成內存泄露進行調試了。我們真的應該感謝Sun和Oracle實現垃圾回收機制。

盡管如此,對于我們來說,析構函數仍然有一個很有趣的特點。它常常會讓我們對以和分配內存相反的順序釋放內存的工作模式感到容易理解。同樣,在JAVA代碼中,當你處理如下類析構函數語法的時候,也要把這個特性牢記在心:

當使用@Before和@After但與注解時

當分配和釋放JDBC資源時

當調用父類的方法時

也有其他不同的使用案例。這有一個顯示如何實現事件監聽的實例:

@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); }</pre>另外一個哲學家用餐的問題,顯示了這有多么的重要。 10 個 Java 編碼中微妙的最佳實踐

關于哲學家用餐的問題,請查看鏈接: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);
}

如果你也需要消息ID和消息源,怎么辦?對于上面的類型,API演進將會阻礙你添加參數。當然,有了Java8,你可以添加一個defender方法,“防御”你早期糟糕的設計決策:

interface EventListener {
  // Bad
  default void message(String message) {
    message(message, null, null);
  }
  // Better?
  void message(
    String message,
    Integer id,
    MessageSource source
  );
}

注意很不幸defender方法不能為final

但是比起用數十個方法污染你的SPI,使用一個上下文對象(或參數對象)好很多。

interface MessageContext {
  String message();
  Integer id();
  MessageSource source();
}

interface EventListener { // Awesome! void message(MessageContext context); }</pre>

比起EventListner SPI你可以更容易演進MessageContext API,因為很少用戶會實現它。

規則: 無論何時你指定SPI的時候, 考慮使用上下文/參數對象,而不要編寫固定參數數量的方法。

備注: 使用特定的MessageResult類型傳遞結果也是一個好的想法,該類型可以通過構造器API構建。這將會為你的SPI提供更多的SPI演進靈活性。

3.避免使用匿名,局部或內部類

Swing程序員通常只要按幾下快捷鍵即可生成成百上千的匿名類。在多數情況下,只要遵循接口、不違法SPI子類型的生命周期(SPI subtype lifecycle),這樣做也無妨。

但是不要因為一個簡單的原因——它們會保存對外部類的引用,就頻繁的使用匿名、局部或者內部類。因為無論它們走到哪,外部類就得跟到哪。例如,在局部類的域外操作不當的話,那么整個對象圖就會發生微妙的變化從而可能引起內存泄露。

規則:在編寫匿名、局部或內部類前請三思能否將它轉化為靜態的或普通的頂級類,從而避免方法將它們的對象返回到更外層的域中。

注意:使用雙層花括號來初始化簡單對象:

new HashMap() {{
  put("1", "a");
  put("2", "b");
}}
這個方法利用了 JLS §8.6規范里描述的實例初始化方法(initializer)。表面上看起來不錯,但實際上不提倡這種做法。因為要是使用完全獨立的HashMap對象,那么實例就不會一直保存著外部對象的引用。此外,這也會讓類加載器管理更多的類。

4. 現在就開始編寫SAM!

Java8的腳步近了。伴隨著Java8帶來了lambda表達式,無論你是否喜歡。盡管你的API使用者可能會喜歡,但是你最好確保他們可以盡可能經常的使用。因此除非你的API接收簡單的“標量”類型,比如int、long、String 、Date,否則讓你的API盡可能經常的接收SAM。

什么是SAM?SAM是單一抽象方法[類型]。也稱為函數接口,很快被注釋為@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()));
  }
});

設想以SAM的方式用jOOX處理XML:

$(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/函數接口。

備注:有許多關于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/

    </li> </ul>

    5.避免讓方法返回null

    我曾寫過1、2篇關于java NULLs的文章,也講解過Java8中引入新的Optional類。從學術或實用的角度來看,這些話題還是比較有趣的。

    盡管現階段Null和NullPointerException依然是Java的硬傷,但是你仍可以設計出不會出現任何問題的API。在設計API時,應當盡可能的避免讓方法返回null,因為你的用戶可能會鏈式調用方法:

    initialise(someArgument).calculate(data).dispatch();

    從上面代碼中可看出,任何一個方法都不應返回null。實際上,在通常情況下使用null會被認為相當的異類。像  jQueryjOOX這樣的庫在可迭代的對象上已完全的摒棄了null。

    Null通常用在延遲初始化中。在許多情況下,在不嚴重影響性能的條件下,延遲初始化也應該被避免。實際上,如果涉及的數據結構過于龐大,那么就要慎用延遲初始化。

    規則:無論何時方法都應避免返回null。null僅用來表示“未初始化”或“不存在”的語義。

    6.設計API時永遠不要返回空(null)數組或List

    盡管在一些情況下方法返回值為null是可以的,但是絕不要返回空數組或空集合!請看 java.io.File.list()方法,它是這樣設計的:

    此方法會返回一個指定目錄下所有文件或目錄的字符串數組。如果目錄為空(empty)那么返回的數組也為空(empty)。如果指定的路徑不存在或發生I/O錯誤,則返回null。

    因此,這個方法通常要這樣使用:

    File directory = // ...

    if (directory.isDirectory()) { String[] list = directory.list();

    if (list != null) { for (String file : list) { // ... } } }</pre>大家覺得null檢查有必要嗎?大多數I/O操作會產生IOExceptions,但這個方法卻只返回了null。Null是無法存放I/O錯誤信息的。因此這樣的設計,有以下3方面的不足:

    • Null無助于發現錯誤
    • Null無法表明I/O錯誤是由File實例所對應的路徑不正確引起的
    • 每個人都可能會忘記判斷null情況
    • </ul>

      以集合的思維來看待問題的話,那么空的(empty)的數組或集合就是對“不存在”的最佳實現。返回空(null)數組或集合幾乎是無任何實際意義的,除非用于延遲初始化。

      規則:返回的數組或集合不應為null。


      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();</pre>

      這使得JDBC API如此的古怪。每個對象都是有狀態的,難以操作。具體的說,有兩個主要的問題:

      • 在多線程環境很難正確的處理有狀態的API
      • 很難使有狀態的資源全局可用,因為狀態沒有被描述
      • </ul>

        10 個 Java 編碼中微妙的最佳實踐

        戲劇海報《阿甘正傳》,版權1994年由派拉蒙影業公司。保留所有權利。相信上述慣例滿足所謂的合理使用

        規則:更多的以函數風格實現。通過方法參數轉移狀態。極少操作對象狀態。

        8. 短路式 equals()

        這是一個比較容易操作的方法。在比較復雜的對象系統中,你可以獲得顯著的性能提升,只要你在所有對象的equals()方法中首先進行相等判斷:

        @Override
        public boolean equals(Object other) {
          if (this == other) return true;
          // 其它相等判斷邏輯...
        }

        注意,其它短路式檢查可能涉及到null值檢查,所以也應當加進去:

        @Override
        public boolean equals(Object other) {
          if (this == other) return true;
          if (other == null) return false;
          // Rest of equality logic...
        }

        規則: 在你所有的equals()方法中使用短路來提升性能。

        9. 盡量使方法默認為final

        有些人可能不同意這一條,因為使方法默認為final與Java開發者的習慣相違背。但是如果你對代碼有完全的掌控,那么使方法默認為final是肯定沒錯的:

        • 如果你確實需要覆蓋(override)一個方法(你真的需要?),你仍然可以移除final關鍵字
        • 你將永遠不會意外地覆蓋(override)任何方法
        • </ul>

          這特別適用于靜態方法,在這種情況下“覆蓋”(實際上是遮蔽)幾乎不起作用。我最近在Apache Tika中遇到了一個很糟糕的遮蔽靜態方法的例子。考慮:

          • TaggedInputStream.get(InputStream)
          • TikaInputStream.get(InputStream)
          • </ul>

            TikaInputStream擴展了TaggedInputStream,以一種相對不同的實現遮蔽了它的靜態get()方法。

            與常規方法不同,靜態方法不能互相覆蓋,因為調用的地方在編譯時就綁定了靜態方法調用。如果你不走運,你可能會意外獲得錯誤的方法。

            規則:如果你完全掌控你的API,那么使盡可能多的方法默認為final。

            10. 避免方法(T…)簽名

            在特殊場合下使用“accept-all”變量參數方法接收一個Object...參數就沒有錯的:

            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");

            編譯器將T推斷為>,這將會使調用不明確!

            所以無論何時你有一個“accept-all”簽名(即使是泛型),你將永遠不能類型安全地重載它。API使用者可能僅僅在走運的時候才會讓編譯器“偶然地”選擇“正確的”限定最多的方法。但是也可能使用accept-all方法或者無法調用任何方法。

            規則: 如果可能,避免“accept-all”簽名。如果不能,不要重載這樣的方法。

            結論

            Java是一個野獸。不像其它更理想主義的語言,它慢慢地演進為今天的樣子。這可能是一件好事,因為以Java的開發速度就已經有成百上千個警告,而且這些警告只能通過多年的經驗去把握。

            敬請期待更多關于這個主題的前十名列表!

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!