# 六角鼠年鐵人賽 Week 16 - Spring Boot - 番外篇 Java 8 Lambda Tutorial ==大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai== ## 劇中佳句 一代宗師 :::info 念念不忘,必有回響 ::: ## 主題 這週與下週都會各自寫個番外篇特別來介紹 Java 8 的 Lambda expression 和 Stream 的學習心得分享。 為什麼要特別提到這兩個 feature 呢? 因為對 Java 來說,從 7 -> 8 可以說是 Java 史上最大幅度的改變! 而當中最大的改變莫屬這兩個 feature 不可。 到底他們有多大的能耐可以造就這歷史級別的差異,就在文中慢慢來介紹了~ :::info **小知識分享** - Java 8 -> 9 也是有不少的內容改變和新 feature 加入,但考慮到 **歷史事件**( Oracle 收購昇陽、Java 開始收費)、**市場考量**(OpenJDK 的出現) 等因素,7 -> 8 的幅度切切實實的影響了所有的 Java 程式設計師。 ::: 時至今日,仍有不少金融企業的內部系統採用穩定的 Java 7;新一點的科技公司會升級使用到 Java 8;部分新創公司則開始嘗試導入 Java 11 的版本。 而為什麼大部分的 Java 系統還在採用 7 或 8 這種老舊的版本呢? 這是因為在眾多的 Java Plugin 使用上,大部分的套件都還是為 7 或 8 版本量身打造(因為這兩版本使用率最高),這些套件也沒有因為 Java 版本升級而去更新,部分會在 Java 版本跳升到使用 9 (甚至是 up 版本)時會有許多錯誤跑出,諸如 **Mavan**, **Gradle** 這類時常更新的套件也受到版本影響。 在企業管理的考量下,自然不會考慮到做升級的打算,畢竟面對無法預期的錯誤與可能造成的損失來看,這個投資性價比十分的低,因此大多公司若非必要是不會進行系統升級的。 (很多都是另起爐灶最快) 因此對於剛投入職場的 Java 工程師們,如果你是學習非常新穎的版本 (例如9, 10, 11, 12, 13, 14...),可能需要重新學習並去習慣較不方便的 7 或 8 的版本了。 > Java 7 在 2015年 4月 發出停止更新版本公告 > Java 8 在 2019年 1月 發出停止更新版本公告 ## Lambda 表示式 英文全名為 **Lambda Expression**,講到 Lambda 表示式,就必須連同 **Functional Interfaces** 和 **匿名類別**一起講,各位看官才會有感覺,因為透過 Lambda 表示式,將會大大改善使用 Functional Interfaces 和 匿名類別的方式。 :::info ### Functional Interfaces 並不是 Java 8 的新 feature,而是由 Interface class 衍生的一種新型態的 interface,強調快速使用為導向,是為了 Lambda 表示式而被強化的 interface,使用 Annotation @FunctionalInterface 進行 Class 的標註,其設計上的限制為該 Interface Class **僅能設計持有一支 abstract method** 例如: ```java= @FunctionalInterface public class testObject{ public void doTest(); // 若增加以下程式碼則會跳出錯誤: Unexpeted @FunctionalInterface Annotation //public void doTest2(); } ``` > Java 8 版本可以在 interface class 中設計實作方法,設計上的限制為必須使用 **default** 或是 **static** 的 method 宣告 > 例如: > ```java= > @FunctionalInterface > public class testObject{ > public void doTest(); > > // 一般類別成員方法 > public default void doCheck(){ > System.out.println("Checked!"); > } > > // 靜態類別方法 > public static void doCheck2(){ > System.out.println("Checked2!"); > } > } > ``` 若一個 interface 繼承一個 functional interface 時的狀況: - 在**不增加** abstract method 的情況下會在 Coding 時,**被視為 functional interface** - 在**增加 default** 或是 **static** 等方法情況下,**仍然被視為 functional interface** - 在**增加** abstract method 的情況下,會**不再被視為 functional interface**,後續無法使用 Lambda 表示式使用該 interface,必須遵照原本 interface 的使用方式。 ::: :::info ### 匿名類別 用來滿足系統實作時,面臨一次性修改且不更動原 Class 內容、透過繼承原類別或實作某些 Interface 的類別後進行修改並建成 Inner Class 存於該使用的 Class 中,並且這些 Inner Class 不需要被引用,我們就會將其設計成匿名內部類別,以方便我們繼續開發系統,避免過多一次性使用的類別而造成整體檔案架構的凌亂。 舉例大家常見的 String 為例子: ```java= Object s1 = new Object(){ @Override public String toString(){ return "I Override and update the content just in this String class."; } }; System.out.println("This is S1: " + s1.toString()); Object s2 = "check toString method."; System.out.println("This is S2: " + s2.toString()); ``` ``` Execute Result: This is S1: I Override and update the content just in this String class. This is S2: check toString method. ``` 從上述例子中大家可以清楚發現,Kai 因為需求關係,必須要修正 s1 物件的 toString() 這個方法,因此我直接在建立物件的同時去 Override 這個方法的內容,這就是一種典型的匿名內部類別,JVM 會在 Compile 該 Class 的同時在裡面建立一個新的 Inner Class 並繼承 Object 物件,然後將設計師寫的 Override 方法給放入其中。 這樣的修改僅會是一次性、專屬性的存在該 Class 當中,可以從 s1 與 s2 兩個物件最後都是呼叫 toString() 的差異看出端倪。 ::: ## 標準寫法 ```java= // 無參數 無回傳 多行 () -> { System.out.println("first line."); System.out.println("second line."); } // 無參數 無回傳 單行 () -> System.out.println("first line."); //有參數 有回傳 多行 (Integer i1,Integer i2) -> { i1 = i1 * 2; i2 = i2 * 3; return i1 + i2; } // 有參數 有回傳 單行 (Integer i1,Integer i2) -> i1 + i2; // 單參數 有回傳 多行 Integer i1 -> { i1 = Math.abs(i1); return i1 * 3; } // 單參數 有回傳 單行 Integer i1 -> Math.abs(i1) * 3; ``` ## 有無回傳值? 特別拿出來強調的一個標題,Lambda 表示式是用來方便開發匿名內部類別用的設計方式,不是用來寫完整新的 function 的東西! **所以用作參考原型的方法有無回傳,在 Lambda 表示式中會保持一樣的格式!!** **所以用作參考原型的方法有無回傳,在 Lambda 表示式中會保持一樣的格式!!** **所以用作參考原型的方法有無回傳,在 Lambda 表示式中會保持一樣的格式!!** 很重要,強調三遍! **Lambda 表示式可以改變的是內部程序,無法改變傳入、傳出的參數、方法的宣告和名稱** **Lambda 表示式可以改變的是內部程序,無法改變傳入、傳出的參數、方法的宣告和名稱** **Lambda 表示式可以改變的是內部程序,無法改變傳入、傳出的參數、方法的宣告和名稱** 再強調三遍! ## 優缺點?! 有好就有壞,而一個東西不論好壞,只要能夠使用在適當的地方,那就成功了一半! 在學習 Lambda 表示式的同時,我們必須了解這種設計方式所帶來的好與壞,才能將其妥善的運用在系統開發中 - 優點: - 大幅減少程式行數 - 增加程式閱讀性 - 增加運行效率 > 一般匿名內部類別,JVM compile 後會在檔案中產生一組 class 檔,但 Lambda 表示式則不會產出新檔案 - 缺點: - 需要了解其使用的 interface - 偵錯時的中斷點不好釘標 ## 常見的 Functional Interfaces 為了順應 Lambda 表示式的到來,許多我們以往常見的 interface 也已經在 Java 8 被調整為 Functional Interface 型態了。 ### Runnable 參數: Null 回傳: void ```java= Runnable r = () -> {System.out.println("Runnable with Lambda Expression");}; ``` ### Predicate 參數: T 回傳: Boolean ```java= Predicate<String> p = string -> string.length > 3; ``` ### Consumer 參數: T 回傳: void ```java= Consumner<String> c = string -> System.out.println(string); ``` ### Function 參數: T 回傳: R ```java= Function<String,Integer> f = string -> string.length; ``` ### Supplier 參數: Null 回傳: T ```java= Supplier<List<String>> s = () -> new ArrayList<String>(); ``` ### UnaryOperator 參數: T 回傳: T ```java= UnaryOperator<Integer> uo = (num) -> num + 1 ; ``` ### BinaryOperator 參數: (T,T) 回傳: T ```java= BinaryOperator<Interger> bo = (num1 , num2) -> num1 + num2; ``` ## Functional Interface 實例分享 ```java= package kai.com.lambda; public class MathMainTest { public static void main(String []args) { MathProcess mp = i1 -> Math.abs(i1) * 10; System.out.println("mp Result: " + mp.doOperator(10)); MathProcess2 mp2_add = (i1, i2) -> i1 + i2; MathProcess2 mp2_subtract = (i1, i2) -> i1 - i2; MathProcess2 mp2_multiply = (i1, i2) -> i1 * i2; MathProcess2 mp2_divided = (i1, i2) -> i1 / i2; MathProcess3 mp3 = (i1, i2, mp2) -> mp2.doOperator(i1, i2); System.out.println("Add Way: " + mp3.getResult(10, 5, mp2_add)); System.out.println("subtract Way: " + mp3.getResult(10, 5, mp2_subtract)); System.out.println("multiply Way: " + mp3.getResult(10, 5, mp2_multiply)); System.out.println("divided Way: " + mp3.getResult(10, 5, mp2_divided)); } } interface MathProcess { public Integer doOperator(int i); } interface MathProcess2 { public Integer doOperator(int i, int i2); } interface MathProcess3 { public Integer getResult(int i1 , int i2, MathProcess2 mp2); } ``` Kai 這邊寫了三個 interface 都放在 MathMainTest 底下,並在第一段,單純展示 Lambda 的應用,以往需要複雜再定義的匿名內部類別寫法,只需要透過 Lambda 短短一行即可實現目的。 ```java= MathProcess mp = i1 -> Math.abs(i1) * 10; System.out.println("mp Result: " + mp.doOperator(10)); ``` 再來是第二部分,Kai 透過兩個 interface 去組件出多種內部類別方法的可能性。 使用 mp2 interface 專門做出不同變化的程序方法,再將其透過 mp3 interface 轉變為統一呼叫使用的模組。 這種作業方式將會大大增強程式之間的彈性,mp2 可因應不同狀況實作不同程序給不同的 Class,而後,mp3 僅需加入包含有 mp2 的 class 與其要求的參數,即可充分運用個別 mp2 method 物件。 ```java= MathProcess2 mp2_add = (i1, i2) -> i1 + i2; MathProcess2 mp2_subtract = (i1, i2) -> i1 - i2; MathProcess2 mp2_multiply = (i1, i2) -> i1 * i2; MathProcess2 mp2_divided = (i1, i2) -> i1 / i2; MathProcess3 mp3 = (i1, i2, mp2) -> mp2.doOperator(i1, i2); System.out.println("Add Way: " + mp3.getResult(10, 5, mp2_add)); System.out.println("subtract Way: " + mp3.getResult(10, 5, mp2_subtract)); System.out.println("multiply Way: " + mp3.getResult(10, 5, mp2_multiply)); System.out.println("divided Way: " + mp3.getResult(10, 5, mp2_divided)); ``` ## Collection 實例分享 ```java= package kai.com.lambda; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class CollectionTest { public static void main(String [] args){ Map<String,String> map = new HashMap<>(); map.put("1","first"); map.put("2","second"); map.put("3","third"); map.forEach((k, v) -> System.out.println("Map get: " + k + ":" + v)); List<String> list = new ArrayList<>(); list.add("first"); list.add("second"); list.add("third"); list.forEach((k) -> System.out.println("List get: " + k)); } } ``` Collection 是非常非常使用的類別,舉凡 List, Set, Map 都包含在內,而在 Java 8 版本中,更特別增加了 - MAP: ```java= public void forEach(java.util.function.BiConsumer<? super K, ? super V> action) ``` - LIST: ```java= public void forEach(java.util.function.Consumer<? super T> action) ``` 等方法可以讓開發者進行 Lambda 表示式的覆寫,讓其使用在 for loop 中的應用更加方便。 ## 結語 :::danger 其餘還有很多 Lambda 的應用,Kai 這邊單純做介紹 Lambda 和其部分應用即可,後續還有一篇會特別來介紹一下 Stream,這個也是跟 Lambda 的應用非常相關的 feature。 [六角鼠年鐵人賽 Week 17 - Spring Boot - 番外篇 Java 8 Stream Tutorial](/l4MvMeuATGWcEzhGTDrGRQ) ::: 首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA) ###### tags: `Spring Boot`,`w3HexSchool`