天天看點

Java8之Lambda文法行為參數化函數式使用函數式接口方法引用複合Lambda表達式結尾

我又回來了

已經很久沒有寫過部落格了。

因為前段時間我感到自己之前寫的部落格毫無深度,像是一個産品的說明書,而這樣的部落格會将一項技術看作黑匣子——你不需要知道這個技術的原理或源代碼的實作邏輯,你隻需要按照接口的說明調接口去完成你要實作的功能就夠了。

這樣将某一項IT技術看作黑匣子,以簡單的利用它的功能實作自己想要的功能為目标的想法在實際工作中是合理的,因為實際工作是講究效率的,沒有那麼多時間讓你撥開面紗悟其核心。但是,作為一名開發者,豈能止步于此?至少在工作之外的時間中,将那個“黑匣子”打開看看它的内部,在下次使用它的時候,讓這個“黑匣子”在你的手裡可以由自己完全定制,并以這個流行的技術為鑒,擇其善者而從之,則其不善者而改之。如此,不斷進步。

我一直想寫有深度的技術部落格,但我可能一直都對深度這個概念有些誤解,到底何為深度?之前的我一直将深度這個詞和複雜聯系在一起,認為隻有将複雜的東西吃透了,才叫深度;但是我現在才意識到,自己對複雜這個概念也有誤解——到底何為複雜?Java基礎簡單嗎?肯定會有不少人質疑:Java基礎不簡單嗎?幾個基本變量、控制語句、類、接口、集合等等,都是很簡單的東西。但是如果我問你,這些東西都是怎麼實作的?應該會有很多人啞口無言。我認為簡單與複雜的關系,一方面是相對的,即之前看上去複雜的東西掌握了之後就會看上去簡單;另一方面就是深度,即之前看上去簡單的東西追求深度之後就會看上去複雜。而簡單的東西是怎麼實作的,就是深度。

我将重拾部落格,寫一些有深度的技術部落格,這樣,也好在這個碎片化的時代,讓自己的技術不那麼碎片化,不停留于片面,養成深度思考的習慣。

關于Java的新功能的部落格,網絡上還真的不少,但是不夠系統也不夠全面,我将這些東西整理後寫成部落格,為想要對Java8的新特性學習的朋友提供參考,共同學習。

我将連續寫Java8相比之前版本的新功能,由此成為一個系列——話說Java8還真是一個呈上啟下的版本。

行為參數化

所謂行為參數化,就是将行為作為參數傳入函數。

比如下面這個接口(通過作者篩選圖書):

List<Book> selectBookdByAuth(String authName);           

我們想要擷取“路遙”的書,我們需要将“路遙”作為參數傳入

selectBookdByAuth

這個函數。

但是,如果我又想根據出版社篩選圖書呢?那就又要建立一個根據出版社篩選圖書的接口了。那如果我又有需求了呢?要根據圖書類别篩選圖書……

有沒有什麼辦法把“我想要根據什麼篩選圖書”作為一個參數呢?這樣我們隻需要一個篩選圖書的接口就可以完成各種篩選圖書的功能了。

在這裡,“我想要根據什麼篩選圖書”是就是一個行為,将“我想要根據什麼篩選圖書”作為參數傳入相應的函數,就被稱為

行為參數化

我們先舉個例子(篩選圖書的接口):

List<Book> selectBook(Predicate<Book> predicate);           

我們暫且不用思考

Predicate

是什麼,當我們建立了這個接口之後,我們就可以将行為作為參數傳入該函數了。

例如我想要擷取以“路遙”為作者的圖書:

List<Book> books = selectBook(boook -> book.getAuth().equals("路遙"));           

就可以擷取到想要的結果了,再例如我想要擷取以“人民郵電出版社”為出版社的圖書:

List<Book> books = selectBook(boook -> book.getPress().equals("人民郵電出版社"));           

函數式

函數式接口

