注意:我正在編寫本指南的第二版,需要您的幫助!請使用此表格向我提供您認為下一版本應包含哪些內容的回饋。謝謝!
Java 是最受歡迎的程式語言之一,但似乎沒有人喜歡使用它。嗯,Java 實際上是一種不錯的程式語言,自從 Java 8 最近發布以來,我決定編譯一個函式庫、實作和工具列表,以便更好地使用 Java。 「更好」是主觀的,所以我建議選擇對你有用的部分並使用它們,而不是嘗試一次使用所有它們。請隨意提交拉取請求建議添加內容。
這篇文章最初發佈在我的部落格上。
閱讀其他語言版本:英文、簡體中文
傳統上,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,這將在有關不變性的部分中討論。
如果您想要為其構建結構的相當複雜的對象,請考慮構建器模式。
您建立一個靜態內部類別來建構您的物件。它使用可變狀態,但是一旦您呼叫 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 編碼模式的一些問題的有趣討論。
一些出色的程式碼產生庫是不可變的、Google 的自動值和 Lombok。
如果需要的話,應謹慎使用受檢查的異常。它們迫使您的用戶添加許多 try/catch 區塊並將您的異常包裝在自己的異常中。更好的方法是讓您的異常擴展 RuntimeException。這允許您的用戶以他們想要的方式處理您的異常,而不是強迫他們每次處理/聲明它都會拋出,這會污染程式碼。
一個巧妙的技巧是將 RuntimeExceptions 放入方法的 throws 宣告中。這對編譯器沒有影響,但會透過文件通知使用者可以拋出這些異常。
這更像是軟體工程部分,而不是 Java 部分,但編寫可測試軟體的最佳方法之一是使用依賴項注入 (DI)。因為Java強烈鼓勵OO設計,所以要製作可測試的軟體,就需要使用DI。
在 Java 中,這通常是透過 Spring 框架完成的。它具有基於程式碼的連接或基於 XML 配置的連接。如果您使用 XML 配置,請務必不要過度使用 Spring,因為它是基於 XML 的配置格式。 XML 中絕對不應該有任何邏輯或控制結構。它應該只注入依賴項。
使用 Spring 的良好替代方案是 Google 和 Square 的 Dagger 庫或 Google 的 Guice。他們不使用Spring的XML設定檔格式,而是將注入邏輯放在註解和程式碼中。
盡可能避免使用空值。當您應該返回空集合時,不要返回空集合。如果要使用 null,請考慮 @Nullable 註解。 IntelliJ IDEA 內建了 @Nullable 註解的支援。
在計算機科學中最嚴重的錯誤中閱讀有關為什麼不使用空值的更多資訊。
如果您使用 Java 8,則可以使用出色的新可選類型。如果一個值可能存在也可能不存在,請將其包裝在一個可選類別中,如下所示:
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的唯一缺點是標準庫沒有良好的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 類別。它們具有建構器,以便您可以動態建置它們,然後透過呼叫建置方法將它們標記為不可變。
應該透過聲明欄位不可變(透過final )和使用不可變集合來使類別不可變。或者,您可以將類別本身設定為最終類,以便它無法擴展或可變。
如果您發現自己在 Util 類別中添加了很多方法,請務必小心。
public class MiscUtil {
public static String frobnicateString ( String base , int times ) {
// ... etc
}
public static void throwIfCondition ( boolean condition , String msg ) {
// ... etc
}
}
這些類別乍看之下很有吸引力,因為其中的方法並不真正屬於任何一個地方。所以你以程式碼重用的名義把它們全部丟在這裡。
治療方法比疾病本身更糟糕。將這些類別放在它們所屬的位置並積極重構。不要將類別、套件或函式庫命名得太通用,例如“MiscUtils”或“ExtrasLibrary”。這鼓勵在那裡轉儲不相關的程式碼。
格式化並不像大多數程式設計師想的那麼重要。一致性是否表明你關心你的手藝並且它是否有助於其他人閱讀?絕對地。但是,我們不要浪費一天的時間在 if 區塊中添加空格以使其「匹配」。
如果您絕對需要程式碼格式化指南,我強烈推薦 Google 的 Java 樣式指南。該指南最好的部分是程式設計實踐部分。絕對值得一讀。
記錄面向使用者的程式碼很重要。這意味著使用範例並使用變數、方法和類別的合理描述。
這樣做的必然結果是不記錄不需要記錄的內容。如果您對參數是什麼沒有什麼可說的,或者如果它很明顯,請不要記錄它。樣板文件比根本沒有文件更糟糕,因為它會欺騙使用者認為有文件。
Java 8 有一個很好的串流和 lambda 語法。你可以寫出這樣的程式碼:
final List < String > filtered = list . stream ()
. filter ( s -> s . startsWith ( "s" ))
. map ( s -> s . toUpperCase ())
. collect ( Collectors . toList ());
而不是這個:
final List < String > filtered = new ArrayList <>();
for ( String str : list ) {
if ( str . startsWith ( "s" ) {
filtered . add ( str . toUpperCase ());
}
}
這使您可以編寫更流暢的程式碼,更具可讀性。
正確部署 Java 可能有點棘手。如今部署 Java 有兩種主要方法:使用框架或使用更靈活的自行開發的解決方案。
由於部署 Java 並不容易,因此已經開發了一個可以提供幫助的框架。其中最好的兩個是 Dropwizard 和 Spring Boot。 Play 框架也可以被視為這些部署框架之一。
他們所有人都試圖降低程式碼發布的障礙。如果您是 Java 新手或需要快速完成工作,它們會特別有用。單一 JAR 部署比複雜的 WAR 或 EAR 部署更容易。
然而,它們可能有些不靈活且相當固執己見,因此如果您的專案不適合框架開發人員所做的選擇,您將不得不遷移到更手動的配置。
不錯的選擇:Gradle。
Maven 仍然是建置、打包和運行測試的標準工具。還有其他替代方案,例如 Gradle,但它們的採用程度不如 Maven。如果您是 Maven 新手,您應該從 Maven by Examples 開始。
我喜歡擁有一個包含您想要使用的所有外部依賴項的根 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 依賴聚合插件,如果您的依賴項不使用相同的版本,建置將會出錯。然後,您有兩種解決衝突的選擇:
選擇哪一個取決於您的情況:如果您想追蹤一個專案的版本,那麼排除是有意義的。另一方面,如果您想明確說明它,您可以選擇一個版本,儘管您在更新其他依賴項時需要更新它。
顯然,您需要某種持續整合伺服器,它將持續建立您的快照版本和基於 git 標籤的標籤建置。
Jenkins 和 Travis-CI 是自然的選擇。
程式碼覆蓋率很有用,Cobertura 有良好的 Maven 插件和 CI 支援。還有其他適用於 Java 的程式碼覆蓋率工具,但我使用了 Cobertura。
您需要一個地方來放置您製作的 JAR、WAR 和 EAR,因此您需要一個儲存庫。
常見的選擇是 Artifactory 和 Nexus。兩者都有效,而且各有優缺點。
您應該有自己的 Artifactory/Nexus 安裝並將您的依賴項鏡像到它上面。這將阻止您的建置因某些上游 Maven 儲存庫崩潰而中斷。
現在您已經編譯了程式碼,設定了儲存庫,並且需要將程式碼放入開發環境中並最終將其投入生產。不要在這裡吝嗇,因為自動化將在很長一段時間內帶來紅利。
Chef、Puppet 和 Ansible 是典型的選擇。我寫了一個名為 Squadron 的替代方案,當然,我認為您應該檢查一下,因為它比其他替代方案更容易正確。
無論您選擇什麼工具,都不要忘記自動化部署。
Java 的最佳特性可能是它擁有大量的函式庫。這是一小部分庫,可能適用於最大的人群。
Java 的標準函式庫曾經向前邁出了驚人的一步,但現在看起來缺少幾個關鍵功能。
Apache Commons 專案有很多有用的函式庫。
Commons Codec對於 Base64 和十六進位字串有許多有用的編碼/解碼方法。不要浪費時間重寫這些。
Commons Lang是字串操作和建立、字元集以及一堆雜項實用方法的首選庫。
Commons IO擁有您可能想要的所有與文件相關的方法。它有 FileUtils.copyDirectory、FileUtils.writeStringToFile、IOUtils.readLines 等等。
Guava 是 Google 的一個優秀的 Java 缺失庫。幾乎很難提煉出這個庫中我喜歡的所有內容,但我會嘗試。
快取是一種獲取記憶體快取的簡單方法,可用於快取網路存取、磁碟存取、記憶功能或任何其他內容。只需實作一個 CacheBuilder 來告訴 Guava 如何建立緩存,就可以了!
不可變的集合。其中有很多:ImmutableMap、ImmutableList,甚至 ImmutableSortedMultiSet(如果這是您的風格)。
我也喜歡用 Guava 方式寫可變集合:
// Instead of
final Map < String , Widget > map = new HashMap <>();
// You can use
final Map < String , Widget > map = Maps . newHashMap ();
列表、映射、集合等都有靜態類別。它們更乾淨、更容易閱讀。
如果您無法使用 Java 6 或 7,可以使用 Collections2 類,它具有過濾和轉換等方法。它們允許您在沒有 Java 8 流暢的情況下編寫流暢的程式碼。
Guava 也有簡單的東西,例如一個連接分隔符號上的字串的Joiner和一個透過忽略中斷來處理中斷的類別。
Google的Gson函式庫是一個簡單快速的JSON解析庫。它的工作原理如下:
final Gson gson = new Gson ();
final String json = gson . toJson ( fooWidget );
final FooWidget newFooWidget = gson . fromJson ( json , FooWidget . class );
與它一起工作真的很容易並且很愉快。 Gson 使用者指南還有更多範例。
我對 Java 的持續煩惱之一是它沒有內建在標準庫中的元組。幸運的是,Java 元組專案解決了這個問題。
它使用簡單並且效果很好:
Pair < String , Integer > func ( String input ) {
// something...
return Pair . with ( stringResult , intResult );
}
Javaslang 是一個函數庫,旨在添加本應屬於 Java 8 一部分的缺失功能。
有幾個 Java 函式庫依賴原始 Java 集合。這些僅限於與以物件導向為重點創建並設計為可變的類別相容。 Java 的 Javaslang 集合是一個全新的版本,其靈感來自 Haskell、Clojure 和 Scala。它們的創建以功能為重點,並遵循不可變的設計。
像這樣的程式碼自動是線程安全的並且無需 try-catch:
// Success/Failure containing the result/exception
public static Try < User > getUser ( int userId ) {
return Try . of (() -> DB . findUser ( userId ))
. recover ( x -> Match . of ( x )
. whenType ( RemoteException . class ). then ( e -> ...)
. whenType ( SQLException . class ). then ( e -> ...));
}
// Thread-safe, reusable collections
public static List < String > sayByeBye () {
return List . of ( "bye, " bye ", "collect" , "mania" )
. map ( String :: toUpperCase )
. intersperse ( " " );
}
Joda-Time 無疑是我用過的最好的時間庫。簡單、直接、易於測試。你還能要求什麼?
只有當您尚未使用 Java 8 時才需要它,因為 Java 8 有自己的新時間庫,而且還不錯。
龍目島是一個有趣的圖書館。透過註釋,它可以讓您減少 Java 所遭受的嚴重的樣板檔案。
想要類別變數的 setter 和 getter 嗎?簡單的:
public class Foo {
@ Getter @ Setter private int var ;
}
現在你可以這樣做:
final Foo foo = new Foo ();
foo . setVar ( 5 );
還有更多。我還沒有在生產中使用 Lombok,但我已經等不及了。
不錯的選擇:Jersey 或 Spark
使用 Java 實作 RESTful Web 服務有兩個主要陣營:JAX-RS 和其他。
JAX-RS是傳統方式。您可以使用 Jersey 之類的工具將註解與介面和實作結合起來形成 Web 服務。這樣做的好處是您可以輕鬆地僅使用介面類別來建立客戶端。
Play 框架與 JVM 上的 Web 服務完全不同:您有一個路由文件,然後編寫這些路由中引用的類別。它實際上是一個完整的 MVC 框架,但您可以輕鬆地將它用於 REST Web 服務。
它適用於 Java 和 Scala。它因 Scala 優先而受到一些影響,但在 Java 中使用它仍然很好。
如果您習慣使用 Python 中的 Flask 等微框架,那麼 Spark 將會非常熟悉。它與 Java 8 配合得特別好。
有很多 Java 日誌記錄解決方案。我最喜歡的是 SLF4J,因為它具有極高的可插入性,並且可以同時組合許多不同日誌框架的日誌。有一個使用 java.util.logging、JCL 和 log4j 的奇怪專案嗎? SLF4J 適合您。
兩頁的手冊幾乎是您入門所需的全部內容。
我不喜歡沉重的 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 模式,您可以使資料庫存取變得輕而易舉。
測試對於您的軟體至關重要。這些軟體包有助於使事情變得更容易。
不錯的選擇:TestNG。
jUnit 無需介紹。它是 Java 單元測試的標準工具。
但您可能沒有充分利用 jUnit 的潛力。 jUnit 支援參數化測試、阻止您編寫大量樣板檔案的規則、隨機測試某些程式碼的理論和假設。
如果您已經完成了依賴注入,那麼這就是它的回報:模擬具有副作用的程式碼(例如與 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 ,然後加入期望。我們期望dep的call方法將使用某個 String 呼叫一次,並且dep的optionalCall方法將被呼叫零次或多次。
如果您必須一遍又一遍地設定相同的依賴項,您可能應該將其放入測試裝置中,並將assertIsSatisfied放入@After裝置中。
你有用過 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" );
這種流暢的介面使您的測試更具可讀性。你還想要什麼?
不錯的選擇:Eclipse 和 Netbeans
最好的 Java IDE 是 IntelliJ IDEA。它具有大量令人敬畏的功能,並且確實是使 Java 的冗長變得可以忍受的主要因素。自動完成功能很棒,檢查是一流的,重構工具確實很有幫助。
免費社群版對我來說已經足夠好了,但終極版中有很多很棒的功能,例如資料庫工具、Spring 框架支援和 Chronon。
我最喜歡的 GDB 7 功能之一是在調試時能夠回到過去。當您獲得終極版時,可以使用 Chronon IntelliJ 外掛程式來實現這一點。
您可以獲得變數歷史記錄、後退、方法歷史記錄等等。第一次使用有點奇怪,但它可以幫助調試一些非常複雜的錯誤,Heisenbugs之類的。
不錯的選擇:DCEVM
持續整合通常是軟體即服務產品的目標。如果您甚至不需要等待建置完成即可即時查看程式碼變更怎麼辦?
這就是 JRebel 所做的。將伺服器連接到 JRebel 用戶端後,您可以立即看到伺服器上的變更。當您想快速進行實驗時,這可以節省大量時間。
Java 的型別系統相當弱。它不區分字串和實際上是正規表示式的字串,也不進行任何污點檢查。然而,Checker 框架可以做到這一點以及更多。
它使用@Nullable等註解來檢查類型。您甚至可以定義自己的註釋,使靜態分析變得更強大。
即使遵循最佳實踐,即使是最好的開發人員也會犯錯。您可以使用許多工具來驗證 Java 程式碼以檢測程式碼中的問題。以下是一些最受歡迎工具的一小部分選擇。其中許多與流行的 IDE(例如 Eclipse 或 IntelliJ)集成,使您能夠更快地發現程式碼中的錯誤。
除了在開發過程中使用這些工具之外,在建置階段運行它們通常也是一個好主意。它們可以綁定到 Maven 或 Gradle 等建置工具,也可以綁定到持續整合工具。
即使在 Java 中,也會發生記憶體洩漏。幸運的是,有一些工具可以做到這一點。我用來解決這些問題的最佳工具是 Eclipse Memory Analyzer。它需要進行堆轉儲並讓您找到問題。
有幾種方法可以取得 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 高手的資源。