OO第一单元作业总结

一、整体概述

  第一单元三次作业主要目的是对表达式结构进行建模然后进行解析和化简去除不必要的括号。而表达式结构则包含了表达式,项和因子,层层嵌套,互相包含。而这三次作业中表达式化简的难度也层层递进,不断迭代。笔者在此使用的是一般输入模式。接下来笔者将对这三次作业进行总结以得到新的感悟,希望诸君也能有所收获。

二、程序结构与度量分析

1.第一次作业

  第一次作业通过对表达式结构进行建模,完成最基础多项式的括号展开,从中我初步体会到了层次化设计的思想。笔者将根据类图和复杂度进行深入的程序结构和度量分析。

第一次作业UML图如下所示:

 

   在第一次作业中,由于形式化描述的种种限制以及题目的要求,此次表达式化简仅设计到常数和x的幂次方,也就是一元多项式的形式。这也就使得我个人的Factor类使用了Hashmap的形式来存储多项式。Hashmap的键是x的幂次,变量类型为整型,而对应的值则为系数,变量类型为大整型。这样,只需要Hashmap一个变量就可以存储所有可能存在的多项式。同时,笔者使用了表达式(Expr),项(Term)和因子(Factor)三层的层级结构。表达式包含无数个项,而项又包含无数个因子,但同时项和表达式本身也可以成为因子,便继承了Factor类。通过层级结构,只需要用+-号将输入的表达式进行区分,获得无数个项,再通过*号将项拆开,获得无数个因子,最后再对因子进行解析。对因子的解析个人使用的便是正则表达式,将因子情况分成常数,x的幂次,表达式,表达式的幂次,十分清晰明白。这样,表达式化简的结构便初步搭建完成,优点极为明显,因为层次结构清晰,所以可拓展性极强,后续的2次作业中也是在此基础上拓展而成。

  但在第一次作业中有一个最大的难点便是括号的展开。因为括号里同样存在+-*的符号,可能会影响表达式和项的拆分。但是在第一次作业中,题目明确规定只能包含一层括号,不存在嵌套括号的产生。于是笔者便决定先对输入的字符串表达式进行处理,将括号里的表达式单独提取出来(此时提取的表达式没有括号)进行表达式解析得到最终多项式的HashMap,并存储在主函数的列表中,运用Parser函数确定要取出的括号和即将取出的括号内表达式的位置,因为括号只有一层,从左向右逐步解析必然顺序是固定的,因此当需要用到括号时,便提取出列表中存放的因子表达式结果,并进行相关运算,得到最终结果。这种方法的好处自然是操作简单,省去括号处理的烦恼。但这样做法的缺点也是极大,因为这样的处理方法完全无法处理嵌套括号,因此在后续作业中只能重构,几乎没有可拓展性。

  总结来说,第一次作业确定了表达式-项-因子的三层基础结构,为后续作业的完成奠定了一个关键性的基础,虽然简单,但却十分重要。

第一次作业类复杂度及方法复杂度图示:

Expr 6.0 10.0 18.0
Factor 1.8571428571428572 3.0 13.0
Main 2.0 2.0 2.0
Parser 1.0 1.0 2.0
Term 6.5 11.0 13.0
Expr.expr() 21.0 1.0 10.0 10.0
Expr.Expr(String, ArrayList, Parser) 6.0 1.0 6.0 6.0
Expr.getResult() 4.0 1.0 3.0 3.0
Factor.add(Factor) 1.0 1.0 2.0 2.0
Factor.Factor() 1.0 1.0 2.0 2.0
Factor.getFactors() 0.0 1.0 1.0 1.0
Factor.getResult() 0.0 1.0 1.0 1.0
Factor.mul(Factor) 3.0 1.0 3.0 3.0
Factor.pow(Factor) 1.0 1.0 2.0 2.0
Factor.sub(Factor) 1.0 1.0 2.0 2.0
Main.main(String[]) 1.0 1.0 2.0 2.0
Parser.getp() 0.0 1.0 1.0 1.0
Parser.setp(int) 0.0 1.0 1.0 1.0
Term.getResult() 1.0 1.0 2.0 2.0
Term.Term(String, ArrayList, Parser) 42.0 1.0 11.0 11.0

  由图示可以看出第一次作业中Expr和Term类圈复杂度较大,而原因便是其中自身构造函数的复杂度高。经笔者分析,因为Expr和Term的构造函数涉及到表达式解析最重要的一步便是拆分。Expr将表达式拆分为各个项相乘,而Term将项拆分成一个个因子相加,所以复杂度都要很高。同时在Term里还使用了正则表达式进行多次if判断来找出因子类型,所以复杂度最高也是意料之中,而其优化可能性并没有很高。

