Джош Блох
.pdfГлава 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
С тать я 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