Работа со ссылочными переменными. Сборка мусора
Объекты, как мы уже знаем, являются экземплярами ссылочных типов. Работа со ссылочными переменными имеет специфику, принципиально отличающую её от работы с переменными примитивного типа. В каждой переменной примитивного типа содержится своё значение, и изменение этого значения не влияет на те значения, которые можно получить с помощью других переменных. Причём имя переменной примитивного типа можно рассматривать как имя ячейки с данными, и у такой ячейки может быть только одно имя, которое не может меняться по ходу работы программы. Для ссылочных переменных это не так.
Переменные ссылочного типа содержат адреса данных, а не сами данные. Поэтому присваивания для таких переменных меняют адреса, но не данные. Кроме того, из-за этого под них выделяется одинаковое количество памяти независимо от типа объектов, на которые они ссылаются. А имена ссылочных переменных можно рассматривать как псевдонимы имён ячеек с данными – у одной и той же ячейки с данными может быть сколько угодно псевдонимов, так как адрес одной и той же ячейки можно копировать в произвольное число переменных соответствующего типа. И все они будут ссылаться на одну и ту же ячейку с данными.
В Java ссылочные переменные используются для работы с объектами: в этом языке программирования используется динамическая объектная модель, и все объекты создаются динамически, с явным указанием в программе момента их создания. Это отличает Java от C++, языка со статической объектной моделью. В C++ могут существовать как статически заданные объекты, так и динамически создаваемые. Но это не преимущество, как могло бы показаться, а проблема, так как в ряде случаев создаёт принципиально неразрешимые ситуации.
Следует отметить, что сами объекты безымянны, и доступ к ним осуществляется только через ссылочные переменные. Часто говорят про ссылочную переменную как про сам объект, поскольку долго и неудобно произносить “объект, на который ссылается данная переменная”. Но этого по мере возможности следует избегать.
Ссылочной переменной любого типа может быть присвоено значение null, означающее, что она никуда не ссылается. Попытка доступа к объекту через такую переменную вызовет ошибку. В Java все ссылочные переменные первоначально инициируются значением null, если им не назначена ссылка на объект прямо в месте объявления. Очень часто встречающаяся ошибка – попытка доступа к полю или методу с помощью ссылочной переменной, которой не сопоставлен объект. Такая ошибка не может быть обнаружена на этапе компиляции и является ошибкой времени выполнения (Run-time error). При этом приложение Java генерирует исключительную ситуацию (ошибку) попытки доступа к объекту через ссылку null с сообщением следующего вида:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException.
Последовательность действий со ссылочными переменными и объектами удобно описывать с помощью рисунков, на которых каждой такой переменной и каждому объекту сопоставлен прямоугольник – символическое изображение ячейки. Около ячейки пишется её имя, если оно есть, а внутри – значение, содержащееся в ячейке.
Объект
Ссылочная переменная
Если переменная является ссылочной, из неё выходит стрелочка-ссылка. Она кончается на том объекте, на который указывает ссылка. Если в ссылочной переменной содержится значение null (ссылка “в никуда”, адрес==0), рисуется “висящая” короткая стрелка, у которой находится надпись “null”. Отметим, что в Java символом равенства является “==”, а не символ “=”, который используется для оператора присваивания.
Если ссылка перещёлкивается, то либо создаётся новый рисунок (в книжке), либо перечёркивается крестиком прежняя стрелочка и рисуется новая (на листе бумаги или на доске). При новом перещёлкивании эта “старая” стрелка перечёркивается двумя крестиками, а “более свежая”, которая была перещёлкнута – одним крестиком, и так далее. Такая система обозначений позволяет наглядно представить, что происходит при работе со ссылками, и не запутаться в том, куда они указывают и с какими ссылками в какой момент связаны динамически создаваемые объекты.
При выполнении оператора new , после которого указан вызов конструктора, динамически выделяется новая безымянная ячейка памяти, имеющая тип, соответствующий типу конструктора, а сам конструктор после окончания работы возвращает адрес этой ячейки. Если у нас в левой части присваивания стоит ссылочная переменная, то в результате ссылка “перещёлкивается” на динамически созданную ячейку.
Рассмотрим этот процесс подробнее. Будем считать, что сначала в ячейке circle1 типа Circle хранится нулевой адрес (значение ссылки равно null). Будем изображать это как стрелку в никуда с надписью null.
Переменная
circle1 типа Circle
null
После оператора
circle1=new Circle(x1,y1,r1) ;
в динамически выделенной безымянной ячейке памяти будет создан объект-окружность с координатами центра x1, y1 и радиусом r1 (это какие-то значения, конкретная величина которых в данном случае не имеет значения):
Объект1 типа Circle
circle1
|
Поля объекта доступны через ссылку как по чтению, так и по записи. До тех пор, пока ссылочная переменная circle1 содержит адрес Объекта1, имя circle1 является псевдонимом, заменяющим имя этого объекта. Его можно использовать так же, как имя обычной переменной в любых выражениях и операторах, не связанных с изменением адреса в переменной circle1.
Поскольку между ссылочными переменными одного типа разрешены присваивания, переменная по ходу программы может сменить объект, на который она ссылается. Если circle2 также имеет тип Circle, то допустимы присваивания вида
circle2= circle1;
Такие присваивания изменяют адреса в ячейках ссылочных переменных, но не меняют содержания объектов.
Рассмотрим следующий участок кода:
Circle circle1=new Circle(x1,y1,r1) ;
Circle circle2=new Circle(x2,y2,r2) ;
Circle circle3;
Ему соответствует следующий рисунок:
Объект1 типа Circle
circle1
|
Объект2 типа Circle
circle2
|
circle3
|
null
Проведём присваивание
circle3=circle2;
В результате получится такая картинка:
Объект1 типа Circle
circle1
|
Объект2 типа Circle
circle2
|
circle3
|
Обе переменные, как circle2, так и circle3, теперь ссылаются на один и тот же объект – в них находится один и тот же Адрес2. То есть оба имени – синоним имени Объекта2. Напомним, что сам объект, как все динамически создаваемые величины, безымянный. Таким образом, circle2.x даст значение x2, точно так же, как и circle3.x. Более того, если мы изменим значение circle2.x, это приведёт к изменению circle3.x – ведь это одно и то же поле x нашего Объекта2.
Рассмотрим теперь, что произойдёт при присваивании
circle1=circle2;
Этот случай отличается от предыдущего только тем, что переменная circle1 до присваивания уже была связана с объектом.
Объект1 типа Circle
|
circle1
|
Объект2 типа Circle
circle2
|
circle3
|
В результате у Объекта2 окажется сразу три ссылочные переменные, которые с ним связаны, и имена которых являются его псевдонимами в данном месте программы: circle1, circle2 и circle3. При этом программная связь с Объектом1 окажется утеряна – он занимает место в памяти компьютера, но программный доступ к нему невозможен, поскольку адрес этого объекта программой утерян. Таким образом, он является бесполезным и напрасно занимает ресурсы компьютера.
Про такие ячейки памяти говорят, что они являются мусором. В Java предусмотрен механизм высвобождения памяти, занятой такими бесполезными объектами. Он называется сборкой мусора (garbage collection) и работает автоматически. Этим в фоновом режиме занимается специальная часть виртуальной Java-машины, сборщик мусора. При программировании на Java, отличие от таких языков как C/C++ или Object PASCAL, программисту нет необходимости самому заботиться о высвобождении памяти, занятой под динамически создаваемые объекты.
Следует подчеркнуть, что намеренная потеря связи ссылочной переменной с ненужным уже объектом – это одно, а непреднамеренная – совсем другое. Если вы не планировали потерю связи с объектом, а она произошла, это логическая ошибка. И хотя она не приведёт к зависанию программы или её неожиданному закрытию, такая программа будет работать не так, как вы предполагали, то есть неправильно или не совсем правильно. Что иногда ещё опасней, так как ошибку можно не заметить или, если заметили, очень трудно понять её причину.