還看不懂同事的代碼?Lambda 表達式、函數接口了解一下


當前時間: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 等(可以推薦來看這篇文章)。

文章代碼已經上傳到 https://github.com/niumoo/jdk-feature)

<完>

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM