模拟电路系统作业总结
一、前言
刚开始学Java的时候,我连一个完整的类都写得磕磕绊绊,没想到这三次PTA作业(作业集4、5、6)直接让我上手做了一个“数字电路模拟程序”,而且是一版一版往里面加东西。作业4做基础逻辑门,作业5加译码器和数据选择器这些复杂元件,作业6还要搞带反馈的时序电路,难度一次比一次大,但我居然真的跟下来了。
先简单说下这三次作业的整体情况:作业4最基础但也最混乱,就是刚学完类和对象,用最直接的方式写代码,要求实现与门、或门、非门、异或门、同或门五种基本元件,能读输入、算输出就行,不用考虑结构好不好,核心就是练继承、集合和基本的流程控制,题量看着不大,但对刚入门的人来说挑战不小。作业5是最难适应的一次,一下加了四个新元件(三态门、译码器、数据选择器、数据分配器),引脚的类型变多了,有控制引脚、输入引脚、输出引脚,而且有些元件输出不全的话要忽略掉。这次逼着我把之前糊在一起的代码拆成好几个类,电路管理归电路管理,输入解析归输入解析,不能再把所有东西都写在一坨。思维转不过弯的时候真的特别痛苦,但熬过去之后发现代码好像突然变清楚了。作业6是在前两次的基础上又加上了D触发器和JK触发器,电路里能出现反馈了,信号可以自己绕回来,这下之前那种“扫一遍全算完”的方法不行了,得想办法处理状态和时序。这次代码量最大,细节也特别多,但因为有前面两次的底子,思路反而比作业5刚重构时要清晰一些,重点是细心、不马虎。
三次作业难度一点点往上加,知识点也从最开始的继承、多态,慢慢过渡到设计类的分工、处理无效状态、模拟时序逻辑,完全跟着课程进度走。没有一上来就扔一个超难的完整项目,而是一步步引导,让我也能跟得上,也真正开始明白,编程不是写完就算了,还要写得工整、好改、不容易出乱子。这三次作业下来,我才算真正用Java写出了一个有点样子的东西,也一点点理解了面向对象设计到底是在学什么。
二、设计与分析
(一)作业4:第一次尝试,把五种基本门做出来
作业要求
作业4的核心是做一个能模拟数字电路的简单程序,支持与门、或门、非门、异或门、同或门这五种基本逻辑门。程序要能读入像INPUT: A-1 B-0这样的输入信号,还有[A A(2)1-1]这样的连接关系,然后自动算出每个门的输出,按照类型和编号排序打印出来。如果某个门的输入引脚没接全,就干脆忽略它,不输出。
我当时的思路
我一开始觉得,这五种门虽然逻辑不一样,但都有共同的特性:有输入引脚,有输出引脚,只不过计算方式不同。那正好可以用老师刚教的继承,搞一个抽象的父类叫Door,里面放上类型字母、编号、一个存输入值的列表in,还有一个输出值out。然后五种门分别写一个子类,每个子类里重写一个MakeOut()方法,与门就检查是不是所有输入都是1,或门就检查有没有一个1,非门就直接翻转,异或门和同或门就比两个输入一不一样。
所有东西都塞在Main类里
现在回头看,我作业4最大的问题就是所有逻辑都写在Main类里面。解析INPUT行、解析连接行、创建门对象、给门灌输入、一遍遍循环计算、最后排序输出,全挤在一个文件里。当时还觉得“反正就这么点东西,拆来拆去多麻烦”,结果写完之后自己看着都晕。后来老师讲到单一职责原则,我才意识到这种写法有多糟糕——想改一个地方,得在一大坨代码里翻半天,生怕改错了影响到别的功能。
模拟电路是怎么跑起来的
我用了一个很笨但能跑的办法:把所有门放在一个列表里,然后一遍遍循环。每次循环就去检查每个门,看它的所有输入引脚是不是都在信号表里能取到值。如果能取到,就立刻算输出,把算出来的输出信号存进全局信号表(一个HashMap,键是信号名,值是0或1)。然后继续循环,直到某一轮没有任何新门被算出来,就停止。因为题目保证了作业4里没有反馈回路,所以这个方法虽然有点傻,但肯定能停,而且不会算错。
一个很低级的Bug
写完作业4提交的时候,样例1和2都过了,但样例3怎么都过不去。我盯着代码看了快一个小时,最后发现是或门ODoor里面,我算出了输出结果out,却忘了把这个结果赋值给成员变量。局部变量算完就没了,所以输出一直是初始值0。就一行代码的事,折腾了大半个晚上。这个教训让我记到现在:操作成员变量的时候一定要想清楚,你到底是在给局部变量赋值,还是在给this.xxx赋值。
作业五
作业5在原来五种门的基础上,又加了四个新元件:三态门、译码器、数据选择器、数据分配器。这四种元件跟之前的基础门差别很大:它们有了控制引脚,有些输出多个值,有些在控制信号不对的时候根本不干活(输出无效)。这就不是简单加几个子类能解决的了,因为我原来的Door类里只用一个List<Integer> in来存输入值,根本没法区分哪个是控制引脚、哪个是普通输入引脚。
不得不重构
我试过直接在原来的代码上改,但越改越乱。最后干脆把抽象父类Door大修了一遍。我把输入输出都改成用Map来存,Map<Integer, String> pinSources记录每个引脚连到了哪个信号源,Map<Integer, Integer> inputValues存实际拿到的值,Map<Integer, Integer> outputs存输出值。这样一来,不管是控制引脚还是普通引脚,都用统一的方式管理,而且像三态门控制为0时不产生输出的情况,就直接不在outputs里放对应的key,输出时检查一下没有就不打印,自然地实现了“忽略无效元件”的要求。
终于学会拆分类
作业5我新建了两个类:Circuit和Parser。Circuit专门管所有元件和信号表,提供添加信号、建立连接、运行模拟、输出结果这些功能;Parser专门管读输入,把那些字符串解析成电路结构。Main类一下子就瘦身了,只剩三句话:读入、模拟、输出。这样拆完之后,改任何一个部分都不需要去动别的类,代码逻辑一下子清晰了很多。这也是我第一次真正体会到“单一职责”的好处,不是书上说说的空话。*四种新元件怎么处理的
- 三态门:控制端为1的时候才导通,输出等于输入;控制端为0的时候是高阻态,我直接不往outputs里放值,输出时跳过就完事了。
- 译码器:这个花了我最多时间。它有3个控制脚和n个输入脚,要满足S1=1且S2=0且S3=0才工作。工作的时候根据输入脚组成的二进制码,决定哪个输出脚置0,其余置1。我用了一个布尔变量
en来标记是否使能,使能的时候才记录输出,输出格式也跟别的门不一样,要单独处理。 - 数据选择器:根据控制脚选一路输入送到输出,逻辑跟译码器有点像,但简单很多。
- 数据分配器:反过来,一路输入根据控制脚送到某一路输出,其他输出都是无效状态,显示成“-”。
(三)作业6:时序电路,最头疼的反馈
作业要求
作业6又加上了D触发器和JK触发器,这跟之前的门最大的不同在于:它们能存状态,而且电路里可以出现反馈——输出信号能绕回来当输入。这样我原来那个“一遍遍扫直到稳定”的简单算法就不灵了,因为一旦有反馈,信号可能会无限更新,程序直接死循环。
为了解决死循环,我分了两步:先把所有不带状态的组合逻辑门(也就是前两次那些门)全都算到稳定,这一步还跟之前一样循环扫描;然后一次性更新所有触发器的状态,用上一轮的状态来计算这一轮的输出。这样每个循环算一个“周期”,模拟了时钟信号来一下的感觉。虽然跟真正的硬件仿真比差远了,但至少程序不会死循环了,也能通过PTA的测试样例。
触发器怎么实现的
D触发器就是存住输入的值,在“时钟沿”的时候更新;JK触发器复杂一点,J和K不同组合会决定是翻转、置0、置1还是保持不变。我给触发器类加了一个state变量来记当前状态,计算的时候根据输入和当前状态一起决定新状态。说实话,这部分我写得不怎么好,只是在样例上能跑通,离真正的边沿触发模拟还差挺远。但通过这次作业,我起码对时序电路有了个感性的认识,知道状态机大概是怎么回事了。
三、踩坑心得
这三次作业写下来,碰到的坑多得数不清,挑几个印象最深的记下来。
坑1:或门输出没赋值,样例通不过
前面已经提过,作业4里或门的输出我算出来了,但是忘了把结果存进成员变量。结果输出永远都是0,样例3死活不对。这个小错误浪费了我快一个小时。从那以后,我给所有计算结果赋值的时候都会特别留意,是给局部变量还是给成员变量,心里先默念一遍。
坑2:空指针异常,程序直接崩
题目说输入引脚不全的元件要忽略输出,我一开始没处理这种情况,直接去信号表里取值,取不到就抛出空指针异常,程序崩了。后来在计算之前加了一层检查,把需要的引脚全部遍历一遍,缺任何一个就直接返回false
坑3:三态门的高阻态输出格式不对
三态门控制端为0的时候是高阻态,题目要求“忽略该元件”。我一开始偷懒,把高阻态的输出值设成-1,结果打印出来是S1-2:-1,格式根本不对。后来改成检查输出map里有没有这个引脚,没有就直接返回null,上层碰到null就不打印,这才真正做到了“忽略”。
坑4:译码器控制条件理解有偏差*
题目写的是“S1=1,S2+S3=0时正常工作”,我一开始图省事用了(s2 + s3) == 0来判断。虽然因为S2和S3只能取0或1,结果碰巧是对的,但这种写法不严谨。如果以后条件变了,这种“取巧”的写法就可能出错。后来我改成了s2==0 && s3==0,虽然代码长了一点,但意思明明白白。
四、改进建议
- 用拓扑排序优化模拟速度
我现在模拟电路是用最笨的循环扫描法,门多的时候要扫很多轮,虽然PTA的测试数据量不大,但总觉得不够优雅。如果能根据连线关系把门排个序,没有反馈的部分一次就能算完,不用一遍遍循环。这个优化在作业6之前其实就能做,而且能顺带检测出不合法的环路。 - 用工厂模式管理元件创建
我在Circuit类里用了一长串if-else来判断元件名字然后new对应的类。以后如果再加新元件,这个方法会越来越长。如果用工厂模式,把每种元件的创建逻辑分散到各自的工厂类里,新增元件的时候就不需要改原来的代码,维护起来更方便。但对我现在来说,工厂模式还有点难,等以后学深了再回头改。 - 输入校验要加上
我现在的程序假设输入格式一定正确,多一个空格或者拼错一个字母就可能崩。虽然PTA环境里输入都是标准格式,但真正用的话肯定不行。后续应该学一下正则表达式或者简单的字符串处理,对输入做一下容错,给用户明确的错误提示,而不是直接崩溃。4. 用单元测试代替肉眼对比
这三次作业我测试全靠手输样例然后看输出,效率很低,而且容易漏掉边界情况。后来听同学说JUnit可以做单元测试,单独测每一个门的逻辑,自动判断对错,听起来就比我自己肉眼对比靠谱多了。这个技能我接下来一定要学。 - 进一步拆分类的职责
虽然我已经把Parser和Circuit拆出来了,但Circuit里面还是管得有点多(管元件、管信号、跑模拟、打印输出全在一起)。如果以后写更大的项目,可以把信号管理单独抽成一个SignalBus类,输出打印单独做成Reporter类,每个类就干一件事。这样改需求的时候心里更有底,不会牵一发动全身。 - 命名要规范,不给自己找麻烦
这次作业里,类多起来之后,名字如果不规范真的很容易搞混。我现在给类取名会尽量见文知意,比如ADoor就是与门,ODoor就是或门,而且严格区分大小写。养成这个习惯之后,就算隔一段时间回来看代码,也能很快想起来每个类是干嘛的。
五、总结
这段时间学到了什么
这三次作业做下来,我从一个连抽象类都写不利索的新手,变成了能独立写出几百行面向对象程序的人。具体来说:第一,继承和多态不再只是课本上的概念,我真真切切用它们解决了一个实际问题,父类写共同逻辑,子类写特殊逻辑,清楚又省事;第二,养成了把代码拆成多个类的习惯,从作业4把东西全塞Main类,到作业5抽出Circuit和Parser,真正体会到了单一职责的好处——修改一个地方不用提心吊胆怕影响别的;第三,排错能力涨了不少,从一看到报错就懵,到能顺着报错信息一步步定位问题,这个进步对我来说特别重要;第四,理解了什么叫做“迭代开发”,好的程序不是一口气写完美的,而是一版一版改出来的,每一版解决一部分问题,慢慢变好;第五,磨练了耐心,编程真的容不得半点马虎,一个字母、一个符号、一个关键字的疏忽,都可能导致程序跑不起来,长时间写代码也让我比以前细心了很多。
自身不足的地方
虽然作业都交上去了,但我很清楚自己还有不少薄弱的地方:面向对象更深层的东西比如接口、抽象类的设计技巧我还掌握得不好,现在只会照着需求写类,自己独立设计的话没什么头绪;代码优化能力也差,只知道怎么让程序跑通,不知道怎么让代码更简洁、效率更高;对于时序逻辑的理解也很浅,作业6的触发器模拟只能算勉强能用,离真正的硬件仿真差太远;还有各种辅助工具比如SourceMonitor、JUnit这些,只会最基础的操作,还没能真正用它们来改善自己的代码。
老师这种用同一个项目迭代好几次的作业方式,我觉得特别适合我这种初学者。不是一上来就给一个大而全的需求,而是一次加一点东西,让我们自己体会到代码是怎么在需求的推动下慢慢演变的。相比每次做一个完全不相关的小题,这种连贯的作业让我更能建立起“项目”的感觉,学得也更扎实。如果后面能再增加一些同学之间互相看代码、互相挑毛病的环节就更好了,自己代码里的问题自己往往看不出来,别人一眼就能发现。
这三次数字电路模拟作业,是我学Java以来写得最累、但也最有成就感的一段经历。从最开始连与或非都算不利索,到后来能处理带控制引脚的译码器,再到硬啃带反馈的触发器,每一步都走得磕磕绊绊,但每一步也都在往前走。以后的路还很长,我要学的东西还很多,但至少现在我有了一点底气,知道自己能从零开始把一个东西慢慢写出来,这种感觉还是挺不错的。

浙公网安备 33010602011771号