函數式接口就是隻定義一個抽象方法的接口。

例如:

/**
 * 運算函數式接口.
 *
 * @author zuoyu
 *
 **/
public interface Operation {

  /**
   * 用于運算兩個int類型的數
   * @param a - 參數一
   * @param b - 參數二
   * @return - 結果
   */
  int opera(int a, int b);
}           

對這個接口的簡單使用:

@Test
  public void operationTest() {
    Operation operation = (a, b) -> a + b;
    int result = operation.opera(1, 1);
    System.out.println(result);
  }    //result:2           

函數描述符

函數式接口的抽象方法的簽名(參數、傳回值)就是Lambda表達式的簽名。其中抽象方法就是函數描述符。

例如上面的

int opera(int a, int b);

可以接受的Lambda表達式為

(a, b) -> a + b

,那麼其中的參數

a

和參數

b

都是int類型,

a + b

結果也為int,那麼這個函數的簽名就是

(int, int) -> int

再打個比方,例如剛才篩選圖書的函數式接口

Predicate

public interface Predicate<T> {
    boolean test(T t);
}           

那麼它的函數簽名就是

T -> boolean

,意味着我們将類型

T

的對象作為參數傳入,傳回

boolean

類型。隻有符合函數描述符的Lambda表達式才能作為參數傳入相應的函數。

使用函數式接口

Java API已經為我們提供了很常用的函數式接口以及其函數描述符,當這些函數式接口不夠我們使用的時候我們也可以自己建立。(一定要記住,一個函數式本身并沒有什麼意義,其意義在于其函數簽名。)

拿幾個函數式接口細說一下:

Predicate<T>

public interface Predicate<T> {
    boolean test(T t);
}           

java.util.function.Predicate<T>

接口定義了一個名為

test

的抽象方法,它接受泛型

T

對象,并傳回一個

boolean

。在你需要一個涉及到類型T的布爾表達式時,就可以使用這個函數式接口。

例如你可以寫一個過濾

List

集合元素的方法,将這個函數式接口作為參數:

/**
   * Predicate<T>接口
   *
   * @param list - 集合
   * @param predicate - 篩選條件
   * @param <T> - 類型
   * @return 符合要求的結果
   */
  public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> results = new ArrayList<>();
    list.forEach(t -> {
      if (predicate.test(t)) {
        results.add(t);
      }
    });
    return results;
  }           

這是一個通用的對

List

集合進行元素過濾的方法:

  • 從一個圖書List集合裡擷取以“圖靈出版社”為出版社的圖書:
    List<Book> pressBooks = filter(books, boook -> book.getPress().equals("圖靈出版社"));           
    • 注:在這裡,泛型

      T

      就是

      Book

      類型
  • 從一個蘋果集合内擷取重量大于1.2的蘋果:
    List<Apple> weightApples = filter(apples, apple -> apple.getwWight() > 1.2D);           
    • 注:在這裡泛型

      T

      Apple

Consumer<T>

public interface Consumer<T> {
    void accept(T t);
}           

java.util.function.Consumer<T>

定義了一個名叫

accept

的抽象方法,它接受泛型T的對象,沒有傳回

(void)

。如果你需要通路類型為

T

的對象,并對其執行某些操作,就可以使用這個接口。

例如我要對一個

List

集合内的每個元素執行行為操作:

/**
   * 對集合進行任何行為操作
   */
  public static <T> void action(List<T> list, Consumer<T> consumer) {
    for (T t : list) {
      consumer.accept(t);
    }
  }           

List

集合進行元素行為操作的方法:

  • 對圖書集合内的元素進行列印:
    action(books, book -> System.out.println(book.toString()));           
    • T

      Book

  • 将蘋果集合内的每個蘋果的重量增加0.5:
    action(apples, apple -> {
          apple.setWeight(apple.getWeight() + 1.00D);
        });           
    • T

      Apple

Function

