java8-lambada表達式和函數式編程淺析

一:lambada表達式

提及java8的新特性,不少人第一反應都是lambada表達式和流式的API,那麼到底什麼是lambada表達式,爲何要引入lambada表達式,以及引入lambada表達式爲
java8帶來了哪些改變呢,本文接來下會一一討論。

1. Definition: 什麼是lambada表達式?

直白的先讓你們有個第一印象,在java8以前,在建立一個線程的時候,咱們可能這麼寫:css

Runnable r = new Runnable() {
    @Override
  public void run() {
        System.out.println("Hello");
  }
};

這段代碼使用了匿名類,Runnable 是一個接口,這裏new 了一個類實現了 Runnable 接口,而後重寫了 run方法,run方法沒有參數,方法體也只有一行打印語句。
這段代碼咱們其實只關心中間打印的語句,其餘都是多餘的。
java8後,咱們採用lambada表達式後,咱們就能夠簡寫爲:html

Runnable r = () -> System.out.println("Hello");

Lambda 表達式是一種匿名函數(對 Java 而言這並不徹底正確,但如今姑且這麼認爲),簡單地說,它是沒有聲明的方法,也即沒有訪問修飾符、返回值聲明和名字。java

你能夠將其想作一種速記,在你須要使用某個方法的地方寫上它。當某個方法只使用一次,並且定義很簡短,使用這種速記替代之尤爲有效,這樣,你就沒必要在類中費力寫聲明與方法了。正則表達式

2. lambaba表達式的語法

lambda 表達式的語法格式以下:算法

(parameters) -> expression

(parameters) ->{ statements; }
  • 一個 Lambda 表達式能夠有零個或多個參數
  • 參數的類型既能夠明確聲明,也能夠根據上下文來推斷。例如:(int a)與(a)效果相同
  • 全部參數需包含在圓括號內,參數之間用逗號相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)
  • 空圓括號表明參數集爲空。例如:() -> 42
  • 當只有一個參數,且其類型可推導時,圓括號()可省略。例如:a -> return a*a
  • Lambda 表達式的主體可包含零條或多條語句
  • 若是 Lambda 表達式的主體只有一條語句,花括號{}可省略。匿名函數的返回類型與該主體表達式一致
  • 若是 Lambda 表達式的主體包含一條以上語句,則表達式必須包含在花括號{}中(造成代碼塊)。匿名函數的返回類型與代碼塊的返回類型一致,若沒有返回則爲空

如下是lambada表達式的一些例子:sql

(int a, int b) -> {  return a + b; }

() -> System.out.println("Hello World");

(String s) -> { System.out.println(s); }

() -> 42

() -> { return 3.1415 };

3. 爲何java 會須要lambada 表達式?

Java 是一流的面嚮對象語言,除了部分簡單數據類型,Java 中的一切都是對象,即便數組也是一種對象,每一個類建立的實例也是對象。
在 Java 中定義的函數或方法不可能徹底獨立,也不能將方法做爲參數或返回一個方法給實例。express

在Java的面向對象的世界裏面,「抽象」是對數據的抽象,而「函數式編程」是對行爲進行抽象,在現實世界中,數據和行爲並存,程序也是如此。
因此java8中lambada表達式的出現也就彌補java在對行爲進行抽象方面的缺失。編程

二:函數式接口

一、Definition: 什麼是函數式接口?

函數式接口(Functional Interface)是Java 8對一類特殊類型的接口的稱呼。 這類接口只定義了惟一的抽象方法的接口(除了隱含的Object對象的公共方法),
所以最開始也就作SAM類型的接口(Single Abstract Method)。數組

首次看到這個概念的時候,有些迷茫。由於接口中的方法都是public abstract 的(即使省略掉這兩個關鍵字也是ok的,接口中每個方法也是隱式抽象的,接口中的方法會被隱式的指定爲 public abstract(只能是 public abstract,其餘修飾符都會報錯)),那麼上面的定義就變成了:只有一個方法聲明的接口就是函數式接口。
可是實際上在代碼中看到的函數式接口有包含一個方法的,也有包含多個方法的,這就讓我迷茫了。
例以下面的兩個函數式接口:Runnable 和 Consummer:多線程

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();
}

java.util.function.Consummer:

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

