- •Содержание
- •1. Введение
- •2. Создание и уничтожение объектов
- •3. Методы, общие для всех объектов
- •4. Классы и интерфейсы
- •5. Замена конструкций на языке c
- •6. Методы
- •7. Общие вопросы программирования
- •8. Исключения
- •9. Потоки
- •10. Сериализация
- •11. Литература
- •Глава 1
- •Глава 2 Создание и уничтожение объектов
- •Рассмотрите возможность замены конструкторов статическими методами генерации.
- •Свойство синглтона обеспечивайте закрытым конструктором
- •Отсутствие экземпляров обеспечивает закрытый конструктор
- •Не создавайте дублирующих объектов
- •Уничтожайте устаревшие ссыпки (на объекты)
- •Остерегайтесь методов flnalize
- •Глава 3 Методы, общие для всех объектов
- •Переопределяя метод equals, соблюдайте общие соглашения
- •Переопределяя метод equals, всегда переопределяйте hashCode
- •Всегда переопределяйте метод toStrlng
- •Подумайте над реализацией интерфейса ComparabIe
- •Глава 4 Классы и интерфейсы
- •Сводите к минимуму доступность классов и членов
- •Предпочитайте постоянство
- •Предпочитайте компоновку наследованию
- •Предпочитайте интерфейсы абстрактным классам.
- •Используйте интерфейсы только для определения типов
- •Предпочитайте статические классы-члены нестатическим
- •Глава 5
- •3Амена констрvкций на языке с
- •3Аменяйте структуру классом
- •3Амеияйте объедииеиие иерархией классов
- •/* Помещает данные в одно из полей объединения ... */
- •3Аменяйте конструкцию enum классом
- •Указатель на функцию заменяйте кпассом и интерфейсом
- •Глава 6 Методы
- •Проверяйте достоверность параметров
- •При необходимости создавайте резервные копии
- •Тщательно проектируйте сигнатуру метода
- •Перегружая методы, соблюдайте осторожность
- •Возвращайте массив нулевой длины, а не null
- •Для всех открытых элементов арi пишите dос - комментарии
- •Глава 7 Общие вопросы программирования
- •Сводите к минимуму область видимости локальных переменных
- •Изучите библиотеки и пользуйтесь ими
- •Не используйте строку там, где более уместен иной тип
- •При конкатенации строк опасайтесь потери производительности
- •Для ссыпки на объект используйте его интерфейс
- •Соблюдайте осторожность при использовании машинно-зависимых методов
- •Соблюдайте осторожность при оптимизации
- •Глава 8 Исключения
- •Используйте исключения лишь в исключительных ситуациях
- •Применяйте обрабатываемые исключения для восстановления, для программных ошибок используйте исключения времени выполнения
- •Избегайте ненужных обрабатываемых исключений
- •Предпочитайте стандартные исключения
- •Инициируйте исключения, соответствующие абстракции
- •Для каждого метода документируйте все инициируемые исключения
- •В описание исключения добавляйте информацию о сбое
- •Добивайтесь атомарности методов по отношению к сбоям
- •Не игнорируйте исключений
- •Глава 9 Потоки
- •Синхронизируйте доступ потоков к совместно используемым изменяемым данным
- •Избегайте избыточной синхронизации
- •Никогда не вызывайте метод wait вне цикла
- •Не попадайте в зависимость от планировщика потоков
- •При работе с потоками документируйте уровень безопасности
- •Избегайте группировки потоков
- •Глава 10 Сериализация
- •Соблюдайте осторожность при реализации интерфейса SerializabIe
- •Рассмотрите возможность использования специализированной сериализованной формы
- •Метод readObject должен создаваться с защитой
- •При необходимости создавайте метод readResolue
Остерегайтесь методов flnalize
Методы finalize непредсказуемы, часто опасны и, как правило, не нужны. Их использование может привести к странному поведению программы, низкой производительности и проблемам с переносимостью. Метод finalize имеет лишь несколько областей применения (см. ниже), а главное правило таково: следует избегать методов finalize.
Программистов, пишущих на С++, следует предостеречь в том, что нельзя рассматривать методы finalize как аналог деструкторов в С++. В С++ деструктор _ это обычный способ утилизации ресурсов, связанных с объектом, обязательное дополнение к конструктору. В языке программирования java, когда объект становится недоступен, очистку связанной с ним памяти осуществляет сборщик мусора. Со стороны же программиста никаких специальных действий не требуется. В С++ деструкторы используются для освобождения не только памяти, но и других ресурсов системы. В языке программирования java для этого обычно применяется блок try-finally.
Нет гарантии, что метод finalize будут вызван немедленно [JLS, 12.6]. С момента, когда объект становится недосТупен, и до момента выполнения метода finalize может пройти сколь угодно длительное время. Это означает, что с помощью метода finalize нельзя выполнять никаких операций, критичных по времени. Например, будет серьезной ошибкой ставить процедуру закрытия открытых файлов в зависимость от метода finalize, поскольку дескрипторы открытых файлов - ресурс ограниченный. Если из-за того, что jVM медлит с запуском методов finalize, открытыми будут оставаться много файлов, программа может завершиться с ошибкой, поскольку ей не удастся открыть новые файлы.
Частота запуска методов finalize в первую очередь определяется алгоритмом сборки мусора, который существенно меняется от одной реализации jVM к другой. Точно так же может меняться и поведение программы, работа которой зависит от частоты вызова методов finalize. Вполне возможно, что программа будет превосходно работать с jVM, на которой проводится ее тестирование, а затем позорно даст сбой на JVM, которую предпочитает ваш самый важный заказчик.
Запоздалый вызов методов finalize - не только теоретическая проблема. Создав ДЛЯ какого-либо класса метод finalize, в ряде случаев можно спровоцировать произвольную задержку при удалении его экземпляров. Один мой коллега недавно отлаживал приложение СИI, которое было рассчитано на длительное функционирование, но таинственно умирало с ошибкой OutOfMemoryError. Анализ показал, что в момент смерти приложение в очереди на удаление стояли тысячи графических объектов, ждавших лишь вызова метода finalize и утилизации. К несчастью, поток утилизации выполнялся с меньшим приоритетом, чем другой поток того же приложения, а потому удаление объектов не могло осуществиться в том же темпе, в каком они становились доступны для удаления. Спецификация языка Java не определяет, в каком из потоков
19
будут выполняться методы finalize. Поэтому нет иного универсального способа предотвратить проблемы такого рода, кроме как воздерживаться от использования методов finalize.
Спецификация языка Java не только не дает поручительства, что методы finalize будут вызваны быстро, она не гарантирует, что они вообще будут вызваны. Вполне возможно, что программа завершится, так и не вызвав метод finalize для некоторых объектов, ставших недоступными. Следовательно, обновление критического фиксируемого (persistent) состояния не должно зависеть от метода finalize. Например, ставить освобождение фиксируемой блокировки разделяемого ресурса, такого как база данных, в зависимость от метода finalize - верный способ привести всю вашу распределенную систему к сокрушительному краху.
Не соблазняйтесь методами System.gc и System.runFinalization. Они могут повысить вероятность запуска утилизации, но не гарантируют этого. Единственные методы, требующие гарантированного удаления,- это System.runFinalizersOnExit и его вредный близнец Runtime.runFinalizersOnExit. Эти методы некорректны и признаны устаревшими.
Стоит обратить внимание еще на один момент: если в ходе утилизации возникает необработанная исключительная ситуация (exception), она игнорируется, а утилизация этого объекта прекращается [JLS, 12.6]. Необработанная исключительная ситуация может оставить объект в испорченном состоянии. И если другой поток попытается воспользоваться испорченным объектом, результат в определенной мере может быть непредсказуем. Обычно необработанная исключительная ситуация завершает поток и выдает распечатку стека, однако в методе finalize этого не происходит; он даже не выводит предупреждений.
Так чем же заменить метод finalize для класса, чьи объекты инкапсулируют ресурсы, требующие завершения, такие как файлы или потоки? Создайте метод для прямого завершения и потребуйте, чтобы клиенты класса вызывали этот метод для каждого экземпляра, ,когда он им больше не нужен. Стоит упомянуть об одной детали: экземпляр сам должен следить за тем, был ли он завершен. Метод прямо го завершения должен делать запись в некое м закрытом поле о том, что объект более не является действительным. Остальные методы класса должны проверять это поле и инициировать исключительную ситуацию IllegalStateException, если их вызывают после того, как данный объект был завершен.
Типичный пример метода прямого завершения - метод close в InputStгеаm и OutputStгеаm. Еще один пример - метод саnсеl из jауа.util.Timer, который нужным образом меняет состояние объекта, заставляя поток (thread), связанный с экземпляром Timer, аккуратно завершить свою работу. Среди примеров из пакета jауа.awt - Graphics.dispose и Window.dispose. На эти методы редко обращают внимание, что сказывается на производительности программы. То же самое касается метода Image. flush, который освобождает все ресурсы, связанные с экземпляром Image, но оставляет последний в таком состоянии, что его еще можно использовать, выделив вновь необходимые ресурсы.
20
Методы прямого завершения часто используются в сочетании с конструкцией try-finally для обеспечения гарантированного завершения. Вызов метода прямого завершения из оператора finally гарантирует, что он будет выполнен, если даже при работе с объектом возникнет исключительная ситуация:
// Блок try-finally гарантирует вызов метода завершения
Foo foo = new Foo( ... );
tгу {
// Делаем то, что необходимо сделать с foo
}finally {
foo.terminate(); // Метод прямого Завершения
}
Зачем же тогда вообще нужны методы finalize? У них есть два приемлемых применения. Первое - они выступают в роли "страховочной сетки" в том случае, если владелец объекта забывает вызвать метод прямо го завершения. Нет гарантии, что метод finalize будет вызван своевременно, однако в тех случаях (будем надеяться, редких), когда клиент не выполняет свою часть соглашения, т. е. не вызывает метод прямого завершения, критический ресурс лучше 'освободить поздно, чем никогда. Три класса, представленных как пример использования метода прямого завершения (InputStream, OutputStream и Тiтeг), тоже имеют методы finalize, которые применяется в качестве страховочной сетки, если соответствующие методы завершения не были вызваны.
Другое приемлемое применение методов finalize связано с объектами, имеющими "местных партнеров" (native peers). Местный партнер - это местный объект (native object), к которому обычный объект обращается через машинно-зависимые методы. Поскольку местный партнер не является обычным объектом, сборщик мусора о нем не знает, и когда утилизируется обычный партнер, утилизировать местного партнера он не может. Метод finalize является приемлемым посредником для решения этой задачи при условии, что местный партнер не содержит критических ресурсов. Если же местный партнер содержит ресурсы, которые необходимо освободить немедленно, данный класс должен иметь метод прямого завершения. Этот метод завершения обязан делать все, что необходимо для освобождения соответствующего критического ресурса. Метод завершения может быть машинно-зависимым методом либо вызывать таковой.
Важно отметить, что здесь нет автоматического связывания методов finalize ("finalizer chaining"). Если в классе (за исключениемОЬjесt) есть метод finalize, но в подклассе он был переопределен, то метод finalize в подклассе должен вызывать. Метод finalize, из суперкласса. Вы должны завершить подкласс в блоке try, а затем в Соответствующем блоке finally вызвать метод finalize суперкласса. Тем самым
21
гарантируется, что метод finalize суперкласса будет вызван, даже если при завершении подкласса инициируется исключительная ситуация, и наоборот:
// Ручное связывание метода finalize
protected void finalize() throws Throwable try {
//'Ликвидируем состояние подкласса
finally {
super.f1nal1ze();
}
}
Если разработчик подкласса переопределяет метод finalize суперкласса, но забывает вызвать его "вручную" (или не делает этого из вредности), метод finalize суперкласса так и не будет вызван. Защититься от такого беспечного или вредного подкласса можно ценой создания некоего дополнительного объекта для каждого объекта, подлежащего утилизации. Вместо того чтобы размещать метод finalize в классе, требующем утилизации, поместите его в анонимный класс (статья 18), единственным назначением которого будет утилизация соответствующего экземпляра. для каждого экземпляра контролируемого класса создается единственный экземпляр анонимного класса, называемый хранителем утилизации (fjnalizer guardian). Контролируемый экземпляр содержит в закрытом экземпляре поля единственную в системе ссылку на хранителя утилизации. Таким образом, хранитель утилизации становится доступен для удаления в момент утилизации контролируемого им экземпляра. Когда хранитель утилизируется, он выполняет процедуры, необходимые для ликвидации контролируемого им экземпляра, как если бы его метод finalize был методом контролируемого класса:
// Идиома хранителя утилизации (Finalizer Guardian)
public class Foo {
// Единственная задача этого объекта - утилизировать
// внешний объект Foo
private final Object finalizerGuardian = new Object() protected void finalize() throws Throwable
// Утилизирует внешний объект Foo
} ;
// Остальное опущено
}
Заметим, что у открытого класса Foo нет метода finalize (за исключением·тривиального, унаследованного от класса Object), а потому не важно, был ли в методе f1nalize подкласса вызов метода super.finalize или нет. Возможность использования этой
22
методики следует рассмотреть для каждого открытого расширяемого класса, имеющего метод finalize.
Подведем итоги. Не применяйте методы finalize, кроме как в качестве страховочной сетки или для освобождения некритических местных ресурсов. В тех редких случаях, когда вы должны использовать метод finalize, не забывайте делать вызов Super.finalize. И последнее: если вам необходимо связать метод finalize с открытым классом без модификатора finaI, подумайте о применении хранителя утилизации, чтобы быть уверенным в том, что утилизация будет выполнена, даже если в подклассе в методе finalize не будет вызова super.finalize.
23