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

開篇

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

Java8的新特性概覽

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

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

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

簡單說一說函數式編程

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

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

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的訂單ide

//選出已經支付完成的訂單
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方法爲例子,詳細解釋方法引用的使用

相關文章
相關標籤/搜索