Показаны сообщения с ярлыком Базовые знания. Показать все сообщения
Показаны сообщения с ярлыком Базовые знания. Показать все сообщения

6 июл. 2015 г.

Полиморфизм и наследование. Практика на понимание переопределения методов.

Если какой-либо из методов базового класса Java был перегружен несколько раз, переопределение имени этого метода в производном классе не скроет ни одну из базовых версий (в отличие от C++). Поэтому перегрузка работает вне зависимости от того, где был определен метод — на текущем уровне или в базовом классе. Чтобы все стало понятнее попрактикуемся на простом примере и уже классическом в этом блоге примере. Есть три класса А, В и С. В наследуется от А, а С наследуется от В. Посмотрим что с ними будет на этот раз…

SS004

SS005

Вывод у этой программы простой. Суть этой программы состоит в том, что переопределенные методы базовых классов не перестают существовать. То есть переопределенные методы базовых классов могут быть вызваны и использованы. По существу все методы здесь вызывались на объекте класса С, так как в этой программе у нас есть только один объект – это объект класса С. И суть в том, что этот объект содержит в себе все методы своих суперклассов, и они могут быть вызваны.

Так как в наследовании участвуют два класса, базовый и производный, не сразу понятно, какой же объект получится в результате. Внешне все выглядит так, словно новый класс имеет тот же интерфейс, что и базовый класс, плюс еще несколько дополнительных методов или же переопределенных методов, а так же полей. Однако наследование не просто копирует интерфейс базового класса. Когда вы создаете объект производного класса, внутри него содержится подобъект базового класса. Этот подобъект выглядит точно так же, как выглядел бы созданный обычным порядком объект базового класса. Поэтому извне представляется, будто бы в объекте производного класса «упакован» объект базового класса.

Как видно из данного примера у нас есть только объект класса С, но он содержит все поля и методы унаследованные от своих предков, классов А и В.

Полиморфизм – ковариантность возвращаемых типов.

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

Co001

И вывод этой программы:

Co002

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

В примере я специально добавил аннотацию @Override, чтобы было явно, что метод build() переопределен в подклассе.

Ковариантность, в данном примере, позволяет возвращать более специализированный тип Circle.

В этой программе, так же, есть один интересный момент: мы переопределили метод toString(), наследуемый от класса Object.

О классе Object мы поговорим чуть позже, а пока опять мотаем на ус.

Классы. Практика на понимание создания объектов.

Теперь, на основе предыдущих знаний еще чуть расширим свое сознание о том как и в какой очередности происходит создание объектов в памяти и инициализация полей значениями. Рассмотрим простой пример:

CC001Вывод этой программы уже должен быть вам понятен:

CC002

Смысл этой программы показать, что оператор new создает объект в памяти сразу после своего вызова и инициализирует все поля значениями по умолчанию.

После этого, последовательно, вызываются все инициализационные блоки и инициализаторы полей суперклассов (если они есть), а затем происходит вызов конструкторов суперклассов.

Как видно из вывода программы при создании объекта Sub, сперва были вызваны инициализационные блоки и инициализатор поля суперкласса. Затем был запущен конструктор суперкласса, но поскольку метод display() переопределен в подклассе Sub, то он нам вывел значение поля set объекта данного подкласса. Данный вывод подсвечен желтым в скрине. Далее происходит вывод значения поля set суперкласса. Затем отрабатывают инициализационные блоки подкласса и инициализация поля set подкласса. Далее отрабатывает конструктор подкласса в котором выводится значение поля set подкласса.

Полиморфизм. Поля и статические методы.

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

Полиморфизм поддерживается только для вызова обычных методов. А, например, прямое обращение к полю или статическому методу будет обработано на стадии компиляции. Данная статья это по существу продолжение этой.

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

FS001

Вывод у программы следующий:

FS002

Ссылка sup у нас имеет тип Super и ей присваивается ссылка на объект типа Sub. Обращение к полю происходит по версии ссылки, а обращение к методу по версии объекта. Во втором выводе работают все те же правила, а доступ к полю суперкласса мы получили через метод getSuperClass.

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

