Основным методом создания объектов в Java безусловно является конструктор, но есть и другие, например десериализация и клонирование. При клонировании, также как и при десериализации, объект создается без использования конструктора. Собственно в этой серии речь пойдет о клонировании объектов и о методе Object.clone.
protected Object clone() throws CloneNotSupportedException
Но несмотря на то, что метод clone определен в базовом классе Object, это еще не значит, что любой объект может быть клонирован. Для того, что бы можно было клонировать объект, необходимо иметь на это "разрешение" (об этом чуть ниже).
Кстати, хочу отметить, что и Cloneable и Serializable, являются реализацией основополагающего паттерна Marker Interface.
Обратимся к документации метода:
/**
* Создает и возвращает копию данного объекта. Точное значение термина
* "копия", может зависеть от класса объекта. Общий смысл заключаются в
* том, что для любого объекта x, выражение:
*
* x.clone() != x
*
* было истинным и выражение:
*
* x.clone().getClass() == x.getClass()
*
* тоже было истинным, но эти требования не являются безусловными. Как
* правило, условие заключается в том, чтобы выражение:
*
* x.clone().equals(x)
*
* было истинным. Но и это требование не является безусловным.
*
* В соответствии с соглашением, возвращаемый объект должен быть получен
* по средствам вызова super.clone. Если класс и все его суперклассы
* (за исключением Object) выполнили это условие, то можно утверждать,
* что x.clone().getClass() == x.getClass() будет истинным.
*
* В соответствии с соглашением, объект, возвращенный этим методом,
* должен быть независимым от этого объекта (который клонируется).
* Для достижения этой независимости, может понадобиться изменить одно
* или несколько полей объекта, полученного с помощью super.clone, до
* его возвращения. Как правило, это применяется когда объект имеет
* "вложенную структуру" и агрегирует в себе изменяемые объекты, при
* клонировании меняют ссылки на копии объектов. Если класс содержит в
* себе только примитивные типы и ссылки на неизменяемые объекты, то
* обычно никаких изменений в объекте, возвращенным super.clone, не
* требуется.
*
* Метод clone в классе Object выполняет определенную операцию
* клонирования. Если класс не реализует интерфейс Cloneable,
* то будет брошено исключение CloneNotSupportedException.
* Отметим, что все массивы реализуют интерфейс Cloneable. Если класс
* реализует Cloneable, то будет создан новый экземпляр того же класса
* и все его поля будут проинициализированы теми же значениями. Таким
* образом, этот метод выполняет "точное копирование" объекта, не
* "глубокое копирование".
*
* Класс Object не реализует интерфейс Cloneable и поэтому, вызов метода
* clone у объекта класса Object приведет к генерации исключения.
*/
Так как интерфейс Cloneable тесно связан с методом clone, хочу сразу привести документацию на данный интерфейс:
/**
* Класс, реализующий интерфейс Cloneable, сообщает методу Object.clone()
* о том, что объекты данного класса могут быть клонированы.
*
* Вызов метода clone у объекта класса Object приведет к генерации
* исключения CloneNotSupportedException.
*
* В соответствии с соглашением, классы реализующие данный интерфейс
* должны переопределить метод Object.clone (который является protected)
* с модификатором public. Подробнее о переопределения Object.clone
* смотрите в документации к методу.
*
* Обратите внимание, что этот интерфейс не содержит метода clone. Это
* означает, что сам факт реализации данного интерфейса еще не говорит о
* том, что экземпляры класса могут быть клонированы.
*/
В документации все довольно просто и понятно изложено, на основе ее содержания можно сделать краткое заключение (оно же, то самое "разрешение", о котором говорилось в начале):
Для того что бы экземпляр класса можно было клонировать, необходимо что бы класс реализовывал интерфейс Cloneable и имел доступный метод clone. При этом, нет никаких строгих ограничений на реализацию метода clone.
При переопределении метода clone, необходимо понимать что происходит в Object.clone. Давайте посмотрим... Если имеется соответствующее "разрешение" на клонирование объекта, то в методе будут выполнены следующие действия:
- определяется размер исходного объекта,
- выделяется такой же объем памяти,
- копируются (побитовое копирование) данные исходного объекта в выделенную область.
Только метод clone способен определить необходимый объем памяти и выполнить побитовое копирование исходного объекта. Таким образом, при переопределении метода clone, первым делом необходимо вызвать реализацию суперкласса. Если все суперклассы придерживаются этого соглашения, то это неизбежно приведет к вызову метода базового класса Object. Таким образом, будет получен объект точно такого же типа.
Приведу пример реализации клонирования:
class Animal implements Cloneable {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Стоит обратить внимание на то, что исключение CloneNotSupportedException является обрабатываемым исключением, а это значит его обязательно придется обработать. Но в своей практике я встречал вот такую реализацию метода clone:
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
assert false : "Этого не может быть!";
return null;
}
}
Данная реализация избавляет от необходимости следить за исключениями CloneNotSupportedException. Создатель класса подразумевает что исключительная ситуация никогда не наступит. Однако, у такой реализации есть один недостаток: если я решу создать наследника от класса с подобной реализацией метода clone и захочу запретить клонирование экземпляров моего класса, то мне не удастся этого сделать. Я не смогу выкинуть исключение CloneNotSupportedException в теле переопределенного мной метода, т.е. донести до пользователя класса, что клонирование не разрешено. Но есть способ обойти это ограничение, например, я могу выкинуть не обрабатываемое исключение UnsupportedOperationException, которое специально предназначено для того, что бы запретить использовать унаследованный метод.
Теперь перейдем к глубокому копированию. Как уже было описано выше, вызов метода Object.clone приводит к созданию точной копии объекта. Следующая схема наглядно демонстрирует создание точной (поверхностной) копии.
Объект машина2 является точной копией объекта машина1. Но такое копирование может не удовлетворять необходимым условиям бизнес-логики. Из представленной схемы видно, что после клонирования обе машины имеют один и тот же пробег, одну и туже марку, одного и того же владельца, один и тот же мотор. Очевидно, что последний факт не имеет ничего общего со здравым смыслом - две машины не могут иметь один и тот же мотор. В этом случае логику копирования придется "допиливать" вручную.
class Car implements Cloneable {
private int mileage;
private String bradn;
private Owner owner;
private Engine engine;
@Override
public Object clone() throws CloneNotSupportedException{
Car copy = (Car) super.clone();
copy.engine = (Engine) this.engine.clone();
return copy;
}
}
class Owner {
}
class Engine implements Cloneable {
@Override
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
Такая реализация Car.clone уже поддается здравой логике - у каждой машины свой мотор :).
В данном примере все просто и понятно, но на практике, реализация глубокого копирования оказывается не такой уж и простой задачей. Большая часть типов в Java Core не разрешают клонирование, а это значит, что при реализации клонирования Вы можете наткнуться на то, что агрегируемый объект не может быть клонирован. Бывают такие ситуации, когда приходится возиться с полями примитивного типа. Например, если класс содержит уникальный идентификатор, то клону придется присвоить новый, отличный от своей копии, идентификатор. Также можно наткнуться на поле с модификатором final, изменить его значения после клонирования объекта будет невозможно. Подводных камней, при реализации глубокого копирования объекта, очень много - так же как и подходов для их обхода. Но я не буду заострять на них внимание. Мне было важно донести до Вас, что при реализации копирования, разработчик должен сам позаботится о логике копирования и предусмотреть все варианты использования его класса. Но в любом случае, хорошо если Вы задокументируете логику копирования. Это поможет другим разработчикам лучше понять, как именно нужно обращаться с экземплярами Вашего класса.
Подведем итог. Клонирование - очень трудно контролируемый механизм и прежде чем его задействовать, нужно хорошо продумать реализацию. Возможно, можно обойтись и без него, например, задействовав конструктор копий (Car(Car c){...}). Но если Вы все же решили его использовать, будьте очень осторожны!
И в завершении, хочу добавить: клонировать объект можно с помощью сериализации, данный подход имеет свои недостатки, но тем не менее, имеет право на существование. Библиотека Apache Commons Lang содержит реализацию этого подхода: org.apache.commons.lang.SerializationUtils.clone(Serializable object)
На этом все. Продолжение следует, в следующей серии об equals и hashCode...
Спасибо большое за статью. случайно наткнулась, но прочла как раз то, что меня интересовало! К тому же, даже не знала о таком количестве подводных камней, которые ведет за собой клонирование.
ОтветитьУдалитьСпасибо еще раз! :)