還看不懂同事的程式碼?Lambda 表示式、函式介面瞭解一下

未讀程式碼發表於2019-11-12

當前時間:2019年 11月 11日,距離 JDK 14 釋出時間(2020年3月17日)還有多少天?

// 距離JDK 14 釋出還有多少天?
LocalDate jdk14 = LocalDate.of(2020, 3, 17);
LocalDate nowDate = LocalDate.now();
System.out.println("距離JDK 14 釋出還有:"+nowDate.until(jdk14,ChronoUnit.DAYS)+"天");
複製程式碼

1. 前言

Java 8 早已經在2014 年 3月 18日釋出,毫無疑問 Java 8 對 Java 來說絕對算得上是一次重大版本更新,它包含了十多項語言、庫、工具、JVM 等方面的新特性。比如提供了語言級的匿名函式,也就是被官方稱為 Lambda 的表示式語法(外界也稱為閉包,Lambda 的引入也讓流式操作成為可能,減少了程式碼編寫的複雜性),比如函式式介面,方法引用,重複註解。再比如 Optional 預防空指標,Stearm 流式操作,LocalDateTime 時間操作等。

在前面的文章裡已經介紹了 Java 8 的部分新特性。

  1. Jdk14 都要出了,Jdk8 的時間處理姿勢還不瞭解一下?

  2. Jdk14都要出了,還不能使用 Optional優雅的處理空指標?

這一次主要介紹一下 Lambda 的相關情況。

2. Lambda 介紹

Lambda 名字來源於希臘字母表中排序第十一位的字母 λ,大寫為Λ,英語名稱為 Lambda。在 Java 中 Lambda 表示式(lambda expression)是一個匿名函式,在編寫 Java 中的 Lambda 的時候,你也會發現 Lambda 不僅沒有函式名稱,有時候甚至連入參和返回都可以省略,這也讓程式碼變得更加緊湊。

3. 函式介面介紹

上面說了這次是介紹 Lambda 表示式,為什麼要介紹函式介面呢?其實 Java 中的函式介面在使用時,可以隱式的轉換成 Lambda 表示式,在 Java 8中已經有很多介面已經宣告為函式介面,如 Runnable、Callable、Comparator 等。

函式介面的例子可以看下 Java 8 中的 Runnable 原始碼(去掉了註釋)。

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
複製程式碼

那麼什麼樣子的介面才是函式介面呢?有一個很簡單的定義,也就是隻有一個抽象函式的介面,函式介面使用註解 @FunctionalInterface 進行宣告(註解宣告不是必須的,如果沒有註解,也是隻有一個抽象函式,依舊會被認為是函式介面)。多一個或者少一個抽象函式都不能定義為函式介面,如果使用了函式介面註解又不止一個抽象函式,那麼編譯器會拒絕編譯。函式介面在使用時候可以隱式的轉換成 Lambda 表示式。

Java 8 中很多有很多不同功能的函式介面定義,都放在了 Java 8 新增的 java.util.function包內。下面是一些關於 Java 8 中函式介面功能的描述。

序號 介面 & 描述
BiConsumer 代表了一個接受兩個輸入引數的操作,並且不返回任何結果
BiFunction 代表了一個接受兩個輸入引數的方法,並且返回一個結果
BinaryOperator 代表了一個作用於於兩個同型別操作符的操作,並且返回了操作符同型別的結果
BiPredicate 代表了一個兩個引數的boolean值方法
BooleanSupplier 代表了boolean值結果的提供方
Consumer 代表了接受一個輸入引數並且無返回的操作
DoubleBinaryOperator 代表了作用於兩個double值操作符的操作,並且返回了一個double值的結果。
DoubleConsumer 代表一個接受double值引數的操作,並且不返回結果。
DoubleFunction 代表接受一個double值引數的方法,並且返回結果
DoublePredicate 代表一個擁有double值引數的boolean值方法
DoubleSupplier 代表一個double值結構的提供方
DoubleToIntFunction 接受一個double型別輸入,返回一個int型別結果。
DoubleToLongFunction 接受一個double型別輸入,返回一個long型別結果
DoubleUnaryOperator 接受一個引數同為型別double,返回值型別也為double 。
Function 接受一個輸入引數,返回一個結果。
IntBinaryOperator 接受兩個引數同為型別int,返回值型別也為int 。
IntConsumer 接受一個int型別的輸入引數,無返回值 。
IntFunction 接受一個int型別輸入引數,返回一個結果 。
IntPredicate 接受一個int輸入引數,返回一個布林值的結果。
IntSupplier 無引數,返回一個int型別結果。
IntToDoubleFunction 接受一個int型別輸入,返回一個double型別結果 。
IntToLongFunction 接受一個int型別輸入,返回一個long型別結果。
IntUnaryOperator 接受一個引數同為型別int,返回值型別也為int 。
LongBinaryOperator 接受兩個引數同為型別long,返回值型別也為long。
LongConsumer 接受一個long型別的輸入引數,無返回值。
LongFunction 接受一個long型別輸入引數,返回一個結果。
LongPredicate 接受一個long輸入引數,返回一個布林值型別結果。
LongSupplier 無引數,返回一個結果long型別的值。
LongToDoubleFunction 接受一個long型別輸入,返回一個double型別結果。
LongToIntFunction 接受一個long型別輸入,返回一個int型別結果。
LongUnaryOperator 接受一個引數同為型別long,返回值型別也為long。
ObjDoubleConsumer 接受一個object型別和一個double型別的輸入引數,無返回值。
ObjIntConsumer 接受一個object型別和一個int型別的輸入引數,無返回值。
ObjLongConsumer 接受一個object型別和一個long型別的輸入引數,無返回值。
Predicate 接受一個輸入引數,返回一個布林值結果。
Supplier 無引數,返回一個結果。
ToDoubleBiFunction 接受兩個輸入引數,返回一個double型別結果
ToDoubleFunction 接受一個輸入引數,返回一個double型別結果
ToIntBiFunction 接受兩個輸入引數,返回一個int型別結果。
ToIntFunction 接受一個輸入引數,返回一個int型別結果。
ToLongBiFunction 接受兩個輸入引數,返回一個long型別結果。
ToLongFunction 接受一個輸入引數,返回一個long型別結果。
UnaryOperator 接受一個引數為型別T,返回值型別也為T。

(上面表格來源於菜鳥教程)

3. Lambda 語法

Lambda 的語法主要是下面幾種。

  1. (params) -> expression

  2. (params) -> {statements;}

Lambda 的語法特性。

  1. 使用 -> 分割 Lambda 引數和處理語句。
  2. 型別可選,可以不指定引數型別,編譯器可以自動判斷。
  3. 圓括號可選,如果只有一個引數,可以不需要圓括號,多個引數必須要圓括號。
  4. 花括號可選,一個語句可以不用花括號,多個引數則花括號必須。
  5. 返回值可選,如果只有一個表示式,可以自動返回,不需要 return 語句;花括號中需要 return 語法。 6. Lambda 中引用的外部變數必須為 final 型別,內部宣告的變數不可修改,內部宣告的變數名稱不能與外部變數名相同。

舉幾個具體的例子, params 在只有一個引數或者沒有引數的時候,可以直接省略不寫,像這樣。

// 1.不需要引數,沒有返回值,輸出 hello
()->System.out.pritnln("hello");

// 2.不需要引數,返回 hello
()->"hello";

// 3. 接受2個引數(數字),返回兩數之和 
(x, y) -> x + y  
  
// 4. 接受2個數字引數,返回兩數之和 
(int x, int y) -> x + y  
  
// 5. 兩個數字引數,如果都大於10,返回和,如果都小於10,返回差
(int x,int y) ->{
  if( x > 10 && y > 10){
    return x + y;
  }
  if( x < 10 && y < 10){
    return Math.abs(x-y);
  }
};
複製程式碼

