Java如何支持函數式編程?

2021-02-13 阿里技術

阿里妹導讀:Java是面向對象的語言,無法直接調用一個函數。Java 8開始,引入了函數式編程接口與Lambda表達式,便於開發者寫出更少更優雅的代碼。什麼是函數式編程?函數式編程的特點是什麼?本文通過代碼實例,從Stream類、Lambda表達式和函數接口這三個語法概念來分享Java對函數式編程的支持。

文末福利:Java微服務沙箱體驗挑戰。

在很長的一段時間裡,Java一直是面向對象的語言,一切皆對象,如果想要調用一個函數,函數必須屬於一個類或對象,然後在使用類或對象進行調用。但是在其它的程式語言中,如JS、C++,我們可以直接寫一個函數,然後在需要的時候進行調用,既可以說是面向對象編程,也可以說是函數式編程。從功能上來看,面向對象編程沒什麼不好的地方,但是從開發的角度來看,面向對象編程會多寫很多可能是重複的代碼行。比如創建一個Runnable的匿名類的時候:
Runnable runnable = new Runnable() {    @Override    public void run() {        System.out.println("do something...");    }};

這一段代碼中真正有用的只有run方法中的內容,剩餘的部分都是屬於Java程式語言的結構部分,沒什麼用,但是要寫。幸運的是Java 8開始,引入了函數式編程接口與Lambda表達式,幫助我們寫更少更優雅的代碼:
Runnable runnable = () -> System.out.println("do something...");

現在主流的編程範式主要有三種,面向過程、面向對象和函數式編程。函數式編程並非一個很新的東西,早在50多年前就已經出現了。近幾年,函數式編程越來越被人關注,出現了很多新的函數式程式語言,比如Clojure、Scala、Erlang等。一些非函數式程式語言也加入了很多特性、語法、類庫來支持函數式編程,比如Java、Python、Ruby、JavaScript等。除此之外,Google Guava也有對函數式編程的增強功能。函數式編程因其編程的特殊性,僅在科學計算、數據處理、統計分析等領域,才能更好地發揮它的優勢,所以它並不能完全替代更加通用的面向對象編程範式。但是作為一種補充,它也有很大存在、發展和學習的意義。函數式編程的英文翻譯是Functional Programming。那到底什麼是函數式編程呢?實際上,函數式編程沒有一個嚴格的官方定義。嚴格上來講,函數式編程中的「函數」,並不是指我們程式語言中的「函數」概念,而是指數學「函數」或者「表達式」(例如:y=f(x))。不過,在編程實現的時候,對於數學「函數」或「表達式」,我們一般習慣性地將它們設計成函數。所以,如果不深究的話,函數式編程中的「函數」也可以理解為程式語言中的「函數」。每個編程範式都有自己獨特的地方,這就是它們會被抽象出來作為一種範式的原因。面向對象編程最大的特點是:以類、對象作為組織代碼的單元以及它的四大特性。面向過程編程最大的特點是:以函數作為組織代碼的單元,數據與方法相分離。那函數式編程最獨特的地方又在哪裡呢?實際上,函數式編程最獨特的地方在於它的編程思想。函數式編程認為程序可以用一系列數學函數或表達式的組合來表示。函數式編程是程序面向數學的更底層的抽象,將計算過程描述為表達式。不過,這樣說你肯定會有疑問,真的可以把任何程序都表示成一組數學表達式嗎?理論上講是可以的。但是,並不是所有的程序都適合這麼做。函數式編程有它自己適合的應用場景,比如科學計算、數據處理、統計分析等。在這些領域,程序往往比較容易用數學表達式來表示,比起非函數式編程,實現同樣的功能,函數式編程可以用很少的代碼就能搞定。但是,對於強業務相關的大型業務系統開發來說,費勁吧啦地將它抽象成數學表達式,硬要用函數式編程來實現,顯然是自討苦吃。相反,在這種應用場景下,面向對象編程更加合適,寫出來的代碼更加可讀、可維護。再具體到編程實現,函數式編程跟面向過程編程一樣,也是以函數作為組織代碼的單元。不過,它跟面向過程編程的區別在於,它的函數是無狀態的。何為無狀態?簡單點講就是,函數內部涉及的變量都是局部變量,不會像面向對象編程那樣,共享類成員變量,也不會像面向過程編程那樣,共享全局變量。函數的執行結果只與入參有關,跟其他任何外部變量無關。同樣的入參,不管怎麼執行,得到的結果都是一樣的。這實際上就是數學函數或數學表達式的基本要求。舉個例子:
int b;int increase(int a) {  return a + b;}
int increase(int a, int b) { return a + b;}

