1. 程式人生 > >Java8系列 (一) Lambda表示式

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