Java8 新特性之 Lambda 表示式

zhenchao發表於2016-10-09

摘要: lambda表示式是Java8給我們帶來的幾個重量級新特性之一,借用lambda表示式,可以讓我們的Java程式設計更加簡潔。本文是Java8新特性的第一篇,將探討行為引數化、lambda表示式,以及方法引用。

Java8新特性系列

lambda表示式是java8給我們帶來的幾個重量級新特性之一,借用lambda表示式,可以讓我們的java程式設計更加簡潔。最近新的專案摒棄了1.6的版本,全面基於java8進行開發,本文是java8新特性的第一篇,將探討行為引數化、lambda表示式,以及方法引用。

一. 行為引數化

行為引數化簡單的說就是函式的主體僅包含模板類通用程式碼,而一些會隨著業務場景而變化的邏輯則以引數的形式傳遞到函式之中,採用行為引數化可以讓程式更加的通用,以應對頻繁變更的需求。

考慮一個業務場景,假設我們需要通過程式對蘋果進行篩選,我們先定義一個蘋果的實體:

/**
 * 蘋果實體
 *
 * @author zhenchao.wang 2016-09-17 12:49
 * @version 1.0.0
 */
public class Apple {

    /** 編號 */
    private long id;

    /** 顏色 */
    private Color color;

    /** 重量 */
    private float weight;

    /** 產地 */
    private String origin;

    public Apple() {
    }

    public Apple(long id, Color color, float weight, String origin) {
        this.id = id;
        this.color = color;
        this.weight = weight;
        this.origin = origin;
    }

    // 省略getter和setter
}

使用者最開始的需求可能只是簡單的希望能夠通過程式篩選出綠色的蘋果,於是我們可以很快的通過程式實現:

/**
 * 篩選綠蘋果
 *
 * @param apples
 * @return
 */
