Java 8里面lambda的最佳實踐
Java 8已經推出一段時間了,越來越多開發人員選擇升級JDK,這條熱門動彈里面看出,JDK7最多,其次是6和8,這是好事!
在8 里面Lambda是最火的主題,不僅僅是因為語法的改變,更重要的是帶來了函數式編程的思想,我覺得優秀的程序員,有必要學習一下函數式編程的思想以開闊思路。所以這篇文章聊聊Lambda的應用場景,性能,也會提及下不好的一面。
Java為何需要Lambda
1996年1月,Java 1.0發布了,此后計算機編程領域發生了翻天覆地的變化。商業發展需要更復雜的應用,大多數程序都跑在更強大的裝備多核CPU的機器上。帶有高效運行期編 譯器的Java虛擬機(JVM)的出現,使得程序員將精力更多放在編寫干凈、易于維護的代碼上,而不是思考如何將每一個CPU時鐘、每一字節內存物盡其 用。
多核CPU的出現成了“房間里的大象”,無法忽視卻沒人愿意正視。算法中引入鎖不但容易出錯,而且消耗時間。人們開發了 java.util.concurrent包和很多第三方類庫,試圖將并發抽象化,用以幫助程序員寫出在多核CPU上運行良好的程序。不幸的是,到目前為 止,我們走得還不夠遠。
那些類庫的開發者使用Java時,發現抽象的級別還不夠。處理大數據就是個很好的例子,面對大數據,Java還欠缺高效的并行操作。Java 8允許開發者編寫復雜的集合處理算法,只需要簡單修改一個方法,就能讓代碼在多核CPU上高效運行。為了編寫并行處理這些大數據的類庫,需要在語言層面上 修改現有的Java:增加lambda表達式。
當然,這樣做是有代價的,程序員必須學習如何編寫和閱讀包含lambda表達式的代碼,但是,這不是一樁賠本的買賣。與手寫一大段復雜的、線程安全 的代碼相比,學習一點新語法和一些新習慣容易很多。開發企業級應用時,好的類庫和框架極大地降低了開發時間和成本,也掃清了開發易用且高效的類庫的障礙。
如果你還未接觸過Lambda的語法,可以看這里。
Lambda的應用場景
你有必要學習下函數式編程的概念,比如函數式編程初探,但下面我將重點放在函數式編程的實用性上,包括那些可以被大多數程序員理解和使用的技術,我們關心的如何寫出好代碼,而不是符合函數編程風格的代碼。
1.使用() -> {} 替代匿名類
現在Runnable線程,Swing,JavaFX的事件監聽器代碼等,在java 8中你可以使用Lambda表達式替代丑陋的匿名類。
- //Before Java 8:
- new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("Before Java8 ");
- }
- }).start();
- //Java 8 way:
- new Thread(() -> System.out.println("In Java8!"));
- // Before Java 8:
- JButton show = new JButton("Show");
- show.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- System.out.println("without lambda expression is boring");
- }
- });
- // Java 8 way:
- show.addActionListener((e) -> {
- System.out.println("Action !! Lambda expressions Rocks");
- });
2.使用內循環替代外循環
外循環:描述怎么干,代碼里嵌套2個以上的for循環的都比較難讀懂;只能順序處理List中的元素;
內循環:描述要干什么,而不是怎么干;不一定需要順序處理List中的元素
- //Prior Java 8 :
- List features = Arrays.asList("Lambdas", "Default Method",
- "Stream API", "Date and Time API");
- for (String feature : features) {
- System.out.println(feature);
- }
- //In Java 8:
- List features = Arrays.asList("Lambdas", "Default Method", "Stream API",
- "Date and Time API");
- features.forEach(n -> System.out.println(n));
- // Even better use Method reference feature of Java 8
- // method reference is denoted by :: (double colon) operator
- // looks similar to score resolution operator of C++
- features.forEach(System.out::println);
- Output:
- Lambdas
- Default Method
- Stream API
- Date and Time API
3.支持函數編程
為了支持函數編程,Java 8加入了一個新的包java.util.function,其中有一個接口java.util.function.Predicate是支持Lambda函數編程:
- public static void main(args[]){
- List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
- System.out.println("Languages which starts with J :");
- filter(languages, (str)->str.startsWith("J"));
- System.out.println("Languages which ends with a ");
- filter(languages, (str)->str.endsWith("a"));
- System.out.println("Print all languages :");
- filter(languages, (str)->true);
- System.out.println("Print no language : ");
- filter(languages, (str)->false);
- System.out.println("Print language whose length greater than 4:");
- filter(languages, (str)->str.length() > 4);
- }
- public static void filter(List names, Predicate condition) {
- names.stream().filter((name) -> (condition.test(name)))
- .forEach((name) -> {System.out.println(name + " ");
- });
- }
- Output:
- Languages which starts with J :
- Java
- Languages which ends with a
- Java
- Scala
- Print all languages :
- Java
- Scala
- C++
- Haskell
- Lisp
- Print no language :
- Print language whose length greater than 4:
- Scala
- Haskell
4.處理數據?用管道的方式更加簡潔
Java 8里面新增的Stream API ,讓集合中的數據處理起來更加方便,性能更高,可讀性更好
假設一個業務場景:對于20元以上的商品,進行9折處理,最后得到這些商品的折后價格。
- final BigDecimal totalOfDiscountedPrices = prices.stream()
- .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
- .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
- .reduce(BigDecimal.ZERO,BigDecimal::add);
- System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
想象一下:如果用面向對象處理這些數據,需要多少行?多少次循環?需要聲明多少個中間變量?
關于Stream API的詳細信息,可以查看我之前寫的文章 。
#p#
Lambda的性能
Oracle公司的性能工程師Sergey Kuksenko有一篇很好的性能比較的文檔: JDK 8: Lambda Performance study, 詳細而全面的比較了lambda表達式和匿名函數之間的性能差別。這里是視頻。 16頁講到最差(capture)也和inner class一樣, non-capture好的情況是inner class的5倍。
lambda開發組也有一篇ppt, 其中也講到了lambda的性能(包括capture和非capture的情況)??雌饋韑ambda最差的情況性能內部類一樣, 好的情況會更好。
Java 8 Lambdas - they are fast, very fast也有篇文章 (需要翻墻),表明lambda表達式也一樣快。
Lambda的陰暗面
前面都是講Lambda如何改變Java程序員的思維習慣,但Lambda確實也帶來了困惑
JVM可以執行任何語言編寫的代碼,只要它們能編譯成字節碼,字節碼自身是充分OO的,被設計成接近于Java語言,這意味著Java被編譯成的字節碼非常容易被重新組裝。
但是如果不是Java語言,差距將越來越大,Scala源碼和被編譯成的字節碼之間巨大差距是一個證明,編譯器加入了大量合成類 方法和變量,以便讓JVM按照語言自身特定語法和流程控制執行。
我們首先看看Java 6/7中的一個傳統方法案例:
- // simple check against empty strings
- public static int check(String s) {
- if (s.equals("")) {
- throw new IllegalArgumentException();
- }
- return s.length();
- }
- //map names to lengths
- List lengths = new ArrayList();
- for (String name : Arrays.asList(args)) {
- lengths.add(check(name));
- }
- 如果一個空的字符串傳入,這段代碼將拋出錯誤,堆棧跟蹤如下:
- ?
- 1
- 2
- at LmbdaMain.check(LmbdaMain.java:19)
- at LmbdaMain.main(LmbdaMain.java:34)
再看看Lambda的例子
- Stream lengths = names.stream().map(name -> check(name));
- at LmbdaMain.check(LmbdaMain.java:19)
- at LmbdaMain.lambda$0(LmbdaMain.java:37)
- at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
- at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
- at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
- at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
- at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
- at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
- at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
- at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
- at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
- at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
- at LmbdaMain.main(LmbdaMain.java:39)
這非常類似Scala,出錯棧信息太長,我們為代碼的精簡付出力代價,更精確的代碼意味著更復雜的調試。
但這并不影響我們喜歡Lambda!
總結
在Java世界里面,面向對象還是主流思想,對于習慣了面向對象編程的開發者來說,抽象的概念并不陌生。面向對象編程是對數據進行抽象,而函數式編程是對行為進行抽象。現實世界中,數據和行為并存,程序也是如此,因此這兩種編程方式我們都得學。
這種新的抽象方式還有其他好處。很多人不總是在編寫性能優先的代碼,對于這些人來說,函數式編程帶來的好處尤為明顯。程序員能編寫出更容易閱讀的代碼——這種代碼更多地表達了業務邏輯,而不是從機制上如何實現。易讀的代碼也易于維護、更可靠、更不容易出錯。
在寫回調函數和事件處理器時,程序員不必再糾纏于匿名內部類的冗繁和可讀性,函數式編程讓事件處理系統變得更加簡單。能將函數方便地傳遞也讓編寫惰性代碼變得容易,只有在真正需要的時候,才初始化變量的值。
總而言之,Java更趨于完美了。