Java8系列 (一) Lambda表達式

  • 2019 年 10 月 28 日
  • 筆記

函數式編程

在介紹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實戰