Код
#База знаний

Создание объектов и конструкторы

Разбираем основные моменты, связанные с написанием собственных конструкторов классов в Java.

Процесс создания объектов — один из важных аспектов программирования на Java. Под созданием подразумевают как минимум две основные операции: создание нового объекта класса и инициализацию полей объекта.

В этой статье мы рассмотрим:

Что такое конструкторы классов

Конструктор — это специальный метод, который имеет имя, совпадающее с именем класса, и вызывается при создании экземпляра объекта совместно с оператором new. Результатом работы этого метода всегда является экземпляр класса.

Следовательно, конструктор класса Cat называется Cat(). В результате работы этого конструктора будет создан новый объект класса Cat. Цель конструктора — правильно инициализировать объект перед его использованием.

Самый простой способ создать объект — это строка вида:

Cat murka  = new Cat();

Рассмотрим порядок создания объекта. В этой строке выполняется три действия:

  • Во-первых, задаётся переменная класса Cat под именем murka. Эта переменная ещё не определяет объект, она просто даёт возможность ссылаться на него.
  • Во-вторых, создаётся физическая копия объекта, а ссылка на него присваивается переменной murka. Это делается с помощью оператора new. Оператор new динамически — во время выполнения программы — выделяет память для объекта и возвращает ссылку на него, которая представляет собой адрес области памяти.
  • В-третьих, ссылка на объект сохраняется в переменной. За это отвечает оператор =.

Приведённый выше код можно разбить на две строки:

Cat murka; // объявление ссылки на объект
murka = new Cat(); // выделение памяти для объекта типа Cat и присвоение значения ссылке

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

new Cat(); // такая операция не позволит дальше работать с объектом

В первой строке кода переменная murka объявляется как ссылка на объект типа Cat. Здесь важно понять, что объектная переменная фактически не содержит никакого объекта. Значение любой объектной переменной в Java представляет собой ссылку на объект, размещённый в памяти. В данный момент переменная murka пока ещё не ссылается на объект (содержит пустое значение null).

Во второй строке кода создаётся новый объект типа Cat, а ссылка на него присваивается переменной murka. С этого момента переменная murka оказывается ассоциированной с объектом. Чтобы работать с объектами, нужно сначала создать их и задать их исходное состояние. Затем к этим объектам можно применять методы.

Теперь взглянем на код класса Cat (без методов):

public class Cat {
    private double originWeight;
    private double weight;
    private double minWeight;
    private double maxWeight;

    public Cat() {
        weight = 1500.0 + 3000.0 * Math.random();
        originWeight = weight;
        minWeight = 1000.0;
        maxWeight = 9000.0;
    }
}

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

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

Для справки. Для числовых типов данных значением по умолчанию является нулевое, для типа boolean — логическое значение false, а для ссылочных типов — пустое значение null.

Но как только вы определите свой собственный конструктор, конструктор по умолчанию предоставляться не будет. Следовательно, если мы удалим конструктор из класса Cat и попытаемся создать объект через new Cat(), то объект будет создан, но все переменные в классе получат значения по умолчанию.

Сразу отметим, что полагаться на действия по умолчанию не следует. Если поля инициализируются неявно, программа становится менее понятной.

Как работают параметризованные конструкторы

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

Важно. Класс может иметь несколько различных конструкторов. Они (кстати, как и методы) отличаются между собой количеством, типом и порядком следования параметров. Если в классе несколько конструкторов с разным набором параметров, это называется перегрузкой конструктора.

Например, конструкторы ниже являются разными и вполне могут существовать в одном классе и иметь разную логику:

public Cat(String name, int weight){
    //код...
}
public Cat(String name, int weight, double tailLength){
    //код...
}

Заметьте, что конструкторы ниже для компилятора одинаковы и вызовут ошибку при запуске программы (тип, количество и порядок следования параметров идентичны):

public Cat(String newName, int weight){
    //код...
}
public Cat(String newName, int initialOriginWeight){
    //код...
}

Разберём пример применения параметризованного конструктора класса Cat с добавленным полем имени:

public class Cat {

    private double originWeight;
    private double weight;
    private double minWeight;
    private double maxWeight;
    private String name;

    public Cat() {
        name = "Безымянная";
        weight = 1500.0 + 3000.0 * Math.random();
        originWeight = weight;
        minWeight = 1000.0;
        maxWeight = 9000.0;
    }
    public Cat(String name)    {
        this.name = name;
        weight = 1500.0 + 3000.0 * Math.random();
        originWeight = weight;
        minWeight = 1000.0;
        maxWeight = 9000.0;
    }
}

Как видно, в этих конструкторах очень много кода повторяется, что в целом не очень удобно и повышает вероятность ошибки: ведь если нам потребуется поменять один конструктор, необходимо будет не забыть внести изменения в другой.

