Руководство по конструкторам в Java

Введение

Конструктор в Java — это специальный блок кода, который выполняется при создании объекта.
В зависимости от вида, конструктор выполняет следующие функции:
  • конструктор по умолчанию — обеспечивает возможность создания объекта
  • конструктор без аргументов — выполняет инициализацию полей фиксированными значениями
  • параметризованный конструктор — инициализирует объект значениями, переданными извне
В этой статье мы разберем все виды конструкторов на примере классов, связанных с банковским счетом.

1. Конструктор по умолчанию

Создадим простой класс, описывающий банковский счет. Он содержит три поля: name («имя»), opened («дата открытия») и balance («баланс»). Для удобства вывода значений полей на консоль переопределим метод toString(), используя для форматирования текстовый блок.
Примечание: для простоты я не указываю модификатор private у полей, не реализую геттеры и сеттеры, а также (в последующих листингах) опускаю не изменившиеся строки методов.
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    String name;
    LocalDateTime opened;
    BigDecimal balance;

    @Override
    public String toString() {
        return """
                Имя: %s
                Дата создания: %s
                Баланс: %s
                """.formatted(name, opened.toString(), balance);
    }
}
Кроме явно объявленных членов, данный класс также содержит неявно объявленный (невидимый) конструктор, называемый конструктором по умолчанию (default constructor). Он создаётся компилятором автоматически, если в классе нет других конструкторов.
Примечание: LocalDateTime и BigDecimal — стандартные классы Java. Первый используется для хранения даты и времени, второй — для работы с денежными значениями.
Для запуска программы создадим основной класс с методом main():
public class Main {

    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        System.out.println(account);
    }
}
Запуск программы приведет к выбросу исключения NullPointerException (далее NPE) из-за вызова метода toString() у переменной opened, значение которой равно null.
Примечание: при наличии в классе только конструктора по умолчанию, все поля экземпляра получают значения по умолчанию, предусмотренные языком.

// name = null, opened = null, balance = null
Как исправить эту ошибку — рассмотрим далее. А пока разберем, что происходит при создании объекта:
BankAccount account = new BankAccount();
При выполнении данной строки происходит следующее:
  1. объявляется переменная account типа BankAccount
  2. оператор new дает команду виртуальной машине Java (далее JVM) выделить память в куче (heap) под новый объект
  3. JVM устанавливает всем полям значения по умолчанию
  4. вызывается конструктор (в данном случае — по умолчанию)
  5. неявно вызывает super() — конструктор родительского класса (в данном случае — Object)
  6. возвращается ссылка на созданный объект
  7. ссылка сохраняется в переменной account. Теперь account ссылается на созданный объект в куче

1.1 Особенности конструктора по умолчанию

Конструктор по умолчанию:
  • создаётся автоматически при компиляции, если в классе нет других конструкторов
  • не виден в исходном коде
  • не содержит кода инициализации полей
  • неявно вызывает super()
  • имеет тот же модификатор доступа, что и класс
При создании объекта JVM (а не конструктор) гарантированно присваивает полям значения по умолчанию до вызова конструктора:
  • числовые примитивы → 0 или 0.0
  • boolean → false
  • char → '\u0000'
  • ссылочные типы → null
Конструктор по умолчанию гарантирует, что у любого класса будет хотя бы один конструктор, если программист не объявил свой.

2. Конструктор без аргументов

Исправим NPE из прошлой главы, добавив явно конструктор без аргументов (no-args constructor), который инициализирует поля безопасными значениями:
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    String name;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount() {
        name = "unknown";
        opened = LocalDateTime.now();
        balance = BigDecimal.ZERO;
    }

    @Override
    public String toString() {
        // без изменений
    }
}
Примечание: LocalDateTime.now() возвращает текущую дату и время, а BigDecimal.ZERO инициализирует баланс нулевым значением.

Значения полей изменились:
1. Было (конструктор по умолчанию):
// name = null, opened = null, balance = null

2. Стало (конструктор без аргументов):
// name = "unknown", opened = текущая дата, balance = 0
Повторно запустив программу, получим следующий вывод (дата и время у вас будут отличаться):
Имя: unknown
Дата создания: 2025-10-14T11:02:52.244236800
Баланс: 0
Теперь, когда мы явно объявили конструктор без аргументов, компилятор перестает автоматически генерировать конструктор по умолчанию. При создании экземпляра BankAccount будет вызываться созданный нами конструктор.

