1. 程式人生 > 其它 >Java8學習筆記之Lambda表示式

Java8學習筆記之Lambda表示式

技術標籤:javaSEjava

文章目錄

一.前言

從2014年3月Java8釋出到2020年3月17日Java14正式GA,Java版本更新迭代很快。但是公司的一些在維護的舊專案還是停留在JDK1.7,只有新的專案才要求用JDK1.8,然而我們很多人對Java8的一些新特性還不是很熟悉,所以這裡做個Java8新特性的總結。

二.Lambda介紹

1.引子–行為引數化傳遞程式碼

場景示例:

假設產品提出這樣一個需求:從員工列表中篩選出部門編號是"20"的員工,程式設計師編寫程式碼如下:

  /**
     * 找出部門編號是20的員工
     *
     * @param empList 所有員工列表
     * @return
     */
    public List<Emp> filterDeptNo(List<Emp> empList) {
        List<Emp> resultList = new ArrayList<>(
); for (Emp emp : empList) { if (emp.getDeptno() == 20) { resultList.add(emp); } } return resultList; }

程式碼寫完之後,產品提了一個新需求:要能夠篩選出其他部門的員工,這時候程式設計師修改了程式碼,將部門編號作為引數,這樣就能夠靈活的應對需求的變化。程式碼如下:

    /**
     * 找出部門編號是20的員工
     *
     * @param empList 所有員工列表
     * @param deptNo  部門編號
     * @return
     */
public List<Emp> filterDeptNo(List<Emp> empList, int deptNo) { List<Emp> resultList = new ArrayList<>(); for (Emp emp : empList) { if (emp.getDeptno() == deptNo) { resultList.add(emp); } } return resultList; }

又過了幾個小時,產品說:還需要能夠篩選出薪水大於15K的員工,於是,程式設計師編寫了一個新方法,用另外一個引數來代表薪水,程式碼如下:

/**
     * 篩選出大於指定薪水值的員工
     *
     * @param empList 員工列表
     * @param sale    指定的薪水
     * @return
     */
    public List<Emp> filterSale(List<Emp> empList, double sale) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (emp.getSal() >= sale) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

可以看出,除了方法名稱、引數、和判斷邏輯,其他都是一樣的程式碼,有大量的重複程式碼出現。這時候程式設計師想到了把所有判斷屬性都結合起來,程式碼如下:

     /**
     * 通過flag判斷是篩選部門編號還是篩選薪水
     *
     * @param empList
     * @param deptNo
     * @param sale
     * @param flag
     * @return
     */
    public List<Emp> filterEmp(List<Emp> empList, int deptNo, double sale, boolean flag) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if ((flag && emp.getDeptno() == deptNo)
                    || (!flag && (emp.getSal() >= sale))) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

這個方法看起來很笨拙,方法引數太多不利於擴充套件修改。這種通過新增很多引數來應對變化的需求的方式不是很好的方案。

我們這裡的場景是根據員工的某些屬性(比如員工的編號,員工的薪水等)來篩選過濾,可以把判斷員工的不同屬性當作不同的演算法來對待,使用策略模式來應對不斷變化的需求。

策略模式使用的就是面向物件的繼承和多型機制,首先將判斷員工屬性抽象為一個“抽象策略角色”,他是判斷員工屬性演算法的抽象,通常就是一個介面,定義每個策略或演算法必須具有的方法和屬性。如下:

/**
 * 員工抽象策略角色
 */
public interface EmpPredicate {
    /**
     * 策略模式的演算法
     * @return
     */
    public boolean test();
}

用EmpPredicate的多個實現代表不同的選擇標準(策略),具體策略就是普通的一個實現類,只要實現介面中的方法就可以。如下:

/**
 * 具體策略(演算法)角色1
 */
public class EmpDeptNoPredicate implements EmpPredicate {
    /**
     * 僅僅篩選出部門20的員工
     */
    @Override
    public boolean test(Emp emp) {
        return "20".equals(emp.getDeptno());
    }
}
/**
 * 具體策略(演算法)角色2
 */
public class EmpSalePredicate implements EmpPredicate {

    /**
     * 僅僅選出薪水大於1萬的
     */
    @Override
    public boolean test(Emp emp) {
        return emp.getSal() > 10000;
    }
}

