假设我按以下方式定义了两个类:
public class A {
public A() {
foo();
}
public void foo() {
System.out.println("A");
}
}
public class B extends A {
private String bar;
public B() {
bar = "bar";
}
@Override
public void foo() {
System.out.println(bar);
}
}
然后我以下列方式实例化B:
A test = new B();
那么为什么编译器分别不能让IDE警告我在B的foo方法中会有一个NullPointer?这不是很难检查,有时非常有用。
答案 0 :(得分:7)
虽然这是设计错误,但它不是语法错误。
以下是来自 Effective Java 2nd Edition,第17项:设计和继承文档的引用,或者禁止它:
为了允许继承,类必须遵守一些限制。 构造函数不得直接或间接调用可覆盖的方法。如果违反此规则,将导致程序失败。超类构造函数在子类构造函数之前运行,因此在子类构造函数运行之前将调用子类中的重写方法。如果重写方法依赖于子类构造函数执行的任何初始化,则该方法将不会按预期运行。
一个顺从的编译器会让它编译得很好,因为它在语言上是合法的。幸运的是,代码分析工具可用于查找这些设计错误,例如: findbugs
:
UR:从超类构造函数调用的字段方法的未初始化读取(UR_UNINIT_READ_CALLED_FROM_SUPER_CONSTRUCTOR)
在超类的构造函数中调用此方法。此时,该类的字段尚未初始化。
答案 1 :(得分:4)
这是一个经典的问题。不要在构造函数中使用实例方法。
你可能想看看PMD,尤其是。 “ConstructorCallsOverridableMethod”规则。
答案 2 :(得分:3)
出现的问题是:您在foo
的构造函数中调用A
方法,但是在您调用它时,该对象尚未完全构造。 B
的构造函数尚未执行,因此bar
仍具有默认值null
。这说明了为什么从构造函数中调用非最终实例方法是个坏主意。
Java编译器没有对此发出警告 - 它警告某些构造,但编译器的第一个目的是编译代码,它并不是一个非常复杂的代码分析工具。
答案 3 :(得分:2)
父亲Josh Bloch在他的着作“有效Java”中说道。
您不能从Java中的构造函数中调用虚方法。
这可能导致您正在观察的确切问题。在致电B.foo()
时,B尚未初始化,因此B.bar
为null
。
答案 4 :(得分:0)
通常,编译器不会尝试证明字段的空值。它将为局部变量执行此操作,但是有太多方法可以更改字段的值(即序列化,反射或其他技巧),以便它可以完成一项彻底的工作。
如果可能的话,你应该尝试声明最终的字段,这对这个和许多其他事情有很大的帮助。或者,编写代码来防御空值(即“Test”.equals(foo)而不是foo.equals(“Test”)或显式空值检查)
答案 5 :(得分:0)
编译器的工作不是检查各种“不太难检查”的东西,这可能是也可能不是编程错误(并且有数百种)。
可以说,IDE可能会使用可配置的警告来标记这一点,但同样,有太多这样的东西,如果IDE必须在按需编译期间检查所有内容,它会使日常工作变得太慢。
这就是像FindBugs这样的静态样式检查器的用途。而且 - 惊喜! - 它检查了这种情况:
UR: Uninitialized read of field method called from constructor of superclass