java8的函數式編程解析

2020-12-15 淺源深科

其實在java8就已經有java的函數式編程寫法,只是難度較大,大家都習慣了對象式用法,但在其它語言中都有函數式的用法,如js,scala,函數式其實是抽象到極致的思想。

什麼是函數式編程

函數式編程並不是Java新提出的概念,其與指令編程相比,強調函數的計算比指令的計算更重要;與過程化編程相比,其中函數的計算可以隨時調用。

當然,大家應該都知道面向對象的特性(抽象封裝繼承多態)。其實在Java8出現之前,我們關注的往往是某一類對象應該具有什麼樣的屬性,當然這也是面向對象的核心--對數據進行抽象。但是java8出現以後,這一點開始出現變化,似乎在某種場景下,更加關注某一類共有的行為(這似乎與之前的接口有些類似),這也就是java8提出函數式編程的目的。如圖1-1所示,展示了面向對象編程到面向行為編程的變化。

函數接口

java8 函數式編程的入口,每個函數接口都帶有 @FunctionalInterface 注釋,有且僅有一個未實現的方法,表示接收 Lambda 表達式,它們存在的意義在於將代碼塊作為數據打包起來。這幾個函數接口,完全可以把它們看成普通的接口,不過他們有且僅有一個抽象方法(因為要接收 Lambda 表達式)。@FunctionalInterface 該注釋會強制 javac 檢查一個接口是否符合函數接口的標準。 如果該注釋添加給一個枚舉類型、 類或另一個注釋, 或者接口包含不止一個抽象方法, javac 就會報錯。

Lambda 表達式

Lambda 表達式,有時候也稱為匿名函數或箭頭函數,幾乎在當前的各種主流的程式語言中都有它的身影。Java8 中引入 Lambda 表達式,使原本需要用匿名類實現接口來傳遞行為,現在通過 Lambda 可以更直觀的表達。

Lambda 表達式,也可稱為閉包。閉包就是一個定義在函數內部的函數,閉包使得變量即使脫離了該函數的作用域範圍也依然能被訪問到。Lambda 表達式的本質只是一個」語法糖」,由編譯器推斷並幫你轉換包裝為常規的代碼,因此你可以使用更少的代碼來實現同樣的功能。Lambda 表達式是一個匿名函數,即沒有函數名的函數。有些函數如果只是臨時一用,而且它的業務邏輯也很簡單時,就沒必要非給它取個名字不可。Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中).Lambda 表達式語法如下:形參列表=>函數體(函數體多於一條語句的可用大括號括起)。在Java裡就是() -> {}:

(parameters) -> expression(parameters) ->{ statements; }

Lambda表達式的重要特徵:

Lambda 表達式主要用來定義行內執行的方法類型接口,例如,一個簡單方法接口。Lambda表達式是通過函數式接口(必須有且僅有一個抽象方法聲明)識別的可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。可選的大括號:如果主體包含了一個語句,就不需要使用大括號。可選的返回關鍵字:如果主體只有一個表達式返回值,則編譯器會自動返回值,大括號需要指定表達式返回一個值。Lambda表達式中的變量作用域:

訪問權限與匿名對象的方式非常類似。只能夠訪問局部對應的外部區域的局部final變量,以及成員變量和靜態變量。在Lambda表達式中能訪問域外的局部非final變量、但不能修改Lambda域外的局部非final變量。因為在Lambda表達式中,Lambda域外的局部非final變量會在編譯的時候,會被隱式地當做final變量來處理。Lambda表達式內部無法訪問接口默認(default)方法.例子:使用Java 8之前的方法來實現對一個string列表進行排序:

List<String> names = Arrays.asList("peter","anna","mike","xenia");Collections.sort(names,newComparator<String>(){@Overridepublic int compare(String a, String b){return b.compareTo(a);}});

Java 8 Lambda 表達式:

Collections.sort(names,(String a, String b)->{return b.compareTo(a);});// 只有一條邏輯語句,可以省略大括號Collections.sort(names,(String a, String b)-> b.compareTo(a));// 可以省略入參類型Collections.sort(names,(a, b)-> b.compareTo(a));