現在我們修改filterEmp方法,讓方法接受empPredicate物件,對Emp做條件測試。這就是行為引數化:讓方法接受多種行為(策略)作為引數,並在內部使用,來完成不同的行為。

行為引數化可以讓程式碼更好的適應不斷變化的需求,減輕未來的工作量。

/**
     * 員工過濾器
     * @param empList
     * @param predicate
     * @return
     */
    public List<Emp> filterEmp(List<Emp> empList, EmpPredicate predicate) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (predicate.test(emp)) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

現在我們可以建立不同的EmpPredicate物件,並將它們傳遞給filterEmp方法。這樣更靈活的應對變化的需求。

//篩選出部門20的員工  
List<Emp> resultList = this.filterEmp(empList, new EmpDeptNoPredicate());
//選出薪水大於1萬的
List<Emp> resultList = this.filterEmp(empList, new EmpSalePredicate());

從中可以看出,當要把新的行為(策略)傳遞給filterEmp方法的時候,我們不得不宣告好幾個實現EmpPredicate介面的類,
然後例項化好幾個只會提到一次的EmpPredicate物件。這就是策略模式的缺點:每一個策略(演算法)都是一個類,
複用的可能性很小,類數量增多,並且所有的策略類都需要對外暴露。

下面我們使用匿名內部類改善程式碼,讓它變得更簡潔。匿名內部類可以同時宣告和例項化一個類。

List<Emp> resultList = this.filterEmp(empList, new EmpSalePredicate() {
            @Override
            public boolean test(Emp emp) {
                return emp.getSal() > 10000;
            }
  });
List<Emp> resultList = this.filterEmp(empList, new EmpDeptNoPredicate() {
            @Override
            public boolean test(Emp emp) {
                return "20".equals(emp.getDeptno());
            }
});

使用匿名內部類處理雖然在某種程度上改善了為一個介面宣告好幾個實體類的問題,但是還是不夠友好,還是存在模板程式碼。

這時候該Java8的Lambda表示式登場了,Lambda表示式的主要作用就是代替匿名內部類的煩瑣語法;如下:

   List<Emp> resultList = this.filterEmp(empList, (Emp emp) -> "20".equals(emp.getDeptno()));

Java8的Lambda表示式是一種更簡潔的傳遞程式碼的方式。

傳遞程式碼:就是將新行為(策略或者演算法)作為引數傳遞給方法。但在Java8之前實現起來比較麻煩(為介面宣告許多隻用一次的實體類而造成的類數量增多),在Java8之前可以用匿名內部類來簡化程式碼。

下面一節我們詳細說明下Lambda表示式。

2.Lambda表示式簡介

可以把Lambda表示式理解為簡潔地表示可傳遞的匿名函式的一種方式,一個lambda表示式是一個帶有引數的程式碼塊:它沒有名稱,但它有引數列表、函式主體、返回型別,可能還有一個可以丟擲的異常列表。

Lambda表示式有如下幾個特點:

  1. 匿名:它不像普通的方法那樣有一個明確的名稱。
  2. 函式:Lambda函式不像方法那樣屬於某個特定的類。但是和方法一樣,有引數列表、函式體、返回型別,還可以有可以丟擲的異常列表。
  3. 傳遞:Lambda表示式可以作為引數傳遞給方法或者儲存在變數中。
  4. 簡潔:無需像匿名類那樣寫很多模板程式碼。

Lambda表示式的格式:引數列表、箭頭 ->、和一個程式碼塊。基本語法是:

(parameters)-> expression 或者

(parameters)-> { statements }

如果程式碼塊只包含一條語句,Lambda表示式允許省略程式碼塊的花括號。

Lambda程式碼塊只有一條return語句,可以省略return關鍵字。

表示式和語句的區別:

1.表示式總是能夠返回一個值;而語句則只幹活,並不返回,一個語句由1個或多個表示式組成

2.表示式有值,語句沒有值, 能作為函式引數即為表示式,否則為語句。

如果Lambda表示式沒有引數,可以只提供一個小括號,如下:

//返回int
() -> 100

如果Lambda表示式的引數型別可以被推導的,那麼可以省略它們的型別

  EmpPredicate predicate = (e) -> "20".equals(e.getDeptno());

如果方法只包含一個引數,並且該引數的型別可以被推匯出來,就可以省略小括號。

 EmpPredicate predicate = e -> "20".equals(e.getDeptno());

