2019年Java项目中的十大bug


2019年即将结束,PVS-Studio团队正在总结即将过去的一年的结果。 在2019年初,我们通过支持Java语言扩展了分析仪的功能。 因此,关于检查打开的项目的出版物列表中充斥着Java项目的评论。 在这一年中发现了很多错误,我们决定准备其中最有趣的10个错误。


第十名:标志性字节


来源: 静态分析器PVS-Studio分析RPC框架Apache Dubbo的源代码

V6007表达式'endKey [i] <0xff'始终为true。 OptionUtil.java(32)

public static final ByteSequence prefixEndOf(ByteSequence prefix) { byte[] endKey = prefix.getBytes().clone(); for (int i = endKey.length - 1; i >= 0; i--) { if (endKey[i] < 0xff) { // <= endKey[i] = (byte) (endKey[i] + 1); return ByteSequence.from(Arrays.copyOf(endKey, i + 1)); } } return ByteSequence.from(NO_PREFIX_END); } 

许多程序员认为,名为byte的类型将是无符号的。 确实,经常是使用不同语言的情况。 例如,在C#中, 字节类型是无符号的。 在Java中,情况并非如此。

在条件endKey [i] <0xff中,该方法作者将字节类型的变量与以十六进制表示形式表示的数字255(0xff)进行比较。 显然,在编写该方法时,开发人员忘记了Java中byte类型的值的范围是[-128,127]。 此条件始终为true,因此for循环将始终仅处理endKey数组的最后一个元素。

第九名:二合一


来源: PVS-Studio for Java已发送到该路径。 下一站是Elasticsearch

V6007表达式'(int)x <0'始终为false。 BCrypt.java(429)

V6025可能索引'(int)x'超出范围。 BCrypt.java(431)

 private static byte char64(char x) { if ((int)x < 0 || (int)x > index_64.length) return -1; return index_64[(int)x]; } 

今天我们有特价! 一种方法一次出现两个错误。 第一个错误的原因是char类型,该类型在Java中是无符号的,这就是为什么条件(int)x <0始终为false。 第二个错误是当(int)x == index_64.length,banal超出了index_64数组的范围 。 由于条件(int)x> index_64.length,这种情况是可能的。 为了摆脱数组的界限,必须将条件'>'替换为'> ='。 正确的条件是: (int)x> = index_64.length

第八名:决策及其后果


来源: 使用PVS-Studio的CUBA平台代码分析

V6007表达式'previousMenuItemFlatIndex> = 0'始终为true。 CubaSideMenuWidget.java(328)

 protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) { List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem); int previousMenuItemFlatIndex = menuItemFlatIndex + 1; if (previousMenuItemFlatIndex >= 0) { // <= return menuItemWidgets.get(previousMenuItemFlatIndex); } return null; } 

如果menuItemWidgets列表包含currentItem ,则findNextMenuItem方法的作者希望摆脱indexOf方法返回的-1。 为此,他将一个值添加到indexOf结果( menuItemFlatIndex变量)中,并将结果值存储在previousMenuItemFlatIndex变量中,该变量将在方法中进一步使用。 解决-1问题的方法失败,因为它一次导致多个错误:

  • return null代码将永远不会执行,因为表达式previousMenuItemFlatIndex> = 0始终为true,这意味着findNextMenuItem方法的返回始终在if内发生;
  • menuItemWidgets列表为空时,将抛出IndexOutOfBoundsException ,因为将访问空列表的第一个元素;
  • currentItem参数是menuItemWidget列表中的最后一个参数时,将发生IndexOutOfBoundsException异常。

第七名:从零开始创建文件


资料来源: 华为云:今天PVS-Studio多云

V6008可能会取消引用“ dataTmpFile”。 CacheManager.java(91)

 @Override public void putToCache(PutRecordsRequest putRecordsRequest) { .... if (dataTmpFile == null || !dataTmpFile.exists()) { try { dataTmpFile.createNewFile(); // <= } catch (IOException e) { LOGGER.error("Failed to create cache tmp file, return.", e); return; } } .... } 

编写putToCache方法时程序员在条件dataTmpFile == null ||中输入了错误。 !dataTmpFile.exists()在创建新的dataTmpFile.createNewFile()文件之前 一个错字是使用运算符'=='而不是'!='。 调用createNewFile方法时,此错字将引发NullPointerException 。 纠正错字后的情况如下:

 if (dataTmpFile != null || !dataTmpFile.exists()) 

“发现了错误,已更正。 您可以放松,“您会想。 但是无论如何!

更正了一个错误之后,我们发现了另一个错误。 现在,调用dataTmpFile.exists()时可能会发生NullPointerException 。 现在,要摆脱异常,有必要在以下情况下替换'||'运算符 在“ &&”上。 所有错误消失的条件如下:

 if (dataTmpFile != null && !dataTmpFile.exists()) 

第六名:一个非常奇怪的逻辑错误


资料来源: PVS-Studio for Java

V6007 [CWE-570]表达式'“ 0” .equals(text)'始终为false。 ConvertIntegerToDecimalPredicate.java 46

 public boolean satisfiedBy(@NotNull PsiElement element) { .... @NonNls final String text = expression.getText().replaceAll("_", ""); if (text == null || text.length() < 2) { return false; } if ("0".equals(text) || "0L".equals(text) || "0l".equals(text)) {// <= return false; } return text.charAt(0) == '0'; } 

此方法很有趣,因为它包含明显的逻辑错误。 如果满意的方法在第一个if之后没有返回值,则已知文本字符串至少包含两个字符。 因此,下一个if中的第一个校验“ 0” .equals(文本)是没有意义的。 开发人员真正的意思仍然是个谜。

第五名:这是转身!


资料来源: PVS-Studio访问Apache Hive

V6034按'bitShiftsInWord-1'的值进行移位可能与以下类型的大小不一致:'bitShiftsInWord-1'= [-1 ... 30]。 UnsignedInt128.java(1791)

 private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord; // check this because "123 << 32" will be 123. final boolean noRestore = bitShiftsInWord == 0; final int roundCarryNoRestoreMask = 1 << 31; final int roundCarryMask = (1 << (bitShiftsInWord - 1)); // <= .... } 

在输入参数wordShifts = 3bitShiftsInWord = 0的情况下 ,变量roundCarryMask用来存储位移的结果(1 <<(bitShiftsInWord-1))将变为负数。 也许开发人员没有想到这种行为。

第四名:例外会散散步吗?


资料来源: PVS-Studio访问Apache Hive

V6051在“ finally”块中使用“ return”语句可能会导致丢失未处理的异常。 ObjectStore.java(9080)

 private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... /*some actions*/ committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } } 

getMPartitionColumnStatistics方法的声明对我们说谎,说它可能引发异常。 当try中发生任何异常时, 提交的变量仍为false ,因此,在finally块中, return语句返回方法中的值,并且所有引发的异常都将丢失并且无法在方法外部进行处理。 因此,此方法中引发的任何异常将永远无法摆脱它。

第三名:我扭曲,扭曲,我想要一个新的面具


资料来源: PVS-Studio访问Apache Hive

V6034移位'j'的值可能与类型的大小不一致:'j'= [0 ... 63]。 IoTrace.java(272)

 public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j); // <= } .... } .... } 

另一个错误与按位转换有关,但这次不仅他参与了此案。 在内部for循环中,变量j [0 ... 63]用作循环计数器。 此计数器涉及1 << j的位移。 没有任何预兆,但是int类型的整数文字“ 1”(32位值)在这里起作用。 因此,在j大于31之后,将开始重复移位的结果。如果所描述的行为不理想,则必须将单位表示为long ,例如1L << j(long)1 << << j

第二名:初始化顺序


资料来源: 华为云:今天PVS-Studio多云

V6050存在类初始化周期。 在初始化“ LOG”之前出现“ INSTANCE”的初始化。 UntrustedSSL.java(32),UntrustedSSL.java(59),UntrustedSSL.java(33)

 public class UntrustedSSL { private static final UntrustedSSL INSTANCE = new UntrustedSSL(); private static final Logger LOG = LoggerFactory.getLogger(UntrustedSSL.class); .... private UntrustedSSL() { try { .... } catch (Throwable t) { LOG.error(t.getMessage(), t); // <= } } } 

在类中声明字段的顺序很重要,因为按照声明它们的顺序初始化字段。 但是,当他们忘记它时,就会发生细微的错误,就像这样。

分析器指出,将静态LOG字段初始化为null时 ,将在构造函数中取消引用,这将导致NullPointerException- > ExceptionInInitializerError异常链。

“为什么在构造函数调用时,静态LOG字段为null ?”您问。

ExceptionInInitializerError 异常是一个提示。 事实是,此构造函数用于初始化在类中声明的INSTANCE静态字段,早于LOG字段。 因此,在构造函数调用时, LOG字段仍未初始化。 为了使代码正常工作,必须在调用构造函数之前初始化LOG字段。

第一名:面向复制粘贴的编程


来源: Apache Hadoop代码质量:生产VS测试

V6072找到两个相似的代码片段。 也许这是一个错字,应该使用“ localFiles”变量而不是“ localArchives”。 LocalDistributedCacheManager.java(183),LocalDistributedCacheManager.java(178),LocalDistributedCacheManager.java(176),LocalDistributedCacheManager.java(181)

 public synchronized void setup(JobConf conf, JobID jobId) throws IOException { .... // Update the configuration object with localized data. if (!localArchives.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALARCHIVES, StringUtils .arrayToString(localArchives.toArray(new String[localArchives // <= .size()]))); } if (!localFiles.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALFILES, StringUtils .arrayToString(localFiles.toArray(new String[localArchives // <= .size()]))); } .... } 

首先是复制粘贴,或者说是由于犯有此罪的人的粗心而引起的错误。 很可能第二个if由第一个if的复制粘贴创建并替换了变量:

  • localFiles上的 localArchives ;
  • MRJobConfig.CACHE_LOCALFILES上的MRJobConfig.CACHE_LOCALARCHIVES

但是,即使使用这种简单的操作,也会出错,因为在第二个分析器的第二个if行中仍然使用localArchives变量,尽管很可能暗示了使用localFiles

结论


纠正在开发的后期或项目发布后发现的错误需要大量资源。 PVS-Studio静态分析器简化了编写代码时的错误检测,从而显着减少了用于修复错误的资源。 不断使用分析仪已经简化了许多公司的开发人员的生活。 如果您想愉快地编程,请尝试使用我们的分析仪

我们的团队不会止步于此,并将继续改进和改进分析仪。 预计明年会有更多有趣的错误的新诊断和文章。

我看着你喜欢冒险! 首先,赢得了2019年C#项目中前10个错误 ,现在Java可以克服了! 欢迎来到有关C ++项目中2019年最佳错误的文章的下一级别。





如果您想与说英语的读者分享这篇文章,请使用以下链接:Valery Komarov。 2019年在Java项目中发现的十大bug

Source: https://habr.com/ru/post/zh-CN481186/


All Articles