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

  • // Кэширующая функция с отложенной инициализацией // для неизменяемого объекта

  • // Закрытая вспомогательная функция, вычисляющая // значение нашего объекта foo

  • // Класс-оболочка: вместо наследования используется композиция

  • // Методы переадресации

  • 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
    страница9 из 25
    1   ...   5   6   7   8   9   10   11   12   ...   25

    // Неизменяемый класс со статическими методами генерации
    // вместо конструкторов
    publiC class Complex { private final float ге;
    private final float im; private Complex(float ге, float im) {
    this. ге = ге; this.im = im; public static Соmрlех valueOf(float ге, float im) {
    return new Сотрlех(ге, im);}
    // Остальное не изменилось Хотя данный подход не используется широко, из трех описанных альтернатив он часто оказывается наилучшим. Он самый гибкий, так как позволяет применять несколько классов реализации, доступных в пределах пакета. для клиентов за пределами пакета этот неизменяемый класс фактически является окончательным, поскольку они не могут расширить класс, взятый из другого пакета, у которого нет ни открытого, ни защищенного конструктора. Помимо того, что этот метод позволяет гибко использовать несколько классов реализации, он дает возможность повысить производительность класса в последующих версиях путем совершенствования механизма кэширования объектов в статических методах генерации. Как показано в статье 1
    , статические методы генерации объектов имеют множество преимуществ по сравнению с конструкторами. Предположим, что вы хотите создать механизм генерации комплексного числа, отталкиваясь от его полярных координат. Использовать здесь конструкторы плохо, поскольку окажется, что собственный конструктор класса Complex будет иметь туже самую сигнатуру, которую мы только что применяли Complex(float, float).
    Со статическими методами генерации все проще - достаточно добавить второй статический метод генерации с таким названием, которое четко обозначит его функцию public static Complex valueOfPolar(float г, float theta) {
    return new Complex((float) (r * Math.cos(theta)), (float) (r * Math,sin(theta»);
    } Когда писались классы BigInteger и BigDecimal, не было согласия в том, что неизменяемые классы должны быть фактически окончательными. Поэтому любой метод этих классов можно переопределить. К сожалению, исправить что-либо впоследствии уже было нельзя, не потеряв при этом совместимость версий снизу вверх. Поэтому, если выпишите класс, безопасность которого зависит от неизменяемости аргумента с типом BigInteger или BigDecimal, полученного от ненадежного клиента, вы должны выполнить проверку и убедиться в том, что этот аргумент действительно является "настоящим" классом BigInteger или BigDecimal, а не экземпляром какого-либо ненадежного подкласса. Если имеет место последнее, необходимо создать резервную копию этого экземпляра, поскольку придется исходить из того, что он может оказаться изменяемым (статья
    24): public void foo(BigInteger b) { if (b.getClass() != BigInteger.class) b = new BigInteger(b.toByteArray());
    } Список правил для неизменяемых классов, представленный вначале статьи, гласит, что ни один метод не может модифицировать объект и все поля должны иметь модификатор final. Эти правила несколько строже, чем необходимо, и их можно ослабить с целью повышения производительности программы. Действительно, ни один метод не может произвести такое изменение состояния объекта, которое можно было бы увидеть извне. Вместе стем, многие неизменяемые классы имеют одно или несколько избыточных полей без модификатора final, в которых они сохраняют однажды полученные результаты трудоемких вычислений. Если в дальнейшем потребуется произвести те же самые вычисления, будет возвращено ранее сохраненное значение, ненужные вычисления выполняться не будут. Такая уловка работает надежно именно благодаря неизменяемости объекта неизменность его состояния является гарантией того, что если вычисления выполнять заново, то они приведут опять к тому же результату. Например, метод hashCode из класса PhoneNumbeT (статья
    8) вычисляет хэш-код. Получив код в первый раз, метод сохраняет его на тот случай, если потребуется вычислять его снова. Такая методика, представляющая собой классический пример отложенной инициализации статья
    48), используется также ив классе String. Никакой синхронизации здесь не требуется, поскольку никаких проблем с повторным вычислением хэша не возникает

    67
    Приведем общую идиому для кэширующей функции с отложенной инициализацией для неизменяемого объекта
    // Кэширующая функция с отложенной инициализацией
    // для неизменяемого объекта
    .private volatile Foo cachedFooVal = UNLIKELY_FOO_VALUE; publlic Foo foo() {
    Foo result = cachedFooVal; if (result == UNLIKELY_FOO_VALUE) result = cachedFooVal = fooValue(); return result;
    }
    // Закрытая вспомогательная функция, вычисляющая
    // значение нашего объекта foo
    private Foo fooVal() { ... } Следует добавить одно предостережение, касающееся сериализуемости объектов. Если вы решили, что ваш неизменяемый класс должен реализовывать интерфейсе, но при этому него есть одно или несколько полей, которые ссылаются на изменяемые объекты, то вы обязаны предоставить явный метод readObject или readResolve, даже если для этого класса можно использовать сериализуемую форму, предоставляемую по умолчанию. Метод readObject, применяемый по умолчанию, позволил бы пользователю создать изменяемый экземпляр вашего во всех остальных ситуациях неизменяемого класса. Эта тема детально раскрывается в статье 56. Подведем итоги. Не стоит для каждого метода get писать метод set. Классы должны оставаться неизменяемыми, если нет веской причины делать их изменяемыми. Неизменяемые классы имеют массу преимуществ, единственный же их недостаток - возможные проблемы с производительностью при определенных условиях. Небольшие объекты значений, такие как PhoneNumber и Complex, всегда следует делать неизменяемыми. (В библиотеках для платформы Java есть несколько классов например java.util.Date и java.awt.Point, которые должны быть неизменяемыми, но таковыми не являются) Вместе стем вам следует серьезно подумать, прежде чем делать неизменяемыми более крупные объекты значений, такие как 5tring и Biglnteger. Создавать для вашего неизменяемого класса открытый изменяемый класс-компаньон следует, только если вы уверены в том, что это необходимо для получения приемлемой производительности (статья 37) .. Есть классы, которым неизменяемость ненужна, например классы-процессы Thread и TimerTask. Если класс нельзя сделать неизменяемым, вы должны ограничить его изменяемость, насколько это возможно. Чем меньше число состояний, в которых может находиться объект, тем проще рассматривать этот объект, тем меньше вероятность ошибки. По этой причине конструктор такого класса должен создавать полностью инициализированный объекту которого все инварианты уже установлены. Конструктор не должен передавать другим методам класса объект, сформированный частично

    68
    Не создавайте открытый метод инициализации отдельно от конструктора, если только для этого нет чрезвычайно веской причины. Точно также не следует создавать метод "повторной инициализации, который позволил бы использовать объект повторно, как если бы он был создан с другим исходным состоянием. Метод повторной инициализации обычно дает (если вообще дает) лишь небольшой выигрыш в производительности за счет увеличения сложности приложения. Перечисленные правила иллюстрирует класс Т. Он является изменяемым, однако пространство его состояний намеренно оставлено небольшим. Вы создаете экземпляр, задаете порядок его выполнения и, возможно, отменяете это решение. Как только задача, контролируемая таймером, запускается на исполнение или отменяется, повторно использовать его вы уже не можете. Последнее замечание, которое нужно сделать в этой статье, касается класса Complex. Этот пример предназначался лишь для того, чтобы продемонстрировать свойство неизменяемости. Он не обладает достоинствами промышленной реализации класса комплексных чисел. для умножения и деления комплексных чисел он использует обычные формулы, для которых нет правильного округления и которые имеют скудную семантику для комплексных значений NaN и бесконечности
    [Kahan91, Smith62, Thomas94]. Предпочитайте компоновку наследованию Наследование (inheritance) - это мощный способ обеспечения многократного использования кода, ноне всегда лучший инструмент для работы. При неправильном применении наследование приводит к появлению ненадежных программ. Наследование можно безопасно использовать внутри пакета, где реализация и подкласса, и суперкласса находится под контролем одних и тех же программистов. Столь же безопасно пользоваться наследованием, когда расширяемые классы специально созданы и документированы для последующего расширения (статья 15). Однако наследование обыкновенных неабстрактных классов за пределами пакета сопряжено с риском. Напомним, что в этой книге слово "наследование" (inheritance) применяется для обозначения наследования реализации (implementation inheritance), когда один класс расширяет Другой. Проблемы, обсуждаемые в этой статье, не касаются наследование интерфейса (interface inheritance), когда класс реализует интерфейс или же один интерфейс расширяет другой. В отличие от вызова метода, наследование нарушает инкапсуляцию [Snyder86]. Иными словами, правильное функционирование подкласса зависит от деталей реализации его суперкласса. Реализация суперкласса может меняться от версии к версии, и если это происходит, подкласс может "сломаться, даже если его код остался в неприкосновенности. Как следствие, подкласс должен развиваться вместе со своим суперклассом, если только авторы суперкласса не спроектировали и не документировали его специально для последующего расширения
    Предположим, что у насесть программа, использующая класс
    HashSet. для повышения производительности нам необходимо запрашивать у
    HashSet, сколько элементов было добавлено с момента его создания (не путать сего текущим размером, который при удалении элемента уменьшается. Чтобы обеспечить такую возможность, мы пишем вариант класса
    HashSet, который содержит счетчик количества попыток добавления элемента и предоставляет метод доступа к этому счетчику. В классе
    HashSet есть два метода, с помощью которых можно добавлять элементы add и addAl1. Переопределим оба метода
    // Ошибка неправильное использование наследования class InstrumentedHashSet extends HashSet {
    // Число попыток вставить элемент private int addCount = 0; public InstrumentedHashSet() {
    } public InstrumentedHashSet(Collection со
    }
    public boolean addAll(Collection с) { addCount += c.size(); return super.addAll(c);
    }
    publlic int getAddCount() return addCount; Представленный класс кажется правильным, ноне работает. Предположим, что мы создали один экземпляр и с помощью метода addAll поместили в него три элемента
    InstrumentedHashSet s = new InstrumentedHashSet(); s.addAll(Arrays.asList(new String[] {"Snap", "Crackle", "Рор"})); Мы предполагаем, что метод getAddCount должен возвратить число 3, но он возвращает 6. Что жене так Внутри класса
    HashSet метод addAll реализован прверх его метода add, хотя в документации эта деталь реализации не отражена, что вполне оправданно Метод addAll в классе
    InstrumentedHashSet добавил к значению поля addCount число
    3. Затем с помощью super. addAl1 была вызвана реализация addAll в классе
    HashSet. В свою очередь, это влечет вызов метода add, переопределенного в классе
    InstrumentedHashSet,- по одному разу для каждого элемента. Каждый из этих трех вызовов добавляет к значению addCount еще единицу, так что в итоге общий прирост составляет шесть добавление каждого элемента с помощью метода addAl1 засчитывается дважды. Мы могли бы "исправить" Подкласс, отказавшись от переопределения метода addAl1. Полученный класс будет работать, но правильность его работы зависит оттого обстоятельства, что метод addAll в классе
    HashSet реализуется поверх метода add. Такое "использование самого себя" является деталью реализации, и нет гарантии, что она будет сохранена во всех реализациях платформы Java, не поменяется при переходе от одной версии к другой. Следовательно, полученный класс
    InstrumentedHashSet может быть ненадежен. Ненамного лучшим решением будет переопределение в качестве метода, который в цикле просматривает представленный набор и для каждого элемента один разв ызывает метод add. Это может гарантировать правильный результат независимо оттого, реализован ли метод addAll в классе
    HashSet поверх метода add, поскольку реализация addAl1 в классе
    HashSet больше не применяется. Однако и такой прием не решает всех проблем. Он подразумевает повторную реализацию методов суперкласса, которые могут приводить, а могут не приводить к использованию классом самого себя. Этот вариант сложен, трудоемок и подвержен ошибкам. К тому же это не всегда возможно, поскольку некоторые методы нельзя реализовать, не имея доступа к закрытым полям, которые недоступны для подкласса. Еще одна причина ненадежности подклассов связана стем, что в новых версиях суперкласс может обзавестись новыми методами. Предположим, безопасность программы зависит оттого, что все элементы, помещенные в некоторую коллекцию, должны соответствовать некоему утверждению. Выполнение этого условия можно гарантировать, создав для этой коллекции подкласс, переопределив в нем все методы, добавляющие элемент, таким образом, чтобы перед добавлением элемента проверялось его соответствие рассматриваемому утверждению. Такая схема работает замечательно до тех пор, пока в следующей версии суперкласса не появится новый метод, Который также может добавлять элемент в коллекцию. Как только это произойдет, Станет возможным добавление "незаконных" элементов' в экземпляр подкласса простым вызовом нового метода, который не был переопределен в подклассе. Указанная проблема не является чисто теоретической. Когда производился пересмотр классов
    Hashtable и
    Vector для включения в архитектуру
    Collections Framework, пришлось закрывать несколько дыр такой природы, возникших в системе безопасности. Обе описанные проблемы возникают из-за переопределения методов. Вы можете решить, что расширение класса окажется безопасным, если при добавлении в класс новых методов воздержаться от переопределения уже имеющихся. Хотя расширение такого рода гораздо безопаснее, оно также не исключает риска.
    Если в очередной версии суперкласс получит новый метод, но окажется, что вы, к сожалению, уже имеете в подклассе метод стой же сигнатурой, нос другим типом возвращаемого значения, то ваш подкласс перестанет компилироваться
    [ILS,8.4.6.3]. Если же вы создали в подклассе метод с точно такой же сигнатурой, как и у нового метода в суперклассе, то переопределите последний и опять столкнетесь с обеими описанными выше проблемами. Более того, вряд ли ваш метод будет отвечать требованиям, предъявляемым к новому методу в суперклассе, так как, когда выписали этот метод в подклассе, они еще небыли сформулированы. К счастью, можно устранить все описанные проблемы. Вместо того чтобы расширять имеющийся класс, создайте в вашем новом классе закрытое поле, которое будет содержать ссылку на экземпляр прежнего класса. Такая схема называется композицией (composition), поскольку имеющийся класс становится частью нового класса. Каждый экземпляр метода в новом классе вызывает соответствующий метод содержащегося здесь же экземпляра прежнего класса, а затем возвращает полученный результат. Это называется передачей вызова (forwarding), а соответствующие методы нового класса носят название методов переадресации (forwarding method). Полученный класс будет прочен, как скала он не будет зависеть от деталей реализации прежнего класса. Даже если к имевшемуся прежде классу будут добавлены новые методы, на новый класс это не повлияет. В качестве конкретного примера использования метода компоновки/переадресации представим класс, который заменяет InstrumentedHashSet:
    // Класс-оболочка: вместо наследования используется композиция
    public class InstrumentedSet implements Set { private final Set s; private int addCount = 0 ; public InstrumentedSet(Set s) { this.s = s; }
    public boolean add(Object ос addCount; }
    // Методы переадресации
    public void clear() { s.clear(); } public boolean contains(Object о) { return s.contains(o);
    public boolean isEmpty() { return s. isEmpty(); } public int size() { return s,size(); } public Iterator iterator() { return s.iterator(); } public boolean remove(Object о) { return s. геmоуе(о); } public boolean containsAll(Collection с) { return s.containsAll(c); public boolean removeAll(Collection с) { return s.removeAll(c); } public boolean retainAll(Collection с) { return s. retainAll(c); }
    public Object[] toArray() { return s.toArray(); } pUblic Object[] toArray(Object[] а) { return s.toArray(a); }
    public boolean equals(Object о) { return s.equals(o); } public 1nt hashCode() { return s.hashCode(); } public Str1ng toStr1ng() { return s.toStr1ng(); }
    Создание класса Inst rumentedSet стало возможным благодаря наличию интерфейса Set, в котором собраны функции класса HashSet. Данная реализация не только устойчива, но и чрезвычайно гибка. Класс InstrumentedSet реализует интерфейс Set и имеет единственный конструктор, аргумент которого также имеет тип Set. В сущности, представленный класс преобразует один интерфейс Set в другой, добавляя возможность выполнения измерений. В отличие от подхода, использующего наследование, который работает только для одного конкретного класса и требует отдельный конструктор для каждого конструктора в суперклассе, данный класс-оболочку можно применять для расширения возможностей любой реализации интерфейса Set, он будет работать с любым предоставленным ему конструктором. Например
    Set s1 = new InstrumentedSet(new TreeSet(list));
    Set s2 = new InstrumentedSet(new HashSet(capacity, loadFactor)); Класс Inst rumentedSet можно применять даже для временного оснащения экземпляра Set, который до сих пор не пользовался этими функциями static void f(Set s) {
    InstrumentedSet sInst = new InstrumentedSet(s);
    // Внутри этого метода вместо s используем sInst Класс InstrumentedSet называется классом-оболочкои (wrapper), поскольку Каждый экземпляр
    InstrumentedSet является оболочкой для другого экземпляра Set. Он также известен как шаблон
    Decorator декоратор) [Саmmа95, стр. 175], класс InstrumentedSet "украшает" Set, добавляя ему новые функции. Иногда сочетание композиции и переадресации ошибочно называют делегuрованuем
    (delegation). Однако формально назвать это делегированием нельзя, если только объект-оболочка не передает себя "обернутому" объекту [Саmmа95, стр.
    Недостатков у классов-оболочек немного. Первый связан стем, что классы-оболочки не приспособлены для использования в схемах с обратным вызовом (callback framework), где один объект передает другому объекту ссылку на самого себя для последующего вызова (callback - обратный вызов. Поскольку обернутый объект не знает о своей оболочке, он передает ссылку на самого себя (this), и, как следствие, обратные вызовы минуют оболочку. Это называется проблемои самоидентификации (SELF ре) [Lieberman86]. Некоторых разработчиков беспокоит влияние методов переадресации на производительность системы, а также влияние объектов-оболочек на расход памяти. На практике жени один из этих факторов не оказывает существенного влияния. Писать методы переадресации несколько утомительно, однако это частично компенсируется тем, что вам нужно создавать лишь один конструктор. Наследование уместно только в тех случаях, когда подкласс действительно является подтипом
    (subtype) суперкласса. Иными словами, класс В должен расширять класс А только тогда, когда между двумя этими классами существует отношение типа "является. Если вы хотите сделать класс В расширением класса А, задайте себе вопрос "Действительно ли каждый В является А" Если вы не можете суверенностью ответить на этот вопрос утвердительно, то Вне должен расширять А. Если же ответ отрицательный, часто это оказывается, что В должен иметь закрытый от всех экземпляр Аи предоставлять при этом меньший по объему и более простой АРА не является необходимой частью В, это лишь деталь его реализации. В библиотеках для платформы Java имеется множество очевидных нарушений этого принципа. Например, стек не является вектором, соответственно класс Stack не должен быть расширением класса Vector. Точно также список свойств не является хэш-таблицей, а потому класс Properties не должен расширять Hashtable. В обоих случаях более уместной была бы композиция. Используя наследование там, где подошла бы композиция, вы безо всякой необходимости раскрываете детали реализации. Получающийся при этом АР привязывает вас к первоначальной реализации, навсегда ограничивая производительность вашего класса. Более серьезно то, что демонстрируя внутренние элементы класса, вы позволяете клиенту обращаться к ним напрямую. Самое меньшее это может привести к запутанной семантике. Например, если р ссылается на экземпляр класса Properties, тор) может давать совсем другие результаты, чем р. get(key): старый метод учитывает значения по умолчанию, тогда как второй метод, унаследованный от класса Hashtable, этого не делает. И самое серьезное напрямую модифицируя. суперкласс, клиент получает возможность разрушать инварианты подкласса. В случае с классом Properties разработчики рассчитывали, что в качестве ключей и значений можно будет применять только строки, однако прямой доступ к базовому классу H.ashtable позволяет обходить это условие. Как только указанный инвариант нарушается, пользоваться другими элементами АР для класса Properties (методами load и store) становится невозможно. Когда эта проблема была обнаружена, исправлять что-либо было слишком поздно, поскольку появились клиенты, работа которых Зависит от возможности применения ключей и значений, не являющихся строками.
    Последняя группа вопросов, которые вы должны рассмотреть, прежде чем решиться использовать наследование вместо композиции есть ли в АР того Класса, который вы намереваетесь расширять, какие-либо изъяны если есть, тоне волнует ли вас то обстоятельство, что эти изъяны перейдут в АР вашего класса Наследование копиру

    т любые дефекты в АР суперкласса, тогда как композиция ПОзволяет разработать новый АР, который скрывает эти недостатки. Подведем итоги. Наследование является мощным инструментом, но оно же создает проблемы, поскольку нарушает принцип инкапсуляции. Пользоваться им МОжно лишь в том случае, когда между суперклассом и подклассом существует реальная связь "тип/подтип". Но даже в этом случае применение наследования может сделать программу ненадежной, особенно если подкласс и суперкласс принадлежат к разным пакетам, асам суперкласс не предназначен для расширения. Для устранения Этой ненадежности вместо наследования используйте композицию и переадресацию, особенно когда для' реализации класса-оболочки имеется подходящий интерфейс. Классы-оболочки не только надежней подклассов, но и имеют большую мощность. Проектируйте bbи документируйте наследование либо запрещайте его Статья предупреждает вас об опасностях создания подклассов для "чужого" класса, наследование которого не предполагалось и не было документировано. Что же означает "класс, спроектированный и документированный для наследования
    Во-первых, требуется четко документировать последствия переопределения каждого

    1   ...   5   6   7   8   9   10   11   12   ...   25


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