Effective Java tmprogramming Language GuideJ o s h u a b lo c h
Скачать 1.05 Mb.
|
// Перечисление, использующее порядковые номера public class Suit implements Comaprable { private final String name; // Порядковый номер следующей масти private static int nextOrdinal = 0; // Назначение порядкового номера данной масти private final int ordinal = nextOrdinal++; private Suit(String пае) { пае = пае } public Stгiпg tоStгiпg() {return пае } public по) { геturn огdinаl – ((Suit)О).огdiпаl; public String toString() { return name;} public static final Suit CLUBS= new Suit("clubs"); // трефы publiC static final Suit DIAMONDS = new Suit("diamonds"); // бубны public static final Suit HEARTS= new Suit("hearts"); // черви public static final Suit SPADES= new Suit("spades"); // пики } Поскольку константы перечисления являются объектами, вы можете помещать их в коллекции. Допустим, вы хотите, чтобы класс Suit передавал неизменяемый перечень мастей в обычном порядке. Достаточно добавить в этот класс следующие две декларации полей private static па Suit[] PRIVATE_VALUES = { CLUBS, DIAMONDS, HEARTS, SPADES }; public static final List VALUES = Соllесtiопs.unmоdifiаblеList(Аггауs.аsList(РRIVAТЕ_VALUES)); В отличие от простейшего шаблона typesafe enum, классы представленной формы, использующей порядковые номера, можно сделать сериализуемыми (serializabIe) (см. главу 10), приложив для этого минимум усилий. Недостаточно добавить в декларацию класса слова imрlеmепts Serializab'le, нужно еще предоставить метод readResolve (статья 57): private Object readResolve() throws ObjectStreamExceptin { return PRIVATE_VALUES[ordinal]; // Канонизация Этот метод, автоматически вызываемый системой сериализации, предупреждает появление в результате десериализации дублирующих констант. Это гарантирует, что каждая константа в перечислении будет представлена одним единственным объектом, и, следовательно, ненужно переопределять метод Obj ect. equals. Без этого условия метод Obj ect. equals давал бы отрицательный результат, сравнивая две равные, но неидентичные константы перечисления. Заметим, что метод readResolve ссылается на массива потому вы должны декларировать этот массив, даже если решили не предоставлять клиентам список VALUES. Заметим также, что поле пате в методе readResolve не используется, а потому его можно и даже нужно исключить из сериализованной формы. Полученный в результате класс не вполне надежен конструкторы любых вновь добавленных значений должны ставиться после уже имевшихся стем, чтобы ранее сериализованные экземпляры класса при десериализации не поменяли свое значение. Это происходит потому, что в сериализованной форме (статья 55) для константы перечисления хранится лишь ее порядковый номер. И если у константы перечисления поменяется порядковый номер, то константа, сериализованная с прежним номером, при десериализации получит новое значение. Каждой константе, которая используется только в пределах того пакета, где содержится класс перечисления, может быть сопоставлена одна или несколько ахем поведения программы. Такие схемы лучше всего реализовать в классе как методы, доступные только в пределах пакета. В результате за каждой константой перечисления выстраивается скрытый набор схем поведения, что дает возможность пакету, содержащему перечисление, правильно реагировать на получение соответствующей константы. Если класс перечисления имеет методы, работа которых кардинально меняется при замене одной константы класса на другую, то вы обязаны для каждой такой константы использовать отдельный закрытый класс либо анонимный внутренний класс. Это позволяет каждой константе иметь собственную реализацию всех таких методов и обеспечивает автоматический вызов нужной реализации. Альтернативный подход заключается в том, чтобы представить каждый такой метод как дерево ветвлений, функционирование которого зависит оттого, для какой константы был вызван метод. Подобный подход уродлив, подвержен ошибками вряд ли способен обеспечить производительность, сравнимую с автоматической диспетчеризацией методов в виртуальной машине Jаvа. Рассмотрим класс перечисления, иллюстрирующий оба приема, о которых говорилось выше. Класс Operation описывает работу калькулятора с четырьмя основными функциями. Все, что вы можете делать с константой типа Ореration за пределами пакета, в котором этот класс определен, - это вызывать методы класса Object: toString, hashCode, equals и т. д. Внутри же пакета вы можете выполнить арифметическую операцию, соответствующую константе. По-видимому, в пакете будет представлен некий объект высокого уровня, соответствующий калькулятору, который будет предоставлять клиенту один или несколько методов, получающих в качестве параметра константу типа Operation. Заметим, что сам Operation является абстрактным классом, содержащим один единственный абстрактный метод eval, который доступен только в пределах пакета и выполняет соответствующую арифметическую операцию. для каждой константы определен анонимный внутренний класс, так что для каждой константы можно определить собственную версию метода eval: // Перечисление типов, схемы поведения которого // закреплены за константами publiC abstract class Operation { private final String name; Operation(String name) { this.name = пате } public String toString() { return this.name; } Выполняет арифметическую операцию, // представленную указанной константой abstract double eval(double х, double уху х + уху х - уху х * уху х / у } } ; Вообще говоря, по производительности перечисления типов сравнимы с перечислениями целых констант. Два разных экземпляра для класса перечисления typesafe еnиm никогда не смогут представить клиенту одно и тоже значение, а потому для проВерки логического равенства используется быстрая проверка тождественности ссылок. Клиенты класса перечисления typesafe еnиm могут использовать оператор == вместо метода equals. Гарантируется, что результаты будут те же самые, асам оператор ==, возможно, будет работать быстрее. Если класс перечисления используется широко, его следует сделать классом верхНего уровня. Если работа с перечислением связана с определенным классом верхнего уровня, перечисление следует сделать статическим классом-членом указанного класса верхнего уровня (статья 18). Например, класс java. math. BigDecimal содержит набор констант перечисления типа int, соответствующих различным способам округления дробной части числа (и modes). Эти способы округления образуют полезную абстракцию, которая в сущности не связана с классом BigDecimal. Реализовать их лучше в самостоятельном классе j ava. math. RoundingMode. В результате любой программист, которому понадобились различные режимы округления, мог бы воспользовался этой абстракцией, что привело бык лучшей согласованности между различными API. Базовый шаблон перечисления типов, проиллюстрированный выше в реализациях класса Suit, зафиксирован пользователи не могут дополнять перечисление новыми элементами, поскольку у класса нет конструкторов, которые были бы доступны пользователю. Фактически это делает данный класс окончательным независимо оттого, декларирован ли он с модификатором final или нет. Как правило, это именно то, чего выждете от класса, но иногда вам необходимо сделать класс перечисления расширяемым. Например, это может потребоваться, когда вы используете класс перечисления для представления различных форматов кодирования изображений, но хотите, чтобы третьи лица могли добавлять в него поддержку новых форматов. Для того чтобы сделать класс перечисления расширяемым, создайте защищенный конструктор. Тогда другие разработчики смогут расширять этот класс и дополнять новыми константами свои подклассы. Вам ненужно беспокоиться о конфликтах между константами перечисления, как в случае применения шаблона int епит. Расширяемый вариант шаблона typesafe епит использует преимущества собственного пространства имен пакета стем, чтобы для расширяемого перечисления создать "магически управляемое" пространство имен. Различные группы разработчиков могут расширять это перечисление, ничего не зная друг о друге- конфликта между их расширениями не возникнет. Простое добавление элемента в расширяемое перечисление еще не является гарантией того, что этот элемент будет иметь полную поддержку методы, принимающие элемент перечисления, должны учитывать возможность получения элементов, неизвестных программисту. Если целесообразность использования сложного дерева ветвлений для фиксированного перечисления сомнительна, то для расширяемых перечислений это вообще смертельно, поскольку каждый раз, когда программист расширяет тип, соответствующий перечислению, ветвление само собой нарастать не будет. Один из способов решения этой проблемы заключается в том, чтобы снабдить класс перечисления всеми теми методами, которые необходимыми для описания схем, соответствующих константам этого класса. Метод, которым клиенты класса не пользуются, следует делать защищенным, однако должна оставаться возможность его переопределения в подклассах. Если такой метод не имеет приемлемой реализации, предлагаемой по умолчанию, его следует декларировать не только как защищенный, но и как абстрактный. В расширяемом классе перечисления полезно переопределить методы equals и hashCode новыми окончательными методами, которые обращаются к методам из класса Object. Тем самым гарантируется, что нив одном подклассе эти методы не будут случайно переопределены, благодаря чему все равные объекты типа перечисления будут к тому же идентичны (a.equals(b) возвращает true тогда и только тогда, когда а == Ь. // Методы, защищенные от переопределения с final boolean equals(Object that) { return super.equals(that); } public final int hashCode() { return super.hashCode(); Заметим, что представленный вариант расширения несовместим с вариантом, обеспечивающим сравнение если вы попытаетесь объединить их, то схема упорядочения элементов подкласса будет зависеть от очередности инициализации подклассов, а она может меняться от программы к программе, от запуска к запуску. Приведенный расширяемый вариант шаблона перечисления совместим с вариантом, обеспечивающим сериализацию, однако объединение этих вариантов требует некоторой осторожности. Каждый подкласс должен назначить собственные порядковые номера и использовать свой собственный метод readResolve. В сущности, каждый класс отвечает за сериализацию и десериализацию своих собственных экземпляров. Для пояснения представим вариант класса Оре ration, который был исправлен таким образом, чтобы быть и расширяемыми сериализуемым: // Сериализуемый и расширяемый класс перечисления public abstract class Operation implements Serializable { private final transient String name; protected Operation(String name) { this.name = name; } public static Operation PLUS = new Operation("+") { protected double eval(double х, double уху х, double уху х, double уху х, double у) { return x/y; } } ; 105 // Выполнение арифметической операции, // представленной данной константой protected abstract double eval(double х, double у public String toString() { return this.name; } // Препятствует переопределению в подклассах // метода Object.equals public final boolean equals(Object that) { return super.equals(that); } public final int hashCode() { return super.hashCode(); } // Следующие четыре декларации необходимы для сериализации private static int nextOrdinal = о private final int ordinal = nextOrdinal++; private static final Operation[] VALUES = { PLUS. MINUS, TIMES, DIVIDE}; Object readResolve() throws ObjectStreamException { return VALUES[ordinal]; // Канонизация Представим подкласс класса Operation, в который добавлены операции логарифма и экспоненты. Причем этот подкласс может находиться за пределами того пакета, где содержится исправленный класс Operation. Данный подкласс может быть открытыми сам может быть расширяемым. Возможно мирное сосуществование различных подклассов, написанных независимо друг от друга. // Подкласс расширяемого сериализуемого перечисления abstract class ExtendedOperation extends Operation { ExtendedOperation(String name) { super(name); } public static Operation LOG = new ExtendedOperation("log") { protected double eval(double х, double у) { return Math.log(y) / Math.log(x); } ; public static Operation ЕХР = new ExtendedOperation("exp") { protected double eval(double х, double у) { return Math.pow(x, у } } ; 106 // Следующие четыре декларации необходимы для сериализации private static int nextOrdinal = 0; private final int ordinal = nextOrdinal++; private static final Operation[] VALUES = { LOG, ЕХР }; Object readResolve() throws ObjectStreamException { return VALUES[ordinal]; // Канонизация } Заметим, что в представленных классах методы readResolve показаны как доступные в пределах пакета, а незакрытые. Это необходимо потому, что экземпляры классов Operation и ExtendedOperation фактически являются экземплярами анонимных подклассов, а потому закрытые методы readReasolve были бы бесполезны (статья 57). Шаблон typesafe епит, по сравнению с шаблоном int enum, имеет несколько недостатков. По- видимому, единственным серьезным его недостатком является то, что он не так удобен для объединения констант перечисления в наборы. В перечислениях целых чисел для констант традиционно выбираются значения в виде различных неотрицательных степеней числа два, сам же набор представляется как побитовое О соответствующих констант // Вариант шаблона int впит с битовыми флажками static final int SUIT_CLUBS =0; public static final int SUIT_DIAMONDS =1’ public static final п SUIT_HEARTS =2’ public static final int SUIT_SPADES =3; public static final int SUIT_BLACK ; SUIT_CLUBS I Набор констант перечисления, представленный таким образом, является краткими чрезвычайно быстрым. Для набора констант перечисления typesafe enum вы можете использовать универсальную реализацию набора, заимствованную из Collections Framework, однако такое решение не является ни кратким, ни быстрым Set blackSuits ; new HashSet(); blackSuits.add(Suit.CLUBS); blackSuits.add(Suit.SPADES); Хотя наборы констант перечисления typesafe eum вряд ли можно сделать такими же компактными и быстрыми, как наборы констант перечисления int enum, указанное неравенство можно уменьшить путем специальной реализации набора Set, которая обслуживает элементы только определенного типа, а для самого набора использует Внутреннее представление в виде двоичного вектора. Такой набор лучше реализовывать в том же пакете, где описывается тип его элементов. Это позволяет через поля или методы, доступные только в пределах пакета, получать доступ к битовому значению, которое соответствует внутреннему представлению каждой константы в перечислении. Имеет смысл создать открытые конструкторы, которые в качестве параметров принимают короткие последовательности элементов, что делает возможным применение идиом следующего типа hand.discard( new SuitSet( Suit.CLUBS, Suit.SPADES )); Небольшой недостаток констант перечисления typesafe enums, по сравнению с перечислением int enum, заключается в том, что для них нельзя использовать оператор switch, поскольку они не являются целочисленными. Вместо этого вы применяете оператор if, например, следующим образом if (suit == Suit.CLUBS) { }else if (suit == Suit.DIAMONDS) { }else if (suit == Suit.HEARTS) { }else if (suit == Suit.SPADES) { }else { throw new NullPointerException("Null Suit"); //suit = = Оператор if работает не так быстро, как оператор switch, однако эта разница вряд ли будет существеной. Более того, при работе с константами перечисления typesafe enum потребность в большом ветвлении должна возникать редко, поскольку они подчиняются автоматической диспетчеризации методов, осуществляемой JVM, как показано в примере с Operation. Еще один недостаток перечислений связан с потерей места и времени при загрузке классов перечислений и создании объектов для констант перечисления. Если отбросить такие стесненные в ресурсах устройства, как сотовые телефоны и тостеры, эта проблема на практике вряд ли будет заметна. Подведем итоги. Преимущества перечислений typesafe enum перед перечислениями int е огромны, и ни один из недостатков не кажется непреодолимым, за исключением случая, когда перечисления применяются прежде всего как элемент набора либо в среде, серьезно ограниченной в ресурсах. Таким образом, когда обстоятельства требуют введения перечисления, на ум сразу же должен приходить шаблон typesafe епит. API, использующие перечисления typesafe enum, гораздо удобнее для программиста, чем API, ориентированные на перечисления int enum. Единственная причина, по которой шаблон typesafe enum не применяется более интенсивно в интерфейсах АР для платформы Java, заключается в том, что в то время, когда писались многие из этих API, данный шаблон еще не был известен. Наконец, стоит повторить еще раз, что потребность в перечислениях любого вида должна возникать сравнительно редко, поскольку большинство этих типов после создания подклассов стали устаревшими (статья 20). Указатель на функцию заменяйте к пассом и интерфейсом Язык С поддерживает указатели на функции (function pointer), что позволяет программе хранить и передавать возможность вызова конкретной функции. Указатели на функции обычно применяются для того, чтобы разрешить клиенту, вызвавшему функцию, уточнить схему ее работы, для этого он передает ей указатель на вторую функцию. Иногда это называют обратным вызовом (callback). Например, функция qsort из стандартной библиотеки С получает указатель на функцию-компаратор (comparator), которую затем использует для сравнения элементов, подлежащих сортировке. Функция- компаратор принимает два параметра, каждый из которых является указателем на некий элемент. Она возвращает отрицательное целое число, если элемент, на который указывает первый параметр, оказался меньше элемента, на который указывает второй параметр, нуль, если элементы равны между собой, и положительное целое число, если первый элемент больше второго. Передавая указатель на различные функции-компараторы, клиент может получать различный порядок сортировки. Как демонстрирует шаблон Strategy [Сатта95, стр. 315], функция-компаратор представляет алгоритм сортировки элементов. В языке Java указатели на функции отсутствуют, поскольку те же самые возможности можно получить с помощью ссылок на объекты. Вызывая в объекте некий метод, действие обычно производят над самим этим объектом. Между тем можно построить объект, чьи методы выполняют действия над другими объектами, непосредственно предоставляемыми этим методам. Экземпляр класса, который предоставляет клиенту ровно один метод, фактически является указателем на этот метод. Подобные экземпляры называются объектами-функциями. Например, рассмотрим следующий класс class StringLengthComparator { public int compare(String s1, String s2) { return s1.1ength() - s2.1ength(); Этот класс передает единственный метод, который получает две строки и возвращает отрицательное число, если первая строка короче второй, нуль, если две строки имеют одинаковую длину, и положительное число, если первая строка длиннее второй. Данный метод - ничто иное как компаратор, который, вместо более привычного лексикографического упорядочения, задает упорядочение строк по длине. Ссылка на объект StringLengthComparator служит для этого компаратора в качестве "указателя на функцию, что позволяет использовать его для любой пары строк. Иными словами, экземпляр класса StrlngLengthComparator - это определенная методика (concrete strategy) сравнения строк. Как часто бывает с классами конкретных методик сравнения, класс StringLengthComparator не имеет состояния у него нет полей, а потому все его экземпляры функционально эквивалентны друг другу. Таким образом, во избежание расходов на создание ненужных объектов можно сделать этот класс синглтоном (статьи 4 и 2): class StringLengthComparator { private StringLengthComparator() { } public static final StringLengthComparator INSTANCE = new StringLengthComparator(); public int compare(String s1, String s2) return s1.length() - s2.length(); Для того чтобы передать методу экземпляр класса StringLengthComparator, нам необходим соответствующий тип параметра. Использовать непосредственно тип StringLengthComparator нехорошо, поскольку это лишит клиентов возможности выбирать какие-либо другие алгоритмы сравнения. Вместо этого следует определить интерфейс Comparator и переделать класс StriпgLепgthСоmраrаtоr таким образом, чтобы он реализовывал этот интерфейс. Другими словами, необходимо определить интерфейс методики сравнения (strategy interface), который должен соответствовать классу конкретной стратегии |