Конструкторы Java
Конструкторы Java для инициализации объектов: стандартный, параметризованный, перегруженный и цепочка конструкторов.
Конструктор — это специальный метод, который выполняется один раз при создании объекта. Его задача — перевести новый экземпляр из состояния «все поля имеют значения по умолчанию» в состояние «готов к использованию». До сих пор вы задавали поля по одному после new Dog(); конструктор позволяет объединить эту работу в вызове new.
Анатомия конструктора
Конструктор похож на метод, но отличается в трёх вещах: он имеет то же имя, что и класс, не имеет типа возврата (даже void), и вызывается только через new:
public class Point {
int x, y;
public Point(int x, int y) { // constructor
this.x = x;
this.y = y;
}
}
Point p = new Point(3, 4); // runs the constructorСписок аргументов передаётся в () после имени класса в выражении new. Java сопоставляет этот список аргументов с конструктором класса так же, как сопоставляет вызов метода с методом.
Конструктор по умолчанию
Если вы не объявляете ни одного конструктора, Java автоматически добавляет классу конструктор по умолчанию — конструктор без аргументов, который ничего не делает:
public class Empty { } // compiler generates a no-arg constructor
Empty e = new Empty(); // worksИменно так new Dog() работал в предыдущих примерах, хотя мы никогда не писали public Dog() { ... }. Как только вы объявляете любой собственный конструктор, бесплатный конструктор по умолчанию исчезает:
public class Point {
int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
}
Point p = new Point(); // ERROR — no no-arg constructor existsЕсли вам нужны оба варианта — объявите оба: смотрите раздел о перегрузке ниже.
Зачем использовать конструктор?
По двум причинам.
1. Обязательные поля. Конструктор позволяет сделать некоторые поля обязательными. При работе с открытыми полями и последующем присваивании ничто не мешает вызывающему коду создать Person без name. С конструктором Person(String name) это невозможно.
2. Инварианты. Конструктор может проверить аргументы до того, как объект будет создан в пригодном для использования виде. Если Circle требует положительного радиуса, конструктор может выбросить исключение при отрицательном значении — и некорректный объект вообще не будет создан.
public class Circle {
double radius;
public Circle(double radius) {
if (radius <= 0) throw new IllegalArgumentException("radius must be > 0");
this.radius = radius;
}
}Теперь вызывающий код не может получить Circle с неположительным радиусом.
Перегруженные конструкторы
Класс может иметь несколько конструкторов с разными списками параметров — точно так же, как перегруженные методы:
public class Rectangle {
double width, height;
public Rectangle() { this(1, 1); }
public Rectangle(double side) { this(side, side); } // a square
public Rectangle(double w, double h) { this.width = w; this.height = h; }
}Теперь все три варианта компилируются:
Rectangle a = new Rectangle(); // 1 x 1
Rectangle b = new Rectangle(5); // 5 x 5
Rectangle c = new Rectangle(3, 4); // 3 x 4Конструкторы копирования
Распространённый вариант перегрузки — конструктор копирования: он принимает другой экземпляр того же класса и создаёт его независимую копию. В Java нет встроенного конструктора копирования (в отличие от C++), поэтому при необходимости вы пишете его сами:
public class Point {
int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public Point(Point other) { this(other.x, other.y); } // copy constructor
}
Point a = new Point(3, 4);
Point b = new Point(a); // a separate Point with the same valuesПоскольку b — это новый объект, изменение b.x в дальнейшем не затрагивает a. Для классов, поля которых сами являются изменяемыми объектами, скопируйте и поля тоже (глубокое копирование), если хотите, чтобы копия была полностью независимой.
this(...) — цепочка конструкторов
Внутри одного конструктора this(args) вызывает другой конструктор того же класса. Именно так в примере с Rectangle выше удалось избежать дублирования кода присваивания полей: два вспомогательных конструктора делегируют вызов полному конструктору.
Два правила:
this(...)должен быть первым оператором в теле конструктора.- Конструктор может делегировать вызов только одному другому конструктору.
public Rectangle() { this(1, 1); } // ok
public Rectangle(double s) { System.out.println("hi"); this(s, s); } // ERRORПричина правила «первого оператора» заключается в том, что JVM должна полностью инициализировать объект ровно один раз, прежде чем выполнится любой другой код.
super(...) — вызов родительского конструктора
Когда класс расширяет другой, каждый конструктор либо явно вызывает родительский конструктор через super(args), либо неявно вызывает конструктор без аргументов родительского класса. Мы полностью разберём это в главах о наследовании и ключевом слове super; пока просто знайте, что super(...) существует и подчиняется тому же правилу первого оператора, что и this(...).
Конструкторы не могут возвращать значение
Конструктор не имеет типа возврата — даже void. Если вы напишете:
public void Point(int x, int y) { ... } // not a constructor!…это будет обычный метод с именем Point. Компилятор не предупредит вас; тогда new Point(3, 4) не скомпилируется, потому что настоящего конструктора, который он ищет, не существует. Это на удивление распространённая опечатка.
Порядок инициализации
Для одного класса порядок следующий:
- Полям присваиваются значения по умолчанию (например, поля
intстановятся равными0). - Встроенные инициализаторы полей (
int count = 5;) и любые блоки инициализации экземпляра выполняются в порядке их появления. - Выполняется тело конструктора.
public class Demo {
int a = compute("a-init", 1);
int b;
{ b = compute("b-block", 2); } // instance initializer
public Demo() {
System.out.println("constructor; a=" + a + ", b=" + b);
}
static int compute(String label, int v) {
System.out.println(label);
return v;
}
}new Demo() выводит a-init, b-block, затем constructor; a=1, b=2.
На практике почти вся инициализация выполняется в теле конструктора. Блоки инициализации экземпляра в реальном коде встречаются редко; они существуют главным образом для ситуаций вроде анонимных классов (рассматриваются позже).
Рабочий пример
Что дальше
Внутри конструкторов вы уже видели this.field = field для разграничения параметра и поля. Глава о ключевом слове this подробно объясняет, что такое this и в каких немногочисленных случаях его нужно писать явно.