Skip to content

Latest commit

 

History

History
198 lines (138 loc) · 11.4 KB

clone.md

File metadata and controls

198 lines (138 loc) · 11.4 KB

java.lang.Object#clone

Введение

Название метода подскажет его назначение. Данный метод задумывался разработчиками как простой и понятный способ создать копию объекта, его клон - отсюда и название.

Объявление метода выглядит так:

protected native Object clone() throws CloneNotSupportedException;

Ключевое слово native означает, что метод реализован в платформенно-зависимом коде, чаще всего на C/C++, и скомпонован в виде динамической библиотеки.

Эта реализация зависит от JVM.

Возможно, вас сейчас это напугало, но на самом деле достаточно просто понимать, что native означает лишь то, что вызываемый код, реализован не на Java.

Клонировать можно только те объекты, которые реализуют интерфейс java.lang.Cloneable. Данный интерфейс является интерфейсом-марекром, как и java.io.Serializable.

Если объект не реализует интерфейс-маркер java.lang.Cloneable, то будет сгенерировано исключение java.lang.CloneNotSupportedException.

Это нетепичный пример использования интерфейсов, когда метод объявлен у одного класса, а право пользования этим методом подмешивается отдельным интерфейсом. Я бы не рекомендовал подражать такому подходу.

Подробнее про интерфейсы

Исходя из JavaDoc документации вызов метода clone создает копию объекта без вызова конструктора. Что далеко не всегда удобно. По умолчанию clone определяет поверхностное копирование, т.е копируются значения всех полей и ссылок.

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

public class CloneTest implements Cloneable {
    private final int i;

    public CloneTest(int i) {
        this.i = i;
    }

    public int getI() {
        return i;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest test = new CloneTest(2);
        CloneTest cloneTest = (CloneTest) test.clone();
        System.out.println("Original : " + test + ", i = " + test.getI());
        System.out.println("Clone : " + cloneTest + ", i = " + cloneTest.getI());
    }
}

Чтобы не делать постоянное приведение к нужному типу, можно переопределить наш метод и явно указать тип возвращаемого объекта. Это считается хорошим тоном и застрахует нас от ошибок.

Подводные камни

Рассмотрим чуть более сложный пример, содержащий ссылочный тип, и переопределим метод так, как мы говорили выше - укажем явно возвращаемый объект:

public class PersonToClone implements Cloneable {
    private String name;
    private int age;
    private Hobby hobby;

    public PersonToClone(String name, int age, Hobby hobby) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }

    @Override
    public String toString() {
        return "PersonToClone{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", hobby=" + hobby +
                '}';
    }

    @Override
    protected PersonToClone clone() throws CloneNotSupportedException {
        return (PersonToClone) super.clone();
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setHobby(Hobby hobby) {
        this.hobby = hobby;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Hobby getHobby() {
        return hobby;
    }
}

На первый взгляд может показаться, что все очень удобно и практично, однако здесь есть несколько подводных камней.

Во-первых, вспомним, что по умолчанию копируются значения всех полей и ссылок, а значит при клонировании будет скопирована именно ссылка. Т.е и клон, и первоначальный объект будут ссылаться на один и тот же объект Hobby.

Что подводит нас к подводному камню, можно скзаать айсбергу, потопившему титаник: если первоначальный объект изменит Hobby, то эти изменения окажутся и у клона! В свою очередь, это работает и в обратную сторону - если клон вдруг изменит Hobby, то и у первоначального объекта оно изменится.

Чувствуете пропасть под ногами? Холодок уже пробежал?

Что делать

Избежать подобного поведения можно двумя способами:

  • Делать для таких 'опасных' объектов, как в примере выше, копии вручную и присваивать их через setter-ы.
  • Конструктор копирования или специальные статические методы.

Первый способ

Сначала вызываем clone у супер-класса, а после уже явно вручную работаем с 'опасными' полями. Вызов clone у супер-класса даст нам копию объекта, но поля, которые содержат изменяемые объекты надо вручную обработать - создать копии.

Рассмотрим как это будет выглядеть в нашем случае:

@Override
protected PersonToCloneBetter clone() throws CloneNotSupportedException {
    PersonToCloneBetter pClone = (PersonToCloneBetter) super.clone();
    pClone.setHobby(new Hobby(hobby.getName()));

    return pClone;
}

Т.е мы явно создаем новый объект Hobby, который уже и присваиваем через setter у клона. Таким образом, проблема с изменением объекта Hobby решена, это два разных объекта с одним состоянием.

Единственное 'но' - такой способ не сработает, если поле hobby объявлено у класса как final, т.е является неизменяемым. Либо же у класса нет setter метода на это поле.

Вдобавок к этому, такой способ может породить трудно уловимые ошибки, ведь надо быть очень внимательным, так как можно просто забыть присвоить копию у клона.

Гораздо удобнее, на мой взгляд, использовать конструктор копирования.

Конструктор копирования

Вы спросите: "Что еще за конструктор копирования?"

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

Для примера рассмотрим следующий код:

public static PersonToClone newInstance(PersonToClone personToClone) {
    return new PersonToClone(
      personToClone.getName(),
      personToClone.getAge(),
      new Hobby(personToClone.hobby.getName())
    );
}

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

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

Этот вариант является более предпочтительным, на мой взгляд.

Плюсы:

  • Проще реализуется.
  • Не работаем с исключениями клонирования, как например, java.lang.CloneNotSupportedException.
  • Поддерживает работу с final-полями.
  • Код получается более явным, мы просто создаем новый объект с помощью конструктора или специального метода.

Заключение

Метод clone задумывался разработчиками как простой и понятный способ создать копию объекта. Помните, что классы, реализующие java.lang.Cloneable должны переопределять clone() и делать его открытым. Также старайтесь придерживаться правила, когда другие интрефейсы не расширяют java.lang.Cloneable. Реализуйте этот интерфейс явно только тогда, когда хотите использовать метод clone.

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

В целом, чаще гораздо более удобно копию объекта получать через конструктор копирования или специального метода, в котором явно создается новый объект и присваиваются значения из копируемого.

Чаще всего этот метод стараются не использовать.