Джош Блох
.pdfГлава 3 • Методы, общие для всех объектов
Заметим, что такое решение не будет работать, если поле ele ments имеет модификатор final, поскольку тогда методу clone было бы запрещено помещать туда новое значение. Это фундаментальная проблема: архитектура клона несовместима с обычным использо ванием полей final, содержащих ссылки на изменяемые объек ты. Исключение составляют случаи, когда эти изменяемые объекты могут безопасно использовать сразу и объект, и его клон. Чтобы сде лать класс клонируемым, возможно потребуется убрать с некоторых полей модификатор final.
Не всегда бывает достаточно рекурсивного вызова метода clone. Например, предположим, что вы пишете метод clone для хэш-табли- цы, состоящей из набора сегментов (buckets), каждый из которых содержит ссылку на первый элемент в связном списке, содержащем несколько пар ключ/значение, или содержит null, если этот сегмент пуст. Для лучшей производительности в этом классе вместо java.util. LinkedList используется собственный упрощенный связный список:
public class HashTable implements Cloneable { private Entry[] buckets =
private static class Entry { final Object key; Object value;
Entry next;
Entry(Object key, Object value, Entry next) { this.key = key;
this, value = value; this, next = next;
}
}
... // Остальное опущено
}
Предположим, вы просто рекурсивно клонируете массив buck ets, как это делалось для класса Stack:
// Ошибка: объекты будут иметь общее внутреннее состояние!
80
С татья 11
(©Override public HashTable clone(){ try {
HashTable result = (HashTable) super.clone(); result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportException e) { throw new AssertionErrorO;
}
}
Хотя клон и имеет собственный набор сегментов, последний ссылается на те же связные списки, что и исходный набор, а это мо жет с легкостью привезти к непредсказуемому поведению и клона, и оригинала. Для устранения проблемы вам придется отдельно ко пировать связный список для каждого сегмента. Представим один из распространенных приемов:
public class HashTable implements Cloneable { private Entry[] buckets = ...;
private static class Entry { final 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.deepCopyO);
}
}
81
Глава 3 • Методы, общие для всех объектов
@Override public HashTable clone(){ try {
HashTable result = (HashTable) super.clone(); result.buckets = new Entry[buckets.length]; for (int i = 0; i < buckets.lenght; i ++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy(); return result;
} catch (CloneNotSupportedException e) { throw new AssertionError()
}
}
... // Остальное опущено
}
Закрытый класс HashTable. Entry был привнесен для реализации метода «глубокого копирования» (deep сору). Метод clone в классе HashTable размещает в памяти новый массив buckets нужного разме ра, а затем в цикле просматривает исходный набор buckets, выполняя глубокое копирование каждого непустого сегмента. Чтобы скопиро вать связный список, начинающийся с указанной записи, метод глу бокого копирования (deepCopy) из класса Entry рекурсивно вызывает себя самого. Хотя этот прием выглядит изящно и прекрасно работает для не слишком длинных сегментов, он не слишком хорош для кло нирования связных списков, поскольку для каждого элемента в спи ске он делает в стеке новую запись. И если список buckets окажется большим, это может легко вызвать переполнение стека. Чтобы поме шать этому случиться, в методе deepCopy вы можете заменить рекур сию итерацией:
// Копирование в цикле связного списка, начинающегося с указанной записи
Entry deepCopyO {
Entry result - new Entry(key, value, next);
for (Entry p = result; p.next ! = null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
82
С татья 11
return result;
}
Окончательный вариант клонирования сложных объектов заключа ется в вызове метода super, clone, установке всех полей в первоначаль ное состояние и вызове методов более высокого уровня, окончательно определяющих состояние объекта. В нашем случае с классом HashTable поле buckets должно получить при инициализации новый массив сегмен тов, а затем для каждой пары ключ/значение в клонируемой хэш-табли це следует вызвать метод put (key, value) (в распечатке не показан). При таком подходе обычно получается простой, достаточно элегантный метод clone, пусть даже и не работающий столь же быстро, как при пря мом манипулировании содержимым объекта и его клона.
Как и конструктор, метод clone не должен вызывать каких-либо переопределяемых методов, взятых из создаваемого клона (статья 17). Если метод clone вызывает переопределенный метод, то этот метод будет выполняться до того, как подкласс, в котором он был определен, установит для клона нужное состояние. Это вполне может привести к разрушению и клона, и самого оригинала. Поэтому метод put (key, value), о котором говорилось в предыдущем абзаце, должен быть либо непереопределяемым (final), либо закрытым. (Если это закрытый метод, то, по-видимому, он является вспомогательным (helper method) для другого, открытого и переопределяемого метода.)
Метод clone в классе Object декларируется как способный ини циировать исключительную ситуацию CloneNotSupportedException, однако в переопределенных методах clone эта декларация может быть опущена. Метод clone в окончательном классе не должен иметь такой декларации, поскольку работать с методами, не инициирую щими обрабатываемых исключений, приятнее, чем с теми, которые их инициируют (статья 59). Если же метод clone переопределяет ся в расширяемом классе, а особенно в классе, предназначенном для наследования (статья 17), новый метод clone подражает поведению Object.clone, он также должен декларироваться как закрытый tected) и иметь декларацию для исключительной ситуации
Глава 3 • Методы, общие для всех объектов
Support Except ion, и класс не должен реализовывать интерфейс Clonebable. Это дает подклассу свободу выбора, реализовывать Cloneable или нет.
Еще одна деталь заслуживает внимания. Если вы решите сде лать клонируемым потоковый класс, помните, что метод clone нуж но должным образом синхронизировать, как и любой другой метод (статья 66). Метод Object’s clone не синхронизирован, и, хотя это в принципе нормально, вам возможно потребуется написать синхро низированный метод clone, который бы запускал super. clone().
Подведем итоги. Все классы, реализующие интерфейс Cloneable, должны переопределять метод clone как открытый. Этот публичный метод должен сначала вызвать метод super.clone, а затем привести в порядок все поля, подлежащие восстановлению. Обычно это означа ет копирование всех изменяемых объектов, составляющих внутреннюю «глубинную структуру» клонируемого объекта и замену всех ссылок на эти объекты ссылками на соответствующие копии. Хотя обычно эти внутренние копии можно получить рекурсивным вызовом метода clone, такой подход не всегда является самым лучшим. Если класс содержит одни только поля простого типа и ссылки на неизменяемые объекты, то в таком случае, по-видимому, нет полей, нуждающихся в восстанов лении. Из этого правила есть исключения. Например, поле, предостав ляющее серийный номер или иной уникальный идентификатор, а также поле, показывающее время создания объекта, нуждаются в восстанов лении, даже если они имеют простой тип или являются неизменяемыми.
Так ли нужны все эти сложности? Не всегда. Если вы расши ряете класс, реализующий интерфейс Cloneable, у вас практически не остается иного выбора, кроме как реализовать правильно работа ющий метод clone. В противном случае вам, по-видимому, лучше отказаться от некоторых альтернативных способов копирования объектов либо от самой этой возможности. Например, для неиз меняемых классов нет смысла поддерживать копирование объектов, поскольку копии будут фактически неотличимы от оригинала.
Изящный подход к копированию объектов — создание кон структора копий или копирование статических методов гене
64
С татья 11
рации. Конструктор копий — это всего лишь конструктор, един ственный аргумент которого имеет тип, соответствующий классу, где находится этот конструктор, например:
public Yum(Yum yum);
Небольшое изменение — и вместо конструктора имеем статиче ский метод генерации:
public static Yum newInstance(Yum yum);
Использование конструктора копий (или, как его вариант, ста тического метода генерации) имеет много преимуществ перед меха низмом Cloneable/clone: оно не связано с рискованным, выходящим за рамки языка Java механизмом создания объектов; не требует сле дования расплывчатым, плохо документированным соглашениям; не конфликтует с обычной схемой использования полей final; не тре бует от клиента перехвата ненужных исключений; наконец, клиент получает объект строго определенного типа. Конструктор копий или статический метод генерации невозможно поместить в интер фейс, Cloneable не может выполнять функции интерфейса, поскольку не имеет открытого метода clone. Поэтому нельзя утверждать, что, когда вместо метода clone вы используете конструктор копий, вы от казываетесь от возможностей интерфейса.
Более того, конструктор копий (или статический метод генера ции) может иметь аргумент, тип которого соответствует интерфейсу, реализуемому этим классом. Например, все реализации коллекций общего назначения, по соглашению, имеют конструктор копий с ар гументом типа Collection или Мар. Конструкторы копий и методы статической генерации, использующие интерфейсы, позволяют кли енту выбирать для копии вариант реализации вместо того, чтобы принуждать его принять реализацию исходного класса. Например, допустим, у вас есть объект Hash Set s, и вы хотите скопировать его как экземпляр Т reeSet. Метод clone не предоставляет такой возмож ности, хотя это легко делается с помощью конструктора копий: new
Т reeSet(s).
85
Глава 3 • Методы, общие для всех объектов
Рассмотрев все проблемы, связанные с интерфейсом Cloneable, можно с уверенностью сказать, что остальные интерфейсы не долж ны становиться его расширением, а классы, которые предназначены для наследования (статья 17), не должны его реализовывать. И з-за множества недостатков этого интерфейса некоторые высококвали фицированные программисты просто предпочитают никогда не пе реопределять метод clone и никогда им не пользоваться за исклю чением, быть может, простого копирования массивов. Учтите, что, если в классе, предназначенном для наследования, вы не создадите по меньшей мере правильно работающий защищенный метод clone, реализация интерфейса Cloneable в подклассах станет невозможной.
Подумайте над реализацией интерфейса Comparable
В отличие от других обсуждавшихся в этой главе методов ме тод compareTo в классе Object не декларируется. Пожалуй, это един ственный такой метод в интерфейсе java, lang.Comparable. По своим свойствам он похож на метод equals из класса Oblect, за исключени ем того, что помимо простой проверки равенства он позволяет выпол нять упорядочивающее сравнение. Реализуя интерфейс Comparable, класс показывает, что его экземпляры обладают естественным свой ством упорядочения (natural ordering). Сортировка массива объек тов, реализующих интерфейс Comparable, выполняется просто:
Arrays.sort(a);
Для объектов Comparable так же просто выполняется поиск, вы числяются предельные значения и обеспечивается поддержка авто матически сортируемых коллекций. Например, следующая програм ма, использующая тот факт, что класс String реализует интерфейс Comparable, печатает в алфавитном порядке список аргументов, ука занных в командной строке, удаляя при этом дубликаты:
86
С тать я 12
public class WordList {
public static void main(String[] args) { Set s = new TreeSet(); s.addAll(Arrays.asList(args)); System.out.println(s);
}
}
Реализуя интерфейс Comparable, вы разрешаете вашему классу взаимодействовать со всем обширным набором общих алгоритмов и реализаций коллекций, которые связаны с этим интерфейсом. При ложив немного усилий, вы получаете огромное множество возмож ностей. Практически все классы значений в библиотеках платформы Java реализуют интерфейс Comparable. И если вы пишете класс зна чений с очевидным свойством естественного упорядочивания — ал фавитным, числовым либо хронологическим, — вы должны хорошо подумать над реализацией этого интерфейса.
public interface Comparable<T> { int compareTo(T t);
}
Общее соглашение для метода compareTo имеет тот же характер, что и соглашение для метода equals:
Выполняет сравнение текущего и указанного объекта и опреде ляет их очередность. Возвращает отрицательное целое число, нуль или положительное целое число, в зависимости от того, меньше ли текущий объект, равен или, соответственно, больше указанно го объекта. Если тип указанного объекта не позволяет сравнивать его с текущим объектом, инициируется исключительная ситуация
ClassCastException.
В следующем описании запись здп(выражение) обозначает ма тематическую функцию signum, которая, по определению, возвраща ет -1, 0 или 1, в зависимости от того, является ли значение выражения отрицательным, равным нулю или положительным.
87
Глава 3 • Методы, общие для всех объектов
•Разработчик должен гарантировать тождество sgn(x.compa-
геТо(у)) == -sgn(y.сошрагеТо(х)) для всех х и у. (Это под
разумевает, что выражение х.compareTo(y) должно иниции ровать исключительную ситуацию тогда и только тогда, когда
у.compareTo(х) инициирует исключение.)
•Разработчик должен также гарантировать транзитивность
отношения: (х. compareTo(у)>0 && у. compareTo(z)>0) подра
зумевает х.compareTo(z)>0.
•Наконец, разработчик должен гарантировать, что из тожде
ства х. compareTo(y) == 0 вытекает тождество sgn(x. compareTo(z)) == sgn(y.compareTo(z)) для всех z.
•Настоятельно (хотя и не безусловно) рекомендуется выпол
нять условие (х. сот рагеТо( у) == 0) == (х.equals(y)). Вооб
ще говоря, для любого класса, который реализует интерфейс Comparable, но нарушает это условие, сей факт должен быть четко оговорен. Рекомендуется использовать следующую формулировку: «Примечание: данный класс имеет естествен ное упорядочение, не согласующееся с условием равенства».
Пускай математическая природа этого соглашения у вас не вы зывает отвращения. Как и соглашения для метода equals (статья 8), соглашения для compareTo не так сложны, как это кажется. Для од ного класса любое разумное отношение упорядочения будет соответ ствовать соглашениям для compareTo. Для сравнения разных классов метод compareTo, в отличие от метода equals, использоваться не дол жен: если сравниваются две ссылки на объекты различных классов, можно инициировать исключительную ситуацию ClassCastException. В подобных случаях метод compareTo обычно т а к делает и должен т а к делать, если параметры класса заданы верно. И хотя представ ленное соглашение не исключает сравнения между классами, в би блиотеках для платформы Java, в частности в версии 1.6, нет классов, которые бы такую возможность поддерживали.
8 8
С тать я 12
Точно так же, как класс, нарушающий соглашения для метода hashCode, может испортить другие классы, работа которых зависит от хэширования, класс, нарушающий соглашения для метода сотра геТо, способен нарушить работу других классов, использующих сравнение. К классам, связанным со сравнением, относятся упорядоченные кол лекции, TreeSet и ТгееМар, а также вспомогательные классы Collec tions и Arrays, содержащие алгоритмы поиска и сортировки.
Рассмотрим условия соглашения для сопрагеТо. Первое условие гласит, что, если вы измените порядок сравнения для двух ссылок на объекты, произойдет вполне ожидаемая вещь: если первый объ ект меньше второго, то второй должен быть больше первого, если первый объект равен второму, то и второй должен быть равен перво му, наконец, если первый объект больше второго, то второй должен быть меньше первого. Второе условие гласит, что если первый объект больше второго, а второй объект больше третьего, то тогда первый объект должен быть больше третьего. Последнее условие гласит, что объекты, сравнение которых дает равенство, при сравнении с любым третьим объектом должны показывать одинаковый результат.
Из этих трех условий следует, что проверка равенства, осущест вляемая с помощью метода сотра геТо, должна подчиняться тем же са мым ограничениям, которые продиктованы соглашениями для метода equals: рефлексивность, симметрия, транзитивность и отличие от null. Следовательно, здесь можно дать то же самое предупреждение: не возможно расширить порождающий экземпляры класс, вводя новый аспект и не нарушая при этом соглашения для метода сотра геТо (ста тья 8). Возможен и обходной путь. Если вы хотите добавить важное свойство к классу, реализующему интерфейс Comparable, не расширяй те его, а напишите новый независимый класс, в котором для исходного класса выделено отдельное поле. Затем добавьте метод представления, возвращающий значение этого поля. Это даст вам возможность реали зовать во втором классе любой метод сотра геТо, который вам нравится. При этом клиент при необходимости может рассматривать экземпляр второго класса как экземпляр первого класса.
89
