深入理解 Java 观察者模式:原理、实现与应用

       在软件开发领域,设计模式堪称开发者智慧的凝练结晶,它们为解决各类常见编程难题提供了行之有效的方案。观察者模式(Observer Pattern)作为行为型设计模式的重要一员,在处理对象间依赖关系与事件通知方面表现卓越。本文将深入剖析 Java 中的观察者模式,涵盖其核心概念、实现途径、优劣之处及适用场景,助力读者全方位掌握并灵活运用这一关键设计模式。​

一、观察者模式核心概念​

(一)定义与核心思想​

观察者模式定义了对象间的一对多依赖关系,当某一对象(被观察者,Subject)的状态发生变动时,所有依赖于它的对象(观察者,Observer)都会收到通知并自动更新。其核心在于解耦观察者与被观察者,让二者借助抽象接口进行交互,而非直接耦合,以此提升系统的灵活性与可扩展性。​

(二)角色划分​

观察者模式主要涉及四个关键角色:​

  1. 抽象被观察者(Subject):定义了添加、移除观察者以及通知观察者的接口或抽象类,维护着一个观察者列表,当自身状态变化时,负责通知列表中的所有观察者。​
  2. 具体被观察者(Concrete Subject):实现抽象被观察者的接口,具体负责管理自身状态,状态变化时,调用通知方法告知观察者。​
  3. 抽象观察者(Observer):定义了用于接收被观察者通知的更新接口,收到通知后执行相应操作。​
  4. 具体观察者(Concrete Observer):实现抽象观察者的更新接口,通常持有对具体被观察者的引用,以便在收到通知时获取被观察者状态并处理。​

二、Java 中观察者模式的实现方式​

在 Java 里,实现观察者模式主要有两种常见途径:一种是借助 JDK 内置的观察者相关类与接口,另一种则是自定义实现。接下来将分别详细阐述。​

(一)JDK 内置的观察者模式实现​

Java 的 java.util 包提供了 Observable 类和 Observer 接口,方便快速实现观察者模式。​

1. Observable 类​

Observable 类是抽象被观察者的具体实现,具备以下主要方法:​

  • addObserver(Observer o):向观察者列表中添加一个观察者。​
  • deleteObserver(Observer o):从观察者列表中移除一个观察者。​
  • notifyObservers():通知所有已注册的观察者,但调用此方法前需先调用setChanged()方法,用以标记被观察者状态已改变。​
  • notifyObservers(Object arg):带参数的通知方法,将参数传递给观察者。​
  • setChanged():设置内部标志,表明被观察者状态已改变,只有调用此方法后再调用notifyObservers(),观察者才会收到通知。​
  • clearChanged():清除状态改变标志。​

2. Observer 接口​

Observer 接口定义了观察者的更新方法,仅有一个方法:​

  • update(Observable o, Object arg):当被观察者状态变化并通知观察者时,该方法被调用。其中,o是发出通知的被观察者对象,arg是传递给观察者的参数(若有)。​

3. 使用步骤示例​

以简单的天气数据监测系统为例,展示如何运用 JDK 内置的观察者模式。​

步骤 1:创建具体被观察者(WeatherData)​

import java.util.Observable;​

public class WeatherData extends Observable {​

private float temperature;​

private float humidity;​

private float pressure;​

public void setWeatherData(float temperature, float humidity, float pressure) {​

this.temperature = temperature;​

this.humidity = humidity;​

this.pressure = pressure;​

// 设置状态改变标志​

setChanged();​

// 通知所有观察者​

notifyObservers();​

// 或者传递具体参数​

// notifyObservers(new WeatherInfo(temperature, humidity, pressure));​

}​

// 获取温度、湿度、气压的方法...​

}​

步骤 2:创建具体观察者(CurrentConditionsDisplay)​

import java.util.Observable;​

import java.util.Observer;​

public class CurrentConditionsDisplay implements Observer {​

private float temperature;​

private float humidity;​

private Observable observable;​

public CurrentConditionsDisplay(Observable observable) {​

this.observable = observable;​

// 将当前观察者添加到被观察者的列表中​

observable.addObserver(this);​

}​

@Override​

public void update(Observable o, Object arg) {​

if (o instanceof WeatherData) {​

WeatherData weatherData = (WeatherData) o;​

this.temperature = weatherData.getTemperature();​

this.humidity = weatherData.getHumidity();​

display();​

}​

}​

private void display() {​

System.out.println("当前天气状况:温度 " + temperature + "℃,湿度 " + humidity + "%");​

}​

}​

