Java开发中常见的危险信号

Dustin Marx是一位专业软件开发者,从业已经有17年的时间,他拥有电子工程学士学位,还是一位MBA。Dustin维护着一个博客,专门介绍软件开发的各个主题。近日,他撰文谈到了Java开发中常见的危险信号,提出了在日常的Java开发中我们需要尽力避免的一些不正确的做法。


经过多年的开发、阅读、回顾并维护了数万行的Java代码后,我经常会看到Java代码中出现的某些“危险信号”,这些信号经常(但也许并不总是)暗示着代码出现了某些问题。我这里所要谈的并不是那些总是错误的实践,而是想要谈谈在某些场景下可能是恰当,但通常却会导致问题的一些实践。这些“危险信号”有时可能并没有问题,但却会造


Dustin Marx是一位专业软件开发者,从业已经有17年的时间,他拥有电子工程学士学位,还是一位MBA。Dustin维护着一个博客,专门介绍软件开发的各个主题。近日,他撰文谈到了Java开发中常见的危险信号,提出了在日常的Java开发中我们需要尽力避免的一些不正确的做法。


经过多年的开发、阅读、回顾并维护了数万行的Java代码后,我经常会看到Java代码中出现的某些“危险信号”,这些信号经常(但也许并不总是)暗示着代码出现了某些问题。我这里所要谈的并不是那些总是错误的实践,而是想要谈谈在某些场景下可能是恰当,但通常却会导致问题的一些实践。这些“危险信号”有时可能并没有问题,但却会造成问题的积累,并最终导致问题的产生。这里我总结出了一些“危险信号”,并且谈谈在哪些情况下他们是没有问题的,在哪些情况下则会导致问题。


这里将要谈及的很多“危险信号”通常都会收到来自于FindBugs等代码分析工具所发出的警告信息,流行的Java IDE也会将它们标记出来。不过,我发现有不少开发者会忽略掉这些来自于工具与IDE的警告信息,要么是因为他们关掉了提示信息,要么是出于自身的开发习惯或是不理解与这些警告信息所关联的风险,因此会忽略掉警告信息。


成问题的积累,并最终导致问题的产生。这里我总结出了一些“危险信号”,并且谈谈在哪些情况下他们是没有问题的,在哪些情况下则会导致问题。


这里将要谈及的很多“危险信号”通常都会收到来自于FindBugs等代码分析工具所发出的警告信息,流行的Java IDE也会将它们标记出来。不过,我发现有不少开发者会忽略掉这些来自于工具与IDE的警告信息,要么是因为他们关掉了提示信息,要么是出于自身的开发习惯或是不理解与这些警告信息所关联的风险,因此会忽略掉警告信息。


 


 


对引用使用==(而不是.equals)


很多Java开发者都知道使用==比较原生类型数据,使用.equals比较引用类型数据。这是一条很容易记住的简单原则,Java开发者这么用也没什么问题。有时使用==来比较标准的Java类型引用(String、Integer、Long等等)也没问题,不过这要取决于被缓存的值的大小,因此这么做并不是一个好的做法。有时,我们需要检查标识的相等性而不是内容的相等性,在这种情况下使用==来比较引用就很适合了。相对而言,我更喜欢Groovy的处理方式,==类似于.equals,而===则是更加严格地比较标识。同理,使用!=来比较两个引用也是一个“危险信号”,因为如果待比较的两个对象不共享相同的标识(内存地址),即便他们拥有相同的内容也总是会返回true。


对枚举使用.equals(而不是==)


坦率地说,对于枚举,Java开发者使用==还是.equals都没有太大关系。不过,我更倾向于对枚举使用==。这么做最重要的原因就是对枚举使用==可以防止不小心将枚举与不相关的对象进行比较(永远不会相等)。Object.equals(Object)方法可以接收任意对象,这意味着编译器并不会强制限定传进来的对象要与被比较的对象是相同的类型。一般来说,我更喜欢静态的编译期问题检测而非动态运行期的问题检测,对枚举使用==可以满足这个要求。同理,在比较枚举时,!=与!.equals也是一样的。


魔数与字符串字面值


我经常会在Java代码中看到有人使用“魔数”和字符串字面值。他们对于未来的维护来说是一种“危险信号”,让我十分怀疑应用的正确性。在单个位置处将其标识为常量(如果可能用枚举来表示更佳),这么做可以改善未来的维护,并且让我可以更加自信地相信使用这些值的所有代码都在使用着相同的值。除此之外,在一个地方定义好常量与枚举可以更方便地使用IDE的“查找使用”特性来找到所有使用这些常量的地方。


