Источник: InfoWorld
Благодаря этому учебному руководству вы узнаете, как избежать распространенных ошибок при копировании объектов в Java, и поймете разницу между поверхностным и глубоким копированием.
Копирование объектов — обычная операция в корпоративных проектах. При копировании объекта мы должны убедиться, что в итоге мы получили новый экземпляр, содержащий нужные нам значения.
Ссылки на объекты
Чтобы правильно выполнить поверхностное или глубокое копирование объекта, для начала мы должны понимать, чего не следует делать. Во-первых, при копировании объекта важно избегать использования одной и той же ссылки на объект. Как показывает пример ниже, это довольно простая, но распространенная ошибка. Перед вами объект Product, который мы будем использовать в наших примерах:
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public double getPrice() { return price; }
public void setName(String name) { this.name = name; }
public void setPrice(double price) { this.price = price; }
}
Теперь давайте создадим и присвоим ссылку на объект Product другой переменной. Это может показаться копией, но на самом деле это один и тот же объект:
public static void main(String[] args) {
Product product = new Product("Macbook Pro", 3000);
Product copyOfProduct = product;
product.name = "Alienware";
System.out.println(product.name);
System.out.println(copyOfProduct.name);
}
Вывод кода:
Alienware
Alienware
Обратите внимание, что в приведенном выше коде мы присваиваем значение объекта другой локальной переменной, но эта переменная указывает на ту же ссылку на объект. Если мы изменим объекты product или copyOfProduct, результатом будет изменение исходного объекта Product.
Это происходит потому, что каждый раз, когда мы создаем объект в Java, ссылка на объект создается в куче памяти Java. Это позволяет нам изменять объекты, используя их ссылочные переменные.
Поверхностное копирование
Техника поверхностного копирования (Shallow copy) позволяет нам копировать значения простого объекта в новый объект, не включая внутренние значения объекта. В качестве примера рассмотрим, как использовать технику поверхностного копирования для копирования объекта Product без использования ссылки на объект:
// Пропускаем объект Product
public class ShallowCopyPassingValues {
public static void main(String[] args) {
Product product = new Product("Macbook Pro", 3000);
Product copyOfProduct = new Product(product.getName(), product.getPrice());
product.setName("Alienware");
System.out.println(product.getName());
System.out.println(copyOfProduct.getName());
}
}
Результат:
Alienware
Macbook Pro
Учтите, что в этом коде, когда мы передаем значения из одного объекта в другой, в куче памяти создаются два разных объекта. Когда мы изменяем одно из значений в новом объекте, значения в исходном объекте останутся прежними. Это доказывает, что объекты разные, и что мы успешно выполнили поверхностное копирование.
Поверхностное копирование с Cloneable
Начиная с Java 7, у нас появился интерфейс Cloneable. Этот интерфейс предоставляет еще один способ копирования объектов. Вместо реализации логики копирования вручную, как мы только что сделали, мы можем реализовать интерфейс Cloneable, а затем реализовать метод clone(). Использование Cloneable и метода clone() автоматически приводит к созданию поверхностной копии. Мне не нравится этот метод, потому что он выдает проверенное исключение, и нам приходится вручную приводить тип класса, что делает код многословным. Но использование Cloneable может упростить код, если у нас есть огромный объект предметной области со множеством атрибутов. Вот что произойдет, если мы реализуем интерфейс Cloneable в объекте домена, а затем переопределим метод clone():
public class Product implements Cloneable {
// Пропускаем атрибуты, методы и конструктор
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Теперь снова метод копирования в действии:
public class ShallowCopyWithCopyMethod {
public static void main(String[] args) throws CloneNotSupportedException {
Product product = new Product("Macbook Pro", 3000);
Product copyOfProduct = (Product) product.clone();
product.setName("Alienware");
System.out.println(product.getName());
System.out.println(copyOfProduct.getName());
}
}
Как видите, метод копирования отлично подходит для создания поверхностной копии объекта. Его использование означает, что нам не нужно копировать каждый атрибут вручную.
Глубокое копирование
Техника глубокого копирования (Deep copy) — это возможность копировать значения составного объекта в другой новый объект. Например, если объект Product содержит объект Category, то ожидается, что все значения из обоих объектов будут скопированы в новый объект. Что произойдет, если у объекта Product есть составной объект? Сработает ли техника поверхностного копирования? Давайте посмотрим, что будет, если мы попытаемся использовать только метод copy(). Для начала создадим класс Product с объектом Order:
public class Product implements Cloneable {
// Пропущенные другие атрибуты, конструктор, геттеры и сеттеры.
private Category category;
public Category getCategory() { return category; }
}
Теперь давайте сделаем то же самое, используя метод super.clone():
public class TryDeepCopyWithClone {
public static void main(String[] args) throws CloneNotSupportedException {
Category category = new Category("Laptop", "Portable computers");
Product product = new Product("Macbook Pro", 3000, category);
Product copyOfProduct = (Product) product.clone();
Category copiedCategory = copyOfProduct.getCategory();
System.out.println(copiedCategory.getName());
}
}
Результат:
Laptop
Обратите внимание: несмотря на то, что результатом является Laptop, операция глубокого копирования не произошла. Вместо этого у нас возникла одна и та же ссылка Category на объект. Вот доказательство:
public class TryDeepCopyWithClone {
public static void main(String[] args) throws CloneNotSupportedException {
// Тот же код, что и в примере выше
copiedCategory.setName("Phone");
System.out.println(copiedCategory.getName());
System.out.println(category.getName());
}
}
Вывод:
Laptop
Phone
Phone
Учтите, что в этом коде копия не была создана, когда мы изменили объект Category. Вместо этого было только присвоение объекта другой переменной. Поэтому мы будем изменять объект, созданный в куче памяти, всякий раз, когда меняем ссылочную переменную.
Глубокое копирование с помощью метода clone()
Теперь мы знаем, что метод clone() не будет работать для глубокой копии, если у нас есть простое переопределение. Давайте посмотрим, как мы можем заставить его заработать. Сначала мы реализуем Cloneable в классе Category:
public class Category implements Cloneable {
// Пропущенные атрибуты, конструктор, геттеры и сеттеры
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Теперь нам нужно изменить реализацию метода Productclone, чтобы клонировать объект Category:
public class ProductWithDeepCopy implements Cloneable {
// Пропущенные атрибуты, конструктор, геттеры и сеттеры
@Override
protected Object clone() throws CloneNotSupportedException {
this.category = (Category) category.clone();
return super.clone();
}
}
Если мы попытаемся выполнить глубокое копирование с тем же примером кода, что и выше, мы получим реальную копию значений объекта в новый объект, как показано здесь:
public class TryDeepCopyWithClone {
public static void main(String[] args) throws CloneNotSupportedException {
Category category = new Category("Laptop", "Portable computers");
Product product = new Product("Macbook Pro", 3000, category);
Product copyOfProduct = (Product) product.clone();
Category copiedCategory = copyOfProduct.getCategory();
System.out.println(copiedCategory.getName());
copiedCategory.setName("Phone");
System.out.println(copiedCategory.getName());
System.out.println(category.getName());
}
}
Вывод:
Laptop
Phone
Laptop
Поскольку мы вручную скопировали метод категории в метод copy() объекта Product, он наконец-то работает. Мы получим копии Product и Category используя метод copy() от Product.
Этот код доказывает, что глубокая копия сработала. Значения исходного и скопированного объектов различны. Следовательно, это не тот же самый экземпляр; это скопированный объект.
Поверхностное копирование с сериализацией
Иногда бывает необходимо сериализовать объект, чтобы преобразовать его в байты и передать через сеть. Эта операция может быть опасной, поскольку в случае неправильной проверки сериализованный объект может быть использован. Безопасность сериализации Java выходит за рамки этой статьи, но давайте посмотрим, как она работает с кодом. Мы будем использовать тот же класс из примера выше, но на этот раз реализуем интерфейс Serializable:
public class Product implements Serializable, Cloneable {
// Пропущенные атрибуты, конструктор, геттеры, сеттеры и метод clone
}
Обратите внимание, что сериализоваться будет только объект Product, поскольку только Product имплементирует Serializable. Объект Category не будет сериализован. Вот пример:
public class ShallowCopySerializable {
public static void main(String[] args) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
Product product = new Product("Macbook Pro", 3000);
out.writeObject(product);
out.flush();
out.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream in = new ObjectInputStream(bis);
Product clonedProduct = (Product) in.readObject();
in.close();
System.out.println(clonedProduct.getName());
Category clonedCategory = clonedProduct.getCategory();
System.out.println(clonedCategory);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Вывод:
Macbook Pro
null
Теперь, если бы мы попытались поместить объект Category в объект Product, то будет выброшено исключение java.io.NotSerializableException. Это потому, что объект Category не реализует Serializable.
Глубокое копирование с сериализацией
Теперь давайте посмотрим, что произойдет, если мы используем тот же код, что и выше, но добавим в класс Category следующее:
public class Category implements Serializable, Cloneable {
// Пропущенные атрибуты, конструктор, геттеры, сеттеры и метод clone
// Добавление toString для хорошего описания объекта
@Override
public String toString() {
return "Category{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}';
}
}
Запустив тот же код, что и код поверхностно сериализуемой копии, мы получим результат от Category с таким выводом:
Macbook Pro
Category{name='Laptop', description='Portable computers'}
Заключение
Иногда техника поверхностного копирования — это все, что вам нужно для поверхностного клонирования объекта. Но если вы хотите скопировать как объект, так и его внутренние объекты, вам необходимо реализовать глубокое копирование вручную. Вот ключевые выводы из этих важных техник.Что следует помнить о поверхностном копировании:
- Поверхностное копирование создает новый объект, но использует общие ссылки на внутренние объекты с исходным объектом.
- Скопированные и исходные объекты относятся к одним и тем же объектам в памяти.
- Изменения, внесенные во внутренние объекты посредством одной ссылки, будут отражены как в скопированных, так и в исходных объектах.
- Мелкое копирование — простой и эффективный процесс.
- Java предоставляет реализацию мелкого копирования по умолчанию через методclone().
Что следует помнить о глубоком копировании:
- Глубокое копирование создает новый объект, а также новые копии его внутренних объектов.
- Скопированные и исходные объекты имеют независимые копии внутренних объектов.
- Изменения, внесенные во внутренние объекты по одной ссылке, не повлияют на другую.
- Глубокое копирование — более сложный процесс, особенно при работе с графами объектов или вложенными ссылками.
- Глубокое копирование должно быть реализовано явно, либо вручную, либо с использованием библиотек или фреймворков.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