Странное поведение Java со статическими и финальными квалификаторами
В нашей команде мы обнаружили какое-то странное поведение, в котором мы использовали квалификаторы static
и final
. Это наш тестовый класс:
public class Test {
public static final Test me = new Test();
public static final Integer I = 4;
public static final String S = "abc";
public Test() {
System.out.println(I);
System.out.println(S);
}
public static Test getInstance() { return me; }
public static void main(String[] args) {
Test.getInstance();
}
}
Когда мы запускаем метод main
, получаем результат:
null
abc
Я бы понял, написал ли он значения null
оба раза, так как код статических членов класса выполняется сверху вниз.
Может ли кто-нибудь объяснить, почему это поведение происходит?
Ответы
Ответ 1
Это шаги, предпринятые при запуске вашей программы:
- До запуска
main
класс Test
должен быть инициализирован путем запуска статических инициализаторов в порядке появления.
- Чтобы инициализировать поле
me
, начните выполнение new Test()
.
- Распечатайте значение
I
. Поскольку тип поля Integer
, то, что похоже на константу времени компиляции 4
, становится вычисленным значением (Integer.valueOf(4)
). Инициализатор этого поля еще не запущен, напечатав начальное значение null
.
- Распечатайте значение
S
. Поскольку он инициализируется константой времени компиляции, это значение выпекается на сайте ссылок, печатая abc
.
-
new Test()
завершается, теперь выполняется инициализатор для I
.
Урок: если вы полагаетесь на нетерпеливо инициализированные статические синглтоны, поместите объявление singleton в качестве последнего объявления статического поля или прибегайте к статическому блоку инициализатора, который возникает после всех других статических объявлений. Это приведет к тому, что класс будет полностью инициализирован единичному строительному коду.
Ответ 2
S
- это константа времени компиляции, следуя правилам JLS 15.28. Поэтому любое появление S
в коде заменяется значением, которое известно во время компиляции.
Если вы измените тип I
на int
, вы тоже увидите то же самое.
Ответ 3
У вас странное поведение из-за типа данных Integer
. Относительно JLS 12.4.2 статические поля инициализируются в том порядке, в котором вы его записываете, НО сначала инициализируются константы компиляции.
Если вы не используете тип оболочки Integer
, но тип int
, вы получите нужное поведение.
Ответ 4
Ваш Test
компилируется в:
public class Test {
public static final Test me;
public static final Integer I;
public static final String S = "abc";
static {
me = new Test();
I = Integer.valueOf(4);
}
public Test() {
System.out.println(I);
System.out.println("abc");
}
public static Test getInstance() { return me; }
public static void main(String[] args) {
Test.getInstance();
}
}
Как вы можете видеть, конструктор для Test
вызывается до того, как инициализируется I
. Вот почему он печатает "null"
для I
. Если вы должны были поменять порядок объявления для me
и I
, вы получите ожидаемый результат, потому что I
будет инициализирован до вызова конструктора. Вы также можете изменить тип для I
от Integer
до int
.
Поскольку 4
необходимо получить автобокс (т.е. завернутый в объект Integer
), он не является константой времени компиляции и является частью блока статического инициализатора. Однако, если тип был int
, число 4
было бы константой времени компиляции, поэтому его не нужно было бы явно инициализировать. Поскольку "abc"
является константой времени компиляции, значение S
выводится как ожидалось.
Если вы замените,
public static final String S = "abc";
с,
public static final String S = new String("abc");
Тогда вы заметите, что вывод S
равен "null"
. Почему это происходит? По той же причине, почему I
также выводит "null"
. Поля, подобные этим, которые имеют буквальные, постоянные значения (для не требуется автобоксинг, например String
), атрибут "ConstantValue"
при компиляции, что означает, что их значение может быть разрешено просто путем поиска в постоянный пул классов, без необходимости запускать какой-либо код.