понедельник, 19 ноября 2007 г.

Конструирование объектов в Java


Как создаются объекты в Java - тема не лишенная некоторых нюансов. Проблема в том, что детальное рассмотрение данного процесса во многих учебниках опущено. Однако, зачастую неправильное или неполное понимание алгоритма создания объекта может привести к ошибкам в программе, в том числе и уровня компиляции.

Рассмотрим процесс конструирования объекта в Java по шагам, двигаясь от простого к сложному - от создания одного класса до создания иерархии:

1 Создание одиночного объекта

     1.1 Конструктор по умолчанию
     1.2 Константы
     1.3 Инициализированные поля
     1.4 Исключения в конструкторах
     1.5 Блок статической инициализации

2 Отцы и дети

3 Общий алгоритм создания объекта


1 Создание одиночного объекта



Прежде всего отметим, что любой класс в Java имеет предка - класс Object. Но класс Оbject довольно известен, его поля и методы описаны и знакомы. Поэтому абстрагируемся от класса Object и рассмотрим создание объекта класса, не имеющего предка.

1.1 Конструктор по умолчанию

Любой класс в Java имеет конструктор. Если программист явно не создаст классу конструктор, то конструктор будет создан на этапе компиляции. Такой конструктор называется конструктором по умолчанию и имеет следующую сигнатуру:

  1. public Parent()

  2. {

  3.     ...

  4. }



Что находится внутри данного конструктора рассмотрим чуть позже.

Если же программист создал для класса хоть один конструктор (не важно с параметрами или без) - конструктор по умолчанию не создается!

Следующий код вызовет ошибку времени компиляции:

  1. public class Parent

  2. {

  3.     public Parent(int i)

  4.     {

  5.         System.out.println("Parent::Parent(int i)");

  6.     }

  7. }

  8.  

  9. public class Demo

  10. {

  11.     public static void main(String[] args)

  12.     {

  13.         Parent p = new Parent();

  14.     }

  15. }

  16.  



Дело в том, что теперь конструктор Parent() не определен. Особенно важен данный факт при наследовании.

1.2 Константы

Константа - публичное поле класса с модификатором final. Т.е. неизменяемое. Константы бывают уровня класса (static) и уровня объекта. 

Константы уровня класса определяются следующим образом:

  1. public class  Parent

  2. {  

  3.     public static final int MAX = 300;

  4.         ...

  5. }



Особенность их в том, что нигде в байт-коде класса им не присваивается значение! Константа описывается лишь в секции Fields и имеет один атрибут - ConstantValue (значение). Дело в том, что компилятор заменяет везде в коде такую константу на соответствующее ей значение. Осуществляется этакий препроцессинг. Поэтому, кстати, при изменении значения константы будут перекомпилированы все использующие ее классы.

Константы уровня объекта определяются следующим образом:

  1. public class  Parent

  2. {

  3.    public final int MAX = 400;

  4.    ...

  5. }



Если посмотреть скомпилированный байт-код (например, с помощью jclasslib bytecode viewer), то увидим следующее (метод init - конструктор)

  1. 0 aload_0

  2. 1 invokespecial #14

  3. 4 aload_0

  4. 5 sipush 400

  5. ...



Т.е. значение константе уровня объекта (без модификатора static) присваивается в начале конструктора класса. Это единственное место, где изменяется
значение переменной с модификатором final. Так же важно знать, что значение задается в каждом конструкторе, в том числе и в конструкторе по-умолчанию.

1.3 Инициализированные поля

На самом деле инициализированные поля, т.е. поля, объявляемые как:

  1. public class Parent

  2. {  

  3.    int i = 200;

  4.    ...

  5. }



мало чем отличаются от констант уровня объекта. Инициализация таких полей осуществляется так же в конструкторах класса:

  1. 0 aload_0

  2. 1 invokespecial #10

  3. 4 aload_0

  4. 5 sipush 200

  5. 8 putfield #12



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

1.4 Исключения в конструкторах

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

Первое и самое существенное отличие - в Java есть такая классная штука, как сборка мусора. Здесь не место спорам на тему нужна ли она вообще, что лучше сборка мусора или ручное управление памятью и т.д. Сборка мусора в Java есть - это аксиома. Соответственно распределенная на момент выбрасывания исключения память будет освобождена. Хотя, если очень извратиться, то получить утечку памяти, используя исключения, можно.
 
Если в конструкторе объекта будет брошено исключение - создан он не будет. Например, следующий код при выполнении выведет null:

  1. public class  Parent

  2. {  

  3.     int i = 200;

  4.  

  5.     public Parent()

  6.     {

  7.         i = 0;    

  8.         System.out.println("Parent i/0 = " + i/0);

  9.         System.out.println("Parent::Parent()");      

  10.         i = 200;

  11.     }

  12.  

  13.     public Parent(int i)

  14.     {    

  15.         System.out.println("Parent::Parent(int i)");

  16.     }

  17.  

  18.     public int getI()

  19.     {

  20.         return i;

  21.     }

  22. }

  23.  

  24. public class Main

  25. {

  26.     public static void main(String[] args)

  27.     {

  28.         Parent p = null;

  29.         try

  30.         {

  31.             p = new Parent();

  32.         }

  33.         catch (Exception ex)

  34.         {

  35.             System.out.println("OOPS Exception");

  36.             ex.printStackTrace();

  37.         }

  38.  

  39.         System.out.println(p);

  40.     }

  41. }



