Урок 19. Метод clone() и его альтернативы в Java
На этом уроке из нашего бесплатного курса Java давайте обсудим метод Java Object clone(). Метод clone() определяется в классе Object, который является суперклассом всех классов. Метод может быть использован на любом объекте, но, как правило, его вообще не рекомендуется применять.
Метод clone()
Метод clone(), как следует из названия, создает идентичную копию объекта. В зависимости от типа реализации, эта копия может быть либо неглубокой, либо глубокой копией. В любом случае объекты будут равны, но не идентичны (Подробнее см. мою статью об идентичности и равенстве в Java).
Использование функции clone() для объекта
Давайте посмотрим, как работает метод clone(). В примере ниже мы создаем объект класса Porsche по имени marcusPorsche, дающий мне очень хороший новый автомобиль. Однако я хороший парень и хотел бы отдать еще один Porsche; поэтому я клонирую объект marcusPorsche и назначаю новый объект ссылочной переменной peterPorsche. Если Вас зовут Питер, поздравляю! Ты только что купил новый Порше!
public class PorscheTest { @Test public void shouldClonePorsche(){ Porsche marcusPorsche = new Porsche(); Porsche peterPorsche = porsche.clone(); assertNotSame(marcusPorsche, peterPorsche); } }Однако что-то все же не так. Метод clone() подсвечивается красным. Проблема в том, что метод clone() из класса защищен от объекта. Каждый объект может вызывать защищенные методы, унаследованные от класса Object на себя. Однако он никогда не сможет вызвать такой метод на других объектах. Хотя clone() доступен как для нашего класса Porsche, так и для нашего класса PorscheTest, PorscheTest никогда не может вызвать унаследованный метод clone() объекта Porsche.
Чтобы устранить эту проблему, мы должны переопределить метод clone() в классе Porsche и увеличить его видимость. Во-первых, мы меняем модификатор доступа метода clone() на public и меняем тип возвращаемого метода с Object на Porsche, что делает наш код более понятным, поскольку нам не нужно бросать клонированные объекты в Porsche. Чтобы реализовать метод clone, мы вызываем super.clone(). Ключевое слово super означает, что мы вызываем метод из суперкласса, который в данном случае является классом объекта.
Обратите также внимание, что метод clone класса Object объявляет проверенное исключение CloneNotSupportedException, поэтому мы должны решить, хотим ли мы объявить его или обработать. Объявление исключения CloneNotSupportedException при реализации (поддержке) метода clone() является противоречивым. Поэтому вы должны опустить его. Все возможные ситуации с ошибками являются серьезными ошибками, поэтому лучшее, что вы можете сделать, - это выдать ошибку вместо этого.
public class Porsche implements Car { @Override public Porsche clone(){ try{ return(Porsche)super.clone(); } catch(CloneNotSupportedException e){ throw new AssertionError(); /* никогда не произойдет */ } } }Однако, когда мы запускаем приведенный выше код, мы сталкиваемся с другой проблемой: CloneNotSupportedException. Чтобы исправить это, мы должны реализовать клонируемый интерфейс. Клонируемый интерфейс не содержит никаких методов, это маркерный интерфейс – пустой интерфейс, используемый для обозначения некоторого свойства класса, который его реализует.
public class Porsche implements Car, Cloneable{Теперь, когда мы запускаем тест, он проходит успешно.
Изменение объекта после клонирования
На этом этапе мы добавим тест для проверки содержимого нового объекта. Мы хотим убедиться, что наш владелец был правильно идентифицирован в каждом объекте. Метод asString() будет использоваться для возврата строкового представления нашего объекта Porsche. Ожидаемый результат, когда мы закончим, заключается в том, что объект будет “Порше Питера”. Во-первых, я собираюсь создать метод asString() в нашем классе Porsche и атрибут owner.
String ownerName; [...] public String asString(){ return “Porsche of” + ownerName; }В приведенном ниже коде функция assertEquals() используется для сравнения выходных данных функции asString (). Когда мы запустим тест, он провалится, так как владельцем обоих Порше по-прежнему является “Маркус”.
@Test public void shouldClonePorsche(){ Porsche marcusPorsche = new Porsche(“Marcus”); Porsche peterPorsche = porsche.clone(); assertNotSame(porsche, peterPorsche); assertEquals(“Porsche of Peter”, peterPorsche.asString()); }Чтобы исправить этот тест, нам нужно создать метод передачи права собственности на автомобиль. Я собираюсь назвать этот метод sellTo().
public void sellTo(String newOwnerName){ ownerName = newOwnerName; }Теперь я вызову метод sellTo() на клонированном объекте Porsche, чтобы передать право собственности на клонированный Porsche Питеру. В качестве окончательного доказательства того, что клонирование объекта Porsche создало полностью независимый второй объект Porsche, я проверю, что передача права собственности Питеру на наш клонированный объект Porsche не повлияла на исходный объект Porsche. Другими словами, Я проверю, принадлежит ли оригинальный объект Porsche по-прежнему Маркусу или нет.
@Test public void shouldClonePorsche(){ Porsche marcusPorsche = new Porsche(“Marcus”); Porsche peterPorsche = porsche.clone(); assertNotSame(porsche, peterPorsche); peterPorsche.sellTo(“Peter”); assertEquals(“Porsche of Marcus”, porsche.asString()); assertEquals(“Porsche of Peter”, peterPorsche.asString()); }На этот раз, когда мы запустим тест, он пройдет. Это доказывает, что мы создали два независимых объекта, которые могут быть изменены независимо, поэтому наш метод клонирования работает так, как ожидалось.
Использование функции clone() для массива
Как я уже говорил, обычно не рекомендуется использовать метод клонирования. Однако один случай, когда я рекомендую его использовать, - это клонирование массивов, как показано в примере ниже. Здесь мы создаем массив типа String. Мы вызываем метод array.clone (), используя наш массив, и назначаем новый клонированный массив ссылочной переменной copiedArray. Чтобы доказать, что это не просто ссылка на тот же объект, а новый, независимый объект, мы вызываем assertNotSame() с нашим исходным массивом и нашим вновь созданным copiedArray. Оба массива имеют одинаковое содержимое, как вы можете видеть, когда мы распечатываем copiedArray в цикле for-each.
@Test public void shouldCloneStringArray() { String[] array = {“one”,“two”,“three”}; String[] copiedArray = array.clone(); assertNotSame(array, copiedArray); for(String str: copiedArray){ System.out.println(str); } }Метод clone() копирует каждый строковый объект в массиве в новый объект массива, содержащий совершенно новые строковые объекты. Если бы вы выполнили приведенный выше код, метод clone() работал бы так, как ожидалось, и тест был бы зеленым. В этом случае метод clone() является предпочтительным методом копирования массива. Clone() отлично работает с массивами примитивных значений и “неизменяемых объектов”, но он не работает так же хорошо для массивов объектов. Кстати, в выпуске 124 бюллетеня специалистов по Java Хайнц провел тест, который показал, что метод clone() немного медленнее для копирования очень маленьких массивов, но для больших массивов, где производительность имеет наибольшее значение, он на самом деле быстрее, чем любой другой метод копирования массивов.
Альтернативы clone(): конструктор копирования
Есть две рекомендуемые альтернативы использованию метода clone(), которые лишены недостатков clone(). Первый метод использует конструктор копирования, который принимает один параметр – объект для клонирования. Конструктор копий на самом деле не представляет собой ничего особенного. Как вы можете видеть, это просто простой конструктор, который ожидает аргумент класса, к которому он принадлежит.
Затем конструктор копирует (клонирует) все вложенные объекты. Если новый объект просто ссылается на старые подобъекты, мы называем его неглубокой копией. Если новый объект ссылается на действительно скопированные объекты, мы называем его глубокой копией.
public class BMW implements Car, Cloneable { private Name ownersName; private Color color; public BMW(BMW bmw) { this.ownersName = new Name(bmw.ownersName); this.color = new Color(bmw.color); } }Альтернативы clone(): статический фабричный метод (виртуальный конструктор)
Другой альтернативой является статический фабричный метод. Как следует из названия, статический фабричный метод - это статический метод, используемый для создания экземпляра класса. Он также может быть использован для создания клона. Существует несколько общих имен для статических заводских методов; я собираюсь использовать newInstance(). Я также создал тот же метод для имени и цвета, чтобы рекурсивно выполнять операцию со всеми подобъектами.
public class BMW implements Car, Cloneable { private Name ownersName; private Color color; public static BMW newInstance(BMW bmw){ return new BMW(Name.newInstance(bmw.ownersName), Color.newInstance(bmw.color)); } public BMW(Name ownersName, Color color){ this.ownersName = ownersName; this.color = color; } }Обе эти альтернативы всегда лучше метода clone() и дают один и тот же результат: клон исходного объекта. Статический фабричный метод может быть немного более мощным, чем конструктор копирования в некоторых случаях, но обычно оба приводят к одному и тому же результату.
Неизменный
Последнее, что я хотел бы сделать в этой статье, - это взглянуть на класс Name:
public class Name implements Cloneable{ private String firstName; private String lastName; public static Name newInstance(Name name){ return new Name(name.firstName, name.lastName); } }Обратите внимание, однако, что строковые члены передаются по ссылке, не создавая новый объект. Причина этого заключается в том, что строка является неизменяемым объектом. Неизменяемый объект не будет изменен ссылкой в новом объекте, и поэтому его можно безопасно использовать без клонирования. Это чрезвычайно важная концепция, которая применяется, когда вы пытаетесь создать глубокую копию.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.
Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.