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

Джош Блох

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

Глава 7 Методы

(variable arity methods) [JLS, 8.4.1]. Методы varargs принимают ноль или более аргументов определенного типа. Vararg сначала соз­ дает массив, размер которого равен числу аргументов, переданных в месте вызова, затем помещает значения аргументов в массив и, на­ конец, передает массив методу.

Например, вот метод varars, который берет последовательность аргументов int и возвращает их сумму. Как вы можете ожидать, зна­ чение sum( 1, 2, 3) равно 6 и значение sum() равно 0:

// Простое использование varargs static int sum(int... args) {

int sum = 0;

for (int arg : args) sum += arg;

return sum;

}

Иногда можно написать метод, которому требуется один или бо­ лее аргументов некоего типа больше нуля. Например, предположим, вы хотите рассчитать минимальное количество аргументов int. Эта функция не очень хорошо определена, если клиент не передает никаких аргументов. Вам необходимо проверить длину массива при запуске:

//Неверное использование varargs для передачи одного или более

//аргументов!

static int min(int... args) { if (args.length == 0)

throw new IllegalArgumentException(“Too few arguments”); int min = args[0];

for (int i = 1; i < args.length; i++) if (args[i] < min)

min = args[i]; return min;

}

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

270

С т а т ь я 42

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

for-each, если только не инициализируете min в Integer.MAX_VALUE,

что также ужасно выглядит.

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

//Правильный способ использовать varargs для передачи одного

//или более аргументов

static int min(int firstArg, int... remainingArgs) { int min = firstArg:

for (int arg : remainingArgs) if (arg < min)

min = arg; return min;

}

Как видно из этого примера, использование varargs эффективно там, где вам действительно требуется метод с переменным числом аргументов. Методы varargs были созданы для метода printf, ко­ торый появился в платформе версии 1.5, и для средств отражения (статья 53), которые были модифицированы для использования пре­ имуществ varargs в этой версии. И printf и отражение очень много выиграли от появления varargs.

Вы можете изменить существующий метод, берущий массив как окончательный параметр, с тем чтобы он принимал параметры varargs вместо него, не затрагивая существующих клиентов. Одна­ ко то, что вы это можете, вовсе не значит, что это следует делать. Рассмотрим случай с Array.asList. Этот метод никогда не разра­ батывался для того, чтобы собирать несколько аргументов в спи­ сок, но переработать его, чтобы он мог это делать после появления

271

Глава 7 Методы

varargs, выглядит довольно неплохой идеей. В результате стало воз­ можно сделать это:

List<$tring> homophones = Arrays.asList(«to», «too», «two»);

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

// Устаревшая идиома для печати массива!

System.out.println(Arrays.asList(myArray));

Она была необходима, потому что массивы наследовали свою реализацию toString от Object, таким образом, вызов toString непо­ средственно на массиве выдает бесполезную строку, таую как [Ljava.lang.Integer;@3e25a5. Идиома работала только на массивах типов объектных ссылок, но, если вы случайно попробуете ее на массивах примитивов, программа не будет компилироваться. Например, эта программа:

public static void main(St ring[] args) {

int[] digits = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 4 }; System.out.println(Arrays.asList(digits));

}

сгенерировала бы ошибку в версии 1.4:

Va.java:6: asList(Object[]) in Arrays can’t be applied to (int[]) System.out.println(Arrays.asList(digits));

