本文主要介绍 Callback 模式,在 Java Design Patterns 网站上有对该模式进行介绍。这里主要是做个笔记,并添加一些扩展,以加深对该设计模式的理解。
介绍
回调(Callback)是一种设计模式,在这种模式中,一个可执行的代码被作为参数传递给其他代码,接收方的代码可以在适当的时候调用它。
在真实世界的例子中,当我们需要在任务完成时被通知时,我们可以将一个回调方法传递给调用者,并等待它调用以通知我们。简单地说,回调是一个传递给调用者的方法,在定义的时刻被调用。
维基百科说
在计算机编程中,回调又被称为“稍后调用”函数,可以是任何可执行的代码用来作为参数传递给其他代码;其它代码被期望在给定时间内调用回调方法。
代码
回调是一个只有一个方法的简单接口。
public interface Callback {
void call();
}
下面我们定义一个任务它将在任务执行完成后执行回调。
public abstract class Task {
final void executeWith(Callback callback) {
execute();
Optional.ofNullable(callback).ifPresent(Callback::call);
}
public abstract void execute();
}
public final class SimpleTask extends Task {
private static final Logger LOGGER = getLogger(SimpleTask.class);
@Override
public void execute() {
LOGGER.info("Perform some important activity and after call the callback method.");
}
}
最后这里是我们如何执行一个任务然后接收一个回调当它完成时。
var task = new SimpleTask();
task.executeWith(() -> LOGGER.info("I'm done now."));
类图
适用场景
回调模式适用于以下场景:
总的来说,回调模式适用于需要在特定事件发生后执行某些操作的情况,以及需要实现解耦和灵活性的场景。它提供了一种在代码间通信的方式,使得代码可以更加模块化和可复用。
Java例子
- CyclicBarrier 构造函数可以接受回调,该回调将在每次障碍被触发时触发。
FAQ
回调模式如何实现解耦和灵活性?
回调模式通过将一个可执行的代码块(回调函数)作为参数传递给其他代码,实现了解耦和灵活性。
- 解耦性:回调模式可以将调用方与被调用方解耦,使它们之间的关系更加松散。调用方只需要知道回调函数的接口,而不需要了解具体的实现细节。被调用方在特定的时机调用回调函数,而不需要知道调用方的具体实现。这种解耦性使得系统中的不同部分可以独立地进行修改和扩展,而不会对彼此产生过多的依赖。
- 灵活性:回调模式提供了一种灵活的扩展机制。通过传递不同的回调函数,可以改变程序的行为或逻辑,而不需要修改原有的代码。这种灵活性使得系统可以适应不同的需求和变化,而不需要进行大规模的修改或重构。同时,回调模式也允许在运行时动态地修改回调函数,从而实现更高级的动态行为。
通过使用回调模式,系统的不同部分可以相互独立地演化和扩展,而不会引入过多的紧耦合关系。这使得代码更加模块化、可复用和可维护。此外,回调模式还可以提高代码的可测试性,因为可以使用模拟或替代的回调函数来进行单元测试。
总而言之,回调模式通过解耦和灵活性的特性,帮助提高了代码的可维护性、可扩展性和可测试性,使系统更加灵活和适应变化。
回调模式和事件驱动模式有什么区别?
回调模式和事件驱动模式是两种常见的设计模式,它们在某些方面有相似之处,但也存在一些区别。
回调模式:
- 在回调模式中,一个可执行的代码块(回调函数)被传递给其他代码,以便在特定事件发生时被调用。
- 回调函数通常由调用方提供,用于定义在特定事件发生时应该执行的操作。
- 回调模式用于实现解耦和灵活性,允许不同模块之间通过回调函数进行通信,但不依赖于具体的实现细节。
事件驱动模式:
- 事件驱动模式是一种编程范式,其中系统的行为和控制是由事件的发生和处理驱动的。
- 在事件驱动模式中,组件(如控件、对象等)可以产生事件,并将其发送到事件处理程序进行处理。
- 事件处理程序是事先定义好的,用于响应特定类型的事件。
- 事件驱动模式通常涉及事件的发布、订阅和分发机制,以便将事件路由到正确的处理程序。
区别:
需要注意的是,回调模式和事件驱动模式并不是互斥的,它们可以同时存在于一个系统中,相互配合使用来实现不同的需求。
回调模式和观察者模式有什么区别?
回调模式和观察者模式是两种常见的设计模式,它们在某些方面有相似之处,但也存在一些区别。
回调模式:
- 在回调模式中,一个可执行的代码块(回调函数)被传递给其他代码,以便在特定事件发生时被调用。
- 回调函数通常由调用方提供,用于定义在特定事件发生时应该执行的操作。
- 回调模式用于实现解耦和灵活性,允许不同模块之间通过回调函数进行通信,但不依赖于具体的实现细节。
观察者模式:
- 观察者模式是一种发布-订阅模式,用于在对象之间建立一对多的依赖关系。当一个对象的状态发生变化时,它会通知所有依赖于它的观察者对象。
- 观察者模式通常由一个主题(被观察者)和多个观察者组成。主题维护观察者列表,并在状态变化时通知观察者。
- 观察者模式用于实现对象之间的松耦合,使得主题和观察者可以独立变化,而不会相互影响。
区别:
需要注意的是,回调模式和观察者模式可以根据具体的应用场景进行选择和组合使用。在某些情况下,它们可以互为补充,实现更灵活和可扩展的系统设计。
使用回调模式,会存在内存泄露吗?
在Java中使用回调模式时,也存在潜在的内存泄漏问题。内存泄漏可能发生在以下情况下:
使用回调模式,如何避免内存泄露?
以下是一些常见的方法来避免内存泄漏:
SomeObject obj = new SomeObject();
// 使用obj对象...
obj = null; // 不再需要obj对象时,将其引用设置为null
public class SomeClass {
private Callback callback;
public void setCallback(Callback callback) {
this.callback = callback;
}
public void doSomething() {
// 使用callback对象...
callback = null; // 不再需要callback对象时,将其引用设置为null
}
}
SomeObject obj = new SomeObject();
WeakReference weakRef = new WeakReference(obj);
// 使用weakRef对象...
obj = null; // 不再需要obj对象时,将其引用设置为null
// 在适当的时机,检查弱引用是否还持有对象
if (weakRef.get() == null) {
// 对象已被垃圾回收
}
public class SomeClass {
public void doSomething() {
final SomeObject obj = new SomeObject();
Runnable runnable = new Runnable() {
@Override
public void run() {
// 使用obj对象...
}
};
// 使用runnable对象...
}
}
在上述示例中,匿名内部类引用了外部类的SomeObject
实例obj
。如果在run()
方法中持续引用了obj
,那么即使doSomething()
方法执行完毕,obj
仍然无法被垃圾回收。为避免该问题,可以将SomeObject
声明为final
,或者使用静态内部类。
1、在Java中,将
SomeObject
声明为final
可以帮助避免匿名内部类引起的内存泄漏问题。当内部类引用外部类的实例时,如果外部类的实例不再需要,但内部类仍然持有对外部类实例的引用,就可能导致内存泄漏。
当将
SomeObject
声明为final
时,编译器会确保在匿名内部类中使用的外部类实例不可变。这意味着在编译时,编译器会将对外部类实例的引用复制给内部类的成员变量,并且该引用在整个内部类的生命周期中保持不变。由于引用是不可变的,因此不会出现外部类实例被内部类持有,从而导致外部类实例无法被垃圾回收的情况。一旦外部类实例不再被引用,即使匿名内部类仍然存在,外部类实例也可以被垃圾回收器回收。
通过将
SomeObject
声明为final
,可以确保在匿名内部类中对外部类实例的引用是安全的,不会导致内存泄漏问题。这是因为编译器在编译时会生成正确的代码,确保内部类不会持有外部类实例的引用超过其生命周期。需要注意的是,虽然使用
final
修饰外部类引用可以帮助避免内存泄漏问题,但这并不是解决所有可能导致内存泄漏的情况的通用解决方案。在处理回调或内部类时,还需要仔细考虑对象引用的生命周期,并采取适当的措施来避免潜在的内存泄漏。2、使用静态内部类可以帮助避免内部类引起的内存泄漏问题。
静态内部类与外部类之间的引用是相互独立的,这意味着静态内部类不会隐式地持有对外部类实例的引用。
当内部类是静态内部类时,它不会隐式地持有对外部类实例的引用。这意味着即使外部类实例不再被引用,静态内部类仍然可以独立存在,而不会阻止外部类实例被垃圾回收。
由于静态内部类不持有对外部类实例的引用,因此在外部类实例不再需要时,可以安全地将其设置为null,并允许垃圾回收器回收内存。
以下是使用静态内部类的示例:
public class SomeClass { private static class CallbackImpl implements Callback { // 实现回调接口的方法 } public void doSomething() { Callback callback = new CallbackImpl(); // 使用callback对象... callback = null; // 不再需要callback对象时,将其引用设置为null } }
在上述示例中,
CallbackImpl
是静态内部类,它实现了Callback
接口。在doSomething()
方法中,我们创建了CallbackImpl
的实例,并使用它进行回调操作。当不再需要callback
对象时,将其引用设置为null,以允许垃圾回收器回收内存。使用静态内部类可以有效地避免内存泄漏问题,因为它们不会持有对外部类实例的引用,从而使得外部类实例可以在不再需要时被垃圾回收。这使得静态内部类成为一种常见的处理回调或复杂逻辑的有效方式。