不同的編程範式之間並不是截然不同的,總是有一些相同的編程規則。比如不管是面向過程、面向對象還是函數式編程,它們都有變量、函數的概念,最頂層都要有main函數執行入口,來組裝編程單元(類、函數等)。只不過,面向對象的編程單元是類或對象,面向過程的編程單元是函數,函數式編程的編程單元是無狀態函數。實現面向對象編程不一定非得使用面向對象程式語言,同理,實現函數式編程也不一定非得使用函數式程式語言。現在,很多面向對象程式語言,也提供了相應的語法、類庫來支持函數式編程。Java這種面向對象程式語言,對函數式編程的支持可以通過一個例子來描述:
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());   }}

這段代碼的作用是從一組字符串數組中,過濾出長度小於等於3的字符串,並且求得這其中的最大長度。Java為函數式編程引入了三個新的語法概念:Stream類、Lambda表達式和函數接口(Functional Inteface)。Stream類用來支持通過「.」級聯多個函數操作的代碼編寫方式;引入Lambda表達式的作用是簡化代碼編寫;函數接口的作用是讓我們可以把函數包裹成函數接口,來實現把函數當做參數一樣來使用(Java 不像C那樣支持函數指針,可以把函數直接當參數來使用)。假設我們要計算這樣一個表達式:(3-1)*2+5。如果按照普通的函數調用的方式寫出來,就是下面這個樣子:
add(multiply(subtract(3,1),2),5);

不過,這樣編寫代碼看起來會比較難理解,我們換個更易讀的寫法,如下所示:
subtract(3,1).multiply(2).add(5);

在Java中,「.」表示調用某個對象的方法。為了支持上面這種級聯調用方式,我們讓每個函數都返回一個通用的Stream類對象。在Stream類上的操作有兩種:中間操作和終止操作。中間操作返回的仍然是Stream類對象,而終止操作返回的是確定的值結果。再來看之前的例子,對代碼做了注釋解釋。其中map、filter是中間操作,返回Stream類對象,可以繼續級聯其他操作;max是終止操作,返回的不是Stream類對象,無法再繼續往下級聯處理了。
public class Demo {  public static void main(String[] args) {    Optional<Integer> result = Stream.of("f", "ba", "hello")             .map(s -> s.length())             .filter(l -> l <= 3)             .max((o1, o2) -> o1-o2);     System.out.println(result.get());   }}

前面提到Java引入Lambda表達式的主要作用是簡化代碼編寫。實際上,我們也可以不用Lambda表達式來書寫例子中的代碼。我們拿其中的map函數來舉例說明。下面三段代碼,第一段代碼展示了map函數的定義,實際上,map函數接收的參數是一個Function接口,也就是函數接口。第二段代碼展示了map函數的使用方式。第三段代碼是針對第二段代碼用Lambda表達式簡化之後的寫法。實際上,Lambda表達式在Java中只是一個語法糖而已,底層是基於函數接口來實現的,也就是第二段代碼展示的寫法。
public interface Stream<T> extends BaseStream<T, Stream<T>> {  <R> Stream<R> map(Function<? super T, ? extends R> mapper);  }
Stream.of("fo", "bar", "hello").map(new Function<String, Integer>() { @Override public Integer apply(String s) { return s.length(); }});
Stream.of("fo", "bar", "hello").map(s -> s.length());

Lambda表達式包括三部分:輸入、函數體、輸出。表示出來的話就是下面這個樣子:
(a, b) -> { 語句1;語句2;...; return 輸出; } 

