Effective Java tmprogramming Language GuideJ o s h u a b lo c h
Скачать 1.05 Mb.
|
finalize нельзя выполнять никаких операций, критичных повремени. Например, будет серьезной ошибкой ставить процедуру закрытия открытых файлов в зависимость от метода finalize, поскольку дескрипторы открытых файлов - ресурс ограниченный. Если из-за того, что jVM медлит с запуском методов finalize, открытыми будут оставаться много файлов, программа может завершиться с ошибкой, поскольку ей не удастся открыть новые файлы. Частота запуска методов finalize в первую очередь определяется алгоритмом сборки мусора, который существенно меняется от одной реализации jVM к другой. Точно также может меняться и поведение программы, работа которой зависит от частоты вызова методов finalize. Вполне возможно, что программа будет превосходно работать сна которой проводится ее тестирование, а затем позорно даст сбой на JVM, которую предпочитает ваш самый важный заказчик. Запоздалый вызов методов finalize - не только теоретическая проблема. Создав ДЛЯ какого-либо класса метод finalize, в ряде случаев можно спровоцировать произвольную задержку при удалении его экземпляров. Один мой коллега недавно отлаживал приложение СИ, которое было рассчитано на длительное функционирование, но таинственно умирало с ошибкой OutOfMem oryError. Анализ показал, что в момент смерти приложение в очереди на удаление стояли тысячи графических объектов, ждавших лишь вызова метода finalize и утилизации. К несчастью, поток утилизации выполнялся с меньшим приоритетом, чем другой поток того же приложения, а потому удаление объектов не могло осуществиться в том же темпе, в каком они становились доступны для удаления. Спецификация языка Java не определяет, в каком из потоков [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, но оставляет последний в таком состоянии, что его еще можно использовать, выделив вновь необходимые ресурсы. // Блок 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 суперкласса. Тем самым // Ручное связывание метода finalize protected void finalize() throw s 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() throw s Throwable // Утилизирует внешний объект Foo } ; // Остальное опущено Заметим, что у открытого класса Foo нет метода finalize (за исключением·тривиального, унаследованного от класса Object), а потому неважно, был ли в методе f1nalize подкласса вызов метода super.finalize или нет. Возможность использования этой Object может иметь экземпляры, прежде всего он предназначен для расширения. Поскольку все его методы без модификатора fina! - equals, hashCode, toString, clone и finalize - служат для переопределения, для них есть общие со lлашения (genera! contracts). Любой класс, в котором эти методы переопределяются, обязан подчиняться соответствующим соглашениям. В противном случае он будет препятствовать правильному функционированию других взаимодействующих с ним классов, работа которых зависит от выполнения указанных соглашений. В этой главе рассказывается о том, как и когда следует переопределять методы класса Object, не имеющие модификатора fina!. Метод finalize в этой главе не рассматривается, речь о нем шла в статье 6. В этой главе обсуждается также метод Соmparable.compareТо, который не принадлежит классу Object, однако имеет схожие свойства. Переопределяя метод, соблюдайте общие соглашения метода equals кажется простой операцией, однако есть множество способов неправильного ее выполнения, и последствия этого могут быть ужасны. Простейший способ избежать проблем вообще не переопределять метод equals. В этом случае каждый экземпляр класса будет равен только самому себе. Это решение будет правильным, если выполняется какое-либо из следующих условий Каждый экземпляр класса внутренне уникален. Это утверждение справедливо для таких классов как Th read, которые представляют не величины, а активные сущности. Реализации метода equals, предлагаемая классом Object, для этих классов работает совершенно правильно Вас не интересует, предусмотрена ли в классе проверка "логического равенства. Например, в классе java.util.Random можно было бы переопределить метод equals стем, чтобы проверять, будут ли два экземпляра Random генерировать одну и туже последовательность случайных чисел, однако разработчики посчитали, что клиенты не должны знать о такой возможности иона им не понадобится. В таком случае тот вариант метода equals, который наследуется от класса Object, вполне приемлем. Метод equals уже переопределен в суперклассе, и функционал, унаследованный от суперкласса, вполне приемлем дли данного класса. Например, большинство реализаций интерфейса Set наследует реализацию метода equals от Класса AbstractSet, List наследует реализацию от AbsctractList, а Мар - от Класс является закрытым или доступен только в пределах пакета, ивы уверены, что его метод eQuals никогда не будет вызван. Сомнительно, что в такой ситуации метод eQuals следует переопределять, разве что на тот случай, если его однажды случайно вызовут Public Boolean equals (Object о) { Throw new UnsupportedOperationException (); Так когда же имеет смысл переопределять Object. equals? Тогда, когда для класса определено понятие логической эквивалентности (Jogica! equality), которая не совпадает с тождественностью объектов, а метод equals в суперклассе не был переопределен стем, чтобы реализовать требуемый функционал. Обычно это случается с классами значении, такими как Integer или Date. Программист, сравнивающий ссылки на объекты значений с помощью метода equals, желает, скорее всего, выяснить, являются ли они логически эквивалентными, а непросто узнать, указывают ли эти ссылки на один и тот же объект. Переопределение метода equals необходимо не только для того, чтобы удовлетворить ожидания программистов, оно позволяет использовать экземпляры класса в качестве ключей в некоей схеме или элементов в некоем наборе, Имеющих необходимое и предсказуемое поведение. Существует один вид классов значений, которым ненужно переопределение метода equals,- перечисление типов (статья 21). Поскольку для классов этого типа гарантируется, что каждому значению соответствует не больше одного объекта, метод equals из Object для этих классов будет равнозначен методу логического сравнения Переопределяя метод equals, вы должны твердо придерживаться принятых для него общих соглашений. Воспроизведем эти соглашения по тексту спецификации java,lang.Object: Метод equals реализует отношение эквивалентности Рефлективность, для любой ссылки назначение х выражение х) должно возвращать true. Симметричность, для любых ссылок назначениях и у выражение х. equals(y) должно возвращать t г тогда и только тогда, когда y.equals(x) возвращает true. Транзитивность, для любых ссылок назначениях, у и z, если x.equals(y) возвращает true и y.equals(z) возвращает true, то и выражение х. equals(z) должно возвращать true. Непротиворечивость. Для любых ссылок назначениях и у, если несколько раз вызвать х. equals(y), постоянно будет возвращаться значение true либо постоянно будет возвращаться значение false при условии, что никакая информация, используемая при сравнении объектов, не поменялась. Для любой ненулевой ссылки назначение х выражение х. equals(null) должно возвращать false. Если у вас нет склонности к математике, все это может показаться ужасным, однако игнорировать это нельзя Если вы нарушите условия, то рискуете получить программу, которая работает неустойчиво или заканчивается с ошибкой, а установить источник ошибок крайне сложно. Перефразируя Джона Донна (John Dоппе), можно сказать ни один класс - не остров. (Нет человека, что был бы сам по себе, как остров ... " - Джон Донн, "Взывая на краю- Прим. пер) Экземпляры одного класса часто передаются другому классу. Работа многих классов, в том числе всех классов коллекции, зависит оттого, соблюдают ли передаваемые им объекты соглашения для метода equals. Теперь рассмотрим внимательнее соглашения для метода equals. На самом деле они не так уж сложны. Как только вы их поймете, придерживаться их будет совсем не Трудно. Рефлективность. Первое требование говорит о том, что объект должен быть равен самому себе. Трудно представить себе непреднамеренное нарушение этого требования. Если вы нарушили его, а затем добавили экземпляр в ваш класс коллекции, то метод contain этой коллекции почти наверняка сообщит вам, что в коллекции нет экземпляра, которой вы только что добавили. Симметрия. Второе требование гласит, что любые два объекта должны сходиться во мнении, равны ли они между собой. В отличие от предыдущего, представить непреднамеренное нарушение этого требования несложно. Например, рассмотрим следующий класс /** * Строка без учета регистра. Регистр исходной строки сохраняется * методом toString, однако, при сравнениях игнорируется. */ public final class CaseInsensitiveString { private String s; public CaseInsensitiveString(String s) { if (s == null) throw new NullPointerException(); this.s = s; } // Ошибка нарушение симметрии boolean equals(Object o) { if (o instanceof CaseInsensitiveString) return s.equalsIgnoreCase( ((CaseInsensitiveString)o).s); if (o instanceof String) // One-way interoperability! return s.equalsIgnoreCase((String)o); return false; } /* Одностороннее взаимодействие // Fixed public boolean equals(Object o) { return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s); } */ // ... // Остальное опущено static void main(String[] args) { CaseInsensitiveString cis = new CaseInsensitiveString("Polish"); String s = "polish"; System.out.println(cis.equals(s)); System.out.println(s.equals(cis)); Исходя из лучших побуждений, метод equals в этом классе наивно пытается взаимодействовать с обычными строками. Предположим, что у насесть одна строка, независимая от регистра, и вторая - обычная. Caselnsensiti veString cis = new Caselnsensiti veString ("Polish"); String s = "polish"; Как и предполагалось, выражение cis,equals(s) возвращает г. Проблема заключается в том, что хотя метод equals в классе г знает о существовании обычных строк, метод equals в классе String не догадывается о строках, нечувствительных к регистру. Поэтому выражение s.equals(cis) возвращает false, явно нарушая симметрию. Предположим, вы помещаете в коллекцию строку, нечувствительную к регистру List list =new ArrayList(); list.add(cis); Какое значение возвратит выражение list.contains(s)? Кто знает. В текущей версии JOK от компании Sun выяснилось, что оно возвращает false, но это всего лишь особенность реализации. В другой реализации может быть возвращено true или вовремя выполнения будет инициирована исключительная ситуация. Нарушив соглашение для equals, вы не можете знать, как поведут себя другие объекты, столкнувшись с вашим объектом. Для устранения этой проблемы удалите из метода equals попытку взаимодействия с классом String. Сделав это, вы сможете перестроить метод так, чтобы он содержал один оператор возврата Public Boolean equals (Object о) { Return о instanceof CaseInsensitiveString& & ((CaseInsensitiveString) o), s.equalsIgnoreCase(s); Транзитивность. Третье требование в соглашениях для метода equals гласит если один объект равен второму, а второй объект равен третьему, то и первый объект должен быть равен третьему объекту. И вновь несложно представить непреднамеренное нарушение этого требования. Допустим, что программист создает подкласс, придающий своему суперклассу новый аспект . иными словами, подкласс привносит некую информацию, окаэываl0УЮ влияние на процедуру сравнения. Начнем с простого неизменяемого класса, соответствующего точке в двухмерном пространстве public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public boolean equals(Object o) { if (!(o instanceof Point)) return false; Point p = (Point)o; return p.x == x && p.y == y; } // ... // Остальное опущено Предположим, что вы хотите расширить этот класс, добавив понятие цветах у, Color color) { super(x, у this.color = color; // Остальное опущено Как должен выглядеть метод equals? Если вы оставите его как есть, реализация метода будет наследоваться от класса Point, и при сравнении с помощью методов equals информация о цвете будет игнорироваться. Хотя такое решение и не нарушает общих соглашений для метода equals, очевидно, что оно неприемлемо. Допустим, выпишите метод equals, который возвращает значение t rue, только если его аргументом является цветная точка, имеющая тоже положение и тот же цвет |