Почему меня не предупреждают об этой проблеме?
Предположим, у меня есть два класса, определенных следующим образом:
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();
Так почему же компилятор не может предупредить меня, что в методе foo класса B будет указан NullPointer? Это было бы не сложно проверить, а иногда и очень полезно.
6 ответов
Хотя это ошибка дизайна, это не грамматическая ошибка.
Вот некоторая цитата из Effective Java 2nd Edition, Item 17: Разработка и документация для наследования, или же запретить это:
Есть еще несколько ограничений, которым должен следовать класс, чтобы разрешить наследование. Конструкторы не должны вызывать переопределяемые методы, прямо или косвенно. Если вы нарушите это правило, произойдет сбой программы. Конструктор суперкласса выполняется перед конструктором подкласса, поэтому метод переопределения в подклассе будет вызван до запуска конструктора подкласса. Если переопределяющий метод зависит от какой-либо инициализации, выполняемой конструктором подкласса, метод не будет работать должным образом.
Послушный компилятор позволил бы ему скомпилироваться просто отлично, поскольку он лингвистически допустим. К счастью, инструменты для анализа кода могут быть использованы для поиска этих ошибок проектирования, например, findbugs
:
UR: неинициализированное чтение метода поля, вызванное из конструктора суперкласса (UR_UNINIT_READ_CALLED_FROM_SUPER_CONSTRUCTOR)
Этот метод вызывается в конструкторе суперкласса. На данный момент поля класса еще не инициализированы.
Это классическая ошибка. Не используйте экземпляры методов в конструкторах.
Вы можете посмотреть на PMD, особенно правило "ConstructorCallsOverridableMethod".
Что идет не так, это то, что вы звоните foo
метод в конструкторе A
, но в тот момент, когда вы вызываете его, объект еще не полностью построен. Конструктор B
еще не выполнено, так bar
по-прежнему имеет значение по умолчанию null
, Это демонстрирует, почему плохая идея вызывать неконечные методы экземпляра из конструктора.
Компилятор Java не предупреждает об этом - он предупреждает о некоторых конструкциях, но первой целью компилятора является компиляция вашего кода, на самом деле он не предназначен для того, чтобы быть очень сложным инструментом анализа кода.
Вы можете использовать инструмент статического анализа кода, например FindBugs или PMD, чтобы найти подобные проблемы в своем коде.
И сказал отец Джош Блох в своей книге "Эффективная Ява":
Вы не должны вызывать виртуальные методы из конструктора в Java.
Это может привести к той проблеме, которую вы наблюдаете. Во время звонка B.foo()
, B еще не инициализирован, поэтому B.bar
является null
,
В общем, компилятор не будет пытаться доказать пустоту полей. Это будет сделано для локальных переменных, но существует слишком много способов изменить значение поля (например, сериализацию, отражение или другие хитрости), чтобы оно сделало тщательную работу.
Вы должны попытаться объявить поля окончательными, если это возможно, что очень помогает для этого и многих других вещей. В качестве альтернативы, напишите свой код для защиты от пустых значений (т. Е. "Test".equals(foo) вместо foo.equals("Test") или явных нулевых проверок).
Работа компилятора не в том, чтобы проверять все виды "не слишком сложных для проверки" вещей, которые могут быть, а могут и не быть ошибкой программирования (а их сотни).
Возможно, IDE может пометить это настраиваемым предупреждением, но, опять же, таких вещей слишком много, и если IDE придется проверять все это во время компиляции по требованию, это слишком сильно замедлит повседневную работу.
Это действительно то, для чего предназначены средства проверки статического стиля, такие как FindBugs. И - сюрприз! - есть проверка именно на это условие:
UR: неинициализированное чтение метода поля, вызванное из конструктора суперкласса