lambda表達式是java8給我們帶來的幾個重量級新特性之一,借用lambda表達式,可以讓我們的java程序設計更加簡潔。最近新的項目摒棄了1.6的版本,全面基於java8進行開發,本文是java8新特性的第一篇,將探討行為參數化、lambda表達式,以及方法引用。
一. 行為參數化
行為參數化簡單的說就是函數的主體僅包含模板類通用代碼,而一些會隨着業務場景而變化的邏輯則以參數的形式傳遞到函數之中,采用行為參數化可以讓程序更加的通用,以應對頻繁變更的需求。
考慮一個業務場景,假設我們需要通過程序對蘋果進行篩選,我們先定義一個蘋果的實體:
package com.lance.code.generation.java8; import java.awt.*; /** * Created by wangwei on 2016/11/2. */ public class Apple { /** 編號 */ private long id; /** 顏色 */ private Color color; /** 重量 */ private float weight; /** 產地 */ private String origin; public Apple() { } @Override public String toString() { return "Apple{" + "id=" + id + ", color=" + color + ", weight=" + weight + ", origin='" + origin + '\'' + '}'; } public Apple(long id, Color color, float weight, String origin) { this.id = id; this.color = color; this.weight = weight; this.origin = origin; } public long getId() { return id; } public void setId(long id) { this.id = id; } public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } public float getWeight() { return weight; } public void setWeight(float weight) { this.weight = weight; } public String getOrigin() { return origin; } public void setOrigin(String origin) { this.origin = origin; } }
用戶最開始的需求可能只是簡單的希望能夠通過程序篩選出綠色的蘋果,於是我們可以很快的通過程序實現:
/** * 篩選綠蘋果 * * @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)
參考:
- Java 8 In Action 大家多多支持正版^_^
- What’s New in JDK 8