И з -за неудачного решения переработать Arrays. asList как ме­ тод varargs в версии 1.5 эта программа теперь компилируется без ошибок или предупреждений. Запуск программы тем не менее при­ водит к результату, который не является ожидаемым и понятным: [[1@3е25а5]. Метод Arrays. asList, теперь «улучшенный», чтобы он мог использовать varargs, собирает объектные ссылки на массив целых чисел digits в одноэлементный массив массивов и сворачи­ вает его в экземпляр List<int[]>. Распечатка этого списка приводит

212

С тать я 42

к тому, что запускается toString на своем единственном элементе, массиве int, с неудачным результатом, описанным выше.

С другой стороны, идиома Arrays, as List теперь устарела для пере­ вода массивов в строки, а современная идиома имеет куда больше воз­ можностей. Также в версии 1.5 классу arrays дается полное дополнение методов Arrays. toSt ring (не методов varargs!), созданных специально для перевода массивов любого типа в строки. Если вы используете Ar­ rays. toSt ring вместе Arrays, as List, программа даст нужный результат:

// Правильный способ напечатать массив

System.out.рrintln(Arrays.toString(myArray));

Вместо модификации Array. asList было бы лучше добавить ме­ тод к Collections специально с целью сбора аргументов в список:

public static <Т> List<T> gather(T... args) { return Arrays.asList(args);

}

Такой метод дает возможность сбора не причиняя вреда провер­ ке типов в уже существующем методе Array.asList.

Урок понятен. Не стоит модифицировать каждый метод, у ко­ торого есть конечный параметр массива; используйте varargs, только когда вызов оперирует действительно с последовательностью значе­ ний с переменным числом аргументов.

Особенно подозрительны две сигнатуры методов:

ReturnTypel suspect1(0bject... args) { } <Т> ReturnType2 suspect2(T... args) { }

Методы с любой из этих сигнатур примут любой список параме­ тров. Любая проверка типов на этапе компиляции, которую вы дела­ ли до модификации, будет утрачена, как было показано на примере, что происходит с Arrays. asList.

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

273

Глава 7 Методы

ходы, но вам нужна гибкость методов varargs, есть шаблон, который позволит решить вам вашу задачу. Предположим, вы определили, что 95% обращений к методу содержит 3 и менее параметров. Тогда объявим 5 перегрузок метода, каждый с количеством обычных па­ раметров от нуля до трех, и один метод varargs для использования, когда количество аргументов превысит 3:

public

void

foo() {}

 

 

 

public

void

foo(inta1) { }

 

 

 

public

void

foo(inta1, int

a2)

{ }

 

public

void

foo(inta1, int

a2,

int a3)

{ }

public

void

foo(inta1, int

a2,

int a3,

int... rest) { }

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

Класс EnumSet использует данный прием для своих методов стати­ ческой генерации для уменьшения затрат на создание наборов перечис­ лимых типов до абсолютного минимума. Было нормально использовать данный прием, потому что наборы перечислимых типов предоставляют конкурентоспособную замену битовым полям (статья 32).

Подведем итоги. Методы varargs удобны в плане определения методов, которые требуют переменного количества аргументов, но не стоит переусердствовать в их использовании. Они могут выдавать запутанные результаты в случаях неправильного использования.

Возвращайте массив нулевой длины, а не null

Нередко встречаются методы, имеющие следующий вид:

private List cheesesInStock = ...;

/* *

274

С татья 43

* @return массив,

содержащий

все сыры, имеющиеся в магазине,

* или null, если

сыров для продажи нет.

* /

 

 

public Cheese[] getCheesesO

{

if (cheesesInStock.size() == 0) return null;

}

Нет причин рассматривать как особый случай ситуацию, когда в продаже нет сыра. Это требует от клиента написания дополнитель­ ного кода для обработки возвращаемого методом значения null, на­ пример:

Cheese[] cheeses = shop.getCheeses(); if (cheeses != null &&

Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON)) System.out.println("Jolly good, just the thing.”);

вместо простого:

if (Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON)) System.out.println(“Jolly good, just the thing.”);

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

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

275

Глава 7 М етоды

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

сконтролем типа:

//Правильный способ вывести массив из коллекции

private final List cheesesInStock = ...;

 

private

static

final Cheese[]

EMPTY_CHEESE_ARRAY

= new Cheese[0]:

j **

 

 

 

 

* @return массив, содержащий

все сыры, имеющиеся

в магазине

*/

 

 

 

 

public

Cheese[]

getCheesesO

{

 

return (Cheese[]) cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);

}

В этой идиоме константа в виде массива нулевой длины переда­ ется методу toArray для того, чтобы показать, какой тип он должен возвратить. Обычно метод toArray выделяет место в памяти для возвращаемого массива, однако, если коллекция пуста, она разме­ щается во входном массиве, а спецификация Collection. toArray(0bject [ ]) дает гарантию, что, если входной массив будет достаточно вместителен, чтобы содержать коллекцию, возвращен будет именно он. Поэтому представленная идиома никогда не будет сама разме­ щать в памяти массив нулевой длины.

Аналогичным образом можно заставить метод с коллекцией зна­ чений возвращать ту же самую пустую неизменяемую коллекцию каждый раз, когда это требуется. Методы Collections.emptySet, emptyList и emptyMap дают в точности то, что нам нужно, как показа­ но ниже:

// Правильный способ возврата копии коллекции. public List<Cheese> getCheeseList() {

276

С тать я 44

if (cheesesInStock.isEmpty())

return Collections.emptyListO; // Always returns same list

else

return new ArrayList<Cheese>(cheesesInStock);

}

Подведем итоги. Нет никаких причин для того, чтобы рабо­ тающий с массивами метод возвращал значение null, а не массив нулевой длины. Такая идиома, по-видимому, проистекает из язы­ ка программирования С, где длина массива возвращается отдельно от самого массива. В языке С бесполезно выделять память под мас­ сив нулевой длины.

Для всех открытых элементов API пишите doc-комментарии

Если A PI будет использоваться, его нужно описывать. Обычно документация к A PI пишется вручную, и поддержание соответствия между документацией и программным кодом — весьма неприятная работа. Среда программирования Java облегчает эту задачу с помо­ щью утилиты, называемой Javadoc. Она автоматически генерирует документацию к A PI, отталкиваясь от исходного текста программы, дополненного специальным образом оформленными коммента­ риями к документации (documentation comment), которые чаще называют doc-комментариями (doc comment). Утилита Javadoc предлагает простой, эффективный способ документирования API и используется повсеместно.

