天天看點

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

lambda文法初探

java8 lambda表達式文法的兩種格式:

  • (parameters)  ->  expression
  • (parameters) -> {statements;}

文法解讀:

  1. (parameters),lambda表達式的參數清單,其定義方法為JAVA普通的方法相同,例如(Object a, Object b)。
  2. -> 箭頭,是參數清單與lambda表達式主題部分的分隔符号。
  3. expression 單表達式
  4. {statements; } 語句。

測試:如下語句是否是正确的lambda表達式。

(1)  () -> {}

(2)  () -> "Raoul"

(3)  () -> {return "Mario";}

(4)  (Integer i) -> return "Alan" + i;

(5)  (String s) -> {"IronMan";}

正解:

(1) 正确。如果使用匿名類(接口名統一使用IDemoLambda)表示如下:

1 new IDemoLambda() {
2     public void test() {
3     }
4}
           

(2)正确。如果使用匿名類(接口名統一使用IDemoLambda)表示如下:

1 new IDemoLambda() {
2     public String test() {
3return "Raoul";  // 如果直接接一個值,表示傳回該值
4     }
5}
           

(3)正确。如果使用匿名類(接口名統一使用IDemoLambda)表示如下:

1 new IDemoLambda() {
2     public String test() {
3return "Mario";
4     }
5}
           

(4)錯誤。因為return是流程控制語句,表示傳回,不是一個表達式,故不符合lambda文法,正确的表示方法應該是 (Integer i) ->{ return "Alan" + i;}。如果使用匿名類(接口名統一使用IDemoLambda)表示如下:

1 new IDemoLambda() {
2     public String test(Integer i) {
3return "Alan" + i;
4     }
5}
           

(5)錯誤。因為"IronMan"是一個表達式,并不是一個語句,故不能使用{}修飾,應修改為 (String s) -> "IronMan"。如果使用匿名類(接口名統一使用IDemoLambda)表示如下:

1 new IDemoLambda() {
2     public String test(String s) {
3return "IronMan";
4     }
5}
           

初步接觸函數式接口

在java8中,一個接口如果隻定義了一個抽象方法,那這個接口就可以稱為函數式接口,就可以使用lambda表達式來簡化程式代碼。Lambda表達式可以直接指派給變量,也可以直接作為參數傳遞給函數,示例如下:

1public static void startThread(Runnable a) {
 2    (new Thread(a)).start();
 3}
 4
 5public static void main(String[] args) {
 6    // lambda表達式可以直接指派給變量,也可以直接以參數的形式傳遞給方法、
 7    Runnable a = () -> {
 8        System.out.println("Hello World,Lambda...");
 9    };
10    // JDK8之前使用匿名類來實作
11    Runnable b = new Runnable() {
12        @Override
13        public void run() {
14            System.out.println("Hello World,Lambda...");
15        }
16    };
17    startThread(a);
18    startThread(() -> {
19        System.out.println("Hello World,Lambda...");
20    });
21}
           

那能将(int a) -> {System.out.println("Hello World, Lambda…");}表達式指派給Runnable a變量嗎?答案是不能,因為該表達式不符合函數式程式設計接口(Runnable)唯一抽象方法的函數簽名清單。

Runnable的函數式簽名清單為public abstract void run();

溫馨提示:如果我們有留意JDK8的Runnable接口的定義,你會發現給接口相對JDK8之前的版本多了一個注解:@FunctionalInterface,該注解是一個辨別注解,用來辨別這個接口是一個函數式接口。如果我們人為在一個不滿足函數式定義的接口上增加@FunctionalInterface,則會在編譯時提示錯誤。

Lambda表達式實戰思考

例如有如下代碼:

1/**
 2 * 處理檔案:目前需求是處理檔案的第一行資料
 3 * @return
 4 * @throws IOException
 5 */
 6public static String processFile() throws IOException {
 7    try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
 8        return  br.readLine();
 9    }
10}
           

目前需求為處理檔案的第一行資料,那問題來了,如果需求變化需要傳回檔案的第一行和第二行資料,那該如何進行改造呢?

在理想的情況下,需要重用執行設定和關閉流的代碼,并告訴processFile()方法對檔案執行不同的操作,換句話說就是要實作對processFile的行為進行參數化。

Step·1:行為參數化

要讀取檔案的頭兩行,用Lambda文法如何實作呢?思考一下,下面這條語句是否可以實作?

(BufferedReader bf) -> br.readLine() + br.readLine()

答案是當然可以,接下來就要思考,定義一個什麼樣的方法,能接收上面這個參數。

Step2:使用函數式接口來傳遞行為

要使用(bufferedReader bf) -> br.readLine() + br.readLine(),則需要定義一個接受參數為BufferedReader,并傳回String類型的函數式接口。

定義如下:

1@FunctionalInterface
2public interface BufferedReaderProcessor {
3     public String process(BufferedReader b) throws IoException;
4}
           

那把processFile方法改造成如下代碼:

1/**
 2 * 處理檔案:目前需求是處理檔案的第一行資料
 3 * @return
 4 * @throws IOException
 5 */
 6public static String processFile(BufferedReaderProcess brp) throws IOException {
 7    try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
 8        return  brp.process(br);
 9    }
10}
           

Step3:使用lambda表達式作為參數進行傳遞

将行為參數化後,并對方法進行改造,使方法接受一個函數式程式設計接口後,就可以将Lambda表達式直接傳遞給方法,例如:

1processFile(  (BufferedReader br)  -> br.readLine()  );
2processFile( (BufferedReader bf) -> br.readLine() + br.readLine()); 
           

Java8中自定義函數式接口

從上面的講解中我們已然能夠得知,要能夠将Lambda表達式當成方法參數進行參數行為化的一個前提條件是首先要在方法清單中使用一個函數式接口,例如上例中的BufferReaderProcess,那如果每次使用Labmbda表達式之前都要定義各自的函數式程式設計接口,那也夠麻煩的,那有沒有一種方式,或定義一種通用的函數式程式設計接口呢?答案是肯定的,Java8的設計者,利用泛型,定義了一整套函數式程式設計接口,下面将介紹java8中常用的函數式程式設計接口。

Predicate

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

所謂函數式程式設計接口就是隻能定義一個抽象方法,Predicate函數接口中定義的抽象方法為boolean test(T t),對應的函數式行為為接收一類對象t,傳回boolean類型,其可用的lambda表達式為(T t) -> boolean類型的表達式,例如(Sample a) -> a.isEmpty()。

該接口通常的應用場景為過濾。例如,要定義一個方法,從集合中進行刷選,具體的刷選邏輯(行為)由參數進行指定,那我們可以定義這樣一個刷選的方法:

1public static <T> List<T> filter(List<T> list, Predicate<T> p) {
2List<T> results = new ArrayList<>();
3for(T s: list){
4if(p.test(s)){
5results.add(s);
6}
7}
8return results;
9}
           

上述函數,我們可以這樣進行調用:

1Predicate<String> behaviorFilter = (String s) -> !s.isEmpty();  // lambda表達式指派給一個變量
2filter(behaviorFilter);  
           

其它add等方法,将在下文介紹(複合lambda表達式)。

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:IntPredicate、LongPredicate、DoublePredicate。我們選擇LongPredicate看一下其函數接口的聲明:

1boolean test(long value);
           

Consumer

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

該函數式程式設計接口适合對對象進行處理,但沒有傳回值,對應的函數描述符:T -> void

舉例如下:

1public static <T> void forEach(List<T> list, Consumer<T> c) {
2    for(T t : list) {
3        c.accept(t);
4    }
5}
           

其調用示例如下:

1forEach(  Arrays.asList(1,2,3,4,5),   (Integer i) -> System.out.println(i) ); 
           

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:IntConsumer、LongConsumer、DoubleConsumer。

Function

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

其适合的場景是,接收一個泛型T的對象,傳回一個泛型為R的對象,其對應的函數描述符:  T -> R。

示例如下:

1public static <T,R> List<R> map(List<T> list, Function<T,R> f) {
2          List<R> result = new ArrayList<>();
3          for(T t : list) {
4                result.add(  f.apply(t) );
5          }
6          return result;
7}
8List<Integer> l = map(Arrays.asList("lambdas", "in", "action"),  (String s)  -> s.length  );
           

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:IntFunction< R>、LongFunction< R>、DoubleFunction< R>、IntToDoubleFunction、IntToLongFunction、LongToIntFunction、LongToDoubleFunction、ToIntFunction< T>、ToDoubleFunction< T>、ToLongFunction< T>。

Supplier< T>

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

函數描述符:() -> T。适合建立對象的場景,例如  () -> new Object();

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier。

UnaryOperator< T >

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

一進制運算符函數式接口,接收一個泛型T的對象,同樣傳回一個泛型T的對象。

1public static <T> List<T> map(List<T> list, UnaryOperator<T> f) {
2          List<R> result = new ArrayList<>();
3          for(T t : list) {
4                result.add(  f.apply(t) );
5          }
6          return result;
7}
8
9map(  list, (int i) -> i ++ );
           

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:IntUnaryOperator、LongUnaryOperator、DoubleUnaryOperator。

BiPredicate

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

接收兩個參數,傳回boolean類型。其對應的函數描述符:(T,U) -> boolean。

BiConsumer

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

在這裡插入圖檔描述

與Consume函數式接口類似,隻是該接口接收兩個參數,對應的函數描述符(T,U)  -> void。

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:ObjIntConsumer、ObjLongConsumer、ObjDoubleConsumer。

BiFunction

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

