OO2022第一单元个人总结

前言

本文是对第一次面向对象课程作业的总结,文章首先总结了本次作业我的总体架构思路,接着分析了三次作业中我的架构迭代历程,之后对于我的最终架构给出了代码度量分析,且分析了架构的优缺点。之后文章分析了在Hack过程中的收获,以及在本次作业设计,编写中我学到的东西,尤其是关于深浅克隆这一部分我分享了我认为比较好的一些资料,和我自己的理解,希望uu们不要跟我一样在这一知识点再犯错误。最后文章阐述了本次作业中我的心得体会。文章如有错误,谢谢指正!


一.总体架构思路

在本次作业中,我们的最终需求是对一个表达式进行化简计算,并进行一些必要的合并与优化。然而要进行计算,首要任务是进行表达式的解析。总而言之,要完成解析和计算这两个需求,我设计了如下架构,我的架构简而言之是:递归向下解析,自底向上计算,输出前再进行合理优化。形象化的表示一下就是如下的流程图。

在我的架构中,将表达式抽象为了四个层次,在作业指导书中,由于已经给出了表达式的形式化表示,我们可以很容易抽象出以下层次:

  • Expr:表达式层

  • TermExpr+-的形式包含多个Term项层

  • FactorTerm项以*形式包含多个Factor因子

  • Element:上述三个层次均包含一个属性,其类型是一个Element数组,有了Element层,方便进行自底向上的运算。

    • 在第一次作业中 Element = a*x**b

    • 在第二、三次作业中 Element = a*x*b*[sin][cos],其中[sin][cos]Sin、Cos类的数组

有了以上这些层次,我们首先对输入进行递归向下解析,解析出表达式中包含哪些层次,经过自底向上的计算,可以得到每个层次的[Elements]数组,最终得到的Expr表达式的[Elements]数组便是我们得到的初步计算结果,经过合并同类项等优化便可以进行输出。

二.迭代过程

在这一部分简要阐述了我的三次迭代心路历程,其中并没有放UML类图,因为三次作业我没有做大规模的重构,三次作业的UML类图重复相似部分较多,我将会在代码度量分析一节中分析UML类图,其中标明了每次作业的新增与修改部分。

2.1第一次作业

  • 分析

在第一次作业中,首次上手java作业打了我一个措手不及,因为之前写的都是流程式的程序,开始时构思面向对象式的架构没有很好地思路,属于是一头雾水了,花了很多时间去理解题目并和同学交流,并且在助教给的训练项目中也得到了一些启发,最终构思了上述的程序架构。

  • 解析方法上,在HW1中,由于因子层面只有常数因子,变量因子与表达式因子,总的来说类型较少,因此采用了正则表达式进行识别,识别流程示意图如下:

HW1

  • 计算方法上,在HW1中,涉及到a*x**b之间的乘法与加减法,只需系数与系数的计算,指数与指数的计算,不再赘述,但在这里我第一次遇到了深克隆和浅克隆的问题,这个问题在本次作业中多次困扰我,这一问题在之后的Bug部分详细阐述。

  • 优化方法上,由于最后的形式是许多a*x**b进行相加,优化方法上只涉及到合并同类项,之后输出时再进行简单的优化即可,例如指数为1不输出指数,变量因子系数为1不输出系数等。

2.2第二次作业

  • 分析

在本次作业中,新增了自变量函数,求和函数,三角函数三种因子,这要求我们对新增的这三种因子进行识别与计算。此外,由于在解析f与sum时需要进行自变量的替换,替换后可能出现表达式括号嵌套的形式,这要求架构中的解析过程要支持嵌套括号的形式,这是本次作业一个潜在的需求。

  • 重构解析方法:在本次解析过程中,由于因子的形式多样,如果继续使用正则表达式进行识别,一来正则表达式很难准确构造,二来由于正则式的不准确所带来的潜在Bug也比较多。因此我对每个层次解析方法Parse进行了重构,采用类似栈的方法,对括号层数进行解析,根据当前括号所处的层数和简单的字符判断,可以依此解析出TermFactor等层次。这样写的好处是不需要绞尽脑汁构造正则表达式,正确性也容易验证,对于新增的几个因子
    • sin,cos,将括号内的内容当做常数或变量因子重新解析
    • f,sum因子,进行字符串的替换,展开等处理后看做表达式重新解析

