Мои Конспекты
Главная | Обратная связь


Автомобили
Астрономия
Биология
География
Дом и сад
Другие языки
Другое
Информатика
История
Культура
Литература
Логика
Математика
Медицина
Металлургия
Механика
Образование
Охрана труда
Педагогика
Политика
Право
Психология
Религия
Риторика
Социология
Спорт
Строительство
Технология
Туризм
Физика
Философия
Финансы
Химия
Черчение
Экология
Экономика
Электроника

Соблюдайте осторожность при переопределении метода Сlоnе



Интерфейс Сlonеаblе проектировался в качестве дополнительного интерфейса (mixin) (статья 16), позволяющего объектам объявлять о том, что они могут быть клонированы. К сожалению, он не может использоваться для этой цели. Его основ­ной недостаток - отсутствие метода clone; в самом же классе Object метод clone является закрытым. Вы не можете, не обращаясь к механизму отражения свойств (reflection) (статья 35), вызывать для объекта метод clone лишь на том основании, что он реализует интерфейс Сlоnеаblе. Даже отражение может завершиться неудачей, поскольку нет гарантии, что у данного объекта есть доступный метод clone. Несмотря на этот и другие. недочеты, данный механизм используется настолько широко, что Имеет смысл с ним разобраться. В этой статье рассказывается о том, каким образом создать хороший метод clone, обсуждается, когда имеет смысл это делать, а также кратко описываются альтернативные подходы.

 

 

Что же делает интерфейс Cloneable, который, как оказалось, не имеет методов?

Он определяет поведение закрытого метода clone в классе Object: если какой-либо класс реализует интерфейс Cloneable, то метод clone, реализованный в классе Object, возвратит его копию с воспроизведением всех полей, в противном случае будет ини­циирована исключительная ситуация CloneNotSupportedException. Это совершенно нетипичный способ использования интерфейсов, и ему не следует подражать. Обычно факт реализации некоего интерфейса говорит кое-что о том, что этот класс может делать для своих клиентов. Интерфейс же Cloneable лишь меняет поведение некоего защищенного метода в суперклассе.

для того чтобы реализация интерфейса Cloneable могла оказывать какое-либо воздействие на класс, он сам и все его суперклассы должны следовать довольно сложному, трудно выполнимому и в значительной степени недокументированному протоколу. Получающийся механизм не укладывается в рамки языка Java: объект создается без использования конструктора.

Общие соглашения для метода clone довольно свободны. Они описаны в специ­фикации класса java.lang.Object:

Метод создает и возвращает копию объекта. Точное значение термина "копия" может зависеть от класса этого объекта. Общая задача ставится так, чтобы для любо­го объекта х оба выражения

x.clone() ! = х

и

x.clone().getClass() == x.getClass()

 

 

возвращали true, однако эти требования не являются безусловными. Обычно условие состоит в том, чтобы выражение

x.clone().equals(x)

возвращало true, однако и это требование не является безусловным копирование объекта обычно приводит к созданию нового экземпляра соответствующего класса, при этом может потребоваться также копирование внутренних структур данных. Никакие конструкторы не вызываются.

Такое соглашение создает множество проблем. Условие "никакие конструкторы не вызываются" является слишком строгим. Правильно работающий метод clone может воспользоваться конструкторами для создания внутренних объектов клона. Если же класс является окончательным (final), метод clone может просто вернуть объект, СОЗДанный конструктором.

Условие, что x.clone().getClaSs() должно быть тождественно x.getClass(), является слишком слабым. Как правило, программисты полагают, что если они расши­ряют класс и вызывают в полученном подклассе метод super. clone, то получаемый в результате объект будет экземпляром этого подкласса. Реализовать такую схему суперкласс может только одним способом - вернуть объект, полученный в резуль­тате вызова метода supeг.clone. Если метод clone возвращает объект, созданный конструктором, это будет экземпляр другого класса. Поэтому, если в расширяемом

 

 

классе вы переопределяете метод clone, то возвращаемый объект вы должны получать вызовом super. clone. Если все суперклассы данного класса выполняют это условие, рекурсивный вызов метода supe г. clone в конечном счете приведет к вызову метода clone из класса Object и к созданию экземпляра именно того класса, который нужен. Этот механизм отдаленно напоминает автоматическое сцепление конструкто­ров, за исключением того, что оно не является принудительным.

В версии 1.3 интерфейс Cloneable не раскрывает, какие обязанности берет на себя класс, реализуя этот интерфейс. В спецификации не говорится ничего, помимо того, что реализация данного интерфейса влияет на реализацию метода clone в классе Object. На практике же требования сводятся к тому, что в классе, реализующем интерфейс Cloneable, должен быть представлен правильно работающий открытый метод clone. Вообще же, выполнить это условие невозможно, если только все супер­классы этого класса не будут иметь правильную реализацию метода clone, открытую или защищенную.