2.1 Особенности конструктора без аргументов

Как вы, вероятно, заметили, конструктор чем-то похож на метод, но таковым не является. Иногда новички все же путают их, а затем долго не могут понять, в чем ошибка.
Основные визуальные отличия конструктора:
  • не имеет возвращаемого значения
  • его имя должно совпадать с именем класса и начинаться с заглавной буквы
Пример возможных ошибок:
// ❌ Не метод, поскольку не указан тип возвращаемого значения
// И не конструктор, поскольку имя указано с маленькой буквы
public bankAccount() { }  

// ❌ Это не конструктор, а метод, поскольку указан возвращаемый тип
public void BankAccount() { }  
В спецификации языка Java указано: «Непосредственно перед тем, как будет возвращен результат в виде ссылки на только что созданный объект, указанный конструктор будет использован для инициализации этого нового объекта.»
Это значит, что конструктор вызывается после выделения памяти и присвоения значений по умолчанию, но до возврата ссылки на объект, обеспечивая его корректную инициализацию.
Процесс создания объекта аналогичен описанному в разделе 1, но на шаге 3 вызывается наш конструктор без аргументов:
  1. выделение памяти
  2. установка значений по умолчанию
  3. вызов конструктора без аргументов
  4. выполнение кода конструктора: вызов super() и инициализация полей пользовательскими значениями
  5. возврат ссылки на созданный объект
Обратите внимание, что установка значений по умолчанию JVM все равно происходит.
Примечание: конструктор по умолчанию создает компилятор, если программист не напишет свой; конструктор без аргументов реализуется программистом, при этом по умолчанию уже не генерируется. Оба конструктора не принимают аргументы, но при этом называются по-разному.

3. Параметризованный конструктор

Чтобы сделать что-то действительно полезное с банковским счетом, необходимо иметь возможность передавать нужные нам начальные значения в объект.
Для этого напишем еще один конструктор, но уже принимающий аргументы. Он называется параметризованный конструктор (parameterized constructor).
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    String name;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount() {
        // без изменений
    }

    public BankAccount(String name, LocalDateTime opened, BigDecimal balance) {
        System.out.println("Параметризованный конструктор:");
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }

    @Override
    public String toString() {
        // без изменений
    }
}
Обратите внимание на ключевое слово this — оно указывает на текущий объект и позволяет различать поля и параметры с одинаковыми именами. Благодаря this не происходит коллизии имен, когда компилятор не знает, какое имя выбрать — экземпляра или параметра.
Теперь в main() можно создать объекты двумя способами:
public static void main(String[] args) {
    BankAccount account1 = new BankAccount();
    System.out.println(account1);

    LocalDateTime opened = LocalDateTime.of(1986, 5, 30, 4, 30);
    BankAccount account2 = new BankAccount("Max", opened, new BigDecimal("123.0"));
    System.out.println(account2);    
}
Примечание: метод LocalDateTime.of() создает экземпляр, проинициализированный заданными нами значениями даты и времени.
Запустим программу:
Конструктор без аргументов:
Имя: unknown
Дата создания: 2025-10-15T11:39:43.423807800
Баланс: 0

Параметризованный конструктор:
Имя: Max
Дата создания: 1986-05-30T04:30
Баланс: 123.0
Теперь у класса BankAccount два конструктора: без аргументов и параметризованный. При этом вызовется тот, который будет соответствовать количеству и типам передаваемых в него аргументов.
Наличие нескольких конструкторов в одном классе называется перегрузкой (overloading).

3.1 Особенности параметризованного конструктора

Параметризованный конструктор позволяет:
  • создавать объекты с конкретными начальными значениями
  • гарантирует, что объект будет проинициализирован только при наличии всех необходимых данных. Это делает объект согласованным и готовым к использованию сразу после создания
  • упростить код, избегая множественных вызовов сеттеров:
// ❌ менее предпочтительно
BankAccount account = new BankAccount();
account.setName("Max");
account.setOpened(LocalDateTime.now());
account.setBalance(new BigDecimal("1000.0"));