2.第二次作业

  第二次作业相比之第一次作业复杂了非常之多,多出了自定义函数,三角函数以及求和函数。函数调用以及三角函数的化简难度可以称之为极高,也因此第一次作业的程序结构已经不再满足第二次作业的需求。第二次作业的程序结构对此除了表达式-项-因子的三层基础结构仍然存在之外其他方面笔者都进行了大规模重构才得以实现第二次作业的功能。

第二次作业UML图如下所示:

 

   由图可知,此次作业对于三角函数的出现,笔者创建了一个Tri类用于存储三角函数的各个信息,包含名称(sin或cos),括号内系数,括号内幂次,括号外幂次。同时一个Factor已经很难使用HashMap来存储表达式所有的信息,所以笔者又创建了一个类名叫SmallFactor,直译为小因子。如果说因子可以包含所有可能出现的值,那么SmallFactor便是用来存储单个项的因子,也就是不包含+-号的连接,同时也排除括号等情况。这样小因子便存在系数,x的指数,三角函数的列表来存储一系列三角函数相乘。这样一个最基础的项的属性便构建完成。而因子则包含小因子的列表,这样在方便化简的同时也将整个表达式的结构全面清晰地确定下来。同时因为有了小因子的存在,三角函数相乘与三角函数相加的化简便显得尤为顺利,只需要2个判断函数同时对因子的每个SmallFactor进行一一比较,便可最终化简。

  但是三角函数只是第二次作业得一个难点之一,自定义函数与求和函数则显得更为棘手。首先因为这两种函数调用的存在,以及三角函数自带的括号,嵌套括号表达式解析的问题必须先得以解决。因此我在这里使用了递归下降的方法,从右到左依次递归,将表达式递归拆成一个个项,再将项递归拆成一个个因子,而判断括号内外的运算符号便可以使用字符串的遍历对左右括号进行类似栈的处理来判断括号的层数防止出现不同层级括号的不对应,这样便可以将表达式通过一个个运算符号拆成左右2个表达式,项和因子,再递归下去,实现递归下降的处理办法。这里要提到一点,如果使用左递归,在遇到-符号的时候会将-右边的整个表达式都加上负号,而这负号本身其实只是一个项的负号,便会产生问题。因此笔者在此使用右递归的方法从右至左进行拆分,便可以顺利得出结果。同时在遇到括号的时候通过括号内的字符串新建一个表达式变量,再进行解析,不断循环下去解除括号,这样嵌套括号的问题也迎刃而解。

  嵌套括号的问题解决了,自定义函数和求和函数的调用便是最后的难点,也是这次作业最容易出错的地方。字符串的直接替换容易导致表达式本身的含义发生改变,因此个人便考虑在字符串替换时对自变量和因变量的外围都加上括号,以免与其他运算符产生冲突导致含义改变。在个人的类中Initial用于处理自定义函数,Sum用于处理求和函数。将表达式中的函数处理完成后将其字符串替换到原本输入的表达式中,结果十分顺利。但这里需要注意的是普通的三角函数内部只有一层括号,如果自定义函数里包含三角函数,那么三角函数里便可能存在2层括号,这是需要进行一定处理和规避的。因此笔者此法虽然处理函数较为简单,但其中却可能蕴含很多小问题,一旦疏忽便会产生错误,是以并不是最优解,可以进一步进行优化,比如将函数提前转化为因子存在再进行解析会省去很多烦恼。

  总得来说,第二次作业是这三次作业里难度跨越最大的,也是最为棘手的。但一旦将第二次作业成功通过后,只要架构相对合理,可拓展性强,第三次作业便会十分轻松惬意。

第二次作业类复杂度及方法复杂度图示:

Expr 11.0 11.0 11.0
Factor 3.3333333333333335 11.0 30.0
Initial 4.0 6.0 12.0
Main 6.0 11.0 12.0
SmallFactor 1.8571428571428572 7.0 26.0
Sum 2.0 3.0 4.0
Term 5.714285714285714 16.0 40.0
Tri 1.0 1.0 11.0
Expr.Expr(String) 18.0 7.0 15.0 15.0
Factor.add(Factor) 9.0 4.0 6.0 6.0
Factor.easy() 27.0 5.0 8.0 11.0
Factor.Factor() 0.0 1.0 1.0 1.0
Factor.getFactors() 0.0 1.0 1.0 1.0
Factor.getResult() 0.0 1.0 1.0 1.0
Factor.mul(Factor) 3.0 1.0 3.0 3.0
Factor.pow(Factor) 1.0 1.0 2.0 2.0
Factor.setfactor(ArrayList) 0.0 1.0 1.0 1.0
Factor.sub(Factor) 9.0 4.0 6.0 6.0
Initial.getname() 0.0 1.0 1.0 1.0
Initial.Initial(String) 6.0 1.0 4.0 5.0
Initial.use(String) 7.0 1.0 6.0 6.0
Main.main(String[]) 22.0 1.0 16.0 17.0
Main.out(String) 0.0 1.0 1.0 1.0
SmallFactor.add(SmallFactor) 0.0 1.0 1.0 1.0
SmallFactor.getindex() 0.0 1.0 1.0 1.0
SmallFactor.getratio() 0.0 1.0 1.0 1.0
SmallFactor.gettri() 0.0 1.0 1.0 1.0
SmallFactor.judgesame(SmallFactor) 16.0 7.0 4.0 7.0
SmallFactor.mul(SmallFactor) 8.0 4.0 5.0 5.0
SmallFactor.pow(SmallFactor) 1.0 1.0 2.0 2.0
SmallFactor.setindex(BigInteger) 0.0 1.0 1.0 1.0
SmallFactor.setratio(BigInteger) 0.0 1.0 1.0 1.0
SmallFactor.settri(Tri) 0.0 1.0 1.0 1.0
SmallFactor.settriangle(ArrayList) 1.0 1.0 2.0 2.0
SmallFactor.SmallFactor() 0.0 1.0 1.0 1.0
SmallFactor.sub() 0.0 1.0 1.0 1.0
SmallFactor.sub(SmallFactor) 0.0 1.0 1.0 1.0
Sum.getOutput() 0.0 1.0 1.0 1.0
Sum.Sum(String) 3.0 1.0 3.0 3.0
Term.p1(Matcher, String) 2.0 1.0 2.0 2.0
Term.p2(Matcher, String) 1.0 1.0 2.0 2.0
Term.p3(Matcher) 5.0 1.0 4.0 4.0
Term.p4(Matcher, String) 1.0 1.0 2.0 2.0
Term.p6(Matcher) 5.0 1.0 4.0 4.0
Term.setterm(String) 9.0 1.0 10.0 10.0
Term.Term(String) 34.0 13.0 21.0 23.0
Tri.getindex() 0.0 1.0 1.0 1.0
Tri.getindexout() 0.0 1.0 1.0 1.0
Tri.getratio() 0.0 1.0 1.0 1.0
Tri.gettype() 0.0 1.0 1.0 1.0
Tri.judgesame(Tri) 1.0 1.0 4.0 4.0
Tri.judgesame2(Tri) 1.0 1.0 3.0 3.0
Tri.setindex(BigInteger) 0.0 1.0 1.0 1.0
Tri.setindexout(BigInteger) 0.0 1.0 1.0 1.0
Tri.setratio(BigInteger) 0.0 1.0 1.0 1.0
Tri.settype(String) 0.0 1.0 1.0 1.0
Tri.Tri() 0.0 1.0 1.0 1.0

  由图示可以看出第二次作业中Expr和Term类圈复杂度依然较大,而原因便是其中自身构造函数的复杂度高。同样经笔者分析,因为Expr和Term的构造函数涉及到递归下降,所以复杂度都要很高。同时在Term里还使用了正则表达式进行多次if判断来找出因子类型。同时,SmallFactor和Factor复杂度同样很高,SmallFactor复杂度高的原因是因为它的2个判断相同进行化简函数的复杂度高,需要一个个三角函数比对,十分消耗时间。而Factor复杂度高的原因便是因为它的输出化简函数,涉及到多重for和if分支,可能性极多,因此复杂度很高。最后main和Initial的复杂度也很高,原因在于Initial需要将每个函数的自变量因变量,记下同时进行调用,占用内存多,复杂度高。在main函数里则是进行了遍历使用for和多重if进行函数的字符串替换化简,所以复杂度有所提升,还有一定优化空间。