3.函式式介面

只包含一個抽象方法的介面就是函式式介面。函式式介面可以包含多個預設方法、類方法,但只能宣告一個抽象方法。例如下面的例子就是定義一個函式式介面

@FunctionalInterface
public interface AddOperation {
    int add(int optA, int optB);
}

Java8專門為函式式介面提供了@FunctionalInterface註解,這個註解對程式功能沒有任何作用,主要用於編譯器執行更嚴格檢查。

如果採用匿名內部類來建立函式式介面的例項,則只需要實現一個抽象方法,在這種情況下即可採用Lambda表示式來建立物件,Lambda表示式創建出來的物件的目標型別就是這個函式式介面。

Lambda表示式的型別是從使用Lambda的上下文推斷出來的。上下文(比如,接受它傳遞的方法的引數,或接受它的值的區域性變數)中
Lambda表示式需要的型別稱為目標型別。目標型別決定了在什麼時候以及在哪裡可以使用lambda表示式。它們可以從賦值的上下文、方法呼叫的上下文(引數和返回值),以及型別轉換的上下文中獲得目標型別。

目標型別->函式式介面->Lambda表示式簽名->在Lambda表示式中省去標註引數型別

Lambda的兩個限制:

  1. Lambda表示式的目標型別必須是明確的函式式介面
  2. Lambda表示式只能為函式式介面建立物件。Lambda表示式只能實現一個方法,因此它只能為只有一個抽象方法的介面(函式式介面)建立物件。

為了保證Lambda表示式的目標型別是一個明確的函式式介面,一般採用如下三種方式

  1. 將Lambda表示式賦值給函式式介面型別的變數。
  2. 將Lambda表示式作為函式式介面型別的引數傳給某個方法。
  3. 使用函式式介面對Lambda表示式進行強制型別轉換。

同一個Lambda表示式可以與不同的函式式介面聯絡起來,只要它們的抽象方法簽名能相容。
也就是說同樣的Lambda表示式的目標型別完全可能是變化的。唯一的要求是Lambda表示式實現的匿名方法與目標型別(函式式介面)中唯一的抽象方法有相同的形參列表。

比如下面的lambda表示式

EmpPredicate predicate = e -> "20".equals(e.getDeptno());
Predicate<Emp> pre = e -> "20".equals(e.getDeptno());

EmpPredicate是自定義的函式式介面,Predicate是Java8預定義的函式式介面。

Java8在java.util.function包下預定義了大量函式式介面,主要有以下4種介面

  • XxxPredicate:這類介面中包含一個test(T t)抽象方法,這個方法通常用來對引數進行某種判斷(test()方法的判斷邏輯由lambda表示式來實現),然後返回一個boolean值。這個函式式介面通常用於判斷引數是否滿足特定條件,經常用於進行篩選過濾資料。
  • XxxConsumer:這類介面中通常包含一個accept(T t)抽象方法,這個方法與XxxFunction介面中的 apply(T)方法基本一樣,都是負責對引數進行處理,只是這個方法不會返回處理結果。
  • XxxFunction:這類介面中包含一個apply(T)抽象方法,該方法對引數進行處理、轉換,然後返回一個新的值。該函式式介面通常用於對指定資料進行轉換處理。
  • XxxSupplier:這類介面通常包含一個get()抽象方法,這個方法不需要輸入引數,該方法會按照某種邏輯演算法(邏輯演算法由Lambda表示式來實現)返回一個數據。

4.使用區域性變數

Java8之前,被區域性內部類、匿名內部類訪問的區域性變數必須使用final修飾,從Java8開始這個限制被取消了,如果區域性變數被匿名內部類訪問,那麼該區域性變數相當於自動使用了final修飾。也就是說對於被匿名內部類訪問的區域性變數,可以用final修飾,也可以不用final修飾,但必須按照有final修飾的方式來用,也就是一次賦值之後,以後不能重複賦值。

同樣的在Lambda表示式中使用區域性變數也有同樣的限制,因為區域性變數儲存在棧中,並且隱式表示它們僅限於其所線上程。如果允許使用可改變的區域性變數,會造成執行緒不安全。而例項變數和靜態變數沒有這個限制,例項變數儲存在堆中,而堆是線上程之間共享的。