步骤 3:客户端使用​

public class Client {​

public static void main(String[] args) {​

WeatherData weatherData = new WeatherData();​

CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);​

// 模拟天气数据变化​

weatherData.setWeatherData(25.0f, 60.0f, 1013.0f);​

weatherData.setWeatherData(28.0f, 55.0f, 1010.0f);​

}​

}​

4. 注意事项​

  • Observable 类是具体类而非接口,这在一定程度上限制了其扩展性,因为 Java 不支持多继承,若一个类已继承其他类,便无法再继承 Observable 类。​
  • 调用 notifyObservers () 方法前,务必先调用 setChanged () 方法,否则观察者不会收到通知。这是由于 Observable 类内部通过布尔变量 changed 判断是否通知观察者,setChanged () 方法将其设为 true,notifyObservers () 方法调用后会将其设为 false(除非使用带参数版本且参数为 null)。​

(二)自定义实现观察者模式​

尽管 JDK 提供了内置的观察者实现,但在某些场景下,我们可能期望更灵活地定义抽象被观察者和抽象观察者接口,或者避免依赖 JDK 中的 Observable 类(比如被观察者需继承其他类时)。此时,可自定义实现观察者模式。​

1. 定义抽象观察者接口(Observer)​

public interface Observer {​

void update(Object data);​

}​

2. 定义抽象被观察者接口(Subject)​

public interface Subject {​

void registerObserver(Observer observer);​

void removeObserver(Observer observer);​

void notifyObservers(Object data);​

}​

3. 实现具体被观察者(ConcreteSubject)​

import java.util.ArrayList;​

import java.util.List;​

public class ConcreteSubject implements Subject {​

private List<Observer> observers = new ArrayList<>();​

private Object data;​

@Override​

public void registerObserver(Observer observer) {​

observers.add(observer);​

}​

@Override​

public void removeObserver(Observer observer) {​

observers.remove(observer);​

}​

@Override​

public void notifyObservers(Object data) {​

this.data = data;​

for (Observer observer : observers) {​

observer.update(data);​

}​

}​

public void setData(Object data) {​

notifyObservers(data);​

}​

}​

4. 实现具体观察者(ConcreteObserver)​

public class ConcreteObserver implements Observer {​

private String name;​

public ConcreteObserver(String name) {​

this.name = name;​

}​

@Override​

public void update(Object data) {​

System.out.println("观察者" + name + "接收到数据:" + data);​

}​

}​

5. 客户端使用示例​

public class CustomObserverClient {​

public static void main(String[] args) {​

ConcreteSubject subject = new ConcreteSubject();​

ConcreteObserver observer1 = new ConcreteObserver("A");​

ConcreteObserver observer2 = new ConcreteObserver("B");​

subject.registerObserver(observer1);​

subject.registerObserver(observer2);​

subject.setData("新数据来了!");​

subject.removeObserver(observer2);​

subject.setData("再次更新数据!");​

}​

}​

(三)两种实现方式的对比​

特性​

JDK 内置实现​

自定义实现​

抽象程度​

使用具体类 Observable 和接口 Observer,Observable 设计存在局限性(如非接口)​

完全自定义抽象接口,可依需求灵活设计接口方法​

扩展性​

因 Observable 是具体类,若被观察者需继承其他类则无法使用,扩展性受限​

被观察者实现自定义 Subject 接口,可自由继承其他类,扩展性更佳​

依赖关系​

依赖 java.util 包中的类和接口​

不依赖 JDK 特定类,可在更广泛环境中使用​

学习成本​

简单易用,适用于快速实现简单观察者模式场景​

需手动定义接口和实现,适用于复杂场景或高度定制需求​

三、观察者模式的优缺点​

(一)优点​

  1. 解耦观察者和被观察者:观察者与被观察者通过抽象接口交互,彼此无需了解对方具体实现,降低代码耦合度。被观察者状态变化时,观察者自动接收通知,无需主动查询,提升系统灵活性。​
  2. 支持广播通信:被观察者能同时通知多个观察者,实现一对多通信模式,契合消息推送、状态变更通知等事件广播场景。​
  3. 提高可维护性和扩展性:观察者与被观察者分离,新增或修改观察者行为不影响被观察者代码,反之亦然。系统更易维护和扩展,符合开闭原则(对扩展开放,对修改关闭)。​

