Java大作业第4~6次的综合性总结
一、前言
1.知识点
这三次题目里,所涉及的字符串匹配问题,侧重的应用不同。
对于答题判题程序,侧重点在于字符串的纠错匹配,因为会有不合法输入
对于后面的电路题,侧重点在于匹配和按格式分组(目前题目里没有错误输入处理)
答题判题程序4的字符串类型变化丰富,比较复杂,相较于第3次的来说其实变化不大,第四次的重点在于类的继承,不止第四次,目前两次电路程序涉及最多的也是类的继承,它允许一个类继承另一个类的属性和方法。通过继承,子类可以重用父类的代码,并且可以根据需要扩展或修改这些代码,其基本语法为:
class 子类名 extends 父类名
{
//子类特有的属性和方法
}
类的继承拥有以下特点:
- 属性继承
子类可以继承父类的实例变量(属性)。
子类可以直接访问父类的非私有(private)成员,如protected、public等。
如果子类需要访问父类的私有成员,可以通过父类提供的公有方法来访问。 - 方法继承:
子类继承父类的方法,但如果需要,可以重写(Override)父类的方法,提供更适合子类的实现。
子类可以直接调用父类的公共方法。 - 构造函数
子类不会继承父类的构造函数,但可以通过super()调用父类的构造函数。
如果父类没有无参构造函数,子类必须显式调用父类的构造函数,并传递相应的参数。
在实际使用的过程中,我们常使用的是这三个关键字:
- extends:用来表示继承关系。
- super:用来访问父类的成员变量或方法,或调用父类的构造函数。
- this:用于访问当前对象的成员变量和方法。
例如在答题判题程序4中,新增加了选择题和填空题,而它们相较于原始题目,只增加了一些新的方法,属性都是一样的:
protected int number;//题目编号
protected String content;//题目内容
protected String standardAnswer;//标准答案
protected boolean stata;//题目状态,true表示已添加,false表示被删除
所以可以使用entends来进行继承,然后添加新的属于自己的特殊方法。在构造时,由于属性和父类完全相同,故在构造方法中使用super:
//选择题
class Question_Select extends Question
{
public Question_Select(int number, String content, String standardAnswer)
{
super(number, content, standardAnswer);//调用父类构造函数
}
//...
}
//填空题
class Question_Fill extends Question
{
public Question_Fill(int number, String content, String standardAnswer)
{
super(number, content, standardAnswer);//调用父类构造函数
}
//...
}
继承会用之后,后面的电路题也会方便很多。
2.难度
大作业都是循序渐进的,题目难度不仅和本身内容有关,还和之前的设计有关。
(1)第四次答题判题程序新增两个题目类型,需要用到继承,字符串处理没多大变化,和之前三次的差不多。
(2)第一次电路程序不算很难,大概就是几个元器件,而且都是串联的,也不涉及什么错误输入,一行就一个链接信息,所以只需要使用contains()方法匹配指定字符然后判断就行。
(3)第二次电路程序相较于第一次,新增了并联电路,而且一条电路信息是整个输入完的,单使用contains()方法很难写,需要使用正则表达式来对字符组“[ ]”里的内容进行匹配。
二、设计与分析
1.第四次答题判题程序
通过读题我们发现,新增加的两个类“选择题”和“填空题”其实属性和“Question”是一样的,只不过有了各自的新的方法(例如checkAnswer()),所以在这里我们使用继承来实现,继承父类的基本方法和属性。同样的,在这次程序中,由于选择题和填空题处理答案的方式不同,所以在继承中重写checkAnswer()方法。
新增的类图如下:

总体的处理流程也是在上次的基础上又加了两种题目的处理而已,由于题目、答题信息等均为乱序输入,所以我们在整体信息全部录入完成之后再对其进行处理,总体处理流程如下:

相比于前三次程序,这一次的程序将输入的信息种类增加到了7种,有了前几次的处理经验之后,这次其实就是扩展原有功能,以我的设计框架作为分析对象,添加内容如下:
在输入信息的录入部分,添加处理两种新题型的分支:
else if(qSMatcher.matches())//此处处理选择题信息题目信息 #Z:
{
makeSelectQuestionInformation(count, questionSelects, str);//将选择题信息存入
infirmationFlag = true;
}
else if(qFMatcher.matches())//此处处理填空题信息题目信息 #K:
{
makeFillQuestionInformation(count, questionFills, str);
infirmationFlag = true;
}
在遍历试卷题目这里,新增两种题型的寻找处理:
if(answerFindFlag == false) System.out.println("answer is null");
else//进入这个else之后,开始对每个题目进行检索(普通题、选择题、填空题)
{
//int questionOrderNum = testAnswer.getQuestionOrderNum(j);//获取题目顺序编号
int tempQuestionNum = testPaper.getQuestionNum(j);//获取题目编号
boolean findFlag = false;
if(!findFlag) findFlag = findFromQuestion(testAnswer,testPaper,questions);//从普通题中寻找,找到就处理,同时返回状态码
if(!findFlag) findFlag = findFromSelectQuestion(testAnswer,testPaper,questionSelects);//从选择题中寻找,找到就处理,同时返回状态码
if(!findFlag) findFlag = findFromFillQuestion(testAnswer,testPaper,questionFills);//从填空题中寻找,找到就处理,同时返回状态码
if(findFlag == false) System.out.println("non-existent question~0");
}
2.第一次电路程序
初次见到这个程序时确实是一头雾水,这就需要我们将题目多读几遍,把样例多看几遍,就能看出解题的思路了。第一次题目拥有三个控制设备和三个受控设备,控制设备的作用主要是调节电路电压(调速器)以及控制电路通断(开关),而受控设备则是根据电压差来进行工作,他们都有一个共同属性,那就是电路设备,所以我们写一个抽象父类CircuitDevice,由于各个电路设备对于输入输出的电压处理不同,所以里面包含两个抽象方法“getInputVoltage()”和“getOutputVoltage()”,由子类实现,在第一次题目中,受控设备都没有电阻,而且调速器只可能接在VCC上,所以处理起来非常简单,没有调速器的情况下只需判断电路所有开关是否闭合即可,然后所有设备都分配满电压。有调速器时,在“给设备分电压”的这一步前先根据调速器设置好输入电压,然后继续上面的步骤就好了。
考虑到后续会有新的电路。比如并联电路、整个串并联电路等。若是都继承Device则显得过于庞大了,所以我单独创建了一个Circuit类,然后将其它设备都聚合起来,一个Circuit对象就代表了一段完整的电路。同时提供对内部调速设备、开关设置的方法。
类图如下:

总体处理流程如下:

对于电路设备的信息的处理,我全部使用一个dealWithDeviceInformation()方法来处理,只传入一个字符串,然后根据字符匹配来进行相应的处理,在方法内部,首先会进行“连接信息”和“控制信息”的判断,然后再在分支里进行小模块的判断,对于连接信息,通过首尾判断后,将设备添加到设备列表当中,首尾判断的目的是设置电压输入端和输出结束端,对于电路设备的添加,统一使用addDevice()方法来进行设备的添加,addDevice()方法会自动判断该设备是何种设备,以及判断该设备是否已经被添加,连接信息的处理分支如下:
if(ctMatcher.find())//连接信息
{
String Content1 = ctMatcher.group(1);
String[] Content = Content1.split(" ");
//VCC和GND要特殊处理
if(Content[0].equals("VCC"))//第一条消息
{
//此处提取设备类型和编号
circuit.addDevice(letter, number);
}
else if(Content[1].equals("GND"))
{
//此处提取设备类型和编号
circuit.addDevice(letter, number);
}
else
{
//此处提取设备类型和编号
circuit.addDevice(letter, number);
//此处提取设备类型和编号
circuit.addDevice(letter2, number2);
}
}
对于控制信息,我在Circuit类中设计了三个方法用以对电路内的控制设备进行设置,在判断到信息为控制信息后,匹配第二个字符来判断是何种设备的控制信息,然后在分支中提取到对应的信息(如设备ID、调速器的调速信息)后,调用相应的set函数,改变电路设备的状态,控制信息的处理分支如下:
else if(clMatcher.find())//控制信息
{
// 获取匹配到的字符串
String matchedString = clMatcher.group();
// 将字符串转为字符数组
char[] charArray = matchedString.toCharArray();
if(charArray[1] == 'K')
{
String switchID = String.valueOf(charArray[2]);
circuit.setSwitch(switchID);
}
else if(charArray[1] == 'F')
{
String gearSpeedControllerID = String.valueOf(charArray[2]);
String gearOperate = String.valueOf(charArray[3]);
circuit.setGearSpeedController(gearSpeedControllerID, gearOperate);
}
else if(charArray[1] == 'L')
{
String continuousSpeedControllerID = String.valueOf(charArray[2]);
String continuousSpeedControllerPosition = new String(charArray, 4, charArray.length - 4);
circuit.setContinuousSpeedController(continuousSpeedControllerID, continuousSpeedControllerPosition);
}
}
3.第二次电路程序
相比于第一次程序,本次新添加了并联电路,并且将电路的链接信息变为了一整行输入,所以我们必须将对输入信息的处理逻辑进行修改,不仅要做到精准判断,还要在一行信息当中提取出我们需要的连接信息(即“[ ] ”内的内容)。本次题目新加的并联电路暂时只有一个,但考虑到后面的迭代会添加很多,所以还是将并联电路单独继承一个类出来比较好处理,然后再继承一个SuperCircuit类来将串并联电路进行集成,方便后续处理。
对于新添加的电阻,在CricuitDevice类里扩展即可,同样的,由于我们要将整段电路当做是一个电路设备来处理,所以在Circuit类中也要添加Resistance属性,并提供get方法。特殊的,对于串联和并联电路,我还提供了一个电阻的计算方法,写在了get方法中,每次调用时就会计算一次,这样做是为了保证在后面的控制信息接收到后,不用每接收一次就重新计算整个电路的电阻,而是在所有操作都完成、需要用到电阻的时候再进行计算。
由于我们现在将整个电路看做了一个Device,所以在添加设备时,不同电路的处理逻辑不同,例如普通的串并联电路只需要添加设备即可,但包含串并联电路的SuperCircuit则则不能继续使用addDevice()来添加。所以我在addDevice()方法上提供了新的方法:updateDeviceList(),该方法用于将电路和设备都添加进来,addDevice()方法包含在里面被调用。
类图如下:

程序主要执行的大体流程(基于第一次程序做的修改):

对于电路设备的信息的处理,还是使用一个dealWithDeviceInformation()方法来处理,再在该方法当中根据不同的电路信息进行不同的分支处理,函数结构如下:
public void dealWithDeviceInformation(SuperCircuit circuit,String str)
{//处理所有输入的设备信息
/**
*此处为正则表达式的匹配和处理
*/
//...
if(seriesMatcher.matches() && str.contains("VCC"))
{//总电路信息 /*总电路中只将整体信息存入设备列表,单个的设备(包含在串、并联电路中的)将不再计入总电路设备*/
//此处对总电路信息进行分割提取
//...
while (matcher.find())
{
String Content1 = matcher.group(1);//提取方括号内的内容
String[] Content = Content1.split(" ");//将内容按空格分割
if(Content[0].equals("VCC"))
{//第一条消息
//更新设备列表和实际设备信息
//...
circuit.addDevice(letter, number);
circuit.updateDeviceList(number, letter);//更新电路设备列表
}
else if(Content[1].equals("GND"))
{
//更新设备列表和实际设备信息
//...
circuit.addDevice(letter, number);
circuit.updateDeviceList(number, letter);//更新电路设备列表
}
else
{
//更新设备列表和实际设备信息
//...
circuit.addDevice(letter2, number2);
circuit.updateDeviceList(number2, letter2);//更新电路设备列表
}
}
}
else if(seriesMatcher.matches() && str.contains("IN"))
{//串联电路信息
//此处对总电路信息进行分割提取
//...
while (matcher.find())
{
//分割
if(Content[0].equals("IN"))
{//第一条消息
//更新串联设备列表和实际设备信息
//...
}
else if(Content[1].equals("OUT"))
{//末节点
//更新串联设备列表和实际设备信息
//...
}
else
{//中间结点
//更新串联设备列表和实际设备信息
//...
}
}
}
else if(parallelMatcher.matches())
{//并联电路信息
//此处对总电路信息进行分割提取
//...
for(int i = 0; i < Content.length; i++)
{
//更新串联设备列表和实际设备信息
//...
}
}
else if(clMatcher.find())
{//控制信息
String matchedString = clMatcher.group();// 获取匹配到的字符串
char[] charArray = matchedString.toCharArray();//将字符串转为字符数组
if(charArray[1] == 'K')
{
//更改总电路设备状态
//更改子电路设备状态
}
else if(charArray[1] == 'F')
{
//更改总电路设备状态
//更改子电路设备状态
}
else if(charArray[1] == 'L')
{
//更改总电路设备状态
//更改子电路设备状态
}
}
}
三、踩坑心得
1.答题判题程序4
得益于前几次的踩坑,这次程序已经能应付大部分潜在bug,所以写好新题目的扩展和处理后,提交没有任何问题,一次测试点全部通过,没有踩坑。

