首頁/ 汽車/ 正文

Java 中那些繞不開的內建介面 -- 函數語言程式設計和 Java 的內建函式式介面

Java 在最開始是不支援函數語言程式設計的,想來也好理解,因為在 Java 中類 Class 才是第一等公民,這就導致在 Java 中實現程式設計不是件那麼容易的事兒,不過雖然難,但是結果我們也已經知道了,在 Java 8 這個大版本里為了支援函數語言程式設計,Java 引入了很多特重要特性,咱們在前面幾篇文章中,分別學習了其中的 Lambda 表示式和 Stream API 裡的各種流操作,今天這篇文章我們再來梳理一下 Java 內建給我們提供的函式式介面。

本文大綱如下:

Java 中那些繞不開的內建介面 -- 函數語言程式設計和 Java 的內建函式式介面

Java 根據常用需求場景的用例,抽象出了幾個內建的函式式介面給開發者使用,比如Function、 Supplier 等等,Stream 中各種操作方法的引數或者是返回值型別往往就是這些內建的函式式介面。

比如 Stream 中 map 操作方法的引數型別就是 Function

Stream map(Function<? super T, ? extends R> mapper);複製程式碼

那為什麼我們在平時使用 Stream 操作的 map 方法時,從來沒有見過宣告這個型別的引數呢?大家可以回顧一下我們 Stream API 操作那一篇文章裡使用 map 方法的例子,比如下面這個透過 map 方法把流中的每個元素轉換成大寫的例子。

List list = new ArrayList();Stream stream = list。stream();Stream streamMapped = stream。map((value) -> value。toUpperCase());複製程式碼

map 方法的引數直接是一個 Lambada 表示式:

(value) -> value。toUpperCase()複製程式碼

這個Lambda 表示式就是Function介面的實現。

函式式介面的載體通常是 Lambda 表示式,透過 Lambda 表示式,編譯器會根據 Lambda 表示式的引數和返回值推斷出其實現的是哪個函式式介面。使用 Lambda 表示式實現介面,我們不必像匿名內部類那樣——指明類要實現的介面,所以像 Stream 操作中雖然引數或者返回值型別很多都是 Java 的內建函式式介面,但是我們並沒有顯示的使用匿名類實現它們。

雖然Lambda 表示式使用起來很方便,不過這也從側面造成了咋一看到那些 Java 內建的函式式介面型別時,我們會有點迷惑“這貨是啥?這貨又是啥?”的感覺。

下面我們先說一下函數語言程式設計、Java 的函式式介面、Lambda 為什麼只能實現函式式介面這幾個問題,把這些東西搞清楚了再梳理 Java 內建提供了哪些函式式介面。

函數語言程式設計

函數語言程式設計中包含以下兩個關鍵的概念:

函式是第一等公民

函式要滿足一下約束 函式的返回值僅取決於傳遞給函式的輸入引數。 函式的執行沒有副作用。

即使我們在寫程式的時候沒有一直遵循所有這些規則,但仍然可以從使用函數語言程式設計思想編寫程式中獲益良多。

接下來,我們來看一下這兩個關鍵概念再 Java 函式程式設計中的落地。

函式是一等公民

在函數語言程式設計正規化中,函式是語言中的第一等公民。這意味著可以建立函式的“例項”,對函式例項的變數引用,就像對字串、Map 或任何其他物件的引用一樣。函式也可以作為引數傳遞給其他函式。

在 Java 中,函式顯然不是第一等公民,類才是。所以 Java 才引入 Lambda 表示式,這個語法糖從表現層上讓 Java 擁有了函式,讓函式可以作為變數的引用、方法的引數等等。為啥說是從表現層呢?因為實際上在編譯的時候 Java 編譯器還是會把 Lambda 表示式編譯成類。

純函式

函式程式設計中,有個純函式(Pure Function)的概念,如果一個函式滿足以下條件,才是純函式:

該函式的執行沒有副作用。

函式的返回值僅取決於傳遞給函式的輸入引數。

下面是一個 Java 中的純函式(方法)示例

public class ObjectWithPureFunction{ public int sum(int a, int b) { return a + b; }}複製程式碼

上面這個sum()方法的返回值僅取決於其輸入引數,而且sum()是沒有副作用的,它不會在任何地方修改函式之外的任何狀態(變數)。

相反,這裡是一個非純函式的例子:

public class ObjectWithNonPureFunction{ private int value = 0; public int add(int nextValue) { this。value += nextValue; return this。value; }}複製程式碼

add()方法使用成員變數value來計算其返回值,並且它還修改了value成員變數的狀態,這代表它有副作用,這兩個條件都導致add方法不是一個純函式

