Java 8th 函數式編程:lambda 表達式

Lambda 表達式是 java 8th 給咱們帶來的幾個重量級新特性之一,借用 lambda 表達式可讓咱們的程序設計更加簡潔。最近新的項目摒棄了 6th 版本,全面基於 8th 進行開發,本文將探討 行爲參數化lambda 表達式 , 以及 方法引用 等知識點。java

一. 行爲參數化

行爲參數化簡單的說就是將方法的邏輯以參數的形式傳遞到方法中,方法主體僅包含模板類通用代碼,而一些會隨着業務場景而變化的邏輯則以參數的形式傳遞到方法之中,採用行爲參數化可讓程序更加的通用,以應對頻繁變動的需求。git

這裏咱們以 java 8 in action 中的例子進行說明。考慮一個業務場景,假設咱們須要經過程序對蘋果按照必定的條件進行篩選,咱們先定義一個蘋果實體:express

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
}

用戶最開始的需求可能只是簡單的但願可以經過程序篩選出綠色的蘋果,因而咱們能夠很快的經過程序實現:編程

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;
}

若是過了一段時間用戶提出了新的需求,但願可以經過程序篩選出紅色的蘋果,因而咱們又須要針對性的添加了篩選紅色蘋果的功能:app

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;
}

更通用的實現是把顏色做爲一個參數傳遞到方法中,這樣就能夠應對之後用戶提出的各類顏色篩選需求:ide

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;
}

這樣的設計不再用擔憂用戶的顏色篩選需求變化了,可是不幸的是某一天用戶提了一個需求但願可以篩選重量達到某一標準的蘋果,有了前面的教訓咱們也把重量的標準做爲參數傳遞給篩選函數:函數式編程

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;
}

這樣經過傳遞參數的方式真的好嗎?若是篩選條件愈來愈多,組合模式愈來愈複雜,咱們是否是須要考慮到全部的狀況,並針對每一種狀況都實現相應的策略呢?而且這些函數僅僅是篩選條件的部分不同,其他部分都是相同的模板代碼(遍歷集合),這個時候咱們就能夠將行爲進行 參數化 處理,讓函數僅保留模板代碼,而把篩選條件抽離出來當作參數傳遞進來,在 java 8th 以前,咱們經過定義一個過濾器接口來實現:函數

// 過濾器
public interface AppleFilter {
    boolean accept(Apple apple);
}

// 應用過濾器的篩選方法
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 等,使用這類接口的時候,咱們均可以在具體調用的地方用匿名類指定函數的具體執行邏輯,不過從上面的代碼塊來看,雖然很極客,可是不夠簡潔,在 java 8th 中咱們能夠經過 lambda 表達式進行簡化:this

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

如上述所示,經過 lambda 表達式極大精簡了代碼,同時行爲參數讓咱們的程序極大的加強了可擴展性。

二. Lambda 表達式

2.1 Lambda 表達式的定義與形式

咱們能夠將 lambda 表達式定義爲一種 __簡潔、可傳遞的匿名函數__,首先咱們須要明確 lambda 表達式本質上是一個函數,雖然它不屬於某個特定的類,但具有參數列表、函數主體、返回類型,甚至可以拋出異常;其次它是匿名的,lambda 表達式沒有具體的函數名稱;lambda 表達式能夠像參數同樣進行傳遞,從而簡化代碼的編寫,其格式定義以下:

  1. 參數列表 -> 表達式
  2. 參數列表 -> {表達式集合}

須要注意 lambda 表達式隱含了 return 關鍵字,因此在單個的表達式中,咱們無需顯式的寫 return 關鍵字,可是當表達式是一個語句集合的時候則須要顯式添加 return 關鍵字,並用花括號 {} 將多個表達式包圍起來,下面看幾個例子:

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

// 2. 始終返回42的無參方法(隱含return語句)
() -> 42

// 3. 包含多行表達式,需用花括號括起來,並顯示添加return
(int x, int y) -> {
    int z = x * y;
    return x + z;
}

2.2 基於函數式接口使用 lambda 表達式

lambda 表達式的使用須要藉助於 __函數式接口__,也就是說只有函數式接口出現地方,咱們才能夠將其用 lambda 表達式進行簡化。那麼什麼是函數接口?函數接口的定義以下:

函數式接口定義爲僅含有一個抽象方法的接口。

按照這個定義,咱們能夠肯定一個接口若是聲明瞭兩個或兩個以上的方法就不叫函數式接口,須要注意一點的是 java 8th 爲接口的定義引入了默認的方法,咱們能夠用 default 關鍵字在接口中定義具有方法體的方法,這個在後面的文章中專門講解,若是一個接口存在多個默認方法,可是仍然僅含有一個抽象方法,那麼這個接口也符合函數式接口的定義。

2.2.1 自定義函數式接口

咱們在前面例子中實現的蘋果篩選接口就是一個函數式接口(定義以下),正由於如此咱們能夠將篩選邏輯參數化,並應用 lambda 表達式:

@FunctionalInterface
public interface AppleFilter {
    boolean accept(Apple apple);
}

