1. 程式人生 > >初識函數語言程式設計與函式式介面(一)

初識函數語言程式設計與函式式介面(一)

目前大部分的 JAVA8 的教程一上來就給大家將 Lambda 表示式,方法引用,給大家搞得雲裡霧裡,最終導致 JAVA8 學習的不是特別透徹。我們先來了解一下什麼時候能用 Lambda 表示式,然後在探究怎麼用 Lambda 表示式。

從函數語言程式設計開始

前一章節我們說過,JAVA8 其實是 Java 像其他語言或者一些優秀的框架學習的結果。函數語言程式設計這個概念提出的非常早,有很多語言都是支援函數語言程式設計的。JAVA8 中也對函數語言程式設計做了支援。我們下面要介紹的函式式介面等概念都是圍繞函數語言程式設計而生的。

  • 什麼是函數語言程式設計?

    簡單說,函數語言程式設計是一種程式設計正規化

    (programming paradigm),也就是如何編寫程式的方法論。這句話是百度百科中給我們的解答,如果單看這句話,我們可能根本理解不了到底什麼是函數語言程式設計

    我們不妨使用我們最熟悉的面向物件程式設計(指令式程式設計)來類比一下,讓大家能對函數語言程式設計有一個簡單的概念。

    指令式程式設計是針對於計算機硬體的,我們寫的每一句話都是一個底層的硬體指令。函數語言程式設計不是我們中的函式不是我們平時在 Java 中編寫的函式或者方法,它是一種針對於數學的概念,可以將其理解為一個表示式或者公式,或者理解為資料之間的轉換關係。

    我們說 JAVA8 以前不支援函數語言程式設計,那麼有哪些具體的體現哪?

    1. Java 中最重要的部分是類和物件,沒有類和物件,我們所要實現的功能也無從談起。我們所定義的方法或者函式是必須依託於類或者物件來存在的,引用現在比較流行的說法,類和物件是一等公民,方法或者叫函式是二等公民。
    2. 在 JAVA8 之前,我們無法將一個函式作為引數傳遞給一個方法,也無法宣告一個返回函式的方法。
  • JAVA8 對函數語言程式設計做了哪些支援?

    JAVA8 通過函式式介面和 Lambda 表示式為我們引入了函數語言程式設計的概念,從而使函式在 Java 中也變為了一等公民。

  • 函數語言程式設計有些好處?

    在 Java 中,我們所謂的變數都是可以進行狀態變化的,比如一個“學生”物件的年齡屬性是可以按照我們的需求去更改的。而函數語言程式設計中變數則於 Java 中完全不同,就是數學中定義的變數,變數的值也是不可更改的。函數語言程式設計雖然最終也是被編譯成機器指令去執行,但是它從思想方面為我們帶來了許多好處,比如,函式的結果不會因為呼叫的時間和位置的變化而變化,函式的執行是獨立於外部環境的。最大的好處就是不可變。因為不可變性,我們在進行多執行緒操作時就不用額外的加鎖,也不用關心各種執行緒安全問題,非常適合用來處理併發問題。這和 Java 中不可變物件概念相同(Immutable Object)。

我們通過例項來看看 JAVA8 引入函數語言程式設計後到底給我們的程式設計帶來了哪些好處。

我們的專案中經常使用各種條件會對集合中的元素進行過濾。比如我們定義一個學生類,我們可以根據學生的分數來過濾選出一部分學生,通過性別過濾出一部分學生,通過其他條件過濾出一部分學生。

來看看我們最開始的寫法:

public static List<Student> selectByMark(List<Student> lists){
  List<Student> result = new ArrayList<>();
  for(Student s:lists){
    if(s.getMark()>60){
      result.add(s);
    }
  }
  return result;
}

public static List<Student> selectBySex(List<Student> lists){
  List<Student> result = new ArrayList<>();
  for(Student s:lists){
    if(s.getSex().equals("Male")){
      result.add(s);
    }
  }
  return result;
}

......
  
.......  selectByName()

按照這種方式寫我們可能會根據不斷變化的需求寫出許多像上面一樣的程式碼。這樣的程式碼不僅冗餘而且無趣,相當於在浪費程式設計師的時間。

如果你是一個有些經驗的程式設計師,你肯定不會使用上面的方法去寫,而是使用設計模式中的策略模式來實現。

//策略介面,一個過濾器
public interface Filter{
  boolean filter(Student s);
}
//不同的實現類,即對學生的過濾方法
public class MarkFilter implements Filter{
  @Override
  public boolean filter(Student s){
    return s.getMark() > 60;
  }
}

public class SexFilter implements Filter{
  @Override
  public boolean filter(Student s){
    return s.getSex().equals("Male");
  }
}

.........
  
  
//給使用者提供的靜態方法
public static List<Student> select(List<Student> lists, Filter f){
    List<Student> result = new ArrayList<>();
    for(Student s:lists){
      if(f.filter(s)){
         lists.add(s);
      }
    }
    return result;
}

使用了上面的策略模式後,其實並沒有好多少,我們只不過是把原來的靜態方法換成了類。而在實際程式設計過程中我們經常會使用匿名內部類來實現(虛擬碼),這樣寫的好處是,我們只定義了一個框架或者行為方式,具體怎麼實現交給呼叫者去實現。我們定義行為,使用者決定細節。

//呼叫者需要過濾的時候需要寫的程式碼,Filter 不需要實現類,只需要在呼叫時 new 一個匿名內部類就可以了。
List<Student> lists = new ArrayList<>();
//lists.add();
List<Student> res = select(lists, new Filter() {
  @Override
  public boolean filter(Student s) {
    return s.getMark()>60;
  }
});

最後給出使用函數語言程式設計的方式的解決方案,使用 Lambda 表示式的方式,它比匿名內部類節省更多空間,程式碼也更整潔,更易於理解(虛擬碼)。

//重點
List<Student> result = select(lists,m->m.getMark()>60);

原來面向物件程式設計時,我們的方法是用來處理邏輯的。而現在邏輯或者演算法是呼叫時動態提供的。現在看方法只能看到一種通用或者巨集觀的邏輯。提供了一種更高層次的抽象化。函數語言程式設計,寫的更少,做的更多。

原來我們使用面向物件程式設計時,當我們的函式寫完了之後,函式的功能就已經固定下來了,我們想實現找到偶數就要寫一個方法,找到奇數要寫一個方法,找到大於5的數字要寫一個方法。函數語言程式設計中,方法的實現是一種抽象的概念,具體實現要使用者或者呼叫端來實現的。

函式式介面

瞭解了函數語言程式設計的概念之後,我們來看看 JAVA8 為了支援函數語言程式設計做了哪些努力。

首先,在 Java 中函數語言程式設計的體現就是 Lambda 表示式(下一章節我們會具體介紹,Lambada 表示式首先它是一個表示式,這就是函數語言程式設計的概念,它就是匿名函式)。

但是 Lambda 表示式在 Java 中不是能隨處用的,如果將這個表示式放入到 Java 語言中那?Java 定義了一個承載 Lambda 表示式的介面,因為這個介面是為函數語言程式設計設計的介面,所以叫函式式介面

下面我們來幫大家理解一下函式式介面:

回顧匿名內部類:

new Thread(new Runnable() {
  @Override
  public void run() {
    System.out.println("this is a thread");
  }
}).start();

這是我們比較熟悉的啟動一個執行緒的方法,Thread 類裡面接收一個 Runnalbe 介面引數,我們需要定義一個介面實現類傳進來,這裡直接使用匿名內部類來實現。

這裡傳入 Runable 介面的意義是什麼那?或者說設計者為什麼要這樣設計那?

這樣設計是在啟動一個執行緒時,設計者只希望我們把執行緒具體要執行的任務傳進去就好了,其他的功能都已經封裝好了,使用者並不需要關心。

這個介面封裝了一個行為,不用傳入引數,不用返回引數的行為,行為的具體實現也就是執行緒具體執行的任務需要呼叫者傳入。這裡就引入了一個概念,函式式介面是對某些行為的抽象,一個函式式介面只能有一個行為。

類似的是 Callable 介面,他也是執行緒的實現方式,它封裝的行為是不需要傳入引數,但需要一個返回值的行為。

通過 Runnable 介面引出函式式介面:

JAVA8 在這個介面帶來的改變是在上面加了一個 FunctionalInterface 函式式介面的註解,這證明 Runnable 已經是一個函式式介面了,裡面只有一個不用傳入引數,也不用返回引數的抽象方法。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

函式式介面的定義:

通過函式式介面的定義我們看不出什麼,到底什麼是函式式介面,我們可以看原始碼裡面的英文註釋,這裡我們直接給出定義。

  • 函式式介面首先是一個介面。
  • 函式式介面必須只有一個抽象函式。
  • 除了一個抽象方法之外,函式式介面允許有預設方法和靜態方法(介面中的預設方法和靜態方法也是 JAVA8 引入的,下面的章節我們會詳細講解)。
  • java.lang.Object 的抽象方法是不計算在內的,就是在函式式介面中可以定義許多 java.lang.Object 的抽象方法。
  • 如果我們的介面滿足上面的條件但是沒有@FunctionalInterface註解依然會被編譯器認作函式式介面。

看完定義之後我們在回頭來看 Callable 介面和 Runaable 介面,他們完全滿足函式式介面的定義。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

如何建立介面的例項那?

既然是介面,我們在使用的使用一定需要建立介面的例項。函式式介面有三種建立方法,Lambda 表示式方法引用構造方法的方式來實現,下面的章節會具體講解 Lambda 表示式和方法引用的使用。

我們反過來想,Lambda 表示式的作用就是用來建立函式式介面的例項。

**Lambda 表示式的實現:

我們下面的章節會詳細講,大家可以先有一個簡單的印象。

//還是上面那個啟動一個執行緒的例子,因為 Runnable 是一個函式式介面,所以我們可以使用 Lambda 表示式來建立介面例項。
new Thread(() -> System.out.println("this is a thread")).start();

方法引用的實現:

這個可能大家理解會非常困難,我們下面的章節會詳細講,大家可以先有一個簡單的印象。

//Lambda 表示式的寫法
list.forEach(item-> System.out.println(item));
//方法引用,相當於呼叫 PrintStream 類的 println 方法
list.forEach(System.out::println);

構造方法的實現就是我們平常使用的匿名內部類或者實現介面,這裡就不詳細介紹了。

通過這一章節,我們已經瞭解了 JAVA8 引入的函數語言程式設計以及具體怎樣實現。並且理清了函數語言程式設計的體現 Lambda 表示式與函式式介面的關係,並且介紹了函式式介面的定義和構建方法,還引出了方法引用這個概念