Если вы еще не знакомы с соглашениями для doc-комментариев, то обязаны их изучить. Эти соглашения не являются частью языка программирования Java, но де-факто они образуют свой A PI, кото­ рый обязан знать каждый программист. Соглашения описаны на веб странице Sun How to Write Doc Comments [учебник Javadoc]. Хотя эта страница и не обновлялась с момента выхода версии 1.4, она все

277

Глава 7 Методы

Ч\ЧЧЧЧ\Ч\ЧЧЧЧЧЧ\ЧЧЧЧЧЧЧЧЧЧЧЧЧЧЧчЧЧЧЧ,ЧЧЧЧЧЧЧЧЧЧЧЧЧЧ^ЧЧ®^ЧЧЧчЧЧЧ«^Ч*ЧЧЧ№ЧЧЧЧЧЧ'№&\ЧЧЧЧЧ^&ЧчЧЧЧЧЧЧЧЧЧЧЧХ№^^

еще остается бесценным ресурсом. Два важных тега были добавлены в Javadoc с версией 1.5, {@literal} и {@code}[Javadoc-5.0]. Эти теги и обсуждаются в данной статье.

Чтобы должным образом документировать A PI, следует предварять doc-комментарием каждую предоставляемую поль­ зователям декларацию класса, интерфейса, конструктора, мето­ да и поля. Если класс является сериализуемым, нужно документиро­ вать также его сериализуемую форму (статья 75).

Единственное исключение обсуждается в конце статьи. Если doc-комментарий отсутствует, самое лучшее, что может сделать Javadoc, — это воспроизвести декларацию элемента A PI как един­ ственно возможную для него документацию. Работа с API, у ко­ торого нет комментариев к документации, чревата ошибками. Что­ бы создать программный код, приемлемый для сопровождения, вы должны написать doc-комментарии даже для тех классов, интер­ фейсов, конструкторов, методов и полей, которые не предоставляют­ ся пользователям.

Doc-комментарий для метода должен лаконично описы­ вать соглашения между этим методом и его клиентами. Соглашение должно оговаривать, что делает данный метод, а не как он это де­ лает. Исключение составляют лишь методы в классах, предназначен­ ных для наследования (статья 17). В doc-комментарии необходимо перечислить все предусловия (precondition), т.е. утверждения, ко­ торые должны быть истинными для того, чтобы клиент мог вызвать этот метод, и постусловия (postcondition), т.е. утверждения, кото­ рые будут истинными после успешного завершения вызова. Обычно предусловия неявно описываются тегами @throws для необработан­ ных исключений. Каждое необработанное исключение соответству­ ет нарушению некоего предусловия. Предусловия также могут быть указаны вместе с параметрами, которых они касаются, в соответ­ ствующих тегах @рагагп.

Помимо пред- и постусловий для методов должны быть также документированы любые побочные эффекты. Побочный эффект

278

С татья 44

(side effect) — это поддающееся наблюдению изменение состоя­ ния системы, которое является неявным условием для достижения постусловия. Например, если метод запускает фоновый поток, это должно быть отражено в документации. Наконец, комментарии к документации должны описывать безопасность класса при рабо­

те с потоками (thread safety), которая обсуждается в статье 70.

Вцелях полного описания соглашений doc-комментарий для ме­ тода должен включать в себя: тег @param для каждого параметра, тег @return, если только метод не возвращает тип void, и тег @throws для

каждого исключения, инициируемого этим методом, как обработанно­ го, так и необработанного (статья 62). По соглашению, текст, кото­ рый следует за тегом @param или @return, представляет собой именную конструкцию (noun phrase — термин грамматики английского языка), описывающую значение данного параметра или возвращаемое зна­ чение. Текст, следующий за тегом @throws, должен состоять из слова if и именной конструкции, описывающей условия, при которых ини­ циируется данное исключение. Иногда вместо именных конструкций используются арифметические выражения. Все эти соглашения иллю­ стрирует следующий краткий doc-комментарий из интерфейса List:

j * *

* Возвращает элемент, который занимает заданную позицию в данном списке.

*

*<р>Этот метод <i>He</i> дает гарантии, что будет выполняться

впостоянное время.

*В некоторых реализациях он будет выполняться во время, *

пропорциональное положению его элементов.

*

 

 

* @param

index

индекс элемента, который нужно возвратить;

* индекс должен

быть меньше размера списка и неотрицательным.

* @return

элемент, занимающий в списке указанную позицию.

* @throws IndexOutOfBoundsException if индекс лежит вне диапазона * ({@code index < 0 || index >= this.size()})

*/

E get(int index);

279

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