摘要: lambda表達式是Java8給我們帶來的幾個重量級新特性之一,借用lambda表達式,可以讓我們的Java程序設計更加簡潔。本文是Java8新特性的第一篇,將探討行爲參數化、lambda表達式,以及方法引用。
Java8新特性系列
- Java8新特性(一) – lambda表達式
- Java8新特性(二) – Optional類
- Java8新特性(三) – 流式數據處理
- 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.Comparator
,java.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)
方法引用是lambda表達式的一種特殊形式,如果正好有某個方法滿足一個lambda表達式的形式,那就可以將這個lambda表達式用方法引用的方式表示,但是如果這個lambda表達式的比較複雜就不能用方法引用進行替換。實際上方法引用是lambda表達式的一種語法糖。
在介紹方法引用使用方式之前,先將方法引用分下類
方法引用共分爲四類:
1.類名::靜態方法名
2.對象::實例方法名
3.類名::實例方法名
4.類名::new
首先來看下第一種 類名::靜態方法名 爲了演示我們自定義了一個Student類
@Setter
@Getter
@ToString
public class Student {
private String name;
private int scope;
public Student() {
}
public Student(String name, int scope) {
this.name = name;
this.scope = scope;
}
public static int compareStudentByScore(Student s1, Student s2) {
return s1.getScope() - s2.getScope();
}
public int studengComparamor1(Student s1, Student s2) {
return s1.getScope() - s2.getScope();
}
}
Student類有兩個屬性name和score並提供了初始化name和score的構造方法,並且在最下方提供了兩個靜態方法分別按score和name進行比較先後順序。
接下來的需求是,按着分數由小到大排列並輸出,在使用方法引用前,我們先使用lambda表達式的方式進行處理
Student student1 = new Student("zhangsan",60);
Student student2 = new Student("lisi",70);
Student student3 = new Student("wangwu",80);
Student student4 = new Student("zhaoliu",90);
List<Student> students = Arrays.asList(student1,student2,student3,student4);
students.sort((o1, o2) -> o1.getScore() - o2.getScore());
students.forEach(student -> System.out.println(student.getScore()));
sort方法接收一個Comparator函數式接口,接口中唯一的抽象方法compare接收兩個參數返回一個int類型值,下方是Comparator接口定義
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
我們再看下Student類中定義的compareStudentByScore靜態方法
public static int compareStudentByScore(Student student1,Student student2){
return student1.getScore() - student2.getScore();
}
同樣是接收兩個參數返回一個int類型值,而且是對Student對象的分數進行比較,所以我們這裏就可以 使用類名::靜態方法名 方法引用替換lambda表達式
students.sort(Student::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));
第二種 對象::實例方法名
我們再自定義一個用於比較Student元素的類
public class StudentComparator {
public int compareStudentByScore(Student student1,Student student2){
return student2.getScore() - student1.getScore();
}
}
StudentComparator中定義了一個非靜態的,實例方法compareStudentByScore,同樣該方法的定義滿足Comparator接口的compare方法定義,所以這裏可以直接使用 對象::實例方法名 的方式使用方法引用來替換lambda表達式
StudentComparator studentComparator = new StudentComparator();
students.sort(studentComparator::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));
第三種 類名::實例方法名
這種方法引用的方式較之前兩種稍微有一些不好理解,因爲無論是通過類名調用靜態方法還是通過對象調用實例方法這都是符合Java的語法,使用起來也比較清晰明瞭。那我們帶着這個疑問來了解一下這個比較特殊的方法引用。
現在再看一下Student類中靜態方法的定義
public static int compareStudentByScore(Student student1,Student student2){
return student1.getScore() - student2.getScore();
}
雖然這個方法在語法上沒有任何問題,可以作爲一個工具正常使用,但是有沒有覺得其在設計上是不合適的或者是錯誤的。這樣的方法定義放在任何一個類中都可以正常使用,而不只是從屬於Student這個類,那如果要定義一個只能從屬於Student類的比較方法下面這個實例方法更合適一些
public int compareByScore(Student student){
return this.getScore() - student.getScore();
}
接收一個Student對象和當前調用該方法的Student對象的分數進行比較即可。現在我們就可以使用 類名::實例方法名 這種方式的方法引用替換lambda表達式了
students.sort(Student::compareByScore);
students.forEach(student -> System.out.println(student.getScore()));
這裏非常奇怪,sort方法接收的lambda表達式不應該是兩個參數麼,爲什麼這個實例方法只有一個參數也滿足了lambda表達式的定義(想想這個方法是誰來調用的)。這就是 類名::實例方法名 這種方法引用的特殊之處:當使用 類名::實例方法名 方法引用時,一定是lambda表達式所接收的第一個參數來調用實例方法,如果lambda表達式接收多個參數,其餘的參數作爲方法的參數傳遞進去。
結合本例來看,最初的lambda表達式是這樣的
students.sort((o1, o2) -> o1.getScore() - o2.getScore());
那使用 類名::實例方法名 方法引用時,一定是o1來調用了compareByScore實例方法,並將o2作爲參數傳遞進來進行比較。是不是就符合了compareByScore的方法定義。第四種 類名::new
也稱構造方法引用,和前兩種類似只要符合lambda表達式的定義即可,回想下Supplier函數式接口的get方法,不接收參數有返回值,正好符合無參構造方法的定義
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
* @return a result
*/
T get();
}
Supplier<Student> supplier = Student::new;
上面就是使用了Student類構造方法引用創建了supplier實例,以後通過supplier.get()就可以獲取一個Student類型的對象,前提是Student類中存在無參構造方法