我决定测试IntelliJ IDEA静态Java代码分析器,并与它一起测试The Chemistry Development Kit项目。 在这里,我将给出一些我发现的错误。 我认为其中一些对于整个Java程序来说都是典型的,因此它们可能很有趣。
化学开发套件是用于解决化学信息学和生物信息学问题的开源Java库。 当我从事生物信息学研究时,我们积极地使用它。 该项目已经开发了20多年,拥有数十名作者,并且代码质量非常不均匀。 不过,该项目具有单元测试,并且pom.xml中规定了与JaCoCo覆盖分析器的集成。 此外,还为三个静态分析器配置了插件: FindBugs , PMD和Checkstyle 。 检查哪些警告仍然更加有趣。
内置在IntelliJ IDEA中的静态Java代码分析器并不劣于专用的静态分析工具,但在某些方面要优于它们。 另外,几乎所有静态分析功能都可以在Community Edition (一个免费的开源IDE)中获得。 特别是,免费版本会产生本文中描述的所有警告。
默认情况下,静态分析是在代码编辑模式下持续进行的,因此,如果您使用IntelliJ IDEA编写代码,那么即使在进行测试之前,您也可以在几秒钟内从字面上纠正许多错误。 您可以使用“ 分析” |“批处理”检查整个项目或其一部分。 使用Analyze | 检查代码或运行单独的检查 按名称运行检查 。 在这种情况下,由于检查的复杂性,有些检查将变得无法在编辑模式下进行。 但是,这种检查很少。
许多IntelliJ IDEA检查不会报告错误,而是提供不准确的代码或提供更惯用,美观或快速的替代方法。 当您不断在IDE中工作时,这很有用。 但是,就我而言,最好从警告实际错误的消息开始。 基本上, Java类别很有趣| 可能的错误 ,尽管还有其他类别值得探讨,例如“ 数字问题” 。
我只会告诉您一些有趣的警告。
1.一元加
该项目中已经有66个一元加号。 写+1
而不是1
有时候我想变得美丽。 但是,在某些情况下,如果不是+=
写 =+
,那么一元加号就会出现:
int totalCharge1 = 0; while (atoms1.hasNext()) { totalCharge1 = +((IAtom) atoms1.next()).getFormalCharge(); } Iterator<IAtom> atoms2 = products.atoms().iterator(); int totalCharge2 = 0; while (atoms2.hasNext()) { totalCharge2 = +((IAtom) atoms2.next()).getFormalCharge(); }
一个明显的错别字,它忽略了循环中除最后一次以外的所有迭代。 它不是写为“等于空格加空格的空间”,而是“等于空格加空格的空间”,这似乎很奇怪。 但是,如果您探究历史 ,陌生感就会消失。 最初,“等号”和“加号”确实存在,但在2008年,它们经历了自动格式化程序,并且代码发生了变化。 顺便说一句,这对于静态分析器来说是道德的:基于奇怪的格式发出警告是合理的,但是如果代码被自动格式化,警告将消失并且错误仍然存在。
2.整数除法导致小数
一个相当烦人的错误,但是静态分析器发现它很好。 这是一个例子 :
angle = 1 / 180 * Math.PI;
不幸的是,该角度不是1度而是0度。 类似错误 :
Integer c1 = features1.get(key); Integer c2 = features2.get(key); c1 = c1 == null ? 0 : c1; c2 = c2 == null ? 0 : c2; sum += 1.0 - Math.abs(c1 - c2) / (c1 + c2);
看来c1
和c2
非负数,这意味着差模不会超过总和。 因此,如果两个数字都不为零,则结果将为0;如果其中一个数字为0,则结果将为1。
3.调用Class.getClass()
有时人们对Class
类型的对象调用getClass()
方法。 结果还是具有常量值Class.class
Class
类型的对象。 这通常是一个错误:无需调用getClass()
。 例如, 在这里 :
public <T extends ICDKObject> T ofClass(Class<T> intf, Object... objects) { try { if (!intf.isInterface()) throw new IllegalArgumentException("expected interface, got " + intf.getClass()); ...
如果发生异常,则报告它将完全无用。 顺便说一句,错误处理过程中的错误通常是通过旧项目中的静态分析发现的:通常,错误处理过程经过最坏的测试。
4.在数组上调用toString()
这是经典的流派:toString()未为数组重新定义,其结果非常无用。 通常,这可以在诊断消息中找到。
int[] dim = {0, 0, 0}; ... return "Dim:" + dim + " SizeX:" + grid.length + " SizeY:" + grid[0].length + " SizeZ:"...
很难用眼睛注意到问题,因为此处dim.toString()
隐式的,但字符串串联将其委托给它。 立即建议修复-将其包装在Arrays.toString(dim)
。
5.读取集合但未填充
这也经常在未经静态分析器进行持续测试的代码库中找到。 这是一个简单的例子 :
final Set<IBond> bondsToHydrogens = new HashSet<IBond>();
显然填充只是错过了。 静态分析器具有更简单的检查来指示未使用的变量,但是此处使用了变量,因此它们是静默的。 我们需要更智能的检查,以了解有关集合的信息。
6.相反:我们填写但不阅读
相反的情况也是可能的。 这是一个数组示例 :
int[] tmp = new int[trueBits.length - 1]; System.arraycopy(trueBits, 0, tmp, 0, i); System.arraycopy(trueBits, i + 1, tmp, i, trueBits.length - i - 1);
检查知道,arraycopy方法的第三个参数仅用于写入数组,此后根本不使用该数组。 根据代码的逻辑判断,跳过trueBits = tmp;
行trueBits = tmp;
。
7.通过==比较整数
这是一个阴险的错误,因为缓存了较小的Integer对象值,并且在一天的数量超过127之前,一切都可以正常工作。这样的问题可能根本不明显 :
for (int a = 0; a < cliqueSize; a++) { for (int b = 0; b < vecSize; b += 3) { if (cliqueList.get(a) == compGraphNodes.get(b + 2)) { cliqueMapping.add(compGraphNodes.get(b)); cliqueMapping.add(compGraphNodes.get(b + 1)); } } }
好吧,似乎已经比较了某些列表中的某些对象,也许一切都很好。 必须注意这些列表包含Integer类型的对象。
8.复制地图
在这次检查中,一张图片值一千字。 看到错误了吗?

9.不使用该方法的结果。
一些方法的结果是愚蠢的不使用,IDEA随即报告 :
currentChars.trim();
可能这意味着currentChars = currentChars.trim();
。 由于Java中的字符串是不可变的,因此如果不重新分配结果,将不会发生任何事情。 例如 ,还可以找到str.substring(2)
。
顺便说一下,这是一个相当复杂的检查。 除了预先准备的方法列表外,我们有时还会尝试自动确定值得使用其结果的方法。 这里,在源文本和库的字节码中都需要进行过程间分析。 所有这些都是在编辑代码的过程中即时完成的!
10.不可达的交换机分支
由于我们排除了代码大于128的字符,因此分支\u2012-\u2212
无法访问。 似乎不值得排除。
11.无法达到的条件
条件链中一个绝对绝妙的问题:
if (oxNum == 0) { if (hybrid.equals("sp3")) { ... } else if (hybrid.equals("sp2")) return 47; } else if (oxNum == 1 && hybrid.equals("sp3")) return 47; else if ((oxNum == 2 && hybrid.equals("sp3")) || (oxNum == 1 && hybrid.equals("sp2")) || (oxNum == 0 && hybrid.equals("sp")))
在复杂的条件逻辑中,这种情况并不罕见:我们检查的条件不能成立,因为上面已经检查了其片段。 在这里,我们有一个单独的分支oxNum == 0
,否则我们检查oxNum == 0 && hybrid.equals("sp")
,当然不能。
12.我们写一个零长度的数组
有时IntelliJ IDEA会通知您是否写入超出其大小的数组:
Point3d points[] = new Point3d[0];
13.访问索引后检查长度
该过程以及错误处理期间的另一个常见问题是:
public void setParameters(Object[] params) throws CDKException { if (params.length > 1) { throw new CDKException("..."); } if (!(params[0] instanceof Integer)) {
在数组为空的情况下,代码的编写者希望安静地退出,但是由于进行了验证,因此它将退出,大声地敲击ArrayIndexOutOfBoundsException。 显然,检查的顺序是混乱的。
14.访问后检查是否为空
再一次,违反了操作顺序, 这次是null :
while (!line.startsWith("frame:") && input.ready() && line != null) { line = input.readLine(); logger.debug(lineNumber++ + ": ", line); }
IDEA写那line != null
始终为true。 碰巧检查实际上是多余的,但是这里的代码看起来好像真的可以为null。
15.分离而不是合取
人们经常将逻辑运算符AND和OR混淆。 CDK项目也不例外 :
if (rStereo != 4 || pStereo != 4 || rStereo != 3 || pStereo != 3) { ... }
无论rStereo
和pStereo
等于什么,很显然它们不能同时等于4和3,因此,此条件始终为真。
16.再次分离而不是合取
类似的错误 ,但被另一条消息捕获:
if (getFirstMapping() != null || !getFirstMapping().isEmpty()) { ... }
仅当getFirstMapping()
返回null
,我们才能进入右侧,但是在这种情况下,我们可以保证有一个NullPointerException,IDEA会警告该异常。 顺便说一句,这里我们依靠getFirstMapping()
方法结果的稳定性。 有时我们使用启发式方法,但是在这里直接分析稳定性。 由于类是最终的,因此该方法不能被覆盖。 IDEA return firstSolution.isEmpty() ? null : firstSolution
检查其主体return firstSolution.isEmpty() ? null : firstSolution
return firstSolution.isEmpty() ? null : firstSolution
,它确定稳定性归结于Map#isEmpty
方法的稳定性,该方法先前已注释为“稳定”。
17.接口和instanceof的层次结构
检查对象是否属于任何接口时,请不要忘记接口可以相互继承:
if (object instanceof IAtomContainer) { root = convertor.cdkAtomContainerToCMLMolecule((IAtomContainer) object); } else if (object instanceof ICrystal) { root = convertor.cdkCrystalToCMLMolecule((ICrystal) object); } ...
ICrystal
接口扩展了IAtomContainer
接口,因此显然无法达到第二个分支:如果晶体进入此处,它将落入第一个分支。
18.遍历一个空列表
这段代码的作者可能不太熟悉 Java语言:
List<Integer> posNumList = new ArrayList<Integer>(size); for (int i = 0; i < posNumList.size(); i++) { posNumList.add(i, 0); }
ArrayList
的size参数指示内部数组的初始大小。 如果您事先知道要放置多少个项目,这将用于优化以减少分配的数量。 但是,实际上,列表中的项没有出现,并且size()
方法返回0。因此,尝试用零初始化列表项的下一个循环完全没有用。
19.不要忘记初始化字段
分析器以一种特殊的方式检查构造函数,并考虑了字段初始化程序。 由于此,发现了这样的错误 :
public class IMatrix { public double[][] realmatrix; public double[][] imagmatrix; public int rows; public int columns; public IMatrix(Matrix m) { rows = m.rows; columns = m.columns; int i, j; for (i = 0; i < rows; i++) for (j = 0; j < columns; j++) { realmatrix[i][j] = m.matrix[i][j];
尽管字段是公共的,但这里没有人可以肯定地在构造函数之前对其进行楔入和初始化。 因此,IDEA大胆地发出警告,指出访问数组元素将引发NullPointerException。
20.不要重复两次
重复的情况也经常发生。 这是一个例子 :
if (commonAtomCount > vfMCSSize && commonAtomCount > vfMCSSize) { return true; }
这样的错误是阴险的,因为您永远不知道,第二个条件仅仅是多余的,或者作者想检查其他内容。 如果没有立即解决此问题,那么可能很难弄清楚。 这是为什么应经常使用静态分析的另一个原因。
我在项目的错误跟踪器中报告了其中一些错误 。 很好奇的是,当该项目的作者修复了一部分时,他们自己使用了IntelliJ IDEA分析器,发现了我没有写过的其他问题,并且也开始修复它们 。 我认为这是一个好兆头:作者意识到了静态分析的重要性。