Книга
Книга
T^uSvvuwmka м?ыр£ссаси<мА^^^1T^uSvvuwmka м?ыр£ссаси<мА^^^1СЕРИЯ JAVAСЕРИЯ JAVAmicrosystemsmicrosystems
T^uSvvuwmka м?ыр£ссаси<мА^^^1
T^uSvvuwmka м?ыр£ссаси<мА^^^1
СЕРИЯ JAVA
СЕРИЯ JAVA
microsystems
microsystems
JAVA6
Том II. Тонкости программирования
Руководство по платформе Java 2 для опытных пользователей - полностью переработанное издание для JDK™ версий 13/1.4, Standard Edition
Еще более протестированные и практичные примеры, чем когда-либо ранее!
Обновленные и совершенно новые темы: XML, модель обеспечения безопасности, работа с сетями, многопоточность, коллекции, удаленные объекты, JDBC API, компоненты JavaBeans™, Swing и многое другое
от Sun Microsystems Press
Кей С. Хорстманн • Гари Корнелл
JAVA™ 2
II.ТОНКОСТИОРОГРАММИРООАНИЯ
Библиотека профессионала
Java™ 2
Volume II - Advanced Features
CAYS. HORSTMANN
GARY CORNELL
PHPTRPHPTRSun Microsystems PressA Pren tice Hall Title
PHPTR
PHPTR
Java™ 2
II.
Библиотека профессионала
КЕЙ С. ХОРСТМАННГАРИ КОРНЕЛЛ
Москва • Санкт-Петербург • Киев 2002
ББК 32.973.26-018.2.75
Х82
УДК 681.3.07
Компьютерное издательство "Диалектика"Зав. редакцией В.В. Александров
Переводсанглийского А.Д.Базавова, канд. физ.-мат. наук Ю.Г. Гордиепко,А. В. ФранчукаПод редакцией канд. физ.-мат. наук Ю.Г. Гордиепко
По общим вопросам обращайтесь в издательство "Диалектика" по адресу:info@dialektika.com, http://www.dialektika.com
Хорстманн, Кей, С, Корнелл, Гари.
Х82Библиотека профессионала. Java 2. Том 2. Тонкости программирования.: Пер. с
англ. — М.: Издательский дом “Вильямс”, 2002. — 1120с.: ил. — Парал. тит. англ.
ISBN 5-8459-0345-9 (рус.)
Книга предназначена для опытных программистов, которые хотели бы овладеть профессиональными приемами программирования Java-приложений и аплетов: многопоточное программирование, коллекции, работа с сетями и базами данных, распределенное программирование, библиотеки Swing и AWT, технология JavaBeans, вопросы обеспечения безопасности и локализации приложений, использование платформно-зависимого кода и языка XML. Каждая глава посвящена описанию отдельной темы, они практически не связаны друг с другом, поэтому их можно читать в произвольном порядке. Описание всех важных концепций иллюстрируется надежными и практичными, а не примитивными или чрезмерно академичными примерами. Все они сопровождаются многочисленными полезными и уместными комментариями. Как сказал Гради Буч (Grady Booch), авторы "уделили большое внимание деталям этого мощного и выразительного языка без ущерба для концептуальной модели объектно-ориентированного программирования".
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Prentice Hall, Inc.
Authorized translation from the English language edition published by Prentice Hall, Ptr., Copyright © 2002 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher.
Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2002
ISBN 5-8459-0345-9 (РУС.)© Издательский дом “Вильямс", 2002
ISBN 0-1309-27384 (англ.)© Prentice Hall, Inc., 2002
Оглавление
Глава 1. Многопоточность15
Глава 2. Коллекции115
Глава 3. Работа с сетями191
Глава 4. Работа с базами данных — интерфейсJDBC251
Глава 5. Работа с удаленными объектами311
Глава 6. Более сложные компоненты пакета Swing385
Глава 7. Расширенные возможности AWT523
Глава 8. Технология JavaBeans™705
Глава 9. Безопасность803
Глава 10. Локализация905
Глава 11. Встраиваемые методы975
Глава 12. Язык XML1039
Предметный указатель1105
Содержание
Глава 1. Многопоточность15
Потоки16
Прерывание потоков30
Свойства потоков33
Приоритеты потоков39
Монопольные потоки46
Синхронизация52
Взаимные блокировки69
Потоки для пользовательского интерфейса78
Использование конвейеров для организации взаимодействия
потоков109
Глава 2. Коллекции115
Интерфейсы коллекций116
Конкретные коллекции124
Архитектура коллекций155
Алгоритмы169
Унаследованные коллекции177
Глава 3. Работа с сетями191
Подключение к серверу192
Создание серверов196
Более сложные способы работы с сокетами206
URL-соединения212
Форма ввода данных224
Сбор информации в Web-среде232
Глава 4. Работа с базами данных —интерфейсJDBC251
Структура интерфейса JDBC253
Язык SQL257
Инсталляция интерфейса JDBC263
Основы программирования интерфейса JDBC264
Просмотр и обновление результатов выполнения запроса287
Метаданные296
Транзакции305
Более сложные методы управления соединениями308
Глава 5. Работа с удаленными объектами311
Введение312
Вызовы удаленных методов314
Конфигурирование RMI-механизма318
Передача параметров для удаленных методов334
Использование RMI-механизма вместе с аплетами352
Активизация объекта-сервера356
Технологии Java IDL и CORBA363
Глава 6. Более сложные компонентыпакета Swing385
Списки385
Деревья402
Таблицы440
Стилизованные текстовые компоненты486
Организаторы компонентов493
Глава 7. РасширенныевозможностиAWT523
Алгоритм рисования524
Фигуры526
Области541
Штрихи545
Раскраски553
Преобразование координат559
Усечение569
Прозрачность и композиция573
Параметры рисования582
Чтение и запись изображений589
Управление изображениями601
Печать618
Буфер обмена659
Перетаскивание683
Глава 8. Технология JavaBeans™705
Зачем нужны компоненты JavaBeans706
Создание компонентов JavaBeans708
Применение компонентов JavaBeans для создания приложений711
Соглашения об именах для свойств исобытий компонента JavaBeans719
Типы свойств компонента JavaBeans722
Создание пользовательских событий741
Редакторы свойств746
Пользовательские соглашения об именах769
Настройщики778
Контекст компонента JavaBeans787
Глава9.Безопасность803
Загрузчики классов804
Верификация байт-кода813
Диспетчеры безопасности и полномочия818
Цифровые подписи855
Подписание кода883
Шифрование891
Глава 10. Локализация905
Числа и денежные суммы912
Дата и время919
Текст927
Пакеты ресурсов951
Локализация графического интерфейса
956
Глава 11. Встраиваемые методы
Вызов функции на языке С в коде на языке Java
Числовые параметры и возвращаемые значения
Строковые параметры
Доступ к полям объекта
Доступ к статическим полям
Сигнатуры
Вызов Java-методов
Массивы
Обработка исключительных ситуаций
API-интерфейс запуска
Пример доступа к реестру Windows
975
978
983
986
991
994
997
999
1007
1012
1017
1021
Глава 12. ЯзыкXML
Введение в язык XML
Синтаксический анализ XML-документа
Определение типа документа
Пространства имен
SAX-анализаторы
Создание XML-документов
XSL-преобразования
1039
1040
1045
1056
1077
1080
1085
1093
Предметный указатель
1105
Предисловие
а"та книга уже выдержала пять изданий. Первое издание данной книги появилось в одном томе в 1996 году. Однако, уже в третьем издании книги (с описанием пакета JDK 1.1) стало очевидно, что теперь невозможно ограничиться лишь одним томом при описании компонентов платформы Java™, которые представляют особый интерес для опытных программистов. Поэтому в дальнейшем весь материал настоящей книги размещался в двух томах. В этом издании описываются версии 1.3 и 1.4 стандартного набора инструментов разработкиJava 2 Standard Edition Software Development Kit.
Том I содержит описание основ языка, а том II — описание более сложных вопросов профессионального программирования. Таким образом, данная книга и все ее предыдущие издания предназначены для тех программистов, которые хотят использо- ватьтехнологию Java для создания реальных проектов.
Опытным программистам, которым знакомы новая модель обработки событий и более сложные понятия, например, внутренние классы, можно пропустить первый том и приступать к изучению второго. (В томе II содержится большое количество ссылок на том I, и авторы надеются, что читатели приобретут оба тома. Дело в том, что все базовые материалы, необходимые для понимания материала тома II, можно найти только в очень подробных вводных руководствах по платформеJava.)
При создании этой книги неизбежно могли возникнуть ошибки и опечатки. Авторы будут признательны читателям за любую информацию о найденных ошибках и опечатках. Все найденные ошибки, опечатки, а также ответы на часто возникающие вопросы собраны и перечислены на Web-странице книги по адресу http://www.horstmann.com/ corej ava. html. В конце Web-страницы (чтобы стимулировать у читателей желание ознакомиться с найденными ранее ошибками и опечатками) приводится форма отчета, которую следует использовать для подачи информации о найденной ошибке или опечатке. Авторы и издатели этой книги внимательно изучают все присланные отчеты о найденных ошибках или опечатках, но, к сожалению, не имеют возможности ответить на все присланные замечания.
Об этой книге
Главы книги, в основном, не связаны друг с другом, поэтому их можно читать независимо и в любом порядке.
В главе 1, "Многопоточность", рассматривается механизм многопоточности, который позволяет программировать параллельное выполнение разных задач. (Потоком (thread) называется отдельный сегмент управления в программе.) В ней описываются способы создания устойчиво работающих потоков. Все теоретические сведения иллюстрируются практическими примерами создания таймеров и анимационных изображений.
В главе 2, "Коллекции", описываются коллекции, используемые в платформе Java 2. Коллекции гораздо удобнее при работе с наборами нескольких объектов, чем объек-
ты-векторы. В этой главе подробно рассматриваются некоторые наиболее полезные стандартные коллекции.
В главе 3, "Работа с сетями", описывается один из наиболее важных API- интерфейсов платформы Java, который предназначен для работы с сетями. Он позволяет с феноменальной скоростью создавать сложные сетевые приложения. При этом особое внимание уделяется описанию важных особенностей модели обеспечения безопасности работы аплетов при сетевом программировании.
В главе 4, "Работа с базами данных — интерфейсJDBC”, описывается JDBC™ —Java API-интерфейс для работы с базами данных. В ней приводятся несколько полезных примеров работы с базами данных на основе основных API-функций JDBC. Учтите, что это далеко не полное описание всех возможностей JDBC, потому что для всестороннего рассмотрения JDBC™ потребуется написать отдельную книгу.
В главе 5, "Работа с удаленными объектами", рассматриваются удаленные объекты и технология удаленных вызовов (Remote Method Invocation— RMI). Этот API- интерфейс позволяет работать с Java-объектами, которые могут располагаться на разных компьютерах. Здесь показаны практические способы использования идеологии распределенных объектов.
В главе 6, "Более сложные компоненты пакета Swing", содержится дополнительный материал о библиотеке Sxving, который не удалось полностью разместить в первом томе. В ней рассматриваются сложные компоненты на основе иерархических древовидных структур и таблиц. Затем приводятся основные сведения о панелях редактирования, а также реализации многодокументного интерфейса средствами Java. Основное внимание в этой главе сконцентрировано на практических примерах использования библиотеки Swing, а не на полноте описания всех компонентов библиотеки Swing и их особенностей. Дело в том, что для полного описания этой библиотеки потребуется несколько томов, которые будут интересны только очень узкому кругу профессионалов.
В главе 7, "Расширенные возможности AWT", рассматривается 2D API-интерфейс, который позволяет создавать реалистичные изображения. В ней также описаны некоторые сложные компоненты пакета AWT (Abstract Window Toolkit), которые не вошли в том I. Эти компоненты включают инструменты печати, копирования и вставки, перетаскивания объектов и многое другое. В данном издании книги также уделяется внимание усовершенствованным методам печати, которые предусмотрены В SDK 1.4.
В главе 8, "Технология JavaBeans™", описывается API-интерфейс для работы с компонентами платформы Java — JavaBeans тм В ней рассматриваются способы создания компонентов, которые могут использоваться другими разработчиками в интегрированных средах разработки приложений. (Однако, здесь не рассматриваются какие- то конкретные интегрированные среды разработки приложений, в которых могут использоваться компоненты JavaBeans.) Возможность использования компонентов JavaBeans является чрезвычайно важным достижением развития технологии Java, ко-; торое может значительно упростить создание и использование пользовательских интерфейсов. Его можно сравнить с успехом технологии ActiveX среди миллионов программистов на языке VisualBasic. Однако, из-за того, что компоненты JavaBeans созд^- ны на языке Java, они обладают преимуществом по сравнению с компонентам^ ActiveX, так как могут использоваться на многих типах платформ.
В главе 9, "Безопасность", речь идет о модели обеспечения безопасности Java. Безопасность была основным фактором, с учетом которого разрабатывался этот язык, и в данной главе рассмотрены основные детали этой модели обеспечения безопасности.
На практических примерах показаны способы создания собственных загрузчиков классов и менеджеров обеспечения безопасности для специальных приложений. Кроме того, рассмотрены новые инструменты для работы с подписанными классами.
В главе 10, "Локализация", описываются инструменты локализации Java-приложений, которые считаются одними из наиболее важных и перспективных элементов платформы Java. Дело в том, что Java является одним из немногих языков программирования, в котором используется Unicode. Однако, предусмотренные в Java инструменты локализации позволяют использовать приложения не только на разных платформах, но и в разных странах с разными региональными стандартами. На примере приложения- калькулятора пенсионного плана представлен простой способ создания приложения с возможностью выбора интерфейса для английских, немецких или китайских пользователей в зависимости от используемого в системе регионального стандарта.
В главе 11, "Встраиваемые методы", рассматриваются встроенные методы, которые позволяют создавать методы для специальной платформы, например, для Microsoft Windows API. Очевидно, что использование этих методов исключает возможность их применения для всех остальных платформ. Тем не менее, профессиональным программистам следует знать об этой функциональной возможности, потому при создании серьезных приложений часто возникает необходимость использования API- функций операционной системы, например, для доступа к реестру Windows.
В главе 12, "Язык XML", которая появилась только в этом издании книги, рассматривается язык расширенной гипертекстовой разметки XML (extensible Markup Language), который используется для обмена данными в неоднородных средах, для более сложной структурированности файлов конфигурации и т.д. Начиная с SDK 1.4, поддержка языка XML встроена в стандартную библиотеку Java и может использоваться в любом приложении. После изучения этой главы читатель сможет сам убедиться в том, насколько просто можно преобразовать разнообразные форматы данных в профессиональные XML-документы. (Для использования XML с прежними версиями SDK нужно использовать библиотеку-расширение JAXP фирмы Sun.)
Соглашения
Как и в большинстве других книг по программированию, для обозначения кода используется моноширинный шрифт.
Примечания содержат комментарии и отступления от основной темы. Они обозначаются пиктограммой с изображением записной книжки.
Полезные советы содержат ссылки и рекомендации по более эффективному использованию Java. Они обозначаются пиктограммой с изображением восклицательного знака.
Предупрежденияуказывают на потенциальные опасности и угрозы.
Замечания содержат описания различий между языком Java и языком C++. Читатели, которых не интересуют эти различия, могут пропустить данные замечания.
ПнПнПлатформа Java содержит огромную библиотеку функций, которые образуют интерфейс прикладного программирования (Application Programming '* Interface — API). После каждого упоминания такой API-функции в конце раздела приводится ее краткое описание, которое обозначается такой пиктограммой. Эти описания менее официальны, но более информативны, чем те, которые приводятся в интерактивной документации API-интерфейсаJava.
Пн
Пн
Коды всех примеров можно найти на Web-узле издательского дома “Вильямс” по адресу:www. williamspublishing.com.
Благодарности
Для написания книги о платформе Java всегда требуется приложить значительные усилия, а для ее переиздания ничуть не меньше, особенно при столь быстром развитии технологии Java. Для претворения этого замысла в жизнь необходимо участие и помощь со стороны преданных делу людей и потому мне очень приятно поблагодарить всю команду создателей этой книги за их вклад в общее дело.
Редактор издательства Prentice Hall PTR Грег Денч (Greg Doench) выполнил огромный объем работы по координации всех аспектов этого сложного проекта. Джули Бетис (Julie Bettis) тщательно проверила всю рукопись и ее соответствие строгим правилам использования торговой марки Java.
Кэти Вести (Kathi Beste), Берт Штуц (Bert Stutz) и Мэрилин Штуц (Marilyn Stutz) из компании Navta Associates, Inc. создали очень привлекательный макет этой книги. Многие другие специалисты издательства Prentice Hall PTR и издательства Sun Microsystems Press также оказали неоценимую помощь, но остались за кулисами всего процесса подготовки настоящей книги. Я хотел бы выразить им свою искреннюю благодарность за оказанную помощь. Отдельное спасибо соавтору прежних изданий этой книги, Гари Корнеллу (Gary Cornell), который теперь принимает участие в других проектах.
Я очень благодарен прекрасной команде рецензентов, которым удалось найти множество ошибок и предложить удачные способы их устранения. Весь новый материал был проверен и отрецензирован Дэвидом Гири (David Geary), Бобом Линчем (Bob Lynch), Полом Филионом (Paul Philion) и Джорджем Тируватукалом (George Thiruvathukal).
Рецензентами предыдущих изданий были Алик Битон (Alec Beaton) из компании PointBase, Inc., Джошуа Блох (Joshua Bloch) из компании Sun Microsystems, Дэвид Браун (David Brown) и доктор Николас Джю Де Лилло (Nicolas J. De Lillo) из Манхэттенского колледжа (Manhattan College), Ракеш Дхупар (Rakesh Dhoopar) из компании Oracle, Анжела Гордон (Angela Gordon) из компании Sun Microsystems, Дэн Гордон (Dan Gordon) из компании Sun Microsystems, Роб Гордон (Rob Gordon) и Камерон Грегори (Cameron Gregory) из компании olabs.com, Марти Холл (Marty Hall) из лаборатории прикладной физики Университета Джона Хопкинса (The John Hopkins University), Винсент Харди (Vincent Hardy) из компании Sun Microsystems, Владимир Иванович (Vladimir Ivanovich) из компании PointBase, Inc., Джери Джексон (Jerry Jackson) из компании ChannelPoint Software, Тим Киммет (Tim Kimmet) из компании Preview Systems, Крис Лафра (Chris Laffra) и Чарли Лай (Charlie Lai) из компании Sun Microsystems, Даг Лангстон (Doug Langston) и Даг Ли (Doug Lea) из компании SUNY Oswego, Грегори Лонгшор (Gregory Longshore) и Марк Мориссей (Mark Morissey) из Орегонского института (The Oregon Graduate Institute), Махиш Нилаканта (Mahesh Neelakanta) из Атлантического университета штата Флорида (Florida Atlantic University), Блэйк Рагсделл (Blake Ragsdell) и Стюарт Реджес (Stuart Reges) из Университета штата Аризона (University of Arizona), Питер Сандерс (Peter Sanders) из Университета ЕССИ, Ницца, Франция (ESSI University), Деванг Шах (Devang Shah) из компании Sun Microsystems, Кристофер Тейлор (Christopher Taylor) и Люк Тейлор (Luke Taylor) из компании Valtech, Ким Топли
(Kim Topley), Джанет Трауб (Janet Traub) и Питер ван дер Линден (Peter van der Linden) из компании Sun Microsystems, а также Барт Волш (Burt Walsh).
И самое главное, я хотел бы выразить свою любовь и благодарности жене Хуи-Чен и детям Томми и Нине за их постоянную поддержку этого бесконечного проекта.
КейХорстманн
Купертино, ноябрь 2001 года
МногопоточностьМногопоточность
Многопоточность
Многопоточность
В этой главе...
т Потоки
т Прерывание потоков
т Свойства потоков
т Приоритеты потоков
т Монопольные потоки
т Синхронизация
т Взаимные блокировки
т Потоки для пользовательского интерфейса
т Использование конвейеров для организации взаимодействия потоков
"^Еитатели наверняка знакомы с понятием многозадачность (multitasking), которое означает способность компьютера одновременно выполнять сразу несколько программ, например, печатать на принтере во время редактирования документа или посылки факса. Конечно, если компьютер не оснащен несколькими процессорами, то операционная система всего лишь распределяет вычислительные ресурсы между разными программами, создавая впечатление их параллельного выполнения. Такое распределение ресурсов возможно, потому что компьютер не всегда занят работой, например, во время ввода данных, и процессор большую часть времени находится в состоянии бездействия. (Например, даже при очень быстром вводе данных на ввод одного символа уходит примерно 1 /го секунды, а это огромный интервал с точки зрения компьютера.)
Многозадачность может быть реализована двумя способами: либо операционная система безоговорочно прерывает выполнение задачи, либо управление операционной системе возвращается по усмотрению самой программы, например, по окончании выполнения какой-то задачи. Первый способ называется вытесняющей многозадачностью (preemptive multitasking), а второй— кооперативной (или невытесняющей) многозадачностью (cooperative multitasking). Кооперативная многозадачность применяется в Windows 3.1 и Mac OS 9, а вытесняющая — в UNIX/Linux, Windows NT/2000 (а также в Windows 95 для 32-разрядных программ) и Mac OS X. (Вытесняющая многозадачность работает гораздо
эффективнее кооперативной, но ее сложнее организовать. Более того, некорректно написанная программа в режиме кооперативной многозадачности способна поглотить все вычислительные ресурсы системы.)
Многопоточные программы развивают идею многозадачности на более низком уровне: отдельные программы способны выполнять "одновременно" несколько задач. Каждая задача обычно сокращенно называется потоком (thread) или полностью ч^^^^к потоком управления (thread of control). Программы, способные запускать более одного потока, называются многопоточными (multithreaded). Каждый поток выполняется в отдельном контексте, т.е. создается видимость того, что каждый поток имеет собственный процессор с регистрами, памятью и своим кодом.
В чем же заключается разница между несколькими процессами и несколькими потоками? Существенное различие заключается в том, что каждый процесс имеет набор собственных переменных, тогда как потоки обращаются к одним и тем же данным. На первый взгляд это выглядит несколько рискованно и, как показано ниже в этой главе, так оно и есть. Гораздо проще создавать и уничтожать отдельные потоки, чем запускать новые процессы, и именно поэтому все современные операционные системы поддерживают многопоточность. Более того, процессы взаимодействуют гораздо медленнее и с большими ограничениями, чем потоки.
Многопоточность имеет очень большое практическое значение. Например, броузер должен обладать возможностью одновременной загрузки нескольких изображений, а почтовая программа должна позволять читать старые сообщения во время получения новых. В языке программирования Java используется специальный поток для "сборки мусора" в фоновом режиме, которая избавляет программистов от необходимости управлять памятью! Программы с графическим пользовательским интерфейсом используют отдельный поток для сбора интерфейсных событий в среде операционной системы. В этой главе описываются способы создания многопоточных приложений и аплетов на языке Java.
Учтите, что создание многопоточных приложений может оказаться очень непростым делом. В этой главе представлены все средства языка Java, необходимые для многопоточного программирования. Для объяснения способов их использования и ограничений приводятся простые, но типичные примеры. Однако, для более подробного изучения этой темы следует обратиться к книге Дуга Ли (Doug Lea) Concurrent Programming in Java [Addison-Wesley1999].
Во многих других языках программирования для использования инструментов многопоточности необходимо загружать внешние пакеты. В языке Java все средства, необходимые для многопоточного программирования, являются встроенными, что значительно облегчает решение таких задач.
Потоки
Начнем с рассмотрения программы, в которой многопоточность не используется, что не позволяет пользователю при работе с ней выполнять одновременно несколько задач. После анализа исходного варианта программы будет показано, как можно с легкостью изменить код для организации одновременного выполнения нескольких потоков. Как показано на рис. 1.1, данная программа имитирует движение бильярдного шара, перемещая его в прямоугольной области окна, определяя направление отражения от стен и перерисовывая его в новой позиции.
После щелчка на кнопке Start (Пуск) шар запускается из левого верхнего угла экрана и начинает движение с отскоками от границ окна. Для запуска шара обработчик кнопки Start вызывает метод addBall.
public void
{
try
Ball b = new canvas.add(b);
for (int i = 1; i <= 1000;
{
b.move();
Thread.sleep(5) ;
}
}
catch (InterruptedException exception)
}
}
Этот метод содержит цикл, состоящий из 1000 перемещений шара. Каждый вызов метода move передвигает шар на небольшое расстояние, корректирует направление, если шар отражается от стены и перерисовывает состояние экрана. При этом статический метод sleep класса Thread устанавливает паузу, равную 5
Рис. Использование одного потока для имитации движения шара
Вызов метод sleep не создает нового потока, так как sleep это статический метод класса Thread, который временно останавливает выполнение текущего потока.
Метод sleep может генерировать исключительную ситуацию Interrupted- Exception. Более подробно эта ситуация и ее обработка рассматриваются ниже. Если вы попытаетесь прервать движение шара, щелкая мышью на кнопке Close (Закрыть) до полного завершения цикла из 1000 итераций, то ничего не произойдет, и шар продолжит движение с отскоками. Дело в том, что у пользователя нет никаких средств взаимодействия с программой вплоть до полного завершения цикла.
При внимательном изучении кода (листинг 1.1) можно заметить внутри метода move класса Ball показанный ниже вызов метода paint.
canvas.paint(canvas.getGraphics());
На первый взгляд это может показаться достаточно странным, потому что обычно для перерисовки окна вызывается метод repaint, и затем пакет AWT автоматически получит графический контекст и перерисует окно. Но, если в этой программе попытаться вызвать метод, canvas.repaint (), то в результате окажется, что экран не перерисовывается до тех пор, пока метод addBall полностью не получит управление. Знакомый нам метод repaint вновь будет применяться в следующей программе, где для вычисления позиции шара используется отдельный поток.
Очевидно, что с такой программой очень неудобно работать. Более того, при выполнении задач, требующих достаточно большого времени, это даже недопустимо. При чтении данных по сети часто приходится выполнять задачи, которые требуется прервать. Например, во время загрузки большого изображения пользователь часто желает прервать процесс загрузки после просмотра загруженной части. Разумеется, программа должна предоставить ему такую возможность, например, при нажатии кнопки Stop (Стоп) или Back (Назад). В следующем разделе представлен способ организации управления программой, при котором важные части кода размещаются в разных потоках.
Полный код этой программы приведен в листинге 1.1.
Листинг 1.1. Bounce. j ava
@version 1.30 2001-05-06
©author Cay Horstmann
*/
import j ava.awt.*;
import j ava.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import j avax.swing.*;
/**
Пример имитации движения бильярдного
шара с отскоками от границ окна
*/
public class Bounce
{
public static void main(String[] args)
{
JFrame frame = new BounceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
}
/ * *
Фрейм с канвой и кнопками
*/
class BounceFrame extends JFrame
/*
Создание фрейма с канвой и кнопками
*/
public BounceFrame()
{
setsize(WIDTH, HEIGHT);
setTitle("Bounce");
Container contentPane = getContentPane();
canvas = new BallCanvas();
contentpane.add(canvas. BorderLayout.CENTER) ;
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
addBall();
} });
addButton(buttonPanel, "Close", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
System.exit(0) ;
} });
contentpane.add(buttonPanel, BorderLayout.SOUTH);
}
/**
Вставка кнопки в контейнер
@param с the container
@param title-the button title
@param listener the action listener for the button
*/
public void addButton(Container c, String title, ActionListener listener)
{
JButton button = new JButton(title);
c.add(button);
button.addActionListener(listener);
/**
Создает шар в канве и выполняет
1000 последовательных перемещений шара
*/
public void addBallO
{
try
Ball b = new Ball(canvas);
canvas.add(b);
for (int i = 1; i <= 1000; i++)
{
b.move();
. Thread.sleep(5);
}
catch (InterruptedException exception)
}
private BallCanvas canvas;
public static final int WIDTH = 450;
public static final int HEIGHT = 350;
/**
Канва, в которой движется шар */ class BallCanvas extends JPanel {
/**
Создание шара в канве
@parain b the ball to add
*/
public void add(Ball b) {
balls.add(b);
}
public void paintcomponent(Graphics g) {
super.paintcomponent(g) ;
Graphics2D g2 = (Graphics2D)g;
for (int i = 0; i < balls.size(); i++) {
Ball b = (Ball)balls.get(i);
b.draw(g2);
}
}
private ArrayList balls = new ArrayList(); }
/**
Шар, который может двигаться в канве и отскакивать от стенок окна
*/ class Ball {
/**
Создание шара в верхнем левом углу
@с the component in which the ball bounces
*/
public Ball(Component c) { canvas = c; }
/**
Рисование шара в его текущей позиции @param g2 the graphics context
*/
public void draw(Graphics2D g2) {
g2.fill(new Ellipse2D.Double(x, y, XSIZE, YSIZE)); }
/**
Перемещение шара в следующую позицию с отражением в случае столкновения с одним из краев окна
*/
public void move()
{
х += dx;
У += dy;
if (x < 0)
{
x = 0;
dx = -dx;
}
if (x + XSIZE >= canvas.getWidth())
{
x = canvas.getWidth() - XSIZE;
dx = -dx;
}
if (У < 0)
{
У = 0;
dy = -dy;
}
if (y + YSIZE >= canvas.getHeight())
{
у = canvas.getHeight() - YSIZE;
dy = -dy;
canvas.paint(canvas.getGraphics() ) ;
private Component canvas;
private static final int XSIZE = 15;
private static final int YSIZE = 15;
private int x = 0;
private int у = 0;
private int dx = 2 ;
private int dy = 2 ;
java.lang.Thread
•Метод static void sleep (long millis) приостанавливает поток на
заданное число миллисекунд.
Параметры:
millisколичество миллисекунд
В предыдущих разделах описаны некоторые требования для разбиения потока выполнения программы на несколько параллельных задач. Например, каждую задачу необходимо поместить в метод run, который наследует класс Thread. А что делать, когда необходимо добавить метод run к классу, который уже наследует другой класс? В большинстве случаев так происходит при создании многопоточного аплета, который уже наследует класс JApplet. В данном случае необходимо использовать интерфейс, потому что в языке Java не допускается наследование от двух родительских классов. Специально для этого в платформе Java предусмотрен интерфейс Runnable, который рассматривается в следующем разделе.
Создание потоков, способных передавать
управление другим задачам
В приведенной выше программе можно организовать параллельную обработку запросов пользователя, если запустить код перемещения шара в отдельном потоке.
В большинстве компьютеров установлен только один процессор, поэтому альная машина Java использует механизм, в котором каждый поток запускается в течение определенного времени, а затем управление переходит к следующему потоку. При обработке потоков виртуальная машина использует заданные в операционной системе параметры и расписание выполнения потоков.
В следующей программе используются два потока: один для расчета движения шара, а другой — для потока управления событиями (event dispatch thread), который обрабатывает события пользовательского интерфейса. Каждый из потоков выполняется в течение некоторого времени. Поэтому главный поток может проследить за нажатием кнопки Close (Закрыть) во время перемещения шара.
Для запуска кода в отдельном потоке достаточно разместить код в методе run класса, производного от класса Thread.
Чтобы программа с шаром выполнялась в отдельном потоке, необходимо создать классна основе класса Thread и поместить код имитации движения ша
ра внутри метода run, например так, как показано ниже.
class BallThread extends Thread {
public void run()
{
try
for (int i = 1; i <= 1000; i++)
{-
b.move();
sleep(5);
}
}
catch (InterruptedException exception)
{-
}
private Ball b;
}
В этом фрагменте кода организован перехват исключительной ситуации Interrupt- edException. Такие методы, как sleep и wait, генерируют эту исключительную ситуацию, когда выполнение одного потока прерывается вызовом метода interrupt в другом потоке. Прерывание потока представляет собой радикальный способ "привлечения его внимания", даже если в данный момент он и не является активным. Дело в том, что обычно поток прерывается для завершения работы. При возникновении исключительной ситуации InterruptedException выполнение метода run прекращается.
Выполнение и запуск потоков
При создании потока, производного от класса Thread, метод run не вызывается, и поток не запускается автоматически.
BallThread thread = new BallThread(. . .); // еще не запущен
Чтобы запустить поток, необходимо явным образом использовать метод start данного потока.
thread.start();
Никогда не вызывайте метод run явно, потому что его неявно вызовет метод start после того, как поток будет настроен и готов к запуску. После вызова метода run содержащийся в нем код будет выполнен в том же потоке без запуска нового потока.
Начинающие программисты часто ошибочно считают, что каждый метод объекта Thread автоматически запускается в новом потоке. Как видите, это совсем не так. Метод любого объекта (а не только Thread) запускается в том потоке, где он вызван. Причем новый поток запускается только с помощью метода start, а его код выполняется с помощью неявного вызова метода run.
Чтобы другие потоки могли выполнять код в своих процедурах run, поток должен сообщить другим потокам о том, что он простаивает. Как показано на рис. 1.2, обычно для этого применяется статический метод sleep. Метод run класса BallThread использует вызов sleep(5), указывая, что в течении последующих пяти миллисекунд поток не будет выполняться. После этого он запустится вновь, но в течение этой паузы могут выполняться другие потоки.
Поток управления событиями
Поток перемещения шара
Запуск
Перемещение шара
Пауза
Перемещение шара
Пауза
Рис. 1.2. Схема управления событиями и потоком перемещения шара
В классе Thread предусмотрено несколько статических методов для работы с текущим потоком. Например, статический методsleep приостанавливает выполнение потока, в котором он был вызван.
Полный код этой программы приведен в листинге 1.2.
Листинг 1 2 BounceThread.lava
/**
©version 1.30 2001-05-06
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import j
/**
Пример имитации движения бильярдного шара с отскоками от границ окна в отдельном потоке
*/
public class BounceThread {
public static void main(String[] args) {
JFrame frame = new BounceFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Фрейм с канвой и кнопками
*/
class BounceFrame extends JFrame {
/**
Создание фрейма с канвой и кнопками
*/
public BounceFrame()
{
setsize(WIDTH,HEIGHT);
setTitle("BounceThread");
Container contentPane = getContentPane();
canvas = new BallCanvas();
contentPane.add(canvas, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
addBall() ;
}
});
addButton(buttonpanel, "Close", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
System.exit(0) ;
} });
contentPane.add(buttonPanel, BorderLayout.SOUTH);
}
/**
Вставляет кнопку в контейнер
@param с the container
@param title the button title
@param listener the action listener for the button
*/
public void addButton(Container c, String title, ActionListener listener)
{
JButton button = new JButton(title);
c.add(button);
button.addActionListener(listener);
}
/**
Создает шар в канве и запускает отдельный поток для перемещений шара
*/
public void addBall()
{
Ball b = new Ball(canvas);
canvas. add.(b) ;
BallThread thread = new BallThread(b);
thread.start() ;
)
private BallCanvas canvas;
public static final int WIDTH = 450;
public static final int HEIGHT = 350;
/ * *
Поток для имитации перемещения шара
*/
class BallThread extends Thread {
/**
Создание потока
BaBall the ball to bounce
*/
public BallThread(Ball aBall) { b = aBall; }
public void run() {
try {
for (int i = 1; i <= 1000; i++) {
b.move(); sleep(5);
}
}
catch (InterruptedException exception) { }
}
private Ball b;
}
/**
Канва, в которой отображается перемещение шара
*/
class BallCanvas extends JPanel {
/**
Включение шара в канву
©param b the ball to add
/
public void add(Ball b) {
balls.add(b);
}
public void paintcomponent(Graphics g) {
super.paintcomponent(g);
Graphics2D g2 = (Graphics2D)g;
for (int i = 0; i < balls.size(); i++) {
Ball b = (Ball)balls.get(i) ; b.draw(g2);
} }
private ArrayList balls = new ArrayList() ; }
/**
Шар, который может двигаться и отскакивать от границ окна */ class Ball {
/**
Создание шара в верхнем левом углу
@с the component in which the ball bounces
/
public Ball(Component c) { canvas c; }
/**
Рисование шара в текущей позиции
©param g2 the graphics context
/
public void draw(Graphics2D g2) {
g2.fill(new Ellipse2D.Double(x, y, XSIZE, YSIZE)); }
/**
Перемещение шара в следующую позицию с отражением
в случае столкновения с одним из краев окна */
public void move()
{
х += dx;
у += dy;
if (x < 0)
{
x = 0;
dx = -dx;
}
if (x + XSIZE >= canvas.getWidth()) {
x = canvas.getWidth() - XSIZE;
dx = -dx;
}
if (У < 0)
{
У = 0;
dy = -dy;
}
if (y + YSIZE >= canvas.getHeight()) {
у = canvas.getHeight() - YSIZE;
dy = -dy;
canvas.repaint();
privateComponentcanvas;
private static final int XSIZE = 15;
private static final int YSIZE = 15; privateintx=0;
private int у = 0;
private int dx = 2 ;
private int dy = 2;
java.lang.Thread
Конструктор Thread () создает новый поток. Для выполнения кода в его методе run необходимо вызвать метод start этого потока.
Метод void run() необходимо переопределить для включения в него кода, который должен запускаться в потоке.
Метод void start () запускает поток, вызывая метод run (), и сразу же возвращает управление. Причем этот новый поток выполняется параллельно с другими потоками.
Запуск нескольких потоков
Запустите программу из предыдущего раздела. Нажмите кнопку Start несколько раз, и, как показано на рис. 1.3, в окне программы появится несколько движущихся шаров. Каждый из них выполнит 1000 шагов и остановится.
Рис. 1.3. Пример программы с несколькими потоками
Этот пример демонстрирует огромное преимущество потоковой архитектуры в языке программирования Java: можно создавать любое число автономных объектов, обработка которых будет выполняться параллельно.
Методы обработки сразу нескольких потоков рассматриваются в разделе о группах потоков ниже в этой главе.
Интерфейс Runnable
В данном случае нам удалось избежать создания нового класса, так как класс Ball является производным от класса Thread. Дополнительным достоинством такого подхода является то, что метод run имеет доступ к закрытым полям класса class Ball extends Thread {
public void run() try
{ for (int i = 1; i <= 1000; {
x += dx;
У += dy;
; sleep(5);
}.
} catch (InterruptedException exception) }
private Component
private int x = 0;
private int у = 0;
private int dx =
private int dy =
}
Концептуально все это выглядит очень сомнительно, потому что сам по себе шар не является потоком, и такое наследование вряд ли можно назвать удачным. Тем не менее, программисты иногда используют такой подход в случаях, когда методу run потока требуется предоставить доступ к закрытым полям другого класса. В предыдущем разделе нам удалось избежать рассмотрения этого вопроса, так как метод run вызывал только открытые методы класса Ball. Однако, следует учесть, что такой способ не всегда удается так просто применить.
Предположим, что методу run требуется предоставить доступ к закрытым полям, но класс, в котором нужно разместить метод run, уже имеет другой родительский класс. Такой класс уже не может наследовать класс Thread, однако, в нем можно реализовать интерфейс Runnable. В этом случае возможно разместить требуемый код в методе run так же, как если бы этот класс наследовался от класса Thread.
class Animation extends JApplet
Implements Runnable
{
public void run()
{
// Здесь поток выполняет какие-то действия
}
Чтобы запустить поток, необходимо создать объект-поток, в конструкторе которого следует поместить указатель на объект, реализующий интерфейс Runnable, после чего поток запускает метод run этого объекта.
class Animation extends JApplet
implements Runnable
{
public void start()
{
runner = new Thread(this);
runner.start() ;
}
private Thread runner;
}
В данном случае аргумент this в конструкторе Thread указывает, что объектом, чей метод run должен вызываться при запуске потока, является экземпляр объекта Animation.
Некоторые разработчики считают, что всегда следует придерживаться именно такого подхода и никогда не создавать класс-поток, производный от класса Thread. Этот совет имел смысл во время использования платформы Java 1.0, т.е. до изобретения внутренних классов, а сейчас он несколько устарел. Если методу run потока требуется предоставить доступ к закрытым членам другого класса, то в таком случае можно воспользоваться внутренним классом.
class Animation extends JApplet
implementsRunnable
public void start()
runner = new Thread(this)
{*
public void run()
{
// Здесь поток выполняет какие-то действия }
};
runner.start();
private Thread runner; }
Интерфейс Runnable можно использовать как пул потоков, в котором ожидают запуска предварительно созданные потоки (pre-spawned threads). Пулы потоков иногда используются в средах, где запускается большое число потоков. Это сокращает затраты на создание потоков и сборку мусора, т.е. удаление неиспользуемых объектов-потоков.
java.lang.Thread
Конструктор Thread (Runnable target) создает новый поток, который вызывает метод run указанного объекта target.
j ava.lang.Runnable
Метод void run() необходимо переопределить и разместить в нем код, который должен выполняться в потоке.
Прерывание потоков
Поток прекращает работу, когда его метод run возвращает управление. (В Java 1.0 использовался метод stop, который вызывался из одного потока для завершения работы другого. В настоящее время он не используется по некоторым причинам, которые рассматриваются ниже в этой главе.)
В текущей версии Java не предусмотрено никакого способа принудительного прекращения работы потока. Вместо этого применяется метод interrupt, который запрашивает о возможности прекращения работы потока. Это значит, что метод run должен периодически проверять, не следует ли ему прекратить выполнение:
public void run() {
while (нет запроса на прекращение работы и есть какая-то работа)
{
выполнение какой-то работы
}
//выйти из метода run и прекратить работу потока }
Как известно, работа потока выполняется не постоянно, а иногда приостанавливается, чтобы дать возможность другим потокам решать свои задачи. Когда выполнение потока приостановлено, он не может проверить, следует ли завершить работу, и
для этого применяется исключительная ситуация InterruptedException. Когда для блокированного объекта вызывается метод interrupt, работа блокирующего вызова (метода sleep или wait) завершается, и управление передается обработчику исключительной ситуации InterruptedException.
В языке Java нет обязательного требования о завершении работы прерываемого потока. Прерывание просто "привлекает внимание" потока, а последний решает, как отреагировать. Для этого в теле оператора catch определяются действия, предпринимаемые при возникновении исключительной ситуации InterruptedException. Для некоторых наиболее важных потоков исключительную ситуацию можно игнорировать за счет его перехвата и продолжения выполнения работы потока. Но в большинстве случаев поток интерпретирует исключительную ситуацию как запрос на прекращение выполнения. В таком потоке метод run имеет следующий вид:
public void run()
{
try
{
while (есть какая-то работа)
{
//выполнение какой-то работы
}
}
catch(InterruptedException exception)
{
//работа потока прервана во время
//паузы или ожидания
}
finally
{
//очистка в случае необходимости
}
//выйти из метода run и прекратить работу потока }
Однако, в этом шаблоне кода не учитывается следующая особенность. Если метод interrupt вызывается не во время паузы (sleep) или ожидания (wait), то исключительная ситуация InterruptedException не генерируется. Поэтому в потоке необходимо вызвать метод interrupted, который проверяет, прерван ли поток в данный момент или нет.
while (1 interrupted()&& есть какая-то работа) {
//выполнение какой-то работы
}
Например, если поток блокируется на время ввода/вывода, то операции вво- да/вывода не прерываются вызовом метода interrupt. Впоследствии, когда блокирующая операция возвращает управление, необходимо воспользоваться методом interrupted, чтобы узнать, прерван ли поток или нет.
Итак, имеются два разных способа обработки прерывания потока: один на основе проверки статуса прерывания, а второй на основе перехвата исключительной ситуации InterruptedException.
Следует отметить, что существует два очень похожих метода: interrupted и islnterrupted. Первый метод является статическим и проверяет, прерван ли текущий поток или нет. (Учтите, что поток прерывается из-за того, что в другом потоке вызван метод interrupt.) Более того, вызов метода interrupted задает значение false для статуса прерыванияпотока. Второй метод, isinterrupted, является методом экземпляра и позволяет проверить, прерван ли любой поток или нет. Он не изменяет статуса прерывания своего аргумента.
Было бы гораздо удобнее, если бы метод sleep (и аналогичные ему методы) были определены так, чтобы они возвращали статус прерывания при его возникновении. В этом случае вообще не потребовалось бы обрабатывать исключительную ситуацию InterruptedException. Хотя конечно, статус прерывания можно установить вручную при перехвате исключительной ситуации InterruptedException, как показано ниже.
try
{
sleep(delay);
}
catch(InterruptedException exception)
{
Thread.currentThread().interrupt();
}
Этот способ приходится пользоваться в случае, когда метод sleep вызывается из метода, который не может генерировать никаких прерываний.
Чтобы не загромождать код многочисленными вызовами метода isinterrupted для проверки статуса прерывания, можно связать статус прерывания с генерацией исключительной ситуации, как показано ниже.
if (islnterrupted()) throw new InterruptedException();
Довольно часто можно встретить примеры кода, в которых исключительная ситуация InterruptedException перехватывается, но никак не обрабатывается.
try { sleep(delay); }
catch (InterruptedException exception){}
// НИКОГДА HE ДЕЛАЙТЕ ТАК!
Ни в коем случае не делайте так! Рекомендуется либо задавать статус прерывания для текущего потока, либо делегировать исключительную ситуацию вызывающему методу (в конечном итоге методуrun).
Если в коде уже предусмотрено прекращение работы метода run при возникновении исключительной ситуации InterruptedException, то таким образом можно безболезненно и немедленно прекратить работу потока при получении прерывания. Принципиальным недостатком данного способа является необходимость включения в определения методов явного указания throws InterruptedException генерации исключительной ситуации InterruptedException.
java.lang.Threadjava.lang.ThreadМетод void interrupt () посылает запрос на прерывание потока. Для прерванного потока статус прерывания имеет значение true. Если поток блокирован вызовом метода wait или sleep, то генерируется исключительная ситуация InterruptedException.
java.lang.Thread
java.lang.Thread
Метод static boolean interrupted() проверяет, прерван текущий поток (т.е. поток, выполняющий эту команду) или нет. Обратите внимание, что это статический метод, вызов которого имеет побочный эффект, т.е. он задает значение falseдля статуса прерывания текущего потока.
Метод boolean islnterrupted() также проверяет, прерван поток или нет, но в отличие от метода interrupted () этот вызов не изменяет статус прерывания потока.
Метод static Thread currentThread () возвращает объект Thread, представляющий текущий выполняемый поток.
Свойства потоков
состояния потока
Поток может находиться в одном из четырех состояний:
созданный (new);
запущенный (runnable);
блокированный (blocked);
остановленный (dead).
Каждое из состояний подробно рассматривается в следующих разделах.
Созданные потоки
После создания с помощью оператора new поток не запускается, а становится “созданным ” (new). В этом состоянии программа еще не выполняет код этого потока. До запуска потока нужно выполнить несколько важных операций.
Запущенные потоки
После вызова метода start поток становится "запущенным" (runnable). На самом деле код запущенного потока начнет выполняться, только если операционная система предоставит ему вычислительные ресурсы. Если код потока начинает выполняться, то такой поток называется “выполняемым” (running). (В документации платформы Java два этих состояния не различаются и считается, что выполняемый поток находится в запущенном состоянии.)
Запущенное (runnable) состояние потока никак не связано с интерфейсом Runnable.
Совсем необязательно, чтобы код выполняемого потока постоянно выполнялся. На самом деле иногда желательно приостанавливать их работу, чтобы предоставить возможность выполнения параллельных потоков. Если в операционной системе нет должной поддержки многопоточности, то на этот случай в Java предусмотрены минимальные средства организации многопоточности. Например, в платформе Java 1.x для операционной системыSolarisимеетсяпакет "зеленых"потоков (green threads). Зеленыйпотокпродолжает
выполняться до тех пор, пока управление не переходит к потоку с более высоким приоритетом. В других системах (например, Windows 9x или Windows NT/2000) каждому запущенному потоку выделяется интервал времени процессора для его выполнения. По окончании этого интервала операционная система выполняет другой поток и т.д. Такой подход называется квантованием времени выполнения (time slicing). Нарис. 1.4показанасхемакванто- вания времени на компьютере с одним процессором. Он обладает важным преимуществом: один поток не может заблокировать выполнение других потоков. В текущих версиях платформы Java для Solaris допускается выбор такой конфигурации, в которой можно использовать встроенные потоки операционной системы Solaris, позволяющие применять квантование времени выполнения.
Помните о том, что запущенный поток может выполняться, а может и не выполняться в данный момент времени. Именно поэтому состояние называется "запущенным" ("runnable"), а не "выполняемым" ("running").
Запущенные потоки высокогоЗапущенные потоки высокогоприоритетапотокиприоритетапотокиЗапущенные
Запущенные потоки высокого
Запущенные потоки высокого
приоритетапотоки
приоритетапотоки
потоки
низкогоБлокированные
I - интервал времени выполнения
Рис. 1.4. Квантование времени выполнения в системе с одним процессором
Блокированныепотоки
Поток становится блаки^ованннм. (blocked) после выполнения одного из следующих действий.
Для данного потока вызван метод sleep ().
Поток вызывает операцию блокирования на время ввода/вывода (blocking on in- put/output), т.е. управление не возвращается до тех пор, пока не будут завершены операции ввода/вывода.
Поток вызывает метод wait ().
Поток пытается заблокировать объект, который уже блокирован другим потоком. Блокировка объектов подробно рассматривается ниже в этой главе.
Для данного потока вызван метод suspend (). По приведенным ниже причинам не рекомендуется использовать этот метод.
Состояние потока, а также схема возможных изменений этого состояния показана на рис. 1.5. Если поток блокирован (или его работа прекращена), то в соответствии с планом выполнения потоков запускается другой поток. Если блокированный поток вновь активизируется (например, по прошествии заданной паузы или завершении операций ввода/вывода), планировщик выполнения потоков проверяет приоритет данного потока. Если он имеет более высокий приоритет, чем выполняемые в данный момент потоки, то прерывает текущий поток и запускает новый.
Эта операция называется вытеснением (preemption) выполняемого потока. (На компьютере с несколькими процессорами каждый из процессоров может независимо обрабатывать свой поток, в результате чего сразу несколько потоков могут выполняться параллельно. При использовании такой архитектуры поток прерывается лишь тогда, когда запускается поток более высокого приоритета и нет свободных процессоров.)
Рис. 1.5. Схема возможных изменений состояния потока
Например, метод run класса BallThread блокирует поток BallThread после завершения очередного перемещения шара с помощью вызова метода sleep с указанием паузы.
Это позволяет процессору выполнять другие потоки (в данном случае, потоки других шаров и главный поток).
Если в системе имеется несколько процессоров, то одновременно может выполняться сразу несколько потоков.
Выход из блокированного состояния
Для выхода из блокированного состояния для потока следует выполнить действия, противоположные тем, которые привели его в это состояние.
Если запущен метод sleep, то поток продолжит выполнение по истечении заданной паузы.
Если поток ожидает завершения операций ввода/вывода, то он продолжит выполнение по окончании ввода/вывода.
Если в потоке был вызван метод wait, то в другом потоке должен быть вызван метод notifyAll или notify. (Методы wait и notifyAll/notify более подробно рассматриваются ниже в этой главе.)
Если поток ожидает разблокировки объекта, заблокированного другим потоком, то последний должен передать права владения блокировкой этого объекта. (Более подробно передача блокировки рассматривается ниже в этой главе.)
Если поток был приостановлен методом suspend, то должен быть вызван метод resume. Но, так как метод suspend не рекомендуется использовать, то точно также не рекомендуется использовать и метод resume.
Л—z——
Блокированныйпоток можновернутьв выполняемоесостояниетолькопотомуже пути, по которому он был блокирован. Например, если поток был блокирован на время выполнения операции ввода, то вызов метода resume не позволит разблокировать его.
Остановленные потоки
Поток может находиться в остановленном (dead) состоянии по следующим двум причинам.
Метод run нормально завершил работу.
Неперехваченная исключительная ситуация внезапно прекратила выполнение метода run.
Поток можно остановить, явно вызывая метод stop, который генерирует исключительную ситуацию ThreadDeath и останавливает выполнение потока. Однако, метод stop не рекомендуется использовать по причинам, которые рассматриваются ниже.
Для определения работоспособности потока (т.е. находится ли он в запущенном или блокированном состоянии) применяется метод isAlive. Если поток запущен или заблокирован, то этот метод возвращает значение true, а если поток является созданным, но еще не запущенным или остановленным, то метод возвращает значение false.
К сожалению, для работоспособного потока нельзя определить, является ли он запущенным или все-таки блокированным, а также, является ли запущенный поток выполняемым. Кроме того, нельзя различить еще не запущенный поток и уже прекращенный поток.
Потоковые демоны
Поток можно превратить в потоковый демон (daemon thread) с помощью вызова метода set Daemon.
t.setDaemon(true);
Демон представляет собой поток, единственным предназначением которого является обслуживание других потоков. Примером может служить поток таймера, который через определенные интервалы времени посылает сигналы другим потокам. Если в данной программе запущенными остаются только потоковые демоны, то такая программа завершает работу (так как продолжать ее просто не имеет смысла).
Группы
В некоторых программах содержится довольно много потоков. В этом случае имеет смысл разбить их на функциональные категории. Допустим, что в Web-броузере несколько потоков пытаются загрузить несколько изображений с Web-сервера. Если пользователь нажимает кнопку Stop (чтобы прервать загрузку текущей страницы), то было бы удобно прервать выполнение одновременно всех потоков загрузки изображений. В языке Java предусмотрен механизм создания так называемой группы потоков для одновременной работы с несколькими потоками.
Для создания группы применяется следующий конструктор:
String= ...;
gnew
Строковый аргумент конструктора ThreadGroup идентифицирует группу и поэтому должен быть уникальным. Потоки добавляются в группу за счет указания группы в конструкторе потока, как показано ниже.
Thread t = new
Для проверки наличия выполняемых потоков в группе запущенных потоков применяется метод activeCount.
if== 0)
{
// все потоки в группе g остановлены }
Для прерывания всех потоков в группе достаточно вызвать метод interrupt для объекта-группы.
// прерывает все потоки в группе д
Подробнее прерывания потоков рассматриваются в следующем разделе.
Группы потоков могут содержать дочерние подгруппы. По умолчанию новая группа потоков становится дочерней по отношению к текущей, но в конструкторе можно явно указать имя родительской группы. Методы activeCount и interrupt относятся ко всем потокам группы и ее дочерним подгруппам.
Замечательное свойство групп потоков заключается в том, что группа может послать уведомление, если поток был остановлен в результате возникновения исключительной ситуации. Для обработки этой исключительной ситуации необходимо создать дочерний
класс на основе класса ThreadGroup и переопределить метод uncaughtException. После чего можно заменить выполняемые по умолчанию действия (т.е. вывод состояния стека в стандартный поток ошибок) какими-то более сложными действиями, например, записать ошибки в файл или вывести их в диалоговом окне.
3 ava • 1апЯ • Thread
I AjP| 11
Конструктор Thread(ThreadGroup g, String name) создает новый поток
Thread в группе ThreadGroup.
Параметры:
gгруппа нового потока
nameимя нового потока
Метод ThreadGroup getThreadGroup () возвращает группу, к которой принадлежит поток.
Метод boolean isAlive () возвращает значение true, если поток запущен и еще не остановлен.
Метод void resume () возобновляет выполнение потока. Этот метод применяется после вызова метода suspend (). He рекомендуется использовать оба эти метода.
Метод void setDaemon (boolean on) отмечает данный поток как пользовательский или потоковый демон. Когда в системе остаются только потоковые демоны, программа прекращает выполнение. Этот метод нужно вызывать до запуска обычного потока.
Метод void stop () прекращает работу потока. Не рекомендуется использовать этот метод.
. !Pff^ava'lang•ThreadGroup
I lurJ • Конструктор ThreadGroup(String name) создает новую группу Thread- Group. Родительской по отношению к ней является группа текущего потока.
Параметры:
nameимя новой группы потоков
Конструктор ThreadGroup(ThreadGroup parent, String name) создает новую группу ThradGroup.
Параметры:
parentгруппа, родительская по отношению к новой группе
nameимя новой группы потоков
Метод int activeCount () возвращает максимальное количество выполняемых потоков в группе.
Метод int enumerate(Thread[] list) возвращает количество всех выполняемых потоков группы из заданного массива ссылок на потоки. Для определения размера массива можно использовать метод activeCount, который возвращает максимальное количество выполняемых потоков. Если массив оказывается слишком коротким (например, когда новые потоки создаются после вызова метода activeCount), то он заполняется полностью, а непоместившиеся потоки не учитываются.
Параметры:
listмассив ссылок на потоки
Метод ThreadGroup getParent () возвращает группу, родительскую по отношению к данной.
Методvoid interrupt () прерывает все потоки в группе и ее дочерних подгруппах.
Методvoid uncaughtException (Thread t, Throwable e) необходимо переопределить, чтобы он реагировал на исключительные ситуации, которые прекращают выполнение потоков в данной группе. По умолчанию вызов этого метода приводит к вызову такого же метода родительской группы (если таковая есть) или выводит содержимое стека в стандартный поток ошибок. (Если исключительная ситуация е является объектом ThreadDeath, то содержимое стека не выводится. Объекты ThreadDeath генерируются устаревшим методом stop.)
Параметры:
tпоток, выполнение которого прекращено неперехваченной
исключительной ситуацией
еобъект неперехваченного прерывания
Приоритеты потоков
В языке программирования Java каждый поток имеет приоритет (priority), который по умолчанию наследуется как приоритет родительского потока. Для повышения или понижения приоритета потока используется метод setPriority. Возможные значения приоритета находятся в диапазоне от минимального значения MIN_PRIORITY (1 в классе Thread) до максимального значения MAX_PRIORITY(10 в классе Thread). Обычный при- opHTeTNORM_PRIORITY, который используется по умолчанию, имеет значение 5.
Вобщем случае планировщиквыполненияпотоковвыбирает поток с наивысшим приоритетом среди запущенных в данный момент.
НИМАМИНИМАМИПрежде всего следует отметить, что правила обработки приоритетов потоков сильно зависят от используемой системы. Если виртуальная машина Java полагается на реализацию потоков в используемой системе, то расписание выполнения потоков будет целиком и полностью зависеть от этой реализации. Виртуальная машина Java устанавливает соответствие между приоритетами потоков и приоритетами процессов в операционной системе (в которой может быть больше или меньше уровней приоритетов). В этом разделе описан идеальный случай, который до определенной степени реализуется в каждой виртуальной машине.
НИМАМИ
НИМАМИ
Запущенный поток с наивысшим приоритетом выполняется до тех пор, пока не будет выполнено одно из перечисленных ниже действий.
Управление передается с помощьюметода yield.
Изменяется статус потока, т.е. запущенный поток либо заблокирован, либо остановлен.
Запущен поток с более высоким приоритетом (либо по истечении заданной паузы, либо после завершения операций ввода/вывода, либо после разблокировки объекта, который нужен данному потоку, с помощью метода notifyAl1/notifу).
После этого планировщик выполнения потоков выбирает новый выполняемый поток, который естественно должен обладать наивысшим приоритетом среди оставшихся запущенных потоков.
А что же происходит, когда несколько потоков имеют одинаковый (высший) приоритет? В таком случае по усмотрению планировщика выполнения потоков выбирается какой-то один из них. В языке программирования Java не гарантируется равноправный подход к обработке всех потоков с одинаковым приоритетом со стороны планировщика выполнения потоков. Конечно же, хотелось бы, чтобы все потоки с одинаковым приоритетом обслуживались по очереди, чтобы у каждого из них была возможность выполнить свою работу. Теоретически, на некоторых платформах возможна ситуация, когда планировщик выбирает поток случайно или же обрабатывает первый доступный из них. Такое свойство является недостатком языка Java и затрудняет создание многопоточных программ, которые могли бы выполнятся строго одинаково на всех виртуальных машинах.
На некоторых платформах (например, Windows NT) предусмотрено меньше уровней приоритета, чем в Java. В таких случаях независимо от того, какое соответствие уровней выбирается, некоторые из 10 уровней JVM будут отвечать одному и тому же уровню приоритета используемой платформы. В Sun JVM для Linux приоритеты потоков игнорируются, поэтому в ней нельзя будет увидеть в действии Express-потоки быстрых шаров, которые используются в примере программы, приведенной в конце этого раздела. Если в операционной системе предусмотрено меньше уровней приоритета, чем в Java, то поток с более высоким приоритетом может оказаться вытесненным потоком с меньшим приоритетом. Это означает, что в многопоточных программах не рекомендуется полагаться на уровни приоритета.
Рассмотрим, например, вызов метода yield. В некоторых реализациях он может не дать никакого эффекта, если уровни приоритета всех запущенных потоков соответствуют одному и тому же уровню приоритета в данной операционной системе. Планировщик выполнения потоков может просто возвратить управление данному потоку, не обращая внимания на другие потоки. Поэтому в таких случаях рекомендуется вызывать метод sleep, тогда, по крайней мере, планировщик будет знать, что текущий поток требуется приостановить в пользу других потоков.
В большинстве виртуальных машин предусмотрено несколько (не обязательно 10) уровней приоритета. В некоторых планировщиках выполнения потоков предусмотрена последовательная обработка потоков с одинаковым приоритетом. Например, если проследить за несколькими потоками в предыдущем примере с шарами, то можно заметить, что все шары двигаются примерно с одинаковой скоростью. Конечно же, такое поведение нельзя гарантировать всегда. Для точного планирования обработки потоков программисту потребуется реализовать самостоятельно все необходимые правила обработки.
Рассмотрим следующий пример, который является видоизмененным вариантом предыдущей программы. В этом примере потоки одного типа (шары красного цвета) имеют больший приоритет, чем потоки другого типа. (Строка кода, выделенная полужирным начертанием, показывает способ увеличения приоритета потока.)
После щелчка на кнопке Start запускается поток с обычным приоритетом, который имитирует движение шара черного цвета. После щелчка на кнопке Express запускается поток с более высоким приоритетом, который имитирует движение шара красного цвета.
class BounceFrame extends JFrame
{
p{ublic BounceFrame()
addButton(buttonPanel, "Start", newActionListener()
{
public void actionPerformed(ActionEvent evt)
{
addBall(Thread.NORM_PRIORITY, Color.black);
} });
addButton(buttonPanel, "Express", newActionListener()
{
p{ublic void actionPerformed(ActionEvent evt)
. addBall(Thread.NORM_PRIORITY+2, Color.red);
public void addBall(int priority, Color color)
{
Ball b= new Ball(canvas, color);
canvas.add(b);
BallThread thread = new BallThread(b);
thread.setPriority(priority);
thread.start();
}
}
Запустите программу и создайте несколько обычных и быстрых шаров. Последние передвигаются быстрее из-за более высокого приоритета потоков, а не потому, что для них задана более высокая скорость. Действительно, для быстрого шара используется тот же код перемещения, что и для обычного шара.
Вот как функционирует эта программа. По завершении паузы, т.е. через 5 мс после вызова метода sleep, планировщик начинает обработку потоков.
Планировщик снова оценивает приоритеты всех запущенных потоков.
Затем он определяет, что потоки быстрых шаров имеют более высокий приоритет.
После этого один из быстрых потоков получает возможность выполнения. Это может быть либо только что разблокированный поток, либо другой быстрый поток, но наверняка этого сказать нельзя. Быстрые потоки обрабатываются в первую очередь и лишь тогда, когда все они находятся в состоянии sleep, планировщик начинает обрабатывать потоки с более низким приоритетом. Внешний вид этой программы показан на рис. 1.6, а ее код представлен в листинге 1.3.
Учтите, что потоки с низким приоритетом не имели бы никакой возможности выполнить свою работу, если бы быстрые потоки приостанавливались методом sleep, a не методом yield.
Еще раз следует отметить, что эта программа надежно работает под управлением операционных систем Windows NT и Solaris. Однако, спецификации языка Java не гарантируют, что она будет также хорошо работать и на других платформах.
Рис. 1.6. Потоки различных приоритетов
•
Учтите, что если для обеспечения работоспособности кода приходится настраивать приоритеты потоков, то в такой код вряд ли будет надежно работать. Прочтите остальную часть этой главы или обратитесь к одной из приведенных ссылок и под- робнееизучитенадежныемеханизмыуправлениямногопоточнымипрограммами.
Листинг 1.3. BounceExpress.java
Aversion 1.30 2001-05-06
@author Cay Horstmann
/
import java.awt.*;
import java.awt.event. * ;
import java.awt.geom.*;
import java.util.*;
import j avax.swing.*;
/**
Пример имитации движения шаров на основе использования двух типов потоков с разным приоритетом
/
public class BounceExpress
{
public static void main(String[] args)
{
JFrame frame = new BounceFrame () ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
}
}
/**
Фрейм с канвой и кнопками
/
class BounceFrame extends JFrame
/**
Создание фрейма с канвой для отображения
движущихся шаров, а также кнопок Start и Close
/
public BounceFrame()
{
setsize(WIDTH, HEIGHT);
setTitle("BounceExpress");
Container contentPane = getContentPane();
canvas = newBallCanvas() ;
contentPane.add(canvas, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
addBall(Thread.NORM_PRIORITY, Color.black); }
});
addBut ton(buttonPanel, "Express", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
addBall(Thread.NORM_PRIORITY + 2, Color.red); }
});
addButton(buttonPanel, "Close", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
System.exit(0);
} });
contentPane. add(buttonPanel, BorderLayout. SOUTH) ;
/**
Создание кнопки в контейнере
Sparam с the container
@param title the button title
Sparam listener the action listener for the button
/
public void addButton(Container c, String title, ActionListener listener)
{
JButton button = new JButton(title);
c.add(button) ;
button.addActionListener(listener) ;
}
/**
Создание шара в канве и запуск его потока
Sparam priority the priority for the threads
Scolor the color for the balls
*/
public void addBall(int priority, Color color) {
Ball b = new Ball(canvas, color);
canvas.add(b);
BallThread thread = new BallThread(b);
thread.setPriority(priority);
thread.start();
private BallCanvas canvas;
public static final int WIDTH = 450;
public static final int HEIGHT = 350;
/**
Поток движения шара.
*/
class BallThread extends Thread {
/**
Создание потока
@aBall the ball to bounce
*/
public BallThread(Ball aBall) { b = aBall; }
public void run() {
try {
for (int i = 1; i <= 1000; i++) {
b.move(); sleep(5);
} }
catch (InterruptedException exception)
) }
private Ball b;
)
/**
Канва с шарами.
*/
class BallCanvas extends JPanel {
/**
Включение шара в канву
@param b the ball to add
*/
public void add(Ball b) {
balls.add(b);
}
p{ublic void paintcomponent(Graphics g) super.paintcomponent(g) ;
Graphics2D g2 = (Graphics2D)g;
for (int i = 0; i < balls.size(); i++) {
Ball b = (Ball)balls.get(i) ;
b.draw(g2);
private ArrayList balls = new ArrayList();
Шар, который может перемещаться в канве и отражаться от границ окна */ class Ball {
/**
Создание шара в верхнем левом углу
@с the component in which the ball bounces @aColor the color of the ball
*/
public Ball(Component c, Color aColor) {
canvas = C;
color = aColor;
/ * *
Отображение шара в его текущей позиции.
@param g2 the graphics context
*/
public void draw(Graphics2D g2)
{
g2.setcolor(color) ;
g2.fill(new Ellipse2D.Double(x, y, XSIZE, YSIZE),;
/**
Перемещение шара в следующую позицию с
обращением направления движения при отражении
от границ окна
*/
public void move()
{
х += dx;
У += dy;
if (x < 0)
{
x = 0;
dx = -dx;
}
if (x + XSIZE >= canvas.getWidth())
{
x = canvas.getWidth() - XSIZE;
dx = -dx;
}
if (y < 0)
{
У = 0;
dy = -dy;
if (у + YSIZE >= canvas.getHeight()) {
у = canvas.getHeight() - YSIZE; dy = -dy;
}
canvas.repaint();
}
privateComponentcanvas;
private Color color;
private static final int XSIZE =15;
private static final int YSIZE = 15;
privateintx=0;
private int у = 0;
private int dx = 2 ;
private int dy = 2 ;
}
A
PI
. i L • Метод void setPriority(int newPriority) устанавливает приоритет SSIпотока в интервале от Thread.MIN_PRIORITYдо Thread.MAX-PRIORITY. По
умолчанию для приоритета задается значение Thread.NORM-PRIORITY.
Значение static int MIN_PRIORITY используется для указания минимального приоритета потока и равно 1.
Значение static int NORM-PRIORITY используется для указания заданного по умолчанию приоритета потока и равно 5.
Значение static int MAX-PRIORITY используется для указания максимального приоритета потока. Оно равно 10.
Метод static void yield () принудительно отнимает управление у текущего потока и передает его планировщику. При наличии других потоков с не меньшим приоритетом, чем у данного потока, они будут следующими в очереди выполняемых потоков. Обратите внимание, что это статический метод.
Монопольные потоки
Каждый поток, обрабатывающий перемещение шара, дает возможность планировщику выполнять и другие потоки. Для этого в нем вызывается метод sleep, который блокирует поток и позволяет планировщику выполнять другие потоки. Если выполнение потока желательно не приостанавливать ни на секунду, то в таком случае необходимо вызывать метод yield() в тех местах, где можно было бы прервать его работу. При выполнении длинного цикла для предотвращения монопольного использования вычислительных ресурсов системы в потоке всегда следует организовать вызов метода sleep или yield. Потоки, нарушающие это правило, называются монопольными потоками (selfish threads).
В следующем примере демонстрируется поток со сплошным циклом (tight loop), т.е. циклом, который выполняется без передачи управления другим потокам. После щелчка на кнопке Selfish (Шар монопольного потока) на экране появляется синий шар, в методе run которого содержится следующий сплошной цикл.
class BallThread extends Thread {
public void run()
{
try
{
for (int i = 1; i <= 1000; i
{
b.move();
if (selfish)
// Пауза 5 миллисекунд
long t = System.currentTimeMillis();
while (System.currentTimeMillis() < t + 5)
} ' else
^ sleep(5);
}
catch (InterruptedException exception)
}
}
bp
rivate Ball b;
oolean selfish;
}
Если установлен флажок selfish, то метод run выполняется в течение пяти секунд без вызова методов sleepи yield, а затем работа потока завершается.
Способ работы этой программы в значительной степени зависит от операционной системы и способа управления потоками. Например, если запустить эту программу в операционной системе Solaris или Linux, где вместо "собственных встроенных потоков" ("native threads") реализованы "зеленые потоки" ("green threads"), то монопольный поток поглощает все вычислительные ресурсы программы. При этом вам не удастся не только закрыть программу или запустить другую, но и просто дождаться обработки щелчка кнопкой мыши. Если же запустить эту программу в операционной системе с "собственными встроенными потоками", например, в Windows, то ничего подобного не происходит. Движение синего шара обрабатывается параллельно с другими действиями.
Причина такой разницы заключается в том, что механизм обработки потоков, лежа- щийвосновеоперационнойсистемы,выполняетразделение времени выполнения (time slicing). Он периодически прерывает выполнение потоков, даже если они никак не взаимодействуют друг с другом. В этом случае даже монопольный поток прерывается и управление переходит к следующему потоку с наивысшим приоритетом. В реализации "зеленых потоков" в операционных системах Solaris и Linux (в отличие от "собственных встроенных потоков") такое разделение времени выполнения не предусмотрено. (Остается загадкой, почему бы всюду не использовать собственные встроенные потоки? До недавнего времени графические системы Xll и Motif вообще не позволяли надежно использовать потоки, и применение встроенных потоков могло привести к зависанию диспетчера окон.) Кроме того, при переносе Java-кода на другие платформы, как правило, сначала реализуется механизм "зеленых потоков", поскольку его легче транслировать.
Если вы уверены, что программа будет надежно работать в среде с системой управления потоками на основе разбиения времени выполнения, то в таком случае беспокоиться не о
чем. Однако, основное достоинство Internet-ориентированного приложения заключается как раз в обратном, т.е. в общем случае разработчику ничего не нужно знать о том, в какой среде это приложение будет выполняться. Поэтому следует приготовиться к самому худшему варианту и организовать вызовы методов sleepили yieldв каждом цикле.
При программировании потоков следует учитывать, что программа может вести себя по-разному на разных платформах. Не стоит полагаться, что одни потоки будут "вытеснены" другими, а лучше явным образом спланировать передачу управления (иначе одни потоки будут выполняться бесконечно). Кроме того, не стоит рассчитывать на то, что потоки будут выполняться без прерываний, потому что на большинстве платформ это не так. Следует быть готовым к тому, что поток может в любой момент потерять управление.
Код программы с применением монопольного потока для имитации движения синего шара приведен в листинге 1.4.
Листинг 1 4. BounceSelfiah.java
@version 1.30 2001-05-06
@author Cay Horstmann
*/
import java.awt.*;
import j ava.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
/**
Пример использования монопольного потока
для имитации движения синего шара
*/
public class BounceSelfish
{
public static void main(String!] args)
{
JFrame frame = new BounceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
)
}
/ * *
Фрейм с канвой и кнопками
*/
class BounceFrame extends JFrame {
/**
Создание фрейма с канвой для отображения
движущихся шаров, а также кнопок Start, Selfish и Close */
public BounceFrame() {
setSize(WIDTH,HEIGHT);
setTitle("BounceSelfish");
Container contentpane =
canvas = new BallCanvas();
contentPane.add(canvas, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start",
new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
addBall(false, Color.black);
}
}) ;
addButton(buttonPanel, "Selfish",
new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
addBall(true, Color.blue);
}
});
addButton(buttonPanel, "Close", new ActionListener()
{
public void actionPerformed(ActionEvent evt) {
System.exit(0) ;
}
});
contentPane.add(buttonPanel, BorderLayout.SOUTH);
/**
Включение кнопки в контейнер
@param с the container
@param title the button title
@param listener the action listener for the button
*/
public void addButton(Container c, String title, ActionListener listener)
{
JButton button = new JButton(title);
c.add(button);
button.addActionListener(listener);
}
/**
Создание шара в канве и запуск его потока
@param priority the priority for the threads
©color the color for the balls
*/
public void addBall(boolean selfish, Color color)
{
Ball b = new Ball(canvas, color);
canvas.add(b);
BallThread thread = new BallThread(b, selfish);
thread.start();
private BallCanvas canvas;
public static final int WIDTH = 450;
public static final int HEIGHT = 350; }
/**
Поток движения шара
*/
class BallThread extends Thread {
Создание потока
©aBall the ball to bounce
©boolean selfishFlag true if the thread is selfish, using a busy wait instead of calling sleep
*/
public BallThread(Ball aBall, boolean selfishFlag) {
b = aBall;
selfish = selfishFlag;
}
public void runt) {
try {
for (int i = 1; i <= 1000; i++) {
b.move();
if (selfish) {
// Пауза 5 миллисекунд
long t = System.currentTimeMillis();
while (System.currentTimeMillis() < t + 5)
}
else
sleep(5) ;
} } catch (InterruptedException exception) { )
}
private Ball b;
boolean selfish;
}
/**
Канва с шарами
*/
class BallCanvas extends JPanel {
/**
Включение шара в канву
@param b the ball to add
*/
public void add(Ball b) {
balls.add(b);
}
public void paintComponent(Graphics g)
super.paintComponent(g) ;
Graphics2D g2 = (Graphics2D)g;
for (int i = 0; i < balls.size(); i++)
{
Ball b = (Ball)balls.get(i) ;
b.draw(g2) ;
}
}
private ArrayList balls = new ArrayList();
/**
Шар, который может перемещаться
в канве и отражаться от границ окна
*/ class Ball {
/ * *
Создание шара в верхнем левом углу
@c the component in which the ball bounces
@aColor the color of the ball
*/
public Ball(Component c, Color aColor) {
canvas = c;
} color = aColor;
/**
Отображение шара в его текущей позиции
Sparam g2 the graphics context
*/
public void draw(Graphics2D g2) {
g2.setColor(color);
g2.fill(new Ellipse2D.Double(x, y, XSIZE, YSIZE));
/**
Перемещение шара в следующую позицию с обращением направления движения при отражении от границ окна
*/
public void move() {
X += dx;
У += dy;
if (x < 0) {
x = 0; dx = -dx; }
if (x + XSIZE >= canvas.getwidth()) {
x = canvas.getwidth() - XSIZE; dx = -dx;
if (у < 0) {
У = 0;
dy = -dy;
}
if (у + YSIZE >= canvas.getHeight(J) {
у = canvas.getHeight() - YSIZE;
dy = -dy;
canvas.repaint();
}
private Component canvas;
private Color color;
private static final int XSIZE = 15;
private static final int YSIZE = 15;
private int x = 0;
private int у = 0;
private int dx = 2 ;
private int dy = 2;
Синхронизация
Довольно часто в многопоточных приложениях двум или более потокам требуется предоставить доступ к одним и тем же объектам. Что же происходит, когда два потока осуществляют доступ к одному объекту и вызывают методы, изменяющие его состояние? Как легко догадаться, они "наступают друг другу на пятки", и, в зависимости от порядка доступа к данным, объект может быть поврежден. Такая ситуация называется гонкой (race condition).
Взаимодействие потоков без синхронизации
Во избежание нежелательных последствий одновременного обращения нескольких разных потоков к одному объекту, необходимо синхронизовать доступ к нему. В этом разделе описываются возможные способы поведения многопоточной программы при отсутствии синхронизации, а в следующем — способы синхронизации.
Следующий пример имитирует работу банка с 10 счетами, в котором для перевода денег с одного счета на другой случайным образом генерируются транзакции. В программе используется 10 потоков — по одному для каждого счета. Транзакция перемещает произвольную сумму со счета, обслуживаемого данным потоком, на случайно выбранный счет.
Код программы достаточно прост и основан на классе Bank с методом transfer. Этот метод пересылает определенную сумму с одного счета на другой. Если исходный счет не содержит достаточной суммы денег, то ничего не происходит, и метод просто возвращает управление. Вот как выглядит код метода transferкласса Bank.
public void transfer(int from, int to, double amount)
// ПРЕДУПРЕЖДЕНИЕ: этот код не надежен
// при вызове из нескольких потоков {
if (accounts[from] < amount) return;
accounts[from] —= amount;
accounts[to] += amount;
ntransacts++;
if (ntransacts % NTEST == 0) test ();
}
НижеприводитсяопределениеклассаTransferThreadсметодомrun, который содержит бесконечный цикл перевода денег с определенного банковского счета. На каждом шаге цикла метод run выбирает произвольным образом счет назначения и сумму, а потом вызывает метод transfer. Затем его выполнение приостанавливается с помощью метода sleep.
class TransferThread extends Thread {
public TransferThread(Bank b, int from, int max)
{
bank = b;
fromAccount = from;
maxAmount = max;
}
public void run()
{
try
{ while (!interrupted()) {
for (int i = 0; i < REPS; i++)
{ int toAccount = (int)(bank.size() * Math.random()); int amount
= (int)(maxAmount *Math.random() / REPS); bank.transfer(fromAccount, toAccount, amount); sleep (1) ;
)
}
}
catch(InterruptedException e) {}
}
private Bank bank;
private int fromAccount ;
private int maxAmount;
private static final int REPS = 1000;
}
При выполнении этих операций неизвестно, сколько денег содержится на каждом счету в банке в заданный момент. Однако, нам известна общая сумма, которая должна оставаться неизменной, поскольку все транзакции происходят в пределах одного банка.
Через каждые 10000 транзакций метод transferвызывает метод test, который подсчитываетивыводитобщуюсуммуденежныхсредствбанка.
Эта программа можетработатьбесконечнодолго, и для выхода из нее необходимо нажать комбинацию клавиш <Ctrl+C>.
Ниже показан типичный результат ее работы.
Transactions:10000 Sum: 10000
Transactions:20000 Sum: 10000
Transactions:30000 Sum: 10000
Transactions:40000 Sum: 10000
Transactions:50000 Sum: 10000
Transactions:60000 Sum: 10000
Transactions:70000 Sum: 10000
Transactions:80000 Sum: 10000
Transactions:90000 Sum: 10000
Transactions:110000 Sum: 10000
Transactions:120000 Sum: 10000
Transactions:13 0000 Sum: 94792
Transactions:140000 Sum: 94792
Transactions:150000 Sum: 94792
Как видите, для нескольких транзакций баланс банка остается равным 100000, т.е. сумме 10 счетов по 10000 каждый. Но через некоторое время он слегка изменяется. При запуске программы эта ошибка может проявиться мгновенно или спустя длительное время. Разумеется, такая работа программы не вызывает доверия к банку, и его клиенты вряд ли захотят размещать в нем свои сбережения.
Полный исходный код программы приведен в листинге 1.5. Попытайтесь самостоятельно найти ошибку и сравните результаты вашего поиска с объяснениями, которые содержатся в следующем разделе.
Листинг1.5. UnsynchBankTest.java
/**
©version 1.20 1999-04-26
©author Cay Horstmann
*/
/**
Пример программы, в которой возможно ошибочное изменение данных при одновременном доступе к ним со стороны нескольких потоков
*/
public class UnsynchBankTest
{
public static void main!String[] args) {
Bank b = new Bank(NACCOUNTS, IMITIAL_BALANCE) ; inti ;
for (i = 0; i < NACCOUNTS; i++) {
TransferThread t = new TransferThread(b, i, INITIAL_BALANCE);
t.setPriority(Thread.NORM_PRIORITY + i % 2) t.start();
}
}
public static final int NACCOUNTS = 10;
public static final int INITIAL_BALANCE = 10000; }
/**
Банк с несколькими счетами
*/
class Bank {
/**
Создание банка
©param n the number of accounts
@param initialBalance the initial balance for each account
*/
public Bank(int n, int initialBalance) {
accounts = new int[n];
int i;
for (i = 0; i < accounts.length; i++) accounts[i] = initialBalance;
ntransacts = 0;
}
/**
Перевод денег с одного счета на другой ©param from the account to transfer from ©param to the account to transfer to @param amount the amount to transfer
/
public void transfer(int from, int to, int amount) throws InterruptedException
{
accounts[from] -= amount;
accounts[to] += amount;
ntransacts++;
if (ntransacts % NTEST == 0) test ();
}
/**
Вывод тестового сообщения для проверки целостности данных о балансе банка
/
public void test()
{
int sum = 0;
for (int i = 0; i < accounts.length; i++) sum += accounts[i] ;
System.out.println("Transactions:" + ntransacts + " Sum: " + sum);
}
у * *
Возвращает количество счетов в данном банке @return the number of accounts
/
public int size()
{ return accounts.length;
}
public static final int NTEST = 10000;
private final int[] accounts;
private long ntransacts = 0;
}
/**
Поток перевода денег с одного счета на другой
*/
class TransferThread extends Thread
/**
Создание потока перевода
@param b the bank between whose account money is transferred
брагам from the account to transfer money from
@param max the maximum amount of money in each transfer
*/
public TransferThread(Bank b, int from, int max)
{
bank = b;
fromAccount = from;
maxAmount =
}
public void run()
{
try
{
while (Iinterrupted())
{
for (int i = 0; i < REPS; i++)
{
int toAccount = (int)(bank.size() * Math.random() ) ;
int amount = (int)(maxAmount *Math.random() / REPS);
bank.transfer(fromAccount, toAccount, amount); sleep(l);
}
}
}
catch(InterruptedException e) {}
}
private Bank bank;
private int fromAccount;
private int maxAmount;
private static final int REPS = 1000;
}
Синхронизация доступа к общим ресурсам
В предыдущем разделе приводится пример программы, в которой несколько потоков могут одновременно обновлять баланс банковских счетов. Спустя некоторое время после запуска программы можно заметить, что какая-то часть постоянных средств банка либо спонтанно теряется, либо возникает. Эта проблема происходит в том случае, когда два потока пытаются одновременно обновить состояние счета. Предположим, что два потока одновременно выполняют следующую инструкцию.
accounts[to] += amount;
Проблема заключается в том, что эта инструкция не является атомарной (atomic), т.е. состоит из нескольких операций, и обрабатывается следующим образом.
Загрузка значения accounts [to] в регистр.
Сложение этого значения со значением amount.
Присвоение полученной суммы значению accounts [to].
Предположим, что первый поток прерывается после выполнения операций 1 и 2, как показано на рис. 1.7. Затем разблокируется другой поток и тоже выполняет one-
рации 1 и 2, а затем присваивает значению accounts [to] результат, который может отличаться от полученной в первом потоке суммы. Допустим, что далее разблокируется первый поток, который выполняет операцию 3.
Регистр потока 1Регистр потока 1Поток 2Поток 2Регистр потока 2Регистр потока 2Поток 1Поток 1accounts[to]accounts[to]Рис. 1.7. Схема одновременного доступа двух потоков к одному объектуВ итоге выполнение вторым потоком операции будут полностью игнорироваться, а проверочное суммирование всех денежных средств банка приведет к ошибочному результату.Эта тестовая программа демонстрирует возможность появления такой ошибки.Рис. 1.7. Схема одновременного доступа двух потоков к одному объектуВ итоге выполнение вторым потоком операции будут полностью игнорироваться, а проверочное суммирование всех денежных средств банка приведет к ошибочному результату.Эта тестовая программа демонстрирует возможность появления такой ошибки.
Регистр потока 1
Регистр потока 1
Поток 2
Поток 2
Регистр потока 2
Регистр потока 2
Поток 1
Поток 1
accounts[to]
accounts[to]
Рис. 1.7. Схема одновременного доступа двух потоков к одному объекту
В итоге выполнение вторым потоком операции будут полностью игнорироваться, а проверочное суммирование всех денежных средств банка приведет к ошибочному результату.
Эта тестовая программа демонстрирует возможность появления такой ошибки.
Рис. 1.7. Схема одновременного доступа двух потоков к одному объекту
В итоге выполнение вторым потоком операции будут полностью игнорироваться, а проверочное суммирование всех денежных средств банка приведет к ошибочному результату.
Эта тестовая программа демонстрирует возможность появления такой ошибки.
DDКомпилятор Java позволяет увидеть байт-код, применяемый в виртуальной машине. Запустите команду javap -с -v Bank, которая декомпилирует файл Bank. class. Например, инструкция
D
D
accounts[to] += amount;
декомпилируется в следующий байт-код.
aload_0
getfield #16 <Field Bank.accounts [J>
iload_l
dup2
laload
iload_3
i21
Isub
lastore
Смысл этого кода не имеет большого значения. Главное, он показывает, что команда сложения состоит из нескольких инструкций, а выполнение потока может быть прервано на любой из них.
Вероятность возникновения такой ошибки достаточно мала, поскольку в каждом потоке выполняется слишком мало операций. Путем экспериментов мы обнаружили, что ее можно увеличить различными способами в зависимости от используемой платформы. Например, в операционной системе Windows 98 для этого нужно присвоить одной половине потоков больший приоритет, чем другой.
for (i = 0; i < NACCOUNTS; i++)
TransferThread t = new TransferThread(b, i,
INITIAL_BALANCE) ;
t.setPriority(Thread.NORM_PRIORITY + i % 2) ;
t.start();
}
Когда поток с более высоким приоритетом разблокируется, он вытесняет поток с более низким приоритетом (т.е. прерывает его выполнение, что, как показано выше, приводит к повреждению данных).
Здесь демонстрируется именно та настройка уровней приоритетов, которую ни в коем случае не следует применять в программах. В этом примере программы она используется лишь для демонстрации способа повреждения данных, чего в реальных приложениях следует избегать.
В операционной системе Linux приоритеты потоков игнорируются, поэтому нужно немного изменить метод run, чтобы перевод денежных средств выполнялся несколько раз до блокировки потока.
final int REPS = 1000;
for (int i = 0; i < REPS; i++)
{
int toAccount = (int) (bank.size () * Math.random());
int amount = (int)(maxAmount * Math.random() / REPS);
bank.transfer(fromAccount, toAccount, amount);
sleep(1);
}
Для достижения этого эффекта на всех платформах следует параллельно с этим тестом запустить несколько программ, которые интенсивно расходуют вычислительные ресурсы компьютера.
Основная проблема заключается в том, что выполнение метода transferможет быть прервано посредине. Состояние объекта банковского счета не повреждалось бы никогда, если бы работа этого метода завершалась до того, как поток потеряет управление.
Во многих потоковых библиотеках для организации беспрерывного доступа к ресурсу предусмотрено использование семафоров (semaphores) и критических секций (critical sections). Этого вполне достаточно для процедурного программирования, но не для объектно-ориентированного программирования. В Java предусмотрен более удобный механизм на основе мониторов (monitors), который был предложен Тони Хоаром (Топу Ноаге).
Для обозначения синхронизируемой операции, которую нельзя прерывать, следует использовать ключевое слово synchronized, например так, как показано ниже.
public synchronized void transfer(int from, int to, int amount) {
if (accounts[from] < amount) return;
accounts[from] —= amount;
accounts[to] += amount;
ntransacts++;
if (ntransacts % NTEST == 0) test();
}
Таким образом, если в потоке вызывается синхронизированный метод, то он гарантированно завершится до того, как другой поток запустит синхронизированный метод для обработки того же самого объекта (рис. 1.8). Если метод transferвызыва- ется в одном, а затем в другом потоке, то работа второго потока будет приостановлена, пока первый поток на завершит выполнение метода transfer.
Запустите эту программу снова, пометив метод transfer ключевым словом synchronized. Теперь баланс банка никогда не нарушится.
В общем случае отмечать ключевым словом synchronized необходимо те методы, для выполнения которых требуется несколько операций чтения и обновления данных. В этом случае все операции будут завершены до того, как обращение к тому же самому объекту будет происходить из другого потока.
НесинхронизированныйСинхронизированный
Поток 1Поток 2Поток 1Поток 2
transfertransfer> transfer> transfer>■ transfer>■ transfer
transfer
transfer
> transfer
> transfer
>■ transfer
>■ transfer
transfer
Рис. 1.8. Сравнение синхронизированных и несинхронизированных потоков
Использование механизма синхронизации связано с некоторыми накладными расходами. Как показано в следующем разделе, при каждом вызове синхронизированного мето-
да запускается дополнительный учетный код. Поэтому не следует синхронизировать все используемые методы. Если какие-то объекты не предназначены для совместного доступа со стороны разных потоков, то их не нужно синхронизировать. Если какой-то метод всегда возвращает одно и то же значение объекта, то его также не нужно синхронизировать. Например, метод size класса Bank синхронизировать не нужно, потому что объект Bank имеет фиксированный размер, заданный его конструктором.
Синхронизация каждого класса обходится слишком дорого с точки зрения вычислительных ресурсов, поэтому для организации взаимодействия потоков рекомендуется использовать пользовательские классы. Предположим, что броузер загружает несколько изображений параллельно и хочет узнать, когда все они будут загружены полностью. Для этого можно определить пользовательский класс ProgressTracker с синхронизированными методами, которые обновляют и запрашивают данные о ходе загрузки изображений.
Некоторые программисты, имеющие опыт работы с другими потоковыми моделями, критически оценивают механизм синхронизации Java, считая его запутанным и неэффективным. Это вполне справедливо для программирования на системном уровне, но неверно для прикладного программирования, где модель синхронизации Java прекрасно себя зарекомендовала. Достаточно лишь помнить о возможности использовании специализированных пользовательских классов, предназначенных для организации взаимодействия потоков, и не пытаться необоснованно перегружать код ключевыми словами synchronized.
В некоторых случаях программисты пренебрегают синхронизацией во избежание накладных расходов в коде, который производит простые независимые операции загрузки или сохранения данных. Однако, такая методика оказывается опасной по двум причинам. Во-первых, загрузка или сохранение 64-битных значений не всегда является атомарной операцией. Поэтому при присвоении значения полю типа double или long может быть выполнена лишь половина присвоения в результате вытеснения потока. В этом случае следующему потоку придется иметь дело с полем, которое содержит некорректный результат. Более того, на компьютере с несколькими процессорами каждый из потоков работает с отдельным кэшем данных в основной памяти. Ключевое слово synchronized гарантирует, что все локальные кэш-области согласуются с основной памятью, а при использовании несинхронизированных методов такой гарантии дать нельзя. В последнем случае может случиться так, что один поток не заметит изменения общей переменной, произведенного другим потоком.
Для решения подобных проблем предусмотрено ключевое слово volatile. Операции загрузки и сохранения 64-битных значений, объявленных с ключевым словом volatile, будут атомарными. На многопроцессорном компьютере эти операции будут работать корректно даже для данных в кэше процессора.
В некоторых ситуациях можно избежать синхронизации, пользуясь переменными volatile. Однако, помимо дополнительных сложностей известно, что в некоторых реализациях виртуальной машины Java обработка таких переменных выполняется некорректно. Поэтому, чтобы гарантировать надежную и корректную обработку потоков, настоятельно рекомендуется синхронизировать методы, а не полагаться только на использование ключевого слова volatile.
Блокировка объектов
Если поток для работы с каким-то объектом вызывает синхронизированный метод, то этот объект блокируется (locked), т.е. запирается так, как если бы у него была дверь
с замком. Программисты на языке Java часто используют аналогию с туалетом, где в одной кабинке в данный момент может находиться только один человек. Воспользуемся более благозвучной аналогией на основе телефонной будки, которая запирается изнутри на защелку. Когда в потоке вызывается синхронизированный метод, он запирает дверь, т.е. блокирует объект. Когда другой поток пытается вызвать синхронизированный метод для этого же объекта, он не может открыть дверь и прекращает выполнение. После завершения работы первого синхронизированного метода дверь отпирается, т.е. объект разблокируется.
Планировщик потоков периодически активизирует потоки, ожидающие снятия блокировок, используя упомянутые выше обычные правила. Всякий раз, когда поток хочет вызвать синхронизированный метод для данного объекта, он проверяет наличие у него блокировки. Если блокировки нет, то поток становится следующим потоком, который может получить эксклюзивный доступ к объекту.
Однако, другие потоки вполне могут вызывать несинхронизированные методы для этого же объекта. Например, метод size класса Bank несинхронизирован, но его можно вызывать даже для заблокированного объекта.
Если поток прекращает выполнение синхронизированного метода в результате возникновения исключительной ситуации, то блокировка объекта снимается во избежание нежелательной бесконечной блокировки.
Если поток наложил блокировку на объект и вызывает для него другой синхронизированный метод, то последнему методу право доступа предоставляется автоматически. При этом блокировка будет снята с объекта лишь после завершения работы последнего синхронизированного метода.
Каждый объект имеет счетчик блокировок (lock count), в котором указано количество синхронизированных методов, вызванных потоком-владельцем блокировок. При каждом таком вызове счетчик увеличивается на единицу, а при завершении работы синхронизированного метода (нормальным образом или в результате не- перехваченной исключительной ситуации) счетчик уменьшается. Когда значение счетчикастановится равным нулю, потокразблокируетобъект.
Обратите внимание, что могут существовать два различных объекта одного класса, каждый из которых может быть заблокирован своим потоком. Причем оба потока могут запустить один и тот же синхронизированный метод. Учтите, что блокируется объект, а не метод! По аналогии с телефонными будками два человека могут находиться в двух отдельных будках. Каждый из них может запустить один и тот же синхронизированный метод или разные методы.
Конечно, блокировкой объекта в заданный момент времени владеет только один поток. В то же время поток может наложить блокировку на несколько объектов, если для них вызываются синхронизированные методы. (Следует отметить, что здесь аналогия с телефонными будками уже не подходит.)
Методы wait и notify
Итак, внесем улучшения в программу, имитирующую работу банка. Прежде всего, запретим перевод средств со счета с недостаточной суммой. Обратите внимание, что для этого нельзя использовать следующий код.
if (bank.getBalance(from) >= amount)
bank.transfer(from, to, amount);
Дело в том, что текущий поток может быть приостановлен между успешной проверкой и вызовом методатак, как показано ниже.
if>= amount)
// Поток может быть приостановлен в этом месте. to, amount);
К моменту, когда поток вновь получит управление, сумма amount может стать меньше той, что требуется для перевода. Необходимо сделать так, чтобы поток не мог быть прерван между проверкой и переводом средств. Для этого обе операции необходимо поместить в синхронизированный метод:
public synchronized voidfrom, int to, int amount)
{
while< amount)
{
// ожидание
}
// перевод денег
}
Что происходит, когда на счету недостаточно средств? Этот поток переходит в состояние ожидания, пока другой поток не добавит требуемое количество на этот счет. Но методобъявлен как синхронизированный, потому этот поток получил
эксклюзивный доступ к объекту банка и другие потоки не могут переводить денежные средства. Для обработки подобных ситуаций предназначен метод wait класса который позволяет в случае необходимости приостановить выполнение синхронизированного метода.
Когда метод wait вызывается внутри синхронизированного метода, текущий поток блокируется и освобождает объект. Это позволяет другим потокам увеличить баланс банковского счета.
Метод wait генерирует исключительную ситуацию InterruptedException, если поток прерывается в режиме ожидания. В таком случае можно либо установить флажок прерывания, либо передать обработку исключительной ситуации дальше. Действительно, обработку прерывания должен выполнить вызывающий поток, а не объект банка. В данном случае исключительная ситуация передается дальше, т.е. к определению методадобавляется спецификатор throws.
public synchronized voidfrom, int to, int amount)
throws InterruptedException
{
while< amount)
wait();
// перевод денег
Учтите, что метод wait принадлежит классуа не классу Thread. При его
вызове снимается блокировка с объекта банка, и блокируется текущий поток. (Здесь метод wait вызывается со ссылкой this.)
После вызова метода wait поток попадает в очередь (wait list) данного объекта. При этом поток блокируется, и планировщик выполнения потоков игнорирует его до тех пор, пока он не будет удален из списка.
Если поток блокирует несколько объектов, то метод wait освобождает только тот объект, для которого он вызван. Это означает, что заблокированный поток может блокировать другие объекты, которые не будут освобождены до тех пор, пока не будет разблокирован поток. В этом случае может возникнуть весьма опасная ситуация, которую следует избегать.
Для удаления потока из списка ожидания в каком-либо другом потоке нужно вызвать метод notifyAll или notify для того же самого объекта. Метод notifyAll удаляет все потоки из списка ожидания объекта, а метод notify— только указанный поток. После удаления из списка потоки вновь становятся выполняемыми, и планировщик может снова запускать их. Такие потоки снова попытаются получить доступ к нужным объектам. Если объект освобожден, то один из этих выполняемых потоков вновь блокирует объект и продолжает выполнение с того места, в котором был вызван метод wait.
Чтобы лучше понять принцип работы методов wait и notifyAll/noti у, вновь обратимся к аналогии с телефонными будками. Допустим, что поток блокирует объект, а затем не может продолжать выполнение, например, потому что телефон сломан. Бессмысленно переходить в состояние sleep, если поток находится в телефонной будке, так как в этом случае ремонтник не сможет попасть в будку и починить оборудование. Вызывая метод wait, поток разблокирует объект, т.е. выходит из будки и ожидает снаружи. Ремонтник входит в будку, закрывает ее изнутри и выполняет какую-то работу. Когда он выходит из будки, ожидающие потоки ничего не знают о результате ремонта, может быть ремонтник просто собрал монетки, накопившиеся в автомате. Поэтому потоки продолжают находиться в состоянии ожидания. Вызывая метод notifyAll, ремонтник сообщает всем потокам, что состояние объекта изменилось, например, телефон исправен. А метод notify уведомляет об этом только один случайно выбранный поток, оставляя все остальные ожидающие потоки в том же состоянии.
Очень важно, чтобы какой-либо поток периодически вызывал метод notify или notifyAll. Когда поток выполняет метод wait, он уже не имеет возможности разблокировать себя самостоятельно, и его судьба зависит от других потоков. Если ни в одном из них не организована разблокировка, то он никогда не сможет запуститься снова. Такая ситуация называется взаимной блокировкой илитупиком (deadlock). Если все потоки заблокированы, а последний активный поток вызывает wait без разблокировки других потоков, то он также блокируется. Таким образом, в программе не остается никаких потоков, которые могли снять заданные блокировки, и после этого программа попросту зависает. Потоки в состоянии ожидания не могут активизироваться автоматически, даже если с объектом не работает никакой поток. Более подробно тупиковые ситуации обсуждаются далее в этой главе.
Из практических соображений чрезвычайно опасно применять метод notify, так как неизвестно, какой поток будет при этом разблокирован. Поэтому рекомендуется использовать notifyAll,который позволяет разблокировать все потоки.
Когда же следует вызывать метод notifyAll? Основное правило гласит, что запускать этот метод нужно всегда, когда состояние объекта изменяется в пользу ожидающих потоков. Например, при каждом изменении баланса счета все ждущие потоки должны получить возможность вновь проверить состояние счета. В нашем примере вызов notifyAllпроисходит по окончании перевода средств.
public synchronized void transfer(int from, int to, int amount)
throws InterruptedException
{
accounts[from] —= amount;
accounts[to] += amount;
ntransacts++;
notifyAll();
}
Такое уведомление предоставляет ждущим потокам возможность запуститься. Поток, ожидающий увеличения баланса, вновь проверяет состояние счета. Если на счету имеется достаточная для перевода сумма, то поток переводит ее, а если нет, то вновь вызывает метод wait.
Учтите, что метод notifyAllне приводит к немедленной активации ждущего потока. Он просто разблокирует ждущие потоки, чтобы они могли завершить обработку объекта после того, как текущий поток завершил выполнение синхронизированного метода.
Если многопоточная программа зависает, убедитесь в том, что каждому вызову метода wait соответствует вызов метода notifyAll.
Запустите программу с синхронизированной версией метода transfer и убедитесь в отсутствии ошибок. Теперь суммарный баланс банка всегда остается равным 100000. (Для выхода из программы нужно нажать комбинацию клавиш <Ctrl+C>.)
Обратите внимание, что программа, код которой приводится в листинге 1.6, работает несколько медленнее. Такова цена, которую приходится платить за накладные расходы, связанные с использованием механизма синхронизации.
Листинг 1.6.SynchBankTest.java
/**
©version 1.20 1999-04-26
©author Cay Horstmann
*/
/“
Пример программы, в которой удается избежать ошибок при одновременном доступе к данным со стороны нескольких потоков
*/
public class SynchBankTest
{
public static void main(String!] args) {
Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE) ;
int i;
for (i = 0; i < NACCOUNTS; i++) {
TransferThread t = new TransferThread(b, i, INITIAL_BALANCE);
t.setPriority(Thread.NORM_PRIORITY + i %2 ); t.start();
}
}
public static final int NACCOUNTS = 10;
}}public static final int INITIAL_BALANCE = 10000;
}
}
/**
Банк с несколькими счетами
*/ class Bank {
/**
Создание банка
©param n the number of accounts
@param initialBalance the initial balance for each account
*/
public Bank(int n, int initialBalance) {
accounts = new int[n];
inti;
for (i = 0; i < accounts.length; i++) accounts[i] = initialBalance;
ntransacts = 0; }
Перевод денег с одного счета на другой
©param from the account to transfer from
©param to the account to transfer to
@param amount the amount to transfer
*/
public synchronized void transfer(int from, int to, int amount) throws InterruptedException
{
while (accounts[from] < amount) wait () ;
accounts[from] —= amount;
accounts[to] += amount;
ntransacts++;
notifyAll();
if (ntransacts % NTEST == 0) test () ; )
/**
Вывод тестового сообщения для проверки целостности данных о балансе банка
*/
public synchronized void test() {
int sum = 0;
for (int i = 0; i < accounts.length; i++) sum += accounts[i];
System.out.printIn("Transactions:" + ntransacts + " Sum: " + sum);
}
/* *
Возвращает количество счетов в данном банке
©return the number of accounts
*/
return accounts.length;return accounts.length;public int size() {
return accounts.length;
return accounts.length;
}
public static final int NTEST = 10000;
private final int [ ] accounts;
} private long ntransacts = 0;
/**
Поток перевода денег с одного счета на другой
*/
class TransferThread extends Thread
{
/**
Создание потока перевода
©param b the bank between whose account money is transferred
©param from the account to transfer money from
@param max the maximum amount of money in each transfer
*/
public TransferThread(Bank b, int from, int max)
{
bank = b;
fromAccount = from;
maxAmount = max;
}
public void run()
{
try
{
while ( ! interrupted() )
{
int toAccount = (int)(bank.size() * Math.random());
int amount = (int)(maxAmount * Math.random() ) ;
bank.transfer(fromAccount, toAccount, amount); sleep(1) ;
}
}
catch(InterruptedException e) {}
}
private Bank bank;
private int fromAccount;
private int maxAmount;
}
Вот краткое описание принципов работы механизма синхронизации.
Для вызова синхронизированного метода его неявный параметр-объект должен быть незаблокированным. Он блокируется только после вызова этого метода. При возврате метода его неявный параметр-объект разблокируется. Именно поэтому только один поток может одновременно запускать синхронизированные методы для определенного объекта.
Когда поток выполняет метод wait, он снимает блокировку с объекта и регистрируется в очереди объекта.
Для удаления потока из очереди какой-либо другой поток должен вызвать метод notifyAll или notifyдля того же самого объекта.
На первый взгляд эти правила работы планировщика выполнения потоков могут показаться очень сложными, однако, они достаточно просто могут применяться на практике. При этом необходимо лишь придерживаться следующих практических правил.
Если методы двух или более потоков модифицируют объект, то эти методы необходимо объявлять как синхронизированные. Также следует синхронизировать методы, предназначенные только для чтения свойств объектов, которые могут подвергаться изменениям.
Если поток должен подождать определенного изменения состояния объекта, то такое ожидание должно быть организовано внутри, а не снаружи объекта, с помощью метода wait и синхронизированного метода.
Внутри синхронизированного метода не следует производить долгих операций. Большинство таких операций обычно обновляют данные1 и быстро возвращают управление. Если синхронизированный метод не может завершиться мгновенно, следует вызвать метод wait, который снимет блокировку с объекта на период ожидания.
Прикаждомизменениисостоянияобъектаметоддолженвызыватьметод
notifyAll. Таким образом ждущим потокам предоставляется возможность продолжить свое выполнение.
Помните, что методы wait и not i fyA11/notifупринадлежат классу Object, а не классу Thread. Убедитесь, что каждому вызову метода wait соответствует вызовметодаnot i fу Al 1 для того же самого объекта.
Синхронизированные блоки
Иногда бывает удобно заблокировать объект с предоставлением исключительного доступа лишь для нескольких операций без необходимости создания нового синхронизированного метода. Для чего можно использовать синхронизированный блок (synchronized block). Такой блок содержит последовательность операторов, заключенных в фигурные скобки { . . . } с префиксом synchronized (obj), где obj — блокируемый объект. Вот как выглядит типичный пример использования синхронизированного блока.
public void run () {
synchroni zed (bank) // Блокировка объекта bank {
if (bank.getBalance(from) >= amount) bank.transfer(from, to, amount);
}
}
В этом фрагменте код в синхронизированном блоке завершится до того, как любой другой поток сможет вызвать синхронизированный метод для объекта bank.
Прикладные программисты предпочитают не использовать такую конструкцию. Они считают, что лучше вернуться на один шаг назад, продумать весь механизм на более высоком уровне, создать класс с его описанием и воспользоваться синхронизированными методами этого класса. Системные программисты, для которых наличие до-
полнительных классов связано с дополнительными накладными расходами ресурсов, наоборот предпочитают использовать синхронизированные блоки.
Синхронизированные статические методы
Одноэлементный класс (singleton) представляет собой класс с одним экземпляром. Такие классы применяются для работы с объектами, уникальными в глобальном контексте, например, буфером принтера, диспетчером соединения с базой данных и т.д.
Вот как выглядит типичная реализация одноэлементного класса.
public class Singleton
{
public static Singleton getlnstance()
{
if (instance == null)
instance = new Singleton(...);
return instance;
}
private Singleton(...) { ... }
private static Singleton instance;
}
Однако, метод getlnstance нельзя назвать надежным при обработке нескольких потоков. Предположим, что один поток вызывает метод getlnstance, а затем теряет управление до присвоения значения для объекта instance. Допустим, что в этот момент активизируется другой поток и также вызывает метод getlnstance. Поскольку значение объекта instance все еще равно null, то второй поток создает второй экземпляр этого объекта. А ведь одноэлементный класс предназначен как раз для того, чтобы предотвращать такие ситуации!
Решение выглядит очень просто: достаточно синхронизировать метод getlnstance.
public static synchronyzed Singleton getlnstance() {
if (instance == null)
instance = new Singleton(...);
return instance;
}
Теперь метод завершается до того, как его может вызвать другой поток.
После внимательного изучения предыдущих разделов может возникнуть справедливый вопрос: а как же все это работает? Когда поток вызывает синхронизированный метод, он блокирует объект. Но данный метод является статическим, поэтому совершенно непонятно, какой объект блокируется потоком при вызове методаSingleton.getlnstance () ?
Запомните, что вызов статического метода блокирует объект класса (class object) Singleton.class . (Как упоминалось в главе 5 тома 1, в Java предусмотрен уникальный объект типа Class, который описывает каждый класс, загруженный виртуальной машиной.) Поэтому, когда один поток вызывает синхронизированный статический метод класса, все синхронизированные статические методы блокируются до того, как первый вызов возвратит управление.
java.lang.Objact
Метод void notifyAll() разблокирует потоки, вызвавшие метод wait для данного объекта. Этот метод можно вызывать только из синхронизированного метода или блока. Метод генерирует исключительную ситуацию IllegalMonitorStateException, если блокировка объекта не принадлежит текущему потоку.
Метод void notify() разблокирует произвольно выбранный поток среди тех, которые вызвали метод wait для данного объекта. Его можно вызывать только из синхронизированного метода или блока. Метод генерирует исключительную ситуацию IllegalMcnitorStateException, если блокировка объекта не принадлежит текущему потоку.
Метод void wai t () переводит поток в состояние ожидания, пока ему не будет прислано уведомление. Этот метод можно вызывать только из синхронизированного метода или блока. Он генерирует исключительную ситуацию IllegalMonitorStateException, если блокировка объекта не принадлежит текущему потоку.
Взаимные блокировки
Хотя предлагаемый в Java механизм синхронизации является удобным и мощным, он, однако, не решает всех проблем, возникающих в многопоточной среде. Рассмотрим следующую ситуацию.
Счет 1: S2000
Счет 2: S3000
Поток 1: перевод $3000 со счета 1 на счет 2
Поток 2: перевод $4000 со счета 2 на счет 1
Как показано на рис. 1.9, в этом примере потоки 1 и 2 блокируют друг друга. Ни один из них не может продолжить свою работу, так как баланс обоих счетов оказывается недостаточным.
Возможно ли возникновение такой взаимной блокировки или тупиковой ситуации (deadlock), когда все 10 потоков в примере из предыдущего раздела будут заблокированы подобным образом.
В данном примере это невозможно по следующей простой причине. Сумма всех денежных средств на десяти счетах равна $100000, поэтому, по крайней мере, один счет имеет более $10000. При каждом переводе пересылается не более $10000, поэтому поток снятия средств с такого счета всегда сможет продолжить работу.
Однако, тупиковая ситуация возникнет достаточно быстро, если изменить метод run так, чтобы транзакции могли переводить суммы больше $10000. Создайте метод TransferThread со значением maxAmount, равным 14000, и запустите программу. Она будет выполняться некоторое время, а затем зависнет.
Другой способ создания тупиковой ситуации основан па том, чтобы заставить г-тый поток переводить деньги на г-тый счет, а не снимать с него. В этом случае есть вероятность, что все потоки попытаются сиять средства с одного счета, причем больше, чем ом содержит. В программе SynchBankTest измените метод run класса TransferThread, т.е. в вызове метода transf er поменяйте местами
fromAccount и toAccount. Запустите программу и убедитесь, что тупиковая ситуация наступает практически мгновенно.
Поток1Поток1bank.transfer(1,2, 3000)bank.transfer(1,2, 3000)bank.accountsbank.accounts
Поток1
Поток1
bank.transfer(1,2, 3000)
bank.transfer(1,2, 3000)
bank.accounts
bank.accounts
bank.wait ()
bank, transfer (2, 1,4000) bank.wait ()
Рас. 1.9. Тупиковая ситуация из-за взаимной блокировки
Вот еще один способ создания тупиковой ситуации. В программе SynchBankTest замените метод notifyAll на метод notify. После запуска программа достаточно быстро зависнет. В отличие от метода notifyAll, который уведомляет все потоки, ждущие увеличения баланса, метод notify разблокирует только один поток. Если этот поток не сможет продолжить выполнение, то все потоки заблокируются. Рассмотрим простой сценарий возникновения тупиковой ситуации.
Счет 1:$19000
Все остальные счета: $9000
Поток 1: перевод $9500 со счета 1 на счет 2
Все остальные потоки: перевод $9500 на другой счет
Очевидно, что все потоки, кроме первого, будут блокированы, так как на их счетах недостаточно средств.
Поток 1 продолжает выполнение, и возникает следующая ситуация.
Счет!: $9500
Счет 2: $18500
Все остальные счета: $9000
Теперь поток 1 вызывает метод notify. Последний выбирает следующий поток случайным образом. Предположим, что он разблокирует поток 3. Этот поток начина-
ет выполняться и определяет, что на счет}' 3 недостаточно средств, а потому вновь вызывает метод wait. Однако, поток I все еще продолжает работу. Генерируется новая транзакция, например:
Поток 1: перевести $9600 со счета 1 на счет 2
Теперь поток 1 также вызывает метод wait, после чего все потоки блокируются. Система зашла в тупик.
Причина такой взаимной блокировки заключается в неправильном вызове метода notify. Он способен разблокировать один поток, но это может оказаться совсем не тот поток, который необходим для дальнейшего выполнения. (В нашем сценарии поток 2 должен перевести средства со счета 2.) Метод же noti fуА1 1 разблокирует все потоки.
К сожалению, язык Java не обладает средствами для автоматического устранения тупиковых ситуаций. Поэтому обработку потоков нужно организовать так, чтобы исключить возможность возникновения таких ситуаций. Для этого следует проанализировать логику работы программы и убедиться в том, что каждый блокированный поток будет вовремя уведомлен и, по крайней мере, один поток сможет продолжать выполнение.
Следует избегать блокирующих вызовов, например, операций ввода/вывода, внутри синхронизированных методов. Если поток блокируется тогда, когда он сам блокирует объект, тс в таком случае блокируется каждый поток, вызывающий синхронизированный метод для зтого объекта. Если все потоки вызовут синхронизированный метод для этого объекта, возникнет тупиковая ситуация. Она называется "черной дырой". (Представьте себе телефонную будку, в которой кто-то закрылся на долгое время., а остальные ждут снаружи, вместо того чтобы заниматься полезным делом.)
Некоторые программисты ошибочно считают синхронизацию потоков в Java источником тупиковых ситуаций просто потому, что они привыкли к другим механизмам, которые плохо транслируются в код на языке Java. Попытка превратить семафоры в набор вложенных синхронизированных блоков, скорее всего, приведет к плохому результату, Если вам пришлось столкнуться с тупиковой ситуацией, вернитесь на шаг назад, уточните нужный способ взаимодействия между потоками, а затем создайте для этой цели отдельный класс. Именно такой подход применяется в объекта o-opnei пировав ном программировании для упрощения запутанной логики многопоточных программ.
Почему методы stop и suspend устарели
В платформе Java 1.0 для остановки потока предусмотрен метод stop, а для блокировки (пока в другом потоке не будет вызван resume) — метод suspend. Оба метода не рекомендуется применять при использовании платформы Java 2. Метод stop является опасным по определению, а применение метода suspend, как показывает опыт, часто приводит к тупиковым ситуациям. В этом разделе описываются некоторые проблемы использования данных методов и способы их устранения.
Вначале рассмотрим метод scop. Когда работа потока останавливается, он немедленно освобождает все заблокированные объекты. Это значит, что некоторые из них могут находиться в противоречивом СОСТОЯНИИ, Предположим, например, что поток TransferThread остановлен в процессе перевода средств с одного счета на другой, после снятия с одного счета и до внесения на другой. В этом случае объект bank поврежден, и повреждение может повлиять на работу всех других активных потоков, которые не были остановлены.
Точнее говоря, метод stop останавливает поток и генерирует исключительную ситуацию ThreadDeath, которая прекращает работу всех ждущих методов, включая метод run.
По этой же причине любая неперехваченная исключительная ситуация в синхронизированном методе может привести к преждевременной остановке метода, а значит, и к повреждению объекта.
Если один поток хочет приостановить работу другого потока, он не сможет определить, приведет ли к повреждению объекта применение метода stop или нет. Поэтому данный метод не рекомендуется использовать.
Некоторые авторы считают, что метод stop не рекомендуется использовать, потому что он может привести к перманентной блокировке объектов остановленным потоком. Однако, это неверно. Остановленный поток завершает выполнение всех синхронизированных методов вследствие обработки исключительной ситуации ThreadDeath. В результате поток освобождает все блокированные объекты.
Если необходимо безопасно остановить поток, нужно организовать периодическую проверку переменной (например,в показанном ниже приме
ре), которая означает наличие запроса на остановку.
public classextends Thread
{
public void run()
{
еще есть какие-то действия) выполнение каких-то действий
}
public void
{ stopRequested = true;
}
private boolean stopRequested; }
Таким образом, метод run сам управляет своим завершением и проверяет, чтобы все объекты остались в неповрежденном состоянии.
Проверка переменной stopRequested в главном цикле потока будет выполняться всегда, если поток не заблокирован. В этом случае поток может завершиться лишь после снятия блокировки. Вывести поток из заблокированного состояния можно, прерывая его выполнение. Поэтому в методе requestStop следует предусмотреть вызов метода interrupt.
public void {
stopRequested = interrupt();
}
Проверку переменной stopRequested можно поместить в блоке catch исключи- ситуации InterruptedException. Например так, как показано ниже.
wait () ;
}
catche)
{
if
return;завершить выполнение метода run
}
В действительности многие программисты считают, что единственным предназначением прерывания потока является остановка его работы. В этом случае переменная stopRequested не требуется вовсе и достаточно будет только организовать выход из метода при поступлении прерывания.
Кстати, если прерывается работающий поток, то метод interrupt не генерирует исключительную ситуацию InterruptedException, а просто устанавливает флажок прерывания. Такое прерывание потока не может привести к повреждению данных объекта, потому что поток сам позаботится об установке флажка прерывания только после того, как будут завершены все важные вычисления.
Рассмотрим теперь потенциальные проблемы при использовании метода suspend. В отличие от метода stop метод suspend не повреждает объекты. Но, если с его помощью приостановить поток, блокирующий объект, то этот объект будет недоступен до тех пор, пока работа потока не будет возобновлена с помощью метода resume. Если поток, вызвавший метод suspend, пытается блокировать тот же самый объект до вызова метода resume, программа заходит в тупик: приостановленный поток ожидает вызова метода resume для продолжения работы, а остановивший его поток ожидает освобождения объекта.
Такая ситуация часто возникает в графических пользовательских интерфейсах. Предположим, что программа на основе графического пользовательского интерфейса имитирует работу банка. Допустим, что кнопка Pause (Приостановить) приостанавливает потоки перевода средств, а кнопка Resume (Продолжить) продолжает их выполнение.
pauseButton.addActionListener(new
ActionListener()
{
public voidevent)
{
i = 0; i <i++)
//делайте так!
}
});
resumeButton,addActionListener(new
ActionListener()
{
public voidevent)
{
for(int i = 0; i < threads.length; i++) //делайте так!
}) ;
Предположим, что метод paintComponent отображает диаграмму для каждого счета, вызывая синхронизированный метод bank. getBalance.
Как показано в следующем разделе, действия кнопок и перерисовка диаграмм происходят в одном и том же потоке, который называется потокомсобытиями
(event dispatch thread).
Рассмотрим следующий сценарий.
Одни из потоков перевода средств блокирует объектbank.
Пользователь щелкает на кнопке Pause.
Выполнение всех потоков перевода средств приостанавливается, причем, один из них все еще блокирует объект bank.
В определенный момент требуется перерисовать диаграмму состояния счетов.
Метод paintcomponent вызывает синхронизированный метод bank. get- Balance.
После этого программа зависает.
Поток управления событиями не может продолжать свою работу, поскольку объект bank: блокирован. Поэтому пользователь не может щелкнуть на кнопке Resume, и остальные потоки уже не могут быть разблокированы.
Для безопасной приостановки работы потока следует воспользоваться переменной suspendRequest и проверять ее значение в безопасном месте метода run, т.е. там, где поток не блокирует объекты, которые требуются другим потокам. Если поток определяет, что переменная suspendReques tedимеет значение true, он переходит в состояние ожидания до тех пор, пока ее значение не станет равным false.
Для большей ясности можно поместить переменную в класс SuspendRequestor, например так, как показано ниже.
classSuspendRequestor
{
public synchronized void set(boolean Ы
{
suspendRequested = b; notifyAll।);
}
public synchronized void waitForResume()
throws InterruptedException
(
while(suspendRequestedj
wait();
private boolean suspendRequested;
}■
class MyThread extends Thread
{
public void requestSuspend()
suspender.set(true) ;
}
public void requestResume()
suspender.set(false) ;
}
public void run ( )
{
try {
while(есть какие-то действия)
suspender. waitForResume (); какие-то действия
}
exception) {
}
}}private SuspendRequestor suspender = new
}
}
Некоторые программисты предпочитают не создавать новый класс для такого простого механизма. Но, все же, им требуется создать некий фиктивный объект для синхронизации, поскольку ждущий поток должен быть добавлен в очередь какого-либо объекта. Для этого можно создать переменную-заглушку так, как показано ниже.
classextends Thread
{
public void
{
= true;
}
public void
{
suspendRequested = false;
synchronized(dummy)
{
dummy.notlfyAll( ) ;
// Разблокировать поток,
// ждущий снятия блокировки с dummy }
public void
throws InterruptedException
{
synchronized(dummy)
// Синхронизированный блок
// для вызова метода wait
{
while(suspendRequested)
// Блокировка этого потока }
private boolean suspendRequested;
private Integer dummy = new
// Подойдет любой ненулевой объект }
Однако, такой стиль программирования не рекомендуется применять, потому что создание класса организуется также просто, и в таком случае блокировка естественным образом связывается с классом, что не приводит к путанице, когда объект используется только ради блокировки и включения в очередь.
Теперь вызов метода suspender.waitForResume () блокирует работу потока при поступлении запроса о его приостановке. Для разблокировки потока какой-то другой поток должен подать запрос о продолжении работы.
Конечно же, отказ от использования метода Thread. suspend не приводит к автоматическому исключению тупиковых ситуаций. Если поток вызывает метод wait, то он также может попасть в тупиковую ситуацию. Однако, между этими двумя случаями есть существенная разница. Поток может сам управлять вызовом метода wait, а метод Thread. suspend запускается для потока только извне и без его согласия. То же самое справедливо и для метода stop. Именно поэтому эти два метода и не рекомендуется использовать.
■ В настоящем разделе определены методы requeststop, requestSuspend и requestResume. Они выполняют те же функции, что и методы stop, suspend и resume, но делают это гораздо безопаснее. Многие программисты используют старые методы, просто переопределяя их. Если в программе встречаются вызовы методов stop, suspend или resume, то это не всегда означает, что программа может работать некорректно. Проверьте, возможно, эти методы переопределены и представляют собой более безопасные версии своих аналогов.
Паузы
При возникновении блокирующего вызова, например, при вызове метода wait или выполнении операции ввода/вывода, поток теряет управление, и его судьба находится в руках других методов или, в случае ввода/вывода, он зависит от каких-то внешних обстоятельств. Для ограничения связанного с этим риска недопустимо длительного простоя используются паузы (timemits).
Существует два варианта метода wait с использованием разных параметров для определения таких пауз.
void wait(long millis)
void wait (long millis, int. nanos)
Данные методы переводят поток в состояние ожидания до тех пор, пока поток не будет разблокирован методом notifyAll/ncti у, либо не пройдет указанное число миллисекунд (параметр millis) и наносекунд (параметр nanos). Учтите, что 1 мс равна 1000000 HC.
Однако, часто необходимо знать причину возврата метода wait: истечение паузы или разблокировка. Это нужно, например, для того чтобы избежать дополнительной проверки достижения нужного условия в тех случаях, если не поступило уведомления о каких-либо изменениях состояния объекта.
Для этого можно вычислить промежуток времени до и после вызова.
long before = System.currentTimeMillis();
wait(delay);
long after = System.currentTimeMillis();
if(after — before > delay);
... // пауза
Альтернативный способ основан на установке соответствующего значения для флажка ожидания разблокируемого потока.
Поток может быть блокирован на неограниченное время, если для операции вво- да/вывода не задана пауза. Например, в примере программы в главе 3 для открытия
сетевого сокета необходимо вызвать его конструктор, в котором не предусмотрено указание паузы. Для создания паузы в потоке можно разместить блокирующую операцию во втором потоке, а затем воспользоваться методом j oin.
join (inillis) ;
Вызов этого метода блокирует текущий поток до тех пор, пока не завершится поток t, или же не пройдет заданное число миллисекунд millis. Такой вызов может применяться в показанном ниже контексте.
Thread t = new
Thread()
{
public void run()
{
блокирующая операция
}
};
t.start();
t.join(millis) ;
В данном случае либо блокирующая операция успешно завершится в течение заданного интервала времени, либо метод join возвратит управление текущему потоку. Все это время поток t остается активным, а выполняемые с ним действия зависят от природы блокирующей операции. Если известно, что операцию можно прервать, то для ее приостановки достаточно вызвать метод t.interrupt ().
При выполнении операций ввода/вывода может использоваться пауза, величина которой зависит от операционной системы. В этом случае поток t нужно оставить активным до тех пор, пока не закончится пауза, или блокирующая операция не возвратит управление. Типичный пример такой ситуации приведен в программе SocketOpener в главе 3, "Работа с сетями".
java.lang.Thread
Методvoid wait (long millis) переводит поток в состояние ожидания до тех пор, пока поток не будет разблокирован методом notifyAll/notify, либо не пройдет указанная пауза. Этот метод можно вызывать только из синхронизированного метода или блока. Он генерирует исключительную ситуацию IllegalMonitorStateException, если блокировка объекта не принадлежит текущему потоку.
Параметры:
mi Hisколичество миллисекунд
Метод void wait (long millis, int nanos) также переводит поток в состояние ожидания до тех пор, пока поток не будет разблокирован методом notifyAll/notify, либо не пройдет указанная пауза. Данный метод можно вызывать только из синхронизированного метода или блока. Он генерирует исключительную ситуацию IllegalMonitorStateException, если блокировка объекта не принадлежит текущему потоку.
Параметры:
milli sколичество миллисекунд
nanosколичество наносекунд, меньше 1000000
Методjoin() блокирует текущий поток, пока указанный поток не
завершит свою работу.
Метод void join (longблокирует текущий поток, пока ука
занный поток не завершит свою работ)', или не пройдет заданная пауза.
Параметры:
mii sколичество миллисекунд
Потоки для пользовательского интерфейса
В следующих разделах обсуждаются вопросы программирования пользовательского интерфейса с помощью потоков.
Потоки и пакет Swing
Как отмечалось во введении, одна из причин использования потоков заключается в том, чтобы сделать программы более управляемыми со стороны пользователей. Если программе требуется выполнить длительную операцию, то в таком случае следует запустить еще один рабочий поток, а не блокировать пользовательский
Однако, при обработке рабочего потока необходимо проявлять осторожность, так как пакет Swing, как это ни удивительно, не является безопасным для потоков. Большинство методов классов Swing не синхронизированы. Поэтому в результате неосторожного управления пользовательским интерфейсом его можно повредить при работе с несколькими потоками.
Например, в программе, код которой приведен в конце этого раздела, для запуска нового потока применяется кнопка Bad (Плохо организованный поток). Этот поток изменяет содержимое поля со списком, произвольным образом добавляя или удаляя значения в нем.
classextends Thread
{
public
{
aCombo; generator = new }
public void
{
try { while {
int i =
if (i % 2 == 0)
Integer(i), else if (combo> 0)
%
sleep(1);
}
catchexception) {}
private JComboBox combo;
private Random generator;
Запустите эту программу и щелкните на кнопке Bad. Если программа запущена в режиме командной строки, то на экране можно увидеть сообщения о возникновении исключительных ситуаций, как показано на рис. 1.10.
Что же в этом случае происходит? Когда в поле со списком вставляется новый элемент, генерируется событие обновления содержимого списка. После этого запускается код отображения списка, который считывает текущий размер поля со списком и подготавливает отображаемые значения списка. По рабочий поток продолжает работу, что, например, может привести к уменьшению количества значений в списке. В таком случае код отображения списка считает, что модель данных содержит больше значений, чем имеется в списке, и запрашивает несуществующие элементы, что приводит к генерации исключительной ситуации ArrayIndexOutO Bound.
213240666 v
Jff l
at javax swing Defaultiist •-•• .crjonM .•" I •"ireVa! ueChangecK Defaul tlistSel HI ecticnModeT.java 170)
at jswing.DefaultL1stSelectionModel.f1 reVal ueChangedC Defaul tLi stSel
ectionMoJava: 150)- —- Л i41
at jSJfcUQl3.Defaul tListSel
ectionModel Java: 197)
at javax.swing,Defaul tlistSelectionModel .changeSelectionfDefaultlistSeleHI ctionModel sjava:359)г - n +1 iu 4J 1i.1+1 i - I H
ctionModaetlJj ava:368) ig. DefaultL iectitLi e
at j avax.swing.DefaultLi stSel ect ionModei.setselectionintervalС Defaul tlis! || tSelectionModel Java: 392)
at javax.swingJ L1st. setSelectedlndexCJList Java: 1 319)
at j avax.swing.plaf.basic.BasicComboPopup.synctistselect 1 onWlThComboBoxS || el ectionCBasi cComboPopupJava: 81 6)
at j avax.swing.plaf.basic.BasicComboPopupSListDataHandl er. intervalAddedC Basi cComboPopupJava: 566)
at javax.swina.AbstractListModel .firelntervalAddedCAbstractListModel . javMI a: 108)
at javax. swing. Defaul tComboBoxModel . insertElementAtCDefaul tComboBoxModel ГЧ1 .java:131)
at javax.swing.JComboBox.InsertItemAtCDComboBox.java:526)Я
at BadWorkerThread.runCSwingihreadiest.java:95)Ц]
Рис. 1.10.Сообщения о возникновении исключительных ситуаций
Такую ситуацию можно исключить, блокируя поля со списком на время его отображения. Однако, разработчики Swing решили не расходовать усилия на обеспечение безопасности при работе с потоками в Swing по следующим двум причинам. Во-первых, для синхронизации потребуется дополни гельное время и вряд ли пользователям понравится, если пользовательский интерфейс на основе Swing станет работать еще медленнее. Гораздо
важнее то, что команда Swing учла печальный опыт разработчиков компонентов пользовательского интерфейса при обеспечении безопасности потоков. При создании библиотеки пользовательского интерфейса, как правило, учитывается возможность ее расширения со стороны других программистов. Однако, при расширении пользовательского интерфейса, в котором обеспечивается безопасность потоков, можно было легко запутаться в правилах синхронизации, что зачастую приводило к тупиковым ситуациям.
Поэтому при использовании потоков вместе с пакетом Swing необходимо учитывать несколько простых правил. Но перед этим познакомимся со способами использования потоков в пакете Swing.
Каждое Java-приложение запускает метод main, который выполняется в главном потоке (main. thread). В программе Swing метод main обычно выполняет следующие действия.
Вызывает конструктор, который размещает компоненты в окне фрейма.
Затем для отображения окна фрейма вызывает метод show или showVisible.
После отображения окна фрейма создается поток управления событиями (event dispatch thread). Все уведомления о событиях, например, вызовы метода actionPerformed или paintComponent, запускаются в этом потоке. Главный поток продолжает выполняться, пока не завершится метод main. Обычно, метод main завершает свою работу после отображения окна, как показано на рис. 1.11.Другие потоки выполняются незаметно для разработчика, например, поток, который размещает события в очереди.
Главный потокГлавный потокПоток управления событиями
Главный поток
Главный поток
Создание фрейма
Отображение фрейма
Завершение метода main
Рис. 1.11.Схема работы потоков в программе с использованием пакета Swing
В приложении с использованием пакета Swing все операции, отвечающие на запросы пользовательского интерфейса и перерисовки окна, находятся в обработчиках событий. Поэтому все они запускаются в потоке управления событиями. Вот несколько правил работы с потоками, которым необходимо следовать.
Если для выполнения какого-либо действия требуется длительный интервал времени, запустите для него новый поток. В противном случае, если длительное действие будет выполняться в потоке управления событиями, приложение "замрет" и не будет отвечать ни на какие события.
Если какое-то действие блокируется операциями ввода/вывода, запустите для него новый поток, иначе пользовательский интерфейс может оказаться заблокированным на неограниченный интервал времени (например, при не реагирующем на запросы сетевом соединении).
Если требуется подождать в течение определенного промежутка времени, не вызывайте метод sleep в потоке управления событиями. Вместо этого воспользуйтесь паузой.
Действия, выполняемые потоками, не должны взаимодействовать с пользовательским интерфейсом. Считывайте информацию до запуска потоков, затем запускайте потоки и обновляйте интерфейс из потока управления событиями лишь после завершения работы потоков.
Последнее правило при программировании с использованием пакета Swing часто называют правилом единственного потока (single thread rule). Ниже перечислены некоторые исключения из этого правила.
Несколько методов пакета Swing являются безопасными при использовании потоков, и в документации для API-интерфейса об этом сказано: "Данный метод является безопасным при использовании потоков в отличие от большинства методов пакета Swing" ("This method is thread safe, although most of Swing methods are not"). Наиболее полезными среди таких методов являются следующие.
JTextComponent.setText
JTextArea.insert
JTextArea.append
JTextArea.replaceRange
Следующие методы класса JComponent можно вызывать из любого потока.
repaint
revalidate
Метод repaint включает событие перерисовки окна в расписание событий. А метод revalidate используется для обновления размера и расположения компонент в случае изменения содержимого компонента. Этот метод отмечает макет компонента как недействительный и включает событие обновления макета в расписание событий. (Как и события перерисовки, события обновления макета объединяются (coalesced), т.е. при наличии в очереди нескольких событий обновления макета этот макет пересчитывается только один раз.)
Методrepaint неоднократно использовался в томе 1 этой книги, а методrevalidate менеераспространен. Он предназначен для перерисовки макета компонента после изменения его содержимого. В традиционном пакете AWT для указания непригодности макета и подтверждения его пригодности предусмотрены методы invalidate и validate. А в пакете Swing вместо этого достаточно вызвать один метод revalidate. (Однако, для подтверждения пригодности компонента JFrame необходимо вызвать validate, так какJFrame принадлежит классу Component, а не классуJComponent.)
В любом потоке разрешается добавлять и удалять слушатели событий. При этом методы-слушатели будут выполняться в потоке управления событиями.
Компоненты можно создавать, устанавливать для них приоритеты и включать в контейнеры до реализации компонентов. Компонент считается реализованным, если может получить события перерисовки или подтверждения действительности. Так происходит либо при вызове для компонента методов show, setVisible(true) или pack, либо при включении компонента в реализованный контейнер. После- реализации компонента им уже нельзя управлять из другого потока.
В частности, допускается создание интерфейса приложения в методе main до вызова метода show или создание аплета с графическим интерфейсом в конструкторе аплета или методе init.
Эти правила достаточно сложно выглядят, но они довольно просто выполняются. Создадим новый поток с длительным процессом, а затем по запросу пользователя организуем сбор всей необходимой информации из графического пользовательского интерфейса, передачу ее данному потоку и запуск потока.
public void actionPerformed(ActionEvent e) {
// Сбор данных, необходимый потоку
MyThread t = new MyThread(data);
t.start();
}
Сложности возникают при обновлении графического интерфейса на основании текущего состояния потока, а также на основании всей проделанной потоком работы после завершения потока. Учтите, что нельзя вызывать компоненты Swing из потока. Например, если нужно обновить строку состояния или надпись, то ее новое значение нельзя установить непосредственно из потока.
Для решения этой проблемы воспользуйтесь следующими двумя удобными методами, которые можно применять в любом потоке для включения определенных действий в очередь событий. Предположим, например, что в потоке нужно постоянно обновлять надпись х% complete (Выполнено х% работы) для обозначения текущего прогресса выполнения какого-то процесса. Для этого нельзя вызывать из потока метод label. setText, но можно воспользоваться методами invokeLater и invokeAndWait класса EventQueue, чтобы вызов произошел в потоке управления событиями.
Эти методы также находятся в классе javax. swing. SwingUtilities. При использовании пакета Swing вместе с пакетом JDK версии 1.1 необходимо использовать именно этот класс, так как в класс EventQueue эти методы были добавлены, начиная с пакета JDK версии 1.2.
Вот что для этого нужно сделать. Swing-код размещается в методе run класса, реализующего интерфейс Runnable. Затем создается объект этого класса и передается статическому методу invokeLater или invokeAndWait. Ниже приводится соответствующий пример обновления надписи.
Сначала создается класс с методом run.
public LabelUpdater(Jlabel aLabel, int aPercentage)public LabelUpdater(Jlabel aLabel, int aPercentage)public class LabelUpdater implements Runnable (
public LabelUpdater(Jlabel aLabel, int aPercentage)
public LabelUpdater(Jlabel aLabel, int aPercentage)
label = aLabel;
percentage = aPercentage;
}
public void run()
{
label.setText(percentage+ "% complete");
}
}
Затем создается объект, который передается методу invokeLater.
Runnable updater = new LabelUpdater(label, percentage); EventQueue.invokeLater(updater);
Метод invokeLater возвращает управление сразу же после размещения события в очереди, а метод run выполняется асинхронно. Метод invokeAndWait дожидается, пока не будет завершено выполнение метода run. Все процедуры синхронизации при этом выполняются классом EventQueue. Для обновления надписи в данном примере лучше всего использовать метод invokeLater. Чаще всего пользователи предпочитают, чтобы рабочий поток быстрее выполнял заданные действия, а не точнее отображал на экране текущую информацию о прогрессе выполняемых действий.
Для запуска кода в потоке управления событиями можно для краткости кода использовать безымянные внутренние классы. Например, приведенный ранее фрагмент кода можно сократить до более сложной, но короткой команды:
EventQueue.invokeLater(new
Runnable()
{
public void run()
{
label.setText(percentage + "% complete");
}
});
Методы invokeLater и invokeAndWait используют объекты, реализующие интерфейс Runnable. Выше уже были показаны способы создания новых потоков в объектах, реализующих интерфейсRunnable. Однако учтите, что в этом случае код методаrunзапускаетсявпотокеуправления событиями, аневновом потоке.
В листинге 1.7 показано, как с помощью метода invokeLater можно безопасно изменить содержимое поля со списком. После щелчка на кнопке Good отдельный поток добавляет и удаляет числа. Однако, реальное изменение содержимого списка происходит в потоке управления событиями.
Листинг 1.7.SwingThreadTest.java
/**
* @version 1.20 1999-04-26
* ©author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
/**
Пример потока, выполняющегося параллельно с потоком управления событиями, который может вызвать появление ошибки в компонентах Swing
*/
public class SwingThreadTest {
public static void main(String[] args) {
SwingThreadFrame frame = new SwingThreadFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show() ;
}
}
/**
Фрейм с двумя кнопками для пополнения списка значениями из отдельного потока. Кнопка Good использует для этого очередь событий, а кнопка Bad непосредственно изменяет список
*/
class SwingThreadFrame extends JFrame {
public SwingThreadFrame() {
setTitle("SwingThread");
setsize(WIDTH, HEIGHT);
final JComboBox combo = new JComboBox();
JPanel p = new JPanel();
p.add(combo);
getContentPane(),add(p, BorderLayout.CENTER);
JButton b = new JButton("Good") ;
b.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
combo.showPopup();
new GoodWorkerThread(combo).start();
}
});
p= newJPanel(); '
p.add(b);
b= newJButton("Bad") ;
b. addActionListener (new ActionListener ()
p{ublic void actionPerformed(ActionEvent event) combo.showPopup();
new BadWorkerThread(combo).start();
}
p.add(b);
getContentPane().add(p, BorderLayout.NORTH) ; }
public static final int WIDTH = 450;
public static final int HEIGHT = 300;
/**
Этот поток изменяет содержимое списка, случайным образом добавляя и удаляя числа. Это может привести к ошибкам, так как список неа перерисовка
списка происходит в потоке управления событиями
*/
class BadWorkerThread extends Thread {
public
{ combo = generator = new
public void run() {
try
{ while { int i = if(i%2==0)
0);
else if> 0)
%
sleep(l);
}
}
catch (InterruptedException exception) {)
private JComboBox combo;
private Random generator;
/**
Этот поток изменяет содержимое списка, случайным образом добавляя и удаляяЧтобы не повредить содержимое
списка, операции редактирования списка синхронизированы с потоком управления событиями
*/
class GoodWorkerThread extends Thread {
publicaCombo)
{ combo = generator = new
public void run() {
try { while {
EventQueue.invokeLater(new
Runnable()
p{ublic void run ( )
int i=Math.abs(generator.nextInt() ) ;
if(i%2==0)
combo. insertltemAt (new Integer (i) , 0) ;
else if (combo.getltemCount() >0) combo.removeltemAt(i %
}
});
Thread.sleep(1) ;
}
}
catch (InterruptedException exception) {}
}
private privateprivate privateJComboBox combo;
private private
private private
}}Random generator;
}
}
.EventQueue
Метод static void invokeLater(Runnable runnable) принудительно запускает метод run объекта runnable в потоке управления событиями после обработки всех событий в очереди.
Метод static void invokeAndWait (Runnable runnable) принудительно запускает метод run объекта runnable в потоке управления событиями после обработки всех событий в очереди. Этот вызов блокируется до тех пор, пока не завершится выполнение метода run.
Анимация
В этом разделе рассматривается один из самых распространенных вариантов применения потоков в аплетах, а именно, для анимации. Анимацией (animation) называется последовательное отображение набора рисунков, которые создают иллюзию движения. Каждый такой рисунок последовательности называется кадром (frame). При использовании сложных кадров иногда необходимо выполнить предварительную подготовку кадров, так как компьютер, выполняющий аплет, не всегда имеет достаточные вычислительные ресурсы для перерисовки изображения в режиме реального времени.
Каждый кадр можно разместить в отдельном файле или же поместить все кадры в одном файле (как и сделано в данном случае), что упрощает процесс их загрузки. В нашем примере один файл содержит все 36 кадров с изображениями Земного шара. (Авторы любезно благодарят Сильвиу Маржеску (Silviu Marghescu) из Университета штата Мэриленд (University of Maryland) за предоставленные для этого примера изображения.) Несколько первых кадров приведены на рис. 1.12.
Сначала аплет извлекает все кадры, а затем отображает каждый из них в течение определенного интервала времени. Для вывода z-ro кадра вызывается метод drawlmage.
g.drawlmage(image, 0, — (imageHeight / imageCount) * i, null);
На рис. 1.13 приведен файл с 36-ю кадрами, предназначенными для создания анимационной последовательности за счет отрицательного смещения по координате у.
При таком смещении каждый следующий кадр помещается выше текущей области просмотра. При этом верхняя часть 2-ГО кадра становится верхней частью области просмотра. После заданной паузы значение переменной г увеличивается, и в окне аплета отображается следующий кадр.
Рис. 1.12. Пример файла с 36-ю кадрами, предназначенными для создания анима- ционнойпоследовательностиРис. 1.12. Пример файла с 36-ю кадрами, предназначенными для создания анима- ционнойпоследовательностиРис. 1.13. Схема выбора кадра из последовательностиРис. 1.13. Схема выбора кадра из последовательности
Рис. 1.12. Пример файла с 36-ю кадрами, предназначенными для создания анима- ционнойпоследовательности
Рис. 1.12. Пример файла с 36-ю кадрами, предназначенными для создания анима- ционнойпоследовательности
Рис. 1.13. Схема выбора кадра из последовательности
Рис. 1.13. Схема выбора кадра из последовательности
Для загрузки изображения используется объект Media-Tracker, а для обработки изображения незаметно для программиста запускается новый поток с помощью метода addimage. Загрузка изображения может быть очень медленной, особенно, если оно содержит много кадров или находится на удаленном сервере. В таком случае вызов метода waitForlD блокируется до тех пор, пока изображение не загрузится полностью.
Читатели, знакомые с пакетом ввода/вывода изображений SDKмогут поинте
ресоваться, почему бы для чтения изображения не использовать метод
Дело в том, что этот метод создает временный файл, а такая операция не разрешена для аплета.
Для этого поток создается в методе start аплета и прерывается в методе
public class Animation extends JApplet {
public void
runner = new
Thread()
{ public void {
} };
runner.start();
public void stop()
{
runner.interrupt();
runner = null;
Метод run выполняет приведенный ниже цикл, перерисовывая экран с отображением очередного кадра с заданной паузой с помощью метода
public void run()
{-
try
{
while
{
repaint();
current = (current +1) % imageCount;
Thread.sleep(200);
}
}
e) {}
}
В заключение рассмотрим другой механизм запуска и остановки анимации по щелчку кнопкой мыши на окне аплета. Обратите внимание, что переменная потока runner применяется здесь в качестве индикатора выполнения потока в данный момент. Когда выполнение прерывается, значение этой переменной равно null. Данный способ довольно часто используется в многопоточных аплетах.
public void
{.
addMouseListener(new MouseAdapter()
{
publicvoidraousePressed(MouseEventevt)
if (runner == null)
start();
else
stop() ;
}
});
}
Аплет считывает имя изображения и количество кадров из дескриптора param в HTML-файле.
<html>
<title>Animation Applet</title>
<body>
<applet code=Animation.class width=100 height=100>
<param name=imagename value="globe.gif">
<param name=imagecount value="36">
</applet>
</body>
</html>
В листинге 1.8 приводится полный код аплета. Обратите внимание, что методы start и stop, которые запускают и останавливают аплет, не являются методами созданного потока.
Этот упрощенный вариант анимационного аплета демонстрирует происходящие "за кулисами" действия, что может быть полезно для понимания принципа работы кода для других аплетов с подобной структурой. Если вас интересует только способ размещения анимационного изображения на Web-странице, тогда обратитесь к аплету Animation в разделе примеров JDK. Он обладает гораздо большими возможностями, чем приведенный здесь аплет, и позволяет использовать звук.
Как показано в следующем разделе, для создания простых анимационных изображений не нужно создавать свои потоки, а достаточно воспользоваться таймером из пакета Swing.
Листинг 1.8. An luxation, java
/**
Sversion 1.20 1999-04-23
@author Cay Horstmann
*/
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import j ava.io.*;
import java.net.*; import javax.swing.*;
/**
Аплет-анимация вращающегося Земного шара */ public class Animation extends JApplet
{
public void initO
{
addMouseListener(new MouseAdapter()
public void mousePressed(MouseEvent evt)
{
if (runner == null)
start();
else
stop();
}
});
try
{
String imageName = getParameter("imagename");
imageCount =
String param =
if (param != null)
imageCount = Integer.parselnt(param);
current = 0;
image = null;
loadimage (new URL(getDocumentBase () , imageName)), }
catch (Exception e)
{
j showStatus("Error: " +e);
/ **
Загрузка изображения
@param url the URL of the image file
*/
public void loadimage(URL url)
throws InterruptedException
// Генерируется методом MediaTracker.wai tFor
{
image = getImage(url);
MediaTracker tracker = new MediaTracker(this) ;
tracker.addimage(image, 0);
tracker.waitForlD(0) ;
imagewidth =
imageHeight =
resize(imagewidth, imageHeight / imageCount);
}
public void paint(Graphics g)
{
if (image == null) return;
g.drawlmage(image, 0, — (imageHeight / imageCount)
* current, null);
public void start()
{
runner = new
Thread()
public void run()
try
{
while (!Thread.interrupted())
repaint();
current = (current +1) % imageCount;
Thread.sleep(200) ;
}
}
catch(InterruptedException e) {} }
};
runner.start();
j showStatus("Click to stop");
public void stop()
{
runner.interrupt();
runner = null;
showStatus("Click to restart" ) ;
}
private Image image;
private int current;
private int imageCount;
private int imagewidth;
private int imageHeight;
private Thread runner;
Таймеры
Во многих средах программирования используются таймеры, которые посылают сигналы элементам программы через определенный интервал времени. Например, для имитации работы часов объект-часы должен ежесекундно получать сигнал.
Пакет Swing содержит простой в использовании встроенный класс-таймер Timer. Таймер создается с указанием интервала между сигналами в миллисекундах и объекта- слушателя, реализующего интерфейс ActionListener.
Timer t = new Timer(1000, listener);
Для вызова таймера применяется метод start.
t.start() ;
После этого вызывается метод actionPerformedкласса-слушателя события с заданным временным интервалом. Это происходит в потоке управления событиями, а не в потоке таймера, поэтому методы Swing можно свободно использовать и для обратных вызовов.
Для остановки таймера вызывается метод stop.
t. stop() ;
Послеэтоготаймерперестаетпосылатьсигналы, еслитолькооннебудетпереза- пущенснова.
Пакет SDK 1.3 содержит не связанный с данной темой класс java.util.Timer, предназначенный для запуска таймера TimerTask после заданной паузы. Этот класс реализует интерфейс Runnable и поддерживает метод cancel для отмены запуска таймера. Однако, в классе j ava.u t i1.Timer не предусмотрен периодический вызов таймера.
В примере, приведенном в конце раздела, демонстрируется работа таймера вместе с пакетом Swing для имитации работы шести различных часов, которые показаны на рис. 1.14.
Рис. 1.14. Имитация работы шести различных часов в разных потоках
Каждые часы представляют собой экземпляр класса ClockCanvas, таймер которых создается в конструкторе.
public ClockCanvas(String tz)
=
czaolneendartz;= new
Timer t = new Timer(1000, new
ActionListener()
{ public void actionPerformed(ActionEvent event) (
calendar.setTime(new Date());
repaint();
}
});
t.start ();
Метод actionPerformed анонимного слушателя событий таймера вызывается примерно каждую секунду. Для получения текущего времени и перерисовки часов вызывается конструктор new Date ().
Как видите, в данном случае не требуется вообще использовать потоковое программирование. Дело в том, что обработкой потоков занимается таймер пакета Swing. Его также можно было бы использовать для создания анимационного изображения в примере из предыдущего раздела.
Полный исходный код этой программы приведен в листинге 1.9.
Листинг 1.9.TimerTest.java
/**
(aversion 1.20 1999-04-27
©author Cay Horstmann
/
import j ava.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*; import javax.swing.Timer;
/**
Этот класс демонстрирует фрейм с несколькими часами, которые обновляются потоками часов
/
public class TimerTest {
public static void main(String[] args) {
TimerTestFrame frame = new TimerTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
} }
/**
Этот фрейм содержит часы
/ class TimerTestFrame extends JFrame {
public TimerTestFrame()
{
setTitle("TimerTest");
setsize(WIDTH, HEIGHT);
Container с = getContentPane();
c . setLayout (new GridLayout (2, 3));
c.add(new ClockCanvas("America/Los_Angeles"));
c.add(new ClockCanvas("America/New_York"));
c.add(new ClockCanvas("America/Caracas"));
c.add(new ClockCanvas("Europe/Rome"));
c.add(new ClockCanvas("Africa/Cairo”));
c.add(new ClockCanvas("Asia/Taipei") ) ;
}
public static final int WIDTH = 450;
public static final int HEIGHT = 300; }
/**
Канва для отображения часов, которые обновляются таймером */ class ClockCanvas extends JPanel {
/**
Создание канвы часов.
@param tz the time zone string
*/
public ClockCanvas(String tz) {
zone = tz;
calendar
=new;
Timer t = new Timer(1000, new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
calendar.setTime(newDate()); repaint();
}
});
t.start() ;
setsize(WIDTH, HEIGHT);
}
public void paintcomponent(Graphics g) {
super.paintcomponent(g) ;
g.drawOval(0, 0, 100, 100);
int seconds = calendar.get(Calendar.HOUR) * 60 * 60
+ calendar.get(Calendar.MINUTE) * 60
+ calendar.get(Calendar.SECOND);
double hourAngle = 2 * Math.PI
* (seconds - 3 * 60 * 60) / (12 * 60 * 60);
double minuteAngle = 2 * Math.PI
* (seconds - 15 * 60) / (60 * 60);
double secondAngle = 2 * Math.PI
* (seconds - 15) / 60;
g.drawLine(50, 50, 50 + (int)(30
* Math.cos(hourAngle)),
50 + (int)(30 * Math.sin(hourAngle))); g.drawLine(50, 50, 50 + (int)(40
* Math.cos(minuteAngle)),
50 + (int)(40 * Math.sin(minuteAngle) ) ) ; g.drawLine(50, 50, 50 + (int)(45
* Math.cos(secondAngle)) ,
50 + (int)(45 * Math.sin(secondAngle))) ;
g.drawstring(zone, 0, 115);
}
private String zone;
private GregorianCalendar calendar;
public static final int WIDTH = 125;
public static final int HEIGHT = 125;
javax.swing.Timer
Конструктор Timer (int delay, ActionListener listener) создает новыйтаймер, которыйпосылаетсобытияслушателю.
Параметры:
delayинтервал времени в миллисекундах между событиями
listener слушатель, который уведомляется об окончании заданного
интервала времени
Метод void start () запускает таймер, который начинает посылать события слушателю.
Методостанавливает таймер, который перестает посылать
события слушателю.
Индикаторы текущего состояния
Индикатор текущего состояния (progress bar) представляет собой простой компонент, как правило, прямоугольник, который частично закрашен другим цветом и показывает текущий прогресс выполнения какой-то операции. По умолчанию на текущее состояние указывает строка п % (где п содержит численное значение), как показано в нижней части окна на рис. 1.15.
Индикатор состояния создается так же, как ползунок (slider), с указанием минимального и максимального значения, а также его ориентации (по желанию).
progressBar = new1000);
progressBar = new0,1000);
Минимальное и максимальное значения можно установить также с помощью методов
В отличие от ползунка индикатор состояния не может корректироваться пользователем. Чтобы обновить его, программа вызывает метод setValue.
Вызов метода setStringPainted пересчитывает состояние индикатора и отображает строку п %.
progressBar.setStringPainted(true) ;
Если нужно вывести другую строку, применяется метод
> 900)
progressBar.setStringPainted("Almost Done");
В программе, код которой приведен втекущего
ге 1.10, создается индикатор текущего состояния состояния для эмулируемого длительного действия.
Класссоздает поток, который 10 раз в секунду увеличивает
значение current. По достижении нужного значения поток завершается. Чтобы завершить поток раньше, его необходимо прервать.
classextends Thread
{
public void run() {
try
{
while (current < target && !interrupted()) {
sleep(100);
current++;
}
}
catch(interruptedException e)
(
}
)
private int current;
private int target;
}
При нажатии кнопки Start (Запуск) запускается новый поток SimulatedActivity. Для обновления индикатора состояния было бы проще вызвать метод setValue из потока эмулируемых операций, однако, он не является безопасным для потоков. Вспомните, что методы Swing необходимо вызывать только из потока управления событиями. Кроме того, это трудно осуществить на практике. В общем случае рабочий поток ничего не знает о существовании индикатора состояния. Поэтому в программе данного примера используется другой подход: запускается таймер, который периодически запрашивает текущее состояние у рабочего потока и обновляет индикатор.
НИМАНИНИМАНИПомните, что даже если рабочий поток знает о существовании индикатора состояния, он все равно не может установить значение индикатора напрямую. Чтобы сделать это из потока управления событиями, рабочий поток применяет метод Swingutilities.invokeLater.
НИМАНИ
НИМАНИ
Следует напомнить, что таймер Swing вызывает метод actionPerformed своих слушателей, и все эти вызовы осуществляются в потоке управления событиями. Это значит, что компоненты Swing можно безопасно обновлять при обратном вызове таймера. Ниже приведен фрагмент кода с обратным вызовом таймера из данного примера. Текущее значение имитируемого действия отображается в текстовой области и в индикаторе состояния. По окончании имитации действия таймер останавливается, а кнопка Start вновь становится доступной для нажатия.
public void actionPerformed(ActionEvent event) {
int current = activity.getCurrent();
// Показать текущее состояние
textArea.append(current + "\n");
progressBar.setValue(current);
// Проверить, завершено ли действие
if (current == activity.getTarget())
{
activityMonitor.stop() ;
startButton.setEnabled(true);
}
Полный код этой программы приведен в листинге 1.10.
В пакете SDK1.4предусмотрена поддержка индикатора неопределенного состояния (indeterminate progress bar), которыйнеотображаетстрокуспроцентнойдолейвыпол- нения задачи, алишьуказываетна некотороеизменениетекущегосостояния. Такойтип индикатора применяется, например, в броузере. Он просто показывает, что броузер ожидает ответ от сервера, причем интервал такого ожидания неизвестен. Для отображения такого индикатора используется метод setlndeterminate.
Листинг 1.10.ProgreasBarTeat.java
/**
©version 1.01 2001-05-25
©author Cay Horstmann
*/
import j
import java.awt.event.*;
import java.util.*;
import j
import javax.swing.event.*;
import javax.swing.Timer;
/**
Пример использования строки состояния
для индикации прогресса выполнения потока
*/
public class ProgressBarTest
{
public static void main(String[] args) {
ProgressBarFrame frame = new ProgressBarFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Фрейм с кнопкой для запуска имитируемого действия, индикатором состояния, а также с текстовой областью для вывода сообщения о выполняемом действии
*/
class ProgressBarFrame extends JFrame {
public ProgressBarFrame()
{
setTitle("ProgressBarTest") ;
setsize(WIDTH, HEIGHT);
Container contentPane = getContentPane() ;
// Текстовая область для вывода сообщения
II О выполняемом действии.
textArea = new JTextArea();
// Настройка панели с кнопкой и индикатором состояния
JPanel panel = new JPanel();
startButton = new JButton("Start");
progressBar = new JProgressBar();
progressBar.setStringPainted(true);
panel.add(startButton);
panel.add(progressBar);
contentpane.add(newJScrollPane(textArea),
BorderLayout.CENTER) ;
contentPane.add(panel, BorderLayout.SOUTH) ;
// Настройка кнопки
startButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
progressBar.setMaximum (1000);
activity = new SimulatedActivity(1000);
activity.start();
activityMonitor.start();
startButton.setEnabled(false);
}
});
// Настройка таймера
activityMonitor = new Tinier (500, new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
int current = activity.getCurrent();
// Показать текущее состояние
textArea.append(current + "\n") ;
progressBar.setvalue(current);
// Проверить, завершено ли действие
if (current == activity.getTarget()) (
activityMonitor.stop();
startButton.setEnabled(true);
}
}
private Timer activityMonitor;
private JButton startButton;
private JProgressBar progressBar;
private JTextArea textArea;
private SimulatedActivity activity;
public static final int WIDTH = 3 00;
public static final int HEIGHT = 200;
/**
Поток имитируемого действия
*/
class SimulatedActivity extends Thread
/**
Создание объекта-потока имитируемого действия.
Этот поток увеличивает значение счетчика, начиная с О
@param t the target value of the counter
*/
public SimulatedActivity(int t)
{
current = 0;
j target = t;
public int getTarget()
{
return target;
}
public int getCurrent()
{ return current;
}
public void runt)
{
try
{
while (current < target && ! interrupted () )
{
sleep(lOO);
current++;
}
}
catch(InterruptedException e)
{
}
}
private int current;
private int target;
}
Мониторы текущего состояния
Индикатор текущего состояния представляет собой довольно простой компонент, который можно размещать в окне. А объект-монитор текущего состояния ProgressMonitor представляет собой самостоятельное диалоговое окно, содержащее индикатор состояния (рис. 1.16). Это диалоговое окно содержит также кнопки ОК и Cancel. При нажатии любой из них окно закрывается. Кроме того, программа может запросить, закрыл ли пользователь окно с помощью кнопки Cancel, и прервать выполняемое действие. (Обратите внимание, что имя этого класса не начинается с буквы J.)
Для создания монитора текущего состояния следует указать следующие параметры.
Родительский компонент, над которым располагается диалоговое окно монитора.
Объект (строка, пиктограмма или компонент), отображаемый в диалоговом окне.
Необязательное примечание, отображаемое под объектом.
Минимальное и максимальное значения.
Однако, самсебе монитор состояния не может измерить прогресс выполняемого
действия или отменить это действие. Для этого необходимо периодически определять
Рис. 1.16. Диалоговое окно тора текущего состоянияРис. 1.16. Диалоговое окно тора текущего состояниящее состояние действия, вызывая метод Progress. (Он эквивалентен методу setValue
Рис. 1.16. Диалоговое окно тора текущего состояния
Рис. 1.16. Диалоговое окно тора текущего состояния
са JProgressBar.) При обновлении значения бходимо также вызывать метод isCanceled для
проверки нажатия пользователем кнопки Cancel.
Для закрытия диалогового окна по окончании отслеживаемого действия необходимо вызвать ме-
этогоэтогооченьВ нейоченьВ нейтод close. Для повторного использования окна можно снова запустить метод start.
этого
этого
очень
В ней
очень
В ней
для наблюдения за имитируемым действием и об-для наблюдения за имитируемым действием и об-Программа в приведенном ниже примере похожа на программу из предыдущего раздела.
для наблюдения за имитируемым действием и об-
для наблюдения за имитируемым действием и об-
новлением монитора текущего состояния необходимо запустить таймер.
public voidevent)
{
int current =
// Отображение прогресса
+ "\n");
progressDialog.setProgress(current);
// Проверка завершения или отмены задачи
if (current ==
||
{
activityMonitor.stop();
progressDialog.close();
activity.interrupt();
startButton.setEnabled(true);
}
}
Учтите, что прекращение работы монитора текущего состояния может быть вызвано после планового завершения действия или после внезапной отмены его со стороны пользователя. В обоих случаях программа выполняет следующие действия.
Прекращает работу таймера, который наблюдает за действием.
Закрывает диалоговое окно.
Прекращает само действие, прерывая его поток.
При выполнении программы, код которой приведен в листингеможно заметить
интересное свойство диалогового окна монитора состояния. Оно выводится не мгновенно, а ожидает в течение некоторого времени, проверяя, может ли отслеживаемое действие завершиться за время меньшее, чем необходимо для отображения окна. Управлять этой паузой отображения диалогового окна монитора можно следующим образом. Для указания паузы (в миллисекундах) между созданием объекта окна и принятием решения о его отображении применяется метод setMillisToDecidePopup (по умолчанию эта пауза равна мс). Методзадает время, в течение которого отображается
окно (по умолчанию равно 2 с).
Очевидно, создатели пакета Swing учли то, что диалоговые окна пакета Swing не всегда отображаются мгновенно. Поэтому не рекомендуется изменять это значение.
В листинге 1.11 приведен полный код описанного монитора состояния. Данный монитор, как и описанная выше строка состояния, отображает состояние имитируемого действия. Как видите, этот монитор имеет достаточно простую структуру, в которой требуется только организовать периодический опрос отслеживаемого потока.
Листинг 1.11. ProoreeaMonitorreiit• java '\ -~\ П
/**
©version 1.01 2001-05-25
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.Timer;
/**
Пример использования диалогового окна с монитором текущего состояния выполняемого действия
*/
public class ProgressMonitorTest
{
public static void main! String[] args)
{
JFrame frame = new ProgressMonitorFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Фрейм с кнопкой запуска имитируемого действия и текстовой областью для вывода информации об отслеживаемом действии
*/
class ProgressMonitorFrame extends JFrame {
public ProgressMonitorFrame()
{
setTitle("ProgressMonitorTest") ;
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane();
// Текстовая область для вывода сообщения //о выполняемом действии
textArea = new JTextArea();
/ / Установка панели кнопки
JPanel panelnew JPanel() ;
startButton = new JButton("Start");
panel.add(startButton);
contentPane. add (new JScrollPane (textArea) , BorderLayout.CENTER);
contentPane.add(panel, BorderLayout.SOUTH);
// Установка кнопки
startButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
// Запуск действия
activity = new SimulatedActivity(1000) ; activity.start() ;
// Запуск диалогового окна монитора progressDialog = new ProgressMonitor(
ProgressMonitorFrame. this,
"Waiting for Simulated Activity", null, 0, activity.getTarget(});
/ / Запуск таймера.
activityMonitor.start() ;
startButton.setEnabledffalse); }
}) ;
// Установка таймера
activityMonitor = new Timer(500, new
ActionListener()
<
public void actionPerformed(ActionEvent event)
{_
int current = activity.getCurrent();
// Отображение прогресса
textArea.append(current + "\n") ; progressDialog.setProgress(current);
// Проверка завершения или отмены
// выполняемой задачи
if (current == activity.getTarget()
|| progressDialog.isCanceled())
<
activityMonitor.stop();
progressDialog.close(); activity.interrupt();
startButton.setEnabledftrue);
}
} }) ;
}
private Timer activityMonitor;
private JButton startButton;
private ProgressMonitor progressDialog;
private JTextArea textArea;
private SimulatedActivity activity;
public static final int WIDTH = 300;
public static final int HEIGHT = 200;
/**
Поток имитируемого действия
*/
class SimulatedActivity extends Thread {
/**
Создание объекта-потока имитируемого действия.
Этот поток увеличивает значение счетчика, начиная с О
@param t the target value of the counter.
*/
public SimulatedActivity(int t)
{
current = 0;
target = t;
)
public int getTarget()
{
return target;
}
public int getCurrent()
{
return current;
}
public void run()
{
try
{ while (current < target && !interrupted()) {
sleep(lOO);
current++;
}
}
catch(InterruptedException e)
}
)
private int current ;
private int target;
}
Мониторинг операций ввода данных
В пакете Swing содержится полезный потоковый фильтр ProgressMonitorInputStream, который автоматически отображает на экране диалоговое окно с информацией о прочитанной части входного потока.
Для его применения достаточно разместить фильтр ProgressMonitorInputStream между фильтрами обычной последовательности. (Подробнее потоки ввода/вывода рассматриваются в главе 12 тома 1 настоящей книги.)
Предположим, что в программе нужно организовать считывание текстового файла. Для начала нужно создать объект класса FilelnputStream.
FilelnputStream in = new Fileinputstream(f);
Обычно объектфайл in преобразуется в объект класса InputStreamReader.
InputStreamReader reader = new InputStreamReader(in);
Однако, для слежения за этим входным потоком следует преобразовать файловый входной поток в поток монитора СОСТОЯНИЯ.
ProgressMonitorInputstream progressIn
= new ProgressMonitorlnputStream(parent, caption, in) ;
Для чего нужно указать родительский компонент parent, заголовок caption и поток in. Метод read отслеживаемого потока пересылает число прочитанных байт и обновляет диалоговое окно состояния.
После этого можно продолжить создание последовательности фильтров.
InputStreamReader reader = new InputStreamReader(progressin);
Вот, собственно, и все. Теперь при чтении файла будет автоматически отображаться монитор текущего состояния процесса чтения.
Для определения общего количества байтов в потоке используется метод available класса Inputstream. Однако учтите, что этот метод возвращает число байт, доступных в потоке без блокировки. Мониторы текущего состояния не могут работать со всеми типами потоков.
Программа, код которой приведен в листинге 1.12, подсчитывает число строк в файле. Диалоговое окно мониторинга состояния выводится при чтении очень большого файла (например, файла The Count of Monte Cristo, который находится на прилагаемом компактдиске).
Обратите внимание, что в программе используется не слишком эффективный способ заполнения текстовой области. Быстрее было бы прочесть файл в StringBuffer, а затем отобразить содержимое строкового буфера в текстовой области. Однако, в данном примере это сделано на- Рис. 1.17. Монитор текущего состоя- меренно, чтобы продемонстрировать применения для входного потоканиеДиалогового окна монитора текущего состоя-
ния длительного процесса.
Во избежание мерцания текстовая область не отображается до тех пор, пока полностью не заполнится.
истинг
@version 1.01 2001-05-25
©author Cay Horstmann
import j ava.awt.*;
import j ava.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import j
/**
Пример мониторинга входного потока
*/
public class ProgressMonitorInputStreamTest {
public static void main(String[] args) {
JFrame frame = new TextFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Фрейм с командами меню для загрузки текстового файла и текстовой областью для его отображения.
Текстовая область создается во время загрузки файла и используется в качестве панели содержания данного фрейма по окончании процесса загрузки. Это позволяет избежать мерцания во время загрузки
*/
class TextFrame extends Jframe {
public TextFrame()
{
setTitle("ProgressMonitorlnputStreamTest”); setSize(WIDTH, HEIGHT);
// Установка меню
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
openitem = new JMenuItem("Open");
openitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
try
{
openFile();
}
catch(lOException exception)
{
exception.printStackTrace();
}
});
fileMenu.add(openitem);
exititem = new JMenuItem("Exit");
exitltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
System.exit(0) ;
}
)) ;
fileMenu.add(exitltem);
}
/**
Этот метод предлагает пользователю выбрать файл, загружает его в текстовую область и использует ее в качестве панели содержания данного фрейма
*/
public void openFile() throws IOException {
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File( "."));
chooser.setFileFilter(
new javax.swing.filechooser.FileFilter() {
public boolean accept(File f) {
String fname =
return fname.endsWith(".txt") ||f.isDirectory();
}
public String getDescription()
{ return "Text Files";
}
}) ;
intr=chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return; final Filef=chooser.getSelectedFile();
// Установка потока и
// последовательности фильтра чтения
Fileinputstream filein = new Filelnputstream(f);
ProgressMonitorInputStrearn progressin
= new ProgressMonitorlnputStream(this, "Reading " + f.getName(), fileln);
InputstreamReader inReader
new InputStreamReader(progressin);
final BufferedReader in = new BufferedReader(inReader);
// Отслеживаемое действие должно // находиться в новом потоке
Thread readThread = new Thread() {
public void run() {
try {
final JTextArea textArea = new JTextArea();
String line;
while ((line = in.readLine()) != null) {
textArea.append(line);
textArea.append("\n");
}
in.close();
// Установка панели содержимого в
// потоке управления событиями
EventQueue.invokeLater(new
Runnable()
{
public void run ()
{
setContentPane(new JScrollPane(textArea)); validate();
} ));
}
catch(lOException exception)
{
exception.printStackTrace() ;
}
}
};
readThread.start();
private JMenuItem openitem;
private JMenuItem exititem;
public static final int WIDTH = 300;
public static final int HEIGHT = 200;
javax.swing.JProgressBar
Конструктор JProgressBar () создает горизонтальный ползунок с заданными по умолчанию минимальным и максимальным значением.
Конструктор JProgressBar (int direction) создает горизонтальный ползунок с направлением direction и заданными по умолчанию минимальным и максимальным значением.
Конструктор JProgressBar (int min, int max) создает горизонтальный ползунок с минимальным значением min и максимальным значением max.
Конструктор JProgressBar (int direction, int min, int max) создает ползунок с направлением direction, минимальным значением min и максимальным значением max.
Параметры:
direction одно из значений SwingConstants . HORIZONTAL или SwingConstants .VERTICAL. По умолчанию используется горизонтальное значение Swing-Constants. HORIZONTAL
minминимальное значение индикатора состояния, которое по
умолчанию равно 0
maxмаксимальное значение индикатора состояния, которое по
умолчанию равно 100
Метод int getMinimum () возвращает минимальное значение индикатора состояния.
Метод int getMaximum() возвращает максимальное значение индикатора СОСТОЯНИЯ.
Метод void setMinimum(int value) задает минимальное значение индикатора состояния.
Метод void setMaximum(int value) задает максимальное значение индикатора состояния.
Метод int getValue {) возвращает текущее значение индикатора состояния.
Метод void setValue(int value) задает текущее значение индикатора СОСТОЯНИЯ.
Метод String getString() возвращает строку, отображаемую в индикаторе СОСТОЯНИЯ.
Метод void setString(String s) задает строку, отображаемую в индикаторе состояния. Если строка s равна null, то по умолчанию отображается строка п %.
Метод boolean isStringPainted() возвращает значение свойства "отображение строки". Если оно равно true, то строка отображается в верхней части индикатора состояния, a если false,то строка не отображается.
Метод void setStringPainted (boolean b) задает значение свойства "отображение строки". Если оно равно true, то строка отображается в верхней части индикатора состояния, а если false,то строка не отображается.
Метод boolean islndeterminate () возвращает значение свойства "неопределенное состояние" (начиная с SDK 1.4). Если оно равно true, то индикатор состояния представляет собой прямоугольник, движущийся в прямом и обратном направлении, обозначающий неизвестный интервал ожидания.
Метод void setlndeterminate (boolean b) задает значение свойства "неопределенное состояние" (начиная с SDK 1.4). Если оно равно true, то индикатор состояния представляет собой прямоугольник, движущийся в прямом и обратном направлении, обозначающий неизвестный интервал ожидания. По умолчанию для этого свойства используется значение false.
javax.swing.ProgressMonitor
КонструкторProgressMonitor (Component parent, Object message, String note, int min, int max) создает диалоговое окно для монито-
ринга текущего состояния выполняемого действия.
Параметры:
parent
родительский компонент, над которым отображается диалоговое окно
message note
сообщение, которое отображается в диалоговом окне дополнительная строка, которая отображается под сообщением. Если она равнаnull, то для примечания не отводится места, и последующий вызов метода setNote не дает никакого эффекта
min max
минимальное значение индикатора состояния максимальное значение индикатора состояния
• Методvoid setNote (String note) изменяет текст примечания.
Метод void setProgress (int value) задает значение value для индикатора СОСТОЯНИЯ.
Метод void close () закрывает диалоговое окно мониторинга текущего состояния.
Метод boolean isCanceled() возвращает значение true, если пользователь закрыл диалоговое окно мониторинга текущего состояния.
javax.swing.ProgressMonito Input Stream
Конструктор ProgressMonitorlnputSream(Component parent, Object message, InputStream in) создает фильтр входного потока вместе со связанным с ним диалоговым окном мониторинга текущего состояния.
Параметры:
parentродительский компонент, над которым отображается диало
говое окно
messageсообщение, которое отображается в диалоговом окне
inвходной поток, подлежащий мониторингу текущего состояния
Использование конвейеров
для организации взаимодействия потоков
Схема взаимодействия потоков имеет достаточно простую структуру. Поток-источник (producer thread) генерирует поток байтов, а поток-обработчик (consumer thread) считывает и обрабатывает этот поток. Если поток байтов недоступен для чтения, то поток-обработчик блокируется. Если поток-источник генерирует данные быстрее, чем поток-обработчик может их принять и обработать, то в таком случае блокируется операция записи потока- источника. В Java для реализации этой схемы взаимодействия потоков предусмотрены классы-конвейеры PipedlnputStream и PipedOutputStream. (Для реализации данной схемы взаимодействия потоков с Unicode-символами, а не байтами, предусмотрена другая пара классов конвейерного чтения и записи, PipedReader и PipedWriter.)
Основная причина использования конвейеров заключается в поддержке простой структуры потоков. Поток-источник просто пересылает результаты потоку вывода и "забывает" о них. В свою очередь поток-обработчик читает данные, не заботясь о том, откуда они поступают. Применение конвейеров позволяет соединять несколько потоков, не заботясь об их синхронизации.
Пример организации взаимодействия потоков на основе конвейеров приводится в листинге 1.13. Поток-источник генерирует случайные числа в случайные моменты
ИсточникИсточникФильтрФильтрОбработчикОбработчикКонвейерные выходныепотоки данныхКонвейерные выходныепотоки данныхКонвейерные входныепотоки данныхКонвейерные входныепотоки данныхвремени, поток-фильтр считывает входные числа и вычисляет их среднее значение, а поток-обработчик выводит полученный результат. (Для выхода из программы необходимо воспользоваться комбинацией клавиш <Ctrl+C>.) На рис. 1.18 приведена схема взаимодействия потоков и соединяющие их конвейеры. Пользователи операционной системы UNIX легко узнают в них конвейеры, соединяющие процессы в UNIX.
Источник
Источник
Фильтр
Фильтр
Обработчик
Обработчик
Конвейерные выходныепотоки данных
Конвейерные выходныепотоки данных
Конвейерные входныепотоки данных
Конвейерные входныепотоки данных
Рис. 1.18. Схема взаимодействия потоков на основе конвейеров
Конвейерную обработку потоков данных можно применять лишь тогда, когда взаимодействие между потоками управления происходит на низком уровне, как в рассмотренном здесь примере с последовательностью чисел. В других ситуациях следует использовать очереди. Поток-источник помещает объекты в очередь, а поток- обработчик читает и удаляет их оттуда.
Листинг 1.13. PlpoTeat. java ■_, *'
aversion 1.20 1999-04-23
@author Cay Horstmann
import java.util. *; import java.io.*;
/**
Пример организации взаимодействия потоков с помощью конвейеров
*/
public class PipeTest
{t ,
public static void main(String args[])
try {
/* Установка конвейеров */
PipedOutputStream poutl = new PipedOutputStreamt) ;
PipedlnputStream pinl = new PipedlnputStream(poutl);
PipedOutputStream pout2 = new PipedOutputStreamt);
PipedlnputStream pin2 = new PipedInputStream(pout2);
/* Создание потоков */
Producer prodnew Producer(poutl);
Filter filt = new Filter(pinl , pout2) ;
Consumer cons = new Consumer(pin2) ;
/* Запуск потоков */
prod.start() ;
filt.start() ;
cons.start();
}
catch (IOException e){}
} }
/**
Поток, который записывает случайные
числа в выходной поток
*/
class Producer extends Thread {
/**
Создание потока-источника
Sparam os the output stream
*/
public Producer(Outputstream os)
{
out = new DataOutputStream(os);
public void run()
{
while (true)
{
try
{
double num =
out.writeDouble(num);
out.flush();
sleep(Math.abs(rand.nextlnt() % 1000))
}
catch(Exception e)
{
System.out.printin("Error: " + e) ;
}
}
}
private DataOutputStream out;
private Random rand = new Random(); }
/**
Поток, который считывает числа из входного потока и записывает их среднее значение в выходной поток */ class Filter extends Thread {
/**
Создание потока-фильтра
@param is the output stream
"Sparam os the output stream
*/
public Filter(Inputstream is, OutputStream os)
in = new DatalnputStream(is);
out = new DataOutputStream(os);
}
public void run()
{
for (;;)
{
try
{
double x = in.readDouble();
total += x;
count++;
if (count != 0) out.writeDouble(total / count); }
catch(lOException e)
{
j System.out.printin("Error: " + e) ;
} }
private DatalnputStream in;
private DataOutputStream out;
private double total = 0;
private int count = 0;
/**
Поток, который считывает числа из потока и выводит только те из них, которые отличаются от предыдущего входного значения на заданную пороговую величину
*/
class Consumer extends Thread
{
/**
Создание потока-обработчика
@param is the input stream
*/
public Consumer(Inputstream is)
{
in = new DatalnputStream(is);
public void run ()
{
t{ry
double x = in.readDouble() ;
if (Math.abs(x- oldx) >THRESHOLD)
{
old
=x;
}}
catch(lOException e)
System.out.printin("Error: "+e);
private double oldx = 0;
private DatalnputStream in;
private static final double THRESHOLD = 0.01;
java.io.PipedlnputStream
Конструктор PipedlnputStream() создает новый конвейерный входной поток, который еще не соединен с конвейерным выходным потоком.
Конструктор PipedlnputStream(PipedOutputStream out) создает новый конвейерный входной поток, который читает данные из конвейерного выходного потока out.
Параметры: outисточник данных
Метод void connect (PipedOutputStream out) присоединяет конвейерный выходной поток out, из которого будут считываться данные.
Параметры:
outисточник данных
java.io.PipedOutputStream
Конструктор PipedOutputStream () создает новый конвейерный выходной поток, который еще не соединен с конвейерным входным потоком.
Конструктор PipedOutputStream(PipedlnputStream in) создает новый конвейерный выходной поток, который записывает данный в конвейерный ВХОДНОЙ поток in.
Параметры:
inместо назначения данных
Метод void connect (PipedlnputStream in) присоединяет конвейерный входной поток in, в который будут записываться данные.
Параметры:
inместо назначения данных
КоллекцииКоллекцииВ этой главе...
Коллекции
Коллекции
Интерфейсы коллекций
Конкретные коллекции
т Архитектура коллекций
т Алгоритмы
т Унаследованные коллекции
Т)
IDпарадигме объектно-ориентированного программирования поддерживается концепция инкапсуляции данных внутри классов. Однако, это не означает, что способы размещения данных в классах имеют меньшее значение, чем в традиционных парадигмах программирования. Конечно же, используемая структура данных зависит от решаемой задачи: для быстрого поиска среди тысяч (или даже миллионов) элементов либо для создания упорядоченной последовательности элементов и вставки/удаления элементов в любом месте последовательности, либо для создания массива, растущего во время выполнения программы, с возможностью произвольного доступа к его элементам и т.д. Способ организации данных внутри класса имеет очень большое значение для реализации методов в естественном стиле, а также для достижения максимальной производительности.
В этой главе показано, как технология Java помогает использовать традиционные структуры данных для создания надежных и эффективных программ. В студенческих учебных программах по информатике курс Структуры данных, как правило, рассчитан на один семестр. Поэтому не трудно догадаться, что этой важной тематике посвящено огромное количество книг. Разумеется, описание всех типов структур данных, которые используются в программировании, не является основной целью этой главы. Здесь речь идет лишь о тех фундаментальных структурах, которые поддерживаются в языке Java. Авторы надеются, что после изучения этой главы читатели смогут легко преобразовать свои структуры данных в структуры данных языка Java.
Интерфейсы коллекций
До выхода платформы Java версии 2 стандартная библиотека содержала лишь небольшой набор классов для наиболее полезных структур данных: Vector, Stack, Hashtable, BitSet и интерфейс Enumeration, предоставляющий абстрактный механизм работы с элементами в произвольном контейнере. Этот мудрый выбор только основных структур данных на начальном этапе развития Java был оправдан тем, что для создания обширной библиотеки классов-коллекций требуется затратить больше времени и усилий.
Однако, в платформуJava версии 2 разработчики включили уже достаточно обширный и полный набор структур данных. При этом они столкнулись с рядом конфликтных ситуаций. С одной стороны, они хотели бы, чтобы эта библиотека была небольшой и простой в изучении, чтобы избежать сложности, присущей библиотеке стандартных шаблонов Standard Template Library (STL) языка C++. А с другой стороны, желательно было бы воспользоваться "обобщенными алгоритмами", которые впервые появились в библиотеке STL. Кроме того, желательно использовать унаследованные классы из прежних версий Java. Как и всем разработчикам библиотек коллекций, им пришлось принимать компромиссные и не всегда удачные решения. В этом разделе рассматривается общая иерархия коллекций языка Java, показаны способы их использования, а также мотивы создания и применения некоторых наиболее противоречивых элементов этой иерархии.
Разделение интерфейса и реализации коллекции
Также, как и во многих других современных библиотеках структур данных, в библиотеке коллекций языка Java разделяются интерфейсы (interfaces) и реализации (implementations) коллекций. Рассмотрим в качестве примера знакомую структуру данных, а именно, очередь (queue). В библиотеке языка Java такой структуры данных нет, но на ее примере можно прекрасно проиллюстрировать базовые концепции использования структур данных.
■ В случае необходимости очередь достаточно просто можно создать на основе класса LinkedList, который рассматривается в этой главе.
В интерфейсе очереди (queue interface) обычно перечисляются операции добавления элемента в конце очереди, удаления элемента в начале очереди, а также определения количества элементов в очереди. Очередь используется для набора объектов по принципу "первый вошел, первый вышел", как схематически показано на рис. 2.1.
НачалоКонец
Рис. 2.1. Очередь
Если бы в библиотеке коллекций Java содержался такой интерфейс очереди, то он б'.;, вы глядел следующим образом.
interface Queue
{
void add(Object ob j) ;
;
int size ();
}
Этот интерфейс ничего не говорит о том, как реализована очередь. Существуют два распространенных варианта реализации очереди: круговой массив (circular array) и связанный список (linked list), которые схематически показаны на рис. 2.2.
НачалоНачалоКонецКонецНачалоКонецНачалоКонецСвязанный списокРис. 2.2. Варианты реализации очередиСвязанный списокРис. 2.2. Варианты реализации очереди
Начало
Начало
Конец
Конец
НачалоКонец
НачалоКонец
Связанный список
Рис. 2.2. Варианты реализации очереди
Связанный список
Рис. 2.2. Варианты реализации очереди
Каждый из этих вариантов реализации можно выразить как класс на основе интерфейса Queue.
class CircularArrayQueue implements Queue {
CircularArrayQueue(int capacity) { ,}
public void add(Object obj) { ... } public Object remove() { ... } public int size() { ... }
private private private }private private private }Object!] elements; int head; int tail;
private private private }
private private private }
class LinkedListQueue implements Queue {
LinkedListQueue() { ... }
public void add(Object obj) { .. . }
public Object remove() { ... }
public int size() { ... }
private Link head;
private Link tail;
}
При использовании очереди в программе после ее создания совсем не обязательно знать, какая именно реализация применяется. Поэтому использовать конкретный класс (например CircularArrayQueue) следует только при конструировании объекта коллекции. Для хранения ссылки на эту коллекцию следует применять интерфейсный тип {interface type).
Queue expressLane = new CircularArrayQueue(100); expressLane.add(new Customer("Harry"));
Данный подход позволяет с легкостью перейти к любой другой реализации, редактируя только код конструктора коллекции. Если окажется, что удобно использовать реализацию LinkedListQueue, то приведенный выше код изменится следующим образом.
Queue expressLane = new LinkedListQueue(); expressLane.add(new Customer("Harry"));
В каких случаях следует использовать ту или иную реализацию? Интерфейс не может дать ответ на вопрос об эффективности разных реализаций. Как известно, круговой массив работает эффективнее, чем связанный список, поэтому его предпочитают использовать в большинстве случаев. Однако, как всегда, за это приходится расплачиваться чем-то другим. Круговой массив представляет собой ограниченную {bounded) коллекцию, т.е. его емкость ограничена. Если заранее неизвестно максимальное количество его элементов во время работы программы, то лучше воспользоваться реализацией на основе связанного списка.
Этот пример демонстрирует еще одну проблему, с которой сталкиваются разработчики библиотеки классов коллекций: в интерфейсе метода add следует предусмотреть возможность неудачного исхода при переполнении кругового массива.
class CircularArrayQueue
{
public void add(Object obj)
throws CollectionFullException
}
Проблема заключается в том, что теперь класс CircularArrayQueue не может реализовать интерфейс Queue, так как при переопределении метода нельзя добавить спецификатор прерывания. Как следует поступить в таком случае: создать класс CircularArrayQueue на основе двух интерфейсов BoundedQueue и Queue либо позволить методу add генерировать исключительную ситуацию CollectionFullException? У обоих подходов есть свои достоинства и недостатки. Проблемы именно такого типа существенно усложняют задачу разработки логически последовательной библиотеки классов коллекций .
Как уже отмечалось выше, в библиотеке Java не предусмотрен отдельный класс- очередь. Пример был приведен лишь как иллюстрация разницы между интерфейсом и реализацией, так как очередь имеет простой интерфейс и две хорошо известных реализации. В следующем разделе рассматривается классификация коллекций в библиотекеJava.
Интерфейсы Collection и Iterator
Фундаментальным интерфейсом для классов-коллекций в библиотеке Java является интерфейс Collection, который имеет следующие два фундаментальных метода:
boolean add(Object obj)
Iterator iterator!)
Помимо них интерфейс Collection содержит еще несколько методов, которые обсуждаются ниже.
Метод addдобавляет объект в коллекцию и возвращает либо значение true, если коллекция изменилась, либо falseв противном случае. Например, если попытаться добавить в коллекцию-множество уже существующий объект, то запрос add будет отвергнут, поскольку по определению коллекция-множество не может содержать объекты-дубликаты.
Метод iterator возвращает объект-итератор, реализующий интерфейс Iterator, который применяется для последовательного обращения к элементам коллекции.
Интерфейс iterator имеет следующие три фундаментальных метода:
Object next()
boolean hasNext() void remove()
Последовательно вызывая метод next, можно перебрать все элементы коллекции. Однако, по достижении конца коллекции он генерирует исключительную ситуацию NoSuchElementException. Поэтому перед вызовом метода next следует вызывать метод hasNext, который возвращает значение true, если конец коллекции еще не достигнут, и объект-итератор может перейти к следующему элементу. Для перебора всех элементов коллекции нужно запросить объект-итератор и продолжать вызывать метод next до тех пор, пока метод hasNext возвращает значение true.
Iterator iter = c.iterator();
while (iter.hdsNext()) {
Object obj = iter.next();
выполнить какие-то действия c объектом obj )
■ Опытные программисты могут заметить, что методы next и hasNext интерфейса Iterator аналогичны методам nextElement и hasMoreElements интерфейса Enumeration. Разработчики библиотеки коллекций платформы Java могли бы расширить интерфейс Enumeration, однако им не понравились запутанные названия методов, поэтому в новой версии платформы используются более короткие имена.
Наконец, метод remove удаляет элемент, возвращенный при последнем вызове метода next.
Читатель может вполне резонно удивиться, почему метод remove является частью интерфейса Iterator. Объяснение достаточно просто: операция удаления элемента наиболее эффективно будет выполнена тогда, когда известно место, где этот элемент находится. Итератор знает о позиции элемента в коллекции, и именно поэтому метод remove используется в интерфейсе Iterator. Перебирая элементы коллекции, можно легко удалить все подлежащие удалению элементы.
Между итераторами библиотеки коллекций Java и итераторами других библиотек существует важное концептуальное различие. В традиционных библиотеках коллекций, например, в библиотеке стандартных шаблонов языка C++, итераторы моделируются как
индексы массива. Зная итератор, можно непосредственно обратиться к элементу коллекции, находящемуся в данной позиции подобно тому, как это делается для элемента массива a[i] при заданном индексе i. Независимо от способа обращения, итератор можно переместить в следующую позицию посредством операции i++ без обращения к элементу коллекции. В Java итераторы применяются по-другому. Обращение к элементу и изменение позиции тесно связаны, потому что единственный способ обращения к элементу основан на вызове его метода next, что приводит к перемещению в следующую позицию.
Итераторы в Java можно рассматривать так, как будто они находятся между элементами. При вызове метода next итератор перескакивает следующий элемент и возвращает ссылку на него, как показано на рис. 2.3.
Итератор
Возвращаемыйэлемент
Рис.2.3. Перемещение итератора
■ По сути, вызов метода iterator . next можно рассматривать как вызов метода inputstream. read, потому что при считывании байта из потока автоматически происходит его "потребление". Далее следующий вызов метода read приводит к потреблению и возвращению следующего байта из потока. Аналогично последовательные вызовы метода next считывают все элементы коллекции.
При использовании метода remove следует проявлять особую осторожность, потому что вызов метода remove удаляет элемент, возвращенный при последнем вызове next. Это оправданно тем, что если требуется удалить определенный элемент, то следовало бы просмотреть его до удаления. Таким образом, даже при удалении элемента по заданной позиции необходимо "пройти" его. Например, вот как удаляется первый элемент коллекции.
Iterator it = с.iterator ( );
it.next(); // пропустить первый элемент
it.remove(); // удалить его
Учтите, что между вызовами методов next и remove существует строгая взаимосвязь. Нельзя вызвать метод remove, если ему не предшествует вызов метода next. В противном случае генерируется исключительная ситуация IllegalStateException.
Если требуется удалить два соседних элемента, нельзя просто дважды подряд вызвать метод remove.
it.remove();
it.remove(); // Ошибка!
Для этого нужно сначала вызвать метод next, чтобы "перепрыгнуть" через элемент, подлежащий удалению.
it.remove();
it.next();
it.remove(); // Правильно!
Интерфейсы Collection и Iterator являются обобщенными интерфейсами. поэтому для них можно создавать обобщенные методы, работающие для любых типов коллекций. Например, вот как выглядит обобщенный метод print, предназначенный для вывода всех элементов коллекции.
public static void print(Collection c)
{
System. out.print(" [");
Iterator iter = c.iterator();
while (iter.hasNext())
System.out.print(iter.next() + " ");
System.out.printin("]");
Этот пример иллюстрирует способ создания обобщенного метода. Однако, чтобы вывести все элементы коллекции, достаточно просто воспользоваться вызовом метода Sys tern.out.printIn(c). Дело в том, что каждый класс-коллекция имеет метод toString, который возвращает строку, содержащую все элементы коллекции.
А вот как выглядит метод, который добавляет все элементы одной коллекции к другой.
public static boolean addAll(Collection to, Collection from) {
Iterator iter = from.iterator();
boolean modified = false;
while (iter.hasNext())
if (to.add(iter.next()));
modified = true;
return modified;
}
Напомним, что метод add возвращает значение true, если добавление элемента изменило коллекцию. Эти полезные методы можно реализовать для произвольных коллекций, так как интерфейсы Collection и Iterator содержат такие фундаментальные методы, как add и next.
Разработчики библиотеки коллекций Java решили, что некоторые из этих обобщенных методов настолько полезны, что должны поддерживаться библиотекой. Благодаря этому, пользователям не нужно вновь изобретать колесо, например, самостоятельно создавать такие методы, как метод addAll.
Если бы Collection представлял собой абстрактный класс, а не интерфейс, было бы очень просто наделить этот класс такой функциональностью. Однако, в платформе Java
интерфейсы не могут содержать реализации методов. Поэтому в библиотеке коллекций применяется несколько иной подход. Интерфейс Collection объявляет несколько полезных методов, которые должны поддерживаться во всех классах реализации.
int size()
boolean isEmpty()
boolean contains(Object obj)
boolean containsAll(Collection c) boolean equals(Object other) boolean addAll(Collection from) boolean remove(Object obj) boolean removeAll(Collection c) void clear()
boolean retainAll(Collection c) Object!] toArrayO
Многие из этих методов имеют вполне очевидные названия, а более подробное их описание можно найти в примечаниях в конце этого раздела.
Конечно же, было бы слишком обременительно, если бы каждый класс, реализующий интерфейс Collection, должен был реализовывать так много вполне стандартных методов: Чтобы упростить процесс реализации этих методов, в классе AbstractCollection оставлены абстрактными только фундаментальные методы (такие, как add и iterator), a на их основе реализованы все остальные стандартные методы.
public class AbstractCollection implements Collection {
public abstract boolean add(Object obj) ;
public boolean addAll(Collection from) {
Iterator iter = from.iterator();
boolean modified = false;
while (iter.hasNext()) if (add(iter.next()) ) modified = true;
return modified;
}
} ' “
Теперь конкретный класс-коллекция может расширить класс AbstractCollec- tionза счет реализации метода add, а метод addAll уже реализован в родительском классе AbstractCollection. Однако, если дочерний класс содержит более эффективный вариант реализации метода addAll, то его можно использовать вместо варианта из родительского класса.
Такой принцип создания структуры классов-коллекций прекрасно подходит пользователям и разработчикам. Пользователи классов-коллекций имеют достаточно богатый выбор методов из обобщенного интерфейса, а разработчикам структур данных не требуется реализовывать все стандартные вспомогательные методы.
java.util. Collection
Метод Iterator iterator () возвращает итератор, используемый для обращения к элементам коллекции.
• Метод int size () возвращает текущее количество элементов в коллекции.
Метод boolean isEmpty() возвращает значение true, если коллекция не содержит элементов.
Метод boolean contains (Object obj ) возвращает значение true, если коллекция содержит объектobj.
Параметры:
objобъект, с которым производится сравнение
Метод boolean containsAll (Collection other) возвращает значение true, если коллекция содержит все элементы коллекции other.
Параметры:
otherколлекция, с которой производится сравнение
Метод boolean add (Object element) добавляет элемент в коллекцию. Возвращает значение true, если коллекция изменилась в результате вызова метода.
Параметры:
elementдобавляемый элемент
Метод boolean addAll (Collection other) добавляет все элементы из коллекции other в данную. Возвращает значение true, если коллекция изменилась в результате вызова метода.
Параметры:
otherколлекция, содержащая добавляемые элементы
Метод boolean remove (Object obj ) удаляет объект obj из коллекции. Возвращает значение true, если объект был удален.
Параметры: objудаляемый объект
Метод boolean removeAll (Collection other) удаляет из данной коллекции все элементы, содержащиеся в коллекции other. Возвращает значение true, если коллекция изменилась в результате вызова метода.
Параметры: otherколлекция, содержащая удаляемые элементы
Метод void clear() удаляет из данной коллекции все элементы.
Метод boolean retainAll(Collection other) удаляет из данной коллекции элементы, не равные тем, что содержатся в коллекции other. Возвращает значение true, если коллекция изменилась в результате вызова метода.
Параметры:
otherколлекция, содержащая сохраняемые элементы
Метод Object[] toArray() возвращает массив с объектами коллекции. java.util.Iterator
Метод boolean hasNext () возвращает значение true, если существует следующий элемент, к которому можно обратиться.
Метод Object next () возвращает следующий объект. Генерирует исключительную ситуацию NoSuchElementExcept ion, если достигнут ко- нецколлекции.
Метод remove () удаляет и возвращает последний прочитанный элемент. Этот метод должен вызываться сразу же после обращения к элементу. Если после чтения элемента коллекция изменилась, данный метод генерирует исключительную ситуацию IllegalStateException.
Конкретные коллекции
Прежде, чем приступать к подробному рассмотрению интерфейсов, познакомимся со структурами данных, которые поддерживаются в библиотеке Java. После предварительного знакомства с основными классами структур данных мы снова вернемся к абстрактным понятиям и увидим, какое место занимают эти классы в иерархии коллекций.
Связанные списки
Удаленный элементУдаленный элементВ многочисленных примерах тома I часто использовались массивы и массивы-списки Однако, массивы и массивы-списки обладают серьезным недостатком. Удаление элемента из середины массива представляет собой очень ресурсоемкую операцию.
Удаленный элемент
Удаленный элемент
венно после удаления элемента все элементы массива, расположенные после него, необходимо переместить на одну позицию ближе к началу, как показано на рис. 2.4. То же самое относится и к операции вставки элемента в середину массива.
2.4. Удаление элемента массива2.4. Удаление элемента массиваЭта проблема решается с помощью другой хорошо известной структуры связанного списка (linked list). Массив сохраняет ссылки на объекты в последовательных ячейках памяти, а связанный список хранит каждый объект как отдельное звено (link). Кроме того, в каждом звене хранится ссылка на следующее звено последовательности. В языке Java все связанные списки, в действительности, являются дважды связанными, т.е. в каждом элементе хранится ссылка и на предыдущий элемент, как показано на рис. 2.5.
2.4. Удаление элемента массива
2.4. Удаление элемента массива
Удаление элемента из середины связанного списка представляет собой простую операцию. Для этого достаточно изменить ссылки на соседние элементы, как показано рис. 2.6.
У читателей, которым приходилось реализовывать связанный список, могли остаться не самые лучшие воспоминания о запутанном процессе связывания ссылок при удалении или добавлении элементов в связанном списке. Они будут приятно удивлены тем, что в библиотеке коллекций Java поддерживается очень удобный в работе класс связанного списка
Рис. 2.5. Дважды связанный список
Рис. 2.6. Удаление элемента из дважды связанного списка
Класс LinkedList реализует интерфейс Collection, а потому для перебора элементов списка можно использовать уже знакомые нам методы. Ниже приведен фрагмент кода для вывода трех первых элементов списка, добавления трех элементов и последующего удаления третьего элемента.
LinkedList staff = new LinkedList();
staff.add("Angela") ;
staff.add("Bob") ;
staff.add("Carl") ;
Iterator iter = staff.iterator(); for (int i = 0; i < 3; i++)
System.out.printin(iter.next() ) ;
iter.remove(); // удалить последний прочитанный элемент
Однако, между связанными списками и коллекциями общего типа существует важное различие. Связанный список представляет собой упорядоченную коллекцию, где положение
объекта имеет большое значение. Метод LinkedList.add добавляет объект в конце списка, но иногда нужно вставить объект в середине списка. Такое поведение метода add определяется итератором, поскольку именно итераторы описывают расположение элементов в коллекции. Использование итератора для добавления элементов имеет смысл только для коллекций, обладающих естественным упорядочением. Например, в коллекции типа множество (set), которая описывается в следующем разделе, не задается упорядочение элементов. Поэтому в интерфейсе Iterator нет метода add. В библиотеке коллекций предусмотрен дочерний интерфейс Listiterator,содержащий метод add.
interface Listlterator extends Iterator {
void add(Object) ;
Рис. 2.7. Добавление элемента в связанный список
В отличие от метода Collection. add данный метод не возвращает значение типа boolean, так как предполагается, что операция add всегда изменяет список.
Кроме того, в интерфейсе Listlterator предусмотрено два метода для обхода списка в обратном направлении.
Obj ect previous()
boolean hasPrevious()
Как и метод next, методprevious возвращает уже пройденный объект.
Метод listiterator класса LinkedList возвращает объект-итератор, реализующий интерфейс Listlterator.
Listlterator iter = staff.listIterator();
Метод add добавляет новый элемент перед позицией итератора. Например, в приведенном ниже фрагменте кода итератор проходит первый элемент связанного списка и добавляет элемент "Juliet" между первым и вторым элементами списка, как показано на рис. 2.7.
Listlterator iter = staff.listIterator();
iter.next();
iter.add("Juliet");
Если метод add вызывается несколько раз, то элементы просто добавляются в порядке указания перед текущей позицией итератора.
Если метод add используется с итератором, который возвращен методом listlterator и указывает на начало связанного списка, то добавляемый элемент становится первым элементом списка. А если итератор проходит последний элемент списка (т.е. метод hasNext возвращает значение false), то добавляемый элемент становится последним элементом списка. В связанном списке из п элементов существует п+1 позиций для добавления нового элемента, которые соответствуют п+1 возможным позициям итератора. Например, в связанном списке с тремя элементами А, В и С есть следующие четыре возможные позиции для добавления элемента (обозначенные как |).
|АВС
А|ВС
АВ|С
АВС|
Учтите, что прямая аналогия с поведением курсора здесь не совсем подходит, потому что метод remove работает не так, как клавиша <Backspace>. Сразу же после вызова метода next, метод remove удаляет элемент слева от итератора подобно клавише <Backspace>. Однако, после вызова метода previous удаляется элемент справа, а не слева. Кроме того, метод remove нельзя вызвать дважды подряд.
В отличие от метода add, который зависит лишь от позиции итератора, метод remove зависит от состояния итератора.
В заключение, метод set заменяет последний элемент, возвращаемый вызовом метода next или previous, новым элементом. Например, следующий фрагмент кода заменяет первый элемент списка новым значением.
Listlterator iter = list.listiterator();
Object oldValue = iter.next(); // возвращает первый элемент iter.set(newValue); // задает для первого элемента
// значение newValue
Если один итератор обходит коллекцию в тот момент, когда другой изменяет ее, то, как легко догадаться, в этом случае может возникнуть путаница. Предположим, что итератор указывает на позицию перед элементом, который был только что удален другим итератором. В таком случае первый итератор становится непригодным к дальнейшему использованию. Итераторы связанного списка разработаны так, чтобы определять подобные изменения списка. Если итератор обнаруживает, что коллекция была изменена другим итератором или методом самой коллекции, то в таком случае он генерирует исключительную ситуацию ConcurrentModificationException. Рассмотрим следующий фрагмент кода.
LinkedList list = ... ;
Listiterator iterl =
Listiterator iter2 = list.listiterator();
iterl.next();
iterl.remove();
iter2.next(); // генерирует исключительную ситуацию
// ConcurrentModificationException
Вызов метода iter2.next генерирует исключительную ситуацию ConcurrentModificationException, потому что итератор iter2 обнаружил, что список был изменен извне.
Во избежание возникновения исключительных ситуаций при параллельных изменениях списка достаточно соблюдать следующее простое правило: к контейнеру можно присоединить любое число итераторов при условии, что все они предназначены только для чтения. Или по-другому: к коллекции следует присоединять только один итератор, который может выполнять операции чтения и записи.
Для определения параллельных изменений контейнер следит за количеством операций добавления и удаления элементов. При этом каждый итератор отдельно подсчитывает число операций, за которые он отвечает. Перед выполнением каждого метода итератора производится проверка равенства количества изменений данного итератора и общего количества изменений для всей коллекции. Если равенство не соблюдается, то генерируется исключительная ситуация ConcurrentModificationException.
Этот очень удобный способ проверки представляет собой значительное усовершенствование коллекций по сравнению с принципиально ненадежными итераторами в структуре классов библиотеки STL языка C++. Учтите, что она не позволяет автоматически создавать коллекции, безопасные при работе с несколькими потоками. Вопросы многопоточного программирования коллекций рассматриваются далее в этой главе.
В определении параллельных изменений существует забавное исключение. Связанный список следит только за структурными изменениями списка, такими как добавление и удаление звеньев. При этом действия метода set не рассматриваются как структурные изменения. К связанному списку можно присоединить несколько итераторов, каждый из которых вызывает метод set для изменения содержимого существующих элементов. Это свойство требуется некоторым алгоритмам для работы с классом Collections, которые обсуждаются в этой главе.
Итак, мы ознакомились с фундаментальными методами класса LinkedList и итератором Listlterator, который используется для обхода элементов связанного списка в обоих направлениях, а также для добавления и удаления элементов.
Как показано в предыдущем разделе, в интерфейсе Collection объявлено множество полезных методов для работы со связанными списками. В основном они реализованы в классе Abstractcollection, родительском классе по отношению к классу LinkedList. Например, метод toString вызывает метод toString для всех элементов и создает одну длинную строку с перечислением элементов в формате [А, В, С]. Этот метод очень удобно использовать для отладки программ. Метод contains применяется для проверки наличия элемента в связанном списке. Например, вызов метода staff.contains ("Harry") возвращает значение true, если связанный список уже содержит строку "Harry". Однако, не предусмотрен метод, который возвращал бы итератор ее позиции. Если требуется произвести какие-либо действия с этим элементом, то необходимо самостоятельно создать цикл обхода всего списка.
В документации по платформе Java не рекомендуется добавлять ссылку на саму коллекцию. Иначе в виртуальной машине Java легко добиться переполнения стека вследствие ее некорректного использования, например так, как показано ниже.
LinkedList list = new LinkedList();
list.add(list); // добавить список в этот же список String contents = list.toString(); // ошибка, вызванная
// бесконечной рекурсией
Очевидно, что такая ситуация возникает крайне редко, но все же следует учитывать вероятность ее возникновения.
В библиотеке коллекций Java поддерживается набор методов, сомнительных с теоретической точки зрения. Связанные списки не могут обеспечить быстрый произвольный доступ, потому что для чтения re-го элемента необходимо начать с начала и пройти первые n-1 элементов. Более короткого пути в связанном списке просто не существует. По этой причине связанные списки обычно не используются там, где требуется предоставить быстрый доступ к элементам списка по значению целочисленного индекса.
Тем не менее, в классе LinkedList предусмотрен метод get, позволяющий обратиться к определенному элементу:
Object obj = list.get(n);
Конечно, он не очень эффективен, и если вам часто приходится использовать его, то возможно вы пытаетесь применить структуру данных, не совсем подходящую для настоящей задачи.
Этот метод имитирует произвольный доступ к элементам списка, поэтому его ни в коем случае не следует применять для работы со связанным списком.
for (int i = 0; i < list.size(); i++)
какие-то действия с методом list.get(i) ;
Дело в том, что показанный выше фрагмент кода чрезвычайно неэффективен. При каждом обращении к следующему элементу его поиск начинается с начала списка, потому что объект LinkedList не сохраняет информацию о предыдущей позиции.
Метод get слегка оптимизирован: если индекс элемента больше или равен size () /2, TOего поиск начинается с конца списка.
В интерфейсе итератора списка также содержится метод, возвращающий индекс текущей позиции. Фактически, таких методов два, потому что итераторы Java указывают на позиции между элементами. Первый метод next Index возвращает целочисленный индекс элемента, который будет возращен последующим вызовом next, a второй метод previouslndex — целочисленный индекс элемента, который будет возращен последующим вызовом previous. Разумеется, второй индекс (полученный с помощью метода previouslndex) на единицу меньше, чем первый (полученный с помощью метода next Index). Эти методы работают эффективно, так как итераторы следят за текущей позицией. В заключение, если имеется целочисленный индекс п, то вызов метода list.listIterator(п) возвращает итератор, указывающий на позицию перед элементом с индексом п. Таким образом, вызов next возвращает тот же элемент, что и list.get (n). Конечно, получение значения этого итератора выполняется не слишком эффективно.
При работе со связанным списком с небольшим количеством элементов не следует придавать большого значения эффективности методов get и set. Дело в том, что единственная причина использования связанного списка заключается в упрощении операций вставки и удаления элемента в середине списка. Если коллекция содержит немного элементов, то в таком случае можно воспользоваться массивом или коллекцией типаArrayList.
Итак, не рекомендуется пользоваться любыми методами, в которых для обозначения позиции в связанном списке применяется целочисленный индекс. Для организации произвольного доступа к элементам коллекции следует использовать массив или коллекцию типа ArrayList, но никак не связанный список.
Применение связанного списка демонстрируется в программе, код которой приведен в листинге 2.1. В ней создаются два списка, затем они объединяются, потом из второго списка удаляется каждый второй элемент, а в заключение проверяется работа метода removeAll. Проследите за работой этой программы, уделяя особое внимание итераторам. С этой целью удобно использовать диаграммы расположения итераторов, как показано ниже.
|ACE IBDFG
А|СЕIBDFG
AB|СЕ В|DFG
Обратите внимание, что вызов метода
System.out.printin(a);
выводит все элементы из связанного списка а.
Листинг 2.1; LinkedListTest.java
/**
(aversion 1.00 1999-07-07
@author Cay Horstmann
*/
import java.util.*;
/**
Демонстрация действий со связанными списками */ public class LinkedListTest
{
public static void main(String[] args)
{
List a = new LinkedList();
a.add("Angela");
a.add("Carl");i
a.add("Erica");
List b = new LinkedList(); b.add("Bob");
b.add("Doug");
b.add("Frances");
b.add("Gloria");
// Включение слов из списка Id в список а
Listiterator alter = a.listiterator();
Iterator biter = b.iterator();
while (biter.hasNext()) {
if (alter.hasNext()) alter.next();
alter.add(biter.next());
System.out.println(a);
// Удаление каждого второго слова из списка b
biter = b.iterator(); while (biter.hasNext())
{
biter. next (); // Пропуск одного элемента if (biter.hasNext()) {
biter.next(); // Пропуск следующего элемента biter.remove(); // Удаление этого элемента
) }
System.out.printin(b);
// Коллективная операция: удаление слов,
// которые присутствуют в списке b, // из списка а
a.removeAll(b);
System.out.printin(a);
)
}
F^f java. util. List
Метод Listlterator listlterator () возвращает итератор списка.
Метод Listlterator listlterator (int index) возвращает итератор списка, причем первый вызов его метода next возвращает элемент с заданным индексом index.
Параметры:
indexиндекс следующего читаемого элемента
Метод void add(int index, Object element) добавляет элемент в указаннойпозиции.
Параметры:
indexпозиция-индекс нового элемента
element добавляемый элемент
Метод void addAll(int index, Collection elements) добавляет все элементы коллекции в указанной позиции.
Параметры:
indexпозиция-индекс первого нового элемента
elements добавляемые элементы
Метод Obj ect remove (int index) удаляет элемент в указанной позиции и возвращает его.
Параметры:
indexпозиция-индекс удаляемого элемента
Метод Object set(int index, Object element) заменяет элемент в указанной позиции новым элементом и возвращает предыдущее значение. Параметры:
indexпозиция-индекс заменяемого элемента
element новый элемент
Метод int indexOf (Object element) возвращает позицию первого вхождения элемента, равного указанному, или значение -1 , если он отсутствует.
Параметры:
elementискомый элемент
Метод int lastlndexOf (Object element) возвращает позицию последнего вхождения элемента, равного указанному, или значение -1 , если он отсутствует.
Параметры:
elementискомый элемент
java.util.Listlterator
Метод void add(Object element) добавляет элемент перед текущей позицией.
Параметры:
element добавляемый элемент
Метод void set (Object element) заменяет последний элемент, прочитанный методом next или previous, новым значением. Генерирует исключительную ситуацию IllegalStateException, если структура списка изменилась после вызова метода next или метода previous.
Параметры:
element новый элемент
Метод boolean hasPrevious () возвращает значение true, если при обходе списка в обратном направлении можно прочитать еще хотя бы один элемент.
Метод Obj ect previous () возвращает предыдущий объект. Генерирует исключительную ситуацию NoSuchElementException, если достигнуто начало списка.
Метод int nextIndex () возвращает индекс элемента, который будет возвращен при последующем вызове метода next.
Метод int previous Index () возвращает индекс элемента, который будет возвращен при последующем вызове метода previous.
java.util.LinkedList
Конструктор LinkedList () создает пустой связанный список.
Конструктор LinkedList (Collection elements) создает связанный список и добавляет в него все элементы указанной коллекции.
Параметры:
elements коллекция с добавляемыми элементами
Методvoid addFirst (Object element) добавляет элемент в начале списка.
Параметры:
elementдобавляемый элемент
Метод void addLast (Object element) добавляет элемент в конце списка.
Параметры: elementдобавляемый элемент
Метод Object getFirst() возвращает первый (начальный) элемент списка.
Метод Object getLast () возвращает последний (конечный) элемент списка.
Метод Object removeFirst () удаляет первый (начальный) элемент списка и возвращает его.
Метод Object removeLast () удаляет последний (конечный) элемент списка и возвращает его.
Списки-массивы
В предыдущем разделе рассматривался интерфейс List и реализующий его класс LinkedList. Интерфейс List описывает упорядоченную коллекцию, в которой большое значение имеет расположение элемента. Для обращения к элементам существует два принципиально разных способа: последовательный доступ с помощью итератора и произвольный доступ с помощью методов get и set. Последний вариант не подходит для связанных списков, но очень эффективен при работе с массивами. Библиотека коллекций co- держит уже знакомый нам список-массив ArrayList, который реализует интерфейс- список Listи инкапсулирует динамический выделяемый массив объектов Obj ect [ ].
Опытные программисты на языке Java, вероятно, уже использовали класс Vector везде, где требовалось использовать динамический массив. Зачем же вместо него применять список-массивArrayList? По одной простой причине: все методы классаVector синхронизированы для предоставления безопасного доступа к объектуVector из нескольких потоков. Но если доступ к такому объекту осуществляется только из одного потока (достаточно распространенный случай), то программа затрачивает достаточно много времени на обеспечение синхронизации. Методы класса ArrayList не синхронизированы, поэтому рекомендуется использовать список-массив ArrayList вместо классаVectorвезде, где не требуется обеспечивать синхронизацию.
Хэш-множества
Связанные списки и массивы позволяют указывать порядок расположения элементов. Но для поиска определенного элемента с неизвестным расположением нужно выполнять обход всех элементов до тех пор, пока не будет найден нужный. В коллекции с большим количеством элементов для такой операции потребуется очень много времени. Если по-
рядок элементов не имеет значения, то можно воспользоваться структурой, в которой поиск осуществляется значительно быстрее. Ее недостаток заключается в том, что она не позволяет управлять порядком расположения элементов, потому что элементы хранятся в том порядке, который удобен для организации поиска в такой структуре.
Хорошо известной структурой для быстрого поиска объектов является хэш-таблица (hash table). В ней для каждого объекта вычисляется целое число, называемые хэш- кодом (hash code). Способы вычисления таких кодов рассматриваются в следующем разделе. Важно отметить, что хэш-коды подсчитываются достаточно быстро, и подсчет зависит только от состояния объекта, для которого вычисляется код, и не зависит от других объектов таблицы.
Как показано на рис. 2.8, хэш-таблица представляет собой массив связанных списков, каждый из которых называется ячейкой (bucket). Для вставки объекта в таблицу нужно вычислить его хэш-код и найти остаток от его деления на общее число ячеек. Полученное в результате число представляет собой номер ячейки, где хранится элемент. Например, если объект имеет хэш-код 345 и всего имеется 101 ячейка, значит, объект размещен в ячейке 42 (поскольку остаток от деления 345/101 равен 42). Если эта ячейка пуста, то достаточно просто поместить элемент в нее. Конечно же, неизбежны случаи, когда в ячейке уже есть какие-то элементы. Такая ситуация называет хэш-конфликтом (hash collision). Тогда необходимо сравнить новый объект со всеми, имеющимися в ячейке, и проверить, содержится ли он в ней. При условии, что хэш-коды распределены в большой степени случайно, а число ячеек достаточно велико, потребуется выполнить лишь несколько сравнений.
Рис. 2.8. Хэш-таблица
Для определения производительности хэш-таблицы можно использовать счетчик ячеек. Счетчик показывает, сколько ячеек используется для хранения объектов с одинаковыми хэш-кодами. Если хэш-таблица содержит довольно много элементов, то вероятность хэш-конфликтов возрастает, а производительность поиска элементов падает.
Если примерно известно, сколько элементов будет храниться в таблице, необходимо установить исходное значение счетчика ячеек равным 150% от ожидаемого числа элементов. Некоторые исследователи считают, что размер хэш-таблицы должен быть простым числом, что позволяет избежать кластеризации ключей. Хотя для этого утверждения нет убедительного доказательства, опыт показывает,
что такая методика не будет лишней. Например, для хранения 100 записей следует задать для счетчика ячеек значение 151.
Конечно же, предполагаемое количество хранимых элементов заранее известно далеко не всегда, кроме того, предполагаемое значение может оказаться слишком малым. Если хэш-таблицапереполнена,длянеенеобходимо заново сгенерировать хэш-коды (rehash), дляче- го нужно создать новую таблицу с большим числом ячеек, вставить в нее все элементы из предыдущей таблицы и удалить последнюю. В языке Java предусмотрен специальный индикатор, указывающий, когда следует производить эту операцию. Он называется коэффициентом загруженности (hadfacto?). Например, если он равен 0,75 (принимаемое по умолчанию значение) и хэш-таблица заполнена более, чем на 75%, то для этой хэш- таблицы автоматически будут сгенерированы новые хэш-коды. При этом в новой таблице будет создано в 2 раза больше ячеек. Для большинства приложений оптимальным является принимаемое поумолчанию значение коэффициента загруженности.
Хэш-таблицы используются для реализации нескольких важных структур данных. Простейшей из них является множество (set), которое представляет собой коллекцию элементов, не содержащую повторяющихся значений. Метод add множества вначале производит поиск добавляемого объекта и лишь при его отсутствии вставляет его.
Библиотека коллекций Java содержит класс HashSet, реализующий множество на основе хэш-таблицы. На момент написания книги конструктор HashSet создавал по умолчанию хэш-таблицу со 101 ячейкой и коэффициентом загруженности 0,75. В последующих версиях платформы эти значения могут измениться, но их можно указать явно, воспользовавшись следующими конструкторами.
HashSet(int initialCapacity)
HashSet(int initialCapacity, float loadFactor)
Для добавления элементов в хэш-множество применяется метод add, а метод contains переопределен для ускорения поиска элемента, который уже присутствует в множестве. Метод contains проверяет только элементы из одной ячейки, а не из всей коллекции.
Итератор хэш-множества по очереди обходит все ячейки. Поскольку при создании ключей элементы добавляются не упорядоченно, то их обход происходит в случайном порядке. Хэш-множество применимо лишь тогда, когда порядок расположения элементов в коллекции не имеет большого значения.
Программа, код которой приведен в листинге 2.2, считывает слова из потока System. in, добавляет их в множество, а затем выводит все элементы множества. В качестве входного потока этой программы можно использовать любой текст, напри- мер,направитьтекстизкнигиAlice in Wonderland (Агиса в Стране чудес), которуюможно найти на Web-узле www. gutenberg. net. Для этого в режиме командной строки нужно запустить следующую команду:
j ava SetTest < alice30.txt
Программа считает все слова из входного потока и вставит их в хэш-множество. Затем она находит уникальные слова и выводит их в произвольном порядке. (В книге "Алиса в Стране чудес" содержится 5909 уникальных слов, включая примечание об авторских правах.)
Рис. 2.9. Связанная хэш-таблицаРис. 2.9. Связанная хэш-таблица
Рис. 2.9. Связанная хэш-таблица
Рис. 2.9. Связанная хэш-таблица
DDВ пакет SDK версии 1.4 добавлен класс LinkedHashSet, в котором учитывается порядок вставки элементов. Итератор этого класса обращается к элементам именно в том порядке, в котором они были добавлены в эту коллекцию. Такой класс представляет собой упорядоченную коллекцию с возможностями быстрого поиска. Конечно же, за эти дополнительные возможности приходится расплачиваться обслуживанием организации элементов в ячейках в виде дважды связанного списка, как показано на рис. 2.9.
D
D
Чтобы увидеть разницу между коллекциями Hashset и LinkedHashSet, достаточно заменить первый класс на второй в листинге 2.2. В последнем случае слова будут выводиться в порядке их появления. (Для слов, встречающихся несколько раз, имеет значение только первое включение его в коллекцию.)
Листинг 2.2. SetTaat.java
Sversion 1.00 1999-07-07 @author Cay Horstmann
*/
import java.util.*;
import j
Пример использования множества для вывода всех уникальных слов из потока System.in
*/
public class SetTest
{
Set words = new HashSet(59999);Set words = new HashSet(59999);public static void main(String[] args) {
Set words = new HashSet(59999);
Set words = new HashSet(59999);
// Попробуйте использовать здесь коллекции
// HashSet, LinkedHashSet или TreeSet
long totalTime =
try
{,
Bu£EeredReader in=new
BufEeredReader(new InputStreamReader(System.in)) ;
String line;
while ((line = in.readLine()) != null)
{
StringTokenizer tokenizer = new StringTokenizer(line); while (tokenizer.hasMoreTokens())
{
String word = tokenizer.nextToken();
long callTime =
words.add(word);
callTime = System.currentTimeMillis() — callTime; totalTime += callTime;
) )
catch (IOException e)
{
System.out.printin("Error"+e) ;
Iterator iter = words.i terator();
while (iter.hasNext())
;
System.out.printin(words.size()
+ " distinct words. " + totalTime + " milliseconds. ") ;
java.util.HashSet
Конструктор HashSet () создает пустое хэш-множество.
Конструктор HashSet (Collection elements) создает хэш-множество и добавляет в него все элементы указанной коллекции.
Параметры-:
elements коллекция с добавляемыми элементами
Конструктор HashSet (int initialCapacity) создает пустое хэш- множество заданной емкости.
Параметры:
initialCapacityисходное число ячеек
Параметры: initialCapacity loadFactorПараметры: initialCapacity loadFactorКонструктор HashSet(int initialCapacity, float loadFactor) создает пустое хэш-множество заданной емкости с указанным коэффициентом загруженности.
Параметры: initialCapacity loadFactor
Параметры: initialCapacity loadFactor
исходное число ячеек
число между 0,0 и 1,0, определяющее процент заполнения таблицы, при котором она будет автоматически расширена с одновременным генерированием новых хэш-кодов
••••••java.util.LinkedHashSet
•
•
•
•
•
•
Конструктор LinkedHashSet () создает пустое связанное хэш-множество.
Конструктор LinkedHashSet (Collection elements) создает связанное хэш-множество и добавляет в него все элементы указанной коллекции.
Параметры:
elements коллекция с добавляемыми элементами
Конструктор LinkedHashSet (int initialCapacity) создает пустое связанное хэш-множество заданной емкости.
Параметры:
initialCapacityисходное число ячеек
Параметры: initialCapacity loadFactorПараметры: initialCapacity loadFactorКонструктор LinkedHashSet(int initialCapacity, float loadFactor) создает пустое связанное хэш-множество заданной емкости с указанным коэффициентом загруженности.
Параметры: initialCapacity loadFactor
Параметры: initialCapacity loadFactor
исходное число ячеек
число между 0,0 и 1,0, определяющее процент заполнения таблицы, при котором она будет автоматически расширена с одновременным генерированием новых хэш-кодов
Хэш-функции
В хэш-множество можно вставлять строки, так как класс String имеет метод hashCode, который подсчитывает хэш-код строки. Хэш-код строки представляет собой целое число, которое вычисляется на основании содержимого этой строки. Несколько примеров хэш-кодов, подсчитанных с помощью метода hashCode класса String, приведены в табл. 2.1.
Таблица2.1.Хэш-коды,вычисленныефункциейhashCode
СтрокаХэш-код
Hello140207504
Harry140013338
Hacker884756206
Метод hashCode определен в классе Object, поэтому каждый объект имеет хэш-код, который по умолчанию вычисляется на основании его адреса в памяти. В общем случае предлагаемая по умолчанию хэш-функция не всегда полезна, так как объекты с одинаковым содержимым могут иметь различные хэш-коды. Рассмотрим следующий пример.
String s = "Ok";
StringBuffer sb = new StringBuffer(s);
System.out.println(s.hashCode () + " " + sb.hashcode Ob-
String t = new String("Ok");
StringBuffer tb = new StringBuffer(t);
System.out.printin(t.hashcode() + " " + tb.hashCode()) ;
Результат выполнения этого кода приведен в табл. 2.2.
Таблица 2.2. Хэш-коды объектов с одинаковым содержимым
ОбъектХэш-код
s3030
sb20526976
t3030
tb20527144
Обратите внимание, что строки s и t имеют одинаковые хэш-значения, поскольку для строк они вычисляются на основании содержимого строки. А строковые буферы sb и tb имеют различные хэш-значения, так как для класса StringBuffer специальная хэш-функция не определена и для них применяется хэш-функция по умолчанию класса Obj ect, которая определяет хэш-код по адресу объекта в памяти.
Для объектов, вставляемых в хэш-таблицу, необходимо всегда предоставлять определение метода hashCode. Он должен всегда возвращать целое число, которое может быть и отрицательным. В хэш-таблице это значение преобразуется путем деления на значение счетчика ячеек с вычислением остатка.
Предположим, например, что имеется класс Item для хранения информации о товарах в виде строки-описания и идентификационного номера.
anltem = new Item("Toaster", 49954);
Если из таких элементов требуется создать хэш-множество, то необходимо определить хэш-код для каждого из них. Например:
class Item {
public int hashcode()
{
return 13 * description..hashcode() + 17 * partNumber;
}
private String description;
private int partNumber;
}
На практике, если идентификационный номер товара уникальным образом идентифицирует элемент, вычислять хэш-код по описанию нет необходимости.
Более того, необходимо также убедиться в том, что метод equals определен правильно. В классе Object метод equals по умолчанию проверяет лишь идентичность двух объектов. Если его не переопределить, то каждый новый объект, добавляемый в таблицу, будетрассматриваться какуникальный.
Поэтому метод equals нужно переопределить так, чтобы он проверял равенство содержимого двух объектов.
classItem (
public boolean equals(Object other) {
if (other != null && getClass() == (
Item otherltem = (Item)other;
return description.equals(otherItem.description)
&& partNumber == otherltem.partNumber;
)
else
return false;
Определения методов equals и hashcode должны быть совместимы: если сравнение X. equals (у) возвращает значение true, то метод x.hashCode() должен возвращать то же значение, что и метод у.hashcode ().
java.lang.Obj ect
Метод boolean equals(Object obj) проверяет равенство двух объектов. Если они равны, возвращает значение true, в противном случае — значение false.
Параметры:
objобъект, сравниваемый с первым объектом (может быть равен
null и в этом случае этот метод должен возвращать значение false)
Методint hashcode () возвращает хэш-код объекта. Он может быть любым целым числом (положительным или отрицательным). Для равных объектов метод должен возвращать одинаковые хэш-коды.
Древовидные множества
Класс древовидного множества TreeSet аналогичен хэш-множеству за исключе- ниемтого,чтоонявляется сортированной коллекцией (sorted collection). Элементымогут добавлятьсяв древовидное множество (иликратко дерево) впроизвольномпорядке.При итеративном обходе коллекции все значения возвращаются в автоматически сортированном порядке. Предположим, что в древовидное множество добавляются три строки, а затем выполняется их обход с выводом результатов обхода.
TreeSet sorter = new TreeSet();
sorter.add("Bob");
sorter.add("Angela");
sorter.add("Carl");
Iterator iter = sorter.iterator();
while (iter.hasNext()) System.println(iter.next()) ;
В таком случае полученные значения выводятся в отсортированном порядке: Angela Bob Carl. Как следует из названия этого класса, сортировка происходит за счет использования древовидной структуры данных. (В текущей реализации использу- етсятакназываемаяструктуракрасно-черногодерева (red-black tree). Болееподробноеописа-
ние этой структуры можно найти в книге Introduction to Algorithms Томаса Кормена (Thomas Gormen), Чарльза Лейзерсона (Charles Leiserson) и Рональда Райвеста (Ronald Rivest) [The MIT Press 1990].) При добавлении к дереву элемент сразу же размещается в нужной позиции. Поэтому итератор всегда обходит элементы в порядке сортировки.
Вставка элемента в дерево производится медленнее, чем в хэш-таблицу, но, все же, гораздо быстрее, чем в нужную позицию массива или связанного списка. Если дерево содержит п элементов, то для нахождения нужной позиции требуется в среднем log2H операций сравнения. Например, если дерево уже содержит 1000 элементов, то для вставки нового элемента потребуется 10 операций.
Как показано в табл. 2.3, вставка элементов в дерево TreeSet выполняется несколько медленнее, чем в хэш-таблицу HashSet, но дерево TreeSet автоматически сортирует вставляемые элементы.
Таблица 2.3. Сравнительный анализ операции вставки элементов в дерево и хэш-множество
Документ
ОбщееЧислоHashSet TreeSet
число слов различных слов
Alice in Wonderland
2819559095 сек7 сек
The Count of Monte Cristo
4663003754575 сек98 сек
java.util.TreeSet
Конструктор TreeSet () создает пустое древовидное множество.
Конструктор TreeSet (Collection elements) создает древовидное множество и добавляет в него элементы указанной коллекции.
Параметры:
elements коллекция с добавляемыми элементами
Сравнение объектов
Как класс TreeSet получает информацию о порядке сортировки элементов? По умолчанию предполагается, что все добавляемые элементы реализуют интерфейс Comparable, в котором определен единственный метод сравнения.
int compareTo(Object other)
Вызов метода а . compareTo (b) возвращает значение 0, если а и b равны; отрицательное целое значение — если а предшествует b в порядке сортировки; или положительное — если а следует после b, Точное значение не важно, важен только его знак (>0, 0 или <0). Интерфейс Comparable реализован в нескольких стандартных классах платформы Java. Например, в классе String метод compareTo сравнивает две строки в стандартном словарном порядке, который иногда называют лексикографическим порядком {lexicographic order).
Для добавления в отсортированное множество собственных пользовательских объектов необходимо определить свой порядок сортировки, реализуя интерфейс Comparable. Для класса Ob j ect метод compareTo по умолчанию не реализован.
Объекты-товары класса Item можно сортировать, например, по их идентификационному номеру.
class Item Implements Comparable
{
public int compareTo(Ob ject other)
{
Item otherltem =
return partNumber — otherltem.partNumber; }
}
Обратите внимание, что явный аргумент метода compareTo имеет тип Object, a не Comparable. Если объект не принадлежит к заданному типу, то его метод compareTo генерирует исключительную ситуацию ClassCastException. (Методы compareTo из стандартной библиотеки ведут себя таким же образом, когда в них указываются неверные типы аргументов.)
При сравнении двух положительных чисел, как в приведенном примере, достаточно вернуть их разность. Она будет отрицательной, если первый элемент предшествует второму в порядке сортировки, равны нулю, если номера элементов идентичны, или положительной в ином случае.
Этот прием применим только для чисел из достаточно небольшого интервала. Если х — большое положительное число, а у — большое (по модулю) отрицательное, тогда их разность х - у может привести к переполнению.
Применение интерфейса Comparable для определения порядка сортировки имеет очевидные ограничения. Интерфейс можно реализовать только один раз. Но, что делать в случае, когда в одной коллекции нужно отсортировать часть элементов по номерам, а в другой— по описаниям? Более того, как сортировать объекты класса, в котором не реализован интерфейс Comparable?
В таких ситуациях для дерева можно создать другой метод сравнения, передавая объект Comparator конструктору TreeSet. Интерфейс Comparator имеет единственный метод с двумя явно заданными параметрами.
int compare(Object a, Object b)
Как и метод compareTo, метод compare возвращает отрицательное целое число, если а предшествует b; ноль — если они идентичны; и положительное целое число — в ином случае.
Для сортировки элементов по описанию достаточно определить класс, реализующий интерфейс Comparator:
class Itemcomparator implements Comparator {
public int compare(Object a, Object b)
{
Item itemA =
Item items = (Item)b;
String descrA = itemA.getDescription()
String descrB = itemB.getDescription() return descrA.compareTo(descrB);
}
}
Затем объект этого класса нужно передать конструктору дерева.
= new
TreeSet= new
Если дерево создается с помощью такого объекта-компаратора (comparator), то этот объект будет использован в нем всякий раз, когда потребуется сравнить два элемента.
Обратите внимание, что компаратор не содержит данных. Он используется лишь для хранения метода, производящего сравнение. Такие объекты иногда называют функциональными объектамиobject).
Функциональные объекты зачастую определяются "на лету", как экземпляры безымянных внутренних классов.
TreeSet sortByDescription = new TreeSet (new
Comparator()
{ public int(Object a. Object b)
{
itemA
Item items »
String descrA
String- itemB.getDescription();
return
1)
Благодаря компараторам, можно сортировать элементы в любом требуемом порядке.
В некоторых случаях компараторы содержат поля экземпляров. Например, в компараторе может храниться флажок для указания порядка сортировки по возрастанию или по убыванию.
После ознакомления с табл. 2.3 может возникнуть вопрос, не следует ли всегда использовать дерево вместо хэш-таблицы. Ведь в конце концов, для вставки элементов в дерево почти не требуется дополнительных затрат времени, но при этом происходит автоматическая сортировка. Ответ зависит от типа данных, которые предполагается хранить в коллекции. Если данные не нужно сортировать, то нет смысла применять для нихКроме того, и это болееданные трудно сор
тировать. Предположим, что в коллекции содержится набор прямоугольников. Как их сортировать? По площади? При этом два разных прямоугольника могут иметь одинаковую площадь. Тогда второй не будет добавлен в множество. Порядок сортировки в дереве должен быть строгим (total ordering), т.е. каждые два элемента должны обладать возможностью сравнения и нулевой результат должен получаться лишь тогда, когда элементы равны. Для прямоугольников можно задать какой-то другой порядок сортировки, например, лексикографический порядок для их координат, но он не является естественным и простым для программирования. По сравнению с этими трудностями хэш- функция, как правило, определяется гораздо проще. Ей требуется только вычислить код объекта, тогда как функции сравнения должны точно различать объекты.
В программе, код которой приведен в листинге 2.3, создаются два дерева с объектами-товарами Item. Первое дерево сортируется по идентификационному номеру товара, т.е. с использованием предлагаемого по умолчанию порядка сортировки объектов Item. Второе дерево сортируется по описанию с использованием специального компаратора.
Листинг 2.3. TreeSetTest.java
/**
©version 1.00 1999-07-07
©author Cay Horstmann
*/
import java.util. *;
/**
Пример программы с сортировкой товаров по их описанию
*/
public class TreeSetTest {
public static void main(String!] args) {
SortedSet parts = new TreeSet();
parts.add(new Item("Toaster", 1234));
parts.add(new Item("Widget”, 4562));
parts.add(new Item("Modem", 9912));
System.out.printIn(parts) ;
SortedSet sortByDescription = new TreeSet(new
Comparator() {
public int compare(Object a, Object b) {
Item itemA =
Item itemB =
String descrA = itemA.getDescription() "• String descrB = itemB.getDescription(); return descrA.compareTo(descrB);
)
});■
sortByDescription.addAll(parts);
System.out.printIn(sortByDescription);
} }
/**
Товар с описанием и идентификационным кодом
*/
class Item implements Comparable {
/**
Создание товара
©param aDescription the item's description @param aPartNumber the item's part number
*/
public Item(String aDescription, int aPartNumber) {
description = aDescription; partNumber = aPartNumber;
}
/**
Возвращает описание товара
©return the description
*/
public String getDescription() {
return description;
public String toString(){return "[descripion=" + description+ ", partNumber="+partNumber+ "]";public booleanother){public String toString(){return "[descripion=" + description+ ", partNumber="+partNumber+ "]";public booleanother){if {if {}}(getClassO == Item otherltem = returnpartNumber==(getClassO == Item otherltem = returnpartNumber==}}else returnfalse;else returnfalse;
public String toString()
{
return "[descripion=" + description
+ ", partNumber="+partNumber+ "]";
public booleanother)
{
public String toString()
{
return "[descripion=" + description
+ ", partNumber="+partNumber+ "]";
public booleanother)
{
if {
if {
}
}
(getClassO == Item otherltem = return
partNumber==
(getClassO == Item otherltem = return
partNumber==
}
}
else returnfalse;
else returnfalse;
public int{return13*+17*,-}public intother){Item otherltem =return partNumber —private String description;private int partNumber;public int{return13*+17*,-}public intother){Item otherltem =return partNumber —private String description;private int partNumber;bbобъекты для сравненияобъекты для сравнения
public int
{
return13*+17*,-
}
public intother)
{
Item otherltem =
return partNumber —
private String description;
private int partNumber;
public int
{
return13*+17*,-
}
public intother)
{
Item otherltem =
return partNumber —
private String description;
private int partNumber;
b
b
объекты для сравнения
объекты для сравнения
lang. Comparable
Метод int compareToother) сравнивает данный объект с
другим и возвращает отрицательное число, если первый предшествует второму; ноль — если объекты идентичны; и положительное значение — если первый объект следует за вторым.
Параметры:
otherобъект для сравнения
java.util.Comparator
Метод int comparea, Object b) сравнивает два объекта и
возвращает отрицательное число, если аноль— если они
идентичны; и положительное число — если а следует за
Параметры:
••java.util.SortedSet
•
•
Метод Comparator comparator () возвращает компаратор, используемый для сортировки элементов или значение null, если элементы сравниваются с помощью метода compareTo интерфейса Comparable.
Метод Object first() возвращает наименьший элемент отсортированного множества.
Метод Object last() возвращает наибольший элемент отсортированного множества.
java.util.TreeSet
Конструктор TreeSet (Comparator с) создает древовидное множество и использует указанный компаратор для сортировки элементов.
Параметры:
скомпаратор, используемый для сортировки
Конструктор TreeSet (SortedSet elements) создает древовидное множество, добавляет в него все элементы из указанного отсортированного множества и использует компаратор из указанного отсортированного множества.
Параметры:
elements отсортированное множество, содержащее добавляемые элементы и компаратор
Карты
Множество представляет собой коллекцию, в которой можно быстро найти существующий в ней элемент. Однако, для этого нужно иметь точную копию требуемого элемента. Это не очень распространенный вид поиска, потому что обычно известна некоторая информация (ключ), по которой можно найти соответствующий элемент. Специально для достижения этой цели предназначена структура данных карта {тар), в которой хранятся пары ключ/значение. Каждое значение можно найти по его ключу. Например, в таблице могут храниться записи с информацией о сотрудниках, где ключами являются идентификационные номера сотрудников, а значениями — объекты Employee.
В библиотеке Java предусмотрено две основных реализации карт: хэш-карта HashMap и карта-дерево TreeMap. В хэш-карте ключи расположены случайным образом, а в карте-дереве— в строгом порядке в виде дерева поиска. Хэш-функция или функция сравнения применяется только для ключей, а сами значения, соответствующие этим ключам, не хэшируются и не сравниваются.
Какую же их карт следует выбрать? Как и для множеств, хэширование несколько быстрее, поэтому его рекомендуется использовать там, где порядок ключей не имеет значения.
Вот как создается хэш-карта для хранения информации о сотрудниках.
HashMap staff = new HashMap () ;
Employee harry = new Employee("Harry Hacker");
staff.put("987-98-999 6" , harry);
При добавлении объекта в карту должен быть указан и его ключ. В данном случае ключом является строка, а соответствующим значением — объект Employee.
Чтобы обратиться к объекту, нужно воспользоваться ключом.
String s = "987-98-9996" ;
е = (Employee)staff.get(s); // читает запись harry
Если данных с указанным ключом в карте нет, метод get возвращает значение null.
Ключи должны быть уникальными: нельзя сохранить два значения с одинаковым ключом. Если вызвать метод put дважды с одинаковым ключом, то второе значение просто заменит первое. Фактически, метод put возвращает предыдущее значение, хранимое с указанным ключом. (Это довольно полезное свойство: если метод put возвращает значение, отличное от значения null, то это значит, что существующая запись была заменена новой.)
Метод remove () удаляет элемент из карты, а метод size() возвращает число элементов карты.
В архитектуре коллекций платформы Java карта сама по себе не рассматривается как коллекция. (В других архитектурах структур данных карта рассматривается как коллекция парили коллекция значений, индексируемых ключами.) Однако, в библиотеке коллекций Java предусмотрено использование представления (view) карты, которое реализует интерфейс Collection или один из его дочерних интерфейсов.
Существует три типа представлений: в виде множества ключей, коллекции значений (которая не является множеством) или множества пар ключ/значение. Ключи и пары ключ/значение формируют множество, так как в карте может присутствовать только один уникальный экземпляр объекта-ключа. Перечисленные ниже методы возвращают эти три типа представлений карты.
Set keyset() // возвращает множество ключей
Collection values() // возвращает множество значений
Set entrySetO// возвращает множество пар ключ/значение
(Элементы последнего множества пар ключ/значение являются объектами внутреннего классаMap. Entry.)
Обратите внимание, что множество ключей не является объектом HashSet или TreeSet, но представляет собой объект некоторого другого класса, реализующего интерфейс Set. Интерфейс Set и его предназначение подробно рассматриваются в следующем разделе. Здесь же кратко укажем, что он расширяет интерфейс Collection, причем, как будет показано ниже, не содержит каких-либо новых методов. Поэтому использовать его можно точно так же, как и интерфейс Collection.
Например, можно перечислить все ключи карты.
Set keys = map.keyset();
Iterator iter = keys.iterator();
while (iter.hasNext()) {
Object key = iter.next();
какие-то действия с ключом }
Если нужно одновременно просматривать ключи и значения, то можно избежать необходимости поиска значений, просто перечисляя все записи. Для этого можно использовать следующий шаблонный код.
Set entries = staff.entrySet();
Iterator iter = entries.iterator(); while (iter.hasNext()) (
Map.Entry entry = (Map.Entry)iter.next();
Object key = entry.getKey();
Object value = entry.getValue();
какие-то действия с ключом и значением
}
Метод итератора remove удаляет из карты ключ и связанное с ним значение. Учтите, что нельзя вставить элемент в представление карты в виде множества ключей, поскольку не имеет смысла включать ключ без указания связанного с ним значения. В таком случае метод add генерирует исключительную ситуацию UnsupportedOperation- Exception. Аналогичные ограничения применяются и к представлению карты в виде множества пар ключ/значение, хотя концептуально вставка нового элемента в это множество имеет смысл.
Используемый в предыдущих версиях платформы Java класс Hashtable (о котором более подробно рассказано далее в этой главе) содержит методы, возвращающие пронумерованные объекты - нумераторы (классический аналог итераторов), которые выполняют обход ключей и значений. Однако, представления коллекции обладают значительно большими возможностями, так как они позволяют работать со всеми ключами или значениями одновременно.
Применение карты иллюстрируется в примере, код которого приводится в листинге 2.4. Сначала в карту добавляются пары ключ/значение. Затем один ключ удаляется, что приводит к удалению соответствующего ему значения. Затем изменяется значение, связанное с другим ключом, а для его поиска вызывается метод get. В заключение, выводится содержимое всего множества.
Листинг 2.4. MapTest.java
/**
©version 1.00 1999-07-07
Sauthor Cay Horstmann
*/
import java.util.*;
/**
Пример использования карты с ключом типа
String и значением типа Employee
*/
public class MapTest
{
public static void main(String[] args)
{
Map staff = new LinkedHashMap(101,0.7 5F,true);
staff.put("144-25-5464", new Employee("Angela Hung"));
staff.put("567-24-2546", new Employee("HarryHacker"));
staff.put("157-62-7935", new Employee("Gary Cooper"));
staff.put("456-62-5527", new Employee("Francesca Cruz"));
// Вывод всех элементов карты
System.out.printIn(staff) ;
// Удаление элемента карты
staff.remove("567-24-2546");
// Замена элемента карты
staff.put("456-62-5527", new Employee("FrancescaMiller"));
// Поиск значения
System.out.printIn(staff.get("157-62-7935")) ;
// Обход и вывод всех значений
Set entries = staff.entrySet();
Iterator iter = entries.iterator(); while (iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
Object key = entry.getKey();
Object value = entry.getValue();
System.out.printin("key=" + key + ", value=" + value); ) } }
/**
Минимальная версия класса-сотрудника для тестирования */ class Employee {
/,*
Создание сотрудника с зарплатой $0
Sparam n the employee name
*/
public Employee(String n) {
name = П;
j salary = 0;
public String toString() {
return "[name=" + name + ", salary=" + salary + "]" ; }
/**
Указание новой зарплаты для сотрудника @param s the new salary.
*/
public void setsalary(double s) {
salary = s;
private String name;
}}private double salary;
}
}
ja.va.util. Map
• Метод Object get (Object key) возвращает объект, соответствующий указанному ключу или значение null, если карта не содержит указанный ключ.
Параметры:
keyключ, используемый для обращения к объекту (может иметь
значение null)
МетодObject put (Object key, Object value) добавляет ключ и значение в карту. Если такой ключ уже имеется, то новый объект заменяет предыдущий, связанный с этим ключом. Этот метод возвращает предыдущее значение объекта или значение null,если ключ не содержался в карте ранее.
Параметры:
keyключ, используемый для обращения к объекту (может иметь
значение null)
valueобъект, связанный с ключом (может иметь значение null)
Метод void putAll (Map entries) добавляет все элементы заданной карты в текущую.
Параметры:
entriesкарта, содержащая добавляемые элементы
Метод boolean containsKey (Object key) возвращает значение true, если в карте имеется указанный ключ.
Параметры:
keyключ для поиска
Метод boolean containsValue (Object value) возвращает значение true, если в карте имеется указанное значение.
Параметры:
valueзначение для поиска
Метод Set entrySet () возвращает представление карты в виде множества объектов Map.Entry, т.е. пар ключ/значение. Из этого представления можно удалять элементы, при этом они удаляются и из карты, но добавлять их нельзя.
Метод Set keyset () возвращает представление карты в виде множества всех ключей. Из этого представления можно удалять элементы, при этом ключи и соответствующие им значения автоматически удаляются из карты, но добавлять новые элементы нельзя.
Метод Collection values О возвращает представление карты в виде множества всех значений. Из этого представления можно удалять элементы, при этом значения и соответствующие им ключи автоматически удаляются из карты, но добавлять новые элементы нельзя.
java.util.Map.Entry
Метод Object getKey() возвращает ключ элемента.
Метод Ob j ect getValue () возвращает значение элемента.
Метод Object setValue (Object value) заменяет текущее значение в связаннойкарте указанным значением и возвращаетпрежнеезначение.
Параметры:
valueновое значение, которое нужно связать с ключом
java.util.HashMap
Конструктор HashMap () создает пустую хэш-карту.
Конструктор HashMap (Map entries) создает хэш-карту и добавляет в нее все элементы из указанной карты.
Параметры:
entriesкарта, содержащая добавляемые элементы
Конструктор HashMap (int initialCapacity) создает пустую хэш- карту заданной емкости с указанным коэффициентом загруженности.
Конструктор HashMap (int initialCapacity, float loadFactor) создает пустую хэш-карту заданной емкости с указанным коэффициентом
загруженности.Параметры: initialCapacity loadFactorзагруженности.Параметры: initialCapacity loadFactorисходное число ячеек
загруженности.
Параметры: initialCapacity loadFactor
загруженности.
Параметры: initialCapacity loadFactor
число между 0,0 и 1,0, определяющее процент заполнения хэш-таблицы, при котором она будет автоматически расширена с одновременным генерированием новых хэш-кодов
APIAPIjava.util.TreeMap
API
API
Конструктор TreeMap (Comparator C ) создает карту-дерево и использует указанный компаратор для сортировки ключей.
Параметры:
сскомпаратор, используемый для сортировки
с
с
Конструктор TreeMap (Map entries) создает карту-дерево и добавляет в нее все элементы из указанной карты.
Параметры:
entriesкарта, содержащая добавляемые элементы
Конструктор TreeMap (SortedMap entries) создает карту-дерево, добавляет в нее все элементы из указанной отсортированной карты и использует компаратор элементов из отсортированной карты.
Параметры:
entriesотсортированная карта, содержащая добавляемые элементы и
компаратор
java.util.SortedMap
Метод Comparator comparator () возвращает компаратор, используемый для сортировки ключей или значение null, если ключи сравниваются с помощью метода compareTo интерфейса Comparable.
Метод Object firstKey() возвращает наименьший ключ карты.
Метод Object lastKey() возвращает наибольший ключ карты.
Специализированные классы карт
Библиотека коллекций содержит несколько классов карт, предназначенных для выполнения специализированных задач. Некоторые из них кратко рассматриваются в этом разделе.
Хэш-карты с нестрогим хэшированием
Класс хэш-карты с нестрогим хэшированием WeakHashMap был разработан для решения интересной задачи. Что происходит со значением, чей ключ больше не используется в программе, например из-за того, что исчезла последняя ссылка на этот ключ? В этом случае обратиться к объекту-значению уже нельзя. А так как этот ключ уже не содержится нигде в программе, то нет никакой возможности удалить его пару ключ/значение из карты. Но почему его не может удалить "сборщик мусора", в чьи обязанности как раз и входит удаление неиспользуемых объектов?
К сожалению, все не так просто. "Сборщик мусора" в системе управления памятью следит за действующими объектами. Пока объект карты активен, все ячейки карты также активны и не могут быть утилизированы. Таким образом, об удалении неиспользуемых значений из активных карт должна позаботиться сама программа. Именно для этого и предназначен класс WeakHashMap. Эта структура данных взаимодействует со "сборщиком мусора" для удаления тех пар ключ/значение, для которых единственной ссылкой на ключ является запись в хэш-таблице.
Вот как работает этот механизм. Класс WeakHashMap для хранения ключей использует нестрогие ссылки (weakreferences). ОбъектWeakReferenc содержитссылкунадругойобъект, т.е. в данном случае на ключ хэш-таблицы. Обычно, если "сборщик мусора" определяет, что некоторый объект не имеет ссылок на себя в программе, то он утилизирует его. А если единственная ссылка на объект имеет тип WeakReference, то "сборщик мусора" помещает эту нестрогую ссылку в очередь. Класс WeakHashMap периодически проверяет появление новых ссылок в очереди, потому что появление в очереди слабой ссылки означает, что данный ключ больше не используется и его объект можно удалить. Таким образом, класс WeakHashMap удаляет соответствующее этому ключу значение.
Связанные хэш-карты
В пакете SDK версии 1.4 предусмотрен класс связанной хэш-карты LinkedHashMap, который запоминает порядок вставки в карту новых пар ключ/значение. Таким образом удается избежать случайного расположения ключей в классе HashMap, не используя для этого более ресурсоемкий класс TreeMap. По мере добавления записей в таблицу они формируют дважды связанный список.
Методы keyset, entrySet и values этого класса возвращают коллекции с итераторами, которые для обхода элементов используют ссылки дважды связанного списка.
Рассмотрим, например, объект-карту staffиз листинга 2.4.
Map staff = new LinkedHashMap();
staff.put("144-25-5464", new Employee("Angela Hung"));
staff.put("567-24-2546", new Employee("Harry Hacker"));
staff.put("157-62-7935”, new Employee("Gary Cooper"));
staff.put("456-62-5527", new Employee("Francesca Cruz"));
Итератор staff.keyset () . iterator () перечислит ее ключи в следующем порядке.
144-25-5464
567-24-2546
157-62-7935
456-62-5527
А итератор staff. values () . iterator () перечислит ее значения в следующем порядке.
Angela Hung
Harry Hacker
Gary Cooper
Francesca Cruz
Для чего может понадобиться такое поведение? Очевидно, что многим программистам может не понравиться то, что порядок элементов в хэш-таблице может изменяться произвольным образом, например, при превышении заданного коэффициента загруженности и генерации ключей заново или при копировании значений из одной хэш-таблицы в другую. Класс LinkedHashMap позволяет управлять порядком расположения элементов. Кроме того, если хэш-таблица заполнена в небольшой степени, то доступ к ее элементам на основе итераций нельзя назвать эффективным, так как итератор должен обойти все пустые ячейки. А итерации в классе LinkedHashMap не зависят от емкости таблицы, так как итератор выполняет обход элементов по ссылкам на них. Недостатком такого способа, естественно, является дополнительный объем памяти, необходимый для хранения ссылок.
Если применяется показанный ниже конструктор, и параметр accessOrder имеет значение true, то связанная хэш-карта для обхода элементов использует порядок доступа, а не порядок вставки.
LinkedHashMap(int initialCapacity, float loadFactor,
boolean accessOrder)
При каждом вызове метода get или put требуемая запись удаляется из своей исходной позиции и размещается в конце связанного списка. (Затрагивается только позиция элемента в связанном списке, но не ячейка хэш-таблицы. Запись всегда остается в той ячейке, которая отвечает хэш-коду ключа.)
Такое поведение полезно использовать для реализации алгоритма "замещения наиболее давних по использованию элементов" ("least recently used") при работе с кэш-памятью. Например, часто требующиеся записи можно разместить в кэш-памяти, а редко требующиеся — в базе данных. Когда нужной записи нет в хэш-таблице, а сама таблица полностью заполнена, то в таком случае можно воспользоваться итератором для замещения нескольких первых элементов таблицы. Именно эти элементы использовались наиболее давно по сравнению с остальными.
Этот процесс можно даже автоматизировать, создавая дочерний класс для класса LinkedHashMap и переопределяя метод removeEldestEntry.
protected boolean removeEldestEntry(Map.Entry eldest)
Если данный метод возвращает значение true, то добавление новой записи приводит к удалению наиболее старой. Например, следующий фрагмент кода поддерживает кэш размером 100 элементов.
Map cache = new
LinkedHashMap(12 5, 0.8F, true)
{
protected boolean removeEldestEntry(Map.Entry eldest)
(
return size() > 100;
}
}
С другой стороны, можно также проверить содержимое наиболее давнего по использованию элемента eldest и решить, следует ли его удалять. Для чего можно, например, проверить отметку времени для этого элемента.
Хэш-карты с идентичным хэшированием
В пакет SDK версии 1.4 добавлен еще один класс хэш-карты с идентичным хэшированием IdentityHashMap, выполняющий другую специализированную задачу. Хэш- коды ключей в нем подсчитываются не методом hashCode, а методом System. identityHashCode. Этот метод используется в Object.hashCode для вычисления хэш-кода по адресу объекта в памяти. Кроме того, для сравнения объектов класс IdentityHashMap применяет оператор ==, а не метод equals.
Иначе говоря, объекты считаются разными, даже если их содержимое совпадает. Этот класс полезен для реализации алгоритмов обхода объектов (например, для сериализации), в которых требуется следить даже за теми объектами, которые уже были пройдены итератором.
F^fjava.util.WeakHashMap
Конструктор WeakHashMap () создает пустую хэш-карту с нестрогим хэшированием.
Конструктор WeakHashMap (int initialCapacity) создает пустую хэш-карту с нестрогим хэшированием с указанной емкостью.
Конструктор WeakHashMap (int initialCapacity, float loadFac
tor) создает пустую хэш-карту с нестрогим хэшированием с указанной емкостью и коэффициентом загруженности.
Параметры:
initialCapacityисходное число ячеек
loadFactorчисло между 0,0 и 1,0, определяющее процент запол
нения хэш-таблицы, при котором она будет автоматически расширена с одновременным генерированием новых хэш-кодов. По умолчанию оно равно 0,75,
a
java.util.LinkedHashMap
Конструктор LinkedHashMap () создает пустую связанную хэш-карту —I(начиная с SDK 1.4).
Конструктор LinkedHashMap (Map entries) создает связанную хэш- карту и добавляет в нее все элементы из указанной карты.
Параметры:
entriesкарта, содержащая добавляемые элементы
Конструктор LinkedHashMap(int initialCapacity) создает пустую связанную хэш-карту с заданной емкостью.
Конструктор LinkedHashMap(int initialCapacity, float loadFactor) создает пустую связанную хэш-карту с заданной емкостью и коэффициентом загруженности.
Конструктор LinkedHashMap (int initialCapacity, float load
Factor, boolean accessOrder) создает пустую связанную хэш-карту с заданной емкостью, коэффициентом загруженности и порядком доступа. Параметры: initialCapacityисходное число ячеек
loadFactorчисло между 0,0 и 1,0, определяющее процент запол
нения хэш-таблицы, при котором она будет автоматически расширена с одновременным генерированием новых хэш-кодов. По умолчанию оно равно 0,75.
Конструктор protected boolean removeEldestEntry(Map.Entry eldest) необходимо переопределить, если нужно, чтобы первый (т.е. наиболее давний по использованию) элемент удалялся автоматически с возвращением значения true после добавления в карту нового элемента. В предлагаемой по умолчанию реализации он возвращает значение false, т.е. устаревшие элементы не удаляются. Метод можно переопределить так, чтобы он выборочно возвращал значение true, например, если наиболее давний по использованию элемент удовлетворяет определенному условию или карта превышает заданный размер.
Параметры: eldestустаревшая запись, которую предполагается удалить
java. util. LinkedHashMap
Атт
Метод IdentityHashMap() создает пустую хэш-карту с идентичным хэшированием (начиная с SDK 1.4).
Метод IdentityHashMap (Map entries) создает хэш-карту с идентичным хэшированием и добавляет в нее все элементы из указанной карты.
Параметры: entries карта, содержащая добавляемые элементы
Метод IdentityHashMap (int expectedMaxSize) создает пустую хэш- карту с идентичным хэшированием.
Параметры: expectedMaxSizeпредполагаемое максимальное число элементов в
карте
java. lang. System
Метод static int identityHashCode(Object obj ) возвращает тот же хэш-код (вычисляемый no адресу объекта в памяти), что и метод Object .hashCode, даже если метод hashCode переопределен в классе объекта obj.
Архитектура коллекций
Архитектура (framework) представляет собой набор классов, формирующих основу для расширения базовой функциональности. Архитектура содержит суперклассы с
полезными свойствами, правилами и механизмами. Пользователь архитектуры формирует дочерние классы, расширяющие исходную функциональность, без повторного изобретения базовых механизмов. Например, библиотека Swing представляет собой архитектуру для создания пользовательского интерфейса.
Библиотека коллекцийJava образует архитектуру для классов коллекций. В ней определены несколько интерфейсов и абстрактных классов для реализации коллекций, которые показаны на рис. 2.10, а также определенные механизмы, как, например, протокол итерации. Как показано в предыдущих разделах, классы коллекций можно использовать, даже не зная основных принципов работы самой архитектуры. Однако, знание принципов работы архитектуры классов потребуется для реализации наиболее общих алгоритмов для нескольких типов коллекций или для создания нового типа коллекции.
Для контейнеров предусмотрено два фундаментальных интерфейса: коллекции Collection и карты Map.
Для вставки элементов в коллекцию (интерфейс Collection) используется метод add. boolean add(Object element)
Рис. 2.10. Интерфейсы архитектуры коллекций
Карта (интерфейс Map) предназначена для хранения пар ключ/значение, а для их вставки применяется метод put.
Object put(object key, object value)
Для чтения элементов коллекции используется итератор (интерфейс Iterator), a для обращения к элементам карты применяется метод get.
Object get(Object key)
Интерфейс списка List представляет собой упорядоченную коллекцию (ordered collection). Его элементы добавляются в определенном месте контейнера одним из двух способов: либо по целочисленному индексу, либо по итератору списка. Интерфейс List содержит определения нескольких методов для обеспечения произвольного доступа к элементам коллекции.
void add(int index, Object element)
Object get(int index) void remove(int index)
Как уже упоминалось выше, интерфейс List предлагает эти методы произвольного доступа независимо от того, являются они эффективными для конкретной реализации или нет. Чтобы избежать ресурсоемких операций произвольного доступа, в пакете SDK версии 1.4 предусмотрен интерфейс RandomAccess. Он не содержит каких- либо методов и применяется лишь для проверки того, поддерживает ли определенная коллекция эффективный произвольный доступ.
if (с instanceof RandomAccess)
{
использовать алгоритм с произвольным доступом
}
else
{
исполь3обать алгоритм c последовательны!/; доступом
}
Этот интерфейс реализуют следующие классы: массив-список ArrayList и вектор Vector.
С теоретической точки зрения имело бы смысл использовать отдельный интерфейс массива Array, расширяющий интерфейс List и поддерживающий методы произвольного доступа. Если бы существовал отдельный интерфейс Array, то алгоритмы произвольного доступа использовали бы параметры интерфейса Array и их нельзя было бы случайно применять к коллекциям, в которых произвольный доступ осуществляется медленно. Однако, разработчики архитектуры коллекций предпочли не создавать отдельный интерфейс, чтобы ограничить общее количество интерфейсов в библиотеке. Кроме того, они не хотели излишне ограничивать программистов. Поэтому в алгоритмах, использующих произвольный доступ, можно также использовать связанные списки. Следует лишь помнить о том, что производительность алгоритма при этом будет более низкой.
В интерфейсе итератора списка Listiteratorопределен метод вставки элемента перед текущей позицией итератора.
void add(Object element)
Для получения и удаления элементов в определенной позиции используются методы next и remove интерфейса Iterator.
Интерфейс множества Set идентичен интерфейсу Collection, но поведение его методов определено более строго. Метод add множества должен отвергать повторяющиеся элементы. Метод equals должен быть определен таким образом, чтобы два множества считались идентичными, если в них содержатся одинаковые элементы (даже, если они отличаются порядком расположения). Метод hashCode должен быть определен так, чтобы два множества с одинаковыми элементами имели одинаковый хэш-код.
Для множеств и списков понятие равенства определено достаточно четко. Два множества считаются равными, если они содержат одинаковые элементы независимо от порядка расположения элементов. Два списка считаются равными, если они содержат одинаковые элементы в одинаковом порядке. Для коллекций нет строго определенного понятия равенства. Поэтому при работе с коллекциями не следует применять метод equals.
Для чего же нужно создавать отдельные интерфейсы, если сигнатуры их методов совпадают? Дело в том, что концептуально не все коллекции являются множествами, поэтому применение интерфейса Set позволяет программистам создавать методы, допустимые только для множеств.
В заключение интерфейсы отсортированного множества SortedSet и отсортированной карты SortedMap поддерживают используемый для сортировки объект сравнения и определяют методы для получения представлений подмножеств контейнеров. Такие представления подробно рассматриваются в следующем разделе.
Перейдем от интерфейсов к рассмотрению их классов реализации. Здесь уже обсуждалось, что интерфейсы коллекций содержат несколько методов, которые могут быть просто реализованы на основе более фундаментальных методов. Существует пять абстрактных классов, которые содержат реализации наиболее распространенных методов.
Abstracted lection
AbstractList
AbstractSequentialList
AbstractSet
AbstractMap
При реализации собственного класса коллекции можно воспользоваться расширением одного из этих классов с уже реализованными в них стандартными операциями.
Кроме того, библиотекаJava содержит шесть конкретных классов.
LinkedList
ArrayList
HashSet
TreeSet
HashMap
TreeMap
Рис. 2.11.Схема взаимосвязи классов в архитектуре коллекций
На рис. 2.11 схематически показаны взаимосвязи между этими классами.
Наконец, существует несколько "унаследованных" классов-контейнеров, которые были представлены в первых версиях платформы Java еще до создания структуры коллекций.
Vector
Stack
Hashtable
Properties
Они также были интегрированы в архитектуре коллекций так, как показано на рис. 2.12. Более подробно эти классы рассматриваются ниже в данной главе.
Рис. 2.12. Унаследованные классыв архитектуре коллекций
Представления и оболочки
После изучения рис. 2.10 и 2.11 может возникнуть мысль об избыточности средств. Зачем понадобилось создавать шесть интерфейсов и пять абстрактных классов для реализации всего шести конкретных классов коллекций. Однако, эти рисунки не полностью отражают возможности архитектуры коллекций. Используя представления, можно получить другие объекты, реализующие интерфейс Collection или Map. Примером этого может служить метод keyset классов-карт. На первый взгляд может показаться, что этот метод создает новое множество, заполняет его ключами и возвращает его. На самом деле это не так. Метод keyset возвращает объект класса, реализующего интерфейс Set, чьи методы работают с исходной картой. Такая коллекция и называется
представлением (view). Разработчик программы не знает (и ему не нужно это знать), какой именно класс использует библиотека для получения конкретного представления.
Технология работы на основе представлений имеет несколько полезных приложений в архитектуре коллекций. Напомним, что методы класса Vector синхронизированы, что нежелательным образом снижает производительность, когда доступ к вектору осуществляется всего лишь из одного потока. Поэтому вместо класса Vector рекомендуется использовать класс ArrayList. Если же доступ к коллекции осуществляется из нескольких потоков, важно, чтобы ее методы были синхронизированы. Например, если один поток пытается добавить элемент в хэш-таблицу, а другой в это же время пытается генерировать новые ключи, это неизбежно приведет к сбою. Разработчики библиотеки могли бы реализовать сопутствующие классы с синхронизированными методами для всех структур данных. Но они поступили иначе и создали более полезный механизм генерации синхронизированных видов для всех интерфейсов. Например, статический метод synchronizedMap в классе Collections может превратить любую карту в класс Map с синхронизированными методами доступа.
HashMap hashMap = new HashMap();
map = Collections.synchronizedMap(hashMap);
Теперь к объекту map можно осуществить доступ из нескольких потоков. В таком случае методы get и put становятся синхронизированными, т.е. вызов каждого из них будет полностью завершен до того, как другой поток сможет запустить подобный метод.
■ Класс Collections содержит несколько вспомогательных методов, чьи параметры или возвращаемые значения являются коллекциями. Его не следует путать с интерфейсом Collection.
Для получения синхронизированных коллекций предусмотрены следующие шесть методов.
Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedSet
Collections.synchronizedSortedSet
Collections.synchronizedMap
Collections.synchronizedSortedMap
Возвращаемые этими методами представления иногда называют оболочками (wrappers).
и
Если список реализует интерфейсRandomAccess, то возвращаемый методом synchronizedList объект-оболочка принадлежит классу, который также реализует интерфейс RandomAccess.
Необходимо убедиться, что никакой из потоков не обращается к структуре данных посредством какого-либо из исходных несинхронизированных методов. Проще всего этот можно обеспечить, не сохраняя ссылку на исходный объект, а просто передавая созданную конструктором коллекцию методу-оболочке.
map = Collections . synchronizedMap (new HashMapt)) ;
При организации доступа к коллекции из нескольких потоков следует обратить внимание на одну особенность. Напомним, что для коллекции можно использовать либо несколько итераторов-читателей, либо один поток-писатель. По этой причине
необходимо убедиться, что представленная ниже итерация не происходит в тот момент, когда другой поток модифицирует коллекцию.
Iterator iter = collection.iterator();
while (iter.hasNext())
какие-то действия с методом iter.next() ;
В противном случае метод next генерирует исключительную ситуацию ConcurrentModificationException.
Если при управлении доступом к одной коллекции из нескольких потоков возникают неразрешимые проблемы, последуйте совету из главы 1: определите, какой цели на высоком уровне служит коллекция, и создайте затем соответствующий класс. Сделайте эту коллекцию полем экземпляра класса и управляйте доступом к нему посредством синхронизированных методов.
Поскольку оболочки относятся к интерфейсу, а не реальному объекту коллекции, то возможен доступ лишь к тем методам, которые определены в интерфейсе. Например, класс LinkedList содержит вспомогательные методы addFirst и addLast, которые не являются частью интерфейса List. Поэтому они недоступны в синхронизированном интерфейсе-оболочке.
МетодsynchronizedCollection (так же, как и методunmodifiableCollection,об- суждаемый далее в этом разделе) возвращает коллекцию, чей метод equals не вызывает методequals базовой коллекции. Вместо этого он наследует методequals класса Object, которыйпроверяет, являютсялиобъекты идентичными. Если множество или списокпреобразуются в обычную коллекцию, проверитьсодержащиеся в ней объекты на равенство уже нельзя. Оболочка действует именно таким образом, потому что проверка равенстванеопределенанаэтомуровнеиерархии.
Тем не менее, классы synchronizedSet и synchronizedListне скрывают методов equals базовых коллекций.
Аналогичным образом оболочки обращаются с методом hashCode.
Класс Collections имеет другой потенциально полезный набор оболочек, предназначенных для создания представлений коллекций, дальнейшая модификация которых невозможна. В этих оболочках предусмотрена проверка существования коллекции на этапе исполнения приложения. Если производится попытка модификации коллекции, то генерируется исключительная ситуация, и содержимое коллекции остается без изменений.
Предположим, что следующий фрагмент кода проверяет содержимое коллекции, не изменяя его.
List staff = new LinkedList ();
lookAt(new Collections.unmodifiableList(staff)) ;
Метод Collections .unmodifiableList возвращает объект класса, реализующий интерфейс List. Методы доступа считывают значения из коллекции staff. Конечно, в этом случае метод lookAt может вызывать все методы интерфейса List, а не только методы доступа. Но все методы-мугаторы (такие, как add) переопределены так, чтобы генерировать исключительную ситуацию UnsupportedOperationException вместо того, чтобы перенаправлять вызов к базовой коллекции. Подобные методы существуют и для получения неизменяемых оболочек для других интерфейсов коллекций.
В документации для API-интерфейса некоторые методы интерфейсов коллекций, например add, описаны как "необязательные операции". Это удивительно: разве цельюинтерфейсанеявляетсяуказаниетехметодов,которые должен реализовывать класс? На самом деле имело бы смысл отделить интерфейсы, предназначенные только для чтения, от интерфейсов, разрешающих полный доступ. Но это удвоило бы число интерфейсов, что разработчики библиотеки сочли неприемлемым.
Класс Arrays содержит статический метод asList, который возвращает интерфейс-оболочку List для обычного массива Java. Этот метод позволяет передать массив методу, аргумент которого имеет тип списка или коллекции.
Card[] cardDeck = new Card[52];
List cardList = Arrays.asList(cardDeck) ;
Возвращаемый объект метода asList не принадлежит классу ArrayList. Это объ- ек-гпредставление, чьи методы get и set могут обращаться к базовому массиву. Все методы, способные изменять размер массива (например, метод add и метод remove соответствующего итератора), генерируют исключительную ситуацию Unsupported- OperationException.
Диапазоны
Для нескольких коллекций можно формировать представления диапазонов (subrange). Предположим, что имеется список staff, из которого необходимо извлечь элементы в диапазоне от 10 до 19. Для получения представления диапазона списка применяется метод subList.
List group2 = staff.subList(10, 20);
Первый индекс включается в диапазон, а второй — нет, точно так же, как для операции substring класса String.
К диапазону можно применять любые операции, которые автоматически отражаются на всем списке. Например, можно удалить весь диапазон.
group2.clear() ; // сокращение списка staff
В таком случае элементы диапазона автоматически удаляются из спискаstaff.
В отсортированных множествах и картах для формирования диапазона используется порядок сортировки, а не позиция элемента. Для этого в интерфейсе SortedSet объявлены три интерфейса.
subSet(from, to)
headset(to)
tailSet(from)
Они возвращают подмножества элементов, больших или равных from и строго меньших to, больших или равных from, строго меньших to, соответственно. Существуют подобные методы и для сортированных карт.
subMap(from, to)
headMap(to)
tailMap (from)
Они возвращают представления карт, содержащие все элементы, чьи ключи принадлежат указанному диапазону.
Оболочки коллекций, использующие минимум ресурсов
Метод Collections.nCopies (n, anObject) возвращает неизменяемый объект, реализующий интерфейс List, и имитирует набор из п элементов, каждый из которых принадлежит классу anObject. Этот метод использует минимальный объем памяти, потому что на самом деле сохраняется только один, а не n объектов. Это возможно, благодаря применению методики интерфейсов-оболочек. Подобный список используется для инициализации конкретного контейнера. Например, следующий вызов создает объект ArrayList, содержащий 100 строк с одинаковым значением "DEFAULT".
ArrayList settings
= new ArrayList(Collections.nCopies(100, "DEFAULT"));
Метод Collections. singleton(anObject) возвращает объект-оболочку, реализующую интерфейс Set (в отличие от метода nCopies, создающего List). Возвращаемый объект представляет собой неизменяемое одноэлементное множество без каких- либо накладных расходов, связанных с использованием хэш-таблицы или дерева. Константы Collections.EMPTY_LISTи Collections .EMPTY_SET возвращают объекты, реализующие интерфейсы List и Set и не содержащие элементов. Они обладают тем же достоинством, что и метод singleton: возвращаемые объекты не требуют дополнительных ресурсов, которые нужны для более сложных структур данных. Одноэлементные классы и пустые объекты очень полезно использовать в качестве параметров для методов, аргументы которых имеют тип списка, множества или контейнера.
В JDK версии 1.3 добавлены методы singletonList и singletonMap, а также константа EMPTY_MAP.
Заключительное замечание о дополнительных операциях
В заключение рассмотрим "дополнительные" или неподдерживаемые операции в интерфейсах коллекции и итератора. Они несомненно представляют собой одно из наиболее спорных решений разработчиков архитектуры коллекций. Проблема связана, несомненно, с мощными и удобными представлениями. Представления удобны, поскольку приводят к созданию эффективного кода, а их "легко подключаемая" природа означает, что для работы с ними достаточно освоить лишь несколько базовых концепций. Однако, использование представлений, как правило, связано с некоторыми ограничениями, т.е. оно может использоваться только для чтения, но не для изменения размера, или может поддерживать удаление, но не вставку, как, например, в представлении ключей для карты. Эти ограничения отличаются для разных представлений, поэтому создание отдельного интерфейса для каждого ограниченного представления привело бы к появлению огромного количества запутанных интерфейсов, непригодных для практического использования.
Следует ли использовать методику "дополнительных" методов в своих собственных интерфейсах? Нет. Хотя коллекции используются достаточно часто, стиль программирования для их реализации нетипичен для других проблемных областей. Разработчикам библиотеки классов коллекций пришлось найти компромиссные решения для удовлетворения целого ряда жестко противоречащих требований. Действительно, пользователям нужна библиотека, которая была бы проста в изучении, удобна в использовании, универсальна, защищена от случайных ошибок, но так же эффективна, как и специально созданные и отлаженные алгоритмы. Очевидно, что невозможно достичь всех целей одновре-
менно или хотя бы приблизиться к ним. Достаточно ознакомиться с несколькими библиотеками, например, библиотекой JGL компании ObjectSpace (www.objectspace.com), чтобы увидеть различные варианты компромисса. Для понимания всей сложности проблемы еще лучше было бы попытаться создать свою собственную библиотеку коллекций и алгоритмов. В этом случае вы достаточно быстро столкнетесь с неизбежными конфликтами и поймете, какую серьезную задачу решили разработчики из компании Sun, и несомненно по достоинству оцените их труд. Однако, при решении своих вычислительных задачах программисту редко приходится сталкиваться со всем набором столь жестких ограничений. Чаще всего удается найти решение, при котором нет необходимости идти на такие крайние меры, как использование "дополнительных" операций интерфейсов.
ITpnfjava.util.Collections
[2 ~U • Метод static Collection synchronizedCollection(Collection с) создает представление указанной коллекции с синхронизированными методами.
Параметры:
сколлекция, для которой создается интерфейс-оболочка
Метод static List synchronizedList (List с) создает представление указанного списка с синхронизированными методами.
Параметры:
ссписок, для которого создается интерфейс-оболочка
Метод static Set synchronizedSet(Set с) создает представление указанного множества с синхронизированными методами.
Параметры:
смножество, для которого создается интерфейс-оболочка
Метод static SortedSet synchronizedSortedSet(SortedSet с) создает представление указанного отсортированного множества с синхронизированными методами.
Параметры:
сотсортированное множество, для которого создается интер
фейс-оболочка
Метод static Map synchronizedMap (Map с) создает представление указанной карты с синхронизированными методами.
Параметры:
скарта, для которой создается интерфейс-оболочка
Метод static SortedMap synchronizedSortedMap(SortedMap с) создает представление указанной отсортированной карты с синхронизированными методами.
Параметры:
сотсортированная карта, для которой создается интерфейс-
оболочка
Метод static Collection unmodifiableCollection(Collection c) создает представление отсортированной коллекции, методы-мутаторы которого генерируют исключительную ситуацию Unsupported- Operat ionExcept ion.
Параметры:
сколлекция, для которой создается интерфейс-оболочка
Метод static List unmodifiableList (List с) создает представле
ние списка, методы-мутаторы которого генерируют исключительную ситуацию UnsupportedOperationException.
Параметры:
ссписок, для которого создается интерфейс-оболочка
Метод static Set unmodif iableSet (Set с) создает представление множества, методы-мутаторы которого генерируют исключительную ситуацию UnsupportedOperationExcept ion.
Параметры:
смножество, для которого создается интерфейс-оболочка
Метод static SortedSet unmodifiableSortedSet(SortedSet c) создает представление отсортированного множества, методы-мутаторы которогогенерируютисключительнуюситуацию
UnsupportedOperationException.
Параметры: сотсортированное множество, для которого создается интер
фейс-оболочка
Метод static Map unmodifiableMap(Map с) создает представление карты, методы-мутаторы которого генерируют исключительную ситуацию UnsupportedOperationException.
Параметры:
скарта, для которой создается интерфейс-оболочка
Метод static SortedMap unmodifiableSortedMap(SortedMap с) создает представление отсортированной карты, методы-мутаторы которого генерируют исключительную ситуацию UnsupportedOperationException.
Параметры:
сотсортированная карта, для которой создается интерфейс-
оболочка
Метод static List nCopies(int n, Object value) создает представление объекта в виде неизменяемого списка с п идентичными элементами.
Параметры: пколичество повторов значения в списке
Метод static Set singleton (Object value) создает представление объекта в виде одноэлементного множества.
Параметры: valueзначение элемента в коллекции
Константа static final List EMPTY_LIST означает неизменяемый интерфейс-оболочку для пустого списка.
Константа static final Set EMPTY_SET означает неизменяемый интерфейс-оболочку для множества.
java.util.Arrays
Метод static List asList (Object [] array) возвращает представление массива в виде списка, который можно модифицировать, но без изменения размера.
Параметры:
arrayмассив, для которого создается объект-оболочка
java.util.List
МетодList subList(int from, int to) возвращает представление части списка из указанного диапазона позиций.
Параметры:
fromпозиция первого элемента, включаемого в представление
toпозиция первого элемента, невключаемого в представление
В
java.util.SortedSet
Метод SortedSet subset (Object from,Object to) возвращает
представление в виде отсортированного множества для элементов из указанного диапазона, т.е. больших или равных fromи строго меньших to.
Параметры:
fromпозиция первого элемента, включаемого в представление
toпозиция первого элемента, невключаемого в представление
Метод SortedSet headset (Object to) возвращает представление в виде отсортированного множества для элементов из указанного диапазона, т.е. строго меньших to.
Параметры:
toпозиция первого элемента, невключаемого в представление
Метод SortedSet tailSet (Object from) возвращает представление в виде отсортированного множества для элементов из указанного диапазона, т.е. больших или равных from.
Параметры:
fromпозиция первого элемента, включаемого в представление
java.util.SortedMap
Метод SortedMap subMap (Object from, Object to) возвращает представление в виде карты для элементов из указанного диапазона, т.е. больших или равных fromи строго меньших to.
Параметры:
fromпозиция первого элемента, включаемого в представление
toпозиция первого элемента, невключаемого в представление
Метод SortedMap headMap(Object to) возвращает представление в виде карты для элементов из указанного диапазона, т.е. строго меньших to.
Параметры:
toпозиция первого элемента, невключаемого в представление
Метод SortedMap tailMap (Object from) возвращает представление в виде карты для элементов из указанного диапазона, т.е. больших или равных from.
Параметры:
fromпозиция первого элемента, включаемого в представление
Пакетные операции
В большинстве приведенных примеров для последовательного обращения к элементам коллекции использовался итератор. Альтернативным вариантом доступа к элементамколлекцииявляются пакетные операции (bulk operations), предусмотренныев библиотеке коллекций Java.
Предположим, что необходимо найти пересечение двух множеств, т.е. только общие элементы двух множеств. Вначале необходимо создать новое множество для сохранения результата.
Set result = new HashSet(a);
Здесь используется тот факт, что каждая коллекция имеет конструктор, чьим параметром является другая коллекция, содержащая исходные значения.
Далее применяется метод retainAll, который сохраняет все элементы множества result, которые содержатся во множестве b.
result.retainAll(b);
Таким образом, пересечение множеств найдено без применения цикла.
Эту идею можно развить, применяя пакетную операцию для представления. Предположим, что имеется карта staf Map, где каждому идентификационному коду сотрудника соответствует объект-сотрудник, и имеется множество terminatedIDs идентификационных кодов сотрудников, которых требуется уволить.
Map staffMap= ... ;
Set terminatedIDs = ... ;
В таком случае можно достаточно просто создать множество ключей и удалить из него идентификаторы всех уволенных сотрудников.
staffMap.keyset().removeAll(terminatedIDs);
Поскольку множество ключей является представлением карты, то ключи и связанные с ними имена сотрудников будут удалены из карты автоматически.
Используя представления диапазонов, пакетные операции можно применять для подчиненных списков или подмножеств. Предположим, что требуется добавить первые десять элементов списка в другой контейнер. Для чего нужно создать подчиненный список, содержащий эти десять элементов.
relocated.addAll(staff.subList(0,10));
С диапазоном значений списка также можно выполнять операции изменения данных.
staff.subList(0, 10).clear();
УнаследованныеAPI-интерфейсы
Поскольку большая часть API-интерфейса платформы Java была разработана до создания архитектуры коллекций, то зачастую необходимо преобразовывать традиционные массивы и векторы в более современные виды коллекций.
Рассмотрим случай, когда значения из массива или вектора требуется разместить в коллекции. Если эти значения содержатся в объекте Vector, то на его основе можно достаточно просто создать коллекцию.
Vector values =
HashSet staff = new HashSet(values);
Все классы коллекций поддерживают конструктор, аргументом которого может быть любой объект коллекции. Начиная с платформы Java 2, класс Vector реализует интерфейс List.
Для выполнения аналогичной операции с элементами массива, его необходимо преобразовать в коллекцию с помощью интерфейса-оболочки Arrays . asList.
String!] values ...;
HashSet staffnew HashSet(Arrays.asList(values));
И наоборот, если необходимо вызвать метод с аргументом-вектором, то его следует создать на основе коллекции.
Vector values = new Vector (staff) ,•
А для вызова метода с аргументом-массивом те же действия выглядят несколько сложнее. Конечно же, можно воспользоваться методом toArray, по его результатом являетсямассив объектов.
Object[] values = staf f . toArray () ;
Даже если точно известно, какого типа объекты содержит коллекция, все равно нельзя воспользоваться явным приведением к типу.
String[] values = (String[j)staff.toArray(); // Ошибка!
Массив, возвращаемый методом toArray, создается как массив типа Object[ ], и изменить его тип нельзя. Поэтому следует воспользоваться вариантом метода toArray, в аргументе которого необходимо указать массив требуемого типа с нулевой длиной. Только в этом случае возвращаемый массив будет принадлежать к этому же типу.
String[J values = (String[])staff.toArray(new String[0]);
• Здесь у читателя может возникнуть вопрос, почему бы просто не передать объект C;iass (например, String.Class)методу toArray. Однако, как показано в примечаниях с описанием API-функций в конце раздела, данный метод выполняет двойные обязанности: заполняет существующий массив (при условии, что он имеет достаточный размер) и создает новый массив.
java.util.Collection
• Метод Object [] toArray(Object [] array) проверяет размер параметра-массива array. Если размер массива больше размера коллекции, то метод toArray добавляет в массив все элементы коллекции с концевым значением null и возвращает этот массив. Если длина массива равна длине коллекции, метод добавляет все элементы без концевого значения null. Если в коллекции недостаточно места, то метод создает новый массив того же типа, что и исходный II заполняет его элементами коллекции.
Параметры:
arrayмассив, содержащий элементы коллекции, или массив, тип
которого используется для создания нового массива
Алгоритмы
Обобщенные интерфейсы коллекций обладают огромным достоинством: благодаря им алгоритмы достаточно реализовать только один раз. Рассмотрим простой алгоритм поиска наибольшего элемента коллекции. По традиции такой алгоритм программируется как цикл. Вот как с помощью цикла можно найти наибольший элемент массива.
if (a.length == 0) throw new NoSuchElementException();
Comparable largest = a[0];
for (int i=l; i < a.length; i++)
if (largest.compareTo(a[i]) < 0} largest = a[i];
Для поиска наибольшего элемента в массиве-списке этот фрагмент кода будет выглядеть несколько иначе.
if (v.sizeO == 0) throw new NoSuchElementException();
Comparable largest = (Comparable)v.get(0 ) ;
for (int i = 1; i < v.sizeO; i + + )
if (largest.compareTo(v.get(i) ) < 0) largest = v.get(i);
Как же быть со связанным списком? Для него нельзя использовать весьма эффективный способ на основе произвольного доступа, поэтому придется воспользоваться итератором.
if (1.isEmpty()) throw new NoSuchElementException();
Iterator iter = 1.iterator();
Comparable largest = (Comparable)iter.next();
while (iter.hasNext())
{
Object next = iter.next();
if (largest.compareTo(next) < 0) largest = next;
}
Создавать разные циклы для разных типов коллекций — это довольно утомительное занятие с высокой вероятностью возникновения ошибок. Нужно организовать проверку наличия ошибок занижения-завышения на единицу числа подсчитываемых объектов, проверить, правильно ли работают эти циклы для пустых контейнеров, одноэлементных контейнеров и т.д. Вряд ли программисту захочется каждый раз снова заниматься рутинной работой по проверке и отладке этого кода для разных типов коллекций. Кроме того, придется создать целый набор новых методов, подобных приведенным ниже.
Object max(Comparable[] а)
Object max(ArrayList v)
Object max(LinkedLi st 1)
Именно здесь на помощь приходят обобщенные интерфейсы коллекций. Для эффективной реализации алгоритма необходимо определить минимальный интерфейс. В результате произвольный доступ посредством методов get и set занимает в этой иерархии более высокое место, чем простая итерация. Как показано во фрагменте кода для поиска наибольшего элемента связанного списка, для решения этой задачи не требуется использовать произвольный доступ. Для поиска достаточно применить простую итерацию. По этому, для создания метода max, который может работать с любым объектом, реализующим интерфейс Collection,нужно применить следующий обобщенный код.
public static Object max(Collection c)
if (c.isEmpty()) throw new NoSuchElementException();
Iterator iter = c.iterator();
Comparable largest = (Comparable)iter.next() ;
while (iter.hasNext() )
Object next = iter.next();
if (largest.compareTo(next) <0)
}largest = next;
return largest;
}
Теперь наибольшее значение связанного списка, списка массива и массива можно определить посредством одного и того же метода.
LinkedList 1;
ArrayList V;
Employee[J a;
largest = max(l);
largest = max(v);
largest = max(Arrays.asList(a) );
Это очень мощная и полезная концепция. Поэтому в стандартной библиотеке языка C++ содержатся десятки подобных полезных алгоритмов, применимых для коллекций общего вида. Библиотека Java не может похвастаться таким богатством алгоритмов, но в ней содержатся все основные алгоритмы сортировки, бинарного поиска, а также несколько других вспомогательных алгоритмов.
Сортировка и перемешивание элементов
Программисты с большим опытом, вероятно, помнят времена, когда использовались перфокарты, и алгоритмы сортировки приходилось создавать буквально вручную.
Теперь такие алгоритмы, конечно же, являются частью стандартных библиотек многих языков программирования и языкаJava в том числе.
Метод sort класса Collections сортирует коллекцию, реализующую интерфейс List.
List staff = new LinkedList();
// заполнить коллекцию ... ;
Collections.sort(staff);
В этом методе предполагается, что элементы списка реализуют интерфейс Comparable. Если список необходимо сортировать каким-либо иным способом, то в качестве второго параметра методу необходимо передать объект Comparator. (Компараторы рассматривались выше в разделе, посвященном сортированным множествам.) Вот как можно отсортировать список сотрудников по возрастанию их заработной платы.
Collections.sort(staff, new
Comparator()
{
public int compareTo(Object a, Object b) {
double salaryDifference = (Employee)a.getSalary()
— (Employee)b.getSalary();
if (salaryDifference < 0) return —1;
if (salaryDifference > 0) return 1; return 0;
});
Если список требуется отсортировать по убыванию, то следует воспользоваться статическим методом Collections .reverseorder(). Он возвращает компаратор, который, в свою очередь, возвращает значение b.compareTo (а). (Все сравниваемые объекты должны реализовывать интерфейс Comparable.) Например, показанный ниже метод сортирует все элементы списка staf в обратном порядке по отношению к порядку, который задан методом compareTo класса сравниваемых элементов.
Collections.sort(staff, Collections.reverseorder())
Здесь у читателя может возникнуть вопрос, а как же метод sort сортирует список. Обычно в литературе можно встретить алгоритмы сортировки для массивов с использованием произвольного доступа к элементам. Для списка такой тип доступа неэффективен. Чтобы отсортировать список, нужно воспользоваться одной из разновидностей сортировки слиянием (merge sort), который описывается, например, в книге Роберта Сэджвика (Robert Sedgewick), Algorithms in C++, [Addison-Wesley 1998, стр. 366369]. Но в реализации Java сортировка списка выполняется иначе. Все элементы списка просто копируются в массив, производится сортировка элементов массива (также с применением одного из вариантов сортировки слиянием) и копирование отсортированной последовательности в список.
Алгоритм сортировки слиянием, применяемый в библиотеке коллекций, работает несколькомедленнее,чемтрадиционныймногоцелевойалгоритм быстрой сортировки. Однако, он обладает огромным достоинством: такая сортировка стабильна, т.е. она не изменяет уже имеющийся порядок расположения равных элементов. Зачем это нужно? Предположим, список сотрудников, который уже отсортирован по их именам, нужно отсортировать по зарплате. При использовании алгоритма стабильной сортировки сохраняется прежний порядок расположения по имени для всех сотрудников с одинаковой зарплатой. Иначе говоря, в результате получается список, отсортированный вначале по зарплате, а затем по имени.
Коллекции могут не иметь реализации для всех своих "дополнительных" методов, поэтому все методы с параметрами-коллекциями должны описывать, в каких случаях можно передавать эти параметры алгоритмам. Например, очевидно, что список unmodifiableList нельзя передать алгоритму сортировки. Списки какого типа можно передавать? Согласно документации список должен быть изменяемым (modifiable), но не обязательно с изменением размера (resizable).
Эти термины определены следующим образом.
• Список является изменяемым (modifiable), если он поддерживает методset.
•Список позволяет изменятьразмер (resizable), еслионподдерживаетметодыidd иremove.
Класс Collections содержит метод-алгоритм shuffle, который производит действие, обратное сортировке, т.е. он перемешивает элементы списка случайным образом. В качестве параметров этого метода можно указать перемешиваемый список и генератор случайных чисел.
ArrayList cards = ...;
Collections.shuffle(cards);
Если указать список, который не реализует интерфейс RandomAccess, то метод shuffleскопирует элементы в массив, перемешает их и скопирует назад в список уже в новом порядке.
Программа, код которой приведен в листинге 2.5, заполняет массив-список 49 объектами Integer, содержащими числа от 1 до 49. Затем она перетасовывает список и выбирает 6 первых значений. В заключение она сортирует и выводит выбранные значения.
Листинг2.5. shufflaTest.java
/**
©version 1.00 1999-07-07
@author Cay Horstmann
*/
import java.util.*;
/**
Пример использования алгоритмов сортировки и перемешивания
*/ public class ShuffleTest
{
public static void main(String[] args) {
List numbers = new ArrayList(49);
for (int i =1; i<=49; i++)
numbers.add(newInteger(i));
Collections.shuffle(numbers) ;
List winningcombination =
Collections.sort(winningcombination);
System.out.printin(winningcombination) ; } )
java.util.Collections
Методы staticvoid sort (List elements) и staticvoid
sort(List elements, Comparator с) сортируют элементы списка, применяя стабильный алгоритм, который гарантированно выполняется за время O(nlogn), где и — длина списка.
Параметры:
elements сортируемый список
скомпаратор, используемый для сортировки
Методы static void shuffle (List elements) и static void shuffle(List elements, Random r) перемешивают элементы списка случайным образом. Алгоритм выполняется за время O(nxa(n)), где п — длина списка, а а(п) — среднее время доступа к элементу.
Параметры:
elements перемешиваемый список
rгенератор случайных чисел
Метод static Comparator reverseOrder () возвращает компаратор, который сортирует элементы в обратном порядке относительно порядка, заданного методом compareTo интерфейса Comparable.
Бинарный поиск
Для поиска объекта в массиве необходимо проверять все элементы до тех пор, пока не будет найден искомый элемент. Но в отсортированном массиве можно поступить следующим образом. Сначала проверить значение среднего элемента. Если оно больше требуемого, продолжить поиск в первой половине массива, если меньше — во второй. Таким образом, на каждом шаге сравнения область поиска сокращается вдвое и т.д. Например, если массив содержит 1024 элемента, то искомый элемент можно найти (или убедиться, что его нет) за 10 шагов, тогда как в линейном поиске для этого нужно в среднем 512 шагов, если искомый элемент существует, и 1024 шага. если такого элемента в массиве нет.
Этот алгоритм реализует метод binarysearch класса Collections. Следует отметить, что коллекция должна быть предварительно отсортирована, иначе алгоритм возвратит неверный результат. Для поиска элемента необходимо указать коллекцию (которая должна реализовывать интерфейс List, подробнее этот вопрос рассматривается ниже в примечании) и искомый элемент. Если коллекция не отсортирована с помощью метода compareTo интерфейса Comparable, то необходимо также указать объект- компаратор.
i =element);
i =element, comparator);
Если значение, возвращаемое методом binarySearch, больше или равно 0, то оно представляет собой индекс искомого элемента, т.е. объект с . get (i) содержит искомый элемент коллекции. Если возвращаемое значение меньше 0, то это значит, что искомый элемент в этой коллекции не найден. В таком случае отрицательное возвращаемое значение можно использовать как позицию, в которую следует вставить искомый элемент element, чтобы коллекция осталась отсортированной в заданном порядке. Она очень просто вычисляется следующим образом.
insertionpoint =—1;
Это не просто значение -i, иначе значение 0 имело бы неоднозначную трактовку. Другими словами, приведенная ниже операция добавляет элемент в нужную позицию отсортированной коллекции.
if (i < 0)
c.add(-i — 1, element);
Применение бинарного поиска имеет смысл лишь при возможности произвольного доступа. А если для доступа к среднему элементу7 связанного списка приходится использовать итерации, то все достоинства бинарного поиска теряются. Поэтому метод binarySearch автоматически использует алгоритм линейного поиска, если в его аргументе указан связанный список.
• В SDK версии 1.3 нет отдельного интерфейса для упорядоченной коллекции с эффективным произвольным доступом и метод binarySearch применяет достаточно неуклюжий прием. Он проверяет, наследует ли параметр-список класс AbstractSequentialList. Этот подход исправлен в SDK версии 1.4, где метод binarySearch проверяет, реализует ли параметр-список интерфейс RandomAccess. Если да, используется алгоритм бинарного поиска, а если нет — линейного.
LAipi if^ ava ■ut^ • Collections
• Методы static int binarysearch(List elements, Object key) и SSSl static int binarysearch(List elements, Object key, Comparator c) выполняют поиск ключа в отсортированном списке с использованием алгоритма линейного поиска, если элементы расширяют класс AbstractSe- quentialList, и алгоритма бинарного поиска в иных случаях. Эти методы гарантированно выполняются за время O(a(n) log и), где п— длина списка, а а(п)— среднее время доступа к элементу. Методы возвращают или индекс ключа в списке, или отрицательное значение i, если ключ отсутствует. В таком случае, чтобы коллекция осталась отсортированной в заданном порядке, ключ необходимо вставить в позиции -i -1 .
Параметры:
elements список, в котором ведется поиск
keyискомый объект
скомпаратор, используемый для сортировки элементов списка
Простые алгоритмы
Класс Collections содержит несколько простых, но очень полезных алгоритмов. Среди них следует упомянуть приведенный в начале этого раздела алгоритм поиска наибольшего значения в коллекции. Другие простые алгоритмы копируют элементы из одного списка в другой, присваивают всем значениям контейнера одно постоянное значение, обращают порядок элементов списка и многое другое. Зачем же такие простые алгоритмы включены в стандартную библиотеку? Ведь большинство программистов может с легкостью реализовать их в виде простых циклов. Дело в том, что алгоритмы значительно повышают читабельность кода. При чтении цикла, созданного другим разработчиком, вам потребуется самостоятельно расшифровать намерения разработчика и понять предназначение цикла. При беглом взгляде на вызов метода Collections .max сразу становятся очевидными те действия, которые выполняет этот фрагмент кода.
В следующем примечании описаны API-функции, которые реализуют некоторые простые алгоритмы класса Collections.
В
java.util.Collections
Методы static Object min(Collection elements) и static ।Object min(Collection elements, Comparator с) возвращают
наименьший элемент коллекции.
Методы static Object max(Collection elements) и static Object max(Collection elements, Comparator с) возвращают наибольшийэлементколлекции.
Параметры:
elements список, в котором ведется поиск
скомпаратор, используемый для сортировки элементов
Метод static void copy (List to, List from) копирует все элементы из исходного списка в те же позиции списка назначения. Длина последнего должна быть не меньше длины исходного.
Параметры:
toсписок назначения
fromисходный список
Метод static void fill (List 1, Object value) присваивает всем
элементам списка одно и то же значение value.
Параметры:
1список, заполняемый значением value
valueзначение заполнения
Метод static void replaceAll(List 1, Object oldValue, Object newValue) заменяет все элементы со значениями oldValue новыми значениями newValue (начиная с SDK 1.4).
Параметры:
список для обработки
oldValue заменяемое значение newValue заменяющее значение
• Методы static int indexOfSubList (List 1, List s) и static int 1as11ndexOfSubL ist (List 1, List s) возвращают соответственно индекс первого или последнего подчиненного списка 1, равного s, либо значение -1 , если в списке 1 не содержится такой подчиненный список s. Например, если список 1 содержит элементы [ S, t, a, r ]и список s содержит элементы [t, a, r], то эти методы возвращают индекс 1 (начиная с SDK 1.4).
Параметры:
список для поиска
sподчиненный список для сравнения
• Метод static void reverse (List 1) обращает порядок расположения элементов в списке. Например, обращение списка [t, а, Г ] дает в результате список [r, a, t] . Этот метод выполняется за время О(и), где п —длинасписка.
Параметры:
обращаемый список
• Метод static void rotate (List 1, int d) циклически прокручивает элементы в списке, перемещая элемент с индексом i в позицию (i + d) % 1 .size(). Например, прокручивание списка [t, а, Г ] на 2 позиции дает в результате список [a, r, t ]. Этот метод выполняется за время O(n), где и —длина списка (начиная c SDK 1.4).
Параметры:
прокручиваемый список
Создание собственных алгоритмов
При создании собственного алгоритма (или, фактически, любого метода с параметром-коллекцией) следует по возможности в максимальной степени использовать
интерфейсы, а не конкретные реализации. Предположим, что поле со списком JComboBox необходимо заполнить некоторым множеством строк. Традиционно такой метод заполнения значениямиреализуют следующим образом.
voidVector choices)
{ for (int i = 0; i <i++)
; }
При этом использование данного метода ограничено тем, что его параметром является вектор. Если для заполнения поля со списком значениями потребуется использовать другой контейнер, то его придется предварительно преобразовать в вектор. Именно поэтому для методов подобного рода желательно использовать обобщенную коллекцию.
Прежде всего следует определить наиболее общий интерфейс коллекции, который может решить нужную задачу. В данном случае требуется всего лишь обратиться ко всем элементам, а такой способностью обладает базовый интерфейс Collection. Вот как можно переписать методчтобы он работал с коллекцией любого типа.
voidcomboBox, Collection choices)
{ Iterator iter = while cornboBox. additem (iter .next) ) ; }
Теперь этот метод можно вызывать с вектором или даже массивом, для которого используется интерфейс-оболочка Arrays . asList.
Если использование интерфейсов коллекций в параметрах методов является столь удачной идеей, то почему же в библиотеке Java это правило не всегда соблюдается? Например, класс JComboBox имеет два конструктора.
items) items)
Это очень просто объясняется историческими причинами. Дело в том, что библиотека Swing была создана задолго до появления библиотеки коллекций. Вероятно, в будущих версиях API-интерфейса следует ожидать большей взаимосвязи с уже имеющейся библиотекой коллекций. На практике следует, по возможности, избегать использования векторов из-за дополнительных накладных расходов, связанных с синхронизацией.
При создании метода, который возвращает коллекцию, нет необходимости заменять тип возвращаемого значения на интерфейс коллекции. Пользователю этого метода, вероятно, может понадобиться максимально конкретный класс. Тем не менее, для собственного удобства можно возвращать интерфейс, а не класс, поскольку в таком случае этот метод можно реализовать повторно с другой коллекцией.
Рассмотрим методкоторый возвращает все элементы поля со спи
ском. Для этого достаточно просто возвратить коллекцию, которая использовалась для сбора элементов, например массив-список
ArrayListcomboBox)
{
ArrayList items = new ArrayList for (int i = 0; i <i++)
items.set(i, cornboBox.getltemAt(i)) ;
return items;
}
Однако, более удачный подход основан на использовании в качестве возвращаемого значения объекта-списка List.
List getAilltems(JComboBox comboBox)
В таком случае предоставляется возможность изменить реализацию в будущем. Например, вместо копирования элементов из поля со списком просто возвратить их представление. Этого легко можно добиться, возвращая анонимный класс AbstractList.
List getAilltems(final JComboBox comboBox) {
return new
AbstractList()
{
public Object get(int i)
{
return comboBox.getltemAt(i);
}
public int size()
{ return comboBox.getItemCount();
};
}
Конечно, это более мощная методика, но при ее использовании следует тщательно документировать все поддерживаемые "дополнительные" операции. В данном случае пользователю, вызывающему метод, следует сообщить, что возвращаемый объект является неизменяемым списком.
Унаследованные коллекции
В настоящем разделе обсуждаются классы-коллекции, существовавшие в языке Java с момента его основания: класс Hashtable и его подчиненный класс Properties, подчиненный класс Stack класса Vector и класс BitSet.
Класс Hashtable
Традиционный класс Hashtable служит той же цели, что и HashMap, и имеет, в сущности, такой же интерфейс. Как и методы класса Vector, методы класса Hashtable синхронизированы. Если вам не требуется обеспечить синхронизацию или совместимость с кодом для предыдущих версий платформы Java, то в таком случае следует воспользоваться классом HashMap.
Имя класса Hashtable содержит букву t в нижнем регистре. В операционной системе Windows при попытке использования имени HashTable можно получить странные сообщения об ошибках, поскольку файловая система Windows в отличие от компилятора Java не различает символы, записанные в разных регистрах.
Перечисления
В унаследованных коллекциях для обхода элементов коллекции используется интерфейс Enumeration, который имеет два метода: HasMoreElements и next Element. Эти методы полностью аналогичны методам hasNext и next интерфейса Iterator.
Например, метод elements класса Hashtable возвращает объект-перечисление значений в таблице.
Enumeration e = staff.elements();
while (e.hasMoreElements())
{
Employee e = (Employee) e.nextElement () ;
}
Иногда можно встретить унаследованный метод с параметром-перечислением. Например, статический методCollections. enumeration возвращает объект, вко- тором перечислены все элементы коллекции.
ArrayList streams = ...; // последовательность входных потоков Sequenceinput Stream in
= new SequenceInputStream(Collections.enumeration(streams) ) ;
// конструктор SequenceinputStream использует
// в качестве параметра объект-перечисление
В языке C++ в качестве параметров очень распространено использование итераторов. К счастью, при программировании для платформы Java немногие разработчики следуют такой методике. Гораздо разумнее передавать параметр-коллекцию, а не параметр-итератор, потому что получатели коллекции всегда смогут при необходимости получить итератор, но кроме этого в их распоряжении будут и все методы коллекции. В некоторых унаследованных программах можно встретить перечисления, так как они представляли собой единственный механизм реализации коллекций до создания архитектуры коллекций для платформы Java 2.
java.util.Enumeration
Метод boolean hasMoreElements () возвращает true, если в перечислении за текущей позицией итератора содержатся элементы.
Метод Object nextElement () возвращает элемент, следующий за текущей позицией итератора. Этот метод не следует вызывать, если метод hasMoreElements () возвращает значение false.
java.util.Hashtable
Метод Enumeration keys () возвращает объект-перечисление, содержащий ключи хэш-таблицы.
Метод Enumeration elements () возвращает объект-перечисление, содержащий значения хэш-таблицы.
java.util.Vector
Метод Enumeration elements О возвращает объект-перечисление, содержащий элементы вектора.
Множества свойств
Множество свойств (property set) представляет собой карту особого типа, которая обладает следующими основными характеристиками.
Ключи и значения являются строками.
Таблицу можно сохранить в файле и загрузить из файла.
Для используемых по умолчанию значений существует вторая таблица.
В платформе Java множество свойств реализовано в виде класса Properties.
Множества свойств полезны для указания конфигурационных параметров программ, например, для переменных окружения в операционных системах UNIX и DOS. На обычном персональном компьютере под управлением операционной системы DOS файл AUTOEXEC . BAT может содержать, например, такие настройки.
SET PROMPT=$p$g
SET TEMP=C:\Windows\Temp
SET CLASSPATH=c:\jdk\lib;.
Вот как можно представить эти настройки с помощью множества свойств в языке Java.
Properties settings = new Properties!) ;
settings.put("PROMPT", "$p$g");
settings .put ("TEMP", "C: WWindowsWTemp”) ;
settings.put("CLASSPATH", "c : \\jdkWlib; . ") ;
Для сохранения списка свойств в файле применяется метод store. В приведенном ниже примере множество свойств выводится в стандартный выходной поток System.out, а второй аргумент метода представляет собой комментарий, который включается в файл.
settings. store (System, out, "Environment settings");
В результате выполнения такого метода будет получен следующий результат.
#Environment settings
#Sun Jan 21 07:22:52 1996
CLASSPATH=c: WjdkWlib;
TEMP=C:\\Windows\\Temp
PROMPT=$p$g
Информация о системе
Еще одним примером универсального использования множества Properties является возможность извлечения информации о локальной системе, хранимой в объекте Properties, возвращаемом методом getProperties класса System. Приложения имеют полный доступ к этой информации, а аплеты Web-страницы при попытке доступа к определенным ключам генерируют исключительную ситуацию. Фрагмент кода, приведенный в листинге 2.6 в конце данного раздела, выводит на экран пары ключ/значение объекта Properties, в котором хранятся сведения о системе.
Ниже приводится пример результатов, полученных с помощью этой программы и содержащих все элементы объекта Properties. (Их значения, конечно, зависят от настроек используемого локального компьютера.)
java.runtime.name=Java(TM) 2 Runtime Environment, Standard Edition sun.boot.library.path=/usr/local/j2sdkl.4.0/jre/lib/i386
j ava.vm.version=l,4.p-beta-b65 java.vm.vendor=Sun Microsystems Inc. java.vendor.url=http://java.sun.com/ path.separator=:,
java.vm.name=Java HotSpot(TM) Client VM file.encoding. pkg=sun.io sun.os.patch.level=unknown,,
java.vm.specification.name=Java Virtual Machine Specification user.dir=/home/cay java.runtime.version=l.4.0-beta-b6 5 java.awt.graphicsenv=sun.awt.XIIGraphicsEnvironment os.arch=i386 java.io.tmpdir=/tmp line.separator=
java.vm.specification.vendor=Sun Microsystems Inc. j ava.awt.fonts= os.name=Linux
java.library.path=/usr/local/j2sdkl.4.0/jre/lib/i386:/usr/local/j2sdkl.4.0 /jre/lib/i386/native_threads/:/usr/local/j2sdkl.4.0/jre/lib/i386/client:/u sr/local/j2sdkl.4.0/jre/../Iib/i386
java.specification.name=Java Platform API Specification j ava.class.version=48.0 java.util.prefs.PreferencesFactory=java.util.prefs.FileSystem- Preferences Factory os.version=2.4.3-20mdk user.home=/home/cay user.timezone= java.awt.printerjob=sun.print.PSPrinterJob file.encoding=ISO-8859-l java.specification.version=l.4 user.name=cay java.class.path=.•
java.vm.specification.version=l.0 sun.arch.data,model=32 java.home=/usr/local/j2sdkl.4.0/j re java.specification.vendor=Sun Microsystems Inc. user.language=en java.vm.info=mixed mode java.version=l.4.0-beta java.ext.dirs=/usr/local/j2sdkl.4.0/jre/lib/ext sun.boot.class.path=/usr/local/j 2sdkl.4.0/jre/lib/rt.jar:/usr/local/j2sdkl
.4.0/jre/lib/il8n.jar:/usr/local/j2sdkl.4.0/jre/lib/sunrsasign.jar:/usr/lo cal/j2sdkl. 4.0/jге/lib/jsse.jar:/usr/local/j2sdkl.4.0/jre/lib/jce.jar:/usr /local/j2sdkl.4.0/jre/lib/charsets.jar:/usr/local/j2sdkl.4.0/jre/classes java.vendor=Sun Microsystems Inc. file.separator=/ java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport.cgi sun.cpu.endian=little sun.io.unicode.encoding=UnicodeLittle user.region=US sun.cpu.isalist=
По соображениям обеспечения безопасности аплетам разрешен доступ лишь к небольшому подмножеству этих свойств.
Данный список свойств системы демонстрирует недостатки формата Properties. Ключи не упорядочены в какой-то иерархической структуре, а пользоваться разделенными точками значениями достаточно сложно. Для решения подобной проблемы (т.е. для сохранения свойств в иерархическом формате) можно воспользоваться языком XML, который подробно рассматривается в главе 12. Альтернативный способ основан на использовании (начиная с SDK версии 1-4) класса Preferences, который позволяет связывать эти свойства с отдельными классами.
Листинг 2.6. Systemlnfo.java
import java.util. *;
/**
Пример вывода информации об используемой системе
*/
public class Systeminfo
{
public static void main(String args[])
{
Properties systemproperties =
Enumeration enum =
while (enum.hasMoreElements())
{
String key = (String)enum.nextElement();
System.out.printIn(key + "=" +
systemproperties.getProperty(key));
}
} }
Предлагаемые no умолчанию значения свойств
Множество свойств удобно использовать для настройки приложения. Например, в программе Customworld, код которой приведен в листинге 2.7, пользователи могут настроить внешний вид программы с помощью следующих настроек в конфигурационном файле Customworld. ini.
Размер окна.
Шрифт.
Размер курсора.
Цвет фона.
Строка сообщения.
Если пользователь не указывает явно значение какой-либо из настроек, то для этой настройки применяется предлагаемое по умолчанию значение.
Класс Properties обладает двумя механизмами использования значений по умолчанию. Во-первых, при подстановке строкового значения настройки можно указать значение, которое будет использовано по умолчанию, если данная настройка отсутствует в таблице свойств.
String font = settings.getProperty("FONT”, "Courier");
Если в таблице свойств содержится свойство "FONT", то для его переменной font задается указанная пользователем строка. В противном случае для переменной font используется предлагаемое по умолчанию значение "Courier".
Однако, вместо рутинной процедуры указания значений по умолчанию, при каждом вызове метода getProperty можно разместить все эти значения во втором множестве свойств и указать их в конструкторе таблицы со значениями подстановки.
Properties defaultSettings = new Properties();
defaultsettings.put("FONT", "Courier");
defaultsettings.put("SIZE", "10");
defaultsettings.put("MESSAGE", "Hello, World");
Properties settings = new Properties(defaultsettings) ;
FileinputStream sf = new Fileinputstream("Customworld.ini”);
settings.load(sf);
Более того, даже для этих значений по умолчанию можно указать их собственные значения по умолчанию, если задать второе множество свойств в конструкторе defaultsettings.Однако, в большинстве случаев это не требуется.
Класс Properties расширяет класс Hashtable, т.е. объекту Properties доступны все методы класса Hashtable. Некоторые из них полезно использовать для работы со свойствами. Например, метод size возвращает число возможных свойств (значения по умолчанию при этом не учитываются). Подобным образом метод keys возвращает перечисление всех ключей, кроме заданных по умолчанию, а для перечисления абсолютно всех ключей предусмотрен метод propertyNames. Метод put следует использовать с особой осторожностью, потому что он не проверят, добавляются ли в таблицу строки, а не какие-то другие объекты.
Применимо ли здесь правило наследования "IS-A"? Является ли хэш-таблицей каждое множество свойств? Не совсем. Справедливость этих предположений зависит от подробностей реализации. Возможно, множество свойств и удобно воспринимать как хэш-таблицу, но тогда она должна быть закрытой переменной экземпляра. В действительности, множество свойств использует две хэш-таблицы: для явно заданных значений и для предлагаемых по умолчанию значений. С точки зрения авторов, более логичным был бы следующий вариант определения класса.
class Properties
{
public String getProperty(String) { ... }
public void put(String, String) { ... }
private Hashtable nonDefaults;
private Hashtable defaults;
J
Это не значит, что авторы не рекомендуют использовать класс Properties из библиотеки коллекций Java. Ничего страшного не произойдет, если для метода put использовать только параметры-строки и не применять автоматическое наследование в своих программах.
Hello, Custom World
Внешний вид программы CustomWorld
На рис. 2.13 показан внешний вид настроенной программы CustomWorld. Для изменения ее внешнего вида нужным вам образом, например, как показано на рис. достаточно отредактировать файл CustomWorld.
Вот как выглядят ее текущие настройки.
#Environment settings
#Sun Jan 21 07:22:52 1996
FONT=Serif
SIZE=400 200
Custom World
COLOR=0 50 100
PTSIZE=36
Листинг2.7.
/**
1.20 1999-07-07 Cay Horstmann
*/
importj
import
import import import
/**
Пример приложения с настройками внешнего вида с использованием файла свойств
*/
public class CustomWorld {
public static voidargs)
{ = new
, j frame.show();
}
/ * *
Фрейм с сообщением. Его размеры, текст сообщения, шрифт и цвет задаются в файле свойств
*/
class CustomWorldFrame extends JFrame {
public CustomWorldFrame()
Properties defaultSettings =newProperties(); defaultsettings.put("FONT", "Monospaced");
defaultsettings.put("SIZE”, "300 200");
defaultsettings.put("MESSAGE", "Hello, World"); defaultsettings.put("COLOR", "05050");
defaultsettings.put("PTSIZE", "12");
Properties settings = new Properties(defaultsettings);
try
Fileinputstream sf
=newFileinputstream ("Customworld.ini”) settings.load(sf);
catch (FileNotFoundException e) {}
catch (IOException e) {}
StringTokenizer st = new StringTokenizer
(settings.getProperty("COLOR")); intred= Integer.parselnt(st.nextToken() ) ; int green = Integer.parselnt(st.nextToken()) ; int blue = Integer.parselnt(st.nextToken());
Color foreground = new Color(red, green, blue);
String name = settings.getProperty("FONT") ;
int size = Integer.parselnt(settings.getProperty("PTSIZE"));
Font f = new Font(name, Font.BOLD, size);
st=newStringTokenizer(settings.getProperty("SIZE"));
int hsize = Integer.parselnt(st.nextToken());
int vsize = Integer.parselnt(st.nextToken() ) ;
setsize(hsize,vsize);
setTitle(settings.getProperty("MESSAGE"));
JLabel label = new JLabel(settings.getProperty("MESSAGE"), Swingconstants.CENTER) ;
label.setFont(f) ;
label.setForeground(foreground);
java.util.Propertiesjava.util.PropertiesgetContentPane().add(label);
java.util.Properties
java.util.Properties
• Конструктор>PProperties (()Propertiesra defaults с)восйосздтва.ет пустой спи-
сок свойств для предлагаемых по умолчанию значений.
Параметры:
defaults предлагаемые по умолчанию значения
e Метод String getProperty (String key) возвращает строку, связанную с указанным ключом в основной таблице, а если он отсутствует — строку, связанную с указанным ключом в таблице с предлагаемыми по умолчанию значениями.
Параметры:
keyключ, связанный с возвращаемой строкой
Метод String getProperty(String key, String defaultvalue) возвращает строку, связанную с указанным ключом в основной таблице, а если он отсутствует — строку, связанную с указанным ключом в таблице с предлагаемыми по умолчанию значениями, а если ключ отсутствует и в этой таблице — строку по умолчанию, указанную во втором аргументе. Параметры: keyключ, связанный с возвращаемой строкой
defaultvalueстрока, возвращаемая при отсутствии ключа
Метод void load(Inputstream in) throws IOException загружает множество свойств из входного потока Inputstream.
Параметры:
inвходной поток
Стеки
Начиная с версии 1.0, стандартная библиотека Java поддерживает класс Stack с уже знакомыми методами push и pop. Однако, класс Stack расширяет класс Vector, что неудовлетворительно с теоретической точки зрения, потому что можно применять операции, несвойственные стеку, например, методы insert и remove для вставки и удаления значений в любых позициях, не только на вершине стека.
java.util.Stack
Метод void push (Object item) размещает значение в стеке.
Параметры:
itemдобавляемый элемент
Метод Obj ect pop () удаляет и возвращает значение с вершины стека. Этот метод не следует вызывать, если стек пуст.
Метод Object peek () возвращает значение с вершины стека, не удаляя его. Этот метод не следует вызывать, если стек пуст.
Битовые наборы
Класс BitSet платформы Java предназначен для хранения последовательности битов. (На самом деле в математическом смысле это вовсе не множество, и для него лучше бы подошел термин битовый вектор или битовый массив.) Он используется для эффективного сохранения последовательности битов, например, значений флажков. Его эффективность объясняется тем, что битовый набор содержит биты, запакованные в виде байтов. Именно поэтому его рекомендуется применять вместо массива- спискаArrayList, состоящего из объектов логического типа Boolean.
Класс BitSet предоставляет удобный интерфейс для чтения, установки-и сброса значений отдельных битов. Его использование позволяет избежать применения ма-
сок и других битовых операций, необходимых при хранении битов в переменных типа int или long.
Например, для объекта bucketOfBits типа BitSet показанный ниже get-метод возвращает значение true, еслигтый бит задан, и значение false— в противном случае.
bucketOfBits.get(i)
Аналогично, показанный ниже set-метод задает г-тый бит.
bucketOfBits.set(i)
Наконец, показанный ниже метод clearсбрасывает г-тый бит.
bucketOfBits.clear(i)
1 I
Шаблон bitset в языке C++ обладает той же функциональностью, что и класс Bitset в языке Java.
rr^VIjava. util. Bitset
|A rill
LU • Конструктор BitSet (int nbits) создает битовый набор.
Параметры: nbitsисходное число битов
Метод int length () возвращает "логическую длину" битового набора: 1 + индекс высшего бита набора.
Метод boolean get (int bit) возвращает значение указанного бита.
Параметры: bitпозиция искомого бита
Метод void set (int bit) устанавливает значение указанного бита.
Параметры: bitпозиция устанавливаемого бита
Метод void clear (int bit) сбрасывает значение указанного бита.
Параметры: bitпозиция сбрасываемого бита
Метод void and (BitSet set) выполняет операцию логического умно- жения(AND)ДЛЯданногоиуказанногобитовыхнаборов.
Параметры: setбитовый набор, предназначенный для выполнения логиче
ской операции с текущим битовым набором
Методvoid or (BitSet set) выполняет операцию логического сложения (OR) для данного и указанного битовых наборов.
Параметры: setбитовый набор, предназначенный для выполнения логиче
ской операции с текущим битовым набором
Метод void xor (BitSet set) выполняет операцию логического исключающего сложения (OR) для данного и указанного битовых наборов.
Параметры:
setбитовый набор, предназначенный для выполнения логиче
ской операции с текущим битовым набором
Метод void andNot (BitSet set) сбрасывает значения тех битов в данном наборе, которые установлены в указанном наборе set.
Параметры:
setбитовый набор, предназначенный для выполнения логиче
ской операции с текущим битовым набором
Решето Эратосфена
В качестве примера применения битовых наборов рассмотрим реализацию алгоритма "решето Эратосфена" для поиска простых чисел. (Простым называют число, делителями которого являются лишь оно само и 1. Например, простыми являются числа 2, 3 и 5. Решето Эратосфена представляет собой один из первых методов поиска таких чисел.) Это не очень эффективный алгоритм поиска, но по определенным причинам он получил широкое распространение для тестирования производительности компилятора. (Хотя на самом деле он не совсем подходит и для этой цели, так как при этом используются, в основном, битовые операции.)
Представленная здесь программа, код которой приведен в листинге 2.8, вычисляет все простые числа в интервале от 2 до 1000000. (В этом интервале всего 78498 простых чисел, поэтому вряд ли стоит выводить все эти числа.) Сначала программа будет выполняться очень медленно, но затем ее скорость возрастет.
Основная идея этой программы заключается в прохождении по битовому набору, содержащему миллион битов. Вначале нужно установить биты для всех чисел из указанного диапазона, а затем необходимо последовательно сбрасывать те биты, делителями которых являются известные простые числа. Позиции битов, которые останутся установленными по завершении этого процесса, сами будут простыми числами. Листинг 2.8 содержит код этой программы на языкеJava, а листинг 2.9 — на языке C++.
Хотя этоталгоритм нельзя назватьхорошим тестом производительности компилятора, все же мы замерили время его выполнения на компьютере IBMThinkpad 233 МГцс оперативной памятью 224 Мбайт под управлением операционной системы Linux Mandrake 8. Итак, для компилятора д++ версии 2.96 языка C++ программа была выполнена за 1740 миллисекунд, а для компилятора JDK версии 1.4 языка Java — за 1133миллисекунд.
Этот тест запускался для всех пяти изданий данной книги и в последних двух изданиях язык Java опередил язык C++. Однако, следует отметить, что последние результаты объясняются плохой реализацией шаблона bitset в компиляторе д++ языка C++. При авторской реализации шаблона bitset время выполнения программы на C++ сократилось до 630 мс.
Листинг 2.8. Sieve.java
/**
©version 1.20 1999-07-03
©author Cay Horstmann
*/
import java.util.*;
/**
Пример реализации теста на основе алгоритма "решето Эратосфена", который вычисляет все простые числа в диапазоне от 0 до 1000000 */ public class Sieve {
public static final boolean PRINT = false;
public static void main(String!] s)
{
int n = 1000000;
long start = System. currentTimeMillis () ;
BitSet b = new BitSet(n);
int count = 0;
int i;.
for (i = 2; i <= n; i++) b.set(i);
i = 2;
while (i * i <= n)
{
if (b.get(i))
{
if (PRINT) System.out.printin(i) ; count++;
int k = 2 * i;
while (k <= n)
{
b.clear(к);
k += i;
}
}
while (i <= n)
{ if (b.get(i)) {
if (PRINT) System.out.printIn(i) ;
} count++;
}
long end = System.currentTimeMillis();
System.out.printin(count + " primes");
System.out.println((end — start) + " milliseconds" ) ; }
Листинг 2.9. Sieve.cpp
/**
©version 1.20 1999-07-03 author Cay Horstmann
*/
#ifndef AVOID_STANDARD_BITSET
#include <bitset>
#else
template<int N>
class bitset
{
public:
bitset() :-1) /8+1]) {}
bool test(int n)
{
return (bits[n» 3] & (1« (n& 7))) != 0;
}
void set(int n)
{ bits[n» 3] |=1« (n&7);
}
void reset(int n)
{
bits[n» 3] &= ~(1« (n& 7));
private:
} ; char* bits;
#endif
#include <iostream>
#include <ctime>
using namespace std;
int main()
{
const int N = 1000000;
clock_t cstart = clock() bitset<N + 1> b;
int count = 0;
int i;
for (i=2; i<=N; i++) b.set(i) ;
i=2;
while (i*i<=N)
if (b.test(i))
intk=2 * i; while (k<=N) { b.reset(k);
k+=i;
} }
} '
for (i=2; i<=N; i++)
if (b.test(i))
{
#ifdef PRINT
cout «i<< "\n" ;
#endif
count++;
}
}
clock_t cend = clock();
double millis = 1000.0
* (cend - cstart) / CLOCKS_PER_SEC;
cout << count << " primes\n"
<< millis « " milliseconds\n";
return 0;
cUul
Работа
с сетями
В этой главе...
т Подключение к серверу
т Создание серверов
т Более сложные способы работы с сокетами
т URL-соединения
т Форма ввода данных
• Сбор информации в Web-среде
Язык программирования Java является наиболее распространенным инструментом для организации соединения между компьютерами в среде Internet и корпоративных сетях intranet. Опытные программисты, которые использовали для создания сетевых соединений языки С и C++, будут приятно удивлены той простотой, с которой то же самое можно сделать с помощью языкаJava. Например, как уже было показано в первом томе при описании аплетов, для открытия Web-страницы по известному URL-адресу достаточно просто передать URL-адрес методу showDocument класса AppletContext.
Эта глава начинается с описания основ организации сетевых соединений, а затем продолжается рассмотрением аплетов, которые уже были представлены в томе I. В остальной части этой главы рассматриваются более сложные вопросы работы с сетями, например, показано как в Java-программе можно организовать работу с CGI- программой сервера. Особое внимание уделяется использованию комбинации аплета и сервлета для обработки информации в сети Internet.
Первая часть этой главы предназначена для читателей, которые не имеют достаточного опыта сетевого программирования. Поэтому опытным программистам, которые имеют опыт создания программ с протоколами TCP/IP, сокетами и портами, можно пропустить вводную часть вплоть до первого примера программы без ущерба для понимания остальной части материала в этой главе. По мере приближения к концу этой главы код в примерах будет усложняться, и для его понимания потребуется более глубокая подготовка.
Подключение к серверу
Перед созданием первой сетевой программы рекомендуется познакомиться с прекрасным инструментом отладки сетевых программ, а именно с программой telnet.exe, которая входит в состав операционной системы UNIX и Windows 95/Windows NT (в несколько упрощенном варианте). Учтите, что она не является обязательным компонентом операционной системы, поэтому проверьте, установлена ли она на вашем компьютере. Если ее невозможно запустить из командной строки, то в таком случае нужно еще раз запустить программу установки и выбрать ее в списке предлагаемых для установки компонентов.
Программа telnet может использоваться не только для соединения с удаленным компьютером и для работы с электронной почтой, но и для работы с другими сетевыми сервисами. Ниже приводится один из примеров необычного использования этой программы.
Введите в командной строке приведенную ниже команду.
telnet time-A.timefreq.bldrdoc.gov 13
На рис. 3.1 показан пример ответной реакции сервера, которая в режиме командной строки будет иметь вид показанной ниже текстовой строки.
50692 97-09-01 21:43:15 50 0 0 50.0 UTC(NIST) *
8
J FHe Edit settings.,Help - -.. I.. , 1.^..»
~$ telnet t1me-A.timefreq.Ыdrdoc.gov 13
Trying 132.163.4.102...
Connected to t1me-A.t1mefreq.bldrdoc.gov.'
Escape character 1 s
52088 01-06-28 14:35113 50 0 0 644.8 UTC(NIST) •
Connection closed by foreign host.
~$ D
Рис. 3.1.Результат работы сервиса "определения времени"
Что же произошло в этом случае? Программа telnet подключилась к сервису "определения времени", который работает на большинстве компьютеров под управлением операционной системы UNIX. Указанный в этом примере сервер находится в Национальном институте стандартов и технологий (National Institute of Standards) в Боулдере, штат Колорадо, США. Его системное время синхронизировано с цезиевыми атомными часами. (Конечно, полученное значение текущего времени будет не совсем точным, из-за задержек, связанных с передачей данных в сети.) По договоренности сервис "определения времени" всегда связан с портом 13.
Рис. 3.2. Схема подключения клиента к порту сервераРис. 3.2. Схема подключения клиента к порту сервераПри описании сетевых технологий портом называется не какое-то физическое устройство, а абстрактное понятие, которое означает организацию соединения между сервером и клиентом, как показано на рис. 3.2.
Рис. 3.2. Схема подключения клиента к порту сервера
Рис. 3.2. Схема подключения клиента к порту сервера
Программное обеспечение сервера постоянно работает на удаленном компьютере и ожидает поступления сетевого трафика по порту 13. При получении операционной системой на удаленном компьютере сетевого пакета с запросом на подключение к порту 13 активизируется процесс-слушатель сервера и устанавливается соединение. Такое соединение может быть прервано одним из его участников.
После запуска программы telnet с параметром time-A.timefreq.bldrdoc . gov и портом 13 независимое сетевое программное обеспечение преобразует строку time-A. timefreq. bldrdoc . gov в IP-адрес 132.163.135.130. Затем программное обеспечение посылает запрос на соединение с заданным компьютером по порту 13. После установления соединения программа на удаленном компьютере посылает обратно строку с данными, а потом закрывает соединение. В общем случае клиенты и серверы до закрытия соединения могут участвовать в более сложных диалогах.
Вот еще один более интересный эксперимент, который следует выполнить с отключением эхо-ответа введенных символов. В операционной системе Windows это можно сделать, указывая в режиме командной строки команду set LOCAL_ECHO off. Затем выполните перечисленные ниже действия.
Подключитесь к серверу j ava.sun. com no порту 80.
Введите строку GET / HTTP / 1.0 точно в показанном здесь виде без использования символа забоя.
Теперь дважды нажмите клавишу <Enter>.
На рис. 3.3 показана ответная реакция сервера в окне программы telnet. Она имеет уже знакомый нам вид страницы текста в формате HTML, а именно основной Web-страницы компании Sun, посвященной технологиям Java.
Именно так обычный Web-броузер получает все Web-страницы.
1 1 СУ </А > *1 1 СУ </А > *Ed,; SWn3« Ha.pл ^~- -height-"30" border-"o' ALT-"sun Microsystems, Inc."?(/A?(BR? (FONT SIZE-"-2"?Copyright © 1995-2001:<A HREF-"http://www.sun.com">Sun Microsystems, Inc.</AxBR>All Rights Reserved. <a href-"http://www.sun.com/share/text/termsofuse.html">Terms of Use</a?. <A HREF-"http://www.sun.com/pr1vacy"?Pr1vacy8nbsp; PoEd,; SWn3« Ha.pл ^~- -height-"30" border-"o' ALT-"sun Microsystems, Inc."?(/A?(BR? (FONT SIZE-"-2"?Copyright © 1995-2001:<A HREF-"http://www.sun.com">Sun Microsystems, Inc.</AxBR>All Rights Reserved. <a href-"http://www.sun.com/share/text/termsofuse.html">Terms of Use</a?. <A HREF-"http://www.sun.com/pr1vacy"?Pr1vacy8nbsp; Po**</TD></TR>(/TABLE?</TD></TR>(/TABLE?</FONT></TDx/TR></TABLE></FONT></TDx/TR></TABLE>< I — End Footer< I — End FooterSegment —>Segment —><I--end server<I--end serverside include for footer —>side include for footer —>"I"I
1 1 СУ </А > *
1 1 СУ </А > *
Ed,; SWn3« Ha.pл ^~- -
height-"30" border-"o' ALT-"sun Microsystems, Inc."?(/A?(BR? (FONT SIZE-"-2"?
Copyright © 1995-2001:
<A HREF-"http://www.sun.com">Sun Microsystems, Inc.</AxBR>
All Rights Reserved. <a href-"http://www.sun.com/share/text/termsofuse.html">Terms of Use</a?. <A HREF-"http://www.sun.com/pr1vacy"?Pr1vacy8nbsp; Po
Ed,; SWn3« Ha.pл ^~- -
height-"30" border-"o' ALT-"sun Microsystems, Inc."?(/A?(BR? (FONT SIZE-"-2"?
Copyright © 1995-2001:
<A HREF-"http://www.sun.com">Sun Microsystems, Inc.</AxBR>
All Rights Reserved. <a href-"http://www.sun.com/share/text/termsofuse.html">Terms of Use</a?. <A HREF-"http://www.sun.com/pr1vacy"?Pr1vacy8nbsp; Po
*
*
</TD>
</TR>
(/TABLE?
</TD>
</TR>
(/TABLE?
</FONT>
</TDx/TR>
</TABLE>
</FONT>
</TDx/TR>
</TABLE>
< I — End Footer
< I — End Footer
Segment —>
Segment —>
<I--end server
<I--end server
side include for footer —>
side include for footer —>
"I
"I
</BODY>
(/HTML?
Connection closed by foreign host.
~$D
Рис.3.3. Использование программы telnet для доступа к HTTP-порту
Программа, показанная в листинге 3.1, выполняет точно такие же действия, т.е. подключается к порту и распечатывает полученные данные.
Листинг 3.1. SocketTeat.java
@version 1.10 1997-06-27
eauthor Cay Horstmann
import java.io.*; import java.net.*;
/**
Пример соединения сокета с атомными часами в Боулдере, штат Колорадо, и вывода на экране текущего времени сервера
*/
public class SocketTest
public static void main(String!] args)
try
Socket s = new Socket("time-A.timefreq.bldrdoc . gov”, 13);
BufferedReader in = new BufferedReader
(new InputStreamReader(s.getlnputStream())); boolean more = true; while (more) {.,
String line = in.readLine(); if (line == null) more = false;
else
System.out.printin(line) ;
}
catch (IOException e)
{
e.printStackTrace() ;
}
}
)
Это очень простая программа, но еще до анализа двух ее ключевых строк обратите внимание на то, что в ней импортируется пакетjava.netи обрабатываются исключительные ситуации при выполнении операций ввода/вывода в блоке try/catch. (При работе с сетями могут возникать сбои, поэтому во многих сетевых методах предусмотрены сообщения об ошибках ввода/вывода. Для успешной компиляции кода эти сообщения необходимо перехватывать и обрабатывать соответствующим образом.)
Этот пример содержит следующие две ключевые строки.
Socket s = new Socket("time-A.timefreq.bldrdoc.gov", 13);
BufferedReader in = new BufferedReader
(new InputStreamReader( s. getlnputStream ()) ) ,•
Первая строка позволяет открыть сокет. Сокет — это абстрактное понятие, которое обозначает программное обеспечение, позволяющее организовать операции обмена данными между программами. Конструктору сокета передается адрес удаленного сервера и номер порта. При неудачном соединении возникает исключительная ситуация UnknownHostException, а при наличии каких-то других проблем— исключительная ситуация IOException. Класс UnknownHostException является производным классом от IOException, поэтому в этом простом примере обрабатывается только исключительная ситуация базового класса.
После открытия сокета метод getlnputStream класса java.net. Socket возвращает объект потока Inputstream, который можно использовать как обычный файл. (Подробнее об этом см. главу 12 тома I.) После получения потока программа приступает к выполнению следующих действий.
Считывает все посланные сервером символы с помощью методаreadLine.
Выводит каждую строку на стандартном устройстве вывода.
Настоящий процесс продолжается до тех пор, пока не закончится поток или не отсоединится сервер. В таком случае метод readLine возвратит null.
Эта программа может работать только с очень простыми серверами, которые предоставляют подобный сервис "определения времени". В более сложных сетевых программах клиент посылает запрос на получение данных серверу, а сервер может в течение некоторого времени поддерживать это соединение без отключения сразу же после получения запроса. Примеры воплощения такого поведения будут представлены ниже в этой главе.
Класс Socket очень удобен в работе, потому что скрывает все сложные подробности установления сетевого соединения и передачи данных в сети. А пакет j ava.net предоставляет TOTже программный интерфейс, который используется для работы с файлами.
Создание серверов
Теперь после создания клиента, который может получать данные из сети, попробуем создать простой сервер, который может посылать данные в сети. После запуска программы-сервера она переходит в режим ожидания присоединения клиентов к своему порту. Выберите номер порта 8189, который не используется никакими стандартными устройствами. Приведенная ниже команда позволяет создать сервер с портом 8189.
ServerSocket s = new ServerSocket(8189);
Затем показанная ниже команда сообщает программе, что она должна ожидать подключения клиентов к заданному порту.
Socket incoming = s. accept О;
Сразу после подключения клиента к порту с помощью переданного по сети корректного запроса этот метод посылает объект Socket, который представляет установленное соединение. Данный объект можно использовать для чтения входных и записи выходных данных так, как показано ниже.
BufferedReader in = new BufferedReader
(new InputStreamReader(incoming.getlnputStream())) ;
PrintWriter out = new Printwriter
(incoming.getOutputStream(), true /* autoFlush */);
Все данные выходного потока сервера становятся данными входного потока клиента и наоборот, все данные выходного потока клиента становятся данными входного потока сервера.
Во всех примерах этой главы текстовые данные в потоках будут передаваться через сокеты. Таким образом, потоки будут выполнять роль читателей и писателей данных с помощью метода readLine (он определен в классе Buf feredReader, но не в классе InputStream) и метода print (он определен в классе PrintWriter, но не в классе Outputstream) соответственно. Для передачи бинарных данных необходимо преобразовать эти потоки в потоки DatalnputStream и DataOutputStream. А для передачи объектов сериализации следует использовать потоки Objectinputstreamи Obj ectOutputStream.
Допустим, что программа-клиент отправила приветствие:
out.printin("Hello! Enter BYE to exit.");
При использовании утилиты telnet для подключения к программе-серверу по порту 8189 показанное выше приветствие будет отображено в окне терминала.
В этой простой программе-сервере построчно считываются входные данные, посланные программой-клиентом, и отображаются на экране в режиме эхо-ответа. Таким образом данный пример демонстрирует получение входных данных от программы-клиента. Программа-сервер должна обработать полученные данные и дать адекватный ответ.
String line = in.readLine();
if (line != null)
{ out.printin("Эхо-ответ: " + line);
if (line.trim().equals("BYE")) done = true;
}
else done = true;
Наконец, следует закрыть используемый сокет.
incoming.close();
Вот и все! Любая программа-сервер, например, Web-сервер, выполняет такой же цикл основных действий.
Получение команды от программы-клиента ("дайте мне нужную информацию") извходногопотокаданных.
Поиск и извлечение заданной информации.
Посылка клиенту найденной информации в выходном потоке данных.
В листинге 3.2 показан полный текст описанной выше программы-сервера.
Листинг 3.2.BchoServer.java
/**
* ©version 1.10 1997-06-27
* ©author Cay Horstmann
*/
import java.io.*;
import java.net.*;
/**
Пример простого сервера, который отслеживает порт 8189 и возвращает клиенту все введенные им данные в виде эхо-ответа
*/
public class EchoServer {
public static void main(String[] args ) {
try {
// Создать сокет
ServerSocket s = new Serversocket(8189) ;
// Перейти в режим ожидания // запросов со стороны клиентов Socket incoming = s.accept( );
BufferedReader in = new BufferedReader
(new InputStreamReader(incoming.getlnputStream())); Printwriter out = new PrintWriter
(incoming.getOutputStream(),true /* autoFlush */) ;
out.println( "Hello! Enter BYE to exit." );
// Эхо-ответ запроса клиента boolean done = false;
while (!done)
{
String line = in.readLine();
if (line == null) done = true; else
{
out.printIn("Echo: " + line);
if (line.trim().equals("BYE")) done = true;
}
}
incoming.close() ;
}
catch (Exception e)
{
e.printStackTrace();
}
Для проверки работоспособности программы ее нужно откомпилировать и запустить. Затем необходимо подключиться с помощью утилиты telnet к серверу 127.0.0.1и порту 8189.
IP-адрес 127 . 0.0.1называется локальным адресом обратной связи (local loopback address), который обозначает локальный компьютер. Поскольку эхо-ответ будет отображаться на экране локального компьютера, то именно к нему и надо подключиться в данном примере.
При использовании коммутируемого подключения для выполнения этого примера потребуется его установить. Дело в том, что даже при взаимодействии с локальным компьютером в нашем случае требуется загрузить необходимое сетевое программное обеспечение.
Теперь любой пользователь сети может получить доступ к программе-серверу при условии, что ему известен IP-адрес вашего компьютера и номер порта.
При подключении к этому порт)' будет получено сообщение, показанное нарис. 3.4.
Hello! Enter BYE to exit.
Введите любую фразу и получите ее в виде эхо-ответа. Для отключения от программы-сервера введите BYE (все символы в верхнем регистре).
Ябш^уммиея-..,».-». Lj^u^k^—-.I -local hostTrying127.0.0.1 ...Connectedto 1 ocal host. 1 ocal domain.("Escape character 1s 'л]'.| Hei 1 о I Enter BYE to exit.| Hell о there! How are you today?Echo: Hello there I How are you today?iIamfine, thanks. And you?Ucbo: I am fine, thanks, And you?Ябш^уммиея-..,».-». Lj^u^k^—-.I -local hostTrying127.0.0.1 ...Connectedto 1 ocal host. 1 ocal domain.("Escape character 1s 'л]'.| Hei 1 о I Enter BYE to exit.| Hell о there! How are you today?Echo: Hello there I How are you today?iIamfine, thanks. And you?Ucbo: I am fine, thanks, And you?«^^^^й&ашшШ^МаЦ.«^^^^й&ашшШ^МаЦ.
Ябш^уммиея-..,».-». Lj^u^k^—-.
I -
local host
Trying127.0.0.1 ...
Connectedto 1 ocal host. 1 ocal domain.
("Escape character 1s 'л]'.
| Hei 1 о I Enter BYE to exit.
| Hell о there! How are you today?
Echo: Hello there I How are you today?
iIamfine, thanks. And you?
Ucbo: I am fine, thanks, And you?
Ябш^уммиея-..,».-». Lj^u^k^—-.
I -
local host
Trying127.0.0.1 ...
Connectedto 1 ocal host. 1 ocal domain.
("Escape character 1s 'л]'.
| Hei 1 о I Enter BYE to exit.
| Hell о there! How are you today?
Echo: Hello there I How are you today?
iIamfine, thanks. And you?
Ucbo: I am fine, thanks, And you?
«^^^^й&ашшШ^МаЦ.
«^^^^й&ашшШ^МаЦ.
C•o$n0nection closed by foreign host.
Рис. З.4. Пример работы с программой-сервером с эхо-ответом
Обслуживание несколькихклиентов
В предыдущем простом примере программы-сервера не предусмотрена возможность одновременного подключения сразу нескольких программ-клиентов. Обычно программа-сервер работает на компьютере-сервере, а программы-клиенты могут одновременно подключаться к ней с помощью сети Internet из любой точки мира. Если на сервере не предусмотрена обработка одновременного подключения нескольких клиентов, то это приведет к тому, что один клиент может монополизировать доступ к данной программе-серверу в течение длительного времени. Во избежание таких ситуаций следует прибегнуть к помощи потоков.
Для каждого нового соединения с сокетом, т.е. при успешной обработке запроса на соединение, будет запущен новый поток, который позаботится об организации взаимодействия между программой-сервером и данной программой-клиентом. Для этого в программе-сервере следует организовать показанный ниже цикл.
while (true)
{ Socket incoming = s.accept();
Thread t = new ThreadedEchoHandler(incoming) ;
t.start();
}
Класс ThreadedEchoHandler является производным от класса Thread и содержит представленный ниже цикл обработки взаимодействия с программой-клиентом в своем методе run.
class ThreadedEchoHandler extends Thread
{...
public void run()
{ try
{ BufferedReader in = new BufferedReader
(new InputStreamReader(incoming.getlnputStream() ) ) ;
Printwriter out = new Printwriter
(incoming.getOutputStream(), true /* autoFlush */);
String line;
while ((line = in.readLine()) != null)
{ код обработки строк
}
incoming.close();
}
catch (Exception e)
{ код обработки исключительной ситуации
}
}
Теперь несколько программ-клиентов могут одновременно подключаться к серверу, потому что для каждого соединения создается новый поток. Это можно легко проверить. Скомпилируйте и запустите программу-сервер, код которой приведен в листинге 3.3. Откройте несколько окон утилиты telnet, как показано на рис. 3.5. Теперь каждое отдельное окно утилиты telnet может независимо взаимодействовать с программой-сервером. Для закрытия данного соединения и окна утилиты telnet нажмите комбинацию клавиш <Ctrl+C>.
З.Я. Пример одновременного доступа к многопоточной программе-серверу с эхо-ответом
-
1.10 1997-06-27
Cay
j
j
Пример создания многопоточного сервера, который отслеживает порт 8189 и возвращает клиенту все введенные им данные в виде эхо-ответа
*/
public class ThreadedEchoServer
{
public static voidargs )
try
{ int i = 1;
ServerSocket s = new
for
Socket incoming = s.accept( );
System.out.printin("Spawning " + i);
Thread t = new ThreadedEchoHandler(incoming, i); t.start();
} ' }
catch (Exception e) {
e.printStackTrace() ; }
}
}
/**
Этот класс управляет входным потоком клиента для одного соединения с сокетом */ class ThreadedEchoHandler extends Thread {
/**
Создание обработчика
@param i the incoming socket
@param с the counter for the handlers (used in prompts) */
public ThreadedEchoHandler(Socket i, int c) {
incoming = i; counter = c;
public void run()
try
{
BufferedReader in=newBufferedReader
(newInputStreamReadei; (incpming.getlnputStream () )) ;
Printwriter out = new PrintWriter
(incoming.getOutputStream()true /* autoFlush */) ;
out.println( "Hello! Enter BYE to exit." );
boolean done = false;
while (I done)
{
String str = in.readLine();
if (str == null) done = true,-
else
{
out.printin("Echo ("+counter+") : "+str);
if (str.trim().equals("BYE")) done=true;
incoming.close();
catch (Exception e)
{
е .printstackTrace {) ;
}
}
private Socket incoming;
private int counter;
)
API
™!_~k • Конструктор ServerSocket (int port) throws lOException создает
■weeсокет сервера, который наблюдает за заданным портом.
Параметры:
portномер порта
Метод Socket accept () throws IOException ожидает до момента соединения и блокирует (т.е. переводит в состояние бездействия) текущий поток до тех пор, пока не будет установлено соединение. Этот метод возвращает объект Socket, с помощью которого программа-сервер взаимодействует с подключенным клиентом.
Методvoid close() throws IOException закрывает сокет программы-сервера.
Отправка электронной почты
В этом разделе приведен пример практического использования программирования сокетов для отправки электронной почты удаленному компьютеру.
Для отправки электронной почты необходимо установить соединение с сокетом по порту 25, который обычно используется для SMTP-протокола. SMTP-протокол (Simple Mail Transport Protocol) описывает формат электронных сообщений. На компьютерах под управлением операционной системы UNIX SMTP-сервис обычно реализуется с помощью демона sendmail. Однако, сервер должен быть готов к приему запроса на соединение. Ранее серверы с демонами sendmail могли принимать любые электронные сообщения, но в настоящее время из-за большого объема спама (т.е. массовой рассылки электронных сообщений с бесполезным содержанием) в большинстве серверов встроена проверка и допуск почтовых сообщений только от разрешенных пользователей, доменов или IP-адресов.
Сразу после соединения с сервером следует послать заголовок сообщения (в формате SMTP, который достаточно просто создать), а затем и текст сообщения так, как показано ниже.
Откройте сокет для вашего компьютера.
Socket s = new Socket("mail.yourserver.com", 25);
// 25 — номер SMTP-порта
Printwriter out = new Printwriter(s.getOutputSteeam()) ;
В выходном потоке передайте следующую информацию.
HELD компьютер-отправитель
MAIL FROM: адрес отправителя
RCPT ТО: адрес получателя
DATA
почтовое сообщение
(любое количество строк)
QUIT
В спецификации протокола SMTP (документ RFC 821) требуется, чтобы строки заканчивались символами /г и /п.
torn.president@*whitehouse gov ,11Го:■ |fiarsiman»csjjsu_edu,И_Dear Ar. Horstmann:It is my pleasure o invite you to a black-tie reception on the front lawn of the hite house I on April 1, 2002.torn.president@*whitehouse gov ,11Го:■ |fiarsiman»csjjsu_edu,И_Dear Ar. Horstmann:It is my pleasure o invite you to a black-tie reception on the front lawn of the hite house I on April 1, 2002.На большинстве SMTP-серверов эта информация не проверяется, т.е. можно передать любую информацию об отправителе. (Имейте в виду, что любой желающий может подсоединиться к SMTP-серверу и послать поддельное сообщение, например, с обратным адресом president@whitehouse.gov с приглашением посетить прием на лужайке Белого дома.)
torn.president@*whitehouse gov ,11
Го:■ |fiarsiman»csjjsu_edu,И
_
Dear Ar. Horstmann:
It is my pleasure o invite you to a black-tie reception on the front lawn of the hite house I on April 1, 2002.
torn.president@*whitehouse gov ,11
Го:■ |fiarsiman»csjjsu_edu,И
_
Dear Ar. Horstmann:
It is my pleasure o invite you to a black-tie reception on the front lawn of the hite house I on April 1, 2002.
220 email sjsu edu ESMTP Sendmail 8 9 +SuM НЕЮ localhost localdomain^250 mail.sjsu edu Hello pe-66-1-145-68 cj MAIL FROM: <president@whrtehou$e.gov>Wjj220 email sjsu edu ESMTP Sendmail 8 9 +SuM НЕЮ localhost localdomain^250 mail.sjsu edu Hello pe-66-1-145-68 cj MAIL FROM: <president@whrtehou$e.gov>WjjРис. 3.6 Программа MailTestРис. 3.6 Программа MailTestВ листинге 3.4 приводится очень простой пример почтовой программы. Как показано на рис. 3.6, с ее помощью можно ввести адрес отправителя, адрес получателя, имя SMTP-сервера, текст сообщения, а потом отправить данное сообщение с помощью кнопки Send.
220 email sjsu edu ESMTP Sendmail 8 9 +SuM НЕЮ localhost localdomain^
250 mail.sjsu edu Hello pe-66-1-145-68 cj MAIL FROM: <president@whrtehou$e.gov>Wjj
220 email sjsu edu ESMTP Sendmail 8 9 +SuM НЕЮ localhost localdomain^
250 mail.sjsu edu Hello pe-66-1-145-68 cj MAIL FROM: <president@whrtehou$e.gov>Wjj
Рис. 3.6 Программа MailTest
Рис. 3.6 Программа MailTest
Эта программа просто передает SMTP-серверу последовательность приведенных выше команд. На экране эти команды отображаются вместе с ответами. Обратите внимание, что взаимодействие с почтовым сервером организовано в отдельном потоке без блокировки потока пользовательского интерфейса. (В
главе 1, "Многопоточность", приводится более подробное описание принципов работы с потоками в Swing-приложениях.)
Листинг 3.4. MailTest.java
©version 1.01 2001-06-26
©author Cay Horstmann
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.net.*;
import java.io.*;
import javax.swing.*;
/**
Пример использования сокетов для передачи
простых текстовых почтовых сообщений
*/
public class MailTest
public static void main(String[] args)
{
JFrame frame = new MailTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) , frame.show();
)
/**
Фрейм для GUI-интерфейса.
*/
class MailTestFrame extends JFrame
{
public MailTestFrame()
{
setSize(WIDTH, HEIGHT);
setTitle("MailTest") ;
getContentPane().setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill =
gbc.weightx = 0;
gbc.weighty = 0;
gbc.weightx = 0;
add (new JLabel ("From:11) , gbc, 0, 0, 1, 1) ;
gbc.weightx = 100;
from = new JTextField(20);
add(from, gbc, 1, 0, 1, 1) ;
gbc.weightx =
add(new JLabel("To:") , gbc, 0, 1, 1, 1) ;
gbc.weightx = 100;
to = new JTextField(20);
add(to, gbc, 1, 1, 1, 1) ;
gbc.weightx = 0;
add (new JLabel (" SMTP server:") , gbc, 0, 2, 1, 1) ;
gbc.weightx = 100;
smtpServer = new JTextField(20);
add(smtpServer, gbc, 1, 2, 1, 1) ;
gbc.fill = GridBagConstraints.BOTH;
gbc.weighty = 100;
message = new JTextArea() ;
add(new JScrollPane(message), gbc, 0, 3, 2, 1) ;
communication = new JTextArea();
add(new JScrollPane(communication), gbc, 0, 4, 2, 1) .
gbc.weighty = 0;
JButton sendButton = new JButton("Send");
sendButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent evt) {
new
Thread()
{ public void run()
{ sendMail();
}
} .start();
}
}) ;
JPanel buttonPanel = new JPanel(); buttonPanel.add(sendButton) ;
add(buttonPanel, gbc, 0, 5, 2, 1);
у * *
Добавление компонента в фрейм
Sparam с the component to add
@param gbc the grid bag constraints
Sparain x the grid bax column
Sparain у the grid bag row
@param wthenumberofgridbagcolumns spanned
@param hthenumberofgridbagrows spanned
*/
private void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx =
gbc.gridy =
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane().add(c, gbc);
)
/**
Отправка сообщения, которое
одобрено в GUI-интерфейсе
*/
public void sendMail()
{
try
{
Socket s = new Socket(smtpServer.getText(), 25);
out = new Printwriter(s.getOutputStream()) ; in = new Buf feredReader(new
InputStreamReader(s.getlnputstream()));
String hostName
=
receive();
send("HELO " + hostName);
receive() ;
send("MAIL FROM: <" + from.getText() + ">");
receive() ;
send("RCPT TO: <" + to.getText() + ">");
receive();
send("DATA");
receive();
StringTokenizer tokenizer = new StringTokenizer( message. getText (), "\n");
while (tokenizer.hasMoreTokens())
send(tokenizer.nextToken());.
send(".");
receive();
s.close();
catch (lOException exception)
communication.append("Error: " + exception); )
)
/**
Передача строки сокету и эхо-ответ в текстовом поле связи @parain s the string to send.
*/
public void send(String s) throws lOException {
communication.append(s) ;
communication.append("\n");
out.print(s);
out.print("\r\n");
out.flush();
)
/**
Получение строки от сокета и ее отображение в текстовом поле связи */ public void receive() throws lOException {
String line = in.readLine(); if (line != null) {
communication.append(line) ; communication.append("\n");
) )
private BufferedReader in;
private PrintWriter out;
private JTextField from;
private JTextField to;
private JTextField smtpServer;
private JTextArea message;
private JTextArea communication;
public static final int WIDTH = 300;
public static final int HEIGHT = 300; }
Более сложные способы работы с сокетами
Паузы при работе с сокетами
В реальных программах не используется чтение из сокета, потому что на время чтения данных работа программы блокируется. Если сервер недоступен, то приложение переходит в состояние длительного ожидания. В таком случае допустимое время бездействия программы может определяться требованиями базовой операционной системы.
Вместо этого в приложении рекомендуется самостоятельно управлять временем простоя (в миллисекундах) с помощью методатак, например, как по
казано ниже.
Socket s = new // время простоя 10 секунд
Если для сокета задано время простоя, то для всех операций чтения недоступных данных будет генерирована исключительная ситуация InterruptedlOException, которую можно обработать для реагирования на возникновение простоя так, как показано ниже.
try
{String line
while ((String line =!= null)
{ обработка строки }
catch (InterruptedlOException exception)
{ реакция на простой
Однако, с обработкой пауз связана еще одна проблема. Конструктор сокета Socket (String host, int port) может быть заблокирован еще до установления соединения с сервером.
При использовании прежних версий SDK для решения этой проблемы потребуется создать сокет в отдельном потоке и ожидать завершения работы этого потока или истечения заданной паузы.
Socket socket = null;
Thread t = new Thread();
public void run() {try
socket = newport);
catch (IOException exception) { }
);
t.start();
try
{
t.join(timeout) ;
}
catch (InterruptedlOException exception) }
Метод join возвращает управление программе после завершения работы потока или истечения заданного времени простоя. Если переменная socket не равна null , то это значит, что успешно открыт в течение заданного промежутка времени.
С помощью SDK версии 1.4 эту проблему можно решить, создавая сначала неподключенный сокет, а потом подключая его с указанием паузы.
Socket s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);
Половинное закрытие
При получении запроса клиента сервер должен определить конец запроса. По этой причине многие Internet-протоколы (например, протокол SMTP) ориентированы на построчную обработку данных. А в других протоколах используется заголовок с явным указанием размера данных запроса. Действительно, определение конца запроса представляет собой более сложную задачу, чем запись данных в файл. Дело в том, что при работе с файлом его можно закрыть по окончании данных, а при закрытии сокета сразу же разрывается соединение с сервером.
Метод половинного закрытия {half-close) позволяет решить эту проблему за счет закрытия выходного потока сокета и указания серверу о завершении передачи запроса, но с сохранением открытого входного потока для чтения отклика сервера.
Для этого программа-клиент должна содержать следующий код.
Socket socket = new Socket(host, port);
BufferedReader reader = new BufferedReader(
new InputStreainReader(socket.getlnputStream()));
Printwriter writer = new Printwriter(
socket. getOutputStreain () ) ;
// передача запроса
writer.print(...);
writer.flush();
socket.shutdownoutput();
// теперь сокет наполовину закрыт
// приступаем к чтению отклика
String line;
while((line =!= null)
socket.close();
В таком случае сервер просто считывает входные данные до тех пор, пока они не закончатся.
Конечно, этот протокол будет полезен только для одноразовых сервисов, например, HTTP-запросов, когда клиент соединяется с сервером, посылает запрос, получает ответ, а затем отсоединяется.
Internet-адреса
Обычно использование Internet-адресов в программах не связано с какими-то трудностями. Эти адреса состоят из 4 (а в недалеком будущем из 6) групп цифр, например, 132.163.135.130. Иногда требуется преобразовать имя компьютера- сервера в Internet-адрес и для этого можно использовать класс InetAddress.
В пакете java.net в SDK версии 1.4 поддерживаются Internet-адреса согласно протоколуIPv6 при условии, что они поддерживаются операционной системой компьютера-
сервера. Например, к моменту создания данной книги это было возможно для операционной системы Solaris, но не для Windows.
Статический метод getByName возвращает объект InetAddress с Internet- адресом (132.163.135.130) указанного компьютера, как показано ниже.
InetAddress address =
Доступ к каждой группе цифр можно получить с помощью метода getAddress.
byte[] addressByte = address.getAddress();
Некоторые имена компьютеров-серверов для распределения нагрузки могут соответствовать нескольким Internet-адресам. Например, во время написания этой книги имя компьютера-сервера j ava.sun. com соответствовало трем разным Internet- адресам. В таком случае будет произвольным образом выбран один из этих адресов. Для получения всех Internet-адресов для заданного имени компьютера предусмотрен метод getAHByName.
InetAddress[] addresses = InetAddress.getAHByName(host);
Наконец, иногда нужно получить адрес локального компьютера. Если для этого использовать описанные выше методы и стандартное имя локального компьютера localhost, то будет получен не очень полезный стандартный результат 127 .0.0. 1 . Для получения фактического Internet-адреса локального компьютера следует использовать метод getLocalHost.
InetAddress address = InetAddress.getLocalHost();
В листинге 3.5 приводится пример вывода Internet-адреса вашего локального компьютера без указания параметров командной строки или всех Internet-адресов для имени компьютера, заданного в командной строке следующего вида.
java InetAddressTest java.sun.com
Листинг 3.5.InetAddressTeat.java
/**
©version 1.01 2001-06-26
©author Cay Horstmann
*/
importj ava.net.*;
/**
Пример использования класса InetAddress.
Здесь в качестве аргумента командной строки передается имя удаленного компьютера, а при его отсутствии используется локальный компьютер
*/
public class InetAddressTest
{
public static void main(String[] args)
{
try
if (args.length > 0)
String host = args[0];
InetAddress[] addresses
= InetAddress.getAllByName(host) ;
for (int i = 0; i < addresses.length; i++) System.out.printIn(addresses[i]) ;
)
else
<
InetAddress localHostAddress =
System.out.printIn(localHostAddress);
}
}
catch (Exception e)
{
e.printStackTrace();
В этой книге рассматривается только сетевой протокол TCP (Transmission Control Protocol), который позволяет установить надежное соединение между двумя компьютерами. Платформа Java поддерживает также протокол UDP (User Datagram Protocol), который может использоваться для передачи пакетов (илидейтаграмм) с гораздо меньшими накладными расходами, чем при использовании протокола TCP. Недостаток этого метода заключается в том, что пакеты передаются в произвольном порядке, причем допускается утрата некоторых пакетов. Получатель должен сам позаботиться о расположении их в правильном порядке или о запросе на повторную передачу утраченных пакетов. Протокол UDP лучше всего подходит для приложений, в которых утрата пакетов не является критическим фактором, например, при передаче аудио- и видеопотоков, либо при передаче данных во время постоянных измерений.
java.net.Socket
Конструктор Socket (String host, int port) создает сокет и соединяет его с заданным портом указанного удаленного компьютера.
Параметры:
hostимя удаленного компьютера
portномер порта
Метод Socket () создает несоединенный сокет.
Метод void connect (SocketAddress address) соединяет сокет no заданному адресу (начиная с SDK 1.4).
Метод void connect (SocketAddress address, int timeout) соединяет сокет no заданному адресу или возвращает управление по истечении заданной паузы (начиная с SDK 1.4).
Параметры:
addressадрес удаленного компьютера
timeoutпауза в миллисекундах
Метод boolean isConnected() возвращает значение true, если сокет подсоединен (начиная с SDK 1.4).
Методvoid close О закрывает сокет.
Метод boolean isClosed() возвращает значение true, если сокет закрыт (начиная с SDK 1.4).
МетодInputstream getlnputstream() возвращает входной поток для чтения данных из сокета.
МетодOutputstream getOutputStream () возвращает выходной поток для записи данных в сокет.
Метод void setSoTimeOut (int timeout) задает время блокировки для запросов на чтение из данного сокета. По истечении заданной паузы генерируется исключительная ситуация InterruptedlOException.
Параметры:
timeout пауза в миллисекундах (0 для бесконечной паузы)
Метод void shutdownOutput () задает для выходного потока значение "конец потока".
Методvoidshutdowninput()задаетдлявходногопотоказначение
конец потока .Метод boolean isOutputShutdown() возвращает значение true, если выходной поток закрыт (начиная С SDK 1.4).Метод boolean isInputShutdown() возвращает значение true, если входной поток закрыт (начиная с SDK 1.4).конец потока .Метод boolean isOutputShutdown() возвращает значение true, если выходной поток закрыт (начиная С SDK 1.4).Метод boolean isInputShutdown() возвращает значение true, если входной поток закрыт (начиная с SDK 1.4).••••""
конец потока .
Метод boolean isOutputShutdown() возвращает значение true, если выходной поток закрыт (начиная С SDK 1.4).
Метод boolean isInputShutdown() возвращает значение true, если входной поток закрыт (начиная с SDK 1.4).
конец потока .
Метод boolean isOutputShutdown() возвращает значение true, если выходной поток закрыт (начиная С SDK 1.4).
Метод boolean isInputShutdown() возвращает значение true, если входной поток закрыт (начиная с SDK 1.4).
•
•
•
•
Ifmj ava. net • Inet Addre s s
IA г II
!IiМетод static InetAddress getByName (String host) создает объект
InetAddress с Internet-адресом компьютера с заданным именем host.
Методы static InetAddress[] getAllByName(String host) создает массив InetAddress [] со всеми Internet-адресами компьютера с заданным именем host.
Метод static InetAddress getLocalHost () создает объект InetAddress с Internet-адресом локального компьютера.
Метод byte[] getAddress () возвращает массив байтов с группами цифр Internet-адреса.
Метод String getHostAddress () возвращает строку с десятичными цифрами, которые разделены точками, например, " 132 .163.135.130".
МетодString getHostName () возвращает имя компьютера.
java. net. InetSocketAddre ss
Конструктор InetSocketAddress (String hostname, int port) создает объект с Internet-адресом для заданного имени компьютера и порта. Если имя компьютера не может быть определено, то в таком случае для свойства unresolved объекта с Internet-адресом задается значение true (начиная с SDK 1.4).
Метод boolean isUnresolved() возвращает значение true, если имя
компьютера для данного объекта с Internet-адресом не может быть разрешено (начиная с SDK 1.4).
URL-соединения
В предыдущем разделе показано, как использовать программирование на уровне сокетов для соединения с SMTP-сервером и отправки электронных сообщений. Мы познакомились с некоторыми деталями внутренней организации этого Internet- сервиса. Однако, для создания большого приложения с функциями обработки электронной почты желательно было бы работать на более высоком уровне и использовать специализированную библиотеку с инкапсуляцией всех деталей. Например, компания Sun Microsystems создала API-интерфейс JavaMail в виде стандартного расширения платформы Java. С помощью этой библиотеки электронное сообщение можно передать, просто используя показанный ниже вызов метода send.
Transport.send(message);
Данная библиотека позаботится обо всех других необходимых действиях: о протоколах, рассылке сообщения сразу нескольким получателям, вставке присоединенных файлов и т.д.
В остальной части этой главы рассматриваются способы работы с высокоуровневыми сетевыми сервисами, предусмотренными в стандартной библиотеке платформы Java. Конечно, для создания и применения этих служб библиотека времени выполнения использует сокеты, но при использовании высокоуровневых сервисов разработчикам уже не нужно заботиться о многих подробностях организации сетевых протоколов.
URL-указатели и URI-идентификаторы
Классы URL и URLConnec t i on инкапсулируют большую часть всех сложных подробностей процесса извлечения информации с удаленного узла. Для указания нужного URL-адреса необходимо создать показанный ниже объект типа URL.
URL url = new URL(протокол:ресурс);
В платформе Java поддерживаются протоколы HTTP и FTP.
Для простого извлечения содержимого заданного ресурса достаточно использовать метод openStream класса URL. Для считывания содержимого ресурса этот метод использует объект типа Inputstream.
Inputstream uin = url. openStream ();
BufferedReader in
= new Buf feredReader (new InputStreamReader(uin) ) ;
String line;
while ((line = in. readLine () ) != null)
{ обработка строк
У
В пакете java.net в SDK 1.4 проводится существенное различие между унифицированными указателями информационного ресурса (uniform resource
locator — URL) и унифицированными идентификаторами информационного ресурса (uniform resource identificator — URI).
URI-идентификатор — это синтаксическая конструкция, которая содержит строку с указанием Web-pecypca. URL-указатель — это особый вид URI-идентификатора с информацией, вполне достаточной для обнаружения указанного ресурса. Например, URI- идентификатор mailto:cay@hortsmann.com не является указателем, потому что он не указывает на какие-то данные. Такой URI-идентификатор называется унифицированным именем информационного ресурса (uniform resource name — URN).
В библиотеке Java класс URI не имеет методов доступа к указанному им ресурсу, потому что он предназначен только для синтаксического разбора строки с указанием Web-pecypca. И наоборот, класс URL может открыть поток для данного ресурса. По этой причине класс URL может работать только с указателями, обработка которых обусловлена в библиотекеJava, т.е. с URL-указателями HTTP и FTP.
Рассмотрим приведенные ниже URL-указатели.
http://maps.yahoo.com/py/maps.py?csz=Cupertino+CA
ftp://имя_пользователя:пароль®!tp. имя_сервера.сот/pub/имя_файла.txt
URI-спецификация задает правила построения этих идентификаторов согласно приведенному ниже синтаксису.
[ схема:]специальная_часть[^фрагмент]
Здесь квадратные скобки ([ ]) обозначают необязательную часть, а двоеточие (:) и знак номера (#)— разделители. Если часть схема: присутствует в URI- идентификаторе, то он называется абсолютным, а в противном случае — относительным.
Абсолютный URI-идентификатор называется непрозрачным, если специаль- ная_частане начинается с косой черты (/), например, имеет такой вид.
mailto:cay@hortsmann.com
Все абсолютные прозрачные URI-идентификаторы и все относительные URL- указатели имеют следующую иерархическую структуру.
http://java.sun.com/index.html
../../java/net/Socket.html#Socket()
Компонент специальная„на с иерархического URI-идентификатора имеет такую структуру.
[/полномочия][путь][?запрос]
Здесь квадратные скобки ([ ]) тоже обозначают необязательную часть.
В URI-идентификаторах серверов компонент полномочия имеет следующую форму, где порт должен иметь целочисленное значение.
[информация_о_пользователе6] имя_компьютера [: порт]
В документе RFC 2396 со спецификацией URI-идентификаторов также допускается механизм указания этой части в другом формате на основе информации из реестра, но он не получил широкого распространения.
Одна из основных задач класса UR1 заключается в синтаксическом разборе отдельных компонентов идентификатора и извлечении ИХ с помощью перечисленных ниже методов.
getScheme
getSchemeSpecificPart
getAuthority
getUserlnfo
getHost
getPort
getPath
getQuery
getFragment
Другая задача класса URI заключается в обработке абсолютных и относительных идентификаторов. Например, имея абсолютный URI-идентификатор
http://docs.company.com/api/java/net/ServerSocket.html
можно определить для относительного URI-идентификатора
. . / . ./java/net/Socket,html#Socket()
его абсолютный URI-идентификатор
http://docs.company.сош/api/java/net/Socket.html#Socket()
Этотпроцессназываетсяразрешением (resolving) относительногоURI-идентификатора.
Обратный процесс называется преобразованием абсолютных адресов в относительные (relativization). Например, имея базовый URI-идентификатор http: //docs. mycompany.com/api, можно получить для абсолютного URI-идентификатора http: //docs.company.com/api/j ava/lang/String.html относительный URI- идентификаторjava/lang/String.html.
Для выполнения этих операций в классе URI предусмотрено два соответствующих метода.
relative = base.relativize(combined); combined = base.resolve(relative);
Извлечение информации с удаленного узла
Для получения дополнительной информации о ресурсе потребуется использовать класс URLConnection, который предоставляет гораздо больше средств управления доступом к Web-ресурсам.
При работе с объектом URLConnection необходимо тщательно продумать и выполнить все перечисленные ниже действия.
Сначала нужно вызвать метод openConnection класса URL для получения объекта URLConnectionтак, как показано ниже.
URLConnection connection = url.openConnection();
Указать значения свойств с помощью перечисленных методов.
setDoInput
setDoOutput
setlfModifiedSince
setUseCaches
setAllowUserInteraction
setRequestProperty
Эти методы подробно рассматриваются ниже в этом разделе и в замечаниях об API-интерфейсе.
Соединиться с удаленным ресурсом с помощью метода connect так, как показано ниже.
connection.connect();
Помимо создания сокета для установления соединения с сервером этот метод такжезапрашиваетусерверазаголовочную информацию (header information).
После подключения к серверу следует запросить у него заголовочную информацию с помощью универсальных методов getHeaderFieldKey и getHeaderField. Кроме того, для удобства разработчиков предусмотрены перечисленные ниже методы обработки стандартных полей.
getContentType
getContentLength
getContentEncoding
getDate
getExpiration
getLastModified
Наконец, для доступа к данным указанного ресурса используйте метод getlnputStream, который предназначен для получения входного потока и считывания информации из него. (Это точно такой же входной поток, который возвращается методом openStream класса URL.) Для этого также можно использовать методgetObject, однако, он менее практичен. Для обработки содержимого стандартных типов, например, текста text/plain и изображений image/gif, потребуется использовать классы библиотеки com. sun. Кроме того, разработчик кода может зарегистрировать свои собственные обработчики содержимого, но эти способы работы не рассматриваются в настоящей книге.
Некоторые разработчики при использовании класса URLConnection ошибочно считают, что методы getlnputStream и getOutpuStream аналогичны одноименным методам класса Socket. Это совсем не так. Класс URLConnection способен выполнять много других действий, в частности, обрабатывать заголовки запросов и откликов. По этой причине рекомендуется строго придерживаться указанной выше последовательности действий.
Рассмотрим эти методы более подробно. Существует несколько методов задания свойств соединения еще до подключения к серверу. Наиболее важными среди них яв-
ляются методы setDoInput и setDoOutput. По умолчанию соединение получает входной поток для чтения, но не получает выходной поток для записи. Для получения выходного потока (например, для размещения данных на Web-сервере, как показано в следующем разделе) необходимо создать следующий вызов.
connection.setDoOutput(true) ;
Далее можно создать заголовок запроса и послать его серверу вместе с командой запроса, например так, как показано ниже.
GET www.server.com/index.html HTTP/1.0
Referer: http://www.somewhere,com/links.html Proxy-Connection: Keep-Alive
User-Agent: Mozilla/4.76 (Windows ME; U) Opera 5.11 [en]
Host: www.server.com
Accept: text/html, image/gif, image/jpeg, image/png, */* Accept-Language: en
Accept-Charset: iso-8859-1, *, utf-8
Cookie: orangemilano=19221888821987
Метод setIfModif iedSince предназначен для уведомления соединения о том, что необходимо получить только те данные, которые были изменены после некоторой даты. В аплетах для этого используются только методы setUseCaches и setAllowUserInteraction. Метод setUseCaches сначала направляет внешний броузер для проверки кэша броузера. Метод setAllowUserlnteraction позволяет приложению использовать показанное на рис. 3.7 диалоговое окно с предложением ввести учетное имя и пароль пользователя для защиты доступа к ресурсам. Однако, эти параметры не имеют никакого значения вне аплета.
[Enter Network Password
J
lK.mNitm. I
Cancel
Рис. З.7. Диалоговое окно для ввода учетного имени и пароля пользователя
Наконец, метод setRequestProperty можно использовать для установки значения любого параметра используемого протокола. Эти параметры не очень хорошо документированы, и сведения о них обычно можно получить только у коллег- программистов. Например, для доступа к защищенной паролем Web-странице необходимо выполнить перечисленные ниже действия.
Создать строку, состоящую из сцепленных вместе строк с учетным именем пользователя, двоеточием и паролем.
String input = username + ": " + password;
Перекодировать полученную строку согласно алгоритму кодирования Base 64, который преобразует последовательность байтов в последовательность ASCII- символов.
String encoding = base64Encode(input);
Вызвать метод setRequestProperty с именем свойства "Authorization" и значением в виде двух сцепленных строк "Basic" и encoding.
connection.setRequestProperty("Authorization", "Basic" + encoding);
Выше описан способ доступа к защищенной паролем Web-странице. Для доступа к защищенному паролем FTP-файлу используется совершенно другой способ, который приведен ниже.
ftp://учетное_имя:пароль®ftp.имя_сервера.com/каталог/имя_файла
Этот способ также не очень хорошо документирован. Его краткое описание можно найти в файле rt ,j ar в классе sun.net .www.protocol.ftp.FtpURLConnection после декомпиляции с помощью утилиты j avap.
После вызова метода connect можно запросить заголовочную информацию. Рассмотрим сначала способ перечисления всех заголовочных полей. Создатели этого класса посчитали необходимым создать свой собственный способ перечисления полей.
String key = connection.getHeaderFieldKey(n);
Такой вызов метода позволяет получить ft-НЫЙ ключ (т.е. поле) заголовка, причем нумерация начинается с единицы! Этот вызов возвращает значение null, если п равно нулю или больше общего количества заголовочных полей. Однако, для возвращения этого количества не предусмотрено никакого другого метода. Для перебора всех полей приходится просто вызывать метод getHeaderFieldKey до тех пор, пока не будет получено значение null. Аналогично работает показанный ниже метод получения значения поля.
String value = connection.getHeaderField(n);
К счастью, в SDK 1.4 предусмотрен метод getHeaderFields, который возвращает объект Map с заголовочными полями. Более подробно он рассматривается в главе 2, "Коллекции".
Map headerFileds = connection.getHeaderFields() ;
Вот несколько заголовочных полей для типичного HTTP-запроса.
Date: Wed, 29 Aug 2001 00:15:48 GMT
Server: Apache/1.3.3 (Unix)
Last-Modified: Sun, 24 Jun 2001 20:53:38 GMT
ETag: "28094e-12cd-37729ad2"
Accept-Ranges: bytes
Content-Length: 4813 Connection: close
Content-Type: text/htznl
Для удобства разработчиков предусмотрено шесть методов получения значений для наиболее распространенных типов заголовочных полей, которые перечислены в табл. 3.1.
Методы, которые возвращают значения типа long, начинают отсчет количества секунд с 1 января 1970 года по среднему времени по Гринвичу (Greenwich Mean Time — GMT).
Таблица 3.1.Стандартные методы доступа к наиболее распространенным типам заголовочных полей
Поле
Метод
Тип возвращаемого значения
Date
getDate
long
Expires
getExpiration
long
Last-Modified
getLastModified
long
Content-Length
getContentLength
int
Content-Type
getContentType
String
Content-Encoding
getContentEncoding
String
Пример в листинге 3.6 позволяет поэкспериментировать с URL-соединениями с указанием URL-адреса, учетного имени и пароля в командной строке, например так, как показано ниже.
java URLConnectionTest http: //mm.имя_сервера .com ^учетное_имя пароль
В результате успешного выполнения программа отобразит на экране следующие данные.
Все заголовочные поля и их значения.
Возвращаемые значения шести методов доступа к наиболее распространенным типам заголовочных полей, которые перечислены в табл. 3.1.
Первые десять строк запрашиваемого ресурса.
Это довольно простая программа за исключением той части, в которой выполняются преобразования по алгоритму Base 64. Вместо использованного в этом примере класса можно применить недокументированный класс sun.raise.Base64Encoder. Достаточно только заменить используемый в примере вызов метода показанным ниже вызовом другого метода.
String encoding =
new sun. mi sc . BASE64Encoder () . encode (input. getBytes () ) ;
Класс javax.mail.internet.MimeUtility в стандартном расширении JavaMail платформы Java также содержит метод кодирования по алгоритму Base 64. В SDK 1.4 включен класс java.util.prefs.Base64, который не является открытым.
Листинг З.б. UHLConnectionTest.java
/**
version 1.00 1999-08-27
author Cay Horstmann
*/
import java.io.*;
import java.net.*;
import java.util. *;
/**
Пример соединения с сервером по заданному URL-указателю и отображения заголовочных данных отклика вместе с 10 строками запрашиваемых данных
Введите в командной строке URL-указатель, а также
(в случае необходимости) учетное имя пользователя и
пароль (для базовой HTTP-идентификации)
*/
public class URLConnectionTest {
public static void main(String[] args)
{
try {
String urlName;
if (args.length > 0)
urlName = args[0];
else
urlName = "http://java.sun.com";
URL url = new URL(urlName);
URLConnection connection = url.openConnection();
// Указание учетного имени пользователя и пароля
if (args.length > 2) {
String username = args[l];
String password = args[ 2 ] ;
String input = username + ":" + password;
String encoding = base64Encode(input);
connection. setRequestProperty("Authorization", "Basic " + encoding);
}
connection.connect();
// Вывод полей заголовка
int n = 1;
String key;
while ((key = connection.getHeaderFieldKey(n)) != null) {
String value = connection.getHeaderField(n);
System.out.printIn(key + ": " + value);
■) '
// Вывод данных вместе с названиями функций
System. out .printin ("");
System.out.printIn("getContentType: "
+ connection.getContentType());
System, out.printin("getContentLength: " + connection.getContentLength());
System.out.printin("getContentEncoding: " + connection.getContentEncoding());
System.out.printin("getDate: "
+ connection.getDate() );
System.out.printin("getExpiration: "
+ connection.getExpiration());
System.out.printIn("getLastModifed: "
+ connection.getLastModified());
System. out .printin ("") ;
BufferedReader in = new BufferedReader(new
- InputStreamReader(connection.getlnputstream()))
// Вывод первых 10 строк данных
String line;
n = 1;
while ((line = in.readLine()) != null && n <= 10) {
System.out.println(line);
}
if (line != null) System.out.printIn(". . . " ) ;
}
catch (IOException exception) {
exception.printStackTrace();
/**
Вычисление кодированного представления (Base64) строки ©param s a string @return the Base 64 encoding of s
*/
public static String base64Encode(String s) {
ByteArrayOutputStream bout
= new ByteArrayOutputStream();
Base640utputStream out = new Base64OutputStream(bOut); try {
out.write(s.getBytes());
out.flush();
}
catch (IOException exception)
{ }
j return bOut.toString();
Этот фильтр потока преобразует поток байтов на основе алгоритма кодирования Base64
Согласно алгоритму Base64 каждые 3 байта преобразуются в 4 символа по следующей схеме.
111111122|22223333|33444444|
Каждый набор из 6 битов кодируется в соответствии с таблицей кодирования toBase64. Если количество входных данных не кратно 3, то последняя группа из 4 символов дополняется одним или двумя символами =. Каждая выходная строка имеет длину не более 76 символов
*/
class Base640utputStream extends FilterOutputStream <
/**
Создание потока-фильтра
@param out the stream to filter
*/
public Base640utputStream(Outputstream out) (
super(out);
)
public void write(int c) throws IOException {
inbuf[i] = c;
if (i == 3) {
super.write(toBase64[(inbuf[0] & OxFC) » 2])
super.write(toBase64[((inbuf[0] & 0x03) « 4)
((inbuf[1] & OxFO) » 4)]);
super.write(toBase64[((inbuf[1] & OxOF) « 2) |
((inbuf[2] & OxCO) » 6)]);
super.write(toBase64[inbuf[2] St 0x3F]);
col += 4;
i = 0;
if (col >= 76) {
super.write('\n');
col = 0;
}
}
}
public void flush() throws IOException {
if (i == 1)
{
super.write(toBase64[(inbuf[0] & OxFC) >> 2];
super.write(toBase64[(inbuf[0] & 0x03)« 4] ) ;
super.write('='); super.write('=');
}
else if (i == 2)
{
super.write(toBase64[(inbuf[0] & OxFC) >> 2]) super.write(toBase64[((inbuf[0] St 0x03) « 4) |
((inbuf[1] & OxFO) » 4)]);
super.write(toBase64[(inbuf[1] & OxOF) « 2]) super.write('=');
A' ,'B' ■ 'CD''E''G'HI' ,,■ J1'K' , 'L' ,'M', 'N' ,'O' ,' p'Q','R' , ' S' , • T ' ,'U', 'V ,'W ,'X'Y'' Z ' , ■a' , 'b' ,1 c', 'd' ,'e' ,'f, ,■ h', 1ij '■ к', 4','m' ,'no' ,'p'- 'q'- 'r'' s '/ ' t' ,'u' ,' V' X1 , 'y' , z '■5', '6', '7''O', '1','2 ' ,■34' ,' 8 ’, ' 9' ,• +' /};privateint col = 0;privateint i = 0;privateint [ ] inbuf =newint[3];A' ,'B' ■ 'CD''E''G'HI' ,,■ J1'K' , 'L' ,'M', 'N' ,'O' ,' p'Q','R' , ' S' , • T ' ,'U', 'V ,'W ,'X'Y'' Z ' , ■a' , 'b' ,1 c', 'd' ,'e' ,'f, ,■ h', 1ij '■ к', 4','m' ,'no' ,'p'- 'q'- 'r'' s '/ ' t' ,'u' ,' V' X1 , 'y' , z '■5', '6', '7''O', '1','2 ' ,■34' ,' 8 ’, ' 9' ,• +' /};privateint col = 0;privateint i = 0;privateint [ ] inbuf =newint[3];static chart] toBase64 =static chart] toBase64 =,,private {private {,,,,,,Часто возникает вопрос о том, поддерживается ли в платформе Java доступ к защищенным Web-страницам. При открытии соединения по URL-указателю на https-страницувнутри аплета такой доступ возможен за счет SSL-механизма броузера. Однако, до появления SDK 1.4 не существовало никакой поддержки URL- указателей на https-страницы. Для соблюдения экспортных и патентных ограничений необходимо установить отдельную библиотеку расширений и выбрать провайдера криптографических услуг в соответствии с алгоритмом RSA. В пакете SDK 1.4 поддержка SSL-механизма входит в состав стандартной библиотеки.
A' ,
'B' ■ 'CD'
'E'
'G
'H
I' ,,
■ J1'K' , 'L' ,
'M'
, 'N' ,
'O' ,
' p
'Q',
'R' , ' S' , • T ' ,
'U'
, 'V ,
'W ,
'X
'Y'
' Z ' , ■a' , 'b' ,
1 c'
, 'd' ,
'e' ,
'f
, ,
■ h', 1ij '
■ к'
, 4',
'm' ,
'n
o' ,
'p'- 'q'- 'r'
' s '
/ ' t' ,
'u' ,
' V
' X1 , 'y' , z '
■5', '6', '7'
'O'
, '1',
'2 ' ,
■3
4' ,
' 8 ’
, ' 9' ,
• +
' /
};
private
int col = 0;
private
int i = 0;
private
int [ ] inbuf =
new
int[3];
A' ,
'B' ■ 'CD'
'E'
'G
'H
I' ,,
■ J1'K' , 'L' ,
'M'
, 'N' ,
'O' ,
' p
'Q',
'R' , ' S' , • T ' ,
'U'
, 'V ,
'W ,
'X
'Y'
' Z ' , ■a' , 'b' ,
1 c'
, 'd' ,
'e' ,
'f
, ,
■ h', 1ij '
■ к'
, 4',
'm' ,
'n
o' ,
'p'- 'q'- 'r'
' s '
/ ' t' ,
'u' ,
' V
' X1 , 'y' , z '
■5', '6', '7'
'O'
, '1',
'2 ' ,
■3
4' ,
' 8 ’
, ' 9' ,
• +
' /
};
private
int col = 0;
private
int i = 0;
private
int [ ] inbuf =
new
int[3];
static chart] toBase64 =
static chart] toBase64 =
,
,
private {
private {
,
,
,
,
,
,
java.net.URL
МетодInputstream openStream() открывает входящий поток для чтения данных информационного ресурса.
Метод URLConnection openConnection() возвращает объект URLConnection, который управляет соединением с ресурсом.
java.net.URLConnection
Метод void setDoInput (boolean dolnput) разрешает получение входящего потока по заданному соединению URLConnection, если параметр dolnput имеет значение true.
Метод void setDoOutput (boolean doOutput) разрешает передачу исходящего потока по заданному соединению URLConnection, если параметр doOutput имеет значение true.
Метод void setlfModifiedSince (long time) конфигурирует соединение URLConnection для извлечения только тех данных, которые были изменены после указанного промежутка времени, который задается в секундах, начиная с 1 января 1970 года, по среднему времени по Гринвичу.
Метод void setUseCaches (boolean useCaches) разрешает извлечение данных из локального кэша, если значение параметра useCaches равно true. Учтите, что объект URLConnection не поддерживает кэш. Кэш должен быть предоставлен внешней программой, например, броузером.
Метод void setAllowUserInteraction (boolean allowUserlnterac- tion) разрешает запрашивать пароль у пользователя, если значение параметра allowUserInteraction равно true. Учтите, что объект URLConnection не обладает никакими средствами выполнения таких запросов. Запрос пароля должен быть организован во внешней программе, например, в броузере или подключаемом модуле броузера.
Метод void setRequestProperty(String key, String value) задает значение для заголовочного поля.
Метод Map getRequestProperties () возвращает пары поле/значение для заголовочных полей запроса (начиная с SDK 1.4).
Метод void connect () соединяется с удаленным информационным ресурсом и извлекает заголовочную информацию отклика.
Метод Map getHeaderFields () возвращает пары поле/значение для заголовочных полей отклика (начиная с SDK 1.4).
Метод String getHeaderFieldKey (int n) возвращает n-oe заголовочное поле отклика или null, если п меньше или равно 0, либо превышает количество заголовочных полей.
Метод String getHeaderField(int n) возвращает значение га-го заголовочного поля отклика или null, если п меньше или равно 0, либо превышает количество заголовочных полей.
Метод int getContentLength () возвращает длину доступного содержимого или -1— в противном случае.
Метод String getContentType () возвращает тип содержимого, например, text/plain или image/gif.
Метод String getContentEncoding () возвращает тип кодирования содержимого, например, gzip. Применяется редко, потому что используемая по умолчанию кодировка не всегда указывается в заголовочном поле Content-Encoding.
Метод long getDate () возвращает дату создания информационного ресурса в секундах, начиная с 1 января 1970 года, по среднему времени по Гринвичу.
Метод long getExpiration() возвращает дату истечения заданного срока информационного ресурса в секундах, начиная с 1 января 1970 года, по среднему времени по Гринвичу.
Метод long getLastModif iedвозвращает дату последней модификации информационного ресурса в секундах, начиная с 1 января 1970 года по среднему времени Гринвича.
Метод Inputstream openinputstream () возвращает поток для чтения входного информационного ресурса.
Метод OutputStream openOutputStream () возвращает поток для записи выходного информационного ресурса.
Метод Object getObject () выбирает соответствующий обработчик содержимого для чтения данных информационного ресурса и преобразова-
ния их в объект. Этот метод вряд ли полезен для чтения данных стандартного типа, например, text/plainили image/gif, если только разработчику не нужно создать собственный обработчик этих типов.
Форма ввода данных
В предыдущем разделе описывается способ чтения данных с Web-сервера, а здесь рассматривается способ передачи данных из программы обратно Web-серверу, а также другим программам, которые может вызывать Web-сервер с помощью CGI- сценариев и сервлетов.
CGI-сценариии сервлеты
Еще до появления технологии Java для создания интерактивных Web-приложений использовался механизм на основе формы ввода данных, которую можно было заполнить в окне Web-броузера и послать Web-серверу, как показано на рис. 3.8.
Рис. З.8. Пример HTML-формы ввода данных
Обычно на такой форме находится кнопка, после щелчка на которой данные в текстовых полях, а также значения флажков и выключателей передаются серверу для обработки специальным CGI-сценарием. Аббревиатура CGI означает общий интерфейс шлюза (Common Gateway Interface). Используемый для этой формы ввода CGI-сценарий задается с помощью атрибута ACTION в дескрипторе FORM.
CGI-сценарий представляет собой программу7, которая обычно находится в каталоге cgi-bin сервера и запускается Web-сервером с входными данными из формы ввода.
CGI-сценарий обрабатывает полученные данные и создает другую HTML-страницу, которую Web-сервер посылает обратно броузеру. Такая последовательность действий схематически показана на рис. 3.9. Страница отклика может содержать новую информацию (например, результаты поиска) или только подтверждение о получении введенных данных. После этого Web-броузер отображает страницу отклика.
Сервер
Рис.3.9. Схема потока данных во время выполнения CGI-сценария
CGI-сценарии обычно создаются на языке Perl, но вообще их можно писать на любом языке, который обладает возможностями чтения из стандартного входного потока и записи в стандартный выходной поток.
Здесь не рассматриваются способы создания HTML-форм и способы их обработки CGI-сценариями. Более подробную информацию по этой теме можно найти в книге Чака Маскиано (Chuck Musciano) и Билла Кеннеди (Bill Kennedy) HTML & XHTML: The Definitive Guide (4th edition, O'Reilly, 2000). Задача данного раздела заключается в описании интерфейса между CGI-сценариями и Java-программами.
CGI-сценарии часто очень удобно использовать из-за их простоты и распространенности среди системных администраторов. Однако, они обладают огромным недостатком, связанным с необходимостью создавать новый процесс (а не поток) для каждого нового запроса. Более того, очень трудно контролировать безопасность выполнения CGI-сценариев. Новая технология на основе сервлетов позволяет преодолеть
эти недостатки. В ней используется способность платформы Java запускать каждый новый сервлет в отдельном потоке и отдельно контролировать их параметры безопасности. Пример такого сервлета будет приведен далее в этой главе. Более подробную информацию по этой теме можно найти в книге Криса Тейлора (Chris Taylor) и Тима Киммета (Tim Kimmet) CareJava Web Server [Prentice-Hall, 1998].
Передача данных Web-серверу
При передаче данных Web-серверу не имеет никакого значения, какая программа будет использована для их интерпретации: CGI-сценарий или сервлет. Клиент посылает данные Web-серверу в стандартном формате, а Web-сервер сам позаботится о том, чтобы найти ту программу, которая даст ответ.
Передача данных Web-серверу может осуществляться с помощью методов GET и POST.
При использовании метода GET его параметры просто указываются в конце URL- указателя в показанном ниже формате.
http: //компьютер/сценарий?параметры,
Например, во время создания этой книги Web-сервер карт maps.yahoo.com компании Yahoo содержал сценарий получения географической карты py.maps.py. Он используется с двумя параметрами addr и csz, которые нужно разделять символом & и использовать кодированные значения параметров так, как описано ниже.
Все пробелы необходимо заменить символом +. Все символы, отличные от цифробуквенных, нужно заменить символом % и двузначным шестнадцатеричным числом. Например, для передачи названия улицы S. Main нужно ввести символы S%2e+Main. Здесь шестнадцатеричное число 2е (или десятичное 46) представляет собой ASCII-код точки.
Такая система URL-кодирования позволяет предохранить промежуточные программы от путаницы с интерпретацией пробелов и других специальных символов.
Например, для получения карты с адресом 1 Infinite Loop, Cupertino, CA нужно ввести следующий URL-указатель.
http://maps.yahoo.com/py/maps.py?addr=l+Infinite+Loop&csz=
S>Cupertino+CA
Метод GET очень прост в употреблении, но он не получил широкого распространения из-за наличия в большинстве броузеров ограничения на количество символов в запросе.
В методе POST не нужно включать параметры в URL-указатель. Вместо этого необходимо получить выходной поток из объекта URLConnection и записать пары по- ле/значение в выходной поток. После чего еще нужно будет выполнить URL- раскодировку значений и отделить их от символов @.
Рассмотрим этот процесс более подробно. Для размещения данных в сценарии нужно сначала установить соединение с помощью объекта URLConnection, например так, как показано ниже.
URL url = new URL("http://компьютер/сценарий”);
URLConnection connection = url.openConnection();
Затем нужно вызвать метод setDoOutput для установления соединения для выходного потока.
connection.setDoOutput(true) ;
После этого нужно с помощью методаполучить поток для пе
редачи данных серверу. Для передачи текстовых данных удобно инкапсулировать поток с помощью метода PrintWriter.
out = new
Теперь можно передавать данные.
+ "=" ++
+ "=" +
После передачи данных нужно закрыть выходной поток.
;
Отклик сервератрадиционным образом.
in = new
inputStreamReader(connection.getlnputstream()));
String line;
while ((line =!= null)
{
обработка строки
}
Рассмотрим следующий пример.http://www.census.gov/ipc/www/
html содержит форму для ввода запроса на получение статистических сведений о численности и структуре населения, которая показана на рис. 3.10. В HTML-коде этой формы содержится строка со следующим дескриптором.
ACTI0N="/cgi-bin/ipc/idbsprd">
В этом дескрипторе указан метод отправки данных (POST) и сценарий /cgi- bin/ipc/idbsprd, который будет выполнен после того как пользователь щелкнет на кнопке, предназначенной для отправки введенных данных.
Далее нужно найти имена полей, которые потребуются указанному сценарию. Рассмотрим внимательно элементы пользовательского интерфейса формы ввода. Каждый из них имеет атрибут NAME, например такой, как показано ниже.
<SELECTSIZE=8>
<0PTI0N VALUE="001">001 Total Midyear Population
...другие параметры...
</SELECT>
В этом примере поле имеет имяОно содержит названия таблиц с разными
статистическими данными. Например, таблица с параметром 001 содержит ежегодные данные об общей численности населения. Далее в HTML-коде этой формы ввода можно найти поле cty с параметрами, которые обозначают сокращенные названия стран, например, US (т.е. United States — США) и СН (т.е. China — Китай). (Как видите, используемые в Бюро переписи США сокращения названий стран не соответствуют общепринятому стандарту
Наконец, в HTML-коде этой формы ввода можно найти поле optyr для выбора года. В данном примере выбран переключатель со значением latest checked, т.е. выбрать данные за последний учетный год. В этом случае оставшиеся поля не будут заполнены данными и в отклик будут включены только самые последние данные.
Таким образом, для получения данных об общей численности населения Китая за последний учетный год нужно создать следующую строку.
Tbl=l&cty=CH&optyr=latest+checked
Затем ее нужно послать Web-серверу по адресу:
http://www.census.gov/cgi-bin/ipc/idbsprd:
DataDataSelect oneSelect one1 \-.1 1004d008 Vital Rates010 InfantRates and Life Exp at Birth,1 \-.1 1004d008 Vital Rates010 InfantRates and Life Exp at Birth,||" 014 Life Table Values, by Sex »024 Number of Births, by SexAge of Metre- •Ы 028 -Age" Specific‘Fertility Rates and Selected Derve^ Measuresj *TaUmeyKe«vil»bUby Urban/RurtlResldence." 014 Life Table Values, by Sex »024 Number of Births, by SexAge of Metre- •Ы 028 -Age" Specific‘Fertility Rates and Selected Derve^ Measuresj *TaUmeyKe«vil»bUby Urban/RurtlResldence.З. /О. Форма для ввода запроса на получение демографических сведенийЗ. /О. Форма для ввода запроса на получение демографических сведений
Data
Data
Select one
Select one
1 \-.1 1
004d
008 Vital Rates
010 InfantRates and Life Exp at Birth,
1 \-.1 1
004d
008 Vital Rates
010 InfantRates and Life Exp at Birth,
|
|
" 014 Life Table Values, by Sex »
024 Number of Births, by SexAge of Metre- •
Ы 028 -Age" Specific‘Fertility Rates and Selected Derve^ Measures
j *TaUmeyKe«vil»bUby Urban/RurtlResldence.
" 014 Life Table Values, by Sex »
024 Number of Births, by SexAge of Metre- •
Ы 028 -Age" Specific‘Fertility Rates and Selected Derve^ Measures
j *TaUmeyKe«vil»bUby Urban/RurtlResldence.
З. /О. Форма для ввода запроса на получение демографических сведений
З. /О. Форма для ввода запроса на получение демографических сведений
После получения и обработки этих параметров сценарий пошлет следующий отклик.
<PRE>
U.S. Bureau of the Census, International Data Base Table 001. Total Midyear Population
CtYearCH1999CtYearCH1999Population1246871951Population1246871951
CtYear
CH1999
CtYear
CH1999
Population
1246871951
Population
1246871951
Source: U.S. Bureau of the Census, International Data Base
</PRE>
Как видите, сценарий форматирует минимально возможную HTML-страницу, размещая дескрипторы <PRE> и </PRE> в начале и конце данных. В этом примере сценарий не заботится о создании табличного представления данных. Именно по этой причине он был выбран для данного примера, чтобы познакомиться с основными принципами работы сценариев без других дополнительных HTML-дескрипторов, которые могут лишь запутать читателей.
В листинге 3.7 приведена программа, которая с помощью метода POST посылает данные по произвольному URL-указателю. Ее можно использовать для передачи данных сценарию Web-сервера Бюро переписи США из рассмотренного выше примера или по любому другому URL-указателю. Данные задаются в файле свойств (по умолчанию он называется PostTest .properties),например так, как показано ниже.
URL=http://www.census.gov/cgi-bin/ipc/idbsprd tbl=001
cty=CH
optyr=latestchecked
После запуска программы сначала проверяется наличие параметров в командной строке, которые переопределяют соответствующие данные из файла свойств, а потом считывается файл свойств.
Properties props = new Properties() ;
Fileinputstream in = new FilelnputStream(fileName); props.load(in);
Далее используется свойство URL для создания объекта с URL-указателем, который сразу же удаляется, чтобы избежать его передачи серверу.
URL url = new URL(props.getProperty("URL")); props.remove("URL");
В методе doPost сначала создается соединение, затем вызывается метод doOutput (true), а потом открывается выходной поток. Затем идет последовательная передача полей и значений объекта Properties в следующем порядке: имя_поля, символ =, значение и символ-разделитель &.
out.print(имя_поля);
out.print('=');
out.print(name);
out.print(URLEncoder.encode(значение));
if (если есть другие пары лоле/знаveHMOut.print('&');
После чего считывается отклик сервера. При этом следует учитывать следующую особенность. Если при работе сценария возникнет ошибка, то в таком случае вызов метода connection.get!nputStream() генерирует возникновение исключительной ситуации FileNotFoundException. Однако, сервер продолжит передачу данных, а именно, пошлет HTML-страницу с сообщением об ошибке. Обычно это сообщение "Error 404-page not found" о том, что данная страница не найдена. Для
перехвата этой страницы нужно выполнить для объекта URLCormection приведение к типу HttpURLConnection и вызвать метод getErrorStream.
InputStreamError err =
((HttpURLConnection)connection).getErrorStream() ;
Затем можно приступать к чтению HTML-страницы с сообщением об ошибке.
Эта программа демонстрирует работу сценария Бюро переписи населения США. В нашем примере также используется еще один файл свойств Zip.properties, который выполняет подстановку почтовых индексов с помощью сценария Почтовой службы США. В таком случае полученный отклик сервера будет содержать названия городов для заданного почтового индекса.
Используемый в этой программе метод очень полезен для создания запросов на получение информации с заданного Web-сервера. Для его создания нужно знать параметры, используемые в HTML-коде опрашиваемой Web-страницы, и отфильтровать из отклика все лишние HTML-дескрипторы и другую ненужную информацию.
Листинг 3.7. PostTest.java
/**
©version 1.01 2001-06-27
@author Cay Horstmann
*/
import j ava.io.*;
import java.net.*;
import java.util.*;
/**
Пример использования класса URLConnection вместе с методом POST. URL-указатель и данные из формы ввода находятся в файле СВОЙСТВ, имя которого указывается в командной строке
*/
public class PostTest
{
public static void main(String[] args) {
try
{
String fileName;
if (args.length > 0)
fileName = args[0];
else
fileName = "PostTest.properties”;
Properties props = new Properties();
Fileinputstream in = new FileInputStream(fileName); props.load(in);
URL url = new URL(props.getProperty("URL")) ;
props.remove("URL");
String r = doPost(url, props);
System.out.println(r);
}
catch (IOException exception) {
exception.printStackTrace() ;
/**
Запрос на основе метода POST и получение отклика сервера
Sparain url the URL to post to
Sparam nameValuePairs a table of name/value pairs to supply in the request.
Sreturn the server reply (either from the input stream or the error stream)
*/
public static String doPost(URL url, Properties nameValuePairs) throws IOException
{
URLConnection connection = url.openConnection(); connection.setDoOutput(true);
PrintWriter out
= new Printwriter(connection.getOutputStream());
Enumeration enum =
while (enum.hasMoreElements())
{
String name = (String)enum.nextElement();
String value = nameValuePairs.getProperty(name);
out.print(name);
out.print('=');
out.print(URLEncoder.encode(value));
if (enum.hasMoreElements()) out.print('&');
}
out.close() ;
BufferedReader in;
StringBuffer response = new StringBuffer(); try {
in = new BufferedReader(new
InputStreamReader(connection.getlnputStreamf)));
}
catch (IOException e) {
if (!(connection instanceof HttpURLConnection)) throw e; Inputstream err
=
if (err == null) throw e;
in = new BufferedReader(new InputStreamReader(err));
}
String line;
while ((line = in.readLine()) != null) response.append(line + "\n");
in.closef);
return response.toString();
В этом примере программы класс URLConnection используется для размещения данных на Web-сервере. Скорее из любопытства, чем из-за практической необходимости вам, вероятно, захочется просмотреть всю посылаемую серверу информацию, а не только подготовленные вами данные.
Объект URLConnection сначала посылает серверу заголовок запроса. В первой строке заголовка должна содержаться информация о типе содержимого.
Content-Type: тип
Здесь тип означает один из следующих форматов данных: text/plain, text/html, application/octet-stream, application/x-www-form-urlconnectedHT.fl.
Вслед за этой строкой должна быть строка с указанием длины содержимого.
Content-Length: длина
Конец заголовка обозначается пустой строкой, вслед за которой идут данные. Web- сервер способен отделить заголовок от данных и передать данные сценарию сервера.
Учтите, что объект URLConnection помещает все передаваемые в выходной поток данные в буфер для определения общей длины содержимого.
java.net.HttpURLConnection
• Метод Inputstream getErrorStream() возвращает поток, из которого считываются сообщения Web-сервера об ошибках.
java.net.URLEnooder
Метод static String encode (String s) возвращает строку s в URL- кодированном виде. В URL-кодировке символы A-Z, a-z, 0-9, -, _, . и * остаются неизменными. Пробелы передаются в виде символов +, а все остальные символы передаются в виде %UV, где UV— младший байт этого символа.
•j Методestatic String decode (String s) возвращает строку s в URL- декодированном виде.
Сбор информации в Web-среде
В последнем примере показано, как можно считывать данные с Web-сервера. Internet содержит огромное количество интересной и не очень интересной информации. Основным недостатком Internet многие пользователи считают трудности поиска нужной информации. Одним из преимуществ технологииJava является способность навести порядок в этом хаосе с помощью специальных Java-аплет, которые могут использоваться для извлечения информации и представления ее в удобном и привлекательном виде.
Вот несколько областей применения Java-аплетов.
Поиск самых свежих изменений на интересующих пользователя Web-страницах.
Посещение Web-страниц с информацией об авиарейсах для поиска наиболее выгодных условий.
Сбор и отображение самых последних котировок, обменных курсов и другой фи- нансовойинформации.
Поиск ответов на часто задаваемые вопросы, пресс-релизов, статей и т.д., а также
извлечение текста, содержащего заданные ключевые слова.
Вот очень простой пример использования такого аплета. Национальная метеорологическая служба США хранит отчеты и прогнозы о состоянии погоды в нескольких файлах на Web-сервере http://iwin.nws.noaa.gov. Например, ежечасный прогноз погоды для Калифорнии хранится в файле http: //iwin.nws .noaa.gov/iwin/ ca/hourly. html.
Другие аналогичные прогнозы хранятся в других каталогах.
Представленный в листинге 3.8 аплет отображает для пользователя два списка: один с перечнем штатов, а второй — с типами отчетов. После щелчка левой кнопкой мыши на кнопке Get report (Получить отчет) в текстовом окне будет представлен заданный отчет так, как показано нарис. 3.11.
Код этого аплета имеет очень простую структуру. Наиболее интересная часть кода относится к методу getWeather, который создает строку запроса. Затем с помощью параметра queryBase из дескриптора аплета извлекается базовый URL-указатель (http: //iwin.nws.noaa.gov/iwin). После этого в строку запроса включается название штата и тип отчета с указанием расширения . html.
String queryBase = getParaineter ("queryBase”) ;
String query = queryBase + state + "/" + report + ".html";
*PPlet_ _ __
. Iliromli ' ' Sute Foreran
FPUS66 KSTO 291103 SFPCA STATE AND EXTENDED FORECAST FOR NORTHERN AND CENTRAL CALIFORNIA NATIONAL WEATHER SERVICE SACRAMENTO CA 430 AM PDT THU JUN23 2001
CAZ001-002-005>010-034-037-051*065-074*075-282330- ;TATE F0RECA5T FOR COASTAL AREAS 430 AM POT THU JUN 28 2001
.TODAY...AREAS OF MORNING LOW CLOUDS AND FOG ON THE COAST AND
LOCALLY INLAND...OTHERWISE MOSTLY SUNNY. HIGHS "ROM THE UPPER 505 TO 5OS COAST...MID BOS TO MID 80S FURTHER INLAND.
.TONIGHT...AREAS OF LOW CLOUDS AND FOG. LOWS IN THE UPPER 405 AND SOS.
: .FRIDAY...AREAS OF MORNING LOW CLOUDS AND FOG...OTHERWISE MOSTLY
SUNNY. HIGHS IN THE 60S AND 70S COAST.. .UPPER 70S TO NEAR 90 WELL INLAND.
EXTENDED FORECAST...
।cm report
|||j|ill|ifciy||||lil|i^^
Рис. 3.11. АплетWeatherReport
Затем нужно создать объект URL и вызвать метод openStream.
URL url = new URL(query);
BufferedReader in = new BufferedReader(new
InputStreamReader(url.openStream()))
В остальной части кода просто считывается заданный файл, удаляются HTML- дескрипторы, и данный файл отображается в текстовом поле аплета.
String line;
while ((line = in.readLine())!= null)
weather.append(removeTags(line) + "\n");
Листинг3.8.WeatherApplet.java
/**
@version 1.11 2001-06-27
@author Cay Horstmann
*/
import java.net.*;
import j ava.io.*;
import java.util.*;
import java.awt.*;
importj ava.awt.event.*;
import java.applet.*;
import javax.swing.*;
/**
Пример аплета с отображением данных о погоде с сервера NOAA.
Данные на этом сервере хранятся преимущественно в текстовом формате, поэтому их можно отобразить в текстовом поле
*/
public class WeatherApplet extends JApplet {
public void init()
{
Container contentPane = getContentPane();
contentPane.setLayout(new BorderLayout());
// Создание списков штатов и типов отчетов
JPanel comboPanel = new JPanel();
state = makeCombo(states, comboPanel);
report = makeCombo(reports, comboPanel);
contentPane.add(comboPanel, BorderLayout.NORTH);
// Создание текстового поля
weather = new JTextArea(20, 80);
weather.setFont(new Font("Courier", Font.PLAIN, 12));
// Создание кнопки получения отчета
contentPane.add(new JScrollPane(weather),
BorderLayout.CENTER) ;
JPanel buttonPanel = new JPanel();
JButton reportButton = new JButton("Get report");
reportButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent evt) {
weather.setText(""); new
Thread() {
public void run ( )
{ getWeather(getltem(state, states), getltem(report, reports));
}
} .start();
}
}) ;
buttonPanel.add(reportButton);
contentPane.add(buttonPanel, BorderLayout.SOUTH);
/**
Создание поля со списком
Sparam items the array of strings whose first column contains the combo box display entries
@param parent the parent to add the combo box to
*/
public JComboBox makeCombo(String[][] items, Container parent) {
JComboBox combo = new JComboBox();
for (int i = 0; i < items. length; i++)
combo.additem(items[ i ] [ 0 ] ) ;
parent.add(combo) ;
return combo;
/**
Получение строки запроса для данных, отображаемых в поле со списком @param box the combo box
@param items the array of strings whose first column contains the combo box display entries and whose second column contains the corresponding query strings
©return the query string
*/
public String getltem(JComboBoxbox, String!][] items) {
return items[box.getSelectedlndex()][ 1] ;
}
/**
Отображение URL-указателя и полученных данных в текстовом поле
©param state the state part of the query
@param report the report part of the query
*/
public void getWeather(String state, String report) {
String r = new String();
try {
String queryBase = getParameter("queryBase");
String query
= queryBase + state + "/" + report + ".html";
URL url = new URL(query);
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String line;
while ((line = in.readLine() ) != null)
weather.append(removeTags(line) + "\n");
}
catch(lOException e)
{
showStatus("Error " + e) ;
}
)
/**
Удаление HTML-дескрипторов из строки
©param s a string
©return s with <...> tags removed.
*/
public static String removeTags(String s)
{
while (true)
{
int lb =
if (lb < 0) return s;
int rb = s.indexOf(1>', lb);
if (rb < 0) return s;
s = s.substring(0, lb) + " " + s.substring(rb + 1); )
}
private JTextArea weather;
private JComboBox state;
private JComboBox report;
private String!][] states =
{
{ "Alabama", "al" },
{ "Alaska", "ak" },
{ "Arizona", "az" },
{ "Arkansas", "ar" },
{ "California", "ca" },
{ "Colorado", "co" },
{ "Connecticut", "ct" },
{ "Delaware", "de" },
{ "Florida", "fl" },
{ "Georgia", "ga" },
{ "Hawaii", "hi" },
{ "Idaho", "id" },
{ "Illinois", "il" },
{ "Indiana", "in" },
{ "Iowa", "ia" },
{ "Kansas", "ks" },
{ "Kentucky", "ky" },
{ "Louisiana", "la" },
{ "Maine", "me" },
{ "Maryland", "md" },
{ "Massachusetts", "ma" },
{ "Michigan", "mi" },
{ "Minnesota", "mn" },
{ "Mississippi", "ms" },
{ "Missouri", "mo" },
{ "Montana", "mt" },
{ "Nebraska", "ne" },
{ "Nevada", " nv" },
{ "New Hampshire", "nh" },
{ "New Jersey", "nj" },
{ "New Mexico", "nm" },
{ "New York", "ny" },
{ "North Carolina", "nc" },
{ "North Dakota", "nd" },
{ "Ohio", "oh" },
{ "Oklahoma", "ok" },
{ "Oregon", "or" },
{ "Pennsylvania", "pa" },
{ "Rhode Island", "ri" },
{ "South Carolina", "sc" },
{ "South Dakota", "sd" },
{ "Tennessee", "tn" },
{ "Texas", "tx" },
{ "Utah", "ut" },
{ "Vermont", "vt" },
{ "Virginia", "va" },
{ "Washington", "wa" },
{ "West Virginia", "wv" },
{ "Wisconsin", "wi" },
{ "Wyoming", "wy" }
private String[][] reports =
{
{ "Hourly (State Weather Roundup)", "hourly" },
{ "State Forecast", "state" },
{ "Zone Forecast", "zone" },
{ "Short Term (NOWCASTS)", "shortterm" },
{ "Forecast Discussion", "discussion" },
{ "Weather Summary", "summary" },
{ "Public Information", "public" },
{ "Climate Data", "climate" },
{ "Hydrological Data", "hydro" },
{ "Watches", "watches" },
{ "Special Weather Statements", "special" },
{ "Warnings and Advisories", "allwarnings" }
К сожалению, попытка запуска этого аплета в броузере или программе просмотра аплетов закончится неудачно из-за нарушения правил обеспечения безопасности. В следующем разделе подробно описываются причины такого поведения и способы преодоления этих трудностей. Здесь же кратко описывается принудительный способ запуска этого аплета.
Сначала создайте текстовый файл WeatherApplet .policy со следующим содержанием.
grant
{
permission java.net.Socketpermission
"iwin.nws.noaa.gov: 8 0", "connect";
)
Более подробно файлы политики безопасности, содержащие правила обеспечения безопасности, описываются в главе 9, "Безопасность". В нашем примере этот файл содержит разрешение на соединение с компьютером iwin.nws.noaa.gov по порту 80, т.е. HTTP-порту. В следующем разделе подробно объясняется необходимость указания такого разрешения.
Запустим теперь наш аплет в окне программы просмотра аплетов с помощью следующей команды.
appletviewer -J-Djava.security.pelicy-WeatherApplet.policy
4>WeatherApplet.html
Параметр ■^J указывает на необходимость передачи аргументов командной строки интерпретатору Java, а параметр -D задает значение системного свойства. В данном случае для системного свойства java.security.policyзадается имя файла, которое содержит правила обеспечения безопасности.
Для запуска аплета в броузере сначала нужно подписать его. Затем пользователь аплета должен сообщить броузеру следующую информацию.
Подтвердить подпись (если она не гарантирована надежным бюро сертификации цифровых подписей).
Одобрить запрошенные полномочия.
Более подробно вопросы обеспечения безопасности рассматриваются в главе 9, "Безопасность".
Безопасность аплетов
Как показано выше, аплету запрещено выполнение даже самых простых операций, например, соединение с Web-сервером по порту 80. В настоящем разделе рассматриваются причины этого запрета и описываются способы распространения аплетов без излишних хлопот, связанных с подписями и сертификатами.
Web-броузеры позволяют аплетам выполнять чтение и запись данных только на том компьютере, который обслуживает данный аплет. Кроме того, аплеты могут соединяться с сокетами только того компьютера, от которого они поступили. Последнее правило часто приводят в более простой формулировке: "аплеты могут звонить только домой".
На первый взгляд такое ограничение не имеет никакого смысла. Почему аплету запрещается выполнять то, что разрешается обычным броузерам, а именно получение данных от Web-сервера? Рассмотрим схему (рис. 3.12), на которой показаны три компьютера.
Исходный компьютер, который предоставляет клиентамWeb-страницу и Java-аплет.
Локальный компьютер, который выполняет аплет.
Внешнее хранилище данных, к которому обращается этот аплет.
Правило обеспечения безопасности работы аплетов гласит, что аплет может читать и записывать данные только на исходном компьютере. Логично, что аплету не разрешаются операции записи данных на локальном компьютере, потому что в противном случае возникает опасность распространения вирусов или изменения файлов с важной информацией. Аплет начинает работу практически сразу же после того, как пользователь откроет Web-страницу с ним, поэтому и возникает необходимость защитить компьютер пользователя от злонамеренных или ошибочных аплетов.
Сервер
Внешнее хранилище данных
Рис. 3.12. Правила обеспечения безопасности не допускают подключение аплета к внешним объектам
Также вполне разумным и обоснованным является требование запрета на чтение с локального компьютера. В противном случае аплет сможет просмотреть содержимое файлов на локальном компьютере с важной информацией, например, с номерами кредитных карточек, открыть сокет соединения с компьютером аплета и записать считанную конфиденциальную информацию. В таком случае пользователь может открыть прекрасно оформленную Web-страницу, активировать аплет, который будет выполнять какие-то забавные или полезные действия, совершенно не подозревая о том, какие действия он выполняет в других потоках. Поэтому броузер не разрешает аплетам выполнять любые действия, связанные с доступом к файлам клиентского компьютера.
Но почему аплетам не разрешается считывать другие файлы в Web-среде? Разве Web-среда не является сокровищницей знаний, которая доступна для всех пользователей? Учтите, что доступ к Web-среде с домашнего компьютера через провайдера Internet-услуг отличается от доступа к Web-среде для поиска рабочей информации с рабочего компьютера вашей компании. Многие компании защищают доступ к корпоративной сети компьютеров с помощью брандмауэра.
Брандмауэр (firewall) — это компьютер, который фильтрует входящий и исходящий трафик корпоративной сети так, как показано на рис. 3.13. Этот компьютер отвергает любые попытки доступа, которые не удовлетворяют условиям обеспечения безопасности. Например, из-за наличия некоторых "дыр" в системе обеспечения безопасности в некоторых реализациях FTP-сервиса брандмауэр может запретить анонимный доступ или перенаправлять анонимные запросы к изолированному FTP-серверу. Кроме того, он может отвергать любые попытки доступа к почтовому порту на всех компьютерах за исключением почтового сервера. В зависимости от используемой философии обеспечения безопасности брандмауэр может использовать более сложные схемы фильтрования трафика между корпоративной сетью и Internet.
(Более подробная информация о принципах работы брандмауэров приводится в книге Вильяма Чезвика (William R. Cheswick) и Стивена Белловина (Steven M. Bellovin) Firewalls andInternet Security: Repelling the Wily Hacker, Addison-Wesley,1994.)
Брандмауэр позволяет компании использовать Web-среду для распространения внутренней информации среди своих сотрудников и не допускать ее утечку за пределы компании. Для этого компания создает специальный Web-сервер, адрес которого известен только сотрудникам, и программирует брандмауэр таким образом, чтобы отвергать любые попытки внешнего доступа к нему. Благодаря этому, сотрудники могут просматривать внутреннюю информацию, используя уже знакомыеWeb-инструменты.
Рис. 3.13. Схема работы брандмауэра
Если кто-то посещает вашу Web-страницу, то ваш аплет автоматически загружается его компьютером (за пределами брандмауэра) и запускается. Если бы он мог считывать все другие Web-страницы, которые доступны броузерам сотрудников компании, то в таком случае он мог бы получить доступ к корпоративной информации. Таким образом он мог бы открыть соединение со своим компьютером и посылать в ответ закрытую информацию всем желающим. Очевидно, что таким образом нарушается безопасность корпоративных данных. Web-броузер не знает, какие именно Web- страницы являются открытыми или конфиденциальными, поэтому он не предоставляет аплету доступ ко всем другим Web-страницам.
Это очень плохо, потому что в таких условиях нельзя создать аплет, который мог бы работать в Web-среде, собирать информацию, обрабатывать и форматировать ее, а затем представлять ее пользователю аплета. Например, аплет с прогнозом погоды не собирается записывать какие-либо данные на свой компьютер. Почему бы
не позволить ему считывать все, что угодно в таком случае? Тогда аплет играл бы роль сборщика и обработчика данных с отображением полученных результатов на экране пользовательского компьютера.
Проблема в том, что броузер не способен различать запросы на чтение от запросов на запись. Запрос на открытие потока по указанному URL-указателю как будто является запросом на чтение. Но это может быть и совсем другой запрос, как показано ниже.
http://www.rogue.com/cgi-bin/cracker.pl?Garys+password+is+Sicily
Здесь ловушка кроется в CGI-механизме, который может принимать любые аргументы и обрабатывать их. Сценарий, который управляет CGI-запросом, может (и часто именно так и делает) обрабатывать и сохранять их. Дело в том, что можно достаточно просто скрыть информацию в CGI-запросе. Поток данных, который содержит скрытую информацию, называется тайным каналом (covert channel).
Таким образом, получается, что броузер должен запрещать доступ с CGI-запросами и разрешать доступ только ко всем простым Web-страницам? Решение не так просто, как кажется. Броузер на самом деле не знает, к какому серверу он подключен по порту 80, т.е. к стандартному HTTP-серверу или нет. Этот может быть всего лишь только программа-оболочка, которая сохраняет все запросы в файле и возвращает HTML- страницу с сообщением о недоступности запрошенных данных. В таком случае аплет может передать конфиденциальную информацию в виде показанного ниже URL- указателя в запросе на чтение.
http://www.rogue.com/Garys/password/is/Sicily
Так как броузер не может отличить запросы на чтение и запросы на запись, то он должен запретить запросы всех типов.
Итак, аплеты запускаются на компьютере, с которого просматривается Web- страница, но они могут соединяться только с компьютером, который обслуживает эту Web-страницу. В этом заключается смысл уже упомянутого выше правила: "аплеты могут звонить только домой”.
Прокси-серверы
Как же создать и распространить аплет, который мог бы собирать информацию для ваших пользователей? Можно создать Web-страницу с аплетом и фиктивными данными, хранимыми на вашем сервере. (Именно такой подход используется фирмой Sun в примере с индикатором биржевых котировок.) Затем можно создать кнопку, с помощью которой пользователь может скопировать на свой компьютер аплет и файл политики. Теперь пользователи могут запускать этот аплет в своей программе просмотра аплетов.
Такой подход сильно ограничивает привлекательность аплета. Многим пользователям не понравятся дополнительные хлопоты с копированием и локальной инсталляцией аплета вместо использования реальных данных на вашей Web-странице.
К счастью, существует способ использования в аплете реальных данных, который основан на использовании прокси-сервера вместе с HTML-сервером. Прокси-сервер извлекает запрошенную информацию из Web-среды и передает ее тому, кто запрашивал. Допустим, что аплет создает показанный ниже запрос на основе запроса GET к прокси-серверу, который находится на компьютере, где располагается аплет.
http://www.server.com/proxysvr?URL=
'bhttp://iwin.nws.noaa.gov/iwin/CA/hourly.html
В таком случае прокси-сервер извлекает Web-страницу для данного аплета и посылает ее в виде результата выполнения запроса на основе метода GET, как показано на рис. 3.14.
Сервер
Другой компьютер
Рис. 3.14. Схема потока данных в аплете с прогнозом погоды
Для реализации этого процесса нужно создать прокси-сервер, например так, как показано в листинге 3.9. Он реализован в виде сервлета, который может запускаться большинством Web-серверов или их расширениями.
Сервлеты не входят в стандартный вариант платформы Java, а их подробное рассмотрение выходит за рамки этой книги. Более подробно этот вопрос рассматривается в книге Криса Тейлора (Chris Taylor) и Тима Киммета (Tim Kimmet) Core Java Web Server [Prentice-Hall, 1998]. Рассмотрим кратко простой сервлет, код которого представлен в листинге 3.9.
После получения запроса на основе метода GET Web-сервер вызывает метод doGet с параметром HttpServletRequest, содержащим параметры запроса. Этот сервлет использует метод getParameter для получения значения параметра URL. В любом сервлете можно послать отклик в потоке Printstream, который возвращается методом getWriter класса HttpServletResponse. Данный сервлет просто соединяется с информационным ресурсом, заданным параметром URL, считывает данные построчно, а затем посылает их в поток отклика. Наконец, метод sendError используется для обработки исключительных ситуаций.
В следующем разделе рассматриваются способы конфигурирования сервлетов при использовании контейнера сервлетов Apache Tomcat. Это программное обеспечение
включает небольшой Web-сервер, который можно запустить локально для проверки аплета с прогнозом погоды.
Длясозданияпрокси-серверанеобязательноиспользоватьсервлеты.Вкомментарии послелистинга 3.9описывается способ создания прокси-сервера наязыкахС и Perl.
version 1.00 1999-08-27
author Cay Horstmann
*/
import java.io.*;
impor tjava.net.*;
import javax.servlet. *;
import javax.servlet.http.*;
/**
Очень простой прокси-сервлет. Параметр request содержит URL-указатель запрашиваемого информационного ресурса. Сервлет получает запрашиваемую информацию и возвращает ее тому, кто ее запрашивал
*/
public class ProxySvr extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String query = null;
response.setContentType(”text/html");
PrintWriter out = response.getWriter();
query = request.getParameter("URL”);
if (query == null) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing URL parameter");
return;
}
try
{
query = URLDecoder.decode(query);
}
catch(Exception exception) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "URL decode error " + exception);
)return;
try
(
URL url = new URL(query);
BufferedReader in = new BufferedReader(new
InputStreamReader(url.openstream()));
String line;
while ((line = in.readLine()) != null)
out.printin(line);
out.flush();
}
catch(lOException exception)
{
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Exception: " + exception);
}
}
}
Применение языков С и Perl для создания сервлетов CGI-сценарии можно создавать на любом языке, который обладает возможностями чтения из стандартного входного потока и записи в стандартный выходной поток. Обычно для этого используется язык Perl. Однако, в данном случае наш первый CGI-сценарий для проксисервера создан на языке С из-за необходимости соединения с сокетами.
Коды этих программа на языках С и Perl показаны в листингах 3.10 и 3.11. Как видите, код на языке С имеет очень большой размер, потому что даже самые простые операции (например, чтение входной строки) должны быть тщательно запрограммированы на языке С. Фрагмент кода для работы с сетями выглядит еще сложнее, и самому создать его очень тяжело. Поэтому в данном случае для него использован немного переделанный пример кода из книги Ричарда Стивенса (W. Richard Stevens) UNIX Networking Programming (Prentice-Hall, 1990). Взгляните на этот код и вы по достоинству оцените простоту и элегантность пакетаjava.net.
Код на языке Perl гораздо короче, но он совершенно нечитабелен и непонятен для непосвященных. Чего только стоят такие "очаровательные" имена переменных, как $! или $ |, или такие "замечательные" выражения:
$url =~ s/% ([a-fA-FO-9][a-fA-FO-9])/раск("C", hex($l))/eg;
Приведенная строка буквально следующее: заменить все строки, которые начинаются с символа % и содержат вслед за ним две шестнадцатеричных цифры, соответствующим шестнадцатеричным числом. Этот код конечно же не был создан авторами, а был заимствован из другого сценария из книги Ларри Волла (Larry Wall), Тома Кристиансена (Tom Christiansen) и Джона Орванта (Jon Orwant) Programming Perl (3rd edition, O'Reilly, 2000). Нельзя сказать, что нам все понятно, но он работает.
После такого эксперимента читателям должно быть понятно, что сервлеты представляют собой прекрасный механизм обработки данных со стороны сервера. Средства обработки строк и сетевого программирования в платформе Java намного превосходят аналогичные средства языка С. Кроме того, код на языке Java более понятен, нагляден и прост в сопровождении, чем аналогичный код на языке Perl.
Листинг 3.10J proxysvr.c
/**
©version 1.01 1999-08-27
©author Cay Horstmann
*/
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket. h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXLINE 512
#define MAXNAME 128
#define HTTP 80
unsigned writen(fd, vptr, n)
int fd;
char* vptr;
unsigned n;
{
unsigned nleft;
unsigned nwritten;
char* ptr;
ptr = (char*)vptr;
nleft = n;
while (nleft > 0)
{
if ((nwritten = write(fd, ptr, nleft)) <= 0) return nwritten;
nleft —= nwritten;
ptr += nwritten;
}
return n — nleft;
unsigned readline(fd, vptr, maxlen) int fd;
char* vptr;
int maxlen;
{
unsigned n;
unsigned rc;
char* ptr;
char c;
ptr = vptr;
for (n = 1; n < maxlen; n++)
{
if ( (rc = read(fd, &c, 1)) == 1) {
ptr++ = c;
if (c == '\n')
{
ptr = 0; return n;
}
}
else if (rc == 0)
{
if (n == 1) return 0; else {
ptr = 0;
return n;
}
}
else
return
}
*ptr = 0;
return n;
void
char* msg;
{
fputs(msg, stderr);
stderr)
exit(l);
voidout, outlen)
char* in;
char* out;
int outlen;
{
inti=0;
intj=0;
while (in[i] !=j < outlen - 1)
{
if (in[i] == ' + ') out[j] = ' ';
else if (in[i] == {
sscan f(in + i + 1, "%x", &ch);
out[j] = ch;
i += 2;
}
else out[j] = i n [ i ] ;
i++ ;
j++;
=
}
intargv)
int argc;
char** argv;
{
int sockfd;
struct sockaddr_in serv_addr,-
intn;
char* name;
struct hostent* hostptr;
char+1];
char+1];
char+1];
char server_name[MAXNAME];
char file_name[MAXLINE];
int port;
int service = 0;
char*
char* q;
url,
р = strstr(url, "URL=http://");
if (p != url)
error("Sorry--can only recognize URL=service://server/file"); service = HTTP;
p += strlen("URL=http://");
q= strchr(p, '/1);
if (q == NULL)
error("Sorry--can only recognize //server/file");
strncpy(server_name, p, q —p);
server_name[q— p] = '\0';
strncpy(file_name, q, sizeof(file_name) —1);
file_name[sizeof(file_name) — 1] = '\0 ' ;
port = service;
if ((sockfd =0)) < 0)
error("Can1t open stream socket");
bzero((char*)&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
hostptr = gethostbyname(server_name);
if (hostptr == 0) error("Can't find host");
name = inet_ntoa(*(struct in_addr*)*hostptr->h_addr_list);
serv_addr.sin_addr.s_addr =
serv_addr.sin_port = htons(port);
if (connect(sockfd, (struct sockaddr*)&serv_addr,
sizeof(serv_addr)) < 0)
error("Can11 connect to server");
sendline[sizeof(sendline) — 1] = 0;
if (service == HTTP) {
st rcpy(sendline,"GET ") ;
strncat(sendline, file_name, sizeof(sendline) — 1
—
}
strncat(sendline, "\r\n", sizeof(sendline) — 1 -
n = strlen(sendline);
if (writen(sockfd, sendline, n) != n)
error("Write error on socket");
fputs("Content-type: text/html\n\n", stdout);
do
{
n = readline(sockfd, recvline, MAXLINE);
if (n < 0)
error("Read error on socket");
else if (n > 0)
{
recvline[n] = 0;
fputs(recvline, stdout);
}
}
while (n>0) ;
return 0;
Листинг 3.11. proxysvr.pl
#!/usr/bin/perl
# Version 1.02 1999-08-27
# Author Cay Horstmann
($url) = @ARGV;
$url =~ tr/+/ /;
$url =- s/% ([a-fA-FO-9][a-fA-FO-9])/pack("C", hex($l))/eg;
$pos = index($url, "URL=http://") ;
if ($pos != 0)
{
die "Sorry--can only recognize URL=http://server/file";
}
$port = 80;
$pos = 11;
$pos2 = index($url, "/", $pos);
if ($pos2 < 0)
{
die "Sorry--can only recognize //server/file";
}
$server_name = substr($url, $pos, $pos2 — $pos);
$file_name= substr($url, $pos2);
$AF_INET = 2;
$SOCK_STREAM =1;
$sockaddr = ' S n a4 x8 ' ;
($name, $aliases, $proto) = getprotobyname (1tcp1);
($name,$aliases,$type,$len,$thataddr)
=
$that = pack($sockaddr, $AF_INET, $port, $thataddr);
if (!socket (S, $AF_INET, $SOCK_STREAM, $proto)) {
die $!;
if (!connect (S, $that))
{
die $!;
}
select(S); $|=1; select(STDOUT) ;
$command = "GET ".$file_name;
print S $ command.''\r\n";;
print "Content-type: text/html\n\n"
while (<S>)
print;
}
Тестирование аплета WeatheiReport
В этом разделе описывается способ тестирования аплета WeatherReport. Для запуска сервлетов или CGI-сценариев необходимо установить Web-сервер. Ниже приводятся подробные инструкции по установке контейнера сервлетов Apache Tomcat. При использовании другого Web-сервера нужно соответствующим образом настроить его параметры.
Скопируйте контейнер сервлетов Apache Tomcat по адресу http: //Jakarta, apache.org/tomcat/index.html.
Установите контейнер сервлетов Apache Tomcat. Далее предполагается, что он установлен в каталог /tomcat. При использовании другого каталога внесите соответствующие исправления в приведенных ниже инструкциях.
Скомпилируйте код сервлета с помощью показанной ниже команды.
javac —classpath /tomcat/lib/servlet.jar: / ProxySvr.java
Скопируйте класс ProxySvr.class в каталог /tomcat/webapps/examples/WEB- INF/classes.
Отредактируйтефайл/1 omc at/webapp s/examples/WEB-INF/web.xml,
вставляя в него показанные ниже строки в соответствующих местах. (Придерживайтесь синтаксиса из других примеров.)
<servlet>
<servlet-name>
ProxySvr
</servlet-name>
<servlet-class>
ProxySvr
</servlet-class>
</servlet>
и
<servlet-mapping>
<servlet-name>
ProxySvr
</servlet-name>
<url-pattern>
ProxySvr
</url-pattern>
</servlet-mapping>
Скопируйте файлы WeatherApplet.html и WeatherApplet*.class в каталог /tomcat/webapps/cxamples. Обратите внимание, что при этом также копируется несколько внутренних классов.
Отредактируйте файл WeatherApplet .html, изменяя дескриптор PARAM показанным ниже образом.
<PARAM NAME="queryBase" VALUE=
"http://localhost:8080/examples/ProxySvr?URL=http://iwin.nws.noaa.gov/iwin/">
Запустите контейнер сервлетов Apache Tomcat.
Запустите аплет с помощью приведенной ниже команды.
appletviewer http://localhost:8080/examples/ WeatherApplet.html
Иначе, откройте файл WeatherApplet. html в броузере. Для работы аплета броузер должен поддерживать Java 2 (например, броузеры Netscape 6 или Opera). Обратите внимание на то, что в данном случае не требуется использовать файл политик.
После проверки работоспособности аплета разберемся с тем, как прокси-сервер решает проблемы обеспечения безопасности при работе с аплетами. При выборе штата и типа отчета аплет запрашивает информацию у прокси-сервера на локальном компьютере. Прокси-сервер затем обращается к Национальному метеорологическому серверу, получает данные и передает их аплету.
Этот способ кажется немного хлопотным, но именно так можно решить проблему обеспечения безопасности, потому что аплет непосредственно общается только с прокси-сервером. При этом прокси-сервер не может выбирать документы, доступные с компьютера, на котором выполняется аплет.
Данный метод можно использовать для распространения аплетов, предназначенных для сбора информации с других Web-узлов. Действительно, задача сбора информации передается серверу, а аплет используется для представления результатов.
Однако, теперь проблему обеспечения безопасности придется решать владельцу прокси-сервера. Инсталлируя прокси-сервер на Web-сервере нужно позаботиться о том, какие файлы можно будет копировать или просматривать всем пользователям с помощью этого сервера. При этом нужно соответствующим образом конфигурировать сеть, чтобы предотвратить доступ к конфиденциальным файлам. Например, можно задать такую конфигурацию прокси-сервера, при которой могут извлекаться файлы только с определенными URL-указателями. В данном примере полезно разрешить извлечение только тех запросов, URL-указатели которых начинаются с фрагмента http: / /iwin.nws.noaa. gov.
Имеет ли смысл создавать прокси-сервер только для извлечения информации и передачи ее аплету? В нашем примере это необходимо сделать, но в общем случае прокси-сервер удобнее использовать для кэширования информации, что позволяет повысить производительность обработки нескольких запросов и предварительной обработки полученной информации. В этом примере обработка на стороне сервера использовалась только для решения проблемы обеспечения безопасности. Однако, во многих ситуациях на сервере бывает удобно обработать как можно больше данных, потому программное обеспечение проще контролировать и сопровождать. Аплеты по-прежнему занимают свое место, т.е. организуют взаимодействие с пользователем и представляют полученные результаты. В типичном 3-уровневом (или п-уровневом) приложении аплет работает в паре с сервлетом или другим программным обеспечением сервера, предназначенным для выполнения интенсивной обработки данных.
Работа с базами
данных —интерфейс JDBCданных —интерфейс JDBCв этой главе...
данных —
интерфейс JDBC
данных —
интерфейс JDBC
т Структура интерфейса JDBC
т Язык SQL
т Инсталляция интерфейса JDBC
т Основы программирования интерфейса JDBC
т Просмотр и обновление результатов выполнения запроса
Метаданные
Транзакции
Более сложные методы управления соединениями
•) Хетом 1996 года компания Sun выпустила первую версию интерфейса для организации доступа Java-приложений к базам данных JDBC (Java DataBase Connectivity). Настоящий интерфейс позволяет программистам соединяться с базой данных, запрашивать и обновлять данные с помощью языка структурированных запросов (Structured Query Language — SQL). Этот язык фактически стал стандартным средством доступа к базам данных. Появление интерфейса — одно из наиболее заметных достижений среди средств программирования для платформы Java. Это связано не только с тем, что системы управления базами данных являются одними из наиболее популярных компонентов программного обеспечения. В этом секторе рынка представлено очень большое количество программных продуктов. Почему же потенциальные возможности JDBC так высоко оцениваются? Дело в том, Java и JDBC обладают очень весомым преимуществом по сравнению с другими инструментами для работы с базами данных.
Программы, созданные с помощью Java и JDBC, не зависят от используемой платформы и поставщика программного обеспечения.
Одна и та же программа на языке Java может одинаково хорошо работать под управлением операционной системы Windows NT, Solaris или в составе системы управления базой данных (СУБД) на основе платформы Java. При этом допускается перемещение данных из одной базы данных в другую, например, из Microsoft SQL в Oracle или даже в крошечную базу данных, встроенную в каком-то аппаратном устройстве. В результате чтение данных может выполнять одна и та же программа. Именно эта возможность выгодно отличает Java и JDBC от других традиционных способов программирования баз данных. Дело в том, что обычно для операций чтения данных применяется специализированный язык программирования используемой СУБД. В результате, полученное приложение может работать на ограниченном числе платформ. Благодаря своей универсальности, Java иJDBC в недалеком будущем заменят все собственные языки программирования баз данных и интерфейсы доступа к данным, которые сейчас используются многими поставщиками программного обеспечения: Oracle, Informix и Microsoft.
Некоторые поставщики программного обеспечения для СУБД встраивают виртуальную машинуJavaдля предоставления возможности выполнения хранимых процедур на языке Java. Эта технология является частью спецификации SQLJ. Более подробную информацию о ней можно получить по адресу http: //www. sqlj. org.
В составе платформы Java версии 2, выпущенной в 1998 году, также была выпущена версия 2 интерфейса JDBC. Во время создания книги интерфейс JDBC версии 2 еще не получил всеобщего распространения, хотя существует большое количество драйверов для JDBC версии 2. В этой версии интерфейса JDBC представлено несколько важных усовершенствований, например, прокручиваемые курсоры и поддержка сложных SQL-типов. В данный момент заканчивается работа над спецификацией интерфейса JDBC версии 3, которая, в основном, содержит усовершенствования технического характера. В этой главе учитываются некоторые различия между разными версиями интерфейса JDBC.
Учтите, что JDK не содержит никаких инструментов для программирования баз данных с помощью Java. Для использования конструкторов форм, запросов или отчетов необходимо использовать программные продукты сторонних разработчиков. "Корпоративные" или "профессиональные" версии интегрированных сред разработки для платформы Java, например, Visual Cafe или JBuilder, обычно содержат все необходимые инструменты для работы с базами данных.
В этой главе рассматриваются следующие вопросы.
Некоторые особенности использования API-интерфейсаJDBC.
Введение в основы языка SQL, который является стандартным языком доступа к базам данных.
Подробные примеры использования интерфейса JDBC для программирования наиболее распространенных способов работы с базами данных.
В течение многих лет для доступа к базам данных было придумано большое количество эффективных и безопасных приемов работы на основе индексов, триггеров, хранимых процедур и инструментов управления транзакциями. В интерфейсе JDBC поддерживаются все эти усовершенствования, но их подробное обсуждение выходит за рамки данной главы. Этой теме посвящено большое количество очень объемных книг. В главе представлены только очень общие сведения, необходимые для эффективного использования интерфейса JDBC. Более подробную информацию по этой теме можно получить в книге Мэйден Фишер (Maydene Fisher), Рика Гартелла (Rick Gartell), Грэхема Гамильтона (Graham Gamilton) и Марка Хапнера (Mark Hapner) JDBC API Tutorial and Reference, Seth White [Addison-Wesley 1999].
Структура интерфейса JDBC
С самого начала разработчики технологии Java в компании Sun понимали потенциальные преимущества Java при работе с базами данных. С 1995 года они начали работу по расширению стандартной библиотеки Java для организации взаимодействия с языком SQL для организации доступа к базам данных. Сначала они попробовали создать такие расширения Java, которые позволили бы осуществлять доступ к произвольной базе данных только средствами Java. Очень скоро разработчики убедились в бесперспективности такой постановки задачи. Кроме того, поставщики программного обеспечения для баз данных всеми силами стремились помочь сотрудникам в деле создания стандартного сетевого протокола доступа к базам данных, но только если за основу будет принят их собственный протокол.
В конечном итоге поставщики СУБД и инструментов доступа к базам данных все-таки согласились с тем, чтобы сотрудники Sun создали API-интерфейс на языке Java для SQL- доступа к данным вместе с менеджером драйверов, который позволил бы сторонним драйверам соединяться со специфическими базами данных. Таким образом, поставщики СУБД могли создавать свои собственные драйверы для взаимодействия с менеджером драйверов. Для этого предполагалось создать простой механизм регистрации сторонних драйверов со стороны менеджера драйверов. Этот механизм должен включать все требования, изложенные в API-интерфейсе менеджера драйверов.
После длительного обсуждения API-интерфейс постепенно превратился в API- интерфейс JDBC, а все правила создания драйверов были инкапсулированы в JDBC- интерфейсе поставщика услуг (service provider interface— SPI). (SPI-интерфейс представляет интерес только для поставщиков СУБД и инструментов доступа к данным и здесь не рассматривается.)
API-интерфейс JDBC основан на очень успешной модели ODBC-интерфейса компании Microsoft, который предоставляет интерфейс доступа к данным на основе языка С. JDBC и ODBC основаны на одной идее: программы, совместимые с JDBC API-интерфейсом, могут взаимодействовать с менеджером драйверов JDBC, который использует подсоединенные драйверы для организации взаимодействия с базой данных.
Список имеющихся JDBC-драйверов можно найти по адресу http: //industry. 1 ■ java.sun.com/products/jdbc/drivers.
Интерфейс JDBC состоит из двух уровней. Высший уровень образует API- интерфейс JDBC, который взаимодействует с API-интерфейсом менеджера драй-
веров, посылая ему SQL-команды. Этот менеджер должен (незаметно для программиста) обмениваться данными с разными драйверами сторонних разработчиков, которые соединяются с базой данных и возвращают результаты запросов или выполняют заданные операции с данными.
JDBC-спецификация на самом деле позволяет передавать драйверу практически любую строку, которую драйвер передает базе данных. Эта возможность позволяет использовать специализированные версии SQL, которые могут поддерживаться данным драйвером и связанной с ним базой данных.
Это все означает, что программистам придется работать только на уровне Java/JDBC, как показано на рис. 4.1.
Рис. 4.1.Схема взаимодействия JDBC и базы данных
Все JDBC-драйверы можно разбить на следующие типы.
е Драйвер типа 1 транслирует JDBC в ODBC и для взаимодействия с базой данных полагается на драйвер ODBC. Компания Sun включила в состав JDK один такой драйвер — мост JDBC/ODBC. Однако, для его использования требуется соответствующим образом установить и конфигурировать ODBC-драйвер. В первом выпуске JDBC этот мост
предполагалось использовать только для тестирования, а не для рабочего применения. В настоящее время уже имеется большое количество более удачных драйверов.
Драйвер типа 2 создается преимущественно на языке Java и частично на собственном языке программирования, который используется для взаимодействия с клиентским API-интерфейсом базы данных. Для использования такого драйвера нужно помимо библиотекиJava установить специфический для данной платформы код.
Драйвер типа 3 создается только на основе библиотеки Java, в которой используется независимый от базы данных протокол взаимодействия сервера и базы данных. Этот протокол позволяет транслировать запросы базы данных в специфические запросы базы данных. Клиентская библиотека не зависит от базы данных, что упрощает ее установку.
Драйвер типа 4 создается только на основе библиотеки Java, которая транслирует JDBC-запросы непосредственно в специфический протокол базы данных.
Большинство поставщиков баз данных применяют для них драйверы типа 3 или 4. Более того, некоторые компании специализируются на создании драйверов, которые позволяют добиться лучшего соответствия принятым стандартам, поддерживают большее количество платформ, обладают более высокой производительностью или надежностью, чем драйверы, предлагаемые поставщиками СУБД.
В общем, основные цели интерфейса JDBC можно сформулировать следующим образом.
Программисты получают возможность осуществлять доступ к любым базам данных с помощью языкаJava на основе стандартных средств языка SQL (или его специализированных расширений), но руководствуясь только соглашениями языка Java. (Все драйверы JDBC должны поддерживать SQL хотя бы начальной версии SQL 92.)
Поставщики СУБД и инструментов доступа к данным могут создавать драйверы только низкого уровня. Таким образом, они могут оптимизировать их драйверы для своих специфических программных продуктов.
DDНа конференции JavaOne в мае 1996 года представители Sun указали перечисленные ниже причины отказа от модели ODBC.'
D
D
Модель ODBC трудна в изучении.
Модель ODBC имеет всего несколько команд с большим количеством параметров. Однако, стиль программирования на языке Java основан на использовании большого количества простых и интуитивно понятных методов.
Модель ODBC основана на использовании указателей типа void* и других компонентов языка С, которые отсутствуют в языке Java.
Модель ODBC менее безопасна и труднее для распространения, чем решение только на основе Java.
Типичные примеры использования JDBC
JDBC можно использовать в приложениях и аплетах. В аплете в таких случаях применяются обычные ограничения безопасности. По умолчанию менеджер обеспечения безопасности не доверяет аплетам, созданным на языке программированияJava.
В частности, аплеты, использующие JDBC, способны открывать соединение с базой данных на сервере, с которого они копируются. Это значит, что Web-сервер и сервер базы данных должны находиться на одном компьютере, что не всегда имеет место. Конечно, Web-сервер может иметь proxy-сервер, который способен маршрутизировать трафик базы данных на другой компьютер. При использовании подписанных аплетов это ограничение может быть в некоторой степени устранено.
С другой стороны, приложения обладают полной свободой доступа к удаленным серверам баз данных. При создании программ с технологией клиент/сервер (как показано на рис. 4.2) для доступа к базе данных часто имеет смысл использовать приложение, а не аплет.
Протоколбазы данных
Рис. 4.2. Приложение с технологией клиент/сервер
Однако, современная тенденция развития программного обеспечения заключается в переходе от архитектуры клиент/сервер к "трехуровневой модели" или даже более совершенной “^уровневой модели". В трехуровневой модели клиент не создает вызовы к базе данных. Вместо этого клиент обращается к промежуточному уровню на сервере, который в свою очередь выполняет запросы к базе данных. Трехуровневая модель имеет два преимущества. Она отделяет визуальное представление (на компьютере-клиенте) от бизнес- логики (промежуточный уровень) и данных (в базе данных). Таким образом, становится возможным доступ к тем же данным и тем же бизнес-правилам со стороны нескольких клиентов, например, Java-приложений, аплетов или Web-форм.
Связь между клиентом и промежуточным уровнем может быть создана на основе протокола HTTP (при использовании Web-броузера в качестве клиента), RMI-метода (при использовании приложений или аплетов), подробнее об этом рассказывается в главе 5, "Работа с удаленными объектами", или с помощью какого-то другого механизма. Интерфейс JDBC используется для управления взаимодействием между промежуточным уровнем и базой данных. На рис. 4.3 схематически показана базовая архитектура трехуровневой модели. Нужно иметь в виду, что у этой модели существует множество вариантов. В частности, в Java 2 Enterprise Edition определена структура серверов приложений, которые управляют модулями кода под названием компоненты Enterprise JavaBeans и предоставляют такие важные сервисы, как распределение нагрузки, кэширование запросов, обеспечение безопасности и организация простого доступа к базам данных. В этой архитектуреJDBC играет важную роль передачи сложных запросов к базе данных. (Более подробную информацию по этой теме можно найти по адресуhttp:/ /java.sun.com/j2ee.)
Рис. 4.3. Архитектура трехуровневой модели
Язык SQL
JDBC образует интерфейс для языка SQL, который, в свою очередь, образует интерфейс для большинства современных реляционных баз данных. Настольные базы данных обычно имеют графический интерфейс пользователя, который позволяет им непосредственно манипулировать данными, но доступ к серверным базам данных возможен только с помощью языка SQL. В большинстве настольных баз данных также предусмотрено использование языка SQL, но, как правило, в нем не поддерживается полный набор функций текущего стандарта ANSI SQL 92.
IlaKeTjDBC задуман как AFl-интерфейс для взаимодействия с SQL-командами доступа к базам данных. В этом разделе приводится краткое описание языка SQL, которого будет недостаточно для полного знакомства с данным языком. Для более тщательного изучения основ языка SQL можно порекомендовать книги Джеймса Мартина (James Martin) и Джо Лебена (Joe Leben) Client/Server Databases [Prentice-Hall, 1998] и книгу Криса Дейта (ChrisJ. Date) A Guide to the SQL Standard [Addison-Wesley, 1997].
База данных представляет собой набор именованных таблиц со строками и столбцами. Каждый столбец (или поле) имеет имя, а данные хранятся в строках, которые также принято называть записями,
В качестве примера базы данных рассмотрим табл. 4.1-4.4 с перечнем книг о языке HTML. Авторы любезно благодарят Кай Вальдман (Суе Н. Waldman) за. предоставленные данные (http://wwwiz.com/books).
Таблица4.1: ТаблицаAuthors
Author ID
NameURL
ARON
Aronson, Larryhttp://.
ARPA
Arpajian, Scotthttp://.
Таблица4.2
Title
ТаблицаBooks•-
ISBN
Piblisher_ID URL
Price
Beyond HTML
0-07-882198-3
00788
http
://.
• •27.95
10 Minute Guide to HTML
0-78970541-9
07897
http
://.
• . 15.00
Таблица4.З.
ISBN
ТаблицаBooksAuthors
Author ID
Se<j_No
1-56-884454-9
TAYL
1
1-56-884645-2
SMIT
1
1-56-884645-2
BEBA
2
Publisher ID
Name
URL
01262
Academic Press
www.apnet.com
18835
Coriolis
www.coriolis.com
На рис. 4.4 показан внешний вид таблицы Books, а на рис. 4.5 — результат соединения таблицы Books и таблицы Publishers. Обе таблицы содержат столбцы с числовым кодом издателя. При соединении таблиц по этому коду в результате запроса на соединение получится таблица, которая содержит данные из обеих таблиц, т.е. информацию о книге, название и адрес Web-узла издательства. Обратите внимание на то, что информация с названием и адресом Web-узла издательства несколько раз дублируется, так как у нас появилось несколько строк, которые относятся к одному издательству.
т. Мили^оК Queiy - IQueiy1|
Г.) "
0-07-882198-3 0-78970541-9 1.56-884454-9 1-56884645-2 1-56276390-3 1-57-169050-7 1-57169066-2 1-56-276352-0
a 0-78970812-4 0-13-232331-1 5| 1-56-884647 9 1-56884990-7
Iff 0-13-242488-6 j4 0-02188448 8 ^1 1-55615948-X -^ 0-53451626 2 -^ 1-56-604288-7 M 0-07-912100-4 ;»^ 0-78970867-1 S 0-47-114242-5
i 15.00 ! 19.99
20 00 24 99 i 39.99 : 39.99 i 24.95 : 34.99 *40.00
30 00 1299 .40 00
17.95 8 29.95 *36.00 i 39.95 .45.00
1799 30 00
07897 15688 15688 15627 15716 15716 15627 07897 ’01356 15688 15688 01356 00218 15561 05345
И 5660 .00791 07897 ■04711
-Л&
i Beyond HTML
nvwm
vwm
Pagesfa
Use HTML 3 < www mcp
. H T M L 3 H ow-T оj w3 mate, com/waite/waite/books. new/HT ML_3„How_T o/html/htrnl3htcov. htn ।
HTML 3 Interactivew3Interactive
HTML 3 0 Manual ofwww mcphtml
HTMLExample
F wwwhtml
lot Dummies 2ndidgbooks
HTML For Dummieswwwj
HTML For Fun and Profit www sun
theWWWhtml
HTM
; HTML Plusli $chofar.ib.vt.edu/pweMhim^^
HTML Publishing for
HTML Publishing the
HTML
Иай-Люс*1
<
4.4. Таблица с данными о книгах
U%
www.mcp.comZque/
j.Miciowfl Uueiy ■ IQuefvIl
гчтц::г’ кгбв
№
www idgbooks com/Web Pages lot Dummies
www mcp com/zdpress/ How to Use HTML 3
www wakeHTML 3
wwwHTML 3 Interactive Course
www mcpHTML 3 0 Manual Style (2nd Ed J
www mcp
10-78970541-9 1-56-884454-9 1-56884645-2
13
“ 1-57-169050-7
2
276352-0 04 0-13
9
7
6
“ 0-02188448-8
1 55615948X
“ 0-53451626 2
17
4
1
— 0-47114242-5
■15.00 И9.99
2000
2499
3999
99 2495
'34
4000
30 00
1299
4000
1795
29 35
36.00 39.95
4500 17
: 30.00
07897 h 5688
15S88
15627
15716
15716
15627 07897
01356
15688
■15688
01356
00218
15561
05345
15660 00791 07897 04711
^ BS3I
S«3
.[Que;
Hog Books
IDG Books
; Ziff-Davis iWaie 'Wake
Davis
Que
I PrenticeHall
IDG Books
IDG Books
PrenticeHall
Peachpit Press Microsoft Press IntegratedMedia Group Netscape Press
; McGraw-HJ
Que
Wiley
nOMinuee Guide Io HTML 1 Г.-nlL.- Г—.И < Г-к D — »
www prenhall
www idgbooks com/ www tdgbooks com/ www prenhall
www peachpit com/
HTML by Example
HTMLAn Internet Publishing
HTML for Dummies 2'nd Edition
HTMLDummies Quick Refere
HTML For Fun and Profit Gold Signature Edition HTML for theWebGO
www rocfosoftHTML Action
wwwHTML Plus!
wwwcor HTML Publishing for Netscape Windows Edition
wwwHTML Publishing on the
www mcpHTML Quick Reference
1
www
HTML Sourcebook 2nd Ed
Рис. 4.5. Результат соединения двух таблиц
Преимущество соединения таблиц заключается в том, что в этом случае можно было бы избежать ненужного дублирования данных в таблицах базы данных. Например, в простейшей структуре базы данных таблица Books может содержать столбцы с названием и адресом Web-узла издательства. Но в таком случае данные будут дублироваться уже не на уровне результатов запроса, а в самой базе данных. При изменении адреса Web-узла издательства придется изменить эти данные во всех записях. Ясно, что при выполнении такой трудоемкой задачи могут легко возникнуть ошибки. В реляционной модели базы данные распределяются среди нескольких таблиц таким образом, чтобы никакая информация не дублировалась без особой на то необходимости. Например, адрес Web-узла каждого издательства хранится в единственном экземпляре в таблице с данными об издательствах. В случае необходимости данные из разных таблиц можно легко соединить.
На этих рисунках показан инструмент с графическим интерфейсом пользователя, предназначенный для работы с базами данных. Многие поставщики программного обеспечения предлагают разнообразные интерактивные инструменты создания запросов с помощью визуальных манипуляций со столбцами и ввода данных в готовые формы. Такие средства создания запросов называются инструментами создания запросов по образцу {query by example— QBE). При использовании языка SQL запрос создается в текстовом виде в строгом соответствии с синтаксисом языка SQL.
SELECT Books.ISBN, Books.Price, Books.Title,
Books.Publisher_Id, Publishers.Name, Publishers.URL
FROM Books, Publishers
WHERE Books.Publisher_Id =
В остальной части этого раздела описываются основные способы создания таких текстовых запросов. Опытные читатели могут пропустить этот раздел.
По соглашению ключевые слова SQL записываются в верхнем регистре, хотя это правило не является обязательным.
Команда SELECT сможет применяться в разных целях, например, для выбора всех элементов таблицы Books, как показано ниже.
SELECT * FROM Books
Предложение FROM является обязательным, оно сообщает базе данных о тех таблицах, в которых нужно выполнить поиск данных.
В команде SELECT можно указать любые нужные столбцы.
SELECT ISBN, Price, Title FROM Books
Выбор строк можно офаничить с помощью условия, указанного в предложении WHERE.
SELECT ISBN, Price, Title FROM Books
WHERE Price <= 29.95
Следует иметь в виду, что для операций сравнения в SQL используются операторы = и о, а не == или ! = (как при программировании наязыкеJava).
Некоторые поставщики баз данных используют оператор ! = для операций сравнения, но учтите, что это не допускается стандартом SQL, а потому не рекомендуется.
В предложении WHERE могут применяться символы подстановки вместе с оператором LIKE. Помимо символов подстановки * и ?, символ % можно использовать для обозначения любого количества символов, а символ для обозначения одного символа, например так, как показано ниже.
SELECT ISBN, Price, Title
FROM Books
WHERE Title NOT LIKE ’ %Н_М%'
Обратите внимание, что строки заключены в одинарные, а не в двойные кавычки. Одинарная кавычка внутри строки обозначается парой одинарных кавычек, например так, как показано ниже.
SELECTTitle
FROM Books
WHERE Books.Title LIKE
В этом примере требуется выполнить поиск всех названий КНИГ, которые содержат одинарную кавычку.
Чтобы выбрать данные из нескольких таблиц, нужно перечислить их так, как показано ниже.
SELECT * FROM Books, Publishers
Без предложения WHERE этот запрос не представляет большого интереса, потому что позволяет получить все комбинации строк из обеих таблиц. В нашем случае таблица Books содержит 37 строк, а таблица Publishers— 18 строк. Поэтому результат выполнения такого запроса будет содержать 37*18 строк с большим количеством дублированных данных. Допустим, что нужно найти только те книги, которые выпущены издательствами, перечисленными в таблице Publishers. Для поиска такого соответствия между книгами и издательствами можно использовать показанный ниже запрос.
SELECT * FROM Books, Publishers
WHERE Books.Publisher_Id =
Результат выполнения этого запроса содержит 37 строк, т.е. по одной строке для каждой книги, так как издательство каждой книги упомянуто в таблице Publishers.
При работе с несколькими таблицами в двух разных местах может упоминаться одно и то же имя столбца, как в показанном выше примере (столбец Publisher_Id из таблицы Books и столбец Publisher_Id из таблицы Publishers). Если такое упоминание столбца приводит к неоднозначному толкованию, для него следует использовать префикс в виде имени таблицы (например, Books. Publisher_id).
Итак, мы рассмотрели все части запроса, который был представлен в самом начале этого раздела.
SELECT Books.ISBN, Books.Price, Books.Title,
Books.Publisher_Id, Publishers.Name, Publishers.URL
FROM Books, Publishers
WHERE Books.Publisher_Id =
Для изменения данных в базе данных можно использовать так называемые активные запросы (т.е. запросы, которые приводят К перемещению или изменению данных). Например, предположим, что вы хотели бы сократить на 5 долларов текущую цену для всех книг, в названиях которых не упоминается строка "HTML 3".
UPDATE Books
SET Price = Price - 5.00
WHERE Title NOT LIKE '%HTML 3%'
Аналогично можно изменить сразу несколько полей, используя несколько разделенных запятыми предложений SET. Существует несколько других ключевых слов SQL, которые можно использовать в активных запросах. Помимо UPDATE, вероятно, наиболее важным среди них является DELETE, которое позволяет удалять записи, удовлетворяющие заданным критериям. Наконец, в SQL предусмотрено использование встроенных функций для вычисления средних значений, поиска максимальных и минимальных значений в столбце и многое другое. (Более подробную информацию можно найти в специализированных книгах о языке SQL.)
Для вставки новых данных в таблицу обычно используется команда INSERT.
INSERT INTO Books
VALUES ('Beyond HTML', '0-07-882198-3', '00788', '', 27.95)
Для вставки каждой строки придется создавать отдельную команду INSERT.
Прежде чем создавать запросы, изменять и вставлять данные, необходимо предоставить свободное место для хранения данных, т.е. создать таблицу. Для создания новой таблицы используется команда CREATE TABLE, в которой указываются имена и типы данных каждого столбца.
CREATE TABLE Books
(
TitleCHAR(60) ,
ISBN CHAR(13),
Publisher_Id CHAR(5),
URL CHAR(80),
Price DECIMAL(6,2)
)
В табл. 4.5 перечислены наиболее распространенные типы данных языка SQL.
Таблица 4.5. Типы данных языка SQL
Тип данных
Описание
INTEGERилиINT
Обычно 32-разрядное целое число
SMALLINT
Обычно 16-разрядное целое число
NUMERIC (m, n) .DECIMAL (m,n) или
DEC(m,n)
FLOAT (n)
Десятичное число с т цифрами и п знаками после запятой
Число с плавающей запятой с п знаками точности
REAL
Обычно 32-разрядное число с плавающей запятой
DOUBLE
Обычно 64-разрядное число с плавающей запятой
Окончание табл. 4.5
Тип данных
Описание
CHARACTER (n) или CHAR (п)
Строка с фиксированной длиной из п
СИМВОЛОВ
VARCHAR(n)
Строка с переменной длиной из п символов
BOOLEAN
Логическое значение
DATE
Календарная дата, зависит от реализации
TIME
Время, зависит от реализации
TIMESTAMP
Дата и время (зависят от реализации)
BLOB
Большой бинарный объект
CLOB
Большой символьный объект
В этой книге не рассматриваются дополнительные предложения, ключи и ограничения, которые часто используются вместе с командой CREATE TABLE.
Инсталляция интерфейса JDBC
Сначала нужно найти систему управления базами данных, совместимую с JDBC, a также создать экспериментальную базу данных, например, под названием COREJAVA. Создайте новую базу данных или попросите администратора базы данных создать ее, а также наделить вас правами для создания, обновления и удаления таблиц.
Если ранее вам никогда не приходилось инсталлировать базу данных с архитектурой клиент/сервер, то процесс ее установки, конечно, покажется очень сложным, в котором довольно трудно обнаружить причину возможной неудачи. Поэтому в таких случаях рекомендуется воспользоваться услугами опытных специалистов.
Одним из альтернативных вариантов может быть установка базы данных, полностью основанной на языке Java, например Cloudscape (http://www.cloudscape.com), Pointbase (http://www.pointbase.com) или InstantDB (http://www.lutris.com/ products/instantDB). Эти базы данных менее мощны, но более просты в установке. Наиболее сложная часть их инсталляции заключается в установке пути доступа к классам, что не должно представлять собой большой проблемы даже для не очень опытного программиста на языкеJava.
Практически все поставщики СУБД предусмотрели в своих продуктах поддержку драйверов JDBC. Для загрузки драйвера необходимо внимательно прочесть инструкции поставщика и установить соединение с базой данных. В следующем разделе будут рассмотрены этапы установки двух типичных баз данных (IBM DB2 и Cloudscape), которые можно использовать на нескольких платформах.
Инструкции для разных баз данных имеют аналогичный вид, хотя, конечно, могут отличаться некоторые подробности их реализации.
Не рекомендуется использовать драйвер-мост JDBC/ODBC, предусмотренный в Java 2 SDK, и в еще большей степени не рекомендуется использовать драйвер, предусмотренный в настольной базе данных Microsoft Access. Они имеют не только запутанную процедуру инсталляции и конфигурирования, но и сложные ограничения драйвера-моста и настольной базы данных. Кроме того, эксперименты с такой конфигурацией не имеют ничего общего с принципами работы реальных баз данных.
В некоторых настольных базах данных предусмотрена поддержка языка SQL на основе собственного механизма, а не JDBC или ODBC. Например, компания Borland использует механизм BDE, а компания Microsoft — механизм Jet, которые обладают более высокой производительностью при работе с настольными базами данных, чем ODBC. Учтите, что эти механизмы не совместимы с Java.
Основы программирования интерфейса JDBC
Программирование классов для интерфейса JDBC не очень отличается от программирования обычных классов Java. На основе базовых классов JDBC для этого создаются объекты, функциональность которых может быть расширена с помощью механизма наследования.
Классы интерфейса JDBC содержатся в пакетахjava.sqlи javax.sql.
URL-указатель базы данных
Для установления соединения с базой данных необходимо указать источник данных и некоторые дополнительные параметры. Например, сетевым драйверам потребуется знать номер порта, а драйверам ODBC — другие атрибуты.
В JDBC используется синтаксис описания источника данных, аналогичный синтаксису URL-указателя.
j
jdbc:cloudscape:COREJAVA;create=true
В этих URL-указателях JDBC указаны базы данных DB2 и Cloudscape no имени COREJAVA согласно приведенному ниже синтаксису.
jdbc:название_подпротокола: другие_св едения
Здесь название_подпрстокоз необходимо для выбора специального драйвера для соединения с используемой базой данных.
Формат представления параметров в части другие^, св едет зависит от используемого подпротокола. Более подробные сведения об этом можно получить в документации поставщика СУБД.
Установление соединения
Сначала нужно найти классы драйверов JDBC. Поставщик СУБД обычно указывает имена классов интерфейсаJDBC в следующем виде.
COM.ibm.db2.jdbc.app.DB2Driver
COM.cloudscape.core.JDBCDriver
Затем нужно найти библиотеку, в которой располагается драйвер, например, db2java.zip или cloudscape.jar. После этого в переменных окружения следует указать полный путь к ним с помощью одного из представленных ниже способов.
Запустите СУБД и укажите путь с помощью аргумента командной строки — classpath.
Отредактируйте переменную окружения CLASSPATH.
Скопируйте библиотеку базыДсШН ЫХ в каталог jre/1 ib/ext.
Класс менеджера драйверов DriverManager отвечает за выбор драйверов базы данных и создание нового соединения с базой данных. Однако, перед активацией менеджера драйверов его нужно зарегистрировать.
Существует два способа регистрации менеджера драйверов. Свойство j dbc. drivers содержит список имен классов для тех. драйверов, которые менеджер драйверов зарегистрирует во время запуска. Имена драйверов в нем перечисляются через двоеточие.
Имена драйверов можно также задать в командной строке .
java -Djdbc.drivers--COM.ibm.db2.jdbc.app.DB2Driver MyProg
Кроме того, в программе можно предусмотреть считывание показанной ниже строки из файла свойств, а потом добавить это свойство в список системных свойств. (Более подробно свойства описываются в главе 2, "Коллекции".)
jdbc.drivers=COM.ibm.db2.jdbc.app.DB2Driver
Как уже говорилось выше, несколько разных драйверов можно указать через двоеточие.
COM. ibm.db2 . j dbc . app . DB2D.river:COM. cloudscape . core.JDBCDriver
Благодаря этому можно инсталлировать соответствующие драйверы, просто изменяя файл свойств.
Созданные далее программы будут считывать все параметры баз данных из файла свойств database.properties. Проверьте содержимое этого файла, чтобы оно соответствовало используемой базе данных.
Иначе драйвер можно зарегистрировать вручную с помощью загрузки соответствующего класса, как показано ниже.
Class . forName ( "COM. ibm.dbz .jdbc . app. DB2Driver” )
После регистрации драйверов можно установить соединение с базой данных, например, с помощью приведенного ниже кода.
String url =;
String username =;
String password = "wombat";
Connection conn = DriverManager.getConnection(url, username, password) ;
Менеджер драйверов пытается найти драйвер, который может использовать протокол, указанный в URL-указателе базы данных, перебирая все доступные драйверы, зарегистрированные менеджером драйверов.
В используемых здесь примерах для указания драйвера, URL-указателя, имени пользователя и пароля удобно применять файл свойств. Вот пример содержимого та- когофайла.
jdbc.drivers=COM. ibm.db2.jdbc.app.DB2Driver
jdbc.url=jdbc.db2.COREJAVA
jdbc.username=db2instl
jsword=wombat
А вот как выглядит код чтения файла свойств и установления соединения с базой данных.
Properties props = new Properties();
FilelnputStream in
= new Fileinputstream("database.properties");
props.load(in);
in.close() ;
String drivers =;
if (drivers != null)
System.setProperty("jdbc.drivers", drivers) ;
String url props .getProperty("jdbc. url"),-
String username = props.getProperty("jdbc.username") ;
String password = props . getProperty (11 jdbc . password") ;
return DriverManager .getConnection (url, username, password) ,-
Метод getConnection возвращает объект Connection, который используется для выполнения SQL-команд.
Прекрасным способом отладки проблем, связанных с JDBC, является использование средств трассировки JDBC. Для передачи сообщений трассировки следует вызвать метод DriverManager.setLogWriter. Он передает в поток PrintWriter подробныйотчетодеятельностиJDBC.
Тестирование установленной базы данных
Первая попытка инсталляции JDBC вероятно может показаться очень сложной. Для инсталляции JDBC потребуется внимательно изучить инструкции поставщика, потому что самая незначительная ошибка конфигурации может привести к появлению сообщений об ошибках. Сначала следует проверить установку базы данных без использования JDBC. Проверьте соединение с базой данных и выполните несколько SQL-команд. Далее в примерах используется база данных COREJAVA, НО вместо нее можно использовать любое другое название базы данных, которую создаст администратор базы данных.
Например, введите следующие команды.
CREATE TABLE Greetings (Name CHAR(20))
INSERT INTO Greetings VALUES ('Hello, World!')
SELECT * FROM Greetings
После этого в командной строке будут отображены данные Hello, World!.
Удалите таблицу с помощью следующей команды.
DROP TABLE Greetings
После проверки правильности установки базы данных и регистрации доступа к ней нужно собрать перечисленные ниже данные.
Учетное имя пользователя базы данных и пароль.
Имя базы данных (например, COREJAVA).
URL-указатель JDBC.
Имя JDBC-драйвера.
Расположение библиотечных файлов с кодом драйвера.
Первые два параметра зависят от установки базы данных, а остальные описываются в документации поставщика СУБД. Например, для СУБД DB2 компании IBM используются следующие значения перечисленных выше параметров.
Учетное имя пользователя базы данных — db2instl (по умолчанию), пароль — предлагается задать во время инсталляции.
Имя базы данных — COREJAVA.
Формат URL-указателя JDBC — j dbc : db2 :имя_базы_даиных.
Имя JDBC-драйвера — COM.ibm.db2 .jdbc.арр.DB2Driver.
Расположение библиотечных файлов с кодом драйвера —
/usr/IBMdb2/V7.1/java/db2java.zip.
Аналогично для СУБД Cloudscape используются такие значения этих параметров.
Учетное имя пользователя базы данных и пароль не задаются.
Имя базы данных — COREJAVA.
Формат URL-указателя JDBC — j dbc :cloudscape -.имя_базы_даннъ;х: create=true.
Имя JDBC-драйвера — COM.cloudscape.core. JDBCDriver.
Расположение библиотечных файлов с кодом драйвера —
/usr/local/cloudscape/lib/cloudscape.jar.
В листинге 4.1 приводится пример простой тестовой программы, которая используется для проверки правильности установки JDBC. Отредактируйте соответствующим образом файл свойств database.properties, а затем запустите тестовую программу с указанием в командной строке пути к библиотечным файлам, например так, как показано ниже.
java -classpath .: /usr/IBMdb2/V7.1/java/db2java.zip TestDB
(Учтите, что в операционной системе Windows для разделения разных путей в переменной окружения используется точка с запятой.)
Эта программа выполняет те же SQL-команды, которые использовались нами ранее при ручном тестировании базы данных. При получении сообщения об ошибке выполнения SQL- команд необходимо внимательно проверить правильность инсталляции. Очень часто ошибки являются результатом неправильной записи имен, путей, несоблюдения формата URL- указателя или неправильной конфигурации базы данных. Если в результате выполнения этой программы в командной строке будет отображена строка Hello, World!, значит все установки выполнены успешно и можно приступать к изучению следующего раздела.
Листинг4.1. TestDB.Iava
/**
©version 1.00 2001-06-30
@author Cay Horstmann
*/
import java.sql.*;
import. j import java.util.*;
/**
Программа проверки правильности конфигурации базы данных и драйвера JDBC
class TestDB
{
public static void main (String args[])
{
try
{
Connection conn = getConnection();
Statement stat = conn.createStatement();
stat.execute("CREATE TABLE Greetings (Name CHAR(20)) ") ; stat.execute(
"INSERT INTO Greetings VALUES ('Hello, World!')"),
ResultSet result
=* FROM Greetings");
result.next();
System.out.printIn(result.getstring(1)) ;
result.close();
stat.execute("DROPTABLE Greetings");
stat.close();
conn.close();
catch (SQLException ex) {
whi le (ex != null)
{
ex.printStackTrace();
ex = ex.getNextException() ;
catch (TOException ex)
{
} ex.printStackTracet) ;
} ''
/“
Установление соединения з соответствии со свойствами, заданными в файле database.properties ©return the database connection
*/
public static Connection getConnection() throws SQLException, lOException
{
Properties propsnew Properties();
FileinputStream in
new Fileinputstream ("database.properties");
props.load(in);
in.close();
String drivers = props.getProperty("jdbc.drivers");
if (drivers != null)
System.setProperty("jdbc.drivers", drivers);
String url =
String username =
String password = props.getProperty("jdbc.password");
return
DriverManager.getConnection(url, username, password)
}
Выполнение SQL-команд
Для выполнения SQL-команд нужно создать объект Statement с помощью объекта Connection, который генерируется после вызова метода DriverManager. getConnection.
Statement stat = conn.createStatement() ;
Затем нужно создать строку с нужной SQL-командой.
String command = "UPDATE Books"
+ " SET Price = Price — 5.00"
+ " WHERE Title NOT LIKE ' %HTML 3 % ' " ;
Для выполнения созданной SQL-команды используется метод executeUpdate класса Statement.
stat.executeupdate(command);
■ Один объект Statement можно использовать для выполнения нескольких несвязанных команд, но возвращен будет только один открытый результат выполнения запроса. Для параллельного получения нескольких результатов выполнения запроса нужно использовать несколько объектов statement.
Учтите, что существует, по крайней мере, одна СУБД (Microsoft SQL Server), в которой драйвер JDBC разрешает одновременно выполнять только одну команду. Для определения максимального количества параллельно выполняемых команд (допустимого драйвером JDBC) можно использовать метод getMaxStatements класса DatabaseMetaData.
На самом деле не рекомендуется параллельно обрабатывать сразу несколько результатов выполнения запроса. Если результаты как-то связаны, то в таком случае рекомендуется создать один комбинированный запрос и анализировать только один набор результатов его выполнения. Более эффективно организовать работу можно на основе комбинированных запросов, а не обрабатывая последовательно несколько наборов данных с результатами выполнения запроса с помощью языка Java.
Метод executeUpdate возвращает количество строк, полученных в результате выполнения SQL-команды. Например, в приведенном выше примере будет возвращено количество книг, цена которых понижена на 5 долларов.
Метод executeUpdate может применяться для выполнения команд INSERT, UPDATE и DELETE, а также команд определения данных CREATE TABLE и DROP TABLE. Но для выполнения команды SELECT нужно использовать другой метод, а
именно executeQuery. Существует также универсальный метод execute, который может применяться для выполнения произвольных SQL-команд, но он используется в основном для интерактивного создания запросов.
Метод executeQuery возвращает объект типа ResultSet с построчными результатами выполнения запроса.
ResultSet rs = stat.executeQuery("SELECT * FROM Books");
Для построчного анализа результатов выполнения запроса используется приведенный ниже цикл.
while (rs.next()) {
обработка строки
}
Способ последовательной обработки строк в классе ResultSet организован несколько иначе, чем в интерфейсах Iteratorи Enumeration, которые рассматривались в главе 2. В классе ResultSet начальное положение итератора задается перед первой строкой. Поэтому для перемещения его к первой строке нужно использовать метод next () .
При обработке отдельной строки нужно с помощью специальных методов получить содержимое каждого столбца.
String isbn = rs.getString(1); float price = rs.getDouble("Price");
Для каждого типа данных языка Java предусмотрен отдельный метод извлечения данных, например getString и getDouble. Каждый из них имеет два способа представления, основанных на числовом и строковом типе аргумента. При использовании числового аргумента метод извлечет данные из столбца с указанным аргументом- номером. Например, метод rs.getString(1) возвратит значение из первого столбца текущей строки.
В отличие от массивов нумерация в столбцах базы данных начинается с 1.
При использовании строкового аргумента метод извлечет данные из столбца с указанным аргументом-именем. Например, метод rs.getDouble("Price") возвратит значение из столбца с именем Price. Первый способ на основе числового аргумента более эффективен, но строковые аргументы позволяют создать более читабельный и простой для сопровождения код.
Каждый метод извлечения данных выполняет преобразование типа, если указанный тип не соответствует фактическому типу. Например, метод rs.getString("Price") преобразует число с плавающей запятой из столбца Price в строку.
Типы данных языков SQL и Java не всегда совпадают. В табл. 4.6 приведен список основных типов данных языка SQL и их эквивалентов в языке Java.
Таблица 4.6. Типы данных языка SQL и соответствующиея
типы данных языка Java
Типы данных языка SQL
Типы данных языка Java
INTEGERилиINT
int
SMALLINT
short
NUMERIC(m,n), DECIMAL(m,n) или
java.sql.Numeric
DEC(m,n)
FLOAT(n)
double
REAL
float
DOUBLE
double
CHARACTER (n) или CHAR (n)
String
VARCHAR(n)
String
BOOLEAN
boolean
DATE
java.sql.Date
TIME
java.sql.Time
TIMESTAMP
java.sql.Timestamp
BLOB
java.sql.Blob
CLOB
java.sql.Clob
ARRAY
j
Более сложные ТИПЫ данных языка SQL (JDBC 2)
Помимо чисел, строк и дат многие СУБД способны хранить большие объекты (large objects). В языке SQL большие бинарные объекты сокращенно называются BLOB- объекты (binary large objects), большие символьные объекты— CLOB-объекты (character large objects). Методы getBlob и getClob возвращают объекты типа BLOB и CLOB. Эти классы обладают соответствующими методами для извлечения отдельных байтов или символов больших объектов.
Тип ARRAY языка SQL представляет собой последовательность значений. Например, таблица Student может иметь столбец с оценками Scores типа ARRAY OF INTEGER, т.е. с массивом целочисленных значений. Для возвращения данных типа j ava . sql Array (который отличается от класса java.lang.reflect . Array, рассмотренного ранее в томе I) используется метод getArray. В интерфейсе java.sql.Array также предусмотрены методы извлечения значений массива.
В языке SQL типы BLOB, CLOB И ARRAY впервые появились в стандарте SQL3, а что касается Java, то тип ARRAY впервые появился в интерфейсе JDBC 2, а типы BLOB И CLOB были существенно усовершенствованы в интерфейсе JDBC 2.
При получении BLOB-объекта или массива из базы данных, фактически, данные будут ПОЛУЧСНЫ только после запроса отдельных значений. Это сделано для повышения эффективности работы, потому что размер таких данных может быть огромным.
В некоторых данных допускается хранение определенных пользователем типов данных. В JDBC 2 поддерживается механизм автоматического отображения структурных типов SQL на типы Java.
В этой главе BLOB-объекты, массивы или определенные пользователем типы дан-
ных не рассматриваются.
•
java.sql.DriverManager
Метод static Connection getConnection(String url, String
pit
•
user, String password) устанавливает соединение с базой данных и возвращает объект Connection.
Параметры:
urlURL-указатель
userучетное имя пользователя базы данных
password пароль
java.sql.Connection
Метод Statement c rea t e S t a tement () создает объект Statement, кото-
kpil
•
•
рый может использоваться для выполнения SQL-команд без параметров.
Метод void close () закрывает текущее соединение.
java. sql. Statement
Метод ResultSet executeQuery (String sql) выполняет заданную в
•
•
виде строки SQL-команду и возвращает объект ResultSet с результатами выполнения этой команды.
Параметры:
sqlSQL-команда
Метод int executeUpdate (String sql) выполняет заданные в виде строки SQL-команды INSERT, UPDATE и DELETE, а также команды определения данных CREATE TABLE и DROP TABLE. Он возвращает количество строк, задействованных в ходе выполнения этой команды, или - 1 , если для данной SQL-команды не создан счетчик обновлений.
Параметры:
sqlSQL-команда
Метод boolean execute (String sql) выполняет заданную в виде строки SQL-команду и возвращает true, если эта команда возвращает какие-то строки, или falseв противном случае.
Параметры:
sqlSQL-команда
Метод int() возвращает количество строк, задейство
ванных в ходе выполнения предыдущей команды обновления, или -1 , если для данной SQL-команды не создан счетчик обновлений. Его следует вызывать только один раз для каждой выполняемой SQL-команды.
Метод ResultSet getResultSet () возвращает объект ResultSet с результатами выполнения предыдущей команды или null, если выполнение предыдущей команды не принесло никаких результатов. Его следует вызывать только один раз для каждой выполняемой SQL-команды.
sql. ResultSet
i • Метод boolean next () перемещает расположение текущей строки в зультатах выполнения SQL-команды на одну позицию вперед. После прохождения последней строки возвращает false. Учтите, что его нужно обязательно вызвать для перемещения к первой строке.
Методы ХххcolumNumber) и ХххString
columName), где Ххх обозначает тип данных, например int, double, String, Date и т.д., возвращают значение столбца по номеру или имени (соответственно) с преобразованием к указанному типу данных. Учтите, что допускаются не все варианты преобразования типов. Более подроб- нуюинформациюпоэтомуповодуможнополучить вдокументации.
Метод int(String columnName) возвращает номер
столбца с заданным именем.
Метод void close () закрывает текущий набор результатов выполнения запросов.
I™^yf3 ava•sql•SQLException
jБольшинствометодов могут инициировать эту исключительную си
туацию SQLException, а потому нужно быть готовым к ее обработке с помощью перечисленных ниже методов.
Метод String getSQLState () возвращает информацию о состоянии SQL-команды, отформатированную согласно
Метод int getErrorCode () позволяет использовать специфический код поставщика, предназначенный для обработки исключительной ситуации.
Метод SQLException позволяет передать управление обработкой исключительной ситуации другому обработчику, который может обладать большей информацией о происшедшей ошибке.
Ввод данных
Попробуем теперь создать первую программу на основе интерфейса JDBC. Было бы просто замечательно, если бы при этом мы могли попробовать выполнить некоторые из перечисленных выше запросов. Но, к сожалению, это невозможно, потому что наша база данных пуста. Причем, нам не удастся использовать какой-то файл с компакт-диска, поскольку в языке SQL не предусмотрены средства обмена данными среди баз данных разных поставщиков. Язык SQL предназначен не для работы с файлами, а для выполнения запросов и обновления базы данных. Конкретный способ и эффек-
тивность выполнения SQL-команд зависит от реализации СУБД. Поставщики СУБД прилагают огромные усилия для создания оптимальных стратегий оптимизации выполнения запросов и хранения данных с использованием совершенно разных механизмов. Таким образом, несмотря на переносимость SQL-команд, представление данных не обладает таким свойством.
Для решения проблемы ввода данных следует использовать набор текстовых файлов, которые содержат SQL-команды создания таблиц и вставки данных в них. Ниже будет представлена программа построчного чтения файла с такими SQL-командами и их выполнения.
Вот как выглядит программа чтения текстового файла с данными.
CREATE TABLE Publishers (Publisher_Id CHAR(5), Name CHAR(30), URL CHAR(80))
INSERT INTO Publishers VALUES ('01262', 'Academic Press', 'www.apnet.com/')
INSERT INTO Publishers VALUES ( '18835', 'Coriolis',
'www.coriolis.com/')
В листинге 4.2 приводится код программы ExecSQL, которая считывает текстовый файл с SQL-командами и выполняет их. Даже если вас не интересует эта программа, ее все же придется выполнить, как показано ниже, поскольку это нужно для работы с другими примерами данной главы.
java ExecSQL Books.sql
java ExecSQL Authors.sql
j ava ExecSQL Publishers.sql
java ExecSQL BooksAuthors.sql
Перед запуском этой программы проверьте содержимое файла свойств database . properties, который должен иметь следующие строки.
jdbc.drivers=COM.cloudscape.core.JDBCDriver
jdbc.url=jdbc:cloudscape:COREJAVA;create-true
jdbc.username=,
jdbc.password=
Эти значения предназначены для работы с базой данных Cloudscape, поэтому для работы с другой базой данных их нужно отредактировать соответствующим образом.
Убедитесь в том, что правильно указаны драйвер базы данных и путь к библиотеке классов. В случае необходимости укажите их явным образом в командной строке так, как показано ниже.
java —classpath путь к драйверу:. ExecSQL 5СЬ файл
Вот как работает программа ExecSQL, представленная в листинге 4.2.
Устанавливается соединение с базой данных. Метод getConnection считывает свойства из файла database .properties и добавляет свойство jdbc .propertiesв список системных свойств. Менеджер драйверов использует свойство j dbc . properties для загрузки соответствующего драйвера базы данных. Для установления соединения метод getConnection использует свой- стваjdbc.url,jdbc.username и j dbc . pa s sword.
Открывается файл с SQL-командами. Если такого файла нет, пользователю предлагается ввести команды в командной строке. Таким образом, программу ExecSQL можно использовать для интерактивного ввода простых запросов.
Все заданные команды выполняются с помощью универсального метода execute. При получении результирующего набора строк этот метод возвращает значение true. Во всех четырех файлах с SQL-командами в конце содержится команда SELECT *, поэтому с ее помощью можно будет просмотреть все успешно вставленные записи.
После получения результатов выполнения запроса они будут показаны на экране. Для универсального представления этих данных нужно получить некоторые сведения о них, т.е. метаданные, которые позволят правильно отформатировать их. Более подробно метаданные рассматриваются в конце главы.
При наличии какой-либо исключительной ситуации, связанной с выполнением SQL-команды, нужно вывести сведения о ней.
В листинге 4.2 приводится код этой программы.
Листинг 4.2. ExecSQL.java
import java.io.*;
importjava.util.*;
import java.sql.*;
/**
Программа, демонстрирующая выполнение SQL-команд, которые содержатся в текстовом файле.
Вот как выглядит синтаксис ее вызова:
ava —classpath путь_к_драйв еру: . ExecSQL 50Ъ_файл
*/
class ExecSQL
{
public static void main (String args[])
{
try
Reader reader;
if (args.length == 0)
reader = new InputStreamReader(System.in);
else
reader = new FileReader(args[ 0 ] ) ;
Connection conn = getConnection();
Statement stat = conn.createStatement();
Buf feredReader in = new BufferedReader(reader);
boolean done = false;
while (!done) {
if (args.length == 0)
Sys tem.out.printIn(
"Enter command or a blank line to exit:") ;
String line = in.readLine();
if (line == null || line.length() == 0) done = true;
else
{
try
{ boolean hasResultSet = stat.execute(line); if (hasResultSet)
showResultSet(stat);
}
catch (SQLException ex)
{
while (ex != null)
{
ex.printStackTrace();
ex = ex.getNextExceptionf);
}
}
}
}
in.close();
stat.close();
conn.close();
}
catch (Exception ex)
{
ex.printStackTrace();
/**
Установление соединения на основе заданных
свойств в файле database.properties
©return the database connection
*/
public static Connection getConnection()
throws SQLException, IOException {
Properties props = new Properties();
Fileinputstream in
= new Fileinputstream("database.properties") ;
props.load(in);
in.close();
String drivers = props.getPropertyC jdbc.drivers") ;
if (drivers != null)
System.setProperty("jdbc.drivers", drivers);
String url =
String username =
String password = props.getProperty("jdbc.password");
return
DriverManager.getConnection(url, username, password)
/**
Вывод результатов выполнения команд @param stat the statement whose result set should be printed
*/
public static void showResultSet(Statement stat) throws SQLException
{
ResultSet result = stat.getResultSet();
ResultSetMetaData metaData =
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
if (i>1) Sys tern.out.print(", ") ;
System.out.print(metaData.getColumnLabel(i));
}
System.out.println();
while (result.next())
{
for (int i = 1; i <= columnCount; i++)
{
if (i>1) System.out.print(", ") ;
System.out.print(result.getString(i));
}
System.out.printin();
}
result.close();
Выполнение запросов
В данном разделе создадим приложение, которое сможет выполнять запросы по отношению к базе данных COREJAVA. Для работы этой программы в базе данных нужно создать описанные в предыдущем разделе таблицы. На рис. 4.6 показан внешний видэтогоприложения.
Lemay. Latinw Sams.net
И1В|11|В|1||^
'J 'Change prices-5 ОО*
(24.99,f each Yourself More Web Publishing with HTML tna Week"
124.99,Teach Yourself Web Publishing with HTML 3 0 in a Week, 26 j54.99,Teach Yourself Web Publishing with HTML 3.2 in 14 Davs
Рис. 4.6. Приложение QueryDB
В этой программе можно выбрать имя автора и/или издательство (либо оставить предлагаемый по умолчанию выбор всех авторов и издательств— Any), а затем щелкнуть на кнопке Query (Выполнить запрос). После чего в нижнем текстовом поле будут отображены все книги, которые соответствуют заданным условиям отбора.
С помощью программы можно изменить данные в базе данных. Выберите нужное издательство и введите число, обозначающее изменение цены, в текстовом поле возле кнопки изменения цены Change price (Изменить цену). После щелчка на кнопке цены на все книги этого издательства будут изменены на указанную величину, а в текстовом поле в нижней части приложения будет указано количество измененных строк. Для предотвращения случайного изменения сразу всех цен здесь не допускается выбор сразу всех книг. При изменении цен выбор автора игнорируется. После изменения цены можно выполнить запрос для проверки измененных данных.
Для выполнения этих действий в программе применяются предварительно подготовленные SQL-команды. Рассмотрим запрос на выборку всех книг некоторого издательства, независимо от их авторов.
SELECT Books.Price, Books.Title
FROM Books, Publishers
WHERE Books.Publisher_Id = AND Publishers.Name =
Вместо создания отдельной SQL-команды для каждого запроса со стороны пользователя программы следует предварительно подготовить запрос с подстановочной переменной и многократно использовать его, меняя только строковое значение подстановочной переменной. Эта функциональная возможность существенно повышает эффективность работы программы, потому что перед каждым выполнением запроса СУБД создает план его эффективного исполнения. Предварительно подготавливая запрос для последующего многократного повторного использования, можно избежать повторного создания плана выполнения запроса, который будет создан только один раз. (Причина, по которой не все запросы предварительно подготавливаются, заключается в том, что оптимальная стратегия может измениться из-за изменения данных. Часто приходится находить компромиссное решение между оптимизацией и эффективностью выполнения запросов.)
Подстановочная переменная в запросе обозначается символом ?. При использовании нескольких подстановочных переменных нужно внимательно следить за их расположением, чтобы правильно выполнять подстановку фактических значений. Вот как выглядит предварительно подготовленный запрос в коде программы.
String=
"SELECT Books.Price, Books.Title " +
"FROM Books, Publishers " +
"WHERE Books.Publisher_Id =AND " +
"Publishers.Name =
PreparedStatement publisherQueryStmt =
Перед выполнением предварительно подготовленного запроса нужно с помощью метода set связать подстановочные переменные с их фактическими значениями. Как и при использовании методов get класса ResultSet, для разных типов данных предусмотрены разные методы set. Вот как задается строковое значение с названием издательства.
publisherQueryStmt.setstring(1, publisher);
Первый аргумент обозначает порядковый номер подстановочной переменной, а второй — ее фактическое значение.
При повторном использовании предварительно подготовленного запроса с несколькими подстановочными переменными все связи остаются в силе, если только они не изменены с помощью метода set. Это значит, что методы set нужно вызывать только для тех подстановочных переменных, которые изменяются в следующих запросах.
После установления связи между подстановочными переменными и их фактическими значениями можно приступать к выполнению запроса.
Resultset rs = publisherQueryStat.executeQuery();
Результаты выполнения запроса обрабатываются обычным образом. Например, для их представления в текстовом поле можно использовать приведенный ниже код.
;
while (rs.next())
result.appendText(rs.getstring(1) + ", " +
rs.getString(2y) + "\n");
rs.close() ;
В листинге 4.3 используется четыре предварительно подготовленных запроса с условиями, которые перечислены в табл. 4.7.
Таблица 4.7. Условия предварительно подготовленных запросов
Автор
Издательство
Любой
Любой
Любой
Выбранный пользователем
Выбранный пользователем
Любой
Выбранный пользователем
Выбранный пользователем
Для разнообразия функция обновления цены основана на команде UPDATE, а не на команде SELECT. Обратите внимание, что для этого используется метод executeUpdate, а не метод executeQuery. Дело в том, что команда UPDATE не возвращает результирующий набор, который в этом случае не нужен. В текстовом окне отображается лишь количество обновленных записей.
String updateStatement = "UPDATE Books
int r = stat.executeUpdate(updateStatement);
result.setText(r + " записей обновлено.");
Вот краткий перечень всех действий, выполняемых этой программой.
Сначала во фрейме создаются все визуальные компоненты программы (см. главу 9 тома I).
Затем выполняются два запроса для наполнения данными списков с именами авторов и названиями издательств.
После щелчка на кнопке Query выполняется поиск подходящего предварительно подготовленного запроса. При первом выполнении предварительного за-
проса выбранного типа для него создается SQL-команда, в которой подстановочная переменная имеет значение null. После этого устанавливается связь между подстановочными переменными и их фактическими значениями, а затем выполняется запрос.
Запросы с указанием имени автора имеют более сложную структуру. Так как одна книга может иметь несколько авторов, то в таблице BooksAuthors задается такое соответствие между авторами и книгами. Допустим, что книга со стандартным международным номером (International Standard Book Number— ISBN) 1-56-604288-7 имеет двух авторов с кодами HARR и KIDD. В таком случае таблица BooksAuthors будет содержать следующие две строки для обозначения этой ситуации.
1-56-604288-7, HARR, 1
1-56-604288-7, KIDD, 2
В третьем столбце указан порядковый номер авторов. (Для этого нельзя использовать информацию о расположении записей в таблице, потому что записи не имеют никакого фиксированного расположения в реляционной таблице.) В таком случае запрос должен выполнить соединение таблиц Books и BooksAuthors, а затем сравнить указанное пользователем имя автора с таблицей Authors.
SELECT Books.Price, Books.Title
FROM Books, BooksAuthors, Authors, Publishers
WHERE Books.Publisher_Id =
AND Publishers.Name = ?
AND BooksAuthors.ISBN =
AND Authors.Author_Id =
AND Authors.Name = ?
Некоторые программисты Java стараются избегать создания таких сложных SQL- команд. Удивительно, но очень часто программисты склонны использовать сложный Java-код для обработки нескольких результатов выполнения запроса. Учтите, что СУБД выполняет запросы гораздо эффективнее, чем Java-программа, потому что СУБД оптимизирована как раз для выполнения запросов. Помните основное правило: если какой-то запрос нельзя реализовать средствами SQL, его не рекомендуется создавать с помощью языка Java.
Результаты выполнения запроса будут представлены в текстовом окне.
После щелчка на кнопке Change price создается и выполняется запрос на обновление данных. Этот запрос имеет довольно сложную структуру, потому что предложение WHERE команды UPDATE должно получить код издательства по известному имени издательства. Эта проблема решается с помощью вложенного запроса, например так, как показано ниже.
UPDATE Books
SET Price = Price + изменение_цены
WHERE=
(SELECT Publisher_Id
FROM Publishers
WHERE Name = название_издательства;
Ранее в конструкторе была выполнена инициализация соединения и объектов Statement, которые используются в течение всего жизненного цикла про-
граммы. Непосредственно перед окончанием работы программы следует перехватить событие "закрытия окна" и закрыть все эти объекты.
class QueryDBFrame extends JFrame
{
public QueryDBFrame()
(
try
{
conn = getConnection() ;
stat = conn.createStatement();
addWindowListener(new WindowAdapter()
{
public void windowclosing(WindowEvent e) {
try
{
stat.close();
conn.close();
}
catch(SQLException ex)
{ while (ex != null)
{
ex.printStackTrace() ;
j ex = ex.getNextException();
}
});
}
}
private Connection conn;
private Statement stat;
}
Полностью код этой программы представлен в листинге 4.3.
Листинг4.3. QueryDB.java
/ @version 1.21 2001-07-06
©author Cay Horstmann
*/
import java.net.*;
import java.sql.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import j avax.swing.*;
/“
Программа, демонстрирующая несколько примеров использования сложных запросов */ public class QueryDB {
public static void main(String[] args) {
JFrame frame = new QueryDBFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
)
/**
Этот фрейм содержит поля со списками для ввода параметров запроса, текстовое поле для результатов выполнения запроса, а также кнопки для запуска запроса и обновления данных
*/
class QueryDBFrame extends JFrame {
public QueryDBFrame()
{
setTitle("QueryDB");
setsize(WIDTH,HEIGHT);
getContentPane().setLayout(new GridBagLayout()) ;
GridBagConstraints gbc = new GridBagConstraints();
authors = new JComboBox();
authors.setEditable(false);
authors.additem("Any");
publishers = new JComboBox();
publishers.setEditable(false);
publishers.additem("Any");
result = new JTextArea(4, 50);
result.setEditable(false);
priceChange = new JTextField(8);
pricechange.setText("-5.00");
try {
conn = getConnection();
stat = conn.createStatement();
String query = "SELECT Name FROM Authors";
ResultSet rs = stat.executeQuery(query) ;
while (rs.next())
authors.addltern(rs.getString(l));
rs.close();
query = "SELECT Name FROM Publishers";
rs = stat.executeQuery(query);
while (rs.next() ) publishers.additem(rs.getString(l));
rs.close();
}
catch(SQLException ex)
{
result.setText("");
while (ex != null) {
result.append("" +ex) ;
ex = ex.getNextException()'
}
catch (IOException ex) {
result.setText("" + ex);
gbc.fill = GridBagConstraints.NONE;
gbc.weightx = 100;
gbc.weighty = 100;
add(authors, gbc, 0, 0, 2, 1);
add(publishers, gbc, 2, 0, 2, 1) ;
gbc.fill = GridBagConstraints.NONE;
JButton queryButton = new JButton("Query");
queryButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
executeQuery() ;
} });
add(queryButton, gbc, 0, 1, 1, 1);
JButton changeButton = new JButton("Change prices"); changeButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
changeprices();
} });
add(changeButton, gbc, 2, 1, 1, 1) ;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(pricechange, gbc, 3, 1, 1, 1);
gbc.fill = GridBagConstraints.BOTH;
add(new JScrollPane(result), gbc, 0, 2, 4, 1) ;
addWindowListener(new
WindowAdapter()
{
public void windowclosing(WindowEvent e) {
try
{
stat.close();
} conn.close();
catch(SQLException ex) {
while (ex != null)
{
ex.printStackTrace();
ex = ex.getNextException();
)
}
});
Создание компонента во фрейме
Sparam с the component to add
Sparain gbc the grid bag constraints
@param x the grid bax column
Sparam у the grid bag row
@param w the number of grid bag columns spanned
Sparam h the number of grid bag rows spanned
*/
private void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane().add(c, gbc);
/**
Выполнение выбранного запроса
*/
private void executeQuery()
{
ResultSet rs = null;
try
{
String author
=;
String publisher =
if (!author.equals("Any")
&& !publisher.equals("Any"))
{
if (author Publi sherQueryStmt == null) {
String=
"SELECT Books.Price, Books.Title " +
"FROM Books, BooksAuthors, Authors, Publishers " +
"WHERE Authors.Author_Id =AND " +
"BooksAuthors.ISBN =AND " +
"Books.Publisher_Id =AND " +
"Authors.Name = ? AND " + "Publishers.Name = ?";
authorPublisherQueryStmt
=
authorPublisherQueryStmt.setstring(1, author);
authorPublisherQueryStmt.setstring(2, publisher);
rs = authorPublisherQueryStmt.executeQuery();
}
else if (!author.equals("Any")
&& publisher.equals("Any"))
{
if (authorQueryStmt == null)
{
String authorQuery =
"SELECT Books.Price, Books.Title " +
"FROM Books, BooksAuthors, Authors " +
"WHERE Authors.Author_Id =" +
"BooksAuthors.ISBN =AND " +
"Authors.Name=?";
authorQueryStmt
=
}
authorQueryStmt.setstring(1, author);
rs = authorQueryStmt.executeQuery();
}
else if (author.equals("Any")
&&!publisher.equals("Any"))
{
if (publi sherQueryStmt == null)
{
String publisherQuery =
"SELECT Books.Price, Books.Title " +
"FROM Books, Publishers " +
"WHERE Books.Publisher_Id =AND "
"Publishers.Name = ?";
publisherQueryStmt
=
}
publisherQueryStmt.setstring(1, publisher);
rs = publisherQueryStmt.executeQuery();
}
else
{
if (allQueryStmt == null)
{
String allQuery =
"SELECT Books.Price, Books.Title FROM Books";
allQueryStmt
=
}
rs = allQueryStmt.executeQuery();
result.setText("");
while (rs.next())
{
result.append(rs.getString(l));
result.append(", ");
result.append(rs.getString(2) ) ; result.append("\n");
}
rs.close();
}
catch(SQLException ex)
{
result.setText("");
while (ex != null)
{
result.append("" + ex);
ex = ex.getNextExcept ion();
}
/**
Выполнение команды обновления цен
*/
public void changePrices()
{
String publisher
= (String)publishers.getSelectedItem();
if (publisher.equals("Any")) {
result.setText
("I am sorry, but I cannot do that."); return;
}
try
{
String updateStatement =
"UPDATE Books " +
"SET Price = Price + " + pricechange.getText() +
" WHERE Books. Publisher_Id = " +
"(SELECT Publisher_Id FROM Publishers WHERE Name =+
publisher + " ' ) ";
int r = stat.executeUpdate(updateStatement);
result.setText(r + " records updated.");
}
catch(SQLException ex) {
result.setText("");
while (ex != null) {
result.append!"" + ex);
ex = ex.getNextException();
}
}
/•*
Установление соединения на основе параметров из файла свойств database.properties ©return the database connection
*/
public static Connection getConnection()
throws SQLException, IOException {
Properties props = new Properties();
FileinputStream in
= new Fileinputstream("database.properties"); props.load(in);
in.close();
String drivers = props.getProperty("jdbc.drivers");
if (drivers != null)
System.setProperty("jdbc.drivers", drivers);
String url =
String username =
String password = props.getProperty("jdbc.password") ;
return
DriverManager.getConnection(url, username, password); }
public static final int WIDTH = 400;
public static final int HEIGHT = 400;
private JComboBox authors;
private JComboBox publishers;
private JTextField priceChange;
private JTextArea result;
private Connection conn;
private Statement stat;
private PreparedStatement authorQueryStmt;
private PreparedStatement authorPublisherQueryStmt;
private PreparedStatement publisherQueryStmt;
private PreparedStatement allQueryStmt;
java.sql.Connection
e Метод PreparedStatement prepareStatement(String sql) возвращает объект PreparedStatement, содержащий предварительно созданную команду. Строка sql содержит SQL-команду с одной или несколькими подстановочными переменными, обозначенными вопросительными знаками.
java.sql.PreparedStatement
Метод void setXxx(int n, Xxx x) задает значение x для гс-го параметра, где Xxx обозначаетгипданных, например, int, double, String, Date И т.д.
Метод void clearParameters () очищает все текущие параметры в предварительно подготовленном запросе.
Метод ResultSet executeQuery () выполняет предварительно подготовленный SQL-запрос и возвращает объект ResultSet.
Метод int executeUpdate () выполняет предварительно подготовленные SQL-команды INSERT, UPDATE или DELETE, представленные в объекте PreparedStatement. Он возвращает количество задействованных строк или значения 0 для команд определения данных.
Просмотр и обновление результатов выполнениязапроса
Наиболее полезные усовершенствования интерфейса JDBC 2 заключены в классе ResultSet. Как уже говорилось выше, метод next класса ResultSet позволяет по-
следовательно перебирать строки в результате выполнения запроса. Его очень удобно использовать для анализа полученных данных. Однако, часто бывает нужно представить пользователю возможность визуального просмотра результатов выполнения запроса с переходом к предыдущим и следующим строкам, например так, как показано на рис. 4.7. В JDBC 1 не было предусмотрено метода для перехода к предыдущим строкам и программистам приходилось кэшировать результаты выполнения запроса. Предусмотренные в JDBC 2 функции прокрутки результатов выполнения запроса позволяют свободно перемещаться не только к предыдущим и следующим записям, но и в произвольное место результатов выполнения запроса.
f, X
Action New Delete
Taele| Properties) SQL Data |
Th' h Д + — Q ®
гг8910u_1213148910u_12131415161718191516171819TITLEj ISBN | pubushSjd]HTML 3 Interact! 1-57169066- 15716HTML 3.0 Manua 1-56-276352 15627]HTML by Exam pl0-78970812-07097IHTML CD- An Ini0-13-232331 01356HTMLforDumm 1-56-884647 15688HTML For Dumm l-5688499tU15688]HTML For Fun ar 0-13-242488 01356HTMLfor the WO0-02188448-00218HTMLIn Action 1-55615948-15561HTML Hus!0-53451626-05345HTML Publishinc 1-56-604288 15660~ iHTML Publishing0-07-912100 00791HTM1 QuickReffП-7Я9?ПЯ67-; П7А97 . .;TITLEj ISBN | pubushSjd]HTML 3 Interact! 1-57169066- 15716HTML 3.0 Manua 1-56-276352 15627]HTML by Exam pl0-78970812-07097IHTML CD- An Ini0-13-232331 01356HTMLforDumm 1-56-884647 15688HTML For Dumm l-5688499tU15688]HTML For Fun ar 0-13-242488 01356HTMLfor the WO0-02188448-00218HTMLIn Action 1-55615948-15561HTML Hus!0-53451626-05345HTML Publishinc 1-56-604288 15660~ iHTML Publishing0-07-912100 00791HTM1 QuickReffП-7Я9?ПЯ67-; П7А97 . .;URLURLw3waite.com /w www.mcpcom/2www.mcp com/2 ;www.prenhall.co: Idbwww.ldgbooli dbwwv.ldgbook: iwuvw.sun com/s ivwwv.peachpitcc jWAW.microsoft с scholar lib vt edi iephraim.vmediaiw3waite.com /w www.mcpcom/2www.mcp com/2 ;www.prenhall.co: Idbwww.ldgbooli dbwwv.ldgbook: iwuvw.sun com/s ivwwv.peachpitcc jWAW.microsoft с scholar lib vt edi iephraim.vmediaiI PRICE | -39 99 2I PRICE | -39 99 2!ии*мтгп rnm/;!ии*мтгп rnm/;2495'2495'34.99;4030512 9940 1795 29953639.954534.99;4030512 9940 1795 29953639.954517 991.17 991.J HtipJ Htip
г
г
8
9
10
u_
12
13
14
8
9
10
u_
12
13
14
15
16
17
18
19
15
16
17
18
19
TITLEj ISBN | pubushSjd]
HTML 3 Interact! 1-57169066- 15716
HTML 3.0 Manua 1-56-276352 15627]
HTML by Exam pl0-78970812-07097I
HTML CD- An Ini0-13-232331 01356
HTMLforDumm 1-56-884647 15688
HTML For Dumm l-5688499tU15688]
HTML For Fun ar 0-13-242488 01356
HTMLfor the WO0-02188448-00218
HTMLIn Action 1-55615948-15561
HTML Hus!0-53451626-05345
HTML Publishinc 1-56-604288 15660~ i
HTML Publishing0-07-912100 00791
HTM1 QuickReffП-7Я9?ПЯ67-; П7А97 . .;
TITLEj ISBN | pubushSjd]
HTML 3 Interact! 1-57169066- 15716
HTML 3.0 Manua 1-56-276352 15627]
HTML by Exam pl0-78970812-07097I
HTML CD- An Ini0-13-232331 01356
HTMLforDumm 1-56-884647 15688
HTML For Dumm l-5688499tU15688]
HTML For Fun ar 0-13-242488 01356
HTMLfor the WO0-02188448-00218
HTMLIn Action 1-55615948-15561
HTML Hus!0-53451626-05345
HTML Publishinc 1-56-604288 15660~ i
HTML Publishing0-07-912100 00791
HTM1 QuickReffП-7Я9?ПЯ67-; П7А97 . .;
URL
URL
w3waite.com /w www.mcpcom/2www.mcp com/2 ;www.prenhall.co: Idbwww.ldgbooli dbwwv.ldgbook: iwuvw.sun com/s ivwwv.peachpitcc jWAW.microsoft с scholar lib vt edi iephraim.vmediai
w3waite.com /w www.mcpcom/2www.mcp com/2 ;www.prenhall.co: Idbwww.ldgbooli dbwwv.ldgbook: iwuvw.sun com/s ivwwv.peachpitcc jWAW.microsoft с scholar lib vt edi iephraim.vmediai
I PRICE | -
39 99 2
I PRICE | -
39 99 2
!ии*мтгп rnm/;
!ии*мтгп rnm/;
2495'
2495'
34.99;
40
305
12 99
40 1795 2995
36
39.95
45
34.99;
40
305
12 99
40 1795 2995
36
39.95
45
17 991.
17 991.
J Htip
J Htip
Рис.4.7.Графическое представление результатов
выполнения запроса
Более того, при визуальном просмотре результатов выполнения запроса у пользователей часто возникает желание исправить какие-то данные. В JDBC 1 для этого необходимо было использовать команду UPDATE, а в JDBC 2 — достаточно просто отредактировать данные в результате выполнения запроса, и СУБД автоматически обновит исправленные данные.
В JDBC 2 предусмотрены дополнительные усовершенствования инструментов работы с результатами выполнения запроса, например, обновление результатов выполнения запроса наиболее свежими данными, если они появились в ходе работы других пользователей, параллельно работающих с базой данных. В JDBC 3 предусмотрено дополнительное усовершенствование, связанное с фиксацией транзакций. Один из вариантов метода getstatement класса Connection позволяет указать возможность удержания ^holdabilit^ результатов выполнения запроса. Так можно указать на необходимость автоматического закрытия результатов выполнения запроса после фиксации транзакции или наоборот, оставить их открытыми. Однако, описание этих функций выходит за рамки главы. Более подробные сведения по этой теме можно найти в книге Сета Байта (Seth White) и др. JDBCAPI Tutorial and Reference [Addison-Wesley, 1999] и в документации JDBC по адресуhttp: //java.sun.com/products/jdbc.
Прокрутка результатов выполнения запроса (JDBC2)
Для организации прокрутки результатов выполнения запроса необходимо получить объект Statement с помощью приведенного ниже способа.
Statement stat = conn.createStatement(type, concurrency);
Для предварительно подготовленного запроса нужно использовать следующий вызов.
PreparedStatement stat = conn.prepareStatement(command, type, concurrency);
Все допустимые значения параметров type и concurrency перечислены в табл. 4.8 и 4.9. Выбирая эти значения, нужно учесть следующие особенности.
Нужно ли организовать прокрутку результатов выполнения запроса? Если это не нужно, используйте значение ResultSet.TYPE_FORWARD_ONLY.
Если все же нужно организовать прокрутку результатов выполнения запроса, то в таком случае выясните, нужно ли обновлять содержимое результатов выполнения запроса теми данными, которые были изменены в базе данных уже после выполнения этого запроса? (Далее предполагается, что установлен параметр ResultSet.TYPE_SCROLL_INSENSITIVE, т.е. содержимое результатов выполнения запроса не "реагирует" на изменения в базе данных, которые произошли после выполнения этого запроса.)
Нужно ли организовать редактирование результатов выполнения запроса и обновление базы данных? (Более подробно этатема рассматривается в следующих разделах.) Для организации прокрутки результатов выполнения запроса без возможности редактирования данных можно использовать следующую команду.
Statement stat
=
ResultSet.CONCUR_READ_ONLY);
Таблица 4.8. Значения параметра type
TYPE_FORWARD_ONLY
Без прокрутки
TYPE_SCROLL_INSENSITIVE
С прокруткой, но без учета изменений в базе данных
TYPE SCROLL SENSITIVE
С прокруткой и с учетом изменений в базе данных
Таблица 4.9. Значения параметра concurrency
CONCUR_READ_ONLYБез редактирования
CONCUR_UPDATABLEС редактированием и обновлением ба
зы данных
Теперь можно прокручивать все результаты выполнения запроса, возвращаемые следующим методом.
Resultset rs = stat.executeStat(query);
Такой набор результатов выполнения запроса имеет курсор для определения текущего положения.
Не все драйверы баз данных поддерживают работу с курсором прокрутки или обновления. (Методы supportResultSetType И supportResultSet класса Database- MetaDataсообщают о типах и режимах параллельной работы, которые поддерживаются в данной СУБД.) Но даже если СУБД поддерживает работу во всех описанных режимах, в некоторых запросах нельзя получить результат в соответствии со всеми заданными свойствами. (Например, результат выполнения сложного запроса может оказаться необновляемым.) В этом случае метод executeQuery возвращает объект ResultSet с меньшими возможностями и с предупреждением для объекта соединения. Все такие сообщения можно просмотреть с помощью метода getWarnings класса Connection. Кроме того, для поиска фактически используемого режима работы можно применять методы getTypeи getconcurrency класса ResultSet. Иногда отсутствие проверки фактического режима работы приводит к использованию неподдерживаемой операции, например, к применению метода previous для не- прокручиваемого результата выполнения запроса. В таком случае неизбежно возникнет исключительная ситуация SQLException.
В драйверах JDBC 1 не предусмотрен метод Statement createStatement(int type, int concurrency). Если программа, компилированная для использования с JDBC 2, случайно загружает драйвер JDBC 1 и вызывает несуществующий в нем метод, то это неизбежно приведет к сбою программы. К сожалению, в JDBC 2 не предусмотрено никаких средств для опроса драйвера и определения его соответствия требованиям JDBC 2. В JDBC 3 можно использовать методы getJDBCMa- jorVersion и getJDBCMinorVersion класса DatabaseMetaData для поиска номера версии драйвера JDBC.
Прокрутка организована очень просто. Например, для перехода к предыдущим записям используется следующая конструкция.
if (rs.previous()) •...
Этот метод возвращает значение true, если курсор располагается в какой-то строке, и значение false, если курсор располагается перед первой строкой.
Для перемещения курсора на п строк вперед или назад используется метод rs . relative in). Для положительных значений п курсор перемещается вперед, а для отрицательных — назад (значение, равное нулю, не приводит ни к каким перемещениям). При попытке перемещения курсора за пределы имеющегося результата выполнения запроса он располагается либо после последней, либо перед первой записью в зависимости от знака п. После чего курсор возвращает значение false и останавливается. Этот метод возвращает значение true только при расположении курсора на фактической строке.
Кроме того, курсор можно расположить на строке с номером п с помощью метода rs . absolute (n), а получить текущий номер строки п можно с помощью показанного ниже метода.
int n = rs.getRowt) ;
Первая запись в результатах выполнения запроса имеет номер 1. Если возвращаемое значение равно 0 — значит, курсор находится не в какой-то строке, а либо после последней, либо перед первой записью.
Порядок расположения строк в результатах выполнения запроса может быть совершенно произвольным, поэтому не следует придавать никакого значения номерам строк.
Для перемещения курсора к первой записи, последней записи, перед первой записью или после последней записи предусмотрены методы first, last, beforeFirst или afterLast. А для проверки расположения курсора у первой записи, последней записи, перед первой записью или после последней предусмотрены методы isFirst, isLast, isBeforeFirst или isAfterLast.
Как видите, прокрутка результатов выполнения запроса организована достаточно просто. Все операции, связанные с кэшированием данных, выполняются драйвером базы данных.
Для создания представления с прокручиваемым содержимым в Web-ориентированном приложении нужно учитывать один существенный недостаток. Дело в том, что при работе с ним между запросами Web-страниц необходимо поддерживать открытым соединение с базой данных. Однако, пользователи могут на долгое время покидать свое рабочее место или просто переключаться на выполнение другой работы без отключения соединения. Во избежание неэффективного расходования такого важного ресурса, как соединения с базой данных, рекомендуется использовать набор строк(row set). Интерфейс RowSetявляется расширением интерфейса ResultSet, в котором набор строк не связан с каким-то соединением с базой данных. Наборы строк также можно кэшировать или использовать для представления данных, которые получены из других источников, а не только из базы данных. В статье, расположенной по адресу http: //developer. java.sun.com/developer/technicalArticles/javaserverpages/cachedrowset, описаны способы использования наборов строк в Web-ориентированном приложении.
Редактирование и обновление результатов выполнения запроса (JDBC 2)
Для организации редактирования результатов выполнения запроса и автоматического обновления базы данных нужно создать обновляемый набор результатов выполнения запроса. Совсем необязательно, чтобы такой набор обладал возможностями прокрутки, но, обычно, обновляемые результаты выполнения запроса создаются с возможностями прокрутки.
Для получения обновляемых результатов выполнения запроса нужно использовать следующую конструкцию.
Statement stat =
ResultSet.CONCUR_UPDATABLE) ;
После этого метод executeQuery возвратит обновляемый набор результатов выполнения запроса.
Не все запросы возвращают обновляемый набор результатов запроса. Например, запрос с соединением нескольких таблиц не всегда может быть обновляемым. Это всегда возможно только для запросов на основе одной таблицы или запросов на основе соединения нескольких таблиц по первичным ключам. Для проверки текущего режима работы рекомендуется использовать метод getconcurrency класса ResultSet.
Для повышения цены на некоторые книги согласно некоторым сложным условиям можно использовать показанную ниже конструкцию.
String query = "SELECT * FROM Books";
ResultSet rs = stat.executeQuery(query);
while (rs.next())
{
if (...)
{
double increase = ...
double price = rs.getDouble("Price");
rs.updateDouble(“Price", price + increase);
rs.updateRow();
)
)
Для всех типов данных SQL предусмотрены соответствующие методы updateXxx, например, updateDouble, updateString и т.д. Как и при работе с методами getXxx, в качестве аргументов этих методов могут использоваться номер или имя столбца, а после него новое значение.
При использовании методов updateA'xx следует иметь в виду, что первый параметр с номером столбца означает номер столбца в наборе результатов выполнения запроса, который может отличаться от номера столбца в базе данных.
Методы updateXxx изменяют только отдельные значения в текущей строке в результатах выполнения запроса, а не в базе данных. Для обновления всех данных из отредактированной строки в базе данных нужно вызвать метод updateRow. При перемещении курсора в следующую строку без вызова метода updateRow все текущие обновления данной строки будут отменены, потому что они не были переданы базе данных. Для отмены обновлений из данной строки в базе данных можно использовать метод cancelRowUpdate s.
В предыдущем примере показан пример изменения текущей строки. Для создания новой строки в базе данных нужно сначала вызвать метод moveToInsertRowдля перемещения курсора в специальную позицию, а именно, в строку вставки (insert row). После чего новая строка создается в этой позиции с помощью методов updateXxx. Затем для вставки новой строки в базу данных используется метод insertRow. По окончании вставки применяется метод moveToCurrentRowдля перемещения курсора назад в позицию, которую он занимал до вызова метода moveToInsertRow. Вот как выглядит весь этот процесс в целом.
rs.MoveToInsertRow();
rs.updateString("Title", title);
rs.updateString("ISBN", isbn);
rs.updateString("Publisher_Id", pubid);
rs.updatestring("URL", url);
rs .updatestring"Price",price),- rs.insertRow();
rs.moveToCurrentRow();
Учтите, что никоим образом нельзя задать расположение вновь созданной строки в результате выполнения запроса или в базе данных.
Наконец, для удаления строки используется приведенный ниже метод.
rs.deleteRow();
Этот метод немедленно удаляет строку из результатов выполнения запроса или базы данных.
Таким образом, методы updateRow, insertRow и deleteRow класса ResultSet обладают такими же возможностями, что и SQL-команды UPDATE, INSERT и DELETE. Программисты, создавая Java-программы, часто предпочитают использовать средства языка Java для работы с результатами выполнения запроса, чем SQL-команды. После широкого распространения JDBC 2 эта тенденция стала еще более заметной.
При неаккуратном использовании обновляемых результатов выполнения запроса можно получить чрезвычайно неэффективный код. Часто бывает так, что использование команды UPDATE позволяет достигнуть гораздо большей эффективности, чем создание запроса со сложной обработкой результатов выполнения запроса. Обработку обновляемых результатов выполнения запроса имеет смысл использовать для интерактивных программ, в которых пользователь может вносить произвольные изменения. Однако, как показывает опыт, в большинстве случаев для программных изменений данных удобнее использовать SQL-команду UPDATE.
java.aql.Connection
Методы Statement createStatement(int type, int concurrency) и PreparedStatement prepareStatement(String command, int type, int concurrency) (JDBC 2) создают SQL-команду или предварительно подготовленную SQL-команду и возвращают результат в соответствии с заданным способом прокрутки и редактирования.
Параметры'. command type
предварительно подготовленная SQL-команда одна из констант (TYPE_FORWARD_ONLY,
CONCUR_UPDATABLE или TYPE_SCROLL_SENSITIVE), обозначающая способ прокрутки
concurrency
одна из констант (CONCUR_READ_ONLY или
CONCUR„UPDATABLE), обозначающая способ редактирования и обновления
Метод SQLWarning getWarnings () возвращает первое из имеющихся предупреждений для данного соединения или null при их отсутствии. Предупреждения образуют непрерывную цепочку, и для их перебора следует ПОСТОянно вызывать метод getNextWarning по отношению к возвращаемому объекту SQLWarning до тех пор, пока он не возвратит значение null. Класс
SQLWarning расширяет класс SQLException. Для анализа предупреждений следует использовать методы getErrorCode и getSQLState.
Метод void clearWarnings () удаляет все предупреждения, которые появились в ходе работы данного соединения.
java.sql.Connection
Метод int getType () (JDBC 2) возвращает константу (TYPE_FORWARD_
ONLY, CONCUR_UPDATABLE или TYPE_SCROLL_SENSITIVE), обозначающую способ прокрутки результатов выполнения запроса.
Метод int getConcurrency () (JDBC 2) возвращает константу (CONCUR_READ_ONLY или CONCUR^UPDATABLE), обозначающую способ редактирования и обновления результатов выполнения запроса.
Методbooleanprevious()(JDBC2)перемещаеткурсорк предыдущей
строке. Он возвращает значение true, если курсор располагается на записи.
Метод int getRow() (JDBC 2) возвращает номер текущей строки. Нумерация строк начинается с 1.
Метод boolean absolute (int r) (JDBC 2) перемещает курсор к строке с номером r. Он возвращает значение true, если курсор располагается на записи.
Метод boolean relative (int d) (JDBC 2) перемещает курсор на d строк. Если d меньше нуля, то перемещение происходит в обратном направлении. Он возвращает значениеtrue, если курсор располагается на записи.
Методfirst()(JDBC2)перемещаеткурсорк первойзаписи.Онвоз
вращает значение true, если курсор располагается на записи.
e Метод last () (JDBC 2) перемещает курсор к последней записи. Он возвращает значение true, если курсор располагается на записи.
Метод beforeFirst () (JDBC 2) размещает курсор перед первой записью. Он возвращает значение true, если курсор располагается на записи.
Метод afterLast () (JDBC 2) перемещает курсор после последней записи. Он возвращает значение true, если курсор располагается на записи.
Метод isFirst () (JDBC 2) проверяет, не находится ли курсор у первой записи. Он возвращает значениеtrue, если курсор располагается на записи.
Метод isLast () (JDBC 2) проверяет, не находится ли курсор у последней записи. Он возвращает значениеtrue, если курсор располагается на записи.
МетодisBeforeFirst()(JDBC2)проверяет,ненаходитсяликурсор
перед первой записью. Он возвращает значение true, если курсор располагается на записи.
Метод isAf terLast () (JDBC 2) проверяет, не находится ли курсор после последней записи. Он возвращает значение true, если курсор располагается на записи.
e Метод void moveToInsertRow() (JDBC 2) перемещает курсор в положение строки вставки. Строкой вставки называется специальная строка,
которая применяется для вставки новых данных с помощью методов updateXxx и innsertRow.
Метод void moveToCurrentRow () (JDBC 2) перемещает курсор обратно из положения строки вставки в исходное положение до вызова метода moveToInsertRow().
Метод void insertRowO (JDBC 2) вставляет содержимое строки вставки в базу данных и в результаты выполнения запроса.
Метод void deleteRow () (JDBC 2) удаляет текущую строку из базы данных и результатов выполнения запроса.
Методы void updateXxx(int column,Xxx data) и void
updateXxx(String columnName, Xxx data) (JDBC 2) обновляют содержимое указанного столбца в текущей строке в результатах выполнения запроса. Здесь Ххх обозначает тип данных, например, int, double, String, Date и т.д.
Метод void updateRow() (JDBC 2) посылает изменения текущей строки в базу данных.
Метод void cancelRowUpdates () (JDBC 2) отменяет изменения текущей строки.
java.sql.DatabaseMetaData
Метод boolean supportsResultSetType (int type) (JDBC 2) возвращает значение true, если база данных может поддерживать заданный тип прокрутки результатов выполнения запроса.
Параметры:
typeодна из констант (TYPE_FORWARD_ONLY,
CONCUR_UPDATABLE или TYPE_SCROLL_SENSITIVE), обозначающая способ прокрутки
Метод boolean supportsResultSetConcurrency(int type,int
concurrency) (JDBC 2) возвращает значение true, если база данных может поддерживать заданный тип прокрутки и обновления результатов выполнения запроса.
Параметры:
typeодна из констант (TYPE_FORWARD_ONLY,
CONCUR_UPDATABLE или TYPE_SCROLL_SENSITIVE), обозначающая способ прокрутки
concurrencyconcurrencyодна из констант (CONCUR_READ_ONLY или
concurrency
concurrency
CONCUR_UPDATABLE), обозначающая способ обновления
Метаданные
Рис. 4.8. Внешний вид viewDBРис. 4.8. Внешний вид viewDBВ предыдущих разделах рассмотрены способы ввода и обновления данных в таблицах базы данных, а также создания запросов. Кроме того, в JDBC предусмотрены дополнительные возможности для получения информации о структуре таблиц и самой базы данных. Например, можно получить список всех таблиц базы данных либо имена всех столбцов с типами данных в них. Эти сведения вряд ли буду очень полезны при создании приложения для работы с какой-то конкретной базой данных, потому что в таких случаях их структура точно известна. Однако, они пригодятся тем разработчикам, которые создают свои программные продук- работы с произвольной базой данных.
Рис. 4.8. Внешний вид viewDB
Рис. 4.8. Внешний вид viewDB
В этом разделе рассматриваются способы создания простого
инструмента, предназначенного для просмотра и анализа структуры базы данных.
Создаваемая здесь программа ViewDB содержит поле со списком всех таблиц базы данных. Как показано на рис. 4.8, после выбора какой-то одной таблицы в центральной части фрейма будут представлены имена столбцов из этой таблицы, а также значения из первой записи. Для просмотра каждой следующей записи нужно щелкнуть на кнопке Next (Далее).
Поставщики таких инструментов анализа структуры базы данных обычно создают более совершенные версии таких программ, которые позволяют редактировать отдельные значения, создавать другие значения, а также обновлять базу данных. Эта профамма создана только для демонстрации основных принципов создания таких инструментов.
В языке SQL данные о структуре базы данных и ее составных частей называются метаданными (metadata), чтобы их можно было отличить от основных данных. Существуют метаданные двух типов: для описания структуры базы данных и структуры результатов выполнения запроса.
Для поиска более подробных сведений о базе данных нужно создать запрос к объекту типана основе соединения с базой данных.
=
Теперь можно приступать непосредственно к получению метаданных. Например, вызов представленного ниже метода приводит к получению результатов выполнения запроса, содержащих информацию обо всех таблицах базы данных. (Другие параметры этого метода описаны ниже при описании API-интерфейса.)
ResultSet rs =null, null, new
{ "TABLE" ));
Каждая строка этих результатов выполнения запроса содержит сведения об отдельной таблице. В данном случае нас интересует только третий параметр rs . getStringкоторый содержит имя таблицы. (Другие параметры этого метода
описаны ниже при описании API-интерфейса.) Вот как выглядит фрагмент кода, в котором выполняется ввод полученных данных в поле со списком. while
tableNames.additem(rs.getString(3)) ;
rs.close();
Информация о структуре базы данных хранится в классе DatabaseMetaData, a сведения о структуре результатов выполнения запроса— в классе ResultSetMetaData. После получения результатов выполнения запроса можно определить количество столбцов, имена столбцов, типы данных в них и ширину полей.
Эта информация пригодится для создания подписей для каждого столбца и текстовых полей достаточной длины для отображения всех значений.
Resultset rs = stat.executeQuery("SELECT * FROM " + tableName) ; ResultSetMetaData =;
for (int i = 1; i <= meta.getColumnCount(); i++) {
String columnName = meta.getColumnLabel(i);
int columnWidth =
Label 1 = new Label(columnName);
TextField = new TextField(columnWidth);
}
Наконец, следует упомянуть еще одно важное применение метаданных. Базы данных могут иметь очень сложную структуру, а стандарт SQL предоставляет достаточно много возможностей. В классе DatabaseMetaData предусмотрено всего более сотни разных методов, которые можно использовать для получения информации о структуре базы данных. Ниже приводятся примеры таких методов.
md.supportCatalogsInPrivelegeDefinitions() md.nullPlusNonNullIsNull()
Судя no названиям этих методов, они предназначены только для очень опытных программистов, в частности, для создания переносимого кода, который способен работать с очень разными типами баз данных. В листинге 4.4 приводится простой пример использования методов доступа к метаданным базы данных. В нем используются методы JDBC, предназначенные для определения возможности прокрутки результатов выполнения запроса, отображаются полученные результаты выполнения запроса, и создается кнопка Previous (Предыдущие данные) для обратной прокрутки этих результатов.
if (meta.supportsResultSetType(
ResultSet.TYPE_SCROLL_INSENSITIVE)) ...
Вот краткий перечень действий, выполняемых этой программой.
Сначала создается макет пользовательского интерфейса программы на основе поля со списком вверху с указанием имени таблицы и текстовыми полями внизу со значениями полей этой таблицы.
Устанавливается соединение с базой данных. Определяется возможность организации прокрутки результатов выполнения запроса. Если прокрутка поддерживается, то в таком случае создается объект Statement с возможностями прокрутки, а в противном случае — объект Statement, создаваемый по умолчанию.
Извлекаются имена таблиц и заносятся в поле со списком.
Для поддержки прокрутки создается кнопка Previous (Предыдущие данные) и кнопка Next (Следующие данные).
После того, как пользователь выберет таблицу, выполняется запрос на извлечение всех записей этой таблицы. Затем извлекаются метаданные для получен-
ных результатов выполнения запроса. Центральная часть пользовательского интерфейса со старой панелью прокрутки заменяется сеткой, содержащей текстовые поля и подписи к ним. Она добавляется во фрейм, который перерисовывается после вызова метода validate. После этого для отображения первой строки вызывается метод showNextRow.
Метод showNextRow также вызывается при каждом щелчке на кнопке Next для получения следующей строки таблицы и ввода значений ее столбцов в текстовые поля фрейма.
Для определения конца результатов выполнения запроса можно было бы просто вызывать метод isLast. Но если прокрутка не поддерживается, то его применение приведет к возникновению исключительной ситуации (или даже ошибке виртуальной машиныJava, если используется драйвер JDBC 1). Поэтому при отсутствии прокрутки используется метод rs.next(), который возвращает значение false по окончании результатов выполнения запроса. После этого набор результатов выполнения запроса закрывается, aдля rs задается значение null.
После щелчка на кнопке Previous вызывается метод showPreviousRow, который позволяет перейти к предыдущим записям. Так как эта кнопка появляется только при работе с прокручиваемыми результатами выполнения запроса, то в ее коде можно использовать методы previous и isFirst.
Метод showRow просто вводит значения из полей результатов выполнения запроса в текстовые поля фрейма.
Полностью код этой программы представлен в листинге 4.4.
Листинг4.4.ViewDB.java
/**
(aversion 1.21 2001-07-05
@author Cay Horstmann
*/
import j ava.net. *;
import java.sql.*;
import j ava.awt.*;
import j ava.awt.event.*;
import java.io.*;
import java.util.*;
import j avax.swing.*;
/**
Программа обработки метаданных для
отображения любых таблиц базы данных
*/
public class ViewDB
{
public static void main(String[] args)
{
JFrame frame = new ViewDBFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Этот фрейм содержит панель данных и кнопки перемещения по записям */ class ViewDBFrame extends JFrame {
public ViewDBFrame()
{
setTitle("ViewDB") ;
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane();
tableNames = new JComboBox();
tableNames.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent event) {
showTable((String)tableNames.getSelectedltemf)); }
});
contentPane. add(tableNames,Border Layout .NORTH);
try {
conn = getConnection(); meta = createStatement();
getTableNames();
}
catch(Exception ex) {
JOptionPane.showMessageDialog(this, ex); }
JPanel buttonPanel = new JPanel(); contentpane.add(buttonPanel, BorderLayout.SOUTH);
if (scrolling) {
previousButton = new JButton("Previous"); previousButton.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent event) {
showPreviousRowt);
} });
buttonPanel.add(previousButton); }
nextButton = new JButton("Next");
nextButton.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent event)
showNextRow();
}
});
buttonPanel.add(nextButton);
addWindowListener(new
WindowAdapter()
{
public void windowclosing(WindowEvent e) {
try {
stat.close();
conn.close();
}
catch(SQLException ex)
{
while (ex != null)
{
ex.printStackTrace();
ex = ex.getNextExceptionf) ;
}
}
}
});
/**
Создание объекта Statement для выполнения запросов. При поддержке в базе данных курсоров прокрутки создается соответствующий набор прокручиваемых результатов выполнения запроса
*/
public void createStatement() throws SQLException {
if (meta.supportsResultSetType(
ResultSet.TYPE_SCROLL_INSENSITIVE))
{
stat = conn. createStatement (
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
scrolling = true;
}
else
{
stat = conn.createStatement(); scrolling = false;
}
/**
Получение имен всех таблиц базы
данных и ввод их в поле со списком
*/
public void getTableNames() throws SQLException
{
ResultSet mrs =null, null,
new String[] { "TABLE" });
while (mrs.next())
tableNames.additem(mrs.getString(3 )) ; mrs.close() ;
/**
Подготовка текстовых полей для отображения структуры новой таблицы и данных из первой записи
@param tableName the name of the table to display
*/
public void showTable(S tring tableName) {
try
{
if (rs != null) rs.close();
rs = stat.executeQuery("SELECT * FROM " + tableName);
if (scrollPane != null)
getContentPane().remove(scrollPane);
dataPanel = new DataPanel(rs);
scrollPane = new JScrollPane(dataPanel);
getContentPane().add(scrollPane, BorderLayout.CENTER);
validate()
showNextRow();
}
catch(Exception ex)
{
j JOptionPane.showMessageDialog(this, ex);
/**
Перемещение к предыдущей строке
*/
public void showPreviousRow()
{
try
{
if (rs == null || rs.isFirst()) return;
rs.previous();
dataPanel.showRow(rs) ;
}
catch(Exception ex)
{
j System.out.println("Error " + ex);
}
/**
Перемещение к следующей строке
*/
public void showNextRow()
{
try
{
if (rs == null || scrolling&& rs.isLast()) return;
if (!rs.next() && !scrolling) {
rs.close();
rs = null; return;
dataPanel.showRow(rs);
} catch(Exception ex) {
System.out.printIn("Error " + ex) } )
/
Установление соединения на основе параметров из файла свойств database.properties ©return the database connection
*/
public static Connection getConnection() throws SQLException, IOException {
Properties props = new Properties();
Fileinputstream in
= new FileInputStream("database.properties"); props.load(in);
in.close();
String drivers = props.getProperty("jdbc.drivers"); if (drivers != null)
System.setProperty("jdbc.drivers", drivers);
String url = props.getProperty("jdbc.url");
String username =
String password = props.getProperty("jdbc.password");
return
DriverManager.getConnection(url, username, password) }
public static final int WIDTH = 300;
public static final int HEIGHT = 200;
private JButton previousButton;
private JButton nextButton;
private DataPanel dataPanel;
private Component scrollPane;
private JComboBox tableNames;
private Connection conn;
private Statement stat;
private DatabaseMetaData meta;
private ResultSet rs; private boolean scrolling;
Создание панели данных
©param rs the result set whose contents this panel displays */
public DataPanel(Resultset rs) throws SQLException {
fields = new ArrayList();
setLayout(new GridBagLayout()) ;
GridBagConstraints gbc = new GridBagConstraints() ;
gbc.weighty = 100;
ResultSetMetaData rsmd =
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
String columnName = rsmd.getColumnLabel(i); int columnwidth = JTextField tb = new JTextField(columnWidth); fields.add(tb);
gbc.weightx =
gbc.anchor =
gbc.fill =
add (new JLabel (columnName) , gbc, 0, i — 1, 1, 1);
gbc.weightx = 100;
gbc.anchor =
gbc.fill = add(tb, gbc, 1, i - 1, 1, 1) ;
}
/**
Отображение строки из базы данных в текстовых полях с именами соответствующих полей
*/
public void showRow(Res u11 Se t rs) throws SQLException {
for (int i = 1; i <= fields.size() ; i++) {
String field = rs.getString(i) ;
JTextField tb
=-1) ;
tb.setText(field) ;
}
Вставка компонента в панель
@param с the component to add
©param gbc the grid bag constraints
©param x the grid bax column
@param у the grid bag row
©param w the number of grid bag columns spanned
©param h the number of grid bag rows spanned
*/
private void add(Component c,
GridBagConstraints gbc, int x, int y, int w, int h) {
gbc.gridx = x;
gbc.gridy = y;
= w;
=
;
}
private Array-List fields;
••••java.sql.Connection
•
•
•
•
Метод() возвращает метаданные для
соединения с базой данных в виде объектаDatabaseMetaData.
java.sql.DatabaseMetaData
Метод ResultSetcatalog, String schemaPattern, String tableNamePattern, Stringполучает
описание всех таблиц в каталоге, которые соответствуют схеме, шаблонам для имен таблиц и заданным типам. (Схемой (schema) называется группа связанных таблиц и полномочий доступа. Каталогом (catalog) называется связанная группа схем. Эти понятия имеют большое значение для структурирования больших баз данных.)
Параметры catalog имогут быть пустыми строками
("") для извлечения таблиц без каталога и схемы либо иметь значения для возвращения таблиц без учета каталога или схемы.
Массив types содержит имена типов возвращаемых таблиц: TABLE, VIEW, SYSTEM TABLE, GLOBAL TEMPORARY, LOCAL TEMPORARY, ALIAS И SYNONYM. Если для types заданы значения null, то будут возвращены таблицы типов.
Результаты выполнения запроса содержат пять столбцов, которые имеют тип String, и показаны в табл. 4.10.
ТаблицаПять столбцов результатов выполнения запроса
1
Каталог таблиц (может быть равен
2
TABLE_SCHEMКаталог схем (может быть равен null)
3
TABLE_NAMEИмя таблицы
4
TABLE_TYPEТип таблицы
5
REMARKSКомментарий для таблицы
Методы int
(JDBC 3) возвращают основные и дополнительные номера версий драйвера JDBC, с помощью которого установлено соединение с базой данных. Например, драйвер JDBC 3.0 имеет основной номер версии 3 и дополнительный номер версии 0.
Метод int getMaxStatements () возвращает максимальное количество параллельно открытых команд для одного соединения с базой данных либо 0, если это количество не ограничено или не известно.
java.sql.Resultset
Метод ResultSetMetaData getMetaData() возвращает метаданные для текущих результатов выполнения запроса ResultSet.
java.sql.ResultSetMetaData
Метод int getColumnCount () возвращает количество столбцов для текущих результатов выполнения запроса ResultSet.
Метод int getColumnDisplaySize(int column) возвращает максимальную ширину указанного столбца.
columnномер столбца
Метод int getColumnLabel (int column) возвращает предполагаемую надпись для указанного столбца.
columnномер столбца
Метод int getColumnName (int column) возвращает имя указанного столбца.
columnномер столбца
Транзакции
В этом разделе кратко описываются принципы создания группы команд (или иначе говоря транзакции), которая может быть зафиксирована после успешного выполнения всех команд, либо отвергнута (выполнен откат), если при выполнении хотя бы одной из команд произойдет какая-то ошибка.
Основная причина использования транзакций заключается в поддержании целостности базы данных. Предположим, например, что в базу данных требуется вставить данные о новой книге. В этом случае нужно одновременно обновить данные в таблицах Books, Authors и BooksAuthors. Если данные будут правильно обновлены только в первых двух таблицах, то в таком случае будут получены противоречивые данные о книгах и авторах.
Говорят, что транзакция зафиксирована (committed), если успешно выполнены все команды транзакции. В противном случае выполняется откат (rollback), т.е. отменяются все изменения в базе данных, которые выполнялись после предыдущей зафиксированной транзакции. Более того, запросы могут выполняться только по отношению к зафиксированным данным.
По умолчанию соединение с базой данных обладает возможностью автоматической фиксации (autocommit mode), т.е. каждая SQL-команда фиксируется после ее успешного выполнения. Причем нельзя выполнить откат для зафиксированной команды.
Для проверки текущего режима автоматической фиксации нужно вызвать метод getAutoCommit класса Connection.
Для отключения режима автоматической фиксации нужно вызвать показанный ниже метод.
conn.setAutoCommit(false);
После этого можно приступать к созданию объекта Statement.
Statement stat = conn.createStatement();
Затем нужное количество раз вызывается метод executeUpdate.
stat.executeUpdate(commandl);
stat.executeUpdate(commands);
stat.executeUpdate(commands);
После выполнения всех этих команд необходимо вызвать метод commit, conn.commit();
В случае возникновения какой-либо ошибки нужно выполнить откат всех предыдущих команд (кроме commit) с помощью метода rollback.
conn.rollback();
Откат обычно используется, если выполнение транзакции прерывается исключительной ситуацией SQLException.
Пакетные обновления (JDBC 2)
Допустим, что в программе следует выполнить несколько команд вставки INSERT для ввода данных в таблицы. В JDBC 2 повысить производительность программы можно с помощью пакетного обновления (batch update). В этом случае обновление выполняется для всей группы из нескольких команд, а не для каждой отдельной команды обновления из этой группы.
Для проверки наличия этой функциональной возможности в используемой СУБД предусмотрен метод supportsBatchUpdates класса DatabaseMetaData.
Помимо команд управления данными INSERT, UPDATE и DELETE в группах команд пакетного обновления можно использовать команды определения данных, например, CREATE TABLE и DROP TABLE. Однако, в них нельзя использовать команду SELECT, потому что она возвращает набор результатов выполнения запроса.
Для организации пакетного обновления сначала нужно создать объект Statement.
Statement stat = conn.createStatement();
Затем вместо вызова метода executeUpdate необходимо вызвать метод addBatch.
String command = "CREATE TABLE ..."; stat.addBatch(command);
while(...)
{
command = "INSERTINTO . . . VALUES (" + ... + ") ";
stat.addBatch(command);
}
Наконец, следует выполнить пакетное обновление.
int [ ] counts = stat.executeBatch();
Метод executeBatch возвращает массив с количеством строк, задействованных при выполнении каждой команды. (Помните, что метод executeUpdate возвращает количество строк, задействованных при выполнении данной команды.) В нашем примере метод executeBatch возвращает массив, в котором первый элемент равен 0, а все остальные — 1 (так как каждая команда INSERT связана со вставкой одной строки.)
Для правильной обработки исключительных ситуаций в режиме пакетного обновления следует рассматривать пакетное обновление как одну транзакцию. При сбое пакетного обновления, очевидно, потребуется выполнить откат к исходному состоянию.
Для организации фиксации и отката такой транзакции нужно выполнить следующие действия. Нужно отключить режим автоматической фиксации, затем надо собрать группу команд пакетного обновления, выполнить и зафиксировать их, а потом восстановить режим автоматической фиксации.
boolean autoCommlt = conn.getAutoCommit();
conn.setAutoCoramit(false) ;
Statement stat = conn.getStatement();
// несколько вызовов команды stat.addbatch();
stat.executeBatch();
conn.commit();
conn.setAutoCommit(autoCommit);
java.sql.Connection
Метод void setAutoCommit (boolean b) задает режим автоматической фиксации в соответствии со значением параметра b; если b равно true, то все команды будут автоматически фиксироваться сразу после их выполнения.
Метод boolean getAutoCommit () возвращает информацию о состоянии автоматической фиксации.
• Метод void commit () фиксирует все команды, которые поступили после последней фиксации.
Методvoid rollback () отменяет все изменения, которые были внесены после последней фиксации.
java.sql.Statement
Метод void addBatch(String command) (JDBC 2) включает указанную команду в текущую группу пакетного обновления.
Метод int [ ] executeBatch () (JDBC 2) выполняет все команды пакетного обновления. Он возвращает массив с количеством строк, задействованных при выполнении каждой команды.
java. sql. DatabaseMetaData
Метод boolean supportsBatchUpdates () (JDBC 2) возвращает значение true, если драйвер поддерживает пакетное обновление.
Болеесложныеметодыуправления соединениями
Упрощенный подход на основе параметров из файла свойств database. properties подходит только для очень простых тестовых программ и совершенно не годится для крупномасштабных приложений.
При установке приложения JDBC 2 в корпоративной среде управление соединениями с базами данных поддерживается с помощью JNDI-интерфейса (Java Naming and Directory Interface). Расположение корпоративных информационных ресурсов организовано с помощью системы каталогов. Это позволяет обеспечить централизованное управление учетными именами пользователей, паролями, именами баз данных и URL-указателямиJDBC.
В такой среде для установления соединения с базой данных рекомендуется использовать следующий код.
Context jndiContext = . . . ;
DataSource source =
(DataSource)jndiContext.lookup(”jdbc/corejava”);
Connection conn = source.getConnection(username, password);
Обратите внимание, что в этом коде уже не используется менеджер драйверов DriverManager. Вместо него для поиска источника данных применяется специальный JNDI-сервис. При этом источник данных образует интерфейс, который позволяет устанавливать простые JDBC-соединения, а также выполнять некоторые более сложные функции, например, выполнять распределенные транзакции по отношению к нескольким базам данных. Интерфейс DataSource определяется в стандартном пакете расширения javax. sql.
Помимо управления учетными именами пользователей и паролями, большое значение также имеет стоимость установленных соединений с базами данных.
Простая программа для работы с базами данных создает соединение с базой данных в самом начале работы программы и закрывает его по окончании работы программы. Однако, в некоторых ситуациях этот подход не пригоден. Рассмотрим типичное Web-приложение. Такое приложение может параллельно обслуживать несколько параллельных запросов на чтение Web-страниц. В таком случае потребуется создать несколько соединений с базой данных. При большом количестве баз данных соединение не всегда может использоваться совместно несколькими потоками. Таким образом, каждому запросу потребуется установить свое собственное соединение с базой данных. При упрощенном подходе для каждого запроса на чтение Web-страницы потребовалось бы создавать и закрывать отдельное соединение. Однако, этот подход характеризуется очень большими расходами времени и других ресурсов. В таких случаях одно соединение можно использовать для нескольких запросов, которое не закрывается после выполнения одного или двух запросов.
Для решения данной проблемы следует использовать пул (pool) соединений. Это значит, что используемые соединения с базами данных не закрываются физически, а хранятся в очереди и повторно используются для других запросов. Интерфейс JDBC содержит несколько инструментов для организации работы с пулом соединений. Но учтите, что пул может быть по-разному реализован в разных СУБД. Причем он не всегда входит в состав
Более сложные методы управления соединениями 309
драйвера JDBC. Некоторые поставщики серверов приложений предлагают пул соединений в составе сервера приложений, например, BEAWebLogic и IBM WebSphere.
Пул соединений используется совершенно незаметно для программиста. На основе информации об источнике данных и с помощью метода getConnection устанавливается соединение с источником соединений, которые предполагается поместить в пул. По окончании использования этого соединения вызывается метод call. При этом физическое соединение не закрывается, а пулу соединений поступает сообщение о прекращении его использования.
Для доставки и установки такого простого Web-приложения, в котором используется контейнер сервлетов ApacheTomcat, необходимо создать собственный пул соединений. В Internet предлагается несколько разных вариантов реализации пула соединений. Для поддержки только одной учетной записи без использования транзакций можно использовать пример, который находится по адресу http://developer.java.sun.com/ developer/onlineTraining/Programming/JDCBook/conpool.html.
В Web-приложениях следует проявлять особую внимательность при работе с пулом соединений в случае возникновения исключительной ситуации. Допустим, что возникла исключительная ситуация, и вызов метода close игнорируется. В таком случае возмущенный пользователь может еще раз выполнить повторный запрос данной Web-страницы, щелкая на кнопке перезагрузки Web-страницы в своем броузере. Это приведет к повторному выполнению уже вышедшего из строя сервиса. В таком случае следует предусмотреть приведенный ниже код в предложении finallyобработчика исключительной ситуации.
public void myService() throws SQLException {
Connection conn = null;
try {
conn = ds.getConnection();
}
finally {
if (conn != null) conn.close();
}
}
Этот метод обладает одним недостатком. Если при вызове метода conn. close () возникнет исключительная ситуация, она скроет исходную исключительную ситуацию. Такова природа механизма обработки исключительных ситуаций Java. Однако, любознательный читатель в качестве упражнения может попробовать создать код для сохранения и повторного инициирования исходной исключительной ситуации.
Теперь мы знаем все основные сведения о работе интерфейса JDBC, которые необходимы для создания простых приложений для работы с базами данных. Однако, как уже говорилось в начале этой главы, базы данных могут иметь очень сложную структуру с очень сложными методами работы. Краткий обзор дополнительных сложных инструментов JDBC приводится в спецификации JDBC по адресу http://java.sun.com/products/jdbc.
Работа
с удаленными объектами
этой главе...
Введение
т Вызовы удаленных методов
т
т Передача параметров для удаленных методов
Использование RMI-механизма вместе с аплетами
Активизация объекта-сервера
т Технологии Javaи
п
в среде программистов возникает идея повсеместного использования объектов" для решения всех возникающих проблем. Она основана на взаимодействии распределенных объектов, которые способны работать совместно в сети, благодаря использованию сетевых протоколов. Например, на компьютере-клиенте может располагаться объект, с помощью которого пользователи могут вводить запросы на получение данных. Этот объект-клиент посылает сообщение с данными запроса объекту-серверу, а объект-сервер на компьютере-сервере собирает запрошенную информацию, либо обращаясь к базе данных, либо с помощью других объектов. После получения ответа объект-сервер посылает его обратно объекту-клиенту. Как и многие другие модные новинки программирования эта технология чрезмерно разрекламирована, что не позволяет сразу же осознать значимость настоящей концепции. В этой главе рассматриваются следующие аспекты применения данной технологии.
е Модели, лежащие в основе объектного взаимодействия.
Ситуации, в которых полезно применять распределенные объекты.
Способы применения удаленных объектов и связанных с ними вызовов удаленных методов (Remote Method Invocation—RMI)ДЛЯ обеспечениявзаимодействия междудвумя виртуальными машинамиJava (которые могут находиться на разных компьютерах).
s Основные сведения об архитектуре CORBA (Common Object Request Broker Archi- tccture), которая позволяет обеспечить взаимодействие между объектами, которые созданы на разных языках программирования (например, Java, C++ и т.д.).
Введение
Вернемся к идее локального сбора информации на компьютере-клиенте и передачи ее компьютеру-серверу по сети. Допустим, что пользователь на локальном компьютере вводит определенные условия в специальной форме создания запросов. После этого введенные данные передаются компьютеру-серверу, который после обработки запроса возвращает полученный отчет компьютеру-клиенту для просмотра пользователем. На рис. 5.1 представлена схема такого взаимодействия.
Передача запросаПередача запросаПолучение ответаПолучение ответаРис. 5.1. Схема передачи объектов междуРис. 5.1. Схема передачи объектов между
Передача запроса
Передача запроса
Получение ответа
Получение ответа
Рис. 5.1. Схема передачи объектов между
Рис. 5.1. Схема передачи объектов между
клиентом и сервером
В традиционной модели клиент/сервер запрос транслируется в промежуточный формат (например, пары имя/значение или XML-данные). Сервер выполняет синтаксический разбор этого формата, находит ответ и посылает его клиенту снова в этом же формате. Затем клиент выполняет синтаксический разбор ответа в этом формате и отображает его для пользователя.
Введение313
При работе со структурированными данными для выполнения таких задач необходимо создать очень сложный код, потому что придется дважды выполнять трансляцию формата.
Еще один метод передачи запроса серверу для получения данных основан на использовании HTML-форм. В этом случае роль клиента играет броузер, а сервер D должен только собрать запрашиваемую информацию и переслать ее в формате
HTML-страницы. Но даже в такой ситуации рекомендуется отделить код обработки формы от кода сбора данных. Обычно обработка форм выполняется Web- сервером, а сбор данных на другом сервере, который называется сервером приложений {application server). При этом опять приходится иметь дело с двумя объектами: объектом-клиентом на Web-сервере и объектом-сервером на сервере приложений. Данная модель распределенных вычислений очень распространена. В этой главе рассматривается традиционный пример модели клиент/сервер, в котором клиент и сервер имеют интуитивно понятный смысл.
Как можно реализовать эту модель? Центральным принципом объектноориентированной идеологии является возможность обмена сообщениями между объектами. Учитывая это, можно размещать объекты на разных компьютерах и организовать непосредственный обмен сообщениями между ними. Предположим, что объект- клиент создан на языке Java, а потому может размещаться практически на любой платформе. Объект-сервер можно реализовать двумя способами: либо на языке Java, либо на другом языке (например, из-за использования унаследованного объекта- сервера или вследствие увлечения какой-то новой технологией).
Во втором случае необходимо предложить способ взаимодействия объектов независимо от используемого языка. Одна только мысль о теоретической возможности тако го способа работы должна вызвать изумление. Действительно, как последовательность байтов на произвольном языке, возможно неизвестном нам, может сообщить о выполняемых сервисах или обрабатываемых сообщениях? Хотя практическая рсали- зация этой идеи не так проста, все же следует признать, что она очень элегантна. Такой обмен данными и сервисами основан на архитектуре универсального брокера объектных запросов CORBA (Common Object Request Broker Architecture), предложенной группой OMG (Object Management Group) (www.omg. org).
Фундаментальной идеей этой архитектуры является делегирование поиска нужной информации и активирования запрошенных сервисов так называемому брокеру объектных запросов (Object RequestBroker- ORB). ORB-брокерможнорассматривать какуНИВСрС.аЛЬНЫЙ транслятор межобъектного взаимодействия. Таким образом, объекты общаются не непосредственно друг с другом, а посредством ORB-6 рокера. Такие брокеры распределены в сети и могут общаться друг с другом. Большинство ORB-брокеров создаются в соответствии со спецификацией, заданной группой OMG для межброкерного взаимодействия. Эта спецификация сформулирована в виде протокола Internet Inter-ORB Protocol (ПОР).
Компания Microsoft для организации межобъектного взаимодействия использует другой протокол низкого уровня COM. Этот протокол был предложен в 1995 году и интегрирован в составе операционной системы Windows. Однако, во время написания данной книги компания Microsoft начала переход от протокола COM к модели .NETдля организации взаимодействия между компонентами программного обеспечения на основе XML.
Архитектура CORBA совершенно не зависит от языка программирования. Клиентские и серверные программы могут создаваться на языках C++, Java или любом другом языке программирования, который поддерживает CORBA. Для создания подписей сообщений и типов данных, которые могут понимать и посылать объекты, используется язык описания интерфейсов {Interface Definition Language- IDL). (Спецификация IDL очень похожа на спецификацию интерфейсов языка Java. Действительно, спецификацию IDL можно представить в виде совокупности интерфейсов, которые должны поддерживаться взаимодействующими объектами. Достоинством этой модели является то, что можно представить IDL-спецификацию для уже существующего унаследованного объекта и потом использовать его сервисы с помощью ORB-брокера, даже если он был создан задолго до появления ORB-брокера.)
Многие считают CORBA объектной моделью будущего, а язык программирования Java- прекрасным способом реализации клиентов и серверов CORBA. Однако, откровенно говоря, CORBA имеет репутацию (порой заслуженную) архитектуры с низкой производительностью, сложной реализацией и проблемами взаимодействия. Как в первом, так и во втором исправленном издании (спустя два года) во время написания примеров для этой главы на основе технологии CORBA постоянно возникали большие проблемы. Эта ситуация напомнила авторам известное высказывание президента Франции Шарля Де Голля (Charles De Gaulle) о Бразилии: "Ее ждет большое будущее ... и всегда будет ждать".
Если оба взаимодействующих объекта созданы на языке программирования Java, то в таком случае не потребуется использовать все сложные механизмы CORBA. Компания Sun предложила простой механизм вызова удаленных процедур (Remote Method Invocation — RMI) специально для взаимодействия Java-приложений.
Сторонникам CORBA не нравится RMI-механизм по той причине, что он полностью игнорирует CORBA. Однако, начиная с пакета SDK 1.3, архитектура CORBA и механизм RMI стали более совместимыми. В частности, RMI можно использовать вместе с протоколом HOP вместо собственного протокола Java Remote Method Protocol. Более подробную информацию об использовании RMI вместе с протоколом HOP можно найти в вводной статье по адресу http:// www. j avawor Id. coin/ j avawor Id/ jw-12- 1999/jw-12-iiop.html и в руководстве http://java.sun.com/j2se/l.4/docs/ guide/rmi-iiop/rmiiiopexample.html.
Если CORBA все еще остается перспективной технологией будущего, то механизм RMI проще для понимания и более удобен для использования. По этой причине данная глава начинается с описания RMI-механизма, который конечно же может использоваться только для организации взаимодействия между Java-объектами. В последнем разделе главы кратко рассматриваются основы программирования на основе архитектуры CORBA на примере создания клиентского приложения на языке C++ и серверного приложения на языке Java, а также на примере создания клиентского приложения на языкеJava и серверного приложения на языке C++.
Вызовы удаленных методов
Механизм RMI позволяет выполнять очень простые действия. Если у вас есть доступ к объектам на другом компьютере, то вы можете вызывать методы этого объекта. Конечно, нужно каким-то образом доставить параметры метода на другой компьютер, нужно сооб-
щить объекту о необходимости выполнения метода, а затем передать обратно возвращаемое значение. Механизм RMI позволяет организовать выполнение всех этих операций.
Допустим, что клиент для поиска информации о продукции на складе запрашивает объект Warehouse. Для этого необходимо вызвать удаленный метод findс одним параметром— объектом Customer. Этот метод возвращает клиенту объект с информацией об искомом продукте. На рис. 5.2 показана схема такого взаимодействия.
СерверСерверВызов метода findВызов метода findКлиент
Сервер
Сервер
Вызов метода find
Вызов метода find
Получение
|объекта Product
Рис. 5.2. Схема вызова удаленного метода на серверном объекте
На языке RMI-механизма объект, который вызывает удаленный метод, называется клиентским объектом, а удаленный объект называется серверным объектом. Помните, что эта терминология относится только к одному вызову удаленного метода, т.е. компьютер при вызове удаленного метода является клиентом только для этого вызова, а компьютер, обрабатывающий этот вызов, является сервером только этого вызова. Вполне возможно, что при выполнении следующей операции эти компьютеры поменяются ролями, т.е. сервер предыдущего вызова может сам стать клиентом при вызове объекта на другом компьютере.
Заглушки и управление параметрами
При вызове метода удаленного объекта, на самом деле, вызывается обычный метод языка Java, который инкапсулирован в суррогатном объекте— заглушке (stub). Заглушка находится на клиентском компьютере, а не на сервере. Она упаковывает параметры удаленного метода в блок байтов на основе аппаратно-независимого алгоритма кодирования каждого параметра. Например, цифры всегда передаются в порядке, при котором сначала передается старший бит (или байт). При этом объекты кодируются с помощью механизма сериализации, который подробно описывается в главе 12 тома I. Процесс кодированияпараметровназывается свертыванием, параметров (parameter marshalling). Ос-
новная цель свертывания параметров заключается в преобразовании параметров в формат передачи параметров от одной виртуальной машины к другой.
Итак, метод-заглушка клиента создает блок, который состоит из следующих элементов.
Идентификатор удаленного объекта.
Описание вызываемого метода.
Свернутые параметры.
Затем метод-заглушка посылает эту информацию серверу. Далее объект-получатель вызова выполняет следующие действия для каждого вызова удаленного метода.
Развертывание параметров.
Поиск вызванного объекта.
Вызов заданного метода.
Извлечение и свертывание возвращаемого значения или исключительной ситуации данного метода.
Посылка пакета, состоящего из свернутых возвращаемых данных, обратно объекту-заглушке на клиентском компьютере.
Клиентский объект-заглушка развертывает возвращаемое значение или исключение. Если удаленный метод возвращает исключение, то объект-заглушка повторит его в пространстве процесса объекта-клиента. На рис. 5.3 приведена схема информационных потоков при использовании механизма вызова удаленных методов.
ситуацииситуации
Рис. 5.3. Схема вызова удаленных методов со свертыванием/развертыванием параметров
Этот процесс имеет очень сложную структуру, но, к счастью, он выполняется полностью автоматически, а потому в значительной степени без участия программиста. Более того, создатели такого механизма постаралась придать удаленному объекту тот же "внешний вид", который характерен для локальных объектов.
Удаленный метод имеет тот же синтаксис, что и локальный метод. Например, для вызова метода getQuantity объекта-заглушки centralWarehouse центрального хранилища данных на удаленном компьютере потребуется использовать приведенный ниже код.
centralwarehouse.getQuantity("SuperSucker 100 Vacuum Cleaner");
Для доступа к удаленным методам клиентский код всегда использует объектные переменные типа interface. Например, с приведенным выше методом может быть связан следующий интерфейс.
interface Warehouse
{
int getQuantity(String description) throws RemoteException;
}
Объявление объекта для переменной, которая реализует этот интерфейс, будет выглядеть так, как показано ниже.
Warehouse centralWarehouse = ...;
Конечно, интерфейсы являются абстрактными сущностями, которые содержат только перечень методов с сигнатурами. Переменные с типом interface всегда должны быть связаны с фактическим объектом некоторого типа. При вызове удаленных методов объектная переменная обращается к объекту-заглушке. При этом клиентская программа ничего не знает о типе объектов-заглушек, а сами классы-заглушки и связанные с ними объекты создаются автоматически.
Несмотря на то, что создатели данного механизма постарались скрыть от программистов подробности его реализации, все же программистам приходится учитывать разнообразные особенности реализации этого механизма. Эти особенности и составляют основную тему обсуждения остальной части главы.
Удаленные объекты, также как и локальные, участвуют в автоматической сборке мусора. Однако, распределенный сборщик использует счетчик ссылок и не сможет обнаружить циклические объекты, которые ссылаются только друг на друга без каких-либо внешних ссылок. В таких случаях такие циклы программисту нужно разрушить явным образом.
Динамическая загрузка классов
При передаче удаленного объекта другой программе в виде параметра либо в виде возвращаемого значения удаленного метода, программа должна уметь обрабатывать связанный объект-заглушку, т.е. для класса-заглушки необходимо создать соответствующий код. Методы-заглушки выполняют не очень интересную часть работы, они всего лишь свертывают и развертывают параметры, а затем контактируют с сервером для передачи вызовов методов. Вся работа выполняется автоматически без участия программиста.
Более того, должны быть загружены классы соответствующих параметров, возвращаемых значений и исключений. Процесс загрузки может оказаться гораздо сложнее, чем можно себе представить. Например, можно объявить удаленный метод с некоторым возвращаемым типом, который известен клиенту, а на самом деле удаленный метод может возвратить объект подчиненного класса, который не известен клиенту. В таком случае загрузчик классов загрузит производный класс.
Классы-заглушки должны быть всегда доступны для рабочей клиентской программы. С этой целью можно было бы разместить их в локальной файловой системе. Однако, при расширении серверной программы и добавлении новых классов для возвращаемых типов и исключений в таком случае возможно возникновение путаницы при обновлении клиентской программы.
По этой причине в механизме RMI клиентские объекты могут автоматически загружать классы-заглушки из других мест. Этот процесс аналогичен загрузке классов, который применяется для запущенных в броузере аплетов.
При загрузке фрагментов кода из сети всегда возникают сомнения по поводу должного обеспечения безопасности. В связи с этим в приложениях с использованием RMI- механизмаприменяется менеджер обеспечения безопасности. Именноэтотмеханизмзащиты используется для защиты заглушек от проникновения в них вирусов. Для специализированных приложений программисты могут использовать собственные загрузчики классов и менеджеры обеспечения безопасности, но в обычных ситуациях достаточно использовать стандартные средства RMI. (Более подробно загрузчики классов и менеджеры обеспечения безопасности описываются в главе 9, "Безопасность".)
Конфигурирование RMI-механизма
Даже для запуска самого простого примера с использованием вызова удаленного метода необходимо выполнить больше подготовительных действий, чем это нужно для запуска обычной программы или аплета. Во-первых, нужно запустить клиентскую и серверную программы. Информация об объектах должна быть разделена на клиентские интерфейсы и серверную реализацию. Кроме того, нужно создать специальный механизм подстановки, который позволяет клиенту обнаружить нужные объекты на сервере.
Рассмотрим все эти операции на нескольких примерах создания простых приложений. В первом примере создадим группу объектов типа Product для компьютера- сервера. А затем запустим объект-клиент, который будет использоваться для поиска объектов-серверов и передачи им вызовов.
Данный пример можно попробовать выполнить на одном компьютере или на паре компьютеров, объединенных в одной сети. Ниже приводятся подробные инструкции для обоих случаев.
Даже при запуске этого кода на одном компьютере необходимо запустить все необходимые сетевые сервисы, например, протокол TCP/IP. Если на вашем компьютере не установлена сетевая плата, в таком случае установите ее и воспользуйтесь программным обеспечением для установления коммутируемого сетевого соединения.
Интерфейсы и ихреализация
Клиентская программа должна манипулировать серверными объектами, но ей не нужны копии этих объектов. Серверные объекты находятся на сервере, а клиентскому коду нужно лишь знать те операции, которые он может выполнять с серверными объектами. Эти операции описаны в интерфейсе, который может совместно использоваться клиентом и сервером, а потому он располагается на обоих компьютерах.
interface Product // совместно используется клиентом и сервером extends Remote
{
String getDescription() throws RemoteException;
}
Так же, как в приведенном выше примере, все интерфейсы удаленных объектов должны быть расширениями интерфейса Remote, который определен в пакете Все методы этих интерфейсов также должны содержать упоминание о генерации исключительной ситуации RemoteException. Это вызвано тем, что вызовы удаленных методов гораздо менее надежны, чем вызовы локальных методов. Например, подключение к серверу или сети может быть временно недоступно либо в сети могут возникать какие-то другие проблемы. Поэтому клиентский код должен быть готов к обработке этих исключительных ситуаций. Именно по этой причине в языке Java используется принудительный перехват исключительной ситуации RemoteException для каждого вызова удаленного метода с указанием соответствующей обработки каждого неудачного вызова удаленного метода.
осуществляет доступ к объекту-серверу с помощью заглушки, которая реализует используемый интерфейс.
Product р = . . . ;
// ниже приводится описание того, как клиент получает
// ссылку заглушки на удаленный объект
String d =
System.out.printin(d) ;
В следующем разделе описываются способы получения ссылки на этот тип удаленного объекта.
Далее, на сервере нужно создать класс, который выполняет методы, указанные в удаленном интерфейсе.
public class// сервер
extends
implements Product
{
publicd)
throws RemoteException
{ descr = d;
}
public String
throws RemoteException
return "I am a " + descr + ". Buy
}
private String descr;
■ Конструктор Productlmpl объявлен вместе с исключительной ситуацией RemoteException, потому что объект UnicastRemoteObject может вызвать появление такой исключительной ситуации в случаях, когда не удастся соединиться с сетевым сервисом, который отслеживает серверные объекты.
Этот класс имеет один метод getDescription, который можно вызывать из удаленного объекта-клиента.
Класс Productimpl можно назвать сервером удаленных методов, потому что он расширяет класс UnicastRemoteObject, который в платформе Java предназначен для использования удаленных объектов.
■ Класс Productimpl не является типичным серверным классом, потому что выполняет очень малый объем работы. Обычно серверные объекты создаются как раз для выполнения очень большого количества интенсивных операций, которые не способен выполнять локальный объект-клиент. Для подробного изучения механизма работы этих удаленных методов ниже приводится пример на основе класса Product.
Серверные классы обычно расширяют класс RemoteServer пакета java.rmi. server, но класс RemoteServer является абстрактным, который определяет только базовые механизмы взаимодействия между объектами-серверами и их удаленными заглушками. В механизме RMI предусмотрен класс UnicastRemoteObject, который расширяет абстрактный класс RemoteServer и является конкретным классом, т.е. его можно использовать без необходимости создания какого-либо кода. "Путь наименьшего сопротивления" для создания класса-сервера основан на создании класса-наследника от класса UnicastRemoteObject, и именно так созданы все классы-серверы в этой главе. На рис. 5.4 показана диаграмма наследования для таких классов.
Рис. 5.4. Диаграмма наследования классов пакета j ava.rmi.server
Объект UnicastRemoteObject располагается на сервере. Он должен быть активен во время запроса и доступен посредством протокола TCP/IP. Именно этот класс является базовым для создания всех остальных классов-серверов в настоящей книге, а также единственным классом-сервером в текущей версии RMI. Компания Sun и сторонние поставщики в будущем обещают создать другие классы-серверы для RMI- механизма. Например, сотрудники Sun предлагают создать класс MulticastRemoteObjectsдля тех объектов, которые могут реплицироваться на нескольких серверах. Другие возможные способы использования объектов основаны на активизации объектов по требованию или применении других сетевых протоколов.
Если программисту требуется создать собственный класс-сервер без расширения класса UnicastRemoteServer, то в такой ситуации он может вручную создать объекты-серверы на основе реализации интерфейса удаленных объектов с последующей передачей их статичному методу exportobject.
Remote server = new MyServer();
UnicastRemoteServer.exportobject(server, 0) ;
Альтернативным образом в конструкторе можно вызвать метод UnicastRemoteServer. exportobject (this, 0) ;. Его второй параметр содержит значение 0, которое обозначает использование любого подходящего порта для прослушивания клиентских соединений.
При использовании RMI-механизма (или любого механизма работы с распределенными объектами) следует тщательно подбирать ясные и понятные имена для этих классов. В примерах главы используется соглашение об именах, которое позволяет легко определить предназначение этих классов (табл. 5.1).
Таблица 5.1.Соглашение об именах для классов RMI
Обозначение
Описание
Без суффикса (например Product)
Удаленный интерфейс
Суффикс Impl (например
Productlmpl)
Класс-сервер, реализующий этот интерфейс
Суффикс Server (например ProductServer)
Программа-сервер, которая создает объекты- серверы
Суффикс Client (например ProductClient)
Программа-клиент, которая вызывает удаленные методы
Суффикс _Stub (например ProductImpl_Stub)
Класс-заглушка, которая автоматически генерируется программой rmic
Суффикс _Skel (например ProductImpl_Skel)
Класс-каркас, который автоматически генерируется программой rmic. Он необходим для SDK 1.1
Класс Productlmpl необходим для генерации заглушек. Напомним, что заглушки являются классами, которые свертывают (шифруют и отправляют) параметры и развертывают результаты, полученные при вызовах удаленных методов. Программисту не требуется использовать эти классы. Более того, их не нужно создавать вручную. Инструмент rmic генерирует их автоматически, например так, как показано ниже.
rmic —vl.2 Productlmpl
В результате такого вызова инструмент rmic генерирует пакет Produc- t!mpl_Stub.class. Если нужный вам класс находится в этом пакете, то для его использования необходимо применить rmic и полностью указать имя пакета.
При работе с SDK 1.1 для этого можно применить показанную ниже команду.
rmic Productlmpl
В результате выполнения этой команды создаются два класса: класс-заглушка и класс-каркас ProductImpl_Skel.class, который не используется ejava 2.
Учтите, что перед использованием инструмента rmic нужно скомпилировать исходный код с помощью программы javac. Для генерации классов-заглушек в составе пакета для инструмента rmic — указать полное имя пакета.
Поиск объектов-серверов
Для организации доступа к объекту-серверу объект-клиент должен найти локальный класс-заглушку. Чаще всего в этих целях используется вызов удаленного метода другого объекта-сервера, который возвращает нужный объект-заглушку. В этом случае возникает проблема "яйца и курицы", потому что нужно каким-то образом получить доступ к первому объекту-серверу. Для этого в механизме RMI компании Sun преду- смотренаслужбарегистрации самозагрузки (bootstrap registry service).
Таким образом, программа-сервер регистрирует объекты с помощью этой службы, а клиент получает заглушки для нужных объектов. Для регистрации объекта-сервера службе самозагрузки нужно предоставить ссылку на объект и его уникальное имя в виде строки.
// сервер
Productlmpl Р1 = new Productlmpl("Blackwell Toaster");
Naming.bind("toaster" , pl);
Клиент получает заглушку для доступа к объекту-серверу с помощью указания имени сервера и имени объекта, как показано ниже.
// клиент
Product p =
URL-указатели в механизме RMI начинаются с префикса rmi: //, за которым следует имя сервера, номер порта (по желанию) и имя удаленного объекта. Ниже показан еще один пример URL-указателя с указанием порта (по умолчанию используется порт 1099).
rmi://localhost:99/central_warehouse
В глобальном реестре довольно трудно поддерживать уникальные имена, поэтому данный метод не рекомендуется использовать для обнаружения объектов на сервере. Вместо чего следует использовать относительно небольшое количество объектов-серверов, которые регистрируются с помощью службы автозагрузки. Именно эти объекты следует использовать для поиска всех других объектов-серверов. В данном примере это правило нарушается только лишь для демонстрации механизма регистрации и поиска объектов.
Исходя из соображений безопасности, приложение может установить связь, удалить связь или повторно установить связь с зарегистрированными объектами, только если оно находится на том же компьютере, где располагается реестр. Это предотвращает возможность злонамеренного изменения информации в реестре. Однако, любые объекты-клиенты могут просматривать зарегистрированные объекты.
В листингах 5.1-5.3 приводится полный код программы регистрации двух объектов toaster и microwave типа Product.
При сравнении этого примера с примерами создания объектов-серверов в руководстве по RMI можно заметить, что в данном примере не используется установка менеджера обеспечения безопасности сервера. Следует отметить, что вопреки утверждениям в руководстве пользователя менеджер обеспечения безопасности не является необходимым или полезным для функционирования RMI-механизма. На самом деле имеет смысл устанавливать его только для ограничения некоторых операций в реализации объекта-сервера или если сам этот объект-сервер является RMI-клиентом для другого объекта-сервера.
Листинг 5.1. Productserver.java
/**
(aversion 1.10 1999-08-21
@author Cay Horstmann
*/
import java.rmi.*;
import j ava.rmi.server.*;
/**
Программа-сервер, которая инициализирует два удаленных объекта, регистрирует их с помощью сервиса.именования, а потом ожидает вызова удаленных объектов со стороны объектов-клиентов
*/
public class Productserver
{
public static void main(String args[])
{
try
{
System.out.printIn
("Constructing server implementations...");
Productlmpl pl
= new Productlmpl("Blackwell Toaster");
Productlmpl p2
= new Productlmpl("ZapXpress Microwave Oven");
System.out.printin
("Binding server implementations to registry...")
Naming.rebind("toaster", pl);
Naming.rebind("microwave", p2);
System.out.printin
("Waiting for invocations from clients...");
}
{
e.printStackTrace();
} }} }}
} }
} }
Листинг 5.2. ProductDapl.java
/**
«aversion 1.00 1996-09-07
©author Cay Horstmann
*/
import j ava.rmi. *;
import j ava.rmi.server. *;
/**
Реализация класса для удаленных объектов типа Product */
public class Productlmpl
extends UnicastRemoteObject
implements Product
{
/** Конструктор класса Product. ©param n the product name
*/
public Productlmpl(String n) throws RemoteException {
name = n;
}
public String getDescription() throws RemoteException {
return "I am a " + name + ". Buyme ! ";
private String name; }
Листинг5.3. Product.java
/**
©version 1.00 1996-09-07
©author Cay Horstmann
*/
import j
/**
Интерфейс для удаленных объектов типа Product
*/
public interface Product extends Remote
{
/**
Получить описание продукта
©return the product description
*/
String getDescription() throws RemoteException;
}
Запуск сервера
Для использования программы-сервера нужно активизировать службу регистрации автозагрузки с помощью команды rmiregtistry & (для UNIX) и команды start rmiregtistry (для Windows). Команда start используется в режиме командной строки или в операционной системе Windows в диалоговом окне Run (Запуск) для запуска программ в новом окне.
Теперь для запуска объекта-сервера в этом примере используйте команду java ProductServer & (для UNIX) и команду start java ProductServer (для Windows). Если для запуска использовать команду java ProductServer, то она никогда не сможет нормально завершить свою работу. На первый взгляд это может показаться странным, ведь программа всего лишь создает и регистрирует два объекта, а функция main должна нормально прекращать работу немедленно после регистрации. Однако, при создании объекта-наследника класса UnicastRemoteObject создается отдельный поток, который поддерживает активность программы неопределенно долгое время. Таким образом, программа ожидает подключения к ней объектов-клиентов.
В версии SDK для Windows предусмотрена специальная команда javaw, которая запускает интерпретатор байт-кодов в виде отдельного процесса Windows и поддерживает его работу. Для открытия отдельного сеанса работы с Java-программой на основе RMI некоторые специалисты рекомендуют использовать команду j avaw, а не start j ava. Это не рекомендуется делать по следующим причинам.
Во-первых, в Windows не предусмотрено никакого механизма для удаления фонового процесса javaw, т.е. он не будет показан в списке текущих задач. Для чего придется удалить и повторно запустить службу регистрации автозапуска при изменении заглушки зарегистрированного класса. А для удаления процесса, запущенного с помощью команды start, достаточно всего лишь щелкнуть на его окне и нажать комбинацию клавиш <Ctrl+C>.
Во-вторых, при запуске процесса сервера с помощью команды j avaw не учитываются (т.е. нигде не отображаются) выходные сообщения или сообщения об ошибках. Для просмотра выходных сообщений или сообщений об ошибках нужно использовать команду start. В таком случае сообщения об ошибках, по крайней мере, будут показаны в окне режима командной строки. Учтите, что при работе с механизмом RMI ошибки могут возникать очень часто, и эти сообщения могут очень пригодиться разработчикам. Например, чаще всего программисты забывают запустить инструмент rmic. При использовании команды javaw программист не получит никакого сообщения об этой распространенной ошибке и может быть озадачен тем, что созданный им объект-клиент не может найти объекты-серверы.
Перед запуском программы-клиента проверьте правильность регистрации удаленных объектов. Класс Naming содержит метод list, который возвращает список всех текущих зарегистрированных имен. В листинге 5.4 показан простой пример программы, которая позволяет просматривать этот список.
Листинг 5.4. ShowBinding.3 ava
/**
@version 1.00 1996-09-07
@author Cay Horstmann
*/
import j ava.rmi.*;
import java.rmi.server.*;
/**
Эта программа показывает все связи, учтенные в службе именования
*/
public class ShowBindings
{ public static void main(String[] args)
{
try
{
String!] bindings = Naming.list("");
for (int i = 0; i < bindings.length; i++)
System.out.println(bindings[i]);
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
В данном примере выходной поток будет содержать следующие строки.
rmi:/toaster rmi:/microwave
Клиентская часть
Создадим теперь программу-клиент, которая будет запрашивать каждый вновь зарегистрированный объект-сервер для вывода на экран его описания.
Клиентские программы на основе RMI должны установить менеджер обеспечения безопасности для управления деятельностью динамически загружаемых заглушек. Роль такого менеджера выполняет объект RMISecurityManager, который инсталлируется с помощью показанной ниже команды.
System.setSecurityManager(new RMISecurityManager());
Если все классы (включая классы-заглушки) доступны локально, то не обязательно использовать менеджер обеспечения безопасности. Потому что если все файлы классов известны на этапе доставки программы, то их можно разместить локально. Однако, часто возникают ситуации, когда программа-сервер усовершенствуется и в нее добавляются новые классы. Именно в таких случаях удобно использовать механизм динамической загрузки классов. Поэтому всякий раз при загрузке кода из другого источника потребуется использовать менеджер обеспечения безопасности.
Аплеты уже имеют менеджер обеспечения безопасности, который может управлять классами-заглушками, поэтому его не нужно устанавливать при использовании RMI-механизма с аплетами. Однако, он необходим для приложений, которые являются RMI-кл центами.
В листинге 5.5 приводится полный код программы-клиента, которая получает ссылки на два объекта типа Product в реестре RMI и вызывает метод getDescriptionдля описания этих объектов.
Листинг 5.5. ProductCliant.java
/** gversion 1.01 2001-07-10 Uauthor Cay Horstmann
*/
import java.rmi.*;
import java.rmi.server.*;
/**
Эта программа демонстрирует вызов удаленного метода для двух объектов, которые зарегистрированы в реестре
*/
public class ProductClient
{ public static void main(String[] args) {
System.setProperty("java.security.policy", "client.policy"' ' System.setSecurityManager(new RMISecurityManager());
String url =
// Используйте "rmi://yourserver.com/ "
// если сервер расположен на удаленном компьютере
// yourserver.com try {
Product cl =+ "toaster");
Product c2 = (Product)Naming.lookup(url + "microwave");
System.out.printIn(cl.getDescription() ) ;
System.out.println(c2.getDescription()),- }
catch(Exception e) {
j e.printStackTrace();
}
}
Запуск программы-клиента
По умолчанию объект RMISecurityManagerне разрешает программе-клиенту устанавливать сетевые соединения. Однако, программе нужно получить доступ к RMI- реестру и установить связь с объектами-серверами.
После доставки программы-клиента необходимо также получить разрешение на загрузку классов-заглушек. Этот вопрос более подробно рассматривается ниже Dпри описании процесса доставки.
Чтобы позволить программе-клиенту подключиться к RMI-реестру и объекту-серверу, нужно предоставить файл политики (policy file). Более подробно он рассматривается в главе 9, "Безопасность", а здесь стоит лишь сказать, что в данном примере используется файл политики для установления сетевого соединения с портом, номер которого находится в диапазоне 1024-65535. (По умолчанию для RMI используется порт с номером 1099, а для объектов-серверов — порт с любым номером, большим или равным 1024.) grant
permission iava.net.Socketpermission "*:1024-65535", "connect";
Несколько объектов-серверов на одном компьютере-сервере могут совместно использовать один порт. Однако, если при поступлении удаленного вызова этот порт занят, то в таком случае будет автоматически открытдругой порт. Таким образом, иногда количество используемых портов можетбыть меньше, чем количество уда- ленныхобъектовнасервере.
Используемый файл политики в программе-клиенте указывается для менеджера обеспечения безопасности с помощью свойства java. security .policyтак, как по- казанониже.
System.setProperty("java.security.policy", "client.policy");
To же самое можно сделать в режиме командной строки с помощью приведенной ниже команды.
Java —Djava.security.policy=client.policy ProductClient
В SDK 1.1 дляRM I-клиентов не требуется использовать файл политик.
После запуска RMI-реестра и объекта-сервера можно приступать к запуску объекта- клиента. Вот как выглядит весь этот процесс с самого начала.
Откомпилируйте исходный код для классов интерфейса, реализации, клиента и сервера.
javac Product*.java
Запустите программуrmicдля класса реализации.
Rmic -vl.2 ProductImp1
Запустите RMI-реестр.
rmiregistry & (для UNIX)
start rmiregistry (дляWindows)
Запустите объект-сервер.
java ProductServer (для UNIX)
start java ProductServer (для Windows)
Запустите объект-клиент.
java ProductClient
(При этом убедитесь, что файл политики client .policyнаходится в текущем каталоге.)
Эта программа просто отобразит на экране следующие строки.
I am a Blackwell Toaster. Buy me!
I am a ZapExpress Microwave Oven. Buy me!
Посмотрим, что происходит за кулисами этого, на первый взгляд, простого действия. Для вызова удаленного метода getDescription программа-клиент получает с помощью метода lookup ссылку на класс-заглушку. После чего вызывается метод getDescription, который посылает сетевое сообщение объекту-получателю на компьютере-сервере. Объект-получатель вызывает метод getDescription для объекта ProductImp1 на компьютере-сервере. Этот метод создает строку с описанием, а объект-получатель посылает ее по сети обратно. Затем заглушка получает ее и возвращает программе-клиенту. На рис. 5.5 показана схема этого процесса.
java.rmi.Naming
Метод static Remote lookup (String url) возвращает удаленный объект для заданного URL-указателя. Если данное имя не связано, то ге-
нерируется исключительная ситуация NotBound.
Метод static void bind (String name, Remote object) связывает имя name с удаленным объектом obj. Если данный объект уже связан, то генерируется исключительная ситуацияAlreadyBoundException.
Метод static void unbind (String name) удаляет связь с именем name. Если данное имя не связано, генерируется исключительная ситуация NotBound.
Метод static void rebind(String name) связывает имя name с удаленным объектом obj с заменой любой существующей связи.
Метод static String [] list(String url) возвращает массив строк с URL-указателями из реестра, расположенного по заданному URL-указателю. Этот массив содержит снимок имен, представленных в реестре.
ситуацииситуацииситуацииситуацииРис. 5.5. Схема вызова удаленного метода getDescriptionРис. 5.5. Схема вызова удаленного метода getDescription
ситуации
ситуации
ситуации
ситуации
Рис. 5.5. Схема вызова удаленного метода getDescription
Рис. 5.5. Схема вызова удаленного метода getDescription
Подготовка кдоставке
Доставка приложения на основе RMI может оказаться очень сложным процессом из-за большого количества ошибок и использования недостаточно информативных сообщений об ошибках. Лучше всего предварительно выполнить этот процесс локально. На подготовительном этапе следует сложить все файлы в трех каталогах: server, download и client.
Каталогserver содержит все файлы, которые нужны для запуска сервера. Впоследствии их нужно будет переместить на компьютер-сервер. В этом примере каталог server содержит следующие файлы.
Productserver.class
Productlmpl.class
Product.class
ProductImpl_Stub.class
Классы-заглушки также следует сложить в каталоге server. Они нужны для регистрации реализованного объекта. Вопреки распространенному ошибочному мнению, сервер не ищет их в каталоге download даже при указании базовой позиции (codebase).
Каталог download содержит те файлы классов, которые будут загружены на компьютере-клиенте, а также классы, от которых они зависят. В данном примере каталог download содержит следующие файлы.
Product.class
ProductImp1_Stub.class
При работе с SDK 1.1 потребуется также включить в этот каталог файлы классов- каркасов (например, ProductImpl_Skel.class). Впоследствии эти файлы необходимо разместить на Web-сервере.
Помимо интерфейсных классов-заглушек каталогdownload содержит также те классы, от которых они зависят. Это относится к удаленным интерфейсам (например, Product) и любым классам или интерфейсам, которые используются в качестве типов параметров или возвращаемых значений. В противном случае сервер может загадочным образом прекратить свою работу при попытке регистрации класса.
Наконец, каталог client содержит следующие файлы, которые потребуются для запуска ■программы-клиента.
Productclient.class
Product.class
client.policy
Впоследствии эти файлы надо будет разместить на компьютере-клиенте.
Файлы классов для удаленных интерфейсов (например Product), а также те классы, от которых они зависят, следует размещать на компьютере-клиенте. В противном случае программа-клиент не сможет загрузиться.
После размещения классов по каталогам можно приступать к их тестированию.
Если на локальном компьютере используется описанный в главе 3 Web-сервер Apache Tomcat, то каталог download следует сделать подчиненным каталогом для каталога webapps. Иначе, для этого можно использовать упрощенную версию Web- сервера, которая доступна для копирования по адресу ftp://java.sun.com/pub/ jdkl.1/rmi/class-server.zip. Это урезанная версия обычного Web-сервера, которую можно гораздо проще инсталлировать и использовать для проверки работоспособности созданных классов.
Сначала нужно переместить каталог download в каталог с Web-документами (или задать соответствующий путь).
Затем следует отредактировать файл client .policy для указания следующих полномочий клиента.
Подключение к порту 1024 и выше для доступа кRMI-реестру и реализациям объектов-серверов.
Подключение к порту HTTP (обычно 80 или 8080) для загрузки файлов классов- заглушек.
Для этого в файл client .policyнеобходимо вставить следующий код.
grant
{
permission java.net.Socketpermission
1024-65535", "connect";
permission java.net.Socketpermission "*:80", "connect";
)
После чего можно приступать к тестированию.
Запуститые Web-сервер.
Запустите новое окно режима командной строки. Убедитесь в том, что для пути к классам не указаны никакие значения. Затем запустите RMI-реестр.
Для проверки программы с размещением клиента, серверов и заглушек в одном каталоге можно запустить RMI-реестр в этом каталоге. Однако, для доставки нужно запустить реестр в отдельном окне режима командной строки без указания пути к классам. В противном случае RMI-реестр может найти лишние файлы классов, которые приведут к путанице при загрузке дополнительных файлов из другого источника. Причина такого поведения разъясняется в статье по адресу http: //java.sun.com/ product s/j dk/1.2/doc s/guide/rmi/codebase.html. Короче говоря, каждый объект-заглушка аннотируется базовой позицией (codebase), на основе которой могут быть скопированы по сети его классы поддержки. Если RMI-реестр ошибочно найдет какой-то другой класс на указанном пути, то его аннотация будет пустой, и классы поддержки не будут найдены.
Путаница при работе с RMI-реестром является основной причиной возникновения трудностей при работе с механизмом RMI. Чтобы избежать этих затруднений, следует сделать так, чтобы RMI-реестр не смог найти никакие файлы классов.
Запустите новое окно режима командной строки. Перейдите в каталог server. Запустите объект-сервер с URL-указателем на каталог download в качестве значения свойства java. rmi . server.codebase.
java
—Djava.rmi.server.codebase=http://localhost/download/
ProductServer &
При использовании Web-сервера Tomcat нужно применить следующий URL- указатель.
http://localhost:8080/download/
Очень важно, чтобы URL-указатель содержал в конце символ/.
Перейдите в каталог client и убедитесь в том, что в нем находится файл client .policy. Запустите объект-клиент с указанием этого файла в качестве значения свойства j ava.security.policy.
java —Djava.security.policy=client.policy Productclient
Если объекты-серверы и объект-клиент удалось запустить без проблем, то в таком случае можно приступать к доставке этих классов на компьютер-клиент и компьютер- сервер. В противном случае придется еще раз проверить правильность выполнения всех перечисленных выше действий.
Если вы не хотите устанавливать локальный Web-сервер, то для проверки загрузки файлов можно использовать файловый URL-указатель. Однако, организация этого способа работы имеет более сложную структуру. Включите в файл политики приведенную ниже строку.
permission java, io.FilePermission " кaтaлoг_download'l, "read";
Здесь параметр кaтaлoг_download означает полный путь к каталогу download, после которого указывается знак минус (-) для обозначения всех файлов в этом каталоге и подчиненных каталогах. В Windows каждый слеш приводится дважды, например так, как показано ниже.
permission java.io.FilePermission
"с: \\home\\test\\download\\-" , "read";
Запустите RMI-реестр, а потом объект-сервер с помощью приведенной ниже команды.
java —Djava.rmi.server.codebase=file://home/test/download ProductServer & /* для UNIX */
start —Djava.rmi.server.codebase=file:/с:\home\test\download/ ProductServer /* для Windows */
(He забудьте добавить символ / в конце этого URL-указателя.) После чего запустите объект-клиент.
Доставка программы
После проверки доставки программы можно приступать к распределению классов среди компьютеров-клиентов и компьютеров-серверов.
Переместите классы из каталога download на Web-сервер. Убедитесь в том, что при запуске объекта-сервера используется соответствующий URL-указатель. Классы из каталога server переместите на компьютер-сервер, а потом запустите RMI-реестр и объект-сервер.
На этом установка объекта-сервера завершается, и можно приступать к установке объекта-клиента. С этой целью нужно внести два изменения в конфигурацию объекта- клиента. Отредактируйте файл политик, заменяя в нем символ звездочки (*) фактическим именем сервера.
grant
{
permission java.net.SocketPermission
"имя_сервера.com:1024-65535", "connect";
permi ssion j ava.net.Socketpermission
"имя_сервера.com: 8 0 " , "connect";
}
Наконец, в URL-указателе RMI-рсестра в коде объекта-клиента нужно заменить имя локального компьютера localhost фактическим именем сервера так, как показано ниже.
String url =;
Product cl =+ "toaster");
После этого нужно перекомпилировать код объекта-клиента и снова запустить его. Если запуск прошел успешно, значит все перечисленные выше действия были выполнены правильно. В противном случае учтите приведенное ниже замечание с перечнем наиболее распространенных проблем, которые могут возникнуть во время установки RMI-механизма.
На практике не рекомендуется указывать URL-указатели на RMI-реестр. Лучше всего указать их в файле политик, как показано в листинге 5.13.
Потенциальные проблемы при установке RMI-механизма
Доставка RMI-классов затрудняется тем, что в случае возникновения проблем сообщения об ошибках содержат достаточно лаконичную информацию о причинах ошибок. Судя по обсуждению этих проблем (которое можно найти по адресу http://archives.java. sun.com/archives/rmi-users.html), для многих программистов решение проблемы связано с очень большими трудностями. Ниже приводится список некоторых распространенных ошибок, которые могут возникнуть во время установки RMI-механизма.
Находятся ли классы-заглушки в каталоге server?
Находятся ли классы-интерфейсы в каталогах download и client?
Находятся ли классы-родители в этих каталогах вместе с основными классами? Например, в следующем разделе используется интерфейсWarehouse, который использует метод с параметром типа Customer. В этом примере следует проверить, доставляется ли файл класса Customer. class вместе с файлом классаWarehouse. class.
Удалена ли информация по пути к классам CLASSPATH перед запуском RMI-реестра rmiregistry? Освобожден ли текущий каталогот файлов классов?
Используется ли файл политик при запуске объекта-клиента? Содержит ли файл политик правильные имена серверов (или символ * для соединения с любым сервером)?
При использовании файлового URL-указателя (с префиксом file: / /) для тестирования правильно ли указано имя файла в файле политик? Указан ли символ минус (-) в конце файлового URL-указателя? Указан ли двойной слеш (\\)для пути к файлу для Windows?
Указан ли слеш в конце URL-указателя базовой позиции?
Передача параметров для удаленных методов
Удаленным методам часто нужно передавать параметры. В этом разделе рассматриваются некоторые методы передачи параметров с указанием некоторых потенциальных проблем.
Передача неудаленных объектов
При передаче удаленного объекта от сервера к клиенту клиент получает класс- заглушку. С его помощью можно манипулировать объектом на сервере за счет вызова удаленных методов. Однако, этот объект-сервер находится на компьютере-сервере. С помощью вызовов удаленных методов можно передавать любые объекты, а не только те, которые основаны на интерфейсе Remote. Например, метод getDescription из предыдущего раздела возвращает строку, т.е. объект типа String. Эта строка создана объ-
ектом-сервером и передана объекту-клиенту. Тип String не основан на типе Remote, поэтому объект-клиент не может вернуть для него класс-заглушку. Вместо этого объект- клиент получает копию строки, для работы с которой у клиента есть собственный объект типа String. Это значит, что для дальнейшей работы с данной строкой объекту-клиенту уже не нужно поддерживать соединение с любыми объектами-серверами.
Итак, всякий раз при передаче объекта, который не является удаленным, виртуальная машинаJava создает копию и посылает ее по сети. Такой метод отличается от метода передачи параметров для локального метода, в котором передаются только ссылки. Дело в том, что ссылки являются адресами объектов в памяти для локальной виртуальной машины Java, а потому эта информация не имеет смысла для другой удаленной виртуальной машиныJava.
Не трудно представить себе, как копия объекта (например, строки) передается по сети. Для этого используется механизм с^ммшауии, который подробно рассматривается в главе 12 тома I. Это означает, что таким образом может копироваться только та информация, которая реализована на основе интерфейса Serializable. В приведенном ниже примере показан способ копирования параметров и возвращаемых значений в упрощенной программе оформления покупок в магазине подарков. На компьютере-клиенте пользователь запускает программу, которая собирает информацию о получателе подарка, т.е. его увлечения, возраст и пол, как показано на рис. 5.6.
Рис. 5.6. Пример программы оформления покупок в магазине подарков с указанием информации о получателе подаркаРис. 5.6. Пример программы оформления покупок в магазине подарков с указанием информации о получателе подаркаПосле этого объект типа Customer передается объекту-серверу. Так как этот объект не является удаленным, то серверу передается копия объекта. Затем программа- сервер передает обратно массив со списком продуктов-подарков, которые соответствуют созданному профилю пользователя плюс еще один продукт, которому будет рад каждый пользователь, а именно, книгу Core Java. Массив ArrayList не является удаленным объектом, а потому объекту-клиенту будет передана его копия. Как говорится в главе 12 тома I, механизм сериализации создает копии для всех объектов, на которые имеется ссылка внутри копируемого объекта. В данном примере будут сделаны копии для всех элементов списка. В этом примере задача несколько усложнена, потому что элементы списка являются удаленными объектами, т.е. наследуют интерфейс Remote. В таком случае объект-клиент получит копию массива с заглушками элементов списка для объектов-продуктов, которые находятся на сервере. На рис. 5.7 показана схема этого взаимодействия между объектом-клиентом и объектом-сервером.
Рис. 5.6. Пример программы оформления покупок в магазине подарков с указанием информации о получателе подарка
Рис. 5.6. Пример программы оформления покупок в магазине подарков с указанием информации о получателе подарка
КлиентКлиентСерверСерверРис. 5.7. Схема копирования и передачи параметров между объектом-клиентом и объектом- серверомРис. 5.7. Схема копирования и передачи параметров между объектом-клиентом и объектом- сервером
Клиент
Клиент
Сервер
Сервер
Рис. 5.7. Схема копирования и передачи параметров между объектом-клиентом и объектом- сервером
Рис. 5.7. Схема копирования и передачи параметров между объектом-клиентом и объектом- сервером
Итак, удаленные объекты передаются по сети как классы-заглушки, а неудаленные объекты копируются. Все эти операции выполняются автоматически и не требуют вмешательства со стороны пользователя.
При каждом вызове удаленного метода класс-заглушка создает пакет, который содержит копии всех значений параметров и посылает его объекту-серверу с помощью механизма сериализации для свертывания параметров. Затем объект-сервер после получения пакета развертывает его. Конечно же, этот процесс может выполняться очень медленно, особенно если параметры-объекты имеют очень большой размер.
Рассмотрим и проанализируем весь код этой программы. В листингах 5.6 и 5.7 приводятся интерфейсы продуктов (объект Product) и склада (объект Warehouse) соответственно.
Листинг 5.6. Product.java
@version 1.00 1996-09-07
*/*/©author Cay Horstmann
*/
*/
import j
/**
Интерфейс для удаленных объектов-продуктов
*/
public interface Product extends Remote {
/**
Получить описание продукта
©return the product description
*/
String getDescription() throws RemoteException;
final int MALE = 1;
final int FEMALE = 2;
final int BOTH = MALE + FEMALE;
}
Листинг5.7. Warehouse.java
/**
©version 1.01 2001-07-10
©author Cay Horstmann
*/
importj ava.rmi.*;
importjava.util.*;
/**
Удаленный интерфейс для объектов-складов с продуктами
*/
public interface Warehouse extends Remote
{
/**
Получить продукты, которые соответствуют
заданному профилю пользователя
©param с the customer to match
©return an array list of matching products
*/
ArrayList find(Customer c) throws RemoteException;
}
В листинге 5.8 показан код реализации класса Productlmpl для обслуживания данных о продуктах: описание, а также возраст, пол и увлечения потенциального клиента этого продукта. Учтите, что этот класс реализует метод getDescription, объявленный в интерфейсе Product, а также метод match, который не указан в интерфейсе Product. Метод match является примером локального метода, который может вызываться только локально, а не удаленно. Для локального метода не нужно указывать исключительную ситуацию RemoteException.
В листинге 5.9 приводится код класса Customer. Еще раз отметим, что он не является удаленным, т.е. его методы не могут вызываться в удаленном режиме. Однако, этот класс допускает сериализацию, поэтому его объекты можно передавать от одной виртуальной машины к другой.
В листингах 5.10 и 5.11 показаны интерфейс Warehouse и код его реализации в классе Warehouselmpl, который применяется для обслуживания данных о складе продуктов.
Аналогично классу Productlmpl для обслуживания данных о продуктах, класс Warehouselmpl содержит локальные и удаленные методы. Локальные методы addи read используются объектом-сервером для добавления и считывания данных о продуктах на складе. А удаленный метод findиспользуется для поиска нужных продуктов на складе.
Для демонстрации копирования объекта Customer метод findкласса Warehouselmpl очищает содержимое полученного объекта Customer. При возвращении этого метода объект WarehouseClient отображает объект Customer, посланный объекту-серверу. Как можно легко убедиться, он не изменился, потому что была очищена копия, a не оригинал. В этом случае метод reset используется только для демонстрации копирования локальных объектов при передаче их в качестве параметров.
Вообще, методы классов-серверов следует (например Productlmpl и Ware- houselmpl) синхронизировать. В таком случае для нескольких заглушек классов- клиентов можно одновременно выполнять вызовы объекта-сервера, даже если некоторые методы изменяют состояние сервера. (Более подробно методы синхронизации рассматриваются в главе 2, "Коллекции".) В листинге 5.11 показан код синхронизации методов класса Warehouselmpl, так как очевидно, что локальный метод add и глобальный findиспользуются одновременно. Методы класса Productlmpl не синхронизируются, потому что объекты-серверы продуктов не изменяют своего состояния. В листинге 5.12 показан код программы-сервера, которая создает объект-склад и регистрирует его в реестре автозагрузки.
Помните, что перед запуском программы-клиента нужно запустить RMI-реестр и программу-сервер.
В листинге 5.13 показан код программы-клиента. После щелчка мышью на кнопке Submit (Послать) будет создан новый объект Customer с данными о клиентах и передан удаленному методу find. В таком случае запись о клиенте отображается в текстовом поле (для демонстрации того, что вызов clear в программе-сервере не влияет на эти данные). Наконец, в текстовом поле описания отображаются данные о продуктах из возвращенного массива. Учтите, что каждый вызов метода getDescription является вызовом удаленного метода по механизму RMI. Это не очень практичный способ для передачи малых объектов, потому что их проще передавать по значению. Но в данном примере всего лишь демонстрируются возможности автоматической замены удаленного объекта классом-заглушкой во время операции развертывания.
При использовании показанной ниже команды на экране компьютера-сервера в режиме командной строки будут отображены удаленные вызовы методов, которые могут быть очень полезны для отслеживания RMI-трафика.
java —Djava.rmi.server.logCalls=true Warehouseserver &
Листинг 5.8. Productlmpl.java
/**
(aversion 1.00 1996-09-07
©author Cay Horstmann
*/
import java.rmi.*;
import java.rmi.server.*;
/**
Код реализации удаленных объектов-продуктов
*/
public class ProductImp1
extends UnicastRemoteObject
implements Product
{
/**
Конструктор продукта
@param n the product name
@param s the suggested sex (MALE, FEMALE, or BOTH)
@param agelthelowerboundforthesuggestedage
@param age2theupperboundforthesuggestedage
Sparam h the hobby matching this product
*/
public ProductImp1(String n, int s, int agel, int age2, String h) throws RemoteException
{
name = n;
ageLow = agel;
ageHigh = age2;
sex = s;
hobby = h;
/**
Проверка соответствия продукта и профиля клиента. Учтите, что этот метод является локальным методом, который не является частью интерфейса Product @param с the customer to match against this product ©return true if this product is appropriate for the customer
*/
public boolean match(Customer c)
{
if (c.getAge()< ageLow || c.getAge() > ageHigh) return false;
if (!c.hasHobby(hobby)) return false;
if ((sex & c.getSex()) == 0) return false; return true;
}
public String getDescription() throws RemoteException {
return "Iama"+name+". Buyme!";
private String name;
private int ageLow;
private int ageHigh;
private int sex;
private String hobby;
Листинг 5.9. Customer.java
/**
©version 1.10 1999-08-23
©author Cay Horstmann
*/
import java.io.*;
/**
Описание клиента. Учтите, что объекты клиентов не являются удаленными, т.е. не реализуют интерфейс Remote
*/ public class Customer implements Serializable {
/**
Конструктор клиента
@param theAge the customer's age
@param theSex the customer's sex (MALE or FEMALE)
©param theHobbies the customer's hobbies
/
public Customer(int theAge, int theSex, String[] theHobbies) {
age = theAge;
sex = theSex; hobbies = theHobbies;
}
/**
Получить возраст клиента
@return the age
/
public int getAge() { return age; }
/ * * Получить пол клиента @return MALE or FEMALE
/
public int getSex() { return sex; }
/**
Проверка, есть ли у клиента какие-то увлечения @param aHobby the hobby to test ©return true if this customer has the hobby
/
public boolean hasHobby(String aHobby) {
if (aHobby == "") return true;
for (int i = 0; i < hobbies.length; i++) if (hobbies[i].equals(aHobby)) return true;
return false;
}
/** Указание значений no умолчанию для параметров клиента
/
public void reset()
age =0;
sex=0; hobbies=null;
public String toString()
String result = "Age: "+age+", Sex: "; if (sex == Product.MALE) result += "Male"; else if (sex == Product.FEMALE) result += "Female" else result += "Male or Female";
result += ", Hobbies:";
for (inti=0; i<hobbies.length; i++) result+=""+;
privateintage;
private int sex;
private String[] hobbies;
Листинг5.10. Warehouse, java
aversion 1.01 2001-07-10
@author Cay Horstmann
*/
import java.rmi.*;
import java.util.*;
/**
Удаленный интерфейс для склада с продуктами
*/
public interface Warehouse extends Remote {
/**
Получить продукты, которые соответствуют профилю клиента @param с the customer to match ©return an array list of matching products
*/
ArrayList find(Customer c) throws RemoteException;
Листинг5.11. Warehouse.java
1.01 2001-07-10
©author Cay Horstmann
*/
import java.io.*;
import java.rmi.*;
import java.util.*;
import j ava.rmi.server.*;
import java.util.*;
/**
Код реализации удаленного интерфейса Warehouse
*/
public class Warehouselmpl extends UnicastRemoteObject implements Warehouse
{
/**
Конструктор склада
*/
public Warehouselmpl() throws RemoteException
{
products = new ArrayList();
coreJavaBook = new Productlmpl("Core Java Book", 0, 200, Product.BOTH, "Computers");
}
/**
Считывание перечня описаний продуктов. Каждая строка имеет следующий формат:
имя|пол|возраст!|возраст2(увлечения, например Blackwell Toaster|BOTH118|2 00|Household @param reader the reader to read from
*/
public void read(BufferedReader reader) throws IOException {
String line;
while ((line = reader.readLine()) I = null) {
StringTokenizer tokenizer = new StringTokenizer(line, " >
String name = tokenizer.nextToken();
String s = tokenizer.nextToken();
int sex = 0;
if (s.equals("MALE")) sex = Product.MALE;
else if (s.equals("FEMALE")) sex = Product.FEMALE;
else if (s.equals("BOTH")) sex = Product.BOTH; int agel =
int age2 = Integer. parselnt(tokenizer.nextToken()); String hobby = tokenizer.nextToken();
add(new Productlmpl(name, sex, agel, age2, hobby)); }
}
/**
Добавление продукта в склад
Учтите, что это локальный метод @param p the product to add
*/
public synchronized void add(Productlmpl p) {
products.add(p);
}
public synchronized ArrayList find(Customer c)
throws RemoteException
ArrayList result = new ArrayList();
// Добавить все подходящие продукты
for (int i = 0; i < products.size(); i++) {
Productlmpl p = (Productlmpl)products.get(i);
if (p.match(c)) result.add(p);
}
// Добавить продукт, соответствующий всем профилям result.add(coreJavaBook);
// Очистка значений для с только для демонстрации // того, что с является копией
c.reset();
return result;
}
private ArrayList products;
private Productlmpl coreJavaBook;
}
Листинг5.12. Warehouseserver.java
/**
@version 1.11 2001-07-10
©author Cay Horstmann
*/
import java.io.*;
import j
import java.rmi.server.* ;
/**
Эта программа-сервер инициализирует удаленный склад, регистрирует его в службе именования и переходит в состояние ожидания удаленных вызовов со стороны клиентов
*/
public class Warehouseserver
{
public static void main(String[] args) {
try
{
System.out.printin
("Constructing server implementations...");
Warehouselmpl w = new Warehouselmpl();
w.read(new BufferedReader(new
FileReader (''products . dat")));
System.out.printin
("Binding server implementations to registry...");
Naming.rebind("central_warehouse", w) ;
System.out.printin
("Waiting for invocations from clients...");
catch(Exception e)
e.printStackTrace();
}
Листинг5.13.Warehouseclient.java
/**
(Aversion 1.20 1999-08-23
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import java.util.*;
import javax.swing.*;
/**
Клиент для программы-склада
*/
public class WarehouseClient {
public static void main(String[] args) {
System.setProperty("java.security.policy", "client.policy");
System.setSecurityManager(new RMISecurityManager());
JFrame frame = new WarehouseClientFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Фрейм для выбора возраста, пола и увлечений клиента, а также для отображения продуктов, соответствующих заданному профилю после возвращения результатов вызова удаленного метода по отношению к удаленному складу
*/
class WarehouseClientFrame extends JFrame {
public WarehouseClientFrame()
{
setTitle("Warehouseclient") ;
setsize(WIDTH, HEIGHT);
initUI();
try
{
Properties props = new Properties();
String fileName =
Fileinputstream in = new FilelnputStream(fileName); props.load(in);
Siftri(unrglu=r=ln= ll)
url= "rmi://localhost/central_warehouse";
centralWarehouse = (Warehouse)Naming.lookup(url); )
catch(Exception e)
System.out.printin("Error: Can'tconnecttowarehouse. "+e); }
/**
Инициализация пользовательского интерфейса
*/
private void initui() {
getContentPane().setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints(); gbc.fill =
gbc.weightx = 100;
gbc.weighty =
add (new JLabel ("Age:") , gbc, 0, 0, 1, 1) ;
age = new JTextField(4);
age.setText("20") ;
add(age, gbc, 1, 0, 1, 1) ;
male = new JCheckBox("Male", true);
female = new JCheckBox("Female", true);
add(male, gbc, 0, 1, 1, 1) ;
add(female, gbc, 1, 1, 1, 1) ;
add (new JLabel ("Hobbies") , gbc, 0, 2, 1, 1) ;
String[] choices { "Gardening", "Beauty", "Computers", "Household", "Sports" };
gbc.fill = GridBagConstraints.BOTH;
hobbies = new JComboBox(choices);
add(hobbies, gbc, 1, 2, 1, 1) ;
gbc.fill = GridBagConstraints.NONE;
JButton submitButton = new JButton("Submit");
add(submitButton, gbc, 0, 3, 2, 1);
submitButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent evt) {
callwarehouse();
});
gbc.weighty =
gbc.fill = GridBagConstraints.BOTH;
result = new JTextArea(4, 40);
result.setEditable(false) ;
add(result, gbc, 0, 4, 2, 1) ;
Добавление компонента в фрейм
Sparam с the component to add
Sparam gbc the grid bag constraints
©param x the grid bax column
@param у the grid bag row
Sparam w the number of grid bag columns spanned
Sparam h the number of grid bag rows spanned
*/
private void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{ gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane().add(c, gbc) ;
/**
Вызов удаленного склада для поиска продуктов
*/
private void callwarehouse()
{
try
{
Customer с = new Customer(Integer.parselnt(age.getText()) , (male.isSelected() ? Product.MALE : 0)
+ (female.isSelected() ? Product.FEMALE : 0),
new String!] { (String)hobbies,getSelectedItem() });
ArrayList recommendations = centralwarehouse.find(c);
result.setText(c + "\n");
for (int i = 0; i < recommendations.size() ; i++) (
Product p = (Product)recommendations.get(i);
String t = p.getDescription() + "\n";
result.append(t) ;
}
}
catch(Exception e)
{
} result.setText("Exception: "+e);
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
private Warehouse centralWarehouse;
private JTextField age;
private JCheckBox male;
private JCheckBox female;
private JComboBox hobbies;
private JTextArea result;
Передача удаленных объектов
Передачаудаленныхобъектовобъекту-клиентуотобъекта-серверавыполняетсяочень просто. Объект-клиентполучает заглушку и сохраняет ее в переменной с типом удаленно-
го интерфейса. Теперь объект-клиент может получить доступ к фактическому удаленному объекту на компьютере-сервере с помощью этой переменной. Объект-клиент может копировать переменную на свой собственный компьютер, но все полученные копии — это не более, чем ссылки на данную заглушку. Учтите, что с помощью заглушки можно получить доступ только к удаленным интерфейсам, т.е. интерфейсам-наследникам класса Remote. Причем, все локальные методы будут недоступны через заглушку. (Помните, что локальным называется метод, который не определен в удаленном интерфейсе.)
RemoteRemoteДалее, заглушки генерируются только на основе классов, которые реализуют удаленный интерфейс, а потому в заглушках-классах приводятся только методы, указанные в этих интерфейсах. Если удаленный интерфейс реализует не подчиненный класс, а суперкласс, то при передаче объекта подчиненного класса удаленному методу будут доступны только методы суперкласса. Для лучшего понимания этого механизма рассмотрим следующий пример, где класс Booklmpl является производным классом от класса Productlmpl.
Remote
Remote
class Booklmpl extends Productlmpl {
public Booklmpl(String title, String thelSBN, int sex, int agel, int age2, String hobby)
{
super(title +Book", sex, agel,
age2, hobby);
ISBN = new thelSBN;
}
public String getStockCode() { return ISBN; }
private String ISBN;
Рис. 5.8. Удаленными являются только
Теперь предположим, что объект-книга переда- методы класса Productlmpl ется удаленному методу либо в качестве параметра, либо как возвращаемое значение. Получатель получает объект-заглушку, но заглушка является не заглушкой-книгой, а заглушкой суперкласса Productlmpl, потому что только этот класс реализует удаленный интерфейс (рис. 5.8). Таким образом, в приведенном случае метод getStockCode не будет доступен в удаленном режиме.
Удаленный класс может реализовывать несколько интерфейсов. Например, класс Booklmpl кроме интерфейса Product может реализовать второй интерфейс. Ниже приводится пример удаленного интерфейса StockUnit и класса Booklmpl, который реализует его.
Interface StockUnit extends Remote {
public String getStockCode() throws RemoteException;
)
class Booklmpl extends Productlmpl implements StockUnit {
public Booklmpl(String title, String thelSBN,
int sex, int agel, int age2, String hobby)
throws RemoteException
super(title + " Book", sex, agel, age2, hobby); ISBN = new thelSBN;
}
public String getStockCode() throws RemoteException {
return ISBN;
}
private String ISBN;
}
На рис. 5.9 показана диаграмма наследования этих классов.
Рис. 5.9. Класс Booklmpl имеет дополнительные удаленные методы
Теперь, при передаче объекта-книги удаленному методу получатель получит заглушку, которая позволяет получить доступ к удаленным методам в классах Product и StockUnit. Для поиска и обнаружения того интерфейса, который реализуется данным удаленным объектом, следует использовать метод instanceof. Ниже приведена типичная ситуация, в которой используется этот метод. Предположим, что удаленный объект получен с помощью переменной типа Product.
ArrayList result = centralwarehouse.find(c);
for (int i = 0; i < result.size(); i++) {
Product p = (Product)result.elementAt(i);
) ’ '
Теперь удаленный объект может быть или не быть объектом-книгой. И для ответа на этот вопрос воспользуемся методом instanceof. Однако, его нельзя использовать так, как показано ниже.
if (p instanceof Booklmpl) // неправильно
{
Booklmpl b = (Booklinpl)p;
Объект р ссылается на объект-заглушку, а класс Booklmpl является классом объекта-сервера. Можно было бы тип объекта-заглушки привести к типу BookImpl_Stub.
if (p instanceof Bookimp1_Stub) {
Bookimp1_Stub b = (Bookimp1_Stub)p; // бесполезно
)
Однако, этот способ вряд ли будет полезен. Объекты-заглушки генерируются автоматически программой rmic для внутреннего использования с помощью RMI- механизма, а потому клиентам не следует заботиться об этом. Вместо этого можно было бы выполнить приведение к типу другого интерфейса.
i£ (p instanceof StockUnit) {
StockUnit s = (StockUnit)p;
String с = s.getStockCode();
}
Данный код проверяет объек-гз аглуш ку, на которую ссылается р, т.е. реализуется ли интерфейс StockUnit этим объектом-заглушкой. В нашем случае будет вызван удаленный метод getStockCode этого интерфейса.
Итак, в отношении вышесказанного можно сделать следующие выводы.
Если объект, относящийся к классу, который реализует удаленный интерфейс, передается удаленному объекту, то удаленный метод получит объект-заглушку.
Тип объекта-заглушки можно привести к типу любого удаленного интерфейса, который реализован.
Все определенные в этих интерфейсах удаленные методы можно вызывать, но нельзя вызывать любые локальные методы с помощью объекта-заглушки.
Использование наборов удаленных объектов
Как уже говорилось в главе 2, "Коллекции", объекты в наборах должны переопределять метод equals. В случае хэш-множества или хэш-таблицы также следует определить метод hashCode. Однако, при сравнении удаленных объектов эта задача значительно усложняется. Для сравнения содержимого двух удаленных объектов метод equals должен обратиться к объектам-серверам, которые содержат удаленные объекты. В случае сбоя в работе этого метода не будет генерироваться исключительная ситуация RemoteException, потому что метод equals объявлен в классе Object без указания этой ситуации. Однако, как известно, все методы удаленного интерфейса должны генерировать данную исключительную ситуацию. Так как метод подчиненного класса не может генерировать больше исключений, чем метод суперкласса, который он переопределяет, то метод equals нельзя переопределить в удаленном интерфейсе. Это замечание также справедливо для метода hashCode.
Вместо этого нужно переопределить методы equals и hashCode в классе RemoteObject, который является суперклассом для всех объектов-заглушек и объектов-серверов. Такие методы сравнивают не содержимое объектов, а расположение объектов-серверов. Метод equals класса RemoteObject считает два объекта-
заглушки равными, если они ссылаются на один и тот же объект-сервер. А если два объекта-заглушки ссылаются на разные объекты-серверы, то такие объекты-заглушки никогда не будут признаны равными, даже если они имеют одинаковое содержимое. Аналогично, хэш-код для них будет вычислен на основании идентификатора объекта. Таким образом, объекты-заглушки для разных объектов-серверов будут иметь разные хэш-коды, даже если объекты-серверы имеют идентичное содержимое.
Это ограничение относится только к объектам-заглушкам. А для классов объектов- серверов допускается переопределение методов equals и hashCode. Данные методы вызываются при вставке объектов-серверов в коллекцию на сервере, но они никогда не вызываются при сравнении или хэшировании объектов-заглушек. Для прояснения этой особенности нарис. 5.10 показана схема наследования методов equals и hashCode.
Класс RemoteObject является базовым классом для классов объекта-заглушки и для объекта-сервера. На стороне объекта-заглушки нельзя переопределить методы equals и hashCode, потому что заглушки генерируются автоматически. На стороне объекта- сервера их можно переопределить для классов реализации интерфейса, но они могут использоваться только локально на своем сервере. При переопределении этих методов реализованный класс и его объект-заглушкауже не считаются идентичными.
Рис. 5.10. Схема наследования методов equals и hashCode
Итак, объекты-заглушки можно использовать в хэш-таблицах, но нужно помнить, что при сравнении или хэшировании не учитывается содержимое удаленных объектов.
Клонирование удаленных объектов
Объекты-заглушки не имеют метода clone, поэтому нельзя клонировать удаленный объект с помощью метода clone. Это объясняется следующими техническими причинами. Если бы это было возможно, то в методе clone пришлось бы предусмотреть генерирование исключительной ситуации Remot eExc ept i on. Но метод клонирования clone в суперклассе Objectне может инициировать никакие исключительные ситуации, кроме CloneNotSupportedException. Такое же ограничение упоминается в предыдущем разделе, т.е. методы equals и hashCode сравнивают ссылки на объекты, а не содержимое объектов. Но вряд ли имеет смысл создавать клон объекта- заглушки только для получения копии ссылки на удаленный объект, потому что это можно сделать с помощью копирования переменной объекта-заглушки. Именно по этой причине метод clone не определен для объектов-заглушек.
Для клонирования удаленного объекта нужно создать отдельный метод, например, remoteClone. Его нужно поместить в интерфейс, который определяет сервисы удаленных объектов. Такой метод способен инициировать исключительную ситуацию RemoteException. В классе, реализующем этот интерфейс, следует просто определить метод remoteClone для вызова метода clone и возвращения клонированного объекта, например так, как показано ниже.
interface Product extends Remote {
public Object remoteClone()
throws RemoteException, CloneNotSupportedException;
)
class Productlmpl extends UnicastRemoteObject implements Product
{
public Object remoteClone()
throws CloneNotSupportedException
{ return clone(); }
}
Непригодные параметры удаленных методов
Предположим, что приложение для заказа покупок нужно усовершенствовать таким образом, чтобы выбранные подарки сопровождались их изображениями. Можно ли для этого создать удаленный метод void paint (Graphics g) throws RemoteException в интерфейсе Product? К сожалению, нет, потому что класс Graphics не может реализовать удаленный интерфейс. Поэтому копию объекта типа Graphics нельзя будет передать удаленному объекту. Дело в том, что класс Graphics является абстрактным классом, а объекты типа Graphics возвращаются с помощью метода getGraphics класса Component. Этот вызов может использоваться для некоторых подчиненных классов, которые реализуют графический контекст для отдельной платформы. Такие объекты, в свою очередь, должны взаимодействовать с собственным графическим кодом платформы. Для этого они должны хранить указатели на области памяти, которые используются собственными графическими методами плат-
формы. Поскольку в Java не поддерживается работа с указателями, то эта информация хранится в виде целочисленных объектов в графическом объекте и в случае необходимости приводится к типу указателя для использования в собственных методах данной платформы. На компьютере-клиенте и компьютере-сервере могут использоваться разные графические оболочки, например Windows и Xll. Поэтому сервер может и не иметь средств для обработки графических объектов клиента. Однако, даже если они используют одинаковые оболочки, все равно значения указателя не будут иметь никакого смысла для сервера. Поэтому нет никакого смысла копировать графический объект. По этой причине класс Graphics не является сериализуемым, а потому не может передаваться с помощью RMI-механизма.
Вместо этого сервер для передачи изображения клиенту должен использовать другой механизм передачи данных по сети. Оказывается, это не так просто сделать. Например, класс Image является аппаратно-зависимым, также как и класс Graphics. Иначе, можно было бы послать данные изображения в виде последовательности байтов в формате JPEG либо послать массив целочисленных значений для обозначения пикселей. В следующем разделе описывается более простой способ на основе передачи клиенту URL-указателя и использования методов класса Applet для чтения изображения по этому URL-указателю.
Использование RMI-механизма вместе
с аплетами
При совместном использовании RMI-механизма и аплета нужно учитывать несколько особенностей. Например, аплеты имеют собственный менеджер обеспечения безопасности, потому что запускаются внутри броузера. Поэтому на стороне клиента можно не использовать менеджер обеспечения безопасности RMISecurityManager.
Прежде всего нужно позаботиться о размещении объектов-заглушек и объектов- серверов. Вот что происходит при открытии в окне броузера Web-страницы с дескриптором APPLET. Броузер загружает упомянутый в этом дескрипторе файл класса и все другие файлы классов по мере их необходимости во время своего выполнения. Файлы классов загружаются с того же компьютера, на котором располагается эта Web- страница. Из-за ограничений безопасности аплета он может соединяться только с исходным компьютером. Поэтому объекты-серверы должны располагаться на том же компьютере, где находится Web-страница. Итак, на одном и том же компьютере- сервере должны располагаться следующие объекты.
Web-страница.
Код аплета.
Классы-заглушки.
Объекты-серверы.
RMI-реестр.
Ниже приводится пример аплета на основе предыдущей программы заказа подарков. Как и в предыдущем примере, аплет получает информацию о клиенте и выбирает подарки, соответствующие профилю клиента. Эта информация сопровождается изображениями отобранных подарков, как показано на рис. 5.11. Как уже упоминалось выше,
передача изображений с компьютера-сервера на компьютер-клиент представляет собой очень сложную задачу, потому что изображения на разных компьютерах могут храниться по-разному в зависимости от типа локальной графической оболочки. Однако, в этом примере сервер просто посылает клиенту строку с именем графического файла, а потом для получения изображения используется метод getImage класса Applet.
Рис. 5.11. Использование аплета для программы заказа подарков
В такой ситуации нужно следующим образом распределить код программы.
Класс java.rmi.registry.RegistryImpl нужно расположить на компьютере- сервере, причем реестр должен быть запущен до старта аплета.
Класс Warehouseserver нужно расположить на компьютере-сервере, причем этот объект-сервер нужно запустить до старта аплета.
Класс Warehouselmpl нужно расположить на компьютере-сервере в любом месте, где его сможет найти объект-сервер WarehouseServer.
Класс WarehouseApplet нужно расположить в том каталоге, который указан в дескрипторе APPLET.
Классы-заглушки нужно расположить в том же каталоге, где находится класс WarehouseApplet.
Аплет ищет RMI-реестр на том же компьютере, на котором находится код аплета.
Для поиска этого компьютера используются методы getCodeBase и getHost.
String url =+ getCodeBase().getHost()
+ "/central_warehouse";
centralwarehouse =
В листинге 5.14 показан код этого аплета. Обратите внимание, что аплет не инсталлирует менеджер обеспечения безопасности. В листинге 5.15 показан код HTML- страницы данного аплета. Код объектов-продуктов и»объекта-склада продуктов немного изменен за счет добавления поля с именем файла с изображением. Более подробные сведения об этих изменениях можно найти в прилагаемом ниже коде.
Листинг 5.14. WarehoueeADPlet. java
/**
Aversion 1.21 2001-07-11
©author Cay Horstmann
*/
import j
import java.awt.event.*;
import j
import j ava.rmi.*;
import java.rmi.server.*;
import java.util.*;
import j avax.swing.*;
/**
Аплет клиента
*/
public class WarehouseApplet extends JApplet {
public void init() {
initUI();
try {
String warehouseName = getParameter("warehouse.name"); if (warehouseName == null)
warehouseName = "central_warehouse";
String url = "rmi://" + getCodeBase().getHost()
+ "/" + warehouseName;
centralwarehouse =
}
catch(Exception e)
{
showstatus("Error: Can't connect to warehouse. " + e) ; }
}
/**
Инициализация пользовательского интерфейса
*/
private void initUI() {
getContentPane().setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints(); gbc.fill =
gbc.weightx = 100;
gbc.weighty =
add(new JLabel("Age:"), gbc, 0, 0, 1, 1) ;
age = new JTextField(4);
age.setText("20") ;
add(age, gbc, 1, 0, 1, 1);
male = new JCheckBox("Male", true);
female = new JCheckBox("Female", true);
add(male, gbc, .0, 1, 1, 1);
add(female, gbc, 1, 1, 1, 1);
add (new JLabel ("Hobbies") , gbc, 0, 2, 1, 1);
String[] choices = { "Gardening", "Beauty", "Computers", "Household", "Sports" };
gbc.fill = GridBagConstraints.BOTH;
hobbies = new JComboBox(choices);
add(hobbies, gbc, 1, 2, 1, 1);
gbc.fill = GridBagConstraints.NONE;
JButton submitButton = new JButton("Submit");
add(submitButton, gbc, 0, 3, 2, 1);
submitButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent evt) {
callwarehouse();
}) ;
gbc.weighty = 100;
gbc.fill = GridBagConstraints.BOTH;
result = new JTextArea(4, 40);
result.setEditable(false); add(result, gbc, 0, 4, 2, 1);
imagePanel = new JPanel();
imagePanel.setMinimumSize(newDimension(0, 60)) add(imagePanel, gbc, 0, 5, 2, 1);
/**
Добавить компонент в фрейм
Sparam с the component to add
Sparam gbc the grid bag constraints
Sparam x the grid bax column
@param у the grid bag row
@param wthenumberof gridbagcolumns spanned
Sparam hthenumberof gridbagrows spanned
*/
private void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{ gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane().add(c, gbc);
Вызвать удаленный склад для поиска соответствующих продуктов
*/ private void callwarehouse() {
try {
Customer с = new Customer(Integer.parselnt(age.getText()), (male.isSelected() ? Product.MALE : 0)
+ (female.isSelected() ? Product.FEMALE : 0), new String[] { (String)hobbies.getSelectedltem() }) ;
ArrayList recommendations = centralwarehouse.find(c); result.setText(c + "\n") ;
imagePanel.removeAll();
for (int i = 0; i < recommendations.size(); i++) {
Product p = (Product)recommendations.get(i) ;
String t = p.getDescription() + "\n";
result.append(t); Image productImage =
imagePanel.add(new JLabel(new Imageicon(productimage))); }
)
catch(Exception e)
{
j result.setText("Exception: "+e); }
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
private Warehouse centralWarehouse;
privateage;
privatemale;
private JCheckBox female;
private JComboBox hobbies;
private JTextArea result;
private JPanel imagePanel;
ЛистингIS. 15.WarehouaeApplet.html
<applet code="WarehouseApplet.class" width="600" height="400">
<param name="warehouse.name" value="central_warehouse"/>
</applet>
Активизация объекта-сервера
В предыдущих примерах для инициализации и регистрации объектов-серверов использовалась программа-сервер. Благодаря этому, объекты-клиенты могли создавать удаленные запросы для них. Однако, в некоторых случаях такой способ приводит к неэффективному использованию ресурсов сервера. Действительно, приходится инициализировать множество объектов-серверов, которые переходят в состояние ожидания соединения с
ними независимо от того, используются ли они объектами-клиентами или нет. Механизм активизации позволяет задержать создание объектов таким образом, что создается только при поступлении вызова со стороны объекта-клиента.
Для применения данного механизма код объекта-клиента можно не изменять, потому что он всего лишь запрашивает удаленную ссылку и выполняет вызовы ее основании.
Однако, программу-сервер в этом случае придется заменить программой активизации, которая создает дескрипторы активизации для объектов, которые предполагается создать позднее, а также связывает получателей удаленных вызовов с сервисом именования. При поступлении первого вызова для некого объекта информация в его дескрипторе активизации используется для создания этого объекта.
Объект-сервер, который используется для механизма активизации, должен расширять класс Activatable, а также реализовывать один или несколько удаленных интерфейсов, например так, как показано ниже.
class
extends Acivatable
implements Product {
Для создания объекта с задержкой необходимо предоставить конструктору следующие параметры.
Идентификатор активизации (которому просто передается конструктор суперкласса).
Один объект, который содержит всю информацию о конструкторе, заключенную в классMarshalledObject.
Для сохранения нескольких параметров конструктора нужно запаковать их в одном объекте с помощью массива Obj ect [ ] иПри этом сериализованная
(или свернутая) копия информации для конструктора сохраняется в дескрипторе активизации. Для десериализации (развертывания) этой информации конструктор объекта должен использовать метод get классаect.
В этом примере в классе Productlmpl для создания объекта-сервера следует предоставить только имя продукта. Свертывание этой информации организуется в классе а развертывание так, как показано ниже в конструкторе объекта- сервера.
publicid,data)
{
0);
String n = name = n;
+ name);
}
Передача значения 0 в качестве второго параметра означает, что должна присвоить подходящий номер порта для порта слушателя.
Этот конструктор выведет на экран сообщение об активированном в данный момент объекте-сервере.
На самом деле не обязательно, чтобы объект-сервер расширял класс Activatable. Например, достаточно, чтобы в конструкторе объекта-сервера содержался следующий вызов статического метода.
id, 0);
Вернемся к программе активизации. Во-первых, в ней необходимо определить группу активизации, которая описывает общие параметры для запуска виртуальной машины, содержащей объекты-серверы. Наиболее важным параметром является политика обеспечения безопасности.
Как и при работе с другими объектами-серверами, никаких проверок безопасности не выполняется. (Предполагается, что объекты поступают из заверенного источника.) Однако, виртуальная машина, в которой запущен активизированный объект, имеет инсталлированный менеджер обеспечения безопасности. Для указания и использования всех полномочий нужно создать файл политики обеспечения безопасности policy. Он может иметь следующее содержание.
grant
{
permission };
Создание дескриптора группы активизации выполняется с помощью приведенного ниже кода.
Properties prop = new
props.put("java.security.policy", "/server/server.policy") ;
ActivationGroupDesc group = newnull);
Второйиспользуется для описания специальных параметров команды,
но в данном примере они не нужны, поэтому для него передается значение null.
Затем нужно создать идентификатор группы с помощью вызова показанного ниже метода.
ActivationGroupID id =
Теперь можно приступать к созданию дескрипторов активизации. Для каждого объекта-сервера, который предполагается создавать по первому требованию, нужно предоставить следующие сведения.
Идентификатор группы активизации для виртуальной машины, в которой должен быть создан данный объект.
Имя класса (например,или
Базовую URL-строку для загрузки файлов классов без указания пути к пакетам.
Свернутую информацию для конструктора.
Это может быть сделано так, как показано ниже.
MarshalledObject
= newToaster");
ActivationDesc desc = new"Productlmpl",
"http://myserver.com/download/", param);
Далее нужно передать этот дескриптор статическому методуActivatable. register. Он возвратит объект некоторого класса, который реализует удаленные интерфейсы класса реализации. Теперь его можно связать с сервисом именования следующим образом.
Product p = (Product)Activetable.register(desc);
Naming.rebind("toaster", p);
В отличие от программы-сервера из предыдущих примеров эта программа активизации завершает свою работу после регистрации и связывания получателей, а объекты-серверы создаются при первом вызове удаленного метода.
В листингах 5.16 и 5.17 показан код реализации класса для активируемого объекта с информацией о продуктах и код программы активизации соответственно. Код класса-интерфейса и код программы-клиента остаются неизменными.
Для запуска данной программы нужно выполнить следующие действия.
Скомпилировать все исходные файлы.
Запустить инструмент runic для генерации заглушки для класса ProductImp1 так, как показано ниже.
rmic -vl.2 Productlmpl
Запустить RMI-реестр.
Запустить демон (или сервис) активизации RMI.
rmid —J-Djava.security.policy=rmid.policy & (для UNIX)
start rmid -J-Djava.security.policy=rmid.policy (для Windows)
Программа rmid перехватывает запросы активизации и активирует объекты в отдельной виртуальной машине. Для запуска виртуальной машины программа rmid должна иметь определенные полномочия, которые указываются в файле политики (листинг 5.18). Параметр —J используется для передачи параметров виртуальной машине, которая запустила демон (или сервис) активизации.
Запустите программу активизации. В этой конфигурации предполагается, что программа запущена в каталоге, который содержит файлы классов и файл политики сервера.
java ProductActivator
Программа активизации завершает свою работу после регистрации и связывания активируемых объектов с сервисом именования.
Запустите программу-клиент.
java —Djava.security.policy=client.policy ProductClient
Она выведет на экран список продуктов с описаниями. При первом запуске программы-клиента в режиме командной строки будут выведены все сообщения конструктора.
л*^г^^Й!йй^1^^■ к?
1.00 Cay Horstmann
*/
import java.io.*;
import java.rmi.*;
import;
/**
Класс реализации удаленных объектов с описанием продуктов
*/
public class Productlmpl extends Activatable
implements Product
/**
Создать продукт
@param id the activation id
@param data the marshalled construction parameter (containing the product name)
*/
publicid,data)
throws RemoteException, IOException, ClassNotFoundException
0);
String n = name =
" + name);
public Stringthrows RemoteException
returnam a " + name + ". Buy
private String name; }
Листинг5.17. ProductActivator.java'
/**
1.10 1999-08-21
©author Cay Horstmann
*/
import java.io.*;
import
j
//import
import
import java.util.*;
/**
для активизации двух удаленных
объектов и регистрации их в сервисе именования
*/
public class ProductActivator
{
public static void main(String args[])
{
try
{
System.out.printin
("Constructing activation descriptors...");
Properties props = new Properties();
// Использование файла server.policy
// из текущего каталога
props.put("java.security.policy”,
new File("server.policy”).getCanonicalPath());
ActivationGroupDesc group
= new ActivationGroupDesc(props, null);
ActivationGroupID id =
MarshalledObject plparam
= new MarshalledObject("Blackwell Toaster");
MarshalledObject p2param
= new MarshalledObject("ZapXpress Microwave Oven");
String classDir = ".";
// Преобразование каталога классов
//в файловый URL-указатель.
//В этом примере предполагается,
// что классы находятся в текущем каталоге
String classURL = new
File(classDir) .getCanonicalFile().toURL().toString(] ;
ActivationDesc descl
= new ActivationDesc(id, "Productlmpl", classURL, plparam);
ActivationDesc desc2
= new ActivationDesc(id, "Productlmpl", classURL, p2param);
Product Pl =
Product p2 = (Product)Activatable.register(desc2);
System.out.printin
("Binding activable implementations to registry. ..”!'■
Naming.rebind("toaster", pl);
Naming.rebind("microwave”, p2) ;
System.out.printin
("Exiting...");
}
catch(Exception e) {
e.printStackTrace() ;
Листинг 5.18. raid.policy
permission com.sun.rmi.rmid.ExecPermission " $ { java.home}${/}bin${/}java";
permission com.sun.rmi.rmid.ExecOptionPermission "-Djava.security.policy=*";
Листинг 5.19. server.policy
grant {
} permission java.security.AllPermission;
Листинг 5.20. client.policy
grant
{
permission java.net.Socketpermission "*:1024-65535", "connect,accept";
permission java.net.Socketpermission ”localhost: 80", "connect";
java.rmi.activation.Activatable
• Метод protected Activatable(ActivationlD id, int port) создает активируемый объект и задает слушателя по указанному порту.
Параметры:
id port
идентификатор активизации
номер порта или значение 0 для автоматического выбора номера порта
• Метод static Remote exportObject(Remote obj, ActivationID id, int port) активирует удаленный объект и возвращает активированный объект, который будет доступен для удаленных вызовов.
Параметры:
obj
id port
объект-сервер, который относится к классу реализации одного или нескольких удаленных интерфейсов
идентификатор активизации
номер порта или значение 0 для автоматического выбора номера порта
•Метод Remote register (ActivationDescriptor desc) регистрирует
дескриптор для активируемого объекта, готовит его для получения удаленных вызовов и возвращает активированный объект, который будет доступен для удаленных вызовов.
Параметры:
descдескриптор активизации
java. rmi.MarshalledObject
Конструктор MarshalledObject(Ob ject obj ) создает объект, содержащий сериализуемые данные указанного объекта.
Параметры:
objсериализуемый объект
• Метод get () десериализует хранимые данные объекта и возвращает этот объект.
j ava.rmi.activation.ActivationGroupDesc
• Конструктор ActivationGroupDesc (Properties prop, Activation- GroupDesc.CommandEnvironment env) создает дескриптор группы активизации, который указывает свойства виртуальной машины, которая содержит активируемые объекты.
Параметры:
props env
системные свойства виртуальной машины, параметры команды (путь к файлу виртуальной машины, а также параметры командной строки) или значение null, если
они не заданы
java, rmi.activation.ActivationGroup
МетодActivationSystem getSystemO возвращает ссылку на систему активизации.
java.rmi.activation.ActivationSystem
Метод ActivationGroupID registerGroup(ActivationGroupDesc group) регистрирует группу активизации и возвращает идентификатор группы.
Параметры:
groupдескриптор группы активизации
java, rmi.activation.ActivationDesc
Конструктор ActivationDesc (ActivationGroupID id, String className, String location, MarshalledObj ect data) создает дескриптор
активизации.
Параметры:
idидентификатор группы активизации
className полностью заданное имя класса объекта-сервера location URL-указатель для загрузки файлов классов dataсвернутая информация для конструктора
Технологии Java IDL и CORBA
В отличие от RMI-механизма технология CORBA позволяет создавать запросы между объектами на языке Java и объектами на других языках программирования. Технология CORBA зависит от наличия брокера объектных запросов (Object Request Broker) или ORB-брокера на компьютере-клиенте и на компьютере-сервере. ORB- брокер можно представить себе как универсальный транслятор межобъектного взаимодействия на основе CORBA. В спецификации CORBA 2 определяется более десятка
сервисов, которые ORB-брокер может использовать для выполнения разных задач: для запуска и сопровождения процессов, для всего жизненного цикла объектов от создания и копирования до перемещения и удаления, а также для сервиса именования, который позволяет находить объекты по имени.
Платформа Java 2 содержит реализацию технологии CORBA 2, совместимую с ORB-механизмом. Таким образом, все приложения и аплеты платформы Java 2 обладают способностью соединяться с удаленными CORBA-объектами.
Компания Sun упоминает технологию CORBA в платформе Java под названием Java IDL, хотя это неправильно. Термин IDL (Interface Definition Language) означает язык описания интерфейсов, т.е. язык описания классов-интерфейсов. Важным аспектом технологии CORBA является организация взаимодействия между объектами, а не только поддержка IDL.
Процесс создания CORBA-объектов включает следующие этапы.
Создание интерфейса, в котором указан способ работы объектов, с помощью языка IDL. Язык описания интерфейсов (Interface Definition Language- IDL) предназначен для описания интерфейсов в формате, который не зависит от языка программирования.
Для генерации классов-заглушек и классов справки используются IDL- компиляторы для целевых языков.
Создание кода реализации объектов-серверов на целевых языках. (Созданный IDL-компилятором каркасный код служит лишь связующим слоем. Поэтому для методов объекта-сервера придется создать код реализации.) Затем— компиляция кода реализации.
Создание программы-сервера, которая создает и регистрирует объекты- серверы. Наиболее удобным способом регистрации объектов-серверов является использование сервиса именования CORBA, который работает аналогично RMI-реестру.
Создание программы-клиента, которая находит объекты-серверы и вызывает их сервисы.
Запуск сервиса именования и программы-сервера на компьютере-сервере и программы-клиента на компьютере-клиенте.
Эти этапы аналогичны этапам, которые используются для создания распределенных приложений с помощью RMI-механизма. Они отличаются только двумя особенностями.
Для реализации объектов-серверов и объектов-клиентов можно использовать любой язык, который поддерживается CORBA.
Для указания интерфейсов используется язык IDL.
В следующих разделах описываются способы применения языка IDL для определения интерфейсов CORBA, а также способы соединения объектов-клиентов, реализованных на языке Java, с объектами-серверами, реализованными на языке C++, а также объектов-клиентов, реализованных на языке C++, с объектами-серверами, реализованными на языке Java.
Технология CORBA представляет собой очень обширный и сложный предмет обсуждения, поэтому в этой книге приводятся лишь некоторые основные сведения и
простейшие примеры для демонстрации их применения. Более подробную информацию по данной теме можно найти в книге Client/Server Programming Java and CORBA Роберта Орфали (Robert Orfali) и Дэна Харки (Dan Harkey) [John Wiley & Sons 1998]. Более тонкие и сложные вопросы этой технологии описываются в книге Advanced CORBA Programming with C++ Мики Хеннинга (Michi Henning) и Стива Виноски (Steve Vinoski) [Addison-Wesley 1999].
Язык
Для знакомства с синтаксисом языка IDL рассмотрим еще раз пример, который использовался для описанияВинтерфейс создается
на языке Java, а в CORBA — на языке IDL так, как показано ниже.
interface Product {
string };
На этом этапе между языками IDL и Java существует несколько различий. В языке IDL определения интерфейса заканчиваются точкой с запятой. Название типа string записывается со строчной первой буквы. Кроме того, строка класса string в CORBA отличается от строки в языке Java тем, что Java-строка содержит 16-разрядные символы Unicode, a CORBA-строка — 8-разрядные символы. При передаче ORB- брокеру строки с 16-разрядными символами с ненулевыми старшими байтами неизбежно возникнет исключительная ситуация. Такую цену приходится платить за установление взаимодействия между разными языками программирования.
Для представления более "широких" символов в CORBA предусмотрено использование типови wstring. Однако, это не гарантирует использование Unicode-
символов в широких строках.
Компилятор кода IDL в код Java (Javaтранслирует IDL-
определения в определения интерфейсов на языке программирования Java. Например, для определенияProduct в файлепосле запуска
программыidl будет получен файлсо
следующим содержанием.
interface ProductOperations {
String }
Кроме того, будет получен файлс определением класса-
интерфейса.
public interface Product extends
Productoperations,
org.omg.CORBA.Object,
org.omg.CORBA.portable.IDLEntity
{ }
В SDK 1.2 для трансляции IDL-файлов используется программа idltojava.
Компилятор IDL также генерирует несколько других файлов: класс-заглушку для организации взаимодействия с ORB-брокером и три вспомогательных класса, которые более подробно рассматриваются ниже в этой главе.
Язык IDL предназначен не для программирования, а для создания интерфейсов. Поэтому описанные на языке IDL объекты CORBA должны быть реализованы на другом языкепрограммирования, например, C++ илиJava.
Правила трансляции IDL-кода в Java-код называются связыванием языка программирования Java. Правила связывания языка определены стандартами OMG-группы, а потому все создатели CORBA-продуктов должны неукоснительно использовать их для трансляции IDL-кода на другие языки программирования. Более подробные сведения о связывании IDL и Java можно найти в документации CORBA на Web-сайте OMG- группы по адресуwww. omg. org. Каждому программисту при работе с языком IDL следует иметь четкое представление об основных принципах его использования.
При определении метода в языке IDL предлагается большее количество способов передачи параметров, чем это предусмотрено в языке Java. Каждый параметр может иметь в своем объявлении дополнительную характеристику in, out или inout. Параметр с характеристикой in просто передается методу, аналогично способу передачи параметров в языке Java. Однако, в языке Java нет аналога для параметра с характеристикой out. Перед возвращением значения метод сохраняет значение каждого параметра с характеристикой out. Таким образом, объект-инициатор вызова этого метода может также извлекать значения, которые хранятся в параметрах с характеристикой out.
В приведенном ниже примере определения интерфейса метод find может хранить сведения о найденном объекте-продукте.
interface Warehouse
{
boolean locate(in String descr, out Product P) ;
) '
Если параметр имеет в своем определении только характеристику out, то этот метод не обязательно инициализировать. Но если он имеет в своем определении характеристику inout, то объект-инициатор вызова данного метода обязательно должен предоставить его значение. Причем, метод может изменить значение параметра с характеристикой inout, чтобы объект-инициатор мог извлечь его обновленное значение. В языке программирования Java эти параметры симулируются с помощью специальных классов-хранителей (holder class),которыегенерируютсякомпиляторомJavaIDL.
Компилятор IDL создает для каждого интерфейса класс с суффиксом Holder. Например, при компиляции класса Product будет автоматически создан класс ProductHolder. Каждый класс-хранитель содержит открытую переменную value.
Если метод содержит параметр с характеристикой out, то компилятор IDL изменит сигнатуру метода для использования класса-хранителя, как показано ниже. interface Warehouse {
boolean locate(String descr, ProductHolder p);
Таким образом, при вызове этого метода ему придется передать объект-хранитель. После возвращения метода из объекта-хранителя можно также извлечь значение параметра с характеристикой out, например так, как показано ниже для метода locate.
Warehouse w =
String descr = ... ;
Product р;
ProductHolder pHolder » new ProductHolder () ,* if (w.locate(descr, pHolder))
p » pHolder.value;
Для базовых типов предусмотрено использование предопределенных классов- хранителей, например IntHolder, DoubleHolder и т.д.
В языке IDL не поддерживается перегрузка методов, поэтому для разных методов всегда нужно использовать разные имена.
Для определения массивов переменного размера в языке IDL предусмотрена конструкция sequence (последовательность). Перед объявлением параметров и возвращаемых значений этой конструкции нужно определить ее тип, например так, как показано ниже в определении "последовательности продуктов".
typedef sequence<Product> ProductSeq;
Затем этот тип можно использовать в объявлении метода.
interface Warehouse
{
ProductSeq find(in Customer c) ;
};
В языке Java последовательности соответствуют массивам, поэтому метод find будет соответствовать показанному ниже методу.
Product!] find(Customer с);
Для метода, который способен вызвать исключительную ситуацию, следует определить тип исключительной ситуации и объявить ее с помощью ключевого слова raises. В приведенном ниже примере метод findможет вызвать появление исключительной ситуации BadCustomer.
interface Warehouse
{
exception BadCustomer { string reason; };
ProductSeq find(in Customer c) raises BadCUstomer;
Компилятор IDL транслирует тип исключительной ситуации в класс.
final public class badCUstomer
extends org.omg.CORBA.UserException
{
public BadCustomer() {}
publicreason) {reason = reason;}
public String reason;
}
При перехвате и обработке исключительной ситуации можно просматривать и обрабатывать его открытые переменные.
При этом ключевое слово raises транслируется в ключевое слово throws модели Java.
ProductSeqc) throws BadCustomer
Как показано ниже, интерфейсы могут содержать константы.
interface Warehouse {
const int= 404;
Кроме того, интерфейсы могут содержать атрибуты, которые имеют вид переменных, но на самом деле являются сокращенными версиями методов доступа (accessor) и методов-мутаторов (mutator). В приведенном ниже примере является атрибутом интерфейса Book.
interface Book
{
attribute string isbn;
Его эквивалентом вJava является пара методов с одинаковым именем
String isbn() // метод доступа
voidisbn) //
Если атрибут объявлен как доступный только для чтения (readonly), то метод- мутатор не генерируется.
В интерфейсах CORBA нельзя указывать переменные, потому что представление данных объектов является частью стратегии реализации объектов, а язык IDL вовсе не предназначен для реализации объектов.
Впредусмотрена поддержка наследования интерфейсов, которое обозна
чается символом двоеточия (:).
interface Book : Product { /* ... */};
Один интерфейс может наследовать сразу несколько интерфейсов.
В языке IDL можно группировать определения интерфейсов, типов, констант и исключений в модулях.
module
{
interface Product
{
interface Warehouse
};
};
Модули транслируются в пакеты языкаJava.
IDL-компилятор в результате обработки IDL-файла позволяет получить классы- заглушки и вспомогательные классы для целевого языка (например, для языка Java и C++).
Например, для преобразования IDL-файлов в код языка Java нужно запустить программу id!j с указанием имени IDL-файла в режиме командной строки.
idlj Product.idl
Эта программа создает следующие пять файлов.
Определение интерфейсаProduct .java.
Определение интерфейса Productoperations.java, которое лежит в основе интерфейса Product. j ava и некоторых других CORBA-интерфейсов.
Класс-хранительProductHolder. j ava для параметров с характеристикой out.
Класс справки ProductHelper.java, который более подробно описывается в следующем разделе.
Класс-заглушка _ProductStub.java для организации взаимодействия с ORB- брокером.
Точно также IDL-файл можно откомпилировать, чтобы получить файлы с кодом на языке C++. В данном случае эта возможность демонстрируется на примере использования бесплатно распространяемого ORB-брокера omni ORB. Пакет omniORB содержит компилятор omniidl для языка C++. Для генерации классов-заглушек на языке C++ нужно использовать приведенную ниже команду.
omniidl —bcxx Product.idl
В результате выполнения этой команды будут получены следующие файлы.
Заголовочныйфайл Product. hhсопределениями классовProduct,
Product_Helper и POA_Product (суперкласс для класса реализации объекта-сервера).
ФайлProductSK.cc, который содержит код этих классов.
Несмотря на стандартизацию связывания, каждый поставщик IDL-компилятора Dможет выбрать собственный способ генерации и упаковки кода связывания. По-
Dэтому разные IDL-компиляторы могут в итоге создавать разные наборы файлов.
Пример использования технологии CORBA
В первом примере демонстрируется совместная работа объекта-сервера на языке C++ и объекта-клиента на языке Java на основе инструментов технологии CORBA, которые предусмотрены в платформе Java 2. На стороне сервера используется ORB- брокер omniORB, который можно бесплатно скопировать по адресу http: // www.uk.research.att.com/omniORB/index.html.
Для использования ORB-брокера omniORB в операционной системе Windows потребуется компилятор Microsoft C++, а в операционной системе Linux — компилятор GNU C++.
На стороне сервера также можно использоватьсовместимый с технологией2. Однако, для использования другого ORB-брокера необходимо
Dвнести соответствующие изменения в код C++. Кроме того, как подсказывает опыт,
в таких случаях нужно учитывать некоторые особенности автоматической загрузки. В конце этого раздела приводится несколько полезных советов для организации надежного взаимодействия с другими ORB-брокерами.
В этом примерена языке C++ просто передает значение перемен
ной окружения сервера. Он обладает следующим интерфейсом.
interface Env {
string getenv(in string name);
Например, приведенный ниже фрагмент программы на языке Java получает значение переменной окружения PATH для того процесса, в котором выполняется объект-сервер.
Env env = ... ;
String value =
Код реализации этого интерфейса на языке C++ основан на вызове метода getenv из стандартной библиотеки языка С.
class
: public P0A_Env, public
{
public:
virtual char*char* name)
{
char* value « return
);
Для понимания материала в данном разделе достаточно рассматривать этот код как часть унаследованного кода, который требуется инкапсулировать в CORBA- объекте, чтобы его можно было вызывать Java-программ.
На стороне сервера нужно создать код на языке C++, который мог бы выполнять следующие действия.
Запуск ORB-брокера.
Создание объекта Envlmpl и регистрация его в ORB-брокере.
Использование сервера именования для связи объекта с конкретным именем.
Ожидание вызовов со стороны клиента.
Эта программа приведена в листинге 5.22. Более подробное обсуждение кода на языке C++ можно найти в документации для omniORB, которая содержит ясные и полные описания всех перечисленных выше этапов.
Вернемся к обсуждению кода программы-клиента. Как уже известно, для вызова удаленного метода объекта-сервера нужно получить ссылку на удаленный объект- сервер. Однако, в этом примере для получения такой ссылки нужно выполнить другие действия, чем при использовании
Вначале нужно инициализировать ORB-брокер, который представляет собой библиотеку классов, способных взаимодействовать с другими ORB-брокерами, а также свертывать и развертывать параметры.
ORB orb = ORB,init(args, null);
Затем нужно найти сервис именования, который поможет обнаружить другие объекты. В CORBA сервис именования представляет собой другой CORBA-объект. Для вызова сервиса именования нужно прежде всего найти его. Для CORBA версии 1 задача поиска сервиса именования представляла собой серьезную проблему из-за того, что не было никакого стандартного способа получения ссылки на него. Однако, в CORBA версии 2 ORB-брокер предусмотрена возможность поиска некоторых стандартных сервисов по их имени. Например, с помощью показанного ниже вызова можно получить список всех стандартных сервисов, к которым может подключиться ORB-брокер. Сервис именования имеет стандартное имя NameService. Большинство ORB-брокеров содержат дополнительные сервисы, например, RootPOA для организации доступа к корневому адаптеру Portable Object Adaptor.
Для получения ссылки на объект данного сервиса следует использовать метод resolve_initial_re erences. Он возвращает родовой CORBA-объект, экземпляр класса org.omg.corba.Obj ect. При его использовании требуется полностью указывать префикс пакета, потому что в противном случае компилятор воспримет его как объект java.lang.Obj ect.
org.omg.corba.Object object
=
Далее нужно преобразовать эту ссылку к типу NamingContext для предоставления возможности вызова методов интерфейса контекста именования NamingContext. В RMI-технологии достаточно было бы выполнить приведение типа этой ссылки к нужному типу, как показано ниже.
NamingContext namingContext
=// Ошибка!
Однако, в CORBA для этого нужно использовать метод narrow класса справки целевого интерфейса.
NamingContext namingContext
=
Приведение типа ссылки на CORBA-объект к подчиненному типу иногда приводит к успеху. Дело в том, что многие ссылки на объект типаorg*omg.CORBA . Object уже указывают на объекты, которые реализуют соответствующий интерфейс. Но ссылка на объект может делегироваться, т.е. относиться к другому объекту, который фактически реализует этот интерфейс. В общем случае нам неизвестен способ генерации объектов-заглушек, поэтому для преобразования типа ссылки на CORBA- объект к подчиненному типу всегда следует использовать метод narrow.
После получения контекста именования его можно использовать для обнаружения объектов-серверов по связанным с ними именам. Имена представляют собой вложенные последовательности компонентов-имен {name components). Вложенныеуровниможноприме- нять для организации иерархии имен аналогично структуре каталогов файловой системы.
Компонент-имя состоит из идентификатора (id) и вида (kind). Идентификатор — это уникальное имя среди всех имен с тем же родительским именем-компонентом, а вид — это указание на тип компонента. Виды не стандартизованы, например, можно использовать вид "Context" — для имен-компонентов с вложенными именами, а вид " Context" — для имен объектов.
В этом примере программа-сервер назначает для объекта Envlmpl следующее имя.
(id="corejava", kind="Context"), (id="Env", kind=”Object")
Теперь извлечение удаленного интерфейса заключается в создании массива имен- компонентов и передаче его методу resolve интерфейса NamingContext.
Namecomponent[] path =
{
new Namecomponent("corejava", "Context"),
new Namecomponent("Env", "Object")
};
org.omg.CORBA.Object envObj = namingContext.resolve(path);
Затем нужно использовать метод narrow для получения ссылки на объект.
Env env = EnvHelper.narrow(envObj);
После этого можно приступать к вызову удаленных методов.
String value = env.getenv("PATH");
Полностью описанный выше код приведен в листинге 5.21.
Он выполняет следующие действия программы-клиента.
Запуск ORB-брокера.
Поисксервисаименованиязасчетизвлеченияисходнойссылкинасервис
именования и ссылки на контекст именования NamingContext.
Поиск объекта, методы которого будут вызываться, за счет определения его имени и вызова метода resolve контекста именования NamingContext.
Определение типа возвращенного объекта к правильному типу и вызов его методов.
Для проверки работоспособности этой программы нужно выполнить перечисленные ниже действия.
Скомпилировать IDL-файлы с помощью C++ IDL-компилятора и Java IDL- компилятора.
Скомпилировать программу-сервер на языке C++. Параметры компиляции зависят от используемого ORB-брокера. Например для ORB-брокера omniORB для операционной системы Linux нужно использовать следующие параметры компиляции.
g++
—о EnvServer
D х86-D linux-D OSVERSION =2
I/usr/loac1/omni/include
L/usr/loacl/omni/lib/i586_linux_2.0_glibc2.1/EnvServer.cpp EnvSK.cc
lomniORB3 —ItcpwrapGK —lomnithread —lpthread
Для поиска нужных параметров компиляции другого ORB-брокера скомпилируйте один из примеров программы, предлагаемых в их документации, а потом измените нужным образом для компиляции ваших собственных программ.
Скомпилируйте программу-клиент на языке Java.
Запустите сервис именования на компьютере-сервере (например, при использовании ORB-брокера omniORB). Он будет работать до тех пор, пока не будет удален явным образом.
Запустите программу-сервер.
./EnvServer &
Она также будет работать до тех пор, пока не будет удалена явным образом.
Запустите программу-клиент.
EnvClient -ORBInitialPort 2809
Эта программа-клиент должна сообщить о значении параметра PATH процесса- сервера.
При использовании ORB-брокера следует правильно указать значения некоторых параметров конфигурации. Например, для параметра (ДЛЯ операционной системы или параметра PATH (ДЛЯ операционной системы Windows) нужно указать каталог, в котором находятся библиотеки ORB-брокера omniORB. Кроме того, нужно создать файл конфигурации и задать значение переменной окружения Файл конфигурации содержит (Interoperable Object Reference) сервиса именования. Внимательно прочтите замечание по поводу использования в конце этого раздела, а также в точности выполняйте все рекомендации из руководства по использованию ORB-брокера
Если программа-сервер находится на удаленном компьютере или если исходный номер порта ORB-сервера не равен значению по умолчаниюто в таком случае
нужно будет установить значения свойств ORBInitialHost и ORBInitialPort. Например, ORB-брокер omniORB использует порта потому для него потребуется
задать исходный номер порта.
Для указания этих значений можно использовать системные свойства org.omg.
ив режиме командной
строки с помощьюИли иначе, непосредственно задавая нужные значения,
как показано ниже.
EnvClient —ORBInitialHost-ORBInitialPort 2809
Параметры командной строки передаются ORB-брокеру с помощью следующего вызова.
ORB orb =null);
Как правило, поставщики ORB-брокеров тщательно описывают все особенности применения их программных продуктов. Однако, бывают случаи, когда поставщики ORB-брокеров ошибочно предполагают, что их бесценный ORB-брокер никогда не будет использоваться вместе с другимипродуктами. Поэтому в их до
кументации может отсутствовать какая-либо полезная информация о способах взаимодействия с другими ORB-брокерами. В случаях, когда программе-клиенту не удалось
найти сервис именования, попробуйте указать одинаковые исходные номера портов для программы-сервера и программы-клиента.
При возникновении проблем во время подключения к сервису именования организуйте выводнаэкранспискавсехисходныхсервисов, которыеудалосьнайтиORB-брокеру. public class ListServices {
public static void main(String args[]) throws Exception
{
ORB orb = ORB.init(args, null) ;
String[] services = orb.list_initial_services();
for (int i = 0; i < services.length; i++)
System.out.println(services[i]);
}
}
В некоторых ORB-брокерах сервис именования NameService не входит в этот список перечисленных сервисов независимо от способа настройки его работы. В таком случае нужно прибегнуть к другому способу, т.е. найти объект-сервер no IOR- ссылке(см. замечание-отступление в концераздела).
В этом разделе описывается способ подключения к объекту-серверу на языке C++. Такая технология позволяет заключать унаследованный код в оболочку CORBA- объектов и осуществлять доступ к ним из любой программы, поддерживаемой платформой Java 2 без необходимости доставки специального программного обеспечения на компьютерах-клиентах. В следующем разделе рассматривается обратный случай, когда объект-сервер создан на языкеJava, а объект-клиент на языке C++.
Листинг 5.21.EnvCllent.java
/**
©version 1.00 1999-08-21
©author Cay Horstmann
*/
import org.omg.CosNaming.*;
import org.omg.CORBA.*;
public class EnvClient
{
public static void main(String args[])
{
try
{
ORB orb = ORB.init(args, null);
org.omg.CORBA.Obj ect namingContextObj
=
NamingContext namingContext
=
NameComponent[] path =
{
new NameComponent("corejava”, "Context"), new NameComponent("Env", "Object")
};
org.omg.CORBA.Object envObj
=;
Env env = EnvHelper. narrow (envObj ) ;
System.out.printIn(env.getenv("PATH";) ; }
catch(Exception e)
{
e.printStackTrace() ;
Листинг 5.22. EnvServer.cpp
/**
aversion 1.10 2001-07-14
@author Cay Horstmann
*/
#include <iostream>
#include <cstdlib>
#include "Env.hh"
using namespace std;
class:
public P0A_Env,
public Portableserver::RefCountServantBase
{
public:
virtual char* getenv(const char *name);
};
char* Envlmpl::getenv(const char *name) {
char* value = std::getenv(name);
return CORBA::string_dup(value);
}
static void bindObjectToName(CORBA::ORB_ptr orb, const char name[], CORBA::Object_ptr objref)
{
CosNaming::NamingContext_var rootContext;
try {
// Получение ссылки на корневой
// контекст сервиса именования
CORBA::Obj ec t_var obj;
obj = orb->resolve_initial_references("NameService");
// Конкретизация ссылки
rootContext = CosNaming::NamingContext::_narrow(obj); if( CORBA::is_nil(rootContext) )
{
cerr « "Failed to narrow the root naming context. « endl;
return;
)
}
catch(CORBA::ORB::InvalidNameS ex) {
// Этого нельзя допустить ни в коем случае!
cerr « "Service required is invalid [does not exist].
<< endl;
return;
}
try
{
// Связывание контекста "test" с корневым контекстом
CosNaming::Name contextName;
contextName.length(1) ;
contextName[0].id = (const char*) "corejava";
contextName[0].kind = (const char*) "Context";
CosNaming::NamingContext_var corejavaContext;
try
{
// Связывание этого контекста с корневым corejavaContext
=
}
catch(CosNaming::NamingContext::AlreadyBound& ex) {
// Если контекст уже существует, возникнет эта
// исключительная ситуация. В этом случае нужно разрешить
// имя и присвоить контекст возвращенному объекту
CORBA::Object_var obj;
obj = rootContext->resolve(contextName);
corej avaContext =
if ( CORBA::is_nil(corejavaContext) ) {
cerr << "Failed to narrow naming context." « endl; return;
}
}
// Связывание ссылки на объект с заданным именем контекста CosNaming::Name objectName;
obj ectName.length(1);
objectName[0].id= name;
objectName[0].kind = (const char*) "Object";
try
{
corejavaContext->bind(objectName, objref);
}
catch(CosNaming::NamingContext::AlreadyBoundS ex)
{
corejavaContext->rebind(objectName, objref);
}
}
catch(CORBA::COMM_FAILURE& ex)
{
cerr
<< "Caught system exception COMM_FAILURE — unable to « "contact the naming service." « endl;
}
catch(CORBA::SystemExceptionS)
{
cerr « "Caught a CORBA::SystemException while using the « "naming service." « endl;
int main(int argc, char *argv[]) {
cout « "Creating and initializing the ORB..." « endl;
1) ;
CORBA::ORB_var orb = CORBA::ORB_init(argc, argv, "omniORB3"
CORBA::Object_var obj
=
Portableserver::POA_var poa =
poa->the_POAManager()->activate();
Envlmpl* envlmpl = new Envlmpl();
poa->activate_object(envlmpl);
// Получение ссылки на объект и регистрация
// его в сервисе именования
obj = envlmpl->_this();
cout << orb->object_to_string(obj) << endl;
cout « "Binding server implementations to registry..."
<< endl;
bindObjectToName(orb, "Env", obj) ;
envlmpl->_remove_ref() ;
cout « "Waiting for invocations from clients..." << endl; orb->run();
return 0;
Поиск объектов с помощью IOR-ссылок
Если не удается конфигурировать ORB-сервер и имя сервиса так, чтобы клиент мог его вызвать, то можно воспользоваться IOR-ссылками (Interoperable Object Reference). IOR-ссылка— это длинная строка с префиксом IOR:и несколькими шестнадцатеричными цифрами.
IOR:012020201000000049444c3a4163636f756e743a312e3000010000000 00000004e000000010100200f0000003232362e31352e3131322e31373900 20350420202e00000001504d43000000001000000049444c3a4163636f756 е743а312е30000е0000004а6163бь20422е20517569б3бь00
IOR-ссылка уникальным образом описывает объект. По договоренности многие классы- серверы способны выводить IOR-ссылки для всех зарегистрированных ими объектов, чтобы объекты-клиенты могли найти их. Нужную IOR-ссылку можно затем использовать в программе-клиенте, напримертак, какпоказанониже.
String ref = "IOR:012020201000000049444c3a4163636f...";
// вставка IOR-ссылки
org.omg.CORBA.Object object = orb.string_to_object(ref) ;
Затем нужно конкретизировать тип возвращаемого объекта.
Env env = EnvHelper.narrow(object);'
Это можно сделать несколько иным способом.
NamingContext context = NamingContextHelper.narrow(object);
При тестировании кода из этой книги данный метод был успешно использован для соединения объектов-клиентов с ORB-брокерами visibroker и omniORB. (Учтите, что в некоторых версиях JDK имеются ошибки, которые не позволяют организовать взаимодействие с сервисом именования omni ORB.)
org.omg.CORBA. ORB
Метод static ORB init(String[] args, Properties props) создает новый ORB-объект и инициализирует его.
Параметры:
argsаргументы командной строки для конфигурирования ORB-
брокера
propsтаблица со свойствами конфигурирования ORB-брокера
Метод String [] list_initial_services () возвращает список доступных сервисов, например, "NameService".
Метод org.omg.COREA.Object resolve_initial_references(String name) возвращает объект, который выполняет один из сервисов.
Параметры:
nameимя сервиса
Методorg.omg.CORBA.Object string_to_object(String ior) находит объект с заданной IOR-ссылкой.
org. omg. CosNaming. NamingContext
Метод org.omg.CORBA.Object resolve(Namecomponent[]name)
возвращает объект, который связан с указанным именем.
org .omg. CosNaming. NameComponent
Конструктор NameComponent (String id, String kind) создает компонент-имя.
Параметры:
idстрока с описанием идентификатора компонента
kindстрока с описанием типа компонента
Реализация CORBA-сервисов
Язык Java очень удобно использовать для создания инфраструктуры CORBA. Дело в том, что связывание в языке Java выполняется более естественно, а потому с его по-
мощью легче создавать объекты-сервер, чем на основе языка C++. В этом разделе описываются способы создания CORBA-сервера на языке Java.
Рассматриваемый здесь пример аналогичен примеру из предыдущего раздела. Создадим сервис для просмотра системного свойства виртуальной машины Java. Вот как выглядит описание IDL-интерфейса.
interfaceSysProp
{
string getProperty(in string name);
};
Например, тестовая программа-клиент может вызывать объект-сервер так, как показано ниже.
CORBA::String_var key = "java.vendor";
CORBA::String_var value = sysProp->getProperty(key);
В результате будет получена строка с описанием поставщика виртуальной машины Java, которая выполняет программу-сервер. Подробности программы-клиента здесь не рассматриваются, но их можно найти в листинге 5.24.
Для создания объекта-сервера нужно запустить компилятор idl j с параметром - fall. (По умолчанию компилятор idlj создает только клиентские заглушки.)
idlj —fall SysProp.java
Затем нужно расширить класс SysPropPOA, который компилятор idlj создает на основе IDL-файла.
class SysPropImpl extends SysPropPOA {
public String getProperty(String key) {
return System.getProperty(key);
)
}
Для класса реализации можно выбрать любое имя. В этой книге используется RMI- соглашение об именах, т.е. классы реализации содержат суффикс Impl. Другие программисты часто предпочитают использовать суффиксы Servant или _±.
Если класс реализации уже наследует какой-то класс, то он не может одновременно наследовать базовый класс реализации. В этом случае можно указать компилятору idljо необходимости создания связующего класса. Тогда класс-сервер реализует интерфейс операций, а не наследует базовый класс реализации. Теперь с помощью связующего класса могут быть созданы любые объекты-серверы. Более подробное описание принципов работы компилятора idlj можно найти в руководстве по использованию языка IDLПОадресу ht tp://java.sun.com/j 2 se/1.4/docs/guide/idl/index.html.
Затем нужно создать программу-сервер, которая выполняет следующие действия.
Запуск ORB-брокера.
Поиск и активизация корневого адаптера переносимых объектов (РОА- адаптера).
Создание кода для объекта-сервера.
Использование РОА-адаптера для преобразования служебных указателей в CORBA-указатели объектов. (Класс реализации объекта-сервера расширяет объект SysPropPOA, который, в свою очередь, расширяет класс org.omg.Portableserver.Servant.)
Вывод IOR-ссылки (см. замечание в конце предыдущего раздела).
Связывание кода сервера с сервисом именования.
Ожидание вызовов со стороны объектов-клиентов.
Полный код программы-сервера приведен в листинге 5.23.
Он начинается с вызова ORB-брокера.
ORB orb = ORB.init(args, null);
POA rootpoa =
rootpoa.the_POAManager().activate() ;
Затем создается объект-сервер, который преобразуется в CORBA-объект.
SysPropImpl impl = new SysPropImpl();
org.omg.CORBA.Object ref = rootpoa.servant_to_reference(impl);
Далее получаем и выводим IOR-ссылку с помощью метода object_to_string.
System.out.println(orb.object_to_string(ref));
Получение ссылки на сервис именования происходит таким же способом, что и при создании кода объекта-клиента.
org.omg.CORBA.Object namingContextObj =
orb.resolve_initial_references("NameService");
NamingContext namingContext =
Затем создается нужное имя объекта, т.е. SysProp в этом примере.
NameComponent[] path =
{
new NameComponent("SysProp”, "Object")
Здесь не используются вложенные имена, потому что во время создания данной книги сервис именования orbd в составе Java SDK не поддерживал вложенные имена.
Далее метод rebind используется для связывания объекта с его именем. namingContext.rebind(path, impl);
Наконец, переходим в состояние ожидания вызовов со стороны объектов- клиентов.
orb.run() ;
Для проверки работоспособности этой программы нужно выполнить следующие действия.
Скомпилировать IDL-файлы с помощью C++ IDL-компилятора и Java IDL- компилятора.
Скомпилировать программу-сервер.
Скомпилировать программу-клиент на языке C++.
Запустить на этом сервере сервис именования orbd, который является частью Java SDK.
orbd -ORBInitialPort 2809
Запустить программу-сервер.
java SysPropServer -ORBInitialPort 2809
Она будет работать до тех пор, пока не будет удалена явным образом.
Запустите программу-клиент.
./SysPropClient &
Результатом успешной работы программы будет вывод на экран описания поставщика виртуальной машины Java объекта-сервера.
В этом примере используется переходный сервис именования. Инструмент orbd также включает постоянный сервис именования, который запоминает имена и ссылки на объекты, если сервис именования прекратил работу и запущен снова. Более подробная информация по этому поводу приводится в документации Java SDK.
Итак, мы рассмотрели способ использования технологии CORBA для соединения объектов-клиентов и объектов-серверов, которые были созданы на разных языках программирования. На этом заканчивается обсуждение технологии CORBA, которая также содержит множество других интересных компонентов. Например, динамический вызов методов, обработка транзакций и многое другое. Более подробно обсуждение этой темы можно найти в книге Client/Server Programming with Java and CORBA Роберта Орфали (Robert Orfali) и Дэна Харкли (Dan Harkey) [John Wiley & Sons 1998].
Листинг 5.23.SysPropServer.java
/**
@version 1.10 2001-07-15
@author Cay Horstmann
*/
import org.omg.CosNaming.*;
import org.omg.CORBA.* ;
import org.omg.Portableserver.*;
class SysPropImpl extends SysPropPOA {
public String getProperty(String key)
return System.getProperty(key) ;return System.getProperty(key) ;{
return System.getProperty(key) ;
return System.getProperty(key) ;
}
}
public class SysPropServer £
public static void
try
System.out.printIn(
"Creating and initializing the ORB...");
ORB orb =null);
System.out.printin(
"Registering server implementation with the ORB..."); POA rootpoa
=
rootpoa.the_POAManager().activate();
SysPropImpl= new
ref
=
System.out.println(orb.object_to_string(ref));
namingContextObj =
orb.resolve_initial_references("NameService"); namingContext
=
path =
{
// new"Context"),
new"Object")
};
System.out.printin(
"Binding server implementation to name service...");
namingContext.rebind(path, ret);
System.out.printin(
"Waiting for invocations from orb.run();
}
catch (Exception e)
{
e.printStackTrace(System.out);
Листинг
/**
©version 1.10 2001-07-15
©author Cay Horstmann
*/
#include <iostream>
#include "SysProp.hh"
CORBA::Object_ptr getObjectReference(CORBA::ORB_ptr orb, const char serviceName[])
{
CosNaming::NamingContext_var rootContext;
try {
// Получение ссылки на корневой
// контекст сервиса именования
CORBA::Object_var initServ;
initServ = orb->resolve_initial_references("NameService") ;
// Конкретизация объекта с помощью метода
// resolve_initial_references()
// с указанием объекта CosNaming::NamingContext
rootContext = CosNaming::NamingContext::_narrow(initServ); if (CORBA::is_nil(rootcontext))
{
cerr << "Failed to narrow naming context." « endl;
return CORBA::Object::_nil();
}
}
catch(CORBA::ORB::InvalidName&)
{
cerr << "Name service does not exist." << endl;
return CORBA::Object::_nil() ;
// Создание объекта, содержащего имя corejava/SysProp CosNaming::Name name;
name.length(1);
name[ 0 ].id = serviceName;
name[0].kind = "Object";
CORBA::Object_ptr obj ;
try {
// Поиск имени no ссылке объекта и присвоение
// возвращенной ссылки для объекта CORBA::Object
obj = rootContext->resolve(name);
)
catch(CosNaming::NamingContext::NotFound&) {
// Эта исключительная ситуация возникает тогда и
// только тогда, когда не найден какой-либо из
// компонентов пути [для контекста или объекта]
cerr << "Context not found." << endl;
return CORBA::Object::_nil();
)
return obj ;
}
int main (int argc, char *argv[]) {
CORBA::ORB_ptr orb=CORBA::ORB_init(argc, argv, "omniORB3");
CORBA::Object_varobj=getObjectReference(orb, "SysProp");
SysPropvar sysProp = SysProp::„narrow(obj);
if (CORBA::is_nil(sysProp))
cerr << "hello: cannot invoke on a nil object reference." « endl;
return1;
}
CORBA::String_var key = "java.vendor";
CORBA::Stringvar value = sysProp->getProperty(key);
cerr « key << "=" << value « endl;
> return0;
B
org.omg.CORBA.ORB
Метод void connect (org. omg. CORBA. Object obj) соединяет заданный ORB-объект с его ORB-брокером, позволяя ORB-брокеру передавать вызовы методов объекта.
Метод String object_to_string(org.omg.CORBA.Object obj) возвращает строку с IOR-ссылкой заданного объекта.
org. omg. CosNaming. NamingContext
Методы bind(NameComponent[] name, org.omg.CORBA.Object obj ) и rebind (NameComponent [ ] name, org.omg.CORBA.Object obj ) связывают указанный объект с указанным именем. При этом метод bind вызывает исключительную ситуацию, если заданный объект уже связан с каким-то именем. А метод rebind заменяет любую предварительно заданную связь.
Параметры:
nameимя, с которым связывается объект
objобъект, в котором хранится контекст именования
Более сложные компоненты пакета Swing
в этой главе...
т Списки
т Деревья
т Таблицы
т Стилизованные текстовые компоненты т Организаторы компонентов
этой главе продолжается начатое в томе 1 обсуждение набора компонентов Swing, которые предназначены для создания пользовательского интерфейса. В томе 1 представлены только базовые и наиболее распространенные компоненты, хотя пакет Swing содержит много других гораздо более сложных компонентов. В этой главе рассматриваются три наиболее сложных компонента Swing: списки, деревья и таблицы. Кроме того, здесь описываются более сложные способы создания текстов с использованием специальных стилей, например, текста в HTML-формате. В конце главы рассматриваются организаторы компонентов в виде панелей с вкладками и панелей рабочего стола с внутренними фреймами.
Списки
Для предоставления пользователю возможности выбора среди нескольких значений параметров можно вместо групп кнопок или флажков (которые занимают довольно много места) использовать поля со списками или списки. Поля со списками (combo boxes) имеют простую структуру и подробно рассматриваются в томе 1. Ком-
понент-список JList имеет более сложную структуру, причем некоторые элементы списка подобны отдельным элементам дерева и таблицы. Именно по этой причине в данной главе сначала рассматриваются списки.
Списки могут состоять не только из строк, но и из любых других произвольных объектов с дополнительными элементами управления. Компонент JListимеет очень элегантную внутреннюю архитектуру, благодаря которой и возможна такая универсальность. К сожалению, разработчики компании Sun уделили чрезмерно большое внимание именно этой элегантности в ущерб простоте использования этого компонента. В некоторых простых ситуациях этот компонент не очень удобен в употреблении, потому что приходится использовать универсальные механизмы, которые применимы на все случаи жизни. Ниже рассматривается простой и очень распространенный пример организации списка строк, а затем приводится более сложный пример создания списка, который демонстрирует гибкость компонентаJList.
Компонент JList
Структура компонента JList аналогична группе флажков или переключателей за исключением того, что все элементы компонента JList располагаются внутри одной области и выбираются щелчком на элементах, а не на кнопках. Если в таком списке допускается выбор сразу нескольких элементов, то их можно выбирать в любых сочетаниях.
На рис. 6.1 показан очень простой пример списка, в котором пользователь может подобрать характеристики для лисы: quick (быстрая), brown (коричневая), hungry (голодная), wild (дикая), а также static, private и final (тут автор исчерпал известные ему характеристики животных и предложил первые пришедшие на ум слова). Эти характеристики будут использованы в предложении о лисе, которая перепрыгивает через ленивую собаку. Например, при выборе в списке характеристик static и final получится предложение The static final fox jumps over the lazy- dog, которое будет показано в окне программы.
Для создания такого компонента нужно начать с создания массива строк, а затем передать его конструктору компонента JList.
String[] words = { "quick", "brown", "hungry", "wild", . . .};
JList wordList = new JList(words);
Иначе, для этого можно использовать безымянный массив.
JList wordList = new JList(new String[]
{ "quick", "brown", "hungry", "wild", ...});
В списках не предусмотрены средства автоматической прокрутки, поэтому для организации прокрутки элементов списка нужно вставить этот список в панель с полосой прокрутки JScrollPane.
JScrollPane scrollPane = new JScrollPane(wordList);
А затем нужно вставить эту панель (а не список) в окружающую ее панель.
Следует признать, что разделение функций отображения списка и механизма прокрутки удобно с теоретической, но не с практической точки зрения. Практически в любом списке используется функция прокрутки содержимого. Поэтому совершенно излишне заставлять программистов программировать те функции, которые могут
быть включены по умолчанию, только для того, чтобы они могли оценить универсализм и изящество архитектуры компонента JList.
ГМ private italic final fox Jumps over the гЖ dog.
hunqn ■—
huge Я private ifj absti j
"final ,
Рис. 6. 1. Пример простого списка
По умолчанию в окне списка отображаются восемь элементов. Но это количество можно изменить с помощью метода setVisibleRowCount, например так, как показано ниже.
wordList.setVisibleRowCount(10) ; // Отобразить 10 элементов.
По умолчанию пользователь может выбрать сразу несколько элементов. Например, для выбора нескольких несмежных элементов нужно щелкать кнопкой мыши на всех необходимых элементах, удерживая при этом нажатой клавишу <Ctrl>. Для выбора нескольких смежных элементов нужно щелкнуть мышью сначала по первому элементу, а потом, удерживая нажатой клавишу <Shift>, щелкнуть мышью по последнему элементу.
Для ограничения вариантов выбора предусмотрен метод setSelectionMode.
wordList.setSelectionMode
(ListSelectionModel.SINGLE_SELECTION);
// разрешается выбирать не более одного элемента
wordList.setSelectionMode
(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
// разрешается выбирать не более одного элемента
// или набора смежных элементов
Как уже говорилось в томе 1, базовые компоненты пользовательского интерфейса посылают сообщения о событиях, связанных с действиями пользователя. В списках используется другой механизм на основе прослушивания и перехвата сообщений о событиях выбора элементов, а не прослушивания и перехвата сообщений обо всех событиях, связанных с действиями пользователя. Для его использования нужно создать в компоненте следующий метод-слушатель.
public void valueChanged(ListSelectionEvent evt);
В процессе выбора пользователем элементов списка генерируется несколько событий. Допустим, что пользователь щелкнул на новом элементе списка, а потом переместил мышь ниже, в таком случае будет генерировано событие временного изменения выбора. Это значит, что метод event.isAdjusting() возвратит значение true, если выбор не был окончательным. Затем, при перемещении мыши вверх генериру-
ется еще одно событие отмены временного изменения выбора, при котором метод event. isAdjusting () возвратит значение false. Если обработка временных событий не нужна, то можно отслеживать только те события, для которых метод event.i sAdjusting() возвращает значение false. Однако, для учета всех манипуляций мышью потребуется обрабатывать все события.
После уведомления о каком-то событии потребуется определить все выбранные элементы списка с помощью метода getSelectedValues, который возвращает массив объектов совсемивыбраннымиэлементамисписка.
Далее нужно будет для всех элементов списка выполнить приведение к типу строки String.
Object [ ] values list.getSelectedValues();
for (int i = 0; i < values.length; i++)
какие-то действия с элементами массива (String)values[i];
Нельзя выполнять приведение к типу String[ ] для возвращаемого значения типа Object[] метода getSelectedValues. Дело в том, что возвращаемое значение метода getSelectedValues представляет собой массив объектов, а не строк. Поэтому приведение типов придется выполнить отдельно для каждого элемента. Для обработки возвращаемого значения в виде массива строк можно использовать приведенный ниже код.
int length = values.length;
String[] words = new String[length);
System.arrayCopy(values, 0, words, 0, length);
Если в списке не допускается выбор сразу нескольких элементов, то в таком случае следует использовать метод getSelectedValue, который возвращает первый (и единственный в случае запрета выбора нескольких элементов) выбранный элемент списка.
String selection = (String)source.getSelectedValue();
В компонентеJList не предусмотрена обработка двойного щелчка кнопкой мыши. Разработчики библиотеки Swing предполагали, что список используется только для выбора элементов, а для выполнения какого-либо действия следует нажать соответствующую кнопку. Однако, в некоторых реализациях пользовательского интерфейса двойной щелчок кнопкой мыши рассматривается одновременно как выбор элемента и одобрение какого-то действия. Такой стиль работы не рекомендуется использовать, потому что пользователи могут и не подозревать, какое именно действие кроется за двойным щелчком кнопкой мыши. Однако, если вам все же необходимо реализовать эту функцию, то в классе-слушателе событий мыши нужно организовать перехват такого события.
public void mouseClicked(MouseEvent evt)
{
if (evt.getClickCount() == 2)
{
JList source = (JList)evt.getSource();
Object[] selection = source.getSelectedValues(); doAction(selection) ;
}
В листинге 6.1 приводится полный код программы со списком строк. Обратите внимание на метод valueChanged, который создает строку сообщения на основе выбранных из списка элементов.
Листинг 6.1. LiatTeat.java
/**
Sversion 1.21 2001-07-17
eauthor Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import javax.swing.* ;
import javax.swing.event. *;
/**
Пример списка с фиксированным набором строк
*/
public class ListTest
{
public static void main(String[] args)
{
JFrame frame = new ListFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show() ;
}
/**
Этот фрейм содержит список слов и надпись-предложение, составленная из выбранных слов. Учтите, что можно выбрать сразу несколько слов с помощью клавиш <Ctrl>, <Shift> и мыши, как описано выше
*/
class ListFrame extends JFrame
{
public ListFrame()
{
setTitle("ListTest");
setsize(WIDTH,HEIGHT);
String!] words = { "quick","brown","hungry","wild","silent", "huge","private","abstract","static","final”
};
wordList = new JList(words);
JScrollPane scrollPane = new JScrollPane(wordList);
JPanel pnew JPanel();
p.add(scrollPane);
wordList.addListSelectionListener(new ListSelectionListener()
{
public void valueChanged(ListSelectionEvent event)
Object[] values = wordList.getSelectedValues();
StringBuffer text = new StringBuffer(prefix); for (int i = 0; i < values.length; i++) {
String word = (String)values[i];
text.append(word) ;
text.append("") ;
}
text.append(suffix) ;
label.setText(text.toString()); }
}) ;
Container contentPane = getContentPane();
contentPane.add(p, BorderLayout.SOUTH) ;
label = new JLabel(prefix + suffix);
contentPane.add(label, BorderLayout.CENTER) ;
}
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
private JList wordList;
private JLabel label;
private String prefix = "The ";
private String suffix = "fox jumps over the lazy dog.";
}
д,i”Y|' javax. swing. Jbist
I। L • Конструктор JList (Object [] items) создает список, в котором будут ■■■■■(отображены элементы указанного массива.
Метод void setVisibleRowCount (int с) задает количество строк списка, которые будут видны без использования полосы прокрутки.
Метод void setSelectionMode (int mode) определяет допустимый режим выбора элементов (например, только один или сразу несколько).
Параметры:
modeодно из следующих значений: SINGLE_SELECTION (только
один элемент), SINGLE_INTERVAL_SELECTION (только один интервал элементов), MULTIPLE_INTERVAL_SELECTION (несколько интервалов элементов)
Метод void addListSelectionListener(ListSelectionListener
listener) вставляет в список класс-слушатель, который уведомляет о каждом изменении выбора.
Метод Object [] getSelectedValues () возвращает выбранные значе
ния или пустой массив, если выбор оказался пустым.
Метод Object getSelectedValue () возвращает первое выбранное
значение или значение null, если выбор оказался пустым.
javax.swing.event.ListSelectionListener
e) вызывается приe) вызывается приМетод void
e) вызывается при
e) вызывается при
каждом изменении выбора элементов списка.
Модели списков
В предыдущем разделе показан наиболее распространенный способ использования списка.
Указание фиксированного набора строк для отображения в списке.
Создание полосы прокрутки.
Слежение за событиями выборки элементов списка.
В остальной части этого раздела рассматриваются более сложные способы использования
Очень длинные списки.
Списки с переменным содержимым.
Списки, которые содержат нестроковые элементы.
В первом примере создается компонент JList с фиксированным набором строк. Однако, набор элементов списка не всегда является фиксированным. Как можно было бы организовать вставку и удаление элементов списка? Удивительно, но таких методов в классе JList не существует. Рассмотрим более подробно внутреннюю структуру списка. Также, как и в текстовом компоненте, в списке используется структура "модель-просмотр-управление" для разделения визуального представления (т.е. отображение столбцов с элементами) и данных (т.е. набора объектов).
Класс JList отвечает за визуальное представление данных и ему ничего неизвестно о способе хранения данных. Он лишь извлекает данные с помощью объекта, реализующего интерфейс
public interface ListModel
{
public int
public Objecti) ;
public void1) ;
public void1);
}
С помощью этого интерфейса компонент JList может получать значения счетчика элементов и извлекать нужный элемент. Кроме того, объект JList может выступать в качестве слушателя данных списка (list data listener). В таком случае он может получать сообщения об изменении состава списка и способен обновлять визуальное представление списка.
Зачем используется такая обобщенная структура? Почему бы просто не хранить объект JList в виде вектора объектов?
Обратите внимание на то, что в этом интерфейсе не указывается способ хранения данных. Более того, он даже не требует сохранять данные! Метод getElementAt можно переопределить нужным образом для вычисления требуемого значения (как показано ниже в следующем примере). Это особенно полезно для представления очень большого набора данных без необходимости сохранения значений.
На рис. 6.2 показан еще один упрощенный пример списка с перечнем всех возмож-
Рис. 6.2. Пример выбора значения из длинного списка
Всего существует 26*26*26=17576 трехбуквенных комбинаций. Вместо хранения всех этих значений было бы разумнее вычислять их по мере необходимости во время прокрутки списка.
Этот способ работы можно довольно просто организовать. Код рутинных операций вставки и удаления слушателей автоматически подготовлен в классе AbstractListModel, который лежит в основе используемого нами класса WordListModel. Остается только создать код методов getSizeи get ElementAt. class WordListModel extends AbstractListModel {
public WordListModel(int n) { length = n; }
public int getSize() { return (int)Math.pow(26, length); } public Object getElementAt(int n)
{
// вычисление п-ой строки
}
)
Технические подробности вычисления м-ой строки приводятся в листинге 6.2.
Теперь, имея такую модель, можно создать список с возможностью прокрутки вычисляемых элементов.
JList wordList = new JList(new WordListModel(3));
wordList.setSelectionMode(ListSelectionModel.SINGLE-SELECTION); JScrollPane scrollPane = new JScrollPane(wordList);
Основная идея заключается в том, что строки не сохраняются, а вычисляются. Причем генерируются только те строки, которые нужны в данный момент пользователю.
Кроме этого, для создания списка необходимо установить для всех элементов списка фиксированную высоту и ширину.
В противном случае высота и ширина будут вычисляться отдельно для каждого элемента, а на это потребуется очень много времени.
С практической точки зрения такие длинные списки используются очень редко, потому что пользователям очень сложно искать и выбирать в них нужные элементы. Видимо, по этой причине управление списком было полностью переработано. Выбранные пользователем элементы образуют достаточно малую часть данных, которые могут быть сохранены непосредственно в компоненте JList. Такой порядок работы позволяет программистам не рассматривать модель списка как отдельную сущность. С другой стороны, компонент JList совместим с классами JTable и JTree, которые имеют аналогичную обобщенную структуру.
Листинг 6.2. LongListTest.java
/**
Sversion 1.21 2001-07-17
Sauthor Cay Horstmann
*/
import java.awt.*;
import j ava.awt.event.*;
import j avax.swing.* ;
import javax.swing.event.*;
/**
Пример списка с динамическим вычислением элементов списка */ public class LongListTest {
public static void main (String|] args) {
JFrame frame = new LongListFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
} }
/**
Этот фрейм содержит список длинных слов и надпись, в которой показано предложение, включающее выбранные слова */ class LongListFrame extends JFrame {
public LongListFrame()
{
setTitle("LongListTest");
setsize(WIDTH, HEIGHT);
wordList = new JList(new WordListModel(3) ) ;
wordList.setSelectionMode
(ListSelectionModel.SINGLE_SELECTION);
wordList.setFixedCellWidth(50) ;
wordList.setFixedCellHeight(15);
JScrollPane scrollPane = new JScrollPane(wordList) ;
JPanel p = new JPanel();
p.add(scrollPane) ;
wordList.addListSelectionListener(new ListSelectionListener()
{
public void valueChanged(ListSelectionEvent evt) {
StringBuffer word =
setSubject(word.toString()); }
));
Container contentPane = getContentPane();
contentPane.add(p, BorderLayout.SOUTH);
label = new JLabel (prefix + suffix) ;
contentpane.add(label, BorderLayout.CENTER);
setSubject("fox");
)
/**
Поместить подлежащее в надпись
©param word the new subject that jumps over the lazy dog */
public void setSubject(String word)
{
StringBuffer text = new StringBuffer(prefix);
text.append(word) ;
text.append(suffix) ;
label.setText(text.toString() ) ;
}
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
private JList wordList;
private JLabel label;
private String prefix = "The quick brown ";
private String suffix = " jumps over the lazy dog.";
/**
Модель, которая динамически генерирует п-буквенные слова */ class WordListModel extends AbstractListModel {
Создать модель
©param n the word length
*/
public WordListModel(int n) { length = n; }
public int getsize()
{
return (int)Math.pow(LAST - FIRST + 1, length) }
StringBuffer r = new StringBuffer 0 ;;
for (int i = 0 ,• i < length; i++)
{
char с = (char)(FIRST + n % (LAST - FIRST + 1) ); r.insert(0,c);
n = n / (LAST - FIRST + 1) ;
}
return r;
}
private int length;
public static final char FIRST = 'a'; public static final char LAST = ' z ' ;
}
javax.swing.JLi st
КонструкторJList(ListModel dataModel) создает список, в котором отображаются элементы согласно указанной модели.
Метод void setFixedCellWidth(int width) указывает ширину каждой ячейки в списке, если width больше 0. По умолчанию width имеет значение - 1, которое означает принудительное измерение размера каждой ячейки.
javax.swing.ListModel
Метод int getSize() возвращает количество элементов в указанной модели.
Метод Object getElementAt (int index) возвращает элемент указанной модели с заданным индексом.
Вставка и удаление значений
Нельзя непосредственно редактировать набор значений списка. Для этого нужно осуществить доступ к модели и только после этого выполнять операции удаления и добавления элементов. Следует отметить, что это не такая простая задача, как может показаться на первый взгляд. Итак, сначала нужно получить ссылку на модель списка.
ListModel model = list.getModel();
Пока это ничего не дает программисту, потому что, как показано выше в предыдущем разделе, интерфейс ListModel не содержит методов добавления и удаления элементов. Основная задача модели заключается в том, чтобы избежать необходимости хранения элементов.
Попробуем поступить иначе и использовать в конструкторе списка вектор объектов.
Vector values = new Vector();
values.addElement("quick");
values.addElement("brown");
JList list = new JList(values);
Теперь можно редактировать вектор, а также добавлять и удалять его элементы. Но в таком случае список ничего не будет знать о таких изменениях, поэтому он никак
не будет реагировать на эти изменения. В частности, список не будет обновляться при добавлении новых элементов.
В конструкторе JList не допускается использование параметра типа ArrayList. Однако из-за бесполезности создания списка на основе вектора это ограничение не имеет большого значения.
Вместо этого нужно создать модель типа DefaultListModel, указать для нее исходные значения и связать их со списком.
DefaultListModel model = new DefaultListModel();
model.addElement("quick");
model.addElement("brown");
JList list = new JList(model);
Теперь в списке можно добавлять и удалять значения из объекта model, который уведомляет список о происходящих изменениях, а список в свою очередь перерисовывается соответствующим образом.
model.removeElement("quick”);
model.addElement("slow");
Как видите, в классе DeafultListModel используются имена методов, отличные от методов класса-коллекции.
Для внутреннего хранения значений в принятой по умолчанию модели типа DeafultListModelиспользуется вектор. Он наследует механизм уведомления списка от класса AbstractListModel также, как в примере из предыдущего раздела.
Некоторые конструкторы спискаJList создаются на основе массива/вектора объектов или строк. В таком случае можно было бы подумать, что эти конструкторы используют класс DeafultListModelдля хранения этих значений. Однако это нет так, конструкторы создают тривиальную модель, в которой можно получить доступ к значениям без каких-либо инструментов уведомления при изменении содержания. Ниже приводится код конструктора для создания спискаJList на основе классаVector. ’
public JList(final Vector listData) {
this (new AbstractListModel()
(
public int getSize() { return listData.size(); }
public Object getElementAt(int i)
{ return listData.elementAt(i); }
}) ;
)
Это значит, что при изменении содержимого вектора после создания списка этот список будет содержать запутанную смесь старых и новых значений до тех пор, пока он не будет полностью перерисован. (Ключевое слово final в приведенном выше конструкторе не накладывает запрет на изменение элементов списка, а только запрещает изменять значение ссылки listData в этом конструкторе. Это ключевое слово необходимо, потому что объект listData используется во внутреннем классе.)
java»:. swing.JList
МетодListModel getModel () получает модель данного списка.
javax.swing.DefaultListModel
Метод void addE lenient (Object obj ) вставляет объект в конце модели.
Метод void removeE1ement (Object obj) удаляет первый экземпляр объекта из модели и возвращает значение true, если такой объект был найден, либо значение false— в противном случае.
Перерисовка значений
Все приведенные до сих пор списки содержали только строки. Но также просто можно создать список пиктограмм, просто передавая массив или вектор объектов типа Icon. Более того, эти значения можно представить на экране любым нужным вам способом.
Хотя список JList может автоматически отображать строки и пиктограммы, для применения другого способа отображения объектов списка JList нужно использовать классфисовалъщик ячеек списка (list cell Tenderer). Этот класс основан на интерфейсе ListCellRenderer.
interface ListCellRenderer
{
Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus);
}
После установки класса-рисовальщика ячеек списка он будет вызываться для каждого значения списка: во-первых, для измерения размера графического представления значения, если для ячеек не выбраны фиксированные размеры, а во-вторых, для рисования объекта. Для этого необходимо создать класс. который возвращает объект типа Component, а также содержит метод указания размера getPre erredSize и все другие необходимые методы рисования возвращаемого объекта для соответствующего отображения элементов списка.
Проще всего для реализации этого способа можно создать внутренний класс со следующими двумя методами.
class MyCellRenderer implements ListCellRenderer {
public Component getListCellRendererComponent
(final JList list, final Object value,
final int index, final boolean isSelected, final boolean cellHasFocus)
{
return new
JPanel()
{
public void paintComponent(Graphics g)
{
// Код рисования
}
public Dimension getPreferredSize()
// Код измерения размеров
}
)
В листинге 6.3 приведен пример программы выбора шрифта для отображения строки из списка шрифтов с графическим представлением каждого шрифта, как показано на рис. 6.3. Для отображения названия каждого шрифта именно этим шрифтом используется метод paintComponent. При этом учитываются цвета фона и текста для выбранных элементов списка с помощью методов getForeground/getBackground и getSelectionForeground/getSelectionBackground класса JList. В методе getPrefferedSize используется способ измерения размера строки, который подробно описывается в главе 7 тома 1.
The quick brown fox jumps over
the lazy dog
serif sansserif.V
monospaced dialog
Рис. 6.3. Пример списка с отображением разных шрифтов
Для установки класса-рисовальщика нужно просто вызвать метод setCellRenderer.
fontList.setCellRenrderer(new FontCellRenderer);
Теперь все элементы списка могут рисоваться с помощью пользовательского класса-рисовальщика.
Существует более простой способ рисования пользовательских классов- рисовальщиков, который можно применять в большинстве случаев. Если рисуемое ИЗОбражение содержит текст, пиктограмму, изменяемые цвета, то все эти эффекты можно получит с помощью класса надписи JLabel. Например для отображения названия шрифта с помощью этого шрифта можно использовать следующий класс-рисовальщик.
class FontCellRenderer implements ListCellRenderer (
public Component getListCellRendererComponent
(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus)
{
JLabel label = new JLabel();
Font font = (Font)value;
label.setText(font.getFamily() ) ;
label.setFont(font) ;
label.setOpaque(true);
label.setBackground(isSelected
? list.getSelectionBackground() :
label.setForeground(isSelected
? list.getSelectionForeground()
:
return label;
} }
Обратите внимание, что нам не пришлось создавать собственные методы paintcomponent или getPrefferedSize, потому что класс JList уже содержит эти методы. Программисту остается только задать параметры надписи, т.е. указать текст, шрифтицвет.'
Для создания более лаконичного кода можно даже создать этот класс-рисовальщик FontCellRenderer на основе класса JLabel, задавая нужные параметры непосредствен- нодля самого классаFontCellRendererииспользуяthisдлявозвращаемогозначения.
class FontCellRenderer extends JLabel implements ListCellRenderer {
public Component getListCellRendererComponent
(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus)
{
Font font = (Font)value;
setText(font.getFamily () ) ;
setFont(font);
setOpaque(true);
setBackground(isSelected
? list.getSelectionBackground() :
setForeground(isSelected
? list.getSelectionForeground()
:
return this;
} }
Этот код удобно использовать в тех случаях, когда какой-то существующий компонент (как класс надписи JLabel в этом примере) уже содержит все функции, необходимые для рисования элементов списка.
Листинг6.3. ListRenderingTest.java
/**
Sversion 1.21 2001-07-16
©author Cay Horstmann
*/
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
/**
Пример использования классов-рисовальщиков элементов для списка
*/
public class ListRenderingTest {
public static void main(String[] args) {
JFrame frame = new ListRenderingFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Этот фрейм содержит список шрифтов и текстовое
поле, в котором показан выбранный шрифт
*/
class ListRenderingFrame extends JFrame {
public ListRenderingFrame()
{
setTitle("ListRenderingTest") ;
setSize(WIDTH, HEIGHT);
ArrayList fonts = new ArrayList();
final int SIZE = 24;
fonts . add (new Font ("Serif", Font. PLAIN, SIZE));
fonts.add(new Font ("SansSerif ”, Font.PLAIN, SIZE));
fonts.add(new Font ("Monospaced", Font.PLAIN, SIZE));
fonts . add (new Font ("Dialog", Font. PLAIN, SIZE));
fonts . add (new Font ("Dialoginput", Font. PLAIN, SIZE));
fontList = new JList(fonts.toArray() ) ;
fontList.setVisibleRowCount(4) ;
fontList.setSelectionMode
(ListSelectionModel.SINGLE_SELECTION);
fontList.setCellRenderer(newFontCellRenderer()); JScrollPane scrollPane = new JScrollPane(fontList);
JPanel p = new JPanel();
p.add(scrollPane) ;
fontList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent evt) {
Font font = (Font) fontList .getSelectedValue (),; text.setFont(font) ;
}
}) ;
Container contentPane = getContentPane();
contentPane.add(p, BorderLayout.SOUTH);
text = new JTextArea(
"The quick brown fox jumps over the lazy dog");
text.setFont((Font)fonts.get(0)) ;
text.setLineWrap(true) ;
text.setWrapStyleWord(true);
contentpane.add(text, BorderLayout.CENTER);
)
private JTextArea text;
private JList fontList;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
/**
Класс-рисовальщик для объектов шрифта Font, который рисует имя шрифта этим же шрифтом
*/
class FontCellRenderer implements ListCellRenderer {
public Component getListCellRendererComponent
(final JList list, final Object value,
final int index, final boolean isSelected, final boolean cellHasFocus)
{
return new
JPanel()
{
public void paintcomponent(Graphics g) {
Font font = (Font)value;
String text = font.getFamily();
FontMetrics fm =
g. setColor(isSelected
? list.getSelectionBackground()
:
g.fillRect(0, 0, getwidth(), getHeight()) ; g.setColor(isSelected
? list.getSelectionForeground()
:
g.setFont(font);
g.drawstring(text, 0, fm.getAscent() ) ;
}
public Dimension getPreferredSize()
Font font = (Font)value;
String text = font.getFamily() ;
Graphics g=getGraphics();
FontMetrics fm= g.getFontMetrics(font) ;
return new Dimension(fm.stringWidth(text) , fm.getHeight()) ;
javax.swing.JList
Метод Color getBackground () возвращает цвет фона для невыбран- ных элементов списка.
Метод Color getSelectionBackground( ) возвращает цвет фона для выбранных элементов списка.
Метод void setCellRenderer(ListCellRenderer cellRenderer) задает метод-рисовалыцик, который используется для рисования элемен-
товсписка.
javax.swing.ListCellRenderer
Метод ComponentgetListCellRendererComponent(JList list,
Object item, int index, boolean isSelected, boolean hasFocus) возвращает компонент, чей метод будет использоваться для рисования содержимого элементов списка. Если элементы списка не имеют фиксированного размера, то этот компонент должен также реализовывать метод getPrefferedSize.
Параметры: list item index isSelected hasSelected
рисуемый список
рисуемый элемент списка
индекс рисуемого элемента списка
имеет значение true, если выбран заданный элемент имеет значение true, если заданный элемент содержит фокус
Деревья
На рис. 6.4 показан знакомый практически всем пользователям пример иерархиче-
ской структуры файловой системы, в которой система каталогов и файлов упорядочена
к a bin Ф (S boot ^Qdev № etc I J© home &&!ib [)- Qnnt opt ffl-Qproc- ^root-QsbinEl-Qtmp B^jusrФ OX11R6 ■ П bin - Q etc - СЭ gamesEl ^i586-mandrake-linux ЙВinclude ф В lib-alibexec localЙ bin ^doc ^emacs-packages - ^3etc - Bi gamest>H include - СЭ infoB-eij2sdkL4.0Cl binIdes ,366к a bin Ф (S boot ^Qdev № etc I J© home &&!ib [)- Qnnt opt ffl-Qproc- ^root-QsbinEl-Qtmp B^jusrФ OX11R6 ■ П bin - Q etc - СЭ gamesEl ^i586-mandrake-linux ЙВinclude ф В lib-alibexec localЙ bin ^doc ^emacs-packages - ^3etc - Bi gamest>H include - СЭ infoB-eij2sdkL4.0Cl binIdes ,366Рис. 6.4. Древовидная система каталогов файловРис. 6.4. Древовидная система каталогов файловв виде дерева. Это всего лишь один из многих примеров использования древовидной структуры. Программистам известно множество других примеров применения древовидной структуры для организации наследования классов. В повседневной жизни такую же древовидную структуру образует система территориальноадминистративного деления на страны, штаты, области и города, пример которой показан на рис. 6.5.
к a bin Ф (S boot ^Qdev № etc I J© home &&!ib [)- Qnnt opt ffl-Qproc
- ^root
-Qsbin
El-Qtmp B^jusr
Ф OX11R6 ■ П bin - Q etc - СЭ games
El ^i586-mandrake-linux ЙВinclude ф В lib
-alibexec local
Й bin ^doc ^emacs-packages - ^3etc - Bi games
t>H include - СЭ info
B-eij2sdkL4.0Cl bin
Ides ,366
к a bin Ф (S boot ^Qdev № etc I J© home &&!ib [)- Qnnt opt ffl-Qproc
- ^root
-Qsbin
El-Qtmp B^jusr
Ф OX11R6 ■ П bin - Q etc - СЭ games
El ^i586-mandrake-linux ЙВinclude ф В lib
-alibexec local
Й bin ^doc ^emacs-packages - ^3etc - Bi games
t>H include - СЭ info
B-eij2sdkL4.0Cl bin
Ides ,366
Рис. 6.4. Древовидная система каталогов файлов
Рис. 6.4. Древовидная система каталогов файлов
Программистам довольно часто приходится использовать эту структуру для создания кода. К счастью, в библиотеке Swing для создания именно таких структур предусмотрен класс JTree. Он вместе со вспомогательными классами (которые предназначены для выполнения рутинных стандартных действий) создает структуру дерева и обрабатывает запросы пользователей на расширение и удаление узлов. В этом разделе рассматриваются только самые общие и наиболее распространенные приемы использования класса JTree, а более подробное его описание можно найти в книге Core Java Foundation Classes Кима Топли (Kim Topley) [PrenticeHall 1998] или в книге Graphic Java 2 Дэвида М. Гири (David М. Geary) [Prentice-Hall 1999].
Для изучения следующего ниже материала познакомимся с некоторыми основными терминами, которые показаны на рис. 6.6. Дерево состоит из узлов (node). Каждый узел может быть либо листком (leaf), либо иметь дочерние узлы (child nodes). Каждый узел, за исключением корневого узла, имеет только одного родителя
(parent). Дерево имеет только один корневой узел (root node). Набор деревьев, каждый из которых имеет свой собственный корень, называется лесом, (forest).
| Мичиган I| Мичиган I\I Бавария |\I Бавария || Мюнхен I | Нюрнберг |I Киль ]| Мюнхен I | Нюрнберг |I Киль ]
| Мичиган I
| Мичиган I
\I Бавария |
\I Бавария |
| Мюнхен I | Нюрнберг |I Киль ]
| Мюнхен I | Нюрнберг |I Киль ]
Рис. 6.5. Древовидная система территориально-административного деления на страны,
штаты, области и города
ЛистЛист> "Дерево> "Дерево> Лес> ЛесРис. 6.6. Терминология, используемая для описания древовидных структурРис. 6.6. Терминология, используемая для описания древовидных структур
Лист
Лист
> "Дерево
> "Дерево
> Лес
> Лес
Рис. 6.6. Терминология, используемая для описания древовидных структур
Рис. 6.6. Терминология, используемая для описания древовидных структур
Простые деревья
В первом простом примере мы попробуем создать дерево с несколькими узлами, которое показано на рис. 6.8. Как и большинство других компонентов библиотеки Swing, компонент JTree основан на структуре "модель-просмотр-управление". Программист создает модель иерархических данных, а компонент автоматически отображает их. Для создания объекта типа JTree нужно в конструкторе создать модель дерева.
TreeModel model = ...;
JTree tree = new JTree(model);
В некоторых конструкторах деревья создаются на основе набора элементов.
JTree(Object[] nodes)
JTree(Vector nodes)
JTree(Hashtable nodes) // значения становятся узлами
Такие конструкторы практически бесполезны, потому что они создают только лес деревьев, каждое из которых содержит только один узел. Третий конструктор еще более бесполезен, потому что узлы появляются в нем в произвольном порядке, который задается хэш-кодом ключей.
Как же получить модель дерева? Можно создать свою собственную модель с помощью класса на основе интерфейса TreeModel. Этот способ будет рассмотрен ниже в настоящей главе, а в этом примере попробуем использовать предлагаемую по умолчанию в библиотеке Swing модель DefaultTreeModel.
Для создания модели дерева по умолчанию нужно просто создать корневой узел.
TreeNode root = . . . ;
DefaultTreeModel model = new DefaultTreeModel(root);
Классы деревьев можно создавать на основе еще одного интерфейса— TreeNode. В используемую по умолчанию модель можно вставлять любые объекты, которые основаны на этом интерфейсе. В этом примере применяется уже готовый класс-узел DefaultMutableTreeNode из библиотеки Swing. Как показано на рис. 6.7, этот класс основан на интерфейсе MutableTreeNode, который основан на интерфейсе TreeNode.
Рис. 6.7. Схема наследования интерфейсов-деревьев
Объект класса DefaultMutableTreeNode содержит пользовательский объект узла дерева. Если для него не задан специальный класс-рисовальщик, то дерево просто отобразит строку с помощью метода toString.
В первом примере в качестве пользовательских объектов применяются строки, хотя на практике обычно используются более сложные объекты. Например для отображения дерева каталогов имеет смысл использовать объекты-файлы File.
Пользовательский объект можно указать либо в конструкторе, либо позже с помощью метода setUserObject.
DefaultMutableTreeNode node
= new DefaultMutableTreeNode("Texas");
node.setUserObject("California");
Затем между родительскими и дочерними узлами следует установить связи. Для этогоЗатем между родительскими и дочерними узлами следует установить связи. Для этогонужно, начиная с корневого узла с помощью метода add, добавлять дочерние узлы.DefaultMutableTreeNode root= new DefaultMutableTreeNode("world");DefaultMutableTreeNode country= new DefaultMutableTreeNode(" USA");root .add(country) ;DefaultMutableTreeNode state= new DefaultMutableTreeNode("California") ; country.add(state) ;На рис. 6.8 показан итоговый вид полученного де-нужно, начиная с корневого узла с помощью метода add, добавлять дочерние узлы.DefaultMutableTreeNode root= new DefaultMutableTreeNode("world");DefaultMutableTreeNode country= new DefaultMutableTreeNode(" USA");root .add(country) ;DefaultMutableTreeNode state= new DefaultMutableTreeNode("California") ; country.add(state) ;На рис. 6.8 показан итоговый вид полученного де-ЕЗ WorldФЕЗ USAЕЗ WorldФЕЗ USACaliforniaSan Jose[^CupertinoФСMichiganDAnn ArborCaliforniaSan Jose[^CupertinoФСMichiganDAnn Arbor$ E3 Germany9 E3 Schleswig-Holstein QKiel$ E3 Germany9 E3 Schleswig-Holstein QKiel
Затем между родительскими и дочерними узлами следует установить связи. Для этого
Затем между родительскими и дочерними узлами следует установить связи. Для этого
нужно, начиная с корневого узла с помощью метода add, добавлять дочерние узлы.
DefaultMutableTreeNode root
= new DefaultMutableTreeNode("world");
DefaultMutableTreeNode country
= new DefaultMutableTreeNode(" USA");
root .add(country) ;
DefaultMutableTreeNode state
= new DefaultMutableTreeNode("California") ; country.add(state) ;
На рис. 6.8 показан итоговый вид полученного де-
нужно, начиная с корневого узла с помощью метода add, добавлять дочерние узлы.
DefaultMutableTreeNode root
= new DefaultMutableTreeNode("world");
DefaultMutableTreeNode country
= new DefaultMutableTreeNode(" USA");
root .add(country) ;
DefaultMutableTreeNode state
= new DefaultMutableTreeNode("California") ; country.add(state) ;
На рис. 6.8 показан итоговый вид полученного де-
ЕЗ World
ФЕЗ USA
ЕЗ World
ФЕЗ USA
California
San Jose
[^Cupertino
ФСMichigan
DAnn Arbor
California
San Jose
[^Cupertino
ФСMichigan
DAnn Arbor
$ E3 Germany
9 E3 Schleswig-Holstein QKiel
$ E3 Germany
9 E3 Schleswig-Holstein QKiel
рева.Рис. 6.8. Простой пример древови-
Таким образом следует связать все узлы, а потом дной структуры
создать класс-модель DeafultTreeNode с корневым
узлом и сам компонент-дерево JTree на основе этой модели.
DefaultTreeModel treeModel =newDefaultTreeModel(root);
JTree tree = new JTree(treeModel);
Либо еще проще можно передать корневой узел конструктору класса JTree, который автоматически создаст принимаемую по умолчанию модель дерева.
JTree tree = new JTree(root);
Полностью код всех этих операций показан в листинге 6.4.
Листинг 6.4. SimpleTree.java
/**
(Aversion 1.01 2001-07-17
@author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import javax.swing.*; import javax.swing.tree.*;
/**
Пример простой древовидной структуры */
public class SimpleTree
{
public static void main(String[] args)
JFrame frame = new SimpleTreeFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
}
/**
Этот фрейм содержит простое дерево
на основе созданной вручную модели
*/
class SimpleTreeFrame extends JFrame {
public SimpleTreeFrame()
{
setTitle("SimpleTree");
setSize(WIDTH,HEIGHT);
// Задание данных для модели дерева
DefaultMutableTreeNode root
= new DefaultMutableTreeNode("World");
DefaultMutableTreeNode country
= new DefaultMutableTreeNode("USA");
root .add(country);
DefaultMutableTreeNode state
= new DefaultMutableTreeNode("California");
country.add(state) ;
DefaultMutableTreeNode city
= new DefaultMutableTreeNode("San Jose");
state.add(city);
city = new DefaultMutableTreeNode("Cupertino");
state.add(city);
statenew DefaultMutableTreeNode("Michigan");
country.add(state);
city = new DefaultMutableTreeNode("AnnArbor");
state.add(city);
country = new DefaultMutableTreeNode("Germany");
root.add(country);
state = new DefaultMutableTreeNode("Schleswig-Holstein");
country.add(state);
city = new DefaultMutableTreeNode("Kiel”); state.add(city);
// Создание дерева и размещение его // в панели с полосой прокрутки
JTree tree = new JTree(root);
Container contentPane = getContentPane();
contentPane.add(new JScrollPane(tree));
}
private static final int WIDTH = 300;
private static final int HEIGHT = 200; }
Сразу после запуска этой программы дерево будет выглядеть так, как показано на рис. 6.9. В окне программы будут видны только корневой узел и его дочерние узлы. Для развертывания подчиненных деревьев нужно щелкнутьмышью по изображению кружкасчерточкой, которое обозначает ручку развертыванияподчиненногодерева. Черточка направлена вправо, если подчиненное дерево свернуто, или вниз, если под-
чиненное дерево развернуто, как показано на рис. 6.10. Такой макет структуры дерева носит условное название Metal, а изображение кружка с черточкой, видимо, является стилизованным обозначением дверной ручки, которую нужно повернуть вниз по часовой стрелке, чтобы "открыть" подчиненное дерево.
Конечно, внешний вид дерева зависит от выбранных программистом графических средств. Здесь описывается макет, используемый по умолчанию в платформе Java (он имеет условное название Metal). В графической оболочке Windows и Motif для обозначения свернутого и развернутого дерева используются символы + и - соответственно, как показано на рис. 6.11.
Развернутое подчиненное дерево
МЩДйЬиг ^48
-3 World «•□USA »■ E Germany
Рис. 6.9. Исходный вид древовидной структурыРис. 6.9. Исходный вид древовидной структурыНИИНИИ9 СЗ9CIU SAе-СЗCalifornia9 BeMichigan□Ann Arbore- Q Germany9 СЗ9CIU SAе-СЗCalifornia9 BeMichigan□Ann Arbore- Q GermanyСвернутое подчиненное деревоРис. 6.10. Пример свернутого и развернутого подчиненного дерева3 Worldi9 l3 USAi9□ CaliforniaD San JoseD Cupertino®-aMichigan9 C3 Germany®"C3 Schleswig-HolsteiniСвернутое подчиненное деревоРис. 6.10. Пример свернутого и развернутого подчиненного дерева3 Worldi9 l3 USAi9□ CaliforniaD San JoseD Cupertino®-aMichigan9 C3 Germany®"C3 Schleswig-Holsteini
Рис. 6.9. Исходный вид древовидной структуры
Рис. 6.9. Исходный вид древовидной структуры
НИИ
НИИ
9 СЗ
9CIU SA
е-СЗCalifornia
9 BeMichigan
□Ann Arbor
e- Q Germany
9 СЗ
9CIU SA
е-СЗCalifornia
9 BeMichigan
□Ann Arbor
e- Q Germany
Свернутое подчиненное дерево
Рис. 6.10. Пример свернутого и развернутого подчиненного дерева
3 Worldi
9 l3 USAi
9□ California
D San Jose
D Cupertino
®-aMichigan
9 C3 Germany
®"C3 Schleswig-Holsteini
Свернутое подчиненное дерево
Рис. 6.10. Пример свернутого и развернутого подчиненного дерева
3 Worldi
9 l3 USAi
9□ California
D San Jose
D Cupertino
®-aMichigan
9 C3 Germany
®"C3 Schleswig-Holsteini
Рис. 6.11. Пример обозначения свернутого и развернутого дерева в WindowsРис. 6.11. Пример обозначения свернутого и развернутого дерева в WindowsРис. 6.12.Дерево без соединитель- НЫХлинийРис. 6.12.Дерево без соединитель- НЫХлиний
Рис. 6.11. Пример обозначения свернутого и развернутого дерева в Windows
Рис. 6.11. Пример обозначения свернутого и развернутого дерева в Windows
Рис. 6.12.Дерево без соединитель- НЫХлиний
Рис. 6.12.Дерево без соединитель- НЫХлиний
В макете дерева в пакете SDK до версии 1.3 включительно по умолчанию не показаны контуры дерева (рис. 6.12). А в SDK версии 1.4 контуры дерева представлены угловатыми линиями между родительскими и дочерними узлами.
Для скрытия этих линий в SDK версии 1.4 нужно для свойства JTree. lineStyle задать значение none, как показано ниже.
tree.putCLientProperty("JTree.linestyle", "none");
И наоборот, для отображения этих линий нужно для свойства JTree. lineStyle задать значение Angled, как показано ниже.
tree.putCLientProperty("JTree.linestyle", "Angled");
На рис. 6.13 показан еще один стиль представления структуры дерева с помощью горизонтальных линий Horizontal. Это дерево содержит горизонтальные линии, которые разделяют только дочерние узлы корневого узла. На первый взгляд трудно представить себе ситуацию, когда могла бы пригодиться такая структура.
Ручка свертывания дереваРучка свертывания дерева9 Ousa»aCaliforniaТЕЗ MichiganDAnn Arbor 6-E3 Germany9 Ousa»aCaliforniaТЕЗ MichiganDAnn Arbor 6-E3 Germany3world3world?' СЗ USA ф СЗ California
Ручка свертывания дерева
Ручка свертывания дерева
9 Ousa
»aCalifornia
ТЕЗ Michigan
DAnn Arbor 6-E3 Germany
9 Ousa
»aCalifornia
ТЕЗ Michigan
DAnn Arbor 6-E3 Germany
3world
3world
D San Jose
D Cupertino ®-C3Michigan таGermany
Рис. 6.13. Дерево с горизонтальными разделительными линиямиРис. 6.13. Дерево с горизонтальными разделительными линиямиРис. 6. 14. Пример дерева с ручкой свертывания всего дереваРис. 6. 14. Пример дерева с ручкой свертывания всего дерева^■[Hschleswig-Holsteln
Рис. 6.13. Дерево с горизонтальными разделительными линиями
Рис. 6.13. Дерево с горизонтальными разделительными линиями
Рис. 6. 14. Пример дерева с ручкой свертывания всего дерева
Рис. 6. 14. Пример дерева с ручкой свертывания всего дерева
По умолчанию корневой узел не содержит ручки для свертывания всего дерева. Для использования этой ручки нужно вызвать представленный ниже метод.
tree. setShowsRootHandles (true) ;
На рис. 6.14 показан полученный результат. Теперь все дерево можно сворачивать и разворачивать целиком.
Кроме того, корневой узел можно скрыть, например, для отображения леса, т.е. набора деревьев с собственными корневыми узлами. Для этого все деревья, необходимые для отображения леса, следует объединить в дереве с общим корнем, а затем скрыть этот общий корень с помощью следующего метода.
tree.setRootVisible(false);
На рис. 6.15 показан пример леса с двумя деревьями с корневыми узлами USA и Germany, которые объединены в одном дереве со скрытым корневым узлом.
Пиктограмма папки
3USA
^E3 California
0-сзMichigan
3 Germany
©• a Schleswig-Holstein
Пиктограмма листа
Рис. 6.15. Пример отображения лесаРис. 6.15. Пример отображения лесаРис. 6.16.Примеры пиктограмм, используемых для изображения узлов дереваРис. 6.16.Примеры пиктограмм, используемых для изображения узлов дерева
Рис. 6.15. Пример отображения леса
Рис. 6.15. Пример отображения леса
Рис. 6.16.Примеры пиктограмм, используемых для изображения узлов дерева
Рис. 6.16.Примеры пиктограмм, используемых для изображения узлов дерева
Рассмотрим теперь узлы-листки дерева, для изображения которых используется другая пиктограмма, показанная на рис. 6.16.
В изображении дерева для каждого узла используется своя пиктограмма. Вообще, существует три типа пиктограмм: в виде листа бумаги, а также в виде открытой и закрытой папки.
Класс-рисовальщик узла должен знать, какой тип пиктограммы нужно использовать для каждого узла. По умолчанию решение принимается следующим образом: если метод isLeaf возвращает значение true, то используется пиктограмма листа бумаги, а в противном случае используется пиктограмма папки.
Метод isLeaf класса DefaultMutableTreeNode возвращает значение true, если данный узел не имеет дочерних узлов. Таким образом, узлы с дочерними узлами будут показаны в виде папок, а узлы без них — в виде листков.
Такой способ обозначения узлов подходит не для всех случаев. Например, при добавлении узла Montana для обозначения штата Монтана без указания городов этот штат будет обозначен пиктограммой листка. Однако, при этом будет нарушена логическая концепция представления дерева, в которой пиктограммами-листами обозначаются города.
Класс JTree для определения листков использует модель дерева. Если узел без дочерних узлов с концептуальной точки зрения не является листком, можно использовать другой критерий определения листков, например, на основе определения допустимости дочерних узлов.
Для этого нужно указать для нужных листков на недопустимость наличия дочерних узлов с помощью следующего метода.
node.setAllowsChildren(false) ;
Затем нужно запросить в модели дерева, допускается ли наличие дочерних узлов для данного узла и соответственно обозначение в виде пиктограммы листка. Для организации этого запроса нужно применить метод setAsksAllowsChildren класса DefaultTreeModel так, как показано ниже.
model.setAsksAllowsChildren(true) ;
В таком случае узлы, для которых допускается наличие дочерних узлов, будут обозначены пиктограммами-папками, а узлы, для которых не допускается наличие дочерних узлов, будут обозначены пиктограммами-листками.
Этот запрос можно организовать в конструкторе дерева с помощью следующей команды.
JTree tree = new JTree(root, tree);
// узлы, для которых не допускается наличие дочерних узлов,
// будут обозначены пиктограммами-листами
Га > P if^ avax. swing. JTree
Конструктор JTree (TreeModel model) создает дерево на основе заданной модели дерева.
Конструкторы JTree(TreeNode root) и JTree(TreeNode root, boolean asksAllowChildren) создают дерево с заданной no умолчанию моделью дерева, указанным корнем и допустимостью наличия дочерних узлов.
Параметры: rootкорневой узел
asksAllowChildrenимеет значение true для обозначения допусти
мости наличия дочерних узлов, т.е. для определения принадлежности данного узла кузлам-листкам
Метод void setShowsRootHandles(boolean b) отображает ручку свертывания дерева для корневого узла, если параметр b имеет значение true.
Метод void setRootVisible (boolean b) отображает корневой узел, если параметр b имеет значение true.
javax.swing.tree.TreeNode
Метод boolean isLeaf () возвращает значение true, если данный узел являетсялистком.
Метод boolean getAllowsChiIdren() возвращает значение true, если данный узел имеет дочерние узлы.
javax. swing. tree.MutableTreeNode
Метод void setUserObject (Object userObject) задает пользовательский объект userObj ect,который используется для рисования узла дерева.
javax. swing, tree .TreeModel
Метод boolean isLeaf (Object node) возвращает значение true, если данный узел node следует отобразить в виде листка.
javax. swing. tree.De faultTreeModel
Метод void setAsksAllowsChildren(boolean b) отображает узлы в виде пиктограмм-листков, если параметр b имеет значение true, а метод getAllowsChildren возвращает значение false. В противном случае эти узлы отображаются в виде пиктограмм-листков, если их метод isLeaf возвращает значение true.
javax. swing.tree.De faultMutableTreeNode
Конструктор DefaultMutableTreeNode (Object userObject) создает узел для заданного пользовательского объектаuserObj ect.
Метод add (MutableTreeNode child) вставляет узел в дерево в виде последнего дочернего узла для данного узла.
Метод setAl1owsChiIdren(boolean b) позволяет вставлять дочерние узлы для данного узла, если параметр b имеет значение true.
javax.swing.JComponent
Метод void puClientProperty (Object key, Object value) создает пару ключ/значение и включает ее в небольшую таблицу, которой управляет каждый компонент. Этот механизм "аварийного выхода" ("escape hatch") используется в некоторых компонентах библиотеки Swing для хранения специальных свойств, управляющих их внешним видом.
Редактированиедеревьевипутейобходадеревьев
В следующем примере рассматриваются способы редактирования деревьев. На рис. 6.17 показан внешний вид программы, в которой предусмотрены кнопки Add Sibling (Вставить равноправный узел) и Add Child (Вставить дочерний узел) для создания нового узла с именем New. Для удаления текущего выбранного узла в программе предусмотрена кнопка Delete (Удалить).
Рис.6.18. Пример пути обхода дереваРис.6.18. Пример пути обхода дереваДля реализации такого поведения нужно найти текущий выбранный узел. В классе JTree предусмотрен интересный способ поиска такого узла на основе пути обхода дерева (tree path), а не на основе самих узлов дерева. Путь обхода дерева начинается с корневого узла и состоит из последовательности дочерних узлов, например такой, которая показана на рис. 6.18.
Рис.6.18. Пример пути обхода дерева
Рис.6.18. Пример пути обхода дерева
Й World
9 S3 usa
©•(3California
ФЕЗ Michigan
D New
Рис.6.17. Пример программы для редактирования дереваРис.6.17. Пример программы для редактирования дереваФ-СЗGermany
Рис.6.17. Пример программы для редактирования дерева
Рис.6.17. Пример программы для редактирования дерева
Казалось бы для поиска текущего узла лучше было бы использовать интерфейс TreeNode и метод getParent. Но на самом деле класс JTree ничего не знает об интерфейсе TreeNode, потому что он используется при реализации класса DefaultTreeModel, а не в интерфейсе TreeModel. Дерево может иметь модель, в которой не используется интерфейс TreeNode. Например, в модели дерева с другими типами объектов могут вообще отсутствовать методы getParent и getChild. В таком случае для организации связи между узлами применяется модель дерева, а класс JTree вообще не имеет никаких сведений об этом. По этой причине в классе JTree предлагается использовать пути обхода дерева.
Класс TreePath управляет последовательностью ссылок на объекты типа Object (а не TreeNode) с помощью нескольких методов. Например, для получения ссылки на последний узел пути обхода можно использовать метод getLastPathcomponent. A для поиска текущего выбранного узла используется метод getSelectionPath класса JTree. Таким образом, зная путь обхода, т.е. на основании объекта TreePath, можно получить ссылку на текущий выбранный узел.
TreePath selectionPath = tree.getSelectionPath(); DefaultMutableTreeNode selectedNode =
selectionpath.getLastPathComponent();
Эта задача выполняется очень часто, поэтому для нее придуман специальный метод.
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
Этот метод называется getLastSelectedPathComponent, а не getSelectedNode, потому что дереву ничего не известно об узлах, а модель дерева работает только с путями обхода.
Кроме путей обхода дерева для описания узлов дерева используются методы класса JTree, которые работают с целочисленными индексами — позициями строк (row positions).Позиция строки — это просто номер строки (нумерация начинается с нуля) узла во внутреннем представлении дерева. Такие номера имеют только видимые узлы, причем номер строки может меняться при свертывании, развертывании или изменении дерева. По этой причине для доступа к узлам не рекомендуется использовать номера строк. Все методы класса JTree для работы с номерами строк имеют эквивалентные методы на основе путей обхода.
После выбора узла можно приступать к его редактированию. Однако, для создания дочернего узла нельзя просто применить метод add этого узла, как показано ниже.
selctedNode.add(newNode) ; // Так нельзя!
При изменении структуры узлов изменяется модель дерева, а не связанное с ним представление. Поэтому нужно послать соответствующее оповещение об этом. Если для вставки нового узла использовать метод insertNodelnto класса DefaultTreeMode1, то такое оповещение будет послано автоматически, например так, как показано ниже.
model.insertNodelnto(newNode, selectedNode, selectedNode.getChildCount()) ;
Аналогичным образом применяется метод removeNodeFromParent для удаления узла и оповещения об обновлении представления дерева.
model.removeNodeFromParent(selectedNode);
Для изменения пользовательского объекта с сохранением структуры узла нужно использовать метод nodeChanged.
model.nodeChanged(changedNode);
Автоматическое оповещение является основным достоинством модели DeafultTreeModel, потому что при создании собственных моделей дерева потребуется самостоятельно организовать такое оповещение. Более подробное обсуждение этой темы можно найти в книге Core Java Foundation Classes Кима Топли (Kim Topley).
Класс DeafultTreeModel имеет метод reload, который перезагружает всю модель. Однако, не следует вызывать методreloadтолько лишь для обновленияде- рева после внесения нескольких изменений. Дело в том, при такой регенерации дерева все узлы за пределами дочернихузлов корневого узла снова будут свернуты. Такой способ работы может вызвать раздражение у пользователей, потому что после каждого изменения узла им придется снова разворачивать дерево.
Если визуальное представление получает оповещение об изменении структуры узлов, оно будет обновлено, но не развернуто для просмотра добавленных узлов. Например, добавление нового дочернего узла для свернутых подчиненных деревьев будет выполнено внешне незаметно без развертывания этих подчиненных деревьев. Таким образом пользователь не имеет никакой обратной связи с информацией о выполненной команде. В таком случае придется специально организовать развертывание родительских узлов для отображения вставленного нового дочернего узла. Для
этого можно применить метод makeVisible класса JTree, который использует путь обхода дерева, ведущий к отображаемому на экране узлу.
Таким образом, для отображения вновь вставленного узла потребуется создать путь обхода дерева от корневого узла. Для получения этого пути сначала следует вызвать метод getPathToRoot класса DeafultTreeModel. Он возвращает массив TreeArray [ ] для всех узлов, начиная с текущего вплоть до корневого узла, который передается конструктору TreePath.
Вот как выглядит пример кода для отображения вновь вставленного узла.
TreeNode[] nodes = model.getPathToRoot(newNode);
TreePath path = new TreePath(nodes); tree.makeVisible(path);
Забавно, что класс DefaultTreeModel создает ложное представление, якобы ему ничего не известно о классе TreePath, хотя его работа как раз и заключается во взаимодействии с классом JTree. В своей работе класс JTree всегда использует пути обхода деревьев и никогда не применяет массивы узлов.
Предположим, что дерево находится в панели с полосой прокрутки. После развертывания дерева новый узел все еще может быть за пределами текущего окна просмотра. Для перехода к новому узлу следует вместо метода makeVisible вызвать метод scrollPathToVisible так, как показано ниже.
tree.scrollPathToVisible(path);
9 0 California' D San Jose D Cupertino9 CMichigan9 0 California' D San Jose D Cupertino9 CMichiganmid J DeIHemid J DeIHeЭтот вызов развертывает все узлы на пути обхода и прокручивает содержимое окна вплоть до самого последнего узла в конце пути обхода, как показано на рис. 6.19.
9 0 California
' D San Jose D Cupertino
9 CMichigan
9 0 California
' D San Jose D Cupertino
9 CMichigan
mid J DeIHe
mid J DeIHe
Ann Arbor
В New
3 Add Sibling J Add 1
Прокрутка для отображения нового узла
Рис. 6.19. Пример автоматической прокрутки дерева для просмотра нового узла
По умолчанию узлы дерева нельзя редактировать, но с помощью метода setEditable можно установить режим редактирования узлов.
tree.setEditable(true);
В таком случае после двойного щелчка мышью откроется показанное на рис. 6.20 окно редактора ячейки (default cell editor), которое основано на классе De aultCellEditor. По окончании редактирования строки следует нажать клавишу <Enter>. Для редактирования объектов-узлов можно использовать собственнЫые редакторы ячеек, но их обсуждение будет продолжено в разделе о таблицах, где они наиболее часто используются.
Пример использования предлагаемого по умолчанию редактора ячейки
В листинге 6.5 представлен полный код программы редактирования узлов дерева. Запустите программу, создайте несколько узлов и отредактируйте их дважды щелкая кнопкой мыши на имени узла. Убедитесь в том, что свернутые деревья разворачиваются, и содержимое окна прокручивается для отображения нового дочернего узла.
Листинг 6.5.
/“
1.01 2001-07-17 Cay
*/
import java.awt.*;
j
import
import
/*
Пример редактирования дерева
*/
public class TreeEditTest
public static voidargs)
JFrame frame = new
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) j frame.show();
/
Фрейм с деревом и кнопки редактирования дерева */ class TreeEditFrame ext ends JFrame {
public
{
setTitle("TreeEditTest") ;
HEIGHT) ;
// Создать дерево.
TreeNode root = model = new tree = new
tree.setEditable(true) ;
// Добавить панель с полосой прокрутки и деревом
JScrollPane scrollPane = new JScrollPane(tree);
getContentPane().add(scrollPane, BorderLayout.CENTER) ;
makeButtons();
// Создать панель кнопок
public TreeNode makeSainpleTree ()
{
DefaultMutableTreeNode root
= new DefaultMutableTreeNode("World");
DefaultMutableTreeNode country
= new DefaultMutableTreeNode("USA”);
root.add(country),-
DefaultMutableTreeNode state
= new DefaultMutableTreeNode("California");
country.add(state) ;
DefaultMutableTreeNode city
= new DefaultMutableTreeNode("San Jose");
state.add(city);
city = new DefaultMutableTreeNode("Cupertino");
state.add(city) ;
state = new DefaultMutableTreeNode("Michigan");
country.add(state) ;
city = new DefaultMutableTreeNode("AnnArbor");
state.add(city) ;
country = new DefaultMutableTreeNode("Germany");
root.add(country);
state = new DefaultMutableTreeNode("Schleswig-Holstein") ; country.add(state) ;
city = new DefaultMutableTreeNode("Kiel");
state.add(city) ;
return root;
/**
Создать кнопки для вставки равноправного
и дочернего узлов, а также для удаления узла
*/
public void makeButtons()
{
JPanel panel = new JPanel();
JButton addsib1ingButton = new JButton("Add Sibling");
addSiblingButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
DefaultMutableTreeNode selectedNode
= (DefaultMutableTreeNode)
tree.getLastSelectedPathComponent();
if (selectedNode == null) return;
DefaultMutableTreeNode parent
= (DefaultMutableTreeNode)
selectedNode.getParent() ;
if (parent == null) return;
DefaultMutableTreeNode newNode
= new DefaultMutableTreeNode("New");
int selectedlndex =
model.insertNodelnto(newNode, parent, selectedlndex + 1);
// Теперь отображаем новый узел
TreeNode[] nodes = model.getPathToRoot(newNode); TreePath path = new TreePath(nodes);
tree.scrollPathToVisible(path);
} });
panel.add(addSiblingButton);
JButton addChildButton = new JButton("Add Child");
addChiIdButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (selectedNode == null) return;
DefaultMutableTreeNode newNode
= new DefaultMutableTreeNode("New");
model.insertNodelnto(newNode, selectedNode, selectedNode.getChildCount()) ;
// Отобразить новый узел
TreeNode[] nodes = model.getPathToRoot(newNode); TreePath path = new TreePath(nodes);
tree.scrollPathToVisible(path) ;
} });
panel.add(addChildButton);
JButton deleteButton = new JButton("Delete");
deleteButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (selectedNode != null && selectedNode.getParent() != null) model.removeNodeFromParent(selectedNode);
}
)) ;
panel.add(deleteButton);
getContentPane().add(panel, BorderLayout.SOUTH); }
private DefaultTreeModel model;
private JTree tree;
private static final int WIDTH = 400;
private static final int HEIGHT = 200;
••••javax.swing.JTree
•
•
•
•
Метод TreePath getSelectionPath() получает путь к текущему выбранному узлу (или путь к первому выбранному узлу, если выбрано сразу несколько узлов). Возвращаетnull, если никакой узел не выбран.
Метод Object getLastSelectedPathComponent () получает объект- узел, который представляет текущий выбранный узел (или первый выбранный узел, если выбрано сразу несколько узлов). Возвращает null, если никакой узел не выбран.
Метод void makeVisible (TreePath path) развертывает все узлы вдоль заданного пути.
Метод void scrollPathToVisible ( (TreePath path) развертывает все узлы вдоль указанного пути и, если дерево содержится в панели с полосой прокрутки, прокручивает дерево, чтобы обеспечить видимость по-
следнего узла заданного пути.
avax. swing.tree. TreePath
Метод Object getLastPathComponent получает последний объект на заданном пути, т.е. объект-узел, представляющий данный путь.
javax. swing, tree. TreeNode
Метод TreeNode getParent () возвращает родительский узел для заданного узла.
Метод TreeNode getChildAt (int index) возвращает дочерний узел для заданного индекса. Значение индекса должно находиться в диапазоне между 0 иgetChildCount0-1.
Метод int getChildCount () возвращает количество дочерних узлов
для данного узла.
Метод Enumeration children() возвращает объект-нумератор, который совершает обход всех дочерних узлов для данного узла.
javax.swing.tree.DefaultTreeModel
Метод void insertNodelnfo(MutableTreeNode neweChild, Mu- tableTreeNode parent, uint index) вставляет указанный объект neweChild в качестве дочернего узла для родительского узла parent по заданному индексу index.
Метод void removeNodeFromParent (MutableTreeNode node) удаляет узел node из этой модели.
Методvoid nodeChanged(TreeNode node) уведомляет слушателей модели дерева, что данный узел node изменился.
Метод void nodesChanged(TreeNode parent, int[], changedChild- Indexes) уведомляет слушателей модели дерева, что изменились все дочерние узлыузлаparent с заданными индексами.
Метод void reload () перезагружает все узлы в этой модели. Эту операцию нужно выполнять только в тех случаях, когда узлы полностью изменились из-за какого-то внешнего воздействия.
Нумерация узлов
Иногда требуется найти узел дерева, начиная с корневого узла и перебирая все дочерние узлы до тех пор, пока не будет найден нужный узел. Класс DefaultMutableTreeNode имеет несколько удобных методов для такого перебора узлов.
Методы breadthFirs tEnumerat i on и depthFirstEnumeration возвращают объекты-нумераторы, которые имеют метод nextElement, предназначенный для обхода в ширину (breadth-first) или глубину (depth-first) всех дочерних узлов текущего узла. На рис. 6.21 схематически показаны оба эти способа обхода узлов.
Обход по ширинеОбход по ширинеРис. 6.21. Способы обхода узлов дереваРис. 6.21. Способы обхода узлов дереваОбход в глубинуОбход в глубину
Обход по ширине
Обход по ширине
Рис. 6.21. Способы обхода узлов дерева
Рис. 6.21. Способы обхода узлов дерева
Обход в глубину
Обход в глубину
Обход узлов дерева в ширину выполняется послойно, сначала корневой узел, затем все его дочерние узлы, затем все дочерние узлы этих дочерних узлов и т.д.
Обход узлов дерева в глубину похож на поиск выхода из лабиринта. В этом случае обход выполняется по какому-то одному пути вплоть до достижения узла-листка. Затем после возврата на некоторое количество уровней находится ближайший новый путь, и обход продолжается по этому пути вплоть до достижения следующего узла-листка и т.д.
В теории информатики такой обход дерева называется обходом в обратном порядке (postorder traversal), потому что в процессе поиска сначала посещаются дочерние, а потом родительские узлы. Метод postOrderTraversal является синонимом метода depthFirstTraversal. Следует отметить, что метод preOrderTraversal предназначен для обхода в глубину, но с перечислением родительских узлов перед дочерними узлами.
Ниже показан типичный способ применения этих методов обхода.
Enumeration breadthFirst = node.breadthFirstEnumeration();
while (breadthFirst. hasMoreEleznents {) )
какие-то действия с методом breadthFirst.nextElement();
Наконец, метод pathFromAncestorEnumeration предназначен для поиска пути от родительского узла к заданному узлу и перечисления узлов вдоль этого пути. Прин-
цип его работы основан на вызове метода getParent для данного узла до тех пор, пока не будет найден заданный родительский узел. После этого метод представляет путь обхода узлов в обратном порядке.
В следующем примере рассматривается применение описанных выше методов для обхода дерева наследования классов. Введите имя класса в текстовое поле в нижней части окна программы. После этого в дерево наследования будет добавлен указанный класс и все его суперклассы, как показано на рис. 6.22.
Рис. 6.22. Пример дерева наследования классов
В этом примере используется важное достоинство объектов-узлов дерева, которое заключается в том, что объект-узел может иметь произвольный тип. В этом примере узлы представляют иерархическую структуру классов, поэтому узлы будут иметь тип Class.
Конечно, во избежание появления классов-дубликатов нужно организовать проверку наличия вставляемого класса с помощью приведенного ниже метода поиска заданного узла в дереве.
public DefaultMutableTreeNode findUserObject(Object obj) {
Enumeration e = root.breadthFirstEnumerationO ;
while (e.hasMoreElements())
{
DefaultMutableTreeNode node
=
if (node.getUserObject().equals(obj)) return node;
}
return null;
Рисование узлов
В приложении часто возникает необходимость изменить способ отображения узлов, например, нужно использовать другие пиктограммы для узлов или другой шрифт для названий узлов. Эти изменения можно вносить с помощью класс-рисовальщика узлов дерева (tree cell Tenderer). По умолчанию для рисования узлов в классе JTreeиспользуется класс De aultTreeCellRenderer, который основан на классе надписи JLabel. Эта надпись содержит пиктограмму и подпись узла.
Класс-рисовальщик узлов дерева не рисует ручки свертывания/развертывания подчиненных деревьев. Они являются частью общего макета, и их не рекомендуется изменять.
Настройка внешнего вида узлов может быть выполнена тремя перечисленными ниже способами.
С помощью класса Defau1tTreeCe1IRendererзаменить пиктограмму, шрифт и цвет фона, которые используются для всех узлов дерева.
Расширяя используемый по умолчанию класс-рисовальщик узлов дерева DefaultTreeCellRenderer, использовать разные пиктограммы, шрифт и цвет фона для разных узлов дерева.
На основе интерфейса TreeCellRenderer создать класс-рисовальщик для использования собственных изображений для разных узлов дерева.
Рассмотрим более подробно каждый из этих способов. Наиболее простой способ изменения пиктограммы, шрифта и цвета фона узлов основан на применении заданного по умолчанию класса-рисовальщика узлов дерева DefaultTreeCellRenderer.
DefaultTreeCellRenderer renderer
= new DefaultTreeCellRenderer() ;
renderer.setLeafIcon(new Imagelcon("blue-ball.gif") ) ;
// пиктограмма для узлов-листков
renderer.setClosed!con(new Imageicon("red-ball.gif"));
// пиктограмма для свернутых узлов
renderer.setOpenlcon(new Imagelcon("yellow-ball.gif"));
// пиктограмма для развернутых узлов tree.setCellRenderer(renderer);
Результат выполнения этих действий показан на рис. 6.22, где в качестве пиктограмм узлов используются изображения шариков.
Не рекомендуется изменять шрифт и цвет фона, потому что они относятся к общему макету дерева.
Однако для выделения отдельных узлов можно попробовать изменить используемый в них шрифт. Это можно сделать, например так, как показано на рис. 6.22, где курсивомвыделены абстрактные классы.
Для изменения внешнего вида отдельных узлов нужно создать собственный класс- рисовальщик узлов дерева. Эта задача подобна уже описанной ранее в главе 9 тома I задаче создания собственного класса-рисовальщика ячеек списка. Интерфейс TreeCellRenderer имеет единственный метод getTreeCellRendererComponent.
Component getTreeCellRendererComponent(JTree tree,
Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus);
Этот метод вызывается для каждого узла и возвращает компонент, метод paint которого рисует узел дерева. Метод paint вызывается с соответствующим объектом Graphics с координатами области рисования.
В этом месте может возникнуть вопрос: почему бы не поместить метод paint в интерфейс TreeCellRenderer? Дело в том, что настраивать работу существующих D компонентов гораздо проще, чем создавать код рисования. Например, используемый по умолчанию класс-рисовальщик узлов дерева просто наследует класс JLabel и освобождает программиста от забот по указанию правильной дистанции между надписью и пиктограммой.
Метод getTreeCellRendererComponent класса DefaultTreeCellRenderer возвращает значение this, т.е. данную надпись. (Напомним, что класс DefaultTreeCellRenderer основан на классе JLabel.) Для настройки этого компонента следует расширить класс DefaultTreeCellRenderer. Затем нужно переопределить метод getTreeCellRendererComponent, т.е. вызвать метод суперкласса, который подготовит все данные, необходимые для создания надписи. Задайте нужные значения свойств и возвратите значение this, как показано ниже.
class MyTreeCellRenderer extends DefaultTreeCellRenderer {
public Component getTreeCellRendererComponent(CTree tree,
Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value,
selected, expanded, leaf, row, hasFocus);
DefaultMutableTreeNode node
= (DefaultMutableTreeNode)value;
// Извлечение пользовательского объекта
Class с = (Class)node.getUserObject();
// Указание шрифта
Fontfont=
setFont(font); return this;
} };
Параметр value метода getTreeCellRendererComponent является объектом- 1 I узлом, а не пользовательским объектом. Напомним, что пользовательский объект является компонентом DefaultMutableTreeNode, а класс JTree может содержать узлы произвольного типа. Если в дереве используются узлы типа DefaultMutableTreeNode, то на втором этапе нужно извлечь пользовательский объект, как это сделано в предыдущем примере.
Метод DefaultTreeCellRenderer использует одинаковый объект-надпись для всех узлов, но изменяет текст надписи для каждого узла. Для изменения шрифта в отдельном узле нужно в следующем вызове этого метода возвратить используемый по умолчанию шрифт. В противном случае все следующие узлы будут использовать новый шрифт! Способ восстановления исходного шрифта показан в листинге 6.6.
Третий способ с использованием собственных изображений для разных узлов дерева практически полностью похож на использование собственного класса- рисовальщика ячеек списка, который описывается выше в этой главе.
Рассмотрим подробнее принципы работы класса-рисовальщика узлов дерева, код которого приведен в листинге 6.6. С помощью этой программы отображается иерархия наследования с отображением курсивом названий абстрактных классов. После ввода полного имени класса (например, java.util.ArrayList) и нажатия клавиши <Enter> или щелчка на кнопке Add (Вставить) в дерево будет вставлен новый класс со всеми его суперклассами.
Весь код создания дерева классов находится в методе addClass. (Подробности организации этого дерева не имеют большого значения. Структура классов используется здесь лишь как удобный пример дерева с иерархией наследования. В собственных приложениях программисты могут использовать любые другие источники иерархических данных.) Этот метод использует алгоритм обхода в ширину для поиска классов- дубликатов вставляемого класса с помощью метода findUserObject, который реализован в предыдущем разделе. Если такого класса нет в данном дереве, то в дерево вставляется его суперкласс, а затем создается и отображается новый узел-класс.
Метод ClassNameTreeCellRenderer отображает имя класса обычным шрифтом или курсивом в зависимости от наличия ключевого слова ABSTRACT у объекта Class. Для обозначения абстрактных классов не используется никакой другой шрифт, чтобы избежать нежелательного влияния на общий макет дерева и надписей в нем. По этой причине курсив для абстрактного класса создается на основе наклонного начертания базового шрифта. Учтите, что существует только один совместно используемый объект типа JLabel, который возвращается всеми вызовами. Перед следующими вызовами метода getTreeCellRendererComponent нужно снова вернуться к исходному основному шрифту.
Обратите внимание на способ изменения пиктограмм узлов в конструкторе ClassTreeFrame.
Листинг 6.6. ClassTree.java
/**
©version 1.01 2001-07-17
©author Cay Horstmann
*/
import java.awt.* ;
importj ava.awt.event.*;
import java.lang.reflect.*;
import java.util.*;
import j avax.swing.* ; import javax.swing.event.*; import javax.swing.tree.*;
/**
Пример перерисовки узлов дерева классов и суперклассов
*/
public class ClassTree
{
public static void main(String[] args)
{
JFrame frame = new ClassTreeFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
/**
Этот фрейм содержит дерево классов, текстовое поле, кнопку для вставки новых классов */ class ClassTreeFrame extends JFrame {
public ClassTreeFrame() {
setTitle("ClassTree");
setsize(WIDTH, HEIGHT);
// Корневым узлом дерева классов является узле типа Object root = new DefaultMutableTreeNode(java.lang.Object.class) ; model = new DefaultTreeModel(root) ;
tree = new JTree(model);
// Вставить класс в дерево с некоторыми данными addClass(getClass()) ;
// Установить пиктограммы узлов.
ClassNameTreeCellRenderer renderer = new ClassNameTreeCellRenderer() ;
renderer.setClosedlcon(new Imagelcon("red-ball.gif")); renderer.setOpenlcon(new Imageicon("yellow-ball.gif")); renderer.setLeafIcon(new Imageicon("blue-ball.gif"));
tree.setCellRenderer(renderer);
getContentPane().add(new JScrollPane(tree), BorderLayout.CENTER);
addTextField() ;
}
/**
Создать текстовое поле и кнопку Add для вставки нового класса
*/
public void addTextField() {
JPanel panel = new JPanel();
ActionListener addListener = new
ActionListener() {
public void actionPerformed(ActionEvent event) {
// Создать класс с указанным // в текстовой строке именем try {
String text = textField.getText(); addClass(Class.forName(text)); // Очистить текстовое поле для // обозначения успешной вставки textField.setText("");
}
catch (ClassNotFoundException e)
{
JOptionPane.showMessageDialog(null,"Class not found");
}
}
};
// Имена новых классов вводятся в этом текстовом поле textField = new JTextField(20);
textField.addActionListener(addListener);
panel.add(textField);
JButton addButton = new JButton("Add");
addButton.addActionListener(addListener);
panel.add(addButton);
getContentPane().add(panel, BorderLayout.SOUTH);
/**
Найти объект в дереве
@param obj the object to find
Sreturn the node containing the object or null if the object is not present in the tree
*/
public DefaultMutableTreeNode findUserObject(Object obj) {
// Найти узел с пользовательским объектом
Enumeration e = root.breadthFirstEnumeration();
while (e.hasMoreElements())
{
DefaultMutableTreeNode node = if (node.getUserObject().equals(obj)) return node;
}
return null;
}
/**
Создать новый класс и любые родительские классы, которые еще не являются частью дерева @param с the class to add @return the newly added node.
*/
public DefaultMutableTreeNode addclass(Class c) {
// Вставить новый класс в дерево
// Пропустить неклассовые типы
if (c.islnterface() || c.isPrimitive()) return null;
// Если такой класс уже есть, вернуть его узел. DefaultMutableTreeNode node = findUserObject(c) ; if (node != null) return node;
// Если такого класса в дереве еще нет, / / сначала создать его родительские классы
Class s = с.getSuperclass();
DefaultMutableTreeNode parent;
if (s == null)
parent = root;
else
parent = addClass(s) ;
// Создать дочерний класс данного родительского класса DefaultMutableTreeNode newNode
= new DefaultMutableTreeNode(c) ;
model.insertNodelnto(newNode, parent, parent.getChiIdCount());
// Отобразить узел
TreePath path = new TreePath(model.getPathToRoot(newNode)); tree.makeVisible(path) ;
return newNode;
)
private DefaultMutableTreeNode root;
private DefaultTreeModel model;
private JTree tree;
private JTextField textField;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
/**
Этот класс рисует имя класса обычным шрифтом или курсивом * Курсивом будут показаны имена абстрактных классов
*/
class ClassNameTreeCellRenderer extends DefaultTreeCellRenderer {
public Component getTreeCellRendererComponent(JTree tree,
Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
// Получить пользовательский объект.
DefaultMutableTreeNode node =
Class с = (Class)node.getUserObjectO;
// Сначала создать курсив на основе обычного шрифта if (plainFont == null) {
plainFont = getFont();
/*
Этот класс-рисовальщик узлов дерева иногда вызывается для ярлыка с неуказанным шрифтом */ if (plainFont != null)
italicFont = plainFont.deriveFont(Font.ITALIC);
// Для абстрактного класса указать курсив
if ( (с.getModifiers() & Modifier.ABSTRACT) ==0) setFont(plainFont);
else
setFont(italicFont);
return this;
private Font plainFont = null;
private Font italicFont = null;
javax.swing.tree.DefaultMutableTreeNode
• Метод Enumeration breadthFirstEnumeration() возвращает объекты-узлы на пути обхода дерева в ширину. Он позволяет раньше совершить обходтехдочернихузлов, которыенаходятсяближеккорневомуузлу.
Метод Enumeration depthFirstEnumeration() узлы на пути обхода дерева в глубину.
Метод Enumeration postOrderEnumeration() метода depthFirstEnumeration().
Метод Enumeration preOrderEnumeration()
возвращает объекты-возвращает объекты-является синонимомявляется синонимомвозвращает объекты-возвращает объекты-ATIATIузлы на пути обхода дерева в глубину. Обход выполняется в прямом порядке, который аналогичен обратному порядку обхода метода postOrderEnumeration, за исключением того, что обход родительских узлов происходит раньше обхода дочерних узлов.
возвращает объекты-
возвращает объекты-
является синонимом
является синонимом
возвращает объекты-
возвращает объекты-
ATI
ATI
javax.swing.tree.TreeCellRenderer
Метод Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) возвращает компонент, метод paintкоторогорисуетузелдерева.
Параметры:
tree value selected expanded leaftree value selected expanded leafrow hasFocusrow hasFocusдерево с рисуемым узлом
tree value selected expanded leaf
tree value selected expanded leaf
row hasFocus
row hasFocus
рисуемый узел
имеет значение true, если данный узел выбран
имеет значение true, если показаны дочерние узлы данного узла имеет значение true, если данный узел нужно показать в виде дерева
номер строки для данного узла
имеет значение true, если текущий узел содержит фокус ввода
javax.swing.tree.DefaultTreeCellRenderer
Метод setLea Icon (Icon icon) задает пиктограмму для обозначения узла-листка.
Метод setOpenlcon(Icon icon) задает пиктограмму для обозначения развернутого подчиненного дерева.
Метод setClosedlcon(Icon icon) задает пиктограмму для обозначения свернутого подчиненного дерева.
Обработка событий дерева
Структура дерева чаще всего используется совместно с каким-то другим компонентом. Например, при выборе какого-то узла в соседнем окне может быть показана какая-то сопроводительная информация. На рис. 6.23 показан пример программы, в которой для каждого узла-класса в правом текстовом окне автоматически будут показаны статичные переменные и переменные экземпляра данного класса.
static int EXIT.ON.CLOSEstatic int EXIT.ON.CLOSEstatic java-Jang. Object defaultLoo nt defauitCloseOperationiavax. swIng.JRootPane rootpane boolean rootPaneCheckingEnable iavax accessibilityAccessibleContstatic java-Jang. Object defaultLoo nt defauitCloseOperationiavax. swIng.JRootPane rootpane boolean rootPaneCheckingEnable iavax accessibilityAccessibleCont3 class java.lang.Object
static int EXIT.ON.CLOSE
static int EXIT.ON.CLOSE
static java-Jang. Object defaultLoo nt defauitCloseOperation
iavax. swIng.JRootPane rootpane boolean rootPaneCheckingEnable iavax accessibilityAccessibleCont
static java-Jang. Object defaultLoo nt defauitCloseOperation
iavax. swIng.JRootPane rootpane boolean rootPaneCheckingEnable iavax accessibilityAccessibleCont
?□ classjava.awt.Component | ф Ей class java. awt.Contains!
ФД class java. awl.Wind;
iclass java awt F
D class Claф Sclass java. util.AbstractCoiilФ Hi class java.util.Abstractl| ; Dclass java. util.Array!D class Claф Sclass java. util.AbstractCoiilФ Hi class java.util.Abstractl| ; Dclass java. util.Array!Рис. 6.23. Пример программы для просмотра содержимого классовРис. 6.23. Пример программы для просмотра содержимого классов:t> c class javax$
D class Cla
ф Sclass java. util.AbstractCoiil
Ф Hi class java.util.Abstractl| ; Dclass java. util.Array!
D class Cla
ф Sclass java. util.AbstractCoiil
Ф Hi class java.util.Abstractl| ; Dclass java. util.Array!
Рис. 6.23. Пример программы для просмотра содержимого классов
Рис. 6.23. Пример программы для просмотра содержимого классов
Длясозданиятакойпрограммынужноиспользовать класс-слушатель событий выбора узлов дерева (tree selection listener). Этот класс нужно создать на основе интерфейса TreeSelectionListener с единственным методом valueChanged.
void valueChanged(TreeSelection event);
Метод valueChanged вызывается при каждом событии выбора или отмены выбора узлов дерева.
Слушатель включается в состав дерева стандартным образом.
tree.addTreeSelectionListener(listener);
При этом можно с помощью свойств модели выбора TreeSelectionModel специально указать, какой способ выбора узлов дерева учитывается: только один узел (свойство SINGLE_TREE_SELECTION), группа смежных узлов (свойство CONTINGUOUS_TREE_SELECTION) или произвольная группа несмежных узлов (свойство DISCONTINGUOUS_TREE_SELECTION). По умолчанию допускается выбор произвольной группы несмежных узлов. В этом примере программы просмотра классов допускается выбор только одного узла-класса.
int mode = TreeSelectionModel.SINGLE_TREE_SELECTION; tree.getSelectionModel().setSelectionMode(mode);
Кроме указания допустимого режима выбора, не надо будет предпринимать более никаких других действий.
Способ выбора нескольких элементов зависит от макетапрограммы. Для выбора несмежных узлов в макете Metal следует, удерживая нажатой клавишу <Ctrl>, щелкать на каждом выбираемом элементе, а для выбора нескольких смежных узлов следует, удерживая нажатой клавишу <Shift>, щелкнуть на первом и на последнем элементах выбираемой группы.
Для поиска текущего выбора нужно послать дереву запрос, вызывая метод getSelectionPaths.
TreePath[] selectedPaths = getSelectionPaths();
При указании ограничения на выбор только одного узла удобнее использовать метод getSelectionPath, который в общем случае возвращает первый выбранный путь или значение null, если нет выбранных путей.
Класс TreeSelectionEvent имеет метод getPaths, который возвращает массив объектов Treepath, но этот массив описывает изменения выбора, а не текущий выбор.
В листинге 6.7 приведен пример программы с возможностями выбора узлов, в котором для краткости не используется пользовательский класс-рисовальщик узлов дерева. В конструкторе фрейма пользователь ограничивается выбором только одного узла и созданием класса-слушателя событий выбора узлов дерева. При вызове метода valueChanged игнорируется значение его параметра-события и запрашивается у дерева текущий выбранный путь. Далее, как обычно, нужно получить последний узел пути и извлечь его пользовательский объект. Затем для создания строки со всеми полями выбранного класса используется метод getFieldDescription. После этого созданная строка отображается в текстовом поле.
Листинг 6.7. ClaaaBroweerTeat.java
/**
@version 1.01 2001-07-17
©author Cay Horstmann
/
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;
import java.util.*;
import j avax.swing.*;
import j
import javax.swing.tree. *;
у * *
Пример обработки событий выбора узлов дерева
/
public class ClassBrowserTest
{
public static void main(String[] args)
{
)
/**
Фрейм с деревом классов, текстовым полем для отображения свойств выбранного класса, а также текстовым полем для создания новых классов
/
class ClassBrowserTestFrame extends JFrame
{
public ClassBrowserTestFrame()
{
setTitle("ClassBrowserTest");
setsize(WIDTH, HEIGHT);
// Корневым узлом дерева классов является класс Object root = new DefaultMutableTreeNode(java.lang.Object.class);
model = new DefaultTreeModel(root);
tree = new JTree(model);
// Вставить этот класс в дерево
addclass(getClass() ) ;
// Указать режим выбора узлов
• tree.addTreeSelectionListener(new TreeSelectionListener()
{
public void valueChanged(TreeSelectionEvent event) {
// Если пользователь выбрал другой
// узел, обновить описание
TreePath path = tree.getSelectionPath();
if (path == null) return;
DefaultMutableTreeNode selectedNode
= (DefaultMutableTreeNode)
path.getLastPathComponent();
Class с = (Class)selectedNode.getUserObject();
String description = getFieldDescription(c); textArea.setText(description) ;
)
});
int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
tree.getSelectionModel().setSelectionMode(mode);
// Эта текстовая область содержит описание класса textArea = new JTextArea();
// Вставить дерево и текстовое поле
// в панель с полосой прокрутки
JPanel panel = new JPanel();
panel. setLayout (new GridLayout (1, 2));
panel.add(new JScrollPane(tree));
panel.add(new JScrollPane(textArea));
getContentPane().add(panel, BorderLayout.CENTER);
addTextField() ;
}
/**
Вставить текстовое поле и кнопку Add
*/
public void addTextField()
JPanel panel = new JPanel() ;
ActionListener addListener = new
ActionListener() {
public void actionPerformed(ActionEvent event) {
// Вставить класс, указанный в текстовом поле try
{
String text = textField.getText(); addclass(Class.forName(text)); // Очистить текстовое поле для // обозначения успешной вставки textField.setText("");
)
catch (ClassNotFoundException e) {
JOptionPane.showMessageDialog(null, "Class not found");
} }
};
// Новые имена классов вводятся в это текстовое поле textField = new JTextField(20);
textField.addActionListener(addListener);
panel.add(textField);
JButton addButton = new JButton("Add"); addButton.addActionListener(addListener);
panel.add(addButton);
getContentPane(),add(panel, BorderLayout.SOUTH);
)
/**
Найти объект в дереве.
©param obj the object to find
ereturn the node containing the object or null if the object is not present in the tree
*/
public DefaultMutableTreeNode findUserObject(Object obj) {
// Найти узел, содержащий пользовательский объект Enumeration e = root.breadthFirstEnumeration();
while (e.hasMoreElements())
{
DefaultMutableTreeNode node =
if (node.getUserObject().equals(obj)) return node;
}
return null ;
}
/**
Создание нового класса и любых родительских классов, которые ранее не входили в состав дерева
@param с the class to add
@return the newly added node.
*/
public DefaultMutableTreeNode addClass(Class c)
{
// Добавить новый класс в дерево
// Пропустить неклассовые типы
if (c.islnterface() || c.isPrimitive()) return null;
// Если класс уже в дереве, вернуть его узел
DefaultMutableTreeNode node = findUserObject(c);
if (node != null) return node;
// Если класса нет, сначала рекурсивно
// вставить его родительские классы
Class s = c.getSuperclass() ;
DefaultMutableTreeNode parent;
if (s == null) parent = root;
else
parent = addClass(s);
// Создать дочерний класс для
// данного родительского класса
DefaultMutableTreeNode newNode
new DefaultMutableTreeNode(c);
model.insertNodelnto(newNode, parent, parent.getChildCount());
// Показать узел
TreePath path = new TreePath(model.getPathToRoot(newNode)); tree.makeVisible(path);
return newNode;
/**
Возвращает описание полей класса @param the class to be described Sreturn a string containing all field types and names */ public static String getFieldDescription(Class c) {
// Использовать отражение для // поиска типов и имен полей StringBuffer r = new StringBuffer();
Field[] fields = c.getDeclaredFields(); for (int i = 0; i < fields.length; i++) {
Field f = fields[i];
if ( (f.getModifiers() & Modifier.STATIC) != 0) r.append("static");
r..append (f.getType().getName());
r.append(" ");
r.append(f.getName() ) ;
r.append("\n");
}
return r.toString();
private DefaultMutableTreeNode root;
private DefaultTreeModel model;
private JTree tree;
private JTextField textField;
private JTextArea textArea;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
javax.swing.JTree
Метод TreePath getSelectionPath() возвращает первый выбранный путь. Если путь не выбран, то в таком случае возвращается значение null.
Метод TreePath [ ] getSelectionPaths () возвращает массив путей ко всем выбранным узлам. Если пути не выбраны, то в таком случае возвращается значение null.
javax.swing.event.TreeSelectionListener
Метод void valueChanged(TreeSelection event) вызывается в каждом случае выбора или отмены выбора узлов дерева.
javax.swing.event.TreeSelectionEvent
Методы TreePath getPath() и TreePath [] getPathsO получают первый путь или все пути, которые изменились в данном событии выбора. Для получения сведений о текущем выборе, а не изменении выбора следует вызвать метод JTree. getSelectionPath.
Пользовательские модели деревьев
1818Sa classObjectlnspectorframe h,s ,HЦ class avax.swing.JTree tree-null■ D intdefaultCloseOperation=lI I<i £3class avax.swing.JRootPane rootPaneD class java.awt.Component mostRecentFocusOwner"n4 j0 int windowDecorationSMe-0Ы0 class javax.swing.JMenuBar menuBar«nul!IO-C5classjava.awt.Container contentpane| 1class avax. swingJLaveredPane layeredPane | | classjava.awt.Component glassPaneD class javax.swing.JButton defaultButton-null| ■D class javax.swingjRootPane$DeraultA£tlon defauh^reT 1 class Javax. swingJRootPaneSDefaultAction del lifJl!^^^Sa classObjectlnspectorframe h,s ,HЦ class avax.swing.JTree tree-null■ D intdefaultCloseOperation=lI I<i £3class avax.swing.JRootPane rootPaneD class java.awt.Component mostRecentFocusOwner"n4 j0 int windowDecorationSMe-0Ы0 class javax.swing.JMenuBar menuBar«nul!IO-C5classjava.awt.Container contentpane| 1class avax. swingJLaveredPane layeredPane | | classjava.awt.Component glassPaneD class javax.swing.JButton defaultButton-null| ■D class javax.swingjRootPane$DeraultA£tlon defauh^reT 1 class Javax. swingJRootPaneSDefaultAction del lifJl!^^^»»В заключительном примере рассмотрим показанную на рис. 6.24 программу, которая позволяет проверять содержимое переменной почти также, как это делает отладчик.
18
18
Sa classObjectlnspectorframe h,s ,H
Ц class avax.swing.JTree tree-null
■ D intdefaultCloseOperation=lI I
<i £3class avax.swing.JRootPane rootPane
D class java.awt.Component mostRecentFocusOwner"n4 j
0 int windowDecorationSMe-0Ы
0 class javax.swing.JMenuBar menuBar«nul!I
O-C5classjava.awt.Container contentpane| 1
class avax. swingJLaveredPane layeredPane | | classjava.awt.Component glassPane
D class javax.swing.JButton defaultButton-null| ■
D class javax.swingjRootPane$DeraultA£tlon defauh^reT 1 class Javax. swingJRootPaneSDefaultAction del lifJl!^^^
Sa classObjectlnspectorframe h,s ,H
Ц class avax.swing.JTree tree-null
■ D intdefaultCloseOperation=lI I
<i £3class avax.swing.JRootPane rootPane
D class java.awt.Component mostRecentFocusOwner"n4 j
0 int windowDecorationSMe-0Ы
0 class javax.swing.JMenuBar menuBar«nul!I
O-C5classjava.awt.Container contentpane| 1
class avax. swingJLaveredPane layeredPane | | classjava.awt.Component glassPane
D class javax.swing.JButton defaultButton-null| ■
D class javax.swingjRootPane$DeraultA£tlon defauh^reT 1 class Javax. swingJRootPaneSDefaultAction del lifJl!^^^
»
»
Рис. 6.24. Пример дерева с функциями проверки
Скомпилируйте и запустите эту программу прежде, чем читать дальнейшие объяснения принципов ее работы. Каждыйузелдеревасоответствуеткакой-топеременной экземпляра. Если переменная является объектом, то разверните структуру этого объекта
для просмотра его подчиненных переменных экземпляра. Как видите, эта программа позволяет просматривать содержимое своего фрейма-окна. При более внимательном изучении нескольких переменных экземпляра можно найти некоторые уже знакомые нам классы. Кроме того, эта программа позволяет оценить сложность компонентов библиотеки Swing, предназначенных для организации пользовательского интерфейса.
Особенностью этой программы является то, что для этого дерева не используется класс модели DefaultTreeModel. При наличии данных, которые уже иерархически организованы в древовидной структуре, вряд ли имеет смысл использовать предлагаемое по умолчанию дерево и синхронизировать оба дерева. Именно такая ситуация имеет место в этом примере: объекты уже связаны друг с другом с помощью ссылок, а потому не нужно дублировать эту структуру связей.
Для этого можно использовать несколько методов интерфейса TreeModel. Первая группа методов позволяет найти узлы дерева, начиная с корневого узла с последующим обходом его дочерних узлов. Класс JTree вызывает перечисленные ниже методы только при развертывании узла.
Obj ect getRoot();
int getChildCount(Object parent);
Object getChild(Object parent, int index);
В этом примере показано, почему для интерфейса TreeModel, также как и для класса JTree, не требуется явное определение узлов. Как видите, корневые и дочерние узлы могут быть объектами произвольного типа. Интерфейс TreeModel отвечает только за уведомление класса JTree об организации связи между ними.
Следующий метод выполняет функцию, обратную функции метода getChild.
int getlndexOfChild(Object parent, Object child);
Как показано в листинге 6.8, этот метод можно реализовать на основе первых трех.
Модель дерева сообщает классу дерева JTree о том, какие узлы следует отобразить как узлы-листки.
boolean isLeaf(Object node);
Если код изменяет модель дерева, то дерево нужно уведомить о необходимости перерисовки. При этом в модель добавляется класс-слушатель TreeModelListener. Таким образом, эта модель должна поддерживать стандартные методы управления слушателей.
void addTreeModelListener(TreeModelListener 1);
void removeTreeModelListener(TreeModelListener 1);
В листинге 6.8 показаны способы использования этих методов в данном примере.
Для изменения содержимого дерева модель использует один из четырех перечисленных ниже методов интерфейса TreeModelListener.
void treeNodesChanged(TreeModelEvent e);
void treeNodesInserted(TreeModelEvent e) ;
void treeNodesRemoved(TreeModelEvent e);
void treeStructureChanged(TreeModelEvent e);
Объект TreeModelEvent описывает расположение изменения. Подробности организации событий модели дерева для обслуживания вставки или удаления узлов имеют технический характер. Программисту достаточно только позаботиться об генерировании таких
событий при удалении или вставке узлов. В листинге 6.8 приводится пример генерирования события для замены корневого узла новым узлом-объектом.
Разработчики библиотеки Swing предусмотрели специальный класс javax.
М swing.EventListenerList, который содержит набор классов-слушателей событий. Более подробные сведения об этом можно найти в главе 8 тома 1.
Наконец, если пользователь редактирует узел дерева, то модель вызывается с помощью следующего метода.
void valueForPathChanged(TreePath path, Object newValue);
Если редактирование не допускается, то этот метод не потребуется использовать. В таком случае модель дерева можно создать с помощью одного из перечисленных ниже методов.
Obj ect getRoot ();
int getChildCount(Object parent);
Object getChild(Object parent, int index);
Эти методы описывают структуру дерева. После реализации остальных пяти методов так, как показано в листинге 6.8, можно приступать к отображению дерева.
В этом примере программы используется дерево с объектами типаVariable.
Если бы в этом примере использовалась модель дерева DefaultTreeModel, то узлы были бы объектами типа DefaultMutableTreeNode с пользовательскими объектами типа Variable.
Например, переменная с объявлением Employee j ое; имеет mun Employee . class и имя j ое, а ее значением является ссылка на объект j ое. Тогда класс v типа Variable, который описывает эту переменную, в этом примере будет иметь следующее объявление.
Variable v = new Variable(Employee.class, "joe", joe);
Если переменная имеет примитивный тип, то для ее значения нужно создать объ- e кт-оболочку.
Variable v =
new Variable(double.class, "salary", new Double(salary));
Если переменная является классом, то она имеет поля, которые можно перечислить и собрать в классе ArrayList. Так как метод getFields класса Class не возвращает поля суперкласса, то для этого придется вызвать методы getFields соответствующего суперкласса. Этот код приводится в конструкторе класса Variable. Метод getFields класса Variable возвращает массив полей. Наконец, метод toString класса Variable форматирует надпись узла. Надпись всегда содержит тип и имя переменной, а если переменная не является классом, то приводится ее значение.
Значения массивов в этом примере не отображаются. Это не трудно сделать, поэтому читателю предлагается самостоятельно решить эту задачу.
D
Рассмотрим теперь методы getRoot и getChildCount модели дерева.
public Object
{
return root;
}
public intparent)
{ return;
}
Методвозвращает новый объект типа Variable, который описывает не
которое поле. Методы getType и getName класса Field позволяют получить тип и имя поля. С помощью механизма отражения можно, вызывая метод get (parentValue), считать значение поля. Этот метод может генерировать исключительную ситуацию IllegalAccessException, связанную с нелегальным доступом. Но свободный доступ ко всем полям переменной типа Variable предоставлен в конструкторе Variable, поэтому такая исключительная ситуация вряд ли может возникнуть.
Вот как выглядит код метода getChild.
public Objectparent, int index)
{
fields =
Field f =
Object parentValue = ((),-
try { return new
;
}
e)
{ return null;
}
}
Итак, эти три метода описывают и обрабатывают структуру дерева объектов для компонента JTree, а остальные методы выполняют другие рутинные операции и описаны в листинге 6.8.
На самом деле эта модель дерева описывает бесконечное дерево. Это можно проверить, рассматривая объектЩелкая на переменной referent, мож
но вернуться к исходному объекту, который содержит идентичное подчиненное дерево с объектом WeakRefКонечно, не существует никакого способа хранения
бесконечного количества узлов. Просто эта модель дерева может генерировать узлы по требованию по мере развертывания родительских узлов.
Этот пример завершает обсуждение структуры и методов работы с деревьями. В следующем разделе приводится описаниееще одного сложного компонента
библиотеки Swing. Хотя деревья и таблицы имеют много различий, в них используются аналогичные принципы создания модели данных и перерисовки структурных элементов.
Листинг
/“
©version 1.01 2001-07-17
Cay
/
import j
import java.awt.event.*;
import java.lang.reflect.*;
import java.util.*;
import j avax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
/**
Пример использования пользовательской модели
дерева для отображения полей объекта
/
public class ObjectlnspectorTest
{
public static void main(String[] args)
{
JFrame frame = new ObjectlnspectorFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Этот фрейм содержит дерево объектов
/
class ObjectlnspectorFrame extends JFrame
{
public ObjectlnspectorFrame()
{
setTitle("ObjectlnspectorTest");
setsize(WIDTH, HEIGHT);
// Проверка фрейма с объектами
Variable v = new Variable(getClass(), "this", this);
ObjectTreeModel model = new ObjectTreeModel();
model.setRoot(v);
// Создание и отображение дерева
tree = new JTree(model);
getContentPane().add(newJScrollPane(tree), BorderLayout.CENTER);
}
private JTree tree;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
}
/**
Модель дерева описывает структуру дерева с Java-объектами. Дочерние объекты отображают переменные экземпляра
/
class ObjectTreeModel implements TreeModel
{
/**
Создать пустое дерево. */ public ObjectTreeModel() {
root = null;
}
/**
Указать корневой узел для данной переменной
Sparam v
*/
public void
{
Variable root = v;
the variable that is being described by this tree
setRoot(Variable v)
oldRoot = v;
fireTreeStructureChanged(oldRoot); }
public Object getRoot()
{
returnroot;
}
public int getChildCount(Object parent)
{
return ((Variable)parent).getFields().size() ;
}
public Object getChiId(Object parent, int index)
ArrayList fields = ((Variable)parent).getFields();
Fieldf= (Field)fields.get(index);
Object parentValue = ((Variable)parent).getValue(); try
}catch(IllegalAccessException e) {returnnull;}catch(IllegalAccessException e) {returnnull;return newVariable(f.getType(), f.getName(), f.get(parentvalue)) ;
}
catch(IllegalAccessException e) {
returnnull;
}
catch(IllegalAccessException e) {
returnnull;
p{ublic int getlndexOfChild(Object parent, Object child) intn=getChildCount(parent) ;
for (inti=0; i<n; i++)
if (getChild(parent, i).equals(child))
return i;
return -1;
}
public boolean isLeaf(Object node)
{
return getChildCount(node) ==0;
}
public void valueForPathChanged(TreePath path,
Object newValue) о
public void addTreeModelListener(TreeModelListener 1) {
listenerList.add(TreeModelListener.class,1); }
public void removeTreeModelListener(TreeModelListener 1) (
listenerList.remove(TreeModelListener.class,1); }
protected void fireTreeStruetureChanged(Object oldRoot) {
TreeModelEvent event
= new TreeModelEvent(this, new Object!] {oldRoot});
EventListener[] listeners = listenerList.getListeners( TreeModelListener .class);
for (int i = 0; i < listeners.length; i++)
((TreeModelListener)listeners[i]).treeStructureChanged(
event);
}
private Variable root;
private EventListenerList listenerList
= new EventListenerList();
/**
Переменная с указанием типа, имени и значения
*/ class Variable {
/**
Создание переменной
@param aType the type
@param aName the name
@param aValue the value
*/
public Variable(Class aType, String aName, Object aValue) {
type = aType;
name = aName;
value = aValue;
fields = new ArrayList();
/*
Найти все поля для класса, за исключением строк и значений null */
if (!type.isPrimitive() && !type.isArray() && !type.equals(String.class) && value != null)
{
// Возвращает поля класса и всех суперклассов for (Class с = value.getClass () ; с != null;
с = c.getSuperclass())
}
}
f =
true);
// Возвращает все нестатичные поля
for (int i = 0; i <i++)
if ( (f [i]&== 0)
fields.add(f[i]);
/**
Возвращает значение переменной the value
*/
public Object
{
return value;
/**
Возвращает все нестатичные поля этой переменной
an array list of variables describing the fields
*/
public ArrayList
{
return fields;
public String toString()
Stringr=type+ " " + if
r else
r else
r
+= if += if +=
return r;
"="+value;
(type.equals(String.class)) "="+value;
(value == null)
"=null";
private Class type;
private String name;
private Object value;
private ArrayList fields;
javax.
Метод Object getRoot () возвращает корневой узел.
Метод intparent) возвращает количество
дочерних узлов для заданного родительского узла.
Метод Objectparent, int index) возвращает
дочерний узел по заданному индексу и родительскому узлу.
Метод intparent, Object child) возвра
щает индекс указанного дочернего узла для заданного родительского узла.
Метод boolean isLeaf(Object node) возвращает значение true, если указанный узел является листком дерева.
Метод void addTreeModeListener(TreeModelListener 1) создает класс-слушатель, который используется для уведомления о происходящих в дереве изменениях.
Метод void removeTreeModeListener(TreeModelListener 1) удаляет класс-слушатель, который используется для уведомления о происходящих в дереве изменениях.
Метод voidvalueForPathChanged(TreePath path,Object
newValue) вызывается в тех случаях, когда редактор узлов изменяет значение узла.
Параметры:
pathпуть к редактируемому узлу
valueновое значение, возвращенное редактором
[
A P Tfjavax.swing.event.TreeModelListener
Метод void treeNodesChanged(TreeModelEvent e) вызывается в модели дерева при изменении узлов дерева.
Метод void treeNodesInserted(TreeModelEvent e) вызывается в модели дерева при вставке узлов в дерево.
Метод void treeNodesRemoved(TreeModelEvent e) вызывается в модели дерева при удалении узлов из дерева.
Метод void treeStructureChanged(TreeModelEvent e) вызывается в модели дерева при изменении структуры дерева.
javax.swing.event.TreeModelEvent
Параметры: eventSource nodeПараметры: eventSource nodeКонструктор TreeModelEvent (Object eventSource, TreePath node) создает событие изменения модели дерева.
Параметры: eventSource node
Параметры: eventSource node
модель дерева, которая генерирует данное событие путь к изменяемому узлу
Таблицы
Компонент JTable используется для отображения таблицы, т.е. двумерной решетки объектов. Таблицы очень широко применяются для создания пользовательских интерфейсов, поэтому команда разработчиков библиотеки Swing уделила большое внимание созданию элементов управления таблицами. Компонент JTable имеет очень сложную структуру, но она очень хорошо (по сравнению с другими компонентами библиотеки Swing) скрыта от пользователей. На основе всего нескольких строк кода можно создать полноценно функционирующую таблицу. Однако, для создания специальных таблиц придется создать дополнительный код и нужным образом настроить их внешний вид и поведение.
В этом разделе рассматриваются методы создания простых таблиц, приемы работы с ними и наиболее распространенные способы более тонкой настройки таблиц.
Также, как и при работе с другими более сложными компонентами Swing, в рамках одного раздела невозможно подробно описать все эти методы. Для более углубленного изучения этой темы рекомендуется использовать книгу Core Java Foundation Classes Кима Топли (Kim Topley) [Prentice-Hall 1998] или книгу Graphicjava 2 Дэвида М. Гири (David M. Geary) [Prentice-Hall 1999].
Простые таблицы
Аналогично компоненту JTree компонент JTable не хранит свои данные, а получает их от модели таблицы. Класс JTable содержит конструктор, который заключает двумерный массив объектов в используемую по умолчанию модель. Именно эта стратегия используется в нашем первом примере. Позднее в этой главе модели таблиц будут рассмотрены более подробно.
На рис. 6.25 показана типичная таблица с описаниями свойств планет солнечной системы. Для справки: столбец Gaseous (Газообразная) содержит значение true, если планета состоит в основном из водорода и гелия, или значение falseв противном случае. Значения в столбце Color (Цвет) пока не имеют какого-то смысла, этот столбец понадобится нам в следующем примере.
Planet
IM ertury
[Venus
Earth
Mars
JSuaptuitrenr
Uranus
Neptune
ii——1 j. MOtHtS : Г Gaseous : | Color,
2440 000faalissee java. awt'Coior[r- 255'4
6052 0false -(java. awt'Color[r= 2 5 5 - - j
2—--java, awt -Colon 0,9
3397 02-- -java, awt-'Color[r»255 -1
71492 016ttrruueejavaawtColor(r-255 {
Я&Ш)17in ip —Uava.awKolor[r»255 4
25559 0817ttrue- - -java.awtCoior[r* 0, g"
24766 081false --javaawt. Colonr* 0,g- - 1
"1137 01falsejavaawtColoH Г-О.О'")
v•
6.25. Пример простой таблицы
Как показано в листинге 6.9, данные таблицы хранятся в виде двумерного массива значений типа Object.
Object[][] cells = ('
"Mercury", new Double(2440), new Integer(O), Boolean.FALSE, Color.yellow
}, {.
"Venus", new Double(6052), new Integer(0), Boolean.FALSE, Color.yellow
);
Таблица просто вызывает метод toString для отображения каждого объекта. Поэтому цвета в последнем столбце представлены в виде строк java.awt.Color[r=. .., g=...,b=...].
Имена столбцов задаются в отдельном массиве строк.
String!]=
{
"Planet", "Radius", "Moons", "Gaseous", "Color"
Затем таблица создается на основании этого массива значений типа Obj ect и массива с именами столбцов. После этого стандартным образом создаются полосы прокрутки за счет вставки таблицы в панель JScrollPane.
JTable table = new JTable(cells, columnNames) ;
JScrollPane pane = new JScrollPane(table);
К удивлению, столь просто полученная таблица обладает очень богатым внешним видом! При достаточно большом уменьшении таблицы автоматически появляется полоса прокрутки. Обратите внимание, что имена столбцов всегда видны при прокрутке таблицы вниз.
Попробуйте щелкнуть кнопкой мыши на одном из столбцов и перетащить его в другое место. Обратите внимание на то, что ее можно визуально отделить от других столбцов небольшим промежутком, как показано на рис. 6.26. Такое переупорядочение столбцов возможно только в представлении таблицы и оно никак не влияет на модельданных.
' Planettabie' PlanettabieI .Planet.•Mercury IVenus Earth Mars Jupiter Saturn Uranus Neptune PlutoI .Planet.•Mercury IVenus Earth Mars Jupiter Saturn Uranus Neptune Plutoftt«ftt«jO-0jO-02161.18.17812161.18.17810 'o0 "o0 'o0 "o'false false 'false true true true true false'false false 'false true true true true falseJavaawtJavaawtjava awt java awt ■java awl •java awt java awt java awt •java awtjava awt java awt ■java awl •java awt java awt java awt •java awt
' Planettabie
' Planettabie
I .Planet.
•Mercury IVenus Earth Mars Jupiter Saturn Uranus Neptune Pluto
I .Planet.
•Mercury IVenus Earth Mars Jupiter Saturn Uranus Neptune Pluto
ftt«
ftt«
jO-0
jO-0
2
16
1.18.
17
8
1
2
16
1.18.
17
8
1
0 'o
0 "o
0 'o
0 "o
'false false 'false true true true true false
'false false 'false true true true true false
Javaawt
Javaawt
java awt java awt ■java awl •java awt java awt java awt •java awt
java awt java awt ■java awl •java awt java awt java awt •java awt
Рис. 6.26. Перемещение столбца
Для изменения размеров столбца поместите указатель мыши на границе раздела между двумя столбцами. После того, как указатель мыши принял форму двойной стрелки, перетащите границу раздела столбцов в нужное место, как показано на рис. 6.27.
jMercury -jMercury -MarsMarsNeptunePluto , „NeptunePluto , „;oDfalsejava awtColorю ■ ■Dfalsejava aw oorbl)falseawtColor'2falsejava awtColor1160truejava awtColor180truejava awtColor17 ,8'otruejava awtColor0truejava awtColor1false. --пауз awt Colscj;oDfalsejava awtColorю ■ ■Dfalsejava aw oorbl)falseawtColor'2falsejava awtColor1160truejava awtColor180truejava awtColor17 ,8'otruejava awtColor0truejava awtColor1false. --пауз awt ColscjMoonssi^dius— 1 GaseousI'——♦ ColorIMoonssi^dius— 1 GaseousI'——♦ ColorI
j
Mercury -
j
Mercury -
Mars
Mars
Neptune
Pluto , „
Neptune
Pluto , „
;o
D
false
java awtColor
ю ■ ■
D
false
java aw oor
bl
)
false
awtColor
'2
false
java awtColor
116
0
true
java awtColor
18
0
true
java awtColor
17 ,8
'o
true
java awtColor
0
true
java awtColor
1
false
. --пауз awt Colscj
;o
D
false
java awtColor
ю ■ ■
D
false
java aw oor
bl
)
false
awtColor
'2
false
java awtColor
116
0
true
java awtColor
18
0
true
java awtColor
17 ,8
'o
true
java awtColor
0
true
java awtColor
1
false
. --пауз awt Colscj
Moonssi^dius— 1 GaseousI'——♦ ColorI
Moonssi^dius— 1 GaseousI'——♦ ColorI
Рис. 6.27. Изменение размеров столбца
Выбор строки осуществляется щелчком мыши в любом месте строки. При этом выделенные строки обозначаются изменением цвета. Более подробно обработка событий выбора описывается ниже в этом разделе. В этом примере допускается редактирование полей, но эти изменения не сохраняются и не изменяют базовые данные. Однако, следует либо вообще запретить редактирование, либо организовать обработку событий редактирования и обновления модели данных. Эти темы подробно рассматриваются ниже в этом разделе.
Листинг6.9. PlanetTaMe .java
/**
@version 1.00 2001-07-18
@author Cay Horstmann
/
import java.awt.*;
import java.awt.event.*;
import j avax.swing.*;
import javax.swing.table.*;
/**
Пример простой таблицы
/
public class PlanetTable {
public static void main(String[] args) {
JFrame frame = new PlanetTableFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) frame.show();
}
}
/**
Фрейм с таблицей с данными о планетах
/
class PlanetTableFrame extends JFrame {
public PlanetTableFrame() {
setTitle("PlanetTable");
setsize(WIDTH,HEIGHT);
JTable table = new JTable(cells, columnNames);
getContentPane().add(new JScrollPane(table) , BorderLayout.CENTER);
}
private Object[][] cells =
{
"Mercury", new Double(2440), new Integer(O), Boolean.FALSE, Color.yellow }, {
"Venus", new Double(6052), new Integer(O), Boolean.FALSE, Color.yellow
{
"Earth", new Double(6378), newInteger(1) ,
"Mars", new Double(3397), new Integer(2), Boolean.FALSE, Color.red"Mars", new Double(3397), new Integer(2), Boolean.FALSE, Color.red}, Boolean.FALSE, Color.blue {
"Mars", new Double(3397), new Integer(2), Boolean.FALSE, Color.red
"Mars", new Double(3397), new Integer(2), Boolean.FALSE, Color.red
"Jupiter", new Double(71492), new Integer(16), Boolean.TRUE, Color.orange
{
"Saturn", new Double(60268), new Integer(18),
Boolean.TRUE, Color.orange
},
<
"Uranus", new Double(25559) , new Integer(17), Boolean.TRUE, Color.blue
(
"Neptune", new Double(24766), new Integer(8)
Boolean.TRUE, Color.blue
(
"Pluto", new Double(1137), new Integer(1) , Boolean.FALSE, Color.black
}
private String[] columnNames = {
"Planet", "Radius", "Moons", "Gaseous", "Color"
private static final int WIDTH = 400;
private static final int HEIGHT = 2 00; }
j avax. swing. JTable
Конструктор JTable(Object[][] entries, Object[] columnNames) создает таблицу с используемой по умолчанию моделью таблицы.
Параметры: entries columnNames
значения данных заголовки столбцов
Модель таблицы
В предыдущем разделе объекты таблицы хранились в виде двумерного массива. Однако, в общем случае рекомендуется использовать свою собственную модель таблицы.
Модель таблицы можно создать достаточно просто, потому что для этого предусмотрен специальный класс AbstractTableModel с реализацией большинства необходимых методов. Программисту остается лишь создать следующие три метода.
public int getRowObj ect() ;
public int getColumnCount() ;
public Object getValueAt(int row, int column);
Метод getValueAt можно реализовать несколькими различными способами: либо просто вычислить значение, либо извлечь его из базы данных или любого другого хранилища данных. Рассмотрим несколько примеров, иллюстрирующих эти способы.
В первом примере создается таблица с отображением вычисленных значений (рис. 6.28), а именно роста инвестиций для разных учетных ставок.
7%.8%7%.8%,9X10%,9X10%$105,000.00 $110,250.00$105,000.00 $110,250.00$115,762.50 $121,550 63 $127,628.16 $134,009.56 $140,710.04 $147,745 54$155,132 82 $162,889.46$171,033.94 $179,585.63$188,564.91$197,993.16$115,762.50 $121,550 63 $127,628.16 $134,009.56 $140,710.04 $147,745 54$155,132 82 $162,889.46$171,033.94 $179,585.63$188,564.91$197,993.16100,000 00100,000.00 [$100,000.00 $100,000 00 $100,000oo [$107,000 00108,000.00 [$109,000.00$112,360 00 l$114,490.00[$118,810.00100,000 00100,000.00 [$100,000.00 $100,000 00 $100,000oo [$107,000 00108,000.00 [$109,000.00$112,360 00 l$114,490.00[$118,810.00$119,101 60 ‘$126,247.70 $133,822 56$141,851 91 $150,363 03 $159,384 81 $168,947 90 $179,084 77 $189,829 86 $201,219 65 $213,292 83 $226,090 40$119,101 60 ‘$126,247.70 $133,822 56$141,851 91 $150,363 03 $159,384 81 $168,947 90 $179,084 77 $189,829 86 $201,219 65 $213,292 83 $226,090 40it 122,504.30it 122,504.304131,079.60 $140,255 17 [$150,073.04 $160,578 15 ($171,818.62 [$183,845.92 ‘$196,715.14 [$210,485.20 [$225,219.16 [$240,984.50 [$257,853.424131,079.60 $140,255 17 [$150,073.04 $160,578 15 ($171,818.62 [$183,845.92 ‘$196,715.14 [$210,485.20 [$225,219.16 [$240,984.50 [$257,853.42[$125,971.20[$141,158.16‘$146,932.81 ‘$153,862.40[$125,971.20[$141,158.16‘$146,932.81 ‘$153,862.40$2.О7.аа2.82..^.2..3.Й..65.5.а2...: $2.75.203^^$2.О7.аа2.82..^.2..3.Й..65.5.а2...: $2.75.203^^it 158,687.43 $171,382 43 [$185,093.02 [$199,900.46 [$215,892.50 [$233,163.90 $251,817 01 ;$271,962.37 [$293,719.36it 158,687.43 $171,382 43 [$185,093.02 [$199,900.46 [$215,892.50 [$233,163.90 $251,817 01 ;$271,962.37 [$293,719.36‘$167,710.01 i$ 182,803 91 [$199,256.26 [$217,189.33 [$236,736.37 ‘$258,042.64 $281,266.48 [$306,580.46 [$334,172.70 $364.248.25‘$167,710.01 i$ 182,803 91 [$199,256.26 [$217,189.33 [$236,736.37 ‘$258,042.64 $281,266.48 [$306,580.46 [$334,172.70 $364.248.25121,000.00 $133,100 00,;$146,410.00. J$ 161,051.00 [$177,156.10 [$194,871.71 [$214,358.88 ‘$235,794.77 $259,374.25 [$285,311.67 $313,842.84 $345,227 12121,000.00 $133,100 00,;$146,410.00. J$ 161,051.00 [$177,156.10 [$194,871.71 [$214,358.88 ‘$235,794.77 $259,374.25 [$285,311.67 $313,842.84 $345,227 12[$379,749.83.$417.724.82 I[$379,749.83.$417.724.82 I
7%.8%
7%.8%
,9X10%
,9X10%
$105,000.00 $110,250.00
$105,000.00 $110,250.00
$115,762.50 $121,550 63 $127,628.16 $134,009.56 $140,710.04 $147,745 54
$155,132 82 $162,889.46
$171,033.94 $179,585.63
$188,564.91
$197,993.16
$115,762.50 $121,550 63 $127,628.16 $134,009.56 $140,710.04 $147,745 54
$155,132 82 $162,889.46
$171,033.94 $179,585.63
$188,564.91
$197,993.16
100,000 00100,000.00 [$100,000.00 $100,000 00 $100,000
oo [$107,000 00108,000.00 [$109,000.00
$112,360 00 l$114,490.00[$118,810.00
100,000 00100,000.00 [$100,000.00 $100,000 00 $100,000
oo [$107,000 00108,000.00 [$109,000.00
$112,360 00 l$114,490.00[$118,810.00
$119,101 60 ‘$126,247.70 $133,822 56
$141,851 91 $150,363 03 $159,384 81 $168,947 90 $179,084 77 $189,829 86 $201,219 65 $213,292 83 $226,090 40
$119,101 60 ‘$126,247.70 $133,822 56
$141,851 91 $150,363 03 $159,384 81 $168,947 90 $179,084 77 $189,829 86 $201,219 65 $213,292 83 $226,090 40
it 122,504.30
it 122,504.30
4131,079.60 $140,255 17 [$150,073.04 $160,578 15 ($171,818.62 [$183,845.92 ‘$196,715.14 [$210,485.20 [$225,219.16 [$240,984.50 [$257,853.42
4131,079.60 $140,255 17 [$150,073.04 $160,578 15 ($171,818.62 [$183,845.92 ‘$196,715.14 [$210,485.20 [$225,219.16 [$240,984.50 [$257,853.42
[$125,971.20
[$141,158.16
‘$146,932.81 ‘$153,862.40
[$125,971.20
[$141,158.16
‘$146,932.81 ‘$153,862.40
$2.О7.аа2.82..^.2..3.Й..65.5.а2...: $2.75.203^^
$2.О7.аа2.82..^.2..3.Й..65.5.а2...: $2.75.203^^
it 158,687.43 $171,382 43 [$185,093.02 [$199,900.46 [$215,892.50 [$233,163.90 $251,817 01 ;$271,962.37 [$293,719.36
it 158,687.43 $171,382 43 [$185,093.02 [$199,900.46 [$215,892.50 [$233,163.90 $251,817 01 ;$271,962.37 [$293,719.36
‘$167,710.01 i$ 182,803 91 [$199,256.26 [$217,189.33 [$236,736.37 ‘$258,042.64 $281,266.48 [$306,580.46 [$334,172.70 $364.248.25
‘$167,710.01 i$ 182,803 91 [$199,256.26 [$217,189.33 [$236,736.37 ‘$258,042.64 $281,266.48 [$306,580.46 [$334,172.70 $364.248.25
121,000.00 $133,100 00
,;$146,410.00. J$ 161,051.00 [$177,156.10 [$194,871.71 [$214,358.88 ‘$235,794.77 $259,374.25 [$285,311.67 $313,842.84 $345,227 12
121,000.00 $133,100 00
,;$146,410.00. J$ 161,051.00 [$177,156.10 [$194,871.71 [$214,358.88 ‘$235,794.77 $259,374.25 [$285,311.67 $313,842.84 $345,227 12
[$379,749.83
.$417.724.82 I
[$379,749.83
.$417.724.82 I
Пример таблицы с вычисленными значениями роста инвестиций для разных учетных ставок
В этом примере метод getValueAt вычисляет соответствующее значение и форматирует его показанным ниже способом.
public Objectr, int с)
{
double rate = (с + minRate) / 100.0;
int nperiods = r;
double futureBalance =
*+ rate, nperiods);
return
NumberFormat.getCurrencylnstance().format(futureBalance);
}
Методы getRowCount и getColumnCount просто возвращают количество строк и столбцов соответственно.
public int
{
return years;
public int
{
return— minRate + 1;
}
Если заголовки столбцов не заданы явно, то методкласса
по умолчанию использует для них имена А, В, С и т.д. Для изменения этих имен нужно переопределить метод getColumnName. В этом примере в качестве заголовка столбца будет использована учетная ставка.
public Stringс)
{
double rate = (с + minRate) / 100.0; return
NumberFormat.getPercentlnstance().format(rate); }
Полный код этой программы приводится в листинге 6.10.
Листинг6.10. investmantTable.1ava
/**
©version 1.00 2001-07-18
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
importj ava.text. *
import javax.swing.*; import javax.swing.table.*;
/**
Пример создания таблицы на основе модели
*/
public class InvestmentTable
{
public static void main(String[] args)
{
JFrame frame = new InvestmentTableFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
} )
/**
Этот фрейм содержит таблицу с данными .06 инвестициях */ class InvestmentTableFrame extends JFrame {
public InvestmentTableFrame()
{
setTitle("InvestmentTable");
setsize(WIDTH, HEIGHT);
TableModel model = new InvestmentTabieModel(30, 5, 10) ;
JTable table = new JTable(model);
getContentPane () . add (new JScrollPane (table) , "Center"), }
private static final int WIDTH = 600;
private static final int HEIGHT = 300; }
/**
Эта модель таблицы вычисляет данные в ячейках таблицы для каждого запроса. В таблице показан рост инвестиций за указанное количество лет для данной учетной ставки */ class InvestmentTableModel extends AbstractTableModel {
/**
Создание модели данных для инвестиций у the number of years @paramthe lowest interest rate to tabulate
@param r2 the highest interest rate to tabulate */ publicy, int rl, int r2)
{ years = y; = rl; =
public int { return years;
}
public int {
return maxRate — minRate + 1; }
public Objectr, int c)
{
double rate = (c + minRate) / 100.0; int nperiods = r; double futureBalance =
*+ rate, nperiods);
return NumberFormat.getCurrencylnstance().format(futureBalance); }
public Stringc)
{ double rate = (c +/ 100.0;
return
NumberFormat.getPercentlnstance().format(rate); }
private int years;
private int minRate;
private int maxRate;
private static double= 100000.0;
Отображение записей, извлеченных из базы данных
Чаще всего в таблицах используются данные, взятые из базы данных. В профессиональной среде разработки программ, как правило, уже имеются удобные компоненты JavaBeans для организации доступа к базе данных. В следующем примере предполагается, что такой возможности у программиста нет. Даже при наличии таких компонентов JavaBeans, в общем случае этот пример очень полезен для понимания некоторых особенностей извлечения данных из базы данных. На рис. 6.29 показан результат выполнения запроса на извлечение данных из таблицы базы данных.
AUTHORJD.,ARON ARPA В EBABRANBROWCASTGEARCHINCHU1DOWNDUNT EWRIEVANFOUSF0X1GAIT GRAM GROVHARRHASSJAMEJUNGKARP KENN KERVAUTHORJD.,ARON ARPA В EBABRANBROWCASTGEARCHINCHU1DOWNDUNT EWRIEVANFOUSF0X1GAIT GRAM GROVHARRHASSJAMEJUNGKARP KENN KERVAronson- Larrv JArpaJian, Scott .BebaK Amur Brandon,liii Brown, Mark Castro, Elizabeth Cearly, Kent. Ch,n<- Francis Chu, Kenny Downing, Troy Duntemann, Jeff Erwin, Mike Evans, firn Foust, Jeff Fox, David Gaither, Mark Graham, Ian Groves, Dawn Harris, Stuart Massinger, Sebastian James, Steve Jung, John jKarpinski, Richard i Kennedy, Bill IKerven, DavidAronson- Larrv JArpaJian, Scott .BebaK Amur Brandon,liii Brown, Mark Castro, Elizabeth Cearly, Kent. Ch,n<- Francis Chu, Kenny Downing, Troy Duntemann, Jeff Erwin, Mike Evans, firn Foust, Jeff Fox, David Gaither, Mark Graham, Ian Groves, Dawn Harris, Stuart Massinger, Sebastian James, Steve Jung, John jKarpinski, Richard i Kennedy, Bill IKerven, DavidnНий, net/-nНий, net/-I db.www ijabooksc [I db.www ijabooksc [www peachpit com/ре'www peachpit com/ре'>*>*found csJM! edu/dowfound csJM! edu/dowHI HI HI HIHI HI HI HIi{ound cs nyu edu/dfoxi{ound cs nyu edu/dfoxwww utirc.utoronto.ca/. www skycat com/~daw www esnet-comj^sjrra..www utirc.utoronto.ca/. www skycat com/~daw www esnet-comj^sjrra..wwwjanw.com/sjbio htwwwjanw.com/sjbio htwww ora.com/www/ite.www ora.com/www/ite.
AUTHORJD.,
ARON ARPA В EBA
BRAN
BROW
CAST
GEAR
CHIN
CHU1
DOWN
DUNT EWRI
EVAN
FOUS
F0X1
GAIT GRAM GROV
HARR
HASS
JAME
JUNG
KARP KENN KERV
AUTHORJD.,
ARON ARPA В EBA
BRAN
BROW
CAST
GEAR
CHIN
CHU1
DOWN
DUNT EWRI
EVAN
FOUS
F0X1
GAIT GRAM GROV
HARR
HASS
JAME
JUNG
KARP KENN KERV
Aronson- Larrv JArpaJian, Scott .BebaK Amur Brandon,liii Brown, Mark Castro, Elizabeth Cearly, Kent
. Ch,n<- Francis Chu, Kenny Downing, Troy Duntemann, Jeff Erwin, Mike Evans, firn Foust, Jeff Fox, David Gaither, Mark Graham, Ian Groves, Dawn Harris, Stuart Massinger, Sebastian James, Steve Jung, John jKarpinski, Richard i Kennedy, Bill IKerven, David
Aronson- Larrv JArpaJian, Scott .BebaK Amur Brandon,liii Brown, Mark Castro, Elizabeth Cearly, Kent
. Ch,n<- Francis Chu, Kenny Downing, Troy Duntemann, Jeff Erwin, Mike Evans, firn Foust, Jeff Fox, David Gaither, Mark Graham, Ian Groves, Dawn Harris, Stuart Massinger, Sebastian James, Steve Jung, John jKarpinski, Richard i Kennedy, Bill IKerven, David
nНий, net/-
nНий, net/-
I db.www ijabooksc [
I db.www ijabooksc [
www peachpit com/ре
'
www peachpit com/ре
'
>
*
>
*
found csJM! edu/dow
found csJM! edu/dow
HI HI HI HI
HI HI HI HI
i{ound cs nyu edu/dfox
i{ound cs nyu edu/dfox
www utirc.utoronto.ca/. www skycat com/~daw www esnet-comj^sjrra..
www utirc.utoronto.ca/. www skycat com/~daw www esnet-comj^sjrra..
wwwjanw.com/sjbio ht
wwwjanw.com/sjbio ht
www ora.com/www/ite.
www ora.com/www/ite.
Рис. 6.29. Пример извлечения информации из таблицы базы данных
В этом примере создается модель таблицы ResultSetTableModel, которая получает данные в виде результатов выполнения запроса на выборку информации из базы данных. (Более подробно методы доступа к базам данных и результатам выполнения запроса на выборку описываются в главе 4, "Работа с базами данных — интерфейс JDBC”.)
Для получения номера и имени столбца используется объект rmsd, который со- держитметаданныерезультатов выполнения запроса.
public String getColuinnName (int с)
{
try
{
return rsmd.getColumnName(c+1);
}
catch(SQLException e)
{
e.printStackTrace();
return "";
} }
public int getColumnCount()
{
try
{
return rsmd.getColumnCount();
}
catch(SQLException e)
{
e.printStackTrace();
return 0;
Если в базе данных поддерживается работа с курсорами, то в таком случае значение ячейки можно получить очень просто, перемещая курсор к нужной ячейке и извлекая нужное значение.
public Object getValueAt(int r, int с) {
try
{
ResultSet rs = getResultSet();
rs.absolute(r + 1);
return rs.getObject(c + 1);
}
catch(SQLException e)
{
e.printStackTrace(); return null;
} }
Эта модель таблицы работает гораздо эффективнее используемой по умолчанию модели, потомуЧТО в ней не приходится дублировать значения в кэше драйвера базы данных.
Если в базе данных не поддерживается работа с курсорами, то в таком случае при использовании драйвераJDBC 1 запрошенные данные придется кэшировать вручную. Этот случай также рассматривается в приведенном ниже примере (листинг 6.11). На основе базового класса ResultSetTableModel созданы специальные подчиненные классы ScrollingResultSetTableModel для использования курсора и CachingResultSetTableModel для кэширования результатов выполнения запроса.
Листинг6.11. ReaultSetTable.java
/**
@version 1.01 2001-07-18
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import j
import java.sql.*;
import java.util.*;
import javax.s^ing.*; import javax.swing.table.*;
/**
Пример отображения результатов выполнения запроса в таблице */ public class ResultSetTable {
public static void main(String!] args) {
JFrame frame = new ResultSetFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
/**
Этот фрейм содержит поле со списком для выбора таблицы базы данных и таблицы для отображения информациии из базы данных
*/
class ResultSetFrame extends JFrame {
public ResultSetFrame()
{
setTitle("ResultSet") ;
setsize(WIDTH, HEIGHT);
/* Найти все таблицы в базе данных и вставить их в поле со списком
*/
Container contentPane = getContentPane() ;
tableNames = new JComboBox();
tableNames.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent evt) {
try { if (scrollPane != null)
getContentPane().remove(scrollpane);
String tableName =
if (rs != null) rs.close();
String query = "SELECT * FROM " + tabi©Name; rs = stat.executeQuery(query);
if (scrolling)
model
= new ScrollingResultSetTableModel(rs); else
model = new CachingResultSetTableModel(rs):
JTable table = new JTable(model); scrollPane = new JScrollPane(table);
getContentPane().add(scrollPane, BorderLayout.CENTER) ;
pack();
doLayout();
}
catch(SQLException e) {
e.printStackTrace() ;
}
)
}) ;
JPanel p = new JPanel() ;
p.add(tableNames);
contentPane.add(p, BorderLayout.NORTH) ;
try {
conn = getConnection();
DatabaseMetaData meta =
if (meta.supportsResultSetType( ResultSet,TYPE_SCROLL_INSENSITIVE))
{ scrolling = true; stat = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
} else {
stat = conn.createStatement(); scrolling = false;
}
ResultSet tables = meta.getTables(null, null, null, new String!] { "TABLE" });
while (tables.next())
tableNames.additem(tables.getString(3)) ;
tables.close();
}
catch(lOException e)
{
e.printStackTrace() ;
}
catch(SQLException e)
{
e.printStackTrace();
}
addWindowListener(new
WindowAdapter()
{ public void windowclosing(WindowEvent event)
{
try {
conn.close();
} catch (SQLException e) {
} e.printStackTrace();
}
});
/**
Установить соединение с базой данных по строке подключения, заданной в файле database.properties @return the database connection
*/
public static Connection getConnection() throws SQLException, IOException
{
Properties props = new Properties();
Fileinputstream in
= new Fileinputstream("database.properties");
props.load(in);
in.close();
String drivers = props.getProperty("jdbc.drivers”) ;
if (drivers I -- null)
System.setproperty("jdbc.drivers", drivers);
String url =
String username =
String password = props.getProperty("jdbc.password");
return
DriverManager.getConnection(ur1, username, password) }
private JScrollPane scrollPane;
private ResultSetTableModel model;
private JComboBox tableNames;
private ResultSet rs;
private Connection conn;
private Statement stat;
private boolean scrolling;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
/**
Этот класс модели таблицы является суперклассом для классов курсора и кэширования результатов выполнения запроса.
В нем хранятся результаты выполнения запроса и их метаданные
*/ abstract class ResultSetTableModel extends AbstractTableModel {
/**
Создание класса модели таблицы
@param aResultSet the result set to display.
*/
public ResultSetTableModel(Resultset aResultSet) {
rs = aResultSet;
try { rsmd = rs.getMetaData();
}
catch(SQLException e) {
e.printStackTrace();
}
}
public String getColumnName(int c)
{ try {
return rsmd.getColumnName(c+1);
catch(SQLException e)
{
e.printStackTrace();
return"",
)
public int getColumnCount()
{
try
{
return rsmd.getColumnCount();
}
catch(SQLException e)
{
e.printStackTrace() ;
return 0;
/**
Получение результатов выполнения запроса @return the result set
*/
protected ResultSet getResultSet()
{
return rs;
}
private ResultSet rs;
private ResultSetMetaData rsmd;
/**
Класс курсора (при использовании JDBC 2)
для обнаружения результатов выполнения запроса
*/
class ScrollingResultSetTableModel extends ResultSetTableModel {
/**
Создание модели таблицы
@param aResultSet the result set to display.
*/
public ScrollingResultSetTableModel(ResultSet aResultSet)
{
super(aResultSet) ;
}
p{ublic Object getValueAt(int r, int c) try
{
ResultSet rs = getResultSet();
rs.absolute(r+1);
} returnrs.getObject(c+1);
catch(SQLException e)
е.printStackTrace(); return null;
}
}
public int getRowCount()
{
try
{
ResultSet rs = getResultSet(); rs.last();
return rs.getRow();
}
catch(SQLException e)
{
e.printStackTrace() ;
return0,-
}
)
/*
Класс кэширования результатов выполнения запроса; используется при отсутствии поддержки курсоров в базе данных
*/
class CachingResultSetTableModel extends ResultSetTableModel {
public CachingResultSetTableModel(Resultset aResultSet)
{
super(aResultSet) ;
try
{
cache = new ArrayListO;
int cols = getColumnCount();
ResultSet rs = getResultSet ()
/* Поместить все данные в список массивов Object[]. Массив Object[][ ] здесь не используется, потому что неизвестно точное количество строк в результатах выполнения запроса
*/
while (rs.next() )
{
Object[] row = new Object[cols];
for (int j =0; j < row.length; j++)
row[j] = rs.getObject(j +1);
cache.add(row);
}
)
catch(SQLException e)
{
System.out.printin("Error" +e);
}
}
public Object getValueAt(int r, int c)
(
if (r < cache. size О)
return ((Object[])cache.get(r)) [c] else
return null;
public int getRowCount() {
return cache.size();
}
private ArrayList cache;
Фильтр сортировки
В последних двух примерах предполагалось, что компоненты-таблицы не хранят данные, а получают их из модели таблицы. Модель таблицы либо вычисляет данные, либо извлекает их из другого источника данных.
В этом разделе рассматривается еще один полезный метод представления информации с помощью модели-фильтра на примере сортировки записей таблицы по столбцам. Скомпилируйте и запустите программу, код которой приведен в листинге 6.12. Как показано на рис. 6.30, после двойного щелчка мышью на заголовке столбца все строки таблицы будут переупорядочены в соответствии с порядком упорядочения данного столбца.
Planet ? Radius. _
— |24400
Venus60520
Earth63780
0false java.awtC... Ц
0falseС
1false(Java awt С
Mars33970
Jupiter714920
SUaratunruns620525658900
Neptune247660
P|IJ r 1i1.V«°«™
false jayaawt С
.jtrue LiavauawtС
Vue(Java.awt.C... ||
17(true -Java-aw!.C,lL||
8Z .LZ^iZZ Java™, С
1falseJava awt С
Рис. 6.30. Сортировка строк таблицы
При этом не происходит никакого переупорядочения строк в модели данных. Вместо этого модель-фильтр поддерживает массив с перестановками индексов строк.
Модель-фильтр хранит ссылку на фактическую модель таблицы. Для подстановки нужного значения в таблицу JTable модель-фильтр вычисляет фактический индекс строки и получает значение с таким индексом с помощью методаgetValueAt.
public Object getValueAt(int r, int c)
{
return.model.getValueAt (.фактическийиндекс строки, с) ;
}
А все другие методы просто обращаются к исходной модели, например так, как показано ниже.
public String getColumnName(int с)
(
return.model.getColumnName(c);
На рис. 6.31 показана схема промежуточного расположения модели-фильтра (класс SortFilterModel) между фактической моделью таблицы (класс TabieModel) и объектом JTable.
Рис. 6.31. Схема взаимного расположения модели-фильтра, модели таблицы и компонентаJTable
При использовании такого фильтра сортировки следует учитывать следующие две особенности. Во-первых, нужно организовать уведомление о двойном щелчке на одном из заголовков столбцов. Код такого уведомления приведен в методе addMouseListenerкласса SortFilterModel в листинге 6.12. Кратко говоря, основная идея заключается в присоединении к компоненту заголовка таблицы класса- слушателя операций с мышью. При обнаружении двойного щелчка мышью необходимо определить тот столбец, к которому относится этот двойной щелчок. Затем нужно преобразовать столбец таблицы в столбец модели, потому что они могут отличаться при перемещениях столбцов в таблице. Наконец, после определения столбца модели можно приступать к сортировке строк таблицы.
При сортировке таблицы нужно не физически, а логически сортировать строки. Поэтому в данном случае выполняется сортировка индексов строк, которые определяют расположение строк для той или иной сортировки. Однако, алгоритмы сортировки классов Arrays и Collections не дают никакой информации о фактическом упорядочении строк. Поэтому их нужно переопределить и организовать слежение за перестановками объектов. Однако, существует гораздо более эффективный способ на основе пользовательских объектов и методов сравнения с применением библиотечных алгоритмов сортировки.
Рассмотрим сортировку объектов-строк типа Row. Объект типа Row содержит индекс строки r в данной модели. При сортировке столбца с для сравнения двух объектов-строк типа Row с индексами rlи r2 используется метод compareTo.
model.getValueAt(rl, c).compareTo(model.getValueAt(r2, с));
Если данные в каком-то столбце не могут сравниваться, то в таком случае следует сравнить их строковые представления. Именно таким образом могут быть отсортированы столбцы со значениями Boolean и Color. (Эти классы не реализуют интерфейс Comparable.)
Класс Row будет представлен как внутренний класс класса SortFilterModel, для того чтобы метод compareTo смог получить доступ к текущей модели и столбцу.
class SortFilterModel extends AbstractTableModel {
private class Row implements Comparable
{
public int index;
public int compareTo(Object other)
{
Row otherRow = (Row)other;
Object a =
Object b =sortColumn)
if (a instanceof Comparable) return else return
private
private int sortColumn;
private Row[] rows; }
В конструкторе создается массив rows, который инициализируется последовательными значениями i.
publicm)
{
model = m; rows = new for (int i = 0; i <i++)
{ = new =
В методе сортировки вызывается алгоритмsort, который сортирует
объекты-строки Row. Критерии сравнения рассматривают значения в соответствующем столбце, поэтому row[0] содержит индекс наименьшего элемента столбца, row [ 1 ] содержит индекс следующего наименьшего элемента столбца и т.д.
После сортировки массива нужно уведомить все классы-слушатели модели таблицы о том, что содержание таблицы изменилось и ее нужно перерисовать.
public voidс)
{ sortColumn = с; ;
fireTableDataChanged() ; }
Наконец, нужно отобразить вычисленные значения метода getValueAt класса- фильтра, который транслирует индекс строкив индекс строки модели
rows[i].index.
public Objectr, int c)
{ returnc);
}
В этом примере еще раз демонстрируется эффективность использования стратегии модель-представление. Благодаря тому, что данные и способ их отображения разделены, всегда можно нужным образом изменить способ их взаимного соответствия.
Листинг 6.12. TableSortTeet.java
/**
©version 1.00 2001-07-18
Sauthor Cay Horstmann
*/
import j ava.awt.*;
import java.awt.event.*;
import java.util.*;
import j avax*swing.*;
import javax.swing.event.*; import j avax.swing.table.*;
/**
Пример сортировки no столбцу таблицы после двойного щелчка на ее заголовке */ public class TableSortTest {
public static void main(String[] args) {
JFrame frame = new TableSortFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
} }
/**
Этот фрейм содержит таблицу с данными о планетах */ class TableSortFrame extends JFrame {
public TableSortFrame() {
setTitle("TableSortTest"); setsize(WIDTH, HEIGHT);
// Создать модель таблицы и сортировки
DefaultTableMode1 model
= new DefaultTableModel(cells, columnNames); final SortFilterModel sorter = new SortFilterModel(model);
// Показать таблицу.
final JTable table = new JTable(sorter);
getContentPane(),add(new JScrollPane(table), BorderLayout.CENTER) ;
// Создать класс-слушатель для // события двойного щелчка мышью
table.getTableHeader().addMouseListener(new MouseAdapter()
{
public void mouseClicked(MouseEvent event) {
// Проверка двойного щелчка мышью
if (event.getClickCount() < 2) return;
// Найти столбец, где произошел
// двойной щелчок мышью int tableColumn
=);
// Транслировать в индекс модели
// таблицы и сортировать
int modelColumn =
sorter.sort(modelColumn);
}
}) ;
rivate Object!][] cells =
{
"Mercury", new Double(2440), new Integer(O)
Boolean.FALSE, Color.yellow }, {
"Venus", newDouble(6052), new Integer(0), Boolean.FALSE, Color.yellow ), {
"Earth", newDouble(6378), newInteger(1), Boolean.FALSE, Color.blue
{
"Mars", newDouble(3397), new Integer(2) ,
}, Boolean.FALSE, Color.red
"Jupiter", newDouble(71492), new Integer(16), Boolean.TRUE, Color.orange
{
"Saturn", newDouble(60268), newInteger(18), Boolean.TRUE, Color.orange }, (
"Uranus", newDouble(25559) , new Integer(17), Boolean.TRUE, Color.blue ), (
"Neptune", newDouble(24766), newInteger(8), Boolean.TRUE, Color.blue }, {
"Pluto", newDouble(1137), newInteger(l), Boolean.FALSE, Color.black }
private=
"Planet", "Radius", "Moons", "Gaseous", "Color"
private static final int WIDTH = 400;
private static final int HEIGHT = 200;
/**
Эта модель таблицы принимает существующую модель таблицы и создает новую модель, которая сортирует все строки таким образом, чтобы были отсортированы все значения в данном столбце
*/
class SortFilterModel extends AbstractTableModel {
/**
Создание модели-фильтра сортировки
©param m the table model to filter
*/
public SortFilterModel(TableModel m)
{
model = m;
rows = new Row[model.getRowCount() ] ;
for (int i = 0; i < rows.length; i++) {
rows[i] = new Row();
=
}
}
/**
Сортировка строк
©param с the column that should become sorted
*/
public void sort(int c)
С
sortcolumn = c;
Arrays.sort(rows);
fireTableDataChangedf);
}
// Вычисление расположения перемещенной строки для
// трех методов доступа к элементам модели
public Object getValueAt(int r, int с) {
returnmodel.getValueAt(rows[r].index, c);
}
public boolean isCellEditable(int r, int c) {
return model.isCellEditable(rows[r].index, c);
}
public void setValueAt(Object aValue, int r, int c)
{
model.setValueAt(aValue, rows[r].index, c) ;
}
// Делегирование всех остальных
// методов из модели
public int getRowCount() {
return model.getRowCount();
}
public int getColumnCount()
{
return model.getColumnCount(); }
public String getColumnName(int c) С
return model.getColumnName(c) ; }
public Class getColumnClass(int c) {
return model.getColumnClass(c);
}
/**
Внутренний класс содержит индекс строки модели.
Строки сравниваются на основе просмотра
значений в столбце сортировки
*/
private class Row implements Comparable {
public int index;
public int compareTo(Object other) {
Row otherRow = (Row)other;
Object a = model.getValueAt(index, sortColumn);
Object b = model.getValueAt(otherRow.index, sortColumn) if (a instanceof Comparable)
return ((Comparable)a).compareTo(b);
else
return a. toString().compareTo(b.toString());
// Возвращение индекса otherRow.index }
}
private TabieModel model;
private int sortColumn;
private Row[] rows;
javax. swing, table. TableModel
Методы int getRowCount () и int getRowCount () возвращают количество строк и столбцов в модели таблицы.
Метод Object getValueAt (int row, int column) возвращает значение в заданной строке row и столбце column.
Метод void setValueAt(Object newValue, int row, int column) задает новое значение newValue в заданной строке row и столбце column.
Метод boolean isCellEditable(int row, int column) возвращает значение true, если разрешается редактирование в заданной строке row и столбце column.
Метод String getColumnName (int column) возвращает имя заданного столбца column.
javax.swing.table.AbstractTableModel
Метод void fireTableDataChanged() уведомляет все классы- слушатели модели таблицы о том, что произошло изменение данных.
javax.swing.JTable
Метод JTableHeader getTableHeader() возвращает компонент- заголовок данной таблицы.
Метод int columnAtPoint (Point p) возвращает номер столбца, который соответствует пикселю с номером р.
Метод int convertColumnlndexToModel(int tablecolumn) возвращает индекс модели для столбца с заданным индексом tableColumn. Этот индекс отличается от индекса tableColumn, в случае если какие-то столбцы перемещались или скрывались.
Перерисовка и редактирование ячеек
В следующем примере снова рассматривается таблица с данными о планетах, но теперь особое внимание будет уделено типам. данных в столбцах. Указанный ниже метод getColumnClass модели таблицы возвращает класс с описанием типа столбца, который может использоваться для выбора соответствующего классарисовальщика в классе JTable.
Class getColumnClass(int columnindex)
В табл. 6.1 показаны классы-рисовальщики, которые используются по умолчанию в классе JTable.
Указанные в таблице флажки параметров и изображения показаны на рис. 6.32. Авторы выражают свою благодарность Джиму Эвинсу (Jim Evins) за любезно представленные рисунки планет (http://www. shaught.com/JimsCoolIcons/Planets).
Таблица6.1. Используемые nd умолчаниюклассы- рисовальщикиклассаJTable
ТипОписание
ImageiconРисунок
BooleanФлажок параметра
ObjectObjectСтрока
Object
Object
Рис. 6.32. Пример таблицы с классами-рисовальщиками ячеек
Для других типов данных можно создать собственные классы-рисовальщики. Классы-рисовальщики ячеек таблицы устроены аналогично классам-рисовальщикам узлов дерева, которые рассмотрены в предыдущих разделах этой главы. Классы- рисовальщики ячеек таблицы основаны на интерфейсе TabieCellInterface, который содержит единственный метод getTableCellRendererComponent.
Component getTableCelIRender erComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row, int column);
Этот метод вызывается каждый раз при перерисовке таблицы и возвращает компонент, метод paint которого используется для рисования содержимого ячейки.
Для отображения ячейки типа Color можно просто возвратить панель с фоном, цвет которого определяется объектом в этой ячейке и передается с помощью параметра value.
class ColorTableCellRenderer implements TableCellRenderer
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row, int column)
(
panel.setBackground((Color)value) ;
return panel;
}
private JPanel panel = new JPanel();
}
Далее нужно указать таблице о необходимости использования этого класса- рисовальщика для всех объектов типа Color. Для этого предусмотрен метод setDefaultRenderer класса JTable.
table.setDefaultRenderer(Color.class,
new ColorTableCellRenderer());
Теперь этот класс-рисовальщик можно использовать для всех объектов данного типа.
Для создания более практичного класса-рисовальщика нужно создать визуальные подсказки для определения события выбора ячейки и события получения фокуса редактирования. Для этого потребуется задать размеры ячеек и цветовую схему. В Java для обозначения выбора используется окраска фона в бледно-голубой цвет. Для получения размеров ячейки нужно использовать метод getcellRect класса JTable, а для получения цвета фона и цвета текста — методы getSelectionBackground и getSelectionForeground соответственно.
Если класс-рисовалыцик просто перерисовывает текстовую строку или пиктограмму, то его можно создать на основе класса DefaultTableCellRenderer. В таком случае обработка событий выбора и получения фокуса будет выполнена автоматически.
Редактирование ячеек
Для редактирования ячеек модель таблицы должна получить сведения о том, какие ячейки можно редактировать с помощью метода isCellEditable. Например, в приведенном ниже фрагменте кода допускается редактирование в четырех столбцах.
public boolean isCellEditable(int r, int с) {
return с == NAME_COLUMN
|| с == MOON_COLUMN | с == GASEOUS_COLUMN
I j С == COLOR_COLUMN;
}
public static final int NAME_COLUMN =
public static final int MOON_COLUMN = 2; public static final int GASEOUS_COLUMN =
public static final int COLOR-COLUMN = 4;
В классе AbstractTableModel no умолчанию предусмотрено, что метод isCellEditable возвращает значение false, а в классе Defau11TabieMode1 этот метод переопределен и по умолчанию возвращает значение true.
В программе, код которой представлен в листинге 6.13, можно устанавливать и снимать флажки параметров в ячейках столбца Gaseous (Газообразная). Кроме того, как показано на рис. 6.33, можно также задавать нужное количество спутников в полях со списком в ячейках столбцаMoons (Спутники). В листинге 6.13 показано, как можно использовать поле со списком в качестве редактора ячейки.
Наконец, в первом столбце Planet (Планета) предусмотрено редактирование названия планеты.
В этом примере демонстрируется применение трех вариантов класса DefaultCellEditor для редактирования ячеек типа JTextField, JCheckBox и JCouiboBox. Класс JTable автоматически инсталлирует редактор флажков для ячеек типа boolean и текстовый редактор для ячеек, в которых разрешается редактирование, но не определен собственный класс-рисовальщик. Текстовые поля позволяют редактировать строки, которые возвращаются методом toString для возвращаемого значения метода getValueAt модели таблицы.
Рис. 6.33. Пример программы с использованием редакторов ячеекРис. 6.33. Пример программы с использованием редакторов ячеек
Рис. 6.33. Пример программы с использованием редакторов ячеек
Рис. 6.33. Пример программы с использованием редакторов ячеек
После завершения редактирования редактор ячейки передает полученный результат в модель таблицы с помощью вызова метода setValueAt. Далее, по усмотрению программиста, можно переопределить метод setValueAt для соответствующего преобразования возвращенного отредактированного значения.
Для преобразования значения ячейки в строку достаточно просто вызвать метод getValueAt, а потом метод toString.Однако, для обратного преобразования потребуется приложить дополнительные усилия. По окончании редактирования редактор ячейки вызывает метод setValueAt с возвращаемым объектом типа string. В этом случае метод setValueAt должен знать, каким образом нужно обработать полученную строку. Например, для сохранения отредактированных целочисленных значений потребуется вызвать метод Integer.parselnt.
Редактор поля со списком придется создать вручную, потому что компонент JTable не знает, какие значения следует использовать для такого типа ячейки. Например, в столбце Moons предполагается организовать выбор любого значения ОТ 0 до 20.
JComboBox moonCombo = new JComboBox();
for (int i = 0; i <= 20; i++)
moonCombo.additem(new Integer(1));
Для создания редактора DefaultCellEditor для такого типа данных нужно просто создать поле со списком в конструкторе так, как показано ниже.
TableCellEditor moonEditor = new DefaultCellEditor(moonCombo);
Затем нужно инсталлировать редактор, который в отличие от класса-рисовальщика ячеек Color не зависит от типа объекта. Его не обязательно использовать для всех объектов типа Integer, а достаточно инсталлировать только в заданном столбце.
Информация обо всех столбцах таблицы хранится в объектах типа TableColumn класса JTable и контролируется классом модели столбцов таблицы TableColumriModel. (На рис. 6.34 показана схема взаимосвязей между наиболее важными классами таблицы.) Если столбцы таблицы не предполагается динамически перемещать или вставлять, то в таком случае не придется очень интенсивно использовать класс модели столбцов таблицы
TableColuwModel. Однако, для получения заданного объекта TableColumn потребуется использовать класс модели столбцов таблицы TableColumnModel.
TableColumriModel columnModel = Tablecolumn moonColumn =
Наконец, нужно установить редактор ячеек.
moonColumn.setCellEditor(moonEditor);
Если высота ячеек выше заданной по умолчанию высоты, то ее придется указать явно. table.setRowHeight(height);
По умолчанию все строки таблицы имеют одинаковую высоту, но для разных ячеек можно задать разные высоты с помощью метода setRowHeight.
table.setRowHeight(row, height);
Фактическая высота строки задается перечисленными выше методами минус ширина границы. По умолчанию ширина границы равна 1, но для нее можно указать любое другое значение.
table.setRowMargin(margin);
Рис. 6.34. Схема взаимосвязей между классами Table
Пользовательские редакторы
Запустите эту программу и щелкните мышью на ячейке столбца Color. После этого на экране появится диалоговое ОКНО выбора цвета для данной планеты. Выберите
нужный цвет и щелкните на кнопке ОК, и цвет ячейки изменится заданным образом, как показано на рис. 6.35.
Редактор выбора цвета является не стандартным, а пользовательским редактором цвета ячейки таблицы. Для создания пользовательского редактора ячеек следует использовать интерфейс TableCellEditor. Этот интерфейс аналогичен другим интерфейсам библиотеки SDK 1.3 и для обработки событий в нем используется класс AbstractClassEditor.
Метод getTableCellEditorComponent интерфейса TableCellEditor запрашивает компонен'гредактор перерисовки ячейки. Он работает также, как и метод getTableCellEditorComponent интерфейса TableCellRenderer, за исключением того, что в нем нет параметра focus.Дело в том, что при редактировании ячейки предполагается, что эта ячейка обязательно имеет фокус ввода. Компонен'рредактор во время редактирования временно заменяет класс-рисовальщик. В этом примере для обозначения состояния редактирования панель отображается как пустая ячейка серого цвета.
Рис. 6.35. Редактирование цвета ячейки с помощью окна выбора цвета
Далее нужно организовать появление окна выбора цвета после щелчка на ячейке.
Класс JTable вызывает редактор после некоторого события (например, после щелчка мышью) для определения допустимости редактирования. Для определения всех событий,
которые могут инициировать редактирование, в классе AbstractCellEditor предусмотрен метод
public booleananEvent)
{
return true;
}
Однако, компонент-редактор не будет использоваться в таблице, если переопределить этот метод так, чтобы он возвращал
После инсталляции компонента-редактора вызывается метод shouldSelectCell, предположительно с тем же событием. В нем следует генерировать редактирование, например, за счет отображения на экране внешнего окна редактирования.
public booleananEvent)
{
colorDialog.setVisible(true) ;
return true;
}
Для отмены или остановки редактирования (например, если пользователь щелкнул на другой ячейке) будет вызван метод cancelCellEditing или stopCellEditing. Затем нужно закрыть диалоговое окно выбора цвета. При вызове метода stopCellEditing в таблице вероятно будет использовано частично отредактированное значение. Если такое значение допустимо, то следует возвратить значение true. Любое значение, выбранное в окне выбора цвета, является допустимым. Но при редактировании других данных нужно обязательно убедиться в допустимости частичных данных, полученных с помощью данного редактора.
Кроме того, следует вызвать методы суперкласса, которые позаботятся о генерировании событий, потому что в противном случае редактирование не удастся отменить должным образом.
public void
{
;
super.cancelCellEditing();
}
Наконец, необходимо определить метод получения значения, созданного пользователем после редактирования.
public Object getCellEditorValue()
{
return
}
Итак, пользовательский редактор должен выполнить следующие действия.
Расширить класс AbstractCellEditor на основеTableCellEditor.
Определить метод getTableCellEditorComponent для получения компонента-редактора. Это может быть либо(например, в виде
дельного диалогового окна), или компонент-редактор непосредственно в ячейке (например, поле со списком или текстовое поле).
Определить методы shouldSelectCell,и cancelCellEdit
ing для управления запуском, завершением и отменой процесса редактирования.
Для уведомления слушателей следует вызвать методы суперкласса11
ing и
Определить метод getCellEditorValue для возвращения значения, которое является результатом процесса редактирования.
Наконец, по окончании редактирования нужно вызвать методы stopCellEditing и cancelCellEditing. Для этого при создании диалогового окна выбора цвета следует указать соответствующие методы обратной связи для одобрения или отмены выполненного редактирования.
=
"Planet Color", false, colorChooser, new
//OK button listener
{ public voidevent)
{
stopCellEditing();
} }, new
// Cancel button listener {
public voidevent)
{
cancelCellEditing();
}
});
Кроме того, редактирование должно быть отменено после того, как пользователь закроет окно выбора цвета. Для обработки этого события нужно определить следующий класс-слушатель.
colorDialog.addWindowListener(new
WindowAdapter()
{
public voidevent)
{
cancelCellEditing();
}
}) ;
На этом код пользовательского редактора заканчивается.
Теперь нам известно, как создать редактируемую ячейку и инсталлировать редактор. Остается открытым только один вопрос: как обновить модель с учетом отредактированного значения. После окончания редактирования класс JTable вызывает следующий метод модели таблицы.
voidvalue, int r, int c);
Для сохранения нового значения этот метод нужно переопределить. Параметр value теперь будет обозначать объект, возвращенный редактором ячейки. В определении редактора ячейки задается тип объекта, который возвращается методом getCellEditor- Value. В данном примере при использовании класса aultCellEditor возможны три варианта указания типа для этого значения. Для редактора флажка параметра — это тип
boolean, для текстового поля — это тип String, а для поля со списком — это тип объекта, заданный пользователем.
Если объект value имеет другой тип, то его необходимо преобразовать к нужному типу. Чаще всего это происходит при редактировании чисел в текстовом поле. В этом примере поле со списком содержит объекты типа Integer, поэтому никакие преобразования не потребуются.
Листинг 6.13. TableCellRenderTest.lava
/**
©version 1.00 2001-07-21
@author Cay Horstmann
*/
import java.awt.*;
import j ava.awt. event
import java.util.*;
import javax.swing.*;
import javax.swing.event.*; import javax.swing.table.*;
/**
Пример перерисовки ячеек и редактирования значений таблицы */
public class TableCellRenderTest {
public static void main(String[] args) {
JFrame frame = new TableCellRenderFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) . frame.show();
}
}
/**
Фрейм-таблица с данными о планетах
*/
class TableCellRenderFrame extends JFrame {
public TableCellRenderFrame() {
setTitle("TableCellRenderTest");
setsize(WIDTH, HEIGHT);
TableModel model = new PlanetTableModel();
JTable table = new JTable(model);
// Указать классы-рисовальщики И классы-редакторы
table.setDefaultRenderer(Color.class, new ColorTableCellRenderer());
table.setDefaultEditor(Color.class, new ColorTableCellEditor()) ;
JComboBox moonCombo = new JComboBox() ; for (int i = 0; i <= 20; i++)
moonCombo.additem (newInteger(i)),- TableColumnModel columnModel =
Tablecolumn moonColumn =
moonColumn.setCellEditor(new DefaultCellEditor(moonCombo));
// Показать таблицу
table.setRowHeight(100);
getContentPane(),add(new JScrollPane(table), BorderLayout.CENTER) ;
private static final int WIDTH = 600; private static final int HEIGHT = 400;
}
/**
Модель таблицы с данными о планетах, свойствами перерисовки и редактирования
*/
class PlanetTableModel extends AbstractTableModel {
public String getColumnName(int c)
{
return columnNames[c] ;
}
public Class getColumnClass(int c) {
return cells[0][c].getClass();
}
public int getColumnCount()
{
return cells[0].length;
}
public int getRowCount()
{ return cells.length;
}
public Object getValueAt(int r, int c) {
return cells[r][c];
}
public void setValueAt(Object obj, int r, int c) {
cells[r][c] = obj;
}
public boolean isCellEditable(int r, int c) {
return с == NAME_COLUMN | С == MOON_COLUMN j С == GASEOUS_COLUMN j с == COLOR_COLUMN;
public
st
tic
fin
al
int
NAME_COLUMN = 0;
public
st
tic
fin
al
int
MOON_COLUMN = 2;
public
st
atic
fin
al
int
GASEOUS_COLUMN =
public
st
tic
fin
al
int
COLOR_COLUMN = 4;
private Object[][] cells = {
"Mercury", new Double(2440), new Integer(O), Boolean.FALSE, Color.yellow, new Imagelcon("Mercury.gif") }, {
"Venus", new Double(6052), new Integer(O), Boolean.FALSE, Color.yellow, new Imageicon("Venus.gif”)
}, (
"Earth", new Double(6378), new Integer(l), Boolean.FALSE, Color.blue, new Imageicon("Earth.gif")
(
"Mars", new Double(3397), new Integer(2) , Boolean.FALSE, Color.red, new Imageicon("Mars.gif“)
}, {
"Jupiter", new Double(71492), new Integer(16), Boolean.TRUE, Color.orange, new Imageicon("Jupiter.gif") ), {
"Saturn", new Double(60268), new Integer(18), Boolean.TRUE, Color.orange, new Imageicon("Saturn.gif") }, {
"Uranus", new Double(25559), new Integer(17) Boolean.TRUE, Color.blue, new Imageicon("Uranus.gif")
"Neptune", new Double(24766), new Integer(8) Boolean.TRUE, Color.blue, new Imageicon("Neptune.gif")
), {
"Pluto", new Double(1137), new Integer(l), Boolean.FALSE, Color.black, new Imageicon("Pluto.gif")
}
private String[] columnNames = {
Planet", "Radius ", "Moons", "Gaseous", "Color", "Image "
}
/**
Этот класс-рисовальщик перерисовывает значение
в столбце Color, окрашивая ячейки в соответствующий цвет
*/
class ColorTableCellRenderer implements TableCellRenderer {
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
panel.setBackground((Color)value);
return panel;
}
// Эта панель возвращается для всех ячеек с цветом фона,
// который определяется значением Color данной ячейки
private JPanel panel = new JPanel();
}
/**
Редактор в виде диалогового окна выбора цвета
*/
class ColorTableCellEditor extends. AbstractCellEditor
implements TableCellEditor
{
ColorTableCellEditor()
{
panel = new JPanel();
// Подготовить диалоговое окно выбора цвета
colorChooser = new JColorChooser();
colorDialog = JColorChooser.createDialog(null,
"Planet Color", false, colorChooser, new
ActionListener() // Кнопка OK {
public void actionPerformed(ActionEvent event)
{
stopCellEditing();
new
ActionListener() // Кнопка Cancel {
public void actionPerformed(ActionEvent event)
{
cancelCellEditing();
}
});
colorDialog.addWindowListener(new
WindowAdapter()
{
public void windowclosing(WindowEvent event) {
cancelCellEditing();
} }) ;
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
// Получение текущего значения
// Color из диалогового окна
colorchooser.setcolor((Color)value);
return panel;
public boolean shouldSelectCell(Eventobject anEvent) {
// Начать редактирование colorDialog.setVisible(true);
// Сообщить об успешном редактировании return true;
public void cancelCellEditing() <
// Отмена редактирования - скрыть диалоговое окно colorDialog.setVisible(false);
super.cancelCellEditing() ;
public boolean stopCellEditing() {
// Завершение редактирования - скрыть диалоговое окно colorDialog.setVisible(false);
super.stopCellEditing();
// Сообщить об успешном выборе цвета return true;
public Object getCellEditorValue() {
return;
}
private Color color;
private JColorChooser colorChooser;
private JDialog colorDialog;
private JPanel panel,
}
javax.swing.JTable
Метод void setRowHeight (int height) задает высоту всех строк таблицы в пикселях.
Методvoid setRowHeight(int row, int height) задает высоту заданной строки таблицы в пикселях.
Метод void setRowMargin(int margin) задает ширину пробела между ячейками смежных строк таблицы.
Метод int getRowHeight () возвращает заданную по умолчанию высоту всех строк таблицы.
Метод int getRowHeight (int row) возвращает высоту заданной строки таблицы.
• Метод int getRowMargin () возвращает ширину пробела между ячейками смежных строк таблицы.
• Метод Rectangle getCellRect(int row, int column, boolean inc ludeSpac ing) возвращает прямоугольную границу ячейки.
Параметры:
row
column
includeSpacing
строка
столбец
имеет значение true, если должно быть учтено пространство между ячейками
Методы Color getSelectionBackground() и Color getSelection- Foreground () возвращают цвет фона и текста для выбранных ячеек.
javax. swing. table. TableModel
Метод Class getColumnClass(int columnindex) возвращает класс значений в указанном столбце, который используется классом-рисовальщиком и классом-редактором.
javax. swing, table. TableCellRenderer
Метод Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) возвращает компонент, метод paint которого вызывается для рисования ячейки таблицы.
Параметры:
tableтаблица, содержащая рисуемые ячейки
valueрисуемая ячейка
selected имеет значение true, если текущая ячейка выбрана
hasFocus имеет значение true, если текущая ячейка содержит фокус ввода rowномер строки с рисуемой ячейкой
columnномер столбца с рисуемой ячейкой
javax. swing, table. TableColumnModel
МетодTableColumn getColumn(int index) возвращает объект столбца таблицы с заданным индексом.
javax.swing.table.TableColumn
Метод void setCellEditor (TableCellEditor editor) задает класс-
редактор для всех ячеек заданного столбца.
Метод void setCellRenderer(TableCellRenderer renderer) задает класс-рисовальщик для всех ячеек заданного столбца.
javax.swing.DefaultCellEditor
Конструктор Def aultCellEditor(JComboBox comboBox) создает класс- редактор ячеек, который представляет поле со списком для выбора значения ячейки.
••••javax.swing.CellEditor
•
•
•
•
Метод boolean isCellEditable(EventObject event) возвращает значение true, если данное событие может запустить процесс редактирования данной ячейки.
Метод boolean shouldSelectCell (EventObject event) запускает процесс редактирования и возвращает значение true, если редактируемая ячейка должна быть выделена, либо значение false,если во время редактирования не требуется выделять ячейку.
Метод void cancelCellEditing () отменяет процесс редактирования, что позволяет отменить частичные результаты редактирования.
Метод boolean stopCellEditing() останавливает процесс редактирования, что позволяет использовать частичные результаты редактирования. Он возвращает значение true, если редактируемое значение является допустимым.
Метод Object getCellEditorValue () возвращает редактируемое значение.
Метод void addCellEditorListener(CellEditorListener 1) вставляет обязательный слушатель редактора ячейки.
Метод void removeCellEditorListener(CellEditorListener 1) удаляет обязательный слушатель редактора ячейки.
E
^yf javax, swing, table. TableCellEdi tor
H • Метод Component getTableCellEditorComponent(JTable table, Object value, boolean selected, int row, int column) возвращает компонент, метод paint которого вызывается для рисования ячейки таблицы.
Параметры:
tableтаблица, содержащая рисуемые ячейки
valueрисуемая ячейка
selected имеет значение true, если текущая ячейка выбрана
rowномер строки с рисуемой ячейкой
columnномер столбца с рисуемой ячейкой
3'avax. swing.JColorChooser
Метод JColorChooser () создает класс-выборщик цвета с белым исходным цветом.
Метод Color getColor () возвращает текущий цвет для класса- выборщика цвета.
Метод void setColor(Color с) задает текущий цвет для класса- выборщика цвета.
Метод static JDialog createDialog(Component parent,String
title, boolean modal, JColorChooser chooser, ActionListener okListener, ActionListener cancelListener) создает диалоговое ок- нос классом-выборщикомцвета.
Параметры: parent title modal
родительский компонент диалогового окна
заголовок диалогового окна
имеет значение true, если диалоговое окно является модальным, т.е. работа программы блокируется до тех пор, пока оно не будет закрыто
chooser okListener
класс-выборщик цвета слушатель кнопки OK
cancelListener слушатель кнопки Cancel
Метод static Color showDialog(Component component, String title, color initialColor) создает и отображает модальное диалоговое окно, которое содержит класс-выборщик цвета.
Параметры:
componentродительский компонент диалогового окна
titleзаголовок диалогового окна
initialColor выбранный по умолчанию цвет
Работа со строками и столбцами
В этом разделе рассматриваются способы работы со строками и столбцами таблицы. При этом нужно учитывать, что таблица библиотеки Swing имеет несимметричную структуру, т.е. для строк и столбцов допускается выполнение разных операций. Дело в том, что компонент-таблица библиотеки Swing предназначена для отображения информации из таблиц базы данных (например, в результате выполнения запроса), а не из произвольной двумерной решетки объектов. Эта асимметрия демонстрируется на примерах этого раздела.
Изменение размеров столбцов
Класс Tablecolumn позволяет управлять размерами столбцов с помощью метода void setPreferredWidth (int width) для указания предпочтительной ширины, метода void setMinWidth (int width) для указания минимальной ширины и метода void setMaxWidth(int width) для указания максимальной ширины. Эти данные используются для создания макета столбцов.
Метод void setResizable (boolean resizable) используется для указания допустимости изменения размеров столбца.
Вручную изменить размер столбца можно с помощью метода void setwidth (in width).
При изменении размера одного столбца общий размер таблицы по умолчанию остается прежним. Конечно, при этом происходит согласованное увеличение или уменьшение размеров остальных столбцов. По умолчанию такие изменения происходят для всех столбцов, которые находятся справа от данного столбца. Благодаря такому правилу, программист может указать нужные размеры для всех столбцов слева направо.
В табл. 6.2 перечислены другие режимы изменения размеров столбца, которые можно указать с помощью метода void setAutoResizeMode(int mode) класса JTable.
Таблица 6.2. Режимы изменения размеров столбцов
Режим
Описание
AUTO_RESIZE_OFF
Изменение размера таблицы без изменения размеров столбцов
AUTO RESIZE NEXT COLUMN
Изменение размера только следующего столбца
AUTO_RESIZE_SUBSEQUENT_COLUMN
Равномерное изменение размеров для всех столбцов справа (используется по умолчанию)
AUTO_RESIZE_LAST_COLUMN
Изменение размера только последнего столбца
AUTO RESIZE_ALL_COLUMNS
Равномерное изменение размеров всех столбцов таблицы (не рекомендуется, так как усложняет настройку размеров для нескольких столбцов)
Выборстрок,столбцовиячеек
В зависимости от заданного режима пользователь может выбирать строки, столбцы и отдельные ячейки таблицы. По умолчанию допускается выбор строки, т.е. после щелчка кнопкой мыши в одной из ячеек будет выбрана вся строка с этой ячейкой, как показано на рис. 6.36. Для отмена режима выбора строк следует использовать следующий метод.
table.setRowSelectionAllowed(false)
EditEdit11 Cells11 CellsColumns f X4E I 5X ColumnsIgColumns f X4E I 5X ColumnsIg4678 "9104678 "910"",8 10,12. И 4 J16 38 20,8 10,12. И 4 J16 38 2032151821 '24273032151821 '24273036 '20 24 28 32 3 6 4036 '20 24 28 32 3 6 4030152025*303540455030152025*30354045506321182430364248546063211824303642485460СС•73421 '2 835•4249 '5663 '70•73421 '2 835•4249 '5663 '70i8 36 '24 32*40 '48 56 64 72 80i8 36 '24 32*40 '48 56 64 72 80HHJ38273645J38273645728190728190J IQ.iZO3040заJ IQ.iZO3040заisoted......100isoted......100
Edit
Edit
11 Cells
11 Cells
Columns f X4E I 5X Columns
Ig
Columns f X4E I 5X Columns
Ig
4
6
7
8 "
9
10
4
6
7
8 "
9
10
"
"
,8 10
,12. И 4 J16 38 20
,8 10
,12. И 4 J16 38 20
32
15
18
21 '24
27
30
32
15
18
21 '24
27
30
36 '20 24 28 32 3 6 40
36 '20 24 28 32 3 6 40
30
15
20
25
*30
35
40
45
50
30
15
20
25
*30
35
40
45
50
6
32
118
24
30
36
42
48
54
60
6
32
118
24
30
36
42
48
54
60
С
С
•7
34
21 '2 8
35
•42
49 '56
63 '70
•7
34
21 '2 8
35
•42
49 '56
63 '70
i8 36 '24 32
*40 '48 56 64 72 80
i8 36 '24 32
*40 '48 56 64 72 80
H
H
J
38
27
36
45
J
38
27
36
45
72
81
90
72
81
90
J IQ.iZO3040за
J IQ.iZO3040за
iso
ted......
100
iso
ted......
100
Рис. 6.36. Пример выбора строки
В режиме выбора строк можно разрешить пользователям выбирать только одну строку, несколько смежных строк или несколько несмежных строк. Для этого нужно получить модель выбора и использовать ее метод setSelectionMode.
table.getSelectionModel().setSelectionMode(mode) ;
В качестве параметра mode можно использовать приведенные ниже значения, которые обозначают допустимые способы выбора строк: только одну строку, несколько смежных строк или несколько несмежных строк соответственно.
ListSelectionModel.SINGLE-SELECTION
ListSelectionModel.SINGLE_INTERVAL_SELECTION
ListSelectionModel.MULTIPLE-INTERVAL-SELECTION
По умолчанию режим выбора строк отключен и его можно включить с помощью приведенного ниже метода.
table.setColuinnSelectionAllowed(true);
Одновременное включение режимов выбора строк и столбцов эквивалентно включению режима выбора ячеек, который показан на рис. 6.37. Этот режим можно указать явно, используя показанный ниже метод.
table.setCellSelectionEnabled(true);
| Selection| Selectionee2_3456789102_3456789104466DE 4531СDE 4531С81012141618208101214161820j112 .151645'20 --2S18j112 .151645'20 --2S181214is ’rJ30?6411214is ’rJ30?641«to273645546340'6070«to273645546340'6070EHEHUS324048566472180US324048566472180^ _110182051_. E^ _110182051_. E3645546372819036455463728190*40501б0‘'70'8090100*40501б0‘'70'8090100
| Selection
| Selection
e
e
2_
3
4
5
6
7
8
9
10
2_
3
4
5
6
7
8
9
10
4
4
6
6
DE 45
31С
DE 45
31С
8
10
12
14
16
18
20
8
10
12
14
16
18
20
j112 .15
16
45'20 --2S
18
j112 .15
16
45'20 --2S
18
1214
is ’rJ
30
?641
1214
is ’rJ
30
?641
«to
2736455463
40'6070
«to
2736455463
40'6070
EH
EH
US
32
40
48
56
64
72
180
US
32
40
48
56
64
72
180
^ _110
1820
51_. E
^ _110
1820
51_. E
36
45
54
63
72
81
90
36
45
54
63
72
81
90
*40
50
1б0‘
'70
'80
90
100
*40
50
1б0‘
'70
'80
90
100
6.37. Пример выбора ячеек
В прежних версиях библиотеки Swing при указании режима выбора строк и столбцов после щелчка мыши на ячейке одновременно выбирались строка и столбец этой ячейки.
Для определения выбранных строк и столбцов используются методы getSelectedRows и которые возвращают массив int[] с индексами выбранных элементов.
Код программы с режимом выбора ячеек приводится в листинге 6.14. В этом примере для указания режима выбора используется команда меню Selection (Выбор). Попробуйте указать разные режимы выбора строк, столбцов и ячеек с помощью этих команд.
Скрытие и отображение столбцов
Методкласса JTable позволяет удалить столбец (аргумент
из представления таблицы, т.е. скрыть ее от пользователя, а не удалить из модели таблицы. Ниже показан способ извлечения фактического объекта- столбца таблицы из модели таблицы по известному номеру столбца (например, полученному с помощью метода getSelectedColumns).
TableColumnModel columnModel =
TableColumn column = columnModel.getColumn(i);
table.removeColumn(column);
Если этот объект-столбец запомнить, то впоследствии его можно вернуть обратно.
table.addColumn(column);
Этот метод добавляет столбец в конец таблицы, а для указания нужного расположения следует использовать метод movecolumn.
Кроме того, можно создать новый столбец, который соответствует индексу столбца в модели таблицы, создавая новый объект TableColumn.
table.addColumn(new TableColumn(modelcolumnindex));
Таким образом в таблице можно создать несколько столбцов, которые будут представлять один и тот же столбец из модели таблицы.
Однако, в классе JTable не предусмотрено никаких методов для вставки нового столбца в модель таблицы или удаления столбца из модели таблицы. В классе JTable не существует также методов для скрытия строк. Для этого придется создать модель- фильтр, аналогично описанному выше фильтру сортировки.
Вставка и удаление строк в используемой по умолчанию модели таблицы
Класс используемой по умолчанию модели таблицы Defau1tTab1eMode1 основан на интерфейсе TableModel и содержит двумерную решетку объектов. Если используемые данные уже имеют табличный формат, то их не рекомендуется копировать в предлагаемую по умолчанию модель таблицы. Ее удобно использовать только для небольшого набора данных. В классе De f au11TabieMode1 предусмотрены методы вставки строк и столбцов, а также удаления строк.
Для создания строк и столбцов с новыми данными используются методы addRow и addColumn на основе массива Object[ ] или вектора с новыми данными. В методе addColumn нужно также указать имя нового столбца. С их помощью новые данные будут добавлены в конце решетки. Для вставки строки в середине таблицы можно использовать метод insertRow, а для вставки столбца в середине таблицы никакого метода не предусмотрено.
Аналогично, для удаления строки можно использовать метод removeRow, но для удаления столбца никакого аналогичного метода не предусмотрено.
Объект JTable регистрируется как слушатель модели таблицы, поэтому модель всегда уведомляется о вставке и удалении данных с обновлением отображаемых сведений.
В листинге 6.14 показан пример выбора и редактирования данных, в котором предлагаемая по умолчанию модель таблицы содержит очень простой набор данных (обычную таблицу умножения). Список команд меню Edit (Редактирование) содержит несколько команд, которые выполняют следующие действия.
Скрытие выбранных столбцов.
Отображение всех скрытых столбцов.
Удаление выбранных строк из модели таблицы,
Создание строки данных в конце модели таблицы.
На этом примере завершается рассмотрение компонентов-таблиц библиотеки Swing. В целом таблицы имеют более понятную структуру, чем деревья, потому что базовая модель данных— решетка объектов— имеет более простое визуальное представление, чем дерево объектов. Однако, на самом деле таблица устроена гораздо сложнее, чем Заголовки столбцов, столбцы с изменяемыми размерами, а также специальные классы-рисовальщики и классы-редакторы значительно усложняют работу с таблицами. В этом разделе внимательно рассмотрены наиболее распространенные способы применения таблиц: отображение информации из базы данных, сортировка строк, пользовательские классы-рисовальщики и классы- редакторы. Более подробно описание таблиц можно найти в книгах Java Foundation Classes Кима Топли (Kim Topley) [Prentice-Hall 1998] или в книге Graphic Java 2 Дэ-
M. Гири (David M. Geary) [Prentice-Hall 1999].
/**
1.01 2001-07-21
@author Cay
*/
import
j
import
j
import
import avax.table.*;
/**
Пример выбора, создания и удаления строк и столбцов */
public class TableSelectionTest
{
public static void main(String[] args)
{
frame = new
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
}
/**
Фрейм с таблицей умножения и меню для
установки режима выбора строк/столбцов/ячеек, а также для создания и удаления строк и столбцов
*/
classextends JFrame
{
public
{
setTitle(“TableSelectionTest");
HEIGHT);
// Создать таблицу умножения
model = new10);
for (inti= 0; i<i++)
for (int j =0; j <j++)
model,setValueAt(
new+1) *+ 1)), i,
table = new
Container contentPane =
contentPane.add(new"Center");
= new
// Создать меню
= new
setJMenuBar(menuBar);
= new
menuBar.add(selectionMenu);
final
= new
final JCheckBoxMenuItem
= new
final JCheckBoxMenuItem
= new
rowsltem.setSelected(table,getRowSelectionAllowed()); columnsltem.setSelected(table.getColumnSelectionAllowedt)); cellsltem.setSelected(table.getCellSelectionEnabled());
rowsltem.addActionListener(new
ActionListener()
{ public void actionPerformed(ActionEvent event) {
table.clearSelection();
table.setRowSelectionAllowed(
rowsltem.isSelected());
cellsltem.setSelected(
table.getCellSelectionEnabled()); }
));
selectionMenu.add(rowsltem);
columnsltem.addActionListener(new
ActionListener()
{ public void actionPerformed(ActionEvent event) {
table.clearSelection() ;
table.setColumnSelectionAllowed(columnsltem.isSelected());
cellsltem.setSelected(
table.getCellSelectionEnabled());
} });
selectionMenu.add(columnsItem);
cellsltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
table.clearSelectiont);
table.setCellSelectionEnabled(cellsltem.isSelectedf));
rowsltem.setSelected(
table.getRowSelectionAllowed());
columnsltem.setSelectedt
table.getColumnSelectionAllowed());
}
}),-
selectionMenu.add(cellsltem) ;
JMenu tabi eMenu=newJMenu("Edit");
menuBar.add(tableMenu);
JMenuItem hideColumnsItem = new JMenuItem("Hide Columns") ; HideColumnsItem.addActionListener(new
ActionListener()
public void actionPerformed(ActionEvent event)
int[] selected= table.getSelectedColumns();
TableColumnModel columnModel =
// Удаление столбцов из представления, // начиная с последнего индекса, так чтобы //не нарушить нумерацию столбцов
for (int i = selected.length — 1; i >= 0; i--)
{
TableColumn column =
■table.removecolumn(column);
// Сохранение удаленных столбцов
// для выполнения последующей команды
// "восстановить столбцы"
removedColumns.add(column);
}
}
}) ;
tableMenu.add(hideColumnsItem);
JMenuItem showColumnsItem = new JMenuItem (" Show Columns"); showColumnsItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
// Восстановить все удаленные столбцы
for (int i = 0; i < removedColumns.size(); i++) table.addColumn(
'(Tablecolumn)removedColumns.get(i));
removedColumns.clear();
)
}) ;
tableMenu.add(showColumnsItem);
JMenuItem addRowItem = new JMenuItem("AddRow");
addRowItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
// Создать новую строку в таблице
// умножения в модели таблицы
Integer[] newCells
= new Integer[model.getColumnCount()];
for (int i = 0; i < newCells.length; i++) newCells[i] = new Integer((i + 1)
* (model.getRowCount() + 1));
model.addRow(newCells) ;
}
});
tableMenu.add(addRowItem);
JMenuItem removeRowsItem = new JMenu11 em("Remove Rows"); removeRowsItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
int[] selected = table.getSelectedRows();
// Удалить строки из модели таблицы, начиная
//с последнего индекса таким образом, чтобы //не повлиять на нумерацию строк
for (int i = selected.length — 1; i >= 0; i--) model.removeRow(selected[i]);
}
}) ;
tableMenu.add(removeRowsItem) ;
JMenuItem clearCellsItem = new JMenuItem("Clear Cells");
clearCellsItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
// Задать для выбранных ячеек значение 0
for (int i = 0; i < table.getRowCount(); i++)
for (int j = 0; j < table.getColumnCount(); j++) if (table.isCellSelected(i, j))
table.setValueAt(new Integer(0), i, j ) ;
)
В ;
tableMenu.add(clearCellsItem);
private DefaultTableModel model;
private JTable table;
privateArrayList removedColumns;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
javax.awing.JTable
Метод void setAutoResizeMode (int mode) указывает режим автоматического изменения размеров столбцов таблицы.
Параметры:
modeодно из следующих значений: AUTO_RESIZE_OFF,
AUTO_RESIZE_NEXT_COLUMN,
AUTO_RESIZE_SUBSEQUENT_COLUMNS,
AUTO_RESIZE_LAST_COLUMN, AUTO_RESIZE_ALL_COLUMNS
Метод ListSelectionModel getSelectionModel () возвращает список режимов выбора строк, столбцов и ячеек.
Метод void setRowSelectionAllowed(boolean b) указывает на допустимость выбора строк после щелчка мыши, если параметр b имеет значение true.
Метод void setColumnSelectionAllowed(boolean b) указывает на допустимость выбора столбцов после щелчка мыши, если параметр b имеет значение true.
Метод void setCellSelectionAllowed(boolean b) указывает на допустимость выбора отдельных ячеек после щелчка мыши, если параметр b имеет значение true. Вызов этого метода эквивалентен вызову методов setRowSelectionAllowed(b) и setColumnSelectionAllowed(b).
Метод boolean getRowSelectionAllowed() возвращает значение true, если разрешается выбор строк.
Метод boolean getColumnSelectionAllowed() возвращает значение true, если разрешается выбор столбцов.
Метод boolean getCellSelectionAllowed() возвращает значение true, если разрешается выбор строк столбцов.
Метод void addColumn(TableColumn column) создает столбец в представлении таблицы.
Метод void moveColumn(int from, int to) перемещает столбец с индексом fromв положение с индексом to, но только в представлении таблицы.
Метод void removeColumn(TableColumn column) удаляет столбец из представления таблицы.
j avax. swing. table. TableColumn
Конструктор TableColumn(int modelcolumnindex) создает столбец таблицы для просмотра модели столбца с заданным индексом.
e Метод void setPre f eredWidth(int width) задает предпочтительную ширину столбца, которая указана в параметре width.
Метод void setMinWidth(int width) задает минимальную ширину столбца, которая указана в параметре width.
Метод void setMaxWidth(int width) задает максимальную ширину столбца, которая указана в параметре width.
Метод void setWidth(int width) задает фактическую ширину столбца, которая указана в параметре width.
Метод void setResizable (boolean b) указывает, что данный столбец имеет изменяемую ширину, если значение параметраbравно true.
javax. swing. ListSelectionModel
Метод void setSelectionMode (int mode) задает режим выбора.
Параметры:
modeодно из следующих значений: SINGLE-SELECTION,
SINGLE_INTERVAL_SELECTIONи
MULTIPLE_INTERVAL_SELECTION
j avax. swing. table. DefaultTableModel
Конструкторы void addRow(Object [] rowData) и void addCol- umn(Object columnName, Object[] columnData) создает строку и стол-
бец данных в конце модели таблицы.
Метод void insertRow(int row, Object[] rowData) создает строку данных с индексом row.
Метод void removeRow (int row) удаляет строку данных с индексом row.
Метод void moveRow(int start, int end, int to) перемещает все строки с индексами между start и end в новое место с индексом to.
Стилизованныетекстовыекомпоненты
В томе 1 рассматриваются базовые классы текстовых компонентов JTextField и JTextArea, которые предназначены для организации ввода данных со стороны пользователя. Наряду с этими очень полезными классами существует еще один полезный класс JEditorPane, который позволяет отображать и редактировать текст в формате HTML и RTF. (Формат Rich Text Format (RTF) используется в приложениях Microsoft для обмена документами. Этот формат не очень хорошо документирован и не совсем удовлетворительно работает даже в приложениях Microsoft. В этой книге не рассматриваются возможности этого формата. К тому же следует отметить, что компания Sun гарантирует только ограниченную поддержку этого формата. Вполне возможно, что в будущем его поддержка вообще будет прекращена в пользу формата HTML.)
Возможности класса-редактора JEditorPane очень ограничены. Его HTML- рисовальщик может отображать простые файлы, но не справляется с более сложны-
ми HTML-файлами, которые часто можно встретить в Web-среде. К тому же этот HTML-редактор обладает ограниченными возможностями и работает не очень стабильно. Следует заметить, что эти недостатки не имеют очень большого значения, потому что редактировать HTML-текст обычно требуется только в специализированных приложениях — редакторах HTML-текста.Наиболее оптимальным использованием компонента JEditorPane, вероятно, является создание справки программы в формате HTML. Задавая оптимальную структуру файлов справки, можно обойти все те ограничения, которые накладываются на компонент JEditorPane.ми HTML-файлами, которые часто можно встретить в Web-среде. К тому же этот HTML-редактор обладает ограниченными возможностями и работает не очень стабильно. Следует заметить, что эти недостатки не имеют очень большого значения, потому что редактировать HTML-текст обычно требуется только в специализированных приложениях — редакторах HTML-текста.Наиболее оптимальным использованием компонента JEditorPane, вероятно, является создание справки программы в формате HTML. Задавая оптимальную структуру файлов справки, можно обойти все те ограничения, которые накладываются на компонент JEditorPane.Для получения более подробной информации о профессиональных инструментах создания справочных систем следует обратиться по адресу http://java.sun.com/ produc t s/javahelp/index.html.Для получения более подробной информации о профессиональных инструментах создания справочных систем следует обратиться по адресу http://java.sun.com/ produc t s/javahelp/index.html.Подчиненный класс JTextPane класса JEditorPane может содержать стилизованный текст со специальными шрифтами, форматированием и вставленными компонентами. Этот класс-компонент не рассматривается в этой книге, но его использование демонстрируется в примере stylePad, который включен в состав SDK.Подчиненный класс JTextPane класса JEditorPane может содержать стилизованный текст со специальными шрифтами, форматированием и вставленными компонентами. Этот класс-компонент не рассматривается в этой книге, но его использование демонстрируется в примере stylePad, который включен в состав SDK.В листинге 6.15 представлена панель редактирования, которая может отображать содержание HTML-страницы. Введите URL-указатель в текстовое поле с префиксом http: или file:,а потом щелкните на кнопке Load (Загрузить). После этого в панели редактирования будет отображена заданная страница, как показано на рис. 6.38.В листинге 6.15 представлена панель редактирования, которая может отображать содержание HTML-страницы. Введите URL-указатель в текстовое поле с префиксом http: или file:,а потом щелкните на кнопке Load (Загрузить). После этого в панели редактирования будет отображена заданная страница, как показано на рис. 6.38.Java™ 2 SDK, Standard EditionDocumentationJava™ 2 SDK, Standard EditionDocumentationConteConteJavaJavaVersion 1.4.0Version 1.4.0General fnfo API & Language Guide to Features Tool. Docs DemasJTutonaisGeneral fnfo API & Language Guide to Features Tool. Docs DemasJTutonaisYour feedback is important to us. Please send us comments: Contacting lava™ Software.Your feedback is important to us. Please send us comments: Contacting lava™ Software.General InformationGeneral InformationLocationLocationReadme, Overview, ChangesReadme, Overview, ChangesComnatibiliComnatibili>Baife^ Editable)>Baife^ Editable)tll£///usr/lo^ 4.0/dpcs/index.htmLtll£///usr/lo^ 4.0/dpcs/index.htmLdocs websitedocs websiteChanges anti Release NatesChanges anti Release NatesРис. 6.38. Панель редактирования с отображением HTML-страницыРис. 6.38. Панель редактирования с отображением HTML-страницы
ми HTML-файлами, которые часто можно встретить в Web-среде. К тому же этот HTML-редактор обладает ограниченными возможностями и работает не очень стабильно. Следует заметить, что эти недостатки не имеют очень большого значения, потому что редактировать HTML-текст обычно требуется только в специализированных приложениях — редакторах HTML-текста.
Наиболее оптимальным использованием компонента JEditorPane, вероятно, является создание справки программы в формате HTML. Задавая оптимальную структуру файлов справки, можно обойти все те ограничения, которые накладываются на компонент JEditorPane.
ми HTML-файлами, которые часто можно встретить в Web-среде. К тому же этот HTML-редактор обладает ограниченными возможностями и работает не очень стабильно. Следует заметить, что эти недостатки не имеют очень большого значения, потому что редактировать HTML-текст обычно требуется только в специализированных приложениях — редакторах HTML-текста.
Наиболее оптимальным использованием компонента JEditorPane, вероятно, является создание справки программы в формате HTML. Задавая оптимальную структуру файлов справки, можно обойти все те ограничения, которые накладываются на компонент JEditorPane.
Для получения более подробной информации о профессиональных инструментах создания справочных систем следует обратиться по адресу http://java.sun.com/ produc t s/javahelp/index.html.
Для получения более подробной информации о профессиональных инструментах создания справочных систем следует обратиться по адресу http://java.sun.com/ produc t s/javahelp/index.html.
Подчиненный класс JTextPane класса JEditorPane может содержать стилизованный текст со специальными шрифтами, форматированием и вставленными компонентами. Этот класс-компонент не рассматривается в этой книге, но его использование демонстрируется в примере stylePad, который включен в состав SDK.
Подчиненный класс JTextPane класса JEditorPane может содержать стилизованный текст со специальными шрифтами, форматированием и вставленными компонентами. Этот класс-компонент не рассматривается в этой книге, но его использование демонстрируется в примере stylePad, который включен в состав SDK.
В листинге 6.15 представлена панель редактирования, которая может отображать содержание HTML-страницы. Введите URL-указатель в текстовое поле с префиксом http: или file:,а потом щелкните на кнопке Load (Загрузить). После этого в панели редактирования будет отображена заданная страница, как показано на рис. 6.38.
В листинге 6.15 представлена панель редактирования, которая может отображать содержание HTML-страницы. Введите URL-указатель в текстовое поле с префиксом http: или file:,а потом щелкните на кнопке Load (Загрузить). После этого в панели редактирования будет отображена заданная страница, как показано на рис. 6.38.
Java™ 2 SDK, Standard Edition
Documentation
Java™ 2 SDK, Standard Edition
Documentation
Conte
Conte
Java
Java
Version 1.4.0
Version 1.4.0
General fnfo API & Language Guide to Features Tool. Docs DemasJTutonais
General fnfo API & Language Guide to Features Tool. Docs DemasJTutonais
Your feedback is important to us. Please send us comments: Contacting lava™ Software.
Your feedback is important to us. Please send us comments: Contacting lava™ Software.
General Information
General Information
Location
Location
Readme, Overview, Changes
Readme, Overview, Changes
Comnatibili
Comnatibili
>Baife^ Editable)
>Baife^ Editable)
tll£///usr/lo^ 4.0/dpcs/index.htmL
tll£///usr/lo^ 4.0/dpcs/index.htmL
docs website
docs website
Changes anti Release Nates
Changes anti Release Nates
Рис. 6.38. Панель редактирования с отображением HTML-страницы
Рис. 6.38. Панель редактирования с отображением HTML-страницы
В этом окне все гиперссылки активны, т.е. после щелчка на каждой ссылке это приложение загрузит указанную Web-страницу. Причем после щелчка на кнопке Back (Назад) будет отображена предыдущая страница.
Фактически эта программа представляет собой простейший Web-броузер. Конечно, в нем нет таких комфортных инструментов, как, например, кэш-память страниц или список использованных ссылок, которые присутствуют в коммерческих Web- броузерах. Кроме того, эта программа не способна отображать аплеты.
После установки флажка параметра Editable (Редактируемый) разрешается редактировать содержимое этой панели. Теперь в ней можно вводить текст и использовать клавишу <Backspace> для удаления текста. Этот компонент также может обрабатывать стандартные комбинации клавиш <Ctrl+X>, <Ctrl+C> и <Ctrl+V>, которые выполняют функции вырезания, копирования и вставки текста. Однако для создания этой программы придется затратить немалые усилия на программирование поддержки шрифтов и форматирования.
При редактировании компонента гиперссылки не являются активными. Кроме того, в режиме редактирования в некоторых Web-страницах будут видны команды JavaScript, комментарии и другие теги, как показано на рис. 6.39. В этом примере режим редактирования разрешается использовать в учебных целях, но в рабочих программах его не рекомендуется использовать.
Рис. 6.39. Панель просмотра HTML-страниц в режиме редактированияРис. 6.39. Панель просмотра HTML-страниц в режиме редактированияПо умолчанию компонентJEditorPane находится в режиме редактирования, а для его отключения можно использовать метод editorPane.setEditable (false).
Рис. 6.39. Панель просмотра HTML-страниц в режиме редактирования
Рис. 6.39. Панель просмотра HTML-страниц в режиме редактирования
Перечисленные в этой программе функции панели редактирования можно достаточно просто использовать. Для загрузки документов следует использовать метод setPage с параметром в виде URL-строки или объекта URL. Класс JEditorPane основан на классе JTextComponent, поэтому для отображения простого текста можно вызвать его метод setText.
Для обработки щелчков мыши на гиперссылках следует создать класс-слушатель HyperlinkListener, который имеет единственный метод hyper1inkupdate с па-
раметром типа HyperlinkEvent. Этот метод вызывается при перемещениях мыши над гиперссылками и щелчках кнопкой мыши на гиперссылках.
Далее нужно вызвать метод getEventType для определения одного из трех перечисленных ниже типов события.
HyperlinkEvent.EventType.ACTIVATED
HyperlinkEvent.EventType.ENTERED
HyperlinkEvent.EventType.EXITED
Значение HyperlinkEvent. EventType. ACTIVATED обозначает событие щелчка кнопкой мыши на гиперссылке, например, для открытия указанной Web-страницы. Значения HyperlinkEvent. EventType. ENTERED и HyperlinkEvent .EventType. EXITED обычно используются для организации визуальной обратной связи, например для отображения подсказки при расположении указателя мыши над гиперссылкой.
Остается загадкой ответ на вопрос, почему разработчики Swing не создали три отдель-
| ных метода для управления этими событиями в интерфейсе HyperlinkListener.
D
Метод getURL класса HyperlinkEvent возвращает URL-указатель данной гиперссылки. Ниже представлен способ создания класса-слушателя для активированных пользователем гиперссылок.
editorPane.addHyperlinkListener(new
HyperlinkListener()
{
public void hyperlinkupdate(HyperlinkEvent event)
{
if (event.getEventType()
==
{
try
{
editorPane.setPage(event.getURL());
}
catch(lOException e)
{ editorPane.setText("Exception: "+e);
)
}
)
));
Обработчик события просто передает URL-указатель и обновляет панель редактирования. Метод setPage может генерировать исключительную ситуацию IOException с отображением сообщения об ошибке в виде простого текста без форматирования.
В листинге 6.15 приводится полный код рассмотренного здесь примера справочной системы с использованием HTML-формата. В общем случае компонент JEditorPane имеет более сложную структуру, чем таблица или дерево. Однако, если на его основе не требуется создавать специальные текстовые редакторы и рисовальщики для работы со сложным пользовательским текстовым форматом, то вся эта сложность будет скрыта от программиста.
ЛистингЙ.15. EdltorPaneTest.java
/**
©version 1.01 2001-07-22
©author Cay Horstmann
*/
import java.awt.* ;
import java.awt.event. *;
import java.io. *;
import java.util.* ;
import j import j
/**
Пример отображения HTML-документов в панели редактирования
*/ public class EditorPaneTest
{
public static void main(String[] args) {
JFrame frame = new EditorPaneFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
} }
/**
Этот фрейм содержит панель редактирования, текстовое поле, кнопку для ввода
URL-указателя и загрузки документа, а также кнопку Back для возврата к ранее загруженному документу */ class EditorPaneFrame extends JFrame {
public EditorPaneFrame() {
setTitle("EditorPaneTest");
setsize(WIDTH, HEIGHT);
final Stack urlStack = new Stack();
final JEditorPane editorPane = new JEditorPane() ; final JTextField url = new JTextFieldOO) ;
// Создание класса-слушателя гиперссылок
editorPane.setEditable(false);
editorPane.addHyperlinkListener(new
HyperlinkListener() {
public void hyperlinkupdate(HyperlinkEvent event) {
if (event.getEventType() ==
{
try
{
// Запомнить URL-указатель
// для кнопки возврата
uristack.push(event.getURL().toString());
// Показать URL-указатель в текстовом поле
ur1.setText(event.getURL().toString() ) ;
editorPane.setPage(event.getURL());
}
catch(lOException e)
{
editorPane.setText("Exception: " +e);
}
}
D ;
// Создание флажка параметра включения/отключения
// режима редактирования
final JCheckBox editable = new JCheckBox();
editable.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
editorPane.setEditable(editable.isSelected()) •
// Создание кнопки для загрузки указанного URL-pecypca
ActionListener listener = new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
try
{
// Запоминание URL-указателя для возврата urlStack.push(url.getText() ) ;
editorPane.setPage(url.getText());
}
catch(lOException e)
{
editorPane.setText("Exception: " + e) ;
}
}
};
JButton loadButton = new JButton("Load")
loadButton.addActionListener(listener);
url.addActionListener(listener);
// Создание кнопки возврата
JButton backButton = new JButton("Back");
backButton.addActionListener(new
ActionListener()
public void actionPerformed(ActionEvent event) {
if (urlStack.size() <= 1) return;
try
{
// Получить URL-указатель из кнопки возврата urlStack.popO ;
// Показать URL-указатель в текстовом поле String urlString = (String)urlStack.peek();
url.setText(urlString);
editorPane.setPage(urlString);
}
catch(lOException e)
{
editorPane.setText("Exception: " + e) ;
)
Container contentPane = getContentPane();
contentPane.add(new JScrollPane(editorPane) , BorderLayout.CENTER);
// Расположить все элементы управления в панели
JPanel panel = new JPanel();
panel.add(new JLabel("URL"));
panel.add(url);
panel.add(loadButton) ;
panel.add(backButton);
panel.add(new JLabel("Editable”));
panel.add(editable);
contentPane.add(panel, BorderLayout.SOUTH);
private static final int WIDTH = 600;
private static final int HEIGHT = 400;
javax.swing.JEditorPane
Метод void setPage(URL url) загружает в панель редактирования Web-страницу, указанную в параметре url.
Метод void addHyperlinkListener(HyperlinkListener listener)
создает в панели редактирования класс-слушатель гиперссылок.
javax. swing, event. HyperlinkListener
e Метод void hyperlinkupdate(HyperlinkEvent event) вызывается в каждом случае выбора гиперссылки.
javax.swing. event. HyperlinkEvent.
* Метод URL getURL () возвращает URL-указатель выбранной гиперссылки.
Организаторы компонентов
В заключение обсуждения сложных компонентов библиотеки Swing рассмотрим способы организации компонентов приложения. Для этого используются панели с границей раздела (splitpane), в которых область окна разбивается на несколько областей меньшего размера с управляемыми границами, панели с вкладками (tabbed pane), в которых пользователь, щелкая мышью, может переходить от одной вкладки к другой, а также панель рабочего стола (desktop pane), в которой можно размещать несколько внутренних фреймов (internalframe).
Панели с границей раздела
Панели с границей раздела используются для разбиения компонента на две части с настраиваемой границей. На рис. 6.40 показан пример разбиения области окна на три панели: горизонтальная граница отделяет нижнюю панель с текстовым полем от двух верхних панелей, а вертикальная граница отделяет список планет слева от изображения выбранной планеты справа.
Рис. 6.40. Пример двух вложенных панелей с границей раздела
При создании такой панели с границей раздела нужно указать ориентацию границы раздела (JSplitPane.HORIZONTAL-SPLIT для горизонтального разделения и JSplitPane.VERTICAL-SPLIT для вертикального разделения) и два полученных компонента-панели.
JSplitPane innerPane
= new JSplitPane(JSplitPane.HORIZONTAL-SPLIT, planetList, planetimage);
По желанию можно добавить пиктограммы полного развертывания панелей ("one touch expand"), которые показаны на рис. 6.40 в виде маленьких треугольных стрелок (для макета Metal). После щелчка на такой пиктограмме граница раздела переместится в указанном направлении для полного раскрытия панели.
Для использования этой функциональной возможности нужно вызвать следующий метод.
innerPane.setOneTouchExpandable(true);
Кроме того, можно использовать функцию постоянной перерисовки ("continuous layout") содержимого панели по мере перемещения границы раздела.
innerPane.setContinuousLayout(true);
Эта функция очень эффектно выглядит, но очень медленно выполняется. В этом примере эта функция не используется для горизонтальной границы раздела, а потому содержимое нижней панели будет перерисовано только после перемещения границы раздела.
Программа, код которой приведен в листинге 6.16, имеет очень простой принцип работы. Сначала создается список с именами планет. Если пользователь выбирает какую-то планету, то в панели справа отображается ее рисунок, а в панели снизу— ее описание. После запуска программы настройте границы раздела и попробуйте использовать функции полного развертывания панели и постоянной перерисовки панели.
/**/**6.16. SpHtPaneTeat. java * '. .6.16. SpHtPaneTeat. java * '. .
/**
/**
6.16. SpHtPaneTeat. java * '. .
6.16. SpHtPaneTeat. java * '. .
©version 1.01 2001-07-22
@author Cay Horstmann
*/
import j ava.awt.*;
import java.awt.event.*;
import java.util.*;
import j avax.swing.*;
import javax.swing.event.*;
/**
Использование панели с границей раздела в качестве организатора компонентов
*/
public class SplitPaneTest
{
public static void main(String[] args)
{
JFrame frame = new SplitPaneFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
)
/**
Этот фрейм содержит две вложенных панели с границей раздела для отображения рисунков и описания планет */ class SplitPaneFrame extends JFrame {
public SplitPaneFrame()
{
setTitle("SplltPaneTest”) ;
setsize(WIDTH,HEIGHT);
// Создание компонентов для имен, // рисунков и описаний планет
final JList planetList = new JList(planets);
final JLabel planetimage = new JLabel();
final JTextArea description = new JTextArea();
planetList.addListSelectionListener(new
ListSelectionListener()
{
public void valueChanged(ListSelectionEvent event) {
Planet value
=
// Обновить рисунки планет
planetimage.seticon(value.getlmage()) ;
description.setText(value.getDescription());
)
});
// Установка панелей с границей раздела
JSplitPane innerPane
new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, planetList, planetlmage);
innerPane.setContinuousLayout(true);
innerPane.setOneTouchExpandable(true);
JSplitPane outerPane
new JSplitPane(JSplitPane.VERTICAL_SPLIT, innerPane, description);
getContentPane().add(outerPane, BorderLayout.CENTER);
private Planet[] planets =
{
new Planet("Mercury", 2440, 0), new Planet("Venus", 6052, 0), new Planet("Earth", 6378, 1), new Planet("Mars", 3397, 2), new Planet("Jupiter", 71492, 16), new Planet("Saturn", 60268, 18) , new Planet("Uranus", 25559, 17), new Planet("Neptune", 24766, 8), new Planet("Pluto", 1137, 1),
};
private static final int WIDTH = 300;
} private static final int HEIGHT = 200;
/**
Описание планеты
*/
class Planet {
/**
Создание планеты
Sparain n the planet name
Sparam r the planet radius
©param m the number of moons
*/
public Planet(String n, double r, int m)
{ name = n;
radius = r;
moons = m;
image = new Imageicon(name + ".gif");
J
public String toStringO
{
return name;
}
/**
Получить описание планеты
©return the description
*/
public String getDescription()
{
j return "Radius: " + radius + “ \nMoons: " + moons + "\n";
/**
Получить рисунок планеты
©return the image
*/
public Imageicon getlmage()
{
return image;
}
private String name;
private double radius;
private int moons;
private Imagelcon image;
javax.swing.JSplitPane
Конструкторы JSplitPane (), JSplitPane (int direction), JSplit- ■■1Pane (int direction, boolean continuousLayout), JSplitPane(int
direction, Component first , Component second), JSplitPane(int direction, boolean continuousLayout, Component first, Component second) создают новую панель с границей раздела.
Параметры:
directionимеет одно из значений: HORIZONTAL_SPLIT или
VERTICAL_SPLIT
continuousLayout имеет значение true, если содержимое компонентов постоянно обновляется при перемещении границы раздела
firstпервый компонент
secondвторой компонент
Метод boolean isOneTouchExpandable() возвращает значение свойства для полного развертывания панели. При установлении значения true для
этого свойства, допускается полное свертывание/развертывание панели с помощью пиктограмм-стрелок.
Метод void setOneTouchExpandable (boolean b) задает значение свойства для полного развертывания панели. При установлении значения true для этого свойства, допускается полное свертыва- ние/развертывание панели с помощью пиктограмм-стрелок.
Методboolean isContinuousLayout () возвращает значение свойства постоянной перерисовки панели при перемещении границы раздела. При установлении значения true для этого свойства, допускается постоянная перерисовка панели при перемещении границы раздела.
Методvoid setContinuousLayout (boolean b) задает значение свойства постоянной перерисовки панели при перемещении границы раздела. При установлении значения true для этого свойства, допускается постоянная перерисовка панели при перемещении границы раздела.
Методы setLeftComponent (Componentс)и setTopComponent
(Component c) имеют TOT же эффект, что и указание компонента с в качестве первого компонента в панели с границей раздела.
Методы setRightComponent(Component с) и setBottomComponent (Component с) имеют тот же эффект, что и указание компонента с в качестве второго компонента в панели с границей раздела.
Панели с вкладками
Панели с вкладками можно использовать для создания очень удобного пользовательского интерфейса. При этом сложное диалоговое окно разбивается на несколько частей с группами связанных по смыслу параметров. Вкладки можно также применять для просмотра группы документов или рисунков, как показано на рис. 6.41.
Рис. 6.41. Панель с вкладками
Для создания панели с вкладками нужно прежде всего создать объект JTabbedPane, а затем включить в него вкладки.
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab(title, icon, component);
Последний параметр метода addTab имеет тип Component. Для включения нескольких компонентов в одну и ту же вкладку нужно сначала упаковать их в контейнер (например, вJpanel).
Пиктограмму icon можно не указывать, и для этого предусмотрен другой вариант метода addTab.
tabbedPane.addTab(title, component);
Вкладку можно включить в нужном месте панели, указывая ее индекс.
tabbedPane.insertTab(title, icon, component, index);
Для удаления вкладки из набора вкладок следует использовать метод removeTabAt.
tabbedPane.removeTabAt(index) ;
Новая вкладка не будет отображена автоматически. Для этого придется выбрать ее с помощью метода setSelectedlndex. Например, для отображения только что добавленной вкладки следует использовать следующий метод.
tabbedPane.setSelectedlndex(tabbedPane.getTabAcount() — 1);
Для отображения большого количества вкладок может потребоваться большое пространство. Для этого в SDK 1.4 предусмотрена полоса прокрутки вкладок. При этом отображается только один ряд вкладок и набор стрелок для прокрутки остальных вкладок, как показано на рис. 6.42.
Для указания режима полного отображения или прокрутки вкладок используются следующие методы.
tabbedPane.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
T ,bb dFin Л ,t
Wrap tihs 4 Scroll tabi
Рис. 6.42. Панель с полосой прокрутки вкладок
В этом примере демонстрируется интересный способ использования панели с вкладками. Иногда перед отображением содержимого компонента требуется обновить его. В этом примере изображение планеты появится во вкладке только после щелчка кнопкой мыши на ней.
Для уведомления о таком событии в панели с вкладками нужно создать класс- слушатель ChangeListener. Учтите, что этот слушатель создается для всей панели, а не для какого-либо компонента.
tabbedPane.addChangeListener(1istener);
После щелчка кнопкой мыши на данной вкладке вызывается метод stateChanged класса-слушателя ChangeListener. При этом в качестве источника события выступает панель с вкладками. Для определения вкладки-источника события нужно использовать метод getSelectedlndex.
public void stateChanged(ChangeEvent event)
{
int n = tabbedPane.getSelectedlndex();
В листинге 6.17 для всех компонентов вкладки сначала задается значение null. При выборе новой вкладки выполняется проверка равенства значению null и подстановка изображения. (Это происходит мгновенно и незаметно для пользователя, который даже не успеет разглядеть пустую вкладку.) Кроме того, для демонстрации использования пиктограмм в заголовках вкладок посещенные вкладки обозначаются красным шариком, а непосещенные — желтым.
Листинг 6.17. TabbadPanaTast.d ava fiiilM
/**
Sversion 1.01 2001-07-22
©author Cay Horstmann
*/
import java.awt.* ;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
/**
Пример организатора компонентов
на основе панели с вкладками
*/
public class TabbedPaneTest
{
public static void main(String[] args)
{
JFrame frame = new TabbedPaneFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
)
}
/**
Этот фрейм содержит панель с вкладками, а также переключатели режима полного отображения и прокрутки вкладок
*/
class TabbedPaneFrame extends JFrame
public TabbedPaneFrame ()
{
setTitle("TabbedPaneTest");
■ setSize(WIDTH,HEIGHT);
final JTabbedPane tabbedPane = new JTabbedPane () /
/ / Указание значения null для вкладок и
/ / задержка их загрузки, кроме первого показа
Imagelcon icon = new Imageicon("yellow-ball.gif");
tabbedPane.addTab("Mercury", icon, null);
tabbedPane.addTab("Venus", icon, null);
tabbedPane.addTab("Earth", icon, null);
tabbedPane.addTab("Mars", icon, null);
tabbedPane.addTab("Jupiter", icon, null);
tabbedPane.addTab("Saturn", icon, null);
tabbedPane.addTab("Uranus", icon, null); tabbedPane.addTab("Neptune", icon, null); tabbedPane.addTab("Pluto", icon, null);
getContentPane().add(tabbedPane, "Center");
tabbedPane.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent event) <
// Проверка наличия компонента
if (tabbedPane.getSelectedComponent() == null) {
// Указание пиктограммы для заголовка
int n = tabbedPane.getSelectedlndex(); String title = tabbedPane.getTitleAt(n); Imagelcon planeticon
= new Imagelcon (title +• ".gif"); tabbedPane.setComponentAt(n, new JLabel(planeticon));
// Обозначение посещения вкладки
tabbedPane.setlconAt(n, new Imageicon("red-ball.gif")) ; ) )
});
JPanel buttonPanel = new JPanel() ;
ButtonGroup buttonGroup = new ButtonGroup();
JRadioButton wrapButton = new JRadioButton("Wrap tabs") wrapButton.addActionListener(new
ActionListener() {
p{ublic void actionPerformed(ActionEvent event) tabbedPane.setTabLayoutPolicy( JTabbedPane.WRAP_TAB_LAYOUT) ;
}
});
buttonPanel.add(wrapButton);
buttonGroup.add(wrapButton);
wrapButton.setSelected(true) ;
JRadioButton scrollButton
= new JRadioButton("Scroll tabs");
scrollButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
tabbedPane.setTabLayoutPolicy(
JTabbedPane.SCROLL_TAB_LAYOUT);
}
));
buttonPanel.add(scrollButton);
buttonGroup.add(scrollButton);
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
javax.swing.JTabbedPane
Конструкторы JTabbedPane () и JTabbedPane (int placement) создают панель с вкладками.
Параметры:
placementодно из значений: SwingConstants . TOP,
Swingconstants.LEFT,Swingconstants.RIGHT, Swingconstants.BOTTOM
Методы void addTab(String title, Component component), void addTab(String title, Icon icon, Component component) и void addTab(String title, Icon icon, Component component, String tooltip) создают вкладку в конце панели с вкладками.
Метод void insertTab(String title, Icon icon, Component c, String tooltip, int index) вставляет вкладку в месте с указанным индексом.
Метод void removeTab (int index) удаляет вкладку с указанным индексом из панели.
Метод void setSelectedIndex(int index) выбирает вкладку с указанным индексом.
Метод int getSelectedlndex() возвращает индекс выбранной вкладки.
Метод Component getSelectedComponent () возвращает выбранный компонент.
Метод String getTitleAt (int index) возвращает заголовок вкладки с заданным индексом.
Метод void setTitleAt(int index, String title) задает заголо-
вок вкладки с заданным индексом.
Метод String getlconAt(int index) возвращает пиктограмму вкладки с заданным индексом.
Методvoid setlconAt (int index, Icon icon) задает пиктограмму вкладки с заданным индексом.
Метод String getComponentAt(int index) возвращает компонент вкладки с заданным индексом.
Методvoid setcomponentAt (int index, Component с) задает компонент вкладки с заданным индексом.
Метод int indexOfTab(String title) возвращает индекс вкладки с заданным заголовком.
Метод int indexOfTab(Icon icon) возвращает индекс вкладки с заданной пиктограммой.
Метод int indexOf Tab (Component с) возвращает индекс вкладки с за- даннымкомпонентом.
Метод int getTabCount () возвращает общее количество вкладок в данной панели.
Метод void setTabLayoutPolicy(int policy) задает способ отображения вкладок.
Параметры:
policyодно из значений: JtabbedPane .WRAP_TAB_LAYOUT или
JTabbedPane.SCROLL_TAB_LAYOUT
Метод void addChangeListener(ChangeListener listener) создает класс-слушатель событий выбора вкладки.
Панель рабочего стола и внутренние фреймы
Во многих приложениях предлагаемая информация отображается в нескольких окнах, которые содержатся внутри большого фрейма. При свертывании фрейма все эти окна также скрываются. В операционной системе Windows такой пользовательский интерфейс называетсямногодокументным интерфейсом (multiple document interface—MDI).Нарис.6.43по- казано типичное приложение, в котором используется такой интерфейс.
Ранее этот стиль представления пользовательского интерфейса был очень распространен, но в последнее время он стал менее популярным. В частности многие броузеры для отображения Web-страниц используют отдельные фреймы высокого уровня. (Типичным исключением является Web-броузер Opera, показанный на рис. 6.43.) Какой способ лучше? У обоих подходов есть свои преимущества и недостатки. MDI- интерфейс позволяет справиться с беспорядком, который возникает при наличии большого количества окон. Но при работе с несколькими окнами высокого уровня для организации переходов между ними придется использовать кнопки и горячие клавиши операционной системы.
В межплатформенном мире Java не предусмотрено использование каких-либо инструментов какой-либо операционной системы. Поэтому пользовательский интерфейс на основе фреймов имеет гораздо больший смысл.
Рис. 6.43. Приложение с многодокументным интерфейсом
На рис. 6.44 показано приложение Java с тремя внутренними фреймами. Два из них содержат пиктограммы для полного развертывания, свертывания и закрытия фрейма. А третий фрейм показан в полностью свернутом виде.
В макете Metal внутренние фреймы имеют разные области "захвата", которые можно использовать для перемещения фреймов. А для изменения размеров окон нужно просто перетащить в нужном направлении уголки фрейма.
Пиктограмма закрытия
Внутренние фреймы
Уголок изменения размера
Рис6.44. Приложение Java с тремя внутренними фреймами
Для реализации этих функциональных возможностей следует выполнить следующие действия.
Использовать для приложения класс-фрейм JFrame.
Создать панель рабочего стола JDesktopPane, которая будет использоваться как панель содержимого.
desktop = new JDesktopPane();
setContentPane(desktop);
Создать внутренние фреймы JInternalFrame. При этом можно также создать пиктограммы для изменения размеров фрейма и его закрытия.
JInternalFrame=
new JInternalFrame(title, true,// можно изменять размер
true,// можно закрывать
true,// можно полностью развертывать
true); // можно полностью свертывать
Создать компоненты в панели содержания фрейма.
iframe.getContentPane().add(c) ;
Создать пиктограмму фрейма, которая будет представлена в левом верхнем углу фрейма.
iframe.setFramelcon(icon);
В текущей версии макета Metal пиктограмма фрейма не отображается в полностью свернутом виде фрейма.
Установить размер внутреннего фрейма. В исходном состоянии внутренний фрейм, как и стандартный фрейм, имеет размеры 0x0. Кроме того, для того чтобы фреймы не закрывали друг друга, нужно указать разные позиции для всех фреймов с помощью метода reshape.
iframe.reshape(nextFrameX, nextFrameY, width, height);
Далее нужно сделать фрейм видимым.
iframe. setvisible(true) ,-
П
В ранних версиях библиотеки Swing внутренние фреймы автоматически становятся видимыми, и этот метод не нужно было вызывать.
Создать фрейм в настольной панели JDesktopPane.
desktop.add(iframe);
Вероятно новый фрейм потребуется сделать выделенным фреймом, который в отличие от других внутренних фреймов имеет фокус ввода. В макете Metal строка заголовка выбранного фрейма имеет голубой цвет, тогда как строка заголовка других невыбранных фреймов имеет серый цвет. Для выбора фрейма используется метод setSelected. Кроме того, на выбранный фрейм можно наложить
запрет (veto), т.е. текущий выбранный фрейм может отказать в передаче фокуса ввода. В этом случае метод setSelected генерирует исключительную ситуацию PropertyVetoException, которую можно обработать нужным образом.
try
{
iframe.setSelected(true);
}
catch(PropertyVetoException e)
{
// наложен запрет на передачу фокуса
}
Для изменения расположения внутреннего фрейма со смещением, чтобы он не закрывал уже существующий фрейм, нужно располагать фреймы с промежутком, размер которого равен высоте строки заголовка фрейма.
int=
iframe.getHeight() —
iframe.getContentPane().getHeight();
Используйте это расстояние для определения положения следующего внутреннего фрейма.
nextFrameX += frameDistance;
nextFrameY += frameDistance;
i£(nextFrameX + width > desktop.getWidth())
nextFrameX = 0;
if(nextFrameY + width > desktop.getWidth())
nextFrameY = 0;
Каскадное и мозаичное расположение фреймов
В операционной системе Windows предусмотрено несколько команд для организации каскадного (cascading) расположения фреймов с перекрытием (рис. 6.45) и мозаичного (tiling) расположения фреймов без перекрытия (рис. 6.46). Однако в классах JDesktopPane и JInternalFrame не предусмотрено никаких средств для поддержки этих операций. В листинге 6.18 показано, как можно самостоятельно организовать такое упорядочение фреймов.
File windowFile windowм И.м И.iJ!!12S5±!£!2!±*“25^^iJ!!12S5±!£!2!±*“25^^READMEREADMEContentsContentsIIJava™ 2 SDK, Standard Edition Version 1.4.0 BetaJava™ 2 SDK, Standard Edition Version 1.4.0 BetaРис.6.45. КаскадноерасположениефреймовРис.6.45. КаскадноерасположениефреймовIntroduction
File window
File window
м И.
м И.
iJ!!12S5±!£!2!±*“25^^
iJ!!12S5±!£!2!±*“25^^
README
README
Contents
Contents
I
I
Java™ 2 SDK, Standard Edition Version 1.4.0 Beta
Java™ 2 SDK, Standard Edition Version 1.4.0 Beta
Рис.6.45. Каскадноерасположениефреймов
Рис.6.45. Каскадноерасположениефреймов
Java 2 SDKDocumentation.
Release Notes
Compatibility
Bug Reportsand Feedback
Contents of the.lava.2 SDK
Рис. 6.46. Мозаичное расположение фреймовРис. 6.46. Мозаичное расположение фреймовJava 2 Runt
Рис. 6.46. Мозаичное расположение фреймов
Рис. 6.46. Мозаичное расположение фреймов
Для каскадного расположения нужно указать для фреймов одинаковый размер и расположить их уступом. Для получения массива всех внутренних фреймов используется метод getAllFrames класса JDesktopPane.
JInternalFrame[] frame = desktop.getAllFrames();
Однако, при этом нужно учитывать текущее состояние фрейма, которое может иметь одно из следующих трех значений.
Пиктограмма, т.е. фрейм полностью свернут.
Имеет промежуточный размер.
Фрейм полностью развернут.
Для поиска полностью свернутых фреймов, которые нужно пропустить, следует использовать метод islcon. Для полностью развернутых фреймов нужно задать промежуточный размер с помощью метода setMaximum(false).Для наложения запрета на изменение этого свойства следует организовать перехват исключительной ситуации PropertyVetoException.
Ниже представлен цикл перебора всех внутренних фреймов панели рабочего стола.
for (int i = 0; i < frames.length; i++)
{
if (! frames [i] .islcon() )
{
try
{
// Попытка максимального развертывания
// фрейма, что может быть запрещено.
frames[ i ].setMaximum(false);
frames[i].reshape(x, y, width, height);
x += frameDistance;
у += frameDistance;
// Начать сначала после достижения
// нижнего правого угла панели рабочего стола.
if (x + width > desktop.getWidth()) х = 0;
if (у + height > desktop.getHeight()) у = 0;
}
catch(PropertyVetoException e)
(}
}
)
Мозаичное расположение фреймов организовано гораздо сложнее, особенно если количество фреймов таково, что они не образуют точно квадратную решетку. Сначала нужно подсчитать количество несвернутых фреймов, а затем определить количество столбцовмозаики.
int cols = (int)Math.sqrt(frameCount);
После этого задается количество строк мозаики.
int rows = frameCount / cols;
За исключением того, что последние несколько столбцов
extra = frameCount % cols;
будут содержать rows+1 строк.
Ниже представлен цикл перебора всех внутренних фреймов настольной панели, которые используются для мозаичного расположения фреймов.
int width = desktop.getWidth() / cols;
int height = desktop.getHeight() / rows;
intr= 0;
intс= 0;
for (inti= 0; i<frames.length; i++)
{
if (!frames[i].islcon())
{
try
{
frames[i].setMaximum(false) ;
frames[i].reshape(c * width, r * height, width, height);
if (r == rows)
{
r=0;
с++;
if (c == cols — extra)
{
// Добавить дополнительную строку.
rows++;
height = desktop.getHeight() / rows;
}
}
}
catch(PropertyVetoException e)
} }
В листинге6.18 показана еще одна распространенная операция: перемещение выбора от текущего фрейма к следующему несвернутому фрейму. В классе JDesktopPane не предусмотрены методы для возвращения выбранного фрейма. Вместо этого нужно вручную организовать обход всех фреймов и вызов метода isSelected до тех пор, пока не будет найден текущий выбранный фрейм. Затем нужно найти следующий несвернутый фрейм и попытаться выбрать его с помощью следующего метода.
frames[next].setSelected(true);
Как и прежде, этот метод может генерировать исключительную ситуацию PropertyVetoException. Возвращение к исходному фрейму означает, что не существует других доступных для выбора фреймов. Вот как выглядит этот цикл перебора всех внутренних фреймов.
for (int i = 0; i < frames.length; i++) {
if (frames[i].isSelected())
{
// Поиск следующего несвернутого
// фрейма, который можно выбрать try
{
int next = (i + 1) % frames.length;
while (next != i && frames[next].islcon()) next = (next + 1) % frames.length;
if (next == i) return;
// Все другие фреймы свернуты или
// свертывание для них запрещено
frames[next].setSelected(true);
frames[next].toFront();
return;
}
catch(PropertyVetoException e)
{}
Запретна изменение свойства
Рассмотрим способы генерирования исключительных ситуаций в случае наложения запрета на изменение свойства. В классе JInternalFrame для слежения за значениями свойств используется механизм компонентов JavaBeans. Очень подробно этот механизм описывается в главе 8, "ТехнологияJavaBeans™", а здесь мы кратко рассмотрим только тот материал, который относится к обработке запретов на изменение свойства.
Запреты обычно используются не для полного свертывания или передачи фокуса ввода, а для закрытия фрейма. Для закрытия фрейма используется метод setClosed класса JInternalFrame. Так как для этого метода предусмотрено использование запретов, то перед закрытием фрейма он вызывает все зарегистрированные классы- слушатели запрещаемых изменений (vetoable change listeners). Такимобразом, каждомутако- му классу-слушателю предоставляется возможность генерировать исключительную ситуацию PropertyVetoException и отменить вызов метода setClosed до выполнения им каких-либо действий.
В данном примере отображается диалоговое окно с предложением подтвердить закрытие данного фрейма, как показано на рис. 6.47. Если пользователь не дает такого подтверждения, то фрейм останется открытым.
Рис.6.47. Пример обработки запрета на закрытие окна
Вот какие действия нужно выполнить для организации такого поведения.
Создать класс-слушатель для каждого фрейма. Объект должен относится к некоторому классу, который основан на интерфейсе VetoableChangeListener.
Это следует сделать сразу же после создания фрейма. В данном примере для создания внутренних фреймов используется класс-фрейм, хотя для этого можно также использовать безымянный внутренний класс.
iframe.addVetoableChangeListener(listener) ;
Реализовать метод vetoableChange, который является единственным методом интерфейса VetoableChangeListener и содержит параметр типа PropertyChangeEvent. Для получения имени изменяемого свойства нужно использовать метод getName, например, для свойства closed, если запрет задается для метода setClosed(true). Как показано в главе 8, "Технология JavaBeans™”, имя свойства получается за счет удаления префикса set из имени метода и указания первой буквы свойства в нижнем регистре.
Для получения нового значения свойства следует использовать метод getNewValue так, как показано ниже.
String name = event.getPropertyName();
Object value = event.getNewValue();
if (name.equals("closed") && value.equals(Boolean.TRUE)) {
// Предложение пользователю подтвердить закрытие фрейма.
}
Генерировать исключительную ситуацию PropertyVetoException для блокирования изменения свойства с обычным возвратом, если запрет на измене- ниесвойстваненакладывается.
class DesktopFrame extends JFrame
implements VetoableChangeListener
{
public void vetoableChange(PropertyChangeEvent event) throws PropertyVetoException
{
// Если пользователь не согласен,
// запретить закрытие фрейма
if (result != JOptionPane.YES_OPTION) throw new PropertyVetoException(
"User canceled close", event);
}
}
Диалоговые окна во внутренних фреймах
Для создания диалоговых окон во внутренних фреймах не рекомендуется использовать класс JDialog, потому что такие диалоговые окна обладают следующими недостатками.
Они очень ресурсоемки, потому что создают новый фрейм в системе окон.
Система управления окнами не знает, как расположить диалоговое окно по отношению к породившему его внутреннему фрейму.
Вместо этого, для создания простых диалогов рекомендуется использовать методы showInternalXxxDialog класса JOptionPane. Они работают аналогично методам showXxxDialog за исключением того, что диалоговое окно располагается непосредственно над внутренним фреймом.
Для создания более сложных диалоговых окон следует использовать класс JInternalFrame, но в нем не предусмотрены инструменты поддержки модальных диалоговых окон.
В данном примере используется внутреннее диалоговое окно, в котором пользователю предлагается подтвердить закрытие фрейма.
int result =
"OK to close?");
Иногда требуется только уведомить о закрытии фрейма, а потому в таких случаях можно не использовать механизм запрета и использовать класс-слушатель InternalFra- DmeListener. Внутренний класс-слушатель фрейма InternalFrazneListenerработает
аналогично классу-слушателю WindowsLi s tener. При закрытии внутреннего фрейма вместо метода windowclosing вызывается метод internalFrameClosing. Аналогичным образом используются все шесть методов-уведомителей о действиях с внутренним фреймом (открыто/закрыто, свернуто/развернуто, активно/неактивно).
Перетаскивание контуров
Разработчики часто жалуются на низкую производительность при использовании внутренних фреймов. Например, медленнее всего выполняется операция перетаскивания фрейма со сложным содержимым по настольной панели. Дело в том, что менеджер настольной панели постоянно перерисовывает фрейм, что существенно замедляет работу.
Такая же ситуация наблюдается при работе в графических оболочках Windows или X Windows с плохо написанным видеодрайвером. Перетаскивание окон в большинстве систем выполняется быстро из-за того, что видеоплаты поддерживают операцию перетаскивания на основе аппаратного отображения изображения внутри фрейма на другое место экрана во время перетаскивания.
Для повышения производительности без ущерба для пользователя можно использовать метод перетаскивания контуров. В таком случае при перетаскивании фрейма постоянно обновляются только контуры фрейма. Внутренняя часть будет перерисована, только когда пользователь опустит фрейм в месте назначения.
Для включения режима перетаскивания контуров нужно вызвать следующий метод.
desktop. setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
Этот параметр эквивалентен установке режима постоянной перерисовки ("continuous layout") содержимого панели в классе JSplitPane.
В ранних версиях библиотеки Swing для включения перетаскивания контуров нужно было использовать следующий метод.
D
desktop.putClientProperty("JDesktopPane.dragMode", "outline");
В данном примере для включения/отключения режима перетаскивания контуров используется команда меню Window=»Drag Outline (Окно1^ Перетаскивание контуров).
• Внутренние фреймы настольной панели управляются классом-менеджером DesktopManager. При использовании стандартных приемов программирования этот класс не потребуется. Он может использоваться для инсталляции нового менеджера настольной панели и реализации разных вариантов поведения настольной панели, нов данной книгеэти вопросы не рассматриваются.
В листинге 6.18 приводится пример настольной панели с внутренними фреймами, предназначенными для отображения HTML-страниц. При выборе команды меню File=>Open (Файл^Открыть) на экране отображается диалоговое окно для выбора локального HTML-файла в новом внутреннем фрейме. После щелчка на выбранном HTML- файле этот файл будет отображен на экране в новом внутреннем фрейме. Попробуйте изменить расположение фреймов на экране с помощью команд меню Window^Cascade (Окно=5> Каскадное расположение) и WindOW^Tile (Окно^Мозаичное расположение). Этот пример завершает обсуждение сложных компонентов библиотеки Swing.
Листинг 6.18. internalFrameTeet.lava
/**
©version 1.01 2001-07-22
©author Cay Horstmann
*/
import java.awt.*;
import j ava.awt.event.* ;
import j ava.beans.*;
import java.io.*;
import java.net.*;
import java.util.*;
import j avax.swing.*;
import javax.swing.event.*;
/**
Пример использования внутренних фреймов
*/
public class InternalFrameTest
{
public static void main(String[] args)
{
JFrame frame = new DesktopFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Этот настольный фрейм содержит панели
редактирования для отображения HTML-документов
*/
class DesktopFrame extends JFrame
{
public DesktopFrame()
{
setTitle("InternalFrameTest");
setSize(WIDTH,HEIGHT);
desktop = new JDesktopPane(); setContentPane(desktop);
// Создание команд меню
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu fileMenu = new JMenu("File");
menuBar.add(fi1eMenu);
JMenuItem openitem = new JMenuItem("Open");
openitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
openFile();
}
});
fileMenu.add(openltem);
JMenuItem exititem = new JMenuItem("Exit");
exi 111em.addAc tionLis tener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
System.exit(0) ;
}
});
fileMenu.add(exitltem);
JMenu windowMenu = new JMenu("Window");
menuBar.add(windowMenu);
JMenuItem nextItem = new JMenuItem("Next");
nextitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
selectNextWindow();
}
});
windowMenu.add(nextitem);
JMenuItem cascadeitem = new JMenuItem("Cascade");
cascadeitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
cascadewindows();
}
));
windowMenu.add(cascadeitem);
JMenuItem tileltem = new JMenultem("Tile");
tileltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
tileWindows();
}
});
windowMenu.add(tileltem);
final
= newOutline");
dragOutlineltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
desktop.setDragMode(dragOutlineltem.isSelected() ?
:
}
В ;
windowMenu.add(dragOutlineltem);
}
/**
Создание внутреннего фрейма в настольной панели с the component to display in the internal frame t the title of the internal frame. */
publicvoidc,Stringt)
final=newJInternalFrame(t,
true, // поддерживает изменение размера
true, // поддерживает закрытие
true, // поддерживает минимизацию
true); // поддерживает отображение пиктограммы
iframe.getContentPaneO.add(c);
desktop.add(iframe);
iframe.setFrameIcon(new
// Создание слушателя для подтверждения закрытия фрейма
iframe.addVetoableChangeListener(new
VetoableChangeListener()
{
public voidevent)
throws PropertyVetoException
{
String name =
Object value =
// Проверка попытки закрытия фрейма if
&& value.equals(Boolean.TRUE))
{
// Вопрос пользователю о закрытии фрейма int result
=
"OK to close?");
// Если пользователь не согласен,
// запретить закрытие фрейма
if (result !=
throw new
"User canceled close", event);
} )
});
// Расположение фрейма
int width = desktop.getWidth() / 2;
int height = desktop.getHeight() / 2 ;
iframe.reshape(nextFrameX, nextFrameY, width, height);
iframe.show();
// Выбор фрейма
try
{
iframe.setSelected(true);
}
catch(PropertyVetoException e)
{}
/* При первом открытии нужно вычислить расстояние между фреймами, которые расположены уступом
*/
if (frameDistance == 0)
frameDistance = -
// Вычислить расположение следующего фрейма
nextFrameX += frameDistance;
nextFrameY += frameDistance;
if (nextFrameX + width > desktop.getWidth()) nextFrameX = 0;
if (nextFrameY + height > desktop.getHeight()) nextFrameY = 0;
/**
Каскадное расположение несвернутых внутренних фреймов на панели */ public void cascadewindows() {
JInternalFrame[] frames = desktop. getAllFrames () ; int x = 0;
int у = 0;
int width = desktop.getWidth() / 2;
int height = desktop.getHeight() / 2;
for (int i = 0; i < frames.length; i++) {
if (!frames[i].islcon()) {
try {
// Попытка максимального развертывания // фрейма, что может быть запрещено frames[i].setMaximum(false);
frames[i].reshape(x, у, width, height);
x += frameDistance;
у += frameDistance;
// Начать сначала после достижения
// нижнего правого угла настольной панели
if (х + width > desktop.getWidth()) x = 0;
if (у + height > desktop.getHeight()) у = 0;
}
catch(PropertyVetoException e)
О
}
}
/**
Мозаичное расположение несвернутых внутренних фреймов на панели
*/
public void tileWindows() {
JInternalFrame[] frames = desktop. getAHFrames();
// Подсчет несвернутых фреймов
int frameCount = 0;
for (int i = 0; i < frames.length; i++) {
if ( ! frames[i].islcon() ) frameCount++;
}
int rows = (int)Math.sgrt(framecount);
int cols = frameCount / rows;
int extra = frameCount % rows;
// Количество столбцов с дополнительной строкой
int width = desktop.getWidth() / cols;
int height = desktop.getHeight() / rows;
int r = 0;
int с = 0;
for (int i = 0; i < frames.length; i++) {
if (!frames[i].islcon()) {
try {
frames[i].setMaximum(false);
frames[i].reshape(c * width, r * height, width, height);
r++;
if (r == rows) {
r = 0;
с++;
if (c == cols - extra) {
// Добавить дополнительную строку rows++;
height = desktop.getHeight() / rows; }
catch(PropertyVetoException e) (}
} }
/**
Отображение следующего несвернутого внутреннего фрейма поверх других фреймов
*/
public void selectNextWindow()
{
JInternalFrame[] frames = desktop.getAHFrames();
for (int i = 0; i < frames.length; i++) {
if (frames[i].isSelected())
{
// Поиск следующего несвернутого фрейма,
// который можно выбрать
try
{
int next = (i + 1) % frames.length;
while (next != i && frames[next].islcon()) next = (next +1) % frames.length;
if (next == i) return;
// Все другие фреймы свернуты или
// свертывание для них запрещено
frames[next].setSelected(true) ;
frames[next].toFront() ;
return;
}
catch(PropertyVetoException e)
(}
}
}
/**
Выбор и открытие HTML-файла пользователем */
public void openFile()
{
// Выбор файла
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File(".")) ;
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter()
{
public boolean accept(File f)
{
String fname =
return fname.endsWith(".html")
||fname.endsWith(".htm")
1,1 f . isDirectory () ;
}
public String getDescription() {
return "HTML Files";
intr=chooser.showOpenDialog(this) ;
if (r == JFileChooser.APPROVE_OPTION) {
// Открытие выбранного файла
String filename = chooser.getSelectedFile().getPath() ; try {
URL fileUrl = new URL("file:" + filename);
createlnternalFrame(createEditorPane(fileUr1), filename);
}
catch(MalformedURLException e)
{
}
}
/**
Создание панели редактирования
@param и the URL of the HTML document
*/
public Component createEditorPane(URL u)
{
// Создание панели редактирования
// с поддержкой гиперссылок
JEditorPane editorPane = new JEditorPane();
editorPane.setEditable(false) ;
editorPane.addHyperlinkListener(new
HyperlinkListener()
{
public void hyperlinkUpdate(HyperlinkEvent event) {
if (event.getEventType()
==
createlnternalFrame(createEditorPane(
'event.getURL0), event.getURL().toString());
}
}) ;
try
{
editorPane.setPage(u);
}
catch(lOException e)
{
editorPane.setText("Exception: " +e);
}
return new JScrollPane(editorPane);
}
private JDesktopPane desktop;
private int nextFrameX;
private int nextFrameY; private int frameDistance;
private static final int WIDTH = 600;
private static final int HEIGHT = 400;
javax.swing.JDesktopPane
МетодJlnternalFrame [] getAllFrames () возвращает все внутренние фреймы данной настольной панели.
Методvoid setDragMode (int mode) задает режим отображения всего содержимого фрейма или только его контуров во время перетаскивания фрейма.
Параметры:
modeодно из значений: JDesktopPane.LIVE_DRAG_MODE и
JDesktopPane.OUTLINE_DRAG_MODE
javax.swing. JlnternalFrame
Конструкторы JlnternalFrame(), JlnternalFrame(String title), JlnternalFrame(String title, boolean resizable), Jlnternal- Frame (String title, boolean resizable, boolean closeable), JlnternalFrame(String title,boolean resizable,boolean
closeable, boolean maximazable) иJlnternalFrame(String title, boolean resizable, boolean closeable, boolean maximazable, boolean iconifiable) создаютновыйвнутреннийфрейм.
Параметры:
titleзаголовок фрейма
resizableесли имеет значение true, то допускается изменение
размеров фрейма
closeablecloseablemaximazablemaximazableiconifiableiconifiableесли имеет значение true, то допускается закрытие фрейма
closeable
closeable
maximazable
maximazable
iconifiable
iconifiable
если имеет значение true, то допускается развертывание фрейма
если имеет значение true, то допускается свертывание фрейма
Метод booleanisResizable() возвращает значение свойства
resizable. Если возвращено значение true, то в строке заголовка отображается пиктограмма изменения размеров внутреннего фрейма.
Метод booleanisCloseable()возвращает значение свойства
closeable. Если возвращено значение true, то в строке заголовка отображается пиктограмма закрытия внутреннего фрейма.
Метод boolean isMaximazable() возвращает значение свойства maximazable. Если возвращено значение true, то в строке заголовка отображается пиктограмма развертывания внутреннего фрейма.
Метод boolean islconifiable () возвращает значение свойства iconifiable. Если возвращено значение true, то в строке заголовка отображается пиктограмма свертывания внутреннего фрейма.
Метод boolean islcon() возвращает значение свойства icon. Если оно равно значению true, то допускается свертывание внутреннего фрейма.
Метод void seticon(boolean b) задает значение свойства icon. Если оно равно значению true, то допускается свертывание внутреннего фрейма.
Метод boolean isMaximum() возвращает значение свойства maximum. Если оно равно значению true, то допускается развертывание внутреннего фрейма.
Метод void setMaximum(boolean b) задает значение свойства maximum. Если оно равно значению true, то допускается развертывание внутреннего фрейма.
Метод boolean isClosed() возвращает значение свойства closed. Если оно равно значению true, то допускается закрытие внутреннего фрейма.
Метод void setClosed (boolean b) задает значение свойства closed. Если оно равно значениюtrue, то допускается закрытие внутреннего фрейма.
Метод booleanisSelectedOвозвращает значениесвойства
selected. Если оно равно значению true, то текущий внутренний фрейм становится выбранным фреймом настольной панели.
Метод void setSelected(boolean b) задает значение свойства selected. Если оно равно значению true, то текущий внутренний фрейм становится выбранным фреймом настольной панели.
Метод void moveToFront () перемещает внутренний фрейм на передний план настольной панели.
Метод void moveToBack () перемещает внутренний фрейм на задний план настольной панели.
Метод void reshape(int x, int у, int width, int height) перемещает и изменяет размеры внутреннего фрейма.
Параметры:
х, укоординаты верхнего левого угла фрейма
widthширина фрейма
heightвысота фрейма
Метод Container getContentPane () возвращает панель с содержимым внутреннего фрейма.
Метод void setContentPane () задает панель с содержимым внутреннего фрейма.
Метод JDesktopPane getDesktopPane () возвращает настольную панель внутреннего фрейма.
Метод Icon getFramelcon () возвращает пиктограмму фрейма, которая отображается в строке заголовка фрейма.
Метод void setFramelcon () задает пиктограмму фрейма, которая отображается в строке заголовка фрейма.
Метод boolean isVisible() возвращает значение свойства visible.
Метод void setVisible (boolean b) задает значение свойства
visible.
Метод void show() отображает внутренний фрейм и перемещает его на передний план.
javax.awing.JComponent
Метод void addVetoableChangeListener(VetoableChangeListener listener) создает класс-слушатель запрещаемых изменений, который уведомляется при попытке изменения свойства с наложенным на него ог-
раничением.
java.beans. VetoableChangeListener
Метод voidvetoableChange(PropertyChangeEvent event) вызывается, когда метод set свойства с наложенным на него ограничением уведомляет класс-слушатель запрещаемых изменений.
j ava.beans.PropertyChangeEvent
МетодStringgetPropertyName () возвращает имя изменяемого свойства.
Метод Object getNewValue () возвращает предлагаемое новое значение
свойства.
java.beans.PropertyVetoException
Конструктор PropertyVetoException(String reason, PropertyChangeEvent event) создает исключительную ситуацию для запре- щаемогосвойства.
Параметры:
reason event
причина запрета запрещаемое событие
Расширенные возможности AWT
В этой главе...
т Алгоритм рисования
Фигуры
т Области
Штрихи
Раскраски
Преобразование координат
т Усечение
т Прозрачность и композиция
Параметры рисования
т Чтение и запись изображений
т Управление изображениями
Печать
т Буфер обмена
т Перетаскивание
JDтоме 1 рассмотрены методы класса Graphics, используемые для создания простых рисунков. Они эффективны и для создания простых аплетов и приложений, но их недостаточно для построения сложных фигур или для осуществления полного контроля над внешним видом рисунка. Java 2D API содержит самую последнюю вер-
сию библиотеки классов, которую можно использовать для создания высококачественных рисунков. В данной главе рассматриваются основные способы применения этой библиотеки. Кроме того, в ней описываются вопросы управления печатью и рассматриваются способы печати, которые можно реализовать в вашей программе.
Наконец, в конце главы рассматриваются две технологии обмена данными между программами: с помощью буфера обмена и механизма перетаскивания. Эти методы можно использовать для передачи данных между двумя приложениями Java или между приложением Java и другой платформозависимой программой.
Алгоритм рисования
В исходной версииJDK 1.0 (Java Development Kit) предусмотрен очень про
стой механизм рисования фигур. Следует выбрать режим цвета и рисования и вызвать методы класса Graphics (например, drawRect или illOval). В Java 2D API- интерфейсе предусмотрено гораздо больше возможностей.
Можно легко создавать множество разных фигур.
Можно управлять параметрами штриха, т.е. линии, ограничивающей фигуру.
Можно заполнять фигуры цветами, изменяя оттенки и повторяя узор.
Можно использовать преобразования для перемещения, масштабирования, поворота и растягивания фигур.
Можно обрезать фигуры произвольными областями усечения.
Можно выбрать правила композиции для комбинирования пикселей старой и новой фигуры.
Можно задать параметры рисования для достижения компромисса между скоростью загрузки и качеством рисунка.
Для того, чтобы нарисовать фигуру, нужно выполнить следующие действия.
Получить объект классакоторый является подклассом класса
Graphics. Если используется версия JDK, допустимая в технологии Java 2D, то такие методы, как paint иавтоматически получают объект
класса Graphics2D. Просто используйте следующее приведение типа.
public voidg)
{
Graphics2D g2 =
}
Использовать метод setRenderingHints для установки режима рисования для достижения компромисса между скоростью и качеством рисунка.
hints ...;
g2 . SetRenderingHints(hints);
Использовать методдля установки параметров штриха, используе
мой при рисовании внешнего контура фигуры. Можно установить толщину и выбрать тип штриха из сплошных и пунктирных линий.
Strokestroke=...;
g2 . setStroke(stroke);
Использовать метод setPaint для установки раскраски, которая используется для заполнения штриха или фигуры. Она может содержать только один цвет, изменяющиеся оттенки или мозаичный узор.
Paint paint = . . .;
g2 .setPaint(paint) ;
Использовать метод setclipдля установки области усечения.
Shape clip = ... ; g2.clip(clip);
Использовать метод setTransformдля преобразования координат рисунка из относительной пользовательской системы координат в абсолютную систему координат используемого аппаратного обеспечения. Это преобразование следует использовать в тех случаях, когда проще создать определение фигуры в пользовательской системе координат, чем использовать с этой целью пиксельные координаты.
AffineTransform transform = . . .; g2.transform(transform) ;
Использовать метод setCompositeдля установки правил композиции, которые описывают, как комбинировать новые фигуры с уже существующими.
Composite (composite) = ... ;
g2.setComposite(composite);
Создать фигуру. В Java 2D API предусмотрено много объектов-фигур и методов их объединения.
Shape shape = ... ;
Нарисовать или заполнить фигуру. Если фигура нарисована, то будут очерчены ее контуры, а если фигура заполнена, то ее внутренняя часть будет закрашена.
g2.draw(shape);
g2.fill(shape);
Конечно, на практике обычно не нужно выполнять все эти действия, потому для многих параметров двумерного (2D) графического контекста предусмотрены значения по умолчанию, которые можно изменить в случае необходимости.
В следующем разделе рассматриваются способы описания фигур, штрихов, раскрасок, преобразований и правил композиции.
Различные set-методы просто устанавливают состояние двумерного (2D) графического контекста и не относятся к конкретным рисункам. Например, изображение генерируется не во время создания объекта Shape, а при вызове методов draw или fil на основании показанного на рис. 7.1 алгоритма рисования (renderingpipeline).
ОчерчиваниеПреобразованиеОчерчиваниеПреобразованиеУсечениеУсечениеРис. 7.1.Алгоритм рисованияРис. 7.1.Алгоритм рисованияЗаполнениеЗаполнение
ОчерчиваниеПреобразование
ОчерчиваниеПреобразование
Усечение
Усечение
Рис. 7.1.Алгоритм рисования
Рис. 7.1.Алгоритм рисования
Заполнение
Заполнение
Для рисования изображения в соответствии с этим алгоритмом выполняются следующие действия.
Очерчивается вид фигуры.
Фигура преобразуется.
Фигура усекается, а если между фигурой и областью усечения нет пересечения, то этот процесс не выполняется.
Оставшаяся после отсечения часть фигуры заполняется раскраской.
Новая фигура комбинируется с существующей, например, как показано на рис. 7.1, где круг представляет существующую фигуру, а чашка накладывается поверх него.
В следующем разделе рассматриваются способы определения фигур, а затем описываются установки двумерного графического контекста.
java.awt.Graphics2D
рисует контуры данной фигуры с текущи-заполняет текущей раскраской внутрен-рисует контуры данной фигуры с текущи-заполняет текущей раскраской внутрен-Методvoid draw (Shape s)ми параметрами очерчивания.
рисует контуры данной фигуры с текущи-
заполняет текущей раскраской внутрен-
рисует контуры данной фигуры с текущи-
заполняет текущей раскраской внутрен-
Метод void fill (Shape s) нюю часть фигуры.
Фигуры
Вот несколько методов класса Graphics, которые используются для рисования фигур.
drawLine drawRectangle drawRoundRect draw3DRect drawPolygon drawPolyline drawOval drawArc
Для них существуют соответствующие fill-методы заполнения фигур, которые были предусмотрены в классе Graphics, начиная с JDK версии 1.0. В Java 2D API используется совершенно другая, объектно-ориентированная модель, где вместо методов используются перечисленные ниже классы.
Line2D
Rectangle2D
RoundRectangle2D
Ellipse2D
Arc2D
QuadCurve2D CubicCurve2D GeneralPath
Данный классы реализуют интерфейс Shape.
Кроме того, предусмотрен класс-точка Point2D, который описывает точку с координатами Xи у. Точки полезны для определения фигур, но сами по себе фигурами не являются.
Для того, чтобы нарисовать фигуру, нужно создать объект класса, который реализует интерфейс Shape, а затем вызвать метод draw для класса Graphics2D.
Классы Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D и Arc2D соответствуют методам drawLine, drawRectangle, drawRoundRect, drawOval и drawArc. Для понятия "трехмерный прямоугольник" (3D rectangle) не предусмотрен соответствующий ему метод draw3DRect. Однако, в Java 2D API поддерживается два дополнительных класса для рисования кривых второго и третьего порядков. Эти фигуры будут рассмотрены далее. Кроме того, для рисования многоугольников не предусмотрен отдельный класс (например, Polygon2D), а вместо этого предлагается класс GeneralPath, который описывает контуры, состоящие из линий и кривых второго и третьего порядков. Класс GeneralPath можно использовать для описания многоугольника, которое приводится ниже.
Перечисленные классы наследуют общий класс RectangularShape.
Rectangle2D
RoundRectangle2D
Ellipse2D
Arc2D
Известно, что эллипсы и дуги не являются прямоугольниками, но их можно вписать в прямоугольник, как показано на рис. 7.2.
Рис. 7.2. Эллипс и дугу можно вписать в прямоугольник
Каждый из классов, название которого заканчивается суффиксом 2D, имеет два подкласса для указания координат в виде чисел типа float и double. Например, в томе I уже встречались такие операторы для указания координат прямоугольника.
Rectangle2D.Float
Rectangle2D.Double
Такая же схема используется и для других классов.
Arc2D.Float
Arc2D.Double
Для внутреннего представления координат все графические классы используют для хранения координат данные типа float, поскольку для них требуется меньше места для хранения, чем для данных типа double, к тому же они имеют достаточную точность для
геометрических вычислений. Однако, в языке программирования Java обработка данных типа float выполняется очень громоздкими и неуклюжими способами. Поэтому большинство методов графических классов используют параметры типа double и возвращают значения типа double. Выбрать тип данных (float или double) для представления координат следует только при создании двумерного объекта, т.е. нужно выбрать конструктор либо на основе чисел типа float,либо на основе чисел типа double.
Rectangle2D floatRect
= new Rectangle2D.Float(5F, 10F, 7.5F, 15F) ;
Rectangle2D doubleRect
new Rectangle2D.Double(5, 10, 7.5, 15);
Классы Xxx2D.Float и Xxx2D. Double являются подклассами класса Xxx2D. После создания объекта нет надобности запоминать подклассы, а можно просто сохранить созданный объект в переменной суперкласса, как показано во фрагменте кода.
Аналогично, судя по названиям Xxx2D. Float и Xxx2D. Double, можно справедливо предположить, что они являются внутренними классами классов Xxx2D. Это простое синтаксическое правило позволяет избежать увеличения длины имен внешних классов.
На рис. 7.3 показаны отношения между классами фигур без указания подклассов Double и Float. Традиционные классы библиотеки 2D, которые унаследованы из прежних версий пакетаJDK, закрашены серым цветом.
Рис. 7.3. Отношения между классами фигур
Классы фигур
Классы Line2D, Rectangle2D и Ellipse2D уже использовались в главе 7 тома 1, а в этом разделе рассматриваются способы использования остальных двумерных фигур.
Для фигуры RoundRectangle2D нужно указать координаты верхнего левого угла, ширину, высоту и параметры (ХИ)>области скругления угла (рис. 7.4). Например, следующий вызов метода создает прямоугольник с радиусом скругления углов, равным 20.
RoundRectangle2D r
new RoundRectangle2D.Double(150, 200, 100, 50, 20, 20);
Ширина скругления
Рис. 7.4. Использование RoundRectangle2D
Для построения дуги нужно указать ширину и высоту области рисования, начальный угол, угол развертки дуги (рис. 7.5) и один из типов замыкания дуги (Arc2D.OPEN,Arc2D.PIE илиArc2D.CHORD).
Arc2D a = new Arc2D(x, y, width, height, startAngle, arcAngle, closureType);
Рис. 7.5.Построение эллиптической дугиРис. 7.5.Построение эллиптической дугиВысота скругленияВысота скругления
Рис. 7.5.Построение эллиптической дуги
Рис. 7.5.Построение эллиптической дуги
Высота скругления
Высота скругления
На рис. 7.6 показаны типы замыкания дуги.
Arc2D.OPENArc2D.OPENArc2D.PIEArc2D.PIEРис. 7.6. Типы замыкания дугиРис. 7.6. Типы замыкания дугиArc2D.CH0RDArc2D.CH0RD
Arc2D.OPEN
Arc2D.OPEN
Arc2D.PIE
Arc2D.PIE
Рис. 7.6. Типы замыкания дуги
Рис. 7.6. Типы замыкания дуги
Arc2D.CH0RD
Arc2D.CH0RD
Несмотря на то, что углы задаются в градусах, они могут искажаться. Например, угол величиной 45° означает диагональную позицию, даже если длина не равна ширине. Если нужно нарисовать круглую дугу (например, секторную диаграмму), то об этом не следует беспокоиться. Однако, для создания эллиптических дуг придется вспомнить тригонометрию (подробности смотрите ниже в главе).
Рис. 7.7. Кривая второго порядкаРис. 7.7. Кривая второго порядкаРИС. 7.8. Криваятретьего порядкаРИС. 7.8. Криваятретьего порядка
Рис. 7.7. Кривая второго порядка
Рис. 7.7. Кривая второго порядка
РИС. 7.8. Криваятретьего порядка
РИС. 7.8. Криваятретьего порядка
В пакете Java 2D API предусмотрено использование кривых второго и третьего порядков. В этой главе не будут рассматриваться математические аспекты этих кривых, но для демонстрации их возможностей предлагается запустить программу, код которой приведен в листинге 7.1. Как показано на рис. 7.7 (и рис. 7.8), кривые второго (и третьего) порядка определяются двумя конечными точками и одной (и двумя) контрольной точкой. Перемещение контрольных точек изменяет форму кривых.
Способы указания углов для эллиптических дуг
Алгоритм рисования эллиптических дуг использует искаженные углы, которые предварительно вычисляются вызывающей программой. Приведенные ниже сведения позволят всем, интересующимся этим вопросом, сэкономить несколько часов, которые пришлось бы затратить, чтобы вспомнить основные сведения из тригонометрии.
Для превращения реальных углов в искаженные используется следующая формула:
distortedAngle =
Иногда (например, в программе, приведенной в конце этого раздела) известна конечная точка дуги или другая точка линии, которая соединяет центр эллипса и конечную точку. В этом случае сперва вычисляются следующие расстояния:
dx = p.getX()-center.getx();
dy = p.getY()-center.getY();■
А затем вычисляется искаженный угол.
distortedAngle = Math.atan2(-dy * width, dx * height);
Знак минус перед dy необходим из-за того, что в пиксельной системе координат у-ось направлена вниз. Поэтому углы отсчитываются по направлению часовой стрелки, а нам нужно указатьугол, который измеряется против часовой стрелки.
Для преобразования единиц измерения результата из радиан в градусы используется формула:
distortedAngle = Math.toDegrees(distortedAngle);
Результатом будет значение в диапазоне между -180и 180°.
Аналогичным образом вычисляются искаженный начальный и конечный углы, а затем вычисляется разность между двумя искаженнымиуглами.
Если начальный угол или разность отрицательны, то следует добавить 360. Затем для создания дуги нужно предоставить конструктору начальный угол и разницу между искаженными углами.
Arc2D a = new Arc2D(x, у, width, height,
distortedStartAngle, distortedAngleDifference, closureType);
В документации не только не дается строгое определение искажения угла, но и не точно используется название "угол развертки дуги" (“ap angle") для разницы между искаженными углами. За исключением округлой дуги, разница между искаженными углами не является ни фактическим углом развертки дуги, ни его искажением.
При запуске программы, которая находится в конце этого раздела, можно будет визуально убедится, что эти вычисления дают правильные значения для создания кривой, показанной нарис.7.9.
Для создания кривых второго и третьего порядков нужно задать координаты конечных и контрольных точек.
QuadCurve2D q = new QuadCurve2D.Double (startX, startY, controix, controlY, endX, endY);
CubicCurve2D q = new CubicCurve2D.Double (startX, startY, controllx, controllY, control2X, control2Y, endX, endY);
Кривые второго порядка не очень гибки и потому не очень широко используются на практике, в отличие от кривых третьего порядка (например, кривая Безье, нарисованная с помощью класса CubicCurve3D). Можно создавать сложные поверхности, комбинируя несколько кривых третьего порядка таким образом, чтобы совпадал угол наклона в точках соединения. Более подробно об этом можно узнать из книги Джеймса Фоли (James D. Foley), Андриса ван Дама (Andries van Dam), Стивена Фэйнера (Steven К. Feiner) идр. Computer Graphics:Principles andPractice, SecondEditiion [AddisonWesley1995].
Для создания и сохранения произвольной кривой в виде последовательности линейных сегментов, кривых второго и третьего порядка предусмотрен класс GeneralPath. Для этого нужно указать первую координату и использовать метод moveTo.
GeneralPath path = new GeneralPath() ; path.moveTo (10,20);
Затем можно продолжить построение кривой с помощью методов lineTo, quadTo или curveTo. Эти методы продолжают кривую линией, кривой второго порядка или кривой третьего порядка соответственно. Для вызова метода lineTo следует указать конечную точку, а для двух других методов построения кривых нужно сначала указать контрольные точки, а затем конечную.
path.lineTo(20, 30);
path.curveTo(controllx, controllY, control2X, control2Y, endX, endY);
Замыкание кривой задается с помощью метода closePath, который устанавливает конец линии в точке последнего вызова метода moveTo.
Например, для создания многоугольника нужно просто вызвать метод moveTo для построения первой вершины многоугольника, а затем несколько раз вызвать метод lineToдля построения остальных вершин многоугольника. Наконец, нужно вызвать метод closePath для того, чтобы замкнуть многоугольник. В листинге 7.1 это построение показано более подробно.
Маршрут не обязательно должен быть связным, т.е. метод moveTo можно вызвать, когда угодно, для начала нового сегмента кривой.
Наконец, можно использовать метод append для добавления произвольного объекта-фигуры типа Shape к данной кривой. При этом контур этой фигуры добавляется в конце данной кривой. Второй параметр метода имеет значение true, если новую фигуру необходимо связать с последней точкой траектории, и false— в противном случае. Например, показанный ниже вызов метода append добавляет контур прямоугольника к кривой, не соединяя его с ней.
Rectangle2D r= ...; path.append(r, false);
А другой приведенный ниже вызов метода append добавляет прямую линию, которая соединяет конечную точку кривой и начальную точку прямоугольника r, а затем добавляет контур прямоугольника к этой кривой.
path.append(r, true);
На рис. 7.7 и 7.8 показаны результаты работы программы, код которой приведен в листинге 7.1. Эта программа содержит поле со списком для выбора следующих типов фигур.
Прямая линия.
Прямоугольник.
Прямоугольник со скругленными углами.
Эллипс.
Дуга (с контурами описанного прямоугольника, маркером начального угла и угла развертки дуги помимо самой дуги).
Многоугольник (на основекласса GeneralPath).
Кривая второго порядка.
Кривая третьего порядка.
Для изменения расположения контрольных точек следует использовать мышь. Обратите внимание, что при их перемещении фигура автоматически перерисовывается.
Программа имеет довольно сложную структуру, поскольку она управляет множеством фигур и поддерживает автоматическую перерисовку фигур при перетаскивании контрольных точек.
Абстрактный суперкласс ShapeMaker инкапсулирует общие черты всех классов создания фигур. Каждая фигура имеет определенное количество контрольных точек, которые пользователь может передвигать, а метод getPointCount возвращает их количество. Для вычисления фактической формы фигуры на основе действительного расположения контрольных точек используется следующий абстрактный метод.
Shape make Shape(Point2D[] points);
Метод toString возвращает имя класса, которое объект ShapeMaker может поместить в объект- список JComboBox.
Для организации перетаскивания контрольных точек в классе ShapePanel нужно предусмотреть управление событиями мыши. Если кнопка мыши нажата над прямоугольным маркером контрольной точки, то последующее движение мыши перемещает этот прямоугольный маркер.
Большинство классов создания фигур имеют очень простую структуру. Их методы makeShape просто создают и возвращают требуемую фигуру. Однако, в классе ArcMaker требуется вычислить искаженные начальный и конечный углы. Как показано на рис. 7.9, для демонстрации правильности выполненных вычислений возвращаемая фигура создается на основе класса-кривой GeneralPath, которая содержит саму дугу, описанную вокруг дуги прямоугольника и линии от центра дуги к ее контрольным точкам начального и конечного углов.
Листинг 7.1.ShapeTeet.java
/** aversion 1.00 1999-07-23 ©author Cay Horstmann
*/
import java.awt. * ;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
/**
Пример рисования разных двумерных фигур
/
public class ShapeTest
{ public static void main(String[] args) {
JFrame frame = new ShapeTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Этот фрейм содержит поле со списком для выбора типа рисуемой фигуры
/
class ShapeTestFrame extends JFrame {
public ShapeTestFrame()
{
setTitle("ShapeTest");
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane() ;
final ShapePanel panel = new ShapePanel(); contentpane.add(panel, BorderLayout.CENTER);
final JComboBox comboBox = new JComboBox();
comboBox.additem(new LineMaker());
comboBox.additem(new RectangleMaker());
comboBox.additem(new RoundRectangleMaker() ) ;
comboBox.additem(new EllipseMaker());
comboBox.additem(new ArcMaker());
comboBox.addltem(new PolygonMaker() ) ;
comboBox.additem(new QuadCurveMaker());
comboBox.additem(new CubicCurveMaker());
comboBox.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
ShapeMaker shapeMaker =
(ShapeMaker)comboBox.getSelectedltem();
panel.setShapeMaker(ShapeMaker);
}
});
contentPane.add(comboBox, BorderLayout.NORTH) ; }
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
/**
В этой панели рисуется фигура и организовано перетаскивание ее контрольных точек
/
class ShapePanel extends JPanel {
public ShapePanel()
{
addMouseListener(new
MonseAdapter() {
public void mousePressed(MouseEvent event) {
Point p = event.getPoint();
for (int i = 0; i < points. length; i++) {
double x = points[ i ].getX() - SIZE / 2;
double у = points[i],.getY() — SIZE / 2; Rectangle2D r
= new Rectangle2D.Double(x, y, SIZE, SIZE); if (r.contains(p))
{
current = i;
return;
}
} )
public void mouseReleased(MouseEvent event) {
current = —1; }
));
addMouseMotionListener(new MouseMotionAdapter()
{
public void mouseDragged(MouseEvent event) {
if (current == —1) return;
points[current] = repaint();
} D ; current = —1;
}
/**
Установка создателя фигуры и инициализация его с помощью набора точек с произвольными координатами
@param aShapeMaker a shape makei that defines a shape
from a point set
*/
public void setShapeMaker(ShapeMaker aShapeMaker)
{
ShapeMaker = aShapeMaker;
int n = shapeMaker.getPointCount();
points = new Point2D[n];
for (int i = 0; i < n; i++)
{
"double x = generator.nextDouble() * getWidth();
double у = generator .nextDouble(.) * getHeight () ;
points[i] = new Point2D.Double(x, y) ;
}
repaint();
public void paintcomponent(Graphics g)
{
super.paintcomponent(g) ;
if (points == null) return;
Graphics2D g2 = (Graphics2D)g;
for (int i = 0; i < points.length; i++)
{ double x = points [i] .getX() - SIZE / 2;
double у = points[i].getY() - SIZE / 2;
g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE)) }
g2.draw(shapeMaker.makeShape(points));
}
private Point2D[] points;
private static Random generator = new Random();
private static int SIZE = 10;
private int current;
private ShapeMaker shapeMaker;
/**
Создание фигуры на основе набора контрольных точек.
Конкретные подчиненные классы должны возвратить фигуру с помощью метода makeShape
*/
abstract class ShapeMaker {
/**
Установка создателя фигуры
Sparam aPointCount the number of points needed to define this shape
*/
public ShapeMaker(int aPointCount) {
pointCount = aPointCount;
}
/** Возвращает количество контрольных точек для определения данной фигуры Sreturn the point count
*/
public int getPointCount()
{ return pointCount;
}
/**
Создает фигуру на основе заданного набора контрольных точек @param p the points that define the shape ©return the shape defined by the points */
public abstract Shape makeShape(Point2D[] p);
public String toString() {
return getClass().getName();-
}
private int pointCount; }
/**
Создатель линии, соединяющей две точки */ class LineMaker extends ShapeMaker {
public LineMaker() { super(2) ; }
public Shape makeShape(Point2D[] p) {
return new Line2D.Double(p[0], p[l]);
} }
/**
Создатель прямоугольника на основе двух угловых точек */ class RectangleMaker extends ShapeMaker {
public RectangleMaker() { super(2); }
public Shape makeShape(Point2D[] p) {
Rectangle2D s = new Rectangle2D.Double();
s. setFrameFrortiDiagonal (p[0] , p[l] ) ; return s;
}
/**
Создатель прямоугольника с закругленными углами на основе двух угловых точек */ class RoundRectangleMaker extends ShapeMaker {
public RoundRectangleMaker() { super(2) ; }
public Shape makeShape(Point2D [ ] p) {
s
= new0, 0, 0, 20, 20)
.setFrameFromDiagonal(p [ 0] , p[1]) ; return
}
/**
Создатель эллипса, вписанного в
прямоугольник на основе двух угловых точек */
classextends
{
public{}
public Shapep)
Ellipse2D s = new
. setFrameFromDiagonal(p [ 0 ] , p[1]);
return
/**
Создатель арки, вписанной в прямоугольник на основе двух угловых точек, а также начального и конечного углов на основе линий, исходящих из центра описанного прямоугольника и заканчивающихся в двух заданных контрольных Для демонстрации корректности вычисления формы дуги возращенная фигура содержит дугу, описанный вокруг нее прямоугольник, а также от центра к контрольным точкам
*/
class ArcMaker extends ShapeMaker {
public{}
public Shapep)
double=(p[0].getX()/2;
double centerY=(p[0].getY()/2;
double width =-
double height =-
double distortedStartAngle
=- centerY)
width, (p[2].getX() - centerX) * height)); double distortedEndAngle
=- centerY)
width, (p[3].getX() - centerX) * height)); double
= distortedEndAngle — distortedStartAngle;
if (distortedStartAngle < 0) distortedStartAngle += 360; if< 0)
+= 360;
Arc2D s = new0, 0, 0,
distortedstartAngle, distortedAngleDifference, Arc2D.0PEN);
s.setFrameFromDiagonal(p[0], p[l]) ;
GeneralPath g = new g.append(s, false); r = new p[l]);
; Point2D center = newcenterY)
g.append(newp[2]), false);
g.append(newp[3]), false);
return g;
}■
}
/**
Создатель многоугольника на основе'' шести угловых точек */ classextends
{ public{}
public Shapep)
{ GeneralPath s = new s.moveTo((float)p[0].getX(), (float)p[0 J. getY()); for (int i = 1; i <i++)
s.lineTo( (float)p[i],
s.closePath(); return s; ) } /**
Создатель кривой второго порядка на основе двух конечных точек и одной контрольной точки */ classextends ShapeMaker
{ public QuadCurveMaker () {}
public Shapep)
{ return new
p[l].getX(),p[2].getX(),
} }
/**
Создатель кривой третьего порядка на основе двух конечных точек и двух контрольных точек */ class CubicCurveMaker extends ShapeMaker { public{}
public Shapep)
return new CubicCurve2D.Double(p[0].getX() , p[0]. getY(), p[l].getX(), p[l].getY(), p[2].getX(), p[2].getY(), p[3] .getX() , p[3] . getYO ) ;
}
}
java.awt.geom.RoundRectangle2D.Double
Конструктор RoundRectangle2D.Double(double x, double y, double w, double h, double arcWidth, double arcHeight) создает прямоугольник со скругленными углами на основе параметров ограничивающего прямоугольника и скругляющей дуги.
Параметры:
уверхний левый угол ограничивающего прямоугольника
hширина и высота ограничивающего прямоугольника
ArcWidthширина прямоугольника, в который вписана скругляющая
эллиптическаядуга
ArcHeight высота прямоугольника, в который вписана скругляющая эллиптическаядуга
java.awt.geom.Arc2D.Double
Конструктор Arc2D. Double (double x, double y, double W, double h, double startAngle, double arcAngle, int type) создает дугу no заданному ограничивающему прямоугольнику, начальному и конечному углу и типу дуги.
Параметры:
уверхний левый угол ограничивающего прямоугольника
w, hширина и высота ограничивающего прямоугольника
startAngle угол между «•осью и линией, соединяющей центр прямоугольника с начальной точкой дуги в радианах
arcAngleразность между искаженными начальным и конечным уг
лами (подробности приведены выше в главе); для округлых дуг это значение равно углу, описываемому дугой
typeтип замыкания, т.е. одно из следующих значений:
Arc2D.OPEN,Arc2D.PIEиArc2D.CHORD
java.awt.geom.QuadCurve2D.Double
Конструктор QuadCurve2D.Double (double xl, double yi, double ctrlx, double ctrly, double x2, double y2) создает кривую второго порядка по начальной, контрольной и конечной точкам.
Параметры:
xl, yiкоординаты начальной точки
ctrlx, ctrly координаты контрольной точки х2,у2координаты конечной точки
java.awt.geom.CubicCurve2D.Double
Конструктор CubicCurve2D.Double(double xl, double yl, double ctrlxl, double ctrlyl, double ctrlx2, double ctrly2 double x2, double y2) создаеткривую третьего порядкаno начальной, двум контрольным и конечной точкам.
Параметры: xl.ylctrlxl, ctrlyl ctrlxl, ctrlyl x2, у2Параметры: xl.ylctrlxl, ctrlyl ctrlxl, ctrlyl x2, у2координаты начальной точки
Параметры: xl.yl
ctrlxl, ctrlyl ctrlxl, ctrlyl x2, у2
Параметры: xl.yl
ctrlxl, ctrlyl ctrlxl, ctrlyl x2, у2
координаты первой контрольной точки координаты второй контрольной точки координаты конечной точки
java.awt.geom. GeneralPath
Конструктор GeneralPath() создает пустую кривую общего типа.
Методvoid moveTo(float x, float у) создает текущую точку с ко-
ординатами х и у, т.е. начальную точку следующего сегмента.
Метод voidlineTo(floatx,floatу)рисуетлиниюоттекущей
точки к конечной с координатами х и у, после чего данная конечная точка становится текущей.
Метод void quadTo(float ctrlx, float ctrly, float x, float у) рисует кривую второго порядка от текущей точки к конечной с координатами х и у, после чего данная конечная точка становится текущей.
Метод void curveTo(float ctrllx, float ctrlly, float ctrl2x, float ctrl2y, float x, float у) рисует кривую третьего порядка от текущей точки к конечной с координатами х и у, после чего данная конечная точка становится текущей.
Метод void append (Shape s, boolean connect) добавляет контур данной фигуры в состав общей кривой. Если значение connect равно true, то текущая точка траектории соединяется прямой линией с начальной точкой добавленной фигуры.
Метод void closePath() замыкает кривую, рисуя прямую линию от текущей точки к первой точке траектории.
Области
В предыдущем разделе описаны способы создания сложных фигур с помощью кривых общего типа, основанных на линиях и кривых второго и третьего порядка. Используя достаточное количество линий и кривых, можно нарисовать практически любую фигуру. Например, фигуры символов в шрифтах, которые отображаются на экране и на распечатках, состоят из кривых второго и третьего порядка.
Однако, иногда легче описать фигуру с помощью композиции областей, т.е. прямоугольников, многоугольников или эллипсов; В Java 2D API поддерживаются следующие четыре метода обработки областей, которые объединяют две области в одну.
Метод add создает область, которая содержит все точки первой и второй фигуры (логическая операция объединения).
Метод subtract создает область, которая содержит все точки первой фигуры, но не входящие во вторую фигуру (логическая операция вычитания).
Метод intersect создает область, которая содержит только те точки, которые принадлежат одновременно первой и второй фигуре (логическая опера- цияпересечения).
МетодexclusiveOr создает область, которая содержит все точки, которые принадлежат первой и второй фигуре, но не принадлежат их пересечению (логическая операция "исключающее ИЛИ").
Результаты выполнения этих методов показаны на рис. 7.10.
ПересечениеИсключающее ИЛИ
Рис. 7.10. Результаты выполнения методов обработки областей
Для создания сложной фигуры нужно создать начальную область с предлагаемой по умолчанию формой.
Area a = new Area () ;
Рис. 7.11. Результат работы программы AreaTestРис. 7.11. Результат работы программы AreaTest
Рис. 7.11. Результат работы программы AreaTest
Рис. 7.11. Результат работы программы AreaTest
Затем следует скомбинировать эту область с фигурой нужной формы.
a.add(new Rectangle2D.Double(...)); a.subtract(path);
Класс Area реализует интерфейс Shape, и для полученной с его помощью фигуры можно либо очертить границу области с помощью метода draw, либо заполнить внутреннюю часть фигуры с помощью метода класса Graphics2D.
В программе, код которой приведен в листинге 7.2, показаны способы применения операций для комбинации областей. Выберите одну из четырех операций, которые показаны на рис. и посмотрите на результат комбинации эллипса и прямоугольника с помощью выбранной операции.
Листинг7.2.
/**
(aversion 1.01 2001-07-23 Cay
*/
import java.awt.*;
import
import
import import j
/**
Пример использования операций для комбинации областей
*/
public class AreaTest {
public static voidargs)
{
frame = new AreaTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Фрейм с набором переключателей для определения
операций для комбинации областей и панели для отображения результатов этих комбинаций
*/
classextends JFrame
{
public
{
setTitle("AreaTest");
HEIGHT);
areal
= new Area(new Ellipse2D.Double(100, 100, 150, 100)); area2
= new Area(new150, 150, 100));
Container contentPane = panel = new
JPanel() {
public void paintcomponent(Graphics g) {
super.paintcomponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.draw(areal) ;
g2.draw(area2) ;
if (area != null) g2.fill(area);
contentPane.add(panel, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
ButtonGroup group = new ButtonGroup();
JRadioButton addButton = new JRadioButton("Add”, false), buttonPanel.add(addButton);
group.add(addButton) ;
addBut ton. addAc t ionLi s tener (new
ActionListener() {
public void actionPerformed(ActionEvent event) <
area = new Area();
area.add(areal) ;
area.add(area2);
panel.repaint(); )
D ;
JRadioButton subtractButton
= newJRadioButton("Subtract”, false);
buttonPanel.add(subtractButton) ;
group.add(subtractButton) ;
subtractButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
area = new Area();
area.add(areal) ;
area.subtract(area2) ;
panel.repaint(); }
}) ;
JRadioButton intersectButton
= new JRadioButton(”Intersect”, false);
buttonPanel.add(intersectButton);
group.add(intersectButton) ;
intersectButton.addActionListener(new
ActionListener()
{
area = new Area();area.add(areal);area = new Area();area.add(areal);public void ac t i onPer formed(Ac t ionEvent event) {
area = new Area();
area.add(areal);
area = new Area();
area.add(areal);
area.intersect(area2);
panel.repaint();
) )) ;
JRadioButton exclusiveOrButton
= new JRadioButton("Exclusive Or", false);
buttonPanel.add(exclusiveOrButton);
group.add(exclusiveOrButton);
exclusiveOrButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
area = new Area();
area.add(areal);
area.exclusiveOr(area2);
panel.repaint();
}
}) ;
contentPane.add(buttonPanel, BorderLayout.NORTH); }
private JPanel panel;
private Area area;
private Area areal;
private Area area2;
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
java.awt.geom.Area
Метод void add (Area other) выполняет операцию объединения.
Метод void subtract (Area other) выполняет операцию вычитания.
Метод void intersect (Area other) выполняет операцию пересечения.
Метод void exclusiveOr (Area other)выполняет операцию
"исключающее ИЛИ".
Штрихи
Метод draw класса Graphics2D рисует границу фигуры с использованием выбранного типа штриха {stroke). По умолчанию для штриха используется сплошная линия с толщиной в один пиксель. Для изменения типа штриха предусмотрен метод setstroke, который в качестве параметра использует объект класса, который реализует интерфейс Stroke. В Java 2D API определен только один такой класс, а именно, класс базового штриха BasicStroke. В этом разделе рассматриваются возможности и способы использования класса BasicStroke.
Для штриха можно задавать линии произвольной ширины. Например, для рисования линии с толщиной 10 пикселей можно применить следующий код.
g2 . setstroke(new BasicStroke(10.OF) ) ; g2.draw(new Line2D.Double(...)) ;
Для штрихов с толщиной линии более одного пикселя для окончания линии можно использовать один из перечисленных ниже стилей, которые показаны на рис. 7.12.
Стык (butt cap) просто обрывает линию в конечной точке.
Овальное перекрытие (round cap) добавляет полукруг в конце штриха.
Прямоугольное перекрытие (square cap) добавляетпрямоугольниквконцештриха.
СтыкСтыкКосоесоединениеКосоесоединениеОвальноеперекрытиеОвальноеперекрытиеСкругленное соединениеСкругленное соединениеРис. 7.12. Стили окончания линийРис. 7.12. Стили окончания линийПрямоугольноеперекрытиеПрямоугольноеперекрытиеРис. 7.13. Стили пересечения штриховРис. 7.13. Стили пересечения штрихов
Стык
Стык
Косоесоединение
Косоесоединение
Овальноеперекрытие
Овальноеперекрытие
Скругленное соединение
Скругленное соединение
Рис. 7.12. Стили окончания линий
Рис. 7.12. Стили окончания линий
Прямоугольное
перекрытие
Прямоугольное
перекрытие
Рис. 7.13. Стили пересечения штрихов
Рис. 7.13. Стили пересечения штрихов
Для пересечения двух толстых штрихов также можно указать один из следующих стилей пересечения (рис. 7.13).
Косое соединение[bevel join) соединяет штрихи по прямой линии, перпендикулярной биссектрисе угла между ними.
Скругленное соединение (round join) соединяет штрихи с помощью овального перекрытия.
Фасетное соединение [miterjoiri) продолжаетштрихи с образованием клина.
Фасетное соединение не очень подходит для линий, которые пересекаются под небольшими углами. Поэтому, если две линии пересекаются под углами, меньшими предельного фасетного угла (miter limit), то вместо фасетного используется косое соединение. Это предотвращает образование чрезмерно длинных клиньев. По умолчанию предельный фасетный угол равен 10°.
Стили окончания и пересечения штрихов можно указать в конструкторе BasicStroke.
g2.setstroke(new BasicStroke(10.OF, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setstroke(new BasicStroke(10.OF, Basicstroke.CAP_BUTT,
BasicStroke.JOIN_MITTER,
15. OF /* предельный фасетный угол */)); .
Наконец, для пунктирных штрихов можно выбрать шаблон пунктира (dash pattern). В листинге 7.3 используется шаблон пунктира, который соответствует сигналу SOS по азбуке Морзе. Шаблон пунктира задается в виде массива float[ ], который содержит длины рисуемых (“on”) и нерисуемых ("off') фрагментов штриха, как показано на рис. 7.14.
10 10 10 10 10 1030103010 10 10 10 10 10301030Рис. 7.14. Пример шаблона пунктираРис. 7.14. Пример шаблона пунктира
10 10 10 10 10 10301030
10 10 10 10 10 10301030
Рис. 7.14. Пример шаблона пунктира
Рис. 7.14. Пример шаблона пунктира
При создании BasicStroke помимо шаблона пунктира указывается также фаза пунктира (dash phase), т.е. место начала шаблона в данной линии. Обычно фаза имеет значение 0.
float[] dashPattern = {10, 10, 10, 10, 10, 10, 30, 10, 30,
g2.setstroke(new BasicStroke(10.OF, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER,
10.OF /* предельный фасетный угол */,
dashPattern,
0 /* фаза пунктира */) ) ;
Стили окончания линий применяются для всех фрагментов пунктира.
Программа в листинге 7.3 позволяет указать стили окончания штриха, стиль соединения штрихов и тип пунктира (рис. 7.15). Кроме того, можно перетащить концы штрихов для проверки учета предельного фасетного угла. Для этого выберите фасетное пересечение, а затем переместите один из концов штриха так, чтобы две линии образовывали очень острый угол. После достижения заданного предельного фасетного угла фасетное соединение автоматически превратится в косое.
Рис. 7.15. Программа StrokeTestРис. 7.15. Программа StrokeTest
Рис. 7.15. Программа StrokeTest
Рис. 7.15. Программа StrokeTest
Эта программа похожа на программу, код которой приведен в листинге 7.1. Слушатель событий мыши реагирует на щелчок на конечной точке штриха и при движении указателя мыши будет перемещать вслед за ним конец штриха. Набор переключателей показывает настройки окончания линии, стиля соединения и типа пунктира. Для создания всей кривой типа GeneralPath используется метод paintComponent класса StrokePanel. Эта кривая состоит из двух отрезков, соединяющих три точки, которые пользователь может перемещать с помощью мыши. После создания кривой метод paintComponent создает класс-штрих BasicStroke в соответствии с выбором пользователя и, наконец, рисует всю кривую.
Листинг 7.3.StrokeTeet.java
/**
@version 1.01 2001-07-23
@author Cay Horstmann
*/
import java.awt.*;
import j ava.awt.event.*;
import java.awt.geom.*;
import java.util.*; import j avax.swing.*;
/**
Пример указания разных параметров штриха */ public class StrokeTest {
public static void main(String[] args) {
JFrame frame = new StrokeTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
} }
/**
Фрейм с набором переключателей для выбора стиля окончания штриха, стиля пересечения штрихов и типа линии (сплошной или пунктирной) */ class StrokeTestFrame extends JFrame {
public StrokeTestFrame() {
setTitle("StrokeTest"); setsize(WIDTH, HEIGHT);
Container contentPane = getContentPane() ; canvas = new StrokePanel() ;
contentPane.add(canvas, BorderLayout.CENTER);
buttonPanel = new JPanel();
buttonPanel.setLayout (new GridLayout(3, 3)) ; contentpane.add(buttonPanel, BorderLayout.NORTH);
ButtonGroup groupl = new ButtonGroup();
makeCapButton("Butt Cap", BasicStroke.CAP_BUTT, groupl); makeCapButton("Round Cap", BasicStroke.CAP_ROUND, groupl), makeCapButton("Square Cap", BasicStroke.CAP_SQUARE, groupl);
ButtonGroup group2 = new ButtonGroup();
makeJoinButton("Miter Join", BasicStroke.JOIN_MITER, group2);
makeJoinButton("Bevel Join", BasicStroke.JOIN_BEVEL, group2);
makeJoinButton("Round Join", BasicStroke.JOIN_ROUND, group2);
ButtonGroup group3 = new ButtonGroup();
makeDashButton("Solid Line", false, groupl);
makeDashButton("Dashed Line", true, groupl);
/**
Создание переключателей для
выбора стиля окончания штриха
Sparam label the button label
Sparam style the cap style
Sparam group the radio button group
*/
private void makeCapButton(String label, final int style, ButtonGroup group)
{
// Выбрать первый переключатель группы
boolean selected = group.getButtonCount() == 0;
JRadioButton button = new JRadioButton(label, selected) buttonpanel.add(button);
group.add(button) ;
button.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
canvas.setCap(style);
});
/**
Создание переключателей для
выбора стиля пересечения штрихов
Sparam label the button label
Sparam style the join style
Sparam group the radio button group
*/
private void makeJoinButton(String label, final int style, ButtonGroup group)
{
// Выбрать первый переключатель группы
boolean selected = group.getButtonCount() == 0;
JRadioButton buttonnew JRadioButton(label, selected);
buttonPanel.add(button);
group.add(button) ;
button.addActionListener (new
ActionListener()
(
public void actionPerformed(ActionEvent event)
{
canvas.setJoin(style);
}
}) ;
}
/**
Создание переключателей для выбора
сплошной или пунктирной линии штриха
@param label the button label
@param style false for solid, true for dashed lines
@param group the radio button group
*/
private void makeDashButton(String label, final boolean style, ButtonGroup group)
{
// Выбрать первый переключатель группы
boolean selected = group.getButtonCount() == 0;
JRadioButton button = new JRadioButton(label, selected);
buttonPanel.add(button) ;
group. add (button) ;
button.addActionListener(new.
ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.setDash(style);
) });
}
private StrokePanel canvas;
private JPanel buttonPanel;
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
/**
Панель, в которой отображаются две соединенные линии с помощью разных объектов-штрихов
В ней предусмотрено перетаскивание трех точек, которые определяют эти объекты-штрихи
*/
class StrokePanel extends JPanel {
public StrokePanel() {
addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent event) {
Point p = event.getPoint();
for (inti=0; i<points.length; i++)
double x=points[i].getX() -SIZE/2;
double у = points[i].getY() - SIZE / 2; Rectangle2D r
= new Rectangle2D.Double (x, y, SIZE, SIZE); if (r.contains(p))
{
current = i; return;
}
public void mouseReleased(MouseEvent event)
{
} current = -1;
}) ;
addMouseMotionListener(new
MouseMotionAdapter()
public void mouseDragged(MouseEvent event)
if (current == —1) return;
points[current] = repaint();
}
}) ;
points = new Point2D[3];
points[0] =newPoint2D.Double(200, 100); points[l] =newPoint2D.Double(100, 200); points[2] =newPoint2D.Double(200, 200); current = -1;
width = 10.OF;
p{ublic void paintcomponent(Graphics g) super.paintcomponent(g);
Graphics2D g2 = (Graphics2D)g;
GeneralPath path new GeneralPath() ;
path.moveTo((float)points[0].getX() , (float)points [0] .getY());
for (inti=1; i<points.length; i++)
path.lineTo((float )points[i].getX(), (float)points[i] .getY());
BasicStroke stroke;
if (dash)
{
float miterLimit =
float[] dashPattern = { 10F, 10F, 10F, 10F, 10F, 10F, 30F, 10F, 30F, 10F, 30F, 10F, 10F, 10F, 10F, 10F, 10F, 30F};
float dashPhase = 0;
stroke = new BasicStroke(width, cap, join, miterLimit, dashPattern, dashPhase);
}
else
stroke = new BasicStroke(width, cap, join);
g2.setstroke(stroke) ;
g2.draw(path);
/**
Установка стиля пересечения штрихов @param j the join style
/
public void setJoin(int j) {
join = j;
repaint();
/**
Установка стиля окончания штриха
@param с the cap style
/
public void setCap(int c)
{
cap = c;
j repaint();
/**
Установка сплошной или пунктирной пинии для штриха @param d false for solid, true for dashed lines
/
public void setDash(boolean d)
{
dash = d;
repaint();
private Point2D[] points;
private static int SIZE10;
private int current;
private float width;
private int cap;
private int join;
private boolean dash;
java.awt.Graphice2D
Метод void setStroke (Stroke s) задает параметры штриха текущего графического контекста для заданного объекта, который реализует ин-
терфейс Stroke.
java.awt.BasicStroke
Конструкторы BasicStroke (float width), BasicStroke (float width, int cap, int join), BasicStroke (float width, int cap, int join, float miterlimit), BasicStroke (float width, int cap, int join, float miterlimit, float[] dash, float dashPhase) создают объект-штрих с заданными атрибутами.
Параметры: width cap
толщинапера
стиль окончания штриха: CAP_BUTT, CAP_ROUND или
join
CAP_SQUARE
стиль соединения штрихов: JOIN_BEVEL, JOIN_MITER
miterlimit
или JOIN_ROUND
предельный фасетный угол (в градусах); если угол между пересекающимися штрихами меньше его, то фасетное
dash
соединение превращается в стыковое
шаблон пунктира; массив длин для нарисованных и
dashPhase
скрытых фрагментов пунктирного штриха
фаза пунктира; отступ от начальной точки штриха, начиная с которого применяется заданный шаблон пунктира
Раскраски
Внутренняя часть фигуры заполняется раскраской (paint). Для указания стиля раскраски для объекта, класс которого реализует интерфейс Paint, используется метод setPaint. В Java 2D API предусмотрено три таких класса-реализации интерфейса Paint.
Класс Color предназначен для заполнения фигуры сплошным цветом, и для этого нужно просто вызвать метод setPaint и указать объект Color.
g2.setPaint(Color.red);
Класс GradientPaint предназначен для заполнения фигуры градиентной заливкой, т.е. постепенно изменяющимся цветом на основе интерполяции между двумя данными значениями цветов, как показано на рис. 7.16.
КлассTexturePaint предназначен для заполнения фигуры мозаичной текстурой,
т.е. несколькими копиями рисунков, как показано на рис. 7.17.
Рис.7.16. Градиентная заливка
sfcSliSi
Color Gradient Paint • Tnrture Paint
,eeeeeeeeeeee^••••••••••••••.ieeeeeeeoeeeeeet
••••••••••••©••вeeeeoeeeeeoeeeeeэеееееееееееоеее••••••••••••ее'
>eeeeeeeeeeeer^eeeeeeeee^
Рис. 7.17. Мозаичная текстура
Для создания объекта GradientPaint нужно указать две точки и цвета для них.
g2.setPaint(new GradientPaint(pl, Color.red, p2, ColorB.blue));
Промежуточные цвета интерполируются вдоль прямой, соединяющей эти точки. Цвета остаются постоянными вдоль прямых, перпендикулярных к линии соединения. При этом для точек, которые находятся за концами линии, задаются цвета соответствующих конечных точек.
Если вызвать конструктор объекта-заливки GradientPaint с параметром cyclic, имеющим значение true, то изменение цвета будет происходить даже за пределами конечных точек, причем циклически.
g2.setPaint(new GradientPaint(pl, Color.red, p2, ColorB.blue));
Для создания объекта-текстуры TexturePaint нужно указать копируемый объект- рисунок BufferImage и прямоугольник привязки {anchor rectangle). Этот прямоугольник повторяется бесконечное количество раз в направлениях №ОСИ и уоси координатной плоскости с образованием мозаики. Заданный объект-рисунок масштабируется таким образом, чтобы соответствовать размерам прямоугольника привязки, а затем повторяется в каждом элементе мозаики.
Класс Buf feredlmage более подробно рассматривается ниже в этой главе при подробном обсуждении способов работы с рисунками. Для создания объекта типа Buf feredlmage нужно указать размер и тип рисунка. В наиболее распространенном типе рисунков TYPE_INT_ARGB каждый пиксель указывается целым числом, которое описывает альфа-канал (alpha channel) или прозрачность, а также интенсивность красного, зеленого и синего цветов.
Bufferedlmage bufferedlmage
= new Bufferedlmage(width, height, TYPE_INT_ARGB) ;
Затем нужно получить графический контекст для рисования изображения.
Graphics2D g2 = bufferedlmage.createGraphics();
Все операции рисования в графическом контексте д2 будут заполнять пикселями рисунок bu fferedlmage. После этого можно приступать к созданию объекта-текстуры.
g2.setPaint(new TexturePaint(bufferedlmage, anchorRectangle)) ;
Программа, код которой приведен в листинге 7.4, позволяет пользователю выбрать сплошную, градиентную или мозаичную раскраску для круговой области.
Для мозаичной текстуры считывается рисунок из GIF-файла. Как будет показано ниже в этой главе, метод read класса Image10 позволяет легко считывать графические файлы с помощью вызова:
bufferedlmage =
Класс ImagelOвходит в состав пакета SDK, начиная с версии 1.4. При использовании прежних версий пакета SDK нужно вместо приведенного выше оператора использовать следующий фрагмент кода для конструктора PaintPanel в листинге 7.4 и закомментировать строку с импортом пакетаjavax. imageio.
Image image =
MediaTracker tracker = new MediaTracker(this) ;
tracker.addimage(image, 0) ;
try { tracker.waitForlD(O); }
catch (InterruptedException e) {}
bufferedlmage = new Bufferedlmage (image.getwidth(null),
image.getHeight(null), Bufferedlmage.TYPE_INT_ARGB);
Graphics2D g2 = bufferedlmage.createGraphics();
g2.drawlmage(image, 0, 0, null);
Для демонстрации возможностей прямоугольника привязки в этом примере используется прямоугольник привязки с двукратным увеличением исходного рисунка.
Rectangle2D anchor = new Rectangle2D.Double(0, 0,
* bufferedlmage.getwidth(),
* bufferedlmage.getHeight());
paint = new texturePaint(bufferedlmage, anchor);
При выборе переключателя Texture Paint (Мозаичная текстура) рисунок масштабируется с учетом размеров прямоугольника привязки, а затем копируется во все элементы мозаики. Элементы мозаики, которые пересекаются с границей фигуры, при этомобрезаются.
Листинг7.4. PaintTest.java
@version 1.01 2001-07-23
@author Cay Horstmann
*/
import j
import java.awt.event.* ;
importj ava.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.*; // Использовать только для SDK версии 1.4
import j avax.swing. *;
/**
Пример использования разных типов раскраски
*/
public class PaintTest
{
public static void main(String[] args)
{
JFraine frame = new PaintTestFrame ()
frame. setDefaultCloseOperation (JFrame .EXIT_ON_CLOSE) frame.show();
}
}
/**
Фрейм с переключателями для выбора типа раскраски
и панелью, в которой отображается круговая область с выбранным типом раскраски
*/
class PaintTestFrame extends JFrame {
public PaintTestFrame()
{
setTitle("PaintTest");
setsize(WIDTH, HEIGHT);
Container contentPane = getContentPane();
canvas = new PaintPanel() ;
contentpane.add(canvas, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
true),-true),-ButtonGroup group = new ButtonGroup();
true),-
true),-
JRadioButton colorButton = new JRadioButton("Color", buttonPanel.add(colorButton) ;
group.add(colorButton);
colorButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.setcolor();
} ));
JRadioButton gradientPaintButton
=newJRadioButton("Gradient Paint", false);
buttonPanel.add(gradientPaintButton);
group. add (gradientPaintButton) ,-
gradientPaintButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
canvas.setGradientPaint();
}
JRadioButton texturePaintButton
= new JRadioButton("Texture Paint", false);
buttonPanel.add(texturePaintButton);
group.add(texturePaintButton);
texturePaintButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.setTexturePaint();
contentPane.add(buttonPanel, BorderLayout.NORTH);
)
private PaintPanel canvas;
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
/** Панель с круговой областью с выбранным типом раскраски
*/.
class PaintPanel extends JPanel {
public PaintPanel() {
try { bufferedlmage = }
catch (IOException exception) {
exception.printStackTrace();
} setColor(); }
public void paintComponent(Graphics g) {
super.paintComponent(g) ; Graphics2D g2 = (Graphics2D)g;
g2.setPaint(paint) ; Ellipse2D circle
= new Ellipse2D.Double(0, 0, getWidth(), getHeight() ) ; g2.fill(circle);
/** Раскраска сплошным цветом */
public void setColor() {
paint = Color.red; repaint();
}
/**
Градиентная заливка
*/
public void setGradientPaint()
{
paint = new GradientPaint(0, 0, Color.red,
(float)getWidth(), (float)getHeight(), Color.blue); repaint();
}
/**
Мозаичная текстура
*/
public void setTexturePaint()
{
Rectangle2D anchor = new Rectangle2D.Double(0, 0, 2 * bufferedlmage.getWidth() , 2 * bufferedlmage.getHeight());
paint = new TexturePaint(bufferedlmage, anchor); repaint();
}
private Paint paint;
private Bufferedlmage bufferedlmage;
java.awt.Graphics2D
Метод void setPaint (Paint s) задает параметры рисунка для заданного объекта, который реализует интерфейс Paint.
java.awt. GradientPaint
Конструктор GradientPaint (float xl, float yl, Color colorl, float x2, float y2, Color color2) создает градиентную заливку, которая заполняет фигуру таким образом, что начальная точка с координатами xl и yl имеет цвет colorl, конечная точка с координатами х2 и у2 имеет цвет color2, а цвет между ними изменяется линейно. Цвета остаются постоянными вдоль прямой, перпендикулярной линии, соединяющей начальную и конечные точки.
Параметры:
xl и ylкоординаты начальной точки
colorlцвет начальной точки
х2 и у2координаты конечной точки
color2цвет конечной точки
Конструктор GradientPaint (float xl, float yl, Color colorl, float x2, float У 2, Color color2, boolean cyclic) создает циклическую градиентную заливку. По умолчанию параметр cyclic имеет значение false, т.е. точки за начальной (конечной) точкой имеют цвет начальной (конечной) точки. Если параметр cyclicимеет значение true, то точки за начальной (конечной) точкой продолжают изменяться до цвета конечной (начальной) точки, а затем неограниченно циклически распространяются дальше.
Параметры
xl и ylкоординаты начальной точки
colorlцвет начальной точки
х2 и у2координаты конечной точки
color2цвет конечной точки
cyclicимеет значение true, если цвет за начальной и конечной
точками продолжает циклически изменяться, либо значение false,еслине изменяется
Конструктор GradientPaint (Point2D Pl, Color colorl, Point2D P2, Color color2) создает градиентную заливку аналогично методу GradientPaint(float xl, float yl, Color colorl, float x2, float y2, Color color2), но с указанием объекта-точки вместо координат.
Параметры:
Plначальная точка
colorlцвет начальной точки
р2конечная точка
color2цвет конечной точки
Конструктор GradientPaint (Point2D Pl, Color colorl, Point2D p2, Color color2, boolean cyclic) создает циклическую градиентную заливку аналогично методу GradientPaint (float xl, float yl, Color colorl, float x2,float y2, Color color2,
booleancyclic),носуказаниемобъекта-точкивместокоординат.
Параметры:
Plначальная точка
colorlцвет начальной точки
р2конечная точка
color2цвет конечной точки
cyclicимеет значение true, если цвет за начальной и конечной
точками продолжает циклически изменяться, либо значение false, если не изменяется
java.awt.TexturaPaint
Конструктор TexturePaint(Buf feredlmage texture, Rectangle2D anchor) создает мозаичную текстуру.
Параметры:
texture текстура, используемая для заполнения фигуры
anchor прямоугольник привязки, который определяет свойство моза- ичнойтекстуры;этотпрямоугольникбесконечноповторяется внаправленияхЛЮСИ иуоси, азаданное изображениемас- штабируетсядлязаполнениякаждогоэлементамозаики
Преобразование координат
Предположим, нужно нарисовать автомобиль по известным параметрам (высота, расстояние между осями, общая длина машины и т.д.), которые выражены в метрах.
Конечно, можно вычислять все координаты рисуемых частей автомобиля в пикселях, подсчитав количество пикселей на метр. Однако, все это можно сделать намного проще с помощью преобразования координат.
g2.scale(pixelsPerMeter, pixelsPerMeter);
g2.draw(new Line2D.Double(координаты_в_метрах));
// преобразование метрических координат
// в пиксельные и рисование линии
Метод scale класса Graphics2D выполняет преобразование координат графического контекста. При этом пользовательские координаты (т.е. единицы измерения, указанные пользователем) преобразуются в аппаратные координаты (пиксели). На рис. 7.18 показан пример такого преобразования.
Пользовательские координатыАппаратные координаты
Рис.7.18.Пользовательские и аппаратные координаты
Преобразование координат очень удобно применять на практике, потому что оно позволяет работать с координатами, выраженными в общепринятых единицах измерения. Причем графический контекст автоматически позаботится о преобразовании их в пиксели.
В платформе Java предусмотрены четыре основных типа преобразований координат.
Масштабирование: растяжение или сжатие всех расстояний от фиксированной точки.
Поворот: вращение всех точек фигуры вокруг фиксированной точки.
Перемещение: смещение всех точек фигуры на фиксированное расстояние.
Сдвиг: фиксация одной линии и перемещение всех паратлельных ей линий фигуры на расстояние, пропорциональное расстоянию отданнойлинии до фиксированной.
На рис. 7.19 показаны четыре основных типа преобразования на примере преоб-
разования квадрата.
ИсходныйквадратМасштабированныйИсходныйквадратМасштабированныйПовернутыйПовернутыйПеремещенныйСдвинутыйПеремещенныйСдвинутый
Исходный
квадратМасштабированный
Исходный
квадратМасштабированный
Повернутый
Повернутый
ПеремещенныйСдвинутый
ПеремещенныйСдвинутый
Рис. 7.19. Основные типы преобразований координат
Методы scale, rotate, translate и shear класса Graphics2D реализуют перечисленные выше типы преобразований координат.
Эти методы можно комбинировать, например, повернуть фигуру и удвоить ее размер. Для чего нужно выполнить следующие операции поворота и масштабирования.
g2.rotate(angle) ;
g2.scale(2, 2) ;
g2.draw(...);
В данном случае не имеет значения, в какой последовательности будут выполняться операции преобразования. Однако, для большинства операций преобразования порядок их применения имеет значение. Например, порядок выполнения операций следует учитывать, если нужно повернуть и сдвинуть изображение. Нужно решить, какое преобразование нужно сделать первым, чтобы добиться необходимого результата. Графический контекст выполняет преобразования в порядке, обратном порядку ввода. Это значит, что последнее введенное преобразование выполняется первым.
При выполнении преобразований координат можно задавать их любое количество. Рассмотрим следующую последовательность преобразований координат.
g2.translate(x, у);
g2.rotate(a) ;
g2.translate(-x, —у);
Последнее преобразование (которое выполняется первым) перемещает всю фигуру так, что точка (x,у) попадает в начало координат. Второе преобразование поворачивает фигуру на угол а вокруг начала координат. Последнее преобразование перемещает опять всю фигуру так, что начало координат попадает в точку с координатами (х,у). В итоге (рис. 7.20) получаем поворот фигуры вокруг центральной точки с координатами (х,у). Поскольку поворот фигуры вокруг точки, отличной от начала координат, выполняется довольно часто, то для этой операции существует соответствующий оператор.
g2.rotate(a, x, у);
Из теории матриц известно, что все повороты, масштабирования, перемещения, сдвиги и их комбинации могут быть выражены в виде преобразования матриц.
XУ1XУ1асе
X
У
1
X
У
1
Ъdf 001
Рис. 7.20. Пример комбинации преобразований координат
Такое преобразование называется аффинным преобразованием (affine transformation) и в Java 2D API описывается с помощью класса Aff ineTrans form. Объект аффинного преобразования можно непосредственно создать, если известны компоненты матрицы преобразования.
AffineTransform t = new Aff ineTransform (a, b, c, d, e, f);
Для реализации отдельных типов преобразований предусмотрены также методы getRotatelnstance, getScalelnstance, getTranslatelnstance и getShearInstance, назначение которых можно легко определить по их названию. Например, приведенный ниже вызов метода
t = AffineTransform.getScalelnstance(2.OF, 0.5F)
возвращает преобразование, которое соответствует следующей матрице аффинного преобразования координат.
2 0 0"
0 0.5 0
001
Наконец, методы setToRotate, setToScale, setToTranslation и setToShear предназначены для указания нового типа преобразования координат.
t.setToRotation(angle);
Для замены текущего преобразования аффинным преобразованием координат графического контекста предусмотрен метод setTransform.
g2.setTransform(t); // замещает текущее преобразование
Однако, на практике не рекомендуется вызывать операцию setTransform, так как она неправильно интерпретирует все существующие усеченные фигуры. Например, графический контекст для печати в альбомной ориентации страницы уже содержит преобразование-поворот на 90°. При вызове setTransformэто преобразование-поворот игнорируется. В таком случае вместо метода setTransformрекоменду- ется вызвать метод transform.
g2. transform!t);
// комбинирует текущее преобразование с преобразованием t
Оператор комбинирует текущее преобразование с новым объектом аффинного преобразования.
Если нужно применить какое-то преобразование временно, то следует сначала сохранить предыдущее преобразование, затем скомбинировать его с новым и, наконец, восстановить старое преобразование.
AffineTransform oldTransform =
// сохранение старого преобразование
g2.transform(t); // выполнение временного преобразования
// рисование в графическом контексте д2
g2.setTransform(oldTransform) ; //восстановление старого преобразования
Программа, код которой приведен в листинге 7.5, предоставляет пользователю возможность выбора одного из четырех основных типов преобразования координат. Метод paintcomponent рисует квадрат, затем применяет выбранное преобразование координат и перерисовывает квадрат. Для получения визуально приятного внешнего вида нужно, чтобы квадрат и его преобразование рисовались в центре рабочей области. Поэтому в коде метода paintComponent сначала задается преобразование-перемещение.
g2.translate(getWidth() / 2, getHeight() / 2);
Этот оператор сдвигает начало координат в центр компонента.
Затем метод paintComponent рисует квадрат с центром в начале координат.
square = new Rectangle2D.Double(-50, -50, 100, 100)
g2.setPaint(Color.gray);
g2.draw(square) ;
Однако, поскольку графическая среда применяет сдвиг к фигуре, то, в действительности, центр квадрата совпадает с центром компонента.
Затем преобразование, которое выбрал пользователь, объединяется с текущим преобразованием, и квадрат перерисовывается снова.
g2.transform(t);
g2.setPaint(Color.black);
g2.draw(square);
ИСХОДНЫЙ квадрат рисуется серым, а преобразованный — черным цветом (рис.7.21).
Рис. 7.21.Результатработыпрограммы Transf ormTestРис. 7.21.Результатработыпрограммы Transf ormTest
Рис. 7.21.Результатработы
программы Transf ormTest
Рис. 7.21.Результатработы
программы Transf ormTest
Листинг 7.5. TransfonnTest.java
/** ©version 1.01 2001-07-23 ©author Cay Horstmann
ПримерПримерimpor importimpor importj
Пример
Пример
impor import
impor import
Г ava.awt.event.*; ava.awt.geom.*; va.util.*;
| avax.swing.*;
использования преобразований координат
*/
public class TransformTest
{
public static void main(String[] args) {
JFrame frame = new TransformTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
Фрейм с набором переключателей для выбора типа преобразования и панель для отображения
результатов выбранного преобразования
*/
class TransformTestFrame extends JFrame {
public TransformTestFrame () {
setTitle("TransformTest");
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane();
canvas = new TransformPanel();
contentpane.add(canvas, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel(); ButtonGroup group = new ButtonGroup();
JRadioButton rotateButton
= new JRadioButton("Rotate", true);
buttonPanel.add(rotateButton);
group.add(rotateButton);
rotateButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.setRotate();
} });
JRadioButton translateButton
=newJRadioButton("Translate", false);
buttonPanel.add(translateButton);
group.add(translateButton) ;
translateButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.setTranslate();
} }) ;
JRadioButton scaleButton
=newJRadioButton("Scale", false);
buttonPanel.add(scaleButton);
group.add(scaleButton) ;
scaleButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.setScale();
} )) ;
JRadioButton shearButton
=newJRadioButton("Shear", false);
buttonPanel.add(shearButton);
group.add(shearButton);
shearButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.setshear();
}
}) ;
contentPane.add(buttonPanel, BorderLayout.NORTH) ;
private TransformPanel canvas;
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
/**
Панель с изображением исходного квадрата и
его вида после применения выбранного преобразования
*/
class TransformPanel extends JPanel {
public TransformPanel() {
square = new Rectangle2D.Double(-50, —50, 100, 100);
t = new AffineTransform() ;
setRotate();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.translate(getwidth() / 2, getHeight() / 2);
g2.setPaint(Color.gray);
g2.draw(square);
g2.transform (t);
/*
Здесь не используется метод setTransform, потому что нам нужно комбинировать преобразования */
g2.setPaint(Color.black);
g2.draw(square);
/**
Установка преобразования-поворота
*/
public void setRotate()
{
t.setToRotation(Math.toRadians(30)) ;
repaint();
/**
Установка преобразования-перемещения.
*/
public void setTranslate()
( t.setToTranslation(20, 15); repaint();
}
Установка преобразования-масштабирования
*/
public void setScale() {
t.setToScale(2.0, 1.5); repaint();
}
/**
Установка преобразования-сдвига
*/
public void setshear()
t.setToShear(-0.2, 0) ;
repaint();
}
private Rectangle2D square;
private AffineTransform t;
j ava. awt. geom. Aff ineTrans form
'ill
LU • Конструкторы AffineTransform (double a, double b, double c, mldouble d, double e, double f) иAffineTransform(floata, float
b,floatc,floatd,floate,floatf)выполняют аффинное пре
образование в соответствии со следующей матрицей преобразования.
асе
Ьd /
001
Конструкторы Af fineTransf orm (double []m)и AffineTrans-
form(float [] m) выполняют аффинное преобразование в соответствии co следующей матрицей преобразования.
m[0]m[2]m[4]
m[l]m[3]m[5]
001
Метод static AffineTransform getRotatelnstance(double a) выполняет поворот против часовой стрелки вокруг начала координат на угол а (в радианах). Матрица преобразования для этого случая имеет следующий вид.
’cos(a) -sin(a) 0
sin(a) cos(a) 0
1
Метод static AffineTransform getRotatelnstance(double a, double x, double у) выполняет поворот против часовой стрелки вокруг точки с координатами (х, у) на угол а (в радианах).
Метод static AffineTransform getScalelnstance(double sx, double sy) выполняет масштабирование с коэффициентами масштабирования sx и sy по х^оси и уоси соответственно. Матрица преобразования для этого случая имеет следующий вид.
SX 0 0"
0 sy
001
Метод static AffineTransform getShearlnstance(double shx, double shy) выполняет сдвиг ПО ЛЮСИ и уоси с соответствующими коэффициентами shx и shy. Матрица преобразования для этого случая имеет следующий вид.
shx 0"
shy 1 0
001
Метод staticAff ineTransform getTranslatelnstance (double
tx, double ty) выполняет перемещение ПО Х"ОСИ и уоси на значения tx и ty соответственно.
10
01O'
00 1J
Метод void setToRotation (double а) задает для данного аффинного преобразования базовую операцию поворота на заданный угол. Объяснение механизма этого преобразования и его параметров приводится в описании метода getRotatelnstance(double a).
Метод void setToRotation(double a, double x, double у) задает для данного аффинного преобразования базовую операцию поворота на заданный угол в точке с указанными координатами. Объяснение механизма данного преобразования и его параметров приводится в описании метода getRotatelnstance(double a, double x, double у).
Метод void setToScale (double sx, double sy) задает для данного аффинного преобразования базовую операцию масштабирования. Объяснение механизма этого преобразования и его параметров приводится в описании метода getScalelnstance.
Метод void setToShear (double sx, double sy) задает для данного аффинного преобразования базовую операцию сжатия. Объяснение механизма этого преобразования и его параметров приводится в описании метода getShearlnstance.
Метод void setToTranslation (double tx, double ty) задает для данного аффинного преобразования базовую операцию перемещения. Объяснение механизма этого преобразования и его параметров приводится в описании метода getTranslationinstance.
java.awt.Graphics2D
Метод void setTransform(AffineTransform t) заменяет существующее преобразование координат данного графического контекста преобразованием t.
Метод void transform(AffineTransform t) комбинирует существующее преобразование координат данного графического контекста с преобразованием t.
Метод void rotate (double а) комбинирует существующее преобразование координат данного графического контекста с поворотом вокруг начала координат. Объяснение механизма этого преобразования и его параметров приводится в описании метода AffineTrans- form.getRotatelnstance(double a).
Метод void rotate (double a, double x, double у) комбинирует существующее преобразование координат данного графического контекста с поворотом вокруг точки с заданными координатами х и у. Объяснение механизма этого преобразования и его параметров приводится в описании метода AffineTrans form.getRotatelnstance(double a, double x, double y) .
Методvoidscale(doublesx,doublesy)комбинируетсущест
вующее преобразование координат данного графического контекста с масштабированием. Объяснение механизма этого преобразования и его параметров приводится в описании метода AffineTrans- form.getScalelnstance.
Метод void shear (double sx, double sy) комбинирует существующее преобразование координат данного графического контекста со сдвигом. Объяснение механизма этого преобразования и его параметров приводится в описании методаAffineTrans orm. getShearlnstance.
Метод void translate (double tx, double ty) комбинирует существующее преобразование координат данного графического контекста со сдвигом. Объяснение механизма этого преобразования и его параметровприводитсявописании метода AffineTrans-
form.getTranslatelnstance.
Усечение
Для выполнения всех графических операций только внутри заданной фигуры пре- дусмотреноиспользование фигуры усечения (clippingshape) графическогоконтекста.
g2.setclip(clipShape); // Лучше использовать метод clip, // подробнее см. ниже
g2.draw(shape); // Рисует только часть контекста, // которая находится внутри фигуры усечения
Однако, на практике не рекомендуется вызывать операцию setclip, так как она заменяет все существующие фигуры усечения данного графического контекста. Например, как показано далее, графический контекст для печати уже содержит прямоугольник усечения, который позволяет избежать печати на полях страницы. Во избежание этого вместо метода setClip нужно вызвать метод clip.
g2.clip(clipShape) ; // Более удачный способ
Метод clip пересекает существующую фигуру усечения новой указанной фигурой.
Если нужно применить фигуру усечения только временно, то необходимо сначала сохранить старую фигуру, затем добавить новую и, наконец, восстановить старую фи- гуруусечения.
Shape oldClip = g2.getClip(); // Сохранение старой фигуры усечения
g2.clip(clipShape); // Установка временной фигуры
// Какие-то операции рисования в графическом контексте д2
g2.setclip(oldClip); // Восстанавление старой фигуры
В примере программы, код которой приводится в листинге 7.6, показаны возможности усечения набора лучей с помощью сложной фигуры усечения в виде контуров набора символов, которые показаны на рис. 7.22.
Для получения контуров символов нужно полу-
чить контекст рисования шрифтов (font render context) с помощью метода getFontRenderContext класса Graphics2D.
FontRenderContext context = g2.getFontRenderContext();
Затем следует создать объект-макет текста TextLayout, используя следующие параметры.
Строка.
Шрифт.
Контекст рисования шрифтов.
Рис. 7.22. Результат работы про- TextLayout layout = new TextLayout("Hello", граммы ClipTestfont, context);
Этот объект текста описывает макет последовательности символов в зависимости от выбранного контекста рисования, потому что одни и те же символы могут отображаться различно на экране или напечатанной странице.
Метод getOutline возвращает объект Shape, который описывает фигуру контура символов в заданном макете текста. Контур фигуры начинается в начале координат (0,0), что не очень удобно для большинства операций рисования. Поэтому нужно указать аффинное преобразование для операции рисования контуров getOutline, ко-
торое определяет, где будет появляться контур. Например, для этого можно указать перемещение базовой точки в точку с координатами (0, 100).
AffineTransform transform
=
Shape outline = layout.getOutline(transform);
Затем скомбинируем этот контур с фигурой усечения.
GeneralPath clipshape = new GeneralPath();
clipshape.append(outline, false);
Наконец, установим фигуру усечения и нарисуем набор линий, которые теперь будут отображаться только внутри контура.
g2.setClip(clipShape);
Point2D p = new Point2D.Double(0, 0);
for (int i=0; i<NLINES; i++) {
double x = ... ;
double у = ... ;
Point2D q = new Point2D.Double(x, y) ;
g2.draw(new Line2D.Double(p,q)); // Линии усекаются no контуру }
Вот как выглядит полный код этой программы.
Листинг№.6. CllpTest .java
/**
©version 1.01 2001-07-23
©author Cay Horstmann
*/
import j ava.awt.*;
import j ava.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.util.*;
import j avax.swing.*;
/**
Пример использования фигуры усечения
*/
public class ClipTest
{
public static void main(String[] args) {
JFrame frame = new ClipTestFrame();
, frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
rZ**
.,. Фрейм с флажком режима усечения, а также панель для рисования набора усекаемых линий
class ClipTestFrame extends JFrame
public ClipTestFrame() {
setTitle("ClipTest");
setSize(WIDTH,HEIGHT);
Container contentPane = getContentPane();
final JCheckBox checkBox = new JCheckBox("Clip"); checkBox.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent event) {
panel.repaint();
; contentpane.add(checkBox, BorderLayout.NORTH);
panel = new
JPanel() {
public void paintcomponent(Graphics g) {
super.paintcomponent(g) ;
Graphics2D g2 = (Graphics2D)g;
if (clipShape == null)
clipShape = makeClipShape(g2);
g2.draw(clipshape) ;
if (checkBox.isSelected()) g2.clip(clipShape);
// Рисование линий. final int NLINES =50;
Point2D p = new Point2D.Double(0, 0);
for (int i = 0; i < NLINES; i++) {
double x = (2 * getWidth() * i) / NLINES;
double у = (2 * getHeightO * (NLINES - 1 - i) i / NLINES;
Point2D g = new Point2D.Double(x, y); g2.draw(newLine2D.Double(p,q) );
} ) };
contentPane.add(panel, BorderLayout.CENTER); }
/**
Создание фигуры усечения
Sparam g2 the graphics context
©return the clip shape
*/
Shape makeClipShape(Graphics2D g2) {
FontRenderContext context = g2.getFontRenderContext(); Font f = new Font("Serif", Font.PLAIN, 100);
GeneralPath clipShape = new GeneralPath();
TextLayout layout =newTextLayout("Hello", f, context) AffineTrans form transform
=100);
Shape outline = layout.getOutline(transform);
clipShape.append(outline, false);
layout = new TextLayout("World", f, context); transform
=200);
outline = layout.getOutline(transform);
clipshape.append(outline, false);
return clipshape;
private JPanel panel;
private Shape clipShape;
private static final int WIDTH = 300;
private static final int HEIGHT = 300; }
java.awt.Graphics
Метод void setClip(Shape s) задает фигуру s в качестве текущей фигуры усечения.
Метод Shape getClip() возвращает текущую фигуру усечения.
java.awt.Graphics2D
Метод void clip(Shape s) пересекает текущую фигуру усечения с фигурой s.
Метод FontRenderContext getFontRenderContext () возвращает контекст рисования шрифтов, необходимый для создания объекта TextLayout.
java.awt.font.TextLayout
Конструктор TextLayout (String s, Font f, FontRenderContext context) создает макет текста на основе заданной строки, шрифта и контекста рисования для получения свойств шрифта для определенного устройства.
Метод Float getAdvance () возвращает ширину макета текста.
Метод Float getAscent () возвращает высоту макета текста над основной линией.
Метод Float getDescent () возвращает высоту макета текста под ос-
новной линией.
Метод Float getLeading() возвращает расстояние между соседними строками шрифта, используемого в данном макете текста.
Прозрачность и композиция
В стандартной RGB-модели цветов каждый цвет описывается на основе трех основных цветов: красного, зеленого и синего. Иногда для описания каких-то частей изображения бывает удобно использовать прозрачные (transparent) или частично прозрачные изображения. При наложении рисунка на уже существующее изображение прозрачные пиксели не закрывают пиксели, находящиеся под ними, а частично прозрачные пиксели смешивают-
ся с пикселями под ними. На рис. 7.23 показан эффект наложения частично прозрачного прямоугольника на уже имеющееся изображение. При этом элементы рисунка хорошо видны под частично прозрачным прямоугольником.
Рис. 7.23. Наложение частично прозрачного прямоугольника на рисунок
В Java 2D API прозрачность описывается с помощью альфа-канала (alpha channel). Каждый пиксель имеет (кроме красной, зеленой и синей составляющей) альфа- значение в интервале от 0 (полностью прозрачен) до 1 (не прозрачен). Например, прямоугольник на рис. 7.23 заполнен бледно-желтым цветом с прозрачностью 50%.
new Color(0.7F, 0.7F, 0.0F, 0.5F);
Теперь давайте посмотрим, что произойдет при наложении двух фигур. Нужно смешать или скомпоновать цвета и альфа-значения пикселей двух фигур. Т. Портер (Т. Porter) и Т. Даф (Т. Duff) — два исследователя в области компьютерной графики, сформулировали двенадцать возможных правил композиции, которые реализованы в Java 2D API. Следует отметить, что только два из них имеют практическое применение. Если эти правила покажутся на первый взгляд очень непонятными, то вместо них рекомендуется использовать простое правило SRC_OVER, которое применяется по умолчанию для объектов Graphics2D и дает интуитивно понятные результаты.
Рассмотрим кратко теоретические основы этих правил. Допустим, что пиксель источника (source pixel) имеет альфа-значение 0.^ а пиксель места назначения (destination pixel) — альфа- значение aff На рис. 7.24 показана диаграмма правил композиции для этих значений.
Портер и Даф считают, что альфа-значение выражает вероятность использования цвета пикселя при композиции изображений. Для пикселя источника с вероятностью a^ исходный цвет будет использоваться, а с вероятностью 1-д _ не будет. Эти же соображения верны и для пикселя назначения. Предположим, что при композиции цветов эти вероятности являются независимыми. На рис. 7.24 показаны четыре возможных случая. Если нужно преимущественно использовать цвет пикселя источника, а не пикселя назначения (эта ситуация обозначается символом S), то вероятность этого события равна a/l-aj-Аналогичным образом вычисляется вероятность аД-а,) преимущественного использования цвета пикселя назначения, а не пикселя источника (эта ситуация обозначается символом D). Что же делать, если источник и место назначения "считают" свой цвет преимущественным? Именно в этой ситуации и используются правила Порте- ра-Дафа. Если предпочтение отдается цвету источника, то в таком случае правый нижний угол диаграммы помечается буквой S, а само правило называется SRC_OVER. В нем комбинируются цвет источника с весом a$ и цвет назначения с весом a/l- a).
Визуальным эффектом применения этого правила является смешение пикселя источника и пикселя назначения с приоритетом для пикселя источника, В частности, если °.= 1, то цвет пикселя назначения вообще не принимается в расчет. А если a = 0, то пиксель источника полностью прозрачен, и цвет пикселя назначения не изменяется.
7.24. Построение правил композиции
В табл. 7.1 перечислены и на рис. 7.25 приведены другие правила, поддерживаемые в Java 2D API. Изображения на рисунке показывают результаты выполнения этих правил для прямоугольной области изображения-источника с альфа-значением и эллиптической области изображения-назначения с альфа-значением 1.0.
Правила композиции.
CLEAR
Изображение-источник удаляет пиксели изображения-назначения
SRC
Изображение-источник перезаписывает пиксели изображения- назначения и пустые пиксели
DST
Изображение-источник не влияет на изображение-назначение
SRC_OVER
Изображение-источник смешивается с изображением-назначением и перезаписывает пустые пиксели
DST_OVER
Изображение-источник не влияет на изображение-назначение и перезаписывает пустые пиксели
SRC_IN
Изображение-источник перезаписывает только пиксели изображения-назначения
SRC_OUT
Изображение-источник удаляет пиксели изображения-назначения и перезаписывает пустые пиксели
DST_IN
Изображение-источник модифицирует только общие пиксели изображения-источника и изображения-назначения, причем в
"
ветствии с альфа-значениями изображения-назначения
Окончание табл. 7.1
DST_OUT
Изображение-источник модифицирует только общие пиксели изображения-источника и изображения-назначения, причем в соответствии с разницей 1 минус альфа-значение пикселя изображения-назначения
SRC_ATOP
Часть изображения-источника, лежащая внутри изображения- назначения, смешивается с изображением-назначением
DST_ATOP
Часть изображения-назначения, лежащая внутри изображения- источника, смешивается с изображением-источником и замещает изображение-назначение
XOR
Часть изображения-источника, которая лежит вне изображения- назначения, смешивается с частью изображения-назначения, которая лежит вне изображения-источника
Как можно заметить, большинство этих правил вряд ли применяются на практике. Рассмотрим правило DST_IN как крайний случай, в котором вовсе не принимается в расчет цвет пикселя-источника, но используется альфа-канал пикселя-источника для изменения пикселя-назначения. Однако, правило SRC потенциально очень полезно, потому что заставляет использовать цвет источника, выключая смешение с цветом назначения.
ПравилаDST, SRC_ATOP, DST_ATOP И XOR были добавлены в SDK1.4.
Более подробно о правилах Портера-Дафа можно прочесть в разделе 17.6.1 книги Джеймса Фоли (James D. Foley), Андриса ван Дама (Andries van Dam), Стивена Фэйне- Pa(Steven К. Feiner) идр. Computer Graphics:Principles and Practice, Second Edititon [Addison Wesley 1995].
Для создания объекта-правила композиции, который реализует интерфейс Composite, используется метод setComposite класса Graphics2D. В интерфейсе Java 2D API содержится только один такой класс, а именно, AlphaComposite, который реализует восемь правил Портера-Дафа (рис. 7.25).
Для создания экземпляра правила типа AlphaComposite нужно указать для метода getlnstance класса AlphaComposite правило и альфа-значение для пикселей изображения-источника.
int rule = AlphaComposite.SRC_OVER;
float alpha = 0.5;
g2.setComposite(AlphaComposite.getlnstance(rule, alpha));
g2.setPaint(Color.blue);
g2.fill(rectangle);
В данном фрагменте кода прямоугольник зарисовывается синим цветом, и для него задается альфа-значение, равное 0.5. Поскольку установлено правило композиции SRC_OVER, то его пиксели смешиваются с учетом коэффициента прозрачности с уже существующим изображением.
S DSS DSS DDS DDSRC OUTDST INDST_OUTSRC_ATOPDSTATOPXORSRC OUTDST INDST_OUTSRC_ATOPDSTATOPXORDDssDDDSDSsDsDS DS D
S DS
S DS
S DD
S DD
SRC OUT
DST IN
DST_OUT
SRC_ATOP
DSTATOP
XOR
SRC OUT
DST IN
DST_OUT
SRC_ATOP
DSTATOP
XOR
D
D
s
s
D
D
DS
DS
sD
sD
S D
S D
Рис. 7.25. Правила композиции Портера-Дафа
Программа, код которой приведен в листинге 7.7, позволяет исследовать правила композиции. Выберите правило из поля со списком и используйте ползунок для установки альфа-значения для прямоугольника, который является изображением- источником типаAlphaComposite.
Кроме того, в нижней части окна программы отображается описание каждого правила. Обратите внимание, что это описание автоматически вычисляется на основе диаграммы правил композиции. Например, строка DS во втором ряду диаграммы приводит к включению строки blends with (смешивается с) в результирующее описание.
Однако, эта программа не гарантирует, что используемый графический контекст имеет альфа-канал, что довольно часто имеет место. Когда пиксели накладываются на изображение-назначение без альфа-канала, то цвета пикселей умножаются на альфа- значение, и альфа-значение отбрасывается. Поскольку несколько правил Портера- Дафа используют альфа-значения для изображения-назначения, наличие этого значения играет важную роль. Поэтому для композиции данных фигур используется буферное изображение с цветовой моделью ARGB. После комбинирования рисунков полученный в результате рисунок будет отображен на экране.
Bufferedlmage image = new Bufferedlmage(getWidth() , getHeight(),Bufferedlmage.TYPE_INT_ARGB);
Graphics2D gImage =
// Теперь рисуем с помощью glmage.
g2.drawlmage(image, null, 0, 0) ;
В листинге 7.7 приведен полный код этой программы, а на рис. 7.26 показан ее внешний вид. После запуска программы переместите ползунок слева направо для того, чтобы посмотреть результат применения выбранного правила композиции фигур для разных альфа-значений. Обратите особое внимание на то, что единственное различие между правилами DST_IN и DST_OUT состоит в направлении изменения цвета изображения-назначения(!) при изменении альфа-значения изображения-источника.
Рис. 7.26. Результат работы программы CompositeTest
р*®^$ц|8$8$д|ц||||М!|М№яИ|№|™||В|йЕ||$||||^ж#^
Листинг7.7. CoepoelteTeet .Java
/ * *
aversion 1.01 2001-07-24
Sauthor Cay Horstmann
import j ava.awt.*;
import j ava.awt.event.*;
import j ava.awt.image.*;
import j ava.awt.geom. *;
import java.util.*;
import j avax.swing.*;
import javax.swing.event.*;
Демонстрация правил Портера-Даффа
для комбинации изображений
*/
public class CompositeTest
{
public static void main(String[] args) {
JFrame frame = new CompositeTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
}
}
Фрейм содержит поле со списком для выбора правила композиции, ползунок для изменения альфа-значения, а также панель с демонстрацией выбранного правила
*/
class CompositeTestFrame extends JFrame
Прозрачность и композиция 579
p{ublicCompositeTestFrame()
setTitle("Compos!teTest") ;
setSize(WIDTH,HEIGHT);
ContainercontentPane=getContentPane() ;
canvas = new CompositePanel() ;,
contentPane. add (canvas, BorderLayout. CENTER)
ruleCombo=newJComboBox(new Object!]
new'" CLEAR # 11 11 #**) /
nneeww Rufe'"SRC", "S", "S"), new Rule("DST", " ", "DD"), new Rule("SRC_OVER", "S", "DS"), new Rule("DST_OVER", "S", "DD"), new Rule("SRC_IN"," IIII S"), new Rule("SRC_OUT", "SIII,I " "), new Rule("DST_IN", " II/II D"), new Rule("DST_OUT",f"D"),
ruleCombo.addActionListener(new ActionListener()ruleCombo.addActionListener(new ActionListener(){{new Rule("SRC_ATOP", n N' "DS"), new Rule("DST_ATOP", " S", "D"), new Rule("XOR","S", "D "),
ruleCombo.addActionListener(new ActionListener()
ruleCombo.addActionListener(new ActionListener()
{
{
p{ublic void actionPerformed(ActionEvent event)
Ruler= (Rule)ruleCombo.getSelectedItem(); canvas.setRule(r.getValue());
explanation.setText(r.getExplanation());
} });
alphaSlider =newJSlider(0, 100, 75);
alphaSlider.addChangeListener(new
{
p{ublic void stateChanged(ChangeEvent event)
canvas.setAlpha(alphaSlider.getValue()); )
JPanel panel = new JPanel();
panel.add(ruleCombo);
;
panel.add(alphaSlider);
contentPane.add(panel, BorderLayout.NORTH);
explanation = new JTextField();
contentPane.addfexplanation, BorderLayout.SOUTH);
canvas.setAlpha(alphaSlider.getValue());
Ruler= (Rule)ruleCombo.getSelectedItem();
canvas.setRule(r.getValue());
explanation.setText(r.getExplanation());
private CompositePanel canvas;
private JComboBox ruleCombo;
private JSlider alphaSlider;
private JTextField explanation;
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
/**
Правило Портера-Даффа
*/ class Rule {
/**
Создание правила Портера-Даффа
Sparam n the rule name
@param pdl the first row of the Porter-Duff square
Sparam pd2 the second row of the Porter-Duff square */
public Rule(String n, String pdl, String pd2) {
name = n;
porterDuffl = pdl;
porterDuf£2 = pd2; }
/**
Создание строки с объяснением правила
©return the explanation
*/
public String getExplanation() {
StringBuffer r = new StringBuffer("Source ") ;
if (porterDuf£2.equals(""))
r.append("clears") ;
if (porterDuf£2.equals(" S")) r.append("overwrites") ;
if (porterDuff2.equals("DS")) r.append("blends with");
if (porterDuf£2.equals(" D")) r.append("alphamodifies");
if (porterDuff2.equals("D "))
r.append("alpha complement modifies");
if (porterDuff2.equals(”DD")) r.append("doesnot affect");
r.append(" destination");
if (porterDuffl.equals(" S"))
r.append(" and overwrites empty pixels");
r.append(".");
return r . toString(); )
public String toString() {
return name; }
/**
Возвращает значение правила в классе AlphaComposite @return the AlphaComposite constant value, or —1 if there is no matching constant.
*/
public int getValue()
try
Integer i = (Integer)
AlphaComposite.class.getField(name).get(null);
} return i.intValue();
catch (Exception ex)
} return —1;
private String name;
private String porterDuffl;
private String porterDuff2;
/**
Панель с двумя фигурами, которые комбинируются
на основе выбранного правила композиции
*/
class CompositePanel extends JPanel {
public CompositePanel() {
shapel = new Ellipse2D.Double(100, 100, 150, 100);
shape2 = new Rectangle2D.Double(150, 150, 150, 100)) }
public void paintcomponent(Graphics g) {
super.paintcomponent(g) ;
Graphics2D g2 = (Graphics2D)g;
Bufferedlmage image = new Bufferedlmage(getWidth(), getHeight(), Bufferedlmage.TYPE_INT_ARGB);
Graphics2D gImage =
glmage.setPaint(Color.red);
glmage.fill(shapel);
AlphaComposite composite
=alpha);
glmage.setComposite(composite);
glmage.setPaint(Color.blue);
glmage.fill(shape2);
g2.drawlmage(image, null, 0, 0) ;
/**
Указание правила композиции
@param r the rule (as an AlphaComposite constant) */
public void setRule(int r) {
rule = r;
repaint();
/“
Указание альфа-значения изображения-источника @param a the alpha value between 0 and 100
*/
public void setAlpha(int a)
{
alpha = (float)a / 100.OF;
repaint();
private int rule;private Shape shapel; private Shape shape2; private float alpha;private int rule;private Shape shapel; private Shape shape2; private float alpha;}}}
private int rule;
private Shape shapel; private Shape shape2; private float alpha;
private int rule;
private Shape shapel; private Shape shape2; private float alpha;
}
}
java.awt.Graphics2D
Метод void setComposite (Composite s) создает объект- композицию для данного графического контекста на основе заданного объекта, который реализует интерфейс Composite.
java.awt.AlphaComposite
Метод static AlphaComposite getlnstance(int rule) создает объект-композицию на основе заданного правила композиции.
Параметры:
ruleодно из правил композиции: CLEAR, DST, SRC, SRC_OVER,
DST_OVER, SRC_ATOP SRC_IN, SRC_OUT, DST_ATOP, DST_IN, DST_OUT, XOR
• Метод static AlphaComposite getlnstance(int rule,float
alpha) создает объект-композицию на основе заданного правила композиции и с учетом альфа-канала.
Параметры:
ruleодно из правил композиции: CLEAR, DST, SRC, SRC_OVER,
DST_OVER, SRC_ATOP, SRC_IN, SRC_OUT, DST_ATOP, DST_IN, DST_OUT, XOR
alphaальфа-значение для пикселей изображения-источника
Параметры рисования
В предыдущем разделе показано, что алгоритм рисования имеет довольно сложную последовательность действий. Несмотря на достаточно большую скорость выполнения функций Java 2D API для многих приложений, все же есть случаи, когда требуется найти компромисс между достижением еще большей скорости выполнения приложения и высоким качеством его внешнего вида. Именно для этого в Java 2D API предусмотрены параметрырисования^rendering hints), каждыйизкоторыхможноуказатьспомощьюметода setRenderingHint класса Graphics2D. В табл. 7.2 представлены коды, значения и описания параметров рисования, которые объявлены в классе RenderingHints.
Наиболее полезным параметром является сглаживание (antialiasing), т.е. технология удаления неровностей при рисовании кривых линий. Как показано на рис. 7.27, кривая линия рисуется в виде "лестницы" из расположенных уступом пикселей. Иногда она может выглядеть просто ужасно, особенно на мониторах с небольшим разреше-
нием. Но если вместо рисования пикселей кривой на основе только двух контрастных цветов (например, черного или белого) нарисовать ее на основе переходных цветов, значения которых пропорциональны занимаемой линией площади пикселя, то такая кривая будет выглядеть более гладкой. Этот метод рисования кривых называется сглаживанием. Конечно, для его выполнение требуется немного больше времени, которое расходуется на расчет значений переходных цветов.
Таблица7.2. Параметры рисования
Ключ
ЗначениеОписание
KEY_ANTIALIAS ING
VALUE_ANTIALIAS_ON Включение/отключение сгла-
VALUE_ANTIALIAS_DEFAULTживания фигур
KEY RENDERING
VALUE_RENDER_QUALIT YО ЛГАПТТТЛ TOттттгт
VALUE_RENDER_SPEEDВыбор алгоритма рисования для
VALUE_RENDER_DEFAULTдостижения лучшего качества
или большей скорости
KEY_DITHERING
VALUE_DITHER„ENABLED,
VALUE_DITHER_DISABLEВключение/отключение раз-
VALUE_DITHER_DEFAULTмытия цветов. Размытие усред
няет значения цветов группы пикселей похожего цвета
KEY TEXT ANTIALIASING
VALUE_TEXT_ANTIALIAS_ONTIt^TTTTJTTQTTTTQ //'1ТТЛПТГ1ГТДТТТ1Д r'T-TTO
VALUE_TEXT_ANTIALIAS_OFFВключение/отключение сгла-
VALUE_TEXT_ANTIALIAS_DEFживания шрифтов
AULT
KEY_FRACTIONAL_METRICS
VALUE_FRACTIONALMETRICS
ONВключение/отключение вы-
VALUE_FRACTIONALMETRICSчисления дробных размеров
OFFшрифтов. Использование дроб-
VALUE_FRACTIONALMETRICSных размеров шрифтов позво-
DEFAULTляет улучшить их расположение
KEY_ALPHA_INTERPOLATION
VALUE_ALPHA_INTERPOLATIO „,
N QUALITYВключение/отключение точно-
VALUE_ALPHA_INTERPOLATIO го вычисления композиции на
N_SPEEDоснове альфа-значений
VALUE_ALPHA_INTERPOLATIO
N_DEFAULT
KEY_COLOR_RENDERING
VALUE_COLOR_RENDER_QUALIТ1ТТЫ
TYВыбор качества или скорости
VALUE_COLOR_RENDER_SPEEDпри отображении цветов
valueZcolorZrenderZdefau
. KEY-INTERPOLATION
vIlue_™olation_near„ .
EST NEIGHBORВыбор правилаинтерполяции
VALUE_INTERPOLATION„BILIпикселей при масштабировании
NEARрисунка
VALUE_INTERPOLATION_BICU
BIC
■ KEY_STROKE_CONTROL
VALUE„STROKE_NORMALIZEП1.1ЬПП ппооттла т^плт^ттттттпптюттттст
VALUE STROKE PUREВыбор правила комбинирования
valueZstrokeZdefaultштрихов
Итак, для включения сглаживания фигур нужно использовать следующий метод.
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
А для включения сглаживания шрифтов используется следующий код.
g2.setRenderingHint (RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
Остальные параметры рисования используются гораздо реже.
Пары ключ/значение параметра рисования можно сгруппировать в коллекции- карте и задавать их одновременно с помощью метода setRenderingHints. Для этого подойдет любой класс-коллекция, реализующий интерфейс Map, а значит и сам класс setRenderingHints, который также реализует интерфейс Map и создает объект- коллекцию по умолчанию, если параметр его конструктора имеет значение null.
RenderingHints hints = new RenderingHints (null) ;
hints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(hints) ;
Применение такого метода демонстрируется в примере программы, код которой приводится в листинге 7.8. Программа рисует изображение в соответствии с заданными параметрами рисования. Не все платформы поддерживают работу с этими параметрами, так что использование некоторых из них может не оказать никакого влияния на рисунок. В операционной системе Windows 98 сглаживание фигур улучша-
ет вид эллипсов, сглаживание текста улучшает вид курсивного шрифта, а все остальные параметры никак не влияют на изображение, но могут сказаться при работе на других платформах или при работе с более сложными рисунками.
На рис. 7.28 представлена данная программа.
KEV ANTIALIASINGKEV ANTIALIASINGКГ» DITHERING <EY3DT АЧ1 IALIASCNC ■ <FY_ FRACTIONALMFTRICS KEY-ALPHA-INTTHPOI ATION <FV.COLOR RENDERING •STROKE CONTROLКГ» DITHERING <EY3DT АЧ1 IALIASCNC ■ <FY_ FRACTIONALMFTRICS KEY-ALPHA-INTTHPOI ATION <FV.COLOR RENDERING •STROKE CONTROLVAI lit.ANTIALIAS ONVAI lit.ANTIALIAS ONVAI Ili^RC NDfH OUAI СП' VAI iJLDITHFR.FNABLEVALUL MM-AM1AIIA&.0N‘ VALUE FRACT1ONA1 METRICS-OSVAI Ili^RC NDfH OUAI СП' VAI iJLDITHFR.FNABLEVALUL MM-AM1AIIA&.0N‘ VALUE FRACT1ONA1 METRICS-OSVAI UE ANTIALIAS OFFVALUE HINDER SPEEDVALUE DITHER DISABLEVAI UE ANTIALIAS OFFVALUE HINDER SPEEDVALUE DITHER DISABLEVALUE TEXT-ANTIALIAS OFFV<U UE FRACTIOHALMEHUCS OF*VALUE TEXT-ANTIALIAS OFFV<U UE FRACTIOHALMEHUCS OF*VAI III.Al PHA INTEHPOLATION-QIIAI IT» VALUE ALPHA INTtRPOLATION.SPE£DVAI III.Al PHA INTEHPOLATION-QIIAI IT» VALUE ALPHA INTtRPOLATION.SPE£D' VALUE.COLOR.RFNDFR.QUALmUiur MROFE MORMAUZE' VALUE.COLOR.RFNDFR.QUALmUiur MROFE MORMAUZEVALUE COLOR RENDER SPEH)VALUE STROKE PUREVALUE COLOR RENDER SPEH)VALUE STROKE PURE
KEV ANTIALIASING
KEV ANTIALIASING
КГ» DITHERING <EY3DT АЧ1 IALIASCNC ■ <FY_ FRACTIONALMFTRICS KEY-ALPHA-INTTHPOI ATION <FV.COLOR RENDERING •STROKE CONTROL
КГ» DITHERING <EY3DT АЧ1 IALIASCNC ■ <FY_ FRACTIONALMFTRICS KEY-ALPHA-INTTHPOI ATION <FV.COLOR RENDERING •STROKE CONTROL
VAI lit.ANTIALIAS ON
VAI lit.ANTIALIAS ON
VAI Ili^RC NDfH OUAI СП
' VAI iJLDITHFR.FNABLE
VALUL MM-AM1AIIA&.0N
‘ VALUE FRACT1ONA1 METRICS-OS
VAI Ili^RC NDfH OUAI СП
' VAI iJLDITHFR.FNABLE
VALUL MM-AM1AIIA&.0N
‘ VALUE FRACT1ONA1 METRICS-OS
VAI UE ANTIALIAS OFF
VALUE HINDER SPEED
VALUE DITHER DISABLE
VAI UE ANTIALIAS OFF
VALUE HINDER SPEED
VALUE DITHER DISABLE
VALUE TEXT-ANTIALIAS OFF
V<U UE FRACTIOHALMEHUCS OF*
VALUE TEXT-ANTIALIAS OFF
V<U UE FRACTIOHALMEHUCS OF*
VAI III.Al PHA INTEHPOLATION-QIIAI IT» VALUE ALPHA INTtRPOLATION.SPE£D
VAI III.Al PHA INTEHPOLATION-QIIAI IT» VALUE ALPHA INTtRPOLATION.SPE£D
' VALUE.COLOR.RFNDFR.QUALmUiur MROFE MORMAUZE
' VALUE.COLOR.RFNDFR.QUALmUiur MROFE MORMAUZE
VALUE COLOR RENDER SPEH)
VALUE STROKE PURE
VALUE COLOR RENDER SPEH)
VALUE STROKE PURE
Рис. 7.28. Результат работы программы RenderQuali tyTes t
Листинг7.8. RenderQualityTeat.java
©version 1.01 2001-07-24
©author Cay Horstmann
import java.awt.*;
import j ava.awt.event.*;
import java.awt.geom.*;
import java.io.*;
import java.util.*;
import javax.imageio.*;
import j avax.swing.*;
/ * *
Пример применения параметров рисования */
public class RenderQualityTest
JFrame frame = new RenderQualityTestFrame();frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();JFrame frame = new RenderQualityTestFrame();frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();public static void main(String[] args) {
JFrame frame = new RenderQualityTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
JFrame frame = new RenderQualityTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
/**
Фрейм с кнопками для указания параметров рисования
и рисунка, который получается в результате их применения */
class RenderQualityTestFrame extends JFrame {
public RenderQualityTestFrame()
{
setTitle("RenderQualityTest”) ;
setsize(WIDTH, HEIGHT);
buttonBox = new JPanel();
buttonBox.setLayout(new GridLayout(9, 3));
hints = new RenderingHints(null);
makeButtons(”KEY_ANTIALIASING",
* VALUE_ANT IALI AS_ON" , ”VALUE_ANTIALIAS_OFF") ;
makeButtons("KEY_RENDERING”, "VALUE_RENDER_QUALITY” , "VALUE_RENDER_SPEED") ;
makeButtons("KEY_DITHERING",
"VALUE_DITHER_ENABLE", "VALUE_DITHER_DISABLE") ;
makeButtons(”KEY_TEXT_ANTIALIASING", "VALUE_TEXT_ANTIALIAS_ON" , "VALUE_TEXT_ANTIALIAS_OFF") ;
makeButtons("KEY_FRACTIONALMETRICS",
"VALUE_FRACTIONALMETRICS_ON" , "VALUE_FRACTIONALMETRICS_OFF");
makeButtons(”KEY_ALPHA_INTERPOLATION", "VALUE_ALPHA_INTERPOLATION_QUALITY", "VALUE_ALPHA_INTERPOLATION_SPEED");
makeButtons("KEY_COLOR_RENDERING”, "VALUE_COLOR_RENDER_QUALITY", "VALUE_COLOR_RENDER_SPEED") ;
makeButtons("KEY_INTERPOLATION",
"VALUE_INTERPOLATION_NEAREST NEIGHBOR", "VALUE_INTERPOLATION_BILINEAR”);
makeButtons("KEY_STROKE_CONTROL" , "VALUE_STROKE_NORMALIZE" , "VALUE„STROKE_PURE") ;
Container contentPane = getContentPane();
canvas = new RenderQualityPanel() ;
contentPane.add(canvas, BorderLayout.CENTER);
contentPane.add(buttonBox, BorderLayout.NORTH);
/**
Создает название ключа и набор кнопок для его значений
Sparam key the key name
Sparam valuel the name of the first value for the key
Sparam value2 the name of the second value for the key */
void makeButtons(String key, String valuel, String value2) (
try
{
final RenderingHints.Key к = (RenderingHints.Key) RenderingHints.class.getField(key).get(null);
final Object vl
=
final Object v2 =
JLabel label = new JLabel(key);
buttonBox.add(label) ;
ButtonGroup group = new ButtonGroup();
JRadioButton Ы = new JRadioButton(valuel, true);
buttonBox.add(bl) ;
group.add(bl);
bl.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
hints.put(k, vl) ;
canvas.setRenderingHints(hints);
D ;
JRadioButton b2 = new JRadioButton(value2, false);
buttonBox.add(b2);
group.add(b2);
b2.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
hints.put(k,v2);
canvas.setRenderingHints(hints);
})
hints.put(k,vl);
}
catch (Exception ex)
}
)
private RenderQualityPanel canvas;
private JPanel buttonBox;
private RenderingHints hints;
private static final int WIDTH = 600;
private static final int HEIGHT = 500;
/**
Панель с рисунком, который получается
в результате применения параметров рисования */
class RenderQualityPane1 extends JPanel
{
public RenderQualityPanel()
{
colorl = new Color(0.7F, 0.7F, 0.0F, 0.5F)
color2 = new Color(0.0F, 0.3F, 0.3F, 0.5F)
try
image = ImagelO.read(new File("clouds.jpg")); }
catch (IOException exception)
(
exception.printStackTrace();
)
public void paintcomponent(Graphics g) {
super.paintcomponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHints(hints);
g2.drawlmage(image, 0, 0, null);
g2.draw(new Ellipse2D.Double(0, 0,
image.getWidth(null), image.getHeight(null)));
. g2.setFont(new Font("Serif", Font.ITALIC, 40));
g2.drawString("Hello”, 75, 75);
g2.setPaint(color1);
g2.translate(0,-80),-
g2 . fill (new Rectangle2D.Double (100, 100, 200, 100));
g2.setPaint(color2);
g2 . fill (new Rectangle2D. Double (120, 120, 200, 100));
/**
Установка параметров и перерисовка
Sparam h the rendering hints
*/
public void setRenderingHints(RenderingHints h) {
hints = h;
repaint();
)
private RenderingHints hints = new RenderingHints(null);
private Color colorl;
private Color color2;
private Image image;
<?« L_< а/й/<?« L_< а/й/java.awt•Graphics2D
<?« L_< а/й/
<?« L_< а/й/
Метод void setRenderingHint(RenderingHints.Key key, Object value) устанавливает параметры рисования для данного графического контекста.
Метод void setRenderingHints (Map m) устанавливает все параметры рисования, пары ключи/значения которых хранятся в коллекции-карте т.
java.awt.RenderingHints
Конструктор RenderingHints (Map m) создает коллекцию-карту для хранения параметров рисования. Если параметр m имеет значение null,
то реализуется предлагаемая по умолчанию коллекция-карта.
Чтение и запись изображений
До версии 1.4 в пакете Java SDK были предусмотрены весьма ограниченные возможности чтения и записи файлов с изображениями. Например, имелись средства для чтения файлов формата GIF и JPEG, но не было вообще никакой официальной поддержки записи графических файлов.
В пакете SDK версии 1.4 ситуация значительно изменилась в лучшую сторону. В нем представлен отдельный пакет javax. image io с готовыми средствами чтения и записи нескольких основных форматов графических файлов, а также предложена архитектура, которая позволяет сторонним разработчикам создавать собственные утилиты для чтения и записи других форматов. Теперь в пакете SDK предусмотрено чтение файлов формата GIF, JPEG и PNG, а также запись файлов формата JPEG и PNG (запись файлов формата GIF не осуществляется, вероятно, из-за патентных ограничений).
При использовании прежних версий SDK разработчику придется поискать информацию о способах записи GIF- и JPEG-файлов. Например, кодировщик GIF-файлов можно найти по адресу www. acme. com/java/. Кроме того, компания Sun Microsystems предлагает пакет com.sun.image.codec.jpeg, являющийся частью SDK, и поддерживает сохранение рисунков в формате JPEG. Более подробную информацию об этом пакете можно найти по адресу http: / / j ava.sun. com/products/jdk/1.2/docs/guide/2d/ api-j peg/overview-summary.html.
Данный пакет имеет чрезвычайно простую структуру. Для загрузки рисунка используется статический метод read класса Image 10.
File f = .. . ;
Bufferedlmage image = ImagelO.read(f);
Класс ImagelO выбирает соответствующую программу чтения на основе указанного типа файла. Для чего может проверить расширение файла и соответствующее значение в заголовке файла. Если для чтения данного файла нельзя найти подходящей программы или она не может расшифровать содержимое файла, то метод read возвращает значение null.
Запись рисунка в файл организована так же просто, как и чтение.
File =
String format= ...;
ImagelO.write(image, format, f;
Строка format содержит формат рисунка (например, JPEG или PNG). Класс ImagelO аналогично выбирает соответствующую программу записи и сохраняет рисунок.
Получение программ чтения и записи графических файлов
Для выполнения расширенных операций записи и чтения изображений, которые выходят за пределы простого использования статических методов read и write класса ImagelO, необходимо прежде получить соответствующие объекты ImageReader и ImageWriter. В классе ImagelO перечисляет все программы чтения и записи графических файлов, используя один из следующих параметров.
Формат изображения, например JPEG.
Расширение файла, например jpg.
MIME-тип, напримерimage/j peg.
Набор стандартов MIME (Multipurpose Internet Mail Extensions) определяет общие форматы данных (например, “image/jpeg", “application/pdf") для передачи мульти- Dмедийной информации посредством электронной почты. HTML-версию RFC-
документа (Requests for Comments) с описанием стандартов MIME можно найти по адресуhttp://ww.oac.uci.edu/indiv/ehood/MIME.
Например, с помощью показанного ниже кода можно получить программу чтения JPEG-файлов.
ImageReader reader = null;
Iterator iter = ImagelO. getlmageReaderByFormatName ("JPEG'*) ; if (iter.hasNext) reader = (ImageReader)iter.next();
Методы getImageReaderBySuff ix и getlmageReaderByMIMEType возвращают программы чтения графических файлов с указанным расширением или MIME-типом.
Класс ImagelO может найти несколько программ чтения, каждая из них способна считывать данный тип файла. В этом случае следует выбрать одну из программ на основе более подробной информации, которую можно получить с помощью интерфейса поставщика услуг (service providerinterface).
ImageReaderSpi spi = reader.getOriginatingProvider() ;
Затем можно получить названия изготовителя и номер версии программы чтения графических файлов.
String vendor = spi.getVendor();
String version = spi.getVersion();
Возможно, эта информация поможет сделать выбор нужной программы чтения или предоставит возможность создать список доступных программ чтения, для того чтобы пользователи могли выбрать наиболее подходящую программу по своему усмотрению.
Далее предполагается, что нам подходит первая программа такого списка.
В примере программы, код которой приводится в листинге 7.9 в конце этого раздела, предложен способ решения некоторых проблем, которые не решены в Java API. Во- первых, нужно найти все расширения доступных программ чтения графических файлов, так чтобы можно было использовать их в фильтре отбора файлов. Класс lOImage может предоставить названия всех считываемых форматов и названия всех MIME-типов. Но он не может перечислить все поддерживаемые расширения файлов, а также получить все программы чтения. Поэтому прежде необходимо нужно перечислить все форматы, а затем программы чтения данного формата. Объект^читатель типа ImageReader не может представить все поддерживаемые расширения файлов, но это может сделать объект- интерфейс поставщика услуг. Управление всеми расширениями файлов возможно организовать с помощью нескольких вложенных циклов и метода addAll интерфейса Set. Полный текст соответствующего метода getRenderSuff ixes представлен в листинге 7.9.
Аналогичная проблема возникает при сохранении файлов. Для представления пользователю списка команд меню со всеми поддерживаемыми форматами изображений можно было бы использовать метод getWriterFormaNames класса IOImage.
Но он возвращает довольно странный список с несколькими избыточными вариантами названий форматов.
JPG
jpeg jpg
PNG
JPEG
png
Это не совсем то, что хотелось бы видеть в списке команд меню. Во-первых, желательно иметь одно предпочтительное название формата. Для сокращения размеров этого избыточного списка можно выбрать первое название нужного формата, а потом найти первую связанную с ним программу записи. Затем следует сделать запрос о форматах, которые поддерживаются этой программой, в надежде, что первым будет указан наиболее популярный формат. Действительно, для программы записи JPEG- файлов этот способ работал нормально, и строка JPEG располагалось в списке первой. Но для программа записи PNG-файлов первой в списке оказалась строка png, а не PNG (т.е. в нижнем регистре, а не в верхнем). Остается только надеяться, что разработчики SDK смогут исправить эту ситуацию в будущем. (Не рекомендуется ограничивать имена форматов только символами в верхнем регистре, потому что в таком случае не будут восприниматься такие форматы, как, например, PostScript).
После выбора программы записи нужно добавить первое название формата в набор имен форматов и удалить все названия форматов из исходного набора. Далее необходимо повторять это действие до тех пор, пока не будут обработаны все форматы файлов. Остальные подробности организации настоящего процесса можно найти в коде метода getWriterFormats в листинге 7.9. Обратите внимание, что данный метод не годится для одной программы записи, которая может записывать несколько форматов графических файлов.
С практической точки зрения, многих программистов совершенно не беспокоят эти сложности. При наличии определенного количества форматов файлов, которые поддерживаются в приложении, можно довольно просто подобрать соответствующие программы для чтения и записи этих форматов и расширений имен файлов.
Чтение и запись файлов,
содержащих несколько рисунков
Некоторые файлы, например, анимированные GIF-файлы, могут содержать несколько изображений. Однако, метод read класса ImagelO может считывать только одно изображение. Для чтения нескольких изображений нужно включить источник входных данных (например, входной поток файла) в объект-поток входных данных изображения Imageinputstream.
Inputstreamin= ...
ImagelnputStream imagein =
Затем следует соединить этот объект-поток с программой чтения.
reader.setinput(imagein, true);
Второй параметр имеет значение true, т.е. поток входных данных находится в режиме просмотра "только вперед". В противном случае используется произвольный доступ с помощью буферизации входного потока при его чтении или с помощью произвольного доступа к файлу. Произвольный доступ позволяет эффективно выполнять определенные операции. Например, для подсчета количества изображений в GIF-файле нужно считать полностью весь файл. Если затем необходимо выбрать какой-то рисунок, то в режиме произвольного доступа не придется снова считывать весь входной поток.
Это следует учитывать только при чтении входного потока, содержащего несколько изображений и при отсутствии запрашиваемой информации о формате рисунка (например, номер рисунка) в заголовке файла. Для чтения файла можно применить показанный ниже простой фрагмент кода.
File f = .. .
Imageinputstream imagein =
Reader.setinput(imagein);
Таким образом, при наличии программы чтения можно считывать отдельные рисунки по известному индексу рисунка index, отсчет которого начинается с нуля.
Bufferedlmage image = reader.read(index);
Если для потока на входе установлен режим чтения "только вперед", то следует продолжать считывать рисунки до тех пор, пока метод read не генерирует исключительную ситуацию IndexOutOgBoundsException. Иначе, для этого можно вызвать метод getNumlmages, параметр которого имеет значение true, т.е. разрешается поиск и подсчет количества изображений.
int n = reader.getNumlmages(true) ;
Данный метод генерирует исключительную ситуацию IllegalStateException, если для входного потока задан режим просмотра "только вперед". С другой стороны, если для параметра метода getNumlmages задано значение falseи он не может определить количество рисунков без проведения поиска, то метод getNumlmages возвращает значение -1 . В этом случае нужно применить альтернативный план и считывать изображения до появления исключительной ситуации IndexOutO BoundsException.
Некоторые файлы могут содержать миниатюры (thumbnail), т.е. уменьшенные версии основного изображения, предназначенные для предварительного просмотра. С помощью следующего вызова можно узнать количество миниатюр для основного изображения по индексу основного изображения.
int count = reader.getNumThumbnails(index);
Для получения некоторой миниатюры можно применить метод getThumbnail с указанием индекса основного изображения и индекса миниатюры.
Bufferedlmage thumbnail = reader.getThumbnail(index, thumbnai1Index);
Иногда необходимо получить размер рисунка еще до получения самого рисунка. Это особенно важно при копировании рисунков по медленным сетевым соединениям. Для получения размеров рисунка с заданным индексом используются следующие методы.
int width = reader.getwidth(index) ;
int height = reader.getHeight(index);
Размеры рисунка, как правило, следует выяснить еще до получения самого рисунка. Если рисунка с заданным индексом нет, то эти методы генерируют исключительную ситуацию IndexOutOfBoundsExcept i on.
Для записи файла с несколькими рисунками нужно сначала создать объект ImageWriter, а затем с помощью класса lOImage перечислить все программы записи, способные записывать изображения данного формата.
String format = ...;
ImageWriter writer = null;
Iterator iter = lOImage. getlmageWritersByFormatName (format) ; if (iter.hasNext()) writer = (ImageWriter)iter.next();
Затем следует преобразовать поток входных данных или файл в объект-поток входных данных типа ImageOutput S tream и соединить его с программой записи.
File f
ImageOutputStream imageOut = ImagelO.createlmageOutputStream(f) ; writer.setoutput(imageOut) ;
Нужно заключить каждый рисунок в объект-оболочку I lOImage, затем (по желанию) составить список свернутых изображений и метаданных (например, алгоритм сжатия и информацию о цветах). В этом примере для списка свернутых изображений и метаданных задано значение null. Более подробную информацию по данной теме можно найти в документации no SDK.
IlOImage iioImage = new IlOImage(images[i], null, null);
Для записи первого изображения используется только метод write.
writer.write(new IlOImage(images[ 0 ] , null, null));
Для всех последующих изображений используется следующее условие проверки.
if (writer.caninsertimage(i))
writer.writeinsert(i, iiolmage, null) ;
Третий параметр может содержать объект ImageWriteParamдля указания таких подробностей, как мозаичная текстура и сжатие. В этом примере для таких параметров используется значение null.
He все форматы файлов могут содержать несколько изображений. В таком случае метод caninsertImage возвращает значение falseдля i > 0 и сохраняется только один рисунок.
Программа, код которой приведен в листинге 7.9, позволяет загружать и сохранять файлы в форматах, для которых в пакете SDK поддерживаются операции чтения и записи. Как показано на рис. 7.29, эта программа может отображать сразу несколько изображений, но не способна отображать свернутые изображения.
Листинг 7.9. imaaelOTeet. java
@version 1.0 2001-07-24
import jimport j@author Cay Horstmann
import j
import j
import import import import import import import importimport import import import import import import importJava.awt.event.*;
import import import import import import import import
import import import import import import import import
java.awt.image.*;
java.io. *;
java.util.*;
java.util.List;
j avax.imageio.*;
javax.imageio.stream.* ;
javax.swing.*;
public class ImagelOTest {
p{ublic static void main(String[] args)
JFrame frame = new ImagelOFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();"
} }
class ImagelOFrame extends JFrame {
public ImagelOFrame()
{
setTitle("ImagelOTest");
setsize(WIDTH,HEIGHT);
JMenu fileMenu = new JMenu("File");
JMenuItem openItem = new JMenuItem("Open");
openitem.addActionListener(new
ActionListener()
public voidevent)
openFile();
});}
fileMenu.add(openltem);
=new
f
Iterator iter =
while (iter.hasNextO)
final String=
=new
saveMenu.add(formatitem);
formatitem.addActionListener(new
ActionListener()
{
p{ublic void actionPerformed(ActionEvent event) saveFile(formatName);
)) ;
JMenuItem=new
exi 111 em.addActi onListener(new
{
public void actionPerformed(ActionEvent event)
{
System.exit(0) ;
}); fileMenu.add(exititem);
JMenuBar menuBar = new menuBar.add(fileMenu); ;
p{ublic void openFile()
JFileChooser chooser = new
chooser. setCurrentDirectory (new File ("."));
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter()
{
public booleanf)
ifreturn true;
String name = f.getName();
intp=
if (p==—1) return false;
String suffix
=+
return readerSuffixes.contains(suffix);
) public String getDescription() {
return "Image files";
}
}) ;
int r = chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
File f = chooser.getSelectedFile() ;
Boxbox=;
try {
String name = f.getName();
String suffix
=+1);
Iterator iter =
ImageReader reader = (ImageReader)iter.next();
Imageinputstream imagein =
reader.setinput(imagein);
int count = reader.getNumlmages(true);
images = new Bufferedlmage[count];
for (int i = 0; i < count; i++) {
images[i] =
box.add(new JLabelfnew Imageicon(images[i]))); }
}
catch (IOException exception)
{
JOptionPane.showMessageDialog(this, exception);
}
setContentPane(newJScrollPane(box));
validate();
/**
@param formatName the file format
*/
public void saveFile(final String formatName) {
if (images == null) return;
Iterator iter = ImagelO.getImagewritersByFormatName( formatName);
Imagewriter writer = (Imagewriter)iter.next();
final List writerSuffixes =
writer.getOriginatingProvider().getFileSuffixes());
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(newFile("."));
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter()
{
public boolean accept(File f)
{
if (f.isDirectory()) return true;
String name = f.getName();
int p = name.lastlndexOf('.');
if (p == —1) return false;
String suffix
=+ 1).toLowerCase();
returnwriterSuffixes.contains(suffix);
}
public String getDescription()
{
} return formatName+" files";
intr=chooser.showSaveDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
Filef=chooser.getSelectedFile();
try
ImageOutputStrearn imageOut
=;
writer.setoutput(imageOut);
writer.write(new IlOImage(images[0], null, null))
for (inti=1; i<images.length; i++)
IlOImage iiolmage
=newIlOImage(images[i], null, null); if (writer.canlnsertlmage(i))
jwriter.writeinsert(i, iiolmage,null);
}
c{atch (lOException exception)
^ JOptionPane.showMessageDialog(this, exception);
/**
@return the file suffix set
*/
public static Set getReaderSuffixes()
TreeSet readerSuffixes =newTreeSet();
String[] informalNames = ImagelO.getReaderFormatNames();
for (inti=0; i<informalNames.length; i++)
Iterator iter = ImagelO.getlmageReadersByFormatName( informalNames[i]);
while (iter.hasNext())
ImageReader reader = (ImageReader)iter.next();
String[] s=reader.getOriginatingProvider()
.getFileSuffixes(),-
readerSuffixes.addAll(Arrays.asList(s) ) ;
)
}.
return readerSuffixes;
}
/**
©return the format name set
*/
public static Set getWriterFormats()
TreeSet writerFormats = new TreeSet() ;
TreeSet formatNames = new TreeSet(Arrays.asList(
ImagelO.getWriterFormatNames() ) ) ;
while (formatNames.size() > 0)
{
String name = (String)formatNames.iterator().next();
Iterator iter = ImagelO.getlmageWritersByFormatName(
name);
ImageWriter writer = (ImageWriter)iter.next();
String[] names = writer.getOriginatingProvider()
.getFormatNames();
writerFormats.add(names[0]);
formatNames.removeAll(Arrays.asList(names));
}
return writerFormats;
)
private Bufferedlmage[] images;
private static Set readerSuffixes =
private static Set writerFormats = getWriterFormats();
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
javax.imageio.ImagelO
Метод static Bufferedlmage read(File input) считывает изображение из файла.
Метод static Bufferedlmage read(InputSteam input) считывает изображение из входного потока.
Метод static Bufferedlmage read(URL input) считывает изображение no заданному URL-указателю.
Метод static boolean write(Renderedlmage image, String formatName, File output) записывает изображение в файл. Возвращает значение false, если соответствующую программу записи не удалось найти.
Метод static boolean write(Renderedlmage image, String formatName, OutputStream output) записывает изображение в выходной поток. Возвращает значение false, если соответствующую программу записи не удалось найти.
Метод static Iterator getlmageReadersByFormatName (String formatName) возвращает все программы чтения графических файлов на основе указанного формата, например, JPEG.
Метод static Iterator getlmageReadersBySuffix (String file Suffix) возвращает все программы чтения графических файлов на основе указанного расширения файла, например, jpg.
Метод static Iterator getlmageReadersByMIMEType (String mimeType) возвращает все программы чтения графических файлов на основе указанного MIME-типа, например, image/ jpg.
Метод static Iterator getlmageWritersByFormatName (String formatName) возвращает все программы записи графических файлов на основе указанного формата, например, JPEG.
Метод static Iterator getlmageWritersBySuffix (String file Suffix) возвращает все программы записи графических файлов на основе указанного расширения файла, например, jpg.
Метод static Iterator getlmageWritersByMIMEType(String
mimeType) возвращает все программы записи графических файлов на основе указанного MIME-типа, например, image/jpg.
Метод static String[] getReaderFormatNames () возвращает названия всех форматов, которые поддерживаются данной программой чтения.
Метод static String [ ] getReaderMIMETypes () возвращает названия всех MIME-типов, которые поддерживаются данной программой чтения.
Метод static String [] getWriterFormatNames () возвращает названия всех форматов, которые поддерживаются данной программой записи.
Метод static String[] getWriterMIMETypes () возвращает названия всех MIME-типов, которые поддерживаются данной программой записи.
МетодImageinputstream createlmagelnputStream(Object
input) создает поток входных данных изображения на основе заданного объекта, который может быть файлом, потоком, файлом типа RandomAccessFi1e или другим объектом с заданным поставщиком услуг. Если для объекта не зарегистрирован поставщик услуг, то в таком случае возвращается значение null.
МетодImagelnputStream createlmageOutputStream(Object
output) создает поток выходных данных изображения на основе заданного объекта, который может быть файлом, потоком, файлом типа RandomAccessFile или другим объектом с заданным поставщиком услуг. Если для объекта не зарегистрирован поставщик услуг, то в таком случае возвращается значение null.
javax.imageio.ImageReader
Метод void setInput (Object input) задает источник входных данных для программы чтения.
Параметры:
inputобъект ImagelnputStream или другой объект, с которым
может работать данная программа чтения
Метод void setinput(Object input, boolean seekForwardOnly) задает источник входных данных для программы чтения и устанавливает направление чтения.
Параметры:
inputобъект ImagelnputStream или другой объект, с ко
торым может работать данная программа чтения
seekForwardOnlyесли задано значение true, то программа должна чи
тать только в прямом направлении; по умолчанию программа чтения использует произвольный доступ и, если нужно, буферизирует данные
Метод Bufferedlmage read(int index) считывает изображение с заданным индексом (отсчет начинается с нуля). Если такого изображения нет, то генерируется исключительная ситуация IndexOutOfBoundsEx- ception.
Метод int getNumlmage (boolean allowSearch) возвращает количество изображений для данной программы чтения. Если параметр allowSearch имеет значение false и количество изображений не может быть определено без выполнения прямого чтения, то возвращается значение -1. Если параметр allowSearch имеет значение true и программа находится в режиме прямого чтения, то генерируется исключительная ситуация IllegalStateException.
Метод int getNumThumbnai1s (int index) возвращает количество свернутых изображений для изображения с заданным индексом.
МетодBufferedlmage readThumbnails(intindex, int
thumbnai1Index) возвращает свернутое изображение с заданным индексом thumbnailIndex для изображения с заданным индексом index.
Метод int getWidth(int index) возвращает ширину рисунка. Если для заданного индекса не найдено никакого изображения, то генерируется исключительная ситуация IndexOutO BoundsException.
Метод int getHeight (int index) возвращает высоту рисунка. Если для заданного индекса не найдено никакого изображения, то генерируется исключительная ситуация IndexOutOf BoundsException.
Метод ImageReaderSpi getOriginalProvider() возвращает объект- поставщик услуг, который создает программу чтения.
[
——javax.imageio.spi.IlOServiceProvider
API
Метод String getVendorName () возвращает название разработчика объекта-поставщикауслуг.
Метод String getVersion() возвращает номер версии объекта- поставщикауслуг.
j avax. imageio. spi. ImageReaderWriterSpi
;P: I
i 1• Метод String [ ] getFormatNames () возвращает названия форматов,
поддерживаемые программами чтения или записи, которые создаются данным объектом-поставщиком услуг.
Метод String [] getFileSuff ixes() возвращает расширения файлов, поддерживаемые программами чтения или записи, которые создаются данным объектом-поставщиком услуг.
Метод String [ ] getMIMETypes () возвращает MIME-типы, поддерживаемые программами чтения или записи, которые создаются данным объ- ектом-поставщикомуслуг.
!
////•1•_.
FFff 3.v<ut.i-.,.1=.IOM,e№lt.,
Метод void setOutput (Object output) задает приемник выходных данных для данной программы записи.
Параметры:
inputобъект ImageoutputStreamили другой объект, с которым
может работать данная программа записи
Методы void write(IlOlmage image) и void write(Renderedlmage image) записывают в выходной поток единственное изображение.
Методvoid writeinsert(intindex,IIOIMage image,
ImageWriteParam param) записывает изображение в файл, состоящий из многих изображений.
Параметры:
indexиндекс изображения
imageзаписываемое изображение
paramпараметры записи или значение null
Метод boolean caninsertImage(int index) возвращает значение true, если в файл, состоящий из многих изображений, можно вставить изображение с заданным индексом.
Метод ImageWriterSpi getOriginatingProvider() возвращает объект-поставщик услуг, который создал данную программу записи.
javax.imageio.IlOlmage
Конструктор IlOlmage(Renderedlmage image, List thumbnails, IIOMetadata metadata) создает объект IlOImage на основе заданного изображения, а также (по желанию) списка его свернутых изображений и
метаданных.
Параметры:
imageизображение
thumbnai1 s список свернутых изображений-объектов Bufferedlmageилизначениеnull
metadataметаданные или значение nul 1
Управление изображениями
Предположим, что необходимо улучшить внешний вид какого-то изображения. Для этого нужно получить доступ к пикселям изображения и заменить их другими или рассчитать новые пиксели, например, для отображения результатов измерений или математических вычислений. Класс Bufferedlmage дает возможность управлять пикселями изображения, а классы, которые реализуют интерфейс BufferedlmageOp, позволяют преобразовывать изображения.
В JDK версии 1.0 классы изображений были оптимизированы для поддержки механизма последовательного рисования {incrementalrendering)изображений. Исходноепредна- значение этих классов — отображение файлов формата GIF и JPEG, которые последовательно, строка за строкой, копировались из Internet. В действительности, изображения могутзагружаться чересстрочно {interlaced), т.е. сначала копируются иотображаютсячетные, а затем нечетные строки. Этот механизм позволяет Wekhdpoyaepy быстро отображать чересстрочно и частично прорисованное изображение, в то время как будут загружаться остальные строки изображения. Для управления сложным процессом последовательного
рисования изображений в JDK 1.0 были предусмотрены интерфейсы ImageProducer, ImageFilter и ImageConsumer. Поэтому управление изображениями в рамках данного подхода имело очень сложную структуру.
К счастью, используемая в JDK 1.0 модель "принудительного рисования" ("push model") изображений в платформе Java 2, заменена более простой моделью "прямого рисования" ("direct model"), которая предоставляет простой и удобный доступ к пикселям изображения. Далее в этой главе рассматривается более простая модель "прямого рисования" изображений. Единственным недостатком модели является то, что все пиксели изображения должны находиться в оперативной памяти компьютера. (На практике модель "принудительного рисования" имеет те же ограничения. Дело в том, что для создания сложного алгоритма управления изображениями на основе обработки поступающих из потока пикселей требуется достаточно высокая квалификация и изобретательность. Поэтому многие пользователи старой модели для упрощения задачи просто буферизировали изображение перед его обработкой.) В будущих версиях платформы Java предполагается поддерживать "конвейерную модель" ("pull model"), в которой конвейерный механизм обработки изображения может уменьшить потребление памяти и увеличить скорость обработки с помощью выборки и обработки только необходимых в данный момент пикселей.
Доступ к данным изображения
Большинство обрабатываемых изображений просто считываются из графического файла, созданного с помощью аппаратных средств (например, цифровой камеры или сканера) либо с помощью программных средств (например, графических пакетов). В этом разделе рассматриваются совершенно другие способы создания изображений, которые основаны на попиксельном создании и обработке изображений.
Для создания изображения следует уже известным способом создать объект Bufferedlmage.
image = new Bufferedlmage(width, height,
Bufferedlmage.TYPE_INT_ARGB);
Теперь нужно вызвать метод getRaster для получения объекта типа WritableRaster, который используется для доступа и изменения пикселей изображения.
WritableRaster raster = image.getRaster();
Метод setPixel позволяет задавать значение для отдельного пикселя, но с его помощью нельзя непосредственно установить значение цвета для пикселя. Дело в том, что для этого нужно знать способ указания цветов для данного буферизированного изображения, который зависит от типа изображения. Если изображение имеет тип TYPE_INT_ARGB, то каждый пиксель описывается четырьмя значениями: для красного, зеленого и синего цвета, а также для альфа-канала (каждое из этих значений лежит в диапазоне от 0 до 255). Эти значения нужно передать пикселю в виде массива из четырех элементов целочисленного типа.
int[] black = {0, 0, 0, 255};
raster.setPixel(i, j, black);
ВтерминахJava2DAPIэтизначенияназываютсявыборочными значениями (sample values) пикселя.
Метод setPixel может принимать в качестве параметра массив типа float [ ] или массив типа double [ ], но учтите, что значения элементов массива не являются нормализованными значениями цветов, лежащими в диапазоне 0.0 и 1.0.
float[] red = {1.0F, 0.0F, O.OF, 1.0F}; raster.setpixel(i, j, red); // Ошибка!
Для элементов такого массива все равно придется указать значения между 0 и 255, несмотря на тип массива.
Для установки значений прямоугольной группы пикселей можно в параметрах метода setPixels указать начальное расположение, ширину и высоту прямоугольника, а также массив, содержащий выборочные значения цветов этой группы пикселей. Например, если изображение имеет тип TYPE_INT_ARGB, то нужно указать значения красного, зеленого и синего цветов и альфа-значение для первого пикселя, потом для второго и т.д.
int[] pixels = new int[4 * width * height];
pixels[0] = ...//значениекрасногоцветадляпервогопикселя
pixels[1] = ...//значениезеленогоцветадляпервогопикселя
pixels[2] = ... // значение синего цвета для первого пикселя
pixels[3] = ... // альфа-значение для первого пикселя
raster.setPixels(x, у, width, height, pixels);
И наоборот, для считывания пикселя используется метод getPixel, который возвращает массив из четырех целых чисел, содержащих соответствующие значения пикселя. int[] sample = new int[4];
raster.getPixel(x, y, sample);
Color с = new Color(sample[0] , sample[l], sample[2], sample[3]) ;
С помощью метода getPixels можно прочитать значения группы пикселей.
raster.getPixels(x, у, width, height, samples);
Методы getPixel/setPixel можно использовать и для другого типа изображения, если известен способ представления значений пикселей для изображения этого типа. Для чего нужно знать способ кодирования значений пикселей в используемом типе изображения.
Управлять изображением становится немного сложнее, если его тип неизвестен. Каждый тип изображений имеет цветовую модель, в соответствии с которой можно преобразовать выборочные значения в стандартную цветовую модель RGB.
На самом деле цветовая модель RGB не является стандартом. Точное представление цветов зависит от характеристик используемого аппаратного обеспечения. Цифровые камеры, сканеры, мониторы и жидкокристаллические дисплеи имеют совершенно разные характеристики. В результате, одно и то же RGB-значение выглядит по-разному в различных устройствах. Международный консорциум по цветам (International Color Consortium — ICC) (www. color.org) рекомендует прилагать ко всем цветовым данным ПС- профиль, который указывает степень соответствия данных цветов стандартной форме представления цветов на основе спецификации 1931 CIE XYZ. Эта спецификация была разработана Международной комиссией по освещению (Commission Internationale de I'Eclairage — CIE) (www. cie.co.at/cie), обеспечивающей техническую поддержку всех типов освещения и цветов. Она регламентирует стандартный метод представления всех цветов, которые человеческий глаз может воспринять на основе трех гипотетических координат X, Y, Z. (Подробнее эта спецификация описана в главе 13 книги Джеймса Фоли (James D. Foley), Андриса ван Дама (Andriesvan Dam), Стивена Фэйнера (Steven К. Feiner) и др. Computer Graphics: Principles and Practice, Second Edititon [Addison Wesley 1995].) Однако, ICC-профили имеют очень сложную структуру. Поэтому, наряду с ними используется более простой стандарт sRGB (http: //www.w3 .org/Graphics/Color/ sRGB.html), который указывает точное соответствие значений цветов RGB и значений по спецификации 1931 CIE XYZ. Он был специально разработан для стандартных цветных мониторов и используется в Java 2D API для преобразования значений цветов RGB в значения цветов других цветовых систем.
Для получения цветовой модели используется метод getColorModel.
ColorModel model = image.getColorModel();
Для определения цвета пикселя нужно вызвать метод getDataElements класса Raster, который возвращает объект с описанием цвета данного пикселя.
Object data = raster.getDataElements(x, у, null);
Объект, который возвращается методом getDataElements, в действительности является массивом выборочных значений. Это объясняет, почему метод getDataElements так называется, но об этом совсем не обязательно знать для обработки данного объекта.
С помощью цветовой модели можно преобразовывать объекты в стандартные ARGB-значения. Метод getRGB возвращает значение int, которое содержит значения альфа-канала, красного, зеленого и синего цветов, упакованные в четыре блока по8битвкаждом.СпомощьюконструктораColor (int argb,boolean
hasAlpha) на основе этого целочисленного значения можно создать значение цвета в виде объекта типа Color.
int argb = model.getRGB(data);
Color color = new Color(argb, true);
Если для пикселя необходимо установить определенный цвет, то нужно выполнить эти действия в обратном порядке. Метод getRGB класса Color возвращает целочисленное комбинированное значение для красного, зеленого и синего цветов и альфа-канала. Именно это значение нужно использовать в качестве параметра для метода getDataElements класса ColorModel, который, в свою очередь, возвратит объект с описанием цвета пикселя в используемой цветовой модели. Далее нужно передать этот объект методу setDataElements класса WritableRaster.
int argb = color.getRGB();
Object data = model.getDataElements(argb, null);
raster.setDataElements(x, y, data);
Для иллюстрации использования данного способа попиксельного создания изображения рассмотрим традиционный пример программы (код которой приведен в листинге 7.10) создания фрактального множества Мандельброта, которое показано на рис. 7.30.
Рис. 7.30. Множество Мандельброта
Множество Мандельброта состоит из раскрашенных точек плоскости, каждая из которых связана с некоторой ограниченной последовательностью цифр. Все другие нераскрашенные точки плоскости связаны с неограниченными последовательностями цифр и остаются прозрачными. Формула вычисления членов последовательности основана на комплексных числах и в данном примере принимается на веру. Более подробные сведения о математических основах фракталов можно найти в многих книгах, например, в сборнике Chaos and Fractals: New Frontiers of Science под редакцией Хайнца-Отто Пейтгена (Heinz-Otto Peitgen), Дитмара Сауна (Dietmar Saup) и Гартмута Юргенса (Hartmut Jurgens), [Springer Verlag 1992].
Итак, простейшее множество Мандельброта основано на том, что для каждой точки с координатами (а, b) определяется последовательность, которая начинается в точке с координатами (x, у) = (0,0) и содержит следующие новые члены последовательности.
Xnew =
Ynew = 2*х*у+Ь
Далее нужно проверить, будет ли последовательность ограниченной или нет, т.е. увеличиваются значения х и у или нет. Известно, что если х или у больше 2, то последовательность является неограниченной. Как уже упоминалось выше, цвет будет присвоен только тем пикселям с координатами (а, b), которые связаны с ограниченной последовательностью.
В примере программы, код которой приведен в листинге 7.10, демонстрируется использование класса ColorModel для преобразования значений цвета Color в выборочные данные пикселей. Этот процесс не зависит от типа изображения. Попро-
буйте поэкспериментировать и измените тип цвета буферизированного изображения на тип TYPE_BYTE_GRAY. В таком случае в данный код не нужно будет вносить никаких других изменений, потому что цветовая модель изображения автоматически позаботится обо всех преобразованиях цветов в выборочные значения.
Листинг 7.10. MandelbrotTeet. java ' ';^' ^S^
/•*
©version 1.11 2001-07-24
©author Cay Horstmann
*/
import java.awt.*;
import j ava.awt.event.*;
import java.awt.image.*;
import j
/**
Пример попиксельного создания изображения
*/
public class MandeIbrotTest
{
public static void main(String[] args) {
JFrame frame = new MandelbrotFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show!);
}
}
/**
Фрейм с изображением множества Мандельброта
*/
class MandelbrotFrame extends JFrame {
public MandelbrotFrame()
{
setTitle("MandelbrotTest") ;
setsize(WIDTH,HEIGHT);
Bufferedlmage image = makeMandelbrot(WIDTH, HEIGHT);
getContentPane().add(new JLabel(new Imageicon(image)), BorderLayout.CENTER) ;
)
/**
Создание множества Мандельброта
©param width the width
©parah height the height
©return the image
*/
public Bufferedlmage makeMandelbrot(int width, int height) {
Bufferedlmage image = new Bufferedlmage(width, height, Buf feredlmage.TYPE_INT_ARGB);
WritableRaster raster = image.getRaster();
ColorModel model = image.getColorModel();
Color fractalColor = Color.red;
int argb =
Object colorData = model.getDataElements(argb, null)
for (int i = 0; i < width; i++)
for (int j = 0; j < height; j++) {
double a = XMIN + i (XMAX - XMIN) / width;
double b = YMIN + j * (YMAX - YMIN) / height; if (!escapesToInfinity(a, b))
raster.setDataElements(i, j, colorData);
}
return image;
}
private boolean escapesToInfinity(double a, double b)
(
double x = 0.0;
double у = 0.0; int iterations = do
(
0;
double xnew =
x *
X_ у * у + a:
double ynew =
2 *
x * у + b;
x = xnew;
у = ynew; iterations++;
if (iterations
MAX-ITERATIONS) return false;
}
while (x <= 2 &&у <= 2); return true;
}
private static private static private static private static private static private static private staticprivate static private static private static private static private static private static private staticfinal double XMIN = —2;
private static private static private static private static private static private static private static
private static private static private static private static private static private static private static
final double XMAX = 2;
final double YMIN = -2;
final double YMAX = 2 ;
final int MAX-ITERATIONS = 16;
final int WIDTH = 400;
final int HEIGHT = 400;
j
height, intheight, intКонструктор Buf feredlmage (int width,int
height, int
height, int
imageType) создает буферизированноеизображение.
Параметры:
width height imageType
ширина изображения
высота изображения
тип изображения: TYPE_INT_RGB, TYPE_INT_ARGB,
TYPE_BYTE_GRAY, TYPE—BYTE-INDEXED,
TYPE—USHORT_555_RGB и т.д.)
java.awt* image • Raster
Метод Object (int x, int Object data) возвращает выборочные данные для точки растра в виде массива, тип элементов и длина которого зависят от цветовой модели. Если значение параметра data не равно null, то предполагается, что это значение является массивом для хранения выборочных данных, который заполнен этими данными. А если значение параметра data равно 1, то создается новый массив.
Параметры:
х, укоординаты пикселя
dataзначениеили массив для выборочных данных пикселя;
тип его элементов и его длина зависят от цветовой модели
Методы int[]int у, int [ ]
[]x, int y, float []sampleValues) и
double[] getPixel(int x, int doublet] sampleValues) возвращают выборочные значения для точки растра в форматах int, и double соответственно.
Параметры: х, укоординаты точки растра
sampleValues значение1 или массив выборочных значений пикселя
Методы int [ ] getPixels (int x, int у, int w, int h, int [ ] sampleValues),[] getPixels (int x, int y, int w, int
h,sampleValues) иx, int y,
intintsampleValues) возвращают выборочные
значения для прямоугольной области растра в форматах int, float и double соответственно. Если значение параметра sampleValues не равно null, то считается, что массив sampleValues имеет достаточную длину для хранения данных и заполнен этими данными. Если значение параметра sampleValues равно null, то создается новый массив. Эти методы полезны только тогда, когда известен алгоритм указания выборочных цветов в данной цветовой модели.
Параметры:
х, укоординаты левого верхнего угла прямоугольной облас
ти растра
hширина и высота прямоугольной области растра
sampleValues значениеили массив выборочных значений
Ia3 ava *awt * *mage . WritableRaster
Метод void setDataElements (int x, intObject data) зада
ет выборочное значение для точки растра.
Параметры:
х, укоординаты пикселя
dataмассив выборочных значений для пикселя; тип его элементов
и длина зависят от цветовой модели
Методы int [ ]х, int у, int [ ] sampleValues),
setPixel(int x, int y,sampleValues) и
doublet] setPixel(int x, int y, doublet] sampleValues) воз-
вращают выборочные значения для точки растра в форматах int, float и double соответственно.
Параметры:
х, укоординаты точки растра
sampleValues значение nullили массив выборочных значений, тип элемента и длина которого зависят от цветовой модели
•Методыint[]setPixels(intx, intу,intw,int h,int[]
sampleValues), float[] setPixels (int x, int y, int w, int h, float[] sampleValues) и doublet] setPixels(int x, int y, int w, int h, double[] sampleValues) возвращают выборочные значения для прямоугольной области растра в форматах int, float и double соответственно.
Параметры:
х, укоординаты прямоугольной области растра
w, hширина и высота прямоугольной области растра
sampleValues значение null или массив выборочных значений, тип элемента и длина которого зависят от цветовой модели
java.awt.image.ColorModel
Метод int getRGB (Object data) возвращает ARGB-значение, которое соответствует выборочным данным, переданным с помощью параметра data.
Параметры:
dataмассив с выборочными данными о пикселе; тип его элементов
и длина зависят от цветовой модели
Метод Object getDataElements(int argb, Object data) возвращает выборочные данные для указанного цвета argb; если значение параметра data не равно null, то предполагается, что этот массив имеет подходящую длину для хранения данных и заполнен этими данными, а если параметр data равен null, то создается новый массив.
Параметры:
argbцвет
dataзначение null или массив для хранения выборочных данных
для указанного цвета; тип элемента и длина массива зависят от цветовой модели
java.awt.Color
Конструктор Color (int argb, boolean hasAlpha) создает цвет на основе указанного ARGB-значения, если параметр hasAlpha имеет значение true, или на основе RGB-значения, если параметр hasAlpha имеет значение false.
Метод int getRGB () возвращает ARGB-значение, соответствующее данному цвету.
Фильтрация изображений
В предыдущем разделе рассматривался процесс создания рисунка с самого начала. Однако, часто пользователю необходимо выполнить обработку уже имеющегося изображения.
Конечно, для чтения графических данных, их обработки и записи можно использовать методы getPixel/getDataElements, уже рассмотренные в предыдущем разделе. Но, к счастью, в платформе Java 2 поддерживаются фильтры {filters), которые автоматически выполняют многие рутинные операции обработки изображений.
Все объекты-операции обработки изображений реализуют интерфейс BufferedlmageOp. Для преобразования одного изображения в другое после создания объекта-операции нужно просто вызвать метод filter.
BufferedlmageOpop = ...;
Bufferedlmage filteredlmage =
image.getType() );
op.filter(image, filteredlmage);
Некоторые операции могут непосредственно выполнять преобразования изображения так, как показано в последней строке данного фрагмента кода, но для большинства остальных операций это невозможно.
Ниже приведены пять классов, которые реализуют интерфейс BufferedlmageOp.
Af fineTransformOp RescaleOp
LookupOp
ColorConvertOp
ConvolveOp
Класс Af f ineTrans form выполняет аффинное преобразование пикселей. Например, следующий фрагмент программы проворачивает изображение вокруг его центра.
AffineTransform transform
=
image.getwidth() / 2, image.getHeight() / 2);
AffineTransformOp op
= new AffineTransformOp(transform, interpolation); op.filter(image, filteredlmage);
Конструктор AffineTransformOp использует в качестве параметров объект аффинного преобразования и стратегию интерполяции (interpolation). Интерполяция необходима для определения дискретных координат пикселей в конечном преобразованном изображении, если полученные после преобразования непрерывные значения координат исходных пикселей находятся где-то между дискретными пикселями конечного преобразованного изображения. Например, при повороте исходных пикселей их новые непрерывные координаты не будут точно соответствовать дискретным координатам пикселей конечного преобразованного изображения. Для интерполяции используются две стратегии: билинейная стратегия AffineTransfor- mOp . TYPE_BILINEAR и стратегия на основе усреднения по ближайшим соседям AffineTrans formOp.TYPE_NEAREST_NEIGHBOR. Билинейная интерполяция выполняется дольше, но позволяет получить более удачный внешний вид.
Программа, код которой приведен в листинге 7.11, позволяет повернуть изображение на 5°, как показано на рис. 7.31.
Конструктор RescaleOp создает объект-операцию масштабирования для всех выборочных значений х данного изображения.
xnew = а*х + Ь
Рис. 7.31.Повернутое изображениеРис. 7.31.Повернутое изображениеДля выборочных значений, которые оказываются после масштабирования слишком большими или маленькими, задаются соответственно наибольшие или наименьшие допустимые значения. Если изображение имеет ARGB- формат, то преобразование выполняется отдельно для красного, зеленого и синего цветов, но не выполняется для альфа-значения. Масштабирование при а>1 приводит к осветлению изображения. Объект RescaleOp создается на основе указанных параметров масштабирования и необязательных параметров рисования. В листинге 7.11 используются следующие операторы.
Рис. 7.31.
Повернутое изображение
Рис. 7.31.
Повернутое изображение
float a = 1.5f;
float b = -20.Of;
RescalOp = new RescaleOp(a, b, null);
Операция LookupOp создает объект-операцию произвольного ото
бражения выборочных значений на основе таблицы с подстановочными данными для всех выборочных значений. В данном примере очень простое отображение используется для вычисления негатива (negative) заданного изображения. Для этого каждый цвет с заменяется цветом 255-c.
Конструктор LookupOp использует в качестве параметров объект-таблицу преобразования LookupTable и необязательную коллекцию-карту с параметрами рисования. Класс LookupTable является абстрактным и имеет два конкретных подкласса: ByteLookupTable и ShortLookupTable. RGB-цвета представлены в виде байтов, поэтому для них используется объект-таблица ByteLookupTable. Такая таблица создается на основе байтового массива и целочисленного смещения внутри него. В данном примере для создания объекта-таблицы LookupOp используется следующий код. byte negative[] = new byte[256];
for (int i = 0; i < 256; i++)
negative[i] = (byte)(255-i);
ByteLookupTable table = new ByteLookupTable(0, negative);
LookupOp op = new LookupOp(table, null);
Подстановка применяется отдельно к каждому цвету, но не применяется к альфа- значению.
Класс LookupOp нельзя применять к изображению с индексированной цифровой моделью, потому что в таких рисунках каждое выборочное значение задается на основе смещения в цветовой палитре.
Для преобразования цветов используется класс ColorConvertOp, который здесь не рассматривается.
Наиболее мощные преобразования можно выполнять с помощью объекта ConvolveOp, который выполняет математическую операцию свертки (convolution). Здесь мы не будем углубляться в математические подробности выполнения операции
свертки, а рассмотрим лишь ее основную идею. Сначала рассмотрим фильтр размытия показанный на рис. 7.32.
Размытие достигается за счет замены каждого пикселя на среднее значение этого пикселя и соседних с ним восьми пикселей. Интуитивно понятно, почему эта опера-
Рис. 7.32. РазмытиеРис. 7.32. Размытиеизображенияизображения7.33.Обнаружение краев7.33.Обнаружение краевция делает изображение размытым, а с математической точки зрения получение среднего значения может быть представлено как операция свертки со следующим ядром {kernel).
Рис. 7.32. Размытие
Рис. 7.32. Размытие
изображения
изображения
7.33.
Обнаружение краев
7.33.
Обнаружение краев
1
1
1
—■—
■■w
w
9
9
9
1
1
1
—^
^*
w*
9
9
9
1
1
1
^*
—■
^*.
9
9
9
Ядро свертки представляет собой матрицу с весами соседних значений. Например, приведенное выше ядро приводит к размытию изображения. Показанное ниже другое ядро выполняет операцию обнаружения краев {edge detection) на основе обнаружения участков, в которых происходит изменение цветов (рис. 7.33). Обнаружение краев имеет очень большое значение для обработки фотографических изображений.
О -1 О'
-1 4 -1
0-10
Для создания объекта-операции свертки ConvolveOp нужно прежде создать массив значений для объекта-ядра Kernel. Затем на основе
этого ядра нужно создатьсвертки и использовать ее для фильтра
ции изображения.
float[] elements = {
O.Of, -l.Of, O.Of,
-l.Of, 4.f, -l.Of, O.Of, -l.Of, O.Of };
Kernel kernel = new3, elements);
ConvolveOp op = new;
op. filter(image, filteredlmage) ;
Программа, код которой приведен в листинге предоставляет пользователю возможность зафужать файлы формата GIF и JPEG для выполнения описанных выше операций обработки зафуженных изображений. Простота кода этой профаммы объясняется тем, что Java 2D API предусмотрены очень мощные средства обработки изображений.
Листинг
1.0 2001-07-24
Cay
import java.awt.*;
import j ava. awt. event. *;
import java.awt.geom.*;
import j ava.awt.image.*;
import java.io.*;
import j avax.imageio.*;
import j
/**
Пример использования разных
операций обработки изображений
*/
public class ImageProcessingTest
{
public static void main(String[] args)
{
JFrame frame = new ImageProcessingFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
/**
Фрейм с командами меню для загрузки изображения и указания разных операций преобразования, а также панель с полученным изображением
*/
class ImageProcessingFrame extends JFrame {
public ImageProcessingFrame()
{
setTitle("ImageProcessingTest");
setsize(WIDTH,HEIGHT);
JPanel panel = new
JPanel()
{
public void paintcomponent(Graphics g) {
super.paintComponent(g);
if (image ! a null) g.drawlmage(image, 0, 0, null);
getContentPane().add(panel, BorderLayout.CENTER);
JMenu fileMenu = new JMenu("File");
JMenuItem openltem = new JMenuItemf"Open");
openitem.addActionListener(new
ActionListener() (
public void actionPerformed(ActionEvent event)
(
openFile();
}
});
fileMenu.add(openitem);
JMenuItem exititem = new JMenuItem("Exit");
exititem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.exit(0);'
}
});
fileMenu.add(exititem) ;
JMenu editMenu = new JMenu(*Edit") ;
JMenuItem blurltem = new JMenuItem("Blur");
blurltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
float weight = 1.0f/9.0f;
float[] elements = new float[9];
for (int i = 0; i < 9; i++)
elements[i] = weight; convolve(elements);
}
});
editMenu.add(blurltem);
JMenuItem sharpenltem = new JMenuItem("Sharpen");
sharpenltem.addActionLi stener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
float[] elements = { O.Of, -l.Of, O.Of,
-l.Of, 5.f, -l.Of, O.Of, -l.Of, O.Of };
convolve(elements);
} }); editMenu.add(sharpenltem);
JMenuItem brightenltem = new JMenuItemf"Brighten"); brightenltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
float a = 1.5f;
float b = -2 O.Of;
RescaleOp op = new RescaleOp(a, b, null); filter(op);
} }); editMenu.add(brightenltem);
JMenuItem edgeDetectltem = new JMenuItem("Edge detect"); edgeDetectltem.addActionListener(new
ActionListener()
public void actionPerformed(ActionEvent event) {
float[] elements = { O.Of, -l.Of, O.Of,
-l.Of, 4.f, -l.Of, O.Of, -l.Of, O.Of } ;
convolve(elements) ;
} )); editMenu.add(edgeDetectltem);
JMenuItem negativeitem = new JMenuItem("Negative"); negativeitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
byte negative!] = new byte[256];
for (int i = 0; i < 256; i++) negative[i] = (byte)(255 - i) ;
ByteLookupTable table
= new ByteLookupTable(0, negative);
LookupOp op = new LookupOp(table, null); filter(op) ;
}
});
editMenu.add(negativeitem);
JMenuItem rotateitem = new JMenuItemf"Rotate"); rotateitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
AffineTransform transform =
Math.toRadians(5) , image.getWidth() / 2 , image.getHeight() / 2);
AffineTransformOp op
= new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
filter(op);
} ) ) : editMenu.add(rotateitem);
JMenuBar menuBar = new JMenuBar() ;
menuBar.add(fi1eMenu);
menuBar.add(editMenu); setJMenuBar(menuBar);
/**
Открытие и загрузка файла с изображением */
public void openFile()
JFileChooser chooser = new JFileChooser(); chooser.setCurrentDirectory(newFile("."));
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter()
{
public boolean accept(File f)
{
String name = f.getName().toLowerCase(); return name.endsWith(".gif")
} public{} public{String getDescription()String getDescription()j return "Image files";j return "Image files";name. endsWi th(".j pg") name.endsWi th(".jpeg") f.isDirectory() ;
} public
{
} public
{
String getDescription()
String getDescription()
j return "Image files";
j return "Image files";
int r = chooser.showOpenDialog(this) ;
if(r != JFileChooser.APPROVE_OPTION) return;
try {
image = ImageIO.read(chooser.getSelectedFile()); }
catch (IOException exception) {
JOptionPane.showMessageDialog(this, exception);
}
repaint();
/**
Применение фильтра и перерисовка
@param op the image operation to apply
*/
private void filter(BufferedlmageOp op)
{
Bufferedlmage filteredlmage
= new Bufferedlmage(image.getWidth(), image.getHeight()- image.getType());
op.filter(image, filteredlmage);
image = filteredlmage;
repaint();
}
/**
Применение свертки и перерисовка
@param elements the convolution kernel (an array of
9 matrix elements)
*/
private void convolve(float[] elements)
{
Kernel kernel = new Kernel(3, 3, elements);
ConvolveOp op = new ConvolveOp(kernel);
filter(op);
private Bufferedlmage image;
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
java. awt. image.BufferedlmageOp
e Метод Bufferedlmage filter(Bufferedlmage source, Buffered- lmage dest) выполняет данную операцию с исходным изображением source и сохраняет (а также возвращает) полученный результат в виде изображения dest. Если для параметра dest задано значение null, то создается совершенно новое итоговое изображение.
j ava. awt. image. Aff ineTrane formOp
Конструктор Af f ineTrans f ormOp (AifineTransform t, int interpolationType) создаетобъект-оператор аффинного преобразования.
Параметры:
tаффинное преобразование
interpolationTypeтип преобразования: TYPE_BILINEAR или
TYPE_NEAREST_NEIGHBOR
java.awt.image.RescaleOp
Конструктор RescaleOp(float a,float b,RenderingHints
hints) создает объект-оператор масштабирования.
Параметры: a, b
коэффициенты преобразования по формуле xM„=a*x+b, которая применяется для выборочных значений
hints
nullили параметры рисования
java.awt.image.LookupOp
• Конструктор LookupOp(LookupTable table, RenderingHints hints) создаетобъект-оператор подстановки.
Параметры:
tableтаблица с подстановочными данными для выборочных
значений
hintsnull или параметры рисования
java.awt.image.ByteLookupTable
Конструктор ByteLookupTable (int offset, byte [ ] data) создает байтовую таблицу с подстановочными данными.
Параметры:
offsetпозиция первого используемого значения
dataтаблица подстановочных данных
java.awt.image.ConvolveOp
Конструктор ConvolveOp(Kernel, kernel) создает объект-оператор
свертки на основе заданного ядра свертки kernel.
Параметры:
kernelматрица свертки
Конструктор ConvolveOp (Kernel, kernel, int edgeCondition, RenderingHints hints) создает объект-оператор свертки на основе заданного ядра свертки kernel с учетом дополнительных параметров.
Парамецтры: kernel edgeCondition
матрица свертки
способ обрезки граничных значений: EDGE_NO_OP или EDGE_ZERO_FILL; граничные значения должны быть обрезаны, поскольку у них нет подходящих соседних значений для вычисления свертки; по умолчанию применяется EDGE_ZERO_FILL
hints
значение null или параметры рисования
java.awt.image.Kernel
Конструктор Kernel (int width, int height, float [] data) создает ядро свертки.
Параметры:
width, height размеры матрицы ядра dataэлементы матрицы
Печать
В исходной версии JDK вовсе не поддерживались никакие средства печати. Например, нельзя было печатать из аплетов, а для печати из приложения необходимо использовать дополнительные библиотеки сторонних разработчиков. Впервые средства организации печати появились в SDK версии 1.1, но с их помощью можно было печатать очень простые документы довольно низкого качества. В SDK версии 1.1 была разработана специальная модель печати для того, чтобы предоставить возможность разработчикам WeWpoyyepoB распечатывать аплет в таком виде, в каком он появляется на Web-странице (чем, однако, они не воспользовались). Во всем остальном модель печати, предложенная в SDK версии 1.1. вполне заслуживает того, чтобы о ней поскорее забыли.
В SDK версии 1.2 были заложены основы устойчивой модели печати, полностью интегрированной со средствами двухмерной графики, которые впоследствии были незначительно усовершенствованы в SDK версии 1.3. Наконец, в SDK версии 1.4 эта модель была значительно усовершенствована возможностями управления параметрами принтеров и организации заданий потоковой печати для управления процессом печати со стороны сервера. В этом разделе рассматриваются способы печати рисунков на одной странице бумаги, способы управления многостраничной печатью, а также демонстрируется стройность модели представления изображений в Java 2D API и простота создания окна предварительного просмотра.
D
В платформе Java поддерживается печать элементов пользовательского интерфейса. Но здесь данная тема не рассматривается, поскольку это в основном интересно создателям Web-броузеров, программ захвата моментальных снимков экрана и т.д. Более подробную информацию по этой теме можно получить по адресу http://java.sun.com/products/jdk/1.1/docs/guide/awt/designspec/pri nting.html.
Печать графики
В настоящем разделе рассматривается одна из наиболее распространенных задач, а именно печать двухмерной графики. Конечно, изображение может содержать фрагменты текста, написанного с использованием различных шрифтов, или даже полностью состоять из текста.
Для организации печати следует выполнить следующие действия.
Предоставить объект, который реализует интерфейс Printable.
Запустить процесс печати.
Интерфейс Printable имеет единственный метод print.
int print(Graphics g, PageFormat format, int page)
Этот метод вызывается всякий раз, когда механизм печати форматирует печатаемую страницу. Созданный программистом код рисует текст и изображение, которые должны быть напечатаны в данном графическом контексте д. Формат страницы formatуказывает размер бумаги и границы печати, а номер страницы page используется для выбора отображаемой страницы.
Для запуска процесса печати используется класс PrinterJob. Сначала для получения объекта-задачи печати вызывается статический метод getPrinterJob, а затем с помощью метода setPrintable указывается объект типа Printable, который нужно распечатать.
Printable canvas =
Printerjob job = PrinterJob.getPrinterJob(); job.setPrintable(canvas) ;
Heпутайте класс Printer Job с устаревшим классом Print Job, который управляет процессом печати в JDK версии 1.1.
Перед тем, как начать печатать, нужно вызвать метод printDialog для отображения диалогового окна с параметрами печати, которое показано на рис. 7.34. Это окно предоставляет пользователю возможность выбирать используемый принтер (если доступны несколько принтеров), указывать диапазон страниц для печати и использовать другие настройки принтера.
Во время создания книги диалоговые окна параметров печати еще не работали долж- I I ным образом в операционной системе Linux. Для печати в Linux нужно создать файл в формате PostScriptи использовать сервис потоковой печати, который рассматривается ниже в этой главе, а затем поместить полученный файл в буфер печати.
Все параметры принтера обычно собираются в одном объекте класса, который реализует интерфейс PrintRequestAttributeSet, который затем передается методу printDialog. Для этого можно воспользоваться классом HaspPrintRequestAttributeSet, который предусмотрен в SDK.
HaspPrintRequestAttributeSet attributes
new HaspPrintRequestAttributeSet() ;
Дальше нужно передать объект attributesметоду printDialog.
Метод printDialog возвращает либо значение true, если пользователь щелкает на кнопке Print, либо значение false, если пользователь отменяет внесенные изменения и прекращает работу с этим диалоговым окном. В первом случае вызывается метод print класса PrinterJob для начала процесса печати. Он может генерировать исключительную ситуацию PrinterException. Вот как выглядит фрагмент кода для организации процесса печати.
if (job.printDialogfattributes)) {
try
{
job.print();
)
catch (PrinterException exception)
}
)
Рис. 7.34. Платформонезависимое диалоговое окно параметровпечати
• Для поддержки SDK версии 1.2 или 1.3 нужно изменить программу для отображения платформозависимого диалогового окна параметров печати вместо платформонезависимого, которое предусмотрено в SDK версии 1.4, Эта ситуация описана подробно в отступлении в конце этого раздела.
Во время печати метод print класса PrinterJob повторно вызывает метод print объекта Printable, который связан с данным процессом печати.
Дело в том, что процессу печати неизвестно количество печатаемых страниц, поэтому метод print вызывается, и печать страниц продолжается до тех пор, пока он возвращает значение Printable.PAGE_EXISTS. Печать прекратится лишь тогда, когда метод print возвратит значение Printable,NO_SUCH_PAGE.
Номера страниц, которые процесс печати передает методуprint, начинаются с нуля.
Поэтому процесс печати не может определить количество страниц до тех пор, пока печать не будет закончена. Исходя из этого, при такой организации печати диалоговое окно параметров печати не может отобразить фактический диапазон печати. В следующем разделе показан способ устранения этого недостатка с помощью объекта Book.
Во время процесса печати задание печати может повторно вызывать метод print объекта Printable, например, для одной и той же страницы. Поэтому можно не считать страницы внутри метода print, а просто полагаться на номер страницы. Возможность многократного обращения к одной и той же странице предназначена для обслуживания некоторых принтеров, особенно матричных и струйных, которые используют полосовой (banding) принцип печати. Они печатают одну полосу за один раз, продвигают бумаг)' и затем печатают следующую полосу. Полосовой принцип печати может использоваться даже для лазерных принтеров, которые обычно печатают сразу всю страницу. Это дает возможность управлять размером спул-файла.
Для создания объекта Printable для полосовой печати следует указать область усечения графического контекста, которая будет располагаться в печатаемой полосе, и вызвать метод print. Таким образом, все операции рисования будут выполняться только в этой полосе, и только этот элемент изображения будет показан на экране. Метод print не обязательно должен знать об этих действиях при условии, что он не конфликтует с областью усечения.
НИМАНИНИМАНИУчтите, что возвращаемый методом print объект типа Graphics усекается по границам страницы. Но рисовать за пределами границ страницы можно, если переместить область усечения. Однако, в графическом контексте принтера нужно строго соблюдать заданную область усечения. То есть, для дальнейшего ограничения области усечения следует вызывать метод clip (неsetclip).А если нужно временно удалить область усечения, то для этого следует вызвать методgetClip в началеметодаp r i n t,азатем восстановитьобластьусечения.
НИМАНИ
НИМАНИ
Параметр PageFormat метода print содержит информацию о печатаемой странице. Методы getWidth и getHeight возвращают размер бумаги, измеряемый в пунктах (points). Одна точка равна 1/72 дюйма, а дюйм равен 25,4 миллиметра. Например, страница формат А4 имеет размер примерно 595 пунктов в ширину и 842 пункта в длину, а страница формата Letter имеет размер примерно 612 пунктов в ширину и 792 пункта в длину.
Пункты являются общепринятой единицей измерения в полиграфической промышленности США. К сожалению полиграфистов остальных стран, в пакете java.awt.printв качестве единицы измерения используются пункты. Это объясняется тем, что размер бумаги и полей измеряется в пунктах, причем пункт также является используемой по умолчанию единицей измерения для всех графических контекстов печати. В этом можно убедиться на примере программы в конце раздела. Эта программа печатает две строки текста, которые находятся на расстоянии 72 пункта друг от друга. Запустите программу, измерьте расстояние между напечатанными строками и убедитесь в том, что оно равно точно 1 дюйму или 25,4 мм.
Методы getwidth и getHeight класса PageFormat возвращают полную ширину и высоту страницы. Но печатать можно не на всей поверхности страницы. Обычно польз»
ватели самостоятельно задают размер поля, но даже если они этого не сделают явно, то все равно поля будут заданы неявно. Дело в том, что принтеру нужно как-то держать листы бумаги, на которых выполняется печать, и поэтому по краям остаются маленькие недоступные для печати области. Методы getImageableWidth и get ImageableHeight возвращают ширину и высоту доступной для печати области. Однако, поля не обязательно должны быть симметричны, поэтому нужно также знать координаты верхнего левого угла доступной для печати области, которая показана на рис. 7.35. Эти координаты можно получить с помощью методов getlmageableXи getlmageableY.
Графический контекст, который передается методу print, обрезается по полям страницы, но начало системы координат, несмотря ни на что, остается В верхнем левом углу страницы. Поэтому имеет смысл перенести начало координат в левый верхний угол доступной для печати области. Для чего нужно просто включить в начале кода метода print следующий оператор.
g.translate(pageFormat.getlmageableX(), pageFormat.getlmageableY());
Высота доступнойдля печати областиВысота доступнойдля печати областиКоординатыдоступнойдля печати области
Высота доступнойдля печати области
Высота доступнойдля печати области
Ширинастраницы
Рис. 7.35. Размерыстраницыидоступнойдляпечатиобласти
Для предоставления пользователю возможности установки границ страницы или переключения книжной и альбомной ориентации страницы, не устанавливая при этом других атрибутов печати, можно просто вызвать метод pageDialog класса Printer Job.
PageFormat format = job.pageDialog(attributes);
Одна из вкладок диалогового окна параметров печати содержит параметры страницы (рис. 7.36), которые пользователь может просмотреть и отредактировать непосредственно перед печатью. Это особенно полезно, если в программе реализован принцип "что видишь на экране, то и получишь при печати". Метод pageDialog возвращает объект PageFormat со значениями параметров печати, которые заданы пользователем.
Рис. 7.36. Платформонезависимоедиалого- вое окно параметров печати
В примере программы, код которой приводится в листинге 7.12, демонстрируются способы отображения набора фигур на экране и в напечатанной странице. Класс PrintPanel расширяет класс JPanel и реализует интерфейс Printable, а его методы paintComponent и print вызывают один и тот же метод drawPage для фактического рисования изображения.
class PrintPanel extends JPanel implements Printable {
public void paintcomponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
drawPage(g2);
}
public int print(Graphics g, PageFormat pf, int page) throws PrinterException
if (page >= 1) return Printable.NO_SUCH_PAGE;
Graphics2D g2 = (Graphics2D) g;
g2.translate(pf.getlmageableX()# pf*getlmageableY() ) / drawpage(g2);
return Printable. PAGE_EXISTS;
}-
public void drawPage(Graphics2D g2) {
// Здесь следует общий код
// фактического рисования изображения
)
}
В SDK версии 1.2 для графического контекста печати не предусмотрена автоматическая установка черного цвета кисти. Если в результате печати будут получены полностью белые страницы, то это значит, что нужно добавить в код программы строку g2.setPaint (Color.black) в начале метода print для установки черного цвета кисти. Этот недостаток устранен в SDK версии 1.3.
В этом примере демонстрируется способ отображения на экране и распечатки изображения из программы, код которой приведен в листинге 7.6. В нем контуры строки Hello, World используются как область усечения для набора линий (см. рис. 7.22).
Для запуска процесса печати щелкните на кнопке Print, а ДЛЯ вызова диалогового окна параметров печати — на кнопке Page setup. Полный текст этой программы приведен в листинге 7.12.
Использование платформозависимого
диалогового окна печати
До появления SDK версии 1.4 в механизме печати Java использовались диалоговые окна параметров печати и настройки параметров страницы именно той платформы, на которой работал пользователь. Для отображения платформозависимого диалогового окна печати нужно вызвать метод printDialog без указания параметров. (В этом случае не существует никакого способа объединения пользовательских установок в наборе атрибутов). Например, на рис. 7.37 показано диалоговое окно печати для операционной системы Windows.
Рис. 7.37. Диалоговое окно параметров печати в операционной системе WindowsРис. 7.37. Диалоговое окно параметров печати в операционной системе WindowsРис. 7.38. Диалоговое окно параметров страницы в операционной системе WindowsРис. 7.38. Диалоговое окно параметров страницы в операционной системе WindowsОдно из преимуществ использования платформозависимого диалогового окна печати заключается в том, что можно использовать специальные возможности драйверов разных принтеров, которые не доступны в платформонезависимом диалоговом окне. Учтите, что для поддержки SDK 1.2 и SDK 1 *3 необходимо использовать платформозависимоедиалоговое окно.
Рис. 7.37. Диалоговое окно параметров печати в операционной системе Windows
Рис. 7.37. Диалоговое окно параметров печати в операционной системе Windows
Рис. 7.38. Диалоговое окно параметров страницы в операционной системе Windows
Рис. 7.38. Диалоговое окно параметров страницы в операционной системе Windows
Для того, чтобы отобразить платформозависимое диалоговое окно параметров страницы, нужно передать объект PageFormat методуpageDialog. Этот метод создает копию объекта, изменяет ее в соответствии с пользовательскими настройками и возвращает скопированный объект.
PageFormat defaultFormat =
pageFormat selectedFormat = printJob.pageDialog(defaultFormat);
На рис. 7.38 показано диалоговое окно параметров страницы в операционной системе Windows.
Листинг 7.12. PrlntTeit.java
/** @version 1.10 2001-08-07 ©author Cay Horstmann
/
import java.awt.*;
import j ava.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import j ava.awt.print.*;
import java.util.*;
import javax.print.*;
import javax.print.attribute.*;
import j avax.swing.*;
/**
Пример печати двумерной графики
/
public class PrintTest
{ public static void main(String[) args) {
JFrame frame = new PrintTestFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
)
}
/**
Фрейм с панелью для двумерного изображения, а также кнопок печати и установки параметров страницы
/
class PrintTestFrame extends JFrame {
public PrintTestFrame()
{
setTitle(”PrintTest”);
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane();
canvas = new PrintPanel();
content Pane.add(canvas, BorderLayout.CENTER);
attributes = new HashPrintRequestAttributeSet();
JPanel buttonPanel = new JPanel();.
JButton printButton = new JButton("Print");
buttonPanel.add(printButton);
printButton.addActionListener(new
ActionListener()
{
public void actionPerf ontied (ActionEvent event) {
try
{
Printerjob job = PrinterJob.getPrinterJob()' j
if (job.printDialog(attributes)) {
job.print(attributes);
}
}
catch (PrinterException exception)
{
JOptionPane.showMessageDialog( PrintTestFrame.this,exception);
}
)
});
JButton pageSetupButton = new JButton("Page setup");
buttonPanel.add(pageSetupButton);
pageSetupButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
PrinterJob job = PrinterJob.getPrinterJob();
job.pageDialog(attributes);
} ));
contentPane.add(buttonPanel, BorderLayout.NORTH);
}
private PrintPanel canvas;
private PrintRequestAttributeSet attributes;
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
/**
Панель с двумерным изображением для
демонстрации его отображения на экране и в напечатанном виде
*/
class PrintPanel extends JPanel implements Printable {
public void paintComponent(Graphics g)
{
super.paintComponent(g) ;
Graphics2D g2 = (Graphics2D)g;
drawPage(g2);
public int print(Graphics g, PageFormat pf, int page) throws PrinterException
{
if (page1) return Printable.NO_SUCH_PAGE;
Graphics2D g2 = (Graphics2D)g;
g2.translate(pf.getlmageableX(), pf.getlmageableY() ) ;
g2.draw(new Rectangle2D.Double(0, 0,
pf.getlmageableWidth(), pf.getlmageableHeight() ) ) ;
drawPage(g2);
return Printable.PAGE_EXISTS;
/**
Код рисования страницы на экране и
в графическом контексте печати
@param g2 the graphics context
*/
public void drawPage(Graphics2D g2)
{
FontRenderContext context = g2.getFontRenderContext() ;
Font f = new Font("Serif", Font.PLAIN, 72);
GeneralPath clipShape = new GeneralPath();
TextLayout layout = new TextLayout("Hello", f, context) AffineTransform transform
=72);
Shape outline = layout.getOutline(transform);
clipshape.append(outline, false);
layout =newTextLayout("World", f, context); transform
=144);
outline = layout.getOutline(transform);
clipshape.append(outline, false);
g2.draw(clipshape);
g2.clip(clipShape);
final int NLINES =50;
Point2D p = new Point2D.Double(0, 0);
for (int i = 0; i < NLINES; i++) {
double x = (2 * getwidth() * i) / NLINES;
double у = (2 * getHeightO * (NLINES - 1 - i)) / NLINES;
Point2D q = new Point2D.Double(x, y) ;
g2.draw(newLine2D.Double(p,q) );
java.awt .print.Printable
Метод int print(Graphics g, PageFormat format, int pageNumber) отображает страницу и возвращает значение PAGE_EXISTS ИЛИNO SUCH PAGE.
Параметры: дformatПараметры: дformatграфический контекст отображаемой страницы формат страницы
Параметры: д
format
Параметры: д
format
pageNumberномер требуемой страницы
java.awt.print.PrinterJob
Метод static Printe Job getPrinterJob () возвращает объект- задание печати.
Метод PageFormat defaultPage () возвращает установленный no умолчанию формат страницы для данного принтера.
Метод boolean printDialog(PrintRequestAttributeSet attributes) вызывает платформонезависимое диалоговое окно параметров печати, которое дает возможность пользователю указать страницы, которые следует напечатать, и настройки принтера. Он изменяет объект attributes с учетом пользовательских настроек и возвращает значение true, если пользователь одобрил имеющиеся параметры печати.
Метод boolean printDialog () вызывает платформозависимое диалоговое окно параметров печати, которое дает возможность пользователю указать страницы, которые нужно напечатать, и настройки принтера. Он возвращает значение true, если пользователь одобрил имеющиеся параметры печати.
МетодPageFormat pageDialog(PrintRequestAttributeSet
attributes) отображает платформонезависимое диалоговое окно настройки параметров страницы. Он возвращает объект PageFormat с тем форматом страницы, который пользователь установил в этом диалоговом окне, и изменяет объект attributes с учетом пользовательских настроек.
Метод PageFormat pageDialog (PageFormat defaults) отображает платформозависимое диалоговое окно настройки параметров страницы. Он возвращает объект PageFormat с тем форматом страницы, который пользователь установил В этом диалоговом окне, но не изменяет объект defaults.
Метод void setPrintable(Printable p) задает для данного задания печати печатаемый объектр, который расширяет интерфейс Printable.
Метод void setPrintable(Printable p, PageFormat format) задает для данного задания печати печатаемый объект р, который расширяет интерфейс Printable, и формат страницы format (по желанию).
Метод void print () распечатывает текущий печатаемый объект с помощью повторных вызовов его метода print и отправки отображаемых страниц принтеру до тех пор, пока они будут доступны.
java.awt.print.PageFormat
Метод double getWidth() возвращает ширину страницы.
Метод double getHeight () возвращает высоту страницы.
Метод double getImagewidth() возвращает ширину доступной для печати области страницы.
Метод double getlmageHeight О возвращает высоту доступной для печати области страницы.
Методы double getlmageableX() и double getlmageableY() возвращают координаты верхнего левого угла доступной для печати области страницы.
Метод int getOrientation() возвращает одно из следующих значений ориентации страницы PORTRAIT, LANDSCAPE, REVERSE-LANDSCAPE. Параметры формата страницы и установки графического контекста автоматически учитывают ориентацию страницы.
Печать нескольких страниц
На практике нежелательно передавать объекты типа Printable без предварительной обработки. Сначала следует получить объект класса, который реализует интерфейс Pageable. В платформе Java для этого предусмотрен класс-книга Book, который состоит из классов-разделов типа Printable. Итак, создадим объект-книгу, указывая объекты-разделы и нумерацию страниц так, как показано ниже.
Book book = new Book();
Printable coverPage =
Printable bodyPages= ...;
book.append(coverPage, pageFormat); // Добавление 1 страницы.
book.append(coverPage, pageFormat, pageCount);
Затем следует использовать метод setPageable для передачи объекта-книги Book.
printJob.setPageable(book) ;
Теперь заданию печати точно известно количество печатаемых страниц и в диалоговом окне печати точно отображается диапазон печатаемых страниц, которым пользователь может самостоятельно управлять, выбирая весь диапазон или только его часть.
НИМАНИНИМАНИКогда задание печати вызывает метод print объектов- разделовтипа Printable , то при этом передается текущий номер страницы в масштабе всей книги, а не текущего раздела. Это очень неудобно, поскольку в каждом разделе должна храниться информация о количестве страниц в предыдущих разделах.
НИМАНИ
НИМАНИ
Наибольшую сложность при использовании класса-книги Book представляет подсчет и сохранение информации о количестве страниц в каждом разделе в ходе процесса печати. В объекте типаPrintable нужно создать алгоритм обработки макета {layout algorithm) для расчета макета печатаемой страницы. Его нужно выполнить перед началом печати для определения разрывов страниц и количества страниц. Данную информацию о макете страницы можно сохранить для использования во время печати.
При этом нужно учесть вероятность того, что пользователь может изменить формат страницы. В таком случае придется повторно рассчитать макет страницы, даже
если печатаемая информация не изменилась.
В примере программы, код которой приведен в листинге 7.13, демонстрируется способ печати нескольких страниц. Эта программа печатает плакат, состоящий из строки символов очень большого размера, которые располагаются на нескольких страницах, как показано на рис. 7.39. После печати нескольких страниц поля страниц
Метод layoutPages класса Banner рассчитывает макет страницы, а потом — макет строки сообщения с размером шрифта 72 пункта. Затем вычисляется высота результирующей строки и сравнивается с высотой печатаемой области страницы. На основании этих двух измерений определяется масштабный коэффициент. Далее при распечатке строки она будет увеличена в соответствии с этим коэффициентом.
Для точного расположения на странице печатаемой информации обычно требуется получить доступ к графическому контексту принтера. К сожалению, не предусмотрено никакого способа получения графического контекста до тех пор, пока действительно не начнется процесс печати. В данной программе все действия выполняются с экранным графическим контекстом в надежде, что характеристики шрифтов на экране и напечатанной странице совпадают.
Метод getPageCount класса Banner сперва вызывает метод определения макета страницы. Затем он масштабирует ширину строки и делит ее на ширину доступной для печати области отдельной страницы. Частное от такого деления, округленное до следующего целого значения, определяет искомое количество страниц.
Перемещение
Усечение
Hello,World!Hello,World!
Hello,World!
Hello,World!
Масштабирование
Рис. 7.40. Алгоритм постраничной печати большого плаката
После всего сказанного выше может показаться, что печать плаката представляет собой довольно трудную задачу, потому что отдельные части символа могут располагаться на разных страницах. Однако, благодаря мощным средствам Java 2D API, эта
задача решается очень просто. Для получения определенной страницы используется метод translate класса Graphics2D, который смещает верхний левый угол строки на заданное расстояние влево. Затем устанавливается прямоугольная область усечения с размерами текущей страницы, как показано на рис. 7.40. Наконец, графический контекст масштабируется в соответствии с масштабным коэффициентом, вычисленным с помощью метода компоновки страницы.
Этот пример иллюстрирует возможности функций преобразования, благодаря которым код рисования остается очень простым. Как видите, преобразования выполняют всю работу по размещению изображения в соответствующем месте и усечению той части изображения, которая выходит за пределы доступной для печати области страницы. В следующем разделе показан еще один пример использования преобразований для организации предварительного просмотра печатаемой страницы.
Предварительный просмотр
В большинстве качественных коммерческих программ предусмотрен механизм предварительного просмотра, который позволяет просматривать внешний вид напечатанной страницы на экране компьютера до запуска процесса печати. Это позволяет не тратить бумагу на печать страниц с неудовлетворительным внешним видом. В классах печати платформы Java не предусмотрено стандартное диалоговое окно предварительного просмотра. Однако, собственное диалоговое окно предварительного просмотра, например, показанное на рис. 7.41, можно довольно легко создать самостоятельно. В этом разделе рассматривается один из способов создания такого окна. Приведенный в листинге 7.13 класс PrintPreviewDialog имеет довольно общую структуру, и его можно использовать для предварительного просмотра внешнего вида любых типов печатаемых страниц.
Для создания класса PrintPreviewDialog используется либо класс Printable, либо класс Book вместе с объектом PageFormat. Для отображения внешнего вида диалогового окна используется класс PrintPreviewCanvas. При использовании кнопок Next (Далее) и Previous (Назад) для перехода к следующей и предыдущей страницам метод paintcomponent вызывает метод print объекта Printable для запрашиваемой страницы.
Обычно метод print отображает содержимое страницы в графическом контексте принтера. Однако, в данном случае используется экранный графический контекст, масштабированный таким образом, чтобы целая печатаемая страница поместилась внутри небольшой прямоугольной области экрана.
float xoff =// левая сторона страницы
float yoff = . .. ; // верхняя сторона страницы float scale = ...; // масштабный коэффициент для подгонки
// размеров страницы к размерам
// небольшой прямоугольной области экрана g2.translate(xoff, yoff);
g2.scale(sclae, scale);
Printable printable = book.getPrintable(currentPage);
printable.print(g2, pageFormat, currentPage);
В данном случае метод print работает так же, как и при фактической печати страницы, т.е. он просто рисует страницу в графическом контексте, которая в уменьшенном виде отображается в диалоговом окне предварительного просмотра. Этот пример очень хорошо демонстрирует возможности графической модели Java 2D.
В листинге 7.13 содержится код программы печати большого плаката с диалоговым окном предварительного просмотра. Введите строку Hello, World! в текстовом поле, посмотрите на внешний вид печатаемой страницы в диалоговом окне предварительного просмотра, а затем напечатайте плакат.
Листинг7.13. BookTest.java
/** (aversion 1.10 2001-08-07 @author Cay Horstmann
*/
import j
import j ava.awt.event.*;
import j ava.awt.font.*;
import java.awt.geom.*;
Рис. 7.41. Диалоговое окно предварительного просмотраРис. 7.41. Диалоговое окно предварительного просмотраimport j ava.awt.print.*;
Рис. 7.41. Диалоговое окно предварительного просмотра
Рис. 7.41. Диалоговое окно предварительного просмотра
import java.util.*;
import javax.print.*;
import javax.print.attribute.*;
import javax.swing.*;
Пример программы, которая демонстрирует механизм печати
многостраничного объекта-книги на примере "плаката" со строкой большого размера, отдельные символы которой
располагаются на разных страницах. Эта программа также
содержит диалоговое окно предварительного просмотра
*/
public class BookTest
{
public static void main(String[] args)
{
JFrame frame = new BookTestFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Фрейм с текстовым полем для ввода текста плаката и кнопками печати, установки параметров страницы и предварительного просмотра
*/
class BookTestFrame extends JFrame {
public BookTestFrame()
{
setTitleCBookTest”) ;
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane(); text = newJTextField();
contentpane.add(text, BorderLayout.NORTH);
attributes newHashPrintReguestAttributeSet () ;
JPanel buttonPanel new JPanel();
JButtonprintButton=newJButton("Print");
buttonPanel.add(printButton);
printButton.addActionListener(new
{
p{ublic void actionPerformed(ActionEvent event)
t{ry
PrinterJob job = PrinterJob.getPrinterJob(J; jo)d.setPageable(makeBook());
(job.printDialog(attributes))(job.printDialog(attributes))if
(job.printDialog(attributes))
(job.printDialog(attributes))
{
}
}
} catch
{
)
job.print(attributes);
(PrinterException exception)
JOptionPane. showMessageDialog,(
BookTestFrame.this, exception);
JButton pageSetupButton = new JButton("Page setup");
buttonpanel.add(pageSetupButton);
pageSetupButton.addActionListener(new
ActionListener()
{
{{public void actionPerformed(ActionEvent event)
{
{
} });} });PrinterJob job = PrinterJob.getPrinterJob(); pageFormat =
} });
} });
JButton printPreviewButton = newJButton("Print preview" ) ; buttonPanel.add(printPreviewButton);
printPreviewButton.addActionListener(new
{
{{public void actionPerformed(ActionEvent event)
{
{
PrintPreviewDialog dialog
) });) });=newPrintPreviewDialog(makeBook()) ; dialog.show();
) });
) });
contentpane.add(buttonPanel, BorderLayout.SOUTH); }
/**
Создание объекта-книги с титульной страницей и обычными страницами */ public Book makeBook() {
if (pageFormat == null) {
PrinterJob job = PrinterJob.getPrinterJob(); pageFormat = job.defaultPage();
}
Book book = new Book();
String message = text.getText();
Banner banner = new Banner(message); int pageCount =
pageFormat);
book.append(new CoverPage(message + " (" + pageCount + " pages)"), pageFormat);
book.append(banner, pageFormat, pageCount); return book;
}
private JTextField text;
private PageFormat pageFormat;
private PrintReqruestAttributeSet attributes;
private static final int WIDTH = 300; private static final int HEIGHT = 100;
/**
Плакат с текстовой строкой,
расположенной на нескольких страницах
*/
class Banner implements Printable {
/**
Создание плаката
©param m the message string
*/
public Banner(String m) { message = m;
)
/**
Определение количества страниц в данном разделе
@param g2 the graphics context
@param pf the page format
©return the number of pages needed
*/
public int getPageCount(Graphics2D g2, PageFormat pf) {
if (message.equals("")) return 0;
FontRenderContext context = g2.getFontRenderContext();
Font f = new Font("Serif", Font.PLAIN, 72);
Rectangle2D bounds = f.getStringBounds(message, context); scale = pf.getlmageableHeight() / bounds.getHeight();
double width = scale * bounds.getWidth() ;
int pages = (int)Math.ceil(width / pf.getlmageableWidth()); return pages;
}
public int print(Graphics g, PageFormat pf, int page) throws PrinterException
{
Graphics2D g2 = (Graphics2D)g;
if (page > gdtPageCount(g2, pf)) return Printable.NO_SUCH_PAGE;
g2.translate(pf.getlmageableXO, pf.getImageableY());
drawPage(g2, pf, page);
return Printable.PAGE_EXISTS;
public void drawPage(Graphics2D g2, PageFormat pf, int page) (
if (message.equals("")) return;
page--;
drawCropMarks(g2, pf) ;
g2.clip(new Rectangle2D.Double(0, 0,
pf.getlmageableWidth(), pf.getlmageableHeight<) ));
g2.translate(-page * pf.getlmageableWidth(), 0);
g2.scale(scale, scale);
FontRenderContext context = g2.getFontRenderContext() ;
Font f = new Font("Serif", Font.PLAIN, 72);
TextLayout layout = new TextLayout(message, f, context); AffineTransform transform =
layout.getAscent());
Shape outline = layout.getOutline(transform);
g2.draw(outline);
)
/**
Отображение контуров доступной для печати области с полями шириной 1/2 дюйма @param g2 the graphics context @param pf the page format
*/
public void drawCropMarks(Graphics2D g2, PageFormat pf) {
final double С = 36; // crop mark length = 1/2 inch double w = pf.getlmageableWidth();
double h = pf.getlmageableHeight();
g2.draw(new Line2D.Double(0, 0, 0, C));
g2 .draw(new Line2D.Double (0, 0, C, 0));
g2.draw(new Line2D.Double(w, 0, w, C));
g2 . draw (new Line2D. Double (w, 0, w — C, 0));
g2.draw(new Line2D.Double(0, h, 0, h — C));
g2 .draw(new Line2D.Double(0, h, C, h));
g2.draw(new Line2D.Double(w, h, w, h — C));
g2.draw(new Line2D.Double(w, h, w — C, h));
}
private String message;
private double scale;
/**
Титульная страница с заголовком
*/
class CoverPage implements Printable {
/**
Создание титульной страницы
брагаш t the title
*/
public CoverPage(String t)
{ title = t;
}
public int print(Graphics g, PageFormat pf, int page) throws PrinterException
{
if (page >= 1) return Printable.NO_SUCH_PAGE;
Graphics2D g2 = (Graphics2D)g;
g2.setPaint(Color.black) ;
g2.translate(pf.getlmageableX(), pf.getlrnageableY()); FontRenderContext context = g2 . getFontRenderContext () Font f = g2.getFont();
TextLayout layout = new TextLayout(title, f, context), float ascent = layout.getAscent();
g2.drawstring(title, 0, ascent);
return Printable.PAGE_EXISTS; J
private String title;
}
/**
Класс диалогового окна предварительного просмотра обобщенного типа
*/
class PrintPreviewDialog extends JDialog {
/**
Создание диалогового окна предварительного просмотра
@param p a Printable
@param pf the page format
@param pages the number of pages in p
*/
public PrintPreviewDialog(Printable p, PageFormat pf, int pages)
{
Book book = new Book();
book.append(p, pf, pages);
layoutUI(book);
}
Создание диалогового окна предварительного просмотра ©param b a Book
*/
public PrintPreviewDialog(Book b)
{
layoutUI(b);
}
/**
Макет пользовательского интерфейса диалогового окна предварительного просмотра ©param book the book to be previewed
*/
public void layoutUI(Book book) {
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane() ;
canvas = new PrintPreviewCanvas(book); contentpane.add(canvas, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
JButton nextButton = new JButton("Next");
buttonPanel.add(nextButton);
nextButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
canvas.flipPage(1);
}
}) ;
JButton previousButton = new JButton("Previous”);
buttonPanel.add(previousButton);
pr evi ousBu11on. addAc t i onLi s t ener (new
ActionListener()
p{ublic void actionPerformed(ActionEvent event) canvas.flipPage(-1);
});
JButton closeButton = new JButton("Close");
buttonPanel.add(closeButton);
closeBut ton. addAc t ionLi s tener (new
ActionListener()
p{ublic void actionPerformed(ActionEvent event) setVisible(false);
});
contentpane.add(buttonPanel, BorderLayout.SOUTH);
}
private PrintPreviewCanvas canvas;
private static final int WIDTH = 300;
private static final int HEIGHT300;
/**
Канва диалогового окна предварительного просмотра
*/
class PrintPreviewCanvas extends JPanel {
/**
Создание канвы диалогового окна предварительного просмотра брагam b the book to be previewed
*/
public PrintPreviewCanvas(Book b) {
book = b;
currentPage = 0;
}
public void paintcomponent(Graphics g) {
super.paintcomponent(g) ;
Graphics2D g2(Graphics2D)g;
PageFormat pageFormat = book.getPageFormat(currentpage);
double xoff;
double yoff;
double scale;
double px = pageFormat.getwidth();
double py = pageFormat.getHeight();
double sx = getwidth() - 1;
double sy = getHeight() — 1;
if
(px /
py <
sx / sy) // центрирование no горизонтали
{
scale
= sy
/ py;
xoff =
0.5
* (sx - scale * px) ;
}
yoff =
0;
else // центрирование no вертикали {
scale = sx / px;
xoff = 0;
yoff = 0.5 * (sy — scale * py);
}
g2 .translate((float)xoff, (float)yoff);
g2.scale((float)scale, (float)scale) ;
// рисование контуров страницы (без полей)
Rectangle2D page = new Rectangle2D.Double(0, 0, px, py);
g2.setPaint(Color.white);
g2.fill(page);
g2.setPaint(Color.black) ;
g2.draw(page);
Printable printable = book.getPrintable(currentpage);
try
{
printable.print(g2, pageFormat, currentPage); }
catch (PrinterException exception)
{
g2 .draw(new Line2D.Double(0, 0, px, py) ) ;
g2 .draw(new Line2D.Double(0, px, 0, py)) ;
}
/**
Переход к странице с заданным номером
@param by the number of pages to flip by.
Negative values flip backwards.
*/
public void flipPage(int by)
{
int newPage = currentPage + by;
if (0 <= newPage && newPage < book. getNuinberOf Pages ()) {
currentPage = newPage;
repaint();
}
private Book book;
private int currentPage;
java.awt.print.PrinterJob
Метод void setPageable (Pageable p) устанавливает для печати объект типа Pageable, например, объект-книгу Book.
java.awt.print.Book
Методы void append (Printable p, PageFormat format) и void append (Printable p, PageFormat format, int pageCount) добавляют новый раздел в данную книгу.
• Метод Printable getPrintable(int page) возвращает печатаемый объект типа Printable для указанной страницы.
Службы печати
В предыдущих разделах до сих пор рассматривались способы печати двумерной графики. Однако, интерфейс печати в SDK версии 1.4 обладает более широкими возможностями печати многих других перечисленных ниже типов данных и использования для них служб печати.
Изображения в форматахGIF, JPEG или PNG.
Текстовые документы в форматах HTML, PostScript или PDF.
Неформатированные данные принтера.
Объекты класса, который реализуют методы Printable, Pageable или
Renderablelmage.
Сами по себе данные могут быть сохранены в виде байтов или символов, например, во входном потоке, по URL-адресу или в массиве. Комбинация типа данных и источника данных называется типом документа (document flavor). Класс DocFlavor содержит несколько внутренних классов для различных источников данных. Каждый из этих внутренних классов содержит константы для указания типов документов. Например, показанная ниже константа описывает изображение в формате GIF, который считывается из входного потока.
DocFlavor.INPUT_STREAM.GIF
Все другие типы документов, т.е. допустимые комбинации источника данных и типа данных (с указанием MIME-типаданных), приведены в табл. 7.3.
Предположим, что нужно напечатать изображение в формате GIF, которое находится в файле. Для выполнения этой задачи следует определить, имеется ли соответствующая служба печати (print service), которая способна управлять выполнением данной задачи. Статический метод lookupPrintServices класса PrintServiceLookup возвращает массив объектов PrintService, которые могут управлять печатью определенных типов документов.
DocFlavor flavor = DocFlavor.INPUT_STREAM. GIF;
Printservice[] services
=null) ;
Второй параметр метода lookupPrintServices имеет значение null, которое означает, что поиск подходящей службы печати не ограничивается какими-то атрибутами печати. Более подробно атрибуты печати рассматриваются в следующем разделе.
В пакете SDK версии 1.4 поддерживаются базовые службы печати для основных типов документов, таких как изображения и двухмерная графика. Но при попытке печати текстового документа или HTML-документа будет возвращен пустой массив служб печати PrintService [ ].
Если в результате поиска служб печати получен массив PrintService [] с более чем одним элементом, то среди перечисленных служб печати следует выбирать какую- то одну. Для извлечения списка имен служб печати (например, имен принтеров) и предоставления пользователю возможности выбора можно вызвать метод getName класса printService.
Далее нужно получить задание печати выбранной службы печати.
DocPrintJob job = services[i].createPrintJob();
Для печати объекта с заданным типом документа нужно создать объект, который реализует интерфейс Doc. Для этого в SDK предусмотрен класс-документ SimpleDoc. Конструктору класса-документа SimpleDoc следует передать объект-источник данных, тип документа и необязательный набор атрибутов.
Inputstream in = new Fileinputstream(fileName);
Doc doc = new SimpleDoc(in, flavor, null);
Теперь все готово для печати.
job.print(doc, null);
Как и раньше, значение nullвторого параметра можно заменить набором атрибутов.
Таблица 7.3.Типы документов
Источник данных
Тип данных
MIME-mun данных
INPUT_
STREAM
GIF
image/gif
URL
JPEG
image/jpeg
BYTE_ARRAY
PNG
image/png
POSTSCRIPT
application/postscript
application/pdf
TEXT_HTML_HOST
text Xhtml (с кодировкой хоста)
TEXT_HTML_US_ASCII
text/html; charset=us-ascii
TEXT_HTML_UTF_8
text/html; charset=utf-8
TEXT_HTML_UTF_16
text/html; charset=utf-16
TEXT_HTML_UTF_16LE
text/html; charset=utf-161e (little-endian - с прямым порядком байтов)
TEXT_HTML_UTF_16BE
text/html; charset=utf-16be (big-endian - с обратным порядком байтов)
TEXT_PLAIN_HO ST
text/plain (с кодировкой хоста)
TEXT_PLAIN_US_ASCII
text/plain; charset=us-ascii
TEXT_PLAIN_UTF_8
text/plain; charset=utf-8
TEXT_PLAIN_UTF_16
text/plain; charset=utf-16
TEXT_PLAIN_UTF_16LE
text/plain; charset=utf-161e (little-endian - с прямым порядком байтов)
TEXT_PLAIN_UTF_16BE
text/plain; charset=utf-16be (big-endian — с обратным порядком байтов)
PCL
application/vnd.hp-PCL(Hewlett Packard Printer Control Language)
AUTOSENSE
application/octet-stream (неформатированные данныепринтера)
READER
TEXT_HTML
text/html; charset=utf-16
STRING
TEXT_PLAIN
text/plain; charset=utf-16
CHAR_ARRAY
SERVICE_
PRINTABLE
—
FORMATTED
PAGEABLE
—
RENDERABLE_IMAGE
-
Учтите, что этот процесс печати совершенно отличается от того, который описывался в предыдущем разделе, потому что здесь отсутствует какое-либо взаимодействие пользователя и процесса печати посредством диалоговых окон печати. На основе данного механизма печати можно реализовать процесс печати на сервере, в котором пользователи могут выполнять печать с помощью Web-форм.
Программа, код которой приводится в листинге 7.14, демонстрирует использование службы печати для распечатки графического файла.
Листинг7.14.PrintServloeTeet • Java
/** aversion 1.00 2001-08-08 @author Cay Horstmann
*/
import j
import j
import java.awt.image.*;
import java.io.*;
import java.util.*;
import j
import j avax.swing.*;
/**
Пример использования служб печати для печати графического файла в формате GIF с помощью любых служб печати, которые поддерживают тип документа GIF
*/
public class PrintServiceTest
{ public static void main(String[] args) {
JFrame frame = new PrintServiceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Фрейм, в котором отображается печатаемое изображение. Он содержит команды меню для открытия графического файла, печати и выбора службы печати
*/
class PrintServiceFrame extends JFrame {
public PrintServiceFrame() {
setTitle("PrintServiceTest") ;
setsize(WIDTH, HEIGHT);
// Установка команд меню
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar) ;
JMenu menu = new JMenu("File");
menuBar.add(menu) ;
JMenuItem openitem = new JMenuItem("Open");
menu.add(openItem);
openitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
openFile();
)
)) ;
JMenuItem printitem = new JMenuItem("Print");
menu.add(printitem);
printitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
printFile() ;
}
1);
JMenuItem exititem = new JMenuItem("Exit" ) ;
menu.add(exitItern) ;
exititem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.exit(0) ;
}
});
menu = new JMenu("Printer");
menuBar.add(menu);
DocFlavor flavor = DocFlavor.INPUT_STREAM.GIF;
addPrintServices(menu, flavor);
// Использование ярлыка для отображения изображений label = new JLabel();
Container contentPane = getContentPane();
contentPane.add(label);
}
/**
Включение служб печати в меню
©param menu the menu to which to add the services
©param flavor the flavor that the services need to support
*/
public void addPrintServices(JMenu menu, DocFlavor flavor)
{
PrintService[] services
=null);
ButtonGroup group = new ButtonGroup();
for (int i = 0; i < services.length; i++) {
final PrintService service = services[i];
JRadioButtonMenuItem item
= new JRadioButtonMenuItem(service.getName());
menu.add(item);
if (i == 0) {
item.setSelected(true); currentService = service;
}
group.add(item);
i
ActionListener() {
public void actionPerformed(ActionEvent event) {
currentService = service;
}
}) ;
)
)
/**
Открытие GIF-файла и отображение изображения
*/
public void openFile()
{
// Установка диалогового окна выбора файла JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File(". ”));
// Выбор всех файлов с расширением .gif.
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter() {
public boolean accept(File f) {
return f.getName().toLowerCase() .endsWith(".gif")
|| f.isDirectory() ;
}
public String getDescription() {
return "GIF Images";
) });
// Отображение диалогового окна выбора файлов
int r = chooser.showOpenDialog(PrintServiceFrame.this);
// Если графический файл выбран,
// то задать его пиктограмму для ярлыка
if (r ==
{
fileName = chooser.getSelectedFile().getPath(); label.seticon(new Imageicon(fileName));
)
)
/**
Печать текущего файла с помощью текущей службы печати */
public void printFile() {
try
if (fileName == null) return;
if (currentservice == null) return;
FilelnputStream in = new Fileinputstream(fileName) ;
DocFlavor flavor = DocFlavor.INPUT_STREAM.GIF;
Doc doc = new SimpleDoc(in, flavor, null);
DocPrintJob job = currentservice.createPrintJob();
job.print(doc,null);
}
catch (FileNotFoundException exception)
I
JOptionPane.showMessageDialog(this, exception);
)■
catch (PrintException exception)
(
JOptionPane.showMessageDialog(this, exception);
private JLabel label;
private String fileName;
private PrintService currentservice;
private static final int WIDTH = 300;
privatestaticfinalintHEIGHT=400
f^p jfjavax.print.PrintServiceLookup
Метод Printservice[] lookupPrintServices(DocFlavor flavor,
AttributeSet attributes) ищет службы печати, которые могут управлять заданным типом документа и атрибутами.
Параметры:
flavorflavorтип документатип документа
flavor
flavor
тип документа
тип документа
attributes требуемые атрибуты печати или значение null, если ат-
рибуты не принимаются в расчет
javax. print. Printservice
J. ■ I • Метод DocPrintJob createPrintJob () создает задание печати для печати объекта класса, который реализует интерфейс Doc (например, SimpleDoc).
E
^~( javax.print.DocPrintJob
Метод void print (Doc doc, PrintRequestAttributeset
attributes) распечатывает заданный документ doc с заданными атрибутами attributes.
объект-документобъект-документdocdocПараметры:
объект-документ
объект-документ
doc
doc
attributes требуемые атрибуты печати или значение null, если атрибуты печати не требуются
javax.print.SimpleDoc
• Конструктор SimpleDoc(Object data, DocFlavor flavor, DocAttributeSet attributes) создает объект-документ SimpleDoc, который может быть распечатан с помощью метода DocPrin Job.
Параметры:
dataобъект, который содержит данные для печати, такие как
входной поток или объект Printable
flavorтип документа с печатаемыми данными
attributesтребуемые атрибуты печати или значение null, если
атрибуты печати не требуются
Потоковые службы печати
Обычные службы печати создают данные печати и посылают их принтеру, а потоковые службы печати создают такие же данные печати, но посылают их в поток. Это необходимо, например, для задержки печати или для использования их в других программах. Например, если данные печати имеют формат PostScript, то полезно сохранить их в файле, поскольку многие программы могут работать с файлами этого типа.
В SDK версии 1.4 предусмотрены потоковые службы печати, которые могут создавать данные печати в формате PostScript для изображений и двухмерной графики. Эти службы можно использовать для всех систем, даже для тех, у которых нетлокальных принтеров.
Перечисление потоковых служб печати организовано немного сложнее, чем перечисление локальных служб печати. Для этого нужно получить объект-тип документа DocFlavor для печатаемого объекта и MIME-тип выходного потока, а затем получить массив StreamPrintServiceFactory.
DocFlavor flavor = DocFlavor.SERVICE-FORMATTED.PRINTABLE;
String mimeType = "application/postscript";
StreamPrintServiceFactory[] factories
= (flavor, mimeType);
В классе StreamPrintServiceFactory нет ни одного метода, с помощью которого можно было бы отличить одну фабрику потоковой службы печати от другой, так что в данном случае следует выбрать самую первую, т.е. factories [0]. Для получения объекта-службы печати StreamPrintService следует вызвать метод getPrintService с указанием выходного потока в качестве параметра.
OutputStream out = new FileOutputStream(fileName) ;
StreamPrintService service = factories[0].getPrintService(out);
Класс StreamPrintService является подклассом класса PrintService, поэтому для организации процесса печати нужно просто выполнить все остальные действия, описанные в предыдущем разделе.
Программа, код которой приведен в листинге 7.15, создает файл печати в формате PostScript со строкой Hello, World!.
• В отличие от других программ печати, эта программа хорошо работает под управлением операционной системы Linux. Для просмотра полученного результата, как показано на рис. 7.42, следует использовать программу просмотраGhostScript, которую можно скопировать по адресуhttp://www.es.wise.edu/~ghost/.
Рис. 7.42. ПросмотрфайлавформатеPostScript
Листинг7.15. StreamPrintServlceTegt.java
©version 1.00 2001-08-08
©author Cay Horstmann
import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import j ava.awt.geom.*;
import java.awt.print.*;
import java.io. *;
'import java.util.*;
import javax.print.*;
import javax.print.attribute.*;
import j*;
Пример использования потоковой службы печати для печати двумерной графики в файл формата PostScript
public class StreamPrintServiceTest {
public static void main(String[] args)
С
JFrame frame = new StreamPrintServiceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Фрейм с панелью, которая содержит двумерную графику, а также кнопки для печати графики в формате PostScript и для указания формата страницы
*/
class StreamPrintServiceFrame extends JFrame {
public StreamPrintServiceFrame()
{
setTitle("StreamPrintServiceTest");
setsize(WIDTH, HEIGHT);
Container contentPane = getContentPane() ;
canvas = new PrintPanel() ;
contentPane.add(canvas, BorderLayout.CENTER);
attributes = new HashPrintRequestAttributeSet();
JPanel buttonPanel = new JPanel();
JButton printButton = new JButton("Print");
buttonpanel.add(printButton);
printButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
String fileName = getFileO;
if (fileName != null) printPostScript(fileName);
}
});
JButton pageSetupButtonnew JButton("Page setup");
buttonPanel.add(pageSetupButton) ;
pageSetupButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
Printerjob job = PrinterJob.getPrinterJob() ; job.pageDialog(attributes);
} });
contentPane.add(buttonPanel, BorderLayout.NORTH);
}
/**
Установка диалогового окна выбора
файлов формата PostScript
@return the file name, or null if the user didn't select a file.
*/
public String getFile()
{
// Установка диалогового окна выбора файлов
JFileChooser chooser = new JFileChooser();
chooser. setCurrentDirectory (new File (". 11) ) ;
// Выбор всех файлов с расширением . ps
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter()
{
public boolean accept(File f)
{
return f.getName().toLowerCase()
.endsWith(".ps") || f.isDirectory();
}
public String getDescription()
{ return "PostScript Files";
}
));
// Отображение диалогового окна выбора файлов int r = chooser.showSaveDialog(this);
// После выбора файла вернуть путь
if(r == JFileChooser.APPROVE_OPTION)
return chooser.getSelectedFile().getPath();
else
return null;
}
/**
Печать двумерной графики в файл формата PostScript
@param fileName the name of the PostScript file
*/
public void printPostScript(String fileName)
{
try
(
DocFlavor flavor
=;
String mimeType = "application/postscript";
StreamPrintServiceFactory[] factories =
StreamPrintServiceFactory
.lookupStreamPrintServiceFactories(flavor, mimeType);
FileOutputStream out = new FileOutputStream(fileName);
if (factories.length == 0) return;
StreamPrintService service =
Doc doc = new SimpleDoc(canvas, flavor, null);
DocPrintJob job = service.createPrintJob();
job.print(doc, attributes);
catch (FileNotFoundException exception) {
JOptionPane.showMessageDialog(this, exception); }
catch (PrintException exception)
{
JOptionPane.showMessageDialog(this, exception); }
}
private PrintPanel canvas;
private PrintRequestAttributeset attributes;
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
/**
Панель с двумерным изображением для отображения на экране и печати
*/
class PrintPanel extends JPanel implements Printable
{
public void paintcomponent(Graphics g)
{
super.paintcomponent(g) ;
Graphics2D g2 = (Graphics2D)g;
drawPage(g2) ;
}
public int print(Graphics g, PageFormat pf, int page) throws PrinterException
{
if (page >= 1) return Printable.NO_SUCH_PAGE;
Graphics2D g2 = (Graphics2D)g;
g2.translate(pf.getlmageableX(), pf.getlmageableY());
g2.draw(new Rectangle2D.Double(0, 0,
pf.getlmageableWidth(), pf.getlmageableHeight() ) ) ;
drawPage(g2);
return Printable.PAGE_EXISTS;
}
/**
Метод рисования изображения в экранном графическом контексте и в графическом контексте принтера @param g2 the graphics context
*/
public void drawPage(Graphics2D g2)
{
FontRenderContext context = g2.getFontRenderContext();
Font f = new Font("Serif", Font.PLAIN, 72);
GeneralPath clipShape = new GeneralPath();
TextLayout layout = new TextLayout("Hello", f, context), AffineTransform transform
=72);
Shape outline = layout.getOutline(transform) ; с1ipShape.append(outline, false) ;
layout = new TextLayout("World", f, context); transform
=144);
outline = layout.getOutline(transform);
clipShape.append(outline, false);
g2.draw(clipShape);
g2 .clip(clipShape) ;
final int NLINES =50;
Point2D p = new Point2D.Double(0, 0);
for (int i = 0; i < NLINES; i++) {
double x = (2 * getWidth() * i) / NLINES;
double у = (2 * getHeight() * (NLINES - 1 - i)) / NLINES;
Point2D q = new Point2D.Double(x, y) ;
g2.draw(new Line2D.Double(p, q));
javax.print.StreamPrintServioeFactory
Метод StreamPrintServiceFactory[] lookupStreamPrintServiceFactories (DocFlavor flavor, String mimeType) ищет потоковые службы печати, которые могут печатать объекты с указанным типом документа flavorи создавать выходной поток данных указанного М1МЕ-типа.
• МетодStreamPrintService getPrintService (OutputStream out) возвращает службу печати, которая передает данные печати в указанный поток выходных данных out.
Атрибуты печати
API-интерфейс службы печати платформы Java содержит сложный набор интерфейсов и классов для указания различных типов атрибутов, которые делятся на четыре основные группы. Первые две группы атрибутов относятся к характеристикам печатаемых принтером объектов-документов.
Атрибуты задания печати (print requestattributes) используются для отдельных характеристик всех объектов-документов данного задания печати (например, двухсторонняя печать или размер бумаги).
Атрибуты документа ^doc attributes) используются только для какого-либо одного объекта-документа.
Остальные две группы атрибутов содержат информацию о принтере и состоянии задания печати.
Атрибуты службы печати {print service attributes) содержат информацию о службе печати (например, о производителе и модели принтера, принимает ли принтер задание печати в данный момент или нет).
Атрибуты задания печати (printjob attributes) содержат информацию о статусе определенного задания печати (например, выполнено ли уже задание или нет).
Для описания различных атрибутов предусмотрен интерфейс Attribute со следующими подчиненными интерфейсами.
PrintRequestAttribute
DocAttribute
PrintServiceAttribute
PrintJobAttribute
SupportedValuesAttribute
Отдельные классы атрибутов реализуют один или более из этих интерфейсов. Например, объекты класса Copies используются для описания количества копий распечатываемого материала. Этот класс реализует интерфейсы PrintRequestAttribute и PrintJobAttribute. Задание печати может содержать запрос на печать нескольких копий, а атрибутом задания печати может быть количество фактически напечатанных копий. Фактическое количество копий может быть меньше требуемого количества из-за ограничений принтера или отсутствия бумаги.
Интерфейс SupportedValuesAttribute показывает, что значения атрибута не отображают реальный запрос или данные о текущем состоянии, а показывают возможности службы. Например, объект класса CopiesSupported реализует интерфейс Supported- ValuesAttribute и сообщает, что принтер поддерживает печать от 1 до 99 копий.
На рис. 7.43 показана диаграмма классов, которые образуют иерархию атрибутов.
Рис. 7.43. Иерархия атрибутов
В дополнение к интерфейсам и классам для отдельных атрибутов, в API- интерфейсе службы печати предусмотрены интерфейсы и классы для наборов атрибутов. Например, интерфейс набора атрибутов AttribureSet содержит четыре подчиненных интерфейса.
PrintRequestAttributeSet
DocAttributeSet
PrintServiceAttributeSet
PrintJobAttributeSet
Для каждого из них предусмотрен класс, реализующий этот интерфейс.
HashAttributeSet
HashPrintRequestAttributeSet
HashDocAttributeSet
HashPrintServiceAttributeSet
HashPrintJobAttributeSet
На рис. 7.44 показана диаграмма классов, которые образуют иерархию наборов атрибутов. Ниже показан типичный пример создания набора атрибутов для запроса Ha печать.
PrintReguestAttributeSet attributes = new HashPrintRequestAttributeSet();
Рис. 7.44. Иерархия наборов атрибутов
Зачем же нужны все эти интерфейсы? Они дают возможность проверить корректность использования атрибутов. Например, интерфейс DocAttributeSet принимает только те объекты, которые реализуют интерфейс DocAttribute, а попытка добавить другой атрибут приведет к ошибке.
Набор атрибутов представляет собой очень специализированный тип коллекции- карты, в которой ключи являются объектами класса Class, а значения— объектами класса, который реализует интерфейс Attribute. Например, если создать и вставить объект Copies (10) в набор атрибутов, то его ключом будет объект Copies.class класса Class. Этот ключ называется категорией (category) атрибутов. Интерфейс Attribute содержит объявление показанного ниже метода, который возвращает категорию атрибута.
Class getCategory()
Класс Copies содержит определение этого метода для возврата объекта Copies.class, но совсем необязательно, чтобы категория была объектом того же класса, что и класс атрибута.
При добавлении атрибута в набор атрибутов категория извлекается автоматически, поэтому достаточно добавить значение атрибута.
attributes.add(new Copies(lO));
Если последовательно добавить другой атрибут из той же категории, то второй атрибут будет записан вместо первого.
Категория используется как ключ для извлечения атрибута.
AttributeSet attributes job.getAttributes() ;
Copies copies = (Copies)attribute.get(Copies.class);
Атрибуты также группируются no типам значения. Например, атрибут Copies может иметь любое целочисленное значение, потому что класс Copies является расширением класса IntegerSyntax, который предназначен для обработки всех целочисленных атрибутов. Метод getValue возвращает целочисленное значение атрибута.
int n = copies. getValue О;
Показанные ниже классы инкапсулируют строку, дату/время и URI-указатель.
TextSyntax
DateTimeSyntax
URISyntax
Наконец, есть много атрибутов, которые могут принимать ограниченное количество значений. Например, атрибут PrintQuality имеет всего три допустимых значения для указания экономного, нормального и высокого качества печати, которые представлены следующими константами.
PrintQuality.DRAFT
PrintQuality.NORMAL
PrintQuali ty.HIGH
Классы атрибутов с ограниченным количеством значений происходят от класса EnumSyntax, который обеспечивает безопасную работу нескольких очень удобных методов для установки значений коллекций-перечислений. Благодаря этим классам, разработчику не нужно беспокоиться о подробностях работы данного механизма при использовании таких атрибутов. Достаточно добавить в набор атрибутов именованные значения.
attributes.add(PrintQuality.HIGH);
Для проверки значения атрибута нужно применить следующую конструкцию.
if (attributes.get(PrintQuality.class) ==
В табл. 7.4 представлены атрибуты печати, где во втором столбце перечислены суперклассы класса атрибутов (например, класс IntegerSyntax атрибута Copies) или набор значений для атрибутов с ограниченным набором значений. В последних четырех столбцах крестиками отмечены интерфейсы, на основе которых реализован данный класс атрибута: DocAttribute (DA), PrintJobAttribute (PJA), PrintRequestAttribute (PRA) и PrintServiceAttribute(PSA).
• Как видите, многие из перечисленных в табл. 7.4 атрибутов являются узкоспециализированными и появились вследствие применения протокола Internet Printing Protocol 1.1 (RFC2911).
На этом рассмотрение механизма печати завершается. В настоящем и предыдущих разделах описаны способы печати двухмерных изображений и других типов документов, выбора служб печати и служб потоковой печати, а также установки и получения значений атрибутов. Далее рассматриваются два важных вопроса организации пользовательского интерфейса, а именно, работа с буфером обмена и механизм перетаскивания.
В SDK 1.4 появились классы JobAttributes и PageAttributes, которые выполняют роль описанных в этом разделе атрибутов печати. Если в вашей программе не требуется организовывать поддержку SDK 1.3, то в таком случае рекомендуется использовать более надежный механизм служб печати, предлагаемый в SDK 1.4.
Таблица7.4. Атрибуты печати
Атрибут
Суперкласс
или набор константDA PJA FRA PSA
Chromaticity
MONOCHROME,COLORXXX
ColorSupported
SUPPORTED, NOT_SUPPORTEDx
Compression
COMPRESS,DEFLATE,GZIP, NONEx
Copies
Integersyntaxxx
DataTimeAtCompleted
DateTimeSyntaxx
Dat aTimeAtCreati on
DateTimeSyntaxx
DataTimeAtProcessing
DateTimeSyntaxx
Destination
URISyntaxXX
DocumentName
TextSyntaxX
Fidelity
FIDELITY_TRUE,XX
FIDELITY_FALSE
Finishing
NONE,STAPLE,EDGE_STITCH,XXX
BIND,SADDLE_STITCH,COVER,...
JobHoldUntil
DateTimeSyntaxXX
Joblmpressions
Integersyntaxxx
JoblnpressionsCompleted
Integersyntaxx
JobKOctets
Integersyntaxxx
JobKOctetsProcessed
Integersyntaxx
JobMediaSheets
Integersyntaxxx
Продолжение табл. 7.4
Атрибут
Суперкласс или набор констант
DA
PJA
PRA
PSA
JobMediaSheetsCcmpleted '
IntegerSyntax
X
JobMessageFromOperator
TextSyntax
X
JobName
TextSyntax
X
X
JobOriginatingUserName
TextSyntax
X
JobPriority
IntegerSyntax
X
X
JobSheets
STANDART, NONE
X
X
JobState
ABORTED,CANCELED,COMPLETED, PENDING,PENDING_HELD, PROCESSING,
PROCESSING-STOPPED
X
JobStateReason
ABORTED—В Y_S YS ТЕМ, DOCUMENT_FORMAT_ERRORИТ.Д.
JobStateReasons
HashSet
X
MediaName
ISO,A4_WHITE,
IS0_A4_TRANSPARENT, NA_LETTER_WHITE, NA_LETTER_TRANSPARENT
X
X
X
Mediasize
ISO. AO - ISO.A10, ISO.BO - ISO.BIO, ISO.C0-ISO.C10,
NA. LETTER,
NA. LEGAL, а также другие размеры страниц и конвертов
MediaSizeName
ISO.AO - ISO.А10, ISO.BO -ISO.BIO, ISO.CO - ISO. CIO,
X
X
X
NA.LETTER,
NA.LEGAL,атакжедругиеимена форматов страниц и конвертов
MediaTray
TOP,MIDDLE,BOTTOM,SIDE, ENVELOPE,LARGE_CAPACITY, MAIN,MANUAL
X
X
X
MultipleDocumentHandling
SINGLE-DOCUMENT,
SINGLE_DOCUMENT_NEW_SHEET, SEPARATE—DOCUMENTS—COLLATED -COPIES,
SEPARATE—DOCUMENTS_UNCOLLAT
ED_COPIES
X
X
Продолжение табл. 7.4
Атрибут
Суперкласс или набор констант
DA
PJA
PRA
PSA
NumbsrOfDocuments
Integersyntax
X
NuitiberOf InterveningJobs
Integersyntax
X
NumberUp
IntegerSyntax
X
X
X
OrientationRequested
PORTRAIT,LANDSCAPE,
REVERSE_PORTRAIT, REVERSE_LANDSCAPE
X
X
X
OutputDeviceAssigned
PageRanges
TextSyntax
X
X
X
X
SetOflnteger
PagesPerMinute
PagesPerMinuteColor
IntegerSyntax
X
X
PDLOverrideSupported
IntegerSyntax
ATTEMPTED, NOT ATTEMPTED
X
PresentationDirection
TORIGHT_TOBOTTOM, TORIGHT_TOTOP, TOBOTTOM_TORIGHT, TOBOTTOM_TOLEFT, TOLEFT_TOBOTTOM, TOLEFT_TOTOP, TOTOP_TORIGHT, TOTOP_TOLEFT
X
X
PrinterInfo
TextSyntax
X
PrinterIsAccept ingJobs
ACCEPTING_JOBS, NOT_ACCEPTING_JOBS
X
Printer-Location
TextSyntax
X
PrinterMakeAndModel
TextSyntax
X
PrinterMessageFromDperat
TextSyntax
X
or
PrinterMorelnfo
URISyntax
X
PrinterMorelnfoManufactu rer
URISyntax
X
PrinterName
TextSyntax
X
PrinterResolution
ResolutionSyntax
X
X
X
PrinterState
PROCESSING,IDLE,STOPPED, UNKNOWN
X
PrinterStateReason
COVER_OPEN, FUSER_OVER_TEMP, MEDIA_JAMидр.
Атрибут
Суперкласс
или набор констант
DA
Окончание табл. 7.4
PJA
PRA
PSA
PrinterStateReasons
HashMap
PrinterURI
URISyntax
X
PrintQuality
DRAFT,NORMAL,HIGH
X
X
X
Queued!obCount
Integersyntax
X
ReferenceUriSchemesSuppo rted
FILE,FTP,GOPHER,HTTP,HTTPS,
NEWS, NNTP, WAIS
RequestingUserName
TextSyntax
X
Severi ty
ERROR,REPORT, WARNING
X
X
X
SheetCollate
COLLATED, UNCOLLATED
X
X
X
Sides
ONE_SIDED, DUPLEX
(TWO_SIDED_LONG_EDGE), TUMBLE (TWO_SIDED_SHORT_EDGE)
X
X
X
javax. print. attribute. Attribute
Метод Class getCategory () возвращает категорию атрибута.
Метод String getName () возвращает название атрибута.
javax.print. attribute. AttributeSet
Метод boolean add (Attribute attr) добавляет атрибут в данный набор атрибутов. Если набор имеет другой атрибут из той же категории, то новый атрибут замещает его. Возвращает значение true, если в результате этой операции набор изменился.
Метод Attribute get (Class category) возвращает атрибут с данной категорией или значение null, если такого атрибута не существует.
Метод boolean remove (Attribute attr) удаляет указанный атрибут из набора атрибутов. Этот метод возвращает значение true, если набор изменился в результате данной операции.
Метод boolean remove (Class category) удаляет атрибут указанной категории из набора атрибутов. Этот метод возвращает значение true, если набор изменился в результате данной операции.
Метод Attribute [] toArray() возвращает массив, содержащий все атрибуты данного набора атрибутов.
javax. print. PrintService
Метод PrintServiceAttributeSet getAttributes() возвращает атрибуты данной службы печати.
javax.print.DocPrintJob
• Метод PrintJobAttributeSet getAttributes() возвращает атрибуты данного задания печати.
Буфер обмена
Одним из наиболее полезных и удобных механизмов обслуживания графического пользовательского интерфейса (например, Windows и X Window System) является технология копирования и вставки (cut andpaste). Она основана на выделении некоторых данных в одной программе, последующего вырезания или копирования их в буфер обмена и вставки содержимого буфера обмена в другую программу. Используя буфер обмена, можно переносить текст, рисунки или другие данные из одного документа в другой или из одной части документа в другую. Механизм вырезания и вставки настолько привычен, что большинство пользователей компьютеров даже не задумываются об этом.
Однако, в SDK 1.0 не предусматривалась поддержка этого механизма, т.е. нельзя было вырезать и вставлять данные в приложениях Java. Например, при использовании Web-броузера, созданного на языке Java, нельзя было копировать текст и изображения из Web-страниц и вставлять их в текстовый редактор, основанный на технологии Java. Дело в том, что в SDK 1.1 был реализован лишь очень примитивный механизм переноса данных на основе буфера обмена.
Впоследствии этот механизм постепенно улучшался и теперь широко используется на многих платформах.
Хотя принцип работы с буфером обмена очень прост, реализация его служб в действительности гораздо сложнее, чем это может показаться на первый взгляд. Предположим, что какой-то фрагмент текста скопирован из текстового редактора в буфер обмена. При вставке этого текста в другой текстовый редактор было бы желательно, чтобы шрифт и элементы форматирования данного фрагмента оставались прежними. Это значит, что буфер обмена должен также содержать информацию о форматировании фрагмента текста. А при вставке этого фрагмента текста в простое текстовое поле достаточно вставить текст без информации о форматировании. Для обеспечения такой гибкости при работе с буфером обмена источник данных должен уметь предоставлять данные в буфере обмена в различных форматах, а потребитель данных должен уметь выбирать один из них.
Системный буфер обмена в Microsoft Windows, OS/2 и Macintosh реализован практически одинаково, за исключением некоторых различий. Например, в механизме работы с буфером обмена в X Window System имеются некоторые ограничения в отношении вырезания и вставки некоторых элементов с более сложным форматом, чем простой текст. Эти ограничения следует учитывать при тестировании программы в данном разделе.
Для проверки типов объектов, которые могут передаваться между приложениями Java и системным буфером обмена, следует проверить содержимое файла jre/lib/flavormap. properties на используемой платформе.
Часто в программах нужно организовать поддержку копирования и вставки таких типов данных, которые не поддерживаются системным буфером обмена. API-интерфейс передачи данных поддерживает передачу ссылок на произвольные локальные объекты в рамках одной виртуальной машины. А между различными виртуальными машинами можно также передавать сериализируемые объекты и ссылки на удаленные объекты.
В табл. 7.5 перечислены допустимые способы передачи данных с помощью буфера обмена.
Таблица 7.5. Допустимые способы передачи данных с помощью буфера обмена Java
Способ передачи данных
Формат данных
Между программой на языке Java и другой платформозависимой программой
Текст, рисунки, списки файлов ... (в зависимости от платформы)
Между двумя программами на языкеJava
Сериализируемые и удаленные объекты
Внутри одной программы на языке Java
Любые объекты
Классы и интерфейсыдля передачи данных
Передача данных в технологии Java реализована при помощи пакета java.awt. datatransfer, который помимо прочих содержит следующие наиболее важные классы и интерфейсы этого набора.
Класс Clipboard описывает буфер обмена. Системный буфер обмена является конкретным примером реализации класса Clipboard.
Буфер обмена может получать или передавать только те объекты, которые реализуют интерфейс Transferable.
Класс DataFlavor описывает типы документов, которые могут быть помещены в буфер обмена.
Класс StringSelection реализует интерфейсTransferable и используется для передачи строк.
Для получения уведомления о перезаписи содержимого буфера класс должен реализовывать интерфейс ClipboardOwner. Управление правом владения интерфейсом позволяет выполнять "отложенное форматирование" сложных данных. Если программа передает простые данные (например, строку), то она просто обновляет содержимое буфера обмена и переходит к выполнению следующих действий. Однако, если программа собирается вставить в буфер обмена сложные данные, которые могут быть представлены различными типами документов, то не рекомендуется обрабатывать все эти типы документов, так как есть вероятность того, что многие из них вообще не понадобятся. Однако, нужно сохранить эти сложные данные в буфере обмена таким образом, чтобы впоследствии можно было обрабатывать типы документов по мере необходимости. Поэтому, владелец буфера обмена с помощью метода lostOwnership должен быть своевременно уведомлен об изменении содержимого буфера обмена. Это означает, что информация о сложных данных больше не требуется. В примерах данной главы механизм управления правами владения буфером обмена не используется.
Передача текста
Для первого знакомства с классами передачи данных начнем с простейшей ситуации, т.е. передачи текста с помощью системного буфера обмена. Идея следующей программы очень проста. Сначала нужно получить ссылку на системный буфер обмена.
Clipboard clipboard =
Toolkit.getDefaultToolkit().getSystemClipboard();
Строки, которые передаются в буфер обмена, необходимо заключить в объект- оболочку StringSelection с помощью показанного ниже конструктора, принимающий в качестве параметра текст, который нужно передать.
String text = ...
StringSelection selection = new Stringselection(text);
Передача осуществляется при вызове метода setContents, который принимает объекты StringSelection и ClipBoardOwner в качестве параметров. Если владелец буфера обмена не имеет значения, то второму параметру следует присвоить значение null.
clipboard.setcontents(selection, null);
Рассмотрим теперь обратную операцию, т.е. чтение строки из буфера обмена, для чего используется метод getContents объекта буфера обмена clipBoard, который возвращает объект типа Transferable.
Transferable contents = clipBoard.getContents(null);
В соответствии с документацией, параметром метода getContents является ссылка на требуемый объект. Однако, в настоящее время не ясно, как буфер обмена собирается использовать данную информацию.
Метод getContents может возвращать значение null. Это значит, что либо буфер обмена пуст, либо не содержит данных, которые могут быть обработаны платформой Java.
Объект типа Transferable позволяет получить информацию о типах документов, которые используются для представления данных в буфере обмена. Обработка типов документов более подробно рассматривается далее в этой главе. В данном примере используется только стандартный тип документа DataFlavor. stringFlavor. Для определения доступности определенного типа документа следует использовать метод isDataFlavorSupported.
Наконец, нужно передать заданный тип документа методу getTrans erData. Вызов этого метода должен быть заключен в блок try, поскольку он может генерировать исключительную ситуацию UnsupportedDataFlavorException, если требуемый тип документа не доступен, либо исключительную ситуацию IOException, если метод не смог прочитать данные.
DataFlavor flavor = DataFlavor.stringFlavor;
if (contents.isDataFlavorSupported(flavor))
try
{
String text = (String) (contents.getTransferData(flavor)) ;
какие-то операции с текстом
catch(UnsupportedFlavorException exception) {
}
catch(lOException exception)
{
}
}
[^TeHTianrfetTeyt №1G[^TeHTianrfetTeyt №1GРис. 7.46. Просмотр содержимого буфера обмена Windows после копирования данныхРис. 7.46. Просмотр содержимого буфера обмена Windows после копирования данныхПрограмма, код которой приведен в листинге 7.16, демонстрирует механизм копирования и вставки в приложениях Java с использованием системного буфера обмена. На рис. 7.45 показан внешний вид этой программы. Если выбрать фрагмент текста в текстовой области и щелкнуть на кнопке Copy (Копировать), то выбранный текст будет скопирован в системный буфер обмена. Как показано на рис. 7.46, скопированный текст действительно скопировался в системный буфер обмена. Если щелкнуть на кнопке Paste (Вставить), то содержимое буфера обмена (которое могло быть скопировано из другой программы) будет вставлено в текущей позиции курсора.
[^TeHTianrfetTeyt №1G
[^TeHTianrfetTeyt №1G
Рис. 7.46. Просмотр содержимого буфера обмена Windows после копирования данных
Рис. 7.46. Просмотр содержимого буфера обмена Windows после копирования данных
«Multimedia Takes Center Stage
Z PasteZ PasteThe JavaTM Media Framework 2.0 API steps {into the spotlight (with a full-featured, easy to use, scalable, extensible, and cross-platform alternative to the proprietary chaos of the past. (September 17)
Z Paste
Z Paste
Рис. 7.45. Внешний вид программы
TextTransferTest
Листинг■7; 16. TextTrans ferTest.java:
/**
(Aversion 1.11 2001-08-11
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.datatransfer.’ ;
import j;
import java.io.*;
import j
/**
Пример передачи текста
между приложением на языке Java и системным буфером обмена
*/ public class TextTransferTest
{
public static void main(String!] args) {
JFrame frame = new TextTransferFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
} }
/**
Фрейм с текстовым окном и кнопками копирования и вставки текста
*/ class TextTransferFrame extends JFrame {
public TextTransferFrame()
{
setTitle("TextTransferTest");
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane();
textArea = new JTextArea();
contentPane.add(new JScrollPane(textArea) , BorderLayout.CENTER);
JPanel panel = new JPanel();
JButton copyButton = new JButton("Copy") ;
panel.add(copyButton);
copyButton.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent event) {
copy();
));
JButton pasteButton = new JButton("Paste") ; panel.add(pasteButton);
pasteButton.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent event) {
pasted ;
});'
contentPane.add(panel, BorderLayout.SOUTH); }
/**
Копирование выбранного текста в системный буфер обмена */
private void copy() <
Clipboard clipboard =
String text = textArea.getSelectedText();
if (text == null) text = textArea.getText();
StringSelection selection = new StringSelection(text); clipboard.setcontents(selection, null);
/**
Вставка выбранного текста в системный буфер обмена
*/
private void paste() {
Clipboard clipboard =
Transferable contents = clipboard.getContents(this) ; if (contents == null) return;
DataFlavor flavor = DataFlavor.stringFlavor;
if (contents.isDataFlavorSupported(flavor)) {
try {
String text =
textArea.replaceSelection(text);
}
catch(UnsupportedFlavorException exception) {
JOptionPane.showMessageDialog(this, exception); } catch(lOException exception) {
j JOptionPane.showMessageDialog(this, exception); }
}
private JTextArea textArea;
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
3ava.awt.Toolkit
• Метод Clipboard getSystemClipboard() возвращает системный буфер обмена.
3 ava.awt.datatransfer.Clipboard
Метод Transferable getContents (Object requester) возвращает содержимое буфера обмена.
Параметры:
requester объект, который запрашивает содержимое буфера обмена;
это значение на самом деле пока никак не используется
Метод void setContents (Transferable contents, ClipboardOwner owner) задает содержимое буфера обмена.
Параметры:
contents объект, инкапсулирующий содержимое буфера обмена ownerобъект будет извещен об изменении содержимого буфера
обмена с помощью метода lostOwnership, либо значение null, если такое извещение не требуется
java.awt.datatransfer.Clipboardowner
Метод void lostOwnership (Clipboard clipboard, Transferable contents) оповещает объект о том, что он больше не является владельцем содержимого буфера обмена.
Параметры:
clipboard. буфер обмена, содержание которого изменилось contentsобъект данных, который был помещен в буфер обмена дан
ным владельцем
java.awt.datatransfer.Transferable
Метод boolean isDataFlavorSupported(DataFlavor flavor) возвращает значение true, если указанный тип документа поддерживается, либо значение false— в противном случае.
Метод Object getTransferData(DataFlavor flavor) возвращает данные, отфоматированные в соответствии с указанным типом документа. Генерирует исключительную ситуацию UnsupportedFlavorException, если указанный тип документа не поддерживается.
Интерфейс Transferable и типы документов
Буфер обмена может содержать только те объекты, которые реализуют интерфейс Transferable. Для определения всех типов документов, которые поддерживает такой объект, предусмотрен метод getTransferDataFlavors.
DataFlavor[] flavors = transferable.getTransferDataFlavors()
Объект типа документа DataFlavor определяется следующими характеристиками.
ИмяMIME-типа, например,image/gif).
Класс его представления, который используется для доступа к данным, например, j ava.awt. Image.
Кроме того, каждый тип документа имеет более читабельное название, например, GIF Image.
Класс представления может быть указан с помощью параметра class в MIME-типе.
image/gif;class=java.awt.Image
Это всего лишь пример, который иллюстрирует синтаксис указания типа документа. Никакого стандартного типа документа для передачи графических данных в Dформате GIF не существует.
Если параметр class не задан, то в качестве класса представления используется класс Inputstream. Для передачи локальных, сериализированных или удаленных Java-объектов предусмотрены следующие три MIME-типа.
applications/x-java-jvm-local-objectref applications/x-java-serial!zed-object applications/x-j ava-remote-object
DDПрефикс х- означает, что это экспериментальное название, которое еще не санкционировано агенством по выделению имен и уникальных параметров протоколов Internet (Internet Assigned Numbers Authority — IANA), которое назначает имена для стандартных MIME-типов.
D
D
Например, стандартный тип документа stringFlavor описывается с помощью следующего MIME-типа.
applications/x-java-serialized-object;class=java.lang.String
••••j ava. awt. datatrans far. DataFlavor
•
•
•
•
Конструктор DataFlavor(String mimeType, String humanPre- sentableName) создает тип документа, который описывает потоковые данные в формате, который приведен в указанном MIME-типе.
Параметры:
mimeTypeMIME-тип
HumanPresentableName более удобное для чтения имя MIME-типа
Конструктор DataFlavor(Class class, String humanPresent- ableName) создает тип документа, который описывает класс платформы Java со следующим MIME-типом.
applications/x-java-seria ized-object class--имя_кла cca
Параметры: classПараметры: classhumanPresentableNamehumanPresentableNameкласс представления, который используется для передачи данных с данным типом документа более удобное для чтения имя MIME-типа
Параметры: class
Параметры: class
humanPresentableName
humanPresentableName
Метод String getMimeType () возвращает строку с MIME-типом данного типа документа.
Метод boolean isMimeTypeEqual(String mimeType) проверяет наличие у данного типа документа соответствующего MIME-типа.
Метод String humanPresentableName () возвращает более удобное для чтения имя формата для данного типа документа.
Метод Class getRepresentationClass () возвращает класс представления объекта, который используется для передачи данных с данным типом документа. Это либо параметр class MIME-типа, либо класс InputStream.
java.awt.datatransfer.Transferable
Метод DataFlavor [ ] getTransferDataFlavors () возвращает массив поддерживаемых типов документов.
Передача изображений через буфер обмена
Объекты, которые нужно передать через буфер обмена, должны реализовывать интерфейс Transferable. Класс Stringselectionявляется единственным открытым классом в стандартной библиотеке Java, который реализует интерфейс Transferable. В этом разделе рассматривается способ передачи изображений через буфер обмена. В SDK не предусмотрены никакие классы для такой передачи изображений, поэтому их придется реализовать самостоятельно.
Показанный ниже класс ImageSelection имеет тривиальную структуру. Он просто сообщает, что единственным типом документа, доступным в данной программе для использования в буфере обмена, является тип DataFlavor. imageFlavor, который содержит объект image.
class ImageSelection implements Transferable {
public Imageselection(Image image)
{= image;
}
public DataFlavor[] getTransferDataFlavors()'
{
return new DataFlavor[] { DataFlavor.imageFlavor };
}
public boolean isDataFlavorSupported(DataFlavor flavor) ;
{ return flavor.equals(DataFlavor.imageFlavor);
}
public Obj ect getTransferData(DataFlavor flavor)
throws UnsupportedFlavorException
{
if(flavor.equals(DataFlavor.imageFlavor))
{ return thelmage;
}
else {
throw new UnsupportedFlavorException(flavor);
)
}
private Image thelmage;
SDK содержит константу DataFlavor. imageFlavor и автоматически выполняет всю работу по преобразованию Java-изображений в платформозависимые изображения буфера обмена. Но, как ни странно, в SDK не предусмотрен класс- оболочка, который необходим для помещения изображения в буфер обмена.
Программа, код которой приведен в листинге 7.17, демонстрирует передачу изображения между приложением Java и системным буфером обмена. При запуске программы она генерирует изображение фрактального множества Мандельброта. Щелкните на кнопке Copy для копирования изображения в буфер обмена, а затем вставьте
его в другое приложение, как показано на рис. 7.47. Скопируйте изображение из другого приложения в системный буфер обмена и затем щелкните на кнопке Paste для вставки скопированного изображения в эту программу, как показано на рис. 7.48.
Рис. 7.47. Копирование изображения из Java-программы в другую платформозависимую программу
Рис. 7.48. Копирование изображения из другой платформозависимой программы в Java-программу
Данная программа является усовершенствованной версией предыдущего примера программы, предназначенной для передачи текста через буфер) обмена. В этой про-
грамме используется тип документа DataFlavor. imageFlavor, а для передачи изображения через системный буфер обмена используется класс ImageSelection.
Листинг 7.17. ZmafjeTranaferTeBt. java
/**
(aversion 1.20 2001-08-11
@author Cay Horstmann‘
*/
import java.io.*;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*; import java.awt.image.*; import javax.swing.*;
/**
Пример передачи изображений через системный буфер обмена */ public class ImageTransferTest {
public static void main(String[] args) {
JFrame frame = new ImageTransferFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
} }
/**
Фрейм с копируемым изображением, а также
с кнопками копирования и вставки изображения */
class ImageTrans ferFrame extends JFrame {
public ImageTransferFrame() {
setTitle("ImageTransferTest"); setsize(WIDTH, HEIGHT);
Container contentPane = getContentPane();
label = new JLabel();
image = makeMandelbrot(WIDTH, HEIGHT);
label.seticon(new Imageicon(image));
contentPane.add(new JScrollPane(label), BorderLayout.CENTER);
JPanel panel = new JPanel();
JButton copyButton = new JButton("Copy");
pane1.add(copyButton) ;
copyButton.addActionListener(new ActionListener()
{ public void actionPerformed(ActionEvent event) (
Сору();
));
JButton pasteButton = new JButton("Paste");
panel.add(pasteButton);
pasteButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
paste();
}
D ;
contentPane.add(panel, BorderLayout.SOUTH);
/**
Копирование текущего изображения в системный буфер обмена
*/ private void copy() {
Clipboard clipboard =
Imageselection selection = new Imageselection(image); clipboard.setcontents(selection, null);
1
/**
Вставка текущего изображения из системного буфера обмена */
private void paste() {
Clipboard clipboard =
Transferable contents = clipboard.getContents(null); if (contents == null) return;
DataFlavor flavor = DataFlavor.imageFlavor; if (contents.isDataFlavorSupported(flavor)) {
try {
image = (Image)(contents.getTransferData(flavor)); label.setIcon(new Imageicon(image));
}
catch(UnsupportedFlavorException exception) {
JOptionPane.showMessageDialog(this, exception);JOptionPane.showMessageDialog(this, exception);JOptionPane.showMessageDialog(this, exception); } catch(lOException exception) { )
JOptionPane.showMessageDialog(this, exception);
JOptionPane.showMessageDialog(this, exception);
/**
Создание изображения для
фрактального множества Мандельброта
@param width the width
@parah height the height
@return the image
*/
public Bufferedlmage makeMandelbrot(int width, int height) {
Bufferedlmage image = new Bufferedlmage(width, height, BufferedImage.TYPE_INT_ARGB);
WritabieRaster raster = image.getRaster();
ColorModel model = image.getColorModel();
Color fractalColor = Color.red;
int argb = fractalcolor.getRGB();
Object colorData = model.getDataElements(argb, null);
for (int i = 0; i < width; i++)
for (int j =0; j < height; j++) {
double a = XMIN + i * (XMAX -/ width;
double b = YMIN + j * (YMAX -/ height;
if (!escapesTolnfinity(a, b))
raster.setDataElements(i, j, colorData);
}
return image;
}
private boolean escapesTolnfinity(double a, double b)
{
double x = 0.0;
double у = 0.0;
int iterations = 0;
do {
double xnew = x * x — у * у + a;
double ynew =2 * x * у + b;
x = xnew;
у = ynew;
iterations++;
if (iterations == MAX_ITERATIONS) return false;
}
while (x <= 2 && у <= 2) ;
return true;
}
private JLabel label;
private Image image;
private static final double XMIN = —2;
private static final double XMAX = 2;
private static final double YMIN = -2;
private static final double YMAX = 2;
private static final int MAX-ITERATIONS = 16;
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
/**
Класс-оболочка для передачи
данных из объектов-изображений
*/
class ImageSelection implements Transferable {
/*,
Выбор изображения
Sparam image an image
*/
public Imageselection(Image image)
{
} thelmage = image;
public DataFlavor[] getTransferDataFlavors()
{
return new DataFlavor[] { DataFlavor.imageFlavor };
}
public boolean isDataFlavorSupported(DataFlavor flavor)
{
return flavor.equals(DataFlavor.imageFlavor);
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException
{
if(flavor.equals(DataFlavor.imageFlavor))
{
return thelmage;
}
else
{
throw new UnsupportedFlavorException(flavor) ;
)
private Image thelmage;
}
Использование локального буфера обмена для передачи ссылок на объекты
Предположим, что нужно передать данные такого типа, передача которого не поддерживается системным буфером обмена. Например, в графическом редакторе может возникнуть необходимость предоставления пользователю возможности копировать и вставлять произвольные фигуры.
В примере программы, код которой приведен в листинге 7.18, демонстрируется способ использования локального буфера обмена для передачи ссылок на объекты. Как показано на рис. 7.49, эта программа содержит две панели. В левой панели имеются четыре прямоугольных маркера, перемещения которых изменяют внешний вид кривой третьего порядка. Правая панель предназначена для вставки и отображения такой кривой.
После щелчка на кнопке Copy кривая третьего порядка из левой панели копируется в буфер обмена, а после щелчка на кнопке Paste кривая, которая хранится в буфере обмена, появится в правой панели. Наглядно этот результат можно наблюдать, копируя кривую из левой панели в буфер обмена, затем изменяя ее на экране и вставляя предыдущее изображение из буфера обмена в правую панель. В результате этих дей-
Рис 7.4 передача объектов через локальный буфер обмена <Рис 7.4 передача объектов через локальный буфер обмена <ствий в правой панели появится сохраненная прежде кривая третьего порядка.
Рис 7.4 передача объектов через локальный буфер обмена <
Рис 7.4 передача объектов через локальный буфер обмена <
Для передачи ссылок на произвольные Java- объекты в рамках одной виртуальной машины используется следующий MIME-тип.
application/x-java-jvm-local- objectref;с1азв=имя_класса
Однако, в SDK не предусмотрен объект-оболочка для создания объекта типа Transferableдля MIME- типа. В этом примере приводится код такого объекта- упаковщика, который полностью аналогичен коду объекта-упаковщика ImageSelection из предыдущего раздела.
Ссылка на объект имеет значение только внутри одной виртуальной машины. Поэтому, нельзя копировать
такой объект в системный буфер обмена. Вместо этого программа использует локальный буфер обмена, конструктор которого содержит имя буфера обмена.
Clipboard clipboard = new Clipboard("local");
Однако, использование локального буфера обмена имеет один существенный недостаток. Нужно синхронизировать использование локального и системного буфера обмена таким образом, чтобы пользователь не путался при работе с ними. Во время создания этой книги такая автоматическая синхронизация в платформе Java еще не поддерживалась.
Листинг 7.18.LocalTranaferTest.java
©version 1.00 2001-08-11
@author Cay Horstmann
import java.awt.*;
import java.awt.datatransfer.*;
import j ava.awt.event.*;
import j
import java.io.*;
import java.util.*;
import javax.swing.*;
/**
Пример передачи ссылок на объекты в рамках одной виртуальной машины */
public static void main(String[] args)public static void main(String[] args)public class LocalTransferTest {
public static void main(String[] args)
public static void main(String[] args)
JFrame frame = new LocalTransf erFrame () ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
/**
Фрейм с левой панелью редактирования кривой третьего порядка и правой панелью для отображения произвольных фигур, а также с кнопками копирования и вставки
*/
class LocalTransferFrame extends JFrame {
public LocalTransferFrame()
{
setTitle("LocalTransferTest") ;
setsize(WIDTH, HEIGHT);
Container contentPane = getContentPane();
curvePanel = new CubicCurvePanel();
curvePanel.setPreferredSize(new Dimension(WIDTH / 2, HEIGHT));
shapePanel = new ShapePanel();
contentpane. add (new JSplitPane (JSplitPane. HORIZONTAL_SPLIT, curvePanel, shapePanel), BorderLayout.CENTER);
JPanel panel = new JPanel();
JButton copyButton = new JButton("Copy");
panel.add(copyButton);
copyButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
copy();
) });
JButton pasteButton = new JButton("Paste") ;
panel.add(pasteButton);
pasteButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
paste();
}
}) ;
contentPane.add(panel, BorderLayout.SOUTH);
}
/**
Копирование текущей кривой третьего
порядка в локальный буфер обмена
*/
private void copy()
LocalSelection selection = new Localselection( curvePanel.getShape());
clipboard.setcontents(selection, null);
/**
Вставка фигуры в правую панель
из локального буфера обмена
*/
private void paste()
{
Transferable contents = clipboard.getContents(null); if (contents == null) return;
try
{
DataFlavor flavor = new DataFlavor(
"application/x-java-jvm-local-objectref;
class=java.awt.Shape");
if (contents.isDataFlavorSupported(flavor)) shapePanel.setshape(
(Shape)contents.getTransferData(flavor));
}
catch(ClassNotFoundException exception)
{
JOptionPane.showMessageDialog(this, exception);
}
catch(UnsupportedFlavorException exception)
{
JOptionPane.showMessageDialog(this, exception);
}
catch(TOException exception)
( JOptionPane.showMessageDialog(this, exception);
)
private CubicCurvePanel curvePanel;
private ShapePanel shapePanel;
private Clipboard clipboard = new Clipboard!"local");
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
/**
Панель для рисования кривой третьего порядка с возможностями перемещения ее маркеров
*/
class CubicCurvePanel extends JPanel {
public CubicCurvePanel() {
addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent event) {
for (int i = 0; i < p.length; i++)
double х = p[i].getX() - SIZE / 2;
double у = p[i].getY() - SIZE / 2;
Rectangle2D r
= new Rectangle2D.Double(x, y, SIZE, SIZE), if (r.contains(event.getPoint()))
{
current = 1; return;
public void mouseReleased(MouseEvent event) {
current = —1 ;
} )) ;
addMouseMotionListener(new
MouseMotionAdapter()
{
public void mouseDragged(MouseEvent event) {
if (current == —1) return;
p[current] = repaint();
} });
current = —1 ;
)
public void paintComponent(Graphics g)
{
super.paintComponent(g) ;
Graphics2D g2 = (Graphics2D)g;
for (int i = 0; i < p.length; i++) {
double x = p[i].getX() - SIZE / 2;
double у = p[i].getY() - SIZE / 2;
g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE))
}
g2.draw(getShape());
}
/**
Возвращение текущей кривой третьего порядка
©return the curve shape
*/
public Shape getShape()
{
return new CubicCurve2D.Double(p[OJ.getX(), p[0].getY() p[l].getx(), p[l].getY(), p[2].getX(), p[2].getY(), p[3].getX(), p[3] . getYO ) ;
}
private Point2D[] p=
{
new Point2D.Double(10, 10),
new Point2D.Double(10, 100), new Point2D.Double(100, 10), new Point2D.Double(100, 200) } ;
private static int SIZE = 10; private int current;
/**
Панель для отображения произвольных фигур
*/
class ShapePanel extends JPanel {
/**
Указание фигуры, которая отображается в панели ©param aShape any shape
*/
public void setshape(Shape aShape) {
shape = aShape;
repaint();
public void paintcomponent(Graphics g)
{*
super.paintcomponent(g) ;
Graphics2D g2 = (Graphics2D)g;
if (shape != null) g2.draw(shape); }
private Shape shape;
}
/**
Класс-оболочка для передачи данных
в рамках одной виртуальной машины
*/
class Localselection implements Transferable {
/**
Выбор объекта.
©param о any object
*/
Localselection(Object о)
{ obj = о;
}
public DataFlavor[] getTransferDataFlavors() {
DataFlavor[] flavors = new DataFlavor[1];
Class type = obj.getclass(); String mimeType =
+ type.getName() ;
try {
flavors[0] = new DataFlavor(mimeType);
return flavors;
catch (ClassNotFoundException exception)
{
return new DataFlavor[0];
}
}
public boolean isDataFlavorSupported(DataFlavor flavor)
{
return "application".equals(flavor.getPrirnaryType())
&& "x-java-jvm-local-objectref",equals(
flavor.getSubType())
&& flavor.getRepresentationClass().isAssignableFrom( obj.getClass());
}
public Object getTransferData(DataFlavor flavor)
throws UnsupportedFlavorException
{
if (! isDataFlavorSupported(flavor))
throw new' UnsupportedFlavorException(flavor);
} return obj ;
} private Object obj ;
[a^ava.awt.datatransfer.Clipboard
• Конструктор Clipboard (String name) создает локальный буфер обмена с данным именем.
Передача объектов Java
через системный буфер обмена
Предположим, что нужно скопировать и вставить объект из одного Java- приложения в другое. Как известно, для этого нельзя использовать локальный буфер обмена, но можно применить системный буфер обмена, помещая в него сериализуемые (senliazed) объекты.
Эта возможность продемонстрирована в примере программы, код которой приводится в листинге 7.19. Данная программа содержит палитру цветов, в которой можно выбрать какой-то цвет и с помощью кнопки Copy скопировать его в системный буфер обмена как сериализуемый объект типа Color. Кнопка Paste проверяет, содержит ли системный буфер объект типа Color, и если содержит, то из него извлекается цвет, который устанавливается текущим в палитре цветов.
Сериализуемые объекты можно передавать между двумя Java-приложениями, как показано на рис. 7.50. Запустите два экземпляра программы SerialTrans erTest, затем щелкните на кнопке Copy в первой программе и на кнопке Paste во второй. После этого объект Color будет передан из одной виртуальной машины в другую.
Рис. 7.50, Копирование данных между двумя экземплярами Java-приложений
Для передачи данных в платформе Java предусмотрено помещение бинарных данных в виде сериализуемого объекта в системный буфер обмена. Другая Java-программа (не обязательного такого же типа, как и программа, которая генерирует данные для буфера обмена) может извлекать данные и десериализировать объект.
Конечно, другие приложения, созданные на других языках (а не на языке Java), не будут знать, что делать с такими данными. Поэтому, программа также передает данные в виде текста, т.е. с использованием другого типа документа. Этот текст является результатом выполнения метода toString по отношению к передаваемому объекту. Для демонстрации возможности использования второго типа документа запустите программу, выберите цвет, а потом щелкните на кнопке Paste в текстовом редакторе. В результате этих действий в окне текстового редактора появится следующая строка.
java.awt.Color[r=255,g=51,b-51]
В действительности, для передачи сериализуемых объектов не требуется создавать дополнительный код, потому что вместо этого используется MIME-тип.
application/x-java-serial!zed-object;с1а8в=имя_класса
Как и в предыдущем примере, нужно самостоятельно создать собственный объект- оболочку передаваемых данных, код которого приводится в листинге 7.19.
Листинг 7.19.SerialTransferTeat.java'
@version 1.00 2001-08-11
©author Cay Horstmann
*/
import java.io.* ;
import java.awt. *;
import java.awt.datatransfer. *;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
/**
Пример передачи сериализуемых объектов
между разными виртуальными машинами
*/
public class SerialTransferTest
{
public static void main(String!] args) {
JFrame frame = new SerialTransferFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
} }
/**
Фрейм с диалоговым окном выбора цветаi
а также с кнопками копирования и вставки
*/
class SerialTransferFrame extends JFrame {
public SerialTransferFrame() {
setTitle("SerialTransferTest") ;
Container contentPane = getContentPane();
chooser = new JColorChooser();
contentPane.add(chooser, BorderLayout.CENTER);
JPanel panel = new JPanel();
JButton copyButton = new JButton("Copy");
panel.add(copyButton);
copyButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
copy();
) ));
JButton pasteButton = new JButton("Paste");
panel.add(pasteButton);
pasteButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
paste();
) });
contentPane.add(panel, BorderLayout.SOUTH); pack();
/**
Копирование выбранного цвета в системный буфер обмена
*/
private void copy()
{
Clipboard clipboard =
Color color = chooser.getColor();
SerialSelection selection = new SerialSelection(color); clipboard.setcontents(selection, null);
/**
Вставка цвета из системного буфера обмена в диалоговое окно выбора цвета
*/
private void paste()
{
Clipboard clipboard =
Transferable contents = clipboard.getContents(null); if (contents == null) return;
try
{
DataFlavor flavor = new DataFlavor( "application/x-java-serial!zed-object; class=java.awt.Color");
if (contents.isDataFlavorSupported(flavor)) {
Color color
=
chooser.setColor(color);
catch(ClassNotFoundException exception)
JOptionPane.showMessageDialog(this, exception); catch(UnsupportedFlavorException exception)
JOptionPane.showMessageDialog(this, exception); catch(lOException exception)
JOptionPane.showMessageDialog(this, exception);
} private JColorChooser chooser;
/**
Класс-оболочка для передачи данных
в виде сериализуемых объектов
*/
class SerialSelection implements Transferable
/**
Выбор объекта.
@param о any serializable object
*/
SerialSelection(Serializable о) { } obj =
public DataFlavor[] getTransferDataFlavors() {
DataFlavor[] flavors = new DataFlavor[2] ;
Class type = obj.getClass(); String mimeType =
+ type.getName();
try {
flavors[0] = new DataFlavor(mimeType);
flavors[1] = return flavors;
}
catch (ClassNotFoundException exception) {
return new DataFlavor[0] ;
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
return
DataFlavor.stringFlavor.equals(flavor)
|| "application".equals(flavor.getPrimaryType())
&& "x-java-serial!zed-object".equals( flavor.getSubType())
&& flavor.getRepresentationClass().isAssignableFrom( obj.getClass()) ;
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException
{
if (!isDataFlavorSupported(flavor))
throw new UnsupportedFlavorException(flavor)'
if (DataFlavor.stringFlavor.equals(flavor)) return obj.toString();
return obj ;
}
private Serializable obj ;
Перетаскивание
При использовании механизма копирования и вставки буфер обмена выполняетПри использовании механизма копирования и вставки буфер обмена выполняетроль промежуточного звена. Технология перетащить и отпустить {drag and drop) по-роль промежуточного звена. Технология перетащить и отпустить {drag and drop) по-зволяет удалить это звено и непосредственно взаимодействовать двум программам. Взволяет удалить это звено и непосредственно взаимодействовать двум программам. Вплатформе Java 2 поддерживаются базовые возможности перетаскивания объектов между приложениями Java и другими платформозависимыми приложениями. В настоящем разделе описываются способы создания Java-приложений, которые являются источниками и приемниками перетаскиваемых объектов.Прежде, чем углубиться в изучение используемого в платформе Java механизма перетаскивания, кратко рассмотрим пользовательский интерфейс перетаскивания на примере программ Windows Explorer и WordPad. На другой платформе можно поэкспериментировать с другими доступными там программа-платформе Java 2 поддерживаются базовые возможности перетаскивания объектов между приложениями Java и другими платформозависимыми приложениями. В настоящем разделе описываются способы создания Java-приложений, которые являются источниками и приемниками перетаскиваемых объектов.Прежде, чем углубиться в изучение используемого в платформе Java механизма перетаскивания, кратко рассмотрим пользовательский интерфейс перетаскивания на примере программ Windows Explorer и WordPad. На другой платформе можно поэкспериментировать с другими доступными там программа-itodo.M todo.txt.bak v2ch1Jm v2ch1 -2.fm v2ch1-3.fm v2ch1 -4.fm v2ch2.fmУ v2ch2fm.lckitodo.M todo.txt.bak v2ch1Jm v2ch1 -2.fm v2ch1-3.fm v2ch1 -4.fm v2ch2.fmУ v2ch2fm.lckВ,v2ch2-4.fm |5v2ch3.fm iQv2ch3-2.fmВ v2ch4.fmВ,v2ch2-4.fm |5v2ch3.fm iQv2ch3-2.fmВ v2ch4.fmВ v2ch4-2 tmВ v2ch5.lmВ v2ch4-2 tmВ v2ch5.lmБКВ7KB2.350KB2.357KB3.445KB3.392KB1571 KB1KB1.605KB,-••4.839KB-1 ^^|-4283KB2.401KBБКВ7KB2.350KB2.357KB3.445KB3.392KB1571 KB1KB1.605KB,-••4.839KB-1 ^^|-4283KB2.401KBTextDocumentBAK FileAdobe FrameMakei...Adobe FrameMaket ...Adobe FfameMaket ...Adobe FrameMakei..AdobeFrameMaker...LCK FileAdobe FrameMakei...Adobe FiameMakei...Adobe FrameMakei...Adobe FiameMakei...AdobeFrameMaker...Adobe FiameMakei...TextDocumentBAK FileAdobe FrameMakei...Adobe FrameMaket ...Adobe FfameMaket ...Adobe FrameMakei..AdobeFrameMaker...LCK FileAdobe FrameMakei...Adobe FiameMakei...Adobe FrameMakei...Adobe FiameMakei...AdobeFrameMaker...Adobe FiameMakei...2.559KB AdobeFrameMaket...2.117KB AdobeFrameMaket...2.559KB AdobeFrameMaket...2.117KB AdobeFrameMaket...
При использовании механизма копирования и вставки буфер обмена выполняет
При использовании механизма копирования и вставки буфер обмена выполняет
роль промежуточного звена. Технология перетащить и отпустить {drag and drop) по-
роль промежуточного звена. Технология перетащить и отпустить {drag and drop) по-
зволяет удалить это звено и непосредственно взаимодействовать двум программам. В
зволяет удалить это звено и непосредственно взаимодействовать двум программам. В
платформе Java 2 поддерживаются базовые возможности перетаскивания объектов между приложениями Java и другими платформозависимыми приложениями. В настоящем разделе описываются способы создания Java-приложений, которые являются источниками и приемниками перетаскиваемых объектов.
Прежде, чем углубиться в изучение используемого в платформе Java механизма перетаскивания, кратко рассмотрим пользовательский интерфейс перетаскивания на примере программ Windows Explorer и WordPad. На другой платформе можно поэкспериментировать с другими доступными там программа-
платформе Java 2 поддерживаются базовые возможности перетаскивания объектов между приложениями Java и другими платформозависимыми приложениями. В настоящем разделе описываются способы создания Java-приложений, которые являются источниками и приемниками перетаскиваемых объектов.
Прежде, чем углубиться в изучение используемого в платформе Java механизма перетаскивания, кратко рассмотрим пользовательский интерфейс перетаскивания на примере программ Windows Explorer и WordPad. На другой платформе можно поэкспериментировать с другими доступными там программа-
itodo.M todo.txt.bak v2ch1Jm v2ch1 -2.fm v2ch1-3.fm v2ch1 -4.fm v2ch2.fm
У v2ch2fm.lck
itodo.M todo.txt.bak v2ch1Jm v2ch1 -2.fm v2ch1-3.fm v2ch1 -4.fm v2ch2.fm
У v2ch2fm.lck
В,v2ch2-4.fm |5v2ch3.fm iQv2ch3-2.fm
В v2ch4.fm
В,v2ch2-4.fm |5v2ch3.fm iQv2ch3-2.fm
В v2ch4.fm
В v2ch4-2 tm
В v2ch5.lm
В v2ch4-2 tm
В v2ch5.lm
БКВ
7KB
2.350KB
2.357KB
3.445KB
3.392KB
1571 KB
1KB
1.605KB
,-••4.839KB
-1 ^^|-
4283KB
2.401KB
БКВ
7KB
2.350KB
2.357KB
3.445KB
3.392KB
1571 KB
1KB
1.605KB
,-••4.839KB
-1 ^^|-
4283KB
2.401KB
TextDocument
BAK File
Adobe FrameMakei...
Adobe FrameMaket ...
Adobe FfameMaket ...
Adobe FrameMakei..
AdobeFrameMaker...
LCK File
Adobe FrameMakei...
Adobe FiameMakei...
Adobe FrameMakei...
Adobe FiameMakei...
AdobeFrameMaker...
Adobe FiameMakei...
TextDocument
BAK File
Adobe FrameMakei...
Adobe FrameMaket ...
Adobe FfameMaket ...
Adobe FrameMakei..
AdobeFrameMaker...
LCK File
Adobe FrameMakei...
Adobe FiameMakei...
Adobe FrameMakei...
Adobe FiameMakei...
AdobeFrameMaker...
Adobe FiameMakei...
2.559KB AdobeFrameMaket...
2.117KB AdobeFrameMaket...
2.559KB AdobeFrameMaket...
2.117KB AdobeFrameMaket...
Рис. 7.51. Пример ОПераЦИИ ПеретаскиванияРис. 7.51. Пример ОПераЦИИ Перетаскиваниями, в которых имеются средства перетаскивания.
Рис. 7.51. Пример ОПераЦИИ Перетаскивания
Рис. 7.51. Пример ОПераЦИИ Перетаскивания
Операция перетаскивания {drag operation) обычно заключается в выборе одного или нескольких элемен-
тов источника перетаскивания {drag source) и переносе их с помощью мыши в другое место, т.е. приемник перетаскивания {drag target). Она обозначается особой формой указателя мыши и контурами перетаскиваемых объектов внутри источника перетаскивания, как показано на рис. 7.51.
При отпускании кнопки мыши над местом назначения приемник запрашивает у источника информацию о перетаскиваемом элементе и выполняет определенную операцию. Например, если перетащить пиктограмму текстового файла из программы Windows Explorer в программу Notepad, то в программе Notepad будет открыт этот файл. Если же пиктограмму данного файла поместить над папкой в программе Windows Explorer, то этот файл будет просто перемещен в эту директорию.
Если при перетаскивании файла удерживать нажатой клавишу <Shift>, то будет выполняться перемещение файла, а если удерживать нажатой клавишу <Ctrl>, то будет выполняться копирование файла. Если держать нажатыми одновременно обе клавиши <Shift> и <Ctrl>, то в папку будет помещен ярлык данного файла. (На других платформах могут использоваться другие комбинации для выполнения этих операций.)
Итак, основными типами операции перетаскивания являются:
перемещение;
копирование;
создание ярлыка.
Целью операции создания ярлыка является установка ссылки на перетаскиваемый элемент. Такие ссылки обычно требуют поддержки со стороны операционной системы (например, ссылки на файлы или объектные ссылки на компоненты документа), и их использование обычно не имеет смысла в платформонезависимых программах. В этом разделе рассматривается использование операции перетаскивания для копирования и перемещения объектов.
Не все программы различают упомянутые выше типы операций перетаскивания. Например, если перетаскивать файл в WordPad, то он игнорирует тип операции и всегда открывает перетаскиваемый файл.
Для обозначения операций перетаскивания часто используются специальные визуальные эффекты. Самым простым визуальным эффектом является изменение формы указателя мыши. При перемещении указателя мыши над потенциальным приемником перетаскивания его внешний вид содержит условное обозначение допустимой операции перетаскивания. На рис. 7.52 показаны различные формы курсоров при выполнении разных операций перетаскивания объектов над приемником перетаскивания.
8 v2ch2 ^8 v2ch2 ^8v2^fm8v2^fmHvaHvaПеретаскиваниене допускаетсяПеретаскиваниене допускаетсяКопированиеКопированиеПеремещениеПеремещениеСозданиеярлыкаСозданиеярлыка
8 v2ch2 ^
8 v2ch2 ^
8v2^fm
8v2^fm
Hva
Hva
Перетаскиваниене допускается
Перетаскиваниене допускается
Копирование
Копирование
Перемещение
Перемещение
Созданиеярлыка
Созданиеярлыка
Рис. 7.52. Внешний вид курсора для обозначения допустимой операции перетаскивания при перемещении указателя мыши над приемником перетаскивания
Кроме пиктограмм файлов можно также перетаскивать другие элементы. Например, можно выделить текст в редакторе WordPad и перетащить его. Попробуйте перетащить фрагмент текста в разные виды приемников перетаскивания и посмотрите на полученный результат.
Этот эксперимент демонстрирует недостаток использования механизма перетаскива- ниявпользовательскоминтерфейсе.Пользователюпоройтруднопредусмотреть,чтои куда можно перетащить и что при этом получится. Операция перемещения файла, которая выполняется по умолчанию, можетудалить его оригинал, поэтому многие пользователи не любят экспериментировать с операциями перетаскивания.
Приемники перетаскивания
В этом разделе создадим простое приложение Java, которое является приемником операции перетаскивания. Данный пример демонстрирует способы определения намерений пользователя перетащить объект, а также способы получения и анализа перетаскиваемых данных.
В качестве приемника перетаскивания можно назначить любой компонент AWT, для чего нужно создать объект DropTarget и передать конструктору этот компонент и слушатель событий перетаскивания. Конструктор регистрирует данный объект в системе поддержки операций перетаскивания.
DropTarget target = new DropTarget(component, listener);
Активизировать и деактивизировать приемник можно с помощью метода setActive, а по умолчанию приемник считается активным.
target.setActive(b);'
Для указания применяемых по умолчанию операций перетаскивания можно использовать метод setDefault Act ion. По умолчанию разрешены все перечисленные ниже операции.
DndConstants. ACTION_COPY
DndConstants.ACTION_MOVE
DndConstants.ACTION_COPY_OR_MOVE
DndConstants.ACTION_LINK
Далее нужно реализовать слушатель событий перетаскивания на основе интерфейса DropTargetListener, который содержит объявления следующих методов.
void dragEnter(DropTargetDragEvent event)
void dragExit(DropTargetEvent event)
void dragOver(DropTargetDragEvent event)
void dropAct ionChanged(DropTargetDragEvent event)
void drop(DropTargetDropEvent event)
Методы dragEnter и dragExit вызываются, когда указатель мыши появляется над приемником или покидает его, соответственно. В методе dragEnter предусмотрена возможность установки другой формы указателя мыши для обозначения возможности приемника принять данный объект.
Обычно метод dragExit используется только при создании сложных механизмов организации визуальной обратной связи с пользователем.
Метод dragOver вызывается непрерывно при перемещении пользователем мыши над приемником. Он может использоваться для создания более сложных механизмов организации визуальной обратной связи с пользователем, которые характеризуют тип операции перетаскивания. Например, если приемник имеет древовидную структуру, то можно подсветить или даже раскрыть узел, который находится под указателем мыши.
Метод dropAc tionChanged вызывается, когда пользователь изменяет тип операции перетаскивания. Например, этот метод будет вызван, если пользователь нажмет клавишу <Ctrl> при перемещении указателя мыши над приемником, что дает возможность изменить визуальные эффекты и приспособить их к соответствующему типу операции.
Наиболее важным методом является метод drop, который вызывается при опускании объекта (обычно при отпускании кнопки мыши). Учтите, что этот метод вызывается независимо от того, допустимо ли перетаскивание или нет.
Рассмотрим еще раз внимательно параметры методов интерфейса DropTargetListener. Три из них принимают параметр типа DropTargetDragEvent, метод drop принимает параметр типа DropTargetDropEvent, а метод dragExit— параметр типа DropTargetEvent. Класс DropTargetEvent является суперклассом двух других классов событий и имеет всего один метод getDropTargetContext, который возвращает класс с несколькими другими открытыми методами, которые здесь не рассматриваются. Предназначением метода dragExit является очистка, поэтому его параметр не представляет большого интереса.
Классы DropTargetDragEvent и DropTargetDropEvent содержат следующие методы.
int getDropAction()
Point getLocation()
DataFlavor[] getCurrentDataFlavors()
boolean IsDataFlavorSupported(DataFlavor flavor)
Эти методы нужны для проверки возможности перетаскивания или опускания объекта, а также для указания визуальных эффектов.
К сожалению, поскольку эти методы не доступны в общем суперклассе, то придется создать код проверки дважды, сначала для проверки возможности перетаскивания, а затем для проверки возможности опускания.
Если перетаскивание недопустимо, то нужно вызвать метод rejectDrag класса DropTargetDragEvent в методе dragEnter или методе dropActionChanged. В результате, форма указателя мыши изменится и примет вид пиктограммы предупреждения. Если пользователь опустит элемент, несмотря на это предупреждение, то в таком случае следует вызвать метод rejectDrop класса DropTargetDropEvent.
Метод getDropAction возвращает тип операции перетаскивания, которую пользователь собирается выполнить. Однако, далеко не всегда необходимо воспринимать операции перетаскивания буквально. Например, при перемещении пиктограммы текстового файла в программу Notepad не нужно удалять исходный файл. В этом случае приемник перетаскивания должен самостоятельно определить фактический тип выполняемой операции, т.е. перемещение или копирование. Для этого в коде метода drop следует вызвать метод acceptDrop класса DropTargetDropEvent с фактической операцией перетаскивания. При использовании показанного ниже метода acceptDrop источник перетаскивания удалит перетаскиваемые объекты.
event.acceptDrop(DnDConstants.ACTIONJMOVE);
// Источник перетаскивания удаляет перетаскиваемые объекты!
Всегда ли можно гарантировать, что пользователи приложения осознают разницу между копированием и перемещением? Понимают ли они, что операция перетаскивания по умолчанию удаляет оригинал перетаскиваемого элемента? Если в этом нет полной уверенности, то при перетаскивании рекомендуется выполнять только копирование с помощью следующего оператора.
event.acceptDrop(DnDConstants.ACTIONS_COPY);
С другой стороны, если действительно нужно различать операции перемещения и копирования, то необходимо использовать следующий вызов.
event.acceptDrop(event.getDropEvent());
Вот как выглядит код типичного слушателя событий приемника перетаскивания.
class ADropTargetListener implements DropTargetListener {
// проверочные методы
public boolean isDragAcceptable(DropTargetDragEvent event)
{
проверка ВОЗМОЖНОСТИ перемещения и доступных типов документа
}
public boolean isDropAcceptable(DropTargetDropEvent event) {
та же проверка, что И в методе isDragAcceptable
}
/ / методы слушателя событий
public void dragEnter(DropTargetDragEvent event)
{
if ( !isDragAcceptable(event))
event.rejectDrag(); return;
)
}
public void dragExit(DropTargetEvent event) }{
••p{ublic void dragOver (DropTargetDragEvent event) // здесь можно вставить визуальные эффекты
•
•
}
public void dropActionChanged(DropTargetDragEvent event)
{
if(IisDragAcceptable(event))
{
event.rejectDrag() ;
return;
}
}
public void drop(DropTargetDropEvent event)
{
if(!isDragAcceptable(event))
{
event.rejectDrop();
return;
}
event.acceptorop(фактическое действие);
обработка данных из источника перетаскивания
event.dropComplete(true);
}
)
После выполнения операции перетаскивания нужно проанализировать данные из источника. Для этого метод getTransferable класса DropTargetDropEvent возвращает ссылку на перетаскиваемый объект типа Transferable. Это тот же интерфейс, который используется для копирования и вставки.
Тип документа DataFlavor. javaFileListFlavorдля списка файлов чаще всего используется для механизма перетаскивания, а не для копирования и вставки с помощью буфера обмена. Такой список файлов описывает набор пиктограмм файлов, переданных приемнику во время перетаскивания. Метод getTrans erData перетаскиваемого объекта возвращает объект, который приводится к типу-списку java.util . List, элементами которого являются объекты-файлы File. Вот как выглядит фрагмент кода, предназначенный для извлечения этих файлов.
DataFlavor[] flavor = transferable.getTransferDataFlavor();
DataFlavor flavor = flavors[i] ,-
if (flavor.equals(DataFlavor.javaFileListFlavor)) {
java.util.List.fileList
=
Iterator iterator = fileList.iterator();
while (iterator.hasNext())
{
File f = (File)iterator.nextО;
какие-то действия с f
}
Другим распространенным перетаскиваемым типом документа является текстовый тип документа DataFlavor.stringFlavor. Наряду с ним также широко используются другие текстовые типы документов с MIME-типом text /plain, а также разными классами представления, например, входным потоком Inputstream и байтовым массивом.
В ранних версиях платформы Java перетаскиваемый текст не преобразовывался в формат Unicode с передачей типа документа stringFlavor. Вместо этого нужно было получать текст с помощью объекта типа Inputstream. MIME-тип text/plain содержит параметр charset, который обозначает кодировку символов. Для того, чтобы устранить такое неудобство, в этих версиях платформы Java также используют имена кодировок символов, которые отличаются от тех имен, которые используются в конструкторе InputStreamReader. Например, приходилось преобразовывать ascii В ISO-8859 и Unicode в Unicode.
При работе в операционной системе Windows возникают дополнительные проблемы. Окончание потока ввода обозначается нулевым символом, и не следует продолжать считывание данных, которые расположены вслед за ним. Кроме того, данные в формате Unicode не начинаются с байтового маркера Unicode, который нужен конструктору InputStreamReader, что приводит к генерации исключительной ситуации sun. io.MalformedlnputException. Таким образом, программистам приходится игнорировать конструктор InputStreamReader и воссоздавать СИМволы Unicode из пар байтов.
К счастью, в SDK 1.4 все эти проблемы решены.
В зависимости от источника перетаскивания можно получить данные, которые будут иметь другой MIME-тип, например, один из показанных ниже.
text/html text/rtf
Для чтения данных в этом формате нужно выбрать подходящий тип документа (например, входной поток) и получить эти данные.
if (flavor.isMirneTypeEqual("text/html")
&& flavor.getRepresentationClass() ==
{
String charset = flavor.getParameter("charset");
InputStreamReader in = new InputStreamReader(
transferable.getTransferData(flavor), charset);
чтение данных из входного потока in
}
'B примере программы, код которой приведен в листинге 7.20, демонстрируется способ перетаскивания элемента в текстовое поле. В начале процесса перетаскивания над текстовой областью отображается тип операции перетаскивания. Апри опускании перетаскиваемого объекта отображаются перетаскиваемые данные. Если перетаскиваемые данные имеют текстовый формат, то программа способна определить кодировкиasciiиUnicode.
В этом примере не создаются никакие визуальные эффекты процесса перетаскивания, кроме изменения формы указателя мыши в виде предупреждающей пиктограммы, которое автоматически происходит при вызове метода re j ectDrag.
Целью программы является создание приемника перетаскивания для проверки его основных возможностей. Попробуйте перетащить в нее группу имен файлов из программы Windows Explorer или текстовый фрагмент из программы Notepad так, как показано на рис. 7.53. Обратите внимание, что здесь не предусмотрена возможность создания ярлыка. Если при перетаскивании одновременно нажать клавиши <Ctrl> и <Shift>, то появится предупреждающая пиктограмма о невозможности такой операции.
^DfopTdigetRRKld
Dropitemsintothistextarea.
*ction_move|
MIME type=appllcatlon/x-Java-flle-llst; class=Java util
D:tCatMooksXc)4v2tv2ch2.fm
Рис. 7.53. Результат работы про- граммыDropTargetTest
Листинг 7.20. DronTarcretTest.java
@version 1.01 2001-08-11
@author Cay Horstmann
*/
import java.awt.*;
import java.awt.datatransfer.* ;
import java.awt.event.*;
import java.awt.dnd.*;
import java.io. *;
import java.util.*;
import j
/**
Пример программы для проверки операций перетаскивания объектов.
Попробуйте перетащить объекты в текстовую область
для просмотра их MIME-типов
*/
public class DropTargetTest
{
public static void main(String[] args)
{
IFrame frame = new DropTargetFrame();
frame.setDefaultCloseOperation(IFrame.EXIT_ON_CLOSE);
frame.show() ;
/**
Фрейм с текстовой областью, которая играет
роль простого приемника перетаскивания
*/
class DropTargetFraine extends JFrazne {
public DropTargetFraine ()
{
setTitle("DropTargetTest");
setsize(WIDTH,HEIGHT);
Container contentPane = getContentPane();
JTextArea textArea
= new JTextArea("Drop items into this text area.\n");
new DropTarget(textArea,
new TextDropTargetListener(textArea)); contentPane. add (new JScrollPane(textArea), "Center");
)
private static final int WIDTH = 300;
private static final int HEIGHT = 300; )
/**
Слушатель событий, который отображает свойства перетаскиваемого объекта */ class TextDropTargetListener implements DropTargetListener {
/**
Создание слушателя
Sparam aTextArea the text area in which to display the properties of the dropped object.
*/
public TextDropTargetListener(JTextArea aTextArea) {
textArea = aTextArea;
)
public void dragEnter(DropTargetDragEvent event) {
inta=event.getDropAction();
if ((a&DnDConstants.ACTION_COPY) != 0) textArea.append("ACTION_COPY\n");
if ((a&DnDConstants.ACTION_MOVE) != 0) textArea.append("ACTION_MOVE\n") ;
if ((a&DnDConstants.ACTION_LINK) != 0) textArea.append("ACTION_LlNK\n");
if (!isDragAcceptable(event)) {
event.rejectDrag();
return;
}
}
public void dragExit(DropTargetEvent event)
}
public void dragOver(DropTargetDragEvent event) {
// Здесь можно вставить код визуальных эффектов
// для обратной связи с пользователем
p{ublic void dropActionChanged(DropTargetDragEvent event) i{f (!isDragAcceptable(event))
event.rejectDrag();
return;
}}
public void drop(DropTargetDropEvent event)
{{if (!isDropAcceptable(event))
{
{
event.rejectDrop();
}}return;
}
}
event.acceptDrop(DnDConstants.ACTION_CO₽Y);
Transferable transferable = event.getTransferable();
DataFlavor[] flavors
=
f{or (inti=0; i<flavors.length; i++)
DataFlavord= £lavors[i];) ;
textArea.append("MIME type=" + d.getMimeType() + "\n" try
{
if (d.equals(DataFlavor.javaFileListFlavor))
java.util.List fileList = (java.util.List) transferable.getTransferData(d);
Iterator iterator = fileList.iterator();
w{hile (iterator.hasNext())
Filef= (File)iterator.next();
textArea.append(f+"\n");
else if (d.equals(DataFlavor.stringFlavor))
String s = (String)
} transferable.getTransferData(d);
} textArea.append(s + "\n");
catch(Exception e) (
textArea.append(e+ "\n");
}
}
textArea.append("\n");
event. dropCornplete (true) ;
public boolean isDragAcceptable(DropTargetDragEvent event) {
// Обычно, в этом месте нужно проверить
// все доступные типы документов.
// Но в данной программе по умолчанию
// принимаются все типы документов
return (event.getDropAction()
& DnDConstants.ACTION_COPY_OR_MOVE) != 0;
}
public boolean isDropAcceptable(DropTargetDropEvent event) <
// Обычно, в этом месте нужно проверить
// все доступные типы документов.
// Но в данной программе по умолчанию
// принимаются все типы документов,
return (event.getDropAction()
& DnDConstants.ACTION_COPY_OR_MOVE) != 0; }
private JTextArea textArea; }
[a pTL
java.awt.dnd.DropTarget
Г//// z-v <Й-4У
A•;!
Конструктор DropTarget(Component c, DropTargetListener listener) создает приемник перетаскивания, который согласовывает с компонентомоперацииперетаскивания.
Параметры:
сприемник перетаскивания
lis tener слушатель событий перетаскивания
Метод void setActive (boolean b) активизирует и деактивизирует приемник перетаскивания.
Метод void setDefaultAct i on (int actions) устанавливает действие, выполняемое по умолчанию для данного приемника перетаскивания. Параметры:
actionsбитовая маска, составленная из констант, определенных в
классе DnDConstants: ACTION_COPY, ACTION_MOVE, ACTION_COPY_OR_MOVE или ACTION_LINK
java. awt. dnd. DropTargetListener
—■
Метод void dragEnter (DropTargetDragEvent event) вызывается, когда указатель мыши располагается над приемником перетаскивания.
Метод void dragExit (DropTargetEvent event) вызывается, когда указатель мыши покидает приемник перетаскивания.
Метод void dragOver(DropTargetDragEvent event) вызывается, когда указатель перемещается над приемником перетаскивания.
Метод void dropAct i onChanged(DropTargetDragEvent event) вызывается, когда пользователь меняет тип операции перетаскивания, в то время как указатель мыши находится над приемником перетаскивания.
Метод void drop(DropTargetDropEvent event) вызывается, когда пользователь опускает перетаскиваемый объектв приемник перетаскивания.
^ava *awt *5115 *^p^tDr^vent
i п ь • Метод int getDropAction() возвращает выбранный тип операции перетаскивания. Все возможные типы операций перетаскивания определены в классеDnDConstants.
Метод void acceptDrag(int action) должен вызываться, если приемник перетаскивания собирается выполнить операцию перетаскивания, отличную от выбранной.
Метод void rejectDrag() оповещает механизм перетаскивания, что перетаскиваемый объект отклонил попытку перетаскивания.
Метод Point getLocation() возвращает значение позиции указателя МЫШИ.
Метод DataFlavor [ ] getCurrentDataFlavors () возвращает типы документов, которые может передать источник перетаскивания.
Метод boolean isDataFlavorSuported (DataFlavor flavor) проверяет, поддерживаетли источник перетаскивания указанный тип документа.
3 ava *awt'■ Dr°PTar9etDr°PEvent
I j !L • Метод int getDropAction() возвращает выбранный в данный момент тип операции перетаскивания. Возможные типы операций перетаскивания определены в классе DnDConstants.
Метод void acceptDrop (int action) должен вызываться, если приемник перетаскивания выполнил операцию перетаскивания, отличную от выбранной.
Метод void rejectDropO оповещает механизм перетаскивания, что перетаскиваемый объект отклонил попытку перетаскивания.
Метод void dropComplete(boolean success) оповещает источник данных, что перетаскивание завершено и было ли оно успешным.
e Метод Point getLocation() возвращает значение позиции указателя МЫШИ.
Метод DataFlavor[] getCurrentDataFlavors () возвращает типы документов, которые может передать источник перетаскивания.
Метод boolean isDataFlavorSuported (DataFlavor flavor) проверяет, поддерживает ли источник перетаскивания указанный тип документа.
Источники перетаскивания
Теперь, после знакомства со способами создания программы с приемником перетаскивания, рассмотрим программу с источником перетаскивания.
Профамма, код которой приведен в листинге 7.21, заполняет компонентсписокJList именами всех файлов из текущей папки, как показано на рис. 7.54. Таким образом, этот список является источником перетаскивания и элементы данного списка можно перетащить в любой приемник перетаскивания, который может принять список файлов.
Для того, чтобы превратить компонент в источник перетаскивания, нужно получить объект DragSource. Для этого нужно вызвать метод DragSource. getDefaultDragSource. Затем вызвать метод createDefaultDragGestureRecog- nizer и передать ему следующие данные.
Компонент, который нужно сделать источником перетаскивания.
Допустимые операции перетаскивания.
Объект, который реализует интерфейсDragGestureListener.
Вот как выглядит код этих действий.
DragSource dragSource =
dragSource. createDefaultDragGestureRecognizer (component,
DnDConstantS.ACTIONS_COPY_OR_MOVE, DragGestureListener);
ISISHHHilHHHilch70agSourceTesWragSourceTest.Jpr*IthTOagSowceTesWragSourceTesthtr™|rch7lDraoSojrceTestl©ragSourceTest cImj|ph7iDragSourceTesWragSourceTest.javaeh70agSourceTesWragSourceTest.dskch7>DragSourceTesWragSourceTest.4av|ch71DragSourceTestl\DragSourceTest.*ds’ch70agSourceTesWragSourceTest.Jpr*IthTOagSowceTesWragSourceTesthtr™|rch7lDraoSojrceTestl©ragSourceTest cImj|ph7iDragSourceTesWragSourceTest.javaeh70agSourceTesWragSourceTest.dskch7>DragSourceTesWragSourceTest.4av|ch71DragSourceTestl\DragSourceTest.*ds’Интерфейс DragGestureListener имеет единственный метод dragGesture- Recognized. Анализатор операций перетаскивания вызывает этот метод, как только заметит, что пользователь начинает операцию перетаскивания. В данном методе нужно создать перетаскиваемый объект типа Transferable, который приемник перетаскивания должен в конечном счете считать с помощью метода drop. После создания перетаскиваемого объекта типа Transferable следует вызвать метод startDrag класса DragGestureEvent. Для указателя мыши можно создать новую форму или подставить значение null в первом пара
IS
IS
HHHil
HHHil
ch70agSourceTesWragSourceTest.Jpr*
IthTOagSowceTesWragSourceTesthtr™|
rch7lDraoSojrceTestl©ragSourceTest cImj|
ph7iDragSourceTesWragSourceTest.java
eh70agSourceTesWragSourceTest.dsk
ch7>DragSourceTesWragSourceTest.4av|
ch71DragSourceTestl\DragSourceTest.*ds’
ch70agSourceTesWragSourceTest.Jpr*
IthTOagSowceTesWragSourceTesthtr™|
rch7lDraoSojrceTestl©ragSourceTest cImj|
ph7iDragSourceTesWragSourceTest.java
eh70agSourceTesWragSourceTest.dsk
ch7>DragSourceTesWragSourceTest.4av|
ch71DragSourceTestl\DragSourceTest.*ds’
Рис 7.54. Результат работы про- метре, если будет использоваться предлагаемая по граммы DragSourceTestумолчанию форма указателя мыши. Для второго па
раметра необходимо указать объект типа Transferable, а для третьего — объект, который реализует интерфейс DragSourceListener.
event.startDrag(null, transferable, dragSourceListener);
Теперь нужно выполнить обычную трудоемкую работу по определению объекта- оболочки для перетаскиваемого объекта типа Transferable. Все подробности этого кода можно найти в листинге 7.21.
Слушатель событий источника перетаскивания получает сообщения о ходе операции перетаскивания и обрабатывает их с помощью следующих пяти методов интерфейса слушателя DragSourceListener.
void dragEnter(DragSourceDragEvent event) void dragExit(DragSourceDragEvent event) void dragOver(DragSourceEvent event)
void dropActionChanged(DragSourceDragEvent event) void dragDropEnd(DragSourceDropEvent event)
Первые четыре метода могут использоваться для создания визуальных эффектов при выполнении операции перетаскивания. Однако, обычно они реализуются в приемнике перетаскивания. Наиболее важным среди этих методов является метод dragDropEnd, который вызывается по окончании действия метода drop. Для выполнения операции перемещения нужно проверить, принял ли приемник перетаскиваемый объект, и обновить источник перетаскивания. (Для выполнения операции копирования ничего делать не нужно.)
В SDK1.4введен вспомогательный абстрактный классDragSourceAdapter, который реализует все методы интерфейса DragSourceListenerв виде пустых методов, которые не производят никаких действий. Этот класс используется в программе, которая приведена в конце данного раздела. При использовании более ранних версий SDK придется самостоятельно определить такие методы.
Ниже приведен код метода dragDropEnd из данной программы, который в случае успешного перемещения имен файлов удаляет их из списка.
public void dragDropEnd(DragSourceDropEvent event) {
if (event.getDropSuccess())
{
int action = event.getDropAction();
if (action == DnDConstants.ACTION_MOVE) {
for (int 1=0; i<draggedValues.length; 1++)
model.removeElement(draggedValues[1])?
}
} }
В этом методе предполагается, что метод drop сообщит о том, какая операция перетаскивания была выполнена фактически. Напомним, что метод drop может изменить тип операции перетаскивания, например, заменить перемещение на копирование, если источник перетаскивания позволяет выполнять оба эти действия. Метод event. getDropAction класса DragSourceDropEvent возвращает тип операции перетаскивания, который приемник перетаскивания использует для вызова метода acceptDrop класса DropTargetDropEvent.
Запустите программу, код которой приведен в листинге 7.21, и переместите имена файлов в различные приемники перетаскивания, например, в программу, код которой приведен в листинге 7.20, или в другую платформозависимую программу, например, Windows Explorer или Notepad.
При перетаскивании нужно следить за тем, чтобы не возникали конфликты между манипуляциями мышью, которые необходимы для перетаскивания объектов, и обычными манипуляциями мыши. Выберите один или несколько элементов, затем щелкните на одном из элементов иуведите мышьза пределы компонента-списка. Теперь операция перетаскивания опознана. После того, как указатель мыши покинет компонент-список, нажмите клавишу <Ctrl> для изменения типа операции перетаскивания.
По умолчанию перетаскивание означает перемещение. Поэтому при перетаскивании файла из компонента-списка в программу Windows Explorer этот файл будет перемещен в указанную папку-приемник.
Как видите, поддержка системного буфера обмена и механизма перетаскивания реализована пока только в очень общих чертах, и основную работу еще предстоит сделать. Будем надеяться, что в следующих версиях платформы Java эти механизмы станут более удобными и совершенными.
В этом разделе рассмотрены только основные сведения о механизме перетаскивания объектов. Для получения более подробной информации по данной теме, особенно для программирования визуальных эффектов, рекомендуется внимательно изучить книгу Core Swing: Advanced ProgrammingКима Топли(Kim Topley) [Prentice Hall 1999].
Листинг 7.21. DraaSourceTeet.java
/**
@version 1.01 2001-08-11
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.awt.event.*;
import java.io.*;
import java.util.*; import javax.swing.*;
/**
Пример источника перетаскивания, который состоит из списка файлов текущего каталога */ public class DragSourceTest {
public static void main(String[] args) {
JFrame frame = new DragSourceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
) }
/**
Фрейм со списком файлов текущего каталога
с поддержкой механизма перетаскивания
файлов в приемник перетаскивания.
Перемещаемые файлы удаляются из данного списка */ class DragSourceFrame extends JFrame {
public DragSourceFrame() {
setTitle("DragSourceTest");
setSize(WIDTH, HEIGHT);
Container contentPane = getContentPane();
File f = new File(".").getAbsoluteFile() ;
File[] files = f.listFiles();
model = new DefaultListModel();
for (inti=0; i<files.length; i++) try
{ model.addElement (files [i].getCanonicalFile());
catch (IOException exception)
JOptionPane.showMessageDialog(this, exception); }
fileList = new JList(model);
contentPane.add(newJScrollPane(fileList),
BorderLayout.CENTER);
contentPane.add(new JLabel("Drag files from this list"), BorderLayout.NORTH) ;
DragSource dragSource = DragSource.getDefaultDragSource(); dragSource.createDefaultDragGestureRecognizer(fileList, DnDConstants.ACTION_COPY_OR_MOVE, new
DragGestureListener() {
public void dragGestureRecognized( DragGestureEvent event) (
draggedValues = fileList.getSelectedValues();
Transferable transferable
=newFileListTransferable(draggedValues) ; event.startDrag(null, transferable,
new FileListDragSourceListener());
}.
)) ;
/**
Слушатель событий источника перетаскивания, который удаляет перемещенные файлы из списка файлов
*/
private class FileListDragSourceListener extends DragSourceAdapter
{
public void dragDropEnd(DragSourceDropEvent event) (
if (event.getDropSuccess()) (
int action = event.getDropAction();
if (action == DnDConstants.ACTION_MOVE)
for (int i = 0; i < draggedValues.length; i++) model.removeElement(draggedValues[ i ]);
}
}
}
private JList fileList;
private DefaultListModel model;
private Object[] draggedValues;
private static final int WIDTH = 300;
private static final int HEIGHT = 200;
class FileListTransferable implements Transferable {
public FileListTransferable(Object[] files)
{
fileList = new ArrayList(Arrays.asList(files));
}
public DataFlavor[] getTransferDataFlavors()
{
return flavors;
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
return Arrays.asList(flavors).contains(flavor);
public Object getTransferData(DataFlavor flavor)
throws UnsupportedFlavorException
{
if(flavor.equals(DataFlavor.javaFileListFlavor)) return fileList;
else if(flavor.equals(DataFlavor.stringFlavor)) return fileList.toString();
else
throw new UnsupportedFlavorException(flavor) ;
private static DataFlavor[] flavors = {
DataFlavor.javaFileListFlavor,
j DataFlavor.stringFlavor
private java.util.List fileList; }
java.awt.dnd.DragSource
Метод static DragSource getDefaultDragSource () возвращает объект DragSource для согласования операции перетаскивания с компонентом.
Метод DragGestureRecognizer createDefaultDragGestureRecog- nizer (Component component, int actions, DragGestureListener listener) создает анализатор операции перетаскивания.
Параметры:
component источник перетаскивания
actionsдопустимые операции
listener слушатель, который оповещается, когда анализатор операции
распознает операцию перетаскивания
java.awt.DragGestureLiatener
Метод void dragGestureRecognized(DragGestureEvent event) вызывается, когда анализатор операции перетаскивания распознает операцию.
B
java.awt.dnd.DragGestureEvent
• Метод voidstartDrag(Cursor dragCursor,Transferable
transferable, DragSourceListener listener) начинает onepa- циюперетаскивания.
Параметры:
dragCursorнеобязательный параметр, который задает форму указа-
transferable listenertransferable listenerтеля мыши; он может принимать значение null, и в этом случае используется предлагаемая по умолчанию форма перетаскиваемые данные
transferable listener
transferable listener
слушатель, который оповещается о ходе операции пере-
таскивания
java.awt. dnd»DragSourceListener
Метод void dragEnter(DragSourceDragEvent event) вызывается, когда указатель мыши появляется над приемником перетаскивания.
Метод void dragExit (DropSourceEvent event) вызывается, когда указатель мыши покидает приемник перетаскивания.
Метод void dragOver (DragSourceDragEvent event) вызывается, когда указатель мыши перемещается над приемником перетаскивания.
Метод void dropAct ionChanged(DragSourceDragEvent event) вызывается, когда пользователь меняет тип операции перетаскивания.
Метод void dragDropEnd(DragSourceDropEvent event) вызывается после того, как операция перетаскивания завершается или отменяется.
java.awt.dnd.DragSourceDropEvent
I Г U • Метод boolean getDropSuccess () возвращает значение true, если
приемник перетаскивания извещает об успешном завершении операции перетаскивания.
Метод int getDropAction() возвращает тип действия, которое было выполнено.
Поддержка передачиданныхв пакете Swing
Начиная с SDK 1.4, компоненты Swing имеют встроенную поддержку передачи данных, которая освобождает программиста от большинства проблем, связанных с реализацией механизмов копирования и вставки, а также механизма перетаскивания.
Например, запустите программу TableSelectionTest из главы 6, "Более сложные компоненты пакета Swing", и выделите диапазон ячеек. Затем нажмите комбинацию клавиш <Ctrl+C> и вставьте содержимое буфера обмена в текстовый редактор. В результате этих действий в окне текстового редактора будет получен следующий текст в формате HTML.
<html><html><table><table><tr><tr><th id=2>C<th id=3>D<th id=4>E<th id=2>C<th id=3>D<th id=4>E
<html>
<html>
<table>
<table>
<tr>
<tr>
<th id=2>C
<th id=3>D
<th id=4>E
<th id=2>C
<th id=3>D
<th id=4>E
<th id=5>F
<tr id=3>
<td>12
<td>16
<td>20
<td>24
</table>
</html>
Если добавить строку
table.setDragEnabled(true);
после конструктора таблицы и перекомпилировать программу, то выбранный фрагмент таблицы возможно перетащить в приемник перетаскивания в виде фрагмента таблицы в формате HTML.
В табл. 7.6 перечислены компоненты Swing, которые являются источниками и приемниками при передаче данных. Учтите, что стандартные комбинации клавиш для копирования, вырезания и вставки подходят для всех компонентов, кроме JColorChoser. Перетаскивание по умолчанию не разрешено, поэтому для его активации нужно вызвать метод setDragEnabled.
Таблица 7.6. Поддержка передачи данных компонентами
Swing
Компонент
* - '*j ^ • -
, Источник данныхПриемник данных
JFileChooser
Списокфайлов
JColorChooser
Локальная ссылка на объ- Любой объект-цвет ект-цвет
JTextField
Выбранный текстТекст
JFormatedTextField
JPasswordField
Отсутствует (по сообра-Текст
жениям безопасности)
JTextArea
Выбранный текстТекст и список файлов; текст
JTextPane
вставляется, а файлы открываются
JEditorPane
JList
HTML-описаниевыбран-
JLabel
ного фрагмента
JTree
В пакете Swing предусмотрен полезный механизм быстрого превращения компо-- нента в приемник перетаскивания. Если компонент содержит метод void
то его можно преобразовать в приемник перетаскивания для типов документов с классом представления Тип с помощью следующего метода
component.setTransferHandler("Имя");
После перетаскивания обработчик передачи данных проверяет, имеет ли один из типов документа класс представления Тип. Если имеет, то вызывается метод
В терминологии JavaBeans таким образом обработчик передачи данных задает значение для свойства Более подробно свойства компонентов JavaBeans описаны в главе 8, "Технология JavaBeans™".
Предположим, что нужно использовать перетаскивание для изменения фонового цвета текстового поля. Для этого следует создать обработчик передачи данных, который вызывает метод setBackground, когда объект Color будет перетащен в текстовое поле.
void с)
Для создания такого обработчика нужно использовать показанный ниже код.
textField.setTransferHandler(new TransferHandler("background"));
Это поведение демонстрируется в примере программы, код которой приводится в листинге 7.22. Как показано на рис. 7.55, верхняя часть панели содержит цветовую палитру, а нижняя — текстовое поле с текстом Drag color here (Перетащить цвет сюда). Выберите в палитре какой-либо цвет, перетащите его в текстовую строку из панели предварительного просмотра Preview (а не из палитры) и убедитесь в том, что фон строки приобрел указанный цвет.
При установке обработчика передачи данных для текстового поля стандартный обработчик передачи данных отключается. Теперь в этом поле нельзя будет вырезать, копировать, вставлять или перетаскивать текст, потому что компоненты Swing могут иметь только один обработчик передачи данных.
Рис. 7.55. Результат работы программыРис. 7.55. Результат работы программы
Рис. 7.55. Результат работы программы
Рис. 7.55. Результат работы программы
Листинг 7.22. SwingDnDTaat.java
/**
©version 1.00 2001-08-11
©author Cay Horstmann
/
import java.awt.*;
import j avax.swing.*;
/**
Пример использования широких возможностей передачи данных в компонентах Swing. Перетащите цвет из панели предварительного просмотра Preview окна выбора цвета в текстовое поле
/
public class SwingDnDTest
{
public static void main(String[] args) {
JFrame frame = new SwingDnDFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
} }
/**
Фрейм с панелью выбора цвета и текстовым полем. Перетаскивание цвета в текстовое поле изменяет цвет его фона
/
class SwingDnDFrame extends JFrame {
public SwingDnDFrame()
{
setTitle("SwingDnDTest") ;
Container contentPane = getContentPane();
JColorChooser chooser = new JColorChooser();
chooser.setDragEnabled(true) ;
contentPane.add(chooser, BorderLayout.CENTER);
JTextField textField = new JTextField("Drag color here") textField.setDragEnabled(true) ;
textField.setTransferHandler(
new TransferHandler("background”)) ;
contentPane.add(textField, BorderLayout.SOUTH); pack();
5 avax swing • JComponent
Метод void setTransferHandler(TransferHandler handler) устанавливает обработчик передачи данных для управления операциями вы-
резания, копирования, вставки и перетаскивания данных.
ЧЯ! U^MWЧЯ! U^MWj avax . swing • TransferHandler
ЧЯ! U^MW
ЧЯ! U^MW
Конструктор TransferHandler (String propertyName) создает обработчик передачи данных, который читает или записывает свойство компонента JavaBeans с заданным именем, когда выполняется операция передачи данных.
javax.swing.JFileChooser
Метод void setDragEnabled (boolean b) разрешает или запрещает перетаскивание данных из компонента-окна выбора файловJFileChooser.
javax.swing.JColorChooser
Метод void setDragEnabled (boolean b) разрешает или запрещает перетаскивание данных из компонента-окна выбора цветов JColorChooser.
javax.swing.JTextComponent
Метод void setDragEnabled (boolean b) разрешает или запрещает перетаскивание данных из текстового компонентаJTextComponent.
javax.swing.JTable
Метод void setDragEnabled (boolean b) разрешает или запрещает перетаскивание данных из компонента-таблицы JTable.
javax.swing.JTree
Метод void setDragEnabled (boolean b) разрешает или запрещает перетаскивание данных из компонента-дереваJTree.
ТехнологияJavaBeans™ТехнологияJavaBeans™
Технология
JavaBeans™
Технология
JavaBeans™
в этой главе...
т Зачем нужны компоненты JavaBeans
т Создание компонентов JavaBeans
т Применение компонентов JavaBeans для создания приложений
Соглашения об именах для свойств
и событий компонента JavaBeans
т Типы свойств компонента JavaBeans
т Создание пользовательских событий
т Редакторы свойств
Пользовательские соглашения об именах
т Настройщики
т Контекст компонента JavaBeans
Официальное определение компонента (bean) в спецификации JavaBeans гласит: "Компонент— это повторно используемый элемент программного обеспечения, основанный на спецификации JavaBeans компании Sun Microsystems, которым можно управлять визуально с помощью инструментов интегрированной разработки".
Написанный одними программистами компонент JavaBeans может быть использован другими программистами для более эффективного создания аплетов или приложений с помощью таких инструментов интегрированной разработки, как Forte, JBuilder, VisualAge или Visual Cafe.
В настоящей книге не рассматриваются подробности использования этих сред, поэтому для знакомства с ними рекомендуется обратиться к документации, предлагаемой поставщиками. В этой главе описываются основные сведения, необходимые для создания таких компонентов JavaBeans, которые могли бы легко использовать другие программисты.
Зачем нужны компоненты JavaBeans
Программисты, имеющие опыт работы в операционной среде Windows (например, с языками программирования Visual Basic или Delphi), прекрасно понимают огромное значение компонентов JavaBeans. Однако, другие программисты, которым приходилось работать в других условиях по принципу "сделай все сам", не сразу смогут это понять. Опыт показывает, что программисты, не имеющие опыта работы с языком Visual Basic, часто просто не могут поверить, что Visual Basic - это один из наиболее ярких и удачных примеров использования технологии повторного использования объектов. Чтобы понять причину популярности Visual Basic, рассмотрим кратко основные этапы создания простого приложения на языке Visual Basic.
Создание пользовательского интерфейса за счет перетаскивания мышью элементовуправления (controls) вокноформы.
Указание в страницах свойств (property sheets) значений свойств этих элементов управления, например, высоты, цвета или поведения.
Перечисление в страницах свойств тех событий, на которые должен реагировать компонент. Для обработки некоторых событий можно создать небольшие фрагменты кода.
Например, в главе 2 тома 1 рассматривается программа, которая отображает рисунок в фрейме. Для этого потребовалось создать код длиной более одной страницы. Ниже перечислены действия, которые нужно выполнить в интегрированной среде разработки Visual Basic для создания программы практически с такими же функциональными возможностями.
Перетащить в окно фрейма два элемента управления: элемент управления Image для отображения рисунков и элемент управления CommonDialog, который содержит диалоговое окно для выбора файлов.
Далее нужно указать для свойства Filter элемента управления CommonDialog только те расширения файлов, которые будут видны в диалоговом окне и могут быть обработаны элементом управления Image. В интегрированной среде разработки Visual Basic эти значения задаются в диалоговом окне свойств Properties (Свойства), которое показано на рис. 8.1.
Теперь нужно написать пять строк кода на языке Visual Basic, который будет выполняться при запуске этого приложения. Запуск приложения соответствует событию загрузки формы Form_Load. Таким образом, код должен содержаться в процедуре обработки события Form_Load. Приведенный ниже код отображает на экране диалоговое окно выбора файла, в котором будут видны только те файлы, которые имеют расширения, указанные в свойстве Filter. После того, как пользователь выберет файл с изображением, следующая строчка кода указывает на необходимость его отображения в элементе управления Image. Весь код, необходимый для выполнения указанных действий, выглядит следующим образом.
Private Sub Form_Load()
On Error Resume Next
CommonDialogl.ShowOpen
Imagel.Picture =
End Sub
Рис. 8.1.Диалоговое окно свойств Properties со свойствами элемента управления Image в интегрированной среде разработки Visual Basic
Вот и все! Вставка компонентов CommonDialog и Image, указание значений для их свойств и эти пять строчек кода позволяют добиться примерно такого же эффекта, что и целая страница Java-кода. Очевидно, что намного легче научиться перетаскивать готовые компоненты и задавать их свойства, чем создавать целые страницы кода.
В настоящее время язык программирования Java все еще остается довольно сложным инструментом, который используют, в основном, первоклассные специалисты в области объектно-ориентированных технологий. Но даже такие опытные программисты вряд ли будут более продуктивны, чем обычный и менее опытный программист, владеющий языком Visual Basic, когда дело касается разработки пользовательского интерфейса для клиентского приложения. Технология JavaBeans позволяет поставщикам программного обеспечения создавать интегрированные среды разработки приложений, которые значительно облегчают создание пользовательских интерфейсов для Java-приложений.
Учтите, что это совсем не означает, что язык Visual Basic является универсальным решением любой проблемы. На самом деле он оптимизирован для решения специфического вида задач, а именно, для создания Windows-приложений с интенсивным использованием пользовательского интерфейса. В отличие от него технология Java имеет гораздо более широкий диапазон применения. Более того, благодаря появлению технологии JavaBeans программисты могут воспользоваться некоторыми преимуществами стиля программирования на языке Visual Basic.
Теперь компоненты JavaBeans имеют такие же функциональные возможности, которыми обладают наиболее распространенные компоненты Visual Basic.
Интегрированные среды разработки Java-приложений стали гораздо проще и по удобству в работе в значительной степени напоминают интегрированную среду разработки приложений на языке Visual Basic.
Компоненты JavaBeans могут применяться в любых интегрированных средах разработки, в которых предусмотрена поддержка технологии JavaBeans. Разработчики могут даже с помощью технологии взаимодействия компонентов ActiveX и JavaBeans использовать компоненты JavaBeans в приложениях Visual Basic и Microsoft Office (более подробные сведения на эту тему можно найти по адресу http://java.sun.com/ products/javabeans/software).
Создание компонентов JavaBeans
Большая часть этой главы посвящена рассмотрению технологий, используемых при создании компонентов JavaBeans. Прежде всего нужно подчеркнуть, что создание компонентов JavaBeans не является технически сложным процессом, потому что для этого потребуется использовать всего лишь несколько новых классов и интерфейсов.
В частности, простейшим видом компонента JavaBeans является обычный класс платформы Java, имена методов и слушателей событий которого удовлетворяют некоторым строгим соглашениям об именах. В листинге 8.1 в конце данного раздела содержится код компонента для просмотра изображений ImageViewerBean. Этот компонент JavaBeans можно использовать в любой интегрированной среде разработки на основе Java. Он выполняет такие же функции, что и элемент управления изображениями в интегрированной среде разработки на основе Visual Basic, который упоминался в предыдущем разделе.
Если внимательно изучить этот код, можно заметить, что класс ImageViewerBean практически ничем не отличается от других классов. Например, все методы для доступа к полям класса начинаются с префикса get, а все методы, изменяющие поля, начинаются с префикса set. Как показано ниже, для работы со свойствами в инструментах интегрированной среды разработки используются стандартные соглашения об именах. Например, таким свойством компонента JavaBeans является поле fileName, поскольку оно имеет get/set-методы.
В нашем примере свойство f ileName образовано на основе переменной экземпляра file. Учтите, что свойство отличается от переменной экземпляра тем, что свойства находятся на концептуально более высоком уровне, чем переменные экземпляра. Свойства являются атрибутами интерфейса, а переменные экземпляра относятся к реализации этого класса.
Для полной поддержки в компонентной среде компонент JavaBeans должен иметь конструктор поумолчанию (или неиметь конструкторавообще, в этом случае конструктор поумолчаниюобеспечиваетсяавтоматически), а такжедолженбытьсериализован.Это позволяет среде обрабатывать новые компоненты JavaBeans и сохранять их в промежутках между сеансами. Учтите, что это всего лишь практические рекомендации, а не строгие требования. Например, компонент JavaBeans ImageViewerBean является сериализуемым, поскольку наследуется от сериализуемого класса JLabel.
При рассмотрении приведенных в главе примеров следует иметь в виду, что реальные компоненты JavaBeans гораздо сложнее по следующим причинам.
Компоненты JavaBeans должны быть доступны даже для не очень опытных специалистов. Им нужно предоставить доступ к функциональным возможностям компонента JavaBeans посредством визуальных средств разработки без дополнительного программирования. Поэтому для настройки поведения компонента JavaBeans необ- ходимопредоставитьпользователямкомпонента большое количество свойств.
Компоненты JavaBeans должны быть рассчитаны на широкую область применения. Чрезвычайно упрощенный компонент JavaBeans не будет удовлетворять требованиям разработчиков в различных ситуациях, поэтому останется невостребованным. Именно по этой причине разработчики готовы потратить сотни долларов на приобретение профессионального полнофункционального компонента, например такого, как элемент управления диаграммами. Во время создания этой книги один из наиболее популярных элементов управления диаграммами содержал 60 свойств, 14 методов, 47 событий и 178 страниц документации.
Другим хорошим примером компонента JavaBeans с богатым набором параметров является CalendarBean, созданный Каем Тодером (Kai Todter), который показан на рис. 8.2. Этот компонент и его исходный код можно бесплатно скопировать по адресу http://www.toedter.com/en/jcalendar.html. Компонент JavaBeans предоставляет
пользователю удобный способ ввода даты за счет ее выбора на изображении календаря.
щAugUStV 2Ml^JлМол Tue Wed Thu ^Frl j Sat ।г2Э4sС7iя!1011 I1213141516I17.Ш11020222324igrf26771291011'щAugUStV 2Ml^JлМол Tue Wed Thu ^Frl j Sat ।г2Э4sС7iя!1011 I1213141516I17.Ш11020222324igrf26771291011'Рис 8.2 Компонен calendarBeanРис 8.2 Компонен calendarBeanОн имеет очень сложный код, который вряд ли следует создавать самостоятельно с самого начала. Используя технологию JavaBeans, можно воспользоваться готовыми результатами работы других программистов, перетаскивая компо- нентыJavaBeans в интегрированной среде разработки.
щAugUStV 2Ml^Jл
Мол Tue Wed Thu ^Frl j Sat ।
г2Э4
sС7iя!1011 I
1213141516I17.Ш1
1020222324igrf
26771291011'
щAugUStV 2Ml^Jл
Мол Tue Wed Thu ^Frl j Sat ।
г2Э4
sС7iя!1011 I
1213141516I17.Ш1
1020222324igrf
26771291011'
Рис 8.2 Компонен calendar
Bean
Рис 8.2 Компонен calendar
Bean
Очевидно, что описанные выше компонент управления диаграммой и компонент -календарь содержат полный набор всех возможных функций. Но даже такой
простой компонент JavaBeans, как текстовая строка для ввода чисел, имеет довольно сложный код.
Допустим, что пользователь ввел какие-то числа в та-
ком текстовом поле. Должны ли введенные цифры немедленно обрабатываться слушателями событий? С одной стороны такой подход обеспечивает непосредственное взаимодействие между пользователем и компонентом JavaBeans. Однако, во многих
приложениях диапазон вводимых чисел ограничивается, поэтому частично введенное число может оказаться за пределами этого диапазона.
Допустим, что в слушателе событий допускается ввод только четных чисел. В таком случае, если пользователь попытается ввести число 256, слушатель событий после ввода цифр 2 и 5 будет рассматривать частично введенное число 25 как недопустимое. Таким образом, принцип немедленной обработки не подходит. В таких случаях введенное значение следует передавать слушателю событий только по окончании ввода числа, т.е. после нажатия клавиши <Enter> или после потери фокуса ввода.
При создании программы для какого-то одного конкретного приложения можно ограничиться каким-нибудь одним вариантом обработки ввода. Но, как правило, компонент JavaBeans создается не для себя, а для широкого круга потенциальных пользователей с совершенно разными запросами. Поэтому в компоненте JavaBeans следует преду-
смотреть оба способа обработки введенного числа, каждый из которых можно было бы выбрать в зависимости от значения свойства incrementalUpdate. Для добавления еще нескольких настроек даже для такого простого компонента JavaBeans, как текстовое поле IntTextBean, потребуется создать десяток дополнительных свойств.
К счастью, для создания компонента JavaBeans с богатым набором свойств достаточно освоить лишь несколько основных принципов. Приведенные в данной главе не совсем тривиальные примеры компонентов JavaBeans, тем не менее, достаточно просты для иллюстрации этих принципов.
Полнофункциональные компоненты JavaBeans, как правило, предлагаются в составе коммерческих интегрированных сред разработки на основе языка Java или бесплатно, например, по адресуhttp: //www.gamelan.com или на Web-узле среды alphaWorks компании IBM по адресуhttp://alphaworks.ibm.com.
Листинг 8.1. ImageVleverBean. j ava
/**
9version 1.21 2001-08-15
@author Cay Horstmann
*/
import j
import j
import javax.imageio.*; // Закомментировать для SDK < 1.4. import javax.swing.*;
/**
Пример компонента для просмотра изображения
*/
public class ImageViewerBean extends JLabel {
public ImageViewerBean()
{
setBorder(BorderFactory.createEtchedBorder() ) ;
)
/**
Установка свойства fileName
©param fileName the image file name
*/
public void setFileName(String fileName) {
try {
file = new File(fileName);
seticon(new Imageicon(ImagelO.read(file)));
}
catch (IOException exception) {
file = null; setlcon(null);
) )
// Для SDK с номером версии меньше < 1.4 /*
public void setFileName(String fileName) f.
file = new File(fileName);
Imag=e image
MediaTracker tracker = new MediaTracker (this) ;
tracker .addimage (image, 0) ;
try { tracker.waitForlD(O); } catch (Exception e) {} setlcon(new Imageicon(image));
repaint();
}
*/
/**
Возвращает значение свойства fileName
@return the image file name
*/
public String getFileName()
{
if (file == null) return null;
else return file.getPath();
}
public Dimension getPreferredSize()
{
return new Dimension(XPREFSIZE, YPREFSIZE)
private File file = null;
private static final int XPREFSIZE = 200;
private static final int YPREFSIZE = 200;
}
ПрименениекомпонентовJavaBeans
для создания приложений
Перед описанием механизма создания компонентов JavaBeans рассмотрим способы их использования и тестирования. Например, компонент ImageViewerBean вне интегрированной среды разработки теряет все свои преимущества. Дело в том, что для использования его в обычной Java-программе нужно создать код конструктора объекта данного компонента, поместить этот объект в контейнер и вызвать метод setFileName. Это не так уж трудно сделать, но программисту вряд ли захочется тратить время на все эти рутинные действия, если он может избежать их при использовании компонентов в интегрированной среде разработки.
В каждой интегрированной среде разработки используются собственные принципы упрощения труда программистов. Здесь и далее рассматривается интегрированная среда разработки Forte Community Edition. Нельзя сказать, что Forte лучше других аналогичных программных продуктов, конечно, она обладает своими собственными недостатками. Среда Forte рассматривается здесь только потому, что является ТИПИЧ-
ным представителем такого класса программного обеспечения, а также потому, что ее можно бесплатно скопировать и использовать.
Предлагаемые далее инструкции и советы могут использовать те читатели, которые предпочитают применять другую интегрированную среду разработки. За исключением некоторых деталей, основные принципы использования компонентов одинаковы для большинства интегрированных сред разработки.
Если вы не хотите устанавливать громоздкую среду разработки, то можно использовать продукт ВеапВох компании Sun Microsystems. BeanBox предназначен для демонстрации возможностей компонентов JavaBeans для разработчиков инструментов. С помощью продукта ВеапВох можно создавать компоненты JavaBeans, но он не имеет никаких инструментов для удобного размещения компонентов в пользовательском интерфейсе. Кроме того, в настоящее время ВеапВох значительно устарел, потому что не обновлялся с момента появления SDK 1.1.
DDИнтегрированную среду разработки Forte Community Edition можно бесплатно скопировать по адресу http://www.sun.com/forte/ffj/buy.html, а среду разработки BeanBox — по адресуhttp: //java.sun.com/products/javabeans/software.
D
D
В следующем примере используются два компонентаJavaBeans: ImageViewerBean и FileNameBean. Код компонента ImageViewerBean достаточно прост и уже представлен выше, а код компонента FileNameBean выглядит немного сложнее и рассматривается далее в этой главе. Сейчас он ограничен тем, что при нажатии кнопки с надписью ... появляется стандартное диалоговое окно для открытия файла, в котором можно выбрать файл.
Перед продолжением описания способов использования компонентов JavaBeans рассмотрим способ упаковки компонентов JavaBeans для их импорта в интегрированную среду разработки.
Упаковка компонента JavaBeans вJAR-архиве
Для использования компонента JavaBeans в интегрированной среде разработки нужно упаковать все используемые в компоненте JavaBeans классы в JAR-архиве. В отличие от JAR-архива для аплета, который рассматривался ранее, в JAR-архив компонента JavaBeans необходимо поместить файл описания {manifest file), в котором будет указано, какие файлы классов в архиве являются компонентами JavaBeans и должны быть включены в интегрированную среду разработки. Ниже показан пример файла описания ImageViewerBean. mfдля компонента ImageViewerBean.
Manifest-Version: 1.0
Name: ImageViewerBean.class
Java-Bean: True
Обратите внимание, что между определением версии и названием компонента JavaBeans находится пустая строка.
Если класс находится в пакете, то для указания пакета нужно использовать правые косые черты (слэши).
Name: com/horstmann/beans/ImageViewerBean.class
Если компонент JavaBeans содержит несколько файлов классов, то в файле описания необходимо указать только файлы классов для тех компонентов JavaBeans, которые требуется отобразить на панели инструментов. Например, можно поместить компоненты ImageViewerBean и FileNameBean в один JAR-файл и использовать следующий файл описания:
Manifest-Version: 1.0
Name: ImageViewerBean.class
Java-Bean: True
Name: FileNameBean.class
Java-Bean: True
Некоторыеинтегрированныесредыразработкичрезвычайночувствительныкфайлам описания, т.е. все строки должны заканчиваться без пробелов, после определения версии и между записями компонентовJavaBeansдолжна располагаться пустаястрока,а также вслед за последней строкой должна располагаться еще одна пустая строка.
Для создания J AR-файла необходимо выполнить следующие действия.
Отредактируйте файл описания.
Соберите все необходимые файлы классов в одном каталоге.
Запустите архиватор jar так, как показано ниже.
jar ctm заг_архив файл_описания ‘.class
Вот как будет выглядеть архивирование файлов классов с файлом описания ImageViewerBean. mf nJAR-архиве ImageViewerBean. jar.
jar ctm ImageViewerBean.jar ImageViewerBean.mf ‘.class
В JAR-файлы можно также включать другие объекты, например, GIF-изображения пиктограмм. Пиктограммы-компоненты JavaBeans более подробно описываются далее в этой главе.
Убедитесь в том, что в JAR-архив упакованы все необходимые файлы, включая файлы внутренних классов, например, ImageViewerBean$l.class.
Интегрированная среда разработки обычно содержит механизмы включения новых компонентов JavaBeans, как правило, на основе загрузки JAR-архивов. Ниже пе-
речислены те действия, которые нужно выполнить для включения новых компонен- товJavaBeans в интегрированную среду разработки Forte.
Откомпилируйте классы ImageViewerBean и FileNameBean и упакуйте их В JAR- архив. Затем запустите интегрированную среду разработки Forte и проделайте следующие действия.
Выберите команду меню Tools^lnstall new JavaBean (Инструменты =>Вставить новый компонентJavaBeans).
Для открытия нужного JAR-файла в диалоговом окне найдите каталог ImageViewerBean и выберите в нем файл ImageViewerBean. j ar.
После этого в диалоговом окне будет представлен список всех компонентов JavaBeans из выбранного JAR-архива. Выберите компонент ImageViewerBean.
Наконец, выберите ту панель инструментов, в которой следует разместить компонент JavaBeans. В данном примере среди предлагаемых панелей (например, для компонентов Swing, для компонентов AVVT и т.д.) выберите панель Beans.
Как показано на рис. 8.3, панель Beans будет содержать новую пиктограмму, которая представляет новый компонент JavaBeans. Для этого компонента используется предлагаемая по умолчанию пиктограмма. Далее в главе рассматриваются способы включения пиктограммы в KOMnoHCHTjavaBeans.
——i
**JEMnsjTcwjMtnria jJrewslna 1' RunninsTDebugging Г ;JEMnsjTcwjMtnria jJrewslna 1' RunninsTDebugging Г ;« ЯМЯй A.Г^е »V <W5^ !■« ЯМЯй A.Г^е »V <W5^ !■
*
*
JEMnsjTcwjMtnria jJrewslna 1' RunninsTDebugging Г ;
JEMnsjTcwjMtnria jJrewslna 1' RunninsTDebugging Г ;
« ЯМЯй A
.Г^е »V <W5^ !■
« ЯМЯй A
.Г^е »V <W5^ !■
‘ v3 Templatesi e- a AWT FormsЯ О Seans®; ©• ^Classes■ ©- □ SampleForms< €*□]$₽L o- • Other$ C3 Swing Forms!3 JApplet ^.JDiafog‘ v3 Templatesi e- a AWT FormsЯ О Seans®; ©• ^Classes■ ©- □ SampleForms< €*□]$₽L o- • Other$ C3 Swing Forms!3 JApplet ^.JDiafogzhй Jlnterna'FrameJ 1®JPanelzhй Jlnterna'FrameJ 1®JPanel
‘ v3 Templates
i e- a AWT Forms
Я О Seans
®; ©• ^Classes
■ ©- □ SampleForms
< €*□]$₽
L o- • Other
$ C3 Swing Forms
!3 JApplet ^.JDiafog
‘ v3 Templates
i e- a AWT Forms
Я О Seans
®; ©• ^Classes
■ ©- □ SampleForms
< €*□]$₽
L o- • Other
$ C3 Swing Forms
!3 JApplet ^.JDiafog
zhй Jlnterna'Frame
J 1®JPanel
zhй Jlnterna'Frame
J 1®JPanel
Рис.8.3. Включение компонента JavaBeans в панель Beans в интегрированной среде разработки Forte
Повторите эти действия для включения компонента FileNameBean. Теперь на основе компонентов JavaBeans можно создать приложение.
.ЭШ
- ‘Teittptois Description -- - ‘-•
fWl Using this template ■you can design a new
1 | JFC (Swing) Frame.I]
, = j Frames are typically used as standaloneOi
". top-level windows as the main user
BIS interface tothe application.L 1 I
New.. *
Name:ilmageVie 1
Л.»м«Stewie ,
Directory. •’ a_
|Finish j Cancel IHelp
Рис. 8.4.Создание новой формы
Создание компонентов JavaBeans в интегрированной среде разработки
В интегрированной среде разработки Forte выберите команду меню File^New
(Файл ^Создать). В диалоговом окне New (Создать) откройте папку шаблонов Swing Forms в левой части окна и выберите шаблон фрейма JFrame. В правой нижней части окна в текстовом поле Name (Имя) укажите имя нового фрейма Imageviewer, как показано на рис. 8.4.
Теперь в интегрированной среде разработки будет открыто окно редактора форм (рис. 8.5) и хорошо знакомое окно редактора кода (рис. 8.6). В редакторе форм показаны контуры формы, а в окне редактора
Рис. 8.5. Окно редактора формРис. 8.5. Окно редактора формкода содержится несколько строк кода.
Рис. 8.5. Окно редактора форм
Рис. 8.5. Окно редактора форм
Для включения компонентов JavaBeans в форму выберите нужный компонент JavaBeans в панели компонентов и щелкните мышью на каком-либо участке формы.
£j Source. Editor (iMgeVievef* *1
h
4Ж Ж. 5; >/ ЛЧ
5
1J public class Imageviewer extends javax. swing. JFrame {
1-4
public ImageViewerO {
i?ini
18pack ();
№ 1
21- ' f;.:;:, r.f-TSiG.i Tг:-?П<ч/ f'"v»r wir(r!;) thv'
22H^t3aV^zc ib- t^:-^..
2:3 Л V'^iA’m.: iJa ^<2Г гро^чту thii; Vt-Ж. 7f;<:.-: vU^'t^t <;r t;v
Рис.8.6. Окно редактора кода
В разных интегрированных средах разработки для работы с компонентами могут использоваться разные приемы. Например, для помещения компонента JavaBeans в определенном месте формы в некоторых средах разработки приложений требу- етсяперетащитькомпонентJavaBeansв нужнуючастьформы.
На рис. 8.7 показан результат добавления компонента JavaBeans для просмотра изображений в центральной части фрейма.
Если теперь посмотреть на окно редактора кода, то можно заметить, что исходный код содержит все Java-инструкции, необходимые для включения объекта ImageViewerBean в главную панель фрейма. Этот код окружен знаками комментария с предупреждениями о том, что их нельзя редактировать, потому что результаты редактирования могут исчезнуть при автоматическом обновлении кода после изменения формы в редакторе формы.
В. 7.ВставкакомпонентаРис.Инспектор свойств
JavaBeans в форму
Невсе интегрированные среды разработки способны обновлять исходный код в процессе создания приложения. Интегрированная среда разработки может генерировать исходный код по окончании редактирования, переделанные компоненты JavaBeans и, возможно, создавая совершенно другое описание ваших действий.
В частности, в SDK имеются методы для загрузки и сохранения XML-описаний для коллекций компонентов JavaBeans. Эта технология все еще является экспериментальной и пока представляет интерес только для производителей инструментов разработки. Дополнительную информацию по этой теме можно получить по адресу http:// java.sun.com/products/jfc/tsc/articles/persistence2/index.html.
Теперь щелкните в форме на компоненте ImageViewerBean и выберите команду меню После этого экране появится окно инспектора свойств, в котором будет отображен список с именами свойств компонента JavaBeans и их текущие значения 8.8). Инспектор свойств образует основную часть инструментов разработки компонентных объектов, потому что значения свойств, заданные в процессе создания компонента, будут определять исходное состояние компонента.
Например, для изменения надписи на компоненте ImageViewerBean достаточно отредактировать свойство text компонента JavaBeans, введя новое значение в инспекторе свойств. Для изменения свойства text необходимо отредактировать строку в соответствующем текстовом поле. Попробуйте сделать это, вводя значение Hello, после чего форма немедленно изменится, реагируя на ваши действия. Если вы не хотите, чтобы текст появлялся возле изображения, то укажите в поле text пустую строку.
При изменении значения свойства интегрированная среда разработки Forte сразу же обновляет исходный код, мгновенно реагируя на ваши действия. Например, если для поля text указать значение Hello, то в коде метода initComponents добавится инструкция imageViewerBean.setText("Hello");. Как уже упоминалось выше, в других интегрированных средах разработки могут использоваться другие принципы записи новых значений свойств.
Свойства не обязательно имеют значения строкового типа, они могут быть значениями какого-нибудь другого типа данных, допустимого в языке Java. Для предоставления пользователям возможности выбора значения произвольного типа в интегрированных средах разработки предусмотрены специальные редакторы свойств. (Редакторы свойств могут быть встроены в интегрированной среде разработки или в компонент JavaBeans. Способы создания собственных редакторов свойств рассматриваются ниже в этой главе.)
В качестве примера простого редактора свойств рассмотрим свойство background, которое определяет цвет фона и имеет тип Color. Как показано на рис. 8.9, это свойство содержит редактор цветов, который представляет собой список с названиями цветов и кнопку с надписью... для отображения диалогового окна редактирования цвета.
Задайте для фона оранжевый цвет, выбирая его в редакторе свойства background. Обратите внимание, что цвет фона изменяется сразу же при изменении значения этого свойства.
Однако, гораздо интереснее поэкспериментировать со свойством fileName. Введите название графического файла в текстовом поле данного свойства, после чего в компоненте ImageViewerBean сразу же будет показано изображение из указанного вами графического файла, как представлено на рис. 8.10.
Рис.8.9. Использование редактора свойств для указания значения свойстваРис.8.9. Использование редактора свойств для указания значения свойстваРис. 8.10. Пример использования свойства fileName компонента ImageViewerBeanРис. 8.10. Пример использования свойства fileName компонента ImageViewerBeani debugGraphk^i debugGraphk^.«t dlsplayedMnen Il ddubltBuffere^'.«t dlsplayedMnen Il ddubltBuffere^'
Рис.8.9. Использование редактора свойств для указания значения свойства
Рис.8.9. Использование редактора свойств для указания значения свойства
Рис. 8.10. Пример использования свойства fileName компонента ImageViewerBean
Рис. 8.10. Пример использования свойства fileName компонента ImageViewerBean
i debugGraphk^
i debugGraphk^
.«t dlsplayedMnen Il ddubltBuffere^'
.«t dlsplayedMnen Il ddubltBuffere^'
На рис. 8.10 приведены и некоторые другие свойства, например, focusCycleRoot и iconTextGap. Они унаследованы от суперкласса JLabel. Далее в этой главе описаны способы скрытия этих свойств в окне инспектора свойств.
Разместите компонент FileNameBeanв нижней части фрейма. Попробуем теперь ор-
Рис. 8.11. Вкладка Events с перечнем событийРис. 8.11. Вкладка Events с перечнем событийганизовать такое поведение приложения, чтобы при изменении свойства £ ileName в компоненте FileNameBean отображалось новое заданное изображение. Такое поведение можно организовать с помощью события PropertyChangeEvent, которое подробно описывается ниже в этой главе.
Рис. 8.11. Вкладка Events с перечнем событий
Рис. 8.11. Вкладка Events с перечнем событий
Для того, чтобы система отреагировала на это событие, выберите компонент FileNameBean. (Более подробно он рассматривается в следующем разделе, а его код приводится в листинге 8.2.) Затем в инспекторе свойств компонента FileNameBean выберите вкладку событий Events, которая показана на рис. 8.11. После ЭТОго щелкните на надписи <попе> события property-Change, и на экране появится пустое диалоговое окно Handlers for propertyChange (Обработчики события propertychange), т.е. данное событие не имеет обработчиков. Щелкните на кнопке Add (Создать) в диалоговом окне и укажите название метода- обработчика этого события, например, loadimage.
Как показано на рис. 8.12, после этого в окне редактора кода
появится код обработки события и новый метод loadlmage.
private void loadimage(j ava.beans.Propertychange evt)
// здесь можно вставить код обработки события
}
Вставьте следующую строку кода.
imageViewerBeanl.setFileName ( fileNameBeanl.getFileName() ) ;
Затем откомпилируйте и запустите приложение. Щелкните на кнопке с надписью ... и выберите файл с изображением. Выбранное изображение появится в окне просмотра изображений.
Этот процесс демонстрирует способ создания Java-приложений на основе компонентов JavaBeans за счет указания их свойств и создания небольших фрагментов кода обработки событий.
В некоторых интегрированных средах разработки, например, в ВеапВох, продвинулись еще дальше, полностью избавляя пользователя от необходимости создания кода. В интегрированной среде разработки ВеапВох предусмотрены механизмы создания различных стандартных ситуаций, например таких, как перечисленные ниже.
Вызов метода для определенного события.
Установка нового значения свойства приизменении значения другого свойства.
С помощью ВеапВох можно создавать простые приложения вообще без использования программирования, а только за счет перетаскивания компонентов в форму, указания значений для их свойств и связывания событий с методами или свойствами.
Однако, этого недостаточно для выполнения более сложных операций. В отличие от BeanBox в интегрированной среде разработки Forte программист может создавать приложения, совмещая кодирование и визуальные манипуляции с компонентами.
Igjif^l^Igjif^l^USUS4243444S46474849505152335455565?585960616263644243444S46474849505152335455565?58596061626364gettontentPane().add(1aageViewerBeani, java.awt.BorderlygettontentPane().add(1aageViewerBeani, java.awt.Borderlyjava. awt.CrldBagConstraints grldBagConstraintsl; ->■fi1elfaaeBeanl.addPnipertyChangeUstener(nw java, beans.Ptpublic void propertythange(java.beans.PropertyChangeH- loadleaee(evt);:g:java. awt.CrldBagConstraints grldBagConstraintsl; ->■fi1elfaaeBeanl.addPnipertyChangeUstener(nw java, beans.Ptpublic void propertythange(java.beans.PropertyChangeH- loadleaee(evt);:g:}3;getContentPane().add(f11eNaaeBeanl, java.awt.BorderLayoui}3;getContentPane().add(f11eNaaeBeanl, java.awt.BorderLayoui||}}private уold loadlaage(java.beans.FropertyChangeEvent evt) { /Л1 л iprivate уold loadlaage(java.beans.FropertyChangeEvent evt) { /Л1 л iУУprivate void ex1 tFora(java.awt.event.WlndowEvent evt) {System.exit Cprivate void ex1 tFora(java.awt.event.WlndowEvent evt) {System.exit CРис.8.12. Код обработки событий в окне редактора кодаРис.8.12. Код обработки событий в окне редактора кода
Igjif^l^
Igjif^l^
US
US
42
43
44
4S
46
47
48
49
50
51
52
33
54
55
56
5?
58
59
60
61
62
63
64
42
43
44
4S
46
47
48
49
50
51
52
33
54
55
56
5?
58
59
60
61
62
63
64
gettontentPane().add(1aageViewerBeani, java.awt.Borderly
gettontentPane().add(1aageViewerBeani, java.awt.Borderly
java. awt.CrldBagConstraints grldBagConstraintsl; ->■
fi1elfaaeBeanl.addPnipertyChangeUstener(nw java, beans.Pt
public void propertythange(java.beans.PropertyChangeH- loadleaee(evt);:g:
java. awt.CrldBagConstraints grldBagConstraintsl; ->■
fi1elfaaeBeanl.addPnipertyChangeUstener(nw java, beans.Pt
public void propertythange(java.beans.PropertyChangeH- loadleaee(evt);:g:
}
3;
getContentPane().add(f11eNaaeBeanl, java.awt.BorderLayoui
}
3;
getContentPane().add(f11eNaaeBeanl, java.awt.BorderLayoui
|
|
}
}
private уold loadlaage(java.beans.FropertyChangeEvent evt) { /Л1 л i
private уold loadlaage(java.beans.FropertyChangeEvent evt) { /Л1 л i
У
У
private void ex1 tFora(java.awt.event.WlndowEvent evt) {
System.exit C
private void ex1 tFora(java.awt.event.WlndowEvent evt) {
System.exit C
Рис.8.12. Код обработки событий в окне редактора кода
Рис.8.12. Код обработки событий в окне редактора кода
Механизм JavaBeans не пытается навязывать интегрированной среде разработки какой-либо способ создания приложения. Вместо этого он просто предоставляет интегрированной среде разработки информацию о компоненте JavaBeans, а интегрированная среда разработки уже по своему усмотрению выбирает тот или иной способ работы в зависимости от этой информации. В следующем разделе описываются способы программирования компонентов JavaBeans таким образом, чтобы интегрированная среда разработки могла правильно интерпретировать эту информацию.
Соглашения об именах для свойств и событий компонента JavaBeans
Прежде всего необходимо подчеркнуть, что для компонентов JavaBeans не существует никакого особого класса, от которого можно было бы унаследовать созданные компоненты JavaBeans. Все визуальные компоненты JavaBeans прямо или косвенно наследуют класс Component, а невизуальные компоненты JavaBeans не имеют никакого особого суперкласса. Напомним, что компонент JavaBeans представляет собой произвольный класс, которым можно управлять при помощи интегрированной среды разработки. Для определения природы компонента JavaBeans интегрированная среда разработки анализирует не суперкласс, а имена методов компонента JavaBeans. Поэтому имена методов компонента JavaBeans должны удовлетворять определенным соглашениям.
Все методы класса java.beans.Beans являются статическими. Поэтому бессмысленно наследовать его, несмотря на то, что это, возможно, сделано для большей "ясности". Действительно, ведь компонент JavaBeans не может наследовать классы Beans и Components одновременно, поэтому визуальные компоненты JavaBeans не могут воспользоваться этим подходом. Класс Beans содержит методы, предназначенные для организации взаимодействия со средой интегрированной разработки, например, для проверки режима использования инструмента, т.е. во время создания или во время выполнения приложения.
В других языках программирования, которые используются в интегрированных средах визуального программирования, например, в Visual Basic и Delphi, для обозначения смысла элементов кода предусмотрены специальные ключевые слова Property и Event. Создатели спецификации языкаJava решили не включать в этот язык специальные ключевые слова для поддержки визуального программирования. Поэтому для анализа компонента JavaBeans и определения его свойств и событий следует использовать какие-то альтернативные способы. Действительно, для этого предусмотрено два альтернативных механизма. Во-первых, если автор компонента JavaBeans использует для свойств и событий стандартные соглашения об именах, то интегрированная среда разработки для определения нужных свойств и событий компонента JavaBeans может использовать механизм отражения (reflection mechanism). Во-вторых, автор компонента JavaBeans может использовать специальный информационный класс компонента (bean information class), которыйсодержит описание свойств и событий компонента JavaBeans, необходимых интегрированной среде разработки. Начнем с более простого способа на основе использования соглашения об именах, а потом рассмотрим применение информационных классов компонента.
Хотя в документации стандартные соглашения об именах называются шаблонами проектирования (design patterns), но на самом деле это всего лишь разновидность соглашений об именах, которая не имеет ничего общего с шаблонами проектирования, которые используются в объектно-ориентированном программировании.
Соглашения об именах для свойств имеют очень простую структуру. Например, метод public X get Имя._ Св ойс тв а() возвращает (т.е. считывает) значение свойства типа X, а метод public void 5еЪИмв._Саойства (X х) задает (т.е. записывает) значение х типа X для этого свойства.
Например, компонент imageViewerBean содержит только одно считывае- мое/записываемое свойство для отображения названия файла, для которого используются следующие методы.
public String getFileName()
public void setFileName(String f)
Свойство может быть определено только для чтения, если для него предусмотрен get-метод, но не предусмотрен set-метод.
Особое внимание следует уделить использованию прописных и строчных букв в именах методов. Создатели спецификации JavaBeans решили, что название свойства (в данном примере fileName) должно начинаться со строчной буквы f, даже если get-метод и set-метод содержат для него прописную букву F (в данном примере getFileName и setFileName). Для определения названия свойства анализатор компонентов JavaBeans выполняет декапиталимцию (decapitalization). (Поэтому первая бук-
ва после префикса get или set преобразуется в нижний регистр.) Логическим обоснованием такого подхода является то, что такие имена методов и свойств являются более привычными для программистов.
•
Созданные get-методы и set-методы, как и любые другие методы Java, могут выполнять произвольные действия, а не только возвращать и задавать значения поля. Например, метод setFileName класса ImageVieverBean не только задает значение для текстового поля fjl eName, но также открывает файл и загружает изображение.
В Visual Basic для работы со свойствами также используются get-методы и set- ■ методы, а в Delphi используются read-методы и write-методы. Но в обоих языках существует ключевое слово Property, так что компилятору не нужно определять намерения программиста на основе анализа имен методов. Использование ключевого слова в этих языках дает еще одно преимущество. Указание имени свойства в левой части выражения автоматически связано с вызовом set-метода, а указание имени свойства в самом выражении автоматически связано с вызовом get- метода. Например, в Visual Basic можно использовать код=
"corejava.gif"; вместо кода imageBean.setFileName("corejava.gif") ;.
В соглашении об именах языка Java для свойств логического типа вместо префиксов get/set нужно использовать префиксы is/ set так, как показано в следующих примерах.
public boolean 18ЙИЯ Свойств а () ;
public void set^4мя_Cвoйcтвa (boolean b);
Например, компонент JavaBeans с анимационными возможностями может иметь свойство running со следующими двумя методами.
public boolean isRunning();
public void setRunning(boolean b);
Таким образом, метод setRunning может запускать и останавливать анимацию, а метод isRunning — сообщать о текущем состоянии анимации.
Соглашение об именах для событий устроено еще проще. Интегрированная среда разработки определяет события, генерируемые компонентом JavaBeans, при наличии методов включения и удаления слушателей событий. Предположим, что компонент JavaBeans генерирует событие типа Имя_СобытияЕяепt (названия всех событий должны иметь суффикс Event). В таком случае интерфейс-слушатель должен называться 14мя_ Co бытияЪ i s t ene г, а методы включения и удаления слушателей событий должны иметь показанные ниже имена.
public void аййИмя_ СобытияТА s t ener(EventNameListener e) ;
public void гетояеИмя_СобытияЪ1stener (EventNameListener e) ;
В коде компонента ImageViewerBean нет никаких открытых объектов-событий, но ниже рассматривается компонент-таймер, который генерирует объекты типа TimerEvent и содержит следующие методы для управления слушателями событий.
public void addTimerListener (TimerListener e);
public void removeTimerLi st ener (TimerListener e);
Что нужно сделать, если в классе присутствует пара из get-метода и set-метода, не соответствующая свойству, которое следует предоставить пользователям для работы с ним в редакторе свойств? В собственных классах, во избежание такой ситуации, следует просто переименовать эти методы. Но при наследовании другого класса наследуются имена методов этого суперкласса. Это происходит, например, при создании компонента JavaBeans на основе класса JPanel или класса JLabel. В таких случаях в редакторе свойств появляется большое количество ненужных свойств. Этот факт демонстрирует то, что использование соглашения об именах нельзя назвать совершенным способом определения свойств. Далее в этой главе будет показано, как можно переопределить процесс автоматического обнаружения свойств за счет использования специальных информационных классов компонента (bean information class), в которых задается соответствие между методами и свойствами.
Типы свойств компонента JavaBeans
Сложный компонент JavaBeans может иметь множество различных типов свойств, которые должны быть предоставлены пользователю во время создания или выполнения приложения. Кроме того, компонент JavaBeans способен генерировать стандартные и пользовательские события. Свойства могут быть совсем простыми, например, как свойство fil eName компонента ImageViewerBean или компонента FileNameBean, либо сложными, например, палитра цветов или массив данных. Более подробно эти сложные свойства рассматриваются далее в этой главе. Более того, как будет показано в этом разделе, свойства могут генерировать события.
Создание свойств компонента JavaBeans, возможно, является наиболее сложной частью всего процесса создания компонентаJavaBeans. В спецификации компонентов JavaBeans предусмотрено использование четырех типов свойств, которые рассматриваются в следующих разделах.
Простые свойства
Простое свойство — это свойство, которое содержит одно простое значение, например, строку или число. Примером простого свойства является свойство fileName компонента ImageVi ewerBean. Простое свойство довольно легко программируется с помощью описанного выше соглашения об именах на основе префиксов set и get. Например, как показано в листинге 8.1, для создания простого свойства типа String достаточно создать следующие set-метод и get-метод.
public void setFileName ( String f )
fileName = f;
image = ...
repaint();
public String getFileName ( String f )
{
if (file == null) return null;
else return file.getPath();
}
Учтите, что в спецификации JavaBeans предусмотрено свойство, определенное только для чтения, которое имеет следующий get-метод.
public Dimension getPreferredSize();
Но это свойство не имеет соответствующего set-метода setPreferredsize. Обычно методы, предназначенные только для чтения свойства, не доступны в окне редактора свойств.
Индексированные свойства
Индексированное свойство — это свойство, которое может возвращать и задавать значения для элементов массива. Рассмотренный ниже компонент ChartBean содержит индексированное свойство для ввода данных. Для работы с индексированным свойством одна пара get/set-методов используется для всего массива, а остальные пары — для отдельных элементов массива. Для них используется следующее соглашение об именах.
X[] деГИмя_Свойства( ;
void зе^Имя„СвойстЕ(X[] x) ;
X getИмя_Cвoйcтвa(ii);
void setИмя_Cвoйcтвa(int i, X x) ;
Вот как выглядит индексированное свойство в компоненте ChartBean, который рассматривается ниже в этой главе.
public doublet] getValuesO { return values; }
public void setvalues(double[] v) { values = v; }
public double getValues(int i) { return values[ i ] ; }
public void setvalues(int i, double v) { values[i] = v; }
private doublet] values;
Пара get/set-методов для обработки отдельных элементов массива способна проверять соответствие номера элемента i заданному размеру массива. Это значит,
что интегрированная среда разработки не будет вызывать эти функции, если индекс i окажется меньше 0 или больше размера массива. В частности, нельзя использовать следующий метод для расширения массива.
веЬИмя_Свойства (ii,nt X[] x);
Для увеличения массива нужно вручную создать новый массив и передать его следующему методу.
setИмя_Свойства(X[] х) ;
На практике get/set-методы можно вызывать не только в интегрированной среде, но и программно. Поэтому, для проверки допустимости значений индекса рекомендуется явным образом создать код такой проверки. Ниже приведен пример такого кода, который используется в компоненте ChartBean.
public double getValues(int i)
{
if (0 <= i && i < values.length) return values[i];
return 0;
}
public void setvalues(int i, double value)
{
if(0 <= i && i < values.length) values[i] = value;
Во время создания этой книги в интегрированных средах разработки Forte и BeanBox не были предусмотрены неявные автоматические средства проверки границ массива. Далее в главе показано, как преодолеть это ограничение с помощью пользовательского редактора свойств массива.
Связанные свойства
Связанные свойства уведомляют слушателей событий об изменении их значения. Например, свойство fileName компонента FileNameBean является примером такого связанного свойства. При изменении названия файла компонент ImageViewerBean автоматически уведомляется об этом, и загружается новый файл.
Для создания связанного свойства необходимо выполнить следующие действия.
При изменении значения свойства компонент JavaBeans должен послать событие PropertyChangeEvent всем зарегистрированным слушателям событий. Такое изменение может произойти при вызове set-метода или при выполнении пользователем какого-то действия, например, редактировании текста или выборе файла.
Для автоматической регистрации слушателей событий в компонент JavaBeans следует включить два метода.
void addPropertyChangeListener(PropertyChangeListener listener)
void removePropertyChangeListener(PropertyChangeListener listener)
Пакет j ava.beansсодержитполезныйклассPropertyChangeSupport, который управляет работой слушателей событий. Для использования данного класса компо- нентуJavaBeans необходимо иметь поле для данных этого типа.
private PropertyChangeSupport changeSupport
= new PropertyChangeSupport (this);
Таким образом, вся работа, связанная с добавлением/удалением слушателей событий, передается этому объекту.
public void addPropertyChangeListener(PropertyChangeListener listener) {
changesupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
changesupport.removePropertyChangeListener(listener);
}
Теперь при изменении значения свойства следует использовать метод f ireProp- ertyChange объекта PropertyChangeSupport для передачи события всем зарегистрированным слушателям событий. Как показано ниже, метод имеет три параметра: имя свойства, прежнее значение и новое значение.
changesupport.firePropertyChange("fileName", oldValue, newValue);
Оба значения должны быть объектами, а если они не является таковыми, то их нужно преобразовать в объекты так, как показано ниже.
changesupport.firePropertyChange("running", new Boolean(false), new Boolean(true));
Если компонент JavaBeans расширяет какой-либо класс Swing, то он в конечном счете расширяет класс Jcomponent, ив этом случае не нужно создавать методы addPropertyChangeListener и removePropertyChangeListener, потому что ониужереализованывсуперклассеJComponent.
Для уведомления слушателей событий изменения свойства вызывается метод f irePropertyChange суперклассаJComponent.
firePropertyChange("propertyName", oldValue, newValue);
Для удобства метод перегружен для следующих типов: boolean, byte, char, double, float, int, long и short. Если значения oldValue и newValue принадлежат этим типам, то их не нужно преобразовывать в объект.
ДругиекомпонентыJavaBeans, которыеследуетуведомитьобизменениизначения свойства, должны создаваться на основе интерфейса PropertyChangeListener, который содержит только один метод.
void propertychange(PropertyChangeEvent event);
Код метода propertyChange выполняется при каждом изменении значения свойства, но при условии указания объекта-получателя события изменения свойства в том компоненте, который генерирует это событие. Объект PropertyChangeEvent содержит старое и новое значения свойства, доступ к которым можно получить следующим образом
Object oldValue = event.getOldValue();
Object newValue = event.getNewValue();
Если тип свойства не является классом, то возвращается объект с соответствующим типом-оболочкой. Например, при изменении свойства типа boolean возвращается тип Boolean, который можно извлечь с помощью метода booleanValue.
Таким образом, структура объекта-слушателя событий имеет следующий вид.
class Listener {
public Listener()
{
bean.addPropertyChangeListener(new
PropertyChangeListener() { void propertychange(PropertyChangeEvent event) { Object newValue = event.getNewValue();
} }); }
}
В листинге 8.2 содержится весь код компонента FileNameBean. Класс FileNameBean наследует класс JPanel, поэтому здесь явно не используется объект PropertyChangeSupport. Вместо этого используются предусмотренные в классе JPanel возможности для управления слушателями событий.
Листинг 8.2. FileNameBean.java
/**
Aversion 1.20 1999-09-24
@author Cay Horstmann
*/
import j ava.awt.*;
import java.awt.event.* ;
import java.beans.* ;
import java.io.*;
import javax.swing.* ;
/**
Компонент выбора файла
*/
public class FileNameBean extends JPanel {
public FileNameBean()
{
dialogButton = new JButton("...");
nameField = new JTextField(30);
chooser = new JFileChooser();
chooser.setCurrentDirectory(newFile(". "));
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter()
{
public boolean accept(File f)
{
String name = f.getName().toLowerCase() ;
return name.endsWith("." + defaultExtension) | | f.isDirectory();
}
public String getDescription()
{
return defaultExtension + " files";
}
));
setLayout(newGridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 100;
gbc.weighty = 100;
gbc.anchor =
gbc.fill =
add(nameField, gbc, 0, 0, 1, 1);
dialogButton.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent evt)
{
int r = chooser.showOpenDialog(null) ;
if(r == JFileChooser.APPROVE_OPTION)
{
File f = chooser.getSelectedFile();
try {
String name = f.getCanonicalPath(); setFileName(name);
}
catch (IOException exception)
}
J
}
});
nameField.setEditable(false);
gbc.weightx = 0;
gbc.anchor = GridBagConstraints.EAST;
gbc.fill =
add(dialogButton, gbc, 1, 0, 1, 1);
/**
Включение компонента в заданное место решетки @param с the component to add
@param gbc the grid bag constraints to use @param x the x grid position
@param у the у grid position
@param w the grid width
@param h the grid height
*/
public void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
add(c, gbc);
/**
Установка значения свойства filename
@param newValue the new file name
*/
public void setFileNazne (String newValue)
{
String oldValue =;
nameField.setText(newValue);
firePropertyChange("filename", oldValue, newValue),
/**
Возвращение значения свойства fileName Sreturn the name of the selected file
*/
public String getFilename()
{
return nameField.getText () ;
/**
Установка значения свойства defaultExtension @param s the new default extension
*/
public void setDefaultExtension(String s)
{
defaultExtension = s;
/**
Возвращение значения свойства defaultExtension
@return the default extension in the file chooser
*/
public String getDefaultExtension()
{
return defaultExtension;
}
public Dimension getPreferredSize()
{
return new Dimension(XPREFSIZE, YPREFSIZE);
private static final int XPREFSIZE = 200;
private static final int YPREFSIZE = 20;
private JButton dialogButton;
private JTextField nameField;
private JFileChooser chooser;
J private String defaultExtension =
java.beans.PropertyChangeListener
Метод void propertyChange(PropertyChangeEvent event) вызывается после генерации события изменения свойства.
Параметры:
eventсобытие изменения свойства
java.beams.PropertyChamgeSupport
МетодPropertyChangeSupport (ObjectsourceBean) создаетобъект изменения свойства PropertyChangeSupport.
Параметры:
sourceBeanкомпонентJavaBeans, который является источником из
менения свойства (обычно this)
Метод void addPropertyChangeListener (PropertyChangeListener listener) регистрирует слушателя событий listener для связанного свойства.
Параметры:
listener объект, который должен быть уведомлен об изменении связанного свойства
Метод void
removePropertyChangeListener (PropertyChangeLis-
tener listener) удаляет слушателя событий listener для связанного свойства.
Параметры:
listener объект, который должен быть удален из списка слушателей
Метод void firePropertyChange(String propertyName, Object oldValue, Object newValue) посылает событие PropertyChangeEvent об изменении связанного свойства зарегистрированным слушателям событий.
Параметры:
propertyName имя свойства
oldValue newValue
старое значение свойства новое значение свойства
java.beams.PropertyChangeEvent
Конструктор PropertyChangeEvent(Object source, String propertyName, Object oldValue, Object newValue) создает новый объект изменения связанного свойства PropertyChangeEvent.
Параметры: source propertyName oldValue newValue
компонентJavaBeans, являющийся источником события имя свойства
старое значение
новое значение
Метод Object getNewValue () возвращает новое значение свойства.
Метод Obj ect getOldValue () возвращает прежнее значение свойства.
МетодObj ect getPropertyName () возвращает имя свойства.
M ^ avax. swing. JComponent
Метод void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) регистрирует слушателя событий listener для связанного свойства.
Параметры:
propertyName имя свойства для слушателя событий listener
1istenerобъект, который должен быть уведомлен об изменениях
связанного свойства
Метод void addPropertyChangeListener (PropertyChangeListener listener) регистрирует слушателя событий listener для всех связанных свойствданного компонента.
Параметры:
listener объект, который должен быть уведомлен об изменениях связанного свойства
Метод void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) удаляет зарегистрированный ранее слушатель событий listener для связанного свойства propertyName.
Параметры:
propertyName имя свойства для слушателя событий listener listener объект, который должен быть удален из списка слушателей
Метод voidremovePropertyChangeListener(PropertyChange
Listener listener) удаляет зарегистрированный ранее слушатель событий listener для всех связанных свойств данного компонента.
Параметры:
listener объект, который должен быть удален из списка слушателей
Метод void firePropertyChange(String propertyName, Xxx old- Value, Xxx newValue) посылает событие PropertyChangeEvent об изменении связанного свойства зарегистрированным слушателям событий.
Параметры:
propertyName имя свойства
oldValueстарое значение
newValueновое значение
Ограниченные свойства
Ограниченные свойства представляют собой наиболее интересный и сложный тип свойств. Ограниченность такого свойства заключается в том, что любые слушатели событий могут наложить "запрет" на предлагаемые изменения значения свойства и вернуть его прежнее значение.
Рассмотрим два текстовых поля для ввода чисел, реализованные в виде компонента IntTextBean. Допустим, что значение введенного числа должно находиться между 0 и 255. Такое ограничение легко реализовать с помощью свойств minValue и maxValue компонента IntTextBean. Для вводимых чисел можно создать много других, более сложных, ограничений, например, число должно быть четным или зависеть от другого числа, значение которого также может изменяться. В компоненте IntTextBean нет такого свойства, которое может учесть все эти ограничения и определить корректность введенного числа. Вместо этого компонент JavaBeans уведомляет о новом значении и предоставляет возможность наложить запрет на новое значение. Свойство, на значение которого можно наложить запрет, называется ограниченным свойством.
Рассмотрим принцип работы ограниченного свойства на примере компонента RangeBean с ограничениями диапазона, показанного на рис. 8.13. Данный компонент содержит два поля IntTextBean для определения нижней и верхней границы диапазона. (Его можно использовать в диалоговом окне параметров печати, в котором пользователь может определять диапазон печатаемых страниц.) Если в поле to будет введено значение меньше текущего значения в поле from (или в поле from будет введено значение больше текущего значения в поле to), то компонент RangeBean запретит подобное изменение.
For» №r.«_2] i
From гTo лI
Рис. 8.13. Компонент RangeBean с ограничениями диапазона
В приведенном примере компонент IntTextBean является источником запрещаемого события, а компонент RangeBean — потребителем изменения свойства, который может наложить запрет.
Для создания ограниченного свойство нужно вставить в компонент RangeBean следующие два метода регистрации объектовVetoableChangeListener.
public void addVetoableChangeListener(VetoableChangeListener listener);
public void removeVetoableChangeListener(VetoableChangeListener listener);
Для управления слушателями изменений ограниченных свойств предусмотрен класс VetoableChangeSupport, который работает аналогично рассмотренному выше классу PropertyChangeSupport. Компонент JavaBeans в данном примере должен содержать объект этого класса.
private VetoableChangeSupport vetoSupport
= newVetoableChangeSupport(this) ;
Добавление и удаление слушателей событий должно осуществляться с помощью этого объекта, например так, как показано ниже.
public void addVetoableChangeListener(VetoableChangeListener listener) {
vetoSupport.addVetoableChangeListener(listener);
}
vetoSupport.removeVetoableChangeListener(listener);vetoSupport.removeVetoableChangeListener(listener);public void removeVetoableChangeListener(VetoableChangeListener listener) {
vetoSupport.removeVetoableChangeListener(listener);
vetoSupport.removeVetoableChangeListener(listener);
}
Класс JComponent поддерживает ограниченные свойства, но не так широко, как связанные свойства. Он содержит один общий список слушателей событий для обработки запрещаемых изменений всех свойств, а не отдельный список слушателей событий для каждого отдельного свойства. Причем, метод fireVetoableChange не перегружается для базовых типов данных. Если компонент наследует класс JComponent и имеет одно ограниченное свойство (так чаще всего и случается), то нет необходимости создавать объект VetoableChangeSupport, посколькудля этого достаточно использовать слушатели событий, предусмотренные в суперклассе JComponent.
Объект, способный наложить запрет на изменения свойства, должен реализовывать интерфейс VetoableChangeListener, который содержит один метод vetoableChange. void vetoableChange(PropertyChangeEvent event)
throws PropertyVetoException
Этот метод использует в качестве параметра объект PropertyChangeEvent, из которого можно извлекать текущее значение и предлагаемое новое значение с помощью методов getOldValue и getNewValue.
Обратите внимание, что метод vetoableChange может генерировать исключительную ситуацию PropertyVetoException, что означает неодобрение предлагаемого изменения со стороны слушателя. Эта исключительная ситуация заставляет компонент, генерирующий событие, отказаться от нового значения.
В данном примере реализация интерфейса VetoableChangeListener в коде компонента RangeBean содержит слушатели событий для текстовых полей to и from. Ниже показан код слушателя события для поля from.
public void vetoableChange(PropertyChangeEvent event)
throws PropertyVetoException
int v = ((Integer)event.getNewValue()).intValue();
if (v > to.getValueO )
throw new PropertyVetoException("from > to", event); }
Рекомендуется, чтобы ограниченные свойства также были связанными свойствами. Это означает, что компонент должен помимо методов, создающих объекты PropertyChangeListener, также содержать методы создания объектов Vetoable- ChangeListener, которые описаны в предыдущем разделе.
Для обновления значения ограниченного свойства компонент выполняет следующие действия.
Все слушатели запрещаемых событий уведомляются о попытке изменения значения свойства. (Для этого используется метод fireVetoableChange класса VetoableChangeSupport.)
Если ни один из слушателей запрещаемых событий не генерирует исключительную ситуацию PropertyVetoException, то значение свойства обновляется.
Затем все слушатели изменений свойства уведомляются о том, что данное изменение подтверждено.
Вот как выглядит типичный пример такого кода для компонента IntTextBean.
public void setValuednt v) throws PropertyVetoException
Integer oldValue = new Integer(getValue()) ;
Integer= new
oldValue, newValue);
// Нет исключительной ситуации, значит запрет не налагается.
valuev;
+ v);
oldValue, newValue );
}
Обратите внимание, что значение свойства не меняется до тех пор, пока оно не будет одобрено всеми зарегистрированными слушателями запрещаемых событий. И наоборот, слушатели запрещаемых событий всегда считают, что предлагаемое изменение еще не произошло. Таким образом, существует только один надежный способ уведомления о происходящем изменении, т.е. с помощью слушателя изменений связанных свойств.
В листинге 8.3 показан код компонента с текстовым полем для ввода целочисленных значений с учетом запретов на основе класса IntTextField из тома 1. В листинге 8.4 показан код компонента RangeBean, который содержит два текстовых поля IntTextBean и запрещает ввод чисел, которые лежат в недопустимом интервале значений.
Обратите внимание, что текстовое поле для ввода целочисленных значений содержит слушатель фокуса ввода. Он сохраняет исходное значение, когда текстовое поле получает фокус, и вызывает методкогда текстовое поле теряет фокус.
publicdefval, int size)
{
super("" + defval, size);
addFocusListener(new
FocusListener()
{
public void focusGained(FocusEvent event) {
if (I event.isTemporary()) lastValue =
} public voidevent)
{ if editComplete(); } }) ; }
Метод editComplete работает так же, как и методно при наличии за
прета на изменение свойства он восстанавливает исходное значение в текстовом поле и возвращает в него фокус ввода.
public void
{
Integer oldValue = new
Integer newValue = new
try
{
oldValue, newValue);
// Запрет не налагается
oldValue, newValue);
}
e)
{
// Нарушен какой-то запрет
JOptionPane.showMessageDialog(this , "" + е,
"Input Error", JOptionPane.WARNING—MESSAGE);
setText("" + lastvalue);
requestFocus();
}
}
Метод requestFocus не выполняется для компонентов Swing в JDK версии 1.2 из- за ошибки #4128659, описание которой можно найти в списке ошибок по адресу http://developer.java.sun.com.
Для демонстрации механизма запретов добавьте компонент RangeBean в форму. Затем в поле to введите заведомо неправильное число, которое превышает значение в поле from, и щелкните кнопкой мыши на форме вне поля to. После этого в поле fromсразу же будет возвращено прежнее корректное значение.
Ниже перечислены все происходящие при этом действия.
Ввод значения в текстовое поле from.
Щелчок на текстовом поле to.
Утрата фокуса ввода в поле from и вызов метода editComplete со стороны слушателя событий фокуса поля from.
Метод editComplete выполняет запрещаемое изменение свойства.
Поле to запрещает некорректное изменение.
Обработчик исключительной ситуации PropertyVetoException в методе editComplete выводит диалоговое окно с сообщением об ошибке (рис. 8.14), восстанавливает исходное значение и возвращает фокус ввода.
Рис. 8.14.Диалоговое окно с сообщением о запрете на изменение значения свойстваРис. 8.14.Диалоговое окно с сообщением о запрете на изменение значения свойства
Рис. 8.14.Диалоговое окно с сообщением о запрете на изменение значения свойства
Рис. 8.14.Диалоговое окно с сообщением о запрете на изменение значения свойства
Рассмотрим другой интересный случай. Вместо редактирования значений непосредственно в текстовом поле, используйте инспектор свойств для указания в поле from заведомо некорректного значения, которое превышает значение в поле to. Легко убедиться в том, что инспектор свойств не позволит сделать это, потому что метод setForm генерирует исключительную ситуацию с запретом такого изменения, и значение не будет установлено. В некоторых интегрированных средах разработки (но не в Forte 2.0) инспектор свойств выполняет роль слушателя запрещаемых событий. Таким образом, он может выдать сообщение об ошибке и восстановить в окне инспектора свойств исходное корректное значение.
Всякий раз, когда вы создаете ограниченное свойство в компоненте, имейте в виду, что компонент может иметь несколько слушателей событий, причем любое из них может наложить свой запрет на изменение свойства. Поэтому, вряд ли имеет смысл создавать ограниченное свойство, которое не является связанным. Слушатели собы-
тий должны быть уведомлены о подтверждении предложенного изменения и фактическом изменении свойства. По этой причине рекомендуется делать связанными все ограниченные свойства.
Листинг 8.3. IntTextBean.java
/**
©version 1.30 2001-08-20
©author Cay Horstmann
*/
import j ava.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import javax.swing.*;
import javax.swing.text.*;
/**
Компонент с текстовым полем для ввода чисел
*/
public class IntTextBean extends JTextField
{
public IntTextBean()
{
super("0", TEXTSIZE);
setinputverifier(new IntTextFieldVerifier());
addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
editComplete();
}
}) ;
addFocusListener(new
FocusListener()
{
public void focusGained(FocusEvent event) {
if (1 event.isTemporary()) lastValue = getValueO;
}
public void focusLost(FocusEvent event)
{
if (!event.isTemporary())
j editComplete() ;
}) ;'-
}
protected Document createDefaultModel()
{
return new IntTextDocument() ;
/**
Этот метод вызывается, когда текстовое поле теряет
фокус ввода или пользователь нажимает клавишу <Enter>
*/
public void editComplete()
{
Integer oldValue = new Integer(lastvalue);
Integer newValue = new Integer(getValue()) ;
try
{
fireVetoableChange("value", oldValue, newValue);
// Запрета нет
firePropertyChange("value", oldValue, newValue);
}
catch(PropertyVetoException e) {
// Нарушен какой-то запрет
JOptionPane.showMessageDialog(this, "" + e, "Input Error", JOptionPane.WARNING_MESSAGE) ;
setText("" + lastValue);
requestFocus();
// Работает не во всех версиях JDK // (см. ошибку #4128659)
)
/**
Проверяется корректность введенного в поле числа
@return true of the field contents is valid
/
public boolean isValid()
{
return IntTextDocuinent.isValid(getText());
/**
Возвращает численное значение содержимого поля
@param the number that the user typed into the field, or 0 if the field contents is not valid.
/
public int getValue()
{
try
{
return Integer.parselnt(getText() ) ;
}
catch(NumberFormatException e)
{
return 0 ;
}
/**
Задает значение для ограниченного свойства
@param v the new value
/
public void setValue(int v) throws PropertyVetoException
{
Integer oldValue = new Integer(getValue()) ;
Integer newValue = new Integer(v);
fireVetoableChange("value", oldValue, newValue);
// Запрета нет.
setText("" +v);
firePropertyChange("value", oldValue, newValue); }
public Dimension getPreferredSize()
{
return new Dimension(XPREFSIZE, YPREFSIZE);
}
private int lastValue;
private static final int XPREFSIZE = 50;
private static final int YPREFSIZE = 20;
private static final int TEXTSIZE = 10;
/**
Документ, который может содержать только корректные значения или фрагменты их представления в виде строки
*/
class IntTextDocument extends PlainDocument {
public void insertString(int offs, String str, AttributeSet a)
throws BadLocationException
{
if (str == null) return;
String oldstring = getText(0, getLength() ) ;
String newString = oldstring.substring(0, offs) + str + oldstring.substring(offs) ;
if (canBecomeValid(newString)) super.insertString(offs, str, a); }
/**
Вспомогательная функция, которая проверяет корректность числа в данной строке @param s a string
Sreturn true if s is a valid integer
*/
public static boolean isValid(String s)
{
try
{
Integer.parselnt(s) ;
return true;
}
catch (NuinberFormatException e) {
return false;
}
}
/**
Вспомогательная функция, которая проверяет, является ли данная строка подчиненной строкой представления в виде строки корректного числа @param s a string
©return true if s can be extended to a valid integer */
public static boolean canBecomeValid(String s) {
return s.equals("") || s .equals ("-") j| isValid(s) ; }
/**
Проверка корректности числа в текстовой строке
*/
class IntTextFieldVerifier extends Inputverifier {
public boolean verify(JComponent component) {
String text = ((JTextComponent)component).getText(); return IntTextDocument.isValid(text) ;
}
}
Листинг8.4. RangeTextBean.java
/**
©version 1.21 2001-08-20
©author Cay Horstmann
*/ import j import java.beans.*; import java.io.*;
import javax.swing.*;
/**
Компонент с двумя текстовыми полями для
указания диапазона допустимых значений */
public class RangeBean extends JPanel {
public RangeBean()
{
add(new JLabel("From"));
add(from);
add(new JLabel("To")); add(to);
from.addVetoableChangeListener(new VetoableChangeListener()
{ public void vetoableChange(PropertyChangeEvent event)
throws PropertyVetoException {
int v = ((Integer)event.getNewValue()).intValue(); if (v > to.getValue())
throw new PropertyVetoException("from > to", event);
} }) ;
to.addVetoableChangeListener(new VetoableChangeListener()
public void vetoableChange(PropertyChangeEvent event) throws PropertyVetoException
{
int v = ((Integer)event.getNewValue()).intValue(); if (v < from.getValue())
throw new PropertyVetoException("to < from", event);
}) ;
/**
Задает значение свойства from
©param v the new lower bound of the range
*/
public void setFrom(int v) throws PropertyVetoException {
from.setvalue(v);
/**
Возвращает значение свойства from
@return the lower bound of the range
*/
public int getFrom() { return from.getValue(); }
/**
Задает значение свойства to
©param v the new upper bound of the range.
*/
public void setTo(int v) throws PropertyVetoException {
to.setValue(v) ;
Возвращает значение свойства to @return the upper bound of the range */ public int getTo() { return to.getValue(); }
private IntTextBean from = new IntTextBean(); private IntTextBean to = new IntTextBean();
)
E
p yfjava. beans. VetoableChangeLi stener
Метод void vetoableChange (PropertyChangeEvent event) вызывается при попытке изменения свойства. Он генерирует исключительную ситуацию PropertyVetoException, если такое изменение недопустимо.
Параметры: eventобъект события, которое выражает изменение свойства
B
java.beans.VetoableChangeSupport
Конструктор VetoableChangeSupport (Object sourceBean) создает —.1объект запрещаемого изменения VetoableChangeSupport.
Параметры:
sourceBean компонент-источник запрещаемого изменения свойства (обычно this)
e Метод void addVetoableChangeListener (VetoableChangeListener listener) регистрирует слушателя события для ограниченного свойства.
Параметры:
listener слушатель события, накладывающий запрет на ограниченное свойство
Метод void removeVetoableChangeListener (VetoableChangeLis- tener listener) удаляет зарегистрированный слушатель события для ог- раниченногосвойства.
Параметры:
listener удаляемый слушатель события
Параметры: propertyName oldValue newValueПараметры: propertyName oldValue newValueМетод void fireVetoableChange(String propertyName, Object oldValue, Object newValue) посылает событие PropertyChangeEvent для обработки блокам прослушивания события перед изменением значения свойства.
Параметры: propertyName oldValue newValue
Параметры: propertyName oldValue newValue
имя свойства
текущее значение
предлагаемое новое значение
j avax. swing. JComponent
Метод void addVetoableChangeListener (VetoableChangeListener listener) регистрирует слушателя события для всех ограниченных свойств компонента.
Параметры:
listener слушатель события, накладывающий запрет на ограниченное свойство
Метод void removeVetoableChangeListener (VetoableChangeLis- tener listener) удаляет зарегистрированный ранее слушатель события для ограниченного свойства компонента.
Параметры:
listener удаляемый слушатель события
Параметры: propertyName oldValue newValueПараметры: propertyName oldValue newValueМетод void fireVetoableChange(String propertyName, Object oldValue, Object newValue) посылает событие PropertyChangeEvent для обработки блокам прослушивания события перед изменением значения свойства.
Параметры: propertyName oldValue newValue
Параметры: propertyName oldValue newValue
имя свойства
текущее значение
предлагаемое новое значение
javax.beans.PropertyVetoException
Конструктор PropertyVetoException (String reason, PropertyChangeEvent event) создает новую исключительную ситуацию PropertyVetoException.
Параметры:
reasonстрока, описывающая причину запрета
eventсобытие PropertyChangeEvent изменения ограниченного
свойства, на которое налагается запрет
Метод PropertyChangeEvent getPropertyChangeEvent() возвращает событие PropertyChangeEvent, используемое для создания исключительной ситуации.
Создание пользовательских событий
При создании ограниченного или связанного свойства предполагается, что компонент JavaBeans может генерировать события при каждой попытке изменения значения свойства. Однако, помимо таких событий существуют другие события, которые может генерировать компонент.
После щелчка кнопкой мыши на элементе управления внутри компонента.
При поступлении новой информации.
Просто по истечении некоторого промежутка времени.
Как и события PropertyChangeEvent, эти события также должны перехваты- ватьсяспециальными пользовательскими (custom) слушателями.
Рассмотрим следующий способ создания компонента JavaBeans, который способен генерировать пользовательские события. (Более подробно обработка событий на платформе Java рассматривается в главе 8 тома 1.) Учтите, что следует точно выполнять перечисленные ниже первые два пункта, потому что в противном случае механизм интроспекции не сможет определить, что разработчик определяет пользовательское событие.
Создайте класс CustomEvent на основе класса EventObject. (Название события должно оканчиваться на Event, чтобы интегрированная среда разработки могла распознать его, используя соглашения об именах .)
Создайте интерфейс CustomListenerс одним или несколькими методами уведомления. Эти методы могут иметь произвольные имена, но должны иметь один параметр типа CustomEvent и возвращать тип void.
Добавьте в компонентJavaBeans следующие два метода:
public void addCustomListener(CustomListener e)
public void removeCustomListener(CustomListener e)
Если класс события не расширяет класс Eventobject,он все равно будет безошибочно компилироваться, потому что на самом деле методы класса Eventobjectне используются. Кроме того, никакие другие методы даже не пытаются выполнять приведение объектов-событий к типу EventObject. Однако, работать такой компонентJava- Beansнебудет,потомучтомеханизминтроспекциинесможетраспознатьсобытия.
При реализации методов, необходимых для создания и удаления пользовательских событий, уже нельзя полагаться на имеющиеся классы, которые позволяют автоматически управлять слушателями событий. Такие слушатели событий придется организовать самостоятельно. В главе 8рассмотрен класссозданный именно для этой цели. Посколькусобытий может реализовы
вать несколько интерфейсов слушателей событий, то элемент списка слушателей события определяется объектом класса интерфейса и объектом-слушателем.
Реализация методов создания и удаления слушателей выглядит очень просто.
public voidlistener)
{
listener);
}
public voidlistener)
{
listener);
}
Для каждого метода интерфейса-слушателя нужно обеспечить метод генерации событий.
public voidevent)
{ listeners = class)
for (int i = 0; i <i++)
{ listener =
;
}'
JavaBeans, этот метод не является обязательным, он только упрощаетсобытий всем заинтересованным слушателям.
Теперь давайте применим этот способ для создания компонента-таймера должен посылать объекты-событиясвоим слушате
лям. (Данный пример является модификацией класса Timer, рассмотренного в главе 8 тома 1.) Еслиrunning имеет значение true, то таймер регулярно гене
рирует события через одинаковые промежутки времени, измеряемые в миллисекундах. Длина интервалов задается с помощью свойства interval. В листингах 8.5-8.7 приведен код следующих компонентов.
Класс TimerEvent — это пользовательское событие, которое генерируется данным компонентом (листинг 8.5).
Класс— это класс-слушатель, содержащий метод уведомления
timeElapsed (листинг
Класс TimerBean— это класс-компонент с методами создания и удаления слушателейи removeTimerListener (листинг 8.7).
Рассмотрим теперь последовательность действий, необходимых для тестирования компонента в интегрированной среде разработки Forte.
Создайте новое приложение на основе фрейма JFrame, как описывалось ранее. Перетащите мышью компонент TimerBean в область фрейма. Это 'нгвиз^ал'ъный компонент, т.е. он не расширяет класс Component, поэтому будет находиться в инспекторе свойств в разделе Non-visual Components (Невизуальные компоненты), как показано на рис. 8.15.
Задайте для свойства running значение true.
2001:TimerEvent(5oun 2001:TimerEventlsoun 2OOl:TlmerEvent[sourc 2001:TimerEvent(sour( 2001:TimerEvent[$ourc 2001:TimerEvent[5our< 2001:TimerEvent($ourc2001:TimerEvent(5oun 2001:TimerEventlsoun 2OOl:TlmerEvent[sourc 2001:TimerEvent(sour( 2001:TimerEvent[$ourc 2001:TimerEvent[5our< 2001:TimerEvent($ourcrue Aug 21 16:16:01 GMT- 08:00 rue Aug 21 16: 16:02GMT-08:00 rue Aug 21 16 16:03 GMT- 08:00 rue Aug 21 16:16:04 GMT-08:00 rue Aug 21 16:16.05 GMT- 08:00 rue Aug 21 16 16:06 GMT- 08:00 rueAug 21 16 16:07 GMT-08:00rue Aug 21 16:16:01 GMT- 08:00 rue Aug 21 16: 16:02GMT-08:00 rue Aug 21 16 16:03 GMT- 08:00 rue Aug 21 16:16:04 GMT-08:00 rue Aug 21 16:16.05 GMT- 08:00 rue Aug 21 16 16:06 GMT- 08:00 rueAug 21 16 16:07 GMT-08:00? Q Non-visualComponents!Jtim'erteanl [TlmerteanJ^Borderlayout? Q Non-visualComponents!Jtim'erteanl [TlmerteanJ^BorderlayoutРис. 8.15. Невизуальный компонент JavaBeansРис. 8.15. Невизуальный компонент JavaBeansгue 21 16 15:54 GMT- 8:00 26Ol:TimerEvent[sourc;*ij TueAug 21 16 15:56 GMT-08:00 2001:TimerEvent{seurdM FueAug 21 16 15:57 MT-08:00 2001:Т1тегЕует[$оигеЦ rue Aug 21 16: 15:58 GMT- 08:00 2OOl:TimerEvent[sourc$ rue Aug 21 16 15:59 GMT- 8'00 ?00] TimerFwenrj<ourr^| rue Aug 21 16 16:00 GMT- 08:00 2001:TimerEventi$ouroB
2001:TimerEvent(5oun 2001:TimerEventlsoun 2OOl:TlmerEvent[sourc 2001:TimerEvent(sour( 2001:TimerEvent[$ourc 2001:TimerEvent[5our< 2001:TimerEvent($ourc
2001:TimerEvent(5oun 2001:TimerEventlsoun 2OOl:TlmerEvent[sourc 2001:TimerEvent(sour( 2001:TimerEvent[$ourc 2001:TimerEvent[5our< 2001:TimerEvent($ourc
rue Aug 21 16:16:01 GMT- 08:00 rue Aug 21 16: 16:02GMT-08:00 rue Aug 21 16 16:03 GMT- 08:00 rue Aug 21 16:16:04 GMT-08:00 rue Aug 21 16:16.05 GMT- 08:00 rue Aug 21 16 16:06 GMT- 08:00 rueAug 21 16 16:07 GMT-08:00
rue Aug 21 16:16:01 GMT- 08:00 rue Aug 21 16: 16:02GMT-08:00 rue Aug 21 16 16:03 GMT- 08:00 rue Aug 21 16:16:04 GMT-08:00 rue Aug 21 16:16.05 GMT- 08:00 rue Aug 21 16 16:06 GMT- 08:00 rueAug 21 16 16:07 GMT-08:00
? Q Non-visualComponents
!Jtim'erteanl [TlmerteanJ
^Borderlayout
? Q Non-visualComponents
!Jtim'erteanl [TlmerteanJ
^Borderlayout
Рис. 8.15. Невизуальный компонент JavaBeans
Рис. 8.15. Невизуальный компонент JavaBeans
TueAug 21 16 16:08 GMT- 08.00 2001:TimerEventf$ourci№ TueAug 21 16 16:09 GMT- 08:00 2001:TlmerEvent[sourc81 rue Aug 21 16: 16:10GMT-08:00 2OOl:TimerEvent[sourdS| FueAug 2 1 16 16:11 GMT-08:00 2001:TimerEvent[sour(^ rue Aug 21 16: 16:12 GMT- 08:00 2001;TimerEventi$ourt^ rue Aug 21 16: 16:13 GMT- •08:00 2OOl:TjmerEvent[souri^| rueAuj 2116 16:14 GMT- 08:00 2001:TimerEvent[5ourc^
Рис.8.16. Список сообщений о пользовательских событиях
Создайтепанельс полосойпрокруткив центрефрейма,перетаскиваяееиз
вкладки Swing на панели компонентов.
Создайте текстовую область внутри панели с полосой прокрутки.
Вставьте событие timeElapsed в метод updateTextArea и введите в нем следующую строку кода.
j TextAreal.append(evt + "\n");
Откомпилируйте и запустите приложение.
Как показано на рис. 8.16, текстовая область должна заполняться списком сообщений. Если сообщения в текстовой области не прокручиваются вверх, то в таком случае двойным щелчком установите для свойства running значение true.
Для компонента TimerBean можно создать более забавный внешний вид, связывая его с другим компонентом EventMonitorBean. После этого можно запускать и останавливать таймер, переключая свойство running в редакторе свойств.
Ниже представлен полный код компонента TimerBean.
Листинг 8.5. TimerBean.java
@version 1.11 2001-08-20
©author Cay Horstmann
import java.io.* ;
import java.util.*;
import j avax.swing.event.*;
/**
Невизуальный компонент, который посылает события таймера
*/
public class TimerBean implements Serializable
{
public TimerBean()
{
jlistenerList = new EventListenerList();
/**
Установка значения свойства interval
@param i the interval between timer ticks (in milliseconds)
*/
public void setinterval(int i) { interval = i; }
/**
Возвращение значения свойства interval
©return the interval between timer ticks (in milliseconds)
*/
public int getlnterval() { return interval; }
/**
Установка значения свойства running
@param b true if the timer is running
*/
public void setRunning(boolean b)
{
if (b && runner == null)
{
if (interval <= 0) return; runner = new
Thread()
{ public void run()
{
try
{
while (!Thread.interrupted())
{
Thread.sleep(interval);
fireTimeElapsed(new TimerEvent(this));
)
}
catch(InterruptedException e) {
} }
};
runner.start();
}
else if (!b && runner != null)
{
runner.interrupt();
runner = null;
}
)
/“
Возвращение значения свойства running ©return true if the timer is running */ public boolean isRunning() { return runner != null; }
/**
Создание слушателя таймера
@param listener the listener to add
*/
public void addTimerListener(TimerListener listener) {
listenerList.add(TimerListener.class, listener);
}
/ * *
Удаление слушателя таймера
@param listener the listener to remove
*/
public void removeTimerListener(TimerListener listener) {
listenerList.add(TimerListener.class, listener);
}
/**
Передача события таймера всем слушателям @param event the event to send
*/
public void fireTimeElapsed(TimerEvent event) {
EventListener[] listeners = listenerList.getListeners( TimerListener.class);
for (int i = 0; i < listeners.length; i++) {
TimerListener listener = (TimerListener)listeners[i], listener.timeElapsed(event);
)
private int interval = 1000;
private EventListenerList listenerList;
private Thread runner;
}
Листинг8.6. TimerListener.java
/**
©version 1.10 1997-10-27
©author Cay Horstmann
*/
import java.util. * ;
/**
Интерфейс-слушатель для уведомления об истечении интервала */ public interface TimerListener extends EventListener {
Этот метод вызывается каждый раз по истечении интервала между уведомлениями @param evt the timer event that contains the time of notification
*/
public void timeElapsed(TimerEvent evt);
Листинг8.7. TimerEvent.java
/**
©version 1.11 2001-08-21
©author Cay Horstmann
*/
import java.util.*;
/**
Класс с описанием событий таймера
*/
public class TimerEvent extends Eventobject {
/**
Создание события таймера
@param source the event source
*/
public TimerEvent(Object source) {
super(source);
now = new Date();
}
/**
Возвращает дату и время создания события ©return the construction date and time
*/
public Date getDate()
{
return now; }
public String toString()
{
return now + ":" + super.toString()■
private Date now;
}
Редакторы свойств
При добавлении в компонент свойства типа integer или String оно автоматически отобразится в инспекторе свойств. Что же произойдет при добавлении свойства более сложного типа, значения которого не так просто отредактировать в текстовом поле, например, свойство-дата или свойство-цвет типа Color? Для этого необходимо создать cnc-
циальный компонент, с помощью которого пользователь смог бы редактировать значения этихсвойств.Такиекомпонентыназываются редакторами свойств (property editors). Например, редактор свойства-даты может быть представлен в виде календаря, в котором пользователь выбирает дату. Редактор свойств для редактирования цвета может быть в виде палитры, в которой пользователь может выбирать нужные цвета.
На самом деле в интегрированной среде разработки Forte есть редактор свойств для редактирования цветов, который показан на рис. 8.9. В нем также имеются редакторы свойств для объектов базовых типов, таких как String (текстовое поле) и boolean (список со значениями true и false). Эти редакторы свойств регистрируются с помощью диспетчера редакторов свойств.
Процесс создания нового редактора свойств состоит из нескольких этапов. Сначала следует создать информационный класс, в котором будет содержаться разнообразная информация о данном компоненте. Например, если имеется свойство с типом int или String, значения которого каким-то образом ограничены, то вряд ли захочется использовать для него универсальный редактор свойств, предусмотренный в интегрированной среде разработки. Вместо него удобнее использовать специальный редактор данного свойства, определение которого будет приведено в информационном классе компонента.
Название информационного класса компонента следует начинать с названия компонента и заканчивать суффиксом Beanin о. Этот класс должен либо реализовывать интерфейс Beanlnf о с восемью методами, либо наследовать класс SimpleBeanln о, что проще. При наследовании класса SimpleBeanlnf о совсем не обязательно реализовывать восемь методов.
//Информационный класс для компонента ChartBean class ChartBeanBeanlnfo extends SimpleBeanlnfo {
}
Для создания специального редактора компонента необходимо переопределить метод getPropertyDescriptors, который возвращает массив объектов PropertyDescriptors. Необходимо создать по одному такому объекту для каждого свойства, которое должно отображаться в окне редактора свойств. Объекты создаются даже для тех свойств, которые используют предлагаемые по умолчанию редакторы
PropertyDescriptor descriptor
= new Property-Descriptor("titlePosition", ChartBean.class);
Для вызова специального редактора свойства следует использовать метод setPropertyEditorClass класса PropertyDescriptor.
descriptor.setPropertyEditorClass(TitlePositionEditor.class);
Затем нужно создать массив описаний для свойств вашего компонента. Например, компонент ChartBean, который рассматривается в этом разделе, имеет следующие четыре свойства.
Свойство title типа String.
Свойство titlePosition типа int.
Свойство values типаdouble [].
Свойство inverse типа boolean.
На рис. 8.17 показан внешний вид компонента ChartBean. Вверху располагается заголовок (свойство title), который можно разместить слева, справа или по центру. Значения свойства values определяют высоту столбцов диаграммы. Если свойство inverse имеет значение true, то фон становится цветным, а столбцы диаграммы — белыми. В листинге 8.8 приведен код компонента ChartBean, который является простой модификацией аплета-диаграммы из главы 10 тома!.
Non-visual Components' ■! BorderLayoute- _1<hartSeinl[Charteean)Non-visual Components' ■! BorderLayoute- _1<hartSeinl[Charteean)8.17. Компонент ChartBean8.17. Компонент ChartBeanРис. Инспектор свойств компонента ChartBeanРис. Инспектор свойств компонента ChartBean
Non-visual Components
' ■! BorderLayout
e- _1<hartSeinl[Charteean)
Non-visual Components
' ■! BorderLayout
e- _1<hartSeinl[Charteean)
8.17. Компонент ChartBean
8.17. Компонент ChartBean
Рис. Инспектор свойств компонента ChartBean
Рис. Инспектор свойств компонента ChartBean
В листинге 8.9 показан код классао с определениями редакто
ров свойств компонента ChartBean.
Редактор DoubleArrayEditor регистрируется как редактор для массива double[]•
Метод getPropertyDescriptors возвращает описание для каждого свойства. Для свойств title и graphColor используются редакторы свойств, предлагаемые по умолчанию в интегрированной среде разработки.
Свойства titlePosition, values и inverse используют специальные редакторы TitlePositionEditor,и InverseEditor соответственно.
На рис. 8.18 показан внешний вид инспектора свойств с этими редакторами. В следующем разделе представлены способы реализации этих видов редакторов.
Редакторы свойств можно также создавать с помощью статического метода классапередавая редактор свойства ме
тоду как параметр.
PropertyEditorManager.registerEditor(Date.class,
Calendarselector.class);
Для предлагаемогоумолчанию редактора не нужно вызывать метод
terEditor, поскольку это автоматически должно быть выполнено интегрирован-
D ной средой разработки.
Для проверки наличия редактора свойств в интегрированной среде разработки можно использовать метод indEditor класса PropertyEditorManager. Этот метод выполняет следующие действия.
Сначала он определяет, какие редакторы свойств уже зарегистрированы. (Это могут быть редакторы, предлагаемые в интегрированной среде разработки и зарегистрированные явно методом registerEditor.)
Затемвыполняетсяпоисккласса,имякоторогосостоитизназваниятипа и
суффикса Edi tor.
Ъ. Если такой класс не обнаружен, то метод findEditor возвращает null.
Например, если класс. CalendarSelector зарегистрирован для объектов java. util .Date, то он может использоваться для редактирования свойства-даты типа Date. В противном случае будет предпринят поиск редактора свойств с именем java.util.DateEditor.
Листинг 8.8. ChartBean.java
/**
Aversion 1.30 2001-08-21
©author Cay Horstmann
*/
import j ava.awt.*;
import j ava.awt.font.*;
import java.awt.geom.*;
import java.util.*;
import java.beans.*;
import java.io.*;
import javax.swing.*;
/**
Компонент-диаграмма
*/
public class ChartBean extends JPanel
{
public void paint(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
if (values == null || values.length == 0) return; double minValue =
double maxValue = 0;
for (int i = 0; i < values.length; i++) {
if (minValue > getValues(i)) minValue = getValues(i) if (maxValue < getValues(i)) maxValue = getValues(i) ;
}
if (maxValue == minValue) return;
Dimension d = getsize();
Rectangle2D bounds = getBounds();
double clientwidth =
double clientHeight = bounds.getHeight();
double barWidth = clientWidth / values.length;
g2.setColor(inverse ? color : Color.white);
g2.fill(bounds);
g2.setColor(Color.black);
Font titleFont = new Font("SansSerif", Font. BOLD, 20);
FontRenderContext context = g2.getFontRenderContext(); Rectangle2D titleBounds
=context);
double titleWidth = titleBounds.getWidth();
double у = —titleBounds.getY();
double x;
if (titlePosition == LEFT)
x = 0;
else if (titlePosition == CENTER)
x = (clientwidth — titleWidth) / 2;
else
x = clientWidth - titleWidth;
g2.setFont(titleFont);
g2.drawString(title, (float)x, (float)y);
double top = titleBounds.getHeight();
double scale = (clientHeight — top)
/ (maxValue —
у = clientHeight;
for (int i = 0; i < values. length; i++) {
double xl = i * barWidth + 1;
double yi = top;
double value = getValues(i) ;
double height = value * scale;
if (value >= 0)
yl += (maxValue — value) * scale;
else
{
yl += (int)(maxValue * scale); height = —height;
}
g2.setColor(inverse ? Color.white : color);
Rectangle2D bar = new Rectangle2D.Double(xl, yl, barWidth - 2, height);
g2.fill(bar);
g2.setColor(Color.black);
g2.draw(bar);
}
/**
Установка значения свойства title
©param t the new chart title.
*/
public void setTitle(String t) { title = t; }
/**
Возвращение значения свойства title
@return the chart title.
*/
public String getTitle() { return title; }
/**
Установка значений индексированного свойства ©param v the values to display in the chart. */
public void setvalues(double[ ] v) { values = v; }
/**
Возвращение значений индексированного свойства ©return the values to display in the chart.
/
public double[] getValues() { return values; }
/**
Установка значений индексированного свойства
©param i the index of the value to set ©param value the new value for that index
/
public void setValues(int i, double value) {
j if (0 <= i && i < values.length) values[i] = value;
/**
Возвращение значений индексированного свойства ©param i the index of the value to get ©return the value for that index
/
public double getValues(int i)
{
if (0 <= i && i < values.length) return values[i]; return 0;
/**
Установка значения свойства inverse
©param b true if the display is inverted (white bars on colored background)
/
public void setinverse(boolean b) { inverse = b; }
/**
Возвращение значения свойства inverse ©return true if the display is inverted
/
public boolean islnverse() { return inverse; }
/**
Установка значения свойства titlePosition.
©param p LEFT, CENTER, or RIGHT
/
public void setTitlePosition(int p) { titlePosition = p; }
/**
Возвращение значения свойства titlePosition
©return LEFT, CENTER, or RIGHT
/
public int getTitlePosition() { return titlePosition; }
/**
Установка значения свойства graphColor
©param с the color to use for the graph
/
public void setGraphColor(Color c) { color = c; }
Возвращение значения свойства graphColor @param с the color to use for the graph
*/
public Color{ return color; }
public Dimension getPreferredSize() {
return newYPREFSIZE);
private static final int LEFT = 0;
private static final int CENTER = 1;
private static final int RIGHT = 2;
private static final int XPREFSIZE = 300;
private static final int YPREFSIZE = 300;
private doublet] values = { 1, 2, 3 } ;
private String title = "Title";
private int titlePosition = CENTER;
private boolean inverse;
private Color color =
Листинг8.9.Java
/**
1.10 1997-10-27
Cay
*/
import
/**
Информационный класс для
с перечнем редакторов свойств
*/
public classextends
{
public
{
try
{
titlePositionDescriptor
= new
ChartBean.class);
titlePositionDescriptor.setPropertyEditorClass( TitlePositionEditor.class);
PropertyDescriptor inverseDescriptor = new
ChartBean.class);
inverseDescriptor.setPropertyEditorClass( InverseEditor.class);
PropertyDescriptor
new
;
valuesDescriptor.setPropertyEditorClass(
;
return new
new PropertyDescriptor("title", ChartBean.class),
titlePositionDescriptor,
valuesDescriptor,
new PropertyDescriptor("graphcolor”, ChartBean.class),
inverseDescriptor
}
catch(IntrospectionException e)
{
e.printStackTrace();
return null;
}
[a P I]
java.beans.PropertyEdi torManager
Метод static PropertyEditor findEditor(Class targetType) возвращает редактор свойства для объекта заданного типа или значение null, если не удалось найти такой зарегистрированный редактор.
Параметры:
targetTypeобъект типа Class, который содержит редактируемый
тип, например, Class.Color
Метод static void registerEditor(Class targetType, Class editorclass) регистрирует класс редактора значений заданного типа.
Параметры:
targetTypeобъект типа Class, который содержит редактируемый тип
editorClassобъект типа Class, который содержит класс-редактор
(при передаче значения nullбудет снята регистрация
|A?l|
текущего редактора)
java.beans.PropertyDescriptor
Ji
W Mt Z-CWV
МетодPropertyDescriptor(String name, Class beanClass) создает объект PropertyDescriptor с описанием свойства.
Параметры:
nameимя свойства
beanClass класс компонента, к которому относится свойство
Метод void setPropertyEditorClass(Class editorClass) задает класс редактора свойства.
Параметры:
editorClass объект Class, который содержит класс-редактор
java, beans .Beaninfo
Метод PropertyDescriptor[] getPropertyDescriptors() возвра-
щает описание всех свойств, которое должно отображаться в инспекторе свойствданногокомпонента.
Создание редактора свойств
Прежде чем приступить к созданию редактора свойств, следует подчеркнуть, что он может иметь очень сложную структуру, несмотря на то, что работает со значением только одного типа данных. Например, редактор шрифтов (который редактирует объекты типа Font) предлагает примеры использования шрифтов, чтобы упростить задачу выбора наиболее подходящего шрифта.
Каждый редактор свойств должен реализовывать интерфейс Property-Editor, содержащий 12 методов. Как и при использовании интерфейса Beanin о, можно наследовать класс PropertyEditorSupport, который предусмотрен в стандартной библиотеке. Этот класс содержит методы для добавления и удаления слушателей событий изменения свойства и реализует все методы интерфейса PropertyEditor. Например, код редактора для заголовка компонента ChartBean сначала имеет следующий вид.
// Класс редактора заголовка
class TitlePositionEditor extends PropertyEditorSupport
}
Обратите внимание, что если класс редактора свойства имеет конструктор, то он также должен иметь используемый по умолчанию конструктор, т.е. без аргументов.
Наконец, учтите, что редактор свойства находится под управлением интегрированной среды разработки, а не компонента. Для отображения текущего значения свойства интегрированная среда разработки выполняет следующие действия.
Инициализирует редакторы всех свойств компонента.
Запрашивает у компонента текущее значение свойства.
Затем предлагаетредактору свойства отобразить это значение.
Для отображения значения редактор свойств может использовать текстовые или графические методы. Более подробно эти методы рассматриваются ниже.
Простыередакторысвойств
Простой редактор свойств работает с текстовыми строками. Для этого нужно переопределить методы setAsText и getAsText. Например, наш компонент ChartBean имеет свойство для размещения заголовка: слева, по центру или справа. Значения этих вариантов размещения хранятся в виде констант.
private static final int LEFT = 0;
private static final int CENTER = 1;
private static final int RIGHT = 2;
Однако, чтобы такой пользовательский интерфейс не вызывал отвращения у пользователей, не рекомендуется в текстовом поле просто отображать эти номера 0, 1,2. Вместо этого нужно определить редактор свойств, возвращающий тип размещения заголовка в виде строки с помощью метода getAsText. Для нахождения значения свойства метод getAsText вызывает метод getValue класса PropertyEditor. Этот универсальный метод возвращает значение типа Obj ect. Если свойство имеет базовый тип, то возвращается
соответствующий объект-оболочка. В нашем случае свойство имеет тип int, а потому вызов функции getValue возвращает объект типа Integer.
class TitlePositionEditor extends PropertyEditorSupport {
public String getAsText()
{
int i = ((Integer)getVelue()).intvalue();
if (0 <= i && i < options . length) return options[i] ; return "";
}
private String[] options = { "Left", "Center", "Right"};
}
Теперь текстовое поле отображает одно из значений Left, Center или Right. Когда пользователь редактирует текстовое поле, вызывается метод setAsText, который обновляет значения свойства посредством вызова метода setvalue. Метод setValue также универсальный, поэтому его параметр имеет тип Object. Для установки значения числового типа (integer, double и др.) нужно передавать ему соответствующий объект-оболочку.
public void setAsText(String s) {
for (int i = 0; i < options.length; i++) {
if (options[i].equals(s)) {
setValue(new Integer(i));
return;
}
} }
На самом деле, чтобы пользовательский интерфейс понравился пользователям, для редактирования свойства titlePosition лучше не использовать данный редактор свойств. Дело в том, что пользователю не известно, какие варианты установок допустимы. Поэтому лучше предоставить ему возможность просмотра всех вариантов расположения заголовка, как показано на рис. 8.19, для чего можно использовать класс PropertyEditorSupport, который предоставляет простой метод для отображения всех вариантов расположения заголовка в редакторе свойств. С этой целью метод getTags возвращает массив всех допустимых значений.
public String[] getTags() { return options; }
griphColor В inverse , I
title titlePosition values
fed
Normal
Diameter of t I
Сел...iSr \
Left «WWW
LRjflht
00
Рис 8 19 Редактор свойств TitlePositionEditor
По умолчанию метод getTags возвращает null, а возвращение ненулевого значения означает поле со списком вариантов расположения заголовка.
Метод getAsText просто возвращает значения, отображаемые в поле со списком, а методы getAsText и setAsText преобразовывают тип данных свойства (который может быть строкой, числом или любым другим типом) в строку и обратно.
В листинге 8.10 приведен полный код этого редактора свойств.
Листинг 8.10.TitlePositionEditor.java
/**
©version 1.10 1997-10-27
©author Cay Horstmann
*/
import java.beans.* ;
Пользовательский редактор свойства titlePosition компонента ChartBean, который позволяет выбирать значения
Left, Center и Right
*/
public class TitlePositionEditor extends PropertyEditorSupport {
public String getAsText()
{
int value = ((Integer)getValue()).intValue() ; return options[value];
)
public void setAsText(String s)
{ for (int i 0; i < options.length; i++) {
if (options[i].equals(s)) {
setValue(new Integer(i));
return;
}
}
}
public String[] getTags() { return options; }
private String[] options = { "Left", "Center", "Right" } ;
java.beans.PropertyEditorSupport
Метод Object getValueО возвращает текущее значение свойства. При этом базовые типы преобразуются в соответствующие объекты-оболочки.
Методvoid setValue(Object newValue) задает значение свойства. При этом базовые типы преобразуются в соответствующие объекты-оболочки.
Параметры:
newValue новое значение объекта
Метод String getAsText () возвращает строковое представление текущего значения свойства. Этот метод можно переопределить для каждого используемого типа. По умолчанию возвращает значение null, что означает невозможность представления свойства в виде строки.
Метод void setAsText (String text) задает для свойства новое значение, которое получается в результате синтаксического анализа строки text. Этот метод можно переопределить для каждого используемого типа. Если текст нельзя преобразовать в допустимое значение или тип свойства не может быть выражен в виде строки, то генерируется исключительная ситуация IllegalArgumentException.
Метод String[] getTags () возвращает массив всех допустимых строковых представлений значений свойства для отображения в поле со списком. Данный метод можно переопределить для каждого используемого типа. По умолчанию возвращает значение null, что означает отсутствие конечного числа строковых значений.
Визуальные редакторы свойств
Многие сложные типы свойств не могут быть выражены в текстовом виде. В таком случае небольшая область, которую занимает текстовое поле или поле со списком, инспектора свойств будет содержать графическое представление текущего значения. После щелчка на этой области появляется стандартное диалоговое окно для редактирования, например такое, как показано на рис. 8.20. В этом диалоговом окне содержится компонент, предназначенный для редактирования значений свойства, предлагаемых редактором свойства, а также кнопки, предлагаемые интегрированной средой разработки.
Рис. 8.20. Окно пользовательского редактора свойств
Для создания визуального редактора свойств выполните следующие действия.
Сообщите интегрированной среде разработки, что нужно нарисовать значение, а не использовать для него текст.
"Нарисуйте" значение, введенное в графическом интерфейсе пользователя, т.е. GUI-интерфейсе.
Сообщите интегрированной среде разработки, что для редактирования свойства будет использоваться визуальный редактор.
Создайте GUI-интерфейс визуального редактора.
Создайте код для проверки значения, введенного пользователем.
На первом этапе переопределите метод getAsText интерфейса PropertyEditor так, чтобы он возвращал значение null, а также переопределите метод isPaintable так, чтобы он возвращал значение true.
public String
{
return null;
}
public boolean {
return true;
}
Затем реализуйте метод paintValue, который получает графический контекст и координаты прямоугольника, внутри которого будет располагаться рисунок. Учтите, что этот рисунок обычно имеет маленький размер, поэтому с его помощью вряд ли удастся очень выразительное представление значения.графического представ
ления свойства inverse (инверсный цвет) на черном фоне изображается белыми буквами строка Inverse или на белом фоне — черными буквами строка Normal.
public voidg, Rectangle box)
{
Graphics2D g2 =
Boolean=
String s = islnverse ? "Inverse" : "Normal";
?:
g2.fill(box);
?:
//расчет позиции строки для ее центрирования
(float x),
}
Конечно, для редактирования такого графического представления пользователь должен щелкнуть на свойстве кнопкой мыши для отображения специального редактора.
Укажите, что будет использоваться специальный редактор, переопределяя метод интерфейса PropertyEditor таким образом, чтобы он возвращал значение true.
public boolean
{
return true;
}
Теперь напишите код создания компонента, который будет содержать пользовательский редактор. Для каждого свойства следует создать отдельный класс пользовательского редактора. Например, класс InverseEditor связан с классом InverseEditorPanel
(листинг 8.12), который определяет панель с двумя переключателями для перехода между нормальным и инверсным состоянием. Он содержит несложный код обработки действий пользовательского интерфейса и обновления значений свойств. Это нужно сделать следующим образом.
Конструктор пользовательского редактора должен получить ссылку на объект редактора свойств и сохранить ее в переменной editor.
Для получения текущего значения свойства в пользовательском редакторе нужно вызвать метод editor . getValue ().
Для установки значения объекта в пользовательском редакторе необходимо вызвать метод editor.setvalue(newValue) вслед за методом editor .firePropertyChange().
В листинге 8.11 представлен полный код класса InverseEditor для отображения текущих установок в инспекторе свойств. В листинге 8.12 приведен код для всплывающей панели редактирования.
Другой пользовательский редактор компонента ChartBean позволяет редактировать массив double [ ]. Напомним, что в интегрированной среде разработки Forte и в интегрированной среде разработки ВеапВох не предусмотрено редактирование массивов свойств. Для устранения этого недостатка создадим еще один пользовательский редактор, который показан на рис. 8.21. Все значения массива с соответствующими им индексами в виде префиксов в скобках отображаются в окне в виде списка. После выделения мышью некоторого значения массива оно будет отображено в расположенном выше текстовом поле с надписью Change (Редактировать), где его можно отредактировать. В другом поле с надписью Resize (Изменить) можно также задавать длину массива. Код класса DoubleArrayPanel, который реализует такой графический пользовательский интерфейс, представлен в листинге 8.14.
Кодкласса редакторасвойств(листинг8.13)оченьпохожнакодкласса
InverseEditor. Отличие состоит в том, что в методе paintValue создается строка с несколькими первыми значениями массива и многоточием (...). И конечно, метод getCustomEditor содержит другой пользовательский редактор. Итак, в листингах 8.11-8.14 содержится полный код компонента ChartBean.
• К сожалению, в этом случае приходится рисовать значения массива. Конечно, гораздо проще возвращать их в виде строки с помощью метода getAsText. Однако, в некоторых интегрированных средах разработки текстовое поле используется всегда, когда метод getAsText возвращает ненулевую строку. При этом пользовательский редактор не отображается после того, как пользователь щелкнет на тексте, даже если метод getCustomEditor возвращает редактор компонента.
8.21. Окно пользовательского редактора для редактирования массива
Листинг 8.1 "L inverseEditor.java
/** ©version 1.20 2001-08-21 ©author Cay
*/
import
import import import
/**
Редактор свойства инверсного цвета компонента
Это свойство изменяет цвет столбцов и фона диаграммы */
public class InverseEditor extends PropertyEditorSupport
public Component
return new
public boolean {
return true; }
public boolean isPaintableO {
return true;
public void paintValue(Graphics g, Rectangle box)
Graphics2D g2 = (Graphics2D)g;
boolean islnverse =
String s = islnverse ? "Inverse" : "Normal";
g2.setColor(islnverse ? Color.black :
g2 . fill(box);
g2.setColor(islnverse ? Color.white :
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D stringBounds = g2.getFont().getStringBounds( s,context);
double w = stringBounds.getWidth();
double x= box.x;
if (w< box.width) x+= (box.width—w) /2;
double ascent = —stringBounds.getY();
double у = box. у
+ (box.height —/ 2 + ascent;
g2.drawstring(s, (float)x, (float)y);
public String getAsText() {
return null;
Листинг8.12. InverseEditorPanel.java
/**
@version 1.21 2001-08-21
@author Cay Horstmann
*/
import java.awt.*;
import j;
import j
import java.lang.reflect.*;
import java.beans.*;
import javax.swing.*;
/**
Панель редактирования свойства инверсного цвета.
Содержит переключатели обычного и инверсного цвета */ public class InverseEditorPanel extends JPanel {
public InverseEditorPanel(PropertyEditorSupport ed)
{
editor = ed;
ButtonGroup g = new ButtonGroup();
boolean islnverse
=,
normal = new JRadioButton("Normal", Iislnverse); inverse = new JRadioButton("Inverse", islnverse)
g.add(normal);
g.add(inverse) ;
add(normal);
add(inverse);
ActionListener buttonListener =
new ActionListener()
{
public void actionPerformed(ActionEvent event) {
editor.setvalue(
newBoolean (inverse.isSelectea()));
editor.firePropertyChange();
};
normal.addActionListener(buttonListener);
inverse.addActionListener(buttonListener) ; }
private JRadioButton normal;
private JRadioButton inverse;
private PropertyEditorSupport editor;
Листинг 8.13. DoubleArrayEditor. java
/**
©version 1.20 2001-08-21
@author Cay Horstmann
*/
import java.awt.*;
import j ava.awt.font.*;
import java.awt.geom.*;
import java.beans.*;
/**
Пользовательский редактор массива
с числами с плавающей запятой
*/
public class DoubleArrayEditor extends PropertyEditorSupport {
public Component getCustomEditor()
{
return new DoubleArrayEditorPanel(this);
}
public boolean supportsCustomEditor() {
return true;
}
public boolean isPaintable()
{
return true;
}
public void paintValue(Graphics g, Rectangle box)
Редакторы свойств763
{
Graphics2D g2 = (Graphics2D)g;
doublet] values = (doublet]) getValueO;
StringBuffer s = new StringBuffer(); for (int i= 0; i < 3; i++) (
if (values.length > i) s.append(values[i]);
if (values.length> i + 1) s.append(", "); } if (values.length > 3) s.append("...");
g2.setColor(Color.white);
g2.fill(box);
g2.setColor(Color.black);
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D stringBounds = g2.getFont().getStringBounds( s.toString(), context);
double w = stringBounds.getWidth();
double x = box.x;
if (w < box.width) x += (box.width— w) / 2;
double ascent = -stringBounds.getY();
double у = box. у
+ (box.height —/ 2 + ascent;
g2.drawstring(s.toString() , (float)x, (float)y);
public String getAsText()
{
returnnull;
Листинг8.14.DoubleArrayEditorPanel.java
@version 1.20 1999-09-28
©author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.lang.reflect.*;
import java.beans.*;
import j
import javax.swing.event.*;
/**
Внутренняя панель редактора DoubleArrayEditor. Содержит список значений массива, кнопки изменения массива и редактирования выбранного значения массива
*/
public class DoubleArrayEditorPanel extends JPanel {
public DoubleArrayEditorPanel(PropertyEditorSupport ed) {
editor = ed;
setArray((double[])ed.getValue());
setLayout(new GridBagLayout()) ;
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 100;
gbc.weighty = 0;
gbc.fill =
add(sizeField, gbc, 0, 0, 1, 1);
add(valueField, gbc, 0, 1, 1, 1);
gbc.fill =
add(sizeButton, gbc, 1, 0, 1, 1) ;
add(valueButton, gbc, 1, 1, 1, 1) ;
sizeButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event) {
changeSize();
}) ;
valueButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
changevalue(); }
}) ;
gbc.weighty =
gbc.fill =
add(new JScrollPane(elementList) , gbc, 0, 2, 2, 1);
elementList.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION);
elementList.addListSelectionListener(new ListSelectionListener()
{
public void valueChanged(ListSelectionEvent event) {
int i = elementList,getSelected!ndex();
if (i < 0) return;
valueField.setText("" + array[ i ]); )
});
elementList.setModel(model) ;
elementList.setSelectedlndex(0) ;
/**
Вставка компонента в заданное место
@param с the component to add
eparam gbc the grid bag constraints to use
Sparam x the x grid position
@param y the у grid position
Sparain w the grid width
@param h the grid height
*/
public void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
add(c, gbc);
/**
Изменяет размер массива
*/
public void changeSize()
{
fmt.setParselntegerOnly(true);
int s = 0;
try
{
s = fmt.parse(sizeField.getText()).intValue();
if (s < 0)
throw new ParseException("Out of bounds", 0);
}
catch(ParseException e)
{
JOptionPane.showMessageDialog(this, "" + e, "Input Error", JOptionPane.WARNING_MESSAGE);
sizeField.requestFocus();
return;
}
if (s == array.length) return;
setArray((doublet])arrayGrow(array, s));
editor.setValue(array) ;
j editor.firePropertyChange();
/**
Редактирует выбранное значение
*/
public void changevalue() {
double v = 0;
fmt.setParselntegerOnly(false);
try
} v = fmt.parse(valueField.getText()).doubleValue();
catch(ParseException e)
JOptionPane.showMessageDialog(this, "" + e, "Input Error", JOptionPane.WARNING_MESSAGE);
valueField.requestFocus();
> return;
int currentlndex = elementList.getSelectedIndex(); setArray(currentindex,v);
editor.firePropertyChange() ;
/**
Задает индексированный массив
©param v the array to edit
*/
public void setArray(double[] v)
{
if (v == null) array = new double[0];
else array = v;
model.setArray(array);
sizeField.setText("" + array.length);
if (array.length > 0)
{
valueField.setText("" + array[0]); elementList.setSelectedlndex(0) ;
}
else
valueField.setText("");
/**
Возвращает индексированный массив
©return the array being edited
*/
public doublet] getArray()
{
return (double[])array.clone();
/**
Задает значение элемента индексированного массива
©param i the index whose value to set
©param value the new value for the given index
*/
public void setArray(int i, double value)
{
if (0 <= i && i < array.length)
{
model.setvalue(i, value);
elementList.setSelectedlndex(i);
valueField.setText("" + value);
}
/**
Возвращает значение элемента
индексированного массива
©param i the index whose value to get
©return the value at the given index
*/
public double getArray(int i)
{
if (0 <= i && i < array.length) return array[i]; return 0;
}
/**
Изменяет размер массива
@param a the array to grow
©param newLength the new length
©return an array with the given length and the same elements as a in the common positions
*/
private static Object arrayGrow(Object a, int newLength)
{
Class cl =
if (!cl.isArray()) return null;
Class componentType =
int length = Array.getLength(a);
Object newArray = Array.newlnstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0,
Math.min(length, newLength));
return newArray;
}
private PropertyEditorSupport editor;
private double[] array;
private NumberFormat fmt =
private JTextField sizeField = new JTextField(4);
private JTextField valueField = new JTextField(12);
private JButton sizeButton = new JButton("Resize");
private JButton valueButton = new JButton("Change”);
private JList elementList = new JList();
private DoubleArrayListModel model = new DoubleArrayListModel() ; }
/**
Модель списка
*/
class DoubleArrayListModel extends AbstractListModel
{
public int getSize()
{
return array.length;
}
public Object getElementAt(int i)
{
return "[" + i + "] " +array[ i ] ;
}
/**
Задает новый массив, который следует отобразить в списке ©param a the new array
*/
public void setArray(double[] a)
{
int oldLength = array == null ? 0 : array. length;
array = a;
int newLength = array == null ? 0 : array.length;
if (oldLength > 0) fireIntervalRemoved(this, 0, oldLength)
if (newLength > 0) firelntervalAdded(this, 0, newLength);
}
/**
Задает новое значение для элемента массива в данном списке
@param i the index whose value to change
©param value the new value for the given index
*/
public void setValue(int i, double value)
{
array[i] = value;
j fireContentsChangedtthis, i, i);
private doublet] array;
}
Выводы
При создании редактора свойств нужно выбрать один из трех путей отображения и редактирования значения свойства.
В виде текстовой строки (определяя методы getAsText и setAsText).
В виде поля со списком (определяя методы getAsText, setAsText и getTags).
Графически, прорисовывая нужное значение (определяя методы isPaintable, paintValue, suportsCustomEditor и getCustomEditor).
Все три варианта были показаны на примере компонента ChartBean.
Наконец, в некоторых редакторах свойств можно организовать поддержку метода getJavalnitializationString. С помощью данного метода можно передать интегрированной среде разработки код на языке Java. Этот код позволяет свойству автоматически генерировать нужный код. Применение такого метода здесь не рассматривается.
[
T^rrifj av • beans .PropertyEditorSupport
Airi I
' e Метод boolean isPaintable () следует переопределить так, чтобы он возвращал значение true, в случае если класс использует метод paintValue для отображения свойства.
Метод void paintValue (Graphics 9"/ Rectangle box) следует переопределить так, чтобы он отображал значение, рисуя его в графическом контексте g в специально отведенном месте инспектора свойств.
Параметры:
дграфический объект, в котором можно рисовать
boxпрямоугольный объект, представляющий область на инспек
торе свойств, в которой можно рисовать значение
Метод boolean supportsCustomEditor () следует переопределить так, чтобы он возвращал значение true, если редактор свойств является пользовательским редактором.
Метод Component getCustomEditor () следует переопределить так, чтобы он возвращал компонент, содержащий пользовательский интерфейс для редактирования значения свойства.
Метод String getJava!nitializationString() следует переопределить так, чтобы он возвращал строку Java-кода, которая используется для генерирования кода инициализации значения свойства, например " 0 11,
"new Color(64, 64, 64) ".
Пользовательские соглашения об именах
Какуже было показано выше, если для элементов компонента JavaBeans использованы стандартные соглашения об именах, то интегрированная среда разработки может использовать механизм отражения для распознавания свойств, событий и методов компонента JavaBeans. Этот механизм облегчает программирование простых компонентов JavaBeans, но, в таком случае, возможности компонента будут существенно ограничены. По мере усложнения компонента JavaBeans в нем будут появляться элементы, которые трудно обнаружить с помощью соглашения об именах и механизма отражения. (Не говоря уже о том, что повсеместное использование английских соглашений об именах в интегрированных средах разработки никак не способствует расширению поддержки локализованных версий для клиентов из разных стран мира.) Более того, как уже упоминалось выше, в компонентах могут присутствовать пары методов get/set, которые не рекомендуется представлять в виде свойств компонентаJavaBeans.
К счастью, в спецификации JavaBeans предусмотрен более гибкий и мощный механизм сохранения информации о компоненте для ее использования в интегрированной среде разработки. Как многие другие особенности компонентов JavaBeans, этот механизм прост с теоретической точки зрения, но может оказаться очень трудоемким на практике. Основная идея заключается в использовании объекта, реализующего интерфейс Beaninf о. (Ранее мы уже использовали класс Beaninf о при создании редакторов свойств компонента ChartBean.)
При реализации этого интерфейса для описания компонента, интегрированная среда разработки обращается к методам интерфейса Beaninf о для получения информации (возможно, очень подробной) о свойствах, событиях и методах этого компонента. Использование класса Beaninf о позволяет избежать обязательного применения соглашений об именах. По иронии судьбы, в спецификации JavaBeans сказано, что на самом деле для связывания объекта Beaninf о с его компонентом требуется использовать соглашение об именах. Для создания имени информационного класса к имени его компонента JavaBeans добавляется суффикс Beaninfо. Например, информационный класс компонента ChartBean должен называться ChartBeanBeanln о. Информационный класс должен находиться в одном пакете вместе со своим компонентом JavaBeans.
Любые описания (свойств, событий и методов этого компонента) в информационном классе компонента переопределяют ту информацию, которую интегрированная среда разработки может получить на основе механизма отражения. Более того, при описании набора атрибутов компонентов (например, набора свойств компонента JavaBeans) нужно в соответствующем информационном классе предоставить информацию о каждом атрибуте этого набора.
Как показано выше, информационный класс создается не с самого начала (т.е. на основе только лишь интерфейса Beaninfo), а на основе вспомогательного класса SimpleBeanln о, который уже содержит пустые реализации (т.е. возвращающие значение null) всех методов интерфейса Beanlnfo. Таким образом, программисту нужно только переопределить те методы, которые ему действительно необходимо изменить. Более того, этот вспомогательный класс включает полезный метод loadimage, который можно использовать для загрузки изображений в компонентах JavaBeans (например, пиктограмм, как показано ниже). Далее, во всех примерах использования информационных классов Beanlnfo используется вспомогательный
класс SimpleBeanln о. Например, информационный класс ChartBeanBeanlnfo создается на основе вспомогательного класса SimpleBeanInf о.
public class ChartBeanBeanlnfo extends SimpleBeanlnfo
Если метод классаSimpleBeanlnfo возвращает значениеnull, то интегрированная среда разработки использует соглашения об именах для распознавания и поиска соответствующих ему атрибутов. В противном случае, т.е. при возвращении отличныхотn u l lзначений, механизм отражения неиспользуется.
Для более тесного знакомства с возможностями механизма на основе информационного класса, начнем с простого, но наиболее полезного метода getlconинтерфей- са Beanlnfо. Этот метод позволяет задавать для компонента JavaBeans пользовательскую пиктограмму, которая отображается в интегрированной среде разработки и придает набору компонентов вид палитры компонентов. В интегрированной среде разработки эта пиктограмма обычно отображается на специальной панели компонентов. Для пиктограммы можно задать четыре изображения с разными размерами и цветами. В интерфейсе Beanlnf о с этой целью предусмотрено четыре константы, определяющие стандартные размеры и цвета пиктограмм.
ICON_COLOR_16x16
ICON_COLOR_3 2x3 2
ICON_MONO_16xl6
ICON_MONO_32x32
Ниже приводится фрагмент кода, показывающий способ использования вспомогательного метода loadlmage класса SimpleBeanlnfoдля добавления пиктограммы в класс.
public Image getlcon(int iconType) {
String name = "";
if (iconType == Beaninfo.ICON_COLOR_16xl6)
name = "COLOR_16xl6";
else if (iconType == Beaninfo.ICON_COLOR_32x32)
name = "COLOR_32x32";
else if (iconType == Beaninfo.ICON_MONO_16x16)
name = "MONO_16xl6";
else if (iconType == Beaninfo.ICON_MONO_32x32)
name = "MONO_32x32" ;
else return null;
return loadlmage("ChartBean_" + name + " .gif') ;
}
Такимобразом, вэтомпримерефайлыизображенийбудутиметьследующиеимена.
ChartBean_COLOR_16xl6.gif
ChartBean_COLOR_32x32.gif
ChartBean_MONO_16xl6.gif
ChartBean_MONO_32x32.gif
Объекты FeatureDescriptor
Для работы с более сложными элементами класса Beanlnfо предусмотрен класс FeatureDescriptor и его многочисленные подклассы. Как подсказывает имя класса FeatureDescriptor, его объект содержит информацию об элементах класса: свойствах, событиях, методах и т.д. Точнее говоря, класс FeatureDescriptor является суперклассом для всех описаний и содержит наиболее общие операции, которые используются при описании какого-либо элемента. Например, имя элемента можно получить при помощи метода getName. Этот метод принадлежит суперклассу, поэтому применяется для всех описаний элементов, независимо от того, что они описывают. Ниже приведен перечень подклассов класса FeatureDescriptor.
BeanDescriptor.
EventSetDescriptor.
MethodDescriptor.
ParameterDescriptor.
Property-Descriptor (с еще одним подклассом IndexedPropertyDescriptor).
Все эти классы работают примерно одинаково. Для каждого элемента, который нужно описать, создается объект-описание. Затем все объекты-описания элементов собираются в одном массиве, который возвращается одним из методов интерфейса Beanlnf о.
Например, для отмены механизма отражения для набора событий нужно возвратить массив объектов-описаний этих событий EventSetDescriptor в информационном классе компонента.
class MyBeanBeanlnfо extends SimpleBeanlnfo {
public EventSetDescriptor[] getEventDescriptors ()
{
)
}
Затем следует создать все объекты-описания событий EventSetDescriptor, которые будут находиться в этом массиве. Вообще, все конструкторы объектов-описаний разных элементов FeatureDescriptor работают одинаково. В частности, наиболее общий конструктор обьекта-описания события имеет следующие параметры.
Класс компонента, содержащего данное событие.
Базовое имя события.
Класс интерфейса-слушателя событий EventListener, который соответствует этому событию.
Методы интерфейса EventListener, которые запускаются этим событием.
Др)тие конструкторы позволяют определять методы компонента JavaBeans, которые можно использовать для создания и удаления объектов-слушателей EventListener.
Примером применения этой технологии является класс ExplicitButtonBean и его информационный класс, которые включены в состав пакета инструментов BDK.
Проанализируем код создания необходимых объектов-описаний событий для компонента JavaBeans. В компоненте ExplicitButtonBean предусмотрены два события: нажатие кнопки и изменение состояния кнопки. Ниже показан код создания двух объектов-описаний EventSetDescriptor, связанных с этими двумя событиями.
EventSetDescriptor push = new EventSetDescriptor(beanClass,
"actionPerformed",
java.awt.event.ActionListener.class,
"actionPerformed");
EventSetDescriptor changed = new EventSetDescriptor(beanClass, "propertychange",
java.beans.PropertyChangeListener.class,
"propertyChange");
Следующим шагом будет указание различных отображаемых имен для объектов- описаний событий EventSetDescriptor в коде объекта ExplicitButton класса Beaninfo.
push.setDisplayName("button push");
changed.setDisplayName("bound property change");
На самом деле код создания необходимых объектов EventSetDescriptor выглядит несколько сложнее, чем показано выше. Этот фрагмент кода необходимо заключить в блок try/catch, поскольку все конструкторы объектов-описаний могут генерировать исключительную ситуацию TntrospectionException. Таким образом, массив объектов-описаний нужно создать в блоке try/catch, например так, как показано в следующем фрагменте кода.
public EventSetDescriptor[] getEventSetDescriptors() {
try
{
EventSetDescriptor push = new
EventSetDescriptor(beanClass,
"actionPerformed",
java.awt.event.ActionListener.class, "actionPerformed");
EventSetDescriptor changed = new
EventSetDescriptor(beanClass,
"propertyChange",
java.beans.PropertyChangeListener.class, "propertyChange");
push.setDisplayName("button push");
changed.setDisplayName("boundproperty change");
return new EventSetDescriptor[] { push, changed };
}
catch (IntrospectionException e) {
throw new RuntimeException(e.toStringf)) ;
} }
Этот объект-описание набора событий необходим потому, что для имен объектов- описаний событий не соблюдается стандартное соглашение об именах.
Имена классов-слушателей событий отличаются от шаблона имя_компонен- raListener.
Отображаемые имена событий отличаются от фактических имен событий.
Итак, если имя атрибута компонента JavaBeans не соответствует стандартному соглашению об именах, то необходимо выполнить следующие действия.
Создать объекты-описания для всех элементов набора атрибутов (событий, свойств, методов).
Возвратить массив этих объектов-описаний с помощью соответствующего метода интерфейса Beanlnfо.
АР
Метод EventSetDescriptor[] getEventSetDescriptors() возвра- aS»щает массив определенных объектов-описаний событий. Возвращаемое
значение null указывает интегрированной среде разработки на необходимость использования соглашения об именах и механизма отражения для распознавания и поиска членов класса.
Метод MethodDescriptor[] getMethodDescriptors() возвращает массив определенных объектов-описаний методов. Возвращаемое значение nullуказывает интегрированной среде разработки на необходимость использования соглашения об именах и механизма отражения для распознавания и поиска членов класса.
Метод Property-Descriptor[] getPropertyDescriptors() возвращает массив определенных объектов-описаний свойств. Возвращаемое значение null указывает интегрированной среде разработки на необходимость использования соглашения об именах и механизма отражения для распознавания и поиска членов класса. Этот метод возвращает набор простых и индексированных описаний свойств. Для проверки принадлежности объекта- описания PropertyDescriptor к классу IndexedPropertyDescriptor можно использовать метод instanceof.
Метод Image getlconfint iconType) возвращает объект-изображение (пиктограмму), который используется для представления компонента JavaBeans в панели компонентов, различных командах меню и т.д. Выше описаны четыре константы, которые определяют стандартные типы пиктограмм.
Параметры:
iconTypeиспользуемый тип пиктограммы (цветная пиктограмма раз
мером 16x16, цветная пиктограмма размером 32x32 и т.д.)
Метод int getDefaultEventIndex() возвращает либо индекс массива объектов-описаний событий, который определяет используемый по умолчанию елемент массива, либо значение - 1 , если не существует заданных по умолчанию событий.
Метод int getDefaultPropertyIndex() возвращает либо индекс массива объектов-описаний свойств, который определяет используемый по умолчанию елемент массива, либо значение -1 , если не существует заданных по умолчанию свойств. В интегрированной среде разработки исполь-
зуемый по умолчанию атрибут может быть визуально выделен, например, за счет перемещения его в начало списка атрибутов или отображения его имени полужирным начертанием.
Метод Beaninfo[] getAdditionalBeanlnfо() возвращает массив объектов Beanin о или null. Этот метод используется, если нужно передать некоторую информацию о компоненте, которая хранится в классах Beanlnfо, другим компонентам. Например, можно использовать данный метод, если компонент группирует множество других компонентов. В случае конфликта более высоким приоритетом обладает информация из текущего класса Beanlnf о.
java.beans.SimpleBeanlnfо
Метод Image loadimage (String resourceName) возвращает объект- изображение, загружая его из файла, путь к которому передается с помощью параметра resourceName. В настоящее время поддерживаются только GIF-изображения.
Параметры:
resourceNameпуть к файлу (относительный путь к каталогу, в кото
ром содержится текущий класс)
java.beans.FeatureDescriptor
Метод String getName () возвращает имя атрибута, которое используется в компоненте.
Метод void setName (String name) задает имя атрибута. Параметры: nameимя атрибута
Метод String getDisplayName() возвращает отображаемое имя атрибута. По умолчанию возвращается значение метода getName. К сожалению, в настоящее время не предусмотрена поддержка имен с учетом региональных стандартов.
Метод void setDisplayName (String displayName) задает отображаемое имя атрибута.
Параметры:
displayName отображаемое имя
Метод String getShortDescription() возвращает строку, которая может использоваться в интегрированной среде разработки для краткого описания атрибута. По умолчанию возвращается значение методом getDisplayName().
Метод void setShortDescription(String text) задает краткую строку (обычно до 40 символов) описания атрибута.
Параметры:
textкраткое описание атрибута
Метод void setValue(String attributeName, Object value) задает значение для атрибута с указанным именем.
Параметры:
attributeName имя атрибута, для которого задается значение
Метод Object getValue(String attributeName) возвращает значение атрибута с указанным именем.
Параметры:
attributeName имя атрибута, значение которого извлекается
Метод Enumeration attributeNames () возвращает объект-перечисление имен для всех атрибутов, зарегистрированных методом setValue ().
Метод void setExpert (boolean b) устанавливает специальный флаг, используемый в интегрированной среде разработки для скрытия атрибута от неопытных пользователей. (В некоторых интегрированных средах разработки этот флаг не поддерживается.)
Параметры:
bесли имеет значение true, то атрибут будет скрыт
Метод boolean isExpert() возвращает значение true, если атрибут скрыт от неопытных пользователей.
Метод void setHidden(boolean b) устанавливает специальный флаг, который означает, что данный атрибут может использоваться только интегрированной средой разработки.
Параметры:
bесли имеет значение true, то атрибут будет скрыт
Метод boolean isHiddenO возвращает значение true, если атрибут нужно скрыть от пользователей, но интегрированная среда разработки может его использовать.
[
ApTf^ aVa"1388118" EventSetDescriptor
U • Конструктор EventsetDescriptor(Class sourceClass,String
SsSi eventSetName, Class listener, String listenerMethod) создает объект EventSetDescriptor на основе стандартных соглашений об именах классов-событий и именах методов создания и удаления слушателей событий. При обнаружении ошибки генерируется исключительная ситуация IntrospectionException.
Параметры: sourceClass eventSetName listener
класс, генерирующий событие имя события
интерфейс-слушатель событий
listenerMethod
метод, запускающийся после получения слушателем сообщения о произошедшем событии
Конструктор EventsetDescriptor(Class sourceClass,String
eventSetName, Class listener, String[] listenerMethods, String addL i s tenerMethod, String removeListenerMethod) создает объект EventSetDescriptor с несколькими методами-слушателями событий и пользовательскими методами создания и удаления слушателей событий. При обнаружении ошибки генерируется исключительная ситуация IntrospectionException.
Параметры: sourceclass eventSetName listener listenerMethodsaddListenerMethod removeListenerMethodПараметры: sourceclass eventSetName listener listenerMethodsaddListenerMethod removeListenerMethodкласс, генерирующий событие
Параметры: sourceclass eventSetName listener listenerMethods
addListenerMethod removeListenerMethod
Параметры: sourceclass eventSetName listener listenerMethods
addListenerMethod removeListenerMethod
имясобытия
интерфейс-слушатель событий
методы, запускающиеся после получения слушателем сообщения о произошедшем событии метод создания слушателя
метод удаления слушателя
Метод Method getAddListenerMethod() возвращает метод, используемый для регистрации слушателей событий.
Метод Method getRemoveListenerMethod() возвращает метод, используемый для удаления зарегистрированного слушателя событий.
Метод Method[ ] getListenerMethods () возвращает массив объектов- методов Method для методов, определенных в интерфейсе-слушателе.
Метод MethodDescriptor[] getListenerMethodDescriptors() возвращает массив объектов-описаний MethodDescriptor для методов, определенных в интерфейсе-слушателе.
Метод Class getListenerType () возвращает объект Class для определения типа интерфейса-слушателя данного события.
Метод void setUnicast (boolean b) устанавливает специальный флаг, который означает, что данное событие должно обрабатываться только одним слушателем (если для параметра b задано значение true).
Метод boolean isUnicast () возвращает значение true, если событие должно обрабатываться только одним слушателем (по умолчанию этот флаг имеет значение false).
java.beams.PropertyDescriptor
Конструкторы PropertyDescriptor (String propertyName, Class beanClass) и PropertyDescriptor(String propertyName, Class beanClass, String getMethod, String setMethod) создают объект PropertyDescriptor. При обнаружении ошибки генерируется исключительная ситуация IntrospectionException. В первом варианте конструктора предполагается использование стандартных соглашений для имен get/set-методов.
Параметры: propertyName beanClass getMethod setMethod
имя свойства
объект Class для описываемого компонента имя get-метода имя set-метода
Метод Class getPropertyType () возвращает объект, определяющий тип свойства.
Метод Method getReadMethod () возвращает get-метод.
Метод Method getWriteMethod() возвращает set-метод.
Метод void setBound (boolean b) задает значение true, если свойство при изменении значения генерирует событие PropertyChangeEvent.
Метод boolean isBound() возвращает значение true, если данное свойство является связанным.
Метод void setConstrained (boolean b) устанавливает специальный флаг, который означает, что данное свойство перед изменением значения генерирует событие VetoableChangeEvent (если для параметра b задано значение true).
Метод boolean isConstrained() возвращает значение true, если дан-
ное свойство является ограниченным.
j ava. beans. IndexedPropertyDescriptor
Конструкторы IndexedPropertyDescriptor(StringpropertyName,
Class beanClass) и IndexedPropertyDescriptor(String propertyName, Class beanClass, String getMethod, String setMethod, String indexedGetMethod, String indexedSetMethod) создают объект IndexedPropertyDescriptor для индексированного свойства. При обнаружении ошибки эти методы генерируют исключительную ситуацию IntrospectionException. В первом конструкторе предполагается использование стандартных соглашений для имен get/set-методов.
Параметры: propertyName beanClass getMethod setMethod indexedGetMethod indexedSetMethodПараметры: propertyName beanClass getMethod setMethod indexedGetMethod indexedSetMethodимя свойства
Параметры: propertyName beanClass getMethod setMethod indexedGetMethod indexedSetMethod
Параметры: propertyName beanClass getMethod setMethod indexedGetMethod indexedSetMethod
объект Class для описываемого компонента
имя get-метода
имя set-метода
имя индексированного get-метода
имя индексированного set-метода
Метод Class getlndexedPropertyType() возвращает класс-описание типа индексированных значений данного свойства, т.е. тип возвращаемого значения индексированного get-метода.
Метод Method getIndexedReadMethod() возвращает индексирован-
ный get-метод.
Метод Method getlndexedWriteMethod() возвращает индексирован-
ный set-метод.
java.beans.MethodDescriptor
Конструкторы MethodDescriptor(Method method) и MethodDescriptor (Method, ParameterDescriptor[] parameterDescriptors) создают обьект-описание метода с его параметрами; при обнаружении ошибки во время интроспекции генерируется исключительная ситуация IntrospectionException.
Параметры:
methodобъект-метод
parameterDescriptorsмассив объектов-описаний параметров метода
Метод Method getMethod () возвращает объект-метод.
Метод ParameterDescriptor[] getParameterDescriptors () возвращает массив объектов-описаний параметров метода.
java.beans.ParameterDescriptor
Конструктор ParameterDescriptor () создает новый объект-описание параметра, который содержит только ту информацию, которая хранится в суперклассе FeatureDescriptor.
Настройщики
Рис 8.22. Настройщик компонента ChartBeanРис 8.22. Настройщик компонента ChartBeanДаже самый сложный редактор свойств не позволяет редактировать одновременно сразу несколько свойств. Однако, если некоторые свойства компонента связаны друг с другом, то было бы разумно предоставить пользователю возможность редактировать их одновременно. Для организации такого сложного редактирования вместо нескольких редакторов свойств (или вместе с ними) нужно использовать настройщик (customizer).
Рис 8.22. Настройщик компонента ChartBean
Рис 8.22. Настройщик компонента ChartBean
В настоящем разделе приводится пример программы с настройщиком компонента ChartBean. Этот настройщик позволяет одновременно задавать значения для нескольких свойств компонента ChartBean и определять файл, из которого считываются данные для диаграммы. На рис. 8.22 пока
зана одна из панелей настройщика компонента ChartBean.
Для создания настройщика в компоненте JavaBeans нужно использовать класс Beanlnf о и переопределить его метод getBeanDescriptor, как показано в следующем примере.
public BeanDescriptor getBeanDescriptor() {
return new BeanDescriptor(ChartBean.class, ChartBeanCustomizer.class) ;
}
Для создания настройщика следует выполнить определенную последовательность действий.
Переопределите метод getBeanDescriptor таким образом, чтобы он возвращал новый объект BeanDescriptor для вашего компонента.
Укажите класс-настройщик в качестве второго параметра конструктора класса BeanDescriptor.
Учтите, что при создании класса-настройщика не нужно следовать каким-либо соглашениям об именах, потому что интегрированная среда разработки способна найти и определить класс-настройщик следующим образом.
Сначала среда находит соответствующий информационный класс Beanlnf о.
Затем вызывает его метод getBeanDescriptor.
Потом вызывает метод getCustomizerClass.
(Тем не менее, рекомендуется использовать для классов-настройщиков шаблон ИмяКомпонентаСиstomizer.)
В листинге 8.15 показан код класса ChartBeanBeanlnfо, в котором упоминается класс-настройщик ChartBeanCustomizer. Способ реализации класса-настройщика описывается в следующем разделе.
/**
@version 1.10 1997-10-27
©author Cay Horstmann
*/
import java.awt.*;
import j ava.beans.*;
/**
Информационный класс компонента ChartBean с указанием пиктограммы и настройщика
*/
public class ChartBeanBeanlnfо extends SimpleBeanlnfo {
public BeanDescriptor getBeanDescriptor() {
return new BeanDescriptor(ChartBean.class, ChartBeanCustomizer.class);
}
public Image getlcon(int iconType) {
String name = "" ;
if (iconType == Beaninfo.ICON„COLOR_16xl6)
name = nCOLOR„16xl6";
else if (iconType == Beaninfo.ICON_COLOR_32x32) name = "COLOR_32x32";
else if (iconType' == Beaninfo. ICON„MONO_16xl6) name =
else if (iconType == Beaninfo.ICON_MONO„32x32) name = "MONO_32x32";
else return null;
return loadimage("ChartBean_" + name + ”.gif") ;
java, beans. Beanlnfo
■ Метод BeanDescriptor getBeanDescriptor О возвращает объект BeanDescriptor с описанием атрибутов компонентаJavaBeans.
java.beans.BeanDescriptor
Конструктор BeanDescriptor (Class beanClass, Class customizerClass) создает объектBeanDescriptor для компонента с настройщиком.
Параметры:
beanClassобъект Class компонента
customizerClass объект Class настройщика компонента
Метод Class getBeanClass () возвращает объект Class с определением компонента.
Метод Class getCustomizerClass () возвращает объект Class с определением настройщика компонента.
Создание настройщика
Любой класс-настройщик должен реализовывать интерфейс Customizer, который содержит три метода.
Метод setobject, который содержит в качестве параметра настраиваемый компонент JavaBeans.
Методы addPropertyChangeListener иremovePropertyChangeListener, которые управляют набором слушателей событий, которые заключаются в изменении свойства в настройщике.
Рекомендуется постоянно обновлять графический интерфейс компонента при изменении пользователем значения любого свойства, а не только по окончании процесса настройки.
В отличие от редакторов свойств настройщики не отображаются автоматически. В интегрированной среде разработки ВеапВох для вызова настройщика компонента нужно выбрать команду меню Edit^Customize (Правка^Настройка). В этом случае ВеапВох вызывает в настройщике метод setObject, передавая ему в качестве параметра настраиваемый компонент JavaBeans. Обратите внимание, что таким образом создание настройщика происходит до его фактического связывания с экземпляром компонента JavaBeans. Поэтому в настройщике невозможно получить какую-либо информацию о состоянии компонента JavaBeans и для создания настройщика придется использовать предлагаемый по умолчанию конструктор, т.е. без аргументов.
Процесс создания класса-настройщика можно разбить на три части.
Создание визуального интерфейса.
Инициализация настройщика в методе setobject.
Обновление компонента JavaBeans после возникновения событий, которые соответствуют действиям пользователя по редактированию свойств компонента с помощью визуального интерфейса настройщика.
По определению, класс настройщика является визуальным. Поэтому он должен наследовать класс Component или какой-либо его подчиненный класс, например, JPanel. Конфигураторы обычно предоставляют пользователю возможность работы с несколькими параметрами. Поэтому для них удобно использовать интерфейс JTabbedPane, который представляет собой панель с несколькими вкладками. Именно этот принцип используется далее при создании настройщика в примерах данного раздела.
Настройщик размещает информацию в трех вкладках.
Color (Цвет) — для выбора цветов и режима отображения.
Title (Заголовок) — для выбора заголовка и его расположения.
Data (Данные) — для выбора отображаемых в диаграмме данных.
Конечно, для создания такого пользовательского интерфейса потребуется выполнить очень большой объем рутинной работы. В нашем примере только в конструкторе используется более 100 строк кода. Однако, для его создания потребуются только
самые основные навыки программирования компонентов Swing, а потому далее будут опущены все несущественные подробности этого процесса.
Попытка использования класса JTabbedPane для настройщика в интегрированной среде разработки ВеапВох привела к возникновению многочисленных трудностей. Дело в том, что в ВеапВох используются старыеAWT-компоненты. Поэтому обработка событий для компонентов AWT и Swing выполняется не вполне корректно, например, не срабатывают щелчки мышью для переключения панелей. Методом проб и ошибок мы обнаружили, что избежать этой проблемы можно, отслеживая и обрабатывая щелчки на панелях и вызывая метод validate. Предположительно, данная проблема очень скоро перестанет быть актуальной, после того как все интегрированные среды разработки будут поддерживать компоненты Swing.
При создании настройщика следует учитывать одну хитрость. Вместо создания совершенно нового интерфейса для редактирования значения свойства какого-то класса, рекомендуется просто найти уже готовый соответствующий редактор свойства и добавить его в пользовательский интерфейс! Например, в настройщике компонента ChartBean нужно указать цвет диаграммы. Как известно, в интегрированной среде разработки ВеапВох содержится отличный редактор цветов, который можно получить следующим образом.
PropertyEditor colorEditor =
PropertyEditorManager.findEditor(Color * Class);
Затем необходимо вызвать метод getCustomEditor для получения компонента, который будет содержать пользовательский интерфейс для указания цветов.
Component colorEditorComponent =
// Теперь добавляем этот компонент в пользовательский интерфейс.
После размещения компонентов инициализируем их значения при помощи метода setObject во время отображения настройщика. Параметром этого метода будет настраиваемый компонент JavaBeans. Ссылку на компонент JavaBeans нужно сохранить, потому что позже она потребуется для уведомления об изменениях свойств. Затем следует проинициализировать все компоненты пользовательского интерфейса. Ниже показан фрагмент кода для метода setobjectнастройщика компонента ChartBean, который выполняет инициализацию.
public void setobject(Object obj) {
bean = (ChartBean)obj;
titleField.setText(bean.getTitle()) ;
colorEditor.setvalue(bean.getGraphColor());
}
Наконец, нужно обеспечить обработку пользовательских действий. При изменении пользователем значения свойства компонент генерирует событие, которое должен обработать настройщик. Обработчику событий необходимо обновить значение этого свойства в компоненте, а также генерировать событие PropertyChangeEvent для обновления других слушателей событий (например, инспектора свойств). Рассмотрим данный процесс на примере нескольких элементов пользовательского интерфейса настройщика компонента ChartBean.
Если пользователь вводит новый заголовок, то следует обновить соответствующее свойство-заголовокTitle, для чего нужно связать слушатель событий DocumentListener с текстовым полемtitleField,вкотороепользовательвводитзаголовок.
titleField.getDocument().addDocumentListener(new
DocumentListener()
public void changeUpdate(DocumentEvent event) {
setTitle(titleField.getText());
}_
public void insertUpdate(DocumentEvent event) t
setTitle(titleField.getText());
}
public void removeUpdate(DocumentEvent event)
{
setTitle(titleField.getText());
}
}) ;
Эти три метода-слушателя событий вызывают метод настройщика setTitle. Метод setTitle обновляет значение свойства, а затем генерирует событие изменения свойства. (Такое обновление необходимо только для несвязанных свойств.) Вот как выглядит код метода setTitle.
public void setTitle(String newValue)
{
if (bean == null) return;
String oldValue = bean.getTitle();
bean.setTitle(newValue);
firePropertyChange("title", oldValue, newValue);
}
При изменении значения цвета в редакторе свойства, нужно обновить цвет диаграммы в компоненте. Для обработки изменений цвета свяжем слушатель событий с редактором свойства. Интересно, что при изменении свойства редактор также посылает соответствующие события.
colorEditor.addPropertyChangeListener(new
PropertyChangeListener()
{
public void propertychange(PropertyChangeEvent event)
{
setGraphColor((Color)colorEditor.getValue()) ;
}
}) ;
При изменении значения цвета в редакторе свойства вызывается метод настройщика setGraphColor. Этот метод обновляет свойство компонента graphColor и генерирует событие изменения свойства graphColor.
public void setGraphColor(Color newValue)
{
if (bean == null) return;
Color oldValue == bean.getGraphColor(); bean.setGraphColor(newValue);
firePropertyChange("graphcolor", oldValue, newValue);
}
В листинге 8.16 приведен полный код настройщикакомпонента ChartBean.
Данный настройщик только задает значения свойств компонента. Хотя, в общем случае настройщик может вызывать любые методы компонента независимо от того, являются они set-методами компонента или нет. Поэтому настройщики имеют более общее применение, чем редакторы свойств. (Некоторые компоненты JavaBeans могут содержать атрибуты, которые не предлагаются в качестве свойств, а потому могут редактироваться только с помощью настройщика.)
Листинг8.18.ChartBeanCuetomizer.java
/** ©version 1.10 1999-09-29 @author Cay Horstmann
*/
import java.awt.*;
import j ava.awt.event.*;
import java.beans.*;
import j ava.io.*;
import java.text.*;
import java.util.* ;
import javax.swing.*;
import j avax.swing.event.*;
/**
Настройщик компонента ChartBean, который позволяет редактировать все свойства диаграммы в одном диалоговом окне с несколькими вкладками
*/
public class ChartBeanCustomizer extends JTabbedPane implements Customizer
{
public ChartBeanCustomizer()
{
data = new JTextArea();
JPanel dataPane = new JPanel();
dataPane.setLayout(new BorderLayout());
dataPane.add(newJScrollPane(data), BorderLayout.CENTER);
JButton dataButton = new JButton("Set data");
dataButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
setData(data.getText{)) ; }
});
JPanel p = new JPanel();
p.add(dataButton);
dataPane. add(p, BorderLayout. SOUTH) ;
JPanel colorPane = new JPanel();
colorPane.setLayout(new BorderLayout() );
normal = new JCheckBox("Normal", true);
inverse= newfalse);
p=new
p.add(normal);
p.add(inverse);
g=new
g.add(normal);
g.add(inverse);
normal.addActionListener(new
{
p{ublic void actionPerformed(ActionEvent event) , setinverse(false);
inverse.addActionListener(
n{ew
p{ublic void actionPerformed(ActionEvent event) j setlnverse(true);
colorEditorcolorEditor});
colorEditor
colorEditor
=
colorEditor.addPropertyChangeListener(
n{ew
public void
{ evt)
setGraphColor((Color)colorEditor.getValue());
}) ;
colorPane.add(p,
colorPane.add(colorEditor.getCustomEditor(),
BorderLayout.CENTER);
JPanel titlePane = new
titlePane. setLayout (newBorderLayout ()) ;
g=new
position=newJCheckBox[3];
=newfalse);
position[l] =newtrue)
position[2] =newfalse)
p=new
for(inti=0;i<i++)
final int value = i;
p.add(position[i]);
;
position[i].addActionListener(new
{
public void actionPerformed(ActionEvent event)
setTitlePosition(value);
В ;
}
titleField = new JTextField();
titleField.getDocument().addDocumentListener(
new DocumentListener()
{
public void changedUpdate(DocumentEvent evt)
{
setTitle(titleField.getText());
}
public void insertUpdate (DocumentEvent evt)
{
setTitle(titleField.getText());
}
public void removeUpdate(DocumentEvent evt)
{
setTitle(titleField.getText()) ;
}
В ;
titlePane.add(titleField, BorderLayout.NORTH) ;
titIePane.add(p, BorderLayout.SOUTH);
addTab("Color", colorPane);
addTab("Title", titlePane);
addTab("Data", dataPane);
// Устранение ошибки при работе с
// панелью JTabbedPane в JDK 1.2
addChangeListener(new
ChangeListener()
{
public void stateChanged(ChangeEvent event)
{
validate();
)) ;
}
/**
Указание данных для диаграммы
@param s a string containing the numbers to be displayed, separated by white space
*/
public void setData(String s)
{
StringTokenizer tokenizer = new StringTokenizer(s);
int i = 0;
doublet] values = new double[tokenizer.countTokens()];
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
try
{ values[i] = Double.parseDouble(token) ;
}
catch (NumberFormatException exception)
{
}
}
setvalues(values);
/**
Установка заголовка диаграммы
@param newValue the new title
*/
public void setTitle(String newValue)
{
if (bean == null) return;
String oldValue = bean.getTitle() ;
bean.setTitle(newValue) ;
firePropertyChange("title", oldValue, newValue);
/“
Установка расположения заголовка диаграммы
@param i the new title position (ChartBean.LEFT,
ChartBean.CENTER, or ChartBean.RIGHT)
*/
public void setTitlePosition(int i) {
if (bean == null) return;
Integer oldValue = new Integer(bean.getTitlePosition());
Integer newValue = new Integer(i);
bean.setTitlePosition(i);
firePropertyChange("titlePosition", oldValue, newValue);
}
/ * *
Установка инверсного режима для диаграммы
@param b true if graph and background color are inverted */
public void setinverse(boolean b) {
if (bean == null) return;
Boolean oldValue = new Boolean(bean.islnverse() ) ;
Boolean newValue = new Boolean(b);
bean.setinverse(b);
firePropertyChange("inverse", oldValue, newValue);
/**
Установка значений, отображаемых в диаграмме
@param newValue the new value array
*/
public void setValues(double[] newValue) {
if (bean == null) return;
doublet] oldValue = bean.getValues();
bean.setvalues(newValue);
firePropertyChange("values", oldValue, newValue)
/**
Установка цвета диаграммы
@param newValue the new color
*/
public void setGraphColor(Color newValue)
{
if (bean == null) return;
Color oldValue = bean.getGraphColor() ;■
bean.setGraphColor(newValue);
firePropertyChange("graphcolor", oldValue, newValue);
}
public void setObject(Object obj)
{
bean = (ChartBean)obj;
data.setText("") ;
doublet] values = bean.getValues();
for (int i= 0; i <values.length; i++)
data.append(values[i] + "\n");
normal.setSelected(Ibean.islnverse());
inverse.setSelected(bean.islnverse()) ;
titleField.setText(bean.getTitle());
for (int i = 0; i<position.length; i++)
position[i].setSelected(i == bean.getTitlePosition());
colorEditor.setvalue(bean.getGraphColor());
}
public Dimension getPreferredSize()
{
return new Dimension(XPREFSIZE, YPREFSIZE);
}
private static final int XPREFSIZE = 200;
private static final int YPREFSIZE = 120;
private ChartBean bean;
private PropertyEditor colorEditor;
private JTextArea data;
private JCheckBox normal;
private JCheckBox inverse;
private JCheckBox[] position;
private JTextField titleField;
)
java, beans. Customizer
I Г • Метод void setobject (Object bean) определяет настраиваемый компо- нентJavaBeans.
Контекст компонента JavaBeans
В данном разделе описываются способы создания компонентовJavaBeans, которые используют преимущества окружающей их среды. Эта технология очень полезна для реализации компонентов, которые могут взаимодействовать с другими компонентами или сервисами, которые предоставляет контекст данного компонента. В частности, здесь показан компонентJavaBeans, который может изменять значение произвольного целочисленного свойства в другом компоненте и использовать сервисы своего контекста.
К сожалению, в настоящее время наиболее распространенные интегрированные среды разработки (включая Forte) не поддерживают работу с контекстом компонента JavaBeans. Приведенные в этом разделе примеры работают в интегрированной среде разработки Box, но могут совсем не работать в используемой вами интегрированной среде разработки.
Использование интроспекции
В спецификации JavaBeans интроспекцией (introspection) называется процесс, при помощи которого интегрированная среда разработки определяет свойства, методы и события компонента. Интроспекция осуществляется следующими
За счет поиска классов и методов, удовлетворяющих стандартным соглашениям об именах.
На основе информационного классао.
Обычно интроспекция используется интегрированной средой разработки для анализа компонентов, но сами компоненты JavaBeans ее не выполняют. Однако, в некоторых случаях компоненту необходимо интроспекцию для анализа других компонентов.
шим примером является связывание пары компонентов в од-
8.23. Spin-компонентв интегрированной среде разработки. Рассмотрим в
Рис. 8.24. Конфигуратор формирующего компонент JavaBeansРис. 8.24. Конфигуратор формирующего компонент JavaBeansкачестве примера spin-компонент, т.е. небольшой элемент управления с двумя кнопками увеличения или уменьшения значения8.23).
Рис. 8.24. Конфигуратор формирующего компонент JavaBeans
Рис. 8.24. Конфигуратор формирующего компонент JavaBeans
Spin-компонент сам по себе бесполезен и используется только совместно с другими компонентами, например, он может быть соединен с компонентом IntTextBean. Каждый раз, когда пользователь щелкает на кнопке spin-компонента, значение в текстовом поле увеличивается или уменьшается. Компонент, связанный со spin- компонентом, называется дружественным (buddy) компонентом. Дружественным компонентом может быть не только компонент IntTextBean, но и любой другой компо- JavaBeans с целочисленным свойством.
Для соединения spin-компонента с дружественным компонентом используется настройщик, который показан на рис. 8.24.
Для организации такого соединения нужно выполнить следующие действия.
Откомпилируйте компоненты SpinBean и IntTextBean и упакуйте их в JAR- файлы.
Запустите интегрированную среду разработки ВеапВох.
Загрузите файлы SpinBean.j arи IntTextBean.j ar.
Добавьте в форму компоненты SpinBean и IntTextBean.
Откройте настройщик компонента SpinBean, выделяя этот компонент и выбирая команду меню EditOCustomize (Правка1^Настройка).
Выберите компонент IntTextBean в списке Buddy (Дружественные компоненты).
Обратите внимание, что в списке Property (Свойства) автоматически представлены все целочисленные свойства (рис. 8.24).
Выберите свойство value и щелкните на кнопке Set Buddy (Установить друж- ственный компонент).
Щелкните на кнопке Done (Готово).
Затем для проверки связи между компонентами щелкните на кнопках +/— и проследите за тем, как увеличивается/уменьшается значение в текстовом поле (рис. 8.25).
Рис. 8.25. Пример связи между компонентом SpinBean и дружественным компонентом IntTextBean
Этот простой на первый взгляд процесс имеет следующие две особенности.
Как найти все свойства компонента, значения которых имеют тип int?
Как запрограммировать установку и получение значения свойства компонента, если оно становится известно только во время выполнения программы?
Для решения обеих этих задач используется интроспекция (т.е. API-функции отражения). Чтобы проанализировать свойства компонента, сначала нужно получить информацию о нем с помощью статического метода getBeanlnfо класса Introspector.
Beaninfo info = Introspector.getBeanlnfо(buddy.getClass());■
После получения информации о классе следует получить массив объектов- описаний СВОЙСТВ.
Property-Descriptor [ ] props = info.getPropertyDescriptors();
Теперь в настройщике компонента SpinBean необходимо выбрать из полученного массива props все свойства типа int и включить их имена в компонентч:писок.
for (int i = 0; i < props.length; i++) {
Class propertyType = props[i].getPropertyType() ;
if (int.class.equals(propertyType))
{
String name = props[i].getName();
propModel.addElement(name);
Таким образом можно находить все нужные свойства компонента.
Затем, надо организовать процесс установки/возвращения значений для выбранного пользователем свойства. Для получения get/set-методов свойства следует вызвать методы getReadMethod и getWriteMethod соответственно.
Method getMethod = prop.getReadMethod() ;
Method setMethod = prop.getWriteMethod();
(Эти методы называются getReadMethod/getWriteMethod, вероятно, потому, что имена getGetMethod/setSetMethod звучат менее благозвучно.)
Теперь нужно вызвать методы для получения значения, увеличения и задания нового увеличенного значения. В таком случае используются API-функции отражения, которые подробно рассматриваются в главе 5 тома 1. Обратите внимание, что здесь для значения типа int необходимо использовать класс-оболочку Integer.
int value = ((Integer)getMethod.invoke(buddy,
null) ) .intValue() ;
value += increment;
setMethod.invoke(buddy,
new Object[] { new Integer(value) });
Можно ли обойтись без отражения, если потребовать, чтобы дружественный компонент имел методы getValue и setValue? Нет, в таком случае можно только вызвать метод buddy. getValue (), если компилятору известно, что компонент buddy является объектом класса, содержащего метод getValue.
int value = buddy.getValue();
Но компонент buddy может быть объектом любого класса, поскольку для компонентов JavaBeans не существует иерархии типов. Именно поэтому для связывания компонентов требуется использовать механизм интроспекции.
Поиск равноправных компонентов
В предыдущем разделе показано, как при помощи кнопок компонента SpinBean можно изменять значение дружественного компонента. Однако, в описании этого процесса не упоминалась одна важная проблема: как настройщик компонента SpinBean предоставляет пользователю все возможные дружественные компоненты.
Задача поиска и перечисления всех компонентов JavaBeans в данной форме гораздо сложнее, чем может показаться на первый взгляд. Например, в интегрированной среде разработки ВеапВох для поиска всех равноправных компонентов нельзя просто вызвать следующий метод.
Component!] siblings = getParent().getComponents()
Дело в том, что в интегрированной среде разработки ВеапВох каждый компонент заключен в свою собственную дополнительную панель. (Вероятно, это сделано для того, чтобы можно было обнаружить щелчок на данном компоненте и нарисовать
контур вокруг выделенного компонента.) Таким образом, в интегрированной среде разработки ВеапВох нужно использовать другой вызов.
Component[] siblings =
Однако, это вовсе не значит, что данное решение будет работать в других интегрированных средах разработки, поскольку эти среды могут иметь другую структуру и не нуждаться в стольких дополнительных панелях.
В платформе Java 2 данную проблему можно очень легко решить. В архитектуре JavaBeans теперь поддерживается понятие контекст компонента (bean context), который образует логическую оболочку между компонентами и их контейнерами, которая, как только что было показано, может отличаться от физической оболочки.
Контекст компонента может содержать другие компоненты, их сервисы и другие контексты компонентов подобно контейнеру AWT, который может содержать как компоненты, так и другие контейнеры (рис. 8.26).
Рис. 8.26. Контексты компонентов
В настоящее время вложенные контексты компонентов употребляются довольно редко. Чаще всего компоненты находятся в одном контексте, а именно, в интегрированной среде разработки. Например, интегрированная среда разработки ВеапВох в BDK 1.1 явля- етсятакимконтекстомкомпонентов.ВеапВохпредоставляет сервис отслеживания сообщений (message tracing service), который может использоваться всеми компонентами для отображения сообщений. Другие контексты компонентов могут предлагать какие-то другие сервисы отслеживания сообщений. В следующем разделе показано, как компоненты JavaBeans могут использовать такой сервис отслеживания сообщений.
Для взаимодействия с окружающим контекстом компонентуJavaBeans следует интерфейскоторый имеет следующие шесть методов.
void
BeanContext
voidname,
PropertyChangeListener listener)
voidname,
PropertyChangeListener listener)
voidname,
VetoableChangeListener listener)
voidname,
VetoableChangeListener listener)
Контекст компонента вызывает метод setBeanContext в начальный момент работы с компонентом. При этом ссылку на контекст можно сохранить в параметре BeanContext и использовать ее для получения доступа к контексту данного компонента.
Чтобы самостоятельно не реализовывать все слушатели событий, можно использовать вспомогательный класскоторый автоматически
выполнит всю рутинную работу. Однако, для использования этого класса нужно решить техническую проблему. Поскольку компонент не может одновременно наследовать класс Component и класс BeanContextChildSupport, то для его реализации необходимо использовать механизм промежуточного объекта.
Реализуйте интерфейс BeanContextProxy.
Создайте переменную экземпляра типа
private BeanContextChildSupport childSupport = new
Возвратите эту переменную экземпляра в методе getBeanContextProxy интерфейса BeanContextProxy.
public BeanContextChild getBeanContextProxy
{
return childSupport;
}
Контекст компонента вызывает данный метод для нахождения промежуточного объекта. Затем вызывает методы класса BeanContextChild для полученного объекта, а не для самого компонента.
Для получения текущего контекста компонента следует вызвать метод getBeanContext объекта childSupport.
BeanContext context
На8.27 показана схема связей между этими классами.
После получения контекста компонента можно перечислить все компоненты, которые содержатся в этом контексте. Класс BeanContext реализует интерфейс Collection, поэтому компоненты можно легко упорядочить следующим образом.
Iterator iter = while {
Object buddy =
// какие-то действия с дружественным компонентом
Рис. 8.27. Схема связей между классами контекста компонента
Однако, этот подход не годится в том случае, если форма будет содержать несколько равноправных компонентов одного класса, так как пользователь не сможет их различить. Например, в предыдущем примере было бы лучше, если бы можно было расположить компонент SpinBean вместе с его дружественным компонентом или нарисовать вокруг них рамку. Однако, в классе BeanContext не предусмотрено использование какой-либо информации о расположении компонентов. (Совершенно бесполезно запрашивать у дружественного компонента информацию о его размещении,
потому что ему известно свое расположение только относительно окружающей его панели.) Возможно в будущей версии спецификации JavaBeans будет предусмотрена поддержка подобных манипуляций.
Сервисы контекста компонента
В данном разделе описываются способы использования сервисов, предоставляемых контекстом компонента. Например, сервис отслеживания сообщений, предусмотренный в интегрированной среде разработки ВеапВох, реализован в классе sunw.demo.metodtracer.MetodTracer. Сначала следует убедиться в том, что контекст компонента реализует интерфейс BeanContextServices, потому что далеко не все контексты реализуют его.
if (beanContext implements BeanContextServices) {
BeanContextServices services =
}
Затем нужно проверить, поддерживает ли контекст компонента нужный сервис.
tracerClass = Class.forName
("sun.demo.methodtracer.MethodTracer");
if (services.hasService(tracerclass)) (
}
Если этот компонент запустить не в интегрированной среде разработки ВеапВох, а в какой-то другой среде, то метод hasService может возвратить значение false.
Наконец, необходимо получить объект, который выполняет нужный сервис, с помощью метода getService, содержащего пять параметров.
Объект, реализующий интерфейс ChildBeanContext; обычно это промежуточный объект BeanContextChiIdSupport.
Запрашивающий объект.
Класс необходимого сервиса.
Вспомогательный объект для выделения необходимого сервиса, если это требуется для данного сервиса, или значение nullв противном случае.
ОбъектBeanContextServiceRevokedListener, который вызывается при прекращении работы сервиса.
Вот как выглядит стандартный вызов этого метода.
=
new BeanContextServiceRevokedListener() {
public void serviceRevoked
(BeanContextServiceRevokedEvent event)
{
tracer = null;
}
tracer = services.getService(childsupport, this, tracerclass, null, revokedLi stener) ;
Учтите, что классы сервисов обычно не доступны при компиляции программы. Например, файл класса MetodTracer не является частью стандартной библиотеки, и компилятор не найдет его, даже если попытаться импортировать пакет sunw. demo .metodtracer,в котором этот класс находится.
По этой причине для вызова методов из классов сервиса используется механизм отражения. В приведенном ниже коде для отображения сообщения в диалоговом окне отслеживания сообщений вызывается метод logText класса MethodTracer.
if (tracer != null) {
String text = "spin: value=" + value
+ " increment=" + increment;
Method logText = tracerclass.getMethod("logText", new Class[] { String.class });
logText.invoke(tracer, new Object[ ] { text }); }
Конечно, если данный сервис не поддерживается контекстом компонента, то этот метод не вызывается. В таком случае используется метод invoke для его вызова с помощью механизма отражения.
После запуска этой программы понаблюдайте за диалоговым окном отслеживания сообщений, которое показано на рис. 8.28. После каждого щелчка на какой-либо кнопке компонента SpinBean в нем будут появляться соответствующие сообщения.
Рис. 8.28. Окносообщений
В листингах 8.17-8.19 содержится полный код компонента SpinBean, включая необходимый для настройщика информационный класс.
ЛистингЙ.17.toinBean.java
/**
aversion 1.201999-10-02
©author Cay Horstmann
*/
import java.awt.*;
import java.awt«event.*;
import j ava.beans.*;
import java.beans.beancontext.*;
import java.lang.reflect.*;
import java.io.*;
import java.util.*;
import j
/**
Компонент с кнопками +/- для увеличения/уменьшения значения свойства в дружественном компоненте
*/
public class SpinBean extends JPanel
implements BeanContextProxy
{
public SpinBean()
{
setLayout (new GridLayout (2, 1));
JButton plusButton = new JButton("+");
JButton minusButton = new JButton(”-”);
add(plusButton) ;
add(minusButton);
plusButton.addActionListener(new
ActionListener()
{ public void actionPerformed(ActionEvent evt) {
spin(1) ;
}
}) ;
minusButton.addActionListener(new
ActionListener()
{ public void actionPerformed(ActionEvent evt) {
spin(-l);
} });
childSupport = new
BeanContextChildSupport()
{
public void setBeanContext(BeanContext context) throws PropertyVetoException
{
super.setBeanContext(context); setTracer(context);
} };
public BeanContextChild getBeanContextProxy() {
return childSupport;
}
/**
Установка дружественного компонента для компонента SpinBean
Sparam b the buddy component
@param p the descriptor of the integer property to increment or decrement
*/
buddy = b; prop = p;buddy = b; prop = p;public void setBuddy(Component b, PropertyDescrip,tor p) {
buddy = b; prop = p;
buddy = b; prop = p;
/**
Увеличение или уменьшение значения
свойства в дружественном компоненте
@param increment the amount by which to change the buddy's value
*/
public void spin(int increment)
{
if (buddy =='null) return;
if (prop == null) return;
Method readMethod = prop.getReadMethod();
Method writeMethod = prop.getWriteMethod();
try
{ int value = ((Integer)readMethod.invoke(buddy, null)) .intValue() ;
if (tracer != null)
{ String text = "spin: value=" + value
+ " increment=" + increment;
Method logText = tracerClass.getMethod("logText", new Class[] { String.class });
logText.invoke(tracer, new Object[] { text }); }
value += increment;
writeMethod.invoke(buddy, new Object!] { new Integer(value) })
}
catch(Exception e)
)
/**
Попытка установления сервиса отслеживания сообщений @param context the bean context that may provide the service.
*/
public void setTracer(BeanContext context) (
try
BeanContextServices services =
tracerClass = Class.forName
("sunw.demo.methodtracer.MethodTracer");
if (services.hasService(tracerclass))
{ BeanContextServiceRevokedListener revokedListener new BeanContextServiceRevokedListener()
{ public void serviceRevoked (BeanContextServiceRevokedEvent event) { tracer = null;
} );
tracer = services.getService(childsupport, this, tracerClass, null, revokedListener);
}
}
catch (Exception exception)
{
tracer = null;
}
}
public Dimension
{
return newMINSIZE);
private static final int MINSIZE = 20;
private Component buddy;
private PropertyDescriptor prop;
private Object tracer;
private Class tracerClass;
private BeanContextChildSupport childSupport;
Java
/**
©version 1.20 1999-09-28
©author Cay
*/
import
importj
import
j
j
import
import java.util.*;
j
import
/**
Настройщик компонента SpinBean, предназначенный для выбора его дружественного компонента
*/
public classextends JPanel
implements
{
public
{
setLayout(new GridBagLayout());
GridBagConstraints gbc = new
=
=
=
=
gbc, 0, 0, 1, 1) ;
add(newgbc, 0, 1, 1, 1) ;
= 100;
=
=
buddyModel = new
propModel = new
buddyList = new
propList = new JList(propModel);
add(new JScrollPane(buddyList), gbc, 1, 0, 1, 1);
add (new JScrollPane (propList) , gbc, 1, 1, 1, 1);
JButton setButton = new JButton("Set Buddy");
JPanel p = new JPanel();
p.add(setButton);
add(p, gbc, 0, 2, 2, 1);
buddyList.addListSelectionListener(
new ListSelectionListener()
{
public void valueChanged(ListSelectionEvent event)
{
findBuddyMethods() ;
}
});
setButton.addActionListener(
new ActionListener()•
{
public void actionPerformed(ActionEvent event)
{ int buddyindex =
if (buddylndex < 0) return;
int propindex =
if (proplndex < 0) return;
bean.setBuddy(buddies[buddyindex] , props[propindex]);
}) ;
/**
Вспомогательный метод для включения компонента в заданное место @param с the component to add
@param gbc the grid bag constraints to use
@param x the x grid position
@param у the у grid position
@param w the grid width
Sparam h the grid height
*/
public void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
add(c, gbc) ;
/**
Поиск методов выбранного дружественного компонента */ public void findBuddyMethods()
{
int buddylndex = buddyList.getSelectedIndex() ;
if (buddylndex < 0) return;
Component buddy = buddies[buddyindex] ;
propModel. removeAHElements () ;
try
{
Beaninfo info
=;
props = info.getPropertyDescriptors();
intj =0;
{{for (inti=0; i<props. length; i++)
{
{
Class propertyType =
i{f (int.class.equals(propertyType))
String name = props[ i ] .getName() ; propModel.addElement(name) ;
props[j++] =
catch(IntrospectionExceptione){}
public Dimension getPreferredSize()
} returnnewDimension(300, 200);
p{ublic void setobject(Object obj) bean = (SpinBean)obj;
BeanContext context
=
buddies = new Component[context.size()]; buddyModel. removeAHElements();
Iterator iter = context.iterator();
inti=0;
while (iter.hasNext())
{ Object buddy =iter.next();
if (buddy instanceof Component)
{ buddies[i] =
String className =
buddyModel.addElement(className);
}
}
private DefaultListModel buddyModel private DefaultListModel propModel; private Property-Descriptor [ ] props; private Component[ ] buddies;
/**
@version 1.10 1997-10-27
eauthor Cay Horstmann
import j ava.awt.*;
import j ava.beans.*;
/**
Информационный класс компонента ChartBean, в котором указан его настройщик
*/
public class SpinBeanBeanlnfo extends SimpleBeanlnfo {
public BeanDescriptor getBeanDescriptor() {
return new BeanDescriptor(SpinBean.class,
j SpinBeanCustomizer.class);
}
java.beans.Introspector
Метод String decapitalize(String name) преобразует строку в COOT ветствии с соглашением об именах, принятым для платформы Java. Например, строка SillyMethod преобразуется в строку sillyMethod. (Если подряд идут две прописные буквы, то они не преобразуются в строчные.)
Метод Beanlnfо getBeanlnfо (Class beanClass) возвращает связанный с компонентом информационный класс Beanlnfо или создает его "на лету" на основе соглашения об именах, которое рассматривалось выше в этой главе. При возникновении ошибок во время интроспекции генерируется исключительная ситуация IntrospecticnException.
java.beans.beancontext.BeanContextChiId
Метод void setBeanContext (BeanContext be) используется для указания контекста для данного компонента. Он генерирует событие изменения свойства для всех запрещаемых слушателей событий, а затем для всех слушателей событий изменения свойства.
Метод BeanContext getBeanContext () возвращает контекст данного компонента.
МетодaddPropertyChangeListener(Stringname,Prop
ertyChangeListener listener) создает слушатель для указанного свойства.
Метод removePropertyChangeListener(String name, PropertyChangeListener listener) удаляет слушатель для указанного свойства.
Метод addVe toableChangeListener(Stringname, Vetoable-
ChangeListener listener) создает слушатель запрещаемых событий для указанного свойства.
Метод removeVetoableChangeListener(String name, Vetoable- ChangeListener listener) удаляет слушатель запрещаемых событий для указанного свойства.
java.beans.beancontext.BeanContextChiIdSupport
Метод BeanContext getBeanContext () возвращает текущий контекст для данного компонента.
java, beans. beancontext. BeanContextProxy
Метод BeanContextChild getBeanContextProxy () возвращает промежуточный объект, который обслуживает методы класса BeanContextChiId.
java.beans.beancontext.BeanContextServices
e Метод boolean hasService(Class cl) проверяет, поддерживает ли данный контекст сервис класса cl.
Метод ObjectgetService(BeanContextChild child,Object
requestor, Class cl, Object selector, BeanContextServiceR- evokedListener listener) возвращает объект, который осуществляет сервис контекста компонента.
Параметры:
childдочерний объект контекста компонента
requestor объект, запрашивающий сервис clкласс сервиса
selector необязательный объект для поиска или инициализации сервиса listenerслушатель, который уведомляется, когда сервис становится
недоступным
java. beans. beancon text. BeanCon textServiceRevokedLis tener
Метод void serviceRevoked(BeanContextServiceRevokedEvent
event) вызывается при отмене сервиса. Необходимо убедиться в том, что после этого уведомления данный сервис не будет вызываться.
java.beans.beancontext.BeanContextServiceRevokedEvent
Метод Class getServiceClass () возвращает класс, который осуществляет отмененный сервис.
Метод boolean isServiceClass (Class cl) проверяет, осуществляет ли данный класс функции отмененного сервиса.
Безопасность
в этой главе...
Загрузчики классов
т Верификация байт-кода
Диспетчеры безопасности и полномочия
Цифровые подписи
т Подписание кода
т Шифрование
1 Появление технологии Java вызвало восхищение специалистов не только в отношении хорошо продуманного языка, но и в отношении безопасности выполнения аплетов, доставленных через Internet (более подробно аплеты рассматриваются в главе 10 тома 1). Понятно, что доставка исполняемых аплетов имеет смысл только тогда, когда получатели уверены, что код не может нанести ущерб работе их компьютеров. Поэтому вопросы безопасности были и остаются основной заботой как разработчиков, так и пользователей технологии Java. Это означает, что в отличие от других языков и систем, где безопасность обеспечивалась в последнюю очередь или для того, чтобы снизить вероятность повреждений, механизмы обеспечения безопасности изначально являются неотъемлемой частью технологии Java.
В технологии Java безопасность обеспечивают следующие три механизма:
конструктивные особенности языка (проверка границ массивов, выполнение только допустимых преобразований типов, отсутствие указателей и т.д.);
механизм контроля доступа, который контролирует выполняемые кодом действия (доступ к файлам, доступ к сети и т.д.);
подписание кода, благодаря чему авторы кодов могут использовать стандартные алгоритмы шифрования для аутентификации созданного ими кода на языке Java. Таким образом пользователи кода смогут точно определить, кто создал код и был ли код изменен после его подписания.
Виртуальная машина fava осуществляет проверку наличия неверных ссылок, выхода за границы массивов и т.н. Кроме того, контролируются объекты, отправляемые виртуальной машинеJava.
Во время загрузки файлов классом в виртуальную машину проверяется их целостность. В этой главе описаны подробности данного механизма загрузки классов. Что еще более важно., здесь описываются способы создания собственного загрузчика классов (class loader) для контроля за объектами, которые передаются вир туальной машинеJava.
Для максимального обеспечения безопасности используемый по умолчанию загрузчик классов и пользовательский загрузчик должны юаимодействовать с диспетчером безопасности (security manager), который контролирует выполняемые кодом: операции. Б настоящей главе описываются способы создания собствешъзго диспетчера безопасности.
Наконец, здесь рассматриваются алгоритмы шифрования, предусмотренные в пакете java. security, которые гфеделтивляют возможность подписывать код и аутентифицировать пользователей.
Как обычно, основное внимание уделяется тем вопросам, которые представляют наибольший интерес для пршишдиых программистов. Тем, кто хотел бы более глубоко изучить данную тему, рекомендуется прочесть книгу Inside Java 2 Platform Security Ли Гонга (Li Gong) [Addison-Wesley 1999].
Загрузчики классов
Компилятор языка Java преобразует исходный код на языке Java В КОД на внутреннем языке гипотетической виртуальной машины (virtual machine). Код виртуальной машины хранится в файлах классов с расширением . class. Файл класса содержит код всех методов своего класса. Эти файлы интерпретируются программой, которая способна преобразовать набор инструкций виртуальной машины в машинные команды используемого компьютера.
Учтите, что интерпретатор виртуальной машины загружает только те файлы классов, которые необходимы для выполнения программы в данный момент. Допустим, что выполнение программы начинается с файла класса Му Program.class. Ниже описаны действия, которые выполняет виртуальная MatuHHaJava.
В виртуальной машине имеется механизм загрузки файлов классов, например, считывание файлов с диска или но сети. Таким образом, этот механизм используется для загрузки содержимого файла класса MyProgram.
Если в классе MyProgram имеются переменные экземпляров или суперклассов другого типа классов, то файлы этих классов также загружаются. (Процесс загрузки всех классов, от которых зависит данный класс, называется разрешением (resolving) класса.)
Затем виртуальная машина выполняет метод main класса MyProgram. (Этот метод является статичным, поэтому никаких экземпляров классов создавать не требуется.)
Если для работы метода main или метода, вызываемого методом main, требуются дополнительные классы, то загружаются также и файлы классов.
Следует заметить, что механизм загрузки классов использует не один, а несколько загрузчиков классов. С каждой программой Java используется, по крайней мере, три загрузчика классов:
первичный загрузчик классов{bootstrap class loader);
расширенный загрузчик классов(exit nsion classloader);
системный загрузчик классов{system class loader), иногда называемый прикладным загрузчиком классов (application class loader).
Первичный загрузчик классов загружает системные классы, как правило, из файла rt.j ar. Он является неотъемлемой частью виртуальной машины и обычно реализуется на языке С. Первичному загрузчику классов не соответствует никакой объект типа ClassLoader. Например, показанный ниже ВЫЗОВ метода возвращает значение null.
String.class.getClassLoader()
Расширенный загрузчик классов загружает стандартное расширение из каталога jre/lib/ext. Если переместить файлы . jar в этот каталог, то расширенный загрузчик классов найдет в нем нужные классы расширения даже без указания пути к классам. (Некоторые специалисты рекомендуют использовать этот механизм, чтобы избежать путаницы с указанием пути к классам, но такой способ связан с некоторыми сложностями, которые рассматриваются далее в разделе.) Расширенный загрузчик классов реализуется на языке Java.
Системный загрузчик классов загружает прикладные классы, которые размещаются в каталогах и файлах форматаJAR/ZIP на пути К классам, который указан г. помощью переменной окружения CLASS PATH или параметра командной строки — classpath. Данный загрузчик классов также реализуется на языкеJava.
Загрузчики классов связаны отношениямиродитель/ потомок. У каждого загрузчика классов, за исключением первичного, имеется родительский загрузчик классов. Предполагается, что загрузчик классов дает шанс своему родителю загрузить любой нужный класс и загружает его сам только в том случае, если этого не может сделать родитель. Например, если системный загрузчик классов запрашивает загрузку системного класса (например, java.util. arrayList),то прежде он предлагает загрузить его расширенному загрузчику классов. Расширенный загрузчик классов, з свою очередь, предлагает сделать это первичному загрузчику классов. Наконец, первичный загрузчик классов находит и загружает класс из файла rt .jar, причем все другие загрузчики классов прекращают дальнейший поиск.
Допускается создание загрузчика классов, который не делегирует загрузку классов своему родителю. Такой загрузчик может даже загружать свою собственную D версию системных классов, но это не дает никаких дополнительных преимуществ.
В данном случае также нет никакого риска для системы обеспечения безопасности, потому что класс может загрузить новый код с помощью загрузчика классов, но не может изменить собственный загрузчик классов.
Аплеты, сервлеты и RMI-заглушки загружаются пользовательскими загрузчиками классов. В особых случаях можно создать собственный загрузчик классов, который предоставит возможность проводить специализированную проверку байт кода перед его передачей виртуальной машине.
Например, можно создать собственный загрузчик, который сможет отказывать в загрузке класса, не помеченного знаком "оплачено", О том, как это сделать, рассказывается в следующем разделе.
При наличии загрузчика классов новый класс загружается следующим образом.
ClassLoader loader = new MyClassLoader();
String className = "...";
Class cl =(className);
После этого класс загружает зависимые классы.
При этом хотелось бы знать, какой именно загрузчик классов отвечает за работу. Напомним еще раз способы загрузки классов.
Неявная загрузка классов происходит, если класс разрешен, т.е. загружаются классы, от которых он зависит.
Явная загрузка классов без указания имени загрузчика класса происходит с помощью метода Class.forName(className).
Явная загрузка классов происходит с помощью метода загрузчика классов loader.loadClass(className).
В первом случае в процессе разрешения используется тот же загрузчик классов, который использовался для загрузки исходного класса. Во втором случае используется тот загрузчик класса, метод которого содержит команду Class. f orName. В третьем случае за процесс отвечает заданный загрузчик классов. Этот случай представляет собой единственный способ переключения на новый загрузчик классов.
Очень серьезные последствия могут возникнуть, если переместить JAR-файл в каталог jre/lib/ext, тогда как один из его классов вызывает метод Class .forName. В таком случае для поиска класса будет использоваться только расширенный загрузчик классов, а не системный. При этом совершенно не учитывается путь к классам, что необходимо иметь в виду при использовании каталога jre/lib/extдля упрощения доступа к файлам классов.
Чтобы не попасть в ловушку, нужно выяснить, что требуется для динамической загрузки классов в расширенной библиотеке. Для получения системного загрузчика классов можно обратиться к статическому методу ClassLoader.getSystemClassLoader. Однако, этот загрузчик классов может оказаться неподходящим для использования его при любых обстоятельствах. Ведь расширенный код может вызываться из класса, который был загружен другим загрузчиком классов. Для решения данной проблемы каждый поток имеет "контекстный загрузчик классов", т.е. загрузчик, который лучше всего подходит при выполнении кода в данном потоке. Чтобы решить проблему, следует вызвать метод loadclass с указанием имени этого загрузчика вместо вызова метода Class. f orName.
Thread t = Thread.currentThread();
ClassLoader loader = t.getContextClassLoader();
Class cl = loader.loadclass(className);
Или наоборот, перед запуском нового потока можно установить контекстный загрузчик классов.
Thread t = newThread(...);
t.setContextClassLoader(myClassLoader);
t.start();
Если контекстный загрузчик классов не установлен, то поток использует контекстный загрузчик классов, который установлен для родительского потока. Для главного потока приложения в качестве контекстного загрузчика используется системный загрузчик классов.
При размещении JAR-файлов в каталоге jre/lib/ext легко угодить в другую ловушку. Иногда программисты забывают о тех файлах, которые поместили в этот каталог ранее, а потом не могут понять, почему загрузчик классов игнорирует заданный путь к классам и загружает старые классы из каталога jre/lib/ext.
Каждый программист знает, что для исключения конфликтов, возникающих при использовании одинаковых имен для разных классов, используются имена пакетов. В стандартной библиотеке Java имеется два класса с одинаковым коротким именем Date и разными полными именами java.util. Date и java. sql. Date. Короткие имена предназначены только для создания более читабельного кода и требуют добавления соответствующих операторов import. В работающей программе все имена классов приводятся полностью и содержат имена своих пакетов.
Удивительно, что в рамках одной и той же виртуальной машины могут существовать два разных класса, которые имеют одно и то же имя класса 11 пакета. Дело в том, что класс определяется но его полному имени и загрузчику класса. Такая технология весьма практична при загрузке кода из нескольких источников. Например, Web-броузер использует отдельные экземпляры загрузчика аплетных классов для каждой Web- страницы. Это позволяет виртуальной машине различать классы из разных Web- страниц независимо от их имен.
Существуют и другие способы использования данной технологии, например, "горячая доставка" сервлетов и компонентов Enterprise JavaBeans. Более подробную информацию по этой теме можно найти по адресу http://developer.java.sun.com/ developer/TechTips/2000/ttl027.html.
Создание собственного загрузчика классов
Чтобы создать собственный загрузчик классов, следует просто расширить класс ClassLoaderи переопределить метод findClass (String className).
Метод loadClass суперкласса ClassLoader отвечает за делегирование родителю функций загрузки классов и вызывает метод findClass в том случае, если класс еще не загружен или если загрузчик классов родителя не смог его загрузить.
В ранних версиях SDK программистам приходилось переопределять метод loadclass, но теперь этого делать не рекомендуется.
Реализацию метода нужно выполнять следующим образом.
Загрузить байт-код класса из локальной файловой системы или из какого-то иного источника.
Вызвать метод defineClass суперкласса CiassLoader для представления байтКОДОВ виртуальной машине.
В примере программы, код которой приводится в листинге 9.1, используется загрузчик зашифрованных классов. Программа запрашивает у пользователя имя первого класса для загрузки (т.е. класса, содержащего метод main) и ключ шифрования. Затем специальный загрузчик классов используется для загрузки заданного класса и вызова метода main. Загрузчик классов расшифровывает заданный класс и все несистемные классы, которые им используются. Наконец, программа вызывает метод mainзагруженного класса. Внешний вид программы показан на рис. 9.1.
Рис. 9.1. Программа ClassLoaderTestРис. 9.1. Программа ClassLoaderTest
Рис. 9.1. Программа ClassLoaderTest
Рис. 9.1. Программа ClassLoaderTest
В целях упрощения изложения в этом примере игнорируется более чем 2000летний опыт и области криптографии, и используется древний шифр Цезаря для шифрования файлов классов.
В прекрасной книге The Codebreakers Дэвида Кана (David Kahn) [Macmillan, NY, 1967, p.84] со ссылкой на Светония (Suetonius) говорится, что Цезарь кодировал 24 буквы латинского алфавита, СДЗИ5 ая их на 3 буквы. Во время создания этой книги правительство США ограничивало экспорт наиболее сложных методов шифрования. Поэтому использование довольно простого метода Цезаря в нашем примере не нарушит экспортные ограничения.
В данном версии тайнописи Цезаря в качестве ключа используется число между 1 и 255. Для шифрования байта просто складываются этот ключ и каждый байт, а результат потом сокращается ни модулю 25G. Алгоритм шифрования приводится в программе Caesar .j ava, код которой находится H листинге 9.2.
Чтобы не поставить в тупик стандартный загрузчик классов, для зашифрованных файлов классов используется расширение . caesar.
Для расшифровки загрузчик классов вычитает значение ключа из каждого байта. На Web-странице настоящей книги предлагается четыре файла классов, зашифрованных с помощью традиционного значения ключа 3. Эти классы нельзя загрузить посредством стандартного интерпретатора байт-кода, но молено запустить, применяя пользовательский загрузчик классов, описанный в программе ClassLoaderTest.
Зашифрованные файлы классов имеют самое разное практическое применение (если, конечно, используется более сложный шифр, чем шифр Цезаря). Без ключа шифрования эти файлы классов бесполезны, потому что не могут выполняться с помощью стандартного интерпретатора байт-кода, и их нелегко дизассемблировать.
Это значит, что для аутентификации пользователя класса или для проверки ТОГО, что программа оплачена до ее запуска, можно применять пользовательский загрузчик классов. Естественно, шифрование является "только одним из возможных приложений пользовательского загрузчика классов. Другие типы загрузчиков классов можно использовать для решения иных проблем, например, для сохранения файлов классов в базе данных.
Листинг9.1. ClassL-oadavTest da-та
/**
(Aversion 1.20 2C01-G8-23
^author Cay Horshmann
*/
import j import j import ;ava.lang.reflect import ;, ava. awt. * ;
import - ava. awt. event.. *;
import j
/**
Пример чолъчсва^члеского зегвуччик^ классов , который расшифровывает классы файлов
*/
public с lass ClassLoaderTest {
public static void ma в ( Ctrl Ogs ] ахав)
JFrame frame ■ new ClarvlcaderFraiKlh
frame . set Def aul tCloseOperat. ion (J Frame . ЕХТТ_/Ж_.СЬС5Е i ; frame.show(: ;
}
/**
Фрейм с двумя ТЕКСТОВ ЫМ 14 полями для ввода имек гл загружаемого файле ключа шифрования
*
class ClassLoaderFrame extends JFrane {
public ClassLoaderFrame() {
setTitlefClassLoaderTest") ;
setsize(WIDTH, HEIGHT);
cetContentPaneti. set.La.ycut(newGridBagLayout(); ; GridBagConstraints gbe = new GridBagConstraints();
=
gbe.we i =
gbc.fill = GridBagConstraints.NONE;
gbc. anchor = GridBagСоnstr ai nts .
add (new JLabel I "Class”) , gbc, 0, 0, 1, 1) ;
add (new JLabel ( "Key" ) . gbc, 0, 1, 1, 1) ;
gbe.weightx = 100;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor =
add(nameField, gbc, 1, 0, 1, 1) ;
add(keyField, gbc, 1, 1, 1, 1) ;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor =
JButton loadButton = new JButton("Load");
add(loadButton, gbc, 0, 2, 2, 1);
loadButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
runClass(nameField.getText(), keyField.getText(j*;
D ;
/**
Вспомогательный метод для включения
компонента в заданное место
@param с the component to add
@param gbc the grid bag constraints to use
@param x the К grid position
Sparain у the у grid position
Spararn w the grid width
*/*/Sparam h the grid height
*/
*/
public void add(Component c, GridBagConstraints gbc, intint y, int w, int h)
gbc.gridx = x;
gbc.gridyy;
gbc.gridwidth - w;
gbc:. gridheight = h;
getContentPane().add(c, gbc);
Выполнение метода main указанного класса
@param name the class name
Sparam key the decryption key for the class files
*/
public void runClass(String name, String key) {
try {
ClassLoader loader
= new CryptoClassLoader(Integer.parselnt(key));
Class с = loader.loadclass(name);
String[] args = new String[] {};
Method m = c.getMethod("main", new Class[] { args.getClass() });
m.invoke(null, new Object[] { args });
}
catch (Throwable e) {
JOptionPane.showMessageDialog(this,e); }
}
private JTextField keyField = new JTextField("3", 4) private JTextField nameField = new JTextField(30);
private static final int WIDTH = 300;
private static final int HEIGHT = 200;
/ * *
Загрузчик зашифрованных файлов классов
*/
class CryptoClassLoader extends ClassLoader {
/**
Создание загрузчика зашифрованных файлов классов. Sparain k the decryption key
*/
public CryptoClassLoader(int k)
{
key = k;
}
protected Class findclass(String name) throws ClassNotFoundException
{
byte[] classBytes = null;
try
{
classBytes = loadClassBytes(name);
} catch (IOException exception)
{
j throw new ClassNotFoundException(name);
Class cl =classBytes,
0, classByt.es. length); if (cl == null)
throw new ClassNotFoundException(name) ; return cl;
}
/**
Загрузка и расшифровка байтов файла класса
@param name the class name
Sreturn an array with the class file bytes
*/
private byte[] loadClassBytes(String name) throws IOException
{
Stringcname =(' . ', '/') +".caesar11 '•
Fileinputstream in = null;
try
{
in = new Filelnputstream (cname) ByteArrayOutputStream buffer
= new ByteArrayOutputStream();
int ch;
while ((ch =!= -1)
{
byte b = (byte)(ch - key); buffer.write(b);
in.close();
return
finally if ! in.close():
private Map classesnew
privatekey;
Листинг
(4 v r 00 97-
Cay
io.*;
Шифрует файл на основе шифра Цезаря
public class Caesar г
[
.!-- 3}
:Caesar in out key") ,
, return:
f
innew
outnew
= Integer(args [2] );
i.nt
while
(byte) (ch key);
out.write(c) ;
in.close();
cut.close() ;
!on exception)
{
except
java.lang.Class
Метод ClassLoader getClassLoader() возвращает загрузчик классов, который загружает данный класс.
java.lang,Cla ssLoader
Метод ClassLoader ge 13e rent () возвращает родительский загрузчик классов или значение mi 1J . если родительский загрузчик классов являет- с.я первичным aa/pv:TjL.KOiT.
Метод г,critic ClassLoader getSystewClassboader () возвращает системный загруерк классов. .те.то;1классов, который исполь-
зовался для тгрхудм кгсивого класса приложения.
Метод protected Cc.l^s zi^Qa.sг: (String name) необходимо переопределить в 31Гр\5ЧИЕ• : К-Чассои 5ЛЯ поиска байт-кода класса и представления еговирту • иыоймашши £ помощью вызова метода defineClass.
Параметры:
nameимя класса; в качестве разделителя в нем следует использовать
символ-точку '. (как при указании имени пакета), а не суффикс .class
е Метод Class defineClass (String name, byte[] data, int offset, int length) передает новый класс виртуальной машине.
Параметры:
nameимя класса* в качестве разделителя в нем следует использовать
символ-точку . " (как при указании имени пакета), а не суффикс . class
dataмассив, хранящий байт-коды класса
offsetначало байт-кодов в массиве
lengthдлина байт-кодов в массиве
java.lang.Thread
Метод C1 a s s L о a de r g e t С о n t ext С1 a s s L о a d e r {) возвращает загрузчик классов, который обозтзачеи создателем этого потока. в качестве наиболее приемлемого для использования в данном потоке.
Метод void setContextС1assLoadsr {ClassLoader loader) задает контекстный загрузчик классов в данном потоке. Если при запуске потока контекстный загрузчик классов tic задан явно, то используется контекстный загрузчик родителя.
Верификация байт-кода
Когда загрузчик классов представляет виртуальной машине байт-коды вновь загруженного класса, эти байт-колы сначала исследуются верификатором (x’erijwY Верификатор проверяет все классы за исключением системных классов и определяет те команды, которые могут нанести ущерб виртуальной матине. Верификацию можно отменить с помощью недокументированного параметра-- neveri £ у.
java —noverify Hello
Ниже приведен перечень некоторых видов проверок, которые производятся верификатором.
Инициализация переменных перед их использованием.
Согласование типов объектных ссылок при вызове метода.
Соблюдение правил доступа к закрытым данным и методам.
Доступ к локальным переменным в рамках стека.
• Отсутствие переполнения стека.
В случае нарушения какого-либо из этих условий класс считается поврежденным и загружаться не будет.
Теорема Гёделя (G6del) утверждает, что невозможно спроектировать алгоритмы, где входными данными являются программы, а выходными данными — логические Dзначения, которые характеризуют какое-то свойство программы (например, пере-
полняемость или непереполняемость стека). В таком случае возникает вопрос: каким образом верификатор может определить, что в файле класса не соблюдается соответствие типов, используются неинициализированные переменные и переполняется стек. Нет ли здесь противоречия с законами логики? Нет, потому что верификатор не является алгоритмом принятия решения в смысле теоремы Гёделя. Дело в том, что верификатор одобряет далеко не все безопасные, а только некоторые наиболее безопасные программы, и по указанным критериям может отвергнуть многие другие фактически безопасные программы.
Такая строгая верификация предложена по соображениям безопасности. Случайные ошибки, связанные с использованием неинициализированных переменных, могут с легкостью причинить заметный ущерб программе, если их вовремя не определить. Еще важнее то, что в широко открытом мире Internet необходимо обеспечить надежную защиту от злонамеренных попыток недобросовестных программистов, стремящихся причинить ущерб вашим программам. Например, если бы можно было модифицировать значения в стеке исполняемой программы или изменять значения закрытых данных системных объектов, то любая программа могла бы нарушить систему защиты Web-броузера.
Зачем же нужно создавать специальный верификатор для проверки всех этих условий? Ведь компилятор все равно не позволит сгенерировать файл класса, в котором будет использоваться неинициализированная переменная или область закрытых данных будет доступна из другого класса. Действительно, файл класса, генерированный компилятором языка Java, всегда проходит верификацию. Однако, используемый в этих файлах формат байт-кодов хорошо документирован, и опытный программист на языке ассемблера и в шестнадцатеричном редакторе может легко вручную создать файл класса с подтвержденными, но опасными командами для виртуальной машины Java. Кроме того, учтите, что верификатор всегда блокирует злонамеренно измененные файлы классов, а не только проверяет созданные компилятором файлы классов.
Ниже приведен пример создания измененного вручную файла класса. Начнем с программы VerifierTest .java(листинг 9.3). Эта простая программа вызывает метод fun и выводит результат его выполнения. Она может быть запущена в режиме командной строки или как аплет. Метод funвыполняет элементарное сложение чисел 1 и 2.
static int fun!)
int m;
int n;
m=1;
n = 2;
intr=m+n; return r;
}
В качестве эксперимента попытаемся скомпилировать следующую модификацию этой программы.
static int fun()
{
int m=1;
intn;
m=1;
m - 2;
intr=m+ П;
return r;
}
В данном случае переменная п не инициализирована и может иметь произвольное значение. Естественно, компилятор обнаружит эту проблему и откажется компилировать программу. С целью создания недопустимого файла класса запустим программу javap, чтобы выяснить, как компилятор транслирует метод fun.
javap —с VerifierTest
Приведенная выше команда показывает байт-коды файла класса в мнемонической форме.
Method int fun()
0 iconst_l
istore_0
iconst_2
istore_l
iload_0
iload_l
iadd
istore_2
iload_2
ireturn
Для изменения инструкции 3, т.е. замены istore_l на istore_0, используем шестнадцатеричный редактор. Таким образом локальная переменная 0 (значение m) будет инициализирована дважды, а локальная переменная 1 (значение п) вообще не будет инициализирована. Для этого необходимо знать шестнадцатеричные значения для данных инструкций. Эти значения приводятся в книге The Java Virtual Machine Specification Тима Линдхольма (Tom Londholm) и Фрэнка Йеллина (Frank Yellin) [Addison-Wesley, 1999].
0 iconst_l 04
istore_0 3B
ioonst_2 05
istore_l 3C
iload_0 1A
iload_l IB
iadd 60
istore_2 3D
8 iload_2 1С
9 ireturn AC
Для редактирования такого значения можно использовать шестнадцатеричный редактор, например, Hex Workshop, который можно скопировать по адресу http: //vtww. bpsof t. com, или редактор emacs (в операционной системе Linux) в режиме hexl-mode. На рис. 9.2 изображен редактор Hex Workshop с загруженным файлом Ver i fierTest. class, в котором выделены байт-коды метода fun.
|HTleHrfyfk*hiiv[Vf-ni^
'plCini-vr^ ( r, z- t ~2) IM Г 2 HU130000 QQ 00String. :t 110<IF > 3 j 1 "21UHon t 0 353I . . .51 1 t 11 1 11 U r 119 f НП iBEЛЛ06 59 12.У. ,L 1b TOv 1 ,1- L0 5spВh Ch Life,U n l l с й x и0 I 1 mHUсо 00 00%11 ri' J "".1 jl01 7 MIl>" i 00 .on0100 ?1 00..-.1 ii О ю ю no cч ни GAlit.36051® illIK'H iH n nr 6d nn 04D(01 1IEOli 12 0000 03 00HSIflX. ... *hl ЭГГ/ТЗ-о()0 35 TO IV 1 Ji '2O0Пи -17 005I...7.I’OU'U HOC; ни ।CO?F F- MB i5012UI E7 00. + ...41,r -3 f L U CU, St. i Ot 11я;14 b6 00IpC'1 VI■ ■P1 L uO2r T и 0-.Гил0 c?02 00 00%IP U 1 i'll13 оз в 1 1": IГ11.Г JI !1A0u11 J 0 21. . . .1pl 1Г 'll0 1Г5 3ГТЕ7 Г0 0ah -v - -.^t n1 0 n 1 [Jill 1 IT_1Г1 i n 00I1joonatoCi i 00 J iri>1L 1 *.. . .•'plCini-vr^ ( r, z- t ~2) IM Г 2 HU130000 QQ 00String. :t 110<IF > 3 j 1 "21UHon t 0 353I . . .51 1 t 11 1 11 U r 119 f НП iBEЛЛ06 59 12.У. ,L 1b TOv 1 ,1- L0 5spВh Ch Life,U n l l с й x и0 I 1 mHUсо 00 00%11 ri' J "".1 jl01 7 MIl>" i 00 .on0100 ?1 00..-.1 ii О ю ю no cч ни GAlit.36051® illIK'H iH n nr 6d nn 04D(01 1IEOli 12 0000 03 00HSIflX. ... *hl ЭГГ/ТЗ-о()0 35 TO IV 1 Ji '2O0Пи -17 005I...7.I’OU'U HOC; ни ।CO?F F- MB i5012UI E7 00. + ...41,r -3 f L U CU, St. i Ot 11я;14 b6 00IpC'1 VI■ ■P1 L uO2r T и 0-.Гил0 c?02 00 00%IP U 1 i'll13 оз в 1 1": IГ11.Г JI !1A0u11 J 0 21. . . .1pl 1Г 'll0 1Г5 3ГТЕ7 Г0 0ah -v - -.^t n1 0 n 1 [Jill 1 IT_1Г1 i n 00I1joonatoCi i 00 J iri>1L 1 *.. . .•ЯеафЯеаф,,,,,,.. }Л ^S tMJ.I^ M .^л^т/йШШ}. -2L^hJ ipoJlHC^ - К~b01 00 OB 74 6F t.. .printin. . .to.. }Л ^S tMJ.I^ M .^л^т/йШШ}. -2L^hJ ipoJlHC^ - К~b01 00 OB 74 6F t.. .printin. . .toРис. 9.2. Модификация байт-кодов с помощью шестнадцатеричного редактораРис. 9.2. Модификация байт-кодов с помощью шестнадцатеричного редактора
'plCini-v
r^ ( r, z- t ~
2) IM Г 2 HU
13
00
00 QQ 00
String. :
t 110<
IF > 3 j 1 "
21
UH
on t 0 35
3I . . .5
1 1 t 11 1 11 U r 1
19 f НП i
BE
ЛЛ
06 59 12.
У. ,
L 1b TO
v 1 ,1- L
0 5
sp
Вh Ch Life,
U n l l с й x и
0 I 1 m
HU
со 00 00%
11 ri' J "".
1 jl01 7 MI
l>" i 00 .
on
01
00 ?1 00.
.
-.
1 ii О ю ю no c
ч ни GA
lit
.36
051® ill
IK'
H iH n nr 6d nn 04
D(
0
1 1
IE
Oli 12 00
00 03 00
HSIflX. ... *
hl ЭГГ/ТЗ
-о()0 35 TO I
V 1 Ji '2
O0
Пи -17 00
5I...7.
I’OU'U HOC
; ни ।CO
?F F- MB i
50
12
UI E7 00
. + ...41
,r -
3 f L U CU
, St. i Ot 11
я;
14 b6 00
IpC'1 VI
■ ■P1 L uO
2r T и 0-.Г
ил
0 c?
02 00 00
%
IP U 1 i'll
13 оз в 1 1": IГ
11.Г JI !
1A0u
11 J 0 21
. . . .1
pl 1Г 'll
0 1Г5 3
ГТ
Е7 Г0 0a
h -v - -.^t n
1 0 n 1 [Jill 1 IT
_
1Г1 i n 00
I1joonato
Ci i 00 J iri
>1L 1 *
.
. . .•
'plCini-v
r^ ( r, z- t ~
2) IM Г 2 HU
13
00
00 QQ 00
String. :
t 110<
IF > 3 j 1 "
21
UH
on t 0 35
3I . . .5
1 1 t 11 1 11 U r 1
19 f НП i
BE
ЛЛ
06 59 12.
У. ,
L 1b TO
v 1 ,1- L
0 5
sp
Вh Ch Life,
U n l l с й x и
0 I 1 m
HU
со 00 00%
11 ri' J "".
1 jl01 7 MI
l>" i 00 .
on
01
00 ?1 00.
.
-.
1 ii О ю ю no c
ч ни GA
lit
.36
051® ill
IK'
H iH n nr 6d nn 04
D(
0
1 1
IE
Oli 12 00
00 03 00
HSIflX. ... *
hl ЭГГ/ТЗ
-о()0 35 TO I
V 1 Ji '2
O0
Пи -17 00
5I...7.
I’OU'U HOC
; ни ।CO
?F F- MB i
50
12
UI E7 00
. + ...41
,r -
3 f L U CU
, St. i Ot 11
я;
14 b6 00
IpC'1 VI
■ ■P1 L uO
2r T и 0-.Г
ил
0 c?
02 00 00
%
IP U 1 i'll
13 оз в 1 1": IГ
11.Г JI !
1A0u
11 J 0 21
. . . .1
pl 1Г 'll
0 1Г5 3
ГТ
Е7 Г0 0a
h -v - -.^t n
1 0 n 1 [Jill 1 IT
_
1Г1 i n 00
I1joonato
Ci i 00 J iri
>1L 1 *
.
. . .•
Яеаф
Яеаф
,
,
,,
,,
.. }Л ^S tMJ.I^ M .^л^т/йШШ}. -2L^hJ ipoJlHC^ - К~b01 00 OB 74 6F t.. .printin. . .to
.. }Л ^S tMJ.I^ M .^л^т/йШШ}. -2L^hJ ipoJlHC^ - К~b01 00 OB 74 6F t.. .printin. . .to
Рис. 9.2. Модификация байт-кодов с помощью шестнадцатеричного редактора
Рис. 9.2. Модификация байт-кодов с помощью шестнадцатеричного редактора
Замените значение 3C значением 3B, сохраните файл, а затем попытайтесь запустить программу Verif ierTest. После выполнения этих действий будет получено следующее сообщение об ошибке:
Exception in thread "main"java.lang.VerifyError: (class: VerifierTest, method: fun signature: ()T) Accessing value from uninitialized register 1
Таким образом, виртуальная машина обнаружила незаконную модификацию.
Теперь запустим программу вместе с параметром -noverify (или -Xverify :none), т.е. без верификации.
java -noverify VerifierTest
Метод fun возвращает некую случайную величину, а точнее 2 плюс случайная величина, которая хранится в переменной п, которая, в свою очередь, никогда не была инициализирован a.
Итак, в результате такого сложения можно получить следующий результат.
1 + 2 = 15102330
Для демонстрации верификации в Web-броузере создадим эту программу также в виде аплета, кол которого приведен в листинге 9.3. Загрузим аплет в Web-броузер, используя следующий U RL-указатель.
:/CoreJavaBook/v2ch9/VerifierTest/VerifierTest.html
Как показано на рис. 9.3, результатом выполнения программы будет сообщение об ошибке, обнаруженной во время верификации.
В верификаторе интерпретатора Java есть курьезная ошибка, известная многим уже длительное время, но она сохраняется по причинам совместимости. Предположим, что у нас имеются два файла классов — A. j ava и B.java.
public class A { public int field; } public class В
{
public static void main(String[] args) {
j System.out.println(new A().field);
}
Конечно, эта программа компилируется и запускается. Отредактируем только файл A. java так, чтобы переменная field стала закрытой, и перекомпилируем только этот файл. Полученная программа не должна пройти верификацию, поскольку в классе в теперь предпринимается попытка доступа к закрытой области класса А. Однако, вплоть до версии SDK 1.4 программа запустится и получит доступ к закрытой области. И только в случае, если запустить интерпретатор Java с параметром -Xverify:all, будет выдано сообщение об ошибке.
Причина заключается в обратной совместимости с неопределенным и плохо продуманным механизмом оптимизации в JDK версии 1.0. Более подробно об этой ошибке можно прочесть по адресу http://developer.java.sun.com/developer/bugParade/ bugs/4030988 .html.Данную ошибку не следует считать опасной, поскольку не подвергаются верификации только классы, загружаемые из локальной файловой системы.
Листинг9.3. VarlflerTest.java
/** ©version 1.00 1997-09-10
©author Cay Horstmann
*/
import java.awt.*;
import java.applet.*;
/**
Пример демонстрации верификатора байт-кода виртуальной машины. При использовании шестнадцатеричного редактора для редактирования файла класса виртуальная машина способна обнаружить подлог
*/
public class VerifierTest extends Applet {
public static void main(String)] args) {
System.out.printin("1 + 2 == " + fun()); }
/**
Вычисление суммы 1+2
©return 3, if the code has not been corrupted
*/
public static int fun О
{
int m;
int n;
m = 1;
n = 2;
// Используйте шестнадцатеричный редактор для
// внесения исправления "m = 2" в файл класса int r=+ п;
return r;
p{ublic void paint(Graphics g)
g.drawstring("1+2== "+ fun(), 20, 20);
Рис. 9.З. Загрузка поврежденного файла класса вызывает ошибку верификации методаРис. 9.З. Загрузка поврежденного файла класса вызывает ошибку верификации метода/home/cay
Рис. 9.З. Загрузка поврежденного файла класса вызывает ошибку верификации метода
Рис. 9.З. Загрузка поврежденного файла класса вызывает ошибку верификации метода
window
4 ?( objects >n finalization queue
1ispIеacyollecthelpmessage
list
printcmonesmoloeryusage dump cosynssteomle properties
ang VerifyEri•or (class: Verifimethod:fun sMignahtudre).01)
atMethod)
§ ■ java.lang. Class, n •ance(Cla$s.j ava:237)
at•tPanel
atsun.plugirt.Appl«tViewer. createAo. pI et(App
atsun.applet.Apple!tPanel.runLoader(AppletPanel.jav
aM i 11wи * . . I
sun.plugin, avig.motif.MotifAppletViewer.maf_rut
at
atjava Ian g. Thread runfThread java:484)
Диспетчеры безопасности и полномочия
После загрузки класса в виртуальную машину загрузчиком классов или используемым по умолчанию механизмом загрузки классов и после его проверки верификатором в действие вступает третий механизм обеспечения безопасности платформы Java:
диспетчер безопасности (security manager). Диспетчер безопасности представляет собой класс, который контролирует дозволенность перечисленных ниже операций.
Создание текущим потоком нового загрузчика классов.
Создание текущим потоком подчиненного процесса.
Остановка работы виртуальной машины текущим потоком.
Загрузка текущим потоком динамически подключаемой библиотеки.
Получение классом доступа к члену другого класса.
Получение текущим потоком доступа к заданному пакету.
Определение текущим потоком классов в заданном пакете.
Получение текущим потоком доступа к системным свойствам или их модификация.
Считывание или запись заданных файлов текущим потоком.
Удаление заданного файла текущим потоком.
Получение текущим потоком подключения к сокету на основе указанного хоста или номера порта.
Открытие текущим потоком подключения к сокету на основе указанного хоста или номера порта.
Ожидание текущим потоком запроса на подключение по указанному номеру локального порта.
IP-широковещание со стороны текущего потока.
Вызов текущим потоком методов stop, suspend, resume, destroy, setPrior- ity/setMaxPriority, setName или метода setDaemon данного потока или группы потоков.
Установление текущим потоком сокета или обработчика потока.
Запуск классом задания принтера.
Получение классом доступа к системному буферу обмена.
Получение классом доступа к очереди событий AWT.
Разрешение текущему потоку на получение доступа к окну более высокого уровня.
По умолчанию при запуске приложений Java никакой диспетчер безопасности не задается, т.е. разрешены все операции. Но программа просмотра аплетов, с другой стороны, всегда задает диспетчер безопасности (AppletSecurity), который накладывает серьезные ограничения на допустимые действия.
Например, аплетам не позволяется покидать виртуальную машину. Если они попытаются вызвать метод exit, в таком случае генерируется исключительная ситуация системы обеспечения безопасности. Дело в том, что метод exit класса Runtime вызывает метод checkExitдиспетчера безопасности. Ниже представлен полный код метода exit.
public void exit(int status)
{
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkExit(status);
exitinternal(status);
}
Диспетчер безопасности проверяет, пришел ли запрос выхода от броузера или отдельного аплета. Если диспетчер безопасности одобряет такой запрос, то метод checkExit просто возвращается, и продолжается обычная обработка. Однако, если диспетчер безопасности не одобряет этот запрос, то метод checkExit генерирует исключительную ситуацию SecurityException.
Метод exit продолжает работать только в том случае, если не генерируется никакая исключительная ситуация. Затем он обращается к закрытому платформозависимому методу exitlnternal, который завершает работу виртуальной машины. Другого способа отключения виртуальной машины не существует, потому что метод exitlnternal является закрытым и не может быть вызван из другого класса. Таким образом, любой код, который пытается покинуть виртуальную машину, должен вызвать метод exit и, соответственно, пройти проверку с помощью метода checkExit без генерации исключительной ситуации обеспечения безопасности.
Понятно, что целостность политики обеспечения безопасности зависит от тщательности и аккуратности программирования. Провайдеры системных служб в стандартной библиотеке перед выполнением любых деликатных операций всегда должны консультироваться по поводу их допустимости с диспетчером безопасности.
Как уже упоминалось выше, по умолчанию при запуске приложений Java никакой диспетчер безопасности не используется. Но его можно установить в программе с помощью статического метода setSecurityManager класса System. Если в программе уже задан какой-то диспетчер безопасности, то попытка установить второй диспетчер безопасности будет успешной в случае, если первый диспетчер одобрит такую замену. Это очень важно, потому что в противном случае "плохой" аплет мог бы установить свой собственный диспетчер безопасности. Таким образом, несмотря на возможность использования нескольких загрузчиков классов, выполнение программы на языке программирования Java может контролироваться только одним диспетчером безопасности. Только создатель диспетчера безопасности может решить, следует ли делегировать всем классам одинаковые права доступа или это нужно делать с учетом их происхождения.
Используемый по умолчанию диспетчер безопасности на платформе Java 2 позволяет программистам и системным администраторам организовать очень детальное и структурированное управление отдельными полномочиями. Более подробно они рассматриваются в следующем разделе. Сначала в нем описывается общая структура модели обеспечения безопасности платформы Java 2. Затем представлены способы управления правами доступа с помощью файлов политики (policy files). И, наконец, рассматриваются некоторые способы определения собственных типов прав доступа и способы расширения предлагаемого по умолчанию класса диспетчера безопасности.
Модель обеспечения безопасности платформы Java 2
Модель обеспечения безопасности в JDK версии 1.0 имела очень простую структуру: локальные классы имели все полномочия, а полномочия удаленных классов были ограничены песочницей (sandbox), т.е. диспетчер безопасности аплета отказывал в лю-
доступе к локальным ресурсам. В JDK версии 1.1 эта модель была слегка модифицирована, т.е. заслуживающие доверия (или доверенные) удаленные объекты имели те же права доступа, что и локальные классы. Тем не менее, обе версии модели обеспечения безопасности были организованы по принципу "все или ничего", т.е. программы либо имели полный доступ к ресурсам, либо ограничивались "песочницей".
Платформа 2 имеет более гибкий механизм обеспечения безопасности на основе политики безопасности согласно которой наборы полномочий sets) образуются наоснове источников исходного кода какпоказано рис. 9.4.
Источник кода 1Расположение кодаСертификатыИсточник кода 1Расположение кодаСертификатыНаборполномочий1Наборполномочий1Полномочие#1аПолномочие#1аПолитика безопасностиПолитика безопасности
Источник кода 1Расположение кодаСертификаты
Источник кода 1Расположение кодаСертификаты
Наборполномочий1
Наборполномочий1
Полномочие
#1а
Полномочие
#1а
Политика безопасности
Политика безопасности
Полномочие
#1b
Источник кода 2Расположение кодаСертификатыИсточник кода 2Расположение кодаСертификатыНаборполномочий2Наборполномочий2Полномочие#2аПолномочие#2а
Источник кода 2Расположение кодаСертификаты
Источник кода 2Расположение кодаСертификаты
Наборполномочий2
Наборполномочий2
Полномочие
#2а
Полномочие
#2а
Полномочие
#2Ь
Политика безопасности
Источник кода характеризуется расположением кода (например, URL-указатель или JAR-файл) и сертификатами. Далее в этой главе описываются способы сертификации кода доверяемой стороной.
Правом доступа является любое свойство, которое проверяется диспетчером безопасности. В SDK 1.2 поддерживается несколько классов прав доступа, каждый из которых инкапсулирует элементы отдельного права доступа. Например, приведенный ниже экземпляр классаразрешает чтение и запись любого файла из каталога
p = new
Однако, гораздо важнее то, что в SDK 1.2 класс Policy по умолчанию считывает права доступа из файла прав доступа {permissions file). Внутри файла прав доступа упомянутые выше права доступа выражаются следующим образом.
"/tmp*",
Более подробно файлы прав доступа рассматриваются в следующем разделе.
На рис. 9.5 схематически изображена иерархия классов прав доступа SDK 1.2.
Permission
I
В предыдущем разделе уже говорилось о том, что диспетчер безопасности может иметь такие методы проверки обеспечения безопасности, как checkExit класса SecurityManager. Эти методы предусмотрены только для обеспечения обратной совместимости и используются программистом в качестве вспомогательного средства. Все они обращаются к одному из двух методов.
void checkPemission (Permission p)
void checkpermission(Permission p, Object context)
Второй метод используется в том случае, если один поток проверяет безопасность другого потока, а объек'рконтекст context инкапсулирует стек вызовов во время такой проверки. Подробное описание способов создания и использования этих контекстных объектов можно найти в книге Inside Java 2 Platform Security Ли Гонга (Li Gong) [Addison-Wesley 1999].
Ниже представлен исходный код метода checkExit.
public void checkExit() {
checkpermission(new RuntimePermission("exitVM"));
)
Каждый диспетчер безопасности может иметь собственную версию метода checkPermission. Однако, в JDK предусмотрена "стандартная модель" проверки прав доступа, которая описывается в остальной части настоящего раздела. Она основана на суперклассе загрузчика классов java. security. SecurityClassboader и суперклассе
диспетчера безопасности java.lang.securityManager, которые используются, практически, во всех случаях, например, для аплетов и удаленных вызовов методов. В принципе, можно создать и установить собственный загрузчик классов и диспетчер безопасности. Однако, эта очень сложная процедура редко используется программистами, которые предпочитают применять расширения стандартных классов.
Для преобразования источников кодов в наборы права доступа в стандартной модели используется объект класса Policy. Одновременно допускается использование только одного объекта класса Policy. Для получения текущей политики используется статический метод getPolicyкласса Policy.
Policy currentpolicy =
Основным методом класса Policy является метод getPermissions, который возвращает набор прав доступа для отдельного источника кода.
Permissioncollection permissions =
Каждый класс имеет домен обеспечения безопасности или домен защиты (protection domain), т.е. объект, который инкапсулирует источник кода и набор прав доступа этого класса. Для получения этого домена используется метод getProtectionDomainкласса Class.
ProtectionDomain domain =
Методы getCodeSource и getPermissions объекта ProtectionDomain возвращают источник кода и набор прав доступа.
В стандартной модели набор прав доступа полностью зависит от источника кода. Домен обеспечения безопасности задается, когда загрузчик классов SecureClassLoader загружает класс. При этом он запрашивает текущую политику для определения прав доступа для данного источника кода, а затем создается объект домена обеспечения безопасности ProtectionDomain с заданным источником кода и правами доступа. Наконец, этот объект передается методу defineClass. На рис. 9.6 схематически показана взаимосвязь между данными классами обеспечения безопасности.
Когда диспетчер безопасности SecurityManager проверяет права доступа, он просматривает классы всех методов в стеке вызовов. Затем он переходит к доменам обеспечения безопасности всех классов и запрашивает каждый домен обеспечения безопасности: позволяет ли его набор прав доступа выполнять проверяемую операцию. Если все домены одобряют выполнение проверяемой операции, то тестирование продолжается. В противном случае генерируется исключительная ситуация SecurityException.
Почему все методы в стеке вызовов должны позволять выполнение отдельной операции? Предположим, что метод init аплета собирается открыть файл с помощью следующего метода.
Reader in = new FileReader(name) ;
Конструктор FileReader вызывает конструктор FileinputStream, который обращается к методу checkRead диспетчера безопасности, который, в конце концов, вызывает метод checkPermission вместе с объектом FilePermission(name, " read"). В табл. 9.1 приведен стек этих вызовов.
Рис. 9.6. Связи между классами обеспечения безопасности
Таблица 9.1.Стек вызовов во время проверки прав доступа
Класс
Метод
Источник кода
Права доступа
SecurityManager
SecurityManager
null
AllPermission
SecurityManager
checkRead
null
AllPermission
FilelnputStream
конструктор
null
AllPermission
FileReader
конструктор
null
AllPermission
аплет
init
источник кода an- лета
права доступа аплета
Классы Fileinputstreamи Security-Managerявляются системными классами, в качестве источника кода которых будет использоваться значение null, а в качестве прав доступа— экземпляр класса AllPermission, который позволяет выполнять все операции. Понятно, что их права доступа не позволяют полностью определить результат проверки. С этой целью методу checkPermis s i on необходимо учесть ограничения прав доступа для класса данного аплета. Проверяя весь стек вызовов, механизм обеспечения безопасности гарантирует, что один класс никогда не вынудит другой класс выполнить какие-то важные операции от его имени.
Это краткое описание дает представление лишь о концепциях механизма проверки прав доступа. Однако, существует множество достаточно сложных технических деталей, которые здесь опущены. Поэтому, для получения дополнительной информации по данной теме рекомендуется прочитать книгу Inside Java 2 Platform Security Ли Гонга (Li Gong) [Addison-Wesley 1999]. Кроме того, для более глубокого изучения модели обеспечения безопасности в платформе Java рекомендуется прочитать книгу Securing Java Гэри Мак-Г poy(Gary McGraw) и Эда Фельтена (Ed Felten)[John Wiley & Sons, 1999]. Электронную версию этой книги можно найти по адресуhttp://www. securing java. com.
java.lang.SecurityManager.
Метод void checkPermission (Permission p) проверяет, предусмотрено ли указанное право доступа р в текущей политике безопасности.
Метод void checkpermission(Permission p, Object context) проверяет, предусмотрено ли указанное право доступа р в текущей политике безопасности. Второй параметр содержит объект-контекст context, который инкапсулирует стек вызовов. Этот метод используется, если один поток вынуждает другой поток проверить права доступа от своего имени.
j ava■security.Policy
Метод static Policy getPolicy() возвращает объект текущей политики или значениеnull, если политика обеспечения безопасности не задана.
Метод Permissioncollection getPermissions(CodeSource source)
возвращает права доступа, связанные с данным источником кода.
java.lang.Class
Метод ProtectionDomain getProtectionDomain() возвращает домен обеспечения безопасности для данного класса или значение null, если этот класс был загружен без домена обеспечения безопасности.
j ava.lang.ClassLoader
Метод Class defineClass(String name, byte[] data, int offset, int length, ProtectionDomain domain) добавляет новый класс в вирту-
альнуюмашину.
Параметры:
name
имя класса; в качестве разделителя в нем следует использовать символ-точку " ." (как при указании имени пакета), а не суффикс .class
data offset length domain
массив, в котором хранятся байт-коды класса
начало байт-кодов в массиве
длина байт-кодов в массиве
домен обеспечения безопасности этого класса
java.security.ProtectionDomain
Конструктор ProtectionDomain(CodeSource source, PermissionCollection collections) создает домен обеспечения безопасности на основе указанного источника кода и прав доступа.
Метод CodeSource getCodeSource () возвращает источник кода для данного домена обеспечения безопасности.
Метод PermissionCollection getPermissions О возвращает права доступа данного домена обеспечения безопасности.
java.security.PermissionCollection
Метод void add (Permission p) включает указанное право доступа р в данный набор прав доступа.
Метод Enumeration elements О возвращает объектперечисление всех прав доступа для данного набора прав доступа.
java.security.CodeSource
Метод Certificate[] getCertif icates () возвращает сертификаты для сигнатуры файла класса, связанного с данным источником кодов.
Метод URL getLocation() возвращает местоположение файлов классов, связанных с данным источником кодов.
Файлы политики обеспечения безопасности
В предыдущем разделе показано, что загрузчик классов SecureClassLoader определяет права доступа во время загрузки классов, запрашивая объект Policy для подстановки прав доступа для источника кода каждого загружаемого класса. В принципе, для сопоставления источников кода и прав доступа можно создать свой собственный класс Policy. Однако, в этом разделе мы ограничимся рассмотрением стандартного класса политики, который используется интерпретатором BJDK 1.2.
DDКласс политики задается в файле java.security в подкаталоге jre/libоснов- ного каталога JDK. По умолчанию этот файл содержит следующую строку.
D
D
policy.provider=sun.security.provider.PolicyFile
Для использования собственного класса политики нужно создать его и соответствующим образом отредактировать эту строку.
При использовании стандартного класса политики считываются файлы политики, которые содержат инструкции для установления соответствия между источниками кода и правами доступа. Такие файлы политики уже рассматривались в главе 5, "Работа с удаленными объектами"; они требовались для предоставления сетевого доступа программам, которые взаимодействовали с RMI-диспетчером безопасности RMISecurityManager. Вот как выглядит типичный файл политики.
grant CodeBase "www.horstmann.com/classes" {
permission java.io.FilePermission "/tmp/*", "read, write";
Этот файл предоставляет право на чтение и запись файлов в каталоге /tmp всему коду, который был загружен по URL-указателю www. horstmann. com/classes.
Файлы политики обычно располагаются в двух местах.
Файл java. policy располагается в основном каталогеплатформы Java.
Файл .java.policy (обратите внимание на точку в начале имени файла) располагается в основном каталоге пользователя.
Расположение файлов можно изменить в конфигурационном файле java, security, редактируя приведенные ниже строки.
policy,url.l=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy
Системный администратор может модифицировать файл java.securityи установить иные URL-указатели для файлов политики, которые будут храниться на другом сервере и не будут редактироваться пользователями. Таким образом, в данном файле может быть задано случайное количество URL-указателей для файлов политики с разными последовательными номерами. При этом все заданные права доступа для всех файлов комбинируются.
Во время тестирования не стоит постоянно модифицировать стандартные файлы. Поэтому в следующих примерах явно задаются файлы политики, которые необходимы для работы каждого приложения. Для этого необходимо разместить права доступа в отдельном файле, например, MyApp. policy, и запускать интерпретатор с помощью следующей команды.
java —Djava.security.policy=MyApp.policy MyApp
Для аплетов следует применять такую команду.
appletviewer —J-Djava.security.policy=MyApplet.policy MyApplet.html
(Здесь параметр - J используется для передачи интерпретатору всех аргументов командной строки.)
На самом деле в приведенных примерах файл MyApp.policyиспользуется вместе с другими заданными политиками. Но если добавить второй знак равенства показанным ниже образом, то в данном приложении будет использоваться только этот файл политики, а все стандартные файлы политики будут игнорироваться.
java —Djava.security.policy==MyApp.policy MyApp
Во время тестирования можно легко допустить ошибку, случайно оставив в текущем каталоге файл . java.policy, который может содержать огромное количество прав доступа, если не все (AllPermission). Если приложение игнорирует ограничения в файле политики, то нужно проверить, не остался ли какой-то забытый файл . java.policy в текущем каталоге. Данная проблема особенно актуальна для операционной системы UNIX, где эту ошибку можно допустить еще легче, поскольку файлы, имена которых начинаются с точки, по умолчанию скрыты.
Как уже говорилось ранее, по умолчанию приложения Java не устанавливают диспетчер безопасности. Поэтому указанные файлы политики не будут применяться до тех пор, пока не будет установлен диспетчер безопасности. Для этого можно, конечно, добавить следующую строку в метод main.
System.setSecurityManager(new SecurityManager());
Иначе можно добавить параметр -Dj ava.security.manager в командной строке во время запуска интерпретатора.
java —Djava.security.manager
—D java.security.policy=MyApp.policy MyApp
В остальной части этого раздела рассматриваются способы описания прав доступа в файле политики, за исключением способов указания сертификатов кода, которые рассматриваются далее в этой главе.
Файл политики безопасности содержит последовательность записей прав доступа с ключевым словом grant. Каждая запись прав доступа имеет следующую форму.
grant источник_кода
{
право_доступа_1;
право_доступа_2;
}
Источник кода содержит базу кодов (которая может быть опущена, если запись права доступа применяется к коду из всех источников) и имена доверенных лиц (которые могут быть опущены, если для этой записи прав доступа подписи не требуются).
База кода указывается следующим образом.
CodeBase "ийЬ_указатель"
Если URL-указатель завершается знаком "/", то это ссылка на каталог, а в противном случае — это ИМЯJAR-файла.
grant CodeBase "www.horstmann.com/classes/"{...}
grant CodeBase "www.horstmann.com/classes/MyApp.jar"{...}
URL-указатель базы кода всегда должен содержать левые косые черты в качестве разделителей даже для файловых URL-указателей для операционной системы Windows.
grant CodeBase “ file: С: /myapps/classes/ 11
Как известно, URL-указатель типа http выглядит так http://. Но с URL- указателями типа fileчасто возникает путаница по поводу их синтаксиса, потому программа чтения файлов политики разрешает использование двух синтаксисов, а именно, file://локальный_файл и file:локальный_файл. Кроме того, последняя косая черта перед именем диска в Windows не является обязательной. Таким образом, приемлемыми являются все следующие ниже виды записи.
file:C:/dir/filename.ext
file:/С:/dir/filename.ext
file://C:/dir/filename.ext■
file:///C:/dir/filename.ext
Более того, запись file: ////С:/dir/filename.ext no непонятной причине тоже является приемлемой. В операционных системах UNIX/Linux необходимо использовать только следующий синтаксис.
file:/dir/filename.ext
Описание прав доступа имеет следующую структуру.
permission имя_класса имя_объекта_доступа, список_действий;
Параметр имя__класса — это полностью квалифицированное имя класса прав доступа (например, java. io. FilePermission). Параметр имя_объекта_доступа— это характерное имя цели доступа, например, файла или каталога для прав доступа к файлам либо хоста и порта для прав доступа к сетевому подключению. Параметр спи- сок_действий это также характерный перечень действий, например, read или connect, разделенных запятыми. Для некоторых классов прав доступа не требуется указывать имена объектов доступа и списки действий. В табл. 9.2 перечислены стандартные права доступа и их действия.
Как показано в табл. 9.2, большинство прав доступа просто разрешает выполнение какой-то операции. Операцию можно представить как некую сущность с предполагаемым действием "permit" (разрешить). Эти классы прав доступа расширяют класс BasicPer- mission, как показано на рис. 9.5. Однако, объекты доступа в виде файлов, сокетов, свойств имеют более сложную структуру и их требуется рассмотреть более детально.
Таблица 9.2. Права доступа, а также связанные с ними объекты и действия
Право доступа
Объект доступаДействие
3ava.10.
FilePennission
файлread,write,
execute,delete
java.net.
SocketPermission
сетевое подключение через сокетaccept,connect,
listen,resolve
java.util.
Propertypermission
СВОЙСТВОread,write
java.lang.
RuntimePermis s i on
createClassLoader
getClassLoader
setContextClassLoader
createSecurityManager
exitVM
setFactory
setlO
modifyThread
modifyThreadGroup getProtectionDomain readFileDescriptor
writeFileDescriptor loadLibrary.имя
accessClassInPackage.имя
defineClasslnPackage.имя accessDeclaredMembers.имя queuePrintJob stopThread
j
AWTPermission
showWindowWithoutWarningBanner accessClipBoard accessEventQueue 1ist enToAl1ANTEvent s readDisplayPixels
java.net.
NetPermission
setDefaultAuthenticator specifyStreamHandler requestPasswordAuthentication
jaba.lang.reflect. ReflectPermission
suppressAccessChecks
Окончание табл. 9.2
Право доступа
Объект доступаДействие
java.i о.Seriali zabl ePermission
enableSubclassImplementation enableSubst i tut i on
java.security.Secur ityPermission
getPolicy setPolicy getProperty. key setproperty, key insertprovider.имя removeProvider.имя set SystemScope se11denti tуPubli cKey setldentitylnfо setidentitycertificate removeidentitycertificate printidentity clearProviderProperties.имя putProviderProperty.имя removeProviderProperty.имя getSignerPrivateKey setSignerKeyPair
java.security.AllPe rmission
Имена файловых объектов прав доступа указываются в соответствии со следующими соглашениями.
файл каталог/ каталог/*
*
файл
каталог
все файлы данного каталога
все файлы текущего каталога
каталог/-
все файлы данного каталога и всех его подкаталогов
-
все файлы текущего каталога и всех его подкаталогов
«ALL FILES»
все файлы файловой системы
Например, следующее описание прав доступа означает доступ ко всем файлам каталога /myapp и всех его подкаталогов.
permission java. io.FilePermission "/myapp/-",
"read,write,delete";
Для обозначения обратной косой черты в именах пути в операционной системе Windows следует использовать две косых черты.
permission java.io.FilePermission "с: \\myapp\\-",
"read,write,delete”;
Имена сетевых объектов прав доступа состоят из имени хоста и номера порта. Имя хоста указывается в соответствии со следующими соглашениями.
имя_хоста или IP_adpeclocalhost или пустая строка* с^ффикс_домена*имя_хоста или IP_adpeclocalhost или пустая строка* с^ффикс_домена*один хост
имя_хоста или IP_adpec
localhost или пустая строка
* с^ффикс_домена
*
имя_хоста или IP_adpec
localhost или пустая строка
* с^ффикс_домена
*
локальный хост
любой хост, домен которого заканчивается таким суффиксом
все хосты
Номера портов указываются в соответствии со следующими соглашениями.
:п:п-:п:п-один порт
:п
:п-
:п
:п-
все порты с номерами и и выше
:-пвсе порты с номерами п и ниже
:п1-п2все порты в диапазоне между Til и п2
Вот как выглядит типичное описание прав доступа к сетевому подключению.
permission java.net.Socketpermission
"*.horstmann.com:8000-8999”, "connect";
Наконец, имена объектов-свойств прав доступа могут иметь одну из двух следующих форм.
свойствоотдельное свойство
префикс_свойства. *все свойства с указанным суффиксом
Вот как выглядит типичное описание прав доступа ко всем свойствам, имена которых начинаются с j ava.vm.
permission java.util.Propertypermission "java.vm.*", "read";
Системные свойства можно использовать в файлах политики. При этом метка $ {свойство}заменяется значением свойства. Например, метка ${user.home} заменяется основным каталогом пользователя. Типичное использование этого системного свойства в описании прав доступа имеет следующий вид.
permission java.io.FilePermission "$(user.home}", "read,write";
Для создания платформонезависимых файлов политики следует использовать свойство file.separator вместо явного указания разделителей / или \\. Для упрощения можно также использовать специальное обозначение $ {/} для метки $ {file.separator}.
Например, краткое описание прав чтения и записи в основном пользовательском каталоге и всех его подкаталогах выглядит следующим образом.
permission java.io.FilePermission "$(user.home}", "read,write";
В SDK имеется устаревший инструмент policytool, который предназначен для редактирования файлов политики. После запуска инструмента считывается указанный файл политики, а затем отображаются все источник кода, для которых заданы права доступа, как показано на рис. 9.7. После щелчка на кнопке Edit Policy Entry (Редактирование записи политики) будут отображены все права доступа для данного источника кода, как показано на рис.9.8.ПослевыбораправадоступаищелчканакнопкеEditPermission
(Редактирование права доступа) будет отображено диалоговое окно, которое позволяет редактировать свойства для данной записи прав доступа, как показано на рис. 9.9. Как видите, диалоговое окно отображает параметры объекта прав доступа.
Конечно, этот инструмент не подходит для конечных пользователей, которые могут запутаться в этих многочисленных параметрах. Данный пример подтверждает необходимость создания административного инструмента, который могли бы использовать в своей работе системные администраторы. К сожалению, в настоящее время отсутствует более понятный набор категорий (например, низкая, средняя и высокая степень обеспечения безопасности), которые были бы понятны неспециалистам. По мнению авторов, платформа Java обладает всеми компонентами для создания детальной модели обеспечения безопасности, но разработчикам следовало бы подумать о более доступных способах использования ее конечными пользователями и системными администраторами.
F<le >£di|F<le >£di|Poh су| /usr/l«al/J2i dkt ч 0/ПЛ1ЬЛвсигЦу/|вуа.ро1му
F<le >£di|
F<le >£di|
Add Policy EntryEditPolityEntry{ . Remove Policy Entry
Рис. 9.7. Инструмент редактирования файлов политики policytool
de=ase |I nedBy 11
Adc Principal . ' . E-dl* Principal .Remove Principal
AddPermissionEditPermissionJRemovePermissionAddPermissionEditPermissionJRemovePermissionим|й|М|1111ИМ|Им1|1^^
AddPermissionEditPermissionJRemovePermission
AddPermissionEditPermissionJRemovePermission
•missiori java lang RunVmeParmisslon^topThreadV ~ mission ava.net5ocketPet mission ocalhost1024-‘. 'listen';
r mission Java
mission SvSutllPropBrtvPermission mvEvenS.
mission" version', read1^
mission ava utltPrapertvPermlsslon ""os ame", *ro>d*:i
mission ava. util PropetyPermission ""os ■■ "read";
mission ava.util Propp-tyPermlsslon "osjarch" "read" mission ava. util Property er miss Ion ■fileseparator'/raed'j
DoneICantali
Рис. 9.8. Отображение прав доступа для источника кода
Рис. 9.9. Редактирование прав доступа
Пользовательские права доступа
В этом разделе описывается способ создания собственных классов прав доступа, которые пользователи могут применять в файлах политики.
Для реализации классов прав доступа следует расширить класси пре
доставить следующие методы.
Конструктор с двумя строковыми параметрами для указания объекта и списка действий.
Метод String getActions ().
Метод boolean equals ().
Метод int hashCode ().
Метод booleanother).
Последний метод имеет самое большое значение. Для прав доступа используется определенный порядок (ordering), в соответствии с которым наиболее общие права доступа предполагают (imply) использование специальных прав доступа. Например, показанное ниже право доступа к файлу позволяет чтение и запись любого файла в каталоге / и любых его подкаталогах.
Р1 = new"read, write");
Это общее право доступа предполагает следующие специальные права доступа.
р2 = new FilePermission("/tmp/-", "read");
рЗ = new"read, write");
p4 = new" write");
Иначе говоря, право доступа к файлупредполагает наличие другого права дос
тупа р2 при выполнении следующих условий.
Набор файлов-объектов доступа в правах доступасодержит набор файлов-
объектов доступа в правахр2.
Набор действий в правах доступа содержит набор действий в правах доступа
Вот как выглядит пример использования метода implies. Если конструктор открывает файл для чтения, то он проверяет наличие прав для та-
кого доступа. Для выполнения этой проверки специальный файл-объект прав доступа передается методу checkPermission.
checkPermission(new FilePermission(fileName, "read"));
Менеджер обеспечения безопасности после этого запрашивает все имеющиеся права доступа о том, имеется ли такое специальное право доступа.
В частности, право доступа Al 1 Permi s s i on предполагает все права доступа.
При определении собственных классов прав доступа необходимо определить предполагаемые права доступа. Например, для управления доступа пользователя Томми к телевизору под управлением Java в определенном промежутке времени можно определить следующий класс доступа TVPermission.
new TVPermission("Tommy:2-12:1900-2200", "watch, record")
Этот класс позволяет Томми смотреть и записывать телевизионные передачи на каналах 2-12 в промежутке времени между 19:00 и 22:00. Для применения следующего специального права доступа потребуется реализовать метод implies.
new TVPermission( "Tommy:4 .-2000-2100", "watch")
Реализация класса прав доступа
В следующем примере программы демонстрируется создание права доступа, которое регламентирует вставку текста в текстовую область. Эта программа предотвращает вставку в текстовую область "недопустимых слов", например sex (секс), drugs (наркотики) и C++. Для перечисления таких слов и передачи их файлу политики применяется пользовательский класс прав доступа.
Ниже приводится код класса, который расширяет класс JTextArea и запрашивает диспетчер обеспечения безопасности о допустимости вставки нового текста.
class WordCheckTextArea extends JTextArea {
public void append(String text)
{
WordCheckPermission p
= new WordCheckPermission(text, "insert");
SecurityManager manager = System.getSecurityManager() ;
if (manager != null) manager.checkPermission(p);
super.append(text);
} }
Новый текст будет добавлен, если диспетчер обеспечения безопасности делегирует право WordCheckPermission. В противном случае метод checkPermission генерирует исключительную ситуацию.
Права доступа предполагают выполнение следующих действий: insert (право доступа для вставки некоторого текста) и avoid (право доступа для вставки любого текста без недопустимых слов). Этот пример программы следует использовать вместе со следующим файлом политики.
grant
{
permission"avoid";
};
Данный файл политики делегирует права доступа для вставки любого текста, который не содержит недопустимые слова sex, drugs и C++.
При создании класса WordCheckPermission нужно уделить особое внимание методу implies, чтобы учесть перечисленные ниже правила, в соответствии с которыми права доступа подразумевают права доступа
Если права доступаопределяют действие avoid, а права доступа р2 — дейст
вие insert, то объект прав доступа р2 не должен иметь слов, упомянутых в правахНапример, следующие права
WordCheckPermission"avoid"
предполагают такие права доступа р2
WordCheckPermission "Mary had a little lamb", "insert"
Если права доступаи р2 определяют действие avoid, то набор слов объекта
прав доступа р2 должен содержать все слова из набора прав доступаНапри
мер, следующие права доступа
WordCheckPermission"avoid"
предполагают такие права доступа р2
WordCheckPermission"avoid"
Если права доступаи р2 определяют действие insert, то текстовая строка
объекта прав доступадолжна содержать текстовую строку прав доступа р2.
Например следующие права
WordCheckPermission "Mary had a little"insert"
предполагают такие права доступа
WcrdCheckPermission "a little lamb", "insert"
Код реализации этого класса приводится в листинге 9.5.
Обратите внимание, что объект прав доступа извлекается с помощью довольно странно названного методакласса Permission.
Права доступа описываются с помощью пары строк в файле политики, поэтому классы прав доступа должны уметь анализировать эти строки. В частности, для преобразования списка разделенных запятыми недопустимых слов в правах доступа в типа Set в данном примере используется следующий метод.
public Set
{
StringTokenizer tokenizer
= new" , ") ;
Set set = new while
set.add(tokenizer.nextToken() ) ;
return set;
}
Этот код позволяет использовать методы equals и containsAll для сравнения множеств. Как показано в главе 2, "Коллекции", метод equals класса-множества считает два множества равными, если они содержат одинаковые элементы в случайном порядке. Например, множества слов, которые получаются после преобразования строк "sex, drugs, C++" и "C++, drugs, sex" считаются равными.
Убедитесь в том, что класс полномочий является открытым классом. Загрузчик файла политики не может загрузить классы с областью видимости пакета, которая выходит за пределы пути к классам загрузки, и потому безмолвно игнорирует все те классы, которые не сможет найти.
Пример программы, код которой приводится в листинге 9.5, демонстрирует принцип работы классаВведите любой текст в текстовую
область и щелкните на кнопке insert (Вставить). При успешном прохождении проверки текст будет вставлен в текстовую область, а в противном случае появится сообщение об ошибке, которое показано на рис. 9.10.
Для запуска этой программы следует указать соответствующий файл политики, ибо в противном случае все попытки вставить какой-либо текст проваляться.
java —Djava.security.policy=PermissionTest.policy
PermissionTest
1. ,,1. ,,Java AppletJava AppletProgrammingProgrammingheJavaLanguageheJavaLanguageИИ
1
. ,,
1
. ,,
Java Applet
Java Applet
Programming
Programming
heJavaLanguage
heJavaLanguage
И
И
Java Applet
am sorry,cannot do
ок
Рис. 9.10. ПрограммаPermissionTest
При внимательном изучении рис. 9.10 можно заметить, что окно фрейма имеет границу со странным заголовком Java Applet Заголовок определяется методом класса
Для изменения этого заголовка можно соответствующим образом отредактиро- ватьфайлполитики.
Листинг
1.01 2001-08-26
Cay
import java.awt.*;
importj ava.awt.event.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import javax.swing.*;
/**
Пример использования пользовательского класса
прав доступа WordCheckPermission
*/
public class PermissionTest {
public static void main(String[] args)
{
System.setSecurityManager(new SecurityManager());
JFrame frame = new PermissionTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
}
/**
Фрейм с текстовым полем для вставки слов в
текстовую область, которая защищена от "недопустимых слов" */ class PermissionTestFrame extends JFrame {
public PermissionTestFrame()
{
setTitle("PermissionTest");
setsize(WIDTH, HEIGHT);
textField = new JTextField(20);
JPanel panel = new JPanel();
panel.add(textField) ;
JButton openButton = new JButton("Insert");
panel.add(openButton);
openButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
insertwords(textField.getText()) ;
) }) ;
Container contentPane = getContentPane();
contentpane.add(panel, BorderLayout.NORTH);
textArea = new WordCheckTextArea();
contentpane.add(new JScrollPane(textArea), BorderLayout.CENTER);
}
/“
Попытка вставить слова в текстовую область.
Отображение диалогового окна с сообщением об ошибке,
если эта попытка провалилась Sparam words the words to insert
*/
public void insertwords(String words) {
try {
textArea.append(words + "\n");
}
catch (SecurityException e) {
JOptionPane.showMessageDialog(this, "I am sorry, but I cannot do that.") }
private JTextField textField;
private WordCheckTextArea textArea;
private static final int WIDTH = 400;
private static final int HEIGHT = 300; }
/**
Текстовая область, в которой метод append отвергает попытки вставить "недопустимые слова"
*/
class WordCheckTextArea extends JTextArea {
public void append(String text) {
WordCheckPermission p
new WordCheckPermission(text, "insert");
SecurityManager manager = System.getSecurityManager(); if (manager != null) manager.checkpermission(p);
super.append(text);
}
Листинг 9.5. WordCheckPermission.java
/**
©version 1.00 1999-10-23
©author Cay Horstmann
*/
import j ava.security. * ;
import java.util.*;
/**
Право на использование определенных слов
*/
public class WordCheckPermission extends Permission
{
/**
Создание прав доступа с проверкой слов.
@param target a comma separated word list
©param anAction "insert" or "avoid"
*/
public WordCheckPermission(String target, String anAction) {
super(target);
action = anAction; }
public String{ return action; }
public booleanother)
{
if (other == null) return false;
ifreturn false;
WordCheckPermission b = ifreturn false;
if return else if return));
else return }
public int {
return+
}
public booleanother)
{
if (!(other instanceof WordCheckPermission)) return false; WordCheckPermission b = if
{ return
>= }
else if))
{ if))
return else if {
Iterator iter = while {
String badWord =
if (b.getMame().indexOf(badWord) >= 0) return false;
} return true;
} elsefalse;
} else return false;
/**
набор недопустимых слов, которые описаны в правиле прав доступа
a set of the bad words
*/
public Set
{
StringTokenizer tokenizer
=new",") ;
Set set = new HashSet();
while (tokenizer.hasMoreTokens()) set.add(tokenizer.nextToken()) ;
return set;
}
private String action;
java.security.Permission
КонструкторPermission (String name) создает объект прав доступа к объекту с указанным именем.
Метод String getName () возвращает имя объекта с данными правами доступа.
• Метод boolean implies (Permission other) проверяет, предполагаются ли указанные права доступа other данными правами доступа. Такая ситуация возникает, если права доступа other являются специальными и подмножеством данных прав доступа.
Пользовательский диспетчер безопасности
В этом разделе приводится способ создания простого, но достаточно полного пользовательского диспетчера безопасности, который называется WordCheckSecurityManager. Он следит за доступом к файлам и гарантирует, что файл не будет открыт, если содержит недопустимые слова: sex, drugs и C++.
Слежение за попытками доступа к файлам организовано на основе переопределенного метода checkPermission стандартного класса диспетчера безопасности. Если используется какое-то другое право доступа, отличное от прав на чтение файла, то вызывается метод super. checkPermission. Для проверки допустимости чтения файла он открывается и сканируется. Затем право чтения файла делегируется только в том случае, если он не содержит недопустимых слов. (В данном примере отслеживаются лишь файлы с расширением . txt, чтобы не препятствовать чтению системных файлов или файлов свойств.)
public class WordCheckSecurityManager extends SecurityManager {
public void checkpermission(Permission p)
{
if (p instanceof FilePermission
&& p.getActions().equals("read")) {
String fileName =
if (containsBadWords(fileName)) throw new SecurityException("Bad words in "
+ fileName);
} else super.checkpermission(p);
}
}
• Еще один способ уведомления о попытках чтения файла основан на переопределении метода checkRead. Этот метод реализован в классе Security Manager для вызова метода checkpermission с объектом FilePermission. Кроме него существует еще около 30 методов, предназначенных для выполнения других проверок обеспечения безопасности, которые также вызывают метод checkPermission (см. АР)-описания в конце раздела). Данные методы существуют лишь по историческим причинам, и в Java 2 предложена новая система прав доступа. Не рекомендуется переопределять все эти методы, гораздо эффективнее было бы выполнять все проверки прав доступа в методе checkPermission.
В нашем примере следует учесть следующую особенность. Допустим, в ходе работы этой программы возникла следующая цепь событий.
Метод какого-то класса открывает файл.
Затем диспетчер безопасности вызывает методcheckpermission.
Метод checkPermission вызывает метод containsBadWords.
В таком случае метод containsBadWords должен сам прочесть этот файл для проверки его содержания, что приведет К повторному вызову диспетчера безопасности! Таким образом будет получена бесконечная последовательность вызовов, если диспетчер безопасности НС сможет определить контекст, в котором он вызывается. В этих целях следует использовать метод getClassContext. Он возвращает массив объектов классов, вызовы которых находятся в очереди. Например, при вызове диспетчера безопасности в первый раз данный массив содержит следующие объекты классов.
class WordCheckSecurityManager
class SecurityManager
class java.io.Fileinputstream
class java.io.FileReader
class SecurityManagerFrame
class java.awt.EventDispatchThread
Класс с индексом 0 содержит текущий исполняемый вызов. К сожалению, при этом сообщаются только имена классов, а не методов. Когда диспетчер безопасности предпринимает попытку самостоятельно открыть файл, он снова вызывает метод getClassContext, который возвращает следующий массив.
class WordCheckSecurityManager
class SecurityManager
class j ava. io. Fi lelnputstreain
class java.io.FileReader
class WordCheckSecurityManager class WordCheckSecurityManager class SecurityManager
class java.io.FilelnputStream
class java.io.FileReader
class SecurityManagerFrame
class j ava.awt.EventDispatchThread
В таком случае диспетчер безопасности должен предоставить доступ к файлам. Как это можно сделать? Например, можно было бы проверить следующее условие.
getClassContext()[0] == getClassContext()(4]
Но этот подход не очень надежен. Допустим, что реализация изменилась таким образом, что диспетчер безопасности вызывается непосредственно в конструкторе FileReader. В данном случае проверка будет бессмысленной, потому что расположение в массиве может измениться. Гораздо надежнее было бы проверить, поступает ли любой ожидающий вызов от одного и того же диспетчера безопасности.
Ниже приводится код метода, который может выполнить такую проверку. Так как метод вызывается из метода checkPermission, то в стеке вызова существуют, по крайней мере, две копии класса менеджера безопасности. Их нужно пропустить, а затем поискать другой экземпляр того же диспетчера безопасности.
boolean inSameManager()
{
Class[] cc =
// Пропустить текущий набор вызовов этого диспетчера.
inti=0;
while (i < cc.length && .cc[0] == cc[i])
// Проверить наличие другого вызова этого диспетчера.
while (х < cc.length)
{
if (cc[0] == cc[i]) return true;
)'
j return false;
Этот метод вызывается в методе checkPermission. Если обнаружится, что диспетчер безопасности вызывается рекурсивно, в таком случае не следует снова вызывать метод containsBadWords.
if (p instanceof FilePermission
&& p.getActions().equals("read")) {
i £ (InSameManager())
return;
String fileName = p.getName() ;
if' (containsBadWords(fileName))
throw new SecurityException("Bad words in " + fileName);
В листинге 9.6 приводится код программы. которая демонстрирует работ)' такого диспетчера безопасности. Диспетчер безопасности инсталлируется в методе main, a проверяемый файл указывается при запуске программы. Если проверка пройдена успешно, содержимое файла загружается в текстовую область. Однако, если проверка провалилась, то программа генерирует исключительную ситуацию и отображает сообщение об ошибке, как показано на рис. 9.11. Например, программа разрешит чтение книги Alice in Wonder/and, но откажет в чтении книги The Count of Monte Cristo.
Здесь может возникнуть вопрос: а почему бы для выбора файла не использовать класс JFileChooser? Дело в том, что этот класс предпринимает попытку чтения отображаемых файлов, чтобы проверить, какие из них действительно являются файлами, а какие — каталогами.
ЕЗЕШЕЕМйь-^^^
Java мм WindowTexlfMci, /д-1. /crstoi6.txt ч л„еп i
^1 sorrybutI cannotdothat
Рис. 9.11.Программа SecurityManagerTest
При использовании программы SecurityManagerTest нужно соблюдать осторожность, потому что сам класс WordCheckSecurityManager должен иметь все права доступа Al1Permi s s ion. Это объясняется тем, что класс WordCheckSecurityManager вызывает суперкласс SecurityManagerдля всех прав доступа, кроме права на чтение. Когда диспетчер безопасности SecurityManager проверяет права доступа, он определяет, все ли методы в стеке вызовов обладают данными правами доступа. Класс WordCheckSecurityManager является одним из таких классов, но не является системным классом, поэтому следует явно передать все права доступа другим классам программы.
Для отделения класса WordCheckSecurityManager от других файлов классов следует создать отдельный J AR-файл.
jar cvf Wordcheck.jar WordCheckSecurityManager.class
Затем следует удалить файл WordCheckSecurityManager . class.
Далее необходимо создать файл политики Wordcheck.policy со следующим содержанием.
grant CodeBase "file:Wordcheck.jar" {
permission java.security.AllPermission;
Данная политика делегирует все права доступа классам в файле WordCheck. j ar.
Наконец, запустим это приложение следующим образом.
java —Djava.security.policy=WordCheck.policy
—classpath WordCheck.jar:. SecurityManagerTest
Для изменения диспетчера безопасности в собственных программах следует сначала проверить, можно ли вместо него использовать стандартный диспетчер безопасности и пользовательские права доступа, как описано в предыдущем разделе. Создание собственного менеджера безопасности сопряжено с вероятными ошибками и потому может привести к сбоям в системе обеспечения безопасности. Гораздо лучше использовать стандартный диспетчер безопасности и дополнить его системой прав доступа.
Листинг 9.6.SecurltyManegerTest.deva
/**
©version 1.11 2001-08-26
©author Cay Horstmann
*/
import java.awt.*;
import j;
import j
import j
import java.util.*;
import javax.swing .*;
/**
Пример использования пользовательского диспетчера безопасности, который запрещает чтение текстовых файлов с недопустимыми словами */ public class SecurityManagerTest {
public static void main(String[] args) {
System.setSecurityManager(new WordCheckSecurityManager());
JFrame frame = new SecurityManagerFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show() ;
} }
/**
Фрейм с текстовым полем для ввода имени файла и текстовой областью для отображения содержимого загруженного файла */ class Securi tyManagerFrame extends JFrame {
public SecurityManagerFrame()
{
setTitle(”SecurityManagerTest”);
setsize(WIDTH, HEIGHT);
fileNameField = new JTextField(20);
JPanel panel = new JPanel();
panel.add(newJLabel("Text file:"));
panel.add(fileNameField);
JButton openButton = new JButton("Open");
panel.add(openButton) ;
openButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
loadFile(fileNameField.getText());
}
});
Container contentPane = getContentPane(); contentPane.add(panel, "North");
fileText = new JTextArea(); contentPane.add(new JScrollPane(fileText), "Center") }
/**
Пытается загрузить файл в текстовую область. Если генерируется исключительная ситуация, вместо содержимого текстового файла в текстовую область вставляется сообщение об ошибке @param filename the file name
*/
public void loadFile(String filename) {
try
{
fileText.setText(”"); BufferedReader in
= new BufferedReader(new FileReader(filename));
String s;
while ((s = in.readLine()) != null) fileText.append(s+ "\n");
in.close();
}
catch (IOException e)
I
f i1eText.append(e+ "\n");
5
catch (SecurityException e) {
fileText.append ("I am sorry, but I cannot do that.") ; )
)
private JTextField fileNameField;
private JTextArea fileText;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
}
Листинг9.7.WordCheckSecurityManager. java
/**
@version 1.10 1999-10-23
©author Cay Horstmann
*/
import j
import java.security.*;
/**
Диспетчер безопасности, который проверяет
наличие недопустимых слов в текстовом файле
*/
public class WordCheckSecurityManager extends SecurityManager
{
public void checkpermission(Permission p) {
if (p instanceof FilePermission
&& p. getActions().equals("read")) {
if (inSameManager() )
return;
String fileName =;
if (containsBadWords(fileName)) throw new SecurityException("Bad words in " + fileName);
}
else super.checkPermission(p);
}
/**
Возвращает значение true, если этот диспетчер вызывается, в то время, когда ожидает в очереди еще один его вызов Sreturn true if there are multiple calls to this manager */ public boolean inSameManager() {
Class[] cc = getClassContext();
// Пропустить текущий набор вызовов этого диспетчера.
int i = 0;
while (i < cc.length && cc[0] cc[i]) i++;
// Проверить наличие другого вызова этого диспетчера. while (i < cc.length)
{
if (cc[0] == cc[i]) return true; i + + ;
} return false;
}
/**
Проверяет наличие недопустимых слов в текстовом файле @param fileName the name of the file @return true if the file name ends with . txt and it contains at least one bad word.
*/
boolean containsBadWords(String fileName) {
if (!fileName.toLowerCase().endswith(".txt”)) return false;
// Проверяет только текстовые файлы
BufferedReader in = null;
try {
in = new Buf feredReader (new FileReader (fileName) ) ;
String s;
while ((s =!= null)
{
for (int i = 0; i <i++)
if!= —1)
return true;
}
in.close();
return false;
}
e)
{
return true;
}
finally
{
if (in != null)
try { in.close(); } catch (IOException e) {}
private String[] badWords = { "sex", "drugs", "с++" }; }
java.lang.System
Методs) задает
чер безопасности s для остальной части приложения. Если параметр s имеет значение null, то никакие действия не предпринимаются. Этот метод генерирует исключительную ситуацию, если текущий диспетчер безопасности не разрешает инсталлировать новый диспетчер безопасности.
Метод SecurityManager getSecurityManager () возвращает системный диспетчер безопасности или значение null, если никакой диспетчер безопасности не инсталлирован.
java.lang.SecurityManager
Метод Class []возвращает массив классов для те
кущих выполняемых методов. Элемент массива с индексом 0 является классом текущего выполняемого метода, а элемент массива с индексом 1 является классом объекта, который вызвал текущий метод, и т.д. Такой
массив содержит только имена классов, а не имена методов.
Метод void() проверяет, может ли текущий
поток создать загрузчик классов.
Метод void checkAccess (Thread g) проверяет, может ли текущий поток вызывать методы stop, suspend, resume, setPriority, setName и setDaemon для потока g.
Метод void(ThreadGroup g) проверяет, может ли теку
щий поток вызывать методы stop, suspend, resume, destroy и для текущей группы потоков д.
Метод void checkExit (int status) проверяет, может ли текущий поток завершитьвиртуальной машины с кодом статуса status.
Метод void checkExec (String cmd) проверяет, может ли текущий поток выполнить системную команду cmd.
Метод void checkLink(String lib) проверяет, может ли текущий поток динамически загрузить библиотеку lib.
Метод void checkRead(FileDescriptor fd) проверяет, может ли текущий поток считать файл с указанным дескриптором.
Метод void checkRead(String file) проверяет, может ли текущий поток считать указанный файл.
Метод void checkWrite(FileDescriptor fd) проверяет, может ли текущий поток записать файл с указанным дескриптором.
Метод void checkWrite (String file) проверяет, может ли текущий поток записать указанный файл.
Метод void checkDelete (String file) проверяет, может ли текущий поток удалить указанный файл.
Метод void checkRead(String file, Object context) проверяет, может ли другой поток считать указанный файл. Другой поток вызывается методом getSecurityContext, а его возвращаемое значение-контекст используется в качестве параметра context.
Метод void checkConnect (String host, int port) проверяет, может ли текущий поток соединиться с хостом host по портуport.
Метод void checkConnect(String host, int port,Object
context) проверяет, может ли другой поток соединиться с хостом host по порту port. Этот поток вызывается методом getSecurityContext, a его возвращаемое значение-контекст используется в качестве параметра context.
Метод void checkListen(int port) проверяет, может ли текущий поток прослушивать соединения по портуport.
Метод void checkAccept (String host, int port) проверяет, может ли текущий поток устанавливать сетевое подключение с хостом host по портуport.
Метод void checkSetFactory () проверяет, может ли текущий поток задавать фабрику сокетов или фабрику обработчиков потоков.
Метод void checkPropertiesAccess () проверяет, может ли текущий поток осуществлять доступ к системным свойствам.
Метод void checkPropertyAccess (String key) проверяет, может ли текущий поток осуществлять доступ к системному свойству по указанному ключу.
Метод void checkSecurityAccess (String key) проверяет, может ли текущий поток осуществлять доступ к свойству обеспечения безопасности по указанному ключу.
Метод boolean window) возвращает значение true, если указанное окно window может отображаться без предупреждения со стороны системы обеспечения безопасности.
Метод void () проверяет, может ли текущий поток осуществлять доступ к заданиям печати.
Метод () проверяет, может ли текущий поток осуществлять доступ к системному буферу обмена.
Метод void checkAwtEventQueueAccess () проверяет, может ли текущий поток осуществлять доступ к очереди событий AWT.
Метод void (String pkg) проверяет, может ли текущий поток загружать классы из указанного пакета pkg. Этот метод вызывается из метода loadClass некоторых загрузчиков классов.
Метод void checkPackageDef pkg) проверяет, может ли текущий поток определять новые классы, которые находятся в указанном пакете pkg. Этот метод часто вызывается из метода loadClass пользовательского загрузчика классов.
Метод void (Class int проверяет, может ли текущий поток осуществлять доступ к члену класса с идентификатором (Более подробно способ получения идентификатора члена класса описывается в главе "Встраиваемые методы".)
Аутентификация пользователей
Начиная с SDK 1.4, стало возможным назначать отдельные права доступа для разных пользователей. Ниже приведен пример, как пользователь Harry обладает всеми правами доступа к файлам, в отличие от других пользователей.
grant principal"harry"
{
permissionFILES>>";
};
Здесь класс sun. security содержит метод возвращает учетное имя пользователя для операционной системы UNIX.
В составе службы аутентификации и авторизации Java (Java Authentication and Authorization Service — JAAS) также предусмотрена поддержка регистрации входа в операционную систему Windows NT, службы аутентификации Kerberos, а также аутентификация на основе
try(try(Регистрацию пользователя следует запрограммировать, чтобы организовать проверку заявления о делегировании прав со стороны диспетчера безопасности. Вот как выглядит базовый код регистрации доступа.
try
(
try
(
System. setSecurityManager (new SecurityManager () ) ; context = new
context.login();
successful.");
// Получитьсубъект Subject
Subject subject =
context.logout();
}
catch (LoginException exception)
{
exception.printStackTrace();
}
Теперь этот субъект обозначает аутентифицированного пользователя.
Строковый параметрв конструктореотносится к записи
с таким же именем в конфигурационном файле JAAS-службы. Типичный конфигурационный файл выглядит следующим образом.
Loginl {
required;
};
Login2
{
com.whizzbang.auth.module.SmartCardModule sufficient;
com.whizzbang.auth.module.RetinaScanModule sufficient;
}
Конфигурационный файл содержит требование Loginl о том, что пользователь должен иметь учетное имя в операционной системе UNIX. Помимо такого требования в конфигурационном файле можно перечислить и другие модули с условиями аутентификации с метками(достаточное), requisite (необходимое) или
optional (необязательное). Модули аутентификации выполняются до тех пор, пока не будет получен успешный результат выполнения какого-либо модуля с меткой неудачный исход выполнения какого-либо модуля с меткой requisite, либо будет достигнут конец списка модулей.
Конечно, в SDK не предусмотрены модули биометрической аутентификации, например, RetinaScanModule, который представлен выше. В SDK предусмотрены следующие модули аутентификации в пакетеsun.
UnixLoginModule
NTLoginModule
Krb5LoginModule
JndiLoginModule
KeyStoreLoginModule
Учетная запись аутентифицирует субъект (subject), который может иметь несколько принципаловКак показано в предложении grant, принципалы управляют
правами доступа. Некоторые права доступа могут быть связаны с именем пользователя, но чаще всего они связываются с группами пользователей. Класс
следует использовать для проверки членства пользователя в группе пользователей в операционной системе UNIX.
При регистрации пользователя в системе необходимо запустить код, который потребует выполнения проверки принципалов в отдельном контексте управления дос-
тупом. Для запуска нового привилегированного действия PriviligedActionнужно использовать статический метод doAs, который содержит метод run.
Subject.doAs(subject, new
PrivilegedAction()
{
public Object run ()
{
// Проверка принципалов
return null;
}
}) ;
Если указанный код приводит к появлению исключительной ситуации, то вместо него придется реализовать интерфейс PriviligedExceptionAction.
Пример программы, код которой приводится в листинге 9.8, демонстрирует ограничения, заданные для определенных пользователей. Для этого программа AuthTest после регистрации входа в систему запускает другую Java-программу. Имя другой программы и ее параметры задаются в режиме командной строки так, как показано ниже.
Java параметры AuthTest LineCount . ./../gutenberg/alice30.txt
Здесь LineCount — другая программа, которая просто подсчитывает строки во всех файлах, которые указаны в режиме командной строки. Код этой программы приводится в листинге 9.9.
Для выполнения данной программы нужно упаковать классы регистрации и классы программы в двух отдел ы i ых J A R -ф ай л ax..
javac AuthTest.java
jar cvf login.jar AuthTest*.class
javac LineCount.java
jar cvf prog.jar LineCount.class
В файле политики, код которого приведен в листинге9.10, указано, что пользователь Harry в операционной системе UNIX обладает правами чтения всех файлов. Замените имя Harry своим учетным именем. (Для запуска этой программы в операционной системе Windows NT нужно заменить символы UNIX символами NT в обоих файлах AuthT- est .policyи jaas . config, которые приводятся в листингах 9.10 и 9.11.)
В операционной системе UNIX для запуска этой программы следует использовать следующую команду.
java —classpath "login.jar:prog.jar"
—Djava.security,policy=AuthTest.policy
—Djava.security.auth.login.config=jaas.config
AuthTest LineCount ../../gutenberg/alice3 0 . txt
В операционной системе Windows для запуска этой программы следует использовать такую команду.
java —classpath " login.jar;prog.jar"
—Djava.security.policy=AuthTest.policy
—Djava.security.auth.login.config=jaas.config
AuthTest LineCount ..\..\gutenberg\alice3 0.txt
Теперь программа LineCount приступит к подсчету количества строк. Однако, при изменении учетного имени в файле AuthTest.policy должна возникнуть исключительная ситуация, потому что это имя не обладает требуемыми правами доступа.
НИМАНИНИМАНИНеобходимо очень тщательно следовать приведенным ниже инструкциям. Дело в том, что в этом случае очень легко получить неверную конфигурацию даже при очень незначительных модификациях. В частности, обратите внимание, что права доступа нужно добавить в предложение grant файла login.jar и предложение grant принципала. (Учтите, что при выполнении метода doAs предоставляются только те права, которые образуют пересечение прав, указанных в этих предложениях.) Кроме того, файлы программы нужно включить в JAR-файл. При использовании явно указанного пути к классам " login .jar: ."права доступа не будут применяться к классам файловой системы, что может стать причиной возникновения ошибок.
НИМАНИ
НИМАНИ
В данном разделе описаны только некоторые основные возможности JAAS- службы. Более подробные сведения по этой теме можно найти в руководствеJAAS Reference Guide, котороевходитвсоставдокументацииSDK.
Листинг 9.8. AuthTest.java
import java.lang.reflect.*;
import java.security.*;
import javax.security.auth.*;
import javax.security.auth.login.* ;
/**
Пример аутентификации пользователя
с помощью учетного имени в операционной системе Unix и выполнения программы, указанной в командной строке с принципалами пользователя .
*/
public class AuthTest (
public static void main(final String[] args) (
try <
System.setSecurityManager(new SecurityManager());
LoginContext context = new Logincontext("Loginl"); context.login();
System.out.printin("Authentication successful.");
Subject subject = context.getSubject();
Subject.doAs(subject, new PrivilegedExceptionAction() {
public Object runt) throws Exception {
// Вызывает метод main класса,
// указанного в аргументе args[0],
// с аргументами args[l] args[2] ...
Class cl = Class.forName(args[ 0 ] ) ;
Method mainMethod =
new Class[] { String[].class });
String!] argsl = new String[args.length— 1];
Systern.arraycopy(args, 1, argsl, 0, argsl.length);
mainMethod.invoke(null,new Object[] { argsl }) ;
return null;
} }) ;
context.logout();
}
catch (LoginException exception)
< exception.printStackTrace();
}
catch (PrivilegedActionException exception) {
exception.printStackTrace();
■}
}
}
Листинг 9.9. Linecount.java
import java.io.*;
/ * *
Пример программы, которая может быть запущена
из программы AuthTest. Она просто подсчитывает строки
во всех файлах, чьи имена указаны в виде аргументов командной строки
*/
public class LineCount
{
public static void main(String[] args) throws Exception
{
for (int i = 0; i < args.length; i++) {
int count = 0;
String line;
Buf feredReader reader
= new BufferedReader(new FileReader(args[i]));
while ((line = reader.readLine()) != null) count++;
System.out.println(args[i] + ": " + count);
}
)
Листинг 9.10.AuthTest.policy
grant codebase "file:login.jar"
{
permission javax.security.auth.AuthPermission
" createLoginContext.Loginl";
permission javax.security.auth.AuthPermission "doAs";
permission java.io.FilePermission "«ALL FILES>>", "read"
grant principal com.sun.security.auth.UnixPrincipal "harry" (
permission java. io.FilePermission "«ALL FILES»", "read"; };
Листинг 9.11. jaas.config
Loginl {
com.sun.security.auth.module.UnixLoginModule required;
E
“ pTyT^ avax. security. auth. login. Logincontext
_С_У * Конструктор LoginContext(String name) создает контекст регистрации, который соответствует дескриптору регистрации name в файле конфигурации JAAS.
Метод void login О регистрирует субъект или генерирует исключительную ситуацию LoginException в случае неудачной регистрации. Он вызывает метод login для всех модулей регистрации, которые указаны в файле конфигурации JAAS.
Метод void logout () снимает регистрацию субъекта. Он вызывает метод logout для всех модулей регистрации, которые указаны в файле конфигурации JAAS.
Метод Subjectget Subj ect () возвращает аутентифицированный субъект.
j avax. security. auth. Subj ect
Метод Set getPrincipals () возвращает набор принципалов данного субъекта.
Метод static Object doAs(Subject subject, PrivilegedAction action) выполняет привилегированное действие с разрешения субъекта и возвращает объект, сгенерированный методом run класса Privileg- edAction.
Метод static Object doAs(Subject subject, PrivilegedExcep- tionAction action) выполняет привилегированное действие с разрешения субъекта и возвращает объект, сгенерированный методом run класса PrivilegedExceptionAction.
Java.security.PrivilegedAction
Метод Obj ect run () нужно реализовать для выполнения того кода, который требуется выполнить с разрешения субъекта.
java. securi ty. PrivilegedExceptionAction
Метод Object run () следует реализовать для выполнения того кода, который требуется выполнить с разрешения субъекта. Он может генерировать любые перехватываемые исключительные ситуации.
java. securi ty. Principal
Метод String getName() возвращает имя данного принципала.
Цифровые подписи
Как уже упоминалось выше, именно применение аплетов вызвало наибольший интерес к платформе Java. Программисты сразу поняли, что с практической точки зрения, несмотря на широкие анимационные возможности аплетов, например, известный "дергающийся текст" ("nervous text"), аплеты не поддерживают многие важные элементы модели обеспечения безопасности JDK 1.0. В пакете JDK 1.0 аплеты очень строго контролируются, поэтому не могут широко использоваться в корпоративной сети, хотя могут вполне безопасно выполняться в защищенной корпоративной сети компании. Специалисты компании Sun быстро осознали полезность аплетов и решили предоставить пользователям возможность присваивать аплетам разные уровни обеспечения безопасности в зависимости от места происхождения аплета. Например, если аплет поступил от надежного поставщика, которому вы доверяете, то данному аплету можно присвоить более обширные права доступа.
Такая дополнительная система контроля работает, благодаря встроенному в пакет Java 1.1 механизму цифровых подписей. Для предоставления аплету более широких прав доступа необходимо иметь следующие сведения.
Откуда поступил аплет?
Был ли он во время пересылки изменен?
За последние 50 лет специалисты в области математики и информатики разработали много сложных алгоритмов для поддержки целостности данных и цифровых подписей. Многие из них реализованы в пакете j ava. security. Для использования таких алгоритмов совсем не обязательно понимать математические принципы, на которых они основаны. В следующих разделах описывается механизм, с помощью которого дайджест сообщения (message digest) может обнаружить изменение данного документа, а также способы, с помощью которых цифровая подпись подтверждает личность подписавшегося пользователя.
Дайджест сообщения
Дайджест сообщения — это цифровой ‘‘дактилоскопический’5 отпечаток блока данных. Например, хэш-алгоритм обеспечения безопасности SilAl (secure hash algorithm #1) сжимает любой блок данных случайного размера в последовательность 160 бит (20 байт). По аналогии с дактилоскопическими отпечатками пальцев можно подумать, что не существует двух одинаковых отпечатков SHA1. На самом деле это не так, потому что алгоритм SHA1
поддерживает 2 отпечатков, и поэтому среди них вполне могут существовать два одинаковых отпечатка. Но число 2 настолько велико, что вероятностьдублирования очень мала. Согласно книге True Odds: How risks Affect Your Everyday Life Джеймса Волша (James Walsh) [Merritt Publishing 1996] вероятность смертельного исхода от удара молнии равняется 1/30000. Если теперь подсчитать вероятность такого же исхода для 10 человек (например, для вас и ваших "любимых" непосредственных начальников или преподавателей), то эта вероятность гораздо выше, чем вероятность существования двух одинаковых отпечатков SHA1. (Конечно, от удара молнии погибло гораздо больше, чем 10 человек, но здесь речь идет о гораздо меньшей вероятности такой гибели для епщилльно выбранной группы людей, т.е. ваших знакомых в данном случае.)
Дайджест сообщения имеет два важных свойства.
Если изменяется один или несколько битов данных, то дайджест сообщения тоже будет изменен.
Нельзя изменить оригинальное сообщение таким образом, чтобы полученное поддельное сообщение имело такой же дайджест, что и у оригинального сообщения.
Второе свойство, конечно, соблюдается с определенной степенью вероятности. Допустим, что некий миллиардер составил завещание.
Upon my death, my property shall be divided equally among my
children; however, my son George shall receive nothing.
После смерти мое имущество должно быть разделено между моими детьми; однако, мой сын Джордж ничего не получит.
Отпечаток SHA1 для данного сообщения имеет такой вид.
2D 8В 35 F3 BF 49 CD В1 94 04 Е0 66 21 2В 5Е 57 70 49 El 7E
Допустим, что миллиардер отдал текст завещания одному адвокату, а его отпечаток — другому. Далее предположим, что Джордж подкупил адвоката, у которого хранится завещание, и слово George заменил словом Bill. В этом случае цифровой отпечаток поддельного завещания будет отличаться от отпечатка оригинального завещания.
2А330В4ВВЗFEСС1С9D5С01А709510В49AC 8F 98 92
Может ли Джордж составить такое поддельное завещание, которое имело бы отпечаток оригинального завещания? Нет, потому что для перебора всех вариантов ему не хватило бы времени с момента сотворения Земли, даже если бы он был счастливым обладателем миллиарда компьютеров, которые могут перебирать миллион вариантов завещания в секунду.
Для определения профиля сообщения существует несколько алгоритмов. Наиболее известными из них являются алгоритм SHA1, разработанный национальным институтом стандартов и технологий (National Institute of Standards and Technology — NIST) и алгоритм MD5, изобретенный Рональдом Райвестом (Ronald Rivest) из Mac- сачуссетского технологического института (Massachussets Institute of Technlogies — MIT). Оба алгоритма зашифровывают сообщение разными оригинальными способами. Более подробно об этих методах можно прочесть в книге Cryptography and Network Security Вильяма Столингса (William Stallings) [Prentice Hall 1998]. В результатах работы алгоритма MD5 совсем недавно обнаружены изъяны, поэтому многие ведущие специалисты-шифровальщики рекомендуют использовать алгоритм SHA1. (Оба алгоритма имеют достаточно простую структуру.)
В языке Java реализованы оба эти алгоритма. Класс MessageDigest представляет собой фабрику (factory) объектов, которые включают алгоритмы создания цифровых отпечатков. Данный класс содержит статический метод getlnstance, который возвращает объект подкласса класса MessageDigest. Поэтому класс MessageDigest может выполнять следующие функции.
Класс-фабрика.
Суперкласс всех алгоритмов создания профиля сообщения.
Ниже приведен пример получения объекта для расчета отпечатка согласно алгоритму SHA1.
MessageDigest alg = MessageDigest.getlnstance("SHA-1");
(Для получения объекта, который использует алгоритм MD5, нужно в качестве аргумента метода getlnstance вместо строки " SHA-1" указать строку "MD5".)
После получения объекта типа MessageDigest ему необходимо передать все байты сообщения с помощью повторных вызовов метода update. Например, следующий фрагмент кода передает все байты файла созданному выше объекту-создателю цифрового отпечатка alg.
FileinputStrearn in = new Fileinputstream(f); int ch;
while ( (ch = in.read()) != -1) alg.update ((byte)ch);
Если байты размещены в массиве, то метод update можно применить сразу для всего массива.
byte[] bytes alg.update(bytes) ;
После чего следует вызвать метод digest, который дополняет входной поток недостающими битами (необходимое условие использования алгоритма создания отпечатков), рассчитывает цифровой отпечаток и возвращает дайджест сообщения в виде массива байтов.
byte[] hash = alg.digest ();
Программа, код которой приводится в листинге 9.12, рассчитывает дайджест сообщения, используя алгоритм SHA или MD5. Данные можно загрузить из файла или ввести с помощью клавиатуры в текстовой области. На рис. 9.12 показан внешний вид этой программы.
Рис. 9.12. Расчет профиля сообщения
Листинг9.12.MessageDigestTest.java
/**
©version 1.11 2001-08-26
@author Cay Horstmann
*/
import java.io.*;
import java.security.*;
import j
import j
import javax.swing.*;
/**
Пример вычисления профиля сообщения для файла или содержимого текстовой области */
public class MessageDigestTest
{
public static void main(String[j args) {
JFrame frame = new MessageDigestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Фрейм с командами меню для вычисления профиля сообщения, заданного в виде текстового файла или текста в текстовой области, с переключателями для выбора алгоритма
SHA-1 или MD5, с текстовой областью, а также с текстовым полем для отображения профиля сообщения
*/
class MessageDigestFrame extends JFrame {
public MessageDigestFrame()
{
setTitle("MessageDigestTest") ;
setsize(WIDTH, HEIGHT);
JPanel panel = new JPanel();
ButtonGroup group = new ButtonGroup();
addRadioButton(panel, "SHA-1", group);
addRadioButton(panel, "MD5", group);
Container contentPane = getContentPane();
content Pane.add(panel, BorderLayout.NORTH);
contentpane.add(new JScrollPane(message),
BorderLayout.CENTER);
contentpane.add(digest, BorderLayout.SOUTH);
digest.setFont (new Font("Monospaced", Font.PLAIN, 12))
setAlgorithm("SHA-1");
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("File");
JMenuItem fileDigestltem = new JMenuItem("File digest") fileDigestltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
loadFile();
}
)) ;
menu.add(fileDigestltem) ;
JMenuItem textDigestltem
= new JMenuItem("Text area digest");
textDigestltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
String m=
computeDigest(m.getBytes()) ;
}
)) ;
menu.add(textDigestltem);
menuBar.add(menu);
setJMenuBar(menuBar) ;
/**
Создание переключателя для выбора алгоритма
Sparain с the container into which to place the button
@param name the algorithm name
@param g the button group
*/
public void addRadioButton(Container c, final String name, ButtonGroup g)
{
ActionListener listener = new
ActionListener()
{ public void actionPerformed(ActionEvent event) {
setAlgorithm(name);
}
};
JRadioButton b
= new JRadioButton(name, g.getButtonCount() == 0);
c.add(b);
g.add(b);
b.addActionListener(listener);
/ * *
Установка алгоритма вычисления профиля сообщения @param alg the algorithm name
*/
public void setAlgorithm(String alg)
{
try
currentAlgorithm =
digest. setText (11") ;
}
catch(NoSuchAlgorithmException e) {
digest.setText(""+e);
}
/**
Загрузка файла и вычисление профиля сообщения */
public void loadFile()
{
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
int r = chooser.showOpenDialog(this) ;
if (r == JFileChooser.APPROVE_OPTION) {
String name =
computeDigest(loadBytes(name));
}
/**
Загрузка байтов
@param name the file name
@return an array with the bytes in the file
*/
public byte[] loadBytes(String name)
{
Fileinputstream in = null;
try
{ in = new Fileinputstream(name);
ByteArrayOutputStream buffer
= new ByteArrayOutputStream();
int ch;
while ( (ch =!= -1)
buffer.write(ch) ;
return buffer.toByteArray() ;
}
catch (IOException e)
{
if (in != null)
{
try { in.close(); } catch (IOException e2) {} } return null;
}
/**
Вычисление профиля сообщения для массива байтов и отображение его в текстовом поле @param b the bytes for which the message digest should
be computed.
*/
public void computeDigest(byte[ ] b)
currentAlgorithm.reset () ;
currentAlgorithm.update(b);
byte[] hash = currentAlgorithm.digest() ;
String d= "";
for (int i = 0; i < hash.length; i++)
{
int v = hash[i] & OxFF;
if (v < 16) d += "0";
++d += Integer.toString(v, 16).toUpperCase() }
+
+
digest.setText(d) ;
}
private JTextArea message = new JTextArea();
private JTextField digest = new JTextField();
private MessageDigest currentAlgorithm;
private static final int WIDTH = 400;
private static final int HEIGHT = 300;
java.security.MessageDigest
Метод static MessageDigest getlnstance(String algorithm) возвращает объект, который реализует указанный алгоритм. Если указанный алгоритм не предусмотрен, то генерируется исключительная ситуация NoSuchAlgorithmExcetion.
Параметры:
algorithmимя алгоритма, например, SHA-1 или MDS
Методvoid update (byte input) обновляет дайджест, используя указанный байт.
Метод void update (byte [] input) обновляет дайджест, используя указанный массив байтов.
Метод void byte update (byte [] input, int offset, int len) обновляет дайджест, используя байты из указанного массива байтов с заданным отступом offsetи длиной len.
Метод byte [ ] digest () выполняет хэш-расчет, возвращает рассчитанный дайджест и возвращает в исходное состояние объект-алгоритм.
Метод void reset О возвращает в исходное состояниеобъекталгоритм.
Подпись сообщения
В предыдущем разделе описываются способы вычисления профиля сообщения, который является аналогом дактилоскопического отпечатка. Если данное сообщение изменить, то новый цифровой отпечаток будет отличаться от исходного. Поэтому, раздельно посылая сообщение и его отпечаток, получатель может узнать, было ли это сообщение изменено. Но если сообщение и его отпечаток посылаются вместе, то но-
вый отпечаток очень легко подделать. Дело в том, что алгоритмы создания профиля сообщения широко известны и для них не нужно использовать ключи обеспечения безопасности. Поэтому, если получателю прислали поддельное сообщение и соответственно поддельный цифровой отпечаток, то он никогда не узнает, что сообщение изменено. Именно для этого и используются описываемые ниже способы аутентификации сообщения с помощью цифровой подписи. Аутентификация гарантирует:
неизменность сообщения;
достоверность отправки сообщения указанным отправителем.
Далее для понимания способов работы с цифровыми подписями следует рассмотреть несколько основных принципов работы криптографии с открытым ключом (public hey cryptography), которая основана на использовании открытого ключа (public key) и закрытого ключа (private key). Открытый ключ можно оглашать где угодно и кому угодно, а закрытый необходимо держать в строгом секрете. Значения этих ключей связаны математической зависимостью. Однако, для вычисления закрытого ключа на основе известного открытого ключа даже при использовании всей имеющейся компьютерной техники не хватит целой жизни.
В это трудно поверить, но до сих пор не существует алгоритма вычисления закрытого ключа на основании известного открытого ключа для современных алгоритмов шифрования. Более того, понадобится затратить тысячи лет для грубого перебора достаточно длинных ключей с помощью компьютеров, изготовленных изо всех атомов солнечной системы. Конечно, нельзя исключать вероятность того, что кто-то сможет разработать более эффективный алгоритм поиска закрытого ключа, чем простой перебор значений. Например, алгоритм шифрования RSA, изобретенный Рай- вестом (Rivest), Шамиром (Shamir) и Эдлсманом (Adleman), зависит от сложности разложения больших чисел на простые множители. Последние 20 лет многие известные математики безуспешно работают над созданием эффективного и быстрого алгоритма разложения чисел на простые множители. Поэтому большинство шифровальщиков считают, что ключ с размером 2000 бит по модулю в настоящее время абсолютно защищен от любой попытки несанкционированного доступа.
В криптографии с открытым ключом используются две разновидности пар закры- тый/открытый ключ: одна пара для /цифрования (encryption), а другая для аутенти фикации (authentication). Если сообщение зашифровано вашим открытым ключом шифрования, то его можно расшифровать только при помощи вашего закрытого ключа дешифрования. И наоборот, если ваше сообщение подписано вашим закрытым ключом аутентификации, то любой получатель может проверить достоверность подписи с помощью вашего открытого ключа. Верификацию (т.е. проверку) успешно могут пройти только подписанные вами сообщения и не смогут пройти все другие сообщения, подписанные другими ключами. (Как пишет Канн в последнем издании своей книги The Codebreakers, это была действительно первая принципиально новая идея в области криптографии за последние сто лет.)
Многие криптографические алгоритмы, например, RSA и алгоритм цифровой подписи DSA (Digital Signature Algorithm), используют эту идею. Точная структура ключей и строгое соответствие между ключами зависит от конкретного алгоритма. Ниже приведен пример открытого и закрытого ключей, которые используются в технологии DSA.
Открытый ключ имеет следующий вид.
fca682ce8el2caba26efccf7110e52 6db07 8b05edecbcdleb4a208f3ael6 17ae01f35b91a47e6df63413c5el2ed0899bcdl32acd50d99151bdc43ee737 5 92е17
9 62eddcc3 69cba8ebb260ee6b6al26d9346e38c5
g: 67 8471b27a9cf44ee91a49c5147dbla9aaf244f05a434d6486931d2dl427 lb9e3 503 0b71fd73dal79069b32e293 5630elc2062354d0da20a6c416e50be79 4ca4
y: c0b6e67b4ac098ebla32c5f8c4clf0e7e6fd9d832532e27d0bdab9ca2d2a 8123ce5a801818161a7 6 048 0fadd040b927281ddb22cb9bc4df596d7de4dlb97 7d50
Соответствующий ему закрытый ключ выглядит вот так.
fca682ce8el2caba2 6e£ccf7110e526db07 8b05edecbcdleb4a208f3ael6 17ae01f3 5b91a47e6df63 413c5el2ed0899bcdl32acd50d99151bdc43ee73 7 59 2е17
962eddcc3 69cba8ebb26 0ee6b6al26d9346e38c5
g: 67 8471b27a9cf44ee91a49c5147dbla9aaf244f05a434d 6 4 86 9 31d2dl427
Ib9e35 03 0b71fd73dal7 9 069b32e2 93 5630elc2 0623 54d0da2 0a6c416e50be79 4ca4
x: 146c09f8 816 5 6cc6c51f27ea6c3a91b85edld7 0a
Между этими ключами существует строгая математическая связь, но она не имеет никакого значения для практического применения данного алгоритма. (Более подробно об этой взаимосвязи можно прочитать в упомянутой выше книге Cryptography and Network Security.)
Как же создается эта пара ключей? Обычно делается это таким образом: сначала получают результаты какого-то случайного процесса, а затем используют их в качестве входных данных некого детерминированного процесса, который и возвращает пару ключей. К счастью, программистам совершенно не нужно заботиться о принципе работы всей этой процедуры, потому что это задача математиков и шифровальщиков.
Ниже приведен практический пример использования этой технологии. Предположим, Алиса послала Бобу сообщение, а Боб хочет удостовериться, что сообщение пришло именно от Алисы, а не от какого-то самозванца. Для этого Алиса подписывает (sign) дайджест сообщения с помощью своего закрытого ключа. Потом Боб получает копию ее открытого ключа и с его помощью проверяет (verify) подпись. Если проверка прошла успешно, то он точно знает следующее.
Сообщение не было изменено.
Сообщение было послано Алисой, а не кем-то другим.
Этот процесс схематически показан на рис. 9.13.
Рис.9.13.Обмен сообщениями с использованием открытого ключа в технологии DSA
Теперь понятно, почему закрытый ключ нужно держать в секрете. Если он перехвачен злоумышленником, то Боб может получить от имени Алисы ложную информацию, которую Алиса на самом деле не посылала.
Пакет обеспечения безопасности Java поддерживает технологию DSA, а для использования технологии RSA следует приобрести соответствующий пакет классов технологии RSA по адресу www. rsa.com. Рассмотрим более подробно принцип работы технологии DSA, который содержит три алгоритма.
Алгоритм генерации пары ключей.
Алгоритм подписания сообщения.
Алгоритм верификации подписи.
Пара ключей создается только один раз и многократно может использоваться для подписания и проверки нескольких сообщений. Для генерации случайной пары ключей недостаточно просто применить класс Random, который основан на текущей дате и времени, и нельзя назвать достаточно хорошим генератором случайных чисел. Для этого нужно использовать очень надежные генераторы случайных чисел, а пакет java. util считается "криптографически не достаточно безопасным". Предположим, то тактовая частота процессора равна 0,1, тогда в течение дня генерируется приблизительно 865000 случайных чисел. Если злоумышленник точно узнает день генерации такого "случайного" ключа
(например, по дате истечения его срока действия), то он сможет достаточно быстро сгенерировать и перебрать все эти "случайные" числа.
В криптографии для генерации случайных чисел вместо класса Random лучше применить более надежный класс SecureRandom. Для генерации последовательности случайных чисел нужно получить начальное число (seed). Лучше всего это сделать, используя специализированное аппаратное обеспечение, например, какой-нибудь генератор "белого" шума. Еще один способ основан на беспорядочных нажатиях клавиш клавиатуры посторонним пользователем. Причем каждое нажатие клавиши генерирует только один или два бита в начальном случайном числе. После получения всех таких случайно генерированных битов их нужно собрать в массив и передать его в качестве параметра методу setSeed.
SecureRandom secrand = new SecureRandom();
Byte[] b = new byte[20];
// Получение "более случайных" битов secrand.setSeed(b) ;
При отсутствии начального случайного числа генератор случайных чисел самостоятельно вычислит собственное начальное число длиной 20 байт, запуская потоки, дезактивируя их и засекая точное время их активизации.
Это совершенно новый алгоритм, и поэтому о его надежности ничего не известно. В прошлом в этих целях использовались алгоритмы, основанные на времени работы некоторых компонентов компьютера, например, времени доступа к жесткому диску. Однако, позднее стало понятно, что таким образом создаются не совсем случайные числа.
После создания начального числа можно с помощью метода nextBytes создать последовательность случайно генерируемых байтов.
byte[] randomBytes = new byte[64]; secrand.nextBytes(randomBytes);
Фактически, для вычисления ключа в технологии DSA не нужно самостоятельно создавать случайные числа. Для этого достаточно разместить объект генератора случайных чисел в алгоритме создания ключа DSA.
Для создания новой пары ключей используется объект типа KeyPairGenerator. Так же как и класс дайджеста сообщения MessageDigest, класс-генератор пары ключей KeyPairGenerator представляет собой класс-фабрику и суперкласс для всех алгоритмов генерации пар ключей.
Для создания объекта-генератора пары ключей DSA нужно применить метод getlnstance с параметром DSA.
KeyPairGenerator keygen = KeyPairGenerator.getlnstance("DSA”);
Возвращаемый объект фактически является объектом класса sun. security. provider .DSAKeyPairGenerator, который, в свою очередь, является подклассом класса KeyPairGenerator.
Для генерации ключей нужно инициализировать объект-алгоритм следующими параметрами: размер ключа и безопасный генератор случайных чисел.
SecureRandom secrand = new SecureRandom();
secrand.setSeed (...);
keygen.initialize(512, secrand);
Теперь можно создавать ключи.
KeyPair keys = keygen.generateKeyPair();
KeyPair morekeys =
В каждой паре ключей должен быть закрытый и открытый ключи.
PublicKey pubkey =
PrivateKey privkey = keys.getPrivate();
Чтобы подписать сообщение, создается объект-алгоритм подписи класса
Signature.•
Signature signalg = Signature.getlnstance("DSA");
Объекты алгоритма подписи могут использоваться как для подписи, так и для верификации сообщения. Далее, чтобы подготовить объект к подписанию сообщения, необходимо вызвать метод initSignи передать ему закрытый ключ.
signalg.initSign(privkey);
Теперь, используя метод update, нужно передать байты объекту алгоритма так же, как это делалось с дайджестом сообщения.
while((ch =().) != -1)
signalg.update((byte)ch);
И в заключение, используя метод sign, нужно вычислить подпись в виде массива байтов.
byte[] signature = signalg.sign();■
Получатель сообщения должен получить объект алгоритма подписи DSA и вызвать метод initVerifyс открытым ключом в качестве параметра.
Signature verifyalg = Signature.getlnstance("DSA"); verifyalg.initVerify(pubkey);
После чего сообщение следует передать объекту алгоритма.
while ((ch =!= -1)
verifyalg.update((byte)ch);
Теперь можно проверить подпись.
boolean check = verifyalg.verify(signature);
Если метод verifyвозвращает значение true, то это значит, что данное сообщение было подписано соответствующим ему закрытым ключом. Другими словами это означает аутентификацию сообщения и его отправителя.
В листинге 9.13 приведен пример создания пары ключей, подписания и верификации сообщения.
Листинг 9.13. SignatureTeat.Java,
aversion 1.00 1997-09-10
©author Cay Horstmann
*/
import
/**
Пример подписания сообщения с помощью
закрытого ключа DSA и верификации с помощью соответствующего открытого ключа */ public class { public static voidargs)
{ try { KeyPairGenerator keygen =;
secrand = new SecureRandom();
keygen.initialize(512, secrand);
KeyPair=
PublicKey pubkeyl =
PrivateKey privkeyl =
KeyPair keys2 =
PublicKey pubkey2 =
PrivateKey privkey2 =
Signature signalg =
signalg.initSign(privkeyl); message
= "Pay authors a bonus of $20,000.";
signalg.update(message.getBytes());■
byte[] signature =
Signature verifyalg =
verifyalg.initVerify(pubkeyl);
verifyalg.update(message.getBytes()); if
");
with private key 1");
verifyalg.initVerify(pubkey2);
verifyalg.update(message.getBytes());
if (Iverifyalg.verify(signature))
");
with private key 2") ;
e) { e.printStackTrace(); )
}
}•.
Метод static KeyPairGeneratoralgorithm)
возвращает объект класса KeyPairGenerator, который выполняет указанный алгоритм. Если алгоритм неуказан, то генерируется исключительная ситуация
Параметры:
algorithm имя алгоритма, например DSA
Метод void initialize(int strength, SecureRandom random) инициализирует генератор случайных чисел.
Параметры:
strength специфический размер; как правило, это количество битов по модулю для одного из параметров алгоритма
randomисточник случайных битов
Метод KeyPair GenerateKeyPair() создает новую пару ключей.
java.security.KeyPair
Метод PrivateKey getPrivate () возвращает закрытый ключ для данной пары ключей.
Метод PublicKey getPublic () возвращает открытый ключ для данной пары ключей.
java.security.Signature
Метод static Signature getlnstance(String algorithm) возвращает объект класса Signature, который реализует указанный алгоритм. Если алгоритм не указан, то генерируется исключительная ситуация NoSuchAlgorithmException.
Параметры:
algorithm имя алгоритма, например DSA
Метод void initsign(PrivateKey privateKey) инициализирует данный объект для подписания. Если тип ключа не соответствует типу алгоритма, то генерируется исключительная ситуация InvalidKeyException.
Параметры:
privateKeyзакрытый ключ подписываемого объекта
Метод void update (byte input) обновляет буфер сообщения, используя указанный байт.
Метод void update(byte [] input) обновляет буфер сообщения, используя указанные байты.
Метод void update(byte [] input, int offset, int len) обновляет буфер сообщения, используя указанные байты, смещение и длину.
Метод byte [] sign() вычисляет и возвращает подпись.
Метод void initVerify(PublicKey publicKey) инициализирует объект для верификации. Если тип ключа не соответствует типу алгоритма, то генерируется исключительная ситуация InvalidKeyException.
Параметры:
publicKey открытый ключ проверяемого объекта
Метод boolean verify(byte [] signature) проверяет подпись.
Аутентификация сообщения
Предположим, что вы получили сообщение от приятеля, который подписал его закрытым ключом с помощью описанного выше метода. При наличии открытого ключа можно проверить, было ли это сообщение действительно создано вашим приятелем или каким-то самозванцем, а также было ли оно изменено за время пересылки или нет. Теперь предположим, что вы получили сообщение с программным обеспечением, которым автор сообщения от имени известной фирмы-производителя программного обеспечения предлагает воспользоваться. Вместе с сообщением незнакомец может даже прислать копию открытого ключа. Благодаря этому можно верифицировать подпись сообщения и успешно аутентифицировать сообщение. Таким образом можно доказать, что сообщение было подписано соответствующим закрытым ключом и не было подделано.
Учтите, чтопри этомна самомделе совершенно не известен авторсообщения. Значит, любой желающий может создать пару ключей, потом подписать сообщение закрытым ключом и выслать сообщение вместе с соответствующим открытым ключом. Задача определе- нияличности отправителя называется задачейаутентификации (authentication probleni).
Обычно она решается очень просто. Предположим, у вас и незнакомца- отправителя сообщения есть общий знакомый, которому вы оба доверяете. Допустим, что незнакомец-отправитель сообщения встретился с вашим общим знакомым и передал ему на дискете открытый ключ. Далее, ваш знакомый встречается с вами и объясняет, что этот незнакомец действительно работает в указанной фирме по разработке программного обеспечения, а затем передает вам дискету с открытым ключом. На рис. 9.14 показана общая схема данного процесса аутентификации незнакомца- отправителя сообщения.
Закрытый ключЗакрытый ключОткрытый ключОткрытый ключДоверенныйДоверенныйканал связиканал связиНезнакомецНезнакомецФайл с открытым ключомФайл с открытым ключомПриятельДоверенныйПриятельДоверенный
Закрытый ключ
Закрытый ключ
Открытый ключ
Открытый ключ
Доверенный
Доверенный
канал связи
канал связи
Незнакомец
Незнакомец
Файл с открытым ключом
Файл с открытым ключом
Приятель
Доверенный
Приятель
Доверенный
канал связи
пп
п
п
Вы
Рис.9.14. Аутентификация через доверенного посредника
Вашему знакомому не обязательно встречаться с вами, потому что он может применить свой собственный закрытый ключ для подписания файла с открытым ключом незнакомца и послать его вам, как показано на рис. 9.15.
Закрытый ключЗакрытый ключФайл с ключомФайл с ключомДоверенныйДоверенныйканал связиканал связиЗакрытый ключЗакрытый ключключключОткрытыйОткрытыйНезнакомец Открытый ключНезнакомец Открытый ключПриятельПриятельПодписьПодписьПодписанный файл с ключомПодписанный файл с ключомДоверенный канал связиДоверенный канал связи
Закрытый ключ
Закрытый ключ
Файл с ключом
Файл с ключом
Доверенный
Доверенный
канал связи
канал связи
Закрытый ключ
Закрытый ключ
ключ
ключ
Открытый
Открытый
Незнакомец Открытый ключ
Незнакомец Открытый ключ
Приятель
Приятель
Подпись
Подпись
Подписанный файл с ключом
Подписанный файл с ключом
Доверенный канал связи
Доверенный канал связи
Вы
Рис. 9.75Аутентификация через подпись доверенного посредника
После получения файла с открытым ключом можно проверить подпись, сделанную вашим приятелем, а так как вы ему доверяете, то можно быть уверенным в идентичности этого незнакомца. '
Однако, у вас и незнакомца такого общего знакомого может и не быть. В некоторых моделях аутентификации используется понятие "цепочка доверия" ("chain of trust"), т.е. цепочка общих знакомых, которые доверяют друг другу. Как показывает практика, это конечно не всегда верно, потому что вы можете доверять вашей знакомой Алисе, которая доверяет Бобу, но вы лично не знаете Боба, и поэтому вам трудно ему довериться. Другая модель аутентификации основана и» том, что все доверяют какой-либо одной зарекомендовавшей себя компании. Многие компании стремятся получить такое доверие со стороны пользователей, например, компания Verisign, Inc. (www.verisign.com) и даже почтовая служба США.
Зачастую пользователям приходится сталкиваться с цифровыми подписями разных организаций и самостоятельно решать вопрос о степени доверия к ним. Например, можно проникнуться большим доверием к компании Verisign после чтения их увесистых официальных заявлений или слухов о невероятных мерах обеспечения безопасности в этой компании.
На самом деле нужно иметь четкое представление о том, что именно вам необходимо аутентифицировать. Дело в том, что Страттон Склавос (Stratton Sclavos), президент компании Verisign, просто физически не может персонально передать каждому своему клиенту открытый ключ, который аутентифицйрован компанией Verisign. Для получения открытого ключа клиент чаще всего .ПрбСТО заполняет соответствующую форму на Web-сайте компании, как показано на рис. 9.16.
CriSign’ iCriSign’ iав&&&&ЭДМав&&&&ЭДМ//dlgitalid.verlsign.com/clienvclasslNetsca//dlgitalid.verlsign.com/clienvclasslNetscaEnrollmentEnrollment
CriSign’ i
CriSign’ i
ав&&&&ЭДМ
ав&&&&ЭДМ
//dlgitalid.verlsign.com/clienvclasslNetsca
//dlgitalid.verlsign.com/clienvclasslNetsca
Enrollment
Enrollment
Help with this PageHelp with this PageVeriSign™ Class 1 Digital IDSM . for Netscape Communicator or Netscape NavigatorStep 1 of 4: Complete Enrollment Form', Step , CompelteEnromllentFormStep3:PcikupDigitalIOStep 2; Check E-mailStep 4: meta# Digital ID,VeriSign™ Class 1 Digital IDSM . for Netscape Communicator or Netscape NavigatorStep 1 of 4: Complete Enrollment Form', Step , CompelteEnromllentFormStep3:PcikupDigitalIOStep 2; Check E-mailStep 4: meta# Digital ID,ККContents of YourDigitalFillinallUsetheEnglishalphabetwithnoaccentedcharacters. ThisInformationisincludedinyourDigitalI andistothepublic.■Contents of YourDigitalFillinallUsetheEnglishalphabetwithnoaccentedcharacters. ThisInformationisincludedinyourDigitalI andistothepublic.■
Help with this Page
Help with this Page
VeriSign™ Class 1 Digital IDSM . for Netscape Communicator or Netscape Navigator
Step 1 of 4: Complete Enrollment Form
', Step , CompelteEnromllentFormStep3:PcikupDigitalIO
Step 2; Check E-mailStep 4: meta# Digital ID,
VeriSign™ Class 1 Digital IDSM . for Netscape Communicator or Netscape Navigator
Step 1 of 4: Complete Enrollment Form
', Step , CompelteEnromllentFormStep3:PcikupDigitalIO
Step 2; Check E-mailStep 4: meta# Digital ID,
К
К
Contents of YourDigital
FillinallUsetheEnglishalphabetwithnoaccentedcharacters. ThisInformationis
includedinyourDigitalI andistothepublic.■
Contents of YourDigital
FillinallUsetheEnglishalphabetwithnoaccentedcharacters. ThisInformationis
includedinyourDigitalI andistothepublic.■
RretName:М«Гк*>ЯП« or middle IniMai allowed (example -- Jacn B)RretName:М«Гк*>ЯП« or middle IniMai allowed (example -- Jacn B)••••i (example i F-mM Al*1 ^ehample j<i (example i F-mM Al*1 ^ehample j<Dos)Dos)lCdoe@ veritign coir)lCdoe@ veritign coir)IIII
RretName:
М«Гк*>ЯП« or middle IniMai allowed (example -- Jacn B)
RretName:
М«Гк*>ЯП« or middle IniMai allowed (example -- Jacn B)
••
••
i (example i F-mM Al
*1 ^ehample j<
i (example i F-mM Al
*1 ^ehample j<
Dos)
Dos)
lCdoe@ veritign coir)
lCdoe@ veritign coir)
I
I
I
I
Рис. 9:16. Форма ввода данных для получения цифрового идентификатора
В этой форме нужно указать имя, организацию, страну и адрес электронной почты. После заполнения формы по указанному адресу электронной почты отсылается ключ или инструкции, как его получить. Таким образом, можно быть уверенным только в подлинности электронного адреса, потому что имя и название организации не проверяются и могут быть любыми. Этот уровень^аутентификации называется цифровой идентификацией 1-го класса, (Class 1 Digital ID). Существуют более строгие классы цифровой идентификации, например, согласно требованиям 3-го класса следует получить нотариально заверенный отчет о финансовом положении корпоративного клиента. У каждой подобной организации, которая занимается аутентификацией сообщений, есть свои собственные требования и процедуры. Таким образом, при получении аутентифицированного" сообщения нужно ясно представлять, что в нем на самом деле аутентифицировано.
Формат сертификата X. 509
Одним из самых популярных форматов для подписанных сертификатов является формат Х.509. Сертификаты такого типа широко используются многими компаниями, в том числе Verisign, Microsoft, Netscape, для подписания электронных писем, аутентификации программного кода и сертификации многих других видов данных. Стандарт Х.509 является частью набора рекомендаций Х.500, разработанных Консультативным комитетом по международной телефонной и телеграфной связи США (Consultative Committee for International Telephone and Telegraphy — CCITT). Простейший вариант сертификата Х.509 содержит следующие данные.
Версия формата сертификата.
Серийный номер сертификата.
Идентификатор алгоритма подписи (идентификатор алгоритма + параметры алгоритма, которые используются для подписания сертификата).
Имя лица, подписавшего сертификат.
Период действия (начальная и конечная даты).
Имя сертифицированного объекта.
Открытый ключ сертифицированного объекта (идентификатор алгоритма + параметры алгоритма + значение открытого ключа).
Подпись (хэш-код всех предыдущих полей, который закодирован с помощью закрытого ключа лица, подписавшего сертификат).
Таким образом, подписавшее сертификат лицо гарантирует, что у указанного объекта есть свой открытый ключ.
Специальные расширения базового формата Х.509 позволяют включать в сертификаты дополнительные данные. (Более подробную информацию по этой теме можно получить по адресу http: //ietf .cnri .reston.va.us/ids.by.wg/X.509 .html.) Ha Web- странице Питера Гутмана no адресу http: / / www. c s. auckland. ac. nz / -pgut 001/ pubs/x509guide.txt можно найти интересную и полезную информацию о многих недостатках формата Х.509, которые были подмечены пользователями.
Точное описание структуры сертификата Х.509 содержится в официальном документе ASN.l (abstract syntax notation #1). На рис. 9.17 показано определениеASN.l для третьей версии формата Х.509. Чтобы пользоваться данным форматом, совсем не обязательно использовать точный синтаксис формата Х.509. Основные правила шифрования (basic encodingrules — ВЕК) достаточно полно и четко описывают правила записи этой структуры в бинарном файле. Иначе говоря, BER-правила описывают способ шифрования целых чисел, символьных строк, битовых строк и конструкций SEQUENCE, CHOICEИOPTIONAL.
Кроме BER-правил существуют другие способы шифрования элементов. Например, исключительные правила шифрования (distinguished encodingrides— DER) позволяют избежать всех двусмысленных ситуаций. Более подробную информацию о BER-правилах можно найти в книге A Layman's Guide to a Subset of ASN.l, BER, and DER Бартона С. Калиски (Burton S. Kaliski, Jr.) no адресу http://www.rsasecurity.com/rsalabs/pkcs. Исходный текст программы для просмотра файлов, зашифрованных с помощью BER-правил, можно найти no aдpecyhttp://www.es.auckland.ac.nz/~pgut001/dumpasnl.c.
Создание сертификатов
Для создания и управления несколькими сертификатами в пакете JDK предусмотрена специальная программа keytool. Пока она может использоваться только в режиме командной строки, но в следующих выпусках JDK, вероятно, появятся более удобные версии этой программы. Ниже показано, как Алиса может, используя программу keytool, подписать документ и послать его Бобу, и как Боб может проверить, что сообщение было действительно подписано Алисой, а не каким-то самозванцем. В этой книге не описываются только некоторые параметры keytool, а более подробную информацию о данной программе можно узнать в документации пакета JDK.
Программа keytool управляет хранилищами ключей (key stores), т.е. базами данных сертификатов и закрытых ключей. Каждый элемент хранилища ключей имеет псевдоним (abas). Далее показано, как Алиса создает хранилище alice.store и генерирует пару ключей с псевдонимом alice.
keytool -genkey -keystore alice.store -alias alice
При создании или открытии хранилища предлагается ввести пароль. В этом примере следует ввести слово password. При работе с хранилищем ключей, предназначенных для более серьезных целей, необходимо очень тщательно подбирать пароли для защиты этих файлов, потому что они содержат закрытые ключи.
При генерировании ключей пользователю предлагается ввести следующие данные.
Enter keystore password: password
What is your first and last name?
[Unknown]: Alice Lee
What is the name of your organizational unit?
[Unknown]: Engineering Department
Which is the name of your organization?
[Unknown]: ACME Software
Which is the name of your City or Locality?
[Unknown]: Cupertino
What is the name of your State or Province?
[Unknown]: California
What is the two-letter country code for this unit?
[Unknown]: US
Is <CN=Alice Lee, OU=Engineering Department, O=ACME Software, L=Cupertino, ST=Cal.ifornia, C=US> correct?
[no]: Y
Более подробную информацию об ASN.1 можно найти в книге ASN. 1—Communication Between Heterogeneous Systems Оливье Дюбуасона (Olivier Dubuisson) [Academic Press 2000] no адресу htttp://www.oss.com/asnl/dubuisson.html или в книге ASN.1 Complete ДжонаЛармаута (John Larmouth) [Morgan Kaufmann Publishers 1999] noадресу http://www.nokalva.com/ asnl/larmouth.html.
В программе keytool для идентификации владельцев ключей и создателей сертификатов используются имена стандарта Х.500 с компонентами для указания общего имени (Common Name— CM), организационного подразделения (Organizational Unit— OU), организации (Organization— О), местонахождения (Location— L), штата (State — ST) и страны (Country — С).
[Certificate : : = SEQUENCE { tbsCertificateTBSCertificate, .
signatureAlgorithm Algorithmidentifier, signatureBIT STRING }
»
TBSCertificat:e ::=SEQU
versionCO]
serialNumber signature vaslsiuderity subject subjectPublicKeylnia issuerUniquelD . [1]
EENXCPELI{CIT Version DEFA.ULT.
CertificateSerialNumber,' .
Algorithm!dentifier,
Name,#
Validity,
Name, SubjectPublicKeylnfo,
IMPLICIT Uniquelderttifier OPTIONAL,
subjectUniquelD [2]
— If present, version must be v2 orv3
IMPLICIT Uniquelderttifier OPTIONAL, — If present, version must be v2 or v3.
extensions ,[3]
EXPLICITOPTIONAL
—Ifpresent, versionmustbev3
Version::= INTEGER {vl (0 ) , v2 (1) , v3(2) }
CertificateSerialNumber ::=INTEGER
Validity ::= SEQUENCE { notBefore
notAfter CertificateValidityDate
CertificateValidityDate ::= CHOICE { utcTime UTCTime,-'
generalTime GeneralizedTime }
::= BIT STRING
SubjectPublicKeylnfo::= SEQUENCE { algorithmHmldentifier,
subjectPublicKey BIT STRING V
Extensions ::= SEQUENCE OF Extension
Extension::= SEQUENCE { OBJECTIDENTIFIER, criticalBOOLEANDEFAULTFALSE, extnValue.OCTET STRING}
Рис.9.17.Определение ASN. 1 Для формата X.509v3
Далее нужно указать пароль или просто нажать клавишу <Enter>, если для пароля ключа предполагается использовать пароль хранилища ключей.
Предположим, Алиса хочет отдать копию своего открытого ключа Бобу. В таком случае ей необходимо экспортировать файл сертификата.
keytool -export -keystore alice.store -alias alice -file alice. cert
Теперь Алиса может выслать файл Бобу, который после получения сертификата выполнит следующую команду.
keytool -printcert -file alice. cert,’ •'
В результате ее выполнения будет получен такой результат.
Owner: CN=Alice Lee, OU=Engineering Department', O=ACME Software, L=Cupertino, ST=California, C=US
Issuer: CN=AliceLee, OU=Engineering Department, O=ACME Software, L=Cupertino,ST=California,C=US
Serial number: 38107867%
Valid from: Fri Oct 22 07:44:55 PDT 1999 until: Thu Jan 20 06:44:55PST2000
Certificate fingerprints:’
MDS: 5D:00:OF:95:01: 30:B4:FE:18:СЕ:9A:OF:C9:90:DD
SHA1:F8:C2:7C:E2:OB:IF:69:E2:6C:31: 9A: F6 : 3 5:FA:A3:4F:83:81:6A: 6A
Этотсертификатназывается самоподписанным (self-signed). Такимобразом, Бобмо- жет и не использовать другой сертификат для подтверждения истинности данного. Вместо этого он может позвонить Алисе и сверить присланный дактилоскопический отпечаток с исходным отпечатком.
Некоторые создатели сертификатов оставляют дактилоскопический отпечаток на своем сайте. Например, в пакете JDK в каталоге jre/lib/security есть хранилище ключей cacerts, в котором находятся сертификаты компаний Thawte и VeriSign. Для просмотра содержимого хранилища ключей используется следующая команда.
keytool -list -keystore jre/lib/security/cacerts
Данное хранилище имеет пароль change i t, а один из сертификатов хранилища имеет следующий вид.
thawtepremiumserverca, Fri Feb 12 12:1526 PST 1999, trustedCertEntry, Certificate fingerprint (MDS):•
06:9F:69:79:16:66:90:02:IB:8C:8C:A2:C3:07:6F:ЗА
Для подтверждения истинности сертификата можно посетить сайт компании Thawte (http: //tvww.thawte.com/certs/trustmap.html).
В такой проверке, конечно, предполагается, что код программы keytool (и JRE- код в файлеrt.j ar, который вызывается программой keytool) не был изменен и является подлинным. В случае необходимости можно скопировать данные файлы надругой компьютер и сверитьихсисходнымисвежимикопиямиэтихфайлов.
Если Брб доверяет этому сертификату, он может импортировать сертификат в свое хранилище ключей.
keytool -import -keystore bob.store -alias alice -file alice.cert
Ни в коем случае не импортируйте в хранилище ключей сертификат, которому вы не доверяете. После включения сертификата в хранилище каждая программа может использовать этот сертификат для проверки подписей.
Теперь Алиса может посылать подписанные документы Бобу. Для подписания и проверки JAR-файлов предусмотрена вспомогательная программа jarsigner. Сначала Алиса запаковывает нужный документ BjAR-файл.,
jar cvf document.jar document.txt
Затем с помощью программыjarsigner Алиса добавляет к файлу подпись, указывая при этом хранилище, JAR-файл и имя используемого ключа.
jarsigner -keystore alice.store document.jar alice
После получения файла Боб использует параметр -vrifyдля верификации этого файла.
jarsigner -verify -keystore bob.store document.jar
При этом Бобу не обязательно указывать псевдоним ключа. Программаjarsigner самостоятельно найдет соответствующий сертификат в хранилище ключей, используя имя владельца ключа в формате Х.509, которое указано в цифровой подписи.
Если JAR-файл подлинный, и подпись прошла верификацию, то будет получено следующее сообщение.
jar verified.
В противном случае будет получено сообщение об ошибке.
Подпись сертификатов
Итак, используя самоподписанный сертификат, можно распространять открытые ключи. Для использования такого способа получателю нужно сверить цифровой отпечаток присланного сертификата с отпечатком отправителя. Чаще всего для этого сертификат подписывается доверенным посредником.
В пакете SDK не предусмотрено вспомогательных программ для подписания сертификатов. Поэтому ниже описывается способ создания программы, которая подписывает сертификат с помощью закрытого ключа из хранилища ключей. Данная программа также демонстрирует способы организации доступа к содержимому сертификата и хранилища ключей.
Перед тем как взглянуть на код программы, рассмотрим принцип ее работы. Предположим, что Алиса хочет послать Синди подписанное сообщение. Синди для верификации цифрового отпечатка не хочет звонить всем отправителям полученных ею подписанных сообщений. Очевидно, что в этом случае нужно использовать такую промежуточную организацию, которой Синди доверяет верификацию подписей. Предположим, что такой организацией является отдел информационных ресурсов Informational Resources Department компании ACME Software. Для имитации данного процесса следует создать хранилище ключей acmesoft. store, затем генерировать ключ и экспортировать самоподписанный сертификат.
keytool -genkey -keystore acmesoft.store -alias acmeroot keytool -export -alias acmeroot -keystore acmesoft.store
—file acmeroot.cert
Далее нужно поместить сертификат вхранилище.
keytool -import -alias acmeroot -keystore cindy.store
—file acmeroot.cert
Синди теперь все еще необходимо верифицировать цифровой отпечаток корневого сертификата (root certificate), нопослеэтогоонаможетвполнеспокойноприниматьвсе сертификаты, подписанные корневым сертификатом.
Чтобы Алиса могла послать сообщение Синди, она должна передать ее сертификат в отдел Information Resources Department компании ACME Software и подписать его.
Программа keytool из пакета JDK такие функции не поддерживает. Поэтому в листинге 9.14 показан код программы, которая пописывает сертификат. Данная программа считывает файл сертификата и подписывает его с помощью закрытого ключа из хранилища ключей. Уполномоченный сотрудник компании ACME Software подтверждает личность Алисы и создает подписанный сертификат следующего типа.
java Certificatesigner -keystore acmesoft.store -alias acmeroot
—infile alice.cert -outfile alice_signedby_acmeroot.cert
Программа, которая подписывает сертификат, должна иметь доступ к хранилищу ключей компании ACME Software, а уполномоченный работник должен знать пароль хранилища ключей. Очевидно, что это довольно щепетильная операция в отношении гарантий обеспечения безопасности.
Теперь Алиса может передать файл alice_signedby_acmeroot.root Синди и кому-нибудь из компании ACME Software. Иначе, этот файл также можно просто сохранить в каталоге компании. Напомним, что этот файл содержит открытый ключ Алисы и утверждение компании ACME Software о том, что ключ действительно принадлежит Алисе.
Программаkeytoolподдерживает разные механизмыподписания ключей. Параметр -certreq позволяет получить запрос-сертификат в стандартном формате, который может быть обработан такими компаниями, как Thawte и Verisign, или локально с помощью специализированного программного обеспечения, например, сервера сертификатов Netscape.
Когда Синди импортирует подписанный сертификат в свое хранилище ключей, в этом хранилище выполняется верификация того, что данный ключ был подписан доверенным корневым ключом, находящимся в хранилище, и Синди не нужно проверять цифровой отпечаток этого сертификата.
Рассмотренный сценарий показан только в демонстрационных целях. На самом деле keytool вряд ли будет полезен для конечных пользователей. Дело в том, что данная программа принимает только те сертификаты, которые подписаны доверенной организацией, предлагающей пользователю подтвердитьдостоверность тех сертификатов, которые не может верифицировать. При этом она никогда не отвергает никакие сертификаты, что может запутать пользователя, который может ошибочно одобрить использование подложного сертификата.
Как только Синди включит корневой сертификат и сертификаты тех отправителей, которые регулярно присылают ей какие-то документы, в хранилище ключей, она сможет больше не волноваться о хранилище ключей.
Рассмотрим теперь исходный код программы (листинг 9.14). Сначала загружается хранилище ключей. Для этого метод getlnstance класса KeyStore создает хранилище ключей соответствующего типа. Хранилище, созданное программой keytool, имеет тип JKC, поставщик которого имеет тип SUN.
KeyStore store = KeyStore.getlnstancefJKC", "SUN");
Теперь нужно загрузить данные хранилища. Метод load запрашивает входной поток и пароль. Учтите, что пароль задается как массив char [ ], а не строка. Дело в том, что виртуальная машина Java JVM может сохранять строки довольно долгое время, т.е. до тех
пор, пока они не будут удалены во время "сборки мусора". Поэтому хакеры могут спокойно их найти, например, просматривая содержимое файлов подкачки. Массивы символьного типа могут быть очищены сразу же после их использования.
Ниже показан код для загрузки хранилища. Следует заметить, что пароль после использования заполняется пробелами.
InputStream in =
char[] password = ... ;
store.load(in, password);
Arrays.fill(password, 1 '); in.close() ;
Далее с помощью метода getKey извлекается закрытый ключ для подписания сертификата на основе указанного псевдонима ключа и его пароля. Возвращаемое значение имеет тип Key и его нужно привести к типу PrivateKey.
= ...;
PrivateKey issuerPrivateKey
=keyPassword);
Arrays.fill(keyPassword, 1 ' );*
Теперь можно считать сертификат, который необходимо подписать. Класс Certi icateFactory может считывать сертификат из входного потока. Для этого прежде нужно получить объект-фабрику соответствующего типа.
CertificateFactory factory =
Далее нужно вызвать метод generateCertificate с указанием входного потока.
in = new FilelnputStream(innarne);.
X509Certificate inCert
=;
in.close();*
Метод generateCertificate возвращает значение абстрактного типа Certificate , который является суперклассом нескольких конкретных классов, например X509Certificate. Так как входной файл, имеет тип X509Certif icate, нужно выполнить соответствующее приведение типа.
Учтите, что существует два класса с именем Certificate: один из них представляет собой устаревший и не рекомендуемый к использованию интерфейс в пакете java.security, а другой является абстрактным классом в пакете java.security.cert.Если в программу импортируются оба пакета, то нужно учесть эту особенность и во избежание двусмысленности явно указать имя класса java.security.cert.Certificate.
Цель данной программы заключается в подписании байтов сертификата, которые извлекаются с помощью метода getTBSCertificate.
byte[] inCertBytes =
Теперь нужно вставить имя отправителя в подписанный сертификат. Это имя находится в сертификате отправителя, который хранится в хранилище ключей. Для извлечения сертификата предусмотрен метод getCerti icate. Так как сертификат
представляет собой открытую информацию, то для его извлечения не нужно указывать пароль, а достаточно просто воспользоваться псевдонимом.
X509Certificate issuerCert =
Метод getCertif icate возвращает значение абстрактного типа Certificate, которое, как известно, следует привести к типу X509Certificate. С помощью метода getSubjectDNможно получить объект-идентификатор принципала, который расширяет интерфейс Principal. Принципалом (principal) может быть любой субъект, выдающий сертификат, например, физическое лицо, общественная организация или коммерческаякомпания.
Principal issuer = issuerCert.getSubjectDN();
Затем нужно получить имя алгоритма подписи.
String issuerSigAlg = issuerCert.getSigAlgName()
Теперь для выполнения следующих действий придется использовать библиотеки создания сертификатов сторонних разработчиков, например, компании RSA Inc. и компании Legion of Bouncy Castle (http: / /www. bouncycastle. org). Дело в том, что стандартная библиотека Java не содержит методов генерации новых сертификатов. Тем не менее, в данном примере описано использование классов пакета sun. security ,x509. Этот способ характеризуется своими собственными недостатками. Например, данный пакет может не поддерживаться сторонними поставщиками, либо компания Sun Microsystems может в любоб время изменить способ его применения.
Приведенный ниже код программы выполняет следующие действия.
Создает объект с информацией о сертификате на основе байтов подписанного сертификата.
Задает имя создателя.
Создает сертификат и пописывает его с помощью закрытого ключа создателя.
Сохраняет подписанный сертификат в файле.
X509CertInfo info = new X509CertInfo(inCertBytes);
info.set(X509CertInfo.ISSUER,
new CertificatelssuerName((X500Name)issuer));
X509CertImpl outCert = new X509CertImpl(info);
outCert.sign(issuerPrivateKey, issuerSigAlg);
outCert.derEncode(out);
Здесь опущены все подробности использования этих классов, потому что авторы не рекомендуют использовать библиотеки компании Sun для создания данного кода. Вполне возможно, что будущие версии пакета SDK будут содержать классы создания сертификатов. Пока можно использовать библиотеки сторонних поставщиков или использовать уже существующее программное обеспечение для создания сертификатов.
Листинг 9.14.CertiflcataSigner.java
/**
©version 1.00 1999-10-23
©author Cay Horstmann
/
import j ava.io.*;
import java.security.*;
import java.security.cert.*;
import java.util.*;
import sun.security.x509.X509CertInfo;
import sun.security.x509.X509CertImpl;
import sun.security.x509.X50OName;
import sun.security.x509.CertificatelssuerName;
/“
Пример программы для подписания сертификата,
в которой используется закрытый ключ другого сертификата из хранилища ключей
*/
public class Certificatesigner
{
public static void main(String[] args)
{
String ksname = null;
String alias = null;
String inname = null;
String outname = null;
for (int i = 0; i < args.length; i += 2)
{
if (args[i].equals("-keystore")) ksname = args[i + 1] ;
else if (args[i].equals("-alias”))
alias = args[i + 1] ;
else if (args[i].equals("-infile"))
inname = args[i + 1] ;
else if (args[i].equals("-outfile")) outname = args[i + 1] ;
else usage();
}
if (ksname == null || alias == null ||
inname == null || outname == null) usage();
PushbackReader console = new PushbackReader(new InputStreamReader(System.in)) ;
Keystore store = KeyStore. getlnstance (" JKS11, "SUN");
Inputstreamin=newFilelnputStream(ksname);
System.out.print("Keystopassword:") ;
System.out.flush() ;
char[] password = readpassword(console) ;
store.load(in,password);
Arrays.fill(password, ' ');
in.close();
System, out .print("Key password for "+ alias + ": ") ,-
System.out.flush() ;
char[] keyPassword = readPassword(console) ;
PrivateKey issuerPrivateKey
=keyPassword);
Arrays.fill(keyPassword, ' ') ;
if (issuerPrivateKey == null) error("No such private key");
in = new Fileinputstream(inname);
CertificateFactory factory =
X509Certificate inCert = in.closed ;
byte[] inCertBytes = inCert.getTBSCertificate();
X509Certificate issuerCert =
Principal issuer = issuerCert.getSubjectDN();
String issuerSigAlg = issuerCert.getSigAlgName() ;
FileOutputStream out = new FileOutputStream(outname);
X509CertInfo info = new X509CertInfo(inCertBytes); info.set(X509CertInfo.ISSUER, new CertificatelssuerName((X500Name)issuer));
X509CertImpl outCert = new X509CertImpl(info); outCert.sign(issuerPrivateKey, issuerSigAlg); outCert.derEncode(out);
out.close();
} catch (Exception exception) {
exception.printStackTrace();
/**
Считывает пароль
©param in the reader from which to read the password
©return an array of characters containing the password
*/
public static char[] readPassword(PushbackReader in)
throws IOException
{
final int MAX_PASSWORD_LENGTH = 100;
int length = 0;
chart] buffer = new char[MAX_PASSWORD_LENGTH];
while (true)
{
int ch =
if (ch == ' \r' || ch == ' \n' || ch == -1
|| length ==MAX_PASSWORD_LENGTH)
{
if (ch == '\r') // handle DOS "\r\n" line ends
{
ch = in.read();
if (ch != ' \n' && ch != -1) in.unread(ch);
}
chart] password = new char[length];
System.arraycopy(buffer, 0, password, 0, length)
Arrays.fill(buffer, 1 ');
return password;
}
else
{
buffer[length] = (char)ch;
length++;
}
}
}
/**
Выводит сообщение об ошибке и завершает работу программы @param message
*/
public static void error(String message)
{
System.out.printin(message);
System.exit(1) ;
/**
Выводит сообщение о результате выполнения программы и завершает работу программы
*/
public static void usage() {
System.out.printin("Usage: java CertificateSigner" + " —keystore keyStore —alias issuerKeyAlias" + " -infile inputFile -outfile outputFile");
;
}
java.security.Keystore
Метод static getlnstance (String type) создает хранилище ключей указанного типа с заданным по умолчанию поставщиком.
Метод static getlnstance(String type, String provider) создает хранилище ключей указанного типа с указанным поставщиком. При использовании вспомогательной программы keytool следует указать тип JKCи поставщика SUN.
Метод void load(InputStream in, char[] password) загружает хранилище ключей из потока. Пароль содержится в символьном массиве, а потому не входит в состав строкового спула виртуальной машиныJava.
Метод Key getKey(String alias, char[] password) возвращает закрытый ключ с указанным псевдонимом, который находится в этом хранилище.
Метод Certificate getCertificate(String alias) возвращает сертификат для открытого ключа с указанным псевдонимом, который находится в этом хранилище.
java.security.cert.CertificateFactory
type) создаетtype) создаетМетод CertificateFactory getlnstance(String фабрикусертификатовуказанноготипа,например,Х509.
type) создает
type) создает
Метод Certif icate generateCertif icate(InputStream in) загружает сертификат из входного потока.
java.security.cert.Certificate
Метод PublicKey getPublicKey О возвращает открытый ключ который подтвержден этим сертификатом.
Метод byte[] getEncoded() возвращает зашифрованную форму сер-
тификата.
Метод String getType О возвращает тип сертификата, например, Х509.
java.security.cert.X509Cert i ficate
Метод Principal getSubjectDN() возвращает имя владельца (или субъекта) сертификата.
Метод Principal getlssuerDN() возвращаетимя создателя сертификата.
Метод Date getNotBef ore () возвращает начальную дату действия сертификата.
Метод Date getNotAfter () возвращает конечную дату действия сертификата.•
Метод Getlnteger getSerialNumber () возвращает серийный номер сертификата.
Метод String getSigAlgName () возвращает имя алгоритма подписи сертификата.
Метод byte getSignature () возвращает владельца подписи сертификата.
Метод byte[] getTBSCertificate () возвращает зашифрованную с помощью DER-правил информацию сертификата, которая должна быть подписана создателем сертификата.
Подписание кода
Одна из самых важных технологий аутентификации заключается в подписании выполняемых программ. Копируя программу из сети, пользователь вполне обоснованно может потребовать гарантий ее подлинности, потому что такая программа может нанести вред, если она заражена каким-то вирусом. Поэтому необходимо быть уверенным в том, что программа выслана надежным источником и во время пересылки не была изменена. Если такая программа создана на языке программирования Java, то, обладая такой информацией, можно принять решение о делегировании ей соответствующих полномочий. Программу можно запустить в "песочнице" ("sandbox") как простой аплет или для нее можно определить какие-то дополнительные права и ограничения. Например, если скопированная из сети программа предназначена для редактирования текста, то ей можно предоставить доступ к принтеру и к файлам в определенном каталоге. В то же время этой программе можно запретить доступ к сетевым соединениям, чтобы она не могла без разрешения рассылать ваши файлы третьим лицам.
Для реализации такой сложной схемы нужно выполнить следующие действия.
С помощью аутентификации проверить источник поступления кода.
Запустить программу с соответствующей политикой обеспечения безопасности, которая определяет права доступа программы, зависящие от источника поступления.
Подписание JAR-файлов
В настоящее время процесс подписания кода зависит от используемой платформы. Если аплеты пакета JDK 1.1 используются в Web-броузере Internet Explorer или Netscape Navigator, то для подписания аплета следует применить специальный инструмент компании Netscape или Microsoft соответственно. Более подробную информацию по этой теме можно получить по адресу www.securing-java.com/appdx-c/ или по адресу www. suitable. com/Doc_CodeSigning.shtml.
В данном разделе описывается способ подписания аплета с помощью подключаемого модуля Java Plug-In для следующих сценариев.
Подписание аплетов в корпоративной intranet-сети.
Подписание аплетов в глобальной сети Internet.
В первом сценарии системный администратор инсталлирует сертификаты и файлы политики на локальных компьютерах. При этом во время загрузки модулем Java Plug-in каждого подписанного аплета с помощью хранилища ключей выполняется проверка подписей и с помощью файлов политики обеспечения безопасности — проверка прав доступа. Установка сертификатов и политик выполняется очень просто и однократно на каждом настольном компьютере. После чего любой пользователь сети может запустить подписанный корпоративный аплет, не используя "песочницы". Теперь каждый новый или измененный старый аплет нужно подписать и доставить на Web-сервер. Тем не менее, этот процесс никак не влияет на работу настольного компьютера. Поэтому, данный сценарий является привлекательной альтернативой для доставки корпоративных приложений на компьютеры корпоративной сети.
Во втором сценарии поставщики программного обеспечения получают сертификаты, подписанные компаниями, выпустившими сертификат, например, Thawte Verisign. Если пользователь посещает Web-страницу, на которой размещен подписанный аплет, то на экране появится диалоговое окно, которое идентифицирует поставщика программы и предоставляет пользователю два варианта продолжения работы: загрузить аплет со всеми имеющимися правами доступа или ограничить его работу "песочницей". Более детально данный сценарий описывается ниже.
Остальная часть этого раздела посвящена описанию способов создания файлов политики обеспечения безопасности, которые предоставляют определенные права доступа для аплетов, поступивших из известных источников. Создание и доставку файлов политики обеспечения безопасности должен выполнять не какой-либо обычный конечный пользователь, а, например, системный администратор. Эти операции можно рассматривать как подготовку к распространению аплетов по корпоративной сети.
Допустим, что компания ACME Software разрешает пользователям запускать определенные аплеты, которым требуется предоставить доступ к локальным файлам. Данная программа не может выполняться в "песочнице", поэтому компании ACME Soft-
ware нужно установить файлы политики на компьютеры сотрудников. ACME может идентифицировать аплет. Это означает, что компании ACME Software нужно было бы изменять файлы политики каждый раз, когда код аплета перемещается на другой сервер. Вместо этого в компании ACME Software принято решение подписывать JAR- файлы, в которых находится код аплета.
Чтобы создать подписанный J AR-файл, нужно сначала упаковать файлы классов в JAR-файл.
jar cvf MyApplet.jar *.class
Потом необходимо запустить вспомогательную программу jarsigner, указывая при этом JAR-файл и псевдоним закрытого ключа.
jarsigner -keystore acmesoft.store MyApplet.jar acmeroot
В данном примере JAR-файл подписан самоподписанным корневым ключом. На практике это выглядит так: компания ACME раздает своим программистам пользовательские ключи, которые подписаны ими с помощью корневого ключа.
Хранилище с корневым ключом обязательно должно находиться в надежном месте. Теперь нужно создать второе хранилище ключей certs . store для сертификатов и добавить в него сертификат acmeroot.
keytool -export -keystore acmesoft.store -alias acmeroot -file
acmeroot.cert
keytool -import -keystore certs.store -alias acmeroot -file acmeroot.cert
Далее следует создать файлы политики, которые определяют права доступа для аплетов, подписанных подписями из данного хранилища.
Для чего в файле обеспечения безопасности необходимо указать место расположения хранилища ключей. С этой целью нужно добавить следующую строку в начале файла политики обеспечения безопасности.
keystore " ЦКЬ_указатель_хранилища", " тип_хранилища" ;
Если хранилище ключей создано программой keytool, то для типа хранилища нужно указатьJKC.
keystore "file:certs.store", "JKC";
Затем в файле политике следует добавить signedBy "имя" для одного или нескольких предложений grant.
grant signedBy "acmeroot11
{ permission java. io.FilePermission "«ALL FILES» " , "read";
};
Теперь любой подписанный код, который может быть заверен открытым ключом с данным псевдонимом, имеет указанные права доступа.
В листинге 9.15 приводится код аплета, который следует использовать для проверки описанного выше материала. Данный аплет пытается прочитать локальный файл, как показано на рис. 9.18. Используемый по умолчанию файл политики разрешает аплету считывать файлы из каталога его базы кода и любого его подкаталога. Запустите этот аплет с по-
мощью программы просмотра аплетов appletviewer и убедитесь в том, что с его помощью можно просматривать файлы только из каталога базы кода (и любого его подкаталога) и нельзя просматривать файлы, которые находятся в других каталогах.
Рис. 9.18. Программа FileReadApplet
Далее нужно упаковать этот аплет в J AR-файл и подписать его ключом acmeroot. После чего необходимо создать файл политики applets.policy с таким содержимым.
keystore ”file:certs.store", "JKC";
grant signedBy "acmeroot"
С
} permission java. io . filePermission "«ALL FILES»", "read";
Для создания файла политики можно также использовать вспомогательную программу policytool.
D
Для успешного проведения теста необходимо, чтобы хранилище ключей, JAR- файл и файл политики находились в одном каталоге.
Затем нужно указать программе просмотра аплетов используемый файл политики.
appletviewer -J-Djava.security,policy=applets.policy
FileReadApplet.html
Теперь этот аплет может считывать все файлы.
Итак, описанный выше механизм подписания корректно работает, и для его доставки конечным пользователям нужно найти соответствующие места расположения файлов политики. Более подробно этот вопрос рассматривается в следующем разделе.
В заключение можно еще протестировать работу этого аплета внутри подключаемого модуля Java Plug-In, для чего нужно изменить файл java.security в каталоге jre/lib/security. В этом файле нужно найти следующий сегмент.
# The default is to have a single system wide policy file, # and a policy file in the user's home directory.
policy.url.l=file:${java.home}/lib/security/java.policy
policy.url.2=file:$(user.home}/.java.policy
Добавьте к нему строку с URL-указателем файла политики.
policy.url.3=file:///home/test/applet.policy
Следует очень осторожно редактировать корректно работающий файл java.security. Дело в том, что подключаемый модуль Java Plug-In использует виртуальную машину не из пакета JDK. В частности, в операционной системе Windows используются JRE-файлы ИЗ каталога \Program Files\JavaSoft.
В зависимости от используемого Web-броузера (Netscape 4 или Internet Explorer) для загрузки подключаемого модуля Java Plug-In в разных программах в коде HTML- страницы придется использовать разные тэги (EMBED или OBJECT). А в Web- броузерах Opera и Netscape версии 6 при обнаружении тега APPLET подключаемый модуль Java Plug-In загружается автоматически. (Более подробно способы загрузки подключаемого модуля Java Plug-In описываются в главе 10 тома 1.)
Если для просмотра аплета вместо appletviewer используется подключаемый модуль Java Plug-In, то следует учесть, что подключаемый модуль считывает файл политики только один раз во время первой загрузки данного аплета. Если в файле политики или в хранилище ключей допущена ошибка, то нужно закрыть Web- броузер, отредактировать файл политик и заново загрузить программу. Обычная перезагрузка Web-страницы с этим аплетом не изменит ситуацию.
Листинг 9.15. FileReadApplet.java
/** @version 1.00 1999-10-23 @author Cay Horstmann
*/
import j ava.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import j avax.swing. *;.
/**
Аплет, который может работать за пределами "песочницы" и считывать локальные файлы при наличии соответствующих прав доступа
*/
public class FileReadApplet extends JApplet {
public FileReadApplet() {
fileNameField = new JTextField(20);
JPanel panel = new JPanel() ;
panel.add(new JLabel("File name:"));
pane1.add(f i1eNameFi e1d} ,•
JButton openButton = new JButton("Open");
panel.add(openButton) ;
openButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
loadFile(fileNameField.getText() ); }
}) ;
Container contentPane = getContentPane();
contentPane.add(panel, "North");
fileText = new JTextArea();
contentPane.add (new JScrollPane(fileText) , "Center");
}
/**
Загружает содержимое файла в текстовую область.
@param filename the file name
*/
public void loadFile(String filename) {
try
{ fileText.setText(""); BufferedReader in
= new BufferedReader(new FileReader(filename)) ;
String s;
while ((s = in.readLine()) != null) fileText.append(s+ "\n");
in.close();
}
catch (IOException e)
{ fileText.append(e+ "\n");
}
catch (SecurityException e)
{ fileText.append("I am sorry, but I cannot do that. ");
}
private JTextField fileNameField;
private JTextArea fileText;
}
Советыno доставке
Прежде всего нужно выбрать место расположения файлов политики. По умолчанию предлагаются следующие два места расположения.
java.policy в каталоге ${java.home}/lib/security.
java.policy в каталоге $ {user.home}.
Это не совсем удачные места, и авторы рекомендуют отредактировать файл java. security в каталоге $ {j ava.home} /jre/lib/security, включая в него строчку с указанием третьего файла политики.
policy.url.3=http://intranet.acmesoft.com/admin/applet.policy
Таким образом можно установить файл политики на сервере корпоративной сети, что очень удобно для управления этим файлом, так как теперь он находится в одном постоянном месте.
Аналогично в файле политики можно привести URL-указатель местонахождения хранилища ключей.
keystore "http://inranet.acmesoft.com/admin/certs.store”, "JKC;
Затем в этом в хранилище ключей нужно разместить сертификат открытого ключа для корпоративных приложений.
Данный сертификат можно также включить в хранилище ключей cacerts. Но тогда при изменении хранилища ключей нужно будет обновлять JRE на каждом компьютере, поэтому файл cacerts не рекомендуется изменять.
Далее нужно подписать JAR-файл с корпоративным приложением соответствующим закрытым ключом.
Не рекомендуется предоставлять все права доступа AllPermission корпоративным аплетам. Будет лучше дополнить имеющиеся у них права доступа в "песочницу" другими правами, например, предоставить доступ ко всем каталогам внутри отдельного подкаталога, к печати, к корпоративной сети и т.д.
Задача доставки аплетов может показаться очень сложной, но эту проблему можно довольно быстро решить, для чего нужно в минимальной степени настроить конфигурацию Java Plug-In на каждом отдельном компьютере, а все остальные характеристики можно настроить в удаленном режиме. Такой подход обладает двумя преимущс- ствами. Во-первых, пользователям не будет предоставлена возможность самостоятельно решать вопросы обеспечения безопасности. Во-вторых, этот способ гарантирует детальный контроль над предоставлением прав доступа для удаленного кода, который загружается конечными пользователями.
Система обеспечения безопасности Java объединяет аутентификацию и управление правами доступа, поэтому она предлагает более высокую степень обеспечения безопасности, чем технология Microsoft ActiveX. Дело в том, что код ActiveX аутентифицируется, но после получения разрешения на запуск его нельзя будет контролировать.
Сертификаты разработчиков программ
До сих пор описывались сценарии рассылки аплетов в корпоративной сети, где права доступа аплетов контролирует системный администратор. Тем не менее, такая стратегия работает только с аплетами из известных источников.
Предположим, что, работая в Internet, пользователь открывает Web-страницу, в которой предлагается запустить аплет неизвестного поставщика, как, например, показано на рис. 9.19. Такой аплет подписан сертификатом разработчика программного обеспечения (software developer), которыйв свою очередь создан компанией Verisign. В этом диалоговом окне указывается разработчик данной программы и создатель сертификата. Таким образом, у пользователя есть два варианта продолжения работы.
Запустить аплет с полными правами доступа.
Ограничить работу аплета "песочницей".
Рис.9.19. Запуск подписанного аплета
Итак, какой информацией обладает пользователь в такой ситуации и что повлияет на его решение?
Компания Thawte продала сертификат разработчику программного обеспечения.
Аплет действительно подписан этим сертификатом и во время пересылки не был изменен.
СертификатдействительноподписанкомпаниейThawte,онбылпроверенс
использованием открытого ключа, который находится в локальном файле cacerts.
Значит ли это, что данный код надежен? Следует ли доверять поставщику, если известно только его имя (* . netbizz . dk) и тот факт, что компания Thawte продала ему сертификат разработчика программного обеспечения? Вероятно, компания Thawte вряд ли сможет гарантировать, что под именем разработчика программного обеспечения * . netbizz. dk не скрывается какой-то злобный хакер.
Более того, создатели сертификатов не претендуют на это, т.е. они не занимаются проверкой честности и компетентности поставщиков компьютерного обеспечения.
В ситуации, когда имя поставщика данного программного обеспечения неизвестно пользователю, нужно тщательно взвесить все аргументы "за" и "против", прежде чем предоставлять данному аплету все права доступа. А если имя поставщика данного программного обеспечения хорошо известно пользователю, то, принимая такое решение, он может учесть репутацию компании.
Вряд ли стоит доверять программе, которая заявляет "дай мне все права, или я вообще не будут работать". Неопытные пользователи часто совершают ошибку и необдуманно предоставляют все права такой программе, что зачастую приводит к возникновению очень опасных ситуаций. Стоит ли использовать аплеты, которые подробно объясняют свои намерения и перечисляют все необходимые им права? К сожалению, в таком случае конечному пользователю придется погрузиться в чисто технические подробности работы данной программы. Вряд ли обычный пользователь сможет разобраться, в каких ситуациях следует предоставить аплету права доступа к очереди событий AWT. Возможно, в будущие версии подключаемого модуля будут включены более понятные определения наборов прав доступа. Однако, в данный момент использование аплетов, подписанных сертификатами программных разработчиков, представляется крайне сомнительным.
Для распространения аплетов в глобальном масштабе при условии, что их пользователи доверяют вам в той мере, чтобы предоставить аплету все права доступа для его выполнения на своих компьютерах, необходимо приобрести сертификат разработчика у компании Thawte или Verisign и подписать распространяемые аплеты. Более подробную информацию по этой теме можно найти по адресу http://java.sun.com/products/plugin/1.2/docs/nsobjsigning.html.
Шифрование
До сих пор рассматривались только те криптографические методы обеспечения безопасности на платформе Java, которые основаны на аутентификации с помощью цифровых подписей. Еще одна важная технология криптографии основана на шифровании (encryption). После аутентификации информации она становится доступной для просмотра всем пользователям, а цифровая подпись просто подтверждает, что эта информация не была изменена. Для просмотра зашифрованных данных этого недостаточно и их нужно расшифровать с помощью соответствующего ключа.
Для подписания кода достаточно использовать механизм аутентификации, и шифрование в этом случае не нужно. Однако, часто бывает так, что аплет или приложение содержат конфиденциальную информацию, например, номера кредитных карточек или другие личные данные, и именно в таких случаях необходимо использовать технологии шифрования.
До недавнего времени во многих компаниях, включая Sun, существовали патентные и экспортные ограничения на использование мощных алгоритмов шифрования. К счастью, на сегодняшний день все эти ограничения утратили силу, а срок патентного ограничения на использование алгоритма RSA вообще истек в октябре 2000 года. В стандартной библиотеке пакета SDK 1.4 предусмотрены достаточно мощные средства шифрования. Кроме того, для более старых версий SDK предусмотрено отдельное расширениеJCE со средствами шифрования.
Симметричные шифры
Криптографическое расширение Java содержит класс Cipher, который является суперклассом всех алгоритмов шифрования. Для создания объекта-алгоритма шифрования используется метод getlnstance.
Cipher cipher = Chiper.getlnstance(algorithName);
Cipher cipher = Chiper.getlnstance(algorithName, providerName);
В SDK для всех шифров используется провайдер SunJCE, и если имя провайдера не указано явно, то оно используется по умолчанию. В следующем разделе подробно описываются способы создания и указания другого провайдера.
Имя алгоритма задается в виде строки, например, DES или DES/CBC/PKCS5Padding.
Стандарт шифрования данных DES (Data Encryption Standard) представляет собой очень старый алгоритм шифрования с длиной ключа в 56 бит. В настоящее время алгоритм шифрования DES не используется потому, что найдена простая возможность его взлома (подробнее об этом можно узнать по адресу http://csrc.nist.gov/ encryption/aes/). Намного лучше для шифрования данных использовать его наследника, т.е. расширенный алгоритм шифрования AES (Advanced Encryption Standard), более
подробное описание которого можно найти по адресу http://csrc.nist.gov/ biencryption/aes/. Во время создания книги в расширении JCE компании Sun поддерживался алгоритм DES и не поддерживался алгоритм AES.
После создания объекта-алгоритма шифрования cipher нужно инициализировать его параметрами mode и key.
int mode = . . . ;
Key key = . . . ;
cipher.init(mode, key);
Параметр mode означает один из следующих режимов шифрования.
Cipher.ENCRYPT_MODE
Cipher.DECRYPT_MODE
Cipher. WRAP_MODE
Cipher.UNWRAP_MODE
Режимы свертки {wrap) и развертки {unwrap) применяются для шифрования одного ключа на основе другого. Пример использования таких режимов приводится в следующем разделе.
Для шифрования нескольких блоков данных нужно повторно вызывать метод update так, как показано ниже.
int blocksize =
byte[] inBytesnew byte[blocksize];
.. . // Считывание блока байтов inBytes int outputsize =
byte[] outBytes = new byte[outputsize];
int outLength = cipher.update(inBytes, 0, outputSize, outBytes);
.. . // Запись блока байтов outBytes
Наконец, в завершение нужно вызвать метод doFinal. Для блока данных с количеством байтов меньшим, чем размер блока blockSize, нужно использовать следующий ВЫЗОВ.
outBytes = cipher.doFinal(inBytes, 0, inLength);
Для завершения шифрования всех вводимых данных следует использовать другой вызов.
outBytes = cipher.doFinal();
Вызов метода doFinal необходим для дополнения {padding) пустых мест в блоке данных. Например, в алгоритме шифрования DES блок имеет размер 8 байт. Предположим, что размер входных данных меньше, чем 8 байт. Если заполнить остальные байты нулями и зашифровать блок, то при расшифровке блок будет иметь совершенно другие данные. Поэтому в таких случаях используется специальная схема дополнения пустых мест {padding scheme). Одна из таких схем описана в стандарте криптографии с открытым ключом Public Key Cryptography Standard (PKCS) #5, созданном специалистами RSA Security Inc. (более подробное описание можно найти по адресу http://www.rsasecurity.com/rsalabs/pkcs/pkcs-5). В этой схеме пустые места заполняются не нулями, а числом, которое равно длине пустоты в байтах. Иначе говоря, если L — это не до конца заполненный блок, то оставшаяся незаполненная часть заполняется таким образом:
L 01
L 02 02
if length(L) if length(L) if length(L)
= 7
= 6
= 5
L
03 03
03
L
07 07
07 07 07 07 07
if length(L)
= 1
И в заключение, если длина зашифрованных входных данных делится без остатка на 8, то в конце добавляется только один блок, который показан ниже.
08 08 08 08 08 08 08 08
При расшифровке для удаления пустых мест учитывается только один последний байт, который сообщает о количестве пустых мест.
Для создания абсолютно случайного ключа нужно выполнить следующие три действия.
Создать объект типа KeyGenerator.
Инициализировать генератор случайных числе. Если длина блока шифра является переменной, то она должна быть указана.
Вызвать метод generateKey.
Например, код генерации ключа в соответствии с алгоритмом шифрования DES будет выглядеть следующим образом.
KeyGenerator keygen =
SecureRandom random = new SecureRandom();
keygen.init(random);
Key key = keygen.generateKey();
Иначе, ключ можно также создать на основе фиксированного множества исходных данных (например, на основе пароля). В таком случае используется класс SecretKeyFactory.
SecretKeyFactory keyFactory =
byte[] keyData =// 8 байт для алгоритма DES.
SecretKeySpec keySpec = new SecretKeySpec(keyData, "DES");
Key key = keyFactory.generateSecret(keySpec) ;
Простая программа, код которой приводится в листинге 9.16, иллюстрирует применение алгоритма шифрования DES. Для ее использования нужно генерировать секретный КЛЮЧ.
java DEStest -genkey secret.key
Таким образом, секретный ключ будет сохранен в файле secret. key.
Далее, для шифрования текстового файла plaintextFile используется следующая команда.
java DEStest -encrypt plaintextFile encryptedFile secret.key
Для расшифровки зашифрованного файла encryptedFile нужно применить другую команду.
java DEStest -decrypt encryptedFile decryptedFile secret.key
Код этой программы имеет очень простую структуру. Сначала с помощью параметра - genkey создается новый секретный ключ и сохраняется в соответствующем файле. Эта операция обычно отнимает много времени, потому много времени расходуется на ини-
цианизацию генератора случайных чисел. Параметры -encrypt и -decrypt используются методом crypt, который, в свою очередь, вызывает методы объекта шифрования update и doFinal. Учтите, что метод update повторно вызывается до тех пор, пока поступают полноразмерные блоки входных данных, а метод doFinal только для частично заполненного блока (для генерации дополнительного байта).
Листинг 9.16.DESTeet.java
import j ava.io.*;
import java.security.* ;
import javax.crypto.* ;
import j avax.crypto.spec.*;
/**
Пример использования алгоритма шифрования DES
java DESTest —genkey keyfile
java DESTest —encrypt plaintext encrypted keyfile
java DESTest —decrypt encrypted decrypted keyfile
*/
public class DESTest
{
public static void main(String[] args)
{
try • {
if (args[ 0 ] .equals("-genkey"))
{ KeyGenerator keygen
=
SecureRandom random = new SecureRandom(); keygen.init(random);
SecretKey key = keygen.generateKey(); .
ObjectOutputstream out = new ObjectOutputstream { new FileOutputStream(args[1]));
out.writeobject(key);
out.close() ;
)
else {
int mode;
if (args[0].equals("-encrypt")) mode = Cipher.ENCRYPT_MODE;
else
mode = Cipher.DECRYPT_MODE;
Objectinputstream keyin = new Objectinputstream( new FilelnputStreamfargs[3J)) ;
Key key = (Key) keyin.readobject();
keyin.close();
InputStream in = new FileInputStream(args[1]);
Outputstream out = new FileOutputStream(args[2]) ; Cipher cipher = Cipher.getlnstance("DES”) ;
cipher.init(mode, key) ;
crypt(in, out, cipher); in.close(.);
out.close() ;
catch (IOException exception) {
exception.printStackTrace();
}
catch (GeneralSecurityException exception) {
exception.printStackTrace();
}
catch (ClassNotFoundException exception) {
j exception.printStackTrace();
Использование шифра для преобразования байтов входного потока и передачи преобразованных байтов в выходной поток
©param in the input stream
@param out the output stream
©param cipher the cipher that transforms the bytes
*/
public static void crypt(Inputstream in, Outputstream out,
Cipher cipher) throws IOException, GeneralSecurityException
{
int blocksize =
int outputsize =
byte[] inBytes = new byte[blocksize];
byte[] outBytes = new byte[outputsize];
int inLength =;
boolean more = true;
while (more)
{
inLength = in.read(inBytes);
if (inLength == blocksize) {
int outLength
=0, blocksize, outBytes);
out.write(outBytes, 0, outLength);
System.out.printin(outLength);
}
else more = false;
}
if (inLength > 0)1
outBytes = cipher.doFinal(inBytes, 0, inLength);
else
outBytes = cipher.doFinal();
System.out.printIn(outBytes.length);
out.write(outBytes);
javax.crypto.Cipher
Метод static Cipher getlnstance(String algorithm) возвращает объекттипаCipher, которыйреализуетуказанныйалгоритм. Еслиника- койалгоритмнеуказан, тогенерируетсяисключительнаяситуацияNo- SuchAlgorithmException.
Параметры:
algorithmимя алгоритма, например, DES или
DES/CBC/PKCS5Padding; для уточнения имени используемого алгоритма следует обратиться к его поставщику
Метод static Cipher getlnstance(String algorithm,String
provider) возвращает объект типа Cipher, который реализует указанный алгоритм указанного поставщика. Если никакой алгоритм не указан, то генерируется исключительная ситуация NoSuchAlgorithmException.
Параметры:
algorithmимя алгоритма, например, DES или
DES/CBC/PKCS5Padding; для уточнения имени используемого алгоритма следует обратиться к его поставщику providerимя поставщика, например SunJCE или ВС
Метод int getBlockSize () возвращает размер блока в байтах или значение 0, если используется неблочный алгоритм шифрования.
Метод int getOutputSize (int inputLength) возвращает размер буфера для выходных данных, который потребуется для заданного количества входных байт inputLength. Данный метод учитывает любые буферизированные байты объекта шифрования.
Метод void init(int mode, Key key) инициализирует объект алгоритма шифрования.
Параметры:
modeрежим шифрования: ENCRYPT_MODE, DECRYPT_MODE,
WRAP_MODE, UNWRAP_MODE
keyключ преобразования
Метод byte[] update (byte [] in) преобразовывает один блок входных данных и возвращает результат преобразования.
Параметры:
inновые входные байты
Метод byte [ ] update (byte[] in, int offset, int length) преобразовывает один блок входных данных с указанным отступом и длиной и возвращает результат преобразования.
Параметры:
inновые входные байты
offsetначальный индекс входных данных в массиве in
lengthчисло входных байт
Метод byte[] update(byte[] in,int offset,int length,
byte[] out) преобразовывает один блок входных данных с указанным отступом и длиной и возвращает количество преобразованных байт, которые помещены в переменной out.
Параметры:
inновые входные байты
offsetначальный индекс входных данных в массиве in
lengthчисло входных байт
outмассив, в котором будут помещены выходные байты
Метод byte[] doFinalО преобразовывает последний блок входных данныхиудаляеталгоритмданногообъектаизбуфера.
Метод byte [ ] doFinal (byte [ ] in) преобразовывает последний блок входныхданныхиудаляеталгоритмданногообъектаизбуфера.
Параметры:
inновые входные байты
Метод byte[] doFinal(byte[] in, int offset, int length) преобразовывает последний блок входных данных и удаляет алгоритм данного объекта из буфера.
Параметры:
inновые входные байты
offsetначальный индекс входных данных в массиве in
lengthчисло входных байт
Метод byte[] doFinal(byte[] in, int offset, int length, byte[] out) преобразует последний блок входных данных и удаляет алгоритм данного объекта из буфера.
Параметры:
inновые входные байты
offset начальный индекс входных данных в массиве in
lengthчисло входных байт
outмассив, в котором будут помещены выходные байты
javax.crypto.KeyGenerator
Метод static KeyGenerator getlnstance(String algorithm) возвращает объект-генератор ключа, который реализует указанный алгоритм. Если никакой алгоритм не указан, то генерируется исключительная ситуация NoSuchAlgorithmException.
Параметры:
algorithmимя алгоритма, например, DES
Метод void initialize(SecureRandom random) инициализирует генератор ключа.
Параметры:
randomисточник случайных событий
Метод void initialize(int keysize, SecureRandom random) инициализирует генератор ключа.
Параметры:
keySize размер ключа
randomисточник случайных событий
МетодKeygenerateKey()создаетновыйключ.
java.crypto.SecretKeyFactory
Метод static SecretKeyFactory getlnstance(String algorithm) возвращает объект типа SecretKeyFactory для указанного алгоритма шифрования.
Параметры: algorithmПараметры: algorithmимя алгоритма, например, DES
Параметры: algorithm
Параметры: algorithm
Метод static SecretKeyFactory getlnstance(String algorithm, String provider) возвращает объект типа SecretKeyFactory для указанного алгоритма шифрования и его поставщика.
Параметры:
algorithmимя алгоритма, например, DES
provider имя поставщика, например, SunJCE или ВС
Метод SecretKey generateSecret (KeySpec spec) создает новый секретный ключ на основе указанной спецификации.
java.crypto.spec.SecretKeySpec
Конструктор SecretKeySpec (byte [] key, String algorithm) создает спецификацию ключа.
Параметры:
keyбайты, которые используются для создания ключа
algorithm имя алгоритма, например, DES
Шифры с открытым КЛЮЧОМ
Рассмотренный выше алгоритм шифрования DES называется симметричным алгоритмом шифрования, т.е. для шифровки и расшифровки используется один и тот же ключ. Уязвимым местом симметричного алгоритма шифрования является распространение ключа. Предположим, что Алиса послала Бобу зашифрованное сообщение. Бобу для расшифровки полученного сообщения понадобится тот же ключ, который Алиса использовала для зашифровки сообщения. Если Алиса поменяла ключ, то ей следует для посылки нового сообщения также послать новый ключ по защищенному каналу связи. Если такого канала нет, то Боб не сможет читать новые сообщения.
Для решения этой проблемы можно использовать криптографию с открытым ключом. В таком случае у Боба есть пара ключей: открытый ключ и соответствующий ему закрытый ключ. Он может кому угодно передавать открытый ключ, но закрытый ключ нужно хранить в тайне. Теперь Алиса для шифрования сообщений Бобу просто использует открытый ключ.
На самом деле не все так просто, потому что все алгоритмы с открытым ключом выполняются намного медленней, чем алгоритмы с симметричными ключами, такими как DES или AES. Поэтому было бы неэффективно и непрактично использовать открытые ключи для шифрования данных большого объема. Эту проблему легко решить, комбинируя шифрование с открытым ключом и симметричное шифрование. Рассмотрим следующий пример такой комбинации.
Алиса создает симметричный ключ случайным образом и с его помощью шифрует свое сообщение.
С помощью открытого ключа Боба Алиса зашифровывает симметричный ключ.
Алиса посылает Бобу зашифрованное сообщение и зашифрованный симметричный КЛЮЧ.'
Используя закрытый ключ, Боб расшифровывает симметричный ключ.
Используя расшифрованный симметричный ключ, Боб расшифровывает полученное сообщение.
В данном примере никто, кроме Боба, не сможет расшифровать симметричный ключ, потому что закрытый ключ есть только у него. Таким образом, неэффективный алгоритм шифрования с открытым ключом применяется только для небольшого объема данных о симметричном ключе.
Наиболее популярным алгоритмом с открытым ключом является алгоритм RSA, изобретенный Райвестом (Rivest), Шамиром (Shamir) и Эдлеманом (Adelman). До октября 2000 года этот алгоритм был защищен очень дорогим патентом компании RSA Security Inc., для использования которого требовалось отчислять 3% от прибыли с минимальным ежегодным взносом $50 000. На сегодняшний день данный алгоритм уже не является коммерческим. К сожалению, в расширении JCE компании Sun не поддерживается алгоритм шифрования RSA, но он реализован в SDK.
Однако, другой поставщик, Legion of Bouncy Castle, поддерживает алгоритм шифрования RSA и много других механизмов шифрования, которые не поддерживаются провайдером SunJCE. Более того, JAR-файл с кодом провайдера подписан компанией Sun Microsystems, и его можно вполне безопасно использовать вместе с расширением JCE. (Если код алгоритма шифрования некоторого поставщика не подписан компанией Sun, то используется альтернативная реализация расширения JCE, которая не является частью SDK.) Для тестирования следующего простого примера нужно скопировать JAR-файл поставщика по указанному ниже адресу.
http:/ /ww.bouncycastle.org/index.html
Для установки нового поставщика нужно включить следующую строчку в файл с описанием параметров обеспечения безопасности jre/lib/security/java. security, в котором по умолчанию уже указаны несколько поставщиков.
security.provider.6=org.bouncycastle.jce.provider
.BouncyCastleProvider
Иначе, можно динамически установить нового поставщика с помощью метода addProvider класса Security:
Security. addProvider (new BouncyCastleProvider О ) ,•
При любом способе установки код поставщика должен быть доступен, для чего можно просто переместитьJAR-файл в каталог j re/lib/extили указать его в пути к классам.
Для использования алгоритма шифрования RSA нужно создать открытый и закрытый ключи с помощью класса-генератора ключей KeyPairGenerator.
KeyPairGenerator pairgen =
SecureRandom random = new SecureRandom();
pairgen.initialize(KEYSIZE, random);
KeyPair keyPair =
Key publicKey = keyPair.getPublic();
Key privateKey = keyPair.getPrivate();
В примере программы, код которой приведен в листинге 9.17, учитываются следующие три параметра. Параметр -genkey используется для создания пары ключей, параметр -encrypt — для генерации ключа алгоритма шифрования DES и свертывания его при помощи открытого ключа.
Key key = ...; // Ключ DES.
Key publicKey = ...; // Открытый ключ RSA
Cipher cipher = Cipher.getlnstance("RSA", "ВС");
cipher.init(Cipher.WRAP_MODE,publicKey);
byte[] wrappedKey =;
Приэтомсоздаетсяфайл,которыйсостоитизтакихэлементов:
длина запакованного ключа;
байты запакованного ключа;
информация, зашифрованная ключом DES.
Параметр -decrypt используется для расшифровки данного файла. Для тестирования этой программы необходимо сначала создать ключи для алгоритма RSA.
java RSATest -genKey public.key private.key
Затем нужно зашифровать файл.
java RSATest -encrypt plaintextFile encryptedFile public.key
И в заключение нужно расшифровать его и убедиться в том, что расшифрованная информация совпадает с ранее зашифрованной.
О
Перед запуском программы следует четко выполнить указанные выше инструкции по установке поставщика "Bouncy Castle", т.е. нужно отредактировать файл java.security либо добавить в программу вызов этого поставщика. Кроме того, необходимо поместить JAR-файл в соответствующий каталог или указать его в пути к классам.
Листинг9.17.RSATest.java
import java.io.*;
import java.security.*;
import j
import javax.crypto.spec. *;
/**
Пример использования алгоритма шифрования RSA
java RSATest —genkey public private
java RSATest —encrypt plaintext encrypted public java RSATest —decrypt encrypted decrypted private
*/
public class RSATest
{
public static void main(String[] args)
(
try
(
if (args[0].equals("-genkey"))
(
KeyPairGenerator pairgen
="ВС")
SecureRandomrandom = new SecureRandom(); pairgen.initialize(KEYSIZE, random);
KeyPair keyPair = pairgen.generateKeyPair();
ObjectOutputstream out = new ObjectOutputStream( new FileOutputStream(args[1])) ;
out.writeObject(keyPair.getPublic());
out.close();
out = new Ob j ectOutputStreain (
new FileOutputStream(args[2]));
out.writeObject(keyPair.getPrivate() ) ;
out.close();
}
else if (args[0].equals("-encrypt"))
{
KeyGenerator keygen =
SecureRandom random = new SecureRandom();
keygen.init(random);
SecretKey key = keygen.generateKey();
// Свертывание с помощью открытого ключа RSA
Objectinputstream keyin = new ObjectInputstream( new FileInputStream(args[3]));
Key publicKey = (Key) keyin.readObject();
keyIn.close();
Cipher cipher = Cipher.getlnstance("RSA", "ВС"); cipher.init(Cipher.WRAP_MODE, publicKey);
byte[] wrappedKey = cipher.wrap(key);
DataOutputStream out = new DataOutputStream(new
FileOutputStream(args[2]));
out.writelnt(wrappedKey.length);
out.write(wrappedKey);
Inputstream in = new FileInputStream(args[1]);
cipher = Cipher.getlnstance("DES", "ВС");
cipher.init(Cipher.ENCRYPT_MODE, key);
crypt(in, out, cipher);
in.close();
out.close();
)
else
(
DatalnputStream in = new DatalnputStream(new
FileInputStream(args[l])); int length = in.readlnt();
byte[] wrappedKey = new byte[length]; in.read(wrappedKey, 0, length);
// Развертывание с помощью открытого ключа RSA
ObjectlnputStream keyln = new ObjectInputstream( new FileInputStream(arg3[3]));
Key privateKey = (Key) keyin.readobject(); keyin.closet);
Cipher cipher = Cipher.getlnstance("RSA", "ВС"); cipher.init(Cipher.UNWRAP_MODE, privateKey);
Key key = cipher.unwrap(wrappedKey, "DES",
Cipher.SECRET_KEY) ;
OutputStream out = new FileOutputStream(args[2]); cipher = Cipher.getlnstance("DES", "ВС");
cipher.init(Cipher.DECRYPT_MODE, key)
crypt(in, out, cipher);
in.close();
out.close() ;
}
}
catch (lOException exception)
{
exception.printStackTrace() ;
}
catch (GeneralSecurityException exception) {
exception.printStackTrace() ;
}
catch (ClassNotFoundException exception) {
j exception.printStackTrace () ;
/ * *
Использование алгоритма шифрования для преобразования байтов входного потока и передачи преобразованных байтов в выходной поток ©param in the input stream
Sparain out the output stream
Sparam cipher the cipher that transforms the bytes
*/
public static void crypt(Inputstream in, OutputStream out, Cipher cipher) throws IOException, GeneralSecurityException {
int blockSize = cipher.getBlockSize();
int outputsize =
byte[] inBytes = new byte[blocksize];
byte[] outBytes = new byte[outputsize];
int inLength = 0;;
boolean more = true;
while (more)
{
inLength = in.read(inBytes);
if (inLength == blockSize) {
int outLength
=0, blockSize, outBytes);
out.write(outBytes, 0, outLength);
} else more = false;
}
if (inLength > 0)
outBytes = cipher.doFinal(inBytes, 0, inLength);
else
outBytes = cipher.doFinal();
out.write(outBytes) ;
}}public static final int KEYSIZE = 128;
}
}
java.security.Security
Метод static int addProvider (Provider provider) включает указанного поставщика в набор поставщиков.
Метод static void removeProvider (String name) удаляет поставщика с указанным именем или ничего не делает, если такого поставщика не существует.
Метод static Provider [] getProviders () возвращает набор установленных поставщиков.
Метод static Provider getProvider (String name) возвращает поставщика с указанным именем или значение null, если данного поставщика не существует.
Метод static String getProperty (String key) возвращает свойство обеспечения безопасности для указанного ключа; по умолчанию эти свойства находятся в файле j re/lib/j ava. security.
Метод static void setProperty(String key, String value) задает свойство обеспечения безопасности.
Шифрованные потоки
Расширение JCE поддерживает несколько удобных потоковых классов, которые автоматически зашифровывают и расшифровывают данные. Вот как с их помощью шифруются данные.
Cipher cipher = . . . ;
cipher.init(Cipher.ENCRYPT_MODE,key);
CipherOutputStream out = new CipherOutputStream (
new FileOutputStream(outputFileName) , cipher);
byte[] bytes = new byte[BLOCKSIZE] ;
int inLength = getData(bytes); // Получение данных
while (inLength != —1)
{
out.write(bytes, 0, inLength);
inLength = getData(bytes); // Получение дополнительных данных
}
out.flush() ;
Аналогично можно считывать и расшифровывать данные из файла с помощью классаCipherInputStream.
Cipher cipher = . . . ;
cipher.init(Cipher.DECRYPT_MODE,key);
CipherlnputStream in = new Cipherinputstream (
new Fileinputstream(inputFileName), cipher);
byte[] bytes = new byte[BLOCKSIZE];
int inLength = in.read(bytes);
while (inLength != -1)
{
putData(bytes, inLength); // put data to destination inLength = in.read(bytes);
}
Из классов шифрованных потоков автоматически вызываются методы update и doFinal, так что программисту не придется выполнять связанную с ними работу.
java.crypto.CipherinputStream
Конструктор Cipherinputstream(Inputstream in, Cipher cipher) создает поток входных данных, который считывает данные из потока in и расшифровывает (или зашифровывает) их при помощи указанного алгоритма шифрования.
Метод int read () считывает данные из входного потока, который автоматически шифруется или расшифровывается.
Метод int read(byte[] b, int off, int len) считывает данные из входного потока с указанным отступом of f и длиной len, который автоматически шифруется или расшифровывается.
java.crypto.Cipheroutputstream
Конструктор CipheroutputStream(Outputstream out, Cipher cipher) создает поток выходных данных, который записывает их в поток out и расшифровывает (или шифрует) при помощи указанного алгоритма шифрования.
Метод int write () записывает данные в выходной поток, который автоматически шифруется или расшифровывается.
Метод int write(byte[] b, int off, int len) записывает данные в выходной поток с указанным отступом offи длиной len, который автоматически шифруется или расшифровывается.
Метод void flush () очищает буфер шифрования.
Локализация
в этой главе...
Числа и денежные суммы
т Дата и время
Текст
т Пакеты ресурсов
т Локализация графического интерфейса
X ано или поздно, но пользователи из разных стран мира в конце концов заинтересуют ся созданным вами приложением или аплетом. С одной стороны, благодаря распространению сети Internet, исчезают барьеры между разными странами, а с другой стороны программист, создавая аплеты на основе стандартного набора символов ASCII, снова сам создает эти барьеры. Дело в том, что такие основные объекты, как числа и даты, могут ото бражаться совершенно по-разному даже в тех странах, где используются ASCII-символы. Например, запись 3/4/95 означает разные даты для жителей Германии и Англии. Аплет калькулятор, описанный в главе 10 тома I, может смутить тех пользователей, которые не используют точку (.) для разделения целой и дробной частей числа.
В настоящее время многие пользователи сети Internet понимают английский язык, но им все же намного удобнее работать с аплетами и приложениями, созданными на их родном языке, с представлением данных в знакомом им формате. Допустим, что тот же калькулятор-аплет создан таким образом, что формат представления результатов меняется в зависимости от географического расположения компьютера, на котором он запускается. Несомненно, что такой аплет гораздо полезнее и ценнее, а потому более предприимчивые компании используют достоинства локализации своих приложений для достижения большей прибыли.
Java- это первый язык программирования, который изначально разрабатывался для создания локализованных приложений. Для достижения эффективной локализации с самого начала в нем предусмотрена возможность представления строк в формате Unicode. Поддержка этого стандарта кодирования символов позволяет легко создавать Java-программы, использующие любой из существующих в мире языков.
Наиболее полную информацию о стандарте кодирования символов Unicode можно найти в книге The Unicode Standard, Version 3.0, [Addison-Wesley, 2000]. Большое количество таблиц кодировок также предлагается на Web-сайте по адресу http://www.unicode.org.
Многие программисты считают, что для локализации своих приложений им достаточно использовать Unicode и перевести на нужный язык все сообщения пользовательского интерфейса. Однако, этого недостаточно, потому что локализация программы означает нечто большее, чем просто организация поддержки Unicode. Более того, не все операционные системы и даже не все броузеры поддерживают Unicode. Например, почти всегда нужно создать уровень преобразования между наборами символов и шрифтами компьютера и Unicode-ориентированной виртуальной машины Java. Кроме того, в разных странах сильно отличаются форматы отображения даты, времени, денежных сумм и даже чисел. Необходимо найти простой способ представления на разных языках команд меню и надписей на кнопках, сообщений и комбинаций клавиш. Причем, все это изменения пользовательского интерфейса нужно инициировать на основе информации, полученной программой от компьютера, на котором запущена данная программа.
В этой главе рассматриваются способы создания локализованных Java- приложений и аплетов, способы локализации времени и даты, чисел и текста, а также элементов графического интерфейса пользователя. Кроме того, перечислены некоторые инструменты SDK, предназначенные для создания локализованных программ. (При этом будет рассмотрено создание калькулятора-аплета, в котором формат представления результатов меняется в зависимости от географического расположения компьютера, на котором онзапускается.)
Региональные стандарты
Приложение, которое адаптировано для международного рынка, легко определить по возможности выбора языка, используемого для работы с ним. Но профессионально адаптированные приложения могут иметь разные региональные настройки даже для тех стран, в которых используется одинаковый язык. Эту ситуацию точно подметил Оскар Уайльд: "Теперь у нас, действительно, все как в Америке, естественно, кроме языка".
В любом случае, команды меню, надписи на кнопках и программные сообщения должны быть переведены на местный язык, возможно, с использованием специального национального алфавита. Существует еще много других более тонких различий, например, в Англии и Германии для представления десятичных чисел используются разные форматы. Число, понятное англичанам в виде 123,456.78, должно быть отображено в виде 123 . 456, 78 для пользователей из Германии. Иными словами, точка и запятая в качестве разделителя дробной части и разделителя групп разрядов используются по-разному в этих странах! Похожие различия можно заметить и в способах представления даты. В США привыкли отображать даты в порядке месяц/день/год, в Германии используют более практичный порядок — день/месяц/год, ав Китае вообще наоборот— год/месяц/день. Таким образом, дата 3/22/61должна быть представлена в виде 22.03.1961 для немецкого пользователя. Конечно, если названия месяцев записаны полностью, то разница в способах представления дат становится очевидной. Например, дата
March 22, 1961
должна быть представлена как
Marz 1961
для немецкого пользователя или как
1961^(???)ЗЛ (???)22 B(???)
для китайского пользователя.
В томе 1 рассматривается класс java. text, который имеет методы форматирования чисел, денежных сумм и дат. На самом деле они обладают гораздо большими возможностями при использовании параметра, описывающего географическое расположение. Для использования этих методов (без учета текущих региональных особенностей) достаточно только передавать объекты класса Locale, описывающие необходимый региональный стандарт (locale). Региональный стандарт состоит из следующих компонентов.
Язык.
Географическое расположение.
Необязательный параметр.
Например, в США используется следующий региональный стандарт: язык = английский, расположение = США.
В Германии региональный стандарт имеет другой вид:
язык = немецкий, расположение = Германия
В Швейцарии используются четыре официальных языка: немецкий, французский, итальянский и ретороманский. Поэтому немецкие пользователи в Швейцарии, вероятно, захотят использовать следующий региональный стандарт:
язык = немецкий, расположение = Швейцария.
При использовании этого регионального стандарта для представления текста, дат и чисел будет использоваться такой же формат, что и для регионального стандарта (язык немецкий, расположение = Германия), но денежные суммы будут отображаться в швейцарских франках, а не в немецких марках.
Вариантные параметры, к счастью, используются довольно редко и применяются только в виде исключения. Например, в настоящее время в норвежском языке (производном от датского языка) используется два набора правил правописания: традиционный букмол (Bokmel) и новый нинорск (Nynorsk). В этом случае для задания традиционных правил орфографии используется вариантный параметр:
язык = норвежский, расположение = Норвегия, вариант = букмол.
Для некоторых европейских региональных стандартов предусмотрен вариант "Евро". Например, задание следующих региональных параметров:
язык немецкий, расположение = Германия, вариант = Евро
приведет к использованию знака вместо DM при отображении денежных сумм.
В вариантном параметре также можно указать информацию, связанную с используемой платформой.
Для выражения языка и расположения в компактной и стандартной форме в Java используются коды, определенные Международной организацией по стандартизации (ISO). Локальный язык обозначается двумя строчными буквами в соответствии со стандартом ISO-639, а страна (расположение) — двумя прописными буквами в соответствии со стандартом ISO-3166. В табл. 10.1 и 10.2 приведены некоторые часто встречающиеся коды.
Полный список обозначений ISO-639 можно найти по адресу http: //www.ics.uci. edu/pub/ietf/http/related/iso639.txt, а перечень кодов ISO-3166- по адресу
Dhttp://www.niso.org/3166.html.
Таблица 10.1;Коды основных языков ' в соответствии со стандартом ISO-639
Язык
Код
английский
en
голландский
nl
греческий
el
датский
d3
испанский
sp
итальянский
it
китайский
zh
корейский
ко
немецкий
de
норвежский
no
португальский
pt
турецкий
tr
финский
fi
французский
fr
шведский
SV
японский
ja
Таблица 10.2.Коды некоторых стран в соответствии со стандартом ISO-3166
Страна
Код
Австрия
AT
Бельгия
BE
Великобритания
GB
Окончание табл. 10.2
Страна
Код
Германия
DE
Греция
GR
Дания
DK
Ирландия
IE
Испания
ES
Италия
IT
Канада
CA
Китай
CN
Корея
KR
Нидерланды
NL
Норвегия
N0
Португалия
PT
Соединенные Штаты Америки
US
Тайвань
TW
Турция
TR
Финляндия
FI
Швейцария
CH
Швеция
SE
Япония
JP
Выбор кодов может показаться случайным, но он, по крайней мере, стандартизирован. Например, следующие коды языков происходят от их названий на местных языках: немецкий = deutsch = de, китайский = zhongwen = zh.
Чтобы задать региональный стандарт, необходимо объединить код языка, код страны и параметр (если он есть), а затем передать полученную строку в конструктор класса Locale. Еще раз напомним, что вариантный параметр не является обязательным.
Locale germanGermany = new Locale("de", "DE");
Locale germanSwit zerland = new Locale("de”, "CH");
Locale norwegianNorwayBokmel =newLocale("no", "NO", "B");
Для задания языка без указания страны в качестве второго параметра в конструктор передается пустая строка.
Locale german=newLocale("de", "") ;
Такой способ задания регионального стандарта можно использовать только для зависящих от языка операций, но нельзя использовать для определения настроек формата даты или денежных сумм.
Для удобства пользователей в SDK предусмотрены несколько предопределенных объектов с региональными настройками.
Locale.CANADA
Locale.CANADA-FRENCH
Locale.CHINA
Locale.FRANCE
Locale.GERMANY
Locale.ITALY
Locale.JAPAN
Locale.KOREA
Locale.PRC
Locale.TAIWAN
Locale.UK
Locale.US
В SDK для некоторых языков так же имеются объекты, позволяющие указать язык без указания страны.
Locale.CHINESE
Locale.ENGLISH
Locale.FRENCH
Locale.GERMAN
Locale.ITALIAN
Locale.JAPANESE
Locale.KOREAN
Locale.SIMPLIFIED—CHINESE
Locale . TRADITIONAL—CHINESE
Помимо возможности создания или выбора предопределенных объектов, существуют еще два пути получения объектов с региональными настройками.
Статический метод getDefault класса Locale получает локальные региональные настройки, которые используются по умолчанию в операционной системе. Точно так же метод getLocale аплета возвращает региональные настройки пользователя того компьютера, на котором он запущен. И, наконец, все зависимые от региональных настроек вспомогательные классы могут возвращать массив поддерживаемых региональных стандартов. Например, приведенный ниже метод возвращает все региональные настройки, поддерживаемые классом DateFormat.
Locale[] supportedLocales =
К примеру, во время создания этой книги класс DateFormat поддерживал китайский формат даты, но не поддерживал вьетнамский. Поэтому среди прочих настроек метод getAvailableLocales () возвращал китайские настройки, но не вьетнамские.
В настоящей книге не рассматриваются способы создания новых специфических языковых настроек, например, с учетом бруклинского или техасского диалектов. Более подробные сведения по этой теме можно найти в документации SDK.
Какие действия можно выполнять на основе полученных региональных настроек? Оказывается, выбор не велик, а единственными полезными методами класса Locale являются методы определения кодов языка и страны. Наиболее важным из них явля-
ется метод getDisplayName, который возвращает строку с описанием региональной настройки. Она содержит не какие-то загадочные двухбуквенные коды, а вполне понятные пользователю слова.
Немецкий (Швейцария)
Но эта строка отображается на используемом по умолчанию языке, что далеко не всегда удобно. Если пользователь выбрал немецкий язык интерфейса, то строку' описания следует отобразить именно на немецком языке, для чего можно передать в качестве параметра немецкую региональную настройку так, как показано ниже.
Locale loc = new Locale("de", "CH");
System.out.println(loc.getDisplayName(Locale.GERMAN));
В результате выполнения этого кода описание региональной настройки будет выведено на указанном в ней языке.
Deutsch (Schweiz)
На самом деле объекты региональных настроек Locale нужны для использования в методах, поведение которых зависит от региональных настроек. Например, методы toLowerCase и toUpperCase класса String содержат параметр типа Locale, потому что правила смены регистра букв зависят от региональных настроек. Во Франции в написании прописных букв ударения обычно опускаются, а во франкоязычной Канаде — сохраняются. Например, записанное прописными буквами слово “etoile” (звезда) во Франции будет иметь вид “ETOILE”, а в Канаде — "ETOILE".
String star = "etoile";
String fr= star.toUpperCase(Locale.FRANCE);
// возвращает строку "ETOILE"
String ca = star.toUpperCase(Locale.CANADA_FRENCH);
// возвращает строку "ETOILE"
Все бы хорошо, но на самом деле метод toUpperCase, который должен бы так работать, не учитывал французские настройки вплоть до появления SDK версии 1.4. Однако, этот пример дает представление о вероятном будущем применении объекта Locale. (Действительно, объект Locale можно передавать в качестве параметра многим другим методам, которые решают задачи, связанные с местной спецификой региональных стандартов. В следующих разделах будет рассмотрено несколько примеров такого поведения.)
[aptT java.util. Locale
Метод static Locale getDeflult () возвращает используемый no
умолчанию региональный стандарт.
Метод static void setDefault (Locale 1) устанавливает заданный
региональный стандарт 1 в качестве используемого по умолчанию регионального стандарта.
Метод String getDisplayName () возвращает строку с описанием текущего регионального стандарта.
Метод String getDisplayName (Locale 1) возвращает строку с описанием текущего регионального стандарта в формате заданного регионального стандарта 1.
Метод String getLanguage () возвращает код языка (две строчные буквы) в соответствии со стандартом ISO-639.
Метод String getDi sp1ayLanguage () возвращает название языка в формате текущего регионального стандарта.
Метод String getDisplayLanguage (Locale 1) возвращает название языка в формате заданного регионального стандарта 1.
Метод String getCountryO возвращает код страны (две прописные буквы) в соответствии со стандартом ISO-3166.
Метод String getDisplayCountry () возвращает название страны в формате текущего регионального стандарта.
Метод String getDisplayCountry (Locale 1) возвращает название страны в формате заданного регионального стандарта 1.
Метод String getVariant () возвращает вариантный параметр.
Метод String getDisplayVariant () возвращает название вариантного параметра в формате текущего регионального стандарта.
Метод String getDisplayVariant (Locale 1) возвращает название вариантного параметра в формате заданного регионального стандарта 1.
Метод String toString () возвращает описание регионального стандарта в виде последовательности кодов языка, страны и вариантного параметра, которые разделены символами подчеркивания (например, "de_CH").
Числа и денежные суммы
В предыдущем разделе описывается зависимость способа представления чисел и денежных сумм от используемых региональных стандартов. Для прямого и обратного преобразования строки в число в соответствии с заданным форматом в классе java.text предусмотрено несколько объектов форматирования. Для форматирования числа в соответствии с конкретным региональным стандартом необходимо выполнить следующие действия.
Получить объект с описанием регионального стандарта, как это было описано в предыдущем разделе.
Использовать "шаблонный метод" получения объекта форматирования.
Применить объект форматирования для прямого и обратного преобразования строки в число в соответствии с заданным форматом.
Здесь в качестве шаблонных методов (factory method) используются статические методы getNumberlnstance, getCurrencylnstance и getPercentInstance класса Number Format. Они получают параметр типа Locale и возвращают объекты, предназначенные для прямого и обратного преобразования чисел, денежных сумм и
процентов. Например, для отображения денежной суммы в принятом в Германии формате можно использовать следующий код.
Locale loc = new Locale("de", "DE");
NumberFormat currFmt =
double amt = 123456.78;
System.out.printIn(currFmt.format(amt));
В результате выполнения этого кода будет получена следующая строка.
123.456,78 DM
Для обозначения немецкой марки здесь используется знак DM, который располагается в конце строки. Кроме того, обратите внимание на особенности применения символов разделения дробной части и десятичных разрядов.
Рассмотрим обратную задачу преобразования строки, записанной в соответствии с определенным региональным стандартом, в число. Для этого предусмотрен метод parse, который выполняет синтаксический анализ строки с автоматическим использованием заданного по умолчанию регионального стандарта. В приведенном ниже примере показан способ преобразования строки, введенной пользователем в текстовое поле, в число. Метод parse способен преобразовывать числа с точками и запятыми, а также с цифрами, представленными в различных стандартах.
TextField inputField;
NumberFormat fmt =
// Получить объект форматирования для используемого
//по умолчанию регионального стандарта
Number input = fmt.parse(inputField.getText().trim());
double x = input.doubleValue();
Метод parse возвращает результат абстрактного типа Number, но возвращенный объект может также иметь тип-оболочку Long или Double в зависимости от того, представляла ли исходная строка целое число или число с плавающей точкой. Если это не важно, то для получения числа достаточно использовать метод doubleValue () класса Number.
Если число представлено в некорректной форме, то метод генерирует исключительную ситуацию ParseException. Например, не допускается наличие символов пробела в начале преобразуемой в число строки. (Для их удаления следует использовать метод trim.) Однако, игнорируются любые символы, которые располагаются в строке после числа, и исключительная ситуация в этом случае не генерируется.
Следует отметить, что на самом деле классы, возвращаемые шаблонными методами getXxxInstance, являются представителями не абстрактного класса NumberFormat, a одного из его производных классов.
Шаблонным методам известно лишь, как найти объект, который принадлежит определенному региональному стандарту.
Очевидно, что создание объекта форматирования для определенного регионального стандарта представляет собой достаточно сложную задачу. В настоящее время в SDK предусмотрен ограниченный набор локализованных объектов форматирования, но со временем их число должно увеличиться, а многие программисты стремятся самостоятельно создавать такие объекты.
Для получения списка поддерживаемых региональных стандартов можно использовать статический метод getAvailableLocales, который возвращает массив региональных стандартов, для которых существуют объекты форматирования чисел.
На рис. 10.1 показан внешний вид программы, которая дает возможность поэкспериментировать с объектами форматирования. В верхней части окна программы расположен список всех региональных стандартов, для которых существуют объекты форматирования. В группе переключателей можно выбрать тип объекта для форматирования чисел, денег или процентов. После каждой попытки выбора нового регионального стандарта и типа объекта форматирования в текстовом поле будет отображено новое представление заданного числа. Даже после просмотра всего нескольких вариантов форматирования чисел и денежных сумм в соответствии с разными региональными стандартами у пользователя несомненно останется глубокое впечатление от богатого разнообразия форматов. В текстовом поле также можно ввести любое число и щелкнуть по кнопке Parse (Обратное преобразование строки в число) для вызова метода parse, который попытается выполнить синтаксический анализ введенной строки. В случае успешного анализа строки она передается методу format, a затем отображается на экране. В противном случае в текстовом поле отображается сообщение Parse error (Ошибка обратного преобразования строки в число).
Рис. 10.1. Программа NumberFormatTest
Показанный в листинге 10.1 код этой программы имеет довольно простую структуру. Сначала в конструкторе вызывается метод NumberFormat. getAvailableLocales. Затем для каждого поддерживаемого регионального стандарта вызывается метод getDisplay- Name, а возвращаемые методом getDisplayName результаты помещаются в список. Всякий раз, когда пользователь выбирает другой региональный стандарт или щелкает по одному из переключателей, создается новый объект форматирования, и обновляется содержимое текстового поля. Если пользователь щелкает на кнопке Parse, то в таком случае вызывается метод parse для обратного преобразования строки в число в соответствии с выбранным региональным стандартом.
Листинг 10.1.NumberFormatTest.java
©version 1.11 2001-08-27
@author Cay Horstmann
import java.awt.*;
import java.awt.event.*;
import j ava.text.*;
import java.util.* ;
import j
/**
Пример форматирования чисел в соответствии
с выбранным региональным стандартом
*/
public class NumberFormatTest
{
public static void main(String[] args)
{
JFrame frame = new NumberFormatFrame() ;
frame.setDefaultCloseOperation(JFrame,EXIT_ON_CLOSE); frame.show();
}
/**
Этот фрейм содержит группу переключателей для выбора типа форматирования, список для выбора регионального стандарта, текстовое поле для отображения результата форматирования, а также кнопку для обратного преобразования строки в число */ class NumberFormatFrame extends JFrame {
public NumberFormatFrame() {
setsize(WIDTH, HEIGHT);
setTitle("NumberFormatTest") ;
getContentPane().setLayout(newGridBagLayout());
ActionListener listener = new
ActionListener() { public void actionPerformed(ActionEvent event) {
updateDisplay() ;
}
) ;
JPanelp=newJPanel() ;
addRadioButton(p, numberRadioButton, rbGroup, listener); addRadioButton(p, currencyRadioButton, rbGroup, listener), addRadioButton(p, percentRadioButton, rbGroup, listener);
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = gbc.anchor = add (new JLabel ("Locale”) , gbc,0,0,1,1); add(p, gbc,1,1,1,1);
add(parseButton, gbc,0,2,1,1);
gbc.anchor =
add(localeCombo, gbc,1,0,1,1);
gbc.fill=GridBagConstraints.HORIZONTAL;
add(numberText, gbc, 1, 2, 1, 1) ;
locales = NumberFormat.getAvailableLocales();
for (inti=0; i<locales.length; i++)
localeCombo.additem(locales[i].getDisplayName()); localeCombo.setSelectedltem(
Locale.getDefault().getDisplayName() );
currentMumber = 123456.78;
updateDisplay();
localeCombo.addActionListener(listener);
parseButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
String s = numberText.getText();
try
{
Number n = currentNumberFormat.parse(s) ; if (n != null) {
currentNumber = n.doubleValue() ; updateDisplay () ;
} else {
} numberText.setText("Parse error: " +s) catch(ParseException e)
{
^ numberText.setText("Parseerror: "+s);
)
});
}
Вставляет компонент в заданное место решетки ©param с the component to add
Sparam gbc the grid bag constraints to use
©param x the x grid position
©param у the у grid position
@param w the grid width
©param h the grid height
*/
public void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane() .add(c, gbc) ;
}
/**
Создает в контейнере кнопку переключателя
©param p the container into which to place the button Sparam b the button
@param g the button group
@param listener the button listener
*/
public void addRadioButton(Container p, JRadioButton b, ButtonGroup g, ActionListener listener)
{
b.setSelected(g.getButtonCount() == 0);
b.addActionListener(listener);
g.add(b);
p.add(b);
/**
Обновляет отображение результатов и форматирует
число в соответствии с выбором пользователя
*/
public void updateDisplay() {
Locale currentLocale = locales[
localeCombo.getSelectedIndex()];
currentNumberFormat = null;
if (numberRadioButton.isSelected())
currentNumberFormat
=
else if (currencyRadioButton.isSelected()) currentNumberFormat
=
else if (percentRadioButton.isSelected()) currentNumberFormat
=;
String n = currentNumberForinat.format(currentNumber) ;
;
}
private Locale[] locales;
private double currentNumber;
private JComboBox localeCombo = new JComboBox();
private JButton parseButton = new JButton("Parse") private JTextField numberText = new JTextField(30) private JRadioButton numberRadioButton
= new JRadioButton("Number");
private JRadioButton currencyRadioButton
= new JRadioButton("Currency");
private JRadioButton percentRadioButton
= new JRadioButton("Percent");
private ButtonGroup rbGroup = new ButtonGroup();
private NumberFormat currentNumberFormat;
private static final int WIDTH = 400;
private static final int HEIGHT = 200;
j ava. text. NumberFormat
Метод static Locale [] getAvailableLocales () возвращает массив объектов Locale, для которых имеются объекты форматирования NumberFormat.
Метод static NumberFormat getNumberlnstance () возвращает объект форматирования чисел в формате текущего регионального стандарта.
Метод static NumberFormat getNumberlnstance(Locale 1) возвращает объект форматирования чисел в формате заданного регионального стандарта 1.
Метод static NumberFormat getCurrencylnstance() возвращает объект форматирования денежных сумм в формате текущего регионального стандарта.
Метод static NumberFormat getCurrencylnstance(Locale 1) возвращает объект форматирования денежных сумм в формате заданного регионального стандарта 1.
Метод static NumberFormat getPercentInstance () возвращает объект форматирования процентов в формате текущего регионального стандарта.
Метод static NumberFormat getPercentlnstance(Localel) возвращает объект форматирования процентов в формате заданного регионального стандарта 1.
Метод String format (double x) возвращает строку, представляющую значение числа с плавающей точкой в формате текущего регионального стандарта.
Метод String format (long x) возвращает строку, представляющую значение целого числа в формате текущего регионального стандарта.
Метод Number parse (String s) возвращает число, полученное после преобразования строки. Это число может иметь тип Long или Double в зависимости от того, представляла ли исходная строка целое число или нет. Строка должна начинаться не с пробелов, а с числа. Любые символы в строке после числа игнорируются. Если преобразование закончилось неудачей, то метод генерирует исключительную ситуацию ParseException.
Метод void setParselntegerOnly (boolean b) указывает, что данный объект форматирования предназначен для преобразования только целочисленных значений, если значение параметра b равно true.
Метод boolean isParselntegerOnlyвозвращает значение true, если данный объект форматирования предназначен для преобразования только целочисленных значений.
Метод void setGroupingllsed (boolean b) указывает, что данный объект форматирования использует символы разделения групп десятичных разрядов (например, 100,000), если значение параметра b равно true.
Метод boolean isGroupingUsed() возвращает значение true, если данный объект форматирования распознает символы разделения групп десятичных разрядов (например, 100, 000).
Метод void setMinimumlntegerDigits (int n) задает минимальное допустимое количество цифр в целой части числа.
Метод int getMinimumlntegerDigits () возвращает минимальное допустимое количество цифр в целой части числа.
Метод void setMaximumlntegerDigits (int n) задает максимальное допустимое количество цифр в целой части числа.
Метод int getMaximumlntegerDigits () возвращает максимальное допустимое количество цифр в целой части числа.
Метод void setMinimumFractionDigits (int n) задает минимальное допустимое количество цифр в дробной части числа.
Метод int getMinimumFractionDigits () возвращает минимальное допустимое количество цифр в дробной части числа.
Метод void setMaximumFractionDigits (int n) задает максимальное
допустимое количество цифр в дробной части числа.
Метод int getMaximumFractionDigits () возвращает максимальное допустимое количество цифр в дробной части числа.
Дата и время
При форматировании даты и времени в соответствии с региональными стандартами следует помнить о следующих четырех особенностях.
Названия месяцев и дней недели должны быть представлены на местном языке.
Существуют особые местные последовательности указания года, месяца и числа.
Для отображения дат может использоваться другой календарь, отличный от григорианского.
Следует учитывать часовые пояса.
Для учета перечисленных особенностей в Java предусмотрен класс DateFormat, который, в общем, используется почти так же, как и класс NumberFormat. Прежде следует получить объект регионального стандарта. Для получения массива региональных стандартов, поддерживающих форматирование даты, можно использовать предлагаемый по умолчанию стандарт или вызвать статический метод getAvailableLocales. Далее нужно вызвать один из трех шаблонных методов.
fmt =loc);
fmt =loc);
fmt = DateFormat.getDateTimelnstance(datestyle, timeStyle, loc);
Для указания нужного стиля в шаблонных методах предусмотрен параметр, который может содержать один из следующих типов представления даты:
DateFormat.DEFAULT;
DateFormat.FULL (например, Thursday, September 18,19978:42:46
o'clock A.M. PDT для регионального стандарта США);
DateFormat.LONG (например, September 18, 1997 8:42:46 A.M. PDT для регионального стандарта США);
DateFormat .MEDIUM (например, Sep 18, 1997 8:42:46 A.M. для регионального стандарта США);
DateFormat. SHORT (например, 9/18/97 8:42 A.M. для регионального стандарта США).
Шаблонный метод возвращает объект форматирования, который используется для форматирования даты.
Date now = new Date();
String s = fmt.format(now);
Для получения обратного преобразования строки в дату используется метод parse, который работает аналогично одноименному методу класса NumberFormat. Например, в следующем фрагменте кода представлен способ обратного преобразования строки, введенной пользователем в текстовом поле, в дату.
TextField inputField;
DateFormat fmt = DateFormat.getDatelnstance(DateFormat.MEDIUM);
// Получить объект форматирования даты
Date input = fmt.parse(inputField.getText(). trim());
В случае некорректного ввода даты попытка преобразования приведет к генерации исключительной ситуации ParseException. Следует отметить, что при выполнении обратного преобразования строки в дату также не допускается наличие пробелов в начале строки. Для их удаления необходимо вызвать метод trim. Однако, при этом игнорируются любые символы строки, которые располагаются после даты. К сожалению, пользователь должен вводить дату в конкретном ожидаемом формате. Например, если установлен тип представления даты MEDIUM в региональном стандарте США, то предполагается, что введенная дата должна иметь вид Sep 18, 1997. Но если пользователь введет строку Sep 18 1997 (без запятой) или 9/18/97 (в кратком формате), то это приведет к ошибке преобразования.
Для интерпретации неточно указанных дат по умолчанию предусмотрено использование флага lenient. Если этот флаг установлен, то неверно заданная дата February 30, 1999 будет автоматически преобразована в дату March 2, 1999. Ta- кое поведение вряд ли можно считать безопасным, поэтому данный флаг рекомендуется отключить. При попытке пользователя ввести некорректную комбинацию дня, месяца и года во время преобразования строки в дату генерируется исключительная ситуация 111egalArgumentException.
В программе, код которой приведен в листинге 10.2, демонстрируется пример использования класса DateFormat. С его помощью можно выбирать различные региональные стандарты и наблюдать за тем, как в разных странах мира форматируются дата и время. Если на компьютере не установлены необходимые для отображения шрифты, то вместо некоторых символов будут отображаться знаки вопроса. Например, если выбрать китайский региональный стандарт, то дата может быть отображена в следующем виде.
1997?9?19?
На рис. 10.2 изображено окно программы, которая выполняется на компьютере под управлением китайской версии Windows и успешно отображает все необходимые
символы.
10.2. Пример локализации даты и времени в программе при работе с китайскойверсиейWindows .
Для проверки преобразования строки в дату введите дату или время в текстовом поле Date (Дата) или Time (Время), установите флажок Parse lenient (Преобразование строки в дату с автоматическим исправлением ошибок), затем щелкните на кнопке Parse date для ввода даты или кнопке Parse time для ввода времени.
Единственным неизвестным компонентом в рассматриваемом примере является класскоторый используется для выполнения следующих действий. Сна
чала нужно заполнить список Date Style (Тип представления даты) значениями Short (Краткий формат), Medium (Средний формат) и Long (Длинный формат), а затем организовать автоматическое преобразование выбранного пользователем варианта в целочисленноезначениеSHORT,или
LONG соответственно. Для этого выбранный пользователем вариант приводится к верхнему регистру, пробелы заменяются на символы подчеркивания, а потом для определения значения статичного поля по его названию используется метод отражения. (Более подробно метод отражения описывается в главе 5 тома 1.)
Листинг 10.2.
1.11 2001-08-27
Cay Horstmann
import
import
import
import
import
Пример форматирования дат для
разных региональных стандартов
*/
public class DateFormatTest
public static void main(String[] args)
{
JFrame= new
frame.setDefaultCloseOperation(JFrame,EXIT_ON_CLOSE); frame.show();
}
/**
Этот фрейм содержит список региональных стандартов, списки типов представления даты и времени, текстовые поля для отображения форматированной даты и времени, кнопки для обратного преобразования строк в текстовых полях, а также флажок автоматического исправления ошибок
*/
class DateFormatFrame extends JFrame {
public DateFormatFrame()
{
setsize(WIDTH, HEIGHT);
setTitle("DateFormatTest");
getContentPane().setLayout(new GridBagLayout() ) ;
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = gbc.anchor = add(new JLabel (“Locale”) , gbc, 0, 0, 1, 1) ;
add(new JLabel("Date style"), gbc,0,1,1,1);
add (new JLabel ("Time style"), gbc,2,1,1,1);
add (new JLabel ("Date") , gbc, 0, 2,1,1);
add (new JLabel (“Time") , gbc, 0, 3,1,1);
gbc.anchor =
add(localeCombo, gbc, 1, 0, 2, 1) ;
add(dateStyleCombo, gbc, 1, 1, 1, 1) ;
add(timeStyleCombo, gbc, 3, 1, 1, 1) ;
add(dateParseButton,gbc,3,2,1,1);
add(timeParseButton,gbc,3,3,1,1);
add(lenientCheckbox,gbc,0,4,2,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
add(dateText, gbc, 1, 2, 2, 1) ;
addttimeText, gbc, 1, 3, 2, 1) ;
locales = DateFormat.getAvailableLocales();
for (int i = 0; i < locales.length; i++)
localeCombo.additem(locales[i].getDisplayName());
localeCombo.setSelectedltem(
Locale.getDefault().getDisplayName());
currentDate = new Date();
currentTime = new Date();
updateDisplay();
ActionListener listener = new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
updateDisplay();
}
};
localeCombo.addActionListener(listener); dateStyleCombo.addActionListener(listener); timeStyleCombo.addActionListener(listener);
dateParseButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
String d = dateText.getText();
try
{
currentDateFormat.setLenient
(lenientCheckbox.isSelected()) ;
Date date = currentDateFormat.parse(d) ; currentDate = date;
updateDisplay();
}
catch(ParseException e)
{
dateText.setText("Parse error: " + d) ;
}
catch(IllegalArgumentException e) {
dateText.setText(“Argument error: " + d) ;
}
});
timeParseButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
String t = timeText.getText();
try
{
currentDateFormat.setLenient
(lenientCheckbox.isSelected()) ;
Date date = currentTimeFormat.parse(t); currentTime = date;
updateDisplay();
}
catch(ParseException e)
{
timeText.setText("Parse error: " + t) ;
}
catch(IllegalArgumentException e) {
timeText.setText("Argument error: " + t) ;
}
)
));
/**
Вставляет компонент в заданное место решетки
@param с the component to add
@param gbc the grid bag constraints to use
©param x the x grid position
@param у the у grid position
@param w the grid width
@param h the grid height
*/
public void add(Component с, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane().add(c, gbc);
}
/**
Обновляет отображение результатов и форматирует
число в соответствии с выбором пользователя
*/
public void updateDisplay()
{
Locale currentLocale = locales[
localeCombo.getSelectedlndex() ] ;
int dateStyle = dateStyleCombo.getValue() ;
currentDateFormat
=
currentLocale);
String d = currentDateFormat.format(currentDate);
dateText.setText(d);
int timeStyle = timeStyleCombo.getValue();
currentTimeFormat
=
currentLocale);
String t = currentTimeFormat.format(currentTime);
timeText. setText (t) ; }
private Locale[] locales;
private Date currentDate;
private Date currentTime;
private DateFormat currentDateFormat;
private DateFormat currentTimeFormat;
private JComboBox localeCombo = new JComboBox();
private EnumCombo dateStyleCombo
=newEnumCombo(DateFormat.class,
new String!] { "Default", "Full", "Long", "Medium", "Shore"});
private EnumCombo timeStyleCombo
=newEnumCombo(DateFormat.class,
new String[] { "Default", "Full", "Long", "Medium", "Short"}) ;
private JButton dateParseButton new JButton("Parse date"); private JButton timeParseButton =newJButton("Parse time"); private JTextField dateText = new JTextField(30);
private JTextField timeText = new JTextFieldOO);
private JTextField parseText = new JTextFieldOO);
private JCheckBox lenientCheckbox
=newJCheckBox("Parse lenient", true);
private static final int WIDTH = 400;
private static final int HEIGHT =200;
/**
Список, с помощью которого пользователи могут выбирать значения статичных полей по названию поля, указанному в конструкторе
*/
classextends
{
/**
Создание списка EnumCombo
@parama class
labels an array of static field names of cl
*/ publiccl,labels)
{
for (int i = 0; i <i++)
{
String label = labels[i];
String name =',;
int value = try { f = value = } e) { label= "("+label+ ")" ; } new additem(label);
} labels )
}
/**
Возвращение значения поля, выбранного пользователем @return the static field value
*/ public int
{ return
}
private Map table = new
•
Метод static[] getAvailableLocales () возвращает массив
объектов Locale, для которых имеются объекты форматирования DateFormat.
Метод static DateFormatdateStyle) воз
вращает объект форматирования даты в формате текущего регионального стандарта.
Метод static DateFormatdateStyle,
Locale 1) возвращает объект форматирования даты в формате заданного регионального стандарта 1.
Метод static DateFormat getTimelnstance(int timestyle) возвращает объект форматирования времени в формате текущего регионального стандарта.
Метод static DateFormat getTimelnstance(int timeStyle, Locale 1) возвращает объект форматирования времени в формате заданного регионального стандарта 1.
Метод static DateFormat getDateTimelnstance(int datestyle, int timeStyle) возвращает объект форматирования даты и времени одновременно в формате текущего регионального стандарта.
Метод static DateFormat getDateTimelnstance(int dateStyle, int timeStyle, Locale 1) возвращает объект форматирования даты и времени одновременно в формате заданного регионального стандарта 1.
Параметры:
dateStyle, timeStyleмогут принимать одно из следующих значений:
DEFAULT, FULL, LONG, MEDIUM, SHORT
Метод String format (Date d) возвращает строку, полученную в результате форматирования указанной даты/времени.
Метод Date parse (String s) выполняет преобразование строки и возвращает дату/время. Строка должна начинаться не с пробелов, а с да- ты/времени. Любые символы в строке после даты/времени игнорируются. Если преобразование закончилось неудачей, то метод генерирует исключительную ситуацию ParseException.
Метод void setLenient (boolean b) устанавливает флаг, определяющий нестрогий или строгий режим преобразования строки в дату. Если этот флаг установлен, то, например, неверно заданная дата February 30, 1999 будет автоматически преобразована в дату March 2, 1999. Этот флаг устанавливается по умолчанию.
Метод boolean isLenient() снимает флаг, определяющий нестрогий или строгий режим преобразования из строки.
Метод void setcalendar(Calendar cal) устанавливает объект- календарь, используемый для получения года, месяцев, дней, часов, минут и секунд из объекта Date. Этот метод используется для замены предлагаемого по умолчанию календаря (обычно григорианского календаря).
Метод Calendar getCalendar () возвращает объект-календарь, используемый для получения года, месяцев, дней, часов, минут и секунд из объекта Date. Этот метод используется в случае замены предлагаемого по умолчанию календаря (обычно григорианского календаря).
Метод void setTimeZone(TimeZone tz) задает часовой пояс, который используется для форматирования времени. Этот метод используется для замены предлагаемого по умолчанию локального часового пояса. Он определяется как часовой пояс предлагаемой по умолчанию региональной настройки, которая используется операционной системой. Для других ре-
гиональных настроек часовой пояс определяется на основании данного географического расположения.
Метод TimeZone getTimeZone () возвращает часовой пояс, который используется для форматирования времени.
Метод void setNumberFormat(NumberFormat f) задает формат числа, используемый для форматирования чисел, представляющих годы, месяцы, дни, часы, минуты и секунды.
Метод NumberFormat getNumberFormat () возвращает формат числа, используемый для форматирования чисел, представляющих годы, месяцы, дни, часы, минуты и секунды.
Текст
При отображении даже простейшего текста в локализованном приложении приходится учитывать множество особенностей. В этом разделе рассматриваются способы представления текста и работы с текстовыми строками. Очевидно, что порядок сортировки строк зависит от используемых региональных настроек. Кроме того, необходимо локализовать и сам текст, т.е. перевести на соответствующий язык инструкции, обозначения и сообщения. (Далее в этой главе описываются способы создания пакетов ресурсов, которые позволяют собирать в одном наборе строки сообщений для определенного языка.)
Сортировка
Сортировка в алфавитном порядке строк, состоящих только из английских ASCII-символов, не вызывает затруднений. Для этого нужно только организовать сравнение строк с помощью метода compareTo класса String. Результат сравнения a. compareTo (b) будет либо отрицательным, если а лексикографически меньше, чем b, либо равен нулю, если строки идентичны, либо положительный, если а лексикографически больше, чем b.
К сожалению, данный метод полезен лишь в случаях, когда слова состоят из прописных английских ASCII-символов. Дело в том, что в методе compareTo для сравнения используются порядковые номера символов в кодировке Unicode. Например, строчные буквы в кодировке Unicode имеют большие значения, чем прописные буквы, а буквы со знаком ударения имеют еще большие значения. Это приводит к абсурдным результатам, например, вот как были бы отсортированы следующие строки с помощью метода compareTo.
America
Zulu
ant
zebra
Angstrom
Для сортировки этих слов в том порядке, который используется в словаре, необходимо считать строчные и прописные символы равнозначными. С точки зрения англичанина правильным будет следующий порядок.
America
Angstrom
ant
zebra
Zulu
Однако, для датчанина указанный порядок будет неправильным. В датском языке буквы А и А совершенно разные, более того, А идет после буквы Z! Поэтому с точки зрения датчанина эти слова должны быть отсортированы так.
America
ant
zebra
Zulu
Angstrom
К счастью, эта проблема решается довольно просто. Сначала нужно получить объект Locale, а затем вызвать шаблонный метод getlnstance для получения объекта- сортировщика Collator. Далее сортировка производится с использованием метода compare объекта Collator, а не метода compareTo класса String.
Locale loc = . . . ;
Collator coll = Collator.getlnstance(loc);
if (coll.compare(a, b) < 0) //а располагается перед b ...;
Важнее всего то, что класс-сортировщик Collator реализует интерфейс Comparator. Поэтому для сортировки списка строк можно использовать метод Collections.sort.
Collections.sort(strings, coll);
Для настройки избирательности сортировки в сортировщике предусмотрено три уровня мощности (strength): первостепенный (primary), второстепенный (secondary) и третьестепенный (tertiary) уровень. Например, для английского языка различие между символами "А" и "Z" учитывается на первостепенном уровне, между "А" и "А" — на второстепенном уровне, а между "А" и "а" — на третьестепенном уровне.
Для того чтобы сортировщик обращал внимание только на первостепенные различия, необходимо установить значение Collator . PRIMARY. Если для мощности сортировки установить значение Collator.SECONDARY, то в таком случае будут учтены также второстепенные различия. В приведенном ниже примере различия в двух строках будут найдены при установке большей избирательности, т.е. мощности Collator.SECONDARY.
// Предполагается английский региональный стандарт
String a = "Angstrom";
String b = "Angstrom";
coll.setstrength(Collator.PRIMARY) ;
if (coll.compare(a,b) ==0) System.out.print("same") ;
else System.out.print("different");
// Будет получен результат "same" coll.setstrength. (Collator.SECONDARY);
if (coll.compare(a, b) == 0) System.out.print("same");
else System.out.print("different");
// Будет получен результат "different"
Втабл.10.3приведенывариантысортировкинаборастрокдляразличныхуровней избирательности.
Таблица 10.3. Сортировка строк для различных уровней
избирательности
Исходный порядок
Отсортированны
й порядок
PRIMARY
SECONDARY
TERTIARY
Ant,Angstrom = Angstrom
Angstrom
Angstrom
ant,Ant = ant
Angstrom,
Angstrom,
Angstrom,
Ant = ant
Ant,
Angstrom,
ant,
В заключение рассмотрим режим декомпозиции (decomposition mode). По умолчанию используется "каноническая декомпозиция", т.е. буквы со знаком ударения раскладываются на буквы и символы ударения. При выборе варианта "без декомпозиции" сортировка происходит быстрее, но правильно отсортированы будут только строки, не содержащие букв с символами ударения. (Кроме того, бессмысленно сортировать буквы с символами ударения по их порядковым номерам в таблицах кодировки Unicode.) Наконец, при использовании режима "полной декомпозиции" производится более подробный анализ вариантов представления идентичных Unicode-символов. Например, японские дисплеи могут отображать английские символы в двух режимах: полу- ширинным или полноширинным шрифтом. Полуширинный шрифт имеет нормальное межсимвольное расстояние, тогда как полноширинные символы отображаются в той же широкой фиксированной сетке, что и идеограммы. (Вообще говоря, это проблема отображения символов, которая не должна решаться на основе применения разных Unicode-символов, но, как говорится, что написано пером, не вырубишь топором.) При полной декомпозиции полноширинные и полуширинные варианты одних и тех же букв считаются идентичными.
Если одна строка сравнивается многократно с другими строками, то во избежание ее повторной декомпозиции и повышения эффективности следует сохранить результат де- композициивобъекте-ключесравнения (collation key). Нижеприводитсяпримериспользова-
ния метода getCollationKey, который возвращает объектключ CollationKey, используемый для более быстрого выполнения всех дальнейших сравнений.
String а = . . .;
CollationKey aKey =
if (aKey.compareTo(coll.getCollationKey(Ь) == 0))
// быстрое сравнение
Показанная на рис. 10.3 программа, код которой приведен в листинге 10.3, позволяет экспериментировать с разными видами сортировки. Введите слово в текстовое поле и щелкните на кнопке Add для добавления его в список. Список слов сортируется заново после каждого добавления слова, изменения регионального стандарта, уровня избирательности или режима декомпозиции. Знак равенства (=) указывает на то, что словасчитаются идентичными.
Рис. 10.3. ПрограммаCollationTest
Листинг10.3.CollatlonTeat.java
/**
©version 1.11 2001-08-27
©author Cay Horstmann
*/
import java.io.* ;
import java.awt. *;
importj ava.awt.event.*;
import j
import java.util.*;
import java.util.List;
import javax.swing.*;
Пример сортировки строк при использовании разных региональных стандартов, уровня избирательности и режима декомпозиции
*/
public class CollationTest {
public static void main(String[] args) (
JFrame frame = new CollationFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
/**
Этот фрейм содержит список региональных стандартов, список уровней избирательности, список режимов декомпозиции, текстовое поле для ввода слова и кнопку для его включения в список слов, а также текстовое поле со списком введенных слов */ class CollationFrame extends JFrame {
public CollationFrame()
{
setsize(WIDTH,HEIGHT);
setTitle("CollationTest");
getContentPane().setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = gbc.anchor =
add (new JLabel ("Locale") , gbc, 0, 0, 1, 1) ;
add (new JLabel ("Strength") , gbc, 0, 1, 1, 1) ; add(new JLabel("Decomposition"), gbc, 0, 2, 1, 1); add(addButton, gbc, 0, 3, 1, 1) ;
gbc.anchor =
add(localeCombo, gbc, 1, 0, 1, 1) ; add(strengthCombo, gbc, 1, 1, 1, 1); add(decompositionCombo, gbc, 1, 2, 1, 1); gbc.fill = GridBagConstraints.HORIZONTAL; add(newWord, gbc, 1, 3, 1, 1);
gbc.fill = GridBagConstraints.BOTH;
add(new JScrollPane(sortedWords), gbc, 1, 4, 1, 1);
locales = Collator.getAvailableLocales();
for (int i = 0; i < locales.length; i++)
localeCombo.additem(locales[i].getDisplayName()); localeCombo.setSelectedltem(
Locale.getDefault().getDisplayName());
strings.add("America");
strings.add("ant”);
strings.add("Zulu”);
strings.add("zebra”);
strings.add("?strom");
strings.add("Angstrom");
strings.add("Ant");
updateDisplay();
addButton.addActionListener(new
ActionListener() <
public void actionPerformed(ActionEvent event) {
strings.add(newWord.getText()); updateDisplay();
});
listener = new
ActionListener()
{
public voidevent)
{
updateDisplay();
}
localeCombo.addActionListener(listener);
strengthcombo.addActionListener(listener);
decompositioncombo.addActionListener(listener) ;
/**
Вставляет компонент в заданное место решетки с the component to add gbc the grid bag constraints to use x the x grid position у the у grid position w the grid width h the grid height
*/
public voidc, GridBagConstraints gbc,
int x, int y, int w, int h)
{
= x; =
= w; = h; gbc);
}
/**
Обновление отображения и сортировки строк в соответствии с установками пользователя */ public void updateDisplay() {
Locale currentLocale =
localeCombo.getSelectedlndex()];
currentcollator
=
currentcollator.setstrength(strengthcombo.getValueO) ; currentcollator.setDecomposition(
decompositioncombo.getValue());
currentCollator);
sortedWords.setText("");
for (int i = 0; i <i++)
{
String s = if (i > 0 —1)) == 0)
sortedWords. append (11 =");
}
sortedWords.append(s+ "\n");
} }
private Locale[] locales;
private List strings = new ArrayList();
private Collator currentCollator;
private JComboBox localeCombo = new JComboBox();
private EnumCombo strengthCombo
=newEnumCombo(Collator.class,
new String[] { "Primary", "Secondary", "Tertiary" }); private EnumCombo decompositionCombo
=newEnumCombo(Collator.class,
new String[] { "Canonical Decomposition",
"Full Decomposition", "No Decomposition" });
private JTextField newWord = new JTextField(20);
private JTextArea sortedWords = new JTextArea(10, 20);
private JButton addButton = new JButton("Add");
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
/**
Список, с помощью которого пользователи могут выбирать значения статичных полей по названию поля, указанному в конструкторе
*/
class EnumCombo extends JComboBox (
/**
Создание списка EnumCombo.
@param cl a class
@param labels an array of static field names of cl
*/
public EnumCombo(Class cl, String[] labels) (
for (int i = 0; i < labels.length; i++) {
String label = labels(i];
String name = label.toUpperCase().replace(’ ', ’_'); int value0;
try (
java.lang.reflect.Field f = cl.getField(name); value = f.getlnt(cl);
}
catch(Exception e) {
label = "(" + label + ")" ;
}
table.put(label, new Integer(value));
additem(label);
}
setSelectedltemdabels [0]);
}
/**
Возвращение значения поля, выбранного пользователем @return the static field value
*/
public int getValue()
return ((Integer)table.get(getSelectedltem())).intValue();
)
private Map table = new HashMap();
}
E
^Sj a.va. text. Collator
I I j • Метод static Locale [] getAvailableLocales () возвращает массив —»объектов Locale, для которых существуют объекты-сортировщики
Collator.
Метод staticCollatorgetlnstance ()возвращает объект-
сортировщик для текущего регионального стандарта.
Метод static Collator getlnstance (Locale 1) возвращает объект- сортировщик для заданного регионального стандарта 1.
Метод int compare (String a, String b) возвращает либо отрицательное значение, если а предшествует b; либо значение 0, если строки считаются идентичными; либо положительное значение, если b предшествует а.
Метод boolean equals (String a, String b) возвращает либо значение true, если строки а и b считаются идентичными, либо значение falseв противном случае.
Метод void setStrength(int strength) устанавливает один из следующих уровней избирательности сортировки: Collator.PRIMARY, Collator.SECONDARYи Collator.TERTIARY.
Метод void getStrength(int strength) возвращает уровень избирательности сортировки.
Метод void setDecomposition(int decomp) устанавливает один из следующих режимов декомпозиции строк: Collator.NO_DECOMPOSITION, Collator.CANONICAL-DECOMPOSITION, Collator.FULL-DECOMPOSITION. Чем выше степень декомпозиции, тем строже выполняется сравнение строк.
Метод int getDecomposition () возвращает режим декомпозиции строк.
МетодCollationKey getCollationKey(String а) возвращает ключ сравнения с результатами декомпозиции строки, которые используются для быстрого сравнения с другим ключом сравнения.
java. text. CollationKey-
u • Метод int compareTo (CollationKey b) возвращает либо отрицательное значение, если данный ключ предшествует ключу b; либо значение 0, если ключи идентичны; либо положительное значение, если данный ключ располагается вслед за ключом b.
Границы текста
Рассмотрим предложение на произвольном языке и попробуем разбить его на слова. На первый взгляд решение этой задачи кажется тривиальным, но как и в случае с сортировкой, при использовании разных локальных языков все оказывается не так просто. На самом деле задача решается гораздо сложнее, чем может показаться, потому что необходимо учитывать проблему определения начала и конца символа. Например, разбиение строки Hello на отдельные символы не составляет труда: Н | е |1|1|о. Но наличие ударений приводит к получению неоднозначных результатов. Например, для буквы а существуют два способа описания: непосредственно в виде символа а (с Unicode-кодом \u00E4) или в виде символа а, который логически связан со следующей за ним тремой " (с Unicode-кодом \u0308). Другими словами, строка из четырех Unicode-символов Ba"r может быть представлена как последовательность трех логических символов: В | а" | г. Но даже эта задача решается гораздо проще, чем разбиение слов на одном из восточных языков, например, на корейском языке.
Вернемся к задаче разбиения предложения на отдельные слова. Границами слов являются начало и конец слова. В английском языке, как впрочем и во многих других языках, слова являются непрерывными последовательностями букв. Попробуем разбить на слова следующее предложение.
The quick, brown fox jump-ed over the lazy dog.
Вот как будет выглядеть полученный результат разбиения.
The | |quick|,| | brown | |fox| [jump-ed| | over | | the | |lazy| |dog.|
(Дефис в слове jump-ed показывает место мягкого переноса.)
Границами строк являются места, в которых можно выполнить перенос строки при отображении на экране или при печати. В английском тексте перенос строк организован довольно просто, потому что строку разрешается переносить перед словом или после дефиса. Например, в приведенном выше примере переносы строк могут располагаться в следующих местах:
The |quick, |brown | f ox |jump-|ed |over |the |lazy |dog.|
Если определение границ символов, слов и строк довольно просто выполняется для европейских и азиатских идеографических систем письма, то этого нельзя сказать о других системах письма, например, системы Деванагари, которая применяется в классическом языке санскрит и современном языке хинди.
В заключение рассмотрим границы предложений. В английском языке конец предложения обозначается точкой, знаком восклицания или вопросительным знаком.
Для поиска границ текста, т.е. границ символов, слов, строк и предложений, предусмотрен класс-итератор разрывов Breakiterator. Он широко используется для создания программ с функциями редактирования, отображения и печати текста.
К счастью, итератор разрывов не разрывает предложения вслепую после каждой точки, потому что способен различить точку внутри кавычек (в соответствии с правилами английского правописания) или многоточие. Рассмотрим следующий фрагмент текста.
The quick, brown fox jumped over the lazy "dog." And then ... what happened?
Итератор разрывов может разбить его на следующие два предложения.
The quick, brown fox jumped over the lazy "dog." |And then ... what happened?|
В листинге 10.4 приводится пример программы с использованием итераторов разрывов. Как всегда, сначала нужно получить итератор разрывов с помощью статичного шаблонного метода. Для определения границ текста можно последовательно использовать каждый из четырех имеющихся итераторов для работы с символами, словами, строками или предложениями. Следует отметить, что каждый отдельный объект-итератор разрывов может работать только с той логической структурой, для которой был создан. Например, для анализа отдельных слов следует использовать приведенный ниже код.
Locale loc = . . . ;
Breakiterator wordlter =
После получения объекта-итератора ему следует указать анализируемую строку.
String msg = "The quick, brown fox" ;
wordlter.setText(msg);
Далее для определения смещения первой границы слова в данной строке вызывают метод first.
int f = wordlter.firstО; // возвращает З
В этом примере метод firstвозвращает значение 3, т.е. положение первого пробела в строке. Для получения смещений для следующих границ нужно последовательно вызывать метод next. Если метод next возвращает значение, равное константе Breakiterator.DONE, то это означает, что в данной строке больше нет границ слов.
The quick, brown fox jump-ed over the lazy "dog.* And then.. .what happened?The quick, brown fox jump-ed over the lazy "dog.* And then.. .what happened?^^n-W/0*" 1,011 UumP-l=d n l"dog." |And |then...|what|happe e^^n-W/0*" 1,011 UumP-l=d n l"dog." |And |then...|what|happe eРис. 10.4. ПрограммаTextBoundaryTestРис. 10.4. ПрограммаTextBoundaryTest
The quick, brown fox jump-ed over the lazy "dog.* And then.. .what happened?
The quick, brown fox jump-ed over the lazy "dog.* And then.. .what happened?
^^n-W/0*" 1,011 UumP-l=d n l"dog." |And |then...|what|happe e
^^n-W/0*" 1,011 UumP-l=d n l"dog." |And |then...|what|happe e
Рис. 10.4. ПрограммаTextBoundaryTest
Рис. 10.4. ПрограммаTextBoundaryTest
Вот как можно было бы организовать циклический поиск остальных разрывов слов. int to;
// какие-то действия со значением to// какие-то действия со значением towhile ((to = currentBreaklterator.next()) != Breakiterator.DONE) ( }
// какие-то действия со значением to
// какие-то действия со значением to
На рис. 10.4 показан внешний вид программы, которая позволяет вводить текст в верхней части формы и выбрать способ его разбиения (на символы, слова, строки или предложения). После этого в нижнем текстовом поле текст будет отображен вместе с заданными границами разбиения.
Листинг 10.4.TextBoundaxyTeat.java“ ’•'<• ’’
/**
1.11 2001-08-27
Cay
*/
import
import
import
import
import
/**
Пример разбиения текста при использовании
разных региональных стандартов
*/
public class
{
public static voidargs)
{
= new
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
/**
Этот фрейм содержит группу переключателей для выбора способа разбиения текста, список региональных стандартов, текстовое поле для ввода текста, а также текстовое поле для отображения текста с разрывами
*/
class TextBoundaryFrame extends JFrame {
public
{
HEIGHT);
setTitle("TextBoundaryTest");
ActionListener listener = new
ActionListener() {
public void actionPerformed(ActionEvent event) {
updateDisplay();
};
JPanel p = new characterRadioButton, rbGroup, rbGroup, listener);
lineRadioButton, rbGroup, listener);
addRadioButton(p, sentenceRadioButton, rbGroup, listener);
getContentPane().setLayout(newGridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill =
gbc.anchor =
add (new JLabel ("Locale”) , gbc, 0, 0, 1, 1);
gbc.anchor =
add(localeCombo, gbc, 1, 0, 1, 1);
add(p, gbc, 0, 1, 2, 1);
gbc.fill = GridBagConstraints.BOTH;
gbc.weighty = 100;
add(new JScrollPane(inputText) , gbc, 0, 2, 2, 1);
add (new JScrollPane (outputText) , gbc, 0, 3, 2, 1);
locales = Breakiterator.getAvailableLocales();
for (int i = 0; i < locales.length; i++)
localeCombo.additem(locales[i].getDisplayName() ) ;
localeCombo.setSelectedltem(
Locale.getDefault().getDisplayName());
localeCombo.addActionListener(listener);
inputText.setText("The quick, brown fox jump-ed\n"
+ "over the lazy \"dog.\" And then...what happened?"); updateDisplay();
Вставка переключателя в контейнер
Sparam p the container into which to place the button
Sparam b the button
@param g the button group
Sparam listener the button listener
*/
public void addRadioButton(Container p, JRadioButton b, ButtonGroup g, ActionListener listener)
{
b.setSelected(g.getButtonCount() ==0);
b.addActionListener(listener) ;
g.add(b);
p.add(b);
/**
Вставляет компонент в заданное место решетки
Sparam с the component to add
Sparam constraints the grid bag constraints to use
Sparam x the x grid position
gparam у the у grid position
gparam w the grid width
gparam h the grid height
*/
public void add(Component c, GridBagConstraints gbc,
int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane().add(с,gbc);
}
/**
Обновление отображения и сортировки строк в соответствии с установками пользователя */
public void updateDisplay() {
Locale currentLocale = locales[
localeCoinbo.getSelectedlndex()];
Breaklterator currentBreaklterator = null;
if (characterRadioButton.isSelected())
currentBreaklterator
=,
else if (wordRadioButton.isSelected())
currentBreaklterator =
else if (lineRadioButton.isSelected()) currentBreaklterator =
else if (sentenceRadioButton.isSelected()) currentBreaklterator =
String text = inputText.getText();
currentBreaklterator.setText(text);
outputText.setText("");
int from = currentBreaklterator.first ();
int to;
while ((to = currentBreaklterator.next()) != Breakiterator.DONE)
{ outputText.append(text.substring(frcto) + " | ") ; from = to;
)
outputText.append(text.substring(from));
}
private Locale[] locales;
private Breaklterator currentBreaklterator;
private JComboBox localeCombo = new JComboBoxt);
private JTextArea inputText = new JTextArea(6, 40);
private JTextArea outputText = new JTextArea(6, 40);
private ButtonGroup rbGroup = new ButtonGroup() ;
private JRadioButton characterRadioButton
= new JRadioButton("Character");
private JRadioButton wordRadioButton
= new JRadioButton("Word") ;
private JRadioButton lineRadioButton
= new JRadioButton("Line");
private JRadioButton sentenceRadioButton
= new JRadioButton("Sentence");
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
java.text.Breakiterator
Метод static Locale [] getAvailableLocales () возвращает массив объектов Locale, для которых имеются объекты Breakiterators.
Метод static Breakiterator getCharacterInstance () возвращает итератор разрыва символов для текущего регионального стандарта.
Метод static Breaklterator getCharacterInstance (Locale 1) возвращает итератор разрыва символов для заданного регионального стандарта 1.
Метод static Breaklterator getWordlnstance () возвращает итератор разрыва слов для текущего регионального стандарта.
Метод static Breaklterator getWordlnstance(Locale 1) возвращает итератор разрыва слов для заданного регионального стандарта 1.
Метод static Breaklterator getLinelnstance() возвращает итератор разрыва строк для текущего регионального стандарта.
Метод static Breaklterator getLinelnstance(Locale 1) возвращает итератор разрыва строк для заданного регионального стандарта 1.
Метод static Breaklterator getSentencelnstance() возвращает итератор разрыва предложений для текущего регионального стандарта.
Метод static Breaklterator getSentencelnstance (Locale 1) возвращает итератор разрыва предложений для заданного регионального стандарта 1.
Метод void setText (String text) задает строку с анализируемым текстом.
Метод void setText(Characteriterator text) задает объект типа Characteriteratorс анализируемым текстом.
Метод Characterlterator getText О возвращает анализируемый текст.
Метод int first() перемещает текущую границу к месту первого возможного разрыва анализируемой строки и возвращает его индекс.
Метод int next () перемещает текущую границу к месту следующего возможного разрыва анализируемой строки и возвращает либо его индекс, либо значение Breakiterator.DONE, если достигнут конец строки.
Метод int previous () перемещает текущую границу к месту предыдущего возможного разрыва анализируемой строки и возвращает либо его индекс, либо значение Breakiterator.DONE, еслидостигнуто начало строки.
Метод int last () перемещает текущую границу к месту последнего возможного разрыва анализируемой строки и возвращает его индекс.
Метод int current () возвращает индекс текущей границы разрыва.
Метод int next (int n) перемещает текущую границу к n-му месту от текущего места и возвращает либо его индекс, либо значение Breakltera- tor. DONE, еслидостигнут конец или начало строки.
Метод int following (int pos) перемещает текущую границу к месту первого возможного разрыва анализируемой строки, которое располагается после смещения pos, и возвращает его индекс. Возвращаемое значение либо больше, чем pos, либо равно Breakiterator.DONE.
Форматирование сообщений
В программах автоматического составления стандартных писем с использованием фиксированных форм, подлежащих заполнению, часто можно встретить следующие конструкции.
"On {2}, а {0} destroyed {1} houses and caused {3} of damage. "
"{2}{0}уничтожил{1}домови нанесущербнасумму{3}."
Здесь номера в фигурных скобках используются как "заполнители" для реальных названий и значений. Действительно, этот метод прекрасно подходит для выполнения локализации определенного вида. Для форматирования текста по шаблону в Java предусмотрен класс MessageFormat, который используется следующим образом.
Сначала нужно создать шаблон в виде строки и разместить в нем до десяти заполнителей {0} . . .{9 }, которые можно использовать в этой строке более одного раза.
Затем нужно создать объект MessageFormat со строкой-шаблоном, передаваемой в качестве параметра конструктора объекта.
Далее создается массив объектов для замены заполнителей, причем число в фигурных скобках соответствует индексу в массиве объектов.
Наконец, вызывается метод formatс массивом объектов в качестве параметра.
На этапах 1 и 2 создается объект MessageFormat, который использует текущий региональный стандартдляформатированиячиселидат. Если требуется использовать другой региональный стандарт, необходимо сначала создать объект MessageFormat с параметром-заглушкой, затем вызвать метод setLocale для указания другого регионального стандарта и, наконец, вызвать метод applyPattern с фактическим шаблоном.
MessageFormat format = new MessageFormat("") ;
format.setLocale(locale) ;
format.applyPattern(pattern);
Рассмотрим типичный пример использования класса MessageFormat. Прежде нужно создать шаблон в виде строки и массив объектов для замены заполнителей.
String pattern =
"On {2}, а {0} destroyed {1} houses and caused {3} of damage";
MessageFormat msgFmt = new MessageFormat(pattern);
Obj ect[] msgArgs = {
"hurricane",
new Integer(99),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
);
String msg =
System.out.printin(msg);
Как уже говорилось выше, номер заполнителя относится к индексу в массиве объектов. Например, первый заполнитель {2} заменяется значением msgArgs [ 2 ]. Для корректного представления объектов следует помнить о необходимости использования соответствующих объектов-оболочек для передачи целых чисел (Integer) и чисел с плавающей запятой (Double). Обратите внимание на громоздкую конструкцию, используемую для передачи даты. Дело в том, что в методе format используется объект типа Date, но вместо конструктора Date (int, int, int) используется класс Calendar. Поэтому для преобразования объекта Calendar в объект Date применяется метод getTime.
В результате выполнения этого фрагмента кода будет получен следующий результат.
On 1/1/99 12:00 AM, a hurricane destroyed 99 houses and caused 100,000,000 of damage.
Попробуем улучшить полученный результат. Уберем время 12:00 AM, а сумму ущерба представим с указанием денежных единиц. Это можно сделать с помощью одного из следующих двух способов указания необязательного формата для некоторых или всех заполнителей.
Указание формата в строке-шаблоне.
Вызов метода setFormat или setFormats.
Начнем с первого более простого варианта. В строке-шаблоне можно указать формат для каждого отдельного заполнителя. В этом примере необходимо, чтобы первый заполнитель ({2}) имел формат даты без указания времени. А для четвертого заполнителя ({3}) требуется указать формат представления денежных сумм. Учтите, что фактическая нумерация заполнителей начинается с 0, поэтому в данном примере потребуется установить формат для заполнителей с порядковыми номерами 0 и 3. Для форматирования заполнителей используются методы объектов, которые уже рассматривались выше в этой главе, а именно, DateFormat.getDatelnstance(loc) и NumberFormat.getCurrencylnstance(loc), где loc— используемый региональный стандарт. Все объекты форматирования происходят от базового класса Format. Метод setFormat класса MessageText использует в качестве параметров порядковый номер заполнителя и применяемый для него формат.
Для получения заданного форматирования нужно просто указать форматы для заполнителей с номерами 0 и 3, а затем вызвать метод format.
msgFmt.setFormat(0, DateFormat.getDatelnstance(DateFormat.LONG, loc)); msgFmt.setFormat(3, NumberFormat.getCurrencylnstance(loc));
String msg = msgFmt.format(msgArgs) ;
System.out.printin(msg) ;
После внесенных изменений строка сообщения будет выглядеть следующим образом.
On January 1, 1999, a hurricane destroyed 99 houses and caused $100,000,000.00 of damage.
Во втором способе вместо указания формата для отдельных заполнителей можно просто собрать все форматы в одном массиве. Если для какого-то заполнителя не требуется задавать особый формат, то вместо него указывают значение null.
Format argFormats[] =
{
DateFormat.getDatelnstance(DateFormat.LONG,loc),
null,
null,
NumberFormat.getCurrencylnstance(loc)
};
msgFmt.setFormats(argFormats);
Обратите внимание на то, что элементы массивов msgArgs и argFormats не соответствуют друг другу. Дело в том, что индексы массива msgArgs соответствуют номерам внутри скобок {}, а индексы массива argFormats соответствуют фактическому расположению разделителей {} в строке сообщения. На первый взгляд такой порядок может показаться очень громоздким и путанным, но от связан с тем, что одни и те же заполнители могут повторяться в строке с указанием разного формата. Поэтому форматы заполнителей нумеруются по порядку от начала строки. Например, если в данном примере известно точное время описываемых в сообщении событий, то объект-дату можно использовать дважды: первый раз для представления собственно даты, а затем для представления времени.
String pattern =
"On {2}, а {0} touched down at {2} and destroyed {1} houses.";
MessageFormat msgFmt = new MessageFormat(pattern);
Format=
{
DateFormat.getDatelnstance(DateFormat.LONG, loc),
null,
DateFormat.getTimelnstance(DateFormat.SHORT, loc) , null
};
msg.setFormats(argFormats);
Object[] msgArgs = {
"hurricane",
new Integer(99),
new GregorianCalendar(1999, 0, 1, 11, 45, 0).getTime(), };
String msg = msgFmt.format(msgArgs);
System.out.println(msg);
В результате выполнения этого фрагмента кода строка сообщения будет выглядеть следующим образом.
On January 1, 1999, a hurricane touched down at 11:45 AM and destroyed 99 houses.
Обратите внимание, что заполнитель {2} был использован дважды и с разными форматами!
Кроме динамического указания форматов заполнителей, требуемый формат можно указать непосредственно в строке сообщения. Например, форматы даты и денежных сумм можно указать непосредственно в строке-шаблоне.
"On {2,date,long}, a {0} destroyed {1} houses and caused {3,пшпЬег,currency} of damage. "
При таком форматировании не требуется применять методы setFormat и setFormats, а сразу после индекса заполнителя следует указать тип и стиль заполнителя, разделенные запятыми. В качестве типа могут применяться следующие объекты.
number (число)
time (время)
date (дата)
choice (выбор формата)
В качестве стиля могут применяться такие объекты, например, для типа number. integer (целое число) currency (денежная сумма) percent (процент)
Кроме того, в качестве стиля может быть указан шаблон числового формата, например $,##0. (Шаблоны числового формата подробно рассматриваются в главе 3 тома I.)
Для типа time или date стиль может иметь следующий вид.
short (короткий) medium (средний) long (длинный) full (полный)
Аналогично числам, в качестве стиля может быть указан шаблон даты. (Возможные форматы более подробно рассматриваются в документации при описании класса SimpleDateFormat.)
Форматы выбора имеют более сложную структуру и подробно рассматриваются далее в главе.
java.text.MessageFormat
Метод MessageFormat (String pattern) создает объект форматирования сообщения по указанному шаблону.
Метод void applyPattern(String pattern) задает шаблон для объекта форматирования сообщения.
Метод void setLocale(Locale loc) задает региональный стандарт для заполнителей сообщения. Он используется только для всех последующих шаблонов, заданных с помощью метода applyPattern.
Метод Locale getLocale () возвращает региональный стандарт для заполнителей сообщения, а также используется только для всех последующих шаблонов, заданных с помощью метода applyPattern.
Метод void setFormats(Format [] formats) задает форматы заполнителей сообщения.
Метод Format [] getFormats() возвращает форматы заполнителей сообщения.
Метод void setFormat (int i, Format format) задает формат i-го заполнителя в строке сообщения.
Метод String format(String pattern, Object[] args) возвращает строку, отформатированную no шаблону pattern, заменяя в нем заполнители {i} значениями объектов из массива args [ i ].
Условия выбора формата
Рассмотрим внимательнее шаблон из предыдущего раздела.
"On{2}, а{0}destroyed{1}housesandcaused{3}ofdamage."
Если заполнитель {0} с названием стихийного бедствия заменить значением earthquake (землетрясение), то получится предложение с нарушением правил английской грамматики.
On January 1 / 1999, a earthquake destroyed . . .
Для решения этой проблемы в заполнитель придется включить артикль а.
"On (2), (0) destroyed {1} houses and caused {3} of damage."
Теперьзаполнитель{0}будетзаменятьсязначениямиahurricaneилиan
earthquake. Этот способ особенно удобен для перевода сообщений на языки, в которых с каждым родом употребляется отдельный артикль. Например, на немецком языке этот шаблон должен выглядеть так, как показано ниже.
"{0}zerstorteam{2}{1}Hauserundrichtete einen Schadenvon{3}an."
В этом случае заполнители будут заменяться на грамматически правильные комбинации артикля и имени существительного, например, Ein Wirbelsturm и Eine Naturkatastrophe.
Если стихийное бедствие оказалось не очень разрушительным, то заполнитель {1} может содержать значение 1. В таком случае снова получится предложение с нарушением правил английской грамматики.
On January 1, 1999, a mudslide destroyed 1 houses and ...
(1 января 1999 оползень уничтожил 1 домов и . . .)
Желательно, чтобы сообщение грамотно изменялось в соответствии со значением заполнителя.
no houses (ни одного дома) one house (один дом)
2 houses (два дома)
Для решения данной проблемы предусмотрен класс ChoiceFormat, в котором для определения условий выбора формата используются два массива:
массив предельных значений(limits');
массив строк подстановки(format strings).
Double[] limits = ... ;
String!] formatstring = ...;
ChoiceFormat choiceFmt = new ChoiceFormat(limits, formatstrings);
double input = ...;
String s = choiceFmt.format(input);
Массивы limits и formatStrings должны иметь одинаковую длину, а предельные значения в массиве limitsдолжны располагаться в порядке возрастания. Метод format проверяет, в каких пределах находится обрабатываемое число input. Если выполняется приведенное ниже условие, то в строке сообщения используется значение formatstrings[i],
limits[i] <= input && input < limits[i + 1]
Если обрабатываемое число больше последнего предельного значения, то используется последняя строка подстановки. Если обрабатываемое число меньше, чем limits [ 0 ], то для подстановки используется строка formatstrings [ 0].
Рассмотрим следующие предельные значения и строки подстановки.
doublet] limits = (0, 1, 2};
String!] formatString = {"no houses" "one house", "many houses"};
В табл. 10.4 представлены возможные результаты выполнения метода format для приведенных выше предельных значений и строк подстановки.
String selected = choiceFmt.format(input);
Таблица 10.4. Пример выбора строк с помощью класса
ChoiceFormat
Значение заполнителя (input)
Результат выбора
input < О
"no houses" (ни одного дома)
0 <= input && input < 1
"no houses" (один дом)
1 <= input && input < 2
"one house" (один дом)
2 <= input
"many houses" (много домов)
Этот пример демонстрирует, что структура класса ChoiceFormat недостаточно четко продумана его разработчиками. Дело в том, что для разделения трех строк требуется всего два предельных значения. В общем случае, необходимое количество предельных значений на единицу меньше, чем количество строк. Поэтому первое предельное значение бессмысленно, а значит, для первых двух предельных значений можно указать одно и тоже число, как показано ниже.
double[] limits = {1, 1, 2};
Stringt] formatStrings = {"no houses", "one house", "many houses"}; ChoiceFormat choiceFmt = new ChoiceFormat(limits, formatStrings);
В данном примере вместо значения many houses (много домов) нужно вернуть конкретное количество пострадавших домов в правильном формате, например так, как показано ниже.
doublet] limits = (0, 1, 2);
String[] formatStrings ="one house", "{1} houses"};
ChoiceFormat choiceFmt = new ChoiceFormat(limits, formatStrings);
msgFmt.setFormat(2, choiceFmt);
Таким образом для третьего заполнителя (учтите, что нумерация заполнителей начинается с нуля) создается объект выбора формата.
Обратите внимание, что в строке подстановки указан заполнитель {!}. После выбора этой строки подстановки возвращается значение {1} houses, далее эта строка снова форматируется с указанием конкретного количества пострадавших домов вместо заполнителя {1}, и полученная фактическая строка включается в итоговый результат. Как правило, следует всегда возвращать те же заполнители, которые были использованы при выборе строки подстановки, так как в противном случае можно получить неожиданные результаты.
Информацию о форматировании можно указать непосредственно в строке выбора формата.
String[] formatStrings = {"no houses", "one house", "{1, number, integer) houses"} ;
Как показано выше, условия выбора формата можно указать непосредственно в строке подстановки. Допустим, что в качестве типа формата выбрано значение choice. Тогда следующий параметр может содержать разделенные символом | пары, состоящие из разделенных символом # предельных значений и строк подстановки. Вот как будут выглядеть строки выбора количества домов в этом представлении.
(1,choice,0#no houses|l#one house|2#{l} houses}
Таким образом заданы следующие три строки выбора.
0#no houses
l#one house
2#{1} houses
Первая строка используется, если значение заполнителя меньше 1, вторая— если значение заполнителя не меньше 1, но меньше 2, а третья — если значение заполнителя не меньше 2.
• Как уже говорилось выше, первое предельное значение не имеет никакого смысла. Однако, в данном представлении нельзя задавать одинаковое значение для первого и второго предельного значения. Например, во время синтаксического анализа показанной ниже строки будет получено сообщение об ошибке.
l#no houses|l#one house | 2#{l} houses
В таком случае для первого предельного значения необходимо указать такое число, которое будет строго меньше второго предельного значения. В качестве первого предельного значения можно даже использовать отрицательное бесконечное значение —<», используя предусмотренный для него Unicode-символ —\u22lE.
Синтаксис этой конструкции был бы намного проще, если бы разработчики этого класса учли, что предельные значения располагаются между вариантами выбора, например так, как показано ниже.
no houses|1|one house|2|{l} houses
// He используемый, но более понятный формат
Для явного указания того, что левый предел строго меньше правого можно использовать<.
Символ < преобразуется в предельное значение, представляющее следующее число с плавающей запятой (добавляется минимальное приращение) после указанного в строке выбора. Для вычисления таких чисел существует даже статический метод ChoiceFormat.nextDouble.
В качестве синонима для символа # также можно использовать символ < с Unicode- кодом
Рассмотрим приведенные ниже примеры эквивалентного представления условий выбора строк подстановки.
(0,choice,-\u221E<negative|0\u2264zero| expositive)
(0, choice, -oocnegative | 0<zero | expositive)
Указанные в них символы определяют следующие результаты выбора.
Значениебудет выбрано, еслизначение заполнителя < 0.
Значение zero будет выбрано, если 0 < значение заполнителя < ChoiceFormat.nextDouble(0).
Значение positive будет выбрано, если ChoiceFormat(0)зна
чения заполнителя.
Вернемся к шаблону строки для представления последствий стихийного бедствия. Если поместить строку с условиями выбора внутри исходной строки сообщения, то получится довольно громоздкая конструкция.
String pattern =
"On{0} destroyed
house|2#{l} houses}
and causedof damage.";
В немецком варианте она будет иметь следующий вид.
String pattern =
{0} zerstorte amHaus|l#ein
Haus|2#{l} Hauser}
undeinen Schaden vonan.";
Примечательно, что последовательность слов в английском и немецком вариантах разная, но методу format передается тот же массив объектов. Под требуемый порядок слов подстраивается только последовательность появления заполнителей.
java.text.ChoiceFormat
Метод ChoiceFormat (String pattern) создает объект с условиями выбора формата по шаблонной строке, состоящей из набора разделенных символом |
МетодStringformat
Strings [] создает объект с условиями выбора формата на основе предельных значений и строк подстановки. Предельные значения должны быть указаны в порядке возрастания. Для значения заполнителя input будет выбрана строка подстановки formatString[i], где i — минимальный индекс, при котором удовлетворяющий неравенству limits [i] <=
input. Если значение заполнителя input меньше, чем limits [1], то будет выбрана строка подстановки formatstrings [ 0 ].
Метод static double next Double (double x) возвращает наименьшее число с плавающей запятой двойной точности, которое строго большее, чем х. Например, при вызове nextDouble (О) будет возвращено значение 4, 9Е-324.
Преобразование символьных кодировок
Как уже было сказано выше, язык программирования Java полностью основан на кодировке Unicode. Однако, во многих операционных системах используются свои собственные доморощенные варианты кодирования символов, часто несовместимые с другими, например, ISO 8859-1 (8-битная кодировка, иногда называемая также ANSI- кодировкой) в США или BIGS в Тайване. По этой причине вводимый пользователем текст может оказаться в какой-то другой кодировке, а выводимые программой сообщений в конечном счете должны соответствовать той кодировке, которая используется в данной операционной системе.
Все символы внутри программы всегда следует использовать в кодировке Unicode в надежде, что реализация виртуальной машины Java на конкретной платформе способна успешно преобразовывать ввод/вывод для используемой кодировки и Unicode. Например, задача правильного отображения надписи на кнопке, заданной в кодировке Unicode, полностью ложится на плечи виртуальной машины Java. Аналогично при получении текста из текстового поля метод getText возвращает строку в кодировке Unicode не зависимо от способа ее ввода пользователем.
Следует очень осторожно обрабатывать текстовые файлы и ни в коем случае не считывать их побайтово! Для этого рекомендуется всегда использовать классы InputStreamReader или FileReader, описанные в главе 12 тома 1. Эти классы автоматически преобразовывают в Unicode текст, выраженный в некоторых кодировках. При этом по умолчанию используется локальная кодировка. Как было указано в главе 12 тома 1, нужную кодировку можно указать явно в конструкторе класса InputStreamReader.
InputS t reamReader in = new InputStreamReader(in, "8859_1");
К сожалению, пока не существует инструментов для организации связи между региональными стандартами и кодировками символов. Например, при выборе традиционного китайского стандарта zh_TW в Java не предусмотрен метод, который сообщил бы, что наиболее подходящей кодировкой в этом случае является BIG5.
При работе с текстовыми файлами необходимо учесть следующие особенности.
Будут ли с этим текстовым файлом работать локальные пользователи или программы?
Предназначен ли данный текстовый файл только для этой программы или еще для какой-то другой программы?
Если текст предназначен для восприятия человеком или для программы, не поддерживающей Unicode, то потребуется представить текст в локальной кодировке с помощью метода Printwriter,как показано в главе 12 тома 1. Иначе, можно использовать метод writeUTF класса DataOutputStream для записи текста в специальном
текстовом Unicode-формате (Unicode Text Format — UTF). Аналогично для чтения такого файла необходимо использовать метод readUTF класса DatalnputStream.
Для ввода данных в Java-программы рекомендуется использовать инструменты сериализации, которые автоматически позаботятся о сохранении и считывании строк.
Учтите, что при использовании потоков DataOutputStream и DatalnputStream результат будет представлен в форме, неудобной для восприятия человеком.
Локализация и исходный код
Не следует забывать о том, что программист во время создания программы вынужден работать с Java-компилятором с учетом региональных настроек локальной системы. Предположим, что для создания исходных файлов Java-программы используется стандартная программа Notepad (Блокнот) в китайской версии операционной системы Windows. Полученный в результате этого исходный код не является переносимым (т.е. независимым от используемой платформы) из-за использования локальной кодировки символов (GB или BIG5, в зависимости от типа региональных настроек операционной системы). Классы становятся переносимыми только после компиляции, так как в них используется UTF-кодировка для названий идентификаторов и текстовых сообщений. Это означает, что даже для компиляции и запуска программы применяются три кодировки символов.
Исходные файлы: локальная кодировка.
Файлы классов: UTF-кодировка
Виртуальная машина: Unicode-кодировка.
Для того, чтобы сделать исходные файлы переносимыми, надо ограничиться применением только ASCII-символов. Иными словами, необходимо заменить все не-ASCII символы их эквивалентным представлением в кодировке Unicode. Например, вместо строки Hauser следует использовать строку H\u0084user. Для преобразования представленных в локальной кодировке текстов в виде ASCII-символов можно использовать утилиту native2ascii, которая предлагается в составе SDK. Эта утилита просто заменяет каждый не-ASCII символ последовательностью \uXXXX, где ХХХХ— четыре шестнадцатеричные цифры Unicode-кода этого символа. При использовании программы native2ascii в командной строке надо указать имя исходного и результирующего файлов.
native2ascii Myfile.java Myfile.temp
Для обратного преобразования следует использовать параметр —reverse.
native2ascii —reverse Myfile.java Myfile.temp
Для указания кодировки предусмотрен параметр —encoding, за которым следует название кодировки. Допустимые названия кодировок приведены в главе 12 тома I.
native2ascii —encoding Cp437 Myfile.java Myfile.temp
В заключение, настоятельно рекомендуется ограничиться использованием ASCII- символов в именах классов. Название файла класса происходит от имени соответствующего класса, поэтому правильная обработка не-ASCII символов в названиях зави-
сит от операционной системы. К большому сожалению, в европейской версии Windows 95 для записи имен файлов используется кодировка Code Page 437 (original PC). Из- за этого Windows 95 всегда пытается преобразовывать имена файлов в ANSI- кодировку и обратно в отличие от загрузчика классов виртуальной машины Java. (В этом отношении операционная система NT работает намного лучше.) Например, для класса с именем Bar загрузчик классов JDK выдаст сообщение об ошибке о том, что "не найден класс B£r”. Такому поведению есть объяснение, но толку от этого мало. Пока компьютеры во всем мире не будут обеспечивать хорошую поддержку Unicode- кодировки, в названиях классов рекомендуется использовать ASCII-символы.
Пакеты ресурсов
При локализации приложений приходится переводить огромное количество сообщений, надписей и т.п. Для упрощения задачи рекомендуется собрать строки сообщений в отдельном месте, которое называется ресурсом (resource). В этом случае переводчику достаточно отредактировать файлы ресурсов, не трогая исходный код программы.
Технология использования ресурсов в Java отличается от технологии использования ресурсов в операционных системах Windows и Macintosh. В исполняемой программе операционной системы Windows такие ресурсы, как меню, диалоговые окна, пиктограммы и сообщения, хранятся отдельно от программного кода. Поэтому специальный редактор ресурсов может просматривать и изменять ресурсы без изменения программного кода.
К сожалению, в технологии использования ресурсов в Java не предусмотрены механизмы хранения внешних ресурсов в текстовых файлах. Вместо этого все данные- ресурсы должны быть размещены в классе в виде статических переменных или возвращаемых методами значений. Для каждого регионального стандарта создается отдельный класс, а для автоматического поиска подходящего класса для необходимого регионального стандарта используется метод getBundle класса ResourceBundle.
В главе 10 тома I описывается концепция использования ресурсов, позволяющая размещать файлы данных, звуки и изображения в JAR-файлах. Метод getResource класса Class находит файл, открывает его и возвращает URL-указатель информационного ресурса. Работающая с файлами программа должна уметь находить эти файлы. При размещении файлов в JAR-архивах задачу нахождения файлов выполняет загрузчик классов, который умеет находить файлы классов. Хотя этот механизм не поддерживает непосредственно локализацию, он все же позволяет находить файлы свойств с региональными установками, которые подробно описываются в следующем разделе.
Поиск ресурсов
Для локализации приложений требуется создать набор классов, который описывает локализованные элементы (например, сообщения, надписи и т.д.) для каждого поддерживаемого регионального стандарта. Для каждого такого класса в качестве базового класса следует использовать класс ResourceBundle. (Более подробно способы создания таких классов описываются ниже.) При этом необходимо соблюдать соглашение об
именах классов, которое соответствует общепринятому обозначению регионального стандарта. Например, класс с локализованными для Германии элементами может называться ProgramResources_de_DE, а общие элементы для стран, в которых используется немецкий язык, могут размещаться в классе ProgramResources_de. Аналогично для Тайваня ресурсы размещаются в файле ProgramResources_zh_TW, а общие элементы для стран, в которых используется китайский язык, — в файле ProgramResources_zh. В общем случае для имени файла ресурсов конкретной страны используется формат РгодгатРезоигсез_язык_страна, а для имени файла ресурсов конкретного языка используется формат ProgramResourceз_язык_страна. В качестве альтернативного варианта можно разместить все англоязычные строки и сообщения в классе с именем ProgramResources (без каких-либо суффиксов). Далее классы компилируются и размещаются вместе со всеми другими классами данного проекта.
Для загрузки пакета ресурсов используется метод getBundle.
=
ResourcesBundle.getBundle("ProgramResources", currentLocale);
Метод getBundle пытается загрузить класс, который соответствует текущему региональному стандарту по языку, расположению и вариантному параметру. При отсутствии такого класса предпринимается попытка загрузки класса, который соответствует текущему региональному стандарту по языку и расположению (с отбрасыванием суффикса вариантного параметра) и т.д. Таким образом, метод getBundle попытается загрузить первый доступный класс из перечисленных ниже классов.
РгодгатРезоигсез_язык_с трана_вариа нт
РгодгатРезоигсез_язык_страна
ProgramResources_язык
ProgramResources
Если все эти попытки окажутся неудачными, то метод getBundle начинает все сначала, но уже не для текущего регионального стандарта, а для предлагаемого по умолчанию регионального стандарта. Если и в этом случае ни один подходящий класс не будет найден, то метод getBundle генерирует исключительную ситуацию MissingResourceException.
Даже если метод getBundle находит класс, например, ProgramResourc es_de_DE, он продолжает искать классы ProgramResources_de и ProgramResources. Если такие классы существуют, то они становятся родительскими классами по отношению к классу ProgramResources_de_DE в иерархии ресурсов. Родительские классы нужны в тех случаях, когда необходимый ресурс не найден в его дочернем классе. Другими словами, если нужный ресурс не найден в классе ProgramResources_de_DE, производится поиск в классах ProgramResources_de и ProgramResources.
Механизм использования ресурсов Java позволяет находить наиболее подходящую информацию для конкретного регионального стандарта. Очевидно, что это очень полезный механизм, для реализации которого программисту придется выполнить большой объем рутинной работы. Для включения в существующую программу новых локальных настроек необходимо всего лишь добавить соответствующие классы с локальными ресурсами.
Рассмотрим теперь способы размещения зависящей от языка информации в классе ресурсов. Разумеется, было бы неплохо, если бы существовали простые инструменты, с помощью которых даже неопытный программист мог бы создавать и изменять ресурсы. Остается только надеяться и ждать, что разработчики интегрированных сред программирования со временем предоставят такую возможность. В следующем разделе описываются способы программного создания и обслуживания файлов ресурсов.
Размещение ресурсов в пакетах
В языке программирования Java ресурсы размещаются внутри классов-наследников класса ResourseBundle. Каждый пакет ресурсов содержит таблицу подстановки. При создании программы для каждого локализованного элемента задается ключевая строка, которая используется для получения фактического локализованного значения. Для извлечения ресурса-строки используется метод getString.
String computeButtonLabel =
Пакет ресурсов может содержать объекты любого типа, а не только локализованные строки! Для извлечения произвольных объектов из пакетов ресурсов используется метод getObject.
Color backgroundcolor
=") ;
doublet] paperSize
=;
Нет необходимости размещать все ресурсы приложения в одном пакете ресурсов.
Отдельные пакеты ресурсов можно использовать для надписей на кнопках, для со- Dобщений об ошибках и т.д.
Для создания собственного класса пакета ресурсов необходимо реализовать сле- дующиедваметода.
Enumeration getKeys();
Object handleGetObject(Stringkey);
Для извлечения объектов методы getObject и getString используют предоставленный им метод handleGetObj ect. Ниже приведены примеры реализации классов пакетов для немецких и английских ресурсов.
Public class ProgramResources extends ResourceBundle
// Размещает метод getKeys в общем суперклассе {
public Enumeration getKeys()
{ return Collections.enumeration(Arrays.asList(keys));
}
private String[] keys = { "computeButton”, "backgroundColor" "defaultPaperSize" };
}
public class ProgramResources_de extends ProgramResources {
public Object handleGetObject(String key)
{
if (key.equals("computeButton"))
return "Rechnen";
else if (key.equals("backgroundcolor"))
return Color.black;
else if (key.equals("defaultPaperSize"))
return new doublet] { 210, 297 };
}
public class ProgramResources_en_US extends PrograinResources
{
public Object handleGetObject(String key)
{
if (key.equals("computeButton") )
return "Compute";
else if (key.equals("backgroundcolor"))
return Color.blue;
else if (key.equals("defaultPaperSize")) return new doublet] { 216, 279 };
}
Во всем мире, за исключением США и Канады, используется формат бумаги ISO 216. (Более подробную информацию о форматах бумаги можно получить по адресу http: //www.cl.cam.ac.uk/~mgk25/iso-paper.html.) По данным Метрологической ассоциации США (U.S. Metric Association, http://lamar.colostate.edu/~hillger) только три страны официально не приняли метрическую систему, а именно: Либерия, Мьянма (Бирма) и США. Для расширения рынка сбыта своей продукции США собирается перейти к использованию метрической системы. Множество ссылок на источники полезной информации о международной системе единиц SI можно найти по адресу http://ts.nist.gov/ts/htdocs/200/202/mpo reso.htm.
Конечно, создавать такой код для каждого пакета ресурсов довольно утомительно. Для облегчения этой работы в стандартной библиотеке Java предусмотрены классы ListResourceBundleи PropertyResourceBundle.
Класс ListResourceBundle позволяет размещать все ресурсы в массиве объектов и автоматически осуществлять их подстановку. Класс РгодгашКе5оигсеэ_язык_страна имеет следующую структуру.
public class РгодгашКеэоигсе5_язык_страна
extends ListResourceBundle
{
public Object!][] getContents() { return contents; } private static final Object[][] contents =
{
// Здесь размещается информация о локализации
}
Например, общие элементы для стран, в которых используется немецкий язык, могут размещаться в классе ProgramResources_de.
public class ProgramResources_de
extends ListResourceBundle
{
public Object[][] getContents() { return contents; } private static final Object[][] contents =
{
{ "computeButton", "Rechnen" },
{ "backgroundColor", Color.black },
{ "defaultPaperSize", new doublet] { 210,297 } }
}
}
public class ProgramResources_en_US
extends ListResourceBundle
{
public Qbj ect[][] getContents() { return contents; }
private static final Object[][] contents =
{ "computeButton", "Compute" },
{ "backgroundColor", Color.blue },
{ "defaultPaperSize", new double[] { 216,279 } }
)
}
Следует отметить, что в этом случае метод getObj ect не создается, так как он уже содержится в суперклассе ListResourceBundle библиотеки Java.
Если все ресурсы являются строками, то можно использовать альтернативный вариант на основе более простого механизма PropertyResourceBundle. Все ресурсы-строки no- мещаются в файл свойств, как это описано в главе 2, "Коллекции". Он представляет собой простой текстовый файл, состоящий из строк типа ключ = значение.
computeButton=Rechnen backgroundcolor=black defaultPaperSize=210x297
Имена для файлов свойств присваиваются в соответствии со следующим соглашением об именах.
ProgramStrings.properties
ProgramStrings_en.properties
ProgramStrings_de_DE.properties
Для загрузки ресурсов используется метод getBundle.
ResourceBundle bundle
=locale);
Метод getBundle находит подходящий файл свойств и автоматически формирует на его основе пакет PropertyResourceBundle.
Возможность размещения всех ресурсов в текстовом файле выглядит чрезвычайно привлекательно. Такая форма представления ресурсов намного проще для редактирования, особенно если переводчик не знаком с языком программирования Java. Обратная сторона медали заключается в том, что в этом случае программа должна производить синтаксический анализ строк (например, определять размер бумаги на основе строки 210x297, как в рассмотренном выше фрагменте кода). Вследствие этого лучшим решением является размещение ресурсов-строк в файлах свойств, а прочих объектов — в пакете ListResourceBundle.
Файлы свойств могут содержать только 7-битные ASCII-символы. А для размещения в них символов в Unicode-кодировке следует использовать формат \uxxxx. Например, строка colorName=Grun будет иметь следующий видcolorNaine=Gr\uOOFCn. Для такого преобразования можно использоватьутилитуnative2ascii.
В этом разделе описаны программные способы создания пакетов ресурсов. Конечно, в основном их применение связано с большим объемом рутинной работы. Для упрощения задачи создания и поддержки пакетов ресурсов предсумотрен специализированный пакет инструментальных средств локализации Java Internationalization and Localization Toolkit (http: //java. sun. com/products/jilkit).
java.util.ResourceBundle
Метод static ResourceBundle getBundle(String baseName, Locale loc) загружает класс пакета ресурсов с заданным именем и для заданного регионального стандарта, а также его родительские классы. Если классы пакетов расположены в Java-пакете, то имя класса пакета ресурсов должно содержать полное ИМЯJava-пакета, например inti.Programresources. Классы пакетов ресурсов должны быть объявлены открытыми (public) для предоставления доступа к ним со стороны метода getBundle.
Метод static ResourceBundle getBundle(String baseName) загружает класс пакета ресурсов с заданным именем и используемым по умолчанию региональным стандартом.
МетодObject getObject (String name) выполняет подстановку объекта из класса пакета ресурсов или его родительских классов.
Метод String getString (String name) выполняет подстановку объекта из класса пакета ресурсов или его родительских классов и представляет его в виде строки.
Метод String[] getStringArray(String name) выполняет подстановку объекта из класса пакета ресурсов или его родительских классов и представляет его в виде массива строк.
Метод Enumeration getKeys () возвращает объект-перечисление ключей заданного пакета ресурсов. При этом также перечисляются ключи в родительских пакетах ресурсов.
Локализация графического интерфейса
В предыдущих разделах подробно рассматривались особенности локализации приложений, а в этом разделе описывается влияние локализации на код программы. Например, при локализации приложений следует очень тщательно выполнять обработку событий, связанных с действиями пользователя. Рассмотрим следующий распространенный фрагмент кода.
public class MyApplet extends Japplet
implements ActionListener■
{
public void init()
{
Jbutton cancelButton = new JButtont"Cancel");
cancelButton.addActionListener(this);
. . .ы
}
public void actionPerformed(ActionEvent evt)
String arg = evt.getActionCommand();
if (arg.equals("Cancel"))
doCancel(); else ...
)
}
Многие программисты успешно используют подобный код до тех пор, пока не возникает необходимость локализации пользовательского интерфейса. Например, для локализации английской версии программы для немецких пользователей надпись на кнопке Cancel следует заменить надписью Abbrechen. Далее, эта надпись должна быть автоматически обновлена в методах init и actionPer formed. Очевидно, что это чревато появлением ошибок, как следствие печально известного закона Мэрфи: если какие-то объекты должны быть синхронизированны, то на самом деле они не будут синхронизированны. В данном примере кнопка работать не будет, если в одном из указанных двух методов не обновить надпись на кнопке. Для решения этой потенциальной проблемы можно использовать следующие три способа.
Использовать внутренние классы вместо отдельных процедур actionPerformed.
Идентифицироватькомпонентыпоссылкам,анепофактическимзначениям
(например, надписям).
Использовать свойство name для определения компонентов.
Рассмотрим подробнее все варианты.
Вместо использования одного обработчика множества событий рекомендуется определить несколько независимых обработчиков событий для каждого компонента.
public class MyApplet extends Japplet implements ActionListener {
public void init() {
JButton cancelButton = new JButton( "Cancel ") ;
cancelButton.addActionListener(new
ActionListener()
{ public void actionPerformed(ActionEvent e) {
doCancel();
} }) ;
}
}
В этом коде создается внутренний класс, который обрабатывает события, связанные только с кнопкой Cancel. Кнопка и ее обработчик тесно связаны, поэтому не существует никакого другого кода синтаксического анализа надписи на кнопке. Следовательно, локализовать придется только одну строку кода.
Применение внутренних классов считается наиболее оптимальным решением проблемы объединения интерфейсных элементов и соответствующих обработчиков событий. Недостатком применения внутренних классов можно считать плохую читабельность исходных текстов и создание отдельных файлов для внутренних классов.
Другой вариант решения этой проблемы основан на преобразовании кнопки в переменную экземпляра и сравнении ее ссылки с источником команды.
public classextends Japplet implements ActionListener
{
public void
{
cancelButton = new JButton("Cancel"); cancelButton.addActionListener(this);
)
public void actionPerformed(ActionEvent evt)
{
Object source =
if (source == cancelButton)
doCancel();
else ...
}
private JButton cancelButton;
}
Необходимо отметить, что в таком случае каждый элемент пользовательского интерфейса необходимо преобразовать в перемешгую экземпляра, а метод должен иметь возможность доступа к этим переменным.
Третий вариант решения проблемы основан на использовании свойства name для определения компонентов. В данном случае в каждом объекте-наследнике класса Component (например, классе Button) можно использовать свойство (имя). Имя может отличаться, а может и не отличаться от надписи для отдельной локализации, но оно остается при изменении локализации. Например, свойство1 кнопки Cancel не
будет отображаться для пользователя, а будет всего лишь строкой текста, связанной с кнопкой. (Имя можно рассматривать как отдельное свойство кнопки. Более подробно свойства описываются в главе 8.) При возникновении события сначала извлекается источник события, а затем определяется имя этого источника события.
public class MyApplet extends Japplet implements ActionListener
{. .
public void
{
JButton cancelButton = new
; cancelButton.addActionListener(this);
}
public voidevt)
Component source =
if (source.getName().equals("cancell")) doCancel();
}
)
j ava. awt. Component
Метод void setName( String s) задает имя компонента, т.е. произвольную метку, связанную с этим компонентом.
Метод String getName () возвращает имя компонента, т.е. произвольную метку, связанную с этим компонентом.
Локализация аплета
В этом разделе изложенный выше материал применяется на практике для локализации аплета-калькулятора, предназначенного для вычисления пенсионного плана.
Как показано на рис. 10.5, аплет рассчитывает прогнозируемую величину пенсии на основе заданного возраста, ежемесячных пенсионных отчислений и т.д.
В текстовой области и на диаграмме представлены данные о ежегодном состоянии пенсионного счета вплоть до заданного конца жизни. Если значения пенсионного счета становятся отрицательными к концу жизни, т.е. прямоугольники на диаграмме располагаются ниже оси абсцисс, то в таком случае следует предпринимать какие-то меры, например, строже экономить деньги, отложить дату выхода на пенсию и т.п.
Рис. 10.5. Пенсионный калькулятор с английским интерфейсомРис. 10.5. Пенсионный калькулятор с английским интерфейсом
Рис. 10.5. Пенсионный калькулятор с английским интерфейсом
Рис. 10.5. Пенсионный калькулятор с английским интерфейсом
Обновленный пенсионный калькулятор теперь имеет три варианта локализации: английский, немецкий и китайский. Выделим основные особенности локализации этого аплета.
Надписи и сообщения переводятся на немецкий и китайский языки. Соответствующие ресурсы находятся в классах RetireResources_de и RetireResources_zh, а английский вариант интерфейса используется в качестве резервного, и его ресурсы находятся в классе RetireResources. Для создания сообщений на китайском языке ис-
пользовалась программа Notepad (Блокнот) в китайской версии Windows, а затем текст был преобразован с помощью утилиты native2asciiв Unicode-код.
При изменении регионального стандарта заново форматируются надписи и содержимое текстовых полей.
Текстовые поля обрабатывают числа, денежные суммы и проценты в соответствии с требованиями выбранного регионального стандарта.
Для форматирования расчетного поля используется классMessageFomat. Строка подстановки для каждого языка хранится в пакете ресурсов.
В демонстративных целях цвет диаграммы меняется в зависимости от выбранного языка.
В листинга 10.5-10.8 приведен исходный код аплета, а в листингах 10.9-10.11 — файлы свойств для локализованных строк. На рис. 10.6 и 10.7 приведены результаты вычислений для немецкого и китайского вариантов интерфейса. Для того, чтобы увидеть китайские символы, необходимо запустить аплет в китайской версии Windows или вручную установить китайские шрифты, в противном случае все китайские символы будут отображаться в виде значков "отсутствующий символ".
Рис. 10.6. Пенсионный калькулятор с немецким интерфейсомРис. 10.6. Пенсионный калькулятор с немецким интерфейсом
Рис. 10.6. Пенсионный калькулятор с немецким интерфейсом
Рис. 10.6. Пенсионный калькулятор с немецким интерфейсом
Этот аплет сложнее обычного локализованного приложения, потому что в нем предусмотрена возможность оперативного изменения используемого регионального стандарта во время выполнения аплета. Поэтому в нем используются процедуры автоматической перерисовки пользовательского интерфейса при каждой смене регионального стандарта. Для применения предлагаемого по умолчанию регионального стандарта при запуске приложения достаточно извлечь его с помощью метода getLocale(), а затем использовать его на протяжении всей дальнейшей работы аплета.
Итак, несмотря на сложности реализации, предусмотренный в Java механизм локализации обладает важным достоинством. После создания всего кода, необходимого для поддержки какого-то одного регионального стандарта, программист имеет возможность очень легко создавать новые локализованные версии для других региональных стандартов. Для этого достаточно создать новые файлы ресурсов, которые будут автоматически загружены в случае необходимости.
Рис. 10.7. Пенсионный калькулятор с китайским интерфейсом
Листинг10.5. Retire.java
/**
(Aversion 1.2 1999-10-13
@author Cay Horstmann
*/
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.applet.*;
import java.util.*;
import java.text.*;
import java.io. *;
importj avax.swing.* ;
/**
Аплет-калькулятор пенсионных планов с представлением пользовательского интерфейса в соответствии с
английским, немецким и китайским региональными стандартами
*/
public class Retire extends JApplet {
public void init()
GridBagLayout gbl = new GridBagLayout(); getContentPane().setLayout(gbl);
GridBagConstraints gbc = new GridBagConstraints(); gbc.weightx =100;
gbc.weighty =
gbc.fill =
gbc.anchor =
add(languageLabel, gbc, 0, 0, 1, 1);
add(savingsLabel, gbc, 0, 1, 1, 1);
add(contribLabel, gbc, 2, 1, 1, 1);
add(incomeLabel, gbc, 4, 1, 1, 1) ;
add(currentAgeLabel, gbc, 0, 2, 1, 1);
add(retireAgeLabel, gbc, 2, 2, 1, 1) ;
add(deathAgeLabel, gbc, 4, 2, 1, 1) ;
add(inflationPercentLabel, gbc, 0, 3, 1, 1);
add(investPercentLabel, gbc, 2, 3, 1, 1);
gbc.fill =
gbc.anchor =
add(localeCombo, gbc, 1, 0, 2, 1);
add(savingsField, gbc, 1, 1, 1, 1) ;
add(contribField, gbc, 3, 1, 1, 1) ;
add(incomeField, gbc, 5, 1, 1, 1) ;
add(currentAgeField, gbc, 1, 2, 1, 1) ;
add(retireAgeField, gbc, 3, 2, 1, 1) ;
add(deathAgeField, gbc, 5, 2, 1, 1);
add(inflationPercentField, gbc, 1, 3, 1, 1) ;
add(investPercentField, gbc, 3, 3, 1, 1);
computeButton.setName("computeButton");
computeButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
getlnfo() ;
updateData();
updateGraph();
}) ;
add(computeButton, gbc,5,3,1,1);
gbc.weighty =100;
gbc.fill =
add(retireCanvas, gbc, 0,4, 4, 1);
add(newJScrollPane(retireText),gbc,4,4,2,1);
retireText.setEditable(false);
retireText.setFont (new Font("Monospaced", Font.PLAIN, 10));
info.setSavings(0) ;
info.setContrib(9000);
info.setincome(60000) ;
infо.setCurrentAge(35) ;
info.setRetireAge(65) ;
info.setDeathAge(85);
info.setlnvestPercent(0.1) ;-
infо.setlnflationPercent(0.05);
localeCombo.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent event) {
setCurrentLocale(
locales[localeCombo.getSelectedlndex() ] );
} }) ;
locales = new Locale []
{ Locale.US, Locale.CHINA, Locale.GERMANY };
int localeindex = 0; // US locale is default selection
for (int i = 0; i < locales.length; i++)
// Выбор регионального стандарта
if (getLocale().equals(locales[i])) localelndex = i;
setCurrentLocale(locales[localelndex]);
/**
Вставляет компонент в заданное место решетки
@param с the component to add
Sparam gbc the grid bag constraints to use
Sparam x the x grid position
Sparam у the у grid position
Sparam w the grid width
@param h the grid height
*/
public void add(Component c, GridBagConstraints gbc, int x, int y, int w, int h)
{
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
getContentPane().add(c, gbc);
/**
Установка текущего регионального стандарта
Sparam locale the desired locale
*/
public void setCurrentLocale(Locale locale)
{
currentLocale = locale;
int localelndex = localeCombo.getSelectedlndex();
localeCombo. removeAHItems();
for (int i = 0; i < locales.length; i++) {
String language =
localeCombo.additem(language);
}
if (localelndex >= 0)
localeCombo.setSelectedlndex(localelndex) ;
res = ResourceBundle.getBundle("RetireResources",
currentLocale);
resStrings = ResourceBundle.getBundle("Retirestrings", currentLocale);
currencyFmt
=
numberFmt
=
percentFmt =
updateDisplay();
updateinfo();
updateData();
updateGraph();
/**
Обновление всех надписей
*/
public void updateDisplay()
{
languageLabel.setText(resStrings.getstring("language")); savingsLabel.setText(resStrings.getstring("savings"));
contribLabel.setText(resStrings.getstring ("contrib"));
incomeLabel.setText(resStrings.getstring("income"));
currentAgeLabel.setText(
resStrings.getstring("currentAge"));
retireAgeLabel.setText(resStrings.getstring("retireAge")) ; deathAgeLabel.setText(resStrings.getstring("deathAge"));
inflationPercentLabel.setText(
resStrings.getString("inflationPercent”));
investPercentLabel.setText(
resStrings.getstring("investPercent”));
computeButton.setText(
resStrings.getstring("computeButton"));
validate();
}
/**
Обновление информации в текстовых полях
*/
public void updateinfo()
{
savingsField.setText(
currencyFmt.format(info.getSavings()));
contribField.setText(
currencyFmt. format (info . getContrib () ) ) ;
incomeField.setText(currencyFmt.format(info.getlncome())) ;
currentAgeField.setText(
numberFmt.format(info.getCurrentAge()));
retireAgeField.setText(
numberFmt.format(info.getRetireAge())) ;
deathAgeField,setText(
numberFmt.format(info.getDeathAge()));
investPercentField.setText(
percentFmt.format(info.getlnvestPercent()));
inflationPercentField.setText(
percentFmt.format(info.getlnflationPercent()));
/**
Обновление данных в текстовой области
*/
public void updateData()
{
retireText.setText("") ;
MessageFormat retireMsg = new MessageFormat("");
retireMsg.setLocale(currentLocale);
retireMsg.applyPattern(resStrings.getString("retire”)> ;
for (int i =
i <= info.getDeathAge(); i++)
{
Object!] args = { new Integer(i),
new Double(infо.getBalance(i)) };
retireText.append(retireMsg.format(args) + "\n");
/**
Обновление диаграммы
*/
public void updateGraph()
{
retireCanvas.setColorPre(
(Color)res.getObject("colorPre")) ;
retireCanvas.setColorGain(
(Color)res.getObject("colorGain")); retireCanvas.setColorLoss(
(Color)res.getObject("colorLoss"));
retireCanvas.setinfo(info);
repaint();
/**
Считывание входных данных в текстовых полях
*/
public void getlnfo()
{
try
{
info.setSavings(currencyFmt.parse( savingsField.getText () ) . doubleValue ());
info.setContrib(currencyFmt.parse(
contribField.getText()).doubleValue());
info.setincome(currencyFmt.parse(
incomeField.getText()).doubleValue());
info.setCurrentAge(numberFmt.parse( currentAgeField.getText()).intValue());
info.setRetireAge(numberFmt.parse( retireAgeField.getText()).intValuet));
info.setDeathAge(numberFmt.parse( ,..
deathAgeField.getText()).intValue());.
info.setlnvestPercent(percentFmt.parse(
investPercentField.getText()).doubleValue()) ;
info.setlnflationPercent(percentFmt.parse( inflationPercentField.getText()).doubleValue());
}
catch (ParseException exception)
{
}
private JTextField savingsField = new JTextField(10) ;
private JTextField contribField = new JTextField(10) ;
private JTextField incomeField = new JTextField(10);
private JTextField currentAgeField = new JTextField(4);
private JTextField retireAgeField = new JTextField(4);
private JTextField deathAgeField = new JTextField(4);
private JTextField inflationPercentField = new JTextField(6);
private JTextField investPercentField = new JTextField(6);
private JTextArea retireText = new JTextArea(10, 25);
private RetireCanvas retireCanvas =newRetireCanvas();
private JButton computeButton = new JButton();
private JLabel languageLabel =newJLabel();
private JLabel savingsLabel =newJLabel();
private JLabel contribLabel =newJLabel();
private JLabel incomeLabel =newJLabel();
private JLabel currentAgeLabel =newJLabel();
private JLabel retireAgeLabel =newJLabel();
private JLabel deathAgeLabel =newJLabel();
private JLabel inflationPercentLabel =newJLabel();
private JLabel investPercentLabel =newJLabel();
private Retireinfo info = new Retireinfo();
private Locale[] locales;
private Locale currentLocale;
private JComboBox localeCombo =newJComboBox();
private ResourceBundle res;
private ResourceBundle resStrings;
private NumberFormat currencyFmt;
private NumberFormat numberFmt;
private NumberFormat percentFmt;
/**
Информация, необходимая для вычисления пенсионного плана */ class Retirelnfo {
/**
Возвращение ежегодного состояния счета.
©param year the year for which to compute the balance ©return the amount of money available (or required) in that year
*/
public double getBalance(int year) {
if (year < currentAge) return 0;
else if (year == currentAge) { age = year;
balance = savings;
return balance;
}
else if (year == age) return balance;
if (year != age + 1)
getBalance(year—1) ;
age = year;
if (age < retireAge)
balance += contrib;
else
balance —= income;
balance = balance
* (1 + (investPercent — inflationpercent));
return balance;
}
/**
Возвращение суммы текущих пенсионных сбережений («return the savings amount
/
public double getSavings()
{ return savings;
}
/**
Указание текущих пенсионных сбережений.
@param x the savings amount
/
public void setSavings(double x)
{ savings = x;
}
/**
Возвращает ежегодную сумму отчислений на пенсионный счет @return the contribution amount
/
public double getContrib()
{
return contrib;
}
/**
Задает ежегодную сумму отчислений на пенсионный счет @param x the contribution amount
/
public void setContrib(double x)
{ contrib = x;
}
/**
Возвращает ежегодный доход
@return the income amount
/
public double getlncome()
{
return income;
)
/**
Задает ежегодный доход
@param x the income amount
/
public void setIncome(double x)
{ income = x;
)
/**
Возвращает текущий возраст
ereturn the age
/
public int getCurrentAge()
{
return currentAge;
/**
Задает текущий возраст
eparam x the age
/
public void setCurrentAge(int x)
{
currentAge = x;
}
/**
Возвращает возраст выхода на пенсию
Bretum the age
/
public int getRetireAge()
{
return retireAge;
}
/**
Задает возраст выхода на пенсию
Bparam x the age
/
public void setRetireAge(int x)
{
retireAge = x;
}
/**
Возвращает предполагаемую продолжительность жизни
Bretum the age
/
public int getDeathAge()
{
return deathAge;
}
/**
Задает предполагаемую продолжительность жизни eparam х the age
/
public void setDeathAge(int x)
{
deathAge = x;
}
/**
Возвращает предполагаемую величину инфляции ereturn the percentage
/
public double getlnflationPercent()
{
return inflationpercent;
)
/**
Задает предполагаемую величину инфляции
Эрагаш x the percentage
/
public void setlnflationPercent(double x)
{
inflationPercent = x;
}
/**
Возвращает величину доходности в процентах @return the percentage
/
public double getlnvestPercent()
{
return investPercent;
}
/**
Задает величину доходности в процентах @param x the percentage
/
public void setlnvestPercent(double x)
{
investPercent = x;
}
private double savings;
private double contrib;
private double income;
private int currentAge;
private int retireAge;
private int deathAge;
private double inflationpercent;
private double investPercent;
private int age;
private double balance;
/**
Панель с диаграммой пенсионного плана
*/
class RetireCanvas extends JPanel {
public RetireCanvas() {
setsize(WIDTH, HEIGHT);
/**
Задает отображаемые на диаграмме данные @param newlnfo the new retirement info. */
public void setinfo(Retireinfo newlnfo) {
info = newlnfo;
repaint();
public void paintComponent(Graphics g)
{ Graphics2D g2 = (Graphics2D)g; if (info == null) return;
double minValue =
double maxValue = 0;
int i;
for (i = info.getCurrentAge();
i <= info.getDeathAge(); i++)
{
double v = infо.getBalance(i);
if (minValue > v) minValue = v;
if (maxValue < v) maxValue = v;
}
if (maxValue == minValue) return;
int barWidth = getwidth() / (info.getDeathAge()
-+ 1);
double scale = getHeight() / (maxValue — minValue);
for (i = info.getCurrentAge();
i <= info.getDeathAge(); i++) {
int xl = (i — info.getCurrentAge()) * barWidth + 1; int yl;
double v = info.getBalance(i) ;
int height;
int yOrigin = (int)(maxValue * scale);
if (v >= 0) {
yl = (int)((maxValue - v) * scale); height = yOrigin — yl;
else (
yl = yOrigin;
height = (int)(-v * scale); )
if (i < info.getRetireAge()) g2.setColor(colorPre);
else if (v >= 0)
g2.setColor(colorGain); else
g2.setColor(colorLoss);
Rectangle2D bar = new Rectangle2D.Double(xl, yl, barWidth - 2, height);
g2.fill(bar);
g2.setColor(Color.black);
g2.draw(bar);
/**
Задает цвет диаграммы до даты выхода на пенсию @param color the desired color
*/
public void setColorPre(Color color) {
colorPre = color;
repaint();
/**
Задает цвет диаграммы после даты выхода на пенсию с положительным балансом
Sparam color the desired color
*/
public void setColorGain(Color color) {
colorGain = color;
repaint();
}
/**
Задает цвет диаграммы после даты выхода на пенсию с отрицательным балансом • .
Sparam color the desired color
*/
public void setColorLoss(Color color) {
colorLoss = color;
repaint();
}
private Retireinfo info = null;
private Color colorPre;
private Color colorGain;
private Color colorLoss;
private static final int WIDTH = 400;
private static final int HEIGHT = 200;
Листинг 10.6. RetireReeources.java
/**
aversion 1.21 2001-08-27
@author Cay Horstmann
/
import java.util.*; import java.awt.*;
/**
Нестроковые ресурсы английской версии аплета-калькулятора пенсионного плана */ public class RetireResources
extends java.util.ListResourceBundle {
public Object[][] getContents() { return contents; } static final Object[][J contents =
{
"colorGain", Color.white }, "colorLoss", Color.red }
/ENDLOCALIZE
};
)
Листинг10.7. RatireRaeaurcoe_.de .java
/** aversion 1.21 2001-08-27 ©author Cay Horstmann
/
import java.util.*;
import java.awt.*;
/**
Нестроковые ресурсы немецкой версии аплета-калькулятора пенсионного плана */ public class RetireResources_de
extends java.util.ListResourceBundle
(
public Object[][] getContents() { return contents; } static final Object[][] contents = {
// BEGIN LOCALIZE
{ "colorPre", Color.yellow },
{ "colorGain", Color.black }, { "colorLoss", Color.red } // END LOCALIZE
)
Листинг 10.8. RatireResourcea._eh. java
/**
aversion 1.21 2001-08-27
@author Cay Horstmann
/
import java.util. * ;
importj ava.awt.*;
/**
Нестроковые ресурсы китайской версии аплета-калькулятора пенсионного плана */ public class RetireResources_zh
extends java.util.ListResourceBundle.
{
public Object[][] getContents() { return contents; } static final Object[][] contents =
{
{ "colorGain", Color.blue }, { "colorLoss", Color.yellow }
// END LOCALIZE
Листинг10.9.RetireStrinffs.properties/
language=Language
computeButton=Compute
savings=Prior Savings
contrib=Annual Contribution
income=Retirement Income currentAge=Current Age retireAge=Retirement Age deathAge=Life Expectancy inflationPercent=Inflation investPercent=Investment Return retire=Age: {0,number} Balance: (1,number,currency}
Листинг 10.10. RetlreStrings_de.properties
language=Sprache
computeButton=Rechnen
savings=Vorherige Ersparnisse
contrib=J?liche Einzahlung income=Einkommen nach Ruhestand
currentAge=Jetziges Alter retireAge=Ruhestandsalter deathAge=Lebenserwartung inflationPercent=Inflation investPercent=Investitionsgewinn retire=Alter: (0,number) Guthaben: {1,number,currency}
Листинг 10.11. RetlreStrings_zh.properties
language=\u8bed\u8a00
computeButton=\u8bal\u7b97
savings=\u65e2\u5b58
Contrib=\u6bcf\u5e74\u5b58\u91dl
income=\u9000\u4fll\u6536\u5165
currentAge=\u73bO\u5cad
retireAge=\u9000\u4fll\u5e7 4\u9f84
deathAge=\u9884\u671f\u5bff\u547d
inflationPercent=\u901a\u8d27\u81a8\u6da8
investPercent=\u62 9 5\u8d44\u62a5\u916c
java.applet.Appletjava.applet.Appletretire=\u5e74\u9f84: {0,number} \u603b\u7ed3 : {1,number,currency}
java.applet.Applet
java.applet.Applet
* Метод Locale getLocaleO возвращает текущий региональный стандарт аплета. Он определяется как текущий региональный стандарт клиентского компьютера, на котором выполняется аплет.
Встраиваемые методыВстраиваемые методы
Встраиваемые методы
Встраиваемые методы
в этой главе...
Вызов функции на языке С
в коде на языке Java
т Числовые параметры
и возвращаемые значения
Строковые параметры
т Доступ к полям объекта
т Доступ к статическим полям
т Сигнатуры
т Вызов Java-методов
Массивы
т Обработка ошибок
т API-интерфейс запуска
Пример доступа к реестру Windows
чв том. что код на языке Java обладает множест-
„итатели, наверно, уже убедились в том, что код на языке Java обладает множеством преимуществ по сравнению с кодом на языках С или C++. Это верно даже для приложений, созданных для специализированной платформы. При этом имеется в виду не переносимость, а другие функциональные возможности языкаJava.
При использовании языка Java повышается вероятность создания кода с меньшим количеством ошибок, чем при использовании языков С или C++.
На языке Java легче, чем на большинстве других языков, программироватьмного- поточные приложения.
Еще проще на языке Java программировать сетевые приложения.
Переносимость приложений следует рассматривать как дополнительное преимущество, которое можно использовать в случае необходимости.
Приложение, созданное исключительно на языке Java, с теоретической точки зрения можно применять для выполнения любых задач. Однако, на практике часто возникают ситуации, когда требуется создать (или использовать) код, написанный на другом языке. Такой код обычно называется встраиваемым (native) и чаще всего является платформозависимым. Ниже перечислены три очевидных случая, в которых рекомендуется использовать встраиваемый код.
Допустим, что у вас уже есть проверенный и отлаженный код большого размера на другом языке. Для преобразования имеющегося кода в Java-код требуется много времени, а полученный Java-код придется снова проверять и отлаживать.
Приложение использует в своей работе доступ к компонентам конкретной операционной системы или аппаратного обеспечения, а в связи с этим технологию Java использовать в лучшем случае затруднительно или вообще невозможно — в худшем случае.
Скорость исполнения кода имеет существенное значение. Если, например, задача является критичной по отношению ко времени выполнения или код используется настолько часто, что его оптимизация может существенно повысить общую производительность приложения. На самом деле это наименее вероятный вариант. При использовании оперативной JIT-компиляции (just-in-time) интенсивные вычисления на основе Java-кода выполняются не на много медленнее, чем откомпилированный С-код.
Вот почему в одной из перечисленных ситуаций, вероятно, имеет смысл вызывать встраиваемый код из программ, написанных на языке Java. Конечно, при использовании обычного диспетчера безопасности встраиваемый код можно будет применять с приложениям, но не с аплетами. В частности, вызываемая библиотека встраиваемого кода должна находиться на клиентском компьютере и поддерживаться архитектурой клиентского компьютера.
Для организации вызовов встраиваемых методов в технологии Java предусмотрены соответствующие способы работы с системными библиотеками, а пакетJDK содержит инструменты, которые могут избавить программиста от выполнения рутинной работы.
Для создания встраиваемого кода не обязательно использовать языки С или C++. LIНапример, допускается применение откомпилированного кода на языке FORTRAN при наличии инструментов связывания элементов языка FORTRAN и Java.
Однако, имейте в виду, что при использовании встраиваемых методов программа становится непереносимой на другие платформы. Поэтому при распространении программы в виде приложения потребуется снабдить ее отдельной библиотекой со встраиваемыми методами для каждой поддерживаемой платформы. Это также означает, что придется затратить дополнительные усилия, чтобы научить пользователей правильно устанавливать эти библиотеки! Кроме того, если пользователи уверены, что аплеты не могут повредить данные и похитить конфиденциальную информацию, то этого нельзя сказать с той же уверенностью о программах, в которых используются библиотеки со встраиваемыми методами. По этой причине многие потенциальные пользователи будут очень неохотно применять Java-программы с использованием встраиваемого кода. Помимо вопросов обеспечения безопасности библиотеки со встраиваемыми методами вряд ли столь же надежны, что и Java-код. Это замечание особенно актуально для фрагментов кода на языках, подобных С или C++, в которых не предусмотрена защита против ошибочной перезаписи
памяти из-за неправильно заданного указателя. В таком случае вполне возможно создание и использование встраиваемых методов, которые могут либо вывести из строя виртуальной машину Java, либо нарушить ее систему обеспечения безопасности, либо вызвать сбой операционной системы.
Таким образом, встраиваемый код рекомендуется использовать только в крайних случаях. Несомненно, что встраиваемый код необходимо использовать для организации доступа к аппаратным устройства, например, к последовательному порту. Однако, для повторного использования уже существующего кода встраиваемые методы следует рассматривать лишь как временную меру до завершения процесса его преобразования в код на языке Java. Если встраиваемый код используется с точки зрения повышения производительности, то рекомендуется проверить фактическую скорость выполнения программ на платформе Java. Как убедительно было показано на конференции JavaOne в 1996 году, в большинстве случаев скорость выполнения программ, полученных с помощью JIT-компиляции, оказывается вполне достаточной. Создатели криптографической библиотеки из компании Sun Microsystems показали, что скорость выполнения криптографических функций только на основе Java-кода вполне удовлетворительна. Конечно, она уступает скорости выполнения тех же функций на языке С, но с практической точки зрения это различие не очень существенно. Дело в том, что программы на языке Java выполняются значительно быстрее, чем сетевые операции ввода/вывода, что в значительно большей степени ограничивает производительность работы системы.
В заключение следует отметить, что вряд ли стоит приносить в жертву переносимость приложения в пользу бессмысленного увеличения скорости работы. Настоятельно рекомендуется использовать встраиваемые методы только в случаях крайней необходимости.
В этой главе рассматривается так называемый JNI-интерфейс связывания встраиваемых методов Java (Java Native Interface — JNI). В Java 1.0 поддерживалась более ранняя версия связывания (raw native interface), разновидность которой использовалась в виртуальной машине Microsoft. Компания Sun Microsystems заверила разработчиков, что описанное здесь JNI-связывание является постоянной частью Java- платформы и должно поддерживаться для всех виртуальных машин Java.
В настоящей главе в качестве основного языка создания встраиваемых методов будет использоваться язык С, поскольку именно он чаще всего используется в этих целях. В частности, будет показано, как обеспечивается соответствие между типами данных, именами переменных и вызовами функций в языках Java и С. (Это соответствие обычно называется С-связыванием?)
Для создания встраиваемого кода вместо языка С можно также использовать язык № C++. В этом случае у C++ есть несколько преимуществ: более строгий контроль соответствия типов и более удобный доступ к JNI-функциям. Однако, JNI не поддерживает прямое соответствие между классами платформы Java и классами C++.
Вызов функции на языке С
в коде на языке Java
Предположим, что в коде на языке Java нужно использовать функцию на языке С, которая выполняет какие-то полезные действия, например, функциюДля ор
ганизации вызова встраиваемой функции printf в языке программирования Java предусмотрено ключевое слово native, причем саму функцию printf необходимо инкапсулировать в классе.
public class Printf
{
public native Strings) ;
}
Такой класс можно успешно откомпилировать, но во время выполнения программы виртуальная машина Java выдаст сообщениео том, что она не
может найти функцию printf. Для устранения этого недостатка во время выполнения программы следует предоставить необходимую информацию о связывании данного класса. Как показано ниже, BJDK необходимо выполнить перечисленные ниже этапы.
Создайте на языке С функцию-заглушку, которая транслирует вызов функции из платформы Java в фактическую функцию на языке С. Заглушка осуществляет эту трансляцию на основе информации о параметрах функции, полученной из стека виртуальной машины Java и переданной откомпилированной функции на языке С.
Создайте специальную совместно используемую библиотеку для экспорта из нее функции-заглушки.
Используйте специальный метод System.для загрузки совмест
но используемой библиотеки во время выполнения программы Java.
Рассмотрим теперь способы выполнения этих этапов на различных примерах, начиная с тривиального использованияв частном случае и заканчивая реальным примером
использования специализированных функций операционной системы Windows для работы с реестром, которые не доступны непосредственно из Java-платформы.
Пример использования функции printf
Начнем с простейшего случая использования функции printf, т.е. создадим вызов встраиваемого метода для вывода сообщения Hello, Native World. При этом мы будем углубляться в подробное изучение возможностей форматирования сообщений, которые предусмотрены в методе printf! Этот пример позволяет легко и просто проверить возможности С-связывания компилятора языка С до их использования в более сложных примерах встраиваемых методов.
Как уже упоминалось выше, сначала необходимо объявить встраиваемый метод в классе с использованием ключевого слова native, которое предупреждает компилятор о наличии внешнего определения данного метода. Естественно, что встраиваемые методы не содержат никакого кода на языке Java, и заголовок метода заканчивается точкой с запятой. Как показано выше, объявление встраиваемого метода очень похоже на объявление абстрактного метода.
classHelloNative
{
public native static void greeting!);
Обратите внимание, что в данном примере встраиваемый метод объявлен как статичный с помощью ключевого слова static . Встраиваемые методы могут быть статичными или нестатичными. В этом простейшем примере встраиваемый метод не принимает никаких параметров. Способы передачи параметров, в том числе и неявных, рассматриваются ниже в других примерах этой главы.
Теперь создадим соответствующую С-функцию, имя которой должно строго соответствовать описанному ниже соглашению об именах для среды выполненияJava.
Используйте полное имя Java-метода, например, HelioNative.greeting. Если класс находится в пакете, то имя пакета должно быть указано явно, например, com.Horstmann.HeiloNative.greeting.
Замените все точки символами подчеркивания и присоедините префикс Java_,например,Java_HelloNative_greetingилиJava_
com_horstmann_HelloNative_greeting.
Если имя класса содержит символы, которые не являются ASCII-символами (например, _ или $) или являются Unicode-символами в виде \u007F, то в таком случае представьте их в виде 0хххх, где хххх — последовательность четырех шестнадцатеричных цифр символа в кодировке Unicode.
При перегрузке встраиваемыхметодов,т.е. при наличии несколькихвстраиваемыхме- тодов с одинаковыми именами следует добавить в конце имени метода двойной символ подчеркивания и суффикс в виде последовательности закодированныхтипов для аргументов этой функции. Более подробно кодировка типов аргументов описывается ниже в этой главе. Например, при наличии двух встроенных методов greeting () и greeting(int repeat) первый метод будет иметь имя Java_HelloNative_ greeting , а второй —Java HelloNative greeting I.
На самом деле эта операция практически никогда не выполняется вручную, потому что для автоматической генерации имен функций предусмотрена специальная утилита 3 avah. Прежде чем использовать утилиту j avah, нужно сначала откомпилировать исходный код, приведенный в листинге 11.3.
javac HelioNative.java
Затем для создания файла с заголовками С-функций вызовите утилиту j avah, которая находится в каталоге j dk/bin.
javah HelloNative
При использовании JDK 1.1 требуется использовать флажок -jni. Без этого флажка инструмент javah в JDK 1.1 генерирует файл с заголовками С-функций для С- связывания в Java 1.0,а начиная сJDK 1.2по умолчанию используетсяJNI-привязка.
В результате выполнения утилиты 3 avah будет получен файл с заголовками С- функций HelloNative .h, который приведен в листинге. 11.1.
Листинг11.1. HelioNative.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloNative */
#ifndef Included_HelloNative
#define Included_HelloNative
#ifdefcplusplus
extern "C" {
#endif
/*
Class:HelloNative
Method:greeting
Signature: () V
JNIEXPORT void JNICALL Java_HelloNative_greeting
(JNIEnv *, jclass);
#ifdefcplusplus
}
#endif
#endif
Этот файл содержит объявление функции Java_HelloNative_greeting. (Строки JNIEXPORT и JNICALL определены в заголовочном файле jni . h. Они указывают на зависящие от конкретного компилятора спецификаторы для функций, экспортируемых из динамически загружаемых библиотек.)
Теперь остается только скопировать прототип функции из этого файла заголовков в файл с исходным кодом и, как показано в листинге 11.2, создать код реализации данной функции.
Листинг11.2. HelioNative.c
/**
©version 1.10 1997-07-01
©author Cay Horstmann
*/•
#include "HelloNative.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_HelloNative_greeting(JNIEnv* env, jclass cl)
{
printf("Hello world!\n");
}
В этом простом примере игнорируются аргументы envncl, но их использование более подробно описывается ниже.
Для реализации встраиваемых методов можно использовать язык C++. Однако, в № таком случае функции, реализующие встраиваемые методы, необходимо объявлять с помощью предложения extern "С".
«include "HelloNative.h"
♦include <stdio.h>
extern "C"
JNIEXPORT void JNICALL Java_HelloNative_greeting
(JNIEnv* env, jclass cl)
{
printf("Hello, Native World!\n");
}
Затем откомпилируйте С-код в виде динамически загружаемой библиотеки в соответствии с требованиями используемого вами компилятора.
Например, для компилятора GNU С для операционной системы Linux нужно использовать следующую команду.
gcc —с —fPIC —I/usr/local/jdk/include/
%>-!/jdk/include/linux HelloNative.c
gcc —shared —о libHelloNative.so HelloNative.o
Для компилятора Sun для операционной системы ОС Solaris используйте следующую команду.
cc —G —I/usr/local/jdk/include
(b-I/usr/local/jdk/include/Solaris
S^HelloNative.c -o libHelloNative.so
А для компилятора Microsoft C++ для операционной системы Windows следует использовать такую команду.
cl —Ic:\jdk\include — Ic:\jdk\include\win32 —LD
HelioNative.cFeHelloNative.dll
Кроме того, можно использовать бесплатно распространяемую среду программирования Cygwin, которую вы сможете скопировать по адресу http://www. cygwin.com. Она содержит компилятор GNU С и библиотеки для написания кода в стиле Unix для операционной системы Windows. В среде Cygwin необходимо использовать такие команды.
дсс —с —D int64="long long"
Ъ-Ic:/jdk/include/ —Ic:/jdk/include/win32 HelloNative.c
^dllwrap —add-stdcall-alias —о HelloNative.dll HelloNative.o
В зависимости от конкретного расположения SDK и файлов с С-заголовками в этих командах, возможно, понадобится указать другие пути.
При использовании компилятора Microsoft C++ для создания DLL-библиотеки следует в режиме командной строки сначала запустить следующий пакетный файл.
c:\devstudio\vc\bin\vcvars32.bat
Этот пакетный файл задаст правильную конфигурацию компилятора, который используется в режиме командной строки, т.е. установит используемые этим компилятором путь и переменные окружения.
Версия файла с заголовками С-функций jni_md.h для операционной системы Windows содержит следующее определение типа, которое характерно для компи- лятораMicrosoft.
typedef _int64 jlong;
При использовании компилятора GNU данный файл можно отредактировать следующим образом.
#ifdefGNUC
typedef long long jlong;
#else
typedefint64 jlong;
#endif
Иначе, для этого в режиме командной строки можно использовать параметр — D int64="long long", как показано выше.
Наконец, для загрузки библиотеки виртуальной машиной еще до первого использования этого класса необходимо добавить вызов метода System. loadLibrary. Проще всего это можно сделать, включая статический блок инициализации в классе, который содержит встраиваемый метод, как показано в листинге. 11.3.
Листинг 11.3.HelioNative.java
/**
(aversion 1.10 1997-07-01
©author Cay Horstmann
*/
class HelloNative
{
public static native void greeting();
static {
System.loadLibrary("HelioNative");
}
}
После выполнения всех описанных выше действий следует приступать к использованию встраиваемой функции в приложении HelloNativeTest, которое показано в листинге 11.4.
Листинг 11.4. HelloNativeTest.Java
/**
©version 1.10 1997-07-01
©author Cay Horstmann
*/
class HelloNativeTest
{
public static void main(String[] args) {
HelioNative.greeting();
После компиляции и запуска программы в режиме командной строки на экране появится сообщение Hello, Native World!.
Конечно, само по себе оно не очень впечатляет. Однако, если учесть, что сообщение получено с помощью функции printf на языке С (а не на языке Java), то становится очевидным, что таким образом мы сделали первые шаги на пути к устранению разрыва между языками С и Java.
D
Для некоторых совместно используемых библиотек со встраиваемыми методами требуется выполнить код инициализации, который можно было бы поместить в методе JNI_OnLoad. Аналогично при завершении работы виртуальной машины Java следует вызвать метод JNI_OnUnload с соответствующим содержанием.
jint JNI_OnLoad(JavaVM* vm, void* reserved);
void JNI_OnUnload(JavaVM* vin,void* reserved);
Метод JNI_OnLoadдолжен возвращать самую раннюю версию виртуальной машины, которая необходима для работы библиотеки, например, JNI_VERSION_1_1.
Iaava"lang■System
Метод void loadLibrary (String libname) загружает библиотеку с заданным именем из каталога, указанного в пути поиска библиотеки. Точный способ поиска библиотеки зависит от используемой операционной системы. Например, в операционной системе Windows поиск осуществляется сначала в текущей папке, а затем в папках, перечисленных в переменной окружения PATH.
Метод void load (String filename) загружаетбиблиотек}' с заданным именем. Если библиотека не найдена, то генерируется сообщение об ошибке UnsatisfiedLinkError.
Числовые параметры и возвращаемые значения
При создании кода на языке С для программ на языке Java нужно иметь четкое представление о соответствии типов данных в обоих языках. Например, в языке С используются типы int и long, реализация которых зависит от платформы. На одних платформах тип данных int означает 16-битное значение, на других — 32-битное. Конечно, на Java-платформе тип данных int всегда означает 32-битное целочисленное значение. Именно поэтому в интерфейсе Java Native Interface определены типы j int, j long и другие.
В табл. 11.1 показано соответствие между типами данных в языках С И Java.
В заголовочном файле j ni . h эти типы данных определяются с помощью оператора typedef на основе соответствующих типов данных для определенной платформы. В данном файле также определяются константы JNI_FALSE = 0 и JNI_TRUE = 1.
Таблица 11.1JТипы данных в языках Java и См^^
Язык Java
Язык С
Количество байт
boolean
jboolean
1
byte
jbyte
1
char
j char
2
short
jshort
2
int
j int
4
long
jlong
8
float
jfloat
4
double
jdouble
8
Использование функции printf для форматирования чисел
Напомним, что в библиотеке Java не предусмотрен удобный способ форматирования чисел с плавающей запятой. Для этого, конечно, можно было бы использовать класс DecimalFormat (см. главу 3 тома 1) и создать собственный формат представления чисел с плавающей запятой, но гораздо проще использовать функцию printf. Поскольку данная функция быстро работает, просто устроена и хорошо знакома нам, давайте попробуем использовать ее во встраиваемом методе.
На самом деле такой подход не рекомендуется применять на практике, поскольку встраиваемый код придется откомпилировать для каждой платформы, на которой планируется использовать данное приложение. Здесь этот подход применяется только лишь для демонстрации способов передачи параметров встраиваемому методу и получения возвращаемого значения.
В листинге 11.5 показан класс Printf1, в котором встраиваемый метод используется для отображения числа с плавающей запятой в поле заданной длины и с заданной точностью.
Листинг 11.5. Printfl.Java
/**
@version 1.10 1997-07-01
@author Cay Horstmann
class Printfl
{
public static native int print(int width, int precision, doublex) ;
static
System.loadLibrary("Printfl");
Обратите внимание на то, что при реализации метода на языке С все параметры типа int и double меняются на параметры типа j int и jdouble, как показано в листинге 11.6.
Листинг 11.6. Printfl.c
/**
(aversion 1.10 1997-07-01
©author Cay Horstmann
*/
#include "Printfl.h"
#include <stdio.h>
JNIEXPORT jint JNICALL Java_Printfl_print
(JNIEnv* env, jclass cl, jint width, jint precision, jdouble x) {
char fmt[30];
jint ret;
sprintf(fmt, "%%%d.%df", width, precision);
ret = printf(fmt, x) ;
fflush(stdout);
return ret;
}
Эта функция просто создает строку форматирования %W.pf и передает ее в переменной fmt для функции printf, которая возвращает количество выведенных символов.
В листинге 11.7 приведена тестовая программа, которая демонстрирует применение класса Printf 1.
Листинг 11.7. PrintfITeet.java
/**
©version 1.10 1997-07-01
©author Cay Horstmann
*/
class PrintflTest
{
public static void main(String[] args)
{
int count = Printfl.print(8, 4, 3.14);
count += Printfl.print(8, 4, (double)count);
System.out.println();
for (int i = 0; i < count; i++)
System.out.print("-");
System.out.printin();
}
(Для компиляции и реализации кода этой программы следует выполнить команды, которые аналогичны командам из предыдущего раздела для программы HelloWorld. - Прим. ред.)
Строковыепараметры
Рассмотримспособы обмена строками со встраиваемыми методами. Как
известно, строки в языке Java представляют собой последовательности 16-битных Unicode-символов, а в языке С — последовательности 8-битных символов с нулевым концевым символом. Таким образом, способы представления строк в обоих языках существенно отличаются. Интерфейс Java Native Interface содержит два набора функций: один — для преобразования Java-строки в UTF-формат (Unicode Text Format), a другой — для преобразования Java-строки в массив Unicode-символов типа j char. Формат UTF подробно рассматривается в главе 1, "Многопоточность". Напомним, что в нем ASCII-символы остаются в таком же виде, а все остальные Unicode-символы кодируются в виде двух или трехбайтовых последовательностей.
С одной стороны, если в коде на языке С уже используются Unicode-символы, то логично воспользоваться вторым набором функций преобразования строк. А с другой стороны, если все строки на языке Java представлены только ASCII-символами, то можно использовать функции преобразования строк в UTF-формат.
Встраиваемый метод со строковым параметром типа String фактически получает значение типа string, а встраиваемый метод, возвращающий строковое значение типа String, должен фактически возвратить значение типа string. Для чтения и создания объектов типа string используются специализированные
Например, функциясоздает новый объект типа string на основе
массива символов типа char, который содержит символы в UTF-формате. К сожалению, JNI-функции имеют немного необычное соглашение о вызовах. Вот как, например, выглядит вызов функции NewStringUTF.
JNIEXPORTJNICALL Java_HelloNative_getGreeting
(JNIEnv* env, jclass {
jstr;
char= "Hello, Native
jstr =greeting);
return
}
Во всех вызовахиспользуется указатель env, который является первым аргументом любого встраиваемого метода. Как показано на рис.env— это
указатель на таблицу, содержащую указатели функций. Вот почему в начале любого необходимо добавлять префикс (*env) ->, который необходим для разыменования фактического указателя функции. Более того, указатель env является первым параметром любой JNI-функции. Эту несколько громоздкую структуру можно было бы сделать намного проще. Для простоты предположим, что в вызове JNI-функции необходимо использовать префикс (*env) ->, и забудем об использовании промежуточной таблицы с фактическими указателями функций.
Указатель envУказатель envРис. 11. 1.Способ использования указателя envРис. 11. 1.Способ использования указателя envВ языке C++ доступ кJNI-функциям имеет более простую структуру. Версия класса JNIEnvдля языка C++ содержит подставляемые функции-члены, которые автоматически выполняют поиск фактических указателей функций. Например, вызов функции NewStringUTF может иметь следующий вид.В языке C++ доступ кJNI-функциям имеет более простую структуру. Версия класса JNIEnvдля языка C++ содержит подставляемые функции-члены, которые автоматически выполняют поиск фактических указателей функций. Например, вызов функции NewStringUTF может иметь следующий вид.
Указатель env
Указатель env
Рис. 11. 1.Способ использования указателя env
Рис. 11. 1.Способ использования указателя env
В языке C++ доступ кJNI-функциям имеет более простую структуру. Версия класса JNIEnvдля языка C++ содержит подставляемые функции-члены, которые автоматически выполняют поиск фактических указателей функций. Например, вызов функции NewStringUTF может иметь следующий вид.
В языке C++ доступ кJNI-функциям имеет более простую структуру. Версия класса JNIEnvдля языка C++ содержит подставляемые функции-члены, которые автоматически выполняют поиск фактических указателей функций. Например, вызов функции NewStringUTF может иметь следующий вид.
jstr = env->NewStringUTF(greeting) ;
Обратите внимание на то, что указатель JNiEnv в списке параметров опускается.
Функция NewStringUTF создает новый объект j string, а для чтения его содержимого следует применять функцию GetStringUTFChars, которая возвращает указатель const jbyte* на UTF-символы строки. Учтите, что некоторые виртуальные машины Java могут использовать UTF-формат для внутреннего представления строки. Поскольку Java-строки считаются постоянными, очень важно серьезно отнестись к обозначению const и не пытаться что-то записать в такой символьный массив. С другой стороны, если виртуальная машина использует UTF-формат для внутреннего представления строк, то вызов этой функции приводит к выделению нового блока памяти, который заполняется эквивалентными UTF-символами.
После завершения работы со строкой в UTF-формате следует с помощью функции ReleaseStringUTFChars сообщить об этом виртуальной машине для того, чтобы она могла освободить память, использовавшуюся данной строкой. (Как известно, такой процесс "сборки мусора" происходит в отдельном потоке и может прерывать работу встраиваемых методов).
Иначе, можно создать собственный буфер для хранения символов строки с помощью методов GetStringRegion или GetStringUTFRegion.
Наконец, функция GetStringUTFLength возвращает количество символов, необходимых для кодирования строки в UTF-формате.
AP l IДоступ х Java-строкам из хода на языке С
L • Метод j string NewStringUTF (JNIEnv* env, const char bytes[]) возвращает новую Java-строку, созданную на основе строки в формате UTF, или значение NULL, если Java-строку создать не удалось.
Параметры:
envуказатель JNI-интерфейса.
bytesстрока в формате UTF с нулевым концевым символом
Метод jsize GetStringUTFLength(JNIEnv* env,jstring string)
возвращает количество символов, необходимых для кодирования строки в формате UTF.
Параметры:
envуказатель JNI-интерфейса
stringобъект-строкаJava
Метод const jbyte* GetStringUTFChars(JNIEnv* env,jstring
string, jboolean* isCopy) возвращает указатель на строку в формате UTF или значение NULL, если символьный массив невозможно создать. Этот указатель действителен до тех пор, пока не будет вызвана функция ReleaseStringUTFChars.
Параметры:
envуказатель JNI-интерфейса
stringобъект-строкаJava
isCopyсодержит значение NULL или указатель на переменную
jboolean, которая имеет значение JNI_TRUE, если копия создана, или значение JNI_FALSE в противном случае
Метод void ReleaseStringUTFChars(JNIEnv* env, jstring string, const jbyte bytes []) сообщает виртуальной машине, что встраиваемому коду больше не требуется побайтовый доступ к объекту-строкеJava.
Параметры:
envуказатель JNI-интерфейса
stringобъект-строкаJava
bytesуказатель, возвращаемый методом GetStringUTFChars
Метод void GetStringRegion(JNIEnv *env, jstring string, jsize start, jsize length, jchar *buffer) копирует последовательность Unicode-символов из строки в созданный пользователем буфер. Параметры:
envуказатель JNI-интерфейса
stringобъект-строкаJava
startначальный индекс
lengthколичество копируемых символов
bufferсозданный пользователембуфер
Метод void GetStringUTFRegion(JNIEnv *env, jstring string , jisize start, jsize length, jbyte *buffer) копирует последовательность символов в формате UTF8 из строки в созданный пользователем буфер. Буфер должен быть достаточной емкости, равной, по крайней мере, утроенной емкости (в байтах) копируемой строки.
Метод jstring NewString(JNIEnv* env, const jchar chars[], jsize length) возвращает новый объект-строку Java на основе строки в формате Unicode или значение NULL, если невозможно создать объект- строкуJava.
Параметры:
envуказатель JNI-интерфейса
charsстрока в формате UTF с концевым нулевым символом
lengthколичество символов в строке
Метод jsize GetStringLength(JNIEnv* env, jstring string) возвращает количество символов в строке.
Параметры:
envуказатель JNI-интерфейса
stringобъект-строкаJava
Метод const jchar* GetStringChars(JNIEnv*env,jstring
string, jboolean* isCopy) возвращает указатель на строку в формате Unicode или NULL, если символьный массив невозможно создать. Указатель действует, пока не будет вызвана функция ReleaseStringUTFChars.
Параметры:
envуказатель JNI-интерфейса
stringобъект-строкаJava
isCopyсодержит значение NULL или указатель на переменную
jboolean, которая имеет значение JNI_TRUE, если копия создана, или значение JNI_FALSE в противном случае
Метод void ReleaseStringChars(JNIEnv* env, jstring string, const j char chars []) сообщает виртуальной машине, что встраиваемому код)' больше не требуется посимвольный доступ к объекту-строкеJava.
Параметры:
envуказательJNI-интерфейса
stringобъект-строкаJava
charsуказатель, возвращаемый методом GetStringChars
Обращение к функции sprintf
из встраиваемого метода
Попробуем использовать описанные выше функции и создадим класс, который вызывает функцию sprintна языке С. Предлагается это сделать, как показано в листинге 11.8.
Листинг 11.8.Printf2Test.java
/**
©version 1.10 1997-07-01
©author Cay Horstmann
*/
class Printf2Test {
public static void main(String[] args) {
double price = 44.95;
double tax = 7.75;
double amountDue = price * (1 + tax / 100) ;
String s = Printf2.sprint("Amount due = %8.2f", amountDue);
System.out.println(s) ;
}
}
В листинге 11.9 представлен код класса со встраиваемой функцией sprintf.
Листинг 11.9. Printf2.java
/**
©version 1.10 1997-07-01
©author Cay Horstmann
*/
class Printf2 {
public static native String sprint(String format, double x) ;
static
{
System.loadLibrary("Printf2");
) }
Таким образом, функция языка С, с помощью которой форматируется число с плавающей запятой, имеет следующий прототип:
JNIEXPORT jstring JNICALL Java_Printf2_sprint
(JNIEnv* env, jclass cl, jstring format, jdouble x)
Код реализации этой функции на языке С представлен в листинге 11.10. Учтите, что для считывания аргумента форматирования format необходимо обратиться к функции GetStringUTFChars, для создания возвращаемого значения— к функции NewStringUTF, а для уведомления виртуальной машины о том, что доступ к строке больше не требуется — к функции ReleaseStringUTFChars.
Листинг 11.10.Printf2.c
/**
(aversion 1.10 1997-07-01
©author Cay Horstmann
*/
#include "Printf2.h”
#include <string.h>
#include <stdlib.h>
#include <float.h>
/**
@param format a string containing a printf format specifier (such as "%8.2f"). Substrings "%%” are skipped.
©return a pointer to the format specifier (skipping the ' % ') or NULL if there wasn't a unique format specifier
*/
char* find_format(const char format[])
char*char*p;p;{
char*
char*
p;
p;
char* q;
р = strchr(format, 1 % ');
while (p != NULL && *(p + 1) == ’%') /* skip %% */
p=strchr(p+2, '%') ;
if (p == NULL) return NULL;
/* now check that % is unique */
P++;
q = strchr(p, 1 % 1) ;
while (q != NULL && *(q + 1) == '%') /* skip %% */
q = strchr(q + 2, ' % ' ) ;
if (q != NULL) return NULL; /* % not unique */
q = p + strspn(p, " -0+#"); /* skip past flags */
q += strspn(q, ”0123456789"); /* skip past field width */
if (*q =={ q++; q += strspn(q, "0123456789"); }
/* skip past precision */
if (strchr("eEfFgG", *q) ==NULL) return NULL;
/* not a floating point format */
return p;
}
JNIEXPORT jstring JNICALL Java_Printf2_sprint(JNIEnv* env, jclass cl, jstring format, jdouble x)
{
const char* cformat;
char* fmt;
jstring ret;
cformat = (*env)->GetStringUTFChars(env, format, NULL);
fmt = find_format(cformat);
if (fmt == NULL)
ret = format;
else
{
char* cret;
int width = atoi(fmt);
if (width == 0) width = DBL_DIG + 10;
cret = (char*)malloc(strlen(cformat) + width);
sprintf(cret, cformat, x) ;
ret = (*env)->NewStringUTF(env, cret);
free(cret);
}
(*env)->ReleaseStringUTFChars(env, format, cformat);
return ret;
}
В этой функции используется достаточно простая обработка ошибок. Если строка форматирования отличается ОТ формы %w.pc (где с — это один из символов е, Е, f, g, G), то полученное число не форматируется. Далее в этой главе рассматриваются способы обработки ошибок за счет генерации исключительных ситуаций во встраиваемых методах.
Доступ к полям объекта
Все рассмотренные выше встраиваемые методы были статическими методами с числовыми и строковыми параметрами. В настоящем разделе рассматриваются встраиваемые методы, которые могут работать с объектами произвольного типа. В качестве примера попробуем создать метод класса Employee, который подробно рассматривается в главе 4 тома!, используя встраиваемый метод. Следует отметить, что этот пример используется
только лишь для демонстрации способов доступа к полям объекта из встраиваемого метода, и его не следует рассматривать как практическую рекомендацию.
Рассмотрим метод raiseSalary, который имеет следующий код наязыке Java.
public native void raiseSalary(double byPercent) {
salary *= 1 + byPercent / 100;
}
Создадим теперь для него встраиваемый метод, который в отличие от рассмотренных ранее встраиваемых методов уже не является статическим. После запуска утилиты j avah получим такой прототип.
JNIEXPORT void JNICALL Java_Employee_raiseSalary(JNIEnv*,
jobject, jdouble)
Обратите внимание на второй аргумент, который имеет тип j object, а не тип jclass. Фактически, он является эквивалентом ссылки this, однако, статические методы получают ссылку на класс, а нестатические методы — на конкретный аргумент-объект.
Рассмотрим, как организован доступ к полю salary неявно заданного аргумента. В "сыром" связывании ЯЗЫКОВ Java и С в Java версии 1.0 это было организовано достаточно просто. Программист мог непосредственно получить доступ к полям данных объекта. Однако, для организации прямого доступа всем виртуальным машинам придется предоставить внутреннюю структуру данных. По этой причине в JNI- интерфейсе требуется, чтобы программисты задавали и возвращали значения полей данных с помощью специальных функцийJNI-интерфейса.
В нашем примере потребуется использовать функции GetDoubleField и SetDoubleField, потому что поле salary имеет тип double. Для полей данных другого типа предусмотрены другие функции, например, GetlntFieldи SetlntField, GetObjectFieldи SetObj ectFieldи т.д. Для их использования нужно применять следующий синтаксис.
х = (*env)->GetXxxField(env, class, fieldID);
(*env) ->Get.YxxField (env, class, fieldID, x) ;
Параметр class обозначает значение, которое представляет объект Java типа Class, параметр fieldID— значение специального типа, которое идентифицирует поле структуры, а суффикс Ххх— тип данныхJava (например, Object, Boolean, Byte и т.д.). Для получения объекта class можно использовать функцию GetObjectClass или функцию FindClass.
Функция GetObjectClassвозвращает класс произвольного объекта.
jclass class_Employee =obj_this);
Функция FindClass позволяет указать имя класса в виде строки (забавно, что вместо точек для разделения пакетов в строке нужно использовать символы /) .
jclass class_String =
Для получения параметра fieldIDследует использовать функцию GetFieldID с указанием имени поля и его сигнатуры, т.е. кодированного представления его типа.
jfieldID id_salary
= (*env)->GetFieldID(env, class_Employee, "salary", "D");
Здесь строка "D" обозначает тип double. Более подробно правила создания и использования сигнатур полей рассматриваются в следующем разделе.
На первый взгляд может показаться, что эти правила доступа к полям данных чрезмерно запутанны. Но необходимо помнить о том, что создатели JNI-интерфейса для предоставления непосредственного доступа к полям данных должны были создать соответствующие set/get-функции. Для сокращения накладных расходов при использовании этих функций вычисление идентификатора поля (что представляет собой наиболее сложную задачу) выделено в отдельную задачу. Таким образом, при многократном использовании set/get-фуикций для значения какого-то поля его идентификатор будет вычислен только один раз при первом обращении к этому полю.
Ссылки на классы действительны только до момента возвращения встраиваемого метода. Поэтому возвращаемые значения метода GetObjectClassне удастся кэшировать (т.е. сохранить) в коде. Для повторного использования ссылки не рекомендуется сохранять ее извне. С этой целью необходимо каждый раз при использовании встраиваемого метода вызывать метод GetObjectClass. Если это недопустимо, то рекомендуется заблокировать данную ссылку с помощью функции NewGlobalRef, например так, как показано ниже.
static jclass class_X = 0; static jfieldID id_a;
if (class_X == 0) { jclass ex =obj) ;
class_X = (*env)->NewGlobalRef(env, ex); id_a = (*env)->GetFieldlD(env,cis,"a", ". . .") ;
}
Теперь заблокированную ссылку на класс и идентификаторы полей можно использовать в последующих вызовах встраиваемого метода. После выполнения всех необходимых операций со ссылкой ее нужно разблокировать с помощью метода DeleteGlobalRef.
(*env)->DeleteGlobalRef(env, class_X);
Итак, все описанные выше действия для метода raiseSalary могут быть представлены следующим фрагментом кода.
JNIEXPORT void JNICALL Java_Employee_raiseSalary(JNIEnv* env,
jobject obj_this, jdouble byPercent) {
/* Возврат класса */
jclass class_Employee = (*env)->GetObjectClass(env, obj_this);
/* Возврат идентификатора поля */
jfieldID id_salary = (*env)->GetFieldID(env, class_Einployee, "salary", "D");
/* Возврат значения поля */
jdouble salary = (*env)->GetDoubleField(env, obj_this, id_salary);
salary *= 1 + byPercent / 100;
/* Установка значения поля */
(*env)->SetDoubleField(env, obj_this, id_salary, salary);
)
В листингах 11.11 и 11.12 приведен код на языке Java для тестовой программы и класса Employee, а в листинге 11.13 приведен код на языке С для встраиваемого метода raiseSalary.
Доступ к статическим полям
Доступ к статическим полям организован аналогично доступу к нестатическим полям. Для этого используются функции GetStaticFieldID и GetStatic.Xxx- Field/SetStaticXxxField,которые работают, практически, аналогично их нестатическим прототипам, за исключением следующих различий.
Поскольку объекты в данном случае отсутствуют, то для получения ссылки на класс необходимо вместо метода GetObjectClassиспользовать метод FindClass.
По той же причине для доступа к полю используется класс, а не экземпляр класса.
Вот как выглядит фрагмент кода для получения ссылки на класс System.out.
/* Возврат класса */
jclass class_System = (*env)->FindClass(env,
"java/lang/System");
/* Возврат идентификатора поля */
jfieldID id_out = (*env)->GetStaticFieldID(env, class_System, "out",11 Ljava/io /Print Stream; ") ;
/* Возврат значения поля */
jobject obj_out = (*env)->GetStaticObj ectField(env, class_System, id_out);
ЛистингM1.11. EmployeeTest.java
/**
aversion 1.10 1999-11-13
©author Cay Horstmann
/
public class EmployeeTest
{
public static void main(String[] args)
{
Employee[] staff = new Employee[3] ;
staff [0] = new Employee("HarryHacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff [2] = new Employee("TonyTester", 38000);
inti;
for (i = 0; i < 3; i++) staff[i].raiseSalary(5); for (i = 0; i < 3; i++) staff[i].print();
}
/**
aversion 1.10 1999-11-13
Sauthor Cay
/
public class Employee
{
publicn, double s)
{
name =
salary = s;
}
public native voidbyPercent);
public void
{
+ " " + salary);
}
private String name;
private double salary;
static
{
System.loadLlbrary("Employee");
Листинг
/**
1.10 1999-11-13
Sauthor Cay Horstmann
/
include "Employee.h"
#include <stdio.h>
voidenv,
obj_this,byPercent)
{
/* Возвращение класса. */
jclass class_Employee = obj_this);
/* Возвращение идентификатора поля. */
id_salary = (*env)->GetFieldID(env, class_Employee, "salary", "D") ;
/* Возвращение значения поля. */
jdouble salary = (*env)->GetDoubleField(env, obj_this, id_salary);
salary *= 1 + byPercent / 100;
/* Установка значения поля. */
L^^^2/ВL^^^2/В(*env)->SetDoubleField(env, obj_this, id_salary, salary);
L^^^2/
В
L^^^2/
В
Функции доступа к полям данных объекта
Функция jfieldlD GetFieldID(JNIEnv *env, jclass cl, const char name[] , const char sig[]) возвращает идентификатор поля данного класса.
Параметры:
envуказатель HaJNI-интерфейс
с 1объект-класс
nameимя поля
sigзакодированная сигнатура поля
Функция Ххх GetXxxFieldID(JNIEnv *env,jobjectobj ,
j fieldlDid) возвращаетзначениеполя. Типполяможетиметьодноиз следующих значений: Object, Boolean, Byte, Char, Short, Int, Long, Float или Double.
Параметры:
envуказатель HaJNI-интерфейс
objобъект, из поля которого возвращается значение
idидентификатор поля
Функция voidSetXxxFieldlD(JNIEnv*env,jobjectobj,
jfieldID id, Xxx value) задает новое значение поля. Тип поля может иметь одно из следующих значений: Object, Boolean, Byte, Char, Short,Int, Long, Floatили Double.
Параметры:
envуказатель HaJNI-интерфейс
objобъект, для поля которого задается новое значение
idидентификатор поля
valueновое значение поля
Функция jfieldID GetStaticFieldID(JNIEnv *env,jclass cl,
const char name [] , const char sig[]) возвращает идентификатор статического поля данного класса.
Параметры:
envуказатель HaJNI-интерфейс
clобъект-класс
nameимя поля
sigзакодированная сигнатура поля
Функция Ххх GetStaticXxxFieldlD(JNIEnv *env, jobject obj, jfieldID id) возвращает значение статического поля. Тип поля может иметь одно из следующих значений: Object, Boolean, Byte, Char, Short, Int, Long, Float или Double.
Параметры:
env cl
указатель HaJNI-интерфейс
объект-класс, из статического поля которого возвращается значение
id
идентификатор поля
Функция void SetXxxFieldID(JNIEnv *env, jobject obj , jfieldID id, Xxx value) задает новое значение статического поля. Тип поля может иметь одно из следующих значений: Object, Boolean, Byte, Char, Short, Int.Long, Floatили Double.
Параметры:
env
cl
указатель HaJNI-интерфейс
объект-класс, для статического поля которого задается новое значение
id value
идентификатор поля
новое значение поля
Сигнатуры
Для доступа к полям объекта и вызова методов, которые определены в языке Java, необходимо знать правила указания имен типов данных и сигнатуры методов. (Напомним, что сигнатура метода описывает параметры и возвращаемое значение метода.) Для этого используется следующая схема обозначений.
byte char double float int longтип класса shortvoid booleanbyte char double float int longтип класса shortvoid booleanВ
byte char double float int long
тип класса short
void boolean
byte char double float int long
тип класса short
void boolean
С
D
F
I
J
Ьимя_класса;
S
V
Z
Учтите, что точка с запятой в конце выражения Ъимя_класса является терминатором выражения типа, а не знаком разделения параметров. Рассмотрим показанный ниже конструктор.
Employee(java.lang.String, double, java.util.Date)
Он имеет следующую сигнатуру.
"(Ljava/lang/String;DLjava/util/Date;)V"
Как видите, между обозначениями D и Ljava/util/Date; нет никаких символов разделения.
Обратите также внимание на то, что в этой системе обозначений для разделения пакета и имени класса вместо точки используется символ /.
Для описания типов массивов используется символ [. Например, для массива строк может использоваться следующее обозначение.
[Ljava/lang/String;
Для массива float [] [] используется такое обозначение.
[[F
Для полного представления сигнатуры метода нужно перечислить все типы параметров в круглых скобках, а затем указать возвращаемый тип. Например, сигнатура метода с двумя параметрами целочисленного типа и целочисленным возвращаемым значением будет иметь следующий вид.
(II) I
Метод print из предыдущего раздела, который получает строку и возвращает значение типа void, будет иметь такую сигнатуру.
(Ljava/lang/String;)V
Для автоматической генерации сигнатур методов можно применять утилиту с параметромнапример так, как показано ниже.
javap -s -private
После выполнения этой команды будет получен следующий результат, в котором указаны сигнатуры всех полей и методов.
public synchronized class Employee extends
/*bit set */
{
private
/**/
private double salary;
/* D */
privatehireDay;
/**/
public
/**/
public void
/* ()V */ public void
/* (D)V */ public int /* 01*/
Нельзя привести никакого логичного объяснения, почему используется именно такой способ представления сигнатуры. Создатели механизма вызовов встраиваемого кода вполне могли бы предложить такой синтаксис для сигнатур, который с успехом используется в языке Java, например, подразумевая под ним любое нужное им внутреннее представление. Однако, использование столь запутанных сигнатур превращает работу программиста в эзотерическое занятие и приближает его к той области деятельности, которой занимается виртуальная машина Java.
Вызов Java-методов
Конечно, функции языка программирования Java могут вызывать функции языка С, и именно для этого предназначены встраиваемые методы. Можно и нужно ли выполнять обратную операцию, т.е. вызывать методы языка Java из функций языка С? Такая необходимость возникает в тех случаях, когда встраиваемому методу нужно организовать запрос на получение какого-то сервиса из переданного ему объекта. Рассмотрим этот механизм сначала для нестатических методов, а затем — для статических.
Нестатические методы
В качестве примера вызова нестатического метода на языке Java из встраиваемого кода расширим класс Printfи создадим член-функцию, которая работает аналогично функции fprintf на языке С. Таким образом, можно выводить строку для произвольного объекта Printwriter.
class Printf3
{
public static native void fprint(Printwriter out, String format, doublex);
}
Сначала составим строку, которая будет выводиться в объекте str типа String, как в реализованном ранее методе sprint. Затем вызовем метод print класса Printwriterиз функции С, которая реализует встраиваемый метод.
ЛюбойJava-метод можно вызвать из языка С с помощью указанного ниже синтаксиса.
(*env) ->CallA'xxMethod(env, implicit parameter, methodic, explicit parameters)
Здесь Xxx означает возвращаемый тип метода, например Void, Int, Obect и т.д. Аналогично идентификатору поля для доступа к полю объекта и для вызова метода нужно использовать идентификатор метода. Для его получения предусмотрена функция JNI-интерфейса GetMethodID с параметрами, которые содержат имя класса, имя метода и сигнатуру метода.
В этом примере нужно получить идентификатор метода print класса PrintWriter. Как показано в главе 1, "Многопоточность", класс PrintWriter содержит девять разных "методов, которые имеют одинаковое имя print. Поэтому для выбора конкретного варианта данной функции нужно точно указать ее параметры и возвращаемое значение. Например, для получения функции void print (java.lang. String) нужно использовать ее сигнатуру в виде строки " (Ljava/lang/String; )V".
Ниже приводится фрагмент кода, который выполняет следующие действия для вызова этого метода.
Получает класс неявно заданного параметра.
- 2. Получает идентификатор метода.
3. Выполняет вызов.
/* Возврат класса */
class_PrintWriter = (*env)->GetObjectClass(env, out);
/* Возврат идентификатора метода */
id_print = (*env)->GetMethodID(env, class_PrintWriter, "print", "(Ljava/lang/String;)V");
/* Вызов метода */
(*env)->CaliVoidMethod(env, out, id_print, str) ;
В листингах 11.14и 11.15 приводится код на языке Java для тестовой программы и программы Print 3, a в листинге 11.16— код на языке С для встраиваемого метода print.
Численные идентификаторы методов и полей концептуально аналогичны объектам Method и Field при использовании механизма отражения Java. Их можно взаимно преобразовывать с помощью перечисленных ниже функций.
jobject ToReflectedMethod(JNIEnv* env, jclass class, jmethodlD methodID); // Возвращает объект-метод.
methodID FromReflectedMethod(JNIEnv* env, jobject method);
jobject ToReflectedField(JNIEnv* env, jclass class, jfieldID fieldID); // Возвращает объект-поле.
fieldID FromReflectedField(JNIEnv* env, jobject field);
Статические методы
Вызов статических методов из встраиваемых выполняется аналогично вызову нестатистических методов, за исключением следующих двух различий.
Для этого используются методыGetstaticMethodID иCallStatic-XxxMethod.
При вызове метода указывается класс, а не неявный параметр-объект.
В качестве примера рассмотрим следующий вызов статического метода getProperty из встраиваемого метода.
System.getProperty("java.class.path")
Возвращаемым значением этого метода является строка с текущим путем к классам.
Вначале нужно найти используемый класс. Из-за отсутствия каких-либо объектов класса System придется вместо метода GetOhj ectClassиспользовать метод FindClass.
jclass class_System = (*env)->FindClass(env, "java/lang/System");
Затем нужно получить идентификатор статического метода getProperty, который имеет следующую закодированную сигнатуру для аргумента и возвращаемого значения типа String .
"(Ljava/lang/String;)Ljava/lang/String;"
Таким образом, идентификатор данного метода будет иметь следующий вид.
jmethodlD id_getProperty = (*env)->GetStaticMethodID(env, class_System, "getProperty",
"(Ljava/lang/String;)Ljava/lang/String;"));
Возвращаемое значение этого метода имеет типjobj ect. Для выполнения с ним строковых операций нужно привести его к типу j string.
jstring str_ret = (jstring)obj_ret;
В языке С типы jstring иjclass, а также типы массивов, которые рассматриваются № ниже, эквивалентны типу jobj ect. Поэтому показанный выше пример приведения к ти- u.пу j string совсем не обязательно выполнять в языке С. Но в языке C++ эти типы опре
делены как указатели на "классы-заглушки", которые имеют корректную иерархию наследования. Например, в языке C++ допускается присвоение значения типа jstring значению типа jobject без приведения типа, но такое приведение типа обязательно для присвоения значения типа j obj ect значению типа j string.
Конструкторы
Встраиваемый метод может создать новый объект Java за счет вызова его конструктора с помощью функции NewObj ect.
jobject obj_new =class, methodID,
construction parameters);
Для получения идентификатора метода нужно использовать функцию GetMethodID с указанием имени метода в виде строки "<init>" и закодированной сигнатуры конструктора с возвращаемым типом void. Например, встраиваемый метод для создания объекта FileOutputStream будет иметь следующий вид.
const char[] fileName = ". . .";
jstring str_fileName =fileName);
jclass class_FileOutputStrearn =
"java/io/FileOutputStream");
jmethodID id_FileOutputStream =
class_FileOutputStrearn, "<init>", " (Ljava/lang/String; )V) ;
jobject obj_stream = (*env)->NewObject(env,
class_FileOutputStream, id_FileOutputStream, str_fileName);
Обратите внимание, сигнатура конструктора содержит параметр типа jString и возвращаемое значение типа void.
Альтернативные вызовы методов
Существует несколько вариантов JNI-функций, предназначенных для вызова Java- методов из встраиваемого кода. Они имеют не такое большое значение, как только что рассмотренные функции, но могут пригодиться в разных ситуациях.
Функции CallNonvirtualXx.xMethodполучают неявно заданный аргумент, идентификатор метода, объект-класс (который должен соответствовать суперклассу неявно заданного аргумента), а также явно заданные аргументы. Такие функции вызывают версию этого метода для указанного класса, минуя обычный динамический механизм управления вызовами.
Все такие функции имеют варианты с суффиксами А или V, которые получают явно заданные параметры в массиве или структуре типаva_list (которая определена в заголовочном файле stdarg.h языка С).
/**/**Листинг 11.14.Prlntf3Test.Java
/**
/**
«aversion 1.10 1997-07-01
©author Cay Horstmann
*/
import j
class Printf3Test {
public static void main(String!] args) {
double price = 44.95;
double tax = 7.75;
double amountDue = price * (1 + tax / 100) ;
PrintWriter out = new Printwriter(System.out);_
Printf3.fprint(out, "Amount due = %8.2f\n", amountDue)' out.flush();
Листинг11.15. PrintfS.java
/**
©version 1.10 1997-07-01
@author Cay Horstmann
*/
import j
class Printf3
public static native void fprint(Printwriter out, String format, double x);
static
(
System.loadLibrary("Printf3");
}
}
Листинг11.16.Prlntf3.a
/**
©version 1.10 1997-07-01
©author Cay Horstmann
*/
«include "Printf3.h"
#include <string.h>
#include <stdlib.h>
#include <float.h>
/**
@param format a string containing a printf format specifier (such as "%8.2f"). Substrings "%%" are skipped.
©return a pointer to the format specifier (skipping the ’ % 1 ) or NULL if there wasn't a unique format specifier
*/
char* find_format(const char format[])
{
char* р;
char* q;
p=strchr(format,'%') ;
while (p != NULL && *(p + 1) == '%') /* skip %% */
p = strchr(p +2, ’ % ’);
if (p == NULL) return NULL;
/* now check that % is unique */
p++;
q = strchr(p, 1 % ' );
while (q != NULL && *(q+ 1) == '%') /* skip %% */
q= strchr (q+ 2, '%');
if (q != NULL) return NULL; /* % not unique */
q=p+ strspn(p, "-0+#"); /* skip past flags */
q+= strspn(q, "0123456789"); /* skip past field width */ if (*q == ' . ' ) { q++; q+= strspn(q, "0123456789“); }
/* skip past precision */
if (strchr("eEfFgG", *q) == NULL) returnNULL;
/* nota floating point format */
return p;
JNIEXPORT void JNICALL Java_Printf3_fprint(JNIEnv* env, jclass cl, jobject out, jstring format, jdouble x)
{
const char* cformat;
char* fmt;
jstring str;
jclass class_PrintWriter;
jmethodID id_print;
cformat = (*env)->GetStringUTFChars(env, format, NULL); fmt = find_format(cformat);
if (fmt == NULL)
str = format;
else
{
char* cstr;
int width = atoi(fmt);
if (width == 0) width = DBL_DIG + 10;
cstr = (char*)malloc(strlen(cformat) + width);
sprintf(cstr, cformat, x) ;
str =cstr);
free(cstr);
}
(*env)->ReleaseStringUTFChars(env, format, cformat);
/* Теперь вызываем ps.print(str) */
/* Возврат класса */ class_PrintWriter = (*env)->GetObjectCLass(env, out);
/* Возврат идентификатора метода */ id_print = (*env)->GetMethodID(env, class_PrintWriter,
"print", ” (Ljava/lang/String; )V) ;
/* Вызов метода */
(*env)->CallVoidMethod(env, out, id_print, str);
Вызов методов на языке Java из кода на языке С
Функция jmethodlD GetMethodID(JNIEnv *env, jclass cl, const char name [ ] , const char sig[]) возвращает идентификатор метода класса.
Параметры:
envуказатель HaJNI-интерфейс
clобъект-класс
nameимя метода
sigзакодированная сигнатура метода
Функцияvoid CallXxxMethod(JNIEnv *env, jobject obj,
jmethodID id, args) вызывает метод с переменным количеством аргументов, которые перечисляются после идентификатора метода id. Этот метод возвращает значение типа Ххх (Object, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
envуказатель наJNI-интерфейс
objнеявный аргумент метода
idидентификатор метода
argsаргументы метода
Функция void CallXxxMethodA(JNIEnv*env,jobject obj,
jmethodID id, jvalue args [ ] ) вызывает метод с переменными аргументами, которые перечислены в массиве значений типа jvalue, каждое из которых представляет собой показанную ниже структуру-объединение.
typedef union jvalue {
jboolean z;
jbyte b;
j char c;
jshort s;
j int i;
J long j;
jfloat f;
jdouble d;
jobject 1;
jvalue;
}
Этот метод возвращает значение типа Ххх (Obj ect, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
envуказатель наJNI-интерфейс
objнеявный аргумент метода
idидентификатор метода
argsаргументы метода
Функция voidCallXx^dtethodV (JNIEnv *env,jobjectobj,
jmethodID id, va_list args) вызывает метод с аргументами, которые
перечислены в структуре типа va_list, которая определена в заголовочном файле stdarg.h языка С. Этот метод возвращает значение типаXxx(Object, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
envуказатель HaJNI-интерфейс
objнеявный аргумент метода
idидентификатор метода
argsаргументы метода
Функция void CallNonvirtualXxxMethod(JNIEnv *env, jobject obj, jclass cl, jmethodID id, args), минуя обычный динамический механизм управления вызовами, вызывает метод с переменным количеством аргументов, которые перечисляются после идентификатора метода id. Этот метод возвращает значение типа Ххх (Object, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
envуказатель наJNI-интерфейс
objнеявный аргумент метода
clкласс, который содержит реализацию вызываемого метода
idидентификатор метода
argsаргументы метода
Функция void CallNonvirtualXxxMethodA (JNIEnv *env, jclass cl, jmethodID id, jobject obj, jvalue args []) , минуя обычный динамический механизм управления вызовами, вызывает метод с переменными аргументами, которые перечислены в массиве значений типаj value, каждое из которых представляет собой уже показанную выше структуру-объединение.
Параметры:
env obj cl id
указатель HaJNI-интерфейс
неявный аргумент метода
класс, который содержит реализацию вызываемого метода идентификатор метода
args
аргументы метода
Функция void CallNonvirtualXxxMethodV(JNIEnv *env,jobject
obj,
jclass cl, jmethodID id, va_list args), минуя обычный ди-
намический механизм управления вызовами, вызывает метод с аргументами, которые перечислены в структуре типа va_list, которая определена в заголовочном файле stdarg.h языка С. Этот метод возвращает значение типа Xxx(Object, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
env obj cl id
указатель HaJNI-интерфейс
неявный аргумент метода
класс, который содержит реализацию вызываемого метода идентификатор метода
args
аргументы метода
Функция jmethodID GetStaticMethodID(JNIEnv *env, jclass cl, const char name [ ] , const char sig[] ) возвращает идентификатор статического метода класса.
Параметры:
envуказатель н a JNI-интерфейс
c1объект-класс
nameимя метода
sigзакодированная сигнатура метода
Функция void CallStaticXxxMethod (JNIEnv *env, jclass cl, jmethodID id, args) вызывает статический метод с переменным количеством аргументов, которые перечисляются после идентификатора метода id. Этот метод возвращает значение типа Ххх (Object, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
envуказатель наJNI-интерфейс
clкласс статического метода
idидентификатор метода
argsаргументы метода
Функция void CalIXxxMethodA(JNIEnv*env,jclass cl,
jmethodID id, jvalue args[]) вызывает статический метод с переменными аргументами, которые перечислены в массиве значений типа jvalue, каждое из которых представляет собой показанную ниже структуру-объединение. Этот метод возвращает значение типа Ххх (Object, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
envуказатель наJNI-интерфейс
clкласс статического метода
idидентификатор метода
argsаргументы метода
Функция void CallXxxMethodV(JNIEnv *env, jclass cl, jmethodID id, va_list args) вызывает статический метод с аргументами, которые перечислены в структуре типа va_list, которая определена в заголовочном файле stdarg.h языка С. Этот метод возвращает значение типа Ххх (Object, Boolean, Byte, Char, Short, Int, Long, Float или Double).
Параметры:
envуказатель наJNI-интерфейс
clкласс статического метода
idидентификатор метода
argsаргументы метода
Функция jobj ect NewObj ect(JNIEnv *env, jclass cl', jmethodID id, args) вызывает метод-конструктор объекта с переменным количеством аргументов, которые перечисляются после идентификатора метода id. Идентификатор этого метода возвращается с помощью функции GetMethodID с указанием имени метода в виде строки "<init>" и закодированной сигнатуры конструктора с возвращаемым типом void.
Параметры:
envуказатель HaJNI-интерфейс
clкласс создаваемого объекта
idидентификатор метода-конструктора
argsаргументы метода-конструктора
Функция j object NewObjectA(JNIEnv *env, j class cl, jmethodID id, j value args [ ]) вызывает метод-конструктор объекта с переменными аргументами, которые перечислены в массиве значений типа jvalue, каждое из которых представляет собой показанную ниже структуру-объединение. Идентификатор этого метода возвращается с помощью функции GetMethodID с указанием имени метода в виде строки "<init>" и закодированной сигнатуры конструктора с возвращаемым типом void.
Параметры:
envуказатель HaJNI-интерфейс
clкласс создаваемого объекта
idидентификатор метода-конструктора
argsаргументы метода-конструктора
Функция j objectNewObjectV(JNIEnv*env,jclass cl,
jmethodID id, va_list args) вызывает метод-конструктор объекта с аргументами, которые перечислены в структуре типа va_list, которая определена в заголовочном файле stdarg. h языка С. Идентификатор этого метода возвращается с помощью функции GetMethodID с указанием имени метода в виде строки "<init>" и закодированной сигнатуры конструктора с возвращаемым типом void.
Параметры:
envуказатель HaJNI-интерфейс
clкласс создаваемого объекта
idидентификатор метода-конструктора
argsаргументы метода-конструктора
Массивы
Все типы массивов в языке Java имеют соответствующие им типы массивов в языке С, которые перечислены в табл. 11.2.
Таблица 11.2.Схема соответствия между типами массивов в языках Java и С
JavaС
boolean[]byte[] char[]boolean[]byte[] char[]jbooleanArray
boolean[]
byte[] char[]
boolean[]
byte[] char[]
jbyteArray
j charArray
int[]short[]long[]float []int[]short[]long[]float []j intArray
int[]
short[]
long[]
float []
int[]
short[]
long[]
float []
j shortArray
jlongArray
jfloatArray
doublet]jdoubleArray
Object[]jobjectArray
Тип jarray обозначает массив общего типа.
В языке С все типы массивов являются синонимами типа jobject. Однако, в языке C++ они упорядочены в иерархической структуре наследования, которая схема- K.тически показана на рис. 11.2.
Функция GetArrayLengthвозвращает длину массива.
jarray array = ... ;
jsize length = ( * env) - >GetArrayLength (env, array);
Способ доступа к элементам массива зависит от того, хранятся ли в нем объекты или значения примитивных типов (например, boolean, char или числовых типов). Для доступа к элементам массива используются методы GetObjectArrayElement и SetObj ectArrayElement.
jobjectArray array = ... ;
int i, j;
jobject x = (‘env)->GetObjectArrayElement(env, array, i);
(*env)->SetObjectArrayEleinent(env, array, j, x) ;
Несмотря на простоту, этот подход очень неэффективен, потому что хотелось бы иметь непосредственный доступ, особенно при выполнении векторных или матричных операций.
Функция GetXxxArrayElements возвращает С-указатель на начальный элемент массива. Как и при использовании обычных строк, по окончании работы с этим указателем всегда нужно вызывать функцию ReleaseXxxArrayElements для уведомления виртуальной машины Java. Здесь Ххх обозначает примитивный тип, а не тип- объект. С помощью С-указателя можно непосредственно считывать и записывать значения элементов массива. Однако, указатель может указывать на копию массива, поэтому любые изменения будут отражены в исходном массиве только после соответствующего вызова функции ReleaseXxxArrayElements.
Чтобы определить, с какой разновидностью массива вы работаете (с исходным массивом или его копией), нужно использовать в качестве третьего параметра функции GetXxxArrayElementsпеременную типа jboolean. Если используется копия массива, то переменная будет содержать значение JNI_TRUE, а если эта информация вас не интересует, то в качестве третьего параметра функции GetXxxArrayElements следует
передать значение NULL.
—
jbooleanArray
—
jbyteArray
—
jcharArray
—
jshortArray
jintArray
—
jlongArray
—
jfloatArray
—
jdoubleArray
Рис. 11.2.Иерархия наследования типов для массивов в языке C++
Ниже приводится фрагмент кода, в котором все элементы массива со значениями типа double умножаются на постоянное значение scaleFactor типа double. В нем с помощью С-указателя а на Java-массив предоставляется доступ к отдельным элементам а [ i ].
jdoubleArray array_a =
double scaleFactor = ... ;
double* a = (*env)->GetDoubleArrayElements(env, array_a, NULL);
for (i = 0; i < (*env)->GetArrayLength(env, array_a); i++)
a[i] = a[i] * scaleFactor;
(*env)->ReleaseDoubleArrayElements(env, array_a, 0);
В зависимости от способа выделения памяти для массивов и организации "сборки мусора" виртуальная машина Java может копировать или не копировать этот массив. Некоторые копирующие "сборщики мусора" перемещают объекты и обновляют ссылки на объекты. Эта стратегия не совместима с концепцией "закрепления" массива в некотором месте, потому что "сборщик мусора" не может обновлять значения указателей во встраиваемом коде. Однако, "сборщик мусора" в виртуальной машине, которая входит в состав пакета SDK, не является копирующим, а значит, в этой виртуальной машине массивы не закрепляются.
В виртуальной машине Sun JVM массивы значений типа boolean представлены в виде запакованных массивов, а метод GetBooleanArrayElements копирует их в незапакованный массив значений jboolean.
Для доступа к нескольким элементам большого массива используются методы GetXxxArrayRegion и S e t XxxAr r ay Reg i on, которые копируют элементы Java- массива в заданном диапазоне в С-массив и обратно.
Для создания новых Java-массивов во встраиваемых методах можно использовать функцию NewXxxArray. Для создания нового массива объектов нужно указать его длину, тип элементов массива, а также исходное значение для всех элементов (как правило,значениеNULL).
jclass class_Employee =
jobjectArray array_e = (*env)->NewObjectArray(env, 100, class_Ernployee, NULL) ;
Массивы элементов примитивных типов устроены проще, т.е. для них достаточно указать длину массива.
jobjectArray array_d = (*env)->NewDoubleArray(env, 100);
Такой массив заполняется нулями.
В пакете SDK версии 1.4 для работы с буферами используются три метода JNI- интерфейса.
jobject NewDirectByteBuffer(JNIEnv *env, void* address, jlong capacity)
void* GetDirectBufferAddress(JNIEnv *env, jobject buf) jlong GetDirectBufferCapacity(JNIEnv *env, jobject buf)
Прямые буферы используются в пакете j ava.nio для поддержки более эффективных операций ввода/вывода, а также для минимизации копирования данных между встраиваемыми и Java-массивами.
Функции управления массивами на языке Java в коде на языке С
Функция jsize GetArrayLength(JNIEnv env, jarray array) возвращает количество элементов массива.
Параметры:
envуказатель HaJNI-иитерфейс
arrayобъект-массив
Функция jobject GetObjectArrayElement(JNIEnv *env, jobjec- tArray array, jsize index) возвращает значение элемента массива с указанным индексом.
Параметры:
env
array index
указатель HaJNI-интерфейс объект-массив
индекс элемента
Функция void SetObjectArrayElement(JNIEnv *env, jobjectAr- ray array, jsize index, jobject value) задает новое значение для элемента массива с указанным индексом.
Параметры:
env
array index value
указатель HaJNI-интерфейс объект-массив
индекс элемента
новое значение
содержит значение JNI_COMMIT, если не нужно освобождать буфер elems после обновления элементов массива, содержит значение JNI_ABORT, если нужно освобождать буфер elems без обновления элементов массива
Функция void GetXxxArrayRegion(JNIEnv *env, jarray array, jint start, jint length, Xxx elems []) копирует элементы из Java- массива в С-массив. Здесь Ххх обозначает один из следующих типов данных: Boolean, Byte, Char, Short, Int, Long, Float или Double.
Параметры:
envуказатель HaJNI-интерфейс
arrayобъект-массив
startначальный индекс
lengthколичество копируемых элементов
elemsуказатель на С-массив, который содержит копируемые элементы
Функция void SetXxxArrayRegion(JNIEnv *env, jarray array, jint start, jint length, Xxx elems[]) копирует элементы из С- массива в Java-массив. Здесь Xxx обозначает один из следующих типов данных: Boolean, Byte, Char, Short, Int, Long, Float или Double.
Параметры:
envуказатель наJNI-интерфейс
arrayобъект-массив
startначальный индекс
lengthколичество копируемых элементов
elemsуказатель на С-массив, который содержит копируемые элементы
Обработка исключительных ситуаций
Использование встраиваемых методов сопряжено со значительным риском обеспечения безопасности в программах на языке Java. В языке С не предусмотрена защита от ошибочного указания диапазона массивов, использования неправильных указателей и т.д. Для сохранения целостности платформы Java программистам при использовании встраиваемых методов необходимо уделить особое внимание обработке исключительных ситуаций, связанных с такими ошибками. В частности, если встраиваемый метод обнаруживает какую-то проблему, с которой не может справиться, то он должен сообщить о ней виртуальной машине Java, которая сгенерирует соответствующую исключительную ситуацию. Однако, в языке С не предусмотрена обработка исключительных ситуаций. Поэтому для создания нового объекта исключительной ситуации нужно вызвать функцию Throw или ThrowNew, которая при выходе встраиваемого метода генерирует исключительную ситуацию в виртуальной машине}ауа.
Для использования функции Throw следует вызвать функцию NewObject для создания подчиненного объекта Throwable. Например, в приведенном ниже фрагменте кода создается объект EOFException и генерируется соответствующая исключительная ситуация.
jclass class_EOFException = (*env)->FindClass(env,
"java/io/EOFException") ;
jmethodID id_EOFException =
class_EOFException,
"<init>", "()V"); /* ID of default constructor */
jthrowable obj_exc = (*env)->NewObject(env, class_EOFException, id_EOFException);
(*env)->Throw(env, obj_exc);
Однако, на практике удобнее использовать функцию ThrowNew, которая создает объект исключительной ситуации для заданного класса и строки в формате UTF.
(*env)->ThrowNew(env, (*env)->FindClass(env,
"java/io/EOFException") ,
"Unexpected end of file");
Обе функции Throw и ThrowNew всего лишь сообщают об исключительной ситуации, а не прерывают поток управления встраиваемого метода. Поэтому виртуальная машина Java может генерировать исключительную ситуацию только после возврата данного метода. Это значит, что после каждого вызова функции Throw или функции ThrowNew должен располагаться оператор return.
При реализации встраиваемых методов на языке C++ пока еще не предусмотрена № генерация Java-объектов для исключительных ситуаций на языке C++. При связывании кода на языке C++ можно было бы реализовать трансляцию исключительных ситуаций для языков C++ и Java, однако, в настоящее время эта возможность не реализована. Для генерации Java-объектов для исключительных ситуаций во встраиваемом коде на языке C++ придется использовать функции Throw и ThrowNew, а также позаботиться о том, чтобы встраиваемые методы не генерировали исключительные ситуации языка C++.
Обычно встраиваемый код не должен заниматься обработкой исключительных ситуаций. Однако, при вызове во встраиваемом коде метода на языке Java, он может генерировать исключительную ситуацию. Более того, некоторые JNI-функции также могут генерировать исключительные ситуации. Например, функция SetObjectArrayElement генерирует исключительную ситуацию ArraylndexOutOfBoundsEx- ception, если значение индекса выходит за границы массива, а также исключительную ситуацию ArrayStoreException, если класс хранимого объекта не является подчиненным классом элемента массива. В таких ситуациях встраиваемый метод должен вызывать функцию ExceptionOccured, чтобы определить наличие генерированной исключительной ситуации. Показанный ниже вызов этой функции возвращает значение NULL, если никакие исключительные ситуации не генерируются, либо значение-ссылку на текущий объект исключительной ситуации.
jtrowable obj_exc = (*env)->ExceptionOccured(env);
Для проверки наличия генерированной исключительной ситуации без возвращения ссылки на нее можно использовать функцию ExceptionCheck.
jboolean occured = (*env)->ExceptionCheck(env);
Обычно встраиваемый метод возвращает исключительную ситуацию, которую виртуальная машина Java может делегировать соответствующему фрагменту кода на языке Java. Однако, встраиваемый метод может проанализировать объект исключительной ситуации для определения возможности ее обработки собственными силами.
Если это возможно, то с помощью следующего метода можно отозвать сообщение об этой исключительной ситуации.
(*env)->ExceptionClear(env);
В примере программы, код которой приведен в конце этого раздела, реализуется встроенный метод fprintf с некоторыми чрезмерными на первый взгляд, но очень полезными на практике, мерами по обработке следующих исключительных ситуаций.
NullPointerException, если указатель строки форматирования содержит значение NULL.
I1legalArgumentException, если строка форматирования не содержит спецификатор %, необходимый для вывода значений типа double.
OutOfMemoryError, если произошла ошибка выделения памяти при выполнении функции malloc.
Наконец, для демонстрации способа проверки наличия генерированной исключительной ситуации при вызове Java-метода из встраиваемого метода в этом примере организована посимвольная передача строки в поток с вызовом функции ExceptionOccured после вывода каждого символа. В листинге 11.17 приведен код встраиваемого метода, а в листинге 11.18— определение класса, содержащего встраиваемый метод. Обратите внимание, что встраиваемый метод при возникновении исключительной ситуации в функции Printwriter.print прекращает свою работу не мгновенно, а только после освобождения буфера cstr. И только после возращения встраиваемого метода виртуальная машина Java снова генерирует исключительную ситуацию. В тестовой программе, код которой приведен в листинге 11.19, показан способ генерации исключительной ситуации во встраиваемом методе для неправильно заданной строки форматирования.
Листинг 11.17. Prlntf4.c 1
/**
@version 1.10 1997-07-01
©author Cay Horstmann
*/
#include "Printf4.h"
#include <string.h>
ttinclude<stdlib.h>
#include <float.h>
/**
@param format a string containing a printf format specifier (such as "%8.2f"). Substrings "%%" are skipped.
©return a pointer to the format specifier (skipping the '%') or NULL if there wasn't a unique format specifier
*/
char* find_format(const char format[])
{
char* p;
char* q;
p = strchr(format, '%');
while (p != NULL && *(p + 1) == '%') /* skip %% */
p = strchr(p + 2 , ' % 1 ) ;
if (p == NULL) return NULL;
/* now check that % is unique */
q=strchr(p, '%');
while (q != NULL && *(q + 1) == ’ % ') /* skip %% */
q = strchr(q + 2, 1 % ') ;
if (q != NULL) return NULL; /* % not unique */
q = p + strspn(p, " -0+#"); /* skip past flags */
q += strspn(q, "0123456789"); /* skip past field width */
if (*q == ' . ’) { q++; q += strspn(q, "0123456789"); }
/* skip past precision */
if (strchr("eEfFgG", *q) == NULL) return NULL;
/* not a floating point format */
return p;
JNIEXPORT void JNICALL Java_Printf4_fprint(JNIEnv* env, jclass cl, jobject out, jstring format, jdouble x)
{
const char* cformat;
char* fmt;
jclass class_PrintWriter;
jmethodID id_print;
char* cstr;
int width;
inti;
if (format == NULL)
{
(*env)->ThrowNew(env,
(*env)->FindClass(env,
"java/lang/NullPointerException"), "Printf4.fprint: format is null"); return;
}
cformat = (*env)->GetStringUTFChars(env, format, NULL); fmt = find_format(cformat);
if (fmt == NULL)
{
(*env)->ThrowNew(env,
(*env)->FindClass(env,
"java/lang/IllegalArgumentException"),
"Printf4.fprint: format is invalid"); return;
}
width = atoi(fmt);
if (width == 0) width = DBL_DIG + 10;
cstr = (char*Jmalloc(strlen(cformat) + width);
if (cstrNULL)
{
(*env)->ThrowNew(env,
(*env)->FindClass(env, "java/lang/OutOfMemoryError")> "Printf4 . fprint: malloc failed");
return;
}
sprintf(cstr, cformat, x) ;
(*env)->ReleaseStringUTFChars(env, format, cformat);
/* Теперь вызываем ps.print(str) */
/* Возврат класса */
class_PrintWriter =out);
/* Возврат идентификатора метода */ id_print =
"print", "(C)V');
/* Вызов метода */
for (i = 0; cstr[i] != 0 && !(*env)->ExceptionOccurred(env); i++)
(‘env)->CallVoidMethod(env, out, id_print, cstr[i]);
free(cstr) ;
Листинг11.18.Printf4.java
/**
©version 1.10 1997-07-01
©author Cay Horstmann
*/
import java.io.*;
class Printf4
{
public static native void fprint(Printwriter ps, String format, double x) ;
static
{
System.loadLibrary("Printf4");
Листинг11.19.Print£4Test.java
/**1.10 1997-07-01
©author Cay Horstmann
*/
import java.io.*;
class Printf4Test
{
double price = 44.95;double tax= 7.75;double amountDue = price * (1 + tax/ 100),double price = 44.95;double tax= 7.75;double amountDue = price * (1 + tax/ 100),public static void main(String[] args) {
double price = 44.95;
double tax= 7.75;
double amountDue = price * (1 + tax/ 100),
double price = 44.95;
double tax= 7.75;
double amountDue = price * (1 + tax/ 100),
Printwriter out = new Printwriter(System.out);
/* Этот вызов генерирует исключительную ситуацию
из-за неверного формата — %% */
Printf4.Sprint(out, "Amount due = %%8.2S\n", amountDue); out.Slush() ;
Обработка исключительных ситуаций в коде на языке С
Функцияjint Throw(JNIEnv *env, jthrowable obj) подготавливает исключительную ситуацию, которая будет генерироваться после выхода из встраиваемого кода. Она возвращает значение 0 в случае успешного исхода или отрицательное значение в противном случае.
Параметры:
envуказатель HaJNI-интерфейс
objгенерируемый объект исключительной ситуации
Функция jint ThrowNew(JNIEnv *env, jclass cl, char msg[]) подготавливает исключительную ситуацию, которая будет генерироваться после выхода из встраиваемого кода, с указанием строки-комментария об этой исключительной ситуации. Она возвращает значение 0 в случае успешного исхода или отрицательное значение в противном случае.
Параметры:
envуказатель наJNI-интерфейс
clкласс, генерируемый исключительной ситуацией
msgстрока в формате UTF, которая является аргументом типа
String для объекта исключительной ситуации
Функция jthrowable Except ionOccured(JNIEnv *env) возвращает объект исключительной ситуации в очереди либо значение NULL в противном случае.
Параметры:
env указатель на JNI-интерфейс
Функция j boolean ExceptionCheck(JNIEnv *env) возвращает значение true, если в очереди находится какой-то объект исключительной ситуации.
Параметры:
envуказатель наJNI-интерфейс
Функция void ExceptionClear(JNIEnv *env) удаляет из очереди все объекты исключительных ситуаций.
Параметры:
envуказатель наJNI-интерфейс
API-интерфейс запуска
До сих пор рассматривались программы на языке Java, которые выполняют лишь несколько вызовов С-функций потому, что код на языке С выполняется быстрее или предоставляет доступ к функциям, которых нет в платформе Java. Допустим, что воз-
никла обратная ситуация, т.е. в программе на языке С или C++ нужно организовать несколько вызовов Java-методов, например, потому что некоторые действия проще программируются на языке Java. Нам уже известны способы вызова Java-методов из кода на языке С или C++ и остается только разобраться в том, как запустить виртуальную машину Java для их интерпретации. Именно для этого предназначен специальный API-интерфейс запуска {invocation API), который используется для вставки виртуальной машиныJava в программу на языке С или C++. Ниже приводится минимальный фрагмент кода, необходимый для инициализации виртуальной машины Java в программе на языке С или C++.
JavaVMOption options[2];
JavaVMInitArgs vm_args;
JavaVM *jvm;
JNIEnv *env;
long status;
options[0].optionstring =
memset(&vm_args, 0, sizeof(vm_args)) ;
vm__args. version =
vm_args.nOptions = 1;
vm_args.options = options;
status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
Функция JNI_CreateJavaVM создает виртуальную машину Java, а также указатель jvm на виртуальную машину и указатель env на среду выполнения.
Для виртуальной машины Java можно указать произвольное количество параметров за счет увеличения размера массива options и значения параметра vm_args.nOptions. Например, ниже приведен пример использования параметра для отключения оперативного JIT-компилятора (преобразующего псевдокод в машинные команды в процессе исполнения программы).
options[i].optionstring =
При возникновении ошибок в работе или сбоя программы, невозможности инициализации виртуальной машины Java или загрузки классов следует включить режим отладки с помощью указанного параметра.
options[i].optionstring =
В этом случае инициализация виртуальной машины Java будет сопровождаться потоком сообщений. А если не удается загрузить классы, то рекомендуется проверить указанный путь к библиотекам классов.
После установки виртуальной машины Java можно вызывать Java-методы тем способом, который описан в предыдущих разделах, т.е. на основе указателя env. При этом указатель jvm понадобится только для вызова других функций API-интерфейса запуска. В настоящее время существуют всего четыре такие функции, а наиболее важной среди них является функция DestroyJavaVM, которая предназначена для прекращения работы виртуальной машиныJava.
(*jvm)~>DestroyJavaVM(jvm);
В программе на языке С, код которой приведен в листинге организован запуск виртуальной машиныJava, а затем вызов методаmain класса Welcome, который описан в главе 2 тома 1. (Перед запуском этой программы необходимо откомпилировать файл
Листинг
/**1.10 1999-11-13
Cay
*/
#include
#include <stdlib.h>
int
{
JavaVMOption options[2];
vm_args;
JavaVM *jvm;■
JNIEnv *env;
long status;
class_Welcome;
jclass class_String;
args;
id_main;
=
memset(&vm_args, 0, sizeof(vm_args));
=
= 1;
= options;
status =
if (status == JNI_ERR)
{
creating
return 1;
}
="Welcome");
=class_Welcome,
"main", "([Ljava/lang/String;)V");
class_String =
args =0, class_String, NULL);
class_Welcome,
id_main, args);
(* j
return
Для компиляции этой программы под управлением операционной системы Linux следует использовать приведенную ниже команду.
gcc —l/usr/local/jdk/include -I/usr/local/jdk/include/linux
—о InvocationTest
4> —L/usr/local/jdk/jre/lib/i386/client
^ —Ijvm
с
Для компиляции данной программы под управлением операционной системы Solaris следует использовать другую команду.
cc -I/usr/local/jdk/include —I/usr/local/jdk/include/solaris
—о InvocationTest
%* -L/usr/local/jdk/jre/lib/sparc
-ljvm
^InvocationTest.c
Для компиляции этой программы под управлением операционной системы Windows с помощью компилятора Microsoft С следует использовать такую команду.
cl —Ic:Xjdk\include -Ic:Xjdk\includeXwin32
^ InvocationTest.c c:\jdk\lib\jvm.lib
Для компиляции настоящей программы в оболочкеследует выполнить несколько других операций. Сначала нужно создать файлсо следующим выра
жением.
EXPORTS
JNI_CreatejavaVM@12
Затем следует запустить такую команду.
dlltool —k
с:
0» —output-lib jvm. a
И наконец, откомпилировать код.
-Ic:\jdk\include
-Dlong"
-о InvocationTest
*b InvocationTest.cjvm.a
Перед запуском программы под управлением операционной системы Linux/UNIX следует убедиться в том, что переменная окружениясодержит пу
ти к каталогам с совместно используемыми библиотеками. Например, при использовании командного процессора bash под управлением операционной системы Linux для этого следует использовать такую команду.
export
4»:/usr/local/jdk/jre/lib/386
Перед запуском программы под управлением операционной системы Windows нужно убедиться в том, что переменная окружения PATH содержит путь к каталогу c:\jdk\jre\bin\client.
Точные расположения разных библиотечных файлов зависят от используемой версии пакета SDK. Для их обнаружения следует найти файлы libjvm.so, jvm.dll или (для более старых версий SDK) libjava.so в каталогах jdk/bin, jdk/lib и jdk/jre/lib, а потом соответствующим образом исправить значения переменных окружения.
При создании приложения, которое вызывает виртуальную машину Java с помощью операционной системы Windows, не следует доверять пользователям указание пути к библиотекам. Следует помочь пользователям и вручную загрузить все совместно используемые библиотеки и DLL-модули. Именно эту операцию выполняют программы javac и java, В качестве примера можно привести файл 1auncher/j ava_md. с в файле src,jar, который является частью пакета SDK.
Функции запуска
Функция jint JNI_CreateJavaVM(JavaVM** p_jvm, void** p_env, JavaVMInitArgs* vm_args) инициализирует виртуальную машинуJava. В случае успешного завершения эта функция возвращает значение o, а в противном случае — значение JNI_ERROR.
Параметры:
P_ j vmуказатель на таблицу API-функций запуска
p_envуказатель на таблицу JNI-функций
vm_argsаргументы виртуальной машины
• Функция jint DestroyJavaVM(JavaVM** jvm) удаляет виртуальную машину Java. В случае успешного завершения эта функция возвращает значение 0, а в противном случае— отрицательное значение. Данная функция может вызываться с помощью указателя виртуальной машины, например, (*jvm)->DestroyJavaVM(jvm).
Параметры:
j vmуказатель виртуальной машины
Пример доступа к реестру Windows
В настоящем разделе рассматривается практический пример использования всех перечисленных в этой главе способов работы со встраиваемыми методами для обработки строк, массивов, объектов, вызова конструкторов и обработки исключительных ситуаций. Основная цель раздела заключается в создании Java-оболочки для подмножества API-функций на языке С, предназначенных для работы с реестром операционной системы Windows. Из-за использования в ней функций для работы со специфическим объектом, т.е. реестром Windows, она, конечно, не может быть перенесена на другие платформы. Именно по этой причине в языке Java не предусмотрены никакие инструменты для работы с реестром, а предполагается, что разработчики используют для этого встраиваемый код.
Обзор реестра Windows
Как известно, реестр представляет собой депозитарий конфигурационных параметров операционной системы Windows и прикладных программ. Этой теме посвя-
щена прекрасная книга Inside the Windows 95 Registry Рона Петруша (Ron Petrusha) [O'ReillyВ более старых версиях Windows конфигурационные параметры опе
рационной системы и прикладных программ хранились в так называемых INI-файлах. Однако, реестр обладает несколькими неоспоримыми преимуществами.
В INI-файлах данные хранятся в виде строк, а в реестре допускается хранение других типов данных, например, целочисленных значений и байтовых массивов.
Разделыне могут иметь подчиненных разделов, а в реестре поддержи
вается древовидная структура.
Конфигурационные параметры распределены среди нескольких INI-файлов, а размещение сразу всех параметров в одном реестре позволяет легко их администрировать и создавать резервную копию.
Недостатком реестра является уязвимость по отношению к сбою в работе реестра, потому что в таком случае компьютер может работать с ошибками или даже не загрузиться! Представленная в этом примере программа корректно работает с реестром, но, прежде чем вносить какие-либо изменения в реестр, необходимо создать резервную копию реестра. Более подробные сведения по данной теме можно найти в упомянутой выше книге.
Авторы не рекомендуют использовать реестр для хранения конфигурационных параметров приложений на языке Java. Для этого лучше использовать XML-файлы, подробнее об этом см. главу 12. Здесь реестр используется для демонстрации включения нетривиальных встраиваемых API-функций Java-класс.
'^IQJxJ
,B«M'.£dil.Yiew_Eavoritet H*„-Systems'• 'LJ Adaptec: tAdobe,B«M'.£dil.Yiew_Eavoritet H*„-Systems'• 'LJ Adaptec: tAdobeNww_ .. J §У (Default) J^JavaHome ^MicfoVersionNww_ .. J §У (Default) J^JavaHome ^MicfoVersion""11''not sell "d;W.4" "0"not sell "d;W.4" "0"• •• •Computer, IncComputer, IncLJiLJ CLASSESLJ ClientsLJiLJ CLASSESLJ Clientsi'i',,':':i ffi^CybefUr* лLJSolutionsLJT.ft.LJLJ Ghostgum LJ Glassbooki ffi^CybefUr* лLJSolutionsLJT.ft.LJLJ Ghostgum LJ Glassbook
,B«M'.£dil.Yiew_Eavoritet H*„
-
Systems
'• 'LJ Adaptec
: tAdobe
,B«M'.£dil.Yiew_Eavoritet H*„
-
Systems
'• 'LJ Adaptec
: tAdobe
Nww_ .. J §У (Default) J^JavaHome ^MicfoVersion
Nww_ .. J §У (Default) J^JavaHome ^MicfoVersion
"
"
1
1
'
'
not sell "d;W.4" "0"
not sell "d;W.4" "0"
• •
• •
Computer, Inc
Computer, Inc
LJ
iLJ CLASSES
LJ Clients
LJ
iLJ CLASSES
LJ Clients
i
'
i
'
,
,
':
':
i ffi^CybefUr* л
LJSolutions
LJT.
ft.LJ
LJ Ghostgum LJ Glassbook
i ffi^CybefUr* л
LJSolutions
LJT.
ft.LJ
LJ Ghostgum LJ Glassbook
;;LJ Hewlett-Packard
;
;
r- LJ
i HuJ Java Developme
13
^C«4»i«AHKh^O»LJMWNE\s6FTWlEUavaSoft^^
Редактор реестра
Основным инструментом для работы с реестром является редактор реестра (registry editor). Его применение чревато большими неприятностями для наивных и неопытных
пользователей, поэтому для его запуска не предусмотрено никаких визуальных элементов пользовательского интерфейса. Для запуска редактора реестра в режиме командной строки (или в диалоговом окне, которое появится на экране после выбора команды меню Start^Run)введите командуregedit. На рис. 11.3показан внешний редактора реестра.
В левой части окна редактора реестра представлены разделы, упорядоченные в иерархической древовидной структуре. Обратите внимание, что все разделы находятся в одном из перечисленных ниже узлов с префиксом HKEY.
HKEY_CLASSES_R00T
HKEY_CURENT_USER
HKEY_LOCAL_MACHINE
В правой части окна редактора реестра представлены пары параметр/значение, которые связаны с каким-то разделом.
Например, данный раздел
HKEY_LOCAL_MACHINE\SOFTWARE\JavaS оf t\Java Development Kit\1.4
может иметь перечисленные ниже пары параметр/значение.
JavaHome="c:\jdkl.4"
MicroVersion=" 0”
В нашем примере значения представлены в виде строк, но они могут также быть целыми числами или массивом байтов.
Интерфейс доступа креестру на основе платформы Java
Попробуем реализовать простой интерфейс для организации доступа к реестру из кода на языке Java, а также реализовать его с помощью встраиваемого кода. Этот интерфейс позволяет выполнять только несколько операций с реестром. Для сокращения размера кода в нем опущены многие другие важные операции, например, вставка, удаление и перечисление разделов. (Следуя предложенной здесь модели и используя информацию из упомянутой выше книги, можно довольно легко добавить все другие API-функции.)
Даже используя ограниченный набор функций этого приложения, можно выполнять следующие действия с реестром.
Перечислять все параметры данного раздела.
Считывать значение параметра.
Задавать значение параметра.
Для выполнения таких действий используется следующий Java-класс, который инкапсулирует раздел реестра.
public class Win32RegKey
{
public Win32RegKey(int theRoot, String thePath) {...}
public Enumeration names() {...}
public native Object getValue(Stringname);
public native void setValue(Stringname, Object value);
public static final int HKEY_CLASSES_ROOT = 0x80000000;
public static final int HKEY_CURRENT_USER = 0x80000001;
public static final int HKEY_LOCAL_MACHINE = 0x80000002;
Метод names возвращает объект-перечисление, который содержит имена всех параметров данного раздела. Их можно получить с помощью методов hasMoreElements/nextElement. Метод getValue возвращает объект, который может быть строкой, объектом типа Integer или массивом байтов. Аналогично параметр value метода setValue также должен иметь такой тип.
Рассмотрим приведенный ниже раздел.
HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Development KitS1.4
Вот как выглядит простая функция, которая способна перечислить все параметры- строки этого раздела. (Измените значение раздела в соответствии с номером версии используемого вами пакета SDK.)
public static void main(String[] args) {
Win32RegKey key = new Win32RegKey(
Win32RegKey.HKEY„LOCAL—MACHINE, "SOFTWARESSJavaSoftSSJava Development KitSS1.4");
Enumeration enum = key.names();
while (enum.hasMoreElements())
String name = (String)enum.nextElement();
System.out.print(name+ " =");
) )
Типичным результатом выполнения такой программы будут следующие строки.
JavaHome=c:\jdkl.4 MicroVersion=0
Реализация функций доступа к реестру в виде встраиваемых методов
Итак, нам нужно реализовать следующие три действия.
Получение значения параметра.
Установка значения параметра.
Перечисление имен параметров раздела.
К счастью, мы уже познакомились со всеми инструментами, необходимыми для преобразования строк/массивов языка Java и строк/массивов языка С. Кроме того, в предыдущих разделах ознакомились со способом генерации и обработки исключительных ситуаций в случае возникновения каких-то ошибок.
Две особенности усложняют применение встраиваемых методов по сравнению с примерами из предыдущих разделов. Методы getValue и setValue работают с объ-
ектами, которые могут иметь один из следующих типов: String, Integer или byte [ ]. Кроме того, объект-перечисление должен сохранять информацию о состоянии между последовательными вызовами методов hasMoreElements/nextElement.
Рассмотрим сначала использование метода getValue в коде, который приводится в листинге 11.22и выполняет следующие действия.
Открыть раздел реестра, потому что это необходимо для чтения значений параметров раздела.
Запросить тип и размер значения данного параметра.
Считать данные в буфер.
Если значение параметра имеет тип REG_SZ (строка), то следует вызвать функцию NewStringUTFдля создания новой строки с этим значением.
Если значение параметра имеет тип REG_DWORD (32-разрядное значение целочисленного типа), то нужно запустить конструктор для создания нового значения типа Integer.
Если значение параметра имеет тип REG_BINARY (массив байтов), то необходимо вызвать функцию NewByteArray для создания нового массива байтов, а потом вызвать функцию SetByteArrayRegion для копирования этого значения данных в массив байтов.
Если значение параметра имеет какой-то другой тип, или при вызове API- функции доступа к реестру возникла какая-то ошибка, в таком случае нужно генерировать исключительную ситуацию и очень аккуратно освободить все ресурсы, используемые для выполнения перечисленных выше действий.
Закрыть раздел и возвратить созданный объект (типа String, Integer или byte[]).
Как видите, в этом примере достаточно четко иллюстрируются варианты создания Java-объектов разного типа.
В данном встраиваемом методе совсем не трудно организовать возвращение значение обобщенного типа, потому что ссылка на объекты типа j string, j int или byte [ ]простовозвращаетсякакссылкана объекттипаj object.Однако,метод
setValue получает ссылку на объект обобщенного типа Obj ect и должен определить его точный конкретный тип для сохранения значения в виде строки, целочисленного значения или массива байтов. Этот тип можно определить, запрашивая класс объекта- значения, обнаруживая ссылки на классы java.lang. String, java.lang. Integer иjava. lang. byte [ ], а затем сравнивая их с помощью функции IsAssignableFrom.
Например, для ссылок на классы classl и class2 показанный ниже метод возвратит значение JNT_TRUE, если классы classl и class2 имеют одинаковый тип или класс classl является подчиненным классом класса class2.
(*env)->IsAssignableFrom(env, classl, class2)
В любом из этих случаев ссылки на объекты класса classl должны быть приведены к типу класса class2.
Например, если показанный ниже метод возвращает значение true, то это значит, что значение value является массивом байтов.
(*env)->IsAssignableFrom(env,
(*env)->GetObjectClass(env, value)
(*env)->FindClass(env, ”[B"))
Теперь рассмотрим использование метода setValue в коде, который приводится в листинге 11.22 и выполняет следующие действия.
Открыть раздел реестра для записи.
Запросить тип и размер значения параметра.
ЕслизначениепараметраимееттипString,тоследуетвызватьфункцию
GetStringUTFCharsдля получения указателя строки и определения ее длины.
Если значение параметра имеет тип Integer, в таком случае следует вызвать функцию intValue для получения целочисленного значения, которое содержится в объекте-оболочке.
Еслизначениепараметраимееттипbyte [],то следуетвызвать функцию
GetByteArrayElements для получения указателя на массив байтов и определения его длины.
Передать реестру полученные сведения о типе данных и длине значения.
Закрыть раздел, а если значение имеет тип String или byte [ ], то освободить область памяти с указателем данной строки или массива.
Наконец, рассмотрим встраиваемые методы для перечисления разделов. Как показано в листинге 11.21, они входят в состав класса Win32RegKeyNameEnumeration. В начале процесса перечисления необходимо открыть раздел и сохранить его дескриптор на время процесса перечисления вместе с объектом-перечислением. Дескриптор имеет тип DWORD (32-разрядное значение целочисленного типа), а потому может быть сохранен в виде целочисленного значения языка Java в поле hkey объекта- перечисления. При запуске процесса перечисления это поле инициализируется с помощью метода SetlntField. В ходе дальнейшего использования объекта- перечисления значения поля hkey считываются с помощью метода GetlntField.
Как показано в примере, использование поля Java-объекта для сохранения состояния встраиваемых данных очень полезно для реализации встраиваемых методов.
В этом примере в объекте-перечислении также сохраняются три других элемента данных. При запуске процесса перечисления можно запросить реестр для подсчета пар параметр/значение и вычисления длины самого длинного имени параметра, которая потребуется для создания символьного массива на языке С. Данные значения сохраняются в полях count и maxsize объекта-перечисления. Наконец, поле index инициализируется значением -1 до начала процесса перечисления либо значением О для обозначения начала процесса перечисления с последующим увеличением этого значения на каждом этапе перечисления объектов.
Рассмотрим последовательность действий, которые выполняются методом hasMoreElements во время процесса перечисления.
Извлечь значения полей index и count.
Если поле index имеет значение -1, то вызвать функцию startNameEnumera- ti on, которая откроет раздел, запросит значение счетчика и максимальную
длину, инициализирует значения полей hkey, count, maxsize и index объекта-перечисления.
Возвратить значение JNI-TRUE, если значение поля index меньше значения поля count, либо возвратить значение JNI_FALSE в противном случае.
Метод nextElement во время процесса перечисления выполняет более сложную последовательность действий.
Извлечь значения полей index и count.
Если поле index имеет значение -1, то вызвать функцию startNameEnumeration, которая откроет раздел, запросит значение счетчика и максимальную длину, инициализирует значения полей hkey, count, maxsize и index объекта-перечисления.
Если значение поля index равно значению поля count, то генерировать исключительную ситуацию NoSuchElementException.
Считать следующее имя из реестра.
Увеличить значение поля index.
Если значение поля index равно значению поля count, то закрыть раздел.
. Для компиляции этой программы необходимо скомпоновать ее с библиотекой advapi32.lib.
Перед компиляцией программы на языке С не забудьте запустить утилиту javah для файлов классов Win32RegKey и Win32RegKeyNameEnumeration. Полностью команда компиляции имеет следующий вид.
cl -Ic:\jdkXinclude -Ic:\jdk\include\win32 —LD ^Win32RegKey.c advapi32.1ib —FeWin32RegKey.dll
А при использовании оболочки Cygwin команда компиляции примет следующий вид.
дсс —с —Ic:\jdkXinclude
—Ic:Xjdk\include\win32
-Ic: \cygwin\usr\include\win.32api
-Dint64="long long"
4>Win32RegKey.c
’bdllwrap -add-stdcall-alias
> -о Win32RegKey.dll Win32RegKey.o
В листинге 11.23 показан код тестовой программы для проверки созданных нами функций для работы с реестром. Их работа демонстрируется на примере вставки трех новых пар (параметр/значение, строка, целочисленное значение, массив байтов) в следующем разделе.
HKEY_LOCAL_MACHINE\SOFTWARE\Java S о f t\Java Development Kit\1.4
Измените значение этого раздела в соответствии с номером версии используемого вами пакета SDK или используйте какой-нибудь другой раздел реестра.
После перечисления всех параметров раздела и извлечения их значений данная программа выведет следующие строки.
JavaHome=c:\jdkl. 4
MicroVersion=0
Default user=Harry Hacker
Lucky number=13
Small primes=2 3 5 7 11
Хотя добавление пар параметр /значение никак не повлияет на работу компьютера, в случае необходимости эти параметры можно удалить с помощью редактора реестра.
Листинг 11.21.Win32RegKey.java
/**
(aversion 1.00 1997-07-01
©author Cay Horstmann
*/ import java.util.*;
/ * *
Объект Win32RegKey может быть использован для получения и возвращения значений параметров реестра Windows
*/
public class Win32RegKey {
/**
Создание объекта для раздела реестра
©param theRoot one of HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_CONFIG, HKEY_DYN_DATA
©param thePath the registry key path
*/
public Win32RegKey(int theRoot, String thePath) {
root = theRoot;
path = thePath;
}
/**
Перечисление всех имен разделов для указанного в объекте пути ©return an enumeration listing all entry names
*/
public Enumeration names() {
return new Win3 2RegKeyNameEnumeration(root, path);
)
/**
Возвращает имя раздела реестра
©param name the entry name ©return the associated value
*/ public native Object getValue(String паше);
/**
Задает значение раздела реестра
Sparam name the entry name
Sparam value the new value
*/
public native void setValue(String name, Object value)
public
static
final
int
HKEY.
_CLASSES_ROOT = 0x80000000;
public
static
final
int
HKEY.
_CURRENT_USER = 0x80000001;
public
static
final
int
HKEY.
_LOCAL_MACHINE = 0x80000002;
public
static
final
int
HKEY.
.USERS = 0x80000003;
public
static
final
int
HKEY.
_CURRENT_CONFIG = 0x80000005;
public
static
final
int
HKEY
DYN_DATA = 0x80000006;
private int root;
private String path;
static
{
System. loadLibrary ("Win32RegKey"J;
)
}
class Win32RegKeyNameEnumeration implements Enumeration
{
Win3 2RegKeyNameEnumeration(int theRoot, String thePath) {
root = theRoot;
path = thePath;
}
public native Object;
public native boolean hasMoreElements();
private int root;
private String path;
private int index = -1;
private int hkey = 0;
private int maxsize;
private int count;
}
class Win32RegKeyException extends RuntimeException
{
public Win32RegKeyException() {}
public Win32RegKeyException(String why)
{
j super(why);
Листинг11.22.win32RegKey.c
/**
©version 1.00 1997-07-01
@author Cay Horstmann
*/
#include "Win32RegKey.h"
#include" Win32RegKeyNameEnumeration.h"
#include <string.h>
#include <stdlib.h>
#include <windows.h>
JNIEXPORT jobject JNICALL Java_Win3 2RegKey_getValue(JNIEnv* env, jobject this_obj, jstring name)
{
const char* cname;
jstring path;
const char* cpath;
HKEY hkey;
DWORD type;
DWORD size;
jclass this_class;
jfieldID id_root;
jfieldID id_path;
HKEY root;
jobject ret;
char* cret;
/* Возврат класса */
this_class = (*env)->GetObjectClass (env, this_obj) ;
/* Возврат идентификаторов поля */
id_root = (*env)->GetFieldID(env, this_class, "root", "I");
id_path =this_class, "path",
"Ljava/lang/String; ");
/* Возврат полей */
root = (HKEY)(*env)->GetIntField(env, this_obj, id_root);
path = (jstring)(*env)->GetObjectField(env, this_obj, id_path) ;
cpath = (*env)->GetStringUTFChars(env, path, NULL);
/* Открытие ключа реестра */
if (RegOpenKeyEx(root, cpath, 0, KEY_READ, Shkey)
! = ERROR—SUCCESS)
{
(*env)->ThrowNew(env,
(*env)->FindClass(env, "Win32RegKeyException"),
"Open key failed");
(*env)->ReleaseStringUTFChars(env,path, cpath);
return NULL;
}
(*env)->ReleaseStringUTFChars(env,path, cpath); cname = (*env)->GetStringUTFChars(env, name, NULL);
/* Поиск типа и размера значения */
if (RegQueryValueEx(hkey, cname, NULL, Stype, NULL, &size)
!= ERROR-SUCCESS)
{
(* env)->ThrowNew(env,
(*env)->FindClass(env, "Win32RegKeyException"),
"Query value key failed");
RegCloseKey(hkey),-
(*env)->ReleaseStringUTFChars(env,name, cname);
return NULL;
}
/* Выделение памяти для хранения значения */ cret = (char*Jmalloc(size);
/* Считывание значения */
if (RegQueryValueEx(hkey, cname, NULL, &type, cret, &size)
!= ERROR_SUCCESS) {
(* env)->ThrowNew(env,
(*env)->FindClass(env, "Win32RegKeyException") , "Query value key failed");
free(cret);
RegCloseKey(hkey);
(*env)->ReleaseStringUTFChars(env,name, cname); return NULL;
}
/* В зависимости от типа значения строки определяется тип массива */
if (type == REG_SZ) {
ret = (‘env)->NewStringUTF(env, cret);
}
else if (type == REG_DW0RD) {
jclass class_Integer = (*env)->FindClass(env,
"java/lang/Integer") ;
/* Возврат идентификатора метода конструктора */ jmethodID id_Integer = (‘env)->GetMethodID(env,
class_Integer, "<init>", " (I) V" ) ;
int value = Mint*) cret;
/* Вызов конструктора */
ret = (*env)->NewObject(env, class_Integer, id_Integer, value);
}
else if (type == REG_BINARY)
{
ret = (*env)->NewByteArray(env, size);
(*env)->SetByteArrayRegion(env, (jarray)ret, 0, size, cret);
)
else
<
(’env)->ThrowNew(env,
(‘env)->FindClass(env, "Win32RegKeyException") , "Unsupported value type");
ret = NULL;
}
free(cret);
RegCloseKey(hkey);
(*env)->ReleaseStringUTFChars(env,name, cname);
return ret;
JNIEXPORT void JNICALL Java_Win32RegKey_setValue(JNIEnv* env, jobject this_obj, jstring name, jobject value)
{
const char* cname;
jstring path;
const char* cpath;
HKEY hkey;
DWORD type;
DWORD size;
jclass this_class;
jclass class_value;
jclass class_Integer;
jfieldID id_root;
jfieldID id_path;
HKEY root;
const char* cvalue;
int lvalue;
/* Возврат класса */
this_class = (*env)->GetObjectClass(env, this_obj);
/* Возврат идентификаторов поля */
id_root = (*env)->GetFieldID(env, this_class, "root", "I") ;
id_path = (*env)->GetFieldID(env, this_class, "path",
"Ljava/lang/String;");
/* Возврат полей */
root = (HKEY)(*env)->GetIntField(env, this_obj, id_root);
path = (jstring)(*env)->GetObjectField(env, this_obj, id_path);
cpath = (*env)->GetStringUTFChars(env, path, NULL);
/* Открытие ключа реестра */
if (RegOpenKeyEx(root, cpath, 0, KEY_WRITE, Shkey)
!= ERROR-SUCCESS)
{
(*env)->ThrowNew(env,
(*env)->FindClass(env, "Win32RegKeyException") ,
"Open key failed");
(*env)->ReleaseStringUTFChars(env,path, cpath);
return;
(*env)->ReleaseStringUTFChars(env,path, cpath);
cname =NULL);
class_value = (*env)->GetObjectClass(env, value);
class_Integer = (‘env)->FindClass(env, "java/lang/Integer");
/* Определение типа объекта */
if ((*env)->IsAssignableFrom(env, class_value, (*env)->FindClass(env, "java/lang/String")))
{
/* Если строка - получение указателя */
cvalue = (*env)->GetStringUTFChars(env, (jstring)value, NULL);
type = REG_SZ;
size = (*env)->GetStringLength(env, (jstring)value) + 1;
}
else if ((*env)->IsAssignableFrom(env, class_value, class_Integer))
{
/* Если целое - вызов intValue для получения значения */ jmethodID id_intValue = (*env)->GetMethodID(env, class_Integer, "intValue", "()I");
ivalue =value, id_intValue);
type = REG—DWORD;
cvalue = (char*) &ivalue;
size = 4;
}
else if ((*env)->IsAssignableFrom(env, class_value, (*env)->FindClass(env,"[B")))
(
/* Если битовый массив - вызов указателя */
type = REG_BINARY;
cvalue = (char*)(*env)->GetByteArrayElements(env, (jarray)value,NULL);
size = (*env)->GetArrayLength(env, (jarray)value);
}
else
{
/* Неизвестный обработчик */
(* env) - >ThrowNew (env,
(*env)->FindClass(env, "Win32RegKeyException") , "Unsupported value type");
RegCloseKey(hkey) ;
(*env)->ReleaseStringUTFChars(env,name, cname);
return;
}
/* Установка значения */
if (RegSetValueEx(hkey, cname, 0, type, cvalue, size)
!= ERROR-SUCCESS)
{(*env)->ThrowNew(env,
(*env) ■ ->FindClass(env, "Win32RegKeyException"), "Query value key failed");
}
RegCloseKey(hkey);
(*env)->ReleaseStringUTFChars(env,name, cname);
/* Если значение массив битов или строк - выгрузка указателя */
if (type == REG_SZ)
{
(*env)->ReleaseStringUTFChars(env, (j string)value, cvalue);
}
else if (type == REG_BINARY)
{
(*env) ->ReleaseByteArrayElement.s (env, (jarray) value, cvalue, 0);
}
1
/* Вспомогательная функция для перечисления имен */
static int startNameEnumeration(JNIEnv* env, jobject this_obj, jclass this_class)
{
jfieldID id_index;
jfieldID id_count;
jfieldID id_root;
jfieldID id_path;
jfieldID id_hkey;
jfieldID id_maxsize;
HKEY root;
jstring path;
const char* cpath;
HKEY hkey;
int maxsize = 0;
int count = 0;
/* Возврат идентификаторов поля*/
id_root = (*env)->GetFieldID(env, this_class, "root", "I"); id_path = (*env)->GetFieldID(env, this_class, "path",
"Ljava/lang/String;") ;
id_hkey = (*env)->GetFieldID(env, this_class, "hkey", " I" ) ; id_maxsize =this_class, "maxsize",
"I");
id_index = (*env)->GetFieldID(env, this_class, "index", ” I ”);
id_count = (*env)->GetFieldID(env, this_class, "count", "I ");
/* Возврат значений поля */
root = (HKEY)(*env)->GetIntField(env, this_obj, id_root);
path = (jstring)(*env)->GetObjectField(env, this_obj, id_path);
cpath = (*env)->GetStringUTFChars(env, path, NULL);
/* Открытие ключа реестра */
if (RegOpenKeyEx(root, cpath, 0, KEY_READ, Shkey)
!= ERROR_SUCCESS) {
(*env)->ThrowNew(env,
(*env)->FindClass(env, "Win32RegKeyException"), "Open key failed");
(*env)->ReleaseStringUTFChars(env,path, cpath);
return —1;
}
(*env)->ReleaseStringUTFChars(env,path, cpath);
/* Выбор числа и максимальной длины имени */
if (RegQueryInfoKey(hkey, NULL, NULL, NULL, NULL,
NULL, NULL, Scount, Smaxsize, NULL, NULL, NULL)
!= ERROR_SUCCESS) {
(*env)->ThrowNew(env,
(*env)->FindClass(env, "Win32RegKeyException"), "Query info key failed");
return —1;
}
/* Учтановка значений поля */
(*env)->SetIntField(env, this_obj , id_hkey, (DWORD)hkey);
(*env)->SetIntField(env, this_obj, id_maxsize, maxsize + 1) (*env)->SetintFieId(env, this_obj, id_index, 0);
(*env)->SetIntField(env, this_obj, id_count, count); return count;
JNIEXPORT jboolean JNICALL
Java_Win32RegKeyNameEnumeration_hasMoreElements(JNIEnv* env, jobject this_obj)
{ jclass this_class;
jfieldID id_index;
jfieldID id_count;
int index;
int count;
/* Возврат класса * /
this_class = (*env)->GetObjectClass(env, this_obj);
/* Возврат идентификаторов поля */
id_index = (*env)->GetFieldID(env, this_class, "index",
ч^ н ) ;
id_count = (*env)->GetFieldID(env, this_class, "count", "I") ;
index = (*env)->GetIntField(env, this_obj, id_index) ;
if (index == -1) /* first time */ {
count = startNameEnumeration(env, this_obj, this_class) index = 0;
}
else
count = (’env)->GetIntField(env, this_obj, id_count); return index < count;
JNIEXPORT jobject JNICALL
Java_Win32RegKeyNameEnumeration_nextElement(JNIEnv* env, jobject this_obj)
{
jclass this_class;
jfieldID id_index;
j fieldID id_hkey;
jfieldID id_count;
jfieldID id_maxsize;
НКЕУ hkey;
int index;
int count;
int maxsize;
char* cret;
j string ret;
/* Возврат класса */
this_class = (*env)->GetObjectClass(env, this_obj);
/* Возврат идентификаторов поля */
id_index = (*env)->GetFieldID(env, this_class, "index", " I" );
id_count = (*env)->GetFieldID(env, this_class, "count", "I") ;
id_hkey(*env)->GetFieldID(env, this_class, "hkey", "I") ;
id_maxsize = (*env)->GetFieldID(env, this_class, "maxsize", "I”) ;
index = (*env)->GetintFieId(env, this_obj, id_index);
if (index == -1) /* first time */ {
count = startNameEnunieration(env, this_obj, this_class); index=0,•
}
else
count = (*env)->GetIntField(env, this_obj -, id_count);
if (index >= count) /* already at end */ {
(*env)->ThrowNew(env,
(*env)->FindClass(env,
"java/util/NoSuchElementException"), "past end of enumeration");
return NULL;
ГлаваВстраиваемые методы
= (*env)->GetIntField(env,
=id_hkey);
cret =
/* Поиск следующего имени */
ifindex, cret,NULL, NULL,
NULL, NULL) !=
{
(*
(‘env)~>FindClass(env, "Win32RegKeyException"),
"Enum value failed");
free(cret);
RegCloseKey(hkey);
id_index, count);
return NULL;
)
ret =cret);
free(cret);
/* Приращение индекса */
index++;
(*env)->SetIntField(env,id_index, index);
if (index == count) /* at end */
{
RegCloseKey(hkey);
}
return ret;
Листинг
/**
1.01 2001-08-30
Cay
*/
import
public class
{
public static voidargs)
{
key = new Win32RegKey(
Win32RegKey.HKEY_LOCAL_MACHINE,
Development Kit\\1.4"
user", "Harry Hacker");
number", newprimes", new
{ 2, 3, 5, 7, 11 })
Enumeration enum =
while (enum.hasMoreElements())
{
String name = (String)enum.nextElement() ;
System.out.print(nam+"=") ;
Object value = key.getValue(name);
if (value instanceof byte[]) {
byte[] bvalue = (byte[))value;
for (int i = 0; i < bvalue.length; i++)
System.out.print((bvalue[i]& OxFF) + " ") ; }
System.out.printin();}System.out.printin();}) ') '}}else System.out.print(value) ;
System.out.printin();
}
System.out.printin();
}
) '
) '
}
}
Функции определения типа
Функция jboolean IsAssignableFrom(JNIEnv *env, jclass ell, jclass cl2) возвращает значение JNI_TRUE, если объекты класса ell можно присваивать объектам класса C12, либо значение JNI_TRUE в противном случае. Это возможно в тех случаях, когда классы ellи с12 имеют одинаковый тип, класс ell является подчиненным классом класса cl2, или класс с12 представляет интерфейс, который реализован классом ell
или одним из его суперклассов.
Параметры:
envуказатель HaJNI-интерфейс
ell,c12ссылки на классы
Функция jclass GetSuperClass(JNIEnv *env, jclass cl) возвращает суперкласс указанного класса. Если класс cl представляет класс Obj ectили интерфейс, то возвращается значение NULL.
Параметры:
envellenvellуказатель HaJNI-интерфейс ссылка на класс
env
ell
env
ell
Язык XMLЯзык XMLс^м^^< 12
Язык XML
Язык XML
В этой главе...
Введение в язык XML-
Синтаксический анализ XML-документа
Определение типа документа
Пространства имен
SAX-анализаторы
Создание XML-документов
XSL-преобразования
предисловии к книге Essential XML Дона Бокса (Don Box) и др. [Addison-Wesley 2001] полушутя говорится, что "язык расширенной гипертекстовой разметки (extensible Markup Language — XML) пришел на смену языку Java, шаблонам проектирования и объектной технологии ... для утоления жажды программистов по отношению к новым технологиям программирования". Действительно, как показано в этой главе, язык XML очень удобно использовать для описания и представления структурированной информации. Инструменты языка XML упрощают обработку и преобразование этой информации. Однако, язык XML не является универсальным средством на все случаи жизни, и для его эффективного использования потребуется также использовать другие специализированные промышленные стандарты и библиотеки. Более того, язык XML совсем не заменяет, а всего лишь дополняет язык Java. Интересно, что наиболее важные библиотеки языка XML сначала были реализованы на языке Java, а многие другие библиотеки языка XML доступны только на языке Java. В конце 90-х годов компании IBM, Apache и многие другие приступили к созданию высококачественных библиотек на языке Java для обработки XML-данных. Начиная с пакета SDK версии 1.4, компания Sun интегрировала наиболее важные библиотеки языка XML в составе платформыJava 2 Platform, Standard Edition.
Для использования возможностей языка XML в прежних версиях пакета SDK можно скопировать библиотеку Java API for XML Processing (JAXP) ПО адресу http://java.sun.com/xml.
В этой главе описываются основы языка XML, а также основные XML- инструменты пакета SDK версии 1.4. Как всегда в ней описаны случаи, в которых обоснованно следовало бы использовать язык XML, а также случаи, в которых вполне можно обойтись и без него, используя другие проверенные временем способы проектирования и программирования.
Введение в язык XML
В главах 2, "Коллекции", и 4, "Работа с базами данных— интерфейс JDBC", рассматривались примеры использования файлов свойств, которые содержат конфигурационные параметры программы. Файл свойств содержит эти параметры в виде пар параметр/значение.
fontname=Times Roman
fontsize=12
windowsize=400 200
color=0 50 100
Для считывания такого файла с помощью одного метода нужно использовать класс Properties. Однако, этот прекрасный инструмент не всегда может пригодится, потому что во многих случаях формат файла свойств не подходит для описания сложноструктурированных данных. Рассмотрим значения fontname и fontsize из приведенного выше примера. Удобнее было бы для них использовать объектно-ориентированный подход и объединить их значения в одном параметре.
font=Times Roman 12
Но впоследствии потребуется создать отдельный громоздкий код для синтаксического анализа такого выражения, т.е. для определения места, где заканчивается название шрифта и начинается его размер.
Дело в том, что файлы свойств имеют иерархию плоского файла. Иногда программисты предпринимают попытки избежать этого с помощью сложных названий параметров.
title.fontname^Helvetica
title.£ontsize=36
body. fontname=Times Roman body.fontsize=12
Еще одно ограничение формата файла свойств связано с тем, что параметры должны быть уникальными. Для сохранения целой последовательности значений придется создать такие имена.
menu.item.1-Times Roman
menu.item.l=Helvetica
menu.item.l=Goudy Old Style
Формат языка XML позволяет решить эти проблемы, потому что создан для представления как раз таких иерархических структур и обладает большей гибкостью по сравнению со структурой плоской таблицы файла свойств.
Например, файл в формате XML с конфигурационными параметрами программы может иметь следующий вид.
<configuration>
<title>
<font>
<name>Helvetica</name>
<size>36</size>
</font>
</title>
<body>
<name>Times Roman</name>
<size>12</size>
</body>
<window>
<width>400</width>
<height>400</height>
</window>
<color>
<red>0</red>
<green>50</green>
<blue>100</blue>
</color>
<menu>
<item>Times Roman</item>
<item>Helvetica</item>
<item>Goudy Old Style</item>
</menu>
</configuration>
Формат XML позволяет без каких-либо затруднений выражать любую иерархическую структуру с повторяющимися элементами.
Как видите, XML-файл имеет очень ясную и четкую структуру, аналогичную структуре HTML-файла. Дело в том, что оба эти языка являются наследниками хорошо известного стандартного обобщенного языка разметки (Standard Generilized Markup Language — SGML).
Язык SGML с 1970-х годов использовался для описания структуры сложных документов в некоторых отраслях промышленности с высокими требованиями к документации, например, в авиастроении. Но из-за присущей ему сложности язык SGML так и не получил широкого распространения. Большая часть сложностей возникла в результате двух противоречивых целей, поставленных для языка SGML. С одной стороны требовалось, чтобы документы оформлялись в строгом соответствии с правилами, заданными для документов этого типа. А с другой стороны нужно было обеспечить простоту и высокую скорость ввода данных с использованием макросов и комбинаций клавиш быстрого доступа. Язык XML представляет собой упрощенную версию языка SGML для использования в Internet. Как это часто бывает в жизни, все гениальное просто, поэтому язык XML сразу же и с энтузиазмом был воспринят теми специалистами, которые многие годы избегали использовать язык SGML.
Прекрасное описание стандарта языка XML можно найти по адресу http:// www.xml.com/axml/axml/html.
D
Несмотря на общие корни, языки XML и HTML имеют несколько важных различий.
В отличие от языка HTML в языке XML учитывается регистр символов, поэтому дескрипторы <Н1> и <hl> в языке XML считаются разными.
В языке HTML можно опускать концевые дескрипторы, например, для абзаца </р> или строки </li>, если из контекста ясно, где заканчивается абзац или строка. В языке XML это не разрешается.
В языке XML дескрипторы без концевого дескриптора должны заканчиваться символом /, например, <img src="coffeecup.png"/>. В таком случае синтаксический анализатор поймет, что не следует искать концевой дескриптор < / img>.
В языке XML значения атрибутов должны быть заключены в кавычки, а в языке HTML кавычки используются по желанию. Например, дескриптор <applet code="MyApplet .class" width=300 height=300> можно использовать в языке HTML, но нельзя в языке XML. В языке XML для значений атрибутов width и height нужно использовать кавычки, т.е. width="300"Hheight="3 00".
В языке HTML допускается использование имен атрибутов без указания их значений, например, <input type="radio" name="language" value="Java" checked>. А в языке XML все атрибуты должны иметь значения, например, checked="true" или checked=" checked".
В настоящее время для Web-документов международный консорциум по использованию Web-среды World Wide Web Consortium (W3C) рекомендует использовать стандарт XHTML, который тесно связывает стандарт HTML для обеспечения совместимости со стандартом XML. Описание стандарта XHTML можно найти по адресу http://www.w3c.org/TR/xhtmll/. Стандарт XHTML обладает обратной совместимостью со всеми современными Web-броузерами, но, к сожалению, многие современные HTML-инструменты его не поддерживают. В дальнейшем при условии более широкого распространения формата XHTML описанные в этой главе XML-инструменты можно будет использоватьдля анализа Web-документов.
Структура XML-документов
XML-документ обычно начинается со следующего заголовка.
<?xml version="l.0" ?>
Заголовок также может иметь показанный ниже вид.
<?xml version="l.0" encoding="UTF-8" ?>
Строго говоря, заголовок использовать не обязательно, но все же настоятельно рекомендуется указывать его.
Язык SGML был предназначен для обработки документов, поэтому XML-файлы принято называть документами, хотя большинство XML-файлов описывают такие наборы данных, которые трудно представить в виде обычного документа.
После заголовка обычно располагается объявление типа документа (document type declaration— DTD), например такое, как показано ниже.
<!D0CTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web~app_2_2.dtd">
Объявления типа документа являются важным механизмом обеспечения корректности документа, но не являются обязательными элементами XML-документа. Более подробно они описываются далее в этой главе.
Наконец, тело XML-документа содержит корневой элемент (root element), который может содержать другие элементы.
<?xml version="1.О" ?>
<!DOCTYPE configuration ...>
<configuration>
<title>
<font>
<name>Helvetica</name>
<size>3 6</size>
</font>
</title>
</configuration>
Каждый элемент может содержать дочерние элементы (child elements), текст либо то и другое вместе. В приведенном выше примере элемент font содержит два дочерних элемента: name и size. А элемент name содержит текст "Helvetica".
В XML-документах рекомендуется создавать такие структуры, элементы которых содержали бы либо дочерние элементы, либо текст. Иначе говоря, следует избегать создания приведенной ниже структуры.
<font>
Helvetica
<size>36</size>
</font>
Такая структура в спецификации XML называется смешанным содержанием (mixed contents). Как показано дальше, если не применять смешанное содержание, то удается создать более четкие и ясные определения типов документов.
XML-элементы могут содержать атрибуты.
<size unit="pt">36</size>
Среди разработчиков XML-документов нет единого мнения в отношении уместности использования элементов и атрибутов. Рассмотрим следующую строку с описанием шрифта.
<font name="Helvetica" size="36"/>
На первый взгляд эта строка с атрибутом выглядит гораздо проще, чем показанная ниже иерархия элементов.
<font>'
<name>Helvetica</name>
<size>3 6</size>
</font>
Однако, атрибуты обладают гораздо меньшей гибкостью. Предположим, что в определение размера шрифта нужно добавить единицу измерения. При использовании атрибутов эту единицу следует указать в значении атрибута.
<font name="Helvetica" size="36 pt"/>
Однако, теперь придется также позаботиться о создании дополнительного кода для синтаксического анализа строки "36 pt", и именно этой ситуации стремились избежать создатели языка XML. Действительно, включение атрибута в элемент size выглядит более элегантно.
<font>
<name>Helvetica</name>
<size unit="pt">36</size>
</font>
Широко распространенное правило гласит, что атрибуты следует использовать только при изменении интерпретации значения, а не для указания значения. Если возникает ситуация, в которой невозможно понять, является ли какой-то атрибут изменением интерпретации значения или нет, в таком случае следует совсем избегать атрибутов и везде применять только элементы. Во многих очень полезных DTD- объявлениях атрибуты вообще не используются.
В языке HTML есть очень простое правило использования атрибутов: если что-то не показано на Web-странице, значит это атрибут. Рассмотрим следующую гипертекстовую ссылку.
<а href = "http: //j ava.sun. coin" >Java Technology</a>
Строка Java Technology отображается на Web-странице, но URL-указатель этой гипертекстовой ссылки не является частью отображаемой страницы. Однако, это правило не всегда подходит для всех XML-файлов, потому что данные в XML- файле не всегда предназначены непосредственно для пользователей.
Элементы и текст являются основными компонентами XML-документов, но в них можно также встретить некоторые другие инструкции разметки, которые перечислены ниже.
Ссылки на символы{character references) имеют вид&$d; или&#xh;, где d обозначает десятичное Unicode-значение, а Л— шестнадцатеричное Unicode-значение. Например, ссылки é ; или ™ обозначают соответственно символы e и ™.
Ссылки на сущности(entity references) имеют вид &name;. Например, ссылки на сущности
<
>
Samp ;
"
'
имеют предопределенное значение: меньше, больше, амперсант, кавычка и апостроф. В DTD-объявлении можно также указать другие ссылки на сущности.
CDATA-разделы (CDATA sections) окружены группами символов <![CDATA[ и ] ] >. Они предназначены для включения строк с символами < > &, которые не следует интерпретировать как символы разметки.
<![CDATA[< & > are my favorite delimiters]] >
В CDATA-разделах не допускается использование строки типа ] ] >, поэтому их нужно применять очень осторожно! Часто они используются для "незаконной" вставки в XML-документ унаследованных данных.
Инструкции обработки[processing instructions') разделяются символами <? и ?>.
<?xml-stylesheet href="mystyle.css" type="text/css"?>
Эти инструкции нужны приложению, в котором используется данный XML- документ. Вообще, практически каждый XML-документ начинается со следующей инструкции обработки.
<?xml version="l.0" ?>
Комментарии (comments) разделяются символами < ! -- и -->.
<!-- This is a comment. —>
В комментариях не допускается использование строки типа --. Комментарии предназначены для пользователей, а потому никогда не следует включать в них скрытые команды. Для выполнения команд предназначены инструкции обработки.
Синтаксический анализ XML-документа
Для обработки XML-документа необходимо выполнить синтаксический анализ (parse) его содержимого. Анализатором (parser) называется программа, которая считывает файл, подтверждает корректность его формата, разбивает его на составные элементы и предоставляет программисту право доступа к ним. Ниже приведены два основных типа XML-анализаторов.
DOM-анализатор (Document Object Model) считывает XML-документ в виде древовидной структуры.
SAX-анализатор (Simple API for XML) генерирует события по мере чтения XML- документа.
DOM-анализатор проще в употреблении для большинства практических целей, поэтому в этой главе сначала рассматривается именно он.
SAX-анализатор обычно применяется для обработки очень длинных документов, когда для древовидного представления XML-документа требуется очень большой объем памяти. Кроме того, его можно использовать для извлечения только некоторых элементов XML-документа, когда контекст XML-документа нас не интересует.
Интерфейс DOM-анализатора стандартизован консорциумом W3C. Пакет org.w3c.dom содержит определения типов интерфейсов, например, Document и Element. Другие компании, например, IBM и Apache, разработали собственные варианты DOM-анализаторов, которые реализуют эти интерфейсы. В библиотеке API- функций обработки XML-документов (Java API for XML Processing — JAXP) компании
Sun предусмотрена возможность подключения данных анализаторов. Кроме того, компания Sun включила в состав пакета SDK собственный DOM-анализатор, который используется далее в этой главе.
Для считывания XML-документа потребуется объект DocumentBuilder, который можно получить с помощью класса DocumentBuilderFactory.
DocumentBuiIderFactory factory =
DocumentBuilder builder =
Теперь можно приступать к чтению документа из файла.
File f =
Document doc = builder.parse(f) ;
Иначе, для этого можно использовать URL-указатель.
URL u = . . . ;
Document doc = builder.parse(u);
Рис. 12.1.Интерфейс Node и его подчиненные интерфейсы
Кроме того, можно использовать произвольный входной поток.
InputStream in = ...;
Document doc = builder.parse(f);
При использовании входного потока в качестве источника данных анализатор не сможет найти те файлы, для которых указано относительное расположение по отношению к данному документу, например, с DTD-объявлением в том же каталоге. Для решения этой проблемы потребуется установить "определитель сущностей" ("entity resolver"). Однако, чаще всего вместо входного потока в качестве источника данных проще использовать файл или URL-указатель.
Объект Document является внутренним представлением древовидной структуры XML-документа. Он состоит из объектов, чьи классы реализуют интерфейс Node и его различные подчиненные интерфейсы. На рис. 12.1 показана иерархия наследования подчиненных интерфейсов.
Начнем с анализа содержимого документа с помощью метода getDocumentEle- merit, который возвращает корневой элемент.
Element root = doc.getDocumentElement();
Рассмотрим следующий XML-документ.
<?xml version-"1.0" ?>
<font>
</font>
В результате вызова метода getDocumentElement будет получен элемент font.
Метод getTagName возвращает название дескриптора элемента. Например, в предыдущем примере в результате вызова метода root. getTagName () будет получена строка " font".
Для извлечения дочерних элементов данного элемента (которые могут содержать подчиненные элементы, текст, комментарии или другие узлы) предназначен метод getChildNodes, который возвращает коллекцию-список NodeList. Этот список был изобретен еще до создания библиотеки коллекций Java, а потому для него используется другой протокол доступа. Метод item возвращает элемент коллекции с заданным индексом, а метод getLength — общее количество элементов коллекции NodeList. Таким образом, для перечисления всех дочерних элементов можно предложить следующий код.
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++)
{
Node child = children.itein(i) ;
)
Анализ дочерних элементов следует выполнять очень внимательно. Казалось бы, приведенный ниже XML-документ содержит два дочерних элемента для элемента font.
<font>
<name>Helvetica</name>
<size>36</size>
</font>
Но анализатор сообщит, что имеется пять дочерних элементов.
Свободное место между <font> и <name> (т.е. символ конца строки, отступ или пробел. — Прим. ред.).
Элемент name.
Свободное место между </name> и <size>.
Элемент size.
Свободное место между </size> и</font>.
На рис. 12.2 показана схема DOM-дерева.
Для обработки только подчиненных элементов нужно игнорировать все свободные места, например, с помощью приведенного ниже кода.
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child instanceof Element)
{ Element childElement = (Element)child;
} )
Рассмотрим теперь более внимательно элементы name и size.
Как показано в следующем разделе, можно поступить еще лучше, если в документе используется DTD-объявление. В таком случае анализатор точно знает, какие элементы не имеют текстовых узлов среди своих дочерних элементов, т.е. автоматически отбрасывают все свободные места.
При анализе элементов name и size нужно извлечь содержащиеся в них текстовые строки, которые находятся в дочерних узлах типа Text. Эти узлы типа Text являются единственными дочерними узлами, поэтому для их извлечения можно использовать метод getFirstChild без необходимости обхода коллекции NodeList. После- чего с помощью метода getData из узла типа Text можно извлечь его строк)'.
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child instanceof Element)
{
Element childElement = (Element)child;
Text textNode = childElement.getFirstChild();
String text = textNode.getData().trim();
if (childElement.getTagName().equals("name")) name = text;
else if (childElement.getTagName.equals("size”)) size = Integer.parselnt(text);
}
)
Ж ^Для удаления начальных и концевых свободных мест в возвращаемом значении
метода getData рекомендуется использовать метод trim. При создании XML- файла рекомендуется размещать все начальные и конечные дескрипторы в отдельных строках, как показано ниже.
<size>
36
</size>
В таком случае анализатор включит все символы конца строки и свободные места в текстовую строку узла, а метод trim удалит все свободные места, окружающие фактические данные.
Для извлечения последнего дочернего узла можно использовать метод getLastChild, а для извлечения следующего равноправного узла — метод getNextSibling. При этом для обхода набора дочерних узлов можно применить приведенный ниже код.
for (Node childNode = element.getFirstChild() ;
childNode != null;
childNode = childNode.getNextSibling()) {
}
Поиск узлов XML-дерева представляет собой очень сложную и рутинную задачу просмотра корня, его дочерних узлов, их дочерних узлов и т.д. Однако, технология XPath существенноупрощаетэтузадачу.Например,дляпоискаузланапути
/configuration/title/font/name достаточно одного вызова метода. К сожалению, технология XPath еще не входит в состав XML-библиотеки платформы Java. Подробное описание спецификации технологии XPath можно найти по адресу http://www. w3c.org/TR/xpath. Компания Apache предлагает собственный вариант реализации технологии XPath в виде библиотеки Xalan, которую можно найти no адресу http://xml.apache.org/xalan-j/overview.html.
Для перечисления атрибутов узла следует вызвать метод getAttributes, который возвращает объект-карту NamedNodeMap с объектами типа Node, содержащими описания атрибутов. Обход узлов объекта-карты NamedNodeMap можно выполнить точно так же, как и обход узлов объекта-списка NodeList. Затем для извлечения имен атрибутов и их значений нужно использовать методы getNodeName и getNodeValue.
NamedNodeMap attributes = element.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attributeNode = attributes.item(i);
String name = attribute.getNodeName();
String value = attribute.getNodeValue() ;
Иначе, если известно имя атрибута, то его значение можно извлечь непосредственно.
String unit = element.getAttribute("unit");
''Описанный здесь способ анализа DOM-дерева демонстрируется в примере программы, код которой приводится в листинге 12.1. Для чтения XML-файла следует выбрать команду меню File^Open и указать в диалоговом окне нужный файл. После этого объект DocumentBuilder выполнит анализ XML-файла и создаст объект Document. Затем в окне программы будет показан объект Document с помощью древовидной структуры компонента JTree, как показано на рис. 12.3.
'
'
1 Rie
33Element<Server>Element<Server>QTexr\nID Comment:Debuglow-leveleventsinXmlMapper stai14■ 2)Text:\n \nD Comment: \n\n Logging:\n\nLogging in TomcatDText:\n\n3Comment:ifyoudon'twantmessagesonscreen,addНОГ5 Text:.I□Element:<Loggername='lc log" verbositvLevel-'WFORM D Text\n\nQBemem: <Logger name«"serVetJog" pah«'togs/seWe<^Text\n\nD Element. <Loggername="J/SPER.LOG" path^logs/jaspe :D Text: wg: D Comment: You can add a "home" attribute to represent t D Text: \n9 £3Element: <ContextManagerdebug»"O"workDir="work s QText\n\nQTexr\nID Comment:Debuglow-leveleventsinXmlMapper stai14■ 2)Text:\n \nD Comment: \n\n Logging:\n\nLogging in TomcatDText:\n\n3Comment:ifyoudon'twantmessagesonscreen,addНОГ5 Text:.I□Element:<Loggername='lc log" verbositvLevel-'WFORM D Text\n\nQBemem: <Logger name«"serVetJog" pah«'togs/seWe<^Text\n\nD Element. <Loggername="J/SPER.LOG" path^logs/jaspe :D Text: wg: D Comment: You can add a "home" attribute to represent t D Text: \n9 £3Element: <ContextManagerdebug»"O"workDir="work s QText\n\nЧ»"Ч»"
3
3
Element<Server>
Element<Server>
QTexr\nI
D Comment:Debuglow-leveleventsinXmlMapper stai14
■ 2)Text:\n \n
D Comment: \n\n Logging:\n\nLogging in Tomcat
DText:\n\n
3Comment:ifyoudon'twantmessagesonscreen,addНОГ
5 Text:.I
□Element:<Loggername='lc log" verbositvLevel-'WFORM D Text\n\n
QBemem: <Logger name«"serVetJog" pah«'togs/seWe<
^Text\n\n
D Element. <Loggername="J/SPER.LOG" path^logs/jaspe :
D Text: wg
: D Comment: You can add a "home" attribute to represent t D Text: \n
9 £3Element: <ContextManagerdebug»"O"workDir="work s QText\n\n
QTexr\nI
D Comment:Debuglow-leveleventsinXmlMapper stai14
■ 2)Text:\n \n
D Comment: \n\n Logging:\n\nLogging in Tomcat
DText:\n\n
3Comment:ifyoudon'twantmessagesonscreen,addНОГ
5 Text:.I
□Element:<Loggername='lc log" verbositvLevel-'WFORM D Text\n\n
QBemem: <Logger name«"serVetJog" pah«'togs/seWe<
^Text\n\n
D Element. <Loggername="J/SPER.LOG" path^logs/jaspe :
D Text: wg
: D Comment: You can add a "home" attribute to represent t D Text: \n
9 £3Element: <ContextManagerdebug»"O"workDir="work s QText\n\n
Ч»"
Ч»"
Рис. 12.3. Результат анализа XML-документа
В данной структуре дерева четко видны дочерние узлы, которые окружены свободными местами и комментариями. Для большей наглядности все символы новой строки, перевода каретки и отступа представлены в виде \п, \r и \t соответственно. (Иначе они были бы представлены в виде пустых квадратиков, которые используются в библиотеке Swing для обозначения непечатаемых символов.)
Здесь используется уже рассмотренный выше (см. раздел о структурах-деревьях в главе 6) способ отображения структуры дерева. Класс DOMTreeModel реализует интерфейс TreeModel. Метод getRoot возвращает корневой элемент документа, а метод getChild получает список дочерних узлов и возвращает узел с заданным индексом. Однако, эта модель дерева возвращает узлы-объекты типа Node, методы toString которых необязательно вернут данные с описанием. Поэтому в данной программе задается пользовательский рисовальщик ячеек дерева, который расширяет предлагаемый по умолчанию рисовальщик ячеек дерева и задает текст для ярлыка в более описательной форме. Этот рисовальщик ячеек дерева отображает следующие компоненты дерева.
Дляэлементов; название дескриптора и все атрибуты.
Для символьных данных: интерфейс (текст, комментарий, раздел CDATA), затем данные с символами новой строки, перевода каретки и отступа представлены в виде \п, \rи \t соответственно.
Для всех других типов узлов: имя класса, за которым следует результат выполнения метода toString.
Для анализа XML-документов, которые содержат другие типы узлов (например, инструкции обработки), нужно соответствующим образом усовершенствовать эту программу. Данный пример предназначен не для создания простого и удобного инструмента просмотра XML-файлов (можно найти много других более удачных программ такого рода), а для демонстрации способов анализа и перечисления дочерних узлов и атрибутов DOM-модели.
Прекрасную программу просмотра XML-файлов компании IBM AlphaWorks можно бесплатно скопировать по адресуhttp: //www.alphaworks.ibm.com/tech/xmlviewer.
Листинг 12.1. DOMTreeTest.java
©version 1.0 2001-09-01
@author Cay Horstmann
*/
import java.awt.*;
import java.awt.event .*;
import java.io.*;
import javax.swing. *;
import javax.swing.event .*;
import javax.swing.tree. *;
import javax.xml.parsers. * ;
import org.w3c.dom.*;
import org.xml.sax.*;
/**
Пример отображения XML-документа
в виде древовидной структуры
*/
public class DOMTreeTest {
public static void main(String[] args) {
JFrame frame = new DOMTreeFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) ; frame.show();
}
}
/**
Фрейм со структурой-деревом, в которой отображается содержимое XML-документа
*/
class DOMTreeFrame extends JFrame {
public DOMTreeFrame()
{
setTitlet"DOMTreeTest") ;
setsize(WIDTH, HEIGHT);
JMenu fileMenu = new JMenu("File");
JMenuItem openItem = new JMenuItem("Open"); openitem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
openFile();
}
});
fileMenu.add(openltem);
JMenuItem exititem = new JMenuItem("Exit");
exititem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
}) ;
fileMenu.add(exititem);
JMenuBar menuBar = new JMenuBar();
menuBar.add(fileMenu);
setJMenuBar(menuBar) ;
/**
Открыть файл и загрузить документ
*/
public void openFile()
{
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
chooser.setFileFilter(new
javax.swing.filechooser.FileFilter()
{
public boolean accept(File f) {
return f.isDirectory()
|| f.getName().toLowerCase().endsWith(".xml"); }
public String getDescription()
{
return "XML files";
}
intr=chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
Filef=chooser.getSelectedFile();
try
if (builder == null)
DocumentBuilderFactory factory =
builder = factory.newDocumentBuilder();
Document doc = buiIder.parse(f);
JTree tree = new JTree(new DOMTreeModel(doc)); tree.setCellRenderer(new DOMTreeCellRenderer() ) ;
setContentPane(new JScrollPane(tree)); validate();
}
catch (TOException exception)
JOptionPane.showMessageDialog(this, exception);
}
catch (ParserConfigurationException exception) {
JOptionPane.showMessageDialog(this, exception);
)
catch (SAXException exception) {
JOptionPane.showMessageDialog(this, exception); )
}
private DocumentBuiIder builder;
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
Модель дерева, которая описывает древовидную структуру Java-объекта. Дочерние узлы являются объектами, которые хранятся в переменных экземпляра
*/
class DOMTreeModel implements TreeModel {
/**
Создание пустого дерева
*/
public DOMTreeModel(Document aDoc) {
dec = aDoc;
public Object getRoot() {
return doc.getDocumentElement();
}
public int getChildCount(Object parent) {
Node node = (Node)parent;
NodeList list = node.getChildNodes();
return list.getLength();
}
public Object getChild(Object parent, int index) {
Node node = (Node)parent;
NodeList list = node.getChildNodes();
return list.item(index);
}
public int getlndexOfChiId(Object parent, Object child) {
Node node = (Node)parent;
NodeList list = node.getChildNodes();
for (int i = 0; i < list.getLength(); i++) if (getChild(node, i) == child)
return i;
return—1;
}
public boolean isLeaf(Object node) {
return getChildCount(node) == 0;
}
public void valueForPathChanged(TreePath path,
Object newValue) {}
public void addTreeModelLis tener(TreeModelLis tener 1) {}
public void removeTreeModelListener(TreeModelListener 1) {}
private Document doc;
/**
Этот класс перерисовывает имя класса в обычном
или наклонном.начертании (для абстрактных классов)
*/
class DOMTreeCellRenderer extends DefaultTreeCellRenderer {
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus)
{
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
Node node = (Node)value;
if (node instanceof Element)
setText(elementstring((Element)node));
else if (node instanceof CharacterData)
setText(characterstring((CharacterData)node));
else
setText(node.getClass() + ": " + node. toString () ) ,- return this;
public static String elementstring(Element e)
{
StringBuffer buffer = new StringBuffer();
buffer.append("Element: <");
buffer.append(e.getTagName() ) ;
NamedNodeMap attributes = e.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
buffer.append (' ');
Node attributeNode = attributes.item(i);
buffer.append(attributeNode.getNodeName());
buffer.append('=');
buffer.append('"');
buffer.append(attributeNode.getNodeValue()) ;
buffer.append)' " ' ) ;
buffer.append(’>');
return buffer.toString();
public static String characterstring(CharacterData node) {
StringBuffer buffer = new StringBuffer(node.getData()) ; for (int i = 0; i < buffer.length(); i++) {
if (buffer.charAt(i) == {
buffer.replace(i, i + 1, "\\r");
} else if (buffer.charAt(i) == {
buffer.replace(i, i + 1, "\\n");
i++;
}
else if (buffer.charAt(i) ==
{ buffer.replace(i, i + 1, "\\t");
}
}
if (node instanceof Text)
buffer.insert(0,"Text: ") ;
else if (node instanceof Comment)
buffer.insert(0,"Comment: ") ;
else if (node instanceof CDATASection)
buffer.insert(0,"CDATASection: ") ;
}}return buffer.toString() ;
}
}
j avax. xml. parsers. DocumentBuilderFactory
Метод static DocumentBuilderFactory newlnstance() возвращает экземпляр класса DocumentBuilderFactory.
Метод DocumentBuilder newDocumentBuilder () возвращает экземпляр класса DocumentBuilder.
javax.xml.parsers • DocumentBuilder
Метод Document parse (File f) анализирует XML-документ из указанного файла и возвращает проанализированный документ.
Метод Document parse (String url) анализирует XML-документ с указанным URL-указателем и возвращает проанализированный документ.
Метод Document parse (InputStream in) анализирует XML-документ из указанного входного потока и возвращает проанализированный документ.
org w3 c.dam.Documen t
Метод Element getDocumentElement() возвращает корневой элемент документа.
org.w3c.dom.Element
Метод String getTagName () возвращает название элемента.
Метод String getAttribute (String name) возвращает значение атрибута для атрибута с заданным именем либо пустую строку, если такого атрибута нет.
org.w3c.dom.Node
Метод NodeList getChildNodes () возвращает узел, который содержит все дочерние узлы данного узла.
Метод Node getFirstChild () возвращает первый узел данного узла либо значение null, если данный узел не имеет дочерних узлов.
••Метод Node getLastChild() возвращает последний узел данного узла либо значение null, если данный узел не имеет дочерних узлов.
•
•
Метод Node getNextSibling() возвращает следующий равноправный узел данного узла либо значение null, если данный узел не имеет равноправных узлов.
Метод Node getPreviousSibling () возвращает предыдущий равноправный узел данного узла либо значение null, если данный узел HC имеет равноправных узлов.
Метод Node getParentNode () возвращает родительский узел данного узла либо значение null, если данный узел является узлом всего документа.
Метод NamedNodeMap getAttributes () возвращает объект-карту узлов, которая содержит узлы типаAttr с описаниями всех атрибутов данного узла.
Метод String getNodeName () возвращает имя данного узла. Если данный узел имеет тип Attr, то возвращается имя атрибута.
Метод String getNodeValue () возвращает значение данного узла. Если данный узел имеет тип Attr, то возвращается значение атрибута.
org. w3c. dom. CharacterData
Метод String getData() возвращает строку, которая хранится в данном узле.
org.w3c.dom.NodeList
Метод int getLength () возвращает количество узлов в данном списке узлов.
Метод Node item (int index) возвращает узел с заданным индексом, который может иметь значение от 0 до getLength О -1.
Определение типа документа
В предыдущем разделе описан способ обхода структуры-дерева DOM-документа. Однако, если просто следовать этому подходу, то придется затратить много усилий на создание довольно трудоемкого кода и организацию проверки ошибок. При этом придется не только организовать поиск и удаление свободных мест между элементами, но также проверить, содержит ли документ нужные узлы. Рассмотрим следующий элемент.
<font>
<name>Helvetica</name>
<size>36</size>
</font>
Сначала будет получен первый дочерний узел без свободного места. Затем потребуется проверить, имеет ли этот узел имя name. После чего следует проверить, имеет ли он дочерний узел типа Text. Затем нужно переместиться к следующему дочернему узлу (который не является свободным местом) и выполнить такую же проверку. Но что делать, если автор XML-документа изменит порядок расположения дочерних узлов или добавит другой дочерний элемент? С одной стороны для проверки всех возможных ошибок и исключительных ситуаций придется создать очень громоздкий код, а с другой — исключить такую проверку было бы очень безрассудно.
К счастью, одним из наиболее важных преимуществ XML-анализатора является его способность автоматически проверять корректность структуры документа. В таком случае анализ XML-документа значительно упрощается. Например, если фрагмент с узлом fontуспешно прошел проверку, то в результате будут получены два дочерних узла, выполнено приведение к типу Text, а затем извлечены текстовые данные без какой-либо дальнейшей проверки.
Для указания структуры документа нужно создать определение типа документа (document type definition— DTD). DTD-определение содержит правила, которые регламентируют структуру документа за счет указания допустимых дочерних элементов и атрибутов каждого элемента. Например, DTD-определение может содержать следующее правило.
<!ELEMENT font (name,size)>
Это правило гласит, что элемент fontдолжен иметь два дочерних узла: name и size.
DTD-определения не являются обязательными, но в большинстве профессиональных приложений XML-документы содержат DTD-определения.
DTD-определение можно включить в начале XML-документа показанным ниже способом.
<?xml version="1.0" ?>
<!D0CTYPE configuration [
<!ELEMENT configuration ...>
К=КИе-TO другие правила
]>
<configuration>
</configuration
Как видите, эти правила заключены в квадратные скобки внутри объявления DOCTYPE. Этот тип документа должен соответствовать имени корневого элемента, т.е. configurationв данном примере.
Внутри XML-документа DTD-определение вставляется крайне редко, потому что может быть очень длинным. Поэтому имеет смысл хранить DTD-определения в отдельном файле. Для этого можно использовать показанное ниже объявление SYSTEM с именем внешнего файла с DTD-определением.
<!DOCTYPE configuration SYSTEM "config.dtd">
Кроме того, объявление SYSTEM может содержать URL-указатель внешнего файла с DTD-определением.
<I DOCTYPE configuration SYSTEM "http://myserver.com/config.dtd">
При использовании относительного URL-указателя для внешнего файла с DTD- определением (например "config.dtd") анализатору нужно передать объект типа File илиURL, а не Inputstream. Для синтаксического анализа входного потока необходимо предоставить определитель сущности, который рассматривается в следующем замечании.
Наконец, можно использовать механизм "известных" DTD-определений, который унаследован из языка SGML.
<I DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2,dtd">
Если XML-процессор знает способ обнаружения DTD-определения с помощью идентификатора PUBLIC, в таком случае этот URL-указатель использовать не придется.
При использовании DOM-анализатора с поддержкой идентификатора PUBLIC следует вызвать метод setEntityResolver класса DocumentBuiIderдля инсталляции объекта класса, который реализует интерфейс EntityResolver с единственным методом resolveEntity. Вот как выглядит типичная реализация этого метода.
class MyEntityResolver implements EntityResolver {
public InputSource resolveEntity(String publicID,
{{String systemID)
{
{
if (publicID.equals(известный идентификатор)) return new Inputsource(DTD-данные)
else
return null; // Поведение no умолчанию.
}
Источник входных данных можно создать на основе класса Inputstream, Reader или String.
Рассмотрим теперь различные правила DTD-определения.
Правило ELEMENT указывает дочерние узлы данного элемента в виде регулярного выражения, которое состоит из указанных в табл. 12.1 компонентов.
Рассмотрим несколько простых, но вполне типичных примеров, Следующее правило гласит, что элемент menu содержит 0 или более элементов item.
<!ELEMENT menu (item)*>
Следующий набор правил указывает, что шрифт описывается текстовыми элементами с именем и размером.
<!ELEMENT font (name, size)>
<!ELEMENT name (# PCDATA)>
<[ELEMENT size (#PCDATA)>
<xsd:complexType<xsd:sequence><xsd:element<xsd:element</xsd: sequence></xsd:complexType><xsd:element name:"font"<xsd:complexType<xsd:sequence><xsd:element<xsd:element</xsd: sequence></xsd:complexType><xsd:element name:"font"name:"name" name:"si2e"name:"name" name:"si2e"type="Font”/>type="Font”/>name="Font">type=”xsd:string"/> type=”xsd:integer"/>name="Font">type=”xsd:string"/> type=”xsd:integer"/>
<xsd:complexType
<xsd:sequence>
<xsd:element
<xsd:element
</xsd: sequence>
</xsd:complexType>
<xsd:element name:"font"
<xsd:complexType
<xsd:sequence>
<xsd:element
<xsd:element
</xsd: sequence>
</xsd:complexType>
<xsd:element name:"font"
name:"name" name:"si2e"
name:"name" name:"si2e"
type="Font”/>
type="Font”/>
name="Font">
type=”xsd:string"/> type=”xsd:integer"/>
name="Font">
type=”xsd:string"/> type=”xsd:integer"/>
Таблица 12.1.Правила DTD-определения
Описание
E
0 или более элементов Е
Е+
1 или более элементов Е
Е?
О или 1 элемент Е
Ei|E2| . . . I En
один из элементов Ei, E2, ..., Ej^
Е1, Е, ,E„
элемент ^1» за которым следуют e2, ..., en
#PCDATA
текст
(#PCDATA|Ex | E2 | . . . | EM) *0 или более текстовых элементов и элементы Ei, E2,
..., En, расположенные в произвольном порядке (смешанное содержание)
ANY
любые дочерние узлы
EMPTY
нет дочерних узлов
Аббревиатура PCDATA означает символьные данные, которые называются проанализированными, потому что анализатор интерпретирует текстовую строку, обнаруживая символ <, который обозначает начало нового дескриптора, или символ &, который обозначает начало сущности.
Более подробное описание спецификации XSD можно найти по адресу http://www.w3c.org/XML/Schema. В настоящее время спецификация XSD не поддерживается в Java SDK.
Спецификация элемента может содержать регулярные вложенные и сложные выражения. Например, вот как выглядит правило, которое описывает структуру этой главы.
<!ELEMENT chapter (intro, (heading, (para|image|table(note)+)+) >
Каждая глава chapter начинается с введения intro, за которым располагается один или несколько разделов, состоящих из заголовка heading и одного или нескольких абзацев para, рисунков image, таблиц table или замечаний note.
Однако, довольно часто элемент содержит текст. Тогда возможны только два разных случая. Во-первых, элемент может содержать только текст.
<!ELEMENT name (# PCDATA)>
Во-вторых, элемент может содержать любую комбинацию текста и дескрипторов в произвольном порядке.
<!ELEMENT para (#PCDATA|em|strong|code)*>
При использовании параметра # PCDATA не допускается использование любых других типов правил, например таких, как показанное ниже правило для рисунка с подписью.
<!ELEMENT captionedlmage (image, #PCDATA)>
Такие правила следует переписать заново, либо вводя другой отдельный элемент caption с подписью, либо используя любую комбинацию дескрипторов рисунка image и текста с его подписью.
Это ограничение упрощает работу XML-анализатора при синтаксическом анализе смешанного содержимого (текст и дескрипторы). Но при использовании смешанного содержимого утрачивается контроль над ним, поэтому рекомендуется проектировать такие DTD-определения, которые содержали бы либо только элементы, либо только текст.
На самом деле это не совсем так, потому что в правилах DTD-определения можно указать произвольные регулярные выражения. XML-анализатор может отвергнуть
D сложные наборы правил, которые приводят к "недетерминистическому" анализу. Например, регулярное выражение ((х,у), (x,z)) является недетерминистическим, потому что анализатор не сможет понять, к какому из двух элементов относится х. Это выражение следует переписать в детерминистическом виде, например, (х, (y|z)). Однако, некоторые определения невозможно изменить, например, ((х,у)*х?). Анализатор Sun не выдает никаких предупреждений при обнаружении двусмысленных DTD-определений. Он просто применяет первый возможный вариант, что может привести к неправильной интерпретации корректных входных данных. Конечно, анализатор достаточно хорошо справляется с правильно заданными DTD-определениями, потому что стандарт XML предполагает использование только недвусмысленных DTD-определений.
На практике двусмысленные DTD-определения встречаются крайне редко, потому что большинство DTD-определений настолько просты, что не вызывают проблем с разрешением двусмысленных ситуаций.
Для описания допустимых атрибутов элементов используется приведенный ниже синтаксис.
<!ATTLIST элемент атрибут тип по в едение_по_умол чанию>
В табл. 12.2 показаны допустимые типы атрибутов, а в табл. 12.3 — синтаксис параметра поведение_по_умолчанию.
Вот как выглядят две типичные спецификации атрибутов.
<!ATTLIST font style (plain|bold|italic|bold-italic) plain>
<IATTLIST size unit CDATA #IMPLIED>
Таблица 12.2. Типы атрибутов
Тип
Описание
CDATA
Произвольная символьная строка
(Ai {Аз | ... |An)
Один из строковых атрибутов At | Аз | ... I An
NMTOKEN, NMTOKENS
Один или несколько маркеров имени
ID
Уникальный идентификатор
IDREF, IDREFS
Одна или несколько ссылок науникальный идентификатор
ENTITY, ENTITIES
Одна или несколько непроанализированных сущностей
Таблица 12.3.Поведение по умолчанию|jgi|||j^^
Параметр
Описание
#REQUIRED
Атрибут является обязательным
#IMPLIED
Атрибут не является обязательным
А
Атрибут не является обязательным; анализатор возвращает значение А, если атрибут не указан
#FIXED А
Атрибут не должен быть указан, либо должен быть равен A; в любом случае анализатор возвращает значение А
В первой спецификации для элемента fontуказан атрибут стиля style, который может иметь 4 допустимых значения, а по умолчанию используется значение plain. Во второй спецификации для элемента sizeуказан атрибут стиля unit, который может содержать любую последовательность символов.
Для описания данных рекомендуется использовать элементы, а не атрибуты. В соответствии с этой рекомендацией стиль шрифта должен содержаться в отдельном элементе, например, в виде<fontxstyle>plain</style>...</font>. Однако, атрибуты обладают несомненным преимуществом при использовании перечислений, потому что анализатор может проверять допустимость тех или иных переменных. Например, если стиль шрифта является атрибутом, то анализатор проверяет наличие найденного стиля среди 4 указанных допустимых значений стиля и использует значение по умолчанию, если такого допустимого значения найти неудалось,
Обработка атрибута типа CDATA слегка отличается от обработки атрибута # PCDATA и практически никак не связана с разделами <! [CDATA [...]]>. Значение атрибута сначала нормализуется, т.е. анализатор обрабатывает ссылки на символы и сущности (такие, как напримерé или <) и заменяет свободные места пробелами.
Тип атрибута-маркера имени NMTOKEN аналогичен типу CDATA, но в нем не допускается использование большинства не цифробуквенных символов и внутренних свободных мест, а также удаляются все начальные и концевые свободные места.
Тип атрибута ID означает уникальный маркер имени для данного документа, причем анализатор проверяет его уникальность. В следующем разделе будет приведен пример использования этого типа атрибута.
Тип атрибута IDREF означает ссылку на идентификатор, уже существующий в данном документе, наличие которого также проверяется анализатором. Атрибут типа IDREFS содержит список ссылок на идентификаторы, разделенных свободными местами.
Атрибут типа ENTITY относится к "непроанализированной внешней сущности", которая унаследована от языка SGML и редко используется на практике. В спецификации языка XML по адресу http://www.xml.com/axml/axml.html приводится пример использования такого типа атрибута.
DTD-определение может также содержать определения сущностей или аббревиатур, которые заменяются после синтаксического анализа. Хороший пример использования сущностей можно найти в описании пользовательского интерфейса Web- броузера Mozilla/Netscape 6. Данные описания отформатированы в соответствии с требованиями языка XML и содержат следующие определения сущностей.
<!ENTITY back, label "Back">
Далее в тексте документа могут встретиться ссылки на эти сущности.
<menuitem label="&back.label;"/>
В таком случае анализатор заменяет ссылку на сущность с помощью строки замены. Для локализации приложения потребуется только заменить определение сущности. Сущности также могут использоваться в других более сложных и менее распространенных случаях. Более подробные сведения по этой теме можно найти в спецификации языка XML.
Итак, мы познакомились с основными сведениями, которые необходимы для создания DTD-определений. Рассмотрим теперь способы конфигурирования анализатора для использования всех преимуществ DTD-определений. Сначала нужно сообщить фабрике конструкторов документов о необходимости проверки.
factory.setvalidating(true);
Все конструкторы, созданные этой фабрикой, проверят соответствие входных данных и DTD-определений. Наиболее полезным результатом такой проверки является игнорирование всех свободных мест в содержимом элемента. Рассмотрим следующий фрагмент XML-документа.
<font>
<name>Helvetica</name>
<size>36</size>
</font>
Анализатор без проверки сообщит обо всех свободных местах между элементами font, name и size , потому что ему ничего не известно о том, являются ли дочерними узлами элемента fontэлементы с одним из следующих правил.
(name,size)
(#PCDATA,name,size) *
ANY
Если в DTD-определении указано правило (name, size), то анализатору наверняка известно, что свободное место между этими элементами не является текстом. Конструктор документа перестанет сообщать о наличии свободных мест в текстовых узлах, если использовать следующий метод.
factory.setlgnoringElementContentWhitespace(true);
Это значит, что теперь можно быть вполне уверенным в том, чторел fontимеет два дочерних узла, и теперь не придется использовать приведенный ниже громоздкий код.
for (int i = 0; i < children.getLength() ; i++) {
Node child = children.item(i);
if (child instanceof Element)
{
Element childElement =
if (childElement.getTagName().equals("name"))
else if (childElement.getTagName.equals("size"))
}
}
Вместо него для доступа к первому и второму дочерним узлам теперь можно использовать следующие строки.
Element nameElement = (Element)children.item(O);
Element sizeElement = (Element)children.item(l);
Именно в этих случаях DTD-определения особенно полезны. Как видите, при использовании DTD-определений уже не нужно перегружать программу сложным кодом с правилами проверки, потому что анализатор проделал эту работу во время получения документа.
Многие программисты, начиная работу с XML-документами, игнорируют DTD- определения и пытаются самостоятельно анализировать древовидную DOM- структуру. Чтобы убедитьсявнеэффективноститакого подхода, достаточнотолько
взглянуть на приведенные выше фрагменты кода.
Учтите, что при организации доступа к узлам на основе их расположения в списке узлов существует опасность ошибки при изменении DTD-определения, потому что в таком случае позиции узлов в списке могут измениться. Используя технологию XPath, можно организовать более надежный способ доступа к элементам дерева по имени пути, например, /configuration/title/font/name. Однако, технология XPath в настоящее время не входит в состав пакета Java SDK.
Если анализатор сообщает о какой-то ошибке, то приложение должно каким-то образом отреагировать на это, например, зарегистрировать ошибку, сообщить о ней пользователю или генерировать исключительную ситуацию для прекращения синтаксического анализа. Поэтому при использовании функций проверки содержимого XML-документа следует также установить обработчик исключительных ситуаций, для чего нужно создать объект, реализующий интерфейс ErrorHandler, который содержит следующие три метода.
void warning(SAXParseException exception) void error(SAXParseException exception) void fatalError(SAXParseException exception)
Для установки обработчика исключительных ситуаций используется метод setErrorHandler класса DocumentBuilder.
builder.setErrorHandler(handler) ;
••javax.xml.parsers.DocumentBuilder
•
•
Методvoid setEntityResolver(EntityResolver resolver) задает определитель сущности для обнаружения сущностей, которые упоминаются в анализируемых XML-документах.
Метод void setErrorHandler(ErrorHandler handler) задает обработчик исключительных ситуаций для выдачи предупреждений и сообщений об ошибках, которые происходят во время анализа XML-документов.
org. xml. sax. EntityResolver
Метод public InputSource resolveEntity(String publicID, String systemID) возвращает источник входных данных, который содержит данные, упомянутые заданными идентификаторами, или значение null для обозначения того, что данный определитель имени не знает, как разрешить данное имя. Параметр publicID может иметь значение null, если нет никаких открытых идентификаторов.
org.xml.sax.Inputsource
Метод InputSource(Inputstream in) создает источник входных данных на основании указанного потока байтов.
Метод InputSource (Reader in) создает источник входных данных на основании указанного потока символов.
Метод InputSource (String systemID) создает источник входных данных на основании указанного системного идентификатора (обычно, относительного или абсолютного URL-указателя).
org.xml . sax SrrorHandl er
Метод void fatalError(SAXParseException exception) можно пе
реопределить для создания собственного обработчика неустранимых ошибок (fatal error).
Метод void error (SAXParseException exception) можно переоп
ределить для создания собственного обработчика устранимых ошибок (nonfatal error).
Метод void warning (SAXParseException exception) можно переопределить для создания собственного обработчика предупреждений.
org.xml.sax.SAXParseException
Метод int getLineNumber () возвращает номер последней строки входного потока, в которой произошла исключительная ситуация.
Метод int getColumnNumber () возвращает номер последнего столбца входного потока, в котором произошла исключительная ситуация.
javax.xml.parsers.DocumentBuilderFactory
Метод boolean isValidating() возвращает значение свойства "проверка"
фабрики. Если возвращается значение true, то это значит, что созданные этой фабрикой анализаторы будут выполнять проверку входных данных.
Метод void setvalidating (boolean value) задает значение свойст
ва "проверка" фабрики. Если параметр value имеет значение true, то это значит, что созданные этой фабрикой анализаторы будут выполнять проверку входных данных.
Метод boolean isIgnoringElementContentWhitespace() возвращает значение свойства "игнорировать свободные места между элементами" фабрики. Если возвращается значение true, то это значит, что созданные этой фабрикой анализаторы будут игнорировать свободные места между узлами, которые не имеют смешанного содержания (т.е. смесь элементов и текста).
Метод void setlgnoringElementContentWhitespace (boolean value) задает значение свойства "игнорировать свободные места между элементами" фабрики. Если параметр value имеет значение true, то это значит, что созданные этой фабрикой анализаторы будут игнорировать свободные места между узлами, которые не имеют смешанного содержания (т.е. смесь элементов и текста).
Практический пример
В данном разделе рассматривается практический пример использования XML- документа в реальной ситуации. В главе 9 тома 1 уже говорилось, что класс GridBagLayout лучше всего подходит для организации макета Swing-компонентов. Однако, многие опасаются его использовать не столько из-за сложности, сколько из-за большого объема рутинной работы. При работе с этим макетом вместо создания большого количества повторяющегося кода гораздо удобнее было бы разместить все инструкции макета в текстовом файле. В этом разделе описываются способы использования языка XML для описания макета типа GxidBagLayout и анализа файлов с таким описанием макета.
Макет типа GridBagLayout состоит из строк и столбцов, которые аналогичны строкам и столбцам HTML-таблицы, а потому могут быть представлены аналогично в виде последовательности строк с ячейками.
<gridbag>
<row>
<cell>...</cell>
<cell>...</cell>
</row>
<row>
<cell>...</cell>
<cell>..,</cell>
</row>
</gridbag>
В файле gridbag.dtdукажем следующие правила.
<I ELEMENT gridbag (row)*>
<!ELEMENT row (cell)*>
Некоторые ячейки могут охватывать несколько строк и столбцов. В макете типа GridBagLayout это достигается с помощью ограничений gridwidth и gridheight, значения которых превышают 1. Соответствующие им атрибуты будут иметь те же имена.
<cell gridwidth="2" gridheight=”2”>
Аналогично введем атрибуты для других ограничений fill, anchor, gridx, gridy, weightx, weighty, ipadx и ipady. (Ограничение insets не используется в данном примере, потому что его значение имеет более сложный тип.)
<cell f i 11 ="HORIZONTAL" anchor ="NORTH">
Для большинства атрибутов используются те же значения по умолчанию, которые предлагаются в конструкторе GridBagConstraints.
<!ATTLIST cell gridwidth CDATA "1">
<!ATTLIST cell gridheight CDATA "1">
<!ATTLIST cell weightx CDATA "100">
<!ATTLIST cell weighty CDATA "100">
<!ATTLIST cell fill (NONE I BOTH I HORIZONTAL|VERTICAL) ”NONE">
<!ATTLIST cell anchor (CENTER|NORTH|NORTHEAST|EAST|SOUTHEAST|SOUTHS |SOUTHWEST|WEST|NORTHWEST) "CENTER">
<!ATTLIST cell ipadx CDATA "0">
<!ATTLIST cell ipady CDATA "0">
Однако, вместо значения 0 для ограничений weightx и weighty полезнее было бы использовать значение 100.
<!ATTLIST cell weightx CDATA "100">
<!ATTLIST cell weighty CDATA "100">
Значения ограничений gridx и gridy задаются и обрабатываются иначе, потому что при указании их вручную часто могут возникать случайные ошибки.
<!ATTLIST cell gridx CDATA #IMPLIED>
<!ATTLIST cell gridy CDATA #IMPLIED>
Как видите, их значения указывать необязательно, потому что в случае их отсутствия программа установит для них значения в соответствии со следующей логикой: в столбце 0 для ограничения gridx по умолчанию будет использоваться значение 0, а во всех других столбцах для ограничения gridx будет использоваться значение gridx плюс значение gridx из предыдущего столбца. Причем, по умолчанию для одной и той же строки значение gridy остается равным номеру строки. Поэтому в большинстве случае охвата компонентом нескольких строк не придется задавать ограничения gridx и gridy.
Опытные пользователи макета типа GridBagLayout могут заметить, что для автоматического определения значений gridx и gridy можно было бы использовать параметры RELATIVE И REMAINDER. Авторы попытались это сделать, но это не помогло им получить макет диалогового окна для выбора шрифта, который показан в томе I. После внимательного изучения исходного кода макета GridBagLayout можно заметить, что данный алгоритм просто не способен выполнить такую тонкую работу по определению соответствующих значений gridx и gridy.
Эта программа анализирует атрибуты и задает ограничения для макета типа GridBagLayout. Например, для чтения ширины решетки в программе используется следующее выражение.
constraints.gridwidth
=
Программе не нужно беспокоиться о пропущенном атрибуте, потому что анализатор автоматически предоставит значение по умолчанию, если оно не будет обнаружено в документе.
Для проверки наличия в документе значений атрибутов gridx и gridy следует вызвать метод getAttribute и проверить, возвращает ли он пустую строку.
String value = e.getAttribute("gridy");
if (value.length() == 0) // Используется no умолчанию. constraints.gridy = r;
else
constraints.gridx =;
Ячейка может содержать любой компонент, но удобнее было бы предоставить пользователю возможность выбора объекта наиболее общего типа, что позволяет использовать даже такой тип, как, например, граница, который не является компонен- TOM JavaBeans. Компонент JavaBeans определяется именем класса и свойствами.
<I ELEMENT bean (class, property*)>
<!ELEMENT class (#PCDATA)>
Как известно, при использовании компонентов JavaBeans класс должен иметь конструктор по умолчанию.
Свойство содержит имя name и значение value.
<!ELEMENT property (name, value)>
<!ELEMENT name (#PCDATA)>
Значение value имеет целочисленный, логический, строковый или компонентный ТИП.
<!ELEMENT value (int|string|boolean|bean)>
<!ELEMENT int (#PCDATA)>
<!ELEMENT string (#PCDATA)>
<!ELEMENT boolean (#PCDATA)>
Вот как выглядит типичный пример использования компонента JavaBeans для надписи JLabel со свойством text, которое имеет значение "".
<bean>
<class>javax.swing.JLabel</class>
<property>
<name>text</name>
<valuexstring>Face: </stringx/value>
</property>
</bean>
На первый взгляд может показаться странным, что строка окружена дескрипторами <string>. Почему бы не использовать параметр #PCDATA для строк, а дескрипторы применять только для других типов? Дело в том, что в таком случае придется использовать смешанное содержание и ослабленное правило для элементаvalue.
<!ELEMENT value ((#PCDATA|int|boo1ean|bean)* >
Однако, такое правило позволяет использовать произвольную смесь текста и дескрипторов.
Значение свойства задается в программе с помощью класса Beanin о, в котором перечисляются описания свойств компонента JavaBeans. Далее нужно найти свойство с соответствующим именем, затем вызвать его set-метод с нужным значением в качестве параметра.
После считывания описания пользовательского интерфейса программа получает достаточное количество информации для создания и расположения компонентов пользовательского интерфейса. Но, конечно, такой интерфейс не будет активным, потому что в нем нет слушателей событий. Для создания слушателей событий необходимо знать расположение всех компонентов и уметь их найти. По этой причине для каждого компонента JavaBeans поддерживается необязательный атрибут-идентификатор ID.
<!ATTLIST bean id ID #IMPLIED>
Например, вот как выглядит идентификатор поля со списком.
<bean id="face“>
<class>javax.swing.JComboBox</class>
</bean>
Напомним, что анализатор проверяет уникальность этих идентификаторов.
Программист может присоединять обработчики событий следующим образом.
gridbag = new GridBagPane("fontdialog.xml");
setContentPane(gridbag);
JCoinboBox face = (JComboBox)gridbag.get("face"); face.addListener(listener) ;
В этом примере язык XML используется только для описания макета компонентов, и читателям предлагается самостоятельно присоединить к ним слушатели событий с помощью соответствующего Java-кода. Можно даже пойти дальше и создать этот код в XML-описании. Наиболее обещающий подход заключается в использовании языка сценариев, например, языка BeanShell, подробное описание которого можно найти по адресу http://www.beanshell. org.
Программа, код которой приведен в листинге 12.2, демонстрирует способ использования класса GridBagPane для выполнения всей трудоемкой и рутинной работы по созданию макета типа GridBagLayout. Определение макета содержится в листинге 12.3, а внешний вид макета показан на рис. 12.4. Эта программа инициализирует только поля со списками (которые достаточно сложны для использования механизма установки свойств компонента JavaBeans, поддерживаемого в классе GridBagPane) и присоединяет слушатели событий. Класс GridBagPane в листинге 12.4 анализирует
XML-файл, создает компоненты и располагает их в окне программы. В листинге 12.5 показано содержимое файла с DTD-определениями.
Рис. 12.4. Диалоговое окно выбора шрифта, параметры которого определяются в XML-файлеРис. 12.4. Диалоговое окно выбора шрифта, параметры которого определяются в XML-файле
Рис. 12.4. Диалоговое окно выбора шрифта, параметры которого определяются в XML-файле
Рис. 12.4. Диалоговое окно выбора шрифта, параметры которого определяются в XML-файле
В этой программе демонстрируется типичный пример использования языка XML, который достаточно надежно позволяет выражать довольно сложные взаимосвязи. XML- анализатор возвращает фактическое значение после выполнения всей рутинной работы по проверке допустимости значения и наличия предлагаемых по умолчанию значений. Это позволяет сократить объем работы как для поставщика утилиты, т.е. создателя класса GridBagPane, так и для ее пользователя, т.е. программиста, который может выразить макет пользовательского интерфейса в файле, а не в длинном коде. Конечно, создание такого XML-файла все еще остается рутинным занятием даже при использовании XML- редактора. Следующим этапом развития было бы предоставление профаммистам инструментов быстрой разработки XML-кода. В среде быстрой разработки XML-кода профам- мисты пользовательского интерфейса на языке Java обладали бы достаточно высокой производительностью труда, сравнимой с производительностью труда профаммистов, которые работают в операционных системах Windows или Macintosh, обладая в то же время всеми преимуществами управления макетом.
Рабочая группа JSR-057 предложила более амбициозный стандарт на основе языка XML для описания и архивирования компонентов JavaBeans. Более подробное описание этого стандарта можно найти по адресу http://java.sun.com/products/ jfc/tsc/articles/persistence/index.html.
Листинг 12.2.GridBagTest.java
©version 1.00 2001-09-06
@author Cay Horstmann
import j ava.awt.* ;
import j ava.awt.event.*;
import javax.swing.*;
/**
Пример использования XML-файла для описания макета типа GridBagLayout
*/
public class GridBagTest {
public static void main(String!] args) {
JFrame frame = new FontFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
)
/**
Фрейм с диалоговым окном выбора шрифта, которое описано в XML-файле
*/
class FontFrame extends JFrame {
public FontFrame()
{
setsize(WIDTH, HEIGHT);
setTitle("GridBagTest") ;
gridbag = new GridBagPane("fontdialog.xml");
setContentPane(gridbag);
face = (JComboBox)gridbag.get("face") ;
size = (JComboBox)gridbag.get("size");
bold = (JCheckBox)gridbag.get("bold"); italic = (JCheckBox)gridbag.get("italic");
face.setModel(new DefaultComboBoxModel(new Object[] {
"Serif", "SansSerif", "Monospaced",
"Dialog", "Dialoginput"
size. setModel (new DefaultComboBoxModel (new Object []
{
"8", "10", "12", "15", "18", "24", "36", "48"
}));
ActionListener listener = new ActionListener()
{
p{ublic void actionPerformed(ActionEvent event) j setSample();
};
face.addActionListener(listener) ; size.addActionListener(listener) ;
bold.addActionListener(listener);
italic.addActionListener(listener);
setSample();
}
/**
Установка образца текста для выбранного шрифта
*/
public void setSample() {
String fontFace = (String)face.getSelected!tem(); int fontsize =
(String)size.getSelectedItem());
JTextArea sample = (JTextArea)gridbag.get("sample"); int fontStyle
=? Font.BOLD : 0)
+ (italic.isSelected() ? Font.ITALIC : 0) ;
sample.setFont(new Font(fontFace, fontStyle, fontSize)); sample.repaint();
}
private GridBagPane gridbag;
private JComboBox face;
private JComboBox size;
private JCheckBox bold;
private JCheckBox italic;
private static final int WIDTH = 400;
private static final int HEIGHT = 400;
Листинг 12.3. fontdialog.xml
<?xml version="1.0" ?>
<!DOCTYPE gridbag SYSTEM "gridbag.dtd">
<gridbag>
<row>
<cell anchor="EAST" weightx="0" weighty="0">
<bean>
<class>javax.swing.JLabel</class>
<property>
<name>text</name>
<valuexstring>Face: </stringx/value>
</property>
</bean>
</cell>
<cell fill="HORIZONTAL" weighty="0">
<bean id="face">
<class>javax.swing.JComboBox</class>
</bean>
</cell>
<cell gridheight="4" fill="BOTH">
<bean id="sample">
<class>javax.swing.JTextArea</class>
<property>
<name>text</name>
<valuexstring>The quick brown fox jumps overt» the lazy dog</stringx/value>
</property>
<property>
<name>editabie</name>
<valuexboolean>false</booleanx/value>
</property>
<property>
<name>lineWrap</name>
<valuexboolean>true</booleanx/value>
</property>
<property>
<name>border</name>
<value>
<bean>
<class>javax.swing.border.EtchedBorder</class>
</bean>
</value>
</property>
</bean>
</cell>
</row>
<row>
<cell anchor="EAST"weightx="0"weighty="0">
<bean>
<class>javax.swing.JLabel</class>
<property>
<name>text</name>
<valuexstring>Size: </stringx/value>
</property>
</bean>
</cell>
<cell fill="HORIZONTAL" weighty="0">
<bean id="size">
<class>javax.swing.JComboBox</class>
</bean>
</cell>
</row>
<row>
<cell gridwidth="2"weightx="0" fill="NONE">
<bean id="bold">
<class>javax.swing.JCheckBoxc/class>
<property>
<name>text</name>
<value><string>Bold</stringx/value>
</property>
</bean>
</cell>
</row>
<row>
<cell gridwidth="2"weightx="0" fill="NONE">
<bean id="italic”>
<class>javax.swing.JCheckBox</class>
<property>
<name>text</name>
<value><string>Italic</stringx/value>
</property>
</bean>
</cell>
</row>
</gridbag>
Листинг
/**
1.00 2001-09-06
@author Cay Horstmann
*/
import
import
import
import j
import
import import
/**
Эта панель использует XML-файл для описания его компонентов и их расположения в макете типа GridBagLayout
*/
public class GridBagPane extends JPanel {
/**
Создание панели с макетом типа GridBagLayout
@param filename the name of the XML file that
describes the pane's components and their positions
*/
publicfilename)
{
setLayout(new GridBagLayout()) ;
constraints = new
try {
DocumentBuilderFactory factory
=
factory.setvalidating(true);
factory.setlgnoringElementContentWhitespace(true);
builder =
Document doc =
parseGridbag(doc.getDocumentElement());
}
catch (ParserConfigurationException exception) {
;
}
catch (SAXException exception) {
;
}
catch (IOException exception) {
;
}
/**
Возвращает компонент с заданным именем
@param name a component name
©return the component with the given name, or null if no component in this grid bag pane has the given name */
public Component get(String name)
{
Component[] components = getComponents();
for (int i = 0; i < components.length; i++)
{
if (components[i].getName().equals(name)) return components[i] ;
}
return null;
}
/**
Анализирует элемент типа GridBag
@param e a gridbag element
*/
private void parseGridbag(Element e)
{
NodeList rows = e.getChildNodes();
for (int i = 0; i < rows.getLength(); i++)
{
Element row = (Element)rows.item(i);
NodeList cells = row.getChiIdNodes();
for (int j = 0; j < cells.getLength(); j {
Element cell = (Element)cells.item(j); parseCell(cell, i, j) ;
}
/**
Анализирует элемент-ячейку
Sparam e a cell element
@param r the row of the cell
Sparam с the column of the cell
*/
private void parseCell(Element e, int r, int c) {
// Возврат атрибутов
String value = e.getAttribute("gridx");
if (value.length() ==0) // Используется no умолчанию {
if (c == 0) constraints.gridx = 0;
else constraints.gridx += constraints.gridwidth;
}
else
constraints.gridx =
value = e.getAttribute("gridy");
if (value.length() == 0) // use default
constraints.gridy = r;
else
constraints.gridx =
constraints.gridwidth =
constraints.gridheight
=;
constraints.weightx
=
constraints.weighty
=
constraints.ipadx =
constraints.ipady
=parselnt (e.getAttribute("ipady"));
// Использование механизма отражения
// для возврата целочисленных значений
// статических полей
Class cl =
try {
String name = e.getAttribute("fill");
Field f = cl.getField(name);
constraints.fill = }
catch(Exception ex) { }
try {
String name = e.getAttribute("anchor") ;
Field f = cl.getField(name);
constraints.anchor = }
catch(Exception ex)
}
Component comp =
(Element)e.getFirstChild());
add(comp, constraints);
/**
Анализирует компонент JavaBeans
Sparam e a bean element
*/
private Object parseBean(Element e) (
try
{.
NodeList children = e.getChiIdNodes() ;
Element classElement =
String className
= ((Text)classElement.getFirstChild()).getData();
Class cl =
Object obj = cl.newlnstance();
if (obj instanceof Component)
((Component)obj).setName(e.getAttribute("id"));
for (inti=1; i<children.getLength(); i++) {
Node propertyElement = children.item(i);
Element nameElement =
String propertyName =
Element valueElement
=
Object value = parseValue(valueElement);
Beaninfo beaninfo =
PropertyDescriptor[] descriptors =
boolean done = false;
f{or (int j =0; I done &&j<descriptors.length; j++)
if (descriptors[j].getNameO.equals(propertyName))
descriptors[ j ] .getWriteMethod().invoke(obj, newObject[] {value}) ;
done = true;
}
}
return obj ;
}
catch (Exception ex)
{
// При использовании механизма отражения могут
// генерироваться разные исключительные ситуации ex.printStackTrace();
return null;
)
/**
Анализирует элемент-значение
@param e a value element
*/
private Object parseValue(Element e) {
Element child = (Element)e.getFirstChild();
if (child.getTagName().equals("bean")) return parseBean(child);
String text = ((Text)child.getFirstChild()).getData();
if (child.getTagName().equals("int")) return new Integer(text);
else if (child.getTagName().equals("boolean")) return new Boolean(text);
else if (child.getTagName().equals("string"))
return text; else
return null;
private GridBagConstraints constraints;
Листинг 12.5.gridbag.dtd
<!ELEMENT gridbag (row)*>
<!ELEMENT row (cell)*>
<!ELEMENT cell (bean)>
<!ATTLIST cell gridx CDATA #IMPLIED>
<!ATTLIST cell gridy CDATA #IMPLIED>
<!ATTLIST cell gridwidth CDATA " 1" >
<!ATTLIST cell gridheight CDATA "1">
<!ATTLIST cell weightx CDATA "100">
<!ATTLIST cell weighty CDATA "100">
<!ATTLIST cell fill (NONE|BOTH|HORIZONTAL|VERTICAL) "NONE">
<!ATTLIST cell anchor (CENTER|NORTH|NORTHEAST|EAST|SOUTHEAST|SOUTHS |SOUTHWEST|WEST|NORTHWEST) "CENTER">
<!ATTLIST cell ipadx CDATA "0">
<!ATTLIST cell ipady CDATA " 0">
<!ELEMENT bean (class, property*)>
<!ATTLIST bean id ID #IMPLIED>
<!ELEMENT class (#PCDATA)>
<!ELEMENT property (name, value)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT value (int|string|boolean|bean)>
<!ELEMENT int (#PCDATA)>
<!ELEMENT String (#PCDATA)>
<!ELEMENT boolean (#PCDATA)>
Пространства имен
Во избежание конфликтов при использовании одинаковых имен в языке Java предусмотрено использование пакетов. Программисты могут использовать одинаковые имена для разных классов, если классы находятся в разных пакетах. В языке XML аналогичный механизм пространства имен (namespace) используется для имен элементов и атрибутов.
Пространство имен обозначается с помощью URI-указателя, например так, как показано ниже.
http://www.w3c.org/2001/XMLSchema
uuid:lc7 59aed-b748-47 5c-ab68-106797 00c4f2 urn:com:books-r-us
Чаще всего для них используется форма URL-указателя согласно протоколу HTTP. Учтите, что URL-указатель используется в данном случае только как строка- идентификатор пространства имен, а не для обнаружения документа. Например, приведенные ниже идентификаторы обозначают разные пространства имен, хотя Web-сервер использует для обоих URL-указателей один и тот же документ.
Более того, по указанному URL-идентификатору пространства имен вообще может не быть никакого документа, потому что XML-анализатор не пытается найти что- нибудь по этому адресу. Однако, в качестве справки для программиста по этому URL- указателю может располагаться документ с описанием предназначения данного пространства имен. Например, по адресу http://www.w3c.org/2001/XMLSchema располагается документ с описанием стандарта XML Schema.
Почему для обозначения пространства имен используются URL-указатели согласно протоколу HTTP? Очевидно, что в таком случае удается гарантировать их уникальность. Действительно, при выборе реального URL-указателя уникальность имени хоста гарантируется уникальностью системы доменных имен, а уникальность остальной части URL-указателя должна обеспечить организация хоста. Именно такая идея лежит в основе уникальности системы имен пакетов на основе обращения доменных имен.
Конечно, при этом желательно было бы иметь максимально короткие имена. В языке Java с этой целью предусмотрен механизм импорта длинных имен пакетов и использования вместо них коротких имен классов. В языке XML предусмотрен аналогичный механизм.
<элемент xmlns="иН1_указатель_пространства_имен"> дочерние_узлы
</элемент>
Таким образом все дочерние узлы элемента становятся частью заданного пространства имен.
Более того, дочерний узел может обеспечить свое собственное пространство имен.
>
<дочерний_узедап1п5=''иК1_указатель_пространства_имен_2" > дочерние_узлы_2
</дочерний_узел> дочерние_узлы
</элемент> •
Здесьи дочерние_узлы_2 являются частью второго пространст
ва имен.
Этот простой механизм хорошо работает только в том случае, когда нужно одно пространство имен, или если пространства имен имеют естественную вложенность друг в друга. Иначе придется использовать другой механизм, который не имеет аналога в языке Java. Для пространства имен можно использовать псевдоним (alias), т.е. короткий идентификатор некоторого документа.
<xsl:stylesheet
xmlns:xsl="http://www.w3c.org/1999/XSL/Transform">
<xsl:output method="html">
</xsl:stylesheet>
Атрибут:определяет
пространство имен и псевдоним. В этом примере псевдонимом является строка Вся строкаstylesheet фактически означает, что "stylesheet находится в
пространстве
Только дочерние элементы наследуют пространство имен своего родителя. Атрибуты без явного указания префикса-псевдонима не считаются частью пространства имен. Рассмотрим следующий пример.
<configuration xmlns="http://www.horstmann.com/corejava" xmlns:si="http://www.bipm.fr/enus/3_SI/si.html"
>
<size value="210" si:unit ="mm"/>
</configuration>
^iЭлементы configuration и size являются частью пространства имен с URI-
указателем http: //www.horstmann.com/corejava. Атрибут si :unit является частью пространства имен с URI-указателем http:/ /www. bipm. fr/enus/ 3_SI/ si.html. Но атрибут value не является частью ни одного из этих пространств имен.
По умолчанию DOM-анализатор компании Sun не учитывает пространства имен.
Для включения режима управления пространствами имен нужно вызвать метод setNamespaceAware класса DocumentBuilderFactory.
factory.setNamespaceAware(true);
После чего все созданные этой фабрикой конструкторы будут поддерживать пространства имен.
Каждый узел обладает следующими тремя свойствами.
Квалифицированным именем, (qualifiedчате) с префиксом-псевдонимом, возвращаемым методом getNodeName, методом getTagName и т.д.
Локальным именем (localчате) без префикса или пространства имен, возвращаемого методом getLocalName.
URI-указателем пространства имен, возвращаемым методомgetNamespaceURI. Предположим, что анализатор получает следующий элемент.
<xsl:stylesheet
xmlns:xsl="http://www.w3c.org/1999/XSL/Transform">
В таком случае он сообщит о наличии таких компонентов.
Квалифицированное имя —xsl: stylesheet.
Локальное имя — stylesheet.
URI-указатель пространства имен —http ://www. w3c. org/ 1999/XSL/Transform.
Если режим управления пространствами имен отключен, то методы getLocalName и getNamespaceURI возвращают значение null.
!
д i^ yfor9 .w3c .dom. Node
: \• Метод String getLocalName () возвращает локальное имя (без пре
фикса-псевдонима) или значение null, если режим управления пространствами имен отключен.
Метод String getNamespaceURI () возвращает URI-указатель пространства имен или значение null, если узел не является частью про-
странства имен или режим управления пространствами имен отключен.
3avax.xml.parsers.DocumentBuilderFactory
Метод boolean isNamespaceAware () возвращает значение свойства "включение режима управления пространствами имен" фабрики. Если оно равно true, то для генерируемых этой фабрикой анализаторов включен режим управления пространствами имен.
Метод void setNamespaceAware (boolean value) задает значение свойства "включение режима управления пространствами имен" фабрики. Если параметр value имеет значение true, то для генерируемых этой фабрикой анализаторов включается режим управления пространствами имен.
SAX-анализаторы
DOM-анализатор полностью считывает XML-документ со всей его древовидной структурой. Для большинства практических приложений этот принцип работы прекрасно подходит. Но он совершенно неэффективен для обработки большого документа при использовании простого алгоритма обработки, который можно было бы применять "на лету" без просмотра всей древовидной структуры. Именно в таких случаях применяются SAX-анализаторы, которые посылают сообщения в ходе анализа XML-документа. При этом документ ne хранится в памяти, и только обработчики событий могут в случае необходимости создать нужную им структуру для хранения полученных данных. Фактически DOM-анализатор основан на SAX-анализаторе, причем DOM-дерево создается на основе сообщений, полученных от SAX-анализатора.
Для использования SAX-анализатора потребуется создать обработчик с определениями событий, которые происходят во время анализа XML-документа. В интерфейсе ContentHandler определено несколько методов обратной связи, которые выполняются анализатором во время анализа XML-документа.
Методы startElement/endElement вызываются при получении начального и конечного дескрипторов.
Метод characters вызывается при получении символьных данных.
МетодыstartDocument/endDocument вызываются в начале и конце документа.
Рассмотрим следующий фрагмент XML-документа.
<font>
<name>Helvetica</name>
<size unit="pt">36</size>
</font>
Во время анализа этого фрагмента SAX-анализатор генерирует вызовы следующих методов:
startElement— для элемента font;
startElement—для элемента name;
characters — для строки Helvetica;
endElement — для элемента name;
startElement — для элемента size с атрибутом units= "pt";
characters — для строки 3 6;
endElament—для элемента size;
endElement — для элемента font.
Для выполнения нужных действий во время вызовов этих методов их необходимо переопределить. Программа, код которой приведен в листинге 12.6 в конце раздела, выводит все гипертекстовые ссылки типа <а href=" ...">, найденные вHTML-файле с указанным URL-указателем в качестве параметра командной строки (при его отсутствии по умолчанию используется URL-указатель http: / / www. w3 c . or g). В ней переопределен метод startElement обработчика событий для проверки всех гипертекстовых ссылок с именем а и атрибутом href. Такой код очень полезен для создания программ Web- навигаторов (Web-crawler), которые способны самостоятельно просматривать большое количество Web-страниц по обнаруженным гипертекстовым ссылкам.
• К сожалению, большинство HTML-страниц настолько отклоняются от общепринятого формата XML, что данный пример программы способен работать далеко не со всеми. Как уже говорилось выше, консорциум W3C рекомендует Web-дизайнерам использовать язык XHTML, т.е. диалект языка HTML, который поддерживается современными Web-броузерами и совместим со стандартом XML. Более подробную информацию о языке XHTML можно найти по адресуhttp:/ /www.w3c.org/TR/xhtmll/.Логично, что все Web-страницы консорциума W3C написаны на языке XHTML и их можно использовать для проверки данной тестовой программы. Например, для создания списка всех гипертекстовых ссылок на Web-странице http://www.w3c.org/MarkUp нужно использовать следующую команду.
java SAXTest http://www.w3c.org/MarkUp
Эта тестовая программа является хорошим примером использования SAX- анализатора. В ней полностью игнорируется контекст, в котором находится элемент а, а также не сохраняется древовидная структура документа.
Для получения SAX-анализатора используется следующая команда.
SAXParserFactory factory = SAXParserFactory.newInstance() ; SAXParser saxParser = factory.newSAXParser();
Для обработки документа следует использовать показанную ниже команду, в которой source — это источник входных данных в виде файла, URL-указателя или входного потока; handler— подчиненный класс класса DefaultHandler. Класс DefaultHandler содержит определения http: Ihw, w3c . org пустых методов для следующих четырех интерфейсов.
ContentHandler
DTDHandler
EntityResolver
ErrorHandler
В примере программы, код которой приводится в листинге 12.6, определен обработчик событий, который переопределяет метод startElement интерфейса ContentHahdler для поиска элементов с именем а и атрибутом href.
DefaultHandler handler = new
DefaultHandler()
{
public void startElement(String namespaceURI,
String Iname, String qname, Attributes attrs)
{
ifattrs != null)
for (int i = 0; i <i++)
String aname = if (aname.equalsIgnoreCase("href") )
System.out.printIn(attrs.getValue(i)) ; } } }
);
Метод startElement имеет три параметра, которые описывают имя элемента. Параметрсообщает квалифицированное имя в видеЕсли
включен режим использования пространства имен, то параметрыи
описывают пространство имен и локальное (неквалифицированное) имя.
Также, как и при работе с DOM-анализатором, режим использования пространства имен по умолчанию отключен. Он включается с помощью вызова метода класса-фабрики SAXParserFactory.
SAXParserFactory factory =
factory.setNamespaceAware(true);
SAXParser saxParser =
Итак, в листинге 12.6 содержится код простейшего Web-навигатора. Далее в главе рассматривается еще один интересный пример использования SAX-анализатора. Простой способ превращения в XML-документ источника данных несовместимого с XML типа заключается в генерации SAX-событий, которые может обрабатывать XML- анализатор. Более подробно эти вопросы рассматриваются ниже в разделе об XSL- преобразованиях.
Листинг12.6.
import java.io.*;
import java.net.*;
import
import
import* ;
/ * *
Пример использования SAX-анализатора. Эта программа выводит все гипертекстовые ссылки, найденные на указанной Web-странице, созданной на языке XHTML.
Синтаксис использования:SAXTest
*/
public class SAXTest {
public static voidargs) throws Exception
{ String url; if0)
{
url = "http://www.w3c.org";
" + url) ;
}
else
url =
DefaultHandler handler = new
DefaultHandler()
{
public void startElement(String namespaceURI, String Iname, String gname, Attributes attrs)
{
if (Iname.equalsIgnoreCase("a") && attrs != null) {
for (int i = 0; i < attrs.getLength(); i++)
{
String aname =
if (aname.equalsIgnoreCase("href")) -
System.out.println(attrs.getValue(i));
}
}
} };
SAXParserFactory factory = SAXParserFactory.newlnstance() ;
;
SAXParser saxParser = factory.newSAXParser();
InputStream in = new URL(url).openStream();
saxParser.parse(in, handler);
B
javax.xml.parsers.SAXParserFactory
Метод static SAXParserFactory newlnstance () возвращает экземпляр класса SAXParserFactory.
Метод SAXParser newSAXParser () возвращает экземпляр класса SAXParser.
Метод boolean isNamespaceAware () возвращает значение свойства "включение режима управления пространствами имен" фабрики. Если оно равно true, то для генерируемых этой фабрикой анализаторов включен режим управления пространствами имен.
Метод void setNamespaceAware (boolean value) задает значение свойства "включение режима управления пространствами имен" фабрики. Если параметр value имеет значение true, то для генерируемых этой фабрикой анализаторов включается режим управления пространствами имен.
Метод boolean isValidating() возвращает значение свойства "проверка входных данных" фабрики. Если оно равно true, то для генерируемых этой фабрикой анализаторов включен режим проверки входных данных.
Метод void setvalidating(boolean value) задает значение свойства "проверка входных данных" фабрики. Если параметр value имеет значение true, то для генерируемых этой фабрикой анализаторов включается режим проверки входных данных.
Ia ^Tf^ avax. xml. parsers. SAXParser
LU v * Метод void parse (File f, DefaultHandler handler) анализирует XML-документ на основании указанного файла.
Метод void parse(String url, DefaultHandler handler) анализирует XML-документ на основании указанного URL-указателя.
Метод void parse(Inputstream in, DefaultHandler handler) анализирует XML-документ на основании входного потока.
or 'j xml ' s'x C fc i eitHandl er
ss™Метод void startDocument () вызывается в начале XML-документа.
Метод void endDocument () вызывается в конце XML-документа.
Метод void startElement(String uri, String Iname, String qname, Attributes attr) вызывается в начале элемента.
Параметры:
uriURI-указатель пространства имен (если включен режим ис
пользования пространства имен)
Inameлокальное имя без префикса-псевдонима (если включен ре
жим использования пространства имен)
qnameимя элемента (если не включен режим использования про
странства имен) или квалифицированное имя с префиксом- псевдонимом (если анализатор сообщает квалифицированные имена помимо локальных имен)
attrатрибуты
Метод void startElement(String uri, String lname, String qname) вызывается в конце элемента.
Параметры:
uriURI-указатель пространства имен (если включен режим ис
пользования пространства имен)
lnameлокальное имя без префикса-псевдонима (если включен ре
жим использования пространства имен)
qnameимя элемента (если не включен режим использования про
странства имен) или квалифицированное имя с префиксом- псевдонимом (если анализатор сообщает квалифицированные имена помимо локальных имен)
Метод characters (char[] data, int start, int length) вызывается при получении символьных данных.
Параметры:
dataмассив символьных данных
startиндекс первого символа в массиве данных, который является
частью полученных символов
lengthдлина полученной строки символов
[a Pors"xm^ "sax* Attributes
Метод int getLength () возвращает количество атрибутов, которые содержатся в коллекции.
Метод String getLocalName(int index) возвращает локальное имя (без префикса-псевдонима) атрибута с заданным индексом или пустую строку, если для анализатора не включен режим использования пространства имен.
Метод String getURI(int index) возвращает URI-указатель атрибута с заданным индексом или пустую строку, если элемент не является частью пространства имен или для анализатора не включен режим использования пространства имен.
Метод String getQName(int index) возвращает квалифицированное имя (без префикса-псевдонима) атрибута с заданным индексом или пустую строку, если квалифицированное имя не получено анализатором.
Метод String getValue(int index) возвращает значение атрибута no заданному индексу или значение null, если такое значение отсутствует.
Метод String getValue (String qname) возвращает значение атрибута по квалифицированному имени или значение null, если такое значение отсутствует.
Метод String getValue(String uri, String Iname) возвращает значение атрибута no URI-указателю и локальному имени или значение null, если такое значение отсутствует.
Создание XML-документов
После изучения способов создания программ на языке Java, предназначенных для чтения XML-документов, рассмотрим программы, которые выполняют обратное действие, т.е. генерируют данные в виде XML-документа. Конечно, XML-документ можно легко создать с помощью последовательности операторов print, которые будут содержать элементы, атрибуты и текст. Но создание такого громоздкого кода представляет собой очень трудоемкий процесс. Кроме того, такой код, как правило, изобилует ошибками, например, при использовании специальных символов " или < в значениях атрибутов и в тексте.
Более удобный подход основан на создании DOM-дерева с содержимым документа, а затем на простом переписывании содержания этого дерева. Создавать DOM-дерево следует с генерации пустого документа с помощью метода newDocument класса DocumentBuilder.
Document doc = builder.newDocument();
Затем для создания элементов документа нужно применить метод createElement класса Document.
Element rootElement =
Element childElement =
Далее, для создания текстовых узлов применяем метод createTextNode.
Text textNode = doc.createTextNode(textcontents);
Для создания корневого элемента документа и дочерних узлов используется приведенный ниже код.
doc.appendchild(rootElement);
rootElement.appendChild(childElement);
childElement.appendChild(textNode);
По мере создания DOM-дерева для указания значений атрибутов элементов следует использовать метод setAttribute класса Element.
rootElement.setAttribute(name, value);
Забавно, что в текущей версии DOM API-интерфейса не поддерживается запись DOM-дерева в выходной поток. Для преодоления этого ограничения следует использовать API-интерфейс XSLT (XML Style Sheet Transformations), который выполняет "ничего не делающее" ("do nothing") преобразование документа и создает необходимый нам выходной поток. Для включения узла DOCTYPE в выходной поток нужно также указать системный идентификатор systemldentifier и открытый идентификатор publicIdentifier в качестве выходных свойств.
// Создание "ничего не делающих" преобразований
Transformer t = TransformerFactory
.newlnstance().newTransformer() ;
// Установка свойств выходного потока для узла DOCTYPE t.setOutputProperty("doctype-system", systemidentifier);
t.setOutputProperty("doctype-public", publicldentifier);
// Применение "ничего неделающих" преобразований
//и передача выходных данных в файл t.transform(new DOMSource(doc),
new StreamResult(new FileOutputStream(f)));
Более подробно XSLT-преобразования описываются в следующем разделе, а пока предположим, что этот код каким-то "магическим" образом позволяет получить выходной поток в виде XML-документа.
Полученный XML-документ не содержит свободных мест (т.е. символов конца строки, отступов или пробелов). Для их включения в выходной XML-документ сле-
Dдует указать значение yes для свойства выходного потока indent.
В листинге 12.7 приведен код типичной программы, которая позволяет получить XML- документ. Эта программа рисует картину в стиле модерн, т.е. произвольный набор прямоугольников разного цвета, которые показаны на рис, 12.5. Для сохранения этого "шедевра" используется масштабируемый векторный формат SVG (Scalable Vector Fonnat). Фактически SVG-формат является вариантом ХМ1х|юрмата, который используется для описания сложной графики в платформо-независимом виде.
Рис. 12.5. Картина, которую предстоит сохранить в формате SVG
Более подробные сведения об этом формате можно найти по адресу http://www.w3c.org/Graphics/SVG. Для просмотра SVG-файлов следует исполь-
зовать специализированную программу просмотра Batik компании Apache (http://xml.apache.org/batik) или подключаемый модуль компании Adobe (http: //www.adobe.com/svg/main.html). На рис. 12.6 показан внешний вид программы Batik.
Рис. 12.6. Программа Batik компании Apache, предназначенная для просмотра SVG-файловРис. 12.6. Программа Batik компании Apache, предназначенная для просмотра SVG-файлов
Рис. 12.6. Программа Batik компании Apache, предназначенная для просмотра SVG-файлов
Рис. 12.6. Программа Batik компании Apache, предназначенная для просмотра SVG-файлов
Здесь мы не будем вдаваться в подробности устройства SVG-формата, но если читателя заинтересовала эта тема, то он может найти прекрасное вводное руководство на Web-узле компании Adobe. В данном случае достаточно только знать способ выражения набора цветных прямоугольников в этом формате.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20000802//EN" "http://www.w3c.org/TR/2000/CR-SVG-2 0000802/DTD/svg-2 0000802.dtd"> <svg width="300" height="150">
<rect x="231" y="61" width="9" height="12" fill=”#6e4al3"/>
<rect x="107" y="106" width="56" height="5" fill="#c406be"/>
</svg>
Как видите, каждый прямоугольник описывается узлом node, атрибуты которого содержат значения координат, ширины, длины и цвета прямоугольника (которое выражается в виде шестнадцатеричного RGB-значения).
В SVG-формате широко используются атрибуты. Причем некоторые из них имеют очень сложную структуру, например, для указания пути.
<path d="M 100 100 L 300 100 L 200 300 z">
Здесь M означает команду moveto(перейти), L — lineto (линия), z — closepath (конец пути). Видимо, создатели этого формата данных не совсем доверяли формату XML для создания структурированных данных. Очевидно, что вместо таких сложных атрибутов следовало бы использовать элементы.
Ниже показан пример программы, в котором демонстрируются способы создания XML-документов.
Листинг 12.7.XMLWrlteTest.java
/**©version 1.00/**©version 1.002001-09-242001-09-24
/**
©version 1.00
/**
©version 1.00
2001-09-24
2001-09-24
©author Cay Horstmann
/
import java.awt.*;
import java.awt.geom.*;
import java.io.
import java.util.*;
import j ava.awt.event.*;
import javax.swing.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.T;
import org.w3c.dom.*;
/**
Пример создания XML-документа, который содержит описание "картины" в стиле модерн
(набор цветных прямоугольников) в SVG-формате
/
public class XMLWriteTest {
public static void main(String[] args) {
XMLWriteFrame frame = new XMLWriteFrame() ;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show();
}
}
/**
Фрейм с панелью для отображения картины
/
class XMLWriteFrame extends JFrame {
public XMLWriteFrame()
{
setTitle("XMLWriteTest");
setsize(WIDTH, HEIGHT);
. // Создание панели в фрейме
panel = new RectanglePanel();
Container contentPane = getContentPane(); contentpane.add(panel);
// Создание команды меню
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu menu = new JMenu("File") ;
menuBar.add(menu) ;
JMenuItem newltem = new JMenuItem("New");
menu.add(newltem) ;
newltem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
panel.newDrawing();
}
JMenuItem saveitem = new JMenuItem("Save");
menu.add(saveitem);
saveItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event) {
try {
saveDocument();
}
catch (TransformerException exception) {
JOptionPane.showMessageDialog(
XMLWriteFrame.this, exception.toString()); }
catch (IOException exception) {
JOptionPane.showMessageDialog(
XMLWriteFrame.this, exception.toString());
)
});'
JMenuItem exititem = new JMenuItem("Exit");
menu.add(exititem);
exititem.addActionListener(new
ActionListener()
{
public void act ionPer formed(ActionEvent event) {
System.exit(0);
)
)) ;
/**
Сохранение рисунка в SVG-формате
*/
public void saveDocument()
throws TransformerException, IOException
{
JFileChooser chooser = new JFileChooser();
if (chooser.showSaveDialog(this) != JFileChooser,APPROVE_OPTION) return;
File f = chooser.getSelectedFile();
Document doc = panel.buildDocument();
Transformer t = TransformerFactory
.newlnstance().newTransformer() ;
t.setOutputProperty("doctype-system", "http://www.w3c.org/TR/2 000/
CR-SVG-20000802/DTD/svg-20000802.dtd" );
t.setOutputProperty("doctype-public", "-//W3C//DTD SVG 20000802//EN");
t.transform(new DOMSource(doc) , new StreamResult(new FileOutputStream(f))) ;
)
public static final int WIDTH = 300;
public static final int HEIGHT = 200;
private RectanglePanel panel;
/**
Панель, в которой показан набор цветных прямоугольников */ class RectanglePanel extends JPanel {
public RectanglePanel()
{
rectsnew ArrayList();
colors = new ArrayList();
generator = new Random();
DocumentBuilderFactory factory =
try {
builder = factory.newDocumentBuiIder();
}
catch (ParserConfigurationException exception) {
) exception.printStackTrace() ;
)
/**
Создание новой картины
*/
public void newDrawing()
{
int n = 10 + generator.nextlnt(20) ;
rects.clear();
for (int i = 1; i <= n; i++) {
int x = generator.nextlnt(getWidth());
int у = generator.nextlnt(getHeight());
int width = generator.nextlnt(getWidth() — x);
int height = generator.nextlnt(getHeight() — y) rects.add(newRectangle(x, y, width, height));
int r = generator.nextlnt(256);
int g = generator.nextlnt(256);
int b = generator.nextlnt(256);.
colors . add (new Color (r, g, b));
}
repaint();
}
public voidg)
{
if== 0)
super.paintcomponent(g) ;
Graphics2D g2 =
// Нарисовать все прямоугольники
for (int i = 0; i <i++)
С
g2.setcolor((Color)colors.get(i));
g2.fill((Rectangle2D)rects.get(i)); }
/**
Создание SVG-документа для текущей картины the DOM tree of the SVG document */ public Document
Document doc =
Element svgElement = doc.appendChiId(svgElement); "" + "" +
for (int i = 0; i <i++)
< Color с =
Rectangle2D r = Element rectElement = rectElement.("x", "" + r.getXO);
"" + r.getYO);
"" + "" +
colorToString(c)) ; svgElement.appendchild(rectElement);
}
return
/**
Шестнадцатеричное представление цвета с a color a string of the form #rrggbb
*/
private static Stringc)
{
StringBuffer buffer = new buf
& OxFFFFFF));
< 6)
buffer.insert(0, '#');
return
private ArrayList rects;
private ArrayList colors;
private Random generator;
private DocumentBuiIder builder;
ввj avax.xml. parsers.Document Builder
в
в
Метод Document newDocument () возвращает пустой документ.
org. w3c.dam. Document
Метод Element createElement (String name) возвращает элемент с заданным именем.
Метод Text createTextNode (String data) возвращает текстовый узел с заданными данными.
org.w3c.dom.Node
Методvoid appendChild(Node child) вставляет узел в список дочерних узлов данного узла.
org.w3c.dom.Element
Метод void setAttribute (String name, String value) указывает
атрибут с заданным именем и значением.
Метод void setAttributeNS(String uri, String qname, String value) указывает атрибут с заданным URI-указателем, квалифицированным именемизначением.
Параметры:
uriURI-указателя пространства имен или значение null
qnameквалифицированное имя; если оно имеет префикс-псевдоним,
то параметр uri должен иметь значение null
valueзначение атрибута -
j avax. xml. transform. TransformerFactory
Метод static TransformerFactory newlnstance () возвращает экземпляр класса TransformerFactory.
Метод Transformer newTransformer () возвращает экземпляр класса Transformer, который выполняет идентичное или "ничего не делающее" преобразование.
javax.xml.transform. Transformer
Метод void setOutputProperty(String name, String value) задает свойство выходного потока. Весь перечень этих свойств можно найти по адресу http : //www. w3c . org/TR/xslt#output, а наиболее полез-
ные из них приведены ниже.
javax.xml. transform. OutputKeys
Константа static String DOCTYPE_PUBLIC — ЭТО открытый идентификатор doctype-public, который будет использоваться в объявлении D0CTYPE.
Константа static String DOCTYPE_SYSTEM — это системный идентификатор doctype-system, который будет использоваться в объявлении D0CTYPE.
Константа static String INDENT— это системный идентификатор indent, который означает использование (значение yes или по) переносов строк и отступов.
Метод void trans form (Source from, Result to) преобразует XML- документ.
javax.xml.transform.dom.DOMSource
Конструктор DOMSource (Node n) создает источник данных на основе заданного узла; обычно п представляет собой узел документа.
j avax. xml .transform, stream. StreamResult
Конструктор StreamResult (File f) создает поток на основе указанного файла.
Конструктор StreamResult (OutputStream out) создает поток на основе указанного выходного потока байтов.
Конструктор StreamResult (Writer out) создает поток на основе указанного выходного потока символов.
Конструктор StreamResult (String systemID) создает поток на основе указанного системного идентификатора (обычно относительного или абсолютного URL-указателя).
XSL-преобразования
Как видите, XML-формат представляет собой удобный и полезный формат обмена структурированными данными. Однако, большинство XML-данных не предназначено для просмотра конечными пользователями. В этом разделе описываются способы преобразования XML-данных в презентационном формате, например, в HTML- формате или в виде простого текста.
О
В этом разделе описываются способы преобразования XML-данных в HTML- данные для их представления в Web-броузере. Альтернативный способ заключается в непосредственном представлении XML-файлов в Web-броузере. Например, в Web-броузере Internet Explorer XML-документы представлены в виде дерева, а в последних версиях Web-броузеров Netscape, Internet Explorer и Opera предусмотрено использование стандарта Cascading Style Sheets (CSS) вместе с XML- документами, что имеет гораздо большее значение. С помощью страницы стилей можно управлять способом отображения XML-элементов в окне Web-броузера. Более подробную информацию о стандарте CSS можно найти в его спецификации по адресу http: //www.w3c.org/TR/REC-css2. Некоторые важные аргументы за и против совместного использования стандарта CSS и XML рассматриваются в статье Панкая Камтхана (Pankaj Kamthan) XML and CSS: Structured Markup with Display Semantics noадресу http: //tech, irt. org/ar tides/ jsl98.
Страница стилей и шаблоны преобразования предназначены для описания способа преобразования XML-документов в некотором другом формате, как схематически показано на рис. 12.7.
Рассмотрим типичный пример преобразования XML-документа с записями о со- трудникахвHTML-документ. ВоткаквыглядитисходныйXML-документ.
<staff>
<employee>
<name>Carl
<salary>7
year="1987"day="15"/>
</employee>
<employee>
Cracker</name>
<salary>5 000 0</salary> year="1989"day="l"/>
</employee>
<employee>
<name>Tony Tester</name>
<salary>40000</salary>
year="1990"day="15"/>
</employee>
</staff>
На его основании желательно получить следующую HTML-таблицу.
<table border="l">
<tr>
<td>Carl
</tr>
<tr>
<td>Harry0</tdxtd>1989-10-l</td>
</tr>
<tr>
<td>Tony
</tr>
</table>
Эта таблица может быть включена в состав другого, более крупного HTML- документа,JSP-страницы (Java ServerPage).
Рис. 12.7. Способ преобразования XML-документа в HTML- документ спомощью XSL-преобразования
Спецификация XSLT имеет чрезвычайно сложную структуру и ее описанию посвящено довольно много книг. Ограничимся здесь описанием только некоторых ком-
понентов XSLT, которые потребуются для понимания принципов работы данного примера. Более подробную информацию об этом можно найти в книге Essential XML Дона Бокса (Don Box) и др. [Addison-Wesley 2001], а описание спецификации XSLT находится по адресу http: //www.w3c.org/TR/xslt.
Страница стилей с шаблонами преобразования имеет следующий вид.
<?xml versions"1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3c.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
шаблоны
</xsl:stylesheet>
В данном примере элемент xsl: output указывает способ представления выходных данных, т.е. в формате HTML в этом примере. Другими возможными форматами представления являются XML-документ (значение xml) и простой текст (значение text).
Типичный шаблон имеет следующий вид.
<xsl:template match="/staff/employee">
<tr><xsl: apply-templates/x/tr>
</xsl:template>
Для атрибута match задано значение в виде выражения в стандарте XPath, который представляет собой еще один вариант XML-стандарта и описывает набор узлов XML- документа. Например, приведенное ниже выражение в стандарте XPath описывает все элементы employee, которые являются дочерними узлами корневого узла staff.
/staff/employee
А следующее выражение в стандарте XPath описывает все узлы-атрибуты year элементов hiredata внутри элементов employee, которые являются дочерними узлами корневого узла staff.Здесь символ @ обозначает атрибут.
/staff/employee/hiredate/@year
Следующее выражение в стандарте XPath описывает все текстовые узлы, которые являются дочерними узлами элементов name внутри элементов employee, которые являются дочерними узлами корневого узла staff.Здесь круглые скобки () обозначают одну из многих функций, которые могут использоваться в выражениях стандарта XPath.
/staff/employee/name/text()
Многие другие примеры выражений в стандарте XPath можно также найти в книге Essential XML Дона Бокса (Don Box) и др. [Addison-Wesley 2001], а описание стандарта XPath — noадресуhttp: / /WWW. w3 c. org/TR/xpath.
Вернемся к примеру с использованием шаблона, который функционирует следующим образом. При обнаружении узла в XPath-наборе /staff/employeeнужно выполнить следующие действия:
создать строку <tr> ;
применять этот шаблон для дочерних узлов данного узла;
создать строку < /tr> после обработки всех дочерних узлов данного узла.
Иначе говоря, этот шаблон генерирует маркеры строк HTML-таблицы вокруг каждой записи о сотрудниках.
XSLT-процессор начинает обработку с проверки корневого элемента. При обнаружении узла, который соответствует заданному шаблону, этот шаблон немедленно применяется. (При совпадении нескольких шаблонов используется шаблон с наибольшей степенью соответствия. Более подробные сведения приводятся в спецификации XSLT.) Если соответствие с шаблоном не найдено, то XSLT-процессор выполняет заданное по умолчанию действие. Для текстовых узлов по умолчанию в выходной поток включается содержимое, а для элементов выходной поток не создается, но продолжается обработка дочерних узлов.
Вот как выглядит шаблон преобразования узлов name в файле с данными о сотрудниках.
<xsl:template match="/staff/employee/name"> ctdxxsl : apply-templates/x/td>
</xsl:template>
Как видите, шаблон создает разделители <td>...</td> и предлагает XSLT- процессору рекурсивно посетить дочерние узлы элемента name, который имеет только один дочерний текстовый узел. Когда процессор посещает узел, он возвращает его текстовое содержимое (при условии, конечно, что не задан никакой другой шаблон соответствия).
Для копирования значений атрибутов в выходной поток придется задать более сложный шаблон.
<xsl:template match="/staff/employee/hiredate">
<tdxxsl:value-of select="@year"/>-<xsl:value-of select="@month"/>-<xsl:value-of select="@day"/></td>
</xsl:template>
При обработке узла hiredate этот шаблон создаст выходной поток со следующими составными частями.
Строка <td>.
Значение атрибута year.
Дефис.
Значение атрибута month.
Дефис.
Значение атрибута day.
Дефис.
Строка <td>.
Выражение xsl:value-of вычисляет строковое значение набора узлов, которое указывается с помощью XPath-значения атрибута select. В этом случае путь является относительным по отношению к текущему обрабатываемому узлу. Набор узлов преобразуется в строку за счет конкатенации строковых значений всех узлов. Строковым значением узла-атрибута является его значение, строковым значением текстового уз-
ла — его содержание, а строковым значением узла-элемента — конкатенация строковых значений его дочерних узлов (но не его атрибутов).
В листинге 12.9 приведены шаблоны преобразования, предназначенные для преобразования XML-файла с записями о сотрудниках в HTML-таблицу.
В листинге 12.10 показан другой набор преобразований того же XML-файла в простой текст в обычном формате файла свойств.
employee.1.name=Car1 Cracker employee.1.salary=75000.0 employee.1.hiredate=1987-12-15 employee.2.name^Harry Hacker employee.2.salary=50000.0 employee.2.hiredate=1989-10-l employee.3.name=Tony Tester employee.3.salary=40000. 0 employee.3,hiredate=1990-3-15
В данном примере используется функция position() , которая получает информацию о расположении текущего узла по отношению к родительскому узлу. Для получения другого выходного потока данных достаточно только поменять страницу стилей. Таким образом, XML-документ можно спокойно использовать для описания собственных данных, не заботясь о том, какой формат данных используется в других приложениях, которые используют эти данные. Для генерации альтернативного формата достаточно применить XSLT-преобразования.
XSLT-преобразования довольно легко можно организовать на языке Java. Для этого нужно для каждой страницы стилей создать фабрику преобразований, затем получить объект-преобразование и передать ему источник преобразуемых данных.
File stylesheet = new File(filename);
StreamSource stylesource = new StreamSource(stylesheet) ;
Transformer t = TransformerFactory
.newlnstance().newTransformer(styleSource); t.transform(source, result);
Параметры объекта transform являются объектами классов, которые реализуют интерфейсы Source и Result. Интерфейс источника данных Source имеет следующие три реализации.
DOMSource SAXSource StreamSource
Источник данных StreamSource можно создать на основе файла, потока байтов, потока символов или URL-указателя, а источник данных DOMSource — на основе узла DOM-дерева. Например, в предыдущем разделе использовалось следующее идентичное преобразование.
t.transform(new DOMSource(doc) , result);
В данном примере используется немного другой подход. Вместо использования уже существующего XML-файла создается SAX-читатель XML-файла, который имитирует анализ XML-файла с генерацией соответствующих SAX-событий. Фактически, XML-читатель считывает плоский файл, как описано в главе 12 тома I. При этом входной файл имеет следующий вид.
Carl Cracker|75000.0|1987|12|15
Harry Hacker|50000.0 11989 |10 11
Tony Tester|40000.0|1990|3|15
По мере обработки входного потока XML-читатель генерирует SAX-события. Вот как выглядит часть метода parse класса EmployeeReader, который реализует интерфейс XMLReader.
String rootElement = "staff”;
Attributeslmpl attributes = new Attributeslmpl();
if (handler == null)
throw new SAXException("No content handler");
handler.startDocument() ;
handler.startElement("", rootElement, rootElement, atts);
String line;.
while ((line = in. readLine () ) ! = null) {
handler. startElement (11", "employee", "employee", atts) ;
StringTokenizer t = new StringTokenizer(line,"|") ;
handler.startElement(”", "name", "name", atts) ;
String s = t.nextToken();
handler.characters(s.toCharArray(), 0, s.length() ) ;
handler.endElement("" , "name", "name") ;
handler. endElement (”", " employee", 11 employee") ;
}
handler.endElement("", rootElement, rootElement) ;
handler.endDocument();’
Источник данных SAXSource для преобразования создается на основе XML- читателя.
t.transform(new SAXSource(new EmployeeReader(), new InputSource(new Fileinputstream(filename))), result);
Этот способ как нельзя лучше подходит для преобразования унаследованных данных He-XML-типа в данные XML-формата. Конечно, в большинстве XSLT- приложений уже используются входные данные XML-типа, а потому можно просто вызвать метод transformкласса StreamSource, как показано ниже.
t. transform (new Streamsource (file) , result);
Результатом такого преобразования будет объект одного из следующих трех классов, которые реализуют интерфейс Result.
DOMResult
SAXResult
StreamResult
Для сохранения результата в виде DOM-дерева нужно использовать пакет DocumentBuiIder для генерации нового документа и заключения его в класс- оболочку DOMResult.'
Document doc = builder. newDocument () ; t.transform(source, new DOMResult(doc));
Для сохранения выходного потока в виде файла следует использовать метод StreamResult.
t.transform(source, new StreamResult(file));
В листинге 12.8 приведен полный код этого примера, а в листингах 12.9 и 12.10 — две страницы стилей. Этот пример завершает обсуждение инструментов поддержки языка XML в библиотеке языка Java. Авторы надеются, что читатели получили представление об основных достоинствах языка XML, в частности, автоматическом анализе и проверке, а также о мощном механизме XSLT-преобразования. Конечно, эта технология будет эффективно работать только при правильном использовании XML- формата. Убедитесь в том, что форматы достаточно полны для выражения всех биз- нес-требований, стабильны и приемлемы для пользователей. Успешное решение более сложных вопросов имеет гораздо большее значение, чем создание анализаторов, DTD-определенийилипреобразований.
Листинг 12.8. TransformTest.java
/**
©version 1.00 2001-09-29
©author Cay Horstmann
*/
import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.sax.*;
import javax.xml.transform.stream.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
/**
Пример XSL-преобразований набора записей о сотрудниках* Эти записи хранятся в файле employee.dat и
преобразуются в таблицу HTML-формата или простой текст.
• Для этого в командной строке указывается страница стилей, например для получения таблицы в HTML-формате: java TransformTest makeprop.xsl
*/
public class TransformTest
{ public static void main(String[] args) throws Exception
{
String filename;
if (args.length> 0) filename = args[0];
else filename = "makehtml.xs1";
File stylesheet = new File(filename);
Streamsource styleSource = new Streamsource(stylesheet) ;
Transformer t =
.newlnstance().newTransformer(stylesource);
t.transform(new SAXSource(new EmployeeReader(), new Inputsource(new FileInputStream("employee.dat"))) , new StreamResult(System.out));
}
/**
Этот класс считывает плоский файл employee.dat и генерирует события SAX-анализатора, чтобы файл анализировался так же, как и XML-файл
*/
class EmployeeReader implements XMLReader {
public void parse(Inputsource source)
throws IOException, SAXException {
InputStream stream = source.getBytestream() ; 1
BufferedReader in = new BufferedReader(
new InputStreamReader(stream)) ;
String rootElement = "staff";
Attributeslmpl atts = new Attributeslmpl();
if (handler == null)
throw new SAXException(“No content handler");
handler.startDocument();
handler.startElement("", rootElement, rootElement, atts);
String line;
while ((line = in.readLine()) != null) {
handler.startElement("", "employee", "employee", atts);
StringTokenizer t = new StringTokenizer(line,"|") ;
handler.startElement("“, "name", "name", atts);
String s = t.nextToken() ;
handler.characters(s.toCharArray (), 0, s.length() ) ;
handler.endElement("", "name", "name");
handler.startElement("”, "salary", "salary", atts); s=t.nextToken() ,-
handler.characters(s.toCharArray(), 0, s.lengthO);
handler.endElement("", "salary", "salary");
atts.addAttribute("", "year", "year", "CDATA", t.nextToken() ) ;
atts.addAttribute("", "month", "month", "CDATA",
t.nextToken());
atts.addAttribute("", "day", "day", "CDATA", t.nextToken() ) ;
handler.startElement("", "hiredate", "hiredate", atts);
handler.endElement("", "hiredate", "hiredate");
atts.clear();
handler.endElement("", "employee", "employee"); )
handler.endElement("", rootElement, rootElement);
handler.endDocument();
}
public void setcontentHandler(ContentHandler aHandler) {
handler = aHandler;
}
public ContentHandler getContentHandler() {
return handler;
}
// Реализации "ничего не пеламидах" методов
public void parse(String systemic!)
throws IOException, SAXException {}
public void setErrorHandler(ErrorHandler handler) {} public ErrorHandler getErrorHandler() { return null; } public void setDTDHandler(DTDHandler handler) {}
public DTDHandler getDTDHandler() { return null; }
public void setEntityResolver(EntityResolver resolver) {} public EntityResolver getEntityResolver() { return null; } public void setProperty(String name, Object value) {} public Object getProperty(String name) { return null; } public void setFeature(String name, boolean value) {} public boolean getFeature(String name) { return false; }
private ContentHandler handler; }
Листинг 12.9. makehtml. xsl
<?xml version-"1.0" encoding="ISO-8859-l"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3c.org/1999/XSL/Transfоrm" version-"1.0">
<xsl:output method="html"/>
<xsl:template match="/staff">
<table border="1"xxsl:apply-templates/></table>
</xsl:template>
<xsl:template match=" /staff /employee">
<tr><xsl:apply-templates/></ tr>
</xsl:template>
<xsl:template match="/staff/employee/name">
<tdxxsl: apply-templates/x/td>
</xsl:template>
<xsl:template match=" / staff/employee/salary">
<td>$<xsl: apply-templates/x/td>
</xsl: template:»
<xsl:template match=" /staff/employee/hiredate”>
<td><xsl:value-of select="@year"/>-<xsl:value-of select="@month"/>-<xsl:value-of select="@day"/></td>
</xsl :template>
</xsl:stylesheet>
Листинг 12.10. makeprop, xel
<?xml version="1.0"?>
<xsl:stylesheet
xmlns : xsl= "http: / /ww. w3c. org/1999 /XSL/Transf orm" version=" 1.0" >
<xsl:output method="text"/>
<xsl:template match="/staff/employee">
employee.<xsl:value-of select="position()"/>.name=<xsl:value-of select="name/text()"/>
employee.<xsl:value-of select="position()" />.salary=<xsl:value-of select="salary/text() " />
employee.<xsl:value-of select="position()"/>.hiredate=<xsl:value-of select="hiredate/@year"/>-<xsl:value-of select="hiredate/@month"/>- <xsl:value-of select="hiredate/@day"/>
</xsl:template>
</xsl:stylesheet>
javax.xml.transform.TransformerFactory
Метод Transformer newTransformer (Source from) возвращает экземпляр класса Transformer, который считывает страницу стилей из
указанногоисточника.
j avax. xml. transform, stream. Streamsource
Конструктор Streamsource (File f) создает поток-источник входных данных на основании указанного файла.
Конструктор Streamsource(Inputstream in) создает поток-источник входных данных на основании указанного входного потока байтов.
Конструктор StreamSource (Reader in) создает поток-источник входных данных на основании указанного входного потока символов.
Конструктор StreamSource (StringsystemID)создает поток-
источник входных данных на основании указанного системного идентификатора (обычно, относительного или абсолютного URL-указателя).
j avax. xml. transform, sax. SAXSource
Конструктор SAXSource(XMLReader reader, InputSource source) создает SAX-источник данных на основании указанного входного потока
source и использует метод-читатель для анализа входного потока.
orgjanl. saxXMLReader
Метод void setContentHandler(ContentHandler handler) задает
обработчик, который уведомляется о возникающих событиях по мере
выполнения анализа.
Метод void parse (InputSource source) анализирует входной поток на основании заданного источника входных данных и передает возни-
1JW U'W
АР,!!
кающие события обработчику содержимого.
javax. xml. transform. dom.DOMRe suit
r/Z/Z <_/<££#
Конструктор DOMResult (Node n) создает источник данных на основе заданного узла документа.
org.xml. sax.helpers.Attributeslmpl
Метод void addAttribute(String uri, String Iname, String qname, String type, String value) создает атрибут в данной коллекции атрибутов.
Параметры:
uriURI-указатель пространства имен
lnameлокальное имя без псевдонима-префикса
qnameквалифицированное имя с псевдонимом-префиксом
typeодин из следующих типов: "CDATA", ""
"IDREFS", " NMTOKEN", " NMTOKENS", "ENTITY", "ENTITIES" или "NOTATION"
valueзначение атрибута
Метод void clear () удаляет все атрибуты из коллекции атрибутов.
Предметный указатель
Apache Tomcat, 249; 331API-интерфейс JavaMail, 212API-интерфейс запуска, 1018ВBase 64, 217; 218BDE, 264BEAWebLogic, 309BeanBox, 712BLOB-объекты, 271СCDATА-разделы, 1045 CGI-сценарии, 225CLOB-объекты, 271Cloudscape, 263CORBA, 313Cygwin, 981С-связывание, 977DDOM-анализатор, 1045; 1080DTD-определения, 1058EEnterpriseJavaBeans, 256FForte Community Edition, 711 FTP-файл,217HApache Tomcat, 249; 331API-интерфейс JavaMail, 212API-интерфейс запуска, 1018ВBase 64, 217; 218BDE, 264BEAWebLogic, 309BeanBox, 712BLOB-объекты, 271СCDATА-разделы, 1045 CGI-сценарии, 225CLOB-объекты, 271Cloudscape, 263CORBA, 313Cygwin, 981С-связывание, 977DDOM-анализатор, 1045; 1080DTD-определения, 1058EEnterpriseJavaBeans, 256FForte Community Edition, 711 FTP-файл,217H
Apache Tomcat, 249; 331
API-интерфейс JavaMail, 212
API-интерфейс запуска, 1018
В
Base 64, 217; 218
BDE, 264
BEAWebLogic, 309
BeanBox, 712
BLOB-объекты, 271
С
CDATА-разделы, 1045 CGI-сценарии, 225
CLOB-объекты, 271
Cloudscape, 263
CORBA, 313
Cygwin, 981
С-связывание, 977
D
DOM-анализатор, 1045; 1080
DTD-определения, 1058
E
EnterpriseJavaBeans, 256
F
Forte Community Edition, 711 FTP-файл,217
H
Apache Tomcat, 249; 331
API-интерфейс JavaMail, 212
API-интерфейс запуска, 1018
В
Base 64, 217; 218
BDE, 264
BEAWebLogic, 309
BeanBox, 712
BLOB-объекты, 271
С
CDATА-разделы, 1045 CGI-сценарии, 225
CLOB-объекты, 271
Cloudscape, 263
CORBA, 313
Cygwin, 981
С-связывание, 977
D
DOM-анализатор, 1045; 1080
DTD-определения, 1058
E
EnterpriseJavaBeans, 256
F
Forte Community Edition, 711 FTP-файл,217
H
I
IBM WebSphere, 309
IDL, 314; 364
ПС-профиль, 604
HOP, 313
InstantDB, 263
IOR-ссылка, 373
J
Java API for XML Processing (JAXP), 1039
JavaBeans, 447
JavaMail, 212
JBuilder, 252
JDBC, 252
JDBC-интерфейс поставщика услуг, 253
Jet, 264
JNI-интерфейс, 977
JSP-страница, 1094
M
MIME, 590
МШЕ-тип, 673
N
Netscape, 250
n-уровневая модель, 256
О
Opera, 250; 502
ORB-брокер, 363
P
Pointbase, 263
HTTP-запрос, 208
Q
QBE, 260
R
RFC 821, 203
RMI, 312; 314
RMI-реестр, 332
s
SAX-анализатор, 1045; 1080 sendmail, 202
SHA1, 855
SMTP-протокол, 202
SPI, 253
SQL, 251
SQL 92, 255
Standard Generilized Markup Language (SGML), 1041
Standard Template Library (STL), 116
T
TCP (Transmission Control Protocol), 210 telnet.exe, 192
Thawte, 875
u
UDP (User Datagram Protocol), 210
Unicode, 906; 949
URI, 213
URL, 213
URL-кодирование, 226
URN, 213
V
Verisign, 872
Visual Cafe, 252
w
Web-броузер, 193
X
XHTML, 1042
XPath, 1095
XSL-преобразование, 1094
A
Абстрактные классы, 420
Активные запросы, 261
Алгоритм,187
Алгоритм обработки макета, 629
Алгоритм поиска наибольшего, 169
Алгоритм рисования, 525
Алгоритм сортировки, 170
Альфа-канал, 554
Анализатор, 1045
Анализатор операций перетаскивания,
694
Анимационная последовательность, 86
Анимация, 86
Аплет, 191
Аппаратные координаты, 560
Архитектура, 155
Атомарная инструкция, 56
Атрибут типа
ENTITY, 1062
ID,1062
IDREF, 1062
NMTOKEN, 1062
Атрибуты, 1044
документа, 651
задания печати, 651; 652
службы печати, 652
Атрибуты печати, 655
Аутентификация, 862
Аутентификация пользователя класса, 809
Аффинное преобразование, 562
Б
Безопасность, 803
Безопасность аплетов, 238
Бесконечное дерево, 435
Библиотека стандартных шаблонов, 116
Бизнес-логика, 256
Бинарный поиск, 173
Битовый массив (вектор), 185
Блокированные потоки, 34
Блокировка объектов, 60
Большие объекты, 271
Брандмауэр, 239
Буфер обмена, 660
Быстрая сортировка, 171
В
Вектор, 395
Верификатор, 813
Взаимная блокировка, 63; 69
Вид компонента-имени, 372
Визуальное представление, 256
Визуальный редактор свойств, 757
Виртуальная машина, 804
Виртуальная машинаJava, 60; 1018
Внутренний фрейм, 493
Время простоя сокета, 207
Выбор строки, 442
Выбор языка, 906
Выделенный фрейм, 504
Вызов удаленных методов, 312
Вытеснение, 35
Вытесняющая многозадачность, 15
Выход из блокированного состояния, 36
Выходной поток, 216
Г
Главный поток, 80
Градиентная заливка, 553
д
Дайджест сообщения, 855
Двенадцать возможных правил
композиции, 574
Дейтаграмма, 210
Декапитализация, 720
Дерево, 140
Дескрипторы активизации, 357
Диапазон, 162
Диспетчер безопасности, 804; 819
Диспетчер редакторов свойств, 747
Длина массива, 1008
Домен обеспечения безопасности, 823
Дочерние элементы, 1043
Дочерний узел, 402
Драйвер
типа 1,254
типа 2, 255
типа 3, 255
типа4,255
Древовидное множество, 140
Дублирование данных, 260
3
Заглушка, 315
Заголовочная информация, 215
Загрузчик классов, 804
Задача аутентификации, 869
Закрытый ключ, 862
Запись, 257
Заполнитель, 942
Запущенные потоки, 33
Звено, 124
Зеленый поток, 33
И
Идентификатор активизации, 357
Идентификатор компонента-имени, 372
Избирательность сортировки, 928
Изменение размеров столбца, 442
Изменения выбора, 428
Изменяемый список, 171
Имя, 257
Индексированное свойство, 723
Индикатор неопределенного состояния,
97
Индикатор текущего состояния, 95
Инспектор свойств, 716
Инструкции обработки, 1045
Инструменты создания запросов по
образцу, 260
Интегрированная среда разработки, 711
Интерполяция, 610
Интерфейс
Attribute, 652
BoundedQueue, 118
Collection, 119; 122
Comparable, 141
DocAttributeSet, 653
DragGestureListener, 694
DropTargetListener, 685
Enumeration, 178
Iterator, 119
ListCcllRenderer, 397
ListModel, 395
Printable, 619
PrintRequestAttributeSet, 619
Queue, 118
Runnable, 21; 29
TableCelllnterface, 463
Transferable, 665; 667
TreeModel, 433; 1050
TrceNode, 411
TreeSelectionListener, 427
Интерфейс JDBC, 252
Интерфейс доступа к реестру, 1023
Интерфейс очереди, 116
Интерфейс поставщика услуг, 590
Интерфейсный тип, 118
Интерфейсы, 116
Интроспекция, 788
Информационный класс компонента, 720
Информация о системе, 179
Информация о форматировании, 947
Исключительная ситауция
IllegalStateException, 592
Исключительная ситуация, 17
ConcunentModificationException,
128
IndexOutOfBoundsException, 592
InterruptedException, 22 IntcrruptedlOException, 207
InvalidKeyException, 868
NoSuchElementException, 132
ParseException, 913
PropertyVetoException, 508; 732
RemoteException, 319
Security Exception, 823
SQLException, 273
UnknownHostException, 195
UnsupportedFlavorException, 665
UnsupportedOperationException,
148
Источник кода, 821
Источник перетаскивания, 683
Итератор, 119
Итератор разрывов, 935
К
Карта, 146
Каскадное расположение фреймов, 505
Каталог, 304
Квалифицированное имя, 1079
Квантование времени выполнения, 34
Класс
AbstractClassEditor, 467
Abstract Collection , 122
AbstractListModel, 392
AbstractTableModel, 444
AllPermission, 825
Area, 543
Arrays, 162
BasicPermission, 829
BitSet, 185
Bufferedlmage, 554
BufferedRcader, 196
CachingResultSetTableModel, 449
ChangeListener, 499
ChoiceFormat, 945
ClassLoader, 807
Clipboard, 660
Collections, 171
Color, 553
Connection, 288
DataFlavor, 660
DateFormat, 919
DeafultListModel, 396
DefaultCellEditor, 464
DefaultMutableTreeNode, 404; 409
DefaultTreeCellRenderer, 419; 421
DefaultTreeModel, 411
DocumentBuilderFactory, 1046
DOMTreeModel, 1050
DriverManager, 265
DropTargetDropEvent, 695
FeatureDescriptor, 771
FilelnputStream, 825
GeneralPath, 532
GradientPaint, 553
Graphics, 524; 526
Graphics2D, 524; 545
HashSet, 135
Hashtable, 177
HyperlinkEvent, 489
HyperlinkListener, 488
IdentityHashMap, 154
ImagelO, 589
InetAddress, 208
InputStream, 196
JComponent, 81
JDesktopPane, 505
JEditorPane, 486
JInternalFrame, 505
JLabel, 399
LinkedHashMap, 152
LinkedHashSet, 136
LinkedList, 125
ListResourceBundle, 954
MarshalledObject, 357
MessageFormat, 941
MulticastRemoteObjects, 321
NumberFormat, 919
OutputStream, 196
Policy, 826
PrintWriter, 196
Productimp], 320
PropertyEditorSupport, 754
RemoteServer, 320
ResourceBundle, 951
ResultSet, 287
ResultSetMetaData, 297
ResultSetTableModel, 449
ScrollingResultSetTableModel, 449
SecurityManager, 825
SimulatedActivity, 95
Socket, 195
SortFilterModel, 456
Stack, 185
StreamPrintServiceFactory, 646
StringSelection, 660
TableColumn, 477
TexturePaint, 553
ThreadedEchoHandler, 199
TimerBean, 742
TimerEvent, 742
TimerListener, 742
UnicastRemoteObject, 320
URI, 213
URL, 212; 213
URLConnection, 212; 214; 232
WeakHashMap, 152
Класса
Thread, 199
Класс-каркас, 331
Класс-настройщик, 780
Класс-рисовальщикячеек списка, 397
Класс-хранитель, 366
Классы передачи данных, 661
Классы-конвейеры, 109
Классы-слушатели запрещаемых
изменений, 509
Клиент/сервер, 256
Клиентский объект, 315
Клонирование удаленного объекта, 351
Ключевое слово
synchronized, 59
Ключи карты, 147
Кодировка Unicode, 949
Команда
CREATE TABLE, 262
INSERT, 262
Комментарии, 1045
Компонент, 709
FileNameBean, 712
ImageViewerBean, 711; 712
JList, 386
JTable, 440
JTree, 403
Компонент JavaBeans, 705
Компонент-имя, 371
Компоненты JavaBeans, 447
Компоненты Swing, 700
Конвейерная модель, 602
Конвейеры, 109
Конструктор
new Date(), 92
Контейнер сервлетов, 249
Контекст, 16
Контекст рисования шрифтов, 570
Контрольная точка, 531
Кооперативная (невытесняющая)
многозадачность, 15
Корневой сертификат, 876
Корневой рел, 402
Корневой элемент, 1043
Косое соединение, 546
Коэффициент загруженности, 135
Кривые второго (третьего) порядка, 531
Криптография с открытым ключом, 862
Критические секции, 58
Круговой массив, 117
Курсор, 290
Л
Лексикографический порядок, 141
Лес, 402
Лист, 402
Локальное имя, 1079
Локальный адрес обратной связи, 198
м
Массив, 1007
предельных значений, 945
строк подстановки, 945
Массив объектов, 168; 388
Массивы, 124
Массивы-списки, 124
Менеджер обеспечения безопасности, 318
Место расположения файлов политики, 888
Метаданные, 296
Метаданные результатов выполнения
запроса, 448
Метод
acceptDrop, 695
actionPerformed, 92
activeCount, 37
add, 405; 541
addBatch, 306
addMouseListener, 456
addPropertyChangeListener, 780
addTab, 498
append, 532
binarySearch, 173
breadthFirstEnumeration, 418
cancelCellEditing, 468
cancelRowUpdates, 292
checkExit, 820
checkPermission, 825; 834
ClassNameTreeCellRenderer, 422
clip, 570
clone, 351
closePath, 532
commit, 306
compareTo, 142
containsBadWords, 841
deleteRow, 293
depthFirstEnumeration, 418
dragEnter, 685
dragExit, 685
dragGestureRecognized, 694
dragOver, 685
draw, 545
dropActionChanged, 685
editComplete, 733
equals, 139; 349
exclusiveOr, 542
executeBatch, 307
executeQuery, 270
executeUpdate, 269
exitinternal, 820
FindClass, 1000
GET, 226; 357
getAsText, 754; 768
getAttributes, 1049
getAutoCommit, 305
getAvailableLocales, 919
getBundle, 955
getByName, 209
getCellEditorValue, 469
getCertificate, 878
getChild, 433
getChildCount, 434
getChildNodes, 1047
getClassContext, 841
getClob, 271
getCollationKey, 929
getColumnCount, 445
getContents, 661
getCustomEditor, 781
getData, 1048
getDataElements, 604
getDefault, 910
getDescription, 320
getDisplayName, 914
getDocumentElement, 1047
getEventType, 489
getFieldDescription, 428
getFirstChild, 1048
getFontRenderContext, 570
getHeaderFieldKey, 217
getHeaderFields, 217
getHeight, 621
getlcon, 770
getlmageableHeight, 622
getlmageableWidth, 622
getlmageableX, 622
getlmageableY, 622
getlmageReaderByMIMEType, 590
getlmageReaderBySuffix, 590
getlnputStream, 215
getlnstance, 68; 865; 891; 928
getLastChild, 1049
getLocalName, 1079
getNamespaceURI, 1079
getNewValue, 510
getNextSibling, 1049
getNodeName, 1079
GetObjectClass, 1000
getParent, 411
getPathToRoot, 413
getPixel, 603
getPointCount, 533
getPolicy, 823
getPrintService, 646
getRGB, 604
getRoot, 434; 1050
getRowCount, 445
getSelectedColumns, 479
getSelectedlndex, 499
getSelectedRows, 479
getSelectedValues, 388
getService, 794
getStatement, 288
GetStringRegion, 987
GetStringUTFRegion, 987
getSubjectDN, 879
getTableCellEditorComponent, 467
getTableCellRendererComponent,
463
getTagNaine, 1047; 1079
getTags, 768
getTBSCertificate, 878
getThumbnail, 592
getTransferable, 687
getTreeCellRendererComponent, 421
getURL, 489
getValue, 1025
getValueAt, 444; 457
getWidth, 621
hashCode, 349
HashSet, 137
hasMoreElements, 178
hasNext, 119
hasService, 794
hyperlinkUpdate, 488
ink,82
insertRow, 293
interrupt, 31; 72
intersect, 541
isCellEditable, 468
isLeaf, 409
isSelected, 508
iterator, 119
JProgressBar(), 107
lineTo, 532
LinkedHashSet, 138
LinkedList, 132
loadCIass, 807
main, 80
moveTo, 532
moveToInsertRow, 292
newDocument, 1085
next, 119
nextElement, 178
next Index, 129
notify, 63
notifyAll, 63
openStream, 212
parse, 913
PipedlnputStream, 113
PipedOutputStream, 113
POST, 227
previouslndex, 129
print, 619
printDialog, 619; 620
ProgressMonitor, 108
ProgressMonitorlnputSream, 109
raiseSalary, 994
readLine, 195
rejectDrag, 689
remove, 119
removeColumn, 479
lemoveNodeFromParent, 412
removePropertyChangeListener, 780
repaint, 81
revalidate, 81
rollback, 306
run,21
setAllowUserlnteraction, 216 setAsksAllowsChildren, 409 setAsText, 754; 768 setAttribute, 1086 setBackground, 701 setBeanContext, 792 setCellRenderer, 398 setClosed, 509 setComposite, 525 setDaemon, 37
setDataElements, 604
setDoInput, 216 setDoOutput, 216 setEditable, 413 setlfModifiedSince, 216
set Object, 780
setPaint, 525
setPriority, 39
setProgress, 100
setRenderingHints, 524; 584
set Req uest Property, 216 setRowHeight, 466 setSecurityManager, 820 setSelectedlndex, 498 setSelectionMode, 387; 478
setSoTimeout, 207
setStroke, 524
setTransform, 525; 563
setUseCaches, 216
setUserObject, 405
setValue, 1026
shouldSelectCell, 468 sleep(), 34
sort, 170
start, 23
stateChanged, 499
stop, 36; 71
stopCellEditing, 468
subtract, 541
supportCustomEditor, 758
suspend,71
suspend(), 34
Thread(), 27
Thread.sleep, 17 toLowerCase, 911
toUpperCase, 911
transfer, 59
translate, 631
TreeSet, 141
uncaughtException, 38
updateRow, 293
valueChanged, 427
vetoableChange, 732
void run(), 27
wait, 62
wait(), 34
yield, 40
Метод половинного закрытия, 208
Методы
getBlob, 271
Механизм активизации, 357
Механизм вызова удаленных процедур, 314
Механизм отражения, 720
Механизм последовательного рисования, 601
Механизм предварительного просмотра, 631
Механизм рисования фигур, 524
Механизм синхронизации, 66
Минимальный интерфейс, 169
Многодокументный интерфейс, 502
Многозадачность, 15
Множество, 126
Множество Мандельброта, 605
Множество свойств, 179
Модель
дерева, 403
иерархических данных, 403
таблицы, 441
Модель, 602
Модель выбора, 478
Модель-просмотр-управление, 403
Модель-фильтр, 455
Модуль, 369
Мозаичная текстура, 554
Мозаичное расположение фреймов, 505
Монитор текущего состояния, 99
Мониторы, 58
Монопольный поток, 46
MoctJDBC/ODBC, 254
н
Набор строк, 291
Надежный генератор случайных чисел,
864
Настройщик, 778
Начальный угол, 529
Невизуальный компонент, 743
Нестрогие ссылки, 152
О
Области, 541
Обнаружение краев, 612
Оболочка, 160
Обработка нескольких потоков, 28
Обработчики событий, 80
Обход
в глубину, 418
в обратном порядке, 418
по ширине, 418
Объект
DocumentBuilder, 1046
DropTarget, 684
сериализации,196
Объект класса, 68
Объект-заглушка, 317; 350
Объект-итератор, 119
Объект-ключ сравнения, 929
Объект-компаратор, 143
Объект-нумератор, 418
Объект-поток, 29
Объявление типа документа, 1042
Обычный приоритет, 39
Овальное перекрытие, 546
Ограничения диапазона, 731
Ограниченная коллекция, 118
Одноэлементный класс, 68
Окно редактора ячейки, 413
Оператор
catch, 31
new, 33
Операция перетаскивания, 683
Определение типа документа, 1056
Определение цвета пикселя, 604
Основные правила шифрования, 872
Остановленный поток, 36
Открытие сокета, 195
Открытый ключ, 862
Отправка электронной почты, 202
Отрицательное смещение, 86
Очередь, 62
п
Пакет
java.net, 195
java.rmi, 319
java.rmi.server, 320
Пакет ресурсов, 927
Пакетное обновление, 306
Пакетные операции, 167
Панель
рабочего стола, 493
с вкладками, 493
с границей раздела, 493
Пара ключей, 863
Параметры рисования, 524; 582
Паузы, 76
Первичный загрузчик классов, 805
Перегрузка встраиваемых методов, 979
Передача удаленного объекта, 334
Переменная окружения
CLASSPATH, 265
Переполнение хэш-таблицы, 135
Перерисовывка экрана, 88
Песочница, 820
Печать двухмерной графики, 619
Печать нескольких страниц, 629
Пиксель источника, 574
Пиксель места назначения, 574
Планировщик потоков, 61
Платформозависимое диалоговое окно печати, 624
Плоский файл, 1040
Позиция строки, 412
Поле, 257
Политики безопасности, 238
Полоса прокрутки вкладок, 498
Полосовой принцип печати, 621
Пользовательские координаты, 560
Пользовательский диспетчер
безопасности, 840
Пользовательский слушатель, 741
Поля со списками, 385
Порядок вставки, 153
Порядок доступа, 153
Порядок прав доступа, 833
Порядок сортировки, 143
Построение дуги, 529
Поток
расчет движения шара, 22
таймера, 37
управления событиями, 22
Поток (поток управления), 16
Поток с более высоким приоритетом, 58
Поток управления событиями, 73; 80
Потоки в Swing, 79
Поток-источник, 109
Поток-обработчик, 109
Потоковый демон, 37
Потоковый фильтр, 103
Правила композиции, 524; 525
Правила композиции Портера-Дафа, 575
Правило единственного потока, 81
Право доступа, 821
Предварительно созданные потоки, 30
Представление, 160
Представления карты, 147
Преобразование абсолютных адресов в
относительные, 214
Преобразование координат, 525; 560
Прерывание процесса загрузки, 18
Прикладной загрузчик классов, 805
Принудительное прекращение работы
потока, 30
Принципал, 850; 879
Приоритет, 39
Программа-сервер, 199
Прозрачность, 573
Произвольный класс, 719
Прокрутка результатов выполнения
запроса, 289
Прокси-сервер, 242
Простое свойство, 722
Пространства имен, 1077
Протокол IPv6, 208
Процесс, 16
Процесс-слушатель, 193
Прямоугольник привязки, 554
Прямоугольное перекрытие, 546
Псевдоним, 873
Пул потоков, 30
Пул соединений, 308
Путь к классам, 332
Путь обхода дерева, 411
Р
Равноправный подход к обработке потоков, 40
Разделение времени выполнения, 47
Разрешение URI-идентификатора, 214
Разрешение класса, 804
Раскраска, 553
Расположение кода, 821
Расширенный загрузчик классов, 805
Реализации, 116
Региональный стандарт, 907
Редактор реестра, 1022
Редактор свойств, 747
Реестр Windows, 1021
Режим декомпозиции, 929
Режимы изменения размеров столбца, 477
Режимы свертки, 892
Результат запроса, 258
Ресурс, 951
Решето Эратосфена, 187
Рисование фигур, 526
Родитель, 402
С
Самоподписанный сертификат, 876
Сборка мусора, 16
Свертка, 611
Свертывание параметров, 315
Связанная хэш-карта, 152
Связанное свойство, 724
Связанный список, 117; 124; 128
Связывание языка программирования
Java, 366
Сглаживание, 582
Сглаживание фигур, 584
Сглаживание шрифтов, 584
Семафор, 58
Сервер приложений, 256; 313
Серверный объект, 315
Сервис именования CORBA, 364
Сервис отслеживания сообщений, 791
Сервлет, 191; 242
Сертификат, 821; 875
Сертификат разработчика
программного обеспечения, 889
Сигнатура метода, 997
Симметричный алгоритм шифрования
898
Синтаксический анализатор, 1042
Синхронизация доступа, 52
Синхронизированный блок, 67
Системный загрузчик классов, 805
Скругленное соединение, 546
Служба печати, 639
Служба регистрации самозагрузки, 322
Слушатель данных списка, 391
Слушатель фокуса ввода, 733
Соглашение об именах, 721
Соединение, 193
Соединение таблиц, 260
Создание новой таблицы, 262
Созданные потоки, 33
Сокет, 195
Сортированная коллекция, 140
Сортировка, 927
по убыванию, 171
Сортировка записей, 455
Сортировка слиянием, 171
Состояния потока, 33
Список,ПОЗВОЛЯЮЩИЙизменятьразмер,
171
Сплошной ЦИКЛ, 46
Ссылка
на символы, 1044
насущности, 1044
Ссылка на объект, 673
Стек, 185
Страница свойств, 706
Страница стилей, 1095
Строгий порядок сортировки, 143
Строка, 257
Строка вставки, 292
Строка-шаблон, 942
Структура красно-черного дерева, 140
Структура таблиц, 296
Стык, 546
Субъект, 850
Счетчик блокировок, 61
Счетчик ячеек, 134
т
Таймеры,91
Тайный канал, 241
Текущий поток, 33
Технология CORBA, 363
Технология перетащить и отпустить, 683
Тип данных
int, 983
Тип документа, 640
Транзакция
автоматическая фиксация, 305
откат, 305
фиксация, 305
У
Угол развертки дуги, 529
Удаление элемента из связанного списка, 124
Удаленный интерфейс, 347
Удержание результатов выполнения
запроса, 288
Узел, 402
дочерний, 402
корневой, 402
Унифицированное имя информационного ресурса, 213
Унифицированный идентификатор
информационного ресурса, 213
Унифицированный указатель
информационного ресурса, 212
Упорядоченная коллекция, 125; 156
Ф
Фаза пунктира, 547
Файл описания, 712
Файл политики, 328; 820; 826
Файл прав доступа, 821
Файл свойств, 1040
database, properties, 265
Фасетное соединение, 546
Фигура
RoundRectangle2D, 529
Фигура усечения, 569
Фигуры, 526
Фильтр ProgressMonitorlnputStream, 103
Фильтр размытия, 612
Фильтрация изображений, 610
Фокус редактирования, 464
Формат Х.509, 872
Форматирование даты и времени, 919
Форматирование сообщений, 941
Функциональные объекты, 143
Функция
ExceptionCheck, 1013
FindClass, 992
GetArrayLength, 1008
GetObjectClass, 992
GetStringUTFLength, 987
JNI_CreateJavaVM, 1018
NewObject, 1001NewStringUTF, 986printf, 984sprint, 989Throw, 1012NewObject, 1001NewStringUTF, 986printf, 984sprint, 989Throw, 1012
NewObject, 1001
NewStringUTF, 986
printf, 984
sprint, 989
Throw, 1012
NewObject, 1001
NewStringUTF, 986
printf, 984
sprint, 989
Throw, 1012
ш
Шаблон пунктира, 547
Шаблонный метод, 912
Шифрование, 862; 891
Штрих, 545
X
Хранилище ключей, 873; 875
Хэш-карта с идентичным хэшированием, 154
Хэш-карты с нестрогим хэшированием, 152
Хэш-код, 134
Хэш-конфликт, 134
Хэш-множество, 349
Хэш-таблица, 134
ц
Цветовая модель, 603
Целостность базы данных, 305
Цепочка доверия, 870
Цифровая подпись, 862
Э
Элемент
name, 1048
size, 1048
Элемент управления, 706
Эффект наложения, 574
Эхо-ответ, 193
Я
Язык описания интерфейсов, 314; 364
Язык структурированных запросов, 251
Ячейка, 134
Научно-популярное издание
Кей С. Хорстманн, Гари Корнелл
Библиотека профессионала. Java 2
Том 2. Тонкости программирования
Литературный редактор Т. Т. ШматкоВерстка А.А. Линник (мл.)
Художественный редактор С.А. ЧернокозинскийКорректор З.В. Александрова
Издательский дом “Вильямс”.
101509, Москва, ул. Лесная, д. 43, стр. 1.
Изд.лиц.ЛР№090230от23.06.99Госкомитета РФ по печати.
Подписано в печать 22.08.2002. Формат70x100/16.
Гарнитура Times. Печать офсетная.
Усл. печ. л. 90,3. Уч.-изд. л. 53,35.
Тираж 3000 экз. Заказ № 1200.
Отпечатано с диапозитивов в ФГУП "Печатный двор"Министерства РФ по делам печати,телерадиовещания и средств массовых коммуникаций.
197110, Санкт-Петербург, Чкаловский пр., 15.