JAVA基礎之四-函式式介面和流的簡介

正在战斗中發表於2024-09-04

自從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之類的原始碼。

相關文章