Руководство по конструкторам в 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 (balance.compareTo(BigDecimal.ZERO) < 0) {
        throw new IllegalArgumentException("Баланс не может быть отрицательным");
    }
    
    this.name = name;
    this.opened = opened;
    this.balance = balance;
}
Компилятор уже не сгенерирует конструктор по умолчанию, если программист реализовал свой конструктор:
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;
    }
}
Попробуем создать экземпляр класса BankAccount не указав ни одного аргумента:
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() {
        // без изменений
    }
}
Запустим программу:
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), также копируются как есть. Несмотря на то, что оригинальный и порожденный объекты ссылаются на тот же самый адрес в памяти, такие объекты никогда не будут изменяться
  • ссылки на изменяемые объекты должны копироваться при помощи глубокого копирования. В противном случае оригинальный и порожденный объекты будут ссылаться на один адрес в памяти, и любые изменения затронут все объекты

5. Связанные конструкторы

Следует отметить, что мы не обязаны в конструктор передавать для инициализации всех полей класса какие-то значения. Часть обязанностей можно возложить на компилятор, который присвоит полям значения по умолчанию (если это не критично). Также, в процессе работы программы, можно использовать сеттеры.
Для демонстрации этой возможности создадим новый банковский счет, введя только имя. Для этого создадим конструктор с параметром имени, а другим параметрам дадим значения по умолчанию. Затем все эти аргументы передадим из одного конструктора в другой, используя this():
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class BankAccount {

    Customer customer;
    LocalDateTime opened;
    BigDecimal balance;

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

    public BankAccount(BankAccount account) {
        this(new Customer(account.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 без изменений.
Запустим программу:
3. Параметризованный конструктор:
1. Конструктор без аргументов:
Имя: unknown
Дата создания: 2025-10-22T08:31:24.343765500
Баланс: 0

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

3. Параметризованный конструктор:
2. Конструктор копирования (глубокое):
Имя: Max
Дата создания: 2025-10-22T08:31:24.376813900
Баланс: 0

Вывод имени после его изменения в account2:
Имя владельца account2: ch1max
Имя владельца account3: Max
Как вы могли заметить, в теле конструктора используется ключевое слово this с круглыми скобками, которое позволяет вызывать другой конструктор, передавая в него аргументы (не любой, а только тот, который подходит по количеству и типам передаваемых аргументов). Это нормальная практика вызывать конструкторы по цепочке (в рамках текущего класса), когда часть значений используется по умолчанию, а какие-то являются новыми.
Но бывает и другая ситуация, когда мы хотим перед отработкой конструктора потомка вызвать конструктор суперкласса. В таком случае нужно использовать ключевое слово super().
Создадим класс VipBankAccount, сделав его наследником BankAccount:
import java.time.LocalDateTime;
  
public class VipBankAccount extends BankAccount {
    String status;

    public VipBankAccount(String name, String status) {
        super(name, LocalDateTime.now(), 0.0f);
        this.status = status;
    }
     
    @Override
    public String toString() {
        return String.format("Имя: %s \nДата создания: %s \nБаланс: %f \nСтатус: %s",
                name, opened.toString(), balance, status);
    }
}  
Перепишем Main.java:
public class Main {
    public static void main(String[] args) {
        VipBankAccount vipAccount = new VipBankAccount("Thomas", "VIP");
        System.out.println(vipAccount);
    }
}

6. Объект-значение

Интересное использование конструкторов в Java — создание объектов-значений (Value Objects). Value Object — это объект, который не меняет своего внутреннего состояния после инициализации (становится immutable). Это значит, что если требуется изменить такой объект, то для этого придется создать его новый экземпляр, вместо того чтобы изменять существующий.
Давайте создадим неизменяемый класс Transaction, представляющий собой транзакцию некоторого количества денежных средств с конкретного счета, с фиксацией даты и времени проведения транзакции:
import java.time.LocalDateTime;
 
public class Transaction {
    final BankAccount bankAccount;
    final LocalDateTime date;
    final double amount;
/* 
   Код ниже приведет к ошибке компиляции, если переменная amount будет final
  
   public Transaction(BankAccount account, LocalDateTime date) {
       this.bankAccount = account;
       this.date = date;
   }
*/
    public Transaction(BankAccount account, LocalDateTime date, double amount) {
        this.bankAccount = account;
        this.date = date;
        this.amount = amount;
    }
    
    @Override
    public String toString() {
        return String.format("Проведена транзакция на сумму - %s, \n\nС банковского счета 17:             - %s. \n\nДата и время транзакции: %s", amount, bankAccount, date.toString());
    }
}  
Обратите внимание, что теперь мы используем ключевое слово final при определении членов класса. Это означает, что каждый из них может быть инициализирован только с помощью конструктора. Их нельзя переназначить позже в каком-либо другом методе. Мы можем считать эти значения, но не можем их изменить.
Если мы создадим несколько конструкторов для класса Transaction, то каждый из них должен будет инициализировать каждую финальную переменную. Невыполнение этого приведет к ошибке компиляции. Например, если раскомментировать конструктор в коде класса Transaction, то на этапе компиляции мы получим ошибку с сообщением «java: variable amount might not have been initialized».
Перепишем файл Main. java в следующем виде:
import java.time.LocalDateTime;
  
public class Main {
    public static void main(String[] args) {
        LocalDateTime actionDate = LocalDateTime.now();
        BankAccount account = new BankAccount("Tom");
        Transaction transaction = new Transaction(account, actionDate, 1234);
   
        System.out.println(transaction);
    }
}

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

  • Конструктор не может быть объявлен как final, static, synchronized или abstract
  • Конструктор может быть перегружен (overload)
Заблуждение: конструктор создает объекты.
Реальность: объекты создает оператор new, а конструктор их инициализирует.
Перегрузка конструктора означает, что какой-либо класс может иметь множество конструкторов, но при этом их списки параметров должны отличаться между собой.
Например:
import java.time.LocalDateTime;

public class BankAccount {
    String name;
    LocalDateTime opened;
    double balance;

    BankAccount() {
        name = "";
        opened = LocalDateTime.now();
        balance = 0.0d;
    }

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

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

    BankAccount(String name, LocalDateTime opened, double balance) {
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }
}
Класс BankAccount имеет 4 различных конструктора, следовательно мы можем создать объект BankAccount четырьмя различными способами:
import java.time.LocalDateTime;

public class Main {
   public static void main(String[] args) {
       LocalDateTime actionDate = LocalDateTime.now();

       BankAccount bankAccount1 = new BankAccount();
       BankAccount bankAccount2 = new BankAccount("Tom");
       BankAccount bankAccount3 = new BankAccount("Tom", actionDate);
       BankAccount bankAccount4 = new BankAccount("Tom", actionDate, 0.0);
   }
}
• Конструкторы не наследуются подобно методам суперкласса
import java.time.LocalDateTime;

public class BankAccount {
    String name;
    LocalDateTime opened;
    double balance;

    BankAccount(String name, LocalDateTime opened, double 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, 0.0);
    }
}
Компилятор выдаст ошибку:
Компилятор будет искать в VipBankAccount конструктор, который принимает три аргумента, но кроме конструктора по умолчанию (об этом сообщается в тексте ошибки «required: no arguments»), ничего не находит.
Чтобы код запускался без ошибок, необходимо переписать класс VipBankAccount в следующем виде:
import java.time.LocalDateTime;

public class VipBankAccount extends BankAccount {
    VipBankAccount(String name, LocalDateTime opened, double balance) {
        super(name, opened, balance);
    }
}
• Конструкторы могут иметь модификатор доступа private
Мы можем сделать конструктор приватным (private), что не позволит создавать его экземпляры. Какой в этом смысл? Например, в паттерне проектирования Одиночка (Singleton) приватный конструктор используется для контроля над количеством создаваемых экземпляров. Всегда можно будет создать только один объект.
Рассмотрим следующий пример:
import java.time.LocalDateTime;

public class BankAccount {
    private static BankAccount instance;

    String name;
    LocalDateTime opened = LocalDateTime.now();
    double balance = 0.0f;

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

    public static BankAccount getInstance(String name) {
        if (instance == null) {
            instance = new BankAccount(name);
        }
        return instance;
    }

    @Override
    public String toString() {
        return String.format("Имя: %s \nДата создания: %s \nБаланс: %f\n", name, opened.toString(), balance);
    }
}
public class Main {
   public static void main(String[] args) {
       BankAccount bankAccount1 = BankAccount.getInstance("Tom");
       BankAccount bankAccount2 = BankAccount.getInstance("Elena");

       System.out.println(bankAccount1);
       System.out.println(bankAccount2);
   }
}
Класс BankAccount (реализует упрощенный паттерн одиночка) предоставляет статический метод getInstance(), который проверяет создан ли уже экземпляр класса или нет. Если создан, то новый не создается, а возвращается ссылка на существующий.
Результат выполнения программы подтверждает, что в программе создается только один объект.
Иногда класс может быть служебным и хранить какие-то статические поля и статические методы. Необходимости в создании экземпляров таких классов нет, поэтому и в конструкторе нет смысла, но как мы уже знаем, компилятор создаст конструктор по умолчанию. Чтобы этого не произошло, мы можем сами создать пустой конструктор и сделать его закрытым, используя модификатор доступа private. Такой конструктор называется закрытый.
Пример такого служебного класса:
public class BankAccountUtils {
    private BankAccountUtils() {}

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

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

public BankAccount() { }
• Конструктор класса вызывает конструктор по умолчанию его суперкласса (по цепочке вплоть до Object)
Компилятор Java автоматически вставляет неявно вызов super () в первую строку любого конструктора. Рассмотрим следующие два класса:
import java.time.LocalDateTime;

public class BankAccount {
    String name;
    LocalDateTime opened;
    double balance;

    public BankAccount(String name, LocalDateTime opened, double balance) {
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }
}
public class VipBankAccount extends BankAccount {
    VipBankAccount() {}
}
Такой код не скомпилируется, поскольку компилятор вставляет вызов super() в конструктор VipBankAccount:
VipBankAccount() {
    super();	// метод вставлен компилятором автоматически
}
Но в классе BankAccount отсутствует конструктор по умолчанию (или пустой конструктор), поэтому компилятор сгенерирует ошибку компиляции следующего вида:
Поэтому, обращайте внимание на это правило при наследовании родительского класса.
• Первым выражением в конструкторе должен быть вызов метода this () или super()
  • Конструктор и сеттеры можно (нужно) использовать совместно:
Если используется конструктор по умолчанию, то далее предполагается, что при помощи сеттеров (setter) полям объекта присваиваются нужные нам значения, которые на момент его создания были неизвестны.
В случае же с параметризованным конструктором, значения полей сразу инициализируются подходящими нам значениями. Один вызов такого конструктора заменяет собой вызов нескольких сеттеров. При этом создается объект с корректными значениями полей. Кроме того, параметризованный конструктор позволяет создать неизменяемый объект (immutable — это объект, состояние которого не может быть изменено после создания), что невозможно при использовании сеттеров.
Также, возможны комбинации, когда объект создается с несколькими обязательными полями, и с полями, инициализированными значениями по умолчанию, которые в дальнейшем могут неоднократно изменяться сеттерами.

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

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);

Заключение

Мы ознакомились с различными способами создания объектов с помощью разных видов конструкторов. Рассмотрели множество примеров и правил при создании и применении конструкторов.
Особенности конструктора:
  • Конструктор вызывается каждый раз при создании объекта
  • Имя конструктора должно совпадать с именем класса
  • Конструктор не может иметь возвращаемый тип (даже void)
  • К конструктору можно применять модификаторы доступа для контроля его вызова
  • Конструкторы в классе размещаются строго в определенном месте
  • Конструктор может принимать в качестве аргумента как примитивные типы данных, так и объекты
  • Класс может содержать любое количество конструкторов
  • Конструкторы нужно создавать по мере необходимости. При большом количестве конструкторов для упрощения кода можно использовать Шаблоны проектирования из категории Creational Design Patterns
Авторы: Малянов Игорь и Чимаев Максим
Оцените статью, если она вам понравилась!