第二次Block作业:数字电路模拟程序
第二次Block作业:数字电路模拟程序
一.前言
学习Java第三个月,我完成了一个数字电路模拟程序的两次迭代开发。本次课程项目要求实现一个支持多种逻辑元件与信号连接的数字电路模拟器,能够根据输入的元件描述、连接关系与激励信号,自动进行信号传播与计算,并最终按指定格式输出各元件的状态结果。
本程序基于Java语言开发,历经两次迭代逐步完善。初始版本实现了基础逻辑门电路的模拟功能,包括与门、或门、非门、异或门和同或门,并通过广度优先搜索算法进行信号传播。实际上第一次代码我就已经加入了超出题目要求的多种功能,具体原因后面详谈。
在第二次迭代中,程序进一步扩展了对三态门、译码器、数据选择器与分配器等复杂元件的支持,同时优化了元件的抽象模型、引脚管理机制与信号传递逻辑,增强了系统的模块性与可维护性。
整个开发过程不仅加深了对面向对象编程中类与对象设计、数据结构选用以及算法实现的理解,也促使我们思考如何有效模拟电路中的并行行为、处理信号竞争与保持逻辑一致性。通过本项目,得以在实践中体会从需求分析、架构设计到功能实现与调试的全过程,为今后开发更复杂的仿真系统积累了宝贵经验。在接下来的内容中,我将详细展示两次迭代的设计思路、关键代码实现以及学习过程中的心得体会。
二、设计与分析
1.第一次作业
作业要求
数字电路是一种处理离散信号的电子电路。与处理连续变化信号(如声音、温度)的模拟电路不同,数字电路只识别和运算两种基本状态:高电平(通常表示为“1”) 和 低电平(通常表示为“0”)。这正好与二进制数制系统相对应,使得数字电路成为所有计算机和数字系统的物理实现基础。
请编程实现数字电路模拟程序,
1、电路元件
电路中包含与门、或门、非门、异或门、同或门五种元件。
2、程序输入
1)元件信息:
用A、O、N、X、Y 分别用作与门、或门、非门、异或门、同或门五种元件的元件标识符。
2)引脚信息:
引脚信息由“元件名-引脚号”构成,。
3)电路的输入信息:
电路的输入格式:
INPUT:英文空格+输入1+”-”+输入信号1+英文空格+输入2+....+输入n+”-”+输入信号n
4)连接信息
引脚的连接信息格式:
[+输出引脚+英文空格+输入引脚1+。。。。+英文空格+输入引脚+]
5)输入结束信息
3、程序输出
按照与门、或门、非门、异或门、同或门的顺序依次输出所有元件的输出引脚电平。同类元件按编号从小到大的顺序排序。
代码复杂度分析
Metrics Details For File 'y.java'
Parameter Value
========= =====
Project Directory D:\Desktop\JAVA
Project Name y.java
Checkpoint Name Baseline
File Name y.java
Lines 401
Statements 242
Percent Branch Statements 27.7
Method Call Statements 114
Percent Lines with Comments 11.0
Classes and Interfaces 0
Methods per Class 0.00
Average Statements per Method 14.56
Line Number of Most Complex Method 312
Name of Most Complex Method getSortedOutputs().validComponents.sort(()
Maximum Complexity 2
Line Number of Deepest Block 286
Maximum Block Depth 7
Average Block Depth 1.86
Average Complexity 2.00
Most Complex Methods in 1 Class(es): Complexity, Statements, Max Depth, Calls
getSortedOutputs().validComponents.sort(() 2, 4, 3, 4
Block Depth Statements
0 41
1 82
2 53
3 34
4 10
5 13
6 8
7 1
8 0
9+ 0

类图

踩坑心得
事实上这次并没有什么特别难的地方,每个元件的输入输出要求包括逻辑都非常清晰,那为什么还要写这份踩坑心得呢,那必须提到错误测试点的问题了,在我写完代码发现只有一个测试点没过的时候,我绞尽脑汁,添加了许多与原题毫不相关的功能都无法实现后(比如增加了大小写检验,当输入为小写字母也可以被识别,实际上逻辑就是先把所以字母转化为小写字母),我与另外一位同学打算另辟蹊径,使用另一种方法解题,我们使用超时判断去一个一个试出这个测试点每一行的长度,然后再去试出这一行的内容(比如当这一行内容满足我们输入的字符串的时候就while,这个测试点就不会报答案错误而是答案超时),就通过这个方法两个人测了大概有五六百次终于扒出来了测试点,再通过手算发现我们的源码的答案应该是没有任何问题的,果不其然,老师最后改了测试点(这个测试点果然有问题,耽误了我好长时间呜呜呜),希望下次老师制作测试点不要那么着急,确定好正确答案再发布。
代码逻辑分析
程序定义了三个核心类:引脚(Pin)表示电路中的连接点,可以存储信号值并记录与其他引脚的连接关系;元件(Component)代表各种逻辑门,如与门、或门、非门、异或门和同或门,每个元件有自己的输入引脚和输出引脚,能够根据输入信号计算输出值;电路(Circuit)作为整个系统的管理器,负责维护所有元件和引脚的集合,处理它们之间的连接关系,并驱动信号的传播过程。
程序的工作流程从读取输入开始,支持两种格式的输入行:一种是以"INPUT:"开头的行,用于设置某些输入引脚的初始信号值;另一种是用方括号括起来的连接关系行,描述电路中引脚之间的连接方式,第一个引脚是输出端,后面的引脚是输入端。读取完所有输入后,程序会进行信号传播计算,采用广度优先搜索算法从已有信号的引脚开始,逐步将信号传递到所有相连的引脚。当一个元件的所有输入引脚都有值后,程序会计算该元件的输出值,并将输出引脚加入传播队列。最后,程序按照元件类型和编号的特定顺序,输出所有有值的输出引脚及其信号值。
代码缺陷分析
引脚连接管理逻辑不清晰,存在设计问题。Pin类中定义了connections列表,但代码中没有明确这个连接是单向还是双向。在addConnection方法中,只将输出引脚连接到输入引脚,但在信号传播时却假设可以从任意引脚传播到它的连接。此外,引脚被同时标记为输入和输出,这种双重身份可能导致逻辑混乱。
信号传播算法存在严重缺陷。在propagateSignal方法中,当引脚值更新时,程序会检查这个引脚是否是某个元件的输入,但这里假设引脚名称包含"-"就一定是元件引脚,这个假设不可靠。更重要的是,算法没有处理信号冲突的情况,当同一个引脚从不同路径接收到不同信号时,程序会使用先接收到的值,这可能导致错误结果。同时,算法也没有处理反馈回路或环形连接的情况,可能导致无限循环。
输入解析部分存在潜在错误。在解析"INPUT:"行时,代码简单地用空格分割,但实际输入中可能包含其他空白字符。对于[输出引脚 输入引脚...]格式的解析,没有验证引脚名称的合法性,也没有处理特殊情况,如引脚名称本身包含空格的情况。当输入格式不符合预期时,程序可能抛出未处理的异常。
元件计算逻辑不够健壮。Component类的calculateOutput方法在计算前会检查所有输入是否就绪,但没有验证输入值的合法性(只能是0或1)。对于多输入与门和或门,代码通过循环计算,但这种方法对于大量输入可能效率不高。更重要的是,同或门通常不是标准逻辑门,而程序中将其与异或门并列,这种设计可能不符合实际电路设计习惯。
不必多言,直接上第二次作业吧
2.第二次作业
作业要求
在上次代码的基础上添加功能元件
1、包含多输入输出的组合电路元件如数据选择器;
2、元件引脚类型除输入、输出之外,增加控制引脚,如三态门。
代码复杂度分析
Metrics Details For File 's.java'
Parameter Value
========= =====
Project Directory D:\Desktop\JAVA
Project Name
Checkpoint Name Baseline
File Name s.java
Lines 809
Statements 524
Percent Branch Statements 32.3
Method Call Statements 205
Percent Lines with Comments 3.6
Classes and Interfaces 5
Methods per Class 4.60
Average Statements per Method 19.65
Line Number of Most Complex Method 207
Name of Most Complex Method Component.calculateOutput()
Maximum Complexity 54
Line Number of Deepest Block 336
Maximum Block Depth 8
Average Block Depth 3.24
Average Complexity 6.86
Most Complex Methods in 6 Class(es): Complexity, Statements, Max Depth, Calls
Circuit.addComponent() 15, 56, 4, 14
Circuit.addConnection() 2, 4, 3, 3
Circuit.Circuit() 1, 2, 2, 0
Circuit.extractParam() 4, 10, 4, 5
Circuit.getOrCreatePin() 12, 26, 7, 18
Component.allPinsReady() 5, 7, 3, 2
Component.calculateOutput() 54, 113, 8, 49
Component.Component() 9, 60, 4, 9
Component.extractNumber() 2, 5, 3, 5
Component.hasAllInputSignal() 1, 1, 2, 1
Component.hasAnyInputSignal() 5, 7, 3, 2
ComponentType.DEMUX.ComponentType() 1, 2, 2, 0
ComponentType.DEMUX.fromCode() 3, 4, 4, 1
ComponentType.DEMUX.getCode() 1, 1, 2, 0
ComponentType.DEMUX.getOrder() 1, 1, 2, 0
getSortedOutputs().validComponents.sort(() 3, 9, 3, 4
Main.main() 18, 39, 8, 30
Pin.equals() 4, 6, 2, 3
Pin.hashCode() 1, 1, 2, 1
Pin.Pin() 1, 6, 2, 0
Pin.toString() 1, 1, 2, 0
Block Depth Statements
0 12
1 75
2 111
3 98
4 125
5 43
6 32
7 25
8 3
9+ 0

类图

踩坑心得
实际上这次也存在一个错误测试点,但是一方面第一次作业我已经提出这个问题了,另一方面我这里确实遇到了其他严重错误
数据选择器上,我遇到了第一个重大逻辑错误,即本该作为0的控制端,由于我的一时疏忽,变成了1为控制端,而0变成了最后的输出端,而当时恰好后几个题目测试点还没发布,所以我并没有意识到这个错误,也是直到后来去一个一个测才恍然大悟
最后就是卡了最久的译码器了,它让我的26号和34号测试点始终答案错误,而我去用别人的或者题目上的测试用例都没用出错,最后发现,在错误的代码中,我对译码器中对所有组件使用统一的连续编号方式,而正确的代码应当是为MUX组件实现了特殊的引脚编号方案,即由于MUX组件数量众多,采用连续编码方式会导致组件功能大出错,固定好编号实施对应功能就能保证代码不出错
代码逻辑分析
组件与引脚建模:
定义了Pin类表示电路引脚,包含引脚名称、值、连接关系及引脚类型属性。定义了ComponentType枚举表示九种逻辑组件类型,每种类型有特定的编码字符和执行顺序。Component类表示电路组件,根据类型自动创建控制引脚、输入引脚和输出引脚,其中MUX组件有特殊的引脚编号规则(控制→输入→输出的固定顺序),其他组件采用连续编号。
电路结构构建:
Circuit类管理整个电路,包含组件映射和引脚映射。通过解析组件名称自动确定引脚数量,支持参数化组件如AND(3)表示3输入与门。提供引脚获取与创建方法,自动处理引脚名称到组件的映射关系。连接信息以方括号语法解析,建立引脚间的信号连接关系。
信号传播机制:
采用基于引脚的BFS传播算法。初始化时将已有信号值的引脚加入队列,每次从队列取出引脚,将其值传播到所有连接的引脚。当目标引脚值发生变化时,触发其所属组件的输出计算。同时遍历所有组件,对满足计算条件的组件(所有输入就绪或MUX类型)执行计算,将新输出的引脚加入队列继续传播。设有最大迭代次数防止死循环。
组件计算逻辑:
每种组件类型实现特定的布尔逻辑。基本逻辑门(AND/OR/NOT/XOR/XNOR)执行标准布尔运算。三态门在控制信号有效时传递输入值。译码器在特定控制信号组合下工作。多路选择器根据控制信号选择对应输入通道。多路分配器将输入路由到指定输出通道。MUX的计算逻辑经过特别处理,需分步校验控制引脚有效性和选择索引范围。
输入输出处理:
主程序读取所有输入行,识别最后的INPUT行作为信号输入。先处理所有连接定义,再应用输入信号。输出时按组件类型顺序和编号排序,不同组件有特定输出格式:基本组件输出"引脚名:值",译码器输出"组件名:选中索引",多路分配器输出"组件名:输出位模式"。
代码缺陷分析
代码存在以下缺陷:
输入解析逻辑错误:
代码假设只处理最后一次INPUT:行,但电路仿真通常需要处理所有输入行,每次输入信号变化后都应重新传播信号并输出结果。当前实现忽略历史输入,可能导致仿真不完整。(但这是因为含有错误测试点乱尝试的历史遗留问题)
信号传播机制存在缺陷:
BFS遍历中设置comp.visited标记防止重复计算,但每次传播都重置为false,可能导致同一组件在单次传播中被多次计算。同时,组件计算后仅将输出引脚加入队列,未考虑这些输出引脚可能触发下游组件的连锁计算。
值冲突处理简单化:
当传播过程中目标引脚已有值与新值不同时,代码选择valueChanged = false忽略冲突,但实际电路可能出现竞争或错误,应更明确地处理信号冲突,如报告错误或采用特定裁决逻辑。
多路选择器索引计算可能错误:
MUX的selectIndex计算采用selectIndex += muxCtrlPin.value * (1 << i),但未明确控制引脚顺序与输入引脚映射关系。引脚编号顺序(0,1,2...)不一定对应二进制位的低到高顺序,需与组件规范一致。
输出排序可能不稳定:
getSortedOutputs按类型顺序和组件编号排序,但当多个同类型同编号组件存在时,顺序可能不稳定。且仅输出isOutputValid为真的组件,但某些组件在无效时也应有特定输出(如三态门禁用时输出高阻态)。
译码器逻辑限制:
译码器仅当控制引脚为1,0,0时才工作,这是硬编码的特定使能条件,不通用。实际译码器可能有不同使能逻辑,应根据组件参数或规范确定。
三、两次数字电路程序作业的踩坑心得
第一次作业的心得体会
测试数据的重要性
这次作业中让我印象最深刻的是测试点问题。花费大量时间排查代码逻辑,最后发现问题出在测试数据上。这让我认识到:永远不要假设测试数据是绝对正确的。在实际开发中,即使是标准测试集也可能存在边界情况未覆盖或错误的情况。这教会了我在调试时不仅要检查代码逻辑,还要验证输入输出是否符合预期。
过度优化的陷阱
我最初的代码已经完成了基本功能,但为了“完善”程序,添加了许多与核心功能无关的特性(如大小写字母识别)。这种做法不仅增加了代码复杂度,还可能引入新的错误。“够用就好” 是这次作业给我的重要启示,在满足需求的前提下保持代码简洁往往更有利于维护和调试。
数据结构选择的重要性
在信号传播算法中,我采用了广度优先搜索(BFS)策略。这种方法虽然直观,但实现中发现需要仔细管理队列和访问标记,避免重复计算或死循环。这让我深刻体会到数据结构与算法选择对仿真系统性能的直接影响。
第二次作业的心得体会
功能扩展的挑战
在扩展支持三态门、译码器等复杂元件时,我遇到了组件抽象和引脚管理的复杂性。最初的简单组件模型无法适应控制引脚、多路选择等新需求,不得不重构组件类的设计。这让我明白:好的架构需要考虑未来的扩展性,即使在初次实现时只支持简单功能。
特殊组件的特殊处理
在实现MUX(多路选择器)时,我错误地理解了控制端逻辑,将0/1控制颠倒。这让我意识到:必须仔细理解每个组件的真值表和功能描述,不能想当然。后来我通过编写针对性的单元测试,逐一验证每个组件的功能,才发现了这个问题。
引脚编号的微妙之处
译码器的26号和34号测试点错误困扰我最久。最终发现是由于我对MUX组件采用了统一的连续编号,而正确的实现需要为MUX使用特殊的引脚编号方案。这让我学到了:看似简单的“编号”问题,可能在复杂系统中产生重大影响。不同组件类型可能需要不同的引脚管理策略。
信号传播的复杂性
第二次作业引入了控制引脚,这使得信号传播逻辑变得更加复杂。我最初的设计没有很好地区分数据引脚和控制引脚,导致某些情况下信号传播顺序错误。通过这次经历,我学到了:控制信号通常需要优先处理,因为它们决定了数据信号的流向。
调试策略的改进
面对复杂错误,我采取了更系统的调试方法:逐步缩小问题范围,为每个组件类型编写独立的测试用例,通过打印中间状态来追踪信号传播过程。这种方法比漫无目的地猜测要高效得多。
四、总结
通过这两次数字电路模拟程序的开发,我不仅熟练掌握了Java面向对象编程和复杂算法设计,更深刻理解了软件工程的完整流程。第一次作业中,基础逻辑门的实现让我建立起清晰的组件模型,而测试数据的“陷阱”则教会了我严谨验证的重要性;第二次作业拓展到三态门、译码器等复杂元件时,我在重构中领悟到架构扩展性的价值,并通过调试MUX引脚编号等具体问题,磨练了系统化排查能力。从需求分析、类设计、算法实现到调试优化,这个项目让我亲身体验了迭代开发的精髓——代码不仅是功能的堆砌,更是持续演进的艺术。这段经历不仅提升了我的技术实力,更培养了我面对复杂工程问题时,那种抽丝剥茧、步步为营的思考方式,为未来挑战更庞大的系统奠定了坚实基础。

浙公网安备 33010602011771号