Подтвердить что ты не робот

В Java существует ли законная причина для вызова не конечного метода из конструктора класса?

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

Во время исследования этого вопроса я наткнулся на этот вопрос и был заинтригован ответом Джона Скита:

В общем, это плохая идея вызывать неконкретный метод внутри конструктора именно по этой причине - тело конструктора подкласса еще не будет выполнено, поэтому вы эффективно вызываете метод в среде, которая hasn ' t была полностью инициализирована.

Мне интересно, существует ли законная причина вызывать неконфиденциальный или абстрактный метод из конструктора? Или это почти всегда является признаком плохого дизайна?

Пример

public class SSCCE {
    static abstract class A {
        public A() {
            method();  // Not good; field arr in B will be null at this point!
        }

        abstract void method();
    }

    static class B extends A {
        final String[] arr = new String[] { "foo", "bar" };

        public B() {
            super();
            System.out.println("In B(): " + Arrays.toString(arr));
        }

        void method() {
            System.out.println("In method(): " + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        new B().method();
    }
}

И вот ожидаемый результат:

В методе(): null
В B(): [foo, bar]
В методе(): [foo, bar]

Проблема, конечно, в том, что в первом вызове method() поле arr имеет значение null, поскольку оно еще не инициализировано.

4b9b3361

Ответ 1

Иногда это может быть очень сложно.

Возьмите Joda Time, например. Его иерархия типа Chronology очень глубокая, но абстрактный класс AssembledChronology основан на идее, что вы собираете кучу "полей" (месяц-год и т.д.). Там используется не конечный метод assembleFields, который вызывается во время конструктора, чтобы собрать поля для этого экземпляра.

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

Я пошел в неприятную длину в Noda Time, чтобы избежать его фактически вызова виртуального метода - но это нечто удивительно похожее, чтобы быть честно.

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

Ответ 2

Одним из примеров является неконфиденциальный (и пакетно-частный) метод HashMap#init(), пустой метод, который используется для точной цели переопределения подклассами:

/**
 * Initialization hook for subclasses. This method is called
 * in all constructors and pseudo-constructors (clone, readObject)
 * after HashMap has been initialized but before any entries have
 * been inserted.  (In the absence of this method, readObject would
 * require explicit knowledge of subclasses.)
 */
void init() {
}

(из источника HashMap)

У меня нет примеров того, как он используется подклассами - если кто-то это делает, не стесняйтесь редактировать мой ответ.

EDIT: Чтобы ответить на комментарий @John B, я не говорю, что это должен быть хороший дизайн, поскольку он использовался в источнике, Я просто хотел указать на пример. Я замечаю, что каждый конструктор HashMap выполняет вызов init() last, но это, конечно, еще до конструктора подкласса. Таким образом, количество ответственности падает на реализацию подкласса, чтобы не гадать вещи.

Ответ 3

Как правило, это не хороший дизайн для вызова методов в классе до его построения; однако Java допускает исключения в случае, когда вы знаете, что делаете (т.е. вы не получаете доступ к неинициализированным полям). С абстрактным методом я не думаю, что можно "знать", что вы делаете в родительском классе.

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

Да, это сделано в JDK (например, в коде HashMap) со специальными методами "init()", которые подразумевают инициализацию всего кода подкласса; но я бы предложил, чтобы следующая схема вызовов была намного чище и обеспечивала большую гибкость.

public class SSCCE {
    static abstract class A {
        public A() {

        }

        abstract void method();
    }

    static class B extends A {
        final String[] arr = new String[] { "foo", "bar" };

        public B() {
            super();
            method();
            System.out.println("In B(): " + Arrays.toString(arr));
        }

        void method() {
            System.out.println("In method(): " + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        new B().method();
    }
}

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

Ответ 4

Хороший вопрос. Я голосую "нет" и буду пытаться включить это в свой будущий код. Я бы подумал, что было бы очень плохой вариант/рискованно назвать абстрактный метод.

Ответ 5

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

Ответ 6

Один очень полезный шаблон - вызов абстрактных (или переопределенных) методов createX. Это позволяет подклассам влиять на конфигурацию базового класса.