Java 7

Хабибуллин Ильдар Шаукатович

Рассмотрено все необходимое для разработки, компиляции, отладки и запуска приложений Java. Изложены практические приемы использования как традиционных, так и новейших конструкций объектно-ориентированного языка Java, графической библиотеки классов Swing, расширенной библиотеки Java 2D, работа со звуком, печать, способы русификации программ. Приведено полное описание нововведений Java SE 7: двоичная запись чисел, строковые варианты разветвлений, "ромбовидный оператор", NIO2, новые средства многопоточности и др. Дано подробное изложение последней версии сервлетов, технологии JSP и библиотек тегов JSTL. Около двухсот законченных программ иллюстрируют рассмотренные приемы программирования. Приведена подробная справочная информация о классах и методах Core Java API.

 

Ильдар Хабибуллин

Санкт-Петербург

«БХВ-Петербург»

2012

УДК 681.3.06 ББК 32.973.26-018.2 Х12

Хабибуллин И. Ш.

Х12 Java 7. — СПб.: БХВ-Петербург, 2012. — 768 с.: ил. — (В подлиннике) ISBN 978-5-9775-0735-6

Рассмотрено все необходимое для разработки, компиляции, отладки и запуска приложений Java. Изложены практические приемы использования как традиционных, так и новейших конструкций объектно-ориентированного языка Java, графической библиотеки классов Swing, расширенной библиотеки Java 2D, работа со звуком, печать, способы русификации программ. Приведено полное описание нововведений Java SE 7: двоичная запись чисел, строковые варианты разветвлений, "ромбовидный оператор", NIO2, новые средства многопоточности и др. Дано подробное изложение последней версии сервлетов, технологии JSP и библиотек тегов JSTL. Около двухсот законченных программ иллюстрируют рассмотренные приемы программирования. Приведена подробная справочная информация о классах и методах Core Java API.

Для программистов

УДК 681.3.06 ББК 32.973.26-018.2

Группа подготовки издания:

Главный редактор Зам. главного редактора Зав. редакцией Редактор

Компьютерная верстка Корректор Дизайн серии Оформление обложки Зав. производством

Екатерина Кондукова Игорь Шишигин Григорий Добин Екатерина Капалыгина Ольги Сергиенко Зинаида Дмитриева Инны Тачиной Елены Беляевой Николай Тверских

Лицензия ИД № 02429 от 24.07.00. Подписано в печать 31.08.11. Формат 70x100 1 / 16 . Печать офсетная. Уcл. печ. л. 61,92.

Тираж 1800 экз. Заказ №

"БХВ-Петербург", 190005, Санкт-Петербург, Измайловский пр., 29.

Санитарно-эпидемиологическое заключение на продукцию № 77.99.60.953.Д.005770.05.09 от 26.05.2009 г. выдано Федеральной службой по надзору в сфере защиты прав потребителей и благополучия человека.

Отпечатано с готовых диапозитивов в ГУП "Типография "Наука"

199034, Санкт-Петербург, 9 линия, 12

ISBN 978-5-9775-0735-6

© Хабибуллин И. Ш., 2011

© Оформление, издательство "БХВ-Петербург", 2011

 

Оглавление

 

Введение

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

□ представляет собой сгусток практического опыта, накопленного автором и его студентами с 1996 г.;

□ содержит ответы на часто задаваемые вопросы, последних "компьютерщики" называют FAQ (Frequently Asked Questions);

□ написана кратко и сжато, как конспект лекций, в ней нет лишних слов (за исключением, может быть, тех, что вы только что прочитали);

□ рассчитана на читателей, стремящихся быстро и всерьез ознакомиться с новинками компьютерных технологий;

□ содержит много примеров применения конструкций Java, которые можно использовать как фрагменты больших производственных разработок в качестве "How to...?";

□ включает материал, являющийся обязательной частью подготовки специалиста по информационным технологиям;

□ не предполагает знание какого-либо языка программирования, а для знатоков — выделяет особенности языка Java среди других языков;

□ предлагает обсуждение вопросов русификации Java.

Прочитав эту книгу, вы вступите в ряды программистов на Java — разработчиков передовой технологии начала XXI века.

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

Пошел второй десяток лет с того дня, когда были написаны эти строки. Все случилось так, как я и написал. Разошлись три издания книги "Самоучитель Java". Я видел много ее экземпляров в самом разном состоянии. Читатели высказали мне множество нелицеприятных соображений по поводу содержания книги, обнаруженных ошибок и опечаток. Студенты на зачетах и экзаменах пересказывали мне целые куски книги, что тоже наводило на размышления по поводу ее содержания и стиля изложения. У меня накопилось много дополнительного материала, который так и просился в книгу.

Технология Java развивается очень быстро. Сначала предназначавшаяся для небольших сетевых приложений, Java прочно утвердилась на Web-серверах, проникла в сотовые телефоны, планшеты и другие мобильные устройства. Популярная операционная система Android базируется на Java. Теперь Java — обязательная часть Web-программирования.

Развивается и сам язык. В него вводятся новые конструкции, появляются новые библиотеки классов. Графическая библиотека Swing стала частью стандартной поставки Java. В стандартную поставку теперь включены и средства работы с документами XML. Вышла уже седьмая версия Java.

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

Ну что же, начнем!

Что такое Java?

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

Сначала Java (официальный день рождения технологии Java — 23 мая 1995 г.) предназначалась для программирования бытовых электронных устройств, таких как сотовые телефоны и другие мобильные устройства.

Потом Java стала применяться для программирования браузеров — появились апплеты.

Затем оказалось, что на Java можно создавать полноценные приложения. Их графические элементы стали оформлять в виде компонентов — появились JavaBeans, с которыми Java вошла в мир распределенных систем и промежуточного программного обеспечения, тесно связавшись с технологией CORBA.

Остался один шаг до программирования серверов — этот шаг был сделан — появились сервлеты (servlets), страницы JSP (JavaServer Pages) и EJB (Enterprise JavaBeans). Серверы должны взаимодействовать с базами данных — появились драйверы JDBC. Взаимодействие оказалось удачным, и многие системы управления базами данных и даже операционные системы включили Java в свое ядро, например Oracle, Linux, MacOS X, AIX. Что еще не охвачено? Назовите и через полгода услышите, что Java уже вовсю применяется и там. Из-за этой размытости самого понятия его описывают таким же размытым словом — технология.

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

Pascal, C++ и др., вобрав их лучшие, по мнению создателей, черты и отбросив худшие. На этот счет есть разные мнения, но бесспорно, что язык получился удобным для изучения, написанные на нем программы легко читаются и отлаживаются: первую программу можно написать уже через час после начала изучения языка. Язык Java становится языком обучения объектно-ориентированному программированию, так же как язык Pascal был языком обучения структурному программированию. Недаром на Java уже написано огромное количество программ, библиотек классов, а собственный апплет не написал только уж совсем ленивый.

Для полноты картины следует сказать, что создавать приложения для технологии Java можно не только на языке Java, есть и другие языки: Clojure, Scala, Jython, есть даже компиляторы с языков Pascal и C++, но лучше все-таки использовать язык Java: на нем все аспекты технологии излагаются проще и удобнее.

Язык Java часто используется для описания различных приемов объектно-ориентированного программирования, так же как для записи алгоритмов применялся вначале язык Algol, а затем язык Pascal.

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

Язык Java тоже очень бурно развивается, некоторые его методы объявляются устаревшими (deprecated), появляются новые конструкции, увеличивается встроенная библиотека классов, но есть устоявшееся ядро языка, сохраняется его дух и стиль. Вот это-то устоявшееся и излагается в книге.

Структура книги

Книга состоит из пяти частей.

Часть I содержит три главы, в которых рассматриваются базовые понятия языка. По прочтении ее вы сможете свободно разбираться в понятиях объектно-ориентированного программирования и их реализации на языке Java, создавать свои объектноориентированные программы, рассчитанные на консольный ввод/вывод.

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

В главе 2 вводятся основные понятия объектно-ориентированного программирования: объект и метод, абстракция, инкапсуляция, наследование, полиморфизм, контракты методов и их поручения друг другу. Эта глава призвана привить вам "объектный" взгляд на реализацию сложных проектов, после ее прочтения вы научитесь описывать проект как совокупность взаимодействующих объектов. Здесь же предлагается реализация всех этих понятий на языке Java. Тут вы, наконец, поймете, что же такое эти объекты и как они взаимодействуют.

В главе 3 определяются пакеты классов и интерфейсы, ограничения доступа к классам и методам, на примерах подробно разбираются правила их использования. Объясняется структура встроенной библиотеки классов Java API.

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

Глава 4 прослеживает иерархию стандартных классов и интерфейсов Java, на этом примере показано, как в профессиональных системах программирования реализуются концепции абстракции, инкапсуляции и наследования.

В главе 5 подробно излагаются приемы работы со строками символов, которые, как и всё в Java, являются объектами, приводятся примеры синтаксического анализа текстов, обсуждаются вопросы русификации.

В главе 6 показано, как в языке Java реализованы коллекции, позволяющие работать с совокупностями объектов и создавать сложные структуры данных.

Глава 7 описывает различные классы-утилиты, полезные во многих ситуациях при работе с датами, случайными числами, словарями и другими необходимыми элементами программ.

В части III объясняется создание графического интерфейса пользователя (ГИП) с помощью стандартной библиотеки классов AWT (Abstract Window Toolkit) с компонентами Swing и даны многочисленные примеры построения интерфейса. Подробно разбирается принятый в Java метод обработки событий, основанный на идее делегирования. Здесь же появляются апплеты как программы Java, работающие в окне браузера. Подробно обсуждается система безопасности выполнения апплетов. После прочтения третьей части вы сможете создавать с помощью Swing полноценные приложения под графические платформы MS Windows, X Window System и др., а также программировать браузеры.

Глава 8 описывает иерархию классов библиотеки AWT, которую необходимо четко себе представлять для создания удобного интерфейса. Здесь же рассматривается библиотека графических компонентов Swing, ставшая стандартной наряду с AWT.

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

В главе 10 обсуждается понятие графического компонента, рассматриваются готовые компоненты AWT и их применение, а также создание собственных компонентов AWT.

В главе 11 рассматриваются графические компоненты общего назначения, относящиеся к библиотеке Swing.

В главе 12 рассматриваются текстовые графические компоненты библиотеки Swing.

В главе 13 подробно обсуждаются возможности создания таблиц средствами Swing.

В главе 14 показано, какие способы размещения компонентов в графическом контейнере имеются в AWT и Swing и как их применять в разных ситуациях.

В главе 15 вводятся способы реагирования компонентов на сигналы от клавиатуры и мыши, а именно модель делегирования, принятая в Java.

В главе 16 описывается создание рамок, окружающих графические компоненты Swing.

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

В главе 18, наконец-то, появляются апплеты — Java-программы, предназначенные для выполнения в окне браузера, и обсуждаются особенности их создания.

В главе 19 собраны сведения о библиотеке Swing, не вошедшие в предыдущие главы.

В главе 20 рассматривается работа с изображениями и звуком средствами AWT.

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

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

Глава 22 рассказывает об интересном свойстве языка Java — способности создавать подпроцессы (threads) и управлять их взаимодействием прямо из программы.

В главе 23 обсуждается концепция потока данных и ее реализация в Java для организации ввода/вывода на внешние устройства.

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

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

В главе 25 описываются те аспекты технологии Java, которые необходимы для Web-программирования: архиватор JAR, компоненты JavaBeans, драйверы соединения с базами данных JDBC.

Глава 26 посвящена основному средству программирования серверов — сервлетам.

В главе 27 разбираются страницы JSP, значительно облегчающие оформление ответов на запросы Web-клиентов.

Наконец, в главе 28 рассматривается вездесущая технология XML и инструменты Java для обработки документов XML.

Выполнение Java-программы

Как вы знаете, программа, написанная на одном из языков высокого уровня, к которым относится и язык Java, так называемый исходный модуль ("исходник", или "сырец" на жаргоне от английского source), не может быть сразу же выполнена. Ее сначала надо скомпилировать, т. е. перевести в последовательность машинных команд — объектный модуль. Но и он, как правило, не может быть сразу же выполнен: объектный модуль надо еще скомпоновать с библиотеками использованных в модуле функций и разрешить перекрестные ссылки между секциями объектного модуля, получив в результате загрузочный модуль — полностью готовую к выполнению программу.

Исходный модуль, написанный на Java, не может избежать этих процедур, но здесь проявляется главная особенность технологии Java — программа компилируется сразу в машинные команды, но не команды какого-то конкретного процессора, а в команды так называемой виртуальной машины Java (Java Virtual Machine, JVM). Виртуальная машина Java — это совокупность команд вместе с системой их выполнения. Для специалистов скажем, что виртуальная машина Java полностью стековая, так что не требуется сложная адресация ячеек памяти и большое количество регистров. Поэтому команды JVM короткие, большинство из них имеет длину 1 байт, отчего команды JVM называют байт-кодами (bytecodes), хотя имеются команды длиной 2 и 3 байта. Согласно статистическим исследованиям средняя длина команды составляет 1,8 байта. Полное описание команд и всей архитектуры JVM содержится в спецификации виртуальной машины Java (Virtual Machine Specification, VMS). Ознакомьтесь с этой спецификацией, если вы хотите в точности узнать, как работает виртуальная машина Java.

Другая особенность Java — все стандартные функции, вызываемые в программе, подключаются к ней только на этапе выполнения, а не включаются в байт-коды. Как говорят специалисты, происходит динамическая компоновка (dynamic binding). Это тоже сильно уменьшает объем скомпилированной программы.

Итак, на первом этапе программа, написанная на языке Java, переводится компилятором в байт-коды. Эта компиляция не зависит от типа какого-либо конкретного процессора и архитектуры конкретного компьютера. Она может быть выполнена один раз сразу же после написания программы, программу не надо перекомпилировать под разные платформы. Байт-коды записываются в одном или нескольких файлах, могут храниться во внешней памяти или передаваться по сети. Это особенно удобно благодаря небольшому размеру файлов с байт-кодами. Затем полученные в результате компиляции байткоды можно выполнять на любом компьютере, имеющем систему, реализующую JVM. При этом не важен ни тип процессора, ни архитектура компьютера. Так реализуется принцип Java "Write once, run anywhere" — "Написано однажды, выполняется где угодно".

Интерпретация байт-кодов и динамическая компоновка значительно замедляют выполнение программ. Это не имеет значения в тех ситуациях, когда байт-коды передаются по сети, сеть все равно медленнее любой интерпретации, но в других ситуациях требуется мощный и быстрый компьютер. Поэтому постоянно идет усовершенствование интерпретаторов в сторону увеличения скорости интерпретации. Разработаны JIT-компиляторы (Just-In-Time), запоминающие уже интерпретированные участки кода в машинных командах процессора и просто выполняющие эти участки при повторном обращении, например в циклах. Это значительно увеличивает скорость повторяющихся вычислений. Корпорация Sun Microsystems разработала целую технологию HotSpot и включает ее в свою виртуальную машину Java. Но, конечно, наибольшую скорость может дать только специализированный процессор.

Компания Sun Microsystems выпустила микропроцессоры picoJava, работающие на системе команд JVM. Есть Java-процессоры и других фирм. Эти процессоры непосредственно выполняют байт-коды. Но при выполнении программ Java на других процессорах требуется еще интерпретация команд JVM в команды конкретного процессора, а значит, нужна программа-интерпретатор, причем для каждого типа процессоров и для каждой архитектуры компьютера следует написать свой интерпретатор.

Эта задача уже решена практически для всех компьютерных платформ. На них реализованы виртуальные машины Java, а для наиболее распространенных платформ имеется несколько реализаций JVM разных фирм. Все больше операционных систем и систем управления базами данных включают реализацию JVM в свое ядро. Создана и специальная операционная система JavaOS, применяемая в электронных устройствах. В большинство браузеров встроена виртуальная машина Java для выполнения апплетов. Операционная система Andriod содержит виртуальную машину Java, называемую Dalvik, которая работает на ядре Linux.

Программы, приведенные в этой книге, выполнялись в операционных средах программирования MS Windows 2000/XP/Server 2003, Red Hat Linux, Fedora Core Linux, SUSE Linux без перекомпиляции. Это видно по рисункам, приведенным во многих главах книги. Они "сняты" с экранов графических оболочек разных операционных систем.

Внимательный читатель уже заметил, что кроме реализации JVM для выполнения байткодов на компьютере еще нужно иметь набор функций, вызываемых из байт-кодов и динамически компонующихся с байт-кодами. Этот набор оформляется в виде библиотеки классов Java, состоящей из одного или нескольких пакетов. Каждая функция может быть записана байт-кодами, но, поскольку она будет храниться на конкретном компьютере, ее можно записать прямо в системе команд этого компьютера, избегнув тем самым интерпретации байт-кодов. Такие функции, написанные чаще всего на языке C/C++ и скомпилированные под определенную платформу, называют "родными" методами (native methods). Применение "родных" методов ускоряет выполнение программы.

Корпорация Oracle, купившая фирму Sun Microsystems — создателя технологии Java, — бесплатно распространяет набор необходимых программных инструментов для полного цикла работы с этим языком программирования: компиляции, интерпретации, отладки, включающий и богатую библиотеку классов. Называется этот набор JDK (Java Development Kit). Он весь содержится в одном файле. Есть наборы инструментальных программ и других фирм. Например, большой популярностью пользуется JDK корпорации IBM.

Что такое JDK?

Набор программ и классов JDK содержит:

□ компилятор из исходного текста в байт-коды j avac;

□ интерпретатор j ava, содержащий реализацию JVM;

□ облегченный интерпретатор j re (в последних версиях отсутствует);

□ программу просмотра апплетов appietviewer, заменяющую браузер;

□ отладчик j db;

□ дизассемблер javap;

□ программу архивации и сжатия jar;

□ программу сбора и генерирования документации j avadoc;

□ программу генерации заголовочных файлов языка С для создания "родных" методов

j avah;

□ программу генерации электронных ключейkeytool;

□ программу native2ascii, преобразующую бинарные файлы в текстовые;

□ программы rmic и rmiregistry для работы с удаленными объектами;

□ программу seriaiver, определяющую номер версии класса;

□ библиотеки и заголовочные файлы "родных" методов;

□ библиотеку классов Java API (Application Programming Interface).

В прежние версии JDK включались и отладочные варианты исполнимых программ:

j avac g, j ava g Рё С‚. Рґ.

Компания Sun Microsystems активно развивала и обновляла JDK, почти каждый год выходили новые версии.

В 1996 г. была выпущена первая версия — JDK 1.0, которая модифицировалась до версии с номером 1.0.2. В этой версии библиотека классов Java API содержала 8 пакетов. Весь набор JDK 1.0.2 поставлялся в упакованном виде в одном файле размером около 5 Мбайт, а после распаковки занимал на диске около 8 Мбайт.

В 1997 г. появилась версия JDK 1.1, последняя ее модификация, 1.1.8, выпущена в 1998 г. В этой версии было 23 пакета классов, занимала она 8,5 Мбайт в упакованном виде и около 30 Мбайт — в распакованном.

В первых версиях JDK все пакеты библиотеки Java API были упакованы в один архивный файл classes.zip и вызывались непосредственно из этого архива, его не нужно было распаковывать.

Затем набор инструментальных средств JDK был сильно переработан.

Версия JDK 1.2 вышла в декабре 1998 г. и содержала уже 57 пакетов классов. В архивном виде это файл размером почти 20 Мбайт и еще отдельный файл размером более 17 Мбайт с упакованной документацией. Полная версия располагается на 130 Мбайт дискового пространства, из них около 80 Мбайт занимает документация.

Начиная с этой версии, все продукты технологии Java собственного производства компания Sun стала называть Java 2 Platform, Standard Edition, сокращенно J2SE, а в литературе утвердилось название Java 2. Кроме 57 пакетов классов, обязательных на любой платформе и получивших название Core API, в Java 2 JDK 1.2 входят еще дополнительные пакеты классов, называемые Standard Extension API.

В версии J2SE JDK 1.5.0, вышедшей в конце 2004 г., было уже под сотню пакетов, составляющих Core API (Application Programming Interface). В упакованном виде — это файл размером около 46 Мбайт и необязательный файл с упакованной документацией такого же размера. В это же время произошло очередное переименование технологии

Java: из версии убрали первую цифру и стали писать Java 2 Platform, Standard Edition

5.0, сокращенно J2SE 5.0 и JDK 5.0, хотя во внутрифирменной документации сохраняется название JDK 1.5.0.

Последнее обновление J2SE 5.0, JDK 1.5.0_22, было выпущено 3 ноября 2009 года.

В шестой версии, вышедшей в начале 2007 г., из названия технологии убрали цифру 2 и стали писать Java Platform, Standard Edition 6, сокращенно — Java SE 6 и JDK 6. Впрочем, во внутрифирменной документации остается прежнее обозначение, например последнее на момент написания книги обновление обозначается JDK 1.6.0_26.

Летом 2011 года появилась седьмая версия Java SE 7 и распространяется JDK 1.7.0, описанию которой посвящена эта книга.

Java SE JDK создается для каждой платформы: MS Windows, Solaris, Linux, отдельно, а документация написана на языке HTML и одинакова на всех платформах. Поэтому она записана в отдельном файле. Например, для MS Windows файл с Java SE JDK 1.7.0 называется jdk-7-windows-i586.exe с добавлением номера обновления, а файл с документацией называется jdk-7-fcs-bin-b147-apidocs-27_jun_2011.zip.

Эти файлы можно совершенно свободно скачать со страницы technetwork/java/javase/downloads/index.html.

Для создания Web-программ в части V книги вам потребуется еще набор пакетов Java Platform, Enterprise Edition (Java EE). Так же как Java SE, он поставляется одним самораспаковывающимся архивом, в который входит SDK (Software Development Kit), Java EE API и сервер приложений. Архив можно скопировать с того же сайта. Набор Java EE SDK — это дополнение к Java SE и поэтому устанавливается после Java SE JDK. Впрочем, на том же сайте есть полная версия архива, содержащая в себе и Java EE SDK, и Java SE JDK.

Java EE входит в состав серверов приложений, поэтому если вы установили JBoss, GlassFish или другой сервер приложений, то у вас уже есть набор классов Java EE.

Кроме JDK компания Oracle отдельно распространяет еще и набор JRE (Java Runtime Environment).

Что такое JRE?

Набор программ и пакетов классов JRE содержит все необходимое для выполнения байт-кодов, в том числе интерпретатор java (в прежних версиях — облегченный интерпретатор jre) и библиотеку классов. Это часть JDK, не содержащая компиляторы, отладчики и другие средства разработки. Именно Oracle JRE или его аналог, созданный другими фирмами, присутствует в тех браузерах, которые умеют выполнять программы на Java, в операционных системах и системах управления базами данных.

Хотя JRE входит в состав JDK, корпорация Oracle распространяет этот набор и отдельным файлом.

Как установить JDK?

Напомню, что набор JDK упаковывается в самораспаковывающийся архив. Раздобыв каким-либо образом этот архив: скачав из Интернета, с сайта technetwork/java/javase/downloads/index.html или какого-то другого адреса, вам остается только запустить файл с архивом на выполнение. Откроется окно установки, в котором среди всего прочего вам будет предложено выбрать каталог (directory) установки, например, /usr/java/jdk1.7.0. Каталог и его название можно поменять, место и название установки не имеют значения.

После установки вы получите каталог с названием, например, jdk1.7.0, а в нем подкаталоги:

□ bin с исполнимыми файлами;

□ db с небольшой базой данных;

□ demo с примерами программ, присутствует не во всех версиях JDK;

□ docs с документацией, если вы ее установили в этот каталог;

□ include с заголовочными файлами "родных" методов;

□ jre с набором JRE;

□ lib с библиотеками классов и файлами свойств;

□ sample с примерами программ, присутствует не во всех версиях JDK;

□ src с исходными текстами программ JDK, получаемый после распаковки файла src.zip.

Да-да! Набор JDK содержит исходные тексты большинства своих программ, написанные на Java. Это очень удобно. Вы всегда можете в точности узнать, как работает тот или иной метод обработки информации из JDK, посмотрев исходный код данного метода. Это очень полезно и для изучения Java на "живых", работающих примерах.

Предупреждение

Не следует распаковывать zip- и jar-архивы, кроме архива исходных текстов src.zip.

После установки надо дополнить значение системной переменной path, добавив в нее путь к каталогу bin, например /usr/java/jdk1.7.0/bin. Некоторые программы, использующие Java, требуют определить и специальную переменную окружения java_home, содержащую путь к каталогу установки JDK, например /usr/j ava/j dk1.7.0.

Проверить правильность установки Java, а заодно и посмотреть ее версию можно, набрав в командной строке

java -version

Как использовать JDK?

Несмотря на то что набор JDK предназначен для создания программ, работающих в графических средах, таких как MS Windows или X Window System, он ориентирован на выполнение из командной строки окна Command Prompt в MS Windows. В системах UNIX, Linux, BSD можно работать и в текстовом режиме, и в окне Xterm.

Написать программу на Java можно в любом текстовом редакторе, например Notepad, WordPad в MS Windows, редакторах vi, emacs в UNIX. Надо только сохранить файл в текстовом, а не графическом формате и дать ему расширение java. Пусть, для примера, именем файла будет MyProgramjava, а сам файл сохранен в текущем каталоге.

После создания этого файла из командной строки вызывается компилятор javac и ему передается исходный файл как параметр:

javac MyProgram.java

Компилятор создает в том же каталоге по одному файлу на каждый класс, описанный в программе, называя каждый файл именем класса с расширением class. Допустим, в нашем примере имеется только один класс, названный MyProgram, тогда получаем файл с именем MyProgram.class, содержащий байт-коды.

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

Далее из командной строки вызывается интерпретатор байт-кодов java, которому передается файл с байт-кодами, причем его имя записывается без расширения (смысл этого вы узнаете позднее):

java MyProgram

На экране появится вывод результатов работы программы или сообщения об ошибках времени выполнения.

Работая в графических оболочках операционных систем, мы привыкли вызывать программу на исполнение двойным щелчком мыши по имени исполнимого файла (в MS Windows у имени исполнимого файла стандартное расширение exe) или щелчком по его ярлыку. В технологии Java тоже есть такая возможность. Надо только упаковать class-файлы с байт-кодами в архив специального вида JAR. Как это сделать, рассказано в главе 25. При установке JDK на MS Windows для файлов с расширением jar автоматически создается ассоциация с интерпретатором java, который будет вызван при двойном щелчке мыши на jar-архиве.

Кроме того, можно написать командный файл (файл с расширением bat в MS Windows или Shell-файл командной оболочки в UNIX), записав в нем строку вызова интерпретатора java со всеми нужными параметрами.

Еще один способ запустить Java-программу средствами операционной системы — написать загрузчик (launcher) виртуальной машины Java. Так и сделано в стандартной поставке JDK: исполнимый файл java.exe содержит программу, написанную на языке С, которая запускает виртуальную машину Java и передает ей на исполнение класс Java с методом main (). Исходный текст этой программы есть среди исходных текстов Java в каталоге src/launcher. Им можно воспользоваться для написания своего загрузчика. Есть много программ, облегчающих написание загрузчика, например программа Java Launcher фирмы SyncEdit, , или Advanced Installer for Java фирмы Caphyon, .

Наконец, существуют компиляторы исходного текста, написанного на языке Java, непосредственно в исполнимый файл операционной системы, с которой вы работаете. Их общее название AOT (Ahead-Of-Time) compiler. Например, у знаменитого компилятора GCC (GNU Compiler Collection) есть вход с именем GCJ, с помощью которого можно сделать компиляцию как в байт-коды, так и в исполнимый файл, а также перекомпиляцию байт-кодов в исполнимый файл.

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

Интегрированные среды Java

Сразу же после создания Java, уже в 1996 г., появились интегрированные среды разработки программ IDE (Integrated Development Environment) для Java, и их число все время возрастает. Некоторые из них, такие как Eclipse, IntelliJ IDEA, NetBeans, являются просто интегрированными оболочками над JDK, вызывающими из одного окна текстовый редактор, компилятор и интерпретатор. Эти интегрированные среды требуют предварительной установки JDK. Впрочем, Eclipse содержит собственный компилятор.

Другие интегрированные среды содержат JDK в себе или имеют собственный компилятор, например JBuilder фирмы Embarcadero или IBM Rational Application Developer. Их можно устанавливать, не имея под руками JDK. Надо заметить, что перечисленные продукты сами написаны полностью на Java.

Большинство интегрированных сред являются средствами визуального программирования и позволяют быстро создавать пользовательский интерфейс, т. е. относятся к классу средств RAD (Rapid Application Development).

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

К технологии Java подключились и разработчики CASE-средств. Например, популярный во всем мире продукт Rational Rose может сгенерировать код на Java.

Для изучения Java, пожалуй, удобнее всего интегрированная среда NetBeans IDE, которую можно свободно скопировать с сайта . Она содержит много примеров, статей и учебников по различным разделам Java.

Особая позиция Microsoft

Вы уже, наверное, почувствовали смутное беспокойство, не встречая название этой корпорации. Дело в том, что, имея свою операционную систему, огромное число приложений к ней и богатейшую библиотеку классов, Microsoft не имела нужды в Java. Но и пройти мимо технологии, распространившейся всюду, компания Microsoft не могла и создала свой компилятор Java, а также визуальное средство разработки, входящее в Visual Studio. Данный компилятор включает в байт-коды вызовы объектов ActiveX. Следовательно, выполнять эти байт-коды можно только на компьютерах, имеющих доступ к ActiveX. Эта "нечистая" Java резко ограничивает круг применения байт-кодов, созданных компилятором корпорации Microsoft. В результате судебных разбирательств с Sun Microsystems компания Microsoft назвала свой продукт Visual J++. Виртуальная машина Java корпорации Microsoft умеет выполнять байт-коды, созданные "чистым" компилятором, но не всякий интерпретатор выполнит байт-коды, написанные с помощью Visual J++. Этот продукт вошел в состав Visual Studio .NET 2005 под названием

J# (J sharp), но он генерирует не байт-коды JVM, а код .NET Framework CLR. Язык J# не получил распространения и был исключен из дальнейших версий Visual Studio .NET.

Чтобы прекратить появление несовместимых версий Java, корпорация Sun разработала концепцию "чистой" Java, назвав ее Pure Java, и систему проверочных тестов на "чистоту" байт-кодов. Появились байт-коды, успешно прошедшие тесты, и средства разработки, выдающие "чистый" код и помеченные как "100 % Pure Java”.

Кроме того, компания Sun распространяет пакет программ Java Plug-in, который можно подключить к браузеру, заменив тем самым встроенный в браузер JRE на "родной".

Java в Интернете

Разработанная для применения в компьютерных сетях, Java просто не могла не найти отражения на сайтах Интернета. Действительно, масса сайтов полностью посвящена технологии Java или содержит информацию о ней. Одна только компания Oracle имеет несколько сайтов с информацией о Java:

□ — основной сайт Java, отсюда можно скопировать JDK;

□ — форумы для разработчиков Java;

□ http :// — сайт для разработчиков, знакомящихся с технологией Java.

На сайте корпорации IBM есть большой раздел , где можно найти очень много полезного для программиста.

Корпорация Microsoft содержит информацию о Java на сайте .

Существует множество специализированных сайтов:

□ — форумы для разработчиков, в том числе Java;

□ — большой сборник статей по Java;

□ — советы разработчикам Java и готовые программы;

□ — Java Review Service;

□ — новостной сайт c русскими статьями, посвященный Java;

□ — еще один новостной сайт;

□ — новости, статьи и советы по Java;

□ — дружественный сайт и форум для разработчиков Java;

□ — электронный журнал;

□ — сборник программ и статей;

□ — советы специалистов;

□ — новинки технологии Java;

□ — вопросы создания серверных Java-приложений;

□ — большой сборник статей, апплетов и других программ;

□ — здесь обсуждаются вопросы безопасности;

□ — здесь обсуждаются вопросы написания сервлетов;

□ — общая информация о Java и не только о Java. Персональные сайты:

□ / — сайт Брюса Эккеля, автора популярных книг и статей;

□ — сайт Девида Рейли, автора многих статей и книг о Java.

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

Литература по Java

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

Полное и строгое описание языка изложено в книге James Gosling, Bill Joy, Guy Steele, Gilad Bracha, "The Java Language Specification, Third Edition". В электронном виде она находится по адресу , занимает в упакованном виде около 400 Кбайт.

Столь же полное и строгое описание виртуальной машины Java изложено в книге Tim Lindholm, Frank Yellin, "The Java Virtual Machine Specification, Second Edition". В электронном виде она находится по адресу .

Здесь же необходимо отметить книгу "отца" технологии Java Джеймса Гослинга, написанную вместе с Кеном Арнольдом и Девидом Холмсом. Имеется русский перевод: Арнольд К., Гослинг Дж., Холмс Д. Язык программирования Java. 3-е изд.: Пер. с англ. — М.: Издательский дом "Вильямс", 2001. — 624 с.: ил.

Официальным учебником хорошего стиля программирования на языке Java стала книга Блоха Д., Java. Эффективное программирование. Пер. с англ. — М.: Лори, 2008. — 223 с. На английском языке вышло второе издание этой книги, значительно расширенное и обновленное.

Компания Oracle содержит на своем сайте постоянно обновляемый электронный учебник Java Tutorial, размером уже в несколько десятков мегабайт: . oracle.com/javase/tutorial/ /. Время от времени появляется его печатное издание: Mary Campione, Kathy Walrath, "The Java Tutorial, Second Edition: Object-Oriented Programming for the Internet".

Полное описание Java API содержится в документации, но есть печатное издание James Gosling, Frank Yellin and the Java Team, "The Java Application Programming Interface", Volume 1: Core Packages; Volume 2: Window Toolkit and Applets.

Благодарности

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

Отдельная благодарность Игорю Шишигину, предложившему ее издать и так быстро оформившему договор, что автор не успел передумать; моим студентам с их бесконечными вопросами; своим "сплюснутым" друзьям, убежденным в том, что "Жаба — это отстой", и сыну, Камилю, для которого эта книга, собственно, и писалась.

 

ЧАСТЬ I

Базовые конструкции языка Java

Глава 1. Встроенные типы данных, операции над ними
Глава 2. Объектно-ориентированное программирование в Java
Глава 3. Пакеты, интерфейсы и перечисления

ГЛАВА 1

 

Встроенные типы данных, операции над ними

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

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

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

Первая программа на Java

По давней традиции, восходящей к языку С, учебники по языкам программирования начинаются с программы "Hello, World!". Не будем нарушать эту традицию. В листинге 1.1 приведена подобная программа. Она написана в самом простом виде, какой только возможен на языке Java.

Листинг 1.1. Первая программа на языке Java

class HelloWorld{

public static void main(String[] args){

System.out.println("Hello, XXI Century World!");

}

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

□ Всякая программа, написанная на языке Java, представляет собой один или несколько классов, в этом простейшем примере только один класс (class).

□ Начало класса отмечается служебным словом class, за которым следует имя класса, выбираемое произвольно, в данном случае это имя HelloWorld. Все, что содержится в классе, записывается в фигурных скобках и составляет тело класса (class body).

□ Все действия в программе производятся с помощью методов обработки информации, коротко говорят просто метод (method). Методы используются в объектноориентированных языках вместо функций, применяемых в процедурных языках.

□ Методы различаются по именам и параметрам. Один из методов обязательно должен называться main, с него начинается выполнение программы. В нашей простейшей программе только один метод, а значит, имя его main.

□ Как и положено функции, метод всегда выдает в результате (чаще говорят возвращает (returns)) только одно значение, тип которого обязательно указывается перед именем метода. Метод может и не возвращать никакого значения, играя роль процедуры. Так и есть в нашем случае. Тогда вместо типа возвращаемого значения записывается слово void, как это и сделано в примере.

□ После имени метода в скобках через запятую перечисляются параметры (parameters) метода. Для каждого параметра указывается его тип и, через пробел, имя. У метода main () только один параметр, его тип — массив, состоящий из строк символов. Строка символов — это встроенный в Java API тип String, а квадратные скобки — признак массива. Имя параметра может быть произвольным, в примере выбрано имя args.

□ Перед типом возвращаемого методом значения могут быть записаны модификаторы (modifiers). В примере их два: слово public означает, что этот метод доступен отовсюду; слово static обеспечивает возможность вызова метода main() в самом начале выполнения программы. Модификаторы, вообще говоря, необязательны, но для метода main() они необходимы.

Замечание

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

□ Все, что содержит метод, тело метода (method body), записывается в фигурных скобках.

Единственное действие, которое выполняет метод main () в нашем примере, заключается в вызове другого метода со сложным именем System.out.println и передаче ему на обработку одного аргумента — текстовой константы "Hello, xxi Century World!". Текстовые константы записываются в кавычках, которые являются только ограничителями и не входят в текст.

Составное имя System.out.println означает, что в классе System, входящем в Java API, определяется переменная с именем out, содержащая экземпляр одного из классов Java API, класса PrintStream, в котором есть метод println (). Все это станет ясно позднее, а пока просто будем писать это длинное имя.

Действие метода println() заключается в выводе заданного ему аргумента в выходной поток, связанный обычно с выводом на экран текстового терминала, в окно MS-DOS Prompt, Command Prompt или Xterm в зависимости от вашей системы. После вывода курсор переходит на начало следующей строки экрана, на что указывает окончание ln, само слово println — сокращение слов print line. В составе Java API есть и метод print (), оставляющий курсор в конце выведенной строки. Разумеется, это прямое влияние языка Pascal.

Сильное влияние языка С привело к появлению в Java SE 5 (Java Standard Edition) метода System.out.printf(), очень похожего на одноименную функцию языка С. Мы подробно опишем этот метод в главе 23, но желающие могут ознакомиться с ним прямо сейчас.

Сделаем сразу важное замечание. Язык Java различает строчные и прописные буквы, имена main, Main, main различны с "точки зрения" компилятора Java. В примере важно писать String, System с заглавной буквы, а main — со строчной. Но внутри текстовой константы неважно, писать Century или century, компилятор вообще не "смотрит" на текст в кавычках, разница будет видна только на экране.

Замечание

Язык Java различает прописные и строчные буквы.

В именах нельзя оставлять пробелы. Свои имена можно записывать как угодно, можно было бы дать классу имя helloworld или helloWorld, но между Java-программистами заключено соглашение, называемое "Code Conventions for the Java Programming Language", хранящееся по адресу . Вот несколько пунктов этого соглашения:

□ имена классов начинаются с прописной (заглавной) буквы; если имя содержит несколько слов, то каждое слово начинается с прописной буквы;

□ имена методов и переменных начинаются со строчной буквы; если имя содержит несколько слов, то каждое следующее слово начинается с прописной буквы;

□ имена констант записываются полностью прописными буквами; если имя состоит из нескольких слов, то между ними ставится знак подчеркивания.

Конечно, эти правила необязательны, хотя они и входят в JLS, п. 6.8, но сильно облегчают понимание кода и придают программе характерный для Java стиль.

Стиль определяют не только имена, но и размещение текста программы по строкам, например расположение фигурных скобок: оставлять ли открывающую фигурную скобку в конце строки с заголовком класса или метода или переносить на следующую строку? Почему-то этот пустячный вопрос вызывает ожесточенные споры, некоторые средства разработки даже предлагают выбрать определенный стиль расстановки фигурных скобок. Многие фирмы устанавливают свой внутрифирменный стиль. В книге мы постараемся следовать стилю "Code Conventions" и в том, что касается разбиения текста программы на строки (компилятор же рассматривает всю программу как одну длинную строку, для него программа — это просто последовательность символов), и в том, что касается отступов (indent) в тексте.

Итак, программа написана в каком-либо текстовом редакторе, например в Блокноте (Notepad), emacs или vi. Теперь ее надо сохранить в файле в текстовом, но не в графическом формате. Имя файла должно в точности совпадать с именем класса, содержащего метод main (). Данное правило очень желательно выполнять. При этом система исполнения Java будет быстро находить метод main () для начала работы, просто отыскивая класс, совпадающий с именем файла. Расширение имени файла должно быть java.

Совет

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

В нашем примере сохраним программу в файле с именем HelloWorldjava в текущем каталоге. Затем вызовем компилятор, передавая ему имя файла в качестве аргумента:

javac HelloWorld.java

Компилятор создаст файл с байт-кодами, даст ему имя HelloWorld.class и запишет этот файл в текущий каталог.

Осталось вызвать интерпретатор байт-кодов, передав ему в качестве аргумента имя класса (а не файла!):

java HelloWorld

На экране появится строка:

Hello, XXI Century World!

Замечание

Не указывайте расширение class при вызове интерпретатора.

На рис. 1.1 показано, как все это выглядит в окне Command Prompt операционной системы MS Windows 2003.

Р РёСЃ. 1.1. РћРєРЅРѕ Command Prompt

При работе в какой-либо интегрированной среде, например Eclipse или NetBeans, все эти действия вызываются выбором соответствующих пунктов меню или "горячими" клавишами — единых правил здесь нет.

Комментарии

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

Комментарии вводятся таким образом:

□ за двумя наклонными чертами, написанными подряд //, без пробела между ними, начинается комментарий, продолжающийся до конца строки;

□ за наклонной чертой и звездочкой /* начинается комментарий, который может занимать несколько строк, до звездочки и наклонной черты */ (без пробелов между этими знаками);

□ за наклонной чертой и двумя звездочками /** начинается комментарий, который может занимать несколько строк, до звездочки и наклонной черты */. Из таких комментариев формируется документация.

Комментарии очень удобны для чтения и понимания кода, они превращают программу в документ, описывающий ее действия. Программу с хорошими комментариями называют самодокументированной. Поэтому в Java и введены комментарии третьего типа, а в состав JDK включена утилита — программа j avadoc, извлекающая эти комментарии в отдельные файлы формата HTML и создающая гиперссылки между ними. В такой комментарий кроме собственно комментария можно вставить указания программе javadoc, которые начинаются с символа @.

Именно так создается документация к JDK.

Добавим комментарии к нашему примеру (листинг 1.2).

Листинг 1.2. Первая программа с комментариями

/**

* Разъяснение содержания и особенностей программы...

* @author Имя Фамилия (автора)

* @version 1.0 (это версия программы)

*/

class HelloWorld{ // HelloWorld — это только имя // Следующий метод начинает выполнение программы

public static void main(String[] args){ // args не используются /* Следующий метод просто выводит свой аргумент * на экран дисплея */

System.out.println("Hello, XXI РЎentury World!");

// Следующий вызов закомментирован,

// метод не будет выполняться

// System.out.println("Farewell, XX РЎentury!");

}

}

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

Аннотации

Обратите внимание на комментарий, приведенный в начале листинга 1.2. В него вставлены указания-теги @author и @version утилите javadoc. Просматривая текст этого комментария и встретив какой-либо из тегов, утилита javadoc выполнит предписанные тегом действия. Например, тег @see предписывает сформировать гиперссылку на другой документ HTML, а тег @deprecated, записанный в комментарий перед методом, вызовет пометку этого метода в документации как устаревшего.

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

Использование таких утилит стало общепризнанной практикой, и, начиная с пятой версии Java SE, было решено ввести прямо в компилятор возможность обрабатывать теги, которые получили название аннотаций. Аннотации записываются не внутри комментариев вида /**...*/, а непосредственно в том месте, где они нужны. Например, после того как мы запишем непосредственно перед заголовком какого-либо метода аннотацию @Deprecated, компилятор будет выводить на консоль предупреждение о том, что этот метод устарел и следует воспользоваться другим методом. Обычно замена указывается тут же, в этом же комментарии.

Несколько аннотаций, количество которых увеличивается с каждой новой версией JDK, объявлено прямо в компиляторе. Ими можно пользоваться без дополнительных усилий. Мы будем вводить их по мере надобности. Кроме них разработчик может объявить и использовать в своем приложении свои аннотации. Как это делается, рассказано в главе 3.

Константы

В языке Java можно записывать константы различных типов в разных видах. Форма записи констант почти полностью заимствована из языка С. Перечислим все разновидности констант.

Целые

Целые константы можно записывать в четырех системах счисления:

□ в привычной для нас десятичной форме: +5, -7, 12345678;

□ в двоичной форме, начиная с нуля и латинской буквы b или b: 0b1001, 0B11011;

□ в восьмеричной форме, начиная с нуля: 027, -0326, 0777 (в записи таких констант недопустимы цифры 8 и 9);

ЗАмЕчАниЕ

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

□ в шестнадцатеричной форме, начиная с нуля и латинской буквы x или x: 0xff0a, 0xFC2D, 0X45a8, 0X77FF (здесь строчные и прописные буквы не различаются).

Для улучшения читаемости группы цифр в числе можно разделять знаком подчеркивания: 1_001_234, 0xFC_2D.

Целые константы хранятся в оперативной памяти в формате типа int (см. далее).

В конце целой константы можно записать латинскую букву "L" (прописную L или строчную l), тогда константа будет сохраняться в длинном формате типа long (см. далее): +25L, -037l, 0xffL, 0XDFDFl.

Совет

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

Действительные

Действительные константы записываются только в десятичной системе счисления в двух формах:

□ с фиксированной точкой: 37.25, -128.678967, +27.035;

□ с плавающей точкой: 2.5e34, -0.345e-25, 37.2E+4; можно писать строчную или прописную латинскую букву E; пробелы и скобки недопустимы.

В конце действительной константы можно поставить букву F или f, тогда константа будет сохраняться в оперативной памяти в формате типа float (см. далее): 3.5f, -4 5.67F, 4.7e-5f. Можно приписать и букву D (или d): 0.04 5D, -456.77889d, означающую тип double, но это излишне, поскольку действительные константы и так хранятся в формате типа double.

Символы

Одиночные символы записываются в апострофах, чтобы отличить их от имен переменных. Для записи символов используются следующие формы:

□ печатные символы, записанные на клавиатуре, просто записываются в апострофах (одинарных кавычках): 'a', 'N', '?';

□ управляющие и специальные символы записываются в апострофах с обратной наклонной чертой, чтобы отличить их от обычных символов:

• '\n' — символ перевода строки LF (Line Feed) с кодом ASCII 10;

• '\r' — символ возврата каретки CR (Carriage Return) с кодом 13;

• '\f' — символ перевода страницы FF (Form Feed) с кодом 12;

• ' \b' — символ возврата на шаг BS (Backspace) с кодом 8;

• '\t' — символ горизонтальной табуляции HT (Horizontal Tabulation) с кодом 9;

• '\\' — обратная наклонная черта;

• 'Vм — кавычка;

• '\'' — апостроф;

□ код любого символа с десятичной кодировкой от 0 до 255 можно задать, записав его не более чем тремя цифрами в восьмеричной системе счисления в апострофах после обратной наклонной черты: '\123' — буква S, '\346' — буква ж в кодировке CP1251. Нет смысла использовать эту форму записи для печатных и управляющих символов, перечисленных в предыдущем пункте, поскольку компилятор сразу же переведет восьмеричную запись в указанную ранее форму. Наибольший восьмеричный код ' \377' — десятичное число 255;

□ код любого символа в кодировке Unicode набирается в апострофах после обратной наклонной черты и латинской буквы u четырьмя шестнадцатеричными цифрами:

'\u0053' — буква S, ' \u0416' — буква ж.

Символы хранятся в формате типа char (см. далее).

Примечание

Прописные русские буквы в кодировке Unicode занимают диапазон от '\u0410' — заглавная буква А, до ' \u042F' — заглавная Я, строчные буквы от '\u0430' — а, до ' \u044F' — я.

В какой бы форме ни записывались символы, компилятор переводит их в Unicode, включая и исходный текст программы.

Замечание

Компилятор и исполняющая система Java работают только с кодировкой Unicode.

Строки

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

Вот некоторые примеры:

"Это строка\пс переносом"

"\"Зубило\" — Чемпион!"

Замечание

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

"Сцепление " + "строк"

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

Чтобы записать длинную строку в виде одной строковой константы, надо после закрывающей кавычки на первой и следующих строках поставить плюс (+); тогда компилятор соберет две (или более) строки в одну строковую константу, например:

"Одна строковая константа, записанная " +

"на двух строках исходного текста"

Тот, кто попытается выводить символы в кодировке Unicode, например слово "Россия":

System.out.println("\u0429\u043e\u0441\u0441\u0438\u044f");

должен знать, что MS Windows использует для вывода в окно Command Prompt шрифт Terminal, в котором буквы кириллицы расположены в начальных кодах Unicode (почему-то в кодировке CP866) и разбросаны по другим сегментам Unicode.

Не все шрифты Unicode содержат начертания (glyphs) всех символов, поэтому будьте осторожны при выводе строк в кодировке Unicode.

СОВЕТ

Используйте Unicode напрямую только в крайних случаях.

Имена

Имена (names) переменных, классов, методов и других объектов могут быть простыми (общее название — идентификаторы (identifiers)) и составными (qualified names). Идентификаторы в Java составляются из так называемых букв Java (Java letters) и арабских цифр 0—9, причем первым символом идентификатора не может быть цифра. (Действительно, как понять запись 2e3: как число 2000,0 или как имя переменной?) В набор букв Java обязательно входят прописные и строчные латинские буквы, знак доллара ($) и знак подчеркивания (_), а также символы национальных алфавитов.

ЗАмЕчАниЕ

Не указывайте в именах знак доллара. Компилятор Java использует его для записи имен вложенных классов.

Вот примеры правильных идентификаторов:

a1 my var var3 5 var veryLongVarName

aName theName a2Vh36kBnMt456dX

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

Придумывая имена, не забывайте о рекомендациях "Code Conventions".

В классе Character, входящем в состав Java API, есть два метода, проверяющие, пригоден ли данный символ для использования в идентификаторе: метод isJavaIdentifierStart(), проверяющий, является ли символ буквой Java, и метод isJavaIdentifierPart(), выясняющий, является ли символ буквой, цифрой, знаком подчеркивания (_) или знаком доллара ($) .

Служебные слова Java, такие как class, void, static, зарезервированы, их нельзя использовать в качестве идентификаторов своих объектов.

Составное имя (qualified name) — это несколько идентификаторов, разделенных точками, без пробелов, например уже встречавшееся нам имя System.out.println.

Примитивные типы данных и операции

Все типы исходных данных, встроенные в язык Java, делятся на две группы: примитивные типы (primitive types) и ссылочные типы (reference types).

Ссылочные типы включают массивы (arrays), классы (classes) и интерфейсы (interfaces). Начиная с Java SE 5 появился перечислимый тип (enum).

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

Числовые типы делятся на целые (integral) и вещественные (floating-point).

Целых типов пять: byte, short, int, long, char.

Символы можно применять везде, где используется тип int, поэтому JLS причисляет тип char к целым типам. Например, символы можно использовать в арифметических вычислениях, скажем, можно написать 2 + 'Ж', к двойке будет прибавляться кодировка Unicode '\u04i6' буквы 'Ж'. В десятичной форме это число 1046, и в результате сложения получим 1048.

Напомним, что в записи 2 + "Ж", где буква Ж записана как строка, в кавычках, плюс понимается как сцепление строк, двойка будет преобразована в строку, в результате получится строка "2Ж".

Вещественных типов всего два: float и double.

На рис. 1.2 показана иерархия типов данных Java.

byte short int long char float double Рис. 1.2. Типы данных языка Java

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

Замечание для специалистов

Java — язык со строгой типизацией (strongly typed language).

Разберем каждый тип подробнее.

Логический тип

Значения логического типа boolean возникают в результате различных сравнений, вроде 2 > 3, и используются главным образом в условных операторах и операторах циклов. Логических значений всего два: true (истина) и false (ложь). Это служебные слова Java. Описание переменных данного типа выглядит так:

boolean b = true, bb = false, bool2;

Над логическими данными можно выполнять операции присваивания, например bool2 = true, в том числе и составные с логическими операциями; сравнение на равенство b == bb и на неравенство b != bb, а также логические операции.

Логические операции

В языке Java реализованы четыре логические операции:

□ отрицание (NOT) — ! (обозначается восклицательным знаком);

□ конъюнкция (AND) — & (амперсанд);

□ дизъюнкция (OR) — | (вертикальная черта);

□ исключающее ИЛИ (XOR) — л (каре).

Они выполняются над логическими данными типа boolean, их результатом будет тоже логическое значение — true или false. Про эти операции можно ничего не знать, кроме того, что представлено в табл. 1.1.

Таблица 1.1. Логические операции
b1 b2 !b1 b1 & b2 b1 | b2 b1 Р» b2
true true false true true false
true false false false true true
false true true false true true
false false true false false false

Словами эти правила можно выразить так:

□ отрицание меняет значение истинности;

□ конъюнкция истинна, только если оба операнда истинны;

□ дизъюнкция ложна, только если оба операнда ложны;

□ исключающее ИЛИ истинно, только если значения операндов различны.

ЗАМЕЧАНиЕ

Если бы Шекспир был программистом, фразу "To be or not to be" он написал бы так:

2b | ! 2b.

Кроме перечисленных четырех логических операций есть еще две логические операции сокращенного вычисления:

□ сокращенная конъюнкция (conditional-AND) — &&;

□ сокращенная дизъюнкция (conditional-OR) — | |.

Удвоенные знаки амперсанда и вертикальной черты следует записывать без пробелов.

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

Это правило очень удобно и довольно ловко используется программистами, например можно записывать выражения (n != 0) && (m/n > 0.001) или (n == 0) | | (m/n > 0.001), не опасаясь деления на нуль.

ЗАМЕЧАНиЕ

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

Упражнения

1. Для переменных b и bb, определенных в разд. "Логический тип" данной главы, найдите значение выражения b & bb && !bb | b.

2. При тех же определениях вычислите выражение (!b || bb) && (bb Л b).

Целые типы

Спецификация языка Java, JLS, определяет разрядность (количество байтов, выделяемых для хранения значений типа в оперативной памяти) каждого типа. Для целых типов она приведена в табл. 1.2. В таблице указан также диапазон значений каждого типа, получаемый на процессорах архитектуры Pentium.

Таблица 1.2. Целые типы
Тип Разрядность(байт) Диапазон
byte 1 РћС‚ -128 РґРѕ 127
short 2 РћС‚ -32 768 РґРѕ 32 767
int 4 РћС‚ -2 147 483 648 РґРѕ 2 147 483 647
long 8 РћС‚ -9 223 372 036 854 775 808 РґРѕ 9 223 372 036 854 775 807
char 2 От ’\u0000 ’ до ’ \uFFFF’, в десятичной форме от 0 до 65 535

Хотя тип char занимает два байта, в арифметических вычислениях он участвует как тип int, ему выделяется 4 байта, два старших байта заполняются нулями.

Вот примеры определения переменных целых типов:

byte b1 = 50, b2 = -99, b3; short det = 0, ind = 1, sh = ’d’;

int i = -100, j = 100, k = 9999;

long big = 50, veryBig = 2147483648L;

char c1 = 'A', c2 = '?', c3 = 36, newLine = '\n';

Целые типы, кроме char, хранятся в двоичном виде с дополнительным кодом. Последнее означает, что для отрицательных чисел хранится не их двоичное представление, а дополнительный код этого двоичного представления.

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

Например, значение 50 переменной b1, определенной ранее, будет храниться в одном байте с содержимым 00110010, а значение -99 переменной b2 — в байте с содержимым, которое вычисляется так: число 99 переводится в двоичную форму, получая 01100011, меняются единицы и нули, получая 10011100, и прибавляется единица, получая окончательно байт с содержимым 10011101.

Смысл всех этих преобразований в том, что сложение числа с его дополнительным кодом в двоичной арифметике даст в результате нуль; старший бит, равный 1, просто теряется, поскольку выходит за разрядную сетку. Это означает, что в такой странной арифметике дополнительный код числа является противоположным к нему числом, числом с обратным знаком. А это, в свою очередь, означает, что вместо того, чтобы вычесть из числа A число B, можно к A прибавить дополнительный код числа B. Таким образом, операция вычитания исключается из набора машинных операций.

Над целыми типами можно производить массу операций. Их набор восходит к языку С, он оказался удобным и кочует из языка в язык почти без изменений. Особенности применения этих операций в языке Java показаны на примерах.

Операции над целыми типами

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

Арифметические операции

К арифметическим операциям относятся:

□ сложение — + (плюс);

□ вычитание — - (дефис);

□ умножение — * (звездочка);

□ деление — / (наклонная черта, слэш);

□ взятие остатка от деления (деление по модулю) — % (процент);

□ инкремент (увеличение на единицу) — ++;

□ декремент (уменьшение на единицу)---.

Между сдвоенными плюсами и минусами нельзя оставлять пробелы.

Сложение, вычитание и умножение целых значений выполняются как обычно, а вот деление целых значений в результате дает опять целое (так называемое целочисленное деление), например 5/2 даст в результате 2, а не 2,5, а 5/(-3) даст -1. Дробная часть попросту отбрасывается, происходит так называемое усечение частного. Это поначалу обескураживает, но потом оказывается удобным для усечения вещественных чисел.

Замечание

В Java принято целочисленное деление.

Это странное для математики правило естественно для программирования: если оба операнда имеют один и тот же тип, то и результат имеет тот же тип. Достаточно написать 5/2.0 или 5.0/2 или 5.0/2.0, и получим 2,5 как результат деления вещественных чисел.

Операция деление по модулю определяется так:

a % b = a — (a / b) * b

например, 5%2 даст в результате 1, а 5%(-3) даст 2, т. к. 5 = (-3) * (-1) + 2, но (-5)%3 даст -2, поскольку -5 = 3 * (-1) — 2.

Операции инкремент и декремент означают увеличение или уменьшение значения переменной на единицу и применяются только к переменным, но не к константам или выражениям, нельзя написать 5++ или (a + b)++.

Например, после приведенных ранее описаний i++ даст -99, а j -- даст 99.

Интересно, что эти операции можно записать и перед переменной: ++i, --j. Разница проявится только в выражениях: при первой форме записи (постфиксной) в выражении участвует старое значение переменной и только потом происходит увеличение или уменьшение ее значения. При второй форме записи (префиксной) сначала изменится переменная, и ее новое значение будет участвовать в выражении.

Например, после приведенных ранее описаний (k++) + 5 даст в результате 10004, а переменная k примет значение 10000. Но в той же исходной ситуации (++k) + 5 даст 10005, а переменная k станет равной 10000.

Приведение типов

Результат арифметической операции имеет тип int, кроме того случая, когда один из операндов типа long. В этом случае результат будет типа long.

Перед выполнением арифметической операции всегда происходит повышение (promotion) типов byte, short, char. Они преобразуются в тип int, а может быть, и в тип long, если другой операнд типа long. Операнд типа int повышается до типа long, если другой операнд типа long. Конечно, числовое значение операнда при этом не меняется.

Это правило приводит иногда к неожиданным результатам. Попытка скомпилировать простую программу, представленную в листинге 1.3, приведет к сообщениям компилятора, показанным на рис. 1.3.

Листинг 1.3. Неверное определение переменной

class InvalidDef{

public static void main(String[] args){

byte b1 = 50, b2 = -99;

short k = b1 + b2; // Неверно!

System.out.println(„k=" + k);

}

}

Рис. 1.3. Сообщения компилятора об ошибке

Эти сообщения означают, что в файле InvalidDefjava, в строке 4, обнаружена возможная потеря точности (possible loss of precision). Затем приводятся обнаруженный (found) и нужный (required) типы, выводится строка, в которой обнаружена (а не сделана) ошибка, и отмечается символ, при разборе которого найдена ошибка. Затем указано общее количество обнаруженных (а не сделанных) ошибок (1 error).

В подобных ситуациях следует выполнить явное приведение типа. В данном случае это будет сужение (narrowing) типа int до типа short. Оно осуществляется операцией явного приведения, которая записывается перед приводимым значением в виде имени типа в скобках. Определение

short k = (short)(b1 + b2);

будет верным.

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

byte b = (byte)300;

даст переменной b значение 44. Действительно, в двоичном представлении числа 300, равном 100101100, отбрасывается старший бит и получается 00101100.

Таким же образом можно произвести и явное расширение (widening) типа, если в этом есть необходимость.

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

Замечание

В языке Java нет целочисленного переполнения.

Операции сравнения

В языке Java шесть обычных операций сравнения целых чисел по величине:

□ больше — >;

□ меньше — <;

□ больше или равно — >=;

□ меньше или равно — <=;

□ равно — ==;

□ не равно — !=.

Сдвоенные символы записываются без пробелов, их нельзя переставлять местами, запись => будет неверной.

Результат сравнения — логическое значение: true, например в результате сравнения 3 != 5; или false, например в результате сравнения 3 == 5.

Для записи сложных сравнений следует привлекать логические операции. Например, в вычислениях часто приходится делать проверки вида a < x < b. Подобная запись на языке Java приведет к сообщению об ошибке, поскольку первое сравнение, a < x, даст true или false, а Java не знает, больше это, чем ь, или меньше. В данном случае следует написать выражение (a < x) && (x < b), причем здесь скобки можно опустить, написать просто a < x && x < b, но об этом немного позднее.

Побитовые операции

Иногда приходится изменять значения отдельных битов в целых данных. Это выполняется с помощью побитовых (bitwise) операций, как говорят, "наложением маски".

В языке Java четыре побитовые операции:

□ дополнение (complement)--(тильда);

□ побитовая конъюнкция (bitwise AND) — &;

□ побитовая дизъюнкция (bitwise OR) — |;

□ побитовое исключающее ИЛИ (bitwise XOR) — л.

Они выполняются поразрядно, после того как оба операнда будут приведены к одному типу int или long, так же как и для арифметических операций, а значит, и к одной разрядности. Операции над каждой парой битов выполняются согласно табл. 1.3.

Таблица 1.3. Побитовые операции
n1 n2 ~n1 n1 & n2 n1 | n2 n1 Р» n2
1 1 0 1 1 0
1 0 0 0 1 1
0 1 1 0 1 1
0 0 1 0 0 0

В нашем примере число bi == 50, его двоичное представление 00110010, число b2 == -99, а его двоичное представление равно 10011101. Перед операцией происходит повышение типа byte до типа int. Получаем представления из 32-х разрядов для b1 — 0...00110010, а для b2 — 1...10011101. В результате побитовых операций получаем:

□ ~b2 == 98, двоичное представление — 0...01100010;

□ b1 & b2 == 16, двоичное представление — 0...00010000;

□ b1 | b2 == -65, двоичное представление — 1...10111111;

□ b1 Л b2 == -81, двоичное представление — 1...10101111.

Двоичное представление каждого результата занимает 32 бита.

Заметьте, что дополнение ~x всегда эквивалентно разности (-x) -1.

РЎРґРІРёРіРё

В языке Java есть три операции сдвига двоичных разрядов:

□ сдвиг влево — <<;

□ сдвиг вправо — >>;

□ беззнаковый сдвиг вправо — >>>.

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

Например, операция b1 << 2 сдвинет влево на 2 разряда предварительно повышенное значение 0...00110010 переменной b1, что даст в результате 0...011001000, десятичное число — 200. Освободившиеся справа разряды заполняются нулями; левые разряды, находящиеся за 32-м битом, теряются.

Операция b2 << 2 сдвинет повышенное значение 1...10011101 на два разряда влево. В результате получим 1...1001110100, десятичное значение--396.

Заметьте, что сдвиг влево на n разрядов эквивалентен умножению числа на 2 в степени n.

Операция b1 >> 2 даст в результате 0...00001100, десятичное — 12, а b2 >> 2 — результат

1...11100111, десятичное--25, т. е. слева распространяется старший бит, правые биты

теряются. Это так называемый арифметический сдвиг.

Операция беззнакового сдвига во всех случаях ставит слева на освободившиеся места нули, осуществляя логический сдвиг. Но вследствие предварительного повышения это имеет эффект только для нескольких старших разрядов отрицательных чисел. Так, b2 >>> 2 имеет результатом 001...100111, десятичное число — 1 073 741 799.

Если же мы хотим получить логический сдвиг исходного значения 10011101 переменной b2, т. е. 0...00100111, надо предварительно наложить на b2 маску, обнулив старшие биты:

(b2 & 0xFF) >>> 2.

Замечание

Будьте осторожны при использовании сдвигов вправо.

Упражнения

3. Каково значение выражения ' D' + 5?

При определениях, сделанных ранее, вычислите выражения:

4. (b1 + СЃ1) % (++b2 / b1++).

5. (b1 < СЃ1) && (b2 == -99) || (ind >= 0).

6. (b1 | СЃ1) & (big Р› b1).

7. (b1<<3 + СЃ1<<2) % (b2>>5 / b1>>>2).

Вещественные типы

Вещественных типов в Java два: float и double. Они характеризуются разрядностью, диапазоном значений и точностью представления, отвечающим стандарту IEEE 7541985 с некоторыми изменениями. К обычным вещественным числам добавляются еще три значения:

□ положительная бесконечность, выражаемая константой positive_infinity и возникающая при переполнении положительного значения, например в результате операции умножения 3.0*6e307 или при делении на нуль;

□ отрицательная бесконечность negative_infinity, возникающая при переполнении отрицательного значения, например в результате операции умножения -3.0*6e307 или при делении на нуль отрицательного числа;

□ "не число", записываемое константой NaN (Not a Number) и возникающее, например, при умножении нуля на бесконечность.

В главе 4 мы поговорим о них подробнее.

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

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

Во всем остальном вещественные типы — это обычные вещественные значения, к которым применимы все арифметические операции и сравнения, перечисленные для целых типов. Характеристики вещественных типов приведены в табл. 1.4.

Знатокам C/C++

В языке Java взятие остатка от деления %, инкремент ++ и декремент — применяются и к вещественным типам.

Таблица 1.4. Вещественные типы
Тип Разрядность Диапазон Точность
float 4 байта 3,4x10 -38 < |х| < 3,4x10 38 7—8 цифр в дробной части
double 8 байтов 1,7х10“ 308 < |х| < 1,7x10 308 17 цифр в дробной части

Примеры определения вещественных типов:

float x = 0.001f, y = -34.789F; double z1 = -16.2305, z2;

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

□ если в операции один операнд имеет тип double, то и другой приводится к типу

double;

□ иначе, если один операнд имеет тип float, то и другой приводится к типу float;

□ в противном случае действует правило приведения целых значений.

Операции присваивания

Простая операция присваивания (simple assignment operator) записывается знаком равенства (=), слева от которого стоит переменная, а справа — выражение, совместимое с типом переменной: x = 3.5, у = 2 * (x - 0.567) / (x + 2), b = x < y, bb = x >= y && b.

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

Операция присваивания имеет еще одно, побочное, действие: переменная, стоящая слева, получает приведенное значение правой части, старое ее значение теряется.

В операции присваивания левая и правая части неравноправны, нельзя написать 3.5 = x. После операции x = y изменится переменная x, став равной y, а после y = x изменится переменная y.

Кроме простой операции присваивания есть еще 11 составных операций присваивания (compound assignment operators): +=, -=, *=, /=, %=, &=, |=, Л=, <<=, >>=, >>>=. Символы запи

сываются без пробелов, нельзя переставлять их местами.

Все составные операции присваивания действуют по одной схеме:

x операция = a

эквивалентно

x = (тип x)(x операция a)

Напомним, что переменная ind типа short определена у нас со значением 1. Присваивание ind += 7.8 даст в результате число 8, то же значение получит и переменная ind. Эта операция эквивалентна простой операции присваивания ind = (short)(ind + 7.8).

Перед присваиванием, при необходимости, автоматически производится приведение типа. Поэтому:

byte b = 1;

b = b + 10; // Ошибка!

b += 10; // Правильно!

Перед сложением b + 10 происходит повышение b до типа int, результат сложения тоже будет типа int и, в первом случае, результат не может быть присвоен переменной b без явного приведения типа. Во втором случае перед присваиванием произойдет сужение результата сложения до типа byte.

Упражнения

8. Чему равно выражение x = y = z = 1?

9. Что получится в результате присваиваний x += y -= z /= x + 2?

Условная операция

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

x < 0 ? 0 : x x > y ? x — y : x + y

Условная операция выполняется так. Сначала вычисляется логическое выражение. Если получилось значение true, то вычисляется первое выражение после вопросительного знака и его значение будет результатом всей операции. Последнее выражение при этом не вычисляется. Если же получилось значение false, то вычисляется только последнее выражение, его значение будет результатом операции.

Это позволяет написать n == 0 ? m : m / n, не опасаясь деления на нуль. Условная операция поначалу кажется странной, но она очень удобна для записи небольших разветвлений.

Упражнения

10. Каков смысл операции x > 0 ? x : -x?

11. Что дает в результате операция x > y ? x : y?

12. Что получится в результате операции x > y ? y : x?

Выражения

Из констант и переменных, операций над ними, вызовов методов и скобок составляются выражения (expressions). Разумеется, все элементы выражения должны быть совместимы, нельзя написать, например, 2 + true. При вычислении выражения выполняются четыре правила.

□ Операции одного приоритета вычисляются слева направо: x + y + z вычисляется как (x + y) + z. Исключение: операции присваивания вычисляются справа налево: x = y = z вычисляется как x = (y = z) .

□ Левый операнд вычисляется раньше правого.

□ Операнды полностью вычисляются перед выполнением операции.

□ Перед выполнением составной операции присваивания значение левой части сохраняется для использования в правой части.

Следующие примеры показывают особенности применения первых трех правил. Пусть

int a = 3, b = 5;

Тогда результатом выражения b + (b = 3) будет число 8; но результатом выражения (b = 3) + b будет число 6. Выражение b += (b = 3) даст в результате 8, потому что вычисляется как первое из приведенных выражений.

Знатокам C/C++

Большинство компиляторов языка C++ во всех этих случаях вычислят значение 8.

Четвертое правило можно продемонстрировать так. При тех же определениях переменных a и b в результате вычисления выражения

b += a += b += 7

получим 20. Хотя операции присваивания выполняются справа налево и после первой, самой правой, операции значение b становится равным 12, но в последнем, левом, присваивании участвует старое значение b, равное 5. А в результате двух последовательных вычислений

a += b += 7; b += a;

получим 27, поскольку во втором выражении участвует уже новое значение переменной b, равное 12.

Знатокам C/C++

Большинство компиляторов C++ в обоих случаях вычислят 27.

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

Порядок вычисления выражения всегда можно отрегулировать скобками, скобок можно ставить сколько угодно. Но здесь важно соблюдать "золотую середину". При большом количестве скобок снижается наглядность выражения и легко ошибиться в расстановке скобок. Если выражение со скобками корректно, то компилятор может отследить только парность скобок, но не правильность их расстановки.

Приоритет операций

Операции перечислены в порядке убывания приоритета. Операции на одной строке имеют одинаковый приоритет.

1. Постфиксные операции ++ и —.

2. Префиксные операции ++ и --, дополнение ~ и отрицание !.

3. Приведение типа (тип).

4. Умножение *, деление / и взятие остатка %.

5. Сложение + и вычитание -.

6. РЎРґРІРёРіРё: <<, >>, >>>.

7. Сравнения: >, <, >=, <=.

8. Сравнения: ==, !=.

9. Побитовая конъюнкция — &.

10. Побитовое исключающее ИЛИ — л.

11. Побитовая дизъюнкция — |.

12. Конъюнкция — &&.

13. Дизъюнкция — | |.

14. Условная операция — ?:.

15. Присваивания: =, +=, -=, *=, /=, %=, &=, л=, |=, <<=, >>=, >>>=.

Здесь перечислены не все операции языка Java, список будет дополняться по мере изучения новых операций.

Знатокам C/C++

В Java нет операции "запятая", но список выражений используется в операторе цикла for.

Операторы

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

Записать его можно в разных формах: в виде блок-схемы, на псевдокоде, на обычном языке, как мы записываем кулинарные рецепты, или как-нибудь еще.

Всякий язык программирования должен иметь средства записи алгоритмов. Они называются операторами (statements) языка. Минимальный набор операторов должен содержать оператор для записи линейных вычислений, условный оператор для записи разветвлений и оператор цикла.

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

Набор операторов языка Java включает:

□ операторы описания переменных и других объектов (были рассмотрены ранее);

□ операторы-выражения;

□ операторы присваивания;

□ условный оператор if;

□ три оператора цикла while, do-while, for;

□ оператор варианта switch;

□ операторы перехода break, continue и return;

□ блок, выделяемый фигурными скобками {};

□ пустой оператор — просто точка с запятой.

Здесь приведен не весь набор операторов Java, он будет дополняться по мере изучения языка.

Замечание

В языке Java нет оператора goto.

Всякий оператор завершается точкой с запятой.

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

Знатокам Pascal

Точка с запятой в Java не разделяет операторы, а является частью оператора.

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

Блок

Блок заключает в себе нуль или несколько операторов с целью использовать их как один оператор в тех местах, где по правилам языка можно записать только один оператор. Например, {х = 5; у = 7;}. Можно записать и пустой блок, просто пару фигурных скобок {} .

Блоки операторов часто применяются для ограничения области действия переменных, а иногда просто для улучшения читаемости текста программы.

Операторы присваивания

Точка с запятой в конце любой операции присваивания превращает ее в оператор присваивания. Побочное действие операции — присваивание — становится в операторе основным.

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

Условный оператор

Условный оператор (if-then-else statement) предназначен для организации разветвлений в программе. На языке Java он записывается так:

if (логВыр) оператор1 else оператор2

и действует следующим образом. Сначала вычисляется логическое выражение логВыр. Если результат вычисления true, то действует оператор1, и на этом работа условного оператора завершается, оператор2 не действует. Далее будет выполняться оператор, следующий за оператором if. Это так называемая "ветвь then" условного оператора. Если результат логического выражения false, то действует оператор2, при этом оператор1 вообще не выполняется ("ветвь else").

Условный оператор может быть сокращенным, без ветви else (if-then statement):

if (логВыр) оператор1

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

Синтаксис языка не позволяет записывать несколько операторов ни в ветви then, ни в ветви else. При необходимости составляется блок операторов в фигурных скобках. Соглашения "Code Conventions" рекомендуют всегда использовать фигурные скобки и размещать оператор на нескольких строках с отступами, как в следующем примере:

if (a < С…){

С… = a + b;

} else {

х = a — b;

}

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

Очень часто одним из операторов является опять-таки условный оператор, например:

if (n == 0){ sign = 0;

} else if (n < 0){ sign = -1;

} else {

sign = 1;

}

При этом может возникнуть такая ситуация ("dangling else"):

int ind = 5, x = 100;

if (ind >= 10) if (ind <= 20) x = 0; else x = 1;

К какому условию if относится ветвь else, первому или второму? Сохранит переменная х значение 100 или станет равной 1? Здесь необходимо волевое решение, и общее для большинства языков, в том числе и Java, правило таково: ветвь else относится к ближайшему слева условию if, не имеющему своей ветви else. Поэтому в нашем примере переменная х останется равной 100.

Изменить этот порядок можно с помощью блока:

if (ind > 10) {if (ind < 20) С… = 0;} else С… = 1;

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

if (ind >= 10 && ind <= 20) С… = 0; else С… = 1;

В листинге 1.4 условный оператор применяется для вычисления корней квадратного уравнения ax 2 + bx + c = 0 для любых коэффициентов, в том числе и нулевых.

Листинг 1.4. Вычисление корней квадратного уравнения

class QuadraticEquation{

public static void main(String[] args){

double a = 0.5, b = -2.7, c = 3.5, d, eps=1e-8; if (Math.abs(a) < eps) if (Math.abs(b) < eps)

if (Math.abs(c) < eps) // Все коэффициенты равны нулю

System.out.println("Решение — любое число");

else

System.out.println("Решений нет");

else

System.out.println(,,x1 = х2 = " +(-c / b)); else { // Коэффициенты не равны нулю

if((d = b*b — 4*a*c)< 0.0){ // Комплексные корни

d = 0.5 * Math.sqrt(-d) / a; a = -0.5 * b/ a;

System.out.println(,,x1 = " +a+ " +i " +d+", С…2 = " +a+ " -i " +d);

} else { // Вещественные корни

d = 0.5 * Math.sqrt(d) / a; a = -0.5 * b / a;

System.out.println("х1 = " +(a + d)+ ", х2 = " +(a — d));

}

}

}

}

В этой программе использованы методы вычисления модуля abs() и вычисления квадратного корня sqrt () из вещественного числа, взятые из входящего в Java API класса Math. Поскольку все вычисления с вещественными числами производятся приближенно, не следует ожидать, что вещественное число будет точно равно нулю. Поэтому мы считаем, что коэффициент уравнения равен нулю, если его модуль меньше 0,00000001. Обратите внимание на то, как в методе println () используется сцепление строк, и на то, как операция присваивания при вычислении дискриминанта вложена в логическое выражение, записанное в условном операторе.

"Продвинутым" пользователям

Вам уже хочется вводить коэффициенты a, b и с прямо с клавиатуры? Пожалуйста. Используйте метод System.in.read(byte[] bt), но учтите, что он записывает вводимые цифры в массив байтов bt в кодировке ASCII, в каждый байт по одной цифре. Массив байтов затем надо преобразовать в вещественное число, например методом Double (new string(bt)).doubleValue(). Непонятно? Загляните в главу 23. Но это еще не все, нужно обработать исключительные ситуации, которые могут возникнуть при вводе (см. главу 21).

Упражнения

13. Вычислите с помощью условного оператора значение у, равное х + 1, если х < 0, равное х + 2, если 0 <= х < 1, и равное х + 10 в остальных случаях.

14. Запишите условный оператор, дающий логической переменной z значение true, если точка M^, у) лежит в единичном круге с центром в начале координат, и значение false в противном случае.

Операторы цикла

Основной оператор цикла — оператор while — выглядит так:

while (логВыр) оператор

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

Оператор в цикле может быть и пустым, например следующий фрагмент кода:

int i = 0; double s = 0.0;

while ((s += 1.0 / ++i) < 10);

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

Можно организовать и бесконечный цикл:

while (true) оператор

Конечно, из такого цикла следует предусмотреть какой-то выход, например оператором break, как сделано в листинге 1.5. В противном случае программа зациклится, и вам придется прекращать ее выполнение комбинацией клавиш + в UNIX или через окно Task Manager в Windows.

Если в цикл надо включить несколько операторов, то следует образовать блок операторов {} .

Второй оператор цикла — оператор do-while — имеет вид:

do оператор while (логВыр)

Здесь сначала выполняется оператор, а потом происходит вычисление логического выражения логВыр. Цикл выполняется, пока логВыр остается равным true.

Знатокам Pascal

В цикле do-while проверяется условие продолжения, а не окончания цикла.

Существенное различие между этими двумя операторами цикла только в том, что в цикле do-while оператор обязательно выполнится хотя бы один раз.

Например, пусть задана какая-то функция fx), имеющая на отрезке [a; b] ровно один корень. В листинге 1.5 приведена программа, вычисляющая этот корень приближенно методом деления пополам (бисекции, дихотомии).

Листинг 1.5. Нахождение корня нелинейного уравнения методом бисекции

class Bisection{

static double f(double С…){

return х*х*х — 3*x*x + 3; // Или что-то другое...

}

public static void main(String[] args){

double a = 0.0, b = 1.5, c, y, eps = 1e-8;

do{

c = 0.5 *(a + b); y = f(c); if (Math.abs(y) < eps) break;

// Корень найден. Выходим из цикла

// Если на концах отрезка [a; c] функция имеет разные знаки:

if (f(a) * y < 0.0) b = c;

// Значит, корень здесь. Переносим точку b в точку c // В противном случае: else a = c;

// Переносим точку a в точку c

// Продолжаем, пока отрезок [a; b] не станет мал } while(Math.abs(b-a) >= eps);

System.out.println("x = " +c+ ", f(" +c+ ") = " +y);

}

}

Класс Bisection сложнее предыдущих примеров: в нем кроме метода main() есть еще метод вычисления функции fx). Здесь метод f () очень прост: он вычисляет значение многочлена и возвращает его в качестве значения функции, причем все это выполняется одним оператором:

return выражение

В методе main () появился еще один новый оператор — break, который просто прекращает выполнение цикла, если мы по счастливой случайности наткнулись на приближенное значение корня. Внимательный читатель заметил и появление модификатора static в объявлении метода f(). Он необходим потому, что метод f() вызывается из статического метода main (), о чем мы поговорим в следующей главе.

Третий оператор цикла — оператор for — выглядит так:

for (списокВыр1; логВыр; списокВыр2) оператор

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

Затем вычисляется логическое выражение логВыр. Если оно истинно, true, то действует оператор, потом вычисляются слева направо выражения из списка выражений списокВыр2.

Далее снова проверяется логВыр. Если оно истинно, то выполняется оператор и списокВыр2 и т. д. Как только логВыр станет равным false, выполнение цикла заканчивается.

Короче говоря, выполняется последовательность операторов

списокВыр1; while (логВыр){ оператор списокВыр2;

}

с тем исключением, что если оператором в цикле является оператор continue, то список-Выр2 все-таки выполняется.

Вместо списокВыр1 может стоять одно определение переменной обязательно с начальным значением. Такие переменные известны лишь в пределах этого цикла.

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

for (;;) оператор

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

Хотя в операторе for заложены большие возможности, используется он главным образом для перечислений, когда их количество известно, например фрагмент кода

int s = 0;

for (int k = 1; k <= N; k++) s += k * k;

// Здесь переменная k уже неизвестна

вычисляет сумму квадратов первых N натуральных чисел.

В языке Java есть еще одна форма оператора for, так называемый оператор "for-each", который используется для перебора элементов массивов и коллекций. Мы познакомимся с ним в разделе этой главы, посвященном массивам.

Оператор continue и метки

Оператор continue используется только в операторах цикла. Он имеет две формы. Первая форма состоит только из слова continue и осуществляет немедленный переход к следующей итерации цикла. В очередном фрагменте кода оператор continue позволяет обойти деление на нуль:

for (int i = 0; i < N; i++){ if (i == j) continue; s += 1.0 / (i — j);

}

Вторая форма содержит метку:

continue метка

Метка записывается, как все идентификаторы, из букв Java, цифр и знака подчеркивания, но не требует никакого описания. Метка ставится перед оператором или открывающей фигурной скобкой и отделяется от них двоеточием. Так получается помеченный оператор или помеченный блок.

Знатокам Pascal

Метка не требует описания и не может начинаться с цифры.

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

Оператор break

Оператор break используется в операторах цикла и операторе варианта для немедленного выхода из этих конструкций.

Оператор

break метка

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

M1: { // Внешний блок

M2: { // Вложенный блок — второй уровень M3: { // Третий уровень вложенности... if (что-то случилось) break M2;

// Если true, то здесь ничего не выполняется

}

// Здесь тоже ничего не выполняется

}

// Сюда передается управление

}

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

Упражнения

15. Напишите цикл, вычисляющий факториал заданного натурального числа.

16. Напишите цикл, определяющий, какая наибольшая степень числа 2 содержится среди делителей заданного натурального числа.

Оператор варианта

Оператор варианта switch организует разветвление по нескольким направлениям. Каждая ветвь отмечается константой или константным выражением какого-либо целого типа (кроме long) и выбирается, если значение определенного выражения совпадет с этой константой. Вся конструкция выглядит так: switch (выражение){

case констВыр1: оператор1 case констВыр2: оператор2

case констВырЫ: операторN default: операторDef

}

Стоящее в скобках выражение может быть простого целого типа byte, short, int, char, но не long. Целые числа, символы, или целочисленные выражения, составленные из констант, констВыр, тоже не должны быть типа long.

Кроме простых целых типов допускаются их классы-оболочки, перечисления и строки символов типа String, которые мы рассмотрим в следующих главах. При этом тип константных выражений должен соответствовать типу выражения. Посмотрите, например, листинги 3.4 и 28.9.

Оператор варианта выполняется так. Все константные выражения вычисляются заранее, на этапе компиляции, и должны иметь отличные друг от друга значения. Сначала вычисляется выражение, записанное в круглых скобках. Если оно совпадает с одной из констант, то выполняется оператор, отмеченный этой константой. Затем выполняются ("fall through labels") все следующие операторы, включая и операторDef, и работа оператора варианта заканчивается.

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

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

Знатокам Pascal

После выполнения одного варианта оператор switch продолжает выполнять все оставшиеся варианты.

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

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

switch (dayOfWeek){

case 1: case 2: case 3: case 4: case 5:

System.out.println("Рабочий день"); break; case 6: case 7:

System.out.println("Выходной день"); break; default:

System.out.println("Нeправильно задан день недели");

}

Если дни недели заданы строковыми константами, то предыдущий оператор можно записать так:

switch (dayOfWeek){

case "Mon": case "Tue": case "Wed": case "Thu": case "Fri": System.out.println("Рабочий день"); break; case "Sat": case "Sun":

System.out.println("Выходной день"); break; default:

System.out.println("Нeправильно задан день недели");

}

Замечание

Не забывайте завершать варианты оператором break, если нужно выполнить только один вариант.

Массивы

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

Массивы в языке Java относятся к ссылочным типам и описываются своеобразно, но характерно для ссылочных типов. Описание производится в три этапа.

Первый этап — объявление (declaration). На этом этапе определяется только переменная типа ссылка (reference) на массив, содержащая тип массива. Для этого записывается имя типа элементов массива, квадратными скобками указывается, что объявляется ссылка на массив, а не простая переменная, и перечисляются имена переменных ссылочного типа, например

double[] a, b;

Здесь определены две переменные — ссылки a и b на массивы типа double. Можно поставить квадратные скобки и непосредственно после имени. Это удобно делать, если массив объявляется среди определений обычных переменных:

int i = 0, ar[], k = -1;

Здесь определены две переменные целого типа i и k и объявлена ссылка на целочисленный массив ar.

Второй этап — определение (instantation). На этом этапе указывается количество элементов массива, называемое его длиной, выделяется место для массива в оперативной памяти, переменная-ссылка получает адрес массива. Все эти действия производятся еще одной операцией языка Java — операцией new тип, выделяющей участок в оперативной памяти для объекта, указанного в операции типа, и возвращающей в качестве результата адрес этого участка.

Например,

a = new double[5]; b = new double[100]; ar = new int[50];

При этом все элементы массива получают нулевые значения.

Индексы массивов всегда начинаются с 0. Массив a состоит из пяти переменных: a[0], a[1] ... a[4] . Элемента a[5] в массиве нет. Индексы можно задавать любыми целочисленными выражениями, кроме типа long, например a[i+j], a[i%5], a[++i]. Исполняющая система Java следит за тем, чтобы значения этих выражений не выходили за границы длины массива. Интерпретатор Java в таком случае прекратит выполнение программы и выведет на консоль сообщение о выходе индекса массива за границы его определения.

Третий этап — инициализация (initialization). На этом этапе элементы массива получают начальные значения. Например,

a[0] = 0.01; a[1] = -3.4; a[2] = 2.89; a[3] = 4.5; a[4] = -6.7; for (int i = 0; i < 100; i++) b[i] = 1.0 / i; for (int i = 0; i < 50; i++) ar[i] = 2 * i + 1;

Первые два этапа можно совместить:

double[] a = new double[5], b = new double[100]; int i = 0, ar[] = new int[50], k = -1;

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

double[] a = {0.01, -3.4, 2.89, 4.5, -6.7};

Можно совместить второй и третий этап:

a = new double[] {0.1, 0.2, -0.3, 0.45, -0.02};

Можно даже создать безымянный массив, сразу же используя результат операции new, например так:

System.out.println(new char[] {THT, 'e', TlT, TlT, 'o'});

Ссылка на массив не является частью описанного массива, ее можно перебросить на другой массив того же типа операцией присваивания. Например, после присваивания a = b обе ссылки a и b будут указывать на один и тот же массив из 100 вещественных переменных типа double и содержать один и тот же адрес.

Ссылка может присвоить "пустое" значение null, не указывающее ни на какой адрес оперативной памяти:

ar = null;

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

Кроме простой операции присваивания со ссылками можно производить еще только сравнения на равенство, например a == b, и неравенство — a != b. При этом сопоставляются адреса, содержащиеся в ссылках; мы можем узнать, не ссылаются ли они на один и тот же массив.

Замечание для специалистов

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

Кроме ссылки на массив для каждого массива автоматически определяется целая константа с одним и тем же именем length. Ее значение равно длине массива. Для каждого массива имя этой константы уточняется именем массива через точку. Так, после наших определений, константа a.length равна 5, константа b.length равна 100, а ar.length равна 50.

С помощью константы length последний элемент массива a можно записать так: a[a.length - 1], предпоследний — a[a.length - 2] и т. д. Элементы массива обычно перебираются в цикле вида:

double aMin = a[0], aMax = aMin; for (int i = 0; i < a.length; i++){ if (a[i] < aMin) aMin = a[i]; if (a[i] > aMax) aMax = a[i];

}

double range = aMax - aMin;

Здесь вычисляется диапазон значений массива. Заметьте, что цикл можно было бы начать с 1.

Ситуация, когда надо перебрать все элементы массива в порядке возрастания их индексов, как в предыдущем примере, встречается очень часто. Начиная с версии Java SE 5, для таких случаев в язык Java введена упрощенная форма оператора цикла for, так называемый оператор "for-each", уже упоминавшийся ранее. Вот как можно записать предыдущий пример оператором "for-each":

double aMin = a[0], aMax = aMin; for (double x : a){

if (x < aMin) aMin = x; if (x > aMax) aMax = x;

}

double range = aMax - aMin;

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

Элементы массива — это обыкновенные переменные своего типа, с ними можно производить все операции, допустимые для этого типа: (a[2] + a[4]) / a[0] и т. д.

Знатокам C/C++

Массив символов в Java не является строкой, даже если он заканчивается нуль-символом

T\u0000 T.

Многомерные массивы

Элементами массивов в Java могут быть массивы. Можно объявить ссылку:

char [][] c;

что эквивалентно

char [] c[];

или char c[] [];

Затем определяем внешний массив и его размерность:

c = new char[3][];

Становится ясно, что с — массив, состоящий из трех элементов-массивов. Теперь определяем его элементы-массивы:

c[0] = new char[2]; c[1] = new char[4]; c[2] = new char[3];

После этих определений переменная c.length равна 3, c[0] .length равна 2, c[1] .length равна 4 и c[2]. length равна 3.

Наконец, задаем начальные значения c[0][0] = Ta% c[0][1] = Tr% c[1][0] = Tr’,

c[1] [1] = TaT, c[1] [2] = TyT Рё С‚. Рґ.

Замечание

Двумерный массив в Java не обязан быть прямоугольным.

Описания можно сократить:

int[] [] d = new int[3] [4];

А начальные значения задать так:

int[][] inds = {{1, 2, 3}, {4, 5, 6}};

В листинге 1.6 приведен пример программы, вычисляющей первые 10 строк треугольника Паскаля, заносящей их в треугольный массив и выводящей его элементы на экран. Рисунок 1.4 показывает вывод этой программы.

Листинг 1.6. Треугольник Паскаля

class PascalTriangle{

public static final int LINES = 10; // Так определяются константы

public static void main(String[] args){ int [][] p = new int [LINES] [ ] ; p[0] = new int[1];

System.out.println(p[0][0] = 1); p[1] = new int[2]; p[1] [0] = p[1] [1] = 1;

System.out.println(p[1][0] + " " + p[1][1]); for (int i = 2; i < LINES; i++){ p[i] = new int[i+1];

System.out.print((p[i][0] = 1) + " "); for (int j = 1; j < i; j++)

System.out.print((p[i][j] = p[i-1][j-1] + p[i-1][j]) + " "); System.out.println(p[i][i] = 1);

}

}

\ Command Prompt

10 10 5 115 20 15 6 1 21 35 35 21 7 1 28 56 70 56 28 8 1 36 84 126 126 84 36 9 1

Microsoft Windows [Uersion 5.2.3790]

Copyright 1985-2003 Microsoft Corp.

C:\>cd progs

C:\progs>jauac PascalTriangle.jaua

C:\progs>java PascalTriangle 1

Рис. 1.4. Вывод треугольника Паскаля в окно Command Prompt

Заключение

Уф-ф-ф! Вот вы и одолели базовые конструкции языка. Раз вы добрались до этого места, значит, умеете уже очень много. Вы можете написать программу на Java, отладить ее, устранив ошибки, и выполнить. Вы способны запрограммировать любой не слишком сложный вычислительный алгоритм, обрабатывающий числовые данные.

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

Вопросы для самопроверки

1. Из чего состоит программа на языке Java?

2. Как оформляется метод обработки информации в Java?

3. Каков заголовок у метода main() ?

4. Как записать комментарии к программе?

5. Что такое аннотация?

6. В каких системах счисления можно записывать целые константы?

7. Какое количество выражено числом 032?

8. Какое количество выражено числом 0х2С?

9. Как записать символ "наклонная черта"?

10. Как записать символ "обратная наклонная черта"?

11. Каков результат операции 3.45 % 2.4?

12. Что получится в результате операций 12 | 14 & 10?

13. Что даст в результате операция 3 << 4?

14. Можно ли записать циклы внутри условного оператора?

15. Можно ли использовать оператор continue в операторе варианта?

16. Можно ли использовать оператор break с меткой в операторе варианта?

17. Можно ли определить массив нулевой длины?

18. Как можно перебрать все элементы массива в порядке возрастания индексов?

19. Как перебрать все элементы массива в порядке убывания индексов?

20. Что случится, если индекс массива превысит его длину?

ГЛАВА 2

 

Объектно-ориентированное программирование в Java

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

Парадигмы программирования

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

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

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

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

В это же время обнаружилось, что удачная или неудачная структура исходных данных может сильно облегчить или усложнить их обработку. Одни исходные данные удобнее объединить в массив, для других больше подходит структура дерева или стека. Появилось множество исследований различных структур данных и рекомендаций по их применению. Никлаус Вирт, создатель языка Pascal, даже назвал одну из своих книг "Алгоритмы + структуры данных = программы".

Возникла идея объединить исходные данные и все процедуры их обработки в один модуль. Эта идея модульного программирования быстро завоевала умы и на некоторое время стала парадигмой. Программы составлялись из отдельных модулей, содержащих десяток-другой процедур и функций. Эффективность таких программ тем выше, чем меньше модули зависят друг от друга. Автономность модулей позволяет создавать и библиотеки модулей, чтобы потом использовать их в качестве строительных блоков для других программ.

Для того чтобы обеспечить максимальную независимость модулей друг от друга, надо четко отделить процедуры, которые будут использоваться другими модулями, — открытые (public) процедуры, от вспомогательных, которые обрабатывают данные, заключенные в этот модуль, — закрытых (private) процедур. Для этого модуль делится на две части. Открытые процедуры перечисляются в первой части модуля — интерфейсе (interface), вторые участвуют только во второй его части — реализации (implementation) модуля. Данные, занесенные в модуль, тоже делятся на открытые, указанные в интерфейсе и доступные для других модулей, и закрытые, доступные только для процедур того же модуля. В различных языках программирования это деление производится по-разному. В языке Turbo Pascal модуль специально делится на интерфейс и реализацию, в языке С интерфейс выносится в отдельные "головные" (header) файлы. В языке С++, кроме того, для описания интерфейса можно воспользоваться абстрактными классами. В языке Java есть специальная конструкция для описания интерфейсов, которая так и называется — interface, но можно написать и абстрактные классы.

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

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

Опять возник вопрос: каким образом разбить программу на модули? Тут кстати оказались методы решения старой задачи программирования — моделирования действий искусственных и природных объектов: роботов, станков с программным управлением, беспилотных самолетов; моделирования поведения людей, животных, растений, систем обеспечения жизнедеятельности, систем управления технологическими процессами.

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

Оказалось удобным сделать и обратное — разбить программу на модули так, чтобы она превратилась в совокупность взаимодействующих объектов. Так возникло объектноориентированное программирование (object-oriented programming), сокращенно ООП (OOP) — современная парадигма программирования.

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

В виде объектов можно представить совсем неожиданные понятия. Например, окно на экране дисплея — это объект, имеющий ширину width и высоту height, определенное расположение на экране, описываемое обычно координатами (x, у) левого верхнего угла окна, а также шрифт, которым в окно выводится текст, скажем, Times New Roman, цвет фона color, несколько кнопок, полосы прокрутки и другие характеристики. Окно может перемещаться по экрану методом, описанным в какой-нибудь процедуре, скажем, move (), увеличиваться или уменьшаться в размерах каким-нибудь методом size(), сворачиваться в ярлык методом iconify(), как-то реагировать на действия мыши и нажатия клавиш. Это полноценный объект! Кнопки, полосы прокрутки и прочие элементы окна — это тоже объекты со своими характеристиками и действиями: размерами, шрифтами, перемещениями.

Разумеется, считать, что окно само "умеет" выполнять действия, а мы только даем ему поручения: "Свернись, развернись, передвинься", — это несколько неожиданный взгляд на вещи, но ведь сейчас можно подавать команды не только манипуляцией мышью и нажатием клавиш, но и голосом!

Идея объектно-ориентированного программирования оказалась очень плодотворной и стала активно развиваться. Выяснилось, что удобно ставить задачу сразу в виде совокупности действующих объектов — возник объектно-ориентированный анализ, ООА (object-oriented analysis, OOA). Решили проектировать сложные системы в виде объектов — появилось объектно-ориентированное проектирование, ООП (object-oriented design, OOD).

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

Принципы объектно-ориентированного программирования

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

Абстракция

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

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

Мы должны абстрагироваться от некоторых конкретных деталей объекта, отбросить их. Очень важно выбрать правильную степень абстракции. Слишком высокая степень даст только приблизительное описание объекта, не позволит правильно моделировать его поведение. Можно охарактеризовать человека как "Двуногое без перьев", но что это даст для его понимания? С другой стороны, слишком низкая степень абстракции сделает модель очень сложной, перегруженной деталями и потому непригодной.

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

Итак, прежде всего нам надо выбрать уровень абстракции, необходимый для правильного описания реального информационного процесса. Затем следует выделить объекты, участвующие в этом процессе, и установить связи между этими объектами. Как это сделать? Опишите процесс словами и проанализируйте получившиеся фразы. "Завод выпускает автомобили". Здесь два объекта — завод и автомобиль. Производственнотехнические характеристики завода составят набор полей объекта "Завод", а процесс выпуска автомобиля будет описан в виде набора методов объекта "Завод".

Пример из другой области: "Преподаватель читает учебный курс". Полями объекта "Преподаватель" будут его фамилия, имя и отчество, научно-педагогический стаж, квалификация, ученая степень, выпущенные им учебники и методические пособия. Методами "Преподавателя" будут такие действия, как "читать", "писать", "повышать квалификацию", "проводить консультацию", "принимать зачет". Полями объекта "Учебный курс" будут его название, программа, количество часов, перечень учебных пособий. Будет ли объект "Учебный курс" обладать какими-то методами или в этом объекте будут только поля? Какие действия выполняет "Учебный курс"? По-видимому, единственным действием объекта "Учебный курс" будет предоставление своих полей другим объектам, значит, нужны методы доступа к полям объекта.

Таким образом, если в словесном описании процесса вам потребовалось сформулировать какое-то понятие, то оно и будет кандидатом на оформление его в виде объекта. Существительные, описывающие это понятие, будут полями объекта, а глаголы — методами будущего объекта.

В объектно-ориентированных языках модель информационного процесса записывается в виде одного или нескольких классов (classes). Каждый класс описывает свойства одного объекта. Класс можно считать проектом, слепком, чертежом, по которому затем будут создаваться конкретные объекты. При описании класса применяются знакомые нам конструкции программирования.

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

Методы обработки информации, используемые объектом, описываются процедурами и функциями. Они называются методами класса (class methods). Сложные объекты могут содержать несколько десятков методов, а значит, несколько десятков процедур и функций. Методы класса активно используют поля класса, но кроме них могут создавать и свои локальные переменные, необходимые для работы метода.

Кроме полей и методов в классе можно описать и вложенные классы (nested classes), и вложенные интерфейсы, в которые, в свою очередь, можно вложить классы и интерфейсы. Мы можем создать сложную "матрешку" вложенных классов. Поля, методы и вложенные классы первого уровня называются членами класса (class members). Разные школы объектно-ориентированного программирования предлагают разные термины для описания структуры класса, мы используем терминологию, принятую в технологии Java.

Вот набросок описания автомобиля:

class Automobile{

int maxVelocity; // Поле, содержащее наибольшую скорость автомобиля.

int speed; // Поле, содержащее текущую скорость автомобиля.

int weight; // Поле, содержащее вес автомобиля.

// Прочие поля...

void moveTo(int x, int y){

// Метод, моделирующий перемещение автомобиля в точку (x, y).

// Параметры метода x и y — уже не поля, а локальные переменные. int a = 1; // a — локальная переменная, а не поле.

// Тело метода. Здесь описывается способ перемещения автомобиля / / в точку (x, y)

}

// Прочие методы класса...

}

Знатокам Pascal

В Java нет вложенных процедур и функций, в теле метода нельзя описать другой метод.

После того как описание класса закончено, можно создавать конкретные объекты, называемые экземплярами (instances) описанного класса. Создание экземпляров производится в три этапа, подобно описанию массивов (см. главу 1). Сначала объявляются ссылки на объекты: записывается имя класса и после пробела через запятую перечисляются экземпляры класса, точнее, ссылки на них.

Automobile lada2110, fordScorpio, oka;

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

lada2110 = new Automobile(); fordScorpio = new Automobile(); oka = new Automobile();

На третьем этапе происходит инициализация объектов, задаются начальные значения. Этот этап, как правило, совмещается со вторым, именно для этого в операции new повторяется имя класса со скобками Automobile (). Это так называемый конструктор (constructor) класса, но о нем поговорим попозже.

Имена полей, методов и вложенных классов у всех объектов одного класса одинаковы, они заданы в описании класса. Поэтому имена надо уточнять именем ссылки на объект:

lada2110.maxVelocity = 150; fordScorpio.maxVelocity = 180; oka.maxVelocity = 350; // Почему бы и нет?

oka.moveTo(35, 120);

Напомним, что текстовая строка в кавычках понимается в Java как объект класса String. Поэтому можно написать

int strlen = "Это объект класса String".length();

Объект "строка" выполняет метод length(), один из методов своего класса String, подсчитывающий количество символов в строке. В результате получаем значение strlen, равное 24. Подобная странная запись встречается в программах, написанных на языке Java, на каждом шагу.

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

Иерархия

Не кажется ли вам, что класс Automobile сильно перегружен? Действительно, в мире выпущены миллионы автомобилей разных марок и видов. Что между ними общего, кроме четырех колес? Да и колес может быть больше или меньше. Не лучше ли написать отдельные классы для легковых и грузовых автомобилей, для гоночных автомобилей и вездеходов? Как организовать все это множество классов? На этот вопрос объектноориентированное программирование отвечает так: надо построить иерархию классов.

Иерархия объектов для их классификации используется давно. Особенно детально она проработана в биологии. Все знакомы с семействами, родами и видами. Мы можем сделать описание своих домашних животных (pets): кошек (cats), собак (dogs), коров (cows) и пр. следующим образом:

class Pet{ // Здесь описываем общие свойства всех домашних любимцев

Master person; // Хозяин животного

int weight, age, eatTime[]; // Вес, возраст, время кормления

int eat(int food, int drink, int time){ // Процесс кормления

// Начальные действия...

if (time == eatTime[i]) person.getFood(food, drink);

// Метод потребления пищи

}

void voice(); // Звуки, издаваемые животным

// Прочее...

}

Затем создаем классы, описывающие более конкретные объекты, связывая их с общим классом Pet:

class Cat extends Pet{ int mouseCatched; void toMouse();

// Прочие свойства

}

class Dog extends Pet{ void preserve();

// Описываются свойства, присущие только кошкам: // число пойманных мышей // процесс ловли мышей

// Свойства собак:

// охранять

Заметьте, что мы не повторяем общие свойства всех домашних животных, описанные в классе Pet. Они наследуются автоматически. Мы можем определить объект класса Dog и использовать в нем все свойства класса Pet так, как будто они описаны в классе Dog. Например, создаем объекты:

Dog tuzik = new Dog(), sharik = new Dog();

После этого определения можно будет написать:

tuzik.age = 3;

int p = sharik.eat(30, 10, 12);

А классификацию можно продолжить так:

class Pointer extends Dog{ ... } // Свойства породы пойнтер

class Setter extends Dog{ ... } // Свойства сеттеров

Заметьте, что на каждом следующем уровне иерархии в класс добавляются новые свойства, но ни одно свойство не пропадает. Поэтому и употребляется слово extends — "расширяет", которое сообщает, что класс Dog — расширение (extension) класса Pet. С другой стороны, количество объектов при этом уменьшается: собак меньше, чем всех домашних животных. Поэтому часто говорят, что класс Dog — подкласс (subclass) класса Pet, а класс Pet — суперкласс (superclass) или надкласс класса Dog.

Часто используют генеалогическую терминологию: родительский класс, дочерний класс, класс-потомок, класс-предок, возникают племянники и внуки, вся беспокойная семейка вступает в отношения, достойные мексиканского сериала.

В этой терминологии говорят о наследовании (inheritance) классов, в нашем примере можно сказать, что класс Dog наследует класс Pet.

Мы еще не определили счастливого владельца нашего домашнего зоопарка. Опишем его в классе Master. Сделаем набросок описания:

class Master{ // Хозяин животного

String name; // Фамилия, имя

// Другие сведения

void getFood(int food, int drink); // Кормление // Прочее...

}

Ответственность

Хозяин и его домашние животные постоянно соприкасаются в жизни. Их взаимодействие выражается глаголами "гулять", "кормить", "охранять", "чистить", "ласкаться", "проситься" и пр. Для описания взаимодействия объектов применяется третий принцип объектно-ориентированного программирования — обязанность или ответственность.

В нашем примере рассматривается только взаимодействие в процессе кормления, описываемое методом eat (). В этом методе животное обращается к хозяину, умоляя его применить метод getFood ( ).

В англоязычной литературе подобное обращение описывается словом message. Это понятие переведено на русский язык напрямую ни к чему не обязывающим словом "сообщение”. Лучше было бы использовать слово "послание", "поручение" или даже "распоряжение". Но термин "сообщение" устоялся и нам придется его применять. Почему же не используется словосочетание "вызов метода", ведь говорят: "Вызов процедуры"? Дело в том, что между этими понятиями есть по крайней мере три отличия.

□ Сообщение идет к конкретному объекту, знающему метод решения задачи. В примере этот объект — текущее значение переменной person. Объекты одного и того же класса отличаются друг от друга. У каждого объекта свое текущее состояние, свои значения полей класса, и это может повлиять на выполнение метода.

□ Способ выполнения поручения, содержащегося в сообщении, зависит от объекта, которому оно послано. В нашем примере этот объект — хозяин животного. Один хозяин поставит миску с Chappi, другой бросит кость, третий выгонит собаку на улицу. Это интересное свойство называется полиморфизмом (polymorphism) и будет обсуждаться далее.

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

Итак, объект sharik, выполняя свой метод eat (), посылает сообщение объекту, ссылка на который содержится в переменной person, с просьбой выдать ему определенное количество еды и питья. Сообщение записано в строке person.getFood(food, drink).

Этим сообщением заключается контракт (contract) между объектами, суть которого в том, что объект sharik берет на себя ответственность (responsibility) задать правильные параметры в сообщении, а другой объект — текущее значение экземпляра person — возлагает на себя ответственность применить метод кормления getFood(), каким бы он ни был.

Модульность

Для того чтобы правильно реализовать принцип ответственности, применяется четвертый принцип объектно-ориентированного программирования — модульность (modularity).

Этот принцип утверждает: каждый класс должен составлять отдельный модуль. Члены класса, к которым не планируется обращение извне, должны быть инкапсулированы.

В языке Java инкапсуляция достигается добавлением модификатора private к описанию члена класса. Например:

private int mouseCatched; private String name; private void preserve();

Эти члены классов становятся закрытыми, ими могут пользоваться только экземпляры того же самого класса, например tuzik может дать поручение sharik.preserve ().

А если в классе Master мы напишем

private void getFood(int food, int drink);

то метод getFood () не будет найден объектами других классов и несчастный sharik не сможет получить пищу.

В противоположность закрытости мы можем объявить некоторые члены класса открытыми, записав вместо слова private модификатор public, например:

public void getFood(int food, int drink);

К таким членам может обратиться любой объект любого класса.

Знатокам C++

В языке Java словами private, public и protected отмечается каждый член класса в отдельности.

Принцип модульности предписывает открывать члены класса только в случае необходимости. Вспомните надпись на железнодорожном переезде: "Нормальное положение шлагбаума — закрытое".

Если же надо обратиться к закрытому полю класса, то рекомендуется включить в класс специальные методы доступа (access methods), отдельно для чтения этого поля (get method) и отдельно для записи в это поле (set method). Имена методов доступа рекомендуется начинать со слов get и set, добавляя к этим словам имя поля. Для классов Java, используемых как компоненты большого приложения (такие классы-компоненты в технологии Java названы JavaBeans), эти рекомендации возведены в ранг закона.

В нашем примере класса Master методы доступа к полю name в самом простом виде могут выглядеть так:

public String getName(){ return name;

}

public void setName(String newName){ name = newName;

}

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

Кроме методов доступа рекомендуется создавать проверочные is-методы, возвращающие логическое значение true или false. Например, в класс Master можно включить метод, проверяющий, задано ли имя хозяина:

public boolean isEmpty(){ return name == null;

}

и использовать этот метод для проверки при доступе к полю name, например: if (master01.isEmpty()) master01.setName("Иванов");

Итак, мы оставляем открытыми только методы, необходимые для взаимодействия объектов. При этом удобно спланировать классы так, чтобы зависимость между ними была наименьшей, как принято говорить в теории ООП, было наименьшее зацепление (low coupling) между классами. Тогда структура программы сильно упрощается. Кроме того, такие классы удобно использовать как строительные блоки для создания других программ.

Напротив, члены класса должны активно взаимодействовать друг с другом, как говорят, иметь тесную функциональную связность (high cohesion). Для этого в класс следует включать все методы, описывающие поведение моделируемого объекта, и только такие методы, ничего лишнего. Одно из правил достижения сильной функциональной связности, введенное Карлом Либерхером (Karl J. Lieberherr), получило название закона Деметра. Закон гласит: "В методе m () класса а следует использовать только методы класса а, методы классов, к которым принадлежат параметры метода m(), и методы классов, экземпляры которых создаются внутри метода m()".

Объекты, построенные по этим правилам, подобны кораблям, снабженным всем необходимым. Они уходят в автономное плавание, готовые выполнить любое поручение, на которое рассчитана их конструкция.

Будут ли закрытые члены класса доступны его наследникам? Если в классе Pet написано

private Master person;

то можно ли использовать sharik.person? Разумеется, нет. Ведь в противном случае каждый, интересующийся закрытыми полями класса а, может расширить его классом в и просмотреть закрытые поля класса а через экземпляры класса в.

Когда надо разрешить доступ наследникам класса, но нежелательно открывать его всему миру, тогда в Java используется защищенный (protected) доступ, отмечаемый модификатором protected, например объект sharik может обратиться к полю person родительского класса Pet, если в классе Pet это поле описано так:

protected Master person;

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

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

Но, говоря о принципах ООП, я не могу удержаться от того, чтобы не напомнить основной принцип всякого программирования.

Принцип KISS

Основной, базовый и самый великий принцип программирования на любом языке и при любой парадигме — принцип KISS — не нуждается в переводе. Он расшифровывается так:

"Keep It Simple, Stupid!"

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

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

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

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

Упражнения

1. Опишите в виде объекта строительный подъемный кран.

2. Опишите в виде объекта игровой автомат.

3. Смоделируйте в виде объекта сотовый телефон.

Как описать класс и подкласс?

Итак, описание класса начинается со слова class, после которого записывается имя класса. Соглашения "Code Conventions" рекомендуют начинать имя класса с заглавной буквы.

Перед словом class можно записать модификаторы класса (class modifiers). Это одно из слов public, abstract, final, strictfp. Перед именем вложенного класса можно поставить также модификаторы protected, private, static. Модификаторы класса мы будем вводить по мере изучения языка.

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

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

Описание поля может начинаться с одного или нескольких необязательных модификаторов public, protected, private, static, final, transient, volatile. Если надо поставить несколько модификаторов, то перечислять их JLS рекомендует в указанном порядке, поскольку некоторые компиляторы требуют определенного порядка записи модификаторов. С модификаторами мы будем знакомиться по мере необходимости.

При описании метода указывается тип возвращаемого им значения или слово void, затем, через пробел, имя метода, потом, в скобках, список параметров. После этого в фигурных скобках расписывается выполняемый метод.

Описание метода может начинаться с модификаторов public, protected, private, abstract,

static, final, synchronized, native, strictfp. Мы будем вводить их по необходимости.

В списке параметров через запятую перечисляются тип и имя каждого параметра. Перед типом какого-либо параметра может стоять модификатор final. Такой параметр нельзя изменять внутри метода. Список параметров может отсутствовать, но скобки сохраняются.

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

В листинге 2.1 показано, как можно оформить метод деления пополам для нахождения корня нелинейного уравнения из листинга 1.5.

Листинг 2.1. Нахождение корня нелинейного уравнения методом бисекции

class Bisection2{

private static double final EPS = 1e-8; // Константа класса.

private double a = 0.0, b = 1.5, root; // Закрытые поля экземпляра.

public double getRoot(){return root;} // Метод доступа к полю root.

private double f(double x){

return x*x*x — 3*x*x + 3; // Можно вернуть и что-нибудь другое.

}

private void bisect(){ // Параметров у метода нет —

// метод работает с полями экземпляра. double y = 0.0; // Локальная переменная — не поле.

do{

root = 0.5 *(a + b); y = f(root);

if (Math.abs(y) < EPS) break;

// Корень найден. Выходим из цикла.

// Если на концах отрезка [a; root] функция имеет разные знаки: if (f(a) * y < 0.0) b = root;

// значит, корень здесь, и мы переносим точку b в точку root.

// В противном случае: else a = root;

// переносим точку a в точку root

// Продолжаем до тех пор, пока [a; b] не станет мал.

} while(Math.abs(b-a) >= EPS);

}

public static void main(String[] args){

Bisection2 b2 = new Bisection2(); b2.bisect();

System.out.println("x = " +

b2.getRoot() + // Обращаемся к корню через метод доступа.

", f() = " +b2.f(b2.getRoot()));

}

}

В описании метода f () сохранен старый процедурный стиль: метод получает аргумент, скопированный в параметр x, обрабатывает его и возвращает результат. Описание метода bisect () выполнено в духе ООП: метод активен, он сам обращается к полям экземпляра b2 и сам заносит результат в нужное поле. Метод bisect () — это внутренний механизм класса Bisection2, поэтому он закрыт (private).

Передача аргументов в метод

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

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

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

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

В языке Java, как и в языке С, реализован только один способ — передача аргументов по значению. Например, выполнив следующую программу:

class Dummy1{

private static void f(int a){ a = 5;

}

public static void main(String[] args){

int x = 7;

System.out.println(,,До: " + x); f(x);

System.out.println("После: " + x);

}

}

вы увидите значение 7 и до и после выполнения метода f(), потому что он менял локальную переменную a, а не переменную-аргумент x.

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

class Dummy2{

private static void f(int[] a){ a[0] = 5;

}

public static void main(String[] args){ int[] x = {7};

System.out.println("До: " + x[0]); f (x);

System.out.println("После: " + x[0]);

}

}

Теперь переменная x — это ссылка на массив, которая копируется в локальную переменную, созданную для параметра a. Ссылка a направляется на тот же массив, что и ссылка x. Она меняет нулевой элемент массива, и мы получаем "До: 7", "После: 5". По-прежнему сделана передача аргумента по значению, но теперь аргумент — это ссылка, и в метод f () передается ссылка, а не объект, на который она направлена.

Передача ссылок по значению приводит иногда к неожиданным результатам. В следующем примере:

class Dummy3{

private static void f(int[] a){ a = new int[]{5};

}

public static void main(String[] args){ int[] x = {7};

System.out.println("До: " + x[0]); f (x);

System.out.println("После: " + x[0]);

}

}

мы опять оба раза увидим на экране число 7. Хотя теперь в методе f() изменилась ссылка на массив — параметр этого метода, а не сам массив, но изменилась копия a ссылки x, а не она сама. Копия a получила новое значение, она направлена на новый массив {5}, но сама ссылка x осталась прежней, она по-прежнему направлена на массив {7}.

Знатокам Pascal и C++

В языке Java применяется только передача аргументов по значению.

Перегрузка методов

Имя метода, число и типы параметров образуют сигнатуру (signature) метода. Компилятор различает методы не по их именам, а по сигнатурам. Это позволяет записывать разные методы с одинаковыми именами, различающиеся числом и/или типами параметров.

Замечание

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

Например, в классе Automobile мы записали метод moveTo (int x, int y), обозначив пункт назначения его географическими координатами. Можно определить еще метод moveTo(String destination) для указания географического названия пункта назначения и обращаться к нему так:

oka.moveTo("РњРѕСЃРєРІР°");

Такое дублирование методов называется их перегрузкой (overloading). Перегрузка методов очень удобна в использовании. Вспомните, в главе 1 мы выводили данные любого типа на экран методом println(), не заботясь о том, данные какого именно типа мы выводим. На самом деле мы использовали разные методы с одним и тем же именем println, даже не задумываясь об этом. Конечно, все эти методы надо тщательно спланировать и заранее описать в классе. Это и сделано в классе Printstream, где представлено около двадцати методов print () и println ().

Переопределение методов

Если же записать метод в подклассе с тем же именем, параметрами и типом возвращаемого значения, что и в суперклассе, например:

class Truck extends Automobile{ void moveTo(int x, int y){

// Какие-то действия...

}

// Что-то еще, содержащееся в классе Truck...

}

то он перекроет метод суперкласса.

Определив экземпляр класса Truck, например:

Truck gazel = new Truck();

и записав gazel.moveTo(25, 150), мы обратимся к методу класса Truck. Произойдет переопределение (overriding) метода.

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

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

При переопределении метода права доступа к нему можно только расширить, но не сузить. Открытый метод public должен остаться открытым, защищенный protected может стать открытым, но не может стать закрытым.

Можно ли внутри подкласса обратиться к методу суперкласса? Да, можно, если уточнить имя метода словом super, например super.moveTo(30, 40). Можно уточнить и имя метода, записанного в этом же классе, словом this, например this.moveTo(50, 70), но в данном случае это уже излишне. Таким же образом можно уточнять и совпадающие имена полей, а не только методов.

Данные уточнения подобны тому, как мы говорим про себя "я", а не "Иван Петрович", и говорим "отец", а не "Петр Сидорович".

Реализация полиморфизма в Java

Переопределение методов приводит к интересным результатам. В классе Pet мы описали метод voice (). Переопределим его в подклассах и используем в классе Chorus, как показано в листинге 2.2.

Листинг 2.2. Пример полиморфного метода

abstract class Pet{

abstract void voice();

}

class Dog extends Pet{ int k = 10;

В©Override void voice(){

System.out.println("Gav-gav!");

}

}

class Cat extends Pet{

В©Override void voice(){

System.out.println("Miaou!");

}

}

class Cow extends Pet{

В©Override void voice(){

System.out.println("Mu-u-u!");

}

}

public class Chorus{

public static void main(String[] args){ Pet[] singer = new Pet[3]; singer[0] = new Dog(); singer[1] = new Cat(); singer[2] = new Cow();

for (Pet p: singer) p.voice();

}

}

На рис. 2.1 показан вывод этой программы. Животные поют своими голосами!

Рис. 2.1. Результат выполнения программы Chorus

Все дело здесь в определении поля singer [ ]. Хотя массив ссылок singer [ ] имеет тип Pet, каждый его элемент ссылается на объект своего типа: Dog, Cat, Cow. При выполнении программы вызывается метод конкретного объекта, а не метод класса, которым определялось имя ссылки. Так в Java реализуется полиморфизм.

Знатокам C++

В языке Java все методы являются виртуальными функциями.

Внимательный читатель заметил в описании класса Pet новое слово abstract. Класс Pet и метод voice () являются абстрактными.

Упражнения

4. Опишите в виде класса строительный подъемный кран.

5. Опишите в виде класса игровой автомат.

6. Смоделируйте в виде класса сотовый телефон.

Абстрактные методы и классы

При описании класса Pet мы не можем задать в методе voice () никакой полезный алгоритм, поскольку у всех животных совершенно разные голоса.

В таких случаях мы записываем только заголовок метода и ставим после закрывающей список параметров скобки точку с запятой. Этот метод будет абстрактным (abstract),

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

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

Как же использовать абстрактные классы? Только порождая от них подклассы, в которых переопределены абстрактные методы.

Зачем же нужны абстрактные классы? Не лучше ли сразу написать необходимые классы с полностью определенными методами, а не наследовать их от абстрактного класса? Для ответа снова обратимся к листингу 2.2.

Хотя элементы массива singer[] ссылаются на подклассы Dog, Cat, Cow, но все-таки это переменные типа Pet и ссылаться они могут только на поля и методы, описанные в суперклассе Pet. Дополнительные поля подкласса для них недоступны. Попробуйте обратиться, например, к полю k класса Dog, написав singer[0] .k. Компилятор "скажет", что он не может найти такое поле. Поэтому метод, который реализуется в нескольких подклассах, приходится выносить в суперкласс, а если там его нельзя реализовать, то объявить абстрактным. Таким образом, абстрактные классы группируются на вершине иерархии классов.

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

void voice(){}

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

Замкнуть же иерархию можно окончательными классами.

Окончательные члены и классы

Пометив метод модификатором final, можно запретить его переопределение в подклассах. Это удобно в целях безопасности. Вы можете быть уверены, что метод выполняет те действия, которые вы задали. Именно так определены математические функции sin ( ), cos ( ) и пр. в классе Math. Мы уверены, что метод Math.cos(x) вычисляет именно косинус числа х. Разумеется, такой метод не может быть абстрактным.

Для полной безопасности поля, обрабатываемые окончательными методами, следует сделать закрытыми (private).

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

Если же пометить модификатором final весь класс, то его вообще нельзя будет расширить. Так определен, например, класс Math:

public final class Math{ . . . }

Для переменных модификатор final имеет совершенно другой смысл. Если пометить модификатором final описание переменной, то ее значение (а оно должно быть обязательно задано или здесь же, или в блоке инициализации, или в конструкторе) нельзя изменить ни в подклассах, ни в самом классе. Переменная превращается в константу. Именно так в языке Java определяются константы:

public final int MIN_VALUE = -1, MAX_VALUE = 9999;

По соглашению "Code Conventions" константы записываются прописными буквами, слова в них разделяются знаком подчеркивания.

Класс Object

На самой вершине иерархии классов Java стоит класс Object.

Если при описании класса мы не указываем никакое расширение, т. е. не пишем слово extends и имя класса за ним, как при описании класса Pet, то Java считает этот класс расширением класса Object, и компилятор дописывает это за нас:

class Pet extends Object{ . . . }

Можно записать это расширение и явно.

Сам же класс Obj ect не является ничьим наследником, от него начинается иерархия любых классов Java. В частности, все массивы — прямые наследники класса Object.

Поскольку такой класс может содержать только общие свойства всех классов, в него включено лишь несколько самых общих методов, например метод equals(), сравнивающий данный объект на равенство с объектом, заданным в аргументе, и возвращающий логическое значение. Его можно использовать так:

Object objl = new Dog(), obj2 = new Cat(); if (obj1.equals(obj2)) ...

Оцените объектно-ориентированный дух этой записи: объект obj 1 активен, он сам сравнивает себя с другим объектом. Можно, конечно, записать и obj2.equals(obji), сделав активным объект obj 2, с тем же результатом.

Как указывалось в главе 1, ссылки можно сравнивать на равенство и неравенство:

obj 1 == obj 2; obj 1 ! = obj 2;

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

Метод equals () же сравнивает содержимое объектов в их текущем состоянии, фактически он реализован в классе Object как тождество: объект равен только самому себе. Поэтому его обычно переопределяют в подклассах; более того, правильно спроектированные, "хорошо воспитанные" классы должны переопределить методы класса Obj ect, если их не устраивает стандартная реализация. Например, в классе String метод equals () сравнивает не адреса размещения строк в оперативной памяти, а символы, из которых состоит строка, как мы увидим в главе 5.

Второй метод класса Object, часто требующий переопределения,- метод hashCode( ) —

возвращает целое число, уникальное для каждого объекта данного класса, его идентификатор. Это число позволяет однозначно определить объект. Оно используется многими стандартными классами Java. Реализация метода hashCode (), сделанная в классе Obj ect, может оказаться недостаточной для какого-то подкласса. В таком случае метод hashCode () следует переопределить.

Третий метод класса Object, который следует переопределять в подклассах, — метод tostring (). Это метод без параметров, который выражает содержимое объекта строкой символов и возвращает объект класса string. В классе Object метод tostring() реализован очень скудно — он выдает имя класса и идентификатор объекта, возвращаемый методом hashCode (). Метод tostring() важен потому, что исполняющая система Java обращается к нему каждый раз, когда требуется представить объект в виде строки, например в методе println(). Обычно метод tostring() переопределяют так, чтобы он возвращал информацию о классе объекта и текущие значения его полей, записанные в виде строк символов.

Конструкторы класса

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

Такой "метод" называется конструктором класса (class constructor). Его задача — определение полей создаваемого объекта начальными значениями. Своеобразие конструктора заключается не только в имени. Перечислим особенности конструктора.

□ Конструктор имеется в любом классе. Даже если вы его не написали, компилятор Java сам создаст конструктор по умолчанию (default constructor), который, впрочем, пуст, он не делает ничего, кроме вызова аналогичного конструктора по умолчанию суперкласса.

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

□ Конструктор не возвращает никакого значения. Поэтому в его описании не пишется даже слово void, но можно задать один из трех модификаторов: public, protected или

private.

□ Конструктор не является методом, он даже не считается членом класса. Поэтому его нельзя наследовать или переопределить в подклассе.

□ Тело конструктора может начинаться:

• с вызова одного из конструкторов суперкласса, для этого записывается слово super () с параметрами конструктора суперкласса в скобках, если они нужны;

• с вызова другого конструктора того же класса, для этого записывается слово this () с параметрами в скобках, если они нужны.

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

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

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

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

В наших примерах мы пока ни разу не рассматривали конструкторы классов, поэтому при создании экземпляров наших классов вызывался конструктор класса Object.

Операция new

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

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

double a[] = new double[100];

Элементы массива обнуляются.

Во втором случае операндом служит конструктор класса. Если конструктора в классе нет, то вызывается конструктор по умолчанию.

Числовые поля класса получают нулевые значения, логические поля — значение false, ссылки — значение null.

Результатом операции new будет ссылка на созданный объект. Эта ссылка может быть присвоена переменной типа "ссылка" на данный тип:

Dog k9 = new Dog();

но может использоваться и непосредственно:

new Dog().voice();

Здесь после создания безымянного объекта сразу выполняется его метод voice (). Такая странная запись встречается в программах, написанных на Java, на каждом шагу. Она возможна потому, что приоритет операции new выше, чем приоритет операции обращения к методу, обозначаемой точкой.

Упражнение

7. Введите конструкторы в классы, определенные в упражнениях 4—6.

Статические члены класса

Разные экземпляры одного класса имеют совершенно независимые друг от друга поля, принимающие различные значения. Изменение поля в одном экземпляре никак не влияет на то же поле в другом экземпляре. В каждом экземпляре для таких полей выделяется своя ячейка памяти. Поэтому такие поля называются переменными экземпляра класса (instance variables) или переменными объекта.

Иногда надо определить поле, общее для всего класса, изменение которого в одном экземпляре повлечет изменение того же поля во всех экземплярах. Например, мы хотим в классе Automobile отмечать порядковый заводской номер автомобиля. Такие поля называются переменными класса (class variables). Для переменных класса выделяется только одна ячейка памяти, общая для всех экземпляров. Переменные класса образуются в Java модификатором static. В листинге 2.3 мы записываем этот модификатор при определении переменной number.

Листинг 2.3. Статическое поле

class Automobile!

private static int number;

Automobile(){ number++;

system.out.println("From Automobile constructor:" + " number = " + number);

}

}

public class AutomobileTest{

public static void main(string[] args){

Automobile lada2105 = new Automobile(), fordscorpio = new Automobile(), oka = new Automobile();

}

}

Получаем результат, показанный на рис. 2.2.

Рис. 2.2. Изменение статического поля

Интересно, что к статическим полям можно обращаться с именем класса, Automobile. number, а не только с именем экземпляра, lada2105. number, причем это можно делать, даже если не создан ни один экземпляр класса. Дело в том, что поля класса определяются при загрузке файла с классом в оперативную память, еще до создания экземпляров класса.

Для работы с такими статическими полями обычно создаются статические методы, помеченные модификатором static. Для методов слово static имеет совсем другой смысл, чем для полей, потому что исполняющая система Java всегда создает в памяти только одну копию машинного кода метода, разделяемую всеми экземплярами, независимо от того, статический это метод или нет.

Основная особенность статических методов — они работают напрямую только со статическими полями и методами класса. При попытке обращения статического метода к нестатическим полям и методам класса на консоль посылается сообщение об ошибке. Это позволяет повысить безопасность программы, предотвратить случайное изменение полей и методов отдельных экземпляров класса.

Такая особенность статических методов приводит к интересному побочному эффекту. Они могут выполняться, даже если не создан ни один экземпляр класса. Достаточно уточнить имя метода именем класса (а не именем объекта), чтобы метод мог работать. Именно так мы пользовались методами класса Math, не создавая его экземпляры, а просто записывая

Math.abs (x), Math. sqrt (x). Точно так же мы использовали метод system.out .println ( ) . Да и методом main () мы пользуемся, вообще не создавая никаких объектов.

Поэтому статические методы называются методами класса (class methods), в отличие от нестатических методов, называемых методами экземпляра (instance methods).

Отсюда вытекают другие особенности статических методов:

□ в статическом методе нельзя использовать ссылки this и super;

□ статические методы не могут быть абстрактными;

□ статические методы переопределяются в подклассах только как статические;

□ при переопределении статических методов полиморфизм не действует, ссылки всегда направляются на методы класса, а не объекта.

Именно по этим причинам в листинге 1.5 мы пометили метод f() модификатором static. Но в листинге 2.1 мы работали с экземпляром b2 класса Bisection2, и нам не потребовалось объявлять метод f () статическим.

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

static int[] a = new a[10]; static{

for (int k: a) a[k] = k * k;

}

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

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

 

Класс Complex

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

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

Листинг 2.4 длинный, но просмотрите его внимательно, при обучении языку программирования очень полезно чтение программ на этом языке. Более того, только программы и стоит читать, пояснения автора лишь мешают вникнуть в смысл действий (шутка).

Листинг 2.4. Класс Complex

class Complex{

private static final double EPs = 1e-12; // Точность вычислений. private double re, im; // Действительная и мнимая части.

// Четыре конструктора:

Complex(double re, double im){ this.re = re; this.im = im;

}

Complex(double re){this(re, 0.0);}

Complex(){this(0.0, 0.0);}

Complex(Complex z){this(z.re, z.im);}

// Методы доступа: public double getRe(){return re;} public double getIm(){return im;}

public Complex getZ(){return new Complex(re, im);} public void setRe(double re){this.re = re;} public void setIm(double im){this.im = im;} public void setZ(Complex z){re = z.re; im = z.im;}

// Модуль и аргумент комплексного числа: public double mod(){return Math.sqrt(re * re + im * im);} public double arg(){return Math.atan2(re, im);}

// Проверка: действительное число? public boolean isReal(){return Math.abs(im) < EPs;} public void pr(){ // Вывод на экран

system.out.println(re + (im < 0.0 ? "" : "+") + im + "i");

}

// Переопределение методов класса Object: public boolean equals(Complex z){ return Math.abs(re — z.re) < EPs &&

Math.abs(im — z.im) < EPs;

}

public string tostring(){

return "Complex: " + re + " " + im;

}

// Методы, реализующие операции +=, -=, *=, /= public void add(Complex z){re += z.re; im += z.im;} public void sub(Complex z){re -= z.re; im -= z.im;} public void mul(Complex z){

double t = re * z.re — im * z.im; im = re * z.im + im * z.re; re = t;

public void div(Complex z){

double m = z.re * z.re + z.im * z.im; double t = re * z.re — im * z.im; im = (im * z.re — re * z.im) / m; re = t / m;

}

// Методы, реализующие операции +, -, *, /

public Complex plus(Complex z){

return new Complex(re + z.re, im + z.im);

}

public Complex minus(Complex z){

return new Complex(re — z.re, im — z.im);

}

public Complex asterisk(Complex z){ return new Complex(

re * z.re — im * z.im, re * z.im + im * z.re);

}

public Complex slash(Complex z){

double m = z.re * z.re + z.im * z.im; return new Complex(

(re * z.re — im * z.im) / m, (im * z.re — re * z.im) / m);

}

}

// Проверим работу класса Complex. public class ComplexTest{

public static void main(string[] args){ Complex z1 = new Complex(),

z2 = new Complex(1.5),

z3 = new Complex(3.6, -2.2),

z4 = new Complex(z3);

// Оставляем пустую строку. "); z1.pr();

"); z2.pr();

"); z3.pr();

"); z4.pr();

// Работает метод toString().

System.out.println(); system.out.print("z1 system.out.print("z2 system.out.print("z3 system.out.print("z4 System.out.println(z4); z2.add(z3);

'); z2.pr(); '); z2.pr(); '); z2.pr(); '); z3.pr();

system.out.print("z2 + z3 z2.div(z3);

system.out.print("z2 / z3 z2 = z2.plus(z2); system.out.print("z2 + z2 z3 = z2.slash(z1); system.out.print("z2 / z1

}

На рис. 2.3 показан вывод этой программы.

Рис. 2.3. Вывод программы ComplexTest

Метод main()

Всякая программа, оформленная как приложение (application), должна содержать метод с именем main. Он может быть один на все приложение или присутствовать в некоторых классах этого приложения, а может находиться и в каждом классе.

Метод main () записывается как обычный метод, может содержать любые описания и действия, но он обязательно должен быть открытым (public), статическим (static), не иметь возвращаемого значения (void). У него один параметр, которым обязательно должен быть массив строк (string [ ]). По традиции этот массив называют args, хотя имя может быть любым.

Эти особенности возникают из-за того, что метод main() вызывается автоматически исполняющей системой Java в самом начале выполнения приложения, когда еще не создан ни один объект. При вызове интерпретатора java указывается класс, где записан метод main (), с которого надо начать выполнение. Поскольку классов с методом main() может быть несколько, допустимо построить приложение с дополнительными точками входа, начиная выполнение приложения в разных ситуациях из различных классов.

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

При вызове интерпретатора java можно передать в метод main() несколько аргументов, которые интерпретатор заносит в массив строк. Эти аргументы перечисляются в строке вызова java через пробел сразу после имени класса. Если же аргумент содержит пробелы, надо заключить его в кавычки. Кавычки не будут включены в аргумент, это только ограничители.

Все это легко понять на примере листинга 2.5, в котором записана программа, просто выводящая на консоль аргументы, передаваемые в метод main () при запуске.

Листинг 2.5. Передача аргументов в метод main()

class Echo{

public static void main(string[] args){ for (string s: args)

system.out.println("arg = " + s);

}

}

На рис. 2.4 показаны результаты работы этой программы с разными вариантами задания аргументов.

Рис. 2.4. Вывод параметров командной строки

Как видите, имя класса не входит в число аргументов. Оно и так известно в методе

main().

Знатокам C/C++

Поскольку в Java имя файла всегда совпадает с именем класса, содержащего метод main (), оно не заносится в args [0]. Вместо параметра argc используется переменная args. length, имеющаяся в каждом массиве. Доступ к переменным среды разрешен не всегда и осуществляется другим способом. Некоторые значения переменных среды можно просмотреть так:

system.getProperties().list(system.out);

Методы с переменным числом аргументов

Как видно из рис. 2.4, при вызове программы из командной строки мы можем задавать ей разное число аргументов. Исполняющая система Java создает массив этих аргументов и передает его методу main(). Такую же конструкцию можно сделать в своей программе:

class VarArgs{

private static int[] argsl = {1, 2, 3, 4, 5, 6};

private static int[] args2 = {100, 90, 80, 70};

public static int sum(int[] args){ int result = 0;

for (int k: args) result += k; return result;

}

public static void main(string[] args){

System.out.println("Sum1 = " + sum(args1));

System.out.println("Sum2 = " + sum(args2));

}

}

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

public static int sum(int... args){ int result = 0;

for (int k: args) result += k; return result;

}

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

public static void main(string[] args){

System.out.println("Sum1 = " + sum(1, 2, 3, 4, 5, 6));

System.out.println("Sum2 = " + sum(100, 90, 80, 70));

}

Где видны переменные

В языке Java нестатические переменные разрешено объявлять в любом месте кода между операторами. Статические переменные могут быть только полями класса, а значит, не должны объявляться внутри методов и блоков. Какова же область видимости (scope) переменных? Из каких методов мы можем обратиться к той или иной переменной? В каких операторах использовать? Рассмотрим на примере листинга 2.6 разные случаи объявления переменных.

Листинг 2.6. Видимость и инициализация переменных

class ManyVariables{

static int x = 9, y; // Статические переменные — поля класса.

// Они известны во всех методах и блоках класса. // Поле y получает значение 0.

static{ // Блок инициализации статических переменных.

// Выполняется один раз при первой загрузке класса // после инициализаций в объявлениях переменных. x = 99; // Этот оператор выполняется в блоке вне всякого метода!

int a = 1, p; // Нестатические переменные — поля экземпляра.

// Известны во всех методах и блоках класса,

// в которых они не перекрыты другими переменными // с тем же именем.

// Поле p получает значение 0.

{ // Блок инициализации экземпляра.

// Выполняется при создании каждого экземпляра // после инициализаций при объявлениях переменных. p = 999; // Этот оператор выполняется в блоке вне всякого метода!

}

static void f(int b){ // Параметр метода b — локальная переменная,

// известная только внутри метода. int a = 2; // Это вторая переменная с тем же именем "a".

// Она известна только внутри метода f()

// и здесь перекрывает первую "a".

int c; // Локальная переменная, известна только в методе f().

// Не получает никакого начального значения // и должна быть определена перед применением.

{ int c = 555; // Сшибка! Попытка повторного объявления.

int x = 333; // Локальная переменная, известна только в этом блоке.

}

// Здесь переменная x уже неизвестна. for (int d = 0; d < 10; d++){

// Переменная цикла d известна только в цикле. int a = 4; // Ошибка!

int e = 5; // Локальная переменная, известна только в цикле for.

e++; // Инициализируется при каждом выполнении цикла.

System.out.println("e = " + e); // Выводится всегда "e = 6".

}

// Здесь переменные d и e неизвестны.

}

public static void main(string[] args){

int a = 9999; // Локальная переменная, известна только внутри

// метода main().

f (a) ;

}

}

Обратите внимание на то, что переменные класса и экземпляра неявно присваивают нулевые значения. Символы неявно получают значение ’ \u0000 ’, логические переменные — значение false, ссылки получают неявно значение null.

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

Внимание!

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

В листинге 2.6 появилась еще одна новая конструкция: блок инициализации экземпляра (instance initialization). Это просто блок операторов в фигурных скобках, но записывается он вне всякого метода, прямо в теле класса. Этот блок выполняется при создании каждого экземпляра, после static-блоков и инициализации при объявлении переменных, но до выполнения конструктора. Он играет такую же роль, как и static-блок для статических переменных. Зачем же он нужен, ведь все его содержимое можно написать в начале конструктора? Он применяется в тех случаях, когда конструктор написать нельзя, а именно в безымянных внутренних классах.

Вложенные классы

В этой главе уже несколько раз упоминалось, что в теле класса можно сделать описание другого, вложенного (nested) класса. А во вложенном классе можно снова описать вложенный, внутренний (inner) класс и т. д. Эта "матрешка" кажется вполне естественной, но вы уже поднаторели в написании классов, и у вас возникает масса вопросов.

□ Можем ли мы из вложенного класса обратиться к членам внешнего класса? Можем, для того это все и задумывалось.

□ А можем ли мы в таком случае определить экземпляр вложенного класса, не определяя экземпляры внешнего класса? Нет, не можем, сначала надо определить хоть один экземпляр внешнего класса, матрешка ведь!

□ А если экземпляров внешнего класса несколько, как узнать, с каким экземпляром внешнего класса работает данный экземпляр вложенного класса? Имя экземпляра вложенного класса уточняется именем связанного с ним экземпляра внешнего класса. Более того, при создании вложенного экземпляра операция new тоже уточняется именем внешнего экземпляра.

в–Ў Рђ?..

Хватит вопросов, давайте разберем все по порядку.

Все вложенные классы можно разделить на вложенные классы-члены класса (member classes), описанные вне методов, и вложенные локальные классы (local classes), описанные внутри методов и/или блоков. Локальные классы, как и все локальные переменные, не являются членами класса.

Классы-члены могут быть объявлены статическими с помощью модификатора static. Поведение статических классов-членов ничем не отличается от поведения обычных классов, отличается только обращение к таким классам. Поэтому они называются вложенными классами верхнего уровня (nested top-level classes), хотя статические классы-члены можно вкладывать друг в друга. В них можно объявлять статические члены. Используются они обычно для того, чтобы сгруппировать вспомогательные классы вместе с основным классом.

Все нестатические вложенные классы называются внутренними (inner). В них нельзя объявлять статические члены.

Локальные классы, как и все локальные переменные, известны только в блоке, в котором они определены. Они могут быть безымянными (anonymous classes).

В листинге 2.7 рассмотрены все эти случаи.

Листинг 2.7. Вложенные классы

class Nested{

static private int pr; // Переменная pr объявлена статической,

// чтобы к ней был доступ из статических классов A и AB.

String s = "Member of Nested";

// Вкладываем статический класс.

static class A{ // Полное имя этого класса — Nested.A

private int a = pr;

String s = "Member of A";

// Во вложенный класс A вкладываем еще один статический класс. static class AB{ // Полное имя класса — Nested.A.AB

private int ab = pr;

String s = "Member of AB";

}

}

// В класс Nested вкладываем нестатический класс. class B{ // Полное имя этого класса — Nested.B

private int b = pr;

String s = "Member of B";

// В класс B вкладываем еще один класс.

class BC{ // Полное имя класса — Nested.B.BC

private int bc = pr;

String s = "Member of BC";

}

void f(final int i){ // Без слова final переменные i и j

// нельзя использовать в локальном классе D.

final int j = 99;

class D{ // Локальный класс D известен только внутри f().

private int d = pr;

String s = "Meimoer of D"; void pr(){

// Обратите внимание на то, как различаются // переменные с одним и тем же именем "s". System.out.println(s + (i+j)); // "s" эквивалентно "this.s".

System.out.println(B.this.s);

System.out.println(Nested.this.s);

// System.out.println(AB.this.s); // Нет доступа.

// System.out.println(A.this.s); // Нет доступа.

}

}

D d = new D(); // Объект определяется тут же, в методе f().

d.pr(); // Объект известен только в методе f().

}

}

void m(){

new Object(){ // Создается объект безымянного класса,

// указывается конструктор его суперкласса.

private int e = pr; void g(){

System.out.println("From g()");

}

}.g(); // Тут же выполняется метод только что созданного объекта.

}

}

public class NestedClasses{

public static void main(String[] args){

Nested nest = new Nested(); // Последовательно раскрываются

// три матрешки.

Nested.A theA = nest.new A(); // Полное имя класса и уточненная

// операция new. Но конструктор только вложенного класса.

Nested.A.AB theAB = theA.new AB(); // Те же правила.

// Операция new уточняется только одним именем.

Nested.B theB = nest.new B(); // Еще одна матрешка.

Nested.B.BC theBC = theB.new BC();

theB.f(999); // Методы вызываются обычным образом.

nest.m();

}

}

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

Теперь дадим пояснения.

□ Как видите, доступ к полям внешнего класса Nested возможен отовсюду, даже к закрытому полю pr. Именно для этого в Java и введены вложенные классы. Остальные конструкции добавлены вынужденно, для того чтобы увязать концы с концами.

□ Язык Java позволяет использовать одни и те же имена в разных областях видимости -поэтому пришлось уточнять константу this именем класса: Nested.this, B.this.

□ В безымянном классе не может быть конструктора, ведь имя конструктора должно совпадать с именем класса, — поэтому пришлось использовать имя суперкласса, в примере это класс Object. Вместо конструктора в безымянном классе используется блок инициализации экземпляра, о котором говорилось в предыдущем разделе.

□ Нельзя создать экземпляр вложенного класса, не создав предварительно экземпляр внешнего класса, — поэтому пришлось подстраховать это правило уточнением операции new именем экземпляра внешнего класса nest. new, theA. new, theB. new.

□ При определении экземпляра указывается полное имя вложенного класса, но в операции new записывается просто конструктор класса.

Введение вложенных классов сильно усложнило синтаксис и поставило много задач разработчикам языка. Это еще не все. Дотошный читатель уже зарядил новую обойму вопросов.

□ Можно ли наследовать вложенные классы? Можно.

□ Как из подкласса обратиться к методу суперкласса? Константа super уточняется именем соответствующего суперкласса, подобно константе this.

□ А могут ли вложенные классы быть расширениями других классов? Могут.

□ А как?.. Помните принцип KISS!!!

Механизм вложенных классов станет понятнее, если посмотреть, какие файлы с байткодами создал компилятор:

□ Nested$1$D.class — локальный класс D, вложенный в класс Nested;

□ Nested$1.class — безымянный класс;

□ Nested$A$AB.class — класс Nested.A.AB;

□ Nested$A.class — класс Nested.A;

□ Nested$B$BC.class — класс Nested.B.BC;

□ Nested$B.class — класс Nested.B;

□ Nested.class — внешний класс Nested;

□ NestedClasses.class — класс с методом main ().

Компилятор разложил "матрешки" и, как всегда, создал отдельные файлы для каждого класса. При этом, поскольку в идентификаторах недопустимы точки, компилятор заменил их знаками доллара. Для безымянного класса компилятор придумал имя. Локальный класс компилятор пометил номером.

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

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

В примере с домашними животными мы сделали объект person класса Master — владелец животного — полем класса Pet. Если класс Master больше нигде не используется, то можно определить его прямо внутри класса Pet, сделав класс Master вложенным (inner) классом. Это выглядит следующим образом:

class Pet{

// В этом классе описываем общие свойства всех домашних любимцев. class Master{

// Хозяин животного. string name; // Фамилия, имя.

// Другие сведения...

void getFood(int food, int drink); // Кормление.

// Прочее...

}

int weight; // Вес животного.

int age; // Возраст животного.

Date eatTime[]; // Массив, содержащий время кормления.

int eat(int food, int drink, Date time){ // Процесс кормления.

// Начальные действия.

if (time == eatTime[i]) person.getFood(food, drink);

// Метод потребления пищи...

}

void voice(); // Звуки, издаваемые животным.

// Прочее.

}

Вложение класса удобно тем, что методы внешнего класса могут напрямую обращаться к полям и методам вложенного в него класса. Но ведь того же самого можно было добиться по-другому. Может, следовало расширить класс Master, сделав класс Pet его наследником?

В каких же случаях следует создавать вложенные классы, а когда лучше создать иерархию классов? В теории ООП вопрос о создании вложенных классов решается при рассмотрении отношений "быть частью" и "являться".

Отношения "быть частью" и "являться"

Теперь у нас появились две различные иерархии классов. Одну иерархию образует наследование классов, другую — вложенность классов.

Определив, какие классы будут написаны в вашей программе и сколько их будет, подумайте, как спроектировать взаимодействие классов. Вырастить пышное генеалогическое дерево классов-наследников или расписать "матрешку" вложенных классов?

Теория ООП советует прежде всего выяснить, в каком отношении находятся объекты классов Master и Pet — в отношении "класс Master является экземпляром класса Pet" или в отношении "класс Master является частью класса Pet". Скажем, "собака является животным" или "собака является частью животного"? Другой пример: "мотор является автомобилем" или "мотор является частью автомобиля"? Ясно, что собака — животное и в этой ситуации надо выбрать наследование, но мотор — часть автомобиля и здесь надо выбрать вложение.

Отношение "класс А является экземпляром класса В" по-английски записывается как "a class A is a class B", поэтому в теории ООП называется отношением "is-a". Отношение же "класс А является частью класса В" по-английски "a class A has a class B", и такое отношение называется отношением "has-a".

Отношение "is-a" — это отношение "обобщение-детализация", отношение большей и меньшей абстракции, и ему соответствует наследование классов.

Отношение "has-a" — это отношение "целое-часть" и ему соответствует вложение классов.

Вернемся к нашим животным и их хозяевам и постараемся ответить на вопрос: "класс Master является экземпляром класса Pet" или "класс Master является частью класса Pet"? Ясно, что не верно ни то, ни другое. Классы Master и Pet не связаны ни тем, ни другим образом. Поэтому мы и сделали объект класса Master полем класса Pet.

Заключение

После прочтения этой главы вы получили представление о современной парадигме программирования — объектно-ориентированном программировании и реализации этой парадигмы в языке Java. Если вас заинтересовало ООП, обратитесь к специальной литературе [3—6].

Не беда, если вы не усвоили сразу принципы ООП. Для выработки "объектного" взгляда на программирование нужны время и практика. Части II и III книги как раз и дадут вам эту практику. Но сначала необходимо ознакомиться с важными понятиями языка Java — пакетами и интерфейсами.

Вопросы для самопроверки

1. Какие парадигмы возникали в программировании по мере его развития?

2. Какова современная парадигма программирования?

3. Что такое объектно-ориентированное программирование?

4. Что понимается под объектом в ООП?

5. Каковы основные принципы ООП?

6. Что такое класс в ООП?

7. Какая разница между объектом и экземпляром класса?

8. Что входит в класс Java?

9. Что такое конструктор класса?

10. Какая операция выделяет оперативную память для объекта?

11. Что такое суперкласс и подкласс?

12. Как реализуется полиморфизм в Java?

13. Для чего нужны статические поля и методы класса?

14. Какую роль играют абстрактные методы и классы?

15. Можно ли записать конструктор в абстрактном классе?

16. Почему метод main() должен быть статическим?

17. Почему метод main() должен быть открытым?

ГЛАВА 3

 

Пакеты, интерфейсы и перечисления

В стандартную библиотеку Java API входят сотни классов. Каждый программист в ходе работы добавляет к ним десятки своих классов. Множество классов растет и становится необозримым. Уже давно принято отдельные классы, решающие какую-то одну определенную задачу, объединять в библиотеки классов. Но библиотеки классов, кроме стандартной библиотеки, не являются частью языка.

Разработчики Java включили в язык дополнительную конструкцию — пакеты (packages). Все классы Java распределяются по пакетам. Кроме классов пакеты могут содержать интерфейсы и вложенные подпакеты (subpackages). Образуется древовидная структура пакетов и подпакетов.

Эта структура в точности отображается на структуру файловой системы. Все файлы с расширением class (содержащие байт-коды), образующие один пакет, хранятся в одном каталоге файловой системы. Подпакеты образуют подкаталоги этого каталога.

Каждый пакет создает одно пространство имен (namespace). Это означает, что все имена классов, интерфейсов и подпакетов в пакете должны быть уникальны. Имена в разных пакетах могут совпадать, но это будут разные программные единицы. Таким образом, ни один класс, интерфейс или подпакет не может оказаться сразу в двух пакетах. Если надо в одном месте программы использовать два класса с одинаковыми именами из разных пакетов, то имя класса уточняется именем пакета: пакет.Класс. Такое уточненное имя называется полным именем класса (fully qualified name).

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

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

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

Если член класса не отмечен ни одним из модификаторов private, protected, public, то по умолчанию к нему осуществляется пакетный доступ (default access), т. е. к такому члену может обратиться любой метод любого класса из того же пакета. Пакеты ограничивают и доступ к классу целиком — если класс не помечен модификатором public, то все его члены, даже открытые, public, не будут видны из других пакетов.

Следует обратить внимание на то, что члены с пакетным доступом не видны в подпакетах данного пакета.

Как же создать пакет и разместить в нем классы и подпакеты?

Пакет и подпакет

Чтобы создать пакет, надо просто в первой строке java-файла с исходным кодом записать строку

package РёРјСЏ;

например:

package mypack;

Тем самым создается пакет с указанным именем mypack и все классы, записанные в этом файле, попадут в пакет mypack. Повторяя эту строку в начале каждого исходного файла, включаем в пакет новые классы.

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

package mypack.subpack;

и все классы этого файла и всех файлов с такой же первой строкой попадут в подпакет subpack пакета mypack.

Можно создать и подпакет подпакета, написав что-нибудь вроде

package mypack.subpack.sub;

и т. д. сколько угодно раз.

Поскольку строка package имя; только одна и это обязательно первая строка файла, каждый класс попадает только в один пакет или подпакет.

Компилятор Java может сам создать каталог с тем же именем mypack, а в нем подкаталог subpack и разместить в них class-файлы с байт-кодами.

Полные имена классов A, B будут выглядеть так: mypack.A mypack.subpack.B.

Соглашение "Code Conventions" рекомендует записывать имена пакетов строчными буквами. Тогда они не будут совпадать с именами классов, которые, по соглашению, начинаются с прописной буквы. Кроме того, соглашение советует использовать в качестве имени пакета или подпакета доменное имя своего сайта, записанное в обратном порядке, например:

com.sun.developer

Это обеспечит уникальность имени пакета во всем Интернете.

До сих пор мы ни разу не создавали пакет. Куда же попадали наши файлы с откомпилированными классами?

Компилятор всегда создает для таких классов безымянный пакет (unnamed package), которому соответствует текущий каталог (current working directory) файловой системы.

Вот поэтому у нас class-файл всегда оказывался в том же каталоге, что и соответствующий исходный java-файл.

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

Например, библиотека классов Java SE 7 API хранится в пакетах java, javax, org. Пакет java содержит только подпакеты applet, awt, beans, dyn, io, lang, math, net, nio, rmi, security, sql, text, util и ни одного класса. Эти пакеты имеют свои подпакеты, например пакет создания ГИП (Графический интерфейс пользователя) и графики java.awt содержит классы, интерфейсы и подпакеты color, datatransfer, dnd, event, font, geom, im, image, print.

Конечно, количество и состав пакетов Java SE API меняется с каждой новой версией.

Права доступа к членам класса

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

Рассмотрим большой пример. Пусть имеется пять классов, размещенных в двух пакетах, как показано на рис. 3.1.

package p1; package p2;
Inp1 Inp2
Base
\- —Derived p2
Derivedpl
Рис. 3.1. Размещение наших классов по пакетам

В файле Basejava описаны три класса: Inp1, Base и класс Derivedp1, расширяющий класс Base. Эти классы размещены в пакете p1. В классе Base определены переменные всех четырех типов доступа, а в методах f () классов Inp1 и Derivedp1 сделана попытка доступа ко всем полям класса Base. Неудачные попытки отмечены комментариями. В комментариях помещены сообщения компилятора. Листинг 3.1 показывает содержимое этого файла.

Листинг 3.1. Файл Base.java с описанием пакета pi

package p1; class Inp1{

public void f(){

Base b = new Base();

// b.priv = 1; // "priv has private access in p1.Base" b.pack = 1; b.prot = 1;

b.publ = 1;

}

}

public class Base{

private int priv = 0;

int pack = 0; protected int prot = 0; public int publ = 0;

}

class Derivedp1 extends Base{ public void f(Base a){

// a.priv = 1; // "priv has private access in p1.Base"

a.pack = 1; a.prot = 1; a.publ = 1;

// priv = 1; // "priv has private access in p1.Base"

pack = 1; prot = 1; publ = 1;

}

}

Как видно из листинга 3.1, в пакете недоступны только закрытые, private, поля другого класса.

В файле Inp2java описаны два класса: Inp2 и класс Derivedp2, расширяющий класс Base. Эти классы находятся в другом пакете p2. В них тоже сделана попытка обращения к полям класса Base. Неудачные попытки прокомментированы сообщениями компилятора. Листинг 3.2 показывает содержимое этого файла.

Напомним, что класс Base должен быть помечен при своем описании в пакете p1 модификатором public, иначе из пакета p2 не будет видно ни одного его члена.

Листинг 3.2. Файл Inp2.java с описанием пакета р2

package p2; import p1.Base; class Inp2{

public static void main(String[] args){

Base b = new Base();

// b.priv = 1; // "priv has private access in p1.Base"

// b.pack = 1; // "pack is not public in p1.Base;

// cannot be accessed from outside package" // b.prot = 1; // "prot has protected access in p1.Base"

b.publ = 1;

}

}

class Derivedp2 extends Base{ public void f(Base a){

// "priv has private access in p1.Base"

// priv = 1;

// pack = 1;

prot = 1; publ = 1; super.prot = 1;

}

}

// "pack is not public in p1.Base; cannot // be accessed from outside package"

// "prot has protected access in p1.Base"

// "priv has private access in p1.Base"

// "pack is not public in p1.Base; cannot // be accessed from outside package"

Здесь, в другом пакете, доступ ограничен в большей степени.

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

Все указанное относится не только к полям, но и к методам.

Подытожим в табл. 3.1 все сказанное.

Таблица 3.1. Права доступа к полям и методам класса
Класс Пакет Пакет и подклассы Все классы
private +
"package" + +
protected + + *
public + + + +
* Особенность доступа к protected-полям и методам из чужого пакета отмечена звездочкой.

Размещение пакетов по файлам

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

Обратимся к уже рассмотренному примеру. Пусть в каталоге D:\jdk1.3\MyProgs\ch3 есть пустой подкаталог classes и два файла — Basejava и Inp2java, — содержимое которых показано в листингах 3.1 и 3.2. Рисунок 3.2 демонстрирует структуру каталогов уже после компиляции.

Мы можем проделать всю работу вручную.

1. В каталоге classes создаем подкаталоги р1 и p2.

2. Переносим файл Basejava в каталог р1 и делаем р1 текущим каталогом.

ch3

-classes-i- р1 —г- Base.class

Base.java

-Derivedpi .class

4np2.java

4np1 .class

T

Derivedp2.class

LP2

I—Inp2.class

Рис. 3.2. Структура каталогов

3. Компилируем Base.java, получая в каталоге р1 три файла: Base.class, Inp1.class, Derivedp1.class.

4. Переносим файл Inp2java в каталог p2.

5. Снова делаем текущим каталог classes.

6. Компилируем второй файл, указывая путь p2\Inp2.java.

7. Запускаем программу java p2.Inp2.

Вместо шагов 2 и 3 можно просто создать три class-файла в любом месте, а потом перенести их в каталог p1. В class-файлах не хранится никакая информация о путях к файлам.

Смысл действий 5 и 6 в том, что при компиляции файла Inp2java компилятор уже должен знать класс p1.Base, а отыскивает он файл с этим классом по пути p1\Base.class, начиная от текущего каталога.

Обратите внимание на то, что в последнем действии (7) надо указывать полное имя класса.

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

1. Вызываем компилятор с ключом -d путь, указывая параметром путь начальный каталог для пакета:

javac -d classes Base.java

Компилятор создаст в каталоге classes подкаталог p1 и поместит туда три class-файла.

2. Вызываем компилятор с еще одним ключом -classpath путь, указывая параметром путь каталог classes, в котором находится подкаталог с уже откомпилированным пакетом p1:

javac -classpath classes -d classes Inp2.java

Компилятор, руководствуясь ключом -d, создаст в каталоге classes подкаталог p2 и поместит туда два class-файла, при создании которых он "заглядывал" в каталог p1, руководствуясь ключом -classpath.

3. Делаем текущим каталог classes.

4. Запускаем программу java p2.Inp2.

Для "юниксоидов" все это звучит, как музыка, ну а прочим придется вспомнить

MS-DOS.

Конечно, если вы используете для работы не компилятор командной строки, а какой-нибудь IDE, вроде Eclipse или NetBeans, то все эти действия будут сделаны без вашего участия.

На рис. 3.3 показан вывод этих действий в окно Command Prompt и содержимое каталогов после компиляции.

Рис. 3.3. Протокол компиляции и запуска программы

Импорт классов и пакетов

Внимательный читатель заметил во второй строке листинга 3.2 новый оператор import. Для чего он нужен?

Дело в том, что компилятор будет искать классы только в двух пакетах: в том, что указан в первой строке файла, и в пакете стандартных классов java.lang. Для классов из другого пакета надо указывать полные имена. В нашем примере они короткие, и мы могли бы писать в листинге 3.2 вместо Base полное имя p1. Base.

Но если полные имена длинные, а используются классы часто, то стучать по клавишам, набирая полные имена, становится утомительно. Вот тут-то мы и пишем операторы import, указывая компилятору полные имена классов.

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

Это тоже может стать утомительным и тогда используется вторая форма оператора import — указывается имя пакета или подпакета, а вместо короткого имени класса ставится звездочка *. Этой записью компилятору предписывается просмотреть весь пакет. В нашем примере можно было написать

import p1.*;

Напомним, что импортировать разрешается только открытые классы, помеченные модификатором public.

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

Пакет java.lang просматривается всегда, его необязательно импортировать. Остальные пакеты стандартной библиотеки надо указывать в операторах import, либо записывать полные имена классов.

Начиная с версии Java SE 5 в язык введена еще одна форма оператора import, предназначенная для поиска статических полей и методов класса — оператор import static. Например, можно написать оператор

import static java.lang.Math.*;

После этого все статические поля и методы класса Math можно использовать без указания имени класса. Вместо записи

double r = Math.cos(Math.PI * alpha);

как мы делали раньше, можно записать просто

double r = cos(PI * alpha);

Подчеркнем, что оператор import вводится только для удобства программистов и слово "импортировать" не означает никаких перемещений классов.

Знатокам C/C++

Оператор import не эквивалентен директиве препроцессора include — он не подключает никакие файлы.

Java-файлы

Теперь можно описать структуру исходного файла с текстом программы на языке Java.

□ В первой строке файла может быть необязательный оператор package.

□ В следующих строках могут быть необязательные операторы import.

□ Далее идут описания классов и интерфейсов.

Еще два правила.

□ Среди классов файла может быть только один открытый public-класс.

□ Имя файла должно совпадать с именем открытого класса, если последний существует.

Отсюда следует, что если в проекте есть несколько открытых классов, то они должны находиться в разных файлах.

Соглашение "Code Conventions" рекомендует открытый класс, если он имеется в файле, описывать первым.

Для технологии Java характерно записывать исходный текст каждого класса в отдельном файле. В конце концов, компилятор всегда создает class-файл для каждого класса.

Интерфейсы

Вы уже заметили, что сделать расширение можно только от одного класса, каждый класс в или с происходит из неполной семьи, как показано на рис. 3.4, а. Все классы происходят только от "Адама", от класса Object. Но часто возникает необходимость породить класс D от двух классов в и с, как показано на рис. 3.4, б. Это называется множественным наследованием (multiple inheritance). В множественном наследовании нет ничего плохого. Трудности возникают, если классы в и с сами порождены от одного класса а, как показано на рис. 3.4, в. Это так называемое "ромбовидное" наследование.

Рђ Р’ РЎ Рђ
Р› V
РІ СЃ D D
Р°) Р±) РІ)
Рис. 3.4. Разные варианты наследования

В самом деле, пусть в классе а определен метод f(), к которому мы обращаемся из некоторого метода класса D. Можем мы быть уверены, что метод f() выполняет то, что написано в классе а, т. е. это метод A.f()? Может, он переопределен в классах в и с? Если так, то каким вариантом мы пользуемся: B.f() или C.f() ? Конечно, допустимо определить экземпляры классов и обращаться к методам этих экземпляров, но это совсем другая ситуация.

В различных языках программирования этот вопрос решается по-разному, главным образом уточнением имени метода f(). Но при этом всегда нарушается принцип KISS. Вокруг множественного наследования всегда много споров, есть его ярые приверженцы и столь же ярые противники. Не будем встревать в эти споры, наше дело — наилучшим образом использовать средства языка для решения своих задач.

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

Но что делать, если все-таки при порождении надо использовать несколько предков? Например, у нас есть общий класс автомобилей Automobile, от которого можно породить класс грузовиков Truck и класс легковых автомобилей Car. Но вот надо описать пикап Pickup. Этот класс должен наследовать свойства и грузовых, и легковых автомобилей.

В таких случаях используется еще одна конструкция языка Java — интерфейс. Внимательно проанализировав ромбовидное наследование, теоретики ООП выяснили, что проблему создает только реализация методов, а не их описание.

Интерфейс (interface), в отличие от класса, содержит только константы и заголовки методов, без их реализации.

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

Описание интерфейса начинается со слова interface, перед которым может стоять модификатор public, означающий, как и для класса, что интерфейс доступен всюду. Если же модификатора public нет, интерфейс будет виден только в своем пакете.

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

Затем в фигурных скобках записываются в любом порядке константы и заголовки методов. Можно сказать, что в интерфейсе все методы абстрактные, но слово abstract писать не надо. Константы всегда статические, но слова static и final указывать не нужно. Все эти модификаторы принимаются по умолчанию.

Все константы и методы в интерфейсах всегда открыты, не обязательно даже указывать модификатор public.

Вот какую схему можно предложить для иерархии автомобилей:

interface Automobile{ . . . } interface Car extends Automobile{ . . . } interface Truck extends Automobile{ . . . } interface Pickup extends Car, Truck{ . . . }

Таким образом, интерфейс — это только набросок, эскиз. В нем указано, что делать, но не указано, как это делать.

Как же использовать интерфейс, если он полностью абстрактен, в нем нет ни одного полного метода?

Использовать нужно не интерфейс, а его реализацию (implementation). Реализация интерфейса — это класс, в котором расписываются методы одного или нескольких интерфейсов. В заголовке класса после его имени или после имени его суперкласса, если он есть, записывается слово implements и, через запятую, перечисляются имена интерфейсов.

Вот как можно реализовать иерархию автомобилей:

interface Automobile{ . . . }

interface Car extends Automobile{ . . . }

class Truck implements Automobile{ . . . }

class Pickup extends Truck implements Car{ . . . }

или так:

interface Automobile{ . . . } interface Car extends Automobile{ . . . } interface Truck extends Automobile{ . . . } class Pickup implements Car, Truck{ . . . }

Реализация интерфейса может быть неполной, некоторые методы интерфейса могут быть расписаны, а другие — нет. Такая реализация — абстрактный класс, его обязательно надо пометить модификатором abstract.

Как реализовать в классе Pickup метод f(), описанный и в интерфейсе Car, и в интерфейсе Truck с одинаковой сигнатурой? Ответ простой — никак. Такую ситуацию нельзя реализовать в классе Pickup. Программу надо спроектировать по-другому.

Итак, интерфейсы позволяют реализовать средствами Java чистое объектно-ориентированное проектирование, не отвлекаясь на вопросы реализации проекта.

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

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

Листинг 3.3 показывает, как можно собрать с помощью интерфейса "хор" домашних животных из листинга 2.2.

Листинг 3.3. Использование интерфейса для организации полиморфизма

interface Voice{ void voice();

}

class Dog implements Voice{

@Override

public void voice(){

System.out.println("Gav-gav!");

}

}

class Cat implements Voice{

@Override

public void voice(){

System.out.println("Miaou!");

}

}

class Cow implements Voice{

@Override

public void voice(){

System.out.println("Mu-u-u!");

}

} public class Chorus{

public static void main(String[] args){

Voice[] singer = new Voice[3]; singer[0] = new Dog(); singer[1] = new Cat(); singer[2] = new Cow(); for (Voice v: singer) v.voice();

}

}

Здесь используется интерфейс Voice вместо абстрактного класса Pet, описанного в листинге 2.2.

Что же лучше использовать: абстрактный класс или интерфейс? На этот вопрос нет однозначного ответа.

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

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

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

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

Наконец, можно использовать интерфейсы просто для определения констант, как показано в листинге 3.4.

Листинг 3.4. Система управления светофором

int ERROR = -1;

}

class Timer implements Lights{ private int delay; private static int light = RED;

Timer(int sec){delay = 1000 * sec;} public int shift(){

int count = (light++) % 3; try{

switch (count){

case RED: Thread.sleep(delay); break;

case YELLOW: Thread.sleep(delay/3); break; case GREEN: Thread.sleep(delay/2); break;

}

}catch(Exception e){return ERROR;} return count;

}

} class TrafficRegulator{

private static Timer t = new Timer(1);

public static void main(String[] args){

System.out.println("Stop!"); break;

System.out.println("Wait!"); break; System.out.println("Walk!"); break; System.err.println("Time Error"); break; System.err.println("Unknown light."); return;

for(int k = 0; k < 10; k++) switch(t.shift()){ case Lights.RED: case Lights.YELLOW: case Lights.GREEN: case Lights.ERROR: default:

}

}

Здесь, в интерфейсе Lights, определены константы, общие для всего проекта.

Класс Timer реализует этот интерфейс и использует константы напрямую как свои собственные. Метод shift() этого класса подает сигналы переключения светофору с разной задержкой в зависимости от цвета. Задержку осуществляет метод sleep () класса Thread из стандартной библиотеки, которому передается время задержки в миллисекундах. Этот метод нуждается в обработке исключений try{}catch(){}, о которой мы будем говорить в главе 21.

Класс TrafficRegulator не реализует интерфейс Lights и пользуется полными именами Lights.RED и т. д. Это возможно потому, что константы RED, YELLOW и GREEN по умолчанию являются статическими.

Перечисления

Просматривая листинг 3.4, вы, наверное, заметили, что создавать интерфейс только для записи констант не совсем удобно. Начиная с версии Java SE 5 для этой цели в язык введены перечисления (enumerations). Создавая перечисление, мы сразу же указываем константы, входящие в него. Вместо интерфейса Lights, описанного в листинге 3.4, можно воспользоваться перечислением, сделав такую запись:

enum Lights{ RED, YELLOW, GREEN, ERROR }

Как видите, запись сильно упростилась. Мы записываем только константы, не указывая их характеристики. Каков же, в таком случае, их тип? У них тип перечисления Lights.

Перечисления в языке Java образуют самостоятельные типы, что указывается словом enum в описании перечисления, но все они неявно наследуют абстрактный класс java.lang.Enum. Это наследование не надо указывать словом extends, как мы обычно делаем, определяя классы. Оно введено только для того, чтобы включить перечисления в иерархию классов Java API. Тем не менее мы можем воспользоваться методами класса Enum для получения некоторых характеристик перечисления, как показано в листинге 3.5.

Листинг 3.5. Общие свойства перечислений

enum Lights { RED, YELLOW, GREEN, ERROR }

public class EnumMethods{

public static void main(String[] args){ for (Lights light: Lights.values()){

System.out.println("РўРёРї: " + light.getDeclaringClass());

System.out.println("4HcnoBoe значение: " + light.ordinal());

}

}

}

Обратите внимание, во-первых, на то, как задается цикл для перебора всех значений перечисления Lights. В заголовке цикла определяется переменная light типа перечисления Lights. Метод values (), имеющийся в каждом перечислении, дает ссылку на его значения. Эти значения получает последовательно, одно за другим, переменная light.

Во-вторых, посмотрите, как можно узнать тип значений перечисления. Его возвращает метод getDeclaringClass ( ) класса Enum. В случае листинга 3.5 мы получим тип Lights.

В-третьих, у каждой константы, входящей в перечисление, есть свой порядковый номер 0, 1, 2 и т. д. Его можно узнать методом ordinal ( ) класса Enum.

Перечисление — это не только собрание констант. Это полноценный класс, в котором можно определить поля, методы и конструкторы. Мы уже видели, что в каждом перечислении есть методы, унаследованные от класса Enum, например метод values (), возвращающий массив значений перечисления.

Расширим определение перечисления Lights. Для использования его в классе TrafficRegulator нам надо сделать так, чтобы числовое значение константы error было равно -1 и чтобы методом shift() можно было бы получить следующую константу. Этого можно добиться следующим определением:

enum Lights{

RED(0), YELLOW (1), GREEN(2), ERROR(-1); private int value;private int currentValue = 0;

Lights(int value){ this.value = value;} public int getValue(){ return value; }

public Lights nextLight(){

currentValue = (currentValue + 1) % 3; return Lights.values()[currentValue];

}

}

Как видите, теперь константы создаются конструктором, определяющим для каждой константы поле value. А сейчас можно применить полученное перечисление Lights для регулирования дорожного движения. Это сделано в листинге 3.6.

Листинг 3.6. Система управления светофором с перечислением

enum Lights{

RED(0), YELLOW (1), GREEN(2), ERROR(-1);

private int value;

private int currentValue = 0;

Lights(int value){ this.value = value;

}

public int getValue(){ return value; }

public Lights nextLight(){

currentValue = (currentValue + 1) % 3; return Lights.values()[currentValue];

}

}

class Timer {

private int delay;

private static Lights light = Lights.RED;

Timer(int sec){

delay = 1000 * sec;

}

public Lights shift(){

Lights count = light.nextLight(); try{

switch (count){

case RED: Thread.sleep(delay); break;

case YELLOW: Thread.sleep(delay/3); break; case GREEN: Thread.sleep(delay/2); break;

}

}catch(Exception e){ return Lights.ERROR;

}

return count;

}

public class TrafficRegulator{

public static void main(String[] args){

Timer t = new Timer(1);

for (int k = 0; k < 10; k++) switch (t.shift()){

case RED: System.out.println("Stop!"); break;

case YELLOW: System.out.println("Wait!"); break; case GREEN: System.out.printlnCWalk!"); break; case ERROR: System.err.println("Time Error"); break; default: System.err.println("Unknown light."); return;

}

}

}

Константы, входящие в перечисление, рассматриваются как константные вложенные классы. Поэтому в них можно определять константно-зависимые поля и методы. Одно такое закрытое поле есть в каждой константе любого перечисления. Оно хранит порядковый номер константы, возвращаемый методом ordinal ().

Программист может добавить в каждую константу свои поля и методы. В листинге 3.7 приведен известный из документации пример простейшего калькулятора, в котором абстрактный метод выполнения арифметической операции eval () переопределяется в зависимости от ее конкретного вида в каждой константе перечисления Operation.

Листинг 3.7. Простейший калькулятор

public enum Operation{
PLUS { double eval(double x, double y){ return x + y; }},
MINUS { double eval(double x, double y){ return x - y; }},
TIMES { double eval(double x, double y){ return x * y; }},
DIVIDE { double eval(double x, double y){ return x / y; }};
abstract double eval(double x, double y);

public static void main(String[] args){ double x = -23.567, y = 0.235; for (Operation op: Operation.values())

System.out.println(op.eval(x, y));

}

}

Объявление аннотаций

Аннотации, о которых уже шла речь в главе 1, объявляются интерфейсами специального вида, помеченными символом "at-sign", на жаргоне называемом "собачкой". Например, аннотация @Override, использованная нами в листинге 2.2, может быть объявлена так: public @interface Override{ }

Таково объявление самой простой аннотации — аннотации без элементов (marker annotation). У более сложной аннотации могут быть элементы, описываемые методами интерфейса-аннотации. У этих методов не может быть параметров, но можно задать значение по умолчанию, записываемое после слова default в кавычках и квадратных скобках. Например, следующий текст

public @interface MethodDescription{ int id();

String description() default "[Method]";

String date();

}

объявляет аннотацию с тремя элементами id, name и date. У элемента name есть значение по умолчанию, равное Method.

Объявление интерфейса-аннотации определяет новый тип — тип аннотации (annotation type).

Аннотация записывается в программе в тех местах, где можно записывать модификаторы. По соглашению аннотация записывается перед всеми модификаторами. Элементы аннотации записываются как пары "имя — значение" через запятую. В каждой паре имя отделяется от значения знаком равенства:

@MethodDescription( id = 123456,

description = "Calculation method", date = "04.01.2008"

)

public int someMethod(){

}

Если у аннотации только один элемент, то его лучше назвать value (), например:

public @interface Copyright{

String value();

}

потому что в этом случае можно записать значение этого элемента просто как строку в кавычках, а не как пару "имя — значение":

@ Copyright("2008 My Company") public class MyClass{

}

Разумеется, интерфейс-аннотация должен быть реализован классом Java, в котором надо записать действия, выполняемые аннотацией. Это можно сделать разными способами, но все они выходят за рамки нашей книги.

Теперь нам известны все средства языка Java, позволяющие проектировать решение поставленной задачи. Заканчивая разговор о проектировании, нельзя не упомянуть о постоянно пополняемой коллекции образцов проектирования (design patterns).

Design patterns

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

Нет ли подобных общих методов в программировании? Есть.

Допустим, вам поручили автоматизировать метеорологическую станцию. Информация от различных датчиков или, другими словами, контроллеров температуры, давления, влажности, скорости ветра поступает в цифровом виде в компьютер. Там она обрабатывается: вычисляются усредненные значения по регионам, на основе многодневных наблюдений делается прогноз на завтра, т. е. создается модель метеорологической картины местности. Затем прогноз выводится по разным каналам: на экран монитора, самописец, передается по сети. Он представляется в разных видах: колонках чисел, графиках, диаграммах.

Такая информационная система очень часто проектируется по схеме MVC.

Схема проектирования MVC

Естественно спроектировать в нашей автоматизированной системе три части.

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

□ Вторая часть, назовем ее Моделью (Model), принимает эту унифицированную информацию от Контроллера, ничего не зная о датчике и не интересуясь тем, от какого именно датчика она поступила, и преобразует ее по своим алгоритмам опять-таки к какому-то однообразному виду, например к последовательности чисел.

□ Третья часть системы, Вид (View), непосредственно связана с устройствами вывода и преобразует поступившую от Модели последовательность чисел в таблицу чисел, график, диаграмму или пакет для отправки по сети. Для каждого устройства вывода придется написать свой модуль, учитывающий особенности именно этого устройства.

В чем удобство такой трехзвенной схемы? Она очень гибка. Замена одного датчика приведет к замене только одного модуля в Контроллере, ни Модель, ни Вид этого даже не заметят. Надо представить прогноз в каком-то новом виде, например для телевидения? Пожалуйста, достаточно написать один модуль и вставить его в Вид. Изменился алгоритм обработки данных? Меняем Модель.

Эта схема разработана еще в 80-х годах прошлого столетия в языке Smalltalk и получила название MVC (Model-View-Controller). Оказалось, что она применима во многих областях, далеких от метеорологии, всюду, где удобно отделить обработку от ввода и вывода информации.

Сбор информации часто организуется так. На экране дисплея открывается поле ввода, в которое вы набиваете сведения, допустим, фамилии в произвольном порядке, а в соседнем поле вывода отображается обработанная информация, например список фамилий по алфавиту. Будьте уверены, что эта программа организована по схеме MVC. Контроллером служит поле ввода, Видом — поле вывода, а Моделью — метод сортировки фамилий.

В объектно-ориентированном программировании каждая из трех частей схемы MVC реализуется одним или несколькими классами. Модель обладает методами setXxx(), которые использует Контроллер для передачи информации в Модель. Одна Модель может получать информацию от нескольких Контроллеров. Модель предоставляет Виду методы getXxx () и isXxx () для получения информации.

В некоторых реализациях схемы MVC Вид и Контроллер не взаимодействуют. Контроллер, реагируя на события, обращается к методам setXxx() Модели, которые меняют хранящуюся в ней информацию. Модель, изменив информацию, сообщает об этом тем Видам, которые зарегистрировались у нее. Этот способ взаимодействия Модели и Вида получил название "подписка-рассылка" (subscribe-publish). Виды подписываются у Модели, и та рассылает им сообщения о всяком изменении состояния объекта методами fireXxx (), после чего Виды забирают измененную информацию, обращаясь к методам getXxx () и isXxx () Модели.

В других реализациях Контроллер руководит взаимодействием Модели и Вида.

По схеме MVC построены компоненты графической библиотеки Swing, которые мы рассмотрим в главе 11.

К середине 90-х годов XX века накопилось много схем, подобных MVC. В них сконцентрирован многолетний опыт тысяч программистов, выражены наилучшие решения типовых задач.

Шаблон Singleton

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

Листинг 3.8. Схема Singleton

final class Singleton{

private static Singleton s = new Singleton(0); private int k;

private Singleton(int i){ // Закрытый конструктор.

k = i;

}

public static Singleton getReference(){ // Открытый статический метод. return s;

public int getValue(){return k;} public void setValue(int i){k = i;}

} public class SingletonTest{

public static void main(String[] args){

Singleton ref = Singleton.getReference();

System.out.println(ref.getValue()); ref.setValue(ref.getValue() + 5);

System.out.println(ref.getValue());

}

}

Класс Singleton окончательный — его нельзя расширить. Его конструктор закрытый — никакой метод не может создать экземпляр этого класса. Единственный экземпляр s класса Singleton — статический, он создается внутри класса. Зато любой объект может получить ссылку на этот экземпляр методом getReference (), изменить состояние экземпляра s методом setValue ( ) или просмотреть его текущее состояние методом getValue ( ).

Это только схема — класс Singleton надо еще наполнить полезным содержимым, но идея выражена ясно и полностью.

Схемы проектирования были систематизированы и изложены в [7]. Четыре автора этой книги были прозваны "бандой четырех" (Gang of Four), а книга, коротко, "GoF". Схемы обработки информации получили название "design patterns". Русский термин еще не устоялся. Говорят о "шаблонах", "схемах разработки", "шаблонах проектирования".

В книге GoF описаны 23 шаблона, разбитые на три группы:

□ шаблоны создания объектов: Factory, Abstract Factory, Singleton, Builder, Prototype;

□ шаблоны структуры объектов: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy;

□ шаблоны поведения объектов: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template, Visitor.

Описания даны в основном на языке C++. В книге [8] те же шаблоны представлены на языке Java. В ней описаны и дополнительные шаблоны. Той же теме посвящено электронное издание [9]. В книге [10] подробно обсуждаются вопросы разработки систем на основе design patterns.

К сожалению, нет возможности разобрать подробно design patterns в этой книге. Но каждый разработчик, программирующий на объектно-ориентированном языке, должен их знать. Описание многих разработок начинается словами: "Проект решен на основе шаблона...", и структура проекта сразу становится ясна для всякого, знакомого с design patterns.

По ходу книги мы будем указывать, на основе какого шаблона сделана та или иная разработка.

Заключение

Вот мы и закончили первую часть книги. Теперь вы знаете все основные конструкции языка Java, позволяющие спроектировать и реализовать проект любой сложности на основе ООП. Оставшиеся конструкции языка, не менее важные, но реже используемые, отложим до части IV. Части II и III книги посвятим изучению классов и методов, входящих в Core API. Это будет для вас хорошей тренировкой.

Язык Java, как и все современные языки программирования, — это не только синтаксические конструкции, но и богатая библиотека классов. Знание этих классов и умение пользоваться ими как раз и определяет программиста-практика.

Вопросы для самопроверки

1. Что такое пакет в Java?

2. Могут ли классы и интерфейсы, входящие в один пакет, располагаться в нескольких каталогах файловой системы?

3. Обеспечивает ли "пакетный" доступ возможность обращения к полям и методам классов, расположенных в подпакете?

4. Можно ли в аналогичной ситуации обратиться из подпакета к полям и методам классов, расположенных в объемлющем пакете?

5. Могут ли два экземпляра одного класса пользоваться закрытыми полями друг друга?

6. Почему метод main() должен быть открытым (public)?

7. Обеспечивает ли импорт пакета поиск классов, расположенных в его подпакетах?

8. Зачем в Java есть и абстрактные классы, и интерфейсы? Нельзя ли было обойтись одной из этих конструкций?

9. Зачем в Java введены перечисления? Нельзя ли обойтись интерфейсами?

 

ЧАСТЬ II

Использование классов из Java API

Глава 4. Классы-оболочки и generics
Глава 5. Работа со строками
Глава 6. Классы-коллекции
Глава 7. Классы-утилиты

ГЛАВА 4

 

Классы-оболочки и generics

Java — полностью объектно-ориентированный язык. Это означает, что все, что только можно, в Java представлено объектами.

Восемь примитивных типов нарушают это правило. Они оставлены в Java не только из-за многолетней привычки к числам и символам. Арифметические действия удобнее и быстрее производить с обычными числами, а не с объектами классов, которые требуют много ресурсов от компьютера.

Но и для этих типов в языке Java есть соответствующие классы — классы-оболочки (wrapper) примитивных типов. Конечно, они предназначены не для вычислений, а для действий, типичных при работе с классами, — создания объектов, преобразования типов объектов, получения численных значений объектов в разных формах и передачи объектов в методы по ссылке.

На рис. 4.1 показана одна из ветвей иерархии классов Java. Для каждого примитивного типа в пакете j ava. lang есть соответствующий класс. Числовые классы имеют общего предка — абстрактный класс Number, в котором описаны шесть методов, возвращающих числовое значение, содержащееся в классе, приведенное к соответствующему примитивному типу: byteValue(), doubleValue(), floatValue(), intValue(), longVaiue (), shortValue (). Эти методы переопределены в каждом из шести числовых классов-оболочек Byte, Short, Integer, Long, Float и Double. Имена классов-оболочек, за исключением класса Integer, совпадают с именами соответствующих примитивных типов, но начинаются с заглавной буквы.

Помимо метода сравнения объектов equals(), переопределенного из класса Object, все описанные в этой главе числовые классы, класс Character и класс Boolean имеют метод

Object—р Number-

- Boolean

-Character

-Class

■ — BigDecimal —Blglnteger

— Byte

— Double —Float —Integer

— Long

— Short

L Character.Subset-i— InputSubset

Character.UnicodeBlock

Рис. 4.1. Классы примитивных типов

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

□ 0, если сравниваемые значения равны;

□ отрицательное число (-1), если числовое значение в данном объекте меньше, чем в объекте-аргументе или, для класса Boolean, в данном объекте false, а в аргументе — true;

□ положительное число (+1), если числовое значение в данном объекте больше числового значения, содержащегося в аргументе или в данном объекте true, а в аргументе — false.

В каждом из этих классов есть статический метод

int compare(xxx a, xxx b);

который сравнивает значения двух чисел, символов или логических переменных a и b, заданных простыми типами boolean, byte, short, char, int, long, float, double, так же, как и метод compareTo (), и возвращает те же значения.

Еще один полезный статический метод

Xxx valueOf(xxx a);

в котором xxx — это один из простых типов boolean, byte, short, char, int, long, float, double, возвращает объект соответствующего типа. Документация настоятельно рекомендует применять этот метод для создания объектов из простых типов, а не конструктор соответствующего класса.

Что полезного можно найти в классах-оболочках?

Числовые классы

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

тип: Byte.parseByte(), Double.parseDouble(), Float.parseFloat(), Integer.parseInt(), Long.parseLong(), Short.parseShort ( ). Исходная строка типа String, как всегда в статических методах, служит параметром метода. Эти методы полезны при вводе данных в поля ввода, обработке аргументов командной строки, т. е. всюду, где числа представляются строками символов, состоящими из цифр со знаками плюс или минус и десятичной точкой.

В каждом из этих классов есть статические константы MAX_VALUE и MIN_VALUE, показывающие диапазон числовых значений соответствующих примитивных типов. В классах

Double и Float есть еще константы POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, о которых шла речь в главе 1, и логические методы проверки isNaN ( ), isInfinite ( ).

Если вы хорошо знаете двоичное представление вещественных чисел, то можете воспользоваться статическими методами floatToIntBits ( ) и doubleToLongBits ( ), представляющими последовательность битов, из которых состоит двоичное представление вещественного числа, в виде целого числа типа int или long соответственно. Исходное вещественное число задается как аргумент метода. Получив целочисленное представление, вы можете изменить отдельные биты получившегося целого числа побитовыми операциями и преобразовать измененное целое число обратно в вещественное значение методами intBitsToFloat ( ) и longBitsToDouble ().

Статическими методами toBinaryString(), toHexString() и toOctalString() классов Integer и Long можно преобразовать целые значения типов int и long, заданные как аргумент метода, в строку символов, показывающую двоичное, шестнадцатеричное или восьмеричное представление числа.

В листинге 4.1 показано применение этих методов, а рис. 4.2 демонстрирует вывод результатов.

Рис. 4.2. Методы числовых классов

Листинг 4.1. Методы числовых классов

class NumberTest{

public static void main(String[] args){ int i = 0; short sh = 0;

double d = 0;

Integer k1 = Integer.valueOf(55);

Integer k2 = Integer.valueOf(100); Double d1 = Double.valueOf(3.14); try{

i = Integer.parseInt(args[0]); sh = Short.parseShort(args[0]);

d = Double.parseDouble(args[1]); d1 = new Double(args[1]); k1 = new Integer(args[0]); }catch(Exception e){} double x = 1.0 / 0.0; System.out.println("i = " + i); System.out.println("sh = " + sh);

System.out.println("d = " + d);

System.out.println("k1.intValue() = " + k1.intValue()); System.out.println("d1.intValue() = " + d1.intValue());

System.out.println("k1 > k2? " + k1.compareTo(k2));

System.out.println("x = " + x);

System.out.println("x isNaN? " + Double.isNaN(x));

System.out.println("x isInfinite? " + Double.isInfinite(x));

System.out.println("x == Infinity? " + (x == Double.POSITIVE INFINITY)); System.out.println("d = " + Double.doubleToLongBits(d));

System.out.println("i = " + Integer.toBinaryString(i));

System.out.println("i = " + Integer.toHexString(i));

System.out.println("i = " + Integer.toOctalString(i));

}

}

Методы parseInt () и конструкторы классов требуют обработки исключений, поэтому в листинг 4.1 вставлен блок try{}catch(){}. Обработку исключительных ситуаций мы подробно разберем в главе 21.

Начиная с версии Java SE 5 в JDK входит пакет java.util.concurrent.atomic, в котором, в частности, есть классы AtomicInteger и AtomicLong, обеспечивающие изменение числового значения этих классов на уровне машинных команд. Начальное значение задается конструкторами этих классов. Затем методами addAndGet ( ), getAndAdd ( ), incrementAndGet ( ), getAndnIncrement(), decrementAndGet(), getAndDecrement, getAndSet(), set() можно изменять это значение.

Автоматическая упаковка и распаковка типов

В листинге 4.1 объекты числовых классов создавались статическим методом, в котором указывалось числовое значение объекта:

Integer k1 = Integer.valueOf(55);

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

Integer k1 = 55;

как будто k1 — простая числовая переменная примитивного типа. Ничего нового в язык Java такая запись не вносит: компилятор, увидев ее, тут же восстановит применение статического метода. Но она облегчает работу программиста, предоставляя ему привычную форму определения переменной. Как говорят, компилятор делает автоматическую упаковку (auto boxing) числового значения в объект. Компилятор может сделать и автоматическую распаковку. После приведенных ранее определений объекта k1 можно написать, например,

int n = k1;

и компилятор извлечет из объекта k1 класса Integer числовое значение 55. Конечно, для этого компилятор обратится к методу intValue () класса Integer, но это незаметно для программиста.

Автоматическая упаковка и распаковка возможна и в методах классов. Рассмотрим простой класс.

class AutoBox{ static int f(Integer value){ return value; // Распаковка.

}

public static void main(String[] args){

Integer n = f(55);

}

}

В методе main() этого примера сначала число 55 приводится к типу параметра метода f() с помощью упаковки. Затем результат работы метода f () упаковывается в объект n класса Integer.

Автоматическую упаковку и распаковку можно использовать в выражениях, написав k1++ или даже (k1 + k2 / k1), но это уже слишком! Представьте себе, сколько упаковок и распаковок вставит компилятор и насколько это замедлит работу программы!

Настраиваемые типы (generics)

Введение в язык Java автоматической упаковки типов позволило определить еще одну новую конструкцию — настраиваемые типы (generics), позволяющие создавать шаблоны классов, интерфейсов и методов. Например, можно записать обобщенный настраиваемый (generic) класс

class MyGenericClass{ private T data;

public MyGenericClass(){}

public MyGenericClass(T data){ this.data = data;

}

public T getData(){ return data;

}

public void setData(T data){ this.data = data;

}

}

в котором есть поле data неопределенного пока типа, обозначенного буквой T. Разумеется, можно написать другую букву или даже идентификатор. Буква T появилась просто как первая буква слова Type.

Перед использованием такого класса-шаблона его надо настроить, задав при обращении к его конструктору определенный тип в угловых скобках. Например:

class MyGenericClassDemo{

public static void main(String[] args){

MyGenericClass iMyGen = new MyGenericClass(55);

Integer n = iMyGen.getData();

MyGenericClass dMyGen = new MyGenericClass(-37.3456);

Double x = dMyGen.getData();

}

}

Если при определении экземпляра настраиваемого класса и слева и справа от знака равенства в угловых скобках записан один и тот же тип, то справа его можно опустить для краткости записи, оставив только пару угловых скобок (так называемый "ромбовидный оператор", "diamond operator"). Используя это новое, введенное в Java 7, сокращение, предыдущий класс можно записать так:

class MyGenericClassDemo{

public static void main(String[] args){

MyGenericClass iMyGen = new MyGenericClass<>(55);

Integer n = iMyGen.getData();

MyGenericClass dMyGen = new MyGenericClass<>(-37.3456);

Double x = dMyGen.getData();

}

}

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

Листинг 4.2. Настраиваемый класс

class Average{ T[] data;

public Average(T[] data) { this.data = data; }

public double average(){ double result = 0.0;

for (T t: data) result += t.doubleValue(); return result / data.length;

}

public static void main(String[] args){

Integer[] iArray = {1, 2, 3, 4};

Double[] dArray = {3.4, 5.6, 2.3, 1.24};

Average iAver = new Average<>(iArray); System.out.println("int average = " + iAver.average()); Average dAver = new Average<>(dArray); System.out.println("double average = " + dAver.average());

}

Обратите внимание на то, что в заголовке класса в угловых скобках указано, что тип T — подкласс класса Number. Это сделано потому, что здесь тип T не может быть произвольным. Действительно, в методе average ( ) использован метод doubleValue ( ) класса Number, а это означает, что тип T ограничен классом Number и его подклассами. Кроме того, операции сложения и деления тоже допустимы только для чисел.

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

У настраиваемого типа может быть более одного параметра. Они перечисляются в угловых скобках через запятую:

class MyGenericClass2{ private S id; private T data;

public MyGenericClass2() {}

public MyGenericClass2(S id, T data){ this.id = id; this.data = data;

}

public S getId(){ return id;

}

public void setId(S data){ this.id = id;

}

public T getData(){ return data;

}

public void setData(T data){ this.data = data;

}

}

Из этих примеров видно, что неопределенные типы S, T могут быть типами параметров конструкторов и типами возвращаемых методами значений. Разумеется, они могут быть типами параметров не только конструкторов, но и любых методов. Более того, типами параметров и типами возвращаемых значений методов могут быть настраиваемые типы. Можно написать метод в такой форме:

public MyGenericClass2 makeClass2(S id,

MyGenericClass data){ return new MyGenericClass2(id, data.getData());

}

и обратиться к нему так, как показано в листинге 4.3.

Листинг 4.3. Настраиваемые классы — параметры методов

public class MyGenericClass2Demo{

public MyGenericClass2

makeClass2(S id, MyGenericClass data){

return new MyGenericClass2(id, data.getData());

}

public static void main(String[] args){

MyGenericClass dMyGen = new MyGenericClass<>(34.456);

MyGenericClass2Demo d = new MyGenericClass2Demo<>();

MyGenericClass2 ldMyGen2 = d.makeClass2(123456L, dMyGen);

}

}

Шаблон типа (wildcard type)

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

Number n = new Long(123456L);

Number d = new Double(27.346);

Более того, это свойство распространяется на массивы:

Number[] n = new Long[100];

Можно ли распространить эту возможность на настраиваемые типы? Например, можно ли написать последний оператор листинга 4.3 так:

MyGenericClass2 n = // Сшибка!

d.makeClass2(123456L, dMyGen);

Ответ отрицательный. Из того, что какой-то класс B является подклассом класса A, не следует, что класс g будет подклассом класса g.

Это непривычное обстоятельство вынудило ввести дополнительную конструкцию — шаблон типа (wildcard type), применяемую в процессе настройки типа. Шаблон типа обозначается вопросительным знаком и означает "неизвестный тип" или "произвольный тип". Предыдущий код не вызовет возражений у компилятора, если написать его в таком виде:

MyGenericClass2 n = // Верно.

d.makeClass2(123456L, dMyGen);

или

MyGenericClass2 n = // Верно.

d.makeClass2(123456L, dMyGen);

Можно написать даже неограниченный шаблон типа

MyGenericClass2 n =

d.makeClass2(123456L, dMyGen);

Такая запись будет почти эквивалентна записи

MyGenericClass2 n =

d.makeClass2(123456L, dMyGen);

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

Кроме записи , означающей "произвольный подтип типа Type, включая сам тип Type", можно написать выражение , означающее "произвольный супертип типа Type, включая сам тип Type".

Шаблон типа можно использовать в тех местах кода, где настраивается тип, в том числе в параметрах метода:

public MyGenericClass2 makeClass2(S id,

MyGenericClass data){

return new MyGenericClass2(id, data.getData());

}

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

Average a = // Ошибка!

new Average(iArray);

Average[] a = // Ошибка!

new Average[10];

Тем не менее при определении массива (но не объекта) можно записать неограниченный шаблон типа:

Average[] a = // Верно.

new Average[10];

Настраиваемые методы

Настраиваемыми могут быть не только типы, но и методы. Параметры настраиваемого метода (type parameters) указываются в заголовке метода в угловых скобках перед типом возвращаемого значения. Это выглядит так, как показано в листинге 4.4.

Листинг 4.4. Настраиваемый метод

public class MyGenericClass2Demo{

public MyGenericClass2

makeClass2(S id, MyGenericClass data){

return new MyGenericClass2(id, data.getData());

} public static void main(String[] args){

MyGenericClass dMyGen = new MyGenericClass(34.456);

MyGenericClass2Demo d =

new MyGenericClass2Demo();

MyGenericClass2 ldMyGen2 = d.makeClass2(123456L, dMyGen);

}

}

Метод makeClass2 () описан в простом, ненастраиваемом, классе MyGenericClass2Demo, и его параметры задаются в угловых скобках . Здесь можно записывать ограниченные параметры

public

MyGenericClass2 makeClass2(S id, MyGenericClass data){

return new MyGenericClass2(id, data.getData());

}

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

Как вы убедились из приведенных примеров, настраиваемые типы и методы допускают сложную структуру параметров, так же как и вложенные классы. Мы еще не касались вопросов наследования настраиваемых типов, реализации настраиваемых интерфейсов, создания массивов настраиваемых типов. Все эти вопросы подробно рассмотрены на сайте Анжелики Лангер (Angelika Langer), в ее Java Generics FAQ, http:// .

 

Класс Boolean

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

Конструктор Boolean (String s) создает объект, содержащий значение true, если строка s равна "true" в произвольном сочетании регистров букв, и значение false — для любой другой строки.

Статический метод valueOf(boolean b) позволяет получить объект класса Boolean из значения примитивного типа boolean.

Пользуясь автоматической упаковкой, можно определение

Boolean b = new Boolean("true");

или

Boolean b = Boolean.valueOf(true);

сократить до

Boolean b = true;

Метод booleanValue () возвращает логическое значение, хранящееся в объекте.

Статический метод parseBoolean(String s) возвращает значение true, если строка s равна "true" в произвольном сочетании регистров букв, и значение false — для любой другой строки.

 

Класс Character

В этом классе собраны статические константы и методы для работы с отдельными символами.

Статический метод

digit(char ch, in radix);

переводит цифру ch системы счисления с основанием radix в ее числовое значение типа

int.

Статический метод

forDigit(int digit, int radix);

выполняет обратное преобразование целого числа digit в соответствующую цифру (тип char) в системе счисления с основанием radix.

Основание системы счисления должно находиться в диапазоне от Character.MIN_RADIX до Character.MAX RADIX.

Метод toString () переводит символ, содержащийся в классе, в строку с тем же символом.

Статические методы toLowerCase(), toUpperCase(), toTitleCase() возвращают символ, содержащийся в классе, в указанном регистре. Последний из этих методов предназначен для правильного перевода в верхний регистр четырех кодов Unicode, не выражающихся одним символом.

Статический метод

getName(int code);

возвращает полное Unicode-имя символа по его коду code.

Множество статических логических методов проверяют различные характеристики символа, переданного в качестве аргумента метода:

□ isDefined () — выясняет, определен ли символ в кодировке Unicode;

□ isDigit () — проверяет, является ли символ цифрой Unicode;

□ isIdentifierIgnorable () — выясняет, нельзя ли использовать символ в идентификаторах;

□ isISOControl () — определяет, является ли символ управляющим;

□ isBmpCodePoint () — определяет, лежит ли код символа в диапазоне \u0000-\uFFFF;

□ isSupplementaryCodePoint () — определяет, что код символа больше \uFFFF;

□ isJavaIdentifierPart ( ) - выясняет, можно ли использовать символ в идентифика

торах;

□ isJavaIdentifierStart () — определяет, может ли символ начинать идентификатор;

□ isLetter () — проверяет, является ли символ буквой Java;

□ isLetterOrDigit () — проверяет, является ли символ буквой или цифрой Unicode;

□ isLowerCase () — определяет, записан ли символ в нижнем регистре;

□ isSpaceChar () — выясняет, является ли символ пробелом в смысле Unicode;

□ isTitleCase () — проверяет, является ли символ титульным;

□ isUnicodeIdentifierPart ( ) - выясняет, можно ли использовать символ в именах

Unicode;

□ isUnicodeIdentifierStart () — проверяет, является ли символ буквой Unicode;

□ isUpperCase () — проверяет, записан ли символ в верхнем регистре;

□ isWhitespace () — выясняет, является ли символ пробельным.

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

Листинг 4.5 демонстрирует использование этих методов, а на рис. 4.3 показан вывод этой программы.

Листинг 4.5. Методы класса Character в программе CharacterTest

class CharacterTest{

public static void main(String[] args){

char ch = ’9’;

Character cl = Character.valueOf(ch);

System.out.println("ch = " + ch);

System.out.println("c1.charValue() = " + cl.charValue());

System.out.println("number of ’A’ = " + Character.digit('A', 16));

System.out.println("digit for 12 = " +

Character.forDigit(12, 16));

System.out.println("c1 = " + c1.toString());

System.out.println("ch isDefined? " +

Character.isDefined(ch));

System.out.println("ch isDigit? " +

Character.isDigit(ch));

System.out.println("ch isIdentifierIgnorable? " + Character.isIdentifierIgnorable(ch));

System.out.println("ch isISOControl? " + Character.isISOControl(ch));

System.out.println("ch isJavaIdentifierPart? " + Character.isJavaIdentifierPart(ch));

System.out.println("ch isJavaIdentifierStart? " + Character.isJavaIdentifierStart(ch)) ;

System.out.println("ch isLetter? " + Character.isLetter(ch));

System.out.println("ch isLetterOrDigit? " + Character.isLetterOrDigit(ch));

System.out.println("ch isLowerCase? " + Character.isLowerCase(ch));

System.out.println("ch isSpaceChar? " + Character.isSpaceChar(ch));

System.out.println("ch isTitleCase? " + Character.isTitleCase(ch)) ;

System.out.println("ch isUnicodeIdentifierPart? " + Character.isUnicodeIdentifierPart(ch));

System.out.println("ch isUnicodeIdentifierStart? " + Character.isUnicodeIdentifierStart(ch)) ;

System.out.println("ch isUpperCase? " + Character.isUpperCase(ch));

System.out.println("ch isWhitespace? " + Character.isWhitespace(ch));

}

}

Рис. 4.3. Методы класса Character в программе CharacterTest

В класс Character вложены классы Subset и UnicodeBlock, причем класс UnicodeBlock и еще один класс, InputSubset, являются расширениями класса Subset, как это видно на рис. 4.1. Объекты этого класса содержат подмножества кодировки Unicode.

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

Вместе с классами-оболочками удобно рассмотреть два класса для работы со сколь угодно большими числами.

 

Класс BigInteger

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

Для того чтобы было можно производить целочисленные вычисления с любой разрядностью, в состав Java API введен класс BigInteger, хранящийся в пакете java.math. Этот класс расширяет класс Number, следовательно, в нем переопределены методы

doubleValue(), floatValue(), intValue(), longValue(). Методы byteValue() и shortValue() не переопределены, а прямо наследуются от класса Number.

Действия с объектами класса BigInteger не приводят ни к переполнению, ни к приведению по модулю. Если результат операции велик, то число разрядов просто наращивается. Числа хранятся в двоичной форме с дополнительным кодом.

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

Шесть конструкторов класса создают объект класса BigInteger из строки символов (знака числа и цифр), массива байтов или задают случайное число. Чаще всего используются три конструктора:

□ BigInteger(String value) — объект будет хранить большое целое число, заданное строкой цифр, перед которыми может стоять знак минус;

□ BigInteger(String value, int radix) — задается строка цифр со знаком value, записанная в системе счисления с основанием radix;

□ BigInteger(byte[] value) — объект будет хранить большое целое число, заданное массивом value, содержащим двоичное представление числа в дополнительном коде.

Три константы — zero, one и ten — моделируют нуль, единицу и число десять в операциях с объектами класса BigInteger.

Метод toByteArray() преобразует объект в массив байтов.

Большинство методов класса BigInteger моделируют целочисленные операции и функции, возвращая объект класса BigInteger:

□ abs () — возвращает объект, содержащий абсолютное значение числа, хранящегося в данном объекте this;

□ add (x) — операция сложения this + x;

□ and(x) — операция побитовой конъюнкции this & x;

□ andNot(x) — операция побитовой дизъюнкции с дополнением this & (~x);

□ divide (x) — операция деления this / x;

□ divideAndRemainder (x) - возвращает массив из двух объектов класса BigInteger, со

держащих частное и остаток от деления this на x;

□ gcd(x) — наибольший общий делитель абсолютных значений объекта this и аргумента x;

□ max(x) — наибольшее из значений объекта this и аргумента x;

□ min(x) — наименьшее из значений объекта this и аргумента x;

□ mod(x) — остаток от деления объекта this на аргумент метода x;

□ modInverse (x) — остаток от деления числа, обратного объекту this, на аргумент x;

□ modPow(n, m) — остаток от деления объекта this, возведенного в степень n, на m;

□ multiply(x) — операция умножения this * x;

□ negate () — перемена знака числа, хранящегося в объекте;

□ not () — операция отрицания -this;

□ or(x) — операция побитовой дизъюнкции this | x;

□ pow(n) — операция возведения числа, хранящегося в объекте, в степень n;

□ remainder(x) — операция взятия остатка от деления this % x;

□ shiftLeft (n) — операция сдвига влево this << n;

□ shiftRight (n) — операция арифметического сдвига вправо this >> n;

□ signum() — функция sign(x);

□ subtract (x) — операция вычитания this - x;

□ xor(x ) — операция "исключающее ИЛИ" this л x.

В листинге 4.6 приведены примеры использования данных методов, а рис. 4.4 показывает результаты выполнения этого листинга.

Листинг 4.6. Методы класса BigInteger В Программе BiglntegerTest

import java.math.BigInteger; class BigIntegerTest{

public static void main(String[] args){

BigInteger a = new BigInteger("99999999999999999"); BigInteger b = new BigInteger("88888888888888888888"); System.out.println("bits in a = " + a.bitLength()); System.out.println("bits in b = " + b.bitLength()); System.out.println("a + b = " + a.add(b)); System.out.println("a & b = " + a.and(b)); System.out.println("a & ~b = " + a.andNot(b)); System.out.println("a / b = " + a.divide(b));

BigInteger[] r = a.divideAndRemainder(b);

System.out.println("a / b: q = " + r[0] + ", r = " + r[1]); System.out.println("gcd(a, b) = " + a.gcd(b)); System.out.println("max(a, b) = " + a.max(b)); System.out.println("min(a, b) = " + a.min(b)); System.out.println("a mod b = " + a.mod(b)); System.out.println("1/a mod b = " + a.modInverse(b)); System.out.println("aAn mod b = " + a.modPow(a, b));

System.out.println("a * b = " + a.multiply(b)); System.out.println("-a = " + a.negate()); System.out.println("~a = " + a.not()); System.out.println("a | b = " + a.or(b)); System.out.println("a л 3 = " + a.pow(3)); System.out.println("a % b = " + a.remainder(b)); System.out.println("a << 3 = " + a.shiftLeft(3)); System.out.println("a >> 3 = " + a.shiftRight(3)); System.out.println("sign(a) = " + a.signum()); System.out.println("a — b = " + a.subtract(b)); System.out.println("a л b = " + a.xor(b));

}

}

Рис. 4.4. Методы класса BigInteger в программе BigIntegerTest

Обратите внимание на то, что в программу листинга 4.6 надо импортировать пакет

j ava.math.

 

Класс BigDecimal

Класс BigDecimal расположен в пакете j ava.math. Каждый объект этого класса хранит два целочисленных значения: мантиссу вещественного числа в виде объекта класса BigInteger и неотрицательный десятичный порядок числа типа int. Например, для числа 76,34862 будет храниться мантисса 7 634 862 в объекте класса BigInteger и порядок 5 как целое число типа int. Таким образом, мантисса может содержать любое количество цифр, а порядок ограничен значением константы Integer.MAX_VALUE.

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

□ round_ceiling — округление в сторону большего целого;

□ round_down — округление к нулю, к меньшему по модулю целому значению;

□ round_floor — округление к меньшему целому;

□ round_half_down — округление к ближайшему целому, среднее значение округляется к меньшему целому;

□ round_half_even — округление к ближайшему целому, среднее значение округляется к четному числу;

□ round_half_up — округление к ближайшему целому, среднее значение округляется к большему целому;

□ round_unnecessary — предполагается, что результат будет целым, и округление не понадобится;

□ round_up — округление от нуля, к большему по модулю целому значению.

Три константы — zero, one и ten — моделируют вещественные нуль, единицу и вещественное число десять в операциях с объектами класса BigDecimal.

В классе BigDecimal около двадцати конструкторов. Четыре из них были введены еще в Java 2.

□ BigDecimal (BigInteger bi) - объект будет хранить большое целое bi, порядок равен

нулю;

□ BigDecimal(BigInteger mantissa, int scale) — задается мантисса mantissa и неотрицательный порядок scale объекта; если порядок scale отрицателен, возникает исключительная ситуация;

□ BigDecimal(double d) — объект будет содержать вещественное число удвоенной точности d; если значение d бесконечно или NaN, то возникает исключительная ситуация;

□ BigDecimal (String val) - число задается строкой символов val, которая должна со

держать запись числа по правилам языка Java.

При использовании третьего из перечисленных конструкторов возникает неприятная особенность, отмеченная в документации. Поскольку вещественное число при переводе в двоичную форму представляется, как правило, бесконечной двоичной дробью, то при создании объекта, например BigDecimal (0.1), мантисса, хранящаяся в объекте, окажется очень большой. Она показана на рис. 4.5. Но при создании такого же объекта четвертым конструктором, BigDecimal ("0.1"), мантисса будет равна просто 1.

Остальные конструкторы определяют точность представления числового значения объекта и правила его округления с помощью объекта класса MathContext или непосредственно.

В классе переопределены методы doubleValue (), floatValue (), intValue (), longValue ( ).

Три константы — zero, one и ten — моделируют нуль, единицу и число десять в операциях с объектами класса BigDecimal.

Большинство методов этого класса моделируют операции с вещественными числами. Они возвращают объект класса BigDecimal. Ниже в описании методов буква x обозначает объект класса BigDecimal, буква n — целое значение типа int, буква r — способ округления, одну из восьми перечисленных ранее констант:

□ abs () — абсолютное значение объекта this;

□ add (x) — операция сложения this + x;

□ divide (x, r) — операция деления this / x с округлением по способу r;

□ divide (x, n, r) — операция деления this / x с изменением порядка и округлением по способу r;

□ max(x) — наибольшее из this и x;

□ min(x) — наименьшее из this и x;

□ movePointLeft (n) — сдвиг влево на n разрядов;

□ movePointRight(n) — сдвиг вправо на n разрядов;

□ multiply(x) — операция умножения this * x;

□ negate () — возвращает объект с обратным знаком;

□ scale () — возвращает порядок числа;

□ setScale(n) — устанавливает новый порядок n;

□ setScale (n, r) — устанавливает новый порядок n и округляет число при необходимости по способу r;

□ signum () — знак числа, хранящегося в объекте;

□ subtract (x) — операция вычитания this — x;

□ toBiginteger () — округление числа, хранящегося в объекте;

□ unscaledValue () — возвращает мантиссу числа;

□ upl () — возвращает расстояние до следующего числа.

Листинг 4.7 показывает примеры использования этих методов, а рис. 4.5 — вывод результатов.

Начиная с версии Java SE 5 в класс BigDecimal введено еще много методов преобразования объекта и получения его характеристик.

Листинг 4.7. Методы класса BigDecimal в программе BigDecimalTest

import java.math.*; class BigDecimalTest{

public static void main(String[] args){

BigDecimal x = new BigDecimal("-12345.67890123456789");

BigDecimal y = new BigDecimal("345.7896e-4");

BigDecimal z = new BigDecimal(new BigInteger("123456789"), 8); System.out.println("|x| = " + x.abs());

System.out.println("x + y = " + x.add(y));

System.out.println("x / y = " + x.divide(y, BigDecimal.ROUND DOWN)); System.out.println("x / y = " + x.divide(y, 6, BigDecimal.ROUND HALF EVEN)); System.out.println("max(x, y) = " + x.max(y));

System.out.println("min(x, y) = " + x.min(y));

System.out.println("x << 3 = " + x.movePointLeft(3)); System.out.println("x >> 3 = " + x.movePointRight(3)); System.out.println("x * y = " + x.multiply(y)); System.out.println("-x = " + x.negate());

System.out.println("scale of x = " + x.scale());

System.out.println("increase scale of x to 20 = " + x.setScale(20)); System.out.println("decrease scale of x to 10 = " + x.setScale(10, BigDecimal.ROUND HALF UP));

System.out.println("sign(x) = " + x.signum());

System.out.println("x — y = " + x.subtract(y)); System.out.println("round x = " + x.toBigInteger()); System.out.println("mantissa of x = " + x.unscaledValue()); System.out.println("mantissa of 0.1 =\n= " +

new BigDecimal(0.1).unscaledValue());

}

}

Рис. 4.5. Методы класса BigDecimal в программе BigDecimalTest

Приведем еще один пример. Напишем простенький калькулятор, выполняющий четыре арифметических действия с числами любой величины. Он работает из командной строки. Программа представлена в листинге 4.8, а примеры использования калькулятора — на рис. 4.6.

Листинг 4.8. Простейший калькулятор

import java.math.*; class Calc{

public static void main(String[] args){ if (args.length < 3){

System.err.println("Usage: java Calc operand operator operand"); return;

}

BigDecimal a = new BigDecimal(args[0]);

BigDecimal b = new BigDecimal(args[2]); switch (args[1].charAt(0)){

case ' + ':
case '-':
case '*':
case в– /' :
default :

System.out.println(a.add(b)); break;

System.out.println(a.subtract(b)); break; System.out.println(a.multiply(b)); break; System.out.println(a.divide(b,

BigDecimal.ROUND_HAL F_EVEN)); break; System.out.println("Invalid operator");

}

Рис. 4.6. Результаты работы калькулятора

Почему символ умножения — звездочка — заключен на рис. 4.6 в кавычки? Приверженцам ОС UNIX это понятно, а для других дадим краткое пояснение.

Это особенность операционной системы, а не языка Java. Введенную с клавиатуры строку вначале просматривает командная оболочка (shell) операционной системы, а звездочка для нее — указание подставить на это место все имена файлов из текущего каталога. Оболочка сделает это, и интерпретатор Java получит от нее длинную строку, в которой вместо звездочки стоят имена файлов, отделенные друг от друга пробелом.

Звездочка в кавычках понимается командной оболочкой как обычный символ. Командная оболочка снимает кавычки и передает интерпретатору Java звездочку, что нам и надо.

Класс Class

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

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

В классе Class нет конструкторов, экземпляр этого класса создается исполняющей системой Java во время загрузки класса и предоставляется методом getclass () класса Object, например:

String s = "Это строка";

Class c = s.getClass();

Таким образом, у каждого действующего в программе объекта есть ссылка на экземпляр класса Class, содержащий описание класса этого объекта. Такое свойство объекта называется рефлексией (reflection). Кроме того, мы можем получить такую ссылку на классы по их имени.

Статический метод forName(String class) класса Class возвращает объект класса Class для класса, указанного в аргументе, например:

Class c1 = Class.forName("java.lang.String");

Третий способ получения экземпляра класса Class — к имени класса через точку добавить слово class:

Class c2 = java.lang.String.class;

Логические методы isAnnotation(), isArray(), isInterface(), isEnum(), isPrimitive() позволяют уточнить, не является ли объект аннотацией, массивом, интерфейсом, перечислением или примитивным типом.

Если объект ссылочного типа, то можно извлечь сведения о вложенных классах, конструкторах, методах и полях методами getDeclaredClasses(), getDeclaredConstructors(), getDeclaredMethods(), getDeclaredFields() в виде массива классов: Class, Constructor, Method, Field соответственно. Последние три класса расположены в пакете j ava. lang.reflect и содержат сведения о конструкторах, полях и методах аналогично тому, как класс Class хранит сведения о классах.

Методы getClasses (), getConstructors (), getInterfaces (), getMethods (), getFields ( ) возвращают такие же массивы, но не всех, а только открытых членов класса.

Метод getSuperclass() возвращает суперкласс объекта ссылочного типа, getPackage ( ) — пакет, getModifiers() — модификаторы класса в битовой форме. Модификаторы можно затем расшифровать методами класса Modifier из пакета java.lang.reflect.

Листинг 4.9 показывает применение этих методов, а рис. 4.7 — вывод результатов.

Листинг 4.9. Методы класса class в программе ciassTest

import java.lang.reflect.*; class ClassTest{

public static void main(String[] args){

Class c = null, c1 = null, c2 = null;

Field[] fld = null;

String s = "Some string";

c = s.getClass();

try{

cl = Class.forName("java.lang.String"); // Старый стиль

c2 = java.lang.String.class; if (!c1.isPrimitive())

fld = c1.getDeclaredFields(); }catch(Exception e){}

System.out.println("Superclass c: " System.out.println("Package c: ' System.out.println("Modi fiers c: ' for(int i = 0; i < fld.length; i++) System.out.println(fld[i]);

}

}

// Новый стиль

// Все поля класса String

+ c);

+ c1);

+ c2);

+ c.getSuperclass()); + c.getPackage());

+ c.getModifiers());

Методы, возвращающие свойства классов, вызывают исключительные ситуации, требующие обработки. Поэтому в программу введен блок try{}catch(){}. Рассмотрение обработки исключительных ситуаций мы откладываем до главы 21.

Рис. 4.7. Методы класса Class в программе ClassTest

Начиная с версии Java SE 5 класс Class сделан настраиваемым: Class — это описание класса string, Class — описание класса Long и т. д. Это полезно, когда ссылка на класс Class передается в метод как параметр и надо определить, на какой же класс она направлена.

Вопросы для самопроверки

1. Зачем кроме примитивных типов в язык Java введены еще соответствующие классы-оболочки?

2. Можно ли использовать объекты числовых классов-оболочек в арифметических выражениях?

3. Какое наибольшее целое значение можно занести в объект класса BigInteger?

4. Какое наибольшее вещественное значение можно занести в объект класса BigDecimal?

5. Можно ли использовать в одном выражении значения примитивных типов и распакованные значения числовых классов-оболочек?

6. Для чего в язык Java введены настраиваемые типы?

7. Можно ли создавать настраиваемые интерфейсы или настраиваемыми могут быть только классы?

8. Должны ли методы настраиваемого класса быть настраиваемыми?

9. Можно ли создавать настраиваемые методы в обычных, не настраиваемых классах?

ГЛАВА 5

 

Работа со строками

Очень большое место в обработке информации занимает работа с текстами. Как и многое другое, текстовые строки в языке Java являются объектами. Они представляются экземплярами класса String или класса StringBuilder. В многопоточной среде вместо класса StringBuilder, не обеспечивающего синхронизацию, следует использовать класс stringBuffer, но эти вопросы мы отложим до главы 23.

Класс StringBuilder введен в стандартную библиотеку Java, начиная с версии Java SE 5, для ускорения работы с текстом в одном подпроцессе.

Все эти классы реализуют интерфейс charSequence, в котором описаны общие методы работы со строками любого типа. Таких методов немного:

□ length () — возвращает количество символов в строке;

□ charAt (int pos) - возвращает символ, стоящий в позиции pos строки. Символы в

строке нумеруются, начиная с нуля;

□ subSequence (int start, int end) - возвращает подстроку, начинающуюся с позиции

start и заканчивающуюся перед позицией end исходной строки.

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

Конечно, можно занести текст в массив символов типа char или даже в массив байтов типа byte, но тогда вы не сможете использовать готовые методы работы с текстовыми строками.

Зачем в язык введены три класса для хранения строк? В объектах класса String хранятся строки-константы неизменной длины и содержания, так сказать, отлитые в бронзе. Это значительно ускоряет обработку строк и позволяет экономить память. Компилятор создает только один экземпляр строки класса String и направляет все ссылки на него. Длину строк, хранящихся в объектах классов StringBuilder и StringBuffer, можно менять, вставляя и добавляя строки и символы, удаляя подстроки или сцепляя несколько строк в одну. Во многих случаях, когда надо изменить длину строки типа String, компилятор Java неявно преобразует ее к типу StringBuilder или StringBuffer, меняет длину, потом преобразует обратно в тип String. Например, следующее действие:

String s = "Это" + " одна " + "строка";

компилятор выполнит примерно так:

String s = new StringBuffern.appendC^To'^.appendC РѕРґРЅР° ")

.append("строка").toString();

Будет создан объект класса StringBuffer или класса StringBuilder, в него методом append ( ) последовательно будут добавлены строки "Это", " одна ", "строка", и получившийся объект класса StringBuffer или StringBuilder будет приведен к типу String методом toString ( ).

Напомним, что символы в строках хранятся в кодировке Unicode, в которой каждый символ занимает два байта. Тип каждого символа — char.

Класс String

Перед работой со строкой ее следует создать, как и объект всякого класса. Это можно сделать разными способами.

Как создать строку

Самый простой способ создать строку — это организовать ссылку типа String на строку-константу:

String s1 = "Это строка.";

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

String s2 = "Это длинная строка типа String, " +

"записанная в двух строках исходного текста";

Замечание

Не забывайте о разнице между пустой строкой String s = "", не содержащей ни одного символа, и пустой ссылкой String s = null, не указывающей ни на какую строку и не являющейся объектом.

Самый правильный способ создать объект с точки зрения ООП — это вызвать его конструктор в операции new. Класс String предоставляет вам более десяти конструкторов:

□ String () — создается объект с пустой строкой;

□ String (String str) — конструктор копирования: из одного объекта создается его точная копия, поэтому данный конструктор используется редко;

□ String (StringBuffer str) -преобразованная копия объекта класса StringBuffer;

□ String(StringBuilder str) — преобразованная копия объекта класса StringBuilder;

□ String(byte[] byteArray) — объект создается из массива байтов byteArray;

□ String (char [ ] charArray) — объект создается из массива charArray символов Unicode;

□ String(byte[ ] byteArray, int offset, int count) — объект создается из части массива байтов byteArray, начинающейся с индекса offset и содержащей count байтов;

□ String (char [ ] charArray, int offset, int count) — то же, но массив состоит из символов Unicode;

□ String (int [ ] intArray, int offset, int count) -то же, но массив состоит из символов

Unicode, записанных в массив целого типа, что позволяет использовать символы Unicode, занимающие больше двух байтов;

□ String(byte [ ] byteArray, String encoding) — символы, записанные в массиве байтов, задаются в Unicode-строке с учетом кодировки encoding;

□ String(byte[] byteArray, int offset, int count, String encoding) — то же самое, но только для части массива;

□ String(byte [ ] byteArray, Charset charset) — символы, записанные в массиве байтов, задаются в Unicode-строке с учетом кодировки, заданной объектом charset;

□ String(byte[] byteArray, int offset, int count, Charset charset) — то же самое, но только для части массива.

При неправильном задании индексов offset, count или кодировки encoding возникает исключительная ситуация.

Конструкторы, использующие массив байтов byteArray, предназначены для создания Unicode-строки из массива байтовых ASCII-кодировок символов. Такая ситуация возникает при чтении ASCII-файлов, извлечении информации из базы данных или при передаче информации по сети.

В самом простом случае компилятор для получения двухбайтовых символов Unicode добавит к каждому байту старший нулевой байт. Получится диапазон '\u0000' — '\u00FF' кодировки Unicode, соответствующий кодам Latinl. Тексты, записанные кириллицей, будут выведены неправильно.

Если же на компьютере сделаны местные установки, как говорят на жаргоне "установлена локаль" (locale) (в MS Windows это выполняется утилитой Regional Options (Язык и стандарты) в окне Control Panel (Панель управления)), то компилятор, прочитав эти установки, создаст символы Unicode, соответствующие местной кодовой странице. В русифицированном варианте MS Windows это обычно кодовая страница CP1251.

Если исходный массив с кириллическим ASCII-текстом был в кодировке CP1251, то строка Java будет создана правильно. Кириллица попадет в свой диапазон '\u0400'— '\u04FF' кодировки Unicode.

Но у кириллицы есть еще по меньшей мере четыре кодировки:

□ в MS-DOS применяется кодировка CP866;

□ в UNIX обычно применяется кодировка KOI8-R;

□ на компьютерах Apple Macintosh используется кодировка MacCyrillic;

□ есть еще и международная кодировка кириллицы ISO8859-5.

Например, байт 11100011 (0xE3 — в шестнадцатеричной форме) в кодировке CP1251 представляет кириллическую букву г, в кодировке CP866 — букву у, в кодировке KOI8-R — букву ц, в ISO8859-5 — букву у, в MacCyrillic — букву г.

Если исходный кириллический ASCII-текст был в одной из этих кодировок, а местная кодировка — CP1251, то Unicode-символы строки Java не будут соответствовать кириллице.

В этих случаях применяются последние четыре конструктора, в которых параметром encoding или charset указывается, какую кодовую таблицу использовать конструктору при создании строки.

Листинг 5.1 показывает различные случаи записи кириллического текста. В нем создаются три массива байтов, содержащих слово "Россия" в трех кодировках:

□ массив byteCp1251 содержит слово "Россия" в кодировке CP1251;

□ массив byteCp866 содержит слово "Россия" в кодировке CP866;

□ массив byteKOI8R содержит слово "Россия" в кодировке KOI8-R.

Из каждого массива создаются по три строки с использованием трех кодовых таблиц.

Кроме того, из массива символов c[] создается строка s1, из массива байтов, записанного в кодировке CP866, создается строка s2. Наконец, создается ссылка s3 на строку-константу.

Листинг 5.1. Создание кириллических строк

class StringTest{

null, winLikeUNIX = null
null, dosLikeUNIX = null
null, unixLikeUNIX = null

public static void main(String[] args){ String winLikeWin = null, winLikeDOS String dosLikeWin = null, dosLikeDOS String unixLikeWin = null, unixLikeDOS String msg = null; byte[] byteCp1251 = {

(byte)0xD0, (byte)0xEE, (byte)0xF1, (byte)0xF1, (byte)0xE8, (byte)0xFF

};

byte[] byteCp866 = {

(byte)0x90, (byte)0xAE, (byte)0xE1, (byte)0xE1, (byte)0xA8, (byte)0xEF

};

byte[] byteKOI8R = {

(byte)0xF2, (byte)0xCF, (byte)0xD3, (byte)0xD3, (byte)0xC9, (byte)0xD1

};

char[] c = {'Р ', 'Рѕ', 'СЃ', 'СЃ', 'Рё', 'СЏ'};

String s1 = new String(c);

String s2 = new String(byteCp866); // Для консоли MS Windows

String s3 = "Р РѕСЃСЃРёСЏ";

System.out.println(); try{

// Сообщение в Cp866 для вывода на консоль MS Windows
msg = new String("\ "Р РѕСЃСЃРёСЏ\" РІ ".getBytes("Cp866") , "Cp1251");
winLikeWin = new String(byteCp1251, "Cp1251"); // Правильно
winLikeDOS = new String(byteCp1251, "Cp866");
winLikeUNIX = new String(byteCp1251, "KOI8-R");
dosLikeWin = new String(byteCp866, "Cp1251"); // Для консоли
dosLikeDOS = new String(byteCp866, "Cp866"); // Правильно
dosLikeUNIX = new String(byteCp866, "KOI8-R") ;
unixLikeWin = new String(byteKOI8R, "Cp1251");
unixLikeDOS = new String(byteKOI8R, "Cp866");
unixLikeUNIX = new String(byteKOI8R, "KOI8-R") ; // Правильно

System.out.print(msg + "Cp1251: ");

System.out.write(byteCp1251);

System.out.println();

System.out.print(msg + "Cp866 : ");

System.out.write(byteCp866);

System.out.println();

System.out.print(msg + "KOI8-R: ") ;

System.out.write(byteKOI8R);

}catch(Exception e){ e.printStackTrace();

}

System.out.println();

System.out.println();

"char array : II + s1);
"default encoding: II + s2);
"string constant : II + s3);
"Cp1251 -> Cp1251 II + winLikeWin);
"Cp1251 -> Cp866 : II + winLikeDOS);
"Cp1251 -> KOI8-R II + winLikeUNIX);
"Cp866 -> Cp1251 II + dosLikeWin);
"Cp866 -> Cp866 : II + dosLikeDOS);
"Cp866 -> KOI8-R II + dosLikeUNIX);
"KOI8-R -> Cp1251 II + unixLikeWin);
"KOI8-R -> Cp866 : II + unixLikeDOS);
"KOI8-R -> KOI8-R II + unixLikeUNIX)

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

System.out.println();

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

System.out.println(msg +

}

}

Все эти данные выводятся на консоль MS Windows 2000, как показано на рис. 5.1.

Рис. 5.1. Вывод кириллической строки на консоль MS Windows 2000

В первые три строки консоли без преобразования в Unicode выводятся массивы байтов

byteCp1251, byteCp866 и byteKOI8R. Это выполняется методом write() класса FilterOutputStream из пакета java.io.

В следующие три строки консоли выведены строки Java, полученные из массива символов c[], массива byteCp866 и строки-константы.

Далее строки консоли содержат преобразованные массивы.

Вы видите, что на консоль правильно выводится только массив в кодировке CP866, записанный в строку с использованием кодовой таблицы CP1251. В чем дело? Здесь свой вклад в проблему русификации вносит вывод потока символов на консоль или в файл.

Как уже упоминалось в главе 1, в консольное окно Command Prompt операционных систем MS Windows текст выводится в кодировке CP866.

Для того чтобы учесть это, слова "\"Россия\" в" преобразованы в массив байтов, содержащий символы в кодировке CP866, а затем переведены в строку msg.

В предпоследней строке рис. 5.1 сделано перенаправление вывода программы в файл codes.txt. В MS Windows вывод текста в файл происходит в кодировке CP1251. На рис. 5.2 показано содержимое файла codes.txt в окне программы Notepad (Блокнот).

Рис. 5.2. Вывод кириллической строки в файл

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

Вопросы русификации мы еще будем обсуждать в главах 9 и 24, а пока заметьте, что при создании строки из массива байтов лучше указывать ту же самую кириллическую кодировку, в которой записан массив. Тогда вы получите строку Java с правильными символами Unicode.

При выводе же строки на консоль, в окно, в файл или при передаче по сети лучше преобразовать строку Java с символами Unicode по правилам вывода в нужное место.

Еще один способ создать строку — это использовать два статических метода:

copyValueOf(char[] charArray);

copyValueOf(char[] charArray, int offset, int length);

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

char[] c = {'C', ’и’, ’м’, ’в’, ’о’, ’л’, ’ь’, ’и1, ’ы’, ’й’};

String s1 = String.copyValueOf(c);

String s2 = String.copyValueOf(c, 3, 7);

получим в объекте s1 строку "Символьный", а в объекте s2-строку "вольный".

Упражнение

1. Потренируйтесь в преобразованиях строки в массивы байтов с разной кириллической кодировкой.

Сцепление строк

Со строками можно производить операцию сцепления строк (concatenation), обозначаемую знаком плюс (+). Эта операция создает новую строку, просто составленную из состыкованных первой и второй строк, как показано в начале данной главы. Ее можно применять и к константам, и к переменным. Например:

String attention = "Внимание: ";

String s = attention + "неизвестный символ";

Вторая операция — присваивание += — применяется к переменным в левой части:

attention += s;

Поскольку операция + перегружена со сложения чисел на сцепление строк, встает вопрос о приоритете этих операций. У сцепления строк приоритет выше, чем у сложения, поэтому записав "2" + 2 + 2, получим строку "222". Но записав 2 + 2 + "2", получим строку "42", поскольку действия выполняются слева направо. Если же запишем "2" + (2 + 2), то получим "24".

Кроме операции сцепления соединить строки можно методом concat (), например:

String s = attention.concat("иеизвестиый символ");

Как узнать длину строки

Для того чтобы узнать длину строки, т. е. количество символов в ней, надо обратиться к методу length ( ):

String s = "Write once, run anywhere."; int len = s.length();

или еще проще

int len = "Write once, run anywhere.".length();

поскольку строка-константа — полноценный объект класса String.

Заметьте, что строка — это не массив, у нее нет поля length.

Внимательный читатель, изучивший рис. 4.7, готов со мной не согласиться. Ну что же, действительно, символы хранятся в массиве, но он закрыт, как и все поля класса String.

Логический метод isEmpty(), появившийся в Java SE 6, возвращает true, если строка пуста, в ней нет ни одного символа.

Как выбрать символы из строки

Выбрать символ с индексом ind (индекс первого символа равен нулю) можно методом charAt(int ind). Если индекс ind отрицателен или не меньше, чем длина строки, возникает исключительная ситуация. Например, после определения

char ch = s.charAt(3);

переменная ch будет иметь значение 't' .

Все символы строки в виде массива символов можно получить методом toCharArray( ).

Если же надо включить в массив символов dst, начиная с индекса ind массива, подстроку от индекса begin включительно до индекса end исключительно, то используйте метод

getChars(int begin, int end, char[] dst, int ind) типа void. В массив будет записано end - begin символов, которые займут элементы массива, начиная с индекса ind до индекса ind + (end — begin) — 1.

Этот метод создает исключительную ситуацию в следующих случаях:

□ ссылка dst == null;

□ индекс begin отрицателен;

□ индекс begin больше индекса end;

□ индекс end больше длины строки;

□ индекс ind отрицателен;

□ ind + (end — begin) больше dst.length.

Например, после выполнения

char[] ch = {’К’, ’о’, ’н’, ’е’, ’ц’, ’ ’, ’с’, ’в’, ’е’, ’т’, ’а’};

"Пароль легко иайти".getChars(2, 8, ch, 2);

результат будет таков:

ch = {’К’, ’о’, ’р’, ’о’, ’л’, ’ь’, ' ', ’л’, ’е’, ’т’, ’а’};

Если надо получить массив байтов, содержащий все символы строки в байтовой кодировке ASCII, то используйте метод getBytes (). Этот метод при переводе символов из Unicode в ASCII использует локальную кодовую таблицу.

Если же надо получить массив байтов не в локальной кодировке, а в какой-то другой, применяйте метод getBytes (String encoding) или метод getBytes (Charset encoding).

Так сделано в листинге 5.1 при создании объекта msg. Строка "\"Россия в\"" перекодировалась в массив СР866-байтов для правильного вывода кириллицы в консольное окно Command Prompt операционных систем MS Windows.

Как выбрать подстроку

Метод substring (int begin, int end) выделяет подстроку от символа с индексом begin включительно до символа с индексом end исключительно. Длина подстроки будет равна end — begin. Индекс можно задать любым целым типом, кроме типа long.

Метод substring ( int begin) выделяет подстроку от индекса begin включительно до конца строки.

Если индексы отрицательны, индекс end больше длины строки или begin больше, чем end, то возникает исключительная ситуация.

Например, после выполнения следующего фрагмента

String s = "Write once, run anywhere.";

String sub1 = s.substring(6, 10);

String sub2 = s.substring(16);

получим в строке sub1 значение "once", а в sub2-значение "anywhere.".

Как разбить строку на подстроки

Метод split (String regExp) разбивает строку на подстроки, используя в качестве разделителей символы, входящие в параметр regExp, записывает подстроки в массив строк и возвращает ссылку на этот массив. Сами разделители не входят ни в одну подстроку.

Например, после выполнения следующего фрагмента

String s = "Write:once,:run:anywhere.";

String[] sub = s.split(":");

получим в строке sub[0] значение "Write", в строке sub[1] значение "once,", в строке sub[2] значение "run", а в sub[3] — значение "anywhere.".

Метод split(String regExp, int n) разбивает строку на n подстрок. Если параметр n меньше числа подстрок, то весь остаток строки заносится в последний элемент создаваемого массива строк. Применение метода

String[] sub = s.split(":", 2);

в предыдущем примере даст массив sub из двух элементов со значением sub[0], равным

"Write", и значением sub[1], равным "once, :run:anywhere.".

Разбить строку можно практически на любые подстроки, поскольку значением параметра regExp может быть любое регулярное выражение. Регулярное выражение (regular expression) — это шаблон для отбора строк, составляемый по сложным правилам, изложению которых посвящены целые книги. Регулярные выражения выходят за рамки нашей книги, но все-таки я приведу пример разбиения строки на слова, разделенные пробельными символами:

String[] word = s.split("\\s+");

Как сравнить строки

Операция сравнения == сопоставляет только ссылки на строки. Она выясняет, указывают ли ссылки на одну и ту же строку. Например, для строк

String si = "Какая-то строка";

String s2 = "Другая строка";

сравнение s1 == s2 дает в результате false.

Значение true получится, только если обе ссылки указывают на одну и ту же строку, например после присваивания s1 = s2.

Интересно, что если мы определим s3 так:

String s3 = "Какая-то строка";

то сравнение s1 == s3 даст в результате true, потому что компилятор устроен так, что он создаст только один экземпляр константы "Какая-то строка" и направит на него все ссылки — и ссылку s1, и ссылку s3. Это не приводит к недоразумениям, поскольку строка типа String неизменяема.

Вы, разумеется, хотите сравнивать не ссылки, а содержимое строк. Для этого есть несколько методов.

Логический метод equals(Object obj), переопределенный из класса Object, возвращает true, если параметр obj не равен null, является объектом класса String, и строка, содержащаяся в нем, полностью идентична данной строке вплоть до совпадения регистра букв. В остальных случаях возвращается значение false.

Логический метод equalsIgnoreCase(Object obj) работает так же, но одинаковые буквы, записанные в разных регистрах, считаются совпадающими.

Например, s2.equals("другая строка") даст в результате false, а s2.equalsIgnoreCase( "другая строка" ) возвратит true.

Метод compareTo (String str) возвращает целое число типа int, вычисленное по следующим правилам:

1. Сравниваются символы данной строки this и строки str с одинаковым индексом, пока не встретятся различные символы с индексом, допустим, k или пока одна из строк не закончится.

2. В первом случае возвращается значение this.charAt(k) — str.charAt(k), т. е. разность кодировок Unicode первых несовпадающих символов.

3. Во втором случае возвращается значение this.length() — str.length(), т. е. разность длин строк.

4. Если строки совпадают, возвращается 0.

Если значение str равно null, возникает исключительная ситуация.

Нуль возвращается в той же ситуации, в которой метод equals () возвращает true.

Метод compareToIgnoreCase (String str) производит сравнение без учета регистра букв, точнее говоря, выполняется метод

this.toUpperCase().toLowerCase().compareTo( str.toUpperCase().toLowerCase());

Эти методы не учитывают алфавитное расположение символов в локальной кодировке.

Русские буквы расположены в Unicode по алфавиту, за исключением одной буквы. Заглавная буква Ё находится перед всеми кириллическими буквами, ее код '\u0401', а строчная буква ё — после всех русских букв, ее код '\u0451'.

Если вас такое расположение не устраивает, задайте свое размещение букв с помощью класса RuleBasedCollator из пакета java.text.

Сравнить подстроку данной строки this с подстрокой той же длины len другой строки str можно логическим методом

regionMatches(int ind1, String str, int ind2, int len);

Здесь ind1 — индекс начала подстроки данной строки this, ind2 — индекс начала подстроки другой строки str. Результат false получается в следующих случаях:

□ хотя бы один из индексов ind1 или ind2 отрицателен;

□ хотя бы одно из ind1 + len или ind2 + len больше длины соответствующей строки;

□ хотя бы одна пара символов не совпадает.

Этот метод различает символы, записанные в разных регистрах. Если надо сравнивать подстроки без учета регистров букв, то используйте логический метод:

regionMatches(boolean flag, int ind1, String str, int ind2, int len);

Если первый параметр flag равен true, то регистр букв при сравнении подстрок не учитывается, если false — учитывается.

Как найти символ в строке

Поиск всегда ведется с учетом регистра букв.

Первое появление символа ch в данной строке this можно отследить методом indexOf(int ch), возвращающим индекс этого символа в строке или -1, если символа ch в строке this нет.

Например, "Молоко".^ехО^ 'о') выдаст в результате индекс 1.

Конечно, этот метод реализован так, что он выполняет в цикле последовательные сравнения this. charAt (k++) == ch, пока не получит значение true.

Второе и следующие появления символа ch в данной строке this можно отследить методом indexOf(int ch, int ind). Этот метод начинает поиск символа ch с индекса ind. Если ind < 0, то поиск идет с начала строки, если ind больше длины строки, то символ не ищется, т. е. возвращается -1.

Например, "Молоко".^ех0^'о', indexOf ('о') + 1) даст в результате индекс 3.

Последнее появление символа ch в данной строке this отслеживает метод lastIndexOf(int ch). Он просматривает строку в обратном порядке. Если символ ch не найден, возвращается -1.

Например, "Молоко".lastIndexOf( 'о') даст в результате индекс 5.

Предпоследнее и предыдущие появления символа ch в данной строке this можно отследить методом lastIndexOf(int ch, int ind), который просматривает строку в обратном порядке, начиная с индекса ind. Если ind больше длины строки, то поиск идет от конца строки; если ind < 0, то возвращается -1.

Как найти подстроку

Поиск всегда ведется с учетом регистра букв.

Первое вхождение подстроки sub в данную строку this отыскивает метод indexOf(String sub). Он возвращает индекс первого символа первого вхождения подстроки sub в строку или -1, если подстрока sub не входит в строку this. Например, "Раскраска" . indexOf ("рас") даст в результате 4.

Если вы хотите начать поиск не с начала строки, а с какого-то индекса ind, используйте метод indexOf (String sub, int ind). Если ind < 0, то поиск идет с начала строки; если ind больше длины строки, то символ не ищется, т. е. возвращается -1.

Последнее вхождение подстроки sub в данную строку this можно отыскать методом lastIndexOf(String sub), возвращающим индекс первого символа последнего вхождения подстроки sub в строку this или -1, если подстрока sub не входит в строку this.

Последнее вхождение подстроки sub не во всю строку this, а только в ее начало до индекса ind можно отыскать методом lastIndexOf(String str, int ind). Если ind больше длины строки, то поиск идет от конца строки; если ind < 0, то возвращается -1.

Для того чтобы проверить, не начинается ли данная строка this с подстроки sub, используйте логический метод startsWith(String sub), возвращающий true, если данная строка this начинается с подстроки sub или совпадает с ней, или подстрока sub пуста.

Можно проверить и появление подстроки sub в данной строке this, начиная с некоторого индекса ind логическим методом startsWith(String sub, int ind). Если индекс ind отрицателен или больше длины строки, возвращается false.

Для того чтобы проверить, не заканчивается ли данная строка this подстрокой sub, используйте логический метод endsWith(String sub). Учтите, что он возвращает true, если подстрока sub совпадает со всей строкой или подстрока sub пуста.

Например, if (fileName.endsWith(".java") ) отследит имена файлов с исходными текстами Java.

Перечисленные ранее методы создают исключительную ситуацию, если sub == null.

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

Как изменить регистр букв

Метод toLowerCase () возвращает новую строку, в которой все буквы переведены в нижний регистр, т. е. сделаны строчными.

Метод toUpperCase () возвращает новую строку, в которой все буквы переведены в верхний регистр, т. е. сделаны прописными.

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

toLowerCase(Locale loc); toUpperCase(Locale loc);

Как заменить отдельный символ

Метод replace (char old, char new) возвращает новую строку, в которой все вхождения символа old заменены символом new. Если символа old в строке нет, то возвращается ссылка на исходную строку.

Например, после выполнения "Рука в руку сует хлеб".гер1асе('у', 'е') получим новую строку "Река в реке сеет хлеб".

Регистр букв при замене учитывается.

Как заменить подстроку

Метод replace (String old, String new) возвращает новую строку, в которой все вхождения подстроки old заменены строкой new. Если подстроки old в исходной строке нет, то возвращается ссылка на исходную строку.

Метод replaceAll (String oldRegEx, String new) возвращает новую строку, в которой все вхождения подстроки oldRegEx заменены строкой new. Если подстроки old в исходной строке нет, то возвращается ссылка на исходную строку. В отличие от предыдущего метода аргументом oldRegEx может служить регулярное выражение, пользуясь которым можно сделать очень сложную замену.

Метод replaceFirst (String oldRegEx, String new) возвращает новую строку, в которой сделана только одна, первая, замена.

Регистр букв при замене учитывается.

Как убрать пробелы в начале и конце строки

Метод trim () возвращает новую строку, в которой удалены начальные и конечные символы с кодами, не превышающими '\u0020'.

Как преобразовать в строку данные другого типа

В языке Java принято соглашение — каждый класс отвечает за преобразование других типов в тип данного класса и должен содержать нужные для этого методы.

Класс String содержит восемь статических методов valueOf(type elem) преобразования в строку примитивных типов boolean, char, int, long, float, double, массива char [] и просто объекта типа Obj ect.

Девятый метод valueOf(char[] ch, int offset, int len) преобразует в строку подмассив массива ch, начинающийся с индекса offset и имеющий len элементов.

Кроме того, в каждом классе есть метод toString(), переопределенный или просто унаследованный от класса Obj ect. Он преобразует объекты класса в строку. Фактически метод valueOf () вызывает метод toString() соответствующего класса. Поэтому результат преобразования зависит от того, как реализован метод toString ().

Еще один простой способ — сцепить значение elem какого-либо типа с пустой строкой: "" + elem. При этом неявно вызывается метод elem.toString( ).

Упражнения

2. Подсчитайте количество появлений того или иного символа в заданной строке.

3. Подсчитайте количество слов в заданной строке.

4. Найдите число появлений заданного слова в заданной строке.

 

Класс StringBuilder

Объекты класса StringBuilder — это строки переменной длины. Только что созданный объект имеет буфер определенной емкости (capacity), по умолчанию достаточной для хранения 16 символов. Емкость можно задать в конструкторе объекта.

Как только буфер начинает переполняться, его емкость автоматически увеличивается, чтобы вместить новые символы.

В любое время емкость буфера можно увеличить, обратившись к методу

ensureCapacity(int minCapacity);

Этот метод изменит емкость, только если minCapacity будет больше длины хранящейся в объекте строки. Емкость будет увеличена по следующему правилу. Пусть емкость буфера равна N. Тогда новая емкость будет равна

Max(2 * N + 2, minCapacity)

Таким образом, емкость буфера нельзя увеличить менее чем вдвое.

Методом setLength (int newLength) можно установить любую длину строки. Если она окажется больше текущей длины, то дополнительные символы будут равны '\u0000'. Если она будет меньше текущей длины, то строка окажется обрезанной, последние символы потеряются, точнее, будут заменены символом '\u0000'. Емкость при этом не изменится.

Если число newLength окажется отрицательным, возникнет исключительная ситуация. Совет

Будьте осторожны, устанавливая новую длину объекта.

Количество символов в строке можно узнать, как и для объекта класса String, методом

length (), а емкость — методом capacity ().

Создать объект класса StringBuilder можно только конструкторами.

Конструкторы

В классе StringBuilder четыре конструктора:

□ StringBuilder () — создает пустой объект с емкостью 16 символов;

□ StringBuilder (int capacity) создает пустой объект заданной емкости capacity;

□ StringBuilder(String str) — создает объект емкостью str.length() + 16, содержащий строку str;

□ StringBuilder(CharSequence str) — создает объект, содержащий строку str.

Как добавить подстроку

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

Основной метод append(String str) присоединяет строку str в конец данной строки. Если ссылка str == null, то добавляется строка "null".

Два аналогичных метода работают с параметром типа StringBuffer и CharSequence.

Шесть методов append(type elem) добавляют примитивные типы boolean, char, int, long, float, double, преобразованные в строку.

Два метода присоединяют к строке массив str и подмассив sub символов, преобразованные в строку:

append(char[] str);

append(char[] sub, int offset, int len);

Еще один метод, append(CharSequence sub, int offset, int len), использует параметр типа CharSequence.

Тринадцатый метод, append(Object obj), добавляет просто объект. Перед этим объект obj преобразуется в строку своим методом toString ( ).

Как вставить подстроку

Более десяти методов insert () предназначены для вставки строки, указанной вторым параметром метода, в данную строку. Место вставки задается первым параметром метода, индексом символа строки, перед которым будет сделана вставка. Он должен быть неотрицательным и меньше длины строки, иначе возникнет исключительная ситуация. Строка раздвигается, емкость буфера при необходимости увеличивается. Методы возвращают ссылку на ту же самую, но преобразованную строку.

Основной метод insert(int ind, String str) вставляет строку str в данную строку перед ее символом с индексом ind. Если ссылка str == null, вставляется строка "null".

Например, после выполнения

String s = new StringBuilder("3TO большая строка").

insert(4, "РЅРµ").toString();

получим s == "Это небольшая строка".

Метод sb.insert(sb.length (), "xxx") будет работать так же, как метод sb.append("xxx").

Шесть методов insert(int ind, type elem) вставляют примитивные типы boolean, char,

int, long, float, double, преобразованные в строку.

Два метода вставляют массив str и подмассив sub символов, преобразованные в строку:

insert(int ind, char[] str);

insert(int ind, char[] sub, int offset, int len);

Десятый метод вставляет просто объект: insert(int ind, Object obj). Объект obj перед добавлением преобразуется в строку своим методом toString().

Еще два метода:

insert(int ind, CharSequence str);

insert(int ind, CharSequence sub, int start, int end); работают с параметром типа CharSequence.

Как удалить подстроку

Метод delete (int begin, int end) удаляет из строки символы, начиная с индекса begin включительно до индекса end исключительно; если end больше длины строки, то до конца строки.

Например, после выполнения

String s = new StringBuilderC^TO небольшая строка").

delete(4, 6).toString();

получим s == "Это большая строка".

Если begin отрицательно, больше длины строки или больше end, возникает исключительная ситуация.

Если begin == end, удаление не происходит.

Как удалить символ

Метод deleteCharAt(int ind) удаляет символ с указанным индексом ind. Длина строки уменьшается на единицу.

Если индекс ind отрицателен или больше длины строки, возникает исключительная ситуация.

Как заменить подстроку

Метод replace (int begin, int end, String str) удаляет символы из строки, начиная с индекса begin включительно до индекса end исключительно, а если end больше длины строки, то до конца строки, и вставляет вместо них строку str.

Если begin отрицательно, больше длины строки или больше end, возникает исключительная ситуация.

Разумеется, метод replace () — это последовательное выполнение методов delete ()

Рё insert().

Как перевернуть строку

Метод reverse () меняет порядок расположения символов в строке на обратный. Например, после выполнения

String s = new StringBuilderC^TO небольшая строка"). reverse().toString();

получим s == "акортс яашьлобен отЭ".

Синтаксический разбор строки

Задача разбора введенного текста — парсинг (parsing) — вечная задача программирования, наряду с сортировкой и поиском. Написана масса программ-парсеров (parser), разбирающих текст по различным признакам. Есть даже программы, генерирующие парсеры по заданным правилам разбора: YACC, LEX и др. Большую помощь в разборе строки оказывает метод split ().

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

В пакет java.util входит простой класс StringTokenizer, облегчающий разбор строк.

Класс StringTokenizer

Класс StringTokenizer из пакета java.util небольшой, в нем три конструктора и шесть методов.

Первый конструктор, StringTokenizer(String str), создает объект, готовый разбить строку str на слова, разделенные пробелами, символами табуляции '\t', перевода строки '\n' и возврата каретки '\r'. Разделители не включаются в число слов.

Второй конструктор, StringTokenizer (String str, String delimeters), задает разделители вторым параметром delimeters, например:

StringTokenizer("Казнить,нельзя:пробелов-нет", " \t\n\r,:-");

Здесь первый разделитель — пробел. Потом идут символ табуляции, символ перевода строки, символ возврата каретки, запятая, двоеточие, дефис. Порядок расположения разделителей в строке delimeters не имеет значения. Разделители не включаются в число слов.

Третий конструктор позволяет включить разделители в число слов:

StringTokenizer(String str, String delimeters, boolean flag);

Если параметр flag равен true, то разделители включаются в число слов, если false — нет. Например:

StringTokenizer("a — (b + c) / b * c", " \t\n\r+*-/()", true);

В разборе строки на слова активно участвуют два метода:

□ метод nextToken () возвращает в виде строки следующее слово;

□ логический метод hasMoreTokens () возвращает true, если в строке еще есть слова, и false, если слов больше нет.

Третий метод, countTokens (), возвращает число оставшихся слов.

Четвертый метод, nextToken(String newDelimeters), позволяет "на ходу" менять разделители. Следующее слово будет выделено по новым разделителям newDelimeters; новые разделители действуют далее вместо старых разделителей, определенных в конструкторе или предыдущем методе nextToken ( ).

Оставшиеся два метода, nextElement () и hasMoreElements (), реализуют интерфейс

Enumeration. Они просто обращаются к методам nextToken () и hasMoreTokens ().

Схема разбора очень проста (листинг 5.2).

Листинг 5.2. Разбиение строки на слова

import java.util.*; class MyParser{

public static void main(String[] args){

String s = "Строка, которую мы хотим разобрать на слова"; StringTokenizer st = new StringTokenizer(s, " \t\n\r,.");

while(st.hasMoreTokens()){

// Получаем слово и что-нибудь делаем с ним, например // просто выводим на экран System.out.println(st.nextToken());

}

}

}

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

Заключение

Все методы представленных в этой главе классов написаны на языке Java. Их исходные тексты можно посмотреть, они входят в состав JDK. Это очень полезное занятие. Просмотрев исходный текст, вы получаете полное представление о том, как работает метод.

Исходные тексты хранятся в ZIP-архиве src.zip, лежащем в корневом каталоге JDK, например в каталоге D:\jdk1.7.0.

После распаковки в каталоге jdk1.7.0 появится подкаталог, например, src, а в нем — подкаталоги, соответствующие пакетам и подпакетам JDK, с исходными файлами.

Вопросы для самопроверки

1. Зачем в язык Java введено несколько классов, обрабатывающих строки символов?

2. Какова разница между классами String и StringBuilder?

3. Какова разница между классами StringBuffer и StringBuilder?

4. Что лучше использовать для сцепления строк: операцию сцепления или метод append() класса StringBuilder?

5. Что лучше использовать для разбора строки: метод split() или класс StringTokenizer?

ГЛАВА 6

 

Классы-коллекции

В листинге 5.2 мы разобрали строку на слова. Как их сохранить для дальнейшей обработки?

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

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

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

В языке Java с самых первых версий есть класс Vector, предназначенный для хранения переменного числа элементов самого общего типа Obj ect.

Класс Vector

В классе Vector из пакета java.util хранятся элементы типа Object, а значит, ссылки любого типа. Количество элементов может быть произвольным и не определяться заранее. Элементы получают индексы 0, 1, 2 и т. д. К каждому элементу вектора можно обратиться по индексу, как и к элементу массива.

Кроме количества элементов, называемого размером (size) вектора, есть еще размер буфера — емкость (capacity) вектора. Обычно емкость совпадает с размером вектора, но можно ее увеличить методом ensureCapacity(int minCapacity) или сравнять с размером вектора методом trimToSize ( ).

В Java 2 класс Vector переработан так, чтобы включить его в иерархию классов-коллекций. Для этого добавлено много новых методов, реализующих методы соответствующих интерфейсов-коллекций. Сейчас многие действия можно совершать старыми и новыми методами. Рекомендуется использовать новые методы, поскольку старые могут быть исключены из следующих версий Java.

Как создать вектор

В классе четыре конструктора:

□ Vector () — создает пустой объект нулевой длины с емкостью в 10 элементов;

□ Vector (int capacity) -создает пустой объект указанной емкости capacity;

□ Vector (int capacity, int increment) - формирует пустой объект указанной емкости

capacity и задает число increment, на которое увеличивается емкость при необходимости;

□ Vector(Collection c) — вектор создается по указанной коллекции.

Если число capacity отрицательно, то возникает исключительная ситуация.

После создания вектора его можно заполнять элементами. В векторе разрешено хранить объекты разных типов, поскольку на самом деле в нем хранятся не значения объектов, а ссылки на них. Класс Vector настраиваемый, и если предполагается заполнять вектор ссылками одного и того же типа, то этот тип можно задать при создании вектора в шаблоне (generic) коренного типа, в угловых скобках, по такой схеме:

Vector v = new Vector();

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

Vector v = new Vector<>();

После этого определения компилятор будет следить за тем, чтобы у всех элементов вектора был тип String. Извлечение элементов из вектора не потребует приведения типа.

Как добавить элемент в вектор

Метод add (Object element) позволяет добавить элемент в конец вектора (то же делает старый метод addElement (Obj ect element)).

Методом add(int index, Object element) или старым методом insertElementAt(Object element, int index) можно вставить элемент в указанное место index. Элемент, находившийся на этом месте, и все последующие элементы сдвигаются, их индексы увеличиваются на единицу.

Метод addAll(Collection coll) позволяет добавить в конец вектора все элементы коллекции coll.

Методом addAll (int index, Collection coll) возможно вставить в позицию index все элементы коллекции coll.

Вот пример создания и заполнения вектора:

Vector v = new Vector(); v.add(new Date()); v.add("CTpoKa символов"); v.add(new Integer(10)); v.add(20);

Обратите внимание в этом примере на две последние строки. Первая из них записана по канонам работы с коллекцией: в вектор вместо числа 10 заносится ссылка на объект класса Integer, содержащий это число. В последней строке применета автоматическая упаковка типа: в методе add() записывается просто число 20, метод сам создает необходимую ссылку.

Как заменить элемент

Метод set(int index, Object element) заменяет элемент, стоявший в векторе в позиции index, на элемент element (то же самое позволяет выполнить старый метод

setElementAt(Object element, int index)).

Как узнать размер вектора

Количество элементов в векторе всегда можно узнать методом size().

Метод capacity() возвращает емкость вектора.

Логический метод isEmpty() возвращает true, если в векторе нет ни одного элемента.

Как обратиться к элементу вектора

Обратиться к первому элементу вектора можно методом firstElement(), к последнему - методом lastElement (), к любому элементу- методом get (int index) или старым

методом elementAt (int index).

Эти методы возвращают объект класса Object. Перед использованием его следует привести к нужному типу, например:

String s = (String)v.get(1);

Если при создании вектора шаблоном был указан определенный тип элементов, например,

Vector v = new Vector<>(); v.add("First"); v.add("Second");

то возвращается объект именно этого типа и явное приведение типа не требуется, можно написать просто

String s = v.get(1);

Получить все элементы вектора в виде массива типа Object[] можно методами toArray() и toArray(Object[] a). Второй метод заносит все элементы вектора в массив a, если в нем достаточно места.

Как узнать, есть ли элемент в векторе

Логический метод contains(Object element) возвращает true, если элемент element находится в векторе.

Логический метод containsAll(Collection c) возвращает true, если вектор содержит все элементы указанной коллекции.

Как узнать индекс элемента

Четыре метода позволяют отыскать позицию указанного элемента element:

□ indexOf (Object element) — возвращает индекс первого появления элемента в векторе;

□ indexOf (Obj ect element, int begin) - ведет поиск, начиная с индекса begin включи

тельно;

□ lastIndexOf (Obj ect element) — возвращает индекс последнего появления элемента в векторе;

□ lastIndexOf (Obj ect element, int start) - ведет поиск от индекса start включительно

к началу вектора.

Если элемент не найден, возвращается число -1.

Как удалить элементы

Логический метод remove(Object element) удаляет из вектора первое вхождение указанного элемента element. Метод возвращает true, если элемент найден и удаление произведено.

Метод remove(int index) удаляет элемент из позиции index и возвращает его в качестве своего результата типа Object.

Аналогичные действия позволяют выполнить старые методы типа void:

removeElement(Object element) и removeElementAt(int index), не возвращающие результата.

Удалить диапазон элементов можно методом removeRange(int begin, int end), не возвращающим результата. Удаляются элементы от позиции begin включительно до позиции end исключительно.

Удалить из данного вектора все элементы коллекции coll возможно логическим методом removeAll(Collection coll).

Удалить последние элементы можно, просто урезав вектор методом setSize(int newSize).

Удалить все элементы, кроме входящих в указанную коллекцию coll, разрешает логический метод retainAll (Collection coll).

Удалить все элементы вектора можно методом clear(), старым методом removeAllElements ( ) или обнулив размер вектора методом setSize (0).

Приведем пример работы с вектором. Листинг 6.1 расширяет листинг 5.2, обрабатывая выделенные из строки слова с помощью вектора.

Листинг 6.1. Работа с вектором

import java.util.*;

class MyParser{

public static void main(String[] args){

Vector v = new Vector<>();

String s = "Строка, которую мы хотим разобрать на слова."; StringTokenizer st = new StringTokenizer(s, " \t\n\r,.");

while (st.hasMoreTokens()){

// Получаем слово и заносим в вектор

v.add(st.nextToken()) ; // Добавляем в конец вектора

System.out.println(v.firstElement()); // Первый элемент System.out.println(v.lastElement()); // Последний элемент v.setSize(4); // Уменьшаем число элементов

v.add("собрать."); // Добавляем в конец укороченного вектора. v.set(3, "опять"); // Ставим в позицию 3.

for (int i = 0; i < v.size(); i++) // Перебираем весь вектор.

System.out.print(v.get(i) + " ");

System.out.println();

for (String s: v) // Другой способ перебора элементов вектора.

System.out.print(s + " ");

System.out.println();

}

}

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

Класс Stack

Второй пример коллекции-класс Stack-расширяет класс Vector.

Класс Stack из пакета java.util объединяет элементы в стек.

Стек (stack) реализует порядок работы с элементами подобно магазину винтовки — первым выстрелит патрон, положенный в магазин последним, — или подобно железнодорожному тупику — первым из тупика выйдет вагон, загнанный туда последним. Т а-кой порядок обработки называется LIFO (Last In — First Out, последним пришел — первым ушел).

Перед работой создается пустой стек конструктором Stack ().

Затем на стек кладутся и снимаются элементы, причем доступен только "верхний" элемент, тот, что положен на стек последним.

Дополнительно к методам класса Vector класс Stack содержит пять методов, позволяющих работать с коллекцией как со стеком:

□ push (Obj ect item) -помещает элемент item в стек;

□ pop () — извлекает верхний элемент из стека;

□ peek () — читает верхний элемент, не извлекая его из стека;

□ empty () — проверяет, не пуст ли стек;

□ search(Object item) — находит позицию элемента item в стеке. Верхний элемент имеет позицию 1, под ним элемент 2 и т. д. Если элемент не найден, возвращается -1.

Листинг 6.2 показывает, как можно использовать стек для проверки парности символов в арифметическом выражении, записанном в строку.

Листинг 6.2. Проверка парности скобок

import java.util.*;

class StackTest{

static boolean checkParity(String expression, String open, String close){

Stack stack = new Stack<>();

StringTokenizer st = new StringTokenizer(expression,

" \t\n\r+*/-(){}", true); while (st.hasMoreTokens()){

String tmp = st.nextToken(); if (tmp.equals(open)) stack.push(open); if (tmp.equals(close)) stack.pop();

}

if (stack.isEmpty()) return true; return false;

}

public static void main(String[] args){

System.out.println(

checkParity("a — (b — (c — a) / (b + c) — 2)", "(", ")"));

}

}

Как видите, коллекции значительно облегчают обработку наборов данных с неизвестным заранее числом элементов.

Еще один пример коллекции совсем другого рода — таблицы — предоставляет класс

Hashtable.

 

Класс Hashtable

Класс Hashtable расширяет абстрактный класс Dictionary. В объектах этого класса хранятся пары "ключ — значение". Их можно представить себе как таблицу из двух столбцов. Такие таблицы часто называют словарями или ассоциативными массивами.

Из таких пар "Фамилия И. О. — номер телефона" состоит, например, телефонный справочник.

Еще один пример — анкета. Ее можно представить как совокупность пар "Фамилия — Иванов", "Имя — Петр", "Отчество — Сидорович", "Год рождения — 1975" и т. д.

Подобных примеров можно привести множество.

Каждый объект класса Hashtable кроме размера (size) — количества пар, имеет еще две характеристики: емкость (capacity) — размер буфера, и показатель загруженности (load factor) — процент заполненности буфера, по достижении которого увеличивается емкость таблицы.

Как создать таблицу Hashtable

Для создания объектов класс Hashtable предоставляет четыре конструктора:

□ Hashtable () — создает пустой объект с начальной емкостью в 101 элемент и показателем загруженности 0,75;

□ Hashtable (int capacity) -формирует пустой объект с начальной емкостью capacity и

показателем загруженности 0,75;

□ Hashtable (int capacity, float loadFactor) — создает пустой объект с начальной емкостью capacity и показателем загруженности loadFactor;

□ Hashtable (Map f) — создает объект класса Hashtable, содержащий все элементы отображения f, с емкостью, равной удвоенному числу элементов отображения f, но не менее 11, и показателем загруженности 0,75.

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

Hashtable h = new Hashtable();

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

Hashtable h = new Hashtable<>();

После такого определения компилятор будет следить за типами заносимых в таблицу элементов. Извлечение элементов из таблицы не потребует явного приведения типов.

Как заполнить таблицу Hashtable

Для заполнения объекта класса Hashtable используются два метода:

□ Object put(Object key, Object value) — добавляет пару "key — value", если ключа key не было в таблице, и меняет значение value ключа key, если он уже есть в таблице. Возвращает старое значение ключа или null, если его не было. Если хотя бы один аргумент равен null, возникает исключительная ситуация;

□ void putAll (Map f) — добавляет все элементы отображения f.

В объектах-ключах key должны быть переопределены методы hashCode() и equals ( ), унаследованные от класса Object.

Как получить значение по ключу

Метод get (Object key) возвращает значение элемента с ключом key в виде объекта класса Obj ect или того класса, с которым создана таблица. Если при создании таблицы класс не был указан, то для дальнейшей работы с полученным объектом его следует преобразовать к конкретному типу.

Как узнать наличие ключа или значения

Логический метод containsKey(Object key) возвращает true, если в таблице есть ключ

key.

Логический метод containsValue(Object value) или старый метод contains(Object value) возвращают true, если в таблице есть ключи со значением value.

Логический метод isEmpty() возвращает true, если в таблице нет элементов.

Как получить все элементы таблицы Hashtable

Метод values ( ) представляет все значения value таблицы в виде объекта типа Collection. Все модификации в этом объекте изменяют таблицу, и наоборот.

Метод keySet () предоставляет все ключи key таблицы в виде объекта типа интерфейса Set. Все изменения в этом объекте типа Set корректируют таблицу, и наоборот.

Метод entrySet () представляет все пары "key — value" таблицы в виде объекта типа интерфейса Set. Все модификации в этом объекте типа Set изменяют таблицу, и наоборот.

Итак, таблицу типа Hashtable можно представить в трех формах: в виде коллекции значений, в виде множества ключей или в виде множества пар.

Метод toString () возвращает строку, содержащую все пары таблицы.

Старые методы elements () и keys () возвращают значения и ключи в виде интерфейса

Enumeration.

Как удалить элементы

Метод remove (Obj ect key) удаляет пару с ключом key, возвращая значение этого ключа, если оно есть, и null, если пара с ключом key не найдена.

Метод clear () удаляет все элементы, очищая таблицу.

В листинге 6.3 показано, как можно использовать класс Hashtable для создания телефонного справочника, а на рис. 6.1 — вывод этой программы.

Листинг 6.3. Телефонный справочник

import java.util.*; class PhoneBook{

public static void main(String[] args){

Hashtable yp = new Hashtable<>();

String name = null;

yp.put("John", "123-45-67");

yp.put("Lennon", "567-34-12");

yp.put("Bill", "342-65-87");

yp.put("Gates", "423-83-49");

yp.put("Batman", "532-25-08");

try{

name = args[0];

}catch(Exception e){

System.out.println("Usage: j ava PhoneBook Name"); return;

}

if (yp.containsKey(name))

System.out.println(name + "’s phone = " + yp.get(name));

else

System.out.println("Sorry, no such name");

}

}

Рис. 6.1. Работа с телефонной книгой

Класс Properties

Класс Properties расширяет класс Hashtable таким образом, что в нем хранятся пары ссылок не на произвольный тип, а на строки — пары типа String. Он предназначен в основном для работы с парами "свойства системы — их значения", записанными в файлах свойств.

В классе Properties два конструктора:

□ Properties () — создает пустой объект;

□ Properties (Properties default) — создает объект с заданными парами свойств default.

Кроме унаследованных от класса Hashtable методов в классе Properties есть еще следующие методы:

□ два метода, возвращающих значение ключа-строки в виде строки:

• String getProperty(String key) — возвращает значение по ключу key;

• String getProperty(String key, String defaultValue) — возвращает значение по ключу key; если такого ключа нет, возвращается defaultValue;

□ метод setProperty(String key, String value) добавляет новую пару, если ключа key нет, и меняет значение, если ключ key есть;

□ метод load(inputStream in) загружает свойства из входного потока in;

□ методы list (PrintStream out) и list(PrintWriter out) выводят свойства в выходной поток out;

□ метод store (OutputStream out, String header) выводит свойства в выходной поток out с заголовком header.

Очень простой листинг 6.4 и рис. 6.2 демонстрируют вывод всех системных свойств Java.

Листинг 6.4. Вывод системных свойств

class Prop{

public static void main(String[] args){

System.getProperties().list(System.out);

}

}

Рис. 6.2. Системные свойства

Примеры классов Vector, Stack, Hashtable, Properties показывают удобство классов-коллекций. Такое удобство и необходимость в коллекциях разных видов привели к тому, что для Java была разработана целая иерархия коллекций, получившая название Java Collections Framework. Она показана на рис. 6.3. Курсивом записаны имена интерфейсов. Пунктирные линии указывают классы, реализующие эти интерфейсы.

Все коллекции разбиты на четыре группы, описанные в интерфейсах List, Set, Queue и Map.

Примером реализации интерфейса List может служить описанный ранее класс Vector, примером реализации интерфейса Map — класс Hashtable.

Коллекции List, Set и Queue имеют много схожего, поэтому их общие методы вынесены в отдельный суперинтерфейс Collection.

Object

—AbstractCollection ■* Collection

—AbstractList Ч- - - - ^

^Vector -*r - ~

L Stack

—AbstractSet 4----- = = - Set—

—HashSet ~ ~

L LinkedHashSet

_ TreeSet __ SortedSet —

NavigableSet

—AbstractQueue -4- ----- - Queue -ArrayBlockingQueue BlockingQueue

- ConcurrentLinkedQueue

- DelayQueue

- LinkedBlockingQueue

- PriorityBlockingQueue

- PriorityQueue n

eq ue

^LinkedBlockingDeque BlockingDeque-J

_ ArrayDeque

Рис. 6.3. Иерархия классов и интерфейсов-коллекций

Интерфейс Map не входит в эту иерархию — по мнению разработчиков Java Collections Framework, отображения типа Map не являются коллекциями. Они показаны на рис. 6.4.

Object

Map

РЈ

РЈ

SortedMap —

' NavigableMap J

Map.Entry

-AbstractMap -4- — — — HashMap

L- LinkedHashMap -4-WeakHashMap —TreeMap _

-Arrays

Bitset ^

Collections

Dictionary — Hashtable — Properties

Рис. 6.4. Иерархия классов и интерфейсов-отображений

Все интерфейсы, входящие в Java Collections Framework, — настраиваемые (см. главу 4), их можно использовать как шаблоны классов, хранящих ссылки на элементы одного и того же типа.

Посмотрим, что, по мнению разработчиков Java API, должно содержаться в этих коллекциях.

Интерфейс Collection

Интерфейс Collection из пакета java.util описывает общие свойства коллекций List, Set и Queue. Он содержит методы добавления и удаления элементов, проверки и преобразования элементов:

□ boolean add (Obj ect obj) — добавляет элемент obj в конец коллекции; возвращает false, если такой элемент в коллекции уже есть, а коллекция не допускает повторяющиеся элементы; возвращает true, если добавление прошло удачно;

□ boolean addAll(Collection coll) - добавляет все элементы коллекции coll в конец

данной коллекции;

□ void clear () — удаляет все элементы коллекции;

□ boolean contains(Object obj) — проверяет наличие элемента obj в коллекции;

□ boolean containsAll(Collection coll) — проверяет наличие всех элементов коллекции coll в данной коллекции;

□ boolean isEmpty() — проверяет, пуста ли коллекция;

□ Iterator iterator () — возвращает итератор данной коллекции;

□ boolean remove (Obj ect obj) — удаляет указанный элемент из коллекции; возвращает false, если элемент не найден, true, если удаление прошло успешно;

□ boolean removeAll(Collection coll) - удаляет элементы указанной коллекции, лежа

щие в данной коллекции;

□ boolean retainAll(Collection coll) -удаляет все элементы данной коллекции, кроме

элементов коллекции coll;

□ int size() — возвращает количество элементов в коллекции;

□ Obj ect [ ] toArray () — возвращает все элементы коллекции в виде массива;

□ Obj ect [ ] toArray (Obj ect[] a) — записывает все элементы коллекции в массив a, если в нем достаточно места.

Интерфейс List

Интерфейс List из пакета java.util, расширяющий интерфейс Collection, описывает методы работы с упорядоченными коллекциями. Иногда их называют последовательностями (sequence). Элементы такой коллекции пронумерованы, начиная от нуля, к ним можно обратиться по индексу. В отличие от коллекции Set элементы коллекции List могут повторяться.

Класс Vector — одна из реализаций интерфейса List.

Интерфейс List добавляет к методам интерфейса Collection методы, использующие индекс index элемента:

□ void add (int index, Object obj) - вставляет элемент obj в позицию index; старые

элементы, начиная с позиции index, сдвигаются, их индексы увеличиваются на единицу;

□ boolean addAll(int index, Collection coll) — вставляет все элементы коллекции coll;

□ Object get(int index) возвращает элемент, находящийся в позиции index;

□ int indexOf (Obj ect obj) — возвращает индекс первого появления элемента obj в коллекции;

□ int lastIndexOf (Object obj) — возвращает индекс последнего появления элемента obj в коллекции;

□ Listiterator listiterator() — возвращает итератор коллекции;

□ ListIterator listIterator(int index) — возвращает итератор конца коллекции от позиции index;

□ Object set (int index, Object obj ) - заменяет элемент, находящийся в позиции index,

элементом obj ;

□ List subList (int from, int to) - возвращает часть коллекции от позиции from вклю

чительно до позиции to исключительно.

Интерфейс Set

Интерфейс Set из пакета java.util, расширяющий интерфейс Collection, описывает неупорядоченную коллекцию, не содержащую повторяющихся элементов. Это соответствует математическому понятию множества (set). Такие коллекции удобны для проверки наличия или отсутствия у элемента свойства, определяющего множество. Новые методы в интерфейс Set не добавлены, просто метод add () не станет добавлять еще одну копию элемента, если такой элемент уже есть в множестве.

Этот интерфейс расширен интерфейсом SortedSet.

Интерфейс SortedSet

Интерфейс SortedSet из пакета java.util, расширяющий интерфейс Set, описывает упорядоченное множество, отсортированное по естественному порядку возрастания его элементов или по порядку, заданному какой-либо реализацией интерфейса Comparator.

Элементы не нумеруются, но есть понятие первого, последнего, большего и меньшего элемента.

Дополнительные методы интерфейса отражают эти понятия:

□ Comparator comparator () — возвращает способ упорядочения коллекции;

□ Object first () — возвращает первый, меньший элемент коллекции;

□ SortedSet headSet(Object toElement) — возвращает начальные, меньшие элементы до элемента toElement исключительно;

□ Object last () — возвращает последний, больший элемент коллекции;

□ SortedSet subSet(Object fromElement, Object toElement) — возвращает подмножество коллекции от элемента fromElement включительно до элемента toElement исключительно;

□ SortedSet tailSet(Object fromElement) — возвращает последние, большие элементы коллекции от элемента fromElement включительно.

Интерфейс NavigableSet

Интерфейс NavigableSet из пакета java.util, расширяющий интерфейс SortedSet, описывает отсортированное множество, в котором можно организовать бинарный поиск.

Чтобы осуществить это, в интерфейсе описаны методы, позволяющие для каждого данного элемента множества найти ближайший больший и ближайший меньший элементы

того же множества.

Методы возвращают null, если элемент не удалось найти:

□ Object lower (Object elem) — возвращает ссылку на наибольший элемент множества, меньший данного элемента elem;

□ Object floor (Object elem) — возвращает ссылку на наибольший элемент множества, меньший или равный данному элементу elem;

□ Object higher (Object elem) — возвращает ссылку на наименьший элемент множества, больший данного элемента elem;

□ Object ceiling (Obj ect elem) — возвращает ссылку на наименьший элемент множества, больший или равный данному элементу elem.

Следующие методы позволяют выделить отсортированное подмножество:

в–Ў NavigableSet subSet(Object fromElement, boolean frominclusive, Object toElement,

boolean toinclusive) - возвращает подмножество коллекции от элемента fromElement

включительно, если frominclusive == true, или исключительно, если

frominclusive == false, до элемента toElement включительно или исключительно в зависимости от истинности последнего параметра toinclusive;

□ NavigableSet headSet(Object toElement, boolean inclusive) — возвращает начальные, меньшие элементы до элемента toElement включительно или исключительно в зависимости от истинности параметра inclusive;

□ NavigableSet tailSet(Object fromElement, boolean inclusive) — возвращает последние, большие элементы коллекции от элемента fromElement включительно или исключительно в зависимости от истинности параметра inclusive.

Наконец, два метода удаляют наименьший и наибольший элементы множества:

□ Object pollFirst () — возвращает ссылку на наименьший элемент множества и удаляет его;

□ Obj ect pollLast () — возвращает ссылку на наибольший элемент множества и удаляет его.

Интерфейс Queue

Интерфейс Queue из пакета java.util, расширяющий интерфейс Collection, описывает методы работы с очередями. Очередью называется коллекция, элементы в которую добавляются с одного конца, а удаляются с другого конца. Хороший пример такой коллекции — обычная житейская очередь в магазине или на автобусной остановке. Такой порядок обработки называется FIFO (First In — First Out, первым пришел — первым ушел).

Интерфейс Queue добавляет к методам интерфейса Collection методы, характерные для очередей:

□ Object element () — возвращает первый элемент очереди, не удаляя его из очереди. Метод выбрасывает исключение, если очередь пуста;

□ Object peek() — возвращает первый элемент очереди, не удаляя его. В отличие от метода element () не выбрасывает исключение;

□ Object remove() — возвращает первый элемент очереди и удаляет его из очереди. Метод выбрасывает исключение, если очередь пуста;

□ Object poll () — возвращает первый элемент очереди и удаляет его из очереди. В отличие от метода remove () не выбрасывает исключение;

□ boolean offer(Object obj) - вставляет элемент в конец очереди и возвращает true,

если вставка удалась.

Интерфейс BlockingQueue

Интерфейс BlockingQueue из пакета java.util.concurrent, расширяющий интерфейс Queue, описывает очередь, с которой работают одновременно несколько подпроцессов, вставляющих и удаляющих элементы. Их работа организуется таким образом, чтобы подпроцесс, пытающийся забрать элемент из пустой очереди, ждал, когда другой подпроцесс занесет в нее хотя бы один элемент. Подпроцесс, ставящий элемент в очередь, ждет, когда для него освободится место, если очередь уже переполнена.

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

□ Obj ect take () — возвращает и удаляет первый элемент, ожидая поступления элемента, если очередь пуста;

□ void put (Object element) — ставит элемент element в очередь, ожидая уменьшения очереди, если она переполнена;

□ int drainTo(Collection coll, int num) — удаляет по крайней мере num элементов из очереди, переписывая их в коллекцию coll, и возвращает их фактическое количество;

□ int drainTo(Collection coll) — удаляет все доступные элементы из очереди, переписывая их в коллекцию coll и возвращая их количество.

Интерфейс Deque

Интерфейс Deque (double ended queue) из пакета java.util, расширяющий интерфейс Queue, описывает методы работы с разновидностью очередей, называемой деком, у которого элементы вставляются и удаляются с обоих концов.

Интерфейс Deque добавляет к методам интерфейса Queue методы, характерные для дека:

□ Object getFirst () — возвращает первый элемент дека, не удаляя его из дека. Эквивалентен методу element () интерфейса Queue. Метод выбрасывает исключение, если дек пуст;

□ Object getLast () — возвращает последний элемент дека, не удаляя его из дека. Метод выбрасывает исключение, если дек пуст;

□ Object peekFirst () — возвращает первый элемент дека, не удаляя его. Эквивалентен методу peek () интерфейса Queue. Не выбрасывает исключение;

□ Object peekLast () — возвращает последний элемент дека, не удаляя его. Не выбрасывает исключение;

□ void addFirst(Object obj) — вставляет элемент в начало дека;

□ void addLast (Obj ect obj) — вставляет элемент в конец дека. Эквивалентен методу add () интерфейса Collection;

□ boolean offerFirst (Object obj ) - вставляет элемент в начало дека и возвращает true,

если вставка удалась;

□ boolean offerLast(Object obj) - вставляет элемент в конец дека и возвращает true,

если вставка удалась. Эквивалентен методу offer() интерфейса Queue;

□ Object removeFirst() — возвращает первый элемент дека и удаляет его из дека. Эквивалентен методу remove () интерфейса Queue. Метод выбрасывает исключение, если дек пуст;

□ Object removeLast() — возвращает последний элемент дека и удаляет его из дека. Метод выбрасывает исключение, если дек пуст;

□ Object pollFirst () — возвращает первый элемент дека и удаляет его из дека. Эквивалентен методу poll () интерфейса Queue. В отличие от метода removeFirst ( ) не выбрасывает исключение;

□ Object pollLast() — возвращает последний элемент дека и удаляет его из дека. В отличие от метода removeLast () не выбрасывает исключение;

□ boolean removeFirstOccurrence(Object obj) — удаляет первый встретившийся элемент obj дека и возвращает true, если удалось это сделать. Метод выбрасывает исключение, если дек пуст;

□ boolean removeLastOccurrence(Object obj) — удаляет последний встретившийся элемент obj из дека и возвращает true, если удалось это сделать. Метод выбрасывает исключение, если дек пуст.

Интерфейс BlockingDeque

Интерфейс BlockingQueue из пакета java.util.concurrent, расширяющий интерфейсы Queue

и Deque, описывает дек, с которым работают одновременно несколько подпроцессов,

вставляющих и удаляющих элементы. Их работа организуется таким образом, чтобы

подпроцесс, пытающийся забрать элемент из пустого дека, ждал, когда другой подпро-

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

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

□ Object takeFirst() — возвращает и удаляет первый элемент, ожидая поступления элемента, если дек пуст. Эквивалентен методу take () интерфейса BlockingQueue;

□ Obj ect takeLast () — возвращает и удаляет последний элемент, ожидая поступления элемента, если дек пуст;

□ void putFirst(Object element) - вставляет элемент element в начало дека, ожидая

уменьшения дека, если он переполнен. Эквивалентен методу put() интерфейса

BlockingQueue;

□ void putLast (Obj ect element) - вставляет элемент element в конец дека, ожидая

уменьшения дека, если он переполнен.

Интерфейс Map

Интерфейс Map из пакета java.util описывает своеобразную коллекцию, состоящую не из элементов, а из пар "ключ — значение". У каждого ключа может быть только одно значение, что соответствует математическому понятию однозначной функции, или отображения (map).

Такую коллекцию часто называют еще словарем (dictionary) или ассоциативным массивом (associative array).

Обычный массив — простейший пример словаря с заранее заданным числом элементов. Это отображение множества первых неотрицательных целых чисел на множество элементов массива, множество пар "индекс массива — элемент массива".

Класс Hashtable — одна из реализаций интерфейса Map.

Интерфейс Map содержит методы, работающие с ключами и значениями:

□ boolean containsKey(Object key) — проверяет наличие ключа key;

□ boolean containsValue(Object value) — проверяет наличие значения value;

□ Set entrySet () — представляет коллекцию в виде множества с элементами в виде пар из данного отображения, с которыми можно работать методами вложенного интерфейса Map.Entry;

□ Object get (Object key) -возвращает значение, отвечающее ключу key;

□ Set keyset () — представляет ключи коллекции в виде множества;

□ Object put(Object key, Object value) — добавляет пару "key — value", если такой пары не было, и заменяет значение ключа key, если такой ключ уже есть в коллекции;

□ void putAll (Map m) — добавляет к коллекции все пары из отображения m;

□ Collection values () — представляет все значения в виде коллекции.

В интерфейс Map вложен интерфейс Map.Entry, содержащий методы работы с отдельной парой отображения.

Вложенный интерфейс Map.Entry

Этот интерфейс описывает методы работы с парами, полученными методом entrySet ():

□ методы getKey () и getValue () позволяют получить ключ и значение пары;

□ метод setvalue(Object value) меняет значение в данной паре.

Интерфейс SortedMap

Интерфейс SortedMap, расширяющий интерфейс Map, описывает упорядоченную по ключам коллекцию Map. Сортировка производится либо в естественном порядке возрастания ключей, либо в порядке, описываемом в интерфейсе Comparator.

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

□ Comparator comparator ( ) -способ упорядочения коллекции;

□ Object firstKey () — первый, меньший элемент коллекции;

□ SortedMap headMap(Object toKey) - начало коллекции до элемента с ключом toKey ис

ключительно;

□ Object lastKey() — последний, больший ключ коллекции;

□ SortedMap subMap(Object fromKey, Object toKey) — часть коллекции от элемента с ключом fromKey включительно до элемента с ключом toKey исключительно;

□ SortedMap tailMap(Object fromKey) - остаток коллекции, начинающийся от элемента

fromKey включительно.

Интерфейс NavigableMap

Интерфейс NavigableMap из пакета java.util, расширяющий интерфейс SortedMap, описывает отсортированное по ключам отображение, в котором можно организовать бинарный поиск.

Чтобы осуществить это, в интерфейсе описаны методы, позволяющие для каждой данной пары из отображения получить ссылки на меньшую и большую ее пары или на ключ такой пары. Они возвращают null, если ключ не удалось найти:

□ Map lowerEntry(Obj ect key) — возвращает ссылку на наибольшую пару отображения с ключом, меньшим данного ключа key;

□ Object lowerKey(Object key) — возвращает ссылку на наибольший ключ отображения, меньший данного ключа key;

□ Map floorEntry(Obj ect key) — возвращает ссылку на наибольшую пару отображения с ключом, меньшим или равным данному ключу key;

□ Object floorKey(Object key) — возвращает ссылку на наибольший ключ отображения, меньший или равный данному ключу key;

□ Map higherEntry (Obj ect key) — возвращает ссылку на наименьшую пару отображения с ключом, большим данного ключа key;

□ Object highe rKey (Obj e ct key) - возвращает ссылку на наименьший ключ отображе

ния, больший данного ключа key;

□ Map ceilingEntry(Object key) — возвращает ссылку на наименьшую пару отображения с ключом, большим или равным данному ключу key;

□ Object ceilingKey(Object key) — возвращает ссылку на наименьший ключ отображения, больший или равный данному ключу key.

Следующие методы позволяют выделить отсортированное подмножество пар:

в–Ў NavigableMap subMap(Object fromKey, boolean frominclusive, Object toKey, boolean

toinclusive) - возвращает подмножество отображения от пары с ключом fromKey

включительно, если frominclusive == true, или исключительно, если

frominclusive == false, до пары с ключом toKey включительно или исключительно в зависимости от истинности последнего параметра toinclusive;

□ NavigableMap headMap(Object toKey, boolean inclusive) — возвращает начальные, меньшие пары до пары с ключом toKey включительно или исключительно в зависимости от истинности параметра inclusive;

□ NavigableMap tailMap(Object fromKey, boolean inclusive) — возвращает последние, большие пары от пары с ключом fromKey включительно или исключительно в зависимости от истинности параметра inclusive.

Наконец, два метода удаляют наименьший и наибольший элементы множества:

□ Map pollFirstEntry() — возвращает ссылку на наименьшую пару отображения и удаляет ее;

□ Map pollLastEntry() — возвращает ссылку на наибольшую пару отображения и удаляет ее.

Абстрактные классы-коллекции

Вы можете создать свои коллекции, реализовав рассмотренные ранее интерфейсы. Это дело трудное, поскольку в интерфейсах много методов. Чтобы облегчить данную задачу, в Java Collections Framework введены частичные реализации интерфейсов — абстрактные классы-коллекции.

Эти классы лежат в пакете j ava. util.

Абстрактный класс AbstractCollection реализует интерфейс Collection, но оставляет нереализованными методы iterator (), size().

Абстрактный класс AbstractList реализует интерфейс List, но оставляет нереализованным метод get (int) и унаследованный метод size(). Этот класс позволяет реализовать коллекцию с прямым доступом к элементам, подобно массиву.

Абстрактный класс AbstractSequentialList реализует интерфейс List, но оставляет нереализованным метод listiterator (int index) и унаследованный метод size(). Данный класс позволяет реализовать коллекции с последовательным доступом к элементам с помощью итератора Listiterator.

Абстрактный класс AbstractSet реализует интерфейс Set, но оставляет нереализованными методы, унаследованные от AbstractCollection.

Абстрактный класс AbstractQueue реализует интерфейс Queue, но оставляет нереализованными методы, унаследованные от AbstractCollection.

Абстрактный класс AbstractMap реализует интерфейс Map, но оставляет нереализованным метод entrySet ().

Наконец, в составе Java API есть полностью реализованные классы-коллекции. Помимо уже рассмотренных классов Vector, Stack, Hashtable и Properties, это классы ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, WeakHashMap и много других классов.

Для работы с указанными классами разработаны интерфейсы iterator, Listiterator, Comparator и классы Arrays и Collections.

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

Интерфейс Iterator

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

Вместе с развитием структур данных развивались и алгоритмы работы с ними: сортировка, поиск, обход, хеширование.

Этим вопросам посвящена обширная литература, посмотрите, например, книгу [11].

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

В частности, задачу обхода возложили на саму коллекцию. В Java Collections Framework введен интерфейс iterator, описывающий способ обхода всех элементов коллекции. В каждой коллекции есть метод iterator(), возвращающий реализацию интерфейса iterator для указанной коллекции. Получив эту реализацию, можно обходить коллекцию в некотором порядке, определенном данным итератором, с помощью методов, описанных в интерфейсе iterator и реализованных в этом итераторе. Подобная техника использована в классе StringTokenizer, описанном в конце главы 5.

В интерфейсе iterator представлены всего три метода:

□ логический метод hasNext () возвращает true, если обход еще не завершен;

□ метод next () делает текущим следующий элемент коллекции и возвращает его в виде объекта класса Object;

□ метод remove () удаляет текущий элемент коллекции.

Можно представить себе дело так, что итератор — это указатель на элемент коллекции. При создании итератора указатель устанавливается перед первым элементом, метод next () перемещает указатель на первый элемент и показывает его. Следующее применение метода next () перемещает указатель на второй элемент коллекции и демонстрирует его. Последнее применение метода next () выводит указатель за последний элемент коллекции.

Метод remove () позволяет при просмотре коллекции удалять из нее ненужные элементы, сохраняя при этом порядок следования элементов. Дело в том, что метод remove () самой коллекции, удалив элемент, перестроит оставшиеся элементы коллекции и итератор может неправильно просмотреть оставшуюся часть коллекции.

В листинге 6.5 к тексту листинга 6.1 добавлена работа с итератором. Впрочем, для обхода коллекции типа List можно использовать оператор "for-each". Этот способ тоже показан в листинге 6.5.

Листинг 6.5. Использование итератора вектора j

Vector v = new Vector<>();

String s = "Строка, которую мы хотим разобрать на слова.";

StringTokenizer st = new StringTokenizer(s, " \t\n\r,.");

while (st.hasMoreTokens()){

// Получаем слово и заносим в вектор

v.add(st.nextToken()); // Добавляем элемент в конец вектора.

}

System.out.println(v.firstElement()); // Первый элемент. System.out.println(v.lastElement()); // Последний элемент. v.setSize(4); // Уменьшаем число элементов.

v.add("собрать."); // Добавляем в конец укороченного вектора.

v.set(3, "опять"); // Ставим элемент в позицию 3.

// Первый способ обхода коллекции типа List // использует индексы ее элементов:

for (int i = 0; i < v.size(); i++) // Перебираем весь вектор.

System.out.print(v.get(i) + " ");

System.out.println();

// Второй способ обхода коллекции использует итератор:

Iterator it = v.iterator(); // Получаем итератор вектора.

try{

while (it.hasNext()) // Пока в векторе есть элементы,

System.out.println(it.next()); // выводим текущий элемент.

}catch(Exception e){}

// Третий способ обхода коллекции использует оператор for-each: for (String s: v) // Цикл по всем элементам вектора.

System.out.println(s); // Выводим текущий элемент вектора.

Интерфейс ListIterator

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

В интерфейс Listiterator добавлены следующие методы:

□ void add (Obj ect element) — добавляет элемент element перед текущим элементом;

□ boolean hasPrevious() — возвращает true, если в коллекции есть элементы, стоящие перед текущим элементом;

□ int nextindex () — возвращает индекс текущего элемента; если текущим является последний элемент коллекции, возвращает размер коллекции;

□ Object previous () — возвращает предыдущий элемент и делает его текущим;

□ int previousindex() — возвращает индекс предыдущего элемента;

□ void set (Obj ect element) заменяет текущий элемент элементом element; выполняется

сразу после next () или previous ( ).

Как видите, итераторы могут изменять коллекцию, в которой они работают, добавляя, удаляя и заменяя элементы. Чтобы это не приводило к конфликтам, предусмотрена исключительная ситуация, возникающая при попытке использования итераторов параллельно "родным" методам коллекции. Именно поэтому в листинге 6.5 действия с итератором заключены в блок try{} catch () {}.

Изменим часть листинга 6.5 с использованием итератора Listiterator.

// Текст листинга 6.1...

// ...

ListIterator lit = v.listIterator(); // Получаем итератор вектора.

// Указатель сейчас находится перед началом вектора.

try{

while(lit.hasNext()) // Пока в векторе есть элементы,

System.out.println(lit.next()); // переходим к следующему

// элементу и выводим его.

// Теперь указатель находится за концом вектора.

// Перейдем к началу вектора. while(lit.hasPrevious())

System.out.println(lit.previous());

}catch(Exception e){}

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

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

Классы, создающие списки

Класс ArrayList полностью реализует интерфейс List и итератор типа iterator. Класс ArrayList очень похож на класс Vector, у него тот же набор методов, он может использоваться в тех же ситуациях. Главное отличие класса ArrayList от класса Vector заключается в том, что класс ArrayList не синхронизован. Это означает, что одновременное изменение экземпляра этого класса несколькими подпроцессами приведет к непредсказуемым результатам. Эти вопросы мы рассмотрим в главе 22.

В классе ArrayList три конструктора:

□ ArrayList () — создает пустой объект;

□ ArrayList (Collection coll) — формирует объект, содержащий все элементы коллекции coll;

□ ArrayList (int initCapacity) — создает пустой объект емкости initCapacity.

В качестве примера использования класса ArrayList перепишем класс Chorus из листинга 3.3, используя вместо массива коллекцию.

public class Chorus{

public static void main(String[] args){

List singer = new ArrayList<>(); singer.add(new Dog()); singer.add(new Cat()); singer.add(new Cow()); for (Voice v: singer) v.voice();

}

}

Двунаправленный список

Класс LinkedList полностью реализует интерфейсы List, Queue и Deque. Он реализует итераторы типа iterator и Listiterator, что превращает его в двунаправленный список. Он удобен и для организации списков, стеков, очередей и деков. Класс LinkedList не синхронизован. Кроме того, он допускает хранение ссылок null.

В классе LinkedList два конструктора:

□ LinkedList () — создает пустой объект;

□ LinkedList (Collection coll) — создает объект, содержащий все элементы коллекции

coll.

В классе LinkedList реализованы только методы интерфейсов. Других методов в нем нет.

Дек

Класс ArrayDeque полностью реализует интерфейсы Queue и Deque. В отличие от класса LinkedList он синхронизован и допускает одновременную работу нескольких подпроцессов с его объектом. Кроме того, он не допускает хранение ссылок null. Он удобен для организации стеков, очередей и деков, тем более что он работает быстрее, чем классы Stack и LinkedList.

В классе ArrayDeque три конструктора:

□ ArrayDeque ( ) -создает пустой объект;

□ ArrayDeque (Collection coll) — создает объект, содержащий все элементы коллекции

coll;

□ ArrayDeque (int numElement) — создает пустой объект емкости numElement.

Упражнение

1. Перепишите листинг 6.1 с использованием классов списков.

Классы, создающие отображения

Класс HashMap полностью реализует интерфейс Map, а также итератор типа iterator. Класс HashMap очень похож на класс Hashtable и может использоваться в тех же ситуациях. Он имеет тот же набор функций и такие же конструкторы:

□ HashMap () — создает пустой объект с показателем загруженности 0,75;

□ HashMap (int capacity) - формирует пустой объект с начальной емкостью capacity и

показателем загруженности 0,75;

□ HashMap (int capacity, float loadFactor) — создает пустой объект с начальной емкостью capacity и показателем загруженности loadFactor;

□ HashMap(Map f) — создает объект класса HashMap, содержащий все элементы отображения f, с емкостью, равной удвоенному числу элементов отображения f, но не менее 11, и показателем загруженности 0,75.

Класс WeakHashMap отличается от класса HashMap только тем, что в его объектах неиспользуемые элементы, на которые никто не ссылается, автоматически исключаются из объекта.

Связанные отображения

Класс LinkedHashMap полностью реализует интерфейс Map. Реализация сделана в виде двунаправленного списка, а значит, его элементы хранятся в упорядоченном виде. Порядок элементов задается порядком их занесения в список.

В этом классе пять конструкторов:

□ linkedHashMap () — создает пустой объект с емкостью в 16 элементов;

□ LinkedHashMap (int capacity) -создает пустой объект с емкостью capacity элементов;

□ LinkedHashMap(int capacity, float loadFactor) — формирует объект с емкостью capacity элементов и показателем загруженности loadFactor;

□ LinkedHashMap(int capacity, float loadFactor, boolean order) — создает объект с емкостью capacity элементов, показателем загруженности loadFactor и порядком элементов order, прямым или обратным;

□ LinkedHashMap(Map sf) — создает объект, содержащий все элементы отображения sf.

Упорядоченные отображения

Класс TreeMap полностью реализует интерфейс SortedMap. Класс реализован как бинарное дерево поиска, что значительно ускоряет поиск нужного элемента.

Порядок задается либо естественным следованием элементов, либо объектом, реализующим интерфейс сравнения Comparator.

В данном классе четыре конструктора:

□ TreeMap () — создает пустой объект с естественным порядком элементов;

□ TreeMap (Comparator c) -создает пустой объект, в котором порядок задается объектом

сравнения c;

□ TreeMap(Map f) — формирует объект, содержащий все элементы отображения f, с естественным порядком его элементов;

□ TreeMap(SortedMap sf) — создает объект, содержащий все элементы отображения sf в том же порядке.

Хотя элементы отображения упорядочены, чтобы получить итератор для его обхода,

надо преобразовать отображение во множество методом entrySet (), например так:

iterator it = tm.entrySet().iterator();

Здесь надо пояснить, каким образом можно задать упорядоченность элементов коллекции.

Сравнение элементов коллекций

Интерфейс Comparator описывает два метода сравнения:

□ int compare (Obj ect objl, Object obj2) - возвращает отрицательное число, если objl

в каком-то смысле меньше obj2; нуль, если они считаются равными; положительное число, если obj 1 больше obj 2. Для читателей, знакомых с теорией множеств, скажем, что этот метод сравнения обладает свойствами тождества, антисимметричности и транзитивности;

□ boolean equals(Object obj) — сравнивает данный объект с объектом obj, возвращая true, если объекты совпадают в каком-либо смысле, заданном этим методом.

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

Листинг 6.6 показывает один из возможных способов упорядочения комплексных чисел - объектов класса Complex из листинга 2.4. Здесь описывается класс ComplexCompare,

реализующий интерфейс Comparator. В листинге 6.7 он применяется для упорядоченного хранения множества комплексных чисел.

Листинг 6.6. Сравнение комплексных чисел

import java.util.*;

class ComplexCompare implements Comparator{

public int compare(Object objl, Object obj2){ Complex zl = (Complex)objl, z2 = (Complex)obj2; double rel = zl.getRe(), iml = zl.getim(); double re2 = z2.getRe(), im2 = z2.getim(); if (rel != re2)

return (int)(rel — re2);

else if (iml != im2)

return (int)(iml — im2); else return 0;

}

public boolean equals(Object z){ return compare(this, z) == 0;

}

}

Упражнение

2. Перепишите листинг 6.3 с использованием классов отображений.

Классы, создающие множества

Класс HashSet полностью реализует интерфейс Set и итератор типа iterator. Класс

HashSet применяется в тех случаях, когда надо хранить только одну копию каждого элемента.

В классе HashSet четыре конструктора:

□ HashSet () — создает пустой объект с показателем загруженности 0,75;

□ HashSet (int capacity) - формирует пустой объект с начальной емкостью capacity и

показателем загруженности 0,75;

□ HashSet (int capacity, float loadFactor) — создает пустой объект с начальной емкостью capacity и показателем загруженности loadFactor;

□ HashSet(Collection coll) — создает объект, содержащий все элементы коллекции coll, с емкостью, равной удвоенному числу элементов коллекции coll, но не менее 11, и показателем загруженности 0,75.

Связанные множества

Класс LinkedHashSet полностью реализует интерфейс Set и итератор типа iterator. Класс

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

В классе LinkedHashSet четыре конструктора, которые создают:

□ LinkedHashSet () — пустой объект с емкостью 16 и показателем загруженности 0,75;

□ LinkedHashSet (int capacity) — пустой объект с начальной емкостью capacity и показателем загруженности 0,75;

□ LinkedHashSet (int capacity, float loadFactor) — пустой объект с начальной емкостью capacity и показателем загруженности loadFactor;

□ LinkedHashSet(Collection coll) — объект, содержащий все элементы коллекции coll, с показателем загруженности 0,75.

Упорядоченные множества

Класс TreeSet полностью реализует интерфейс SortedSet и итератор типа iterator. Класс TreeSet реализован как бинарное дерево поиска. Это существенно ускоряет поиск нужного элемента.

Порядок задается либо естественным следованием элементов, либо объектом, реализующим интерфейс сравнения Comparator.

Этот класс удобен при поиске элемента во множестве, например для проверки, обладает ли какой-либо элемент свойством, определяющим множество.

В классе TreeSet четыре конструктора, создающих:

□ TreeSet () — пустой объект с естественным порядком элементов;

□ TreeSet (Comparator c) - пустой объект, в котором порядок задается объектом срав

нения c;

□ TreeSet(Collection coll) — объект, содержащий все элементы коллекции coll, с естественным порядком ее элементов;

□ TreeSet(SortedMap sf) — объект, содержащий все элементы отображения sf, в том же порядке.

В листинге 6.7 показано, как можно хранить комплексные числа в упорядоченном виде. Порядок задается объектом класса ComplexCompare, определенного в листинге 6.6.

Листинг 6.7. Хранение комплексных чисел в упорядоченном виде !

TreeSet ts = new TreeSet<>(new ComplexCompare());

ts.add(new Complex(l.2, 3.4));

ts.add(new Complex(-l.25, 33.4));

ts.add(new Complex(l.23, -3.45));

ts.add(new Complex(l6.2, 23.4));

iterator it = ts.iterator();

while (it.hasNext())

((Complex)it.next()).pr();

// for (Complex z: ts) z.pr(); // Другой способ обхода множества.

Действия с коллекциями

Коллекции предназначены для хранения элементов в удобном для дальнейшей обработки виде. Очень часто обработка заключается в сортировке элементов и поиске нужного элемента. Эти и другие методы обработки собраны в класс Collections.

Методы класса Collections

Все методы класса Collections статические, ими можно пользоваться, не создавая экземпляры класса Collections. Методов очень много, их количество увеличивается с каждой новой версией JDK, поэтому мы перечислим только основные методы.

Как обычно в статических методах, коллекция, с которой работает метод, задается его параметром.

Сортировка может быть сделана только в упорядочиваемой коллекции, реализующей интерфейс List. Для сортировки в классе Collections есть два метода:

□ static void sort(List coll) — сортирует в естественном порядке возрастания коллекцию coll, реализующую интерфейс List;

□ static void sort(List coll, Comparator c) — сортирует коллекцию coll в порядке, заданном объектом c.

После сортировки можно осуществить бинарный поиск в коллекции:

□ static int binarySearch(List coll, Object element) — отыскивает элемент element в отсортированной в естественном порядке возрастания коллекции coll и возвращает индекс элемента или отрицательное число, если элемент не найден; отрицательное число показывает индекс, с которым элемент element был бы вставлен в коллекцию, с обратным знаком;

□ static int binarySearch(List coll, Object element, Comparator c) — то же, но коллекция отсортирована в порядке, определенном объектом c.

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

□ static Object max(Collection coll) - возвращает наибольший в естественном поряд

ке элемент коллекции coll;

□ static Object max(Collection coll, Comparator c) — то же в порядке, заданном объектом c;

□ static Object min(Collection coll) — возвращает наименьший в естественном порядке элемент коллекции coll;

□ static Object min(Collection coll, Comparator c) — то же в порядке, заданном объектом c.

Два метода "перемешивают" элементы коллекции в случайном порядке:

□ static void shuffle(List coll) — случайные числа задаются по умолчанию;

□ static void shuffle(List coll, Random r) — случайные числа определяются объектом r.

Метод reverse (List coll) меняет порядок расположения элементов на обратный.

Метод copy(List from, List to) копирует коллекцию from в коллекцию to.

Метод fill (List coll, Object element) заменяет все элементы существующей коллекции coll элементом element.

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

Упражнение

3. Упорядочите коллекции, созданные в этой главе, и проделайте в них бинарный поиск.

Заключение

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

В следующей главе мы рассмотрим аналогичные средства для работы с массивами, датами, для получения случайных чисел и прочих необходимых средств программирования.

Вопросы для самопроверки

1. Что называется коллекцией?

2. В чем отличие вектора от массива?

3. Что дает задание конкретного класса в шаблоне при определении коллекции?

4. В чем различие интерфейсов List и Set?

5. В чем различие интерфейсов List и Queue?

6. Что дополняет интерфейс Deque к интерфейсу Queue?

7. Зачем в Java введены интерфейсы NavigableSet и NavigableMap?

8. Что такое стек?

9. Что такое ассоциативный массив?

10. Что такое линейный список?

11. Что такое двунаправленный список?

12. Какие способы обхода коллекции вы знаете?

13. Каким классом-коллекцией лучше всего организовать очередь?

14. Когда удобнее использовать класс Vector, а когда — ArrayList?

15. Можно ли совсем отказаться от объекта iterator в пользу цикла "for-each"?

16. Какие классы-коллекции реализуют структуру данных "дерево"?

ГЛАВА 7

 

Классы-утилиты

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

Работа с массивами

В классе Arrays из пакета java.util собрано множество методов для работы с массивами. Их можно разделить на несколько больших групп.

Сортировка массива

Восемнадцать статических методов класса Arrays сортируют массивы с разными типами числовых элементов в порядке возрастания чисел или просто объекты в их естественном порядке.

Восемь из них имеют простой вид:

static void sort(type[] a);

где type может быть один из семи примитивных типов: byte, short, int, long, char, float, double — или тип Object.

Восемь методов с теми же типами сортируют часть массива от индекса from включительно до индекса to исключительно:

static void sort(type[] a, int from, int to);

Оставшиеся два метода сортировки упорядочивают массив или его часть с элементами типа Object по правилу, заданному объектом c, реализующим интерфейс Comparator:

static void sort(Object[] a, Comparator c);

static void sort(Object[] a, int from, int to, Comparator c);

Бинарный поиск в массиве

После сортировки массива можно организовать в нем бинарный поиск элемента element одним из восемнадцати статических методов поиска.

Восемь методов имеют вид:

static int binarySearch(type[] a, type element);

где type один из семи примитивных типов (byte, short, int, long, char, float, double)

или тип Object.

Восемь методов сортируют часть массива, начиная от элемента с индексом from включительно до элемента с индексом to исключительно:

static int binarySearch(type[] a, int from, int to, type element);

Оставшиеся два метода поиска применяют настраиваемые типы и имеют более сложный вид:

static int binarySearch(T[] a, T element, Comparator c); static int binarySearch(T[] a, int from, int to, T element,

Comparator c);

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

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

Заполнение массива

Восемнадцать статических методов класса Arrays заполняют массив или часть массива указанным значением value:

static void fill(type[], type value);

static void fill(type[], int from, int to, type value);

где type-один из восьми примитивных типов или тип Object.

Копирование массива

Восемь методов класса Arrays, написанных для всех примитивных типов, обозначенных здесь словом type:

static type[] copyOf(type[] a, int newLength);

копируют массив a, возвращая ссылку на копию массива. Они обрезают массив a до длины newLength, если newLength меньше длины массива a, или дополняют массив нулями, если его длина меньше newLength.

Следующий метод использует настраиваемый тип:

static T[] copyOf(T[] a, int newLength);

Еще один метод копирования позволяет изменить тип массива:

static T[] copyOf(U[] a, int newLength, Class newType);

При несовместимости старого и нового типов возникает исключительная ситуация.

Часть массива, начиная от элемента с индексом from и заканчивая перед элементом с индексом to, можно скопировать одним из десяти методов copyOfRange (). Восемь из них написаны для примитивных типов. Они имеют вид:

static type[] copyOfRange(type[] a, int from, int to);

Индекс to может оказаться больше длины массива, в таком случае массив-копия будет дополнен нулями.

Девятый метод использует настраиваемый тип массива:

static T[] copyOfRange(T[] a, int newLength, int from, int to);

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

static T[] copyOfRange(U[] a, int from, int to, Class newType);

При несовместимости старого и нового типов возникает исключительная ситуация.

Сравнение массивов

Девять статических логических методов класса Arrays сравнивают массивы:

static boolean equals(type[] a1, type[] a2);

где type-один из восьми примитивных типов или тип Object.

Массивы считаются равными и возвращается true, если они имеют одинаковую длину, и равны все элементы массивов с одинаковыми индексами.

Еще один метод сравнения массивов:

static boolean deepEquals(Object[] a1, Object[] a2);

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

Представление массива строкой

Девять статических методов класса Arrays преобразуют массив в строку:

static String toString(type[] a);

где type-один из восьми примитивных типов или тип Object.

В формируемой строке массив записывается в квадратных скобках, а его элементы перечисляются через запятую и пробел. Каждый элемент преобразуется в строку методом String.valueOf (type).

Еще один метод преобразования массива в строку:

static String deepToString(Object[] a);

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

Получение хеш-кода массива

Девять статических методов класса Arrays вычисляют хеш-код массива:

static int hashCode(type[] a);

где type один из восьми примитивных типов или тип Object.

Сначала массив представляется списком типа List, а затем вычисляется хеш-код списка методом List.hashCode ().

Еще один метод:

static int deepHashCode(Object[] a);

применяется для вычисления хеш-кода многомерных массивов. Он просматривает их подмассивы с любым количеством индексов.

В листинге 7.1 приведен простой пример работы с некоторыми из методов класса

Arrays.

Листинг 7.1. Применение методов класса Arrays

import java.util.*; class ArraysTest{

public static void main(String[] args){

int[] a = {34, -45, 12, 67, -24, 45, 36, -56}; Arrays.sort(a);

for (int i: a)

System.out.print(a[i] + " ");

System.out.println();

Arrays.fill(a, Arrays.binarySearch(a, 12), a.length, 0);

for (int i: a)

System.out.print(a[i] + " ");

System.out.println();

}

}

Локальные установки

Некоторые данные — даты, время — традиционно представляются в различных местностях по-разному. Например, дата в России выводится в формате число.месяц.год (через точку): 27.06.2011. В США принята запись месяц/число/год (через наклонную черту): 06/27/11.

Совокупность таких форматов для данной местности, как говорят на жаргоне "локаль", хранится в объекте класса Locale из пакета java.util. Для создания такого объекта достаточно знать язык language и местность country. Иногда требуется третья характеристика — вариант variant, определяющая программный продукт, например: "win", "mac",

"POSIX".

По умолчанию местные установки определяются операционной системой и читаются из системных свойств.

Посмотрите на следующие строки (см. также рис. 6.2):

user.language = ru // Язык — русский

user.region = RU // Местность — Россия

file.encoding = Cp1251 // Байтовая кодировка — CP1251

Они определяют русскую локаль и локальную кодировку байтовых символов. Локаль, установленную по умолчанию на той машине, где выполняется программа, можно выяснить статическим методом Locale.getDefault ( ).

Чтобы работать с другой локалью, ее надо прежде всего создать. Для этого в классе Locale есть два конструктора:

Locale(String language, String country);

Locale(String language, String country, String variant);

Параметр language — это строка из двух строчных букв, определенная стандартом ISO639, например: "ru", "fr", "en". Параметр country — строка из двух прописных букв, определенная стандартом ISO3166, например: "ru", "us", "gb". Параметр variant не определяется стандартом, это может быть, например, строка "Traditional".

Локаль часто указывают одной строкой "ru_RU", "en_GB", "en_US", "en_CA" и т. д.

После создания локали можно сделать ее локалью по умолчанию статическим методом:

Locale.setDefault(Locale newLocale);

Несколько статических методов класса Locale позволяют получить сведения о локали по умолчанию или локали, заданной параметром locale:

□ String getCountry() — стандартный код страны из двух букв;

□ String getDisplayCountry() — страна записывается словом, обычно выводящимся на экран;

□ String getDisplayCountry(Locale locale) — то же для указанной локали.

Такие же методы есть для языка и варианта.

Можно просмотреть список всех локалей, определенных для данной JVM (Java Virtual Machine, виртуальная машина Java), и их параметров, выводимый в стандартном виде:

Locale[] getAvailableLocales();

String[] getISOCountries();

String[] getlSOLanguages();

Установленная локаль в дальнейшем используется при выводе данных в местном формате.

Работа с датами и временем

Методы работы с датами и показаниями времени собраны в два класса из пакета

java.util: Calendar Рё Date.

Объект класса Date хранит число миллисекунд, прошедших с 1 января 1970 г. 00:00:00 по Гринвичу. Это "день рождения" операционной системы UNIX, он называется "Epoch".

Класс Date удобно использовать для отсчета промежутков времени в миллисекундах.

Получить текущее число миллисекунд, прошедших с момента Epoch на той машине, где выполняется программа, можно статическим методом System.currentTimeMillis ( ).

В классе Date два конструктора. Конструктор Date () заносит в создаваемый объект текущее время машины, на которой выполняется программа, по системным часам, а конструктор Date (long millisec) — указанное число.

Получить значение, хранящееся в объекте, можно методом long getTime (), установить новое значение — методом setTime(long newTime).

Три логических метода сравнивают отсчеты времени:

□ boolean after(long when) — возвращает true, если время when больше данного;

□ boolean before(long when) — возвращает true, если время when меньше данного;

□ boolean after(Object when) — возвращает true, если when — объект класса Date и времена совпадают.

Еще два метода, сравнивая отсчеты времени, возвращают отрицательное число типа int, если данное время меньше параметра when; нуль, если времена совпадают; положительное число, если данное время больше параметра when:

в–Ў int compareTo(Date when);

□ int compareTo (Object when) — если when не относится к объектам класса Date, создается исключительная ситуация.

Преобразование миллисекунд, хранящихся в объектах класса Date, в текущее время и дату производится методами класса Calendar.

Часовой пояс и летнее время

Методы установки и изменения часового пояса (time zone), а также летнего времени (Daylight Savings Time, DST), собраны в абстрактном классе TimeZone из пакета java.util. В этом же пакете есть его реализация-подкласс SimpleTimeZone.

В классе SimpleTimeZone три конструктора, но чаще всего объект создается статическим методом getDefault (), возвращающим часовой пояс, установленный на машине, выполняющей программу.

В этих классах множество методов работы с часовыми поясами, но в большинстве случаев требуется только узнать часовой пояс на машине, выполняющей программу, статическим методом getDefault (), проверить, осуществляется ли переход на летнее время, логическим методом useDaylightTime() , и установить часовой пояс методом

setDefault(TimeZone zone).

Класс Calendar

Класс Calendar — абстрактный, в нем собраны общие свойства большинства календарей: юлианского, григорианского, лунного. В Java API пока есть только одна его реализация — подкласс GregorianCalendar.

Поскольку Calendar — абстрактный класс, его экземпляры создаются четырьмя статическими методами по заданной локали и/или часовому поясу:

Calendar getInstance();

Calendar getInstance(Locale loc);

Calendar getInstance(TimeZone tz);

Calendar getInstance(TimeZone tz, Locale loc);

Для работы с месяцами определены целочисленные константы от January до December, а для работы с днями недели — константы от Monday до Sunday.

Первый день недели можно узнать методом int getFirstDayOfWeek(), а установить — методом setFirstDayOfWeek(int day), например:

setFirstDayOfWeek(Calendar.MONDAY);

Остальные методы позволяют просмотреть время и часовой пояс или установить их.

Метод get(int field) возвращает элемент календаря, заданный параметром field. Для этого параметра в классе Calendar определены следующие статические целочисленные

константы:
ERA WEEK OF YEAR DAY OF WEEK SECOND
YEAR WEEK OF MONTH DAY OF WEEK IN MONTH MILLISECOND
MONTH DAY OF YEAR HOUR OF DAY ZONE OFFSET
DATE DAY OF MONTH MINUTE DST_OFFSET
Метод set (int field, int value), использующий эти константы, устанавливает соответствующие значения даты и времени, оставляя остальные значения без изменения. Еще

несколько методов set () устанавливают дату и время по дням, часам, минутам и другим элементам.

Метод setTime(Date d), наиболее часто применяемый для заполнения календаря, устанавливает в календарь все элементы даты d полностью.

Подкласс GregorianCalendar

В григорианском календаре две целочисленные константы определяют эры: bc (Before Christ) и ad (Anno Domini).

Семь конструкторов определяют календарь по времени, часовому поясу и/или локали:

GregorianCalendar();

GregorianCalendar(int year, int month, int date);

GregorianCalendar(int year, int month, int date, int hour, int minute); GregorianCalendar(int year, int month, int date, int hour, int minute, int second); GregorianCalendar(Locale loc);

GregorianCalendar(TimeZone tz);

GregorianCalendar(TimeZone tz, Locale loc);

После создания объекта следует определить дату перехода с юлианского календаря на григорианский календарь методом setGregorianChange(Date date). По умолчанию это 15 октября 1582 г. На территории России переход на григорианский календарь был осуществлен 14 февраля 1918 г., значит, создание объекта greg надо выполнить так:

GregorianCalendar greg = new GregorianCalendar(); greg.setGregorianChange(new

GregorianCalendar(1918, Calendar.FEBRUARY, 14).getTime());

Узнать, является ли год високосным в григорианском календаре, можно логическим методом isLeapYear ().

Представление даты и времени

Различные способы представления дат и показаний времени можно осуществить методами, собранными в абстрактный класс DateFormat и его подкласс SimpleDateFormat из пакета j ava. text.

Класс DateFormat предлагает четыре стиля представления даты и времени:

□ short — представляет дату и время в коротком числовом виде: 27.07.11 17:32; в локали США: 07/27/11 5:32 PM;

□ medium — задает год четырьмя цифрами и показывает секунды: 27.07.2011 17:32:45; в локали США месяц представляется тремя буквами;

□ long — представляет месяц словом и добавляет часовой пояс:

27 июль 2011 г. 17:32:45 GMT+03:00;

□ full — в русской локали таков же, как и стиль long; в локали США добавляется еще день недели.

Есть еще стиль default, совпадающий со стилем medium.

При создании объекта класса simpleDateFormat можно задать в конструкторе шаблон, определяющий какой-либо другой формат, например:

SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy hh.mm");

System.out.println(sdf.format(new Date()));

Получим вывод в таком виде: 27-07-2011 17.32.

В шаблоне буква d означает цифру дня месяца, M — цифру месяца, y — цифру года, h — цифру часа, m — цифру минут. Полностью обозначения для шаблона указаны в документации по классу SimpleDateFormat.

Эти буквенные обозначения можно изменить с помощью класса DateFormatSymbols.

Не во всех локалях можно создать объект класса SimpleDateFormat. В таких случаях используются статические методы getinstance () класса DateFormat, возвращающие объект класса DateFormat. Параметрами этих методов служат стиль представления даты и времени и, может быть, локаль.

После создания объекта метод format() класса DateFormat возвращает строку с датой и временем, согласно заданному стилю. В качестве аргумента задается объект класса

Date.

Например:

System.out.println("LONG: " + DateFormat.getDateTimeInstance( DateFormat.LONG, DateFormat.LONG).format(new Date()));

или

System.out.println("FULL: " + DateFormat.getDateTimeInstance(

DateFormat.FULL,DateFormat.FULL, Locale.US).format(new Date()));

Получение случайных чисел

Получить случайное неотрицательное число, строго меньшее единицы, в виде типа double можно статическим методом random( ) из класса java.lang.Math.

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

Более серьезные действия со случайными числами можно организовать с помощью методов класса Random из пакета java.util. В классе два конструктора:

□ Random(long seed) — создает генератор псевдослучайных чисел, использующий для начала работы число seed;

□ Random () — выбирает в качестве начального значения текущее время.

Создав генератор, можно получать случайные числа соответствующего типа методами nextBoolean (), nextDouble(), nextFloat(), nextGaussian(), nextInt(), nextLong(),

nextint(int max) или записать сразу последовательность случайных чисел в заранее определенный массив байтов bytes методом nextBytes (byte [ ] bytes).

Вещественные случайные числа равномерно располагаются в диапазоне от 0,0 включительно до 1,0 исключительно. Целые случайные числа равномерно распределяются по всему диапазону соответствующего типа за одним исключением: если в аргументе указано целое число max, то диапазон случайных чисел будет от нуля включительно до max исключительно.

Копирование массивов

Для копирования массивов, кроме новых методов copyOf () и copyOfRange () класса Arrays ( ), можно применить статический метод копирования массивов из класса System пакета java.lang, который использует сама исполняющая система Java. Этот метод действует быстро и надежно, его удобно применять в программах. Синтаксис:

static void arraycopy(Object src, int src ind, Object dest, int dest ind, int count);

Из массива, на который указывает ссылка src, копируется count элементов, начиная с элемента с индексом src_ind, в массив, на который указывает ссылка dest, начиная с его элемента с индексом dest_ind.

Все индексы должны быть заданы так, чтобы элементы лежали в массивах, типы массивов должны быть совместимы, а примитивные типы обязаны полностью совпадать. Ссылки на массивы не должны быть равны null.

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

После выполнения

int [ ] arr = {5, 6, 7, 8, 9, 1, 2, 3, 4, 5, -3, -7};

System.arraycopy(arr, 2, arr, 1, arr.length — 2);

получим {5, 7, 8, 9, 1, 2, 3, 4, 5, -3, -7, -7}.

Взаимодействие с системой

Класс System позволяет осуществить и некоторое взаимодействие с системой во время выполнения программы (run time). Но кроме него для этого есть специальный класс

Runtime.

Класс Runtime содержит некоторые методы взаимодействия с JVM во время выполнения программы. Каждое приложение может получить только один экземпляр данного класса статическим методом getRuntime (). Все вызовы этого метода возвращают ссылку на один и тот же объект.

Методы freeMemory( ) и totalMemory( ) возвращают количество свободной и всей памяти, находящейся в распоряжении JVM для размещения объектов, в байтах, в виде числа типа long. Не стоит полностью полагаться на эти числа, поскольку количество памяти меняется динамически.

Метод exit(int status) запускает процесс останова JVM и передает операционной системе статус завершения status. По соглашению, ненулевой статус означает ненормальное завершение. Удобнее использовать аналогичный метод класса System, который является статическим.

Метод halt(int status) осуществляет немедленный останов JVM. Он не завершает запущенные процессы нормально и должен использоваться только в аварийных ситуациях.

Метод loadLibrary(String libName) позволяет подгрузить динамическую библиотеку во время выполнения по ее имени libName.

Метод load (String fileName) подгружает динамическую библиотеку по имени файла fileName, в котором она хранится.

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

Метод gc () запускает процесс освобождения ненужной оперативной памяти (garbage collection). Этот процесс периодически запускается самой виртуальной машиной Java и выполняется на фоне с небольшим приоритетом, но можно его запустить и из программы. Опять-таки здесь удобнее использовать статический метод System.gc ().

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

Например, Runtime.getRuntime().exec("notepad") запускает текстовый редактор Notepad (Блокнот) на платформе MS Windows.

Методы exec () возвращают экземпляр класса Process, позволяющего управлять запущенным процессом. Методом destroy( ) можно остановить процесс, методом exitValue ( ) получить его код завершения. Метод waitFor() приостанавливает основной подпроцесс до тех пор, пока не закончится запущенный процесс.

 

ЧАСТЬ III

Создание графического интерфейса пользователя и апплетов

Глава 8. Принципы построения графического интерфейса
Глава 9. Графические примитивы
Глава 10. Основные компоненты AWT
Глава 11. Оформление ГИП компонентами Swing
Глава 12. Текстовые компоненты
Глава 13. Таблицы
Глава 14. Размещение компонентов и контейнеры Swing
Глава 15. Обработка событий
Глава 16. Оформление рамок
Глава 17. Изменение внешнего вида компонента
Глава 18. Апплеты
Глава 19. Прочие свойства Swing
Глава 20. Изображения и звук

ГЛАВА 8

 

Принципы построения графического интерфейса

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

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

За десятилетия развития вычислительной техники создано много различных графических систем: MS Windows, X Window System, Macintosh. В каждой из них свои правила построения окон и их компонентов: меню, полей ввода, кнопок, списков, полос прокрутки. Эти правила сложны и запутаны. Графические API, предназначенные для создания пользовательского интерфейса, содержат сотни функций.

Для облегчения создания окон и их компонентов написаны библиотеки функций и классов: MFC, Motif, OpenLook, Qt, Tk, Xview, OpenWindows, OpenGL, GTK+ и множество других. Каждый класс такой библиотеки описывает сразу целый графический компонент, управляемый методами этого и других классов.

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

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

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

Такие интерфейсы были названы peer-интерфейсами.

Библиотека классов Java, основанных на peer-интерфейсах, получила название AWT (Abstract Window Toolkit). Для вывода на экран объекта, созданного в приложении Java и основанного на peer-интерфейсе, создается парный ему (peer-to-peer) объект графической подсистемы операционной системы, который и отображается на экране. Поэтому графические объекты AWT в каждой графической среде имеют вид, характерный для этой среды: в MS Windows, Motif, OpenLook, OpenWindows, — везде окна, созданные в AWT, выглядят как "родные" окна этой графической среды.

Пара объектов, реализующих один peer-интерфейс, тесно взаимодействуют во время работы приложения. Изменение объекта в приложении Java немедленно влечет изменение объекта графической оболочки и меняет его вид на экране.

Именно из-за такой реализации peer-интерфейсов и других "родных" (native) методов, написанных главным образом на языке С++, приходится для каждой платформы выпускать свой вариант JDK.

В версии JDK 1.1 библиотека AWT была переработана. В нее добавлена возможность создания компонентов, полностью написанных на Java и не зависящих от peer-интерфейсов. Такие компоненты стали называть "легкими" (lightweight), в отличие от компонентов, реализованных через peer-интерфейсы, названных "тяжелыми" (heavy).

"Легкие" компоненты везде выглядят одинаково, сохраняют заданный при их создании вид (look and feel). Более того, приложение можно разработать таким образом, чтобы после его запуска можно было выбрать какой-то определенный вид: "Motif1, "Metal", "Windows 95" или еще какой-нибудь другой, и сменить этот вид в любой момент работы.

Эта интересная особенность "легких" компонентов получила название PL&F (Pluggable Look and Feel). Это сокращение иногда записывают в виде "plaf'.

Тогда же была создана обширная библиотека "легких" компонентов Java, названная Swing. В ней были переписаны все компоненты библиотеки AWT, так что компоненты библиотеки Swing могут использоваться самостоятельно, несмотря на то, что все классы из нее расширяют классы библиотеки AWT.

Библиотека классов Swing поставлялась как дополнение к JDK 1.1. В следующие версии Java SE JDK она включена наряду с AWT как основная графическая библиотека классов, реализующая идею "100 % Pure Java".

В Java SE библиотека AWT значительно расширена не только библиотекой Swing, но и добавлением новых средств рисования, вывода текстов и изображений, получивших название Java 2D, и средств, реализующих перемещение текста методом DnD (Drag and Drop).

Кроме того, в Java SE включены новые методы ввода/вывода Input Method Framework и средства связи с дополнительными устройствами ввода/вывода, такими как световое перо или клавиатура Брайля, названные Accessibility.

Все перечисленные средства Java SE: AWT, Swing, Java 2D, DnD, Input Method Framework и Accessibility — составили библиотеку графических средств Java, названную JFC (Java Foundation Classes).

Описание каждого из этих средств составит целую книгу, поэтому мы вынуждены ограничиться представлением только основных средств библиотек AWT и Swing.

Компонент и контейнер

Основное понятие графического интерфейса пользователя (ГИП) — компонент (component) графической системы. В русском языке слово "компонент" подразумевает просто составную часть, элемент чего-нибудь, но в графическом интерфейсе это понятие гораздо конкретнее. Оно означает отдельный, полностью определенный элемент, который можно использовать в графическом интерфейсе независимо от других элементов. Например, поле ввода, кнопка, строка меню, полоса прокрутки, радиокнопка (переключатель). Само окно приложения — тоже его компонент. Компоненты могут быть и невидимыми, например панель, объединяющая компоненты, тоже является компонентом.

Вы не удивитесь, узнав, что в AWT компонентом считается объект класса Component или объект всякого класса, расширяющего класс Component. В классе Component собраны общие методы работы с любым компонентом графического интерфейса пользователя. Этот класс — центр библиотеки AWT.

Каждый компонент перед выводом на экран помещается в контейнер (container). Контейнер "знает", как поместить компоненты на экран. Разумеется, в языке Java контейнер — это объект класса Container или всякого его расширения. Прямой наследник этого класса — класс JComponent — вершина иерархии многих компонентов библиотеки Swing.

Создав компонент — объект класса Component или его расширения, следует добавить его к предварительно созданному объекту класса Container или его расширения одним из методов контейнера add ().

Класс Container сам является невидимым компонентом, он расширяет класс Component. Таким образом, в контейнер наряду с компонентами можно помещать контейнеры, в которых находятся какие-то другие компоненты, достигая тем самым большой гибкости расположения компонентов.

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

□ строка заголовка (title bar), с левой стороны которой необходимо поместить кнопку контекстного меню, а с правой — кнопки сворачивания и разворачивания окна и кнопку закрытия приложения;

□ окно должно быть окружено рамкой (border), реагирующей на действия мыши.

Окно с этими компонентами в готовом виде описано в классе Frame. Чтобы создать окно в библиотеке AWT, достаточно сделать свой класс расширением класса Frame, как показано в листинге 8.1. Всего восемь строк текста, и окно готово.

Листинг 8.1. Слишком простое окно приложения AWT

import java.awt.*;

class TooSimpleFrame extends Frame{

public static void main(String[] args){

Frame fr = new TooSimpleFrame(); fr.setSize(400, 150); fr.setVisible(true);

}

}

Класс TooSimpleFrame обладает всеми свойствами класса Frame, являясь его расширением. В нем создается экземпляр окна fr, и методом retsiref) устанавливаются размеры окна на экране — 400x150 пикселов. Если не задать размер окна, то на экране появится окно минимального размера — будет видна только строка заголовка. Конечно, потом окно можно растянуть с помощью мыши до любого размера.

Затем окно выводится на экран методом setVisible(true). Дело в том, что, с точки зрения библиотеки AWT, создать окно — значит, выделить область оперативной памяти, заполненную нужными пикселами, а вывести содержимое этой области на экран — уже другая задача, которую и решает метод setVisible (true ).

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

В листинге 8.2 к программе листинга 8.1 добавлены заголовок окна и обращение к методу, позволяющему завершить приложение.

Листинг 8.2. Простое окно приложения

import java.awt.*;

import java.awt.event.*;

class SimpleFrame extends Frame{

SimpleFrame(String s){ super(s);

setSize(400, 150); setVisible(true);

addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){

System.exit(0);

}

});

}

public static void main(String[] args){ new SimpleFrame(" Моя программа");

}

}

Для того чтобы показать разные варианты построения программы, в нее добавлен конструктор класса SimpleFrame, обращающийся к конструктору своего суперкласса Frame, который записывает свой аргумент s в строку заголовка окна.

В конструктор перенесена установка размеров окна, вывод его на экран и добавлено обращение к методу addWindowListener (), реагирующему на действия с окном. В качестве аргумента этому методу передается экземпляр безымянного внутреннего класса, расширяющего класс WindowAdapter. Этот безымянный класс реализует метод windowClosing (), обрабатывающий попытку закрытия окна. Данная реализация очень проста — приложение завершается статическим методом exit () класса System. Окно при этом закрывается автоматически.

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

Итак, окно готово. Но оно пока пусто. Выведем в него, по традиции, приветствие "Hello, XXI Century World!", правда, слегка измененное. В листинге 8.3 представлена полная программа этого вывода, а рис. 8.1 демонстрирует окно.

Листинг 8.3. Графическая программа с приветствием

import java.awt.*;

import java.awt.event.*;

class HelloWorldFrame extends Frame{

HelloWorldFrame(String s){ super(s);

}

public void paint(Graphics g){

g.setFont(new Font("Serif", Font.ITALIC|Font.BOLD, 30)); g.drawString("Hello, XXI Century World!", 20, 100);

}

public static void main(String[] args){

Frame f = new НеИоИогШЕгатеСЗдравствуй, мир XXI века!"); f. setSize(400, 150); f.setVisible(true);

f.addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){

System.exit(0);

}

});

}

}

Рис. 8.1. Окно программы-приветствия

Для вывода текста мы переопределяем метод paint () класса Component. Класс Frame всегда наследует этот метод, но с пустой реализацией.

Метод paint () получает в качестве аргумента экземпляр g класса Graphics, умеющего, в частности, выводить на экран текст методом drawString (). В этом методе кроме текста мы указываем положение начала строки в окне — 20 пикселов от левого края и 100 пикселов сверху. Эта точка — левая нижняя точка первой буквы текста, H.

Кроме того, мы установили новый шрифт "Serif" большего размера — 30 пунктов, полужирный, курсив. Всякий шрифт — объект класса Font, а задается он методом

setFont ( ) класса Graphics.

Работу со шрифтами мы рассмотрим в следующей главе.

В листинге 8.3, для разнообразия, мы вынесли вызовы методов установки размеров окна, вывода его на экран и завершения программы в метод main ().

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

Иерархия классов AWT

На рис. 8.2 показана иерархия основных классов AWT. Основу ее составляют готовые компоненты: Button, Canvas, Checkbox, Choice, Container, Label, List, Scrollbar, TextArea, TextField, MenuBar, Menu, PopupMenu, MenuItem, CheckboxMenuItem. Если этого набора не хватает, то от класса Canvas можно породить собственные "тяжелые" компоненты, а от класса Component — "легкие" компоненты.

Object

-Component — -Color -Cursor -Font -FontMetrics - Image -Polygon -BorderLayout -Card Layout -FlowLayout -GridBagLayout -GridLayout

-Button —Canvas -Checkbox — Choice —Container —r- JComponent

Label — List Scrollbar —T extComponent

t TextArea TextField

Panel-Applet

— ScrollPane

- JApplet

Window -p Dialog —p FileDialog kjWindow L JDialog L Frame — JFrame

GridBagConstaints

-Graphics -Graphics2D

Point2D-Point

—RectangularShape — Rectangle2D — Rectangle CheckboxGroup -MenuShortcut

MenuComponent-p MenuItem —Menu-PopupMenu

-Event L MenuBar I— CheckboxMenuItem

-EventObject —AWTEvent MediaTracker

Рис. 8.2. Иерархия основных классов AWT

Основные контейнеры — это классы Panel, ScrollPane, Window, Frame, Dialog, FileDialog. Свои "тяжелые" контейнеры можно породить от класса Panel, а "легкие" — от класса

Container.

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

На рис. 8.2 показаны и начальные классы иерархии библиотеки Swing — классы

JComponent, JWindow, JFrame, JDialog, JApplet.

Окно библиотеки Swing

Для получения окна с помощью средств библиотеки Swing необходимо импортировать в свою программу пакет javax.swing и расширить класс JFrame, как показано в листинге 8.4. Вместо длинного метода закрытия окна можно обратиться к методу

setDefaultCloseOperation(JFrame.EXIT ON CLOSE);

указав в нем константу EXIT_ON_CLOSE класса JFrame, предписывающую завершить работу приложения при закрытии окна. Другие константы, определенные в интерфейсе

WindowConstants, предписывают:

□ dispose_on_close — закрыть окно и освободить память, занимаемую им, но не завершать приложение;

□ do_nothing_on_close — игнорировать команду закрытия окна;

□ hide_on_close — только убрать окно с экрана. Это значение по умолчанию.

Фон окна Swing серый, поэтому в конструктор добавлен еще метод setBackground(Color.WHITE), устанавливающий белый цвет фона.

Листинг 8.4. Простое окно приложения Swing

import java.awt.*;

import javax.swing.*;

class SimpleFrame extends JFrame{

SimpleFrame(String s){ super(s);

setBackground(Color.WHITE); setSize(400, 150); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void paint(Graphics g){

g.setFont(new Font("Serif", Font.ITALIC|Font.BOLD, 30)); g.drawString("Hello, XXI Century World!", 20, 100);

}

public static void main(String[] args){ new SimpleFrame(" Моя программа");

}

Метод paint () принадлежит классу Component, он выглядит точно так же, как в листинге 8.3. Его можно без всяких изменений перенести в программу, использующую библиотеку Swing.

Использование системных приложений

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

Эти возможности использует класс Desktop из пакета java.awt. У него есть методы, позволяющие запустить некоторые приложения пользователя:

□ browse(uri file) — открывает браузер по умолчанию, загружающий указанный файл

file;

□ mail () — открывает почтовый клиент по умолчанию;

□ mail (uri mailto) — открывает почтовый клиент по умолчанию, заполняя поля "To", "Cc", "Subject", "Body" значениями, взятыми из аргумента mailto;

□ edit(File file) — открывает текстовый редактор, связанный с указанным файлом file, и загружает в него файл file;

□ open(File file) — открывает указанный файл file;

□ print(File file) — печатает указанный файл file на принтере, назначенном этому файлу.

Связь с приложениями пользователя устанавливается через операционную систему, поэтому экземпляр класса Desktop создается не конструктором, а статическим методом getDesktop (). Эту связь удается установить не во всех системах, поэтому предварительно следует сделать проверку статическим логическим методом isDesktopSupported(). Итак, создание экземпляра класса Desktop выглядит так:

Desktop d = null; if (Desktop.isDesktopSupported()) d = Desktop.getDesktop();

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

public boolean isSupported(Desktop.Action action);

Аргументом этого метода может служить одна из следующих констант вложенного перечисления Action:

□ browse — у пользователя есть браузер по умолчанию;

□ MAIL — у пользователя есть почтовый клиент по умолчанию;

□ EDIT — можно открыть текстовый редактор, связанный с файлами;

□ OPEN — можно открыть файл;

□ PRINT — можно напечатать файл.

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

if (d.isSupported(Desktop.Action.BROWSE))

d.browse(new URI(""));

if (d.isSupported(Desktop.Action.MAIL)) d.mail();

if (d.isSupported(Desktop.Action.EDIT))

d.edit(new File("/home/user/j ava/src/MyDesktop.j ava");

if (d.isSupported(Desktop.Action.OPEN))

d.open(new File("/home/user/j ava/src/MyDesktop.j ava");

if (d.isSupported(Desktop.Action.PRINT))

d.print new File("/home/user/java/src/MyDesktop.java");

System Tray

Графическое приложение Java может установить ярлык в зоне уведомлений (notification area), называемой также системным лотком (system tray) или просто треем. Эта зона обычно размещается в правом нижнем углу экрана и содержит часы и ярлыки запущенных программ. Для работы с зоной уведомлений в пакет java.awt включен класс SystemTray. Его метод add(Trayicon icon) помещает ярлык icon в зону уведомлений, а метод remove (Trayicon icon) удаляет его из зоны. Каждое приложение может поместить в зону уведомлений несколько ярлыков.

Как видно из заголовков, параметр этих методов, ярлык- это объект класса TrayIcon.

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

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

TrayIcon icon = null; if (SystemTray.isSupported()){

SystemTray tray = SystemTray.getSystemTray();

Image im = Toolkit.getDefaultToolkit.getImage("myicon.gif"); icon = new TrayIcon(im); tray.add(icon);

}

Более подробно работа с зоной уведомлений показана в документации к классу SystemTray. Она будет понятна после прочтения главы 15.

Splash Screen

Очень часто при загрузке приложения на экране вначале появляется небольшое окнозаставка (splash screen) с каким-нибудь изображением, сменяемое затем главным окном приложения. Такое окно можно открыть при запуске приложения из командной строки, указав ключ -splash. Например, если файл с изображением называется name.gif, то запустить приложение можно так:

java -splash:name.gif SimpleFrame

При запуске приложения из архива, например

java -jar SimpleFrame.jar

имя файла с изображением записывается в файл MANIFEST.MF, как показано в главе 25.

Некоторые возможности управления окном-заставкой предоставляет класс SplashScreen из пакета java.awt. Это возможность менять изображение методом setImageURL(URL image) и возможность рисовать в окне, получив ссылку на объект класса Graphics2D методом createGraphics (). После заполнения окна-заставки оно выводится на экран методом update (). Все это делается по следующей схеме:

SplashScreen splash = SplashScreen.getSplashScreen(); if (splash != null){

Graphics2D g = splash.createGraphics();

g.setPaintMode();

g.drawString("Loading...", 100, 200);

// и т. д., рисуем, как написано в главе 9. g.update();

}

Заключение

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

Оно приводит к созданию все новых библиотек классов и расширению существующих. Независимыми производителями создано уже много графических библиотек Java: KL Group, JBCL, SWAT, SWT и появляются все новые и новые библиотеки. Сведения о них можно получить на сайтах, указанных во введении.

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

Вопросы для самопроверки

1. Что такое графический интерфейс пользователя?

2. Что такое графическая библиотека классов?

3. Что называется графическим компонентом?

4. Назовите известные вам графические компоненты.

5. Что такое контейнер в графическом интерфейсе?

6. Будет ли основное окно приложения контейнером?

7. Можно ли использовать библиотеку Swing без библиотеки AWT?

8. Какая разница между компонентами AWT и компонентами Swing?

9. Можно ли совсем отказаться от компонентов библиотеки AWT?

ГЛАВА 9

 

Графические примитивы

При создании графического компонента, т. е. объекта класса Component, автоматически формируется его графический контекст (graphics context). В контексте размещается область рисования и вывода текста и изображений. Контекст содержит текущий и альтернативный цвет рисования и цвет фона — объекты класса Color, текущий шрифт для вывода текста — объект класса Font.

В контексте определена система координат, начало которой — точка с координатами (0, 0) — расположено в верхнем левом углу области рисования, ось Ox направлена вправо, ось Oy — вниз. Точки координатной системы находятся между точками области рисования.

Управляет контекстом класс Graphics или более новый класс Graphics2D, созданный в рамках библиотеки Java 2D. Поскольку графический контекст сильно зависит от конкретной графической платформы, эти классы сделаны абстрактными. Поэтому нельзя непосредственно создать экземпляры класса Graphics или Graphics2D.

Однако каждая виртуальная машина Java реализует методы этих классов, создает их экземпляры для компонента и предоставляет объект класса Graphics методом getGraphics ( ) класса Component или передает его как аргумент методов paint () и update ().

Посмотрим сначала, какие методы работы с графикой и текстом предоставляет нам класс Graphics.

Методы класса Graphics

При создании контекста в нем задается текущий цвет для рисования, обычно черный, и цвет фона области рисования — белый или серый. Изменить текущий цвет можно методом setColor (Color newColor), аргумент newColor которого — объект класса Color.

Узнать текущий цвет можно методом getColor ( ), возвращающим объект класса Color.

Как задать цвет

Цвет, как и все в Java, — объект определенного класса, а именно класса Color. Основу класса составляют семь конструкторов цвета.

Самый простой конструктор,

Color(int red, int green, int blue);

создает цвет, получающийся как смесь красной red, зеленой green и синей blue составляющих. Эта цветовая модель называется RGB. Каждая составляющая меняется от 0 (отсутствие составляющей цвета) до 255 (полная интенсивность этой составляющей цвета). Например, следующие строки:

Color pureRed = new Color(255, 0, 0);

Color pureGreen = new Color(0, 255, 0);

определяют чистый ярко-красный цвет pureRed и чистый ярко-зеленый цвет pureGreen.

Во втором конструкторе интенсивность составляющих можно изменять более гладко вещественными числами от 0.0 (отсутствие составляющей) до 1.0 (полная интенсивность составляющей):

Color(float red, float green, float blue);

Например:

Color someColor = new Color(0.05f, 0.4f, 0.95f);

Третий конструктор,

Color(int rgb);

задает все три составляющие в одном целом числе. В битах 16—23 записывается красная составляющая, в битах 8—15 — зеленая, а в битах 0—7 — синяя составляющая цвета. Например:

Color c = new Color(0xFF8F48AF);

Здесь красная составляющая задана с интенсивностью 0x8F, зеленая — 0x4 8, синяя —

0xAF .

Следующие три конструктора:

Color(int red, int green, int blue, int alpha);

Color(float red, float green, float blue, float alpha);

Color(int rgb, boolean hasAlpha);

вводят четвертую составляющую цвета, так называемую альфу, определяющую прозрачность цвета. Эта составляющая проявляет себя при наложении одного цвета на другой. Если альфа равна 255 или 1.0, то цвет совершенно непрозрачен, — предыдущий, нижний, цвет не просвечивает сквозь него. Если альфа равна 0 или 0.0, то цвет абсолютно прозрачен, — для каждого пиксела виден только предыдущий цвет. Промежуточные значения позволяют создать полупрозрачные изображения, сквозь которые просвечивает фон.

Последний из этих конструкторов учитывает альфа-составляющую, находящуюся в битах 24—31, если параметр hasAlpha равен true. Если же hasAlpha равно false, то составляющая альфа считается равной 255, независимо от того, что записано в старших битах параметра rgb.

Можно сказать, что первые три конструктора создают непрозрачный цвет с альфой, равной 255 или 1.0.

Седьмой конструктор,

Color(ColorSpace cspace, float[] components, float alpha);

позволяет создавать цвет не только в цветовой модели (color model) RGB, но и в других моделях: CMYK, HSB, CIE XYZ, определенных объектом класса ColorSpace.

Для создания цвета в модели HSB можно воспользоваться статическим методом

getHSBColor(float hue, float saturation, float brightness);

Если нет необходимости тщательно подбирать цвета, то можно просто воспользоваться одной из тринадцати статических констант типа Color, имеющихся в классе Color. Вопреки соглашению "Code Conventions" в первых версиях JDK их записывали строчными буквами: black, blue, cyan, darkGray, gray, green, lightGray, magenta, orange, pink, red,

white, yellow. В последние версии JDK добавили те же константы, записанные прописными буквами: BLACK, blue, cyan, dark_gray, gray, green, light_gray, magenta, orange, pink, red, white, yellow. Сейчас можно использовать и ту и другую запись.

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

Методы класса Color позволяют получить составляющие текущего цвета: getRed(),

getGreen(), getBlue(), getAlpha(), getRGB(), getColorSpace(), getComponents().

Два метода создают более яркий brighter () и более темный darker () цвета по сравнению с текущим цветом. Они полезны, если надо выделить изображение активного компонента ярким цветом или, наоборот, показать неактивный компонент бледнее остальных компонентов.

Два статических метода возвращают цвет, преобразованный из цветовой модели RGB в модель HSB и обратно:

float[] RGBtoHSB(int red, int green, int blue, float[] hsb); int HSBtoRGB(int hue, int saturation, int brightness);

Создав цвет, можно рисовать им в графическом контексте.

Упражнение

1. Создайте чисто желтый цвет в разных цветовых моделях.

Как нарисовать чертеж

Основной метод рисования,

drawLine(int x1, int y1, int x2, int y2);

вычерчивает текущим цветом отрезок прямой между точками с координатами (xi, yi)

Рё (x2, y2).

Одного этого метода достаточно, чтобы нарисовать любую картину по точкам, вычерчивая каждую точку с координатами (x, у) методом drawLine(x, у, х, у) и меняя цвета от точки к точке. Но никто, разумеется, не станет этого делать.

Другие графические примитивы:

□ drawRect (int x, int y, int width, int height) -чертит прямоугольник со сторонами,

параллельными краям экрана, задаваемый координатами верхнего левого угла (x, y), шириной width пикселов и высотой height пикселов;

□ draw3DRect(int x, int y, int width, int height, boolean raised) — чертит прямоугольник, как будто выделяющийся из плоскости рисования, если параметр raised равен true, или как будто вдавленный в плоскость, если параметр raised равен false;

□ drawOval(int x, int y, int width, int height) - чертит овал, вписанный в прямо

угольник, заданный параметрами метода. Если width == height, то получится окружность;

□ drawArc(int x, int y, int width, int height, int startAngle, int arc) — чертит дугу овала, вписанного в прямоугольник, заданный первыми четырьмя параметрами. Дуга имеет величину arc градусов и отсчитывается от угла startAngle. Угол отсчитывается в градусах от оси Ox. Положительный угол отсчитывается против часовой стрелки, отрицательный — по часовой стрелке;

□ drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) — чертит прямоугольник с закругленными краями. Закругления рисуются четвертинками овалов, вписанных в прямоугольники шириной arcWidth и высотой arcHeight, построенные в углах основного прямоугольника;

□ drawPolyline (int [ ] xPoints, int[] yPoints, int nPoints) — чертит ломаную с вершинами в точках (xPoints [i] , yPoints [i] ) и числом вершин nPoints;

□ drawPolygon (int [ ] xPoints, int[] yPoints, int nPoints) — чертит замкнутую ломаную, проводя замыкающий отрезок прямой между первой и последней точкой;

□ drawPolygon (Polygon p) — чертит замкнутую ломаную, вершины которой заданы объектом p класса Polygon.

Класс Polygon рассмотрим подробнее.

Класс Polygon

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

Объекты этого класса можно создать двумя конструкторами:

□ Polygon () — создает пустой объект;

□ Polygon (int [ ] xPoints, int[] yPoints, int nPoints) — задаются координаты вершин многоугольника (xPoints[i], yPoints[i]) и их число nPoints. Несоответствие параметров вызывает исключительную ситуацию.

После создания объекта в него можно добавлять вершины методом

addPoint(int x, int y);

Логические методы contains () позволяют проверить, не лежит ли в многоугольнике заданная параметрами метода точка, отрезок прямой или целый прямоугольник со сторонами, параллельными сторонам экрана:

boolean contains(int x, int y); boolean contains(double x, double y); boolean contains(Point p); boolean contains(Point2D p);

boolean contains(double x, double y, double width, double height); boolean contains(Rectangle2D rectangle);

Два логических метода intersects () позволяют проверить, не пересекается ли с данным многоугольником отрезок прямой, заданный параметрами метода, или прямоугольник со сторонами, параллельными сторонам экрана:

boolean intersects(double x, double y, double width, double height); boolean intersects(Rectangle2D rectangle);

Методы getBounds () и getBounds2D() возвращают прямоугольники класса Rectangle и класса Rectangle2D соответственно, целиком содержащие в себе данный многоугольник.

Упражнение

2. Сделайте рисунок по описанию: "Точка, точка, запятая, минус — рожица кривая".

Прочие методы класса Graphics

Вернемся к методам класса Graphics. Несколько методов вычерчивают фигуры, залитые текущим цветом: fillRect(), fill3DRect (), fillArc(), fillOval(), fillPolygon(), fillRoundRect (). У них такие же параметры, как и у соответствующих методов, вычерчивающих незаполненные фигуры.

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

public void paint(Graphics g){

Color initColor = g.getColor(); // Сохраняем исходный цвет.

g.setColor(new Color(0, 0, 255)); // Устанавливаем цвет фона.

// Заливаем область рисования.

g.fillRect(0, 0, getSize().width — 1, getSize().height — 1); g.setColor(initColor); // Восстанавливаем исходный цвет.

// Дальнейшие действия...

}

Как видите, в классе Graphics собраны только самые необходимые средства рисования. Нет даже метода, задающего цвет фона (хотя можно указать цвет фона компонента методом setBackground () класса Component). Средства рисования, вывода текста в область рисования и вывода изображений значительно дополнены и расширены в подклассе Graphics2D, входящем в систему Java 2D. Например, в нем есть метод задания цвета фона setBackground(Color c).

Перед тем как обратиться к классу Graphics2D, рассмотрим средства класса Graphics для вывода текста.

Как вывести текст

Для вывода текста в область рисования текущим цветом и шрифтом, начиная с точки (x, y), в классе Graphics есть несколько методов:

□ drawString(String s, int x, int y) — выводит строку s;

□ drawBytes(byte[] b, int offset, int length, int x, int y) — выводит length элементов массива байтов b, начиная с индекса offset;

□ drawChars(char[] ch, int offset, int length, int x, int y) — выводит length элементов массива символов ch, начиная с индекса offset.

Четвертый метод выводит текст, занесенный в объект класса, реализующего интерфейс AttributedCharacteriterator. Это позволяет задавать свой шрифт для каждого выводимого символа:

drawString(AttributedCharacterIterator iter, int x, int y);

Во всех этих методах точка (x, y) — это левая нижняя точка первой буквы текста на базовой линии (baseline) вывода шрифта.

Как установить шрифт

Метод setFont (Font newFont) класса Graphics устанавливает текущий шрифт для вывода текста.

Метод getFont () возвращает текущий шрифт.

Как и все в языке Java, шрифт — это объект, а именно объект класса Font. Посмотрим, какие возможности предоставляет данный класс.

Как задать шрифт

Объекты класса Font хранят начертания (glyphs) символов, образующие шрифт. Их можно создать двумя конструкторами:

□ Font (Map attributes ) -задает шрифт с указанным аргументом attributes атрибутами.

Ключи атрибутов и некоторые их значения задаются константами класса TextAttribute из пакета java.awt.font. Этот конструктор характерен для Java 2D и будет рассмотрен далее в настоящей главе;

□ Font(String name, int style, int size) — задает шрифт по имени name, со стилем style и размером size типографских пунктов. Этот конструктор характерен для JDK 1.1, но широко используется и в Java 2D в силу своей простоты.

Типографский пункт в России и некоторых европейских странах равен 0,376 мм, точнее, 1/72 части французского дюйма. В англо-американской системе мер пункт равен 1/72 части английского дюйма — 0,351 мм. Этот-то пункт и применяется в компьютерной графике.

Именем шрифта name может быть строка с физическим именем шрифта, например

"Courier New", или одна из строк "Dialog", "Dialoginput", "Monospaced", "Serif", "SansSerif", "Symbol". Это так называемые логические имена шрифтов (logical font names). Если name == null, то задается шрифт по умолчанию.

Стиль шрифта style — это одна из констант класса Font:

□ bold — полужирный;

□ ITALIC — курсив;

□ PLAIN-обычный.

Полужирный курсив (bolditalic) можно задать операцией побитового сложения, Font. bold| Font. italic, как это сделано в листинге 8.3.

При выводе текста логическим именам шрифтов и стилям сопоставляются физические имена шрифтов (font face name) или имена семейств шрифтов (font name). Это имена реальных шрифтов, имеющихся в графической подсистеме операционной системы.

Например, логическому имени "Serif" может быть сопоставлено имя семейства (family) шрифтов "Times New Roman", а в сочетании со стилями — конкретные физические имена "Times New Roman Bold", "Times New Roman Italic". Эти шрифты должны находиться в составе шрифтов графической системы той машины, на которой выполняется приложение.

Список имен доступных шрифтов можно просмотреть следующими операторами:

Font[] fonts = Toolkit.getGraphicsEnvironment.getAllFonts(); for (Font f: fonts)

System.out.println(f.getFontName());

В состав Java SE входит семейство шрифтов Lucida. Установив JDK, вы можете быть уверены, что эти шрифты есть в вашей системе.

Таблицы сопоставления логических и физических имен шрифтов находятся в виртуальной машине Java или в файлах с именами:

в–Ў fonteonfig.properties; в–Ў fonteonfig.2003.properties;

в–Ў fontconfig.Me.properties; в–Ў fontconfig.RedHat.properties

в–Ў fontconfig.2000.XP.properties; Рё С‚. Рґ.

в–Ў fontconfig.XP.properties;

Эти файлы должны быть расположены в JDK в каталоге jdk1.7.0/jre/lib или каком-либо другом подкаталоге lib корневого каталога JDK той машины, на которой выполняется приложение.

Файлы хранятся в исходном виде, с расширением src, и в откомпилированном виде, с расширением bfc.

Нужный файл выбирается виртуальной машиной Java по названию операционной системы. Если такой файл не найден, то применяется файл fonteonfig.properties, не соответствующий никакой конкретной операционной системе.

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

В листинге 9.1 показано сокращенное содержимое файла fontconfig.properties.src из Java SE 7 для платформы MS Windows.

Листинг 9.1. Примерный файл fontconfig.properties.src

#

# Copyright В© 2003, 2010, Oracle and/or its affilates. All rights reserved.

#

# Version version=1

# Component Font Mappings allfonts.chinese-ms936=SimSun allfonts.chinese-gb18030=SimSun-18030 allfonts.chinese-hkscs=MingLiU HKSCS allfonts.devanagari=Mangal allfonts.dingbats=Wingdings allfonts.lucida=Lucida Sans Regular allfonts.symbol=Symbol

allfonts.thai=Lucida Sans Regular

serif.plain.alphabetic=Times New Roman serif.plain.chinese-ms950=MingLiU serif.plain.hebrew=David serif.plain.japanese=MS Mincho serif.plain.korean=Batang

serif.bold.alphabetic=Times New Roman Bold

# И так далее

serif.italic.alphabetic=Times New Roman Italic

# И так далее

serif.bolditalic.alphabetic=Times New Roman Bold Italic

# И так далее

sansserif.plain.alphabetic=Arial

# И так далее

monospaced.plain.alphabetic=Courier New

# И так далее

dialog.plain.alphabetic=Arial

# И так далее

dialoginput.plain.alphabetic=Courier New

# И так далее

# Search Sequences

sequence.allfonts=alphabetic/default,dingbats,symbol

# И так далее

# Exclusion Ranges

exclusion.alphabetic=0700-1e9f,1f00-20ab,20ad-f8ff exclusion.chinese-gb18030=0390-03d6,2200-22ef,2701-27be exclusion.hebrew=0041-005a,0060-007a,007f-00ff,20ac-20ac

# Monospaced to Proportional width variant mapping

# (Experimental private syntax) proportional.MS Gothic=MS PGothic proportional.MS Mincho=MS PMincho proportional.MingLiU=PMingLiU

# Font File Names filename.Arial=ARIAL.TTF filename.Arial Bold=ARIALBD.TTF filename.Arial Italic=ARIALI.TTF filename.Arial Bold Italic=ARIALBI.TTF filename.Courier New=COUR.TTF filename.Courier New Bold=COURBD.TTF filename.Courier New Italic=COURI.TTF filename.Courier New Bold Italic=COURBI.TTF

filename.Times New Roman=TIMES.TTF

filename.Times New Roman Bold=TIMESBD.TTF

filename.Times New Roman Italic=TIMESI.TTF

filename.Times New Roman Bold Italic=TIMESBI.TTF

filename.SimSun=SIMSUN.TTC

filename.SimSun-18030=SIMSUN18030.TTC

filename.MingLiU=MINGLIU.TTC

filename.PMingLiU=MINGLIU.TTC

filename.MingLiU HKSCS=hkscsm3u.ttf

filename.David=DAVID.TTF

filename.David Bold=DAVIDBD.TTF

filename.MS_Mincho=MSMINCHO.TTC

filename.MS_PMincho=MSMINCHO.TTC

filename.MS_Gothic=MSGOTHIC.TTC

filename.MS_PGothic=MSGOTHIC.TTC

filename.Gulim=gulim.TTC

filename.Batang=batang.TTC

filename.GulimChe=gulim.TTC

filename.Lucida Sans Regular=LucidaSansRegular.ttf filename.Mangal=MANGAL.TTF filename.Symbol=SYMBOL.TTF filename.Wingdings=WINGDING.TTF

Большая часть этого файла занята сопоставлениями логических и физических имен. Вы видите, что:

□ логическому имени "dialog" сопоставлено имя семейства "Arial";

□ логическому имени "dialoginput" сопоставлено имя семейства "Courier New";

□ логическому имени "serif" сопоставлено имя семейства "Times New Roman";

□ логическому имени "sansserif" сопоставлено имя семейства "Arial";

□ логическому имени "monospaced" сопоставлено имя семейства "Courier New".

Там, где указан стиль: dialog.italic, dialog.bold и т. д., подставлен соответствующий физический шрифт.

В строках листинга 9.1, начинающихся со слова filename, указаны файлы с соответствующими физическими шрифтами, например:

filename.Arial=ARIAL.TTF

Эти строки необязательны, но они ускоряют поиск файлов со шрифтами.

Теперь посмотрите на другие строки листинга 9.1. Строка

exclusion.alphabetic=0700-1e9f,1f00-20ab,20ad-f8ff

означает, что в алфавитных шрифтах не станут отыскиваться начертания (glyphs) символов с кодами в диапазонах '\u07 00' — '\u1e9f', '\u1f00 ' —' \u20ab' и ' \u20ad' —' \ uf8ff'.

Они будут взяты из шрифта, следующего далее в строке

sequence.allfonts=alphabetic/default,dingbats,symbol

а именно из шрифта Wingdings.

Итак, собираясь выводить строку str в графический контекст методом drawString( ), мы создаем текущий шрифт конструктором класса Font, указывая в нем логическое имя шрифта, например "Serif". Исполняющая система Java отыскивает в файле fonteonfig.properties, соответствующем локальному языку, сопоставленный этому логическому имени физический шрифт операционной системы, например Times New Roman. Если это Unicode-шрифт, то из него извлекаются начертания символов строки str по их кодировке Unicode и отображаются в графический контекст. Если это байтовый ASCII-шрифт, то строка str предварительно перекодируется в массив байтов методами соответствующего класса, например класса CharToByteCp1251.

Эти вопросы обсуждаются в документации Java SE в файле docs/technotes/guides/intl/ fontconfig.html.

Завершая рассмотрение логических и физических имен шрифтов, следует сказать, что в JDK 1.0 использовались логические имена "Helvetica", "TimesRoman", "Courier", из лицензионных соображений замененные в JDK 1.1 на "SansSerif", "Serif", "Monospaced" соответственно.

При выводе строки в окно приложения очень часто возникает необходимость расположить ее определенным образом относительно других элементов изображения: центрировать, вывести над или под другим графическим объектом. Для этого надо знать метрику строки: ее высоту и ширину. Для измерения размеров отдельных символов и строки в целом разработан класс FontMetrics.

В Java 2D класс FontMetrics заменен классом TextLayout. Его мы рассмотрим в конце этой главы, а сейчас выясним, какую пользу можно извлечь из методов класса

FontMetrics.

Класс FontMetrics

Класс FontMetrics является абстрактным, поэтому нельзя воспользоваться его конструктором. Для получения объекта класса FontMetrics, содержащего набор метрических характеристик шрифта f, следует обратиться к методу getFontMetrics(Font f) класса Graphics или класса Component.

Подробно с характеристиками компьютерных шрифтов можно познакомиться по книге [12].

Класс FontMetrics позволяет узнать ширину отдельного символа ch в пикселах методом charWidth(ch), общую ширину всех символов массива или подмассива символов или байтов — методами getchars () и getBytes (), ширину целой строки str в пикселах — методом stringWidth(str).

Несколько методов возвращают в пикселах вертикальные размеры шрифта.

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

Среднее расстояние от базовой линии шрифта до верхней точки прописных букв и выступающих элементов той же строки (ascent) возвращает метод getAscent(), а максимальное — метод getMaxAscent ( ).

Среднее расстояние свисающих элементов от базовой линии той же строки (descent) возвращает метод getDescent (), а максимальное — метод getMaxDescent ().

Наконец, высоту шрифта (height) — сумму ascent + descent + leading — возвращает метод getHeight (). Высота шрифта равна расстоянию между базовыми линиями соседних строк.

Эти элементы показаны на рис. 9.1.

РђР±РІРґРѕ\ f ascent Height
__J_ _descent
leading
Жфйёь Ц baseline

Рис. 9.1. Элементы шрифта

Дополнительные характеристики шрифта можно определить методами класса LineMetrics из пакета java.awt.font. Объект этого класса можно получить несколькими методами getLineMetrics ( ) класса FontMetrics.

Листинг 9.2 показывает применение графических примитивов и шрифтов, а рис. 9.2 — результат выполнения программы из этого листинга.

Листинг 9.2. Использование графических примитивов и шрифтов

import java.awt.*; import javax.swing.*;

class GraphTest extends JFrame{

GraphTest(String s){ super(s);

setBounds(0, 0, 500, 300); setVisible(true);

}

public void paint(Graphics g){

Dimension d = getSize();

int dx = d.width / 20, dy = d.height / 20; g.drawRect(dx, dy + 20,

d.width — 2 * dx, d.height — 2 * dy — 20); g.drawRoundRect(2 * dx, 2 * dy + 20,

d.width — 4 * dx, d.height — 4 * dy — 20, dx, dy);

g.fillArc(d.width / 2 — dx, d.height — 2 * dy + 1,

2 * dx, dy — 1, 0, 360);

g.drawArc(d.width / 2 — 3 * dx, d.height — 3 * dy / 2 — 5, dx, dy / 2, 0, 360);

g.drawArc(d.width / 2 + 2 * dx, d.height — 3 * dy / 2 — 5, dx, dy / 2, 0, 360);

Font f1 = new Font("Serif", Font.BOLD|Font.ITALIC, 2 * dy);

Font f2 = new Font("Serif", Font.BOLD, 5 * dy / 2);

FontMetrics fm1 = getFontMetrics(f1);

FontMetrics fm2 = getFontMetrics(f2);

String si = "Всякая последняя ошибка";

String s2 = "является предпоследней.";

String s3 = "Закон отладки";

int firstLine = d.height / 3;

int nextLine = fm1.getHeight();

int secondLine = firstLine + nextLine / 2;

g.setFont(f2);

g.drawString(s3, (d.width-fm2.stringWidth(s3)) / 2, firstLine); g.drawLine(d.width / 4, secondLine — 2,

3 * d.width / 4, secondLine — 2); g.drawLine(d.width / 4, secondLine — 1,

3 * d.width / 4, secondLine — 1); g.drawLine(d.width / 4, secondLine,

3 * d.width / 4, secondLine);

g.setFont(f1);

g.drawString(s1, (d.width — fm1.stringWidth(s1)) / 2, firstLine + 2 * nextLine);

g.drawString(s2, (d.width — fm1.stringWidth(s2)) / 2, firstLine + 3 * nextLine);

}

public static void main(String[] args){

GraphTest f = new GraphTest(" Пример рисования"); f.setDefaultCloseOperation(EXIT ON CLOSE);

}

}

Рис. 9.2. Пример использования класса Graphics

В листинге 9.2 использован простой класс Dimension, главная задача которого — хранить ширину и высоту прямоугольного объекта в своих полях width и height. Метод getSize () класса Component возвращает размеры компонента в виде объекта класса Dimension. В ЛИСТИНГе 9.2 размеры компонента f типа GraphTest установлены в конструкторе методом setBounds () равными 500x300 пикселов.

Еще одна особенность листинга 9.2 — для вычерчивания толстой линии, отделяющей заголовок от текста, пришлось провести три параллельные прямые на расстоянии один пиксел друг от друга.

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

Эти ограничения можно обойти разными хитростями: чертить несколько параллельных линий, прижатых друг к другу, как в листинге 9.2, или узкий заполненный прямоугольник, выводить текст по одной букве, получить разрешение экрана методом getScreenSize ( ) класса java.awt.Toolkit и использовать его в дальнейшем. Но все это затрудняет программирование, лишает его стройности и естественности, нарушает принцип KISS.

Класс Graphics, в рамках системы Java 2D, значительно расширен классом Graphics2D.

Упражнение

3. Поупражняйтесь в выводе текста различными шрифтами с разным расположением на экране.

Возможности Java 2D

В систему пакетов и классов Java 2D, основа которой — класс Graphics2D пакета j ava. awt, внесено несколько принципиально новых положений.

□ Кроме координатной системы, принятой в классе Graphics и названной координатным пространством пользователя (User Space), введена еще система координат устройства вывода (Device Space): экрана монитора, принтера. Методы класса Graphics2D автоматически переводят (transform) систему координат пользователя в систему координат устройства при выводе графики.

□ Преобразование координат пользователя в координаты устройства можно задать "вручную", причем преобразованием способно служить любое аффинное преобразование плоскости, в частности поворот на любой угол и/или сжатие/растяжение. Оно определяется как объект класса AffineTransform. Его можно установить как преобразование по умолчанию методом setTransform(). Возможно выполнять преобразование "на лету" методами transform() и translate () и делать композицию преобразований методом concatenate ().

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

□ Графические примитивы: прямоугольник, овал, дуга и др., реализуют теперь новый интерфейс Shape пакета java.awt. Для их вычерчивания можно использовать новый единый для всех фигур метод draw (), аргументом которого способен служить любой объект, реализовавший интерфейс Shape. Введен метод fill (), заполняющий фигуры — объекты класса, реализовавшего интерфейс Shape.

□ Для вычерчивания (stroke) линий введено понятие пера (pen). Свойства пера описывает интерфейс Stroke. Класс BasicStroke реализует этот интерфейс. Перо обладает четырьмя характеристиками:

• оно имеет толщину (width) в один (по умолчанию) или несколько пикселов;

• оно может закончить линию (end cap) закруглением — это статическая константа cap_round, прямым обрезом — константа cap_square (по умолчанию) или не фиксировать определенный способ окончания — константа cap_butt;

• оно может сопрягать линии (line joins) закруглением — статическая константа join_round, отрезком прямой — константа join_bevel или просто состыковывать — константа join_miter (по умолчанию);

• оно может чертить линию различными пунктирами (dash) и штрихпунктирами, при этом длины штрихов и промежутков задаются в массиве, элементы которого с четными индексами задают длину штриха, а с нечетными индексами — длину промежутка между штрихами.

□ Методы заполнения фигур описаны в интерфейсе Paint. Несколько классов реализуют этот интерфейс. Класс Color реализует его сплошной (solid) заливкой, класс GradientPaint — градиентным (gradient) заполнением, при котором цвет плавно меняется от одной заданной точки к другой заданной точке, класс TexturePaint — заполнением по предварительно заданному образцу (pattern fill). Класс MultipleGradientPaint организует градиентную заливку с несколькими градиентами, причем его подкласс LinearGradientPaint делает линейную заливку, а подкласс RadialGradientPaint — радиальную заливку.

□ Буквы текста понимаются как фигуры, т. е. объекты, реализующие интерфейс Shape, и могут вычерчиваться методом draw () с использованием всех возможностей этого метода. При их вычерчивании применяется перо, все методы заполнения фигур и их преобразования.

□ Кроме имени, стиля и размера шрифт получил много дополнительных атрибутов, например преобразование координат, подчеркивание или перечеркивание текста, вывод текста справа налево. Цвет текста и его фона являются теперь атрибутами самого текста, а не графического контекста. Можно задать разную ширину символов шрифта, надстрочные и подстрочные индексы. Атрибуты устанавливаются константами класса TextAttribute.

□ Процесс визуализации (rendering) регулируется правилами (hints), определенными константами класса RenderingHints.

С такими возможностями Java 2D стала полноценной системой рисования, вывода текста и изображений. Посмотрим, как реализованы эти возможности и как ими можно

воспользоваться.

Преобразование координат

Правило преобразования координат пользователя в координаты графического устройства (transform) задается автоматически при создании графического контекста так же, как цвет и шрифт. В дальнейшем его можно изменить методом setTransform() так же, как меняется цвет или шрифт. Параметром этого метода служит объект класса AffineTransform из пакета java.awt.geom, подобно объектам класса Color или Font при задании цвета или шрифта.

Рассмотрим подробнее класс AffineTransform.

Класс AffineTransform

Аффинное преобразование координат задается двумя основными конструкторами класса AffineTransform:

AffineTransform(double a, double b, double c, double d, double e, double f); AffineTransform(float a, float b, float c, float d, float e, float f);

При этом точка с координатами (x, y) в пространстве пользователя перейдет в точку с координатами (a * x + c * y + e, b * x + d * y + f) в пространстве графического устройства.

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

Следующие два конструктора используют в качестве параметра массив из шести элементов-коэффициентов преобразования {a, b, c, d, e, f} или массив из четырех элементов {a, b, c, d}, если e = f = 0, составленный из таких же коэффициентов в том же порядке:

AffineTransform(double[] arr);

AffineTransform(float[] arr);

Пятый конструктор создает копию другого, уже имеющегося, объекта:

AffineTransform(AffineTransform at);

Шестой конструктор — конструктор по умолчанию — создает тождественное преобразование:

AffineTransform();

Эти конструкторы математически точны, но неудобны при задании конкретных преобразований. Попробуйте рассчитать коэффициенты поворота на 57° вокруг точки с координатами (20, 40) или сообразить, как будет преобразовано пространство пользователя после выполнения методов:

AffineTransform at =

new AffineTransform(-1.5, 4.45, -0.56, 34.7, 2.68, 0.01); g.setTransform(at);

Во многих случаях удобнее создать преобразование статическими методами, возвращающими объект класса AffineTransform.

□ getRotateInstance (double angle) — возвращает поворот на угол angle, заданный в радианах, вокруг начала координат. Положительное направление поворота таково, что точки оси Ox поворачиваются в направлении к оси Oy. Если оси координат пользователя не менялись преобразованием отражения, то положительное значение angle задает поворот по часовой стрелке.

□ getRotateInstance (double angle, double x, double y) — такой же поворот вокруг точки с координатами (x, y) .

□ getRotateInstance (double vx, double vy) - поворот, заданный вектором с координа

тами (vx, vy). Эквивалентен методу getRotateInstance(Math.atan2(vx, vy)).

□ getRotateInstance(double vx, double vy, double x, double y) — поворот вокруг точки с координатами (x, y), заданный вектором с координатами (vx, vy). Эквивалентен методу getRotateInstance(Math.atan2(vx, vy), x, y).

□ getQuadrantRotatelnstance (int n) - поворот n раз на угол 90° вокруг начала коорди

нат. Эквивалентен методу getRotateInstance(n * Math.PI / 2.0).

□ getQuadrantRotateInstance(int n, double x, double y) — поворот n раз на угол 90° вокруг точки с координатами (x, y). Эквивалентен методу getRotateInstance(n * Math.PI / 2.0, x, y).

□ getScaleInstance (double sx, double sy) — изменяет масштаб по оси Ox в sx раз, по оси Oy — в sy раз.

□ getShareInstance (double shx, double shy) — преобразует каждую точку (x, y) в точку (x + shx * y, shy * x + y).

□ getTranslateInstance (double tx, double ty) — сдвигает каждую точку (x, y) в точку (x + tx, y + ty).

Метод createInverse () возвращает преобразование, обратное текущему преобразованию.

После создания преобразования его можно изменить методами:

setTransform(AffineTransform at);

setTransform(double a, double b, double c, double d, double e, double f); setToIdentity(); setToRotation(double angle);

setToRotation(double angle, double x, double y); setToRotation(double vx, double vy);

setToRotation(double vx, double vy, double x, double y); setToQuadrantRotation(int n);

setToQuadrantRotation(int n, double x, double y); setToScale(double sx, double sy); setToShare(double shx, double shy); setToTranslate(double tx, double ty);

сделав текущим преобразование, заданное одним из этих методов.

Преобразования, заданные методами:

concatenate(AffineTransform at); rotate(double angle);

rotate(double angle, double x, double y); rotate(double vx, double vy);

rotate(double vx, double vy, double x, double y); quadrantRotate(int n);

quadrantRotate(int n, double x, double y); scale(double sx, double sy); shear(double shx, double shy); translate(double tx, double ty);

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

Преобразование, заданное методом preConcatenate(AffineTransform at), напротив, осуществляется после текущего преобразования.

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

Пора привести пример. Добавим в начало метода paint (), показанного в листинге 9.2, строку импорта

import java.awt.geom.*;

и четыре оператора, как записано в листинге 9.3.

Листинг 9.3. Преобразование пространства пользователя

// Начало листинга 9.2... public void paint(Graphics gr){

Graphics2D g = (Graphics2D)gr;

AffineTransform at =

AffineTransform.getRotateInstance(-Math.PI/4.0, 250.0, 150.0); at.concatenate(

new AffineTransform(0.5, 0.0, 0.0, 0.5, 100.0, 60.0)); g.setTransform(at);

Dimension d = getSize();

// Продолжение листинга 9.2

Метод paint () начинается с получения экземпляра g класса Graphics2D простым приведением аргумента gr к типу GraphicsiD. Затем. методом getRotateInstance () определяется поворот на 45° против часовой стрелки вокруг точки (250.0, 150.0). Это преобразование — экземпляр at класса AffineTransform. Метод concatenate (), выполняемый объектом at, добавляет к этому преобразованию сжатие в два раза по обеим осям координат и перенос начала координат в точку (100.0, 60.0). Наконец, композиция этих преобразований устанавливается как текущее преобразование объекта g методом setTrans form ().

Преобразование выполняется в следующем порядке. Сначала пространство пользователя сжимается в два раза вдоль обеих осей, затем начало координат пользователя — левый верхний угол — переносится в точку (100.0, 60.0) пространства графического устройства. Потом картинка поворачивается на угол 45° против часовой стрелки вокруг точки (250.0, 150.0).

Результат этих преобразований показан на рис. 9.3.

Рис. 9.3. Преобразование координат

Упражнение

4. Напишите полную программу по листингу 9.3 и выполните ее несколько раз, меняя коэффициенты преобразований.

Рисование фигур средствами Java 2D

Характеристики пера для рисования фигур описаны в интерфейсе Stroke. В Java 2D есть пока только один класс, реализующий этот интерфейс, класс BasicStroke.

Класс BasicStroke

Конструкторы класса BasicStroke определяют характеристики пера. Основной конструктор,

BasicStroke(float width, int cap, int join, float miter, float[] dash, float dashBegin);

задает:

□ толщину пера width в пикселах;

□ оформление конца линии cap; это одна из констант:

• cap_round — закругленный конец линии;

• cap_square — квадратный конец линии;

• cap_butt — оформление отсутствует;

□ способ сопряжения линий j oin; это одна из констант:

• join_round — линии сопрягаются дугой окружности;

• join_bevel — линии сопрягаются отрезком прямой, перпендикулярным биссектрисе угла между линиями;

• join_miter — линии просто стыкуются;

□ расстояние между линиями miter, начиная с которого применяется сопряжение

join_miter;

□ длину штрихов и промежутков между штрихами — массив dash; элементы массива с четными индексами задают длину штриха в пикселах, элементы с нечетными индексами — длину промежутка; массив перебирается циклически;

□ индекс dashBegin, начиная с которого перебираются элементы массива dash. Остальные конструкторы задают некоторые характеристики по умолчанию:

□ BasicStroke(float width, int cap, int join, float miter) — сплошная линия;

□ BasicStroke (float width, int cap, int join) — сплошная линия с сопряжением JOIN_ROUND или JOIN_BEVEL; для сопряжения JOIN_MITER задается значение miter = 10. of;

□ BasicStroke(float width) — прямой обрез cap_square и сопряжение join_miter со значением miter = 10.0f;

□ BasicStroke () — ширина 1. of.

Лучше один раз увидеть, чем сто раз прочитать. В листинге 9.4 определены пять перьев с разными характеристиками, рис. 9.4 показывает, как они рисуют.

Листинг 9.4. Определение перьев

import j ava.awt.*; import java.awt.geom.*; import j avax.swing.*;

class StrokeTest extends JFrame{

StrokeTest(String s){ super(s);

setSize(500, 400); setVisible(true);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE);

}

public void paint(Graphics gr){

Graphics2D g = (Graphics2D)gr; g.setFont(new Font("Serif", Font.PLAIN, 15));

BasicStroke pen1 = new BasicStroke(20, BasicStroke.CAP BUTT, BasicStroke.JOIN_MITER,30);

BasicStroke pen2 = new BasicStroke(20, BasicStroke.CAP ROUND, BasicStroke.JOIN_ROUND);

BasicStroke pen3 = new BasicStroke(20, BasicStroke.CAP SQUARE, BasicStroke.JOIN_BEVEL); float[] dash1 = {5, 20};

BasicStroke pen4 = new BasicStroke(10, BasicStroke.CAP ROUND, BasicStroke.JOIN_BEVEL, 10, dash1, 0);

float[] dash2 = {10, 5, 5, 5};

g. g. g. g. g. g. g. g. g. g. g. g. g. g. g. g. g. g. g. g. g.

}

public static void main(String[] args){ new StrokeTest(" Различные перья");

}

BasicStroke pen5 = new BasicStroke(10, BasicStroke.CAP BUTT, BasicStroke.JOIN_BEVEL, 10, dash2, 0); setStroke(pen1);

draw(new Rectangle2D.Double(50, 50, 50, 50)); draw(new Line2D.Double(50, 180, 150, 180)); setStroke(pen2);

draw(new Rectangle2D.Double(200, 50, 50, 50)); draw(new Line2D.Double(50, 230, 150, 230)); setStroke(pen3);

draw(new Rectangle2D.Double(350, 50, 50, 50)); draw(new Line2D.Double(50, 280, 150, 280)); drawString("JOIN_MITER", 40, 130);

drawStringC'JOIN ROUND" drawString("JOIN BEVEL" drawString("CAP_BUTT", drawString("CAP ROUND", drawString("CAP_SQUARE" setStroke(pen5); draw(new Line2D.Double( setStroke(pen4); draw(new Line2D.Double( drawString("{10, 5, 5, 5,...}

180, 130);

330, 130);

170, 190);

170, 240);

170, 290);

Рис. 9.4. Перья с различными характеристиками
330, 250, 330) )
360, 250, 360) )
..}", 260, 335);
260, 365) ;

После создания пера ОДНИМ ИЗ конструкторов И установки пера методом setStroke ()

можно рисовать различные фигуры методами draw() и fill ().

Общие свойства фигур, которые можно нарисовать методом draw() класса Graphics2D, описаны в интерфейсе Shape. Данный интерфейс реализован для создания обычного набора фигур- прямоугольников, прямых, эллипсов, дуг, точек- классами Rectangle2D,

RoundRectangle2D, Line2D, Ellipse2D, Arc2D, Point2D пакета java.awt.geom. В этом пакете есть еще классы CubicCurve2D и QuadCurve2D для создания кривых третьего и второго порядка.

Все эти классы абстрактные, но существуют их реализации — вложенные классы Double и Float для задания координат числами соответствующего типа. В листинге 9.4 использованы классы Rectangle2D.Double и Line2d.Double для вычерчивания прямоугольников и отрезков.

Класс GeneralPath

В пакете java.awt.geom есть еще один интересный класс — GeneralPath. Объекты этого класса могут содержать сложные конструкции, составленные из отрезков прямых или кривых линий и прочих фигур, соединенных или не соединенных между собой. Более того, поскольку этот класс реализует интерфейс Shape, его экземпляры сами являются фигурами и могут быть элементами других объектов класса GeneralPath.

Объект класса GeneralPath строится так. Вначале создается пустой объект класса GeneralPath конструктором по умолчанию GeneralPath() или объект, содержащий одну фигуру, конструктором GeneralPath (Shape sh).

Затем к этому объекту добавляются фигуры методом

append(Shape sh, boolean connect);

Если параметр connect равен true, то новая фигура соединяется с предыдущими фигурами с помощью текущего пера.

В объекте есть текущая точка. Вначале ее координаты (0, 0), затем ее можно переместить в точку (x, у) методом moveTo(float x, float y).

От текущей точки к точке (x, у) можно провести:

□ отрезок прямой методом lineTo(float x, float у);

□ отрезок квадратичной кривой методом quadTo(float x1, float у1, float x, float у);

□ кривую Безье методом curveTo(float x1, float у1, float x2, float у2, float x, float у).

Текущей точкой после этого становится точка (x, у) . Начальную и конечную точки можно соединить методом closePath (). Вот как можно создать треугольник с заданными вершинами:

GeneralPath p = new GeneralPath();

p.moveTo(x1, y1); // Переносим текущую точку в первую вершину,

p.lineTo(x2, y2); // проводим сторону треугольника до второй вершины,

p.lineTo(x3, y3); // проводим вторую сторону,

p.closePath(); // проводим третью сторону до первой вершины.

Способы заполнения фигур определены в интерфейсе Paint. В настоящее время Java 2D

содержит несколько реализаций этого интерфейса- классы Color, GradientPaint,

TexturePaint, абстрактный класс MultipleGradientPaint и его расширения LinearGradientPaint и RadialGradientPaint. Класс Color нам известен, посмотрим, какие способы заливки предлагают классы GradientPaint и TexturePaint.

Классы GradientPaint и TexturePaint

Класс GradientPaint предлагает сделать заливку следующим образом.

В двух точках, м и N, устанавливаются разные цвета. В точке M(x1, у1) задается цвет c1, в точке N(x2, у2) — цвет c2. Цвет заливки гладко меняется от c1 к c2 вдоль прямой, соединяющей точки М и N, оставаясь постоянным вдоль каждой прямой, перпендикулярной прямой mn. Такую заливку создает конструктор

GradientPaint(float x1, float Сѓ1, Color c1, float x2, float Сѓ2, Color c2);

При этом вне отрезка mn цвет остается постоянным: за точкой м — цвет c1, за точкой N - цвет c2.

Второй конструктор,

GradientPaint(float x1, float Сѓ1, Color c1,

float x2, float Сѓ2, Color c2, boolean cyclic);

при задании параметра cyclic == true повторяет заливку полосы mn во всей заливаемой фигуре.

Еще два конструктора задают точки как объекты класса Point2D.

Класс TexturePaint поступает сложнее. Сначала создается буфер — объект класса BufferedImage из пакета java.awt.image. Это большой сложный класс. Мы с ним еще встретимся в главе 20, а пока нам понадобится только его графический контекст, управляемый экземпляром класса Graphics2D. Этот экземпляр можно получить методом createGraphics () класса BufferedImage. Графический контекст буфера заполняется фигурой, которая будет служить образцом заполнения.

Затем по буферу создается объект класса TexturePaint. При этом еще задается прямоугольник, размеры которого являются размерами образца заполнения. Конструктор выглядит так:

TexturePaint(BufferedImage buffer, Rectangle2D anchor);

После создания заливки — объекта класса Color, GradientPaint или TexturePaint — она устанавливается в графическом контексте методом setPaint(Paint p) и используется в дальнейшем методом fill (Shape sh).

Все это демонстрируют листинг 9.5 и рис. 9.5.

Листинг 9.5. Способы заливки

import java.awt.*; import java.awt.geom.*;

import java.awt.image.*; import javax.swing.*;

class PaintTest extends JFrame{

PaintTest(String s){ super(s);

setSize(300, 300); setVisible(true);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE);

}

public void paint(Graphics gr){

Graphics2D g = (Graphics2D)gr;

BufferedImage bi =

new BufferedImage(20, 20, BufferedImage.TYPE INT RGB); Graphics2D big = bi.createGraphics(); big.draw(new Line2D.Double(0.0, 0.0, 10.0, 10.0)); big.draw(new Line2D.Double(0.0, 10.0, 10.0, 0.0));

TexturePaint tp = new TexturePaint(bi,

new Rectangle2D.Double(0.0, 0.0, 10.0, 10.0)); g.setPaint(tp);

g.fill(new Rectangle2D.Double(50,50, 200, 200)); GradientPaint gp =

new GradientPaint(100, 100, Color.white,

150, 150, Color.black, true);

g.setPaint(gp);

g.fill(new Ellipse2D.Double(100, 100, 200, 200));

}

public static void main(String[] args){ new PaintTest(" Способы заливки");

}

}

Рис. 9.5. Способы заливки

Классы LinearGradientPaint и RadialGradientPaint

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

MultipleGradientPaint.

Так же, как и в классе GradientPaint, задается промежуток mn, но теперь промежуток делится на несколько частей точками с вещественными координатами от 0.0 — начало промежутка — до 1.0 — конец промежутка. Эти точки заносятся в массив, например:

float[] dist = {0.0f, 0.2f, 1.0f};

Здесь промежуток разделен на две неравные части: одна пятая часть и четыре пятых части.

В каждой точке деления промежутка задается свой цвет. Цвета тоже записываются в массиве:

Color[] color = {Color.RED, Color.WHITE, Color.BLUE};

В каждой части промежутка цвет плавно меняется от одного цвета к другому. В нашем примере на одной пятой части промежутка идет переход от красного к белому цвету, а на оставшихся четырех пятых — переход от белого к синему цвету.

После этого для линейной заливки создается экземпляр класса LinearGradientPaint:

LinearGradientPaint(float x1, float Сѓ1, float x2, float Сѓ2, float[] dist, Color[] color);

а для радиальной заливки — экземпляр класса RadialGradientPaint.

RadialGradientPaint(float x, float Сѓ, float radius, float[] dist, Color[] color);

Как вы поняли, для линейной заливки задаются координаты начальной M(x1, у1) и конечной N(x2, у2) точки промежутка, а для радиальной заливки — координаты центра круга A(x, у) и радиус окружности radius.

Способы задания цветов вне промежутка mn заданы константами вложенного класса MultipleGradientPaint. cycleMethod. В шестой версии Java SE есть три способа:

□ no_cycle — используются первый и последний цвет (по умолчанию);

□ reflect — перед промежутком циклически перебираются цвета от первого до последнего, а после промежутка — от последнего до первого;

□ REFLECT — циклически перебираются цвета от первого до последнего.

Эти константы указываются в следующих конструкторах:

LinearGradientPaint(float x1, float Сѓ1, float x2, float Сѓ2, float[] dist, Color[] color,

MultipleGradientPaint.CycleMethod method);

RadialGradientPaint(float x, float Сѓ, float radius, float[] dist, Color[] color,

MultipleGradientPaint.CycleMethod method);

Еще несколько конструкторов задают точки как объекты класса Point2D.

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

После создания заливки — объекта класса LinearGradientPaint или RadialGradientPaint — она устанавливается в графическом контексте методом setPaint (Paint p) и используется в дальнейшем методом fill (Shape sh).

Вывод текста средствами Java 2D

Шрифт — объект класса Font — кроме имени, стиля и размера имеет еще полтора десятка атрибутов: подчеркивание, перечеркивание, наклон, цвет шрифта и цвет фона, ширину и толщину символов, аффинное преобразование, расположение слева направо или справа налево.

Атрибуты шрифта задаются как статические константы класса TextAttribute. Наиболее используемые атрибуты перечислены в табл. 9.1.

Таблица 9.1. Атрибуты шрифта
Атрибут Значение
BACKGROUND Цвет фона. Объект, реализующий интерфейс Paint
FOREGROUND Цвет текста. Объект, реализующий интерфейс Paint
BIDI EMBEDDED Уровень вложенности просмотра текста. Целое от 1 до 15
CHAR REPLACEMENT Фигура, заменяющая символ. Объект GraphicAttribute
FAMILY Семейство шрифта. Строка типа String
FONT Шрифт. Объект класса Font
JUSTIFICATION Допуск при выравнивании абзаца. Объект класса Float со значениями от 0.0 до 1.0. Есть две константы: justification full и JUSTIFICATION NONE
KERLING Керлинг — сдвиг букв в слове с целью уменьшения промежутков между ними, например в слове "AWAY". Константа KERLING ON
LIGATURES Лигатура — слияние букв, например в слове "float". Константа LIGATURES ON
POSTURE Наклон шрифта. Объект класса Float . Есть две константы: POSTURE OBLIQUE и POSTURE REGULAR
RUN DIRECTION Просмотр текста: RUN DIRECTION LTR — слева направо, RUN DIRECTION RTL — справа налево
SIZE Размер шрифта в пунктах. Объект класса Float
STRIKETHROUGH Перечеркивание шрифта. Задается константой strikethrough ON , по умолчанию перечеркивания нет
SUPERSCRIPT Подстрочные или надстрочные индексы. Константы: SUPERSCRIPT NONE , SUPERSCRIPT SUB , SUPERSCRIPT SUPER
SWAP COLORS Замена местами цвета текста и цвета фона. Константа SWAP COLORS ON , по умолчанию замены нет
Таблица 9.1 (окончание)
Атрибут Значение
TRAKING Трекинг — пропорциональное изменение расстояний между буквами. Константа TRAKING TIGHT увеличивает расстояния, а константа traking loose уменьшает их
TRANSFORM Преобразование шрифта. Объект класса AffineTransform
UNDERLINE Подчеркивание шрифта. Константы: UNDERLINE ON , UNDERLINE LOW DASHED , UNDERLINE LOW DOTTED , UNDERLINE LOW GRAY , UNDERLINE LOW ONE PIXEL , UNDERLINE LOW TWO PIXEL
WEIGHT Толщина шрифта. Константы: WEIGHT ULTRA LIGHT , WEIGHT EXTRA LIGHT , WEIGHT LIGHT , WEIGHT DEMILIGHT , WEIGHT REGULAR , WEIGHT SEMIBOLD , WEIGHT MEDIUM , WEIGHT DEMIBOLD , WEIGHT BOLD , WEIGHT HEAVY , WEIGHT EXTRABOLD , WEIGHT__ULTRABOLD
WIDTH Ширина шрифта. Константы: width condensed, width semi condensed, WIDTH REGULAR , WIDTH SEMI EXTENDED , WIDTH EXTENDED
К сожалению, не все шрифты позволяют задать все атрибуты. Посмотреть список допустимых атрибутов для данного шрифта можно методом getAvailableAttributes ( ) класса Font. Например:Font f = new Font("Times New Roman", Font.BOLD, 12);AttributedCharacterIterator.Attribute[] a = f.getAvailableAttributes(); for (int i = 0; i < a.length; i++)System.out.println(a[i]);

В классе Font есть конструктор Font (Map attributes), которым можно сразу задать нужные атрибуты создаваемому шрифту. Это требует предварительной записи атрибутов в специально созданный для этой цели объект класса, реализующего интерфейс Map: класса HashMap, WeakHashMap или Hashtable (см. главу 7). Например:

HashMap hm = new HashMap();

hm.put(TextAttribute.SIZE, new Float(60.0f));

hm.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);

Font f = new Font(hm);

Можно создать шрифт и вторым конструктором, которым мы пользовались в листинге 9.2, а потом добавлять и изменять атрибуты методами deriveFont ( ) класса Font.

Текст в Java 2D обладает собственным контекстом- объектом класса FontRenderContext,

хранящим всю информацию, необходимую для вывода текста. Получить его можно методом getFontRenderContext () класса Graphics2D.

Вся информация о тексте, в том числе и о его контексте, собирается в объекте класса TextLayout. Этот класс в Java 2D заменяет класс FontMetrics.

В конструкторе класса TextLa^out задается текст, шрифт и контекст. Начало метода paint () со всеми этими определениями может выглядеть так:

public void paint(Graphics gr){

Graphics2D g = (Graphics2D)gr;

FontRenderContext frc = g.getFontRenderContext();

Font f = new Font("Serif", Font.BOLD, 15);

String s = "Какой-то текст";

TextLayout tl = new TextLayout(s, f, frc);

// Продолжение метода

}

В классе TextLayout есть не только более двадцати методов getXXX(), позволяющих узнать различные сведения о тексте, его шрифте и контексте, но и метод

draw(Graphics2D g, float x, float y);

вычерчивающий содержимое объекта класса TextLayout в графической области g, начиная с точки (x, y) .

Еще один интересный метод,

getOutline(AffineTransform at);

возвращает контур шрифта в виде объекта Shape. Этот контур можно затем заполнить по какому-нибудь образцу или вывести только контур, как показано в листинге 9.6.

Листинг 9.6. Вывод текста средствами Java 2D

import java.awt.*; import java.awt.font.*; import java.awt.geom.*; import javax.swing.*;

class StillText extends JFrame{

StillText(String s){ super(s);

setSize(400, 200); setVisible(true);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE);

}

public void paint(Graphics gr){

Graphics2D g = (Graphics2D)gr;

int w = getSize().width, h = getSize().height;

FontRenderContext frc = g.getFontRenderContext();

String s = "Тень";

Font f = new Font("Serif", Font.BOLD, h/3);

TextLayout tl = new TextLayout(s, f, frc);

AffineTransform at = new AffineTransform(); at.setToTranslation(w/2-tl.getBounds().getWidth()/2, h/2);

Shape sh = tl.getOutline(at); g.draw(sh);

AffineTransform atsh = new AffineTransform(1, 0.0, 1.5, -1, 0.0, 0.0); g.transform(at); g.transform(atsh);

Font df = f.deriveFont(atsh);

TextLayout dtl = new TextLayout(s, df, frc);

Shape sh2 = dtl.getOutline(atsh); g.fill(sh2);

}

public static void main(String[] args){ new StillText(" Эффект тени");

}

}

На рис. 9.6 показан вывод этой программы.

Рис. 9.6. Вывод текста средствами Java 2D

Еще одна возможность создать текст с атрибутами — определить объект класса Attributedstring из пакета java.text. Конструктор этого класса

AttributedString(String text, Map attributes);

задает сразу и текст, и его атрибуты. Затем можно добавить или изменить характеристики текста одним их трех методов addAttibute ( ).

Если текст занимает несколько строк, то встает вопрос его форматирования. Для этого вместо класса TextLayout применяется класс LineBreakMeasurer, методы которого позволяют отформатировать абзац. Для каждого сегмента текста можно получить экземпляр класса TextLayout и вывести текст, используя его атрибуты.

Для редактирования текста необходимо отслеживать курсором (caret) текущую позицию в тексте. Это осуществляется методами класса TextHitinfo, а методы класса TextLayout позволяют получить позицию курсора, выделить блок текста и подсветить его.

Наконец, можно задать отдельные правила для вывода каждого символа текста. Для этого надо получить экземпляр класса GlyphVector методом createGlyphVector ( ) класса Font, изменить позицию символа методом setGlyphPosition (), задать преобразование символа, если это допустимо для данного шрифта, методом setGlyphTransform(), и вывести измененный текст методом drawGlyphVector() класса Graphics2D. Все это показано в листинге 9.7 и на рис. 9.7 — выводе программы листинга 9.7.

Листинг 9.7. Вывод отдельных символов

import java.awt.*; import java.awt.font.*;

import java.awt.geom.*; import javax.swing.*;

class GlyphTest extends JFrame{

GlyphTest(String s){ super(s);

setSize(400, 150); setVisible(true);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE);

}

public void paint(Graphics gr){ int h = 5;

Graphics2D g = (Graphics2D)gr;

FontRenderContext frc = g.getFontRenderContext();

Font f = new Font("Serif", Font.BOLD, 30);

GlyphVector gv = f.createGlyphVector(frc, "Пляшущий текст"); int len = gv.getNumGlyphs(); for (int i = 0; i < len; i++){

Point2D.Double p = new Point2D.Double(25 * i, h = -h); gv.setGlyphPosition(i, p);

}

g.drawGlyphVector(gv, 10, 100);

}

public static void main(String[] args){

new GlyphTest(" Вывод отдельных символов");

}

}

Рис. 9.7. Вывод отдельных символов

Методы улучшения визуализации

Визуализацию (rendering) созданной графики можно усовершенствовать, установив метод улучшения (hint) одним из методов класса Graphics2D:

setRenderingHints(RenderingHints.Key key, Object value); setRenderingHints(Map hints);

Ключи — методы улучшения — и их значения задаются константами класса RenderingHints, некоторые из них перечислены в табл. 9.2.

Таблица 9.2. Методы визуализации и их значения
Метод Значение
KEY ANTIALIASING Размывание крайних пикселов линий для гладкости изображения; три значения, задаваемые константами: VALUE ANTIALIAS DEFAULT , VALUE ANTIALIAS ON , VALUE ANTIALIAS OFF
KEY TEXT ANTIALIASING То же для текста. Константы: VALUE TEXT ANTIALIASING DEFAULT , VALUE TEXT ANTIALIASING ON , VALUE TEXT ANTIALIASING OFF . Для LCD-мониторов константы: value text antialias gasp,VALUE TEXT ANTIALIAS LCD HRGB , VALUE TEXT ANTIALIAS LCD HBGR , VALUE TEXT ANTIALIAS LCD VRGB , VALUE TEXT ANTIALIAS LCD VBGR
KEY RENDERING Три типа визуализации. Константы: VALUE RENDER SPEED , VALUE RENDER QUALITY , VALUE RENDER DEFAULT
KEY COLOR RENDERING То же для цвета. Константы: VALUE COLOR RENDER SPEED , VALUE COLOR RENDER QUALITY , VALUE COLOR RENDER DEFAULT
KEY ALPHA INTERPOLATION Плавное сопряжение линий. Константы: VALUE ALPHA INTERPOLATION SPEED , VALUE ALPHA INTERPOLATION QUALITY , VALUE ALPHA INTERPOLATION DEFAULT
KEY INTERPOLATION Способы сопряжения. Константы: VALUE INTERPOLATION BILINEAR , VALUE INTERPOLATION BICUBIC , VALUE INTERPOLATION NEAREST NEIGHBOR
KEY DITHERING Замена близких цветов. Константы: VALUE DITHER ENABLE , VALUE DITHER DISABLE , VALUE DITHER DEFAULT
KEY ALPHA INTERPOLATION Способ альфа-интерполяции. Константы: VALUE ALPHA INTERPOLATION DEFAULT , VALUE ALPHA INTERPOLATION QUALITY , VALUE ALPHA INTERPOLATION SPEED
KEY STROKE CONTROL Способ рисования. Константы: VALUE STROKE DEFAULT , VALUE STROKE NORMALIZE , VALUE STROKE PURE

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

Вот как может выглядеть начало метода paint () с указанием методов улучшения визуализации:

public void paint(Graphics gr){

Graphics2D g = (Graphics2D)gr;

g.setRenderingHint(RenderingHints.KEY ANTIALIASING,

RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY RENDERING,

RenderingHints.VALUE_RENDER_QUALITY);

// Продолжение метода

Для того чтобы как можно лучше связать визуализацию с настройками дисплея, определено свойство "awt.font.desktophints", хранящее таблицу типа Map с методами улучшения визуализации, имеющимися в настройках дисплея. Воспользоваться этим свойством можно так:

public void paint(Graphics gr){

Graphics2D g = (Graphics2D)gr;

Toolkit tk = Toolkit.getDefaultToolkit();

Map map = (Map)(tk.getDesktopProperty("awt.font.desktophints")); if (map != null) g.addRenderingHints(map);

// Продолжение метода

}

Упражнение

5. Перепишите предыдущие упражнения средствами Java 2D.

Заключение

В этой главе мы, разумеется, не смогли подробно разобрать все возможности Java 2D. Мы не коснулись моделей задания цвета и смешивания цветов, печати графики и текста, динамической загрузки шрифтов, изменения области рисования. В главе 20 будут рассмотрены средства Java 2D для работы с изображениями, в главе 23 — средства печати.

В документации Java SE, в каталоге docs/technotes/guides/2d/spec, есть руководство Programmer's Guide to the Java 2D API с обзором всех возможностей Java 2D. Там помещены ссылки на руководства и пособия по Java 2D. В каталоге demo/jfc/Java2D/ приведена демонстрационная программа и исходные тексты программ, использующих Java 2D.

Вопросы для самопроверки

1. Что такое цвет в библиотеке AWT?

2. Что такое шрифт в библиотеке AWT?

3. Что такое графический контекст?

4. Как нарисовать треугольник?

5. Как нарисовать окружность?

6. Как преобразовать чертеж: повернуть его, уменьшить или увеличить?

7. Можно ли писать текст сверху вниз или справа налево?

ГЛАВА 10

 

Основные компоненты AWT

Казалось, что компоненты AWT постепенно выходят из употребления. Они не могут удовлетворить возрастающие потребности программистов и пользователей и заменяются компонентами Swing, SWAT, SWT и других графических библиотек. Тем не менее апплеты и некоторые приложения еще часто приходится создавать на основе компонентов AWT, а они используются в сотовых телефонах и других мобильных устройствах.

Графическая библиотека AWT предлагает более двадцати готовых компонентов. Они показаны на рис. 8.2. Наиболее часто используются подклассы класса Component — классы Button, Canvas, Checkbox, Choice, Container, Label, List, Scrollbar, TextArea, TextField, Panel, ScrollPane, Window, Dialog, FileDialog, Frame.

Еще одна группа компонентов- это компоненты меню - классы MenuItem, MenuBar,

Menu, PopupMenu, CheckboxMenuItem.

Забегая вперед, мы для каждого компонента перечислим события, которые в нем происходят. Обработку событий разберем в главе 15.

Начнем изучать эти компоненты от простых компонентов к сложным и от наиболее часто используемых к применяемым реже. Но сначала посмотрим на то общее, что есть во всех этих компонентах, на сам класс Component.

 

Класс Component

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

BOTTOM_ALIGNMENT, CENTER_ALIGNMENT, LEFT_ALIGNMENT, RIGHT_ALIGNMENT, TOP_ALIGNMENT, и около сотни методов.

Большинство методов — это методы доступа getXXX(), is^XX(), set^XX(). Изучать их нет смысла, надо просто посмотреть, как они используются в подклассах.

Конструктор класса недоступен — он защищенный (protected), потому что класс Component абстрактный, он не может использоваться сам по себе, применяются только его подклассы.

Компонент всегда занимает прямоугольную область со сторонами, параллельными сторонам экрана, и в каждый момент времени имеет определенные размеры, измеряемые в пикселах. Размеры компонента можно узнать методом getSize (), возвращающим объект класса Dimension, или целочисленными методами getHeight ( ) и getWidth(), возвращающими высоту и ширину прямоугольника. Новый размер компонента можно установить из программы методами setSize(Dimension d) или setSize(int width, int height), если это допускает менеджер размещения контейнера, содержащего компонент.

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

Dimension, а установить — методом setPreferredSize(Dimension) .

Компонент обладает минимальным и максимальным размерами. Их возвращают методы

getMinimumSize ( ) и getMaximumSize() в виде объекта Dimension. Установить эти размеры можно методами setMinimumSize (Dimension) и setMaximumSize (Dimension).

В компоненте есть система координат. Ее начало — точка с координатами (0, 0) — находится в левом верхнем углу компонента, ось Ox идет вправо, ось Oy — вниз, координатные точки расположены между пикселами.

В компоненте хранятся координаты его левого верхнего угла в системе координат объемлющего контейнера. Их можно узнать методами getLocation(), а изменить — методами setLocation (), переместив компонент в контейнере, если это позволит менеджер размещения компонентов.

Можно выяснить сразу и положение, и размер прямоугольной области компонента методом getBounds (), возвращающим объект класса Rectangle, и изменить разом и положение, и размер компонента методами setBounds(), если это позволит сделать менеджер размещения.

Компонент может быть недоступен для действий пользователя, тогда он выделяется на экране обычно светло-серым цветом. Доступность компонента можно проверить логическим методом isEnabled (), а изменить — методом setEnabled (boolean enable).

Для многих компонентов определяется графический контекст — объект класса Graphics, — который управляется методом paint (), описанным в предыдущей главе, и который можно получить методом getGraphics ( ).

В контексте есть текущий цвет и цвет фона — объекты класса Color. Цвет фона можно получить методом getBackground(), а изменить — методом setBackground(Color color). Текущий цвет можно получить методом getForeground(), а изменить- методом

setForeground(Color color) .

В контексте есть шрифт — объект класса Font, возвращаемый методом getFont() и изменяемый методом setFont ( Font font).

В компоненте определяется локаль — объект класса Locale. Его можно получить методом getLocale ( ), изменить-методом setLocale (Locale locale ).

В компоненте существует курсор, показывающий положение мыши, — объект класса Cursor. Его можно получить методом getCursor (). Изменяется форма курсора в "тяжелых" компонентах с помощью метода setCursor(Cursor cursor).

Положение курсора мыши над компонентом можно отследить методом getMousePosition ( ), возвращающим координаты курсора в виде объекта класса Point.

Остановимся подробнее на классе Cursor.

Класс Cursor

Основа класса — статические константы, определяющие форму курсора:

□ crosshair_cursor — курсор в виде креста, появляется обычно при поиске позиции для размещения какого-то элемента;

□ default_cursor — обычная форма курсора — стрелка влево вверх;

□ hand_cursor — "указующий перст", появляется обычно при выборе какого-то элемента списка;

□ move_cursor — крест со стрелками, возникает обычно при перемещении элемента;

□ text_cursor — вертикальная черта, появляется в текстовых полях;

□ wait_cursor — изображение часов, появляется при ожидании.

Следующие курсоры появляются обычно при приближении к краю или углу компонента:

□ e_resize_cursor — стрелка вправо с упором;

□ n_resize_cursor — стрелка вверх с упором;

□ ne_resize_cursor — стрелка вправо вверх, упирающаяся в угол;

□ nw_resize_cursor — стрелка влево вверх, упирающаяся в угол;

□ s_resize_cursor — стрелка вниз с упором;

□ se_resize_cursor — стрелка вправо вниз, упирающаяся в угол;

□ sw_resize_cursor — стрелка влево вниз, упирающаяся в угол;

□ w_resize_cursor — стрелка влево с упором.

Перечисленные константы используются для задания аргумента type в конструкторе класса Cursor(int type).

Вместо конструктора можно обратиться к статическому методу getPredefinedCursor(int type), создающему объект класса Cursor и возвращающему ссылку на него.

Получить курсор по умолчанию можно статическим методом getDefaultCursor(). Затем созданный курсор надо установить в компонент. Например, после выполнения:

Cursor curs = new Cursor(Cursor.WAIT CURSOR); someComp.setCursor(curs);

при появлении указателя мыши в компоненте someComp указатель примет вид часов.

Как создать свой курсор

Кроме перечисленных предопределенных курсоров можно задать собственную форму курсора. Ее тип носит название custom_cursor. Сформировать свой курсор можно методом

createCustomCursor(Image cursor, Point hotspot, String name);

создающим объект класса Cursor и возвращающим ссылку на него. Перед этим следует создать изображение курсора cursor — объект класса Image. Как это сделать, рассказывается в главе 20. Аргумент name задает имя курсора, можно написать просто null. Аргумент hotspot определяет точку фокуса курсора. Эта точка должна быть в пределах изображения курсора, точнее, в пределах, показываемых методом

getBestCursorSize(int desiredWidth, int desiredHeight);

возвращающим ссылку на объект класса Dimension. Аргументы метода означают желаемый размер курсора. Если графическая система не допускает создание курсоров, возвращается (0, 0). Данный метод показывает приблизительно размер того курсора, который создаст графическая система, например (32, 32). Изображение cursor будет подогнано под этот размер, при этом возможны искажения.

Третий метод — getMaximumCursorColors ( ) — возвращает наибольшее количество цветов, например 256, которое можно использовать в изображении курсора.

Это методы класса java.awt.Toolkit, с которым мы еще не работали. Класс Toolkit содержит некоторые методы, связывающие приложение Java со средствами платформы, на которой выполняется приложение. Поэтому нельзя создать экземпляр класса Toolkit конструктором, для его получения следует выполнить статический метод

Toolkit.getDefaultToolkit().

Если приложение работает в окне Window или его расширениях, например Frame, то можно получить экземпляр Toolkit методом getToolkit ( ) класса Window.

Соберем все это вместе:

Toolkit tk = Toolkit.getDefaultToolkit();

int colorMax = tk.getMaximumCursorColors(); // Наибольшее число цветов.

Dimension d = tk.getBestCursorSize(50, 50); // d — размер изображения.

int w = d.width, h = d.height, k = 0;

Point p = new Point(0, 0); // Фокус курсора будет

// в его верхнем левом углу. int[] pix = new int[w * h]; // Здесь будут пикселы изображения.

for(int i = 0; i < w; i++) for(int j = 0; j < h; j++)

if (j < i) pix[k++] = 0xFFFF0000; // Левый нижний угол — красный.

else pix[k++] = 0; // Правый верхний угол — прозрачный.

// Создается прямоугольное изображение размером (w, h),

// заполненное массивом пикселов pix, с длиной строки w.

Image im = createImage(new MemoryImageSource(w, h, pix, 0, w));

Cursor curs = tk.createCustomCursor(im, p, null); someComp.setCursor(curs);

В этом примере создается курсор в виде красного прямоугольного треугольника с катетами размером 32 пиксела и устанавливается в некотором компоненте someComp.

Упражнение

1. Создайте курсор в форме правильного треугольника.

События

Событие ComponentEvent происходит при перемещении компонента, изменении его размера, удалении с экрана и появлении на экране.

Событие FocusEvent возникает при получении или потере фокуса.

Событие KeyEvent проявляется при каждом нажатии и отпускании клавиши, если компонент имеет фокус ввода.

Событие MouseEvent происходит при манипуляциях мыши на компоненте.

 

Класс Container

Каждый компонент перед выводом на экран помещается в контейнер — подкласс класса Container.

Класс Container — прямой подкласс класса Component, и наследует все его методы. Кроме них основу класса составляют методы добавления компонентов в контейнер:

□ add(Component comp) — компонент comp добавляется в конец контейнера;

□ add(Component comp, int index) — компонент comp добавляется в позицию index в контейнере, если index == -1, то компонент добавляется в конец контейнера;

□ add(Component comp, Object constraints) — менеджеру размещения контейнера даются указания объектом constraints;

□ add(String name, Component comp) — компонент получает имя name.

Два метода удаляют компоненты из контейнера:

□ remove(Component comp) удаляет компонент с именем comp;

□ remove(int index) — удаляет компонент с индексом index в контейнере.

Один из компонентов в контейнере получает фокус ввода (input focus), на него направляется ввод с клавиатуры. Фокус можно переносить с одного компонента на другой и обратно клавишами и +. Компонент способен запросить фокус методом requestFocus () и передать фокус следующему компоненту методом transferFocus (). Компонент может проверить, имеет ли он фокус, своим логическим методом hasFocus ( ). Это методы класса Component.

Для облегчения размещения компонентов в контейнере определяется менеджер размещения (layout manager)- объект, реализующий интерфейс LayoutManager или его

подынтерфейс LayoutManager2. Каждый менеджер размещает компоненты в каком-то своем порядке: один менеджер расставляет компоненты в таблицу, другой норовит растащить компоненты по сторонам, третий просто располагает их один за другим, как слова в тексте. Менеджер определяет смысл слов "добавить в конец контейнера" и "добавить в позицию index".

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

setLayout(LayoutManager manager);

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

setLayout(null).

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при добавлении и удалении компонентов в контейнере происходит событие ContainerEvent.

Текстовая метка Label

Перейдем к рассмотрению конкретных компонентов. Самый простой компонент описывает класс Label.

Компонент Label — это строка текста, оформленная как графический компонент для размещения в контейнере. Текст можно поменять только методом доступа setText (String text), но не вводом пользователя с клавиатуры или с помощью мыши.

Создается объект этого класса одним из трех конструкторов:

□ Label () — пустой объект без текста;

□ Label (String text) — объект с текстом text, который прижимается к левому краю компонента;

□ Label (String text, int alignment) — объект с текстом text и определенным размещением в компоненте текста, задаваемого одной из трех констант: center, left, right.

Размещение можно изменить методом доступа setAlignment(int alignment).

Остальные методы, кроме методов, унаследованных от класса Component, позволяют получить текст getText () и размещение getAlignment (), а также установить текст методами

setText(String) и размещение setAlignment(int).

События

В классе Label происходят события класса Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent.

РљРЅРѕРїРєР° Button

Немногим сложнее класс Button.

Компонент Button — это кнопка стандартного для данной графической системы вида с надписью, умеющая реагировать на щелчок кнопки мыши — при нажатии она "вдавливается" в плоскость контейнера, при отпускании — становится "выпуклой".

Два конструктора, Button ( ) и Button (String label), создают кнопку без надписи и с надписью label соответственно.

Методы доступа getLabel() и setLabel(String label) позволяют получить и изменить надпись на кнопке.

Главная функция кнопки — реагировать на щелчки мыши, и прочие методы класса обрабатывают эти действия. Мы рассмотрим их в главе 15.

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при воздействии на кнопку происходит событие ActionEvent.

Кнопка выбора Checkbox

Немного сложнее класса Button класс Checkbox, создающий кнопки выбора.

Компонент Checkbox — это кнопка с двумя состояниями. Графически она выглядит как надпись справа от небольшого квадратика, в котором в некоторых графических системах появляется галочка после щелчка кнопкой мыши — компонент переходит в состояние (state) on. После следующего щелчка галочка пропадает — это состояние off. В других графических системах состояние on отмечается "вдавливанием" квадратика. В компоненте Checkbox состояния on/off отмечаются логическими значениями true/false соответственно.

Три конструктора, Checkbox(), Checkbox(String label), Checkbox(String label, boolean state), создают компонент без надписи, с надписью label в состоянии off и в заданном состоянии state соответственно.

Методы доступа getLabel(), setLabel(String label), getState(), setState(boolean state) возвращают и изменяют эти параметры компонента.

Компоненты Checkbox удобны для быстрого и наглядного выбора из списка, целиком расположенного на экране, как показано на рис. 10.1. Там же продемонстрирована ситуация, в которой нужно выбрать только один пункт из нескольких. В таких ситуациях образуется группа так называемых радиокнопок (radio buttons). Они помечаются обычно кружком или ромбиком, а не квадратиком, выбор обозначается жирной точкой в кружке или "вдавливанием" ромбика.

События

В классе Checkbox происходят события класса Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, а при изменении состояния кнопки возникает событие ItemEvent.

Класс CheckboxGroup

В библиотеке AWT радиокнопки не образуют отдельный компонент. Вместо этого несколько компонентов Checkbox объединяются в группу с помощью объекта класса

CheckboxGroup.

Класс CheckboxGroup очень мал, поскольку его задача — просто дать общее имя всем объектам Checkbox, образующим одну группу. В него входит один конструктор по умолчанию CheckboxGroup () и два метода доступа:

□ getSelectedCheckbox (), возвращающий выбранный объект Checkbox;

□ setSelectedCheckbox(Checkbox box), задающий выбор.

Как создать группу радиокнопок

Чтобы организовать группу радиокнопок, надо сначала сформировать объект класса CheckboxGroup, а затем создавать кнопки конструкторами

Checkbox(String label, CheckboxGroup group, boolean state);

Checkbox(String label, boolean state, CheckboxGroup group);

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

Только одна радиокнопка в группе может иметь состояние state == true.

Пора привести пример. В листинге 10.1 представлена программа, помещающая в контейнер Frame две метки Label сверху, под ними слева — три объекта Checkbox, справа — группу радиокнопок. Внизу — три кнопки Button. Результат выполнения программы показан на рис. 10.1.

Листинг 10.1. Размещение компонентов

import java.awt.*; import java.awt.event.*;

class SimpleComp extends Frame{

SimpleComp(String s){ super(s); setLayout(null);

Font f = new Font("Serif", Font.BOLD, 15); setFont(f);

Label l1 = new Label("Выберите товар:", Label.CENTER); l1.setBounds(10, 50, 120, 30); add(l1);

Label l2 = new Label("Выберите способ оплаты:"); l2.setBounds(160, 50, 200, 30); add(l2);

Checkbox ch1 = new CheckboxCR^в„ў"); ch1.setBounds(20, 90, 100, 30); add(ch1);

Checkbox ch2 = new Checkbox("Диски"); ch2.setBounds(20, 120, 100, 30); add(ch2);

Checkbox ch3 = new Checkbox("Игрушки"); ch3.setBounds(20, 150, 100, 30); add(ch3);

CheckboxGroup grp = new CheckboxGroup();

Checkbox chg1 = new Checkbox("Почтовым переводом", grp, true); chg1.setBounds(170, 90, 200, 30); add(chg1);

Checkbox chg2 = new Checkbox("Кредитной картой", grp, false); chg2.setBounds(170, 120, 200, 30); add(chg2);

Button b1 = new Button("Продолжить"); b1.setBounds( 30, 220, 100, 30); add(b1);

Button b2 = new Button("Отменить");

b2.setBounds(140, 220, 100, 30); add(b2);

Button b3 = new Button^^Mв„ў"); b3.setBounds(250, 220, 100, 30); add(b3);

setSize(400, 300); setVisible(true);

}

public static void main(String[] args){

Frame f = new SimpleComp(" Простые компоненты"); f.addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){ System.exit(0);

}

});

}

}

Рис. 10.1. Простые компоненты

Заметьте, что каждый создаваемый компонент следует заносить в контейнер, в данном случае Frame, методом add (). Левый верхний угол компонента помещается в точку контейнера с координатами, указанными первыми двумя аргументами метода setBounds (). Размер компонента задается последними двумя параметрами этого метода.

Раскрывающийся список Choice

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

Компонент Choice — это раскрывающийся список, один, выбранный, пункт (item) которого виден в поле, а другие появляются при щелчке кнопкой мыши на небольшой кнопке справа от поля компонента.

Вначале конструктором Choice () создается пустой список.

Затем методом add(string text) в список добавляются новые пункты с текстом text. Они располагаются в порядке написания методов add () и нумеруются от нуля.

Вставить новый пункт в нужное место можно методом insert(String text, int position). Выбор пункта можно произвести из программы методом select(String text) или

select(int position).

Удаление одного пункта из списка выполняется методом remove(String text) или

remove (int position), а всех пунктов сразу-методом removeAll().

Число пунктов в списке можно узнать методом getItemCount ( ).

Выяснить, какой пункт находится в позиции pos, можно методом getItem(int pos), возвращающим строку.

Наконец, определение выбранного пункта производится методом getSelectedIndex( ), возвращающим позицию этого пункта, или методом getSelectedItem(), возвращающим выделенную строку.

События

В классе Choice происходят события класса Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, а при выборе пункта возникает событие ItemEvent.

РЎРїРёСЃРѕРє List

Если надо показать на экране несколько пунктов списка, то создайте объект класса List.

Компонент List - это список с линейкой прокрутки, в котором можно выделить один или несколько пунктов. Количество видимых на экране пунктов определяется конструктором списка и размером компонента.

В классе три конструктора:

□ List() — создает пустой список с четырьмя видимыми пунктами;

□ List ( int rows ) -создает пустой список с rows видимыми пунктами;

□ List (int rows, boolean multiple) - создает пустой список, в котором можно отме

тить несколько пунктов, если multiple == true.

После создания объекта в список добавляются пункты с текстом item:

□ метод add (string item) — добавляет новый пункт в конец списка;

□ метод add ( String item, int position) -добавляет новый пункт в позицию position.

Позиции нумеруются по порядку, начиная с нуля.

Удалить пункт можно методами remove(String item), remove(int position), removeAll().

Метод replaceItem(String newItem, int pos)позволяет заменить текст пункта в позиции

pos.

Количество пунктов в списке возвращает метод getItemCount ( ).

Выделенный пункт можно получить методом getSelectedItem(), а его позицию — методом getSelectedIndex().

Если список позволяет осуществить множественный выбор, то выделенные пункты в виде массива типа String[] можно получить методом getSelectedItems(), а позиции выделенных пунктов в виде массива типа int [ ] -методом getSelectedIndexes ( ).

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

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при двойном щелчке кнопкой мыши на выбранном пункте происходит событие ActionEvent.

В листинге 10.2 с помощью классов Choice и List задаются компоненты, аналогичные компонентам листинга 10.1, а рис. 10.2 показывает, как изменится при этом интерфейс.

Листинг 10.2. Использование списков

import java.awt.*; import java.awt.event.*;

class ListTest extends Frame{

ListTest(String s){ super(s); setLayout(null);

setFont(new Font("Serif", Font.BOLD, 15));

Label l1 = new Label("Выберите товар:", Label.CENTER); l1.setBounds(10, 50, 120, 30); add(l1);

Label l2 = new Label("Выберите способ оплаты:");

l2.setBounds(170, 50, 200, 30); add(l2);

List l = new List(2, true); l.add("Книги"); l.add("Диски") ; l.add("Игрушки");

l.setBounds(20, 90, 100, 40); add(l);

Choice ch = new Choice(); ch.add("Почтовым переводом"); ch.add("Кредитной картой"); ch.setBounds(170, 90, 200,30); add(ch);

Button b1 = new Button("Продолжить"); b1.setBounds( 30, 150, 100, 30); add(b1);

Button b2 = new Button("Отменить");

b2.setBounds(140, 150, 100, 30); add(b2);

Button b3 = new ButtonC^bMв„ў"); b3.setBounds(250, 150, 100, 30); add(b3);

setSize(400, 200); setVisible(true);

}

public static void main(String[] args){

Frame f = new ListTest(" Простые компоненты"); f.addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){

System.exit(0);

}

});

}

Рис. 10.2. Использование списков

Компоненты для ввода текста

В библиотеке AWT есть два компонента для ввода текста с клавиатуры: компонент TextField, позволяющий ввести только одну строку, и компонент TextArea, в который можно ввести множество строк.

Оба класса расширяют класс Textcomponent, в котором собраны их общие методы, такие как выделение текста, позиционирование курсора, получение текста.

Класс TextComponent

В классе Textcomponent нет конструктора, этот класс не используется самостоятельно.

Основной метод класса — getText () — возвращает в виде строки string текст, находящийся в поле ввода.

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

isEditable ( ), изменить значения в нем — методом setEditable(boolean editable).

Текст, находящийся в поле, хранится как объект класса string, поэтому у каждого символа есть индекс (у первого — индекс 0). Индекс используется для определения позиции курсора (caret) методом getCaretPosition (), для установки позиции курсора методом

setCaretPosition(int ind) и для выделения текста.

Текст выделяется, как обычно, мышью или клавишами со стрелками при нажатой клавише , но можно выделить его из программы методом select(int begin, int end). При этом помечается текст от символа с индексом begin включительно до символа с индексом end исключительно.

Весь текст выделяет метод selectAll (). Можно отметить начало выделения методом

setSelectionStart(int ind);

и конец выделения методом

setSelectionEnd(int ind);

Важнее все-таки не задать, а получить выделенный текст. Его возвращает метод getSelectedText (), а начальный и конечный индекс выделения возвращают методы

getSelectionStart() Рё getSelectionEnd().

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении текста пользователем происходит событие TextEvent.

Строка ввода TextField

Компонент TextField — это поле для ввода одной строки текста. Ширина поля измеряется в колонках (column). Ширина колонки — это средняя ширина символа в шрифте, которым вводится текст. Нажатие клавиши заканчивает ввод и служит сигналом к началу обработки введенного текста, т. е. при этом происходит событие

ActionEvent.

В классе четыре конструктора:

□ TextField () — создает пустое поле шириной в одну колонку;

□ TextField (int columns) -создает пустое поле с числом колонок columns;

□ TextField(String text) — создает поле с текстом text;

□ TextField (String text, int columns) - создает поле с текстом text и числом колонок

columns.

К методам, унаследованным от класса Textcomponent, добавляются методы getColumns () и

setColumns(int col).

Интересная разновидность строки ввода — строка для ввода пароля. В таком поле вместо вводимых символов появляется какой-нибудь особый эхо-символ, чаще всего звездочка, чтобы пароль никто не подсмотрел из-за плеча.

Строка ввода пароля получается из обычной строки ввода после выполнения метода setEchoChar(char echo). Аргумент echo — это символ, который будет появляться в поле. Проверить, установлен ли эхо-символ, можно логическим методом echoCharIsSet (); получить эхо-символ — методом getEchoChar ( ).

Чтобы вернуть строку ввода в обычное состояние, достаточно выполнить метод

setEchoChar(0).

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении текста пользователем происходит событие TextEvent, а при нажатии клавиши — событие ActionEvent.

Поле ввода TextArea

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

Основной конструктор класса,

TextArea(String text, int rows, int columns, int scrollbars);

создает область ввода с текстом text, числом видимых строк rows, числом колонок columns и заданием полос прокрутки scrollbars одной из четырех констант:

SCROLLBARS_NONE, SCROLLBARS_HORIZONTAL_ONLY, SCROLLBARS_VERTICAL_ONLY, SCROLLBARS_BOTH.

Остальные конструкторы задают некоторые параметры по умолчанию:

□ TextArea (String text, int rows, int columns) -присутствуют обе полосы прокрутки;

□ TextArea (int rows, int columns) — в поле пустая строка;

□ TextArea(string text) — размеры устанавливает контейнер;

□ TextArea ( ) -конструктор по умолчанию.

Среди методов класса TextArea наиболее важны:

□ append (string text) — добавляет текст text в конец уже введенного текста;

□ insert (String text, int pos) -вставляет текст в указанную позицию pos;

□ replaceRange(String text, int begin, int end) — удаляет текст, начиная с индекса begin включительно по end исключительно, и помещает вместо него текст text.

Другие методы позволяют изменить и получить количество видимых строк.

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении текста пользователем происходит событие TextEvent.

Рассмотрим пример. В листинге 10.3 создаются три поля (tf1, tf2, tf3 — для ввода имени пользователя, его пароля и заказа) и не редактируемая область ввода, в которой накапливается заказ. В поле ввода пароля tf2 появляется эхо-символ *. Результат показан на рис. 10.3.

Листинг 10.3. Поля ввода

import java.awt.*; import java.awt.event.*;

class TextTest extends Frame{

TextTest(String s){ super(s); setLayout(null);

setFont(new Font("Serif", Font.PLAIN, 14));

Label 11 = new ЬаЬе1("Ваше имя:", Label.RIGHT); l1.setBounds(20, 30, 70, 25); add(l1);

Label 12 = new Labe1("naponb:", Label.RIGHT);

l2.setBounds(20, 60, 70, 25); add(l2);

TextField tf1 = new TextField(30);

tf1.setBounds(100, 30, 160, 25); add(tf1);

TextField tf2 = new TextField(30); tf2.setBounds(100, 60, 160, 25); add(tf2); tf2.setEchoChar('*');

TextField tf3 = new TextFie1d("Введите сюда Ваш заказ", 30); tf3.setBounds(10, 100, 250, 30); add(tf3);

TextArea ta = new TextArea("Вaш заказ:", 5, 50,

TextArea.SCROLLBARS_NONE);

ta.setEditable(false); ta.setBounds(10, 150, 250, 140); add(ta);

Button b1 = new Р’С‹^Рѕ^^СЂРёРјРµ^^"); b1.setBounds(280, 180, 100, 30); add(b1);

Button b2 = new Button("Отменить");

b2.setBounds(280, 220, 100, 30); add(b2);

Button b3 = new ButtonC'BbMв„ў"); b3.setBounds(280, 260, 100, 30); add(b3);

setSize(400, 300); setVisible(true);

}

public static void main(String[] args){

Frame f = new TextTest(" Поля ввода"); f.addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){

System.exit(0);

}

});

}

}

Рис. 10.3. Поля ввода

Линейка прокрутки Scrollbar

Компонент Scrollbar- это линейка прокрутки, но В библиотеке AWT класс Scrollbar

используется еще и для организации ползунка (slider). Объект может располагаться горизонтально или вертикально, обычно полосы прокрутки размещают внизу и справа.

Каждая линейка прокрутки охватывает некоторый диапазон значений и хранит текущее значение из этого диапазона. В линейке прокрутки есть пять элементов управления для перемещения по диапазону. Две стрелки на концах линейки вызывают перемещение на одну единицу (unit) в соответствующем направлении при щелчке на стрелке кнопкой мыши. Положение движка или бегунка (bubble, thumb) показывает текущее значение из диапазона и может его изменять при перемещении бегунка с помощью мыши. Два промежутка между движком и стрелками позволяют переместиться на один блок (block) щелчком кнопки мыши.

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

Методы работы с данным компонентом описаны в интерфейсе Adjustable, который реализован классом Scrollbar.

В классе scrollbar три конструктора:

□ Scrollbar () — создает вертикальную полосу прокрутки с диапазоном 0—100, текущим значением 0 и блоком величиной в 10 единиц;

□ Scrollbar (int orientation) - ориентация полосы прокрутки orientation задается од

ной из двух констант: HORIZONTAL или VERTICAL;

□ Scrollbar(int orientation, int value, int visible, int min, int max) — задает, кроме ориентации, еще начальное значение value, размер блока visible, диапазон значений

min—max.

Аргумент visible определяет еще и длину движка — она устанавливается пропорционально диапазону значений и длине полосы прокрутки. Например, конструктор по умолчанию задаст длину движка равной 0,1 длины полосы прокрутки.

Основной метод класса — getValue () — возвращает значение текущего положения движка на полосе прокрутки. Остальные методы доступа позволяют узнать и изменить характеристики объекта, примеры их использования показаны в листинге 15.6.

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении значения пользователем происходит событие AdjustmentEvent.

В листинге 10.4 создаются три вертикальные полосы прокрутки — красная, зеленая и синяя, позволяющие выбрать какое-нибудь значение соответствующего цвета в диапазоне 0—255, с начальным значением 127. Кроме них создается область, заполняемая получившимся цветом, и две кнопки. Линейки прокрутки, их заголовок и масштабные метки помещены в отдельный контейнер p типа Panel. Об этом чуть позже в данной главе.

Как все это выглядит, показано на рис. 10.4.

Рис. 10.4. Полосы прокрутки для выбора цвета В листинге 15.6 мы "оживим" эту программу.

Листинг 10.4. Линейки прокрутки для выбора цвета

import java.awt.*; import java.awt.event.*;

class ScrollTest extends Frame{

Scrollbar sbRed = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255)
Scrollbar sbGreen = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255)
Scrollbar sbBlue = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255)

Color mixedColor = new Color(127, 127, 127);

Label lm = new Label();

Button b1 = new Button("Применить");

Button b2 = new Ви^опСОтменить");

ScrollTest(String s){ super(s); setLayout(null);

setFont(new Font("Serif", Font.BOLD, 15));

Panel p = new Panel(); p.setLayout(null);

p.setBounds(10,50, 150, 260); add(p);

Label lc = new ЬаЬе1("Подберите цвет"); lc.setBounds(20, 0, 120, 30); p.add(lc);

Label lmin = new Label("0", Label.RIGHT); lmin.setBounds(0, 30, 30, 30); p.add(lmin);

Label lmiddle = new Label("127", Label.RIGHT); lmiddle.setBounds(0, 120, 30, 30); p.add(lmiddle); Label lmax = new Label("255", Label.RIGHT); lmax.setBounds(0, 200, 30, 30); p.add(lmax);

sbRed.setBackground(Color.red); sbRed.setBounds(40, 30, 20, 200); p.add(sbRed);

sbGreen.setBackground(Color.green); sbGreen.setBounds(70, 30, 20, 200); p.add(sbGreen);

sbBlue.setBackground(Color.blue); sbBlue.setBounds(100, 30, 20, 200); p.add(sbBlue);

Label lp = new Label("Образец:"); lp.setBounds(250, 50, 120, 30); add(lp);

lm.setBackground(new Color(127, 127, 127)); lm.setBounds(220, 80, 120, 80); add(lm); b1.setBounds(240, 200, 100, 30); add(b1); b2.setBounds(240, 240, 100, 30); add(b2);

setSize(400, 300); setVisible(true);

}

public static void main(String[] args){

Frame f = new ScrollTest(" Выбор цвета"); f.addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){ System.exit(0);

}

});

}

}

Контейнер Panel

В листинге 10.4 использован контейнер Panel. Рассмотрим возможности этого класса.

Контейнер Panel — это невидимый компонент графического интерфейса, служащий для объединения нескольких других компонентов в один объект типа Panel.

Класс Panel очень прост, но важен. В нем всего два конструктора:

□ Panel ( ) -создает контейнер с менеджером размещения по умолчанию FlowLayout;

□ Panel (LayoutManager layout) — создает контейнер с указанным менеджером размещения компонентов layout.

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

add():

Panel p = new Panel(); p.add(comp1); p.add(comp2);

и т. д. Размещает компоненты в контейнере его менеджер размещения, о чем мы поговорим в следующей главе.

Контейнер Panel используется очень часто. Он удобен для создания группы компонентов.

В листинге 10.4 три полосы прокрутки вместе с заголовком Подберите цвет и масштабными метками 0, 127 и 255 образуют естественную группу. Если мы захотим переместить ее в другое место окна, нам придется переносить каждый из семи компонентов, входящих в указанную группу. При этом придется следить за тем, чтобы их взаимное положение не изменилось. Вместо этого мы создали панель p и разместили на ней все семь элементов. Метод setBounds () каждого из рассматриваемых компонентов указывает в данном случае положение и размер компонента в системе координат панели p, а не окна Frame. В окно мы поместили сразу целую панель, а не ее отдельные компоненты.

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

Контейнер ScrollPane

Контейнер ScrollPane может содержать только один компонент, но зато такой, который не помещается целиком в окне. Контейнер обеспечивает средства прокрутки для просмотра большого компонента. В контейнере можно установить линейки прокрутки либо постоянно, константой SCROLLBARS_ALWAYS, либо так, чтобы они появлялись только при необходимости (если компонент действительно не помещается в окно) константой SCROLLBARS_AS_NEEDED.

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

В классе два конструктора:

□ ScrollPane () — создает контейнер, в котором полосы прокрутки появляются по необходимости;

□ ScrollPane(int scrollbars) — создает контейнер, в котором появление линеек прокрутки задается одной из трех указанных ранее констант.

Конструкторы создают контейнер размером 100x100 пикселов, в дальнейшем можно изменить размер унаследованным методом setSize(int width, int height) .

Ограничение, заключающееся в том, что ScrollPane может содержать только один компонент, легко обходится. Всегда можно сделать этим единственным компонентом объект класса Panel, разместив на панели что угодно.

Среди методов класса интересны те, что позволяют прокручивать компонент в

ScrollPane:

□ методы getHAdj ustable ( ) и getVAdj ustable ( ) возвращают положение линеек прокрутки в виде интерфейса Adjustable;

□ метод getScrollPosition () показывает в виде объекта класса Point координаты (x, y) точки компонента, находящейся в левом верхнем углу панели ScrollPane;

□ метод setScrollPosition(Point p) или setScrollPosition(int x, int y) прокручивает компонент в позицию (x, y) .

Контейнер Window

Контейнер Window — это пустое окно, без внутренних элементов: рамки, строки заголовка, строки меню, линеек прокрутки. Это просто прямоугольная область на экране. Окно типа Window самостоятельно, оно не содержится ни в каком контейнере, его не надо заносить в контейнер методом add (). Однако оно не связано с оконным менеджером графической системы. Следовательно, нельзя изменить его размеры, переместить в другое место экрана. Поэтому оно может быть создано только каким-нибудь уже существующим окном, владельцем (owner) или родителем (parent) окна Window. Когда окновладелец убирается с экрана, вместе с ним убирается и порожденное окно. Владелец окна указывается в конструкторе:

□ Window (Frame f) — создает окно, владелец которого — фрейм f;

□ Window (Window owner) — создает окно, владелец которого — уже имеющееся окно или подкласс класса Window.

Созданное конструктором окно не выводится на экран автоматически. Его следует отобразить методом setVisible(true). Убрать окно с экрана можно методом setVisible (false), а проверить, видно ли окно на экране, — логическим методом

isShowing().

Методами

setIconImage(Image icon);

setIconImages(List icons);

можно задать один или несколько ярлыков для окна, а посмотреть их можно методом

List getIconImages();

Окно типа Window возможно использовать для создания всплывающих окон предупреждения, сообщения, подсказки. Для создания диалоговых окон есть подкласс Dialog, всплывающих меню — класс PopupMenu.

Видимое на экране окно выводится на передний план методом toFront () или, наоборот, помещается на задний план методом toBack ( ). Методом setAlwaysOnTop (true) можно дать указание графическому менеджеру всегда держать окно на переднем плане. Не все графические менеджеры могут выполнить это указание, поэтому такую возможность следует предварительно проверить логическим методом isAlwaysOnTopSupported (). Выполнение этого указания можно проверить логическим методом isAlwaysOnTop ( ).

Уничтожить окно, освободив занимаемые им ресурсы, можно методом dispose (). Менеджер размещения компонентов в окне по умолчанию — BorderLayout.

Окно создает свой экземпляр класса Toolkit, который можно получить методом

getToolkit() .

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении размеров окна, его перемещении или удалении с экрана, а также показе на экране происходит событие WindowEvent.

Контейнер Frame

Контейнер Frame — это полноценное готовое окно со строкой заголовка, в которую помещены кнопки контекстного меню, сворачивания окна в ярлык и разворачивания во весь экран и кнопка закрытия приложения. Заголовок окна записывается в конструкторе или методом setTitle(String title). Окно окружено рамкой. В него можно установить строку меню методом setMenuBar (MenuBar mb). Это мы обсудим в конце данной главы.

На кнопке контекстного меню в левой части строки заголовка изображена дымящаяся чашечка кофе — логотип Java. Вы можете установить там другое изображение методом setIconImage(Image icon), создав предварительно изображение icon в виде объекта класса Image. Как это сделать, объясняется в главе 20.

Все элементы окна Frame вычерчиваются графической оболочкой операционной системы по правилам этой оболочки. Окно Frame автоматически регистрируется в оконном менеджере графической оболочки и может перемещаться, менять размеры, сворачиваться в панель задач (task bar) с помощью мыши или клавиатуры, как "родное" окно операционной системы.

Создать окно типа Frame можно следующими конструкторами:

□ Frame () — создает окно с пустой строкой заголовка;

□ Frame(String title) — записывает аргумент title в строку заголовка;

□ Frame(GraphicsConfiguration gc) -определяет конфигурацию окна параметром gc.

□ Frame(String title, GraphicsConfiguration gc) — определяет строку заголовка title и конфигурацию окна параметром gc.

Методы класса Frame осуществляют доступ к элементам окна, но не забывайте о том, что класс Frame наследует около двухсот методов классов Component, Container и Window. В частности, наследуется менеджер размещения по умолчанию-BorderLayout.

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении размеров окна, его перемещении или удалении с экрана, а также показе на экране происходит событие WindowEvent.

Программа листинга 10.5 создает два окна типа Frame, в которые помещаются строки — метки Label. При закрытии основного окна щелчком по соответствующей кнопке в строке заголовка или комбинацией клавиш + выполнение программы завершается обращением к методу System.exit(0), и закрываются оба окна. При закрытии второго окна происходит обращение к методу dispose (), и закрывается только это окно.

Листинг 10.5. Создание двух окон

import java.awt.*; import java.awt.event.*;

class TwoFrames{

public static void main(String[] args){ Fr1 f1 = new Fr1(" РћСЃРЅРѕРІРЅРѕРµ РѕРєРЅРѕ");

Fr2 f2 = new Fr2(" Второе окно");

}

}

class Fr1 extends Frame{

Fr1(String s){ super(s); setLayout(null);

Font f = new Font("Serif", Font.BOLD, 15); setFont(f);

Label l = new Label('^TO главное окно", Label.CENTER); l.setBounds(10, 30, 180, 30); add(l);

setSize(200, 100); setVisible(true);

addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){

System.exit(0);

}

});

}

}

class Fr2 extends Frame{

Fr2(String s){ super(s); setLayout(null);

Font f = new Font("Serif", Font.BOLD, 15); setFont(f);

Label l = new Label('^TO второе окно", Label.CENTER); l.setBounds(10, 30, 180, 30); add(l);

setBounds(50, 50, 200, 100); setVisible(true);

addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){ dispose();

}

});

}

}

Рис. 10.5. Программа с двумя окнами

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

Контейнер Dialog

Контейнер Dialog — это окно обычно фиксированного размера, предназначенное для ответа на сообщения приложения. Оно автоматически регистрируется в оконном менеджере графической оболочки, следовательно, его можно перемещать по экрану, менять его размеры. Но окно типа Dialog, как и его суперкласс — окно типа Window, — обязательно имеет родительское окно — владельца owner, который указывается в конструкторе.

Окно типа Dialog может быть модальным (modal), в котором надо обязательно выполнить все предписанные действия, иначе из окна нельзя будет выйти. Модальное окно блокирует родительское окно и, возможно, еще несколько окон, образующих область блокировки (scope of blocking). Заблокированные окна не могут получить фокус ввода и находятся на экране позади модального окна.

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

Dialog.ModalityType:

□ modeless — отсутствие блокировки;

□ document_modal — блокируются родительские окна, образующие один документ, причем под документом здесь понимаются все окна с общим предком (document root);

□ appli cat I on_modal — блокируются родительские окна, относящиеся к одному приложению;

□ toolkit_modal — блокируются родительские окна, относящиеся к одному экземпляру класса Toolkit.

Более подробное и точное описание типов модальности приведено в документе The AWT Modality, хранящемся в файле docs/api/java/awt/doc-files/Modality.html.

В классе Dialog определена константа default_modality_type, равная application_modal в Java SE 6. Она неявно применяется в конструкторах класса и методе setModal(true).

Графическая система, в которой открыто окно, может не отрабатывать все типы модальности, поэтому в класс Toolkit введен логический метод

isModalityTypeSupported(Dialog.ModalityType modalityType), которым можно проверить тот или иной тип modalityType.

Отдельные окна можно исключить из области блокировки. Для этого в класс Window введен метод setModalExclusionType(Dialog.ModalExclusionType excType), аргументом которого служат константы из перечисления Dialog.ModalExclusionType:

□ no_exclude — отсутствие исключения;

□ application_exclude — модальные окна, имеющие тип модальности application_modal, не могут блокировать это родительское окно;

□ toolkit_exclude — модальные окна, имеющие тип модальности toolkit_modal, не могут блокировать это родительское окно.

Опять-таки графическая система, в которой открыто окно, может не отрабатывать все эти исключения, поэтому в класс Toolkit введен еще один логический метод —

isModalExclusionTypeSupported(Dialog.ModalExclusionType excType).

В классе Dialog более десяти конструкторов. Из них:

□ Dialog(Dialog owner) — создает немодальное диалоговое окно типа MODELESS с пустой строкой заголовка;

□ Dialog (Dialog owner, String title) - создает немодальное диалоговое окно типа

modeless со строкой заголовка title;

□ Dialog(Dialog owner, String title, boolean modal) — создает диалоговое окно, которое будет модальным типа default_modality_type, если параметр modal == true;

□ Dialog(Dialog owner, String title, boolean modal, GraphicConfiguration gc) — создает

диалоговое окно с конфигурацией, определяемой параметром gc.

Пять конструкторов аналогичны предыдущим, но создают диалоговые окна, принадлежащие окну типа Frame:

Dialog(Frame owner);

Dialog(Frame owner, String title);

Dialog(Frame owner, boolean modal);

Dialog(Frame owner, String title, boolean modal);

Dialog(Frame owner, String title, boolean modal, GraphicsConfiguration gc);

Еще у пяти конструкторов, аналогичных предыдущим, первый параметр — типа Window, а не Frame.

Среди методов класса Dialog интересны методы, связанные с модальностью: метод

Dialog.ModalityType getModalityType();

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

setModalityType(Dialog.ModalityType type);

меняет тип модальности. Если тип модальности type не отрабатывается графической системой, то устанавливается тип modeless. Во многих графических системах новый тип модальности вступит в силу только после того, как окно будет закрыто и вновь открыто.

В прежних версиях Java SE использовались методы isModal(), проверяющий состояние модальности, и setModal(boolean modal), меняющий это состояние. Эти методы сейчас не применяются, но оставлены в JDK для обратной совместимости со старыми программами.

События

Кроме событий класса Componen — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении размеров окна, его перемещении или удалении с экрана, а также появлении на экране происходит событие WindowEvent.

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

Листинг 10.6. Модальное окно доступа

import java.awt.*; import java.awt.event.*;

class LoginWin extends Dialog{

LoginWin(Frame f, String s){ super(f, s, true); setLayout(null);

setFont(new Font("Serif", Font.PLAIN, 14));

Label l1 = new Label("Ваше имя:", Label.RIGHT);

11. setBounds(20, 30, 70, 25); add(l1);

Label l2 = new Label("Пароль:", Label.RIGHT);

12. setBounds(20, 60, 70, 25); add(l2);

TextField tf1 = new TextField(30); tf1.setBounds(100, 30, 160, 25); add(tf1);

TextField tf2 = new TextField(30); tf2.setBounds(100, 60, 160, 25); add(tf2); tf2.setEchoChar('*');

Button b1 = new Button("Применить"); b1.setBounds(50, 100, 100, 30); add(b1);

Button b2 = new Button("Отменить"); b2.setBounds(160, 100, 100, 30); add(b2);

setBounds(50, 50, 300, 150);

}

}

class DialogTest extends Frame{

DialogTest(String s){ super(s); setLayout(null); setSize(200, 100); setVisible(true);

Dialog d = new LoginWin(this, " РћРєРЅРѕ РІС…РѕРґР°"); d.setVisible(true);

}

public static void main(String[] args){

Frame f = new DialogTest(" Окно-владелец"); f.addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){ System.exit(0);

}

});

}

Рис. 10.6. Модальное окно доступа

Контейнер FileDialog

Контейнер FileDialog — это модальное окно с владельцем типа Frame, содержащее стандартное окно выбора файла для открытия (константа load) или сохранения (константа save). Окна операционной системы создаются и помещаются в объект класса FileDialog автоматически.

В классе шесть конструкторов:

□ FileDialog ( Frame owner) -создает окно с пустым заголовком для открытия файла;

□ FileDialog(Frame owner, String title) — создает окно открытия файла с заголовком

title;

□ FileDialog(Frame owner, String title, int mode) — создает окно открытия или сохранения документа; аргумент mode имеет два значения: окно открытия файла

FileDialog.LOAD и окно сохранения файла FileDialog.SAVE.

Остальные три конструктора аналогичны первым трем, только первый параметр у них типа Dialog, а не Frame.

Методы класса getDirectory( ) и getFile ( ) возвращают только выбранный каталог и имя файла в виде строки String. Загрузку или сохранение файла затем нужно производить методами классов ввода/вывода, как рассказано в главе 23. Там же приведены примеры использования класса FileDialog.

Можно установить начальный каталог для поиска файла и имя файла методами

setDirectory(String dir) Рё setFile(String fileName).

Вместо конкретного имени файла fileName можно написать его шаблон, например *.java (первые символы — звездочка и точка), тогда в окне будут видны только имена файлов, заканчивающиеся точкой и словом java.

Метод setFilenameFilter (FilenameFilter filter) устанавливает шаблон filter для имени выбираемого файла. В окне будут видны только имена файлов, подходящие под шаблон. Этот метод не реализован в Sun JDK на платформе MS Windows.

События

Кроме событий класса Component — ComponentEvent, FocusEvent, KeyEvent, MouseEvent — при изменении размеров окна, его перемещении или удалении с экрана, а также появлении на экране происходит событие WindowEvent.

Создание собственных компонентов

Создать собственный компонент, дополняющий свойства и методы уже существующих компонентов AWT, очень просто — надо лишь унаследовать свой класс от существующего класса Button, TextField или другого класса-компонента.

Если нужно скомбинировать несколько компонентов в один, новый, компонент, то достаточно расширить класс Panel, расположив компоненты на панели.

Если же требуется создать совершенно новый компонент, то AWT предлагает две возможности: создать "тяжелый" или "легкий" компонент. Для создания собственных "тяжелых" компонентов в библиотеке AWT есть класс Canvas — пустой компонент, для которого создается свой peer-объект графической системы.

Компонент Canvas

Компонент Canvas — это пустой компонент. Класс Canvas довольно прост — в нем чаще всего используется только конструктор по умолчанию Canvas () и пустая реализация метода paint(Graphics g).

Чтобы создать свой "тяжелый" компонент, необходимо расширить класс Canvas, дополнив его нужными полями и методами, и при необходимости переопределить метод

paint().

Например, как вы заметили, на стандартной кнопке Button можно написать только одну текстовую строку. Нельзя написать несколько строк или отобразить на кнопке рисунок. Создадим свой "тяжелый" компонент — кнопку с рисунком.

В листинге 10.7 кнопка с рисунком — это класс FlowerButton. Рисунок задается методом drawFlower(), а рисуется методом paint(). Метод paint(), кроме того, чертит по краям кнопки внизу и справа отрезки прямых, изображающих тень, отбрасываемую "выпуклой" кнопкой. При нажатии кнопки мыши на компоненте такие же отрезки чертятся вверху и слева — кнопка "вдавилась". При этом рисунок сдвигается на два пиксела вправо вниз — он "вдавливается" в плоскость окна.

Кроме этого, в классе FlowerButton задана реакция на нажатие и отпускание кнопки мыши. Это мы обсудим в главе 15, а пока скажем, что при каждом нажатии и отпускании кнопки меняется значение поля isDown и кнопка перечерчивается методом repaint (). Это достигается выполнением методов mousePressed () и mouseReleased ( ).

Для сравнения рядом помещена стандартная кнопка типа Button того же размера. Рисунок 10.7 демонстрирует вид этих кнопок.

Листинг 10.7. Кнопка с рисунком

import java.awt.*; import java.awt.event.*;

class FlowerButton extends Canvas implements MouseListener{ private boolean isDown=false;

public FlowerButton(){ super();

setBackground(Color.lightGray);

addMouseListener(this);

}

public void drawFlower(Graphics g, int x, int y, int w, int h){ g.drawOval(x + 2*w/5 — 6, y, w/5, w/5);

g.drawLine(x + w/2 — 6, y + w/5, x + w/2 — 6, y + h — 4); g.drawOval(x + 3*w/10 — 6, y + h/3 — 4, w/5, w/5); g.drawOval(x + w/2 — 6, y + h/3 — 4, w/5, w/5);

}

public void paint(Graphics g){

int w = getSize().width, h = getSize().height; if (isDown){

g.drawLine(0, 0, w — 1, 0); g.drawLine(1, 1, w — 1, 1); g.drawLine(0, 0, 0, h — 1); g.drawLine(1, 1, 1, h — 1); drawFlower(g, 8, 10, w, h);

}else{

g.drawLine(0, h — 2, . w — 2, h — 2)
g.drawLine(1, h — 1, 1 s: \—i1s 1)
g.drawLine(w — 2, h — 2, w — 2, 0)
g.drawLine(w — 1, h — 1, w — 1, 1)
drawFlower(g, 6, 8, w, h);

}

}

public void mousePressed(MouseEvent e){ isDown=true; repaint();

}

public void mouseReleased(MouseEvent e){ isDown=false; repaint();

}

public void mouseEntered(MouseEvent e){} public void mouseExited(MouseEvent e) {} public void mouseClicked(MouseEvent e){}

}

class DrawButton extends Frame{ DrawButton(String s){ super(s); setLayout(null);

Button b = new Button("OK"); b.setBounds(200, 50, 100, 60); add(b);

FlowerButton d = new FlowerButton(); d.setBounds(50, 50, 100, 60); add(d);

setSize(400, 150); setVisible(true);

public static void main(String[] args){

Frame f = new DrawButton(" РљРЅРѕРїРєР° СЃ СЂРёСЃСѓРЅРєРѕРј"); f.addWindowListener(new WindowAdapter(){

public void windowClosing(WindowEvent ev){ System.exit(0);

}

});

}

}

Р РёСЃ. 10.7. РљРЅРѕРїРєР° СЃ СЂРёСЃСѓРЅРєРѕРј

Создание "легкого" компонента

"Легкий" компонент, не имеющий своего peer-объекта в графической системе, создается как прямое расширение класса Component или Container. При этом необходимо задать те действия, которые в "тяжелых" компонентах выполняет peer-объект.

Например, заменив в листинге 10.7 заголовок класса FlowerButton строкой

class FlowerButton extends Component implements MouseListener{

а затем перекомпилировав и выполнив программу, вы получите "легкую" кнопку, но увидите, что ее фон стал белым, потому что метод setBackground(Color.lightGray) не сработал.

Это объясняется тем, что теперь всю черную работу по изображению кнопки на экране выполняет не peer-двойник кнопки, а "тяжелый" контейнер, в котором расположена кнопка, в нашем случае — класс Frame. Контейнер же ничего не знает о том, что надо обратиться к методу setBackground (), он рисует только то, что записано в методе paint ( ). Придется убрать метод setBackground() из конструктора и заливать фон серым цветом вручную в методе paint (), как показано в листинге 10.8.

"Легкий" контейнер не умеет рисовать находящиеся в нем "легкие" компоненты, поэтому в конце метода paint () "легкого" контейнера нужно обратиться к методу paint () суперкласса:

super.paint(g);

Тогда рисованием займется "тяжелый" суперкласс-контейнер. Он нарисует и лежащий в нем "легкий" контейнер, и размещенные в контейнере "легкие" компоненты.

Совет

Завершайте метод paint() "легкого" контейнера обращением к методу paint () суперкласса.

Предпочтительный размер "тяжелого" компонента устанавливается peer-объектом, а для "легких" компонентов его надо задать явно, переопределив метод getPreferredSize(), иначе некоторые менеджеры размещения, например FlowLayout (), установят нулевой размер, и компонент не будет виден на экране.

СОВЕТ

Переопределяйте метод getPreferredSize ().

Интересная особенность "легких" компонентов — они изначально рисуются прозрачными, незакрашенная часть прямоугольного объекта не будет видна. Это позволяет создать компонент любой видимой формы. Листинг 10.8 показывает, как можно изменить метод paint () листинга 10.7 для создания круглой кнопки и задать дополнительные методы, а рис. 10.8 демонстрирует ее вид.

Листинг 10.8. Создание круглой кнопки

getSize().height;

// Диаметр круга // Сохраняем текущий цвет // Устанавливаем серый цвет ; // Заливаем круг серым цветом

// Восстанавливаем текущий цвет

public void paint(Graphics g){ int w = getSize().width, h int d = Math.min(w, h);

Color c = g.getColor(); g.setColor(Color.lightGray); g.fillArc(0, 0, d, d, 0, 360); g.setColor(c); if (isDown){

g.drawArc(0, 0, d, d, 43, 180)

g.drawArc(1, 1, d — 2, d — 2, 43, 180); drawFlower(g, 8, 10, d, d);

}else{

g.drawArc(0, 0, d, d, 229, 162); g.drawArc(1, 1, d — 2, d — 2, 225, 170); drawFlower(g, 6, 8, d, d);

}

}

public Dimension getPreferredSize(){ return new Dimension(30,30);

}

public Dimension getMinimumSize(){ return getPreferredSize();

}

public Dimension getMaximumSize(){ return getPreferredSize();

}

Рис. 10.8. Круглая кнопка

Сразу же надо дать еще одну рекомендацию. "Легкие" контейнеры не занимаются обработкой событий без специального указания. Поэтому в конструктор "легкого" компонента следует включить обращение к методу enableEvents () для каждого типа событий. В нашем примере в конструктор класса FlowerButton полезно добавить строку

enableEvents(AWTEvent.MOUSE_EVENT_MASK);

на случай, если кнопка окажется в "легком" контейнере. Подробнее об этом мы поговорим в главе 15.

Упражнение

2. Создайте треугольную кнопку.

Создание меню

В контейнер типа Frame заложена возможность установки стандартной строки меню (menu bar), располагаемой ниже строки заголовка, как показано на рис. 10.9. Эта строка — объект класса MenuBar.

Рис. 10.9. Система меню

Все, что нужно сделать для установки строки меню в контейнере Frame, — это создать объект класса MenuBar и обратиться к методу setMenuBar ( ) :

Frame f = new Frame("Пример меню");

MenuBar mb = new MenuBar(); f.setMenuBar(mb);

Если имя mb не понадобится, можно совместить два последних обращения к методам:

f.setMenuBar(new MenuBar());

Разумеется, строка меню еще пуста и пункты меню не созданы.

Каждый элемент строки меню — выпадающее меню (drop-down menu ) — это объект класса Menu.

Создать эти объекты и занести их в строку меню ничуть не сложнее, чем создать строку меню:

Menu mFile = new Menu("Файл"); mb.add(mFile);

Menu mEdit = new Menu("Правка"); mb.add(mEdit);

Menu mView = new Menu("Р’РёРґ"); mb.add(mView);

Menu mHelp = new Menu("Справка"); mb.setHelpMenu(mHelp);

и т. д. Элементы располагаются слева направо в порядке обращения к методам add (), как показано на рис. 10.9. Во многих графических системах принято меню Справка (Help) прижимать к правому краю строки меню. Это достигается обращением к методу setHelpMenu(), но фактическое положение меню Справка определяется графической оболочкой.

Затем определяем каждое выпадающее меню, создавая его пункты. Каждый пункт меню — это объект класса MenuItem. Схема его создания и добавления к меню точно такая же, как и самого меню:

MenuItem create = new MenuItem("Создать"); mFile.add(create);

MenuItem open = new MenuItem("Открыть..."); mFile.add(open);

и т. д. Пункты меню будут расположены сверху вниз в порядке обращения к методам

add().

Часто пункты меню объединяются в группы. Одна группа от другой отделяется горизонтальной чертой. На рис. 10.9 черта проведена между командами Открыть и Отправить. Эта черта создается методом addSeparator() класса Menu или определяется как пункт меню с надписью специального вида — дефисом:

mFile.add(new MenuItem("-"));

Интересно, что класс Menu расширяет класс MenuItem, а не наоборот. Это означает, что меню само является пунктом меню и позволяет задавать меню в качестве пункта другого меню, тем самым организуя вложенные подменю:

Menu send = new Menu("Отправить"); mFile.add(send);

Здесь меню send добавляется в меню mFile как один из его пунктов. Подменю send заполняется пунктами меню, как обычное меню.

Часто команды меню создаются для выбора из них каких-то возможностей, подобно компонентам Checkbox. Такие пункты можно выделить щелчком кнопки мыши или отменить выделение повторным щелчком. Эти команды — объекты класса

CheckboxMenuItem:

CheckboxMenuItem disk = new CheckboxMenuItem("Диск A:", true); send.add(disk);

send.add(new CheckboxMenuItem("РђСЂС…РёРІ"));

Рё С‚. Рґ.

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

Многие графические оболочки, но не MS Windows, позволяют создавать отсоединяемые (tear-off) меню, которые можно перемещать по экрану. Это указывается в конструкторе

Menu(String label, boolean tearOff);

Если tearOff == true и графическая оболочка умеет создавать отсоединяемое меню, то оно будет создано. В противном случае этот аргумент просто игнорируется.

Наконец, надо назначить действия командам меню. Команды меню типа MenuItem порождают события типа ActionEvent, поэтому нужно присоединить к ним объект класса-слушателя как к обычным компонентам, записав что-то вроде

create.addActionListener(new SomeActionEventHandler()); open.addActionListener(new AnotherActionEventHandler());

Пункты типа CheckboxMenuItem порождают события типа ItemEvent, поэтому надо обращаться к объекту-слушателю этого события:

disk.addItemListener(new SomeItemEventHandler());

Очень часто действия, записанные в командах меню, вызываются не только щелчком кнопки мыши, но и "горячими" клавишами-акселераторами (shortcut), действующими чаще всего при нажатой клавише . На экране в пунктах меню, которым назначены "горячие" клавиши, появляются подсказки вида Ctrl+N, Ctrl+O. "Горячая" клавиша определяется объектом класса MenuShortcut и указывается в его конструкторе константой класса KeyEvent, например:

MenuShortcut keyCreate = new MenuShortcut(KeyEvent.VK N);

После этого "горячей" будет комбинация клавиш +. Затем полученный объект указывается в конструкторе класса MenuItem:

MenuItem create = new MenuItem("Создать", keyCreate);

Нажатие комбинации клавиш + будет вызывать окно создания. Эти действия, разумеется, можно совместить, например:

MenuItem open = new MenuItem("Открыть...", new MenuShortcut(KeyEvent.VK O));

Можно добавить еще нажатие клавиши . Действие пункта меню будет вызываться нажатием комбинации клавиш ++, если воспользоваться вторым конструктором:

MenuShortcut(int key, boolean useShift); с аргументом useShift == true.

В листинге 10.9 приведена полная программа рисования с обработкой событий. Ее объяснение отложим до главы 15. Результат работы программы показан на рис. 10.10.

Листинг 10.9. Программа рисования с меню

import java.awt.*; import java.awt.event.*;

public class MenuScribble extends Frame{ public MenuScribble(String s){ super(s);

ScrollPane pane = new ScrollPane();

pane.setSize(300, 300); add(pane, BorderLayout.CENTER);

Scribble scr = new Scribble(this, 500, 500); pane.add(scr);

MenuBar mb = new MenuBar(); setMenuBar(mb);

Menu f = new Menu^'J^m");

Menu v = new MenuC'B^") ; mb.add(f); mb.add(v);

MenuItem open = new MenuItem("OTKpbiTb...",

new MenuShortcut(KeyEvent.VK O)); MenuItem save = new MenuItem("CoxpaHHTb",

new MenuShortcut(KeyEvent.VK S)); MenuItem saveAs = new MenuItem("CoxpaHHTb как..."); MenuItem exit = new MenuItem(,,Выxод,,,

new MenuShortcut(KeyEvent.VK Q)) ; f.add(open); f.add(save); f.add(saveAs); f.addSeparator(); f.add(exit);

open.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ FileDialog fd = new FileDialog(new Frame(),

" Загрузить", FileDialog.LOAD); fd.setVisible(true);

}

});

saveAs.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ FileDialog fd = new FileDialog(new Frame(),

" Сохранить", FileDialog.SAVE); fd.setVisible(true);

}

});

exit.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ System.exit(0);

}

});

Menu c = new Menu("UBeT");

MenuItem clear = new MenuItem("Очистить",

new MenuShortcut(KeyEvent.VK D)); v.add(c); v.add(clear);

MenuItem red = new MenuItem("Красный");

MenuItem green = new MenuItem("3exeHbM");

MenuItem blue = new MenuItem("РЎРёРЅРёР№");

MenuItem black = new MenuItem("4epHbM"); c.add(red); c.add(green); c.add(blue); c.add(black);

red.addActionListener(scr); green.addActionListener(scr); blue.addActionListener(scr); black.addActionListener(scr); clear.addActionListener(scr);

addWindowListener(new WinClose()); pack();

setVisible(true);

}

class WinClose extends WindowAdapter{

public void windowClosing(WindowEvent e){

System.exit(0);

}

}

public static void main(String[] args){

new MenuScribble(" \"Рисовалка\" с меню");

}

}

class Scribble extends Component implements

ActionListener, MouseListener, MouseMotionListener{ protected int lastX, lastY, w, h; protected Color currColor = Color.black; protected Frame f;

public Scribble(Frame frame, int width, int height){ f = frame; w = width; h = height; enableEvents (AWTEvent.MOUSE_EVENT_MASK |

AWTEvent. MOUSE_MOTION_EVENT_MASK) ; addMouseListener(this); addMouseMotionListener(this);

}

public Dimension getPreferredSize(){ return new Dimension(w, h);

}

public void actionPerformed(ActionEvent event){

String s = event.getActionCommand();

if (s.equals("Очистить")) repaint();

else if (s.equals("Красный")) currColor = Color.red;

else if (s.equals("Зеленый")) currColor = Color.green;

else if (s.equals("РЎРёРЅРёР№")) currColor = Color.blue;

else if (s.equals("Чeрный")) currColor = Color.black;

}

public void mousePressed(MouseEvent e){

return;

if ((e.getModifiers() & MouseEvent.BUTTON1 MASK) == 0) lastX = e.getX(); lastY = e.getY();

public void mouseDragged(MouseEvent e){

if ((e.getModifiers() & MouseEvent.BUTTON1 MASK) == 0) return; Graphics g = getGraphics(); g.setColor(currColor);

g.drawLine(lastX, lastY, e.getX(), e.getY());

lastX = e.getX(); lastY = e.getY();

}

public void mouseReleased(MouseEvent e){} public void mouseClicked(MouseEvent e){} public void mouseEntered(MouseEvent e){} public void mouseExited(MouseEvent e){} public void mouseMoved(MouseEvent e){}

}

Рис. 10.10. Программа рисования с меню

Всплывающее меню

Всплывающее меню (popup menu) появляется обычно при нажатии или отпускании правой или средней кнопки мыши и является контекстным (context) меню. Его команды зависят от компонента, на котором была нажата кнопка мыши. В языке Java всплывающее меню — объект класса PopupMenu. Этот класс расширяет класс Menu, следовательно, наследует все свойства меню и пункта меню MenuItem. Всплывающее меню присоединяется не к строке меню типа MenuBar или к меню типа Menu в качестве подменю, а к определенному компоненту. Для этого в классе Component есть метод

add(PopupMenu menu).

У некоторых компонентов, например TextField и TextArea, уже существует всплывающее меню. Подобные меню нельзя переопределить.

Присоединить всплывающее меню можно только к одному компоненту. Если надо использовать всплывающее меню с несколькими компонентами в контейнере, то его присоединяют к контейнеру, а нужный компонент определяют с помощью метода getComponent () класса MouseEvent, как показано в листинге 10.10.

Кроме унаследованных свойств и методов, в классе PopupMenu есть метод show(Component comp, int x, int y), показывающий всплывающее меню на экране так, что его левый верхний угол располагается в точке (x, y) в системе координат компонента comp. Чаще всего это компонент, на котором нажата кнопка мыши, возвращаемый методом getComponent (). Компонент comp должен быть внутри контейнера, к которому присоединено меню, иначе возникнет исключительная ситуация.

Всплывающее меню появляется в MS Windows при отпускании правой кнопки мыши, в Motif — при нажатии средней кнопки, а в других графических системах могут быть иные правила. Чтобы учесть эту разницу, в класс MouseEvent введен логический метод isPopupTrigger(), показывающий, что возникшее событие мыши вызывает появление всплывающего меню. Его нужно вызывать при возникновении всякого события мыши, чтобы проверять, не является ли оно сигналом к появлению всплывающего меню, т. е. обращению к методу show (). Было бы слишком неудобно включать такую проверку во все семь методов классов-слушателей событий мыши. Поэтому метод isPopupTrigger() лучше вызывать в методе processMouseEvent ().

Переделаем программу рисования из листинга 10.9, введя в класс Scribble всплывающее меню для выбора цвета рисования и очистки окна и изменив обработку событий мыши. Для простоты уберем строку меню, хотя ее можно было оставить. Результат показан в листинге 10.10, а на рис. 10.11 — вид всплывающего меню в MS Windows.

Листинг 10.10. Программа рисования со всплывающим меню

import java.awt.*; import java.awt.event.*;

public class PopupMenuScribble extends Frame{ public PopupMenuScribble(String s){ super(s);

ScrollPane pane = new ScrollPane();

pane.setSize(300, 300); add(pane, BorderLayout.CENTER);

Scribble scr = new Scribble(this, 500, 500); pane.add(scr);

addWindowListener(new WinClose()); pack();

setVisible(true);

}

class WinClose extends WindowAdapter{

public void windowClosing(WindowEvent e){

System.exit(0);

}

}

public static void main(String[] args){

new PopupMenuScribble(" \"Рисовалка\" с всплывающим меню");

}

class Scribble extends Component implements ActionListener{ protected int lastX, lastY, w, h; protected Color currColor = Color.black; protected Frame f; protected PopupMenu c;

public Scribble(Frame frame, int width, int height){ f = frame; w = width; h = height; enableEvents (AWTEvent.MOUSE_EVENT_MASK |

AWTEvent.MOUSE_MOTION_EVENT_MASK) ;

c = new РорирМепи("Цвет");

add(c);

MenuItem clear = new Мепи^ешСОчистить",

new MenuShortcut(KeyEvent.VK D)); MenuItem red = new MenultemCKpacHbM");

MenuItem green = new MenuItem(,,Зеленый,,);

MenuItem blue = new MenuItem("РЎРёРЅРёР№");

MenuItem black = new MenultemC'depHbM"); c.add(red); c.add(green); c.add(blue); c.add(black); c.addSeparator(); c.add(clear);

red.addActionListener(this); green.addActionListener(this); blue.addActionListener(this); black.addActionListener(this); clear.addActionListener(this);

}

public Dimension getPreferredSize(){ return new Dimension(w, h);

}

public void actionPerformed(ActionEvent event){

String s = event.getActionCommand();

if (s.equals("Очистить")) repaint();

else if (s.equals("Красный")) currColor = Color.red;

else if (s.equals("Зеленый")) currColor = Color.green;

else if (s.equalsCG^Hm")) currColor = Color.blue;

else if (s.equals("Черный")) currColor = Color.black;

}

public void processMouseEvent(MouseEvent e){ if (e.isPopupTrigger())

c.show(e.getComponent(), e.getX(), e.getY()); else if (e.getID() == MouseEvent.MOUSE_PRESSED){ lastX = e.getX(); lastY = e.getY();

}

else super.processMouseEvent(e);

}

public void processMouseMotionEvent(MouseEvent e){ if (e.getID() == MouseEvent.MOUSE_DRAGGED){

Graphics g = getGraphics();

g. setColor(currColor);

g.drawLine(lastX, lastY, e.getX(), e.getY()); lastX = e.getX(); lastY = e.getY();

}

else super.processMouseMotionEvent(e);

}

}

Рис. 10.11. Программа рисования со всплывающим меню

Вопросы для самопроверки

1. Почему класс Container наследует от класса Component, а не наоборот?

2. Каковы общие свойства всех компонентов?

3. Почему класс Component сделан абстрактным?

4. Почему надпись в контейнере — это целый компонент класса Label, а не просто строка символов?

5. Почему для группы радиокнопок не создан отдельный компонент?

6. В чем разница между текстовыми компонентами TextField и TextArea?

7. Почему мы всегда используем окно типа Frame, а не типа Window?

8. Чем отличается создание "тяжелого" компонента от создания "легкого" компонента?

ГЛАВА 1 1

 

Оформление ГИП компонентами Swing

Стандартная поставка Java Platform, Standard Edition (Java SE), включает в себя богатейшую библиотеку классов, обеспечивающих создание графического интерфейса пользователя GUI (Graphical User Interface). Эта графическая библиотека получила название JFC (Java Foundation Classes). В библиотеке JFC можно выделить шесть основных частей:

□ AWT (Abstract Window Toolkit) — базовая библиотека классов с несколько устарелым набором "тяжелых" (heavyweight) графических компонентов, расположенная в пакете j ava. awt и его подпакетах. Мы уже рассмотрели ее возможности в предыдущих главах;

□ Swing — библиотека "легких" (lightweight) графических компонентов, дополняющая и во многом заменяющая библиотеку AWT. Занимает почти двадцать пакетов с префиксом j avax. swing;

□ Java 2D — часть библиотеки AWT, обеспечивающая рисование графики, выбор цвета, вывод изображений и фигурного текста, а также преобразование их перед выводом. Ее возможности уже показаны в главе 9;

□ DnD (Drag and Drop) — библиотека классов, позволяющих перемещать объекты из одного компонента в другой с помощью буфера обмена (clipboard). Классы этой библиотеки помещены в пакеты j ava. awt. datatrans fer и j ava. awt. dnd;

□ Input Method Framework — классы для создания новых методов ввода/вывода. Они занимают пакеты j ava. awt .im и j ava. awt. im.spi;

□ Accessibility — библиотека классов для взаимодействия с нестандартными устройствами ввода/вывода: клавиатурой Брайля, световым пером и др. Она расположена в пакете javax.accessibility.

Основное средство построения графического интерфейса пользователя в технологии Java — это библиотека Swing. Она может применяться везде, где установлен пакет Java Runtime Environment (JRE). Для браузеров, в которые не встроен пакет JRE, корпорация Oracle выпускает модуль Java Plug-in, автоматически загружающийся с сайта при загрузке апплета, использующего классы библиотеки Swing. Модуль Java Plug-in входит в состав JRE и автоматически подгружается в браузер, работающий там, где установлен пакет JRE. Нужно лишь, чтобы браузер распознавал тег или , в котором указан класс апплета.

В состав Java SE в число демонстрационных программ входят апплет и приложение SwingSet2, расположенные в каталоге $JAVA_HOME/demo/jfc/SwingSet2/. Они показывают большинство возможностей Swing. Там же можно посмотреть исходные тексты соответствующих классов. Если в состав браузера входит библиотека Swing, то для просмотра апплета достаточно загрузить в браузер файл SwingSet2.html. Если Swing в браузере нет, то для загрузки Java Plug-in надо загрузить в браузер файл SwingSet2Plugin.html.

Состав библиотеки Swing

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

Для построения же стандартного интерфейса достаточно возможностей, предоставляемых классами из пакета javax.swing. Очень легко создать стандартное окно приложения. В листинге 11.1 показан шаблон для приложения, использующего библиотеку Swing.

Листинг 11.1. Шаблон приложения, использующего Swing

import j ava.awt.*; // Базовые классы AWT.

import j avax.swing.*; // Основные классы Swing.

public class SwingApplicationTemplate extends JFrame{

public SwingApplicationTemplate(String title){

// Создаем основное окно. super(title);

// Получаем контейнер верхнего уровня.

// Для JDK 5.0 и выше это необязательно.

Container c = getContentPane();

// Помещаем компонент в контейнер. c.add(xxxx);

// Прочие установки...

// Задаем начальную ширину и высоту окна. setSize(500, 400);

// Завершаем работу приложения при закрытии окна.

setDefaultCloseOperation(EXIT ON CLOSE);

// Выводим окно на экран. setVisible(true);

public static void main(String[] args){

new SwingApplicationTemplate("Заголовок основного окна");

}

}

Если создаваемое приложение должно предоставить пользователю нестандартное диалоговое окно выбора цвета (а стандартное окно — это экземпляр класса JColorChooser), то понадобится пакет javax.swing.colorchooser.

При создании диалогового окна выбора файла (окно Открыть или Сохранить как) методами класса JFileChooser, для отбора файлов по типу или другому признаку применяются классы из пакета javax. swing. filechooser.

Для обработки содержимого таблицы — экземпляра класса JTable — пригодится пакет

j avax.swing.table.

Работу с объектами, расположенными в виде дерева типа JTree, можно организовать с помощью классов пакета javax.swing.tree.

При создании текстового редактора большую помощь окажут классы из пакета javax.swing.text. Они помогут создать нужную форму курсора, отследить и изменить его позицию, выделить фрагмент текста, задать формат записи дат и чисел и многое другое.

Возможность отмены и повтора действий (undo/redo) в текстовом редакторе обеспечивают классы пакета j avax. swing. undo.

Подпакеты javax.swing.text.html и javax.swing.text.rtf дадут возможность текстовому редактору работать с форматами HTML и RTF, а классы подпакета j avax. swing. text. html. parser содержат средства синтаксического разбора HTML-файлов.

Для оформления рамок различного вида, ограничивающих группы компонентов, предназначены классы из пакета javax.swing.border.

Наконец, пять пакетов javax.swing.plaf.* задают внешний вид и поведение приложения (Look and Feel, L&F) в различных графических средах. Можно сделать вид и поведение независимым от графической оболочки операционной системы. Тогда приложение в любой графической оболочке будет выглядеть одинаково и в равной степени реагировать на действия мыши и клавиатуры. Можно, наоборот, сделать так, что в каждой графической среде: MS Windows, CDE/Motif, Macintosh, приложение будет выглядеть как "родное" для этой среды и реагировать на внешние воздействия по правилам данной графической среды. Можно заложить изменение внешнего вида и поведения в настройки приложения, сделав его изменяемым (Pluggable Look and Feel, PL&F, PLAF или plaf) по желанию пользователя. Эту возможность можно реализовать классами пакета

j avax.swing.plaf.multi.

Технология Java предлагает свой собственный стиль, называемый "Java Look and Feel", ранее называвшийся стилем "Metal". Этот стиль у нас иногда называется "приборным" стилем, потому что приложение, оформленное в этом стиле, выглядит как алюминиевая панель научного прибора. "Родной" стиль Java L&F реализуется классами пакета j avax. swing.plaf.metal и принимается по умолчанию в технологии Java. На сайте есть подробнейшее руководство по созданию графического интерфейса пользователя в стиле Java L&F — "Java Look and Feel Design

Guidelines". Разумеется, в этой книге мы не сможем полностью рассмотреть все возможности библиотеки Swing, но ее структуру и основные средства освоим в той мере, которая позволит создать удобное и красивое приложение, приятное для работы. Изложение библиотеки Swing в этой части книги рассчитано на то, что читатель знаком с постоянно обновляемым электронным учебником "The Java Tutorial. A practical guide for programmers", расположенным по адресу tutorial/.

Начнем с обзора готовых графических компонентов Swing.

Основные компоненты Swing

В библиотеку Swing входит около тридцати готовых графических компонентов: надписи, кнопки, поля ввода, линейки прокрутки, ползунки, меню и пункты меню, деревья, таблицы. Они собраны главным образом в пакет javax.swing. Рассмотрим их последовательно от самых простых до самых сложных компонентов. Но начнем с вершины иерархии компонентов класса JComponent.

Компонент JComponent

Основные свойства всех компонентов Swing сосредоточены в их суперклассе JComponent. Класс JComponent расширяет класс Container, входящий в графическую библиотеку AWT. Поэтому компонент JComponent и все его расширения являются контейнерами и могут содержать в себе другие компоненты. Класс Container, в свою очередь, расширяет класс Component, содержащий около сотни методов работы с компонентами. Эти методы и методы класса Container наследуются классом JComponent, который добавляет к ним добрую сотню своих методов. Все компоненты Swing расширяют класс JComponent, наследуя его богатейшие свойства.

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

В классе JComponent сосредоточена основная функциональность компонентов Swing. Перечислим некоторые возможности компонентов.

□ Для компонента JComponent и его наследников можно задать рамку методом

setBorder(Border).

□ Компонент можно сделать прозрачным или непрозрачным с помощью метода

setOpaque(boolean).

□ Фон непрозрачного компонента можно закрасить определенным цветом методом

setBackground(Color) .

□ Для любого компонента можно установить шрифт методом setFont(Font) и его цвет методом setForeground(Color).

□ Для каждого компонента создается графический контекст класса Graphics, которым можно воспользоваться для рисования фигур и линий на компоненте, обратившись к методу paint (Graphics ).

□ Можно задать определенную форму курсора мыши методом setCursor(Cursor). Такую форму курсор мыши будет принимать, когда он проходит над компонентом.

□ Каждый компонент разрешается снабдить всплывающей подсказкой, которая появится, если задержать на секунду курсор мыши над компонентом. Для этого достаточно задать текст всплывающей подсказки методом setToolTipText(String).

□ Для каждого компонента можно определить минимальный, максимальный и предпочтительный размер методами setMinimumSize(Dimension), setMaximumSize(Dimension) и setPreferredSize(Dimension) соответственно, а также собственную локаль - методом

setLocale(Locale).

□ Все компоненты отслеживают события клавиатуры KeyEvent и мыши MouseEvent, MouseWheelEvent, передачу фокуса FocusEvent, события изменения компонента

ComponentEvent и контейнера ContainerEvent.

У компонентов Swing сложное строение — они построены по схеме MVC.

Схема MVC в компонентах Swing

Конструктивная схема Модель-Вид-Контроллер (MVC, Model-View-Controller) рассмотрена нами в главе 3. Повторим вкратце ее основные понятия.

Первую часть, Model, составляет один или несколько классов, в которых хранится или вырабатывается вся информация, обрабатываемая компонентом, и текущее состояние объектов, созданных этим компонентом. Эти классы обладают методами setXXX() ввода и изменения информации.

Вторая часть - один или несколько классов, составляющих View. Эта часть компонента описывает способ представления результатов, сгенерированных Моделью, на экране дисплея, принтере или другом устройстве в определенном виде: таблица, график, диаграмма. К одной Модели можно подключить несколько Видов, по-разному представляющих одни и те же результаты или отражающие разную информацию. Виды получают информацию методами getXxx() и isXxx () Модели.

Третья часть — классы, образующие Controller, — создают интерфейс для ввода информации и изменения состояния объекта. Они реагируют на события ввода с клавиатуры, действия мыши и прочие воздействия на объект и обращаются к методам setXxx () Модели, изменяя ее поля или вызывая генерацию информации. Одна Модель может использоваться несколькими Контроллерами.

Вид и Контроллер не взаимодействуют. Контроллер, реагируя на события, обращается к методам setXxx() Модели, которые меняют хранящуюся в ней информацию. Модель, изменив информацию, сообщает об этом тем Видам, которые зарегистрировались у нее. Этот способ взаимодействия Модели и Вида получил название "подписка-рассылка" (subscribe-publish). Виды подписываются у Модели, и та рассылает им сообщения о всяком изменении состояния объекта методами fireXxx(), после чего Виды забирают измененную информацию, обращаясь к методам getXxx() и isXxx() Модели.

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

Таблица 11.1. Интерфейсы моделей и классы, реализующие их
Интерфейс Класс
BoundedRangeModel DefaultBoundedRangeModel
ButtonModel De faultButtonModel
JToggleButton.ToggleButtonModel
ComboBoxModel De faultComboBoxModel
MutableComboBoxModel
ListModel AbstractListModel
DefaultListModel
ListSelectionModel DefaultListSelectionModel
SingleSelectionModel DefaultSingleSelectionModel
ColorSelectionModel DefaultColorSelectionModel
SpinnerModel AbstractSpinnerModelSpinnerDateModelSpinnerListModelSpinnerNumberModel
TableColumnModel DefaultTableColumnModel
TableModel DefaultTableModel
TreeModel DefaultTreeModel
TreeSelectionModel DefaultTreeSelectionModel
JTree.EmptySelectionModel

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

Для реализации модели MVC библиотека Swing использует делегирование (delegation) полномочий, назначая в качестве модели данных представителя (delegate) — экземпляр класса с именем вида xxxModel. Класс, описывающий компонент, содержит защищенное или даже закрытое поле model — объект этого класса-модели, и метод getModel (), предоставляющий разработчику доступ к полю model. Сложные компоненты могут иметь несколько моделей, например в классе JTable есть три поля-представителя:

protected TableColumnModel columnModel;

protected TableModel dataModel;

protected ListSelectionModel selectionModel;

и, соответственно, три метода доступа:

TableColumnModel getColumnModel();

TableModel getModel();

ListSelectionModel getSelectionModel();

Делегирование полномочий используется и для обеспечения PL&F. Класс JComponent содержит защищенное поле ui — экземпляр класса-представителя ComponentUI из пакета javax.swing.plaf, непосредственно отвечающего за вывод изображения на экран в нужном виде. Класс-представитель содержит методы paint() и update(), формирующие и обновляющие графические примитивы. Такие представители образуют целую иерархию с общим суперклассом ComponentUI. Они собраны в пакет javax.swing.plaf и его подпакеты. В их именах есть буквы UI (User Interface), например: ButtonUI, BasicButtonUI.

Представители класса тоже являются полями класса компонента, а доступ к ним осуществляется методами вида getUI ().

Класс, описывающий компонент, дублирует большинство методов модели, например в том же классе JTable есть множество методов доступа к информации getXxx(), большинство из них просто обращаются к соответствующим методам модели, например метод получения числа строк таблицы:

public int getRowCount(){

return getModel().getRowCount();

}

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

Надпись JLabel

Различные неизменяемые надписи и небольшие изображения в окне приложения представляются компонентом JLabel. Для создания экземпляра этого класса есть шесть конструкторов.

□ Конструктор по умолчанию JLabel () выделяет прямоугольную область в контейнере без надписи и изображения, в которую потом можно поместить текст методом

setText (String) и изображение методом setIcon (Icon).

□ Конструкторы JLabel (String) и JLabel(Icon) выделяют прямоугольную область и заносят в нее строку текста — экземпляр класса String, или изображение — экземпляр класса, реализующего интерфейс Icon, обычно это класс ImageIcon. Изображение размещается в центре области, а строка в центре по вертикали и слева.

□ В конструкторах с двумя параметрами JLabel(String, int) и JLabel (Icon, int) второй параметр задает горизонтальное размещение текста или изображения константами LEFT, CENTER, RIGHT, LEADING или TRAILING интерфейса SwingConstants. Этот интерфейс реализован в классе JLabel. Понятия leading и trailing зависят от установленной ло-кали. Для языков с написанием слева направо это левая и правая сторона области, для других, например арабского языка, наоборот.

Размещение можно потом изменить методом setHorizontalAlignment(int). Можно изменить и размещение по вертикали методом setVerticalAlignment(int) с константами

TOP, CENTER или BOTTOM.

□ Последний конструктор, JLabel(String, Icon, int), задает и строку, и изображение, и размещение, при этом строка располагается справа от изображения для языков с написанием слева направо. Изменить расположение текста относительно изображения по горизонтали и вертикали можно методами setHorizontalTextPosition(int) и setVerticalTextPosition (int) с такими же константами. По умолчанию текст от изображения отделяют 4 пиксела. Изменить это расстояние можно методом

setIconTextGap(int).

Например, создание надписи и размещение ее сверху и справа в выделенной для компонента области контейнера выглядит так:

JLabel l = new JLabel("Какая-то надпись", JLabel.RIGHT);

l.setVerticalAlignment(JLabel.TOP);

Если же мы хотим разместить в компоненте JLabel текст и изображение, причем текст расположить слева от изображения, оставив между ними 10 пикселов, то надо сделать примерно так:

JLabel l = new JLabel("Надпись",

new ImageIcon("myimage.gif"), JLabel.CENTER);

l.setHorizontalTextPosition(JLabel.LEFT); l.setIconTextGap(10);

Интересно, что Swing "понимает" разметку языка HTML и в строке можно с помощью тегов менять цвет, шрифт, создавать списки, размещать текст в нескольких строках:

l.setText("Первая строка

вторая");

Внимание!

Тег должен начинать строку, идти сразу же после открывающей кавычки, без пробелов.

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

.

Еще одно интересное свойство. Компонент JLabel можно связать с другим компонентом методом setLabelFor(Component). Затем с какой-либо буквой надписи, например А, нужно связать командную клавишу методом

setDisplayedMnemonic(’A’);

или методом

setDisplayedMnemonic(KeyEvent.VK A);

Второй из этих методов требует включения в программу пакета j ava. awt. event. Буква a в надписи будет подчеркнута. После этого нажатие комбинации клавиш + вызовет передачу фокуса связанному с надписью компоненту (на компонент JLabel фокус никогда не передается). В тексте HTML буква, связанная с командной клавишей, не подчеркивается автоматически, для нее надо задать подчеркивание тегом .

Если в надписи несколько одинаковых букв, то будет подчеркнута первая из них. Методом setDisplayedMnemonicIndex (int) можно подчеркнуть букву с указанным в качестве параметра индексом. У первой буквы надписи нулевой индекс.

При переводе компонента JLabel в недоступное состояние методом setEnabled(false) текст и изображение становятся бледными. Можно при этом заменить изображение, если оно уже было в компоненте, другим изображением с помощью метода setDisabledlcon(Icon).

Остальные методы класса JLabel выполняют проверки и предоставляют сведения о компоненте, но не забывайте, что можно воспользоваться еще и методами классов JComponent, Container и Component. Достаточно просто установить цвет надписи методом setForeground(Color), шрифт- методом setFont(Font), цвет фона- методом

setBackground(Color). При этом учтите, что по умолчанию компонент JLabel прозрачен, и перед закрашиванием фона надо сделать его непрозрачным, обратившись к методу setOpaque (true). Можно обрамить компонент методом setBorder(Border). Можно задать всплывающую подсказку методом setToolTipText(String). Можно даже задать реакцию на внешние воздействия, но для этого лучше применять кнопки.

РљРЅРѕРїРєРё

Библиотека Swing предлагает целую иерархию кнопок, показанную на рис. 11.1. В нее включены и пункты меню JMenultem, и кнопки выбора JCheckBox, и радиокнопки

JRadioButton.

JComponent

AbstractButton -i-JButton

Р•

BasicArrowButton

MetalComboBoxButton

-JMenultem—JCheckBoxMenultem —JMenu

— JRadioButtonMenultem

ElToggleButton-i—JCheckBox LjRadioButton

Рис. 11.1. Иерархия классов кнопок

Во главе иерархии стоит абстрактный класс AbstractButton, содержащий методы, общие для всех типов кнопок. В нем также собраны константы, определяющие общее поведение всех кнопок.

Все кнопки типа AbstractButton реагируют на событие ActionEvent, происходящее при щелчке кнопкой мыши, событие класса ChangeEvent из пакета j avax. swing.event, возникающее при всех действиях мыши: наведении курсора мыши на компонент, удалении его с компонента, нажатии кнопки мыши и т. д., и на событие itemEvent, возникающее при смене состояния кнопки.

На любую кнопку всегда можно поместить новый текст методом setText(String) и сменить существующее изображение методом setIcon(Icon).

Как и в компоненте класса JLabel, можно методом setDisabledIcon(Icon) сменить изображение на кнопке, сделанной недоступной, т. е. на кнопке, к которой применен метод setEnabled (false). При попытке выделения недоступной кнопки можно установить на ней новое изображение методом setDisabledSelectedIcon(Icon).

Кроме того, можно сменить изображение при наведении курсора мыши на кнопку методом setRolloverIcon(Icon), но только если предварительно эта возможность включена методом setRolloverEnabled (true). По умолчанию она отключена.

Аналогично можно сменить изображение при выделении кнопки методом setSelectedIcon(Icon) , при наведении курсора мыши на выделенную кнопку методом

setRolloverSelectedIcon (Icon), при нажатии кнопки мыши setPressedIcon(Icon). Эти возможности всегда включены.

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

Командную клавишу можно назначить кнопке методом setMnemonic(int) с указанием в качестве параметра этого метода константы из класса java.awt.event.KeyEvent. Будет подчеркнута первая буква надписи, связанная с командной клавишей. Как и в классе JLabel, можно подчеркнуть не только первое появление этой буквы, но и какое-нибудь из следующих появлений с помощью метода

setDisplayedMnemonicIndex(int) .

Всплывающая подсказка для кнопки задается методом setToolTipText(String).

Кнопки типа AbstractButton используют по умолчанию модель класса DefaultButtonModel, реализующего интерфейс ButtonModel. Эта модель отслеживает пять состояний кнопки.

□ Кнопка находится в состоянии "наведенная" (rollover), когда над ней располагается курсор мыши. Контроллер отмечает это состояние методом setRollover(boolean) модели, а вид курсора определяется методом isRollover ( ).

□ В состояние "наготове" (armed) кнопка переходит при нажатии на ней кнопки мыши. Это состояние устанавливается в модели методом setArmed(boolean), а отслеживается логическим методом isArmed ( ).

□ В состояние "нажатая" (pressed) кнопка переходит из состояния "наготове" после отпускания кнопки мыши. За этим состоянием следят методы setPressed(boolean) и

isPressed().

□ После щелчка кнопка "выделяется" (selected), что отмечается методами

setSelected(boolean) Рё isSelected().

□ Наконец, кнопку можно сделать "доступной" (enabled) или "недоступной" (disabled) методом setEnabled(boolean) и отследить ее состояние методом isEnabled ( ).

Класс AbstractButton дублирует только методы setEnabled (boolean) и isEnabled(), остальные состояния надо отслеживать методами модели DefaultButtonModel, получив предварительно ее экземпляр методом getModel ( ).

На практике, разумеется, применяются не объекты класса AbstractButton, а расширения этого класса, которые мы рассмотрим подробнее. Самое небольшое расширение, реализующее все свойства кнопки AbstractButton, это компонент JButton.

РљРЅРѕРїРєР° JButton

Обычная прямоугольная кнопка — экземпляр класса JButton — может, так же как и Jlabel, содержать текст и/или изображение. Их размещение и взаимное положение не задается конструктором, а устанавливается методами

setHorizontalAlignment(int); setVerticalAlignment(int); setHorisontalTextPosition(int); setVerticalTextPosition(int); setIconTextGap(int);

точно так же, как и в компонентах Jlabel, и с теми же константами в качестве параметра этих методов. По умолчанию и текст, и изображение располагаются по центру кнопки, а изображение слева от текста.

Для создания объектов класса JButton есть пять конструкторов: конструктор по умолчанию JButton (), конструкторы кнопок с текстом JButton (String) и изображением Jbutton (Icon), конструктор с двумя параметрами JButton(String, Icon). Пятый конструктор, JButton (Action), использует объект класса, реализующего интерфейс Action.

Как прямое расширение класса AbstractButton, класс JButton наследует все его свойства и методы и к нему относится все сказанное в предыдущем разделе. С учетом этого определение кнопки может выглядеть так:

ImageIcon def = new ImageIcon("default.gif");

JButton b = new JButton(,,Далее,,, def); b.setBackground(new Color(183, 220, 65)); b.setFont(new Font("Lucida", Font.ITALIC, 12)); b.setPreferredSize(new Dimension(100, 30)); b.setMnemonic(KeyEvent.VK L);

b.setToolTipText("Переход к следующей странице"); b.setRolloverEnabled(true);

b.setRolloverIcon(new ImageIcon("rollover.gif")); b.setSelectedIcon(new ImageIcon("select.gif")); b.setRolloverSelectedIcon(new ImageIcon("rollselect.gif")); b.setPressedIcon(new ImageIcon("press.gif")); b.setDisabledIcon(new ImageIcon("disable.gif")); b.setDisabledSelectedIcon(new ImageIcon("disselect.gif")); b.setActionCommand("next"); b.addActionListener(this) ; b.addChangeListener(this); b.addItemListener(this);

Кнопка JButton реагирует на событие ActionEvent, возникающее при щелчке кнопкой мыши на компоненте, событие ChangeEvent, происходящее при всех действиях мышью на компоненте, и событие ItemEvent. Кроме того, кнопка наследует события ComponentEvent и ContainerEvent, а также события мыши и клавиатуры.

Кнопка выбора JToggleButton

Компонент JToggleButton представляет прямоугольную кнопку стандартного вида, имеющую два состояния, отмечаемые как булево значение true/false, и меняющую одно состояние на другое при щелчке кнопкой мыши на компоненте или нажатии "горячей" клавиши. Изменение L&F при смене состояния обычно заключается в том, что кнопка на экране становится "нажатой" и остается в этом состоянии до следующего щелчка кнопкой мыши. Отследить текущее состояние кнопки можно логическим методом isSelected (), установить то или другое состояние программно — методом

setSelected(boolean) .

Экземпляры класса JToggleButton создаются восемью конструкторами. Основной конструктор

JToggleButton(String, Icon, boolean);

создает кнопку с надписью, изображением и выбранным значением true или false. В других конструкторах отсутствуют какие-то из этих параметров, причем отсутствующий третий параметр считается равным false. В строке можно сделать разметку HTML. Конструктор JToggleButton(Action) использует в качестве параметра экземпляр класса, реализующего интерфейс Action.

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

Листинг 11.2. Простейшая кнопка с двумя состояниями

import java.awt.*; import java.awt.event.*; import javax.swing.*;

class DummyToggleButton extends JFrame{

private JToggleButton tb;

public DummyToggleButton(){

tb = new JToggleButton("Да?

Нет?"); tb.setMnemonic(KeyEvent.VK L); tb.setToolTipText("Сделайте выбор"); add(tb);

// Для JDK версии ранее 5.0 уберите комментарий // getContentPane().add(tb);

setSize(300,300);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE); setVisible(true);

}

public static void main(String[] args){ new DummyToggleButton();

}

}

Класс JToggleButton применяется обычно как суперкласс для создания кнопок выбора нестандартного вида. Для получения стандартных кнопок выбора используются его подклассы JCheckBox и JRadioButton.

Кнопка выбора JCheckBox

Компонент JCheckBox - это стандартная кнопка выбора с меткой или изображением слева или справа от надписи, в которой показывается состояние кнопки: true или false. Форма метки зависит от установки L&F.

Создать экземпляр класса JCheckBox можно одним из восьми конструкторов. Основной конструктор- JCheckBox (String, Icon, boolean), в других конструкторах те же парамет

ры, что и у класса JToggleButton.

Класс JCheckBox не добавляет функциональности своему суперклассу и используется точно так же, как класс JToggleButton.

Радиокнопка JRadioButton

Стандартная радиокнопка создается одним из восьми конструкторов, основной из

них- JRadioButton (String, Icon, boolean), остальные имеют те же параметры, что и

конструкторы класса JToggleButton.

Класс JRadioButton не добавляет функциональности своему суперклассу и используется точно так же, как класс JToggleButton.

Радиокнопки имеет смысл использовать только в составе группы, обеспечивая выбор одного из нескольких значений. Для получения группы сначала создается пустая группа как экземпляр класса ButtonGroup, затем она заполняется радиокнопками методом

add(AbstractButton).

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

Листинг 11.3. Группа радиокнопок

import java.awt.*; import javax.swing.*; import javax.swing.border.*;

class RadioButtonTest extends JFrame{

public RadioButtonTest(){ setBackground(Color.white); setLayout(new FlowLayout());

JPanel p = new JPanel();

p.setLayout(new BoxLayout(p, BoxLayout.X AXIS)); p.setBorder(BorderFactory.createEtchedBorder());

JRadioButton rb1 =

new JRadioButton ( "Rрасный

фон,,); rb1. setMnemoni c (KeyEvent. VK R);

rb1.setToolTipText("E^i выбираете

красный фон");

rb1.addActionListener(this); rb1.setActionCommand("red");

JRadioButton rb2 =

new JRadioButton (XhtmlXuX^u>еленый

фон,,);

rb2. setMnemoni c (KeyEvent .VK P);

rb2.setToolTipText("Вы выбираете^Хеленый фон"); rb2.addActionListener(this); rb2.setActionCommand("green");

JRadioButton rb3 =

new JRadioButton ( XhtmlXuXX/u>РёРЅРёР№

фон" ); rb3.setMnemoni c(KeyEvent.VK C);

rb3.setToolTipText("Вы выбираете

синий фон"); rb3.addActionListener(this); rb3.setActionCommand("blue");

ButtonGroup bg = new ButtonGroup(); bg.add(rb1); bg.add(rb2); bg.add(rb3);

p.add(rb1); p.add(rb2); p.add(rb3); add(p);

setSize(300, 150);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE); setVisible(true);

}

public static void main(String[] args){ new RadioButtonTest() ;

}

}

Рис. 11.2. Группа радиокнопок и всплывающая подсказка

Группу радиокнопок составляют несколько объектов класса JRadioButton, что при большом числе вариантов приводит к неоправданному расходу ресурсов. Кроме того, такая группа занимает много места в окне приложения. Для выбора из большого числа вариантов библиотека Swing предлагает создать объекты одного из двух классов: JList и JComboBox.

Упражнение

1. Перепишите листинг 10.1 с использованием компонентов Swing.

Раскрывающийся список JComboBox

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

Создать экземпляр раскрывающегося списка можно конструктором по умолчанию JComboBox ( ), а затем заносить в него элементы методами addItem(Object) и insertItemAt(Object, int).

Однако чаще бывает удобнее занести элементы в список сразу же при его создании конструктором JComboBox (Obj ect [ ] ) или JComboBox(Vector), предварительно создав массив или вектор, содержащий элементы. Например:

String[] data = {"Иванов", "Петров", "Сидоров"};

JComboBox cb = new JComboBox(data);

Если в списке будет использована модель, отличная от модели, принятой по умолчанию, то элементы сначала заносятся в нее, а затем конструктором JComboBox (ComboBoxModel) создается объект, связанный с этой моделью.

В список можно занести и изображения, например:

Object[] data = {new ImageIcon("apple.gif"), new ImageIcon("grape.gif"), new ImageIcon("pear.gif")};

JComboBox fruits = new JComboBox(data);

Есть возможность заносить в список объекты и других типов. Мы поговорим о реализации этой возможности в разд. "Визуализация элементов списков” данной главы.

После создания списка в его окне виден первый элемент. Чтобы поместить в окно какой-то другой элемент, следует обратиться к методам setSelectedItem(Object) или setSelectedIndex(int). Если в этих методах указать в качестве параметра null или -1 соответственно, то окно будет пустым. Это удобно в случае редактируемого списка, в окно которого можно вводить новый элемент или редактировать выбранный. Список становится редактируемым после выполнения метода setEditable (true). Для редактирования привлекается текстовый редактор — экземпляр класса, реализующего интерфейс ComboBoxEditor. По умолчанию используется одна из реализаций этого интерфейса — класс BasicComboBoxEditor, открывающий для редактирования поле ввода класса JTextField.

Редактирование выбранного элемента списка в окне не приводит к изменению этого элемента в списке, а влияет только на объект, возвращаемый методом getSelectedItem( ).

Текст HTML интерпретируется в элементах списка, но в окне редактируемого списка при использовании в качестве редактора объекта класса BasicComboBoxEditor появляется в "плоском" ASCII-виде и в таком же виде возвращается методом getSelectedItem( ).

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

setMaximumRowCount(int);

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

При выборе элемента или окончании редактирования (нажатии клавиши ) в раскрывающемся списке происходит событие класса ActionEvent и одно или два события класса ItemEvent, а при раскрытии и свертывании списка — событие класса PopupMenuEvent. Обработчик события может получить выбранный элемент методом

getSelectedItem (), а его индекс — методом getSelectedIndex ().

Моделью данных для класса JComboBox служит класс DefaultComboBoxModel, реализующий сразу три интерфейса: ListModel, ComboBoxModel и MutableComboBoxModel, и расширяющий класс AbstractListModel. Нет никакой необходимости в непосредственном обращении к методам этой модели, поскольку они дублируются методами класса JComboBox, за одним исключением. В модели данных класса DefaultComboBoxModel при изменении списка происходит событие класса ListDataEvent, не отслеживаемое классом JComboBox. Но эта модель не реагирует на события ActionEvent и ItemEvent.

Больше того, сам класс JComboBox зачем-то реализует интерфейсы ActionListener, EventListener, ListDataListener, но использовать его как слушателя событий нельзя.

Есть еще несколько нестыковок. В классе JComboBox элементы называются Item, например такое имя использовано в названии метода getItemAt(int). В классе DefaultComboBoxModel аналогичный метод называется getElementAt (int). Это приходится учитывать при создании собственной модели данных.

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

Список выбора JList

Вместо группы кнопок выбора можно создать список класса JList. В таком списке допустимо выбирать не только один элемент, но и группу подряд идущих элементов, и несколько таких групп. Кроме конструктора по умолчанию JList (), создающего пустой список, можно задать список с заданным массивом объектов конструктором JList (Obj ect [ ]), с заданным вектором при помощи конструктора JList(Vector) или с определенной заранее моделью JList(ListModel). Это делается так же, как и при создании экземпляра класса JComboBox.

Список типа JList выглядит на экране просто как столбец из всех своих элементов. Чтобы ограничить число видимых на экране строк и снабдить список полосой прокрутки для показа остальных строк, следует поместить список на панель типа JScrollPane. После этого можно задать число видимых строк методом setVisibleRowCount(int). C учетом всего этого определение списка выбора может выглядеть так:

JFrame f = new JFrame();

String[] data = {"Иванов", "Петров", "Сидоров"};

JList list = new JList(data);

list.setVisibleRowCount(2);

list.addListSelectionListener(this);

JScrollPane sp = new JScrollPane(list); f.getContent Pane().add(sp);

Так же, как и в раскрывающийся список JComboBox, в список JList можно занести не только текст, но и изображения.

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

setSelectionMode(ListSelectionModel.SINGLE SELECTION);

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

ListSelectionModel классом DefaultListSelectionModel. Данная реализация модели выбора применяется в классе JList по умолчанию. Если такая реализация почему-либо не устраивает разработчика, то он может реализовать интерфейс ListSelectionModel своим классом и установить созданную модель выбора методом

setSelectionModel(ListSelectionModel).

Один (первый) выбранный элемент можно получить методом getSelectedValue (), массив типа Object [] всех выбранных элементов — методом getSelectedValues (). Индекс первого выбранного элемента выдает метод getSelectedindex(), массив индексов всех выбранных элементов — метод getSelectedIndices ( ).

Кроме модели выбора- реализации интерфейса ListSelectionModel- класс JList свя

зан еще с моделью данных. Она описана интерфейсом ListModel и частично реализована абстрактным классом AbstractListModel. Класс JList использует расширение этого класса — класс DefaultListModel. Класс DefaultListModel хранит данные в закрытом поле delegate типа Vector на основе идеи делегирования и дублирует фактически все методы класса Vector, обращаясь к классу-представителю, например:

public Object getElementAt(int index){ return delegate.elementAt(index);

}

Класс JList отслеживает событие ListSelectionEvent, происходящее при смене выделенного элемента списка. Его модель данных отслеживает, кроме того, событие ListDataEvent, возникающее при всяком изменении списка.

Визуализация элементов списков

Компоненты классов JList и JComboBox могут содержать десятки и сотни элементов, имеющих тип String или Icon. Создание графического объекта для каждого элемента списка приведет к колоссальному расходу оперативной памяти и к большим затратам времени на создание объектов. Чтобы избежать этого расхода ресурсов, для изображения элементов списков назначается объект-рисовальщик. Он последовательно выводит элементы на экран или на принтер, переходя от одного элемента к другому. Короче говоря, реализуется design pattern, известный под именем Flyweight.

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

public Component getListCellRendererComponent(

JList list, // Список, элементы которого выводятся на экран

// Элемент списка, который будет выведен // Порядковый индекс этого элемента // Выбран ли этот элемент?

Object value, int index, boolean isSelected, boolean cellHasFocus

// Имеет ли фокус этот элемент?

Этот метод должен сформировать компонент и поместить в него текущий элемент списка value, имеющий порядковый номер index. Вид компонента может зависеть не только от его класса или порядкового номера, но и от того, выбран ли он isSelected (обычно выбранный элемент выделяется синим цветом фона) и имеет ли фокус cellHasFocus (обычно обводится тонкой рамкой). Полученный компонент затем выводится на экран своим методом paint ( ).

В библиотеке Swing интерфейс ListCellRenderer реализован классами BasicComboBoxRenderer и DefaultListCellRenderer, расширяющими класс JLabel. Именно потому, что выводом элементов фактически занимается класс JLabel, можно использовать в элементах списка текст или изображение. Интересный эффект получится, если смешать в одном списке типа JComboBox и текст, и изображения. Класс BasicComboBoxRenderer попытается вывести их вместе. Список типа JList, в котором текст перемежается изображениями, будет выведен правильно. Все дело в разной реализации интерфейса ListCellRenderer. Вот фрагмент реализации:

public class DefaultListCellRenderer extends JLabel implements ListCellRenderer, Serializable{

public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus){

setComponentOrientation(list.getComponentOrientation());

if (isSelected){

setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground());

}else{

setBackground(list.getBackground()); setForeground(list.getForeground());

}

if (value instanceof Icon){ seticon((Icon)value);

setText("");

}else{

seticon(null);

setText((value == null) ? "" : value.toString());

}

setEnabled(list.isEnabled()); setFont(list.getFont()); setBorder((cellHasFocus) ?

UIManager.getBorder("List.focusCellHighlightBorder”) : noFocusBorder);

return this;

}

}

Как видно из этого фрагмента, в каждый формирующийся компонент — один элемент списка JList — может быть помещено либо изображение типа Icon, либо текст типа String. Всякий другой объект будет преобразован в строку его методом toString (), в том числе и объект класса JLabel. Легко изменить эту реализацию, убрав условие if(value instanceof Icon) из приведенного фрагмента и применив унаследованные от класса JLabel методы

setText(((JLabel)value).getText()); seticon(((JLabel)value).geticon());

После этого элементами списка могут служить объекты класса JLabel. Но это еще не все. Метод getSelectedValue () по-прежнему будет возвращать строку, выдаваемую методом JLabel. toString (), а не ссылку на объект. Значит, надо еще расширить класс JLabel, переписав метод toString ( ).

Итак, если разработчику нужно создать список, содержащий объекты других типов, отличных от String и Icon, то он должен написать класс, экземпляры которого будут служить элементами списка. В данном классе следует переопределить, кроме методов getXxx()/setXxx(), метод toString(), а при необходимости и метод paint (). Экземпляры этого класса записываются в конструктор JList(Object[]) и передаются методу getListCellRendererComponent () как параметр value.

Потом следует написать свою реализацию интерфейса ListCellRenderer. Обычно она расширяет класс JLabel или JPanel.

Упражнение

2. Перепишите листинг 10.2 с использованием компонентов Swing.

Счетчик JSpinner

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

текущее значение — 0, следующее — 1, предыдущее--1, и так можно перебирать все

целые числа.

Для создания более сложного счетчика конструктором JSpinner(SpinnerModel) придется сначала определять модель данных. Она описана интерфейсом SpinnerModel, частично реализована абстрактным классом AbstractSpinnerModel и полностью реализована в трех классах. Класс spinnerDateModel реализует модель, содержащую даты, класс SpinnerListModel — модель, содержащую коллекцию типа List, в частности, массив произвольных объектов типа object[]. Класс spinnerNumberModel содержит целые или вещественные числа или объекты класса Number.

Например, следующая строка:

JSpinner sp = new JSpinner(new SpinnerNumberModel(50, 0, 100, 5));

создает счетчик с текущим значением 50, диапазоном значений от 0 до 100 и шагом изменения значений 5. В поле можно ввести любое значение из указанного диапазона, например 47, тогда предыдущее значение будет равно 42, а следующее — 52.

С помощью класса SpinnerNumberModel можно создать еще модель с вещественными числами конструктором

SpinnerNumberModel(double current, double min, double max, double step);

и числовую модель общего вида конструктором

SpinnerNumberModel(Number current, Comparable min, Comparable max, Number step);

Значения min и max могут быть null, в таком случае нижняя или верхняя границы не существуют.

Текущее, предыдущее и следующее значение можно получить от счетчика JSpinner методами getValue (), getPreviousValue() и getNextValue() соответственно. Эти методы возвращают объект класса Object. Если значения выходят за заданные в конструкторе границы, то указанные методы возвращают null.

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

sp.addChangeListener(this);

// . . . .

public void stateChanged(ChangeEvent e){ comp.setValue((int)sp.getValue());

}

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

Конструктор

SpinnerDateModel(Date value, Comparable first,

Comparable last, Date step);

задает произвольную текущую дату value, диапазон значений дат от first до last и шаг step. Если одно или оба значения диапазона равны null, то соответствующая граница отсутствует. Шаг step определяет также и форму представления даты и может принимать значения одной из констант: era, year, month, week_of_year, week_of_month, DAY_OF_MONTH, DAY_OF_YEAR, DAY_OF_WEEK, DAY_OF_WEEK_IN_MONTH, AM_PM, HOUR, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND класса Calendar.

Более широкие возможности предоставляет третья модель данных класса —

SpinnerListModel. В конструкторе SpinnerListModel(Object[] ) этого класса можно задать массив произвольных объектов, например:

String[] data = {"Дворник", "Уборщица", "Программист", "Сторож"};

SpinnerListModel model = new SpinnerListModel(data);

JSpinner emp = new JSpinner(model);

В другом конструкторе, SpinnerListModel(List), задается экземпляр коллекции, реализующей интерфейс List, например экземпляр класса Vector.

Хотя в счетчик можно заложить любые объекты, в поле будет показана только строка, полученная методом toString () текущего объекта. Это происходит потому, что редактор по умолчанию, заложенный в класс JSpinner, — это экземпляр класса

JFormattedTextField. Определяет его вложенный в JSpinner класс DefaultEditor и его подклассы DateEditor, ListEditor и NumberEditor.

Полосы прокрутки JScrollBar

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

Полосы прокрутки определяются четырьмя числами-свойствами, хранящимися в модели данных. Это наименьшее значение полосы minimum, наибольшее значение maximum, текущее значение value и шаг изменения extent. От последнего числа зависит размер видимой области компонента, связанного с полосой прокрутки, и длина ползунка на полосе прокрутки. Действия с этими числами описаны интерфейсом BoundedRangeModel. При всяком изменении данных чисел происходит событие ChangeEvent. Класс JScrollBar использует в качестве модели данных реализацию DefaultBoundedRangeModel этого интерфейса.

Полосы прокрутки создаются конструктором

JScrollBar(int orientation, int value, int extent, int min, int max);

или конструктором JScrollBar(int orientation), устанавливающим значения min = 0, max = 100, value = 0, extent = 10. Вертикальная или горизонтальная ориентация задается константами vertical или horizontal.

Все значения можно потом изменить методами setMinimum(int), setMaximum (int), setValue(int) и setExtent(int).

Текущее значение возвращается методом getValue(). Метод getUnitIncrement(int) возвращает величину перемещения на одну единицу вверх, если параметр этого метода равен —1, или вниз, если параметр равен 1. Эта величина устанавливается равной 1 при создании полосы и может быть изменена методом setUnitincrement(int). Аналогично метод getBlockincrement (int) возвращает величину перемещения вверх или вниз на один блок. Размер блока вначале равен величине extent, затем ее можно изменить методом

setBlockIncrement(int).

Полоса прокрутки реагирует на событие AdjustmentEvent, происходящее при каждом изменении ее модели данных.

Ползунок JSlider

Ползунок представляет собой линейку с указателем, которым можно установить какое-то значение value из диапазона min-max. Внутренне ползунок устроен так же, как и полоса прокрутки.

Компонент JSlider тоже использует модель данных класса DefaultBoundedRangeModel с наименьшим, наибольшим и текущим значениями, а также шагом изменения. Впрочем, можно применить другую модель, задав ее в конструкторе JSlider(BoundedRangeModel) или установив методом setModel(BoundedRangeModel) .

Основной конструктор:

JSlider(int orientation, int min, int max, int value);

В других конструкторах отсутствует тот или иной параметр, при этом устанавливается горизонтальный ползунок со значениями min = 0, max = 100, value = (min + max)/2.

Рядом с линейкой ползунка можно разметить шкалу со штрихами, отстоящими друг от друга на расстояние, устанавливаемое методом setMajorTickSpacing(int). Вначале это расстояние равно нулю. После определения расстояния шкала задается методом setPaintTicks(true) . К штрихам можно добавить числовые значения методом setPaintLabels (true). Между штрихами допускается размещение более мелких штрихов методом setMinorTickSpacing ( int). Если применить метод setSnapToTicks (true), то движок ползунка будет останавливаться только против штрихов.

Основную линейку ползунка можно убрать методом setPaintTrack(false), оставив только шкалу.

Числовые значения в шкале ставятся против каждого штриха. Методом createStandardLabels ( int incr, int start) можно изменить это правило, задав другой шаг расстановки чисел incr на шкале и другой начальный отсчет start. Затем этот шаг надо установить на шкале методом setLabelTable(Dictionary). Все это удобно делать вместе, например после определений:

JSlider sl = new JSlider();

sl.setMaj orTickSpacing(10); sl.setMinorTickSpacing(5); sl.setPaintTicks(true); sl.setPaintLabels(true);

sl.setLabelTable(sl.createStandardLabels(20, 28));

получим отмеченные значения 28, 48, 68, 88, как показано на рис. 11.3.

Рис. 11.3. Ползунок JSlider

Метод setLabelTable(Dictionary) позволяет сделать и более сложные изменения, установив в качестве меток не только числа, но и какие-то другие значения словаря типа

Di ctionary.

Внешний вид ползунка определяется абстрактным классом sliderUi. У него два расширения — классы BasicSliderUi и MultiSliderUi. На рис. 11.3 ползунок нарисован расширением класса BasicSliderUI — классом MetalSliderUI. При желании можно создать свой класс-рисовальщик, расширив один из этих классов и установив новый класс методом

setUI(SliderUi).

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

Упражнение

3. Перепишите листинг 10.4, заменив полосы прокрутки ползунками Swing.

Индикатор JProgressBar

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

JProgressBar(int orientation, int min, int max);

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

По мере выполнения процесса он должен передавать степень своего выполнения в индикатор методом setvalue (int). Это значение немедленно отражается в индикаторе. После обращения к методу setStringPainted(true) в окне индикатора появится еще число — процент выполнения процесса.

Если время выполнения процесса, связанного с индикатором, не определено, то можно перевести индикатор в неопределенный режим (indeterminate mode). Это делается методом setindeterminate(true). В этом режиме индикатор мигает, показывая, что процесс выполняется. Когда окончание процесса определится, надо занести наибольшее значение процесса в индикатор методом setMaximum(int), текущее значение методом setValue (int) и перевести индикатор в обычный режим методом setindeterminate ( false).

Внешний вид индикатора описывается абстрактным классом ProgressBarUI. У него два расширения — классы BasicProgressBarUI и MultiProgressBarUI. Стандартный вид Java L&F обеспечивается классом MetalProgressBarUi. При необходимости изменения внешнего вида индикатора следует расширить один из этих классов и установить новый вид методом setUI ( ProgressBarUI).

Индикатор может работать в отдельном окне, эту возможность предоставляет класс

ProgressMonitor.

Дерево объектов JTree

Дерево JTree располагает объекты в иерархическую структуру. Она создается только на экране, но не в оперативной памяти. На уровне 0 находится один корневой (root) объект, на уровнях 1, 2 и т. д. размещаются его потомки (child) — узловые (node) объекты, имеющие своих потомков и одного предка (parent). На самом нижнем уровне расположены листья (leaf). Это узлы, не имеющие потомков.

Для экономии ресурсов дерево не определяется рекурсивно, его узлы не являются ссылками типа JTree. Вместо этого узел описан интерфейсом TreeNode и его расширением — интерфейсом MutableTreeNode. Это расширение добавляет методы замены объекта, находящегося в узле, а также методы добавления и удаления потомков из узла. Оно реализовано классом DefaultMutableTreeNode из пакета javax.swing.tree.

Узел дерева JTree создается конструктором DefaultMutableTreeNode(Object), в котором задается ссылка на содержащийся в узле объект.

Каждый узел класса DefaultMutableTreeNode содержит ссылку на своего предка, которую можно получить методом getParent (). Если этот метод возвращает null, значит, узел корневой, но для такой проверки есть специальный логический метод isRoot ().

Узел хранит ссылки на потомков в структуре типа Vector, получить их можно многочисленными методами getXxx (). Метод getLevel () показывает уровень узла относительно корня, а метод getDepth () — количество уровней поддерева, начинающегося с данного узла. Метод insert (MutableTreeNode, int) добавляет к узлу нового потомка в позицию, указанную вторым параметром. Метод setUserObject(Object) меняет ссылку на объект, расположенную в узле.

Узлы дерева можно сделать редактируемыми методом setEditable(true). Редактор должен реализовать методы интерфейса TreeCellEditor. По умолчанию используется реализация DefaultTreeCellEditor, открывающая для редактирования окно класса JTreeField.

Дерево класса JTree создается одним из семи конструкторов. Проще всего воспользоваться конструктором JTree(TreeNode), создающим корень дерева, а затем создавать и добавлять к дереву новые узлы.

Например:

DefaultMutableTreeNode root = new DefaultMutableTreeNode("KopeHb");

JTree tr = new JTree(root);

DefaultMutableTreeNode subtreel = new DefaultMutableTreeNode("Y3en 1"); root.add(subtree1);

subtree1.add(new DefaultMutableTreeNode('^HCT 2a"));

DefaultMutableTreeNode subtree2 = new DefaultMutableTreeNode("Y3en 2"); subtree1.add(subtree2);

subtree2.add(new DefaultMutableTreeNodeCVHHCT 3a")); subtree2.add(new DefaultMutableTreeNodeCVHHCT 3b")); subtree2.add(new DefaultMutableTreeNodeCVHHCT 3c"));

subtree1.add(new DefaultMutableTreeNode(YnucT 2b"));

root.add(new DefaultMutableTreeNode('^HCT 1"));

// Рё С‚. Рґ....

Для простоты каждый узел в этом дереве содержит строку класса String. Полученное дерево показано на рис. 11.4.

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

toString().

Второй способ — сформировать вектор узлов дерева и воспользоваться конструктором JTree (Vector). Поддеревья создаются вложенными векторами. Вот как будет создано предыдущее дерево:

Vector root = new Vector();

Рис. 11.4. Дерево класса JTree

Vector subtree1 = new Vector(); root.add(subtree1);

subtreel.add("Лист 2a");

Vector subtree2 = new Vector();

subtree1.add(subtree2); subtree2.add("Лист 3a"); subtree2.add("Лист 3b"); subtree2.add("Лист 3c"); subtree1.add('Vn^cT 2b");

root.add('^cT 1");

JTree tr = new JTree(root);

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

Второй недостаток можно устранить использованием конструктора JTree(Hashtable), аргумент которого — хеш-таблица. Вот как будет создано предыдущее дерево этим способом:

Hashtable root = new Hashtable();

Hashtable subtree1 = new Hashtable();

root.put("РЈР·eР» 1", subtreel);

Hashtable subtree2 = new Hashtable(); subtree1.put("Лист 2a", new Integer(21)); subtree1.put(,,Узeл 2", subtree2); subtree2.put("Лист 3a", new Integer(31)); subtree2.put("Лист 3b", new Integer(32)); subtree2.put("Лист 3c", new Integer(33)); subtree1.put("Лист 2b", new Integer(22));

root.put("Лист 1", new Integer(1));

JTree tr = new JTree(root);

Из каждой пары "ключ — значение", хранящейся в хеш-таблице, ключ выводится на экран в виде строки. У этого дерева тоже нет корня. Кроме того, его узлы выводятся не в том порядке, в котором помещаются в хеш-таблицу, т. к. она "перемешивает" свои данные.

Дерево с большим числом узлов удобно поместить на панель типа JScrollPane и указать число строк, помещающихся в окне, методом setVisibleRowCount(int), например:

JTree tr = new JTree(root);

JScrollPane sp = new JScrollPane(tr); tr.setVisibleRowCount(8);

В дереве можно выделить один или несколько узлов. Все узлы дерева пронумерованы сверху вниз, начиная от корневого узла, имеющего номер 0. Метод getSelectionRows ( ) возвращает массив типа int [ ] номеров выделенных узлов. Говоря точнее, нумерация узлов дерева описана интерфейсом RowMapper. В пакете javax.swing.tree есть абстрактный класс AbstractLayoutCache, реализующий этот интерфейс, и два его подкласса:

FixedHeightLayoutCache Рё VariableHeightLayoutCache.

Иногда удобнее получить путь к узлу в виде последовательности объектов, хранящихся в узлах, ведущих от корня к данному узлу. Такая последовательность хранится в классе TreePath. Метод getSelectionPath() возвращает экземпляр этого класса, метод getSelectionPaths () возвращает массив таких экземпляров для всех выделенных узлов. Есть и другие аналогичные методы получения экземпляров класса TreePath для различных узлов дерева.

Получив экземпляр класса TreePath, можно выбрать из него массив типа Object[] объектов, содержащихся в узлах, методом getPath(). Первый элемент этого массива всегда содержит объект, хранящийся в корне. Другие методы класса TreePath позволяют получить отдельные элементы этого массива.

Вся информация о выделенных элементах дерева находится в модели выбора, описанной интерфейсом TreeSelectionModel и реализованной классом DefaultTreeSelectionModel. Данный класс хранит массив типа TreePath [ ] путей к выделенным узлам и методы работы с этим массивом. Он предлагает три готовые модели выбора: выбор только одного узла дерева single_tree_selection, выбор диапазона узлов contiguous_tree_selection и выбор нескольких диапазонов discontiguous_tree_selection. Последний выбор принимается по умолчанию. Изменить модель выбора в дереве tr можно так:

tr.getSelectionModel().setSelectionMode(

TreeSelectionModel.SINGLE_TREE_SELECTION);

Модель выбора реагирует на событие класса TreeSelectionEvent, происходящее при всяком новом выборе узлов. В отличие от большинства классов событий, класс TreeSelectionEvent содержит несколько полезных при обработке события методов, например метод getPaths ( ) выдает массив типа TreePath [ ].

Вся информация об узлах дерева хранится в модели данных, описанной в интерфейсе TreeModel. Этот интерфейс не описывает класс узлов дерева. Узел может быть экземпляром любого класса. Но реализация DefaultTreeModel, принятая в классе JTree по умолчанию, уже требует, чтобы узел имел интерфейс TreeNode.

Модель данных реагирует на событие класса TreeModelEvent, происходящее при изменении узлов дерева. Этот класс события позволяет получить полезные данные о дереве, в частности, метод getTreePath() возвращает в виде объекта класса TreePath путь к предку тех узлов, которые были изменены, добавлены или удалены.

Подобно классу JList дерево типа JTree делегирует визуализацию классу-представителю, реализующему интерфейс TreeCellRenderer. Интерфейс описывает один метод:

Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus);

Как видно из этого описания, изображение узла value дерева tree может меняться в зависимости от его порядкового номера в дереве row и от того, выбран ли этот узел selected, раскрыта ли соответствующая ветвь дерева expanded, является ли узел листом leaf и имеет ли узел фокус hasFocus.

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

В состав Java SE JDK входит пример построения дерева шрифтов, расположенный в каталоге $JAVA_HOME/demo/jfc/SampleTree/. В нем показано, как можно изменить стандартную визуализацию дерева.

Построение меню средствами Swing

Все компоненты, составляющие меню в библиотеке Swing: строка меню, пункты меню, всплывающее меню, имеют один и тот же тип, описанный интерфейсом MenuElement. Это позволяет объединять элементы какого-либо подменю или все меню, находящиеся в линейке меню, в массив с помощью метода getSubElements() и рассматривать их как одно целое.

Для создания системы меню сначала следует создать строку меню и установить ее в контейнер типа JFrame, JApplet, JDialog методом setJMenuBar(JMenuBar). Этот метод располагает строку меню горизонтально ниже строки заголовка окна.

Строка меню JMenuBar

Строка меню создается единственным конструктором класса JMenuBar(). Полученная строка не содержит ни одного меню, их надо добавлять методом add(JMenu) по мере создания. Добавляемые меню будут располагаться слева направо в порядке обращения к методам add (JMenu). В некоторых графических системах меню Справка (Help) располагается справа. Чтобы учесть эту особенность, в класс JMenuBar включен специальный метод setHelpMenu (JMenu). Впрочем, этот метод реализован далеко не во всех выпусках JDK.

Начнем создавать примерное меню:

JFrame f = new JFrame("npnMep системы меню");

JMenuBar mb = new JMenuBar()); f.setJMenuBar(mb);

JMenu file = new JMenu("0aRn"));

JMenu edit = new JMenu("npaBKa"));

JMenu view = new JMenu("BHJ"));

JMenu help = new JMenu("CnpaBKa"));

mb.add(file); mb.add(edit); mb.add(view); mb.add(help);

Меню JMenu

Каждое меню по существу представляет собой два компонента: "кнопку" с текстом и всплывающее меню типа JPopupMenu, появляющееся при щелчке кнопкой мыши по этой "кнопке". Как видно из рис. 11.1, меню JMenu относится к типу кнопок, расширяющих класс AbstractButton. Кроме того, класс JMenu непосредственно расширяет класс пункта меню JMenultem. Следовательно, объект класса JMenu может служить пунктом какого-то другого меню, образуя таким образом подменю.

Меню создается конструктором JMenu(String). Второй конструктор JMenu (String, boolean) создает плавающее (tear-off) меню, если второй параметр равен true. Это возможно не во всех графических системах.

Вновь созданное меню не содержит ни одного пункта. Пункты меню добавляются один за другим методом add(JMenuitem) или методом add(string). Интересно, что эти методы возвращают ссылку на объект класса JMenultem, а второй метод сам создает такой объект. Еще один метод, add(Component), добавляет к меню произвольный компонент. Это означает, что пунктом меню может служить любой компонент, но для встраивания в систему меню он должен реализовать интерфейс MenuElement. Например, иногда пунктом меню служит раскрывающийся список JComboBox. Но чаще среди пунктов меню встречаются экземпляры подклассов класса JMenultem: подменю — объекты класса JMenu, кнопки выбора класса JCheckBoxMenuitem и радиокнопки класса

JRadioButtonMenultem.

В меню можно отделить одну группу пунктов от другой горизонтальной чертой с помощью метода addSeparator( ).

Пункты меню, включая разделительную черту, нумеруются сверху вниз, начиная от нуля. Методы insert(JMenuItem, int), insert(String, int) и add(Component, int) позволяют вставить новый пункт в указанную вторым параметром позицию, а метод insertSeparator (int) вставляет горизонтальную разделительную черту в указанную позицию.

Методы remove (Component), remove (int), remove (JMenultem) и removeAll ( ) удаляют пункты из меню. В сочетании с методами add() и insert () они позволяют динамически перестроить меню при изменении содержимого окна.

Меню, как и всякой кнопке, можно назначить командную клавишу методом setMnemonic (int). Добавим командные клавиши-акселераторы к меню нашего примера:

file.setMnemonic(KeyEvent.VK A); edit.setMnemonic(KeyEvent.VK G); view.setMnemonic(KeyEvent.VK D); help.setMnemonic(KeyEvent.VK C);

Меню реагирует на событие класса MenuEvent, происходящее при раскрытии, выборе пунктов и закрытии меню.

Пункт меню JMenuItem

Класс JMenultem расширяет класс AbstractButton и поэтому во многом наследует поведение кнопки. При создании пункта меню в нем можно задать текст

JMenultem (String), изображение JMenultem(Icon) или сразу и то и другое конструктором JMenultem(String, Icon). Взаимное положение текста и изображения можно отрегулировать так же, как это делалось для кнопки.

Добавим пункты и разделительную черту к меню Файл нашего примера. Забегая вперед, добавим и методы обработки событий. Объяснение этих методов будет дано в главе 15.

JMenu nw = new JMenu("Создать");

file.add(nw); // Добавляем как подменю

nw.addC'OaRn"); // Пункты подменю

nw.add("Сообщение"); nw.add("Образ");

JMenultem open = file.add("OTKpbiTb...");

JMenultem close = file.add("3aKpbiTb"); file.addSeparator();

// Другой способ:

JMenultem exit = new JMenuItem("Выход"); file.add(exit);

// Обрабатываем события:

open.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){

JFileChooser fch = new JFileChooser(); fch.showOpenDialog(null);

}

});

exit.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){

System.exit(0);

}

});

При создании пункта меню, содержащего текст, можно сразу же задать командную клавишу-акселератор, используя конструктор JMenuitem(string, int). Потом это сделать не удастся, поскольку метод setMnemonic(int) не реализован в классе JMenultem, точнее говоря, он переопределен так, что только выбрасывает исключение. Назначать командную клавишу следует специальным методом setAccelerator(KeyStroke), при этом в пункт меню добавляется описание командной клавиши, например Ctrl+O. Добавим эту командную клавишу к пункту Открыть нашего меню Файл:

open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK O, Event.CTRL MASK));

Класс JMenultem отслеживает, кроме унаследованных событий ChangeEvent, ActionEvent, itemEvent, еще и события классов MenuKeyEvent, происходящие при нажатии и отпускании клавиш, и MenuDragMouseEvent, происходящие при прохождении курсора мыши над пунктом меню.

Пункт меню JCheckBoxMenuItem

Вставить в меню кнопки выбора удобнее всего с помощью специально включенного в библиотеку Swing класса JCheckBoxMenuItem. Этот класс наследует все свойства своего суперкласса JMenultem и добавляет логический метод getstate (), позволяющий отследить состояние кнопки. Впрочем, можно пользоваться и унаследованным методом

isSelected().

Добавим кнопки выбора к меню Вид нашего примера:

JCheckBoxMenuItem cbml = new JCheckBoxMenuItem("Текст");

JCheckBoxMenuItem cbm2 = new JCheckBoxMenuItem("Знaчки");

JCheckBoxMenuItem cbm3 = new JCheckBoxMenuItem("Р РёСЃСѓРЅРєРё");

view.add(cbml); view.add(cbm2); view.add(cbm3);

view.addSeparator();

cbm1.addItemListener(new ItemListener(){ public void itemStateChanged(ItemEvent e){

if (e.getStateChange() == ItemEvent.SELECTED) ch.setText(txt); else ch.setText("");

}

});

Пункт меню JRadioButtonMenuItem

Для размещения в меню группы радиокнопок удобно воспользоваться классом JRadioButtonMenultem. Он не добавляет функциональности своему суперклассу JMenultem и используется точно так же, как в обычной группе радиокнопок.

Добавим группу радиокнопок к меню Вид нашего примера:

JRadioButtonMenultem rbml =

new JRadioButtonMenuItem("Большой");

JRadioButtonMenultem rbm2 =

new JRadioButtonMenuItem("Средний");

JRadioButtonMenultem rbm3 =

new JRadioButtonMenuItem("Мaлый");

view.add(rbml);view.add(rbm2); view.add(rbm3);

ButtonGroup bg = new ButtonGroup();

bg.add(rbml); bg.add(rbm2); bg.add(rbm3);

Всплывающее меню JPopupMenu

Всплывающее меню (pop-up menu) используется обычно как контекстное меню. Оно появляется в MS Windows и Java L&F при отпускании правой кнопки мыши. В некоторых графических системах для появления контекстного меню надо нажать

среднюю кнопку мыши. Контекстное меню обычно содержит перечень действий, доступных в компоненте, над которым находится курсор мыши, при данном положении курсора мыши. Поэтому контекстное меню не привязывается к строке меню или другому меню, а соотнесено с одним или несколькими компонентами. Оно добавляется к компонентам при обработке событий мыши класса MouseEvent.

Для того чтобы отследить событие, при наступлении которого в данной графической системе следует вызвать всплывающее меню, в классе MouseEvent есть логический метод isPopupTrigger (). К этому методу следует обращаться при всяком событии мыши. Когда метод isPopupTrigger ()вернет true, показав тем самым, что надо вызывать всплывающее меню, следует обратиться к методу show(Component, int, int). В нем первый параметр — это компонент, над которым появится окно всплывающего меню, второй и третий параметры — координаты курсора мыши в системе координат этого компонента. Вот стандартная конструкция, в которой popup — это экземпляр класса JPopupMenu:

public void processMouseEvent(MouseEvent e){ if (e.isPopupTrigger())

popup.show(e.getComponent(), e.getX(), e.getY()); else super.processMouseEvent(e);

}

При этом следует убедиться, что компонент, для которого вызывается всплывающее меню и в котором записан приведенный ранее метод processMouseEvent(), отслеживает события мыши, т. е. к нему присоединен методом addMouseListener( ) хотя бы один слушатель или в нем есть обращение к методу enableEvents (AWTEvent. MOUSE_EVENT_MASK).

Действия со всплывающим меню похожи на действия с обычным меню. Сначала создается пустое меню конструктором JPopupMenu ( ) или JPopupMenu (String). Строка, записанная во втором конструкторе, должна быть заголовком меню, но она отображается на экран не всеми графическими системами. Эту строку можно добавить потом методом setLabel (String). Затем в созданное всплывающее меню методами add(Action), add(JMenultem) или add(String) добавляются пункты меню. Методы insert ( ) и remove() позволяют динамически перестроить меню.

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

Панель выбора цвета JColorChooser

Многие приложения, связанные с рисованием, обработкой текстов и изображений, требуют задания определенных цветов. Библиотека Swing предлагает простой класс JColorChooser, предоставляющий панель с палитрами цветов в моделях RGB и HSB. Есть три способа использования этого класса.

Самый простой способ — создать диалоговое окно, содержащее панель выбора цветов статическим методом showDialog(Component, String, Color). Первый параметр этого метода задает окно верхнего уровня, в которое помещается панель. Значение null указывает, что окном будет служить экземпляр класса Frame, располагающийся в центре экрана. Второй параметр дает заголовок этому окну, а третий параметр задает начальный цвет, причем null устанавливает белый цвет. Например:

Color c = JColorChooser.showDialog(null, "Цвет", null);

На экране появляется модальное диалоговое окно с цветовой палитрой, ползунками, задающими интенсивность цвета, и кнопками OK, Cancel и Reset. Метод showDialog () возвращает выбранный цвет после щелчка по кнопке OK и null, если щелчок был сделан по кнопке Cancel. После этого диалоговое окно удаляется с экрана, но не уничтожается. Оно запоминает выбранный цвет и при следующем выборе этот цвет можно восстановить щелчком по кнопке Reset. При первом появлении окна на экране щелчок по кнопке Reset возвращает начальный цвет, заданный третьим параметром метода.

Более гибкий и сложный способ — создать диалоговое окно с панелью цветов статическим методом createDialog(Component, String, boolean, JColorChooser, ActionListener, ActionListener). Этот метод, кроме окна верхнего уровня и его заголовка, позволяет задать третьим параметром модальность диалогового окна. Четвертый параметр обеспечивает выбор цвета не только экземпляра класса JColorChooser, но и любого расширения этого класса. Пятый и шестой параметры позволяют задать нестандартную обработку щелчков по кнопкам OK и Cancel соответственно. При этом выбранный цвет можно получить методом getColor (). Например:

JDialog d = JColorChooser.createDialog( new JFrame(), "Выбор цвета", false, cc = new JColorChooser(), new OkColor(), new CancelColor());

d.setVisible(true);

class OkColor implements ActionListener{

public void actionPerformed(ActionEvent e){ comp.setColor(cc.getColor());

}

}

class CancelColor implements ActionListener{

public void actionPerformed(ActionEvent e){ comp.setColor(defColor) ;

}

}

Третий способ — если нет необходимости создавать отдельное диалоговое окно, а нужна только панель выбора цветов, то можно воспользоваться конструкто-ром JColorChooser (Color), в котором задается начальный цвет, или конструктором JColorChooser (), устанавливающим белый цвет в качестве исходного.

В этом случае определять момент окончания выбора цвета придется самостоятельно. Класс JColorChooser отслеживает только событие PropertyChangeEvent изменения свойств Java Bean, которое можно обработать, присоединив обработчик унаследованным от класса JComponent методом

addPropertyChangeListener(PropertyChangeListener);

Чтобы отследить выбор цвета, можно также обратиться к модели данных, все интерфейсы и классы которой собраны в пакет j avax. swing. colorchooser.

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

getSelectionModel ( ). Эта модель отслеживает событие ChangeEvent.

При необходимости изменения модели данных можно сделать другую реализацию интерфейса ColorSelectionModel и установить ее при создании объекта конструктором

JColorChooser(ColorSelectionModel).

Упражнение

4. Перепишите "рисовалку" листинга 10.9 или листинга 10.10 с использованием компонентов Swing.

Окно выбора файла JFileChooser

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

Для создания экземпляра окна выбора файла есть несколько конструкторов. Конструктор JFileChooser (File) или JFileChooser(String) создает окно, в котором показан каталог с указанным файлом. Конструктор по умолчанию JFileChooser() открывает окно с начальным каталогом пользователя. Он соответствует JFileChooser(null). Еще в трех конструкторах задается объект класса FileSystemView, позволяющий получить различные атрибуты файла.

По умолчанию окно показывает только файлы (режим files_only). Перед выводом окна на экран можно установить режим показа только каталогов directories_only или файлов и каталогов files_and_directories. Эти режимы устанавливаются методом

setFileSelectionMode(int).

По умолчанию окно не отображает скрытые (hidden) файлы. Чтобы задать их показ, надо обратиться к методу setFileHidingEnabled (false).

По умолчанию в окне можно отметить один файл. Возможность выбора нескольких файлов задается методом setMultiSelectionEnabled(true).

Фильтр файлов FileFilter

По умолчанию окно показывает все файлы в выбранном каталоге. Установив фильтр, можно ограничить отображение отдельными файлами. Для этого надо расширить абстрактный класс FileFilter из пакета javax.swing.filechooser (не перепутайте с интерфейсом FileFilter из пакета java.io) и установить полученный фильтр в окне выбора файла методом addChoosableFileFilter (FileFilter). Этот метод можно применить несколько раз с разными параметрами, определив несколько фильтров в одном окне.

Класс FileFilter содержит всего два абстрактных метода. Логический метод accept (File) возвращает true, если его параметр следует показать в окне. Метод getDescription() возвращает строку описания данного фильтра, которая будет отображена в поле Тип файлов (Files of type) окна выбора.

Вот пример фильтра, отбирающего только файлы с расширением java. Вид окна открытия файла с соответствующим фильтром показан на рис. 11.5.

class JavaFileFilter extends javax.swing.filechooser.FileFilter{ public boolean accept(File f){ if (f != null){

String name = f.getName();

int i = name.lastIndexOf(’.’);

if (i>0 && i < name.length() — 1)

return name.substring(i + 1).equalsIgnoreCase("j ava");

}

return false;

}

public String getDescription(){ return "Файлы Java";

}

}

Рис. 11.5. Окно открытия файла

После создания экземпляра окна выбора и установки режимов и фильтров окно можно показать на экране как модальное окно открытия файла методом showOpenDialog(Component), как модальное окно сохранения файла showSaveDialog(Component) или как модальное окно произвольного выбора showDialog(Component, string). В последнем методе второй параметр задает произвольную надпись вместо надписи Открыть (Open) или Сохранить как (Save as). Первый параметр этих методов задает компонент, от которого зависит и над контейнером которого будет расположено окно выбора файла. Чаще всего это Frame, который можно задать просто параметром null.

Итак, создать и показать окно выбора файла очень просто:

JFileChooser fch = new JFileChooser();

fch.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); fch.setFileHidingEnabled(false);

fch.setMultiSelectionEnabled(true); fch.addChoosableFileFilter(new JavaFileFilter()); fch.addChoosableFileFilter(new AnotherFileFilter());

switch (fch.showDialog(null, "Открыгть")) { case JFileChooser.APPROVE OPTION:

File selectedFile = fch.getSelectedFile();

File directory = fch.getCurrentDirectory(); break;

case JFileChooser.CANCEL_OPTION: break;

case JFileChooser.ERROR OPTION:

System.err.println("Error"); break;

}

Как получить выбранный файл

Как видно из этого примера, методы вида showXxxDialog () возвращают целое число — одну из трех констант, соответствующих выбору файла и щелчку по кнопке Открыть (Open) или Сохранить (Save) — константа approve_option, щелчку по кнопке Отмена (Cancel) — константа cancel_option или появлению ошибки — константа error_option.

После того как пользователь отметил файл в окне выбора, этот файл можно получить методом getSelectedFile () в виде экземпляра класса File, как показано ранее. Если установлен режим выбора и файлов, и каталогов — files_and_directories, — то при выборе каталога этот метод возвращает null. Если же установлен режим выбора только каталогов directories_only, то возвращается каталог. Если задан выбор нескольких файлов, то их массив типа File[] можно получить методом getSelectedFiles(). Каталог, в котором находится выбранный файл, можно получить в виде экземпляра класса File методом getcurrentDirectory(), как показано ранее.

Итак, стандартная работа с окном выбора файлов выглядит следующим образом:

JFileChooser fch = new JFileChooser(); int state = fch.showOpenDialog(null);

File f = fch.getSelectedFile();

if (f != null && state == JFileChooser.APPROVE OPTION)

JOptionPane.showMessageDialog(null, f.getPath()); else if (state == JFileChooser.CANCEL OPTION)

JOptionPane.showMessageDialog(null, "Canceled");

Разумеется, окно выбора файла не открывает и не сохраняет файл на самом деле, оно только предоставляет этот файл вызвавшей окно выбора программе.

Дополнительный компонент

Возможности окна выбора файла легко расширить, добавив в него произвольный компонент методом setAccessory(JComponent). Чаще всего добавляется небольшое окно предварительного просмотра отмеченного файла, хотя можно добавить что угодно, да-

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

class ImagePreviewer extends Jlabel implements PropertyChangeListener{

public ImagePreviewer(JFileChooser fch){ if (fch == null)

throw new IllegalArgumentException("fileChooser must be non-null"); fch.addPropertyChangeListener(this);

}

public void loadImageFromFile(File f){

Icon icon = null; if (f != null){

ImageIcon im = new ImageIcon(f.getPath());

Dimension size = getSize();

if (im.getIconWidth() != size.width)

icon = new ImageIcon(im.getImage().getScaledInstance(

size.width, size.height, Image.SCALE DEFAULT)); else icon = im;

}

setIcon(icon);

}

public void propertyChange(PropertyChangeEvent e){

String prop = e.getPropertyName();

if (prop.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)){

File f = (File)e.getNewValue(); if (isShowing()){

loadlmageFromFile(f); repaint();

}

}

}

}

После определения объекта класса JFileChooser, но до его вывода на экран, присоединяем этот компонент:

JFileChooser fch = new JFileChooser();

ImagePreviewer ip = new ImagePreviewer(fch); ip.setPreferredSize(new Dimension(200, 200)); fch.setAccessory(ip);

Получившееся окно открытия файла показано на рис. 11.5.

Замена изображений

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

жения методом setFileView(FileView). Например, выведем перед исходными файлами Java изображение дымящейся чашечки кофе:

class JavaFileView extends FileView{

public String getName(File f){

return null; // Оставляем системную реализацию

}

public String getDescription(File f){

String ext = getExtension(f); if (ext != null && ext.equals("java")) return "Исходным файл Java"; else return null;

}

public String getTypeDescription(File f){ return getDescription(f);

}

Icon icon = new ImageIcon("javacup.gif");

public Icon getIcon(File f){

String ext = getExtension(f); if (ext != null && ext.equals("java")) return icon; return null;

}

public Boolean isTraversable(File f){

return null; // Оставляем системную реализацию

}

protected String getExtension(File f){ if (f != null){

String name = f.getName();

int i = name.lastIndexOf(’.’);

if (i > 0 && i < name.length() — 1)

return name.substring(i + 1).toLowerCase();

}

return null;

}

}

Теперь достаточно к описанию объекта fch класса JFileChooser добавить строку

fch.setFileView(new JavaFileView());

и стандартный значок перед исходными файлами Java с расширением java будет заменен изображением из файла javacup.gif, как показано на рис. 11.5.

Более содержательный пример создания окна выбора файла имеется в составе J2SE JDK в каталоге $JAVA_HOME/demo/jfc/FileChooserDemo/.

Окно выбора файлов чаще всего создается в отдельном диалоговом окне, но это обычный компонент Swing, и его можно поместить в контейнер. При этом можно убрать кнопки выбора и отмены выбора методом setControlButtonsAreShown(boolean).

Русификация Swing

Как видно из примеров этой главы, компоненты библиотеки Swing правильно отображают кириллицу по обычным правилам, изложенным в главе 5. Остается лишь русифицировать стандартные надписи, что видно на рис. 11.6. Для этого надо написать несколько файлов ресурсов (properties), которые, если они есть, просматриваются классами-рисовальщиками компонентов перед выводом на экран. Эти файлы уже написаны Сергеем Астаховым и лежат в файле swing_ru.jar, ссылку на который можно найти, например, по адресу . Достаточно поместить архив swing_ru.jar, не распаковывая, в каталог $JAVA_HOME/lib/ext/, и все надписи будут сделаны по-русски, как показано на рис. 11.6.

Рис. 11.6. Русифицированное окно открытия файла

На том же сайте: , да и на других страницах Интернета находится статья Сергея Астахова "Русские буквы и не только...", в которой собрано все касающееся русификации Java. По всем вопросам, возникающим при работе с кириллицей, безусловно, нужно обращаться к этому ресурсу.

Вопросы для самопроверки

1. Чем отличаются компоненты Swing от компонентов AWT?

2. Какая конструктивная схема использована в компонентах Swing?

3. В каких случаях приходится изменять Модель компонента?

4. В каких случаях приходится изменять Вид компонента?

5. Какими компонентами Swing можно создать кнопку с двумя состояниями?

6. Какие компоненты Swing создают радиокнопки?

7. Как в Swing показать каталоги и имена файлов в виде дерева?

8. Можно ли средствами Swing создать всплывающее меню?

ГЛАВА 12

 

Текстовые компоненты

При создании приложения с графическим интерфейсом очень часто приходится использовать поля для ввода текста. Такое поле может состоять из одной или нескольких строк, быть редактируемым или не редактируемым. В редактируемом окне часто приходится задавать возможность смены шрифта, цвета, вставку дополнительных символов. Библиотека Swing предоставляет для этого большие возможности, предлагая пакеты интерфейсов и классов javax.swing.text, javax.swing.text.html, j avax. swing.text. html .parser и j avax. swing. text. rtf.

На вершине иерархии текстовых компонентов стоит класс JTextComponent — непосредственное расширение класса JComponent. Это абстрактный класс, вобравший в себя общие свойства всех текстовых компонентов. У него три расширения: однострочное текстовое поле JTextField, многострочная текстовая область JTextArea и небольшой, но мощный текстовый редактор JEditorPane, имеющий расширение- класс JTextPane, ме

тодами которого можно оформить текст в каком-то определенном стиле.

У класса JTextField есть два расширения- поле для ввода пароля JPasswordField, в ко

тором вместо вводимых символов показывается один заранее определенный символ, по умолчанию звездочка, и поле для редактирования форматированных объектов JFormattedTextField, например даты — объекта класса Date, или чисел, заданных в определенном формате.

Рассмотрим текстовые компоненты подробнее и начнем с вершины их иерархии.

 

Компонент JTextComponent

Абстрактный класс JTextComponent стоит на вершине иерархии текстовых компонентов и содержит их общие свойства.

Текстовые компоненты построены по схеме "Model-View-Controller", которая отражена во внутреннем устройстве класса JTextComponent. Рассмотрим это устройство подробнее. Начнем с модели данных текстовых компонентов.

Модель данных — документ

Модель данных схемы MVC текстовых компонентов описана интерфейсом Document и называется документом. Документ может быть простым, "плоским", или сложным, структурированным. Интерфейс Document описывает простой текст (content), содержащийся в компоненте, как последовательность символов Unicode. Количество символов в тексте можно узнать методом getLength(). Последовательность символов нумеруется, начиная от нуля. Каждый символ имеет свой порядковый номер, называемый позицией (position, location или offset) символа в тексте. Точнее говоря, модель данных задает позицию не символа, а позицию между символами, перед текущим символом текста, что удобно для вставки текста.

Если текст документа сложный, структурированный, то, чтобы скрыть сложность определения текущей позиции при частых вставках и удалениях текста, удобно воспользоваться объектом, описанным интерфейсом Position. Экземпляр класса, реализующего интерфейс Position, создается методом createPosition (int). Методы getStartPosition( ) и getEndPosition() возвращают начальную и конечную позиции текста в виде объекта, реализующего интерфейс Position. В интерфейсе Position всего один метод — getOffset (), возвращающий позицию символа в виде целого числа типа int. Кроме того, в интерфейсе Position есть вложенный класс Bias, в котором определены два экземпляра класса Bias: поле Forward и поле Backward. Они уточняют символ, позиция которого определена: текущий или предыдущий. Позиция символа используется, например, при получении текста методами

String getText(int offset, int length);

void getText(int offset, int length, Segment text);

извлекающими из документа фрагмент текста длиной length символов, начиная от позиции offset. Второй метод заносит извлеченный фрагмент в экземпляр text класса Segment. Опишем этот небольшой, но удобный класс.

Строка символов Segment

Класс Segment представляет строку символов в виде, удобном для быстрого просмотра. У строки класса Segment всегда есть текущий символ, который можно получить методом current ( ). Позицию текущего символа можно узнать методом getIndex (), а установить — методом setIndex (int). Класс Segment содержит методы previous () и next (), возвращающие предыдущий и следующий символ строки, а также методы first () и last (), возвращающие первый и последний символ строки. Будьте внимательны: эти четыре метода меняют текущую позицию строки!

Интересно, что для быстроты доступа строка класса Segment хранится в открытом (public) поле — массиве с именем array типа char [ ], начиная с индекса offset, и занимает в этом массиве count элементов. Это открытые поля, так что к массиву символов можно обращаться напрямую и менять его элементы, хотя при этом теряется главное назначение класса Segment- быстро просматривать текст. Метод getBeginIndex() воз

вращает индекс начала строки в массиве array, т. е. число offset, а getEndIndex( ) индекс элемента массива, следующего за последним символом строки.

Заканчивая обзор класса Segment, скажем, что для большей надежности выполнения метода getText(int, int, Segment) в класс Segment введен метод setPartialReturn(boolean). Если в этом методе задать параметр true, то передача текста методом getText () в экземпляр класса Segment будет происходить, по возможности, без дополнительного копирования. Значение параметра по умолчанию — false. С учетом этого, работа с классом Segment начинается примерно так:

Segment seg = new Segment(); seg.setPartialReturn(true); doc.getText(0, doc.getLength(), seg);

// Работаем с объектом seg...

Запись текста в документ

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

void insertString(int offset, String text, AttributeSet attr);

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

Атрибуты текста

Как видно из сигнатуры метода insertString(), у вносимого текста могут быть атрибуты, например: имя шрифта, размер шрифта, цвет. Если у текста нет атрибутов, то третьему параметру метода надо дать значение null.

Атрибуты записываются в виде пар "имя — значение" в объект, реализующий интерфейс AttributeSet. Этот интерфейс описывает неизменяемое множество атрибутов, в нем не описаны методы добавления и удаления атрибутов. Такие методы внесены в его расширение — интерфейс MutableAttributeSet. В библиотеке Swing есть реализация данного интерфейса — класс SimpleAttributeSet. С помощью этого класса можно определить любые пары "имя — значение", но общепринятые атрибуты удобнее задавать с использованием констант и статических методов класса StyleConstants и четырех его подклассов, которые в то же время вложены в него: CharacterConstants, ColorConstants, FontConstants и ParagraphConstants.

Объект, реализующий интерфейс AttributeSet, может содержать ссылку на другой, "родительский" объект того же типа. Ссылка хранится как значение атрибута, имеющего имя ResolveAttribute. Так можно получить цепочку объектов, содержащих атрибуты текста. Если какая-то пара "имя — значение" не найдена в первом объекте методом getAttribute (Object), то она отыскивается в родительском объекте, который определяется методом getResolveParent (), затем поиск идет далее по цепочке.

У интерфейса MutableAttributeSet есть свое расширение- интерфейс Style. Это расши

рение дает возможность получить имя множества атрибутов методом getName ( ), создав так называемый стиль (style), и присоединить к множеству слушателя события

ChangeEvent методом

void addChangeListener(ChangeListener chl);

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

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

StyleContext stc = StyleContext.getDefaultStyleContext();

Затем в полученный объект stc можно добавить новые атрибуты методами addAttribute (), удалить атрибуты методами removeAttribute(), создать цепочку стилей.

Удаление текста из документа

Обратная операция — удаление части или всего текста из документа — выполняется методом

remove(int offset, int length);

Он удаляет length символов, начиная от символа, находящегося в позиции offset.

Фильтрация документа

Операции занесения текста в документ методом insertString() и удаления методом remove () можно изменить так, чтобы они производили различные проверки и модификации, фильтруя таким способом вставляемый или удаляемый текст. Это можно сделать прямым расширением класса, реализующего Document, и переопределением его методов insertString () и remove (). Но есть еще один способ, не изменяющий документ.

Сначала надо расширить класс DocumentFilter, переопределив его методы. В классе DocumentFilter всего три метода:

void insertString(DocumentFilter.FilterBypass fb, int offset,

String text, AttributeSet attr);

void remove(DocumentFilter.FilterBypass fb, int offset, int length); void replace(DocumentFilter.FilterBypass fb, int offset, int length,

String text, AttributeSet attr);

Их стандартная реализация просто вызывает соответствующие методы вложенного абстрактного класса FilterBypass, которые оставлены абстрактными.

После расширения класса DocumentFilter, вызванного переопределением методов, полученный фильтр надо установить в документ методом

void setDocumentFilter(DocumentFilter filter);

класса AbstractDocument. Далее всякое обращение к методам insertString () и remove () документа будет пропускаться через методы созданного фильтра.

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

Внесение структуры в документ

В модель данных можно внести структуру дерева, например разбить документ на главы, параграфы, разделы. Каждый элемент разбиения описывается интерфейсом Element. Элемент занимает какую-то область текста с начальной позицией, возвращаемой методом getStartOffset (), и конечной позицией getEndOffset (). У элемента может быть родительский элемент, который легко получить методом getParentElement(). Область текста, занимаемая родительским элементом, полностью включает в себя область исходного элемента. У элемента могут быть дочерние элементы, чья область текста полностью лежит внутри области самого элемента. Их можно получить методом getElement(int). Число дочерних элементов возвращает метод getElementCount (), а индекс дочернего элемента — метод getElementIndex (int). Элементу можно дать имя, а затем получить его методом getName ().

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

в AbstractDocument классами — BranchElement и LeafElement, расширяющими класс AbstractElement. Основная разница между ними в том, что у класса BranchElement могут быть дочерние элементы, а у класса LeafElement — нет.

Класс BranchElement, в свою очередь, расширяется классом SectionElement, вложенным в класс DefaultStyledElement, и классом BlockElement, вложенным в класс HTMLDocument.

Класс LeafElement расширяется классом RunElement, вложенным в класс HTMLDocument. Элементы создаются методами

Element createLeafElement(Element parent, AttributeSet attr,

int pos1, int pos2);

Element createBranchElement(Element parent, AttributeSet attr);

Для каждого структурного элемента можно задать свое множество атрибутов методом

addAttribute(Object name, Object value) или методом addAttributes(AttributeSet).

В документе допускается задание нескольких независимых структур. Их корневые элементы можно получить методом getRootElements ( ), возвращающим массив типа Element [ ]. Один из корневых элементов можно сделать корневым элементом по умолчанию и получать его методом getDefaultRootElement(), возвращающим элемент в виде объекта, реализующего интерфейс Element. Получив корневые элементы структурного дерева, легко обойти его, используя метод getElement(int) интерфейса Element или метод children (), имеющийся в его реализациях.

События в документе

При всяком изменении документа или его структуры происходит событие, описанное интерфейсом DocumentEvent. Он предлагает метод getDocument ( ), позволяющий узнать, в каком документе произошло событие, методы getOffset () и getLength (), сообщающие о начальной позиции и длине измененного текста. Вложенный интерфейс ElementChange содержит метод getElement (), позволяющий узнать элемент, в котором произошло событие, и методы, помогающие отследить добавление и удаление элемента из документа.

Реализация интерфейса DocumentEvent — класс DefaultDocumentEvent, вложенный в класс AbstractDocument, — добавляет к методам интерфейса метод undo (), отменяющий изменения, метод redo (), восстанавливающий изменения, и еще несколько информационных методов.

Если модель данных позволяет отменять и восстанавливать изменения (undo/redo), то при каждом таком действии в ней происходит событие класса UndoableEditEvent.

Реализации документа

Интерфейс Document частично реализован абстрактным классом AbstractDocument. Этот класс вносит понятие блокировки документа. Документ могут просматривать несколько подпроцессов-"читателей" и один подпроцесс-"писатель". Доступ их к документу блокируется методами readLock() и writeLock(). Блокировки снимаются методами

readUnlock() Рё writeUnlock().

Класс AbstractDocument обычно не расширяется непосредственно, а используются или расширяются его подклассы PlainDocument и DefaultStyledDocument.

Класс PlainDocument задает модель простого документа с "плоским" текстом, которая используется полями ввода JTextField, JPasswordField, JTextArea. Текст в этой модели имеет структуру: структурные элементы текста — это строки. Корневой элемент структуры можно получить методом getDefaultRootElement(). Метод getParagraphElement(int offset) возвращает элемент структуры — строку в виде объекта типа Element, к которому принадлежит позиция offset. Каждой строке, как любому элементу структуры, можно придать атрибуты. Отдельные символы атрибутов не имеют.

Более сложную структуру вносит в документ модель класса DefaultStyledDocument, используемая в текстовом редакторе JTextPane. В этой модели не только строке, но и каждому символу текста можно задать свой стиль.

Наконец, класс DefaultStyledDocument расширяется классом HTMLDocument. Это модель разметки языка HTML. В ней можно определить таблицы стилей (style sheets).

Установка модели данных

После того как новая модель данных определена, ее надо установить в компонент методом setDocument(Document) класса JTextComponent.

Р’РёРґ

Вторая часть схемы MVC — Вид — построена для текстовых компонентов так, что для каждого элемента документа создается свой вид, значит, каждый элемент может получить свое графическое оформление.

Эта идея частично реализована абстрактным классом View. Он задает для каждого документа целую структуру Видов, отвечающую структуре элементов документа. Каждый вид из этой структуры при своем создании получает ссылку на определенный элемент документа, которую можно отследить методом getElement (). Документ, к которому относится Вид, можно получить методом getDocument(). У каждого Вида есть родительский вид, который определяется методами getParent() и setParent ( ), и множество дочерних видов. Их число можно определить методом getViewCount(), получить дочерние виды можно методом getView(int).

Создание дочерних Видов выполняется методом create(Element), описанным в интерфейсе ViewFactory. В документе у каждого элемента есть объект, реализующий этот интерфейс, который можно получить методом getViewFactory(). Еще несколько методов занимаются добавлением видов в иерархию и удалением их оттуда.

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

Shape modelToView(int startPos, Position.Bias b0,

int endPos, Position.Bias b1, Shape fig);

который возвращает двумерную фигуру класса Shape. Параметры метода задают начальную startPos и конечную endPos позиции текста, направления b0 и b1 от этих позиций к преобразуемому тексту, которые могут принимать одно из двух значений:

Position.Bias.Backward или Position.Bias. Forward, и окружающую фигуру fig.

Обратное преобразование выполняется методом

int viewToModel(float x, float y, Shape fig, Position.Bias[] b);

возвращающим позицию символа, имеющего координаты (x, y) на экране в фигуре fig. Кроме того, метод вычисляет массив направлений b, уточняющий положение символа в модели данных.

Для вывода на экран в каждом виде создается графический контекст — экземпляр класса Graphics. Непосредственный вывод элемента на экран выполняется методом paint (Graphics) подобно выводу компонента. Графическим контекстом можно воспользоваться не только в методе paint(Graphics), но и непосредственно, получив его методом getGraphics ( ).

Каждый Вид устанавливает свой размер методом setSize(float width, float height), у него есть минимальный, предпочтительный и максимальный размер.

В этом класс View напоминает класс Component. Сходство усиливается тем, что подобно тому, как класс Component порождает множество компонентов, создающих на экране графический интерфейс, класс View порождает множество подклассов-видов, отображающих на экране различные типы документов. Их иерархия показана на рис. 12.1.

Object

L View-г CompositeView— BoxView

PlainView

ImageView - IconView

-1— BlockView — ListView

— FlowView-ParagraphView

FieldView — TableView

L PasswordView — TableView.TableRow

— WrappedPlainView

— ZoneView

-GlyphView-LabelView—InlineView

-AsyncBoxView

- ComponentView-p FormView LobjectView

Рис. 12.1. Иерархия классов-видов

Как видно из рис. 12.1, иерархия Видов обширна и разветвлена. Большинство классов связано с интерпретацией языка HTML, например класс ImageView обрабатывает тег

, класс FormView тег

и относящиеся к нему теги ,