两个经典例子让你彻底理解java回调机制

简介: 两个经典例子让你彻底理解java回调机制

前言

先让我们通过一个生活中的场景来还原一下回调的场景:你遇到了一个技术难题(比如,1+1等于几?太难了!),于是你去咨询大牛,大牛说现在正在忙,待会儿告诉你结果。

此时,你可能会去刷朋友圈了,等大牛忙完之后,告诉你答案是2。

那么,这个过程中询问问题(调用对方接口),然后问题解决之后再告诉你(对方处理完再调用你,通知结果),这一过程便是回调。

系统调用的分类

应用系统模块之间的调用,通常分为:同步调用,异步调用,回调。image.png同步调用是最基本的调用方式。类A的a()方法调用类B的b()方法,类A的方法需要等到B类的方法执行完成才会继续执行。如果B的方法长时间阻塞,就会导致A类方法无法正常执行下去。image.png如果A调用B,B的执行时间比较长,那么就需要考虑进行异步处理,使得B的执行不影响A。通常在A中新起一个线程用来调用B,然后A中的代码继续执行。

异步通常分两种情况:第一,不需要调用结果,直接调用即可,比如发送消息通知;第二,需要异步调用结果,在Java中可使用Future+Callable实现。

image.png通过上图我们可以看到回到属于一种双向的调用方式。回调的基本上思路是:A调用B,B处理完之后再调用A提供的回调方法(通常为callbakc())通知结果。

通常回调分为:同步回调和异步回调。网络上大多数的回调案例都是同步回调。

其中同步回调与同步调用类似,代码运行到某一个位置的时候,如果遇到了需要回调的代码,会在这里等待,等待回调结果返回后再继续执行。

而异步回调与异步调用类似,代码执行到需要回调的代码的时候,并不会停下来,而是继续执行,当然可能过一会回调的结果会返回回来。

同步回调实例

下面我们以同步回调为例来讲解回调的Java代码实现。整个过程就模拟上面问答问题的场景。

首先,定义给一个CallBack的接口,将回调的功能进行单独抽离:

public interface CallBack {
    void callback(String string);
}

CallBack接口中提供了一个callback方法,用于回调时调用。

然后定义问问题的人Person:

public class Person implements CallBack {
    private Genius genius;
    public Person(Genius genius) {
        this.genius = genius;
    }
    @Override
    public void callback(String string) {
        System.out.println("收到答案:" + string);
    }
    public void ask() {
        genius.answer(this);
    }
}

由于Person要提供回调方法,因此实现CallBack接口及其方法,方法中主要针对回调结果进行处理。

同时,由于Person要调用Genius对应的方法,因此要持有Genius的引用,这里通过构造方法传入。

定义回答问题的大神Genius类:

