Java设计模式:Callback

2023年 10月 13日 18.9k 0

本文主要介绍 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."));

类图

alt text

适用场景

回调模式适用于以下场景:

  • 异步操作:当需要在异步操作完成后执行某些操作时,可以使用回调模式。例如,在网络请求中,可以传递一个回调函数,在请求完成后调用该函数处理响应数据。
  • 事件处理:当需要对事件进行响应和处理时,可以使用回调模式。例如,在图形界面开发中,可以注册某个控件的回调函数,以便在用户触发事件时执行相应的操作。
  • 插件扩展:当需要为应用程序提供扩展性,允许第三方插件在特定事件发生时进行自定义操作时,可以使用回调模式。例如,游戏引擎中的事件系统允许开发者注册回调函数以响应游戏中的特定事件。
  • 回调链:当需要按特定顺序执行多个回调函数,并将前一个回调函数的结果传递给下一个回调函数时,可以使用回调模式。这种情况下,回调函数形成了一个回调链。
  • 模板方法模式:回调模式常与模板方法模式结合使用。模板方法模式定义了一个算法的骨架,而具体的步骤由子类实现。可以使用回调模式将子类中的具体步骤作为回调函数传递给模板方法。
  • 总的来说,回调模式适用于需要在特定事件发生后执行某些操作的情况,以及需要实现解耦和灵活性的场景。它提供了一种在代码间通信的方式,使得代码可以更加模块化和可复用。

    Java例子

    • CyclicBarrier 构造函数可以接受回调,该回调将在每次障碍被触发时触发。

    FAQ

    回调模式如何实现解耦和灵活性?

    回调模式通过将一个可执行的代码块(回调函数)作为参数传递给其他代码,实现了解耦和灵活性。

    • 解耦性:回调模式可以将调用方与被调用方解耦,使它们之间的关系更加松散。调用方只需要知道回调函数的接口,而不需要了解具体的实现细节。被调用方在特定的时机调用回调函数,而不需要知道调用方的具体实现。这种解耦性使得系统中的不同部分可以独立地进行修改和扩展,而不会对彼此产生过多的依赖。
    • 灵活性:回调模式提供了一种灵活的扩展机制。通过传递不同的回调函数,可以改变程序的行为或逻辑,而不需要修改原有的代码。这种灵活性使得系统可以适应不同的需求和变化,而不需要进行大规模的修改或重构。同时,回调模式也允许在运行时动态地修改回调函数,从而实现更高级的动态行为。

    通过使用回调模式,系统的不同部分可以相互独立地演化和扩展,而不会引入过多的紧耦合关系。这使得代码更加模块化、可复用和可维护。此外,回调模式还可以提高代码的可测试性,因为可以使用模拟或替代的回调函数来进行单元测试。

    总而言之,回调模式通过解耦和灵活性的特性,帮助提高了代码的可维护性、可扩展性和可测试性,使系统更加灵活和适应变化。

    回调模式和事件驱动模式有什么区别?

    回调模式和事件驱动模式是两种常见的设计模式,它们在某些方面有相似之处,但也存在一些区别。

    回调模式:

    • 在回调模式中,一个可执行的代码块(回调函数)被传递给其他代码,以便在特定事件发生时被调用。
    • 回调函数通常由调用方提供,用于定义在特定事件发生时应该执行的操作。
    • 回调模式用于实现解耦和灵活性,允许不同模块之间通过回调函数进行通信,但不依赖于具体的实现细节。

    事件驱动模式:

    • 事件驱动模式是一种编程范式,其中系统的行为和控制是由事件的发生和处理驱动的。
    • 在事件驱动模式中,组件(如控件、对象等)可以产生事件,并将其发送到事件处理程序进行处理。
    • 事件处理程序是事先定义好的,用于响应特定类型的事件。
    • 事件驱动模式通常涉及事件的发布、订阅和分发机制,以便将事件路由到正确的处理程序。

    区别:

  • 角色和通信方式:在回调模式中,回调函数是被调用方提供给调用方的,通过函数参数进行传递。而在事件驱动模式中,组件产生事件并将其发送给事件处理程序进行处理。
  • 控制流:在回调模式中,调用方主动调用回调函数来传递控制权,以响应特定事件。而在事件驱动模式中,控制流是由事件的发生和处理驱动的,事件处理程序被动地等待事件的发生。
  • 灵活性和扩展性:回调模式更加灵活,因为可以将不同的回调函数传递给相同的调用方,从而改变其行为。而事件驱动模式更加适用于大型系统,因为可以通过添加、移除或替换事件处理程序来扩展系统的功能。
  • 通信机制:回调模式通常使用函数参数进行通信,而事件驱动模式通常使用发布-订阅或观察者模式来实现事件的传递和处理。
  • 需要注意的是,回调模式和事件驱动模式并不是互斥的,它们可以同时存在于一个系统中,相互配合使用来实现不同的需求。

    回调模式和观察者模式有什么区别?

    回调模式和观察者模式是两种常见的设计模式,它们在某些方面有相似之处,但也存在一些区别。

    回调模式:

    • 在回调模式中,一个可执行的代码块(回调函数)被传递给其他代码,以便在特定事件发生时被调用。
    • 回调函数通常由调用方提供,用于定义在特定事件发生时应该执行的操作。
    • 回调模式用于实现解耦和灵活性,允许不同模块之间通过回调函数进行通信,但不依赖于具体的实现细节。

    观察者模式:

    • 观察者模式是一种发布-订阅模式,用于在对象之间建立一对多的依赖关系。当一个对象的状态发生变化时,它会通知所有依赖于它的观察者对象。
    • 观察者模式通常由一个主题(被观察者)和多个观察者组成。主题维护观察者列表,并在状态变化时通知观察者。
    • 观察者模式用于实现对象之间的松耦合,使得主题和观察者可以独立变化,而不会相互影响。

    区别:

  • 角色和通信方式:在回调模式中,回调函数是被调用方提供给调用方的,通过函数参数进行传递。而在观察者模式中,主题通常维护观察者列表,并通过通知方法将状态变化信息传递给观察者。
  • 控制流:在回调模式中,调用方主动调用回调函数来传递控制权,以响应特定事件。而在观察者模式中,主题对象在状态变化时被动地通知观察者,并由观察者决定如何处理通知。
  • 关注点:回调模式更关注于事件发生后的回调操作。观察者模式更关注于主题和观察者之间的状态变化通知和处理。
  • 依赖关系:在回调模式中,调用方和被调用方之间存在直接依赖关系,因为回调函数是由调用方提供的。而在观察者模式中,主题和观察者之间松耦合,它们只通过接口进行通信,不直接依赖于具体的实现。
  • 需要注意的是,回调模式和观察者模式可以根据具体的应用场景进行选择和组合使用。在某些情况下,它们可以互为补充,实现更灵活和可扩展的系统设计。

    使用回调模式,会存在内存泄露吗?

    在Java中使用回调模式时,也存在潜在的内存泄漏问题。内存泄漏可能发生在以下情况下:

  • 长期持有回调对象:如果一个对象持有一个回调对象的引用,并且该回调对象的生命周期比持有对象更长,那么即使持有对象不再使用,回调对象仍然保持对其的引用,从而导致内存泄漏。
  • 匿名内部类回调:当使用匿名内部类作为回调对象时,如果匿名内部类引用了外部类的实例,且该实例的生命周期比回调对象更长,那么即使外部类实例不再需要,回调对象仍然保持对其的引用,导致内存泄漏。
  • 使用回调模式,如何避免内存泄露?

    以下是一些常见的方法来避免内存泄漏:

  • 及时释放对象引用:确保在不再需要对象时,显式地将其引用设置为null。这样可以使垃圾回收器能够回收对象所占用的内存。
  • SomeObject obj = new SomeObject();
    // 使用obj对象...
    obj = null; // 不再需要obj对象时,将其引用设置为null
    
  • 避免长期持有对象引用:当一个对象持有另一个对象的引用时,确保持有引用的对象的生命周期不比被引用对象更长。在不再需要持有对象时,及时将其引用设置为null。
  • public class SomeClass {
        private Callback callback;
    
        public void setCallback(Callback callback) {
            this.callback = callback;
        }
    
        public void doSomething() {
            // 使用callback对象...
            callback = null; // 不再需要callback对象时,将其引用设置为null
        }
    }
    
  • 使用弱引用或软引用:对于某些情况下,当对象不再被强引用引用时,希望能够被垃圾回收,可以使用弱引用(WeakReference)或软引用(SoftReference)来持有对象。这样,在内存不足时,垃圾回收器可以回收这些对象。
  • 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,以允许垃圾回收器回收内存。

      使用静态内部类可以有效地避免内存泄漏问题,因为它们不会持有对外部类实例的引用,从而使得外部类实例可以在不再需要时被垃圾回收。这使得静态内部类成为一种常见的处理回调或复杂逻辑的有效方式。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论