Java 8th 函数式编程:lambda 表达式
Lambda 表達(dá)式是 java 8th 給我們帶來的幾個重量級新特性之一,借用 lambda 表達(dá)式可以讓我們的程序設(shè)計更加簡潔。最近新的項目摒棄了 6th 版本,全面基于 8th 進(jìn)行開發(fā),本文將探討 行為參數(shù)化 、 lambda 表達(dá)式 , 以及 方法引用 等知識點。
一. 行為參數(shù)化
行為參數(shù)化簡單的說就是將方法的邏輯以參數(shù)的形式傳遞到方法中,方法主體僅包含模板類通用代碼,而一些會隨著業(yè)務(wù)場景而變化的邏輯則以參數(shù)的形式傳遞到方法之中,采用行為參數(shù)化可以讓程序更加的通用,以應(yīng)對頻繁變更的需求。
這里我們以 java 8 in action 中的例子進(jìn)行說明。考慮一個業(yè)務(wù)場景,假設(shè)我們需要通過程序?qū)μO果按照一定的條件進(jìn)行篩選,我們先定義一個蘋果實體:
public class Apple {/** 編號 */private Long id;/** 顏色 */private Color color;/** 重量 */private Float weight;/** 產(chǎn)地 */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 }用戶最開始的需求可能只是簡單的希望能夠通過程序篩選出綠色的蘋果,于是我們可以很快的通過程序?qū)崿F(xiàn):
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; }如果過了一段時間用戶提出了新的需求,希望能夠通過程序篩選出紅色的蘋果,于是我們又需要針對性的添加了篩選紅色蘋果的功能:
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; }更通用的實現(xiàn)是把顏色作為一個參數(shù)傳遞到方法中,這樣就可以應(yīng)對以后用戶提出的各種顏色篩選需求:
public static List<Apple> filterApplesByColor(List<Apple> apples, Color color) {List<Apple> filterApples = new ArrayList<>();for (final Apple apple : apples) {// 依據(jù)傳入的顏色參數(shù)進(jìn)行篩選if (color.equals(apple.getColor())) {filterApples.add(apple);}}return filterApples; }這樣的設(shè)計再也不用擔(dān)心用戶的顏色篩選需求變化了,但是不幸的是某一天用戶提了一個需求希望能夠篩選重量達(dá)到某一標(biāo)準(zhǔn)的蘋果,有了前面的教訓(xùn)我們也把重量的標(biāo)準(zhǔn)作為參數(shù)傳遞給篩選函數(shù):
public static List<Apple> filterApplesByColorAndWeight(List<Apple> apples, Color color, float weight) {List<Apple> filterApples = new ArrayList<>();for (final Apple apple : apples) {// 依據(jù)顏色和重量進(jìn)行篩選if (color.equals(apple.getColor()) && apple.getWeight() >= weight) {filterApples.add(apple);}}return filterApples; }這樣通過傳遞參數(shù)的方式真的好嗎?如果篩選條件越來越多,組合模式越來越復(fù)雜,我們是不是需要考慮到所有的情況,并針對每一種情況都實現(xiàn)相應(yīng)的策略呢?并且這些函數(shù)僅僅是篩選條件的部分不一樣,其余部分都是相同的模板代碼(遍歷集合),這個時候我們就可以將行為進(jìn)行 參數(shù)化 處理,讓函數(shù)僅保留模板代碼,而把篩選條件抽離出來當(dāng)做參數(shù)傳遞進(jìn)來,在 java 8th 之前,我們通過定義一個過濾器接口來實現(xiàn):
// 過濾器 public interface AppleFilter {boolean accept(Apple apple); }// 應(yīng)用過濾器的篩選方法 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; }通過上面行為抽象化之后,我們可以在具體調(diào)用的地方設(shè)置篩選條件,并將條件作為參數(shù)傳遞到方法中:
public static void main(String[] args) {List<Apple> apples = new ArrayList<>();// 篩選蘋果List<Apple> filterApples = filterApplesByAppleFilter(apples, new AppleFilter() {@Overridepublic boolean accept(Apple apple) {// 篩選重量大于100g的紅蘋果return Color.RED.equals(apple.getColor()) && apple.getWeight() > 100;}}); }上面的行為參數(shù)化方式采用匿名類實現(xiàn),這樣的設(shè)計在 jdk 內(nèi)部也經(jīng)常采用,比如 java.util.Comparator,java.util.concurrent.Callable 等,使用這類接口的時候,我們都可以在具體調(diào)用的地方用匿名類指定函數(shù)的具體執(zhí)行邏輯,不過從上面的代碼塊來看,雖然很極客,但是不夠簡潔,在 java 8th 中我們可以通過 lambda 表達(dá)式進(jìn)行簡化:
// 篩選蘋果 List<Apple> filterApples = filterApplesByAppleFilter(apples,(Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);如上述所示,通過 lambda 表達(dá)式極大精簡了代碼,同時行為參數(shù)讓我們的程序極大的增強(qiáng)了可擴(kuò)展性。
二. Lambda 表達(dá)式
2.1 Lambda 表達(dá)式的定義與形式
我們可以將 lambda 表達(dá)式定義為一種 __簡潔、可傳遞的匿名函數(shù)__,首先我們需要明確 lambda 表達(dá)式本質(zhì)上是一個函數(shù),雖然它不屬于某個特定的類,但具備參數(shù)列表、函數(shù)主體、返回類型,甚至能夠拋出異常;其次它是匿名的,lambda 表達(dá)式?jīng)]有具體的函數(shù)名稱;lambda 表達(dá)式可以像參數(shù)一樣進(jìn)行傳遞,從而簡化代碼的編寫,其格式定義如下:
需要注意 lambda 表達(dá)式隱含了 return 關(guān)鍵字,所以在單個的表達(dá)式中,我們無需顯式的寫 return 關(guān)鍵字,但是當(dāng)表達(dá)式是一個語句集合的時候則需要顯式添加 return 關(guān)鍵字,并用花括號 {} 將多個表達(dá)式包圍起來,下面看幾個例子:
// 1. 返回給定字符串的長度(隱含return語句) (String s) -> s.length()// 2. 始終返回42的無參方法(隱含return語句) () -> 42// 3. 包含多行表達(dá)式,需用花括號括起來,并顯示添加return (int x, int y) -> {int z = x * y;return x + z; }2.2 基于函數(shù)式接口使用 lambda 表達(dá)式
lambda 表達(dá)式的使用需要借助于 __函數(shù)式接口__,也就是說只有函數(shù)式接口出現(xiàn)地方,我們才可以將其用 lambda 表達(dá)式進(jìn)行簡化。那么什么是函數(shù)接口?函數(shù)接口的定義如下:
函數(shù)式接口定義為僅含有一個抽象方法的接口。
按照這個定義,我們可以確定一個接口如果聲明了兩個或兩個以上的方法就不叫函數(shù)式接口,需要注意一點的是 java 8th 為接口的定義引入了默認(rèn)的方法,我們可以用 default 關(guān)鍵字在接口中定義具備方法體的方法,這個在后面的文章中專門講解,如果一個接口存在多個默認(rèn)方法,但是仍然僅含有一個抽象方法,那么這個接口也符合函數(shù)式接口的定義。
2.2.1 自定義函數(shù)式接口
我們在前面例子中實現(xiàn)的蘋果篩選接口就是一個函數(shù)式接口(定義如下),正因為如此我們可以將篩選邏輯參數(shù)化,并應(yīng)用 lambda 表達(dá)式:
@FunctionalInterface public interface AppleFilter {boolean accept(Apple apple); }AppleFilter 僅包含一個抽象方法 accept(Apple apple),依照定義可以將其視為一個函數(shù)式接口。在定義時我們?yōu)樵摻涌谔砑恿?@FunctionalInterface 注解,用于標(biāo)記該接口是一個函數(shù)式接口,不過該注解是可選的,當(dāng)添加了該注解之后,編譯器會限制了該接口只允許有一個抽象方法,否則報錯,所以推薦為函數(shù)式接口添加該注解。
2.2.2 jdk 自帶的函數(shù)式接口
jdk 為 lambda 表達(dá)式已經(jīng)內(nèi)置了豐富的函數(shù)式接口,如下表所示(僅列出部分):
| 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>,其余接口幾乎都是對這三個接口的定制化,下面就這三個接口舉例說明其用處,針對接口中提供的邏輯操作默認(rèn)方法,留到后面介紹接口的 default 方法時再進(jìn)行說明。
- Predicate<T>
Predicate 的功能類似于上面的 AppleFilter,利用我們在外部設(shè)定的條件對于傳入的參數(shù)進(jìn)行校驗并返回驗證通過與否,下面利用 Predicate 對 List 集合的元素進(jìn)行過濾:
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 對集合元素進(jìn)行驗證,并將驗證不過的元素從集合中移除。我們可以利用上面的函數(shù)式接口篩選整數(shù)集合中的偶數(shù):
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>
Consumer 提供了一個 accept 抽象函數(shù),該函數(shù)接收參數(shù)并依據(jù)傳遞的行為應(yīng)用傳遞的參數(shù)值,下面利用 Consumer 遍歷字符串集合并轉(zhuǎn)換成小寫進(jìn)行打印:
private <T> void forEach(List<T> list, Consumer<T> consumer) {for (final T value : list) {// 應(yīng)用行為consumer.accept(value);} }利用上面的函數(shù)式接口,遍歷字符串集合并以小寫形式打印輸出:
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>
Funcation 執(zhí)行轉(zhuǎn)換操作,輸入類型 T 的數(shù)據(jù),返回 R 類型的結(jié)果,下面利用 Function 對字符串集合轉(zhuǎn)換成整型集合,并忽略掉不是數(shù)值型的字符:
private List<Integer> parse(List<String> list, Function<String, Integer> function) {List<Integer> result = new ArrayList<>();for (final String value : list) {// 應(yīng)用數(shù)據(jù)轉(zhuǎn)換if (NumberUtils.isDigits(value)) result.add(function.apply(value));}return result; }下面利用上面的函數(shù)式接口,將一個封裝字符串的集合轉(zhuǎn)換成整型集合,忽略不是數(shù)值形式的字符串:
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 一些需要注意的事情
- 類型推斷
在編碼過程中,有時候可能會疑惑我們的調(diào)用代碼會具體匹配哪個函數(shù)式接口,實際上編譯器會根據(jù)參數(shù)、返回類型、異常類型(如果存在)等因素做正確的判定。在具體調(diào)用時,一些時候可以省略參數(shù)的類型以進(jìn)一步簡化代碼:
// 篩選蘋果 List<Apple> filterApples = filterApplesByAppleFilter(apples,(Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);// 某些情況下我們甚至可以省略參數(shù)類型,編譯器會根據(jù)上下文正確判斷 List<Apple> filterApples = filterApplesByAppleFilter(apples,apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);- 局部變量
上面所有例子中使用的變量都是 lambda 表達(dá)式的主體參數(shù),我們也可以在 lambda 中使用實例變量、靜態(tài)變量,以及局部變量,如下代碼為在 lambda 表達(dá)式中使用局部變量:
int weight = 100; List<Apple> filterApples = filterApplesByAppleFilter(apples,apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= weight);上述示例我們在 lambda 中使用了局部變量 weight,不過在 lambda 中使用局部變量還是有很多限制,學(xué)習(xí)初期 IDE 可能經(jīng)常會提示我們 Variable used in lambda expression should be final or effectively final 的錯誤,即要求在 lambda 表達(dá)式中使用的變量必須 __顯式聲明為 final 或事實上的 final 類型__。
為什么要限制我們直接使用外部的局部變量呢?主要原因在于內(nèi)存模型,我們都知道實例變量在堆上分配的,而局部變量在棧上進(jìn)行分配,lambda 表達(dá)式運(yùn)行在一個獨(dú)立的線程中,了解 JVM 的同學(xué)應(yīng)該都知道棧內(nèi)存是線程私有的,所以局部變量也屬于線程私有,如果肆意的允許 lambda 表達(dá)式引用局部變量,可能會存在局部變量以及所屬的線程被回收,而 lambda 表達(dá)式所在的線程卻無從知曉,這個時候去訪問就會出現(xiàn)錯誤,之所以允許引用事實上的 final(沒有被聲明為 final,但是實際中不存在更改變量值的邏輯),是因為對于該變量操作的是變量副本,因為變量值不會被更改,所以這份副本始終有效。這一限制可能會讓剛剛開始接觸函數(shù)式編程的同學(xué)不太適應(yīng),需要慢慢的轉(zhuǎn)變思維方式。
實際上在 java 8th 之前,我們在方法中使用內(nèi)部類時就已經(jīng)遇到了這樣的限制,因為生命周期的限制 JVM 采用復(fù)制的策略將局部變量復(fù)制一份到內(nèi)部類中,但是這樣會帶來多個線程中數(shù)據(jù)不一致的問題,于是衍生了禁止修改內(nèi)部類引用的外部局部變量這一簡單、粗暴的策略,只不過在 8th 之前必須要求這部分變量采用 final 修飾,但是 8th 開始放寬了這一限制,只要求所引用變量是 “事實上” 的 final 類型即可。
三. 方法引用
方法引用可以更近一步的簡化代碼,有時候這種簡化讓代碼看上去更加直觀,先看一個例子:
/* ... 省略apples的初始化操作 */// 采用lambda表達(dá)式 apples.sort((Apple a, Apple b) -> Float.compare(a.getWeight(), b.getWeight()));// 采用方法引用 apples.sort(Comparator.comparing(Apple::getWeight));方法引用通過 :: 將方法隸屬和方法自身連接起來,主要分為三類:
- 靜態(tài)方法
- 參數(shù)的實例方法
- 外部的實例方法
鑒于作者水平有限,文中不免有錯誤之處,歡迎批評指正
個人博客:www.zhenchao.org
總結(jié)
以上是生活随笔為你收集整理的Java 8th 函数式编程:lambda 表达式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java函数式编程之接口Default方
- 下一篇: JavaWordCountCore