// ✅ более надежно
BankAccount account =
        new BankAccount("Max", LocalDateTime.now(), new BigDecimal("1000.0"));
  • проверять входные параметры на корректность перед созданием объекта:
public BankAccount(String name, LocalDateTime opened, BigDecimal balance) {
    // Проверка параметров на корректность
    if (name == null || name.trim().isBlank()) {
        throw new IllegalArgumentException("Имя не может быть пустым");
    }
    if (opened == null) {
        throw new IllegalArgumentException("Дата не может быть пустой");
    }
    if (balance.compareTo(BigDecimal.ZERO) < 0) {
        throw new IllegalArgumentException("Баланс не может быть отрицательным");
    }
    
    this.name = name;
    this.opened = opened;
    this.balance = balance;
}
Компилятор не сгенерирует конструктор по умолчанию, если программист реализовал свой конструктор. Попробуем создать экземпляр класса BankAccount не указав ни одного аргумента:
public class BankAccount {
    String name;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount(String name, LocalDateTime opened, BigDecimal balance) {
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }
}

public static void main(String[] args) {
    BankAccount account = new BankAccount();
}
Компилятор выдал ошибку, поскольку не смог найти конструктор без аргументов. Если требуется такой конструктор, то его нужно реализовать самостоятельно.

4. Конструктор копирования

В реальных проектах часто требуется создавать копии объектов. Для этого используется конструктор копирования (copy constructor) — специализированный вид параметризованного конструктора.
В отличие от стандартного (автоматического) клонирования объектов с помощью метода clone(), конструктор позволяет реализовать собственную управляемую логику инициализации полей в новой копии.
Конструктор копирования принимает в качестве параметра объект того же класса, в котором он объявлен, и инициализирует все поля нового объекта значениями из оригинала.
Рассмотрим ситуацию, когда требуется создать новую учетную запись на основе существующей: она должна иметь то же имя, что и старая, но сегодняшнюю дату открытия и нулевой баланс:
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    String name;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount() {
        // без изменений
    }

    public BankAccount(BankAccount account) {
        System.out.println("Конструктор копирования:");
        name = account.name;
        opened = LocalDateTime.now();
        balance = BigDecimal.ZERO;
    }

    public BankAccount(String name, LocalDateTime opened, BigDecimal balance) {
        // без изменений
    }

    @Override
    public String toString() {
        // без изменений
    }
}
Добавим в метод main() создание еще одного объекта:
public static void main(String[] args) {
    BankAccount account1 = new BankAccount();
    System.out.println(account1);

    LocalDateTime opened = LocalDateTime.of(1986, 5, 30, 4, 30);
    BankAccount account2 = new BankAccount("Max", opened, new BigDecimal("123.0"));
    System.out.println(account2);

    BankAccount account3 = new BankAccount(account2);
    System.out.println(account3);
}
Результат выполнения программы:
Конструктор без аргументов:
Имя: unknown
Дата создания: 2025-10-15T16:52:51.938440600
Баланс: 0

Параметризованный конструктор:
Имя: Max
Дата создания: 1986-05-30T04:30
Баланс: 123.0

Конструктор копирования:
Имя: Max
Дата создания: 2025-10-15T16:52:51.952046700
Баланс: 0
Конструктор копирования создал новый объект account3 на основе account2, но с обновленной датой открытия счета и нулевым балансом. Это демонстрирует гибкость подхода — мы можем контролировать, какие данные копировать, а какие инициализировать заново.
Обратите внимание, что в конструкторе копирования не используется this, поскольку нет конфликта имен.
Конструктор копирования незаменим, когда нужно:
  • создать модифицированную копию объекта (как в нашем примере)
  • защитить оригинальный объект от изменений

4.1 Поверхностное копирование

Рассмотрим механизм так называемого поверхностного копирования (shallow copy), когда в конструкторе копирования происходит копирование ссылок на значения из оригинального в создаваемый объект.
Если копируемая ссылка принадлежит любому изменяемому объекту, то при ее изменении в любом объекте (не важно, в новом или исходном) ее значение изменится во всех ссылающихся на нее объектах.
Создадим новый класс Customer, перенеся в него имя владельца счета из BankAccount:
public class Customer {
    String name;

    public Customer(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}
Добавим в BankAccount поле типа Customer, изменив также код методов:
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    Customer customer;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount() {
        System.out.println("1. Конструктор без аргументов:");
        customer = new Customer("unknown");
        opened = LocalDateTime.now();
        balance = BigDecimal.ZERO;
    }

    public BankAccount(BankAccount account) {
        System.out.println("2. Конструктор копирования (поверхностное):");
        customer = account.customer;
        opened = LocalDateTime.now();
        balance = BigDecimal.ZERO;
    }

    public BankAccount(Customer customer, LocalDateTime opened, BigDecimal balance) {
        System.out.println("3. Параметризованный конструктор:");
        this.customer = customer;
        this.opened = opened;
        this.balance = balance;
    }

    @Override
    public String toString() {
        return """
                Имя: %s
                Дата создания: %s
                Баланс: %s
                """.formatted(customer, opened, balance);
    }
}
Также внесем изменения в класс Main:
public static void main(String[] args) {
    BankAccount account1 = new BankAccount();
    System.out.println(account1);

    LocalDateTime opened = LocalDateTime.of(1986, 5, 30, 4, 30);
    Customer customer = new Customer("Max");
    BankAccount account2 =
            new BankAccount(customer, opened, new BigDecimal("123.0"));
    System.out.println(account2);

    BankAccount account3 = new BankAccount(account2);
    System.out.println(account3);

    // Изменяем имя в account2
    account2.customer.name = "ch1max";

    System.out.println("Вывод имени после его изменения в account2:");
    System.out.println("Имя владельца account2: " + account2.customer.name);
    System.out.println("Имя владельца account3: " + account3.customer.name);
}
При поверхностном копировании создается новый объект account3 с полем customer, который имеет такую же ссылку, что и у оригинала account2.
Это происходит потому, что account2.customer и account3.customer
ссылаются на один и тот же объект в памяти.
Запустим программу:
1. Конструктор без аргументов:
Имя: unknown
Дата создания: 2025-10-21T09:30:00.043552400
Баланс: 0

3. Параметризованный конструктор:
Имя: Max
Дата создания: 1986-05-30T04:30
Баланс: 123.0

2. Конструктор копирования (поверхностное):
Имя: Max
Дата создания: 2025-10-21T09:30:00.061132800
Баланс: 0

Вывод имени после его изменения в account2:
Имя владельца account2: ch1max
Имя владельца account3: ch1max
Как видно из результата, изменения поля customer одного аккаунта затрагивают все аккаунты, ссылающиеся на тот же объект, что может привести к непредсказуемым побочным эффектам.
Это наглядно показывает риск поверхностного копирования: изменения в одном объекте неожиданно влияют на другие.
Поверхностное копирование требует осторожности — оно подходит только когда допустимо, чтобы несколько объектов использовали общие данные.

4.2 Глубокое копирование

Если результат работы поверхностного копирования вас не устраивает, можно воспользоваться глубоким копированием (deep copy).
Данный вид копирования позволяет создавать новые объекты на основе оригинальных, содержащие абсолютно независимые от них значения полей (в том числе ссылочных).
Рассмотрим пример:
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    Customer customer;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount() {
        // без изменений
    }

    public BankAccount(BankAccount account) {
        System.out.println("2. Конструктор копирования (глубокое):");
        customer = new Customer(account.customer.name);
        opened = LocalDateTime.now();
        balance = BigDecimal.ZERO;
    }

    public BankAccount(Customer customer, LocalDateTime opened, BigDecimal balance) {
        // без изменений
    }

    @Override
    public String toString() {
        // без изменений
    }
}
Классы Main и Customer остаются без изменений.
Запустим программу:
1. Конструктор без аргументов:
Имя: unknown
Дата создания: 2025-10-21T14:42:45.169857500
Баланс: 0

3. Параметризованный конструктор:
Имя: Max
Дата создания: 1986-05-30T04:30
Баланс: 123.0

2. Конструктор копирования (глубокое):
Имя: Max
Дата создания: 2025-10-21T14:42:45.190725400
Баланс: 0

Вывод имени после его изменения в account2:
Имя владельца account2: ch1max
Имя владельца account3: Max
Как видно по результату, account2 и account3 имеют независимые объекты Customer, и изменение одного не затрагивает другой.

4.3 Ключевые моменты

Ниже в таблице собраны различия между поверхностным и глубоким копированием.
Уточнение о типах полей:
  • значение примитивов (int, float, double и т. д.) копируются как есть — безопасно
  • ссылки на неизменяемые объекты (например, String), также копируются как есть. Несмотря на то, что оригинальный и порожденный объекты ссылаются на тот же самый адрес в памяти (объект String в пуле строк), такие объекты никогда не будут изменяться
  • ссылки на изменяемые объекты должны копироваться при помощи глубокого копирования. В противном случае оригинальный и порожденный объекты будут ссылаться на один адрес в памяти, и любые изменения затронут все объекты

5. Явный вызов конструкторов

Часто бывают случаи, когда в момент создания объекта набор значений для инициализации его полей неполон. При этом мы не хотим дублировать код: писать повторно проверки для валидации входных значений, строки инициализации полей и т. д.
В таком случае используют явный вызов конструкторов (explicit constructor invocation), также известный как делегирование конструкторов.
Это механизм, при котором один конструктор может быть вызван внутри другого конструктора с использованием ключевого слова this() и списка аргументов.
Изменим класс BankAccount, чтобы для создания объекта (нового банковского счета), требовалось только имя клиента. Для этого добавим новый конструктор, принимающий Customer, а другим параметрам дадим значения по умолчанию. Затем все эти аргументы передадим из одного конструктора в другой, используя this():
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    Customer customer;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount() {
        this("unknown");
        System.out.println("1. Конструктор без аргументов:");
    }

    public BankAccount(String name) {
        this(new Customer(name), LocalDateTime.now(), BigDecimal.ZERO);
        System.out.println("2. Параметризованный конструктор с одним параметром:");
    }

    public BankAccount(Customer customer, LocalDateTime opened, BigDecimal balance) {
        System.out.println("3. Параметризованный конструктор:");
        this.customer = customer;
        this.opened = opened;
        this.balance = balance;
    }

    @Override
    public String toString() {
        // без изменений
    }
}
Файл Main:
public static void main(String[] args) {
    BankAccount account1 = new BankAccount();
    System.out.println(account1);
   
    BankAccount account2 = new BankAccount("Max");
    System.out.println(account2);
}
Запустим программу:
// Создание account1:
3. Параметризованный конструктор:
2. Параметризованный конструктор с одним параметром:
1. Конструктор без аргументов:
Имя: unknown
Дата создания: 2025-11-04T16:09:05.004008100
Баланс: 0

// Создание account2:
3. Параметризованный конструктор:
2. Параметризованный конструктор с одним параметром:
Имя: Max
Дата создания: 2025-11-04T16:09:05.022858300
Баланс: 0
Порядок выполнения конструкторов:
Порядок выполнения для account1:
1. new BankAccount() вызывает this("unknown")
2. BankAccount(String name) вызывает 
    this(new Customer(name), LocalDateTime.now(), BigDecimal.ZERO)  
3. Выполняется тело конструктора (3) — объект готов
4. Возврат в конструктор (2) — вывод сообщения
5. Возврат в конструктор (1) — вывод сообщения

Порядок выполнения для account2:
1. new BankAccount("Max") вызывает
    this(new Customer(name), LocalDateTime.now(), BigDecimal.ZERO)
2. Выполняется тело конструктора (3) — объект готов
3. Возврат в конструктор (2) — вывод сообщения
Обратите внимание на порядок вывода сообщений конструкторов. Сначала выполняется параметризованный конструктор (3), затем конструктор с одним параметром (2), и только потом конструктор по умолчанию (1). Это происходит потому, что вызовы this() идут первой инструкцией в конструкторе: сначала создается объект с помощью конструктора с наибольшим количеством параметров, содержащего всю логику инициализации, затем управление возвращается по цепочке вызывающих конструкторов.
Вызов конструкторов по цепочке — это стандартная практика в Java (в рамках текущего класса), когда часть значений используется по умолчанию, а какие-то являются новыми.
Делегирование конструкторов не только устраняет дублирование кода,
но и централизует логику инициализации в одном месте, что упрощает сопровождение и уменьшает вероятность ошибок.

