Lambda表達式總結

在理解lambda表達式之前,先來看下行為參數化的概念。

什么是行為參數化

在軟件開發過程中,我們面對的需求總是在不斷變化。所以在開發過程中,需要考慮代碼的通用性和復用性,在實現新功能時讓代碼改動盡量簡單。在Java中,泛型就是一個很好的例子,泛型通過把數據類型參數化,將方法實現和數據類型解耦,使一個方法或者一個類可以適用于多種數據類型,進而實現類和方法的復用。除了像泛這種需要處理不同類型的數據,在某些場景下,我們也需要一個方法或者類可以實現不同的行為。例如,需要一個排序的方法既可以按照高低排序也可以按照重量排序,這時候就需要把行為參數化。泛型是將數據類型作為參數傳入一個方法進而實現代碼復用,那么類比泛型的概念,行為參數化可以理解為把方法A(行為)作為一個參數傳入另外一個方法或者類B,然后在方法或者類B的內部調用傳入的方法A來實現不同的邏輯。這樣方法B的行為就基于方法A被參數化了。

為什么需要行為參數化

行為參數化的作用是讓方法或者類在面對不斷變化的需求時,可以改動的盡量少的代碼。下面來看一個例子。在使用美團的時候,我們都會根據各種條件來篩選符合我們需求的餐廳,假設現在有一個需求是要根據餐廳的類型來篩選餐廳。

需求1:根據餐廳類型來篩選餐廳。

我們先定義一個餐廳的實體類Restaurant

public class Restaurant {

    private String name;
    private String type;
    private int distance;
    private double evaluate;

    public Restaurant(String name, String type, int distance, double evaluate){
        this.name = name;
        this.type = type;
        this.distance = distance;
        this.evaluate = evaluate;
    }

    public String getName() {
        return name;
    }

    public String getType() {
        return type;
    }

    public int getDistance() {
        return distance;
    }

    public double getEvaluate() {
        return evaluate;
    }
}

然后定義一個按照餐廳類型來篩選餐廳的方法。

public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, String type){
    List<Restaurant> result = new ArrayList<>();
    for (Restaurant restaurant:restaurants) {
        if (type.equals(restaurant.getType())) {
            result.add(restaurant);
        }
    }
    return result;
}

這樣就可以滿足需求了。但是,現在又有了新的需求,期望按照餐廳的距離來篩選餐廳。

需求2:根據餐廳距離來篩選餐廳。

例如,我們需要篩選出距離<=目標距離的餐廳,那么我們可以有兩種方案。

方案1 重新定義方法

public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, int distance){
    List<Restaurant> result = new ArrayList<>();
    for (Restaurant restaurant:restaurants) {
        if (restaurant.getDistance() <= distance) {
            result.add(restaurant);
        }
    }
    return result;
}

重新定義一個方法很好的滿足了需求,而且也和原來的篩選類型的方法解耦,但是,經過仔細觀察,可以發現篩選距離的方法和篩選類型的方法很多地方是重復的,僅僅是篩選條件不一樣。這不符合軟件設計的原則。所以,我們考慮通過修改原來的方法以滿足新的需求。

方案2 修改原有方法

public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, String type, int distance, int filterType){
    List<Restaurant> result = new ArrayList<>();
    for (Restaurant restaurant:restaurants) {
        if (filterType == 1) { //按照餐廳類型篩選
            if (type.equals(restaurant.getType())) {
                result.add(restaurant);
            }
        }else if (filterType == 2) { //按照餐廳距離篩選
            if (restaurant.getDistance() <= distance) {
                result.add(restaurant);
            }
        }
    }
    return result;
}

這樣寫雖然滿足了需求,但是代碼的可維護性和擴展性會非常差,如果后續再加一種篩選方式,比如按照評價來篩選,那么我們還要修改這個方法。隨著篩選方式的不斷增加,方法的入參會越來越多,而且方法中會有很多if-else分支,會變得非常復雜。這也違反了面向對象設計的開閉原則。而且如果想將篩選條件進行組合,比如篩選類型是火鍋,同時距離小于5的餐廳,那么還要對上面的方法進行修改。

