Java 的最佳實踐

oschina發表於2015-09-24

Java 是在世界各地最流行的程式語言之一, 但是看起來沒人喜歡使用它。而 Java 事實上還算是一門不錯的語言,隨著 Java 8 最近的問世,我決定編制一個庫,實踐和工具的清單,彙集 Java 的一些最佳實踐。

本文被放到了 Github 上。你可以隨意地提交貢獻,並加入自己的有關 Java 方面的建議和最佳實踐。

  • 風格
    •  Javadoc
    • 構建器模式
    • 結構
    •  依賴注入
    • 避免空值
    • 預設不可變更
    • 避免大量的工具類
    •  格式化
    •  流
  • 釋出
    •   依賴收斂
    •   框架
    •   Maven
    •   持續整合
    •   Maven 資源庫
    •   配置管理
    •   jUnit 4
    •   jMock
    •   AssertJ
    •   Apache Commons
    •   Guava
    •   Gson
    •   Java Tuples
    •   Joda-Time
    •   Lombok
    •   Play framework
    •   SLF4J
    •   jOOQ
    •  Missing Features
    •   Testing
  • 工具
    •   Chronon
    •   IntelliJ IDEA
    •   JRebel
    •   Checker 框架
    •   Eclipse 記憶體分析器
  •   資源
    • 書籍
    • 播客

風格

通常,我們會以一種非常詳細繁雜的企業級 JavaBean 的風格進行 Java 程式碼的編寫。新的風格則更加清晰,正確,且看上去也更加的簡單。

結構

作為程式設計師的我們要做的最簡單的事情之一,就是傳遞資料。一般的方式就是定義一個 JavaBean:

public class DataHolder {
    private String data;

    public DataHolder() {
    }

    public void setData(String data) {
        this.data = data;
    }

    public String getData() {
        return this.data;
    }}

這有點麻煩,並且也有點浪費。儘管你的 IDE 也能自動的生成這樣的程式碼,但那也是種浪費。所以,別這麼做。

相反,我更願意選擇編寫類 C 的結構體風格的類,類裡面只容納資料:

public class DataHolder {
    public final String data;

    public DataHolder(String data) {
        this.data = data;
    }}

這樣就在程式碼行數上減少了一半。此外,這個類是不能被修改的,除非你對它進行了擴充套件,因此我們可以更加容易的理解它,因為我們明白它不可以被修改。

如果你要儲存像 Map 或者 List 這樣容易被修改的物件,就應該使用 ImmutableMap 和 ImmutableList,這一點會在不可變性質的那一節被講到。

Builder 模式

如果你有一個相當複雜的物件想要去為其構建一個結構,可以考慮使用 Builder 模式。

你可以在物件中建立一個能幫助你構建出這個物件的子類。它使用了可變語句,但是一旦你呼叫了build,它就會提供給你一個不可變的物件。

想象一下我們要有一個更加複雜的 DataHolder。針對它的構建器看起來可能像是下面這樣:

public class ComplicatedDataHolder {
    public final String data;
    public final int num;
    // lots more fields and a constructor

    public static class Builder {
        private String data;
        private int num;

        public Builder data(String data) {
            this.data = data;
            return this;
        }

        public Builder num(int num) {
            this.num = num;
            return this;
        }

        public ComplicatedDataHolder build() {
            return new ComplicatedDataHolder(data, num); // etc
        }  
    }}

然後這樣去使用它:

final ComplicatedDataHolder cdh = new ComplicatedDataHolder.Builder()
    .data("set this")
    .num(523)
    .build();

還有其它關於構建器的更好的例子 ,而這裡提供給你淺嘗輒止。這樣做最終會得到許多的我們努力去避免的樣板式程式碼,不過這也讓你得到了不可變的物件和一個非常流暢的介面。

依賴注入

這是更偏向軟體工程而不是 Java 的一節。編寫可測試軟體的最佳方式之一就是使用依賴注入(DI)。因為 Java 非常鼓勵 OO 設計,為了創造出可測試的軟體,你需要使用DI。

