Effective Java tmprogramming Language GuideJ o s h u a b lo c h
Скачать 1.05 Mb.
|
// Ошибка объекты будут иметь общее внутреннее состояние Object clone() throws CloneNotSupportedException { HashTable result = (HashTable) super.clone(); result.buckets = (Entry[]) buckets.clone(); return result; Хотя клони имеет собственный набор сегментов, последний ссылается нате же Связные списки, что и исходный набора это может привести к непредсказуемому поведению и клона, и оригинала. для устранения этой проблемы вам придется отдельна копировать связный список для каждого сегмента. Представим один из распространенных приемов public class HashTable implements Cloneable { private Entry[] buckets = … .; private static class Entry { Object key; Object value; Entry next; Entry(Object key, Object value, Entry next) { this.key = key; this.value = value; this.next = next; } // Рекурсивно копирует связный список. начинающийся // с указанной записи Entry deepCopy() { return new Entry(key, value, next = = null ? null : next.deepCopy()); public Object clone() throws CloneNotSupportedException HashTable result = (HashTable) super.clone(); result.buckets = new Entry[buckets.length]; for (int i = о i < buckets.lenght; i ++) if (buckets[i] != null) result.buckets[i] = (Entry) buckets[i]:deepCopy(); return result; } // Остальное опущено Закрытый класс HashTable. Entry был привнесен для реализации метода "глубокого копирования" (deep сору. Метод clone в классе HashTable размещает в памяти новый массив buckets нужного размера, а затем в цикле просматривает исходный набор buckets, выполняя глубокое копирование каждого непустого сегмента. Чтобы скопировать связный список, начинающийся с указанной записи, метод глубокого копирования (deepCopy) из класса Ent гу рекурсивно вызывает самого себя. Этот прием выг/\ядит изящно и прекрасно работает для не слишком длинных сегментов, однако он не совсем подходит для клонирования связных списков, поскольку для каждого элемента в списке он делает в стеке новую запись. И если список buckets окажется большим, может возникнуть переполнение стека. Во избежание этого можно заменить в методе deepCopy рекурсию итерацией // Копирование в цикле связного списка. // начинающегося с указанной записи г deepCopy() { Entry result = new гр р = p.next) p.next = new г, p.next.value, p.next.next); return result; Окончательный вариант клонирования сложных объектов заключается в вызове метода г, в установке всех полей в первоначальное состояние ив вызове методов более высокого уровня, окончательно определяющих состояние объекта. В случае с классом HashTable поле buckets должно получить при инициализации новый массив сегментов, а затем для каждой пары ключ/значение в клонируемой хэш-таблице следует вызвать метод put(key, value) (не показан в распечатке. При таком подходе обычно получается простой, довольно элегантный метод clone, пусть даже и неработающий столь же быстро, как при прямом манипулировании содержимым объекта и его клона. Как и конструктор, метод clone не должен вызывать каких-либо переопределяемых методов создаваемого клона (статья 15). Если метод clone вызывает переопределенный метод, этот метод будет выполняться до того, как подкласс, в котором он был определен, установит для клона нужное состояние. Это может привести к разрушению и клона, и самого оригинала. Поэтому метод put (key, val ue) должен быть либо не переопределяемым (final), либо закрытым. (Если это закрытый метод, то, по- видимому, он является вспомогательным (helper method) для другого, открытого и переопределяемого метода) Метод clone в классе Object декларируется как способный инициировать исключительную ситуацию CloneNotSupportedException, однако в переопределенных методах clone эта декларация может быть опущена. Метод clone в окончательном классе не должен иметь такой декларации, поскольку работать с методами, не инициирующими обрабатываемых исключений, приятнее, чем с теми, которые их инициируют (статья 41). Если же метод clone переопределяется в расширяемом классе, особенно в классе, предназначенном для наследования (статья 15), новый метод clone должен иметь декларацию для исключительной ситуации г. Это дает возможность изящно отказаться в подклассе от клонирования, реализовав следующий метод clone: , // Метод клонирования, гарантирующий невозможность // клонирования экземпляров public final Object clone() throws г г new г } Следовать указанному совету необязательно, поскольку если для переопределяемого метода clone не было заявлено, что он может инициировать CloneNotSupportedException, новый метод clone в подклассе, не подлежащем клонированию, всегда может инициировать необрабатываемое исключение, например UnsupportedOperationException. Однако установившаяся практика говорит, что в этих условиях правильным будет исключение CloneNotSupportedExceptrion. Подведем итоги. Все классы, реализующие интерфейс Cloneable, должны переопределять метод clone как открытый. Этот метод должен сначала вызвать метод supe га затем привести в порядок все поля, подлежащие восстановлению. Обычно это означает копирование всех изменяемых объектов, составляющих внутреннюю "глубинную структуру" клонируемого объекта, и замену всех ссылок на эти объекты ссылками на соответствующие копии. Хотя обычно внутренние копии можно получить рекурсивным вызовом метода clone, такой подход не всегда является самым лучшим. Если класс содержит только поля простого типа и ссылки на неизменяемые объекты, 'ГО, по-видимому, нет полей, нуждающихся в восстановлении. Из этого правила есть исключения. Например, поле, предоставляющее серийный номер или иной уникальный идентификатора также поле, показывающее время создания объекта, нуждаются в восстановлении, даже если они имеют простой тип или являются неизменяемыми. Нужны ли все эти сложности Не всегда. Если вы расширяете класс, реализующий интерфейсу вас практически не остается иного выбора, кроме как реализовать правильно работающий метод clone. В противном случае вам, по-видимому, лучше отказаться от некоторых альтернативных способов копирования объектов либо от самой этой возможности. Например, для неизменяемых классов нет смысла поддерживать копирование объектов, поскольку копии будут фактически неотличимы от оригинала. Изящный подход к копированию объектов - создание конструктора копий. Конструктор копии - это всего лишь конструктор, единственный аргумент которого имеет тип, соответствующий классу, где находится этот конструктор, например public Yum(Yum yum); Небольшое изменение - и вместо конструктора имеем статический метод генерации public static Yum newlnstance(Yum yum); Использование конструктора копий (или, как его вариант, статического метода генерации) имеет множество преимуществ перед механизмом Cloneableclone: оно не связано с рискованным, выходящим за рамки языка Java механизмом создания объектов не требует следования расплывчатым, плохо документированным соглашениям не конфликтует с обычной схемой использования полей final не требует от клиента перехвата ненужных исключений наконец, клиент получает объект строго определенного типа. Конструктор копий или статический метод генерации невозможно поместить в интерфейс, Cloneable не может выполнять функции интерфейса, поскольку не имеет открытого метода clone. Поэтому нельзя утверждать, что, используя конструктор копий вместо метода clone, вы отказываетесь от возможностей интерфейса. Более того, конструктор копий (или статический метод генерации) может иметь аргумент, тип которого соответствует интерфейсу, реализуемому этим классом. Например, все реализации коллекций общего назначения, по соглашению, имеют конструктор копий с аргументом типа Collection или Мар. Конструкторы копий, использующие интерфейсы, позволяют клиенту выбирать для копии вариант реализации вместо того, чтобы принуждать его принимать реализацию исходного класса. Допустим, что у вас есть объективы хотите скопировать его как экземпляр ArrayList. Метод с!опе не предоставляет такой возможности, хотя это легко делается с помощью конструктора копий new ArrayList(l). Рассмотрев все проблемы, связанные с интерфейсом Cloneable, можно суверенностью сказать, что остальные интерфейсы не должны становиться его расширением, а классы, предназначенные для наследования (статья 15), не должны его реализовывать. Из-за множества недостатков этого интерфейса некоторые высококвалифицированные программисты предпочитают никогда не переопределять метод clone и никогда им не пользоваться за исключением, быть может, случая простого копирования массивов. Учтите. что, если в классе, предназначенном для наследования, вы не создадите, по меньшей мере, правильно работающий защищенный метод clone, реализация интерфейса Cloneable в подклассах станет невозможной. Подумайте над реализацией интерфейса в отличие от других обсуждавшихся в этой главе методов, метод соmрагеТо в классе Object не декларируется. Пожалуй, это единственный такой метод в интерфейсе java.lang.СоmрагаЫе. По своим свойствам он похож на метод equals из класса Object, за исключением того, что, помимо простой проверки равенства, он позволяет выполнять упорядочивающее сравнение. Реализуя интерфейс СоmрагаЫе, класс показывает, что его экземпляры обладают естественным своиством упорядочения и ordering). Сортировка массива объектов, реализующих интерфейс СоmрагаЫе, выполняется просто Arrays.sort(a); Для объектов СоmрагаЫе также просто выполняется поиск, вычисляются предельные значения и обеспечивается поддержка автоматически сортируемых коллекций. Например, следующая программа, использующая тот факт, что класс String реализует I1нтерфейс СоmрагаЫе, печатает в алфавитном порядке список аргументов, указанных в командной строке, удаляя при этом дубликаты public class WordList { public static void main(String[] args) { Set 5 = new TreeSet(); s,addAll(Arrays.asList(args)); System.out.println(s); } } Реализуя интерфейс СотрагаЫе, вы разрешаете вашему классу взаимодействовать совсем обширным набором общих алгоритмов и реализаций коллекций, которые связаны с этим интерфейсом. Приложив немного усилий, вы получаете огромные возможности. Практически все классы значений в библиотеках платформы Java реализуют интерфейс СотрагаЫе. И если выпишите класс значений с очевидным свойством естественного упорядочения - алфавитным, числовым либо хронологическим- вы должны хорошо продумать реализацию этого интерфейса. В этой статье рассказывается о том, как к этому приступить. Общее соглашение для метода соmрагеТо имеет тот же характер, что и соглашение Для метода equals. Приведем его текст по спецификации интерфейса СотрагаЫе: Выполняет сравнение текущего и указанного объекта и определяет их очередность. Возвращает отрицательное целое число, нуль или положительное целое число в зависимости оттого, меньше ли текущий объект, равен или больше указанного объекта. Если тип указанного объекта не позволяет сравнивать его с текущим объектом, инициируется исключительная ситуация ClassCastException. В следующем описании запись sgп(выражение) обозначает математическую функцию т, которая, по определению, возвращает -1, О или 1 в зависимости оттого, является ли значение выражения отрицательным, равным нулю или положительным. Разработчик должен гарантировать тождество sgn(x.compareTo(y)) = = -sgп(у.соmрагеТо(х)) для всех хи у. (Это подразумевает, что выражение х. сотрагеТо(у) должно инициировать исключительную ситуацию тогда и только тогда, когда у. сотрагеТо(х) инициирует исключение) Разработчик должен также гарантировать транзитивность отношения (х.сотрагеТо(у»О & & y.compareTo(z»O) подразумевает x.compareTo(z»O. Наконец, разработчик должен гарантировать, что из тождествах. соmрагеТо(у) == О вытекает тождество sgn(x.compareTo(z)) == sgn(y.compareTo(z)) для всех z. Настоятельно рекомендуется выполнять условие х. сотрагеТо(у) = = Ох. Вообще говоря, для любого класса, который реализует интерфейс СотрагаЫе, но нарушает это условие, сей факт должен быть четко оговорен (в сопроводительной документации. Рекомендуется использовать следующую формулировку "Примечание данный класс имеет естественное упорядочение, не согласующееся с условием равенства Как и соглашения для метода equals (статья 7), соглашения для соmрагеТо не так сложны, как это кажется. Для одного класса любое разумное отношение упорядочения будет соответствовать соглашениям для соmрагеТо. Для сравнения разных классов метод соmрагеТо, в отличие от метода equals, использоваться не должен если сравниваются две ссылки на объекты различных классов, можно инициировать исключительную ситуацию ClassCastException. Метод compareTo обычно таки делает. И хотя представленное соглашение не исключает сравнения между классами, в библиотеках для платформы Java, в частности в версии 1.4, нет классов, которые поддерживали бы такую возможность. Точно также, как класс, нарушающий соглашения для метода 'hashCode, может испортить другие классы, работа которых зависит от хэширования, класс, не соблюдающий соглашений для метода соmрагеТо, способен нарушить работу других классов, использующих сравнение. К классам, связанным со сравнением, относятся упорядоченные коллекции, TreeSet и ТгееМар, а также вспомогательные классы Collections и Arrays, содержащие алгоритмы поиска и сортировки. Рассмотрим условия соглашения для сотрагеТо. Первое условие гласит, что если вы измените порядок сравнения двухссылок на объекты, произойдет вполне ожидаемая вещь если первый объект меньше второго, то второй должен быть больше первого, если первый объект равен второму, то и второй должен быть равен первому, наконец, если первый объект больше второго, то второй должен быть меньше первого. Второе условие если первый объект больше второго, а второй объект больше третьего, то первый объект должен быть больше третьего. Последнее условие объекты, сравнение которых дает равенство, при сравнении с любым третьим объектом должны показывать одинаковый результат. Из этих трех условий следует, что проверка равенства, осуществляемая с помощью метода соmрагеТо, должна подчиняться тем же самым ограничениям, которые продиктованы соглашениями для метода equals: рефлективность, симметрия, транзитивность и отличие от null. Следовательно, здесь справедливо тоже самое предупреждение невозможно расширить порождающий экземпляры класс, вводя новый аспект и не нарушая при этом соглашения для метода сотрагеТо (статья 7). Возможен обходной Путь. Если вы хотите добавить важное свойство к классу, реализующему интерфейс СотрагаЫе, не расширяйте его, а напишите новый независимый класс, в котором для исходного класса выделено отдельное поле. Затем добавьте метод представления, возвращающий значение этого поля. Это позволит вам реализовать во втором классе любой метод сотрагеТо, который вам нравится. При необходимости клиент может рассматривать экземпляр второго класса как экземпляр первого класса. Последний пункт соглашений для соmрагеТо, являющийся скорее предположением, чем настоящим условием, постулирует, что проверка равенства, осуществляемая с помощью метода соmрагеТо, обычно должна давать те же самые результаты, что и метод equals. Если это условие выполняется, считается, что упорядочение, задаваемое методом соmрагеТо, согласуется с проверкой равенства (consistent with equals). Если же оно нарушается, то упорядочение называется несогласующимся с проверкой равенства (inconsistent with и. Класс, чей метод соmрагеТо устанавливает порядок, несогласующийся с условием равенства, будет работоспособен, однако отсортированные коллекции, содержащие элементы этого класса, могут не соответствовать общим соглашениям для соответствующих интерфейсов коллекций (Collection, Set или Мар). Дело в том, что общие соглашения для этих интерфейсов определяются в терминах метода equals, тогда как в отсортированных коллекциях ИСПОЛl>зуется проверка равенства, которая реализуется методом соmрагеТо, а не equals. Если это произойдет, катастрофы не будет, но иногда это следует учитывать. Например, рассмотрим класс BigDecimal, чей метод соmрагеТо не согласуется с проверкой равенства. Если вы создадите HashSet и добавите в него новую запись BigDecimal("1.0"), а затем BigDecimal("1.00"), этот набор будет содержать два элемента, поскольку два добавленных в него экземпляра класса BigDecimal не будут равны, если их сравнивать с помощью' метода equals. Однако если вы выполняете туже самую процедуру сане, полученный набор будет содержать только один элемент, поскольку два представленных экземпляра BigDecimal оказываются равны при их сравнении с помощью метода соmаргеТо. (См. документацию на BigDecimal.) Процедура написания метода соmрагеТо похожа на процедуру для метода equals, но есть несколько ключевых различий. Перед преобразованием типа нет необходимости проверять тип аргумента. Если аргумент имеет неправильный тип, метод соmрагеТо обязан инициировать исключительную ситуацию ClassCastException. Если аргумент имеет значение null, метод compare То должен инициировать исключительную ситуацию NullPointerException. Тоже самое вы получите, если приведете аргумент к правильному типу, а затем попытаетесь обратиться к его членам. Сравнение полей само по себе является упорядочивающим сравнением, а не сравнением с проверкой равенства. Сравнение полей, имеющих ссылки на объекты, осуществляйте путем рекурсивного вызова метода сотрагеТо. Если полене реализует интерфейс Сотра rable или вам необходимо нестандартное упорядочение, вы можете использовать явную реализацию интерфейса Comparator. Либо напишите ваш собственный метод, либо воспользуйтесь уже имеющимся, как это было в случае с методом сотра геТо в классе CaselnsensitiveString (статья 7): public int compareTo(Object о) { CaselnsensitiveString cis = (CaselnsensitiveString)o; return String.CASE_INSENSIT IVE_ORDER.compare(s, cis.s); Поля простого типа нужно сравнивать с помощью операторов < и >, массивы применяя эти инструкции для каждого элемента. Если у класса есть несколько значимых полей, порядок их сравнения критически важен. Вы должны начать с самого значимого поля и затем следовать в порядке убывания значимости. Если сравнение дает что-либо помимо нуля (означающего равенство, все, что вам нужно сделать- возвратить этот результат. Если самые значимые поля равны, продолжайте сравнивать следующие по значимости поля и т. д. Если все поля равны, равны и объекты, поэтому возвращайте нуль. Такой прием демонстрирует метод соmрагеТо для класса PhoneNumber из статьи 8: public int compareTo(Object о) { PhoneNumber р = (PhoneNumber)o; |