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

Джош Блох

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

Глава 8 Общие вопросы программирования

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

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

использование цикла for-each

До версии 1.5 предпочтительнее было использовать следующую идиому для итерации коллекции:

// Более не яляется предпочтительной идиомой для итерации коллекции! for (Iterator i - с. iterator(); i.hasNext(); ) { doSomething((Element) i.nextO); // (No generics before 1.5)

}

Так выглядела наиболее предпочтительная идиома для итерации массива:

// Более не яляется предпочтительной идиомой для итерации массива! for (int i = 0; i < a.length; i++) {

doSomething(a[i]);

}

Эти идиомы лучше, чем циклы while (статья 45), но они несо­ вершенны. Итератор и переменная индекса путаются друг с другом.

290

С тать я 46

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

Цикл for-each, представленный в версии 1.5, избавляет нас от этой путаницы и возможности для ошибки, полностью скрывая итератор или переменную индекса. В результате получается идиома, подходящая и для коллекций, и для массивов:

// Предпочтительная идиома для итерации коллекций или массивов for (Element е : elements) {

doSomething(e);

}

Двоеточие следует читать как «в». Следовательно, цикл будет читаться как «для каждого элемента е в элементах». Обратите вни­ мание, что использование цикла for-each не наносит никакого ущер­ ба производительности, даже для массивов. На самом деле данный цикл может предложить небольшое преимущество в производитель­ ности над простым циклом for в некоторых обстоятельствах, так как он подсчитывает ограничения индекса массива только один раз. В то время как вы можете сделать это вручную (статья 45), программи­ сты так не делают.

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

// Можете ли вы найти ошибку?

enum

Suit

{ CLUB,

DIAMOND, HEART, SPADE }

enum

Rank

{ ACE,

DEUCE,

THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,

NINE,

TEN,

JACK,

QUEEN,

KING }

Collection<Suit> suits = Arrays.asList(Suit.values()); Collection<Rank> ranks = Arrays.asList(Rank.values());

291

Глава 8 Общие вопросы программирования

List<Card> deck = new ArrayList<Card>();

for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) for (Iterator<Rank> j = ranks. iterator(); j.hasNextO; ) deck.add(new Card(i.next(), j.next()));

He огорчайтесь, если вы не смогли найти ошибку. Многие экс­ перты в программировании делают эту ошибку . Проблема в том, что метод next вызывается слишком много раз на итераторе для внешней коллекции (suits). Он должен вызываться из внешнего цикла, так чтобы на каждый элемент приходился один вызов, но вместо этого он вызывается из внутреннего цикла, потому вызывается по одному разу на каждую карточку. После того как у вас закончатся костюмы,

цикл выведет ошибку NoSuchElementException.

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

// Та же ошибка, но с другими симптомами!

enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }

Collection<Face> faces = Arrays.asList(Face.valuesO);

 

for

(Iterator<Face>

i = faces. iterator();

i.hasNextO;

)

for

(Iterator<Face>

j = faces. iterator();

j.hasNextO;

)

System, out. println(i. next() + “ “ + j.nextO);

Данная программа не выводит ошибку, но она печатает только шесть двойных значений (от «1:1» до «6:6»), вместо ожидаемых 36 комбинаций.

Для решения данной проблемы вам надо добавить переменную в диапазоне внешнего цикла, чтобы хранить внешние элементы:

// Решено, но выглядит ужасно - можно сделать лучше!

for (Ite rato r<Suit> i = suits. iterator(); i.hasNextO; ) { Suit suit = i.next О ;

for (Iterator<Rank> j = ranks. iterator(); j.hasNextO; )

292

for-each:
for-each.

С тать я 46

deck. add(new Card(suit, j.nextO));

}

Если вместо этого использовать вложенный цикл for-each, то проблема просто исчезает. Получившийся в результате код на­ столько краток, насколько вы хотите:

/ / Предпочтительная идиома для вложенной итерации коллекций и массивов for (Suit suit : suits)

for (Rank rank : ranks) deck.add(new Card(suit, rank));

