OO第一单元作业总结
OO第一单元作业总结
第一次作业
简介
第一次作业为解析含加、减、乘号以及幂次和括号的运算表达式,完成表达式的括号展开任务并输出。
架构思路
解析表达式的方式思考过两种,一是将表达式转成后缀表达式,以后缀表达式构建表达式树,表达式树节点为运算符,对于不同的运算节点只需写出对应的运算方式(加、减、乘、幂)。笔者在第一次的构造中采取了构建表达式树的方式,但经学长提点,考虑到课题组的对于形式化表述的着重强调,以及学习后续编译的基础,笔者在第一次的重构中采用了递归下降的解析方式,以下将着重阐述递归下降的思路。
递归下降
由作业中形式化语言的描述,表达式由多个项(Term)通过加减(+/-)连接,项由多个因子(Factor)通过乘号(*)连接。
所以解析大致结构可写成如下:
parseExpr() {
parseExpr();
while (str == "+" || str == "-") {
parseExpr();
}
}
parseTerm() {
parseFactor();
while (str == "*") {
parseExpr();
}
}
parseFactor() {}
以 “全局性容器” 简化解析、存储和计算
递归下降返回“全局性容器“
第一次作业中Factor中无论是常数因子还是幂函数因子都可写成以下形式:
所以在不考虑表达式因子的前提下,因为是常数因子和幂函数因子相乘,所以任何项(Term),也可写成上述形式,所以表达式(Expr)可以写成以下形式。
因此,笔者将项(Term)和因子(Factor)的概念同一化,统称为Factor,都简化成
此时,可以创建两个类,Fcator、Expr(Polynomial类),Expr(Polynomial类)则是一个存储Fcator的容器(在笔者的实际代码中,Expr类被命名为Polynomial类)。
对于解析方法parseExpr()只需返回Expr对象,parseTerm()、parseFactor()只需返回Factor对象。但是考虑到,第一次作业中,因子类有表达式因子,所以,为了简化解析过程,以及便于后续的计算,解析方法统一返回Expr对象。
“全局性容器''可简化计算和合并同类项
计算的过程,其实就是当得到一个新的Term,与原有的Term做乘法;当得到一个新的Expr,与原有的新的Expr进行加减合并。由于笔者的解析函数都将返回Expr的对象,所以,加法、减法、乘法、幂次都可写在Expr类中作为同一类的方法。
合并同类项即可简化为,判断是否存在相同index的Factor,如果index(系数)相同,则合并两个Factor的系数。
第一次作业UML
Parser类 && Lexer类
Parser类就像是一个语法分析器,而Lexer则是一个词法分析器。Parser根据Lexer所反馈的当前字符信息来得到常数因子或幂函数、控制进入或退出parseExpr(),parseTerm(),parseFactor()的解析方法。
Factor类和Polynomial类
在上文思路中提过,笔者将项(Term)和因子(Factor)的概念同一化,所以,一个Factor类代表着一个
在Factor类中,只需要以属性存储系数和指数。Polynomial类则是相当于Expr类,是存储多个Factor的容器。
度量分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Factor.Factor(BigInteger, int) | 1.0 | 1.0 | 1.0 |
Factor.getCoefficient() | 1.0 | 1.0 | 1.0 |
Factor.getIndex() | 1.0 | 1.0 | 1.0 |
Factor.mul(Factor) | 1.0 | 1.0 | 1.0 |
Factor.setCoefficient(BigInteger) | 1.0 | 1.0 | 1.0 |
Factor.setIndex(int) | 1.0 | 1.0 | 1.0 |
Lexer.getNumber() | 1.0 | 4.0 | 5.0 |
Lexer.Lexer(String) | 1.0 | 1.0 | 1.0 |
Lexer.next() | 2.0 | 1.0 | 2.0 |
Lexer.peek() | 2.0 | 2.0 | 2.0 |
Mainclass.main(String[]) | 1.0 | 1.0 | 1.0 |
Parser.parseExpr() | 1.0 | 7.0 | 7.0 |
Parser.parseFactor() | 1.0 | 5.0 | 5.0 |
Parser.Parser(Lexer) | 1.0 | 1.0 | 1.0 |
Parser.parseTerm() | 1.0 | 2.0 | 2.0 |
Polynomial.add(Polynomial) | 1.0 | 2.0 | 2.0 |
Polynomial.addFactor(int, Factor) | 1.0 | 3.0 | 3.0 |
Polynomial.mul(Polynomial) | 1.0 | 4.0 | 4.0 |
Polynomial.neg() | 1.0 | 2.0 | 2.0 |
Polynomial.pow(int) | 1.0 | 3.0 | 3.0 |
Polynomial.sub(Polynomial) | 1.0 | 1.0 | 1.0 |
Polynomial.toString() | 1.0 | 2.0 | 5.0 |
Total | 24.0 | 47.0 | 52.0 |
Average | 1.0909090909090908 | 2.1363636363636362 | 2.3636363636363638 |
ev(G)基本复杂度是用来衡量程序非结构化程度的,从上表中可以看出,第一次作业的方法ev(G)都比较低,说明方法的可维护性较高;iv(G):即Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在[1,v(G)]之间,值越大联系越紧密,从上表中可以看出,parseExpr()、parseFactor()与其他模块的耦合度较高,主要原因在于递归以及parseFactor()涉及字符串返回处理,但总体上来讲,圈复杂度不高。
Parser类的平均循环复杂度较高,应该是由于递归的原因,但其余类的复杂度不高,第一次作业的性能还算可以,
Bug分析
1.在本地测试时,发现(x+1)**3此类幂次在计算时会出现问题,仔细查看multi()方法的写法,是由于在计算幂次时采用以下
Polynomial mul() {
HashMap<Inter,Variable> otherPoly = this;
伪代码 **** this * otherpoly *****
}
上述写法只做示范,写法的问题在于,hashMap、Arraylist或者自定义类在java中为引用传递,我们在计算this*other时,看似other是一个固定的map,其实,当this对应的map改变时,other也将改变,这涉及到Java深拷贝和浅拷贝的问题。
2.在优化已展开完毕的括号时,为了优化,尽可能减少字符,以字符串替换的形式将 1*x
直接替换成 x
,而忽略了31*x
,21*x
等此类各位为1的幂函数,导致在互测环节被Hack了7刀!!!
感想与体会(愿称之为小亮点)
1.在第一次作业中,初步尝试利用已有方法,简化新方法;虽然此类操作可能会加大这个方法的圈复杂度和与其他方法的耦合度,但能极大地简化代码。
比如,在实现减法时,可以先将待加Poly全部系数取反,再使用add()加法。
在实现Poly类的multi()方法时,由于Poly类是Factor类的容器,所以可以现在Factor类中写出Factor类的乘法,在Poly类中只需遍历表达式,取出poly和othepoly中的factor1和factor2进行相乘。
在实现幂次时,可以套用多次multi()乘法。
2.字符串化简,在题意所给的表达式中,可以会出现,空格符、制表符或者是多余的加减号,为了解析时的便利,可以在解析前,simplify input,去除多余的加减号、空格、制表,以及可将我们的幂次改成^
用于解析。
3.不可变对象,在写运算方法时,之前没有过仔细思考缘由,但还是参考BigInteger的实现方法,在每次运算中都会创建一个新类返回。
没有想到在之后的task中,此种返回不可变对象的方法构造为笔者降低了出错的可能。
第二次作业
简介
第二次作业中引入了三角函数、自定义函数、求和函数作为新的变量因子。
迭代架构思路
笔者这一次还是打算将“全局性容器”进行到底,但需要改变Factor类(受上一次作业的影响,实际上应该为Term类,但没有定义Term类,所以暂且模糊一起称之为Factor类)的属性。不难得出,作业2中的表达式Expr(Polynomial类)一定可以写成以下形式:
由作业1的经验,Term(Factor类)的形式就是
所以,Factor类的属性如下:
variable 作为新定义类存储 a*x^b 的结构,同时定义两个HashMap来分别存储sin和cos。
第二次作业UML图
在第一次作业的基础上,新增Sum类,SelfDefined类,Variable类,Trig类
Sum类 && SelfDefined类 :
当Parser解析识别到有求和函数或者自定义函数,将求和函数、自定义函数整体(以字符串参数形式)传到Sum类或Selfdefined类,在各自类中完成对求和函数、自定义函数的解析。笔者主要为这两个类大致设了两个方法,一是将传入的字符串解析,获得求和函数的上下限、求和表达式,获得自定义函数的实参(由于,自定义函数最先从控制台传入,所以,自定义函数的形参和表达式在解析表达时候之前就已完成);二是由类中各种属性(求和表达式、自定义表达式、实参等)解析计算得到一个Polynomial(即表达式类)的计算结果。
Trig类
一个Trig类代表着如下结构
所以,Trig类包含Variable类属性和指数TrigIndex。
度量分析
calcTrig()方法非结构化程度、复杂度飘红了(不太妙),应该是由于笔者,在设计这一方法时,在此方法内处理字符串的拼接用于计算三角函数内部,以及判断了sin(0)和cos(0)的特殊情况,所以大致大量使用if...else if语句,加大了负责度和非结构化程度;用于计算三角函数的calcSum()方法复杂度也较高,主要原因还是由于,笔者在设计这一方法时,为了简易处理,将求和表达式字符串拼接用于得到需要计算的求和表达式,所以在结构上较为复杂。
Factor类总循环次数高,应该是由于笔者在写Factor的mul()方法时,考虑到三角函数的合并同类项,所以会多次遍历Factor类的hashMap;而Polynomial循环次数多则可能是因为,笔者将计算三角函数的方法写入clacTrig()写入Polynomial类中,笔者想Trig只用作一个纯纯的存储结构,所以计算方法也写在了Poly类中,没成想飘红了,另一原因可能是Polynamial的toString()方法也需要遍历hashmap,所以循环次数也增加了。SelfDefined()类,最大的可能是因为,笔者在这个类中,实参换形参的时候采用了字符串替换拼接。
Bug分析
笔者想硬气地说,笔者在这一次强测和互测中没被找到bug,所以此处暂且忽略。
感想与体会(愿称之为小亮点)
1.第二次作业的合并同类项,笔者认为主要在两个层面,一是Factor之间的加减合并合同类,二是在Factor类中,sin和cos的乘法合并同类型。先说第一层,Factor类之间的加减合并,笔者在Expr(Polynomial)类中改进了hashmap:
以Factor作为key,value则适合Factor的系数,所以在合并合同类时只需要用contains(key)判断是否有相同的Factor,有,则只需put(Factor,新的系数);这一方法极大简化了我们判断Factor是否相同的方法,如果用自身重写equals()则容易出错。
另外说一句,此方法需要重写所有Factor中包含所有自定义类的hashCode()和equals()方法,用IDEA生成即可。
2.在此也呼应一下第一次作业中的感想第三点,很明显,当笔者的程序在运算乘法时,很容易改变Factor的结构,但如果Factor类在hashMap中为key,则要求它加入map后不可变,否则将出大问题,如果不能改变Factor,那只能在乘法方法中重新new一个Polynomial,用新Polynommial存储新的Factor,运算完成后,再返回我们新的 Polynamial。
第三次作业
简介
第三次作业中,允许三角函数内部含表达式因子,允许自定义函数实参能够调用自定义函数、求和函数。
迭代架构思路
相对于第二次的迭代,第三次改动并不多,自定义函数实参调用自定义函数,仍可以通过计算自定义函数时递归调用parser类解决,只需将存储有自定义表达式的容器一起传入parser即可完成递归。三角函数内部含表达式因子,所以笔者将三角函数类管理的属性类型改为Polynomial。另外,笔者修改了一下三角函数类,使三角函数类为一个容器,
一个Trigs的对象可管理类似
的结构。
第三次作业UML
度量分析
又能发现飘红的新面孔,polyIsFactor()方法主要用于判断三角函数内部是否为变量因子,是则可以在字符串输出时去除多余的空格,上表中可以看到polyIsFactor()方法非调用子模块的部分复杂度很高,应该是由于在判断时嵌套了多层的if_else if 语句所导致。Trig类中 printString()方法需要调用Poly类中的polyisFactor()来简化输出,以及由于三角函数内部为Polynomial类,需要多次调用Polynomial的toString()方法,所以Trig类中 printString()与其余模块的关联度很高。
将三角函数内部字符串解析移出Polynomial类,新建TrigParser类,结果TrigParser飘红,Polynomial恢复正常,似乎也印证了作业2中Polynomial中飘红是因为涉及字符串。其余飘红原因,在第一次作业中和第二次作业中都有分析,只有Trigs类复杂度高,实在想不清楚是为什么。
Bug分析
由于未考虑到诸如sum(i,1,3,i**3)此类情况,导致在字符串替换时,会出现表达式中含常数的幂次,而parser无法分析计算常数的幂次,解决方法为,在sum函数字符串替换时,加上括号()
Hack策略
笔者好没本事,第三周才开始真正hack人,策略也没什么特殊,一是查Sum的上下限是否支持BigInteger,二是验证同学的sin|cos 输出是否能满足基础要求(当表达式因子时多一层括号),三是玩一点小花招,当sin内部为Sum函数时,如下图
0
sin(sum(i,0,2,sin((i*x))))
程序是否会把内部当做变量因子,而不加括号。
感想与体会(算是总结吧)
有点遗憾,因为时间原因,没尝试写三角函数平方和化简和两倍角公式化简。
由于是第一次写面向对象的构造设计,写完三次作业后一直试着反思,面向对象编程和面向过程编程思想的区别,思来想去,也总结不好,就以助教哥哥的几句话作结:面向对象要思考的问题就是,这个问题需要设计哪几个类,设计类需要考虑这些类单独需要管理什么数据,处理数据需要什么方法,类与类之间如何协作办事。可能笔者比喻得不恰当,但以第一次作业为例,笔者认为,面向编程思想拿到表达式最先考虑的是一个字符串的处理吧,就如同笔者第一周拿到作业第一次,从处理字符串的角度很难着手展开括号。面向对象的思考方式,应该是思考表达式中有哪些类,什么类要存储管理什么所需要的特定数据,相应的运算方法具体化为类方法。
本次作业最大的收获是,明显察觉到自身不再是像写代码shit山,在写类方法时开始考虑是否能调用已有的方法简化,考虑是否能通过Java的多态性减少if_else if 的使用,更重要的是,开始思考能否应用递归调用来简化方法,比如笔者至今都“洋洋得意”的一处设计就是当Polynomial类做mul方法是调用Factor()的mul方法。除此以外,笔者对于java容器的使用,如何用自定义类作为容器的key,以及对Java可变对象、不可变对象,Java深拷贝、浅拷贝有了初步的类了解。