最後才瞭解了緣由在於:函數式接口中除了那個抽象方法外還能夠包含靜態方法和默認方法。

  • Java 8之前的規範中接口中不容許定義靜態方法。 靜態方法只能在類中定義。 Java 8中能夠定義靜態方法。

一個或者多個靜態方法不會影響SAM接口成爲函數式接口。

  • Java 8中容許接口實現方法, 而不是簡單的聲明, 這些方法叫作默認方法,使用特殊的關鍵字default。

由於默認方法不是抽象方法,因此不影響咱們判斷一個接口是不是函數式接口。

參考連接: Java 8函數式接口functional interface的祕密

二、Brief introduction: 函數式接口簡介

爲何會單單從接口中定義出此類接口呢?
緣由是在Java Lambda的實現中, 開發組不想再爲Lambda表達式單獨定義一種特殊的Structural函數類型,
稱之爲箭頭類型(arrow type), 依然想採用Java既有的類型系統(class, interface, method等), 緣由是增長一個結構化的函數類型會增長函數類型的複雜性,
破壞既有的Java類型,並對成千上萬的Java類庫形成嚴重的影響。 權衡利弊, 所以最終仍是利用SAM 接口做爲 Lambda表達式的目標類型。

函數式接口表明的一種契約, 一種對某個特定函數類型的契約。 在它出現的地方,實際指望一個符合契約要求的函數。
Lambda表達式不能脫離上下文而存在,它必需要有一個明確的目標類型,而這個目標類型就是某個函數式接口。
換句話說:什麼地方能夠用lambada表達式呢? 全部須要FI (Functional Interface)實例的地方,均可以使用lambada表達式。

Java 不會強制要求你使用@FunctionalInterface註解來標記你的接口是函數式接口, 然而,做爲API做者,
你可能傾向使用@FunctionalInterface指明特定的接口爲函數式接口, 這只是一個設計上的考慮, 可讓用戶很明顯的知道一個接口是函數式接口。

三、Why: 爲何會有函數式接口?

提及函數式接口的原由就不得不提lambada表達式,提及lambada表達式的原由就不得不說函數式編程,函數式編程相比命令式編程有諸多的優勢:(最突出的優勢有2點:
引用透明-->函數的運行不依賴於外部的狀態;沒有反作用-->函數的運行不改變外部的狀態),java8爲了使用函數式編程的優勢,從而就使用了lambada表達式,從而
就定義了一種規範和約束,這個規範和約束就是函數式接口。
關於函數式編程的一些基礎概念會在下面將。(注意:函數式編程和函數式接口是不一樣的概念。函數式編程是一種編程範式,與之在同一個維度的有:命令式編程、邏輯式編程)

四、What: java8裏面的函數式接口都有哪些?

JDK8 以前已經有的函數式接口

  • java.lang.Runnable
  • java.util.concurrent.callable
  • java.awt.event.ActionListener

這裏就列舉這幾個,還有其餘的暫時就不列舉了。

JDK8 新定義的函數式接口

接口 參數 返回類型 描述
Predicate<T> T boolean 用於判別一個對象。好比求一我的是否爲男性
Consumer<T> T void 用於接收一個對象進行處理但沒有返回,好比接收一我的並打印他的名字
Function<T, R> T R 轉換一個對象爲不一樣類型的對象
Supplier<T> None T 提供一個對象
UnaryOperator<T> T T 接收對象並返回同類型的對象
BinaryOperator<T> (T, T) T 接收兩個同類型的對象,並返回一個原類型對象
  • 其中 Cosumer 與 Supplier 對應,一個是消費者,一個是提供者。
  • Predicate 用於判斷對象是否符合某個條件,常常被用來過濾對象。
  • Function 是將一個對象轉換爲另外一個對象,好比說要裝箱或者拆箱某個對象。
  • UnaryOperator 接收和返回同類型對象,通常用於對對象修改屬性。BinaryOperator 則能夠理解爲合併對象。

若是之前接觸過一些其餘 Java 框架,好比 Google Guava,可能已經使用過這些接口,對這些東西並不陌生。

三:函數式編程