5.1 Особенности явного вызова конструкторов

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

6. Дополнительные ключевые моменты

Рассмотрим ряд дополнительных особенностей конструкторов, которые не вошли в предыдущие главы.
1
Конструктор не может быть объявлен как final, static, synchronized или abstract
  • final — бесполезен, так как конструкторы и так не наследуются (дочерние классы не могут переопределить конструкторы родителя)
  • static — противоречит назначению конструктора. Статические методы принадлежат классу и не работают с экземплярами. Конструктор же создаёт и инициализирует конкретный экземпляр объекта, работая с полями this. Статический конструктор — оксюморон
  • abstract — бессмыслен, поскольку конструктор всегда конкретен. Абстрактные методы не имеют реализации и должны быть переопределены. Но конструктор нельзя переопределить — можно только вызвать из дочернего класса. Абстрактный конструктор не мог бы создать объект, что противоречит его цели
  • synchronized — не применим к процессу создания объекта. Синхронизация защищает общие ресурсы от конкурентного доступа. Но конструктор работает с ещё не опубликованным объектом — другие потоки не могут получить к нему доступ до завершения конструктора. Синхронизировать нечего
2
Заблуждение: конструктор создает объекты.
Реальность: объекты создает оператор new, а конструктор их инициализирует (либо JVM, если это значения по умолчанию).
3
Конструкторы могут иметь модификатор доступа private
Модификатор private у конструктора запрещает создания экземпляра класса. Подобный подход используется, например, в паттерне проектирования Одиночка (Singleton), где приватный конструктор используется для контроля над количеством создаваемых экземпляров: всегда можно будет создать только один объект.
Иногда класс может быть служебным и хранить какие-то статические поля и статические методы. Необходимости в создании экземпляров таких классов нет, поэтому и в конструкторе нет смысла. Но как мы уже знаем, компилятор создаст конструктор по умолчанию. Чтобы этого не произошло, мы можем сами создать пустой конструктор и сделать его закрытым, используя модификатор доступа private. Такой конструктор называется закрытый.
Пример такого служебного класса:
public class BankAccountUtils {
    private BankAccountUtils() {}

    public static String concat(String name, String surname) {
        return name + " " + surname;
    }
}
Метод concat объединяет имя и фамилию в одну строковую переменную. Закрытый конструктор BankAccountUtils() делает невозможным создание экземпляра класса BankAccountUtils. Следующий код выведет объединенную строковую переменную без создания объекта:
public class Main {
    public static void main(String[] args) {
        System.out.println(BankAccountUtils.concat("Ch1", "Max"));
    }
}
4
Конструктор по умолчанию имеет тот же модификатор доступа, что и класс
Если мы определим класс следующим образом:

public class BankAccount { }
то компилятор вставит конструктор по умолчанию с таким же самым модификатором доступа:

public BankAccount() { }
5
Первым выражением в конструкторе должен быть вызов this() или super()
  • this() — вызов другого конструктора этого же класса
  • super() — вызов конструктора родительского класса
  • если не указано явно, компилятор автоматически добавит super()
(только если в конструкторе нет вызова this())
6
Конструктор и сеттеры можно (нужно) использовать совместно
Если используется конструктор по умолчанию, то далее предполагается, что при помощи сеттеров полям объекта присваиваются нужные нам значения, которые на момент его создания были неизвестны.
Также, возможны комбинации, когда объект создается с несколькими обязательными полями, и с полями, инициализированными значениями по умолчанию, которые в дальнейшем могут неоднократно изменяться сеттерами.
Конструкторы задают начальное состояние, сеттеры позволяют изменять его позже.
7
Конструкторы не наследуются подобно методам суперкласса
public class BankAccount {
    String name;
    LocalDateTime opened;
    BigDecimal balance;

    BankAccount(String name, LocalDateTime opened, BigDecimal balance) {
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }
}

class VipBankAccount extends BankAccount {
}
Мы не сможем сделать что-то вроде этого:
import java.time.LocalDateTime;

