Действительно ли clone () когда-либо использовался? Как насчет защитного копирования в методах получения / установки? - PullRequest
8 голосов
/ 29 мая 2009

Люди практически когда-либо использовали защитные добытчики / сеттеры? Мне 99% времени вы намерены сделать объект, который вы задали в другом объекте, копией той же ссылки на объект, и вы намереваетесь, чтобы изменения, которые вы вносите в него, также были сделаны в объекте, в котором он был установлен. Если Вы setDate ( Date dt ) и измените DT позже, кого это волнует? Если я не хочу какой-то базовый неизменяемый компонент данных, в котором есть только примитивы и, возможно, что-то простое, например, Date, я никогда не буду его использовать.

Что касается клонирования, существуют проблемы с тем, насколько глубока или неглубока копия, поэтому кажется «опасным» знать, что получится, когда вы клонируете Объект. Я думаю, что я использовал clone() только один или два раза, и это было для копирования текущего состояния объекта, потому что другой поток (то есть другой HTTP-запрос, обращающийся к тому же объекту в сеансе) мог изменить его.

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

Но опять же, вы ДЕЙСТВИТЕЛЬНО изменили Дату, так что это своего рода вина, отсюда и все обсуждение термина "защита". Если весь код приложения находится под вашим собственным контролем среди небольшой и средней группы разработчиков, будет ли достаточно документирования ваших классов в качестве альтернативы созданию копий объектов? Или это не обязательно, так как вы всегда должны предполагать, что что-то НЕ копируется при вызове метода установки / получения?

Ответы [ 8 ]

11 голосов
/ 29 мая 2009

Из Эффективной Java Джоша Блоха:

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

Пункт 24: Делать защитные копии при необходимости

5 голосов
/ 29 мая 2009

Это нетривиальный вопрос. По сути, вы должны думать о любом внутреннем состоянии класса, которое вы передаете любому другому классу с помощью метода get или путем вызова метода установки другого класса. Например, если вы сделаете это:

Date now = new Date();
someObject.setDate(now);
// another use of "now" that expects its value to not have changed

тогда у вас потенциально есть две проблемы:

  1. someObject потенциально может изменить значение "now", что означает, что приведенный выше метод при последующем использовании этой переменной может иметь значение, отличное от ожидаемого, и
  2. если после передачи "now" в someObject вы измените его значение, а если someObject не сделал защитную копию, то вы изменили внутреннее состояние someObject.

Вы должны либо защищаться от обоих случаев, либо документировать свое ожидание того, что разрешено или запрещено, в зависимости от того, кто является клиентом кода. Другой случай, когда у класса есть Map, и вы предоставляете получатель для самого Map. Если Map является частью внутреннего состояния объекта и объект ожидает полностью управления содержимым Map, то вы должны никогда позволить Map из. Если вы должны предоставить получатель для карты, верните Collections.unmodifiableMap(myMap) вместо myMap. Здесь вы, вероятно, не хотите делать клон или защитную копию из-за потенциальной стоимости. Возвращая свою Карту, упакованную так, чтобы ее нельзя было изменить, вы защищаете свое внутреннее состояние от изменения другим классом.

По многим причинам clone() часто не является правильным решением. Некоторые лучшие решения:

  1. Для добытчиков:
    1. Вместо того, чтобы возвращать Map, возвращайте только Iterator s либо в keySet, либо в Map.Entry, либо к тому, что позволяет клиентскому коду делать то, что ему нужно. Другими словами, верните что-то, что по сути является представлением вашего внутреннего состояния только для чтения, или
    2. Возвращает объект изменяемого состояния, завернутый в неизменяемую оболочку, аналогичную Collections.unmodifiableMap()
    3. Вместо того, чтобы возвращать Map, предоставьте метод get, который берет ключ и возвращает соответствующее значение из карты. Если все клиенты будут делать с Map только получение значений из него, то не давайте клиентам сам Map; вместо этого предоставьте метод получения, который обернет метод Map get().
  2. Для конструкторов:
    1. Используйте конструкторы копирования в ваших конструкторах объектов, чтобы сделать копию всего, что передается в нем, является изменчивым.
    2. Разработайте так, чтобы по возможности принимать неизменяемые величины в качестве аргументов конструктора, а не изменяемые величины. Иногда имеет смысл взять длинную, возвращаемую, например, new Date().getTime(), а не Date объект.
    3. Сделайте как можно больше своего состояния final, но помните, что объект final все еще может быть изменяемым, а массив final все еще может быть изменен.

Во всех случаях, если возникает вопрос о том, кто «владеет» изменяемым состоянием, запишите его в методах получения, установки или конструктора. Документируйте это где-нибудь.

Вот тривиальный пример плохого кода:

import java.util.Date;

