Привет! Разберемся, как работают конструкторы базовых классов в Java и в каком порядке инициализируются переменные при создании объекта. Коротко: при создании объекта сначала вызывается конструктор базового (родительского) класса, а потом конструктор самого класса. Вызвать конструктор родителя можно через super(), и он должен стоять в конструкторе первым.

Кратко

  • При создании объекта сначала вызывается конструктор базового класса, потом конструктор самого класса.
  • Наследование задается словом extends: класс-потомок получает доступ к полям и методам базового класса.
  • super() вызывает конструктор родителя и должен стоять в конструкторе первым.
  • Общие поля выносят в базовый класс, чтобы не дублировать их в потомках.
  • Порядок инициализации: статические поля (сначала базовый класс, потом потомок), затем нестатические поля и конструкторы в том же порядке.
В прошлый раз мы говорили о конструкторах и узнали о них достаточно много. Сейчас поговорим о такой вещи, как конструкторы базовых классов.

Что такое базовый класс

Дело в том, что в Java несколько разных классов могут иметь общее происхождение. Наследование: у нескольких классов-потомков один общий базовый классЭто называется наследованием. У нескольких классов-потомков может быть один общий класс-предок. Например, представим что у нас есть класс Animal (животное):

public class Animal {

   String name;
   int age;
}
Мы можем создать для него, например, 2 класса-потомка, Cat и Dog. Это делается с использованием ключевого слова extends.

public class Cat extends Animal {

}

public class Dog extends Animal {

}
Это может нам пригодиться в будущем. Например, если будет задача ловить мышей, создадим в программе объект Cat. Если задача бегать за палочкой, тут мы используем объект Dog. А если будем создавать программу, симулирующую ветеринарную клинику, она будет работать с классом Animal (чтобы уметь лечить и кошек, и собак).

Порядок вызова конструкторов

Очень важно запомнить на будущее, что при создании объекта в первую очередь вызывается конструктор его базового класса, а только потом конструктор самого класса, объект которого мы создаем. То есть при создании объекта Cat сначала отработает конструктор класса Animal, а только потом конструктор Cat. Чтобы убедиться в этом, добавим в конструкторы Cat и Animal вывод в консоль.

public class Animal {

   public Animal() {
       System.out.println("Отработал конструктор Animal");
   }
}


public class Cat extends Animal {

   public Cat() {
       System.out.println("Отработал конструктор Cat!");
   }

   public static void main(String[] args) {
       Cat cat = new Cat();
   }
}
Вывод в консоль:

Отработал конструктор Animal
Отработал конструктор Cat!
Действительно, все так и работает! Для чего это нужно? Например, чтобы не дублировать общие поля двух классов. Например, у каждого животного есть сердце и мозг, но не у каждого есть хвост. Мы можем объявить общие для всех животных поля brain и heart в родительском классе Animal, а поле tail в подклассе Cat. Теперь мы создадим конструктор для класса Cat, куда передадим все 3 поля.

public class Cat extends Animal {

   String tail;

   public Cat(String brain, String heart, String tail) {
       this.brain = brain;
       this.heart = heart;
       this.tail = tail;
   }

   public static void main(String[] args) {
       Cat cat = new Cat("Мозг", "Сердце", "Хвост");
   }
}
Обрати внимание: конструктор успешно работает, хотя в классе Cat нет полей brain и heart. Эти поля "подтянулись" из базового класса Animal. У класса-наследника есть доступ к полям базового класса, поэтому в нашем классе Cat они видны. Поэтому нам не нужно в классе Cat дублировать эти поля, мы можем взять их из класса Animal.

Ключевое слово super()

Более того, мы можем явно вызвать конструктор базового класса в конструкторе класса-потомка. Базовый класс еще называют "суперклассом", поэтому в Java для его обозначения используется ключевое слово super. В предыдущем примере

public Cat(String brain, String heart, String tail) {
       this.brain = brain;
       this.heart = heart;
       this.tail = tail;
   }
Мы отдельно присваивали каждое поле, которое есть в нашем родительском классе. На самом деле этого можно не делать. Достаточно вызвать конструктор родительского класса и передать ему нужные параметры:

public class Animal {

   String brain;
   String heart;

