Java中的回調函數

ioc-aop.png



    function foo(callback) {
        callback();
    }

    function bar() {
        // do something ...
    }

    foo(bar);

相信你一定看過上面這種形式的代碼,沒錯,這就是在動態語言中被頻繁運用到的[回調函數](“https://zh.wikipedia.org/wiki/%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0“)。
C、C++和Pascal中也允許將函數作爲指針傳遞

那麼問題來了,如何使用沒有函數類型的Java語言,寫出一個回調函數呢?

Why Callback?

爲什麼要使用回調函數呢?或者換個方式提問,使用回調函數有什麼好處呢?

事件回調

定義一個Person 的類,調用它的 greeting()方法。


    let Person = (function() {

        // 構造
        function Person(name) {
            this.name = name;
        }

        Person.prototype.name;
        Person.prototype.say = () => {
            return "說:"
        };

        Person.prototype.greeting = () => {
            let greets = "你好!";
            console.log(this.name + this.say() + greets);
        };

        return Person;
    }());

    let ming = new Person("小明");
    ming.greeting();

    // 執行結果
    > 小明說:你好!

如果天氣會影響到小明的心情,那麼也許打招呼的方式也會不同。我把greeting這個方法重寫一下,讓一個函數傳入,看看會有什麼不一樣的地方。


    // override
    Person.prototype.greeting = (greetingByWeather) => {
        let greets = "你好!";
        greetingByWeather(greets);
    };

    ming.greeting((greets) => {
        console.log("今天是個大晴天!");
        console.log(ming.name + ming.say + greets);
    });

    ming.greeting((greets) => {
        console.log("今天下了小雨!");
        console.log(ming.name + "沒有出門");
    });

    // 執行結果
    > 今天是個大晴天!
    > 小明說:你好!
    > 今天下了小雨!
    > 小明沒有出門

OMG!很明顯,小雨天對小明來說不太友好了,所以他選擇宅在家裏。這次,兩個不同回調函數導致了這個打招呼的行爲改變。
這種回調方式在JavaScript中被稱作同步回調

這種把自身的全部或部分邏輯交給回調函數處理的方式,可以讓寫代碼的人擺脫繁瑣的if……else……代碼塊,帶來簡潔、靈活、高效的書寫體驗。但是,這種方法也會帶來可讀性差等缺點。不過,這個不在這篇文章的討論範圍中。

相同的對象,調用其相同的方法,參數也相同時,但表現的行爲卻不同。是不是讓你浮想連篇……這不就是Java中常說的多態嘛!

代理回調

重新再定義一個Person的類,調用它的 greeting()方法。


    class Person(object):

        # 構造函數
        def __init__(self, name):
            self.name = name;

        def greeting(self):
            print(self.name + self.say(), "你好")

        def say(self):
            return "說:"


    ming = Person("小明")
    ming.greeting()

    # 執行結果
    >>> 小明說: 你好

這個時候,如果小明想要記下自己打招呼的時間,就有了


    def greeting(self):
        print("在", datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "時:")
        print(self.name + self.say(), "你好")

    # 輸出
    >>> 在 2018-07-12 17:06:38 時:
    >>> 小明說: 你好

但是,如果需要記錄 Person這個類中所有方法的執行時間。是不是要在所有方法裏添加這行代碼呢?

使用回調函數來處理這件事情,會有什麼不同呢?


    def record_time(func):
        print("在", datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "時:")
        return func()

    record_time(ming.greeting)

    # 輸出
    >>> 在 2018-07-12 17:19:54 時:
    >>> 小明說: 你好

這樣一來,我們重複性的代碼能減少許多,我們使用record_time()作爲一個代理,去調用我們真正的業務代碼。

Python給了這種代理函數一個好聽的名字,叫做 裝飾器 ,並且賦予了它特殊的書寫樣式。

現在我就來修改一下上述的代碼,讓它在調用時變得更加符合直覺。


    def record_time(func):
        def wrapper(*args, **kwargs):
            print("在", datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "時:")
            return func(*args, **kwargs)
        return wrapper

    def greeting(self):
        print(self.name + record_time_v2(self.say), "你好")

    @record_time
    def greeting_v2(self):
        print(self.name + self.say_v2(), "你好")

    ming.greeting = record_time(ming.greeting)
    ming.greeting()

    ming.greeting_v2()

把方法greeting_v2()想象成一張自左向右滾動的紙帶,然後用剪刀在紙帶上任意位置裁開一道口子,把我們所需要的東西織入紙帶。
以滿足我們日益增多的變態需求。

這些開口,就被稱之爲程序切面(Aspect),這種編程的思維被稱作面向切面編程(Aspect Oriented Programming)。

aop.png

Java 8 的函數指針

接口重載

通過接口重載,Java中可以很容易地實現”多態“。


    public interface MyInterface {

        void greeting();

    }

    public class MyInterfaceImpl implements MyInterface {

        @Override
        void greeting() {
            System.out.println("你好");
        }

    }

    public static void main(String args[]) {
        MyInterface i = new MyInterfaceImpl();
        i.greeting();
    }

接口MyInterface無法被實例化,但是被允許以參數的形式傳入方法中。在調用該方法時,調用方不得不去實現該接口內定義的方法。

Java中使用這種方法進行回調的例子很多,最典型的就是Thread這個對象。


    public static void main(String args[]){

        new Thread(new Runnable() {

            @Override
            public void run() {
                // your codes here
            }

        }).start(););

    }