public class Test {
  public static void main(String[] args) {
    Date now = new Date();
    Thread t1 = new Thread(new MyRunnable(now, 500));
    t1.start();
    try { Thread.sleep(250); } catch (InterruptedException e) { }
    now.setTime(new Date().getTime());   // BAD!  Mutating our Date!
    Thread t2 = new Thread(new MyRunnable(now, 500));
    t2.start();
  }

  static public class MyRunnable implements Runnable {
    private final Date date;
    private final int  count;

    public MyRunnable(final Date date, final int count) {
      this.date  = date;
      this.count = count;
    }

    public void run() {
      try { Thread.sleep(count); } catch (InterruptedException e) { }
      long time = new Date().getTime() - date.getTime();
      System.out.println("Runtime = " + time);
    }
  }
}

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

    public MyRunnable(final Date date, final int count) {
      this.date  = new Date(date.getTime());
      this.count = count;
    }

тогда вы получите правильную информацию о времени. Это тривиальный пример. Вы не хотите отлаживать сложный пример.

ПРИМЕЧАНИЕ. общий результат неправильного управления состоянием - ConcurrentModificationException при итерации по коллекции.

Вы должны защищаться? Если вы можете гарантировать , что одна и та же небольшая команда опытных программистов всегда будет писать и поддерживать ваш проект, что они будут постоянно работать над ним, чтобы они сохраняли память о деталях проекта, что одно и то же люди будут работать над ним на протяжении всего жизненного цикла проекта, и что проект никогда не станет «большим», тогда, возможно, вы можете сойти с рук, не сделав этого. Но стоимость защитного программирования невелика, за исключением редких случаев, и выгода велика. Плюс: защитное кодирование - это хорошая привычка. Вы не хотите поощрять развитие вредных привычек передавать изменяемые данные в места, где их не должно быть. Это укусит вас однажды. Конечно, все это зависит от требуемого времени работы вашего проекта.

3 голосов
/ 29 мая 2009

Для обеих этих проблем суть явный контроль состояния. Может случиться так, что большую часть времени вы можете «уйти», не думая об этих вещах. Это имеет тенденцию быть менее верным, поскольку ваше приложение становится больше, и становится все сложнее рассуждать о состоянии и о том, как оно распространяется среди объектов.

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

class A {
   Map myMap;
}


class B {
   Map myMap;
   public B(A a)
   {
        myMap = A.getMap();//returns ref to A's myMap
   }
    public void process (){ // call this and you inadvertently destroy a
           ... do somethign destructive to the b.myMap... 
     }
}

Дело не в том, что вы всегда хотите клонировать, это было бы глупо и дорого. Смысл не в том, чтобы делать общие заявления о том, когда такие вещи уместны.

1 голос
/ 29 мая 2009

Я могу вспомнить одну ситуацию, когда клонирование намного предпочтительнее копирования конструкторов. Если у вас есть функция, которая принимает объект типа X, а затем возвращает его измененную копию, может быть предпочтительно, чтобы эта копия была клоном, если вы хотите сохранить внутреннюю информацию, не относящуюся к X. Например, функция, которая увеличивает Date на 5 часов, может быть полезной, даже если ей был передан объект типа SpecialDate. Тем не менее, в большинстве случаев использование композиции вместо наследования позволит полностью избежать подобных проблем.

1 голос
/ 29 мая 2009

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

0 голосов
/ 09 апреля 2010

Я начал использовать следующую практику:

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

  2. Создайте копируемый интерфейс следующим образом:

     public interface Copyable<T> {
            public T copy();
     }

Имейте метод copy классов, реализующих Copyable, вызовите конструктор защищенного копирования. Производные классы могут затем вызывать super.Xxx (obj_to_copy); использовать конструктор копирования базового класса и при необходимости добавлять дополнительные функции.

Тот факт, что Java поддерживает ковариантный тип возвращаемого значения , делает эту работу. Производные классы просто реализуют метод copy () соответствующим образом и возвращают безопасное для типов значение для своего конкретного класса.

0 голосов
/ 07 сентября 2009

Одна вещь, которую я всегда упускаю при «обсуждении защитной копии», это аспект производительности. Эта дискуссия является ИМХО идеальным примером производительности по сравнению с удобочитаемостью / безопасностью / надежностью.

Защитные копии отлично подходят для рока. Но если вы используете его в критически важной части вашего приложения, это может стать серьезной проблемой производительности. Недавно мы обсуждали это, когда вектор данных сохранял свои данные в двойных значениях []. getValues ​​() возвращает значения .clone (). В нашем алгоритме getValues ​​() был вызван для множества различных объектов. Когда мы задались вопросом, почему этот простой кусок кода занял так много времени, мы проверили код - заменили возвращаемые values.clone () возвращаемыми значениями, и внезапно наше общее время выполнения было уменьшено до менее чем 1/10 от исходного значение. Ну, мне не нужно говорить, что мы решили пропустить оборону.

Примечание: я больше не являюсь защитными копиями в целом. Но используйте свой мозг при клонировании () ing!

0 голосов
/ 29 мая 2009

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

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

...