HW2

  • 修改计算方法

    本次作业中的计算,不仅仅是a*x**b之间的乘法与加减法,此时Elementa*x*b*[sin][cos],需要修改两处:

    • [Elements]*[Elements],除了修改系数a,指数b,另需要将二者[sin][cos]数组合并,这里又涉及到了深拷贝与浅拷贝问题。然而梅开二度,我在这里又没有意识到犯了浅拷贝这个“错误”,虽然HW2中没有出现问题,但导致我在HW3优化二倍角时出现了错误,这里会在Bug部分详细阐述。
    • [Elements]+-[Elements],这里需要修改equals方法,合并同类项时考虑[sin][cos]数组是否一致。
  • 新增优化方法

    • 在解析过程中进行初步优化,对于sin(0),cos(0),sin()**0,cos()**0在解析的过程中就将其解析为对应常数,进行初步优化。
    • 三角函数计算的优化
      • HW2,因为时间关系,只增加了简单的Acos(B)**2+Asin(B)**2=A的合并,其中A是任意项,B是三角函数内的常数或变量因子。

2.3第三次作业

  • 分析

在本次作业中,新增了函数间的相互调用,三角函数内嵌套等新需求。关于函数之间的相互调用,在HW2中对于自定义函数的处理方法是,进行自变量的替换后直接看做表达式进行处理,因此HW2架构已经可以实现函数之间的互调用。在本次作业中主要实现了三角函数的内嵌套,相较于第二部分修改较少。

  • 修改解析方法, 如上文所述,此次迭代,只需要修改三角函数内部嵌套部分的解析方式。显而易见,将括号内部的部分当做Expr表达式继续解析即可。

  • 计算方法:不需要修改。
  • 新增优化方法
    • 二倍角公式,在HW3中实现了二倍角的简化,2Asin(B)cos(B) = Asin(2B),遗憾的是这里将HW2中的Bug显现了出来,因为在优化时对B进行了系数的加倍,而在计算时采用的浅克隆方式,修改B导致了其他某些sincos内部的内容也随之修改。

三.代码度量分析

先放一下HW3最终的UML类图

  • 第二次作业修改了Caculate内的方法,新增Sin,Cos,自定义函数Fun,Sum类,修改了Element属性,其他UML部分与第一次作业基本相同。

  • 第三次作业修改了Sin,Cos内部属性,同时更新了Caculate计算方法,其他UML类基本未做变动。

  • 优缺点分析

    • 优点是进行了比较系统的抽象,不同层次代表了表达式中不同的组合,
    • 缺点是首先Sin,Cos两个类基本相同,可以合并,在编写代码时我为了区分Cos,Sin,创建了两个类,但其实二者基本相同,完全可以合并为一个类,以减少复杂度。此外,代码之间内在的的关联度高,不容易达到“高内聚低耦合”的原则,程序的结构化程度不是很好。

之前我并未接触过代码度量的含义,了解之后学习到其一些指标含义如下:

  1. CogC:认知复杂度,反应一个方法的可理解性,循环分支等结构越多,可理解性越差,数值越高。
  2. ev(G):基本复杂度是用来衡量程序非结构化程度的。
  3. Iv(G):模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系,越高说明模块和其他模块之间的调用关系复杂,耦合程度越高.
  4. v(G):是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,数值越高说明程序越难以维护

方法复杂度

PreFun.printans(ArrayList) 48.0 3.0 16.0 17.0
PreFun.mergecs2(ArrayList) 42.0 10.0 9.0 12.0
Factor.parse() 31.0 4.0 12.0 15.0
Element.matchcs(Element) 30.0 9.0 10.0 12.0
Element.Element(String) 28.0 1.0 15.0 18.0
Fun.parse() 26.0 3.0 10.0 11.0
PreFun.neg(ArrayList) 19.0 1.0 8.0 8.0
Expr.parse() 12.0 1.0 8.0 9.0
Term.parse() 12.0 1.0 7.0 8.0
PreFun.addcos(StringBuilder, Element) 10.0 1.0 6.0 6.0
PreFun.addsin(StringBuilder, Element) 10.0 1.0 6.0 6.0
PreFun.issingle(ArrayList) 8.0 5.0 11.0 12.0
Cos.cossequal(ArrayList, ArrayList) 7.0 4.0 3.0 4.0
Element.elementsequal(ArrayList, ArrayList) 7.0 4.0 3.0 4.0
PreFun.mergecs(ArrayList) 7.0 1.0 5.0 5.0
Sin.sinsequal(ArrayList, ArrayList) 7.0 4.0 3.0 4.0

(由于方法较多,篇幅限制只列出了分析结果中数值较大,标红的部分数据。)

接下来进行代码度量结果的分析

  • 方法复杂度来看,可以看出其集中出现在PreFun内的优化函数上,原因是我在优化过程中采用的forif-else较多,为优化性能对许多情况进行分别讨论,这导致了复杂度过高,这从某种程度上说明了我在优化部分容易出错的原因。

  • 同时每个类内Parse方法,和Element方法的实现也比较复杂,这是因为耦合性过强,例如在我的架构中,他们都是为解析功能服务的函数,Exprparse需要依赖Term中的Parse,而TermParse又需要依赖FactorParse方法之间的依赖性很强,不容易达到“高内聚低耦合”的原则,程序的结构化程度不是很好。

