feng - 云代码空间
——
这篇博客来自我前年实习到去年编码实践过程的一些感悟(全部来自个人实践),因为各种原因没有继续更新下去,这篇博客的标题称之为十个坑,希望读者你对照着看看你在编码过程中是否也遇到过这些坑(当然有几个坑现在看来略显低级)。
A. 利用代码静态分析技术(2011年12月29日)
代码静态分析,具体操作技术又有代码走查、代码审查、桌面走查等多种,通俗理解即写完代码后先不急着运行看结果,而是逐行阅读自己的代码,看是否存在语法和逻辑错误,是否有一些小疏忽,代码走查速度一般为10行/分钟。
至于我为什么推荐这个,源于今天(2011年12月29日下午)碰到的一个“不是bug的bug”(后面就知道为什么我这么说),由于是下午精神状态也不是很好,花了2个多小时才终于排查出来。我采用的是大多数人用的方法,运行代码,然后观察结果,没有任何异常和错误抛出,但却得不到我预期的输出,当时有点慌了。如果是异常或错误还好,至少能定位到发生异常或错误的代码行。这下好了,只得从那个输出结果的代码开始沿着调用链往上一小块一小块地添加打印语句,然后执行观察结果了。最后在所有我认为可能涉及的类中全部加上打印语句并且输出后,居然还是没发现明显的错误信息。这下凌乱了,当时心一下子凉了“这下任务得延迟到明天了,还不知道明天能不能解决”。后来抱着“病急乱投医”的想法,认为会不会是数据结构的选择有问题呢,是不是Set接口不能用呢?改用List接口呢?然后就将Set接口逐个替换,替换过程,突然发现一个东西,让我眼前一亮,当时大呼“我靠,原来在这里!”
原来,我定义了三个类,假设为A、B、C好了,都实现了接口I,而且A、B、C的方法基本类似,在写代码过程,我给A的某个方法加了一行代码,而忘了给B、C也加上(虽然A、B、C各自的这行代码类似,但也有点小差异,所以没放到父类),这样之后就出现之后的“不是bug的bug”了。更悲剧的是我认为A、B、C、I的关系很明显,而且A、B、C类都是些简单的属性和SET/GET方法以及几个增加元素、删除元素方法,所以压根没怀疑过问题会出在这个里面。
好吧,事实证明我的想当然导致辛苦调试2个多小时。这时我想如果当初我写完A、B、C、I之后以及每修改一个类的某处后,都先审查下这个类以及和它有关的类(即它依赖的、它关联的以及依赖它的、关联它...的类)是否会因为这个修改而产生一些问题,而不是马上急着写其他类的代码,最后将1000多行的代码全部写完后,一起跑一遍,发现问题再调试,那样的话或许我只需要10分钟甚至5分钟就能避免后面这个bug,也因此可以节省2个多小时的时间。
总的教训是步调宜小,步调求稳。即不要将一大团的意大利面条似的代码全部写完,然后才开始测试,应该步步为营,写一块测试一块,然后块之间两两测试,再逐步扩大范围。这样我想bug会少很多的吧。
此外,可以利用一些代码静态分析工具来帮助你排查一些代码缺陷。比如这个链接有关于Java静态代码分析工具的比较:http://www.ibm.com/developerworks/cn/java/j-lo-statictest-tools/
B. 不要相信未经验证的数据,包括自己程序产生的和从其他地方获取的(2012年1月13日)
比如一个桌面Widget的主要功能是根据当天的天气状况给出穿衣、防暑保暖建议。获取天气数据是通过一个第三方WebService完成的,那么我们不能假设在任何情况下都能成功获取到天气信息,而应该考虑并处理获取不到天气信息的情况,这样可以避免很多莫名其妙的程序崩溃。
C. 避免程序员的“自大心理”,务必虚心,出了问题时先想想会不会是自己的代码有问题,而不是先入为主地认为一定是别人的接口实现有问题(2012年2月9日)
程序员的“自大心理",没错,作为程序员都有死不认错,出了错往别人身上找错的习惯(或许是人本性中的弱点)。在做公司的一个测试工具时,有一个部分是调用其他地方的Web Service,但是出现一个很诡异的问题,从第二次调用开始的每次调用的结果都和第一次相同。我一遍又一遍检查我写的Web Service调用流程、调用顺序、调用参数....,但是始终找不到问题所在。这个问题从去年农历新年前一直拖到今天,成为第一个让我束手无策的bug,今天终于被mentor解决掉了,在感叹mentor犀利之余,我不禁想到为什么我找了这么多遍都找不到问题呢?或许真的是程序员的”自大心理“在作祟,我潜意识里一直认为我调用Web Service部分代码是没有任何问题的,因为流程很清晰,参数也正确,调用顺序也恰当。所以我后来一直在纠结是不是这个Web Service本身的缺陷呢?比如请求参数的问题、比如请求间隔的问题、还有比如缓存问题。越往这些方面考虑,越不得要领,所以我昨天终于对mentor说了句”我搞不定那个web service bug“。也正是因为这种要命的潜意识让我一直没仔细检查调用Web Service的那个类的类变量,而问题恰恰出现在这里。具体场景就不细说了,将这个bug简要总结下:
对于类变量的修改必须尤其谨慎,比如有这么一个类变量:
1 | private static String s = "hello world <no>"; |
类中有一个静态方法 test,test中对s进行替换(i是传入的字符串):
1 | s.replaceFirst("<no>",i); |
第一次调用s = s.replaceFirst("<no>", 1); 则s现在为 "hello world 1",达到了预期效果。而下次再调用 s = s.replaceFirst("<no>", 2); 时,不能达到预期效果。
类变量的使用有两种情况:
① 类中的方法修改类变量,一般用在多线程中作为控制(或状态)变量(被多个线程共享)
② 类变量只是作为一个“模板”,这时建议将类变量定义为private final static,这样就能阻止类中的方法意外修改类变量。而使用也很简单,比如对于上面的场景:
1 | public class Test { |
2 | private final static String s = "hello world <no>"; |
3 | public static void test(String i) { |
4 | String newStr = s; |
5 | newStr = newStr.replaceFirst("<no>", i); |
6 | } |
7 | } |
上述test方法的第一行之所以这样做事为了突出“模板”的作用,也即newStr只是简单的拷贝模板内容。也可以将两行简写为一行代码如下:
1 | String newStr = s.replaceFirst("<no>", i); |
不过容易给编程人员(尤其是Java新手)造成模板s也改变了的错觉(当然没有改变,字符串类是final的嘛!)。
D. 养成良好的调试习惯和编程习惯,不要只知道System.out.println();(2012年2月23日)
这四天来一直在看别人的代码,总共就一个java文件,但是有近4000行代码,而且基本无注释,好不容易依葫芦画瓢写了个自己的JSP(JSP中wrap多个类)也有一千多行,但是怎么都不能运行成功,这是由于在JSP中,而且代码行数太多,很难用以前惯用的System.out.println调试(虽然可以)。在静态走查代码一遍又一遍后,我突然意识到我忽视了eclipse自带的断点调试功能。于是试了下断点调试功能,将代码运行模式调为Debug模式,并且体验了下是条件断点的设置,着实感觉不错。
除此之外常见的日志工具比如Log4J用起来也是挺方便的,大公司一般都封装自己的日志工具,但是用法同开源日志工具Log4J基本相同。
E. 尽量高效
相信我们所有人都遇到过下面这些情况的一种或多种:
如果你经常遇到上面的情况,那么就很可能你正处在一个低效的团队,而你自己也是一个低效的开发者。虽然说干的事少,能节省不少脑细胞,但是这种“温水煮青蛙”的日子迟早会让你抓狂,让你慢慢对生活产生厌倦。而相反一个高效的团队和一个高效的开发者是斗志昂扬,有条不紊的。斗志昂扬能够确保团队或个人应对开发过程中的困难,有条不紊能保证战斗力的持久性。在一个高效的团队中工作是愉快而有成就感的(当然前提是团队充分认可每个人,让每个人都能在团队中找到归属感)。
而要打造一支高效的团队或塑造一个高效的开发者显然离不开这么四条:
F. 双鸟在林,不如一鸟在手
有时候一个简单的原型往往能帮助你赢得市场,赢得市场的因素往往是产品的创意,而不是产品背后用了多么复杂的技术和多么精妙的设计。
所以不要一直拖延实现,一直沉迷于设计。及早实现原型能帮助你更好地看清系统的整体结构,也对今后的设计重构起了至关重要的作用。最重要地是一个早期原型能让用户看到希望,而让用户能更清晰地阐述需求,并且能更好地帮你向用户解释某些需求是不合理的。沟通成本降低了,开发效率也必然提高。
G.切记“测试代码不要带来副作用”(2012年4月6日14:54)
也许你认为这是句废话,因为任何搞过几年编程的人都知道这么个道理,这几乎是默认的常识。但现实往往是我们在写测试代码时,没有一个声音时刻在我们耳边提醒“你确认你的测试代码不会给原有代码带来副作用?”
如果你没意识到这点,那么即使测试代码具有副作用,你也会一直纠结于原有代码,而不是测试代码,结果必然是徒劳而无益的(就像一头被蒙住眼睛的驴,拉着磨,一圈又一圈,却始终走不出这个圈子)。
今天下午,同学和我一起在搞毕业设计项目,项目的数据库是小巧方便的嵌入式数据库sqllite。由于sqllite本身对事务支持有限和事务处理代码本身的原因(写得比较赶),在界面上点击频率一快时,经常出现“the database is locked”。代码大概如下:
我们很清楚这可能是前一个操作的事务还占据着数据库的锁没有释放才导致这么个异常。所以同学在每个事务commit之后都加了这么一句:
1 | System.out.println(result.isClosed());// result 是ResultSet类型 |
现在代码结构大致如下:
01 | public void readAndInsert(...) { |
02 |
03 | //读事务 |
04 | 开始读; |
05 | commit; |
06 |
07 | System.out.println(result.isClosed());// result 是ResultSet类型 |
08 |
09 |
10 | //写事务 |
11 | 插入数据; |
12 | commit; |
13 |
14 | System.out.println(result.isClosed());// result 是ResultSet类型 |
15 |
16 | } |
结果更诡异的事情发生了,本来慢点点击还不会有问题,现在一点击就出现“the database is locked”异常了。
幸好昨天我看了 SqlLite的锁机制(可参考 http://www.sqlite.org/lockingv3.html),原来SqlLite中进行读前必须获得Shared锁,且Shared锁可以有多个,即允许多个进程或线程同时读。而如果之后要写数据或修改数据,则必须获得Exclusive锁,对于每个数据库文件只允许有一个Exclusive锁。并且在锁由Shared状态进入Exclusive状态之前有一个过渡状态-Pending状态,该状态用来等待所有被占用的Shared锁释放,并且不允许申请新的Shared锁。等Shared锁全部被清掉后才可以进入Exclusive状态。
于是我想会不会是因为result.isClosed()方法会重新去查数据库,导致重新获得Shared锁,从而导致后面的写事务不能进行,因为Shared锁没清掉,进入不了Exclusive状态。
把这两行代码删掉,再测试,果不其然。
这说明result.isClosed()方法对于Sqllite数据库的确会再去查询数据库,导致又获取了一个Shared锁。很明显带来了副作用。
类似的带有副作用的代码还有 result.next(); ,即数据库游标操作。
H. 变则通,通则活(2012年4月28日 09:22)
故事发生在昨天,mentor交待一个简单的数据验证任务。类中有一个方法直接可以对输入进行校验返回true或false。另外输入字符串包含两位数字校验位,但是校验方法没有生成校验位的方法,只有验证整体数据是否有效的方法。
而现在的某些输入是非法的,要求找出这些输入并且改正。于是我根据那个校验方法逆向推导出一个生成校验位公式,大概像这样:F(Check Digits) = 97*n+1 - F(A) - F(B),F(String s)定义如下:
于是乎我开始了漫漫的求索...要命的是A和B都是非常长的数字字符串(用BigInteger表示),这样试探的上下限相差非常大(达到几百万的差距)。我应该早就想到这点的,几百万的差距就说明这条路不应该继续走下去,即使走下去最后整出解决方案来,运行效率肯定也是非常之低的。
而我一直忽视了一个问题,两位数字校验位的范围为[00, 99],我可以枚举下(只需要100次),然后代入验证即一样可以完成任务。
通过这件事,我也有几条发现:
A. 不必拘泥于因变量或自变量,如果因变量的范围非常大,回头看看自变量是否能给你启示
B. 正向推导如果非常繁和烦的话,可以试试“验证”方法
C. 我算法太烂了,我该补补课了
I. 调试有道(2012年9月14日20:27)
公司其他部门的同事调用我提供的一个接口时,如果成功能快速返回,但如果失败需要20多秒才能响应。几番周折终于确定是发送邮件超时导致。
期间我采用的是比较笨的“地毯式搜索方法”定位耗时操作,花费了不少时间。但如果我掌握一些必要的调试技巧,必能大大提高调试效率,以下是我经过这次调试过程而得出的所谓“技巧”:
A. 可以采用二分搜索,这样可以减少定位耗时操作代码的时间
B. 代码执行其实是一个个分支,我遇到的这种情况正是异常分支的一条。所以调试时尤其需要关注异常分支(如catch分支、throw exception分支)和不太可能执行的分支(也许不太可能执行但就是执行了)
C. 依靠直觉,而直觉往往来自经验的积累。比如我这个例子,如果你是有经验的编程人员,你肯定知道以前遇到的耗时操作就那么些,然后逐一排查即可(当然我不在此列,所以我特地开了个清单用于记录耗时操作)
J. 谨慎处理异常(2012年11月16日15:11)
参考自 http://kb.cnblogs.com/page/162987/ ,上代码:
1 | Guy AddNewGuy (string name) |
2 | { |
3 | Guy guy = new Guy (name); // 1 |
4 | AddToLeague (guy); // 2 |
5 | guy.Team = ChooseRandomTeam (); // 3 |
6 |
7 | return guy; |
8 | } |
上面的代码有什么问题?你可能会说没什么问题。但是这段代码却隐含了风险。第1行代码不太可能发生异常,即使发生异常也不要紧。第2行代码如果发生异常就不会往数据库中插数据,所以也不要紧。但是如果第2行代码运行成功,而第3行代码发生异常会如何?会导致数据库中多了这个成员的数据,但是这个成员不属于任何组。这显然不是我们开始想要的,我们想要的是为每个新增成员随机分配一个小组。
怎么改呢?使用事务是一种解决方法,还有另一种简单的方法,只要调换2、3行代码即可:
1 | Guy AddNewGuy (string name) |
2 | { |
3 | Guy guy = new Guy (name); // 1 |
4 | guy.Team = ChooseRandomTeam (); // 3 |
5 | AddToLeague (guy); // 2 |
6 |
7 | return guy; |
8 | } |
关于异常和异常处理框架的一篇不错的文章推荐:http://www.ibm.com/developerworks/cn/java/j-lo-exceptionframework/