查看原文
其他

新手也能看懂的Java“回调函数”大揭秘!

JavaGuide 2020-12-28

为什么要推荐这篇文章?

平时我们写代码以及源码阅读中Java“回调函数”的使用太常见了,可能你还没有发现。


作者:Evink

地址:https://www.jianshu.com/p/e57f3cf0a67f

function foo(callback) {
callback();
}

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

foo(bar);

相信你一定看过上面这种形式的代码,没错,这就是在动态语言中被频繁运用到的回调函数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)。

img

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 时:
小明说: 你好!

受限于笔者自身水平,文章可能含有技术性或描述上的错误。欢迎指出问题、提出问题和制造问题。

文章推荐

 浏览Github必备的5款神器级别的Chrome插件
 阿里面试官:分别说说微信和淘宝扫码登录背后的实现原理?
 懒人 IDEA 插件推荐: EasyCode 一键帮你生成所需代码~
 2020 字节跳动后端面经分享!已拿 offer!


武汉加油!中国加油! 

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存