Java 函数式编程入门
Java 函數式編程入門
- 函數式編程實戰
- 改進
- 完整代碼
??像 JavaScript 這種語言很早就支持閉包了,雖然 C++ 很早就有了函數指針,Java 也很早就提供了反射中的 Method 類,不過使用它們都不能算是真正的函數式編程(面向函數編程)。原因它們還不夠方便和優雅。編程語言是為人類設計的語言,如果僅僅為了可實現,那任何編程思想、設計模式、架構模式都沒有意義。
??Java 從 Java 8 開始支持 lambda 表達式,這才算是支持函數式編程。函數式編程有什么好處呢?如果將其與依賴注入技術結合,可以很好地遵守開閉原則,實現控制反轉,便于異步調用等等。在事件驅動模型中常常應用這一技術。
??舉個例子。原始的系統對外提供的 API(方法),其實參是此系統的輸入、返回值是此系統的輸出,且需要該系統的使用者先得到運行,然后該系統的使用者主動令該系統得到運行。
??如果使用函數式編程,則該系統的實參可以是一個函數,且該系統的輸入與輸出都可以在實參提供與接收。另外,可以實現該系統與該系統的使用者之間的解耦,令該系統與該系統的使用者的運行沒有主次關系。在構造算法的時候,甚至都可以無需提前知曉具體的業務,以開閉原則完成本算法的實現。
函數式編程實戰
??上面的解釋太抽象了,還是使用具體的代碼實現意義會更大。
??這里構造一個黑盒系統,該系統是一個虛假的服務器,對外提供一個回調方法。每隔一段時間,該服務器就會調用這個回調方法來通知外界自己接收到了信息,并傳遞這個信息。
??在給出具體的代碼之前,需要介紹以下這些概念:鉤子(hook)、處理器(handler)。鉤子是系統對外提供的一個回調方法,該系統的使用方負責提供該方法的實現。處理器是系統內部這個回調方法的調用方。更多的信息,可見筆者的另一篇博客:
代理、委托、鉤子與打樁:
https://blog.csdn.net/wangpaiblog/article/details/115436520
??為了使用代碼更簡潔,這里使用了 Lombok,不過本文不打算詳細介紹 Lombok。
??這里使用 事件驅動模型 來描述這一情景。當服務器收到信息時,它就會為之生成一個事件(event),此事件中包含了 API 調用方與 API 內部之間交互的必要信息,然后該服務器會調用外界使用方提供的那個回調方法,并將此事件作為實參傳遞。
??在 JavaScript 中實現上述情景相當簡單,不過這在 Java 中略顯麻煩。首先,需要定義一個事件類。這個類的字段中儲存了需要傳遞該使用方的必要信息。
package org.wangpai.demo.fp.blackbox.event;import java.util.Map; import lombok.Getter;@Getter public class Event {private Map<Object, Object> data; // value 為具體的數據,key 為為 value 而起的名字public Event(Map<Object, Object> data) {super();this.data = data;} }??然后,需要構造一個處理器。構造處理器的目的不僅是為了調用回調方法,更重要的是為了儲存這個回調方法。這里因為 Java 是完全的面向對象語言,數據的最小粒度是對象,因此外界傳入的回調方法需要通過使用一個對象來保存。
??用對象來保存方法?這看來是一個新穎的說法,但實際上,現在的高級編程語言基本上都提供了這樣的功能。世界上的任何活動都可以歸結為數據以及對數據的操作,這實際上就是面向對象中的字段與方法。因此,如果能使用對象,那就基本上可以干任何事情。
??注意,為了能使用 Lambda 表達式這種語法糖,處理器需要是一個函數式接口。
package org.wangpai.demo.fp.blackbox.handler;import org.wangpai.demo.fp.blackbox.event.Event;@FunctionalInterface public interface Handler {void handle(Event event); }??現在可以開始構造一個虛假的服務器了。該服務器要作的事情很簡單:每隔一段時間調用一次回調方法,通知使用方接收到了信息。
package org.wangpai.demo.fp.blackbox;import java.util.HashMap; import lombok.Setter; import lombok.SneakyThrows; import org.wangpai.demo.fp.blackbox.event.Event; import org.wangpai.demo.fp.blackbox.handler.Handler;@Setter public class MockServer {private Handler onReceiveHandler;@SneakyThrowspublic void start() {System.out.println("---方法 start 開始調用---");for (int index = 1; index <= 10; ++index) {Thread.sleep(1000); // 每次休眠 1 秒if (onReceiveHandler == null) {continue; // 如果使用者沒有提供回調,什么也不做}var msgData = new HashMap<String, Object>(1);msgData.put("text", "接收到第 " + index + " 條信息");onReceiveHandler.handle(new Event(msgData));}System.out.println("***方法 start 結束調用***");} }??寫完服務器的代碼就可以進行測試了。測試很簡單,模擬服務器的使用方,對服務器接收到的信息進行控制臺輸出。
package org.wangpai.demo.fp.blackbox;import java.util.concurrent.Executors;public class MockServerTest {public static void main(String[] args) {var server = new MockServer();server.setOnReceiveHandler(event -> System.out.println("來自服務器的反饋:" + event.getData().get("text")));Executors.newCachedThreadPool().execute(() -> server.start());System.out.println("***方法 main 結束調用***");} }??運行結果如下:
??可以看出,對于服務器的使用方,僅僅需要提供一個回調即可實現接收與服務器主動傳入的信息。
改進
??上述的代碼雖然已經實現了 事件驅動 功能,不過也有此不足之處。如果服務器類存在很多個回調,那使用方就需要實現很多個回調方法。對使用方來說,這很容易造成遺漏。雖然服務器可以為每個回調提供一種默認實現,不過有些場景下要求使用方一定要提供實現。
??實現這個需求最好的方法是將服務器所需的所有回調放入一種 抽象類 中。很多編程語言都提供了抽象類這種功能。對于抽象類來說,它強制要求非抽象子類實現它的所有抽象方法。而在 Java 中,更好的方式是使用接口。在這里,這個接口不妨叫就做 Hooks。
package org.wangpai.demo.fp.blackbox.hook;import org.wangpai.demo.fp.blackbox.event.Event;public interface Hooks {void onReceiveData(Event event);void onDestroy(Event event); }??現在,需要一個類來將這整個接口中的這些個回調方法與各個處理器相應對應,這個類不妨叫就做 Handlers 類。
package org.wangpai.demo.fp.blackbox.handler;import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import org.wangpai.demo.fp.blackbox.hook.Hooks;@Setter(AccessLevel.PROTECTED) @Getter(AccessLevel.PROTECTED) @Accessors(chain = true) public abstract class Handlers {private Handler onReceiveHandler;private Handler onDestroyHandler;public Handlers setHooks(Hooks hooks) {this.onReceiveHandler = hooks::onReceive;this.onDestroyHandler = hooks::onDestroy;return this;} }??上面的代碼看起來很像 C++ 代碼,但很遺憾,它就是 Java 代碼。上面使用的雙冒號::語法叫做方法引用(method reference)。出于本文的重點,這里不做詳解。
??實現了上面的 Handlers 類之后,服務器類可以選擇通過繼承或者組合來并入該類。簡單起見,這里選擇繼承。不過這里有個問題,服務器需要使用 getXXXHandler().handle(event) 來調用這個回調方法。雖然可以這樣做,但這太底層了,最好是來構造一個類來過渡一下。有人認為這沒有必要,但每個人在使用其他人的代碼的時候,都想著盡量可以不需要看對方的源碼。起一個好的名稱,提供邏輯更通順簡單的接口能大大減少使用者的工作量。這里選擇再構造一個類進行過渡。
package org.wangpai.demo.fp.blackbox;import org.wangpai.demo.fp.blackbox.event.Event; import org.wangpai.demo.fp.blackbox.handler.Handler; import org.wangpai.demo.fp.blackbox.handler.Handlers;public abstract class OnServerAction extends Handlers {public final void setOnReceive(Handler handler) {this.setOnReceiveHandler(handler);}public final void onReceive(Event event) {this.handle(this.getOnReceiveHandler(), event);}public final void setOnDestroy(Handler handler) {this.setOnDestroyHandler(handler);}public final void onDestroy(Event event) {this.handle(this.getOnDestroyHandler(), event);}private void handle(Handler handler, Event event) {if (handler != null) {handler.handle(event);}} }??另外,上面 Event 的 Map 中的 key 值似乎太隨意了,而且也不便于管理。最好是能夠約定 Event 攜帶的數據到底可以是哪些類型。
package org.wangpai.demo.fp.blackbox.event;public enum DataType {TEXT,BINARY }??相應的類 Event 修改如下:
package org.wangpai.demo.fp.blackbox.event;import java.util.HashMap; import java.util.Map;public class Event {private Map<DataType, Object> data;private Event() {super();this.data = new HashMap<>(2);}public Object getData(DataType dataType) {return data.get(dataType);}public Event setData(DataType dataType, Object data) {this.data.put(dataType, data);return this;}public static Event getInstance() {return new Event();} }??但是,Event 內部是用 Map 來存儲數據的,如果事先知道傳輸的數據類型(Map 中的 key 值)呢?可以選擇遍歷 Map,看看都儲存了哪些數據,但是這樣做的耦合度太高了。
??一個解決辦法是,令 Event 的發送方與接收方事先約定 Event 會攜帶哪些字段。
??另一個解決辦法是,使用一個所謂的 Head-Content 協議。這種傳輸方式要求使用額外的空間來記錄所傳輸的數據的一些重要信息。為此,可以將上述 EventType 修改為如下:
package org.wangpai.demo.fp.blackbox.event;public enum DataType {HEAD,TEXT,BINARY }??然后讓服務器在傳輸時,將數據與數據的類型一起傳輸。
??對于本示例中的簡單情形,看不出這樣做的明顯好處,這看起來與直接遍歷 Map 沒有區別,甚至可以 List 或者直接用 Object 來代替 Map。不過,如果傳輸的數據個數有很多且有冗余,或者需要以職責鏈模式來依次處理 Event 中的數據,這樣做就能保證分層處理數據時的井然有序。
package org.wangpai.demo.fp.blackbox;import lombok.SneakyThrows; import org.wangpai.demo.fp.blackbox.event.DataType; import org.wangpai.demo.fp.blackbox.event.Event;public class MockServer extends OnServerAction {@SneakyThrowspublic void start() {System.out.println("---方法 start 開始調用---");for (int index = 1; index <= 10; ++index) {Thread.sleep(1000); // 每次休眠 1 秒if (this.getOnReceiveHandler() == null) {continue; // 如果使用者沒有提供回調,什么也不做}var event = Event.getInstance();var dataType = DataType.TEXT;event.setData(DataType.HEAD, dataType);event.setData(dataType, "接收到第 " + index + " 條信息");this.onReceive(event);}if (this.getOnDestroyHandler() != null) {this.onDestroy(null);}System.out.println("***方法 start 結束調用***");} }
??對于使用方,只需要構造一個 Hooks 對象。這需要實現其中的所有方法,這就能防止使用方忘記實現某個回調方法。
package org.wangpai.demo.fp.blackbox;import java.util.concurrent.Executors; import org.wangpai.demo.fp.blackbox.event.DataType; import org.wangpai.demo.fp.blackbox.event.Event; import org.wangpai.demo.fp.blackbox.hook.Hooks;public class MockServerTest {public static void main(String[] args) {final var executor = Executors.newCachedThreadPool();var hooks = new Hooks() {@Overridepublic void onReceive(Event event) {var dataType = (DataType) event.getData(DataType.HEAD);System.out.println("來自服務器的反饋:" + event.getData(dataType));}@Overridepublic void onDestroy(Event event) {System.out.println("服務器停止信息接收");executor.shutdown();}};var server = new MockServer();server.setHooks(hooks);executor.execute(() -> server.start());System.out.println("***方法 main 結束調用***");} }??運行結果如下:
完整代碼
??已上傳至 GitCode 中,可免費下載:https://gitcode.net/wangpaiblog/20220201-functional_programming
總結
以上是生活随笔為你收集整理的Java 函数式编程入门的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 同步阻塞、同步非阻塞、异步阻塞、异步非阻
- 下一篇: autoflowchart软件使用步骤_