细微的收获
写在前面
学习可以从模仿开始:如果对如何构建工程化代码没有头绪,看看前人流传下来的优秀工程代码,试着模仿也许会有帮助。
jdk中的开源代码就很值得阅读,如果你正在使用eclipse且安装了带源码版本的jdk,只需按住ctrl
键,并点击editor内某个类的声明类名,即可查阅源代码(对于自己构建的代码同理)。
比如,在editor中有如下代码:
String str = new String("test");
按住ctrl
键:点击前一个String
将会打开String
类的源代码文件;点击后一个String
将打开文件,并跳转到对应的构造方法。
下面列举一些我认为目前值得学习的基础类和其中的方法。
1. String
String
是一个final
类,不可被继承。
String
类中主要维护了一个private final char value[]
,在一个String
类被构造后这一数组的值就不会再改变。
方法 | 说明 | 关注 |
---|---|---|
public boolean equals(Object anObject) | 重写了equals 方法 |
判断相等的条件是什么; 什么地方体现了“低耦合”思想; instanceof 的用法。 |
public int compareTo(String anotherString) | 比较两个字符串的序关系 | 判断序关系的标准是什么;为什么要申明c1 和c2 。 |
public String toLowerCase(Locale locale) | 将字符串转换为小写 | 什么是Local ;String 如何支持多国语言。 |
接触JAVA后的一个感想是String
的+
太方便了。但是在浏览了所有String
中的源代码后,我也没有发现对+
的重载操作。查阅相关资料得知JAVA不支持运算符重载,对String类型+
的“重载”是在编译器中完成的。这样的处理方式让String
类型对象有一定的特殊地位。(TODO:查阅OpenJDK javac 源代码)
2. Integer
Integer
类型是基础类型int
的包装类。查看源代码可以得知Integer
的最大值和最小值。
方法 | 说明 | 关注 |
---|---|---|
public static int parseInt(String s, int radix) | 将String 转换为Integer 类型 |
什么地方体现了“低耦合”思想;声明为类的静态方法有什么好处;如何使用thow 。 |
private static class IntegerCache | 缓冲小数据以节省内存开销 | 这样的处理方式有什么好处;将导致什么(考虑== ) |
3.Vector
Vector
(java.util.Vector) 在传说中,是多线程安全的。翻阅代码也可以发现其中基本所有操作方法都加上了synchronized
标记,这个标记保证了在某一线程在执行该对象的这一方法的整个流程中不会有别的线程访问这一方法用到的对象。
那么有这样一个问题:
class RequestList {
private Vector<Request> requestList;
public boolean add(Request rq) {
int requestListLenth = requestList.size(); //获取请求列表的长度
if(requestListLenth < 10) { //如果请求列表长度小于10
requestList.add(rq); //则在列表尾部添加
return true;
}
else {
return false;
}
}
}
RequestList
中使用了多线程安全的Vector
,而其中的add
方法也使用了Vector
中的多线程安全的方法size()
和add()
,那么add
方法也是多线程安全的吗?
synchronized
标识符能使其所作用的代码段(或方法)在多线程操作中“原子化”。“原子操作”在多线程中是安全的,不会被其他进程影响结果的。——佚名
这里先给出结论,以上的代码在多线程运行过程中requestList的长度可能大于10。
详细的阅读笔记在另一篇博客中更新:经典研读 JDK
第一次作业
第一次作业主要实现了多项式的加减操作。
类图
其中各个方法都调用了Log
类的静态方法(图中省略了与Log的关系),用传递枚举变量的方式传递错误信息,这种方法的扩展性差,之后应该改进,考虑使用thow
来传递错误信息。
其中主要的聚合关系是:Term -> Poly -> ComputePoly。这是处理问题的主要框架。
代码质量分析
从类图中就可以看出Term类下的方法太多了,其中的很多方法没有存在的必要。在构建代码时,我的考虑是所构建的类应该更具普遍性,而非单纯完成这次作业,导致代码量激增。事后看来完全没有这个必要。
在保证代码没有太大问题的情况下 ,代码量大不容易被Hack。——某OS助教
metric
可以看到总体质量一般。其中Cyclomatic complexity偏高:Logging类是用来处理控制台日志的,其中使用switch case来判定日志类型,导致复杂度升高。之后可以考虑在需要记录日志的代码段直接提供文本信息,而不使用枚举类来管理。
Debug
测试方法
- 静查
静查包括了对主体业务逻辑,特殊情况处理等的审查。 - 细化bug树构建测试样例
细化bug树是一项有技巧但是回报较高的工作,比如:“多逗号”一项,可以继续细化,如下图所示。
本人代码
由于每个方法的代码量都不大,在构建的过程中不太容易出现逻辑漏洞,在测试后没有发现bug。
对方代码
- 输入:所测试的代码使用类似状态机的方法来判定输入的合法性并解析指令中的相关信息。总体代码量大,静查过于耗费精力,在粗略检查控制台输入部分的代码后,我开始细化bug树构建测试用例,最终发现了一些细节问题。
- 主体逻辑:这部分的代码量比较大,我细化bug树构建测试用例找到了细节问题。
- 输出:逻辑清晰简单,没有与Readme不符的地方。
总结来看,在此次测试代码中单个方法分支多,逻辑复杂的部分比较容易出现bug,此时可以使用bug树进行测试。
第二次作业
第二次作业主要实现了一部功能简单的电梯。
类图
同样,其中几乎每个类都调用了Logger的静态方法(图中省略了与Logger的关系),这种调用方式的可扩展性太差。其余架构设计与推荐架构没有太大区别。
代码质量分析
metric
其中代码圈复杂度较高的是Floor.parseRequest()
函数:
public static Request parseRequset(String str) {
String pattern = "\(FR,([+]?[0-9]{1,}),([A-Z]{1,}),([+]?[0-9]{1,})\)";
Pattern reg = Pattern.compile(pattern);
ButtonType bt;
int floor;
long timeStamp;
try {
Matcher m = reg.matcher(str);
if(m.find()) {
floor = Integer.parseInt(m.group(1));//throw
if(floor>this.maxFloorNO
||floor<this.minFloorNO)return null;
if(m.group(2).equals("DOWN"))bt = ButtonType.DOWN;
else bt = ButtonType.UP;
if(floor == this.minFloorNO && bt.equals(ButtonType.DOWN)
||floor == this.maxFloorNO && bt.equals(ButtonType.UP)) return null;
timeStamp = Long.parseLong(m.group(3));//throw
if(timeStamp>this.maxTimeStamp) return null;
return new Request(RequestType.FLOOR, floor, bt, timeStamp);
}else return null;
}catch(Throwable e) {
return null;
}
}
其中的验证的逻辑分支数较多,可以考虑用别的函数检验,简化逻辑。
Debug
本人代码
未发现bug。
对方代码
对此次得到的测试代码使用使用与第一次相同的方式测试,没有找到漏洞。但是发现了一处会导致crash的写法:
String str=s.nextLine();
////////////
int count=0;
while(!str.equals("RUN")) {
//////////////////
count++;
if(count>=100) {
break;
}
else {
str=s.nextLine();
}
}
s.close();
由于测试中要求不测试终止符,故上述代码不会出错,但是s.nextLine()
在遇到终止符时会抛出异常,应当有相应的处理办法。
第三次作业
在第二次作业的基础上使用了新的调度规则。细节复杂度有较的提升。
类图
代码质量分析
metric
其中调度代码的逻辑比较复杂,在规划优秀架构和短时间完成开发的取舍问题上,在这次的作业中我稍稍偏向于短时间完成开发对架构的设计没有投入足够的精力。
Debug
本人代码
未发现bug。
对方代码
- 静态数组:在动态场景下使用静态数组往往在边界处理的地方比较容易出错。
//class RequestList
RequestList() {
RList = new Request[1000];
head = 0;
tail = -1;
}
///void Main
Scanner s = new Scanner(System.in);
String str = "";
int count = 0;
while (s.hasNextLine()) {
/////
if (req.destination == -1) {
////////
}
else{
////////
count++;
flag = true;
}
if (count > 1000) {
System.out.println("INVALID: we can only handle 1000 requests");
return;
}
}
这样的写法将产生数组访问越界错误,恰好比数组结尾下标多出1。
2. 核心代码复杂:实现主要业务逻辑的代码太长,这里暂不引用。其中的圈复杂度高达113,在细化bug树自动化构建测试集后,发现了两个逻辑问题。
总结
从这三次作业来看,遇到的问题主要有:对Java语言的特性以及基础知识不够了解;核心逻辑不够精简,验证性代码和功能性代码混杂;风格不够成熟,扩展性,维护性不好。
有关风格我在面向对象先导课程——PART3中有所讨论,这里补充一点,如果对抽象的Java开发手册不太感冒,可以考虑阅读优秀的源码,不止在风格方面的学习,对语言特性,常用套路方面的学习也会有所帮助。所以才有了一开始的写在前面。