Почему this() и super() должны быть первыми выражениями в конструкторе?
Вопрос: Почему в Java вызов this()
или super()
в конструкторе должен быть первой инструкцией?
В Java существует требование, что если вы вызываете this()
или super()
в конструкторе, то этот вызов должен быть первой инструкцией. Почему это так?
Например, рассмотрим следующий код:
public class MyClass {
public MyClass(int x) {}
}
public class MySubClass extends MyClass {
public MySubClass(int a, int b) {
int c = a + b;
super(c); // ОШИБКА КОМПИЛЯЦИИ
}
}
Компилятор Sun сообщает: вызов super должен быть первой инструкцией в конструкторе
. Компилятор Eclipse говорит: вызов конструктора должен быть первой инструкцией в конструкторе
.
Однако, это ограничение можно обойти, немного изменив код:
public class MySubClass extends MyClass {
public MySubClass(int a, int b) {
super(a + b); // ОК
}
}
Вот еще один пример:
public class MyClass {
public MyClass(List list) {}
}
public class MySubClassA extends MyClass {
public MySubClassA(Object item) {
// Создаем список, содержащий элемент, и передаем его в super
List list = new ArrayList();
list.add(item);
super(list); // ОШИБКА КОМПИЛЯЦИИ
}
}
public class MySubClassB extends MyClass {
public MySubClassB(Object item) {
// Создаем список, содержащий элемент, и передаем его в super
super(Arrays.asList(new Object[] { item })); // ОК
}
}
Таким образом, это правило не предотвращает выполнение логики перед вызовом super()
. Оно лишь ограничивает выполнение логики, которую нельзя уместить в одно выражение.
Для вызова this()
действуют аналогичные правила. Компилятор выдает сообщение: вызов this должен быть первой инструкцией в конструкторе
.
Почему компилятор накладывает такие ограничения? Можете ли вы привести пример кода, в котором, если бы эти ограничения отсутствовали, произошло бы что-то плохое?
5 ответ(ов)
Конструктор родительского класса должен быть вызван перед конструктором подкласса. Это гарантирует, что если вы вызываете какие-либо методы родительского класса в своем конструкторе, то родительский класс уже был корректно инициализирован.
То, что вы пытаетесь сделать, передать аргументы в конструктор суперкласса, вполне законно. Вам просто нужно либо сформировать эти аргументы непосредственно внутри вашего конструктора, как вы и делаете, либо передать их в ваш конструктор и затем передать в super
:
public class MySubClassB extends MyClass {
public MySubClassB(Object[] myArray) {
super(myArray);
}
}
Если бы компилятор не требовал этого порядка, вы могли бы сделать следующее:
public class MySubClassB extends MyClass {
public MySubClassB(Object[] myArray) {
someMethodOnSuper(); // ОШИБКА: super еще не инициализирован
super(myArray);
}
}
В случаях, когда у родительского класса есть конструктор по умолчанию, вызов super
автоматически вставляется компилятором. Поскольку каждый класс в Java наследуется от Object
, конструктор объектов должен каким-то образом быть вызван и он должен выполняться первым. Автоматическая вставка super()
компилятором это обеспечивает. Выявление необходимости, чтобы super
вызывался первым, гарантирует, что тела конструкторов выполняются в правильном порядке: Object → Parent → Child → ChildOfChild → и так далее.
Я нашел способ обойти эту проблему, используя цепочку конструкторов и статических методов. То, что я хотел сделать, выглядело примерно так:
public class Foo extends Baz {
private final Bar myBar;
public Foo(String arg1, String arg2) {
// ...
// ... Некоторая дополнительная логика для создания объекта 'Bar'...
// ...
final Bar b = new Bar(arg1, arg2);
super(b.baz());
myBar = b;
}
}
В основном, мне нужно было создать объект на основе параметров конструктора, сохранить этот объект в поле и также передать результат метода этого объекта в конструктор суперкласса. Кроме того, важно, чтобы поле было final
, так как класс предполагает неизменяемость. Обратите внимание, что создание Bar
на самом деле требует нескольких промежуточных объектов, поэтому его нельзя свести к одной строке в моем реальном случае.
В конце концов, мне удалось сделать это так:
public class Foo extends Baz {
private final Bar myBar;
private static Bar makeBar(String arg1, String arg2) {
// Моя более сложная логика настройки для создания 'Bar' здесь...
return new Bar(arg1, arg2);
}
public Foo(String arg1, String arg2) {
this(makeBar(arg1, arg2));
}
private Foo(Bar bar) {
super(bar.baz());
myBar = bar;
}
}
Это легальный код, который позволяет выполнить несколько операций перед вызовом конструктора суперкласса.
Вы спросили, почему вызов конструктора суперкласса допустим только в самом начале конструктора. Остальные ответы, по моему мнению, не совсем объясняют эту особенность. Дело в том, что вы не совсем вызываете конструктор в обычном смысле. В C++ синтаксис эквивалентен следующему:
MySubClass: MyClass {
public:
MySubClass(int a, int b): MyClass(a+b) {
}
};
Когда вы видите инициализатор на отдельной строке, перед открывающей фигурной скобкой, следует понять, что это особый случай. Этот код выполняется до того, как будет запущен остальной код конструктора, и, на самом деле, до инициализации любых переменных экземпляра. В Java ситуация схожая. Есть способ заставить код (например, другие конструкторы) выполняться до реального начала конструктора, прежде чем инициализируются члены подкласса. Эта возможность заключается в том, чтобы поместить "вызов" (например, super
) на самую первую строку. В некотором смысле, super
или this
находятся как бы "перед первой фигурной скобкой", хотя вы и пишете их позже, ведь они выполняются до того, как будет достигнута точка, где все структуры полностью построены.
Если же через несколько строк кода вы встретите конструкцию, которая говорит "ага, когда ты создаешь этот объект, вот параметры, которые я хочу, чтобы ты передал конструкторам суперкласса", это уже слишком поздно и не имеет смысла. Поэтому вы получаете ошибку компиляции.
Причина, по которой это реализовано именно так, заключается в философии наследования. Согласно спецификации языка Java, определение тела конструктора выглядит следующим образом:
ConstructorBody:
Первое выражение в теле конструктора может быть либо:
- явным вызовом другого конструктора этого же класса (с помощью ключевого слова "this"); или
- явным вызовом конструктора прямого суперкласса (с помощью ключевого слова "super").
Если тело конструктора не начинается с явного вызова конструктора, и создаваемый конструктор не принадлежит первичной группе классов, такой как Object, то тело конструктора неявно начинается с вызова конструктора суперкласса "super();", который не принимает аргументов. Это будет цепочка вызовов конструкторов, продолжающаяся до конструктора класса Object, поскольку "Все классы в платформе Java являются потомками класса Object". Это явление называется "Цепочка конструкторов".
И почему это так?
Причина, по которой Java определила тело конструктора таким образом, заключается в необходимости поддержания иерархии объектов. Не забывайте определение наследования: это расширение класса. И, следовательно, вы не можете расширить что-то, что не существует. Базовый класс (суперкласс) должен быть создан первым, а затем вы сможете создать производный класс (подкласс). Поэтому их называют родительскими и дочерними классами; у вас не может быть ребенка без родителя.
На техническом уровне подкласс наследует все члены (поля, методы, вложенные классы) от своего родителя. Поскольку конструкторы НЕ являются членами (они не принадлежат объектам; они отвечают за создание объектов), они НЕ наследуются подкласспами, но могут быть вызваны. Важно помнить, что в момент создания объекта выполняется только ОДИН конструктор. Как же мы можем гарантировать создание суперкласса, когда создается объект подкласса? Вот тут и возникает концепция "цепочки конструкторов"; у нас есть возможность вызывать другие конструкторы (например, super) из текущего конструктора. Java требует, чтобы этот вызов был первой строкой в конструкторе подкласса, чтобы поддерживать иерархию и гарантировать ее. Если вы не создадите объект родителя ЯВНО ПЕРВЫМ (например, если вы об этом забыли), Java сделает это неявно за вас.
Эта проверка выполняется на этапе компиляции. Но я не уверен, что произойдет во время выполнения, и какую ошибку времени выполнения мы получим, ЕСЛИ Java не выдаст ошибку компиляции, когда мы явно попытаемся вызвать конструктор базового класса из конструктора подкласса не с самой первой строки, а из середины тела...
Я почти уверен (пусть те, кто знаком с Java Specification, меня поправят), что это сделано для предотвращения (а) использования частично сконструированного объекта и (б) для того, чтобы конструктор родительского класса работал с "новым" объектом.
Примеры "плохих" случаев:
class Thing
{
final int x;
Thing(int x) { this.x = x; }
}
class Bad1 extends Thing
{
final int z;
Bad1(int x, int y)
{
this.z = this.x + this.y; // ОШИБКА! x еще не установлен
super(x);
}
}
class Bad2 extends Thing
{
final int y;
Bad2(int x, int y)
{
this.x = 33;
this.y = y;
super(x); // ОШИБКА! x должен быть final
}
}
Такие конструкции нарушают инкапсуляцию и могут приводить к непредсказуемым результатам в работе программы.
Как вызвать один конструктор из другого в Java?
Как инициализировать значения HashSet при создании?
Как создать утечку памяти в Java?
Инициализация ArrayList в одну строчку
Имеют ли круглые скобки после имени типа значение при использовании new?