上篇教學介紹了一些有關  java.util.function 中的 Java Lambda 應用,這篇將繼續討論它們的一些進階應用。如果大家看下去有甚麼不明白的地方或忘記了 Java Lambda 的語法,可隨時回顧我早前的兩篇教學介紹:

  1. Java Lambda 教學 Part 1
  2. Java Lambda 教學 Part 2 

 Java 8 Interface Static and Default Method

Java 8 中除了引入 Lambda Expression ,還加入了一項十分富爭議的新功能,就是 Interface Static Method 和 Interface Default Method。由於這篇教學主要針對 Lambda 的應用,故我不會用太多篇幅來講解這兩項新功能,有興趣了解這兩項新功能的網友,可以參考以下Java Tutorial的連結。

http://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

簡單而言,由 Java 8 起,我們可以於 Interface 實現方法 (method implementation)。Interface static method 就好像把 Java Interface 變成一個Java Utility Class,提供一些 utility static method,而 Interface default method 就好像 Java Abstract Class 那樣,可以提供一些預設的 method 給 implementation class 來繼承。

(其實,我對這兩大新功能有極大的保留,好像破壞了 Java Interface 的完整性。 Interface 就是 caller 和 provider 之間的 API contract ,不應該存在其他邏輯。本來這件事可以交給 Abstract Class 來分工,但現在反而令 Interface 和 Abstact Class 的界線模糊了,而且令 Java 出現了好像 C++ Multiple Inheritance 問題;即如果一個 Java Class 同時 implement 兩個不同的 interface ,而它們同時有一個相同名稱的 method 和 method signature 的時候,implementation class 就必須 override 撞名的 method 。
Oracle 搞甚麼的?食左 Sun 之後把 Java 搞成這樣)

由於 java.util.function package 內的 Functional Interface 提供了一些有用的 default method 及 static method,姑勿論 default method 和 static method 在 Java community 的爭議,我們作為求知者,都應該要知道它們的用途,用與不用和怎樣用,就要視乎您們項目中的 Architect 如何看吧。

Predict<T> Interface

以下的介紹將會用 Generics Interface 作例子,因為 Generics Interface 的應用應比 Primitive Type Interface 為多,而且明白了 Generics Interface 的用法,應該可以舉一反三地套用在 Primitive Type Interface 之上。

大家如果查看 Predicate<T> 的 JavaDoc ,會看到 Predicate<T> 提供了以下的 default method 及 static method

Default method

  1. Predicate<T> and(Predicate<? super T> other)
  2. Predicate<T> or(Predicate<? super T> other)
  3. Predicate<T> negate()

Static method

  1. <T> Predicate<T> isEqual(Object targetRef)

您們首先會問,不是說 Functional Interface 只可以有一個 public method 嗎?為甚麼 Predicate 可以有那麼多其他 methods 呢?

其實要澄清一點,Functional Interface 的定義是它只可以有一個 Single Abstract Method (SAM) ,而 default method 和 static method 則不計算在內的。

Predicate 的 Semantics 作用就是為程序提供一個條件判斷的邏輯,而 Condition 經常就會遇上 And 、Or 、Not 這些 Operators ,相信大家做程式開發的,一定不會對 and(), or() 或 negate() 的用法感到陌生。不過,比較特別的是,這三個 default methods 都是 return 一個 Java Lambda,而在教學 Part 1 中已提及 Java Lambda 其實也只是一個 Java Object,所以細心想想,也不是十分特別的事,舉一個例子說明,大家應該明白如何妙用這些 Lambda-returning methods。

假設我們想找出同時間在公司服務了超過五年的 IT Architect,我們當然可以用以下這條 Lambda Expression 就可以完成了這個任務。

staff -> staff.getServiceYear() > 5 && staff.getJobRole().equals("IT Architect")

但如果我們已經分別已經有兩個不同的 Predicate Lambda,我們是無須要再另外寫一條合併的 Lambda,我們可以用 Predicate.and() 把它們連結起來,如下:

Predicate<Staff> over5Year = staff -> staff.getServiceYear() > 5;
Predicate<Staff> isItArchitect = staff -> staff.getJobRole().equals("IT Architect");
findStaff(staffList, over5Year.and(isItArchitect));

如果我們想找出在公司服務了超過五年或是IT Architect的員工,則可以用 Predicate.or() 把以上的 Predicate Lambda 連結起來,得出我們想要的效果,如下:

findStaff(staffList, over5Year.or(isItArchitect));

至於 negate() ,則是 not 的意思,以下就是利用 negate() 來找出服務少於或等於五年的員工的語法:

findStaff(staffList, over5Year.negate());

至於 isEqual() 這個 static method 就十分特別,您可以給他一個 Reference Java Object,它就會回傳一個可以判斷是否相等於那個 Reference Java Object 的 Predicate,例子如下

// Assume the 6th element of staffList is a staff named as Foo Bar
Staff foobarStaff = staffList.get(5);
/* Pass the Foo Bar staff to isEqual to generate a predicate which can test if the Staff is "Foo Bar" */
Predicate<Staff> foobarStaffPredicate = Predicate.isEqual(foobarStaff);
// It will find the Staff, Foo Bar, from the staffList
findStaff(staffList, foobarStaffPredicate);

