Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 4 Классы и интерфейсы

PhysicalConstants. AVOGADROS_NUMBER. Если будет трудно использо­

вать константы, передаваемые вспомогательным классом, то вы мо­ жете отказаться от необходимости связывания констант с названием класса, используя возможности статического импорта, представ­ ленные в версии 1.5.

//Используем статический импорт для избежания необходимости

//связывания констант

import static com.effectivejava.science.PhysicalConstants.*; public class Test {

double atoms(double mols) { return AVOGADROS_NUMBER * mols;

}

//Другие варианты использования uses PhysicalConstants

//для статического импорта.

}

Таким образом, интерфейсы нужно использовать только для определения типов. Их не надо использовать для передачи констант.

Объединение заменяйте иерархией классов

Иногда вам может встретиться класс, экземпляры которого бы­ вают двух и более видов и могут содержать поле с признаком, кото­ рый определяет вид экземпляра. Например, рассмотрим класс, кото­ рый может представлять круг или прямоугольник:

// Tagged class - vastly inferior to a class hierarchy! class Figure {

enum Shape { RECTANGLE, CIRCLE };

//Tag field - the shape of this figure final Shape shape;

//These fields are used only if shape is RECTANGLE

140

С татья 20

double length; double width;

//This field is used only if shape is CIRCLE double radius;

//Constructor for circle

Figure(double radius) { shape = Shape.CIRCLE; this, radius = radius;

}

// Constructor for rectangle Figure(double length, double width) {

shape = Shape.RECTANGLE; this.length = length; this.width = width;

}

double area() { switch(shape) {

case RECTANGLE:

return length * width; case CIRCLE:

return Math.PI * (radius * radius); default:

throw new AssertionError();

}

}

}

У таких объединенных классов множество недостатков. Они за ­ громождены шаблонами, включая в себя объявление перечислимых типов, меченых полей и выражений переключения. Читаемость кода еще больше страдает, потому что большое количество реализаций смешивается в один класс.

Растет расход памяти, потому что экземпляры нагружены не­ нужными полями другого вида. Поля не могут быть завершенными, если конструкторы не инициализируют ненужные поля, что приво­ дит к созданию большего объема кода. Конструкторы должны за­

141

Глава 4 Классы и интерфейсы

дать поля с отметками и инициализировать корректные поля данных без помощи компилятора: если будут инициализированы не те поля, то программа даст сбой при выполнении. Вы не можете добавить признак к объединенному классу, если вы не измените его исходный файл. Если вы действительно добавите признак, то должны не за ­ быть добавить регистр к каждому выражению при переключении, в противном случае класс будет выполняться с ошибкой. Наконец, тип данных в экземпляре не дает понятия об имеющемся признаке. Короче говоря, связанные классы слишком многословны, могут содержать ошибки и неэффективны.

К счастью, у объектно ориентированных языков программи­ рования есть намного лучший механизм определения типа данных, который можно использовать для представления объектов разных типов: создание подтипов. Явное объединение в действительности является лишь бледным подобием иерархии классов.

Чтобы преобразовать объединение в иерархию классов, опреде­ лите абстрактный класс, содержащий метод для каждой операции, работа которой зависит от значения тега. В предыдущем примере единственной такой операцией является area. Полученный абстракт­ ный класс будет корнем иерархии классов. Если есть какая-либо опе­ рация, функционирование которой не зависит от значения тега, пред­ ставьте ее как неабстрактный метод корневого класса. Точно так же, если в явном объединении помимо tag и union есть какие-либо поля данных, то эти поля представляют данные единые для всех типов, а потому их нужно перенести в корневой класс. В приведенном при­ мере подобных операций и полей данных, не зависящих от типа, нет.

