Java理论与实践: 消除bug
副标题#e#
许多有关编程气势气魄的发起都是为了建设高质量、可维护的代码,这很公道, 因为最容易修复 bug 的时间就是在发生 bug 之前(少量的防范法子……)。遗 憾的是,只防范往往是不足的,固然有一些精良的东西可以辅佐您建设好的代码 ,可是很少有东西可以辅佐您阐明、维护或提高现有代码的质量。
写线程安详的类很难,而阐明现有类的线程安详性更难,加强类使其仍然保 持线程安详也很难。以隐含假定、稳定式以及预期用例(固然在开拓人员的脑子 中很清晰,可是没有以设计条记、注释可能文档的方法记录下来)的方法编写完 类之后,人们很快就不再相识类的事情方法(可能应该如何事情),现有代码总 是比新代码难以利用。
需求:更好的代码审核东西
虽然,确保高质量代码的最佳机缘就是在编写代码时,因为在这个时期您最 相识它的组织方法。关于如何编写高质量代码可以找到许多发起(阅读本栏目即 可!),可是未必能从新编写所有代码或花许多时间来编写它。那么在这种环境 下该怎么办?开拓人员凡是喜欢从头编写代码(究竟,与修复他人的代码或修复 本身编写但 bug 许多的代码对比,编写新代码有趣得多),可是这也是一种奢 侈,而且凡是只是用本日已知的错误与来日诰日未知的错误互换。您需要的是下面这 种东西:阐明和审核现有的代码库以辅佐开拓人员举办代码审核并找出 bug。
我很兴奋地说,跟着 FindBugs 的引入,在自动代码检测和审核东西方面已 经取得重大进步。到今朝为止,大大都检测东西要么积极试图证明措施是正确的 ,要么注重一些外貌问题,如代码的名目编排和定名法则,最多还存眷一些简朴 的 bug 模式,如自赋值、未利用的域或潜在的错误(如未利用的要领参数,或 可以声明为私有或掩护的要领被声明为民众的)。可是 FindBugs 差异,它操作 字节码阐明和许多内置的 bug 模式检测器来查找代码中的常见 bug。它可以帮 助您找出代码的哪些位置有意可能无意地偏离了精采的设计道理。(有关 FindBugs 的先容,请参阅 Chris Grindstaff 的文章,“ FindBugs,第 1 部 分: 提高代码质量”和“ FindBugs,第 2 部门: 编写自界说检测器”。)
设计发起和 bug 模式
对付每种 bug 模式,设计发起中都存在相应的防范要素,用于申饬我们制止 这种 bug 模式。因此假如 FindBugs 是 bug 模式检测器,那么它理所虽然可以 用作审核东西,权衡代码与一组设计道理的切合水平。Java 理论与实践的许多 期文章都专门报告设计发起的详细要素(或相应的 bug 模式)。在这一期,我 将表明 FindBugs 如何确保现有代码库遵循设计发起。让我们以新方法反复前面 的一些发起,并相识在没有遵守这些发起时,FindBugs 如何辅佐检测。
关于异常的争论
在“ Java 理论与实践: 关于异常的争论”中,阻挡查抄型异常的一个论据 是:“探索”(也就是捕捉)这种异常太容易了,而且它既不采纳批改行为,也 不抛出其他异常,如清单 1 所示。在原型设计中,有时仅仅为了使措施编译, 编写空的 catch 块,目标是今后返回并填充某种错误处理惩罚计策,这时常常呈现 这种“探索”。固然一些人提供产生这种情景的频率,是为了作为例子说明 Java 语言设计回收的异常处理惩罚要领的不易操纵性,可是我认为这仅仅是错误地 利用了正确的东西。FindBugs 可以利便地检测和标志这些空的 catch 块。假如 想要忽略这种异常,可以利便地给该异常添加描写性注释,这样读者就知道您是 有意的忽略它,而不是仅仅忘了处理惩罚。
清单 1. “探索”异常
try {
mumbleFoo();
}
catch (MumbleFooException e) {
}
哈希
在“ Java 理论与实践: 哈希”中,我略述了正确地重载 Object.equals() 和 Object.hashCode() 的根基法则,出格是相等工具(按照 equals() ) 的 hashCode() 值必需相等。固然只要相识了这项法则,遵守起来就相当简朴(并 且有些 IDE 包括一些领导,用于以一致的气势气魄为您界说这两个要领),可是如 果重载了个中一个要领,而健忘重载另一个要领,那么通过检测很难找出 bug, 因为错误并非位于存在的代码中,而是位于不存在码中。
FindBugs 有一个检测器用于检测这个问题的许多实例,如重载了 equals() 但没有重载 hashCode() ,或重载了 hashCode() 但没有重载 equals() 。这些 检测器是 FindBugs 中最简朴的,因为它们只需要查抄该类中一组要领签名,并 确定是否同时重载了 equals() 和 hashCode() 。还大概错误地利用 Object 之 外的参数范例界说 equals() ;固然这个结构是正当的,可是它的行为和您想像 的差异。Covariant Equals 检测器将检测如下有问题的重载:
public void boolean equals(Foo other) { ... }
#p#分页标题#e#
与这个检测器相关的是 Confusing Method Names 检测器,它是对名称雷同 hashcode() 和 tostring() 的要领触发的,对付下面这些类也会触发这个检测 器:具有一些只在名称巨细写方面存在差此外要领,可能其要领与超类结构函数 的名称沟通。固然按照该语言的类型,这些要领名称是正当的,可是它们大概不 是您想要的。雷同地,假如域 serialVersionUID 不是 final ,不是 long , 也不是 static ,就会触发 Serialization 检测器。
#p#副标题#e#
Finalizer 不是伴侣
在“ Garbage collection and performance”中,我极力阻止利用 finalizer。Finalizer 需要牺牲许多机能,而且它们不能(甚至完全不能)保 证在估量的时间段运行。仍然有些时候需要利用 finalizer,而这样做的进程中 大概发生许多错误。假如必需利用 finalizer,凡是应该如清单 2 所示来组织 它:
清单 2. 正确的 finalizer 界说
protected void finalize() {
try {
doStuff();
}
finally {
super.finalize();
}
}
FindBugs 检测许多有问题的 finalizer 结构,如:
空的 finalizer(它抵消超类 finalizer 的浸染)。
不实现任何成果的 finalizer(它只挪用 super.finalize() ,可是这对运 行时优化大概造成一些损害)。
显式的 finalizer 挪用(从用户代码中挪用 finalize() )。
民众 finalizer(finalizer 应该声明为 protected )。
没有挪用 super.finalize() 的 finalizer。
这些 bug 模式的例子如清单 3 所示:
清单 3. 常见的 finalizer 错误
// negates effect of superclass finalizer
protected void finalize() { }
// fails to call superclass finalize method
protected void finalize() { doSomething(); }
// useless (or worse) finalizer
protected void finalize() { super.finalize(); }
// public finalizer
public void finalize() { try { doSomething(); } finally { super.finalize() } }
在“ Garbage collection and performance”中,还讲到另一种垃圾收集危 险:显式地挪用 System.gc() 。这种显式的挪用险些完全是“辅佐”或“欺骗 ”来机收集器的误导实验,而且它们最终常常损害机能,而不是对其有利。 FindBugs 可以检测显式的 System.gc() 挪用,并标志它们(在 Sun JVM 上, 还可以利用 -XX:+DisableExplicitGC 启动选项,禁用显式的垃圾收集)。
安详结构技能
在“ Java 理论和实践:安详结构技能”中,我展示了答允工具的引用逃避 其结构函数如何导致一些严重的问题。从当时起,答允 this 引用逃避结构的风 险变得越来越严重。假如答允工具的引用逃避其结构函数,新的 Java Memory Model(如 JSR 133 所指定,并由 JDK 1.5 实现的)抵消了所有初始化安详保 证。
工具的引用可以以几种方法逃避它的结构函数,直接和简接都可以。绝对不 可以将 this 引用生存在静态变量或数据布局中,可是有更微妙的方法答允引用 逃避结构,如发布对非静态内部类的引用,可能从结构函数中启动一个线程(这 险些老是发布对新线程的引用)。FindBugs 有一个检测器,用于寻找从结构函 数启动线程的实例,固然今朝它不能检测所有这些危险,可是将来的版本很大概 包罗用于其他初始化安详模式的检测器。
为内存模子带来长处
在“ 修复 Java 内存模子,第 1 部门”中,我回首了同步的根基法则:只 要读取大概由其他线程写入的变量,可能写入随后由其他线程读取的变量,就必 须举办同步。很容易“健忘”这个法则,出格是在读取时 —— 可是这么做可以 造成许多有关措施线程安详的风险。这种 bug 凡是是在维护类时引入的:这个 类本来是正确同步的,可是维护人员并没有完全领略线程安详需求。
幸运的是,FindBugs 拥有大量的检测器,它们可以辅佐识别错误同步的类。 Inconsistent Synchronization 检测器很大概是 FindBugs 所利用的最巨大的 检测器;它必需阐明整个措施,而不只仅是单个要领,利用数据流阐明来确定什 么时候加锁,并利用直观揣度来推出一个类想要提供线程安详担保。根基上,对 于每个域,它城市查察该域的会见模式,而且假如大大都会见都是同步实现的, 那么没有同步的会见将被标志为大概的错误。雷同地,假如一个属性的配置函数 是同步的,而获取函数不是,那么 Inconsistent Synchronization 检测器将生 成一条告诫。
#p#分页标题#e#
除了 inconsistent synchronization 之外,FindBugs 还包括其他许多用于 检测常见线程错误的检测器,如在加锁两次的环境下期待监督器(这固然不必然 是 bug,可是大概导致死锁),利用双检测加锁模式,不正确地初始化非易失性 的域,对线程挪用 run() 而不是启动线程,从结构函数中挪用 Thread.start() ,可能没有将 wait() 包装到轮回中就挪用它。
变革,或稳定革
在“ 变照旧稳定?”(和其他文章中),我赞扬了不行变的利益,不行变对 象不能进入不不变的状态。它们在本质上就是线程安详的(假设它们的不行变性 是通过利用 final 要害字担保的),而且您可以随意共享缓和存对不行变工具 的引用,而不必复制可能克隆它们。
Java 语言中包罗 final 要害字是为了辅佐开拓人员建设不行变类,并答允 编译器和运行时情况以声明的不行变性为基本举办优化。然而,固然域可以是 final,可是数组元素不行以。通过正确地利用 final 和 private 域,可以使 工具成为不行变的,可是假如工具的状态包罗数组,那么防备对这些内部数组的 引用逃避该类的要领是很重要的。清单 4 展示的类实验成为不行变的,可是不 是,因为在挪用 getStates() 之后,挪用者可以修改状态数组。(相关的大概 bug 是在可变类大概返回可变数组的引用时,而且在挪用者利用这个数组时,它 的内容大概已经变动了。)固然凡是将其看作一种“恶意代码”懦弱性(而且很 多开拓人员并不体贴“恶意代码”,因为他们的系统并不加载“不受信任”的类 ),可是这种习惯仍然大概导致各类与恶意代码无关的问题。返回一个不行修改 的 List 可能在返回之前克隆该数组大概更好。FindBugs 可以检测雷同 getStates() 中的错误(如清单 4 所示)—— 固然它不必知道 States 类是假 定为不行变的,可是知道这个配置函数返回了可变私有数组的句柄,而且相应地 做了标志。
清单 4. 错误地返回可变数组的引用
public class States {
private final String[] states = { "AL", "AR", "AZ", ... };
public boolean isState(String stateCandidate) { ... }
public String[] getStates() { return states; }
}
bug 都很重要
FindBugs 确实是一种不寻常的东西,它险些可以在任何时间找出实际的 bug 。您大概认为它搜索的一些变量自赋值之类的 bug 模式,它们太微不敷道了, 以至于不必贫苦地查找,可是您错了 —— FindBugs 的每个检测器都已经在测 试、产物、专业的开拓代码中发明白 bug。您的代码中是否躲藏着未知的 bug? 下载一个 FindBugs,并实验对您的代码利用它。功效大概会开导(和滋扰)您 。