Предположим, что вы хотите реализовать интерфейс Cloneable с помощью класса, чьи суперклассы имеют правильно работающие методы clone. В зависимости от природы этого класса объект, который вы получите после вызова super.clone(), может быть, а может и не быть похож на тот, что вы будете иметь в итоге. С точки зрения любого суперкласса этот объект будет полнофункциональным клоном исходно­го объекта. Поля, объявленные в вашем классе (если таковые имеются), будут иметь те же значения, что и поля в клонируемом объекте. Если все поля объекта содержат значения простого типа или ссылки на неизменяемые объекты, то возвращаться будет именно тот объект, который вам нужен, и дальнейшая обработка в этом случае не требуется. Такой вариант демонстрирует, например, класс PhoneNumbe r из статьи 8. Все, что здесь нужно,- это обеспечить в классе Object открытый доступ к защищен­ному методу clone:

public Object clone() {

try {

return super,clone();

catch(CloneNotSupportedException е) {

throw new Error("Assertion failure"); // Этого не может быть

}

}

Однако если ваш объект содержит поля, имеющие ссылки на изменяемые объекты, такая реализация метода clone может иметь катастрофические последствия. Рассмотрим класс Stack' из статьи 5:

public class Stack {

private Object[] elements;

private int size = о;

public Stack(int initialCapacity) {

this.elements = new Object[initialCapacity];

}

 

 

 

public void push(Object е) {

ensureCapacity();

elements[size ++] = е;

}

public Object рор() {

if (size == О)

throw new EmptyStackException();

Object result = elements[- size];

elements[size] = null; // Убираем устаревшую ссылку

return result;

}

 

 

// Убедимся в том, что в стеке есть место хотя бы

// еще для одного элемента

private void ensureCapacity() {

if (elements.length == size) {

Object oldElements[] = elements;

elements = new Object[2 * elements.length + 1]; System.arraycopy(oldElements, о, elements, О, size);

}

}

}

 

Предположим, что вы хотите сделать этот класс клонируемым. Если его метод clone просто вернет результат вызова super.clone(), полученные экземпляр Stack будет иметь правильное значение в поле size, однако его поле elements будет ссылаться на тот же самый массив, что и исходный экземпляр Stack. Следовательно, изменение в оригинале будет нарушать инварианты клона, и наоборот. Вы быстро обнаружите, что ваша программа выдает бессмысленные результаты либо инициирует исключительную ситуацию NullPointerException.

Подобная ситуация не могла бы возникнуть, если бы использовался основной конструктор класса Stack. Метод clone фактически работает как еще один кон­структор, и вам необходимо убедиться в том, что он не вредит оригинальному объекту и правильно устанавливает инварианты клона. Чтобы метод clone в клас­се Stack работал правильно, он должен копировать содержимое стека. Проще всего это сделать путем рекурсивного вызова метода clone для массива elements:

public Object clone() throws CloneNotSupportedException {

Stack result = (Stack) super.clone();

result.elements = (Object[]) elements.clone();

return result;

}

 

 

Заметим, что такое решение не будет работать, если поле elements имеет моди­фикатор final, поскольку тогда методу clone запрещено помещать туда новое значе­ние. Это фундаментальная проблема: архитектура клона не совместима с обычным применением полей final, содержащих ссылки на изменяемые объекты. Исклю­чение составляют случаи, когда изменяемые объекты могут безопасно использовать сразу и объект, и его клон. Чтобы сделать класс клонируемым, возможно, потребуется убрать' у некоторых полей модификатор final.

Не всегда бывает достаточно рекурсивного вызова метода clone. Предположим, что вы пишите метод clone для хэш-таблицы, состоящей из набора сегментов (buckets), каждый из которых содержит либо ссылку на пер выи элемент в связном списке, имеющем несколько пар ключ/значение, либо null, если сегмент пуст. для лучшей производительности в этом классе вместо java.util.LinkedList используется собственный упрощенный связный список:

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;

}

}

// Остальное опущено

}

Предположим, что вы рекурсивно клонируете массив buckets, как это делалось для класса Stack:

// Ошибка: объекты будут иметь общее внутреннее состояние

public 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 рекурсию итерацией:

// Копирование в цикле связного списка.

// начинающегося с указанной записи

Entгy deepCopy() {

Entry result = new Entгy(key, value, next);

for (Entry р = result; p.next ! = null; р = p.next)

p.next = new Entгy(p.next.key, p.next.value, p.next.next);

return result;

}