   public Animal(String brain, String heart) {
       this.brain = brain;
       this.heart = heart;
   }
}

public class Cat extends Animal {

   String tail;

   public Cat(String brain, String heart, String tail) {
       super(brain, heart);
       this.tail = tail;
   }

   public static void main(String[] args) {
       Cat cat = new Cat("Мозг", "Сердце", "Хвост");
   }
}
В конструкторе Cat мы вызвали конструктор Animal и передали в него два поля. Нам осталось явно проинициализировать только одно поле, tail, которого в Animal нет. Помнишь, мы говорили о том, что при создании объекта в первую очередь вызывается конструктор класса-родителя? Так вот, именно поэтому слово super() всегда должно стоять в конструкторе первым! Иначе логика работы конструкторов будет нарушена и программа выдаст ошибку.

public class Cat extends Animal {

   String tail;

   public Cat(String brain, String heart, String tail) {
       this.tail = tail;
       super(brain, heart);//ошибка!
   }

   public static void main(String[] args) {
       Cat cat = new Cat("Мозг", "Сердце", "Хвост");
   }
}
Компилятор знает, что при создании объекта класса-потомка сначала вызывается конструктор базового класса. И если ты попытаешься вручную изменить это поведение, он не позволит этого сделать. Подробнее о ключевом слове super можно прочитать в документации Oracle (Using the Keyword super).

Порядок инициализации при создании объекта

Выше мы с тобой рассмотрели пример с базовым и родительским классом, Animal и Cat. Теперь на примере этих двух классов мы рассмотрим процесс создания объекта и инициализации переменных. Мы знаем, что переменные бывают статическими и переменными экземпляра (нестатическими). Также мы знаем, что в базовом классе Animal есть свои переменные, а в классе-потомке Cat свои. Добавим к классу Animal и Cat по одной статической переменной для наглядности. Переменная animalCount в классе Animal будет означать общее число видов животных на земле, а переменная catsCount число видов семейства кошачьих. Кроме того, присвоим всем нестатическим переменным у обоих классов стартовые значения (которое потом изменится в конструкторе).

public class Animal {

   String brain = "Изначальное значение brain в классе Animal";
   String heart = "Изначальное значение heart в классе Animal";

   public static int animalCount = 7700000;

   public Animal(String brain, String heart) {
       System.out.println("Выполняется конструктор базового класса Animal");
       System.out.println("Были ли уже проинициализированы переменные класса Animal?");
       System.out.println("Текущее значение статической переменной animalCount = " + animalCount);
       System.out.println("Текущее значение brain в классе Animal = " + this.brain);
       System.out.println("Текущее значение heart в классе Animal = " + this.heart);
       System.out.println("Были ли уже проинициализированы переменные класса Cat?");
       System.out.println("Текущее значение статической переменной catsCount = " + Cat.catsCount);

       this.brain = brain;
       this.heart = heart;
       System.out.println("Конструктор базового класса Animal завершил работу!");
       System.out.println("Текущее значение brain = " + this.brain);
       System.out.println("Текущее значение heart = " + this.heart);
   }
}

public class Cat extends Animal {

   String tail = "Изначальное значение tail в классе Cat";

   static int catsCount = 37;

   public Cat(String brain, String heart, String tail) {
       super(brain, heart);
       System.out.println("Конструктор класса Cat начал работу (конструктор Animal уже был выполнен)");
       System.out.println("Текущее значение статической переменной catsCount = " + catsCount);
       System.out.println("Текущее значение tail = " + this.tail);
       this.tail = tail;
       System.out.println("Текущее значение tail = " + this.tail);
   }

   public static void main(String[] args) {
       Cat cat = new Cat("Мозг", "Сердце", "Хвост");
   }
}
Итак, мы создаем новый объект класса Cat, унаследованного от Animal. Добавим в нашу программу подробный консольный вывод, чтобы посмотреть что и в каком порядке будет происходить. Вот что будет выведено в консоль в результате создания объекта Cat:

Выполняется конструктор базового класса Animal
Были ли уже проинициализированы переменные класса Animal?
Текущее значение статической переменной animalCount = 7700000
Текущее значение brain в классе Animal = Изначальное значение brain в классе Animal
Текущее значение heart в классе Animal = Изначальное значение heart в классе Animal
Были ли уже проинициализированы переменные класса Cat?
Текущее значение статической переменной catsCount = 37
Конструктор базового класса Animal завершил работу!
Текущее значение brain = Мозг
Текущее значение heart = Сердце
Конструктор класса Cat начал работу (конструктор Animal уже был выполнен)
Текущее значение статической переменной catsCount = 37
Текущее значение tail = Изначальное значение tail в классе Cat
Текущее значение tail = Хвост
Итак, теперь мы наглядно видим в каком порядке происходит инициализация переменных и вызов конструкторов при создании нового объекта:
  1. Инициализируются статические переменные базового класса (Animal). В нашем случае переменной animalCount класса Animal присваивается значение 7700000.

  2. Инициализируются статические переменные класса-потомка (Cat). Обрати внимание: мы все еще внутри конструктора Animal, а в консоли уже написано:

    
        Выполняется конструктор базового класса Animal
        Были ли уже проинициализированы переменные класса Animal?
        Текущее значение статической переменной animalCount = 7700000
        Текущее значение brain в классе Animal = Изначальное значение brain в классе Animal
        Текущее значение heart в классе Animal = Изначальное значение heart в классе Animal
        Были ли уже проинициализированы переменные класса Cat?
        Текущее значение статической переменной catsCount = 37
    
  3. Дальше инициализируются нестатические переменные базового класса. Мы специально присвоили им первоначальные значения, которые потом в конструкторе меняются на новые. Конструктор Animal еще не отработал до конца, но первоначальные значения brain и heart уже присвоены:

    
        Выполняется конструктор базового класса Animal
        Были ли уже проинициализированы переменные класса Animal?
        Текущее значение статической переменной animalCount = 7700000
        Текущее значение brain в классе Animal = Изначальное значение brain в классе Animal
        Текущее значение heart в классе Animal = Изначальное значение heart в классе Animal
    
  4. Начинает работу конструктор базового класса.

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

  5. Инициализация нестатических полей дочернего класса (Cat).

    Она происходит раньше, чем конструктор Cat начинает работу.

    На момент, когда он начал работу, у переменной tail уже было значение:

    
    Конструктор класса Cat начал работу (конструктор Animal уже был выполнен)
    Текущее значение статической переменной catsCount = 37
    Текущее значение tail = Изначальное значение tail в классе Cat
    
  6. Вызывается конструктор класса потомка Cat

Вот так выглядит процесс создания объекта в Java! Коротко весь порядок инициализации можно свести в таблицу:
ШагЧто инициализируется
1статические поля базового класса (Animal)
2статические поля класса-потомка (Cat)
3нестатические поля базового класса
4конструктор базового класса
5нестатические поля класса-потомка
6конструктор класса-потомка
Надо сказать, что мы не большие поклонники зубрежки, но порядок инициализации переменных и вызова конструкторов лучше выучить наизусть и запомнить на будущее. Это сильно увеличит твое понимание хода работы программы, и состояния твоих объектов в каждый конкретный момент. Тем более, что часто у классов нет прямых родительских классов (суперклассов/ базовых классов). В таком случае пункты, связанные с базовым классом, не будут выполняться.

Вопросы и ответы

Что такое базовый класс в Java?

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

В каком порядке вызываются конструкторы при наследовании?

Сначала вызывается конструктор базового класса, потом конструктор класса-потомка. Например, при создании Cat сначала отработает конструктор Animal, затем конструктор Cat.

Что делает ключевое слово super()?

super() вызывает конструктор базового класса и передает ему нужные параметры. Оно должно стоять первой строкой в конструкторе потомка.

Почему super() должен быть первым в конструкторе?

Потому что конструктор базового класса всегда выполняется раньше конструктора потомка. Если поставить super() не первым, компилятор выдаст ошибку.

Какой порядок инициализации переменных при создании объекта?

Сначала статические поля базового класса, потом статические поля потомка, затем нестатические поля базового класса и его конструктор, потом нестатические поля потомка и его конструктор.

Зачем нужны базовые классы?

Чтобы не дублировать общие поля и методы. Общее выносят в базовый класс, а специфичное оставляют в потомках. Это уменьшает количество кода.

Читайте также