四.Hack与被Hack

4.1 个人Bug发现与分析

关于本次作业的Bug较多的集中在深克隆与浅克隆这个问题上,这可能与我的代码架构有关,在对ArrayList操作时使用了较多的add方法。

在一次作业中我首次遇到了深克隆与浅克隆的问题,但在课下时我已经发现了这个Bug并予以解决。但在第二次作业时,又再一次出现了深克隆浅克隆的问题,这个Bug原因是我在计算[Elements]数组时为了实现合并两数组的[sin][cos],直接采用了add方法,因为第二次作业我并未对Sin,Cos内部进行操作,所以当时我认为直接采用add方法是正确的,简便的,因而这个Bug在第二次作业时并没有显现。

然而在第三次作业中,在优化部分我新增了二倍角优化方法,直接对Sin,Cos内部项的系数进行了操作,此时我忽略了之前采用add方法合并数组时采用了浅克隆而非深克隆,直接引发了第二次作业所埋下的隐患。以后再也不瞎优化了(bushi,所以有时候不优化,保证正确性可能会更好一点。不过言归正传,这个Bug十分隐蔽,我在课下并没有发现,导致一个强测点直接寄掉。

这个Bug出现的原因有二:

  • 一方面是因为对于深克隆浅克隆的理解还是不够深刻,在编写代码是没有意识到自己当前所写代码实际上是再进行克隆操作,也没有进一步思考这里的克隆是需要深克隆还是浅克隆,在第二次作业中我采用的浅克隆方式对于HW2的需求来说是“正确”的(因为恰巧第二次作业中sincos内部只有常数与自变量,不需要对其内部进行操作),但其实是并不合理的,因为在架构中需要新构造一个项,进一步思考不难意识到我们需要的实际上是深克隆,关于深浅克隆的问题,为了加深我自己的理解,将在“我学到的”这一部分进行进一步阐述。

  • 另一方面是因为在作业迭代的过程中,上一次作业的某些架构在本次作业中埋下了隐患。在进行作业的迭代时,编写完代码后我只检查了本次修改的,迭代的代码正确性,“理所当然的认为”之前的代码都通过了上一次强测,那肯定是正确的吧。但事实证明这样做法是错误的,我忽略了两次迭代之间的关联性,新增的需求可能会是上次架构中"正确"但不合理的操作变为Bug。以后的作业中,应该有意识的去思考迭代过程中两次架构中有哪些可能产生Bug的关联之处,不能只着眼与本次作业的新增部分、修改部分,以防止这种现象的发生。

4.2 互测他人Bug发现与分析

本次互测发现了其他同学如下几个Bug

  • -1+1等简单的操作无法输出,这一Bug产生的原因是一些同学的架构进行迭代优化后,由于架构的不合理性,反而无法实现最基本的测试点,这也提醒了我们测试全面覆盖的重要性。
  • sum上下界定义为了int,而非BigInteger,这一Bug产生的原因我认为可能是没有仔细阅读指导书,先入为主习惯性的将一些整数定义为了int,测试时
  • sum(i,下界,上界,i**2)出现了问题,这一Bug产生的原因是进行字符串替换时没有加括号,这里的i是常数,只有替换时加括号才是正确的形式化表述。

五.我学到的

5.1 深克隆与浅克隆

深克隆与浅克隆在本次作业中对我的“身心摧残”已在上文阐述,接下来需要进一步深入理解深浅克隆的区别与实现,以防以后出现类似的问题。

1.首先我们需要知道什么时候我们在进行克隆?

  • 简而言之,如果此时我们在根据一个已有数据创建一个新数据的时候,我们便在进行克隆。意识到自己正在进行克隆十分重要,只有意识到了这一点才能进一步去思考深浅克隆的问题。

2.深浅克隆的区别是什么?

  • 浅克隆不会克隆原对象中的引用类型,仅仅拷贝了引用类型的指向。深克隆则拷贝了所有。也就是说深克隆能够做到原对象和新对象之间完全没有影响。

3.我在深浅克隆中犯的错误

  • 首先第一点,我没有意识到自己在进行克隆,之前并没有在代码中了解过克隆这个概念。
  • 第二点,我在克隆操作中为了实现类中非基本数据类型的属性的克隆(在本次架构中是一个[Elements]数组),错误的使用了addalladd方法,这仅仅拷贝了引用类型的指向,并非深克隆。或从某种意义上讲,这都不是规范的克隆方法。

4.如何进行规范的克隆?

  • 分享两篇博客资料,写的比较好,我在这里找到了答案。

[(40条消息) Java对象克隆——浅克隆和深克隆的区别_JeffCoding的博客-CSDN博客_浅克隆和深克隆的区别]

java 深克隆(深拷贝)与浅克隆(拷贝)详解 - mindcarver - 博客园 (cnblogs.com)(这一篇比较长,其实上一篇对于理解和实用来说就完全足够)

我来简单总结一下,java中已经为类准备了clone方法,我们不需要重复造轮子,要实现规范的clone,还需要以下两点:

  • 实现Cloneable接口,只需在类的后面加implements Cloneable即可
  • Override重写Clone方法,如何重写在上面两篇博文中都有很好的阐述。

接下来,我认为重要的是理解为什么要重写Clone方法,对于只包含基本数据类型属性的类来说,clone方法完全够用,不需重写,clone后的新旧对象互不影响。重写Clone方法是为了应对类中包含了其他自定义的对象属性这种情况(比如在本次HW2作业中,我在SinCos类中包含了[Elements]数组作为属性),这时Clone方法在拷贝这个属性时,拷贝的只是引用类型的指向,也就是我们所说的浅拷贝,后续再对这个属性进行操作时,便会在不经意间对新旧对象同时操作,这样的Bug很难查找出来,因此在设计时就要予以避免。

如何进行避免?方法也很简单,只需要在本类中修改重写的Clone方法,包含的其他自定义的对象里面也重写Clone方法,多个浅拷贝便可以实现深拷贝。引用第一篇博客中的例子如下(Customer类中包含顾客地址Address类),在两个类中均重写Clone方法,之后再调用Clone方法可以对Customer深克隆。

//Address
@Override  
public Address clone() throws CloneNotSupportedException {  
    return (Address) super.clone();  
}  
//Class Customer(Address是Customer的一个属性成员)
@Override  
public Customer clone() throws CloneNotSupportedException {  
    Customer customer = (Customer) super.clone();  
    customer.address = address.clone();  
    return customer;  
}  

总而言之,我认为对于java深浅拷贝的这样一种理解很准确:Java中定义的clone没有深浅之分,都是统一的调用Objectclone方法。为什么会有深克隆的概念?是由于我们在实现的过程中刻意的嵌套了clone方法的调用。也就是说深克隆就是在需要克隆的对象类型的类中全部实现克隆方法

5.2 去哪里找Bug

这一点其实在理论课上有讲,我认为总结的是在太好了,这里再将其罗列一下。

  • 调用是否对返回值进行了接收和检查
  • 类库及方法的使用是否符合要求
  • 容器访问的越界保护(课下自己测试过程中几次出现的数组越界问题)
  • 对象拷贝是否彻底(强测出现深浅克隆的Bug)
  • 数值计算是否溢出
  • 是否对循环体内对循环变量进行修改

可以看到,我属于是精准踩雷有木有。所以在静态检查时,可以有意识的去思考以上检查要点,这能帮助我们更快更好的找到Bug,虽然不能覆盖所有Bug,但我觉得可以覆盖很大一部分非理解题意错误而导致的Bug。

5.3 测试数据

主要采用随机测试的方式,手动构造了一些我认为比较容易Error和Wrong Answer的边界数据,虽然也有一定的Hack量,但Hack成果并没有很显著,如果写个自动评测机可能会更好一点,周末开着自动评测机让其自动Hack,自己可以做其他的事情,多是一件美事啊。所以在这里我反思一下,下一次一定要写个自动评测机,下次一定。

六. 心得体会

通过第一单元的作业,我有以下几点收获

  • 架构真的很重要,好的架构不仅正确度高,逻辑性强,在DeBug的时候很容易找到Bug的出处,有时候好的架构甚至会实现意想不到的功能,例如实现括号嵌套的支持等等。因此我们在开始动手之前,一定要多与同学进行交流,做好初步的设计工作,之后再逐步优化细节,进行实现。这样既可以减少之后重构的可能性,减少工作量,同时最终实现的效果正确性,准确性也会更好。
  • 初步体会了面向对象与面向过程程序设计的不同,通过面向对象的程序设计,可以更好的实现代码维护与迭代更新,一次可能只需要修改某几个类就可以实现更多的需求,这一点是面向过程设计所做不到的。
  • 最后一点肯定是更加熟悉了java中的各种语法以及容器的使用,从HW1的无从下手与茫然到慢慢熟练运用java中的常用类与常用功能,很像一个升级打怪的过程,在这个过程中我还是很满足的。第一单元的OO作业从心态和知识上都让我提升了很多,也收获了很多设计,编程经验,期待OO课程中之后的学习!
posted @ 2022-03-26 13:52  Lei_YuYu  阅读(88)  评论(2编辑  收藏  举报