public interface Function<T, R> {
    R apply(T t);
}           

java.util.function.Function<T, R>

接口定義了一個叫做

apply

的方法,它接受一個泛型

T

的對象,并傳回一個泛型

R

的對象。如果你需要定一個Lambda用于輸入對象的資訊映射到輸出,你便可以利用這個接口來完成。

List

集合内所有對象的某一個屬性進行提取:

/**
   * 對集合内對象的某一進制素進行提取
   */
  public static <T, R> List<R> function(List<T> list, Function<T, R> function) {
    List<R> rList = new ArrayList<>();
    for (T t : list) {
      R r = function.apply(t);
      rList.add(r);
    }
    return rList;
  }           

List

集合的對象操作的方法:

  • 對圖書集合内的所有書的書名進行提取:
    List<String> booksName = function(books, book -> book.getName());           
    • T

      Book

      類型,泛型

      R

      String

  • 對蘋果集合内的所有蘋果的重量進行提取,并增加0.5:
    List<Double> appleWeight = function(apples, apple ->
            apple.getWeight() + 0.5D
        );           
    • T

      Apple

      R

      Double

Function的原始類型化

衆所周知,在Java中的泛型隻能綁定到引用類型上,不能綁定在原始類型上。在Java中有一個将原始類型轉換為對應的引用類型的機制——裝箱;相反的将引用類型轉換為原始類型的機制——拆箱。這一系列操作都是Java自動完成的,但是這個機制是要付出代價的——

裝箱:把原始資料類型包裹起來,并儲存到堆裡。是以,裝箱後需要更多的記憶體來儲存,并需要額外的記憶體用來搜尋并擷取被包裹的原始值。

Java8為避免這個現象對其所提供的函數式接口帶來了一個專門版本,在資料的輸入和輸出都是原始類型時避免自動裝箱的操作,以此節省記憶體。

例如我要根據下标擷取一個蘋果對象:

IntFunction<Apple> appleIntFunction = (int i) -> apples.get(i);
Apple apple = appleIntFunction.apply(2);           

我們來看一下

IntFunction

接口:

public interface IntFunction<R> {
    R apply(int value);
}           

java.util.function.IntFunction<R>

接口的參數為

int

原始類型,傳回一個

R

類型,與我們想要完成相同功能的

java.util.function.Function<T, R>

接口相比較,避免了必須傳入

Integer

類型的自動裝箱操作。

再比如我要擷取一個

double

随機數的2倍數:

IntToDoubleFunction intToDoubleFunction = (int i) -> Math.random() * i;
double random = intToDoubleFunction.applyAsDouble(2);           

我們看一下

IntToDoubleFunction

public interface IntToDoubleFunction {
    double applyAsDouble(int value);
}           

上面的功能如果我們使用

java.util.function.Function<T, R>

接口來實作這個功能,需要将接口寫成

Function<Integer, Double>

,輸入

Integer

類型并輸出

Double

類型;相對于

java.util.function.IntToDoubleFunction

接口,輸入

int

類型輸出

double

類型,省去了自動裝箱。

Function的變種函數:

  • IntFunction<R>

  • IntToDoubleFunction

  • IntToLongFunction

  • LongFunction<R>

  • LongToDoubleFunction

  • LongToIntFunction

  • DoubleFunction<R>

  • ToIntFunction<T>

  • ToDoubleFunction<T>

  • ToLongFunction<T>

其他函數式接口

JavaAPI自帶的函數接口還有不少,為的是我們日常使用。當然也有不能滿足我們需求的時候,比如我要輸入三個參數,那就需要自己定義接口了。還是那句話,函數接口本身并無意義,其意義在于其函數簽名(參數數量與傳回類型)。

JavaAPI自帶的函數式接口(不一一細說了):

Predicate<T>

T

-> boolean

Consumer<T>

T

->

void

Function<T, R>

T

R

Supplier<T>

(

void

) ->

T

