Клонирование объектов в Java: clone(), Cloneable и глубокое копирование - Вопросы

Всего: 5 вопросов

1. 

Что нужно сделать, чтобы объекты класса можно было клонировать методом clone(), и зачем нужен интерфейс Cloneable?

Метод clone() объявлен в классе Object как protected и по умолчанию делает поверхностную копию. Чтобы клонирование работало, класс должен:

  • реализовать интерфейс Cloneable;
  • переопределить clone() как public и вызвать в нём super.clone().

Cloneable — интерфейс-маркер: в нём нет ни методов, ни констант, он лишь разрешает Object.clone() копировать экземпляры класса. Если класс не реализует Cloneable, super.clone() во время выполнения выбрасывает CloneNotSupportedException. Если clone() не переопределён как public, вызов car.clone() из другого класса не компилируется: clone() has protected access in java.lang.Object. Начиная с Java 5 переопределённый метод может возвращать сам класс (public Car clone()) — ковариантный тип возвращаемого значения избавляет от приведения типа.

2. 

Чем поверхностное копирование (shallow copy) отличается от глубокого (deep copy)?

При поверхностном копировании копируются только значения полей: клон — новый объект (clone != original), но его поля-ссылки указывают на те же вложенные объекты. Изменение вложенного объекта через клон видно и в оригинале: после clonedCar.getDriver().setName("Вася") водитель меняется у обоих автомобилей. Так по умолчанию работают Object.clone(), clone() массива объектов, ArrayList.clone() и new ArrayList<>(list).

При глубоком копировании вложенные изменяемые объекты тоже копируются, поэтому клон и оригинал ничего не делят. Общие ссылки на неизменяемые объекты (String, Integer, LocalDate) безопасны, их копировать не нужно; копировать нужно изменяемые поля — Date, массивы, коллекции, объекты своих классов.

3. 

Как реализовать глубокое копирование через метод clone()?

Сначала вызвать super.clone(), затем вручную скопировать каждое изменяемое поле-ссылку и присвоить копию полю клона:

@Override
public Car clone() {
    try {
        Car copy = (Car) super.clone();
        if (driver != null) {
            copy.driver = driver.clone();
        }
        return copy;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(e);
    }
}

Метод можно объявить без throws CloneNotSupportedException: класс реализует Cloneable, исключение невозможно, и перекладывать проверяемое исключение на вызывающий код незачем (так советует Джошуа Блох в «Effective Java»). Если вложенный объект сам содержит изменяемые объекты, их копируют по той же схеме на каждом уровне. Копию нельзя создавать через new: тогда наследники получат объект не того типа. И такой приём не работает для final-полей — им нельзя присвоить копию.

4. 

Что такое конструктор копирования и фабричный метод копирования, и чем они лучше clone()?

Конструктор копирования принимает объект того же класса и копирует его поля: public Car(Car other). Фабричный метод копирования — тот же приём в виде статического метода с говорящим именем, например public static Car copyOf(Car other); он удобен, когда копию нужно вернуть как интерфейс или подтип.

Преимущества перед clone():

  • не нужен Cloneable и нет проверяемых исключений;
  • работает с final-полями;
  • это обычный конструктор: проверки и инициализация выполняются, тогда как Object.clone() конструкторы обходит;
  • явно видно, какие поля копируются глубоко.

Сам по себе конструктор копирования, как и clone(), копирует ссылки. Для глубокой копии вложенные объекты копируют явно: new Driver(other.driver), а список — поэлементно: cars.stream().map(Car::new).collect(Collectors.toCollection(ArrayList::new)). Для собственных классов это выбор по умолчанию; clone() Блох советует оставить для массивов.

5. 

Как получить глубокую копию через сериализацию и какой способ клонирования выбрать в каком случае?

Объект записывают в массив байт через ObjectOutputStream поверх ByteArrayOutputStream и сразу читают обратно через ObjectInputStream поверх ByteArrayInputStream. Получается глубокая копия всего графа объектов без ручного кода для каждого поля. Условие — все классы графа реализуют Serializable, иначе будет NotSerializableException; поля transient в копии получают значения по умолчанию (null, 0, false). Способ заметно медленнее остальных.

Что выбирать:

  • конструктор копирования — для собственных классов по умолчанию: явный, работает с final-полями, без исключений;
  • фабричный метод (copyOf) — когда нужно вернуть интерфейс/подтип или нужно говорящее имя;
  • clone() — для массивов и наследников классов, уже поддерживающих clone();
  • сериализация — для редкой глубокой копии сложного графа, не в часто выполняемом коде.
Страница 1 из 1