Далее, для каждого типа, который может быть представлен объ­ единением, определите неабстрактный подкласс корневого класса. В предыдущем примере такими типами являются круг и прямоуголь­ ник. В каждый подкласс поместите те поля данных, которые харак­ терны для соответствующего типа. В нашем примере радиус является характеристикой круга, а длина и ширина характеризуют прямоу­ гольник. Кроме того, в каждый подкласс поместите соответствую­ щую реализацию для всех абстрактных методов в корневом классе.

142

С татья 20

Представим иерархию классов, которая соответствует нашему при­ меру явного объединения:

// Class hierarchy replacement for a tagged class abstract class Figure {

abstract double area();

}

class Circle extends Figure {

final

double

radius;

 

 

 

Circle(double

radius)

{ this.radius =

radius; }

double

area()

{ return

Math.PI

* (radius * radius); }

}

 

 

 

 

 

 

class Rectangle extends Figure {

 

 

final

double

length;

 

 

 

final

double width;

 

 

 

Rectangle(double length, double width)

{

 

this.length

= length;

 

 

 

this.width

= width;

 

 

}

 

 

 

 

 

 

double

area()

{ return

length

* width;

}

}

Иерархия классов исправляет все недостатки связанных классов, отмеченных ранее. Код прост и ясен, не содержит шаблонов. Реали­ зация каждого типа выделена в свой собственный класс, и ни один из этих классов не нагружен ненужными полями данных. Все поля окончательны. И компилятор проверяет, что каждый конструктор классов инициализирует свои поля данных и что у каждого класса есть реализация для каждого абстрактного метода, продекларированного в корневом классе. Это помогает избежать возможности ошибки при выполнении, ввиду отсутствия выражений переключения. Различные программисты могут расширить иерархию независимо друг от друга без необходимости доступа к исходному коду корневого класса. Для каждого типа имеется отдельный тип данных, позволяющий про­ граммистам определять тип переменных и входные параметры для конкретного типа.

143

Глава 4 Классы и интерфейсы

Еще одно преимущество иерархии классов связано с ее способ­ ностью отражать естественные иерархические отношения между ти­ пами, что обеспечивает повышенную гибкость и улучшает проверку типов на этапе компиляции. Допустим, что явное объединение в ис­ ходном примере допускает также построение квадратов. В иерархии классов можно показать, что квадрат — это частный случай прямоу­ гольника (при условии, что оба они неизменны):

class Square extends Rectangle { Square(double side) {

super(side, side):

Обратите внимание, что для классов в этой иерархии, за исклю­ чением класса Square, доступ предоставляется непосредственно к по­ лям, а не через методы доступа. Делается это для краткости, и это было бы ошибкой, если бы классы были открытыми (статья 14).

Подводя итоги, можно сказать, что связанные классы редко приемлемы. Если у вас будет искушение написать такой класс, по­ думайте, можно ли его заменить иерархией. Если вам попадется уже существующий класс, подумайте над возможностью преобразования его в иерархию.

Используйте объект функции для выполнения сравнения

Некоторые языки поддерживают указатели на функции

(function pointer), делегаты (delegates), выражения лямбда (lambda expressions) и другие возможности, что дает программе возможность хранить и передавать возможность вызова конкретной функции. Та­ кие возможности обычно используются для того, чтобы позволить клиенту, вызвавшему функцию, уточнять схему ее работы, для этого он передает ей указатель на вторую функцию. Например, функция qsort из стандартной библиотеки Си получает указатель на функци­ ю-компаратор (comparator), которую затем использует для сравнения

144

С татья 21

элементов, подлежащих сортировке. Функция-компаратор принима­ ет два параметра, каждый из которых является указателем на некий элемент. Она возвращает отрицательное целое число, если элемент, на который указывает первый параметр, оказался меньше элемента, на который указывает второй параметр, нуль, если элементы равны между собой, и положительное целое число, если первый элемент больше второго. Передавая указатель на различные функции-компа­ раторы, клиент может получать различный порядок сортировки. Как демонстрирует шаблон Strategy из [Gamma95, с. 315], функция-ком­ паратор представляет алгоритм сортировки элементов.

В языке Java указатели отсутствуют, поскольку те же самые воз­ можности можно получить с помощью ссылок на объекты. Вызы­ вая в объекте некий метод, действие обычно производят над самим этим объектом. Между тем можно построить объект, чьи методы выполняют действия над другими объектами, непосредственно предоставленным этим методами. Экземпляр класса, который пре­ доставляет клиенту ровно один такой метод, фактически является указателем на этот метод. Подобные экземпляры называются объ­ ектами-функциями. Например, рассмотрим следующий класс:

class StringLengthComparator {

public int compare(String si, String s2) { return sl.length() - s2.length();

Этот класс передает единственный метод, который получает две строки и возвращает отрицательное число, если первая строка короче второй, нуль, если эти две строки имеют одинаковую длину, и положительное число, если первая строка длиннее второй. Данный метод — не что иное, как компаратор, который вместо более при­ вычного лексикографического упорядочивания задает упорядочение строк по длине. Ссылка на объект StringLengthComparator служит для этого компаратора в качестве «указателя на функцию», что по­ зволяет его использовать для любой пары строк. Иными словами,

экземпляр класса StringLengthComparator — это определенная м ето ­

дика (concrete strategy) сравнения строк.

145

Глава 4 Классы и интерфейсы

Как часто бывает с классами конкретных методик сравнения,

класс StringLengthComparator не имеет состояния: у него нет по­

лей, а потому все экземпляры этого класса функционально эквива­ лентны друг другу. Таким образом, чтобы избежать расходов на со­ здание ненужных объектов, этот класс можно сделать синглтоном (статьи 3 и 5):

class StringLengthComparator {

private Stringl_engthComparator() { } public static final StringLengthComparator INSTANCE = new StringLengthComparator(); public int compare(String s1, String s2) {

return s1.length() - s2.1ength();

}

}

Чтобы передать методу экземпляр класса StringLengthCompara­ tor, нам необходим соответствующий тип параметра. Использовать непосредственно тип StringLengthComparator было бы нехорошо, по­ скольку это лишило бы клиентов возможности выбирать какие-либо другие алгоритмы сравнения. Вместо этого нам следует определить интерфейс Comparator и переделать класс StringLengthComparator та­ ким образом, чтобы он реализовывал этот интерфейс. Иначе говоря, нам необходимо определить интерфейс методики сравнения (strategy interface), который должен соответствовать классу конкретной стра­ тегии. Представим этот интерфейс:

// Интерфейс методики сравнения public interface Comparator<T> {

public int compare(T t1, T t2);

}

Оказывается, что представленное определение интерфейса Com­ parator есть в пакете java, util, хотя никакого волшебства в этом нет и вы могли точно так же определить его сами. Интерфейс Com­ parator является одним из средств обобщенного программирования (статья 26), что применимо для компараторов объектов, не являю­

146

С татья 21

щихся строковыми. Его метод compare предпочитает брать два па­ раметра типа Т (его формальные параметры ти п а) вместо String.

Класс StringLengthComparator, показанный выше, можно заставить реализовывать Comparator<String> просто при его декларировании:

class StringLengthComparator implements Comparator<String> {

... // class body is identical to the one shown above

}

Классы конкретных методик сравнения часто создаются с помо­ щью анонимных классов (статья 22).

Arrays. sort(stringArray, new Comparator<String>() { public int compare(String si, String s2) {

return s1.length() - s2.1ength();

}

});

Обратите внимание на то, что использование анонимного класса приведет к созданию нового экземпляра каждый раз при выполне­ нии вызова. Если выполнение должно происходить постоянно, рас­ смотрите возможность хранения объекта функции в закрытом стати­ ческом завершенном поле и повторного его использования. Другое преимущество этого заключается в том, что вы можете дать полю дескриптивное имя для объекта функции.

Поскольку интерфейс методики сравнения используется как тип для всех экземпляров конкретных методик сравнения, то для того, чтобы предоставить конкретную методику сравнения, нет необхо­ димости делать соответствующий класс открытым. Вместо этого класс-хозяин (host) может передать открытое статическое поле (или статический метод генерации), тип которого соответствует интер­ фейсу методики сравнения, сам же класс методики сравнения может оставаться закрытым классом, вложенным в класс-хозяин. В следу­ ющем примере вместо анонимного класса используется статический класс-член, что позволяет реализовать в классе методики сравнения второй интерфейс — Serializable:

147

Глава 4 Классы и интерфейсы

// Предоставление конкретной методики сравнения class Host {

private static class StrLenCmp

implements Comparator<String>, Serializable { public int compare(String si, String s2) {

return s1.length() - s2.1ength();

}

}

// Возвращаемый компаратор является сериализуемым public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmpO;

... // Основная часть класса опущена

}

Представленный шаблон используется в классе String для того, чтобы через его поле CASE_INSENSITIVE_ORDER передавать компаратор строк, не зависящий от регистра.

Подведем итоги. Первоначально указатели использовались для ре­ ализации шаблона Strategy. Для того чтобы реализовать этот шаблон в языке программирования Java, необходимо создать интерфейс, пред­ ставляющий стратегии, а затем для каждой конкретной стратегии нужно построить класс, который этот интерфейс реализует. Если конкретная стратегия используется только один раз, ее класс обычно декларируется и реализуется с помощью анонимного класса. Если же конкретная стра­ тегия передается для многократного использования, ее класс обычно ста­ новится закрытым статическим классом-членом и передается через поле public static final, чей тип соответствует интерфейсу стратегии.

Предпочитайте статические классы-члены нестатическим

Класс называется вложенным (nested), если он определен вну­ три другого класса. Вложенный класс должен создаваться только для того, чтобы обслуживать окружающий его класс. Если вложенный

148

С татья 22

класс оказывается полезен в каком-либо ином контексте, он должен стать классом верхнего уровня. Существует четыре категории вло­ женных классов: статический класс-член (static member class), не­ статический класс-член (nonstatic member class), анонимный класс

(anonymous class) и локальный класс (local class). З а исключени­ ем первого, остальные категории классов называются внутренними (inner class). В этой статье рассказывается о том, когда и какую ка­ тегорию вложенного класса нужно использовать и почему.

Статический класс-член — это простейшая категория вложенного класса. Лучше всего рассматривать его как обычный класс, который декларирован внутри другого класса и имеет доступ ко всем членам окружающего его класса, даже к закрытым. Статический класс-член является статическим членом своего внешнего класса и подчиняется тем же правилам доступа, что и остальные статические члены. Если он декларирован как закрытый, доступ к нему имеет лишь окружаю­ щий его класс, и т.д.

В одном из распространенных вариантов статический класс-член используется как открытый вспомогательный класс, который приго­ ден для применения, только когда есть внешний класс. Например, рассмотрим перечисление, описывающее операции, которые может выполнять калькулятор (статья 30). Класс Operation должен быть открытым статическим классом-членом класса Calculator. Клиенты класса Calculator могут ссылаться на эти операции, выполняемые калькулятором, используя такие имена, как Calculator.Operation.

PLUS или Calculator. Operation. MINUS. Этот вариант приводится ниже.

С точки зрения синтаксиса единственное различие между статиче­ скими и нестатическими классами-членами заключается в том, что в декларации статических классов-членов присутствует модификатор static. Несмотря на свою синтаксическую похожесть, эти две кате­ гории вложенных классов совершенно разные. Каждый экземпляр нестатического члена-класса неявным образом связан с содержащим его экземпляром класса-контейнера (enclosing instance). И з метода в экземпляре нестатического класса-члена можно вызывать методы содержащего его экземпляра, либо, используя специальную кон-

149

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]