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

Джош Блох

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

Глава 2 Создание иуничтожение объектов

// Builder Pattern

public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium;

private final int carbohydrate; public static class Builder {

// Required parameters private final int servingSize; private final int servings;

// Optional parameters - initialized to default values private int calories = 0;

private int fat = 0;

private int carbohydrate = 0; private int sodium = 0;

public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings;

}

public Builder calories(int val)

{calories = val; return this; } public Builder fat(int val)

{fat = val; return this; } public Builder carbohydrate(int val)

{carbohydrate = val; return this; } public Builder sodium(int val)

{sodium = val; return this; } public NutritionFacts build() {

return new NutritionFacts(this);

}

}

private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings;

20

С татья 2

calories = builder.calories; fat = builder.fat;

sodium = builder.sodium; carbohydrate = builder.carbohydrate;

}

}

Обратите внимание, что NutritionFacts является неизменным и что все значения параметров по умолчанию находятся в одном месте. Сеттеры объекта «конструктор» возвращают сам этот «кон­ структор». Поэтому вызовы можно объединять в цепочку. Вот как выглядит код клиента:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8). calories(100).sodium(35).carbohydrate(27).build();

Этот клиентский код легко писать и, что еще важнее, легко чи­ тать. Шаблон «конструктора» имитирует именные дополнитель­ ные параметры, так же как в языках Ada и Python.

Как и конструктор, «конструктор» может навязывать инварианты на свои параметры. Метод build позволяет проверить эти инварианты. Очень важно, чтобы они были проверены после копирования параме­ тров из «конструкора» на объект и чтобы они были проверены на по­ лях объекта, а не на полях «конструктора» (статья 39). Если хоть один инвариант нарушается, то метод build должен вывести сообщение IIlegalStateExceprion (статья 60). Детали ошибки в сообщении содер­ жат информацию, какие инварианты были нарушены (статья 63).

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

Другое небольшое преимущество использования «конструкто­ ра» вместо конструкторов заключается в том, что у «конструктора»

21

Builder<Nutri-

Глава 2 Создание иуничтожение объектов

может быть несколько параметров varargs. У конструкторов, как и у методов, может быть только один параметр varargs. Поскольку «конструкторы» используют отдельные методы для установки каж­ дого параметра, они могут иметь сколько угодно параметров varargs до ограничения в один параметр на один сеттер.

Шаблон «конструктора» обладает гибкостью. Один «конструк­ тор» может использоваться для создания нескольких объектов. П а­ раметры «конструктора» могут быть изменены для того, чтобы соз­ давать различные объекты. «Конструктор» может автоматически заполнить некоторые поля, например серийный номер, который ав­ томатически увеличивается каждый раз, когда создается объект.

«Конструктор», параметры которого заданы, создает отличный шаблон проектирования (Abstract factory) [Gamma95, с. 87]. Другими словами, клиент может передать такой «конструктор» методу, чтобы ме­ тод мог создавать один или более объектов для клиента. Чтобы сделать возможным такое использование, вам необходим тип для представления построения. Если вы используете релиз 1.5 или более поздний, то будет достаточно одного родового типа (статья 26) для всех «конструкторов», вне зависимости от того, какой тип объекта они создают:

// A builder for objects of type T public interface Builder<T> { public T build();

}

Обратите внимание, что наш класс NutritionFacts. Builder мо­ жет быть объявлен для реализации «конструктора»

tionFacts>.

Методы, которые работают с экземпляром «конструктора», обычно накладывают ограничения на его параметры, используя связанные типы групповых символов (bounded wildcard type) (статья 28). Например, вот метод, который строит дерево, используя предоставленный клиен­ том экземпляр «конструктора», для построения каждого узла.

Tree buildTree(Builder<? extends Node> nodeBuilder) {

}

Традиционным применением шаблонов проектирования в Java являются классы, с методом newlnstance, являющимся частью ме-

22

С татья 2

тода build. При таком использовании очень много проблем. Метод newlnstance всегда пытается запустить конструктор класса без пара­ метров, который может даже и не существовать. И вы не получите сообщение об ошибке на этапе компиляции, если у класса нет до­ ступа к конструктору без параметров. Вместо этого клиентский код

натолкнется на ошибку InstantiationExceprion или IllegalAccessEx-

ception в процессе выполнения, что ужасно неприятно. Также метод newlnstance распространяет любое сообщение об исключении (ошиб­ ке), выведенное конструктором без параметров, даже если в тексте метода нет соответствующих выражений throws. Другими словами,

Class.newlnstance прерывает проверку ошибок на этапе компиля­ ции. Интерфейс Builder, рассмотренный выше, исправляет данный недостаток.

У шаблона «конструктор» есть свои недостатки. Для создания объекта вам надо создать сначала его «конструктор». Затраты на со­ здание «конструктора» малозаметны на практике на самом деле, но в некоторых ситуациях, где производительность является важным моментом, это может создать проблемы. Кроме того, шаблоны «кон­ структора» более длинные, нежели шаблоны телескопических кон­ струкций, поэтому использовать их нужно при наличии достаточного количества параметров, например четырех и более. Но имейте в виду, что в будущем вы можете захотеть добавить параметры. Если вы нач­ нете использовать конструкторы или статические методы, а затем до­ бавите «конструктор», когда количество параметров в классе выйдет из-под контроля, то уже ненужные конструкторы или статические методы будут для вас словно заноза в пальце. Поэтому изначально желательно начинать именно с «конструктора».

В общем, шаблон «конструктора» — это хороший выбор при проектировании классов, у чьих конструкторов и статических мето­ дов имеется много параметров, особенно если большинство из них не являются обязательными. Клиентский код легче читать и писать при использовании «конструкторов», чем при использовании тра­ диционных шаблонов телескопических конструкторов. Кроме того, «конструкторы» гораздо безопаснее, чем JavaBeans.

23

Глава 2 Создание и уничтожение объектов

Свойство синглтон обеспечивайте закрытым конструктором или типом перечислений

С и н г л т о н (singleton) — это просто класс, для которого экземпляр создается только один раз [Gamma95, с. 127]. Синглтоны обычно представляют некоторые компоненты системы, которые действи­ тельно являются уникальными, например видеодисплей или файловая система. Превращение класса в синглтон может создать сложности для тестирования его клиентов, так как невозможно заменить лож­ ную реализацию синглтоном, если только он не реализует интерфейс, который одновременно служит его типом.

До релиза 1.5 для реализации синглтонов использовалось два подхода. Оба они основаны на создании закрытого (private) кон­ структора и открытого (public) статического члена, который позво­ ляет клиентам иметь доступ к единственному экземпляру этого клас­ са. В первом варианте открытый статический член является полем

типа final:

// Синглтон с полем типа final public class Elvis {

public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... }

public void leaveTheBuilding() { ... }

}

Закрытый конструктор вызывается только один раз, чтобы ини­ циализировать поле Elvis.INSTANCE. Отсутствие открытых или за ­ щищенных конструкторов гарантирует «вселенную с одним Elvis»: после инициализации класса Elvis будет существовать ровно один экземпляр Elvis — не больше и не меньше. И ничего клиент с этим поделать не может, за одним исключением: клиент с расширенны­ ми правами может в свою очередь запустить частный конструктор

с помощью метода AccessibleObject.setAccessible. Если вы хотите

защиту от такого рода атаки, необходимо изменить конструктор так,

24

С татья 3

чтобы он выводил сообщение об ошибке, если поступит запрос на со­ здание второго экземпляра.

Во втором варианте вместо открытого статического поля типа final создается открытый статический метод генерации:

// Синглтон со статическим методом генерации public class Elvis {

private static final Elvis INSTANCE = new ElvisO;

private Elvis() { . . .

}

 

public

static Elvis getlnstanceQ

{ return INSTANCE; }

public

void leaveTheBuilding() {

}

}

public static Elvis getlnstance() { return INSTANCE;

}

Все вызовы статического метода Elvis, getlnstance возвращают ссылку на один и тот же объект, и никакие другие экземпляры Elvis никогда созданы не будут (за исключением того же вышеупомянуто­ го недостатка).

Основное преимущество первого подхода заключается в том, что из декларации членов, составляющих класс, понятно, что этот класс яв­ ляется синглтоном: открытое статическое поле имеет тип final, а потому это поле всегда будет содержать ссылку на один и тот же объект. Пер­ вый вариант по сравнению со вторым более не имеет прежнего преиму­ щества в производительности: современная реализация J V M встраивает вызовы для статического метода генерации во втором варианте.

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

25

Глава 2 Создание иуничтожение объектов

Зачастую ни одно из этих преимуществ не имеет значение и подход с использованием открытого поля.

Чтобы класс синглтона был сериализуемым (глава 11) , надо просто добавить к его декларации implements Serializable будет недостаточно. Чтобы дать синглтону нужные гарантии, вам необ­ ходимо объявить все экземпляры полей как временные (transient), а также создать метод readResolve (статья 77). В противном случае каждая десериализация сериализованного экземпляра будет приво­ дить к созданию нового экземпляра, что в нашем примере приведет к обнаружению ложных Elvis. Чтобы предотвратить это, добавьте

вкласс Elvis следующий метод readResolve:

//Метод readResolve для сохранения свойств синглтона private Object readResolve() {

//Возвращает один истинный Elvis и дает возможность *сборщику мусора*

//избавиться от самозванца Elvis

return INSTANCE;

}

В релизе 1.5 также имеется третий подход к реализации синглто­ нов. Просто создать тип перечислений одним элементом:

// Перечисление синглтона - более предпочтительный подход. public enum Elvis {

INSTANCE;

public void leaveTheBuilding() { ... }

}

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

26

С татья 4

Отсутствие экземпляров обеспечивает

закрытый конструктор

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

лано в библиотеках java. lang. Math или java. util. Arrays, либо чтобы

собирать вместе статические методы объектов, в том числе методов статической генерации (статья 1) для объектов, которые реализуют определенный интерфейс, как это сделано в java. util.Collections. Можно также собрать методы в неком окончательном (final) классе, вместо того, чтобы заниматься расширением класса.

Подобные классы утилит (utility class) разрабатываются не для того, чтобы для них создавать экземпляры — такой экземпляр был бы абсурдом. Однако, если у класса нет явных конструкторов, ком­ пилятор по умолчанию сам создает для него открытый конструктор (default constructor), не имеющий параметров. Для пользователя этот конструктор ничем не будет отличаться от любого другого. В опубли­ кованных API нередко можно встретить классы, непреднамеренно наделенные способностью порождать экземпляры.

Попытки запретить классу создавать экземпляры, объявив его абстрактным, не работают. Такой класс может иметь подкласс, для которого можно создавать экземпляры. Более того, это вводит пользователя в заблуждение, заставляя думать, что данный класс был разработан именно для наследования (статья 17). Есть, однако, простая идиома, гарантирующая отсутствие экземпляров. Конструк­ тор по умолчанию создается только тогда, когда у класса нет явных конструкторов, и потому запретить создание экземпляров можно, поместив в класс единственный явный закрытый конструктор:

27

Глава 2 Создание и уничтожение объектов

// Класс утилит, не имеющий экземпляров public class UtilityClass {

//Подавляет появление конструктора по умолчанию, а заодно

//и создание экземпляров класса

private UtilityClass() {

throw new AssertionError();

}

... // Остальное опущено

}

Поскольку явный конструктор заявлен как закрытый (private), то за пределами класса он будет недоступен. Не обязательна строка с Asse rtionE ггог, но является подстраховкой на случай, если конструк­ тор будет случайно вызван в самом классе. Она гарантирует, что для класса никогда не будет создано никаких экземпляров. Эта идиома не­ сколько алогична, поскольку конструктор создается здесь именно для того, чтобы им нельзя было пользоваться. Соответственно, есть смысл поместить в текст программы комментарий, как описано выше.

Побочным эффектом является то, что данная идиома не позво­ ляет создавать подклассы для этого класса. Явно или неявно, все конструкторы должны вызывать доступный им конструктор суперк­ ласса. Здесь же подкласс лишен доступа к конструктору, к которому можно было бы обратиться.

Избегайте ненужных объектов

Вместо того чтобы создавать новый функционально эквивалентный объект всякий раз, когда в нем возникает необходимость, часто мож­ но просто еще раз использовать тот же объект. Использовать что-ли­ бо снова и изящнее, и быстрее. Если объект является неизменяемым (immutable), его всегда можно использовать повторно (статья 15).

28

С татья 5

Рассмотрим следующий оператор, демонстрирующий, как де­ лать не надо:

String s = new String(“stringette”); // Никогда так не делайте!

При каждом проходе этот оператор создает новый экземпляр String, но ни одна из процедур создания объектов не является необ­ ходимой. Аргумент конструктора String — «stringette» — сам явля­ ется экземпляром класса String и функционально равнозначен всем объектам, создаваемым этим конструктором. Если этот оператор попадает в цикл или часто вызываемый метод, без всякой надобно­ сти могут создаваться миллионы экземпляров String.

Исправленная версия выглядит просто:

String s = “stringette”;

В этом варианте используется единственный экземпляр String вместо того, чтобы при каждом проходе создавать новые. Более того, дается гарантия того, что этот объект будет повторно исполь­ зовать любой другой программный код, который выполняется на той же виртуальной машине, где содержится эта строка-константа [JLS, 3.10.5].

Создания дублирующих объектов часто можно избежать, если в неизменяемом классе, имеющем и конструкторы, и статические ме­ тоды генерации (статья 1), вторые предпочесть первым. Например, статический метод генерации Boolean.valueOf(String) почти всегда предпочтительнее, чем конструктор Boolean(String). При каждом вызове конструктор создает новый объект, тогда как от статического метода генерации этого не требуется.

Вы можете повторно использовать не только неизменяемые объ­ екты, но и изменяемые, если знаете, что последние меняться уже не будут. Рассмотрим более тонкий и гораздо более распростра­ ненный пример того, как не надо поступать. В нем участвуют из­ меняемые объекты Date, которые более не меняются, после того как их значение вычислено. Этот класс моделирует человека и имеет ме­ тод isBabyBoomer который говорит, является ли человек рожденным

29