字符串常量


在看到有限的相关字符串常量时,我就在想使用枚举应该更加适合。对于高度内聚的字符串常量的情况来说更是如此,因为枚举可以更好地表达出这些字符串所表示的概念。相比于字符串常量来说,枚举提供了编译期的静态类型安全与潜在的性能优势。对于程序的正确性来说,编译期的安全是最吸引我的地方。


使用Java的“goto”


很少有人会使用标签代码,如果使用了那也说明用法不当。换句话说,如果使用了也是滥用而已。在大多数情况下,使用Java的“goto”会造成代码的可读性极差。


根据作用域来确定恰当的变量引用


我认为这种方式永远都是不恰当的,但它却能运行,甚至有时是被某些Java开发者有意而为之。比如说,Java开发者将传递进方法的变量在方法执行时指向了另一个引用。该变量(临时指向方法参数)指向了另一个引用,直到方法结束为止,这时它脱离了作用域。在这种情况下,在方法签名的参数定义前加上final关键字会导致编译器错误,这也是我喜欢在方法参数前加上final的原因之一。对于我来说,在方法中声明一个新的局部变量是更加清晰且可读的方式,因为它只能在方法中使用。更为重要的是,作为代码的读者,我不知道是开发者有意希望该参数名只是指向一个不同的值还是引入了Bug,因为将参数重新指向新的引用实际上会改变调用端的值。如果我看到有人这么写,那么我就会找代码的编写者或是通过单元测试来验证代码的意图。


equals(Object)与hashCode()方法的不匹配


虽然我认为每个Java类都应该重写toString()方法,但对于equals(Object)与hashCode()方法来说却并不这么认为。我觉得只有在需要这些方法的场合下才应该重写类中的这两个方法,因为他们的存在暗示着其设计与开发某种程度上的完全改变。特别地,equals与hashCode方法要能满足其意图与契约(位于Object类的API文档),并且需要保持一致。大多数IDE与分析工具都会在其中一个方法重写而另一个没有重写的情况下给出提示。然而,我要确保equals与hashCode使用的是相同的属性,并且在这两个方法中属性的顺序要保持一致。


缺乏Javadoc注释


我倾向于将所有的契约方法(特别是public方法)都使用Javadoc注释起来。此外,我还觉得对属性添加注释是必要的,注释要描述清楚属性存储的内容是什么。我听说有人拿代码是“自文档”的作为不写Javadoc注释的借口,不过我对此却并不认同,我想对这些人说的是简单的Javadoc注释可以表达出相同的信息,而阅读代码则需要花费更长的时间。虽然有些方法的名字会很长,但有时也无法准确地描述清楚期望的输入条件与输出结果。我认为很多Java开发者(就像我一样)在使用JDK时更喜欢阅读它的Javadoc注释而不是JDK源代码。既然如此,那我们自己编写的代码为何就要反其道而行之呢?我有时也会阅读源代码,这主要是因为注释不够充分,或是行为与描述的不一致,还有可能是我有理由相信注释过期了。


我还喜欢查看类属性的Javadoc注释。有些人倾向于在public get/set方法前加上属性描述信息,不过我不喜欢这种方式,因为这么做就是在假设属性总是有get/set方法(我是不会做这种假设的)。


在方法的Javadoc注释中描述实现细节


虽然我认为没有Javadoc注释是个危险信号,不过错误地使用Javadoc注释同样也是个危险信号。Javadoc注释不应该说明实现细节,而应该关注于客户端对方法的期望(参数)与方法对客户端的期望(返回类型,还有可能是抛出异常等)。在真正的面向对象系统中,实现应该是可以在public接口下进行修改的,因此将接口文档中的这些实现细节放到Javadoc注释中是不恰当的做法。这是个“危险信号”,因为Javadoc中存在的实现细节会让我怀疑注释的时效性。换句话说,这种注释经常会过期,随着代码的不断演化而出现错误。


源代码中的注释


虽然接口(通常是Javadoc)中缺乏注释是一种危险信号,不过方法体和其他源代码中的注释也可能是个危险信号,会导致潜在的问题。如果需要为代码添加注释以帮助人们理解代码的行为,那就很有可能是代码的复杂性过大(“注释是恶臭代码的芳香剂”,这句话有些滑稽,不过在一定程度上却真的如此)。就像文中提到的众多“危险信号”一样,有时我也认为源代码中的注释是有意义的,可以提供很多信息,这时就没有必要删除这些注释了。然而,我认为代码中的注释(不是接口描述的Javadoc注释)应该尽量少一些,而且主要应该关注于“为什么”这么写而不是“如何做”。代码自身应该能够说明“如何”这一问题,不过很多时候是无法清楚地解释出“为什么”(由于客户/管理方向、设计决策、正式的接口需求、正式的算法需求等等)。


