W3docs

Клонирование объектов в Java

Копирование объектов в Java с помощью clone(), интерфейса Cloneable и разница между поверхностным и глубоким копированием.

Клонирование объекта означает создание нового объекта с таким же состоянием — копии, которую можно изменять независимо от оригинала. Встроенный механизм Java — это Object.clone() в сочетании с маркерным интерфейсом Cloneable, однако этот подход имеет достаточно шероховатостей, поэтому большинство современного кода предпочитает конструктор копирования или фабричный метод. В этой главе рассмотрены оба подхода и ловушка между ними.

Встроенный способ: Object.clone()

Object.clone() является protected и выполняет поверхностное копирование полей экземпляра. Чтобы воспользоваться им, необходимо:

  1. Реализовать в классе интерфейс Cloneable — маркерный интерфейс без методов. Без него clone() выбрасывает CloneNotSupportedException.
  2. Переопределить clone(), сделав его public и (как правило) сузив возвращаемый тип до конкретного класса.
public class Box implements Cloneable {
  int size;

  @Override
  public Box clone() {
    try {
      return (Box) super.clone();
    } catch (CloneNotSupportedException e) {
      throw new AssertionError(e);   // can't happen — we implement Cloneable
    }
  }
}

super.clone() создаёт фактическую копию. Блок try/catch — это формальность: проверяемое исключение объявлено в Object.clone, но поскольку наш класс реализует Cloneable, исключение недостижимо.

Поверхностное копирование: что происходит на самом деле

Поверхностная копия дублирует непосредственные поля. Ссылки внутри объекта копируются как ссылки, а не как новые объекты — поэтому оригинал и клон разделяют всё то, на что эти ссылки указывают:

public class Person implements Cloneable {
  String name;
  int[]  scores;

  @Override
  public Person clone() {
    try { return (Person) super.clone(); }
    catch (CloneNotSupportedException e) { throw new AssertionError(e); }
  }
}

Person a = new Person();
a.scores = new int[]{1, 2, 3};
Person b = a.clone();
b.scores[0] = 99;
System.out.println(a.scores[0]);   // 99 — they share the same array

Для примитивов и неизменяемых значений (String, Integer, LocalDate) поверхностного копирования достаточно. Для изменяемых вложенных объектов это почти всегда неверно — изменение клона затрагивает и оригинал.

Глубокое копирование: решение проблемы

Чтобы получить по-настоящему независимую копию, переопределите clone() с рекурсивным копированием изменяемых полей:

@Override
public Person clone() {
  try {
    Person copy   = (Person) super.clone();
    copy.scores   = scores.clone();          // arrays have their own clone()
    return copy;
  } catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
  }
}

Массивы реализуют Cloneable нативно и копируются с помощью .clone(). Коллекции — нет: для поля типа List<String> следует написать copy.items = new ArrayList<>(items); (см. ArrayList). Если граф объектов содержит собственные изменяемые типы, каждый тип в этом графе должен участвовать в глубоком копировании.

Почему у Cloneable плохая репутация

Несколько особенностей делают clone() неудобным:

  • Это маркерный интерфейс без метода clone() — сам Cloneable ничего не предоставляет, контракт находится на уровне Object.clone().
  • Он обходит конструкторы — поля нового объекта заполняются JVM, поэтому инварианты, установленные в конструкторе, не проверяются заново.
  • Подклассы наследуют обязательство: если Parent переопределяет clone(), каждый подкласс должен поддерживать логику глубокого копирования в актуальном состоянии, иначе он молча наследует нерабочую поверхностную версию.
  • Проверяемое исключение, приведение типа, вызов super.clone() — каждое переопределение повторяет один и тот же шаблонный код.

Современная альтернатива: конструктор копирования

Конструктор копирования — это обычный конструктор, принимающий экземпляр того же класса и копирующий его поля:

public class Person {
  String name;
  int[]  scores;

  public Person(Person other) {
    this.name   = other.name;
    this.scores = other.scores.clone();   // deep where it matters
  }
}

Person b = new Person(a);

Он выполняется через обычный конструктор, поэтому инварианты проверяются. Это чистый Java-код без маркерных интерфейсов, CloneNotSupportedException и приведений типов. Подклассы просто пишут свой конструктор копирования, вызывающий super(other). Рекомендация Effective Java — предпочитать конструкторы копирования (или статические фабрики copyOf) вместо clone.

Классы, подобные коллекциям, уже следуют этому паттерну: new ArrayList<>(other), new HashMap<>(other), Set.copyOf(other).

Какой подход использовать?

СитуацияРекомендуемый подход
Новый простой класс под вашим контролемКонструктор копирования или статическая фабрика copyOf
Класс только с примитивными / неизменяемыми полямиЛюбой — даже поверхностный clone() безопасен
Класс с изменяемыми полями (списки, массивы, вложенные объекты)Конструктор копирования с явным глубоким копированием
Существующий API, уже требующий CloneableПереопределить clone() и глубоко скопировать изменяемые поля
Тип-значение, который можно перепроектироватьСделать его неизменяемым — тогда копирование не понадобится вовсе

Коротко: предпочитайте конструктор копирования для нового кода, а к clone() обращайтесь лишь тогда, когда этого требует существующий контракт.

Records и неизменяемые типы

Records являются неизменяемыми, поэтому они вообще не нуждаются в клонировании — одну и ту же ссылку можно передавать где угодно. Если нужна изменённая копия, напишите небольшие методы with...:

record Point(int x, int y) {
  Point withX(int newX) { return new Point(newX, y); }
}

Этот стиль — «создать новый экземпляр с одним изменённым полем» — обычно нагляднее, чем клонирование с последующей мутацией.

Практический пример

java— editable, runs on the server

Что дальше

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

Практика

Практика
Что делает стандартный `Object.clone()` с полем, которое хранит изменяемый список?
Что делает стандартный `Object.clone()` с полем, которое хранит изменяемый список?
Was this page helpful?