在 Java 中,一般使用 Spring 框架 的 DI 實現。它同時支援基於程式碼的裝配和基於 XML 配置的裝配。 如果你使用的是 XML 配置,因為其基於 XML 的配置, 不去過分使用 Spring 這一點很重要。XML 中絕對不能有任何邏輯或者控制結構,只能用來注入依賴。

使用 Spring 的好的選擇就是 Google 和 Square 的 Dagger 庫以及Google 的 Guice。他們不使用 Spring 的 XML 配置檔案格式,而是將依賴邏輯放到註解和程式碼中。

避免空值

盡你所能避免空值。如果你可以返回一個空的集合,就不要返回一個空值。如果你要使用空值,就考慮使用 @Nullable 註解。IntelliJ IDEA 內建有對於 @Nullable 註解的支援。

如果你使用的是 Java 8,就可以利用其優秀的新的 Optional 型別。如果一個可能存在也可能不存在,那就像下面這樣把它封裝到一個 Optional 類中:

public class FooWidget {
    private final String data;
    private final Optional<Bar> bar;

    public FooWidget(String data) {
        this(data, Optional.empty());
    }

    public FooWidget(String data, Optional<Bar> bar) {
        this.data = data;
        this.bar = bar;
    }

    public Optional<Bar> getBar() {
        return bar;
    }}

這樣現在就能很確定資料永遠都不會是空值了, 不過 bar 可能存在也可能不存在。Optional 有一些諸如 isPresent 這樣的方法,這使得其感覺跟只檢查空值的做法小同大異。但是它能讓你寫出像下面這樣的語句:

final Optional<FooWidget> fooWidget = maybeGetFooWidget();
final Baz baz = fooWidget.flatMap(FooWidget::getBar)
                         .flatMap(BarWidget::getBaz)
                         .orElse(defaultBaz);

這樣就比鏈條時的 if 空值檢檢視起來好多了。使用 Optional 的唯一缺陷就是標準庫並沒有對 Optional 有很好的支援,因此針對空值的處理還是需要的。

預設不可被改變

除非你有一個好的理由要這樣做,那麼變數、類和集合都是不應該被修改的。

變數的引用可以用 final 來變成不可被修改的:

final FooWidget fooWidget;if (condition()) {
    fooWidget = getWidget();} else {
    try {
        fooWidget = cachedFooWidget.get();
    } catch (CachingException e) {
        log.error("Couldn't get cached value", e);
        throw e;
    }}// fooWidget is guaranteed to be set here

現在你就可以確信 fooWidget 不會突然被重新賦值了。final 關鍵字一般同 if/else 塊和 try/catch 塊一起使用。當然,如果 fooWidget 不是不可被修改的,那你就可以很輕易了修改它了。

集合就應該無論何時都儘量使用 Guava 的 ImmutableMap,ImmutableList,或者 ImmutableSet 類。這些都擁有構建器,因此你可以動態地構建它們,並通過呼叫 build 方法來將它們標記為不可變。

類應該(通過 final)宣告其屬性域不可變和使用不可變的集合而變成不可變的。你也可以選擇使得類自身為 final,那樣它就不能被擴充套件和被改變了。

避免許多的工具類

在你發現自己新增了太多的方法到一個工具類中時要小心。

public class MiscUtil {
    public static String frobnicateString(String base, int times) {
        // ... etc
    }

    public static void throwIfCondition(boolean condition, String msg) {
        // ... etc
    }}

這些類一開始看起來很吸引人,因為它們裡面包含的方法並不真的屬於任何一塊。所以你就以程式碼重用的名義將它們扔到了一塊兒。

治病比生病更糟糕。將這些類放到原本屬於它們的地方,要不如果你必須要有像這麼一些方法的話,就考慮使用 Java 8 的介面上的預設方法。然後你就可以將公共方法統統扔到介面中去。而因為他們是介面,你就可以多次實現它們。

public interface Thrower {
    default void throwIfCondition(boolean condition, String msg) {
        // ...
    }

