DBMNG数据库管理与应用

科学是实事求是的学问,来不得半点虚假。
当前位置:首页 > 网文摘录 > IT杂谈

Java开发者必知:开发中常见的危险信号

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

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

  这里将要谈及的很多“危险信号”通常都会收到来自于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使用的是相同的属性,并且在这两个方法中属性的顺序要保持一致。

  编译器警告与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中使用的接口范围过大。

本站文章内容,部分来自于互联网,若侵犯了您的权益,请致邮件chuanghui423#sohu.com(请将#换为@)联系,我们会尽快核实后删除。
Copyright © 2006-2023 DBMNG.COM All Rights Reserved. Powered by DEVSOARTECH            豫ICP备11002312号-2

豫公网安备 41010502002439号