Effective Java tmprogramming Language GuideJ o s h u a b lo c h
Скачать 1.05 Mb.
|
// Скелетная реализация public abstract class AbstractMapEntry implements Мар. Entry { // Примитивы public abstract Object getKey(); public abstract Object getValue(); 83 // Элементы в изменяемых схемах должны переопределять этот метод publiC Object setValue(Object value) { throw пе UnsupportedOperationException(); } // Реализует основные соглашения для метода Мар.Entry.equals public boolean equals(Object о) { if (о == this) return true; if (!(o iпstапсеоf Map,Entry)) return false; Map.Entry arg = (Мар. Entry)o; return eq(getKey(), arg.getKey()) && eq(getValue(), arg.getValue()); private static boolean eq(Object 01, Object 02) { return (01 == null ? 02 == null : 01.equals(02)); } // Реализует основные соглашения для метода Мар. Entry.hashCode public int hashCode() { return (getKey() == п ? 0 : getKey().hashCode()) (getValue() == null ? 0 : getValue().hashCode()); Поскольку скелетная реализация предназначена для наследования, вы должны выполнять все указания по разработке и документированию, представленные в статье 15. для краткости в предыдущем примере опущены комментарии к документации, однако качественное документирование для скелетных реализаций абсолютно необходимо. При определении типов, допускающих множественность реализаций, абстрактный класс имеет одно огромное преимущество перед интерфейсом абстрактный класс совершенствуется гораздо легче, чем интерфейс. Если в очередной версии вы захотите добавить в абстрактный класс новый метод, вы всегда сможете представить законченный метод с правильной реализацией, предлагаемой по умолчанию. После этого новый метод появится у всех имеющихся реализаций данного абстрактного класса. Для интерфейсов этот прием не работает. Вообще говоря, в открытый интерфейс невозможно добавить какой-либо метод, не разрушив все имеющиеся программы, которые используют этот интерфейс. В классе, ранее реализовавшем этот интерфейс, новый метод не будет представлен, и, как следствие, класс компилироваться не будет. Ущерб можно несколько уменьшить, если новый метод добавить одновременно ив скелетную реализацию, ив интерфейс, однако по- настоящему это не решит проблемы. Любая реализация интерфейса, не наследующая скелетную реализацию, все равно работать не будет. Следовательно, открытые интерфейсы необходимо проектировать аккуратно. Как толрко интерфейс создан и повсюду реализован, поменять его почти невозможно. В действительности его нужно правильно строить с первого же раза. Если в Интерфейсе есть незначительньый изъян, он уже всегда будет раздражать иваси пользователей. Если же интерфейс имеет серьезные дефекты, он способен погубить АР. Самое лучшее, что можно предпринять при создании нового интерфейса- заставить как можно больше программистов реализовать этот интерфейс самыми разнообразными способами, прежде чем он будет "заморожен. Это позволит вам найти все ошибки, пока у вас еще есть возможность их исправить. Подведем итоги. Интерфейс обычно наилучший способ определения типа, который допускает несколько реализаций. Исключением из этого правила является случай, когда легкости совершенствования придается большее значение, чем гибкости и эффективность. При этом для определения типа вы должны использовать абстрактный классно только если вы осознаете и готовы принять все связанные с этим ограничения. Если вы предоставляете сложный интерфейс, вам следует хорошо подумать над созданием скелетной реализации, которая будет сопровождать его. Наконец, вы должны проектировать открытые интерфейсы с величайшей тщательностью, всесторонне проверяя их путем написания многочисленных реализаций. Используйте интерфейсы только для определения типов Если класс реализует интерфейс, то этот интерфейс может служить как некий тип, который можно использовать для ссылки на экземпляры этого класса. То, что класс реализует некий интерфейс, должно говорить нечто о том, что именно клиент может делать с экземплярами этого класса. Создавать интерфейс для каких-либо иных целей неправомерно. Среди интерфейсов, которые не отвечают этому критерию, числится так называемый интерфейс констант (constant interface). Он не имеет методов и содержит исключительно поля static final, передающие константы. Классы, в которых эти константы используются, реализуют данный интерфейс для того, чтобы исключить необходимость в добавлении к названию констант названия класса. Приведем пример // Шаблон интерфейса констант - не использоваты! public interface РhуsiсаlСопstапts // Число Авогадро (моль) static final double AVOGADROS NUMBER = е 85 // Постоянная Больцмана (Дж/К) static final double е // Масса электрона (кг) static final double ELECTRON е Шаблон интерфейса констант представляет собой неудачный вариант использования интерфейсов. Появление внутри класса каких-либо констант является деталью реализации. Реализация интерфейса констант приводит к утечке таких деталей во внешний АР данного класса. То, что класс реализует интерфейс констант, для пользователей класса не представляет никакого интереса. На практике это может даже сбить их столку. Хуже того, это является неким обязательством если в будущих версиях класс поменяется так, что ему уже не будет нужды использовать данные константы, он все равно должен будет реализовывать этот интерфейс для обеспечения совместимости на уровне двоичных кодов (binary compatibility). Если же интерфейс констант реализует неокончательный класс константами из этого интерфейса будет засорено пространство имен всех его подклассов. В библиотеках для платформы Java есть несколько интерфейсов с константами, например jауа.io.ObjectSt reamConstants. Подобные интерфейсы нужно воспринимать как отклонение от нормы, и подражать им не следует. Для передачи констант существует несколько разумных способов. Если константы сильно связаны с имеющимся классом или интерфейсом, вы должны добавить их непосредственно в этот класс или интерфейс. Например, все классы-оболочки в библиотеках платформы Java, связанные с числами, такие как Integer и Float, предоставляют константы Ми. Если же константы лучше рассматривать как члены перечисления, то передавать их нужно с помощью класса перечисления (статья 21). В остальных случаях вы должны передавать константы с помощью вспо- могательнoго класса (utility c!ass), не имеющего экземпляров (статья 3). Представим вариант вспомогательного класса для предыдущего примера PhysicalConstants: // Вспомогательный класс для констант public class PhysicalConstants { private PhysicalConstants() { } // Предотвращает появление экэемпляра public static final double AVOGADROS_NUMBER ее =9.10938188е-31; } Хотя представление PhysicalConstants в виде вспомогательного класса требует, чтобы клиенты связывали названия констант с именем класса, это небольшая цена за получение осмысленного API. Возможно, что со временем язык Java позволит осуществлять импорт статических полей. Пока же вы можете сократить размер программы, поместив часто используемые константы в локальные переменные или закрытые статические поля, например р static final double Р = Math. Таким образом, интерфейсы нужно использовать только для определения типов. Их не 'следует применять для передачи констант. Предпочитайте статические классы члены нес тати чески м Класс называется вложенным (nested), если он определен внутри другого класса. Вложенный класс должен создаваться только для того, чтобы обслуживать окружающий его класс. Если вложенный класс оказывается полезен в каком-либо ином контексте, он должен стать классом верхнего уровня. Существуют четыре категории вложенных классов статический класс-член (static member class), нестатический класс-член (nonstatic member class), анонимный класс (и class) и локальный класс (local class). За исключением первого, остальные категории классов называются внутренними (inner class). В этой статье рассказывается о том, когда и какую категорию вложенного класса нужно использовать и почему. Статический класс-член - это простейшая категория вложенного класса. Лучше всего рассматривать его как обычный класс, который декларирован внутри другого класса и имеет доступ ко всем членам окружающего его класса, даже к закрытым. Статический класс-член является статическим членом своего внешнего класса и подчиняется тем же правилам доступа, что и остальные статические члены. Если он декларирован как закрытый, доступ к нему имеет лишь окружающий его класс, и т. д. Водном из распространенных вариантов статический класс-член используется как открытый вспомогательный класс, который пригоден для применения, только когда есть внешний класс. Например, рассмотрим перечисление, описывающее операции, которые может выполнять калькулятор статья 21). Класс Operation должен быть открытым статическим классом-членом класса Calculator. Клиенты класса Calculator могут ссылаться на операции, выполняемые калькулятором, используя такие имена, как Calculator.Ореration.PLUS или Calculator.Ореration.MINUS. Этот вариант приводится ниже. Сточки зрения синтаксиса, единственное различие между статическими и нестатическими классами-членами заключается в том, что в декларации статических Классов-членов присутствует модификатор static. Несмотря на свою синтаксическую схожесть, эти две категории вложенных классов совершенно разные. Каждый экземпляр нестатического члена-класса неявным образом связан с содержащим его экземпляром класса-контеинера (enclosing instance). Из метода в экземпляре нестатического класса- члена можно вызывать методы содержащего его экземпляра, либо, используя специальную конструкцию this [JLS 15.8.4], можно получить ссылку на включающий экземпляр. Если экземпляр вложенного класса может существовать в отрыве от экземпляра внешнего класса, то вложенный класс не может быть нестатическим классом-членом: нельзя создать экземпляр нестатического класса- члена, не создав включающего его экземпляра. Связь между экземпляром нестатического класса-члена и включающим его эк- земпляроМ устанавливается при создании первого, и после этого поменять ее нельзя. Обычно эта связь задается автоматически путем вызова конструктора нестатического класса-члена из экземпляра метода во внешнем классе. Иногда можно установить связь вручную, используя выражение enclosinglnstance.newMemberClass(args). Как можно предположить, эта связь занимает место в экземпляре нестатического класса-члена и увеличивает время его создания. Нестатические классы-члены часто используются для определения адаптера (Adapter) [Сатта95, стр. 139], при содействии которого экземпляр внешнего класса воспринимается своим внутренним классом как экземпляр некоторого класса, не имеющего к нему отношения. Например, в реализациях интерфейса Мар нестатические классы-члены обычно применяются для создания представлении / (оллекции (collection view), возвращаемых методами keySet, entrySet и values интерфейса Мар. Аналогично, в реализациях интерфейсов коллекций, таких как Set и List, нестатические классы-члены обычно используются для создания итераторов // Типичный вариант использования нестатического класса-члена public class MySet extends AbstractSet { // Основная часть класса опущена publiC Iterator iterator() { return new Mylterator(); } private class Mylterator implements Iterator Если вы объявили класс-член, которому ненужен доступ к экземпляру содержащего его класса, не забудьте поместить в соответствующую декларацию модификатор static стем, чтобы сделать этот класс-член статическим. Если вы не установите модификатор static, каждый экземпляр класса будет содержать ненужную ссылку на внешний объект. Поддержание этой связи требует и времени, и места, ноне приносит никакой пользы. Если же вам когда-нибудь потребуется разместить в памяти экземпляр этого класса без окружающего его экземпляра, вы не сможете это сделать, так как нестатические классы-члены обязаны иметь окружающий их экземпляр. Закрытые статические классы-члены обычно должны представлять составные части объекта, доступ к которым осуществляется через внешний класс. Например, рассмотрим экземпляр класса Мар, который сопоставляет ключи и значения. Внутри экземпляра Мар для каждой пары ключ/значение обычно создается объект Entry. Хотя каждая такая запись ассоциируется со схемой, клиенту не надо обращаться к собственным методам этой записи (geyKey, getValue и setValue). Следовательно, использовать нестатические классы-члены для представления отдельных записей в схеме Мар было бы расточительностью, самое лучшее решение - закрытый статический класс-член. Если в декларации этой записи вы случайно пропустите модификатор statlc, схема будет работать, но каждая запись будет содержать ненужную ссылку на общую схему, напрасно занимая время и место в памяти. Вдвойне важно правильно сделать выбор между статическими нестатическим классом-членом, когда этот класс является открытым или защищенным членом класса, передаваемого клиентам. В этом случае класс-член является частью внешнего API, ив последующих версиях уже нельзя будет сделать нестатический класс-член статическим, не потеряв совместимости на уровне двоичных кодов. Анонимные классы в языке программирования Java непохожи ни на какие другие. Анонимный класс не имеет имени. Он не является членом содержащего его класса. Вместо того чтобы быть декларированным с остальными членами класса, он одновременно декларируется и порождает экземпляр в момент использования. Анонимный класс можно поместить в любом месте программы, где разрешается применять выражения. В зависимости от местоположения анонимный класс ведет себя как статический либо как нестатический класс-член: в нестатическом контексте появляется окружающий его экземпляр. Применение анонимных классов имеет несколько ограничений. Поскольку анонимный класс одновременно декларируется и порождает экземпляр, его можно использовать, только когда его экземпляр должен порождаться лишь водном месте программы. Анонимный класс не имеет имени, поэтому может применяться только в том случае, если после порождения экземпляра ненужно на него ссылаться. Анонимный класс обычно реализует лишь методы своего интерфейса или суперкласса. Он не объявляет каких-либо новых методов, так как для доступа к ним нет поименованного типа. Поскольку анонимные классы стоят среди выражений, они должны быть очень короткими, возможно, строк двадцать или меньше. Использование более длинных анонимных Классов может усложнить программу сточки зрения ее чтения. Анонимный класс обычно служит для создания объекта функции (function object), такого как экземпляр класса Comparator. Например, при вызове следующего метода строки в массиве, будут отсортированы по их длине // Типичный пример использования анонимного класса Arrays.sort(args, new Comparator() { public int compare(Object o1, Object o2) { return ((String)o1).length() – ((String)o2).length(); } ); Другой распространенный случай использования анонимного класса - создание объекта процесса (process object), такого как экземпляры классов Thread, Runnable или ТiтerTask. Третий вариант в статическом методе генерации (см. метод intArrayAsList в статье 16). Четвертый вариант инициализация открытого статического поля final, которое соответствует сложному перечислению типов, когда для каждого экземпляра в перечислении требуется отдельный подкласс (см. класс Operation в статье 21). Если, как было рекомендовано ранее, класс Operation будет статическим членом класса Calculator, то отдельные константы класса Operation окажутся дважды вложенными классами // Типичный пример использования открытого // статического класса-члена public class Calculator { public static abstract class Operation{ private final String name; Operation(String пате) { this.name = name; } public String toString() { return this.name; } // Выполняет арифметическую операцию, представленную данной константой abstract double eval(double х, double у // Дважды вложенные анонимные классы public static final Operation PLUS = new Operation("+"){ double eval(double х, double уху х, double у) {геturn х - уху х * уху х / у } }; } // Возвращает результат указанной операции public double calculate(double хору геturn op.eval(x, у } } Локальные классы, вероятно, относятся к наиболее редко используемой из четырех категорий вложенных классов. Локальный класс можно декларировать везде, где разрешается декларировать локальную переменную, ион подчиняется тем же самым правилам видимости. Локальный класс имеет несколько признаков, объединяющих его с каждой из трех других категорий вложенных классов. Как и классы-члены, локальные классы имеют имена и могут использоваться многократно. Как и анонимные классы, они имеют окружающий их экземпляр тогда и только тогда, когда применяются в нестатическом контексте. Как и анонимные классы, они должны быть достаточно короткими, чтобы не мешать удобству чтения метода или инициализатора, в котором они содержатся. Подведем итоги. Существуют четыре категории вложенных классов, каждая из которых занимает свое место. Если вложенный класс должен быть виден за пределами одного метода или он слишком длинный для того, чтобы его можно было удобно разместить в границах метода, используйте класс- член. Если каждому экземпляру класса-члена необходима ссылка на включающий его экземпляр, делайте его нестатическим, в остальных случаях он должен быть статическим. Предположим, что класс находится внутри метода. Если вам нужно создавать экземпляры этого класса только водном месте программы и уже есть тип, который характеризует это класс, сделайте его анонимным классом. В противном случае это должен быть локальный класс. Глава 5 3 а мена кон стр к ц и й на языке С Между языками программирования Си много общего, однако в Java отсутствует несколько конструкций языка с. В большинстве случаев совершенно очевидно, почему таили иная конструкция языка С опущена и как без нее обойтись. В этой главе предлагается замена для ряда конструкций языка С, альтернатива которым не столь очевидна. Общая идея, объединяющая статьи этой главы, заключается в том, что все непринятые конструкции скорее были ориентированы на данные, назвать их объектно-ориентированными нельзя. Язык программирования Java обеспечивает мощную систему типизации, и потому предлагаемая замена в полной мере использует преимущества этой системы, добиваясь более высокого качества абстракции, чем имели соответствующие конструкции С. Даже если вы решите пропустить эту главу, все же прочтите статью 21, в которой обсуждается шаблон перечисления, который заменяет конструкцию еnиm из языка С. Вовремя написания этой книги данный шаблон был мало известен, однако он имеет ряд преимуществ перед другими, широко используемыми сегодня методами. |