    default void throwAorB(Throwable a, Throwable b, boolean throwA) {
        // ...
    }}

然後每個有需要的類都可以簡單的實現這個介面。

格式化

格式化比起大多數程式設計師所認為的更加不被重視。那麼它是不是同你對於自己技術水平的在意目標一致,還有是不是能有助於其他人的對於程式碼的解讀呢?當然是。但我們也不要浪費一整天加空格來使得 if 的括號能“匹配”。

如果你絕對需要一個程式碼格式手冊,我強烈推薦 Google 的 Java 程式碼風格指南。該指南的最佳部分就是程式設計實踐這一節。絕對值得一讀.

Javadoc

為你的使用者所要面對的程式碼加註文件是很重要的。而這就意味著要使用示例和對於變數、方法和類的極值描述。

這樣做的必然結果就是對於不需要加註文件的就不要去加註文件. 如果就一個引數代表的是什麼你不想多費口舌,因為答案很明顯,就不要為其加註文件。樣板一樣的文件比沒有文件更糟糕,因為這對於會思考此處為何要加註的文件的使用者而言這會是一種戲弄。

Java 8 有了一個不錯的流和 lambda 語法。你可以像下面這樣編寫程式碼:

final List<String> filtered = list.stream()
    .filter(s -> s.startsWith("s"))
    .map(s -> s.toUpperCase());

而不是再像以前這樣寫:

