溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java中怎么支持函數(shù)式編程

發(fā)布時(shí)間:2021-07-24 14:23:24 來(lái)源:億速云 閱讀:126 作者:Leah 欄目:編程語(yǔ)言

這篇文章將為大家詳細(xì)講解有關(guān)Java中怎么支持函數(shù)式編程,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。

Java一直是面向?qū)ο蟮恼Z(yǔ)言,一切皆對(duì)象,如果想要調(diào)用一個(gè)函數(shù),函數(shù)必須屬于一個(gè)類或?qū)ο螅缓笤谑褂妙惢驅(qū)ο筮M(jìn)行調(diào)用。但是在其它的編程語(yǔ)言中,如JS、C++,我們可以直接寫一個(gè)函數(shù),然后在需要的時(shí)候進(jìn)行調(diào)用,既可以說(shuō)是面向?qū)ο缶幊?,也可以說(shuō)是函數(shù)式編程。從功能上來(lái)看,面向?qū)ο缶幊虥](méi)什么不好的地方,但是從開發(fā)的角度來(lái)看,面向?qū)ο缶幊虝?huì)多寫很多可能是重復(fù)的代碼行。比如創(chuàng)建一個(gè)Runnable的匿名類的時(shí)候:

Runnable runnable = new Runnable() {     @Override     public void run() {         System.out.println("do something...");     } };

這一段代碼中真正有用的只有run方法中的內(nèi)容,剩余的部分都是屬于Java編程語(yǔ)言的結(jié)構(gòu)部分,沒(méi)什么用,但是要寫。幸運(yùn)的是Java  8開始,引入了函數(shù)式編程接口與Lambda表達(dá)式,幫助我們寫更少更優(yōu)雅的代碼:

// 一行即可 Runnable runnable = () -> System.out.println("do something...");

現(xiàn)在主流的編程范式主要有三種,面向過(guò)程、面向?qū)ο蠛秃瘮?shù)式編程。

函數(shù)式編程并非一個(gè)很新的東西,早在50多年前就已經(jīng)出現(xiàn)了。近幾年,函數(shù)式編程越來(lái)越被人關(guān)注,出現(xiàn)了很多新的函數(shù)式編程語(yǔ)言,比如Clojure、Scala、Erlang等。一些非函數(shù)式編程語(yǔ)言也加入了很多特性、語(yǔ)法、類庫(kù)來(lái)支持函數(shù)式編程,比如Java、Python、Ruby、JavaScript等。除此之外,Google  Guava也有對(duì)函數(shù)式編程的增強(qiáng)功能。

函數(shù)式編程因其編程的特殊性,僅在科學(xué)計(jì)算、數(shù)據(jù)處理、統(tǒng)計(jì)分析等領(lǐng)域,才能更好地發(fā)揮它的優(yōu)勢(shì),所以它并不能完全替代更加通用的面向?qū)ο缶幊谭妒?。但是作為一種補(bǔ)充,它也有很大存在、發(fā)展和學(xué)習(xí)的意義。

什么是函數(shù)式編程

函數(shù)式編程的英文翻譯是Functional Programming。

那到底什么是函數(shù)式編程呢?實(shí)際上,函數(shù)式編程沒(méi)有一個(gè)嚴(yán)格的官方定義。嚴(yán)格上來(lái)講,函數(shù)式編程中的“函數(shù)”,并不是指我們編程語(yǔ)言中的“函數(shù)”概念,而是指數(shù)學(xué)“函數(shù)”或者“表達(dá)式”(例如:y=f(x))。不過(guò),在編程實(shí)現(xiàn)的時(shí)候,對(duì)于數(shù)學(xué)“函數(shù)”或“表達(dá)式”,我們一般習(xí)慣性地將它們?cè)O(shè)計(jì)成函數(shù)。所以,如果不深究的話,函數(shù)式編程中的“函數(shù)”也可以理解為編程語(yǔ)言中的“函數(shù)”。