5.方法引用與構造器引用

方法引用和構造器引用可以讓Lambda表示式的程式碼塊更加簡潔。方法引用可以被看作僅僅呼叫特定方法的Lambda的一種快捷寫法。
就是讓你根據已有的方法實現來建立Lambda表示式。方法引用和構造器引用都需要使用兩個英文冒號。

Lambda表示式支援的方法引用和構造器引用如下

5.1.指向靜態方法的方法引用(引用類方法)

@FunctionalInterface
public interface TypeConverter {
    Integer convert(String numStr);
}


  //用Lambda表示式建立TypeConverter物件
TypeConverter typeConverter = numStr -> Integer.valueOf(numStr);
 Integer convertResult = typeConverter.convert("101");

上面Lambda表示式的程式碼塊只有一行呼叫類方法的程式碼,因此可以用方法引用進行替換

//方法引用代替Lambda表示式
//函式式介面中被實現方法的全部引數傳給該類方法作為引數
TypeConverter typeConverter = Integer::valueOf;

當呼叫TypeConverter介面中的唯一的抽象方法時,呼叫引數將會傳給Integer類的valueOf類方法。

5.2.指向任意型別例項方法的方法引用

就是引用一個物件的方法,而這個物件本身是Lambda的一個引數。

比如:Lambda表示式(String s) -> s.toUppeCase() 可以寫作String::toUpperCase。

5.3.指向現有物件(特定物件)的例項方法的方法引用

就是在lambda中呼叫一個已存在的外部物件中的方法。如下

 String lang = "Java,C++,C#,Python";
 TypeConverter typeConverter1 = soustr -> lang.indexOf(soustr);

上面lambda表示式的程式碼塊只有一行呼叫"lang".indexOf()例項方法的程式碼,因此可以用方法引用替換

//引用特定物件的例項方法
TypeConverter typeConverter2 = lang::indexOf;

5.4.引用構造器

Supplier<Emp> newEmp = () -> new Emp();
//呼叫Supplier的get方法將產生一個新的Emp物件
 Emp emp = newEmp.get();

上面Lambda表示式的程式碼塊只有一行 new Emp(),因此可以使用構造器引用替換

 Supplier<Emp> supplier = Emp::new;
//呼叫Supplier的get方法將產生一個新的Emp物件
Emp emp1 = supplier.get();

如果建構函式的簽名是Emp(String empName),那麼就適合Function介面的簽名

  Function<String, Emp> empFunction = (empName) -> new Emp(empName);
  Emp emp2 = empFunction.apply("Smith");

等價於

  Function<String,Emp> function2 = Emp::new;
  Emp smith = function2.apply("Smith");

6.Lambda表示式複合

可以將多個簡單的Lambda複合成複雜的表示式,Java8的Comparator、Function、Predict都提供了允許你複合的方法。

例子1:找出部門編號是20並且薪水大於15K的員工

Predicate<Emp> empPredicate = emp -> "20".equals(emp.getDeptno());
Predicate<Emp> empSalPredicate = empPredicate.and(emp -> emp.getSal() > 15000);
List<Emp> list = filterEmp(empList, empSalPredicate);
public List<Emp> filterEmp(List<Emp> empList, Predicate predicate) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (predicate.test(emp)) {
                resultList.add(emp);
            }
        }
        return resultList;
}

例子2:對員工先按照部門編號排序,再按照薪水排序

 Comparator<Emp> empComparator = Comparator.comparing(Emp::getDeptno).thenComparing(Emp::getSal);
 empList.sort(empComparator);

7.Lambda表示式與匿名內部類的聯絡和區別

相同點:

  1. 都可以直接訪問區域性變數,以及外部變數。
  2. Lambda表示式和匿名內部類生成的物件一樣,都可以直接呼叫從介面中繼承的預設方法。

不同點:

  1. 匿名內部類可以為任意介面建立例項,不管介面包含多少個抽象方法,只要匿名內部類實現所有的抽象方法即可。但是Lambda表示式只能為函式式介面建立例項。
  2. 匿名內部類可以為抽象類或者普通類建立例項,但Lambda表示式只能為函式式介面建立例項。
  3. 匿名內部類實現的抽象方法裡面允許呼叫介面中定義的預設方法,但是Lambda表示式的程式碼塊不允許呼叫介面中定義的預設方法。