Джош Блох
.pdfГлава 2 • Создание иуничтожение объектов
раметров переданы статическому методу генерации. Это может быть любой класс, который является подтипом по отношению к возвраща емому типу, заявленному в интерфейсе. Класс возвращаемого объек та может также меняться от версии к версии, что повышает удобство сопровождения программы и повышает ее производительность.
В момент, когда пишется класс, содержащий статический метод генерации, класс, соответствующий возвращаемому объекту, может даже не существовать. Подобные гибкие статические методы генера ции лежат в основе систем с предоставлением услуг (service provider frameworks) , таких как Java Cryptography Extension (JC E ). Система
спредоставлением услуг — это такая система, где поставщик может создавать различные реализации интерфейса API, доступные поль зователям этой системы. Чтобы сделать эти реализации доступными для использования, предусмотрен механизм регистрации {register). Клиенты могут пользоваться указанным API, не беспокоясь о том,
скакой из его реализаций они имеют дело.
Укласса java. util. EnumSet (статья 32), представленного в вер сии 1.3, нет открытых конструкторов, только статические методы. Они возвращают одну из двух реализаций в зависимости от размера типа перечисления: если значение равно 64 и менее элементов (как у боль шей части типов перечислений), то статический метод возвращает эк земпляр RegularEnumSet, подкрепленный единичным значением long. Если же тип перечислений содержит 63 и более элементов, то метод возвращает экземпляр JumboEnumSet, подкрепленный массивом long.
Существование двух реализаций классов невидимо для клиен тов. Если экземпляр RegularEnumSet перестанет давать преимущество
впроизводительности небольшому количеству типов перечислений, то его можно избежать в дальнейшем без каких-либо вредных по следствий. Таким же образом, при будущем выполнении могут до бавиться третья и четвертая реализации EnumSet, если это улучшит производительность. Клиенты не знают и не должны беспокоиться о классах объектов, возвращаемых им методами, — для них важны только некоторые подклассы EnumSet.
Ю
С татья 1
Классу объекта, возвращаемого статическим методом, нет необ ходимости существовать на момент написания класса, содержащего метод. Подобная гибкость методов генерации создает основу для си стем предоставления услуг (service provider frameworks), таких как
Java Database Connectivity API (JDBC). Система предоставления
услуг — это система, в которой несколько провайдеров реализуют службы, и система делает эти реализации доступными для своих кли ентов, разъединяя их с реализациями.
Имеется три основных компонента системы предоставления услуг: интерфейс службы (service interface), который предоставля ется поставщиком, интерфейс регистрации поставщика (provider registration A P I), который использует система для регистрации ре ализации, и интерфейс доступа к службе (service access A P I), кото рый используется клиентом для получения экземпляра службы. Ин терфейс доступа к службе обычно позволяет определить некоторые критерии для выбора поставщика, которые тем не менее не являются обязательными. При отсутствии таковых он возвращает экземпляр реализации по умолчанию. Интерфейс доступа к службе — это «гиб кий производственный метод», составляющий основу системы пре доставления услуг.
Есть еще не обязательный четвертый компонент службы предо ставления услуг — интерфейс поставщика службы (service provider interface), который внедряется провайдером для создания экземпля ров реализации их служб. При отсутствии этого интерфейса реали зации регистрируются по имени класса, а их экземпляры создаются рефлективным образом (статья 53). В случае с JD B C Connection здесь играет роль интерфейса службы, DriverManager. registerDriv er — интерфейс регистрации провайдера, DriverManager.getConnection - интерфейс доступа к службе, и Driver - интерфейс постав щика службы.
Может быть несколько вариантов шаблона службы предоставле ния услуг. Например, интерфейс доступа к службе может возвратить более развернутый интерфейс службы, чем требуемый провайдером,
11
Глава 2 • Создание и уничтожение объектов
при использовании паттерна «Адаптер» [Gamma 95, с. 139], Здесь приведена простая реализация с интерфейсом службы поставщика
ипоставщиком по умолчанию:
//Service provider framework sketch
//Service interface
public interface Service {
// Service-specific methods go here
}
// Service provider interface public interface Provider {
Service newService();
}
// Noninstantiable class for service registration and access
public class Services |
{ |
|
||
private |
ServicesO |
{ } // Prevents instantiation (Item 4) |
||
// Maps |
service |
names |
to services |
|
private |
static |
final |
Map<String, Provider> providers = |
new ConcurrentHashMap<String, Provider>();
public static final String DEFAULT_PROVIDER_NAME = "<def>”;
// Provider registration API
public static void registerDefaultProvider(Provider p) { registerProvider(DEFAULT_PROVIDER_NAME, p);
}
public static void registerProvider(String name, Provider p){ providers.put(name, p);
}
// Service access API
public static Service newlnstance() {
return newInstance(DEFAULT_PROVIDER_NAME);
}
12
С татья 1
public static Service newInstance(String name) { Provider p = providers.get(name);
if (p == null)
throw new IllegalArgumentException(
“No provider registered with name: “ + name); return p.newService();
}
}
Четвертое преимущество статических шаблонов проектиро вания заключается в том, что они уменьшают многословие при создании экземпляров типов с параметрами. К сожалению, вам необходимо определить параметры типа при запуске конструктора классов с параметрами, даже если они понятны из контекста. Поэто му приходится обозначать параметры типа дважды:
Map<String, List<String>> m =
new HashMap<String, List<String»();
Эта излишняя спецификация становится проблемой по мере уве личения сложности параметров типов. При использовании же стати ческих методов компилятор сможет за вас создать параметры. Это еще называется (type inference). Например, предположим, что реа лизация HashМар дала нам следующий метод (статья 27):
public static <К, V> HashMap<K, V> newlnstance() { return new HashMap<K, V>();
}
В данном случае многословное выражение может быть заменено следующей краткой альтернативой:
Map<String, List<String>> m = HashMap.newlnstance();
Когда-нибудь вывод типа, сделанный таким образом, будет воз можно применять при вызове конструкторов, а не только при вызове статических методов, но в релизе 1.6 платформы такое пока невозможно.
К сожалению, у стандартного набора реализаций, таких как HashMap, нет своих статических методов в версии 1.6, но вы можете доба-
13
Глава 2 • Создание и уничтожение объектов
вить эти методы в собственный класс параметров. Теперь вы сможете предоставлять статические методы генерации в собственных классах с параметрами.
Основной недостаток использования только статических методов генерации заключается в том, что классы, не имеющие открытых или защищенных конструкторов, не могут иметь под классов. Это же верно и в отношении классов, которые возвраща ются открытыми статическими методами генерации, но сами откры тыми не являются. Например, в архитектуре Collections Framework невозможно создать подкласс ни для одного из классов реализации. Сомнительно, что в такой маскировке может быть благо, поскольку поощряет программистов использовать не наследование, а компози цию (статья 14).
Второй недостаток статических методов генерации состоит в том, что их трудно отличить от других статических методов.
В документации A PI они не выделены так, как это было бы сде лано для конструкторов. Поэтому иногда из документации к клас су сложно понять, как создать экземпляр класса, в котором вместо конструкторов клиенту предоставлены статические методы генера ции. Возможно, когда-нибудь в официальной документации по Java будет уделено должное внимание статическим методам. Указанный недостаток может быть смягчен, если придерживаться стандартных соглашений, касающихся именования. Эти названия статических ме тодов генерации становятся общепринятыми:
•valueOf — возвращает экземпляр, который, грубо говоря,
имеет то же значение, что и его параметры. Статические ме тоды генерации с таким названием фактически являются опе раторами преобразования типов.
•of — более краткая альтернатива для valueOf, распростра
ненная при использовании EnumSet (статья 32).
getlnstance — возвращает экземпляр, который описан пара метрами, однако говорить о том, что он будет иметь то же
14
С татья 2
значение, нельзя. В случае с синглтоном этот метод возвра щает единственный экземпляр данного класса. Это название является общепринятым в системах с предоставлением услуг.
•newlnstance — то же, что и getlnstance, только newlnstance
дает гарантию, что каждый экземпляр отличается от всех остальных.
•getType — то же, что и getlnstance, но используется, когда
метод генерации находится в другом классе. Туре обозначает тип объекта, возвращенного методом генерации.
•newType — то же, что и newlnstance, но используется, когда
метод генерации находится в другом классе. Туре обозначает тип объекта, возвращенного методом генерации.
Подведем итоги. И статические методы генерации, и открытые конструкторы имеют свою область применения, имеет смысл разо браться, какие они имеют достоинства друг перед другом. Обычно статические методы предпочтительнее, поэтому не надо бросаться создавать конструкторы, не рассмотрев сначала возможность исполь зования статических методов генерации, поскольку последние часто оказываются лучше. Если вы взвесили обе возможности и не нашли достаточных доводов ни в чью пользу, вероятно, лучше всего создать конструктор, просто потому, что такой подход является нормой.
Используйте шаблон Builder, когда приходится иметь дело с большим количеством параметров конструктора
У конструкторов и статических методов есть одно общее ограни чение: они плохо масштабируют большое количество необязательных параметров. Рассмотрим такой случай: класс, представляющий собой этикетку с информацией о питательности на упаковке с продуктами питания. На этих этикетках есть несколько обязательных полей —
15
Глава 2 • Создание и уничтожение объектов
размер порции, количество порций в упаковке, калорийность, а так же ряд необязательных параметров — общее содержание жиров, со держание насыщенных жиров, содержание жиров с трансизомерами жирных кислот, содержание холестерина, натрия и т.д. У большин ства продуктов не нулевыми будут только несколько из этих необя зательных значений.
Какой конструктор или какие методы нужно использовать для на писания данного класса? Традиционно, программисты будут исполь зовать шаблоны с телескопическими конструкторами (Telescoping Constructor Pattern), при использовании которых вы выдаете набор конструкторов: конструктор с одними обязательными параметра ми, конструктор с одним необязательным параметром, конструктор с двумя обязательными параметрами и т.д., до тех пор пока не будет конструктора со всеми необязательными параметрами. Вот как это выглядит на практике. Для краткости мы будем использовать только
4не обязательных параметра:
//Telescoping constructor pattern - does not scale well! public class NutritionFacts {
private final int servingSize; // (ml_) required
private final int servings: // (per container) required private final int calories; // optional
private final int fat; // (g) optional private final int sodium; // (mg) optional
private final int carbohydrate; // (g) optional
public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0);
}
public |
NutritionFacts(int |
servingSize, |
int servings, |
||||
int |
calories) |
{ |
|
|
|
|
|
|
|
this(servingSize, |
servings, |
calories, |
0); |
||
} |
|
|
|
|
|
|
|
public |
NutritionFacts(int |
servingSize, |
int servings, |
||||
int |
calories, |
int fat) { |
|
|
|
|
|
|
|
this(servingSize, |
servings, |
calories, |
fat, 0); |
16
С татья 2
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat;
this.sodium = sodium; this.carbohydrate = carbohydrate;
}
}
Если вы хотите создать экземпляр данного класса, то вы будете использовать конструктор с минимальным набором параметров, ко торый бы содержал все параметры, которые вы хотите установить:
NutritionFacts cocaCola =
new NutritionFacts(240, 8, 100, 0, 35, 27);
Обычно для вызова конструктора потребуется передавать мно жество параметров, которые вы не хотите устанавливать, но вы в лю бом случае вынуждены передать для них значение. В нашем случае мы установили значение 0 для поля fat. Поскольку мы имеем толь ко шесть параметров, может показаться, что это не так уж и плохо, но ситуация выходит из-под контроля, когда число параметров уве личивается.
Короче говоря, шаблоны телескопических конструкторов нормально работают, но становится трудно писать код програм мы-клиента, когда имеется много параметров, а еще труднее этот код читать. Читателю остается только гадать, что означают все эти значения, и нужно тщательно высчитывать позицию параметра, чтобы выяснить, к какому полю он относится. Длинные последова
Глава 2 • Создание иуничтожение объектов
тельности одинаково типизированных параметров могут приводить к тонким ошибкам. Если клиент случайно перепутает два из таких параметров, то компиляция будет успешной, но программа будет не правильно работать при выполнении (статья 40).
Второй вариант, когда вы столкнулись с конструктором со мно гими параметрами, — это использование шаблонов JavaBeans, где вы вызываете конструктор без параметров, чтобы создать объект, а затем вызываете сеттеры для установки обязательных и всех инте ресующих необязательных параметров:
// JavaBeans Pattern - allows inconsistency, mandates mutability public class NutritionFacts {
// Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // “ “ “ “
private int calories = 0; private int fat = 0; private int sodium = 0;
private int carbohydrate = 0; public NutritionFacts() { }
// Setters
public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}
Данный шаблон лишен недостатков шаблона телескопических конструкторов. Он прост, хотя и содержит большое количество слов, но получившийся код легко читается.
NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100);
С татья 2
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
К сожалению, шаблон JavaBeans не лишен серьезных недостатков. Поскольку его конструкция разделена между несколькими вызовами,
JavaBean может находиться в неустойчивом состоянии частично из-за такой конструкции. У класса нет возможности принудительно обеспечить стабильность простой проверкой действительности пара метров конструктора. Попытка использования объекта, если он нахо дится в неустойчивом состоянии, может привести к ошибкам выполне ния даже после удаления ошибки из кода, что создает трудности при отладке. Схожим недостатком является то, что шаблон JavaBeans ис ключает возможность сделать класс неизменным (статья 15), что требует дополнительных усилий со стороны программиста для обеспе чения безопасности в многопоточной среде.
Действие этого недостатка можно уменьшить, вручную «замо раживая» объект после того, как его создание завершено, и запре тив его использование, пока он заморожен, но этот вариант редко используется на практике. Более того, он может привести к ошиб кам выполнения, так как компилятор не может удостовериться в том, вызывает ли программист метод заморозки для объекта до того, как он будет использоваться.
К счастью, есть и третья альтернатива, которая сочетает в себе безопасность шаблона телескопических конструкций и читаемость шаблона JavaBeans. Она является одной из форм шаблона «конструк тора» [Gamma 95, с. 97]. Вместо непосредственного создания же лаемого объекта клиент вызывает конструктор (или статический ме тод) со всеми необходимыми параметрами и получает объект Builder (builder object). Затем клиент вызывает сеттеры на этом объекте для установки всех интересующих параметров. Наконец, клиент вызы вает метод build для генерации объекта, который будет являться не изменным. «Конструктор» является статическим внутренним клас сом в классе (статья 22), который он создает. Вот как это выглядит на практике:
19