7 июл. 2009 г.

Java: `Странное` поведение инициализации final свойств класса

Давеча рассматривая давно избитую проблему с вызовом переопределённого виртуального метода из конструктора наткнулся на странное поведение, которое меня озадачило.
И так пусть у нас есть класс A:
public class A {
private final int i = 3;
public A(){
foo();
}

public void foo(){
System.out.println("A.foo, i:" + this.i);
}
}
и его наследник - класс B:
public class B extends A {
private final int i = 5;

@Override
public void foo(){
System.out.println("B.foo, i:" + this.i);
}
}
Вопрос не в том, что как плохо вызывать виртуальный метод из конструктора не final класса, а в том какое мы увидим значение, создав новый экземпляр класса B ?
Казалось бы очерёдность должна быть такая:
  • Вызов конструктора родительского класса (т.е A())
    • в конструкторе A: вызов конструктора родительского класса (т.е Object())
    • в конструкторе A: вызов метода foo()
      • вызов метода foo() (класса B): печать "B.foo, i:" и значения свойства i класса B
  • инициализация свойства i класса B значением 5
Что в общем-то наглядно видно и в байт-коде конструктора класса B (для просмотра байткода использовал jclasslib):
0 aload_0
1 invokespecial #1 <A.<init>>
4 aload_0
5 iconst_5
6 putfield #2 <B.i>
9 return
Т.к. в момент печати значения свойства i класса B данное свойство ещё не было проинициализированно, его значение должно быть равно значению по-умолчанию, т.е 0.
Однако, на деле оказывается, что не совсем так:
B.foo, i:5
Что собственно меня и очень сильно удивило - как так оказывается, что свойство i оказывается инициализировано до момента инициализации - и зачем тогда после вызова конструктора класса A в таком случае производить инициализацию ???
Как верно заметил Матвей, стоит посмотреть на байкод самого метода foo и всё сразу же станет понятно:
0 getstatic #3 <java/lang/System.out>
3 new #4 <java/lang/StringBuilder>
6 dup
7 invokespecial #5 <java/lang/StringBuilder.<init>>
10 ldc #6 <B.foo, i:>
12 invokevirtual #7 <java/lang/StringBuilder.append>
15 aload_0
16 invokevirtual #8 <java/lang/Object.getClass>
19 pop
20 iconst_5
21 invokevirtual #9 <java/lang/StringBuilder.append>
24 invokevirtual #10 <java/lang/StringBuilder.toString>
27 invokevirtual #11 <java/io/PrintStream.println>
30 return
и сравните байткод этого метода, когда свойство i не final:
0 getstatic #3 <java/lang/System.out>
3 new #4 <java/lang/StringBuilder>
6 dup
7 invokespecial #5 <java/lang/StringBuilder.<init>>
10 ldc #6 <B.foo, i:>
12 invokevirtual #7 <java/lang/StringBuilder.append>
15 aload_0
16 getfield #2 <B.i>
19 invokevirtual #8 <java/lang/StringBuilder.append>
22 invokevirtual #9 <java/lang/StringBuilder.toString>
25 invokevirtual #10 <java/io/PrintStream.println>
28 return

Т.о. компилятор, которому известно значение final свойства на момент компиляции всюду заменяет обращение к данному свойству на известное значение - т.е производит inline, что собственно и описано в JLS, Chaprer 4 Types, Values, and Variables - final Variables.

Развивая данную тему, Матвей, в ходе нашей переписки - обращает внимание на следующую проблему: пусть есть некоторый класс, определяющий константы:
public final class Consts {
public static final int SMTH = 1000;
}
и пусть некоторый класс, который использует данную константу, пусть например
public final class Foo {
public Foo(){
System.out.println(Consts.SMTH);
}
}
Скомпилировав, при создании экземпляра класса Foo увидим, как и ожидается, 1000.
После изменим SMTH на какое-нибудь другое число, например 10. Перекомпилировав только изменившиеся классы и запустив приложение, мы по прежнему увидим 1000, но никак не 10 - причина та же самая - constant variable inline.

мораль: во избежании "чудес" лучше принудительно перекомпилировать весь проект целиком, а не только измененные классы. © Матвей

Комментариев нет: