Удаление неиспользуемых объектов и метод finalize. Проблема деструкторов для сложно устроенных объектов
Как мы знаем, конструктор занимается созданием и рядом дополнительных действий, связанных с инициализацией объекта. Уничтожение объекта также может требовать дополнительных действий. В таких языках программирования как C++ или Object PASCAL для этих целей используют деструкторы – методы, которые уничтожают объект, и совершают все сопутствующие этому сопроводительные действия.
Например, у нас имеется список фигур, отрисовываемых на экране, и мы хотим удалить из этого списка какую-нибудь фигуру. Перед уничтожением фигура должна исключить себя из списка, затем дать команду списку заново отрисовать содержащиеся в нём фигуры, и только после этого “умереть”. Именно такого рода действия характерны для деструкторов. Заметим, что возможна другая логика работы: дать списку команду исключить из него фигуру, после чего перерисовать фигуры, содержащиеся в списке. Но желательно, чтобы язык программирования поддерживал возможность реализации обоих подходов.
В Java имеется метод finalize(). Если в классе , который производит завершающие действия перед уничтожением объекта сборщиком мусора, переопределить этот метод, он, как может показаться, может служить некой заменой деструктора. Но так как момент уничтожения объекта неопределёнен и может быть отнесён по времени очень далеко от момента потери ссылки на объект, метод finalize не может служить реальной заменой деструктору. Даже явный вызов сборщика мусора System.gk() сразу после вызова метода finalize() не слишком удачное решение, так как и в этом случае нет гарантии правильности порядка высвобождения ресурсов. Кроме того, сборщик мусора потребляет много ресурсов и в ряде случаев может приостановить работу программы на заметное время.
Гораздо более простым и правильным решением будет написать в базовом классе разрабатываемой вами иерархии метод destroy() - “уничтожить, разрушить”, который будет заниматься выполнением всех необходимых вспомогательных действий (можно назвать метод dispose() – “избавиться, отделаться”, можно free() – “освободить”). Причём при необходимости надо будет переопределять этот метод в классах-наследниках. В случае, когда надо вызывать прародительский деструктор, следует делать вызов super.destroy(). При этом желательно, чтобы он был последним оператором в деструкторе класса – в противном случае может оказаться неправильной логика работы деструктора. Например, произойдёт попытка обращения к объекту, исключённому из списка, или попытка записи в уже закрытый файл.
Логика разрушения объектов является обратной той, что используется при их создании: сначала разрушается часть, относящаяся к самому классу. Затем разрушается часть, относящаяся к непосредственному прародителю, и далее по иерархии, заканчивая частью, относящейся к базовому классу. Поэтому последним оператором деструктора бывает вызов прародительского деструктора super.destroy().
Перегрузка методов
Напомним, что имя функции в сочетании с числом параметров и их типами называется сигнатурой функции. Тип возвращаемого значения и имена параметров в сигнатуру не входят. Понятие сигнатуры важно при задании подпрограмм с одинаковыми именами, но разными списками параметров – перегрузке (overloading) подпрограмм. Методы, имеющие одинаковое имя, но разные сигнатуры, разрешается перегружать. Если же сигнатуры совпадают, перегрузка запрещена. Для задания перегруженных методов в Java не требуется никаких дополнительных действий по сравнению с заданием обычных методов. Если же перегрузка запрещена, компилятор выдаст сообщение об ошибке.
Чаще всего перегружают конструкторы при желании иметь разные их варианты, так как имя конструктора определяется именем класса. Например, рассмотренные ранее конструкторы
Circle(Graphics g, Color bgColor){
…
}
и
Circle(Graphics g, Color bgColor, int r){
…
}
отличаются числом параметров, поэтому перегрузка разрешена.
Вызов перегруженных методов синтаксически не отличается от вызова обычных методов, но всё-таки в ряде случаев возникает некоторая специфика из-за неочевидности того, какой вариант метода будет вызван. При разном числе параметров такой проблемы, очевидно, нет. Если же два варианта методов имеют одинаковое число параметров, и отличие только в типе одного или более параметров, возможны логические ошибки.
Напишем класс Math1, в котором имеется подпрограмма-функция product , вычисляющая произведение двух чисел, у которой имеются варианты с разными целыми типами параметров. Пример полезен как для иллюстрации проблем, связанных с вызовом перегруженных методов, так и для исследования проблем арифметического переполнения.
public class Math1 {
public static byte product(byte x, byte y){
return x*y;
}
public static short product(short x, short y){
return x*y;
}
public static int product(int x, int y){
return x*y;
}
public static char product(char x, char y){
return x*y;
}
public static long product(long x, long y){
return x*y;
}
}
Такое задание методов разрешено, так как сигнатуры перегружаемых вариантов различны. Обратим внимание на типы возвращаемых значений – они могут задаваться по желанию программиста. Подпрограммы заданы как методы класса (static) для того, чтобы при их использовании не пришлось создавать объект.
Если бы мы попытались задать такие варианты методов:
public static byte product(byte x, byte y){
return x*y;
}
public static int product(byte a, byte b){
return a*b;
}
то компилятор выдал бы сообщение об ошибке, так как у данных вариантов одинаковая сигнатура. - Ни тип возвращаемого значения, ни имена параметров на сигнатуру не влияют.
Если при вызове метода product параметры имеют типы, совпадающие с заданными в одном из перегруженных вариантов, всё просто. Но что произойдёт в случае, когда в качестве параметра будут переданы значения типов byte и int? Какой вариант будет вызван? Проверка идёт при компиляции программы, при этом перебираются все допустимые варианты. В нашем случае это product(int x, int y) и product(long x, long y). Остальные варианты не подходят из-за типа второго параметра – тип подставляемого значенния должен иметь диапазон значений, “вписывающийся” в диапазон вызываемого метода. Из допустимых вариантов выбирается тот, который ближе по типу параметров, то есть в нашем случае product(int x, int y).
Если среди перегруженных методов среди разрешённых вариантов не удаётся найти предпочтительный, при компиляции класса, где делается вызов, выдаётся диагностика ошибки. Так бы случилось, если бы мы имели следующую реализацию класса Math2
public class Math2 {
public static int product(int x, byte y){
return x*y;
}
public static int product(byte x, int y){
return x*y;
}
}
и в каком-нибудь другом классе имели переменные byte b1, b2 и сделали вызов Math1.product(b1,b2). Оба варианта перегруженного метода подходят, и выбрать более подходящий невозможно. Отметим, что класс Math2 при этом компилируется без проблем – в самом нём ошибок нет. Проблема в том классе, который его использует.
Самая неприятная особенность перегрузки – вызов не того варианта метода, на который рассчитывал программист. Особо опасные ситуации при этом возникают в случае, когда перегруженные методы отличаются типом параметров, и в качестве таких параметров выступают объектные переменные. В этом случае близость совместимых типов определяется по близости в иерархии наследования – по числу этапов наследования. Отметим, что выбор перегруженного варианта проводится статически, на этапе компиляции. Поэтому тип, используемый для этого выбора, определяется типом объектной переменной, передаваемой в качестве параметра, а не типом объекта, который этой переменной назначен.