Философия Java

Управление клонируемостью объектов


У вас возможно сложилось впечатление что для отключения клонируемости достаточно определить метод clone() как private, но это не так, поскольку оперируя методом базового класса вы не можете изменять его статус. Так что это не такая простая задача. И, тем не менее, необходимо уметь управлять клонируемостью своих объектов. Существует ряд типовых реализаций, которыми вы можете руководствоваться при разработке своих классов:

  • Ничего не предпринимайте в связи с клонированием, тогда ваш класс не может быть клонирован, но при необходимости в класс-наследник может быть добавлена возможность клонирования. Такой вариант возможен лишь в том случае, если метод Object.clone() справится с клонированием всех полей вашего класса.
  • Поддержка метода clone(). Реализуйте интерфейс Cloneable и переопределите метод clone(). В переопределенном методе clone() вы должны разместить вызов super.clone() и обработать возможные исключительные ситуации (таким образом, переопределенный метод clone() не будет возвращать исключительных ситуаций).
  • Условная поддержка клонирования. Если ваш класс содержит ссылки на другие объекты и вы не можете быть уверены что все они являются клонируемыми (например, контейнеры), ваш метод clone() может предпринять попытку клонировать все объекты, на которые указывают ссылки, и если это приведет к появлению исключительной ситуации, он просто передает эту исключительную ситуацию далее, для последующей обработки программистом. В качестве примера возьмем особый объект типа ArrayList, который пытается клонировать все свои объекты. Создавая такой ArrayList, вы не знаете точно объекты какого типа программист захочет разместить в вашем ArrayList, а значит не знаете будут они клонируемыми или нет.
  • Метод clone() переопределяется как защищенный (protected) но интерфейс Cloneable не реализуется. Таким образом обеспечивается правильное копирование всех полей класса. Вы должны помнить что для обеспечения правильного копирования ваш метод должен вызывать super.clone(), несмотря на то что этот метод ожидает вызова от объекта, реализующего интерфейс Cloneable (поэтому такой вызов приведет к возникновению исключительной ситуации), поскольку иначе для объектов-наследников вашего класса такой вызов будет невозможен. Метод будет работать только для классов-наследников, которые могут реализовать интерфейс Cloneable.

  • Попытка предотвратить клонирование не реализовав интерфейс Cloneable и переопределив метод clone() для генерации исключительной ситуации. Этот прием работает лишь при условии что все классы-наследники при переопределении метода clone() вызывают super.clone(). В противном случае вашу блокировку можно будет обойти.


  • Защита от клонирования путем описания класса как завершенного (final). Такая защита будет работать лишь в том случае, если метод clone() не был переопределен в каком-либо его родительском классе. В протвном случае потребуется снова переопределить его и сгенерировать исключительную ситуацию CloneNotSupportException. Сделать свой класс завершенным (final) - единственная гарантированная защита от клонирования. Кроме того, при работе с защищенными объектами или в других ситуациях, когда требуется контроль за числом создаваемых объектов, необходимо определить все конструкторы как private и создать один или несколько специальных методов, используемых при создании объектов. Таким образом, эти методы могут ограничить число создаваемых с их помощью объектов и контролировать условия, в которых они создаются. (Одним из примеров таких классов может служить singleton,  рассмотренный в документе Размышления над примерами на Java (Thinking in Patterns with Java), доступном по адресу http://www.bruceeckel.com/).


  • Ниже приведен пример, демонстрирующий различные способы при которых клонирование может быть наследовано или "отключено" в объектах-наследниках:

    //: Приложение А:CheckCloneable.java

    // Проверка, может ли ссылка клонироваться.

    // Не может клонироваться, поскольку не переопредлен

    // метод clone():



    class Ordinary {}

    // Переопределяется clone, но не реализуется

    // интерфейс Cloneable:

    class WrongClone extends Ordinary { public Object clone() throws CloneNotSupportedException { return super.clone(); // Возвращает исключительную ситуацию

    } }

    // Соблюдены все необходимые для клонирования условия:

    class IsCloneable extends Ordinary implements Cloneable { public Object clone() throws CloneNotSupportedException { return super.clone(); } }



    // Клонирование отключено с генерацией исключительного события:

    class NoMore extends IsCloneable { public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } }

    class TryMore extends NoMore { public Object clone() throws CloneNotSupportedException { // Вызов NoMore.clone(), что приводит к появлению исключительного события:

    return super.clone(); } }

    class BackOn extends NoMore { private BackOn duplicate(BackOn b) { // Создается и возвращается копия b.

    // Это простейшее копирование, использованное лишь в качестве примера:

    return new BackOn(); } public Object clone() { // Метод NoMore.clone() не вызывается:

    return duplicate(this); } }

    // Не удается наследовать, а потому и переопределить

    // метод clone как это было сделано в BackOn:

    final class ReallyNoMore extends NoMore {}

    public class CheckCloneable { static Ordinary tryToClone(Ordinary ord) { String id = ord.getClass().getName(); Ordinary x = null; if(ord instanceof Cloneable) { try { System.out.println("Попытка клонирования " + id); x = (Ordinary)((IsCloneable)ord).clone(); System.out.println("Клонирован " + id); } catch(CloneNotSupportedException e) { System.err.println("Не удается клонировать "+id); } } return x; } public static void main(String[] args) { // Подмена типов:

    Ordinary[] ord = { new IsCloneable(), new WrongClone(), new NoMore(), new TryMore(), new BackOn(), new ReallyNoMore(), }; Ordinary x = new Ordinary(); // Это не удастся откомпилировать, пока clone()

    // описан как protected в классе Object:

    //! x = (Ordinary)x.clone();

    // tryToClone() сначала осуществляет проверку чтобы

    // определить, реализует ли данный класс интерфейс Cloneable:

    for(int i = 0; i < ord.length; i++) tryToClone(ord[i]); } } ///:~

    Первый класс, Ordinary, относится к группе классов, рассмотреных в этой книге: он не поддерживает клонирование, но при этом не имеет механизмов ее блокировки. Однако, если вы имеете дело с ссылкой на Ordinary объект, ставший таковым в результате подмены, вы не можете быть уверены в том, сможет ли он быть клонирован или нет.



    Класс WrongClone иллюстрирует неправильную реализацию наследования клонирования. В нем метод Object.clone() переопределяется как public, но не реализован интерфейс Cloneable, поэтому вызов super.clone() (в результате которого вызывается Object.clone()) приводит к возникновению исключительной ситуации CloneNotSupportedException и клонирование не выполняется.

    В классе IsCloneable клонирование реализовано правильно: метод clone() переопределяется и реализуется интерфейс Cloneable. Однако, метод clone(), а также некоторые другие методы в этом примере не перехватывают исключительную ситуацию CloneNotSupportException, а лишь возвращают ее вызвавшему методу, где должна быть предусмотрена обработка в блоке операторов try-catch. Вам скорее всего придется обрабатывать эту ситуацию внутри вашего метода clone() гораздо чаще чем просто передавать ее, но в качестве примера гораздо информативнее было ограничиться лишь передачей.

    В классе NoMore предпринята попытка отключения клонирования способом, рекомендуемым разработчиками Java: в методе clone() класса-наследника генерируется исключительная ситуация CloneNotSupportedException. В методе clone() класса TryMore как и положено вызывается метод super.clone(), который таким образом приводит к вызову метода NoMore.clone(), который генерирует исключительную ситуацию и предотвращает клонирование.

    Но что если программист не станет следовать "правильной" схеме вызова метода super.clone() в переопределенном методе clone()? На примере класса BackOn вы можете наблюдать пример таких действий. Этот класс использует специальный метод duplicate() для копирования текущего объекта и в clone() вызывает этот метод вместо вызова super.clone(). При этом исключительная ситуация не генерируется и класс может быть клонирован. Это пример того, что нельзя рассчитывать на генерацию исключительной ситуации как на защиту класса от клонирования. Единственный верный способ для этого, показан на примере класса ReallyNoMore где класс описан как завершенный (final), то есть как класс, который не может быть наследован. Это означает что если метод clone() генерирует исключительную ситуацию в final классе, то ее не удасться обойти при помощи наследования, что обеспечивает гарантированную защиту от клонирования (вы не можете явно вызвать Object.clone() из класса с произвольным уровнем наследования; можно вызвать лишь метод super.clone(), через который будет произведено обращение к методу базового класса). таким образом, разрабатывая объекты с высоким уровнем защиты, такие классы лучше описывать как final.



    Первый метод класса CheckCloneability - tryToClone(), берет произвольный Ordinary объект и проверяет, является ли он клонируемым с помощью instanceof. Если да, он подменяет тип объекта на IsCloneable и вызывает для него метод clone(), после чего для результатов выполняет обратную подмену в Ordinary, перехватывая все возникающие в ходе операции исключительные ситуации. Обратите внимание на определение типа объекта в процессе выполнения  метода (см. Главу 12) используемое для вывода на экран имени класса для идентификации событий.

    В методе main(), создаются различные типы Ordinary объектов с подменой типа на Ordinary при определении массива. Следующие за этим две строки кода создают простой Ordinary объект и пытаются клонировать его. Однако этот код не удастся откомпилировать, поскольку в классе Object метод clone() определен как защищенный (protected). Остальной код пробегает по всему массиву и пытается клонировать каждый из его объектов, информируя при этом об успешности этих операций. Вы получите следующие результаты:

    Попытка клонирования IsCloneable Клонирован IsCloneable Попытка клонирования NoMore Не удается клонировать NoMore Попытка клонирования TryMore Не удается клонировать TryMore Попытка клонирования BackOn Клонирован BackOn Попытка клонирования ReallyNoMore Не удается клонировать ReallyNoMore

    В заключение, сформулируем требования, предъявляемые к клонируемым классам:

    1. Реализация интерфейса Cloneable.

    2. Переопределение метода clone()

    3. Вызов метода super.clone() из переопределенного метода clone()

    4. Обработка исключительных ситуаций в методе clone()


    Содержание раздела