final List<String> filtered = Lists.newArrayList();for (String str : list) {
    if (str.startsWith("s") {
        filtered.add(str.toUpperCase());
    }}

這就讓你能寫出更加流暢的程式碼,更具可讀性。

釋出

釋出 Java 通常有點棘手。如今有兩種主要的 Java 釋出方式 : 使用一套框架,或者根據靈活性的本地增量方案。

框架

因為釋出 Java 並不容易,現有的框架可能會有所幫助。最好的兩個就是 Dropwizard 和 Spring Boot。Play 框架 也可以被考慮也作為這些部署框架的其中之一。

它們全都試圖降低讓你的程式碼釋出出去的門檻. 它們在你是名Java新手或者希望能快速執行起來時特別有幫助. 單個的JAR部署比複雜的WAR和EAR部署更簡單.

不過,它們可能不怎麼靈活,而且詳單笨拙,因此如果你的專案不適合框架開發者為你的框架所做出選擇,你就得自己整合一個更加手動的配置了。

Maven

好的選擇: Gradle。

Maven 讓然是構建,打包並執行你的測試的標準工具。不過還有其它可選項,比如 Gradle,但是它們並不像Maven那樣為人們所接受。如果你是Maven新手,你應該通過 示例 來上手Maven.

我喜歡能有一個根 POM,裡面有你想要使用的所有的外部依賴包。它看起來像是這樣的。這個根 POM 只有一個外部依賴,而如果你自己的專案足夠大,就會有很多個。你的根 POM 自身可能也是一個專案:收到版本控制中並且像其它的 Java 專案那樣進行釋出。

如果你想過要為你的根POM標記出的每一個外部依賴的變化太多了,你不必浪費一個星期去跟蹤除錯多個專案的依賴錯誤。

你的所有的 Maven 專案都將包含你的根 POM,以及他所有的版本資訊。這樣,你就能得到你的公司所選擇的每一個外部依賴的版本,以及所有的正確的 Maven 外掛。如果你需要拉入外部依賴,就會像下面這樣運作:

<dependencies>
    <dependency>
        <groupId>org.third.party</groupId>
        <artifactId>some-artifact</artifactId>
    </dependency>
</dependencies>

如果你想要外部的依賴,那就應該被每一個獨立的專案部分管理起來。否則就很難保持根 POM 的有序性。

依賴收斂

Java 最好的部分就是大量的第三方庫能幫助你做任何事情。基本上每一個 API 或者工具包都有一個 Java SDK,並且很容易用 Maven 獲取。

而那些 Java 庫自身則還要依賴於其它的特定版本的庫. 如果你引入了夠多的庫,就會發生版本衝突, 那會像下面這樣:

Foo library depends on Bar library v1.0
Widget library depends on Bar library v0.9

哪個版本會引入到你的專案中呢?

使用 Maven 的依賴收斂外掛, 構建就會在你的依賴沒有使用相同的版本時報錯。之後要解決衝突,你可以有兩種選擇:

  1. 在你的dependencyManagement 一節為Bar明確挑選一個版本
  2. 將 Bar 從 Foo 或者 Widget 中排除出去

選擇哪種方案要視你的情形而定:如果你想要跟蹤一個專案的版本,那麼就用排除的方案。另外一方面,如果你想要明確的指定它,你就可以挑選一個版本,雖然你將需要在更新其它依賴的同時對它進行更新。

持續整合

顯然你需要一些持續整合的服務來讓你連續不斷地建立你的 SNAPSHOT(快照)版本和建立基於 git 標籤的 tag。

Jenkins 和 Travis-CI 是自然的選擇。

程式碼覆蓋率測試是有用的,並且 Cobertura 有一個很好的 Maven 外掛 並對 CI 提供支援。還有其他 Java 的程式碼覆蓋率工具,但我是使用 Cobertura 的。

Maven 資源庫

你需要一個地方放置你的 jar 包,war 包和 ear 包 , 因此你需要一個資源庫。

通常的選擇是 Artifactory 和 Nexus。都很有用,且都有他們自己的優劣勢。

你應該有一個自己安裝的 Artifactory/Nexus 並且上面有你的依賴的映象。這樣你的工作就不會因為線上的 Maven 資源庫掛掉而中斷。

配置管理

現在你已經把程式碼編譯好了,資源庫也設定好了,要做的就是讓你開發環境的程式碼最後放到生產上去。在這兒不要偷懶,因為自動化一些東西將會給你帶來長久的好處。

Chef, Puppet, 和 Ansible 是典型的選擇。我也編寫過一個叫做 Squadron 的可選方案,當然我認為你應該拿來看看,因為它比其他選擇更容易上手。

不管你選擇的哪個工具,都不要忘了對你的部署操作進行自動化。

可能 Java 最棒的特性就是它所擁有的大量的庫 . 這裡是可能大部分人都會用到的一些庫的集合.

缺少的功能特性

Java 的標準處曾今踏出了了不起的一步,現在看起來則缺少了幾個關鍵的功能特性。

Apache Commons

Apache Commons 專案有一堆實用的庫。

Commons Codec 有許多針對 Base64 和 16 進位制字串的編碼/解碼方法。你就不要再浪費時間再去重新編寫他們了。

Commons Lang 是針對 String 的建立和操作,字符集以及一堆實用工具方法的庫。

Commons IO 擁有你可以想象得到的所有檔案相關的方法。它有 FileUtils.copyDirectory,FileUtils.writeStringToFile,IOUtils.readLines 以及更多的東西。

Guava

Guava 是 Google 的優秀的補充Java所缺的庫。幾乎很難提交我所喜歡的有關於這個庫的所每個功能,但我會試試。

Cache 是獲取一個記憶體快取的簡單方法,可以被用來快取網路訪問,磁碟訪問,記憶函式或者任何實在的資料。只要實現一個 CacheBuilder 就能告訴 Guava 如何去構建你的快取,一切盡在你的掌握之中 !

Immutable 集合。有一堆這樣東西 : ImmutableMap,ImmutableList, 或者如果那是你的風格的話,就還有 ImmutableSortedMultiSet .

我也喜歡用 Guava 的方式編寫不可變的集合:

// Instead of
final Map<String, Widget> map = new HashMap<String, Widget>();

// You can use
final Map<String, Widget> map = Maps.newHashMap();

還有針對 Lists, Maps, Sets 以及更多集合的靜態類。 他們更清晰和可讀。

如果你還在 Java 6 或者 7 的坑裡面, 你可以使用 Collections2 類, 它擁有像 filter 和 transform 這樣的方法. 能讓你在沒有 Java 8 對流的支援下寫出流暢的程式碼。

Guava 也有一些簡單的東西, 比如 Joiner 能用分隔符將字串連線起來,以及一個通過忽略它們來 處理中斷的類.

Gson

Google的Gson 庫是一個簡單快速的JSON轉換庫。像下面這樣運作:

final Gson gson = new Gson();
final String json = gson.toJson(fooWidget);
final FooWidget newFooWidget = gson.fromJson(json, FooWidget.class);

相當簡單且令人愉悅。Gson使用者手冊 有許多的示例。

Java Tuples

Java經常令我頭疼的一點就是他的標準庫裡面並沒有內建元組。幸運的是, Java tuples 專案解決了這個問題。

它易於使用而且表現很棒:

Pair<String, Integer> func(String input) {
    // something...
    return Pair.with(stringResult, intResult);}

Joda-Time

Joda-Time 是我所使用過的最棒的時間庫. 簡答,直接,易於測試. 夫復何求?

所以你只要在如果沒有使用Java8時使用這個庫,因為Java8有了新的 日期時間 庫。

Lombok

Lombok 是一個有趣的庫。它能通過註解讓你減少 Java 所嚴重遭受的樣板式程式碼。

想要為你的類變數加入設定器和獲取器? 簡單:

public class Foo {
    @Getter @Setter private int var;}

現在你可以這樣做:

final Foo foo = new Foo();foo.setVar(5);

還有 更多的東西。我還沒有將 Lombok 用於生產環境,但我迫不及待的想要這麼做了。

Play framework

好的選擇 : Jersey 或者 Spark

在Java中實現 RESTful web 服務又兩個主要的陣營 : JAX-RS 和其餘一切。

JAX-RS 是傳統的方式。你可以使用諸如Jersey之類的東西來將註解結合介面和實現來組織 web 服務。這裡有意思的是你可以簡單的從介面類建立出客戶端。

Play framework 是 JVM 的 web服務的一個異類:你會有一個路由檔案,然後你要編寫在這些路由中被引用的類。它實際上是一個完整的 MVC 框架, 但是你可以簡單地只把他用於 REST web 服務。

它在 Java 和 Scala 上都能用。它稍有偏向 Scala 優先,不過在Java中也還好.

如果你用過Python中向Flash這樣的微型框架, 你就能熟悉 Spark。它在 Java 8 上能執行得很好。

SLF4J

有許多Java日誌的解決方案。我喜歡的就是 SLF4J 因為它的極度的可插入性,並且可以同時結合來自不同日誌框架的日誌. 有沒有過一個使用了 java.util.logging, JCL, 以及 log4j 的古怪專案? SLF4J 為你而生。

兩頁篇幅的操作手冊 就是你入門所需要的。

jOOQ

我不想換重量級的 ORM 框架,因為我喜歡 SQL。因此我寫了許多 JDBC 模板, 而這樣就有點難以維護。 jOOQ 是一個更好的解決方案。

他能讓你用 Java 以一種更加型別安全的方式編寫SQL:

// Typesafely execute the SQL statement directly with jOOQ

Result<Record3<String, String, String>> result = 
create.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
    .from(BOOK)
    .join(AUTHOR)
    .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID))
    .where(BOOK.PUBLISHED_IN.equal(1948))
    .fetch();