正如我們看到的,函數語言程式設計並不是解決所有問題的銀彈。尤其是“函式是沒有副作用的”這個原則就使得在一些場景下很難使用函數語言程式設計,比如要寫入資料庫的場景,寫入資料庫就算是一個副作用。所以,我們需要做的是瞭解函數語言程式設計擅長解決哪些問題,把它用在正確的地方。

函式式介面

Java中的函式式介面在 Lambda 表示式那篇文章裡提到過,這裡再詳細說說。

函式式介面是隻有一個抽象方法的介面

(抽象方法即未實現方法體的方法)。一個 Interface 介面中可以有多個方法,其中預設方法和靜態方法都自帶實現,但是

只要介面中有且僅有一個方法沒有被實現,那麼這個介面就可以被看做是一個函式式介面

下面這個介面只定義了一個抽象方法,顯然它是一個函式式介面:

public interface MyInterface { public void run();}複製程式碼

下面這個介面中,定義了多個方法,不過它也是一個函式式介面:

public interface MyInterface2 { public void run(); public default void doIt() { System。out。println(“doing it”); } public static void doItStatically() { System。out。println(“doing it statically”); }}複製程式碼

因為doIt方法在介面中定義了預設實現,靜態方法也有實現,介面中只有一個抽象方法run沒有提供實現,所以它滿足函式式介面的要求。

這裡要注意,

如果介面中有多個方法沒有被實現,那麼介面將不再是函式式介面,因此也就沒辦法用 Java 的 Lambda 表示式實現介面了

編譯器會根據 Lambda 表示式的引數和返回值型別推斷出其實現的抽象方法,進而推斷出其實現的介面,如果一個介面有多個抽象方法,顯然是沒辦法用 Lambda 表示式實現該介面的。

@FunctionalInterface 註解

這裡擴充一個標註介面是函式式介面的註解@FunctionalInterface

@FunctionalInterface // 標明介面為函式式介面public interface MyInterface { public void run(); //抽象方法}複製程式碼

一旦使用了該註解標註介面,Java 的編譯器將會強制檢查該介面是否滿足函式式介面的要求:“

確實有且僅有一個抽象方法

”,否則將會報錯。

需要注意的是,即使不使用該註解,只要一個介面滿足函式式介面的要求,那它仍然是一個函式式介面,使用起來都一樣。該註解只起到——標記介面指示編譯器對其進行檢查的作用。

Java 內建的函式式介面

Java 語言內建了一組為常見場景的用例設計的函式式介面,這樣我們就不必每次用到Lambda 表示式、Stream 操作時先建立函式式介面了,Java 的介面本身也支援泛型型別,所以基本上 Java 內建的函式式介面就能滿足我們平時程式設計的需求,我自己在開發專案時,印象裡很少見過有人自定義函式式介面。

在接下來的部分中,我們詳細介紹下 Java 內建為我們提供了的函式式介面。

Function

Function介面(全限定名:java。util。function。Function)是Java中最核心的函式式介面。 Function 介面表示一個接受單個引數並返回單個值的函式(方法)。以下是 Function 介面定義的:

@FunctionalInterfacepublic interface Function { R apply(T t); default Function compose(Function<? super V, ? extends T> before) { Objects。requireNonNull(before); return (V v) -> apply(before。apply(v)); } default Function andThen(Function<? super R, ? extends V> after) { Objects。requireNonNull(after); return (T t) -> after。apply(apply(t)); } static Function identity() { return t -> t; }複製程式碼

Function介面本身只包含一個需要實現的抽象方法apply,其他幾個方法都已在介面中提供了實現,這正好符合上面我們講的函式式介面的定義:“有且僅有一個抽象方法的介面”。

Function 介面中的其他三個方法中compse、andThen 這兩個方法用於函數語言程式設計的組合呼叫,identity用於返回呼叫實體物件本身,我們之前在把物件 List 轉換為 Map 的內容中提到過,可以回看前面講 List 的文章複習。

Function介面用Java 的類這麼實現

public class AddThree implements Function { @Override public Long apply(Long aLong) { return aLong + 3; } public static void main(String[] args) { Function adder = new AddThree(); Long result = adder。apply(4L); System。out。println(“result = ” + result); }}複製程式碼

不過現實中沒有這麼用的,前面說過 Lambda 表示式是搭配函式式介面使用的,用Lambda表示式實現上Function 介面只需要一行,上面那個例子用 Lambda 實現的形式是:

Function adder = (value) -> value + 3;Long resultLambda = adder。apply(8L);System。out。println(“resultLambda = ” + resultLambda);複製程式碼

是不是簡潔了很多。後面的介面示例統一用 Lambda 表示式舉例,不再用類實現佔用太多篇幅。

Function介面的常見應用是 Stream API 中的 map 操作方法,該方法的引數型別是Function介面,表示引數是一個“接收一個引數,並返回一個值的函式”。

Stream map(Function<? super T, ? extends R> mapper);複製程式碼

所以我們在程式碼裡常會見到這樣使用 map 操作:

stream。map((value) -> value。toUpperCase())複製程式碼

Predicate

Predicate 介面 (全限定名:java。util。function。Predicate)表示一個接收單個引數,並返回布林值 true 或 false 的函式。以下是 Predicate 功能介面定義:

public interface Predicate { boolean test(T t);}複製程式碼

Predicate 接口裡還有幾個提供了預設實現的方法,用於支援函式組合等功能,這裡不再贅述。 用 Lambda 表示式實現 Predicate 介面的形式如下:

Predicate predicate = (value) -> value != null;複製程式碼

Stream API 中的 filter 過濾操作,接收的就是一個實現了 Predicate 介面的引數。

Stream filter(Predicate<? super T> predicate);複製程式碼

寫程式碼時,會經常見到這樣編寫的 filter 操作:

Stream longStringsStream = stream。filter((value) -> { // 元素長度大於等於3,返回true,會被保留在 filter 產生的新流中。 return value。length() >= 3;});複製程式碼

Supplier

Supplier 介面(java。util。function。Supplier),表示提供某種值的函式。其定義如下:

@FunctionalInterfacepublic interface Supplier { T get();}複製程式碼

Supplier介面也可以被認為是工廠介面,它產生一個泛型結果。與 Function 不同的是,Supplier 不接受引數。

Supplier supplier = () -> new Integer((int) (Math。random() * 1000D));複製程式碼

上面這個 Lambda 表示式的 Supplier 實現,用於返回一個新的 Integer 例項,其隨機值介於 0 到 1000 之間。

Consume

Consumer 介面(java。util。function。Consume)表示一個函式,該函式接收一個引數,但是不返回任何值。

@FunctionalInterfacepublic interface Consumer { void accept(T t);}複製程式碼

Consumer 介面常用於表示:要在一個輸入引數上執行的操作,比如下面這個用Lambda 表示式實現的 Consumer,它將作為引數傳遞給它的value變數的值列印到System。out標準輸出中。

Consumer consumer = (value) -> System。out。println(value);複製程式碼

Stream API 中的 forEach、peek 操作方法的引數就是 Consumer 介面型別的。

Stream peek(Consumer<? super T> action);void forEach(Consumer<? super T> action);複製程式碼

比如,Stream API 中的 forEach 操作,會像下面這樣使用 Consume 介面的實現

Stream stream = stringList。stream();// 下面是Lambda 的簡寫形式// 完整形式為:value -> System。out。println(value);stream。forEach(System。out::println);複製程式碼

Optional

最後再介紹一下 Optional 介面,Optional 介面並不是一個函式式介面,這裡介紹它主要是因為它經常在一些 Stream 操作中出現,作為操作的返回值型別,所以趁著學習函數語言程式設計的契機也學習一下它。

Optional 介面是預防NullPointerException的好工具,它是一個簡單的容器,其值可以是 null 或非 null。比如一個可能返回一個非空結果的方法,方法在有些情況下返回值,有些情況不滿足返回條件返回空值,這種情況下使用 Optional 介面作為返回型別,比直接無值時返回 Null 要更安全。 接下來我們看看 Optional 怎麼使用:

// of 方法用於構建一個 Optional 容器Optional optional = Optional。of(“bam”);// 判斷值是否為空optional。isPresent(); // true// 取出值,如果不存在直接取會丟擲異常optional。get(); // “bam”// 取值,值為空時返回 orElse 提供的預設值optional。orElse(“fallback”); // “bam”// 如果只存在,執行ifPresent引數中指定的方法optional。ifPresent((s) -> System。out。println(s。charAt(0)));// “b”複製程式碼

Stream 操作中像 findAny、 findFirst這樣的操作方法都會返回一個 Optional 容器,意味著結果 Stream 可能為空,因此沒有返回任何元素。我們可以透過 Optional 的 isPresent() 方法檢查是否找到了元素。

總結

本文從函數語言程式設計思想、原則到 Java 對函數語言程式設計的實現,給大家梳理了一遍,建議大家重點理解 Java 的函式式介面,為什麼 Lambda 表示式對函式式介面的實現,以及 Java 內建提供的覆蓋了大多數函數語言程式設計應用場景的函式式介面。

至此

用 Java 程式設計那些繞不開的介面

這個子系列的文章已經更新完畢,感興趣的請

作者:kevinyan

連結:https://juejin。cn/post/7163826133228584991

著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關文章

頂部