FunctionalInterface

Java8的新引入,包含函數式的設計,接口都有@FunctionalInterface的註解。如聲明一個接口

@FunctionalInterfacepublic interface fun{} 這會編譯錯,編譯器會告訴你*no target method*。而如果加一個方法: @FunctionalInterface public interface fun{ void run(); } 這就OK了,一個函數式接口聲明好了。再加一個呢? @FunctionalInterface public interface fun{ void run(); void test(); } 不ok,明確說了只有一個抽象方法嘛。但是如果換一種函數籤名: @FunctionalInterface public interface fun{ Object equals(Object o); void test(); } 這就OK了。一個抽象方法,一個Object的public方法,相安無事。Object還有其他方法,clone方法試試會怎麼樣? @FunctionalInterface public interface fun{ Object clone(); void test(); }這又不行了,因為前面明確說了,要是Object的public方法,而clone是protected的。

小結:函數式接口,有且僅有一個抽象方法,Object的public方法除外。

因為Java本身支持多接口實現,你定義一個Class可以implements多個interface。所以這個限制也沒什麼影響,如果想約定一個函數式接口來統一,也可以做一些默認的實現來達到一個接口多個抽象方法的目的,比如下面這種做法:

一個普通接口NonFunc:

public interface NonFunc {void foo(); void voo(); } 函數式接口Func: public interface Func extends NonFunc { default void foo(); default void voo(); void run(); } 實現的測試類: Public class FunTest implements Func { public static void main(String... args) { Func func = new FunTest(); func.run(); func.foo(); func.voo(); } @Override public void run() { System.out.println("run"); } @Override public void foo() { System.out.println("foo"); } @Override public void voo() { System.out.println("voo"); } } 函數式接口的一大特性就是可以被lambda表達式和函數引用表達式代替。 public class FunTest { public static void main(String... args) { FunTest t = new FunTest(); //lambda t.test(10, ()->System.out.println("xxxxx")) //method reference t.test(100, t::customedFunc); } public void customedFunc(){ System.out.println("a customed method reference"); } public void test(int x, Func func) { System.out.println(x); func.run(); } } 上面例子列舉了一個lambda模式和一個方法引用模式,這樣就可以利用函數式編程強大的能力,將方法作為參數了。 下面再加一例 public class Main { public static void main(String[] args) { Action action = System.out :: println; action.execute("Hello World!"); test(System.out :: println, "Hello World!"); } static void test(Action action, String str) { action.execute(str); } } @FunctionalInterface interface Action<T> { public void execute(T t); }

Function

關於Function接口,其接口聲明是一個函數式接口,其抽象表達函數為

@FunctionalInterfacepublicinterfaceFunction<T,R>{Rapply(T t);...}

函數意為將參數T傳遞給一個函數,返回R。即R=Function(T)其默認實現了3個default方法,分別是compose、andThen和identity,對應的函數表達為:compose對應V=Function(ParamFunction(T)),體現嵌套關係;andThen對應V=ParamFunction(Function(T)),轉換了嵌套的順序;還有identity對應了一個傳遞自身的函數調用對應Function(T)=T。從這裡看出來,compose和andThen對於兩個函數f和g來說,f.compose(g)等價於g.andThen(f)。看個例子:

public static void lambdaFunction(){Function<Integer, Integer> incr1 = x->x*2; Function<Integer, Integer> multiply = x->x*2; int x=2; System.out.println("f(x)=x+1,when x="+x+", f(x)="+incr1.apply(x)); System.out.println("f(x)=x+1,g(x)=2x, when x="+x+",f(g(x))="+incr1.compose(multiply).apply(x)); System.out.println("f(x)=x+1,g(x)=2x, when x="+x+",g(f(x))="+incr1.andThen(multiply).apply(x)); System.out.println("compose vs andThen: f(g(x))="+incr1.compose(multiply).apply(x)+","+multiply.andThen(incr1).apply(x));}控制臺輸出:-------------------------------------------------------f(x)=x+1,when x=2, f(x)=4f(x)=x+1,g(x)=2x, when x=2,f(g(x))=8f(x)=x+1,g(x)=2x, when x=2,g(f(x))=8compose vs andThen: f(g(x))=8,8

高階函數

只是普通的lambda表達式,其能力有限。我們會希望引入更強大的函數能力——高階函數,可以定義任意同類計算的函數。

比如這個函數定義,參數是z,返回值是一個Function,這個Function本身又接受另一個參數y,返回z+y。於是我們可以根據這個函數,定義任意加法函數:

//high orderfunctionFunction<Integer,Function<Integer, Integer>> makeAdder = z->y->z+y;x=2;//defineadd1Function<Integer, Integer> add1 = makeAdder.apply(1);System.out.println("f(x)=x+1,when x="+x+",f(x)="+add1.apply(x));//define add5Function<Integer, Integer> add5 = makeAdder.apply(5);System.out.println("f(x)=x+5, when x="+x+", f(x)="+add5.apply(x));

由於高階函數接受一個函數作為參數,結果返回另一個函數,所以是典型的函數到函數的映射。BiFunction提供了二元函數的一個接口聲明,舉例來說:

//binary funBiFunction<Integer, Integer, Integer> multiply =(a,b)>a*b;System.out.println("f(z)=x*y, when x=3,y=5 when f(z)="+multiply,apply(3,5));

其輸出結果將是:f(z)=x*y, when x=3,y=5, then f(z)=15。二元函數沒有compose能力,只是默認實現了andThen。

有了一元和二元函數,那麼可以通過組合擴展出更多的函數可能。

Function接口相關的接口包括:

BiFunction :R apply(T t, U u);接受兩個參數,返回一個值,代表一個二元函數;DoubleFunction :R apply(double value);只處理double類型的一元函數;IntFunction :R apply(int value);只處理int參數的一元函數;LongFunction :R apply(long value);只處理long參數的一元函數;ToDoubleFunction:double applyAsDouble(T value);返回double的一元函數;ToDoubleBiFunction:double applyAsDouble(T t, U u);返回double的二元函數;ToIntFunction:int applyAsInt(T value);返回int的一元函數;ToIntBiFunction:int applyAsInt(T t, U u);返回int的二元函數;ToLongFunction:long applyAsLong(T value);返回long的一元函數;ToLongBiFunction:long applyAsLong(T t, U u);返回long的二元函數;DoubleToIntFunction:int applyAsInt(double value);接受double返回int的一元函數;DoubleToLongFunction:long applyAsLong(double value);接受double返回long的一元函數;IntToDoubleFunction:double applyAsDouble(int value);接受int返回double的一元函數;IntToLongFunction:long applyAsLong(int value);接受int返回long的一元函數;LongToDoubleFunction:double applyAsDouble(long value);接受long返回double的一元函數;LongToIntFunction:int applyAsInt(long value);接受long返回int的一元函數;Operator

Operator其實就是Function,函數有時候也叫作算子。算子在Java8中接口描述更像是函數的補充,和上面的很多類型映射型函數類似。算子Operator包括:UnaryOperator和BinaryOperator。分別對應單元算子和二元算子。算子的接口聲明如下:

@FunctionalInterfacepublicinterfaceUnaryOperator<T>extendsFunction<T,T>{static<T>UnaryOperator<T>identity(){return t->t;}}

二元算子的聲明:算子就是一個針對同類型輸入輸出的一個映射。在此接口下,只需聲明一個泛型參數T即可。對應上面的例子:

publicclassTestOperator{publicstaticvoidmain(String... args){UnaryOperator<Integer>add= x->x+1; System.out.println(add.apply(1));BinaryOperator<Integer> addxy =(x,y)->x+y; System.out.println(addxy.apply(3,5));BinaryOperator<Integer> min = BinaryOperator.minBy((o1,o2)->o1-o2); System.out.println(min.apply(100,200));BinaryOperator<Integer> max = BinaryOperator.maxBy((o1,o2)->o1-o2); System.out.println(max.apply(100,200));}}

例子裡補充一點的是,BinaryOperator提供了兩個默認的static快捷實現,幫助實現二元函數min(x,y)和max(x,y),使用時注意的是排序器可別傳反了:)

其他的Operator接口:(不解釋了)

LongUnaryOperator:long applyAsLong(long operand);IntUnaryOperator:int applyAsInt(int operand);DoubleUnaryOperator:double applyAsDouble(double operand);DoubleBinaryOperator:double applyAsDouble(double left, double right);IntBinaryOperator:int applyAsInt(int left, int right);LongBinaryOperator:long applyAsLong(long left, long right);Predicate

predicate是一個謂詞函數,主要作為一個謂詞演算推導真假值存在,其意義在於幫助開發一些返回bool值的Function。本質上也是一個單元函數接口,其抽象方法test接受一個泛型參數T,返回一個boolean值。等價於一個Function的boolean型返回值的子集。

@FunctionalInterfacepublicinterfacePredicate<T>{booleantest(T t);...}

其默認方法也封裝了and、or和negate邏輯。寫個小例子看看:

publicclassTestJ8Predicate{publicstaticvoidmain(String... args){TestJ8Predicate t =newTestJ8Predicate();t.pringBigValue(10, val->val>5);t.printBigValueAnd(10, val->val>5);t.printBigValueAnd(6, val->val>5);}publicvoidpringBigValue(intvalue, Predicate<Integer> predicate){if(predicate.test(value)){System.out.println(value);}}publicvoidprintBigValueAnd(intvalue, Predicate<Integer> predicate){if(predicate.and(v->v<8).test(value)){System.out.println("value < 8:"+value);}else{System.out.println("value should < 8 at least.");}}}

Predicate在Stream中有應用,Stream的filter方法就是接受Predicate作為入參的。這個具體在後面使用Stream的時候再分析深入。

其他Predicate接口:

BiPredicate:boolean test(T t, U u);接受兩個參數的二元謂詞DoublePredicate:boolean test(double value);入參為double的謂詞函數IntPredicate:boolean test(int value);入參為int的謂詞函數LongPredicate:boolean test(long value);入參為long的謂詞函數Consumer

看名字就可以想到,這像謂詞函數接口一樣,也是一個Function接口的特殊表達——接受一個泛型參數,不需要返回值的函數接口。

@FunctionalInterfacepublicinterfaceConsumer<T>{voidaccept(T t);...}

這個接口聲明太重要了,對於一些純粹consume型的函數,沒有Consumer的定義真無法被Function家族的函數接口表達。因為Function一定需要一個泛型參數作為返回值類型(當然不排除你使用Function來定義,但是一直返回一個無用的值)。比如下面的例子,如果沒有Consumer,類似的行為使用Function表達就一定需要一個返回值。

publicstaticvoidmain(String... args){Consumer<Integer> consumer = System.out::println; consumer.accept(100); //use function, you always need one return value. Function<Integer,Integer> function = x->{System.out.println(x);return x;}function.apply(100);}

其他Consumer接口:

BiConsumer:void accept(T t, U u);接受兩個參數DoubleConsumer:void accept(double value);接受一個double參數IntConsumer:void accept(int value);接受一個int參數LongConsumer:void accept(long value);接受一個long參數ObjDoubleConsumer:void accept(T t, double value);接受一個泛型參數一個double參數ObjIntConsumer:void accept(T t, int value);接受一個泛型參數一個int參數ObjLongConsumer:void accept(T t, long value);接受一個泛型參數一個long參數Supplier

其聲明如下:

@FunctionalInterfacepublicinterfaceSupplier<T>{Tget();...}

其簡潔的聲明,會讓人以為不是函數。這個抽象方法的聲明,同Consumer相反,是一個只聲明了返回值,不需要參數的函數(這還叫函數?)。也就是說Supplier其實表達的不是從一個參數空間到結果空間的映射能力,而是表達一種生成能力,因為我們常見的場景中不止是要consume(Consumer)或者是簡單的map(Function),還包括了new這個動作。而Supplier就表達了這種能力。

比如你要是返回一個常量,那可以使用類似的做法:

這保證supplier對象輸出的一直是1。

如果是要利用構造函數的能力呢?就可以這樣:

Supplier<TestJ8Supplier> anotherSupplier;for(int i=0; i<10; i++){anotherSupplier = TestJ8Supplier::new; System.out.println(anotherSupplier.get())}

這樣的輸出可以看到,全部的對象都是new出來的。

這樣的場景在Stream計算中會經常用到,具體在分析Java 8中Stream的時候再深入。

其他Supplier接口:BooleanSupplier:boolean getAsBoolean();返回booleanDoubleSupplier:double getAsDouble();返回doubleIntSupplier:int getAsInt();返回intLongSupplier:long getAsLong();返回long

相關焦點

  • 輕鬆玩轉函數式編程
    於是我抽時間捋了捋,將平時工作中用到的函數式編程案例和思想整理了出來,相信閱讀本文後,大家都能快速上手函數式編程。 函數式編程目前使用範圍非常廣,常用的框架,語言幾乎都能看到它的身影。 前端框架:react、vue 的 hooks 用法。
  • 函數式編程那些事兒
    函數式編程是一種編程範式,在其中它試圖將每個函數都綁定到純數學函數中。這是一種聲明式的編程風格,著重於解決什麼而不是如何解決。Clojure,Common Lisp,Erlang,Haskell和Scala是遵循函數式編程方法的一些著名程式語言。
  • 大數據入門:Scala函數式編程
    提到Scala,首先會提到的一個概念,就是函數式編程,這也是Scala語言區別與其他程式語言的典型特徵。Scala是一門多範式(multi-paradigm)的程式語言,設計初衷是要集成面向對象編程和函數式編程的各種特性。
  • 知識分享之函數式編程的簡單介紹
    各位小夥伴們大家好,好久不見,這次小編要分享的一個知識是有關於函數式編程的。函數式編程是近年來比較熱門的一個話題,很多人都在談FunctionalProgramming,函數式編程有如下特點:函數即為數據,第一等公民。
  • 函數式編程很難懂?其實真心很簡單
    話不多說,開始今天的學習:現在直播一直都很火,今天我們就用Java代碼簡單地模擬一個直播案例,以此來一步步說明什麼叫函數式編程。不要看這個名字好像挺難懂的樣子,其實很簡單,兩分鐘時間即可看完。可以的,也就是今天的重點,函數式編程。二、函數式編程函數,這個概念我們在數學裡面我們就接觸過。y=f(x)(y=x+1)這就是函數的格式,其中f是函數名,x是變量,y是函數值,還有定義域,值域什麼的。
  • 從Python到Haskell:程式設計師為何與函數式編程「墜入愛河」?
    儘管像Google這樣的大公司依賴於函數式編程的關鍵概念,但是普通程式設計師對此幾乎一無所知。這種情況即將改變了。不僅是Java或Python這樣的語言越來越多地採用了函數式編程的概念,類似Haskell這樣的新語言也正在完全實現函數式編程。簡單來說,函數式編程就是為不可變變量構建函數。
  • 寫Python 代碼不可不知的函數式編程技術
    選自 Medium作者:Raivat Shah參與:魔王、Jamin本文對 Python 中的函數式編程技術進行了簡單的入門介紹。近來,越來越多人使用函數式編程(functional programming)。因此,很多傳統的命令式語言(如 Java 和 Python)開始支持函數式編程技術。本文對 Python 中的函數式編程技術進行了簡單的入門介紹。本文適合對函數式編程有基本了解的讀者。
  • 一文讀懂函數解析式的求法
    函數解析式的求法向來是高中數學的重點和難點部分,該部分內容在高考中經常遇到,能夠快速的求到函數解釋式對於高考想要拿高分的同學是必不可少的。本節就主要介紹各種函數解析式的求法,希望對大家有所幫助!函數解析式的求法函數解析式的求法主要分為以下幾類:構造法該方法是已知f [g(x)]的解析式,要求f
  • 純Java中的函數式編程:Functor和Monad示例
    絕大多數程式設計師,特別是那些沒有功能編程背景的程式設計師,都傾向於認為monad是某種神秘的計算機科學概念,因此從理論上講,它對他們的編程事業沒有幫助。這種消極的觀點可以歸因於數十篇文章或博客文章過於抽象或過於狹窄。但是事實證明,即使在標準Java庫中,monad也無處不在,尤其是從Java Development Kit(JDK)8開始(以後會有更多介紹)。
  • 初二上學期,待定係數法求函數解析式,基礎卻很重要的知識點
    很多同學初二剛接觸函數,覺得函數比較難。其中,求函數解析式是最基礎的知識點,但是也很重要。因為很多函數題目,第一小問基本上都是求函數解析式,如果函數解析式求解錯誤,那麼後面的小問無論思路正確與否都會出錯。
  • Java新玩法,Java8新特性終極解析
    函數式接口是只定義了一個抽象方法的接口。Java 8引入了FunctionalInterface註解來表明一個接口打算成為一個函數式接口。例如,java.lang.Runnable就是一個函數式接口。@FunctionalInterfacepublic interface Runnable { public abstract void run();}注意,不管FunctionalInterface註解是否存在,Java編譯器都會將所有滿足該定義的接口看作是函數式接口。
  • 讓你徹底明白yield語法糖的用法和原理及在C 函數式編程中的作用
    通過使用 yield 定義迭代器,可在實現自定義集合類型的 IEnumerator 和 IEnumerable 模式時無需其他顯式類(保留枚舉狀態的類,有關示例,請參閱 IEnumerator)。沒用過yield之前,看這句話肯定是一頭霧水,只有在業務開發中踩過坑,才能體會到yield所帶來的快感。
  • C語言函數調用過程中的內存變化解析
    打開APP C語言函數調用過程中的內存變化解析 TOMORROW 星辰 發表於 2020-12-11 16:21:13 相信很多編程新手村的同學們都會有一個疑問:C 語言如何調用函數的呢?
  • 函數方程思想在解析幾何中的應用,如此應用,才得法,您知道嗎?
    在高考數學的考察中,解析幾何作為五大核心考點之一,命題專家是絞盡腦汁,作為考生,我們該如何應對呢?眾所周知,直線與圓的方程,橢圓,雙曲線,拋物線等,他們的定義,方程,幾何性質及圖形等是支撐解析幾何的基石,是高考命題的基本元素,在解析幾何中的求解問題中,函數方程思想至關重要。
  • 利用EasyX圖像編程製作y=x^2函數圖像
    ;cmath>#include<conio.h>int main(){int x=-50;initgraph(100,200);SetWindowText(GetHWnd(),"二次函數圖像
  • Spark程式語言之scala
    scala具有自己特有的語法:增強,函數式編程,偏函數,函數的柯裡化高階函數,將函數作為參數傳遞等。2.Spark架構由scala語言編寫。3.Scala語言的特點:Scala是以一門以java虛擬機JVM為運行環境的將面向對象,函數式編程結合在一起的靜態類型程式語言。Scala原始碼.scala會被編譯成Java字節碼.class,然後運行於JVM上。
  • Linux伺服器編程簡介
    Linux伺服器編程的特點是異步高並發,代碼不能阻塞、不能休眠,以提高伺服器的並發效率。給nginx寫自定義的模塊,就是典型的Linux伺服器編程。nginx-rtmp-module就是一個開源的nginx模塊,它為nginx添加了rtmp協議的支持。
  • 大數據入門:Java和Scala編程對比
    在學習大數據之初,很多人都會對程式語言的學習有疑問,比如說大數據編程主要用什麼語言,在實際運用當中,大數據主流編程是Java,但是涉及到Spark、Kafka框架,還需要懂Scala。今天的大數據入門分享,我們就來對Java和Scala這兩門語言的編程做個對比。
  • 知函數f(x)是偶函數則f(-ln3)和f(2^m)大小?統一戰線是解題關鍵
    求出f(x)的解析式因為函數f(x)=2^|x-m|-1是在定義域R上的偶函數,定義域R已滿足原點對稱,所以只需要函數f(x)滿足f(x)=f(-x)即可。圖二求出f(x)的解析式後,要想比較a,b,c這三者之間的關係,要將這三者內的變量值轉化成同一個單調區間內。