Это нетривиальный вопрос. По сути, вы должны думать о любом внутреннем состоянии класса, которое вы передаете любому другому классу с помощью метода get или путем вызова метода установки другого класса. Например, если вы сделаете это:
Date now = new Date();
someObject.setDate(now);
// another use of "now" that expects its value to not have changed
тогда у вас потенциально есть две проблемы:
someObject
потенциально может изменить значение "now
", что означает, что приведенный выше метод при последующем использовании этой переменной может иметь значение, отличное от ожидаемого, и
- если после передачи "
now
" в someObject
вы измените его значение, а если someObject
не сделал защитную копию, то вы изменили внутреннее состояние someObject
.
Вы должны либо защищаться от обоих случаев, либо документировать свое ожидание того, что разрешено или запрещено, в зависимости от того, кто является клиентом кода. Другой случай, когда у класса есть Map
, и вы предоставляете получатель для самого Map
. Если Map
является частью внутреннего состояния объекта и объект ожидает полностью управления содержимым Map
, то вы должны никогда позволить Map
из. Если вы должны предоставить получатель для карты, верните Collections.unmodifiableMap(myMap)
вместо myMap
. Здесь вы, вероятно, не хотите делать клон или защитную копию из-за потенциальной стоимости. Возвращая свою Карту, упакованную так, чтобы ее нельзя было изменить, вы защищаете свое внутреннее состояние от изменения другим классом.
По многим причинам clone()
часто не является правильным решением. Некоторые лучшие решения:
- Для добытчиков:
- Вместо того, чтобы возвращать
Map
, возвращайте только Iterator
s либо в keySet
, либо в Map.Entry
, либо к тому, что позволяет клиентскому коду делать то, что ему нужно. Другими словами, верните что-то, что по сути является представлением вашего внутреннего состояния только для чтения, или
- Возвращает объект изменяемого состояния, завернутый в неизменяемую оболочку, аналогичную
Collections.unmodifiableMap()
- Вместо того, чтобы возвращать
Map
, предоставьте метод get
, который берет ключ и возвращает соответствующее значение из карты. Если все клиенты будут делать с Map
только получение значений из него, то не давайте клиентам сам Map
; вместо этого предоставьте метод получения, который обернет метод Map
get()
.
- Для конструкторов:
- Используйте конструкторы копирования в ваших конструкторах объектов, чтобы сделать копию всего, что передается в нем, является изменчивым.
- Разработайте так, чтобы по возможности принимать неизменяемые величины в качестве аргументов конструктора, а не изменяемые величины. Иногда имеет смысл взять длинную, возвращаемую, например,
new Date().getTime()
, а не Date
объект.
- Сделайте как можно больше своего состояния
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
при итерации по коллекции.
Вы должны защищаться? Если вы можете гарантировать , что одна и та же небольшая команда опытных программистов всегда будет писать и поддерживать ваш проект, что они будут постоянно работать над ним, чтобы они сохраняли память о деталях проекта, что одно и то же люди будут работать над ним на протяжении всего жизненного цикла проекта, и что проект никогда не станет «большим», тогда, возможно, вы можете сойти с рук, не сделав этого. Но стоимость защитного программирования невелика, за исключением редких случаев, и выгода велика. Плюс: защитное кодирование - это хорошая привычка. Вы не хотите поощрять развитие вредных привычек передавать изменяемые данные в места, где их не должно быть. Это укусит вас однажды. Конечно, все это зависит от требуемого времени работы вашего проекта.