每個(gè)編程范式都有自己獨(dú)特的地方,這就是它們會(huì)被抽象出來(lái)作為一種范式的原因。面向?qū)ο缶幊套畲蟮奶攸c(diǎn)是:以類、對(duì)象作為組織代碼的單元以及它的四大特性。面向過(guò)程編程最大的特點(diǎn)是:以函數(shù)作為組織代碼的單元,數(shù)據(jù)與方法相分離。那函數(shù)式編程最獨(dú)特的地方又在哪里呢?實(shí)際上,函數(shù)式編程最獨(dú)特的地方在于它的編程思想。函數(shù)式編程認(rèn)為程序可以用一系列數(shù)學(xué)函數(shù)或表達(dá)式的組合來(lái)表示。函數(shù)式編程是程序面向數(shù)學(xué)的更底層的抽象,將計(jì)算過(guò)程描述為表達(dá)式。不過(guò),這樣說(shuō)你肯定會(huì)有疑問(wèn),真的可以把任何程序都表示成一組數(shù)學(xué)表達(dá)式嗎?

理論上講是可以的。但是,并不是所有的程序都適合這么做。函數(shù)式編程有它自己適合的應(yīng)用場(chǎng)景,比如科學(xué)計(jì)算、數(shù)據(jù)處理、統(tǒng)計(jì)分析等。在這些領(lǐng)域,程序往往比較容易用數(shù)學(xué)表達(dá)式來(lái)表示,比起非函數(shù)式編程,實(shí)現(xiàn)同樣的功能,函數(shù)式編程可以用很少的代碼就能搞定。但是,對(duì)于強(qiáng)業(yè)務(wù)相關(guān)的大型業(yè)務(wù)系統(tǒng)開發(fā)來(lái)說(shuō),費(fèi)勁吧啦地將它抽象成數(shù)學(xué)表達(dá)式,硬要用函數(shù)式編程來(lái)實(shí)現(xiàn),顯然是自討苦吃。相反,在這種應(yīng)用場(chǎng)景下,面向?qū)ο缶幊谈雍线m,寫出來(lái)的代碼更加可讀、可維護(hù)。

再具體到編程實(shí)現(xiàn),函數(shù)式編程跟面向過(guò)程編程一樣,也是以函數(shù)作為組織代碼的單元。不過(guò),它跟面向過(guò)程編程的區(qū)別在于,它的函數(shù)是無(wú)狀態(tài)的。何為無(wú)狀態(tài)?簡(jiǎn)單點(diǎn)講就是,函數(shù)內(nèi)部涉及的變量都是局部變量,不會(huì)像面向?qū)ο缶幊棠菢?,共享類成員變量,也不會(huì)像面向過(guò)程編程那樣,共享全局變量。函數(shù)的執(zhí)行結(jié)果只與入?yún)⒂嘘P(guān),跟其他任何外部變量無(wú)關(guān)。同樣的入?yún)ⅲ还茉趺磮?zhí)行,得到的結(jié)果都是一樣的。這實(shí)際上就是數(shù)學(xué)函數(shù)或數(shù)學(xué)表達(dá)式的基本要求。舉個(gè)例子:

// 有狀態(tài)函數(shù): 執(zhí)行結(jié)果依賴b的值是多少,即便入?yún)⑾嗤?nbsp;// 多次執(zhí)行函數(shù),函數(shù)的返回值有可能不同,因?yàn)閎值有可能不同。 int b; int increase(int a) {   return a + b; }  // 無(wú)狀態(tài)函數(shù):執(zhí)行結(jié)果不依賴任何外部變量值 // 只要入?yún)⑾嗤?,不管?zhí)行多少次,函數(shù)的返回值就相同 int increase(int a, int b) {   return a + b; }

不同的編程范式之間并不是截然不同的,總是有一些相同的編程規(guī)則。比如不管是面向過(guò)程、面向?qū)ο筮€是函數(shù)式編程,它們都有變量、函數(shù)的概念,最頂層都要有main函數(shù)執(zhí)行入口,來(lái)組裝編程單元(類、函數(shù)等)。只不過(guò),面向?qū)ο蟮木幊虇卧穷惢驅(qū)ο?,面向過(guò)程的編程單元是函數(shù),函數(shù)式編程的編程單元是無(wú)狀態(tài)函數(shù)。

Java對(duì)函數(shù)式編程的支持

實(shí)現(xiàn)面向?qū)ο缶幊滩灰欢ǚ堑檬褂妹嫦驅(qū)ο缶幊陶Z(yǔ)言,同理,實(shí)現(xiàn)函數(shù)式編程也不一定非得使用函數(shù)式編程語(yǔ)言?,F(xiàn)在,很多面向?qū)ο缶幊陶Z(yǔ)言,也提供了相應(yīng)的語(yǔ)法、類庫(kù)來(lái)支持函數(shù)式編程。

Java這種面向?qū)ο缶幊陶Z(yǔ)言,對(duì)函數(shù)式編程的支持可以通過(guò)一個(gè)例子來(lái)描述:

