本文部分摘錄自 On Java 8
概述
通常,傳遞給方法的資料不同,結果也不同,同樣的,如果我們希望方法被呼叫時的行為不同,該怎么做呢?結論是:只要能將代碼傳遞給方法,那么就可以控制方法的行為,
說得再具體點,過去我們總是創建包含所需行為的物件,然后將物件傳遞給想要控制的方法,一般使用匿名內部類來實作,假設現在有這么一個需求:有一個員工資訊串列,根據年齡過濾出符合條件的員工資訊
// 過濾出大于35歲的員工
public List<Employee> filterEmployee(List<Employee> list) {
List<Employee> emps = new ArrayList<>();
for(Employee emp : list) {
if(emp.getAge() > 35) {
emps.add(emp);
}
}
return emps;
}
// 過濾出大于45歲的員工
public List<Employee> filterEmployee2(List<Employee> list) {
...
}
這樣寫當然能實作需求,但如果需求變了,要過濾 45 歲的,那豈不是又得寫一個 filterEmplyee2() 方法?如果還要過濾 50 歲的,60 歲的,那就沒完沒了了,而且代碼的實作邏輯幾乎沒有區別,于是我們借助策略模式的思想來簡化代碼,
public interface MyPredicate<> {
boolean predicate(T t);
}
// 如果有其他過濾需求,只需要實作 MyPredicate 介面即可
public class EmployeeFilter implements MyPredicate<Employee> {
@Override
public boolean predicate(Employee employee) {
return t.getAge() >= 35;
}
}
// 根據傳入的 MyPredicate 物件來實作不同的過濾邏輯
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> mp) {
List<Employee> emps = new ArrayList<>();
for(Employee emp : list) {
if(mp.predicate(emp)) {
emps.add(emp);
}
}
return emps;
}
public void test(List<Employee> list) {
// 創建實作類物件,傳入過濾方法
MyPredicate<Employee> predicate = new EmployeeFilter<>();
List<Employee> res = filterEmployee(list, predicate);
// 更簡單的方式是使用匿名內部類
List<Employee> res2 = filterEmployee(list, new MyPredicate<Employee>() {
@Override
public boolean predicate(Employee employee) {
return t.getAge() >= 100;
}
});
}
通過觀察我們發現,我們需要的只有 predicate() 方法的代碼,其他的我們一律不關心,如果 MyPredicate 介面還有其他抽象方法,我們又必須每一個做一次實作,但真正用上的只有 predicate() 方法,不僅顯得冗余,而且可讀性也很低,為了解決這個問題,Java8 為我們提供了 Lambda 運算式和方法參考兩種更加簡潔的方式,
Lambda 運算式
Lambda 運算式是一個匿名函式,可以把 Lambda 運算式理解為是一段可以傳遞的代碼(將代碼像資料一樣傳遞),雖然在 JVM 規范規定一切都是類,但其幕后執行的各種操作使得 Lambda 看起來像是函式,因此我們可以大膽假設 Lambda 運算式產生的就是一個函式,而不是類,
Lambda 的基本語法有是:(引數) -> {方法體}
- 其中
->可以視為將引數傳遞給方法體使用的一個中間橋梁 - 左側為運算式的引數串列,使用括號包裹引數,當只有一個引數時,可以不需要括號,如果沒有引數,則必須使用括號表示空引數串列,引數串列的資料型別可以省略不寫,因為 Java 的編譯器可以幫助我們根據背景關系推斷資料型別
- 右側為運算式中所需執行的功能,方法體如果只有單行,可以省略花括號,此時執行結果自動轉化為 Lambda 運算式的放回值,使用 return 關鍵字是非法的;如果方法體有多行,則必須放在花括號中,這時如果有回傳值,就需要使用 return
Lambda 運算式能產生比匿名內部類更易讀的代碼,因此我們應該盡可能使用 Lambda 運算式,回到之前的例子,我們可以用 Lambda 運算式來替換匿名內部類,
public interface MyPredicate<> {
boolean predicate(T t);
}
// 根據傳入的 MyPredicate 物件來實作不同的過濾邏輯
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> mp) {
List<Employee> emps = new ArrayList<>();
for(Employee emp : list) {
if(mp.predicate(emp)) {
emps.add(emp);
}
}
return emps;
}
public void test(List<Employee> list) {
// 使用 Lambda 運算式
List<Employee> res = filterEmployee(list, e -> e.getAge() <= 5000);
}
Lambad 運算式通常比匿名內部類產生更易讀的代碼,因此我們應該盡可能使用 Lambda 運算式,
如果我們想撰寫遞回的 Lambda 運算式,必須注意:
方法參考
Lambda 運算式可以幫助我們實作僅呼叫方法,而不做其他多余動作(如創建物件)的目的,而有些情況下,已經存在能滿足需求的方法,我們可以不必再撰寫 Lambda 運算式,而通過方法參考直接使用該方法,可以理解為方法參考是 Lambda 運算式的另一種表現形式,
方法參考的組成:類名或物件名,后面跟 ::,然后跟方法名稱,如果要分類的話,可以用如下組合:
-
參考靜態方法
className::staticMethod -
參考某個物件的實體方法
instance::instanceMethod -
參考某個型別的任意物件的實體方法
className::instanceMethod -
參考構造方法
className::new
interface Callable {
void call(String s);
}
class Describe {
void show(String msg) {
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) {
System.out.println("Hello, " + name);
}
public static void main(String[] args) {
// 物件名:: 方法名稱
Describe d = new Describe();
Callable c = d::show;
c.call("call()");
// 類名::方法名
c = MethodReferences::hello;
c.call("Bob");
}
}
要注意的是,方法參考的簽名(引數型別和回傳型別)必須符合 Callable 的 call() 的簽名,上述代碼我沒有演示 className::instanceMethod 和 className::new 的情況,這兩個有點特殊,待會再介紹,
Runnable 介面
通過之前的學習,我們發現 Runnable 介面也符合特殊的單方法介面格式:它的 run() 方法不帶引數,也沒有回傳值,因此我們可以使用 Lambda 運算式和方法參考作為 Runnable
class Go {
static void go() {
System.out.println("thread go");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
// 匿名內部類方式
new Thread(new Runnable() {
public void run() {
System.out.println("Anonymous");
}
}).start();
// Lambda 運算式方式
new Thread(
() -> System.out.println("lambda")
).start();
// 方法參考方式
new Thread(Go::go).start();
}
}
未系結的方法參考
未系結的方法參考是指沒有關聯物件的普通(非靜態方法),使用未系結的參考,我們必須先提供物件
class X {
String f() { return "X::f()"; }
}
interface MakeString {
String make();
}
interface TransformX {
String transform(X x);
}
public class UnboundMethodReference {
public static void main(String[] args) {
// MakeString ms = X::f; // 無法通過編譯
TransformX sp = X::f;
X x = new X();
System.out.println(sp.transform(x));
System.out.println(x.f()); // 同等效果
}
}
我們看到在 MakeString ms = X::f; 中,即使 make() 和 f() 有相同的方法簽名,卻無法通過編譯,這是因為實際上還有另一個隱藏引數 this 沒有考慮,你不能在沒有 X 物件的情況下呼叫 f(),因為它尚未系結到物件,
要解決這個問題,我們需要一個 X 物件,所以我們的介面需要一個額外的引數如 TransformX,用來接收一個 X 物件,同樣的,在呼叫 transform(X x) 方法時,也必須傳遞一個 X 物件作為引數,如果你的方法有多個引數,就以第一個引數接受 this 的模式來處理,
建構式參考
還可以捕獲建構式的參考,然后通過參考去呼叫該建構式,
class Dog {
String name;
int age;
Dog() { name = "stray"; }
Dog(String nm) { name = nm; }
Dog(String nm, int yrs) { name = nm; age = yrs; }
}
interface MakeNoArgs {
Dog make();
}
interface Make1Arg {
Dog make(String name);
}
interface Make2Args {
Dog make(String name, int age);
}
public class CtorReference {
public static void main(String[] args) {
MakeNoArgs mna = Dog::new;
Make1Arg m1a = Dog::new;
Make2Args m2a = Dog::new;
Dog dn = mna.make();
Dog d1 = m1a.make("Comet");
Dog d2 = m2a.make("Ralph", 4);
}
}
函式式介面
介面中只有一個抽象方法的介面,稱為函式式介面,可以使用注解 @FunctionalInterface 檢查一個介面是否符合函式式介面的規范,
Lambda 運算式和方法參考都要賦值給對應的函式式介面參考,Java8 提供了一組 java.util.function 包,它包含一組完整的函式式介面,一般情況下,我們可以直接使用,而不需要自己再定義,
Java 為我們提供了內置的四大核心函式式介面:
-
消費型介面
有引數,無回傳值型別的介面
@FunctionalInterface public interface Consumer<T> { void accept(T t); } -
供給型介面
只有產出,沒有輸入,就是只有回傳值,沒有入參
@FunctionalInterface public interface Supplier<T> { T get(); } -
函式型介面
既有入參,也有回傳值,T 表示函式的引數型別,R 表示函式的回傳型別
@FunctionalInterface public interface Function<T, R> { R apply(T t); } -
斷言型介面
輸入一個引數,回傳一個 boolean 型別的回傳值
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
除了上述的四個核心內置介面,Java 還為我們提供其他常用的函式式介面,如 BiFunction<T, U, R> 也是函式型介面,但可以接收兩個引數,我們可以根據需要去查閱 API 檔案,
函陣列合
意為多個組合成新的函式,一些 java.util.function 介面包含支持函陣列合的方法
-
andThen(Function<? super R,? extends V> after)回傳一個組合函式,前一個函式的結果作為后一個函式的入參
-
compose(Function<? super V,? extends T> before)回傳一個組合函式,后一個函式首先處理原始入參,再將結果交給前一個函式處理
-
and(Predicate<? super T> other)回傳一個組合的謂詞,表示該謂詞與另一個謂詞的短路邏輯與
-
or(Predicate<? super T> other)回傳一個組合的謂詞,表示該謂詞與另一個謂詞的短路邏輯或
-
negate()回傳表示此謂詞的邏輯否定的謂詞
閉包
考慮一個函式,x 是 其中的一個入參,i 則是其中的一個區域變數,回傳一個 Lambda 運算式
public class Closure {
IntSupplier makeFun(int x) {
int i = 0;
return () -> x + i;
}
}
我們知道,函式的入參的區域變數只在方法的生命周期內有效,正常情況下,當 makeFun(int x) 方法執行完后,x 和 i 就會消失,但它回傳的 Lambda 運算式卻依然保存著 x 和 i 的值,相當于 makeFun(int x) 回傳的 IntSupplier 關住了 x 和 i
另外要注意的一點是:被 Lambda 運算式參考的區域變數必須是 final 或是等同 final 效果的,所謂等同 final,意思是即使你沒有明確宣告變數是 final,但因變數值沒被改變過而實際上有了 final 同等的效果,Java8 默認 Lambda 中的區域變數具有等同 final 效果,
柯里化
柯里化意為:將一個多引數的函式,轉換為一系列單引數函式
public class CurryingAndPartials {
// 未柯里化
static String uncurried(String a, String b) {
return a + b;
}
public static void main(String[] args) {
// 柯里化的函式
// a -> b -> a + b,意思是傳入引數 a,回傳 b -> a + b 的函式
// 由于 Lambda 運算式的閉包特性,b -> a + b 中的 a 是有保存值的
Function<String, Function<String, String>> sum = a -> b -> a + b;
System.out.println(uncurried("Hi ", "Ho"));
Function<String, String> hi = sum.apply("Hi ");
System.out.println(hi.apply("Ho"));
Function<String, String> sumHi = sum.apply("Hup ");
System.out.println(sumHi.apply("Ho"));
System.out.println(sumHi.apply("Hey"));
}
}
柯里化的目的是通過提供一個引數來創建一個新函式,根據上述的例子,我們可以通過添加級別來柯里化具有更多引數的函式
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/213727.html
標籤:Java