實際上,Lambda表達式的寫法非常靈活。上面給出的是標準寫法,還有很多簡化寫法。比如,如果輸入參數只有一個,可以省略 (),直接寫成 a->{…};如果沒有入參,可以直接將輸入和箭頭都省略掉,只保留函數體;如果函數體只有一個語句,那可以將{}省略掉;如果函數沒有返回值,return語句就可以不用寫了。
Optional<Integer> result = Stream.of("f", "ba", "hello")        .map(s -> s.length())        .filter(l -> l <= 3)        .max((o1, o2) -> o1-o2);        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表達式與匿名類的異同集中體現在以下三點上:實際上,上面一段代碼中的Function、Predicate、Comparator都是函數接口。我們知道,C語言支持函數指針,它可以把函數直接當變量來使用。但是,Java沒有函數指針這樣的語法。所以它通過函數接口,將函數包裹在接口中,當作變量來使用。實際上,函數接口就是接口。不過,它也有自己特別的地方,那就是要求只包含一個未實現的方法。因為只有這樣,Lambda表達式才能明確知道匹配的是哪個方法。如果有兩個未實現的方法,並且接口入參、返回值都一樣,那Java在翻譯Lambda表達式的時候,就不知道表達式對應哪個方法了。函數式接口也是Java interface的一種,但還需要滿足:滿足這些條件的interface,就可以被視為函數式接口。例如Java 8中的Comparator接口:
@FunctionalInterfacepublic interface Comparator<T> {        int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() { return Collections.reverseOrder(this); }
public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() { return Collections.reverseOrder(); }
}

函數式接口有什麼用呢?一句話,函數式接口帶給我們最大的好處就是:可以使用極簡的lambda表達式實例化接口。為什麼這麼說呢?我們或多或少使用過一些只有一個抽象方法的接口,比如Runnable、ActionListener、Comparator等等,比如我們要用Comparator實現排序算法,我們的處理方式通常無外乎兩種:
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());            }        });    } }

匿名內部類實現的代碼量沒有多到哪裡去,結構也還算清晰。Comparator接口在Jdk 1.8的實現增加了FunctionalInterface註解,代表Comparator是一個函數式接口,使用者可放心的通過lambda表達式來實例化。那我們來看看使用lambda表達式來快速new一個自定義比較器所需要編寫的代碼:
Comparator<Person> comparator = (p1, p2) -> Integer.compareTo(p1.getAge(), p2.getAge());

-> 前面的 () 是Comparator接口中compare方法的參數列表,-> 後面則是compare方法的方法體。下面將Java提供的Function、Predicate這兩個函數接口的源碼,摘抄如下:
@FunctionalInterfacepublic interface Function<T, R> {    R apply(T t);  
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; }}
@FunctionalInterfacepublic interface Predicate<T> { boolean test(T t);
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註解使用場景我們知道,一個接口只要滿足只有一個抽象方法的條件,即可以當成函數式接口使用,有沒有 @FunctionalInterface 都無所謂。但是jdk定義了這個註解肯定是有原因的,對於開發者,該註解的使用一定要三思而後續行。如果使用了此註解,再往接口中新增抽象方法,編譯器就會報錯,編譯不通過。換句話說,@FunctionalInterface 就是一個承諾,承諾該接口世世代代都只會存在這一個抽象方法。因此,凡是使用了這個註解的接口,開發者可放心大膽的使用Lambda來實例化。當然誤用 @FunctionalInterface 帶來的後果也是極其慘重的:如果哪天你把這個註解去掉,再加一個抽象方法,則所有使用Lambda實例化該接口的客戶端代碼將全部編譯錯誤。特別地,當某接口只有一個抽象方法,但沒有用 @FunctionalInterface 註解修飾時,則代表別人沒有承諾該接口未來不增加抽象方法,所以建議不要用Lambda來實例化,還是老老實實的用以前的方式比較穩妥。函數式編程更符合數學上函數映射的思想。具體到程式語言層面,我們可以使用Lambda表達式來快速編寫函數映射,函數之間通過鏈式調用連接到一起,完成所需業務邏輯。Java的Lambda表達式是後來才引入的,由於函數式編程在並行處理方面的優勢,正在被大量應用在大數據計算領域。


Java微服務沙箱體驗挑戰

10分鐘搭建一個Task Manager任務管理器

使用阿里雲Java工程腳手架一鍵生成你的代碼框架,並通過場景體驗,學習使用微服務構建一套簡單的分布式應用,最終實現一款代辦事項管理軟體。完成4個實驗,通過Java基礎和體驗相關知識自測還可領取start.aliyun.com x IntelliJ聯名小禮物!

點擊「閱讀原文」,立即去挑戰吧!      

相關焦點