通過上面可以看到,通過添加參數的方法來滿足不斷變化的需求并不是一個很好的解決方案,其實對于filterRestaurant來說,它最核心的邏輯是判斷一個餐廳是否滿足篩選的條件,所以我們可以把這個判斷做更高一級的抽象,并和filterRestaurant方法解耦,如下所示。

public interface RestaurantFilter {
    boolean filter(Restaurant restaurant);
}

然后我們把filterRestaurant方法做一下改造。

public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, RestaurantFilter restaurantFilter){
    List<Restaurant> result = new ArrayList<>();
    for (Restaurant restaurant:restaurants) {
        if (restaurantFilter.filter(restaurant)) {
            result.add(restaurant);
        }
    }
    return result;
}

此時,如果我們想篩選某種類型的餐廳,我們可以實現RestaurantFilter接口并傳入filterRestaurant方法,例如我們要篩選燒烤類型的餐廳,可以定義燒烤餐廳篩選類并實現RestaurantFilter接口。

public class BarbecueRestaurantFilter implements RestaurantFilter {
    @Override
    public boolean filter(Restaurant restaurant) {
        return "燒烤".equals(restaurant.getType());
    }
}

然后通過調用filterRestaurant方法來篩選燒烤類型的餐廳。

filterRestaurant(restaurants, new BarbecueRestaurantFilter());

同樣,如果需求更復雜一點,想篩選5km以內,而且評價高于4.5分的餐廳,我們可以定義篩選器如下所示。

public class ComplexFilter implements RestaurantFilter {

    @Override
    public boolean filter(Restaurant restaurant) {
        return restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5;
    }
}

對于需求變動,我們只需要定義相應的篩選類就可以,不會對現有的邏輯造成影響。這其實就是行為參數化,在上面的例子中,把篩選蘋果這個行為通過一個對象傳入了filterRestaurant方法,讓filterRestaurant方法用一套代碼實現不同的能力。行為參數化帶來的益處是,行為和使用行為的方法解耦,增加代碼的擴展性和復用性。

但是這種方式有一個問題,雖然我們把篩選蘋果的行為做了抽象,但是每次增加一種新的篩選方式,我們都要增加一個篩選類,而且這個類的對象被創建后只使用了一次,隨著篩選條件的增多,這種類的數量會越來越多。如果不想創建這么多類該怎么辦呢?在Java中,匿名內部類特別適用于這種場景,匿名內部類適用于類的對象只使用一次或者定義回調方法的場景。所以,這里可以使用匿名內部類來代替定義具體的篩選類。如下所示。

List<Restaurant> filterResult = filterRestaurant(restaurants, new RestaurantFilter() {
    @Override
    public boolean filter(Restaurant restaurant) {
        return restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5;
    }
});

這樣,就可以避免定義多個篩選類,代碼會變得簡潔一點。

至此,通過行為參數化,把行為和使用行為的方法解耦,我們可以定義擴展性和復用性良好的filterRestaurant方法。通過使用匿名內部類,代碼的形式得以進一步簡化,但是匿名內部類還是略微顯得有點啰嗦,因為匿名內部類中有很多無用的模板代碼。比如,在上面的例子中,我們想傳給filterRestuarant方法的其實主要是filter方法中的代碼塊,但是我們確要寫很多模板代碼。所以為了解決這個問題,Java8引入了lambda表達式。

Lambda表達式

通過上面的例子可以看到,在Java中,想傳遞一段代碼非常不方便,我們必須首先構建一個對象,然后通過對象調用代碼,因為Java是面向對象的,任何事物的傳遞都需要通過對象來實現。為了解決這個問題,Java8引入了Lambda表達式,我們可以把它看成是一種語法糖,它允許把函數當做參數來使用,這是一種是面向函數式編程的思想,使用lambda表達式來代替匿名內部類可以使代碼更加簡潔。

什么是Lambda表達式

Lambda表達式是傳遞匿名函數的一種方式,它沒有名稱,但是包含參數列表,函數主體,以及返回值。Lambda表達式其實表示的是一個函數,只不過這個函數沒有名字,但是它仍然包含構成函數的主體:參數列表,函數體以及返回值。

Lambda表達式語法

Lambda表達式語法如下所示。

