Что не так с overridable вызовами метода в конструкторах?

У меня есть класс страницы Wicket, который устанавливает заголовок страницы в зависимости от результата абстрактного метода.

public abstract class BasicPage extends WebPage {

    public BasicPage() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();

}

NetBeans предупреждает меня с сообщением "Overridable method call in constructor", но что с ним должно быть? Единственная альтернатива, которую я могу себе представить, - передать результаты других абстрактных методов суперструктору в подклассах. Но это может быть трудно читать со многими параметрами.

Ответ 1

При вызове переопределенного метода из конструкторов

Проще говоря, это неправильно, потому что это излишне открывает возможности для многих ошибок. Когда вызывается @Override, состояние объекта может быть непоследовательным и/или неполным.

Цитата из Effective Java 2nd Edition, Item 17: Разработка и документация для наследования, или же запретить это:

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

Вот пример для иллюстрации:

public class ConstructorCallsOverride {
    public static void main(String[] args) {

        abstract class Base {
            Base() {
                overrideMe();
            }
            abstract void overrideMe(); 
        }

        class Child extends Base {

            final int x;

            Child(int x) {
                this.x = x;
            }

            @Override
            void overrideMe() {
                System.out.println(x);
            }
        }
        new Child(42); // prints "0"
    }
}

Здесь, когда конструктор Base вызывает overrideMe, Child не завершил инициализацию final int x, и метод получил неправильное значение. Это почти наверняка приведет к ошибкам и ошибкам.

Смежные вопросы

Смотрите также


На строительство объекта со многими параметрами

Конструкторы с множеством параметров могут привести к плохой читаемости, и существуют лучшие альтернативы.

Вот цитата из Effective Java 2nd Edition, Item 2: Рассмотрим шаблон компоновщика, когда сталкиваемся со многими параметрами конструктора:

Традиционно программисты использовали шаблон телескопического конструктора, в котором вы предоставляете конструктору только необходимые параметры, другой - с одним необязательным параметром, третий - с двумя дополнительными параметрами и т.д.

Шаблон телескопического конструктора, по сути, выглядит примерно так:

public class Telescope {
    final String name;
    final int levels;
    final boolean isAdjustable;

    public Telescope(String name) {
        this(name, 5);
    }
    public Telescope(String name, int levels) {
        this(name, levels, false);
    }
    public Telescope(String name, int levels, boolean isAdjustable) {       
        this.name = name;
        this.levels = levels;
        this.isAdjustable = isAdjustable;
    }
}

И теперь вы можете сделать любое из следующего:

new Telescope("X/1999");
new Telescope("X/1999", 13);
new Telescope("X/1999", 13, true);

Однако в настоящее время вы не можете установить только name и isAdjustable, а levels оставить по умолчанию. Вы можете предоставить больше перегрузок конструктора, но очевидно, что число будет взорваться при увеличении количества параметров, и вы можете даже иметь несколько boolean и int аргументов, что на самом деле приведет к путанице.

Как вы можете видеть, это не приятный шаблон для написания, а еще менее приятный для использования (что означает "истинный" здесь? Что 13?).

Bloch рекомендует использовать шаблон компоновщика, который позволит вам написать что-то вроде этого:

Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();

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

Смотрите также

Смежные вопросы

Ответ 2

Вот пример, который помогает понять это:

public class Main {
    static abstract class A {
        abstract void foo();
        A() {
            System.out.println("Constructing A");
            foo();
        }
    }

    static class C extends A {
        C() { 
            System.out.println("Constructing C");
        }
        void foo() { 
            System.out.println("Using C"); 
        }
    }

    public static void main(String[] args) {
        C c = new C(); 
    }
}

Если вы запустите этот код, вы получите следующий вывод:

Constructing A
Using C
Constructing C

Вы видите? foo() использует C до конструктора C. Если foo() требует, чтобы C имел определенное состояние (т.е. Конструктор закончен), тогда он столкнется с состоянием undefined в C, и все может сломаться. И поскольку вы не можете знать в A, что ожидает перезаписанный foo(), вы получаете предупреждение.

Ответ 3

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

В вашем примере, что произойдет, если подкласс переопределяет getTitle() и возвращает null?

Чтобы "исправить" это, вы можете использовать factory метод вместо конструктора, это общая схема инициализации объектов.

Ответ 4

Если вы вызываете в своем конструкторе методы, которые переопределяют подклассы, это означает, что вы менее склонны ссылаться на переменные, которые еще не существуют, если логически разделять вашу инициализацию между конструктором и методом.

Посмотрите на этот образец ссылки http://www.javapractices.com/topic/TopicAction.do?Id=215

Ответ 5

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

class A {

    protected int minWeeklySalary;
    protected int maxWeeklySalary;

    protected static final int MIN = 1000;
    protected static final int MAX = 2000;

    public A() {
        setSalaryRange();
    }

    protected void setSalaryRange() {
        throw new RuntimeException("not implemented");
    }

    public void pr() {
        System.out.println("minWeeklySalary: " + minWeeklySalary);
        System.out.println("maxWeeklySalary: " + maxWeeklySalary);
    }
}

class B extends A {

    private int factor = 1;

    public B(int _factor) {
        this.factor = _factor;
    }

    @Override
    protected void setSalaryRange() {
        this.minWeeklySalary = MIN * this.factor;
        this.maxWeeklySalary = MAX * this.factor;
    }
}

public static void main(String[] args) {
    B b = new B(2);
    b.pr();
}

Фактически результат:

minWeeklySalary: 0

maxWeeklySalary: 0

Это связано с тем, что конструктор класса B сначала вызывает конструктор класса A, где выполняется исполняемый метод внутри B. Но внутри метода мы используем переменную экземпляра factor, у которой еще не была инициализирована (поскольку конструктор A еще не завершен), поэтому коэффициент равен 0, а не 1 и определенно не 2 (то, что программист может подумать, что это будет). Представьте себе, насколько сложно было бы отслеживать ошибку, если бы логика расчета была в десять раз более скручена.

Я надеюсь, что это поможет кому-то.

Ответ 6

В конкретном случае Wicket: Именно по этой причине я спросил Калитку devs, чтобы добавить поддержку для явного процесса инициализации двухфазного компонента в жизненном цикле структуры построения компонента i.e.

  • Конструкция - через конструктор
  • Инициализация - через onInitilize (после построения, когда работают виртуальные методы!)

Были довольно активные дебаты о том, было ли это необходимо или нет (полностью необходимо IMHO), поскольку эта ссылка демонстрирует http://apache-wicket.1842946.n4.nabble.com/VOTE-WICKET-3218-Component-onInitialize-is-broken-for-Pages-td3341090i20.html)

Хорошей новостью является то, что отличные разработчики в Wicket в конечном итоге представили двухфазную инициализацию (чтобы сделать наиболее впечатляющую инфраструктуру Java UI еще более удивительной!), поэтому с помощью Wicket вы можете выполнить всю свою инициализацию пост-пост в методе onInitialize, автоматически вызывается каркасом, если вы переопределяете его - на этом этапе жизненного цикла компонента его конструктор завершил свою работу, поэтому виртуальные методы работают так, как ожидалось.

Ответ 7

Я думаю, для Wicket лучше назвать метод add в onInitialize() (см. жизненный цикл компонентов):

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

    @Override
    public void onInitialize() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();
}