深入探尋JAVA8 part1:函數式編程與Lambda表達式

開篇

在很久之前粗略的看了一遍《Java8 實戰》。客觀的來,說這是一本寫的非常好的書,它由淺入深的講解了JAVA8的新特性以及這些新特性所解決的問題。最近重新拾起這本書並且對書中的內容進行深入的挖掘和沉澱。接下來的一段時間將會結合這本書,以及我自己閱讀JDK8源碼的心路歷程,來深入的分析JAVA8是如何支持這麼多新的特性的,以及這些特性是如何讓Java8成爲JAVA歷史上一個具有里程碑性質的版本。

Java8的新特性概覽

在這個系列博客的開篇,結合Java8實戰中的內容,先簡單列舉一下JAVA8中比較重要的幾個新特性:

  1. 函數式編程與Lambda表達式
  2. Stram流處理
  3. Optional解決空指針噩夢
  4. 異步問題解決方案CompletableFuture
  5. 顛覆Date的時間解決方案

後面將針對每個專題發博進行詳細的說明。

簡單說一說函數式編程

函數式編程的概念並非這兩年才涌現出來,這篇文章用一種通俗易懂的方式對函數式編程的理念進行講解。顧名思義,函數式編程的核心是函數。函數在編程語言中的映射爲方法,函數中的參數被映射爲傳入方法的參數,函數的返回結果被映射爲方法的返回值。但是函數式編程的思想中,對函數的定義更加嚴苛,比如參數只能被賦值一次,即參數必須爲final類型,在整個函數的聲明週期中不能對參數進行修改。這個思想在如今看來是不可理喻的,因爲這意味着任何參數的狀態都不能發生變更。

那麼函數式編程是如何解決狀態變更的問題呢?它是通過函數來實現的。下面給了一個例子:

String reverse(String arg) {
    if(arg.length == 0) {
        return arg;
    }
    else {
        return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1);
    }
}

對字符串arg進行倒置並不會修改arg本身,而是會返回一個全新的值。它完全符合函數式編程的思想,因爲在整個函數的生命週期中,函數中的每一個變量都沒有發生修改。這種不變行在如今稱爲Immutable思想,它極大的減少了函數的副作用。這一特性使得它對單元測試,調試以及編髮編程極度友好。因此在面向對象思想已經成爲共識的時代,被重新提上歷史的舞臺。

但是,編程式思想並不只是侷限於此,它強調的不是將所有的變量聲明爲final,而是將這種可重入的代碼塊在整個程序中自由的傳遞和複用。JAVA中是通過對象的傳遞來實現的。舉個例子,假如現在有一個篩選訂單的功能,需要對訂單從不同的維度進行篩選,比如選出所有已經支付完成的訂單,或是選出所有實付金額大於100的訂單。

簡化的訂單模型如下所示:

public class Order{

    private String orderId;

    //實付金額
    private long actualFee;
    
    //訂單創建時間    
    private Date createTime;
    
    private boolean isPaid
}

接着寫兩段過濾邏輯分別實現選出已經支付完成的訂單,和所有實付金額大於100的訂單

//選出已經支付完成的訂單
public List<Order> filterPaidOrder(List<Order> orders) {
    List<Order> paidOrders = new ArrayList<>();
    for(Order order : orders) {
        if(order.isPaid()) {
            paidOrders.add(order);
        }
    }
    return paidOrdres;
}

//選出實付金額大於100的訂單
public List<Order> filterByFee(List<Order> orders) {
    List<Order> resultOrders = new ArrayList<>();
    for(Order order : orders) {
        if(order.getActualFee()>100) {
            resultOrders.add(order);
        }
    }
    return resultOrders;
}

可以看到,上面出現了大量的重複代碼,明顯的違背了DRY(Dont Repeat Yourself)原則,可以先通過模板模式將判斷邏輯用抽象方法的形式抽取出來,交給具體的子類來實現。代碼如下:

public abstract class OrderFilter{
    
    public List<Order> filter(List<Order> orders) {
        List<Order> resultOrders = new ArrayList<>();
        for(Order order : orders) {
            //調用抽象方法
            if(isWantedOrder(order)) {
                resultOrders.add(order);
            }
        }
        return resultOrders;
    }
    abstract boolean isWantedOrder(Order o);
}

public abstract class PaidOrderFilter extends OrderFilter{
    //重寫過濾的判斷邏輯
    boolean isWantedOrder(Order o){
        return o.isPaid();
    }
}

public abstract class FeeOrderFilter extends OrderFilter{
    //重寫過濾的判斷邏輯
    boolean isWantedOrder(Order o){
        return o.getActualFee() > 100;
    }
}

但是,繼承本身會帶來類和類之間比較重的耦合,而可重入函數的傳遞則解決了這個問題。代碼如下:

public interface OrderFilter{
    boolean isWantedOrder(Order o);
}

public List<Order> filter(List<Order> orders, OrderFilter orderFilter) {
    List<Order> resultOrders = new ArrayList<>();
    for(Order order : orders) {
        if(orderFilter.isWantedOrder(o)) {
            resultOrders.add(order);
        }
    }
    return resultOrders;
}

//過濾出已經支付的訂單
filter(orders, new OrderFilter(){
    @Override
    public boolean isWantedOrder(Order o){
        return o.isPaid();
    }
})

通過這種方式,filter方法基本上處於穩定,只需要自定義傳入的訂單過濾器即可。但是,在當代對可讀性和減少重複代碼的極致追求下,重構到這種程度依然不能讓具有代碼潔癖的程序員們滿意,於是Lambda表達式應運而生。

