淺顯易懂的Java函數式教學

寫在最前

        本篇文章由筆者對函數式長時間使用及歸納而來,作爲典型的Java猿,學習函數式時中間走了很多彎路,在此將相關經驗總結歸納,幫助有需要的小夥伴過坑~;

        本篇文章將以函數式觀點出發學習,請暫且先忘掉關於‘lambda=內部類簡化’的類似認知,這對知識體系的整體架構是至關重要的;

一、函數式接口的概念

        函數式接口被定義爲有且僅有一個抽象方法的接口;與之相關的註解爲@FunctionalInterface;

        關於定義:由於Java1.8的接口引入了default方法,接口也允許一定程度的實現,故這裏強調一下“有且僅有一個抽象方法”;

        關於註解:有人經常對@FunctionalInterface這個註解的作用抱有疑問;這裏以我們常見的@Override做類比:

        衆所周知,@Override常被標記到對父類方法重寫的子類方法上,表示這個方法重寫了父類方法;如果不標記@Override的話,依然也是有效的重寫行爲;@Override的作用是,當標記方法沒有重寫父類方法時,給出編譯級別的提示信息,這一定程度上避免了修改父類方法名而使子類的方法由重寫變爲定義成全新方法的問題;

        同理,@FunctionalInterface標記在一個接口上,作用爲當接口中出現多於一個抽象方法時,給出編譯級別的提示信息,這樣來避免我們不小心將函數式接口拓展爲普通接口的錯誤;

        只要你願意,你可以按照此概念創造無數的自定義函數式接口;

二、三大基本函數式接口

        函數式思維的出發點,是將函數本身作爲“一等公民”,將所有問題概括爲三個步驟:數據從哪裏來?數據經過何種變換?數據的最終歸宿是哪裏?這三個問題對應了三個基本的函數式接口,這些函數式接口被定義在java.util.function包下;

        數據從哪裏來:對應java.util.function.Supplier<T> ,此函數式接口定義了數據的產生,其抽象方法 [T get();] 入參爲空出參爲T,意爲“憑空製造了T”或者說“T的誕生”;我們這裏簡寫記爲() -> T;

        數據經過何種變換:對應java.util.function.Function<T, R>,參考Supplier我們很容易理解,其抽象方法[R apply(T t);]意爲“將T加工爲R”或“將T變換爲R”,記爲T -> R;

        數據的最終歸宿是哪裏:對應java.util.function.Consumer<T>,很容易理解,其抽象方法[void accept(T t);]意爲“將T消費”,記爲T -> ();

        此概念貫穿全文,務必牢記;在此準備一個小問題,你能找到型如 [() -> ()]對應的函數式接口嗎?提示一下此接口在多線程中非常常用。

三、函數式接口的衍生

        如果觀察java.util.function包下的函數式接口,數量有40+;死記硬背很難;但只要細心觀察,所有函數式接口都有三大基本函數式接口衍生而來,此處將衍生規律做整理,方便記憶:

        1.基本類型簇:三大函數式接口均是泛型接口,雖然能夠自動裝拆箱,但依然有希望對基本類型方法直接抽象的需求,故Java中追加了一些基本類型函數式接口,這裏注意一下它們的命名規律,都是把基本類型名寫於接口上;舉例:

        IntConsumer定義爲int -> ();

        BooleanSupplier定義爲() -> boolean;

        LongFunction<R>定義爲long->R;而ToDoubleFunction<T>定義爲T->double;

        此外,有一些特例的函數式接口被賦予了別名,例如Predicate<T>定義爲T->boolean,Predicate爲“推測、下判斷”之意,這裏是對T判別一個真僞;“ToBooleanFunction<T>”則並不存在;

        2.二元參數簇:三大基本函數式接口只操作了一個數據;而實際使用時,操作多個數據的需求很常見,故官方囊括了一些此類接口,舉例如下:

        BiConsumer<T,U> 定義爲一個二元消費 (T,U) -> ();  //“Bi”爲“Binary”前綴,意爲“二元的”;

        BiFunction<T, U, R>定義爲(T,U) -> R;                      //將T、R兩個參數加工爲R;

        BiPredicate<T, U>定義爲(T,U) -> boolean;            //結合T、U下一個判斷;

        3.運算符簇:考慮Function<T,T>,其定義爲T -> T即出入參類型相同,此時我們賦予了它一個新的命名,即Operator,意爲運算符;Operator作爲函數式接口時常伴有代表參數元數的短語,舉例:

        UnaryOperator<T> 定義爲 T -> T;是一個“單目運算符”,將一個類型運算後得到一個相同的類型;實際上,它繼承了Function<T, T>;類比i++中的“++”;

        BinaryOperator<T>定義爲(T,T) -> T;是一個“雙目運算符”,同理,它繼承了BiFunction<T,T,T>;類比1+1中的“+”;

        4.複合體簇:其實很多函數式接口的名稱都是遵循上述規律的複合體,在掌握上面的命名規律後,我們可以做到“見名知型”了;舉例:

        DoubleBinaryOperator定義爲(double,double) -> double;DoubleBinaryOperator中的“Double”代表其與基本數據類型double有關,“Operator”代表其出入參相同,“Binary”代表其有兩個入參,這樣就很好推測了;

        DoubleToLongFunction定義爲double -> long;見到“ToLong”一定是返回了long型,“Double”則意味着其入參爲基本類型double;

        這裏建議參考java.util.function包下的函數式接口名稱,練習推測一下其抽象方法的型構;另外在自己定義新的函數式接口時建議延用這些規律;