public class Main {
    public static void main(String[] args) {
        LocalDateTime actionDate = LocalDateTime.now();
        VipBankAccount vipBankAccount =
                new VipBankAccount("Tom", actionDate, BigDecimal.ZERO);
    }
}
Компилятор выдаст ошибку:
Компилятор будет искать в VipBankAccount конструктор, который принимает три аргумента, но кроме конструктора по умолчанию (об этом сообщается в тексте ошибки «required: no arguments»), ничего не находит.
Чтобы код запускался без ошибок, необходимо переписать класс VipBankAccount в следующем виде:
public class VipBankAccount extends BankAccount {
    VipBankAccount(String name, LocalDateTime opened, BigDecimal balance) {
        super(name, opened, balance);
    }
}
8
Конструктор класса вызывает конструктор по умолчанию его суперкласса (по цепочке вплоть до Object)
Компилятор Java автоматически вставляет неявно вызов super() в первую строку любого конструктора. Рассмотрим следующие два класса:
public class BankAccount {
    String name;
    LocalDateTime opened;
    BigDecimal balance;

    public BankAccount(String name, LocalDateTime opened, BigDecimal balance) {
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }
}
public class VipBankAccount extends BankAccount {
    VipBankAccount() {}
}
Такой код не скомпилируется, поскольку компилятор вставляет вызов super() в конструктор VipBankAccount:
VipBankAccount() {
    super();	// метод вставлен компилятором автоматически
}
Но в классе BankAccount отсутствует конструктор по умолчанию (или пустой конструктор), поэтому компилятор сгенерирует ошибку компиляции следующего вида:

7. Правила форматирования конструкторов

1
Конструкторы следует размещать после блока с полями (это соответствует естественному порядку чтения кода)
неправильно:
class BankAccount {

    private String name;

    String getName() {
        return name;
    }

    public BankAccount(String name) {
        this.name = name;
    }
}
правильно:
class BankAccount {

    private String name;

    public BankAccount(String name) {
        this.name = name;
    }

    String getName() {
        return name;
    }
}
2
Между именем конструктора и ( пробел не ставится
неправильно:
Player (String name)
Resume (String uuid)
new Cat ()
правильно:
Player(String name)
Resume(String uuid)
new Cat()
3
Конструкторы следует размещать в порядке, зависящем от принимаемых ими числа аргументов: от меньшего к большему
неправильно:
class Resume {

    Resume(String name, int number) {}

    Resume() {}

    Resume(String name) {}
}
правильно:
class Resume {

    Resume() {}

    Resume(String name) {}

    Resume(String name, int number) {}
}
4
Перенесенные на новую строку аргументы конструктора необходимо смещать вправо на 8 пробелов относительно первой строки. При этом старайтесь, чтобы на каждой строке было примерно равное количество параметров
неправильно:
public Jaeger(String modelName, String mark, String origin,
    float height, float weight, int strength) {
    this.modelName = modelName;
    this.mark = mark;
    ...
}

Jaeger jaeger = new Jaeger("Guardian Bravo", "Mark-6", "Japan",
    73.21f, 2.18f, 35, 70);
правильно:
public Jaeger(String modelName, String mark, String origin,
        float height, float weight, int strength) {
    this.modelName = modelName;
    this.mark = mark;
    ...
}

Jaeger jaeger = new Jaeger("Guardian Bravo", "Mark-6", "Japan",
        73.21f, 2.18f, 35, 70);

Заключение

В этой статье мы рассмотрели основные виды конструкторов в Java — от автоматически создаваемого конструктора по умолчанию до сложных цепочек вызовов с использованием this() и super(). На примере класса BankAccount мы увидели, как каждый тип конструктора решает конкретные задачи инициализации объектов.
Как вы могли заметить из главы «Дополнительные ключевые моменты», мир конструкторов гораздо шире и содержит множество нюансов, которые требуют отдельной статьи.
Освоение конструкторов — важный шаг в изучении объектно-ориентированного программирования в Java. Понимание того, когда и какой тип конструктора использовать, позволяет создавать более надежный, поддерживаемый и безопасный код. Помните: хороший конструктор не просто создает объект, а гарантирует его корректное начальное состояние.
Авторы: Малянов Игорь и Чимаев Максим
Оцените статью, если она вам понравилась!