Ц икл for-each не только дает вам возможность итерации кол­ лекций и массивов, но и позволяет выполнять итерации любых объ­ ектов, реализующих интерфейс Iterable. Этот простой интерфейс, содержащий лишь один метод, был добавлен к платформе вместе с циклом Вот как он выглядит:

public interface Iterable<E> {

// Returns an iterator over the elements in this iterable Iterator<E> iterator();

}

He так сложно реализовать интерфейс Iterable. Если вы пишете тип, представляющий группу элементов, заставьте его реализовать Iterable, даже если вы решите не реализовывать Collection. Это по­ зволит вашим пользователям применить итерацию на типах с исполь­ зованием цикла for-each, за что они будут вам благодарны.

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

1.Фильтрация — если вам требуется пройти через коллекцию и удалить выбранные элементы, тогда вам нужно использовать явный итератор, чтобы вы могли вызвать его метод remove.

2.Преобразование — если вам требуется пройти через список или массив и заменить некоторые или все значения его эле­

293

Глава 8 Общие вопросы программирования

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

3.Параллельная итерация — если вам необходимо пройти через несколько коллекций параллельно, тогда вам нужен явный контроль над итератором или переменной индекса, чтобы все итераторы или переменные индекса могли быть расширены в рамках строгой системы (как было непреднамеренно пока­ зано выше в примерах с картами и костями).

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

Изучите библиотеки и пользуйтесь ими

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

private static final Random rnd = new Random(); // Неправильно, хотя встречается часто

static int random(int n) {

return Math. abs(rnd. nextlntO) %n;

}

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

294

С тать я 47

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

public static void main(String[] args) { int n = 2 * (Integer.MAX_VALUE / 3); int low = 0;

for (int i = 0; i < 1000000; i++) if (random(n) < n/2)

low++;

System.out.println(low);

}

Если б ы метод random работал правильно, программа печатала б ы число близкое к полумиллиону, однако, запустив эту программу,

вы обнаружите, что она печатает число близкое к 6 6 6 666. Д в е трети чисел, сгенерированных методом random, попадает в н и ж н ю ю полови­ ну диапазона!

Третий недостаток представленного метода random заключается

втом, что он может, хотя и редко, потерпеть полное фиаско, выдавая результат, выходящий за пределы указанного диапазона. Э т о про­ исходит потому, что метод пытается преобразовать значение, воз­ вращенное методом rnd.nextlnt (), в неотрицательное целое число, используя метод Math.abs. Если nextlnt() вернул Integer. MIN_VALUE, то Math.abs также возвратит Integer. MIN_VALUE. Затем, если п не яв­ ляется степенью числа два, оператор остатка ( % ) вернет отрицатель­ ное число. Это почти наверняка вызовет сбой в вашей программе,

ивоспроизвести обстоятельства этого сбоя будет трудно.

Ч т о б ы написать такой вариант метода random, в котором были б ы исправлены все эти три недостатка, необходимо изучить генера­ торы линейных конгруэнтных псевдослучайных чисел, теорию чисел и арифметику дополнения до двух. К счастью, делать это вам не н у ж ­ но, все это уже сделано для вас. Необходимый метод называется Ran­ dom. nextlnt (int), он был добавлен в пакет java, util стандартной би­ блиотеки в версии 1.2.

295

Глава 8 Общие вопросы программирования

Нет нужды вдаваться в подробности, каким образом метод пехtlnt(int) выполняет свою работу (хотя любопытные личности могут изучить документацию или исходный текст метода). Старший инже­ нер с подготовкой в области алгоритмов провел много времени за раз­ работкой, реализацией и тестированием этого метода, а затем показал метод экспертам в данной области с тем, чтобы убедиться в его пра­ вильности. После этого библиотека прошла стадию предварительного тестирования и была опубликована, тысячи программистов широко пользуются ею в течение нескольких лет. До сих пор ошибок в ука­ занном методе найдено не было. Но если какой-либо дефект обна­ ружится, он будет исправлен в следующей же версии. Обращаясь

кстандартной библиотеке, вы используете знания написавших

ееэкспертов, а также опыт тех, кто работал с ней до вас.

Второе преимущество от применения библиотек заключается

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

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

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

му росту ее производительности.

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

296

С тать я 47

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

