簡潔Java之道
計算機專家在問題求解時非常重視表達式簡潔性的價值。Unix的先驅者Ken Thompson曾經說過非常著名的一句話:“丟棄1000行代碼的那一天是我最有成效的一天之一。”這對于任何一個需要持續支持和維護的軟件項目來說,都是一個當之無愧的目標。早期的Lisp貢獻者Paul Graham甚至將語言的 簡潔性等同 為語言的能力。這種對能力的認識讓可以編寫緊湊、簡介的代碼成為許多現代軟件項目選擇語言的首要標準。
任何程序都可以通過重構,去除多余的代碼或無用的占位符,如空格,變得更加簡短,不過某些語言天生就善于表達,也就特別適合于簡短程序的編寫。認識到這一點之后,Perl程序員普及了 代碼高爾夫 競賽;其目標是用盡可能短的代碼量解決某一特定的問題或者實現某個指定的算法。APL語言的設計理念是利用特殊的圖形符號讓程序員用很少量的代碼就可以編寫功能強大的程序。這類程序如果實現得當,可以很好地映射成標準的數學表達式。簡潔的語言在快速創建小腳本時非常高效,特別是在目的不會被簡潔所掩蓋的簡潔明確的問題域中。
相比于其他程序設計語言,Java語言的冗長已經名聲在外。其主要原因是由于程序開發社區中所形成的慣例,在完成任務時,很多情況下,要更大程度地考慮描述性和控制。例如,長期來看,長變量名會讓大型代碼庫的可讀性和可維護性更強。描述性的類名通常會映射為文件名,在向已有系統中增加新功能時,會顯得很清晰。如果能夠一直堅持下去,描述性名稱可以極大簡化用于表明應用中某一特定的功能的文本搜索。這些實踐讓Java在大型復雜代碼庫的大規模實現中取得了極大的成功。
對于小型項目來說,簡潔性則更受青睞,某些語言非常適于短腳本編寫或者在命令提示符下的交互式探索編程。Java作為通用性語言,則更適用于編寫跨平臺的工具。在這種情況下,“冗長Java”的使用并不一定能夠帶來額外的價值。雖然在變量命名等方面,代碼風格可以改變,不過從歷史情況來看,在一些基本的層面上,與其他語言相比,完成同樣的任務,Java語言仍需更多的字符。為了應對這些限制,Java語言一直在不斷地更新,以包含一些通常稱為“ 語法糖 ”的功能。用這些習語可以實現更少的字符表示相同功能的目標。與其對應的更加冗長的配對物相比,這些習語更受程序開發社區的歡迎,通常會被社區作為通用用法快速地采用。
本文將著重介紹編寫簡潔Java代碼的最佳實踐,特別是關于JDK8中新增的功能。簡而言之,Java 8中 Lambda表達式 的引入讓更加優雅的代碼成為可能。這在用新的Java Streaming API處理集合時尤其明顯。
冗長的Java
Java代碼冗長之所以名聲在外,一部分原因是由于其面向對象的實現風格。在許多語言中,只需要一行包含不超過20個字符的代碼就可以實現經典的“Hello World”程序示例。而在Java中,除了需要類定義中所包含的main方法之外,在main方法中還需要包含一個方法調用,通過System.out.println()將字符串打印到終端。即使在使用最少的方法限定詞、括號和分號,并且將所有空格全都刪除的極限情況下,“Hello World”程序最少也需要86個字符。為了提高可讀性,再加上空格和縮進,毋庸置疑,Java版的“Hello World”程序給人的第一印象就是冗長。
Java代碼冗長一部分原因還應歸咎于Java社區將描述性而非簡潔性作為其標準。就這一點而言,選擇與代碼格式美學相關的不同標準是無關緊要的。此外,樣板代碼的方法和區段可以包含在整合到API中的方法中。無需犧牲準確性或清晰度,著眼于簡潔性的程序代碼重構可以大大簡化冗余Java代碼。
有些情況下,Java代碼冗長之所以名聲在外是由于大量的老舊代碼示例所帶來的錯覺。許多關于Java的書籍寫于多年之前。由于在整個萬維網最初興起時,Java便已經存在,許多Java的在線資源所提供的代碼片段都源自于Java語言最早的版本。隨著時間的推移,一些可見的問題和不足不斷得到完善,Java語言也日趨成熟,這也就導致即使十分準確并實施的當的案例,可能也未能有效利用后來的語言習語和API。
Java的設計目標 包括面向對象、易于上手(在當時,這意味著使用C++格式的語法),健壯、安全、可移植、多線程以及高性能。簡潔并非其中之一。相比于用面向對象語法實現的任務,函數式語言所提供的替代方案要簡潔的多。 Java 8中新增的Lambda表達式 改變了Java的表現形式,減少了執行許多通用任務所需的代碼數量,為Java開啟了函數式編程習語的大門。
函數式編程
函數式編程將函數作為程序開發人員的核心結構。開發人員可以以一種非常靈活的方式使用函數,例如將其作為參數傳遞。利用Lambda表達式的這種能力,Java可以將函數作為方法的參數,或者將代碼作為數據。Lambda表達式可以看作是一個與任何特定的類都無關的匿名方法。這些理念有著非常豐富多彩并且引人入勝的數學基礎。
函數式編程和Lambda表達式仍然是比較抽象、深奧的概念。對于開發人員來說,主要關注如何解決實際生產中的任務,對于跟蹤最新的計算趨勢可能并不感興趣。隨著Lambda表達式在Java中的引入,對于開發人員來說對這些新特性的了解至少需要能夠達到可以讀懂其他開發人員所編寫代碼的程度。這些新特性還能帶來實際的好處——可以影響并發系統的設計,使其擁有更優的性能。而本文所關心的是如何利用這些機制編寫簡潔而又清晰的代碼。
之所以能夠用Lambda表達式生成簡潔的代碼,有如下幾個原因。局部變量的使用量減少,因此聲明和賦值的代碼也隨之減少。循環被方法調用所替代,從而將三行以上的代碼縮減為一行。本來在嵌套循環和條件語句中的代碼現在可以放置于一個單獨的方法中。實現 連貫接口 ,可以將方法以類似于Unix管道的方式鏈接在一起。以函數式的風格編寫代碼的凈效應并不只限于可讀性。此類代碼可以避免狀態維護并且不會產生副作用。這種代碼還能夠產生易于并行化,提高處理效率的額外收益。
Lambda 表達式
與Lambda表達式相關的語法比較簡單直白,不過又有別于Java之前版本的習語。一個Lambda表達式由三部分組成,參數列表、箭頭和主體。參數列表可以包含也可以不包含括號。此外還新增了由雙冒號組成的相關操作符,可以進一步縮減某些特定的Lambda表達式所需的代碼量。這又稱為 方法引用 。
線程創建
在這個示例中,將會創建并運行一個線程。Lambda表達式出現在賦值操作符的右側,指定了一個空的參數列表,以及當線程運行時寫到標準輸出的簡單的消息輸出。
Runnable r1 = () -> System.out.print("Hi!"); r1.run()
箭頭 |
主體 |
|
() |
-> |
System.out.print("Hi!"); |
處理集合
Lambda表達式的出現會被開發人員注意到的 首要位置 之一就是與集合API相關。假設我們需要將一個字符串列表根據長度排序。
java.util.List<String> l; l= java.util.Arrays.asList(new String[]{"aaa", "b", "cccc", "DD"});
可以創建一個Lambda表達式實現此功能。
java.util.Collections.sort(l, (s1, s2) -> new Integer(s1.length()). compareTo(s2.length())
這個示例中包含兩個傳遞給Lambda表達式體的參數,以比較這兩個參數的長度。
參數列表 |
箭頭 |
主體 |
(s1, s2) |
-> |
new Integer(s1.length()). compareTo(s2.length())); |
除此之外還有許多替代方案,在無需使用標準的“for”或“while”循環的前提下,就可以操作列表中的各個元素。通過向集合的“forEach”方法傳入Lambda表達式也可以完成用于比較的語義。這種情況下,只有一個參數傳入,也就無需使用括號。
l.forEach(e -> System.out.println(e));
Argument List |
Arrow |
Body |
e |
-> |
System.out.println(e) |
這個特殊的示例還可以通過使用方法引用將包含類和靜態方法分開的方式進一步減少代碼量。每個元素都會按順序傳入println方法。
l.forEach(System.out::println)
java.util.stream 是在Java 8中新引入的包,以函數式程序開發人員所熟悉的語法處理集合。在包的摘要中對包中的內容解釋如下:“為流元素的函數式操作提供支持的類,如對集合的map-reduce轉換。”
下方的類圖提供了對該包的一個概覽,著重介紹了接下來的示例中將要用到的功能。包結構中列示了大量的Builder類。這些類與 連貫接口 一樣,可以將方法鏈接成為管道式的操作集。
字符串解析和集合處理雖然簡單,在真實世界中仍有許多實際應用場景。在進行自然語言處理(NLP)時,需要將句子分割為單獨的詞。生物信息學將DNA和RNA表示為有字母組成的堿基,如C,G,A,T或U。在每個問題領域中,字符串對象會被分解,然后針對其各個組成部分進行操作、過濾、計數以及排序等操作。因此,盡管示例中所包含的用例十分簡單,其理念仍適用于各類有實際意義的任務。
下方的示例代碼解析了一個包含一個句子的字符串對象,并統計單詞的數量和感興趣的字母。包括空白行在內,整個代碼清單的行數不超過70行。
1. import java.util.*; 2. 3. import static java.util.Arrays.asList; 4. import static java.util.function.Function.identity; 5. import static java.util.stream.Collectors.*; 6. 7. public class Main { 8. 9. public static void p(String s) { 10. System.out.println(s.replaceAll("[\\]\\[]", "")); 11. } 12. 13. private static Listuniq(List letters) { 14. return new ArrayList (new HashSet (letters)); 15. } 16. 17. private static List sort(List letters) { 18. return letters.stream().sorted().collect(toList()); 19. } 20. 21. private static Map uniqueCount(List letters) { 22. return letters. stream(). 23. collect(groupingBy(identity(), counting())); 24. } 25. 26. private static String getWordsLongerThan(int length, List words) { 27. return String.join(" | ", words 28. .stream().filter(w -> w.length() > length) 29. .collect(toList()) 30. ); 31. } 32. 33. private static String getWordLengthsLongerThan(int length, List words) 34. { 35. return String.join(" | ", words 36. .stream().filter(w -> w.length() > length) 37. .mapToInt(String::length) 38. .mapToObj(n -> String.format("%" + n + "s", n)) 39. .collect(toList())); 40. } 41. 42. public static void main(String[] args) { 43. 44. String s = "The quick brown fox jumped over the lazy dog."; 45. String sentence = s.toLowerCase().replaceAll("[^a-z ]", ""); 46. 47. List words = asList(sentence.split(" ")); 48. List letters = asList(sentence.split("")); 49. 50. p("Sentence : " + sentence); 51. p("Words : " + words.size()); 52. p("Letters : " + letters.size()); 53. 54. p("\nLetters : " + letters); 55. p("Sorted : " + sort(letters)); 56. p("Unique : " + uniq(letters)); 57. 58. Map m = uniqueCount(letters); 59. p("\nCounts"); 60. 61. p("letters"); 62. p(m.keySet().toString().replace(",", "")); 63. p(m.values().toString().replace(",", "")); 64. 65. p("\nwords"); 66. p(getWordsLongerThan(3, words)); 67. p(getWordLengthsLongerThan(3, words)); 68. } 69. }
示例程序執行輸出:
Sentence : the quick brown fox jumped over the lazy dog Words : 9 Letters : 44 Letters : t, h, e, , q, u, i, c, k, , b, r, o, w, n, , f, o, x, , j, u, m, p, e, d, , o, v, e, r, , t, h, e, , l, a, z, y, , d, o, g Sorted : , , , , , , , , a, b, c, d, d, e, e, e, e, f, g, h, h, i, j, k, l, m, n, o, o, o, o, p, q, r, r, t, t, u, u, v, w, x, y, z Unique : , a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, t, u, v, w, x, y, z Counts letters a b c d e f g h i j k l m n o p q r t u v w x y z 8 1 1 1 2 4 1 1 2 1 1 1 1 1 1 4 1 1 2 2 2 1 1 1 1 1 words quick | brown | jumped | over | lazy 5 | 5 | 6 | 4 | 4
上述代碼已經經過了多重精簡。其中一些方式并非在各個版本的Java中都可行,而且有些方式可能并不符合公認的編碼風格指南。思考一下在較早版本的Java中如何才能夠獲得相同的輸出?首先,需要創建許多局部變量用于臨時存儲數據或作為索引。其次,需要通過許多條件語句和循環告知Java如何處理數據。新的函數式編程方式更加專注于需要什么數據,而并不關心與其相關的臨時變量、嵌套循環、索引管理或條件語句的處理。
在某些情況下,采用早期版本中的標準Java語法以減少代碼量是以犧牲清晰度為代價的。例如,示例代碼第一行的標準import語句中的Java包引用了java.util下的所有類,而不是根據類名分別引用。對System.out.println的調用被替換為對一個名為p的方法的調用,這樣在每次方法調用時都可以使用短名稱(行9-11)。由于可能違反某些Java的編碼規范,這些改變富有爭議,不過有著其他背景的程序開發人員查看這些代碼時可能并不會有何問題。
另外一些情況下,則利用了從JDK8預覽版才新增的功能特性。靜態引用(行3-5)可以減少內聯所需引用的類的數量。而正則表達式(行10,45)則可以用與函數式編程本身無關的方式,有效隱藏循環和條件語句。這些習語,特別是正則表達式的使用,經常會因為難以閱讀和說明而受到質疑。如果運用得當,這些習語可以減少噪音的數量,并且能夠限制開發人員需要閱讀和說明的代碼數量。
最后,示例代碼利用了JDK 8中新增的Streaming API。使用了Streaming API中大量的方法對列表進行過濾、分組和處理(行17-40)。盡管在IDE中它們與內附類的關聯關系很清晰,不過除非你已經很熟悉這些API,否則這種關系并不是那么顯而易見。下表展示了示例代碼中所出現的每一次方法調用的來源。
方法 |
完整的方法名稱引用 |
stream() |
java.util.Collection.stream() |
sorted() |
java.util.stream.Stream.sorted() |
collect() |
java.util.stream.Stream.collect() |
toList() |
java.util.stream.Collectors.toList() |
groupingBy() |
java.util.stream.Collectors.groupingBy() |
identity() |
java.util.function.Function.identity() |
counting() |
java.util.stream.Collectors.counting() |
filter() |
java.util.stream.Stream.filter() |
mapToInt() |
java.util.stream.Stream.mapToInt() |
mapToObject() |
java.util.stream.Stream.mapToObject() |
uniq()(行13)和sort()(行17)方法體現了同名的Unix實用工具的功能。sort引入了對流的第一次調用,首先對流進行排序,然后再將排序后的結果收集到列表中。UniqueCount()(行21)與uniq -c類似,返回一個map對象,其中每個鍵是一個字符,每個值則是這個字符出現次數的統計。兩個“getWords”方法(行26和行33)用于過濾出比給定長度短的單詞。getWordLengthsLongerThan()方法調用了一些額外的方法,將結果格式化并轉換成不可修改的String對象。
整段代碼并未引入任何與Lambda表達式相關的新概念。之前所介紹的語法只適用于Java Stream API特定的使用場景。
總結
用更少的代碼實現同樣任務的理念與愛因斯坦的理念一致:“必須盡可能地簡潔明了,但又不能簡單地被簡單。”Lambda表達式和新的Stream API因其能夠實現擴展性良好的簡潔代碼而備受關注。它們讓程序開發人員可以恰當地將代碼簡化成最好的表現形式。
函數式編程習語的設計理念就是簡短,而且仔細思考一下就會發現許多可以讓Java代碼更加精簡的場景。新的語法雖然有點陌生但并非十分復雜。這些新的功能特性清晰地表明,作為一種語言,Java已經遠遠超越其最初的目標。它正在用開放的態度接受其他程序設計語言中最出色的一些功能,并將它們整合到Java之中。
關于作者
在過去的15年中,Casimir Saternos曾擔任過軟件開發人員、數據庫管理員以及軟件架構師。他最近 編寫 并創建了一個R程序設計語言的 截屏視頻 。他曾經在Java雜志和Oracle技術網絡上發表過多篇文章。他還是 O'Reilly Media 上 Client-Server Web Apps with JavaScript and Java 的作者。
查看英文原文:Concise Java