使用這個和 DAO 模式, 你可以使得訪問資料庫變得輕而易舉。

測試

測試對於你的軟體至關重要。這些包能使得測試更簡單。

jUnit 4

jUnit 不需要介紹了。它是Java中單元測試的標準工具.

而你可能不會發揮 jUnit 的全部潛能。jUnit 支援 引數化測試,,讓你不用編寫過多樣板程式碼的 規則,隨機測試特定程式碼的 理論, 以及 假設。

jMock

如果依賴注入那塊你弄好了的話,這時候它就能發揮點作用了 : 模擬那些有副作用的程式碼 (比如同一個REST伺服器進行互動) 並且仍然可以對呼叫它的程式碼進行斷言。

jMock 是標準的Java模擬工具。它看起來像這樣:

public class FooWidgetTest {
    private Mockery context = new Mockery();

    @Test
    public void basicTest() {
        final FooWidgetDependency dep = context.mock(FooWidgetDependency.class);

        context.checking(new Expectations() {{
            oneOf(dep).call(with(any(String.class)));
            atLeast(0).of(dep).optionalCall();
        }});

        final FooWidget foo = new FooWidget(dep);

        Assert.assertTrue(foo.doThing());
        context.assertIsSatisfied();
    }}

這裡通過 jMock 設定了一個 FooWidgetDependency,然後加入了一個預期( expectation)。我們預期 dep 的 call 方法會使用某個字串被呼叫一次並且 dep 的 optionalCall 方法會被呼叫0到多次。