实现继承(extends)


我看到很多时候使用extends(实现继承)都很不错,也适合于相应的场景。但很多时候也存在使用不当的情况,实现继承会导致更多的麻烦。Allen Holub曾说到extends是邪恶的,四人帮的设计模式一书也用了很大的篇幅解释为何说组合要比实现继承更好。随着编写Java代码时间的不断增长,更为重要的是随着我维护Java代码时间的不断增长,我越来越觉得组合相对于实现继承的优势了。我确实看到了实现继承所带来的好处,不过更多的时候类是被“硬塞”到继承体系当中的,子类只不过是对UnsupportedOperationExceptions打洞或是“空实现”,因为他们所继承下来的方法并不适用于他们自己。加上Effective Java一书中所提及的继承问题(比如说具体类的equals与hashCode方法实现等),这真就成了一个大问题了。实现继承并不总是糟糕的,不过很多人经常没有正确地使用他们,因此我觉得这是个“危险信号”。


无作用的代码


程序中出现用不上的代码永远都不是一件好事。人们很快就会忘记它是怎么用的,为什么要这么用。不久之后,开发者就想知道这是不是出于某个原因被扔在那里的。无作用的代码不仅会无意义地增加代码长度,增加维护的复杂度,对于IDE来说,无作用的代码还有可能会被不小心调用,这种情况说明代码基出现了问题。


注释掉的代码


注释掉的代码可能不像可执行的、无意义的代码那么糟糕,因为至少它不会被不小心地调用,显然,这些代码是不会被用到的,不过这仍旧是个危险信号,因为它表明可能会出现潜在的代码基问题。就像无意义的可执行代码一样,注释掉的代码越多,人们就越难理解代码为何会出现在那里、为什么会被注释掉、不使用了为什么不将其删除掉。开发者不敢删除他们,因为留下来肯定是有原因的,只不过没人能够记起来原因是什么了。


To-Do语句


现在越来越流行向代码添加“to-do”语句了,因为现代的Java IDE会为其提供特别的支持与特性。这些特性很有帮助,因为他们经常会将todo标记放在审查列表中,不过“to-do”注释依然是个危险信号,可能会带来与无意义的代码和注释掉的代码相同的问题。当然了,我也会使用“to-do”语句实现短期提醒,不过我觉得最好为其加上一个“过期日期”和联系信息,这样人们就可以清理他们了。加上过期日期与联系信息后,代码中的“to-do”注释就不会一直存在了,否则没有人会记得他们是干什么的,也没人敢删除他们,因为他们可能是在某个时间点被加进去的,很可能是经过某人的深思熟虑后才添加的。当我看到代码中的“to do”语句时,我真是没有任何的办法,只是想知道代码是不是缺少某些功能。


编译器警告与IDE或工具的警告、提示及发现


来自于javac编译器的警告以及IDE和其他代码分析工具的提示信息是Java中明显的危险信号。可以这么说,几乎每一个警告与提示信息都是潜在的危险信号。事实上,我在文中所提及的很多危险信号都是来自于javac编译器或是其他工具与IDE的警告或提示信息。这些警告与提示信息不仅直接对应于很多Java代码的危险信号,而且他们一起出现时更是表明代码出现了严重的问题,这需要引起开发者的足够重视。


关闭编译器警告与IDE和工具的警告及提示信息


当然了,我并不认为FindBugs所标识出的Java代码中的每个问题都是Bug或是Defect。事实上,在某些情况下,我甚至会禁用掉某些提示信息,因为我并不认为这些提示信息是正确的,他们会搞乱FindBugs的输出。也就是说,如果关闭提示信息你需要非常谨慎才行,确保只忽略掉那些不正确的提示信息而保留下重要的警告信息。类似地,我倾向于开启很多IDE警告,不过会将其中一小部分关闭掉。我觉得在禁用掉javac警告的情况下来构建Java应用并不是明智之举。这样做会隐藏掉出现的危险信号,不过关闭这些警告信息的动作本身很可能就是个更大的危险信号,因为这些警告与提示信息会指出很多潜在的危险信号,需要你重点关注。


过度工程化与过早优化


过于复杂的软件通常是一种常见的开发者失调行为的结果。为了代码的灵巧而丧失可读性,或是关注于灵活性及进行没必要的过早优化而造成可读性的降低常常会导致其他问题的产生。过度工程化的开发者常常会这么做,看看能否用点什么新玩儿意,不过这对于高质量的软件来说却并没有什么好处。一旦软件变得过于复杂并且难以理解,那么维护起来就不是那么容易的事情了,而且常常会导致修改时出现问题。


举个例子,在阅读代码时你可能想知道为什么开发者没有采取更加直接的方式来实现。一方面,你可能感叹于开发者能够使用一些更加高级的特性,但另一方面,你可能又会觉得这么做要比正常情况更加复杂了。有很多事实可以证明这是一个危险信号,不过我只在几个地方看到有人会这么做。一种情况是将本来用静态Java代码实现好好的众多功能改用反射、Spring或是其他依赖注入、动态代理、观察者模式等方式来实现。如果用得好的话当然没什么问题,不过我经常看到有人过度使用或是滥用这些特性,这直接导致其他开发者很难理解代码的意图与作用。


将日志消息直接输出到控制台


Java中的日志框架由来已久,如今已经有为数不少的日志框架(有些框架构建在别的框架之上),这包括传统的Log4j 1.2、Log4j 2、java.util.logging(Java Logging API)、Apache Commons Logging及SLF4J等。既然有这么多的日志框架,因此我会很奇怪为什么很多Java代码中还有System.out与System.err语句。


Java代码中存在着向标准输出与标准错误中进行输出可能有很多原因。其中一个原因就是有些代码还不太成熟,后面还会修改,改成输出到日志,不过到最后也没有改。使用标准输出与标准错误的另一个弊端就是这些日志消息并不会被写到日志文件中,而使用日志框架的日志消息则会被写到文件中,这样就会出现不一致的情况。第3个问题就是日志框架提供了不少优秀的特性,如果直接写到标准输出或是标准错误中就无法使用这些特性了。这些特性包括轻松控制日志消息的级别、轻松将捕获到的异常关联到一个日志错误消息上、轻松将输出重定向到不同的目标及使用不同的格式等。虽然在直接使用输出与错误流时这些都可以通过手工来实现,不过这需要自己编写代码而不是“开箱即用的”。


除了直接使用System.out与System.err外,有些Java代码还是会将信息写到标准输出与标准错误上(通常也是隐含使用了System.out与System.err)。比如说,Throwable.printStackTrace()(在处理异常时常常会用到)就会这么做,根据Javadoc的说明,它会将异常与堆栈信息打印到标准错误流中。


使用StringBuffer而非StringBuilder


坦白地说,这只是个小问题而已,不过却能标识出过时的Java代码(StringBuffer是JDK 1.0引入的,而StringBuilder则是J2SE 5引入的)或是开发者并没有真正理解他们之间的区别。在大多数情况下,这两者之间的性能差别对于应用来说是微乎其微的,但由于StringBuilder更适合于大多数使用了StringBuffer的场景,因此我们还是可以从StringBuilder获得性能上的微小提升。 我很少发现使用StringBuffer而不能使用StringBuilder的情况。


方法与构造方法中使用了过多的参数


当方法与构造方法拥有太多的参数时,我总是担心客户端无法正确地使用他们,特别是有些参数是相同类型的情况。如果一个方法接收了3个字符串和3个布尔值,那么客户端就很容易搞混传递进去的值。编译器在这种情况下也无能为力,检测问题的唯一办法就是在运行期(通过单元测试或是其他测试)查看调用结果。过多的参数表明了不恰当的设计。


过多的显式类型转换


显式类型转换最有可能是个危险信号,因为类型转换本身并不会影响到任何功能或是逻辑,不过这却表明情况与预想的不一致。类型转换表明了不太好的设计决策(比如说没有正确利用好多态、在不合适的地方使用了继承、或是强制将一些本不该放在一些的东西放到了一起)。当然了,显式类型转换在很多情况下是恰当和必要的(比如说在获取Spring框架的Bean时),不过有时也表明设计上出现了问题。类型转换还表明API定义的范围过大或是API中使用的接口范围过大。




















































































































































http://bbs.dahe.cn/read-htm-tid-992604805-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604805-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604900-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604902-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604904-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604926-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604928-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604931-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604933-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604935-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604938-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604939-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604943-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604944-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604947-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604949-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604952-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604954-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604956-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604958-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604961-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604963-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604965-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604968-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604970-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604973-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604974-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604978-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604979-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604984-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604985-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604988-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604991-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604992-ds-1.htm
http://bbs.dahe.cn/read-htm-tid-992604996-ds-1.htm




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值