public static List<Apple> filterGreenApples(List<Apple> apples) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (Color.GREEN.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

如果過了一段時間使用者提出了新的需求,希望能夠通過程式篩選出紅色的蘋果,於是我們又針對性的新增了篩選紅色蘋果的功能:

/**
 * 篩選紅蘋果
 *
 * @param apples
 * @return
 */
public static List<Apple> filterRedApples(List<Apple> apples) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (Color.RED.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

更好的實現是把顏色作為一個引數傳遞到函式中,這樣就可以應對以後使用者提出的各種顏色篩選請求了:

/**
 * 自定義篩選顏色
 *
 * @param apples
 * @param color
 * @return
 */
public static List<Apple> filterApplesByColor(List<Apple> apples, Color color) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (color.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

這樣設計了之後,再也不用擔心使用者的顏色篩選需求變化了,但是不幸的是,某一天使用者提了一個需求要求能夠選擇重量達到某一標準的蘋果,有了前面的教訓,我們也把重量的標準作為引數傳遞給篩選函式,於是得到:

/**
 * 篩選指定顏色,且重要符合要求
 *
 * @param apples
 * @param color
 * @param weight
 * @return
 */
public static List<Apple> filterApplesByColorAndWeight(List<Apple> apples, Color color, float weight) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (color.equals(apple.getColor()) && apple.getWeight() >= weight) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

這樣通過傳遞引數的方式真的好嗎?如果篩選條件越來越多,組合模式越來越複雜,我們是不是需要考慮到所有的情況,並針對每一種情況都有相應的應對策略呢,並且這些函式僅僅是篩選條件的部分不一樣,其餘部分都是相同的模板程式碼(遍歷集合),這個時候我們就可以將行為 引數化 ,讓函式僅保留模板程式碼,而把篩選條件抽離出來當做引數傳遞進來,在java8之前,我們通過定義一個過濾器介面來實現:

/**
 * 蘋果過濾介面
 *
 * @author zhenchao.wang 2016-09-17 14:21
 * @version 1.0.0
 */
@FunctionalInterface
public interface AppleFilter {

    /**
     * 篩選條件抽象
     *
     * @param apple
     * @return
     */
    boolean accept(Apple apple);

}

/**
 * 將篩選條件封裝成介面
 *
 * @param apples
 * @param filter
 * @return
 */
public static List<Apple> filterApplesByAppleFilter(List<Apple> apples, AppleFilter filter) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (filter.accept(apple)) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

通過上面行為抽象化之後,我們可以在具體呼叫的地方設定篩選條件,並將條件作為引數傳遞到方法中:

public static void main(String[] args) {
    List<Apple> apples = new ArrayList<>();

    // 篩選蘋果
    List<Apple> filterApples = filterApplesByAppleFilter(apples, new AppleFilter() {
        @Override
        public boolean accept(Apple apple) {
            // 篩選重量大於100g的紅蘋果
            return Color.RED.equals(apple.getColor()) && apple.getWeight() > 100;
        }
    });
}

上面的行為引數化方式採用匿名類來實現,這樣的設計在jdk內部也經常採用,比如java.util.Comparatorjava.util.concurrent.Callable等,使用這一類介面的時候,我們都可以在具體呼叫的地方用過匿名類來指定函式的具體執行邏輯,不過從上面的程式碼塊來看,雖然很極客,但是不夠簡潔,在java8中我們可以通過lambda來簡化:

// 篩選蘋果
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

通過lambda表示式極大的精簡了程式碼,下面來學習java的lambda表示式吧~

二. lambda表示式定義

我們可以將lambda表示式定義為一種 簡潔、可傳遞的匿名函式,首先我們需要明確lambda表示式本質上是一個函式,雖然它不屬於某個特定的類,但具備引數列表、函式主體、返回型別,以及能夠丟擲異常;其次它是匿名的,lambda表示式沒有具體的函式名稱;lambda表示式可以像引數一樣進行傳遞,從而極大的簡化程式碼的編寫。格式定義如下:

格式一: 引數列表 -> 表示式
格式二: 引數列表 -> {表示式集合}

需要注意的是,lambda表示式隱含了return關鍵字,所以在單個的表示式中,我們無需顯式的寫return關鍵字,但是當表示式是一個語句集合的時候,則需要顯式新增return,並用花括號{ }將多個表示式包圍起來,下面看幾個例子:

//返回給定字串的長度,隱含return語句
(String s) -> s.length() 

// 始終返回42的無參方法
() -> 42 

// 包含多行表示式,則用花括號括起來
(int x, int y) -> {
    int z = x * y;
    return x + z;
}

三. 依託於函式式介面使用lambda表示式

lambda表示式的使用需要藉助於函式式介面,也就是說只有函式式介面出現地方,我們才可以將其用lambda表示式進行簡化。

自定義函式式介面

函式式介面定義為只具備 一個抽象方法 的介面。java8在介面定義上的改進就是引入了預設方法,使得我們可以在介面中對方法提供預設的實現,但是不管存在多少個預設方法,只要具備一個且只有一個抽象方法,那麼它就是函式式介面,如下(引用上面的AppleFilter):

/**
 * 蘋果過濾介面
 *
 * @author zhenchao.wang 2016-09-17 14:21
 * @version 1.0.0
 */
@FunctionalInterface
public interface AppleFilter {

    /**
     * 篩選條件抽象
     *
     * @param apple
     * @return
     */
    boolean accept(Apple apple);

}

AppleFilter僅包含一個抽象方法accept(Apple apple),依照定義可以將其視為一個函式式介面,在定義時我們為該介面新增了@FunctionalInterface註解,用於標記該介面是函式式介面,不過這個介面是可選的,當新增了該介面之後,編譯器就限制了該介面只允許有一個抽象方法,否則報錯,所以推薦為函式式介面新增該註解。

jdk自帶的函式式介面

jdk為lambda表示式已經內建了豐富的函式式介面,如下表所示(僅列出部分):

函式式介面 函式描述符 原始型別特化
Predicate<T> T -> boolean IntPredicate, LongPredicate, DoublePredicate
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Funcation<T, R> T -> R IntFuncation<R>, IntToDoubleFunction, IntToLongFunction<R>, LongFuncation…
Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator<T> T -> T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator<T> (T, T) -> T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L, R> (L, R) -> boolean
BiConsumer<T, U> (T, U) -> void
BiFunction<T, U, R> (T, U) -> R

下面分別就Predicate<T>Consumer<T>Function<T, R>的使用示例說明。

Predicate<T>

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

Predicate的功能類似於上面的AppleFilter,利用我們在外部設定的條件對於傳入的引數進行校驗,並返回驗證結果boolean,下面利用Predicate對List集合的元素進行過濾:

/**
 * 按照指定的條件對集合元素進行過濾
 *
 * @param list
 * @param predicate
 * @param <T>
 * @return
 */
public <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> newList = new ArrayList<T>();
    for (final T t : list) {
        if (predicate.test(t)) {
            newList.add(t);
        }
    }
    return newList;
}

利用上面的函式式介面過濾字串集合中的空字串:

demo.filter(list, (String str) -> null != str && !str.isEmpty());

Consumer<T>

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}

Consumer提供了一個accept抽象函式,該函式接收引數,但不返回值,下面利用Consumer遍歷集合:

/**
 * 遍歷集合,執行自定義行為
 *
 * @param list
 * @param consumer
 * @param <T>
 */
public <T> void filter(List<T> list, Consumer<T> consumer) {
    for (final T t : list) {
        consumer.accept(t);
    }
}

利用上面的函式式介面,遍歷字串集合,並列印非空字串:

demo.filter(list, (String str) -> {
        if (StringUtils.isNotBlank(str)) {
            System.out.println(str);
        }
    });

Function<T, R>

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

Funcation執行轉換操作,輸入是型別T的資料,返回R型別的資料,下面利用Function對集合進行轉換:

/**
 * 遍歷集合,執行自定義轉換操作
 *
 * @param list
 * @param function
 * @param <T>
 * @param <R>
 * @return
 */
public <T, R> List<R> filter(List<T> list, Function<T, R> function) {
    List<R> newList = new ArrayList<R>();
    for (final T t : list) {
        newList.add(function.apply(t));
    }
    return newList;
}

下面利用上面的函式式介面,將一個封裝字串(整型數字的字串表示)的介面,轉換成整型集合:

demo.filter(list, (String str) -> Integer.parseInt(str));

上面這些函式式介面還提供了一些邏輯操作的預設實現,留到後面介紹java8介面的預設方法時再講吧~

使用過程中需要注意的一些事情

型別推斷

在編碼過程中,有時候可能會疑惑我們的呼叫程式碼會去具體匹配哪個函式式介面,實際上編譯器會根據引數、返回型別、異常型別(如果存在)等做正確的判定。
在具體呼叫時,在一些時候可以省略引數的型別,從而進一步簡化程式碼:

// 篩選蘋果
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

// 某些情況下我們甚至可以省略引數型別,編譯器會根據上下文正確判斷
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

區域性變數

上面所有例子我們的lambda表示式都是使用其主體引數,我們也可以在lambda中使用區域性變數,如下:

int weight = 100;
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= weight);

該例子中我們在lambda中使用了區域性變數weight,不過在lambda中使用區域性變數必須要求該變數 顯式宣告為final或事實上的final ,這主要是因為區域性變數儲存在棧上,lambda表示式則在另一個執行緒中執行,當該執行緒檢視訪問該區域性變數的時候,該變數存在被更改或回收的可能性,所以用final修飾之後就不會存線上程安全的問題。

四. 方法引用

採用方法引用可以更近一步的簡化程式碼,有時候這種簡化讓程式碼看上去更加的直觀,先看一個例子:

/* ... 省略apples的初始化操作 */

// 採用lambda表示式
apples.sort((Apple a, Apple b) -> Float.compare(a.getWeight(), b.getWeight()));

// 採用方法引用
apples.sort(Comparator.comparing(Apple::getWeight));

方法引用通過::將方法隸屬和方法自身連線起來,主要分為三類:

靜態方法

(args) -> ClassName.staticMethod(args)
轉換成
ClassName::staticMethod

引數的例項方法

(args) -> args.instanceMethod()
轉換成
ClassName::instanceMethod  // ClassName是args的型別

外部的例項方法

(args) -> ext.instanceMethod(args)
轉換成
ext::instanceMethod(args)

參考:

相關文章