如果你要一次又一次設定相同的依賴,你可能應該將那些放到一個 測試夾具(est fixture)中,並將 assertIsSatisfied 放到一個 @After 夾具中。

AssertJ

你曾經用 jUnit 這樣做過嗎?

final List<String> result = some.testMethod();
assertEquals(4, result.size());
assertTrue(result.contains("some result"));
assertTrue(result.contains("some other result"));
assertFalse(result.contains("shouldn't be here"));

這都是煩人的樣板程式碼。 AssertJ 會把這些都幹掉。你可以將同樣的程式碼轉換成這樣:

assertThat(some.testMethod()).hasSize(4)
                             .contains("some result", "some other result")
                             .doesNotContain("shouldn't be here");

這樣流暢的介面讓你的測試更加可讀. 夫復何求?

工具

IntelliJ IDEA

好的選擇: Eclipse 和 Netbeans

最好的java ide是 IntelliJ IDEA。 它有大量超讚的功能特性,並且真正讓開發 Java 相關的所有細節都暴露無遺。自動補全很棒, 檢查也是頂尖的,還有重構工具真的很有幫助。

免費的社群版本對我而言已經很好的,而旗艦版本則還有許多很棒的功能,比如資料庫工具,對 Spring Framework 和 Chronon 的支援。

Chronon

我最喜愛的 GDB 7 的一個功能就是除錯的時候可以倒著走。這在你獲取到旗艦版本並且使用了 Chronon IntelliJ 外掛 時是可能的。

你可以獲取到變數的歷史,後退,方法的歷史以及更多其它的東西。初次使用會有點點怪,但它能幫助你解決一些非常複雜的問題,諸如此類的海森堡 bug。

JRebel

持續整合常常是軟體即服務產品的目標。如果你不想等待編譯構建結束就看到程式碼變化所產生的效果呢?

那正是 JRebel 所要做的。一旦你把你的伺服器掛到了你的 JRebel 客戶端,你就可以實時看見伺服器上的變化. 當你想要快速地進行試驗時這能節省大量的時間。

檢查框架(Checker Framework)

Java 的型別系統很不咋地. 它不能區分字串和實際上是正規表示式的字串,也沒有做 壞點檢查。不過, Checker Framework 能做到並且能做到更多。

它使用像 @Nullable 這樣的註解來檢查型別. 你甚至可以 自己定義註解 來是的靜態分析如虎添翼。

Eclipse  記憶體分析器

即使是在Java中,記憶體也會發生洩漏。幸運的是,有針對這個問題的工具。我用過的最好的解決這些問題的工具就是 Eclipse 記憶體分析器。它能獲取到堆疊,讓你可以找出問題何在。

有幾種方法可以獲取到一個 JVM 程式的堆疊 , 而我使用的是 jmap:

$ jmap -dump:live,format=b,file=heapdump.hprof -F 8152
Attaching to process ID 8152, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 23.25-b01
Dumping heap to heapdump.hprof ...
... snip ...
Heap dump file created

之後你就可以用記憶體分析器開啟 heapdump.hprof 檔案,並快速的看到到底發生了什麼.

資源

能幫助你成為 Java 大師的資源

資料

部落格

相關文章