Как видно из скриншота ниже прямое обращение к полям компилируется в инструкцию invokespecial.

Обычные же методы – в инстуркцию invokevirtual.

Для sub все происходит точно так же, как и для sup.

FS003

Прошу обратить внимание, что объекты класса Sub фактически имеют два поля str: одно свое, другое унаследованное от суперкласса. Поэтому использовать одно имя для поля базового и производного класса вообще не рекомендуется, потому что это создает путаницу и вообще это плохая идея. Но знать о подобных граблях надо и мы еще к ним вернемся когда будем изучать внутренние классы. Там это может проявиться не явно. Поэтому сейчас мы уже начали готовится к новому удару граблями :)

Теперь перейдем к статическим методам. Статические методы не поддерживают полиморфного поведения.

Еще раз повторюсь, что во первых статические методы правильно вызывать по имени класса, и во вторых статические методы всегда вызываются по версии ссылки.

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

Полиморфизм – позднее связывание.

В первой статье по полиморфизму мы кратко познакомились с тем что это такое. По существу это просто переопределение методов суперкласса в подклассах. Но наверное вся мощь и красота этого еще не совсем понятна. И может не совсем ясно для чего все это нужно. Теперь попробуем разобраться более глубже. Приготовились к глубокой медитации. Оммммм…. Ну и погнали! :)

Возьмем затертый до дыр пример с фигурами. Не будем отклонятся от классиков жанра :)

И так общим суперклассом у нас будет класс Shape, и у него будут наследники царь, царевич, король, королевич, Circle, Square, Triangle. Но мы пойдем чуть дальше заезженного примера :) и образуем еще парочку наследников. Oval у нас будет наследником Circle, а Rect наследником Square.

На диаграмме все можно изобразить примерно так:

S0001

Методы drow() в каждом классе будут переопределены, а метод erase() будет просто наследоваться от Shape. Теперь осталось всю эту красоту забабахать в коде :)

S0002

Код у нас вышел очень красивый :) Буквочка к буквочке :) и вывод у него такой же :)

S0003

Теперь внимательно посмотрим на код. У нас есть одномерный массив shape классов Shape размером 6. И первому элементу массива мы присвоили ссылку на объект Shape (созадется new Shape()). А вот далее начинается магия, которую вы уже видели и должны понимать. Это называется восходящее преобразование. Я уже про это говорил, что ссылка суперкласса может указывать на объекты подклассов. И так далее мы присваиваем следующим элементам массива shape ссылки на подклассы. Но затем в выводе работает вообще сумасшедшая магия полиморфизма – вызываются методы подклассов, хотя ссылка имеет тип суперкласса.

Теперь вопрос от куда компилятор знает метод какого объекта должен быть вызван?

А компилятор то и не знает… :) Ну а кто же тогда знает?

Кто, кто? Дракон в пальто!

Хотя в приведенной программе это не очень очевидно, что компилятор не знает, так как мы присваиваем элементам массива ссылки на конкретные объекты.

Но я это сделал для простоты понимания и наглядности того что происходит.

Чтобы все было по честному и было видно кто компилятор точно не может знать на какой объект будут ссылаться элементы массива, эту программу можно модифицировать так, чтобы массив заполнялся случайным образом.

S0006Слева как раз и приведен пример измененного отрывка этой же программы, но массив уже заполняется случайным образом, что видно из вывода программы:

S0004

S0005

Встает все тот же вопрос – кто знает метод какого объекта надо вызывать в каждом конкретном случае? А знает это JVM. Но как она узнает? И тут начинается серьезная магия виртуальной машины Java вкупе с компилятором Java.

Сам компилятор не знает, но может подсказать JVM как надо обрабатывать инструкции вызова методов.

Чтобы в полной мере разобраться в сути про­исходящего, необходимо рассмотреть понятие связывания (binding).

