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

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

// 距離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 時間操做等。python

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

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

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

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

2. Lambda 介紹

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

3. 函數接口介紹

上面說了此次是介紹 Lambda 表達式,爲何要介紹函數接口呢?其實 Java 中的函數接口在使用時,能夠隱式的轉換成 Lambda 表達式,在 Java 8中已經有不少接口已經聲明爲函數接口,如 Runnable、Callable、Comparator 等。shell

函數接口的例子能夠看下 Java 8 中的 Runnable 源碼(去掉了註釋)。express

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 核心知識整理&面試資料。

公衆號
相關文章
相關標籤/搜索