Главная страница
Навигация по странице:

  • // Предоставление конкретной методики сравнения class Host { // Основная часть класса опущена

  • // Возвращаемый компаратор является сериализуемым

  • * @throws ArithmeticException, если m */

  • // Неправильный класс "неизменяемого" public final class Period { private final Date start; private final Date end; /** * @рагат start - начало периода.

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

  • // Атака на содержимое экземпляра Period Date start = new Date(); Date end = new Date(); Period р = new Period(start, end); end.setYear(78); // Изменяет содержимое объекта р!

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

  • Effective Java tmprogramming Language GuideJ o s h u a b lo c h


    Скачать 1.05 Mb.
    НазваниеEffective Java tmprogramming Language GuideJ o s h u a b lo c h
    Дата03.04.2018
    Размер1.05 Mb.
    Формат файлаpdf
    Имя файлаBlokh_Dzh_-_Java_Effektivnoe_programmirovanie.pdf
    ТипДокументы
    #40178
    страница14 из 25
    1   ...   10   11   12   13   14   15   16   17   ...   25

    // Интерфейс методики сравнения
    public interface Comparator { public int compare(Object 01, Object 02); Оказывается, что представленное определение интерфейса Сора есть в пакете java.util. Никакого волшебства в этом нет, вы могли точно также определить его сами. Так, для того чтобы можно было сравнивать не только строки, но и другие объекты, метод compare в интерфейсе принимает параметры типа Object, а не String. Следовательно, приведенный выше класс
    Str1ngLengthComparator необходимо слегка изменить, чтобы реализовать интерфейс Comparator. Перед вызовом метода length параметры типа Object нужно привести к типу String. Классы конкретных методик сравнения часто создаются с помощью анонимных классов (статья
    18). Так, следующий оператор сортирует массив строк по их длине
    Arrays.sort(stringArray, new Comparator() { public int compare(Object 01, Object 02) {
    String s1 = (String)o1;
    String s2 = (String)o2; return s1.length() - s2.length(); }
    });
    Поскольку интерфейс методики сравнения используется как тип для всех экземпляров конкретных методик сравнения, для того чтобы предоставить конкретную методику сравнения, нет необходимости делать соответствующий класс стратегии открытым. Вместо этого "класс-хозяин" (host) может передать открытое статическое поле (или статический метод генерации, тип которого соответствует интерфейсу методики сравнения, сам же класс методики сравнения может оставаться закрытым классом, вложенным в класс-хозяин. В следующем примере вместо анонимного класса используется статический класс-член, что позволяет реализовать в классе методики сравнения второй интерфейс -
    Serializable:
    // Предоставление конкретной методики сравнения
    class Host {
    // Основная часть класса опущена
    private static plass StrLenCmp implements Comparator, Serializable { public int compare(Object o1, Object o2) {
    String s1 = (String)o1;
    String s2 = (String)o2; return s1.1ength() - s2.length(); }
    }
    // Возвращаемый компаратор является сериализуемым
    public static final Comparator
    STRING_LENGTH_COMPARATOR = new StrLenCmp(); Представленный шаблон используется в классе String для того, чтобы через его поле Т передавать компаратор строк, независящий от регистра. Подведем итоги, первоначально указатели на функции в языке С использовались для реализации шаблона Strategy. для того чтобы реализовать этот шаблон в языке программирования Java, необходимо создать интерфейс, представляющий стратегии, а затем для каждой конкретной стратегии нужно построить класс, реализующий этот Интерфейс. Если конкретная стратегия применяется только один раз, ее класс обычно декларируется и реализуется с помощью анонимного класса. Если же конкретная стратегия передается для многократного использования, ее класс обычно становится закрытым статическим классом-членом и передается через поле public static final, чей тип соответствует интерфейсу стратегии.
    Глава
    6
    Методы
    В данной главе рассматривается несколько аспектов проектирования методов как обрабатывать параметры и возвращаемые методом значения, как строить сигнатуры методов и как документировать методы. Значительная часть материала относится как к методам, таки к конструкторам. Особое внимание уделяется удобству, устойчивости и гибкости программ. Проверяйте достоверность параметров bБольшинство методов и конструкторов имеет ограничения на то, какие значения могут быть переданы с параметрами. Например, нередко указывается, что индексы должны быть неотрицательными, а ссылки на объекты отличны от null. Вы обязаны четко документировать все эти ограничения и начинать метод сих проверки. Это частный случай более общего принципа стараться выявлять ошибки как можно ко после того, как они произойдут. В противном случае обнаружение ошибки станет менее вероятным, а определение источника ошибки - более трудоемким. Если методу передано неверное значение параметра, но перед началом обработки он проверяет полученные параметры, то вызов этого метода быстро и аккуратно завершится с инициированием соответствующего исключения. Если же метод не проверяет своих параметров, может произойти несколько событий. Метод может завершиться посередине обработки, инициировав непонятное исключение. Хуже, если метод завершится нормально, без возражений вычислив неверный результат. Но самое худшее, если метод завершится нормально, но оставит некий объект в опасном состоянии, что впоследствии в непредсказуемый момент времени вызовет появление ошибки в какой-либо другой части программы, никак несвязанной с этим методом.
    В открытых методах для описания исключений, которые будут инициироваться, когда значения параметров нарушают ограничения, используйте тег @throws генератора документации Javadoc (статья
    44). Как правило, это будет исключение IllegalArgumentException, IndexOutOfBoundsException или
    NullPointe rException (статья 42). После того, как вы документировали ограничения для параметров метода и исключения, которые будут инициироваться в случае нарушения ограничений, установить эти ограничения для метода не составит труда. Приведем типичный пример
    /**
    * Возвращает объект Biglnteger, значением которого является
    * (this mod m) Этот метод отличается от метода remainder тем,
    * что всегда возвращает неотрицательное значение Biglnteger.
    *
    * @раrаm m - модуль, должен быть положительным числом *
    @return this mod m
    * @throws ArithmeticException, если m <= О
    */
    public Biglnteger mod(Biglnteger m) { if (m.signum() <= О) throw new ArithmeticException("Modulus not positive");
    // Вычисления Если метод не предоставляется в распоряжение пользователей, то вы как автор пакета контролируете все условия, при которых этот метод вызывается, а потому можете и обязаны убедиться в том, что ему будут передаваться только правильные значения параметра. Методы, не являющиеся открытыми, должны проверять свои параметры, используя утверждения (assertion), а нес помощью обычных проверок. В случае применения платформы Java, поддерживающей утверждения (версия 1.4 и выше, вы должны пользоваться конструкцией assert, в противном случае следует применять собственный механизм проверки утверждений. Особенно важно проверять правильность параметров, которые не используются методом, а откладываются для обработки в дальнейшем. Например, рассмотрим статический метод генерации из статьи 16, который получает массив целых чисел и возвращает представление этого массива в виде экземпляра List. Если клиент метода передаст значение null, метод инициирует исключение
    NullPointerException, поскольку содержит явную проверку. Если бы проверка отсутствовала, метод возвращал бы ссылку на вновь Сформированный экземпляр List, который будет инициировать исключение NullPointerException, как только клиент попытается им воспользоваться. К сожалению, к тому моменту определить происхождение экземпляра List будет уже трудно, что может значительно усложнить задачу отладки.
    Частным случаем принципа, требующего проверки параметров, которые должны быть сохранены для использования в дальнейшем, являются конструкторы. Для конструкторов очень важно проверять правильность параметров, чтобы не допустить создания объектов, нарушающих инварианты соответствующего класса. Существуют исключения из правила, обязывающего перед выполнением вычислений проверять параметры метода. Важное значение имеет ситуация, когда явная проверка является дорогостоящей или невыполнимой операцией, но вместе стем параметры все же неявно проверяются непосредственно в процессе их обработки. Например, рассмотрим метод Collections.sort(List), сортирующий список объектов. Все" объекты в списке должны быть взаимно сравнимы. Входе его сортировки каждый объект в нем будет сравниваться с каким-либо другим объектом из того же списка. Если объекты не будут взаимно сравнимы, в результате одного из таких сравнений будет инициировано исключение ClassCastException, а это именно то, что должен делать в таком случае метод sort. Таким образом, нет смысла выполнять упреждающую проверку взаимной сравнимости элементов в списке. Заметим, однако, что неразборчивое использование данного подхода может привести к потере такого качества, как атомарность сбоя (статья 46). Иногда входе обработки неявно осуществляется требуемая проверка некоторых параметров, однако когда проверка фиксирует ошибку, инициируется совсем не то исключение. Другими словами, исключение, которое инициируется входе обработки и связано с обнаружением неверного значения параметра, не соответствует тому исключению, которое, согласно вашему описанию, должно инициироваться этим методом. В таких случаях для преобразования внутреннего исключения в требуемое вы должны использовать

    диому трансляции исключении, описанную в статье 43. Не следует из этой статьи делать вывод, что произвольное ограничение параметров является хорошим решением. Наоборот, вы должны создавать методы как можно более общими. Чем меньше ограничений накладывается на параметры, тем лучше, при условии, что метод для каждого полученного значения параметра может сделать что-то разумное. Часто, однако, некоторые ограничения обусловлены реализацией абстракций. Подведем итоги. Каждый раз, когда выпишете метод или конструктор, вы должны подумать над тем, какие существуют ограничения для его параметров. Эти ограничения необходимо отразить в документации и реализовать в самом начале метода в виде явной проверки. Важно привыкнуть к такому порядку. Та скромная работа, которая с ним связана, будет с лихвой вознаграждена при первом же обнаружении неправильного параметра. При необходимости создавайте резервные копии Одно из особенностей, благодаря которой работа с языком программирования 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 “ + е this.start = start; this.end = end;
    }
    public Date start() {
    return start; }

    115
    public Date end() {
    return end; }
    // Остальное опущено На первый взгляд может показаться, что это неизменяемый класс, который успешно выполняет условие, заключающееся в том, что началу периода не предшествует его же конец. Однако, воспользовавшись изменяемостью объекта Date, можно с легкостью нарушить этот инвариант
    // Атака на содержимое экземпляра Period
    Date start = new Date();
    Date end = new Date();
    Period р = new Period(start, end); end.setYear(78); // Изменяет содержимое объекта р!
    Для того чтобы защитить содержимое экземпляра Ре от нападений такого типа, для каждого изменяемого параметра конструктор должен создавать резервную копию (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 - он может вернуть экземпляр ненадежного подкласса, созданного специально для нанесения ущерба. Например, такой подкласс может записывать в закрытый статический список
    ссылку на экземпляр в момент создания последнего, а затем предоставить злоумышленнику доступ к этому списку. В результате злоумышленник получит полный контроль над всеми этими экземплярами. Чтобы предотвратить атаки такого рода, не используйте метод 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 в классе Ре относятся к классу jаvа.util.Date, а не какому-то потенциально ненадежному подклассу. Резервное копирование параметров производится не только для неизменяемых классов. Всякий раз, когда выпишете метод или конструктор, который помещает во внутреннюю структуру объект, созданный клиентом, задумайтесь, не является ли этот объект потенциально изменяемым. Если да, проанализируйте, будет ли ваш класс устойчив к изменениям в объекте, когда он получит доступ к этой структуре данных. Если ответ отрицательный, вы должны создать резервную копию объекта и поместить ее в структуру данных вместо оригинала. Например, если ссылку на объект,
    предоставленный клиентом, вы предполагаете использовать как элемент во внутреннем экземпляре
    Set или как ключ во внутреннем экземпляре Мар, следует учитывать, что инварианты этого набора или схемы могут быть нарушены, если после добавления в них объект вдруг поменяется. Тоже самое справедливо ив отношении резервного копирования внутренних компонентов перед возвращением их клиенту. Вы должны дважды подумать, является ли ваш класс изменяемым или нет, прежде чем передавать клиенту ссылку на внутренний компонент, который можно изменить. Возможно, вам все же следует возвращать резервную копию. Крайне важно также помнить о том, что массивы ненулевой длины всегда являются изменяемыми. Поэтому для внутреннего массива вы всегда должны делать резервную копию, прежде чем возвращать его клиенту. Как альтернатива, вы можете возвращать пользователю неизменяемое представление этого массива. Оба приема показаны в статье 12. Таким образом, урок, который можно извлечь из всего сказанного, заключается в том, что в качестве составных частей объектов вы должны по возможности использовать неизменяемые объекты, чтобы не пришлось беспокоиться о резервном копировании (статья 13). В случае же с примером Period стоит отметить, что опытные программисты для внутреннего представления времени часто применяют не ссылку на объекта простой тип long, возвращаемый методом Date, getTime(). И поступают они так в первую очередь потому, что Date является изменяемым. Не всегда можно создать резервную копию изменяемого параметра перед его включением в объект. Существуют такие методы и конструкторы, чей вызов означает явную передачу объекта, на который указывает параметр-ссылка. Вызывая такой метод, клиент дает обещание, что он не будет напрямую менять этот объект. Для метода или конструктора, предполагающего, что ему будет полностью передано управление изменяемым объектом, предоставленным клиентом, это обстоятельство должно быть четко оговорено в документации. Классы, где содержатся методы или конструкторы, вызов которых означает передачу управления объектом, неспособны защитить себя от злоумышленника. Такие классы можно использовать только тогда, когда есть взаимное доверие между классом и его клиентами или же когда нарушение инвариантов класса неспособно нанести ущерба никому, кроме самого клиента. Последнюю ситуацию иллюстрирует шаблон класса-оболочки (статья 14). При определенном характере класса-оболочки клиент может разрушить инварианты этого класса, используя прямой доступ к объекту уже после того, как он попал в оболочку, однако обычно это не наносит вреда никому, кроме самого клиента.
    1   ...   10   11   12   13   14   15   16   17   ...   25


    написать администратору сайта