2.电路程序1
有了前几次在设计上踩坑的经历,在写这次程序时,我并没有直接开写,而是现在纸上分析了很久,最终确定了一个合适的解决方案。但尽管如此,还是出现了潜在的问题,首先是第一次提交后发现的问题:

从上往下慢慢想,看到提示时我明白了,这是整个电路只有一个设备,VCC直连进,GND直连出,于是赶快写了个测试样例试了一下,非零返回,打开代码一看,发现是makeCircuit()方法里连接电路的函数出了问题:
if(i == 0)
{
lastDevice = "VCC";
nextDevice = deviceNames.get(i + 1) + deviceIds.get(i + 1);
inputVoltage = 220;
outputVoltage = 0;
}
在这里由于不存在nextDevice,所以访问时会越界,从而非零返回,于是我在第一个设备里新添了一个判断,保证不会访问越界:
if(i == 0)
{
lastDevice = "VCC";
if(i == deviceIds.size() - 1) nextDevice = "GND";
else nextDevice = deviceNames.get(i + 1) + deviceIds.get(i + 1);
inputVoltage = 220;
outputVoltage = 0;
}
上面三个测试点算是过了:

下面这四个测试点,我测试了很多例子也没过,于是我想到了从题目当中来获取信息,再次读题时发现了一条规则,那就是相同设备需要排序后再输出,于是在showAllDevices()方法中添加sort方法:
public void showAllDevices()//显示所有设备信息
{
sortSwitchs();
sortGearSpeedControllers();
sortContinuousSpeedControllers();
sortIncandescentLamps();
sortFluorescentLamps();
sortCeilingFans();
//输出设备信息
//...
}
再次提交后果然没错了:

3.电路程序2
有了上一次读题的经历,这一次我反反复复将题目读了好多遍,还是遵循“先设计再编码”的原则,设计草稿画了3天,最后用一填时间来编码,果然,“最好的测试点都藏在题目里”,把题目研究明白无遗漏后,样例全过就是水到渠成的事了。

四、改进建议
由于上次踩了很多坑,所以这次开始,我更加注重于设计逻辑,保证草稿上的东西能行得通,再去实际实现,然后在实现的过程中再慢慢发现问题、解决问题。随着不断地进步,旧的问题解决了,也会出现新的问题,比如说虽然这次main中的代码非常少,但我的Circuit类显的过于庞大。尤其是继承后的SuperCircuit类,由于要处理串并联电路,所以SuperCircuit中又新增了许多方法,使得整个类的功能非常丰富,但是比较臃肿,这导致后面出现了新问题,那就是代码长度超限的问题,写完电路程序2后,总共有1300+行,提交不了,于是我将其缩减到了1000+行后才能提交。后面问了老师后,老师说是按照字节来算的,所以我超长的原因可能是变量名函数名太长,基于这些问题,我对未来我的改进建议是:
- 继续保持“先设计再实现”的习惯
- 类的职责可以再进一步的进行细分,这样不会显得类很臃肿
- 既然用了继承,那就多考虑子类中是否有相同方法,进行归一
- 尽量使用简短但易懂得变量名以及函数名
- 继续考虑代码的可扩展性,避免“牵一发而动全身”
五、总结
写了这几次大作业后,我意识到了很多方面的重要性,也确实从中得到了好处。比如答题判题程序4让我意识到了可扩展的重要性,在这次题目当中,我几乎没有写很多东西,就是添加了两个新题目进去,也没有出很多错,测试点一遍全部通过。正是因为之前的代码分工较明确,之间的关联也很少,而且改修的bug也修了,所以这次才会写的比较轻松。
而电路程序则是让我意识到了读题和设计的重要性,第一次电路程序就是因为读题不仔细,遗漏了“排序”这一点,导致后面4个测试点卡了好久,等第二次电路程序时,我便先反复读题分析,再进行设计,最后也没有卡任何测试点,我想这大概就是“仔细读题”带来的正反馈把。
浙公网安备 33010602011771号