如何寫出更好的Java程式碼

deepinmind發表於2014-05-21

  Java是最流行的程式語言之一,但似乎並沒有人喜歡使用它。好吧,實際上Java是一門還不錯的程式語言,由於最近Java 8釋出了,我決定來編輯一個如何能更好地使用Java的列表,這裡麵包括一些庫,實踐技巧以及工具。

  這篇文章在GitHub上也有。你可以隨時在上面貢獻或者新增你自己的Java使用技巧或者最佳實踐。

  • 編碼風格
    • 結構體
      • builder模式
    • 依賴注入
    • 避免null值
    • 不可變
    • 避免過多的工具類
    • 格式
      • 文件
      • Stream
  • 部署
    • 框架
    • Maven
      • 依賴收斂
    • 持續整合
    • Maven倉儲
    • 配置管理
    • 遺失的特性
      • Apache Commons
      • Guava
      • Gson
      • Java Tuples
      • Joda-Time
      • Lombok
      • Play framework
      • SLF4J
      • jOOQ
    • 測試
      • jUnit 4
      • jMock
      • AssertJ
  • 工具

    • IntelliJ IDEA
      • Chronon
    • JRebel
    • 校驗框架
    • Eclipse Memory Analyzer
  • 資源

    • 書籍
    • 播客

  編碼風格

  傳統的Java編碼方式是非常囉嗦的企業級JavaBean的風格。新的風格更簡潔準確,對眼睛也更好。

  結構體

  我們這些碼農乾的最簡單的事情就是傳遞資料了。傳統的方式就是定義一個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 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();
 

  關於Builder的使用這裡還有些更好的例子,我這裡舉的例子只是想讓你大概感受一下。當然這會產生許多我們希望避免的樣板程式碼,不過好處就是你有了一個不可變物件以及一個連貫介面。

  依賴注入

  這更像是一個軟體工程的章節而不是Java的,寫出可測的軟體的一個最佳方式就是使用依賴注入(Dependency injection,DI)。由於Java強烈鼓勵使用物件導向設計 ,因此想寫出可測性強的軟體,你需要使用DI。

  在Java中,這個通常都是用Spring框架來完成的。它有一個基於XML配置的繫結方式,並且仍然相當流行。重要的一點是你不要因為它的基於XML的配置格式而過度使用它了。在XML中應該沒有任何的邏輯和控制結構。它只應該是依賴注入。

  還有一個不錯的方式是使用Dagger庫以及Google的Guice。它們並沒有使用Spring的XML配置檔案的格式,而是將注入的邏輯放到了註解和程式碼裡。

  避免null值

  如果有可能的話儘量避免使用null值。你可以返回一個空的集合,但不要返回null集合。如果你準備使用null的話,考慮一下@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;
    }
}
 

  現在問題就清楚了,data是不會為null的,而bar可能為空。Optional類有一些像isPresent這樣的方法,這讓它感覺跟檢查null沒什麼區別。不過有了它你可以寫出這樣的語句:

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

  這比使用if來檢查null好多了。唯一的缺點就是標準類庫中對Optional的支援並不是很好,因此你還是需要對null進行檢查的。

  不可變

  變數,類,集合,這些都應該是不可變的,除非你有更好的理由它們的確需要進行修改。

  變數可以通過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, or ImmutableSet類。這些類都有自己的構造器,你可以動態的建立它們,然後將它們設定成不可變的,。

  要使一個類不可變,你可以將它的欄位宣告成不可變的(設定成final)。你也可以把類自身也設定成final的這樣它就不能被擴充套件並且修改了,當然這是可選的。

  避免大量的工具類

  如果你發現自己新增了許多方法到一個Util類裡,你要注意了。

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 {
    public void throwIfCondition(boolean condition, String msg) {
        // ...
    }

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

  這樣需要使用它的類只需簡單的實現下這個介面就可以了。

  格式

  格式遠比許多程式設計師相像的要重要的多。一致的格式說明你關注自己的程式碼或者對別人有所幫助?是的。不過你先不要著急為了讓程式碼整齊點而浪費一整天的時間在那給if塊加空格了。

  如果你確實需要一份程式碼格式規範,我強烈推薦Google的Java風格指南。這份指南最精彩的部分就是程式設計實踐這節了。非常值得一讀。

  文件

  面向使用者的程式碼編寫下文件還是很重要的。這意味著你需要提供一些使用的示例,同時你的變數方法和類名都應該有適當的描述資訊。

  結論就是不要給不需要文件的地方新增文件。如果對於某個引數你沒什麼可說的,或者它已經非常明顯了,別寫文件了。模板化的文件比沒有文件更糟糕,因為它欺騙了你的使用者,讓他覺得這裡有文件。

  流

  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 BootPlay Framework也可以算是一個部署框架。

  這些框架都試圖降低部署程式的門檻。如果你是一個Java的新手或者你需要快速把事情搞定的話,那麼框架就派上用場了。單個jar的部署當然會比複雜的WAR或者EAR部署要更容易一些。

  然而,這些框架的靈活性不夠,並且相當頑固,因此如果這些框架的開發人員給出的方式不太適合你的專案的話,你只能自己進行配置了。

  Maven

  備選方案:Gradle

  Maven仍然是編譯,打包,執行測試的標準化工具。還有其它一些選擇,比如Gradle,不過它們的採用程度遠不Maven。如果你之前沒用過Maven,你可以看下這個Maven的使用示例

  我喜歡用一個根POM檔案來包含所有的外部依賴。它看起來就像是這樣。這個根POM檔案只有一個外部依賴,不過如果你的產品很大的話,你可能會有很多依賴。你的根POM檔案自己就應該是一個專案:它有版本控制,並且和其它的Java專案一樣進行釋出。

  如果你覺得為每個外部依賴的修改都給POM檔案打個標籤(tag)有點太浪費了,那是你還沒有經歷過花了一個星期的時間來跟蹤專案依賴錯誤的問題。

  你的所有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的依賴收斂的外掛後,如果你的依賴版本不一致的話,編譯的時候就會報錯。那麼你有兩種解決衝突的方案:

  • 在dependencyManagement區中顯式地選擇某個版本的bar。
  • Foo或者Widget都不要依賴Bar。

  到底選擇哪種方案取決你的具體情況: 如果你想要跟蹤某個工程的版本,不依賴它是最好的。另一方面,如果你想要明確一點,你可以自己選擇一個版本,不過這樣的話,如果更新了其它的依賴,也得同步地修改它。

  持續整合

  很明顯你需要某種持續整合的伺服器來不斷地編譯你的SNAPSHOT版本,或者對Git分支進行構建。

  JenkinsTravis-CI是你的不二選擇。

  程式碼覆蓋率也很重要,Cobertura有一個不錯的Maven外掛,並且對CI支援的也不錯。當然還有其它的程式碼覆蓋的工具,不過我用的是Cobertura。

  Maven庫

  你需要一個地方來儲存你編譯好的jar包,war包,以及EAR包,因此你需要一個程式碼倉庫。

  常見的選擇是Artifactory或者Nexus。兩個都能用,並且各有利弊。

  你應該自己進行Artifactory/Nexus的安裝並且將你的依賴做一份映象。這樣不會由於下載Maven 庫的時候出錯了導到編譯中斷。

  配置管理

  那現在你的程式碼可以編譯了,倉庫也搭建起來了,你需要把你的程式碼帶出開發環境,走向最終的釋出了。別馬虎了,因為自動化執行從長遠來看,好處是大大的。

  ChefPuppet,和Ansible都是常見的選擇。我自己也寫了一個可選方案,Squadron。這個嘛,當然了,我自然是希望你們能下載下它的,因為它比其它那些要好用多了。

  不管你用的是哪個工具,別忘了自動化部署就好。

  庫

  可能Java最好的特性就是它擁有的這些庫了。下面列出了一些庫,應該絕大多數人都會用得上。

  Java的標準庫,曾經還是很不錯的,但在現在看來它也遺漏掉了很多關鍵的特性。

  Apache Commons

  Apache Commons專案有許多有用的功能。

  • Commons Codec有許多有用的Base64或者16進位制字串的編解碼的方法。別浪費時間自己又寫一遍了。
  • Commons Lang是一個字串操作,建立,字符集,以及許多工具方法的類庫。
  • Commons IO,你想要的檔案相關的方法都在這裡了。它有FileUtils.copyDirectory,FileUtils.writeStringToFile, IOUtils.readLines,等等。

  Guava

  Guava是一個非常棒的庫,它就是Java標準庫"所缺失的那部分"。它有很多我喜歡的地方,很難一一贅述,不過我還是想試一下。

  • Cache,這是一個最簡單的獲取記憶體快取的方式了,你可以用它來快取網路訪問,磁碟訪問,或者幾乎所有東西。你只需實現一個CacheBuilder,告訴Guava如何建立快取就好了。
  • 不可變集合。這裡有許多類: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的方法。沒有Jvaa 8的stream的支援,你也可以用它們來寫出連貫的程式碼。

  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是我用過的最好的時間庫了。簡直,直接,容易測試。你還想要什麼?

  這個庫裡我最喜歡的一個類就是Duration,因為我用它來告訴說我要等待多長時間,或者過多久我才進行重試。

  Lombok

  Lombok是一個非常有趣的庫。它通過註釋來減少了Java中的飽受詬病的樣板程式碼(注:setter,getter之類的)。

  想給你類中的變數增加setter, getter方法?太簡單了:

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

  現在你可以這麼寫了:

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

  這裡還有更多的示例。我還沒在生產程式碼中用過Lombok,不過我有點等不及了。

Play框架

  備選方案:Jersey或者Spark

  在Java中實現REST風格的WEB服務有兩大陣營:JAX-RS和其它。

  JAX-RS是傳統的方式。你使用像Jersey這樣的東西來將註解和介面,實現組合到一起來實現WEB服務。這樣做的好處就是,你可以通過一個介面就能很容易建立出一個呼叫的客戶端來。

  Play框架是在JVM上實現WEB服務的截然不同的一種方式:你有一個routes檔案,然後你去實現routes中那些規則所引用到的類。它其實就是個完整的MVC框架,不過你可以只用它來實現REST服務。

  它同時支援Java和Scala。它優先使用Scala這點可能有點令人沮喪,但是用Java進行開發的話也非常不錯。

  如果你習慣了Python裡的Flask這類的微框架,那麼你應該會對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還支援引數化測試,以及能讓你少寫很多樣板程式碼的測試規則,還有能隨機測試程式碼的Theory,以及Assumptions

  jMock

  如果你已經完成了依賴注入,那麼它回報你的時候來了:你可以mock出帶副作用的程式碼(就像和REST伺服器通訊那樣),並且仍然能對呼叫它的程式碼執行斷言操作。

  jMock是Java中標準的mock工具。它的使用方式是這樣的:

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 ,然後新增了一些期望的操作。我們希望dep的call方法被呼叫一次而dep的optionalCall 方法會被呼叫0或更多次。

  如果你反覆的構造同樣的FooWidgetDependency,你應該把它放到一個測試裝置(Test 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 and Netbeans

  最好的Java IDE當然是 IntelliJ IDEA。它有許多很棒的特性,我們之所以還能忍受Java這些冗長的程式碼,它起了很大的作用。自動補全很棒,< a href="http://i.imgur.com/92ztcCd.png" target="_blank">程式碼檢查也超讚,重構工具也非常實用。

  免費的社群版對我來說已經足夠了,不過在旗艦版中有許多不錯的特性比如資料庫工具,Srping框架的支援以及Chronon等。

  Chronon

  GDB 7中我最喜歡的特性就是除錯的時候可以按時間進行遍歷了。有了IntelliJ IDEA的Chronon外掛後,這個也成為現實了。當然你得是旗艦版的。

  你可以獲取到變數的歷史值,跳回前面執行的地方,獲取方法的呼叫歷史等等。第一次使用的話會感覺有點怪,但它能幫忙你除錯一些很棘手的BUG。

  JRebel

  持續整合通常都是SaaS產品的一個目標。你想想如果你甚至都不需要等到編譯完成就可以看到程式碼的更新?

  這就是JRebel在做的事情。只要你把你的伺服器掛到某個JRebel客戶端上,程式碼一旦有改動你馬上就能看到效果。當你想快速體驗一個功能的話,這個的確能節省不少時間。

  驗證框架

  Java的型別系統是相當弱的。它不能區分出普通字串以及實際上是正則的字串,也不能進行

$ 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

  然後你就可以用Memory Analyzer來開啟heapdump.hprof檔案,看看到底發生了什麼。

  資源

  好的資源能幫助你成為一名Java大師。

  書籍

  播客

  The Java Posse

  原文連結:http://blog.seancassidy.me

相關文章