四、Lambda表達式與函數引用

        在瞭解了函數式接口的概念後,這裏對Lambda表達式與函數引用進行講解;它們與函數式接口的概念息息相關;

        1.有關Lambda表達式:

        我相信很多人在不瞭解函數式體系時,已經寫過很多lambda了;在這裏結合函數式接口的概念整體歸納一下;Lambda不能單獨存在,必須被賦予與其型構(即入參類型列表與返回值類型)相當的類型才得以存在;Lambda由小括號組成的參數列表、箭頭符號"->"、主體邏輯 三部分組成,舉例:       

        // Lambda由小括號組成的參數列表、箭頭符號"->"、主體邏輯 三部分組成
        Supplier<String> sup = () -> {
            return "HelloWorld";
        };

        Function<Object,String> func = (o) -> {
            return o.toString();
        };
        
        // 當Lambda只有一個參數時,小括號可以省略
        Function<Object,String> funI = o -> {
            return o.toString();
        };
        
        // 當主體結構只有一行語句時,大括號和語句結尾的分號可以省略,如果是return語句,則return可以省略
        Function<Object,String> funII = o -> o.toString();

        // 當需要匹配多個接口類型時(尤其是標籤接口)使用&指明其類型
        Runnable r = (Runnable & Serializable) () -> {};
        System.out.println(Serializable.class.isInstance(r)); // true

        如果JavaScript寫的比較多的話,其實上面的例子類似於定義局部作用域函數:

        var sup = function() {
            return "HelloWorld";
        };

        var func = function(o) {
            return o.toString();
        };
        
        var funI = function(o) {
            return o.toString();
        };

        var funII = o => o.toString();// es6

        2.對類型的靜態函數進行引用

        函數引用即雙冒號“::”,是一種對指定函數式接口的同型方法進行直接引用的特性;對類型的靜態引用,使函數式接口與要引用的方法同型即可,舉例如下:

        Function<String,Integer> funcStoI = Integer::valueOf;//String -> Integer;
        IntUnaryOperator intUO = Math::abs;// int -> int;
        DoubleBinaryOperator doubleBO = Math::max;// (double,double) -> double;

        // 當然,構造方法也算一種特殊的靜態函數
        Supplier<String> strSup = String::new; // new String();
        UnaryOperator<String> strUO = String::new;// new String(str);

        // 同理,數組的構造參數亦可
        IntFunction<int[]> intArray = int[]::new; // new int[x];

        3.對實例的非靜態函數函數進行引用

        同理,對實例的非靜態函數引用,與對類的靜態函數引用遵循相同規則,舉例如下:

        Supplier<String> strSup = "WantString?"::toString;

        IntSupplier intSup = "WantSomeInt?"::hashCode;

        Consumer<String> strCons = System.out::println;

        4.對類型的非靜態函數進行引用,等價關係的辯證

        在上述觀點中,可以看出,Java中“靜態/非靜態”的概念與“類/實例”的概念高度一致,這些函數引用看起來非常自然;但這裏設想一下,對類的非靜態函數進行引用,是否有意義呢?答案是肯定的,這裏先引入一個有關函數純粹性的“等價關係”;

        考慮這樣一個觀點:

        person.say("hello") 與 say(person,"hello")具備某種等價關係;兩個say方法在滿足條件時可以相互轉化;這裏我們先簡寫爲 persion.say("hello") <=> say(persion,"hello");依次類推,persion.flyBetween("北京","上海") <=> flyBetween(persion,"北京","上海");它們彼此間的編程語義其實非常明確;

        這暗示了一個規律,即一個實例的非靜態方法,可以等價的變爲追加該實例類型作爲參數的靜態方法,這在函數引用中也有所體現,舉例如下:

        Function<Object,String> funcIII = Object::toString; // o.toString() <=> toString(o)
        ToIntFunction<Object> toIntFunc = Object::hashCode; // o.hashCode() <=> hashCode(o)
        
        Consumer<Runnable> runCon = Runnable::run; // r.run() <=> run(r)
        
        BiConsumer<PrintStream,String> biCons = PrintStream::println; // out.println(str) <=> println(out,str);

        可以看到,對類型的非靜態函數引用中,類型對應的實例會自動追加作爲參數到參數列表的第一個位置上;對類的非靜態方法引用,實際是上提取了非靜態方法中的靜態含義