參數列表 -> 函數主體

其中

參數列表:Lambda表達式所表示的匿名函數的參數。

箭頭:把參數列表和函數體分隔開。

函數主體:Lambda表達式表示的匿名函數的函數體。

返回值:無需指定lambda表達式的返回類型,因為返回類型總能根據上下文推導出來。

在使用時,Lambda具體的形式有以下兩種。

1. (parameters) -> expression
// parameters: 參數列表,準確的說是形參列表。
// expression: 表達式,表達式后面沒有分號,表達式的結果作為匿名函數的返回值。例如,1+2,“hello,world” 

2. (parameters) -> {statements; }
// parameters: 參數列表,準確的說是形參列表。
// statements: 語句,語句后面帶分號,并且外面要加大括號,就是Java中的普通的代碼塊,如果想要定義返回值,需要使用return.

// 舉例:
// (1) ()-> {}  沒有入參,函數體為空
// (2) () -> 1  沒有入參,返回值為1 
// (3)  (int a, int b) -> a+b  入參為a,b,返回值為a+b 
// (4) (String a) -> a.toUpperCase() 入參為a,返回值為將a的所有字母轉換成大寫
// (5) (o1,o2) -> o1>o2 如果可以從上下文推斷出參數的類型,在參數列表中可以不寫參數類型
// (6) (int a, int b) -> {return a>b;} 
// (7) a -> {return a*2;} //如果只有一個參數,可以省略小括號。 

了解了Lambda表達式的定義,我們可以把篩選餐廳的方法改寫成Lambda表達式來實現。

filterRestaurant(restaurants, (Restaurant restaurant) -> restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5 ); //形式1

也可以寫成

filterRestaurant(restaurants, (Restaurant restaurant) -> { return restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5; }); //形式2

可以看到,Lambda表達式就是對Restaurant接口中filter方法的一個實現。在底層,編譯器會將Lambda表達式轉換成Restaurant類型的對象。所以從這個層面來說,Lambda表達式其實就是一個語法糖。

另外,在使用Lambda表達式時,編譯器能夠根據上下文推斷出一個lambda表達式的參數類型,所以在參數列表中可以不寫參數類型。上面的方法調用可以進一步簡化為:

filterRestaurant(restaurants, (restaurant) -> restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5 );

如果只有一個參數,那么參數外面的小括號是可以省略的。因此,我們可以進一步簡化。

filterRestaurant(restaurants, restaurant -> restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5);

什么情況下使用Lambda表達式

從上面的例子可以看到,Lambda表達式通常用于代替匿名內部類實現一個接口。那什么情況下才能使用Lambda表達式呢?如果一個接口有兩個方法,能使用Lambda表達式嗎?Java8規定,只能在函數式接口上使用Lambda表達式。下面來看下什么事函數式接口。

函數式接口

在Java中,只能用lambda表達式來表示函數式接口。所謂的函數式接口,是指只定義了一個抽象方法的接口。因為Java8允許在接口中定義默認實現,所以這里的只定義一個抽象方法其實包含兩層含義:

  • 接口只有一個方法

  • 接口中不止定義了一個方法,但是只有一個抽象方法需要被實現,其他的方法在接口中都定義了默認實現。

總的來說,函數式接口,就是只有一個抽象方法需要被實現的接口。Lambda表達式以內聯的形式為函數式接口的抽象方法提供實現,并把整個表達式作為函數式接口的實例。在底層,方法還是會將lambda表達式轉換成一個接口類型的對象,并通過對象調用lambda表達式中的方法。所以,Lambda表達式的簽名和函數式接口中抽象方法的簽名必須是一致的

在Java8中,可以使用@FunctionalInterface注解來定義一個函數式接口,使用@FunctionalInterface用于表示該接口會設計成

一個函數式接口。如果你用@FunctionalInterface定義了一個接口,而它卻不是函數式接口的話,編譯器將返回一個提示原因的錯誤

例如,我們可以將RestaurantFilter接口定義為一個函數式接口。

@FunctionalInterface
public interface RestaurantFilter {
    boolean filter(Restaurant restaurant);
}

加了@FunctionalInterface注解以后,如果我們再在RestaurantFilter中定義抽象方法,編譯器就會報錯。錯誤提示如下所示。

