回调(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中使用回调模式时,也存在潜在的内存泄漏问题。内存泄漏可能发生在以下情况下:
以下是一些常见的方法来避免内存泄漏:
- 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,以允许垃圾回收器回收内存。
使用静态内部类可以有效地避免内存泄漏问题,因为它们不会持有对外部类实例的引用,从而使得外部类实例可以在不再需要时被垃圾回收。这使得静态内部类成为一种常见的处理回调或复杂逻辑的有效方式。