JDK8引入的Lambda表達式和Stream爲Java平臺提供了函數式編程的支持,極大地提升了開發效率.本文結合網絡資源和自身使用經驗,介紹下Java中的函數式編程html
語言面臨着要麼改變,要麼衰亡的壓力. Java是傳統的命令式編程,而函數式編程.是一種更"高級"的編程範式,Java爲了支持它,推出了Lambda表達式和Stream.java
一言以蔽之:spring
函數式編程是:shell
"我如今想要這樣東西(怎麼辦到我無論,你來處理)"數據庫
命令式編程是:編程
"你要先...,再...,最後...,就能拿到這樣東西了"設計模式
事實上,函數式編程的底層實現仍是命令式編程,就像面嚮對象語言核心部分(如JVM)是由面向過程語言(如C)實現的.畢竟髒活累活老是要有人去作的.api
以一個比較蘋果重量的Comparator爲例,類Apple定義以下網絡
public class Apple{
private int weight;
private int type;
public int getWeight(){
return this.weight;
}
public int getType(){
return this.type;
}
}
複製代碼
若是按照匿名類實現,代碼會是這樣,整體來講比較繁瑣.session
Comparator<Apple> byWeight=new Comparator<>(){
@Override
public int compareTo(Apple a1,Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
複製代碼
若是使用Lambda表達式,最繁瑣的形式會是這樣
Comparator<Apple> byWeight=
(Apple a1,Apple a2) -> {return a1.getWeight().compareTo(a2.getWeight());}
複製代碼
要搞清楚Lambda表達式的工做原理,首先要了解它的語法以及函數式接口
Lambda的語法結構以下
// 參數列表 箭頭 方法體
( ParameterType1 param1,ParameterType2 param2... ) -> { ... }
複製代碼
方法的語法結構以下(暫不考慮throws)
訪問權限 ReturnType methodName(ParameterType1 param1,ParameterType2 param2...){
...
}
複製代碼
能夠看出,Lambda表達式能夠看作方法的簡化形式: 沒有訪問權限,返回類型以及方法名.而且它還能夠進一步簡化.
有且只有一個抽象方法的接口
首先澄清一下這裏抽象方法的定義(java doc)
下面以Comparator爲例(適當精簡)
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);//1
boolean equals(Object obj);//2
default Comparator<T> reversed() {//3
return Collections.reverseOrder(this);
}
}
複製代碼
@FunctionalInterface用來標識一個接口是函數式接口,它和@Override註解相似,只是編譯時起檢查做用,若是這個接口定義不符合的話,編譯時就會報錯,若是一個接口符合函數式接口的定義,即便沒有這個註解依然是有效的
再來看下Comparator中有幾個抽象方法
只有一個抽象方法,所以Comparator接口是一個函數式接口.
說了這麼多,函數式接口到底有什麼做用呢?
Lambda表達式容許你直接之內聯的形式爲函數式接口的抽象方法提供實現,並把整個表達式做爲函數式接口的實例
當咱們把一個Lambda表達式賦給一個函數式接口時,這個表達式對應的一定是接口中惟一的抽象方法,所以就不須要以匿名類那麼繁瑣的形式去實現這個接口.能夠說在語法簡化上,Lambda表達式完成了方法層面的簡化,函數式接口完成了類層面的簡化.
在Lambda中,除了參數列表的大括號()
和箭頭→
不能省略,其餘部分若是編譯器能夠自動推斷,都能省略.
簡化規則1: 若是編譯器能夠推斷出參數類型,參數列表中就能夠省略參數類型
Comparator<Apple> byWeight=
(a1,a2) -> {return a1.getWeight().compareTo(a2.getWeight());}
複製代碼
簡化規則2: 若是方法體只有一條語句,花括號*{}和return*(若是有的話)均可以省略
Comparator<Apple> byWeight=
(a1,a2) -> a1.getWeight().compareTo(a2.getWeight())
複製代碼
簡化規則3: 能夠經過
方法引用
來調用方法
首先要介紹一個新概念:方法引用,它的基本思想是:若是一個Lambda表明的只是直接調用這個方法,那最好仍是用名稱來調用它,而不是去描述如何調用它. 這樣可讀性更好.
方法引用的通常形式以下
//能夠表示對靜態/實例方法的調用
類名::方法名
//只能表示實例方法
this::方法名
複製代碼
針對上面的例子,首先利用JDK提供的工具作一些簡化
Comparator<Apple> byWeight=
Comparator.comparingInt((a)->a.getWeight())
複製代碼
而後利用方法引用能夠簡化爲以下形式,是否是簡單明瞭?
Comparator<Apple> byWeight= Comparator.comparingInt(Apple::getWeight)
複製代碼
然而Lambda並非萬金油,它也有本身的限制.
Lambda引用局部變量時,要求局部變量時final或effective final(即僅被賦值一次,以後不被修改).實例變量則能夠隨意使用.這個限制有以下幾個緣由
堆和棧的差別
局部變量是存儲在棧上的,即局部變量是線程私有的,而Lambda表達式不是線程私有的,它可能在其餘線程上執行,而其餘線程上是沒有對應的局部變量的(實例變量是在堆上分配的,任何線程都能訪問到),爲了解決這個問題,Java會將局部變量的拷貝一份保存到在Lambda表達式中.所以Java在訪問局部變量時,實際是在訪問它的副本,而不是訪問原始變量. 若是局部變量不是effective final的(好比在Lambda表達式以後對原始變量進行了修改),拷貝就可能和原始變量不一致,會引起不少語義上的問題(匿名內部類中局部變量也是相同緣由)
避免函數式編程的不正確使用
局部變量必須是effective final剛好符合函數式編程的特徵之一—immutable data 數據不可變.數據不可變便沒有了數據競爭問題,這樣最有利於並行
假設非effective final局部變量是被容許的,那麼下面這句代碼其實是串行執行的,由於每一個任務都在競爭sum這個變量
int sum=0;
//parallelStream()會以多線程形式執行任務
ints.parallelStream().forEach(i->sum+=i);
複製代碼
以函數式編程的思想來寫,應該是這樣,沒有數據競爭問題,可以充分利用並行.
int sum=ints.parallelStream().reduce(0, (e1, e2) -> e1 + e2);
複製代碼
併發問題
引用JLS的說明 ,什麼狀況下會致使併發問題筆者還沒搞清楚.
The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.
鴨子類型: 「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就能夠被稱爲鴨子。」
仍是以Comparator爲例,首先看下Comparator.compare方法簽名
int compare(T o1, T o2);
複製代碼
而上面咱們提供的Lambda表達式正好符合這個形式: 兩個同類型參數,返回int值
(Apple a1,Apple a2) -> {return a1.getWeight().compareTo(a2.getWeight());}
複製代碼
在這裏 Lambda表達式就表明"鴨子"這個類型,而Comparator的行爲徹底符合鴨子的特徵("走起來像鴨子,游泳起來像鴨子,叫起來也想鴨子"),就能夠認爲它"是"一隻"鴨子"
假設如今咱們有以下接口
public interface SomeClass<T>{
int someMethod(T a1,T a2);
}
複製代碼
那麼上面這個Lambda一樣適用於這個方法,由於它也符合鴨子的特徵
SomeClass<Apple> someMethod=
(Apple a1,Apple a2) -> {return ...;}
複製代碼
Lambda表達式的匹配規則至關的寬鬆簡單,這也讓它的使用更加方便.那麼如何有效的利用它呢?
Java中的Stream是對函數式編程中pipeline的實現,平常業務開發中用的特別特別多
,很值得學習.
需求: 有一堆蘋果List apples,以重量從小到大,獲取他們的品種.
以命令式編程來作會是:
Comparator<Apple> byWeight=new Comparator<>(){
@Override
public int compareTo(Apple a1,Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
apples.sort(byWeight);
List<Integer> types=new ArrayList();
for(Apple apple:apples){
types.add(apple.getType());
}
複製代碼
有了Stream,會是這樣,語義清晰了不少,我的很是喜歡這種鏈式調用(鏈式調用一時爽,一直鏈式一直爽)再次展現出命令式編程和函數式編程的不一樣
List<Integer> types=apples.stream()
.sorted(Comparator.comparingInt(Apple::getWeight))
.map(Apple::getType)
.collect(Collectors.toList());
複製代碼
在平常開發中,將一個列表進行排序過濾轉化最後收集這個套路十分常見,這個過程當中變化的只是咱們傳遞過去的Lambda表達式
,這也被稱爲行爲參數化
行爲參數化
一個方法接受多個不一樣的行爲做爲參數,並在內部使用它們,完成不一樣行爲.
何爲行爲
實際一點來講,獲取蘋果的類型(這個方法)就是一個行爲,在代碼中就是就是 map(Apple::getType)中的Apple::getType,假設Apple增長了一個屬性尺寸Size,獲取蘋果的尺寸這個新的行爲就是Apple::getSize.
參數化
Apple::getType這個行爲 是做爲一個參數傳遞給map()的,這就是參數化
假設如今有一個包含100w元素的List,要對它進行一系列操做,元素不少,會消耗不少時間.
elements.stream().filter(...).map(...).collect(...);
複製代碼
很明顯,多線程執行可以加速執行,只須要一點點修改就能使它以多線程模式執行,wonderful!
elements.parallelStream().filter(...).map(...).collect(...);
複製代碼
parallelStream的底層是fork/join框架.能夠把它理解一個智能的線程池,它能將任務拆分並分發給不一樣的線程執行,最終彙總.然而 parallelStream並非銀彈,如下幾點須要注意
java.util.function下有不少JDK預約義的函數式接口.以經常使用的爲例
JDK8的底層機制也添加了不少和函數式編程相關的改進,做爲開發者,如何更好的享受這免費的午飯呢?
又又又是一個例子: 對Map<String,Integer> map中的全部值進行+1操做,在JDK8中,最好的作法以下
map.replaceAll((key,oldVal)->oldVal+1);
複製代碼
首先看replaceAll方法的簽名,replaceAll接收一個BiFunction做爲參數,很明顯 這個BiFunction就是咱們要傳遞的行爲
/**
* Replaces each entry's value with the result of invoking the given
* function on that entry until all entries have been processed or the
* function throws an exception. Exceptions thrown by the function are
* relayed to the caller.
*
**/
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function){
...
}
複製代碼
再來看BiFunction的定義,它是一個函數式接口,apply方法接收兩個參數,有返回值
@FunctionalInterface
public interface BiFunction<T, U, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
}
複製代碼
再來看咱們提供的Lambda表達式,符合Map.replaceAll中對BiFunction.apply的方法簽名要求.完美!!!
(key,oldVal)->oldVal+1
複製代碼
使用JDK8新增的Lambda相關方法,能夠大概遵循下面這個步驟
又是一個例子,使用Lambda來實現模板方法模式,需求以下
根據不一樣行爲對Apple進行不一樣的處理
public void templateMethod(Supplier<Apple> supplier,Consumer<Apple> consumer){
...
Apple apple=supplier.get();
...
consumer.accept(apple);
...
}
//eg.
Supplier normalSupplier=()->new Apple(10,1);
Consumer<Apple> weightConsumer=(a)->System.out.println(a.getWeight());
templateMethod(normalSupplier,weightConsumer);
//eg.
Supplier bigSupplier=()->new Apple(100,2);
Consumer<Apple> typeConsumer=(a)->System.out.println(a.getType());
templateMethod(bigSupplier,typeConsumer);
複製代碼
上面這個例子就是行爲參數化
的直觀體現,相比傳統的模板方法設計模式,免去了抽象出類的麻煩,更加易用.
Lambda表達式和Stream使Java用起來再也不那麼繁瑣,即便不探究其底層原理,也能用的很舒服.可是隻有真正的理解函數式編程的思想,才能真正發揮Lambda表達式的威力.