Multiple non-overriding abstract methods found in interface RestaurantFilter

錯誤提示表明這個接口存在多個抽象法。所以,函數式接口有且只能有一個抽象方法。添加@FunctionalInterface注解后,并不會改變函數式接口的本質,只是起到了一個提示作用。

函數式接口中的抽象方法的簽名和Lambda表達式的簽名是一致的。這種抽象方法叫作函數描述符。例如,RestaurantFilter中抽象方法為filter,該方法的簽名可以表示為 Restaurant -> boolean,也就是入參為Restaurant類型,返回值為boolean類型。所以,只要是滿足Restaurant -> boolean這個類型的Lambda表達式,都能用于實現RestaurantFilter接口。

方法引用

方法引用是Lambda表達式的一種快捷寫法,它通過引用已有的方法來代替Lambda表達式,可以把方法引用看作是Lambda表達式的一種語法糖。例如,在上面篩選餐廳的例子中,定義一個判斷餐廳距離是否滿足需求的方法。

public static boolean isRestaurantNotFar(Restaurant restaurant) {
    return restaurant.getDistance() <= 5;
}

這個方法的簽名和RestaurantFilter接口中filter方法的簽名是一致的,通過方法引用,我們可以把isRestaurantNotFar這個方法作為Lambda表達式傳入filterRestaurant方法,如下所示。

filterRestaurant(restaurants, LambdaTest::isRestaurantNotFar);

其中LambdaTestisRestaurantNotFar所在類的類名。下面來具體看下如何創建方法引用。

方法引用分類

我們可以通過三種方式來創建方法引用。如下所示。

  1. 指向類靜態方法的方法引用

    格式:ClassName::staticMethod
    含義:引用類中的靜態方法
    等效的Lambda表達式: (args) -> ClassName.staticMethod(args)  
    
  2. 指向對象實例方法的方法引用

    格式:instance::instanceMethod
    含義:引用對象實例的方法
    等效的Lambda表達式: (args) -> instance.instanceMethod(args)  
    
  3. 指向類實例方法的方法引用

    格式:ClassName::instanceMethod
    含義:引用對象的實例方法,但是這個對象本身是Lambda表達式的一個參數
    等效的Lambda表達式: (arg0, restArgs) -> instance.instanceMethod(arg0,restArgs)  
    

下面看一個例子,我們新建一個類如下所示。

public class CompareMethod {

    public int compareAscend(String s1, String s2) {
        return s1.compareTo(s2);
    }

    public static int compareDescend(String s1, String s2) {
        return s2.compareTo(s1);
    }
}

這里有兩個對字符串排序的方法,一個升序排序,一個降序排序方法。下面通過方法引用來對一個字符串List進行排序。

// 方式1 類::靜態方法
List<String> list = Arrays.asList("a","f","c","b");
list.sort(CompareMethod::compareDescend);  
list.forEach(s-> System.out.println(s));  // [f c b a]   

// 方式2 對象::實例方法
List<String> list = Arrays.asList("a","f","c","b");
list.sort(new CompareMethod()::compareAscend); 
list.forEach(s-> System.out.println(s));  // [a b c f]

// 方式3 類::實例方法
List<String> list = Arrays.asList("a","f","c","b");
list.sort(String::compareTo);
list.forEach(s-> System.out.println(s));  // [a b c d]

下面看下String類的compareTo方法簽名。

public int compareTo(String anotherString);

這里看到compareTo方法的簽名和List.sort方法中Comparator接口抽象方法的簽名不一致,那為什么可以使用呢?這里可以認為編譯器將這個方法引用轉換成了一個等效的Lambda表達式。

String::compareTo  ===== (s1,s2) -> s1.compareTo(s2)

其實下次每次看到形式3的方法引用,我們都可以將其轉換為等效的Lambda表達式來理解。

總結

行為參數化的作用:使代碼可以應對不斷變化的需求,便于擴展和復用。

Lambda表達式:用于傳遞匿名函數,比匿名內部類更便捷。

函數式接口:只有函數式接口才能使用Lambda表達式來實現。

方法引用:Lambda表達式的一種便捷形式。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容