五、Optional與Stream

        關於函數式的概念整理告一段落,這裏開始介紹其主要的使用場景,即圍繞着Optional與Stream的 構造操作、變換操作、終止操作 展開;在文末會對函數式的其它使用場景做一個總結;Optional與Stream的核心思想有很多一致性,但前者要簡單很多;在未定義終止操作時,Optional與Stream中定義的中間邏輯(構造過程、變換過程)不會執行;

        1.Optional;Optional本意爲“可選的”這裏引申爲“可有可無的”,Optional<T> 定義了一個可有可無的T實例,我們可以對T進行一系列操作,若在操作中的任何一個步驟T變爲null,則終止操作;

        1.1 Optional的構造 Optional常由其靜態函數Optional.ofNullable(T t);進行構造,其中t可爲null;

        Optional<Integer> optionalInteger = Optional.ofNullable(1);

        1.2 Optional的變換 Optional<T>.map 定義了對T的變換操作,此處可以傳入一個Function<T,R> 將T變換爲R;即Optional<T> 變爲 Optional<R>;

        // 構造Optional<Integer>
        Optional<Integer> optionalInteger = Optional.of(1);
        // 變換爲Optional<String>
        Optional<String> optionalStr = optionalInteger.map(String::valueOf);

        Optional<T>.filter 定義了一個對T的斷言,此處傳入一個Predicate<T>爲T下一個判斷,需要滿足斷言條件爲true時纔可繼續執行操作;

        // 構造包含隨機Integer的Optional
        Optional<Integer> optionalRnd = Optional.of(new Random().nextInt());
        // 傳入偶數斷言,得到一個僅存在偶數的Optional
        Optional<Integer> optionalEven = optionalRnd.filter(i -> i % 2 == 0);

        1.3 Optional的終止 這裏列舉了一些終止操作,其中ifPresent較爲常用;

        // Optional.ifPresent
        Optional.of(new Random().nextInt()) // 獲得一個包含隨機數的Optional實例
                .filter(i -> i % 2 == 0) // 斷言 使用傳入的Predicate<T> 保證數據爲偶數
                .map(String::valueOf) // 變換 使用傳入的Function<T,R>變換類型(Integer -> String)
                .ifPresent(System.out::println); // 若滿足條件則使用傳入的Consumer<T>進行消費(打印)

        // Optional.orElse
        String evenStr = Optional.of(new Random().nextInt()) // 獲得一個包含隨機數的Optional實例
                .filter(i -> i % 2 == 0) // 變換數據爲偶數
                .map(String::valueOf) // 變換類型爲字符串
                .orElse("not even"); // 若不滿足條件 則使用orElse傳入的默認值
        
        // Optional.orElseGet
        String evenStrI = Optional.of(new Random().nextInt()) // 獲得一個包含隨機數的Optional實例
                .filter(i -> i % 2 == 0) // 變換數據爲偶數
                .map(String::valueOf) // 變換類型爲字符串
                .orElseGet(String::new); // 若不滿足條件 則調用orElseGet傳入的Supplier<T>獲取一個默認值

        // Optional.orElseThrow
        String evenStrII = Optional.of(new Random().nextInt()) // 獲得一個包含隨機數的Optional實例
                .filter(i -> i % 2 == 0) // 變換數據爲偶數
                .map(String::valueOf) // 變換類型爲字符串
                .orElseThrow(RuntimeException::new); // 若不滿足條件 則拋出一個異常

        這裏我用常見的防禦性編程再舉個例子,我們經常有需要取出組合對象的多級結構,但是爲了防止空指針,我們在每次取出對象時判斷一下空:

        A a = getAFromSomeWhere();
        if(a == null) {
            return "a is null";
        }
        
        B b = a.getB();
        if(b == null) {
            return "b is null";
        }
        
        C c = b.getC();
        if(c == null) {
            return "c is null";
        }
        
        if(!c.isLegal()) {
            return "c is illegal";
        }
        
        doSomething(c);

        使用Optional的版本爲則簡化爲:

        Optional.ofNullable(getAFromSomeWhere())
        .map(A::getB)
        .map(B::getC)
        .filter(C::isLegal)
        .ifPresent(this::doSomething);

        此外,按照函數式接口的設計思想,也存在一組描述基本類型的Optional類,它們的命名規律爲Optional+類型名,由於是基本數據類型,不存在爲null的情況;這裏對基本類型是否存在的判斷是靠Optional類中一個單獨的布爾值控制的;

        // int
        OptionalInt intOp = OptionalInt.of(1);
        // long
        OptionalLong longOp = OptionalLong.of(1L);
        // double
        OptionalDouble doubleOp = OptionalDouble.of(1.);

        2.Stream;Stream<T>的抽象含義爲一系列待處理的多個對象,類比Optional,Stream對一系列對象定義一系列統一操作,在終止操作前中間操作並不會執行;Stream更加常用也更爲複雜,這裏會以相當的篇幅對Stream詳細介紹;

        2.1 Stream的構造

        這裏提供了一些Stream的直接構造方式

        // 空Stream
        Stream<Object> empty = Stream.empty();
        
        // 用可枚舉的已有對象構造一個有限流
        Stream<Integer> limitedStream = Stream.of(1,2,3);
        
        // 用迭代方式構造一個無限流 0,1,2,3...
        Stream<Integer> unlimitedStream = Stream.iterate(0, i -> i + 1);
        
        // 在無限流上增加限制
        Stream<Integer> subStream = unlimitedStream.limit(100);

        但上述對Stream的構造在實際應用中並不多,從已有的Array或Collection組件得到Stream則更爲常用: 

        // 由List得到Stream
        List<String> strList = Lists.newArrayList("a","b","c");
        Stream<String> strStream = strList.stream();
        
        // 由Set得到Stream
        Set<Long> longSet = Sets.newHashSet(1L,2L,3L);
        Stream<Long> longStream = longSet.stream();
        
        // Map則常用entrySet得到流
        Map<Integer,String> map = new HashMap<>();
        Stream<Entry<Integer, String>> kvStream = map.entrySet().stream();

        // 數組構造,這裏可以注意一下基本類型流的命名規則,類比於OptionalInt這裏有IntStream
        // int stream
        IntStream intS = Arrays.stream(new int[] {1,2,3});
        // double stream
        DoubleStream doubleS = Arrays.stream(new double[] {1.,2.,3.});
        // String stream
        Stream<String> strS = Arrays.stream(new String[]{"a","1","2"});

        可以合併多個流得到一個新的流:

        // 合併兩個流 a b 
        Stream.concat(a, b);

        2.2 Stream的變換 這裏介紹Stream的變換操作;類比Optional中的內容,有很多相似之處:

        Stream<T>.map 定義了對T的變換操作,此處可以傳入一個Function<T,R> 將T變換爲R;即Stream<T> 變爲 Stream<R>;

        // String stream
        Stream<String> strStream = Arrays.stream(new String[]{"0","1","2"});
        // map to Integer
        Stream<Integer> integerStream = strStream.map(Integer::valueOf);

        Stream<T>.filter 定義了一個對T的斷言,此處傳入一個Predicate<T>爲T下一個判斷,將滿足斷言條件爲true的對象構造爲一個新的Stream;

        // 過濾得到一個偶數流
        Stream<Integer> evenStream = Stream.iterate(0, i -> i + 1).filter(i -> i % 2 == 0);
        // 過濾得到一個字符串流
        Stream<String> strStream = Stream.of("123",1L,123.456).filter(String.class::isInstance).map(String.class::cast);

        此外,正如之前提到的,Stream本身的含義是多個對象組成的流,因此一些變換操作是圍繞着數量進行的:

        // limit限制了流的數量(對於無限流而言)
        Stream<Integer> limitedStream = Stream.iterate(0, i -> i + 1).limit(100);
        
        // skip可以跳過若干個流的對象
        Stream<Integer> skiptedStream = limitedStream.skip(90);// 90,91,92...99
        
        // flatMap 映射爲多個流並打平數據
        // 1 -> 1
        // 2 -> 1,2
        // 3 -> 1,2,3
        // integerStream: 1,1,2,1,2,3
        Stream<Integer> integerStream = Stream.of(1,2,3).flatMap(i -> Stream.iterate(1, j -> j + 1).limit(i));
        
        // sorted排序 sortedStream: 1,1,1,2,2,3 
        Stream<Integer> sortedStream = integerStream.sorted();
        
        // distinct去重 distinctStream: 1,2,3
        Stream<Integer> distinctStream = sortedStream.distinct();

        2.3 Stream的終止 執行終止操作後,會觸發中間操作的依次執行,並最終得到希望的結果

        // forEach 是常用終止操作,這裏打印了所有對象
        Stream.iterate(0, i -> i + 1).limit(10).forEach(System.out::println);
        
        // collect 這裏將Stream收集變爲list
        List<Integer> list = Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.toList());
        // collect 亦可收集爲Set
        Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.toSet());
        // toArray 將Stream的對象變爲數組,注意這裏toArray入參爲函數式接口IntFunction<Integer[]> 
        Integer[] arr = Stream.iterate(0, i -> i + 1).limit(10).toArray(Integer[]::new);
        // 將Stream變爲map toMap需要定義兩個關於Stream類型T的Function以決定map的key-value類型 請注意toMap的重載
        Map<Integer, Integer> map = Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.toMap(Functions.identity(), Functions.identity()));
        // 分組聚合 這裏按照基偶將流分裂爲兩個list 並放置於map中 請注意groupingBy方法的入參是一個Function
        // 0 -> [0,2,4,6,8]
        // 1 -> [1,3,5,7,9]
        Map<Integer, List<Integer>> groupByMap = Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.groupingBy(i -> i % 2));
        
        // 下面介紹一些列嘗試合併整個Stream流對象得到一個值的功能
        // 統計流中對象的數量
        long count = Stream.iterate(0, i -> i + 1).limit(10).count();
        // 對流使用斷言 allMatch即全部滿足時返回true
        boolean allBiggerThanTen = Stream.iterate(0, i -> i + 1).limit(15).allMatch(i -> i > 10);
        // 對流使用斷言 anyMatch即流中至少有一個對象滿足時返回true
        boolean hasBiggerThanTen = Stream.iterate(0, i -> i + 1).limit(15).anyMatch(i -> i > 10);
        // reduce 常用操作之一,傳入BinaryOperator<T>並對流對象進行迭代,最終合併爲一個結果;由於Stream中可能爲0個對象,故這裏得到的結果爲Optional(可有可無)
        // 這裏是用reduce求和
        Optional<Integer> sum = Stream.iterate(0, i -> i + 1).reduce((i,j) -> i + j);
        // 得到第一個對象
        Optional<Integer> first = Stream.iterate(0, i -> i + 1).reduce((i,j) -> i);
        // 得到最後一個對象
        Optional<Integer> last = Stream.iterate(0, i -> i + 1).reduce((i,j) -> j);
        // 得到第一個對象(相對於上面reduce來講更爲正統方式)
        Optional<Integer> realFirst = Stream.iterate(0, i -> i + 1).findFirst();
        // 一個map+reduce,搞大數據的看了可能會比較親切
        Stream.of("1","2","3").map(Integer::valueOf).reduce((x,y) -> x + y).ifPresent(System.out::println);// 打印6
        // 求最大,這裏注意max的入參 Comparator<T>,已經被@FunctionalInterface所標記
        Optional<Integer> max = Stream.iterate(0, i -> i + 1).max(Integer::compareTo);
        // 求最小
        Optional<Integer> min = Stream.iterate(0, i -> i + 1).min(Integer::compareTo);

        此外,繼續類比Optional,我們也有針對基本數據類型的Stream:

        // int stream
        IntStream intS = IntStream.iterate(0, i -> i + 1);
        // long stream
        LongStream longS = LongStream.iterate(0, i -> i + 1L);
        // double stream
        DoubleStream doubleS = DoubleStream.iterate(0, i -> i + 1.);
        // 亦可以由對象流映射到基本類型流
        IntStream intFromInteger = Stream.iterate(0, i -> i + 1).mapToInt(Integer::intValue);
        
        // 此外,基本類型流定義了一些使用方法 這裏的結果也會對應基本類型的Optional
        OptionalInt max = intS.max();