AppleFilter 僅包含一個抽象方法 accept(Apple apple),依照定義能夠將其視爲一個函數式接口。在定義時咱們爲該接口添加了 @FunctionalInterface 註解,用於標記該接口是一個函數式接口,不過該註解是可選的,當添加了該註解以後,編譯器會限制了該接口只容許有一個抽象方法,不然報錯,因此推薦爲函數式接口添加該註解。

2.2.2 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>,其他接口幾乎都是對這三個接口的定製化,下面就這三個接口舉例說明其用處,針對接口中提供的邏輯操做默認方法,留到後面介紹接口的 default 方法時再進行說明。

  • 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,利用咱們在外部設定的條件對於傳入的參數進行校驗並返回驗證經過與否,下面利用 Predicate 對 List 集合的元素進行過濾:

private <T> List<T> filter(List<T> numbers, Predicate<T> predicate) {
    Iterator<T> itr = numbers.iterator();
    while (itr.hasNext()) {
        if (!predicate.test(itr.next())) {
            itr.remove();
        }
        itr.next();
    }
    return numbers;
}

上述方法的邏輯是遍歷集合中的元素,經過 Predicate 對集合元素進行驗證,並將驗證不過的元素從集合中移除。咱們能夠利用上面的函數式接口篩選整數集合中的偶數:

PredicateDemo pd = new PredicateDemo();
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
list = pd.filter(list, (value) -> value % 2 == 0);
System.out.println(list);
// 輸出:[2, 4, 6]
  • 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 遍歷字符串集合並轉換成小寫進行打印:

private <T> void forEach(List<T> list, Consumer<T> consumer) {
    for (final T value : list) {
        // 應用行爲
        consumer.accept(value);
    }
}

利用上面的函數式接口,遍歷字符串集合並以小寫形式打印輸出:

ConsumerDemo cd = new ConsumerDemo();
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList("I", " ", "Love", " ", "Java", " ", "8th"));
cd.forEach(list, (value) -> System.out.print(value.toLowerCase()));
// 輸出:i love java 8th
  • 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 對字符串集合轉換成整型集合,並忽略掉不是數值型的字符:

private List<Integer> parse(List<String> list, Function<String, Integer> function) {
    List<Integer> result = new ArrayList<>();
    for (final String value : list) {
        // 應用數據轉換
        if (NumberUtils.isDigits(value)) result.add(function.apply(value));
    }
    return result;
}

下面利用上面的函數式接口,將一個封裝字符串的集合轉換成整型集合,忽略不是數值形式的字符串:

FunctionDemo fd = new FunctionDemo();
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a", "1", "2", "3", "4", "5", "6"));
List<Integer> result = fd.parse(list, (value) -> Integer.valueOf(value));
System.out.println(result);
// 輸出:[1, 2, 3, 4, 5, 6]
2.2.3 一些須要注意的事情
  • 類型推斷

在編碼過程當中,有時候可能會疑惑咱們的調用代碼會具體匹配哪一個函數式接口,實際上編譯器會根據參數、返回類型、異常類型(若是存在)等因素作正確的斷定。在具體調用時,一些時候能夠省略參數的類型以進一步簡化代碼:

// 篩選蘋果
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 中使用實例變量、靜態變量,以及局部變量,以下代碼爲在 lambda 表達式中使用局部變量:

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

上述示例咱們在 lambda 中使用了局部變量 weight,不過在 lambda 中使用局部變量仍是有不少限制,學習初期 IDE 可能常常會提示咱們 Variable used in lambda expression should be final or effectively final 的錯誤,即要求在 lambda 表達式中使用的變量必須 __顯式聲明爲 final 或事實上的 final 類型__。

爲何要限制咱們直接使用外部的局部變量呢?主要緣由在於內存模型,咱們都知道實例變量在堆上分配的,而局部變量在棧上進行分配,lambda 表達式運行在一個獨立的線程中,瞭解 JVM 的同窗應該都知道棧內存是線程私有的,因此局部變量也屬於線程私有,若是肆意的容許 lambda 表達式引用局部變量,可能會存在局部變量以及所屬的線程被回收,而 lambda 表達式所在的線程卻無從知曉,這個時候去訪問就會出現錯誤,之因此容許引用事實上的 final(沒有被聲明爲 final,可是實際中不存在更改變量值的邏輯),是由於對於該變量操做的是變量副本,由於變量值不會被更改,因此這份副本始終有效。這一限制可能會讓剛剛開始接觸函數式編程的同窗不太適應,須要慢慢的轉變思惟方式。

實際上在 java 8th 以前,咱們在方法中使用內部類時就已經遇到了這樣的限制,由於生命週期的限制 JVM 採用複製的策略將局部變量複製一份到內部類中,可是這樣會帶來多個線程中數據不一致的問題,因而衍生了禁止修改內部類引用的外部局部變量這一簡單、粗暴的策略,只不過在 8th 以前必需要求這部分變量採用 final 修飾,可是 8th 開始放寬了這一限制,只要求所引用變量是 「事實上」 的 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)

鑑於做者水平有限,文中難免有錯誤之處,歡迎批評指正
我的博客:www.zhenchao.org

相關文章
相關標籤/搜索