Почему считается неправильной практикой вызывать метод из конструктора?

В Java почему считается неправильной практикой вызывать метод из конструктора? Это особенно плохо, если метод вычислительно тяжелый?

Ответы

Ответ 1

Во-первых, вообще нет проблем с вызовами методов в конструкторе. Проблемы конкретно связаны с конкретными случаями вызова переопределяемых методов класса конструктора и передачи объекта this ссылки на методы (включая конструкторы) других объектов.

Причины для избежания переопределяемых методов и "утечки this" могут быть сложными, но в основном они направлены на предотвращение использования не полностью инициализированных объектов.

Избегайте вызывать переопределяемые методы

Причины отказа от вызова переопределяемых методов в конструкторах являются следствием процесса создания экземпляра, определенного в §12.5 Спецификации языка Java (JLS).

Кроме того, процесс § 12.5 гарантирует, что при создании экземпляра производного класса [1] инициализация его базового класса (т.е. установка его членов в их начальные значения и выполнение его конструктора ) происходит до его собственной инициализации. Это позволяет обеспечить последовательную инициализацию классов с помощью двух ключевых принципов:

  • Инициализация каждого класса может сосредоточиться на инициализации только тех членов, которые он явно декларирует сам, безопасно в понимании того, что все остальные члены, унаследованные от базового класса, уже инициализированы.
  • Инициализация каждого класса может безопасно использовать члены его базового класса в качестве исходных данных для инициализации его собственных членов, так как гарантировано, что они были правильно инициализированы к моменту начала инициализации класса.

Однако есть улов: Java допускает динамическую отправку в конструкторах [2]. Это означает, что если конструктор базового класса, выполняющий роль экземпляра производного класса, вызывает метод, который существует в производном классе, он вызывается в контексте этого производного класса.

Прямым следствием всего этого является то, что при создании экземпляра производного класса конструктор базового класса вызывается до инициализации производного класса. Если этот конструктор вызывает вызов метода, который переопределяется производным классом, это метод производного класса (а не метод базового класса), который называется , хотя производный класс еще не инициализирован. Очевидно, это проблема, если этот метод использует любые члены производного класса, так как они еще не были инициализированы.

Очевидно, что проблема возникает из-за метода вызова конструктора базового класса, который может быть переопределен производным классом. Чтобы предотвратить проблему, конструкторы должны вызывать только методы своего собственного класса, которые являются окончательными, статическими или частными, поскольку эти методы не могут быть переопределены производными классами. Конструкторы конечных классов могут вызывать любой из своих методов, поскольку (по определению) они не могут быть получены из.

Пример 12.5-2 JLS - хорошая демонстрация этой проблемы:

class Super {
    Super() { printThree(); }
    void printThree() { System.out.println("three"); }
}
class Test extends Super {
    int three = (int)Math.PI;  // That is, 3
    void printThree() { System.out.println(three); }

    public static void main(String[] args) {
        Test t = new Test();
        t.printThree();
    }
}

Эта программа печатает 0, затем 3. Последовательность событий в этом примере выглядит следующим образом:

  • new Test() вызывается в методе main().
  • Так как Test не имеет явного конструктора, вызывается конструктор по умолчанию его суперкласса (а именно Super()).
  • Конструктор Super() вызывает printThree(). Это отправляется в переопределенную версию метода в классе Test.
  • Метод printThree() класса Test печатает текущее значение переменной-члена three, которое является значением по умолчанию 0 (поскольку экземпляр Test еще не инициализирован).
  • Метод printThree() и Super() конструктор каждого выхода, и экземпляр Test инициализируется (в этот момент three устанавливается значение 3).
  • Метод main() снова вызывает printThree(), который на этот раз печатает ожидаемое значение 3 (поскольку экземпляр Test теперь инициализирован).

Как описано выше, в §12.5 говорится, что (2) должно произойти до (5), чтобы гарантировать, что Super инициализируется до Test. Однако динамическая отправка означает, что вызов метода в (3) выполняется в контексте неинициализированного класса Test, что приводит к неожиданному поведению.

Избегать утечки this

Ограничение на передачу this от конструктора к другому объекту немного легче объяснить.

В принципе, объект не может считаться полностью инициализированным до тех пор, пока его конструктор не завершит выполнение (поскольку его целью является завершение инициализации объекта). Итак, если конструктор передает объект this другому объекту, этот другой объект затем ссылается на объект, даже если он не был полностью инициализирован (поскольку его конструктор все еще запущен). Если другой объект затем пытается получить доступ к неинициализированному элементу или вызвать метод исходного объекта, который полагается на полностью инициализированный, может возникнуть неожиданное поведение.

Пример того, как это может привести к неожиданному поведению, см. в этой статье.

<ч/" > [1] Технически каждый класс в Java, кроме Object, является производным классом. Я просто использую термины "производный класс" и "базовый класс" здесь, чтобы описать взаимосвязь между конкретными классами. [2] В JLS нет (по моим сведениям) причин, почему это так. Альтернатива - запрещая динамической диспетчеризации в конструкторах. - бы весь вопрос спорный, который, вероятно, почему именно С++ не позволяет

Ответ 2

Конструкторы должны только когда-либо вызывать методы, которые являются частными, статическими или окончательными. Это помогает избавиться от проблем, которые могут возникать при переопределении.

Кроме того, конструкторы не должны запускать потоки. Есть две проблемы с запуском потока в конструкторе (или статическом инициализаторе):

  • в классе, не являющемся конечным, повышает опасность проблем с подклассами
  • он открывает дверь, позволяющую этой ссылке уйти от конструктора

Нет ничего плохого в создании объекта потока в конструкторе (или статическом инициализаторе) - просто не запускайте его там.

Ответ 3

Метод вызывающего экземпляра в конструкторе опасен, поскольку объект еще не полностью инициализирован (это относится в основном к методам, которые могут быть переопределены). Известно, что сложная обработка в конструкторе оказывает негативное влияние на тестовую способность.

Просто будьте осторожны, когда делаете, его плохая практика, чтобы сделать это с переопределяющими способными методами.