【PTA作业集4~6】阶段性总结Blog
一、前言:知识点、题量与难度总结
知识点:本阶段三次作业围绕“数字电路模拟程序”展开,进行三次迭代开发。
作业4主要知识点是继承与多态(Gate父类,五个子类)、引脚封装(Pin类)、电路模拟的传播算法、以及ArrayList的使用。核心是实现与门、或门、非门、异或门、同或门的逻辑模拟。
作业5新增了接口(Device)、三态门、译码器、数据选择器、数据分配器。重点是多引脚元件的引脚编号管理、控制引脚与输入输出的区分,以及Simulator统一调度的设计。
作业6增加了子电路的定义与实例化、连接信息的异常检测(5种错误类型及优先级处理)。强化了递归解析和错误处理机制。
题量:三次作业代码量逐渐增加。单次作业的元件种类从5种增加到9种,再加上子电路解析和异常检测,逻辑复杂度逐次提升。作业5调试耗时因新增元件的引脚顺序容易搞错而较长;作业6更是有许多测试点无论如何也没有通过,提交多次修改多次,但是每次都无法通过,也完全想不到原因。
难度:这三次迭代作业对我来说难度非常大,首次打开第四次作业时看见题目完全无法下手,从文字的理解就感觉到了困难,加上是生活中接触较少的领域,单单看题目就理解了很久才开始编写代码。就算是开始编写代码了,心里对整个程序也并不够清晰。后续迭代加入复杂电路、优先级等考量,就算最开始读懂了意思,在纸张上清晰地写出来,编码的时候还是不停地在思考,甚至作业6初次提交只有5分。
其中作业5难度最大。因为新增的译码器、选择器等元件引脚数量多且类型(控制/输入/输出)混杂,引脚编号顺序一旦搞错整个电路就出错。作业6的子电路和异常检测虽然增加了复杂度,但递归解析和优先级判断有清晰的规则,主要难点在于处理多个子电路实例时的命名冲突和多种异常同时发生时的优先级。
二、设计与分析
1. 作业4分析
(1)设计思路
作业4要求模拟与门、或门、非门、异或门、同或门构成的组合电路。主要功能包括:
- 解析输入信号(如“A-1 B-0”)
- 解析连接信息(如“[A A(2)1-1 B A(2)1-2]”)
- 构建电路元件和连接关系
- 传播电平信号,计算每个元件的输出
- 按顺序输出所有元件的输出引脚电平
通过阅读题目可以分析得到:Gate抽象类定义了引脚列表和输出引脚,子类AndGate、OrGate等重写compute()实现具体逻辑。Pin类封装引脚名和电平(用Integer允许null表示未就绪)。Circuit类管理所有元件和连接,核心算法是反复传播:当元件所有输入就绪时计算输出,若输出值变化则通过连接表更新下游,直到稳定。最开始并没有设计circuit类,但是考虑到后续迭代需要,加入了该类,最终整个程序共设计了以下类:
Main :主控类,负责读取输入、解析连接、构建电路、调用模拟和输出
Gate :门父类,定义引脚列表和输出引脚,提供isReady()和抽象compute()
AndGate :与门,继承Gate,实现compute()
OrGate :或门,继承Gate,实现compute()
NotGate :非门,继承Gate,实现compute()
XorGate :异或门,继承Gate,实现compute()
XnorGate :同或门,继承Gate,实现compute()
Pin :引脚类,封装引脚名称和电平值
Circuit :电路类,管理所有元件和外部输入,实现传播算法
(2)类图展示

从类图可以看出,作业4中:Gate作为父类被五个子类继承,每个子类重写compute()实现不同逻辑。Pin类独立,Circuit类包含所有Gate对象和连接映射。Main类依赖Circuit和Gate子类来完成整个流程。但是Main类承担了输入解析、电路构建、连接绑定等多个职责,违反了单一职责原则,这也是这次作业类设计的不足之处,后续可拆分出独立的Parser类。
(3)SourceMonitor报表内容分析

