Главная » Java, Советы » При необходимости создавайте резервные копии

0

 

Одно из особенностей, благодаря которой работа с языком программирования Java доставляет такое удовольствие, является его безопасность. Это означает, что в отсутствие машинно-зависимых методов (native method) он неуязвим по отношению

 

 

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

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

Хотя другой класс не сможет поменять внутреннее состояние объекта без какой-либо поддержки со стороны последнего, оказать такое содействие, не желая того, на удивление просто. Например, рассмотрим класс, задачей которого является представление неизменного периода времени:

 

// Неправильный класс "неизменяемого"

public final class Period {

private final Date start;

private final Date end;

/**

* @рагат start – начало периода.

* @рагат end – конец периода; не должен предшествовать началу.

* @throws IllegalArgumentException. если start позже, чем end.

* @throws"NullPointerException, если start или end равен null.

*/

public Period(Date start, Date end) {

if (start.compareTo(end) > 0)

throw new IllegalArgumentException(start + “ after “ + еnd);

this.start = start;

this.end = end;

}

public Date start() {

return start;   }

 

 

public Date end() {

return end;   }

// Остальное опущено

}

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

 

// Атака на содержимое экземпляра Period

 Date start = new Date();

Date end = new Date();

Period р = new Period(start, end);

          end.setYear(78);      // Изменяет содержимое объекта р!

 

Для того чтобы защитить содержимое экземпляра Реriod от нападений такого типа, для каждого изменяемого параметра конструктор должен создавать резервную копию (defensive сору) и использовать именно эти копии, а не оригинал, как составные части экземпляра Period:

 

// Исправленный конструктор:

// создает резервные копии для параметров

public Period(Date start, Date end) {

this.start = new Date(start.getTime());

                this.end = new Date(end.getTime());

if (this.start.compareTo(this.end) > 0)

throw new IllegalArgumentException(start +" after"+ end);  }

 С новым конструктором описанная ранее атака уже не может воздействовать на экземпляр Period. Заметим, что резервные копии создаются до проверки правильности параметров (статья 23), так что сама проверка выполняется уже не для оригинала, а для его копии. Такой порядок может показаться искусственным, но он необходим, поскольку защищает класс от подмены параметров, которая выполняется из параллельного потока в пределах "окна уязвимости" (window of vulnerability): с момента, когда параметры проверены, и до того момента, когда для них созданы копии.

Заметим также, что для создания резервных копий мы не пользовались методом clone из класса Date. Поскольку Date не является окончательным классом, нет гарантии, что метод clone возвратит объект именно класса jаvа.util.Date – он может вернуть экземпляр ненадежного подкласса, созданного специально для нанесения ущерба. Например, такой подкласс может записывать в закрытый статический список

 

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

Обновленный конструктор успешно защищает от вышеописанной атаки, ,однако все равно остается возможность модификации экземпляра Period, поскольку его методы предоставляют доступ к его внутренним частям, которые можно поменять:

// Вторая атака на содержимое экземпляра Period

Date start = new Date();

Date end = new Date();

Period р = new Period(start, end);

p.end().setYear(78); // Изменяет внутренние данные р!

для защиты от второй атаки модифицируйте методы доступа таким образом, чтобы возвращались резервные копии изменяемых внутренних полей.

// Исправленные методы доступа:

// создаются резервные копии внутрених полей

public Date start() {

return (Date) start.clone(); }

public Date end() {

return (Date) end.clone();  }

Получив новый конструктор и новые методы доступа, класс Period стал действительно неизменяемым. Теперь некомпетентному программисту или злоумышленнику не удастся нарушить инвариант, гласящий, что начало периода предшествует его концу. Это так, поскольку, за исключением самого класса Period, никакой другой класс не имеет возможности получить доступ хоть к какому-нибудь изменяемому полю экземпляра Period. Указанные поля действительно инкапсулированы в этом объекте.

Заметим, что новые методы доступа, в отличие от нового конструктора, для создания резервных копий используют метод clone. Такое решение приемлемо (хотя и необязательно), поскольку мы точно знаем, что внутренние объекты Date в классе Реriod относятся к классу jаvа.util.Date, а не какому-то потенциально ненадежному подклассу.

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

 

 

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

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

Таким образом, урок, который можно извлечь из всего сказанного, заключается в том, что в качестве составных частей объектов вы должны по возможности использовать неизменяемые объекты, чтобы не пришлось беспокоиться о резервном копировании (статья 13). В случае же с примером Period стоит отметить, что опытные программисты для внутреннего представления времени часто применяют не ссылку на объект Date, а простой тип long, возвращаемый методом Date, getTime(). И поступают они так в первую очередь потому, что Date является изменяемым.

Не всегда можно создать резервную копию изменяемого параметра перед его включением в объект. Существуют такие методы и конструкторы, чей вызов означает явную передачу объекта, на который указывает параметр-ссылка. Вызывая такой метод, клиент дает обещание, что он не будет напрямую менять этот объект. Для метода или конструктора, предполагающего, что ему будет полностью передано управление изменяемым объектом, предоставленным клиентом, это обстоятельство должно быть четко оговорено в документации.

Классы, где содержатся методы или конструкторы, вызов которых означает передачу управления объектом, не способны защитить себя от злоумышленника. Такие классы можно использовать только тогда, когда есть взаимное доверие между классом и его клиентами или же когда нарушение инвариантов класса не способно нанести ущерба никому, кроме самого клиента. Последнюю ситуацию иллюстрирует шаблон класса-оболочки (статья 14). При определенном характере класса-оболочки клиент может разрушить инварианты этого класса, используя прямой доступ к объекту уже после того, как он попал в оболочку, однако обычно это не наносит вреда никому, кроме самого клиента.

 

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии