Перейти к содержанию

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

Конструкторы являются привратниками объектно-ориентированного проектирования.

В этом руководстве рассмотрим, как они действуют как единое место, из которого можно инициализировать внутреннее состояние создаваемого объекта.

 Давайте создадим простой объект, представляющий банковский счет.

Настройка банковского счета

Представьте, что нужно создать класс, представляющий банковский счет. Он будет содержать имя, дату создания и баланс.

Кроме того, переопределим метод toString для вывода сведений в консоль:

class BankAccount {
    String name;
    LocalDateTime opened;
    double balance;
    
    @Override
    public String toString() {
        return String.format("%s, %s, %f", 
          this.name, this.opened.toString(), this.balance);
    }
}

Теперь этот класс содержит все поля, необходимые для хранения информации о банковском счете, но еще не содержит конструктора.

Это означает, что если мы создадим новый объект, значения полей не будут инициализированы:

BankAccount account = new BankAccount();
account.toString();

Запуск метода toString, приведенного выше, приведет к исключению, поскольку поля name и opend объекта по-прежнему имеют значение null:

java.lang.NullPointerException
    at com.baeldung.constructors.BankAccount.toString(BankAccount.java:12)
    at com.baeldung.constructors.ConstructorUnitTest
      .givenNoExplicitContructor_whenUsed_thenFails(ConstructorUnitTest.java:23)

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

Исправим это с помощью конструктора:

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

Обратите внимание на некоторые особенности конструктора, который мы только что написали. Во-первых, это метод, но он не имеет возвращаемого типа. Это связано с тем, что конструктор неявно возвращает тип объекта, который он создает. Вызов new BankAccount() вызовет описанный выше конструктор.

Во-вторых, он не требует аргументов. Конструктор такого типа называется конструктором без аргументов.

Но почему он не понадобился в первый раз? Это потому, что, когда мы явно не пишем какой-либо конструктор, компилятор добавляет конструктор по умолчанию без аргументов.

Вот почему мы смогли сконструировать объект в первый раз, хотя и не писали конструктор явно. По умолчанию конструктор без аргументов устанавливает для всех элементов значения по умолчанию.

Для объектов это значение null, что привело к исключению, которое видели ранее.

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

Настоящим преимуществом конструкторов является то, что они помогают поддерживать инкапсуляцию при внедрении состояния в объект.

Чтобы сделать что-то действительно полезное с этим банковским счетом, нужно иметь возможность ввести в объект некоторые начальные значения.

Для этого напишем параметризованный конструктор, то есть конструктор, который принимает некоторые аргументы:

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

Теперь можно сделать что-нибудь полезное с классом BankAccount:

    LocalDateTime opened = LocalDateTime.of(2018, Month.JUNE, 29, 06, 30, 00);
    BankAccount account = new BankAccount("Tom", opened, 1000.0f); 
    account.toString();

Обратите внимание, что класс теперь имеет 2 конструктора. Явный конструктор без аргументов и параметризованный конструктор.

Можно создать столько конструкторов, сколько захотим, но не нужно создавать слишком много. Это только запутает.

Если обнаружим слишком много конструкторов в коде, могут оказаться полезными несколько порождающих шаблонов проектирования.

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

Конструкторы не должны ограничиваться только инициализацией. Их также можно использовать для создания объектов другими способами. Представьте, что нужно иметь возможность создать новую учетную запись из существующей. Новая учетная запись должна иметь то же имя, что и старая учетная запись, текущую дату создания и отсутствие средств. Можно сделать это с помощью конструктора копирования:

public BankAccount(BankAccount other) {
    this.name = other.name;
    this.opened = LocalDateTime.now();
    this.balance = 0.0f;
}

Теперь появилось следующее поведение:

LocalDateTime opened = LocalDateTime.of(2018, Month.JUNE, 29, 06, 30, 00);
BankAccount account = new BankAccount("Tim", opened, 1000.0f);
BankAccount newAccount = new BankAccount(account);

assertThat(account.getName()).isEqualTo(newAccount.getName());
assertThat(account.getOpened()).isNotEqualTo(newAccount.getOpened());
assertThat(newAccount.getBalance()).isEqualTo(0.0f);

Сцепленный конструктор

Конечно, можно вывести некоторые параметры конструктора или присвоить некоторым из них значения по умолчанию.

Например, можно создать новый банковский счет, указав только имя.

Давайте создадим конструктор с параметром name и присвоим другим параметрам значения по умолчанию:

public BankAccount(String name, LocalDateTime opened, double balance) {
    this.name = name;
    this.opened = opened;
    this.balance = balance;
}
public BankAccount(String name) {
    this(name, LocalDateTime.now(), 0.0f);
}

С помощью ключевого слова this вызываем другой конструктор.

Необходимо помнить, что если хотим связать конструктор суперкласса, то должны использовать super вместо this.

Кроме того, помните, что this или super всегда должно быть первым оператором.

Типы значений

Интересным применением конструкторов в Java является создание объектов-значений. Объект-значение – это объект, который не меняет своего внутреннего состояния после инициализации.

То есть объект неизменяемый. Неизменяемость в Java имеет некоторые нюансы, и при создании объектов следует соблюдать осторожность.

 Давайте создадим неизменяемый класс:

class Transaction {
    final BankAccount bankAccount;
    final LocalDateTime date;
    final double amount;

    public Transaction(BankAccount account, LocalDateTime date, double amount) {
        this.bankAccount = account;
        this.date = date;
        this.amount = amount;
    }
}

Обратите внимание, что теперь используем ключевое слово final при определении членов класса. Это означает, что каждый из этих членов может быть инициализирован только в конструкторе класса. Их нельзя переназначить позже внутри любого другого метода. Можно прочитать эти значения, но не изменить их.

Если создадим несколько конструкторов для класса Transaction, каждый конструктор должен будет инициализировать каждую final-переменную. Невыполнение этого требования приведет к ошибке компиляции.

Заключение

Мы ознакомились с различными способами, которыми конструкторы создают объекты. При разумном использовании конструкторы образуют основные строительные блоки объектно-ориентированного проектирования в Java.

Образцы кода можно найти на GitHub.

Оригинал