(二)缺点​

  1. 通知顺序问题:被观察者通知观察者时,通常按注册顺序依次调用更新方法。若观察者间存在依赖关系或对通知顺序有特定要求,可能需额外处理,否则易导致意外结果。​
  2. 潜在的性能问题:若观察者数量众多,或每个观察者的更新方法执行耗时较长,被观察者通知所有观察者时可能耗时过多,引发性能下降,极端情况下甚至导致线程阻塞。​
  3. 过度使用可能导致复杂度增加:观察者模式虽能解耦对象,但系统中过度使用会使对象间依赖关系复杂,难以追踪和维护。尤其当多个被观察者和观察者存在复杂交互时,可能形成难以管理的网状结构。​

四、观察者模式的适用场景​

(一)当一个对象的状态变化需要通知多个对象时​

这是观察者模式最典型的应用场景。比如在股票交易系统中,某只股票价格变动时,需通知所有关注该股票的用户;新闻订阅系统里,有新新闻发布时,要通知所有订阅该新闻频道的用户。​

(二)当系统需要实现事件驱动机制时​

观察者模式可用于实现事件监听与处理机制。以 GUI 编程为例,按钮点击事件、窗口关闭事件等,都可通过观察者模式实现。用户触发事件(相当于被观察者状态变化)时,注册的事件监听器(相当于观察者)接收通知并执行相应处理逻辑。​

(三)当需要解耦具有依赖关系的对象时​

若两个或多个对象存在一对多的依赖关系,即一个对象变化影响多个对象,使用观察者模式可将它们解耦,使其通过抽象接口交互,而非直接依赖具体实现,提升系统灵活性与可维护性。​

(四)当需要实现数据的实时更新和同步时​

在分布式系统或实时数据处理系统中,常需将数据变化实时同步到多个客户端或模块。观察者模式能便捷实现数据实时更新,数据源(被观察者)数据变化时,所有订阅该数据源的客户端(观察者)自动接收通知并更新自身数据。​

五、实际应用案例分析​

(一)Swing 中的事件处理​

在 Java Swing GUI 开发中,观察者模式广泛应用于事件处理。例如,用户点击按钮时,按钮对象(被观察者)通知所有注册的动作监听器(观察者)。动作监听器实现 ActionListener 接口(相当于抽象观察者接口),其中的 actionPerformed 方法(相当于 update 方法)在按钮点击时被调用,执行相应事件处理逻辑。​

(二)Spring Framework 中的事件机制​

Spring 框架提供基于观察者模式的事件发布 - 订阅机制。通过 ApplicationEvent 类(抽象事件,相当于被观察者状态变化通知)和 ApplicationListener 接口(观察者接口),开发者可自定义事件和事件监听器。事件发生时,Spring 容器将事件发布给所有注册监听器,监听器执行相应处理逻辑。该机制在 Spring Boot 的自动配置、事务管理等模块均有应用。​

(三)消息中间件中的订阅 - 发布模式​

消息中间件(如 Kafka、RabbitMQ 等)的订阅 - 发布模式(Publish/Subscribe Pattern)与观察者模式极为相似。生产者(相当于被观察者)发布消息到主题(Topic),消费者(相当于观察者)订阅主题并接收消息。尽管订阅 - 发布模式通常借助消息代理(Broker)作为中介,而观察者模式中被观察者和观察者可直接交互,但二者核心思想均为一对多依赖关系和事件通知。​

六、总结与最佳实践​

观察者模式是极为实用的设计模式,通过解耦观察者和被观察者,实现灵活的事件通知机制,适用于多种场景。在 Java 中,既可用 JDK 内置的 Observable 和 Observer 快速实现简单观察者模式,也可通过自定义接口实现更灵活、贴合需求的模式。​

运用观察者模式时,需留意以下最佳实践:​

  1. 合理设计抽象接口:抽象被观察者和抽象观察者的接口应尽量通用,涵盖所有可能操作,避免频繁修改接口。​
  2. 控制观察者数量和更新逻辑:避免注册过多观察者,或在观察者更新方法中执行耗时操作,防止性能问题。​
  3. 处理循环依赖问题:若观察者和被观察者存在循环依赖(如观察者更新时修改被观察者状态,导致再次通知观察者),需谨慎处理,避免陷入无限循环。​
  4. 结合具体场景选择实现方式:依据项目具体需求,选择 JDK 内置实现或自定义实现。若需高度灵活性和扩展性,自定义实现更佳;若场景简单,追求快速实现,JDK 内置实现更便捷。​

