Java8系列 (一) Lambda表示式
函數語言程式設計
在介紹Lambda表示式之前, 首先需要引入另一個概念, 函數語言程式設計。
函數語言程式設計是一種程式設計正規化, 也就是如何編寫程式的方法論。它的核心思想是將運算過程儘量寫成一系列巢狀的函式呼叫,關注的是做什麼而不是怎麼做,因而被稱為宣告式程式設計。以 Stateless(無狀態)和 Immutable(不可變)為主要特點,程式碼簡潔,易於理解,能便於進行並行執行,易於做程式碼重構,函式執行沒有順序上的問題,支援惰性求值,具有函式的確定性——無論在什麼場景下都會得到同樣的結果
我們把以前的程序式程式設計正規化叫做 Imperative Programming – 指令式程式設計,而把函數語言程式設計正規化叫做 Declarative Programming – 宣告式程式設計。下面通過一個簡單的示例介紹兩者的區別。
//指令式程式設計 int a = 1; int b = 2; int c = a+b; int d = c - 10; //宣告式程式設計 minus(plus(a, b), 10);
函式式介面
在Java8中, 引入了函式式介面這個新的概念, 函式式介面就是一個有且僅有一個抽象方法
,但是可以有多個非抽象方法(靜態方法和default關鍵字修飾的預設方法)的介面。
如果介面中宣告的是java.lang.Object類中的 public 方法,那麼這些方法就不算做是函式式介面的抽象方法。因為任何一個實現該介面的類都會有Object類中公共方法的預設實現。
@FunctionalInterface 註解用於標註介面會被設計成一個函式式介面,雖然他不是必須的,但是推薦使用,這樣會在編譯期檢查使用 @FunctionalInterface 的介面是否是一個函式式介面。
Runnable執行緒任務類、Comparator比較器都只有一個抽象方法, 所以他們都是函式式介面, 另外Java8新引入了幾個常用的泛型函式式介面 Predicate、Consumer、Function、Supplier, 以及在此基礎之上擴充套件的一些函式式介面, 如 BiFunction、BinaryOperator等等。
為了避免自動裝箱操作,Java8對Predicate、Function、Supplier、Consumer等一些通用的函式式介面的原始型別進行了特化,例如: IntFunction。
@Test public void test6() { IntPredicate intPredicate = (int i) -> i % 2 == 1; intPredicate.test(1000); Predicate<Integer> predicate = (Integer i) -> i % 2 == 1; predicate.test(1000); }
上面的示例中, Predicate<Integer> 每次呼叫它的方法時都要進行一次裝箱和拆箱, 而 IntPredicate 避免了這個問題, 當處理的資料比較多時, 使用 IntPredicate 可以提高你的程式執行效率。
你可以像下面這樣自定義一個函式式介面:
@Test public void test3() { FunctionInterface1<String, Integer, List, Map<String, Object>> f1 = (str, num, list) -> new HashMap<>(16); } @FunctionalInterface public interface FunctionInterface1<O, T, K, R> { R apply(O o, T t, K k); }
Lambda表示式
Lambda表示式的基本語法是: (引數列表) -> 函式主體:
- (parameters) -> expression
- (parameters) -> {statements;}
Runnable r1 = () -> System.out.println("test"); Runnable r2 = () -> { System.out.println("test"); };
Lambda表示式允許你直接以內聯的形式為函式式介面的抽象方法提供實現,並把整個表示式作為函式式介面的例項(具體的說,是函式式介面的一個具體實現的例項)。
Lambda表示式可以被賦給一個變數,也可以作為引數傳遞給一個接受函式式介面作為入參的方法, 還可以作為一個返回值型別為函式式介面的方法返回值。
public Callable<String> fetch() { return () -> "測試Lambda表示式"; }
上面的示例中, Callable<String> 的抽象方法簽名是 () -> String , 和Lambda表示式 () -> "測試Lambda表示式" 的簽名是一致的, 所以可以將其作為方法返回值。
只要Lambda表示式和函式式介面的抽象方法簽名(及函式描述符)相同,則同一個Lambda表示式可以與多個不同的函式式介面聯絡起來。
@Test public void test7() { Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); ToIntBiFunction<Apple, Apple> c2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); BiFunction<Apple, Apple, Integer> c3 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); }
如果一個Lambda的主體是一個表示式,它就和一個返回 void 的函式描述符(即函式式介面的抽象方法簽名, 例如 (T, U) -> R
)相容。下面這個語句是合法的,雖然Lambda主體返回的是List,而不是Consumer上下文要求的 void。
Consumer<String> c = s -> Arrays.asList(s);
Lambda表示式可以沒有限制的在其主體中引用例項變數和靜態變數,但如果是區域性變數,則必須顯式的宣告為final或只能被賦值一次,才能在Lambda主體中被引用。
public class ChapterTest3 { String s1 = ""; static String s2 = ""; @Test public void test8() { String str = "區域性變數"; str = "區域性變數"; new Thread(() -> System.out.println(str)).start();//區域性變數str重新賦值了,這一行就無法通過編譯 new Thread(() -> System.out.println(s1)).start(); new Thread(() -> System.out.println(s2)).start(); s1 = "例項變數"; s2 = "靜態變數"; } }
方法引用主要有三類
- 指向靜態方法的方法引用,例如 s -> String.valueOf(s) 可簡寫成 String::valueOf
- 指向任意型別的例項方法的方法引用,例如 (String s) -> s.length() 可簡寫成 String::length (簡單的說,就是你在引用一個物件的方法,而這個物件本身是Lambda的一個入參)
- 指向Lambda表示式外部的已經存在的物件的例項方法的方法引用,下面的示例很好的展示瞭如何將 Lambda 重構成對應的方法引用
@Test public void test10() { Consumer<String> c1 = i -> this.run(i); //上面的Lambda表示式可以簡寫成下面的方法引用,符合方法引用的第三類方式, this引用即所謂的外部物件 Consumer<String> c2 = this::run; } public void run(String s) { } @Test public void test9() { //指向靜態方法的方法引用 Function<Integer, String> f1 = s -> String.valueOf(s); Function<Integer, String> f2 = String::valueOf; //指向例項方法的方法引用 List<String> list = Arrays.asList("a", "b", "A", "B"); list.sort((s1, s2) -> s1.compareToIgnoreCase(s2)); //上面這個Lambda表示式轉變成更簡潔的方法引用 list.sort(String::compareToIgnoreCase); }
下面的轉換模板圖, 通俗易懂的總結了如何將Lambda表示式重構為等價的方法引用。
關於建構函式引用,下面展示了一個簡單易懂的栗子
@Test public void test11() { //無參構造 Supplier<Apple> c1 = () -> new Apple(); Supplier<Apple> c2 = Apple::new; Apple a1 = c2.get(); //有參構造 BiFunction<String, Integer, Apple> f1 = (color, weight) -> new Apple(color, weight);//Lambda表示式 BiFunction<String, Integer, Apple> f2 = Apple::new;//建構函式引用 Apple a2 = f2.apply("red", 10); }
最後我們總結一下Lambda表示式的使用, 假設我們需要對一個List集合進行不同規則的排序,這個不同規則對應的就是一個比較器Comparator, 我們可以有多種實現方式。
最原始的方式就是定義一個Comparator介面的實現類作為入參, 其次就是使用匿名類的方式提供一個Comparator介面的實現作為入參。
在Java8中, 我們可以不必像上面這麼囉嗦, Lambda表示式很好地簡化了這個實現過程, 比如我們這裡需要按蘋果的重量排序, 那麼可以這樣寫
@Test public void test12() { List<Apple> inventory = new ArrayList<>(); inventory.add(new Apple("red", 94)); inventory.add(new Apple("green", 100)); inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight())); }
再想想, 還能不能更簡化一下, 使用方法引用的方式進一步簡化呢? 在Comparator介面中, 提供了靜態方法 Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor) , 就是為了簡化Lambda表示式準備的, 讓我們重新將上面的程式碼重構成方法引用
@Test public void test12() { List<Apple> inventory = new ArrayList<>(); inventory.add(new Apple("red", 94)); inventory.add(new Apple("green", 100)); inventory.sort(Comparator.comparing(Apple::getWeight)); }
關於 Comparator比較器、Predicate謂詞、Function函式的組合用法
/** * 函式的組合用法 */ @Test public void test15() { Function<String, Integer> f = i -> Integer.valueOf(i);//方法引用寫法: Integer::valueOf Function<Integer, Apple> g = weight -> new Apple(weight); //建構函式引用寫法: Apple::new Function<String, Apple> h = f.andThen(g); // andThen()相當於數學上的 g(f(x)) 函式 Apple apple = h.apply("99"); //result: Apple(color=null, weight=99) Function<Apple, String> y = Apple::getColor; Function<Apple, Integer> z = f.compose(y); // compose()相當於數學上的 f(y(x)) 函式 Integer result = z.apply(new Apple("red", 78));//會報 java.lang.NumberFormatException: For input string: "red" 異常 } /** * 謂詞的組合用法 * and和or方法是按照在表示式鏈中的位置,從左到右確定優先順序的,如a.or(b).and(c).or(d) 可以看成 ((a || b) && c) || d */ @Test public void test14() { Predicate<Apple> p1 = apple -> "green".equals(apple.getColor()); final Predicate<Apple> negate = p1.negate(); //非 System.out.println(negate.test(new Apple("green", 98)));// result: false final Predicate<Apple> and = p1.and(apple -> apple.getWeight() > 150);//與 System.out.println(and.test(new Apple("green", 140)));//result: false final Predicate<Apple> or = p1.or(apple -> apple.getWeight() > 150);//或 System.out.println(or.test(new Apple("blue", 170)));//result: true } /** * 比較器組合的用法 */ @Test public void test13() { inventory.sort(Comparator.comparing(Apple::getWeight).reversed());//蘋果按重量倒序排序 System.out.println(inventory); //蘋果按重量倒序排序,當蘋果重量相同時,按顏色升序排序 inventory.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor)); System.out.println(inventory); }
參考資料
函數語言程式設計初探
Java 8