缺陷描述
本文档列出了 SpotBugs 报告的标准 Bug 模式。
不良实践(BAD_PRACTICE)
违反推荐且必要的编码实践。示例包括 hashCode 与 equals 问题、cloneable 惯用法、被丢弃的异常、Serializable 问题以及 finalize 的误用。我们力求使该分析准确无误,尽管某些团队可能并不在意其中某些不良实践。
JUA:在测试中对 instanceof 断言是不推荐的。(JUA_DONT_ASSERT_INSTANCEOF_IN_TESTS)
在测试中对类型检查进行断言是不推荐的,因为类型转换异常的消息往往能比 instanceof 断言更好地指出误用了错误类实例这一原因。
在调试因错误转换而失败的测试时,观察产生的 ClassCastException 的输出可能更有用,因为它可以提供关于实际遇到的类型的有用信息。而在转换之前先断言类型,则只会得到一个信息量更少的 "false is not true" 消息。
如果 JUnit 与 hamcrest 一起使用,可以改用 hamcrest 中的 IsInstanceOf 类。
AA:对公共方法的参数使用断言(AA_ASSERTION_OF_ARGUMENTS)
不得使用断言来验证公共方法的参数,因为当断言被禁用时,这些验证将不会执行。
更多信息参见 SEI CERT 规则 MET01-J. 永远不要使用断言来验证方法参数。
CT:小心让构造函数抛出异常。(CT_CONSTRUCTOR_THROW)
在构造函数中抛出异常的类容易受到终结器攻击(Finalizer attack)。
可以通过将类声明为 final、声明一个声明为 final 的空终结器,或巧妙地使用私有构造函数来防止终结器攻击。
更多信息参见 SEI CERT Rule OBJ-11。
CNT:发现已知常量的近似值(CNT_ROUGH_CONSTANT_VALUE)
建议使用预定义的库常量,以提高代码的清晰度和精度。
NP:返回布尔类型的方法显式返回 null(NP_BOOLEAN_RETURN_NULL)
既可能返回 Boolean.TRUE、又可能返回 Boolean.FALSE 或 null 的方法迟早会出问题。该方法可以被当作返回 boolean 类型值的方法来调用,编译器会自动插入对 Boolean 值的拆箱操作。如果返回了 null 值,将导致 NullPointerException。
SW:某些 Swing 方法必须在 Swing 线程中调用(SW_SWING_METHODS_INVOKED_IN_SWING_THREAD)
(摘自 JDC Tech Tip):Swing 的 show()、setVisible() 和 pack() 方法会为该框架创建相应的对等体(peer)。随着对等体的创建,系统会创建事件分发线程。这会带来问题,因为事件分发线程可能在 pack 和 validate 仍在处理时就去通知监听器。这种情况可能导致两个线程同时操作基于 Swing 组件的 GUI——这是一个严重的缺陷,可能引发死锁或其他相关的线程问题。调用 pack 会使组件被实现(realize)。在组件被实现的过程中(即此时未必可见),它们可能会在事件分发线程上触发监听器通知。
FI:终结器仅将字段置空(FI_FINALIZER_ONLY_NULLS_FIELDS)
该终结器除了把字段置为 null 之外什么也不做。这完全毫无意义,而且要求对象先被垃圾回收、被终结,然后再被垃圾回收一次。你应当直接删除 finalize 方法。
FI:终结器将字段置空(FI_FINALIZER_NULLS_FIELDS)
该终结器将字段置为 null。这通常是一种错误,因为它无助于垃圾回收,而该对象反正都要被垃圾回收。
UI:如果类被继承,使用 GetResource 可能不安全(UI_INHERITANCE_UNSAFE_GETRESOURCE)
如果该类被另一个包中的类继承,调用 this.getClass().getResource(...) 得到的结果可能与预期不同。
AM:创建空的 zip 文件条目(AM_CREATES_EMPTY_ZIP_FILE_ENTRY)
代码调用了 putNextEntry(),紧接着又调用了 closeEntry()。这会产生一个空的 ZipFile 条目。该条目的内容应在 putNextEntry() 和 closeEntry() 两次调用之间写入 ZipFile。
AM:创建空的 jar 文件条目(AM_CREATES_EMPTY_JAR_FILE_ENTRY)
代码调用了 putNextEntry(),紧接着又调用了 closeEntry()。这会产生一个空的 JarFile 条目。该条目的内容应在 putNextEntry() 和 closeEntry() 两次调用之间写入 JarFile。
IMSE:可疑地捕获 IllegalMonitorStateException(IMSE_DONT_CATCH_IMSE)
IllegalMonitorStateException 通常只有在代码存在设计缺陷时才会抛出(例如,在自己并未持有锁的对象上调用 wait 或 notify)。
CN:类定义了 clone() 但未实现 Cloneable(CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE)
该类定义了 clone() 方法,但并未实现 Cloneable。在某些情况下这样是可以的(例如,你希望控制子类如何克隆自身),但请确保这正是你的本意。
CN:类实现了 Cloneable 但未定义或使用 clone 方法(CN_IDIOM)
该类实现了 Cloneable,但既未定义也未使用 clone 方法。
CN:clone 方法未调用 super.clone()(CN_IDIOM_NO_SUPER_CALL)
此类非 final 类定义了一个未调用 super.clone() 的 clone() 方法。如果本类(“A”)被某个子类(“B”)继承,而子类 B 调用了 super.clone(),那么 B 的 clone() 方法很可能会返回一个类型为 A 的对象,这就违反了 clone() 的标准约定。
如果所有 clone() 方法都调用 super.clone(),就可以保证它们使用的是 Object.clone(),而该方法总是返回类型正确的对象。
DE:方法可能丢弃异常(DE_MIGHT_DROP)
此方法可能会丢弃异常。一般而言,异常应当以某种方式被处理或报告,或者应当从方法中抛出。
DE:方法可能忽略异常(DE_MIGHT_IGNORE)
此方法可能会忽略异常。一般而言,异常应当以某种方式被处理或报告,或者应当从方法中抛出。
Dm:方法调用 System.exit(…)(DM_EXIT)
调用 System.exit 会关闭整个 Java 虚拟机。只有在适当的情况下才应这样做。这样的调用会使其他代码难以甚至无法调用你的代码。可以考虑改为抛出 RuntimeException。
Nm:使用了在 Java 后续版本中为关键字的标识符(NM_FUTURE_KEYWORD_USED_AS_IDENTIFIER)
该标识符所对应的词在 Java 的后续版本中被保留为关键字,为了能在后续版本的 Java 中编译,需要修改你的代码。
Nm:使用了在 Java 后续版本中为关键字的标识符(NM_FUTURE_KEYWORD_USED_AS_MEMBER_IDENTIFIER)
该标识符在 Java 的后续版本中被用作关键字。为了能在后续版本的 Java 中编译,这段代码以及任何引用该 API 的代码都需要修改。
JCIP:不可变类的字段应为 final(JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS)
该类被标注了 net.jcip.annotations.Immutable 或 javax.annotation.concurrent.Immutable,而这些注解的规则要求所有字段都必须是 final。
Dm:方法调用了危险方法 runFinalizersOnExit(DM_RUN_FINALIZERS_ON_EXIT)
无论出于何种原因,都绝不要调用 System.runFinalizersOnExit 或 Runtime.runFinalizersOnExit:它们是 Java 类库中最危险的方法之一。 —— Joshua Bloch
NP:equals() 方法未检查 null 参数(NP_EQUALS_SHOULD_HANDLE_NULL_ARGUMENT)
此 equals(Object) 实现违反了 java.lang.Object.equals() 所定义的约定,因为它没有检查传入的参数是否为 null。所有 equals() 方法在收到 null 值时都应返回 false。
FI:应删除空的终结器(FI_EMPTY)
空的 finalize() 方法毫无用处,因此应当删除。
FI:终结器覆盖了父类的终结器(FI_NULLIFY_SUPER)
此空的 finalize() 方法显式抵消了其父类所定义的任何终结器的作用。为父类定义的任何终结操作都不会被执行。除非这是有意为之,否则请删除该方法。
FI: 终结器除了调用父类终结器外什么也不做(FI_USELESS)
此 finalize() 方法所做的唯一一件事就是调用父类的 finalize() 方法,因此它是多余的。请删除它。
FI: 终结器未调用父类终结器(FI_MISSING_SUPER_CALL)
此 finalize() 方法没有调用其父类的 finalize() 方法。因此,为父类定义的所有终结器操作都不会被执行。请添加对 super.finalize() 的调用。
FI: 显式调用终结器(FI_EXPLICIT_INVOCATION)
此方法包含对某个对象上 finalize() 方法的显式调用。由于终结方法本应只执行一次,并且只能由 VM 执行,因此这样做是不妥的。
如果一组相互关联的对象变得可终结,那么 VM 将在所有可终结对象上调用 finalize 方法,并且可能在不同线程中同时进行。因此,在类 X 的 finalize 方法中对 X 所引用的对象调用 finalize 尤其不妥,因为这些对象可能已经在另一个线程中被终结了。
Eq: Equals 检查了与自身不兼容的操作数(EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS)
这个 equals 方法在检查参数是否属于某种不兼容的类型(即该类既不是定义此 equals 方法的类的父类型,也不是其子类型)。例如,Foo 类可能具有如下所示的 equals 方法:
public boolean equals(Object o) {
if (o instanceof Foo)
return name.equals(((Foo)o).name);
else if (o instanceof String)
return name.equals(o);
else return false;
}这种做法被认为是不良实践,因为它使得实现对称且传递的 equals 方法变得非常困难。缺少这些性质时,可能出现非常意外的行为。
Eq:equals 方法对子类失效(EQ_GETCLASS_AND_CLASS_CONSTANT)
本类的 equals 方法在被子类继承时会被破坏。它将一个类字面量与参数的类进行比较(例如,在类 Foo 中可能会检查 Foo.class == o.getClass())。更好的做法是检查 this.getClass() == o.getClass()。
Eq:定义了协变的 equals() 方法(EQ_SELF_NO_OBJECT)
本类定义了 equals() 的一个协变版本。要在 java.lang.Object 中正确重写 equals() 方法,equals() 的参数类型必须为 java.lang.Object。
Co:定义了协变的 compareTo() 方法(CO_SELF_NO_OBJECT)
本类定义了 compareTo() 的一个协变版本。要在 Comparable 接口中正确重写 compareTo() 方法,compareTo() 的参数类型必须为 java.lang.Object。
Co:compareTo()/compare() 返回 Integer.MIN_VALUE(CO_COMPARETO_RESULTS_MIN_VALUE)
在某些情况下,这个 compareTo 或 compare 方法会返回常量 Integer.MIN_VALUE,这是极其糟糕的做法。对于 compareTo 的返回值,唯一重要的是结果的符号。但人们有时会对 compareTo 的返回值取反,期望这样就能反转结果的符号。实际上确实可以,唯一的例外是返回值为 Integer.MIN_VALUE 的情况。因此请直接返回 -1,而不要返回 Integer.MIN_VALUE。
Co:compareTo()/compare() 对 float 或 double 值处理不正确(CO_COMPARETO_INCORRECT_FLOATING)
该方法使用类似 val1 > val2 ? 1 : val1 < val2 ? -1 : 0 的模式比较 double 或 float 值。这种模式对于 -0.0 和 NaN 值处理不正确,可能导致排序结果错误或集合损坏(如果被比较的值被用作键)。建议使用 Double.compare 或 Float.compare 静态方法,它们能正确处理所有特殊情况。
RV:对 compareTo()/compare() 的结果取反(RV_NEGATING_RESULT_OF_COMPARETO)
这段代码对 compareTo 或 compare 方法的返回值取反。这是一种值得怀疑或不良的编程实践,因为如果返回值是 Integer.MIN_VALUE,对返回值取反并不会反转结果的符号。可以通过交换操作数的顺序而不是对结果取反来达到相同的效果。
ES:使用 == 或 != 比较 String 对象(ES_COMPARING_STRINGS_WITH_EQ)
这段代码使用 == 或 != 运算符比较 java.lang.String 对象的引用是否相等。除非两个字符串都是源文件中的常量,或者已经通过 String.intern() 方法进行了驻留(intern),否则相同的字符串值可能由两个不同的 String 对象表示。建议改用 equals(Object) 方法。
ES:使用 == 或 != 比较 String 参数(ES_COMPARING_PARAMETER_STRING_WITH_EQ)
此代码使用 == 或 != 运算符对一个 java.lang.String 参数进行引用相等比较。要求调用方只能向方法传入 String 常量或驻留(interned)字符串是不必要的脆弱设计,且很少能带来可测量的性能提升。建议改用 equals(Object) 方法。
Eq:类定义了 compareTo(…) 并使用 Object.equals()(EQ_COMPARETO_USE_OBJECT_EQUALS)
该类定义了一个 compareTo(...) 方法,但其 equals() 方法继承自 java.lang.Object。通常,compareTo 的返回值应为零当且仅当 equals 返回 true。如果违反这一点,PriorityQueue 等类中就会出现奇怪且不可预知的故障。在 Java 5 中,PriorityQueue.remove 方法使用 compareTo 方法,而在 Java 6 中则使用 equals 方法。
以下是 Comparable 接口中 compareTo 方法的 JavaDoc:
强烈建议(但并非严格要求)
(x.compareTo(y)==0) == (x.equals(y))。一般而言,任何实现了 Comparable 接口但违反此条件的类,都应清楚地表明这一事实。推荐使用的表述是:“注意:此类具有与 equals 不一致的自然排序。”
HE:类定义了 hashCode() 并使用 Object.equals()(HE_HASHCODE_USE_OBJECT_EQUALS)
该类定义了一个 hashCode() 方法,但其 equals() 方法继承自 java.lang.Object(后者通过比较对象引用来定义相等性)。虽然这大概可以满足“相等的对象必须具有相等的哈希码”这一约定,但这很可能不是重写 hashCode() 方法的本意。(重写 hashCode() 意味着对象的标识基于比简单引用相等更复杂的判定标准。)
如果你认为该类的实例永远不会被放入 HashMap/HashTable 中,建议使用的 hashCode 实现如下:
public int hashCode() {
assert false : "hashCode not designed";
return 42; // any arbitrary constant will do
}HE:类定义了 hashCode() 但没有定义 equals()(HE_HASHCODE_NO_EQUALS)
此类定义了 hashCode() 方法,但没有定义 equals() 方法。因此,该类可能违反"相等的对象必须具有相等的哈希码"这一不变式。
HE:类定义了 equals() 并使用了 Object.hashCode()(HE_EQUALS_USE_HASHCODE)
此类重写了 equals(Object),但没有重写 hashCode(),并且从 java.lang.Object 继承了 hashCode() 的实现(该实现返回标识哈希码,即由虚拟机分配给对象的一个任意值)。因此,该类极有可能违反"相等的对象必须具有相等的哈希码"这一不变式。
如果你认为此类的实例永远不会被插入到 HashMap/HashTable 中,建议使用的 hashCode 实现方式如下:
public int hashCode() {
assert false : "hashCode not designed";
return 42; // any arbitrary constant will do
}HE:类继承了 equals() 而使用 Object.hashCode() (HE_INHERITS_EQUALS_USE_HASHCODE)
该类从一个抽象父类继承了 equals(Object),并从 java.lang.Object 继承了 hashCode()(它返回标识哈希码,即虚拟机为对象分配的一个任意值)。因此,该类极有可能违反“相等的对象必须具有相等的哈希码”这一不变式。
如果你不想定义 hashCode 方法,并且(或者)认为该对象永远不会被放入 HashMap/Hashtable 中,请定义 hashCode() 方法使其抛出 UnsupportedOperationException。
HE:类定义了 equals() 但未定义 hashCode() (HE_EQUALS_NO_HASHCODE)
该类重写了 equals(Object),但没有重写 hashCode()。因此,该类可能违反“相等的对象必须具有相等的哈希码”这一不变式。
Eq:抽象类定义了协变的 equals() 方法 (EQ_ABSTRACT_SELF)
该类定义了 equals() 的协变版本。为了正确地重写 java.lang.Object 中的 equals() 方法,equals() 的参数类型必须是 java.lang.Object。
Co:抽象类定义了协变的 compareTo() 方法 (CO_ABSTRACT_SELF)
该类定义了 compareTo() 的协变版本。为了正确地重写 Comparable 接口中的 compareTo() 方法,compareTo() 的参数类型必须是 java.lang.Object。
IC:父类在初始化期间使用了子类 (IC_SUPERCLASS_USES_SUBCLASS_DURING_INITIALIZATION)
在类的初始化过程中,该类主动使用了某个子类。而在发生这种使用时,该子类尚未完成初始化。例如,在以下代码中,foo 将为 null。
public class CircularClassInitialization {
static class InnerClassSingleton extends CircularClassInitialization {
static InnerClassSingleton singleton = new InnerClassSingleton();
}
static CircularClassInitialization foo = InnerClassSingleton.singleton;
}SI: 静态初始化器在所有静态 final 字段赋值前创建实例(SI_INSTANCE_BEFORE_FINALS_ASSIGNED)
该类的静态初始化器在所有静态 final 字段赋值之前就创建了该类的一个实例。
It: Iterator 的 next() 方法不能抛出 NoSuchElementException(IT_NO_SUCH_ELEMENT)
此类实现了 java.util.Iterator 接口。但是,它的 next() 方法无法抛出 java.util.NoSuchElementException。应当修改 next() 方法,使其在被调用且没有更多元素可返回时抛出 NoSuchElementException。
ME: 枚举字段是 public 且可变的(ME_MUTABLE_ENUM_FIELD)
公共枚举内部定义了可变的 public 字段,因此该字段可能被恶意代码修改,也可能被其他包中的代码意外修改。虽然可变的枚举字段可用于延迟初始化,但将它们暴露给外部世界是不好的做法。建议将该字段声明为 final 和/或包私有。
ME: 公共枚举方法无条件设置其字段(ME_ENUM_FIELD_SETTER)
公共枚举中声明的这个公共方法无条件地设置枚举字段,因此该字段可能被恶意代码修改,也可能被其他包中的代码意外修改。虽然可变的枚举字段可用于延迟初始化,但将它们暴露给外部世界是不好的做法。建议删除该方法或将其声明为包私有。
Nm: 方法名应以小写字母开头(NM_METHOD_NAMING_CONVENTION)
方法名应当是动词,采用混合大小写形式,首字母小写,每个内部单词的首字母大写。
Nm: 非 final 字段名应以小写字母开头,final 字段名应全部大写并以下划线分隔单词(NM_FIELD_NAMING_CONVENTION)
非 final 字段的名称应采用混合大小写形式,首字母小写,后续单词的首字母大写。final 字段的名称应全部大写,单词之间以下划线('_')分隔。
Nm: 类名不应遮蔽所实现接口的简单名称(NM_SAME_SIMPLE_NAME_AS_INTERFACE)
该类/接口的简单名称与其实现/继承的接口的简单名称相同,只是该接口位于不同的包中(例如 alpha.Foo extends beta.Foo)。这会极其令人困惑,会造成许多需要查看 import 语句才能解析引用的情况,并且会带来许多意外定义了并未重写其父类中方法的方法的机会。
Nm: 类名不应遮蔽父类的简单名称(NM_SAME_SIMPLE_NAME_AS_SUPERCLASS)
该类的简单名称与其父类的简单名称相同,只是其父类位于不同的包中(例如 alpha.Foo extends beta.Foo)。这会极其令人困惑,会造成许多需要查看 import 语句才能解析引用的情况,并且会带来许多意外定义了并未重写其父类中方法的方法的机会。
Nm: 类名应以大写字母开头(NM_CLASS_NAMING_CONVENTION)
类名应使用名词,采用混合大小写形式,其中每个内部单词的首字母大写。尽量使类名简洁且具有描述性。使用完整的单词——避免使用缩写和简称(除非该缩写比全称使用得广泛得多,例如 URL 或 HTML)。
Nm: 令人非常困惑的方法名(但可能是有意为之)(NM_VERY_CONFUSING_INTENTIONAL)
被引用的这些方法的方法名仅在大小写上有所不同。这非常令人困惑,因为如果大小写完全相同,其中一个方法就会覆盖另一个方法。从其他方法的存在来看,这两个方法同时存在似乎是刻意为之,但确实令人困惑。你应该尽力消除其中之一,除非由于 API 已冻结而必须同时保留两者。
Nm: 由于参数使用了错误的包,方法未能覆盖超类中的方法(NM_WRONG_PACKAGE_INTENTIONAL)
子类中的方法没有覆盖超类中的类似方法,因为某个参数的类型与超类中对应参数的类型不完全匹配。例如,如果你有:
import alpha.Foo;
public class A {
public int f(Foo x) { return 17; }
}
----
import beta.Foo;
public class B extends A {
public int f(Foo x) { return 42; }
public int f(alpha.Foo x) { return 27; }
}类 B 中定义的 f(Foo) 方法并未覆盖类 A 中定义的 f(Foo) 方法,因为两者的参数类型是来自不同包的 Foo 类型。
在这种情况下,子类确实定义了一个与父类中方法签名完全相同的方法,因此这一点理应被理解。然而,这类方法极其容易造成混淆。你应当认真考虑删除该签名相似但并不相同的方法,或为其加上弃用标记。
Nm:令人混淆的方法名(NM_CONFUSING)
被引用的方法名称仅在大小写上有所不同。
Nm:类并非派生自 Exception,尽管其命名如此(NM_CLASS_NOT_EXCEPTION)
该类并非派生自其他异常类,但名称以 'Exception' 结尾。这会给该类的使用者造成困惑。
RR:方法忽略 InputStream.read() 的返回结果(RR_NOT_CHECKED)
该方法忽略 java.io.InputStream.read() 某个变体的返回值,而该方法可能返回多个字节。如果不检查返回值,调用方将无法正确处理实际读取的字节数少于请求字节数的情况。这是一种尤为隐蔽的缺陷,因为在许多程序中,从输入流读取数据通常确实会读满请求的数据量,从而导致程序只是偶尔才会出错。
RR:方法忽略 InputStream.skip() 的返回结果(SR_NOT_CHECKED)
该方法忽略 java.io.InputStream.skip() 的返回值,而该方法可能跳过多个字节。如果不检查返回值,调用方将无法正确处理实际跳过的字节数少于请求字节数的情况。这是一种尤为隐蔽的缺陷,因为在许多程序中,对输入流执行跳过操作通常确实会跳过所请求的全部数据量,从而导致程序只是偶尔才会出错。不过,对于带缓冲的流而言,skip() 只会跳过缓冲区中的数据,因而在跳过所请求的字节数时会经常失败。
Se:类是 Serializable,但其父类未定义无参构造函数(SE_NO_SUITABLE_CONSTRUCTOR)
该类实现了 Serializable 接口,而其父类没有实现。当这样的对象被反序列化时,需要调用父类的无参构造函数来初始化父类的字段。由于父类没有无参构造函数,序列化和反序列化将在运行时失败。
Se:类是 Externalizable,但未定义无参构造函数(SE_NO_SUITABLE_CONSTRUCTOR_FOR_EXTERNALIZATION)
该类实现了 Externalizable 接口,但没有定义公共的无参构造函数。当 Externalizable 对象被反序列化时,首先需要通过调用公共的无参构造函数来构造它。由于该类没有这样的构造函数,序列化和反序列化将在运行时失败。
Se:Comparator 未实现 Serializable(SE_COMPARATOR_SHOULD_BE_SERIALIZABLE)
此类实现了 Comparator 接口。你需要考虑它是否也应该实现 Serializable 接口。如果用比较器来构造有序集合(例如 TreeMap),那么 TreeMap 只有在比较器也可序列化时才能被序列化。由于大多数比较器几乎没有状态,让它们可序列化通常是简单易行的,也是一种良好的防御性编程实践。
SnVI:类是可序列化的,但没有定义 serialVersionUID(SE_NO_SERIALVERSIONID)
此类实现了 Serializable 接口,但没有定义 serialVersionUID 字段。哪怕是像添加对 .class 对象的引用这样简单的改动,也会为该类增加合成字段,从而不幸地改变隐式的 serialVersionUID(例如,添加对 String.class 的引用会生成一个静态字段 class$java$lang$String)。此外,不同的源码到字节码编译器对于为类对象或内部类引用生成的合成变量,可能采用不同的命名约定。为确保 Serializable 在不同版本之间的互操作性,建议添加显式的 serialVersionUID。
Se:readResolve 方法的返回类型必须声明为 Object(SE_READ_RESOLVE_MUST_RETURN_OBJECT)
为了让序列化机制能够识别 readResolve 方法,其返回类型必须声明为 Object。
Se:反序列化时未被赋值的瞬态字段(SE_TRANSIENT_FIELD_NOT_RESTORED)
此类中包含一个在类的多处被更新的字段,因此它似乎是该类状态的一部分。然而,由于该字段被标记为 transient,且未在 readObject 或 readResolve 中赋值,在该类的任何反序列化实例中它都将保持默认值。
Se:防止可外部化对象被覆写(SE_PREVENT_EXT_OBJ_OVERWRITE)
readExternal() 方法必须声明为 public,且不受恶意调用者的防护,因此该代码允许任何调用者随时重置对象的值。
为防止可外部化对象被覆写,你可以使用一个布尔标志,在实例字段填充完毕之后将其置位。你还可以通过对一个私有锁对象进行同步来防范竞态条件。
Se:serialVersionUID 不是 final(SE_NONFINAL_SERIALVERSIONID)
此类定义了一个非 final 的 serialVersionUID 字段。如果打算用它来指定序列化用途的版本 UID,则应将该字段声明为 final。
Se:serialVersionUID 不是 static(SE_NONSTATIC_SERIALVERSIONID)
此类定义了一个非 static 的 serialVersionUID 字段。如果打算用它来指定序列化用途的版本 UID,则应将该字段声明为 static。
Se:serialVersionUID 不是 long(SE_NONLONG_SERIALVERSIONID)
此类定义了一个非 long 类型的 serialVersionUID 字段。如果打算用它来指定序列化用途的版本 UID,则应将该字段声明为 long 类型。
Se:可序列化类中的非瞬态、非可序列化实例字段(SE_BAD_FIELD)
此 Serializable 类定义了一个非原始类型的实例字段,该字段既不是 transient,也不实现 Serializable,也不实现 java.lang.Object,并且似乎没有实现 Externalizable 接口,也没有实现 readObject() 和 writeObject() 方法。如果该字段中存储了不可序列化的对象,则此类的对象将无法被正确反序列化。
Se: 可序列化的内部类(SE_INNER_CLASS)
此 Serializable 类是一个内部类。对其执行的任何序列化尝试都会同时序列化关联的外部实例。外部实例是可序列化的,因此不会失败,但这可能会序列化比预期多得多的数据。如果可能的话,将内部类改为静态内部类(也称为嵌套类)应当可以解决这个问题。
Se: 不可序列化的类包含可序列化的内部类(SE_BAD_FIELD_INNER_CLASS)
此 Serializable 类是某个不可序列化类的内部类。因此,尝试序列化它时也会尝试关联其所属的外部类的实例,从而导致运行时错误。
如果可能的话,将内部类改为静态内部类应当可以解决这个问题。使外部类可序列化也可能有效,但那意味着序列化内部类的实例时总会同时序列化外部类的实例,而这通常并不是你真正想要的。
Se: 不可序列化的值被存入可序列化类的实例字段(SE_BAD_FIELD_STORE)
一个不可序列化的值被存入了可序列化类的非 transient 字段中。
RV: 方法忽略了异常返回值(RV_RETURN_VALUE_IGNORED_BAD_PRACTICE)
此方法返回了一个未被检查的返回值。应当检查该返回值,因为它可能表示异常或意外的函数执行情况。例如,File.delete() 方法在文件无法成功删除时返回 false(而不是抛出异常)。如果你不检查结果,就无法察觉该方法是否通过返回一个非典型的返回值来表明出现了意外行为。
NP: toString 方法可能返回 null(NP_TOSTRING_COULD_RETURN_NULL)
此 toString 方法在某些情况下似乎会返回 null。对规范的宽松解读或许可以认为这是允许的,但这很可能是个坏主意,并可能导致其他代码出错。应返回空字符串或其他合适的字符串,而不是返回 null。
NP: Clone 方法可能返回 null(NP_CLONE_COULD_RETURN_NULL)
此 clone 方法在某些情况下似乎会返回 null,但 clone 从不允许返回 null 值。如果你确信该路径不可达,请改为抛出 AssertionError。
OS: 方法可能未能关闭流(OS_OPEN_STREAM)
该方法创建了一个 IO 流对象,但既没有将它赋给任何字段,也没有将其传递给可能关闭它的其他方法,也没有返回它,而且似乎没有在方法的所有退出路径上关闭该流。这可能导致文件描述符泄漏。通常,使用 finally 块来确保流被关闭是一个好主意。
OS: 方法在异常时可能未关闭流 (OS_OPEN_STREAM_EXCEPTION_PATH)
该方法创建了一个 IO 流对象,但没有将其赋值给任何字段、传递给其他方法或返回它,并且似乎没有在方法所有可能的异常路径上将其关闭。这可能导致文件描述符泄漏。通常建议使用 finally 代码块来确保流被关闭。
RC: 对常量的引用比较可疑 (RC_REF_COMPARISON_BAD_PRACTICE)
该方法使用 == 或 != 运算符将引用值与常量进行比较,而比较该类型实例的正确方式通常是使用 equals() 方法。可以创建相等但 == 比较结果为 false 的不同实例,因为它们是不同的对象。通常不应通过引用比较的类包括 java.lang.Integer、java.lang.Float 等。
RC: 对 Boolean 值的引用比较可疑 (RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN)
该方法使用 == 或 != 运算符比较两个 Boolean 值。通常只有两个 Boolean 值(Boolean.TRUE 和 Boolean.FALSE),但可以使用 new Boolean(b) 构造函数创建其他 Boolean 对象。最好避免创建此类对象,但如果确实存在,使用 == 或 != 检查 Boolean 对象的相等性会得到与使用 .equals(...) 不同的结果。
FS: 格式字符串应使用 %n 而非 \n (VA_FORMAT_STRING_USES_NEWLINE)
该格式字符串包含换行符 (\n)。在格式字符串中,通常更倾向于使用 %n,它会生成特定于平台的行分隔符。使用 Java 15 中引入的文本块时,请使用 \ 转义序列:String value = """ first line%n\ second line%n\ """;
BIT: 检查位运算的符号 (BIT_SIGNED_CHECK)
该方法比较了诸如 ((event.detail & SWT.SELECTED) > 0) 之类的表达式。使用位运算后与大于运算符进行比较可能导致意外结果(当然这取决于 SWT.SELECTED 的值)。如果 SWT.SELECTED 为负数,这很可能是一个 bug。即使 SWT.SELECTED 不为负数,使用 '!= 0' 代替 '> 0' 似乎也是良好的实践。
ODR: 方法可能未关闭数据库资源 (ODR_OPEN_DATABASE_RESOURCE)
该方法创建了数据库资源(如数据库连接或行集),但没有将其赋值给任何字段、传递给其他方法或返回它,并且似乎没有在方法所有路径上关闭该对象。未能在方法所有路径上关闭数据库资源可能导致性能不佳,并可能使应用程序在与数据库通信时出现问题。
ODR: 方法在异常时可能未关闭数据库资源 (ODR_OPEN_DATABASE_RESOURCE_EXCEPTION_PATH)
该方法创建了数据库资源(如数据库连接或行集),但未将其赋值给任何字段、未传递给其他方法,也未返回它,而且在方法的所有异常退出路径上似乎都没有关闭该对象。如果不能在方法的所有退出路径上关闭数据库资源,可能会导致性能下降,甚至引起应用程序与数据库通信出现问题。
ISC:仅提供静态方法的类被无谓地实例化(ISC_INSTANTIATE_STATIC_CLASS)
该类分配了一个基于仅提供静态方法的类的对象。这个对象并不需要被创建,只需以类名作为限定符直接访问静态方法即可。
DMI:随机对象被创建且仅使用一次(DMI_RANDOM_USED_ONLY_ONCE)
这段代码创建了一个 java.util.Random 对象,用它生成一个随机数,然后就丢弃了该 Random 对象。这样做产生的随机数质量一般,而且效率低下。如果可能,请重写代码,使 Random 对象只创建一次并保存下来,每次需要新的随机数时,调用现有 Random 对象上的方法来获取。
如果生成的随机数不可被猜测这一点很重要,那么你_一定_不要为每个随机数都创建一个新的 Random;这样的值太容易被猜到了。你应当认真考虑改用 java.security.SecureRandom(并且避免为每个所需的随机数都分配一个新的 SecureRandom)。
BC:Equals 方法不应对其参数的类型做任何假设(BC_EQUALS_METHOD_SHOULD_WORK_FOR_ALL_OBJECTS)
equals(Object o) 方法不应对 o 的类型做任何假设。如果 o 的类型与 this 不同,它应当简单地返回 false。
J2EE:将不可序列化的对象存入 HttpSession(J2EE_STORE_OF_NON_SERIALIZABLE_OBJECT_INTO_SESSION)
这段代码似乎正在将一个不可序列化的对象存入 HttpSession。如果该会话被钝化或迁移,将会导致错误。
GC:泛型调用中的未检查类型(GC_UNCHECKED_TYPE_IN_GENERIC_CALL)
这个对泛型集合方法的调用,在编译期类型为 Object 的情况下传递了参数,而期望的是泛型类型参数中的某个具体类型。因此,无论是标准的 Java 类型系统还是静态分析,都无法提供有用的信息来判断作为参数传递的对象类型是否合适。
PZ:不要在迭代器中重用条目对象(PZ_DONT_REUSE_ENTRY_OBJECTS_IN_ITERATORS)
entrySet() 方法允许返回底层 Map 的一个视图,其中的迭代器与 Map.Entry 共享同一对象。这个巧妙的想法在多个 Map 实现中被使用过,但也引入了严重的编码错误隐患。如果某个 map m 为 entrySet 返回这样的迭代器,那么 c.addAll(m.entrySet()) 就会出大问题。OpenJDK 7 中所有的 Map 实现都已被重写以避免这种情况,你也应当这样做。
DMI:添加条目集的元素可能因 Entry 对象被复用而失败(DMI_ENTRY_SETS_MAY_REUSE_ENTRY_OBJECTS)
entrySet() 方法允许返回底层 Map 的一个视图,在该视图中同一个 Entry 对象会被复用,并在迭代过程中被反复返回。从 Java 6 开始,IdentityHashMap 和 EnumMap 就是这样实现的。遍历这样的 Map 时,Entry 的值只在进入下一次迭代之前有效。例如,如果你试图把这样一个 entrySet 传给某个 addAll 方法,就会出现严重错误。
DMI:不要使用 removeAll 来清空集合(DMI_USING_REMOVEALL_TO_CLEAR_COLLECTION)
如果你想从集合 c 中移除所有元素,请使用 c.clear,而不是 c.removeAll(c)。调用 c.removeAll(c) 来清空集合不够清晰,容易因拼写错误而出错,效率更低,而且对于某些集合还可能抛出 ConcurrentModificationException。
THROWS:方法有意抛出 RuntimeException。(THROWS_METHOD_THROWS_RUNTIMEEXCEPTION)
该方法有意抛出 RuntimeException。
根据 SEI CERT ERR07-J 规则,抛出 RuntimeException 可能会导致错误,例如调用方无法检查该异常,从而无法从中正确恢复。
此外,抛出 RuntimeException 会迫使调用方捕获 RuntimeException,从而违反 SEI CERT ERR08-J 规则。
请注意,你可以继承 Exception 或 RuntimeException,并抛出该异常的新实例。
THROWS:方法在其 throws 子句中列出了 Exception。(THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION)
该方法在其 throws 子句中列出了 Exception。
声明方法时,throws 子句中的异常类型应当是最具体的。因此,在 throws 子句中使用 Exception 会迫使调用方要么在自己的 throws 子句中也使用它,要么把它放在 try-catch 块中(而此时它未必包含关于所抛异常的任何有意义的信息)。
更多信息请参见 SEI CERT ERR07-J 规则。
THROWS:方法在其 throws 子句中列出了 Throwable。(THROWS_METHOD_THROWS_CLAUSE_THROWABLE)
该方法在其 throws 子句中列出了 Throwable。
声明方法时,throws 子句中的异常类型应当是最具体的。因此,在 throws 子句中使用 Throwable 会迫使调用方要么在自己的 throws 子句中也使用它,要么把它放在 try-catch 块中(而此时它未必包含关于所抛异常的任何有意义的信息)。
此外,这样使用 Throwable 在语义上是一种不良做法,因为 Throwable 也包含 Error,而根据定义 Error 发生在不可恢复的场景中。
更多信息请参见 SEI CERT ERR07-J 规则。
PA:原始类型字段为 public(PA_PUBLIC_PRIMITIVE_ATTRIBUTE)
SEI CERT 规则 OBJ01-J 要求必须限制字段的可访问性。否则,字段的值可能在类外部被修改,从而产生意想不到或不希望出现的行为。一般来说,要求不允许任何字段为 public 是过度的,也是不现实的。该规则本身也提到 final 字段可以是 public 的。除了 final 字段之外,public 字段可能还有其他用途:某些 public 字段可以作为影响类行为的“标志(flag)”。这类标志字段预期由当前实例(对于静态字段则是当前类)读取,而由其他对象写入。如果一个字段既被当前实例的方法(对于静态字段则是当前类的方法)写入,又在类外部被写入,那么这段代码就值得怀疑。可以考虑将这些字段设为 private,并在必要时提供相应的 setter。请注意,构造器、初始化器和终结器是例外:如果在类内部只有它们写入该字段,则该字段不视为被类自身写入。
PA: 数组类型的字段为 public(PA_PUBLIC_ARRAY_ATTRIBUTE)
SEI CERT 规则 OBJ01-J 要求必须限制字段的可访问性。将数组类型的字段声明为 final 并不能阻止其他类修改数组的内容。不过,一般来说,要求不允许任何字段为 public 是过度的,也是不现实的。public 字段可能有其用途:某些 public 字段可以作为影响类行为的“标志(flag)”。这类标志字段预期由当前实例(对于静态字段则是当前类)读取,而由其他对象写入。如果一个字段既被当前实例的方法(对于静态字段则是当前类的方法)写入,又在类外部被写入,那么这段代码就值得怀疑。可以考虑将这些字段设为 private,并在必要时提供相应的 setter。请注意,构造器、初始化器和终结器是例外:如果在类内部只有它们写入该字段,则该字段不视为被类自身写入。
PA: 可变对象类型的字段为 public(PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE)
SEI CERT 规则 OBJ01-J 要求必须限制字段的可访问性。将一个可变对象类型的字段声明为 final 并不能阻止其他类修改该对象的内容。不过,一般来说,要求不允许任何 public 字段是过于严苛且不切实际的。public 字段可能存在合理的用途:某些 public 字段可以充当影响该类行为的“标志”。这类标志字段预期由当前实例(对于静态字段则是当前类)读取,而由其他方写入。如果某个字段既由当前实例的方法(对于静态字段则是当前类的方法)写入,又从外部写入,那么这段代码就值得怀疑。如有必要,请考虑将这些字段设为 private 并提供相应的 setter。请注意,构造器、初始化器和终结器是例外:如果在类内部只有这些成员写入该字段,则该字段不视为由该类自身写入。对于对象类型的字段,“写入”是指调用那些名称暗示修改操作的方法。
PI:不要将 JSL 中的 public 标识符重用作类名(PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_CLASS_NAMES)
避免将 Java 标准库中的 public 标识符重用作类名是一种良好的实践。这是因为 Java 标准库是 Java 平台的一部分,预期在所有 Java 环境中都可用。这样做会导致命名冲突和混淆,使得代码更难理解和维护。最佳实践是选择独特且具有描述性的类名,准确地表达你自己代码的目的和功能。举个例子,假设你想在应用中创建一个用于处理日期的类。与其使用 “Date” 这样的通用名称(它与已有的 java.util.Date 类冲突),不如选择更具体、更独特的名称,如 “AppDate” 或 “DisplayDate”。在选择名称作为标识符时,有几点需要注意:
- 使用有意义的前缀或命名空间:在类名前加上项目特定的前缀或命名空间,使其与众不同。例如,如果你的项目名为 “MyApp”,可以使用 “MyAppDate” 作为类名。
- 使用描述性名称:选择能够清楚表明其用途和功能的描述性类名。这有助于避免遮蔽(shadowing)已有的 Java 标准库标识符。例如,可以考虑使用 “CustomAppList” 而不是 “List”。
- 遵循命名约定:遵循 Java 的命名约定,例如类名使用驼峰式命名(如 MyClass)。这能提升代码可读性,并降低发生冲突的可能性。
参见 SEI CERT 规则 DCL01-J. 不要重用 Java 标准库中的 public 标识符。
PI:不要将 JSL 中的 public 标识符重用作字段名(PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_FIELD_NAMES)
避免在代码中将 Java 标准库的公共标识符复用为字段名,是一种良好的实践。这样做会导致混淆和潜在的冲突,使代码库更难以理解和维护。相反,建议为字段选择独特且具有描述性的名称,使其准确反映字段用途,并与标准库标识符区分开来。举个例子,假设你想为应用中处理日期创建一个类。不要使用 "Date" 这样的常见名称(它与现有的 java.util.Date 类冲突),而应选择更具体、更独特的名称,如 "AppDate" 或 "DisplayDate"。再比如,假设你正在创建一个表示汽车的类。不要将 "Component" 用作字段名(它与现有的 java.awt.Component 类冲突),而应选择更具体、更明确的名称,例如 "VehiclePart" 或 "CarComponent"。在选择标识符名称时,需要牢记以下几个要点:
- 使用描述性的名称:选择能清楚表明其用途和功能的描述性字段名。这有助于避免遮蔽现有的 Java 标准库标识符。例如,不要使用 "list",可以考虑使用 "myFancyList"
- 遵循命名约定:遵守 Java 的命名约定,例如字段名使用混合大小写。首字母小写,其后的每个单词首字母大写(例如 myFieldUsesMixedCase)。这可以提高代码的可读性,并减少冲突的可能性。
参见 SEI CERT 规则 DCL01-J. Do not reuse public identifiers from the Java Standard Library。
PI:不要将 JSL 的公共标识符复用为方法名(PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_METHOD_NAMES)
避免在代码中将 Java 标准库的公共标识符复用为方法名,是一种良好的实践。这样做会导致混淆、潜在的冲突以及意外的行为。为了保持代码清晰并确保功能正常,建议为方法选择独特且具有描述性的名称,使其准确反映方法用途,并与标准库标识符区分开来。举个例子,假设你想创建一个方法来处理应用中自定义文件的生成。不要使用 "File" 这样的常见名称作为方法名(它与现有的 java.io.File 类冲突),而应选择更具体、更独特的名称,例如 "generateFile" 或 "createOutPutFile"。在选择标识符名称时,需要牢记以下几个要点:
- 使用描述性名称:选择能够清晰表明其用途和功能的描述性方法名称。这有助于避免遮蔽现有的 Java 标准库标识符。例如,不使用 "abs()",可以考虑使用 "calculateAbsoluteValue()"。
- 遵循命名约定:遵守 Java 的命名约定,例如方法名使用混合大小写。方法名应为动词,首字母小写,内部每个单词的首字母大写(例如 runFast())。这样可以提升代码可读性,并降低发生冲突的可能性。
参见 SEI CERT 规则 DCL01-J. Do not reuse public identifiers from the Java Standard Library。
PI:不要将 JSL 中的公共标识符用作方法名(PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_LOCAL_VARIABLE_NAMES)
在 Java 中声明局部变量时,最好不要重复使用 Java 标准库中的公共标识符。将这些标识符用作局部变量名会导致混淆、妨碍代码理解,并可能与 Java 标准库中已有的公共标识符名称产生冲突。为了保持代码清晰并避免此类问题,最佳做法是为局部变量选择唯一且具有描述性的名称。举个例子,假设你想将一个自定义字体值存储在变量中。与其使用 "Font" 这样的通用名称作为变量名(它会与现有的 java.awt.Font 类冲突),不如选择更具体、更唯一的名称,例如 "customFont" 或 "loadedFontName"。在选择标识符名称时,有以下几点需要注意:
- 使用描述性名称:选择能够清晰表明其用途和功能的描述性变量名称。这有助于避免遮蔽现有的 Java 标准库标识符。例如,不使用 "variable",可以考虑使用 "myVariableName"。
- 遵循命名约定:遵守 Java 的命名约定,例如变量名使用混合大小写。首字母应小写,内部单词的首字母应大写(例如 myVariableName)。这样可以提升代码可读性,并降低发生冲突的可能性。
参见 SEI CERT 规则 DCL01-J. Do not reuse public identifiers from the Java Standard Library。
ENV:建议使用可移植的 Java 属性,而不是环境变量。(ENV_USE_PROPERTY_INSTEAD_OF_ENV)
环境变量并不具可移植性,变量名本身(而不仅是其值)可能因运行的操作系统而异。不仅特定环境变量的名称可能不同(例如 Windows 中的 `USERNAME` 与 Unix 系统中的 `USER`),甚至其语义也可能不同,例如大小写敏感性(Windows 不区分大小写,而 Unix 区分大小写)。此外,java.lang.System.getenv() 返回的环境变量 Map 及其集合视图可能不遵守 Object.equals(java.lang.Object) 和 Object.hashCode() 方法的一般契约。因此,使用环境变量可能会带来意想不到的副作用。另外,与 Java Properties 相比,环境变量的可见性限制更少:它们对定义进程的所有后代进程都是可见的,而不仅仅是紧邻的 Java 子进程。基于这些原因,java.lang.System 的 Java API 也建议在可能的情况下使用 Java 属性(java.lang.System.getProperty(java.lang.String))而非环境变量(java.lang.System.getenv(java.lang.String))。
如果某个值既可以通过 System.getProperty() 访问,也可以通过 System.getenv() 访问,则应使用前者访问。
对应的 Java 系统属性对照表:
| 环境变量 | 属性 |
|---|---|
| JAVA_HOME | java.home |
| JAVA_VERSION | java.version |
| TEMP | java.io.tmpdir |
| TMP | java.io.tmpdir |
| PROCESSOR_ARCHITECTURE | os.arch |
| OS | os.name |
| USER | user.name |
| USERNAME | user.name |
| HOME | user.home |
| HOMEPATH | user.home |
| CD | user.dir |
| PWD | user.dir |
参见 SEI CERT 规则 ENV02-J. 不要信任环境变量的值。
正确性(CORRECTNESS)
可能的缺陷——明显的编码错误,导致代码很可能并非开发者的本意。我们力求保持较低的误报率。
CN:父类方法被标注为 @OverridingMethodsMustInvokeSuper,但覆盖方法没有调用父类方法。(OVERRIDING_METHODS_MUST_INVOKE_SUPER)
父类方法被标注为 @OverridingMethodsMustInvokeSuper,但覆盖方法没有调用父类方法。
NP:返回类型为 Optional 的方法显式返回了 null(NP_OPTIONAL_RETURN_NULL)
使用 Optional 返回类型(java.util.Optional 或 com.google.common.base.Optional)始终意味着从设计上就不希望显式返回 null。在这种情况下返回 null 值违反了契约,很可能会破坏客户端代码。
NP:非空字段未初始化(NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR)
该字段被标记为非空,但构造方法并未对其赋值。该字段可能在构造过程中的其他位置被初始化,也可能总是在使用前被初始化。
VR:类引用了无法解析的类或方法(VR_UNRESOLVABLE_REFERENCE)
该类引用了一个类或方法,但在用于分析的库中无法解析该引用。
IL:明显的死循环(IL_INFINITE_LOOP)
此循环似乎没有任何终止方式(除非抛出异常)。
IO:向对象输出流追加内容的徒劳尝试(IO_APPENDING_TO_OBJECT_OUTPUT_STREAM)
这段代码以追加模式打开一个文件,然后按如下方式将其包装在对象输出流中:
OutputStream out = new FileOutputStream(anyFile, true);
new ObjectOutputStream(out);这将不允许你向文件中已存在的对象输出流追加内容。若希望向对象输出流追加数据,就必须让该对象输出流保持打开状态。
只有在读取该文件时打算以随机访问模式打开它,并定位到追加开始处的字节偏移量的情况下,以追加模式打开文件并写入对象输出流才可能奏效。
IL:看似无限的递归循环(IL_INFINITE_RECURSIVE_LOOP)
该方法无条件地调用自身。这似乎表明存在一个无限递归循环,将导致栈溢出。
IL:集合被添加到自身(IL_CONTAINER_ADDED_TO_ITSELF)
一个集合被添加到自身中。因此,计算该集合的 hashCode 时会抛出 StackOverflowException。
RpC:重复的条件测试(RpC_REPEATED_CONDITIONAL_TEST)
代码中有一个条件测试被执行了两次,且前后紧邻(例如 x == 0 || x == 0)。第二处出现的内容可能是想写成别的东西(例如 x == 0 || y == 0)。
FL:方法使用浮点精度进行数学运算(FL_MATH_USING_FLOAT_PRECISION)
该方法使用浮点精度执行数学运算。浮点精度非常不精确。例如,16777216.0f + 1.0f = 16777216.0f。建议改用 double 进行运算。
CAA:协变数组中存储了可能不兼容的元素(CAA_COVARIANT_ARRAY_ELEMENT_STORE)
值被存入数组,但该值的类型与数组类型不匹配。分析表明,实际数组类型比其变量或字段的声明类型更窄,而此次赋值不满足原有的数组类型。该赋值可能在运行时引发 ArrayStoreException。
Dm:对 EasyMock 方法的无效/空洞调用(DMI_VACUOUS_CALL_TO_EASYMOCK_METHOD)
该调用没有向 EasyMock 方法传递任何对象,因此这个调用不起任何作用。
Dm:试图修改 ScheduledThreadPoolExecutor 最大线程池大小的徒劳尝试(DMI_FUTILE_ATTEMPT_TO_CHANGE_MAXPOOL_SIZE_OF_SCHEDULED_THREAD_POOL_EXECUTOR)
(见 Javadoc)尽管 ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,但其中几个继承来的调优方法对它并无用处。特别是,由于它使用 corePoolSize 线程和无界队列,表现为一个固定大小的线程池,因此对 maximumPoolSize 的调整不会产生任何有效作用。
DMI:由无法精确表示的 double 构造的 BigDecimal(DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE)
此代码会从一个无法准确转换为十进制数字的 double 值创建 BigDecimal。例如,人们可能认为在 Java 中编写 new BigDecimal(0.1) 会创建一个恰好等于 0.1 的 BigDecimal(非标度值为 1,标度为 1),但实际上它等于 0.1000000000000000055511151231257827021181583404541015625。你可能想要使用 BigDecimal.valueOf(double d) 方法,该方法使用 double 的字符串表示来创建 BigDecimal(例如,BigDecimal.valueOf(0.1) 得到 0.1)。
Dm: 以零核心线程数创建 ScheduledThreadPoolExecutor(DMI_SCHEDULED_THREAD_POOL_EXECUTOR_WITH_ZERO_CORE_THREADS)
(Javadoc) 核心线程数为零的 ScheduledThreadPoolExecutor 永远不会执行任何任务;对最大线程池大小的修改会被忽略。
Dm: 无法使用反射检查不带运行时保留策略的注解是否存在(DMI_ANNOTATION_IS_NOT_VISIBLE_TO_REFLECTION)
除非注解本身带有 @Retention(RetentionPolicy.RUNTIME) 注解,否则无法通过反射观察到该注解(例如,使用 isAnnotationPresent 方法)。
NP: 方法未检查参数是否为 null(NP_ARGUMENT_MIGHT_BE_NULL)
该方法的某个参数被识别为应当始终检查其是否为 null 的值,但它在没有进行 null 检查的情况下就被解引用了。
RV: 错误地计算带符号随机整数的绝对值(RV_ABSOLUTE_VALUE_OF_RANDOM_INT)
此代码生成一个带符号的随机整数,然后计算该随机整数的绝对值。如果随机数生成器返回的数字是 Integer.MIN_VALUE,那么结果同样为负数(因为 Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE)。(long 类型的值也会出现同样的问题)。
RV: 错误地计算带符号 32 位 hashcode 的绝对值(RV_ABSOLUTE_VALUE_OF_HASHCODE)
此代码生成一个 hashcode,然后计算该 hashcode 的绝对值。如果 hashcode 为 Integer.MIN_VALUE,那么结果同样为负数(因为 Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE)。
在 2^32 个字符串中,有一个字符串的 hashCode 为 Integer.MIN_VALUE,包括 "polygenelubricants"、"GydZG_" 和 "DESIGNING WORKHOUSES"。
RV: 0 到 1 的随机值被强制转换为整数 0(RV_01_TO_INT)
0 到 1 之间的随机值被强制转换为整数值 0。你可能需要先将随机值乘以其他数,然后再将其强制转换为整数,或者使用 Random.nextInt(n) 方法。
Dm: Math.max 与 Math.min 的错误组合(DM_INVALID_MIN_MAX)
此代码试图使用类似 Math.min(0, Math.max(100, value)) 的结构来限制值的范围。然而常量的顺序是错误的:应该是 Math.min(100, Math.max(0, value))。因此,此代码始终产生相同的结果(如果 value 为 NaN,则产生 NaN)。
Eq: equals 方法比较的是类名而不是类对象(EQ_COMPARING_CLASS_NAMES)
此类定义了一个 equals 方法,它通过比较两个对象的类名是否相同来判断两个对象是否属于同一个类。如果由不同的类加载器加载,不同名的类也可能具有相同的名称。直接检查类对象本身是否相同即可。
Eq: equals 方法始终返回 true(EQ_ALWAYS_TRUE)
此类定义了一个始终返回 true 的 equals 方法。这种写法颇有创意,但并不明智。此外,它还意味着 equals 方法不满足对称性。
Eq: equals 方法始终返回 false(EQ_ALWAYS_FALSE)
此类定义了一个始终返回 false 的 equals 方法。这意味着对象与其自身不相等,因而无法为该类创建有用的 Map 或 Set。更根本地说,这表示 equals 不满足自反性,而自反性是 equals 方法的要求之一。
其原本期望的语义很可能是对象标识:即对象与自身相等。这也是从类 Object 继承而来的行为。如果你需要覆盖从其他超类继承的 equals 方法,可以使用:
public boolean equals(Object o) {
return this == o;
}Eq: equals 方法覆盖了父类中的 equals,可能不具备对称性 (EQ_OVERRIDING_EQUALS_NOT_SYMMETRIC)
该类定义了一个 equals 方法,覆盖了父类中的一个 equals 方法。这两个 equals 方法都使用 instanceof 来判定两个对象是否相等。这样做隐患重重,因为 equals 方法必须具备对称性(换句话说,即 a.equals(b) == b.equals(a))。如果 B 是 A 的子类型,而 A 的 equals 方法检查参数是否为 instanceof A,B 的 equals 方法检查参数是否为 instanceof B,那么由这些方法定义的等价关系很可能不具备对称性。
Eq: 为枚举定义了协变的 equals() 方法 (EQ_DONT_DEFINE_EQUALS_FOR_ENUM)
该类定义了一个枚举,而枚举的相等性是通过对象标识来定义的。为枚举值定义协变的 equals 方法是极其糟糕的做法,因为这很可能导致两个不同的枚举值在使用协变枚举方法比较时相等,而在正常比较时不相等。不要这样做。
Eq: 定义了协变的 equals() 方法,但继承了 Object.equals(Object) (EQ_SELF_USE_OBJECT)
该类定义了一个 equals() 方法的协变版本,但继承了基类 java.lang.Object 中定义的常规 equals(Object) 方法。该类很可能应该定义一个 boolean equals(Object) 方法。
Eq: 定义的 equals() 方法没有覆盖 Object.equals(Object) (EQ_OTHER_USE_OBJECT)
该类定义了一个 equals() 方法,但它并没有覆盖基类 java.lang.Object 中定义的常规 equals(Object) 方法。该类很可能应该定义一个 boolean equals(Object) 方法。
Eq: 定义的 equals() 方法没有覆盖 equals(Object) (EQ_OTHER_NO_OBJECT)
该类定义了一个 equals() 方法,但它并没有覆盖基类 java.lang.Object 中定义的常规 equals(Object) 方法,而是从某个父类继承了一个 equals(Object) 方法。该类很可能应该定义一个 boolean equals(Object) 方法。
HE: 签名声明在哈希结构中使用了不可哈希的类 (HE_SIGNATURE_DECLARES_HASHING_OF_UNHASHABLE_CLASS)
某个方法、字段或类声明的泛型签名中,在要求使用可哈希类的上下文中使用了不可哈希的类。一个声明了 equals 方法却从 Object 继承 hashCode() 方法的类是不可哈希的,因为它不满足相等的对象必须具有相等的 hashCode 这一要求。
HE: 在哈希数据结构中使用了没有 hashCode() 方法的类 (HE_USE_OF_UNHASHABLE_CLASS)
某个类定义了 equals(Object) 方法但没有定义 hashCode() 方法,因此不满足相等的对象必须具有相等的 hashCode 这一要求。该类的实例被用于哈希数据结构中,因此修复此问题的紧迫性极高。
UR: 在构造方法中读取了未初始化的字段 (UR_UNINIT_READ)
此构造函数读取了一个尚未赋值的字段。这通常是因为程序员误将该字段当作构造函数的参数之一来使用。
UR:从超类构造函数调用的字段读取未初始化(UR_UNINIT_READ_CALLED_FROM_SUPER_CONSTRUCTOR)
此方法在超类的构造函数中被调用。此时,该类的字段尚未初始化。
为了更具体地说明,考虑以下这些类:
abstract class A {
int hashCode;
abstract Object getValue();
A() {
hashCode = getValue().hashCode();
}
}
class B extends A {
Object value;
B(Object v) {
this.value = v;
}
Object getValue() {
return value;
}
}当 B 被构造时,A 类的构造函数会在 B 的构造函数设置 value 之前 被调用。因此,当 A 的构造函数调用 getValue 时,读取到的 value 是一个未初始化的值。
Nm: 令人困惑的方法名(NM_VERY_CONFUSING)
被引用的方法名称仅在大小写上有所不同。这非常容易造成困惑,因为如果大小写相同,其中一个方法就会覆盖另一个。
Nm: 由于参数的包错误,方法未覆盖超类中的方法(NM_WRONG_PACKAGE)
子类中的方法没有覆盖超类中的类似方法,因为某个参数的类型与超类中对应参数的类型不完全匹配。例如,如果你有:
import alpha.Foo;
public class A {
public int f(Foo x) { return 17; }
}
----
import beta.Foo;
public class B extends A {
public int f(Foo x) { return 42; }
}在类 B 中定义的 f(Foo) 方法并未覆盖在类 A 中定义的 f(Foo) 方法,因为两者的参数类型是来自不同包的 Foo。
Nm:方法/构造方法混淆(NM_METHOD_CONSTRUCTOR_CONFUSION)
这个普通方法的名称与其所在的类相同。它很可能本应是一个构造方法。如果它本应是构造方法,请删除 void 返回值的声明。如果你是意外定义了这个方法,后来意识到了错误,定义了正确的构造方法,但出于向后兼容的原因无法删除该方法,请将该方法标记为过时(deprecated)。
Nm:类中定义了 hashcode();它应该是 hashCode() 吗?(NM_LCASE_HASHCODE)
该类定义了一个名为 hashcode() 的方法。此方法并未覆盖 java.lang.Object 中的 hashCode() 方法,而这很可能才是原本的意图。
Nm:类中定义了 tostring();它应该是 toString() 吗?(NM_LCASE_TOSTRING)
该类定义了一个名为 tostring() 的方法。此方法并未覆盖 java.lang.Object 中的 toString() 方法,而这很可能才是原本的意图。
Nm:类中定义了 equal(Object);它应该是 equals(Object) 吗?(NM_BAD_EQUAL)
该类定义了一个方法 equal(Object)。此方法并未覆盖 java.lang.Object 中的 equals(Object) 方法,而这很可能才是原本的意图。
Se:readResolve 方法不得声明为静态方法。(SE_READ_RESOLVE_IS_STATIC)
为了让序列化机制能够识别 readResolve 方法,它不得声明为静态方法。
Se:方法必须是私有的才能使序列化正常工作(SE_METHOD_MUST_BE_PRIVATE)
该类实现了 Serializable 接口,并定义了一个用于自定义序列化/反序列化的方法。但由于该方法没有声明为 private,它将被序列化/反序列化 API 静默忽略。
SF:由于 switch 语句贯穿(fall through)导致的死存储(SF_DEAD_STORE_DUE_TO_SWITCH_FALLTHROUGH)
由于 switch 的贯穿,在前一个 case 中存储的值在此处被覆盖。很可能是你忘记在前一个 case 的末尾加上 break 或 return。
SF:由于 switch 语句贯穿到 throw 导致的死存储(SF_DEAD_STORE_DUE_TO_SWITCH_FALLTHROUGH_TO_THROW)
由于 switch 贯穿到抛出异常的位置,在前一个 case 中存储的值在此处被忽略。很可能是你忘记在前一个 case 的末尾加上 break 或 return。
NP:读取未写入的字段(NP_UNWRITTEN_FIELD)
程序正在解引用一个似乎从未被写入过非 null 值的字段。除非该字段是通过分析未检测到的某种机制初始化的,否则解引用该值将产生空指针异常。
UwF:字段仅被赋值为 null(UWF_NULL_FIELD)
对该字段的所有赋值都是常量值 null,因此读取该字段始终返回 null。请检查是否存在错误,或者在它无用时将其删除。
UwF:未写入的字段(UWF_UNWRITTEN_FIELD)
该字段从未被写入。所有读取操作返回的都是默认值。请检查是否存在错误(它本应被初始化吗?),或者如果它毫无用处就将其删除。
SIC:非静态内部类与线程局部变量的死锁(SIC_THREADLOCAL_DEADLY_EMBRACE)
这个类是一个内部类,但它可能应该是静态内部类。目前的写法下,内部类与外部类中的线程局部变量之间存在发生严重死锁的危险。由于该内部类不是静态的,它会保留对外部类的引用。如果线程局部变量中保存了该内部类实例的引用,那么内部实例和外部实例都会保持可达,从而无法被垃圾回收。
RANGE:数组下标越界(RANGE_ARRAY_INDEX)
执行了数组操作,但数组下标越界,这将在运行时导致 ArrayIndexOutOfBoundsException。
RANGE:数组偏移量越界(RANGE_ARRAY_OFFSET)
调用的方法带有数组参数和偏移量参数,但偏移量越界。这将在运行时导致 IndexOutOfBoundsException。
RANGE:数组长度越界(RANGE_ARRAY_LENGTH)
调用的方法带有数组参数和长度参数,但长度越界。这将在运行时导致 IndexOutOfBoundsException。
RANGE:字符串下标越界(RANGE_STRING_INDEX)
调用了字符串方法,但指定的字符串下标越界。这将在运行时导致 StringIndexOutOfBoundsException。
RV:方法忽略返回值(RV_RETURN_VALUE_IGNORED)
该方法的返回值应当被检查。导致此警告的一个常见原因是对不可变对象调用了方法,却误以为该方法会更新对象。例如,在下面的代码片段中,
String dateString = getHeaderField(name);
dateString.trim();程序员似乎认为 trim() 方法会更新 dateString 所引用的 String。但由于 String 是不可变的,trim() 函数会返回一个新的 String 值,而这个值在这里被忽略了。代码应更正为:
String dateString = getHeaderField(name);
dateString = dateString.trim();RV:创建了异常但未抛出(RV_EXCEPTION_NOT_THROWN)
这段代码创建了一个异常(或错误)对象,却没有对它做任何处理。例如,类似这样的代码:
if (x < 0) {
new IllegalArgumentException("x must be nonnegative");
}程序员的本意很可能是抛出这个已创建的异常:
if (x < 0) {
throw new IllegalArgumentException("x must be nonnegative");
}RV:代码检查 compareTo 返回的特定值 (RV_CHECK_COMPARETO_FOR_SPECIFIC_RETURN_VALUE)
此代码调用了 compareTo 或 compare 方法,并检查返回值是否为某个特定值,例如 1 或 -1。调用这些方法时,应只检查结果的符号,而不是检查某个特定的非零值。虽然许多甚至大多数 compareTo 和 compare 方法只返回 -1、0 或 1,但也有一些会返回其他值。
NP:空指针解引用 (NP_ALWAYS_NULL)
此处解引用了一个空指针。当代码执行时,这将导致 NullPointerException。
NP:对始终为 null 的值调用 close() (NP_CLOSING_NULL)
正在对一个始终为 null 的值调用 close()。如果该语句被执行,将发生空指针异常。但这里最大的风险在于:你从未关闭本应关闭的资源。
NP:将 null 值存入标注为 @Nonnull 的字段 (NP_STORE_INTO_NONNULL_FIELD)
一个可能为 null 的值被存入了一个被标注为 @Nonnull 的字段中。
NP:方法中异常路径上的空指针解引用 (NP_ALWAYS_NULL_EXCEPTION)
此处解引用了一个在异常路径上为 null 的指针。当代码执行时,这将导致 NullPointerException。请注意,由于 SpotBugs 目前不会剪除不可行的异常路径,因此这可能是一条误报。
另请注意,SpotBugs 将 switch 语句的 default 分支视为异常路径,因为 default 分支通常是不可行的。
NP:可能的空指针解引用 (NP_NULL_ON_SOME_PATH)
存在一个语句分支,一旦执行, 就保证会解引用一个 null 值,当代码执行时这将产生 NullPointerException。当然,问题可能在于该分支或语句是不可行的,空指针异常永远不会被执行;判断这一点超出了 SpotBugs 的能力。
NP:方法中异常路径上可能的空指针解引用 (NP_NULL_ON_SOME_PATH_EXCEPTION)
此处解引用了一个在某条异常控制路径上为 null 的引用值。当代码执行时,这可能导致 NullPointerException。请注意,由于 SpotBugs 目前不会剪除不可行的异常路径,因此这可能是一条误报。
另请注意,SpotBugs 将 switch 语句的 default 分支视为异常路径,因为 default 分支通常是不可行的。
NP:方法调用为非空参数传递了 null (NP_NULL_PARAM_DEREF)
此方法调用为一个非空方法参数传递了 null 值。该参数要么被标注为应始终非空的参数,要么分析已表明它将始终被解引用。
NP:非虚方法调用为非空参数传递了 null (NP_NULL_PARAM_DEREF_NONVIRTUAL)
一个可能为 null 的值被传递给了一个非空方法参数。该参数要么被标注为应始终非空的参数,要么分析已表明它将始终被解引用。
NP: 方法调用将 null 传给非空参数(NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS)
在调用点处传入了一个可能为 null 的值,而所有已知的目标方法都要求该参数非空。要么该参数被标注为必须始终非空的参数,要么分析表明该参数将始终被解引用。
NP: 方法调用将 null 传给非空参数(NP_NONNULL_PARAM_VIOLATION)
此方法将一个 null 值作为参数传递给一个必须非空的方法。要么该参数已被显式标记为 @Nonnull,要么分析确定该参数始终会被解引用。
NP: 方法可能返回 null,但声明为 @Nonnull(NP_NONNULL_RETURN_VIOLATION)
此方法可能返回一个 null 值,但该方法(或其覆盖的父类方法)被声明为返回 @Nonnull。
NP: null 值保证会被解引用(NP_GUARANTEED_DEREF)
存在一条语句或分支,如果执行到它,则保证在该点处某个值为 null,并且该值保证会被解引用(涉及运行时异常的前向路径除外)。
请注意,诸如 if (x == null) throw new NullPointerException(); 这样的检查会被视为对 x 的解引用。
NP: 值为 null 且在异常路径上保证会被解引用(NP_GUARANTEED_DEREF_ON_EXCEPTION_PATH)
在异常路径上存在一条语句或分支,如果执行到它,则保证在该点处某个值为 null,并且该值保证会被解引用(涉及运行时异常的前向路径除外)。
DMI: 方法参数顺序颠倒(DMI_ARGUMENTS_WRONG_ORDER)
此方法调用的参数顺序似乎有误。例如,调用 Preconditions.checkNotNull("message", message) 的参数顺序被颠倒了:要检查的值应当是第一个参数。
RCN: 对先前已解引用的值进行空值检查(RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE)
这里对一个值进行检查以判断其是否为 null,但该值不可能为 null,因为它先前已被解引用,如果它为 null,那么在先前的解引用处就会抛出空指针异常。本质上,这段代码与先前的解引用对于该值是否允许为 null 的判断不一致。要么这个检查是多余的,要么先前的解引用是错误的。
RC: 可疑的引用比较(RC_REF_COMPARISON)
此方法使用 == 或 != 运算符比较两个引用值,而对于该类型的实例,正确的比较方式通常应使用 equals() 方法。可以构造出彼此相等但用 == 比较不相等的不同实例,因为它们是不同的对象。一般不应通过引用比较的类的示例包括 java.lang.Integer、java.lang.Float 等。RC_REF_COMPARISON 仅覆盖基本类型的包装类型。可通过添加逗号分隔的类名的 frc.suspicious 系统属性来扩展可疑类型列表:
<systemPropertyVariables>
<frc.suspicious>java.time.LocalDate,java.util.List</frc.suspicious>
</systemPropertyVariables>VA: 将原始数组传递给期望可变数量对象参数的函数 (VA_PRIMITIVE_ARRAY_PASSED_TO_OBJECT_VARARG)
此代码将一个原始数组传递给一个接受可变数量对象参数的函数。它会创建一个长度为一的数组来容纳该原始数组,并将其传递给该函数。
EC: 使用 equals() 比较数组与非数组 (EC_ARRAY_AND_NONARRAY)
此方法调用 .equals(Object o) 来比较一个数组和一个看起来不是数组的引用。如果被比较的对象类型不同,它们必然不相等,而这种比较几乎可以肯定是错误的。即使两者都是数组,数组上的 equals() 方法也只能判断两个数组是否为同一个对象。要比较数组的内容,请使用 java.util.Arrays.equals(Object[], Object[])。
EC: 调用 equals(null) (EC_NULL_ARG)
此方法调用 equals(Object),并将 null 值作为参数传入。根据 equals() 方法的契约,该调用应始终返回 false。
EC: 调用 equals() 比较无关的类和接口 (EC_UNRELATED_CLASS_AND_INTERFACE)
此方法在两个引用上调用 equals(Object),其中一个为类,另一个为接口,而该类及其所有非抽象子类均未实现该接口。因此,被比较的对象在运行时不太可能属于同一个类(除非某些应用类未被分析,或者运行时可能发生动态类加载)。根据 equals() 的契约,不同类的对象应始终比较为不相等;因此,根据 java.lang.Object.equals(Object) 所定义的契约,此比较的结果在运行时将始终为 false。
SA: 对局部变量进行自赋值而非给字段赋值 (SA_LOCAL_SELF_ASSIGNMENT_INSTEAD_OF_FIELD)
此方法包含对局部变量的自赋值,且存在一个同名字段,例如:
int foo;
public void setFoo(int foo) {
foo = foo;
}该赋值没有任何作用。您是不是想为字段赋值?
INT:int 值与 long 常量的错误比较(INT_BAD_COMPARISON_WITH_INT_VALUE)
此代码将一个 int 值与一个超出了 int 可表示范围的 long 常量进行比较。该比较恒为假,且可能不正确。
INT:有符号字节的错误比较(INT_BAD_COMPARISON_WITH_SIGNED_BYTE)
有符号字节的取值范围只能是 -128 到 127。将有符号字节与该范围之外的值进行比较恒为假,且很可能不正确。若要将有符号字节 b 转换为范围在 0..255 之间的无符号值,请使用 0xff & b。
INT:非负值与负常量或零的错误比较(INT_BAD_COMPARISON_WITH_NONNEGATIVE_VALUE)
此代码将一个保证非负的值与一个负常量或零进行比较。
BIT:有符号字节值的按位加法(BIT_ADD_OF_SIGNED_BYTE)
将一个字节值与一个已知低 8 位全为 0 的值相加。从字节数组中加载的值在进行任何按位运算之前会先符号扩展为 32 位。因此,如果 b[0] 中包含的值为 0xff,而 x 初始为 0,那么代码 ((x << 8) + b[0]) 会将 0xff 符号扩展为 0xffffffff,从而得到结果 0xffffffff。
特别地,下面这段将字节数组打包为 int 的代码是严重错误的:
int result = 0;
for (int i = 0; i < 4; i++)
result = ((result << 8) + b[i]);以下惯用写法可以替代:
int result = 0;
for (int i = 0; i < 4; i++)
result = ((result << 8) + (b[i] & 0xff));BIT:对带符号字节值进行按位或(BIT_IOR_OF_SIGNED_BYTE)
加载一个字节值(例如,从字节数组中加载的值,或由返回类型为 byte 的方法返回的值),并对该值执行按位或运算。在对该值执行任何按位运算之前,字节值会被符号扩展到 32 位。因此,如果 b[0] 中包含的值为 0xff,且 x 初始为 0,那么代码 ((x << 8) | b[0]) 将把 0xff 符号扩展为 0xffffffff,从而得到结果值 0xffffffff。
特别地,下面这段将字节数组打包进 int 的代码是严重错误的:
int result = 0;
for (int i = 0; i < 4; i++) {
result = ((result << 8) | b[i]);
}以下惯用写法可以替代:
int result = 0;
for (int i = 0; i < 4; i++) {
result = ((result << 8) | (b[i] & 0xff));
}BIT:检查涉及负数的位运算符号(BIT_SIGNED_CHECK_HIGH_BIT)
该方法比较一个位运算表达式,例如 ((val & CONSTANT) > 0),其中 CONSTANT 是一个负数。先进行位运算,再用大于运算符进行比较,可能导致意想不到的结果。这种比较不太可能按预期工作。良好的做法是使用 '!= 0' 而不是 '> 0'。
BIT:不兼容的位掩码(BIT_AND)
该方法比较形式为 (e & C) 的表达式与 D,由于常量 C 和 D 的具体取值,二者总是不相等。这可能表示存在逻辑错误或笔误。
BIT:检查 ((…) & 0) == 0(BIT_AND_ZZ)
该方法比较形式为 (e & 0) 的表达式与 0,二者总是相等。这可能表示存在逻辑错误或笔误。
BIT:不兼容的位掩码(BIT_IOR)
该方法比较形式为 (e | C) 的表达式与 D。由于常量 C 和 D 的具体取值,二者总是不相等。这可能表示存在逻辑错误或笔误。
通常,该缺陷的产生是因为代码本想在位集合中进行成员判断,却使用了按位或运算符("|")而不是按位与运算符("&")。
此外,此类缺陷也可能出现在诸如 (e & A | B) == C 的表达式中,该表达式被解析为 ((e & A) | B) == C,而原本的意图是 (e & (A | B)) == C。
SA:字段的自赋值(SA_FIELD_SELF_ASSIGNMENT)
该方法包含对字段的自赋值;例如
int x;
public void foo() {
x = x;
}这样的赋值毫无用处,并且可能表明存在逻辑错误或拼写错误。
SA:涉及字段的无意义自身运算(如 x & x)(SA_FIELD_SELF_COMPUTATION)
此方法用一个字段与其自身的另一个引用进行无意义的运算(例如 x&x 或 x-x)。由于该运算的性质,这一操作似乎毫无意义,可能表明存在拼写错误或逻辑错误。请仔细核对该运算。
SA:涉及变量的无意义自身运算(如 x & x)(SA_LOCAL_SELF_COMPUTATION)
此方法用一个局部变量与其自身的另一个引用进行无意义的运算(例如 x&x 或 x-x)。由于该运算的性质,这一操作似乎毫无意义,可能表明存在拼写错误或逻辑错误。请仔细核对该运算。
SA:字段与自身的比较(SA_FIELD_SELF_COMPARISON)
此方法将一个字段与自身进行比较,可能表明存在拼写错误或逻辑错误。请确保比较的对象是正确的。
SA:值与自身的比较(SA_LOCAL_SELF_COMPARISON)
此方法将一个局部变量与自身进行比较,可能表明存在拼写错误或逻辑错误。请确保比较的对象是正确的。
UMAC:匿名类中定义的不可调用方法(UMAC_UNCALLABLE_METHOD_OF_ANONYMOUS_CLASS)
此匿名类定义了一个未被直接调用、且并未重写父类中任何方法的方法。由于其他类中的方法无法直接调用匿名类中声明的方法,因此该方法似乎无法被调用。这个方法可能只是死代码,但也有可能本意是想重写父类中声明的方法,只是由于拼写错误或其他错误,实际上并未重写它本应重写的方法。
IJU:run 方法中的 JUnit 断言不会被 JUnit 识别(IJU_ASSERT_METHOD_INVOKED_FROM_RUN_METHOD)
在一个 run 方法中执行了 JUnit 断言。JUnit 断言失败只会抛出异常。因此,如果该异常发生在调用测试方法的线程之外的线程中,异常只会终止该线程,而不会导致测试失败。
IJU:TestCase 声明了不规范的 suite 方法(IJU_BAD_SUITE_METHOD)
该类是一个 JUnit TestCase,并定义了 suite() 方法。然而,suite 方法需要声明为以下形式之一:
public static junit.framework.Test suite()或
public static junit.framework.TestSuite suite()IJU:TestCase 定义了未调用 super.setUp() 的 setUp 方法(IJU_SETUP_NO_SUPER)
类是一个 JUnit TestCase 并实现了 setUp 方法。setUp 方法应当调用 super.setUp(),但没有调用。
IJU:TestCase 定义了未调用 super.tearDown() 的 tearDown 方法(IJU_TEARDOWN_NO_SUPER)
类是一个 JUnit TestCase 并实现了 tearDown 方法。tearDown 方法应当调用 super.tearDown(),但没有调用。
IJU:TestCase 实现了非静态的 suite 方法(IJU_SUITE_NOT_STATIC)
类是一个 JUnit TestCase 并实现了 suite() 方法。suite 方法应当声明为 static,但没有这样做。
IJU:TestCase 没有任何测试(IJU_NO_TESTS)
类是一个 JUnit TestCase,但没有实现任何测试方法。
BOA:类错误地覆盖了父类 Adapter 中实现的方法(BOA_BADLY_OVERRIDDEN_ADAPTER)
该方法覆盖了父类中的一个方法,而该父类是一个 Adapter,其实现了在 java.awt.event 或 javax.swing.event 包中定义的监听器。因此,当事件发生时,这个方法不会被调用。
SQL:方法尝试访问索引为 0 的结果集字段(SQL_BAD_RESULTSET_ACCESS)
对结果集的 getXXX 或 updateXXX 方法的调用中,字段索引为 0。由于 ResultSet 的字段索引从 1 开始,这总是一个错误。
SQL:方法尝试访问索引为 0 的预编译语句参数(SQL_BAD_PREPARED_STATEMENT_ACCESS)
对预编译语句的 setXXX 方法的调用中,参数索引为 0。由于参数索引从 1 开始,这总是一个错误。
SIO:使用 instanceof 运算符进行的多余类型检查(SIO_SUPERFLUOUS_INSTANCEOF)
使用 instanceof 运算符进行类型检查,而实际上可以静态确定对象是否属于所请求的类型。
BAC:不良的 Applet 构造函数依赖于未初始化的 AppletStub(BAC_BAD_APPLET_CONSTRUCTOR)
该构造函数调用了父类 Applet 中依赖 AppletStub 的方法。由于 AppletStub 直到该 applet 的 init() 方法被调用时才会初始化,因此这些方法将无法正确执行。
EC:使用 equals(…) 比较不兼容的数组(EC_INCOMPATIBLE_ARRAY_COMPARE)
该方法调用 .equals(Object o) 来比较两个数组,但这两个数组的类型不兼容(例如 String[] 和 StringBuffer[],或者 String[] 和 int[])。它们永远不会相等。此外,当使用 equals(...) 比较数组时,它只会检查是否为同一个数组,而忽略数组的内容。
EC:在数组上调用 equals(),其效果等同于 ==(EC_BAD_ARRAY_COMPARE)
该方法在数组上调用了 .equals(Object o) 方法。由于数组没有覆盖 Object 的 equals 方法,在数组上调用 equals 与比较它们的地址相同。要比较数组的内容,请使用 java.util.Arrays.equals(Object[], Object[])。若要比较数组的地址,使用 == 显式检查指针相等性会更清晰。
STI: 不必要地调用 currentThread() 以调用 interrupted()(STI_INTERRUPTED_ON_CURRENTTHREAD)
此方法调用 Thread.currentThread(),只是为了调用 interrupted() 方法。由于 interrupted() 是静态方法,使用 Thread.interrupted() 更为简洁明了。
STI: 在线程实例上调用了静态的 Thread.interrupted() 方法(STI_INTERRUPTED_ON_UNKNOWNTHREAD)
此方法在某个 Thread 对象上调用 Thread.interrupted() 方法,而该对象看起来并非当前线程的 Thread 对象。由于 interrupted() 方法是静态的,因此实际调用的将是与作者本意不同的对象上的 interrupted 方法。
DLS: return 语句中的无效自增(DLS_DEAD_LOCAL_INCREMENT_IN_RETURN)
此语句包含形如 return x++; / return x--; 的 return。后缀自增/自减不会影响表达式的值,因此该自增/自减没有任何效果。请确认该语句的行为是否正确。
DLS: 类字面量的死存储(DLS_DEAD_STORE_OF_CLASS_LITERAL)
此指令将一个类字面量赋值给某个变量,随后从未使用过该变量。此处的行为在 Java 1.4 与 Java 5 之间有所不同。 在 Java 1.4 及更早版本中,对 Foo.class 的引用会强制执行 Foo 的静态初始化器(如果尚未执行的话);而在 Java 5 及更高版本中则不会。
更多细节、示例以及在 Java 5+ 中强制进行类初始化的建议,请参阅 Oracle 的 Java SE 兼容性文章。
IP: 参数在方法入口处即为死值,却被覆盖(IP_PARAMETER_IS_DEAD_BUT_OVERWRITTEN)
该参数的初始值被忽略,并在此处被覆盖。这通常表明开发者误以为对参数的写入会传回给调用方。
MF: 方法定义的变量遮蔽了字段(MF_METHOD_MASKS_FIELD)
此方法定义了一个局部变量,其名称与本类或某个父类中的字段同名。这可能导致方法从该字段读取到未初始化的值,或使该字段始终保持未初始化状态,也可能两者兼有。
MF: 类定义的字段遮蔽了父类字段(MF_CLASS_MASKS_FIELD)
此类定义了一个字段,其名称与某个父类中可见的实例字段同名。这会造成混淆;如果方法在本意是操作其中一个字段时却更新或访问了另一个字段,则可能表明存在错误。
FE: 对 NaN 的相等比较注定失败(FE_TEST_IF_EQUAL_TO_NOT_A_NUMBER)
此代码用于检查某个浮点值是否等于特殊的"非数"(Not A Number)值(例如 if (x == Double.NaN))。然而,由于 NaN 的特殊语义,没有任何值等于 Nan,包括 NaN 本身。因此,x == Double.NaN 的结果始终为 false。若要检查 x 中的值是否为特殊的"非数"值,请使用 Double.isNaN(x)(若 x 为浮点精度,则使用 Float.isNaN(x))。
ICAST: int 值被转换为 long 并用作绝对时间(ICAST_INT_2_LONG_AS_INSTANT)
此代码将一个 32 位 int 值转换为 64 位 long 值,然后将该值作为方法参数传递,而该参数要求的是一个绝对时间值。绝对时间值是从标准基准时间(即 1970 年 1 月 1 日 00:00:00 GMT,称为“纪元”)起算的毫秒数。例如,下面这个旨在将自纪元起的秒数转换为 Date 的方法存在严重错误:
Date getDate(int seconds) { return new Date(seconds * 1000); }乘法运算是使用 32 位算术完成的,然后再转换为 64 位值。当一个 32 位值被转换为 64 位并用于表示绝对时间值时,只能表示 1969 年 12 月和 1970 年 1 月的日期。
上述方法的正确实现如下:
// Fails for dates after 2037
Date getDate(int seconds) { return new Date(seconds * 1000L); }
// better, works for all dates
Date getDate(long seconds) { return new Date(seconds * 1000); }ICAST: 整数值被强制转换为 double 后传入 Math.ceil (ICAST_INT_CAST_TO_DOUBLE_PASSED_TO_CEIL)
此代码将整数值(如 int 或 long)转换为双精度浮点数,然后将结果传入 Math.ceil() 函数,该函数会将一个 double 向上取整为下一个更大的整数值。此操作应当始终是无效操作,因为将整数转换为 double 应当得到一个没有小数部分的数字。传入 Math.ceil 的值很可能本应通过双精度浮点运算来生成。
ICAST: int 值被强制转换为 float 后传入 Math.round (ICAST_INT_CAST_TO_FLOAT_PASSED_TO_ROUND)
此代码将 int 值转换为单精度浮点数,然后将结果传入 Math.round() 函数,该函数会返回最接近参数的 int/long 值。此操作应当始终是无效操作,因为将整数转换为 float 应当得到一个没有小数部分的数字。传入 Math.round 的值很可能本应通过浮点运算来生成。
NP: 已知的 null 值被检查是否是某个类型的实例 (NP_NULL_INSTANCEOF)
此 instanceof 测试将始终返回 false,因为被检查的值保证为 null。虽然这是安全的,但请确认它并非某种误解或其他逻辑错误的迹象。
DMI: 在 int 上调用 Double.longBitsToDouble (DMI_LONG_BITS_TO_DOUBLE_INVOKED_ON_INT)
调用了 Double.longBitsToDouble 方法,但传入的参数是一个 32 位的 int 值。这几乎可以肯定是无意为之,并且不太可能得到预期的结果。
BC: 不可能的类型转换 (BC_IMPOSSIBLE_CAST)
此强制转换将始终抛出 ClassCastException。SpotBugs 会跟踪 instanceof 检查中的类型信息,还会利用从方法返回值和字段加载值的更精确类型信息。因此,它可能掌握比变量声明类型更精确的信息,并据此判断某次强制转换在运行时始终会抛出异常。
BC: 不可能的向下转型 (BC_IMPOSSIBLE_DOWNCAST)
此强制转换将始终抛出 ClassCastException。分析认为它知道被转换值的确切类型,因此尝试将其向下转型为子类型将始终失败并抛出 ClassCastException。
BC: toArray() 结果的不可能向下转型 (BC_IMPOSSIBLE_DOWNCAST_OF_TOARRAY)
此代码将对集合调用 toArray() 的结果强制转换为比 Object[] 更具体的类型,例如:
String[] getAsArray(Collection<String> c) {
return (String[]) c.toArray();
}这通常会因为抛出 ClassCastException 而失败。几乎所有集合的 toArray() 都返回一个 Object[]。它们实际上也别无选择,因为 Collection 对象并不持有该集合所声明的泛型类型的引用。
从集合中获取特定类型数组的正确方式是使用 c.toArray(new String[0]); 或 c.toArray(new String[c.size()]);(自 Java 6 后期更新起,前者效率略高])。
这里有一个常见的、已知的例外:由 Arrays.asList(...) 返回的列表,其 toArray() 方法会返回一个协变类型的数组。例如,Arrays.asArray(new String[] { "a" }).toArray() 会返回一个 String []。SpotBugs 会尝试检测并忽略这类情况,但可能仍会遗漏一些。
BC:instanceof 将始终返回 false(BC_IMPOSSIBLE_INSTANCEOF)
该 instanceof 测试将始终返回 false。虽然这样是安全的,但请确认它并非某种误解或其他逻辑错误的征兆。
RE:正则表达式中使用了「.」或「|」(RE_POISBLE_UNINTENDED_PATTERN)
这里调用了某个字符串函数,并将「.」或「|」作为参数传递给了接收正则表达式的参数。这符合你的本意吗?例如
s.replaceAll(".", "/")返回的 String 中,每一个 字符都会被替换为「/」s.split(".")总是 返回一个长度为零的 String 数组"ab|cd".replaceAll("|", "/")将返回「/a/b/|/c/d/」"ab|cd".split("|")将返回包含六个(!)元素的数组:[, a, b, |, c, d]
请考虑改用 s.replace(".", "/") 或 s.split("\\.")。
RE:正则表达式语法无效(RE_BAD_SYNTAX_FOR_REGULAR_EXPRESSION)
这里的代码使用了一个按照正则表达式语法来说无效的正则表达式。该语句在执行时将抛出 PatternSyntaxException。
RE:将 File.separator 用作正则表达式(RE_CANT_USE_FILE_SEPARATOR_AS_REGULAR_EXPRESSION)
这里的代码在需要正则表达式的地方使用了 File.separator。这在 Windows 平台上会失败,因为那里的 File.separator 是反斜杠,而在正则表达式中会被解释为转义字符。除其他选择外,你可以直接使用 File.separatorChar=='\\' ? "\\\\" : File.separator 来代替 File.separator。
DLS:被覆盖的自增/自减(DLS_OVERWRITTEN_INCREMENT)
代码执行了自增/自减操作(例如 i++ / i--),随后立即将其覆盖。例如,i = i++ / i = i-- 会立即用原始值覆盖自增/自减后的值。
BSHIFT:32 位 int 的移位量不在 -31..31 范围内(ICAST_BAD_SHIFT_AMOUNT)
代码对一个 32 位 int 执行了移位操作,移位量为范围 -31..31 之外的常量。其结果是使用整数值的低 5 位来决定实际移位多少位(例如,移位 40 位等同于移位 8 位,移位 32 位等同于移位 0 位)。这大概不是预期的行为,至少会造成困惑。
BSHIFT:移位运算的解析可能有误(BSHIFT_WRONG_ADD_PRIORITY)
代码执行了类似 (x << 8 + y) 的操作。虽然这可能是正确的,但其本意很可能是执行 (x << 8) + y,然而移位运算符的优先级较低,因此实际上被解析为 x << (8 + y)。
IM:对整数取余结果进行整数乘法(IM_MULTIPLYING_RESULT_OF_IREM)
代码将一个整数取余的结果与一个整数常量相乘。请确保你没有混淆运算符的优先级。例如,i % 60 * 1000 表示的是 (i % 60) * 1000,而不是 i % (60 * 1000)。
DMI:对数组调用 hashCode(DMI_INVOKING_HASHCODE_ON_ARRAY)
代码对一个数组调用了 hashCode。对数组调用 hashCode 所返回的值与 System.identityHashCode 相同,它会忽略数组的内容和长度。如果你需要一个取决于数组内容的 hashCode a,请使用 java.util.Arrays.hashCode(a)。
USELESS_STRING:对数组调用 toString(DMI_INVOKING_TOSTRING_ON_ARRAY)
代码对一个数组调用了 toString,这将产生一个相当无用的结果,例如 [C@16f0472。建议使用 Arrays.toString 将数组转换为可读的字符串,以显示数组的内容。参见《Programming Puzzlers》第 3 章第 12 个谜题。
USELESS_STRING:对匿名数组调用 toString(DMI_INVOKING_TOSTRING_ON_ANONYMOUS_ARRAY)
代码对一个(匿名)数组调用了 toString。对数组调用 toString 会产生一个相当无用的结果,例如 [C@16f0472。建议使用 Arrays.toString 将数组转换为可读的字符串,以显示数组的内容。参见《Programming Puzzlers》第 3 章第 12 个谜题。
DMI:月份常量值无效(DMI_BAD_MONTH)
此代码将一个超出预期范围 0..11 的常量月份值传递给了某个方法。
DMI:hasNext 方法调用了 next(DMI_CALLING_NEXT_FROM_HASNEXT)
hasNext() 方法调用了 next() 方法。这几乎可以肯定是错误的,因为 hasNext() 方法不应改变迭代器的状态,而 next 方法才应该改变迭代器的状态。
QBA:方法在布尔表达式中赋值布尔字面量(QBA_QUESTIONABLE_BOOLEAN_ASSIGNMENT)
此方法在 if 或 while 表达式中将一个字面布尔值(true 或 false)赋值给一个布尔变量。这很可能应该是使用 == 进行布尔比较,而不是使用 = 进行赋值。
GC:泛型参数与方法实参之间没有关联(GC_UNRELATED_TYPES)
此对泛型集合方法的调用包含一个与集合参数类型不兼容的类的实参(即,实参的类型既不是相应泛型类型实参的超类型,也不是其子类型)。因此,该集合中不太可能包含任何与此处使用的方法实参相等的对象。最有可能的情况是,向该方法传递了错误的值。
一般来说,两个不相关的类的实例是不相等的。例如,如果 Foo 类和 Bar 类之间不存在子类型关系,那么 Foo 的实例就不应该等于 Bar 的实例。除其他问题外,这样做很可能导致 equals 方法不具备对称性。例如,如果你定义的 Foo 类使得 Foo 可以等于 String,那么你的 equals 方法就不对称,因为 String 只能等于 String。
在极少数情况下,人们确实会定义不对称的 equals 方法,并且仍设法让代码正常工作。尽管没有任何 API 对此作出文档说明或保证,但通常的情况是:当你检查一个 Collection<String> 是否包含一个 Foo 时,用于执行相等性检查的是参数的 equals 方法(例如 Foo 类的 equals 方法)。
DMI:对集合的无意义调用(DMI_VACUOUS_SELF_COLLECTION_CALL)
这个调用没有意义。对于任何集合 c,调用 c.containsAll(c) 的结果应该始终为 true,而 c.retainAll(c) 不会产生任何效果。
DMI:哎呀!一个不合逻辑的方法调用(DMI_DOH)
这个特定的方法调用没有道理,原因从检查代码时就应当显而易见。
DMI:集合不应包含自身(DMI_COLLECTIONS_SHOULD_NOT_CONTAIN_THEMSELVES)
对泛型集合方法的这次调用,只有在集合包含自身时才有意义(例如当 s.contains(s) 为 true 时)。这种情况不太可能成立,即使成立也会引发问题(比如计算哈希码时导致无限递归)。很可能是作为参数传入了错误的值。
TQ:在要求必须具有类型限定符的位置使用了没有类型限定符的值(TQ_UNKNOWN_VALUE_USED_WHERE_ALWAYS_STRICTLY_REQUIRED)
某个值的使用方式要求该值必须带有类型限定符注解。该类型限定符是严格的,因此该工具会拒绝任何没有相应注解的值。
要强制让一个值带有严格注解,可以定义一个恒等函数,其返回值标注了该严格注解。这是将未注解的值转换为带有严格类型限定符注解的值的唯一方法。
TQ:比较具有不兼容类型限定符的值(TQ_COMPARING_VALUES_WITH_INCOMPATIBLE_TYPE_QUALIFIERS)
一个被声明带有类型限定符注解的值,与一个从未携带该限定符的值进行了比较。
更准确地说,一个标注了 when=ALWAYS 的类型限定符的值,与另一个同一类型限定符标注为 when=NEVER 的值进行了比较。
例如,假设 @NonNegative 是类型限定符注解 @Negative(when=When.NEVER) 的别名。下面的代码将产生此警告,因为 return 语句要求返回一个 @NonNegative 值,但收到的却是标记为 @Negative 的值。
public boolean example(@Negative Integer value1, @NonNegative Integer value2) {
return value1.equals(value2);
}TQ: 值标注了类型限定符,却被用在不允许携带该限定符的位置(TQ_ALWAYS_VALUE_USED_WHERE_NEVER_REQUIRED)
某个值被指定携带类型限定符注解,却在一个或多个要求该值不得携带该注解的位置被使用。
更准确地说,一个标注了类型限定符且指定 when=ALWAYS 的值,被保证会传递到一个或多个该类型限定符指定 when=NEVER 的使用位置。
例如,假设 @NonNegative 是类型限定符注解 @Negative(when=When.NEVER) 的别名。下面的代码将产生此警告,因为 return 语句要求一个 @NonNegative 值,但接收到的却是被标记为 @Negative 的值。
public @NonNegative Integer example(@Negative Integer value) {
return value;
}TQ:标注为从不携带类型限定符的值被用在必须携带该限定符的位置(TQ_NEVER_VALUE_USED_WHERE_ALWAYS_REQUIRED)
被指定为不携带类型限定符注解的值,保证会在一个或多个要求该值必须携带此注解的位置被使用。
更准确地说,标注了类型限定符 when=NEVER 的值,保证会到达一个或多个同一类型限定符指定为 when=ALWAYS 的使用位置。
待办:示例
TQ:可能不携带类型限定符的值总是以要求该类型限定符的方式被使用(TQ_MAYBE_SOURCE_VALUE_REACHES_ALWAYS_SINK)
一个被标注为可能不是该类型限定符所指值的实例的值,保证会以要求该类型限定符所指值的方式被使用。
TQ:可能携带类型限定符的值总是以禁止其拥有该类型限定符的方式被使用(TQ_MAYBE_SOURCE_VALUE_REACHES_NEVER_SINK)
一个被标注为可能是该类型限定符所指值的实例的值,保证会以禁止该类型限定符所指值的方式被使用。
FB:SpotBugs 产生意外的/不希望出现的警告(FB_UNEXPECTED_WARNING)
SpotBugs 生成了一个警告,根据 @NoWarning 注解,该警告是意外的或不希望出现的。
FB:SpotBugs 缺少期望的/希望出现的警告(FB_MISSING_EXPECTED_WARNING)
SpotBugs 没有生成某个警告,根据 @ExpectedWarning 注解,该警告是被期望或希望出现的。
EOS:读取的数据在与 -1 比较之前被转换(EOS_BAD_END_OF_STREAM_CHECK)
方法 java.io.FileInputStream.read() 返回一个 int。如果该 int 被转换为 byte,那么 -1(表示 EOF)和字节 0xFF 将变得无法区分,将(转换后的)结果与 -1 比较会导致读取(通常是在循环中)在遇到字符 0xFF 时提前结束。类似地,方法 java.io.FileReader.read() 也返回一个 int。如果它被转换为 char,那么 -1 会变成 0xFFFF,即 Character.MAX_VALUE。将结果与 -1 比较是毫无意义的,因为 Java 中的字符是无符号的。如果对 EOF 的检查是循环的条件,那么该循环将成为无限循环。
参见 SEI CERT 规则 FIO08-J. 区分从流中读取的字符或字节与 -1。
FL:不要将浮点变量用作循环计数器(FL_FLOATS_AS_LOOP_COUNTERS)
不应将浮点变量用作循环计数器,因为它们并不精确,这可能导致循环出现错误。循环计数器是一个在每次迭代中都会改变并用于控制循环何时终止的变量,它在每次迭代中会以固定的量递减或递增。
参见规则 NUM09-J。
SING:使用单例设计模式的类直接实现了 Cloneable 接口(SING_SINGLETON_IMPLEMENTS_CLONEABLE)
如果使用单例设计模式的类直接实现 Cloneable 接口,则有可能创建该对象的副本,从而违反单例模式。
因此,应避免实现 Cloneable 接口。
更多信息请参见:SEI CERT MSC07-J.
SING:使用单例设计模式的类间接实现 Cloneable 接口。(SING_SINGLETON_INDIRECTLY_IMPLEMENTS_CLONEABLE)
如果使用单例设计模式的类间接实现 Cloneable 接口,则有可能创建该对象的副本,从而违反单例模式。
因此,应避免实现 Cloneable 接口。如果由于继承的父类而无法避免,则解决方案是重写 clone 方法,使其无条件抛出 CloneNotSupportedException。
更多信息请参见:SEI CERT MSC07-J.
SING:使用单例设计模式的类实现 clone() 方法,但并非无条件抛出 CloneNotSupportedException。(SING_SINGLETON_IMPLEMENTS_CLONE_METHOD)
该类使用单例设计模式,并且没有实现 Cloneable 接口,但实现了 clone() 方法,且并非无条件抛出 CloneNotSupportedException。这样就有可能创建该对象的副本,从而违反单例模式。
因此,应避免实现 clone 方法,否则解决方案是重写 clone 方法,使其无条件抛出 CloneNotSupportedException。
更多信息请参见:SEI CERT MSC07-J.
SING:使用单例设计模式的类具有非私有构造函数。(SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR)
该类使用单例设计模式,并且具有非私有构造函数(请注意,可能存在非私有的默认构造函数)。据此,就有可能创建该对象的副本,从而违反单例模式。
最简单的解决方案是将构造函数设为私有。
SING:使用单例设计模式的类直接或间接实现 Serializable 接口。(SING_SINGLETON_IMPLEMENTS_SERIALIZABLE)
该类(使用单例设计模式)直接或间接实现了 Serializable 接口,该接口允许对类进行序列化。
反序列化使得单例类的多次实例化成为可能,因此应予以避免。
SING:使用单例设计模式的类的实例获取方法未同步。(SING_SINGLETON_GETTER_NOT_SYNCHRONIZED)
使用单例设计模式的类的实例获取方法未同步。当该方法被两个或多个线程同时调用时,单例类的多次实例化就成为可能。
实验性 (EXPERIMENTAL)
实验性的、尚未完全经过验证的 Bug 模式
SKIPPED: 类太大无法分析 (SKIPPED_CLASS_TOO_BIG)
该类的规模超出了有效处理的范围,因此未对其进行全面的错误分析。
TEST: 未知的 bug 模式 (UNKNOWN)
系统记录了一条警告,但 SpotBugs 找不到该 bug 模式的描述,因而无法对其进行说明。这种情况只会出现在 SpotBugs 或其配置本身存在缺陷时,或者是分析结果由某个插件生成、而该插件当前并未加载的情况下。
TEST: 测试 (TESTING)
该 bug 模式只会由尚不完善的新缺陷检测器生成。
TEST: 测试 1 (TESTING1)
该 bug 模式只会由尚不完善的新缺陷检测器生成。
TEST: 测试 2 (TESTING2)
该 bug 模式只会由尚不完善的新缺陷检测器生成。
TEST: 测试 3 (TESTING3)
该 bug 模式只会由尚不完善的新缺陷检测器生成。
OBL: 方法可能未清理流或资源 (OBL_UNSATISFIED_OBLIGATION)
该方法可能未能清理(关闭、释放)流、数据库对象或其他需要显式执行清理操作的资源。
一般来说,如果一个方法打开了流或其他资源,就应当使用 try/finally 块,以确保在方法返回之前清理该流或资源。
该 bug 模式与 OS_OPEN_STREAM 和 ODR_OPEN_DATABASE_RESOURCE 模式在本质上相同,但它基于一种不同的(而且希望是更好的)静态分析技术。我们非常希望收到关于该 bug 模式实用价值的反馈。如需反馈,请参阅:
特别需要说明的是,该 bug 模式的误报抑制启发式规则尚未经过充分调优,因此收到误报反馈对我们很有帮助。
分析技术的描述可参见 Weimer 和 Necula 的论文 Finding and Preventing Run-Time Error Handling Mistakes(PDF)。
OBL: 方法可能在受检异常时未清理流或资源 (OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE)
该方法可能未能清理(关闭、释放)流、数据库对象或其他需要显式执行清理操作的资源。
一般来说,如果一个方法打开了流或其他资源,就应当使用 try/finally 块,以确保在方法返回之前清理该流或资源。
该 bug 模式与 OS_OPEN_STREAM 和 ODR_OPEN_DATABASE_RESOURCE 模式在本质上相同,但它基于一种不同的(而且希望是更好的)静态分析技术。我们非常希望收到关于该 bug 模式实用价值的反馈。如需反馈,请参阅:
特别需要说明的是,该 bug 模式的误报抑制启发式规则尚未经过充分调优,因此收到误报反馈对我们很有帮助。
参见 Weimer 和 Necula 的 Finding and Preventing Run-Time Error Handling Mistakes(PDF),其中描述了该分析技术。
LG:由于 OpenJDK 中的弱引用可能导致日志记录器设置丢失(LG_LOST_LOGGER_DUE_TO_WEAK_REFERENCE)
OpenJDK 引入了一个潜在的不兼容问题。具体来说,java.util.logging.Logger 的行为发生了变化:它不再使用强引用,而是在内部改用弱引用。这本身是一个合理的改动,但遗憾的是,有些代码依赖旧行为——在修改日志记录器配置时,它们直接丢弃了对 logger 的引用。这意味着垃圾回收器可以随时回收该内存,从而导致日志记录器的配置丢失。例如,考虑以下代码:
public static void initLogging() throws Exception {
Logger logger = Logger.getLogger("edu.umd.cs");
logger.addHandler(new FileHandler()); // call to change logger configuration
logger.setUseParentHandlers(false); // another call to change logger configuration
}在方法结束时,该 logger 引用会丢失(它不会逃逸出该方法),因此如果在调用 initLogging 之后立即发生一次垃圾回收,日志记录器的配置就会丢失(因为 Logger 只持有弱引用)。
public static void main(String[] args) throws Exception {
initLogging(); // adds a file handler to the logger
System.gc(); // logger configuration lost
Logger.getLogger("edu.umd.cs").info("Some message"); // this isn't logged to the file as expected
}Ulf Ochsenfahrt 与 Eric Fellheimer
国际化(I18N)
与国际化及区域设置相关的代码缺陷
Dm:考虑使用被调用方法的带 Locale 参数的版本(DM_CONVERT_CASE)
某个 String 正在使用平台的默认编码转换为大写或小写。当处理国际化字符时,这可能导致转换不当。请改用以下版本:
- String.toUpperCase( Locale l )
- String.toLowerCase( Locale l )
Dm:依赖默认编码(DM_DEFAULT_ENCODING)
发现一处对某方法的调用,该方法会执行字节到 String(或 String 到字节)的转换,并假定平台的默认编码是合适的。这将导致应用程序在不同平台上的行为不一致。请改用其他 API,并显式指定字符集名称或 Charset 对象。
恶意代码漏洞(MALICIOUS_CODE)
容易受到不可信代码攻击的代码
DP:所调用的方法应只在 doPrivileged 块内调用(DP_DO_INSIDE_DO_PRIVILEGED)
此处代码调用了一个需要进行安全权限检查的方法。如果此代码会被授予安全权限,但却可能被不具备安全权限的代码调用,那么该调用就需要发生在 doPrivileged 块内。
DP:类加载器应只在 doPrivileged 块内创建(DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED)
此处代码创建了一个类加载器,如果安装了安全管理器,则创建操作需要相应权限。如果此代码可能被不具备安全权限的代码调用,那么类加载器的创建就需要发生在 doPrivileged 块内。
FI:Finalizer 方法应为 protected 而非 public(FI_PUBLIC_SHOULD_BE_PROTECTED)
类的 finalize() 方法应具有 protected 访问级别,而非 public。
MS:公共静态方法可能通过返回可变对象或数组暴露内部表示(MS_EXPOSE_REP)
某个公共静态方法返回了对可变对象或数组的引用,而该对象或数组是类静态状态的一部分。任何调用此方法的代码都可以随意修改底层数组。一种修复方式是返回该数组的副本。
MS:可能通过返回共享非公开数据的缓冲区暴露内部表示(MS_EXPOSE_BUF)
某个公共静态方法要么返回一个缓冲区(java.nio.*Buffer),该缓冲区包装了类静态状态的一部分数组,且仅持有对同一数组的引用;要么返回类静态状态中某个缓冲区的浅拷贝,该拷贝与原缓冲区共享其引用。任何调用此方法的代码都可以随意修改底层数组。一种修复方式是返回只读缓冲区,或返回一个包含该数组副本的新缓冲区。
EI:可能通过返回对可变对象的引用而暴露内部表示(EI_EXPOSE_REP)
返回存储在对象某个字段中的可变对象值的引用,会暴露该对象的内部表示。如果实例会被不可信代码访问,且对可变对象的未受检查的修改会危及安全性或其他重要属性,那么你就需要采用其他做法。在许多情况下,返回对象的一个新副本是更好的方案。
EI:可能通过返回共享非公开数据的缓冲区而暴露内部表示(EI_EXPOSE_BUF)
返回一个包装了存储在对象某个字段中的数组的缓冲区(java.nio.*Buffer)的引用,会暴露该数组元素的内部表示,因为缓冲区只是保存了对数组的引用,而不会复制其内容。类似地,返回存储在对象某个字段中的此类缓冲区的浅拷贝(使用其 duplicate() 方法),也会暴露该缓冲区的内部表示。如果实例会被不可信代码访问,且对数组的未受检查的修改会危及安全性或其他重要属性,那么你就需要采用其他做法。在许多情况下,返回一个只读缓冲区(使用其 asReadOnly() 方法)或把数组复制到新缓冲区(使用其 put() 方法)是更好的方案。
EI2:可能通过纳入对可变对象的引用而暴露内部表示(EI_EXPOSE_REP2)
这段代码将对外部可变对象的引用存入了对象的内部表示。如果实例会被不可信代码访问,且对可变对象的未受检查的修改会危及安全性或其他重要属性,那么你就需要采用其他做法。在许多情况下,存储该对象的一个副本是更好的方案。
MS:可能通过将可变对象存入静态字段而暴露内部静态状态(EI_EXPOSE_STATIC_REP2)
这段代码将对外部可变对象的引用存入了静态字段。如果对可变对象的未受检查的修改会危及安全性或其他重要属性,那么你就需要采用其他做法。在许多情况下,存储该对象的一个副本是更好的方案。
EI2:可能通过创建纳入数组引用的缓冲区而暴露内部表示(EI_EXPOSE_BUF2)
这段代码创建了一个缓冲区,并将对外部数组或外部缓冲区所含数组的引用存入了对象的内部表示。如果实例会被不可信代码访问,且对数组的未受检查的修改会危及安全性或其他重要属性,那么你就需要采用其他做法。在许多情况下,存储该数组的一个副本是更好的方案。
MS:可能通过创建将外部数组存入静态字段的缓冲区而暴露内部静态状态(EI_EXPOSE_STATIC_BUF2)
此代码创建一个缓冲区,该缓冲区将对外部数组或外部缓冲区数组的引用保存到静态字段中。如果对该数组的不受控修改会危及安全性或其他重要属性,您就需要采用其他方式。在许多情况下,保存数组的副本是更好的做法。
MS:字段应移出接口并设为包级保护(MS_OOI_PKGPROTECT)
在接口中定义的 final 静态字段引用了可变对象,例如数组或哈希表。该可变对象可能被恶意代码修改,也可能被其他包中的代码意外修改。要解决此问题,需要将该字段移到类中,并将其设为包级保护,以避免此漏洞。
MS:字段应同时为 final 和包级保护(MS_FINAL_PKGPROTECT)
可变的静态字段可能被恶意代码修改,也可能被其他包中的代码意外修改。可以将该字段设为包级保护和/或设为 final,以避免此漏洞。
MS:字段不是 final,但应该是(MS_SHOULD_BE_FINAL)
此 public static 或 protected static 字段不是 final,可能被恶意代码修改,也可能被其他包中的代码意外修改。可以将该字段设为 final,以避免此漏洞。
MS:字段不是 final,但应通过重构使其成为 final(MS_SHOULD_BE_REFACTORED_TO_BE_FINAL)
此 public static 或 protected static 字段不是 final,可能被恶意代码修改,也可能被其他包中的代码意外修改。可以将该字段设为 final,以避免此漏洞。但是,静态初始化器中对该字段有多次写入,因此这样做需要进行一些重构。
MS:字段应为包级保护(MS_PKGPROTECT)
可变的静态字段可能被恶意代码修改,也可能被意外修改。可以将该字段设为包级保护,以避免此漏洞。
MS:字段是可变的 Hashtable(MS_MUTABLE_HASHTABLE)
final 静态字段引用了一个 Hashtable,并可能被恶意代码访问,也可能被其他包中的代码意外访问。该代码可以随意修改 Hashtable 的内容。
MS:字段是可变的数组(MS_MUTABLE_ARRAY)
final 静态字段引用了一个数组,并可能被恶意代码访问,也可能被其他包中的代码意外访问。该代码可以随意修改数组的内容。
MS:字段是可变的集合(MS_MUTABLE_COLLECTION)
一个可变的集合实例被赋值给 final 静态字段,因此可能被恶意代码修改,也可能被其他包中的代码意外修改。可考虑使用 Collections.unmodifiableSet/List/Map 等包装该字段,以避免此漏洞。
MS:字段是可变的集合,且应设为包级保护(MS_MUTABLE_COLLECTION_PKGPROTECT)
一个可变集合实例被赋值给 final static 字段,因此可能被恶意代码或其他包中的代码意外修改。可以将该字段改为包级保护以避免此漏洞。或者,你可以用 Collections.unmodifiableSet/List/Map 等包装该字段以避免此漏洞。
MS:字段不是 final 的,无法防止恶意代码修改(MS_CANNOT_BE_FINAL)
一个可变静态字段可能被恶意代码或其他包中的代码意外修改。遗憾的是,该字段的使用方式使得无法简单地修复此问题。
REFLC:公共方法使用反射创建其参数中传入的类,这可能会提高任何类的可访问性(REFLC_REFLECTION_MAY_INCREASE_ACCESSIBILITY_OF_CLASS)
SEI CERT SEC05-J] 规则禁止使用反射来提高类、方法或字段的可访问性。如果某个包中的类提供了一个公共方法,该方法以 java.lang.Class 实例作为参数并调用其 newInstance() 方法,那么它会提高同一包中没有公共构造器的类的可访问性。攻击者代码可以调用此方法并传入这样的类来创建其实例。应当通过将该方法改为非公共,或检查该包的包访问权限来避免此问题。第三种选择是使用 java.beans.Beans.instantiate() 方法代替 java.lang.Class.newInstance(),前者会检查所接收的 Class 对象是否具有公共构造器。
REFLF:公共方法使用反射修改其参数中传入的字段,这可能会提高任何类的可访问性(REFLF_REFLECTION_MAY_INCREASE_ACCESSIBILITY_OF_FIELD)
SEI CERT SEC05-J] 规则禁止使用反射来提高类、方法或字段的可访问性。如果某个包中的类提供了一个公共方法,该方法以 java.lang.reflect.Field 实例作为参数并调用 setter(或 setAccessible())方法,那么它会提高同一包中私有、受保护或包级私有字段的可访问性。攻击者代码可以调用此方法并传入这样的字段来修改它。应当通过将该方法改为非公共,或检查该包的包访问权限来避免此问题。
MC:从构造器中调用了可重写的方法(MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR)
在构造器中调用可重写的方法可能导致使用未初始化的数据,也可能泄露部分构造完成的对象的 this 引用。构造器中只应调用 static、final 或 private 方法。
参见 SEI CERT 规则 MET05-J. 确保构造器不调用可重写的方法]。
MC:从 clone() 方法中调用了可重写的方法(MC_OVERRIDABLE_METHOD_CALL_IN_CLONE)
从 clone() 方法中调用可重写方法是不安全的,因为子类可以重写该方法,从而影响 clone() 的行为。子类还可能观察到或修改处于部分初始化状态的 clone 对象。clone() 方法中只应调用 static、final 或 private 方法。
参见 SEI CERT 规则 MET06-J. Do not invoke overridable methods in clone().
MC:在 readObject 方法中调用了可重写方法。(MC_OVERRIDABLE_METHOD_CALL_IN_READ_OBJECT)
readObject() 方法不得调用任何可重写方法。从 readObject() 方法中调用可重写方法,会使被重写的方法在对象完全初始化之前访问其状态。之所以存在这种过早访问的可能,是因为在反序列化过程中,readObject 扮演着对象构造函数的角色,因此在 readObject 返回之前,对象的初始化并未完成。
参见 SEI CERT 规则 SER09-J. Do not invoke overridable methods from the readObject() method.
PERM:自定义类加载器未调用其父类的 getPermissions()(PERM_SUPER_NOT_CALLED_IN_GETPERMISSIONS)
SEI CERT 规则 SEC07-J 要求自定义类加载器必须在其自身的 getPermissions() 方法中始终调用父类的 getPermissions() 方法,以初始化其最终返回的对象。省略这一调用意味着,使用该自定义类加载器定义的类所拥有的权限,与系统范围策略文件中指定的权限完全无关。实际上,该类的权限会覆盖策略文件中的权限。
USC:基于不可信来源的安全检查存在风险。(USC_POTENTIAL_SECURITY_CHECK_BASED_ON_UNTRUSTED_SOURCE)
公共类的公共方法可能在包外被调用,这意味着不可信数据可能被传递给它。在 doPrivileged 之前调用某个方法以检查其返回值,然后在类内部再次调用同一个方法,如果该方法或其所在的类不是 final 的,就会产生危险。攻击者可能传递该类的一个恶意子类的实例,而不是预期的实例;在该子类中,这个方法被重写为每次调用返回不同的值。例如,一个返回文件路径的方法可能在进入 doPrivileged 块之前的检查中返回一个无害的路径,而在 doPrivileged 块内的调用中返回一个敏感文件。为避免这种情况,应防御性地复制参数中接收到的对象,例如使用形参类型所对应类的拷贝构造函数。这样可以确保该方法的行为完全符合预期。
参见 SEI CERT 规则 SEC02-J. Do not base security checks on untrusted sources.
VSC:非 private 且非 final 的安全检查方法存在漏洞(VSC_VULNERABLE_SECURITY_CHECK_METHODS)
执行安全检查的方法应防止被重写,因此它们必须声明为 private 或 final。否则,当恶意子类重写这些方法并省略检查时,这些方法就可能被绕过。
请参阅 SEI CERT 规则 MET03-J. 执行安全检查的方法必须声明为 private 或 final。
多线程正确性 (MT_CORRECTNESS)
与线程、锁和 volatile 相关的代码缺陷
AT: 对并发抽象的多次调用序列可能不是原子的 (AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION)
这段代码包含对某个并发抽象(例如并发哈希映射)的一系列调用。这些调用不会以原子方式执行。
STCAL: 静态 Calendar 字段 (STCAL_STATIC_CALENDAR_INSTANCE)
尽管 JavaDoc 中没有相关提示,但 Calendar 本质上并不适合多线程使用。在没有适当同步的情况下跨线程共享同一个实例,会导致应用程序行为异常。在 1.4 版本下,问题似乎较少出现,而在 Java 5 下则更常见,你可能会在 sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate() 中随机看到 ArrayIndexOutOfBoundsException 或 IndexOutOfBoundsException。
你还可能遇到序列化问题。
建议使用实例字段。
有关更多信息,请参阅 JDK Bug devlive-community/knowforge#6231579 和 JDK Bug devlive-community/knowforge#6178997。
STCAL: 静态 DateFormat (STCAL_STATIC_SIMPLE_DATE_FORMAT_INSTANCE)
正如 JavaDoc 所述,DateFormat 本质上并不适合多线程使用。在没有适当同步的情况下跨线程共享同一个实例,会导致应用程序行为异常。
你还可能遇到序列化问题。
建议使用实例字段。
有关更多信息,请参阅 JDK Bug devlive-community/knowforge#6231579 和 JDK Bug devlive-community/knowforge#6178997。
STCAL: 对静态 Calendar 的调用 (STCAL_INVOKE_ON_STATIC_CALENDAR_INSTANCE)
尽管 JavaDoc 中没有相关提示,但 Calendar 本质上并不适合多线程使用。检测器发现了一处对通过静态字段获取的 Calendar 实例的调用。这看起来很可疑。
有关更多信息,请参阅 JDK Bug devlive-community/knowforge#6231579 和 JDK Bug devlive-community/knowforge#6178997。
STCAL: 对静态 DateFormat 的调用 (STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE)
正如 JavaDoc 所述,DateFormat 本质上并不适合多线程使用。检测器发现了一处对通过静态字段获取的 DateFormat 实例的调用。这看起来很可疑。
有关更多信息,请参阅 JDK Bug devlive-community/knowforge#6231579 和 JDK Bug devlive-community/knowforge#6178997。
NP: 对同一字段进行同步和 null 检查。(NP_SYNC_AND_NULL_CHECK_FIELD)
由于该字段是被同步的对象,它不太可能为 null。如果它为 null,那么同步时会抛出 NullPointerException,这个检查也就毫无意义了。最好改为对另一个字段进行同步。
VO:指向数组的 volatile 引用并不会把数组元素视为 volatile(VO_VOLATILE_REFERENCE_TO_ARRAY)
这声明了一个指向数组的 volatile 引用,这可能并不是你想要的。对于指向数组的 volatile 引用,只有对该数组引用的读写被视为 volatile 操作,而数组元素是非 volatile 的。要获得 volatile 的数组元素,你需要使用 java.util.concurrent 中的某个原子数组类(Java 5.0 中提供)。
VO:对 volatile 字段的自增操作不是原子的(VO_VOLATILE_INCREMENT)
这段代码对一个 volatile 字段执行自增/自减操作。volatile 字段的自增/自减不是原子的。如果有多个线程同时对该字段进行自增/自减,可能会丢失某些自增/自减操作。
Dm:在 Condition 上调用 Monitor wait()(DM_MONITOR_WAIT_ON_CONDITION)
此方法在 java.util.concurrent.locks.Condition 对象上调用 wait()。等待 Condition 应该使用 Condition 接口定义的某个 await() 方法来完成。
Dm:使用默认的空 run 方法创建了线程(DM_USELESS_THREAD)
此方法创建线程时既没有通过继承 Thread 类,也没有通过传入 Runnable 对象来指定 run 方法。因此,这个线程什么也不做,只是浪费时间。
DC:字段可能被双重检查(DC_DOUBLECHECK)
此方法中可能包含双重检查锁定的实例。按照 Java 内存模型的语义,这种惯用法是不正确的。更多信息请参阅网页 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html。
DC:可能暴露部分初始化的对象(DC_PARTIALLY_CONSTRUCTED)
看起来此方法使用了带有双重检查锁定的字段延迟初始化。虽然该字段已正确声明为 volatile,但在字段赋值之后对象的内部结构仍可能被改变,因此另一个线程可能会看到部分初始化的对象。
要修复此问题,可以考虑先将对象存储到局部变量中,只有在对象完全构造之后再将其保存到 volatile 字段中。
DL:对 String 字面量进行同步(DL_SYNCHRONIZATION_ON_SHARED_CONSTANT)
代码对 String 字面量进行了同步。
private static String LOCK = "LOCK";
...
synchronized(LOCK) {
...
}
...常量字符串会被驻留(intern)并在 JVM 加载的所有其他类之间共享。因此,这段代码锁定的对象可能同时也被其他代码锁定。这可能导致非常奇怪且难以诊断的阻塞与死锁行为。参见 http://www.javalobby.org/java/forums/t96352.html 和 http://jira.codehaus.org/browse/JETTY-352。
更多信息请参见 CERT LCK01-J. 不要对可能被重用的对象进行同步。
DL:对布尔值进行同步(DL_SYNCHRONIZATION_ON_BOOLEAN)
代码对装箱后的基本类型常量(例如 Boolean)进行同步。
private static Boolean inited = Boolean.FALSE;
...
synchronized(inited) {
if (!inited) {
init();
inited = Boolean.TRUE;
}
}
...由于通常只存在两个 Boolean 对象,这段代码可能会与其他不相关的代码对同一个对象进行同步,从而导致无响应以及可能的死锁。
更多信息请参见 CERT LCK01-J. Do not synchronize on objects that may be reused。
DL:对装箱基本类型进行同步(DL_SYNCHRONIZATION_ON_BOXED_PRIMITIVE)
该代码对一个装箱基本类型常量(如 Integer)进行同步。
private static Integer count = 0;
...
synchronized(count) {
count++;
}
...由于 Integer 对象可能被缓存并共享,此代码可能会与其他无关代码在同一个对象上进行同步,从而导致无响应甚至可能的死锁。
更多信息请参见 CERT LCK01-J. Do not synchronize on objects that may be reused。
DL:对装箱基本类型的值进行同步(DL_SYNCHRONIZATION_ON_UNSHARED_BOXED_PRIMITIVE)
代码在一个看似未共享的装箱基本类型(例如 Integer)上进行同步。
private static final Integer fileLock = new Integer(1);
...
synchronized(fileLock) {
.. do something ..
}
...在这段代码中,最好将 fileLock 重新声明为局部变量
private static final Object fileLock = new Object();现有代码可能没有问题,但它容易让人困惑,而且未来的重构操作(例如 IntelliJ 中的「Remove Boxing(移除装箱)」重构)可能会将这段代码替换为对整个 JVM 中共享的驻留 Integer 对象的使用,从而导致非常令人困惑的行为和潜在的死锁。
DL:对驻留的 String 进行同步(DL_SYNCHRONIZATION_ON_INTERNED_STRING)
该代码对驻留的 String 进行同步。
private static String LOCK = new String("LOCK").intern();
...
synchronized(LOCK) {
...
}
...常量字符串会被驻留,并在 JVM 加载的所有其他类之间共享。因此,这段代码锁定的对象可能也被其他代码锁定。这可能导致非常奇怪且难以诊断的阻塞和死锁行为。参见 http://www.javalobby.org/java/forums/t96352.html 和 http://jira.codehaus.org/browse/JETTY-352。
有关更多信息,请参阅 CERT LCK01-J. 不要对可能被重用的对象进行同步。
WL:对 getClass 而不是类字面量进行同步(WL_USING_GETCLASS_RATHER_THAN_CLASS_LITERAL)
此实例方法对 this.getClass() 进行同步。如果该类被子类化,子类将对子类的类对象进行同步,这很可能不是原本的意图。例如,考虑来自 java.awt.Label 的这段代码:
private static final String base = "label";
private static int nameCounter = 0;
String constructComponentName() {
synchronized (getClass()) {
return base + nameCounter++;
}
}Label 的子类不会在同一个子类上同步,从而导致数据竞争。相反,此代码应该在 Label.class 上进行同步。
private static final String base = "label";
private static int nameCounter = 0;
String constructComponentName() {
synchronized (Label.class) {
return base + nameCounter++;
}
}由 Jason Mehrens 贡献的 Bug 模式。
ESync:空的同步块(ESync_EMPTY_SYNC)
代码中包含一个空的同步块:
synchronized() {
}空的同步块比大多数人所认识到的更加微妙,也更难正确使用,而且空的同步块几乎从来都不是比更自然的解决方案更好的选择。
MSF:可变的 servlet 字段(MSF_MUTABLE_SERVLET_FIELD)
Web 服务器通常只为 servlet 或 JSP 类创建一个实例(即把该类当作单例),并会有多个线程在该实例上调用方法,以服务多个同时到达的请求。因此,拥有一个可变的实例字段通常会造成竞态条件。
IS:同步不一致(IS2_INCONSISTENT_SYNC)
本类的字段在同步方面的访问方式似乎不一致。此缺陷报告表明,缺陷模式检测器判定:
- 该类中同时存在加锁访问和未加锁访问,
- 该类没有被标注为 javax.annotation.concurrent.NotThreadSafe,
- 至少有一次加锁访问是由该类自身的某个方法执行的,并且
- 未同步的字段访问(读和写)次数不超过所有访问次数的三分之一,其中写入的权重按读取的两倍计算
符合此缺陷模式的典型问题是:在一个本应线程安全的类中,忘记对其中某个方法进行同步。
你可以选择标记为“未同步访问”的节点,以查看检测器认为字段在未同步情况下被访问的代码位置。
请注意,此检测器存在多种不准确的来源;例如,检测器无法静态检测出所有持锁的情况。另外,即使检测器在区分加锁访问与未加锁访问方面是准确的,相关代码也可能是正确的。
NN:裸 notify(NN_NAKED_NOTIFY)
调用了 notify() 或 notifyAll(),但没有伴随任何(明显的)可变对象状态修改。一般来说,在监视器上调用 notify 方法,是因为另一个线程正在等待的某个条件已经变为真。然而,要使该条件有意义,它必须涉及一个对两个线程都可见的堆对象。
此缺陷并不一定表示存在错误,因为对可变对象状态的修改可能发生在某个方法中,而该方法随后调用了包含此通知的代码所在的方法。
Ru:在线程上调用 run(你是不是本想启动它?)(RU_INVOKE_RUN)
此方法在一个对象上显式调用了 run()。一般来说,类实现 Runnable 接口是因为打算在新线程中调用其 run() 方法,在这种情况下,调用 Thread.start() 才是正确的方法。
SP:方法在字段上自旋(SP_SPIN_ON_FIELD)
此方法在一个读取字段的循环中自旋。编译器可以合法地将该读取提升到循环之外,从而使代码变成死循环。应修改该类,使其使用适当的同步(包括 wait 和 notify 调用)。
TLW:持有两个锁进行等待(TLW_TWO_LOCK_WAIT)
持有两个锁的同时等待监视器可能导致死锁。执行 wait 只会释放被等待对象上的锁,而不会释放其他任何锁。这不一定是 bug,但值得仔细检查。
UW:无条件等待(UW_UNCOND_WAIT)
此方法中包含一处 java.lang.Object.wait() 调用,该调用没有被条件控制流所保护。代码在调用 wait 之前应当先确认它所要等待的条件尚未满足;否则之前的任何通知都会被忽略。
UG:未同步的 get 方法,同步的 set 方法(UG_SYNC_SET_UNSYNC_GET)
该类中包含名称相似的 get 和 set 方法,其中 set 方法是同步的,而 get 方法不是。这可能导致运行时行为不正确,因为 get 方法的调用者未必能看到对象的一致状态。应当将 get 方法改为同步方法。
IS:字段未针对并发访问加以保护(IS_FIELD_NOT_GUARDED)
该字段被标注了 net.jcip.annotations.GuardedBy 或 javax.annotation.concurrent.GuardedBy,但其访问方式似乎违反了这些注解的约束。
ML:对字段进行同步,试图以此保护该字段(徒劳无功)(ML_SYNC_ON_FIELD_TO_GUARD_CHANGING_THAT_FIELD)
此方法对某个字段进行同步,看起来是试图防止该字段被同时更新。但是,保护一个字段实际上是获取被引用对象上的锁,而不是该字段上的锁。这可能无法提供你所需要的互斥性,而且其他线程也可能(出于其他目的)在获取被引用对象上的锁。这种模式的例子如下:
private Long myNtfSeqNbrCounter = new Long(0);
private Long getNotificationSequenceNumber() {
Long result = null;
synchronized(myNtfSeqNbrCounter) {
result = new Long(myNtfSeqNbrCounter.longValue() + 1);
myNtfSeqNbrCounter = new Long(result.longValue());
}
return result;
}ML:方法在已更新的字段上同步(ML_SYNC_ON_UPDATED_FIELD)
此方法对一个来自可变字段所引用的对象进行同步。这不太可能具有有意义的语义,因为不同的线程可能在不同的对象上进行同步。
WS:类的 writeObject() 方法是同步的,但其他方法不是(WS_WRITEOBJECT_SYNC)
此类具有一个 writeObject() 方法,它是同步的;然而,该类的其他任何方法都不是同步的。
RS:类的 readObject() 方法是同步的(RS_READOBJECT_SYNC)
这个可序列化类定义了一个同步的 readObject()。按照定义,由反序列化创建的对象只能被一个线程访问,因此 readObject() 没有必要是同步的。如果 readObject() 方法本身使该对象对其他线程可见,那是一种非常可疑的编码风格。
SC:构造方法调用 Thread.start()(SC_START_IN_CTOR)
构造方法启动了一个线程。如果该类曾经被继承/派生,这很可能是错误的,因为线程会在子类构造方法开始执行之前就被启动。
Wa:Wait 不在循环中(WA_NOT_IN_LOOP)
此方法包含一个不在循环中的 java.lang.Object.wait() 调用。如果监视器用于多个条件,调用者期望等待的条件可能并非实际发生的那个。
Wa:Condition.await() 不在循环中(WA_AWAIT_NOT_IN_LOOP)
此方法包含一个不在循环中的 java.util.concurrent.await()(或其变体)调用。如果该对象用于多个条件,调用者期望等待的条件可能并非实际发生的那个。
No:使用 notify() 而非 notifyAll()(NO_NOTIFY_NOT_NOTIFYALL)
此方法调用 notify() 而不是 notifyAll()。Java 监视器经常用于多个条件。调用 notify() 只会唤醒一个线程,这意味着被唤醒的线程可能不是等待调用者刚刚满足的那个条件的线程。
UL:方法并非在所有路径上都释放锁(UL_UNRELEASED_LOCK)
此方法获取了一个 JSR-166(java.util.concurrent)锁,但并非在方法的所有退出路径上都释放它。一般来说,使用 JSR-166 锁的正确惯用写法是:
Lock l = ...;
l.lock();
try {
// do something
} finally {
l.unlock();
}UL:方法未在所有异常路径上释放锁(UL_UNRELEASED_LOCK_EXCEPTION_PATH)
此方法获取了一个 JSR-166(java.util.concurrent)锁,但未在方法的所有异常退出路径上释放该锁。通常,使用 JSR-166 锁的正确惯用写法如下:
Lock l = ...;
l.lock();
try {
// do something
} finally {
l.unlock();
}MWN: 不匹配的 wait() (MWN_MISMATCHED_WAIT)
此方法在没有明显持有对象锁的情况下调用了 Object.wait()。在未持有锁的情况下调用 wait() 将导致抛出 IllegalMonitorStateException。
MWN: 不匹配的 notify() (MWN_MISMATCHED_NOTIFY)
此方法在没有明显持有对象锁的情况下调用了 Object.notify() 或 Object.notifyAll()。在未持有锁的情况下调用 notify() 或 notifyAll() 将导致抛出 IllegalMonitorStateException。
LI: 静态字段的不正确延迟初始化 (LI_LAZY_INIT_STATIC)
此方法包含对非 volatile 静态字段的未同步延迟初始化。由于编译器或处理器可能对指令进行重排序,如果该方法可能被多个线程调用,则无法保证线程看到的是完全初始化的对象。您可以将该字段声明为 volatile 来纠正此问题。有关更多信息,请参见 Java 内存模型网站。
LI: 静态字段的不正确延迟初始化和更新 (LI_LAZY_INIT_UPDATE_STATIC)
此方法包含对静态字段的未同步延迟初始化。字段被设置后,存储到该位置的对象还会被进一步更新或访问。字段一旦设置,其他线程立即可见。如果设置该字段的方法中后续的访问操作用于初始化该对象,那么您就存在一个_非常严重_的多线程缺陷,除非有其他机制能阻止任何其他线程在对象完全初始化之前访问它。
即使您确信该方法绝不会被多个线程调用,在您要赋给静态字段的值完全填充/初始化之前不要设置该静态字段,可能也是更好的做法。
JLM: 在 util.concurrent 实例上执行同步 (JLM_JSR166_UTILCONCURRENT_MONITORENTER)
此方法在一个属于 java.util.concurrent 包中某个类(或其子类)实例的对象上执行同步。这些类的实例拥有自身的并发控制机制,它们与 Java 关键字 synchronized 提供的同步机制相互独立。例如,在 AtomicBoolean 上进行同步并不能阻止其他线程修改 AtomicBoolean。
这样的代码可能是正确的,但应当经过仔细审查并加以文档说明,否则可能会让日后维护该代码的人感到困惑。
JLM: 在 util.concurrent 抽象上使用监视器风格的 wait 方法 (JML_JSR166_CALLING_WAIT_RATHER_THAN_AWAIT)
此方法在一个同时提供了 await()、signal()、signalAll() 方法的对象上调用了 wait()、notify() 或 notifyAll()(例如 util.concurrent 的 Condition 对象)。这大概不是您想要的效果,而且即使您确实想要这样做,也应当考虑重新设计,因为其他开发者会觉得这极其令人困惑。
JLM: 在 Lock 上执行同步 (JLM_JSR166_LOCK_MONITORENTER)
此方法在一个实现了 java.util.concurrent.locks.Lock 的对象上进行同步。这样的对象是通过 acquire()/release() 来锁定/解锁的,而不是使用 synchronized (...) 结构。
SWL:持有锁时调用 Thread.sleep()(SWL_SLEEP_WITH_LOCK_HELD)
此方法在持有锁的情况下调用了 Thread.sleep()。由于其他线程可能正在等待获取该锁,这可能导致非常差的性能与可伸缩性,甚至导致死锁。更好的做法是在该锁上调用 wait(),它会释放锁并允许其他线程运行。
RV:putIfAbsent 的返回值被忽略,传给 putIfAbsent 的值被重复使用(RV_RETURN_VALUE_OF_PUTIFABSENT_IGNORED)
putIfAbsent 方法通常用于确保单个值与给定键相关联(即 put if absent 成功时对应的那个值)。如果你忽略返回值并保留传入值的引用,就有可能保留的并非映射中与该键关联的那个值。如果你的逻辑取决于使用哪个值,而你使用了未存储在映射中的那个,程序的行为就会不正确。
SSD:在共享的静态数据上使用了实例级锁(SSD_DO_NOT_USE_INSTANCE_LOCK_ON_SHARED_STATIC_DATA)
如果用于修改该静态字段的锁或同步方法不是静态的,那么共享的静态数据可能无法免受并发访问的影响。这可能以两种方式发生:同步方法使用了非静态的锁对象,或者同步方法被声明为非静态。这两种方式都是无效的。最佳解决方案是使用私有静态 final 的锁对象来保护共享的静态数据。
参见 SEI CERT 规则 LCK06-J. Do not use an instance lock to protect shared static data.
虚假的随机噪声(NOISE)
虚假的随机噪声:旨在作为数据挖掘实验中的对照,而不是用于发现软件中的实际缺陷。
NOISE:关于空指针解引用的虚假警告(NOISE_NULL_DEREFERENCE)
虚假警告。
NOISE:关于方法调用的虚假警告(NOISE_METHOD_CALL)
虚假警告。
NOISE:关于字段引用的虚假警告(NOISE_FIELD_REFERENCE)
虚假警告。
NOISE:关于某项操作的虚假警告(NOISE_OPERATION)
虚假警告。
性能(PERFORMANCE)
不一定不正确、但可能效率低下的代码
HSC:巨大的字符串常量在多个类文件中重复(HSC_HUGE_SHARED_STRING_CONSTANT)
一个大型 String 常量在多个类文件中重复出现。这通常是因为某个 final 字段被初始化为字符串常量,而 Java 语言规定,来自其他类的所有对该 final 字段的引用都必须内联到那个类文件中。关于 JDK 中该缺陷的一个实例及其解决后如何将 JDK 大小缩减 1 兆字节的说明,参见 JDK bug 6447475。
Dm:URL 的 equals 和 hashCode 方法存在阻塞(DMI_BLOCKING_METHODS_ON_URL)
URL 的 equals 和 hashCode 方法会执行域名解析,这可能导致严重的性能损失。更多信息参见 http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html,建议改用 java.net.URI。
Dm:URL 的集合可能成为性能瓶颈(DMI_COLLECTION_OF_URLS)
该方法或字段使用了或依赖于由 URL 构成的 Map 或 Set。由于 URL 的 equals 和 hashCode 方法都会执行域名解析,这可能导致严重的性能损失。更多信息参见 http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html,建议改用 java.net.URI。
Dm:方法调用了低效的 new String(String) 构造函数(DM_STRING_CTOR)
使用 java.lang.String(String) 构造函数会浪费内存,因为由此创建的对象与作为参数传入的 String 在功能上完全无法区分。请直接使用参数 String。
Dm:方法调用了低效的 new String() 构造函数(DM_STRING_VOID_CTOR)
使用无参构造函数创建新的 java.lang.String 对象会浪费内存,因为由此创建的对象与空字符串常量 "" 在功能上完全无法区分。Java 保证相同的字符串常量由同一个 String 对象表示。因此,应该直接使用空字符串常量。
Dm:方法对 String 调用了 toString() 方法(DM_STRING_TOSTRING)
调用 String.toString() 是多余的操作,直接使用该 String 即可。
Dm:显式垃圾回收;除了基准测试代码外都极其可疑(DM_GC)
代码中显式调用了垃圾回收。除非在基准测试等特定场景下使用,否则这种做法非常值得怀疑。
过去,人们在 close 或 finalize 等方法中显式调用垃圾回收器的情况曾导致巨大的性能黑洞。垃圾回收本身是有开销的。任何强制触发成百上千次垃圾回收的场景都会使机器陷入卡顿。
Dm:方法调用了低效的 Boolean 构造函数;请改用 Boolean.valueOf(…)(DM_BOOLEAN_CTOR)
创建 java.lang.Boolean 的新实例会浪费内存,因为 Boolean 对象是不可变的,且该类型只有两个有效值。请改用 Boolean.valueOf() 方法(或 Java 5 的自动装箱)来创建 Boolean 对象。
Bx:方法调用了低效的 Number 构造函数;请改用静态 valueOf(DM_NUMBER_CTOR)
使用 new Integer(int) 总是会产生一个新对象,而 Integer.valueOf(int) 允许由编译器、类库或 JVM 对值进行缓存。使用缓存的值可以避免对象分配,从而使代码运行更快。
-128 到 127 之间的值保证存在对应的缓存实例,使用 valueOf 比使用构造函数快约 3.5 倍。对于超出该常量范围的值,两种方式的性能相同。
除非该类必须与 Java 5 之前的 JVM 兼容,否则在创建 valueOf()、Long、Integer、Short 和 Character 的实例时,请使用自动装箱或 Byte 方法。
Bx:方法调用了低效的浮点 Number 构造函数,请改用静态 valueOf(DM_FP_NUMBER_CTOR)
使用 new Double(double) 始终会创建一个新对象,而 Double.valueOf(double) 允许编译器、类库或 JVM 对值进行缓存。使用缓存的值可以避免对象分配,从而使代码运行得更快。
除非该类必须与 Java 5 之前的 JVM 保持兼容,否则在创建 Double 和 Float 的实例时,请使用自动装箱或 valueOf() 方法。
Bx:方法仅为调用 toString 而分配了一个装箱基本类型(DM_BOXED_PRIMITIVE_TOSTRING)
仅为调用 toString() 而分配了一个装箱基本类型。直接使用接受基本类型值的静态 toString 形式会更有效。因此,
| 将……替换为 | 使用…… |
|---|---|
| new Integer(1).toString() | Integer.toString(1) |
| new Long(1).toString() | Long.toString(1) |
| new Float(1.0).toString() | Float.toString(1.0) |
| new Double(1.0).toString() | Double.toString(1.0) |
| new Byte(1).toString() | Byte.toString(1) |
| new Short(1).toString() | Short.toString(1) |
| new Boolean(true).toString() | Boolean.toString(true) |
Bx:为解析基本类型而进行装箱/拆箱(DM_BOXED_PRIMITIVE_FOR_PARSING)
从 String 创建了一个装箱基本类型,只为取出其中的拆箱基本类型值。直接调用静态 parseXXX 方法会更高效。
Bx:为进行比较而装箱基本类型(DM_BOXED_PRIMITIVE_FOR_COMPARE)
仅为调用 compareTo() 方法而创建了一个装箱基本类型。使用静态 compare 方法(double 和 float 自 Java 1.4 起提供,其他基本类型自 Java 7 起提供)会更高效,该方法直接对基本类型进行操作。
Bx:基本类型值被拆箱并强制转换以用于三元运算符(BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR)
在条件三元运算符(即 b ? e1 : e2 运算符)的求值过程中,包装的基本类型值被拆箱并转换为另一种基本类型。Java 的语义规定,如果 e1 和 e2 是包装的数值,则这些值会被拆箱并转换/强制转换为其公共类型(例如,如果 e1 的类型是 Integer,e2 的类型是 Float,则 e1 会被拆箱、转换为浮点值并重新装箱)。参见 JLS 第 15.25 节。
Bx:装箱值被拆箱后立即重新装箱(BX_UNBOXING_IMMEDIATELY_REBOXED)
一个装箱值被拆箱后立即被重新装箱。
Bx:基本类型值被装箱后立即拆箱(BX_BOXING_IMMEDIATELY_UNBOXED)
一个基本类型被装箱,然后立即被拆箱。这很可能是因为在需要拆箱值的地方进行了手动装箱,从而迫使编译器立即撤销装箱所做的工作。
Bx:基本类型值被装箱后拆箱以执行基本类型强制转换(BX_BOXING_IMMEDIATELY_UNBOXED_TO_PERFORM_COERCION)
构造了一个基本类型的装箱值,随后立即将其转换为不同的基本类型(例如 new Double(d).intValue())。请直接执行基本类型强制转换(例如 (int) d)。
Dm: 方法分配对象仅为获取类对象(DM_NEW_FOR_GETCLASS)
此方法分配一个对象只是为了在其上调用 getClass(),以获取对应的 Class 对象。直接访问该类的 .class 属性会更简单。
Dm: 使用 Random 的 nextInt 方法而非 nextDouble 来生成随机整数(DM_NEXTINT_VIA_NEXTDOUBLE)
如果 r 是一个 java.util.Random,你可以使用 0 来生成 n-1 到 r.nextInt(n) 之间的随机数,而无需使用 (int)(r.nextDouble() * n)。
nextInt 的参数必须为正数。例如,如果你想生成 -99 到 0 之间的随机值,请使用 -r.nextInt(100)。
SS: 未读取的字段:该字段是否应为 static?(SS_SHOULD_BE_STATIC)
此类包含一个实例 final 字段,其初始值为编译时静态值。请考虑将该字段声明为 static。
UuF: 未使用的字段(UUF_UNUSED_FIELD)
此字段从未被使用。请考虑将其从类中移除。
UrF: 未读取的字段(URF_UNREAD_FIELD)
此字段从未被读取。请考虑将其从类中移除。
SIC: 应为静态内部类(SIC_INNER_SHOULD_BE_STATIC)
此类是一个内部类,但并未使用其对创建它的对象的内嵌引用。该引用会使类的实例占用更多内存,并可能使对创建者对象的引用比必要的存活更久。如有可能,应将此类设为 static。
SIC: 可重构为静态内部类(SIC_INNER_SHOULD_BE_STATIC_NEEDS_THIS)
此类是一个内部类,除了在构造内部对象期间外,并未使用其对创建它的对象的内嵌引用。该引用会使类的实例占用更多内存,并可能使对创建者对象的引用比必要的存活更久。如有可能,应将此类重构为 静态 内部类。由于在构造内部实例期间需要对外部对象的引用,因此需要对内部类进行重构,将外部实例的引用传递给内部类的构造函数。
SIC: 可重构为具名静态内部类(SIC_INNER_SHOULD_BE_STATIC_ANON)
此类是一个内部类,但并未使用其对创建它的对象的内嵌引用。该引用会使类的实例占用更多内存,并可能使对创建者对象的引用比必要的存活更久。如有可能,应将此类重构为 静态 内部类。由于匿名内部类无法标记为 static,因此这样做需要对内部类进行重构,使其成为一个具名内部类。
UPM: 私有方法从未被调用(UPM_UNCALLED_PRIVATE_METHOD)
此私有方法从未被调用。虽然该方法有可能通过反射被调用,但更有可能它从未被使用,应当将其移除。
SBSC: 方法在循环中使用 + 拼接字符串(SBSC_USE_STRINGBUFFER_CONCATENATION)
该方法似乎在循环中使用字符串拼接来构建字符串。在每次迭代中,字符串都会被转换为 StringBuffer/StringBuilder,进行追加,然后再转换回 String。这可能导致代价与迭代次数成平方关系,因为不断增长的字符串在每次迭代中都会被重新复制。
通过显式使用 StringBuffer(或 Java 5 中的 StringBuilder)可以获得更好的性能。
例如:
// This is bad
String s = "";
for (int i = 0; i < field.length; ++i) {
s = s + field[i];
}
// This is better
StringBuffer buf = new StringBuffer();
for (int i = 0; i < field.length; ++i) {
buf.append(field[i]);
}
String s = buf.toString();IIL: 在循环中调用 NodeList.getLength()(IIL_ELEMENTS_GET_LENGTH_IN_LOOP)
该方法在循环内部调用 NodeList.getLength(),而这个 NodeList 是由 getElementsByTagName 调用产生的。该 NodeList 并不存储其长度,而是每次都以不太高效的方式重新计算。建议在循环之前先将长度保存到变量中。
IIO: 在循环中调用 prepareStatement(IIL_PREPARE_STATEMENT_IN_LOOP)
该方法在循环内部调用 Connection.prepareStatement 并传入常量参数。如果该 PreparedStatement 需要执行多次,就没有必要在每次循环迭代时都重新创建它。请将该调用移到循环之外。
IIO: 在循环中调用 Pattern.compile(IIL_PATTERN_COMPILE_IN_LOOP)
该方法在循环内部调用 Pattern.compile 并传入常量参数。如果该 Pattern 需要使用多次,就没有必要在每次循环迭代时都编译它。请将该调用移到循环之外,甚至可以移入 static final 字段。
IIO: 在循环中编译正则表达式(IIL_PATTERN_COMPILE_IN_LOOP_INDIRECT)
该方法在循环内部创建相同的正则表达式,因此每次迭代都会重新编译它。更高效的做法是在循环之外使用 Pattern.compile 预先编译该正则表达式。
IIO: 低效地使用 String.indexOf(String)(IIO_INEFFICIENT_INDEX_OF)
此代码向 String.indexOf() 传入了一个长度为 1 的常量字符串。使用 String.indexOf() 的整数形式会更高效。例如,调用 myString.indexOf('.') 而不是 myString.indexOf(".")
IIO: 低效地使用 String.lastIndexOf(String)(IIO_INEFFICIENT_LAST_INDEX_OF)
此代码向 String.lastIndexOf() 传入了一个长度为 1 的常量字符串。使用 String.lastIndexOf() 的整数形式会更高效。例如,调用 myString.lastIndexOf('.') 而不是 myString.lastIndexOf(".")
ITA: 方法使用了带零长度数组参数的 toArray()(ITA_INEFFICIENT_TO_ARRAY)
该方法使用了集合派生类的 toArray() 方法,并传入了一个零长度的原型数组参数。使用 myCollection.toArray(new Foo[myCollection.size()]) 会更高效。如果传入的数组足够大,能够存储集合中的所有元素,那么它会被直接填充并返回。这样就避免了需要通过反射创建第二个数组来作为返回结果。
WMI: 使用 keySet 迭代器而非 entrySet 迭代器导致的低效(WMI_WRONG_MAP_ITERATOR)
该方法使用从 keySet 迭代器中检索到的键来访问 Map 条目的值。使用 Map 的 entrySet 迭代器会更高效,可以避免 Map.get(key) 查找。
UM: 在常量值上调用静态 Math 类的方法(UM_UNNECESSARY_MATH)
该方法在常量值上使用了 java.lang.Math 的静态方法。在这种情况下,该方法的结果可以在静态时确定,直接使用常量会更快,有时也更精确。检测到的方法包括:
| 方法 | 参数 |
|---|---|
| abs | -任意- |
| acos | 0.0 或 1.0 |
| asin | 0.0 或 1.0 |
| atan | 0.0 或 1.0 |
| atan2 | 0.0 |
| cbrt | 0.0 或 1.0 |
| ceil | -任意- |
| cos | 0.0 |
| cosh | 0.0 |
| exp | 0.0 或 1.0 |
| expm1 | 0.0 |
| floor | -任意- |
| log | 0.0 或 1.0 |
| log10 | 0.0 或 1.0 |
| rint | -任意- |
| round | -任意- |
| sin | 0.0 |
| sinh | 0.0 |
| sqrt | 0.0 或 1.0 |
| tan | 0.0 |
| tanh | 0.0 |
| toDegrees | 0.0 或 1.0 |
| toRadians | 0.0 |
IMA:方法访问所属类的私有成员变量(IMA_INEFFICIENT_MEMBER_ACCESS)
内部类的这个方法会读取或写入所属类的私有成员变量,或者调用所属类的私有方法。编译器必须生成一个特殊方法来访问该私有成员,这会导致效率降低。放宽该成员变量或方法的访问保护级别,可使编译器将其视为普通访问。
安全(SECURITY)
以可能产生可远程利用的安全漏洞的方式使用不可信输入。
XSS:Servlet 错误页面中的反射型跨站脚本漏洞(XSS_REQUEST_PARAMETER_TO_SEND_ERROR)
此代码直接将 HTTP 参数写入服务器错误页面(使用 HttpServletResponse.sendError)。回显该不可信输入会带来反射型跨站脚本漏洞。有关更多信息,请参见 http://en.wikipedia.org/wiki/Cross-site_scripting。
SpotBugs 只查找最明显、最直白的跨站脚本案例。如果 SpotBugs 找到了_任何_一个,你_几乎肯定_还有更多 SpotBugs 未报告的跨站脚本漏洞。如果你担心跨站脚本问题,应认真考虑使用商业静态分析或渗透测试工具。
XSS:Servlet 反射型跨站脚本漏洞(XSS_REQUEST_PARAMETER_TO_SERVLET_WRITER)
此代码直接将 HTTP 参数写入 Servlet 输出,从而带来反射型跨站脚本漏洞。有关更多信息,请参见 http://en.wikipedia.org/wiki/Cross-site_scripting。
SpotBugs 只查找最明显、最直白的跨站脚本案例。如果 SpotBugs 找到了_任何_一个,你_几乎肯定_还有更多 SpotBugs 未报告的跨站脚本漏洞。如果你担心跨站脚本问题,应认真考虑使用商业静态分析或渗透测试工具。
XSS:JSP 反射型跨站脚本漏洞(XSS_REQUEST_PARAMETER_TO_JSP_WRITER)
此代码直接将 HTTP 参数写入 JSP 输出,从而带来跨站脚本漏洞。有关更多信息,请参见 http://en.wikipedia.org/wiki/Cross-site_scripting。
SpotBugs 只查找最明显、最直白的跨站脚本案例。如果 SpotBugs 找到了_任何_一个,你_几乎肯定_还有更多 SpotBugs 未报告的跨站脚本漏洞。如果你担心跨站脚本问题,应认真考虑使用商业静态分析或渗透测试工具。
HRS:由不可信输入形成的 HTTP Cookie(HRS_REQUEST_PARAMETER_TO_COOKIE)
此代码使用不可信的 HTTP 参数构造 HTTP Cookie。如果该 Cookie 被添加到 HTTP 响应中,将导致 HTTP 响应拆分漏洞。详见 http://en.wikipedia.org/wiki/HTTP_response_splitting。
SpotBugs 只检测最明显、最直白的 HTTP 响应拆分情况。如果 SpotBugs 发现了 任何 一个,那么你 几乎肯定 还存在更多 SpotBugs 未报告的漏洞。如果你担心 HTTP 响应拆分问题,应当认真考虑使用商业静态分析或渗透测试工具。
PT:Servlet 中的绝对路径遍历(PT_ABSOLUTE_PATH_TRAVERSAL)
软件使用 HTTP 请求参数构造本应位于受限目录内的路径名,但未正确中和诸如 "/abs/path" 之类的绝对路径序列,这些序列可能解析到该目录之外的位置。详见 http://cwe.mitre.org/data/definitions/36.html。
SpotBugs 只检测最明显、最直白的绝对路径遍历情况。如果 SpotBugs 发现了 任何 一个,那么你 几乎肯定 还存在更多 SpotBugs 未报告的漏洞。如果你担心绝对路径遍历问题,应当认真考虑使用商业静态分析或渗透测试工具。
PT:Servlet 中的相对路径遍历(PT_RELATIVE_PATH_TRAVERSAL)
软件使用 HTTP 请求参数构造本应位于受限目录内的路径名,但未正确中和诸如 ".." 之类的序列,这些序列可能解析到该目录之外的位置。详见 http://cwe.mitre.org/data/definitions/23.html。
SpotBugs 只检测最明显、最直白的相对路径遍历情况。如果 SpotBugs 发现了 任何 一个,那么你 几乎肯定 还存在更多 SpotBugs 未报告的漏洞。如果你担心相对路径遍历问题,应当认真考虑使用商业静态分析或渗透测试工具。
Dm:硬编码的常量数据库密码(DMI_CONSTANT_DB_PASSWORD)
此代码使用硬编码的常量密码创建数据库连接。任何能够访问源代码或编译后代码的人都可以轻易得知该密码。
Dm:空数据库密码(DMI_EMPTY_DB_PASSWORD)
此代码使用空白或空密码创建数据库连接。这表明该数据库未设置密码保护。
SQL:将非常量字符串传递给 SQL 语句的 execute 或 addBatch 方法(SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE)
该方法使用一个看起来是动态生成的 String 来调用 SQL 语句的 execute 或 addBatch 方法。建议改用预编译语句(prepared statement),它效率更高,且不易受到 SQL 注入攻击。
SQL:由非常量 String 生成预编译语句(SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING)
代码从一个非常量字符串创建 SQL 预处理语句。如果不对这种情况加以检查,构建该字符串时可能会用到来自用户的受污染数据,从而可能导致 SQL 注入,使预处理语句执行非预期的、不良的操作。
ASE:断言中的表达式可能产生副作用(ASE_ASSERTION_WITH_SIDE_EFFECT)
断言中使用的表达式不得产生副作用。
详见 SEI CERT Rule EXP06。
ASE:断言中调用的方法可能产生副作用(ASE_ASSERTION_WITH_SIDE_EFFECT_METHOD)
断言中使用的表达式不得产生副作用。
详见 SEI CERT Rule EXP06。
可疑代码(STYLE)
令人困惑、异常或写法本身容易引发错误的代码。例如:无效的局部变量赋值、switch 贯穿、未经确认的类型转换,以及对已知为 null 的值进行多余的 null 检查。本类别接受更多的误报。在早期版本的 SpotBugs 中,该类别被称为 Style。
CAA:对字段进行协变数组赋值(CAA_COVARIANT_ARRAY_FIELD)
将协变类型的数组赋值给字段。这会令人困惑,并且如果之后有其他类型的引用被存入该数组,可能在运行时导致 ArrayStoreException,例如以下代码:
Number[] arr = new Integer[10];
arr[0] = 1.0;考虑更改所创建数组的类型或该字段的类型。
CAA:方法返回协变数组(CAA_COVARIANT_ARRAY_RETURN)
方法返回了协变类型的数组。这容易造成混淆,并且如果调用方代码尝试将其他类型的引用存入返回的数组中,可能会在运行时引发 ArrayStoreException。
考虑更改所创建数组的类型或方法的返回类型。
CAA:将协变数组赋值给局部变量(CAA_COVARIANT_ARRAY_LOCAL)
将协变类型的数组赋值给了局部变量。这容易造成混淆,并且如果稍后向该数组中存入其他类型的引用,可能会在运行时引发 ArrayStoreException,例如以下代码:
Number[] arr = new Integer[10];
arr[0] = 1.0;考虑更改所创建数组的类型或局部变量的类型。
Dm:调用了不受支持的方法(DMI_UNSUPPORTED_METHOD)
该方法调用的所有目标都会抛出 UnsupportedOperationException。
Dm:在需要 Runnable 的地方传入了 Thread(DMI_THREAD_PASSED_WHERE_RUNNABLE_EXPECTED)
一个 Thread 对象被作为参数传递给了期望接收 Runnable 的方法。这相当不寻常,可能表明存在逻辑错误,或导致意外的行为。
NP:未进行空值检查即解引用 readLine() 的结果(NP_DEREFERENCE_OF_READLINE_VALUE)
调用 readLine() 的结果在未检查其是否为 null 的情况下就被解引用了。如果没有更多文本行可读取,readLine() 将返回 null,对其解引用会产生空指针异常。
NP:立即解引用 readLine() 的结果(NP_IMMEDIATE_DEREFERENCE_OF_READLINE)
调用 readLine() 的结果被立即解引用。如果没有更多文本行可读取,readLine() 将返回 null,对其解引用会产生空指针异常。
RV:32 位有符号随机整数的余数(RV_REM_OF_RANDOM_INT)
这段代码生成一个有符号的随机整数,然后计算该值对另一个值取模的余数。由于随机数可能为负,余数运算的结果也可能为负。请确认这是预期的行为,并强烈建议改用 Random.nextInt(int) 方法。
RV:hashCode 的余数可能为负(RV_REM_OF_HASHCODE)
这段代码计算出一个 hashCode,然后计算该值对另一个值取模的余数。由于 hashCode 可能为负,余数运算的结果也可能为负。
如果你希望确保计算结果为非负值,可能需要修改代码。如果已知除数是 2 的幂,可以改用按位与运算符(即,不使用 x.hashCode()%n,而使用 x.hashCode()&(n-1))。这可能比计算余数更快。如果不知道除数是否为 2 的幂,可以对余数运算的结果取绝对值(即,使用 Math.abs(x.hashCode()%n))。
Eq:不寻常的 equals 方法(EQ_UNUSUAL)
该类没有采用我们所识别的任何用于检查参数类型是否与 this 对象类型兼容的模式。这段代码可能没有问题,但值得审查。
Eq:类未覆盖父类的 equals(EQ_DOESNT_OVERRIDE_EQUALS)
此类扩展了一个定义了 equals 方法的类并添加了字段,但其本身并未定义 equals 方法。因此,该类实例之间的相等性比较会忽略子类的身份以及所添加的字段。请确认这正是您所期望的行为,并确认是否需要覆盖 equals 方法。即使不需要覆盖 equals 方法,也建议考虑覆盖它,以便明确文档化这样一个事实:该子类的 equals 方法只需返回调用 super.equals(o) 的结果。
NS:非短路逻辑的可疑使用(NS_NON_SHORT_CIRCUIT)
这段代码似乎使用了非短路逻辑(如 & 或 |),而不是短路逻辑(&& 或 ||)。非短路逻辑会导致表达式两侧都被求值,即使仅凭左侧的结果就能推断出最终结果。这可能降低效率,并且当左侧用于保护那些对右侧求值可能产生错误的情况时,还可能导致错误。
详见 Java 语言规范。
NS:非短路逻辑的潜在危险使用(NS_DANGEROUS_NON_SHORT_CIRCUIT)
这段代码似乎使用了非短路逻辑(如 & 或 |),而不是短路逻辑(&& 或 ||)。此外,根据左侧的取值,可能你不希望对右侧求值,因为它可能产生副作用、引发异常或代价高昂。
非短路逻辑会导致表达式两侧都被求值,即使仅凭左侧的结果就能推断出最终结果。这可能降低效率,并且当左侧用于保护那些对右侧求值可能产生错误的情况时,还可能导致错误。
详见 Java 语言规范。
IC:初始化循环(IC_INIT_CIRCULARITY)
在该缺陷实例所引用的两个类的静态初始化器中检测到了循环依赖。此类循环可能引发多种意想不到的行为。
IA:继承方法或外部方法的调用可能存在歧义(IA_AMBIGUOUS_INVOCATION_OF_INHERITED_OR_OUTER_METHOD)
内部类正在调用一个既可能解析为继承方法、也可能解析为外部类中定义的方法的方法。例如,你调用了 foo(17),而它同时在父类和外部方法中都有定义。按照 Java 的语义,它将解析为调用继承的方法,但这可能并非你的本意。
如果你确实打算调用继承的方法,请通过在 super 上调用该方法来实现(例如,调用 super.foo(17)),这样代码的其他读者以及 SpotBugs 就能清楚地知道你想要调用的是继承的方法,而不是外部类中的方法。
如果你调用 this.foo(17),那么继承来的方法将被调用。但是,由于 SpotBugs 只查看 class 文件,它无法区分 this.foo(17) 和 foo(17) 的调用,因此仍然会报告可能存在歧义的调用。
Se: 私有 readResolve 方法未被子类继承(SE_PRIVATE_READ_RESOLVE_NOT_INHERITED)
该类定义了一个私有的 readResolve 方法。由于它是私有的,因此不会被子类继承。这可能是有意为之且没问题,但应当加以审查以确认这确实是预期的行为。
Se: 非 Serializable 类中的 transient 字段(SE_TRANSIENT_FIELD_OF_NONSERIALIZABLE_CLASS)
该字段被标记为 transient,但该类并未实现 Serializable,因此将其标记为 transient 完全没有任何作用。这可能是代码早期版本中该类是 Serializable 时留下的标记,也可能是对序列化工作方式的误解。
仅当设置了特殊选项 reportTransientFieldOfNonSerializableClass 时才会报告此问题。
SF: 发现 switch 语句中某个 case 落入下一个 case(SF_SWITCH_FALLTHROUGH)
此方法包含一个 switch 语句,其中某个 case 分支会落入下一个 case。通常你需要用 break 或 return 来结束该 case。
SF: 发现 switch 语句中缺少 default 分支(SF_SWITCH_NO_DEFAULT)
此方法包含一个 switch 语句,其中缺少 default 分支。通常你需要提供一个 default 分支。
由于该分析只查看生成的字节码,如果 default 分支位于 switch 语句的末尾,且 switch 语句中没有针对其他 case 的 break 语句,该警告可能会被错误触发。
UuF: 未使用的 public 或 protected 字段(UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD)
该字段从未被使用。该字段是 public 或 protected 的,因此它可能是打算供分析中未看到的类使用。如果不是这样,请考虑将其从类中移除。
UrF: 未被读取的 public/protected 字段(URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD)
该字段从未被读取。该字段是 public 或 protected 的,因此它可能是打算供分析中未看到的类使用。如果不是这样,请考虑将其从类中移除。
QF: for 循环中复杂、可疑或错误的增量(QF_QUESTIONABLE_FOR_LOOP)
你确定这个 for 循环递增/递减的是正确的变量吗?看起来 for 循环初始化和检查的是另一个变量。
NP: 读取从未写入的 public 或 protected 字段(NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD)
程序正在解引用一个 public 或 protected 字段,而该字段似乎从未被写入过非 null 的值。除非该字段通过某种分析未检测到的机制进行了初始化,否则解引用该值将产生空指针异常。
UwF: 字段未在构造函数中初始化却在未进行空值检查的情况下被解引用(UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR)
该字段从未在任何构造方法中初始化,因此对象构造完成后该字段可能为 null。在其他地方,该字段在未进行 null 检查的情况下被读取并解引用。这可能是一个错误,也可能是有问题的设计,因为这意味着如果在初始化之前解引用该字段,将会产生空指针异常。
UwF:未写入的公共或受保护字段(UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD)
未观察到对该公共/受保护字段的任何写入操作。对它的所有读取都将返回默认值。请检查是否存在错误(它本应被初始化吗?),或者如果它毫无用处则将其删除。
UC:无用的非空 void 方法(UC_USELESS_VOID_METHOD)
我们的分析表明,这个非空的 void 方法实际上没有执行任何有用的工作。请检查它:其代码中可能有错误,或者它的方法体可以被完全删除。
我们正在尽力减少误报,但在某些情况下,此警告可能是错误的。常见的误报情况包括:
- 该方法旨在触发某个类的加载,而该加载可能产生副作用。
- 该方法旨在隐式抛出某个罕见的异常。
UC:条件没有效果(UC_USELESS_CONDITION)
该条件的结果始终与之前已收窄的相关变量的值相同。可能原本想表达其他意思,或者该条件可以被删除。
UC:条件由于变量类型而没有效果(UC_USELESS_CONDITION_TYPE)
由于相关变量的类型取值范围,该条件的结果始终相同。可能原本想表达其他意思,或者该条件可以被删除。
UC:创建了无用的对象(UC_USELESS_OBJECT)
我们的分析表明该对象毫无用处。它被创建并被修改,但其值从未超出该方法的作用范围,也没有产生任何副作用。要么这里存在错误、该对象本应被使用,要么它可以被删除。
这种分析很少产生误报。常见的误报情况包括:
- 该对象用于隐式抛出某个罕见的异常。
- 该对象被用作桩(stub)以泛化代码。
- 该对象用于持有对弱引用/软引用对象的强引用。
UC:在栈上创建了无用的对象(UC_USELESS_OBJECT_STACK)
创建该对象只是为了执行一些不会产生任何副作用的修改操作。可能原本想表达其他意思,或者该对象可以被删除。
RV:方法忽略返回值,这样可以吗?(RV_RETURN_VALUE_IGNORED_INFERRED)
这段代码调用了一个方法并忽略了其返回值。该返回值的类型与调用该方法的对象类型相同,根据我们的分析,这个返回值可能是重要的(例如,就像忽略了 String.toLowerCase() 的返回值一样)。
我们仅通过简单分析方法体就推测,忽略返回值可能不是一个好主意。你可以使用 @CheckReturnValue 注解来指示 SpotBugs,忽略该方法的返回值究竟是不可接受的还是可以接受的。
请仔细调查以确定忽略返回值是否可以接受。
RV: 无副作用方法的返回值被忽略(RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT)
此代码调用了一个方法并忽略了其返回值。然而,我们的分析表明,该方法(包括其在子类中的任何实现)除了返回值之外不会产生任何其他效果。因此,这次调用可以被移除。
我们正尽可能地减少误报,但在某些情况下此警告可能是错误的。常见的误报情况包括:
- 该方法被设计为可被重写,并在超出本次分析范围的其他项目中产生副作用。
- 调用该方法是为了触发类加载,而类加载可能会产生副作用。
- 调用该方法只是为了获取某个异常。
如果你认为我们的假定不正确,可以使用 @CheckReturnValue 注解来指示 SpotBugs 忽略该方法的返回值是可以接受的。
RV: 方法检查 String.indexOf 的结果是否为正数(RV_CHECK_FOR_POSITIVE_INDEXOF)
该方法调用了 String.indexOf 并检查其结果是正数还是非正数。更典型的做法是检查结果是否为负数或非负数。只有当所查找的子串出现在字符串中非开头的位置时,结果才是正数。
RV: 方法在检查 readLine 的结果非空之后将其丢弃(RV_DONT_JUST_NULL_CHECK_READLINE)
在检查 readLine 的返回值是否非空之后,该返回值就被丢弃了。在几乎所有情况下,如果结果非空,你都会希望使用这个非空的值。再次调用 readLine 会得到另一行内容。
NP: 参数必须非空,却被标记为可空(NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE)
该参数的所有使用方式都要求它非空,但该参数却被显式标注为 Nullable。要么是参数的使用方式有误,要么是注解有误。
NP: 由于被调用方法的返回值,可能出现空指针解引用(NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE)
某个方法的返回值在未进行空值检查的情况下被解引用,而该方法的返回值通常应当进行空值判断。当代码执行时,这可能导致 NullPointerException。
NP: 在可能不可行的分支上可能出现空指针解引用(NP_NULL_ON_SOME_PATH_MIGHT_BE_INFEASIBLE)
存在一个分支语句,如果执行, 将保证某个 null 值被解引用,这会在代码运行时产生一个 NullPointerException。当然,问题也可能在于该分支或语句是不可达的,空指针异常根本不会被执行;判定这一点超出了 SpotBugs 的能力。由于该值此前已经进行过 null 检查,这种情况确实很可能发生。
NP:加载已知为 null 的值(NP_LOAD_OF_KNOWN_NULL_VALUE)
此处引用的变量因之前的 null 检查而被确定为 null。虽然这样做是合法的,但可能是一个错误(或许你本意是想引用另一个变量,又或者之前检查变量是否为 null 的判断本应是检查其是否非 null)。
PZLA:考虑返回长度为零的数组而非 null(PZLA_PREFER_ZERO_LENGTH_ARRAYS)
通常更好的设计是返回一个长度为零的数组,而不是返回 null 引用,以此表示没有结果(即结果列表为空)。这样,调用该方法的客户端就不需要进行显式的 null 检查。
另一方面,使用 null 来表示"这个问题没有答案"可能是恰当的。例如,File.listFiles() 在给定的目录不包含任何文件时返回一个空列表,而在该文件不是目录时返回 null。
UCF:无效的控制流(UCF_USELESS_CONTROL_FLOW)
此方法包含一个无效的控制流语句,无论是否进入该分支,控制流都会继续到同一个位置。例如,当 if 语句拥有一个空的语句块时,就会出现这种情况:
if (argv.length == 0) {
// TODO: handle this case
}UCF:无用的下一行控制流(UCF_USELESS_CONTROL_FLOW_NEXT_LINE)
此方法包含一条无用的控制流语句,无论是否执行分支,控制流都会继续到相同或下一行。这通常是由于无意中将空语句用作 if 语句的主体所导致的,例如:
if (argv.length == 1);
System.out.println("Hello, " + argv[0]);RCN: 对已知为 null 的值进行多余的判空(RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE)
此方法包含一处对已知为 null 的值与常量 null 进行的多余检查。
RCN: 对已知为非 null 的值进行多余的判空(RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE)
此方法包含一处对已知为非 null 的值与常量 null 进行的多余检查。
RCN: 对两个 null 值的多余比较(RCN_REDUNDANT_COMPARISON_TWO_NULL_VALUES)
此方法包含一处对两个已知肯定都为 null 的引用所进行的多余比较。
RCN: 将非 null 值与 null 进行多余比较(RCN_REDUNDANT_COMPARISON_OF_NULL_AND_NONNULL_VALUE)
此方法包含一个已知非 null 的引用与另一个已知为 null 的引用之间的比较。
SA: 局部变量自赋值(SA_LOCAL_SELF_ASSIGNMENT)
此方法包含一处局部变量的自赋值;例如
public void foo() {
int x = 3;
x = x;
}这类赋值毫无用处,并且可能表示存在逻辑错误或笔误。
INT: 整数对 1 取余 (INT_BAD_REM_BY_1)
任何表达式 (exp % 1) 都保证始终返回零。你是否想写成 (exp & 1) 或 (exp % 2)?
INT: 整数值的无意义比较 (INT_VACUOUS_COMPARISON)
此处存在一个始终返回相同结果的整数比较(例如 x <= Integer.MAX_VALUE)。
INT: 整数值的无意义位掩码运算 (INT_VACUOUS_BIT_OPERATION)
这是一个(与、或、异或)整数位运算,它没有执行任何有用的操作(例如 v & 0xffffffff)。
SA: 局部变量的重复赋值 (SA_LOCAL_DOUBLE_ASSIGNMENT)
该方法包含对同一局部变量的重复赋值;例如
public void foo() {
int x,y;
x = x = 17;
}对同一个变量重复赋值毫无意义,可能表明存在逻辑错误或拼写错误。
SA:字段的重复赋值(SA_FIELD_DOUBLE_ASSIGNMENT)
此方法包含对字段的重复赋值;例如
int x,y;
public void foo() {
x = x = 17;
}对同一字段赋值两次是没有意义的,可能表明存在逻辑错误或拼写错误。
DLS:return 语句中的无用赋值(DLS_DEAD_LOCAL_STORE_IN_RETURN)
该语句在 return 语句中对局部变量进行了赋值。这个赋值没有任何作用。请检查该语句的行为是否正确。
DLS:对局部变量的死存储(DLS_DEAD_LOCAL_STORE)
这条指令为局部变量赋了一个值,但该值从未在任何后续指令中被读取或使用。这通常表明存在错误,因为计算出的值从未被使用。
注意,Sun 的 javac 编译器经常会为 final 局部变量生成死存储。由于 SpotBugs 是一个基于字节码的工具,因此没有简单的方法来消除这些误报。
DLS:对遮蔽字段的局部变量的死存储(DLS_DEAD_LOCAL_STORE_SHADOWS_FIELD)
这条指令为局部变量赋了一个值,但该值从未在任何后续指令中被读取或使用。这通常表明存在错误,因为计算出的值从未被使用。存在一个与该局部变量同名的字段。您是否想改为对该字段赋值?
DLS:将 null 死存储到局部变量(DLS_DEAD_LOCAL_STORE_OF_NULL)
代码将 null 存入了一个局部变量,而存储的值从未被读取。这种存储最初可能是为了辅助垃圾回收器而引入的,但从 Java SE 6.0 起,它已不再必要,也不再有用。
REC:捕获了从未抛出的 Exception(REC_CATCH_EXCEPTION)
该方法使用了 try-catch 块来捕获 Exception 对象,但在 try 块中并未抛出 Exception,并且也没有显式捕获 RuntimeException。try { ... } catch (Exception e) { something } 是一种常见的缺陷模式,它作为简写形式用于捕获多种类型的异常(这些异常的 catch 块内容完全相同),但这种写法也会意外地捕获 RuntimeException,从而掩盖潜在的缺陷。
更好的做法是:要么显式捕获实际抛出的特定异常,要么显式捕获 RuntimeException 并重新抛出它,然后再捕获所有非 RuntimeException 的异常,如下所示:
try {
...
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
... deal with all non-runtime exceptions ...
}DCN:捕获到 NullPointerException(DCN_NULLPOINTER_EXCEPTION)
根据 SEI Cert 规则 ERR08-J,不应捕获 NullPointerException。处理 NullPointerException 被认为是不如进行空值检查的次等替代方案。
以下是非合规代码,它捕获 NullPointerException 来判断传入的参数是否为 null:
boolean hasSpace(String m) {
try {
String ms[] = m.split(" ");
return names.length != 1;
} catch (NullPointerException e) {
return false;
}
}合规的解决方案应使用空值检查,如下例所示:
boolean hasSpace(String m) {
if (m == null) return false;
String ms[] = m.split(" ");
return names.length != 1;
}FE:测试浮点数相等性(FE_FLOATING_POINT_EQUALITY)
此操作对两个浮点值进行相等性比较。由于浮点运算可能涉及舍入,计算得到的 float 和 double 值可能并不精确。对于必须精确的值(例如货币金额),请考虑使用 BigDecimal 等固定精度类型;对于无需精确的值,请考虑在某个范围内比较是否相等,例如:if ( Math.abs(x - y) < .0000001 )。参见《Java 语言规范》第 4.2.4 节。
CD:测试类之间的循环依赖(CD_CIRCULAR_DEPENDENCY)
该类与其他类之间存在循环依赖。这会使这些类的构建变得困难,因为每个类都依赖另一个类才能正确构建。考虑使用接口来打破这种强依赖。
RI:类实现了与父类相同的接口(RI_REDUNDANT_INTERFACES)
该类声明实现了一个其父类也已实现的接口。这是冗余的,因为一旦父类实现了某个接口,所有子类默认也会实现该接口。这可能表明自该类创建以来继承层次结构已发生变化,应当考虑该接口实现的归属问题。
MTIA:类继承了 Struts Action 类并使用实例变量(MTIA_SUSPECT_STRUTS_INSTANCE_FIELD)
该类继承自某个 Struts Action 类,并使用了实例成员变量。由于 Struts 框架只会创建一个 Struts Action 类的实例,并以多线程方式使用,这种做法非常不被推荐,且极可能存在问题。请考虑只使用方法内的局部变量。只有在监视器(monitor)之外被写入的实例字段才会被报告。
MTIA:类继承了 Servlet 类并使用实例变量(MTIA_SUSPECT_SERVLET_INSTANCE_FIELD)
该类继承自某个 Servlet 类,并使用了实例成员变量。由于 J2EE 框架只会创建一个 Servlet 类的实例,并以多线程方式使用,这种做法非常不被推荐,且极可能存在问题。请考虑只使用方法内的局部变量。
PS:类在其公开接口中暴露了同步与信号量(PS_PUBLIC_SEMAPHORES)
该类在自身(this 引用)上使用同步以及 wait()、notify() 或 notifyAll()。使用该类的客户端类还可能将该类的实例用作同步对象。由于两个类使用同一个对象进行同步,多线程正确性存疑。不应在公开引用上进行同步或调用信号量方法。请考虑使用内部私有成员变量来控制同步。
ICAST:整数乘法的结果被转换为 long(ICAST_INTEGER_MULTIPLY_CAST_TO_LONG)
此代码执行整数乘法,然后将结果转换为 long,例如:
long convertDaysToMilliseconds(int days) { return 1000*3600*24*days; }如果使用 long 运算执行该乘法,就可以避免结果溢出的可能性。例如,你可以将上面的代码修改为:
long convertDaysToMilliseconds(int days) { return 1000L*3600*24*days; }或者
static final long MILLISECONDS_PER_DAY = 24L*3600*1000;
long convertDaysToMilliseconds(int days) { return days * MILLISECONDS_PER_DAY; }ICAST: 整数除法结果被转换为 double 或 float(ICAST_IDIV_CAST_TO_DOUBLE)
这段代码将整数除法(例如 int 或 long 的除法)运算的结果转换为 double 或 float。对整数进行除法运算会将结果截断为最接近零的整数值。既然结果被转换成了 double,说明本应保留这一精度。通常的意图应该是将其中一个或两个操作数转换为 double,_然后_再执行除法运算。下面是一个例子:
int x = 2;
int y = 5;
// Wrong: yields result 0.0
double value1 = x / y;
// Right: yields result 0.4
double value2 = x / (double) y;BC: 可疑地转换为具体集合类型(BC_BAD_CAST_TO_CONCRETE_COLLECTION)
此代码将一个抽象集合(如 Collection、List 或 Set)强制转换为某种具体的实现类型(如 ArrayList 或 HashSet)。这种做法可能并不正确,而且会使你的代码变得脆弱,因为将来要切换到其他具体实现时会更加困难。除非你有特别的理由,否则请直接使用抽象集合类。
BC: 未经检查/未经确认的强制转换(BC_UNCONFIRMED_CAST)
此强制转换未经检查,且被转换类型的实例并非全部都能转换为所要转换的类型。请检查你的程序逻辑,确保此转换不会失败。
BC: 对方法返回值未经检查/未经确认的强制转换(BC_UNCONFIRMED_CAST_OF_RETURN_VALUE)
此代码对某个方法的返回值执行了未经检查的强制转换。代码可能以某种方式调用该方法,从而保证转换是安全的,但 SpotBugs 无法验证该转换是否安全。请检查你的程序逻辑,确保此转换不会失败。
BC: instanceof 将始终返回 true(BC_VACUOUS_INSTANCEOF)
此 instanceof 测试将始终返回 true(除非被测试的值为 null)。虽然这样做是安全的,但请确认它并非某种误解或其他逻辑错误的征兆。如果你确实想测试该值是否为 null,那么进行 null 测试而不是 instanceof 测试,或许会更清晰。
BC: 可疑地转换为抽象集合类型(BC_BAD_CAST_TO_ABSTRACT_COLLECTION)
此代码将一个 Collection 强制转换为某个抽象集合类型(如 List、Set 或 Map)。请确保你能保证该对象就是你要转换成的类型。如果你只需要能够遍历一个集合,就不必将它转换为 Set 或 List。
IM: 对负数无效的奇偶性检查(IM_BAD_CHECK_FOR_ODD)
代码使用 x % 2 == 1 来判断一个值是否为奇数,但这对负数不起作用(例如 (-5) % 2 == -1)。如果此代码是想检查奇偶性,请考虑使用 (x & 1) == 1 或 x % 2 != 0。
IM: 平均值计算可能发生溢出(IM_AVERAGE_COMPUTATION_COULD_OVERFLOW)
此代码使用除法或带符号右移来计算两个整数的平均值,然后将结果用作数组索引。如果被求平均的数值非常大,就可能发生溢出(从而计算出一个负的平均值)。假定结果应当是非负的,你可以改用无符号右移。也就是说,不要使用 (low+high)/2,而要使用 (low+high) >>> 1
此缺陷曾出现在许多早期的二分查找和归并排序实现中。Martin Buchholz 在 JDK 库中](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6412541)发现并修复了它,而 Joshua Bloch 则广泛传播了这一缺陷模式](http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html)。
BSHIFT: 将无符号右移结果转换为 short/byte(ICAST_QUESTIONABLE_UNSIGNED_RIGHT_SHIFT)
代码执行了无符号右移运算,其结果随后被强制转换为 short 或 byte,这会丢弃结果的高位。由于高位被丢弃,有符号右移与无符号右移可能没有区别(取决于移位的位数)。
DMI: 代码包含对绝对路径名的硬编码引用(DMI_HARDCODED_ABSOLUTE_FILENAME)
此代码使用硬编码的绝对路径名(例如 new File("/home/dannyc/workspace/j2ee/src/share/com/sun/enterprise/deployment");)构造 File 对象。
DMI: 调用 substring(0),其返回值即原始值(DMI_USELESS_SUBSTRING)
此代码对某个 String 调用了 substring(0),其返回值就是原始值本身。
ST: 从实例方法写入静态字段(ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD)
此实例方法写入了一个静态字段。如果同时操作多个实例,要正确处理这一点是很棘手的,而且通常也是一种不好的实践。
DMI: 将不可序列化对象写入 ObjectOutput(DMI_NONSERIALIZABLE_OBJECT_WRITTEN)
此代码似乎正在将一个不可序列化对象传递给 ObjectOutput.writeObject 方法。如果该对象确实不可序列化,将会导致错误。
DB: 方法的两个分支使用了相同的代码(DB_DUPLICATE_BRANCHES)
此方法使用相同的代码来实现条件分支的两个分支。请检查确认这不是一个编码错误。
DB: 方法的两个 switch 子句使用了相同的代码(DB_DUPLICATE_SWITCH_CLAUSES)
此方法使用相同的代码来实现 switch 语句的两个子句。这可能是重复代码,但也可能表明存在编码错误。
XFB: 方法直接分配了 xml 接口的特定实现(XFB_XML_FACTORY_BYPASS)
此方法分配了某个 xml 接口的一个特定实现。最好使用所提供的工厂类来创建这些对象,以便在运行时可以更改实现。详情请参见:
- javax.xml.parsers.DocumentBuilderFactory
- javax.xml.parsers.SAXParserFactory
- javax.xml.transform.TransarterFactory
- org.w3c.dom.Document.create_XXXX_
USM: 方法多余地委托给父类方法(USM_USELESS_SUBCLASS_METHOD)
此派生方法只是用收到的完全相同的参数调用同一个父类方法。该方法可以删除,因为它没有提供任何额外价值。
USM: 抽象方法已在所实现的接口中定义(USM_USELESS_ABSTRACT_METHOD)
此抽象方法已经在一个由该抽象类所实现的接口中定义了。该方法可以删除,因为它没有提供任何额外价值。
CI: 类为 final,但声明了 protected 字段(CI_CONFUSED_INHERITANCE)
此类被声明为 final,但其字段被声明为 protected。由于该类是 final 的,它不能被继承,因此使用 protected 令人困惑。该字段的访问修饰符应改为 private 或 public,以体现该字段的真实用途。
TQ:值必须不具有类型限定符,但被标记为未知 (TQ_EXPLICIT_UNKNOWN_SOURCE_VALUE_REACHES_NEVER_SINK)
某个值的使用方式要求它绝不能是类型限定符所表示的值,但却有一个显式注解声明,无法确定该值被禁止具有该类型限定符的位置。要么是用法有误,要么是注解有误。
TQ:值必须具有类型限定符,但被标记为未知 (TQ_EXPLICIT_UNKNOWN_SOURCE_VALUE_REACHES_ALWAYS_SINK)
某个值的使用方式要求它必须始终是类型限定符所表示的值,但却有一个显式注解声明,无法确定该值必须具有该类型限定符的位置。要么是用法有误,要么是注解有误。
NP:方法放宽了返回值的空值注解 (NP_METHOD_RETURN_RELAXING_ANNOTATION)
方法应当始终实现其所覆盖方法的契约。因此,如果某个方法被注解为返回 @Nonnull 值,就不应在子类中用一个被注解为返回 @Nullable 或 @CheckForNull 值的方法来覆盖它。这样做违反了该方法不应返回 null 的契约。
NP:方法收紧了参数的空值注解 (NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION)
方法应当始终实现其所覆盖方法的契约。因此,如果某个方法的参数被标记为 @Nullable,就不应在子类中用一个该参数被标记为 @Nonnull 的方法来覆盖它。这样做违反了该方法应当处理 null 参数的契约。
评论
登录后参与评论
KnowForge