Присоединение вызова метода к телу метода называется связыванием. Если связывание проводится перед запуском программы (компилятором и компоновщиком, если он есть), оно называется ранним связыванием (early binding). В процедурных языках никакого выбора связывания не было. Компиляторы C поддерживают только один тип вызова — раннее связывание.

Проблема определения метод какого объекта вызывать в нашей программе решается благодаря позднему связыванию (late binding), то есть связыванию, проводимому во время выполнения программы, в зависимости от типа объекта. Позднее связывание также называют динамическим (dynamic binding) или связыванием на стадии выполнения (runtime binding).

В языках, реализующих позднее связывание, должен существовать механизм определения фактического типа объекта во время работы программы, для вызова подходящего метода. Иначе говоря, компилятор не знает тип объекта, но механизм вызова методов определяет его и вызывает соответствующее тело метода. Механизм позднего связывания зависит от конкретного языка, но нетрудно предположить, что для его реализации в объекты должна включаться какая-то дополнительная информация. Теперь мы попытаемся выяснить, что же это за информация.

В прошлом посте, мы уже вкратце коснулись этого вопроса. Теперь постараемся понять более глубоко.

Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как private. Вызов private метода компилируется в инструкцию байт-кода invokespecial, которая вызывает реализацию метода из конкретного класса, определенного в момент компиляции. Вызов метода с другим уровнем доступа компилируется в invokevirtual, которая уже смотрит на тип объекта по ссылке в момент исполнения. Финальные неприватные методы тоже вызываются через invokevirtual.

В инструкцию байт-кода invokespesial компилируются:

  • Инициализационный вызов (<init>) при создании объекта
  • Вызов private метода
  • Вызов метода с использованием ключевого слова super

Есть конечно еще несколько других инструкций байт-кода для вызова методов: invokedynamic, invokeinterface и invokestatic. Но хотя об их использовании и говорят их названия, пока мы их обсуждать не будем. Если кому-то сильно хочется то можно почитать эту статью на враждебном каждому правоверному программисту буржуйском языке :) Чтиво полезное, но для понимания того о чём сейчас речь, достаточно того, что я тут уже написал. Так же можно почитать эту статью на родном языке.

И так, надо уже переходить к практике. Модифицируем программу из этого поста, следующим образом:

S0007

Я подсветил private и final модификаторы чтобы вы обратили на них внимание и затем на то, какой байт-код для них создаст компилятор. Вывод у нашей программы сейчас следующий:

Root
Branch

Заострю внимание на том, что ссылка root имеет тип Root, но указывает на объект типа Branch. И как я уже не однократно писал, обычные методы вызываются по версии объекта на который указывает ссылка. Именно через это свойство и реализуется полиморфизм.

Но в нашем случае, не смотря на это, первая команда вывела на консоль Root, а не Branch.

Теперь заглянем под капот этой программе при помощи команды: javap -c -p -v Root.class

Эта команда сгенерирует достаточно длинный вывод, но нам нужна только эта часть:

S0008

Как видно из вывода команда root.prt() была преобразована в вызов типа invokespecial, а команда branch.prt() в invokevirtual.

Вот мы и раскрыли магию всего этого действа. Надеюсь вам понравилось представление :) и теперь вы стали чуть больше понимать как работают полиморфные методы в Java.

2 июл. 2015 г.

Делегирование.

Еще один вид отношений, не поддерживаемый в Java напрямую, называется делегированием (delegation). Он занимает промежуточное положение между наследованием и композицией: экземпляр существующего класса включается в создаваемый класс (как при композиции), но в то же время все или некоторые методы встроенного объекта становятся доступными в новом классе (как при наследовании). Очень часто такие виды отношений используются при построении графического интерфейса, например для реализации модели MVC библиотека Swing использует делегирование.

А сейчас рассмотрим на простом примере что же это за зверь такой – делегирование.

Например, класс SpaceShipControls имитирует модуль управления космическим кораблем. А Для построения космического корабля можно воспользоваться наследованием в классе SpaceShip.

SS001

SS002

