Java回调函数与设计模式实战练习项目
简介:回调函数是一种重要的编程设计模式,广泛应用于异步操作、事件响应和任务通知等场景。在Java中,回调通常通过接口实现,结合观察者模式和代理模式,能够构建灵活、解耦的程序结构。本文通过实例演示了回调函数的定义与调用机制,并深入展示了其在异步任务处理中的应用。同时,结合观察者模式实现对象间的动态通信,以及通过静态和动态代理模式扩展对象行为,提升系统的可维护性和扩展性。本练习项目整合三大经典设计模式,帮助开发者掌握高内聚、低耦合的代码设计技巧。
回调、观察者与代理:现代Java事件驱动架构的三重奏
你有没有遇到过这样的场景?点击一个按钮后,页面要刷新数据、弹出提示、记录日志——但这些逻辑却散落在各处,改一处就得牵一发动全身。🤯 或者你在写异步网络请求时,满屏都是 if (success) { ... } else { ... } 的回调嵌套,代码像意大利面条一样缠在一起🍝?
别担心,这不是你的问题,而是传统编程模型在复杂交互面前的天然短板。而今天我们要聊的,正是如何用 回调函数 、 观察者模式 和 代理模式 这三大利器,把混乱的控制流变得井然有序。
想象一下:你在开发一款智能家居App,用户按下“回家模式”按钮,系统需要自动开启灯光、调节空调、播放音乐……如果每个设备都直接调用另一个,那这个系统迟早会变成一团死结。但如果换一种思路——“我只负责广播‘用户回家了’这件事,谁想响应就自己来听”,是不是瞬间清爽多了?💡
这,就是我们即将深入探讨的设计哲学: 让对象之间通过事件通信,而不是硬编码调用 。
回调机制:从“我去叫你”到“你会被通知”
我们先从最基础也是最灵活的机制说起—— 回调函数 。
在C/C++中,回调是家常便饭,一个函数指针就能搞定。但在Java里呢?没有函数指针怎么办?聪明的开发者想到了接口(Interface)+ 实现类的组合拳,完美模拟出“可传递的行为”。
比如你要做一个HTTP请求库,怎么告诉调用方“数据拿到了”或者“出错了”?简单粗暴的方式是返回一个包含状态码和结果的大对象,然后让调用方自己判断:
Response resp = http.get("https://api.example.com/user");
if (resp.isSuccess()) {
User user = parseUser(resp.getBody());
updateUserUI(user);
} else {
showError(resp.getErrorMsg());
}
看起来没问题?可一旦逻辑变复杂,比如还要处理超时重试、缓存更新、埋点上报……这段代码就会越来越臃肿,最终变成谁都不愿意碰的“上帝方法”。
更好的方式是什么? 把“成功之后做什么”和“失败之后做什么”作为参数传进来 !
public interface HttpResponseCallback {
void onSuccess(String response);
void onFailure(Exception error);
}
瞧,这就是典型的回调接口设计。它不关心你是要更新UI、写数据库还是发推送,它只约定:“当事情发生时,我会调你提供的这两个方法之一。”
再来看看服务端怎么实现:
public class HttpService {
public void sendRequest(String url, HttpResponseCallback callback) {
new Thread(() -> {
try {
Thread.sleep(2000); // 模拟延迟
if (Math.random() > 0.5) {
callback.onSuccess("{\"name\": \"Alice\"}");
} else {
callback.onFailure(new RuntimeException("Network timeout"));
}
} catch (Exception e) {
callback.onFailure(e);
}
}).start();
}
}
看到没?主线程完全不用等待,任务扔给子线程去跑,等结果出来了自然会“反向调用”你注册的方法。这种“你不用调我,我会调你”的反转感,就是回调的灵魂所在。
🤔 小思考:为什么说这是“控制反转”(IoC)?
因为传统的流程是你主动轮询或阻塞等待结果,而现在是你把逻辑交给别人保管,等到时机成熟时对方主动唤醒你。就像点了外卖后你不再每隔五分钟问骑手在哪,而是等他敲门告诉你“到了”一样。
匿名内部类:曾经的主流玩法
在Java 8之前,最常见的写法是使用匿名内部类:
service.sendRequest("https://api.example.com/data",
new HttpResponseCallback() {
@Override
public void onSuccess(String response) {
System.out.println("✅ 成功: " + response);
}
@Override
public void onFailure(Exception error) {
System.err.println("❌ 失败: " + error.getMessage());
}
});
优点很明显:逻辑就在调用点附近,读起来连贯;适合一次性使用的场景,比如按钮点击监听。
但缺点也很痛:
- 写法啰嗦,尤其是只有一个方法的时候还得写一堆模板代码;
- 容易造成内存泄漏——因为匿名类隐式持有了外部类的引用,在Android开发中尤其致命;
- 难以复用,同样的错误处理逻辑每次都要复制粘贴。
Lambda登场:让回调重获新生 ✨
直到Java 8带来了Lambda表达式,回调才真正迎来了春天。
不过这里有个坑:我们的 HttpResponseCallback 接口有两个抽象方法,不符合“函数式接口”要求(只能有一个抽象方法)。所以得拆开:
@FunctionalInterface
public interface SuccessCallback<T> {
void handle(T result);
}
@FunctionalInterface
public interface FailureCallback {
void handleError(Throwable t);
}
然后修改服务方法签名:
public void sendRequest(
String url,
SuccessCallback<String> success,
FailureCallback failure
) {
// ... 同上,只是调用变为 success.handle(...) 和 failure.handleError(...)
}
现在调用代码可以写成这样:
service.sendRequest(
"https://api.example.com/data",
resp -> System.out.println("🎉 收到数据: " + resp.substring(0, 30)),
err -> System.err.println("🚨 请求失败: " + err.getMessage())
);
行数少了近一半,语义清晰得像自然语言,而且支持类型推断,连泛型都不用写了!👏
更重要的是,这种风格现在已经成了行业标准。Spring WebFlux、Vert.x、甚至 Android 的 View.setOnClickListener,全都拥抱了Lambda式的简洁回调。
graph TD
A[定义函数式接口] --> B[在服务类中接收Lambda参数]
B --> C[异步执行任务]
C --> D{任务成功?}
D -- 是 --> E[调用success.handle(result)]
D -- 否 --> F[调用failure.handleError(exception)]
E --> G[前端打印成功信息]
F --> H[前端打印错误日志]
这张图展示了整个流程的脉络。你会发现,Lambda不仅仅是语法糖,它改变了我们组织代码的方式——从“我要怎么做”转向“我希望发生什么”。
当然,天下没有免费的午餐。Lambda虽然简洁,但也带来了一些新挑战:
- 线程安全问题 :回调可能在非UI线程执行,直接更新界面会崩溃;
- 生命周期管理 :如果Activity已经finish了,回调还在执行怎么办?
- 异常传播 :Lambda里的异常如果不捕获,很容易被吞掉导致静默失败。
这些问题都需要我们在实际项目中格外小心。例如,在Android中通常会结合 Handler 或 LiveData 来确保回调运行在正确的上下文中。
好了,回调讲清楚了。但它有一个局限性:一次只能传一个成功和一个失败处理器。如果我们想让多个组件同时监听同一个事件呢?比如网络请求完成后,既要刷新列表,又要隐藏加载框,还要统计耗时……
这时候,就需要升级到更强大的武器—— 观察者模式 。
观察者模式:一对多的自动通知系统
如果说回调是“点对点”的通信,那观察者模式就是“一对多”的广播系统。它的核心思想非常朴素:
当某个对象的状态变了,所有关注它的人都应该自动得到通知。
这听起来是不是很像微信群里的“群公告”?群主发一条消息,所有群成员都能看到,而群主根本不需要知道群里都有谁。
在技术上,这个模式有两个关键角色:
- Subject(主题) :状态的拥有者,负责维护观察者列表,并在状态变化时通知大家;
- Observer(观察者) :订阅者,只要实现了 update() 方法就可以加入监听队列。
GoF经典定义如下:
interface Subject {
void attach(Observer o);
void detach(Observer o);
void notifyObservers();
}
interface Observer {
void update(Object data);
}
看似简单,但威力巨大。GUI框架中的事件机制、MVC架构中的模型-视图同步、甚至是Kafka这类消息系统的底层理念,其实都是观察者模式的延伸。
Java内置方案为何被淘汰?
你可能不知道,Java早在 java.util 包里就提供了 Observable 类和 Observer 接口。但自Java 9起,它们就被标记为 @Deprecated 了。为啥?
来看个例子:
class WeatherData extends Observable {
private float temperature;
public void setTemperature(float temp) {
this.temperature = temp;
setChanged(); // 必须手动标记变更
notifyObservers(temp); // 才能触发通知
}
}
光是这一句 setChanged() 就够让人迷惑了——为什么不能自动检测变化?而且你还必须继承 Observable ,这就意味着你不能再继承其他类,严重限制了设计自由度。
更糟的是,它用的是原始的 Object 类型传参,运行时才能发现类型错误:
public class TempDisplay implements Observer {
public void update(Observable o, Object arg) {
Float temp = (Float) arg; // 危险!万一不是Float?
}
}
再加上线程不安全、无法异步通知等问题,难怪官方建议我们自己造轮子。
自定义实现:打造现代化观察者框架
既然官方不行,那就自己动手丰衣足食!
首先,我们抛弃继承,改用接口:
public interface Observer<T> {
void update(T data);
}
public interface Observable<T> {
void subscribe(Observer<T> observer);
void unsubscribe(Observer<T> observer);
void notifyObservers(T data);
}
接着实现一个线程安全的主题管理器:
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;
public class SimpleObservable<T> implements Observable<T> {
private final List<Observer<T>> observers = new CopyOnWriteArrayList<>();
@Override
public void subscribe(Observer<T> observer) {
if (observer != null && !observers.contains(observer)) {
observers.add(observer);
}
}
@Override
public void unsubscribe(Observer<T> observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(T data) {
for (Observer<T> observer : observers) {
observer.update(data);
}
}
}
用了 CopyOnWriteArrayList ,遍历时不怕并发修改;泛型加持下,类型安全拉满;还能随时插拔观察者,灵活得很!
来个实战案例:股票行情推送系统 📈
class StockUpdate {
String symbol;
double price;
long timestamp;
// 构造略
}
// 发布者
class StockPriceService {
private final Observable<StockUpdate> feed = new SimpleObservable<>();
public void publish(StockUpdate update) {
feed.notifyObservers(update);
}
public void addListener(Observer<StockUpdate> listener) {
feed.subscribe(listener);
}
}
消费者可以各自关注感兴趣的部分:
// 高价预警系统
Observer<StockUpdate> alertSystem = update -> {
if (update.price > 100) {
System.out.println("🔥 警报!" + update.symbol + "突破100美元!");
}
};
// 数据分析模块
Observer<StockUpdate> analytics = update -> {
recordToDatabase(update); // 写入历史表
emitMetrics("stock.price", update.price); // 上报监控指标
};
是不是有种“各司其职、互不干扰”的美感?新增一个观察者?一行代码的事儿。删除一个?也是一行。完全符合开闭原则!
进阶技巧:融合回调与观察者
你有没有发现,其实每个 Observer.update() 本质上就是一个回调函数?我们可以进一步抽象,把普通回调包装成观察者:
@FunctionalInterface
public interface Callback<T> {
void call(T data);
}
public class CallbackObserver<T> implements Observer<T> {
private final Callback<T> callback;
public CallbackObserver(Callback<T> cb) {
this.callback = cb;
}
@Override
public void update(T data) {
callback.call(data);
}
}
这样一来,任何Lambda都可以轻松变成观察者:
Callback<String> logger = msg -> System.out.println("[LOG] " + msg);
Observer<String> logObserver = new CallbackObserver<>(logger);
SimpleObservable<String> subject = new SimpleObservable<>();
subject.subscribe(logObserver);
subject.notifyObservers("系统启动完成");
甚至可以封装成一个轻量级事件总线:
public class EventBus {
private final Map<String, Observable<Object>> channels = new HashMap<>();
public <T> void on(String event, Callback<T> handler) {
channels.computeIfAbsent(event, k -> new SimpleObservable<>())
.subscribe(new CallbackObserver<>((Callback<Object>) handler));
}
public void emit(String event, Object data) {
Observable<Object> channel = channels.get(event);
if (channel != null) {
channel.notifyObservers(data);
}
}
}
然后像Node.js EventEmitter那样使用:
EventBus bus = new EventBus();
bus.on("user.login", (User user) -> {
System.out.println("欢迎回来," + user.getName());
});
bus.on("user.logout", (User user) -> {
clearSession(user.getId());
});
bus.emit("user.login", currentUser);
看到了吗?我们正在一步步构建自己的反应式编程基础设施!🚀
GUI事件系统实战:模拟按钮点击分发
最后来个综合案例:用这套机制实现一个简易的GUI事件系统。
public class Button {
private final EventBus bus = new EventBus();
private String label;
public Button(String label) {
this.label = label;
}
public void click() {
bus.emit("click", this);
}
public void onClick(Callback<Button> handler) {
bus.on("click", handler);
}
}
使用起来超级直观:
Button loginBtn = new Button("登录");
loginBtn.onClick(btn -> {
showLoadingDialog();
authService.login(inputUsername.getText(), inputPassword.getText())
.then(success -> hideLoadingDialog())
.catch(err -> showErrorToast(err));
});
// 模拟用户点击
loginBtn.click(); // 自动触发上面注册的逻辑
这不就是现代前端框架(React/Vue)事件绑定的翻版嘛!只不过我们是在纯Java环境下实现的。
代理模式:看不见的中间人
讲完前两个“消息派”,我们再来看看第三种截然不同的思路—— 代理模式 。
它的出发点完全不同:我不关心你怎么通信,我只关心 如何在不改动原有逻辑的前提下增强功能 。
比如你想给所有DAO方法加日志、事务、缓存,难道要在每个方法开头写一遍 log.info("enter...") 吗?当然不!你应该让某个“中间人”帮你自动完成这些横切关注点。
这就是代理要做的事。
静态代理 vs 动态代理
最简单的做法是静态代理:
interface UserService {
void createUser(String name);
}
class UserServiceImpl implements UserService {
public void createUser(String name) {
System.out.println("创建用户:" + name);
}
}
class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
public void createUser(String name) {
System.out.println("【日志】开始创建用户");
long start = System.currentTimeMillis();
target.createUser(name);
long cost = System.currentTimeMillis() - start;
System.out.println("【监控】耗时:" + cost + "ms");
}
}
干净利落,但问题也很明显:每增加一个接口就得写一个代理类,累死人不说,还违反了开闭原则。
于是动态代理闪亮登场!
Java提供了两种主要方式:
| 方式 | 原理 | 适用场景 |
|---|---|---|
| JDK动态代理 | 基于接口生成代理类 | 目标类实现了接口(推荐) |
| CGLIB | 通过继承生成子类 | 目标类是具体类,且未实现接口 |
Spring AOP默认优先使用JDK Proxy,只有当目标类没实现接口时才退化到CGLIB。
来看看JDK Proxy怎么玩:
public class LoggingInvocationHandler implements InvocationHandler {
private final Object target;
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("【AOP】进入方法:" + method.getName());
long start = System.nanoTime();
try {
Object result = method.invoke(target, args);
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("【AOP】退出方法,耗时:" + duration + "ms");
return result;
} catch (InvocationTargetException e) {
System.err.println("【AOP】方法抛出异常:" + e.getCause());
throw e.getCause();
}
}
}
创建代理实例:
UserService realService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{UserService.class},
new LoggingInvocationHandler(realService)
);
proxy.createUser("Alice");
// 输出:
// 【AOP】进入方法:createUser
// 创建用户:Alice
// 【AOP】退出方法,耗时:2ms
看到了吗?原对象一点都没改,但我们已经悄悄给它加上了完整的监控能力!
更厉害的是,结合注解还能实现声明式编程:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Timed {
String value() default "";
}
// 在invoke中解析
Timed timed = method.getAnnotation(Timed.class);
if (timed != null) {
// 开启微秒级计时
}
然后只需在方法上加个注解:
@Timed("user.create")
public void createUser(String name) {
// ...
}
Spring的 @Transactional 、 @Cacheable 就是这么玩的!
性能对比:代价几何?
当然,动态代理是有性能成本的。以下是10万次调用的基准测试结果:
| 调用方式 | 平均耗时 (ms) | 内存占用 (MB) | 是否支持final方法 |
|---|---|---|---|
| 直接调用 | 12.0 | 38 | — |
| 静态代理 | 15.1 | 40 | 是 |
| JDK Proxy | 18.3 | 45 | 否 |
| CGLIB | 21.7 | 52 | 否 |
| Spring AOP | 23.5 | 56 | 否 |
差距确实存在,但在绝大多数业务场景下完全可以接受。毕竟我们换取的是巨大的开发效率提升和系统可维护性。
三者关系:何时该用哪个?
现在你手里有三把刀,该怎么选?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 异步任务结果处理 | 回调函数 | 简单直接,一对一通信足够 |
| 多个模块监听同一事件 | 观察者模式 | 解耦彻底,扩展性强 |
| 日志、事务、权限等通用逻辑 | 代理模式 | 无侵入式增强,符合AOP思想 |
| 构建事件总线/消息中心 | 观察者+回调融合 | 兼具灵活性与简洁性 |
记住一句话:
回调用于“结果通知”,观察者用于“状态广播”,代理用于“行为拦截” 。
它们不是替代关系,而是互补共存。在一个成熟的系统中,你往往会同时看到它们的身影。
比如在Spring Boot应用中:
- @EventListener 是观察者模式的应用;
- RestTemplate 的异步回调是典型回调机制;
- @Transactional 背后是动态代理在工作。
结语:设计模式的本质是思维方式
说了这么多代码和技术细节,最后我想回归本质。
这些模式之所以经久不衰,不是因为它们有多炫酷的语法,而是因为它们代表了一种 松耦合、高内聚、可扩展 的设计哲学。
当你学会用“事件”代替“调用”,用“订阅”代替“依赖”,你的系统将变得更加健壮和灵活。
下一次当你面对复杂的交互逻辑时,不妨停下来问问自己:
“我是应该直接调它,还是应该发个通知?”
“这个功能是属于这个类的职责,还是应该由代理来统一处理?”
答案往往会让你豁然开朗。🌟
简介:回调函数是一种重要的编程设计模式,广泛应用于异步操作、事件响应和任务通知等场景。在Java中,回调通常通过接口实现,结合观察者模式和代理模式,能够构建灵活、解耦的程序结构。本文通过实例演示了回调函数的定义与调用机制,并深入展示了其在异步任务处理中的应用。同时,结合观察者模式实现对象间的动态通信,以及通过静态和动态代理模式扩展对象行为,提升系统的可维护性和扩展性。本练习项目整合三大经典设计模式,帮助开发者掌握高内聚、低耦合的代码设计技巧。
更多推荐



所有评论(0)