内功与轻功
时隔半年回顾面向对象先导课程,有一些细节记不太清了,但温故知新,在这里发表一些微小的心得体会。
课程内容
课程的主体内容整理如下:
编号 | 主要内容 |
---|---|
0 | 课堂测试 |
1 | 重新认识代码风格 |
2 | 字符串处理 |
3 | 快速debug |
其中课堂测试与之前的几次课程差别不大,而主体内容真的让人耳目一新,我将其总结为内功和轻功:所谓内功即在众多细微隐藏巨大的能量,简单的一掌就将人打得七荤八素;而轻功就是天下武功,为快不破。下文将分模块进行讨论。
代码风格
代码风格在我看来就是内功的一种。它说起来非常抽象,似乎是玄学,玄之又玄,而且只说风格也可能会是相当枯燥,所以我决定从实例入手,来阐述我对代码风格的理解。
这里给出两段代码,两段代码的功能都是段判断传入的StringSet
(片段1中为stringSet
)类型对象是否为当前对象的子集。
- 代码片段1
//public stringSet();
//private Vector<String> stringSet;
public int issubset(stringSet a){ //issubset
int lenOfA = a.getlen(); //a.getlen()
if(lenOfA == 0){
System.out.println("Yes!");
return 1; //return 1
}
for(int i = 0; i < lenOfA; i++){ //for(int i...)
if(stringSet.indexOf(a.getByIndex(i)) == -1){ //stringSet.indexOf
System.out.println("No!");
return 0;
}
}
System.out.println("Yes!");
this.times ++;
return 1;
}
- 代码片段2
//public StringSet();
//private Vector<String> stringSet;
public boolean isSubset(StringSet a) {
if(a.size() == 0) {
System.out.println("Yes!");
return True;
}
for(String inte: a.stringSet) {
if(this.stringSet.contains(inte)) {
System.out.println("No!");
return false;
}
}
System.out.println("Yes!");
this.times ++;
return True;
}
哪一段代码看上去更友好一些呢?
第一个代码片段摘自我的2017年先导课第二次代码作业,而第二段代码是我现在重新理解后功能后重构的。当然我个人认为第二段代码更友好一些。
实际上,我在重读第一段代码时都遇到了不少让自己费解的地方,列举如下:
行号 | 代码片段 | 问题描述 |
---|---|---|
3 | public int issubset |
一开始:“这个词我好像看不太懂”;之后:“这个类干什么来着?”;“哦。。。”;最后:“反面教材就是它了。” |
4 | a.getlen() |
感觉可能知道这个方法的作用,但是在Java众多的库里都没见过这样的方法 ,总觉得不放心。 |
9 | for(int i...) |
应该是想做个遍历,但里面的getByIndex 看上去不像随机访问接口,需要写着么多东西吗? |
10 | stringSet.indexOf |
这怎么调用了个类的静态方法?不对类的命名和类中元素的命名重复了。 |
17 | return 1 |
整个方法看下来,基本就是做个真伪判断了,为什么要像C一样返回1 或0 呢? |
我自己都看不明白,让别人看可能就更难了。如果要debug,还需要花费不小的学习成本,简言之,可维护性差。Java语言还是自由的,自由意味创造力,但也意味着有出错风险。怎么权衡两者,就是编程风格要解决的问题了。
回到例子,针对以上问题给出如下建议:
命名
命名做到能“望文生意”,用一套自己习惯的方式对不同类型的元素进行不同格式的命名。Java开发常用驼峰命名法,具体规则参考链接:命名。
这里再添加一些我现在使用的规则:
编号 | 描述 |
---|---|
0 | 不混用中文拼音与英文,除了一些国际通用的名称,正例如:Beijing。 |
1 | 不随便缩写单词,反例如:Button缩写为BT。 |
2 | 不使用错误的单词拼写。 |
3 | 不以$作为开头。 |
为什么需要有这样的一套命名规则呢?
简而言之,提升代码可读性。目前大部分的代码维护工作还是由人来完成的,之所以使用高级语言的一个原因就是提升代码的可读性,进而从某种程度上降低代码的维护难度,降低(大工程)开发难度。Java是典型的适用于大工程的语言,(考虑到OO之后的代码量)我想还是不要辜负它“易读”的特性。
上述问题中不易读的情况有:
issubset
(这里不是针对某些脚本语言)在第一眼看那上去时可能不太容易理解;
stringSet
类的命名和类中元素的名称重复就更容易造成误解了。
读尚且费劲,如果要debug就更让人头大了,维护2000+代码再加上随意的命名方式可能就是火葬场了。
结构
你要是不知道这个方法应该怎么写,看看Java自带库是怎么做的。 ——神.昂
上述问题中,getlen
的设计就与官方库中约定俗成的习惯不符。
如果在一个类中要一个从字符串解析出数据的方法,应该怎么设计这个方法呢?
如果是初学,可以看看Java官方库怎么做的:
Integer.parseInt(String s):int -Integer
Parameters:
s a String containing the int representation to be parsed
Returns:
the integer value represented by the argument in decimal.
Throws:
NumberFormatException - if the string does not contain a parsable integer.
如果使用这样的结构设计解析数据方法,可能比新建一个对象然后用parse方法对其赋值返回boolean
要更易读。使用try
和catch
的一个主要原因是避免维护结构正确性的代码与实现主要业务逻辑的代码混杂在一起。
这里举一对例子:
- 代码片段3
if(!flag)
{
Log.Logging(rawLine);
System.out.println("ERROR");
return ;
}
String s = reader.getLine();
if(s==null) {
System.out.println("ERROR");
return ;
}
if(!cp.parse(s)) {
Log.Logging(rawLine);
System.out.println("ERROR");
return ;
}
else {
cp.compute();
if(!cp.printResult()) {
Log.Logging(rawLine);
return ;
}
}
- 代码片段4
try {
Matcher m = reg.matcher(str);
if(m.find()) {
x = Integer.parseInt(m.group(1)); //throw
time = Long.parseLong(m.group(2)); //throw
checkFomat.check(x, time); //throw
return new Request(RequestType.XX, x, bt, time);
}else return null;
}catch(Throwable e) {
return null;
}
两段代码都截取自parse类别的方法。
相比于代码片段3,使用try
和catch
的代码片段4,其主要工作一气呵成,代码也清晰简单了很多。
需要指出的是代码片段4也依然有提升空间,但在风格上会比代码片段3要好一些。申明:代码和第二次作业没有关系。
LESS IS MORE
可能你会觉得很诧异,我居然会说开发Java也需要精简。
这里须指出Java主要的复杂之处在于各种通过设计约束,降低代码运行时自由度和后续开发自由度以提升稳定性,进而降低大型工程开发难度。
这并不是说用Java实现业务逻辑的代码也会很复杂,其他语言精简逻辑的思维方法在这里依然适用。但要注意一点不要轻易尝试自己设计方法来完成标准库已有方法可以完成的功能,简言之,不要重复造轮子。
原因是:耗费时间;未必正确;得不偿失,标准库里的方法往往经过特殊优化。
上述问题中for(int i)
来遍历所有元素就显得比较冗杂,且未必会快。(我也不容易知道getByIndex
是不是随机访问。)
大厂开发手册整理
如果你还没有形成自己的风格,不妨看看大厂是怎么做的,也许能悟出些道理。——航航
字符串处理
字符串处理之后会在拾遗中持续更新。
快速debug
课上要求的快速debug现在依然记忆犹新。
这一部分我认为是轻功。天下武功,为快不破,如何在限定时间之内找到所有的bug,实在是很不容易。
构建Bug树
Bug树是一个很好的办法。
STEP1: 根据需求判断程序的主要功能分支,先测试主要功能是否能够正确执行。
STEP2: 如果发现有问题,调试定位找具体位置;
STEP3: 若没有问题,尝试往边界条件试探,返回STEP2。
积累常识
- bug代码1:
//private int[] box;
public Object clone(){
Object a = new Object();
a.box = this.box;
return a;
}
其中犯了常识性错误:clone要将对象在内存中复制一遍避免意料之外的共享,但执行上述代码后this.box
和a.box
依然是引用了内存中的同一个对象。
- bug代码2:
pattern = "[\-|+]";
pattern用来构建正则表达式,除了匹配-
和+
还匹配|
。注意要和(\-|+)
区别。
- bug代码3.1:
int index = 0;
int temp ;
char[] tempWord = new char[20];
while((temp = reader.read()) != -1){
//读取单词,逐个字符读取
if((char)temp == ' ' || (char)temp == '
'){
if(index == 0){
continue;
}
tempWord[index] = ' ';
stringSet.myAdd(new String(tempWord,0,index));
index = 0;
}
else{
tempWord[index++] = (char)temp;
}
}
当时课上我们需要在一份代码中找出若干bug并修复。代码的主要功能是从英文文本中截取单词并保存在一个Set中(文本中的语义符号算作单词一部分),以上是代码截选。根据需求可知需要找出一种合适的分割单词的方式,常规的想法是用“空格”和“换行”来分割单词。以上代码似乎就实现了这一功能。但要考虑到:Unicode编码中有很多字符展现出来是所谓“空格”和“换行”的,以上代码显然没有达到目的。
- bug代码3.2:
public int getWordNum(String word){
int num = 0;
Iterator<String> ite = this.stringSet.iterator();
while(ite.hasNext()){
String temp = ite.next();
if(ite.equals(temp))num++;
}
return num;
}
接上一段代码,这个方法作为保存String字符串对象的方法,功能是统计传入的word
在文本中出现了几次。以上代码依然不能满足要求,原因是没有考虑到word="test"
而Set中有"test!"
的情况。此时应该进一步明确单词内匹配的模式(e.g. 贪婪模式)。
- bug代码4:
Long a = 19971l;
阅读代码时很难区分是199711
还是19971l
。错误的理解可能导致意料之外的bug产生,应该使用19971L
来表示。另:Java中不加L
或l
的数将被解析为int类型,超出范围将报错。
未完待续
写在后面
这篇随笔完全是抛砖引玉,希望有更多的同学发表自己的想法见解!
谢谢助教学姐提供的帮助!
之后将更新接口部分的内容,暂时先这样。