深入理解观察者模式的原理、实现方式和适用场景,并遵循最佳实践,开发者便能在软件开发中灵活运用该模式,提升代码质量与系统可维护性。随着技术持续发展,观察者模式也在不断演进,与责任链模式、策略模式等结合,用以解决更复杂的问题。因此,持续学习和实践设计模式,对提升软件开发能力意义重大。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79658.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络原理 TCP/IP

1.应用层 1.1自定义协议 客户端和服务器之间往往进行交互的是“结构化”数据&#xff0c;网络传输的数据是“字符串”“二进制bit流”&#xff0c;约定协议的过程就是把结构化”数据转成“字符串”或“二进制bit流”的过程. 序列化&#xff1a;把结构化”数据转成“字符串”…

2025年5月HCIP题库(带解析)

某个ACL规则如下:则下列哪些IP地址可以被permit规则匹配&#xff1a; rule 5 permit ip source 10.0.2.0 0.0.254.255 A、10.0.4.5 B、10.0.5.6 C、10.0.6.7 D、10.0.2.1 试题答案&#xff1a;A;C;D 试题解析&#xff1a; 10.0.2.000001010.00000000.00000010.0000000…

【Redis | 基础总结篇 】

目录 前言&#xff1a; 1.Redis的介绍&#xff1a; 2.Redis的类型与命令&#xff1a; 3.Redis的安装&#xff1a; 3.1.Windows版本 3.2.Linux版本 4.在java中使用Redis&#xff1a; 4.1.介绍 4.2.Jedis 4.3.Spring Data Redis 前言&#xff1a; 本篇主要讲述了Redis的…

38.前端代码拆分

因为前端代码之前是一体编写的&#xff0c;所以为了方便对代码进行了拆分 之前是这样的&#xff1a; 为了更加规范&#xff0c;所以拆分成vue、util、store、api等部分&#xff1a; css&#xff1a; store: 拆分后的大致界面为&#xff1a; 其实还有点别扭需要后续再调整

tinyrenderer笔记(Shader)

tinyrenderer个人代码仓库&#xff1a;tinyrenderer个人练习代码 前言 现在我们将所有的渲染代码都放在了 main.cpp 中&#xff0c;然而在 OpenGL 渲染管线中&#xff0c;渲染的核心逻辑是位于 shader 中的&#xff0c;下面是 OpenGL 的渲染管线&#xff1a; 蓝色是我们可以自…

C++高性能内存池

目录 1. 项目介绍 1. 这个项目做的是什么? 2. 该项目要求的知识储备 2. 什么是内存池 1. 池化技术 2. 内存池 3. 内存池主要解决的问题 4.malloc 3. 先设计一个定长的内存池 4.高并发内存池 -- 整体框架设计 5. 高并发内存池 -- thread cache 6. 高并发内存池 -- …

LintCode407-加一,LintCode第479题-数组第二大数

第407题: 描述 给定一个非负数&#xff0c;表示一个数字数组&#xff0c;在该数的基础上1&#xff0c;返回一个新的数组。 该数字按照数位高低进行排列&#xff0c;最高位的数在列表的最前面. 样例 1&#xff1a; 输入&#xff1a;[1,2,3] 输出&#xff1a;[1,2,4] 样例 …

SMT贴片钢网精密设计与制造要点解析

内容概要 SMT贴片钢网作为电子组装工艺的核心载体&#xff0c;其设计与制造质量直接影响焊膏印刷精度及产品良率。本文系统梳理了钢网全生命周期中的15项关键技术指标&#xff0c;从材料选择、结构设计到工艺控制构建完整技术框架。核心要点涵盖激光切割精度的微米级调控、开口…

OpenCV进阶操作:角点检测

文章目录 一、角点检测1、定义2、检测流程1&#xff09;输入图像2&#xff09;图像预处理3&#xff09;特征提取4&#xff09;角点检测5&#xff09;角点定位和标记6&#xff09;角点筛选或后处理&#xff08;可选&#xff09;7&#xff09;输出结果 二、Harris 角点检测&#…

