Кроме обычных методов классы могут определять специальные методы, которые называются конструкторами. Конструкторы вызываются при создании нового объекта данного класса. Обычно конструкторы выполняют инициализацию объекта.
Если в классе не определено ни одного конструктора, то для этого класса автоматически создается конструктор без параметров. Например:
class Program{
public static void main(String[] args) {
Person tom = new Person(); // вызов конструктора по умолчанию
tom.name = "Tom";
tom.age = 41;
tom.print();
}
}
class Person{
String name;
int age;
void print(){
System.out.printf("Name: %s; Age: %d\n", name, age);
}
}
Выше определенный класс Person не имеет никаких конструкторов. Поэтому для него автоматически создается конструктор по умолчанию, который мы можем использовать для создания объекта Person.
Для создания объекта Person используется выражение new Person(). Оператор new выделяет память для объекта
Person. И затем вызывается конструктор по умолчанию, который не принимает никаких параметров. В итоге после выполнения данного выражения в памяти
будет выделен участок, где будут храниться все данные объекта Person. А переменная tom получит ссылку на созданный объект.
Если конструктор не инициализирует значения переменных объекта, то они получают значения по умолчанию. Для переменных числовых типов это число 0,
а для типа String и классов - это значение null (то есть фактически отсутствие значения).
После создания объекта мы можем обратиться к переменным объекта Person через переменную tom и установить или получить их значения, например, tom.name = "Tom".
В итоге мы увидим на консоли:
Name: Tom; Age: 41
Если необходимо, чтобы при создании объекта производилась какая-то логика, например, чтобы поля класса получали какие-то определенные значения, то можно определить в классе свои конструкторы. При этом если в классе определяются свои конструкторы, то этот класс лишается конструктора по умолчанию.
На уровне кода конструктор представляет метод, который называется по имени класса, который может иметь параметры, но для него не надо определять возвращаемый тип. Например, определим в классе Person простейший конструктор:
class Program{
public static void main(String[] args) {
Person tom = new Person(); // вызов конструктора, который мы сами определили
tom.print();
}
}
class Person{
String name;
int age;
// конструктор класса
Person(){
System.out.println("Создание объекта Person");
name = "Tom";
age = 41;
}
void print(){
System.out.printf("Name: %s; Age: %d\n", name, age);
}
}
Итак, здесь в классе Person определен конструктор, который выводит на консоль некоторое сообщение и инициализирует поля класса:
Person(){
System.out.println("Создание объекта Person");
name = "Tom";
age = 41;
}
Определив конструктор, мы можем вызвать его для создания объекта Person:
Person tom = new Person();
В данном случае выражение Person() как раз представляет вызов определенного в классе конструктора (это больше не автоматический конструктор по умолчанию, которого у класса теперь нет). Соответственно при его выполнении на консоли будет выводиться строка "Создание объекта Person"
Подобным образом мы можем определять и другие конструкторы в классе. Например:
class Program{
public static void main(String[] args) {
Person tom = new Person(); // вызов 1-ого конструктора без параметров
Person bob = new Person("Bob"); //вызов 2-ого конструктора с одним параметром
Person sam = new Person("Sam", 25); // вызов 3-его конструктора с двумя параметрами
tom.print(); // Имя: Неизвестно Возраст: 18
bob.print(); // Имя: Bob Возраст: 18
sam.print(); // Имя: Sam Возраст: 25
}
}
class Person{
String name;
int age;
Person()
{
name = "Неизвестно";
age = 18;
}
Person(String username)
{
name = username;
age = 18;
}
Person(String username, int userage)
{
name = username;
age = userage;
}
void print(){
System.out.printf("Имя: %s; Возраст: %d\n", name, age);
}
}
Теперь в классе определено три коструктора, каждый из которых принимает различное количество параметров и устанавливает значения полей класса.
Консольный вывод программы:
Имя: Неизвестно Возраст: 18 Имя: Bob Возраст: 18 Имя: Sam Возраст: 25
Из выше определенного кода несложно увидеть ряд преимуществ конструкторов по сравнению с версией программы без конструкторов:
Автоматическая инициализация: конструкторы обеспечивают корректную инициализацию объектов при их создании, снижая вероятность ошибок из-за неинициализированных переменных.
Краткость кода: код инициализации сосредоточен в конструкторе, что делает основной код более лаконичным и удобочитаемым.
Объектно-ориентированный дизайн: конструкторы способствуют более объектно-ориентированному дизайну, инкапсулируя логику инициализации внутри самого класса.
Ключевое слово this представляет ссылку на текущий экземпляр (объект) класса. Через это ключевое слово мы можем обращаться к переменным, методам объекта, а также вызывать его конструкторы. Например:
class Program{
public static void main(String[] args) {
Person tom = new Person(); // вызов 1-ого конструктора без параметров
Person bob = new Person("Bob"); //вызов 2-ого конструктора с одним параметром
Person sam = new Person("Sam", 25); // вызов 3-его конструктора с двумя параметрами
tom.print(); // Имя: Неизвестно Возраст: 18
bob.print(); // Имя: Bob Возраст: 18
sam.print(); // Имя: Sam Возраст: 25
}
}
class Person{
String name;
int age;
Person()
{
this("Неизвестно", 18);
}
Person(String name)
{
this(name, 18);
}
Person(String name, int age)
{
this.name = name;
this.age = age;
}
void print(){
System.out.printf("Имя: %s; Возраст: %d\n", name, age);
}
}
В третьем конструкторе параметры называются так же, как и поля класса. И чтобы разграничить поля и параметры, применяется ключевое слово this:
this.name = name;
Так, в данном случае указываем, что значение параметра name присваивается полю name.
Кроме того, у нас три конструктора, которые выполняют идентичные действия: устанавливают поля name и age. Чтобы избежать повторов, с помощью this можно вызвать один из конструкторов класса и передать для его параметров необходимые значения:
Person(String name)
{
this(name, 18);
}
В итоге результат программы будет тот же, что и в предыдущем примере.
Кроме конструктора начальную инициализацию объекта вполне можно было проводить с помощью инициализатора объекта. Инициализатор выполняется до любого конструктора. То есть в инициализатор мы можем поместить код, общий для всех конструкторов:
class Program{
public static void main(String[] args) {
Person undef = new Person(); // вызов 1-ого конструктора без параметров
undef.print(); // Имя: Неизвестно Возраст: 18
Person tom = new Person("Tom", 22); // вызов 2-ого конструктора с двумя параметрами
tom.print(); // Имя: Tom Возраст: 22
}
}
class Person{
String name;
int age;
/*начало блока инициализатора*/
{
name = "Неизвестно";
age = 18;
}
/*конец блока инициализатора*/
Person() { } // пустой конструктор, ничего не делает
Person(String name, int age)
{
this.name = name;
this.age = age;
}
void print(){
System.out.printf("Имя: %s; Возраст: %d\n", name, age);
}
}
Консольный вывод:
Имя: Неизвестно; Возраст: 18 Имя: Tom; Возраст: 22
Таким образом, у нас есть три способа инициализации полей:
Инициализация по умолчанию, когда мы явным образом не присваиваем полям никаких значений. В этом случае числовым полям присваивается число 0, булевым полям - значение false, а полям других классов
- значение null
Явная инициализация полей - полям при определении явным образом присваиваются некоторые значения
Инициализация в инициализаторе
Инициализация в конструкторе
Теперь рассмотрим, в каком порядке все это происходит:
Поля получают значения, которые им присваиваются при определении. Если при определении полям не присваивается никаких значений, то они получают значения по умолчанию
Поля получают значения, которые им присваиваются в инициализаторе
Поля получают значения, которые им присваиваются в конструкторе
Рассмотрим следующую программу:
class Program{
public static void main(String[] args) {
State state = new State();
System.out.println("Final value: " + state.value);
}
}
class State{
String value = "Default"; // значение по умолчанию
// инициализатор
{
System.out.println("Initializator. Old value: " + value);
value = "Initializator";
}
// конструктор
State(){
System.out.println("Constructor. Old value: " + value);
value = "Constructor";
}
}
В данном случае в классе State есть поле value, которому присвоено некоторое начальное значение. Кроме того, в инициализаторе и конструкторе присваиваем этому полю новое значение, при этом выводя старое значение. В итоге мы получим следующий консольный вывод:
Initializator. Old value: Default Constructor. Old value: Initializator Final value: Constructor
Здесь мы види, что сначала полю value присваивается начальное значение, затем выполняется инициализатор, а затем конструктор.
Можно также отловить самое начальное значение:
class Program{
public static void main(String[] args) {
State state = new State();
System.out.println("Final value: " + state.value); // 0
}
}
class State{
String value = defaultValue();
{
System.out.println("Initializator. Old value: " + value);
value = "Initializator";
}
State(){
System.out.println("Constructor. Old value: " + value);
value = "Constructor";
}
String defaultValue(){
System.out.println("Default. Old value: " + value);
return "Default";
}
}
Консольный вывод:
Default. Old value: null Initializator. Old value: Default Constructor. Old value: Initializator Final value: Constructor