iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
自我挑戰組

Java SE系列 第 19

Day19:別說那麼多廢話,講重點

  • 分享至 

  • xImage
  •  

Lambda在剛開始學Java一定會很不想碰,會覺得好不容易對Java有點熟悉了,結果又搞出一整陀新的語法,心想,反正不用Lambda也都可以把邏輯寫出來,能跑就好了啦,改天再學吧!
結果好死不死,我第一份工作上工後,一打開Service類別,滿滿的Stream操作搭配Lambda語法......

到現在總是摸熟了,老實說,就回不去原本不用Lambda的寫法了,尤其是在集合的操作上,真的是好用,讚。

  1. Lambda

百聞不如一見,理論學再多不如實戰一回,我們這邊就來用經典的Arrays.sort(T[] a, Comparator<> c)來比較一波:(Arrays.sort())

  • 匿名類別(Anonymous Class)
String[] osArr = {"Windows", "Mac", "Linux"};
Comparator<String> byLength = new Comparator<String>({
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});
Arrays.sort(osArr, byLength);
  • Lambda
String[] osArr = {"Windows", "Mac", "Linux"};
Arrays.sort(osArr, (s1, s2) -> s1.length() - s2.length());

Lambda是不是簡潔很多? 不過簡潔不是只是偷懶,我們要思考的點在於,為什麼少寫了那些東西程式一樣可以跑?
關鍵就在於方法簽章。我們可以看到,最關鍵的地方就是compare方法輸入什麼以及輸出什麼,在字串的比較上我們的compare方法要輸入2個字串引數,最後會輸出一個int整數,所以Lambda讓我們可以只寫這些最關鍵的地方,箭頭(->)左邊就是輸入,右邊就是輸出,而且JVM會很聰明知道因為在sort方法第一個引數我們放的是String[],所以後面的Comparator也會是String系列的型別,所以我們在輸入的部分甚至還不用表明型別,只寫了(s1, s2)。

還有一個問題需要討論一下,就是為什麼JVM知道我們的Lambda描述是在描述Comparator的compare方法? 介面又不是只能有一個方法,如果有兩個勒?
答對了! 如果一個介面有2個方法時,還就真的不能這樣用Lambda描述呢!
而這種只有一個方法需要實作的介面,就稱為功能介面(Functional Interface),舉凡Comparator, Runnable, Callable等等,而這些介面官方也會標註上@FunctionalInterface,我們自己也可以定義這類的介面並貼上@FunctionalInterface。

  1. Stream

介紹完Lambda後就一定要緊接著認識Stream,因為Stream真的是太好用了,寫起來又爽,看起來又酷炫。
假設我們有一個字串陣列,我們想把它裡面的元素轉換為數字,並且只保留偶數,在中間要print一下過濾後的元素確保程式運作正常,而且過濾後的元素還要很囉唆的再加上1。這樣的需求再還不會Stream時大概就會用for迴圈這樣寫吧:

String[] arr = {"1", "2", "3", "4", "5"};
List<String> strings = Arrays.asList(arr);

List<Integer> numbersForLoop = new ArrayList<>();
for(String str : strings) {
    if(Integer.valueOf(str) % 2 == 0) {
        System.out.println(str);
        numbersForLoop.add(Integer.valueOf(str) + 1);
    }
}

需要使用for迴圈搭配if條件式來寫。

如果使用Stream的話:

String[] arr = {"1", "2", "3", "4", "5"};
List<String> strings = Arrays.asList(arr);

List<Integer> numbersLambda = strings.stream()
    .filter(str -> Integer.valueOf(str) % 2 == 0)
    .peek(str -> System.out.println(str))
    .map(str -> Integer.valueOf(str) + 1)
    .collect(Collectors.toList());

是不是看起來就很酷? 感覺很清爽,每個動作都變成一行,一目了然;而且也不用先在scope外面創一個空的List然後過程中裝進去,這個步驟寫久了就很不爽快,用stream就可以直接collect成一個List,真的爽。

若我們查看Collection官方API會發現在Java8以後新增了這個方法,會回傳一個Stream< E >,E代表Collection裡面各個元素的型別。這代表說我們平常用的ArrayList, HashSet等等,都可以呼叫這個方法,把一個集合轉換為一條管線(Stream)。

接著我們來看看Stream官方API,可以看到裡面有很多種管線操作,這邊就把剛剛範例用到的說明一下。

filter(Predicate<? super T> predicate),裡面要放入Predicate介面型態的實例,這是什麼鬼呢:
Predicate,原來是個Functional Interface! 那它需要繼承的那個方法是啥:
boolean test(T t),是一個放入某某類別參數後,會回傳boolean的方法。

說明到這邊應該滿清楚了吧!這時候可以回頭看看範例,所以filter的操作就是我們要丟入一個實作Predicate的實例,內部的test方法實作內容會將我們管線傳入的元素運算成一個boolean,true的話就代表可以繼續往下其他操作,false就代表這個元素要被過濾掉了!

peek(Consumer<? super T> action),裡面要放入Consumer介面型態的實例:
Consumer,一樣是個Functional Interface,需要繼承實作的方法是:
void accept(T t),沒有回傳值的一個方法。

代表說管線經過peek時元素的狀態都不會被改變,會保留原樣地繼續往下流,不過在這中間我們就可以拿各個元素做一些事情,在範例中就是把元素打印到console。

map(Function<? super T,? extends R> mapper),裡面需要Function型別的實例:
Function,Functional Interface,需要繼承實作的方法是:
R apply(T t),會回傳R型態,可以注意到傳入的參數型態是T,這代表說經過這個方法運作後,可以產出一個不同於傳入參數型別的回傳值!如果只用基本型別(Primitive Type)來想可能不會覺得有什麼,但是如果把類別型別(Class Type)的概念納進來,就會發現,wow,可以做好多事事情了。

所以在map的操作中我們就把String型別的元素轉換成Integer,並且加上了1。

而管線操作方法很多都是和官方的Functional Interface一起搭配使用,這就代表我們可以用Lambda語法簡潔的表達出我們想要的實作方式!

最後的collect()就是如何收集這些管線產物的方法,這邊我們用了Collectors.toList(),那就是我們決定把一個個元素用List蒐集起來囉。


上一篇
Day18:亞季軍
下一篇
Day20:銀河帝國
系列文
Java SE30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言