写在前面
OO作业已经做了三周,有种“一周蜕一层皮”的感觉,还是挺酸爽的。之前虎头蛇尾的博客经历告诉我,及时发博文总结反思是必要的,很开心能在博客园建一个专属的技术博客。希望这些拙劣的碎碎念可以给自己也给大家提供帮助和启迪,也期待和大家共同交流!
PS. 讲一个悲伤的故事,这是这个博文的第三稿...前两稿都被博客园和typora无情吞掉了,于是在默写第三遍之后有些话就省去了...希望以后的博客作业能被电脑温柔对待;(
一、需求分析
用JAVA实现多项式的合法性判断和求导运算。
1)多项式是带常数的幂函数之和
2)多项式是带常数的幂函数和基本三角函数的乘积项之和
3)在(2)的基础上,三角函数的自变量可以进行嵌套
二、思路分析
1、基于度量的程序结构分析
类关系图(利用UML Support插件)
第一次
第二次
第三次
代码行数统计(利用Statistic插件)
第一次
第二次
第三次
代码设计复杂度(利用MetricsReloaded插件)
ev(G)基本复杂度,用来衡量程序非结构化程度
iv(G)模块设计复杂度,用来衡量模块判定结构
v(G)独立路径条数
第一次
第二次
第三次
2、BUG分析
第一次:
强测中得分 100
互测:由于被 hack 扣分 1 分。这其中包括了 13 个错误,同时hack 他人成功 8 次,得分 18.6 分。
自己错误:1)未判断其他空白字符的不合法性 2)处理第一项前面的符号时,未考虑空白字符 的情况
他人错误:正则表达式疏漏,特别是对符号和数字间的空格的特殊处理。例如 - - 5
第二次:
在强测中得分 93.8423
未被hack。hack 他人成功 7 次,得分 0.625 分。
他人错误:正则表达式疏漏,特别是三角函数特殊位置空白字符的判断。例如cos ( x ), -+x
第三次:
在强测中得分 84.7059
由于被 hack 扣分 2 分。这其中包括了 3 个错误。同时, hack 他人成功 25 次,得分 16.75 分
自己错误:1)某一处合并同类项写挂了,导致求导结果不对 2)乘积项输出时,若为空串应直接return,否则会执行下一条charAt(0)语句导致访问越界。例如:+-sin ((x-x) ) ^ +07
他人错误:同前两次。随机数据都能一串三,也警醒我们自测的重要性。
三、知识技能总结
1、正则表达式
(1)正则表达式的组命名:
//正则表达式的组命名 String sin = "(sin\((?<quad>.*)\)(\^" + num + ")?)"; String cos = "(cos\((?<quad>.*)\)(\^" + num + ")?)"; //调用 r = Pattern.compile(sin); m = r.matcher(str); if (m.matches()) return isFactor(m.group("quad"));
(2)正则表达式的回溯
java的正则表达式,相当于把我们在C语言中用栈处理表达式运算符的过程封装起来,所以在匹配过程中要考虑到爆栈问题。正则表达式量词匹配模式区别就在于回溯(弹栈)方式的不同。具体如下:
1 private String matchProduct(String str) { 2 String num = "[+-]?\d+"; 3 String pow = "x(\^" + num + ")?"; 4 String sin = "sin\(x\)(\^" + num + ")?"; 5 String cos = "cos\(x\)(\^" + num + ")?"; 6 String factor = 7 "(" + num + ")|(" + pow + ")|(" + sin + ")|(" + cos + ")"; 8 String product = 9 "^[+-]{1,2}(\d+\*)?((" + factor + ")\*)*+(" + factor + ")"; 10 Pattern r = Pattern.compile(product); 11 Matcher m = r.matcher(str); 12 if (m.find()) { 13 return m.group(); 14 } else { 15 return ""; 16 } 17 }
2、Hashmap
• 第一次作业,直接用指数作为Key即可
• 第二次作业,需要使用(a,b,c)三元对作为自定义Key
1 import java.util.Arrays; 2 public class Tuple { 3 private final int x; 4 private final int y; 5 private final int z; 6 public Tuple(int x, int y, int z) { 7 this.x = x; 8 this.y = y; 9 this.z = z; 10 } 11 public int getX() { 12 return x; 13 } 14 public int getY() { 15 return y; 16 } 17 public int getZ() { 18 return z; 19 } 20 21 @Override 22 public boolean equals(Object obj) { 23 if (obj == this) { 24 return true; 25 } else if (obj instanceof Tuple) { 26 return (((Tuple) obj).x == this.x) 27 && (((Tuple) obj).y == this.y) 28 && (((Tuple) obj).z == this.z); 29 } else { 30 return false; 31 } 32 } 33 34 @Override 35 public int hashCode() { 36 return Arrays.hashCode(new int[]{x, y, z}); 37 } 38 @Override 39 public String toString() { 40 return String.format("(%s, %s, %s)", x, y, x); 41 } 42 } 43 /******************************************************/ 44 import java.util.HashMap; 45 public class Main { 46 private static final HashMap<Tuple, Integer> hashMap = new HashMap<>(); 47 private static void showHashMapValue(Tuple tuple) { 48 if (hashMap.containsKey(tuple)) { 49 System.out.println(String.format( 50 "Value of %s is : %s", tuple.toString(), hashMap.get(tuple))); 51 } else { 52 System.out.println(String.format( 53 "Value of %s not found!", tuple.toString())); 54 } 55 } 56 57 public static void main(String[] args) { 58 hashMap.put(new Tuple(1, 2, 3), 1); 59 hashMap.put(new Tuple(1, 3, 4), 2); 60 hashMap.put(new Tuple(2, 3, 5), 3); 61 showHashMapValue(new Tuple(1, 2, 3)); 62 showHashMapValue(new Tuple(1, 3, 4)); 63 showHashMapValue(new Tuple(2, 3, 5)); 64 showHashMapValue(new Tuple(2, 4, 0)); 65 } 66 }
3、考虑合并同类项的设计思路
4、对拍程序
已上传至GitHub
5、继承与接口
首先明确一点:能用接口就不用继承。
静态绑定与动态绑定
静态绑定:在程序执行前已经被绑定,也就是说在编译过程中就已经明确一个对象类应该调用哪个方法,此时由编译器获取其他连接程序实现。
动态绑定(多态):在程序运行根据具体对象的类型进行绑定,调用对应的方法。
6、递归下降分析
1 /*******************分解表达式************************/ 2 import java.util.ArrayList; 3 public class ExpressionParser { 4 private enum Status { // 表达式解析状态 5 BEGIN, // 开始状态 6 SIGN, // 符号状态 7 TERM, // 项状态 8 END, // 结束状态 9 } 10 11 private Status status; // 当前状态信息 12 private ArrayList<ExpressionItem> items; 13 14 private final String string; 15 private int index; 16 17 public ExpressionParser(String string, int index) { 18 this.string = string; 19 this.index = index; 20 this.status = Status.BEGIN; 21 } 22 23 public Expression parse() { 24 // 起初为开始状态 25 // 如果读到了+/-,则进入符号状态,否则记录符号为+后进入项状态 26 27 // 加减状态 28 // 读取+/-,并记录,之后进入项状态 29 30 // 项状态 31 // 实例化一个新的TermParser,传入字符串以及当前位置 32 // 用此TermParser来生成解析出一个Term,进行存储 33 // 如果接下来还有+/-,则进入加减状态,否则进入结束状态 34 35 // 结束状态 36 // 整理获取到的值,并返回 37 } 38 } 39 /**************分解乘积项***********************/ 40 import java.util.ArrayList; 41 public class TermParser { 42 private enum Status { 43 BEGIN, // 开始状态 44 SIGN, // 符号状态 45 FACTOR, // 因子状态 46 END, // 结束状态 47 } 48 private Status status; 49 50 private ArrayList<Factor> factors; 51 private final String string; 52 private int index; 53 54 public TermParser(String string, int index) { 55 this.string = string; 56 this.index = index; 57 this.status = Status.BEGIN; 58 } 59 public Term parse() { 60 // 起初为开始状态 61 // 直接进入因子状态 62 63 // 符号状态 64 // 读取*后,进入因子状态 65 66 // 因子状态 67 // 因子状态下,实例化一个FactorParser,传入字符串和位置信息 68 // 使用该FactorParser来解析一个因子,并进行存储 69 // 如果下一个字符为*,则进入符号状态,否则进入结束状态 70 71 // 结束状态 72 // 整理获取到的值,并返回 73 } 74 } 75 /***************分解因子*******************/ 76 public class FactorParser { 77 private final String string; 78 private int index; 79 public FactorParser(String string, int index) { 80 this.string = string; 81 this.index = index; 82 } 83 public Factor parse() { 84 // 解析一个因子 85 86 // 如果开头是数字,则连续读取一串数字,实例化Constant对象,返回 87 88 // 如果开头是x,则读取x,实例化XFunction对象,返回 89 90 // 如果开头是s,则读取sin(x),实例化SinFunction对象,返回 91 92 // 如果开头是c,则读取cos(x),实例化CosFunction对象,返回 93 } 94 }
7、设计模式
1)工厂模式
2)单例模式
3)装饰者模式
(%cyx)
以第三次作业为例:
我们可以用Factor(因子)作为抽象父类
以具体的ExprFactor、Sin、Cos等作为被装饰者
用可以嵌套的Sin、Cos作为装饰者(注意到它们既是被装饰者也是装饰者)
调用公有的方法——求导,实现对复合函数的求导
四、自己的一点思考
1、学习方法
1)即用即学
上学期报了java一般专业课,最后摸了一份大作业出来。但这学期正式开始用java编程时,却发现自己之前真正学到的只是偏GUI的一些语法,连正则表达式都还不会用。于是开学初想像学C那样通读语言书,却发现大多数java编程入门书晦涩生硬,也缺少有用的例子。现在再回过头来再看语法,便能理解得更加透彻了,因为在编程中摸爬滚打尝试过一些东西后,语言书变成了使用正确性的印证和内部机制的解释,而不是必须系统学习后才能开始编程的必要前提。因为不同环节侧重的理论知识不同,“即用即学”远比“在没真正使用一个东西的时候通过阅读来提前学习”效率要高。
2)从算法竞赛看工程作业
算法竞赛追求巧妙的思维、代码短小精悍易编写,遇到特殊的边界条件直接特判来莽,总之快速AC是王道。这种写程序的策略有点像在“贪心”。
而工程思维可能更像是“动态规划”,追求“全局最优”,而非“当前最优”。“不行的话我就在这添一添,那里删一删”的想法在这时变得很危险,乱搞和特判的补丁只会让程序看起来极其恶心。设计上的全面考虑和可扩展性在此时显得更加有智慧。
但是,这两者有共同的要求:
1)同样注重代码实现能力,即在规定时间内完成特定功能的“硬实力”。写大作业就像长跑,配速低并不意味着我们可以在长距离长时限下允许思维怠惰。
2)对bug“零”容忍。虽说“世界上不存在没有bug的程序”,但“只要出错就爆零”的紧迫感,如果迁移到工程上或许会敦促我们把作业完成得更好。
2、调试方法
1)边写边调试
从HW1到HW3,我的强测成绩逐渐变形(笑容逐渐消失),其实用来构思的时间逐渐变长,写代码时间逐渐变长,也感到压力逐渐增大。
这些年自己写过的代码量应该有5w+,却面对上千行、面向对象的代码debug时常感到崩溃。我认为做出按照层次来编写程序会有效缓解这一问题:
输入处理(合法性检查)——>求导运算——>输出处理——>合并同类项——>输出优化(指数、系数、符号)——>进阶优化(三角函数)
每个模块编程完成后,顺手debug,既能减少工作量,又能减轻心理压力。
2)构造测试数据
在编码前就尽可能充分考虑不同数据的处理方式,而不是在周二写完代码开始debug的时候才开始造数据。
在编写代码前能够考虑得约细致,编写代码的过程就越顺利,debug的时间也会减少。
但是,对于我们初学者来说,很多实现细节在缺乏足够的经验时确实很难提前考虑到,踩坑和磨练大概是必经之路。
3)小黄鸭调试法
大概这就是“肉眼debug”的精髓。在完成程序后先前前后后捋一遍,就可以大幅度减少在输出调试和单步调试中的许多脑残、手残错误。
其实在思路卡壳不知道该怎么做的时候也可以尝试这个方法,跟身边的人讲一讲思路,就算他们无法给出可行的解决方案,自己也会对问题更加明晰。
所以欢迎大家来和我唠嗑!说不定可以互相启迪> < ~
毕竟哪个程序员都不想每天只对着二次元纸片人和小黄鸭自言自语:))