3.第三次作业

  第三次作业相较于第二次作业来说多出了两点需要添加,一点是三角函数内部可以是任意一个表达式因子,同时函数可以进行嵌套调用,同时支持任意层括号嵌套。而笔者在第二次作业中架构已经趋于完善,因此第三次作业笔者完成所用时间不长,而且由于笔者已经对嵌套括号的情况进行了处理,所以此次作业难度不大。

第三次作业UML图如下所示:

 

   由上图可以看出,Tri相比第二次作业包含了一个Expr类型的变量,也就是三角函数括号内部的表达式因子。于是因子包含三角函数,同时三角函数也包含因子,二者互相包含,结构清晰,使得对表达式的解析变得十分顺利,难度并不是很大。而难点反而在于函数嵌套的字符串替换上。需要强调的是,在进行字符串替换的时候,一次遍历并不能将所有函数替换完成,因为函数嵌套的原因,函数替换后可能产生更多的未替换函数,因此需要从头再次进行遍历替换,经历足够多的遍历后,自然可以将所有函数替换完成。同时,笔者也对Initial的调用函数进行了修改,之前可以使用正则表达式提取自变量,而这次作业只能只用遍历的方法找到最外层逗号所在,分割得到调用函数的自变量,再进行调用,便会顺利很多。此方法优点在于思路简单,但遍历次数过多,复杂度和时间消耗会有所上升,也是一个很明显的缺陷。

  总结于此,第三次作业只是在第二次作业基础上进行了一次小小的拓展,难度系数并不很高,因此笔者也不过多赘述了。当然可以说明的是,在性能优化上,第三次作业很明显更加复杂,例如sin(2x)等情况的出现会使得优化可能无穷无尽。因为笔者实力有限,所以便只对sin(0),cos(0)等基础三角函数进行性能优化。

第三次作业类复杂度及方法复杂度图示:

Expr 6.0 11.0 12.0
Factor 3.1818181818181817 9.0 35.0
Initial 4.333333333333333 7.0 13.0
Main 12.0 12.0 12.0
SmallFactor 2.2 7.0 33.0
Sum 2.0 3.0 4.0
Term 4.857142857142857 16.0 34.0
Tri 1.0 1.0 9.0
Expr.Expr(String) 18.0 7.0 15.0 15.0
Expr.getexp() 0.0 1.0 1.0 1.0
Factor.add(Factor) 9.0 4.0 6.0 6.0
Factor.easy() 18.0 5.0 6.0 9.0
Factor.Factor() 0.0 1.0 1.0 1.0
Factor.getFactors() 0.0 1.0 1.0 1.0
Factor.getResult() 0.0 1.0 1.0 1.0
Factor.judgesame(Factor) 14.0 6.0 4.0 6.0
Factor.mul(Factor) 3.0 1.0 3.0 3.0
Factor.out(String) 0.0 1.0 1.0 1.0
Factor.pow(Factor) 1.0 1.0 2.0 2.0
Factor.setfactor(ArrayList) 0.0 1.0 1.0 1.0
Factor.sub(Factor) 9.0 4.0 6.0 6.0
Initial.getname() 0.0 1.0 1.0 1.0
Initial.Initial(String) 6.0 1.0 4.0 5.0
Initial.use(String) 8.0 1.0 8.0 8.0
Main.main(String[]) 25.0 1.0 17.0 18.0
SmallFactor.add(SmallFactor) 0.0 1.0 1.0 1.0
SmallFactor.getindex() 0.0 1.0 1.0 1.0
SmallFactor.getratio() 0.0 1.0 1.0 1.0
SmallFactor.gettri() 0.0 1.0 1.0 1.0
SmallFactor.judgesame(SmallFactor) 16.0 7.0 4.0 7.0
SmallFactor.judgesamen(SmallFactor) 17.0 7.0 5.0 8.0
SmallFactor.mul(SmallFactor) 8.0 4.0 5.0 5.0
SmallFactor.pow(SmallFactor) 1.0 1.0 2.0 2.0
SmallFactor.setindex(BigInteger) 0.0 1.0 1.0 1.0
SmallFactor.setratio(BigInteger) 0.0 1.0 1.0 1.0
SmallFactor.settri(Tri) 0.0 1.0 1.0 1.0
SmallFactor.settriangle(ArrayList) 1.0 1.0 2.0 2.0
SmallFactor.SmallFactor() 0.0 1.0 1.0 1.0
SmallFactor.sub() 0.0 1.0 1.0 1.0
SmallFactor.sub(SmallFactor) 0.0 1.0 1.0 1.0
Sum.getOutput() 0.0 1.0 1.0 1.0
Sum.Sum(String) 3.0 1.0 3.0 3.0
Term.p1(Matcher, String) 2.0 1.0 2.0 2.0
Term.p2(Matcher, String) 1.0 1.0 2.0 2.0
Term.p3(Matcher) 0.0 1.0 1.0 1.0
Term.p4(Matcher, String) 1.0 1.0 2.0 2.0
Term.p6(Matcher) 0.0 1.0 1.0 1.0
Term.setterm(String) 9.0 1.0 10.0 10.0
Term.Term(String) 34.0 13.0 21.0 23.0
Tri.getin() 0.0 1.0 1.0 1.0
Tri.getindexout() 0.0 1.0 1.0 1.0
Tri.gettype() 0.0 1.0 1.0 1.0
Tri.judgesame(Tri) 1.0 1.0 3.0 3.0
Tri.judgesame2(Tri) 1.0 1.0 2.0 2.0
Tri.setin(String) 0.0 1.0 1.0 1.0
Tri.setindexout(BigInteger) 0.0 1.0 1.0 1.0
Tri.settype(String) 0.0 1.0 1.0 1.0
Tri.Tri(String) 0.0 1.0 1.0 1.0

  由上图可知,第三次作业复杂度和第二次作业复杂度相差不大。但在SmallFactor里新添对于表达式因子判断相同的函数复杂度相对较高,原因便在于表达式判断相同需要层层递归一一比较得到最终结果,复杂度自然是非常高,也因此提升了整个SmallFactor类的复杂度。同时Initial的复杂度因为函数调用的遍历也导致了一定程度的上升。

三、bug分析

1、第一次作业

  第一次作业由于结构是最基础的一元多项式,十分简单,坑点很少,所以笔者测试时没有出现bug,同时在互测上也并没有检测出其他人的bug,所以可说不多。但笔者在写代码的时候仍然出现了很多需要debug的情况,由此可见一段项目的代码即使功能再简单,也需要反复打磨才能成功。

