1. Инициализация переменных

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

Однако эти же переменные можно инициализировать и в конструкторе. Поэтому теоретически возможна ситуация, когда одним и тем же переменным класса значения присваиваются дважды. Пример

Код Примечание
class Cat
{
   public String name;
   public int age = -1;

   public Cat(String name, int age)
   {
     this.name = name;
     this.age = age;
   }

   public Cat()
   {
     this.name = "Безымянный";
   }
}



Переменной age присваивается стартовое значение




Стартовое значение перетирается


Для age используется стартовое значение
 Cat cat = new Cat("Васька", 2);
Так можно: вызовется первый конструктор
 Cat cat = new Cat();
Так можно: вызовется второй конструктор

Вот что будет происходить при выполнении кода Cat cat = new Cat("Васька", 2);:

  • Создается объект типа Cat
  • Инициализируются все переменные класса своими стартовыми значениями
  • Вызывается конструктор и выполняется его код.

Т.е. переменные класса сначала инициализируются своими значениями, а уже затем выполняется код конструкторов.


2. Порядок инициализации переменных класса

Переменные не просто инициализируются до работы конструктора: они еще и инициализируются в четко заданном порядке — порядке объявления в классе.

Давайте рассмотрим такой интересный код:

Код Примечание
public class Solution
{
   public int a = b + c + 1;
   public int b = a + c + 2;
   public int c = a + b + 3;
}

Такой код не скомпилируется, т.к. на момент создания переменной а, переменных b и c еще нет. А вот так записать можно, и этот код отлично скомпилируется и будет работать.

Код Примечание
public class Solution
{
   public int a;
   public int b = a + 2;
   public int c = a + b + 3;
}


0
0+2
0+2+3

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

Тут нужно помнить, что все переменные класса до того, как им присвоили какое-либо значение, имеют значение по умолчанию. Для типа int это ноль.

Когда JVM будет инициализировать переменную а, просто присвоит ей значение по умолчанию для типа int — 0.

Когда очередь дойдет до b, переменная a уже будет известна и содержать значение, поэтому JVM присвоит ей значение 2.

Ну а когда дело дойдет до переменной c, переменные а и b уже будет проинициализированы, и JVM легко вычислит стартовое значение для с: 0+2+3.

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


3. Константы

Раз уж мы продолжаем разбирать процесс создания объекта, стоит затронуть вопрос инициализации констант — переменных класса, которые имеют модификатор final.

Если переменная класса имеет модификатор final, ей должно быть присвоено стартовое значение. Это вы уже знаете, и в этом нет ничего удивительного.

Но вот чего вы не знаете, так это того, что стартовое значение можно сразу не присваивать, если присвоить его в конструкторе. И это отлично будет работать для final-переменной. Единственное требование — если конструкторов несколько, final переменной должно быть присвоено значение во всех конструкторах.

Пример:

public class Cat
{
   public final int maxAge = 25;
   public final int maxWeight;

   public Cat (int weight)
   {
      this.maxWeight = weight; // занесение стартового значения в константу
   }
}

undefined
12
Задача
Java Syntax Pro, 12 уровень, 2 лекция
Недоступна
Солнечная система — наш дом
Программа выводит на экран основные факты о Солнечной системе. Перепиши код так, чтобы в выводе использовались все переменные класса SolarSystem, но результат работы программы при этом не изменился. Ожидаемый вывод: Человечество живет в Солнечной системе. Ее возраст около 4568200000 лет. В Солнечно

4. Код в конструкторе

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

Например, одно важное замечание насчет конструкторов. Теоретически в конструкторе можно писать код любой сложности. Но не нужно этого делать. Пример:

class FilePrinter
{
   public String content;

   public FilePrinter(String filename) throws Exception
   {
      FileInputStream input = new FileInputStream(filename);
      byte[] buffer = input.readAllBytes();
      this.content = new String(buffer);
   }

   public void printFile()
   {
      System.out.println(content);
   }
}






