Java中的函數式編程

前言

JDK8引入的Lambda表達式和Stream爲Java平臺提供了函數式編程的支持,極大地提升了開發效率.本文結合網絡資源和自身使用經驗,介紹下Java中的函數式編程html

Java中的函數式編程

出現的緣由

語言面臨着要麼改變,要麼衰亡的壓力. Java是傳統的命令式編程,而函數式編程.是一種更"高級"的編程範式,Java爲了支持它,推出了Lambda表達式和Stream.java

函數式編程 VS 命令式編程

一言以蔽之: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表示式 VS 方法

Lambda的語法結構以下

// 參數列表 箭頭 方法體
( ParameterType1 param1,ParameterType2 param2... ) -> { ... } 
複製代碼

方法的語法結構以下(暫不考慮throws)

訪問權限 ReturnType methodName(ParameterType1 param1,ParameterType2 param2...){
	... 
}
複製代碼

能夠看出,Lambda表達式能夠看作方法的簡化形式: 沒有訪問權限,返回類型以及方法名.而且它還能夠進一步簡化.

函數式接口

有且只有一個抽象方法的接口

首先澄清一下這裏抽象方法的定義(java doc)

  1. 接口中的default方法不是抽象方法,由於它有默認實現
  2. 若是接口中的方法覆蓋了java.lang.Object中的方法,也不作計數

下面以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中有幾個抽象方法

  1. 是抽象方法
  2. 覆蓋了Object.equal()方法,因此不是
  3. 是default方法,也不是抽象方法

只有一個抽象方法,所以Comparator接口是一個函數式接口.

說了這麼多,函數式接口到底有什麼做用呢?

Lambda表達式容許你直接之內聯的形式爲函數式接口的抽象方法提供實現,並把整個表達式做爲函數式接口的實例

當咱們把一個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的局部變量限制

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.

Lambda表達式的匹配

鴨子類型: 「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就能夠被稱爲鴨子。」

仍是以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表達式的匹配規則至關的寬鬆簡單,這也讓它的使用更加方便.那麼如何有效的利用它呢?

Stream

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()的,這就是參數化

parallelStream—並行化任務的最簡單方式

假設如今有一個包含100w元素的List,要對它進行一系列操做,元素不少,會消耗不少時間.

elements.stream().filter(...).map(...).collect(...);
複製代碼

很明顯,多線程執行可以加速執行,只須要一點點修改就能使它以多線程模式執行,wonderful!

elements.parallelStream().filter(...).map(...).collect(...);
複製代碼

parallelStream的底層是fork/join框架.能夠把它理解一個智能的線程池,它能將任務拆分並分發給不一樣的線程執行,最終彙總.然而 parallelStream並非銀彈,如下幾點須要注意

  1. parallelStream()的後續操做中進行排序(調用sorted())得出的結果是無效的.緣由很簡單,它是多線程執行的.這個問題的解決辦法就是在parallelStream結束後再進行排序.
  2. 執行的任務不能依賴於線程私有數據(好比ThreadLocal),因爲是多線程執行,其餘線程並無當前線程棧上的數據,一個最多見的例子就是在spring中執行數據庫操做,session是綁定在線程上的,這時候以parallelStream執行就會報錯 can't obtain session
  3. 任務數量必須足夠多/單個任務耗時很長(io/網絡操做)纔有必要使用parallelStream,否則運行反而會更慢. 由於要fork/join也是要付出很大代價的: 劃分子任務,分配任務給線程.具體的計算規則能夠參考這篇文章

更好的使用Lambda

預約義的函數式接口

java.util.function下有不少JDK預約義的函數式接口.以經常使用的爲例

預約義的FunctionInterface

使用JDK中新增的Lambda相關方法

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相關方法,能夠大概遵循下面這個步驟

  1. 查找符合需求的api,如replaceAll
  2. 查看該方法要求的行爲(參數)的定義,如BiFucntion.apply()
  3. 編寫符合方法簽名的Lambda表達式

用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表達式的威力.


相關文章
相關標籤/搜索