自從J8開始,對於開發JAVAEE應用的工程師而言,函式式介面會常常接觸,某種程度上有點不可繞過。
這是因為在絕大部分企業中都會使用Spring來開發JAVAEE,而Spring在它的實現中越來越多地使用上函數語言程式設計。
如果我們閱讀它的原始碼,函數語言程式設計是繞不過去的。
函數語言程式設計有其好處,這個好處就是工程上的:讓程式碼看起來簡潔;如果你熟練一點,還是能夠節省一些時間的。
就具體而言,函數語言程式設計用起來和JS的郎打表示式差不多,不過後者更加隨意的(因為不需要考慮效能和穩定性,相對後端而言)。
要了解java的函數語言程式設計,需要掌握以下內容:
- 函式式介面
- 流api(即stream api)
- 函數語言程式設計優缺點和適用的業務場景
- JAVA中函數語言程式設計的未來瞻望
需要特別注意的是,在java函數語言程式設計中,幾個重點類/介面所在的包:
1.java.util.function
2.java.util.stream
而FunctionalInterface介面則位於java的核心的核心:java.lang,獲得了類似Integer,String等基礎類的核心地位。
看起來JCP似乎要給予FunctionalInterface一個重要的位置和期許。
一、函式式介面
1.1、定義
新增的核心型別-函式介面註解
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FunctionalInterface {}
函式式介面的實現是java自行實現的。和我們在spring中定義各種註解不太一樣。
看這個註解,知道三個重要資訊:
Documented -- 會在javaDoc之類工具生成的文件中展示
Retention- 只有執行時才會生效
Target- 只能用於物件(具體是介面)
如前, FunctionalInterface位於java.lang下,而不是位於java.lang.annotation。
函式式介面定義
一種特有的介面,必須具備兩個特點:
1.必須在介面上新增@FunctionalInterface註解,表明這是一個函式介面
2.在介面體內只能定義一個public abstract型別的方法。
@FunctionalInterface public interface Isort { public int add(int a,int b); }
3.雖然只能定義一個公共抽象方法,但其實還可以定義其它亂七八糟的東西,但只有以下是允許的:only public, private, abstract, default, static and strictfp are permitted
換言之,可以定義私有方法,預設方法等,但只要保證一個原則即可:只能有一個公共的抽象方法
package study.base.oop.interfaces.functional; import java.util.Random; @FunctionalInterface public interface Isort { /** * 1.允許定義公共靜態屬性 * 2.允許預設方法 * 3.允許私有方法(私有,靜態私有) */ public static int SORT_TYPE_ASC=1; public static int SORT_TYPE_DESC=2; //私有靜態 private static int rand() { return (new Random()).nextInt(100); } //私有方法 private void testPrivate() { System.out.printf("生車一個隨機數%d\n", rand()); } //預設方法 default void doSomething(int a,int b) { testPrivate(); System.out.printf("兩個引數分別是%d,%d",a,b); } //公共抽象方法 -- 這是函式式介面對外暴露的唯一方法 public int add(int a,int b); }
驗證程式碼見後端有關章節。
java自身從J8之後,建立了一個很重要的型別
@FunctionalInterface public interface Function<T, R> { }
並有大量基於這個介面的實現,某種形式上,Function類似於Object在類中地位。
除了Funciton,還推出了相關一堆的型別,以便支援流式API,例如:
Predicate,Supplier,Consumer...
概念有點小多,需要專門另開文章闡述。
1.2、簡單實現
java目前提供了5種方式,用於實現函式式介面:
1.傳統類
2.朗打表示式
3.匿名函式
4.方法引用
5.建構函式
其中2~5是重點,目的都是為了節省編碼+實現流式API。
為了演示這幾種實現方式,我寫了一個相對完整的例子,具體如下(為了節省篇幅,放在一起,不再列出包等資訊),其中最重要的函式式介面Isort 見前文。
//實現類 public class Sort { public int add(int a, int b) { int total= a+b; System.out.println("雖然不是函式式實現,但是方法同約定方法一樣的結果:"+total); return total; } } //用於演示基於建構函式引用 @FunctionalInterface public interface IFace { public Face show(int a,int b); } public class Face { int a; int b; public Face(int a,int b) { this.a=a; this.b=b; } public void write() { System.out.println(a+b); } }
測試程式碼:
public class StudentSortImpl implements Isort { @Override public int add(int a, int b) { int total = a + b; System.out.println(total); this.doSomething(a,b); return total; } public static void main(String[] args) { // 1.0 函式式介面的傳統實現-類實現 System.out.println("1.函式式介面的實現方式一:實現類"); Isort sort = new StudentSortImpl(); sort.add(10, 20); // 函式式介面的實現二-朗打方式 System.out.println("2.函式式介面的實現方式一:朗打表示式"); // 2.1 有返回的情況下,注意不要return語句,只能用於單個語句的 // 如果只有一個引數,可以省掉->前的小括弧 // 如果有返回值,某種情況下,也可以省略掉後面的花括弧{} // 有 return的時候 // a->a*10 // (a)->{return a*10} 要花括弧就需要加return // (a,b)->a+b // (a,b)->{return a+b;} Isort sort2 = (a, b) -> a + b; Isort sort3 = (a, b) -> { return a * 10 + b; }; // 2.2 有沒有多條語句都可以使用 ->{}的方式 Isort sort4 = (a, b) -> { a += 10; return a + b; }; int a=10; int b=45; int total=sort2.add(a, b)+sort3.add(a, b)+sort4.add(a, b); System.out.println("總數="+total); // 3 使用 new+匿名函式的方式來實現 System.out.println("3.函式式介面的實現方式一:匿名類"); Isort sort5 = new Isort() { @Override public int add(int a, int b) { int total = a * a + b; System.out.println(total); return total; } }; sort5.add(8, 2); // 4.0 基於方法引用-利用已有的方法,該方法必須結構同介面的方式一致 // 在下例中,從另外一個類例項中應用,而該例項僅僅是實現了方法,但是沒有實現介面 // 可以推測:編譯的時候,透過反射或者某些方式實現的。具體要看編譯後的位元組碼 System.out.println("4.函式式介面的實現方式一:方法引用"); Sort otherClassSort=new Sort(); Isort methodSort = otherClassSort::add; methodSort.add(90, 90); // 5.0 基於建構函式 // 這種方式下,要求建構函式返回的物件型別同函式介面的返回一致即可,當然引數也要一致 System.out.println("5.函式式介面的實現方式一:建構函式引用"); IFace conSort=Face::new; Face face=conSort.show(10, 90); face.write(); //小結:基於方法和基於建構函式的實現,應該僅僅是為了stream和函式式服務,和朗打沒有什麼關係 //這個最主要是為了編寫一個看起來簡介的表示式。 } }
函式式介面簡化了介面,是一種極其特殊用途的介面,以便方便實現流式操作等功能。
二、流式API
如果光有函式式介面,那麼距離函式式變成還有一點距離:流式API
從java8開始,java新增一個java.util.stream.Stream<T>介面,該介面約定了流式操作所需要包含的各種實現抽象定義。
有了流式API,那麼透過連續的點號和流式操作可以實現看起來相對高效,相對簡潔的程式碼。
注意:這裡強調了“相對“,這是因為現有函數語言程式設計(包括流式API)都是有特定使用場景,至少在目前階段,它的實現未必是四海皆准,這是在JAVAEE應用中
看起來還不錯,還是具有不錯的工程價值。
限於篇幅,流式API不是本篇的重點,這裡簡單介紹流式API是什麼,如何定義。
2.1、Stream介面及其基本方法
java.util.stream.Stream<T>
以下是J17中JAVADoc的內容:
Stream<T>是一個支援順序和並行聚合操作的元素序列。以下示例展示瞭如何使用Stream和IntStream執行聚合操作: java int sum = widgets.stream() .filter(w -> w.getColor() == RED) .mapToInt(w -> w.getWeight()) .sum(); 在這個示例中,widgets是一個Collection<Widget>。我們透過Collection.stream()方法建立了一個Widget物件的流,然後使用filter方法過濾出只有紅色的Widget,接著將其轉換為一個包含每個紅色Widget重量的int值流。最後,對這個流進行求和操作以得到總重量。 除了Stream(一個物件引用的流)之外,還有針對原始型別的專門化,如IntStream、LongStream和DoubleStream,所有這些都被稱為“流”,並遵守此處描述的特徵和限制。
為了執行計算,流操作被組合成一個流管道。流管道由一個源(可能是陣列、集合、生成器函式、I/O通道等)、零個或多箇中間操作(將流轉換為另一個流,如Stream.filter(Predicate))和一個終端操作(產生結果或副作用,如Stream.count()或Stream.forEach(Consumer))組成。流是惰性的;只有在啟動終端操作時才會對源資料進行計算,並且只有在需要時才會消耗源元素。 流實現被允許在最佳化結果計算方面有很大的自由度。例如,如果流實現可以證明從流管道中省略某些操作(或整個階段)不會影響計算結果,那麼它可以自由地省略這些操作(或整個階段),以及省略行為引數的呼叫。這意味著除非另有說明(如由終端操作forEach和forEachOrdered指定),否則行為引數的副作用可能不會總是被執行,因此不應依賴它們。 集合和流雖然表面上有一些相似之處,但它們有不同的目標。集合主要關注於其元素的高效管理和訪問。相比之下,流不提供直接訪問或操作其元素的方式,而是關注於宣告性地描述其源和將對其源執行的聚合計算操作。然而,如果提供的流操作不提供所需的功能,可以使用iterator()和spliterator()操作進行受控遍歷。 像上面的“widgets”示例這樣的流管道可以被視為對流源的查詢。除非源被明確設計為支援併發修改(如ConcurrentHashMap),否則在查詢流源時修改它可能會導致不可預測或錯誤的行為。 大多數流操作接受描述使用者指定行為的引數,如上面mapToInt中傳遞的lambda表示式w -> w.getWeight()。為了保持正確的行為,這些行為引數: 必須是非干擾性的(它們不修改流源); 在大多數情況下必須是無狀態的(它們的結果不應依賴於在執行流管道期間可能更改的任何狀態)。 這樣的引數始終是功能介面(如java.util.function.Function)的例項,並且經常是lambda表示式或方法引用。除非另有說明,否則這些引數必須非空。 一個流應該只被操作(呼叫中間或終端流操作)一次。這排除了例如“分叉”流的情況,即同一個源同時供給兩個或多個管道,或對同一流進行多次遍歷。如果流實現檢測到流正在被重用,它可能會丟擲IllegalStateException。但是,由於某些流操作可能會返回接收器本身而不是新的流物件,因此可能無法在所有情況下檢測到重用。 流具有close()方法並實現AutoCloseable介面。在流關閉後對其進行操作將丟擲IllegalStateException。大多數流例項在使用後實際上不需要關閉,因為它們是由集合、陣列或生成函式支援的,這些不需要特殊的資源管理。通常,只有其源是I/O通道(如Files.lines(Path)返回的流)的流才需要關閉。如果流需要關閉,則必須在try-with-resources語句或類似的控制結構中將其作為資源開啟,以確保在操作完成後及時關閉。 流管道可以順序執行或並行執行。這是流的屬性。流在建立時可以選擇順序執行或並行執行(例如,Collection.stream()建立順序流,而Collection.parallelStream()建立並行流)。可以透過sequential()或parallel()方法修改執行模式的選擇,並透過isParallel()方法查詢。
個人覺得,官方的JavaDoc已經把流api說的比較清楚了(部分翻譯可能不是很恰當),上文可以歸納為幾點:
1.流是一個支援順序和並行聚合操作的元素序列。
2.流管道-由一個源(可能是陣列、集合、生成器函式、I/O通道等)、零個或多箇中間操作(將流轉換為另一個流,如Stream.filter(Predicate))和一個終端操作(產生結果或副作用,如Stream.count()或Stream.forEach(Consumer))組成
3.流是惰性(lazy)-只有在啟動終端操作時才會對源資料進行計算,並且只有在需要時才會消耗源元素。參考了另外一些資料,可以概述為:流管道的操作是比較智慧高效,知道中止、知道最佳化,並非每個中間都會執行
注:lazy“惰性“的翻譯可能值得商榷,也是翻譯為"延遲"更好一些。這個含義大體同spring中用於bean上@lazy註解,行為上也是相似的。
4.其它一些注意事項:一個流應該只被操作(呼叫中間或終端流操作)一次;只有其源是I/O通道(如Files.lines(Path)返回的流)的流才需要關閉;
5.流引數-應該必須是非干擾性的,在大多數情況下必須是無狀態的
2.2、流式API的優缺點
這個待完善,因為本人對於流式API的體會並不是那麼深刻,所以只能給出大部分人認可的優缺點。
2.2.1、優點
1.程式碼看起來更加簡潔 - 可以算一個
2.高效-這個有待商榷-因為有人專門研究了這個東西。 不過如前所述,在大部分的JAVAEE開發中,只要秉著專業技能編寫,使用流式API處理資料還是一個不錯的主意。
對於程式設計師的主要影響是兩個:能以較小的程式碼實現併發(並非是工程師自己寫java程式碼實現,而是編譯器和JVM暗地裡實現了); 能夠實現可接受的“高效能“。
關於stream效能這個事,有許多研究參考,雖然不算非常嚴謹,但大體可用:
JDK8 Stream 資料流效率分析 -- https://www.cnblogs.com/jpfss/p/11262231.html
Java8 Stream 資料流,大資料量下的效能效率怎麼樣?-- https://blog.csdn.net/2401_84048338/article/details/138879395
3.靈活可擴充套件 -- 操作可以靈活組合、容易新增中間或者終端操作
2.2.2、缺點
1.不好除錯 - 這是實話-即使idea之類的工具有針對朗打表示式的除錯,但是針對對於流的除錯還不算友好
2.並行流效能可能不如預期 - 如前。並行流並不總是比順序流快。並行化的開銷以及任務劃分的複雜性可能導致效能下降;在處理小資料集或資料集分割不均勻時,並行流可能效率不高
還有一些,但個人認為不屬於流所有特有的。因為當你選擇流的時候,意味著就要承受的對應的缺陷,例如開啟並行就要耗費更多資源。
除非這個缺陷是非常顯著的、難於忽視的,才值得單列。
因為流式api的特點,所以在日常工作中,我對於使用流式api並不是很熱衷,並警告有關人不要濫用。
但在有些業務場景也會考慮用:
a.這個業務對效能要求不高
b.一般屬於sql無法完成的,例如轉換
有些同事老是把網際網路開發規則放到非網際網路行業。似乎阿里之類的都是對的,並熱衷於把資料撈到jvm中,做各種集聚操作(通常是流)。
那樣做其實至少有兩大壞處:浪費資料庫資源(閒置),在集聚上sql做得比java好多了;很可能會撐爆應用伺服器
這種行為,在非網際網路行業,或者說併發不是那麼大的情況下,並不值得提倡,而應該批評。
正確的做法是應該儘量利用資料庫的能力,在聚集方面傳統的rdbms做得比java好太多了,也比絕大部分程式設計師基於流式API編寫的好得多。
三、函數語言程式設計適用業務場景
java是一個OOP程式語言,JAVA函數語言程式設計有什麼用?
個人認為的核心效果:可以接受的效果,新增新特性(完成升級JAVA的KPI)
事實上,“函數語言程式設計“本身我並沒有找到官定的(後面會繼續找找)。
就我個人理解而言,JAVA的所為函數語言程式設計就是:利用函式式介面+流式api+朗打表示式 建立有關功能。
雖然函數語言程式設計具有所為的一些好處,但考慮到java的現狀,函式式變成還是隻能侷限在幾個方面,前文已經提到,此處不再贅述。
由於我個人的習慣和企業業務特點,所以基本沒有考慮使用函數語言程式設計,主要用到的就是Stream的map功能。
由於個人的習慣,很多場景如果僅僅是簡單處理,還是更喜歡使用for,foreach。藉助於ai編碼,這些會很快完成,即使沒有也是很快的。
個人把函數語言程式設計當作一個可有可無的東西,堅持程序導向和麵向物件才是真正的核心!!!
四、函數語言程式設計的未來瞻望
如果JCP不能把JAVA變成JS,我個人覺得函數語言程式設計應該適可而止,優缺點列出了。
作為JAVAEE工程師,只有在特定的條件下,才會考慮用用,或者僅僅是為了便於讀懂Spring之類的原始碼。