1. Введение
Конструкторы являются привратниками объектно-ориентированного проектирования
.
В этом руководстве мы увидим, как они действуют как единое место, из которого можно инициализировать внутреннее состояние создаваемого объекта.
Давайте продвинемся вперед и создадим простой объект, представляющий банковский счет.
2. Настройка банковского счета
Представьте, что нам нужно создать класс, представляющий банковский счет. Он будет содержать имя, дату создания и баланс.
Кроме того, давайте переопределим метод 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
, приведенного выше, приведет к исключению, потому что имя
объекта и открытый
объект по- прежнему имеют значение null
:
java.lang.NullPointerException
at com.foreach.constructors.BankAccount.toString(BankAccount.java:12)
at com.foreach.constructors.ConstructorUnitTest
.givenNoExplicitContructor_whenUsed_thenFails(ConstructorUnitTest.java:23)
3. Конструктор без аргументов
Давайте исправим это с помощью конструктора:
class BankAccount {
public BankAccount() {
this.name = "";
this.opened = LocalDateTime.now();
this.balance = 0.0d;
}
}
Обратите внимание на некоторые особенности конструктора, который мы только что написали. Во-первых, это метод, но он не имеет возвращаемого типа. Это связано с тем, что конструктор неявно возвращает тип объекта, который он создает. Вызов new BankAccount()
вызовет описанный выше конструктор.
Во-вторых, он не требует аргументов. Конструктор такого типа называется конструктором с аргументом o
.
Но почему он нам не понадобился в первый раз? Это потому, что когда мы явно не пишем какой-либо конструктор, компилятор добавляет конструктор по умолчанию без аргументов .
Вот почему мы смогли сконструировать объект в первый раз, хотя мы и не писали конструктор явно. По умолчанию конструктор без аргументов просто устанавливает для всех членов значения по умолчанию .
Для объектов это значение null,
что привело к исключению, которое мы видели ранее.
4. Параметризованный конструктор
Настоящим преимуществом конструкторов является то, что они помогают нам поддерживать инкапсуляцию
при внедрении состояния в объект.
Итак, чтобы сделать что-то действительно полезное с этим банковским счетом, нам нужно иметь возможность ввести в объект некоторые начальные значения.
Для этого напишем параметризованный конструктор
, то есть конструктор, принимающий некоторые аргументы :
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 конструктора. Явный конструктор без аргументов и параметризованный конструктор.
Мы можем создать столько конструкторов, сколько захотим, но мы, вероятно, не хотели бы создавать слишком много. Это было бы немного запутанно.
Если мы обнаружим слишком много конструкторов в нашем коде, могут оказаться полезными несколько порождающих шаблонов проектирования .
5. Конструктор копирования
Конструкторы не должны ограничиваться только инициализацией. Их также можно использовать для создания объектов другими способами. Представьте, что нам нужно иметь возможность создать новую учетную запись из существующей.
Новая учетная запись должна иметь то же имя, что и старая учетная запись, текущую дату создания и отсутствие средств. Мы можем сделать это с помощью конструктора копирования
:
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);
6. Цепной конструктор
Конечно, мы можем вывести некоторые параметры конструктора или присвоить некоторым из них значения по умолчанию.
Например, мы могли бы просто создать новый банковский счет, указав только имя.
Итак, давайте создадим конструктор с параметром имени
и присвоим другим параметрам значения по умолчанию:
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
.
Кроме того, помните, что это
или супервыражение
всегда должно быть первым оператором.
7. Типы значений
Интересным применением конструкторов в 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
, каждый конструктор должен будет инициализировать каждую конечную переменную. Невыполнение этого требования приведет к ошибке компиляции.
8. Заключение
Мы ознакомились с различными способами, которыми конструкторы создают объекты. При разумном использовании конструкции образуют основные строительные блоки объектно-ориентированного проектирования в Java.
Как всегда, образцы кода можно найти на GitHub.