UnaryOperator<T>

T

T

BinaryOperator<T>

T

,

T

T

BiPredicate<L, R>

L

R

boolean

BiConsumer<T, U>

T

U

void

BitFunction<T, U, R>

T

U

R

  • 注:以上函數式接口都有其原始類型化的變種。

方法引用

方法引用可以重複的使用現有的方法定義,可以将其了解為Lambda的簡化方式。

例如,我要根據蘋果的重量對其從小到大排序:

apples.sort((apple1, apple2) -> apple1.getWeight().compareTo(apple2.getWeight()));           

上面是Lambda表達式的寫法,那麼換作方法引用的方式可以簡化代碼:

apples.sort(Comparator.comparing(Apple::getWeight));           

相對于Lambda表達式的寫法,方法引用的寫法在這裡意思更加清晰直覺。

方法引用主要有三類:

  1. 指向靜态方法的方法引用(例:

    Integer

    parseInt()

    方法可以直接寫成

    Integer::parseInt

    )。
  2. 指向任意類型執行個體方法的方法引用(例:

    String

    length()

    String::length

  3. 指向現有對象的執行個體方法的方法引用(例:假設有一個局部變量

    book

    指向用于存放

    Book

    類型的對象,它有一個執行個體方法

    getName()

    ,你可以直接寫成

    book::getName

複合Lambda表達式

Java8API中的函數式接口都提供了複合方法,即通過這些方法把多個簡單的Lambda表達式複合成複雜的表達式。

謂詞複合

謂詞接口包含三個方法:

negate

(否定)、

and

(并且)、

or

(或)。

這幾個謂詞類似布爾語句之間的關系,舉個例子:

  • 比如我現在有三種對圖書的篩選方案:
    • 篩選出以“路遙”為作者的圖書的邏輯接口:
    Predicate<Books> bookPredicateByAuth = book -> book.getAuth().equals("路遙"));           
    • 篩選出以“人民郵電出版社”為出版社的圖書的邏輯接口:
    Predicate<Books> bookPredicateByPress = boook -> book.getPress().equals("人民郵電出版社"));           
    • 篩選出印刷時間在2010年之後的圖書的邏輯接口:
    Predicate<Books> bookPredicateByPrintingData = boook -> book.getPrintingData.before(new SimpleDateFormat("yyyy-MM-dd").parse("2010-01-01"););           
  • 那麼我現在想要篩選出以“路遙”為作者的,印刷時間在2010年之後的圖書,不要以“人民郵電出版社”出版的,可以該邏輯接口這麼寫:
    Predicate<Books> bookPredicate = bookPredicateByAuth.and(bookPredicateByPrintingData).negate(bookPredicateByPress);           
  • 進行篩選:
    List<Book> books = filter(books, bookPredicate));           

函數複合

函數複合的接口方法有兩個:

andThen

compose

andThen

方法會傳回一個函數,它先對輸入應用一個給定的函數,再對輸出應用另一個函數。

compose

方法把給定的函數作用

compose

的參數裡面給的那個函數,然後再把函數本身用于結果。

這兩個函數的作用就是函數套函數,舉個例子:

  • 我現在有兩個函數:
    • 第一個函數為兩數相加的函數:
    Function<Integer, Integer> add = x -> x + 1;           
    • 第二個為兩數相乘的函數:
    Function<Integer, Integer> multiply = x -> x * 2;           
  • 如果想要先算加法再算乘法,達到

    multiply(add())

    的效果(隻是可以這樣了解,實際不是這樣的結構):
    Function<Integer, Integer> function = add.andThen(multiply);
    function.apply(1);  // result: 4           
  • 如果想要先算乘法再算加法,達到

    add(multiply)

    Function<Integer, Integer> function = add.compose(multiply);
    function.apply(1);  // result: 3           

結尾

轉載請表明出處

下期咱們聊一聊Java8的stream流

繼續閱讀