2、第二次作业

  第二次作业的难度很明显高于第一次作业甚多,也因此bug横生。在写代码通过中测的时候便遇到了很多问题,仍然记得当时中测第7个点一直过不去十分苦恼,后来才发现原来是函数替换的时候出了问题,并没有完全替换,而一次遍历时字符串的长度是会发生变化的,所以从头开始遍历替换才能解决这个问题。而我的强测后面错了三个点,互测也被人找出一个bug,比较遗憾的是仍然没有找到其他人的bug。而这次作业错的bug点主要是出在sin(0)的化简没有考虑到正则表达式对多重括号的误判以及判断相同函数的变量没有归零导致出现了一些问题。通过这次第二次作业的多个bug,我也意识到测试是一个绝对不容忽视的一环,评测机也不应该成为判断有无的唯一依据。同时,第二次作业bug里最难发现的毋庸置疑是对象的深克隆问题。如果直接用等于号连接,看似不同名称的两个对象变量地址就会一样,修改其中一个,另一个也会随之发生变化,这叫做浅克隆。而之后我采用for循环的方法才规避掉了浅克隆带来的严重bug。

3、第三次作业

  第三次作业因为相对于第二次作业变化不大,所以遇到的bug不多,但是在测试时仍有强测和互测点出现问题,而这些bug都是因为笔者对sum的上下限情况考虑不周,没有考虑到下限大于上限时要默认为0。这个bug其实在第二次作业中就应该发现。但第二次的强测和互测均没有发现我的bug,由此可见即使是刁钻的强测互测,也不能完全遍历所有可能的bug情况,即使通过,也不代表代码是完美无暇的。所以仔细打磨,边界测试就显得更加重要。而我个人发现,出现bug的可能性与圈复杂度其实是成正比的,因为复杂度越高,情况的可能性就越多,一旦有一种情况没有考虑到,臃肿的复杂度结构就会带来很大的灾难,并且很难debug出问题。而这次互测中,笔者debug出别人7个同质bug,原因在于sum里的i是负数的时候如果将它进行平方,字符串的直接替换会变成负数,而真正的含义应该是正数,笔者之所以能找到这个bug,便是因为笔者也犯过一样的错误,但却自己测试修复了。由此可见,字符串的直接替换是一个风险非常大的行为,一定要谨慎谨慎再谨慎。而这次bug对我来说也让我更加明白了互测以及自己测试都需要集中精神,走好这万里长征的最后一步。

四、架构设计体验

  经历这三次作业,笔者个人感觉自己的架构设计水平从一无所知到初步入门,收获极大。第一次作业最基础的表达式-项-因子的基础架构构建使我在摸索中逐渐掌握了方法。而第二次作业加入三角函数,自定义函数和求和函数后架构瞬间变得复杂了许多,只能进行重构,让笔者构建起来一度崩溃,但最后还是坚持下来,同时整体架构逐渐变得成熟,架构水平稳步提升。而最后第三次作业时我对于这次架构的把握便极为娴熟,很短的时间内便构建出了第三次的结构,也因此出错极少。这一单元的架构过程让我深刻体会到架构的重要性,如果有一个好的架构,会省去不少debug以及拓展的难度,可以说非常重要。

五、心得体会

  经历了第一单元的训练,笔者才真正发自内心的体会到什么是面向对象,而笔者之前写的代码一直在面向过程,让笔者大为汗颜。而在面向对象和架构的过程中,笔者经历过debug到深夜却没有结果的痛苦,也经历过最终看到强测AC的快乐,但不管快乐还是痛苦,笔者的收获之大毋庸置疑。首先,笔者对java的使用娴熟程度有了一个突飞猛进的提升,但这还不是最大的收获。最重要的是,我初步体会了构建类和对象的抽象思想,也明白了它可以让代码拥有清晰的结构,对每个程序员来说都非常重要。我相信,在接下来的学习中,我会逐步了解更多关于面向对象的知识,经历十倍百倍难度的挑战,但我不会畏惧,我会向最高峰攀登,永不停下。

posted @ 2022-03-26 15:41  谷小来  阅读(25)  评论(1编辑  收藏  举报