Циклы – неотъемлемая часть языка программирования, без них вам приходилось бы перебирать значения массива из 100 элементов вручную, сотней строк кода. В Java циклы есть на все случаи жизни: вам доступны циклы for, for each, while и do while. Этим, кстати, могут похвастаться не все языки программирования: оператором for each не обладает С, циклы do-while недоступны в Python (в обоих случаях недоступные инструменты приходится эмулировать, что крайне неудобно).
Ниже – о том, как все эти циклы работают, где их лучше использовать и в каких случаях циклы while и for причиняют больше вреда, чем пользы.
Нередко перед программистом встает задача выполнить одно и то же действие многократно. Самый простой пример – написание шести подряд слов «Привет». Чтобы решить ее, требуется написание следующего программного кода.
System.out.println("Привет");
System.out.println("Привет");
System.out.println("Привет");
System.out.println("Привет");
System.out.println("Привет");
System.out.println("Привет");
Кажется, что в дублировании одинаковой части кода нет ничего сложного. Но только в том случае, если речь идет об ограниченном количестве повторов. Если их число увеличивается до полусотни, сотни и т.д., простым копирование уже не обойтись – слишком большие для этого потребуются трудозатраты.
В подобной ситуации намного проще и правильнее воспользоваться циклом. В этом случае исходный код для описанного выше примера становится заметно короче и приобретает такой вид.
for (int i = 0; i < 6; i++) {
System.out.println("Привет");
}
Важно отметить, что использование цикла фактически не ведет к увеличению продолжительность программного кода и для 6, и для 100, и для любого другого числа итераций. Что очень удобно и серьезно экономит силы и время программиста. Все изменение касается цифры, которая указывается внутри приведенной выше структуры (в данном примере – вместо «6»).
Для программирования Java, как мы уже говорили, используются 4 вида циклов:
Теперь перейдем к более детальному рассмотрению.
Конструкцию while пишут вот так:
while (something) {
statement_1;
statement_2;
}
Something – это условие, которое должно возвращать true или false: x > 5, например. Можно и просто написать while (true), так организовывается бесконечный цикл. О том, как управлять исполнением бесконечных циклов (через операторы break), мы расскажем ниже.
Statement – это какие-то строки, которые нужно исполнить. Итерация – это когда все statement цикла исполнены. Условие проверяется 1 раз за итерацию, то есть если у вас есть 5 строк кода внутри цикла – сначала у цикла выполнятся все 5 строк, а потом он проверит, выполняется ли условие, и примет решение о том, нужно ли ему выполняться дальше.
Циклы while очень полезны тогда, когда вам нужно выполнять что-то, пока не наступят определенные условия. Например, вы пишете программу, которая принимает от пользователя какой-то символ, который он вводит через консоль, и делает что-то в зависимости от введенного символа. Если пользователь вводит символ ‘q’, то работа программы останавливается:
char input_value = ‘ ‘;
while (input_value != ‘q’) {
System.out.println("Input a character: ");
Input_value = (char) System.in.read();
//делаем что-то, исходя из символа в input_value
…
}
Как вы можете заметить, здесь есть одна проблема – процесс инициализации выполняется до первой итерации цикла, то есть мы пишем
char input_value = ‘ ’
и запихиваем в переменную пробел.Это – не очень хорошая практика, потому что потом мы можем изменить формат ввода, пробел будет просачиваться в первую итерацию и выполнять какую-то команду, завязанную на пробел – получим сложный в поиске баг.
Чтобы исправить ситуацию, будем использовать циклы do while:
char input_value;
do {
System.out.println("Input a character: ");
Input_value = (char) System.in.read();
//делаем что-то, исходя из символа в input_value
…
} while (input_value != ‘q’);
Как видите, теперь на месте условия стоит do, а само условие переместилось вниз. Оператору do все равно, верно ли условие – в первый раз цикл будет запущен в любом случае. Поэтому инициализация переменной input_value у нас теперь не включает присвоение пробела – в переменную сразу будет записано значение, введенное с клавиатуры.
С for все проще и сложнее одновременно. Сам for выглядит вот так:
for (init; termination_condition; incr) {
statement_1;
statement_2;
…
}
Сложнее – потому что нужно задавать больше условий в шапке цикла. В init выполняется инициализация переменной, которая будет счетчиком; в termination_condition указано условие для прерывания циклов; в incr указывается, как должны себя вести переменные счетчика, когда итерация подойдет к концу. Как это все работает:
Как это выглядит в реальной жизни:
for (int i = 1; i < 10; i++) {
System.out.println("Итерация " + i);
}
Цикл будет выводить: «Итерация 1», «Итерация 2», …, «Итерация 9». После того, как «Итерация 9» выведется, к i прибавится еще 1, и, поскольку 10 < 10 = false, цикл закончится.
Отметим, что инициализация переменной, условие прерывание и инкремент – опциональные, можно писать for (;;). Инициализация счетчика не нужна, если вы пользуетесь уже существующей переменной, вместо условия остановки можно использовать прерывания итерации через break, а инкремент можно делать прямо внутри цикла, поскольку i – это такая же переменная, как и остальные. Но использовать for без параметров следует осторожно, потому что от цикла for ждут конечного количества повторения цикла, и ваш код может быть сложно прочитать, если вы используете for нестандартно.
For each выглядит так:
for (Type variable : iterable) {
statement_1;
statement_2;
…
}
Iterable – это объект, который мы собираемся перебирать. Variable – это переменная, в которую мы будем класть каждый элемент перебираемого объекта. После того, как for кладет в variable элемент, с этим элементом можно что-то делать – для этого есть statement_1 и остальные. Когда все элементы перебраны, цикл заканчивается, то есть бесконечного цикла здесь быть не может.
Использование цикла For требует дополнительного учета нескольких существенных нюансов, к числу которых относятся такие:
int i = 0;
for (; i < 10; i++) {
System.out.println(i);
}
int any = 0;
int i = 0;
for (someMethod(), any += 2, i++; i < 10; i++) {
//code
}
Такой формат цикла несколько отличается от описанного выше While и называется с постусловием. Его базовый синтаксис выглядит так.
do {
<тело цикла>
} while (<условие выполнения цикла>);
Порядок выполняемых действий имеет следующий вид:
Такой вариант циклического процесса запускается в том случае, когда нет информации об условии выхода (так как она размещается внутри цикла) или таких условий несколько. Аналогичная необходимость возникает в том случае, когда требуется, чтобы цикл никогда не заканчивался, хотя на практике подобная ситуация складывается достаточно редко.
Цикл создается одним из трех способов, причем все они имеют примерно одинаковую эффективность, что упрощает выбор подходящего. Первый выглядит так:
while (true) {
<тело цикла>
}
Второй имеет следующий вид:
for (;;) {
<тело цикла>
}
Третий и последний:
do {
<тело цикла>
} while(true);
В качестве наглядного примера бесконечного цикла, выполнение которого не должно прекратиться, можно привести такой. Его запуск приведет к тому, что приветствие будет появляться на консоли до ее выключения или перегрузки системы.
while (true) {
System.out.println("Привет!");
}
Вложенным называют цикл, тело которого находится внутри другого. Такой формат исходного кода требуется в том случае, когда необходима работа с двухмерными массивами. Примером программы выступает такая.
char[][] chars =
{{'x', ' ', ' ', ' ', 'x'},
{' ', 'x', ' ', 'x', ' '},
{' ', ' ', 'x', ' ', ' '},
{' ', 'x', ' ', 'x', ' '},
{'x', ' ', ' ', ' ', 'x'}};
for (int i = 0; i < chars.length; i++) {
for (int j = 0; j < chars[i].length; j++) {
System.out.print(chars[i][j]);
}
System.out.println();
Чтобы заниматься написанием вложенных циклов, необходимо не только знание языка программирования Java. Требует еще и навык работы с массивами и понимание базовых принципов их функционирования.
Continue и break используются, когда вам нужно управлять потоком исполнения непосредственно внутри цикла. Операторы continue досрочно завершают текущую итерацию и возвращают управление к проверке условия, оператор break вообще завершает выполнение цикла. Хрестоматийный пример с continue:
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue;
System.out.println(i + " ");
}
Здесь continue используется для того, чтобы прервать выполнение итерации, если i – четное число. В консоль выводится 1, 3, 5, 7, 9. Когда поток исполнения натыкается на строку кода с проверкой на четность и убеждается, что i – четное, он исполняет continue, и остальная часть итерации не выполняется.
Теперь пример с break:
for (int i = 1; i <= 10; i++) {
if (i >= 7) break;
System.out.println(i + " ");
}
Здесь будет выведено 1, 2, 3, 4, 5, 6. Когда i доходит до 7, условие внутри цикла становится истинным, и срабатывает break, после чего управление принудительно передается следующему блоку кода (сразу после цикла).
Циклы – это отличный инструмент, если вам нужно сделать большое или неизвестное количество одинаковых операций. Но именно циклы чаще всего приводят к переполнению стэка, а тот, в свою очередь, приводит к аварийному завершению программы. И виной тому – вложенные циклы и бесконечные циклы.
Вложенные циклы опасны потому, что увеличивают количество итераций в геометрической прогрессии. Например, вам нужно найти все корни уравнения x^2 + y^2 = z^2, x/y/z <= 1000. Самый простой способ решения – запустить 3 цикла, самый верхний перебирает x, средний перебирает y, внутренний цикл перебирает z. Поскольку нам нужно найти все корни, циклы должны исполниться полностью. Сколько операций будет произведено? 1000 * 1000 * 1000 = 1 000 000 000. Миллиард операций – много, но еще терпимо. Теперь предположим, что вы ищете все корни уравнения x^2 + y^2 + z^2 = n^2, x/y/z/n <= 1000. Сколько нужно совершить операций? 1 000 000 000 000. Здесь ваш компьютер уже серьезно задумается. И это мы не берем случаи, когда внутри циклов производятся собственные сложные вычисления, возведение в квадрат – это довольно простая операция.
Вторая проблема – это бесконечный цикл. Вообще, бесконечный цикл бывает контролируемым и неконтролируемым. Контролируемый бесконечный цикл – это когда вы специально его создали и у вас все под контролем (ну, или вы по крайней мере так думаете). Такие бесконечные циклы очень полезны – например, они используются в web-серверах, когда сервер вечно (пока подается питание) ждет запроса на соединение. Но есть и другие бесконечные циклы – те, которые заставляют систему зависнуть, бесконечно исполняя какой-то код. Например, вы написали цикл for, в котором с каждой итерацией увеличиваете i на единицу. Но где-то внутри цикла вы случайно написали i--. Что произойдет? Цикл начался, i = 1. Поток исполнения дошел до i--, уменьшил переменную до 0. Итерация завершилась, for увеличил i до 1. i-- снова уменьшил i до 0 – и это будет продолжаться бесконечно.
Именно поэтому в циклах for настоятельно рекомендуется заводить под счетчик новую переменную i прямо в шапке цикла и нигде, кроме секции инкремента, эту i не трогать.