Рубрики
Без рубрики

Вступает в силу со вторника! Разумно переопределить `клонирование`

Погружение в двенадцатую главу “Эффективной Java”. Помеченный как java, эффективный, клонирование, архитектура.

Сегодня мы говорим об интерфейсе Cloneable и связанных с ним клонировать функцию. Честно говоря, я не взаимодействовал с этим методом в прошлом и после того, как узнал о нем Я не уверен, что действительно этого хочу, это кажется довольно подверженным ошибкам. Итак, давайте углубимся и разберемся в этом методе, чтобы мы могли принимать разумные решения о том, использовать этот метод или нет.

Итак, что же такое Cloneable интерфейс. Это смешанный интерфейс это сигнализирует пользователям класса о том, что над классом может быть выполнено определенное действие. Чрезвычайно странная вещь в этом конкретном смешивании заключается в том, что вместо того, чтобы требовать реализации конкретной функции, оно просто действует как флаг, который позволяет реализующему классу вызывать метод родительского класса. Это означает, что пользователь класса, который реализует Cloneable не обязательно всегда может вызывать метод clone для этого класса, не прибегая к отражению, и даже тогда это может не сработать. Все это, как говорится, является частью класса Object , поэтому стоит понимать его и знать, как реализовать метод, а также каковы альтернативы. Эта глава Effective Java проходит через это.

Итак, что такое контракт Cloneable ? Как мы узнали выше, он не включает в себя никаких методов, а вместо этого действует как флаг для защищенного клонируйте метод в классе Object . Если класс вызывает clone для объекта и этот класс реализует Поддающийся клонированию , Реализация Object /| clone вернет копию объекта по полю. Если класс не реализует Cloneable , выдается исключение CloneNotSupportedException . Если такое использование интерфейса кажется странным, это хорошо, это не то поведение, которое вы должны пытаться имитировать в своих собственных классах. Общий (хотя и слабый) контракт заключается в следующем:

  • Реализующий класс должен создать общедоступный класс clone , который вызывает метод super.clone() .
  • (x.clone()) проще говоря, clone должен возвращать новый объект, а не просто возвращать текущий объект.
  • (x.clone().getClass().getClass() это не является абсолютным требованием, но ожидается.
  • x.clone.equals(x) Опять же, это не является абсолютным требованием, но уменьшает удивление от того, как это будет работать.

Можно было бы подумать, что вы могли бы просто пропустить вызов метода super.clone() в вашем собственном клонируйте метод и просто вызывайте конструктор для создания нового объекта но это может вызвать проблемы для класса, который расширяет ваш класс и вызывает super.clone() , поскольку он вернет объект неправильного класса. Как упоминалось выше, на самом деле это не нарушает контракт, но противоречит соглашению.

Итак, давайте немного углубимся в то, как это работает и как вы должны это реализовать. Первым шагом реализации метода clone является вызов super.clone() , который вернет полностью функционирующую копию вызывающего класса. Если ваш класс содержит только примитивы или ссылки на неизменяемые объекты, это может быть все, что вам нужно сделать. Давайте посмотрим на пример:

@Override
public Address clone() {
  try {
    return (Address) super.clone();
  } catch (CloneNotSupportedException impossible) {
    // This will never happen.
    throw new AssertionException();
  }
}

Давайте рассмотрим здесь некоторые интересные вещи. Потому что Объект ‘s тип возвращаемого значения метода clone равен Объект мы хотим привести его к типу нашего класса. Это нормально, потому что Java допускает ковариантные типы. Проще говоря, это позволяет нам использовать подкласс требуемого класса вместо родительского типа. Это приведение всегда будет успешным и позволяет клиентскому коду пропустить приведение типов. Следующая интересная вещь здесь – это try-catch . Сигнатура метода объекта включает в себя то, что он выдает исключение Clonenotsupportedexception . В случае, когда класс реализует интерфейс Cloneable , это исключение никогда не будет выдано, это пример неправильного использования проверяемого исключения и должно было быть RuntimeException .

Итак, давайте рассмотрим пример, в котором класс немного сложнее с непримитивными полями.

public class Stack {
  private Object[] elements;
  private int size;
  private static final int DEFAULT_INITIAL_SIZE = 16;

  public Stack() {
    elements = new Object[DEFAULT_INITIAL_SIZE];
  }

  public void push(Object o) {
    ensureCapacity();
    elements[size++] = o
  }

  public Object pop() {
    if (size == 0) {
      throw new StackEmptyException();
    }

    Object result = elements[--size];
    elements[size] = null;
    return result;
  }

  private ensureCapacity() {
    if (elements.length == size) {
      elements = Arrays.copyOf(elements, 2 * size + 1);
    }
  }
}

Итак, если мы хотим сделать этот Стек класс реализуемым Клонируемым и попытался имитировать то, что мы сделали с классом Address ? В итоге мы получили бы класс-реплику с скопированным полем size , но с массивом Object , который является общим для двух экземпляров. Это приведет ко многим проблемам, поэтому нам нужно пойти немного дальше. Думайте о методе clone как о типе конструктора, который должен защищать исходный объект. Итак, давайте посмотрим на рабочий метод clone для этого Stack класса:

@Override
public class Stack clone() {
  try {
    // this gets us a replica with copied size field
    Stack copy = (Stack) super.clone();
    copy.elements = elements.clone();
    return copy;
  } catch (CloneNotSupportedException impossible) {
    throw new AssertionError();
  }
}

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

Другие вещи, о которых нужно подумать:

  • Поскольку методы clone похожи на конструкторы, они не должны вызывать переопределяемые методы.
  • Даже несмотря на то, что Object ‘s clone метод выдает CloneNotSupportedException , ваши переопределения не должны.
  • При разработке класса для наследования у вас есть два варианта. Реализуйте метод clone с той же сигнатурой, что и Object , предоставляя реализующему классу свободу выбора для реализации Поддающийся клонированию или нет. Другой вариант – реализовать clone и просто вызвать CloneNotSupportedException , который заблокирует клонирование.
  • Если ваш класс должен быть потокобезопасным, помните, что ваша реализация clone также должна быть синхронизирована. Объект ‘s метод clone не синхронизирован.

Так стоит ли это реализовывать? Скорее всего, нет. Есть гораздо более простые способы добиться этого. Часто конструктор копирования или фабрика копирования могут выполнить эту работу гораздо более простым способом. Таким образом, в нашем случае Address это будет выглядеть следующим образом:

public class Address(Address originalAddress) { ... }

или

public static Address newInstance(Address originalAddress) { ... }

Итак, каковы некоторые преимущества использования одного из этих методов по сравнению с реализацией Cloneable :

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

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

Оригинал: “https://dev.to/kylec32/effective-java-tuesday-override-clone-judiciously-4fg”