根据SourceMonitor的分析报告,作业4的代码度量情况如下:
- 总语句数176,注释比例17.9%。说明代码量适中,注释习惯在三次作业中是最好的。
- 最大圈复杂度为8,位于
Main.main()方法,因为该方法包含了输入解析、元件创建、连接建立等多个步骤,逻辑分支较多。 - 最大块深度5,平均块深度1.35,表明代码嵌套不深,可读性较好。
- 五个门子类的圈复杂度均为1~2,非常简洁,符合单一职责原则。
需要改进的地方:
Main类承担了过多职责,可以将输入解析逻辑单独抽取成Parser类。- 传播算法采用全量扫描,最坏情况O(N²),可改为拓扑排序优化。
(4)作业4心得
作业4由于对题目理解的困难,仅仅是阅读题目理解题目就花费了很多时间。不同与前三次作业给出类图的形式,这三次作业没有给出类图,因此对抽象能力有了更高的要求,我必须完全理解题目再自己画出类图设计好整个程序才能开始动手,但是并不熟悉的领域让我很难理解题目。这也提醒我:在未来的编程过程中是不可能一直面对自己熟知的、接触过的领域的,编程不仅仅是写出代码就可以,理解需求的能力一样重要。
通过作业4,我初步掌握了以下内容:
- 继承与多态的实际应用:父类定义骨架,子类实现具体逻辑,
compute()方法的动态绑定使得电路模拟非常灵活。 - 引脚封装与连接管理:
Pin类独立出来,方便存储电平值和判断是否就绪。 - 传播算法设计:反复扫描所有元件,直到输出稳定。虽然简单但容易理解。
实验过程中遇到的问题及解决办法:
- 传播算法无限循环:没有使用
changed标志,导致无变化时仍继续循环。加入boolean changed标志,一轮无变化则退出。 - 输入解析时元件未预先创建:连接信息中可能出现尚未创建的元件,我改为在解析连接时动态创建元件。
- 引脚编号解析错误:最初用简单的split,遇到“A(2)1-1”这种格式解析失败,后来用正则匹配。
2. 作业5分析
(1)设计思路
作业5在作业4的基础上新增了三态门、译码器、数据选择器、数据分配器。主要新增功能包括:
- 三态门:控制引脚为高电平时导通,否则输出无效(高阻态)
- 译码器:根据控制引脚和输入引脚的编码,仅一个输出为0,其余为1
- 数据选择器:根据控制引脚选择一路数据输入到输出
- 数据分配器:根据控制引脚将输入数据分配到一路输出
本次作业对前一次架构进行了重构:引入Device接口统一所有元件,BaseGate抽象类实现通用方法。新增了TriGate、Decoder、Multiplexer、Demultiplexer四个类。Simulator类取代Circuit,集中管理设备、连接和值传播。整个程序共设计了以下类:
Main :主控类,负责读取输入、解析连接、动态创建元件、调用Simulator
Simulator :模拟器类,管理设备、连接、值映射,运行传播算法并输出
Device :接口,定义所有元件必须实现的方法
BaseGate :抽象类,实现Device的部分通用逻辑
AndGate / OrGate / NotGate / XorGate / XnorGate :基础门
TriGate :三态门
Decoder :译码器
Multiplexer :数据选择器
Demultiplexer :数据分配器
Link :连接类,记录源引脚和目标引脚列表
(2)类图展示

从类图可以看出,作业5的类结构比作业4复杂很多:Device接口被所有元件实现,BaseGate作为抽象类减少了重复代码。Simulator类持有所有Device和Link,负责传播。Decoder和Demultiplexer有多个输出引脚,需要特殊处理。因此使用接口统一元件行为,便于Simulator统一调度,同时使用Link类单独记录连接,使得传播时容易查找。但是Simulator类承担了过多职责(设备管理、传播、输出),而且注释比例为0.0%,这是代码中比较突出的问题。
(3)SourceMonitor报表内容分析

根据SourceMonitor的分析报告,作业5的代码度量情况如下:
- 总语句数487,类/接口数14,每个类平均方法数15.29。说明类的数量增多,且
Simulator类的方法数偏多。 - 注释比例为0.0%,完全没有写注释。这直接导致作业6中复用
Decoder类时无法理解引脚对应关系,调试极其困难。 - 最大块深度达到9+,主要位于
Simulator.run()方法中。该方法包含两层循环(外层检测变化,内层遍历所有设备)以及对不同类型设备的特殊处理(译码器、分配器需要更新多个引脚),导致圈复杂度和嵌套深度都很高。 - 由于工具未识别出具体方法(报表显示“No methods found”),无法获取每个方法的精确圈复杂度,但从代码结构判断,
Simulator.run()的圈复杂度大约在7~8。
需要改进的地方:
- 必须添加注释,特别是引脚编号约定(如译码器0-2控制、3-5输入、6-13输出),写完代码几小时后就完全不知道自己写了什么了。
Simulator.run()应拆分为多个小方法,例如processDevice()、propagateOutputs()等,降低圈复杂度。- 可为每种元件单独编写
propagateOutput方法,避免在run中写大段if-else。
(4)作业5心得
通过作业5,我进一步掌握了以下内容:
- 接口与抽象类的配合使用:
Device接口规定了所有元件必须提供set、eval、valid等方法,BaseGate实现了通用部分,子类只需关注具体逻辑。 - 多输出元件的处理:译码器和分配器需要同时更新多个输出引脚,我在
Simulator.run()中单独判断设备类型并更新values映射。 - 引脚编号管理:译码器的0-2控制、3-5输入、6-13输出,必须严格对应,否则全错。
实验过程中遇到的问题及解决办法:
- 译码器引脚顺序容易搞反:题目文档中虽然写了顺序,但实现时容易按自然顺序处理。我通过在代码中添加注释明确每个段的含义,并在
set方法中根据引脚号范围判断类型。 - 三态门无效状态传播:控制为低电平时输出为null,我在传播时检查
valid(),只有有效才更新下游。 - 数据选择器控制引脚数量:根据控制引脚数(1或2)计算数据引脚数,用移位计算。
3. 作业6分析
(1)设计思路
作业6在作业5的基础上增加了子电路和异常检测。主要新增功能包括:
- 子电路:通过
C编号:定义内部电路,可以像元件一样在主电路中实例化 - 异常检测:连接信息中的5种错误及优先级处理
- 子电路的输入输出端口映射
本次作业扩展了Simulator:增加了subInputPorts和subOutputPorts记录每个子电路的端口,增加了internalLinks存储子电路展开后的内部连接,增加了checkErrors()方法实现5种优先级错误检测。类设计与作业5基本相同,增加了子电路相关数据结构。整个程序共设计了以下类:
Main :主控类,先解析所有子电路定义,再解析主电路
Simulator :增加了子电路端口映射、内部链接列表、错误检测方法
其余类与作业5一致
(2)类图展示

从类图可以看出,作业6的类结构在作业5基础上增加了子电路相关属性(subInputPorts、subOutputPorts、internalLinks)。Simulator类进一步膨胀。
为避免了引脚名冲突,为每个子电路实例添加前缀。但是Simulator类已经过于臃肿,方法数超过20,严重违反单一职责原则,且checkErrors()方法超过80行,圈复杂度极高,难以维护,这是本次实验的不足之处。
(3)SourceMonitor报表内容分析

根据SourceMonitor的分析报告,作业6的代码度量情况如下:
- 总语句数473,类/接口数10,每个类平均方法数23.4。
Simulator类的方法数进一步增加。 - 注释比例仍然为0.0%,作业5和作业6完全没有注释,这是极大的教训。
- 最大块深度8,平均块深度2.86。深度增加主要来自子电路递归解析中的循环和条件判断。
- 由于工具未识别方法,无法获得精确圈复杂度,但从代码看,
checkErrors()方法包含5种错误检测,每种都有循环和条件,整体圈复杂度估计超过10。
需要改进的地方:
- 必须为所有代码添加注释,特别是错误检测的优先级逻辑。
checkErrors()应拆分为checkMultipleOutput、checkNoInput、checkNoOutput、checkSequenceError、checkConflict等私有方法。- 子电路的递归解析可以单独抽取为一个
SubcircuitParser类。
(4)作业6心得
通过作业6,我进一步掌握了以下内容:
- 子电路的递归解析:为每个实例添加前缀,并将内部连接“展平”为主电路中的
internalLinks。 - 错误检测的优先级实现:按照题目规定的顺序依次检查,一旦发现直接返回,不再继续。
- 命名空间隔离的重要性:没有前缀会导致不同子电路实例的引脚互相覆盖。
实验过程中遇到的问题及解决办法:
- 子电路引脚名冲突:两个相同子电路实例的内部引脚被当作同一个引脚。解决方法是添加实例前缀(如“C1-”)。
- 错误检测优先级顺序:一个连接同时有多个输出和无输入,应该输出“多个输出”的错误,但我一开始先检查了“无输入”。后来严格按照题目顺序调整。
- 多个异常同时发生时,题目规定只输出优先级最高的第一个错误。我实现了“早返回”模式,但在处理多个连接时仍需小心,尚未完全验证所有边界情况。
- 子电路的输入输出端口在主电路中引用时,我使用端口集合判断,基本可行,但代码比较臃肿。
三、踩坑心得
在三次作业的提交过程中,我遇到的问题及解决过程记录如下:
1:译码器引脚顺序容易混淆(作业5)
问题:译码器有控制引脚、输入引脚、输出引脚,顺序一旦写错,输出全错。我最初按照自然顺序(0,1,2,3...)处理,导致控制信号和输入信号错位。
原因分析:题目明确规定了引脚编号范围(0-2控制,3-5输入,6-13输出),但我没有仔细对照文档。
解决方案:在代码中用注释明确每个引脚段的含义,并在set方法中根据引脚号范围判断类型。例如:if (pin>=0 && pin<3) ctrl[pin]=val; else if (pin>=3 && pin<6) input[pin-3]=val;。
心得:对于多引脚元件,一定要先画出引脚编号表再编码。
2:注释缺失导致后期无法维护(作业5、6)
问题:作业5代码几乎没有注释,作业6想复用Decoder类时,完全不记得引脚对应关系,花费大量时间重新分析。
原因分析:赶进度忽略了注释,以为代码自己能看懂,但一周后就看不懂了。
解决方案:从此强制要求每个类和重要方法都写注释,特别是引脚编号约定和复杂算法。作业6中虽然补充了一些,但也完全不够。
心得:没有注释,代码非常难维护,一定要养成写注释的习惯。
3:子电路引脚名冲突(作业6)
问题:两个相同子电路实例的内部元件引脚被互相覆盖,导致一个实例的输出传到另一个实例。
原因分析:子电路内部的引脚名(如“A(2)1-0”)没有加实例前缀,导致不同实例的同一引脚名指向同一个Pin对象。
解决方案:为每个子电路实例添加唯一前缀(如“C1-”),所有内部引脚和元件名都加上此前缀。同时,子电路的输入输出端口也要加上前缀,以便在主电路中查找。
心得:实例化可复用模块时,必须使用命名空间隔离。
4:异常检测的优先级顺序(作业6)
问题:一个连接同时触发多种异常,程序应该输出优先级最高的错误。我一开始没有严格按照题目顺序检查,导致输出错误类型。
原因分析:题目列出的5种错误有明确优先级(1>2>3>4>5),但我没有仔细看,随意写了检查顺序。
解决方案:按照题目列出的1~5顺序依次检查(1.多输出,2.无输入,3.无输出,4.顺序错误,5.输入冲突),一旦命中立即返回,不再继续。
当前状态:基本实现,但尚未完全验证所有组合情况(如多个连接同时包含异常时,是否只处理第一个)。
5:传播算法效率低(作业4、5、6)
问题:全量扫描每次变化都遍历所有设备,最坏情况O(N²)。在大规模电路中可能超时。
原因分析:选择了简单的实现方式,未考虑性能优化。
解决方案:可以改为拓扑排序按依赖顺序计算,一次传播完成。目前尚未重构,这是潜在的性能隐患。
心得:简单算法适合小规模,但应考虑更优方案。
四、改进建议
1. 拆分Simulator类
目前Simulator类承担了设备管理、链接存储、传播算法、错误检测、子电路处理等多个职责,代码超过400行。建议拆分为CircuitBuilder(解析构建)、PropagationEngine(传播算法)、ErrorChecker(异常检测)等独立类。
2. 强制添加注释
从作业5开始注释比例为0%,这是不可接受的。强制要求每个类、每个public方法、每个复杂逻辑块都有注释。特别是引脚编号约定必须写清楚。可以使用JavaDoc格式。
3. 优化传播算法
全量扫描效率低,可改为拓扑排序按依赖顺序一次计算完成。对于无环的组合电路,这是更优解。目前尚未实现。
4. 统一错误处理机制
目前的checkErrors()直接打印错误并System.exit(0),可改为抛出自定义异常或收集错误列表,由上层决定处理方式,便于单元测试和后续扩展。
5. 编写单元测试
为每种元件的逻辑编写独立的JUnit单元测试,而不是每次都跑完整电路。例如单独测试译码器在给定输入下输出是否正确,这也有助于发现引脚顺序问题。
五、总结与展望
学到了什么
- 掌握了Java接口与抽象类的实际应用场景。Device接口统一了所有元件的对外方法,BaseGate提供了公共实现。
- 理解了组合逻辑电路的模拟方法,特别是多输出元件的特殊处理(译码器、分配器需要更新多个引脚)。
- 学会了子电路的定义、实例化和命名空间隔离的技巧。
- 熟悉了错误检测的优先级处理模式,以及多种异常类型的判断逻辑。
需要进一步学习及研究的地方
- 注释习惯:必须从作业5的0%提升到20%以上。养成边写代码边写注释的习惯。
- 方法拆分:避免超过50行的方法,降低圈复杂度。Simulator.run()和checkErrors()必须拆分。
- 异常处理:学习使用自定义异常,而不是System.exit。
- 算法优化:学习拓扑排序,改进传播算法效率。
对课程、作业等的建议
- 关于作业迭代设计:三次作业以迭代方式逐步增加功能,让我体会到了扩展的挑战。但作业5到作业6的难度跳跃较大,建议中间增加一次小作业熟悉多引脚元件。
- 关于课上教学:希望上课ppt可以上传群文件,有时候下课忘了或者没跟上下课还可以自学一下。
浙公网安备 33010602011771号