Lambda表達式

Java8中的Lambda表達式和Lambda Calculus並不是一個概念,因此所有被Lambda計算傷害過的小夥伴千萬不要恐懼。在Java8中,它更加類似於匿名類的代碼糖,從而極大的提高代碼的可讀性(大部分場景),靈活性和簡潔性。Lambda表達式的基本結構如下:

(parameters) -> expression
(parameters) -> {expression}

它其實就是函數的一個簡化版本,括號中的parameters會填入這個函數的參數類型,在expression中會填入具體執行的語句。如果沒有大括號,則expression只允許填入一條語句,且會根據Lambda表達是的上下文,自動補全return語句。舉幾個具體的例子:

() -> "hello world" 類似於 String methodName(){return "hello world";}
(int i, int j) -> i > j 類似於 Boolean compare(){ return i > j; } 

因此Lambda表達式本質上就是對匿名函數的一種快捷展示。而上面的代碼使用lambda表達式還可以繼續重構如下:

//標記該接口爲函數式接口,要求只能有一個待實現的函數聲明
@FuncationalInterface
public interface OrderFilter{
    boolean isWantedOrder(Order o);
}

public List<Order> filter(List<Order> orders, OrderFilter orderFilter) {
    List<Order> resultOrders = new ArrayList<>();
    for(Order order : orders) {
        if(orderFilter.isWantedOrder(o)) {
            resultOrders.add(order);
        }
    }
    return resultOrders;
}

//過濾出已經支付的訂單
filter(orders, (Order o) -> o.isPaid());
filter(orders, (Order o) -> o.getActualFee() > 100);

Lambda表達式本身還有一些約定,以及進一步簡化的空間,這點各位筆者可以通過這篇文章自行再去了解。

Lambda的靈活性還體現在同樣的Lambda表達式可以賦值給不同的函數式接口,代碼如下:

@FuncationalInterface
public interface Runnable{
    void run();
}

@FuncationalInterface
public interface AnotherInterface{
    void doSomething();
}

Runnable r = () -> System.out.println("hello world");
AnotherInterface a = () -> System.out.println("hello world");

那麼編譯器是如何解析Lambda表達式的呢?它其實是根據上下文推斷該Lambda表達式該映射到什麼函數式接口上的。就以上文的filter方法爲例子,它傳入的函數式接口爲OrderFilter,其中函數的定義爲傳入Order並返回Boolean值。編譯器就會根據這個上下文來判斷Lambda表達式是否符合函數式接口的要求,如果符合,則將其映射到該函數式接口上。

Lambda表達式中的局部變量和異常

Lambda表達式作爲匿名類的語法糖,它的特性和匿名類保持一致。即如果Lambda表達式要拋出一個非檢查性異常(Unchecked Error), 則需要在函數式接口中顯示的聲明出來。如下:

@FuncationalInterface
public interface AnotherInterface{
    void doSomething() throws UncheckedException;
}

除此以外,還有一個場景是需要在Lambda表達式中引用外部的變量。外部的變量包括局部變量,實例變量和靜態變量。其中,只允許對實例變量和靜態變量進行修改,所有的被引用的局部變量都必須顯性的或是隱形的聲明爲final。代碼如下:

//實例變量
int fieldVariable;

public void someMethod() {
    //局部變量
    int localVariable = 0;
    
    //不允許修改局部變量
    Runnable r1 = () -> localVariable++;
    
    //可以修改實例變量
    Runnable r2 = () -> fieldVarialbe++;
    
    //不允許,因爲被Lambda表達式引用的局部變量必須顯式或隱式的聲明爲局部變量
    Runnable r3 = () -> System.out.println(localVariable);
    localVariable++;
}

之所以有這樣的約定,是因爲局部變量是保存於棧上的,保存於棧上意味着一旦該方法執行完畢,棧中的局部變量就會被彈出並回收。這裏也隱式的表明局部變量其實是約束於當前線程使用的。此時如果Lambda表達式是傳遞到其它線程中執行的,比如上文中創建的Runnable對象傳遞給線程池執行,則會出現訪問的局部變量已經被回收的異常場景。而實例變量和靜態變量則不同,二者是保存在堆中的,本身就具有多線程共享的特性

方法的引用

方法的引用證明程序員對代碼的潔癖已經到了無法搶救的程度。JAVA8中提出的方法引用的思想允許我們將方法定義傳遞給各個函數。比如如果要使用System.out.print方法,則可以傳入System.out::println。方法的引用主要有三種場景:

  1. 指向靜態的方法的引用。如Integer中的靜態方法parseInt,可以通過Integer::parseInt來引用
  2. 指向任意類型實例方法的方法引用。如list.sort((s1, s2)->s1.compareToIgnoreCase(s2));, 可以修改爲list.sort(String::compareToIgnorecase),即滿足arg0.someMethod(restArgs)語法
  3. 指向現有對象實例的方法引用,如類ClassA有一個實例classA,並且有一個方法someMethod,則可以通過classA::someMethod進行方法引用。
  4. 構造函數引用ClassName::new。對於有參數的構造函數,則需要結合已有的函數式接口進行引用。

下期預告

下一篇文章將會結合JAVA8中預定義的一些FunctionalInterface的源碼來介紹如何使用這些函數式接口幫助我們編程。

  1. Consumer
  2. Supplier
  3. Predicate
  4. Function

並且會以JAVA8的comparing方法爲例子,詳細解釋方法引用的使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章