摘要:自定義函數式接口我們在前面例子中實現的蘋果篩選接口就是一個函數式接口定義如下,正因為如此我們可以將篩選邏輯參數化,并應用表達式僅包含一個抽象方法,依照定義可以將其視為一個函數式接口。
Lambda 表達式是 java 8th 給我們帶來的幾個重量級新特性之一,借用 lambda 表達式可以讓我們的程序設計更加簡潔。最近新的項目摒棄了 6th 版本,全面基于 8th 進行開發,本文將探討 行為參數化 、 lambda 表達式 , 以及 方法引用 等知識點。
一. 行為參數化行為參數化簡單的說就是將方法的邏輯以參數的形式傳遞到方法中,方法主體僅包含模板類通用代碼,而一些會隨著業務場景而變化的邏輯則以參數的形式傳遞到方法之中,采用行為參數化可以讓程序更加的通用,以應對頻繁變更的需求。
這里我們以 java 8 in action 中的例子進行說明。考慮一個業務場景,假設我們需要通過程序對蘋果按照一定的條件進行篩選,我們先定義一個蘋果實體:
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 ListfilterGreenApples(List apples) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { // 篩選出綠色的蘋果 if (Color.GREEN.equals(apple.getColor())) { filterApples.add(apple); } } return filterApples; }
如果過了一段時間用戶提出了新的需求,希望能夠通過程序篩選出紅色的蘋果,于是我們又需要針對性的添加了篩選紅色蘋果的功能:
public static ListfilterRedApples(List apples) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { // 篩選出紅色的蘋果 if (Color.RED.equals(apple.getColor())) { filterApples.add(apple); } } return filterApples; }
更通用的實現是把顏色作為一個參數傳遞到方法中,這樣就可以應對以后用戶提出的各種顏色篩選需求:
public static ListfilterApplesByColor(List apples, Color color) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { // 依據傳入的顏色參數進行篩選 if (color.equals(apple.getColor())) { filterApples.add(apple); } } return filterApples; }
這樣的設計再也不用擔心用戶的顏色篩選需求變化了,但是不幸的是某一天用戶提了一個需求希望能夠篩選重量達到某一標準的蘋果,有了前面的教訓我們也把重量的標準作為參數傳遞給篩選函數:
public static ListfilterApplesByColorAndWeight(List apples, Color color, float weight) { List 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 ListfilterApplesByAppleFilter(List apples, AppleFilter filter) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { if (filter.accept(apple)) { filterApples.add(apple); } } return filterApples; }
通過上面行為抽象化之后,我們可以在具體調用的地方設置篩選條件,并將條件作為參數傳遞到方法中:
public static void main(String[] args) { Listapples = new ArrayList<>(); // 篩選蘋果 List filterApples = filterApplesByAppleFilter(apples, new AppleFilter() { @Override public boolean accept(Apple apple) { // 篩選重量大于100g的紅蘋果 return Color.RED.equals(apple.getColor()) && apple.getWeight() > 100; } }); }
上面的行為參數化方式采用匿名類實現,這樣的設計在 jdk 內部也經常采用,比如 java.util.Comparator,java.util.concurrent.Callable 等,使用這類接口的時候,我們都可以在具體調用的地方用匿名類指定函數的具體執行邏輯,不過從上面的代碼塊來看,雖然很極客,但是不夠簡潔,在 java 8th 中我們可以通過 lambda 表達式進行簡化:
// 篩選蘋果 ListfilterApples = filterApplesByAppleFilter(apples, (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
如上述所示,通過 lambda 表達式極大精簡了代碼,同時行為參數讓我們的程序極大的增強了可擴展性。
二. Lambda 表達式 2.1 Lambda 表達式的定義與形式我們可以將 lambda 表達式定義為一種 __簡潔、可傳遞的匿名函數__,首先我們需要明確 lambda 表達式本質上是一個函數,雖然它不屬于某個特定的類,但具備參數列表、函數主體、返回類型,甚至能夠拋出異常;其次它是匿名的,lambda 表達式沒有具體的函數名稱;lambda 表達式可以像參數一樣進行傳遞,從而簡化代碼的編寫,其格式定義如下:
參數列表 -> 表達式
參數列表 -> {表達式集合}
需要注意 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 關鍵字在接口中定義具備方法體的方法,這個在后面的文章中專門講解,如果一個接口存在多個默認方法,但是仍然僅含有一個抽象方法,那么這個接口也符合函數式接口的定義。
我們在前面例子中實現的蘋果篩選接口就是一個函數式接口(定義如下),正因為如此我們可以將篩選邏輯參數化,并應用 lambda 表達式:
@FunctionalInterface public interface AppleFilter { boolean accept(Apple apple); }
AppleFilter 僅包含一個抽象方法 accept(Apple apple),依照定義可以將其視為一個函數式接口。在定義時我們為該接口添加了 @FunctionalInterface 注解,用于標記該接口是一個函數式接口,不過該注解是可選的,當添加了該注解之后,編譯器會限制了該接口只允許有一個抽象方法,否則報錯,所以推薦為函數式接口添加該注解。
jdk 為 lambda 表達式已經內置了豐富的函數式接口,如下表所示(僅列出部分):
函數式接口 | 函數描述符 | 原始類型特化 |
---|---|---|
Predicate |
T -> boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer |
T -> void | IntConsumer, LongConsumer, DoubleConsumer |
Funcation |
T -> R | IntFuncation |
Supplier |
() -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator |
T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator |
(T, T) -> T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate |
(L, R) -> boolean | |
BiConsumer |
(T, U) -> void | |
BiFunction |
(T, U) -> R |
其中最典型的三個接口是 Predicate
Predicate
@FunctionalInterface public interface Predicate{ /** * 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 集合的元素進行過濾:
privateList filter(List numbers, Predicate predicate) { Iterator itr = numbers.iterator(); while (itr.hasNext()) { if (!predicate.test(itr.next())) { itr.remove(); } itr.next(); } return numbers; }
上述方法的邏輯是遍歷集合中的元素,通過 Predicate 對集合元素進行驗證,并將驗證不過的元素從集合中移除。我們可以利用上面的函數式接口篩選整數集合中的偶數:
PredicateDemo pd = new PredicateDemo(); Listlist = 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
@FunctionalInterface public interface Consumer{ /** * Performs this operation on the given argument. * * @param t the input argument */ void accept(T t); }
Consumer 提供了一個 accept 抽象函數,該函數接收參數并依據傳遞的行為應用傳遞的參數值,下面利用 Consumer 遍歷字符串集合并轉換成小寫進行打印:
privatevoid forEach(List list, Consumer consumer) { for (final T value : list) { // 應用行為 consumer.accept(value); } }
利用上面的函數式接口,遍歷字符串集合并以小寫形式打印輸出:
ConsumerDemo cd = new ConsumerDemo(); Listlist = new ArrayList<>(); list.addAll(Arrays.asList("I", " ", "Love", " ", "Java", " ", "8th")); cd.forEach(list, (value) -> System.out.print(value.toLowerCase())); // 輸出:i love java 8th
Function
@FunctionalInterface public interface Function{ /** * 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 Listparse(List list, Function function) { List result = new ArrayList<>(); for (final String value : list) { // 應用數據轉換 if (NumberUtils.isDigits(value)) result.add(function.apply(value)); } return result; }
下面利用上面的函數式接口,將一個封裝字符串的集合轉換成整型集合,忽略不是數值形式的字符串:
FunctionDemo fd = new FunctionDemo(); Listlist = new ArrayList<>(); list.addAll(Arrays.asList("a", "1", "2", "3", "4", "5", "6")); List result = fd.parse(list, (value) -> Integer.valueOf(value)); System.out.println(result); // 輸出:[1, 2, 3, 4, 5, 6]
類型推斷
在編碼過程中,有時候可能會疑惑我們的調用代碼會具體匹配哪個函數式接口,實際上編譯器會根據參數、返回類型、異常類型(如果存在)等因素做正確的判定。在具體調用時,一些時候可以省略參數的類型以進一步簡化代碼:
// 篩選蘋果 ListfilterApples = filterApplesByAppleFilter(apples, (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100); // 某些情況下我們甚至可以省略參數類型,編譯器會根據上下文正確判斷 List filterApples = filterApplesByAppleFilter(apples, apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
局部變量
上面所有例子中使用的變量都是 lambda 表達式的主體參數,我們也可以在 lambda 中使用實例變量、靜態變量,以及局部變量,如下代碼為在 lambda 表達式中使用局部變量:
int weight = 100; ListfilterApples = 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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70650.html
摘要:當滿足條件時執行傳入的參數化操作。最后提醒一點,好用但不能濫用,在設計一個接口方法時是否采取類型返回需要斟酌,一味的使用會讓代碼變得比較啰嗦,反而破壞了代碼的簡潔性。鑒于作者水平有限,文中不免有錯誤之處,歡迎批評指正個人博客 NullPointException 可以說是所有 java 程序員都遇到過的一個異常,雖然 java 從設計之初就力圖讓程序員脫離指針的苦海,但是指針確實是實際...
摘要:在支持一類函數的語言中,表達式的類型將是函數。匿名函數的返回類型與該主體表達式一致如果表達式的主體包含一條以上語句,則表達式必須包含在花括號中形成代碼塊。注意,使用表達式的方法不止一種。 摘要:此篇文章主要介紹 Java8 Lambda 表達式產生的背景和用法,以及 Lambda 表達式與匿名類的不同等。本文系 OneAPM 工程師編譯整理。 Java 是一流的面向對象語言,除了部分簡...
摘要:而在中,表達式是對象,它們必須依附于一類特別的對象類型函數式接口。即表達式返回的是函數式接口類型。 Java8被稱作Java史上變化最大的一個版本。其中包含很多重要的新特性,最核心的就是增加了Lambda表達式和Stream API。這兩者也可以結合在一起使用。首先來看下什么是Lambda表達式。Lambda表達式,維基百科上的解釋是一種用于表示匿名函數和閉包的運算符,感覺看到這個解釋...
摘要:但,如果加入了函數式編程,也就是將方法作為形參傳遞,這必然讓開發者為難。但是,其他語言早就使用了函數式編程,比如最常見腳本語言。這就是函數式編程,傳遞的是一個函數直到,才引用了函數式編程,也就是我們所說的表達式。 導讀 Java從jdk1發展到今天,方法的形參類型可以是基本變量,可以是jdk自帶的類型,也可以是用戶自定義的類型,但是,方法能不能作為形參來傳遞?我們希望java能夠像其他...
摘要:但,如果加入了函數式編程,也就是將方法作為形參傳遞,這必然讓開發者為難。但是,其他語言早就使用了函數式編程,比如最常見腳本語言。這就是函數式編程,傳遞的是一個函數直到,才引用了函數式編程,也就是我們所說的表達式。 導讀 Java從jdk1發展到今天,方法的形參類型可以是基本變量,可以是jdk自帶的類型,也可以是用戶自定義的類型,但是,方法能不能作為形參來傳遞?我們希望java能夠像其他...
閱讀 3185·2021-11-24 09:39
閱讀 2923·2021-11-23 09:51
閱讀 887·2021-11-18 10:07
閱讀 3544·2021-10-11 10:57
閱讀 2740·2021-10-08 10:04
閱讀 2999·2021-09-26 10:11
閱讀 1046·2021-09-23 11:21
閱讀 2779·2019-08-29 17:28