Помню, как на втором месяце изучения Java я писал класс для геометрических фигур. У меня было три конструктора для класса Circle, и в каждом я копировал одни и те же строчки инициализации. Код превратился в кашу из повторений, и я подумал: "Должен же быть способ избежать этого дублирования!" Спойлер: способ есть, и называется он this().
Но как часто бывает с новичками, я сначала сделал все неправильно. И компилятор мне за это "спасибо" не сказал. Давайте разберемся, как вызывать один конструктор из другого правильно, и главное, какие ошибки не стоит повторять. Если коротко: внутри конструктора пишется this(...) со списком аргументов нужного конструктора, и этот вызов обязан стоять первой строкой. Так общая часть инициализации пишется один раз, а остальные конструкторы ее переиспользуют.![Разноцветные крапчатые яйца на зеленом фоне, а из расколотой большой скорлупы выглядывают яйца поменьше]()
Да, Java использует механизм перегрузки. В зависимости от количества и типов параметров компилятор определяет, какой именно конструктор нужно вызвать. Почему нельзя написать this.SomeClass(a, b)?
Потому что конструктор это не метод объекта. У него нет имени в обычном смысле. Для вызова конструктора используется специальный синтаксис this() или super(). Можно ли вызвать конструктор с меньшим количеством параметров из конструктора с бóльшим?
Да, это стандартная практика. Обычно делают наоборот: более простой конструктор вызывает более сложный, подставляя значения по умолчанию для недостающих параметров. Сколько раз можно вызывать this() в одном конструкторе?
Только один раз, и обязательно первой строкой. Если вам нужна сложная цепочка вызовов, она строится через несколько конструкторов: первый вызывает второй, второй вызывает третий и т.д. Что если мне нужно выполнить какую-то логику ДО вызова другого конструктора?
Никак. Вызов должен быть первой строкой. Если вам нужна предварительная обработка параметров, вынесите ее в статический метод и вызывайте его прямо в параметрах: this(prepareValue(param), otherParam). Чем this() отличается от super()?
this() вызывает другой конструктор того же класса, super() вызывает конструктор класса-родителя. Правила у них общие: оба должны стоять первой строкой, и в одном конструкторе может быть только один из них. Если не написать ни того, ни другого, компилятор сам подставит super() без аргументов.
Кратко
- this(...) внутри конструктора вызывает другой конструктор того же класса и избавляет от дублирования общей инициализации.
- Вызов this() или super() должен стоять первой строкой конструктора, и больше одного такого вызова в конструкторе быть не может. Правило про первую строку действует по Java 24 включительно.
- Если явного вызова нет, компилятор сам подставляет super() первой строкой.
- Поэтому нельзя сначала присвоить пару полей, а потом позвать this(): до Java 25 такой код не компилировался. В Java 25 это ограничение смягчили, см. раздел про гибкие тела конструкторов.
- Рекурсия между конструкторами запрещена: ни прямая, когда конструктор зовет сам себя, ни кольцевая, когда два конструктора зовут друг друга.
- Если перед вызовом нужна подготовка аргументов, вынесите ее в статический метод и вызовите прямо в скобках: this(prepareValue(param), otherParam).
Зачем вообще вызывать конструктор из конструктора?
Представьте: у вас есть класс с несколькими конструкторами, которые инициализируют разное количество полей. Часть кода у них общая: например, первые два поля всегда инициализируются одинаково. Вместо того чтобы копировать этот код в каждый конструктор, можно один раз написать базовую инициализацию, а потом просто переиспользовать ее. Это не только экономит время, но и делает код более поддерживаемым. Если нужно что-то изменить в логике инициализации, вы меняете в одном месте, а не бегаете по всем конструкторам.Как это делается правильно
Для вызова одного конструктора из другого используется ключевое слово this, после которого в круглых скобках указываются параметры для соответствующего конструктора. В руководстве Oracle такой вызов называется explicit constructor invocation, а общий разбор конструкторов есть в статье про конструкторы в Java. Вот простой пример:public class SomeClass {
int a;
int b;
int c;
public SomeClass(int a, int b) {
this.a = a;
this.b = b;
}
public SomeClass(int a, int b, int c) {
// Вызов конструктора с двумя параметрами
this(a, b);
this.c = c;
}
public void show() {
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
}
}
В конструкторе с тремя параметрами мы вызываем конструктор с двумя параметрами через this(a, b), а затем инициализируем только третью переменную. Получается, что код инициализации a и b написан один раз, а используется в обоих конструкторах.Более реалистичный пример
В учебных примерах конструкторы обычно простые: пара присваиваний и все. Но в реальной жизни конструкторы могут быть гораздо сложнее. Вот условный пример класса, который умеет отправлять GET и POST запросы:public class Request {
String url, postData, headers, method;
boolean auth;
Connection connection;
public Request(String url, boolean auth) {
this.method = "GET";
this.url = url;
if (auth) {
this.headers = this.getAuthHeaders(Application.config);
}
this.connection = new Connection(Application.config);
// и это очень условный пример, в реальной жизни все еще сложнее
}
public Request(String url, boolean auth, String postData) {
this(url, auth); // Переиспользуем всю логику из первого конструктора
this.method = "POST";
this.postData = postData;
}
}Видите? Вместо того чтобы копировать всю логику настройки соединения, заголовков и прочего, мы просто вызываем первый конструктор и добавляем только то, что специфично для POST-запроса.Типичная ошибка новичка (я тоже так делал)
А теперь самое интересное. Когда я впервые узнал про this(), я подумал: "Окей, я могу сначала инициализировать пару переменных, а потом вызвать конструктор для оставшихся!" И написал вот такой код:public class Circle {
public double x;
public double y;
public double radius;
public Circle(double x, double y, double radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
public Circle(double x, double y) {
this.x = x; // Сначала инициализируем
this.y = y; // две переменные
this(x, y, 10); // А потом вызываем конструктор
}
public Circle() {
this(5, 5, 1);
}
}
Логика мне казалась железной: сначала инициализируем x и y, потом вызываем конструктор, который присвоит им те же значения еще раз (ну и что?) плюс инициализирует radius. Какие проблемы?
Компилятор мне объяснил проблемы очень быстро. Код не скомпилировался. Точнее, не скомпилировался на той версии Java, на которой я тогда писал: в Java 25 правила изменились, и об этом будет отдельный разговор ниже.Почему так нельзя: техническое объяснение
Вызов другого конструктора (через this() или super()) должен быть либо первой строкой в конструкторе, либо его вообще не должно быть. Так было во всех версиях Java до 24 включительно, и компилятор это правило не нарушал. В Java 25 требование смягчили, но разобраться сначала стоит именно со старым порядком: на нем держится вся логика создания объекта. Вот что происходит под капотом. Когда вы пишете конструктор и не вызываете явно другой конструктор, компилятор автоматически добавляет вызов конструктора родительского класса, super(). Причем добавляет его первой строкой. Как устроена эта цепочка вверх по иерархии, разобрано в статье про конструкторы базовых классов. Когда я написал:public Circle(double x, double y) {
this.x = x;
this.y = y;
this(x, y, 10);
}
Компилятор видит это так:
public Circle(double x, double y) {
super(); // Компилятор добавил это неявно
this.x = x;
this.y = y;
this(x, y, 10); // Ошибка! Попытка вызвать конструктор после super()
}
Получается, что цепочка конструкторов уже была запущена (через super()), а я пытаюсь запустить ее второй раз через this(). Грамматика языка такого не допускала: тело конструктора обязано было начинаться с одного и только одного вызова.
При этом сам объект создает не конструктор, а оператор new. Память под объект выделяется и все поля получают значения по умолчанию еще до того, как отработает первый конструктор. Конструкторы только заполняют уже существующий объект, причем строго сверху вниз по иерархии: сначала родительский, потом дочерний. Ради этого порядка язык и требовал ставить вызов первой строкой.
Еще одна важная деталь: все эти переменные (x, y, radius) это не "глобальные переменные". Это поля экземпляра класса. Зачем их инициализировать, если потом вы вызовете другой конструктор, который присвоит им другие (или те же) значения?Золотые правила вызова конструкторов
Давайте сформулируем четкие правила, чтобы не наступать на те же грабли: 1. Вызов другого конструктора (this() или super()) должен быть первой строкой кода в вызывающем конструкторе. Не второй, не третьей, а именно первой. Это верно для Java по 24 включительно, в Java 25 правило смягчили. 2. В одном конструкторе может быть только один вызов другого конструктора. Либо вы вызываете конструктор своего класса через this(), либо конструктор родительского класса через super(), либо не вызываете ничего (тогда компилятор сам добавит super()). 3. Конструкторы Java исключают рекурсию. Нельзя написать конструктор, который вызывает сам себя (прямая рекурсия), или цепочку конструкторов, которая замыкается в кольцо (косвенная рекурсия). Компилятор такой код просто не пропустит. Примеры неправильной рекурсии:// Прямая рекурсия, не скомпилируется
class A {
public A(int x) {
this(1);
}
}
// Косвенная рекурсия, тоже не скомпилируется
class A {
public A() {
this(1);
}
public A(int x) {
this();
}
}
4. Вложенный конструктор не может иметь никакой явной или неявной ссылки на this в качестве параметра. То есть вы не можете передать результат вызова нестатического метода или внутренний класс в качестве параметра конструктора при вызове через this(). Почему? Потому что объект еще не создан! Вы находитесь в конструкторе, а это значит, что объект, на который ссылается this, еще не существует полностью.Что изменилось в Java 25
Все, что написано выше, верно для Java 8 и вплоть до Java 24. А в Java 25 требование «вызов первой строкой» смягчили: появились гибкие тела конструкторов (JEP 513, Flexible Constructor Bodies). Теперь тело конструктора делится на две части. Пролог это код до вызова this() или super(), эпилог это все, что после вызова. Раньше пролог обязан был быть пустым, теперь в нем можно что-то делать. Правило пролога такое: обращаться к создаваемому объекту в нем нельзя. Ни читать его поля, ни вызывать его методы, ни передавать его куда-либо. Единственное исключение: можно присваивать значения полям своего класса, если у этих полей нет собственных инициализаторов. Отсюда следует неожиданное: тот самый код с Circle, из-за которого я в свое время получил ошибку, на Java 25 компилируется.public Circle(double x, double y) {
this.x = x; // пролог: присваивание полю своего класса разрешено
this.y = y;
this(x, y, 10); // на Java 25 ошибки здесь уже нет
}
Правда, смысла в таком коде по-прежнему немного: конструктор с тремя параметрами все равно перезапишет x и y теми же значениями. Послабление затевалось ради другого, ради проверки аргументов до вызова другого конструктора.
public Circle(double radius) {
if (radius <= 0) {
throw new IllegalArgumentException("Радиус должен быть положительным");
}
this(0, 0, radius); // проверка прошла, теперь зовем основной конструктор
}
Раньше такую проверку приходилось прятать в статический метод и вызывать прямо в скобках. Теперь она пишется там, где ее естественно читать.
Что не изменилось: вызов другого конструктора по-прежнему может быть только один, рекурсия между конструкторами по-прежнему запрещена, и трогать this в прологе по-прежнему нельзя.Можно ли вызывать this() в обычных методах?
Нет, this() используется только в конструкторах. Если вам нужно переиспользовать код в обычных методах, используйте обычный вызов метода. Вот пример:public class Car {
private int speed;
private int price;
private String model;
public void setInfo(int speed, int price) {
this.speed = speed;
this.price = price;
}
public void setInfo(int speed, int price, String model) {
setInfo(speed, price); // Просто вызываем первый метод
this.model = model;
}
}
Кстати, в методах this необязателен: компилятор и так поймет, что вызывается метод текущего объекта. В конструкторах же this() это специальный синтаксис для вызова другого конструктора.Частые вопросы
При вызове конструктора Java автоматом определяет, какой вызывать?Да, Java использует механизм перегрузки. В зависимости от количества и типов параметров компилятор определяет, какой именно конструктор нужно вызвать. Почему нельзя написать this.SomeClass(a, b)?
Потому что конструктор это не метод объекта. У него нет имени в обычном смысле. Для вызова конструктора используется специальный синтаксис this() или super(). Можно ли вызвать конструктор с меньшим количеством параметров из конструктора с бóльшим?
Да, это стандартная практика. Обычно делают наоборот: более простой конструктор вызывает более сложный, подставляя значения по умолчанию для недостающих параметров. Сколько раз можно вызывать this() в одном конструкторе?
Только один раз, и обязательно первой строкой. Если вам нужна сложная цепочка вызовов, она строится через несколько конструкторов: первый вызывает второй, второй вызывает третий и т.д. Что если мне нужно выполнить какую-то логику ДО вызова другого конструктора?
Никак. Вызов должен быть первой строкой. Если вам нужна предварительная обработка параметров, вынесите ее в статический метод и вызывайте его прямо в параметрах: this(prepareValue(param), otherParam). Чем this() отличается от super()?
this() вызывает другой конструктор того же класса, super() вызывает конструктор класса-родителя. Правила у них общие: оба должны стоять первой строкой, и в одном конструкторе может быть только один из них. Если не написать ни того, ни другого, компилятор сам подставит super() без аргументов.