為什麼要使用函數語言程式設計
函數語言程式設計更多時候是一種程式設計的思維方式,是種方法論。函式式與指令式程式設計的區別主要在於:函數語言程式設計是告訴程式碼你要做什麼,而指令式程式設計則是告訴程式碼要怎麼做。說白了,函數語言程式設計是基於某種語法或呼叫API去進行程式設計。
例如,我們現在需要從一組數字中,找出最小的那個數字,若使用用指令式程式設計實現這個需求的話,那麼所編寫的程式碼如下:
public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7, 8}; int min = Integer.MAX_VALUE; for (int num : nums) { if (num < min) { min = num; } } System.out.println(min);}
而使用函數語言程式設計進行實現的話,所編寫的程式碼如下:
public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7, 8}; int min = IntStream.of(nums).min().getAsInt(); System.out.println(min);}
從以上的兩個例子中,可以看出,指令式程式設計需要自己去實現具體的邏輯細節。而函數語言程式設計則是呼叫API完成需求的實現,將原本命令式的程式碼寫成一系列巢狀的函式呼叫,在函數語言程式設計下顯得程式碼更簡潔、易懂,這就是為什麼要使用函數語言程式設計的原因之一。所以才說函數語言程式設計是告訴程式碼你要做什麼,而指令式程式設計則是告訴程式碼要怎麼做,是一種思維的轉變。
說到函數語言程式設計就不得不提一下lambda表示式,它是函數語言程式設計的基礎。在Java還不支援lambda表示式時,我們需要建立一個執行緒的話,需要編寫如下程式碼:
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println("running"); } }).start();}
而使用lambda表示式一句程式碼就能完成執行緒的建立,lambda強調了函式的輸入輸出,隱藏了過程的細節,並且可以接受函式當作輸入(引數)和輸出(返回值):
public static void main(String[] args) { new Thread(() -> System.out.println("running")).start();}
注:箭頭的左邊是輸入,右邊則是輸出
該lambda表示式的作用其實就是返回了Runnable介面的實現物件,這與我們呼叫某個方法獲取例項物件類似,只不過是將實現程式碼直接寫在了lambda表示式裡。我們可以做個簡單的對比:
public static void main(String[] args) { Runnable runnable1 = () -> System.out.println("running"); Runnable runnable2 = RunnableFactory.getInstance();}
JDK8介面新特性
1.函式介面,介面只能有一個需要實現的方法,可以使用@FunctionalInterface 註解進行宣告。如下:
@FunctionalInterfaceinterface Interface1 { int doubleNum(int i);}
使用lambda表示式獲取該介面的實現例項的幾種寫法:
public static void main(String[] args) { // 最常見的寫法 Interface1 i1 = (i) -> i * 2; Interface1 i2 = i -> i * 2; // 可以指定引數型別 Interface1 i3 = (int i) -> i * 2; // 若有多行程式碼可以這麼寫 Interface1 i4 = (int i) -> { System.out.println(i); return i * 2; };}
2.比較重要的一個介面特性是介面的預設方法,用於提供預設實現。預設方法和普通實現類的方法一樣,可以使用this等關鍵字:
@FunctionalInterfaceinterface Interface1 { int doubleNum(int i); default int add(int x, int y) { return x + y; }}
之所以說預設方法這個特性比較重要,是因為我們藉助這個特性可以在以前所編寫的一些介面上提供預設實現,並且不會影響任何的實現類以及既有的程式碼。例如我們最熟悉的List介面,在JDK1.2以來List介面就沒有改動過任何程式碼,到了1.8之後才使用這個新特性增加了一些預設實現。這是因為如果沒有預設方法的特性的話,修改介面程式碼帶來的影響是巨大的,而有了預設方法後,增加預設實現可以不影響任何的程式碼。
3.當介面多重繼承時,可能會發生預設方法覆蓋的問題,這時可以去指定使用哪一個介面的預設方法實現,如下示例:
@FunctionalInterfaceinterface Interface1 { int doubleNum(int i); default int add(int x, int y) { return x + y; }}@FunctionalInterfaceinterface Interface2 { int doubleNum(int i); default int add(int x, int y) { return x + y; }}@FunctionalInterfaceinterface Interface3 extends Interface1, Interface2 { @Override default int add(int x, int y) { // 指定使用哪一個介面的預設方法實現 return Interface1.super.add(x, y); }}
函式介面
我們本小節來看看JDK8裡自帶了哪些重要的函式介面:
可以看到上表中有好幾個介面,而其中最常用的是Function介面,它能為我們省去定義一些不必要的函式介面,減少介面的數量。我們使用一個簡單的例子演示一下 Function 介面的使用:
import java.text.DecimalFormat;import java.util.function.Function;class MyMoney { private final int money; public MyMoney(int money) { this.money = money; } public void printMoney(Function<Integer, String> moneyFormat) { System.out.println("我的存款: " + moneyFormat.apply(this.money)); }}public class MoneyDemo { public static void main(String[] args) { MyMoney me = new MyMoney(99999999); Function<Integer, String> moneyFormat = i -> new DecimalFormat("#,###").format(i); // 函式介面支援鏈式操作,例如增加一個字串 me.printMoney(moneyFormat.andThen(s -> "人民幣 " + s)); }}
執行以上例子,控制檯輸出如下:
我的存款: 人民幣 99,999,999
若在這個例子中不使用Function介面的話,則需要自行定義一個函式介面,並且不支援鏈式操作,如下示例:
import java.text.DecimalFormat;// 自定義一個函式介面@FunctionalInterfaceinterface IMoneyFormat { String format(int i);}class MyMoney { private final int money; public MyMoney(int money) { this.money = money; } public void printMoney(IMoneyFormat moneyFormat) { System.out.println("我的存款: " + moneyFormat.format(this.money)); }}public class MoneyDemo { public static void main(String[] args) { MyMoney me = new MyMoney(99999999); IMoneyFormat moneyFormat = i -> new DecimalFormat("#,###").format(i); me.printMoney(moneyFormat); }}
然後我們再來看看Predicate介面和Consumer介面的使用,如下示例:
public static void main(String[] args) { // 斷言函式介面 Predicate<Integer> predicate = i -> i > 0; System.out.println(predicate.test(-9)); // 消費函式介面 Consumer<String> consumer = System.out::println; consumer.accept("這是輸入的資料");}
執行以上例子,控制檯輸出如下:
false這是輸入的資料
這些介面一般有對基本型別的封裝,使用特定型別的介面就不需要去指定泛型了,如下示例:
public static void main(String[] args) { // 斷言函式介面 IntPredicate intPredicate = i -> i > 0; System.out.println(intPredicate.test(-9)); // 消費函式介面 IntConsumer intConsumer = (value) -> System.out.println("輸入的資料是:" + value); intConsumer.accept(123);}
執行以上程式碼,控制檯輸出如下:
false輸入的資料是:123
有了以上介面示例的鋪墊,我們應該對函式介面的使用有了一個初步的瞭解,接下來我們演示剩下的函式介面使用方式:
public static void main(String[] args) { // 提供資料介面 Supplier<Integer> supplier = () -> 10 + 1; System.out.println("提供的資料是:" + supplier.get()); // 一元函式介面 UnaryOperator<Integer> unaryOperator = i -> i * 2; System.out.println("計算結果為:" + unaryOperator.apply(10)); // 二元函式介面 BinaryOperator<Integer> binaryOperator = (a, b) -> a * b; System.out.println("計算結果為:" + binaryOperator.apply(10, 10));}
執行以上程式碼,控制檯輸出如下:
提供的資料是:11計算結果為:20計算結果為:100
而BiFunction介面就是比Function介面多了一個輸入而已,如下示例:
class MyMoney { private final int money; private final String name; public MyMoney(int money, String name) { this.money = money; this.name = name; } public void printMoney(BiFunction<Integer, String, String> moneyFormat) { System.out.println(moneyFormat.apply(this.money, this.name)); }}public class MoneyDemo { public static void main(String[] args) { MyMoney me = new MyMoney(99999999, "小明"); BiFunction<Integer, String, String> moneyFormat = (i, name) -> name + "的存款: " + new DecimalFormat("#,###").format(i); me.printMoney(moneyFormat); }}
執行以上程式碼,控制檯輸出如下:
小明的存款: 99,999,999
方法引用在學習了lambda表示式之後,我們通常會使用lambda表示式來建立匿名方法。但有的時候我們僅僅是需要呼叫一個已存在的方法。如下示例:
Arrays.sort(stringsArray, (s1, s2) -> s1.compareToIgnoreCase(s2));
在jdk8中,我們可以透過一個新特性來簡寫這段lambda表示式。如下示例:
Arrays.sort(stringsArray, String::compareToIgnoreCase);
這種特性就叫做方法引用(Method Reference)。方法引用的標準形式是:類名::方法名。(注意:只需要寫方法名,不需要寫括號)。
目前方法引用共有以下四種形式:
下面我們用一個簡單的例子來演示一下方法引用的幾種寫法。首先定義一個實體類:
public class Dog { private String name = "二哈"; private int food = 10; public Dog() { } public Dog(String name) { this.name = name; } public static void bark(Dog dog) { System.out.println(dog + "叫了"); } public int eat(int num) { System.out.println("吃了" + num + "斤"); this.food -= num; return this.food; } @Override public String toString() { return this.name; }}
透過方法引用來呼叫該實體類中的方法,程式碼如下:
package org.zero01.example.demo;import java.util.function.*;/** * @ProjectName demo * @Author: zeroJun * @Date: 2018/9/21 13:09 * @Description: 方法引用demo */public class MethodRefrenceDemo { public static void main(String[] args) { // 方法引用,呼叫列印方法 Consumer<String> consumer = System.out::println; consumer.accept("接收的資料"); // 靜態方法引用,透過類名即可呼叫 Consumer<Dog> consumer2 = Dog::bark; consumer2.accept(new Dog()); // 例項方法引用,透過物件例項進行引用 Dog dog = new Dog(); IntUnaryOperator function = dog::eat; System.out.println("還剩下" + function.applyAsInt(2) + "斤"); // 另一種透過例項方法引用的方式,之所以可以這麼幹是因為JDK預設會把當前例項傳入到非靜態方法,引數名為this,引數位置為第一個,所以我們在非靜態方法中才能訪問this,那麼就可以透過BiFunction傳入例項物件進行例項方法的引用 Dog dog2 = new Dog(); BiFunction<Dog, Integer, Integer> biFunction = Dog::eat; System.out.println("還剩下" + biFunction.apply(dog2, 2) + "斤"); // 無參建構函式的方法引用,類似於靜態方法引用,只需要分析好輸入輸出即可 Supplier<Dog> supplier = Dog::new; System.out.println("建立了新物件:" + supplier.get()); // 有參建構函式的方法引用 Function<String, Dog> function2 = Dog::new; System.out.println("建立了新物件:" + function2.apply("旺財")); }}
型別推斷透過以上的例子,我們知道之所以能夠使用Lambda表示式的依據是必須有相應的函式介面。這一點跟Java是強型別語言吻合,也就是說你並不能在程式碼的任何地方任性的寫Lambda表示式。實際上Lambda的型別就是對應函式介面的型別。Lambda表示式另一個依據是型別推斷機制,在上下文資訊足夠的情況下,編譯器可以推斷出引數表的型別,而不需要顯式指名。
所以說 Lambda 表示式的型別是從 Lambda 的上下文推斷出來的,上下文中 Lambda 表示式需要的型別稱為目標型別,如下圖所示:
接下來我們使用一個簡單的例子,演示一下 Lambda 表示式的幾種型別推斷,首先定義一個簡單的函式介面:
@FunctionalInterfaceinterface IMath { int add(int x, int y);}
示例程式碼如下:
public class TypeDemo { public static void main(String[] args) { // 1.透過變數型別定義 IMath iMath = (x, y) -> x + y; // 2.陣列構建的方式 IMath[] iMaths = {(x, y) -> x + y}; // 3.強轉型別的方式 Object object = (IMath) (x, y) -> x + y; // 4.透過方法返回值確定型別 IMath result = createIMathObj(); // 5.透過方法引數確定型別 test((x, y) -> x + y); } public static IMath createIMathObj() { return (x, y) -> x + y; } public static void test(IMath iMath){ return; }}
變數引用Lambda表示式類似於實現了指定介面的內部類或者說匿名類,所以在Lambda表示式中引用變數和我們在匿名類中引用變數的規則是一樣的。如下示例:
public static void main(String[] args) { String str = "當前的系統時間戳是: "; Consumer<Long> consumer = s -> System.out.println(str + s); consumer.accept(System.currentTimeMillis());}
值得一提的是,在JDK1.8之前我們一般會將匿名類裡訪問的外部變數設定為final,而在JDK1.8裡預設會將這個匿名類裡訪問的外部變數給設定為final。例如我現在改變str變數的值,ide就會提示錯誤:
至於為什麼要將變數設定final,這是因為在Java裡沒有引用傳遞,變數都是值傳遞的。不將變數設定為final的話,如果外部變數的引用被改變了,那麼最終得出來的結果就會是錯誤的。
下面用一組圖片簡單演示一下值傳遞與引用傳遞的區別。以列表為例,當只是值傳遞時,匿名類裡對外部變數的引用是一個值物件:
若此時list變數指向了另一個物件,那麼匿名類裡引用的還是之前那個值物件,所以我們才需要將其設定為final防止外部變數引用改變:
而如果是引用傳遞的話,匿名類裡對外部變數的引用就不是值物件了,而是指標指向這個外部變數:
所以就算list變數指向了另一個物件,匿名類裡的引用也會隨著外部變數的引用改變而改變:
級聯表示式和柯里化在函數語言程式設計中,函式既可以接收也可以返回其他函式。函式不再像傳統的面向物件程式設計中一樣,只是一個物件的工廠或生成器,它也能夠建立和返回另一個函式。返回函式的函式可以變成級聯 lambda 表示式,特別值得注意的是程式碼非常簡短。儘管此語法初看起來可能非常陌生,但它有自己的用途。
級聯表示式就是多個lambda表示式的組合,這裡涉及到一個高階函式的概念,所謂高階函式就是一個可以返回函式的函式,如下示例:
// 實現了 x + y 的級聯表示式Function<Integer, Function<Integer, Integer>> function1 = x -> y -> x + y;System.out.println("計算結果為: " + function1.apply(2).apply(3)); // 計算結果為: 5
這裡的 y -> x + y 是作為一個函式返回給上一級表示式,所以第一級表示式的輸出是 y -> x + y這個函式,如果使用括號括起來可能會好理解一些:
x -> (y -> x + y)
級聯表示式可以實現函式柯里化,簡單來說柯里化就是把本來多個引數的函式轉換為只有一個引數的函式,如下示例:
Function<Integer, Function<Integer, Function<Integer, Integer>>> function2 = x -> y -> z -> x + y + z;System.out.println("計算結果為: " + function2.apply(1).apply(2).apply(3)); // 計算結果為: 6
函式柯里化的目的是將函式標準化,函式可靈活組合,方便統一處理等,例如我可以在迴圈裡只需要呼叫同一個方法,而不需要呼叫另外的方法就能實現一個數組內元素的求和計算,程式碼如下:
public static void main(String[] args) { Function<Integer, Function<Integer, Function<Integer, Integer>>> f3 = x -> y -> z -> x + y + z; int[] nums = {1, 2, 3}; for (int num : nums) { if (f3 instanceof Function) { Object obj = f3.apply(num); if (obj instanceof Function) { f3 = (Function) obj; } else { System.out.println("呼叫結束, 結果為: " + obj); // 呼叫結束, 結果為: 6 } } }}
級聯表示式和柯里化一般在實際開發中並不是很常見,所以對其概念稍有理解即可,這裡只是簡單帶過,若對其感興趣的可以查閱相關資料。