Что не так с переопределенными вызовами методов в конструкторах? - PullRequest
358 голосов
/ 04 августа 2010

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

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

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

Ответы [ 7 ]

456 голосов
/ 04 августа 2010

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

Проще говоря, это неправильно, потому что это излишне открывает возможности для МНОГО ошибок. Когда вызывается @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();

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

Смотри также

Похожие вопросы

54 голосов
/ 04 августа 2010

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

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 имел определенное состояние (т. Е. Конструктор завершил ), то он столкнется с неопределенным состоянием в C, и вещи могут сломаться.И поскольку вы не можете знать в А, что ожидает перезаписанный foo(), вы получите предупреждение.

11 голосов
/ 04 августа 2010

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

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

Чтобы «исправить» это, вы можете использовать фабрику Метод вместо конструктора, это общий шаблон реализации объектов.

4 голосов
/ 22 января 2015

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

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 (вещь, которую программист может подумать, что так и будет).Представьте себе, насколько сложно было бы отследить ошибку, если логика вычислений была бы в десять раз больше искажена.

Надеюсь, это кому-нибудь поможет.

4 голосов
/ 04 августа 2010

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

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

2 голосов
/ 06 мая 2017

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

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

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

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

0 голосов
/ 09 декабря 2016

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

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

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

    protected abstract String getTitle();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...