Открываем поток чтения файла
Читаем файл в массив байт
Сохраняем массив байт в виде строки




Выводим содержимое файла на экран

В конструкторе класса FilePrinter мы сразу открыли байтовый поток к файлу и прочитали его содержимое. Это достаточно сложное поведение, которое может потенциально привести к ошибкам.

А что если бы такого файла не было? А если бы были проблемы с его чтением? А если бы он был слишком большим?

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

Пример 1 – Сериализация

В стандартной Java-программе есть много ситуаций, когда объекты вашего класса создаются не вами. Например, вы решили передать объект по сети: в таком случае Java-машина сама превратит ваш объект в набор байт, передаст его и снова по набору байт создаст объект.

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

Пример 2 — Инициализация полей класса

Если конструктор вашего класса может выбросить checked-исключения – содержит ключевое слово throws, вы обязаны перехватить это исключение в методе, который создает ваш объект.

А если такого метода нет? Пример:

Код  Примечание
class Solution
{
   public FilePrinter reader = new FilePrinter("c:\\readme.txt");
}
Такой код не скомпилируется.

Конструктор класса FilePrinter содержит checked-исключения: вы не можете создать объект FilePrinter, не обернув его в try-catch. А try-catch можно писать только в методе


undefined
12
Задача
Java Syntax Pro, 12 уровень, 2 лекция
Недоступна
Земля: техническая характеристика
Сделай все переменные класса Planet не статическими. После этого внеси необходимые правки в метод main.

5. Конструктор базового класса

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

Если вы унаследуете свой класс от другого класса, фактически в объект вашего класса будет встроен объект класса-родителя. Причем этот класс-родитель имеет свои переменные класса и свои конструкторы.

Поэтому вам очень важно знать и понимать, как же происходит инициализация параметров и вызов конструкторов, когда у вашего класса есть класс-родитель, чьи переменные и методы вы наследуете.

Классы

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

Код Примечание
class ParentClass
{
   public String a;
   public String b;

   public ParentClass()
   {
   }
}

class ChildClass extends ParentClass
{
   public String c;
   public String d;

   public ChildClass()
   {
   }
}










Класс ChildClass наследуется от класса ParentClass.

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

Логирование

Логированием называется запись в консоль или файл действий, которые происходят во время работы программы.

Определить, что вызвался конструктор, довольно просто: нужно в теле конструктора написать в консоль сообщение об этом. А вот как определить, что переменная инициализировалась?

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

Финальный код

public class Main
{
   public static void main(String[] args)
   {
      ChildClass obj = new ChildClass();
   }

   public static String print(String text)
   {
      System.out.println(text);
      return text;
   }
}

class ParentClass
{
   public String a = Main.print("ParentClass.a");
   public String b = Main.print("ParentClass.b");

   public ParentClass()
   {
      Main.print("ParentClass.constructor");
   }
}

class ChildClass extends ParentClass
{
   public String c = Main.print("ChildClass.c");
   public String d = Main.print("ChildClass.d");

   public ChildClass()
   {
      Main.print("ChildClass.constructor");
   }
}




Создаем объект типа ChildClass


Этот метод пишет в консоль переданный текст и возвращает его же





Объявляем ParentClass

Пишем текст и им же инициализируем переменные




Пишем в консоль сообщение о вызове конструктора. Возвращаемое значение игнорируем.


Объявляем ChildClass

Пишем текст и им же инициализируем переменные




Пишем в консоль сообщение о вызове конструктора. Возвращаемое значение игнорируем.

Если выполнить этот код, на экран выведется текст:

Вывод на экран метода Main.print()
ParentClass.a
ParentClass.b
ParentClass.constructor
ChildClass.c
ChildClass.d
ChildClass.constructor

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


undefined
12
Задача
Java Syntax Pro, 12 уровень, 2 лекция
Недоступна
Презентация роботов
В методе main создается 5 роботов и выводится информация о них. Убери максимально возможное количество модификаторов static так, чтобы функционал программы не изменился. В методе main менять ничего не нужно.