Java函數式編程和lambda表達式

爲何要使用函數式編程

函數式編程更多時候是一種編程的思惟方式,是種方法論。函數式與命令式編程的區別主要在於:函數式編程是告訴代碼你要作什麼,而命令式編程則是告訴代碼要怎麼作。說白了,函數式編程是基於某種語法或調用API去進行編程。例如,咱們如今須要從一組數字中,找出最小的那個數字,若使用用命令式編程實現這個需求的話,那麼所編寫的代碼以下:java

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表達式裏。咱們能夠作個簡單的對比:app

public static void main(String[] args) {
    Runnable runnable1 = () -> System.out.println("running");
    Runnable runnable2 = RunnableFactory.getInstance();
}

JDK8接口新特性

1.函數接口,接口只能有一個須要實現的方法,可使用@FunctionalInterface 註解進行聲明。以下:分佈式

@FunctionalInterface
interface Interface1 {
    int doubleNum(int i);
}

使用lambda表達式獲取該接口的實現實例的幾種寫法:ide

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等關鍵字:函數式編程

@FunctionalInterface
interface Interface1 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

之因此說默認方法這個特性比較重要,是由於咱們藉助這個特性能夠在之前所編寫的一些接口上提供默認實現,而且不會影響任何的實現類以及既有的代碼。例如咱們最熟悉的List接口,在JDK1.2以來List接口就沒有改動過任何代碼,到了1.8以後才使用這個新特性增長了一些默認實現。這是由於若是沒有默認方法的特性的話,修改接口代碼帶來的影響是巨大的,而有了默認方法後,增長默認實現能夠不影響任何的代碼。

3.當接口多重繼承時,可能會發生默認方法覆蓋的問題,這時能夠去指定使用哪個接口的默認方法實現,以下示例:

@FunctionalInterface
interface Interface1 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

@FunctionalInterface
interface Interface2 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

@FunctionalInterface
interface Interface3 extends Interface1, Interface2 {

    @Override
    default int add(int x, int y) {
        // 指定使用哪個接口的默認方法實現
        return Interface1.super.add(x, y);
    }
}

函數接口

咱們本小節來看看JDK8裏自帶了哪些重要的函數接口:
Java函數式編程和lambda表達式

能夠看到上表中有好幾個接口,而其中最經常使用的是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;

// 自定義一個函數接口
@FunctionalInterface
interface 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)。方法引用的標準形式是:類名::方法名。(注意:只須要寫方法名,不須要寫括號)。

目前方法引用共有如下四種形式:

類型 示例 代碼示例 對應的Lambda表達式
引用靜態方法 ContainingClass::staticMethodName String::valueOf (s) -> String.valueOf(s)
引用某個對象的實例方法 containingObject::instanceMethodName x::toString() () -> this.toString()
引用某個類型的任意對象的實例方法 ContainingType::methodName String::toString (s) -> s.toString
引用構造方法 ClassName::new String::new () -> new String()

下面咱們用一個簡單的例子來演示一下方法引用的幾種寫法。首先定義一個實體類:

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表達式另外一個依據是類型推斷機制,在上下文信息足夠的狀況下,編譯器能夠推斷出參數表的類型,而不須要顯式指名。

若是你們想學習以上路線內容,在此我向你們推薦一個架構學習交流羣。交流學習羣號874811168 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

因此說 Lambda 表達式的類型是從 Lambda 的上下文推斷出來的,上下文中 Lambda 表達式須要的類型稱爲目標類型,以下圖所示:
Java函數式編程和lambda表達式

接下來咱們使用一個簡單的例子,演示一下 Lambda 表達式的幾種類型推斷,首先定義一個簡單的函數接口:

@FunctionalInterface
interface 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就會提示錯誤:
Java函數式編程和lambda表達式

至於爲何要將變量設置final,這是由於在Java裏沒有引用傳遞,變量都是值傳遞的。不將變量設置爲final的話,若是外部變量的引用被改變了,那麼最終得出來的結果就會是錯誤的。

下面用一組圖片簡單演示一下值傳遞與引用傳遞的區別。以列表爲例,當只是值傳遞時,匿名類裏對外部變量的引用是一個值對象:
Java函數式編程和lambda表達式

若此時list變量指向了另外一個對象,那麼匿名類裏引用的仍是以前那個值對象,因此咱們才須要將其設置爲final防止外部變量引用改變:
Java函數式編程和lambda表達式

而若是是引用傳遞的話,匿名類裏對外部變量的引用就不是值對象了,而是指針指向這個外部變量:
Java函數式編程和lambda表達式

因此就算list變量指向了另外一個對象,匿名類裏的引用也會隨着外部變量的引用改變而改變:
Java函數式編程和lambda表達式

級聯表達式和柯里化

在函數式編程中,函數既能夠接收也能夠返回其餘函數。函數再也不像傳統的面向對象編程中同樣,只是一個對象的工廠或生成器,它也可以建立和返回另外一個函數。返回函數的函數能夠變成級聯 lambda 表達式,特別值得注意的是代碼很是簡短。儘管此語法初看起來可能很是陌生,但它有本身的用途。

級聯表達式就是多個lambda表達式的組合,這裏涉及到一個高階函數的概念,所謂高階函數就是一個能夠返回函數的函數,以下示例:

// 實現了 x + y 的級聯表達式
Function<Integer, Function<Integer, Integer>> function1 = x -> y -> x + y;
System.out.println("計算結果爲: " + function1.apply(2).apply(3));  // 計算結果爲: 5

這裏的 y -&gt; x + y 是做爲一個函數返回給上一級表達式,因此第一級表達式的輸出是 y -&gt; 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

若是你們想學習以上路線內容,在此我向你們推薦一個架構學習交流羣。交流學習羣號874811168 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

函數柯里化的目的是將函數標準化,函數可靈活組合,方便統一處理等,例如我能夠在循環裏只須要調用同一個方法,而不須要調用另外的方法就能實現一個數組內元素的求和計算,代碼以下:

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
            }
        }
    }
}

級聯表達式和柯里化通常在實際開發中並非很常見,因此對其概念稍有理解便可,這裏只是簡單帶過,若對其感興趣的能夠查閱相關資料。

 

出處:http://blog.51cto.com/zero01/2284379

相關文章
相關標籤/搜索