Окончательный вариант клонирования сложных объектов заключается в вызове метода supeг.clone, в установке всех полей в первоначальное состояние и в вызове методов более высокого уровня, окончательно определяющих состояние объекта. В случае с классом 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 должен иметь декларацию для исключительной ситуации CloneNotSuppoгtedException. Это дает возможность изящно отказаться в подклассе от клонирования, реализовав следу­ющий метод clone:

,

// Метод клонирования, гарантирующий невозможность

// клонирования экземпляров

public final Object clone() throws CloneNotSuppoгtedException{

thгow new CloneNotSuppoгtedException();

}

 

 

Следовать указанному совету необязательно, поскольку если для переопределяе­мого метода clone не было заявлено, что он может инициировать CloneNotSupported­Exception, новый метод clone в подклассе, не подлежащем клонированию, всегда может инициировать необрабатываемое исключение, например UnsupportedOperation­Exception. Однако установившаяся практика говорит, что в этих условиях прав ильным будет исключение CloneNotSupportedExceptrion.

Подведем итоги. Все классы, реализующие интерфейс Cloneable, должны пере­определять метод clone как открытый. Этот метод должен сначала вызвать метод supe г. clone, а затем привести в порядок все поля, подлежащие восстановлению. Обычно это означает копирование всех изменяемых объектов, составляющих внут­реннюю "глубинную структуру" клонируемого объекта, и замену всех ссылок на эти объекты ссылками на соответствующие копии. Хотя обычно внутренние копии можно получить рекурсивным вызовом метода clone, такой подход не всегда является самым лучшим. Если класс содержит только поля простого типа и ссылки на неизменяемые объекты, 'ГО, по-видимому, нет полей, нуждающихся в восстановлении. Из этого пра­вила есть исключения. Например, поле, предоставляющее серийный номер или иной уникальный идентификатор, а также поле, показывающее время создания объекта, нужда­ются в восстановлении, даже если они имеют простой тип или являются неизменяемыми.

Нужны ли все эти сложности? Не всегда. Если вы расширяете класс, реализую­щий интерфейс Cloneable, у вас практически не остается иного выбора, кроме как реа­лизовать правильно работающий метод clone. В противном случае вам, по-видимому, лучше отказаться от некоторых альтернативных способов копирования объектов либо от самой этой возможности. Например, для неизменяемых классов нет смысла поддерживать копирование объектов, поскольку копии будут фактически неотличимы от оригинала.

Изящный подход к копированию объектов - создание конструктора копий.

Конструктор копии - это всего лишь конструктор, единственный аргумент которо­го имеет тип, соответствующий классу, где находится этот конструктор, например:

 

public Yum(Yum yum);

 

Небольшое изменение - и вместо конструктора имеем статический метод генерации:

 

public static Yum newlnstance(Yum yum);

 

Использование конструктора копий (или, как его вариант, статического метода генерации) имеет множество преимуществ перед механизмом Cloneableclone: оно не связано с рискованным, выходящим за рамки языка Java механизмом создания объектов; не требует следования расплывчатым, плохо документированным соглаше­ниям; не конфликтует с обычной схемой использования полей final не требует от клиента перехвата ненужных исключений; наконец, клиент получает объект строго определенного типа. Конструктор копий или статический метод генерации невозможно поместить в интерфейс, Cloneable не может выполнять функции интерфейса, посколь­ку не имеет открытого метода clone. Поэтому нельзя утверждать, что, используя кон­структор копий вместо метода clone, вы отказываетесь от возможностей интерфейса.

 

 

 

Более того, конструктор копий (или статический метод генерации) может иметь аргумент, тип которого соответствует интерфейсу, реализуемому этим классом. Напри­мер, все реализации коллекций общего назначения, по соглашению, имеют конструктор копий с аргументом типа Collection или Мар. Конструкторы копий, использующие интерфейсы, позволяют клиенту выбирать для копии вариант реализации вместо того, чтобы принуждать его принимать реализацию исходного класса. Допустим, что у вас есть объект LinkedList 1 и вы хотите скопировать его как экземпляр ArrayList. Метод с!опе не предоставляет такой возможности, хотя это легко делается с помощью конструктора копий new ArrayList(l).

Рассмотрев все проблемы, связанные с интерфейсом Cloneable, можно с уверен­ностью сказать, что остальные интерфейсы не должны становиться его расширением, а классы, предназначенные для наследования (статья 15), не должны его реализовы­вать. Из-за множества недостатков этого интерфейса некоторые высококвалифициро­ванные программисты предпочитают никогда не переопределять метод clone и никогда им не пользоваться за исключением, быть может, случая простого копирования мас­сивов. Учтите. что, если в классе, предназначенном для наследования, вы не создади­те, по меньшей мере, правильно работающий защищенный метод clone, реализация интерфейса Cloneable в подклассах станет невозможной.