Статьи

Java / NetBeans: переопределяемый вызов метода в конструкторе

Я написал о подсказке NetBeans «Переопределяемый вызов метода в конструкторе» в блоге « Семь незаменимых подсказок Java NetBeans» . В этом посте, я смотрю на то, почему имея Overridable метода вызывается из конструктора родительского класса является не очень хорошей идеей .

Следующий класс, Employee, является надуманным примером класса, в котором конструктор расширяемого класса вызывает переопределяемый метод (setSalaryRange ()).

Employee.java

package dustin.examples.overridable;

/**
 * Simple employee class that is intended to be a parent of a specific type of
 * employee class. The main purpose of this class is to demonstrate the
 * insidious dangers associated with a constructor calling an overridable method.
 * 
 * @author Dustin
 */
public class Employee
{
   private String lastName;
   private String firstName;
   private JobTitle jobTitle;
   protected int minWeeklySalary;
   protected int maxWeeklySalary;

   public enum JobTitle
   {
      CHIEF_EXECUTIVE_OFFICER("CEO"),
      COMPUTER_SCIENTIST("Computer Scientist");

      private String displayableTitle;

      JobTitle(final String newDisplayableTitle)
      {
         this.displayableTitle = newDisplayableTitle;
      }

      public String getDisplayableTitle()
      {
         return this.displayableTitle;
      }
   }

   public Employee(
      final String newLastName, final String newFirstName, final JobTitle newJobTitle)
   {
      this.lastName = newLastName;
      this.firstName = newFirstName;
      this.jobTitle = newJobTitle;
      setSalaryRange();
   }

   public void setSalaryRange()
   {
      this.minWeeklySalary = 5;
      this.maxWeeklySalary = 10;
   }

   @Override
   public String toString()
   {
      return  this.firstName + " " + this.lastName + " with title '"
            + this.jobTitle.getDisplayableTitle()
            + "' and with a salary range of $" + this.minWeeklySalary + " to $"
            + this.maxWeeklySalary + ".";
   }
}

NetBeans помечает существование переопределенного метода, вызываемого из конструктора, как показано на следующем снимке экрана ( в данном случае NetBeans 7.3 ):

Чтобы продемонстрировать общую проблему, связанную с переопределяемыми методами, вызываемыми в конструкторе, необходим дочерний класс. Это показано далее вместе с листингом кода для ComputerScientist, который расширяет Employee.

ComputerScientist.java

package dustin.examples.overridable;

/**
 * Class representing a specific type of employee (computer scientist), but its
 * real purpose is to demonstrate how overriding a method called in the parent
 * class's constructor leads to undesired behavior.
 * 
 * @author Dustin
 */
public class ComputerScientist extends Employee
{
   private final int MIN_CS_WEEKLY_SALARY_IN_DOLLARS = 1000;
   private static final int MAX_CS_WEEKLY_SALARY_IN_DOLLARS = 60000;

   private int marketFactor = 1;

   public ComputerScientist(
      final String newLastName, final String newFirstName, final int newMarketFactor)
   {
      super(newLastName, newFirstName, JobTitle.COMPUTER_SCIENTIST);
      this.marketFactor = newMarketFactor;
   }

   @Override
   public void setSalaryRange()
   {
      this.minWeeklySalary = MIN_CS_WEEKLY_SALARY_IN_DOLLARS * this.marketFactor;
      this.maxWeeklySalary = MAX_CS_WEEKLY_SALARY_IN_DOLLARS * this.marketFactor;
   }
}

Наконец, для запуска этого примера требуется простое тестовое приложение для вождения. Это показано в следующем простом исполняемом классе (Main.java).

Main.java

package dustin.examples.overridable;

import dustin.examples.overridable.Employee.JobTitle;
import static java.lang.System.out;

/**
 * Simple driver of the demonstration of why calling an overridable method in
 * the constructor of an extendible class is a bad idea.
 * 
 * @author Dustin
 */
public class Main
{
   public static void main(final String[] arguments)
   {
      final ComputerScientist cs = new ComputerScientist("Flintstone", "Fred", 5);
      final Employee emp = new Employee("Rubble", "Barney", JobTitle.CHIEF_EXECUTIVE_OFFICER);

      out.println(cs);
      out.println(emp);
   }
}

Можно ожидать, что компьютерный ученый, Фред Флинтстоун, будет получать еженедельную зарплату в диапазоне от 1000 до 60 000 долларов. Однако это не то, что показано при выполнении простого основного приложения (показаны выходные данные командной строки и выходные данные NetBeans).

Причина, по которой диапазон зарплат Computer Scientist не является правильным, заключается в том, что конструктор родительского класса (Employee’s) должен быть сначала запущен до того, как будет полностью создан экземпляр расширяющего класса (ComputerScientist). Дочерний класс переопределяет метод setSalaryRange (), но эта переопределенная реализация зависит от переменной экземпляра (marketFactor), которая еще не инициализирована в дочернем экземпляре, когда конструктор родителя вызывает переопределенный метод этого дочернего класса.

Есть несколько способов избежать этой проблемы. Возможно, самые лучшие и простые подходы — это те, которые рекомендованы подсказкой NetBeans, которая помечала эту проблему.

Как показывает приведенный выше снимок экрана, NetBeans предоставляет четыре простых и эффективных способа решения проблемы переопределяемого метода, вызываемого из конструктора класса. Поскольку проблема связана с «переопределяемым» методом и дочерним классом, экземпляр которого создается не полностью, когда этот переопределяемый метод вызывается во время конструктора родителя, очевидная тактика состоит в том, чтобы сделать родительский класс финальным, чтобы его нельзя было расширить. Это очевидно будет работать только для новых классов, которые не имеют классов, расширяющих их. Я обнаружил, что Java-разработчики часто не уделяют много внимания тому, чтобы сделать окончательный класс или планировать его расширение, но это пример того, где такое рассмотрение стоит.

Когда нецелесообразно делать родительский класс окончательным, NetBeans предлагает три других подхода для решения проблемы переопределенного метода, вызываемого из конструктора родительского класса. Даже если класс не может быть окончательным, к этому методу может быть применен модификатор final, чтобы конструктор больше не вызывал «переопределяемый» метод. Это позволяет дочернему классу по-прежнему расширять родительский класс, но он не может переопределить этот метод и поэтому не будет иметь реализацию, которая зависит от переменных уровня экземпляра, которые еще не были инициализированы.

Два других способа показали, что NetBeans использует для обработки проблему переопределяемого метода, вызываемого из конструктора, а также фокусируется на том, чтобы сделать этот вызываемый метод не переопределяемым. В этих двух других случаях это достигается за счет того, что вызываемый метод является статическим (уровень класса, а не уровень экземпляра), или за счет того, что метод становится закрытым (но затем недоступным для дочернего класса).

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

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

Мой пример, показанный в этом посте, относительно прост, и легко понять, почему результаты оказались не такими, как ожидалось. В гораздо большем и более сложном примере это может быть сложнее найти, и это может привести к пагубным ошибкам. NetBeans очень помогает, предупреждая о переопределяемых методах, вызываемых из конструкторов, чтобы можно было предпринять соответствующие действия. Если класс не расширен и нет планов по его расширению, то сделать финал класса легко и уместно. В противном случае NetBeans предоставляет другие варианты, гарантирующие, что все методы, вызываемые из конструктора, не могут быть переопределены.