public class Genius {
    public void answer(CallBack callBack) {
        System.out.println("在忙其他事...");
        try {
            Thread.sleep(2000);
            System.out.println("忙完其他事,开始计算...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("天才计算出答案为:2");
        // 回调告诉你
        callBack.callback("2");
    }
}

这模拟大神正在忙碌,线程睡眠2秒,忙碌完之后,开始帮忙计算答案,获得答案之后,调用CallBack接口的callback方法进行回调,通知结果。

通过Main方法进行测试:

public static void main(String[] args) {
    Genius genius = new Genius();
    Person you = new Person(genius);
    you.ask();
}

执行打印结果如下:

在忙其他事...
忙完其他事,开始计算...
天才计算出答案为:2
收到答案:2

上面的过程,就实现了一个同步回调的功能。当然,从程序设计上来说,可以对Person和Genius进一步抽象化处理,通过接口的形式呈现。

在上述回调机制的代码实现中,最核心的是在调用answer方法时传递了this参数,即调用者自身。

从本质上来说,回调是一种思想,是一种机制,至于具体如何实现,如何通过代码将回调实现得优雅、实现得可扩展性比较高,就需要八仙过海各显神通了。

异步回调实例

上面的实例演示了同步回调,很明显在调用的过受到Genius执行时长的影响,需要等到Genius处理完才能继续执行Person方法中的后续代码。

下面在上述示例上进行改进,Person提供一个支持异步回调的方法:

public void askASyn() {
    System.out.println("创建新线程请教问题");
    new Thread(() -> genius.answer(this)).start();
    System.out.println("新线程已启动...");
}

在该方法内,新建了一个线程用来处理Genius#answer方法的调用,这样就能够跳过Genius#answer方法的阻塞,直接执行下面的操作(日志打印)。

在main方法中将调用的方法改为askASyn,打印结果如下:

创建新线程请教问题
新线程已启动...
在忙其他事...
忙完其他事,开始计算...
天才计算出答案为:2
收到答案:2

可以看出,直接打印了“新线程已启动...”,后续才打印出Genius#answer方法方法中处理日志和回调时callback方法接收到的信息。

基于Future的半异步

除了上述的同步,异步处理,还有一种介于同步和异步之间的基于Future的半异步处理。

在Java使用nio后无法立即拿到真实的数据,而是先得到一个"future",可以理解为邮戳或快递单,为了获悉真正的数据我们需要不停的通过快递单号"future"查询快递是否真正寄到。

Futures是一个抽象的概念,它表示一个值,在某一点会变得可用。一个Future要么获得计算完的结果,要么获得计算失败后的异常。

通常什么时候会用到Future呢?一般来说,当执行一个耗时的任务时,使用Future就可以让线程暂时去处理其他的任务,等长任务执行完毕再返回其结果。

经常会使用到Future的场景有:1. 计算密集场景。2. 处理大数据量。3. 远程方法调用等。

Java在java.util.concurrent包中附带了Future接口,它使用Executor异步执行。

例如下面的代码,每传递一个Runnable对象到ExecutorService.submit()方法就会得到一个回调的Future,使用它检测是否执行,这种方法可以是同步等待线处理结果完成。

public class TestFuture {
    public static void main(String[] args) {
        //实现一个Callable接口
        Callable<User> c = () -> {
            //这里是业务逻辑处理
            //让当前线程阻塞1秒看下效果
            Thread.sleep(1000);
            return new User("张三");
        };
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 记得要用submit,执行Callable对象
        Future<User> fn = es.submit(c);
        // 一定要调用这个方法,不然executorService.isTerminated()永远不为true
        es.shutdown();
        // 无限循环等待任务处理完毕  如果已经处理完毕 isDone返回true
        while (!fn.isDone()) {
            try {
                //处理完毕后返回的结果
                User nt = fn.get();
                System.out.println(nt.name);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    static class User {
        private String name;
        private User(String name) {
            this.name = name;
        }
    }
}

此种情况下虽然是创建了新线程来进行处理,但还是需要等待处理的结果。好处是可以将批量的处理,分为几个线程同时进行处理,最后对结果进行合并,达到提升处理效率的目的。

小结

经过这篇文章,想必大家对Java的回调机制已经有所了解,在各类开源框架中,其实也会经常看到回调的使用,活学活用。

目录
相关文章
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
537 32
AQS:Java 中悲观锁的底层实现机制
|
12月前
|
人工智能 Java 关系型数据库
Java——SPI机制详解
SPI(Service Provider Interface)是JDK内置的服务提供发现机制,主要用于框架扩展和组件替换。通过在`META-INF/services/`目录下定义接口实现类文件,Java程序可利用`ServiceLoader`动态加载服务实现。SPI核心思想是解耦,允许不同厂商为同一接口提供多种实现,如`java.sql.Driver`的MySQL与PostgreSQL实现。然而,SPI存在缺陷:需遍历所有实现并实例化,可能造成资源浪费;获取实现类方式不够灵活;多线程使用时存在安全问题。尽管如此,SPI仍是Java生态系统中实现插件化和模块化设计的重要工具。
668 0
|
10月前
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
368 4
|
Java 区块链 网络架构
酷阿鲸森林农场:Java 区块链系统中的 P2P 区块同步与节点自动加入机制
本文介绍了基于 Java 的去中心化区块链电商系统设计与实现,重点探讨了 P2P 网络在酷阿鲸森林农场项目中的应用。通过节点自动发现、区块广播同步及链校验功能,系统实现了无需中心服务器的点对点网络架构。文章详细解析了核心代码逻辑,包括 P2P 服务端监听、客户端广播新区块及节点列表自动获取等环节,并提出了消息签名验证、WebSocket 替代 Socket 等优化方向。该系统不仅适用于农业电商,还可扩展至教育、物流等领域,构建可信数据链条。
|
缓存 Dubbo Java
理解的Java中SPI机制
本文深入解析了JDK提供的Java SPI(Service Provider Interface)机制,这是一种基于接口编程、策略模式与配置文件组合实现的动态加载机制,核心在于解耦。文章通过具体示例介绍了SPI的使用方法,包括定义接口、创建配置文件及加载实现类的过程,并分析了其原理与优缺点。SPI适用于框架扩展或替换场景,如JDBC驱动加载、SLF4J日志实现等,但存在加载效率低和线程安全问题。
687 7
理解的Java中SPI机制
|
12月前
|
人工智能 JavaScript Java
Java反射机制及原理
本文介绍了Java反射机制的基本概念、使用方法及其原理。反射在实际项目中比代理更常用,掌握它可以提升编程能力并理解框架设计原理。文章详细讲解了获取Class对象的四种方式:对象.getClass()、类.class、Class.forName()和类加载器.loadClass(),并分析了Class.forName()与ClassLoader的区别。此外,还探讨了通过Class对象进行实例化、获取方法和字段等操作的具体实现。最后从JVM类加载机制角度解析了Class对象的本质及其与类和实例的关系,帮助读者深入理解Java反射的工作原理。
298 0
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
1089 1
|
缓存 运维 Java
Java静态代码块深度剖析:机制、特性与最佳实践
在Java中,静态代码块(或称静态初始化块)是指类中定义的一个或多个`static { ... }`结构。其主要功能在于初始化类级别的数据,例如静态变量的初始化或执行仅需运行一次的初始化逻辑。
477 4
|
Java 开发者
Java中的异常处理机制深度剖析####
本文深入探讨了Java语言中异常处理的重要性、核心机制及其在实际编程中的应用策略,旨在帮助开发者更有效地编写健壮的代码。通过实例分析,揭示了try-catch-finally结构的最佳实践,以及如何利用自定义异常提升程序的可读性和维护性。此外,还简要介绍了Java 7引入的多异常捕获特性,为读者提供了一个全面而实用的异常处理指南。 ####
254 20
|
运维 Java 编译器
Java 异常处理:机制、策略与最佳实践
Java异常处理是确保程序稳定运行的关键。本文介绍Java异常处理的机制,包括异常类层次结构、try-catch-finally语句的使用,并探讨常见策略及最佳实践,帮助开发者有效管理错误和异常情况。
1202 6