更好的Java

cen 9年前發布 | 33K 次閱讀 Java Java開發

Java是最流行的語言之一,但是似乎沒人喜歡使用它。好吧,Java僅僅是一種“還好”的編程語言。自從Java 8的面世,我決定編輯一個關于Java的列表,包括庫、最佳實踐以及工具讓我們能更好的使用Java。

這篇文章在Github上,你可以自由的添加你所使用到的一些Java工具及最佳實踐。

  • 風格

    • 結構

      • 構建者模式
      • </ul> </li>

      • Dependency injection
      • Avoid Nulls
      • 默認為不可變
      • 避免太多的工具類
      • 格式化

        • Javadoc
        • </ul> </li>

        • Streams
        • </ul> </li>

        • 部署

          • 框架
          • Maven

            • 重復依賴檢測
            • </ul> </li>

            • 持續集成
            • Maven倉庫
            • 配置管理
            • </ul> </li>

              • 缺失的特性

                • Apache Commons
                • Guava
                • Gson
                • Java Tuples
                • Joda-Time
                • Lombok
                • Play框架
                • SLF4J
                • jOOQ
                • </ul> </li>

                • 測試

                  • jUnit 4
                  • jMock
                  • AssertJ
                  • </ul> </li> </ul> </li>

                  • 工具

                    • IntelliJ IDEA

                      • Chronon
                      • </ul> </li>

                      • JRebel
                      • Checker框架
                      • Eclipse Memory Analyzer
                      • </ul> </li>

                      • 資源

                        • 書籍
                        • 播客
                        • </ul> </li> </ul>

                          風格

                          傳統上,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;
                          }
                          

                          }</pre>

                          這段代碼非常冗長、浪費。即使IDE可以自動生成這些代碼,但是它還是一種浪費,所以不要這么做

                          相反,我更喜歡C結構風格的類,該類只保存數據:

                          public class DataHolder {
                              public final String data;

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

                          }</pre>

                          這段代碼的行數減少了一半。更重要的是,該類是不可以變的,除非你繼承該類。所以我們更容易使用它,因為我們知道它不能改變。

                          你應該使用 ImmutableMap 和 ImmutableList 來替代將對象存儲在一個能輕易改變 Map 或 List 中,這些內容在關于不可變性的部分中討論。

                          建造者模式

                          如果你有一個相當復雜的對象,你想使用某種結構來創建這種對象的時候,可以考慮使用建造者模式。

                          你可以在構造你的對象的類中創建一個子類,它的狀態是可變的,但是一旦你調用 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
                              }  
                          }
                          

                          }</pre>

                          然后這么使用:

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

                          可能會有其它更好的建造者模式例子,這個例子只是讓你了解一下建造者模式是什么東西?最后,這里我們試著避開很多公式化的例子,創建了一個不可變的對象和一個很流暢的接口。

                          依賴注入

                          這個更多的是軟件工程的內容,而不是Java的內容。但是,編寫可測試軟件的最好方式就是使用依賴注入(DI)。因為Java強力推薦面向對象設計,為了創建可測試軟件,你需要使用DI。

                          在Java里面,最典型的例子是Spring框架。它可以使用基于注解的注入或基于XML配置的注入。如果你想使用XML配置,很重要的事情是不要過度使用Spring的這種基于XML的配置形式。在這個XML中絕不應該有邏輯和控制結構,它僅僅是依賴注入。

                          使用的比較好的Spring例子是Google和Square的Dagger庫以及Google的Guice。它們都沒有使用Spring的XML配置文件,而是把注入的邏輯寫在了注解和代碼里面。

                          避免空值

                          盡可能的避免使用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;
                          }
                          

                          }</pre>

                          所以,現在非常清晰,data 從來不會為 null,但是 bar 可能存在,也可能不存在。Optional 有一些方法,如 isPresent,該方法感覺跟判空沒什么不同,但是它允許你像下面這樣寫代碼:

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

                          這樣比連續的進行 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的 ImmutableMapImmutableList ImmutableSet 類。這些已存在的建造者可以讓你通過調用它們的 build 方法動態的建造不可變的集合。

                          你應該通過聲明不可變的成員變量(通過 final)和使用不可變的集合類型來創建不可變的類。還有一種選擇,你可以申明類自身為 final,這樣的話該類就是不能被繼承和修改的。

                          避免使用過多的工具類

                          如果你發現加了很多的方法在 Util 類中,你就要小心了:

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

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

                          }</pre>

                          首先,這些類看起來都非常有吸引力,因為這些方法不屬于任何地方,所以你可以在任何地方重用這些代碼。

                          比疾病更糟糕的是治療。這些類就應該放到屬于它們的地方,否則你必須使用這樣的通用方法,考慮一下Java 8 的接口的默認方法。然后你組合通用的方法在一個接口里。因為它們是接口,所以你能夠用多種方式實現它們。

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

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

                          }</pre>

                          任何需要它的類都可以很簡單地實現這個接口。

                          格式化

                          格式對編碼的人來說沒那么重要。但是它是不是能幫助你持續的關注你的原稿?是不是能幫助別人閱讀你的代碼?很明顯是的。但是請不要浪費一天去對 if 塊增加空格來保證它是“匹配”的。

                          如果你特別想要一份代碼格式化指南,我強烈推薦Google的Java編程風格指南。該指南最好的部分是編程實踐,絕對值得一讀。

                          Javadoc

                          文檔對使用你代碼的人來說是很重要的。這包括使用的實例及對變量、方法和類的有意義的描述等。

                          這個推論是如果不需要文檔說明的時候,不要寫文檔。如果對參數沒有什么需要說明,或者說明是模糊的,那就不要寫說明文檔。無意義的文檔比完全沒有文檔還要糟糕,因為它會誤導用戶以為這就是說明文檔。

                          Stream

                          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比較困難,框架可以幫助解決這個問題。兩個最好框架是DropwizardSpring BootPlay框架也是部署框架中的一種。

                          所有的這些框架都是為了降低代碼部署的門檻。它們尤其對Java新手或者需要快速完成某件事情有很好的幫助。單個jar包部署比復雜的WAR和EAR部署要更簡單。

                          但是,框架也從某種意義上降低了靈活性,不能自由發揮。所以,如果你的項目不適合于你選擇的框架,你將不得不遷移到一個更多手工操作的配置上去。

                          Maven

                          其它好的選擇:Gradle

                          Maven仍然是標準的構建、打包和運行測試工具。有其它可選的工具,例如Gradle,但是他們與maven采用的方式不同。如果你剛開始使用Maven,你應該從Maven實例開始。

                          我喜歡有一個根POM來包含所有的使用到的外部依賴。它看起來像這樣,只有這個根POM有外部依賴,但是你的產品非常大,可以有很多個模塊。你的根POM文件應該歸屬于這樣一個項目:有版本控制和發布,類似于其他的Java項目。

                          所有的Maven項目應該包含你的根POM及所有的版本信息。用這種方式,你能使你的公司使用的每個外部依賴和maven插件都是一致的。如果你需要加一個外部依賴,只要這樣就可以了:

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

                          如果你想使用內部依賴,你需要獨立的管理項目的各個部分。否則,這將比通過POM來管理穩定的版本更加困難。

                          重復依賴檢測

                          關于Java最好的一部分是存在大量的第三方庫可以做任何事情。本質上講每個API或者工具都是建立在Java SDK上的,所有很容易通過Maven獲取下來。

                          這些庫本身又會依賴指定版本的其它類庫。如果你下載下來夠多的庫,你就會遇到版本沖突,類似這樣:

                          Foo庫依賴Bar庫的1.0版
                          Widget庫依賴Bar庫的0.9版本

                          那到底哪個版本會拉到你的項目里面呢?

                          使用Maven依賴收斂插件,如果你的依賴使用到不同的版本,將會出現錯誤。你可以有兩種選擇來解決沖突:

                          1. 顯示的在依賴管理部分選擇一個Bar版本。
                          2. 從Foo或Widget中排除Bar。
                          3. </ol>

                            至于選擇哪種方式就要根據你的具體情況了:如果你想監測一個項目的版本,排除是有意義的。另一方面,如果你想顯示的知道是哪個版本,就應該自己選擇一個版本,雖然這樣當其他依賴更新的時候同時需要更新該依賴。

                            持續集成

                            很明顯,你需要一種持續集成的服務器,這樣能持續的為你構建SNAPSHOT版本和基于git標簽的標簽構建。

                            JenkinsTravis-CI是很自然的選擇。

                            代碼覆蓋率是很有用的,Cobertura有一個很好的Maven的插件,以及對CI的支持。還有一些其它Java檢查代碼覆蓋率的工具,但是我只用過Cobertura。

                            Maven倉庫

                            你需要一個地方來存放你打包的JAR、WAR及EAR,所以你需要一個倉庫:

                            通常的選擇是ArtifactoryNexus。兩個都還可以,各自有它們的優缺點

                            你應該自己安裝的 Artifactory/Nexus 服務器,并在上面建立依賴包的鏡像。這樣避免在構建你的項目時由于要從外部倉庫下載jar而被中斷。

                            配置管理

                            現在你的代碼已經編譯了,你的倉庫也已經建立了,這個時候你需要將你的代碼從開發環境推到生產環境。不要節省這個過程,因為這個過程的自動化將會獲得長久的好處。

                            ChefPuppetAnsible都是典型的選擇。也可以選擇我曾寫過的Squadron。當然,我想你應該測試一下,因為正確了解這些東西比選擇更容易。

                            盡管有很多工具可以選擇,但是不要忘記使你的部署自動化。

                            Java可能最好的特性是存在許多可擴展的庫。對大部分人的應用來說,用到的庫只是整個庫中的很小一部分。

                            缺失的特性

                            Java的標準庫,每向前一步都非常令人驚訝,不過就現在來看,還是缺乏幾個關鍵的特性。

                            Apache Commons

                            Apache Commons項目提供了許多有用的庫。

                            • Commons Codec提供了許多關于Base64和16進制字符串的編碼和解碼方法。不要浪費你的時間去重寫這些方法。
                            • Commons Lang提供了String類的創建、操作及字符集等一系列五花八門的工具方法。
                            • Commons IO擁有所有的你能想到的文件相關的方法。它有 FileUtils.copyDirectoryFileUtils.writeStringToFileIOUtils.readLines等方法。
                            • </ul>

                              Guava

                              Guava是Google旗下一個優秀庫,提供了Java現在缺乏的特性。很難提煉關于這個庫的所有東西,但是我盡量試試。

                              Cache提供一個簡單的方式來建立內存級別的緩存,這可以用來緩存網絡訪問、磁盤訪問以及內存函數或其它的東西。只需要實現CacheBuilder來告訴Guava怎樣創建你緩存,這就是你所有的設置。

                              Immutable集合,這里面有許多這種集合:ImmutableMapImmutableList甚至ImmutableSortedMultiSet

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

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

                              // You can use final Map<String, Widget> map = Maps.newHashMap(); There are static classes for Lists, Maps, Sets and more. They're cleaner and easier to read.</pre>

                              這個庫為ListsMapsSets等創建了靜態的類。它們讀起來更清晰、簡單。

                              如果你使用的是Java 6或7,你可以使用Collections2類,該類提供類似 filter 和 transform 的方法。這些方法允許你在沒有Java 8的stream的支持下寫出流式的代碼。

                              Guava也做一些簡單的事情,如Joiner,就是通過分隔符連接字符串以及中斷處理的類

                              Gson

                              Google的Gson庫是一個簡單快速處理JSON的庫,它類似如下工作:

                              final Gson gson = new Gson();
                              final String json = gson.toJson(fooWidget);

                              它使用起來確實很簡單令人愉悅。Gson用戶指南上有更多的例子。

                              Java Tuples

                              一件讓我很煩惱的事情是Java沒有內嵌建立元組的標準庫。幸運的是,Java tuples項目解決了這個問題。它使用簡單,而且效果很好:

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

                              Joda-Time

                              Joda-Time是我使用過的最簡單的時間庫。簡單、直接且易于測試。你還能有什么要求?

                              你唯一的要求如果沒使用Java 8,這樣的話你就不能使用最新的日期時間庫。

                              Lombok

                              Lombok是一個非常有趣的庫。通過注解,可以讓你減少模式化的代碼,這種模式化代碼讓Java看起很不友好。

                              想要為你變量生成 setters 和 getters 方法嗎?非常簡單:

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

                              現在,你可以這樣做了:

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

                              這個庫還有更多的功能,雖然我還沒有在生產環境中使用Lombok,但是我已經等不及要使用了。

                              Play框架

                              其它好的選擇:JerseySpark

                              Java中關于RESTful的Web Service存在兩個主要的陣營:JAX-RS和其它。

                              JAX-RS是傳統的方式。通過聯合注解和接口的實現的形式來實現Web Service,Jersey就是這樣實現的。這樣做的好處是可以很容易的建立客戶端,只需要一個實現接口的類就行了。

                              Play框架使用非常不同的方式在JVM上建立Web Service:你有一個遠程文件,然后你將一個類的引用寫入到這個遠程文件中。它的本質是一個完整的MVC框架,但是可以很簡單使用它來做Rest Web Service。它對Java和Scala都是有效的。它剛開始是用于Scala,但是在Java中同樣很好用。

                              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的全部功能。jUnit支持參數化的測試,支持一些規則可以讓你不需要寫那么多的模式化代碼,支持隨機測試指定代碼的思想,支持假設

                              jMock

                              如果你已經使用了依賴注入,這是你付出代價的地方:模擬出能夠產生副作用(類似于一個REST的服務器)的代碼,并聲明該代碼調用時的行為。

                              在Java里,jMock是一個標準的模擬工具,它類似于這樣:

                              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();
                              }
                              

                              }</pre>

                              這段代碼通過jMock建立一個 FooWidgetDependency 對象,然后增加期望值。我們期望dep的 call 方法一旦被同一個String調用,則dep的optionalCall 將被調用0次或多次。

                              如果你重復設置相同的依賴,可能需要將它們放到測試夾具(test fixture)中,并在@After夾具后添加assertIsSatisfied

                              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(&quot;some result&quot;, &quot;some other result&quot;)
                                                           .doesNotContain(&quot;shouldn&#039;t be here&quot;);

                              這個流式的接口使得你的測試可讀性更強。還有什么是你需要的?

                              工具

                              IntelliJ IDEA

                              其它好的選擇:EclipseNetbeans

                              最好的Java IDE是IntelliJ IDEA。它有很多的非常好的特性,確確實實的使得冗長的Java變得干脆利落。自動補全相當好用,檢測功能也極為強大以及重構工具也非常的有幫助。

                              免費的版本對我來說已經足夠,但是完整的版本存在更多優秀的特性,例如數據庫工具、Spring框架的支持以及Chronon。

                              Chronon

                              我最喜歡的GDB7功能是在調試時可以即時返回(travel back)。如果你使用的是“旗艦版”,可以通過Chronon IntelliJ插件使用這個功能。

                              你可以獲取歷史變量、向后的步驟、歷史方法等等。第一次使用的時候可能會覺得有點奇怪,但是它能幫助你調試出很多錯綜復雜的bug,Heisenbugs等工具與之類似的。

                              JRebel

                              持續集成通常的目標是軟件即服務的產品。但是,如果你想不需要等到產品構建完成的時候就能看到代碼的變化是什么?

                              這就是JRebel做的事情。一旦你將你的服務器掛上一個JRebel客戶端,你可以實時的觀察到服務器的變化。當你想快速試驗的時候這會給你節省大量的時間。

                              Checker框架

                              Java的類型系統是非常脆弱的。它區分不是字符串,而是正則表達式,也不做污點檢測(taint checking)。但是,Checker框架會這樣做,并且還有其他。它使用像 @Nullable 這樣的注解來做類型檢查。你甚至能自定義注解,使得靜態分析更加強大。

                              Eclipse Memory Analyzer

                              即使在Java中,內存泄露也是會發生的。幸運的是,有工具來檢測內存泄露。我用過的最好的工具是Eclipse Memory Analyzer,它能從heap dump文件幫助你發現問題。

                              有多種方式來獲取JVN進程的heap dump文件,我使用的是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

                              然后你就可以通過Memory Analyzer來打開heapdump.hprof文件,很快的發現發生了什么:

                              資源

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

                              書籍

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