Для этого применяется вызов конструктора из конструктора с использованием ключевого слова this, которое означает ссылку на текущий объект. Обратиться к конструктору из другого конструктора можно через вызов this() — так будет выполнен конструктор без параметров. Если же нужен конструктор с параметрами, их указывают в скобках.

Применять ключевое слово this для вызова другого конструктора очень удобно — нужно лишь один раз написать общий код для конструирования объекта.

Важно. Вызов другого конструктора всегда должен стоять первой строкой в конструкторе.

Вот пример оптимизации кода первого из конструкторов:

public Cat() {
  this("Безымянная"); //конструктор по умолчанию сокращён до одной строки
}

Обратите внимание. Несколько слов насчёт ключевого слова this. Синтаксис языка Java не запрещает использовать имена параметров или локальных переменных, совпадающие с именами переменных экземпляра (класса). В таком случае говорят, что локальная переменная или параметр скрывает переменную экземпляра. При этом доступ к скрытой переменной экземпляра обеспечивается с помощью ключевого слова this.

Приведённый ниже пример конструктора класса Cat показывает, каким образом лучше выполнять присваивание переданных в конструктор параметров переменным класса:

public Cat(String name)    {
  this.name = name; //значение параметра присваивается переменной класса
  // остальной код конструктора...
}

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

Но в целом читаемость кода — это отдельная и довольно обширная тема.

Инициализация полей объекта

Основная задача конструкторов — подготовка объекта к работе с ним и установка значений для полей (переменных) объекта. Но есть и другие варианты установки значения для полей. Это явная инициализация и так называемые блоки инициализации.

Явная инициализация — это возможность присвоить полю соответствующее значение указанным ниже образом:

public class Cat {
    ...
    private double minWeight = 1000.0;
    private double maxWeight = 9000.0;

Здесь присваивание выполняется до вызова конструктора. Такой подход оказывается полезным, когда требуется, чтобы поле имело конкретное значение — независимо от вызова конструктора класса.

Блоки инициализации выглядят так:

public class Cat {
    private static int count;
    private int id;
    ...
    {
      id = count;
      count++;
    }

Такой блок выполняется каждый раз, когда создаётся объект данного класса. В этом примере начальное значение поля id задаётся в блоке инициализации объекта. Причём неважно, какой именно конструктор используется для создания экземпляра класса. Первым выполняется блок инициализации, а вслед за ним — тело конструктора.

Блоки инициализации могут быть и статическими. Перед ними пишется ключевое слово static.

    static {
      count++;
    }

В таком случае блок инициализации будет выполнен при первом обращении к этому классу. Попробуйте выполнить вот такой код:

public class Main {
  static {
    System.out.println("Static");
  }
  public static void main(String[] args) {
    System.out.println("Test");
  }
}

Что в итоге

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

1. Если в первой строке кода одного конструктора вызывается второй конструктор, то второй конструктор выполняется с предоставляемыми аргументами.

2. Иначе:

  • все поля инициализируются значениями, предусмотренными по умолчанию (0, false или null);
  • инициализируются все поля и блоки инициализации в порядке их следования в объявлении класса.

3. Выполняется тело конструктора.

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

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

На заметку. Важный момент, о котором часто забывают при написании собственных конструкторов, — в конструкторах не должно содержаться никакой бизнес-логики. Их задача — корректное создание объектов и подготовка их к дальнейшему использованию. Вся логика должна находиться в соответствующих методах.

Для изучения — пример, который содержит все элементы, описанные в статье. В нём есть:

  • применение конструктора без аргументов;
  • перегрузка конструкторов;
  • вызов одного конструктора из другого;
  • инициализация полей явным способом;
  • инициализация полей с использованием блока инициализации.
 public class Cat {
 private static int count = 0;
 static { //статический блок инициализации
   count = 1;
 }
 private double maxWeight = 9000.0;  //явная инициализация
 private double minWeight = 1000.0;
 private double originWeight;
 private double weight;
 private String name;
 private int id;

 { //блок инициализации
   id = count; //count к этому моменту получит значение 1
   count++;
 }

 //перегрузка конструкторов
 public Cat() {
   //вызов конструктора с именем, в качестве параметра передаётся имя по умолчанию
   this("Безымянная");
 }

 public Cat(String newName)    {
   //вызов конструктора с именем и весом, вес генерируется случайным образом
   this(newName, 1500.0 + 3000.0 * Math.random());
 }

 public Cat(String newName, double initialWeight){
   name = newName;
   weight = initialWeight;
 }
}

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


Проверьте свой английский. Бесплатно ➞
Нескучные задания: small talk, поиск выдуманных слов — и не только. Подробный фидбэк от преподавателя + персональный план по повышению уровня.
Пройти тест
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована