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

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

  • // Пустое поле final заполняется конструктором

  • 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
    страница10 из 25
    1   ...   6   7   8   9   10   11   12   13   ...   25
    метода в этом классе. Иными словами, для класса должно быть документировано, какие из переопределяемых методов он использует сам (self-use): для каждого открытого или защищенного метода, каждого конструктора в документации должно быть указано, какие переопределяемые методы он вызывает, в какой последовательности, а также каким образом результаты их вызова влияют на дальнейшую обработку. (Под переопределяемостью (overridable) метода здесь подразумевается То, что он является неокончательным, а также что он либо открытый, либо защищенный) В общем, в документации должны быть отражены все условия, при которых класс может вызвать переопределяемый метод. Например, вызов может поступать из фонового потока или от статического метода-инициализатора. По соглашению, метод, который сам вызывает переопределяемые методы, должен содержать описание этих обращений в конце своего dос-комментария. Такое Описание начинается с фразы "This implementation". Эту фразу не следует использовать лишь для того, чтобы показать, что поведение метода может меняться от версии к версии. Она подразумевает, что следующее описание будет касаться внутренней работы данного метода. Приведем пример, взятый из спецификации класса java.util.AbstractCollection: publlic boolean remove(Object о)
    Удаляет изданной коллекции один экземпляр указанного элемента, если таковой имеется необязательная операция. Или более формально удаляет элементе, такой, что (о == null
    ? е == null : о, при условии, что в коллекции содержится один или несколько таких элементов. Возвращает значение true, если в коллекции присутствовал указанный элемент или, что то ж самое, если в результате этого вызова произошло изменение коллекции. В данной реализации организуется цикл по коллекции с поиском заданного элемента. Если элемент найден, он удаляется из коллекции с помощью метода remove, взятого у итератора. Метод iterato r коллекции возвращает объект итератора. Заметим, что если у итератора не реализован метод гетоуе, то данная реализация инициирует исключительную ситуацию Unsuppo rtedOpe rationException. Приведенное описание не оставляет сомнений в том, что переопределение метода iterator повлияет на работу метода гетоуе. Более того, в ней точно указано, каким образом работа экземпляра Iterator, возвращаемого методом iterator, будет влиять ii работу метода гетоуе. Сравните это с ситуацией, рассмотренной в статье 14, когда программист, создающий подкласс для HashSet, просто не мог знать, повлияет переопределение метода add на работу метода addAll. Но разве это не нарушает авторитетное мнение, что хорошая документация API должна описывать, что делает данный метода не то, как он это делает Конечно, нарушает Это печальное следствие того обстоятельства, что наследование нарушает принцип инкапсуляции. Для того чтобы в документации к классу показать, что его можно наследовать безопасно, вы должны описать детали реализации, которые в других случаях можно было бы оставить без уточнения. Проектирование наследования не исчерпывается описанием того, как класс использует сам себя, для того чтобы программисты могли писать полезные подклассы, не прилагая чрезмерных усилий, от класса может потребоваться создание механизма для диагностирования своей собственной внутренней деятельности в виде правильно выбранных защищенных методов или, в редких случаях, защищенных полей. Например, рассмотрим метод removeRange из класса j ava. util. Abst ractList: protected void removeRange(int fromlndex, int tolndex) Удаляет из указанного списка все элементы, чей индекс попадает в интервал от fromlndex включительно) до tolndex исключая. Все последующие элементы сдвигаются влево (уменьшается их индекс. Данный вызов укорачивает список
    ArrayList на
    (tolndex - fromlndex) элементов. (Если tolndex
    == fromlndex, процедура ни на что не влияет) Этот метод используется процедурой clea r как в самом списке, таки в его подсписках (subList - подмножество из нескольких идущих подряд элементов- Прuм. пер. При переопределении этого метода, дающем доступ к деталям реализации списка, можно значительно повысить производительность операции очистки как для списка, таки для его подсписков.
    В данной реализации итератор списка ставится переда затем в цикле делается вызов
    Listrterator. next, за которым следует
    Listlterator. remove. Итак до тех пор, пока полностью не будет удален указанный диапазон. Примечание если время выполнения операции
    Listlterator. гетоуе зависит от числа элементов в списке линейным образом, тов данной реализации зависимость является квадратичной. Параметры
    Fromlndex индекс первого удаляемого элемента индекс tolndex последнего удаляемого элемента
    Описанный метод не представляет интереса для конечных пользователей реализации
    List. Он служит только для того, чтобы облегчить реализацию в подклассе быстрого метода очистки подсписков. Если бы метод removeRange отсутствовал, в подклассе пришлось бы довольствоваться квадратичной зависимостью для метода clear, вызываемого для подсписка, либо полностью переписывать весь механизм subList - задача не из легких Как же решить какие из защищенных методов и полей мощно раскрывать при построении класса, предназначенного для наследования К сожалению, чудодейственного рецепта здесь не существует. Лучшее, что можно сделать- это выбрать самую приемлемую гипотезу и проверить ее на практике, написав несколько подклассов. Вы должны предоставить клиентам минимально возможное число защищенных методов и полей, поскольку каждый из них связан с деталями реализации. С другой стороны, их количество не должно быть слишком малым, поскольку отсутствие защищенного метода может сделать класс практически негодным для наследования. Готовя к наследованию класс, который, по-видимому, получит широкое распространение, учтите, что вы навсегда задаете схему использования классом самого себя, а также реализацию, неявно представленную защищенными методами и полями. Такие обязательства могут усложнять или дaжe делать невозможным дальнейшее улучшение Производительности и функциональных возможностей в будущих версиях класса. Заметим также, что специальные описания, обязательные для организации наследования, усложняют обычную документацию, которая предназначена для программистов, создающих экземпляры вашего класса и использующих их методы. Что же касается собственно документации, то лишь немногие инструменты и правила комментирования способны отделить документацию обычного АР от той информации, которая представляет интерес только для программистов, создающих подклассы.
    Есть лишь несколько ограничений, которым обязан соответствовать класс, чтобы его наследование стало возможным. Конструкторы класса не должны вызывать переопределяемые методы, непосредственно или опосредованно. Нарушение этого правила может привести к аварийному завершению программы. Конструктор суперкласса выполняется прежде конструктора подкласса, а потому переопределяющий метод в подклассе будет вызываться перед запуском конструктора этого подкласса. И если переопределенный метод зависит от инициализации, которую осуществляет конструктор подкласса, то этот метод будет работать совсем не так, как ожидалось. для пояснения приведем пример класса, нарушающеГ9 это правило public class г {
    // Ошибка конструктор вызывает переопределяемый метод
    public г) { me();} public void m() {
    } Представим подкласс, в котором переопределяется метод т, неправомерно вызываемый единственным конструктором класса г final class Sub extends г г final Date date;
    // Пустое поле final заполняется конструктором
    Sub() { date = new Date();
    }
    // Переопределяет метод г, используемый конструктором
    Super() publiC void m() {
    System.out.println(date); }
    public static void г г) {
    Sub s = new Sub(); s.m(); Предполагается, что эта программа напечатает текущую дату дважды, однако в первый раз она выводит null, поскольку метод т вызывается конструктором г) прежде, чем конструктор Sub() получает возможность инициализировать поле даты. Отметим, что данная программа видит поле final в двух разных состояниях.
    Реализация интерфейсов Cloneable и Se г при проектировании наследования создает особые трудности. Вообще говоря, реализовывать какой-либо из этих интерфейсов в классах, предназначенных для наследования, не очень хорошо уже потому, что они создают большие сложности для программистов, расширяющих этот класс. Есть, однако, специальны приемы, которые можно использовать стем, чтобы обеспечить передачу реализации этих интерфейсов в подкласса не заставлять его реализовывать их заново. Эти приемы описаны в статьях 10 и 54. Если вы решите реализовать интерфейс Cloneable или г в классе, предназначенном для наследования, то учтите, что, поскольку методы clone и г edt в значительной степени работают как конструкторы, к ним применимо тоже самое ограничение ни методу clone, ни методу readObject не разрешается вызывать переопределяемый метод, непосредственно или опосредованно. В случае с методом readObj ect переопределенный метод будет выполняться перед десериализацией состояния подкласса. Что же касается метода clone, то переопределенный метод будет выполняться прежде, чем метод сlопе в подклассе получит возможность установить состояние клона. В обоих случаях, по-видимому, последует сбой программы. При работе с методом clone такой сбой может нанести ущерб и клонируемому объекту, и клону.
    И, наконец, если вы решили реализовать интерфейс Serializable в классе, предназначенном для наследования, ау этого класса есть метод readResolve или wri teReplace, то вы должны делать этот метод незакрытым, а защищенным. Если эти методы будут закрытыми, то подклассы будут молча игнорировать их. Это еще один случай, когда для обеспечения наследования детали реализации класса становятся частью его АР. Таким образом, проектирование класса для наследования накладывает на него существенные ограничения. В ряде ситуаций это необходимо делать, например, когда речь идет об абстрактных классах, содержащих "скелетную реализацию" интерфейса (статья 16). В других ситуациях этого делать нельзя, например, в случае с неизменяемыми классами (статья 13). А как же обычные неабстрактные классы По традиции, они не являются окончательными, не предназначаются для порождения подклассов, не имеют соответствующего описания. Однако подобное положение дел опасно. Каждый раз, когда в такой класс вносится изменение, существует вероятность того, что перестанут работать классы клиентов, которые расширяют этот класс. Это непросто теоретическая проблема. Нередко сообщения об ошибках в подклассах возникают после того, как в неокончательном, неабстрактном классе, не предназначавшемся для наследования и не имевшем нужного описания, поменялось содержимое. Наилучшим решением этой проблемы является запрет на создание подклассов для тех классов, которые небыли специально разработаны и не имеют требуемого описания для безопасного выполнения данной операции. Запретить создание подклассов можно двумя способами. Более простой заключается в объявлении класса как окончательного (final). Другой подход состоит в том, чтобы сделать все Конструкторы класса закрытыми или доступными лишь в пределах пакета, а вместо них создать открытые статические методы генерации. Такая альтернатива, дающая возможность гибко использовать класс внутри подкласса, обсуждалась в статье 13. Приемлем любой из указанных подходов.
    Возможно, этот совет несколько сомнителен, поскольку так много программистов выросло с привычкой создавать для обычного неабстрактного класса подклассы лишь для того, чтобы добавить новые возможности, например средства контроля, оповещения и синхронизации, либо наоборот, чтобы ограничить его функциональные возможности. Если класс реализует некий интерфейс, в котором отражена его сущность, например Set, List или Мар, то у вас не должно быть сомнений по поводу запрета подклассов. Шаблон класса-оболочки (wrapper class), описанный в статье 14, создает превосходную альтернативу наследованию, используемому всего лишь для изменения функциональности. Если только неабстрактный класс не реализует стандартный интерфейс, то, запретив наследование, вы можете создать неудобство для некоторых программистов. Если вы чувствуете, что должны позволить наследование для этого класса, то один из возможных подходов заключается в следующем убедитесь в том, что класс не использует каких-либо собственных переопределяемых методов, и отразите этот факт в документации. Иначе говоря, полностью исключите использование переопределяемых методов самим классом. Сделав это, вы создадите класс, достаточно безопасный для создания подклассов, поскольку переопределение метода не будет влиять на работу других методов в классе. Вы можете автоматически исключить использование, классом собственных переопределяемых методов, оставив прежними его функции. Переместите тело каждого переопределяемого метода в закрытый вспомогательный метода затем поместите в каждый переопределяемый метод вызов своего закрытого вспомогательного метода. Наконец, каждый вызов переопределяемого метода в классе замените прямым вызовом закрытого соответствующего вспомогательного метода. Предпочитайте интерфейсы абстрактным классам.
    В языке программирования Java предоставлены два механизма определения типов, которые допускают множественность реализаций интерфейсы и абстрактные классы. Самое очевидное различие между этими механизмами заключается в том, что в абстрактные классы можно включать реализацию некоторых методов, для интерфейсов это запрещено. Более важное отличие связано стем, что для реализации типа, определенного неким. Абстрактным классом, класс должен стать подклассом этого абстрактного класса. С другой стороны, реализовать интерфейс может любой класс, независимо от его места в иерархии классов, если только он отвечает общепринятым соглашениями в нем есть все необходимые для этого методы. Поскольку в языке Java не допускается множественное наследование, указанное требование для абстрактных классов серьезно ограничивает их использование при определении типов.
    Имеющийся класс несложно подогнать под реализацию нового интерфейса. Все, что для этого нужно- добавить в класс необходимые методы, если их еще нет, и внести в декларацию класса пункт о реализации. Например, когда платформа Java была дополнена интерфейсом г, многие существовавшие классы были перестроены под его реализацию. С другой стороны, уже имеющиеся классы, вообще говоря, нельзя перестраивать для расширения нового абстрактного класса. Если вы хотите, чтобы два класса расширяли один и тот же абстрактный класс, вам придется поднять этот абстрактный класс в иерархии типов настолько высоко, чтобы прародитель обоих этих классов стал его Подклассом. К сожалению, это вызывает значительное нарушение иерархии типов, заставляя всех потомков общего предка расширять новый абстрактный класс независимо оттого, целесообразно это или нет. Интерфейсы идеально подходят для создания дополнений (mixin). Помимо своего "первоначального типа, класс может реализовать некий дополнительный тип (mixin), объявив о том, что в нем реализован дополнительный функционал. Например, г является дополнительным интерфейсом, который дает классу возможность декларировать, что его экземпляры упорядочены по отношению к другим, сравнимым сними объектам. Такой интерфейс называется п, поскольку позволяет к первоначальным функциям некоего типа примешивать (mixed in) дополнительные функциональные возможности. Абстрактные классы нельзя использовать для создания дополнений по той же причине, по которой их невозможно встроить в уже имеющиеся классы класс не может иметь более одного родителя, ив иерархии классов нет подходящего места, куда можно поместить mixin. Интерфейсы позволяют создавать структуры типов без иерархии. Иерархии типов прекрасно подходят для организации одних сущностей, но зато другие сущности аккуратно уложить в строгую иерархию типов невозможно. Предположим, что у нас один интерфейс представляет певца, а другой - автора песен public interface Singer {
    AudioClip Sing(Song s); }
    publlic interface Songwriter {
    Song compose(boolean hit); В жизни некоторые певцы являются и авторами песен. Поскольку для определения этих типов мы использовали не абстрактные классы, а интерфейсы, то одному классу никак не запрещается реализовывать оба интерфейса Singer и Songwriter. В действительности мы можем определить третий интерфейс, который расширяет оба Интерфейса и добавляет новые методы, соответствующие сочетанию public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum(); void actSensitive(); }
    Такой уровень гибкости нужен не всегда. Если же он необходим, интерфейсы становятся спасительным средством. Альтернативой им является раздутая иерархия классов, которая содержит отдельный класс для каждой поддерживаемой ею комбинации атрибутов. Если в системе имеется п атрибутов, то существует 2 в степени n сочетаний, которые, возможно, придется поддерживать. Это называется комбинаторным взрывом (combinatorial explosion). Раздутые иерархии классов могут привести к созданию раздутых классов, содержащих массу методов, отличающихся друг от друга лишь типом аргументов, поскольку в такой иерархии классов не будет типов, отражающих общий функционал. Интерфейсы позволяют безопасно и мощно наращивать функциональность, используя идиому клacca-оболочки, описанную в статье 14. Если же для определения типов выпри меняете абстрактный класс, то вы не оставляете программисту, желающему добавить новые функциональные возможности, иного выбора, кроме как использовать наследование. Получаемые в результате классы будут не такими мощными и не такими надежными, как классы-оболочки. Хотя в интерфейсе нельзя хранить реализацию методов, определение типов с помощью интерфейсов не мешает оказывать программистам помощь в реализации класса. Вы можете объединить преимущества интерфейсов и абстрактных классов, сопроводив каждый предоставляемый вами нетривиальный интерфейс. абстрактным классом с наброском (скелетом) реализации (skeletal implementation class). Интерфейс по-прежнему будет определять типа вся работа по его воплощению ляжет на скелетную реализацию. По соглашению, скелетные реализации носят названия вида п, где п - это имя реализуемого ими интерфейса. Например, в архитектуре Collections Framework представлены скелетные реализации для всех основных интерфейсов коллекций AbstractCollection,
    AbstractSet, AbstractList и AbstractMap. При правильном проектировании скелетная реализация позволяет программистам без труда создавать свои собственные реализации ваших интерфейсов. В качестве примера приведем статический метод генерации, содержащий завершенную, полнофункциональную реализацию интерфейса List:
    // Адаптер интерфейса List для массива целых чисел (int) static List intArrayAsList(final int[] а) if (а = = null) throw new NullPointerException(); return new AbstractList() { public Object get(int i) { return new Integer(a[i]); }
    public int size() { return a.length; }
    public Object set(int i, Object о) {
    int oldVal = a[i]; a[i] = «Integer)o).intValue(); return new Integer(oldVal);
    }
    };
    }
    Если принять во внимание все, что делает реализация интерфейса
    List, то этот пример демонстрирует всю мощь скелетных реализаций. Кстати, пример является адаптером

    (Adapter)
    [Сатта95, стр. 139], который позволяет представить массив int в виде списка экземпляров
    Integer.
    Из-за всех этих преобразований из значений int в экземпляры
    Integer и обратно производительность метода не очень высока. Отметим, что здесь приведен лишь статический метод генерации, сам же класс является недоступным анонимным lUIассом статья 18), спрятанным внутри статического метода генерации. Достоинство скелетных реализаций заключается в том, что они оказывают помощь в реализации абстрактного класса, не налагая при этом строгих ограничений, как это имело бы место, если бы для определения типов использовались абстрактные классы. для большинства программистов, реализующих интерфейс, расширение скелетной реализации - это очевидный, хотя и необязательный выбор. Если имеющийся класс нельзя заставить расширять скелетную реализацию, он всегда может реализовать представленный интерфейс сам. Более того, скелетная реализация помогает в решении стоящей перед разработчиком задачи. Класс, который реализует данный интерфейс, может переадресовывать вызов метода, указанного в интерфейсе, содержащемуся внутри его экземпляру закрытого класса, расширяющего скелетную реализацию. Такой прием, известный как искусственное множественное наследование multiple inheritance), тесно связан с идиомой класса-оболочки статья 14). Он обладает большинством преимуществ множественного наследования и при этом избегает его подводных камней. Написание скелетной реализации - занятие относительно простое, хотя иногда и скучное. Во- первых, вы должны изучить интерфейс и решить, какие из методов Являются примитивами (primitive) в терминах, в которых можно было бы реализовать остальные методы интерфейса. Эти примитивы и будут абстрактными методами в вашей скелетной реализации. После этого вы должны предоставить конкретную реализацию всех остальных методов данного интерфейса. В качестве примера приведем скелетную реализацию интерфейса Мар.
    Entry. В том виде, как это показано здесь, Класс не включен в библиотеки для платформы Java, хотя, вероятно, это следовало бы сделать.
    1   ...   6   7   8   9   10   11   12   13   ...   25


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