Однако космический корабль не может рассматриваться как частный случай своего управляющего модуля — несмотря на то, что ему, к примеру, можно приказать двигаться вперед (forward()). Точнее сказать, что SpaceShip содержит SpaceShipControls, и в то же время все методы последнего предоставляются классом SpaceShip. Проблема решается при помощи делегирования:

SS003

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

1 июл. 2015 г.

Композиция.

Механизм построения нового класса из объектов существующих классов называется композицией (composition).

И мы это уже не раз делали,  но просто не знали что это так называется :). Например в паттерне Builder мы внедрили объекты класса String (а String – это класс), как поля класса Contact. Точно так же мы могли бы использовать и объекты классов которые мы создали сами. Хотя по существу при композиции полями класса являются ссылки на объекты определенных классов. И мы можем использовать готовую функциональность этих классов. Например:

Co003

Вывод данной программы:

Co004

В этом примере видно что в наш класс MyClass мы включили два других класса как поля, а по существу как ссылки на объекты этих классов: String и MyString. Соответственно мы создали два поля ссылающихся на эти классы str и mystr, а затем в методе main() мы создали объект класса MyClass и использовали функциональность этих объектов, которые вывели нам результат.

Обратите внимание что в классе MyString мы переопределили метод класса Object toString(), что позволило нам вывести значение поля myStr, которое кстати тоже является ссылкой на объект.

Композиция позволяет повторно использовать существующий код или функциональность классов.

Но в случае композиции мы ни как не можем повлиять на эту функциональность используемых классов. Мы можем только использовать ее, но не изменить поведение, так как нам может быть нужно. Но есть и другой способ который гораздо интереснее когда новый класс создается как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса. Этот механизм называется наследованием (inheritance). И далее мы переходим к изучению наследования в Java.

3 мая 2015 г.

Оператор return

Оператор return используется для выполнения явного возврата из метода. То есть он снова передает управление объекту, который вызвал данный метод. Как таковой этот оператор относится к операторам перехода. Хотя полное описание оператора return придется отложить до рассмотрения классов и их методов, все же кратко ознакомимся с его особенностями.

Оператор return предписывает интерпретатору Java остановить выполнение текущего метода. Если метод возвращает значение, оператор return сопровождается некоторым выражением. Значение этого выражения становится возвращаемым значением метода.

Некоторые методы объявляются как void, так как они не возвращают никакого значения. Интерпретатор Java выполняет такие методы путем последовательного выполнения операторов до тех пор, пока он не достигнет конца метода. Выполнив последний оператор, интерпретатор выполняет возврат. Однако иногда метод void должен завершиться, не дожидаясь выполнения последнего оператора. В этом случае можно использовать оператор return без выражения.

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

Чтобы изобразить пример нам понадобятся два класса, один с методом main, другой просто класс с вызываемыми статическими методами. Если не понятно что такое статические методы, то не переживайте, это скажем ну просто вызываемые “подпрограммы”. И так код в студию:

“Главная программа” (главный класс с методом main)

R0001

В строках 8 и 10 вызывается метод vReturn, который не возвращает ни каких значений, а просто вывод, в зависимости от переданного в него значения, то или иное сообщение.

В строках с 13 по 15 вызывается метод strReturn, который возвращает разные строки в зависимости от переданного в него значения.

Передаваемые в методы значения заключены в круглые скобки.

И вызываемая “подпрогорамма” (класс с вызываемыми статическими методами)

R0002

Метод vReturn не выводит ни какого сообщения если в него передано значение больше пяти, в ином случае он выводит сообщение.

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

В операторе if в строках 13 и 14 не использованы фигурные скобки, так как в них нет необходимости, хотя это и повысило бы читаемость кода.

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

И надеюсь вывод, который генерируют данные классы, поможет понять как работает оператор return:

R0003

Первый вызов метода vReturn был сделан с параметром 3 и поскольку он меньше 5, то было напечатано сообщение из этого метода. Второй же вызов этого метода был сделан с параметром 7 и поэтому сразу же произошел возврат из метода, так как 7 больше 5 и данный метод ни чего не напечатал.

Метод strReturn возвращает строку в зависимости от переданного в нее параметра.

Оператор continue

Если оператор break прерывает цикл, то оператор continue завершает текущую итерацию цикла и начинает новую. Оператор continue, как с меткой, так и без нее, можно использовать только внутри цикла while, do или for. Оператор continue без метки заставляет ближайший цикл начать новую итерацию. Оператор continue с меткой, то есть с именем окружающего цикла, заставляет этот цикл начать новую итерацию.

Ну и теперь немного примеров:

C00001

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

C00002

 

C00003

Данный фрагмент показывает пример работы оператора continue, который завершает текущую итерацию и начинает следующую итерацию внешнего цикла по метке outer. То есть прерывается текущая итерация как внутреннего цикла (строка 16), так соответственно и внешнего. И затем начинается новая итерация внешнего цикла.
Данный код выводит псевдо-графический треугольник.

C00004

В каждом из циклов while, do и for оператор continue начинает новую итерацию по-разному:

  • В случае цикла while интерпретатор Java просто возвращается в начало цикла, еще раз проверяет условие и, если оно равно true, снова выполняет тело цикла.
  • В случае цикла do интерпретатор Java переходит в конец цикла, где он проверяет условие цикла, чтобы определить, нужно ли выполнять следующую итерацию цикла.
  • Для цикла for интерпретатор переходит в начало цикла, где он сначала вычисляет выражение итерационное выражение, а затем выражение проверки условий цикла, определяя необходимость повторного выполнения цикла.

Оператор break

С оператором break мы уже встречались когда изучали оператор switch, а так же в операторах циклов.

В Java оператор break находит три применения. Во-первых, как уже было показано, он завершает последовательность операторов в операторе switch. Во-вторых, его можно использовать для выхода из цикла. И, в-третьих, его можно использовать в качестве “цивилизованной” формы оператора безусловного перехода (“goto”). С оператором switch мы уже разобрались поэтому рассмотрим последние два применения.

Оператор break заставляет интерпретатор Java сразу перейти к окончанию составного оператора, в который входит этот оператор break.

После оператора break может стоять метка составного оператора. В таком виде оператор break заставляет интерпретатор Java немедленно выйти из указанного блока, который может быть любым оператором, а не только оператором switch или циклом.

Метка должна быть объявлена до ее использования в операторе break. Метка представляет из себя любой допустимый идентификатор Java заканчивающийся двоеточием (:). Примеры меток:

label1:
loop_01:

Примеры оператора break с меткой:

break label1;
break loop_01;

Ну а теперь рассмотрим несколько примеров…

B00001

Оператор break в строке 10 прерывает выполнение цикла for (строка 8) и передает управление на оператор следующий за циклом, то есть на строку 13. Таким образом, что хоть цикл и определен отработать 100 итераций, он ни когда этого не сделает, так как по условию в 9 строке выполнит оператор break. Данный код сгенерирует вот такой вывод:

B00002

 

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

B00003

В данном коде есть вложенный цикл (строка 18), который, по идее, должен отработать 99 итераций, но такого не случается поскольку срабатывает условие в 18 строке, которое прерывает выполнение внутреннего цикла.

Но это не оказывает ни какого влияния на внешний цикл, который отрабатывает положенные три раза.

Ниже приведен вывод генерируемый данным кодом:

B00004

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

B00005

Теперь данный код сгенерирует вот такой вывод:

B00006

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

Еще раз напомню что нельзя выполнить переход к какой-либо метке, которая определена не для содержащего данный оператор break блока.

В примере выше, строка 26 и соответственно оператор break входит в блок кода из строк 23-29, но если бы метка определена за пределами этого блока (в данном примере), то программа бы просто даже не откомпилировалась.

Теперь проведем работы по расширению сознания понимания оператора break. И сделаем это на примере следующего кода:

B00007

Сразу хочу заметить что идентификаторы меток я сделал на русском языке просто так для разнообразия.

Ну а теперь по самой программе. Оператор break в строке 20 прерывает исполнение блока 01 (строка 9) и передает управление на строку 32, которая следует за блоком 01.

Оператор break в строке 18 прерывает выполнение оператора switch и передает управление на строку 26.

Тут важно уяснить что break прерывает исполнение блока кода (составного оператора). Если метка не указана, то прерывает исполнение блока к которому принадлежит, если метка указана, то прерывает исполнение указанного блока.

Вывод данной программы зависит от количества переданных аргументов в командной строке.

Например:

B00008

В первом случае в командной строке не было передано ни одного аргумента, поэтому выполнились все блоки программы с 1 по 3.

Во втором случае был передан один аргумент, поэтому выполнение блока 01 было прервано, и соответственно было прервано выполнение блоков 02 и 03, следовательно не были выведены сообщения из концов данных блоков, а в первом случае они вывелись.

В общем, как уже и говорилось, оператор break можно использовать не только с операторами циклов и switch, но и с любым составным оператором (блоком кода).

29 апр. 2015 г.

Операторы for и foreach

Начиная с версии JDK 5, в Java существуют две формы цикла for. Первая — традиционная форма, используемая начиная с исходной версии Java. Вторая — новая форма “for-each”. Мы рассмотрим оба типа цикла for, начиная с традиционной формы.

Общая форма традиционного оператора for выглядит следующим образом:

F00001

Если в цикле будет повторяться только один оператор, фигурные скобки можно опустить.

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

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

Приведем пару примеров поясняющих все вышесказанное:

F00002

В этом примере переменная i объявлена вне цикла (строка 7), поэтому она так же доступна после его завершения (строка 12).

Из вывода выполнения данной программы видно, что выражение повторения цикла, а именно префиксный инкремент (++i) переменной i выполняется после выполнения тела цикла, то есть после выполнения строки 10, которая выводит приветствие.

Этот момент очень важно понимать, чтобы иметь правильное представление о работе цикла for.

 

Теперь посмотрим на вывод этой программы с аргументами командной строки и без них:

F00003

Как видно из вывода данной программы приращение переменной i происходит после выполнения последней команды цикла, которая выводит приветствие (строка 10).

А теперь объявим переменную внутри цикла (оператора for):

F00004

Как видно Eclipse нам сразу же указал на ошибку, что переменная j, объявленная в строке 15, не видна вне цикла, так как ее область действия или область видимости распространяется только на тело цикла, в котором она была объявлена.

Чтобы программа заработала необходимо закомментировать строку 19.

Вывод этого кода, аналогичен выводу, кода который мы только что рассмотрели, за исключением того, что вместо “Привет” выводится “Hello”. Ну и то что после цикла не возможно вывести значение переменной j.

При объявлении переменной внутри цикла for необходимо помнить о следующем важном обстоятельстве: область и время существования этой переменной полностью совпадают с областью и временем существования оператора for.

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

Например:

F00005

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

Данный код генерирует следующий вывод:

F00006

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

F00007

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

По существу эта программа делает то же приветствие аргументов, если они есть. Если их нет, то ни чего не выводит. Сразу же приведу пример ее вывода:

F00008

Как видно из вывода этой программы, итерационная часть выполняется, как уже и говорилось, после выполнения тела цикла. В данном случае это оператор println в строке 9. Оператор for в данном коде растянулся на две строки 9 и 10, поскольку он достаточно длинный. Я это сделал для демонстрации того, что каждая часть оператора for может быть применена в разных целях. Стоит еще заметить, что приращение переменной i происходит в строке 12 и там же задается условие для продолжения или выхода из цикла, которое проверяется в строке 9.

Еще один подобный пример, цикл for можно задействовать для прохождения по элементам связного списка:

F00009

Стоит, так же, отметить, что любую из частей цикла for (инициализацию, условие и итерационную) или даже все можно пропустить. Например, можно создать таким образом бесконечный цикл:

for (;;){
   
//бесконечный цикл
}

Инициализационное или итерационное выражения либо они оба могут отсутствовать:

F00010

В этом примере инициализационное и итерационное выражения вынесены за пределы определения оператора for. В результате соответствующие части оператора for пусты.

 

 

Чтобы последовательность выполнения частей оператора for стала более наглядный я приведу небольшой пример. Хотя методы мы еще не изучали, но я надеюсь идея этой программы будет вам понята. Смысл ее в том, чтобы наглядно показать последовательность выполнения всех частей оператора for.

for01

for02

Из вывода программы видно, что инициализационная часть программы (метод initTest()) выполняется только один раз.

Затем выполняется проверка условия, представленная методом condTest().

После проверки условия, выполняется тело цикла.

И уже после этого выполняется часть повторение, представленная методом recTest().

В методе condTest() выполняется проверка условия продолжения цикла. В данном случае переменная i сравнивается c 4, и пока переменная i меньше 4, то тело цикла выполняется.

Тело цикла выполняется четыре раза так как переменная i была по умолчанию проинициализирована нулем.

Оператор foreach

Начиная с версии JDK 5 в Java можно использовать вторую форму цикла for, реализующую цикл в стиле foreach (“для каждого”). Цикл в стиле foreach предназначен для строго последовательного выполнения повторяющихся действий по отношению к коллекциям объектов, например, таких как массивы. В Java возможность применения цикла foreach реализована за счет усовершенствования цикла for.  Общая форма версии foreach цикла for имеет следующий вид:

for (тип итерационная переменная : коллекция) блок-операторов

Тип это тип переменной, итерационная переменная — имя итерационной переменной, которая последовательно будет принимать значения из коллекции, от первого до последнего. Элемент коллекция указывает коллекцию, по которой должен выполняться цикл. С циклом for можно применять различные типы коллекций, но пока мы будем использовать только массивы, кстати которые тоже пока не проходили, но по крайней мере уже было много примеров с приветствиями из массива строк, куда попадают аргументы командной строки.

На заметку: оператор foreach применим к массивам и классам, реализующим интерфейс java.lang.Iterable.

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

Хотя повторение цикла for в стиле foreach выполняется до тех пор, пока не будут обработаны все элементы массива (коллекции), цикл можно прервать и раньше, используя оператор break.

Поскольку итерационная переменная получает значения из коллекции, ее тип должен совпадать (или быть совместимым) с типом элементов, хранящихся в коллекции. Таким образом, при выполнении цикла по массивам тип должен быть совместим с базовым типом массива.

Чтобы понять побудительные причины применения циклов в стиле foreach, рассмотрим тип цикла for, для замены которого предназначен этот стиль.

Возьмем опять наш пример с приветствием аргументов из командной строки:

F00011

Не правда ли, это куда элегантней, чем применение других операторов цикла, для этой цели?

Собственно у этой программы простой вывод:

F00012

Мы его уже много раз видели в разных вариантах, но повторенье – мать ученья.

 

Для полной ясности рассмотрим еще несколько примеров.

F00013

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

Вывод этой части программы такой:

F00014

Хотя повторение цикла for в стиле foreach выполняется до тех пор, пока не будут обработаны все элементы массива, цикл можно прервать и раньше, используя оператор break. Например:

F00015

В данном примере цикл отработает только три итерации, после чего произойдет выход из цикла по условию оператора if, который вызовет срабатывание оператора break.

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

Данный код выведет следующее:

F00016

Любой метод, возвращающий массив, может использоваться с foreach. Например класс String содержит метод toCharArray, возвращающий массив char. Пример:

F00017Данный код просто выведет посимвольно строку Привет Мир!

 

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

На последок, приведу еще пару примеров продвинутого оператора for. По большому счету, это просто отрефактореный код того примера что я уже приводил.

F00018 Не правда ли, что этот код стал более читаем и понятен, чем тот, что я уже приводил? Или же не понятно? Ну тогда смотрим на другой пример кода, который делает тоже самое.

F00019

Неужели опять не понятно? Smile

Оба этих кода делают одинаковый вывод:

 

F00020

Конечно при условии что аргументами в командной строке были Вася и Петя.

На этом с оператором for и его тенью foreach закончим.