與Function函數式接口類似,其對應的函數描述符:(T,U) -> R。

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:ToIntBiFunction(T,U)、ToLongBiFunction(T,U)、ToDoubleBiFunction(T,U)。

BinaryOperator< T >

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

二維運算符,接收兩個T類型的對象,傳回一個T類型的對象。

另外,為了避免java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了如下函數式程式設計接口:IntBinaryOperator、LongBinaryOperator、DoubleBinaryOperator。

上述就是JDK8定義在java.util.function中的函數式程式設計接口。重點關注的是其定義的函數式程式設計接口,其複合操作相關的API将在下文中詳細介紹。

類型檢查、類型推斷以及限制

類型檢查

java8是如何檢查傳入的Lambda表示式是否符合約定的類型呢?

例如

1public static <T> List<T> filter(List<T> list, Predicate<T> p) {
 2    List<T> results = new ArrayList<>();
 3    for(T s: list){
 4        if(p.test(s)){
 5            results.add(s);
 6       }
 7   }
 8   return results;
 9}
10
11List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
           
  • 其類型檢測的步驟:

    首先檢視filter函數的參數清單,得出Lambda對應的參數類型為Predicate。

  • 函數式接口Predicate中定義的抽象接口為  boolean test(T t),對應的函數描述符(  T  ->  boolean)。
  • 驗證Lambda表達式是否符合函數描述符。

注意:如果一個Lambda的主體式一個語句表達式,它就和一個傳回void的函數描述符相容(當然參數清單也必須相容)。例如,以下兩行都是合法的,盡管List的add方法傳回一個boolean,而不式Consumer上下文(T -> void)所要求的void:

1// Predicate傳回了一個boolean
2Predicate<String> p = s -> list.add(s);
3// Consumer傳回了一個void
4Consumer<String> b = s -> list.add(s);
           

思考題:如下表達式是否正确?

1Object o = () -> {System.out.println("Tricky example"); };
           

答案是錯誤的,該語句的含義就是把lambda表達式複制給目标對象(Object o),lambda對應的函數描述符為() -> void,期望目标對象擁有一個唯一的抽象方法,參數清單為空,傳回值為void的方法,顯然目标對象Object不滿足該條件,如果換成如下示例,則能編譯通過:

1Runnable r = () {System.out.println("Tricky example"); };
           

因為Runnable的定義如下:

java8實戰讀書筆記:Lambda表達式文法與函數式程式設計接口

類型推斷

所謂的類型推斷,指的式java編譯器能根據目标類型來推斷出用什麼函數式接口來配合Lambda表達式,這也意味着它也可以推斷出适合Lambda的簽名,因為函數描述符可以通過目标類型得到。

例如:

1List<Apple> greenApples =  filter(inventory, (Apple a) -> "green".equals(a.getColor()));
2也可以寫成
3List<Apple> greenApples =  filter(inventory, a  -> "green".equals(a.getColor()));
4
5Lambda表達式有多個參數,代碼可讀性的好處就更為明顯。例如,你可以這樣來建立一個Comparator 對象:
6Comparator<Apple> c =  (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
7Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
           

由于java編譯器能根據目标類型來推導出Lambda的函數簽名,故lambda的函數簽名清單時,可以去掉參數的類型。

局部變量

Lambda表達式主體部分也能引入外部的變量,例如:

1int portNumber = 1337;
2Runnable r = () -> System.out.println(portNumber);
           

其中portNumber參數并不是方法簽名參數,但這樣有一個限制條件,引入的局部變量必須是常量(實際意義上的常量,可以不用final來定義,但不能改變其值。例如如下示例是錯誤的:

1int portNumber = 1337;
2Runnable r = () -> System.out.println(portNumber);
3portNumber = 1228;  // 因為portNumber的值已改變,不符合局部變量的捕獲條件,上述代碼無法編譯通過。
           

方法引用

方法引用常用的構造方法

JDK8中有3中方法引用:

(1)指向靜态方法的方法引用

   Integer.parseInt  對應的方法引用可以寫成: Integer::parseInt。

(2)指向任意類型的執行個體方法的引用

 (Strng str ) -> str.length  對應的方法引用:String::length。(注意這裡的屬性為方法清單)

(3)lambda捕獲外部的執行個體對象

   例如如下代碼:

1    Apple a = new Apple();
2    process(  () -> a.getColor()  );  // 則可以寫成  process ( a::getColor ); 
           

構造函數引用

大家可以回想一下,jdk8中定義了一個建立對象的函數式程式設計接口Supplier,函數描述符:() -> T。适合建立對象的場景,例如  () -> new Object();

對于沒有構造函數的,我們可以這樣來建立對象:

1Supplier<Apple> c1 = Apple:new;
2Apple a1 = c1.get();
           
1Function<Integer, Apple> c2 = Apple::new;
2Apple a2 = c2.apply(weight);