六、總結與拾遺

        總結一下本文內容;

        ※ 介紹了函數式接口的概念(有且僅有);

        ※ 介紹了函數式設計理念及對應的三大函數式接口(從哪來,經過何種加工,最終歸宿);

        ※ 介紹了三大函數式接口的衍生規律(基本類型,參數數量,運算符,混合);

        ※ 介紹了lambda表達式及函數引用相關規律(還記得等價關係嗎);

        ※ 介紹了Optional與Stream這兩個伴隨函數式大展身手的結構;

        下面補充一些經驗性質的東西;

        ⚪ 對於初學者,建議在已有代碼中,先對List、Set這類結構嘗試使用Stream進行處理,儘可能的使用函數式消除一部分控制流程語句,感受一下它帶來的方便之處;

        ⚪ 非常建議在定義方法的形參、類成員時使用函數式接口類型,結合函數引用機制可使代碼大大簡化;

        ⚪ 在定義自己的函數式接口前,儘量先找找有沒有滿足定義的已存在接口,避免重複定義;

        ⚪ 在一些場景下,使用函數式會造成性能下降,但這類‘下降’很難成爲服務瓶頸;

        ⚪ 如果一個函數式接口實現了Serializable,則能通過函數式接口實例得到被引用的方法名,這意味着我們在使用方法名時,能夠存在一個編譯時的強檢查機會,這對於一些中間件來講至關重要:

//參考位置:https://github.com/abel533/Mapper/blob/master/weekend/src/main/java/tk/mybatis/mapper/weekend/reflection/Reflections.java
// 順帶一提,這個關於mybatis的類庫非常棒,強烈推薦!
/**
 * @author Frank
 */
public class Reflections {
    private static final Pattern GET_PATTERN = Pattern.compile("^get[A-Z].*");
    private static final Pattern IS_PATTERN  = Pattern.compile("^is[A-Z].*");

    private Reflections() {
    }

    public static String fnToFieldName(Fn fn) {
        try {
            Method method = fn.getClass().getDeclaredMethod("writeReplace");
            method.setAccessible(Boolean.TRUE);
            SerializedLambda serializedLambda = (SerializedLambda) method.invoke(fn);
            // 注意這裏
            String getter = serializedLambda.getImplMethodName();
            if (GET_PATTERN.matcher(getter).matches()) {
                getter = getter.substring(3);
            } else if (IS_PATTERN.matcher(getter).matches()) {
                getter = getter.substring(2);
            }
            return Introspector.decapitalize(getter);
        } catch (ReflectiveOperationException e) {
            throw new ReflectionOperationException(e);
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章