JavaRush /Java блог /Java Developer /Конструкторы базовых классов
Автор
Владимир Портянко
Java-разработчик в Playtika

Конструкторы базовых классов

Статья из группы Java Developer
Привет! В прошлый раз мы говорили о конструкторах, и узнали о них достаточно много. Сейчас мы поговорим о такой вещи, как конструкторы базовых классов. Что такое базовый класс? Дело в том, что в Java несколько разных классов могут иметь общее происхождение. Конструкторы базовых классов   - 2Это называется наследованием. У нескольких классов-потомков может быть один общий класс-предок. Например, представим что у нас есть класс 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. Более того, мы можем явно вызвать конструктор базового класса в конструкторе класса-потомка. Базовый класс еще называют “суперклассом”, поэтому в 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("Мозг", "Сердце", "Хвост");
   }
}
Компилятор знает, что при создании объекта класса-потомка сначала вызывается конструктор базового класса. И если ты попытаешься вручную изменить это поведение - он не позволит этого сделать.

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

Выше мы с тобой рассмотрели пример с базовым и родительским классом — 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! Надо сказать, что мы не большие поклонники зубрежки, но порядок инициализации переменных и вызова конструкторов лучше выучить наизусть и запомнить на будущее. Это сильно увеличит твое понимание хода работы программы, и состояния твоих объектов в каждый конкретный момент. Тем более, что часто у классов нет прямых родительских классов (суперклассов/ базовых классов). В таком случае пункты, связанные с базовым классом, не будут выполняться.
Комментарии (313)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Filand Gor Уровень 29
9 марта 2024
в пример следует добавить статик области и {}
{Java_Shark} Уровень 16
1 марта 2024
Отличная информация, автору огромный респект++
Dmitriy Kvitka Уровень 29
8 января 2024
Не согласен с утверждением, что инициализация статических переменных происходит в процессе создания новых экземпляров классов (объектов). Статические переменные инициализируются при "запуске программы". Именно поэтому посмотреть значение поля public static int animalCount = 7700000; мы можем в любой момент до создания объекта.
Вячеслав Уровень 28
11 ноября 2023
Сначала создается животное (конструктор базового класса) потом отращиваются плавники, или крылья (конструктор самого класса) тоже самое что развитие эмбрионов
Vladislav Korol Уровень 13
26 октября 2023
Лекция супер, но не раскрыт один тонкий момент. Если в классе Animal будет несколько конструкторов, то как понять какой именно из этих конструкторов будет выполняться первым. Поигравшись в идеи понял, что в родительском классе выполняться будет именно тот конструктор, поля которого мы вызываем в конструкторе класса Cat. Например: имеем такие конструктора в классеAnimal public Animal(String brain, String heart) { System.out.println("Конструктор с 2мя элементами класса Animal"); } public Animal(String brain) { System.out.println("Конструктор с одним элементом класса Animal"); } public Animal() { System.out.println("Пустой конструктор класса Animal"); } И если в конструкторе Cat мы не используем никакие поля из класса Animal, нет этой строчки super(какое-то поле); public Cat(String brain, String tail) { System.out.println("Конструктор класса Cat начал работу (конструктор Animal уже был выполнен)"); System.out.println(this.tail = tail); } То будет использован базовый конструктор в классе Animal. И получим вывод в консоль : Пустой конструктор класса Animal Конструктор класса Cat начал работу (конструктор Animal уже был выполнен) Хвост Если добавить в конструктор класса Cat строчку super(brain); то будет использован конструктор класса Animal с таким же полем и получим вывод: Конструктор с одним элементом класса Animal Конструктор класса Cat начал работу (конструктор Animal уже был выполнен) Хвост Пока понимание такое, надеюсь Я прав и в следующих лекциях более подробно это все раскроется )
Anatoly Уровень 30
26 августа 2023
nice
PhanSca Уровень 48
14 июля 2023
Схема, которую тут активно продвигают, не совсем верна "1. static field parent 2. static { } parent 3. static field child 4.static { } сhild 5. non static field parent 6. non static { } parent 7. constructor parent 8. non static field child 9. non static { } child 10. constructor child" Правильный вариант 1. static field parent /static { } parent – в зависимости от порядка записи 2. static field child / static { } сhild – в зависимости от порядка записи 3. non static field parent / non static { } parent – в зависимости от порядка записи 4. constructor parent 5. non static field child / non static { } child - в зависимости от порядка записи 6. constructor child Что значит "в зависимости от порядка записи"? Например,

static {
		System.out.println("Текущее значение статической переменной catsCount = " + Cat.catsCount);
		//еще не проинициализирована, так как идет позже - равна 0
	}
	static int catsCount = 37;
	
	static {
		System.out.println("Текущее значение статической переменной catsCount = " + Cat.catsCount);
		//проинициализирована, так как идет раньше - равна 37
	}
Если вдруг переменная будет объявлена ПОСЛЕ блока, то сначала будет выполнен блок.
12 июля 2023
Почему базовый класс не стартует самым первым?
ForJavaRush Уровень 15
10 июня 2023
Полезно про порядок инициализации но блин чувствую что это можно и подзабыть)
biba444 Уровень 29
26 мая 2023
Чудесная лекция)