通過上面的幾種情況,已經可以大致瞭解 Lambda 的語法結構了。

4. Lambda 使用

4.1 對於函式介面

從上面的介紹中已經知道了 Runnable 介面已經是函式介面了,它可以隱式的轉換為 Lambda 表示式進行使用,通過下面的建立執行緒並執行的例子看下 Java 8 中 Lambda 表示式的具體使用方式。

/**
 * Lambda 的使用,使用 Runnable 例子
 * @throws InterruptedException
 */
@Test
public void createLambda() throws InterruptedException {
    // 使用 Lambda 之前
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("JDK8 之前的執行緒建立");
        }
    };
   new Thread(runnable).start();
   // 使用 Lambda 之後
   Runnable runnable1Jdk8 = () -> System.out.println("JDK8 之後的執行緒建立");
   new Thread(runnable1Jdk8).start();
   // 更加緊湊的方式
   new Thread(() -> System.out.println("JDK8 之後的執行緒建立")).start();
}
複製程式碼

可以發現 Java 8 中的 Lambda 碰到了函式介面 Runnable,自動推斷了要執行的 run 方法,不僅省去了 run 方法的編寫,也程式碼變得更加緊湊。

執行得到結果如下。

JDK8 之前的執行緒建立
JDK8 之後的執行緒建立
JDK8 之後的執行緒建立
複製程式碼

上面的 Runnable 函式介面裡的 run 方法是沒有引數的情況,如果是有引數的,那麼怎麼使用呢?我們編寫一個函式介面,寫一個 say 方法接受兩個引數。

/**
 * 定義函式介面
 */
@FunctionalInterface
public interface FunctionInterfaceDemo {
    void say(String name, int age);
} 
複製程式碼

編寫一個測試類。

 /**
  * 函式介面,Lambda 測試
  */
 @Test
 public void functionLambdaTest() {
     FunctionInterfaceDemo demo = (name, age) -> System.out.println("我叫" + name + ",我今年" + age + "歲了");
     demo.say("金庸", 99);
 }
複製程式碼

輸出結果。

我叫金庸,我今年99歲了。
複製程式碼

4.2 對於方法引用

方法引用這個概念前面還沒有介紹過,方法引用可以讓我們直接訪問類的例項或者方法,在 Lambda 只是執行一個方法的時候,就可以不用 Lambda 的編寫方式,而用方法引用的方式:例項/類::方法。這樣不僅程式碼更加的緊湊,而且可以增加程式碼的可讀性。

通過一個例子檢視方法引用。

@Getter
@Setter
@ToString
@AllArgsConstructor
static class User {
    private String name;
    private Integer age;
}
public static List<User> userList = new ArrayList<User>();
static {
    userList.add(new User("A", 26));
    userList.add(new User("B", 18));
    userList.add(new User("C", 23));
    userList.add(new User("D", 19));
}
/**
 * 測試方法引用
 */
@Test
public void methodRef() {
    User[] userArr = new User[userList.size()];
    userList.toArray(userArr);
    // User::getAge 呼叫 getAge 方法
    Arrays.sort(userArr, Comparator.comparing(User::getAge));
    for (User user : userArr) {
        System.out.println(user);
    }
}
複製程式碼

得到輸出結果。

Jdk8Lambda.User(name=B, age=18) Jdk8Lambda.User(name=D, age=19) Jdk8Lambda.User(name=C, age=23) Jdk8Lambda.User(name=A, age=26)

4.3 對於遍歷方式

Lambda 帶來了新的遍歷方式,Java 8 為集合增加了 foreach 方法,它可以接受函式介面進行操作。下面看一下 Lambda 的集合遍歷方式。

/**
 * 新的遍歷方式
 */
@Test
public void foreachTest() {
    List<String> skills = Arrays.asList("java", "golang", "c++", "c", "python");
    // 使用 Lambda 之前
    for (String skill : skills) {
        System.out.print(skill+",");
    }
    System.out.println();
    // 使用 Lambda 之後
    // 方式1,forEach+lambda
    skills.forEach((skill) -> System.out.print(skill+","));
    System.out.println();
    // 方式2,forEach+方法引用
    skills.forEach(System.out::print);
}
複製程式碼

執行得到輸出。

java,golang,c++,c,python,
java,golang,c++,c,python,
javagolangc++cpython
複製程式碼

4.4 對於流式操作

得益於 Lambda 的引入,讓 Java 8 中的流式操作成為可能,Java 8 提供了 stream 類用於獲取資料流,它專注對資料集合進行各種高效便利操作,提高了程式設計效率,且同時支援序列和並行的兩種模式匯聚計算。能充分的利用多核優勢。

流式操作如此強大, Lambda 在流式操作中怎麼使用呢?下面來感受流操作帶來的方便與高效。

流式操作一切從這裡開始。

// 為集合建立序列流
stream()
// 為集合建立並行流
parallelStream()
複製程式碼

流式操作的去重 distinct和過濾 filter

@Test
public void streamTest() {
    List<String> skills = Arrays.asList("java", "golang", "c++", "c", "python", "java");
    // Jdk8 之前
    for (String skill : skills) {
        System.out.print(skill + ",");
    }
    System.out.println();
    // Jdk8 之後-去重遍歷
    skills.stream().distinct().forEach(skill -> System.out.print(skill + ","));
    System.out.println();
    // Jdk8 之後-去重遍歷
    skills.stream().distinct().forEach(System.out::print);
    System.out.println();
    // Jdk8 之後-去重,過濾掉 ptyhon 再遍歷
    skills.stream().distinct().filter(skill -> skill != "python").forEach(skill -> System.out.print(skill + ","));
    System.out.println();
    // Jdk8 之後轉字串
    String skillString = String.join(",", skills);
    System.out.println(skillString);
}
複製程式碼

執行得到結果。

java,golang,c++,c,python,java,
java,golang,c++,c,python,
javagolangc++cpython
java,golang,c++,c,
java,golang,c++,c,python,java
複製程式碼

流式操作的資料轉換(也稱對映)map

 /**
  * 資料轉換
  */
 @Test
 public void mapTest() {
     List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5);
     // 資料轉換
     numList.stream().map(num -> num * num).forEach(num -> System.out.print(num + ","));

     System.out.println();

     // 資料收集
     Set<Integer> numSet = numList.stream().map(num -> num * num).collect(Collectors.toSet());
     numSet.forEach(num -> System.out.print(num + ","));
 }
複製程式碼

執行得到結果。

1,4,9,16,25,
16,1,4,9,25,
複製程式碼

流式操作的數學計算。

/**
 * 數學計算測試
 */
@Test
public void mapMathTest() {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    IntSummaryStatistics stats = list.stream().mapToInt(x -> x).summaryStatistics();
    System.out.println("最小值:" + stats.getMin());
    System.out.println("最大值:" + stats.getMax());
    System.out.println("個數:" + stats.getCount());
    System.out.println("和:" + stats.getSum());
    System.out.println("平均數:" + stats.getAverage());
    // 求和的另一種方式
    Integer integer = list.stream().reduce((sum, cost) -> sum + cost).get();
    System.out.println(integer);
}
複製程式碼

執行得到結果。

得到輸出
最小值:1
最大值:5
個數:5
和:15
平均數:3.0
15
複製程式碼

5. Lambda 總結

Lamdba 結合函式介面,方法引用,型別推導以及流式操作,可以讓程式碼變得更加簡潔緊湊,也可以藉此開發出更加強大且支援平行計算的程式,函式程式設計也為 Java 帶來了新的程式設計方式。但是缺點也很明顯,在實際的使用過程中可能會發現調式困難,測試表示 Lamdba 的遍歷效能並不如 for 的效能高,同事可能沒有學習導致看不懂 Lamdba 等(可以推薦來看這篇文章)。

文章程式碼已經上傳到 github.com/niumoo/jdk-…

<完>

個人網站:www.codingme.net
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回覆資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。

公眾號

相關文章