一、程序结构
1)第一次作业
本次作业我使用了两个类:Polynomial (主类) 和 Item ,其中 Item 类表示一个项,其属性保存了一个项的系数和幂函数x的指数,Item 通过正则表达式解析一个项的字符串,提取其中的系数和指数,并进行求导计算,最后再将求导后的结果转化成字符串;Polynomial 是主类,完成了输入表达式字符串的读取,定义 Item 数组保存表达式各个项的信息,预处理输入字符串,同类项合并,化简(即将项数组中系数为0的项去掉)等工作,最后输出结果。
Item
类
Item
类有三个属性:
-
private String value
存储输入的一个项的字符串
-
private BigInteger coefficient
为该项的系数,初始化为0
-
private BigInteger exponent
表示该项幂函数的指数,初始化为1
Item
类有七个方法:
-
public Item(String value)
(4 行)
这是构造方法,读入一个项的字符串,并调用求导函数 derivation
进行求导。
-
public void derivation()
(38 行)
这是对一个项的求导方法,由于面向对象思想还不丰富,这个方法其实杂糅了正则解析,保存数据和求导三个功能,通过使用正则表达式,对 value
进行解析,匹配并解析每一个因子,若因子是系数,则与本地系数相乘;若因子是指数,则与本地指数相加。匹配结束后,对该项进行求导,即根据求导规则,修改该项的系数和指数。
-
public String toString()
(29 行)
重写了 toString
方法,返回该项的字符串形式
-
4个
getters and setters
方法
Polynomial
主类
Polynomial
的 main
函数:
-
变量定义:
定义动态数组 ArrayList<Item> itemlist = new ArrayList<>()
用来保存每一个项的数据
-
预处理策略:
首先,将所有的空白字符去掉
将所有的 **
改为 ^
,以区分乘法和乘方
将所有连续的两个加减符号替换为一个
(由于后来测试阶段发现无法读取 -x
前面的负号,)将所有的 -x
改为 -1*x
-
解析输入字符串策略:
写出一个项的正则表达式,用该正则去匹配每一个项,将每一个匹配到的项存入项动态数组中
项的正则表达式定义如下:
String regInt = "(((\\+)|(-))?(\\d)(\\d)*)"; // 带符号整数的正则表达式
String regPower = "(x\\^)" + regInt; // 带指数的幂函数的正则表达式
String regX = "(x)"; // 不带指数的幂函数的正则表达式
String regFactor = "(" + regInt + "|" + regPower + "|" + regX + ")"; // 因子的正则表达式
String regItem = regFactor + "(\\*" + regFactor + ")*"; // 项的正则表达式
-
优化策略:
调用静态方法 combine
合并同类项,并去掉所有系数为0的项
-
输出结果:
调用静态方法 print
输出最终结果
Polynomial
有两个静态方法:
-
public static void combine(ArrayList<Item> itemlist)
(26 行)
该方法合并同类项(即指数相同的项系数相加),并将系数为0的项从动态数组中去掉
-
public static void print(ArrayList<Item> itemlist)
(16 行)
该方法将优化后的项数组以字符串形式输出,如果数组为空,则直接输出0;如果第一个项的符号为+,则不输出+;其他情况直接输出项的字符串形式
2)第二次作业
本次作业我使用了三个类:Main
,Expression
,Term
,Calculation
。
本次作业我的大体思路是将括号展开,然后将每一个项看成一个由系数、幂函数、sin和cos组成,然后进行求导计算。在优化方面,我仍然使用了第一次作业中的合并同类项和去掉系数为0的项这两种优化策略。在合并同类项时我使用了 HashMap
这个数据结构,用指数作为Key值,每次获得新的项时,就在Map中寻找是否存在指数相同的项,并进行合并或添加操作,但是本题中一个项有三个指数,三个指数不可能同时作为Key值,因此我将三个指数用空格分隔表示成一个字符串,以该字符串作为一个项的“指数”。
Term
类
该类用来保存一个项的信息,并实现了一些有关项的方法
Term
类有四个属性:
-
private BigInteger coe
表示该项的系数,初始为1
-
private BigInteger expX
表示该项幂函数的指数,初始为0
-
private BigInteger expS
表示该项sin函数的指数,初始为0
-
private BigInteger expC
表示该项cos函数的指数,初始为0
Term
类有5+8个方法,其中8个是 getters and setters
-
public Term(BigInteger coe, BigInteger expX, BigInteger expS, BigInteger expC)
(6行)
这是构造方法,初始化成员变量
-
public Term multiply(Term term)
(7 行)
这个方法实现了两个项相乘,并返回一个新的项表示相乘的结果
-
public String getIndex()
(3 行)
这个方法返回一个项的“系数”,即由空格隔开的三个指数的字符串
-
public String toString()
(32 行)
返回一个项带符号的字符串形式
-
public String funToString(BigInteger expX, BigInteger expS, BigInteger expC, String str)
(34 行)
返回这个项的函数的字符串形式,由于之前的 toString
方法太长,故将其分为两部分,这个方法用于生成最终结果的函数字符串部分。
Expression
类
该类是定义了一个 HashMap
用来保存一个表达式的各个项,实现了解析输入字符串并提取每个项的信息的功能,并将最终结果输出
Expression
类有一个成员变量:
-
private HashMap<String, Term> expression = new HashMap<String, Term>();
用一个 HashMap
保存所有项,其中Key值表示一个项的“指数”,即 expX + “ ” + expS + " " + expC
Expression
类一共有十五个方法
Expression
类有三个构造方法:
-
public Expression()
构造一个不存在项的 Expression
对象
-
public Expression(HashMap<String, Term> expression)
构造一个 Expression
对象,并初始化它的项列表
-
public Expression(Term term)
构造一个 Expression
对象,并将 term
加入到它的项中
Expression
类有四个方法,用于合并表达式,乘上新的表达式,去掉0项,输出结果
-
public void combine(Expression expA)
(12 行)
将新的表达式与自身表达式进行合并
-
public void multiply(Expression expA)
(4 行)
将自身表达式乘上新的表达式
-
public void cleanZero()
(9 行)
去掉自身表达式中系数为0的项
-
public void print()
(19 行)
输出最终结果
Expression
类有七个方法,用于解析输入字符串并匹配每个因子
-
public void getMap(String str)
(20 行)
循环匹配每个项,并将每个项的信息保存在项列表中,定义一个新的子表达式用于保存每次新匹配到的项/括号表达式,每次匹配完一个项之后,将这个表达式加入到这个类的表达式中
-
public static String match(Expression subE, String str)
(48行) -
public static String matchX(String str)
(10行) -
public static String matchS(String str)
(10行) -
public static String matchC(String str)
(10行) -
public static String matchB(String str)
(16行) -
public static String matchI(String str)
(7行)
这是用于匹配一个项并解析其参数的方法组合,匹配策略为:
判断第一个字符,若为 x
则是幂函数,若为 s
则是 sin
函数,若为 c
则是 cos
函数,若为 (
则是括号表达式,其余情况则是整数因子
然后调用相应的亚方法匹配出对应因子的字符串,并返回该因子字符串
解析该因子字符串,并将这个因子的信息保存到一个新表达式中,然后调用 multiply
方法将子表达式乘上新表达式,并从原字符串中删掉该字符串
Calculation
类
该类用于实现表达式相乘和求导的功能
Calculation
类有两个方法:
-
public static Expression multiply(Expression expA, Expression expB)
(25行)
该方法实现了两个表达式相乘的功能,返回一个表达式,表示最终结果
-
public static Expression derivation(Expression expA)
(42行)
该方法实现了对一个表达式求导
Main
主类
主要实现了对输入字符串的读入,预处理,定义一个表达式实例用于解析输入字符串并求导,最后输出结果
Main
类有一个静态方法:
-
public static String init(String str)
对输入字符串进行预处理
3)第三次作业
本次作业我使用了六个类:Sin
,Cos
,Term
,Expression
,WrongFormatException
,PolyNomial
。
本次作业我仍是把每个表达式看成项的集合,每个项五个成员变量:系数,x的指数,sin函数表,cos函数表,表达式表,三角函数的成员变量包括指数和由一个因子组成的项。由于这次作业增加了错误格式判断,因此不能直接用正则表达式去匹配每个因子,我采用的方法是每个字符逐个读取判断,在匹配因子时,如果格式正确则用正则表达式匹配读取因子,否则抛出异常。
Sin
类
存储 sin
函数的信息,实现其求导方法和转换成字符串的方法
sin
类有两个成员变量:
-
private int exp
三角函数的指数
-
private Term term
三角函数括号内的因子,只包含一个因子的项
sin
类有两个构造方法:
-
public Sin()
-
public Sin(int exp, Term term)
sin
类有两个3+2个方法,其中3个 getters and setters
方法:
-
public Term derivation()
(9行)
实现三角函数的求导
-
public String toString()
(13行)
转换成字符串
Cos类
存储 cos
函数的信息,实现其求导方法和转换成字符串的方法
cos
类有两个成员变量:
-
private int exp
三角函数的指数
-
private Term term
三角函数括号内的因子,只包含一个因子的项
cos
类有两个构造方法:
-
public Cos()
-
public Cos(int exp, Term term)
cos
类有两个3+2个方法,其中3个 getters and setters
方法:
-
public Term derivation()
(9行)
实现三角函数的求导
-
public String toString()
(13行)
转换成字符串
Term
类
用于保存一个项的信息,实现了项的求导方法
Term
类有五个成员变量:
-
private BigInteger coe
系数,初始化为1
-
private int exp
x函数的指数,初始化为0
-
private ArrayList<Sin> sin
sin
函数列表,初始化为空列表
-
private ArrayList<Cos> cos
cos
函数列表,初始化为空列表
-
private ArrayList<Expression> expression
括号表达式列表,初始化为空列表
Term
类有两个构造方法:
-
public Term()
-
public Term(BigInteger coe, int exp, ArrayList<Sin> sin, ArrayList<Cos> cos, ArrayList<Expression> expression)
Term
类有五个方法,用于添加成员变量:
-
public void addCoe(BigInteger coe)
-
public void addExp(int exp)
-
public void addSin(Sin sin)
-
public void addCos(Cos cos)
-
public void addExpression(Expression expression)
Term
类有一个方法,用于求导:
-
public Expression derivation()
(60行)
该方法用于对一个项进行求导,对一个有多个因子的项求导,返回值为一个表达式
Term
类有三个方法,用于返回该项的字符串
-
public String toString()
-
public boolean isConstant()
-
public String xtoString()
此外,Term
类还有六个 getters and setters
方法
Expression
类
该类用于解析输入字符串,逐个字符匹配,并判断非法格式
Expression
类有一个成员变量:
-
private ArrayList<Term> expression
保存一个表达式的每个项
Expression
类有一个构造方法:
-
public Expression()
Expression
类有两个方法,用于添加成员变量:
-
public void addTerm(Term term)
-
public void addExpression(Expression expression)
Expression
类有十二个方法,用于解析输入字符串,匹配因子,判断格式:
-
public void parse(String str0) throws WrongFormatException
(25行) -
public static String preTerm(String str) throws WrongFormatException
(49行) -
public static String match(String str0, Term term) throws WrongFormatException
(23行) -
public static String matchSigned(String str0, Term term) throws WrongFormatException
(11行) -
public static String matchUnsigned(String str0, Term term) throws WrongFormatException
(8行) -
public static String matchX(String str0, Term term) throws WrongFormatException
(50行) -
public static String matchB(String str0, Term term) throws WrongFormatException
(8行) -
public static String getBracket(String str0) throws WrongFormatException
(21行) -
public static String matchS(String str0, Term term) throws WrongFormatException
(23行) -
public static String getSinExp(String str0, Sin sin) throws WrongFormatException
(50行) -
public static String matchC(String str0, Term term) throws WrongFormatException
(23行) -
public static String getCosExp(String str0, Cos cos) throws WrongFormatException
(50行)
循环匹配每个项,匹配每个项之前,调用 preTerm
方法预处理字符串,将项前面的空格去掉,若存在符号则把可能的多个符号处理成一个符号;在匹配每个项时,再循环匹配每个因子,即判断第一个字符属于哪种因子,然后调用对应因子的匹配方法读取该因子信息,并将读取过的字符串从原字符串中去掉,其中,匹配括号表达式和三角函数的底数时递归调用了表达式类的 parse
方法匹配子表达式。在上面的过程中,如果存在格式非法,则立马抛出异常。
Expression
类有一个方法,用于求导:
-
public Expression derivation()
(7行)
对表达式的每一个项调用求导方法进行求导
Expression
类有一个方法,用于输出最终结果
-
public String toString()
(9行)
WrongFormatException
类
有一个方法,用于打印错误信息:
-
public void printExceptionInfomation()
Polynomial
类
读取输入字符串,定义表达式对象,对输入字符串进行处理,求导,并输出最终结果,同时捕获异常。
4)优缺点自我评价
优点:将表达式进行层次分解,每个层次写一个类,分别管理各个层次对应的数据,实现相应的方法,在一定程度上降低了程序的耦合性。
缺点:三次作业中我的类使用都特别复杂,将很多方法杂糅到一个类中,导致程序不够简洁明确,各个类之间的关系比较大,导致耦合性比较高,由于有些功能(如字符串匹配,toString
等)实现方法比较繁琐,导致对应的方法规模太大,不得不分解成数个子方法,这就导致了类方法之间的耦合性高,独立性差。
二、自己程序的bug
第一次作业有一个bug:在最终输出结果时乘方符号 **
少了一个 *
第二次作业没有发现bug
第三次作业有两个bug:
-
并未考虑括号表达式以及三角函数的底数为空串时应该抛出异常
-
输出最终结果时将三角函数底数的第一个字符吞掉,导致输出结果错误
三、发现别人bug时采用的策略
组合了所有基本功能的样例
极端情况的样例,比如输入为0,x-x,sin(((()))等
阅读同学的代码,根据代码功能反向构造样例
四、重构经历总结
第二次作业由于三角函数底数只能是x,所以我将所有括号展开,这样整个表达式就变成了项的集合,而每个项有统一的形式,因此第二次作业就是在第一次作业的基础上,增加了递归读取括号,并将括号展开的功能,之后再按照第一次作业的方式,将每个项依次求导。
第三次作业三角函数的底数变成了因子,所以就算我将所有括号展开了,真个表达式仍然不能像第二次作业那样有统一的形式,(即使有,那也是非常复杂的,因为每个项的三角函数因子底数可能不同,所以每个项仍然需要三角函数列表)因此第三次作业我几乎是重写了整个代码,项类和三角函数类相互调用,表达式类和项类也相互调用,并没有进行化简操作。
五、心得体会
通过本单元作业,我深深感受到了OO课程的一大特点:架构才是写好一个程序最重要的。第二次和第三次作业在动手写代码之前,我都花了几乎两三个小时去构思整个代码架构,包括如何将一个表达式进行层次解构,如何设计类来管理每个层次的数据,如何管理各层次数据之间的关系,如何匹配一个字符串,提取其中的因子信息。此外,我也对面向对象编程思想有了一定的认识,将所有的数据看成对象的属性,将具有相同特点的一些数据封装到一个类中,将管理使用这些数据的功能写成方法,封装到类中,实现程序的简介性,封装性,低耦合性,使得问题的解决更加简便。