江苏正力新能Verify认知能力测评笔试已通知 | SHL测评题库预测题 | 华东同舟求职讲求职

江苏正力新能入职笔试通知&#xff0c;Verify&#xff08;认知能力测评&#xff09;&#xff0c;用时约46分钟&#xff0c;其中正式测试部分计时36分钟&#xff1b;时间到了试卷会自动提交&#xff0c;请合理安排答题时间&#xff01;前面有10分钟练习时间&#xff0c;可以略过…

在若依里创建新菜单

首先打开左侧菜单栏的系统管理&#xff0c;然后点击菜单管理 可以点击左上角的新增&#xff0c;也可以点击右侧对应目录的新增 这里我选择了右侧的新增&#xff0c;即在系统管理目录下新增菜单 其中的组件路径就是写好的页面的路径 &#xff08;从views的下一级开始写即可&…

【AI知识库云研发部署】RAGFlow + DeepSeek

可以分成两台机器部署,一台gpu,一台cpu,cpu的机器运行ragflow的主程序,使用模型时才访问gpu。当然全部在一台机器上部署是完全ok的。全文没有复杂的环境问题 gpu 安装screen:yum install screen 配置ollama: 下载官方安装脚本并执行: curl -fsSL https://ollama.co…

Java后端开发day40--异常File

&#xff08;以下内容全部来自上述课程&#xff09; 异常 异常&#xff1a;异常就是代表程序出现的问题 1. 异常的分类 1.1 Error 代表的是系统级别的错误&#xff08;属于严重问题&#xff09; 系统一旦出现问题&#xff0c;sun公司会把这些错误封装成Error对象。 Error…

算法思想之深度优先搜索(DFS)、递归以及案例(最多能得到多少克黄金、精准核酸检测、最富裕的小家庭)

深度优先搜索&#xff08;DFS&#xff09;、递归 深度优先搜索&#xff08;Depth First Search&#xff0c;DFS&#xff09;是一种用于遍历或搜索树或图的算法。在 DFS 算法中&#xff0c;从起始节点开始&#xff0c;沿着一条路径尽可能深地访问节点&#xff0c;直到到达叶子节…

Spark,HDFS客户端操作

hadoop客户端环境准备 找到资料包路径下的Windows依赖文件夹&#xff0c;拷贝hadoop-3.1.0到非中文路径&#xff08;比如d:\hadoop-3.1.0&#xff09; ① 打开环境变量 ② 在下方系统变量中新建HADOOP_HOME环境变量,值就是保存hadoop的目录。 效果如下&#xff1a; ③ 配置Pa…

共享会议室|物联网解决方案:打造高效、智能的会议空间!

在数字化转型的浪潮下&#xff0c;企业、园区、公共机构的会议室面临诸多痛点&#xff0c;如何通过物联网技术实现会议室资源的智能调度、环境设备的自动化控制以及用户体验的全面升级&#xff1f;本文将结合行业实践与技术方案&#xff0c;探讨基于物联网的共享会议室解决方案…

ts bug 找不到模块或相应类型的声明,@符有红色波浪线

解决方法&#xff1a;在env.d.ts文件中添加以下代码&#xff0c;这段代码是一个 TypeScript 的声明文件&#xff0c;用于让 TypeScript 知道如何处理 Vue 单文件组件&#xff08;.vue 文件&#xff09;的导入。 /// <reference types"vite/client" /> // 声明…

端口隔离基本配置

1.top图 2.交换机配置 # sysname sw1 # vlan batch 10 # interface GigabitEthernet0/0/1port link-type trunkport trunk allow-pass vlan 10 # interface GigabitEthernet0/0/2port link-type trunkport trunk allow-pass vlan 10sys sw2 # vlan batch 10 # interface Giga…

Android View#post()源码分析

文章目录 Android View#post()源码分析概述onCreate和onResume不能获取View的宽高post可以获取View的宽高总结 Android View#post()源码分析 概述 在 Activity 中&#xff0c;在 onCreate() 和 onResume() 中是无法获取 View 的宽高&#xff0c;可以通过 View#post() 获取 Vi…

SecureCrt设置显示区域横列数

1. Logical rows //逻辑行调显示区域高度的 一般超过50就全屏了 2. Logical columns //逻辑列调显示区域宽度的 3. Scrollback buffer //缓冲区大小