public class Demo {   public static void main(String[] args) {     Optional<Integer> result = Stream.of("a", "be", "hello")             .map(s -> s.length())             .filter(l -> l <= 3)             .max((o1, o2) -> o1-o2);     System.out.println(result.get()); // 輸出2   } }

這段代碼的作用是從一組字符串?dāng)?shù)組中,過(guò)濾出長(zhǎng)度小于等于3的字符串,并且求得這其中的最大長(zhǎng)度。

Java為函數(shù)式編程引入了三個(gè)新的語(yǔ)法概念:Stream類、Lambda表達(dá)式和函數(shù)接口(Functional  Inteface)。Stream類用來(lái)支持通過(guò)“.”級(jí)聯(lián)多個(gè)函數(shù)操作的代碼編寫方式;引入Lambda表達(dá)式的作用是簡(jiǎn)化代碼編寫;函數(shù)接口的作用是讓我們可以把函數(shù)包裹成函數(shù)接口,來(lái)實(shí)現(xiàn)把函數(shù)當(dāng)做參數(shù)一樣來(lái)使用(Java  不像C那樣支持函數(shù)指針,可以把函數(shù)直接當(dāng)參數(shù)來(lái)使用)。

Stream類

假設(shè)我們要計(jì)算這樣一個(gè)表達(dá)式:(3-1)*2+5。如果按照普通的函數(shù)調(diào)用的方式寫出來(lái),就是下面這個(gè)樣子:

add(multiply(subtract(3,1),2),5);

不過(guò),這樣編寫代碼看起來(lái)會(huì)比較難理解,我們換個(gè)更易讀的寫法,如下所示:

subtract(3,1).multiply(2).add(5);

在Java中,“.”表示調(diào)用某個(gè)對(duì)象的方法。為了支持上面這種級(jí)聯(lián)調(diào)用方式,我們讓每個(gè)函數(shù)都返回一個(gè)通用的Stream類對(duì)象。在Stream類上的操作有兩種:中間操作和終止操作。中間操作返回的仍然是Stream類對(duì)象,而終止操作返回的是確定的值結(jié)果。

再來(lái)看之前的例子,對(duì)代碼做了注釋解釋。其中map、filter是中間操作,返回Stream類對(duì)象,可以繼續(xù)級(jí)聯(lián)其他操作;max是終止操作,返回的不是Stream類對(duì)象,無(wú)法再繼續(xù)往下級(jí)聯(lián)處理了。

public class Demo {   public static void main(String[] args) {     Optional<Integer> result = Stream.of("f", "ba", "hello") // of返回Stream<String>對(duì)象             .map(s -> s.length()) // map返回Stream<Integer>對(duì)象             .filter(l -> l <= 3) // filter返回Stream<Integer>對(duì)象             .max((o1, o2) -> o1-o2); // max終止操作:返回Optional<Integer>     System.out.println(result.get()); // 輸出2   } }

Lambda表達(dá)式

前面提到Java引入Lambda表達(dá)式的主要作用是簡(jiǎn)化代碼編寫。實(shí)際上,我們也可以不用Lambda表達(dá)式來(lái)書寫例子中的代碼。我們拿其中的map函數(shù)來(lái)舉例說(shuō)明。

下面三段代碼,第一段代碼展示了map函數(shù)的定義,實(shí)際上,map函數(shù)接收的參數(shù)是一個(gè)Function接口,也就是函數(shù)接口。第二段代碼展示了map函數(shù)的使用方式。第三段代碼是針對(duì)第二段代碼用Lambda表達(dá)式簡(jiǎn)化之后的寫法。實(shí)際上,Lambda表達(dá)式在Java中只是一個(gè)語(yǔ)法糖而已,底層是基于函數(shù)接口來(lái)實(shí)現(xiàn)的,也就是第二段代碼展示的寫法。

// Stream類中map函數(shù)的定義: public interface Stream<T> extends BaseStream<T, Stream<T>> {   <R> Stream<R> map(Function<? super T, ? extends R> mapper);   //...省略其他函數(shù)... }  // Stream類中map的使用方法示例: Stream.of("fo", "bar", "hello").map(new Function<String, Integer>() {   @Override   public Integer apply(String s) {     return s.length();   } });  // 用Lambda表達(dá)式簡(jiǎn)化后的寫法: Stream.of("fo", "bar", "hello").map(s -> s.length());

Lambda表達(dá)式包括三部分:輸入、函數(shù)體、輸出。表示出來(lái)的話就是下面這個(gè)樣子:

(a, b) -> { 語(yǔ)句1;語(yǔ)句2;...; return 輸出; } //a,b是輸入?yún)?shù)

實(shí)際上,Lambda表達(dá)式的寫法非常靈活。上面給出的是標(biāo)準(zhǔn)寫法,還有很多簡(jiǎn)化寫法。比如,如果輸入?yún)?shù)只有一個(gè),可以省略 (),直接寫成  a->{&hellip;};如果沒(méi)有入?yún)ⅲ梢灾苯訉⑤斎牒图^都省略掉,只保留函數(shù)體;如果函數(shù)體只有一個(gè)語(yǔ)句,那可以將{}省略掉;如果函數(shù)沒(méi)有返回值,return語(yǔ)句就可以不用寫了。

