КМБ. Некоторые особенности клонирования объектов в Java

Эта статья рассчитана на новичков или людей, которые слышали о данной технологии, но никогда не имели с ней дело. В русскоязычном интернете, по моему мнению, этот вопрос раскрыт недостаточно хорошо, поэтому задачей этой статьи является максимально осветить вопросы клонирования и методы их реализации. В данной статье будут рассмотрены поверхностное и глубокое клонирование сложных объектов.
Начнем с примера. Предположим, у нас есть некий класс, который хранит в себе переменную типа Integer:

public class SomeProperty
{
private Integer a = null;
public SomeProperty(int value)
{
setA(new Integer(value));
}
public void setA(Integer a)
{
this.a = a;
}
public Integer getA()
{
return a;
}
}

Поскольку в Java все сложные объекты передаются в методы по ссылке, а при присваивании объектов мы присваиваем их указатели, то после строк:

SomeProperty a = new SomeProperty (1);
SomeProperty b = new SomeProperty (2);
b = a;
и а, и b будут указывать на a. Соответственно, при изменении внутреннего состоянии объекта а, поменяется и внутреннее состояние объекта b.

image

Проблемы, возникающие присваивания ссылок, а не значений происходит и в случае:

SomeProperty prop = new SomeProperty(2);
List a = new ArrayList();
a.add(prop);
List b = new ArrayList(a);
b.get(0).setA(3);
При этом и a, и b будут иметь одно и то же значение.
Отдельно следует рассмотреть случай с List.
Integer c = 2;
List a = new ArrayList();
a.add©;
List b = new ArrayList(a);
c = b.get(0);
c = 3;
в обоих списках лежат ссылки на один и тот же объект с, однако после строки c = 3, значение в списках не меняются, т.к. Integer – лишь декоратор над простым типом int, и при присваивании мы создаем новый объект:

image

Итак, если нам нужно произвести какие-либо действия с внутренним состоянием сложного объекта, при этом сохранив его начальную копию для дальнейших манипуляций, прибегают к клонированию объектов.
Как результат – мы будем иметь два объекта с идентичным состоянием, но при изменении состояния одного из них, второй останется неизменным.

Небольшие выжимки из теории:

Чтобы объект можно было клонировать, он должен реализовывать интерфейс Сloneable. Использование интерфейса влияет на поведение метода (clone) родительского класса (Object). Таким образом, вызов метода SomeProperty.clone() создаст новый объект SomeProperty.

image

Поверхностное клонирование:

image

Глубокое клонирование:

Глубокое клонирование подразумевает клонирование и ссылочных, и обыкновенных полей (клонировать объекты, на которые они ссылаются) объекта.

Для использования механизма клонирования возможны 2 варианта: переопределение метода clone(), не переопределять метод clone() — в этом случае клонированием будет заниматься JVM. При вызове этого метода копирование выполняется на уровне виртуальной машины без вызова конструктора нового объекта. Значения всех полей копируются.
Самое важное в процессе клонирования – запомнить, что если у вас сложный глобальный объект, то все те внутренние объекты, которые он инкапсулирует, будут присваиваться по ссылке, не клонироваться! То есть по умолчанию в Java реализуется поверхностное клонирование объектов.

Рассмотрим пример:

public class SomeProperty implements Cloneable
{
private int a;
private SomeData someData = null;

public Object clone()
{
Object result = null;
try
{
result = super.clone();
} catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
return result;
}
public SomeProperty(final int value, final SomeData prop)
{
setA(value);
setSomeData(prop);
}
public void setA(int a)
{
this.a = a;
}
public int getA()
{
return a;
}
public void setSomeData(SomeData someData)
{
this.someData = someData;
}
public SomeData getSomeData()
{
return someData;
}

}

public class SomeData implements Cloneable
{
public Object clone()
{
Object result = null;
try
{
result = super.clone();
} catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
return result;
}
private int someInt;

public SomeData(final int data)
{
someInt = data;
}
public void setSomeInt(int someInt)
{
this.someInt = someInt;
}

public int getSomeInt()
{
return someInt;
}
}
После строк:

SomeData innerData = new SomeData(5);
SomeProperty firstProperty = new SomeProperty(2, innerData);
SomeProperty secondProperty = (SomeProperty) firstProperty.clone();

Имеем:

image

Если бы мы вызвали innerData.setSomeInt(10), то значение поменялось бы и в классе firstProperty, и в классе secondProperty (аналогично примерам, описанным выше для ссылочных полей).

Если же мы перепишем метод SomeProperty.clone() и вручную создадим объект класса SomeData, данные и внутренне состояние будут несвязанны:

public Object clone()
{
Object result = null;
try
{
result = super.clone();
((SomeProperty)result).setSomeData((SomeData) someData.clone());
} catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
return result;
}
image

Все хорошо, все работает, но ровно до тех пор, пока мы не захотим реализовать какую-либо рекуррентную структуру: композит, дерево, список.

Рассмотрим пример:

public class SomeProperty implements Cloneable
{
private int a;
private SomeProperty someProperty = null;

public Object clone()
{
Object result = null;
try
{
result = super.clone();
if (someProperty != null)
{
((SomeProperty)result).setSomeProperty((SomeProperty) someProperty.clone());
}
} catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
return result;
}
public SomeProperty(final int value, final SomeProperty prop)
{
setA(value);
setSomeProperty(prop);
}
public void setA(int a)
{
this.a = a;
}
public int getA()
{
return a;
}
public void setSomeProperty(SomeProperty someProperty)
{
this.someProperty = someProperty;
}
public SomeProperty getSomeProperty()
{
return someProperty;
}

}

В данном классе строчка ((SomeProperty)result).setSomeProperty((SomeProperty) someProperty.clone());
будет вызывать переполнение буфера виртуальной машины, так как если мы попытаемся создать двусвязный список:

SomeProperty innerProperty = new SomeProperty(5, null);
SomeProperty firstProperty = new SomeProperty(2, null);
innerProperty.setSomeProperty(firstProperty);
firstProperty.setSomeProperty(innerProperty);
SomeProperty secondProperty = (SomeProperty) firstProperty.clone();

Мы не сможем корректно клонировать объекты из-за их двусторонней связности.
В этом случае глубокое клонирование реализовывается через сериализацию. При сериализации связи между объектами «разрываются», т.к. вложенные объекты сохраняются по значению, а не ссылке:
public Object clone() throws {
SomeProperty obj = (SomeProperty) super.clone();
Object object = null;
try {
object = getDeepCloning(obj);
} catch (Exception e) {

e.printStackTrace();
}
return object;
}

public Object getDeepCloning(Object obj) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}

image
Надеюсь, новичкам эта статья окажется полезной.


0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.