  • 為什麼函數式編程在Java中很危險?
    在我的日常工作中,我身邊的開發者大多是畢業於CS編程頂級院校比如MIT、CMU以及Chicago,他們初次涉及的語言是Haskell、Scheme及Lisp。他們認為函數式編程是一種自然的、直觀的、美麗的且高效的編程樣式。
  • 自從學會Java中的lambda表達式和函數式編程技巧,再也不用加班了!
    題記本文將分享如何在Java程序中使用lambda表達式和函數式編程技巧在Java SE 8之前,匿名類通常用於將功能傳遞給方法。這種做法混淆了原始碼,使代碼難以理解***。Java 8通過引入lambda來解決這個問題。本教程首先介紹lambda的語言特性,然後詳細的介紹了如何使用lambda表達式並根據目標類型進行函數式編程。
  • 函數式編程二 異常處理
    基礎函數式編程一在java中用函數式的方式去做事情,Happy Path確實很好玩,但是編程中最不好玩的就是異常的情況。
  • Kotlin函數式編程
    那麼在函數式編程中當然一切皆是函數。在Kotlin中函數式的地位和對象一樣高,你可以在方法中輸入函數,也可以返回函數。函數式編程FP特徵:函數式編程核心概念:函數是「一等公民」:是指函數與其他數據類型是一樣的,處於平等地位。函數可以作為其他函數的參數傳入,也可以作為其他函數的返回值返回。
  • 函數式編程,真香
    最近在研究函數式編程,真的是在學習的過程中感覺自己的思維提升了很多,抽象能力大大的提高了,讓我深深的感受到了函數式編程的魅力。所以我打算後面用 5 到 8 篇的篇幅,詳細的介紹一下函數式編程的思想,基礎、如何設計、測試等。今天這篇文章主要介紹函數式編程的思想。函數式編程有用嗎?什麼是函數式編程?函數式編程的優點。
  • java8的函數式編程解析
    其實在java8就已經有java的函數式編程寫法,只是難度較大,大家都習慣了對象式用法,但在其它語言中都有函數式的用法,如js,scala,函數式其實是抽象到極致的思想。什麼是函數式編程 函數式編程並不是Java新提出的概念,其與指令編程相比,強調函數的計算比指令的計算更重要;與過程化編程相比,其中函數的計算可以隨時調用。
  • 高階函數與函數式編程
    根據程式語言理論,一等對象必須滿足以下條件:Python 函數同時滿足這幾個條件,因而也被稱為 一等函數 。高階函數 則是指那些以函數為參數,或者將函數作為結果返回的函數。對高階函數稍加利用,便能玩出很多花樣來。本節從一些典型的案例入手,講解 Python 函數高級用法。
  • 函數式編程
    ,我們會看到如下函數式編程的長相:函數式編程的三大特性:immutable data 不可變數據:像Clojure一樣,默認上變量是不可變的,如果你要改變變量,你需要把變量copy出去修改。函數式編程的幾個技術map & reduce :這個技術不用多說了,函數式編程最常見的技術就是對一個集合做Map和Reduce操作。這比起過程式的語言來說,在代碼上要更容易閱讀。
  • 大數據入門:Scala函數式編程
    命令式編程VS函數式編程命令式編程,程序邏輯的基本元素是:變量+操作符+控制結構,這些元素構成一條條的代碼指令,因此,稱之為命令式編程。函數式編程,程序邏輯由:map+匿名函數組成。整個代碼中,看不到變量、控制結構、操作符等元素,看到的只有函數,因此,函數式編程的本質就是:程序邏輯的基本元素是函數。
  • Golang 函數式編程簡述
    函數式編程,是指忽略(通常是不允許)可變數據(以避免它處可改變的數據引發的邊際效應),忽略程序執行狀態(不允許隱式的、隱藏的、不可見的狀態),通過函數作為入參,函數作為返回值的方式進行計算,通過不斷的推進(迭代、遞歸)這種計算,從而從輸入得到輸出的編程範式。在函數式編程範式中,沒有過程式編程所常見的概念:語句,過程控制(條件,循環等等)。
  • Java 8裡面 lambda 的最佳實踐
    在8裡面Lambda是最火的主題,不僅僅是因為語法的改變,更重要的是帶來了函數式編程的思想,我覺得優秀的程式設計師,有必要學習一下函數式編程的思想以開闊思路。所以這篇文章聊聊Lambda的應用場景,性能,也會提及下不好的一面。Java為何需要Lambda1996年1月,Java1.0發布了,此後計算機編程領域發生了翻天覆地的變化。
  • Python(27)常用指引:函數式編程指引
    函數式編程指引本文檔提供恰當的 Python 函數式編程範例,在函數式編程簡單的介紹之後,將簡單介紹Python中關於函數式編程的特性如 iterator
  • 現代C++函數式編程
    導讀: 本文作者從介紹函數式編程的概念入手,分析了函數式編程的表現形式和特性,最終通過現代C++的新特性以及一些模板雲技巧實現了一個非常靈活的pipeline
  • 【第1679其】函數式編程淺析
    最近花了比較久的一段時間看了一些函數式編程的資料,還在公司組內進行了一次關於函數式編程的分享,本文是分享對應的文字稿。註:很多語言都支持函數式編程,文中代碼已 JavaScript 為例。在閱讀下文之前,可以花一分鐘時間閱讀以下兩種風格的代碼,看看那一種更容易被理解。
  • 函數式編程聖經
    上帝看到約翰·麥卡錫發明了表處理語言 Lisp,卻只用來學術研究,很是傷心,就把 Lisp 解釋器的秘密告訴了他的學生史蒂芬·羅素,史蒂芬·羅素將eval函數在IBM 704機器上實現後,函數式編程的大門第一次向人類打開了。
  • 10分鐘學會python函數式編程
    在這篇文章裡,你將學會什麼是函數範式以及如何使用Python進行函數式編程。你也將了解列表推導和其它形式的推導。
  • Python中的函數式編程
    (英語:functional programming)或稱函數程序設計,又稱泛函編程,是一種編程範型,它將電腦運算視為數學上的函數計算,並且避免使用程序狀態以及易變對象。函數程式語言最重要的基礎是λ演算(lambda calculus)。而且λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。
  • 一文帶你了解什麼是JavaScript 函數式編程?
    函數式編程在前端已經成為了一個非常熱門的話題。在最近幾年裡,我們看到非常多的應用程式代碼庫裡大量使用著函數式編程思想。本文將略去那些晦澀難懂的概念介紹,重點展示在 JavaScript 中到底什麼是函數式的代碼、聲明式與命令式代碼的區別、以及常見的函數式模型都有哪些?
  • 支持多語言:Serverless雲函數如何解鎖語言限制?
    一、背景 SCF 作為騰訊雲 FaaS 核心產品,支持 javascript、python、php、java、go等多語言函數。但是,在用戶實際使用過程中,我們發現了一些問題: 1.
  • 函數式編程很難,所以你要學習它
    很 奇怪不是,很少有人每天都使用函數式程式語言。如果你用Scala,Haskell,Erlang,F#或某個Lisp方言來編程,很可能沒有公司會花錢 聘你。這個行業裡的絕大部分人都是使用像Python,Ruby,Java或C#等面向對象的程式語言——它們用起來很順手。不錯,你也許會偶然用到一兩 個「函數式語言特徵」,例如「block」,但人們不會去做函數式編程。