Consumer<T> Interface

Consumer<T> Interface 就比較簡單,它只提供了以下的 default method。BiConsumer<T, U> 只是 Consumer<T> 的兩個 inputs 版本。

Default method

  1. Consumer<T> andThen(Consumer<? super T> after)
  2. BiConsumer<T, U> andThen(Consumer<? super T, ? super U> after)

andThen() 的作用就是把輸入 ( inputs ) 給予 A consumer ,然後 ( andThen ) 再把同一個輸入給予 B Consumer,即 A.andThen(B) 。(BiConsumer 版本只是 Consumer 的兩個 inputs 版本)

舉一個例子說明,如果我們想把 Staff name 同時輸出到 System.out 和 log4j logger,我們可以這樣把兩個 Consumer 串連起來。

// Firstly, create a consumer java lambda to output to System.out
Consumer<Staff> outputToSystemOut = s -> System.out.println(s.getFullName());
// Secondly, create another consumer java lambda to output via log4j logger
Consumer<Staff> outputToLog4j = s -> logger.info(s.getFullName());
/* Lastly, chain them together using andThen and invoke accept() method */
outputToSystemOut.andThen(outputToLog4j).accept(staff);

當然,如果 JVM 在處理 outputToSystemOut 出現錯誤並有 exception thrown,那麼 outputToLog4j 是不會執行的。

Function<T, R> Interface

Function<T, R> Interface 提供了以下的 default method 及 static method,BiFunction<T, U, R> 是 Function<T, R> 的兩個 inputs 版本,至於 UnaryOperator<T> 和 BinaryOperator<T> 則分別是 Function<T, R> 和 BiFunction<T, U, R> 當 input type 和 return type 都是相同類型的版本。

Default method

  1. Function<T, V> andThen(Function<? super R, ? extends V> after)
  2. Function<V, R> compose(Function<? super V, ? extends T> before)

Static method

  1. Function<T, T> identity()

Function 的 andThen() 和 compose() 都是把兩個 Functions 串連在一起,A.andThen(B) 就是把 Function A 執行後的結果成為 Function B 的輸入,然後讓 Function B 執行。至於,compose() 則是 andThen() 相反,A.compose(B) 的意思是先執行 Function B ,然後才執行 Function A。

Function andThen() 與 Consumer andThen() 的分別在於,Function 的 andThen() 是會把執行結果傳入下一個串連的 Function 執行,但 Consumer 的 andThen() 都是把同一個輸入 ( input ) 作為所有串連 Consumer 的輸入。

舉一個數學例子說明,我們中學時期應該學習了一個畢氏定理 ( Pythagoras theorem ),就是直角三角形直角邊的平方和等於斜邊的平方,即 x2 + y2 = z2。可以用以下 Functions 串連起來

BiFunction<Double, Double, Double> sqtSum = (x, y) -> x*x + y*y;
/* It is method reference to the method Math.sqrt(double). Recall: It is also a Java Lambda */
Function<Double, Double> sqtRoot = Math::sqrt;
/* Chain the 2 functions together to form the Pythagorean Theorem */ 
BiFunction<Double, Double, Double> pythagoreanTheorem = sqtSum.andThen(sqtRoot);

Function.identity() static method 則是一個永遠傳回一個跟您輸入的 input 一樣的 function ,但實際有甚麼用途呢?可能您未來會有機會踫上一些 Java API ,要求給予一個 Function Java Lambda 來提供轉換的邏輯 ( transformation ) ,但您有可能不想作出任何轉換,您就可以用 identity() 來替補 method signature 的要求。例如

/* Original API design invokes transformFunc.apply(T) on each element of inputList */
List<T> massageListElement(List<T> inputList, Function<T, T> transformFunc);
// Don't want to transform anything in the inputStaffList
List<Staff> staffList = massageListElement(inputStaffList, Function.identity());

Supplier<T> Interface

Suppler<T> Interface 並沒有提供其他 default method 和 static method。

Return Java Lambda Method

其實以上所有 Java Lambda Interface Method 的例子都有一個特徵,就是它們全是會傳回一個 Java Lambda,由於在第一篇教學已有提過 Java Lambda 其實也只是一個 Java Object,我們隨時都可以寫一個 Java Method,而它是傳回一個 Java Lambda 的。

例如,我們想找出在公司已經服務超過 n 年的員工,我們可以定義一個這樣的 method。

public static Predicate<Staff> findServiceYearPredicate(int minServiceYear) {
    return (staff -> staff.getYearOfService() > minServiceYear);
}

這樣做,我們就不用 hardcode 了 Predicate 的邏輯,可以隨著 minServiceYear 去改變篩選條件。

另外一個較進階的做法就是用 Java Lambda 去產生另一條 Lambda Expression,但會比較難讀,建議都是用回以上的 method 做法。這個方法是用 Function Lambda 去幫忙的,方法如下:

Function<Integer, Predicate<Staff>> findServiceYearPredicate = minServiceYear -> (staff -> staff.getYearOfService() > minServiceYear);

大家請細細領悟以上雙重 Java Lambda Expression 的奥秘吧!

下篇再續,如有問題,請留言給我,謝謝!

Java Lambda 教學 Part 3
Tagged on:                 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

zh_HKChinese (Hong Kong)