Учитывая все эти преимущества, логичным казалось бы при­ менение библиотек, а не частных разработок, однако многие про­ граммисты этого не делают. Но почему? Может быть, потому, что они не знают о возможностях имеющихся библиотек. С каждой следующей версией в библиотеки включается множество но­ вых функций, и стоит быть в курсе этих новшеств. Вы можете внимательно изучать соответствующую документацию в режиме online либо прочесть о новых библиотеках в самых разных книгах [J2SE -A P Is, ChanOO, Flanagan99, Chan98]. Библиотеки слишком объемны, чтобы просматривать всю документацию, однако каж­ дый программист должен хорошо знать java.lang, java, util и в меньшей степени java.io. Остальные библиотеки изучаются по мере необходимости.

Обзор всех возможностей библиотек выходит за рамки данной статьи, однако некоторые из них заслуживают особого упоминания. В версии 1.2 в пакет j ava .util была добавлена архитектура Collections Framework. Она должна входить в основной набор инструментов каждого программиста. Collections Framework — унифицированная архитектура, предназначенная для представления и управления кол­ лекциями и позволяющая манипулировать коллекциями независимо от деталей представления. Она сокращает объемы работ по програм­ мированию и в то же время повышает производительность. Эта ар­ хитектура позволяет достичь унифицированности несвязанных API, упрощает проектирование и освоение новых API, способствует по­ вторному использованию программного обеспечения. Если вы хотите больше узнать, см. документацию на веб-сайте Sun или прочитайте учебник [ВЬсЬОб].

В версии 1.5 появился набор взаимозаменяемых утилит в пакете java.util.concurrent. Этот пакет содержит как высокоуровневые ути­

297

Глава 8 Общие вопросы программирования

литы совместимости для упрощения многопоточного программиро­ вания, так и низкоуровневые взаимозаменяемые примитивы, что позволяет экспертам писать свои собственные высокоуровневые взаимозаменяемые абстракции. Высокоуровневая часть java.utils, concurrent также должна быть частью набора каждого программиста (статьи 68, 69).

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

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

Если требуются точные ответы, избегайте использования типов float и double

Типы float и double в первую очередь предназначены для науч­ ных и инженерных расчетов. Они реализуют бинарную арифмети­ ку с плавающей точкой (binary floating-point arithmetic), которая была тщательно выстроена с тем, чтобы быстро получать правильное приближение для широкого диапазона значений. Однако эти типы не дают точного результата, и в ряде случаев их нельзя использо-

298

С тать я 48

вать. Типы float и double не подходят для денежных расчетов,

поскольку с их помощью невозможно представить число 0.1 (или лю­ бую другую отрицательную степень числа десять).

Например, у вас в кармане лежит $1,03, и вы тратите 42 цента. Сколько денег у вас осталось? Приведем фрагмент наивной програм­ мы, которая пытается ответить на этот вопрос:

System.out.рrintln(1.03 - .42);

Как ни печально, программа выводит 0.6100000000000001. И это не единственный случай. Предположим, что у вас в карма­ не есть доллар и вы покупаете 9 прокладок для крана по 10 центов за каждую. Какую сдачу вы получите?

System.out.println(1.00 - 9*.10);

Если верить этому фрагменту программы, то вы получите $0.09999999999999995. Может быть, проблему можно решить, округлив результаты перед печатью? К сожалению, это срабатыва­ ет не всегда. Например, у вас в кармане есть доллар, и вы видите полку, где выстроены в ряд вкусные конфеты за 10, 20, 30 центов

ит.д. вплоть до доллара. Вы покупаете по одной конфете каждого вида, начиная с той, что стоит 10 центов, и т.д., пока у вас еще есть возможность взять следующую конфету. Сколько конфет вы купите

исколько получите сдачи? Решим эту задачу следующим образом:

//Ошибка: использование плавающей точки для денежных // расчетов! public static void main(String[] args) {

double funds = 1.00; int itemsBought = 0;

for (double price = .10; funds >= price; price += .10) { funds -= price;

itemsBought++;

}

System.out.println(itemsBought + “ items bought.”); System.out.println(“Change: $” + funds);

}

299

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