一、編程範式

  1. 命令式編程(Imperative Programming): 專一於」如何去作」,這樣無論」作什麼」,都會按照你的命令去作。解決某一問題的具體算法實現。
  2. 函數式編程(Functional Programming):把運算過程儘可能寫成一系列嵌套的函數調用。
  3. 邏輯式編程(Logical Programming):它設定答案須符合的規則來解決問題,而非設定步驟來解決問題。過程是事實+規則=結果。
關於這個問題也有一些爭議,有人把函數式歸結爲聲明式的子集,還有一些別的七七八八的東西,這裏就不作闡述了。
聲明式編程:專一於」作什麼」而不是」如何去作」。在更高層面寫代碼,更關心的是目標,而不是底層算法實現的過程。
如, css, 正則表達式,sql 語句,html,xml…

二、函數式編程簡介

相比於命令式編程關心解決問題的步驟,函數式編程是面向數學的抽象,關心數據(代數結構)之間的映射關係。函數式編程將計算描述爲一種表達式求值。

在狹義上,函數式編程意味着沒有可變變量,賦值,循環和其餘的命令式控制結構。即,純函數式編程語言。

  • Pure Lisp, XSLT, XPath, XQuery, FP
  • Haskell (without I/O Monad or UnsafPerformIO)

在廣義上,函數式編程意味着專一於函數

  • Lisp, Scheme, Racket, Clojure
  • SML, Ocaml, F#
  • Haskell (full language)
  • Scala
  • Smalltalk, Ruby

三、「函數」概念的不一樣!

函數式編程中的函數,這個術語不是指命令式編程中的函數,而是指數學中的函數,即自變量的映射(一種東西和另外一種東西之間的對應關係)。
也就是說,一個函數的值僅決定於函數參數的值,不依賴其餘狀態。

在函數式語言中,函數被稱爲一等函數(First-class function),與其餘數據類型同樣,做爲一等公民,處於平等地位,能夠在任何地方定義,在函數內或函數外;
能夠賦值給其餘變量;能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。

純函數是這樣一種函數,即相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。

  • 不依賴外部狀態
  • 不改變外部狀態

四、函數式編程的好處

函數式的最主要的好處主要是不變性帶來的:

4.1 引用透明(Referential transparency)

引用透明(Referential transparency),指的是函數的運行不依賴於外部變量或」狀態」,只依賴於輸入的參數,任什麼時候候只要參數相同,
引用函數所獲得的返回值老是相同的。

其餘類型的語言,函數的返回值每每與系統狀態有關,不一樣的狀態之下,返回值是不同的。這就叫」引用不透明」,很不利於觀察和理解程序的行爲。

沒有可變的狀態,函數就是引用透明(Referential transparency)

4.2 沒有反作用(No Side Effect)

反作用(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。

函數式編程強調沒有」反作用」,意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。

函數即不依賴外部的狀態也不修改外部的狀態,函數調用的結果不依賴調用的時間和位置,這樣寫的代碼容易進行推理,不容易出錯。這使得單元測試和調試都更容易。

還有一個好處是,因爲函數式語言是面向數學的抽象,更接近人的語言,而不是機器語言,代碼會比較簡潔,也更容易被理解。

4.3 無鎖併發

沒有反作用使得函數式編程各個獨立的部分的執行順序能夠隨意打亂,(多個線程之間)不共享狀態,不會形成資源爭用(Race condition),
也就不須要用鎖來保護可變狀態,也就不會出現死鎖,這樣能夠更好地進行無鎖(lock-free)的併發操做。

尤爲是在對稱多處理器(SMP)架構下可以更好地利用多個處理器(核)提供的並行處理能力。

4.4 惰性求值

惰性求值(lazy evaluation,也稱做call-by-need)是這樣一種技術:是在將表達式賦值給變量(或稱做綁定)時並不計算表達式的值,
而在變量第一次被使用時才進行計算。

這樣就能夠經過避免沒必要要的求值提高性能。

總而言之,函數式編程因爲其不依賴、不改變外部狀態的基本特性,衍生出了不少其餘的有點,尤爲簡化了多線程的複雜性,提高了高併發編程的可靠性。

參考資源


版權聲明:本文爲博主原創文章,遵循 CC 4.0 by-sa 版權協議,轉載請附上原文出處連接和本聲明。posted from openWrite

相關文章
相關標籤/搜索