Дело в том, что в конструкторе класса Parent присутствует деление на ноль. Соответственно, будет брошено исключение, которое будет обработано в классе Main, но объект класса Parent создан не будет. Данную ситуацию обязательно необходимо обработать, иначе можно получить много "приятных" сюрпризов. Самым безобидным из них будет NPE.

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

1.5 Блок статической инициализации.

Блок статической инициализации вызывается в программе один раз - при загрузке класса, т.е. до вызова конструктора класса либо до обращения к статической переменной класса. Между обращениями к различным статическим переменным блок статической инициализации вызываться не будет. Под обращением к статической переменной (имеется ввиду именно переменная, т.е. без модификатора final) имеется ввиду как ее чтение, так и запись.

Блок статической инициализации присутствует в программе не всегда, даже если в классе объявлена статическая переменная. Например, в данном классе нет блока статической инициализации:

  1. public  class Parent

  2. {  

  3.     public static int MAX;      

  4.  

  5.     public final int i = 200;  

  6.  

  7.     public Parent()

  8.     {

  9.         System.out.println("Parent::Parent()");

  10.     }

  11.  

  12.     public Parent(int i)

  13.     {    

  14.         System.out.println("Parent::Parent(int i)");

  15.     }

  16.  

  17.     public int getI()

  18.     {

  19.         return i;

  20.     }

  21. }



Но если написать так:

  1. public  static int MAX = 100;



то блок статической инициализации будет создан. Собственно, именно в нем и происходит присваивание значения статической переменной MAX.

И конечно никто не мешает определить данный блок явно:

  1. public class Parent

  2. {  

  3.     public static int MAX;  

  4.  

  5.     static

  6.     {

  7.         MAX = 200;

  8.         System.out.println("Parent::MAX = " + MAX);

  9.     }

  10.     ...

  11. }



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

2 Отцы и дети



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

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

Причем, если не указано явно - вызывается конструктор без параметров (созданный явно или по-умолчанию). Но данный конструктор может отсутствовать. Например, такой код вызовет ошибку времени компиляции:

  1. public class Parent

  2. {  

  3.     public Parent(int i)

  4.     {    

  5.         System.out.println("Parent::Parent(int i)");

  6.     }  

  7. }

  8.  

  9. public class Child extends Parent

  10. {

  11.     int i = 100;  

  12.  

  13.     public Child()

  14.     {

  15.  

  16.     }

  17.  

  18.     public int getI()

  19.     {

  20.         return i;

  21.     }

  22. }



Дело в том, что при создании класса Child должен быть вызван конструктор без параметров класса Parent, которого нет.

Исправить такие ошибки можно, вызывая конструктор базового класса явно, через ключевое слово super:

  1. public  class Child extends Parent

  2. {

  3.     int i = 100;

  4.  

  5.     public Child()

  6.     {

  7.         super(10);

  8.     }

  9.  

  10.     public int getI()

  11.     {

  12.         return i;

  13.     }

  14. }



Теперь будет вызываться конструктор с параметром, который мы и определили.

Больше про наследование применительно к построению объектов сказать нечего. Пора подводить итоги.

3 Общий алгоритм создания объекта



Итак, подводя итоги, составим общий алгоритм создания объекта: 

1. Вызывается блок статической инициализации базового класса (если он есть)

2. Вызывается блок статической инициализации создаваемого класса (если он есть)

3. Вызывается конструктор класса

3.1. Вызывается конструктор базового класса

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

3.3. Вызывается остальной код конструктора.

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

Понравилось сообщение - подпишись на блог через RSS


25 комментариев:

Shef комментирует...

Спасибо за инфу. Я об этих вопросах даже не задумывался (опыта мало), но теперь буду знать.

Samolisov Pavel комментирует...

Очень рад, что Вам пригодилась информация, представленная в моем блоге. А знать все это полезно, особенно когда работаешь с чужим кодом, в котором используется сложная иерархия классов. Ну и на собеседованиях любят спрашивать...

Анонимный комментирует...

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

Анонимный комментирует...

думаю будет что-то типа этого

SomeClass.static intializer
Super.static intializer
Child.static intializer
SuperSomeClass.static intializer
SuperSomeClass.instance initializer
SuperSomeClass.SomeClass
Super.instance initializer
Super.Super
SomeClass.instance initializer
SomeClass.SomeClass
Child.instance initializer
Child.Child

где
Super - базовый класс
Child - класс унаследованый от базового
SomeClass - явноинициализированое поле в Child
SuperSomeClass - явноинициализированое поле в Super

Samolisov Pavel комментирует...

Ну вообще в статье порядок инициализации описан, порядок смотрелся с помощью банальных System.out.println(). Что касается инициализированых и статических полей - порядок их создания проверялся с помощью браузера байт-кода. В случае инициализированых полей - их инициализация происходит в конструкторе.

З.Ы. Кстати забавный вопрос на засыпку - чем
public static final String a = "a";

отличается от

private static final String a = "a";

с точки зрения инициализации.

Анонимный комментирует...

могу ошибаться.. но разницы с точки зрения инициализации нет никакой

Samolisov Pavel комментирует...

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

Анонимный комментирует...

Про исключения в кострукторе объекта С++ ваша информация не совсем точна. Если конструктор выбрасывает исключение, объект не будет создан и память будет освобождена. Т.е. код типа
MyClass a = new MyClass();
с точки зрения утечки памяти безопасен.
Отдельный вопрос о выделяемых в процессе контруирования ресурсах, но и он тоже решается вполне культурно.

Pavel Samolisov комментирует...

Не буду спорить, С++ знаю весьма поверхностно - вполне мог ошибаться. Спасибо за замечание.

Анонимный комментирует...

Мы тут с коллегой немного поэкспериментировали и наткнулись на такой момент.

class A {

static {
N = 1; // почему тут не ошика?
System.out.println(N); // тут ошибка компиляции
}

private static final int N;

}

Alexandr name комментирует...

Возможно стоит также упомянуть о классе Class. Он ведь тоже причастен к созданию объектов.

Pavel Samolisov комментирует...

Не совсем понял в каком контексте об этом стоит упомянуть. В данном случае class уже загружен и происходит инстанцирование объекта. По-моему интсанцирование через new и reflection осуществляется одинаково.

alevlako комментирует...

class A {

static {
N = 1; // почему тут не ошика?
System.out.println(A.N); // и все чисто :-)
}

private static final int N;
}

Pavel Samolisov комментирует...

А почему там должна быть ошибка? У вас статическое финальное поле, которому вы один раз (!) присваиваете значение в блоке статической инициализации. Когда вы пишите private static final int N = 1; то у вас по сути генерируется такой же код, только без System.out.println.

cardio комментирует...

цитата из "Философия Джава" Б.Эккель:
"...в любом случае переменные инициализируются перед вызовом любого метода - даже конструктора"

Согласно же вашему описанию:

".... Вызывается конструктор класса
3.1. Вызывается конструктор базового класса
3.2. Происходит инициализация переменных в порядке их определения."

Видим не соответствие, подскажите, пожалуйста, это ошибка в книге или ...?

msangel комментирует...

Спасибо за статью!
Я пришел к ней, когда искал информацию об освобождении ресурсов, захваченных в конструкторе до выброса исключения.
Т.к. Ваша статья является концентрированным местом информации об конструкторах в яве, то хочу предложить Вам добавить в нее еще несколько деталей:
Собственно в блок "Исключения в конструкторах" можно добавить, что освобождение ресурсов, которые уже захвачены но не освобождаются сами(например файловые потоки, сетевые соединения) можно и нужно в finally методе.
Больше информации тут: http://www.daniweb.com/software-development/java/threads/26712
Так же по теме вот еще одно интересное замечание:
http://futuretask.blogspot.com/2006/05/java-tip-10-constructor-exceptions-are.html.

Pavel Samolisov комментирует...

Большое спасибо за замечания, разберусь и добавлю.

msangel комментирует...

гм... я наверное сам себе противоречу, но продолжая открывать для себя этот вопрос, я пришел к интересному решению этой проблемы:
http://www.bruceeckel.by.ru/tij/Chapter10.html#Index1108

Vest комментирует...

Спасибо за статью, очень интересная.
Скажите, а существует ли возможность проинициализировать поля класс (если они публичные) после создания объекта.

Я понимаю, что можно создать параметризированный конструктор, но не уверен, что возможно провернуть что-то вроде:

public class TestClass { public int i; }

TestClass obj = new TestClass() { this.i = 10; }

я просто повидал много других языков, и сказывается привычка полученная из других языков :(

Pavel Samolisov комментирует...

Здравствуйте. Массивы так можно инициализировать. А чем вам не нравится TestClass obj = new TestClass(10)?

Sergey Travin комментирует...

огромное спасибо, за ваш блог.
один из лучших по java. буду рад новым статьям.

Pavel Samolisov комментирует...

Большое спасибо за теплый отзыв, постараюсь не разочаровать.

Евгений Карпов комментирует...

К сути статьи этот комментарий отношения не имеет, однако... Если Java - это "ява", то Jazz - это "яззь"

RuSTer комментирует...

Спасибо за статью. Просто и понятно

Александр Васильев комментирует...

Спасибо, статья интересная.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!