Поговорим про порядок инициализации полей класса. Это довольно важная тема, учитывая, что часто необходимо понимать, какое поле в какой момент уже проинициализировано, особенно, если у нас не просто один класс, а целая иерархия.
Для начала вспомним, что
в Java
поля могут принадлежать как классу, так и объекту класса.
Поля принадлежащие классу - это статические поля. Статическое поле доступно без создания экземпляра класса - а это значит, что инициализация статических полей и полей, принадлежащих объекту класса, происходит в разное время.
Но в какой момент вообще происходит инициализация класса или интерфейса?
Класс или интерфейс - T - будет инициализирован сразу перед появлением следующих ситуаций:
- T является классом и создаётся экземпляр T.
- Вызывается статический метод, объявленный в T.
- Присваивается значение статическому полю, объявленному в T.
- Используется значение статического поля T.
Другими словами инициализация происходит тогда, когда нам впервые понадобится класс. Это довольно логично, неправда ли? Зачем загружать все подряд, если многое может и не понадобится?
Перед инициализацией класса сначала инициализируются все его суперклассы,
также как и все интерфейсы, которые объявляют так называемые default-методы
.
Методы по умолчанию появились в
Java 8
и представляют по сутиdefault
-ую реализиацию метода интерфейса.При реализации интерфейса мы можем не переопределять такие методы, если нас устраивает поведение по-умолчанию, которое реализовано в интерфейсе.
В свою очередь существуют несколько типов инициализации:
- Инициаилиазция в месте объявления.
- Инициаилизация в конструкторе.
- Инициализация в блоке.
Инициализация в блоке - имеется в виду блок кода, выделенный в {}.
Подробнее про каждый тип:
Данный тип применим как к полям класса, так и к полям объекта класса.
Пример:
class Test {
public static final int BUFFER_SIZE = 32;
public int current = 14;
}
Данный тип применять следует в случаях, когда инициализация может быть произведена коротким выражением, при этом необходимый контекст доступен.
Это значит, что если вы обладаете всем необходимым для инициализации этого поля и при этом это можно сделать одной/двумя строчками - то подобный подход оправдан. Оправдан подобный подход и тем, что, как мы узнаем далее из тексат - такая инициализация произойдет до конструктора.
Однако, если инициализация происходит ,например, вызовом метода или в блоке - то стоит задуматься - возможно, было бы правильно вынести это в конструктор класса?
Лично я предпочитаю инициализацию в месте объявления применять только для статических полей, либо задания каких-то примитивов. Просто потому, что обычно основная инициализация происходит в конструкторе.
Данный тип применим только для инициализации полей объекта класса - что довольно логично.
Пример:
class Test {
public int current;
public Test(int current) {
this.current = current;
}
}
Применяется, если для инициализации нужны параметры конструктора - нужен контекст.
Я предпочитаю в конструкторе же производить и инициализацю полей типа коллекций и т.д
Если эти поля принадлежат именно объекту класса - instance-у, то и инициализировать поля такие лично мне кажется логичнее в конструкторе - при создании объекта класса.
Данный тип применим для полей как класса, так и объекта класса. В данном случае объявляется целый блок кода, в котором описывается логика.
Пример:
class Test {
static List<Character> lst;
static {
lst = new ArrayList<Character>();
for (char c = 'a'; c <= 'z'; c++) {
lst.add(c);
}
}
{
System.out.println("Block init");
}
}
Статический блок - выполнится при загрузке класса, что часто бывает нужно. Также применяется тогда, когда инициализирующий код неудобно записывать одним выржением:
Map<String, String> map = new HashMap<String, String>() {
{
put("паук", "арахнид");
put("птица", "архозавр");
put("кит", "зверь");
}
};
После рассмотрения возможных типов инициализации рассмотрим - в каком порядке она происходит.
Зададим класс, без иерархии, со всеми типами инициализации, которые были описаны выше, а для отслеживания порядка иницаилизации добавим везде выводы в консоль:
public class ClassInitializationOrder {
private static final String CONST = printAndGetStringConst();
private String sField = printAndGetString();
public ClassInitializationOrder(String s) {
System.out.println(s + " in constructor initialization");
}
static {
System.out.println("Static init 1");
}
{
System.out.println("Block init 1");
}
static {
System.out.println("Static init 2");
}
{
System.out.println("Block init 2");
}
private static String printAndGetStringConst() {
System.out.println("CONST initialization");
return "CONST";
}
public static void main(String[] args) {
new ClassInitializationOrder("FIRST");
new ClassInitializationOrder("SECOND");
}
private String printAndGetString() {
System.out.println("sField initialization");
return "sField";
}
}
Итак, если коротко - у нас есть класс ClassInitializationOrder
, у которого два поля:
sField
принадлежит объекту класса, CONST
является статическим и принадлежит классу.
Есть конструткор и четыре блока кода, два из которых статические.
В методе main
мы создадим два экземпляра класса, запустим и увидим:
CONST initialization
Static init 1
Static init 2
sField initialization
Block init 1
Block init 2
FIRST in constructor initialization
sField initialization
Block init 1
Block init 2
SECOND in constructor initialization
Разберем что тут случилось.
Вспомним, что при первом обращении к классу идет его загрузка с помощью ClassLoader
-а.
При загрузке класса идет инициализация всего, что относится к классу - а значит статические переменные, как и статические код блоки, инициализируются на данном этапе.
Отметим, что статические переменные и блоки инициализируются строго в порядке объявления - что мы и видим в выводе работы программы:
- Инициализируется константа, принадлежащая классу.
- Происходит инициализация статических блоков - также, в порядке объявления.
Итак, класс загружен ClassLoader
-ом, теперь происходит инициализация полей, принадлежащих объекту - также, в порядке объявления.
И уже в конце, после всех инициализаций, происходит вызов конструктора.
А вот создание второго объекта класса вывело на консоль меньше информации.
Почему создание второго объекта класса вышло короче - 4 выведенных строчки?
Это произошло потому, что класс уже загружен ClassLoader
-ом,
нет необходимости его загружать второй раз - это как минимум нелогично -
когда вы хотите открыть дверь и у вас в кармане ключи - вы не идете заказывать новую копию ключей же?
Поэтому, так как статические поля и блоки уже инициализированы при загрузке класса, при создании второго объекта класса происходит инициализация только полей, принадлежащих именно этому объекту.
Для закрепления, повторим порядок:
- Инициализация статических полей и блоков строго в порядке объявления.
- Инициализация полей и блоков объекта строго в порядке объявления.
- Инициализация полей в конструкторе.
Как же влияет наследование на порядок инициализации?
На самом деле - все происходит точно также, как было описано, за исключением того, что при наследовании необходимо(и это логично) в начале отработать с родительским классом, а уже потом - с дочерним.
Пусть у нас есть два класса - Parent
и Child
, где родитель стоит выше по иерархии, а ребенок наследуется от него.
Тогда порядок инициализации будет:
- Статические поля класса
Parent
- Статический блок инициализации класса
Parent
- Статические поля класса
Child
- Статический блок инициализации класса
Child
- Нестатические поля класса
Parent
- Нестатический блок инициализации класса
Parent
- Конструктор класса
Parent
- Нестатические поля класса
Сhild
- Нестатический блок инициализации класса
Сhild
- Конструктор класса
Сhild
В целом - тут понятно, что в начале загружается и инициализируется класс Parent
, после него происходит загрузка класса Child
- класса наследника.
Далее происходит инициализация нестатических полей и блоков - по сути создание объекта уже началось. И сначала также инициализируется родитель, а после - наследник.
В качестве примера возьмем два класса и повторим наш эксперимент:
public class Parent {
private static final String PCONST = printAndGetStringConst();
static {
System.out.println("Parent static code block");
}
{
System.out.println("Parent non static code block");
}
public Parent() {
System.out.println("Parent constructor");
}
private static String printAndGetStringConst() {
System.out.println("Parent_CONST initialization");
return "Parent_CONST";
}
}
И класс наследник:
public class Child extends Parent {
private static final String CONST = printAndGetStringConst();
static {
System.out.println("Child static code block");
}
{
System.out.println("Child non static code block");
}
public Child() {
System.out.println("Child constructor");
}
private static String printAndGetStringConst() {
System.out.println("CONST initialization");
return "CONST";
}
}
Код проверки приведем следующий:
public class ExampleParentChildInitOreder {
public static void main(String[] args) {
new Child();
}
}
Запускаем и смотрим на вывод в консоль:
Parent_CONST initialization
Parent static code block
Child CONST initialization
Child static code block
Parent non static code block
Parent constructor
Child non static code block
Child constructor
Т.е сначала инициализируется все, что относится к классу родителя, потом к классу ребенка, объект которого мы создаем.
После чего идет инициализация родительских полей, вызывается конструктор родителя и уже после этого - инициализация полей и вызов конструктора ребенка.
-
Инициализация класса или интерфейса происходит в момент, когда нам впервые понадобился данный класс.
Помните!
Обращение к статическому полю приводит к инициализации только того класса или интерфейса, который объявляет это поле, даже если обращение к этому полю было по имени дочернего класса, дочернего интерфейса или класса, реализующего интерфейс.
-
Инициализация полей класса просиходит в следующем порядке:
- Инициализация статических полей и блоков строго в порядке объявления.
- Инициализация полей и блоков объекта строго в порядке объявления.
- Инициализация полей в конструкторе.
-
Инициализация полей классов при наследовании происходит в следующем порядке:
- Статические поля класса
Parent
- Статический блок инициализации класса
Parent
- Статические поля класса
Child
- Статический блок инициализации класса
Child
- Нестатические поля класса
Parent
- Нестатический блок инициализации класса
Parent
- Конструктор класса
Parent
- Нестатические поля класса
Сhild
- Нестатический блок инициализации класса
Сhild
- Конструктор класса
Сhild
- Статические поля класса