Optional<Integer> result = Stream.of("f", "ba", "hello")         .map(s -> s.length())         .filter(l -> l <= 3)         .max((o1, o2) -> o1-o2);          // 還原為函數(shù)接口的實(shí)現(xiàn)方式 Optional<Integer> result2 = Stream.of("fo", "bar", "hello")         .map(new Function<String, Integer>() {           @Override           public Integer apply(String s) {             return s.length();           }         })         .filter(new Predicate<Integer>() {           @Override           public boolean test(Integer l) {             return l <= 3;           }         })         .max(new Comparator<Integer>() {           @Override           public int compare(Integer o1, Integer o2) {             return o1 - o2;           }         });

Lambda表達(dá)式與匿名類的異同集中體現(xiàn)在以下三點(diǎn)上:

  • Lambda就是為了優(yōu)化匿名內(nèi)部類而生,Lambda要比匿名類簡(jiǎn)潔的多得多。

  • Lambda僅適用于函數(shù)式接口,匿名類不受限。

  • 即匿名類中的this是“匿名類對(duì)象”本身;Lambda表達(dá)式中的this是指“調(diào)用Lambda表達(dá)式的對(duì)象”。

函數(shù)接口

實(shí)際上,上面一段代碼中的Function、Predicate、Comparator都是函數(shù)接口。我們知道,C語(yǔ)言支持函數(shù)指針,它可以把函數(shù)直接當(dāng)變量來(lái)使用。

但是,Java沒(méi)有函數(shù)指針這樣的語(yǔ)法。所以它通過(guò)函數(shù)接口,將函數(shù)包裹在接口中,當(dāng)作變量來(lái)使用。實(shí)際上,函數(shù)接口就是接口。不過(guò),它也有自己特別的地方,那就是要求只包含一個(gè)未實(shí)現(xiàn)的方法。因?yàn)橹挥羞@樣,Lambda表達(dá)式才能明確知道匹配的是哪個(gè)方法。如果有兩個(gè)未實(shí)現(xiàn)的方法,并且接口入?yún)?、返回值都一樣,那Java在翻譯Lambda表達(dá)式的時(shí)候,就不知道表達(dá)式對(duì)應(yīng)哪個(gè)方法了。

函數(shù)式接口也是Java interface的一種,但還需要滿足:

  • 一個(gè)函數(shù)式接口只有一個(gè)抽象方法(single abstract method);

  • Object類中的public abstract method不會(huì)被視為單一的抽象方法;

  • 函數(shù)式接口可以有默認(rèn)方法和靜態(tài)方法;

  • 函數(shù)式接口可以用@FunctionalInterface注解進(jìn)行修飾。

滿足這些條件的interface,就可以被視為函數(shù)式接口。例如Java 8中的Comparator接口:

@FunctionalInterface public interface Comparator<T> {     /**      * single abstract method      * @since 1.8      */     int compare(T o1, T o2);      /**      * Object類中的public abstract method       * @since 1.8      */     boolean equals(Object obj);      /**      * 默認(rèn)方法      * @since 1.8      */     default Comparator<T> reversed() {         return Collections.reverseOrder(this);     }           /**      * 靜態(tài)方法      * @since 1.8      */     public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {         return Collections.reverseOrder();     }      //省略... }

函數(shù)式接口有什么用呢?一句話,函數(shù)式接口帶給我們最大的好處就是:可以使用極簡(jiǎn)的lambda表達(dá)式實(shí)例化接口。為什么這么說(shuō)呢?我們或多或少使用過(guò)一些只有一個(gè)抽象方法的接口,比如Runnable、ActionListener、Comparator等等,比如我們要用Comparator實(shí)現(xiàn)排序算法,我們的處理方式通常無(wú)外乎兩種:

  • 規(guī)規(guī)矩矩的寫一個(gè)實(shí)現(xiàn)了Comparator接口的Java類去封裝排序邏輯。若業(yè)務(wù)需要多種排序方式,那就得寫多個(gè)類提供多種實(shí)現(xiàn),而這些實(shí)現(xiàn)往往只需使用一次。

  • 另外一種聰明一些的做法無(wú)外乎就是在需要的地方搞個(gè)匿名內(nèi)部類,比如:

public class Test {      public static void main(String args[]) {          List<Person> persons = new ArrayList<Person>();         Collections.sort(persons, new Comparator<Person>(){             @Override             public int compare(Person o1, Person o2) {                 return Integer.compareTo(o1.getAge(), o2.getAge());             }         });     }  }

匿名內(nèi)部類實(shí)現(xiàn)的代碼量沒(méi)有多到哪里去,結(jié)構(gòu)也還算清晰。Comparator接口在Jdk  1.8的實(shí)現(xiàn)增加了FunctionalInterface注解,代表Comparator是一個(gè)函數(shù)式接口,使用者可放心的通過(guò)lambda表達(dá)式來(lái)實(shí)例化。那我們來(lái)看看使用lambda表達(dá)式來(lái)快速new一個(gè)自定義比較器所需要編寫的代碼:

Comparator<Person> comparator = (p1, p2) -> Integer.compareTo(p1.getAge(), p2.getAge());

-> 前面的 () 是Comparator接口中compare方法的參數(shù)列表,-> 后面則是compare方法的方法體。

下面將Java提供的Function、Predicate這兩個(gè)函數(shù)接口的源碼,摘抄如下:

@FunctionalInterface public interface Function<T, R> {     R apply(T t);  // 只有這一個(gè)未實(shí)現(xiàn)的方法      default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {         Objects.requireNonNull(before);         return (V v) -> apply(before.apply(v));     }      default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {         Objects.requireNonNull(after);         return (T t) -> after.apply(apply(t));     }      static <T> Function<T, T> identity() {         return t -> t;     } }  @FunctionalInterface public interface Predicate<T> {     boolean test(T t); // 只有這一個(gè)未實(shí)現(xiàn)的方法      default Predicate<T> and(Predicate<? super T> other) {         Objects.requireNonNull(other);         return (t) -> test(t) && other.test(t);     }      default Predicate<T> negate() {         return (t) -> !test(t);     }      default Predicate<T> or(Predicate<? super T> other) {         Objects.requireNonNull(other);         return (t) -> test(t) || other.test(t);     }      static <T> Predicate<T> isEqual(Object targetRef) {         return (null == targetRef)                 ? Objects::isNull                 : object -> targetRef.equals(object);     } }

@FunctionalInterface注解使用場(chǎng)景

我們知道,一個(gè)接口只要滿足只有一個(gè)抽象方法的條件,即可以當(dāng)成函數(shù)式接口使用,有沒(méi)有 @FunctionalInterface  都無(wú)所謂。但是jdk定義了這個(gè)注解肯定是有原因的,對(duì)于開發(fā)者,該注解的使用一定要三思而后續(xù)行。

如果使用了此注解,再往接口中新增抽象方法,編譯器就會(huì)報(bào)錯(cuò),編譯不通過(guò)。換句話說(shuō),@FunctionalInterface  就是一個(gè)承諾,承諾該接口世世代代都只會(huì)存在這一個(gè)抽象方法。因此,凡是使用了這個(gè)注解的接口,開發(fā)者可放心大膽的使用Lambda來(lái)實(shí)例化。當(dāng)然誤用  @FunctionalInterface  帶來(lái)的后果也是極其慘重的:如果哪天你把這個(gè)注解去掉,再加一個(gè)抽象方法,則所有使用Lambda實(shí)例化該接口的客戶端代碼將全部編譯錯(cuò)誤。

特別地,當(dāng)某接口只有一個(gè)抽象方法,但沒(méi)有用 @FunctionalInterface  注解修飾時(shí),則代表別人沒(méi)有承諾該接口未來(lái)不增加抽象方法,所以建議不要用Lambda來(lái)實(shí)例化,還是老老實(shí)實(shí)的用以前的方式比較穩(wěn)妥。

關(guān)于Java中怎么支持函數(shù)式編程就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI