Часть I. Теория
Глава 1. Введение
Язык программирования — это инструмент решения прикладных задач. В идеале разработчик должен хорошо разбираться в нескольких языках программирования и подбирать инструмент под задачу, а не пытаться подогнать задачу под возможности инструмента.
После прочтения этой книги вы получите достаточно полное представление о языке Java и его возможностях. Может даже оказаться, что он не подходит для ваших сегодняшних задач. Замечательно! — вы не потеряете напрасно время на чтение толстых учебников и углубленное изучение языка. Зато вы будете хорошо знать, для чего пригодится язык Java, и сможете вернуться к нему в любое время. Если Java — именно то, что вам сейчас нужно, то после прочтения этой книги будет легче приступить к углубленному изучению языка.
Объем и сложность материала вводного курса подобраны таким образом, чтобы уделяя по вечерам 1–2 часа на чтение и работу с компьютером, вы приблизительно за неделю смогли овладеть навыками программирования на языке Java в среде разработки NetBeans.
Разумеется, эта книга станет лишь первым шагом в изучении языка Java и среды разработки NetBeans. Впереди вас ждет поиск и усвоение огромного объема информации.
1.1 Особенности текста книги и архив файлов
Книга подготовлена и опубликована при помощи издательского сервиса Ridero. Это новый проект, который помогает издавать книги быстрее и делать их дешевле и доступнее.
Но технические возможности издателя пока не полностью адаптированы к изданию технических текстов. Например, система набора текста автоматически заменяет в листингах двойные «технические» кавычки на «лингвистические», двойной минус (декремент) на длинное тире. Мы просим отнестись с пониманием к этим мелким временным недостаткам издательского сервиса.
В файловом архиве книги вы найдете полные исходные коды всех примеров программ из книги, а также дополнительные файлы с наборами иконок для графического интерфейса. Архив можно скачать из файловых хранилищ
Dropbox:
https://www.dropbox.com/s/wo0u8916cnyc31p/Java_Files.zip?dl=0
Яндекс Диск:
https://yadi.sk/d/fIoAfXyp3Sj8gP
1.2 Идеология Java
Разработка языка Java началась в 1990 году под названием Oak (дуб) — не самое лучшее название для интеллектуального продукта. В процессе работы значительно изменилась концепция языка, а затем и его название. Окончательный вариант открытого и общедоступного языка Java был обнародован в 1995 году.
Нельзя сказать, что новый язык легко и быстро завоевал популярность, но сегодня это самый востребованный язык программирования. Он удачно занял нишу языка для приложений массового пользования, широко распространяемых через Интернет. Для таких приложений важна независимость от платформы — работа прикладной программы не должна зависеть от аппаратной части компьютера и операционной системы.
При распространении программ на языке Java не возникает проблем с отсутствием на компьютере пользователя нужных программных библиотек или модулей. Дистрибутив программы на языке Java, как правило, состоит из одного файла, который содержит в себе всё необходимое для работы приложения на любом компьютере с установленной Java-машиной. Впрочем, в состав дистрибутива иногда могут входить отдельные внешние файлы настроек или базы данных, которые невозможно упаковать внутрь файла скомпилированного приложения.
Язык Java популярен еще и потому, что применяется при разработке приложений Android. Можно писать приложения на «чистом» языке Java (в реализации Java Mobile) или использовать среду разработки, предоставляющую расширенные возможности. В любом случае знание Java является обязательным условием для разработчика приложений Android.
Официальная среда разработки программ на Java полностью бесплатная, включая большое количество дополнительных модулей и библиотек, разработанных сообществом программистов. Существуют платные инструменты разработки, но мы прекрасно обойдемся без них.
У языка Java низкий порог вхождения — первые полезные приложения с полноценным графическим интерфейсом можно создавать через несколько дней после начала изучения языка. По этой причине язык Java очень популярен, например, среди радиолюбителей, которые разрабатывают собственные приложения для взаимодействия компьютеров с электронными устройствами.
Разумеется, Java широко применяется в профессиональной среде. Это мощный язык программирования с поддержкой многопоточности, на котором разработано большое количество коммерческих приложений.
Давайте разберемся, как работает Java–программа.
1.3 Как работает Java
Языки программирования общего назначения можно разделить на интерпретирующие и компилирующие.
В первом случае специальная программа–интерпретатор поочередно преобразовывает каждую строку программы в команды процессора и отправляет их на выполнение под управлением операционной системы. Поэтому для каждого типа процессора и операционной системы нужна отдельная версия интерпретатора. Интерпретируемые программы работают медленнее, чем скомпилированные, потому что построчное преобразование программы в двоичный код занимает больше времени, чем выполнение готового кода. Но существуют ситуации, когда применение интерпретатора оправдано.
Во втором случае компилятор заранее и полностью преобразует программу в бинарный процессорный код. Эта процедура выполняется один раз. Далее программа распространяется в виде готового кода и может быть запущена без участия компилятора. При этом тоже необходимо обеспечить совместимость кода программы с процессором и операционной системой компьютера пользователя.
Java — необычный язык программирования. При компиляции программа на языке Java превращается в специальный байт-код. Он представляет собой набор унифицированных инструкций для специальной Java-машины (Java Virtual Machine, JVM), установленной на компьютере. Иными словами, программа выполняется внутри виртуальной машины, которая служит «посредником» между программой и компьютером.
Совместимость вашего приложения с аппаратной частью компьютера и операционной системой обеспечивается виртуальной машиной. Вы можете из программы обращаться к портам компьютера, файловой или графической системе, и совершенно не задумываться о том, как это будет реализовано. Об этом позаботились разработчики нужной версии Java-машины.
Таким образом, вместо того, чтобы разрабатывать разные версии прикладной программы, достаточно установить на компьютер готовую и бесплатную Java-машину, которая учитывает и реализует особенности операционной системы. Виртуальные машины Java для большинства операционных систем можно скачать на сайте www.java.com. В операционную систему Android поддержка Java встроена по умолчанию.
Java-машина не занимает много места в памяти компьютера. Времена, когда программы на языке Java долго запускались и медленно работали, остались в прошлом. Сейчас они лишь незначительно отстают в быстродействии от обычных скомпилированных программ.
Чтобы избежать путаницы, отметим, что язык JavaScript не имеет ничего общего с языком Java. Это язык для написания сценариев (скриптов), которые включены в состав HTML-страниц и выполняются средствами браузера. Слово «Java» было добавлено компанией Netscape — разработчиком языка JavaScript — исключительно из маркетинговых соображений.
1.4 Что читать дальше?
О программировании на языке Java издано много хороших книг, в том числе на русском языке. Настоятельно рекомендую несколько изданий, которые особенно хороши для знакомства с Java:
Хабибуллин И. Ш. Самоучитель Java. — 3-е изд., перераб. и доп. — СПб.: БХВ-Петербург, 2008. — 768 с.
Хабибуллин И. Ш. Java 7 в подлиннике. — СПб.: БХВ-Петербург, 2012. — 768с.
Прохоренок Н. А. Основы Java. — СПб.: БХВ-Петербург, 2017. — 704 с.
Васильев А. Н. Программирование на Java для начинающих. — Москва: Издательство «Э», 2017. — 704с.
Монахов В. В. Язык программирования Java и среда NetBeans. — СПб.: БХВ-Петербург, 2012. — 704с. + DVD.
1.5 Другие книги автора
Друзья, если вы интересуетесь техническим творчеством и программированием микроконтроллеров, вам могут пригодиться эти книги:
https://www.ozon.ru/context/detail/id/141872715/
https://www.ozon.ru/context/detail/id/135412298/
Глава 2. Подготовка к работе с Java
Давно остались в прошлом времена, когда программист набирал исходный код программы в текстовом редакторе, а затем запускал компилятор в командной строке и мучительно пытался разобраться в сообщениях об ошибках. Теперь любой серьезный язык программирования располагает интегрированной средой разработки (Integrated Development Environment, IDE). Это специальный набор инструментов разработчика, который может включать в себя редактор со множеством удобных функций, средство управления проектами, компилятор, отладчик, эмулятор мобильных устройств, справочную систему и многое другое.
В настоящее время для программирования на языке Java применяется несколько популярных сред разработки: NetBeans, Eclipse, JDeveloper, JBuilder, IntelliJ IDEA.
В этой книге вы познакомитесь с бесплатной средой NetBeans, которая разработана корпорацией Sun и распространяется с открытым исходным кодом. Допускается использование среды в коммерческих разработках. Название среды содержит игру слов: Java — сорт кофе, Beans — зерна. В тоже время словосочетание Net Beans можно перевести как «сетевые компоненты», потому что дополнительные компоненты IDE можно скачивать из сети по мере необходимости.
Пусть вас не смущает бесплатность и открытость исходного кода NetBeans IDE. По мнению многих профессиональных программистов, эта среда является самой удобной и развитой. Она оснащена отличным редактором графических интерфейсов и продвинутым средством разработки приложений для мобильных устройств.
Возможности среды можно расширять при помощи бесплатных плагинов, которые размещены в специальном репозитории.
2.1 Устанавливаем JDK и NetBeans
Чтобы приступить к программированию на Java, вы должны установить на свой компьютер два обязательных компонента: JDK и NetBeans.
JDK (Java Development Kit) — средство разработки, в состав которого входит компилятор, библиотеки, справочная документация, и собственно сама среда выполнения программ JRE (Java Runtime Environment). В принципе, можно обойтись этим набором, но вам придется набирать код программы в каком-то текстовом редакторе и вручную запускать компилятор из командной строки. В таком случае не может быть речи о средствах отладки или визуального редактирования интерфейсов.
Для полноценной и комфортной работы после установки JDK необходимо установить оболочку NetBeans. Причем установку следует выполнять именно в таком порядке — сначала JDK, затем NetBeans. В противном случае вам придется вручную указать пути к файлам JDK в настройках NetBeans. Но мы поступим еще проще и воспользуемся составным пакетом «JDK + NetBeans Bundle». Установщик пакета сделает за нас всю работу.
Войдите на сайт Oracle по адресу
http://www.oracle.com/technetwork/java/javase/downloads/jdk-netbeans-jsp-142931.html
Если к тому моменту, когда вы читаете эту книгу, изменится версия JDK или NetBeans IDE, то указанная ссылка может перестать работать. В таком случае введите в любую поисковую систему ключевые слова «JDK NetBeans IDE bundle», и это будет проще, чем искать новую ссылку на запутанном корпоративном сайте Oracle.
Щелкните по пункту «Accept License Agreement» (Принять лицензионное соглашение) и скачайте установочный файл для своей операционной системы. Обратите внимание на соответствие разрядности. В примере на рис. 2.1 выбран пакет для 32–разрядной ОС Windows.
Запустите установочный файл и согласитесь со всеми пунктами настроек. На момент подготовки книги распространялась версия NetBeans IDE 8.2. При первом запуске NetBeans наверняка обнаружит обновления и предложит загрузить их. После установки обновлений можно приступать к знакомству с интерфейсом NetBeans и написанию первой программы.
2.2 Соглашение об именах
Прежде, чем приступить к созданию первого проекта, сделаем небольшое отступление и перечислим основные правила составления имен в языке Java. Обязательно ли нужно выполнять эти правила? Если на званом ужине вы будете вытирать руки о скатерть, то вас не прогонят из-за стола. Но второй раз не пригласят. С выполнением общепринятых соглашений в программировании похожая ситуация. Отклонение от правил именования в большинстве случаев не вызовет ошибку компиляции, но затруднит понимание исходного кода другими программистами. Более того, даже вам будет трудно разрабатывать и отлаживать собственный код, обращаясь к чужим примерам с корректно заданными именами. Надо с первых шагов приучить себя к строгому соблюдению как формальных, так и неписаных правил программирования. Работоспособность приложения — не единственный критерий качества кода.
Язык Java регистрозависимый. Например, filesize и fileSize — разные имена. Тем не менее, лучше избегать использования имен, которые различаются лишь регистром символов, чтобы не затруднять понимание и отладку программы. Обычно «горбатый регистр» (Camel casing) применяется для выделения первых букв слова в составном имени, например, MyFirstClass.
Итак, вот пункты соглашения об именах Java (в скобках приведены примеры):
Пакеты и подпакеты — существительные в единственном числе, только в нижнем регистре, в составных именах слова разделяются подчеркиванием (input_control).
Классы и интерфейсы — существительные или словосочетания в значении существительного. Первые буквы слов в верхнем регистре, слова не разделяются (UserInfo). Имена классов–исключений заканчиваются словом Exception (InvalidCountException).
Классы–наследники — рекомендуется использовать имена, в которых содержится имя родительского класса (LocalConnect extends Connect). Исключение составляют имена классов–наследников, из которых очевидно, что они наследуют суперкласс (Oval extends Figures).
Поля и локальные переменные — существительные в нижнем регистре (size). Если название составное, то следующие слова начинаются с заглавной буквы, разделители не используются (imageHeight). Имена переменных должны соответствовать типу хранимых данных. Например, имя переменной currentUser интуитивно соответствует номеру пользователя (целое число). Для хранения имени пользователя (строка) лучше использовать переменную с именем currentUserName.
Переменные типа static final — существительные или словосочетания в верхнем регистре, слова разделены подчеркиваниями (MAIN_COLOUR).
Методы — глаголы в нижнем регистре (calculate) или словосочетания, отражающие действие (printAmount). Глаголы должны максимально полно и точно описывать действие, которое выполняет метод.
Имена методов, выполняющих чтение или изменение значений полей класса, должны начинаться на get и set соответственно (getFileSize, setFontColour). Исключение составляют методы, возвращающие значения полей типа boolean. Их имена должны начинаться на is (isFileOpen).
Имена методов, выполняющих преобразование к другому типу данных, начинаются на to (toString).
Имена методов, которые создают и возвращают объект, начинаются с create (createDataset).
Имена методов, инициализирующих поля класса или элементы графического интерфейса, начинаются с init (initWindow) и применяются только в конструкторе класса.
2.2.1 Зарезервированные слова и литералы
В таблице 2.1 приведены ключевые слова, зарезервированные для синтаксических конструкций языка Java. Они не могут быть именами переменных, классов и т.п., их нельзя переопределять.
2.3 Первый проект на Java
Сейчас вы создадите свой первый проект. Возможно, у вас появится много вопросов, но не волнуйтесь — ответы будут даны позже. Сначала вы должны получить базовые навыки работы с NetBeans IDE, чтобы использовать примеры по мере прочтения книги.
Запустите NetBeans. Выберите пункты меню Файл | Создать проект или нажмите на значок с изображением зеленой папки и символа «+». Выберите категорию Java и тип проекта Приложение Java (рис. 2.2). Введите имя нового проекта. Пусть это будет HelloJava (рис. 2.3).
Обратите внимание, что вам предложено создать главный класс, для которого автоматически сформировано имя пакета. Так как название пакета состоит из двух слов, поставьте между ними подчеркивание, чтобы имя пакета полностью соответствовало соглашению об именах. Нажмите кнопку Готово, и через несколько секунд NetBeans сформирует новый проект и откроет шаблон исходного кода.
Пока не обращайте внимания на серые строки комментариев, где предложено ввести информацию о лицензии и авторе проекта. Если эти комментарии мешают, удалите их.
Найдите строку
// TODO code application logic here
Она указывает на место, в которое необходимо вставить основной исполняемый код. Вместо этой строки введите
System.out.println («Hello Java»);
У вас должна получиться программа как в листинге 2.1 (комментарии удалены).
Листинг 2.1 Первая программа на языке Java
package hello_java;
public class HelloJava {
public static void main (String [] args) {
System.out.println («Hello Java»);
}
}
Запустите программу на выполнение, нажав значок зеленого треугольника или выбрав пункт меню Выполнить | Запустить проект. Спустя несколько секунд сборка проекта будет завершена. В нижней части интерфейса NetBeans откроется окно терминала, в который будет выведен текст «Hello Java» и сообщение об успешной сборке проекта (рис. 2.4).
Попробуйте совершить ошибку в тексте программы и посмотрите, как отреагирует среда разработки. Удалите одну из кавычек, обрамляющих строку «Hello Java». Система контроля синтаксиса немедленно отреагирует на ошибку. Ближайшая круглая скобка будет выделена красным цветом (из-за отсутствующей кавычки эта скобка оказалась не на своем месте), а напротив строки, содержащей ошибку, появился восклицательный знак на красном фоне. Это обозначение критической ошибки, которая приведет к ошибке компиляции. При наведении указателя мыши на значок ошибки появляется всплывающая подсказка (рис. 2.5).
Теперь сделайте ошибку в названии пакета, и вместо hellojava в первой строке введите yellojava. Слева от строки вновь появился значок, только теперь это лампочка с маленьким восклицательным знаком. Это означает, что система не видит здесь фатальную синтаксическую ошибку, которая требует обязательной правки кода, а лишь уведомляет, что вы что-то перепутали или упустили. В данном случае вы ссылаетесь на пакет, которого нет в проекте. Если вы и в самом деле включите в состав проекта пакет с названием yellojava, то значок ошибки исчезнет.
Если вопреки сообщениям об ошибке принудительно запустить компиляцию проекта, то в окне системного терминала будет выведено диагностическое сообщение с указанием строки (или нескольких строк), где присутствуют ошибки. Щелкните на ссылку в сообщении, и курсор в окне редактора автоматически переместится на нужную строку программы.
2.4 Забегая вперед: классы, объекты и методы
Изучение сложного языка программирования — это борьба за первенство между курицей и яйцом. Чтобы понять программу на языке Java, необходимо владеть основными понятиями объектно-ориентированного программирования (ООП). С другой стороны, чтобы изучить понятия ООП применительно к Java, сначала надо познакомиться с синтаксисом и операторами. Если вы уже знакомы с ООП по другим языкам, то вам будет намного проще изучать Java.
Чтобы продолжить рассказ о языке Java и среде разработки, я немного забегу вперед и скажу несколько слов о классах, объектах и методах. Более подробно об этом будет рассказано в главе 6 «Классы и объекты». Если есть желание, можете перейти к чтению главы 6 прямо сейчас, а затем вернуться к главе 2.
Итак, любая программа Java состоит из классов, на основе которых создаются объекты. Объект в общем случае представляет собой набор переменных и методов. Метод — это именованный фрагмент кода, предназначенного для обработки переменных объекта и выполнения иных действий.
Программа практически всегда содержит главный класс и главный метод main (), который выполняется при запуске программы.
Вернемся к листингу 2.1. В нем объявлен главный класс HelloJava, который содержит единственный метод main (). Если вы не объявили главный класс при создании нового проекта, то впоследствии компилятор все равно спросит вас, какой класс считать главным.
2.5 Структура проекта Java
Современные программы давно перестали состоять из одного файла, поэтому теперь вместо «программа» принято говорить «проект». Как вы сейчас увидите, даже если имеется всего один файл исходного кода, проект приложения на языке Java включает в себя и другие компоненты. Далее в книге мы будем применять термин «программа» только к ограниченным фрагментам кода в примерах или к отдельным файлам кода. Говоря о приложении в целом, будем использовать слов «проект».
На вершине иерархии Java располагается собственно проект, с создания которого мы начинаем свою работу (рис. 2.6). В нашем случае это проект под названием HelloJava. Проект состоит из одного или нескольких пакетов исходных кодов, а также подключаемых библиотек.
Даже небольшой учебный проект может состоять из нескольких десятков классов. Серьезные коммерческие проекты, которые разработаны коллективом программистов, состоят из тысяч классов. В такой ситуации возникает реальная проблема конфликта имен. С одной стороны, желательно использовать наглядные имена, которые облегчают понимание, отладку и документирование кода. С другой стороны, если классов сотни и тысячи, то неизбежны совпадения имен классов.
Для устранения возможных конфликтов имен классов и четкого структурирования проекта применяется разбиение на пакеты.
С физической точки зрения пакет Java — это отдельный каталог (папка) на диске компьютера. Имя каталога совпадает с именем пакета. Например, если вы установили NetBeans IDE на компьютер с ОС Windows с настройками по умолчанию, то в папке Documents будет создана папка NetBeansProjects. В ней расположены папки проектов. Сейчас там появилась папка HelloJava, внутри нее находится папка src. Она соответствует папке «Пакеты исходных кодов» на рис. 2.6. Внутри нее находится собственно папка пакета hellojava, которая содержит файл HelloJava. java. Как видите, физическая структура каталогов повторяет структуру проекта, отображаемую в окне NetBeans IDE.
При разработке простого приложения имя пакета можно не указывать, и среда NetBeans автоматически создаст безымянный «пакет по умолчанию». Но лучше сразу привыкать к использованию именованных пакетов.
Каждый пакет формирует отдельное пространство имен. Это важно для крупных профессиональных разработок, когда один и тот же пакет может быть включен в состав различных независимых проектов. Благодаря разделению классов по пакетам, разработчики застрахованы от случайных конфликтов имен.
В первой строке листинга 2.1 мы указали, что создаем пакет hello_java и работаем в его пространстве имен. Допускается создание подпакетов (вложенных пакетов). В таком случае имя пакета и подпакета разделяется точкой:
package main_pack.sub_pack;
Глубина вложенности пакетов формально не ограничена. Физическая структура файлов и папок на диске компьютера должна соответствовать структуре вложенных пакетов проекта.
Чтобы воспользоваться в программе классами из стороннего пакета, его нужно импортировать при помощи инструкции import. После нее указывают имя пакета и, через точку, имя импортируемого класса или звездочку *, если импортируются все публичные классы пакета:
import mypack.MyClass;
import nextpack.*;
Использование звездочки не увеличит размер приложения, потому что компилятор все равно включит в него только нужные классы. Но если пакет содержит несколько сотен или тысяч классов, то время компиляции может заметно возрасти.
Теперь разверните в окне просмотра проекта (рис. 2.6) папку «Библиотеки». По умолчанию там находится главный системный пакет JDK, который содержит предоставленные разработчиком классы для работы с системой. Этот пакет всегда подключен на уровне среды разработки, поэтому в явном импорте классов SDK нет нужды.
Теперь мы можем сказать, что означает строка из листинга 2.1:
System.out.println («Hello Java»);
В этой строке мы последовательно обращаемся к встроенному классу System, его полю out и методу println (String). Компилятор преобразует эту строку в байт–код, который заставит виртуальную Java–машину вывести в окно терминала строку текста.
При разработке собственных приложений вы можете подключать к проекту библиотеки сторонних разработчиков. Например, чтобы работать с последовательными портами компьютера, можно воспользоваться библиотекой JSSC, а для работы с базами данных MS Access пригодится библиотека UCanAccess.
Глава 3. Переменные и операторы
Вы получили общее представление о языке Java. Теперь настало время перейти к более конкретным понятиям. В этой главе будет рассказано о переменных, типах данных и операторах.
3.1 Переменные и типы данных
Может показаться, что в программировании нет ничего проще, чем переменная. Какие могут быть сложности? Тем не менее, для начинающих программистов сложности есть. Неправильное понимание того, как устроен мир переменных и данных, может привести к появлению трудно локализуемых ошибок.
Переменная представляет собой указатель на физическую область памяти, в которой хранятся данные. При помощи указателя мы можем записывать значения в память и считывать их оттуда. Иными словами, переменная — это имя фрагмента памяти компьютера. Размер этого фрагмента зависит от того, какие данные мы собираемся хранить.
Разрабатывая или запуская программу, мы не знаем заранее, по каким физическим адресам будут находиться данные в конкретном компьютере. Более того, в компиляторах современных языков принимаются специальные меры для дополнительного сокрытия информации о физическом размещении данных. Это делается для того, чтобы злоумышленнику было труднее получить доступ к критически важным данным, анализируя содержимое оперативной памяти компьютера.
До первого обращения к переменной ее надо объявить. При объявлении переменной указывают ее тип и имя. Это важно, потому что компилятор должен заранее знать, какой объем памяти выделить для хранения переменной, и как истолковывать данные, прочитанные из памяти.
Типы данных в языке Java можно разделить на две основные категории: примитивные (простые) и ссылочные. Они различаются по способу размещения данных в памяти.
Данные примитивного типа хранятся непосредственно в той ячейке памяти, которая ассоциирована с именем переменной. Обращаясь к переменной по имени, мы тем самым, обращаемся к данным в памяти. Если вы сравниваете две переменных, то сравниваются данные, которые с ними связаны. Если вы присваиваете одной переменной примитивного типа значение другой переменной примитивного типа, то происходит копирование данных.
В случае использования ссылочного типа в ячейке памяти, которая ассоциирована с именем переменной, хранится адрес данных, т.е. ссылка на данные, а не сами данные.
Необходимость в ссылочном типе данных можно продемонстрировать с помощью простого примера. Допустим, вы объявили строковую переменную с начальным значением «Java». Под это значение выделяется место в памяти. В процессе работы программы этой переменной присваивается новое значение «Hello, World!». Очевидно, что это совершенно другой объем данных, который не поместится в ранее отведенном фрагменте памяти.
Иными словами, ссылочные типы предназначены для работы с динамически создаваемыми и уничтожаемыми сущностями, объем которых невозможно предсказать заранее.
В таком случае программа размещает новые данные в другом фрагменте памяти. Новый адрес этих данных записывается в ячейку, которая ассоциирована с переменной ссылочного типа. Если на старые данные больше не ссылается ни одна переменная, то они превращаются в мусор (garbage) и удаляются из памяти при помощи специального сборщика мусора (garbage collector). В языке Java сборка мусора выполняется автоматически.
При проверке ссылочных переменных на равенство сравниваются не сами данные, а их адреса, хранящиеся в ссылочных переменных. Если вы присваиваете одной ссылочной переменной значение другой ссылочной переменной, при этом копируется адрес данных, а не сами данные.
3.1.1 Примитивные типы данных
В языке Java заявлено восемь примитивных типов данных. Первые четыре используются для хранения целых чисел.
byte — однобайтное целое — предназначен для хранения целых чисел в диапазоне от -128 до 127 и занимает один байт в памяти.
short — короткое целое — занимает два байта в памяти и применяется для хранения чисел в диапазоне от -32768 до32767.
int — целое — занимает 4 байта в памяти и применяется для хранения чисел в диапазоне от -231 (-2147483648) до 231—1 (2147483647). Это стандартный тип данных для работы с целыми числами.
При работе с числовыми данными старайтесь использовать тип int. Это связано с особенностями автоматического приведения типов, а также с тем, что целочисленные литералы (например, 10 или 123) в коде программы обрабатываются компилятором, как тип int. Приведение типов мы обсудим далее в этой главе.
long — длинное целое — занимает 8 байтов в памяти и хранит числа в диапазоне от -263 до 263—1. На практике настолько большие числа встречаются редко. Чтобы определить длинное целое число, следует добавить суффикс «L» в конце, например 5201225834L.
В дополнение к целочисленным типам, имеется два типа данных для хранения чисел с плавающей точкой.
float — с плавающей точкой — занимает 4 байта в памяти и может хранить числа в диапазоне от -3,4×1038 до 3,4×1038 с дискретностью 3,4×10—38. Такая точность представления соответствует 7 знакам после запятой. Если вы попытаетесь сохранить в типе float число 1,234567891 (10 знаков), оно будет округлено до 1,234568 (7 знаков).
Что такое дискретность? Вы не можете задать значение типа float с произвольной точностью. Ведь количество байт памяти для хранения этого числа ограничено. Если мы начнем перечислять подряд, начиная с ноля, числа с плавающей точкой, то они будут следовать с некоторым шагом (дискретностью) в младших разрядах: 0; 3,4×10—38; 6,8×10—38 и т. д. Величину дискретности можно условно назвать погрешностью представления числа. Для достижения более высокой точности применяется тип double.
double — с плавающей точкой, двойной точности — занимает 8 байтов в памяти и может хранить числа в диапазоне от -1,7×10308 до 1,7×10308 с дискретностью 1,7×10—308. Если вы не скованы ограничениями объема памяти, используйте тип double вместо float, как более точный.
По умолчанию, как только вы использовали десятичную точку в программе на языке Java, этому значению присваивается тип double. Если вы хотите, чтобы это число было истолковано именно как float, добавьте суффикс «F» в конце числа.
Кроме шести перечисленных типов, Java располагает двумя специфическими типами данных.
char — символ — занимает 2 байта и применяется для хранения одиночного символа Unicode, например «A», "@», «$» и т. д.
boolean — логический — это особый тип данных, который может хранить только два фиксированных значения: true (истина) и false (ложь). Размер занятой памяти зависит от реализации Java–машины. Этот тип данных широко используется в условных операторах и операторах цикла, которые мы рассмотрим позже.
Все остальные типы данных, включая пользовательские типы, являются ссылочными.
3.1.2 Объявление и инициализация переменных
При объявлении переменной указывается тип переменной и ее имя. Переменная может быть объявлена в любом месте программы, главное — до первого использования.
boolean fileSaved;
Если объявляется несколько переменных одного типа, то их можно перечислить через запятую.
int userNum, userAge, userWeight;
Одновременно с объявлением переменной ей можно присвоить значение. Эта процедура называется инициализацией.
int start=10, end=100;
Допускается динамическая инициализация переменной, когда ей присваивается значение, полученное вычислением из значений других переменных. Исходные переменные должны быть объявлены и инициализированы ранее.
int start=5, end=10;
int sum=a+b;
В этом примере переменная sum инициализирована значением 15.
Обратите внимание, что в момент динамической инициализации не возникает связь между переменными. Например, если после инициализации изменится значение переменных start и end, это никак не повлияет на значение sum.
3.1.3 Доступность переменных
Доступность, или область видимости переменных — это важный аспект программирования. Если кратко, переменная доступна внутри блока, определенного парой фигурных скобок, внутри которого она объявлена. Например, если переменная объявлена внутри цикла, то она будет доступна только внутри этого цикла. Снаружи цикла может быть объявлена переменная с таким же именем, но фактически это будет совершенно другая переменная.
Допустим, в некой фирме работает Иванов, он выполняет свои задачи в пределах штата фирмы. В соседнем офисе тоже работает Иванов, но это другой человек, который делает другую работу. Директор первой фирмы не может отдавать распоряжения Иванову из второй фирмы. Для него второй Иванов недоступен.
Если переменная доступна только внутри некого метода (функции), то она называется локальной. Если переменная задана на уровне класса, она называется глобальной. Глобальные переменные обычно доступны любому из методов, входящих в класс. При использовании глобальных переменных необходимо соблюдать осторожность. Если внутри одного из методов случайно изменить значение глобальной переменной, другие методы будут получать неправильное значение. Это приведет к появлению трудно локализуемой логической ошибки, на которую не реагирует компилятор.
3.1.4 Ввод и считывание данных
Переменным можно присваивать значения, введенные извне. Давайте немного отвлечемся от абстрактных рассуждений и запустим две простых программы, которые запрашивают данные у пользователя и обрабатывают их. К этому моменту вы должны уметь создавать проекты в среде NetBeans IDE, поэтому я привожу только исходный код примеров.
Программа из листинга 3.1 поддерживает консольный ввод — пользователь читает запросы программы и вводит данные в окне системного монитора среды NetBeans. В программе из листинга 3.2 задействованы модальные окна с привычным графическим оформлением. Вы увидите, насколько просты эти программы. Не волнуйтесь, если что-то непонятно. Пока просто привыкайте к новым терминам. По мере чтения этой книги придет полное понимание.
Листинг 3.1 Чтение консольного ввода, вывод в консоль
import java.util.Scanner;
public class Listing3_1 {
public static void main (String [] args) {
// Создаем объект input класса Scanner
Scanner input = new Scanner(System.in);
// Переменная для хранения имени пользователя
String name;
// Переменная для хранения отчества пользователя
String surName;
// Переменная для хранения даты рождения пользователя
int yearBorn;
// Переменная для хранения текущего года
int yearNow;
// Выводим запрос данных
System.out.print («Ваше имя:»);
// Считываем имя (строка)
name = input.nextLine ();
System.out.print («Ваше отчество:»);
// Считываем отчество (строка)
surName = input.nextLine ();
System.out.print («Какой сейчас год?»);
// Считываем текущий год (целое число)
yearNow = input.nextInt ();
System.out.print («В каком году вы родились?»);
// Считываем год рождения (целое число)
yearBorn = input.nextInt ();
System.out.println («Здравствуйте, "+name+" "+surName+»!»);
System.out.println («Ваш возраст: "+ (yearNow-yearBorn) +».»);
}
}
В первой строке этой программы мы импортируем класс Scanner, который входит в состав системного пакета java. util. Затем мы создаем новый объект класса Scanner и назначаем ему идентификатор (имя) input. После этого приступаем к получению данных от пользователя. Выводим в консоль текстовый запрос и считываем ответ. Обратите внимание, что текстовые ответы мы считываем при помощи метода nextLine (), а целочисленные при помощи метода nextInt (). В противном случае возникнет ошибка несоответствия типа данных. Ведь мы объявили переменные yearNow и yearBorn как целые числа.
Отдельно разберем строку
System.out.println («Ваш возраст: "+ (yearNow-yearBorn) +».»);
В этой строке происходит арифметическое вычисление возраста пользователя, формирование строки вывода и вывод в консоль. Выражение (yearNow-yearBorn) обязательно должно быть в круглых скобках, потому что сначала должно быть вычислено его значение, а затем результат вычисления будет преобразован из числа в строку (автоматическое приведение типов).
Наберите или скачайте исходный код программы и запустите проект на выполнение. Введите ответы на вопросы. В окно терминала должно быть выведено что-то наподобие этого:
run:
Ваше имя: Иван
Ваше отчество: Петрович
Какой сейчас год? 2018
В каком году вы родились? 1988
Здравствуйте, Иван Петрович!
Ваш возраст: 30.
СБОРКА УСПЕШНО ЗАВЕРШЕНА (общее время: 22 секунды)
На компьютере с ОС Windows вместо символов кириллицы вы можете увидеть квадратики. В этом случае необходимо настроить кодировку проекта. В окне просмотра содержимого проекта щелкните правой кнопкой мыши на названии проекта и выберите пункт Свойства контекстного меню. В открывшемся окне найдите поле «Кодировка» и выберите в списке кодировку windows–1251. Нажмите OK.
Вторая программа имеет графический интерфейс, основанный на модальных окнах. Это специальные окна, которые содержат сообщение или поле ввода. Чтобы программа продолжила выполнение, пользователь обязательно должен отреагировать на появление окна — ввести данные или прочитать сообщение и закрыть.
Листинг 3.2 Ввод и вывод данных в модальных окнах
// импортируем класс JOptionPane из библиотеки Swing
import javax.swing.JOptionPane;
public class Listing3_2 {
public static void main (String [] args) {
// Объявление числовых переменных
int yearNow, yearBorn, userAge;
// Объявление строковой переменной
String userData;
// Выводим окно запроса текущей даты
userData = JOptionPane.showInputDialog («Какой сейчас год?»);
// Преобразуем строку в число в явном виде
yearNow = Integer.parseInt (userData);
// Выводим окно запроса года рождения
userData = JOptionPane.showInputDialog («В каком году вы родились?»);
// Преобразуем строку в число в явном виде
yearBorn = Integer.parseInt (userData);
// Вычисляем возраст
userAge = yearNow — yearBorn;
// Выводим окно сообщения с результатом
JOptionPane.showMessageDialog (null, «Ваш возраст: " + userAge);
}
}
В первой строке программы мы импортируем класс JOptionPane из библиотеки Swing. Библиотека Swing содержит набор классов для разработки приложений с графическим интерфейсом. Это очень емкая и мощная библиотека, входящая в пакет поставки SDK. Вы будете постоянно использовать ее при разработке приложений с графическим интерфейсом. Класс JOptionPane предназначен для создания стандартных модальных (диалоговых) окон. Для вывода окна с запросом данных применяется метод showInputDialog (), а для вывода сообщения — метод showMessageDialog ().
Любые значения, возвращаемые методом showInputDialog () являются строковыми данными. Чтобы выполнить над ними арифметические действия, необходимо в явном виде преобразовать строки в числа. Мы делаем это при помощи метода parseInt () системного класса Integer:
yearNow = Integer.parseInt (userData);
Программа завершается вычислением возраста пользователя и выводом результата.
Запустите проект на выполнение. Вы должны поочередно увидеть три диалоговых окна (рис. 3.1).
Если все работает правильно, нажмите клавишу F11 или выберите пункт меню Выполнить | Собрать проект. Будет создан исполняемый файл приложения. Его можно запустить на любом компьютере, где установлена Java-машина. Оформление окон приложения — цветовая схема, форма кнопок — может различаться в зависимости от операционной системы и реализации Java-машины.
По умолчанию файл проекта находится в папке Документы | NetBeansProjects. Внутри папки с именем проекта найдите папку dist. В этой папке находится готовый распространяемый файл приложения с расширением jar.
3.2 Приведение типов
Иногда возникает ситуация, когда в одном выражении присутствуют разные типы данных. Будем считать, что это осознанное действие, а не ошибка программиста, ибо такие ошибки чрезвычайно коварны. Несоответствие типов в выражении не всегда влечет за собой ошибку компиляции, но программа может вести себя не так, как ожидалось. Изменение типа данных в процессе выполнения программы называется приведением типа. Реализация приведения типов зависит от конкретного языка. Некоторые языки допускают большие вольности в приведении типов. За это их резко критикуют профессиональные программисты.
Про особенности приведения типов в разных языках программирования можно написать отдельную брошюру. Но в нашем вводном курсе мы ограничимся изложением основных принципов.
Приведение типов разделяется на явное (указанное программистом в коде) и неявное (автоматическое).
При явном приведении типов перед значением или выражением в скобках указывается новый тип, например:
double x = 15.7;
y = (int) 15.7;
В этом примере число с плавающей точкой приводится к типу «целое», при этом просто отбрасывается дробная часть. Результатом приведения будет усеченное значение 15, а не округленное 16.
Обратное преобразование из целого в число с плавающей точкой тоже выполняется, но это происходит автоматически. Запомните простое правило: если в выражении участвуют операнды разных типов, то результат приводится к тому типу, который занимает больше места в памяти. Поэтому важно, чтобы тип переменной, которой вы хотите присвоить результат вычислений, совпадал с типом результата. Вот простой пример приведения типов:
byte a = 2;
а = (byte) (a*5);
В этом примере целочисленный литерал 5 трактуется, как значение типа int, поэтому результат умножения будет расширен до типа int. Но переменная объявлена, как byte, поэтому возникнет конфликт выделения памяти и ошибка компиляции.
Чтобы избежать ошибки, мы в явном виде приводим результат умножения к типу byte. При этом из 32 байт остаются только младшие 8, а остальные отбрасываются. Это опасная потеря информации. Может получиться так, что при маленьких исходных значениях результат будет верным. Но стоит разрядности результата умножения превысить 8 битов, и после приведения типов вы получите неправильный результат вычислений. Такая блуждающая ошибка зависит от сочетания факторов и трудно поддается локализации в коде.
Автоматическое приведение типов часто применяется при суммировании строки и числа. В этом случае число автоматически преобразуется в строку и выполняется обычная конкатенация (слияние) строк. Например:
int yearNow = 2018;
System.out.println («Текущий год " + yearNow);
В окно терминала будет выведена строка «Текущий год: 2018».
Обратное преобразование из строки в число автоматически не выполняется. Необходимо воспользоваться специальными методами, такими как Integer.parseInt (), Double.parseInt () и т. п. в зависимости от нужного типа. В листинге 3.2 вы уже встречали преобразование из строки в число.
3.3 Основные операторы
Основные операторы языка Java можно разделить на четыре группы: арифметические, логические, битовые и операторы сравнения.
По количеству обязательных операндов в выражении операторы разделяются на унарные (один операнд), бинарные (два операнда) и тернарные (три операнда).
3.3.1 Арифметические операторы
К арифметическим операторам относятся сложение (+), вычитание (-), умножение (*), деление (/), вычисление остатка (%), инкремент (++) и декремент (- -).
Допустим, мы задали значения x=18 и y=4. Тогда результаты использования операторов будут выглядеть так:
Сложение: x + y = 22
Вычитание: x — y = 14
Умножение: x*y = 72
Пока ничего необычного, но дальше будет немного сложнее.
Деление: 18 / 4 = 4
Неожиданно, не так ли? В языке Java результат деления одного целого числа на другое целое число будет целочисленным, остаток отбрасывается без округления. Получить результат деления с дробной частью можно двумя способами: объявить один или оба операнда как число с плавающей точкой или использовать явное приведение.
18 / 4.0 = 4.50
(double) 18/4 = 4.50
Вычисление остатка: 18%4 = 2. При делении 18/4 нацело мы получаем частное 4 (4*4=16) и остаток 2 (18—16=2). Иными словами, остаток — это побочный продукт целочисленного деления.
Инкремент: оператор постфиксного инкремента x++ сперва возвращает исходное значение переменной, затем увеличивает его на единицу. Оператор префиксного инкремента ++x сперва увеличивает значение переменной на 1, затем возвращает новое значение.
Строка с постфиксным инкрементом
System.out.print (x++);
равнозначна последовательности команд
System.out.print (x);
x = x +1;
Строка с префиксным инкрементом
System.out.print (++x);
равнозначна последовательности команд
x = x +1;
System.out.print (x);
Декремент: оператор постфиксного декремента сперва возвращает исходное значение переменной, затем уменьшает его на единицу. Оператор префиксного декремента сперва уменьшает значение переменной на 1, затем возвращает новое значение.
Строка с постфиксным декрементом
System.out.print (x — -);
равнозначна последовательности команд
System.out.print (x);
x = x — 1;
Строка с префиксным декрементом
System.out.print (- — x);
равнозначна последовательности команд
x = x — 1;
System.out.print (x);
3.3.2 Логические операторы
Логические операторы предназначены для использования с логическими операндами и создания условий для логических операторов.
Логическое И (&) — результатом выражения A&B является true, если оба операнда имеют значение true. Если хотя бы один из операндов имеет значение false, то результатом является false.
Укороченное логическое И (&&) — выражение A&&B вычисляется точно так же, как A&B, но если при проверке операнда A оказывается, что оно равно false, то значение B уже не проверяется, а сразу возвращается значение false.
Логическое ИЛИ (|) — результатом выражения A|B является true, если значение хотя бы одного из операндов является true. В ином случае возвращается значение false.
Укороченное логическое ИЛИ (||) — результат выражения A||B совпадает с результатом A|B, но если при проверке операнда A оказывается, что он имеет значение true, то второй операнд не проверяется, и сразу возвращается значение true.
Логическое исключающее ИЛИ (^) — результатом выражения A^B является true, если один операнд имеет значение true, а другой имеет значение false. Если оба операнда одновременно имеют значение true, или оба операнда одновременно имеют значение false, то возвращается значение false.
Унарное логическое отрицание (!) — результатом выражения! A является false, если операнд имеет значение true, и наоборот.
При помощи логических операторов можно формировать сложные выражения с участием нескольких операндов, например:
A&B&C — это выражение возвращает значение true, только если все три операнда одновременно имеют значение true.
A|B|C — это выражение возвращает true, если хотя бы один из операндов имеет значение true.
A&B|C — это выражение возвращает true, если A и B одновременно имеют значение true, или C имеет значение true. Оператор & имеет более высокий приоритет, поэтому сначала вычисляется значение выражения A&B, и результат вступает в логическую операцию ИЛИ c операндом C.
3.3.3 Битовые операторы
Битовые (или побитовые) операторы предназначены для операций с целыми числами на уровне их побитового представления.
Битовое И (&) — выражение A&B выполняется побитово, т.е. отдельно для каждого разряда. Если оба бита единичные, то в соответствующем разряде результата будет единица. Если хотя бы один из битов нулевой, в разряд результата записывается ноль.
Пример: 1101 & 0110 = 0100
Битовое ИЛИ (|) — выражение A|B выполняется побитово. Если хотя бы один из битов единичный, то в соответствующий разряд результата будет записана единица. Если оба бита нулевые, то в разряд результата будет записан ноль.
Пример: 1101 | 0110 = 1111
Битовое исключающее ИЛИ (^) — выражение A^B выполняется побитово. Если один из сравниваемых битов нулевой, а другой единичный, то в разряд результата записывается единица. Если оба бита нулевые, или оба бита единичные, то в разряд результата записывается ноль.
Пример: 1101 ^ 0110 = 1011
Битовый сдвиг вправо (>>) — результатом выполнения оператора A>> n является число, которое получилось сдвигом двоичного числа A вправо на n позиций. При сдвиге сохраняется знак числа, то есть младшие разряды теряются, а старшие заполняются содержимым знакового бита (0 для положительных чисел и 1 для отрицательных).
Примеры: (11010010)>> 2=11110100, (01010010)>> 2=00010100
Беззнаковый битовый сдвиг вправо (>>>) — результатом выполнения оператора A>>> n является число, которое получилось сдвигом двоичного числа A вправо на n позиций. При сдвиге НЕ сохраняется знак числа, то есть младшие разряды теряются, а старшие заполняются нулями.
Битовый сдвиг влево (<<) — результатом выполнения оператора A <<n является число, которое получилось сдвигом двоичного числа A влево на n позиций. При этом старшие разряды теряются, а младшие дополняются нулями.
3.3.4 Операторы сравнения
Если условие, заданное оператором сравнения, выполняется, то выражение возвращает значение true. В противном случае возвращается значение false. Все операторы сравнения бинарные — содержат только два операнда.
Равно (==) — выражение A==B возвращает true, если значение операнда A равно значению операнда B. Обратите внимание, оператор сравнения состоит из двух знаков равенства. Если вы используете одиночный знак равенства, то получится не сравнение, а присвоение значения. Среда NetBeans предупредит вас о возможной ошибке, хотя с формальной точки зрения это логическая, а не синтаксическая ошибка.
Не равно (!=) — выражение A!=B возвращает true, если значение операнда A отлично от значения операнда B.
Больше (>) — выражение A> B возвращает true, если значение операнда A больше значения операнда B.
Больше или равно (> =) — выражение A> =B возвращает true, если значение операнда A больше или равно значению операнда B.
Меньше (<) — выражение A <B возвращает true, если значение операнда A меньше значения операнда B.
Меньше или равно (<=) — выражение A <=B возвращает true, если значение операнда A меньше или равно значению операнда B.
3.3.5 Тернарный оператор
В языке Java имеется единственный оператор, у которого три операнда. Этот оператор обозначается символом вопроса (?) и имеет следующий синтаксис:
условие? значение: значение
Условием является выражение с логическим значением. Сначала вычисляется значение выражения, указанного в условии. Если оно истинное, то оператор возвращает значение, расположенное после вопросительного знака. Если значение условия ложное, то оператор возвращает значение, следующее после двоеточия. Например:
int a=10,b;
b = (a> 5)? 50: 60;
В данном случае переменной b будет присвоено значение 50.
int a=3,b;
b = (a> 5)? 50: 60;
Во втором случае переменной b будет присвоено значение 60.
Тернарный оператор представляет собой сокращенную форму условного оператора, о котором будет рассказано в главе 4.
Глава 4. Управляющие инструкции
Управляющие инструкции устанавливают порядок выполнения программы в зависимости от некого условия. Применяя управляющие инструкции, можно создавать точки ветвления, останова, многократно выполнять блоки операторов или всю программу.
4.1 Условный оператор if
Условный оператор if в языке Java имеет следующий вид:
if (условие) {
// Блок команд, если условие истинное
}
else {
// Блок команд, если условие ложное
}
// Продолжение программы
При выполнении оператора проверяется истинность условия в круглых скобках. Если условное выражение возвращает значение true, то выполняется первый блок команд в фигурных скобках, следующий за ключевым словом if, а блок после ключевого слова else игнорируется.
Если условное выражение возвращает значение false, то первый блок команд игнорируется, и выполняется блок команд после ключевого слова else.
Пример условного оператора:
if (a+b> 100) {
a = 0;
b = 0;
}
else {
a = a +5;
b = b +2;
}
Допускается упрощенная запись оператора, в которой отсутствует блок else. В таком случае, если условие ложное, то никакие специальные действия не выполняются. Пример упрощенной записи:
if (a + b> 100) {
a = 0;
b = 0;
} // Конец условного оператора
// Следующие команды программы
4.1.1 Вложенные условные операторы
Условный оператор может располагаться внутри другого условного оператора. Такая конструкция называется вложенными условными операторами. Количество вложенных операторов формально не ограничено, но в шаблоне для наглядности покажем только два вложенных условных оператора:
if (условие 1) {
// Блок команд 1
}
else if (условие 2) {
// Блок команд 2
}
else if (условие 3) {
// Блок команд 3
}
else {
// Блок команд 4
}
Если условие 1 истинное, то выполняется блок команд 1, остальные блоки игнорируются и продолжается выполнение команд после конструкции. Если условие 1 ложное, то проверяется условие 2. Если условие 2 истинное, то выполняется блок команд 2, и так далее. Последний блок команд выполняется только в том случае, если все предшествующие условия оказались ложными.
4.2 Оператор выбора switch
Логика работы оператора switch напоминает конструкцию из вложенных операторов if, которую мы только что рассмотрели. Принципиальное различие состоит в том, что проверяемое выражение может возвращать только целочисленное или символьное значение, а не логические значения true или false. В общем виде шаблон оператора switch выглядит следующим образом:
switch (выражение) {
case значение_1:
// Блок команд 1
break;
case значение_2:
// Блок команд 2
break;
case значение_3:
// Блок команд 3
break;
// другие case–блоки
case значение_n:
// Блок команд n
break;
default:
// Блок команд по умолчанию
}
При выполнении оператора switch вычисляется значение выражения в круглых скобках. Затем это значение поочередно, сверху вниз, сравнивается со значениями, указанными в начале каждого case–блока. Как только обнаружено совпадение, выполняется набор команд соответствующего блока.
Коварство оператора switch заключается в том, что при обнаружении совпадения выполняются все команды до конца оператора, включая команды в case–блоках, расположенных ниже. Если необходимо, чтобы выполнялись команды только одного блока, его необходимо завершать инструкцией break.
Оператор завершается необязательным блоком default. Команды этого блока выполняются в том случае, если не обнаружено ни одного совпадения с контрольными значениями. Поскольку блок default завершает конструкцию, в нем не используется инструкция break.
Вернитесь к среде разработки NetBeans и введите или загрузите пример программы, использующей оператор выбора (листинг 4.1).
Листинг 4.1 Пример использования оператора выбора
// импортируем класс JOptionPane из библиотеки Swing
import javax.swing.JOptionPane;
public class Listing4_1 {
public static void main (String [] args) {
int userData;
String userInput;
// Выводим окно запроса текущей даты
userInput = JOptionPane.showInputDialog («Введите число от 1 до 3»);
// Преобразуем строку в число в явном виде
userData = Integer.parseInt (userInput);
switch (userData) {
case 1:
JOptionPane.showMessageDialog (null, «Вы ввели число 1»);
break;
case 2:
JOptionPane.showMessageDialog (null, «Вы ввели число 2»);
break;
case 3:
JOptionPane.showMessageDialog (null, «Вы ввели число 3»);
break;
default:
JOptionPane.showMessageDialog (null, «Вы ввели недопустимое число!»);
}
}
}
В этой программе мы используем уже знакомые вам диалоговые окна, чтобы попросить пользователя ввести число от 1 до 3 и вывести ответное сообщение. Если пользователь вводит число в указанном диапазоне, то выводится подтверждение ввода. Если введенное число не соответствует ни одному из контрольных значений, то срабатывает блок default и выводится сообщение об ошибке.
Данная программа наглядно демонстрирует работу оператора switch, но не является оптимальной с точки зрения кода программы.
Давайте воспользуемся знаниями о логических операторах и условном операторе if и перепишем программу. Попробуйте переделать программу самостоятельно, не заглядывая в готовый пример из листинга 4.2.
Листинг 4.2 Пример программы с использованием логического и условного оператора
// импортируем класс JOptionPane из библиотеки Swing
import javax.swing.JOptionPane;
public class Listing4_2 {
public static void main (String [] args) {
int userData;
String userInput;
// Выводим окно запроса текущей даты
userInput = JOptionPane.showInputDialog («Введите число от 1 до 3»);
// Преобразуем строку в число в явном виде
userData = Integer.parseInt (userInput);
if ((userData> =1) & (userData <=3)) {
JOptionPane.showMessageDialog (null, «Вы ввели число " + userData);
}
else {
JOptionPane.showMessageDialog (null, «Вы ввели недопустимое число!»);
}
}
}
Отредактированная часть программы выделена жирным шрифтом. Как видите, получилась более компактная и универсальная конструкция. В условном операторе if использовано составное условие
(userData> =1) & (userData <=3)
Оно означает, что условие будет истинным, если значение переменной userData больше или равно единице И меньше или равно трем. В этом случае выводится диалоговое окно с сообщением об ошибке.
4.3 Операторы цикла
Операторы цикла предназначены для многократного выполнения блоков команд. В языке Java применяются операторы while, do… while и for.
4.3.1 Оператор цикла while
Шаблон оператора цикла while имеет вид:
while (условие) {
// Блок команд
}
При выполнении оператора цикла сначала проверяется условие. Если условие истинно, то выполняется блок команд в теле цикла. Затем условие проверяется снова. Если оно осталось истинным, вновь выполняется блок команд. Если условие стало ложным, то работа оператора цикла прекращается, и управление передается командам, следующим за циклом. Пример цикла while:
int a = 0;
while (a <10) {
System.out.println (a);
a++;
}
System.out.println («Выполнение цикла завершено»);
В этом примере цикл выполняется до тех пор, пока значение переменной a остается меньше 10. Вы уже знакомы с оператором автоинкремента (++), при помощи которого изменяется значение переменной. Если не менять значение переменной в теле цикла, то цикл будет выполняться вечно, потому что условие всегда будет истинным. Иногда такие «вечные циклы» бывают необходимы. Но в большинстве случаев это логическая ошибка, которая приводит к «зацикливанию» программы.
При определенных обстоятельствах может случиться так, что блок команд внутри цикла while не будет выполнен никогда, если условие цикла изначально будет ложным. Например, если перед выполнением цикла переменной a будет присвоено значение 10, то цикл из примера не сработает ни разу.
4.3.2 Оператор цикла do… while
Оператор do… while похож на оператор while, но имеет другую конструкцию, а блок команд будет выполнен как минимум один раз, потому что истинность условия проверяется после выполнения блока:
do {
// Блок команд
} while (условие);
Перепишем предыдущий пример, используя оператор do… while:
int a = 0;
do {
System.out.println (a);
a++;
} while (a <10);
Программа из этого примера выводит в окно терминала числа от 0 до 9. Но если переменную a инициализировать значением 10 или больше, то цикл сработает один раз и выведет начальное значение переменной.
4.3.3 Оператор цикла for
У оператора цикла for наиболее сложная конструкция, которая содержит все компоненты — инициализацию, условие, изменение:
for (инициализация; условие; инкремент/декремент) {
// Блок команд
}
Инициализация переменной цикла выполняется только один раз при обращении к оператору цикла. Затем проверяется истинность условия. Если оно возвращает значение true, то выполняется блок команд. Далее производится вычисление нового значения переменной цикла и вновь проверяется истинность условия. Если оно осталось истинным, то вновь выполняется блок команд. Цикл повторяется до тех пор, пока условие не перестанет быть истинным.
Пример цикла for:
for (int i=0; i <=10; i++) {
System.out.println (i);
}
Если тело цикла состоит из одной команды, то можно обойтись без фигурных скобок:
for (int i=0; i <=10; i++) System.out.println (i);
4.3.4 Вложенные циклы
Оператор цикла может быть вложен в тело другого цикла. В этом случае при каждом проходе внешнего цикла будет срабатывать и полностью выполняться вложенный цикл. Вложенные циклы обычно требуются для последовательного перебора элементов двумерных или многомерных структур (матриц, массивов, таблиц) и выполнения действий с этими элементами.
В листинге 4.3 во внешнем цикле последовательно перебираются дни недели weekDay, с первого по седьмой. При каждом проходе цикла выводится на печать номер дня недели, затем запускается вложенный цикл. Когда вложенный цикл отработал, выполняется перенос строки при помощи управляющей последовательности \n и запускается следующая итерация внешнего цикла.
Во вложенном цикле последовательно перебираются часы внутри текущего дня dayHour, с 1 по 24. Значения счетчика часов последовательно выводятся в одной строке через запятую с пробелом.
Листинг 4.3 Пример использования вложенного цикла
public class Listing4_3 {
public static void main (String [] args) {
for (int weekDay=1; weekDay <=7; weekDay++) {
System.out.print («День недели: "+weekDay+" Часы:»);
for (int dayHour=1; dayHour <=24; dayHour++) {
System.out.print (dayHour+«»);
}
System.out.print (»\n»);
}
}
}
В качестве самостоятельной работы сделайте так, чтобы во внешнем цикле вместо номера дня недели выводилось его название. Ваших знаний уже достаточно, чтобы решить эту задачу, используя один из ранее изученных операторов. Но решение пока не будет оптимальным с точки зрения программирования. В главе 5 вы познакомитесь с массивами, которые предназначены для работы с упорядоченными наборами значений.
4.4 Операторы досрочного выхода
Иногда возникает необходимость досрочно прервать выполнение цикла при возникновении определенной ситуации. Для этого используется уже знакомый вам оператор break, а также операторы continue и return.
4.4.1 Оператор досрочного выхода break
Оператор break полностью прерывает выполнение текущего цикла. Управление передается командам, следующим за циклом.
Давайте отвлечемся от сухих описаний и вместе напишем программу, в которой применяется оператор break. Эта программа генерирует случайное число от 1 до 10 и предлагает пользователю угадать его.
Прежде всего, сгенерируйте случайное число. Для этого вам придется забежать немного вперед и воспользоваться приемами объектно–ориентированного программирования. Импортируйте класс генератора случайных чисел Random:
import java. util. Random;
Здесь надо сделать небольшое отступление. Генератор случайных чисел — это обычная компьютерная программа, жесткий алгоритм, в котором нет места случайностям. Поэтому на самом деле генерируются псевдослучайные числа. Равномерность распределения вероятности по диапазону генерации зависит от качества генератора. Чтобы при каждом запуске программы генератор не выдавал одну и ту же последовательность чисел, его надо инициализировать неким начальным значением, которое является случайным по отношению к программе и не повторяется при запуске. На практике для инициализации генератора часто используют системное время компьютера в миллисекундах. Время запуска программы заранее не определено и никак не связано с системными часами. Поэтому вероятность повторения времени запуска программы с точностью до миллисекунды исчезающе мала.
Итак, создайте новый объект класса Random и инициализируйте его при помощи значения системного времени компьютера в миллисекундах. Пусть это будет новый объект с именем rnd:
Random rnd = new Random(System.currentTimeMillis ());
Чтобы сгенерировать целое число, воспользуйтесь методом nextInt (limit). Этот метод генерирует псевдослучайное целое число в диапазоне от нуля до предела limit, но не включая его. Например, метод nextInt (10) возвратит целое число в диапазоне от 0 до 9 включительно.
Сгенерируйте псевдослучайное число secret в диапазоне от 1 до 10 при помощи метода nextInt () объекта rnd:
int secret = 1 + rnd.nextInt (10);
Окончательный фрагмент кода для генерации псевдослучайного целого числа выглядит так:
Random rnd = new Random(System.currentTimeMillis ());
int secret = 1 + rnd.nextInt (10);
Теперь у вас есть «секретное» случайное число, на которое ссылается переменная secret. Осталось реализовать сравнение секретного значения со значением, которое ввел пользователь. Запрос на ввод значения должен повторяться до тех пор, пока пользователь не угадает.
Разработайте программу самостоятельно, а затем сравните результат с листингом 4.4. Ваш код не обязательно должен совпасть с примером — главное, чтобы он правильно работал.
Листинг 4.4 Пример прерывания цикла
import javax.swing.JOptionPane;
import java. util. Random;
public class Listing4_4 {
public static void main (String [] args) {
Random rnd = new Random(System.currentTimeMillis ());
int secret = 1 + rnd.nextInt (10);
int userData;
String userInput;
while (true) {
// Выводим окно запроса
userInput = JOptionPane.showInputDialog («Угадайте число от 1 до 10»);
// Преобразуем строку в число в явном виде
userData = Integer.parseInt (userInput);
if (userData == secret) {
JOptionPane.showMessageDialog (null, «Вы угадали число!»);
break;
}
}
}
}
Поскольку заранее не известно сколько раз придется задать вопрос, мы сознательно запускаем «вечный» цикл while со служебным значением true вместо условия. В каждом проходе цикла мы сравниваем введенное пользователем число со значением, загаданным в программе. В случае совпадения выводим сообщение и принудительно прерываем цикл. Количество попыток не может быть больше десяти, поэтому другие способы выхода из программы не предусмотрены.
Самостоятельно доработайте программу:
— Добавьте в тело цикла счетчик попыток. Пусть значение счетчика выводится в окне, сообщающем о совпадении: «Вы угадали число! Количество попыток:». Используйте конкатенацию строк, а также служебную последовательность "\n» для переноса строки текста.
— Добавьте прекращение угадывания и выход из программы при вводе числа 99.
4.4.2 Оператор досрочного выхода continue
Оператор continue прерывает выполнение тела цикла и вызывает досрочный переход к следующей итерации цикла, например:
for (int i=1; i <=10; i++) {
if (i== (i/2) *2) {
continue;
}
System.out.println («i=" + i);
}
Условие i== (i/2) *2 выполняется только в том случае, если значение i — четное, потому что тип переменной i объявлен как int. При делении нечетного числа на 2 дробная часть будет отброшена, и после умножения на 2 исходное значение не вернется. При истинности выражения сработает оператор continue и вызовет следующую итерацию цикла, минуя вывод на печать. Поэтому в окно консоли будут выведены только нечетные числа.
4.4.3 Оператор возврата return
Оператор return обычно применяется для выхода из подпрограмм, и его не принято использовать в циклах. Но, поскольку он тоже может досрочно прерывать выполнение блока команд, мы рассматриваем его в этом разделе.
Оператор return может возвращать из подпрограммы параметр, который указан после ключевого слова, например:
if (a <5) return a*20;
else return a*10;
Если параметр не указан, происходит выход из подпрограммы без передачи какого-либо значения в вызывающую программу.
Глава 5. Массивы и строки
Массив – это упорядоченный набор однотипных данных, объединенных общим именем. Допустим, мы захотели сохранить возраст нескольких пользователей. Мы можем создать несколько переменных с именами userAge1, userAge2, userAge3 и так далее. Но в этом случае возникает проблема с обращением к переменным, если нужно перебрать все значения в цикле. Кроме того, при разработке программы мы должны точно знать, сколько пользователей у нас будет, и заранее объявить переменную для каждого из них.
Можно поступить более рационально и объявить массив данных с именем userAge. Для обращения к элементу набора применяется порядковый номер (индекс) элемента: userAge [i]. В языке Java индексация элементов начинается с нуля.
Элементом массива может быть другой массив, который, в свою очередь, тоже может состоять из массивов. Количество индексов, которые необходимо указать для однозначной идентификации элемента, называется размерностью массива. Размерность массива может быть произвольной, но на практике чаще всего применяются одномерные и двумерные массивы. Трехмерные массивы применяются намного реже.
5.1 Одномерные массивы
При создании массива объявляется переменная, которая не является массивом, а содержит ссылку на массив. Для создания собственно массива (выделения ячеек памяти) применяется служебное слово new:
int [] userAge;
userAge = new int [10];
В первой строке объявлена переменная userAge, которая является целочисленным массивом. Обратите внимание на квадратные скобки после ключевого слова int. Во второй строке выделяется память для хранения десяти целочисленных элементов массива, связанных с именем userAge.
Допускается сокращенная запись в одной строке:
int [] userAge = new int [10];
Количество элементов массива называется размером массива. Размер одномерного массива часто называют длиной. Индекс последнего элемента массива на единицу меньше длины. Для хранения массива в памяти отводится ровно столько места, сколько было заявлено при его создании.
Для определения размера массива следует обратиться к его свойству length:
int a = userAge. length;
5.1.1 Инициализация одномерного массива
При создании массива его ячейки автоматически заполняются нулями, если речь идет об элементах базовых типов, либо значениями null, если массив состоит из ссылок на другие объекты. Перед использованием в программе массив необходимо инициализировать — присвоить ячейкам массива осмысленные значения.
Можно инициализировать массив непосредственно во время объявления:
int [] userAge = {28,32,19,44,52};
Допускается равноценная, но более сложная синтаксическая конструкция:
int [] userAge = new int [] {28,32,19,44,52};
Аналогичным образом можно создать и инициализировать массив строковых значений:
String [] userName = {«Иван», «Петр», «Ольга», «Егор»};
Элементы массива можно инициализировать по отдельности:
userAge [0] = 28;
userAge [1] = 32;
Если массив должен содержать некие серийные данные, сформированные по определенному закону, то для инициализации массива удобно использовать цикл, последовательно перебирающий элементы массива.
5.1.2 Специальная форма оператора for
Специальная форма оператора for позволяет перебирать непосредственно элементы массива, не используя индексы. Конструкция оператора for в этом случае имеет вид:
for (тип переменная: массив) {
// Блок команд
}
Например, цикл для перебора значений массива userAge имеет вид:
for (int age: userAge) {
System.out.println (age);
}
В этом примере рабочая переменная age поочередно принимает значения всех элементов массива userAge. В теле цикла текущее значение переменной age выводится на печать. Таким образом, мы выводим на печать содержимое массива userAge.
При помощи специальной формы оператора for мы можем только читать текущие значения элементов массива. Для инициализации или модификации элементов массива следует использовать обычный цикл, в котором происходит перебор индексов массива в явном виде.
В примере из листинга 5.1 в первом цикле элементам массива even [] присваиваются четные значения от 2 до 20. Далее применяется специальная форма цикла for для вывода значений всех элементов массива на печать.
Листинг 5.1 Перебор элементов массива
public class Listing5_1 {
public static void main (String [] args) {
int [] even = new int [10];
// Инициализация массива
for (int i=0;i <10;i++) {
even [i] = i*2+2;
}
// Вывод значений элементов массива
for (int data: even) {
System.out.println (data);
}
}
}
5.1.3 Присваивание массивов
Переменные массивов относятся к переменным ссылочного типа. Это значит, что в переменной массива хранится ссылка на область памяти, в которой хранится массив. Следовательно, этой переменной можно присвоить ссылку на другой массив. Массивы должны быть одного и того же типа и размерности, но вот размер не обязательно должен совпадать, потому что переменной массива присваиваются не новые данные, а новая ссылка на них.
Операция присвоения массивов проста, но может привести к неочевидным последствиям. Рассмотрим простой пример присвоения:
int [] first = {10,20,30,40};
int [] second = new int [6];
second = first;
first [2] = 50;
В первой строке мы создаем массив из четырех элементов. Во второй строке объявляем массив из шести элементов. В третьей строке переменной второго массива присваиваем ссылку на первый массив. После выполнения команды обе переменных ссылаются на один и тот же массив. Как вы думаете, какое значение будет у элемента second [2] после выполнения команды first [2] = 50? Правильно, тоже 50. Ведь это одна и та же ячейка памяти, на которую ссылаются разные переменные массива.
5.2 Двумерные массивы
Двумерный массив проще всего представить в виде таблицы, состоящей из строк и столбцов. Каждый элемент двумерного массива однозначно определяется двумя индексами — номером строки и номером столбца, на пересечении которых находится элемент. К сожалению, этот образ хоть и нагляден, но не совсем корректен. Дело в том, что строки в этой «таблице» не обязательно должны иметь одинаковую длину.
Более правильно двумерный массив можно представить как одномерный «внешний» массив, элементами которого являются ссылки на одномерные «вложенные» массивы. Первый индекс определяет ссылку на вложенный массив. Второй индекс определяет элемент вложенного массива. В таком случае более очевидно, что вложенные массивы могут иметь различный размер.
Строго говоря, многомерные массивы с вложенными массивами одинакового размера являются лишь частным случаем общего типа коллекции объектов. Язык Java позволяет работать с коллекциями, в которых вложенные объекты имеют разный размер. Но углубленное изучение понятия коллекций выходит за рамки книги для начинающих. Пока вам достаточно знать, что вложенные массивы могут иметь разную длину, и в работе с такими массивами нет принципиальных особенностей.
Двумерный массив определяется так же, как одномерный:
int [] [] coord = new int [10] [15];
Данную команду можно разделить на две:
int [] [] coord;
coord = new int [10] [15];
Нумерация по каждому индексу начинается с нуля. Размер массива зависит от того, о каком массиве идет речь — внешнем или вложенном. Размер по первому индексу означает количество вложенных массивов (количество строк):
int x = coord. length; // x = 10
Размер по второму индексу означает количество элементов вложенного массива (количество столбцов):
int y = coord [0].length; // y = 15
Поскольку при создании классического двумерного массива (а не коллекции) все вложенные массивы имеют одинаковый размер, то первый индекс не имеет особого значения. Важно лишь, чтобы он находился в переделах размерности по количеству вложенных массивов.
5.2.1 Инициализация двумерного массива
Двумерный массив можно инициализировать при создании, перечислив значения в явном виде в конструкции из фигурных скобок:
int [] [] nums = {{4,9,12,0}, {2,7,3,5}};
Для инициализации массива упорядоченными данными используются вложенные циклы. Сейчас вы уже готовы написать программу инициализации двумерного цикла самостоятельно. Один из возможных вариантов программы приведен в листинге 5.2.
Листинг 5.2 Инициализация двумерного массива
public class Listing5_2 {
public static void main (String [] args) {
// Объявление двумерного массива 10x15
int [] [] coord = new int [10] [15];
// Перебор элементов внешнего массива
for (int i=0;i <coord. length; i++) {
// Перебор элементов вложенного массива
for (int j=0;j <coord [0].length; j++) {
// Пример выражения для генерации значений
coord [i] [j] = (i+j) *j;
}
}
// Вывод сформированных значений на печать
for (int [] tmp1:coord) {
for (int tmp2:tmp1) {
System.out.print (tmp2+"\t»);
}
System.out.print (»\n»);
}
}
}
Разберем подробнее этот пример. После того, как объявлен двумерный массив с размерностью 10х15, мы организуем вложенный цикл для заполнения ячеек массива некими автоматически сгенерированными данными.
В качестве граничного параметра цикла используем запрос длины массива, например:
for (int i=0;i <coord. length; i++) {
Как вы помните, индексация начинается с нуля, и максимальный индекс на единицу меньше, чем размер массива. Именно поэтому в цикле используется условие «меньше», а не «меньше или равно». В данном случае размер внешнего массива равен 10, а индексы принимают значения от 0 до 9. Аналогично происходит перебор элементов массива по второму индексу при помощи вложенного цикла.
Для генерации значений использовано произвольное выражение:
coord [i] [j] = (i+j) *j;
Вместо него можно подставить любое другое выражение или источник данных. Важно лишь, чтобы тип данных, возвращаемых выражением, совпадал с типом данных массива.
Сформировав данные, мы выводим их на печать для проверки. Для перебора значений используем сокращенную форму оператора for. В случае с двумерным массивом есть некоторые тонкости. Обратите внимание на типы переменных цикла в объявлении внешнего и внутреннего цикла:
for (int [] tmp1:coord) {
for (int tmp2:tmp1) {
Для переменной tmp1 заявлен тип int [] с квадратными скобками, потому что элементы внешнего массива сами являются массивами (т.е. во внешнем цикле мы перебираем массивы). Для переменной tmp2 заявлен тип int без квадратных скобок, потому что элементы вложенного массива являются целыми числами.
Значения, выведенные на печать, разделяются символами табуляции при помощи служебной последовательности «\t»:
System.out.println (tmp2+"\t»);
5.3 Методы для операций с массивами
Для работы с массивами в языке Java предусмотрены стандартные методы, которые описаны в классе java.util.Arrays. Перед обращением к методам необходимо импортировать класс:
import java.util.Arrays;
Напомним, что команды импорта должны располагаться сразу после оператора именования пакета, но перед объявлением главного класса. Если используется пакет по умолчанию, то программа начинается непосредственно с импорта классов.
Далее мы подробно рассмотрим основные методы работы с массивами, которые используются в повседневной практике. С полным перечнем методов можно ознакомиться по адресу
https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html
equals () — метод применяется для сравнения массивов. Как вы уже знаете, переменная массива хранит ссылку на массив. Если два массива абсолютно одинаковые, но хранятся в разных местах, то у них будут разные ссылки и простое сравнение переменных вернет отрицательный результат false. Поэтому для сравнения массивов применяется специальный метод, который сравнивает количество ячеек, содержимое ячеек и порядок их расположения. Если хотя бы один из параметров не совпадает, результат сравнения будет отрицательным. Например, возьмем три массива:
int [] arr1 = {5,3,4,6,8,10};
int [] arr2 = {5,3,4,6,8,10};
int [] arr3 = {10,8,6,4,3,5};
boolean result1 = Arrays. equals (arr1, arr2);
boolean result2 = Arrays. equals (arr1, arr3);
Сравнение массивов arr1 и arr2 вернет результат true, потому что массивы совпадают по всем параметрам. Сравнение массивов arr1 и arr3 вернет результат false, поскольку у них не совпадает порядок расположения значений.
copyOfRange () — копирование фрагмента исходного массива в другой массив. Методу требуются три аргумента: источник, начальный индекс, конечный индекс. Допустим, у нас объявлен массив:
int [] source = {-2, -1,0,1,2,3,4,5,6};
После выполнения команды
int [] dest = Arrays.copyOfRange (source,2,5);
в новый массив будут скопированы значения {0,1,2}. Обратите внимание, что копируются элементы с индексом до второго значения, но не включая его. Поэтому элемент с индексом 5 не будет скопирован.
toString () — преобразование содержимого массива в строку. Это простой способ вывести содержимое массива на печать, например:
int [] arr = {3,8,10,1,6};
System.out.println(Arrays.toString (arr));
На печать будет выведена строка [3, 8, 10, 1, 6].
sort () — сортировка элементов массива по возрастанию. Метод sort () не возвращает новый массив. Он просто модифицирует имеющийся. Допустим, мы объявили массив и выполнили сортировку:
int [] arr = {10,3, -1,6,0};
Arrays.sort (arr);
System.out.println(Arrays.toString (arr));
На печать будет выведена строка [-1, 0, 3, 6, 10].
binarySearch () — поиск индекса заданного значения в отсортированном массиве. Если вы не уверены, что значения элементов массива расположены по возрастанию, то перед использованием метода binarySearch () необходимо отсортировать массив при помощи метода sort (). Допустим, у нас есть отсортированный массив:
int [] arr = {2,7,15,42,56,78};
int myIndex = Arrays.binarySearch (arr, 56);
Переменной myIndex будет присвоено значение 4. Это индекс элемента массива, имеющего значение 56.
Что случится, если в качестве аргумента указать значение, которого нет в массиве? Мы получим странный результат, который требует отдельных пояснений. Например, попробуем найти индекс для значения 18:
int myIndex = Arrays.binarySearch (arr, 18);
Переменной myIndex будет присвоено значение -4. Минус означает, что такое значение не найдено. Число 3 означает, какой у этого значения был бы индекс, если бы оно было в массиве. Но к этому индексу зачем-то прибавлена единица! Иными словами, если бы в массиве arr имелось значение 18, то у него был бы индекс 4—1=3.
Как мы уже упоминали в предыдущем разделе, длина массива (количество элементов) определяется через свойство length, поэтому метод для определения длины массива не применяется.
5.4 Строки
Строго говоря, строка в языке Java — это не тип данных, а экземпляр встроенного класса String. У класса String есть собственные методы для работы со строками. Класс String, в отличие от класса Arrays, не требует импорта.
Мы не случайно говорим о строках и массивах в одной главе. Строку текста можно рассматривать, как упорядоченный набор символьных значений, где каждый символ имеет собственный порядковый номер (индекс). Переменная, которая ассоциирована со строкой, хранит ссылку на область памяти. В этом строки похожи на массивы.
Строку можно создать разными способами. Наиболее очевидный – создать экземпляр класса String в явном виде:
String str = «Hello, World!»;
Можно создать пустой объект класса String:
String str = new String ();
Можно создать строку из массива символов:
char [] chars = {«J», ’a’, ’v’, ’a’};
String str = new String (chars);
Строки могут являться элементами массива:
String [] userNames = {«Василий», «Петр», «Николай»};
Строки можно объединять при помощи оператора +. Эта операция называется конкатенацией строк:
String str1 = «Java»;
String str2 = «Language»;
String str3 = str1 + str2;
Допускается сокращенная форма оператора присваивания +=:
String str3 += «Language»;
Строка является неизменяемым объектом. Если в результате манипуляций со строкой меняется ее текст, то на самом деле в памяти создается новая строка, и строковой переменной присваивается новая ссылка. Если старая строка больше нигде не используется, то автоматический сборщик мусора удаляет ее, освобождая память.
5.5 Методы для операций со строками
Язык Java предлагает много полезных методов для работы со строками. В этой книге мы перечислим только самые необходимые.
charAt () — возвращает символ с указанным смещением от начала строки. Отсчет начинается с нуля. Не используйте отрицательные и несуществующие значения индекса. Метод напоминает обращение к элементу массива по индексу:
String lang = «Java»;
char myChr = lang.charAt (2); // myChr = «v»
contains () — проверяет, содержится ли заданная последовательность символов в строке:
String str = «Codemagic»;
boolean tmp = str.contains («mag»); // возвратит true
endsWith () — проверяет, заканчивается ли строка заданной последовательностью символов:
String str = «Codemagic»;
boolean tmp = str. endsWith («magic»); // возвратит true
Метод startsWith () аналогичным образом проверяет, начинается ли строка с заданной последовательности символов.
equals () — сравнивает строки и возвращает логическое значение true, если совпадают количество символов, их порядок и регистр:
String str1 = «Java program»;
String str2 = «Java Program»;
boolean cmp1 = str1.equals (str2); // false — регистр не совпадает
boolean cmp2 = str1.equals («Java program»); // true — совпадение
equalsIgnoreCase () — сравнивает строки без учета регистра символов.
length () — возвращает количество символов в строке, включая пробелы.
split () — разделяет строку на части в соответствии с заданным разделителем и возвращает массив фрагментов строки:
String names = «Василий, Петр, Ольга, Игорь»;
String [] splitNames = names. split (»,»);
В данном примере метод split () возвратит строковый массив {«Василий», «Петр», «Ольга», «Игорь»}.
substring () — возвращает заданный фрагмент строки. В качестве аргумента указывают индекс начального символа и индекс символа, следующего за конечным:
String str1 = «Hello, Java»;
String str2 = str1.substring (0,4); // str2 = «Hell»
String str3 = str1.substring (7); // str3 = «Java»
Если в качестве аргумента метода указан только один индекс, то извлекается фрагмент начиная с указанного индекса и до конца строки.
toUpperCase () /toLowerCase () — преобразование регистра всех символов строки в верхний / нижний регистр:
String str1 = «Hello, Java»;
String str2 = str1.toUpperCase (); // str2 = «HELLO, JAVA»;
trim () — удаляет пробелы и служебные символы в начале и конце строки.
Глава 6. Классы и объекты
Если вы уже знакомы с основами объектно-ориентированного программирования (ООП), то можете пропустить эту главу или выборочно прочитать некоторые разделы, чтобы освежить знания в памяти. В любом случае, без понимания концепции ООП вы не сможете программировать на языке Java.
Забегая вперед, отметим, что объектно–ориентированный подход — не панацея от всех проблем и не инструмент на все случаи жизни. Не случайно в языке Java, начиная с версии Java 8, добавлены лямбда–выражения, при помощи которых намного удобнее реализуется отложенное выполнение кода и программирование обработки событий. Об этом будет рассказано в главе 11.
Ответу на вопрос «Зачем нужно ООП и как оно работает?» посвящено много статей и книг. Решив заняться программированием всерьез, вы не сможете обойтись без глубокого изучения массива информации. Но это будет позже. Сейчас мы разберем основные понятия ООП и обрисуем общую картину. Этого будет вполне достаточно на первое время, особенно для программирования на уровне хобби.
6.1 Основная идея ООП
Любая прикладная программа реализует последовательность действий для решения некой задачи. Иными словами, программа — это инструмент, который мы создаем своими руками. Поскольку большинство задач можно решить различными способами, то и внутреннее устройство инструмента может быть разным. С этой точки зрения ООП — один из подходов к конструкции инструмента.
С другой стороны, ООП — это специальный образ мышления, особая философия. Необходимость научиться мыслить новыми понятиями вызывает затруднения у начинающих программистов. В данном случае бесполезно заучивать определения — необходимо понять суть.
Парадигма ООП заключается в том, что решаемую задачу можно разделить на обособленные объекты, над которыми мы совершаем определенные действия. Здесь нас подстерегает первая проблема: уровень абстракции. Если неправильно определить уровень «дробления» задачи или некорректно распределить задачу по объектам, то все достоинства ООП мгновенно превратятся в недостатки. Поэтому объектное программирование начинается с понимания целей и структуры проекта. Хороший программист это, прежде всего, менеджер проекта (как минимум, своей части проекта) и лишь затем составитель кода программы. Сказанное относится даже к простейшим программам. Либо вы мыслите понятиями ООП всегда и полностью, независимо от масштаба проекта, либо вы плохой программист.
Обычно в этом месте начинающие программисты восклицают: «Ну почему так сложно?!» Если вы хотите уметь работать с эффективными и универсальными инструментами, без сложностей не обойтись. Но могу вас успокоить — в повседневной жизни мы постоянно используем проекты и абстракцию разных уровней. Это естественный образ мышления человека! Для облегчения понимания рассмотрим простой пример.
Допустим, перед вами стоит задача регулярно косить траву на лужайке перед домом. Что требуется для решения этой задачи? Прежде всего, нужна газонокосилка. Перед покупкой газонокосилки вы решаете, какой она будет — бензиновой или электрической, ручной или на колесиках. Это абстракция на уровне типа газонокосилки. Нет никакой необходимости спускаться в абстракции ниже, до уровня карбюратора или гаек в составе газонокосилки. В данном случае вы интуитивно верно выбираете уровень абстракции, руководствуясь элементарным здравым смыслом. Помните, что готовые решения и правила достаточно условны, а окончательный выбор уровня абстракции и инструментов остается за вами.
У завода-изготовителя газонокосилок есть подробные чертежи, описание технологии производства, свойств изделия и приемов работы с ним. В программировании такой описательный набор называется класс. Но самое подробное описание изделия — это еще не изделие. Заказчик обращается на завод с запросом на изготовление экземпляра газонокосилки. В программировании это называется экземпляр класса или объект класса. В целом, термины «объект» и «экземпляр» взаимозаменяемы, но есть тонкие смысловые нюансы. Термин «объект» чаще используется, когда делается смысловой акцент на функциональной сущности объекта реального мира, а термин «экземпляр» чаще применяется, когда идет речь о структурной единице программного кода.
В объектном программировании класс описывает свойства и методы, которые будут присутствовать у объекта, построенного на основе описания класса (экземпляра класса).
Разбирая пример с газонокосилкой, мы подразумевали, что разработчиком класса «газонокосилка» является кто-то другой. В программировании это обычная ситуация. Мы постоянно используем классы и библиотеки сторонних разработчиков. Даже простейшая программа из нескольких строк на языке Java на самом деле обращается к системным классам языкового пакета. Но программистам постоянно приходится разрабатывать собственные классы для решения прикладных задач. В этом нет ничего сложного, но начинающие программисты часто попадают в ловушку чрезмерно глубокой абстракции. Они разрабатывают классы и создают объекты слишком низкого уровня, что порождает путаницу, несовместимость, скрытые ошибки и прочие проблемы, из-за которых у объектного подхода к программированию есть свои противники.
Итак, мы установили, что класс — это описательный шаблон, на основе которого в процессе выполнения программы создается объект класса. В состав объекта класса входят поля и методы, описанные в классе.
Поля — это переменные разных типов, включая ссылки на объекты других классов.
Методы — это именованные блоки команд, выполняемые при вызове метода и предназначенные для обработки полей объекта и внешних переменных.
Поля и методы, описанные в классе, называют членами класса. Запомните это определение.
Поля также часто называют свойствами объекта. В случае с газонокосилкой примерами свойств могут служить название марки, мощность двигателя, количество оборотов, количество топлива в баке. Если марка и мощность это постоянные свойства, то количество оборотов и количество топлива — изменяемые свойства, которые характеризуют мгновенное состояние объекта, но могут меняться с течением времени.
Также у косилки есть методы «завести», «косить», «заглушить». Газонокосилка должна реагировать на нажатие регулятора оборотов, поэтому в метод «косить» мы должны передать аргумент, показывающий силу нажатия на регулятор: косить (силаНажатия). Метод получит аргумент и при помощи внутренних команд преобразует его в заданное число оборотов. Если мы используем готовый класс, то обычно не вникаем в реализацию методов. Нам достаточно знать описание функциональности метода и требования к аргументам.
При желании вы можете придумать множество других примеров, наглядно демонстрирующих главное достоинство ООП — максимальную схожесть с интуитивным механизмом мышления в реальной жизни. Мы, сами того не сознавая, разбиваем окружающий мир на объекты и постоянно используем методы и свойства.
Теперь закончим лирическое отступление и обсудим реализацию классов и объектов в языке Java.
6.2 Описание класса и создание объектов
Описание класса начинается с ключевого слова class, после которого следует имя класса и размещается в блоке из фигурных скобок:
class имя {
// Описание класса
}
Рассмотрим пример описания класса, который состоит только из полей и не содержит методы.
Листинг 6.1 Пример класса, содержащего только поля
// Описание пользовательского класса
class MyFields {
// Поля класса
int data;
char letter;
}
// Описание класса с главным методом программы
// Шаблон описания автоматически создается средой NetBeans
class Listing6_1 {
// Главный метод
public static void main {
// Создаем объект класса MyFields
MyFields demo = new MyFields ();
// Присваиваем значения полям
demo. data = 1234;
demo. letter = «B»;
// Выводим значения полей на печать
System.out.println («Число: "+demo. data);
System.out.println («Буква: "+demo. letter);
}
}
В этом примере описан пользовательский класс MyFields, который состоит только из двух полей — целочисленного и символьного. Пока это лишь описание, мы не можем обращаться к полям. На основе описания класса создан объект (экземпляр класса) с именем demo. Теперь мы можем обращаться к полям объекта, присваивать им значения и считывать их. Иными словами, класс — это описание, а объект класса осязаемая сущность, которой можно манипулировать. Мы можем создать в программе несколько объектов одного и того же класса и присвоить им разные имена. Для обращения к полю объекта сначала указывают имя объекта, и через точку имя поля.
Теперь опишем класс, который содержит только методы (листинг 6.2). При описании метода кроме блока исполняемых команд необходимо указать тип возвращаемого результата, имя метода и список аргументов. Если метод не возвращает результат, то идентификатором типа является ключевое слово void.
В методе могут использоваться локальные переменные. Они принципиально отличаются от полей объекта, потому что доступны только внутри тела метода и существуют, пока работает метод. По окончании работы метода локальные переменные удаляются из памяти.
Листинг 6.2 Пример класса, содержащего только методы
// Описание пользовательского класса
class MyClass {
// Описание метода, выполняющего сложение
int summ (int a, int b) {
int summa=a+b;
return summa;
}
// Описание метода, выполняющего умножение
int proiz (int a, int b) {
int proizvedenie=a*b;
return proizvedenie;
}
}
public class Listing6_2 {
public static void main (String [] args) {
// Создаем объект класса MyClass
MyClass test=new MyClass ();
// Вызов метода, выполняющего сложение
System.out.println («Сумма чисел 4+5="+test.summ (4,5));
// Вызов метода, выполняющего умножение
System.out.println («Произведение чисел 5*6="+test.proiz (5,6));
}
}
В примере из листинга 6.2 мы описали класс, который содержит два метода: сложение двух целых чисел и умножение двух целых чисел. В методах используются локальные переменные a и b, которые существуют только во время выполнения блока команд метода.
В главном методе программы мы создаем объект класса и присваиваем ссылку на него объектной переменной test. Чтобы вызвать метод и передать ему аргументы, мы используем конструкцию вида объект. метод (аргументы). Мы можем создать сколько угодно много объектов одного класса, поэтому при вызове метода необходимо сначала указать, какой именно объект мы имеем в виду, и затем через точку указать имя метода.
Для упрощения программы вызов метода располагается непосредственно в команде вывода строки на печать.
Теперь вы умеете описывать простые классы и создавать объекты на их основе. Для тренировки напишите свою программу. Опишите в ней класс, который содержит поля и методы. Пусть программа при помощи модальных диалоговых окон запрашивает у пользователя ввод двух целых чисел. Затем в диалоговое окно должны выводиться результаты сложения и перемножения этих чисел. Один из возможных вариантов такой программы приведен в листинге 6.3.
Листинг 6.3 Пример класса с полями и методами
import javax.swing.JOptionPane;
class MyClass {
// Поля класса
int fieldOne;
int fieldTwo;
// Метод для присваивания значений полям
void set (int a, int b) {
fieldOne = a;
fieldTwo = b;
}
// Метод для перемножения значений полей
int multiply () {
return fieldOne*fieldTwo;
}
// Метод для суммирования значений полей
int summ () {
return fieldOne+fieldTwo;
}
}
public class Listing6_3 {
public static void main (String [] args) {
// Объявляем переменные главного класса
int input1, input2;
String inputString;
// Создаем объект своего класса
MyClass obj=new MyClass ();
// Окно ввода первого значения
inputString=JOptionPane.showInputDialog («Введите первое значение»);
input1 = Integer.parseInt (inputString);
// Окно ввода второго значения
inputString=JOptionPane.showInputDialog («Введите второе значение»);
input2 = Integer.parseInt (inputString);
// Вызываем метод для присвоения значений полям объекта
obj.set (input1, input2);
// Выводим в диалоговое окно результат сложения
JOptionPane.showMessageDialog (null,«Результат сложения: "+obj.summ ());
// Выводим в диалоговое окно результат умножения
JOptionPane.showMessageDialog (null,«Результат умножения: "+obj.multiply ());
}
}
В примере из листинга 6.3 поля класса и метод для присвоения значений этим полям использованы в качестве иллюстрации. В данном примере мы могли бы передавать значения в методы сложения и умножения напрямую, через аргументы вызова. Но на практике в языке Java принято использовать специально написанные методы. Почему?
6.2.1 Геттеры и сеттеры
Метод, присваивающий значения полям объекта, называется сеттер (setter, от английского to set — установить, назначить). Согласно правилам именования Java этот метод должен иметь имя set <свойство> (). Метод, возвращающий значения полей объекта, называется геттер (getter, от английского to get — взять, получить). Этот метод должен иметь имя get <свойство> (). Впрочем, указывать имя свойства не обязательно, о чем говорят угловые скобки.
Геттеры и сеттеры — это стандартные термины программирования на языке Java. Использование геттеров и сеттеров является более безопасным, чем прямое обращение к переменным объекта, поскольку не позволяет менять значения случайно. При использовании сеттера значения полей будут изменены только при вызове метода set (), и только способом, который описан в этом методе.
6.2.2 Перегрузка методов
Язык Java позволяет описывать несколько методов с одинаковыми именами в одном и том же классе. Одноименные методы различаются типом и/или количеством аргументов. Такой подход называется перегрузкой методов и позволяет создавать эффективный и гибкий программный код.
Что происходит, когда мы вызываем метод с одним и тем же именем, но с разными аргументами? На самом деле в программе создаются разные методы, обозначенные одним именем. При вызове метода по имени программа определяет, какой из методов «подходит» для выполнения, исходя из количества и типа переданных аргументов. Для пользователя это выглядит так, будто вызываются разные версии одного метода.
В листинге 6.4 приведен пример использования перегрузки методов для присваивания значений полям объекта.
Листинг 6.4 Пример использования перегрузки методов
// Объявляем собственный класс
class MyClass {
// Объявляем поля класса
int digit;
char letter;
// Метод с одним числовым аргументом
void set (int n) {
digit=n;
}
// Метод с одним символьным аргументом
void set (char s) {
letter=s;
}
// Метод с двумя аргументами
void set (int n, char s) {
set (n); //Присвоить значение полю digit
set (s); //Присвоить значение полю letter
}
// Метод без аргументов
void set () {
// Присваиваем значение 5 полю digit
// и значение А полю letter
set (5, «A»);
}
// Метод для отображения значений полей
void show () {
System.out.println («Поле digit: "+digit);
System.out.println («Поле letter: "+letter);
}
}
public class Listing6_4 {
public static void main (String [] args) {
// Объявляем первый объект класса MyClass
MyClass objFirst=new MyClass ();
// Объявляем второй объект класса MyClass
MyClass objSecond=new MyClass ();
// Присваиваем числовое значение полю
// первого объекта
objFirst.set (10);
// Присваиваем символьное значение полю
// первого объекта
objFirst.set («F»);
// Присваиваем значения по умолчанию полям
// второго объекта
objSecond.set ();
// Выводим на печать значения полей первого объекта
System.out.println («Свойства первого объекта»);
objFirst.show ();
// Выводим на печать значения полей второго объекта
System.out.println (»\nСвойства второго объекта»);
objSecond.show ();
}
}
В этом примере мы описали метод с именем set () для присвоения значений полям объекта. Напомним, что такой метод принято называть «сеттер». Если в качестве аргумента сеттеру передано целое число, то он присваивает значение числовому полю. Если передано символьное значение, то оно присваивается символьному полю. Наконец, если переданы оба значения, то они присваиваются обоим полям в соответствии с их типом.
Обратите внимание, как реализована обработка вызова сеттера без аргументов. Мы захотели, чтобы в этом случае полям были присвоены значения по умолчанию: целочисленное 5 и символьное «A». Поэтому внутри сеттера без аргументов вызывается сеттер с двумя аргументами. В свою очередь, внутри сеттера для двух аргументов поочередно вызываются сеттеры для целочисленного аргумента и символьного аргумента. Со стороны это выглядит так, словно метод несколько раз вызывает сам себя. Но вы уже знаете, что на самом деле это разные методы с одинаковым именем. Допустимость таких конструкций — достоинство языка Java, позволяющее писать мощный и легко читаемый код. В нашем примере вложенная структура вызовов выглядит излишней, потому что мы используем очень простые методы — казалось бы, проще присвоить значения полям непосредственно в вызванном сеттере. Но в реальном программировании, когда каждый метод состоит из сотен строк кода, намного выгоднее вызвать уже описанный и отлаженный метод внутри другого метода, чем дублировать описание.
В главном классе программы мы создаем два объекта класса MyClass. Полям первого объекта мы присваиваем значения в явном виде. Полям второго объекта присваиваем значения по умолчанию. Затем мы выводим значения полей на печать.
Что произойдет, если вы попытаетесь в программе вызвать метод с аргументом, который не подходит ни одному из описаний методов класса? Ваша ошибка будет обнаружена и заблокирована средой NetBeans еще в процессе ввода текста программы. Кроме того, стоит вам ввести имя перегружаемого метода, как NetBeans покажет подсказку с перечислением доступных аргументов этого метода.
6.2.3 Конструктор класса
В листинге 6.4 мы использовали специальный метод (сеттер) для присвоения значений полям объекта. Но для присвоения начальных значений в момент создания объекта существует более простой и удобный механизм — конструктор. Это метод, автоматически вызываемый при создании объекта класса. В конструкторе определены действия, которые необходимо выполнить при создании объекта. Если конструктор не задан в явном виде, то при создании объекта используется так называемый конструктор по умолчанию. Когда в рассмотренных ранее примерах мы создавали объекты, то в этот момент использовался конструктор по умолчанию, который не выполнял никаких действий кроме выделения памяти под объект.
Если в описании конструктора применяются аргументы, то при создании объекта их необходимо передать конструктору, например:
MyClass obj=new MyClass (10, «A»);
Если аргументы конструктора не предусмотрены, то скобки остаются пустыми, и такая запись ничем не отличается от уже знакомого вам вызова конструктора по умолчанию:
MyClass obj=new MyClass ();
Если в классе описан хотя бы один конструктор, то конструктор по умолчанию становится недоступен. В этом случае вы можете вызывать конструктор только с теми аргументами, тип и количество которых описаны в конструкторе.
В классе может быть описано несколько конструкторов, которые можно перегружать аналогично обычным методам. Какой из конструкторов вызвать, определяется автоматически по количеству и типу аргументов. Чтобы сохранить возможность вызова конструктора объекта без аргументов, в классе нужно отдельно описать конструктор без аргументов. Конструктор может быть «пустым», то есть не выполнять никаких действий.
При описании конструктора следует соблюдать определенные правила. Имя конструктора должно совпадать с именем класса. Конструктор никогда не возвращает результат, но ключевое слово void не используется.
В листинге 6.5 приведен пример программы, в которой используются конструкторы объектов с перегрузкой.
Листинг 6.5 Пример использования конструкторов
class MyClass {
// Объявляем поля класса
int digit;
char letter;
// Конструктор класса без аргументов
MyClass () {
digit=9;
letter=«Z»;
System.out.println («Вызван конструктор объекта без аргументов.»);
System.out.println («Полям присвоены значения "+digit+" и "+letter);
}
// Конструктор класса с двумя аргументами
MyClass (int a, char b) {
digit=a;
letter=b;
System.out.println («Вызван конструктор объекта с двумя аргументами.»);
System.out.println («Полям присвоены значения "+digit+" и "+letter);
}
}
public class Listing6_5 {
public static void main (String [] args) {
// Создаем первый объект класса MyClass
// Вызывается конструктор без аргументов
MyClass objFirst=new MyClass ();
// Создаем второй объект класса MyClass
// Вызывается конструктор с двумя аргументами
MyClass objSecond=new MyClass (8, «B»);
}
}
В данном примере описаны два конструктора класса, которые при создании объекта присваивают начальные значения его полям. В набор команд конструктора добавлен вывод отладочных сообщений, чтобы вы могли наблюдать, что происходит при вызове конструктора класса.
6.2.4 Статические поля и методы
Когда мы создаем объект класса, то поля, описанные в классе, фактически превращаются в переменные объекта. Методы, описанные в классе, становятся методами объекта и имеют доступ к полям только «своего» объекта. Такие члены класса называют нестатическими.
Но могут существовать и статические члены класса. Они являются «общими» для всех объектов класса и существуют, даже если не создан ни один объект. К статическому члену класса можно обращаться через объект, указав через точку после имени объекта имя статического члена. Но предпочтительным является прямое обращение через имя класса. При этом после имени класса через точку указывают имя вызываемого статического члена класса.
При описании статического члена используется ключевое слово static. Статическое поле при необходимости можно инициализировать присвоением значения непосредственно в теле класса.
В определенном смысле, статические поля можно рассматривать, как глобальные переменные, доступные из любого места программы, а статические методы — как глобальные функции.
В листинге 6.6 приведен пример описания класса со статическими членами и обращения к ним.
Листинг 6.6 Пример класса со статическими членами
class MyClass {
// статическое числовое поле
static int number=5;
// статическое текстовое поле
static String text=«Hello»;
// статическй метод (вывод текста на печать)
static void showText () {
System.out.println (text);
}
// статический метод (вывод числа на печать)
static void showNumber () {
System.out.println (number);
}
}
public class Listing6_6 {
public static void main (String [] args) {
// прямое обращение к статическим методам
// без создания объекта класса
MyClass.showText ();
MyClass.showNumber ();
// прямое обращение к статическим полям
// без создания объекта класса
MyClass.number=15;
MyClass. text=«Java»;
// проверяем, изменились ли статические поля
// после прямого обращения
MyClass.showText ();
MyClass.showNumber ();
// создаем объект класса
MyClass obj=new MyClass ();
// обращаемся к статическим полям
// в качестве полей объекта
obj.showText ();
obj.showNumber ();
}
}
В данном примере мы включили в описание класса два статических поля, целочисленное и текстовое, а также два статических метода, которые выводят содержимое полей на печать.
В главном классе мы обращаемся напрямую к статическим методам класса. Для этого указываем имя класса и через точку — имя статического метода, принадлежащего классу. Затем аналогичным способом обращаемся напрямую к статическим полям и присваиваем им новые значения. Чтобы убедиться, что значения полей изменились, вновь обращаемся к статическим методам вывода на печать.
Далее создаем объект класса и выводим значения статических полей, обращаясь к методам через имя объекта с точкой. На печать будут выведены те же самые значения, что и в случае прямого обращения. В этом вся суть статических членов класса.
6.2.5 Закрытые члены класса
Очевидно, что статические поля являются общими для любых объектов класса. Вы можете создать сколько угодно объектов класса, и все они будут обращаться к одним тем же статическим полям и методам класса. Если в процессе выполнения программы изменить значение статического поля, то изменение затронет все объекты и фрагменты кода, использующие это поле. С одной стороны, это может быть удобно, если вы используете статическое поле в качестве глобальной переменной. Но в остальных случаях ошибочное изменение содержимого статического поля может быть очень опасным и приводит к трудно локализуемым ошибкам. Не зря в редакторе среды NetBeans IDE каждое внешнее обращение к статическому полю помечается предупреждением (желтый треугольник с восклицательным знаком).
Чтобы гарантированно предотвратить ошибочную модификацию значения статического поля, его объявляют закрытым при помощи ключевого слова private. Закрытые члены класса доступны только в теле класса, и к ним нет прямого доступа извне.
Как обратиться к закрытому полю? Для этого необходимо описать в классе открытый статический метод и вызвать его как обычно, через точку после имени класса. Пример класса с закрытым статическим полем и открытым статическим методом приведен в листинге 6.7.
Листинг 6.7 Пример класса с закрытым статическим полем
class MyClass {
// закрытое статическое текстовое поле
private static String text=«Hello»;
// открытый статический метод
// для изменения закрытого поля
static void setText (String txt) {
text=txt;
}
static void showText () {
System.out.println (text);
}
}
public class Listing6_7 {
public static void main (String [] args) {
// Выводим значение поля на печать
MyClass.showText ();
// Модифицируем значение поля
MyClass.setText («New text»);
// Выводим новое значение на печать
MyClass.showText ();
}
}
В данном примере мы описали класс с закрытым статическим полем text. Поскольку поле закрытое, мы не можем обратиться к нему извне через имя класса с точкой, как это было в листинге 6.6. Для работы с полем мы описали два открытых статических метода, showText () и setText ().
В главном классе программы сначала мы выводим содержимое поля на печать при помощи метода showText (). Затем модифицируем значение поля при помощи метода setText () и вновь выводим содержимое поля на печать, чтобы убедиться, что оно изменилось.
Ключевое слово public
При описании открытых членов класса можно использовать ключевое слово public, которое определяет уровень доступа. Во всех предыдущих примерах открытые члены класса были описаны без ключевого слова public (конструкция по умолчанию). В таком случае их доступность ограничивается текущим пакетом, и этого достаточно для простых программ. Если открытый член класса описан с идентификатором доступа public, то он доступен также и в других пакетах.
Глава 7. Наследование
Наследование — это один из ключевых принципов объектно-ориентированного программирования. Идея наследования проста: при описании нового класса мы берем за основу существующий класс, его поля и методы.
Исходный класс называется суперклассом или родительским классом (parent class). Класс, созданный на основе суперкласса, называется подклассом или дочерним классом (child class).
Подкласс наследует у суперкласса все его открытые поля и методы. Допускается каскадное или многократное наследование, то есть подкласс сам может быть суперклассом по отношению к созданным на его основе подклассам.
Суперкласс может быть пользовательским или библиотечным. Пользовательский суперкласс описан непосредственно в программе пользователя. Библиотечный суперкласс описан в одной из библиотек. Это могут быть как стандартные библиотеки языка Java, так и подключаемые библиотеки сторонних разработчиков.
Почему удобнее создавать подклассы, а не ограничиваться использованием уже имеющихся классов? Зачастую оказывается, что функциональности готового суперкласса недостаточно для выполнения регулярно вызываемого действия в программе. Подкласс расширяет возможности суперкласса за счет добавления новых полей и методов. При вызове стандартных методов библиотечного класса могут использоваться громоздкие наборы аргументов, которые сами по себе требуют предварительной обработки. В таком случае удобнее «упаковать» обработку данных и обращение к стандартным методам внутрь подкласса. Кроме того, при использовании библиотечных классов из пакета поставки Java не предусмотрена возможность свободно редактировать их по своему усмотрению. Единственный разумный подход — описать в программе собственный наследующий класс и дорабатывать его, не рискуя испортить стандартные библиотеки.
Если говорить о готовом коде, то наследование значительно улучшает общую структуру, читаемость и надежность программы.
В языке Java запрещено множественное наследование. Подкласс может наследовать поля и методы только у одного суперкласса. В некоторых других языках, включая С++, подкласс может наследовать одновременно несколько суперклассов.
7.1 Создание подкласса
Для создания подкласса необходимо после его имени указать ключевое слово extends (расширяет) и далее указать имя суперкласса:
class ChildClass extends ParentClass {
// поля и методы подкласса
}
В результате подкласс ChildClass будет иметь те же поля и методы, что и суперкласс ParentClass, но к ним добавятся еще поля и методы из описания подкласса.
Объекты, созданные на основе подкласса, не зависят от объектов, созданных на основе суперкласса, и никак не влияют друг на друга.
В листинге 7.1 приведен пример использования суперкласса и подкласса для создания независимых объектов, обладающих разным набором методов.
Листинг 7.1 Создание подкласса на основе пользовательского суперкласса
// описание суперкласса
class MyParentClass {
// числовое поле суперкласса
int number=5;
// текстовое поле суперкласса
String text=«Hello»;
// методы суперкласса
void showText () {
System.out.println (text);
}
void showNumber () {
System.out.println (number);
}
}
// описание подкласса
class MyChildClass extends MyParentClass {
int sum (int a) {
return number+a;
}
}
public class Listing7_1 {
public static void main (String [] args) {
// создаем объект суперкласса
MyParentClass objParent=new MyParentClass ();
// создаем объект подкласса
MyChildClass objChild=new MyChildClass ();
// вызываем методы суперкласса
objParent.showNumber ();
objParent.showText ();
// вызываем методы подкласса
objChild.showNumber ();
objChild.showText ();
// вызываем дополнительный метод подкласса
int b=objChild.sum (12);
// выводим результат вызова метода на печать
System.out.println (b);
}
}
В данном примере мы описали класс MyParentClass, в котором определены два поля — целочисленное и текстовое, а также два метода для вывода этих полей на печать.
Допустим, что при использовании одного из объектов класса нам приходится регулярно суммировать некие числа со значением целочисленного поля number. Очевидно, что для этого необходимо добавить метод, которого нет в описании класса. Конечно, можно было бы переписать класс MyParentClass, добавив в него новый метод. Но в случае, когда класс проверен, отлажен и применяется во многих других программах, не следует редактировать его по любому поводу, рискуя внести ошибку или путаницу в готовый код.
Для внесения изменений и дополнений мы воспользуемся механизмом наследования. Создадим подкласс MyChildClass, в котором опишем дополнительный метод sum (). Подкласс полностью наследует открытые поля и методы суперкласса, поэтому метод sum () свободно обращается к полю number. Это поле объявлено и существует, хотя и не упомянуто в явном виде при описании подкласса.
Далее, в главном методе программы мы создаем объект суперкласса objParent и объект подкласса objChild. Еще раз подчеркну, что это абсолютно равноправные и независимые объекты. Разница лишь в том, что объект objChild располагает методом sum (), которого нет у объекта objParent.
Убедимся в том, что свойства объектов именно такие, как ожидалось. Сначала выведем на печать содержимое полей объекта objParent:
objParent.showNumber ();
objParent.showText ();
Затем выведем на печать содержимое полей объекта objChild:
objChild.showNumber ();
objChild.showText ();
Результат выполнения этих блоков команд будет одинаковым, потому что реализовано наследование полей:
5
Hello
5
Hello
Теперь для объекта objChild вызовем метод sum () и выведем результат работы метода на печать. Как видите, подкласс MyChildClass успешно расширил суперкласс MyParentClass при помощи нового метода. Благодаря наследованию мы можем произвольно редактировать дополнительные поля и методы, не затрагивая исходный суперкласс.
7.1.1 Конструктор подкласса
Давайте вспомним, что такое конструктор класса, о котором подробно говорилось в разделе 6.2.3. Зачастую при создании объекта необходимо присвоить его полям начальные значения. Поскольку можно создать несколько объектов одного класса, то их поля могут быть инициализированы разными значениями. Для этого в классе должен быть описан специальный метод (конструктор), который срабатывает в момент создания объекта, получает аргументы и выполняет нужные действия.
При создании объекта подкласса ситуация сложнее — сначала вызывается конструктор суперкласса, и мы должны как-то передать ему аргументы. Для этого в теле конструктора подкласса первой командой следует указать вызов конструктора суперкласса при помощи ключевого слова super с круглыми скобками. В скобках указывают аргументы, которые передаются конструктору суперкласса. Если аргументов нет, оставляют пустые скобки.
В листинге 7.2 приведен пример использования конструктора подкласса. Обратите внимание на то, как происходит обращение к полям при помощи нового для вас ключевого слова this.
Листинг 7.2 Использование конструктора подкласса
// описание суперкласса
class MyParentClass {
// поля родительского класса
String text;
int number;
// конструктор родительского класса
MyParentClass (String text, int number) {
// присваиваем полям значения аргументов
this. text=text;
this.number=number;
// выводим значения полей на печать
System.out.println («Сработал конструктор суперкласса!»);
}
}
// описание подкласса
class MyChildClass extends MyParentClass {
char letter;
int digit;
// конструктор подкласса
MyChildClass (String text, int number, char letter, int digit) {
// вызываем конструктор суперкласса
super (text, number);
this. letter=letter;
this. digit=digit;
System.out.println («Сработал конструктор подкласса!»);
}
// описание метода подкласса
void show () {
// Выводим на печать значения всех полей объекта
// присвоенные конструктором подкласса
System.out.println («text="+this. text);
System.out.println("number="+this.number);
System.out.println («letter="+this. letter);
System.out.println («digit="+this. digit);
}
}
public class Listing7_2 {
public static void main (String [] args) {
// создаем объект подкласса
// и передаем аргументы в конструктор подкласса
MyChildClass obj=new MyChildClass («Hello», 200,«S», 5);
obj.show ();
}
}
В описании родительского класса MyParentClass присутствует конструктор с двумя аргументами. Конструктор получает в виде аргументов строку и целое число, которые присваивает полям объекта.
Отступление: ключевое слово this
Ключевое слово this может использоваться, как ссылка на объект, из которого вызывается метод. Если this используется в конструкторе, то является ссылкой на создаваемый объект, или применяется при вызове одной версии конструктора из другой версии конструктора.
В данной программе имена аргументов конструктора совпадают с именами полей класса. Аргументы методов и конструкторов являются локальными переменными. Если имя локальной переменной совпадает с именем поля класса, то по умолчанию считается, что речь идет о локальной переменной, а не о поле. Чтобы в такой ситуации обратиться к полю, нужно указать его полное имя, включая имя объекта через точку. Вместо указания полного имени объекта в конструкторах и методах применяют универсальное ключевое слово this («этот» — англ.) Вы не можете использовать в конструкторе или методе какое-то конкретное имя объекта. Ведь на основе класса и его конструктора может быть создано множество независимых объектов с разными именами. Поэтому применяется универсальная ссылка «этот», указывающая на объект, с которым программа работает в данный момент.
Разумеется, имена аргументов конструкторов и методов класса могут не совпадать с именами полей. В таком случае можно обойтись без ключевого слова this. Но для улучшения читаемости программы имена аргументов часто делают совпадающими с именами полей. Это облегчает понимание того, каким полям суперкласса или подкласса мы передаем значения при вызове конструктора или метода. За такое удобство приходится платить обязательным использованием ключевого слова this.
Вернемся к коду примера из листинга 7.2. В описании подкласса MyChildClass объявлены поля подкласса letter и digit, а также метод show (). Они расширяют описание суперкласса MyParentClass.
В теле главного метода программы создается объект подкласса. При вызове конструктора подкласса ему передаются четыре аргумента. Первые два аргумента «предназначены» для полей суперкласса, оставшиеся два — для полей подкласса. Когда срабатывает конструктор подкласса, то в первой же строке он вызывает конструктор суперкласса при помощи ключевого слова super () и передает аргументы в круглых скобках. Только после этого конструктор подкласса выполняет остальные команды. В завершение программы происходи обращение к методу show (), который выводит на печать значения всех полей созданного объекта.
7.2 Переопределение и перегрузка методов
Довольно часто возникает необходимость изменить метод, описанный в суперклассе, чтобы он выполнял другие действия. Для этого следует переопределить данный метод в описании подкласса.
Пример переопределения метода приведен в листинге 7.3.
Листинг 7.3 Пример переопределения метода
class MyParentClass {
int number=5;
// исходный метод суперкласса
void show () {
System.out.println («Метод суперкласса»);
System.out.println (number);
}
}
class MyChildClass extends MyParentClass {
// переопределение метода суперкласса
@Override
void show () {
System.out.println («Новый метод подкласса»);
System.out.println (number*2);
}
}
public class Listing7_3 {
public static void main (String [] args) {
// создаем объект суперкласса
MyParentClass objParent=new MyParentClass ();
// создаем объект подкласса
MyChildClass objChild=new MyChildClass ();
// вызываем метод суперкласса
objParent.show ();
// вызываем переопределенный метод подкласса
objChild.show ();
}
}
В описании суперкласса определен метод, который выводит на печать значение поля number. В описании подкласса этот метод переопределен таким образом, чтобы на печать выводилось удвоенное значение поля number. Далее мы создаем объект суперкласса и объект подкласса, и для каждого из них вызываем метод show (). Поскольку метод show () был переопределен, то в первом случае на печать выводится число 5, а во втором — число 10. Мы убедились, что переопределение метода относится только к объектам подкласса и никак не затрагивает объекты суперкласса.
Отступление: аннотации @Override и @Deprecated
В листинге 7.3 мы впервые применили аннотацию компилятора @Override. Эта аннотация вынуждает компилятор проверить, существует ли метод суперкласса, который мы хотим переопределить. Ведь на практике вполне возможна ситуация, когда в качестве суперкласса вы используете класс из библиотеки стороннего разработчика, а он вдруг взял и удалил некоторые методы в новой версии библиотеки.
Если метод суперкласса отсутствует, то благодаря аннотации @Override возникнет ошибка компиляции. На самом деле, в среде NetBeans IDE красный индикатор ошибки появится еще в момент написания кода. Вы можете не использовать аннотацию @Override, но в таком случае рискуете допустить логическую ошибку использования методов, которую сложно обнаружить. Поэтому рекомендуется помещать аннотацию @Override перед описанием каждого переопределяемого метода.
Если суперкласс содержит устаревший метод, и вы хотите предупредить других пользователей (или напомнить себе), что это метод скоро будет удален, то перед описанием этого метода следует поставить аннотацию @Deprecated. При компиляции кода, в котором используется или переопределяется устаревший метод, в логе компилятора будет выведено предупреждение.
В разделе 6.2.2 мы уже говорили о том, что в рамках класса можно описать несколько версий метода с одинаковым именем, которые различаются только набором аргументов. Такой подход называется перегрузкой метода. Язык Java позволяет очень гибко и свободно оперировать приемами переопределения и перегрузки методов при описании подклассов.
Допустим, у нас имеется суперкласс, в котором описано несколько версий одного и того же метода, различающиеся набором аргументов (перегрузка метода). В описании подкласса вы можете выполнить следующие действия:
— описать заново одну или несколько версий метода (переопределение метода),
— дописать одну или несколько новых версий метода (расширение метода),
— оставить без изменения остальные версии метода (наследование метода).
Важно, что все перечисленные операции вы можете выполнить одновременно, в одном описании подкласса и применительно к одному и тому же методу. В любом из перечисленных случаев компилятор определяет, о какой версии перегруженного метода идет речь, на основе набора аргументов.
7.2.1 Вызов метода суперкласса
Зачастую бывает удобно не переписывать метод суперкласса полностью, а вызвать исходный метод суперкласса, и по его завершению выполнить дополнительные команды. В качестве примера возьмем фрагмент кода из листинга 7.3 и добавим в него вызов исходного метода, обозначенный жирным шрифтом:
// переопределение метода суперкласса
@Override
void show () {
super.show ();
System.out.println («Новый метод подкласса»);
System.out.println (number*2);
}
В данном примере при вызове метода подкласса сначала сработает одноименный метод суперкласса, к которому мы обратились через ключевое слово super. Затем сработают две дополнительных команды переопределенного метода подкласса.
Глава 8. Абстрактные классы и интерфейсы
Из предыдущих глав этой книги вы узнали, что класс состоит из полей и методов, которые нужно описать. При описании метода мы обязательно указываем сигнатуру (тип возвращаемого результата, название метода, список аргументов). В теле метода мы размещаем программный код, выполняемый при вызове метода.
8.1 Абстрактные классы
В языке Java существует возможность указать только сигнатуру метода, и оставить пустым тело метода, не вставляя туда исполняемый код. Такой «пустой» метод называется абстрактным и обозначается ключевым словом abstract.
Если в классе есть хотя бы один абстрактный метод, такой класс тоже называется абстрактным и описывается с ключевым словом abstract.
На основе абстрактного класса нельзя создать объект. Причина вполне очевидна: объект, созданный на основе абстрактного класса, содержит методы, программный код которых не определен. Абстрактные классы используются исключительно в качестве «заготовки» для наследования. На основе абстрактного суперкласса создаются подклассы, в которых определяется программный код методов. Иными словами, на основе одного абстрактного шаблона вы можете создать множество подклассов с похожей структурой, но совершенно разным программным кодом методов.
Если в подклассе, созданном на основе абстрактного суперкласса, хотя бы один из методов оставить без описания, такой подкласс тоже будет абстрактным.
Начинающие программисты обычно отрицательно относятся к идее абстрактных классов, считая ее излишней и надуманной. Но при разработке сложных программ, состоящих из десятков и сотен тысяч строк программного кода, использование абстрактных классов является мощным методом упорядочения и структурирования кода, а также облегчает документирование и рабочее взаимодействие групп программистов.
В листинге 8.1 приведен пример использования абстрактного суперкласса для создания трех подклассов.
Листинг 8.1 Пример использования абстрактного суперкласса
// абстрактный суперкласс
abstract class Animals {
String name;
String sound;
int weight;
// конструктор класса
Animals (String nm, String snd, int wt) {
name=nm;
sound=snd;
weight=wt;
}
// абстрактный метод
abstract void doAnimal ();
}
// подкласс (кошка)
class Cat extends Animals {
// конструктор
Cat (String nm, String snd, int wt) {
super (nm, snd, wt);
}
// описание метода, наследованного из суперкласса
@Override
void doAnimal () {
System.out.println («Животное "+name+" весит примерно "+weight+«кг, издает звук "+sound);
System.out.println («Это животное выполняет действие:»);
System.out.println («Ловит мышей.»);
}
}
// подкласс (собака)
class Dog extends Animals {
// конструктор
Dog (String nm, String snd, int wt) {
super (nm, snd, wt);
}
// описание метода, наследованного из суперкласса
@Override
void doAnimal () {
System.out.println («Животное "+name+" весит примерно "+weight+«кг, издает звук "+sound);
System.out.println («Это животное выполняет действие:»);
System.out.println («Охраняет дом и хозяина.»);
}
}
public class Listing8_1 {
public static void main (String [] args) {
// объект кошка Маруся подкласса Cat
Cat objCat=new Cat («Маруся», «Мур-мур-мур», 3);
// объект пес Тузик подкласса Dog
Dog objDog1=new Dog («Тузик», «Гав-гав-гав», 9);
// объект пес Барбос подкласса Dog
Dog objDog2=new Dog («Барбос», «Р-р-р-р-р», 15);
// метод объекта подкласса Cat
objCat.doAnimal ();
// метод первого объекта подкласса Dog
objDog1.doAnimal ();
// метод второго объекта подкласса Dog
objDog2.doAnimal ();
}
}
Пример из листинга 8.1 наглядно демонстрирует удобство и гибкость использования шаблона (абстрактного суперкласса) для последующего описания подклассов и создания конкретных объектов. Фактически, мы формируем удобную иерархическую систему. Сначала на основе абстрактного суперкласса мы можем создать различные подклассы, реализующие нужные варианты методов.
В нашем случае и кошки, и собаки относятся к общему абстрактному классу Animals (животные), но выполняют совершенно разные действия. Кошки ловят мышей, собаки стерегут дом. Более того, мы знаем, что разные собаки могут выполнять разные функции. Овчарка пасет овец, болонка радует хозяйку. При необходимости мы легко можем описать разные подклассы для овчарок и болонок в рамках абстрактного класса Animals. Или можно поступить еще правильнее — создать абстрактный подкласс Dog, а от него унаследовать подклассы Bolonka, Ovcharka и так далее. Пусть это будет заданием для вашей самостоятельной работы на основе листинга 8.1.
8.2 Интерфейсы
Интерфейс — это объявление методов и/или статических констант, напоминающее абстрактные классы, но без описания класса. Подчеркнем, что в интерфейсе методы только объявляются. Использование интерфейса в классе напоминает наследование абстрактного класса и называется реализацией интерфейса.
Существуют определенные правила реализации интерфейсов. Класс, который реализует интерфейс, должен содержать описание всех методов интерфейса. Один и тот же класс может реализовать одновременно несколько интерфейсов. Это важный нюанс. Напомним, что в языке Java запрещено множественное наследование, и подкласс может наследовать только один суперкласс. Реализация нескольких интерфейсов позволяет в какой-то мере обойти это ограничение.
Описание интерфейса начинается с ключевого слова interface. В теле интерфейса объявляются методы и статические поля:
interface имя_интерфейса {
// объявление методов и полей
}
Поля интерфейса объявляются, как обычные поля со значениями, но воспринимаются компилятором так, будто имеют ключевые слова static и final, то есть являются константами. В объявлении методов не используется слово public, но на самом деле эти методы по умолчанию являются открытыми методами.
В листинге 8.2 приведен пример реализации интерфейса.
Листинг 8.2 Пример реализации интерфейса
// описание интерфейса
interface MyInterface {
// статическая константа
int DISTANCE=25;
// объявление методов
int mult (int a);
double div (double b);
}
// класс, реализующий интерфейс
class MyClass implements MyInterface {
// реализация метода mult ()
@Override
public int mult (int a) {
return (a*2);
}
// реализация метода div ()
@Override
public double div (double b) {
return (b/3);
}
}
public class Listing8_2 {
public static void main (String [] args) {
// объект класса
MyClass obj=new MyClass ();
// вывод на печать результатов работы
// реализованных методов и константы
System.out.println (obj. DISTANCE);
System.out.println(obj.mult (5));
System.out.println (obj. div (7));
}
}
В данном примере мы описали интерфейс, состоящий из одной константы и двух методов. Далее описан класс, в котором реализованы методы. Для указания на реализуемый интерфейс используется ключевое слово implements.
Обратите внимание, что в описании реализуемых методов обязательно должно присутствовать ключевое слово public. Таково требование языка Java.
В описании класса не упомянута константа DISTANCE. Но она подключается автоматически, когда мы ссылаемся на интерфейс при помощи ключевого слова implements. В этом нетрудно убедиться, если создать объект класса и вывести константу данного объекта на печать. Программа завершается выводом на печать результатов работы реализованных методов интерфейса.
8.2.1 Интерфейсные переменные
В языке Java допускается использование интерфейсных переменных. Такая переменная может ссылаться на объект класса, реализующего интерфейс. Вместо типа переменной в описании указывают имя класса. Это вполне очевидно, потому что интерфейсная переменная ссылается на объект, свойства которого полностью определены в описании конкретного класса.
Важное ограничение интерфейсной переменной: она имеет доступ только к тем методам объекта, которые объявлены в реализуемом интерфейсе. Иначе говоря, если вы создали класс, который реализует методы интерфейса, добавили в этот класс дополнительные методы, и создали объект на основе данного класса, то интерфейсная переменная не будет иметь доступ к дополнительным методам объекта.
В листинге 8.3 приведен пример использования интерфейсной переменной и показано, как можно обращаться к дополнительным методам класса.
Листинг 8.3 Пример использования интерфейсной переменной
// объявление интерфейса
interface MyInterface {
// объявление метода интерфейса
void show ();
}
// описание класса, реализующего интерфейс
class MyClass implements MyInterface {
int number;
// конструктор класса
MyClass (int n) {
number=n;
}
// реализация метода интерфейса
@Override
public void show () {
System.out.println (number);
}
// дополнительный метод класса
void showDouble () {
System.out.println (number*2);
}
}
public class Listing8_3 {
public static void main (String [] args) {
// объявляем интерфейсную переменную ref
MyInterface ref;
// создаем объект класса MyClass
// и сохраняем ссылку в переменной интерфейса
ref=new MyClass (5);
// вызываем метод интерфейса
ref.show ();
// создаем второй объект класса MyClass
MyClass obj=new MyClass (6);
// присваиваем ссылку интерфейсной переменной
ref=obj;
// вызываем метод интерфейса
ref.show ();
// вызываем дополнительный метод класса
obj.showDouble ();
}
}
В данном примере класс MyClass не только реализует интерфейсный метод show (), но и описывает дополнительный метод showDouble (), который выводит на печать удвоенное значение аргумента.
Далее мы создаем два объекта класса и по очереди присваиваем интерфейсной переменной ссылку на эти объекты. В первом случае мы используем сокращенный вариант записи присвоения, когда ссылка присваивается переменной непосредственно в момент создания объекта:
ref=new MyClass (5);
В данном случае объект доступен только через интерфейсную переменную, и никакой другой ссылки на объект не существует. Поэтому мы никак не можем вызывать в теле главного класса дополнительный метод showDouble () — он не объявлен в интерфейсе, к которому принадлежит переменная.
Во втором случае мы используем привычный подход, и сначала создаем объект, на который ссылается обычная объектная переменная obj.
MyClass obj=new MyClass (6);
Затем мы присваиваем ссылку на этот объект интерфейсной переменной ref.
ref=obj;
Теперь мы можем вызывать интерфейсный метод show () как через интерфейсную переменную, так и через обычную объектную переменную. Но дополнительный метод showDouble () мы можем вызвать только через объектную переменную:
obj.showDouble ();
Данный пример иллюстрирует гибкость языка Java и нюансы доступности объектов.
Отступление: зачем нужны интерфейсные переменные?
Остается открытым вопрос — зачем придумали интерфейсные переменные, если существует обычный механизм обращений к методам и свойствам объекта через имя объектной переменной с точкой? Использование интерфейсных переменных облегчает структурирование программы и групповую работу над ней. Представьте ситуацию, когда группа разработчиков договорилась объявить некий общий перечень доступных методов программного продукта. А дальше каждое из подразделений по мере необходимости создает собственные реализации методов в виде набора классов (библиотек). Создание и использование объектов этих классов — это иной уровень разработки, на котором удобнее использовать осмысленные имена интерфейсных переменных. Кроме того, если вы, реализовав согласованный ранее интерфейс, попытаетесь вызвать не объявленный в данном интерфейсе метод, то получите ошибку компиляции. В таком случае вам придется убедить остальных разработчиков (или руководителя проекта) в необходимости добавить в интерфейс (и, кстати, обязательно задокументировать!) новый метод. Да, это сложно. Но это — дисциплина программирования, без которой невозможна разработка сложных продуктов.
8.2.2 Методы по умолчанию
Мы говорили о том, что в интерфейсе языка Java методы только объявляются, но не описываются. На самом деле, начиная с версии Java 8 допускается описание методов интерфейса — так называемые методы по умолчанию.
Если метод по умолчанию явно не определен в реализующем классе, то будет использован код из описания метода в интерфейсе. При описании метода по умолчанию применяется ключевое слово default, например:
interface MyInterface {
default void print () {
System.out.println («Метод по умолчанию»);
}
}
Интерфейс может одновременно содержать произвольное количество различных методов по умолчанию и «пустых» объявлений методов. Если интерфейс содержит несколько разных методов по умолчанию, то в реализующем классе можно переопределить одни методы и использовать по умолчанию другие. В этом смысле программисту предоставлена полная свобода действий.
Проблема возникает только в том случае, если класс реализует несколько интерфейсов, и метод с одинаковой сигнатурой (имя, тип, аргументы) описан более чем в одном интерфейсе. Данная неоднозначность порождает ошибку компиляции, потому что не ясно, какое описание метода использовать. Для устранения неоднозначности используйте явное указание имени интерфейса, ключевое слово super и имя метода, разделенные точками:
Имя_Интерфейса.super. имя_метода ();
8.2.3 Наследование интерфейсов
Интерфейс может наследовать (расширять) описания методов и статических констант из другого интерфейса. Механизм наследования интерфейсов аналогичен наследованию классов. В описание наследующего интерфейса добавляется ключевое слово extends.
Методы, объявленные в родительском интерфейсе, могут быть переобъявлены в наследующем интерфейсе. Наследующий интерфейс может содержать собственные методы и константы.
В качестве примера приведем короткий фрагмент программы, в котором выполняется наследование интерфейса:
interface Parent {
//
int static final DISTANCE=10;
//
default void show () {
System.out.println («Метод по умолчанию»);
}
}
interface Child extends Parent {
void draw ();
}
В данном примере в родительском интерфейсе Parent объявлены константа и метод по умолчанию. В наследующем интерфейсе Child объявлен абстрактный метод, который расширяет сигнатуру родительского интерфейса. Напомним, что все абстрактные методы интерфейса, не содержащие код, должны быть описаны в классе, который реализует интерфейс. В противном случае этот класс сам станет абстрактным.
8.2.4 Совмещение наследования и реализации
Класс может быть наследником суперкласса и одновременно реализовывать один или несколько интерфейсов. Несложно догадаться, что в описании подкласса одновременно используются два ключевых слова — extends и implements, например:
class MyChildClass extends MyParentClass implements One, Two {
// описание полей и методов подкласса,
// реализация методов интерфейсов
}
В данном фрагменте кода подкласс MyChildClass наследует поля и методы суперкласса MyParentClass и одновременно реализует методы интерфейсов One и Two.
В листинге 7.1 приведен пример создания подкласса на основе суперкласса, а в листинге 8.2 приведен пример реализации интерфейса, поэтому нет необходимости загромождать книгу еще одним примером, в котором эти механизмы используются одновременно. Если вы внимательно прочитали и усвоили материал предыдущих разделов, то вполне можете разработать собственный пример и проверить его в среде NetBeans IDE.
Глава 9. Обработка исключительных ситуаций
Исключительными ситуациями называют ошибки, возникающие во время выполнения программы. В зарубежной практике их называют коротко — исключения (exceptions). В большинстве случаев возникновение исключительной ситуации приводит к аварийному завершению программы с нежелательными последствиями — потеря несохраненных данных, поломка оборудования и т. д. Поэтому в языке Java, как и во многих других языках программирования, предусмотрены специальные средства для перехвата и обработки исключительных ситуаций.
Исключительные ситуации можно условно разделить на две группы — непредсказуемые и предсказуемые. Непредсказуемые исключения чаще всего возникают вследствие ошибок, допущенных при разработке программы. Мы не знаем, в каком месте программы затаилась ошибка, допущенная разработчиком, и не можем назначить обработку ошибочной ситуации. К счастью, возникновение многих исключительных ситуаций можно предвидеть. Обычно это ошибки взаимодействия программы с внешним миром, такие, как невозможность сохранить файл на диск, обрыв канала связи, неправильные входные данные и тому подобные. Такие ошибки не должны приводить к внезапному аварийному завершению программы. Как минимум, следует уведомить пользователя о проблеме и дать ему возможность предпринять какие-то действия.
Обработчик исключительных ситуаций может выполнять различные действия, которые определяются программным кодом обработчика, например:
— сообщить пользователю о возникновении исключительной ситуации;
— принудительно завершить программу, предварительно сохранив рабочие данные и логи выполнения;
— продолжить выполнение программы, сохранив запись об ошибке в логе;
— направить выполнение программы по другой ветке алгоритма;
— циклически проверять параметры, вызвавшие исключение, и ждать, пока ситуация нормализуется.
Все перечисленные выше подходы относятся к решениям на уровне алгоритма приложения и определяются разработчиком программы. Но пока об этом говорить рано. Сначала вы должны понять, как устроен и работает механизм обработки исключительных ситуаций на уровне языка Java.
9.1 Перехват исключений в блоке try–catch
В языках предыдущих поколений для перехвата исключительных ситуаций применялись многочисленные проверки на допустимость введенных значений и математических операций, разбросанные по всему коду программы. Такие проверки существенно замедляли выполнение программы и не могли гарантировать, что разработчик предусмотрел условия для проверок на все случаи жизни.
В современных языках программирования, включая Java, существует специальный механизм обработки исключительных ситуаций — защищенный блок кода. Если при выполнении команд внутри этого блока возникает исключительная ситуация, она не приводит к аварийному завершению программы, а всего лишь порождает исключение, которое подлежит обработке. Более того, после обработки исключения выполнение программы может быть успешно продолжено. Например, если пользователь пытается сохранить файл на защищенный от записи носитель, ему будет предложено сохранить файл в другом месте.
Защищенный блок кода обозначается ключевым словом try (попытаться), после которого следуют необязательные блоки перехвата исключений catch и необязательный завершающий блок finally:
try {
//проверяемый блок операторов
}
catch (Исключение_типа_1 переменная1) {
// операторы обработки Исключения_1
}
catch (Исключение_типа_2 переменная2) {
// операторы обработки Исключения_2
}
catch (Исключение_типа_3 переменная3) {
// операторы обработки Исключения_3
}
finally {
// блок финальных операторов
}
В общем случае проверяемый блок программного кода может породить несколько исключений, поэтому допускается использование нескольких операторов catch.
Если выполнение проверяемого блока не вызвало исключение, то операторы catch игнорируются, выполняется блок finally (если он существует), и далее выполняется остальной код программы.
Команды необязательного блока finally исполняются в любом случае, независимо от того, возникало ли исключение. Обычно блок finally используется при обработке вложенных связок try-catch, которые мы рассмотрим позже.
Если в процессе выполнения проверяемого блока операторов возникло исключение, то выполнение программы приостанавливается, и создается объект, который содержит информацию об ошибке. Вы знаете, что объекты создаются на основе классов. В языке Java имеется суперкласс Throwable и его подклассы Error и Exception. Подкласс Error относится к фатальным ошибкам, которые невозможно обработать программными методами. У подкласса Exception имеется несколько собственных подклассов. Среди них есть подкласс RuntimeException, который является родителем для множества классов, описывающих исключения (exception), возникающие во время выполнения программы (runtime).
Созданный объект (исключение) передается для обработки методу, который вызвал ошибку. Иногда говорят, что исключение вбрасывается (throw) в метод. Если в данном методе обработка ошибки не предусмотрена, то объект передается выше — тому методу, который вызвал ошибочный метод, вплоть до главного метода программы. Если ошибка нигде не обрабатывается, то срабатывает обработчик по умолчанию, реализованный в Java-машине, и программа досрочно прекращает работу.
Итак, если мы предполагаем, что некий блок кода может вызвать ошибку, то «упаковываем» его в блок try, за которым располагаем блоки catch. Давайте рассмотрим простой пример перехвата и обработки исключительных ситуаций. Для этого вернемся к программе, представленной в листинге 4.4. Пользователю предлагается угадать число в диапазоне от 1 до 10, введя его в поле диалогового окна. Если вместо целого числа пользователь введет буквенные символы, например «А», это приведет к аварийному завершению программы, потому что возникнет исключение по несоответствию типов данных, которое выглядит следующим образом:
Exception in thread «main» java.lang.NumberFormatException:
For input string: «A»
at java.lang.NumberFormatException.forInputString
(NumberFormatException. java:65)
at java.lang.Integer.parseInt (Integer. java:580)
at java.lang.Integer.parseInt (Integer. java:615)
at Listing4_4.main (Listing4_4.java:14)
Ошибка возникает в строке номер 14 исходного кода, поднимается вверх в иерархии методов и порождает исключение NumberFormatException. Аналогичное исключение возникает, если пользователь нажал кнопку «Отмена». Наша задача — перехватить и обработать исключение NumberFormatException, сообщив пользователю, что он ввел недопустимое значение. В листинге 9.1 представлена доработанная программа, в которой реализована обработка исключительной ситуации.
Во время работы программы возможно возникновение трех ошибочных ситуаций:
— Пользователь ввел число, которое не лежит в диапазоне от 1 до 10;
— Пользователь нажал кнопку отмены ввода;
— В строке ввода присутствуют любые не числовые символы.
Как вы думаете, может ли первая ошибка вызвать аварийное завершение программы? На первый взгляд нет, и самое худшее, что может случиться — пользователь не угадает число. Но не забывайте, что для типа данных int допустимые значения лежат в диапазоне от -231 (-2147483648) до 231—1 (2147483647). Если пользователь введет целое число меньше -2147483648 или больше 2147483647, то попытка преобразовать строку ввода в тип int породит исключительную ситуацию. Как видите, ввод слишком большого или слишком маленького числа может вызвать двоякую реакцию программы (ошибка может случиться лишь иногда), и программист должен быть к этому готов.
Пользователь имеет право в любой момент прекратить угадывание, нажав кнопку отмены ввода. Но при этом тоже возникает исключительная ситуация, поскольку окно ввода возвращает пустое значение null, которое невозможно преобразовать в тип int.
Наконец, третья ошибка возникает, если в строке ввода присутствует хотя бы один не цифровой символ, включая десятичную точку. Программа ожидает ввод целого числа, поэтому ни буквы, ни числа с десятичной дробью не допускаются.
Листинг 9.1 Пример перехвата и обработки исключительной ситуации
import javax.swing.JOptionPane;
import java. util. Random;
public class Listing9_1 {
public static void main (String [] args) {
Random rnd = new Random(System.currentTimeMillis ());
int secret = 1 + rnd.nextInt (10);
int userData=0;
String userInput;
while (true) {
// Выводим окно запроса
userInput = JOptionPane.showInputDialog («Угадайте число от 1 до 10»);
// проверка опасного участка кода
try {
// Преобразуем строку в число в явном виде
userData = Integer.parseInt (userInput);
// проверяем введенное число на совпадение с секретным
if (userData == secret) {
JOptionPane.showMessageDialog (null, «Вы угадали число!»);
break;
}
}
// обработчик исключения
catch (NumberFormatException e) {
// если пользователь нажал кнопку «Cancel»
if(e.toString().contains («null»)) {
// прерывание работы программы
System. exit (0);
}
// если пользователь ввел недопустимое значение
System.out.println (e);
JOptionPane.showMessageDialog (null,«Недопустимое значение!","Ошибка",JOptionPane.ERROR_MESSAGE);
}
}
}
}
При помощи оператора try мы проверяем блок кода, состоящий из команды явного преобразования типа String в тип Integer и проверки полученного числа на совпадение с секретным числом, которое сгенерировала программа. Других участков кода, которые мы можем заподозрить в порождении исключительной ситуации, в программе нет. Если преобразование типа прошло успешно, программа продолжает работу в обычном режиме, команды из блока catch игнорируются.
Если в области действия оператора try возникло исключение, то выполнение штатного кода приостанавливается (проверка на совпадение уже не производится), и управление передается следующим за ним операторам catch.
Оператор catch имеет аргументы. Первый аргумент — стандартное имя исключения, описанное в документации. Поскольку все классы ошибок наследуют суперкласс Exception, то вместо имени конкретного исключения можно указать имя Exception и тогда в блоке catch будут перехвачены любые исключения, возникшие в предшествующем блоке try. Второй аргумент — ссылка на объект исключения, который создается, если возникла исключительная ситуация. По умолчанию в языке Java принято использовать букву «e».
Бесплатный фрагмент закончился.
Купите книгу, чтобы продолжить чтение.