Есть ли в Java законная причина для вызова не финального метода из конструктора класса? - PullRequest
12 голосов
/ 20 сентября 2011

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

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

В общем, плохая идея вызывать неконечный метод внутри конструктора именно по этой причине - тело конструктора подкласса еще не было выполнено, поэтому вы фактически вызываете метод в среде, которая не 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();
    }
}

А вот и ожидаемый вывод:

В методе (): ноль
В B (): [foo, bar]
В методе (): [foo, bar]

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

Ответы [ 5 ]

11 голосов
/ 20 сентября 2011

Временами бывает очень трудно не делать этого.

Взять, например, Время Йода .Его иерархия типов Chronology очень глубока, но абстрактный класс AssembledChronology основан на идее, что вы собираете кучу «полей» (месяц года и т. Д.).Существует не финальный метод, assembleFields, который вызывается во время конструктора, чтобы собрать поля для этого экземпляра.

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

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

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

6 голосов
/ 20 сентября 2011

Одним из примеров является не финальный (и закрытый для пакета) метод 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)

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

РЕДАКТИРОВАТЬ: Чтобы ответить на комментарий @ John B , я не говорю, что это должен быть хороший дизайн, так как он используется в источнике. Я просто хотел указать на пример. Я заметил, что каждый конструктор HashMap позаботится о том, чтобы последний вызывал init(), но это, конечно, еще до конструктора подкласса. Таким образом, ответственность за реализацию подкласса ложится не на то, чтобы все испортить.

3 голосов
/ 20 сентября 2011

Как правило, не очень хорошо вызывать методы класса до его создания; тем не менее, 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();
    }
}

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

0 голосов
/ 20 сентября 2011

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

0 голосов
/ 20 сентября 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...