當然,在Java程序規範中並不鼓勵以上這種寫法。面向對象的觀點傾向於抽象出一個具體的實現類,然後調用這個具體的實現類。(除非是用完之後就被使用者拋棄)

基於此,我們可以很容易的寫出這種類似回調函數的東西。


    interface GreetingByWeather {

        void greeting(Person person);

    }

    class Person {

        public String name;

        public Person () {};

        public Person (String name) {
            this.name = name;
        }

        public void greeting (GreetingByWeather greetingByWeather) {
            greetingByWeather.greeting(this);
        }

        public String say() {
            return "說:";
        }

    }

    public static void main(String args[]) {

        Person ming = new Person("小明");
        ming.greeting(new GreetingByWeather {

            @Override
            public void greeting(Person person) {
                System.out.println("今天是個大晴天!");
                System.out.print(person.name);
                System.out.print(person.say());
                System.out.println("你好!");
            }

        });

        ming.greeting(new GreetingByWeather {

            @Override
            public void greeting(Person person) {
                System.out.println("今天下起了雨!");
                System.out.print(person.name);
                System.out.println("沒有出門。");
            }

        });

    }

    // 運行結果
    今天是個大晴天!
    小明說:你好!

    今天下去了雨!
    小明沒有出門。

通過上面的例子,我們發現,接口GreetingByWeather定義了一個方法,傳入了一個Person參數,返回了一個結果(void)。這也意味着,如果我們沒有對接口進行聲明,那麼我們也無法正常地調用該函數(廢話blabla……)

如果,能在業務調用的時候聲明參數和返回值的類型,該有多好啊。

函數接口

Java 8 帶來了許多新特性,諸如枚舉類、lambda表達式、接口默認方法和函數接口等。其中,大部分都是基於原有的Java特性的增強和補充。而像lambda表達式之類的,更像是一種函數式編程的語法糖,它讓原有的函數式編程代碼更加簡潔。

java.util.function包下,多了許多函數式的接口聲明,它們也可以被近似地看做是一種語法糖。
而事實上,Java8爲了在函數式編程的方向上有所發展,放棄了簡單的接口重載,而是通過動態調用 **[invokeddynamic](https://stackoverflow.com/questions/30002380/why-are-java-8-lambdas-invoked-using-invokedynamic)** 來實現的。

下面以Function接口爲例。


    @FunctionalInterface
    public interface Function<T, R> {

        /**
        * Applies this function to the given argument.
        *
        * @param t the function argument
        * @return the function result
        */
        R apply(T t);


    public class Person {

        ...

        public void greeting(Function<Person, Boolean> function) {
            boolean result = function.apply(this);
            // do sth with result ……
        }

        ...

    }

    public static void main(String args[]) {

        Person ming = new Person("小明");
        ming.greeting(new Function<Person, String>() {
            @Override
            public String apply(Person person) {
                System.out.println(person.name + person.say() + "你好");
                return true;
            }
        });
    }

上面的實現還可以輕易被lambda表達式替代。


    ming.greeting(person -> {
        System.out.println(person.name + "沒有出門");
        return false;
    });

Function接口,允許我們在編寫具體業務代碼的同時,聲明傳入的參數和返回值類型。而我們也不必花費額外的開銷去聲明一個接口。這種書寫方式看上去和接口重載很像,但絕不一樣。

Java中的動態代理

前面我們已經提到過AOP編程。

而一提到AOP,就讓人聯想到Java中聲名顯赫的Spring框架。Spring帶來了管家式的編程體驗,幾乎接管了J2EE世界裏的一切組件。在某種程度上,我們討論Java Web編程的時候,就是在討論Spring框架。

而在這裏,我們脫離Spring框架,使用原生的Java代碼來實現動態代理。

定義接口和實現


    public interface GreetingService {

        void greeting(Person person);

    }

    public class GreetingServiceImpl implements GreetingService {

        @Override
        void greeting(Person person) {
            System.out.println(person.name + person.say() + "你好!");
        }

    }

    public class Person {

        ...

        public void greeting(greetingService greetingService) {
           greetingService.greeting(this);
        }

        ...

    }

自jdk1.3版本以來,反射機制被引入Java體制內,它可以獲取到被編譯完成的類中之屬性、方法或註解等元素。

定義一個InvocationHandler的實現類


    private class GreetingIHImpl implements InvocationHandler {

        private Object obj;

        GreetingIHImpl(Object obj) {
            this.obj = obj;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable {
            System.out.println("在 " + DateFormat.getDateTimeInstance().format(new Date()) + " 時:");
            Object reflectObj = method.invoke(obj, args);
            return reflectObj;
        }

    }

最後,我們在調用greetingService.greeting()方法的時候,將代理中的實現GreetingIHImpl織入到原有的接口中。


    GreetingService greetingService = (GreetingService) Proxy.newProxyInstance(
        GreetingService.class.getClassLoader(),
        new Class[]{GreetingService.class},
        new GreetingIHImpl(new GreetingServiceImpl())
    );

    ...

    ming.greeting(greetingService);

    // 輸出2018-07-12 17:19:54 時:
    小明說: 你好!

受限於筆者自身水平,文章可能含有技術性或描述上的錯誤。
歡迎指出問題、提出問題和製造問題。


原文地址:https://code.evink.me/2018/07/post/java8-callback-function/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章