其實在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