Реферат: Общие представления о языке Java 5
Название: Общие представления о языке Java 5 Раздел: Остальные рефераты Тип: реферат | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Содержание Введение.......................................................................................................................................................................................... 6 Глава 1. Общие представления о языке Java........................................................................................................................ 7 1.1. Java и другие языки программирования. Системное и прикладное программирование............................ 7 1.2. Виртуальная Java-машина, байт-код, JIT-компиляция. Категории программ, написанных на языке Java 11 1.3.Алфавит языка Java. Десятичные и шестнадцатеричные цифры и целые числа. Зарезервированные слова 16 Алфавит языка Java......................................................................................................................................................... 16 Десятичные и шестнадцатеричные цифры и целые числа................................................................................... 16 Зарезервированные слова языка Java........................................................................................................................ 18 1.4. Управляющие последовательности. Символы Unicode. Специальные символы........................................ 18 Управляющие последовательности............................................................................................................................ 18 Простые специальные символы................................................................................................................................... 19 Составные специальные символы............................................................................................................................... 20 1.5.Идентификаторы. Переменные и типы. Примитивные и ссылочные типы..................................................... 21 Краткие итоги по главе 1..................................................................................................................................................... 24 Задания..................................................................................................................................................................................... 25 Глава 2. Объектно-ориентированное проектирование и платформа NetBeans....................................................... 26 2.1.Процедурное и объектно-ориентированное программирование. Инкапсуляция........................................ 26 2.2. Работа со ссылочными переменными. Сборка мусора...................................................................................... 29 2.3. Проекты NetBeans. Пакеты. Уровни видимости классов. Импорт классов................................................... 33 2.4. Базовые пакеты и классы Java................................................................................................................................... 35 2.5. Создание в NetBeans простейшего приложения Java......................................................................................... 38 2.6. Компиляция файлов проекта и запуск приложения............................................................................................. 42 2.7. Структура проекта NetBeans..................................................................................................................................... 43 2.8. Создание в NetBeans приложения Java с графическим интерфейсом............................................................ 46 2.9. Редактор экранных форм............................................................................................................................................. 49 2.10. Внешний вид приложения......................................................................................................................................... 53 2.11. Ведение проектов........................................................................................................................................................ 54 2.11. Редактирование меню экранной формы............................................................................................................... 56 2.12. Создание нового класса............................................................................................................................................ 58 2.13. Документирование исходного кода в Java.......................................................................................................... 61 2.14. Основные компоненты пакетов swing и awt......................................................................................................... 65 2.15. Технологии Java и .Net............................................................................................................................................... 70 Краткие итоги по главе 2..................................................................................................................................................... 72 Задания..................................................................................................................................................................................... 73 Глава 3. Примитивные типы данных и операторы для работы с ними...................................................................... 74 3.1.Булевский (логический) тип......................................................................................................................................... 74 3.2.Целые типы, переменные, константы........................................................................................................................ 75 3.3.Основные операторы для работы с целочисленными величинами................................................................. 77 3.4.Вещественные типы и класс Math............................................................................................................................. 78 3.5.Правила явного и автоматического преобразования типа при работе с числовыми величинами......... 81 3.6. Оболочечные классы. Упаковка (boxing) и распаковка (unboxing)................................................................. 83 3.7.Приоритет операторов.................................................................................................................................................. 84 3.8.Типы-перечисления (enum).......................................................................................................................................... 85 Краткие итоги по главе 3..................................................................................................................................................... 88 Задания..................................................................................................................................................................................... 89 Глава 4. Работа с числами в языке Java............................................................................................................................... 91 4.1 Двоичное представление целых чисел..................................................................................................................... 91 Позиционные и непозиционные системы счисления.............................................................................................. 91 Двоичное представление положительных целых чисел....................................................................................... 92 Двоичное представление отрицательных целых чисел. Дополнительный код............................................. 93 Проблемы целочисленной машинной арифметики................................................................................................ 94 Шестнадцатеричное представление целых чисел и перевод из одной системы счисления в другую.... 95 4.2. Побитовые маски и сдвиги.......................................................................................................................................... 97 4.3. Двоичное представление вещественных чисел.................................................................................................. 100 Двоичные дроби............................................................................................................................................................. 100 Мантисса и порядок числа......................................................................................................................................... 100 Стандарт IEEE 754 представления чисел в формате с плавающей точкой*............................................... 102 Краткие итоги по главе 4.................................................................................................................................................. 106 Задания.................................................................................................................................................................................. 106 Глава 5. Управляющие конструкции.................................................................................................................................. 108 Составной оператор..................................................................................................................................................... 108 Условный оператор if................................................................................................................................................ 108 Оператор выбора switch.............................................................................................................................................. 112 Условное выражение …?... : ….................................................................................................................................. 112 Операторы инкремента ++ и декремента --............................................................................................................ 113 Оператор цикла for..................................................................................................................................................... 113 Оператор цикла while – цикл с предусловием.................................................................................................. 117 Оператор цикла do...while – цикл с постусловием...................................................................................... 118 Операторы прерывания continue, break, return, System.exit............................................................................... 119 Краткие итоги по главе 5.................................................................................................................................................. 122 Задания.................................................................................................................................................................................. 122 Глава 6. Начальные сведения об объектном программировании.............................................................................. 123 Наследование и полиморфизм. UML-диаграммы...................................................................................................... 123 Функции. Модификаторы. Передача примитивных типов в функции................................................................. 129 Локальные и глобальные переменные. Модификаторы доступа и правила видимости. Ссылка this........ 132 Передача ссылочных типов в функции. Проблема изменения ссылки внутри подпрограммы.................... 134 Наследование. Суперклассы и подклассы. Переопределение методов.............................................................. 139 Наследование и правила видимости. Зарезервированное слово super............................................................... 144 Статическое и динамическое связывание методов. Полиморфизм....................................................................... 146 Базовый класс Object......................................................................................................................................................... 147 Конструкторы. Зарезервированные слова super и this. Блоки инициализации................................................. 149 Удаление неиспользуемых объектов и метод finalize. Проблема деструкторов для сложно устроенных объектов 152 Перегрузка методов............................................................................................................................................................ 152 Правила совместимости ссылочных типов как основа использования полиморфного кода. Приведение и проверка типов.................................................................................................................................................................................................. 155 Рефакторинг.......................................................................................................................................................................... 157 Reverse engineering – построение UML-диаграмм по разработанным классам................................................ 160 Краткие итоги по главе 6.................................................................................................................................................. 166 Задания.................................................................................................................................................................................. 167 Глава 7. Важнейшие объектные типы................................................................................................................................ 169 Массивы................................................................................................................................................................................. 169 Коллекции, списки, итераторы........................................................................................................................................ 173 Работа со строками в Java. Строки как объекты. Классы String, StringBuffer и StringBuilder....................... 176 Работа с графикой............................................................................................................................................................... 180 Исключительные ситуации.............................................................................................................................................. 183 Обработка исключительных ситуаций................................................................................................................... 183 Иерархия исключительных ситуаций...................................................................................................................... 185 Объявление типа исключительной ситуации и оператор throw....................................................................... 186 Объявление метода, который может возбуждать исключительную ситуацию. Зарезервированное слово throws............................................................................................................................................................................................. 187 Работа с файлами и папками........................................................................................................................................... 188 Работа с файлами и папками с помощью объектов типа File............................................................................ 188 Выбор файлов и папок с помощью файлового диалога..................................................................................... 192 Работа с потоками ввода-вывода.............................................................................................................................. 195 Краткие итоги по главе 7.................................................................................................................................................. 202 Задания.................................................................................................................................................................................. 203 Глава 8. Наследование: проблемы и альтернативы. Интерфейсы. Композиция................................................... 204 Проблемы множественного наследования классов. Интерфейсы......................................................................... 204 Отличия интерфейсов от классов. Проблемы наследования интерфейсов........................................................ 206 Пример на использование интерфейсов....................................................................................................................... 208 Композиция как альтернатива множественному наследованию.......................................................................... 210 Краткие итоги по главе 8.................................................................................................................................................. 212 Задания.................................................................................................................................................................................. 212 Глава 9. Дополнительные элементы объектного программирования на языке Java........................................... 214 Потоки выполнения (threads) и синхронизация.......................................................................................................... 214 Преимущества и проблемы при работе с потоками выполнения.................................................................... 214 Синхронизация по ресурсам и событиям............................................................................................................... 215 Класс Thread и интерфейс Runnable. Создание и запуск потока выполнения............................................ 217 Поля и методы, заданные в классе Thread.............................................................................................................. 219 Подключение внешних библиотек DLL.“Родные” (native) методы*.................................................................... 221 Краткие итоги по главе 9.................................................................................................................................................. 224 Задания.................................................................................................................................................................................. 225 Глава 10. Введение в сетевое программирование.......................................................................................................... 227 Краткая справка по языку HTML................................................................................................................................... 227 Апплеты................................................................................................................................................................................. 232 Сервлеты................................................................................................................................................................................ 234 Технология JSP – Java Server Pages................................................................................................................................ 237 Краткие итоги по главе 10................................................................................................................................................ 241 Задания.................................................................................................................................................................................. 242 Глава 11. Встроенные классы.............................................................................................................................................. 243 Виды встроенных классов................................................................................................................................................ 243 Вложенные (nested) классы и интерфейсы................................................................................................................... 243 Внутренние (inner) классы................................................................................................................................................ 244 Локальные (local) классы.................................................................................................................................................. 246 Анонимные (anonimous) классы и обработчики событий....................................................................................... 246 Анонимные (anonimous) классы и слушатели событий (listeners)......................................................................... 247 Краткие итоги по главе 11................................................................................................................................................ 250 Задания.................................................................................................................................................................................. 251 Глава 12. Компонентное программирование................................................................................................................... 252 Компонентная архитектура JavaBeans........................................................................................................................ 252 Мастер создания компонента в NetBeans.................................................................................................................... 253 Пример создания компонента в NetBeans – панель с заголовком......................................................................... 256 Добавление в компонент новых свойств...................................................................................................................... 259 Добавление в компонент новых событий..................................................................................................................... 261 Краткие итоги по главе 12................................................................................................................................................ 265 Задания.................................................................................................................................................................................. 266 Литература................................................................................................................................................................................. 267 Дополнительная литература........................................................................................................................................... 267 Языку Java посвящены сотни тысяч книг и учебных курсов. Поэтому, без сомнения, предлагаемый курс может рассматриваться только как краткое введение в данную область. Его назначение – дать основные представления о концепциях языка и современном состоянии дел. Java быстро развивается, из-за чего многие прежние представления оказываются устаревшими. Данный курс основан на лекциях, читавшихся автором студентам кафедры вычислительной физики физического факультета СПбГУ, но был существенно переработан. Курс рассчитан на широкую аудиторию начинающих программистов, от школьников-старшеклассников до студентов старших курсов, а также людей, желающих самостоятельно научиться программировать на этом языке. Кроме того, курс может быть интересен даже опытным программистам, которые по каким-либо причинам не отслеживали изменения и нововведения в Java за последние годы. Курс содержит информацию об основных синтаксических конструкциях языка Java, особенностях и типичных ошибках их использования, а также сведения о современных средах разработки. Одной из привлекательных особенностей языка Java с самого начала была бесплатность распространения базовых средств разработки (SDK – Software Development Kit) и исполняющей среды Java (виртуальной Java-машины). Однако компилятор, входящий в состав SDK, работал в режиме командной строки, то есть отставал идеологически по крайней мере на 20 лет от современных профессиональных компиляторов. В 2006 году корпорация Sun Microsystems пошла на беспрецедентный шаг – сделала бесплатными профессиональные средства разработки программного обеспечения. Еще одним шагом Sun в направлении открытости программного обеспечения, в том числе - исходного кода, стала разработка платформы NetBeans. Это среда, основанная на принципах компонентного программирования. Она включает в себя как среду разработки, так и набор библиотечных компонентов ( Beans – “зёрна”. Игра слов: язык Java получил название по имени кофе, которое любили программисты, так что название компонентной модели Java Beans может быть расшифровано как “зёрна кофе Java”). Компонентная модель NetBeans – дальнейший шаг после Java Beans. Среда разработки NetBeans может быть свободно загружено с сайта Sun и распространяется на условиях лицензии Sun Public License (SPL). Данная лицензия подразумевает, что всё программное обеспечение, написанное на условиях SPL, поставляется с открытым исходным кодом (source code). В настоящее время продукт Sun Java Studio Enterprise прекратил самостоятельное существование и стал расширением среды NetBeans – теперь это NetBeans Enterprise Pack. Все примеры, приводящиеся в данной книге, разработаны и проверены в среде NetBeans версии 5.5 с JDK1.5.0_04. Упоминаемые в данной книге названия Java ® и Solaris ® - являются торговыми марками корпорации Sun Microsystems Windows® - является торговой маркой корпорации Microsoft MacOS® - является торговой маркой корпорации Apple Глава 1. Общие представления о языке Java 1.1. Java и другие языки программирования. Системное и прикладное программирование Язык программирования Java был создан в рамках проекта корпорации Sun Microsystems по созданию компьютерных программно-аппаратных комплексов нового поколения. Первая версия языка была официально опубликована в 1995 году. С тех пор язык Java стал стандартом де-факто, вытеснив за десять лет языки C и C++ из многих областей программирования. В 1995 году они были абсолютными лидерами, но к 2006 году число программистов, использующих Java, стало заметно превышать число программистов, использующих C и C++, и составляет более четырёх с половиной миллионов человек. А число устройств, в которых используется Java, превышает полтора миллиарда. Как связаны между собой языки C, C++, JavaScript и Java? Что между ними общего, и в чём они отличаются? В каких случаях следует, а в каких не следует их применять? Для того чтобы ответить на этот вопрос, следует сначала остановиться на особенностях программного обеспечения предыдущих поколений и о современных тенденциях в развитии программного обеспечения. Первоначально программирование компьютеров шло в машинных кодах. Затем появились языки ASSEMBLER, которые заменили команды процессоров мнемоническими сокращениями, гораздо более удобными для человека, чем последовательности нулей и единиц. Их принято считать языками программирования низкого уровня (то есть близкими к аппаратному уровню), так как они ориентированы на особенности конкретных процессоров. Именно поэтому программы, написанные на языках ASSEMBLER, нельзя было переносить на компьютеры с другим типом процессора - процессоры имели несовместимые наборы команд. То есть они были непереносимы на уровне исходного кода (source code). Программы, написанные в машинных кодах, то есть в виде последовательности ноликов и единиц, соответствующих командам процессора и необходимым для них данным, нет необходимости как-то преобразовывать. Их можно скопировать в нужное место памяти компьютера и передать управление первой команде программы (задать точку входа в программу). Программы, написанные на каком-либо языке программирования, сначала надо перевести из одной формы (текстовой) в другую (двоичную, то есть в машинные коды). Процесс такого перевода называется трансляцией (от английского translation – “перевод”, “перемещение”). Не обязательно переводить программу из текстовой формы в двоичные коды, возможен процесс трансляции с одного языка программирования на другой. Или из кодов одного типа процессора в коды другого типа. Имеется два основных вида трансляции – компиляция и интерпретация . При компиляции первоначальный набор инструкций однократно переводится в исполняемую форму (машинные коды), и в последующем при работе программы используются только эти коды. При интерпретации во время каждого вызова необходимых инструкций каждый раз сначала происходит перевод инструкций из одной формы (текстовой или двоичной) в другую – в исполняемые коды процессора используемого компьютера. И только потом эти коды исполняются. Естественно, что интерпретируемые коды исполняются медленнее, чем скомпилированные, так как перевод инструкций из одной формы в другую обычно занимает в несколько раз больше времени чем выполнение полученных инструкций. Но интерпретация обеспечивает большую гибкость по сравнению с компиляцией, и в ряде случаев без неё не обойтись. В 1956 году появился язык FORTRAN – первый язык программирования высокого уровня (то есть не ориентированный на конкретную аппаратную реализацию компьютера). Он обеспечил переносимость программ на уровне исходных кодов, но довольно дорогой ценой. Во-первых, быстродействие программ, написанных на FORTRAN, было в несколько раз меньше, чем для ассемблерных. Во-вторых, эти программы занимали примерно в два раза больше места в памяти компьютера, чем ассемблерные. И, наконец, пришлось отказаться от поддержки особенностей периферийных устройств – общение с “внешним миром” пришлось ограничить простейшими возможностями, которые в программе одинаково реализовывались для ввода-вывода с помощью перфокарточного считывателя, клавиатуры, принтера, текстового дисплея и т.д. Тем не менее, языки программирования высокого уровня постепенно вытеснили языки ASSEMBLER, поскольку обеспечивали не только переносимость программ, но и гораздо более высокую их надёжность, а также несоизмеримо более высокую скорость разработки сложного программного обеспечения. FORTRAN до сих пор остаётся важнейшим языком программирования для высокопроизводительных численных научных расчётов. Увеличение аппаратных возможностей компьютеров (количества памяти, быстродействия, появления дисковой памяти большого объёма), а также появление разнообразных периферийных устройств, привело к необходимости пересмотра того, как должны работать программы. Массовый выпуск компьютеров потребовал унификации доступа из программ к различным устройствам. Возникла идея, что из программы можно обращаться к устройству без учёта особенностей его аппаратной реализации. Это возможно, если обращение к устройству идёт не напрямую, а через прилагающуюся программу – драйвер устройства (по-английски driver означает “водитель”). Появились операционные системы - наборы драйверов и программ, распределяющих ресурсы компьютера между разными программами. Соответственно, программное обеспечение стало разделяться на системное и прикладное . Системное программное обеспечение – непосредственно обращающееся к аппаратуре, прикладное – решающее какие-либо прикладные задачи и использующее аппаратные возможности компьютера не напрямую, а через вызовы программ операционной системы. Прикладные программы стали приложениями операционной системы, или, сокращённо, приложениями (applications). Этот термин означает, что программа может работать только под управлением операционной системы. Если на том же компьютере установить другой тип операционной системы, программа-приложение первой операционной системы не будет работать. Требования к прикладным программам принципиально отличаются от требований к системным программам. От системного программного обеспечения требуется максимальное быстродействие и минимальное количество занимаемых ресурсов, а также возможность доступа к любым необходимым аппаратным ресурсам. От прикладного – максимальная функциональность в конкретной предметной области. При этом быстродействие и занимаемые ресурсы не имеют значения до тех пор, пока не влияют на функциональность. Например, нет совершенно никакой разницы, реагирует программа на нажатие клавиши на клавиатуре за одну десятую или за одну миллионную долю секунды. Правда, на первоначальном этапе создания прикладного программного обеспечения даже прикладные по назначению программы были системными по реализации, так как оказывались вынуждены напрямую обращаться к аппаратуре. Язык C был создан в 1972 году в одной из исследовательских групп Bell Laboratories при разработке операционной системы Unix. Сначала была предпринята попытка написать операционную систему на ASSEMBLER, но после появления в группе новых компьютеров пришлось создать платформонезависимый язык программирования высокого уровня, с помощью которого можно было бы писать операционные системы. Таким образом, язык C создавался как язык для создания системного программного обеспечения, и таким он остаётся до сих пор. Его идеология и синтаксические конструкции ориентированы на максимальную близость к аппаратному уровню реализации операций – в той степени, в какой он может быть обеспечен на аппаратно-независимом уровне. При этом главным требованием была максимальная скорость работы и минимальное количество занимаемых ресурсов, а также возможность доступа ко всем аппаратным ресурсам. Язык C является языком процедурного программирования , так как его базовыми конструкциями являются подпрограммы . В общем случае подпрограммы принято называть подпрограммами-процедурами (откуда и идёт название “процедурное программирование”) и подпрограммами-функциями. Но в C имеются только подпрограммы-функции. Обычно их называют просто функциями . Язык C произвёл настоящую революцию в разработке программного обеспечения, получил широкое распространение и стал промышленным стандартом. Он до сих пор применяется для написания операционных систем и программирования микроконтроллеров. Но мало кто в полной мере осознаёт причины его популярности. В чём они заключались? - В том, что он смог обеспечить необходимую функциональность программного обеспечения в условиях низкой производительности компьютеров, крайней ограниченности их ресурсов и неразвитости периферийных устройств! При этом повторилась та же история, что и с FORTRAN, но теперь уже для языка системного программирования. Переход на язык программирования высокого уровня, но с минимальными потерями по производительности и ресурсам, дал большие преимущества. Большое влияние на развитие теории программирования дал язык PASCAL, разработанный в 1974 году швейцарским профессором Никлаусом Виртом. В данной разработке имелось две части. Первая состояла в собственно языке программирования PASCAL, предназначенном для обучения идеям структурного программирования . Вторая заключалась в идее виртуальной машины. Никлаус Вирт предложил обеспечить переносимость программ, написанных на PASCAL, за счёт компиляции их в набор команд некой абстрактной P-машины (P- сокращение от PASCAL), а не в исполняемый код конкретной аппаратной платформы. А на каждой аппаратной платформе должна была работать программа, интерпретирующая эти коды. Говорят, что такая программа эмулирует (то есть имитирует) систему команд несуществующего процессора. А саму программу называют виртуальной машиной. В связи с ограниченностью ресурсов компьютеров и отсутствием в PASCAL средств системного программирования этот язык не смог составить конкуренцию языку C, так как практически всё промышленное программирование вплоть до середины последней декады двадцатого века по реализации было системным. Идеи P-машины были в дальнейшем использованы и значительно усовершенствованы в Java. Развитие теории и практики программирования привело к становлению в 1967-1972 годах нового направления – объектного программирования , основанного на концепциях работы с классами и объектами . Оно обеспечило принципиально новые возможности по сравнению с процедурным. Были предприняты попытки расширения различных языков путём введения в них конструкций объектного программирования. В 1982 году Бьерном Страуструпом путём такого расширения языка C был создан язык, который он назвал “C с классами”. В1983 году после очередных усовершенствований им был создан первый компилятор языка C++. Два плюса означают “C с очень большим количеством добавлений”. C++ является надмножеством над языком C – на нём можно писать программы как на “чистом C”, без использования каких-либо конструкций объектного программирования. В связи с этим, а также дополнительными преимуществами объектного программирования, он быстро приобрёл популярность и стал промышленным стандартом, сначала “де факто”, а потом и “де юре”. Так что в настоящее время C++ является базовым языком системного программирования. Длительное время он использовался и для написания прикладных программ. Но, как мы уже знаем, требования к прикладным программам совпадают к требованиям к системным только в том случае, когда быстродействие компьютера можно рассматривать как низкое, а ресурсы компьютера – малыми. Кроме этого, у языков C и C++ имеется ещё два принципиальных недостатка: а) низкая надёжность как на уровне исходного кода, так и на уровне исполняемого кода; б) отсутствие переносимости на уровне исполняемого кода. С появлением компьютерных сетей эти недостатки стали очень существенным ограничивающим фактором, поскольку вопросы безопасности при работе в локальных, и, особенно, глобальных сетях приобретают первостепенную значимость. В 1995 году появились сразу два языка программирования, имеющие в настоящее время огромное значение –Java, разработанный в корпорации Sun, и JavaScript, разработанный в небольшой фирме Netscape Communication, получившей к тому времени известность благодаря разработке браузера Netscape Navigator. Java создавался как универсальный язык, предназначенный для прикладного программирования в неоднородных компьютерных сетях как со стороны клиентского компьютера, так и со стороны сервера. В том числе – для использования на тонких аппаратных клиентах (устройствах малой вычислительной мощности с крайне ограниченными ресурсами). При этом скомпилированные программы Java работают только под управлением виртуальной Java-машины, поэтому они называются приложениями Java . Синтаксис операторов Java практически полностью совпадает с синтаксисом языка C, но, в отличие от C++, Java не является расширением C – это совершенно независимый язык, со своими собственными синтаксическими правилами. Он является гораздо более сильно типизированным по сравнению с C и C++, то есть вносит гораздо больше ограничений на действия с переменными и величинами разных типов. Например, в C/C++ нет разницы между целочисленными числовыми, булевскими и символьными величинами, а также адресами в памяти. То есть, например, можно умножить символ на булевское значение, из которого вычтено целое число, и разделить результат на адрес! В Java введён вполне разумный запрет на почти все действия такого рода. Язык JavaScript создавался как узкоспециализированный прикладной язык программирования HTML-страниц, расширяющий возможности HTML, и в полной мере отвечает этим потребностям до сих пор. Следует подчеркнуть, что язык JavaScript не имеет никакого отношения к Java. Включение слова “Java” в название JavaScript являлось рекламным трюком фирмы Netscape Communication. Он также C-образен, но, в отличие от Java, является интерпретируемым. Основное назначение JavaScript – программное управление элементами WWW-документов. Языки HTML и XML позволяют задавать статический, неизменный внешний вид документов, и с их помощью невозможно запрограммировать реакцию на действия пользователя. JavaScript позволяет ввести элементы программирования в поведение документа. Программы, написанные на JavaScript, встраиваются в документы в виде исходных кодов (сценариев) и имеют небольшой размер. Для упрощения работы с динамически формируемыми документами JavaScript имеет свободную типизацию – переменные меняют тип по результату присваивания. Поэтому программы, написанные на JavaScript, гораздо менее надёжны, чем написанные на C/C++, не говоря уж про Java. Java, JavaScript и C++ являются объектно-ориентированными языками программирования, и все они имеют C-образный синтаксис операторов. Но как объектные модели, так и базовые конструкции этих языков (за исключением синтаксиса операторов), в этих языках принципиально различны. Ни один из них не является версией или упрощением другого – это совсем разные языки, предназначенные для разных целей. Итак, Java- универсальный язык прикладного программирования, JavaScript – узкоспециализированный язык программирования HTML-документов, C++ - универсальный язык системного программирования. В 2000 году в корпорации Microsoft была разработана платформа .Net (читается “дотнет”, DotNet– в переводе с английского “точка Net” ). Она стала альтернативой платформе Java и во многом повторяла её идеи. Основное различие заключалось в том, что для этой платформы можно использовать произвольное количество языков программирования, а не один. Причём классы .Net оказываются совместимы как в целях наследования, так и по исполняемому коду независимо от языка, используемого для их создания. Важнейшим языком .Net стал Java-образный язык C# (читается “Си шарп”). Фактически, C# унаследовал от Java большинство особенностей - динамическую объектную модель, сборку “мусора”, основные синтаксические конструкции. Хотя и является вполне самостоятельным языком программирования, имеющим много привлекательных черт. В частности, компонентные модели Java и C# принципиально отличаются. Java стал первым универсальным C-образным языком прикладного программирования, что обеспечило лёгкость перехода на этот язык большого числа программистов, знакомых с C и C++. А наличие средств строгой проверки типов, ориентация на работу с компьютерными сетями, переносимость на уровне исполняемого кода и поддержка платформонезависимого графического интерфейса, а также запрет прямого обращения к аппаратуре обеспечили выполнение большинства требований, предъявлявшихся к языку прикладного программирования. Чем больше становятся быстродействие и объём памяти компьютеров, тем больше потребность в разделении прикладного и системного программного обеспечения. Соответственно, для прикладных программ исчезает необходимость напрямую обращаться к памяти и другим аппаратным устройствам компьютера. Поэтому среди прикладных программ с каждым годом растёт доля программного обеспечения, написанного на Java и языках .Net. Но как по числу программистов, так и по числу устройств, использующих соответствующие платформы, Java в настоящее время лидирует с большим отрывом. 1.2. Виртуальная Java-машина, байт-код, JIT-компиляция. Категории программ, написанных на языке Java Первоначально слово “программа” означало последовательность инструкций процессора для решения какой-либо задачи. Эти инструкции являлись машинными кодами, и разницы между исходным и исполняемым кодом программы не было. Разница появилась, когда программы стали писать на языках программирования. При этом программой стали называть как текст, содержащийся в файле с исходным кодом, так и исполняемый файл. Для устранения неоднозначности термина “программа”, исполняемый код программы принято называть приложением (application). Термин “приложение” – сокращение от фразы “приложение операционной системы”. Он означает, что исполняемый код программы может работать только под управлением соответствующей операционной системы. Работа под управлением операционной системы позволяет избежать зависимости программы от устройства конкретного варианта аппаратуры на компьютере, где она должна выполняться. Например, как автору программы, так и пользователю совершенно безразлично, как устроено устройство, с которого считывается информация – будет ли это жёсткий диск с одной, двумя или шестнадцатью считывающих головок. Или это будет CD-привод, DVD-привод или ещё какой-либо другой тип носителя. Но переносимость обычных приложений ограничивается одним типом операционных систем. Например, приложение MS Windows® не будет работать под Linux, и наоборот. Программы, написанные на языке Java, выполняются под управлением специальной программы -виртуальной Java-машины, и поэтому обладают переносимостью на любую операционную систему, где имеется соответствующая Java-машина. Благодаря этому они являются не приложениями какой-либо операционной системы, а приложениями Java . Программы, написанные на языке Java, представляют из себя наборы классов и сохраняются в текстовых файлах с расширением .java. (Про то, что такое классы, будет рассказано несколько позже). При компиляции текст программы переводится (транслируются) в двоичные файлы с расширением .class. Такие файлы содержат байт-код - инструкции для абстрактного Java-процессора в виде байтовых последовательностей команд этого процессора и данных к ним. Для того, чтобы байт-код был выполнен на каком-либо компьютере, он должен быть переведён в инструкции для соответствующего процессора. Именно этим и занимается Java-машина. Первоначально байт-код всегда интерпретировался: каждый раз, как встречалась какая-либо инструкция Java-процессора, она переводилась в последовательность инструкций процессора компьютера. Естественно, это значительно замедляло работу приложений Java. В настоящее время используется более сложная схема, называемая JIT-компиляцией (Jast-In-Time) – компиляцией “по ходу дела”, “налету”. Когда какая-либо инструкция (или набор инструкций) Java-процессора выполняется в первый раз, происходит компиляция соответствующего ей байт-кода с сохранением скомпилированного кода в специальном буфере. При последующем вызове той же инструкции вместо её интерпретации происходит вызов из буфера скомпилированного кода. Поэтому интерпретация происходит только при первом вызове инструкции. Сложные оптимизирующие JIT-компиляторы действуют ещё изощрённей. Поскольку обычно компиляция инструкции идёт гораздо дольше по сравнению с интерпретацией этой инструкции, время её выполнения в первый раз при наличии JIT-компиляции может заметно отличаться в худшую сторону по сравнению с чистой интерпретацией. Поэтому бывает выгоднее сначала запустить процесс интерпретации, а параллельно ему в фоновом режиме компилировать инструкцию. Только после окончания процесса компиляции при последующих вызовах инструкции будет исполняться её скомпилированный код. – До этого все её вызовы будут интерпретироваться. Разработанная Sun виртуальная машина HotSpot осуществляет JIT-компиляцию только тех участков байт-кода, которые критичны к времени выполнения программы. При этом по ходу работы программы происходит оптимизация скомпилированного кода. Благодаря компиляции программ Java в платформонезависимый байт-код обеспечивается переносимость этих программ не только на уровне исходного кода, но и на уровне скомпилированных приложений. Конечно, при этом на компьютере, где выполняется приложение, должна быть установлена программа виртуальной Java-машины (Java Virtual Machine - JVM), скомпилированная в коды соответствующего процессора (native code – “родной” код). На одном и том же компьютере может быть установлено несколько Java-машин разных версий или от разных производителей. Спецификация Java-машины является открытой, точно так же, как требования к компилятору языка Java. Поэтому различные фирмы, а не только Sun, разрабатывают компиляторы Java и Java-машины. Приложение операционной системы запускается с помощью средств операционной системы. Приложение Java, напротив, запускается с помощью виртуальной Java-машины, которая сама является приложением операционной системы. Таким образом, сначала стартует Java-машина. Она получает в качестве параметра имя файла с компилированным кодом класса. В этом классе ищется и запускается на выполнение подпрограмма с именем main. Приложения Java обладают не только хорошей переносимостью, но и высокой скоростью работы. Однако даже при наличии JIT-компиляции они всё-таки могут выполняться медленнее, чем программы, написанные на C или C++. Это связано с тем, что JIT-компиляция создаёт не такой оптимальный код как многопроходный компилятор C/C++, который может тратить очень большое время и много ресурсов на отыскивание конструкций программы, которые можно оптимизировать. А JIT-компиляция происходит “на лету”, в условиях жёсткой ограниченности времени и ресурсов. Для решения этой проблемы были разработаны компиляторы программ Java в код конкретных программно-аппаратных платформ (native code – “родной” код). Например, свободно распространяемый фондом GNU компилятор gjc. Правда, заметные успехи Sun в усовершенствовании Java-машины позволили практически достичь, а в ряде случаев даже обогнать по быстродействию программы, написанные на других языках. В частности, приложения Java, активно занимающиеся выделением-высвобождением памяти, работают быстрее своих аналогов, написанных на C/C++, благодаря специальному механизму программных слотов памяти (slot – “паз, отверстие для вставления чего-либо”). Виртуальная Java-машина не только исполняет байт-код (интерпретирует его, занимается JIT-компиляцией и исполняет JIT-компилированный код), но и выполняет ряд других функций. Например, взаимодействует с операционной системой, обеспечивая доступ к файлам или поддержку графики. А также обеспечивает автоматическое высвобождение памяти, занятой ненужными объектами – так называемую сборку мусора (garbage collection). Программы Java можно разделить на несколько основных категорий: · Приложение (application) – аналог “обычной” прикладной программы. · Апплет (applet) – специализированная программа с ограниченными возможностями, работающая в окне WWW-документа под управлением браузера. · Сервлет (servlet) - специализированная программа с ограниченными возможностями, работающая в WWW на стороне сервера. Используется преимущественно в рамках технологии JSP (Java Server Pages - Серверных Страниц Java) для программирования WWW-документов со стороны сервера. · Серверное приложение (Enterprise application) – предназначено для многократного использования на стороне сервера. · Библиотека (Java Class Library – библиотека классов, либо NetBeans Module – модуль платформы NetBeans) – предназначена для многократного использования программами Java Между приложениями и апплетами Java имеется принципиальное различие: приложение запускается непосредственно с компьютера пользователя и имеет доступ ко всем ресурсам компьютера наравне с любыми другими программами. Апплет же загружается из WWW с постороннего сервера, причём из-за самой идеологии WWW сайт, с которого загружен апплет, в общем случае не может быть признан надёжным. А сам апплет имеет возможность передавать данные на произвольный сервер в WWW. Поэтому для того, чтобы избежать риска утечки конфиденциальной информации с компьютера пользователя или совершения враждебных действий у апплетов убраны многие возможности, имеющиеся у приложений. Сервлеты – это приложения Java , запускаемые со стороны сервера. Они имеют возможности доступа к файловой системе и другим ресурсам сервера через набор управляющих конструкций, предопределённых в рамках технологии JSP и пакета javax.servlet. Технология JSP заключается в наличии дополнительных конструкций в HTML- или XML-документах, которые позволяют осуществлять вызовы сценариев (“скриптов”), написанных на языке Java. В результате удаётся очень просто и удобно осуществлять обработку данных или элементов документа, и внедрять в нужные места документа результаты обработки. Сценарии Java перед первым выполнением автоматически компилируются на стороне сервера, поэтому выполняемый код выполняется достаточно быстро. Но, конечно, требует, чтобы была установлена соответствующая Java-машина. Например, входящая в состав Sun Application Server – программного обеспечения, обеспечивающего поддержку большого количества необходимых серверных возможностей для работы в WWW. Отметим, что Sun Application Server также распространяется бесплатно и входит в комплект NetBeans Enterprise Pack. Первоначально Java позиционировался Sun как язык, обеспечивающий развитые графические возможности WWW-документов благодаря включению в них апплетов. Однако в настоящее время основными областями использования Java является прикладное программирование на основе приложений, страниц JSP и сервлетов, а также других видов серверных программ. При этом использование апплетов играет незначительную роль. Виртуальную Java-машину часто называют исполняющей средой (Java Runtime Environment - JRE). Существует два основных способа установки Java-машины на клиентский компьютер: · JRE из поставки Software Development Kit (SDK) - Комплекта разработки программного обеспечения. · Специализированный вариант JRE в составе Интернет-браузера, называющийся Java plugin. Комплект последних версий SDK можно свободно загружать с сайта Sun http://java.sun.com/ . При использовании апплетов требуется, чтобы в состав браузера входил специализированный комплект JRE. Как правило, он поставляется вместе с браузером, и может при необходимости обновляться. Для MS Internet Explorer такой комплект и его обновления могут быть свободно загружены с сайта Microsoft. Имеется возможность установки Java-машины от различных производителей, не обязательно устанавливать комплект SDK от Sun. На одном и том же компьютере может быть установлено сразу несколько различных Java-машин, в том числе комплекты SDK разных версий. Правда, опыт показывает, что при этом некоторые программы, написанные на Java, теряют работоспособность (частично или полностью). Комплекты SDK имеют классификацию, опирающуюся на версию Java (языка программирования и, соответственно, Java-машины) и тип создаваемых приложений. Так, ко времени написания данного текста выходили версии SDK 1.0, 1.1, 1.2, 1.3, 1.4, 1.5 и 1.6. У каждой версии имеется ряд подверсий, не сопровождающихся изменением языка программирования, а связанных в основном с исправлением ошибок или внесением небольших изменений в библиотеки. Например, 1.4.1_01 или 1.5.0_04. Версии Java 1.0 и 1.1 принято называть Java 1. Все версии Java начиная c 1.2 называют Java 2. Однако более надёжно классифицировать по номеру SDK, так как язык Java для версии SDK 1.5 очень заметно отличается по возможностям от языка Java для более ранних версий SDK – в него добавлено большое количество новых синтаксических конструкций, а также изменён ряд правил. Поэтому код, правильный в Java для версии SDK 1.5, может оказаться неправильным в Java для версии SDK 1.4. Не говоря уж про Java для версии SDK 1.3 или 1.2. Кроме того, недавно компания Sun перестала использовать в названиях комплектов программного обеспечения термин Java 2 и происходящие от него сокращения вида j2. Комплекты разработки SDK одной версии отличаются по типу создаваемых с их помощью приложений. Имеется три типа SDK: · Java ME – комплект Java Micro Edition (микро-издание) http://java.sun.com/j2me/, предназначенный для программирования “тонких аппаратных клиентов”. То есть устройств, обладающих малыми ресурсами - наладонных компьютеров, сотовых телефонов, микроконтроллеров, смарт-карт. Старое название J2ME. · Java SE – комплект Java Standard Edition (стандартное издание) http://java.sun.com/j2se/, предназначенный для программирования “толстых клиентов”. То есть устройств, обладающих достаточно большими ресурсами - обычных компьютеров. Старое название J2SE. · Java EE– комплект Java Enterprise Edition http://java.sun.com/j2ee/, предназначенный для написания серверного программного обеспечения. Старое название J2EE. При распространении какого-либо продукта, написанного на Java, возможна установка только программного обеспечения Java-машины (JRE – Java Runtime Environment). Например, в случае использования Java 1.4.1_01 - комплекта j2re1.4.1_01. При этом создаётся папка с именем j2re1.4.1_01 с вложенными папками bin и lib. В папке bin содержатся файлы и папки, необходимые для работы Java-машины и дополнительных инструментов для работы с ней в специальных режимах. В папке lib содержатся вспомогательные файлы и библиотеки, в основном связанные с параметрами настроек системы. Также возможна установка целиком SDK. Например, при установке SDK Java SE 1.5.0_04 создаётся папка JDK1.5.0_04 с вложенными папками bin, demo, include, jre, lib, sample, а также архивом src.zip с исходными кодами стандартных классов Java. В папке bin содержатся файлы инструментов разработки, в папке demo - файлы примеров с исходными кодами. В папке include - заголовки файлов C для доступа к ряду библиотек Java и отладчику виртуальной Java-машины на платформо-зависимом уровне - на основе интерфейсов JNI (Java Native Interface) и JVMDI (Java Virtual Machine Debugging Interface), соответственно. В папке jre находятся файлы, необходимые для работы с виртуальной Java-машиной. Папка lib содержит ряд библиотек и сопроводительных файлов, необходимых для работы инструментов из папки bin. В папке sample находятся примеры с исходными кодами. Аббревиатура JDK расшифровывается как Java Development Kit – комплект разработки программного обеспечения на Java. К сожалению, в комплекте отсутствует даже самая простейшая документация с описанием назначения имеющихся в нём инструментов – даны ссылки на сайт компании Sun, где можно найти эту информацию. Поэтому перечислим назначение основных инструментов. Они делятся на несколько категорий. Средства разработки приложений
Кроме этого, имеются средства поддержки работы в WWW и корпоративных сетях (интранет) с интерфейсом RMI - интерфейсом удалённого вызова методов. Это программы rmic, rmiregistry, rmid. Также имеются средства поддержки информационной безопасности keytool, jarsigner, policytool, и ряд файлов других категорий утилит. Подчеркнём, что набор утилит JDK рассчитан на морально устаревший режим командной строки, и что гораздо удобнее и правильнее пользоваться современной профессиональной средой разработки NetBeans. Для её работы из JDK необходим только комплект JRE. 1.3.Алфавит языка Java. Десятичные и шестнадцатеричные цифры и целые числа. Зарезервированные слова Алфавит языка JavaАлфавит языка Java состоит из букв, десятичных цифр и специальных символов. Буквами считаются латинские буквы (кодируются в стандарте ASCII), буквы национальных алфавитов (кодируются в стандарте Unicode, кодировка UTF-16), а также соответствующие им символы, кодируемые управляющими последовательностями (о них будет рассказано чуть позже). Буквы и цифры можно использовать в качестве идентификаторов (т.е. имён) переменных, методов и других элементов языка программирования. Правда, при использовании в идентификаторах национальных алфавитов в ряде случаев могут возникнуть проблемы – эти символы будут показываться в виде вопросительных знаков. Как буквы рассматривается только часть символов национальных алфавитов. Остальные символы национальных алфавитов - это специальные символы. Они используются в качестве операторов и разделителей языка Java и не могут входить в состав идентификаторов. Латинские буквы ASCII ABCD...XYZ - заглавные (прописные) , abcd...xyz – строчные Дополнительные “буквы” ASCII _ - знак подчеркивания, $ - знак доллара. Национальные буквы на примере русского алфавита АБВГ…ЭЮЯ - заглавные (прописные), абвг…эюя – строчные 0 1 2 3 4 5 6 7 8 9 Десятичные и шестнадцатеричные цифры и целые числаЦелые числовые константы в исходном коде Java (так называемые литерные константы) могут быть десятичными или шестнадцатеричными. Они записываются либо символами ASCII, или символами Unicode следующим образом. Десятичные константы записываются как обычно. Например, -137. Шестнадцатеричная константа начинается с символов 0x или 0X (цифра 0, после которой следует латинская буква X), а затем идёт само число в шестнадцатеричной нотации. Например, 0x10 соответствует 1016 =16; 0x2F соответствует 2F16 =47, и т.д. О шестнадцатеричной нотации рассказано чуть ниже. Ранее иногда применялись восьмеричные числа, и в языках C/C++, а также старых версиях Java можно было их записывать в виде числа, начинающегося с цифры 0. То есть 010 означало 108 =8. В настоящее время в программировании восьмеричные числа практически никогда не применяются, а неадекватное использование ведущего нуля может приводить к логическим ошибкам в программе. Целая константа в обычной записи имеет тип int. Если после константы добавить букву L (или l, что хуже видно в тексте, хотя в среде разработки выделяется цветом), она будет иметь тип long, обладающий более широким диапазоном значений, чем тип int. Поясним теперь, что такое шестнадцатеричная нотация записи чисел и зачем она нужна. Информация представляется в компьютере в двоичном виде – как последовательность бит. Бит – это минимальная порция информации, он может быть представлен в виде ячейки, в которой хранится или ноль, или единица. Но бит – слишком мелкая единица, поэтому в компьютерах информация хранится, кодируется и передаётся байтами - порциями по 8 бит. В данной книге под “ячейкой памяти” будет пониматься непрерывная область памяти (с последовательно идущими адресами), выделенная программой для хранения данных. На рисунках мы будем изображать ячейку прямоугольником, внутри которого находятся хранящиеся в ячейке данные. Если у ячейки имеется имя, оно будет писаться рядом с этим прямоугольником. Мы привыкли работать с числами, записанными в так называемой десятичной системе счисления. В ней имеется 10 цифр (от 0 до 9), а в числе имеются десятичные разряды . Каждый разряд слева имеет вес 10 по сравнению с предыдущим, то есть для получения значения числа, соответствующего цифре в каком-то разряде, стоящую в нём цифру надо умножать на 10 в соответствующей степени. То есть 52=5∙10+2, 137=1∙102 +3∙101 +7, и т.п. В программировании десятичной системой счисления пользоваться не всегда удобно, так как в компьютерах информация организована в виде бит, байт и более крупных порций. Человеку неудобно оперировать данными в виде длинных последовательностей нулей и единиц. В настоящее время в программировании стандартной является шестнадцатеричная система записи чисел. Например, с её помощью естественным образом кодируется цвет, устанавливаются значения отдельных бит числа, осуществляется шифрование и дешифрование информации, и так далее. В этой системе счисления всё очень похоже на десятичную, но только не 10, а 16 цифр, и вес разряда не 10, а 16. В качестве первых 10 цифр используются обычные десятичные цифры, а в качестве недостающих цифр, больших 9, используются заглавные латинские буквы A, B, C, D, E, F: 0 1 2 3 4 5 6 7 8 9 A B C в E F То есть A=10, B=11, C=12, D=13, E=14, F=15. Заметим, что в шестнадцатеричной системе счисления числа от 0 до 9 записываются одинаково, а превышающие 9 отличаются. Для чисел от 10 до 15 в шестнадцатеричной системе счисления используются буквы от A до F, после чего происходит использование следующего шестнадцатеричного разряда. Десятичное число 16 в шестнадцатеричной системе счисления записывается как 10. Для того, чтобы не путать числа, записанные в разных системах счисления, около них справа пишут индекс с указанием основания системы счисления. Для десятичной системы счисления это 10, для шестнадцатеричной 16. Для десятичной системы основание обычно не указывают, если это не приводит к путанице. Точно так же в технической литературе часто не указывают основание для чисел, записанных в шестнадцатеричной системе счисления, если в записи числа встречаются не только “обычные” цифры от 0 до 9, но и “буквенные” цифры от A до F. Обычно используют заглавные буквы, но можно применять и строчные. Рассмотрим примеры. 0x10 = 1016 =16 ; 0x100 = 10016 =16 ∙16=256; 0x1000 = 100016 =(16)3 =4096; 0x20 = 2016 =2∙16 =32; 0x21 = 2116 =2∙16 +1=33; 0xF = F16 =15 ; 0x1F = 1F16 =1∙16 +15=31 ; 0x2F = 2F16 =2∙16 +15=47 ; 0xFF = FF16 =15 ∙16+15=255; Более подробно вопросы представления чисел в компьютере будут рассмотрены в отдельном разделе. Зарезервированные слова языка JavaЭто слова, зарезервированные для синтаксических конструкций языка, причём их назначение нельзя переопределять внутри программы.
Их нельзя использовать в качестве идентификаторов (имён переменных, подпрограмм и т.п.), но можно использовать в строковых выражениях. 1.4. Управляющие последовательности. Символы Unicode. Специальные символы Управляющие последовательностиУправляющие последовательности - символы формирования текста Иногда в тексте программы в строковых константах требуется использовать символы, которые обычным образом в текст программы ввести нельзя. Например, символы кавычек (их надо использовать внутри кавычек, что затруднительно), символ вопроса (зарезервирован для тернарного условного оператора), а также различные специальные символы. В этом случае используют управляющую последовательность – символ обратной косой черты, после которой следует один управляющий символ. В таблице приведены управляющие последовательности, применяющиеся в языке Java.
Управляющие последовательности – символы Unicode Управляющая последовательность может содержать несколько символов. Например, символы национальных алфавитов могут кодироваться последовательностью “\u”, после которой идёт код символа в шестнадцатеричной кодировке для кодовых таблиц UTF-16 или UTF-8. Например: \u0030 - \u0039 – цифры ISO-LATIN от 0 до 9 \u0024 – знак доллара $ \u0041 - \u005a – буквы от A до Z \u0061 - \u007a – буквы от a до z Простые специальные символыСоставные специальные символы
1.5.Идентификаторы. Переменные и типы . Примитивные и ссылочные типы Идентификаторы - это имена переменных, подпрограмм-функций и других элементов языка программирования. В идентификаторах можно применять только буквы и цифры, причём первой всегда должна быть буква (в том числе символы подчёркивания и доллара), а далее может идти произвольная комбинация букв и цифр. Некоторые символы национальных алфавитов рассматриваются как буквы, и их можно применять в идентификаторах. Но некоторые используются в качестве символов-разделителей, и в идентификаторах их использовать нельзя. Язык Java является регистро-чувствительным . Это значит, что идентификаторы чувствительны к тому, в каком регистре (верхнем или нижнем) набираются символы. Например, имена i1 и I1 соответствуют разным идентификаторам. Это правило привычно для тех, кто изучал языки C/C++, но может на первых порах вызвать сложности у тех, кто изучал язык PASCAL, который является регистро-нечувствительным. Длина идентификатора в Java любая, по крайней мере, в пределах разумного. Так, даже при длине идентификатора во всю ширину экрана компилятор NetBeans правильно работает. Переменная – это именованная ячейка памяти, содержимое которой может изменяться. Перед тем, как использовать какую-либо переменную, она должна быть задана в области программы, предшествующей месту, где эта переменная используется. При объявлении переменной сначала указывается тип переменной, а затем идентификатор задаваемой переменной. Указание типа позволяет компилятору задавать размер ячейки (объём памяти, выделяемой под переменную или значение данного типа), а также допустимые правила действий с переменными и значениями этого типа. В Java существует ряд предопределённых типов: int – целое число, float – вещественное число, boolean – логическое значение, Object – самый простой объектный тип (класс) Java, и т.д. Также имеется возможность задавать собственные объектные типы (классы), о чём будет рассказано позже. Объявление переменных a1 и b1, имеющих некий тип MyType1, осуществляется так: MyType1 a1,b1; При этом MyType1 – имя типа этих переменных. Другой пример – объявление переменной j типа int : int j; Типы бывают предопределённые и пользовательские. Например, int – предопределённый тип, а MyType1– пользовательский. Для объявления переменной не требуется никакого зарезервированного слова, а имя типа пишется перед именами задаваемых переменных. Объявление переменных может сопровождаться их инициализацией - присваиванием начальных значений. Приведём пример такого объявления целочисленных переменных i1 и i2 : int i1=5; int i2=-78; либо int i1=5,i2=-78; Присваивания вида int i1=i2=5;, характерные для C/C++, запрещены. Для начинающих программистов отметим, что символ “=” используется в Java и многих других языках в качестве символа присваивания , а не символа равенства, как это принято в математике. Он означает, что значение, стоящее с правой стороны от этого символа, копируется в переменную, стоящую в левой части. То есть, например, присваивание b =a означает, что в переменную (ячейку) с именем b надо скопировать значение из переменной (ячейки) с именем a . Поэтому неправильное с точки зрения математики выражение x=x+1 в программировании вполне корректно. Оно означает, что надо взять значение, хранящееся в ячейке с именем x , прибавить к нему 1 (это будет происходить где-то вне ячейки x ), после чего получившийся результат записать в ячейку x , заменив им прежнее значение. После объявления переменных они могут быть использованы в выражениях и присваиваниях: переменная=значение; переменная=выражение; переменная1= переменная2; и так далее. Например, i1=i2+5*i1; Примитивными типами называются такие, для которых данные содержатся в одной ячейке памяти, и эта ячейка не имеет подъячеек. Ссылочными типами называются такие, для которых в ячейке памяти (ссылочной переменной ) содержатся не сами данные, а только адреса этих данных, то есть ссылки на данные. При присваивании в ссылочную переменную заносится новый адрес, а не сами данные. Но непосредственного доступа к адресу, хранящемуся в ссылочных переменных, нет. Это сделано для обеспечения безопасности работы с данными – как с точки зрения устранения непреднамеренных ошибок, характерных для работы с данными по их адресам в языках C/C++/PASCAL, так и для устранения возможности намеренного взлома информации. Если ссылочной переменной не присвоено ссылки, в ней хранится нулевой адрес, которому дано символическое имя null. Ссылки можно присваивать друг другу, если они совместимы по типам, а также присваивать значение null. При этом из одной ссылочной переменной в другую копируется адрес. Ссылочные переменные можно сравнивать на равенство, в том числе на равенство null. При этом сравниваются не данные, а их адреса, хранящиеся в ссылочных переменных. В Java все типы делятся на примитивные и ссылочные. К примитивным типам относятся следующие предопределённые типы: целочисленные типы byte,short,int, long, char, типы данных в формате с плавающей точкой float, double, а также булевский (логический) тип boolean и типы-перечисления, объявляемые с помощью зарезервированного слова enum (сокращение от enumeration – “перечисление”). Все остальные типы Java являются ссылочными. В Java действуют следующие соглашения о регистре букв в идентификаторах: · Имена примитивных типов следует писать в нижнем регистре (строчными буквами). Например, int, float, boolean и т.д. · Имена ссылочных типов следует начинать с заглавной (большой) буквы, а далее для имён, состоящих из одного слова, писать все остальные буквы в нижнем регистре. Например, Object, Float, Boolean, Collection, Runnable. Но если имя составное, новую часть имени начинают с заглавной буквы. Например, JButton, JTextField, JFormattedTextField, MyType и т.д. Обратите внимание, что типы float и Float, boolean и Boolean различны – язык Java чувствителен к регистру букв! · Для переменных и методов имена, состоящие из одного слова, следует писать в нижнем регистре. Например, i, j, object1. Если имя составное, новую часть имени начинают с заглавной буквы: myVariable, jButton2, jTextField2.getText() и т.д. · Имена констант следует писать в верхнем регистре (большими буквами), разделяя входящие в имя составные части символом подчёркивания “_”. Например, Double.MIN_VALUE, Double.MAX_VALUE , JOptionPane.INFORMATION_MESSAGE, MY_CHARS_COUNT и т.п. · Символ подчёркивания “_” рекомендуется использовать для разделения составных частей имени только в именах констант и пакетов. Переменная примитивного типа может быть отождествлена с ячейкой, в которой хранятся данные. У неё всегда есть имя. Присваивание переменной примитивного типа меняет значение данных. Для ссылочных переменных действия производятся с адресами ячеек, в которых хранятся данные, а не с самими данными. Для чего нужны такие усложнения? Ведь человеку гораздо естественнее работать с ячейками памяти, в которых хранятся данные, а не адреса этих данных. Ответ заключается в том, что в программах часто требуются динамически создаваемые и уничтожаемые данные. Для них нельзя заранее создать необходимое число переменных, так как это число неизвестно на этапе написания программы и зависит от выбора пользователя. Такие данные приходится помещать в динамически создаваемые и уничтожаемые ячейки. А с этими ячейками удаётся работать только с помощью ссылочных переменных. Ссылочные типы Java используются в объектном программировании. В частности, для работы со строками, файлами, элементами пользовательского интерфейса. Все пользовательские типы (задаваемые программистом) , кроме типов-перечислений, являются ссылочными. В том числе – строковые типы. Краткие итоги по главе 1 - Алфавит языка Java состоит из букв, десятичных цифр и специальных символов. Буквами считаются латинские буквы (кодируются в стандарте ASCII), буквы национальных алфавитов (кодируются в стандарте Unicode), а также соответствующие им символы, кодируемые управляющими последовательностями. - В программах разрешается пользоваться десятичными и шестнадцатеричными целыми числовыми константами. Шестнадцатеричная константа начинается с символов 0x или 0X, после чего идёт само число в шестнадцатеричной нотации. - Java - универсальный язык прикладного программирования, JavaScript – узкоспециализированный язык программирования HTML-документов, C++ - универсальный язык системного программирования. Java - компилируемый, платформонезависимый, объектно-ориентированный язык с C-образным синтаксисом. - Программы Java переносимы как на уровне исходных кодов, так и на уровне скомпилированных исполняемых кодов – байт-кода. Байт-код является платформонезависимым, так как не содержит инструкций процессора конкретного компьютера. Он интерпретируется виртуальной Java-машиной (JVM). - JIT-компиляция (Jast-In-Time) – компиляция байт-кода в код конкретной платформы в момент выполнения программы, то есть “по ходу дела”, “налету”. Она позволяет ускорить работу программ за счёт замены интерпретации байт-кода на выполнение скомпилированного кода. - Основные категории программ Java: · Приложение (application) – аналог “обычной” прикладной программы. · Апплет (applet) – специализированная программа, работающая в окне WWW-документа под управлением браузера. · Сервлет (servlet) - специализированная программа, работающая в WWW на стороне сервера · Модуль EJB (Enterprise JavaBeans) – предназначен для многократного использования серверными приложениями Java · Библиотека – предназначена для многократного использования программами классов Java - Версии Java 1.0 и 1.1 принято называть Java 1. Все версии Java начиная c 1.2 принято называть Java 2. - Поставить на компьютер исполняющую среду Java (виртуальную Java-машину) можно путём установки SDK (Software Development Kit) - Комплекта разработки программного обеспечения. Имеется три типа SDK: · Java ME – комплект Java Micro Edition, предназначенный для программирования “тонких аппаратных клиентов”. · Java SE – комплект Java Standard Edition, предназначенный для программирования обычных компьютеров. · Java EE– комплект Java Enterprise Edition, предназначенный для написания серверного программного обеспечения. - Язык Java является регистро-чувствительным. Исходные коды программ Java набираются в виде последовательности символов Unicode. - Управляющая последовательность применяется в случае, когда требуется использовать символ, который обычным образом в текст программы ввести нельзя. Простая управляющая последовательность начинается с символа “\”, после которого идёт управляющий символ. Управляющая последовательность для кодирования символа Unicode начинается с последовательности из двух символов -“\u”, после которой следует четыре цифры номера символа в шестнадцатеричной нотации. Например, \u1234 . - Специальные символы используются в качестве операторов и разделителей языка Java и не могут входить в состав идентификаторов. Специальные символы бывают простые и составные. Они используются в операторах, для форматирования текста и как разделители. - Идентификаторы - это имена переменных, процедур, функций и т.д. В идентификаторах можно применять только буквы и цифры, причём первой всегда должна быть буква, а далее может идти произвольная комбинация букв и цифр. Длина идентификатора в Java любая. - Переменная – это именованная ячейка памяти, содержимое которой может изменяться. При объявлении переменной сначала указывается тип переменной, а затем идентификатор задаваемой переменной. - Типы в Java делятся на примитивные и ссылочные. Существует несколько предопределённых примитивных типов, все остальные – ссылочные. Все пользовательские типы кроме типов-перечислений являются ссылочными. Значение null соответствует ссылочной переменной, которой не назначен адрес ячейки с данными. Типичные ошибки: - Путают языки Java и JavaScript, либо считают, что JavaScript – это интерпретируемый вариант Java. Хотя эти языки не имеют друг к другу никакого отношения. - Ошибочно считают, что приложение Java может быть запущено на любом компьютере без установки исполняющей среды (JRE). - Не различают приложения (applications) и апплеты (applets). - При записи шестнадцатеричного числа вида 0x… вместо ведущего нуля пишут букву O. - Ошибочно считают, что в идентификаторах Java нельзя использовать символы национальных алфавитов. - Ошибочно считают, что не имеет значения, в каком регистре набраны символы идентификатора (характерно для тех, кто раньше программировал на PASCAL или FORTRAN). · Написать в 16-ричном виде числа 0, 1, 8, 15, 16, 255, 256. · Дать ответ, являются ли допустимыми идентификаторами i1, i_1, 1i, i&1, i1234567891011, IJKLMN ? · Являются ли допустимыми и различными идентификаторы myObject, MyObject, myobject, Myobject, my object, my_object ? Глава 2. Объектно-ориентированное проектирование и плат форма NetBeans 2.1.Процедурное и объектно-ориентированное программирование. Инкапсуляция Объектно-ориентированное программирование (ООП) - это методология программирования, опирающаяся на три базовых принципа: - инкапсуляцию , - наследование , - полиморфизм . Язык Java является объектно-ориентированным и в полном объёме использует эти принципы. В данном параграфе рассматривается принцип инкапсуляции, наследованию и полиморфизму посвящены отдельные параграфы. Построение программ, основанных на ООП, принципиально отличается от более ранней методики процедурного программирования , в которой основой построения программы служили подпрограммы . Программа – это набор инструкций процессору и данных, объединённых в единую функционально законченную последовательность, позволяющую выполнять какую-нибудь конкретную деятельность. Подпрограмма – это набор инструкций и данных, объединённых в относительно самостоятельную последовательность, позволяющую выполнять какую-нибудь конкретную деятельность внутри программы. При этом подпрограмма не может работать самостоятельно - она запускается из программы, и может получать из неё данные или передавать их в программу. Подпрограммы принято делить на подпрограммы-процедуры и подпрограммы-функции. Подпрограммы-процедуры вызываются для выполнения каких-либо действий, например – распечатки текста на принтере. Подпрограммы-функции выполняют какие-либо действия и возвращают некоторое значение. Например, проводится последовательность действий по вычислению синуса, и возвращается вычисленное значение. Или создаётся сложно устроенный объект и возвращается ссылка на него (адрес ячейки, в которой он находится). Программы, написанные в соответствии с принципами процедурного программирования, состоят из набора подпрограмм, причём для решения конкретной задачи программист явно указывает на каждом шагу, что делать и как делать. Эти программы практически полностью (процентов на девяносто) состоят из решения конкретных задач. Программы, написанные в соответствии с принципами ООП, пишутся совершенно иначе. В них основное время занимает продумывание и описание того, как устроены классы. Код с описанием классов предназначен для многократного использования без внесения каких-либо изменений. И только небольшая часть времени посвящается решению конкретной задачи – написания такого кода с использованием классов, который в других задачах такого типа не применить. Именно благодаря такому подходу объектное программирование приобрело огромную популярность – при необходимости решения сходных задач можно использовать уже готовый код, модифицировав только ту часть программы, которая относится к решению конкретной задачи. Подробный разбор принципов ООП будет дан позже. Пока же в общих чертах разъясним их суть. Самым простым из указанных в начале параграфа принципов является инкапсуляция . Это слово в общем случае означает “заключение внутрь капсулы”. То есть ограничение доступа к внутреннему содержимому снаружи и отсутствие такого ограничения внутри капсулы. В объектном программировании “инкапсуляция” означает использование классов – таких типов , в которых кроме данных описаны подпрограммы, позволяющие работать с этими данными, а также выполнять другие действия. Такие подпрограммы, инкапсулированные в класс, называются методами . Поля данных и методы, заданные в классе, часто называют членами класса (class members). Класс – это описание того, как будет устроен объект , являющийся экземпляром данного класса , а также какие методы объект может вызывать. Заметим, что методы, в отличие от других подпрограмм, могут напрямую обращаться к данным своего объекта. Так как экземплярами классов (“воплощением” в реальность того, что описано в классе) являются объекты, классы называют объектными типами . Все объекты, являющиеся экземплярами некоторого класса, имеют одинаковые наборы полей данных (атрибуты объекта) – но со значениями этих данных, которые свои для каждого объекта. Поля данных это переменные, заданные на уровне описания класса, а не при описании метода. В процессе жизни объекта эти значения могут изменяться. Значения полей данных объекта задают его состояние . А методы задают поведение объекта. Причём в общем случае на это поведение влияет состояние объекта – методы пользуются значениями его полей данных. Классы в Java задаются следующим образом. Сначала пишется зарезервированное слово class, затем имя класса, после чего в фигурных скобках пишется реализация класса – задаются его поля (глобальные переменные ) и методы. Объектные переменные – такие переменные, которые имеют объектный тип. В Java объектные переменные – это не сами объекты, а только ссылки на них. То есть все объектные типы являются ссылочными. Объявление объектной переменной осуществляется так же, как и для других типов переменных. Сначала пишется тип, а затем через пробел имя объявляемой переменной. Например, если мы задаём переменную obj1 типа Circle, “окружность”, её задание осуществляется так : Circle obj1; Связывание объектной переменной с объектом осуществляется путём присваивания. В правой части присваивания можно указать либо функцию, возвращающую ссылку на объект (адрес объекта), либо имя другой объектной переменной. Если объектной переменной не присвоено ссылки, в ней хранится значение null. Объектные переменные можно сравнивать на равенство, в том числе на равенство null. При этом сравниваются не сами объекты, а их адреса, хранящиеся в объектных переменных. Создаётся объект с помощью вызова специальной подпрограммы, задаваемой в классе и называемой конструктором . Конструктор возвращает ссылку на созданный объект. Имя конструктора в Java всегда совпадает с именем класса , экземпляр которого создаётся. Перед именем конструктора во время вызова ставится оператор new – “новый”, означающий, что создаётся новый объект. Например, вызов obj1=new Circle(); означает, что создаётся новый объект типа Circle, “окружность”, и ссылка на него (адрес объекта) записывается в переменную obj1. Переменная obj1 до этого уже должна быть объявлена. Оператор new отвечает за динамическое выделение памяти под создаваемый объект. Часто совмещают задание объектной переменной и назначение ей объекта. В нашем случае оно будет выглядеть как Circle obj1=new Circle(); У конструктора, как и у любой подпрограммы, может быть список параметров. Они нужны для того, чтобы задать начальное состояние объекта при его создании. Например, мы хотим, чтобы у создаваемой окружности можно было при вызове конструктора задать координаты x, y её центра и радиус r. Тогда при написании класса Circle можно предусмотреть конструктор, в котором первым параметром задаётся координата x, вторым – y, третьим – радиус окружности r. Тогда задание переменной obj1 может выглядеть так: Circle obj1=new Circle(130,120,50); Оно означает, что создаётся объект-окружность, имеющий центр в точке с координатами x=130, y=120, и у которой радиус r=50. Если разработчики класса не создали ни одного конструктора, в реализации класса автоматически создаётся конструктор по умолчанию, имеющий пустой список параметров. И его можно вызывать в программе так, как мы это первоначально делали для класса Circle. Отметим ещё одно правило, касающееся используемых имён. Как мы помним, имена объектных типов принято писать с заглавной буквы, а имена объектных переменных – с маленькой. Если объектная переменная имеет тип Circle, она служит ссылкой на объекты-окружности. Поэтому имя obj1 не очень удачно – мы используем его только для того, чтобы подчеркнуть, что именно с помощью этой переменной осуществляется связь с объектом, и чтобы читатель не путал тип переменной, её имя и имя конструктора. В Java принято называть объектные переменные так же, как их типы, но начинать имя со строчной буквы. Поэтому предыдущий оператор мог бы выглядеть так: Circle circle=new Circle(130,120,50); Если требуется работа с несколькими объектными переменными одного типа, их принято называть в соответствии с указанным выше правилом, но добавлять порядковый номер. Следующие строки программного кода создают два независимых объекта с одинаковыми начальными параметрами: Circle circle1=new Circle(130,120,50); Circle circle2=new Circle(130,120,50); С помощью объектных переменных осуществляется доступ к полям данных или методам объекта: сначала указывается имя переменной, затем точка, после чего пишется имя поля данных или метода. Например, если имя объектной переменной obj1, а имя целочисленного поля данных x, то присваивание ему нового значения будет выглядеть как obj1.x=5; А если имя подпрограммы show, у неё нет параметров и она не возвращает никакого значения, то её вызов будет выглядеть как obj1.show(); Методы делятся на методы объектов и методы классов . Чаще всего пользуются методами объектов. Они так называются потому, что пользуются полями данных объектов, и поэтому их можно вызывать только из самих объектов. Методы классов, напротив, не пользуются полями данных объектов, и могут работать при отсутствии объекта. Поэтому их можно вызывать как из классов, так и из объектов. Формат вызова: имяКласса.имяМетода(список параметров) или имяОбъекта. имяМетода(список параметров) . При задании метода класса перед его именем необходимо поставить модификатор static – “статический”. Это крайне неудачное название, пришедшее в язык Java из C++. Мы никогда не будем называть такие методы статическими, а будем называть их методами класса, как это принято в теории программирования. Точно так же переменные (поля данных) делятся на переменные объектов и переменные классов . При задании переменной класса перед её именем необходимо поставить модификатор static. Переменные класса, как и методы класса, можно вызывать как из классов, так и из объектов. Формат вызова: имяКласса.имяПеременной или имяОбъекта.имяПеременной . Не следует путать классы, объекты и объектные переменные. Класс – это тип, то есть описание того, как устроена ячейка памяти, в которой будут располагаться поля данных объекта. Объект – это содержимое данной ячейки памяти. А в переменной объектного типа содержится адрес объекта, то есть адрес ячейки памяти. Сказанное относится только к языкам с динамической объектной моделью, каким, в частности, является Java. В C++ это не так. Как уже было сказано, кроме полей данных в классе описываются методы. Несмотря на схожесть задания в классе полей и методов их реальное размещение во время работы программы отличается. Методы не хранятся в объектах, но объекты могут их вызывать. Каждый объект имеет свой комплект полей данных – “носит свои данные с собой”. Если имеется сотня объектов одного типа, то есть являющихся экземплярами одного и того же класса, в памяти компьютера будет иметься сотня ячеек памяти, устроенных так, как это описано в классе. Причём у каждого объекта значения этих данных могут быть свои. Например, если объект является окружностью, отрисовываемой на экране, у каждой окружности будет свой набор координат, радиусов и цветов. Если мы будем отрисовывать окружности с помощью метода show(), нет необходимости в каждом объекте хранить код этого метода – он для всех ста объектов будет одним и тем же. Поэтому методы не хранятся в объектах – они хранятся в классах. Класс – более общая сущность, чем объект, и до того, как во время работы программы в памяти компьютера будет создан объект, сначала должен быть загружен в память соответствующий ему класс. В Java имеется возможность создавать переменные типа “класс”, и с их помощью обращаться к классам таким образом, как будто это объекты особого рода. Но, в отличие от обычных объектов, такие “объекты” не могут существовать в нескольких экземплярах, и правила работы с ними принципиально отличаются от работы с объектами. Такие сущности называются метаобъектами . Объявление переменных может осуществляться либо в классе, либо в методе. В первом случае мы будем говорить, что переменная является полем данных объекта, или глобальной переменной. Во втором – что она является локаль ной переменной. 2.2. Работа со ссылочными переменными. Сборка мусора Объекты, как мы уже знаем, являются экземплярами ссылочных типов. Работа со ссылочными переменными имеет специфику, принципиально отличающую её от работы с переменными примитивного типа. В каждой переменной примитивного типа содержится своё значение, и изменение этого значения не влияет на те значения, которые можно получить с помощью других переменных. Причём имя переменной примитивного типа можно рассматривать как имя ячейки с данными, и у такой ячейки может быть только одно имя, которое не может меняться по ходу работы программы. Для ссылочных переменных это не так. Переменные ссылочного типа содержат адреса данных, а не сами данные. Поэтому присваивания для таких переменных меняют адреса, но не данные. Кроме того, из-за этого под них выделяется одинаковое количество памяти независимо от типа объектов, на которые они ссылаются. А имена ссылочных переменных можно рассматривать как псевдонимы имён ячеек с данными – у одной и той же ячейки с данными может быть сколько угодно псевдонимов, так как адрес одной и той же ячейки можно копировать в произвольное число переменных соответствующего типа. И все они будут ссылаться на одну и ту же ячейку с данными. В Java ссылочные переменные используются для работы с объектами: в этом языке программирования используется динамическая объектная модель , и все объекты создаются динамически, с явным указанием в программе момента их создания. Это отличает Java от C++, языка со статической объектной моделью . В C++ могут существовать как статически заданные объекты, так и динамически создаваемые. Но это не преимущество, как могло бы показаться, а проблема, так как в ряде случаев создаёт принципиально неразрешимые ситуации. Следует отметить, что сами объекты безымянны, и доступ к ним осуществляется только через ссылочные переменные. Часто говорят про ссылочную переменную как про сам объект, поскольку долго и неудобно произносить “объект, на который ссылается данная переменная”. Но этого по мере возможности следует избегать. Ссылочной переменной любого типа может быть присвоено значение null, означающее, что она никуда не ссылается. Попытка доступа к объекту через такую переменную вызовет ошибку. В Java все ссылочные переменные первоначально инициируются значением null, если им не назначена ссылка на объект прямо в месте объявления. Очень часто встречающаяся ошибка – попытка доступа к полю или методу с помощью ссылочной переменной, которой не сопоставлен объект. Такая ошибка не может быть обнаружена на этапе компиляции и является ошибкой времени выполнения (Run-time error). При этом приложение Java генерирует исключительную ситуацию (ошибку) попытки доступа к объекту через ссылку null с сообщением следующего вида: Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException. Последовательность действий со ссылочными переменными и объектами удобно описывать с помощью рисунков, на которых каждой такой переменной и каждому объекту сопоставлен прямоугольник – символическое изображение ячейки. Около ячейки пишется её имя, если оно есть, а внутри – значение, содержащееся в ячейке. Объект Ссылочная переменная Если переменная является ссылочной, из неё выходит стрелочка-ссылка. Она кончается на том объекте, на который указывает ссылка. Если в ссылочной переменной содержится значение null (ссылка “в никуда”, адрес==0), рисуется “висящая” короткая стрелка, у которой находится надпись “null”. Отметим, что в Java символом равенства является “==”, а не символ “=”, который используется для оператора присваивания. Если ссылка перещёлкивается, то либо создаётся новый рисунок (в книжке), либо перечёркивается крестиком прежняя стрелочка и рисуется новая (на листе бумаги или на доске). При новом перещёлкивании эта “старая” стрелка перечёркивается двумя крестиками, а “более свежая”, которая была перещёлкнута – одним крестиком, и так далее. Такая система обозначений позволяет наглядно представить, что происходит при работе со ссылками, и не запутаться в том, куда они указывают и с какими ссылками в какой момент связаны динамически создаваемые объекты. При выполнении оператора new , после которого указан вызов конструктора, динамически выделяется новая безымянная ячейка памяти, имеющая тип, соответствующий типу конструктора, а сам конструктор после окончания работы возвращает адрес этой ячейки. Если у нас в левой части присваивания стоит ссылочная переменная, то в результате ссылка “перещёлкивается” на динамически созданную ячейку. Рассмотрим этот процесс подробнее. Будем считать, что сначала в ячейке circle1 типа Circle хранится нулевой адрес (значение ссылки равно null). Будем изображать это как стрелку в никуда с надписью null. Переменная circle1 типа Circle
null После оператора circle1=new Circle(x1,y1,r1) ; в динамически выделенной безымянной ячейке памяти будет создан объект-окружность с координатами центра x1, y1 и радиусом r1 (это какие-то значения, конкретная величина которых в данном случае не имеет значения): Объект1 типа Circle circle1
Поля объекта доступны через ссылку как по чтению, так и по записи. До тех пор, пока ссылочная переменная circle1 содержит адрес Объекта1, имя circle1 является псевдонимом, заменяющим имя этого объекта. Его можно использовать так же, как имя обычной переменной в любых выражениях и операторах, не связанных с изменением адреса в переменной circle1. Поскольку между ссылочными переменными одного типа разрешены присваивания, переменная по ходу программы может сменить объект, на который она ссылается. Если circle2 также имеет тип Circle, то допустимы присваивания вида circle2= circle1; Такие присваивания изменяют адреса в ячейках ссылочных переменных, но не меняют содержания объектов. Рассмотрим следующий участок кода: Circle circle1=new Circle(x1,y1,r1) ; Circle circle2=new Circle(x2,y2,r2) ; Circle circle3; Ему соответствует следующий рисунок: Объект1 типа Circle circle1
Объект2 типа Circle circle2
circle3
null Проведём присваивание circle3=circle2; В результате получится такая картинка: Объект1 типа Circle circle1
Объект2 типа Circle circle2
circle3
Обе переменные, как circle2, так и circle3, теперь ссылаются на один и тот же объект – в них находится один и тот же Адрес2. То есть оба имени – синоним имени Объекта2. Напомним, что сам объект, как все динамически создаваемые величины, безымянный. Таким образом, circle2.x даст значение x2, точно так же, как и circle3.x. Более того, если мы изменим значение circle2.x, это приведёт к изменению circle3.x – ведь это одно и то же поле x нашего Объекта2. Рассмотрим теперь, что произойдёт при присваивании circle1=circle2; Этот случай отличается от предыдущего только тем, что переменная circle1 до присваивания уже была связана с объектом. Объект1 типа Circle
circle1
Объект2 типа Circle circle2
circle3
В результате у Объекта2 окажется сразу три ссылочные переменные, которые с ним связаны, и имена которых являются его псевдонимами в данном месте программы: circle1, circle2 и circle3. При этом программная связь с Объектом1 окажется утеряна – он занимает место в памяти компьютера, но программный доступ к нему невозможен, поскольку адрес этого объекта программой утерян. Таким образом, он является бесполезным и напрасно занимает ресурсы компьютера. Про такие ячейки памяти говорят, что они являются мусором . В Java предусмотрен механизм высвобождения памяти, занятой такими бесполезными объектами. Он называется сборкой мусора (garbage collection) и работает автоматически. Этим в фоновом режиме занимается специальная часть виртуальной Java-машины, сборщик мусора . При программировании на Java, отличие от таких языков как C/C++ или Object PASCAL, программисту нет необходимости самому заботиться о высвобождении памяти, занятой под динамически создаваемые объекты. Следует подчеркнуть, что намеренная потеря связи ссылочной переменной с ненужным уже объектом – это одно, а непреднамеренная – совсем другое. Если вы не планировали потерю связи с объектом, а она произошла, это логическая ошибка. И хотя она не приведёт к зависанию программы или её неожиданному закрытию, такая программа будет работать не так, как вы предполагали, то есть неправильно или не совсем правильно. Что иногда ещё опасней, так как ошибку можно не заметить или, если заметили, очень трудно понять её причину. 2.3. Проекты NetBeans. Пакеты. Уровни видимости классов. Импорт классов Современное программное обеспечение построено по модульному (блочному) принципу. Программы давно перестали состоять из одного файла. Поэтому вместо слова “программа” лучше употреблять слово “проект”. Тем более что термин “программа”, как уже говорилось, неоднозначен. Идеология Java подразумевает работу в компьютерных сетях и возможность подгрузки в необходимый момент через сеть требуемых классов и ресурсов, в которых нуждается программа, и которые не были до того загружены. Для обеспечения такого рода работы приложения Java разрабатываются и распространяются в виде большого числа независимых классов. Однако такой способ разработки приводит к чрезвычайно высокой фрагментации программы. Даже небольшие учебные проекты часто состоят из десятков классов, а реальные проекты – из сотен. При этом каждому общедоступному (public) классу соответствует свой файл, имеющий то же имя. Для того чтобы справиться с таким обилием файлов, в Java предусмотрено специальное средство группировки классов, называемое пакетом (package). Пакеты обеспечивают независимые пространства имён (namespaces), а также ограничение доступа к классам. Классы всегда задаются в каком-либо пакете. Пакеты могут быть вложенными с произвольным уровнем вложения (ограничивается только операционной системой и, как правило, не менее 256). Каждому пакету соответствует папка с исходными кодами соответствующих классов, при этом пакету, вложенному в другой, соответствует папка, вложенная в другую. Для того чтобы поместить класс в пакет, требуется продекларировать имя пакета в начале файла, в котором объявлен класс, в виде package имя_пакета; Кроме того, необходимо поместить исходный код класса в папку, соответствующую пакету. Если декларация имени пакета отсутствует, считается, что класс принадлежит пакету с именем default. Вложенным пакетам соответствуют составные имена. Например, если мы имеем пакет с именем pkg1, в который вложен пакет с именем pkg2, в который вложен пакет с именем pkg3, то объявление, что класс с именем MyClass1 находится в пакете pkg3, будет выглядеть как package pkg1.pkg2.pkg3; class MyClass1 { …. } Внутри фигурных скобок должно содержатся описание класса. Оно заменено многоточием. В качестве разделителя имён пакетов в программе используется точка независимо от типа операционной системы. Хотя в разных операционных системах вложенность папок будет обозначаться по-разному: в MS Windows ® : pkg1\pkg2\pkg3\ в Unix и Linux : pkg1/pkg2/pkg3/ в Mac OS : pkg1:pkg2:pkg3: При создании проекта в среде NetBeans помещение класса в пакет происходит автоматически. При декларации класса можно указывать, что он общедоступен, с помощью модификатора доступа public: public class MyClass2 { …. } В этом случае возможен доступ к данному классу из других пакетов. Если же модификатор public отсутствует, как в случае MyClass1, то доступ к классу разрешён только из классов, находящихся с ним в одном пакете. Про такие файлы говорят, что у них пакетный вариант доступа (в C++ аналогичный вид доступа называется “дружественным” - friend). В файле .java можно располагать только один общедоступный класс и произвольное число классов с пакетным уровнем видимости. Класс может использовать общедоступные (public) классы из других пакетов напрямую, с указанием полного имени класса в пространстве имён, включающего имя пакета. Например, доступ к классу MyClass2 в таком варианте осуществляется как pkg1.pkg2.pkg3.MyClass2 Для того, чтобы задать переменную объектного типа, надо до того указать её класс. В нашем случае это будет выглядеть так: pkg1.pkg2.pkg3.MyClass2 myObject; Для того чтобы отличать имена классов от имён пакетов, в Java принято имена пакетов писать только строчными буквами, имена классов начинать с заглавной буквы, а имена полей данных (в том числе имена объектных переменных) и методов начинать со строчной буквы. Если имя класса, поля данных или метода (но не пакета!) состоит из нескольких слов, каждое новое слово принято или писать с заглавной буквы. Новое слово также можно отделять от предыдущего символом подчёркивания. Таким образом, из названия javax.swing.JMenuItem понятно, что javax и swing – пакеты, а JMenuItem – имя класса. Существует способ доступа к именам из другого пакета “напрямую”, без указания каждый раз полного пути в пространстве имён. Это делается с помощью оператора import. Если мы хотим импортировать имя класса MyClass2 из пакета pkg3, то после объявления имени нашего пакета (например, mypack1), но до объявления нашего класса (например, MyClass3) пишется import pkg1.pkg2.pkg3.MyClass2; При этом в классе MyClass3 имя MyClass2 можно использовать напрямую, без указания перед ним имени пакета pkg1.pkg2.pkg3. При этом задание переменной будет выглядеть так: MyClass2 myObject; Но если мы импортируем пакеты, содержащие классы с одинаковыми именами, требуется указание полного имени класса – квалифицированного именем пакета. Если мы хотим импортировать имена всех классов из пакета, в операторе import после имени пакета вместо имени класса следует написать * . Пример: import pkg1.pkg2.pkg3.*; Заметим, что импортируются только имена файлов, находящихся точно на уровне указанного пакета. Импорта имён из вложенных в него пакетов не происходит. Например, если записать import pkg1.*; или import pkg1.pkg2.*; , то имя класса MyClass2 не будет импортировано, так как хотя он и находится внутри pkg1 и pkg2, но не непосредственно, а в пакете pkg3. Имеется одно исключение из правила для импорта: классы ядра языка Java, содержащиеся в пакете java.lang, импортируются автоматически без указания имени пакета. Пример: объявление графического объекта g, имеющего тип , может проводиться тремя способами. Во-первых, напрямую, с указанием имени пакета и класса: java.awt.Graphics g; Во-вторых, с предварительным импортом класса Graphics из пакета java.awt и последующим указанием имени этого класса без его спецификации именем пакета: import java.awt.Graphics; … Graphics g; В-третьих, с предварительным импортом всех классов (в том числе Graphics) из пакета java.awt и последующим указанием имени этого класса без его спецификации именем пакета: import java.awt.*; … Graphics g; 2.4. Базовые пакеты и классы Java В пакете java находятся следующие пакеты и классы:
Пакет javax обеспечивает поддержку новых возможностей, введённых в Java 2. В нём находятся следующие пакеты:
Пакет com.sun от фирмы Sun Microsystems в основном обеспечивает расширение возможностей пакета javax. В нём находятся следующие пакеты:
В пакете org находятся следующие пакеты, предоставляемые свободным сообществом разработчиков:
2.5. Создание в NetBeans простейшего приложения Java Создадим с помощью среды NetBeans приложение Java. Для этого запустим интегрированную среду разработки (IDE) NetBeans , и выберем в главном меню File/New Project… В открывшемся диалоге выберем General / Java Application / Next> Создание нового проекта. Шаг 1. После чего можно нажимать кнопку Finish – значения по умолчанию для начала менять не стоит. Это можно будет делать потом, когда вы освоитесь со средой разработки. Создание нового проекта. Шаг 2. На следующем рисунке показано, как выглядит редактирование исходного кода приложения в среде NetBeans. В левом верхнем окне “Projects” показывается дерево проектов. В нашем случае это дерево для проекта JavaApplication1. Это окно может быть использовано для одновременного показа произвольного числа проектов. По умолчанию все деревья свёрнуты, и нужные узлы следует разворачивать щелчком по узлу с “плюсиком” или двойным щелчком по соответствующему имени. В правом окне “Source” показывается исходный код проекта. В левом нижнем окне “Navigator” показывается список имён членов класса приложения – имена переменных и подпрограмм. Двойной щелчок по имени приводит к тому, что в окне редактора исходного кода происходит переход на то место, где задана соответствующая переменная или подпрограмма. Редактирование исходного кода приложения Рассмотрим, как выглядит сгенерированный исходный код нашего приложения Java: /* * Main.java * * Created on 21 Июнь 2006 г., 13:08 * * To change this template, choose Tools | Template Manager * and open the template in the editor. */ package javaapplication1; /** * * @author User */ public class Main { /** Creates a new instance of Main */ public Main() { } /** * @param args the command line arguments */ public static void main(String[] args) { // TODO code application logic here } } Сначала идёт многострочный комментарий /* … */ Он содержит информацию об имени класса и времени его создания. Затем объявляется, что наш класс будет находиться в пакете javaapplication1. После этого идёт многострочный комментарий /** … */ , предназначенный для автоматического создания документации по классу. В нём присутствует инструкция задания метаданных с помощью выражения @author – информация об авторе проекта для утилиты создания документации javadoc. Метаданные – это некая информация, которая не относится к работе программы и не включается в неё при компиляции, но сопровождает программу и может быть использована другими программами для проверки прав на доступ к ней или её распространения, проверки совместимости с другими программами, указания параметров для запуска класса и т.п. В данном месте исходного кода имя “User” берётся средой разработки из операционной системы по имени папки пользователя. Его следует заменить на имя реального автора, т.е. в нашем случае на “Вадим Монахов”. Далее следует объявление класса Main, который является главным классом приложения. В нём объявлены две общедоступных (public) подпрограммы. Первой из них является конструктор: public Main() { } Его имя совпадает с именем класса. Он занимается созданием объектов типа Main. Обычно такой конструктор вызывается из метода main, и с его помощью создаётся всего один объект, “олицетворяющий” собой приложение. Но, вообще говоря, таких объектов в простых программах может и не создаваться, как это и происходит в нашем случае. Все классы и объекты приложения вызываются и управляются из метода main, который объявлен далее и выглядит следующим образом: public static void main(String[] args) { } Он является методом класса, и поэтому для его работы нет необходимости в создании объекта, являющегося экземпляром класса Main. Хотя если этот объект создаётся, это происходит во время работы метода main. Метод main является главным методом приложения и управляет работой запускаемой программы. Он автоматически вызывается при запуска приложения. Параметром args этого метода является массив строк, имеющий тип String[].Это параметры командной строки, которые передаются в приложение при его запуске. Слово String означает “Строка”, а квадратные скобки используются для обозначения того, что это массив строк. После окончания выполнения метода main приложение завершает свою работу. При объявлении любого метода в Java сначала указывается модификатор видимости, указывающий права доступа к методу, затем другие модификаторы, после чего следует тип возвращаемого методом значения. Если модификатор видимости не указан, считается, что это private (читается “прайвит”)– “закрытый, частный”, не позволяющий доступ к методу из других классов. Конструкторы представляют особый случай, в них имя типа и имя метода совпадают. В Java они не считаются методами, хотя в других языках программирования такого тонкого различия не делается. Далее следует имя метода, после чего в круглых скобках идёт список параметров (аргументов), передаваемых в данный метод при его вызове. После этого в фигурных скобках идёт тело метода , то есть его реализация – пишется тот алгоритм, который будет выполняться при вызове метода. В языке Java, как и в C/C++ подпрограммы всегда являются подпрограммами-функциями, возвращающими какое-либо значение. Если надо написать подпрограмму-процедуру, в которой не надо возвращать никакого значения, в C/C++/Java пользуются подпрограммами-функциями с типом возвращаемого значения void – “пустота, пустое пространство”. Как и происходит в случае метода main. Среда NetBeans создаёт заготовку методов – в них имеется пустое тело. Для осуществления методом какой-либо деятельности следует дописать свой собственный код. Напишем традиционный пример – вывод сообщения “Привет!”. Для этого вместо комментария // TODO code application logic here (“описать тут логику работы приложения”) напишем строку вывода текста System.out.println("Привет!"); Класс System, “система”, имеет поле out, “наружу”. Это объект, предназначенный для поддержки вывода. У него есть метод println, предназначенный для вывода текста в режиме консоли. Консольный ввод-вывод ранее широко применялся в операционных системах, ориентированных на работу в режиме командной строки. При этом основным средством взаимодействия пользователей с программами служила текстовая консоль ( “пульт управления”). В ней устройством ввода служила клавиатура, а устройством вывода – окно операционной системы, обеспечивающее вывод текста в режиме пишущей машинки (системным шрифтом с буквами, имеющими одинаковую ширину). Очень много примеров программ в учебных курсах по Java ориентированы на работу в таком режиме. В настоящее время в связи с тем, что подавляющее большинство пользователей работают с программами в графическом режиме, работу в консольном режиме нельзя рассматривать как основную форму ввода-вывода. Тем более, что NetBeans позволяет без особых усилий создавать графический пользовательский интерфейс (GUI – Graphics User Interface) приложения. А консольный режим следует применять только как промежуточный, удобный в отладочном режиме как средство вывода вспомогательной информации. 2.6. Компиляция файлов проекта и запуск приложения В современных средах разработки используется два режима компиляции – compile (“скомпилировать”) и build (“построить”). В режиме “compile” происходит компиляция только тех файлов проекта, которые были изменены в процессе редактирования после последней компиляции. А в режиме “build” перекомпилируются заново все файлы. Для компиляции проекта следует выбрать в меню среды разработки Build / Build Main Project (или, что то же, клавиша <F 11 >, или на панели инструментов иконка с голубой книжкой и гаечным ключом). При этом будут заново скомпилированы из исходных кодов все классы проекта. Пункт Build / Clean and Build Main Project (или, что то же, комбинация клавиш <Shift > <F 11 >, или на панели инструментов иконка с оранжевой книжкой и веником) удаляет все выходные файлы проекта (очищает папки build и dist), после чего по новой компилируются все классы проекта. Пункт Build / Generate Javadoc for “ JavaApplication 1” запускает создание документации по проекту. При этом из исходных кодов классов проекта выбирается информация, заключённая в документационные комментарии /** … */, и на её основе создаётся гипертекстовый HTML-документ. Пункт Build / Complile “ Main . java ” (или, что то же, клавиша <F 9 >) компилирует выбранный файл проекта – в нашем случае файл Main.java, в котором хранятся исходные коды класса Main. Для того чтобы запустить скомпилированное приложение из среды разработки, следует выбрать в меню среды разработки Run / Run Main Project (или, что то же, клавиша <F 6 >, или на панели инструментов иконка с зелёным и жёлтыми треугольниками). При запуске приложение всегда автоматически компилируется (но не “строится”), так что после внесения изменений для запуска обычно достаточно нажать <F 6 >. После запуска нашего проекта в выходной консоли, которая находится в нижней части окна проекта, появится служебная информация о ходе компиляции и запуска: Информация о ходе компиляции и запуска в выходной консоли. В неё же осуществляется вывод методов System.out.print и System.out.println. Метод System.out.print отличается от метода System.out.println только тем, что в println после вывода осуществляется автоматический переход на новую строку, а в print продолжается вывод в ту же строку консоли. Поэтому вывод System.out.println("Привет!"); System.out.println("Привет!"); Даст текст Привет! Привет! а System.out.print("Привет!"); System.out.print("Привет!"); даст Привет!Привет! 2.7. Структура проекта NetBeans Рассмотрим, из каких частей состоит проект NetBeans. На рисунке показаны основные элементы, отображаемые в среде разработки. Это Source Packages (пакеты исходного кода), Test Packages (пакеты тестирования), Libraries (библиотеки) и Test Libraries (библиотеки поддержки тестирования). Ветви дерева представления элементов проекта можно разворачивать или сворачивать путём нажатия на узлы, отмеченные плюсами и минусами. Мы пока будем пользоваться только пакетами исходного кода. В компонентной модели NetBeans пакеты приложения объединяются в единую конструкцию – модуль. Модули NetBeans являются базовой конструкцией не только для создания приложений, но и для написания библиотек. Они представляют собой оболочку над пакетами (а также могут включать в себя другие модули). В отличии от библиотек Java скомпилированный модуль – это не набор большого количества файлов, а всего один файл, архив JAR (Java Archive, архив Java). В нашем случае он имеет то же имя, что и приложение, и расширение .jar : это файл JavaApplication1.jar. Модули NetBeans гораздо лучше подходят для распространения, поскольку не только обеспечивают целостность комплекта взаимосвязанных файлов, но и хранят их в заархивированном виде в одном файле, что намного ускоряет копирование и уменьшает объём занимаемого места на носителях. Отметим не очень удобную особенность NetBeans – после сохранения проекта и закрытия среды разработки не сохраняется конфигурация открытых окон и развёрнутых деревьев проекта - деревья проектов показываются в свёрнутом виде. Поэтому для того, чтобы вновь попасть в режим редактирования исходного кода нашего приложения, в окне Projects, “Проекты” (левом верхнем окне среды разработки) следует развернуть последовательность узлов JavaApplication1/Source Packages/javaapplication1/ . Это делается нажатием на плюсики в соответствующих узлах или двойным щелчком по имени узла. Затем надо сделать двойной щелчок с помощью левой кнопкой мыши по имени узла Main.java, либо с помощью щелчка правой кнопкой мыши по этому имени открыть всплывающее меню и выбрать в нём первый пункт – “Open”. Имеется и более простой способ. По умолчанию сначала открывается окно Welcome (“Привет”, “Приветствие”). Но среда разработки сохраняет список открытых окон, и в верхней части окна редактирования кода щелчком мыши можно выбрать нужное имя окна. Хотя при этом не видна структура проекта, так что первый способ во многих случаях может быть предпочтительным. Если вы открываете новый проект, старый не закрывается. И в дереве проектов видны все открытые проекты. То же относится и к списку открытых окон. Это позволяет работать сразу с несколькими проектами, например – копировать в текущий проект участки кода из других проектов. Один из открытых проектов является главным (Main Project) – именно он будет запускаться на исполнение по Run / Run Main Project . Для того, чтобы установить какой-либо из открытых проектов в качестве главного, следует в дереве проектов с помощью правой кнопкой мыши щелкнуть по имени проекта и выбрать пункт меню Set Main Project. Аналогично, для того, чтобы закрыть какой-либо из открытых проектов, следует в дереве проектов с помощью правой кнопкой мыши щелкнуть по имени проекта и выбрать пункт меню Close Project. Рассмотрим теперь структуру папок проекта NetBeans. По умолчанию головная папка проекта располагается в папке пользователя. В операционной системе. Windows® XP проект по умолчанию располагается в папке C:\Documents and Settings\ИмяПользователя\ . Дальнейшее расположение папок и файлов приведено ниже, при этом имена папок выделены жирным шрифтом, а имена вложенных папок и файлов записаны под именами их головных папок и сдвинуты относительно них вправо. build classes javaapplication1 Main.class … .class META-INF dist javadoc lib JavaApplication1jar README.TXT nbproject src javaapplication1 Main.java … .java … .form META-INF test build.xml manifest.mf - В папке build хранятся скомпилированные файлы классов, имеющие расширение .class. - В папке dist - файлы, предназначенные для распространения как результат компиляции (модуль JAR приложения или библиотеки, а также документация к нему). - В папке nbproject находится служебная информация по проекту. - В папке src - исходные коды классов. Кроме того, там же хранится информация об экранных формах (которые будут видны на экране в виде окон с кнопками, текстом и т.п.). Она содержится в XML-файлах, имеющих расширение .form. - В папке test - сопроводительные тесты, предназначенные для проверки правильности работы классов проекта. Приведём перевод файла README.TXT, находящегося в папке dist - там же, где архив JAR, предназначенный для распространения как файл приложения: ======================== ОПИСАНИЕ ВЫВОДА КОМПИЛЯЦИИ ======================== Когда Вы компилируете проект приложения Java, которое имеет главный класс, среда разработки (IDE) автоматически копирует все файлы JAR-архивов, указанные в classpath ваших проектов, в папку dist/lib. Среда разработки также автоматически прибавляет путь к каждому из этих архивов в файл манифеста приложения (MANIFEST.MF). Чтобы запустить проект в режиме командной строки, зайдите в папку dist и наберите в режиме командной строки следующий текст: java -jar "JavaApplication3.jar" Чтобы распространять этот проект, заархивируйте папку dist (включая папку lib), и распространяйте ZIP-архив. Замечания: * Если два JAR-архива, указанные в classpath ваших проектов, имеют одинаковое имя, в папку lib будет скопирован только первый из них. * Если в classpath указана папка с классами или ресурсами, ни один из элементов classpath не будет скопирован в папку dist. * Если в библиотеке, указанной в classpath, также имеется элемент classpath, указанные в нём элементы должны быть указаны в пути classpath времени выполнения проектов. * Для того чтобы установить главный класс в стандартном проекте Java, щёлкните правой кнопкой мыши в окне Projects и выберите Properties. Затем выберите Run и введите данные о названии класса в поле Main Class. Кроме того, Вы можете вручную ввести название класса в элементе Main-Class манифеста. 2.8. Создание в NetBeans приложения Java с графическим интерфейсом Экранной формой называется область, которая видна на экране в виде окна с различными элементами - кнопками, текстом, выпадающими списками и т.п. А сами эти элементы называются компонентами. Среды, позволяющие в процессе разработки приложения в интерактивном режиме размещать на формы компоненты и задавать их параметры, называются RAD-средами. RAD расшифровывается как Rapid Application Development - быстрая разработка приложений. В NetBeans и других современных средах разработки такой процесс основан на объектной модели компонентов, поэтому он называется Объектно-Ориентированным Дизайном (OOD – Object-Oriented Design). NetBeans является RAD-средой и позволяет быстро и удобно создавать приложения с развитым графическим пользовательским интерфейсом (GUI). Хотя языковые конструкции Java, позволяющие это делать, не очень просты, на начальном этапе работы с экранными формами и их элементами нет необходимости вникать в эти тонкости. Достаточно знать основные принципы работы с такими проектами. С точки зрения автора изучение того, как создавать приложения с графическим интерфейсом, весьма важно для начинающих программистов, и это следует делать с самых первых шагов по изучению Java. Во-первых, с самого начала осваивается создание полноценных приложений, которые можно использовать в полезных целях. Трудно месяцами изучать абстрактные концепции, и только став профессионалом иметь возможность сделать что-то такое, что можно показать окружающим. Гораздо интереснее и полезнее сразу начать применять полученные знания на практике. Во-вторых, такой интерфейс при решении какой-либо задачи позволяет лучше сформулировать, какие параметры надо вводить, какие действия и в какой последовательности выполнять, и что в конце концов получается. И отобразить всё это на экране: вводимым параметрам будут соответствовать пункты ввода текста, действиям – кнопки и пункты меню, результатам – пункты вывода текста. Пример открытия проекта с существующим исходным кодом. В NetBeans 5.0 имелся хороший пример GUI-приложения, однако в NetBeans 5.5 он отсутствует. Поэтому для дальнейшей работы следует скопировать аналогичный пример с сайта автора или сайта, на котором выложен данный учебный курс. Пример называется JavaApplicationGUI_example. Сначала следует распаковать zip-архив, и извлечь находящуюся в нём папку с файлами проекта в папку с вашими проектами (например, C:\Documents and Settings\User). Затем запустить среду NetBeans, если она не была запущена, и закрыть имеющиеся открытые проекты, чтобы они не мешали. После чего выбрать в меню File/Open Project, либо или на панели инструментов иконку с открывающейся фиолетовой папочкой, либо нажать комбинацию клавиш <Shift>+<Ctrl>+O. В открывшемся диалоге выбрать папку JavaApplicationGUI_example (лучше в неё не заходить, а просто установить выделение на эту папку), после чего нажать кнопку Open Project Folder. При этом, если не снимать галочку “Open as Main Project”, проект автоматически становится главным. В окне редактора исходного кода появится следующий текст: /* * GUI_application.java * * Created on 22 Июня 2006 г., 13:41 */ package java_gui_example; /** * * @author Вадим Монахов */ public class GUI_application extends javax.swing.JFrame { /** * Creates new form GUI_application */ public GUI_application() { initComponents(); } /** This method is called from within the constructor to * initialize the form. * WARNING: Do NOT modify this code. The content of this method is * always regenerated by the Form Editor. */ +Generated Code private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) { System.exit(0); } /** * @param args the command line arguments */ public static void main(String[] args) { java.awt.EventQueue.invokeLater(new Runnable() { public void run() { new GUI_application().setVisible(true); } }); } // Variables declaration - do not modify private javax.swing.JMenuItem aboutMenuItem; private javax.swing.JMenuItem catchwordMenuItem; private javax.swing.JMenuItem copyMenuItem; private javax.swing.JMenuItem cutMenuItem; private javax.swing.JMenuItem deleteMenuItem; private javax.swing.JMenu editMenu; private javax.swing.JMenuItem exitMenuItem; private javax.swing.JMenu fileMenu; private javax.swing.JMenu helpMenu; private javax.swing.JMenuBar menuBar; private javax.swing.JMenuItem openMenuItem; private javax.swing.JMenuItem pasteMenuItem; private javax.swing.JMenuItem saveAsMenuItem; private javax.swing.JMenuItem saveMenuItem; // End of variables declaration } Поясним некоторые его части. Указание пакета java_gui_example, в котором будет располагаться код класса приложения, нам уже знакомо. Декларация самого класса GUI_application в данном случае несколько сложнее, чем раньше: public class GUI_application extends javax.swing.JFrame Она означает, что задаётся общедоступный класс GUI_application, который является наследником класса JFrame, заданного в пакете swing, вложенном в пакет javax. Слово extends переводится как “расширяет” (класс-наследник всегда расширяет возможности класса-прародителя). Общедоступный конструктор GUI_application()создаёт объект приложения и инициализирует все его компоненты, методом initComponents(), автоматически генерируемом средой разработки и скрываемом в исходном коде узлом +Generated Code. Развернув узел, можно увидеть реализацию этого метода, но изменить код нельзя. Мы не будем останавливаться на том, что в нём делается. Далее следует закрытый (private) метод private void exitMenuItemActionPerformed Он будет обсуждаться чуть позже. Метод public static void main(String[] args) нам уже знаком – это главный метод приложения. Он является методом класса нашего приложения и автоматически выполняется Java-машиной при запуске приложения. В данном примере метод создаёт экранную форму приложения и делает её видимой. Для того, чтобы понять, как это делается, потребуется изучить довольно много материала в рамках данного курса. Далее следует область объявления компонентов– пунктов меню нашей формы. Она автоматически создаётся в исходном коде редактором экранных форм и недоступна для изменения в редакторе исходного кода.
Запущенное приложение. Приложение с раскрытым меню. При запуске приложения экранная форма выглядит так, как показано на рисунке. В ней уже имеется заготовка меню, которое способно разворачиваться и сворачиваться, и даже работает пункт Exit – “Выход”. При нажатии на него происходит выход из приложения. Именно за нажатие на этот пункт меню несёт ответственность оператор exitMenuItemActionPerformed. При проектировании экранной формы он назначен в качестве обработчика события – подпрограммы, которая выполняется при наступлении события. В нашем случае событием является выбор пункта меню Exit, и при этом вызывается обработчик exitMenuItemActionPerformed. Внутри него имеется всего одна строчка System.exit(0); Она вызывает прекращение выполнения метода main и выход из приложения с нулевым кодом завершения. Как правило, ненулевой код завершения возвращают при аварийном завершении приложения для того, чтобы по его значению можно было выяснить причины “вылета” программы. Нажмём закладку Design (“дизайн”) в левой верхней части редактора исходного кода. При этом мы переключимся из режима редактирования исходного кода (активна закладка Source – “исходный код”) в режим редактирования экранной формы, как это показано на рисунке. Редактирование экранной формы. Вместо исходного кода показывается внешний вид экранной формы и находящиеся на ней компоненты. Справа от окна, в котором показывается экранная форма в режиме редактирования, расположены окна Palette (“палитра”) палитры компонентов и окно Properties (“свойства”) показа и редактирования свойств текущего компонента. Свойство – это поле данных, которое после изменения значения может проделать какое-либо действие. Например, при изменении значения ширины компонента отрисовать на экране компонент с новой шириной. “Обычное” поле данных на такое не способно. Таким образом, свойство – это “умное поле данных”. Палитра компонентов предназначена для выбора типа компонента, который нужен программисту для размещения на экранной форме. Например, добавим на нашу форму компонент типа JButton (сокращение от Java Button – “кнопка Java”). Для этого щёлкнем мышью по пункту JButton на палитре и передвинем мышь в нужное место экранной формы. При попадании мыши в область экранной формы на ней появляется кнопка стандартного размера, которая передвигается вместе с мышью. Щелчок в нужном месте формы приводит к тому, что кнопка остаётся в этом месте. Вокруг неё показываются рамка и маленькие квадратики, обозначающие, что наш компонент является выделенным. Для него осуществляется показ и редактирование свойств в окне Properties. Кроме того, от выделенного компонента исходят линии, к которым идет привязка для задания положения компонента на форме. По умолчанию надписи на компонентах задаются как имя типа, после которого идёт номер компонента. Но вместо заглавной буквы, в отличие от имени типа, используется строчная. Поэтому первая кнопка будет иметь надпись jButton1, вторая – jButton2, и так далее. Такие же имена будут приобретать автоматически создаваемые в исходном коде переменные, соответствующие кнопкам. Изменить надпись на кнопке можно несколькими способами. Во-первых, сделав по ней двойной щелчок, и отредактировав текст. Во-вторых, перейдя в окно Properties, изменив значение свойства Text и нажав <Enter> для завершения ввода. В-третьих, изменив аналогичным образом свойство label. Наконец, можно в окне Properties отредактировать текст не в однострочном поле ввода значений для свойств Text или label, а открыв многострочный редактор путём нажатия на кнопку, находящуюся справа от пункта редактирования значения свойства. Однако многострочность редактора не помогает сделать надпись на кнопке многострочной. Введём на кнопке надпись “OK” – используем эту кнопку для выхода из программы. Редактирование свойств компонента Размер компонента задаётся мышью путём хватания за рамку и расширения или сужения по соответствующим направлениям. Установка на новое место – перетаскиванием компонента мышью. Некоторые свойства выделенного компонента (его размер, положение, текст) можно изменять непосредственно в области экранной формы. Однако большинство свойств просматривают и меняют в окне редактирования свойств. Оно состоит из двух столбцов: в левом показываются имена свойств, в правом – их значения. Значения, стоящие в правом столбце, во многих случаях могут быть отредактированы непосредственно в ячейках таблицы. При этом ввод оканчивается нажатием на <Enter> или выходом из редактируемой ячейки, а отменить результаты неоконченного ввода можно нажатием <Escape>. В правой части каждой ячейки имеется кнопка с надписью “…” – в современных операционных системах принято добавлять три точки в названии пунктов меню и кнопок, после нажатия на которые открывается диалоговое окно. В данном случае раскрывается окно специализированного редактора соответствующего свойства, если он существует. Если требуется просматривать и редактировать большое количество свойств компонента, бывает удобнее щёлкнуть правой кнопкой мыши по нужному компоненту и в появившемся всплывающем меню выбрать пункт “Properties”. В этом случае откроется отдельное окно редактирования свойств компонента. Можно держать открытыми одновременно произвольное количество таких окон. Булевские свойства в колонке значений свойств показываются в виде кнопок выбора checkbox – квадратиков с возможностью установки галочки внутри. Если галочки нет, значение свойства false, если есть – true. Перечислим на примере кнопки ряд некоторых важнейших свойств, которые можно устанавливать для компонентов. Многие из них относятся и к другим компонентам.
В качестве примера добавим всплывающую подсказку для нашей кнопки: введём текст “Эта кнопка предназначена для выхода из программы” в поле, соответствующее свойству toolTipText. К сожалению, подсказка может быть только однострочной – символы перевода на новую строку при выводе подсказки игнорируются, даже если они заданы в строке программным путём. Наконец, зададим действие, которое будет выполняться при нажатии на кнопку – обработчик события (event handler) нажатия на кнопку. Для этого сначала выделим кнопку, после чего щёлкнем по ней правой кнопкой мыши, и в появившемся всплывающем меню выберем пункт Events/Action/actionPerformed. Назначение обработчика события Events означает “События”, Action – “Действие”, actionPerformed – “выполненное действие”. После этого произойдёт автоматический переход в редактор исходного кода, и там появится заготовка обработчика события: private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { // TODO add your handling code here: } Аналогичный результат можно получить и более быстрым способом – после того, как мы выделим кнопку в окне редактирования формы (Design), в окне Navigator показывается и выделяется имя этой кнопки. Двойной щелчок по этому имени в окне навигатора приводит к созданию заготовки обработчика события. Рядом с обработчиком jButton1ActionPerformed будет расположен уже имеющийся обработчик события, срабатывающий при нажатии на пункт меню “Выход”: private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) { System.exit(0); } Заменим в нашем обработчике события строку с комментарием на код, вызывающий выход из программы: private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { System.exit(0); } Теперь после запуска нашего приложения подведение курсора мыши к кнопке приведёт к появлению всплывающей подсказки, а нажатие на кнопку – к выходу из программы. Часто встречающийся случай – показ сообщения при наступлении какого-либо события, например – нажатия на кнопку. Этом случае вызывают панель с сообщением: javax.swing.JOptionPane.showMessageDialog(null,"Меня нажали"); Если классы пакета javax.swing импортированы, префикс javax.swing при вызове не нужен. На этапе редактирования приложения внешний вид его компонентов соответствует платформе. Однако после запуска он становится совсем другим, поскольку по умолчанию все приложения Java показываются в платформо-независимом виде.: Внешний вид запущенного приложения с платформо-независимым пользовательским интерфейсом, задаваемым по умолчанию Кроме того, наше приложение появляется в левом верхнем углу экрана, а хотелось бы, чтобы оно появлялось в центре. Для того, чтобы показать приложение в платформо-ориентированном виде (то есть в том виде, который использует компоненты и настройки операционной системы), требуется изменить код конструктора приложения, вставив перед вызовом метода initComponents задание типа пользовательского интерфейса (User’s Interface, сокращённо UI): import javax.swing.*; import java.awt.*; ... public GUI_application() { try{ UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); }catch(Exception e){}; initComponents(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Dimension frameSize = getSize(); setLocation(new Point( (screenSize.width-frameSize.width)/2, (screenSize.height-frameSize.width)/2 ) ); } Внешний вид запущенного приложения с платформо-ориентированным пользовательским интерфейсом в операционной системе Windows ® XP Код, следующий после вызова initComponents(), предназначен для установки окна приложения в центр экрана. Имеется возможность задания ещё одного платформо-независимого вида приложения – в стиле Motiff, используемого в операционной системе Solaris® . Для установки такого вида вместо вызова UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() Следует написать UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel"); Внешний вид запущенного приложения с платформо-независимым пользовательским интерфейсом в стиле Motiff Использованные конструкции станут понятны читателю после изучения дальнейших разделов методического пособия. Для того, чтобы не запутаться в разных проектах и их версиях, особенно с учётом того, что учебные проекты бывает необходимо часто переносить с одного компьютера на другой, следует серьёзно отнестись к ведению проектов. Автором в результате многолетней практики работы с разными языками и средами программирования выработана следующая система (откорректированная в применении к среде NetBeans): · Под каждый проект создаётся папка с названием проекта. Будем называть её папкой архива для данного проекта. Названия используемых папок могут быть русскоязычными, как и имена приложений и файлов. · При создании нового проекта среда разработки предлагает ввести имя папки, где его хранить - следует указать имя папки архива. Кроме того, предлагается ввести имя проекта. Это имя будет использовано средой NetBeans для создания папки проекта, так и для названия вашего приложения. Для того, чтобы облегчить работу с вашим приложением в разных странах, рекомендуется делать это название англоязычным. В папке проекта среда разработки автоматически создаст систему вложенных папок проекта и все его файлы. Структура папок проектов NetBeans была описана ранее. · Если берётся проект с существующим исходным кодом, его папка копируется в папку нашего архива либо вручную, либо выбором соответствующей последовательности действий в мастере создания проектов NetBeans. · При получении сколько-нибудь работоспособной версии проекта следует делать его архивную копию. Для этого в открытом проекте в окне “Projects” достаточно щелкнуть правой кнопкой мыши по имени проекта, и в появившемся всплывающем меню выбрать пункт “Copy Project”. Откроется диалоговая форма, в которой предлагается автоматически образованное имя копии – к первоначальному имени проекта добавляется подчёркивание и номер копии. Для первой копии это _1, для второй _2, и так далее. Причём головная папка архива по умолчанию остаётся той же, что и у первоначального проекта. Что очень удобно, поскольку даёт возможность создавать копию всего тремя щелчками мышки без набора чего-либо с клавиатуры. Создание рабочей копии проекта Скопированный проект автоматически возникает в окне “Projects”, но не становится главным. То есть вы продолжаете работать с прежним проектом, и все его открытые окна сохраняются. Можно сразу закрыть новый проект – правой кнопкой мыши щёлкнуть по его имени, и в появившемся всплывающем меню выбрать пункт “Close Project”. Для чего нужна такая система ведения проектов? Дело в том, что у начинающих программистов имеется обыкновение разрушать результаты собственного труда. Они развивают проект, не сохраняя архивов. Доводят его до почти работающего состояния, после чего ещё немного усовершенствуют, затем ещё – и всё перестаёт работать. А так как они вконец запутываются, восстановить работающую версию уже нет возможности. И им нечего предъявить преподавателю или начальнику! Поэтому следует приучиться копировать в архив все промежуточные версии проекта, более работоспособные, чем уже сохранённые в архив. В реальных проектах трудно запомнить все изменения, сделанные в конкретной версии, и, что важнее, все взаимосвязи, вызвавшие эти изменения. Поэтому даже опытным программистам время от времени приходится констатировать: “Ничего не получается!” И восстанавливать версию, в которой ещё не было тех нововведений, которые привели к путанице. Кроме того, часто бывает, что новая версия в каких-то ситуациях работает неправильно. И приходится возвращаться на десятки версий назад в поисках той, где не было таких “глюков”. А затем внимательно сравнивать работу двух версий, выясняя причину неправильной работы более поздней версии. Или убеждаться, что все предыдущие версии также работали неправильно, просто ошибку не замечали. 2.11. Редактирование меню экранной формы Имеются случаи, когда процесс редактирования компонентов несколько отличается от описанного выше. Например, для главного меню экранной формы и для всплывающих меню. Рассмотрим, как изменить текст пунктов меню формы с английского на русский. Если щёлкнуть мышью по какому-либо пункту (item) меню, в окне редактора свойств появятся значения свойств этого пункта. И мы легко сменим “File” на “Файл”, “Edit” на “Правка”, “Help” на “Справка”. Для того, чтобы без компиляции и запуска программы посмотреть, как будет выглядеть наша экранная форма, можно нажать иконку Preview Design (третья по счёту после закладки Design в окне редактирования экранной формы). Но вложенные пункты меню, появляющиеся при выборе любого из пунктов верхнего уровня, так отредактировать невозможно. Они редактируются немного другим путём. При переходе в режим дизайна, а также в этом режиме при щелчке в области экранной формы, в левом нижнем окне (Inspector - “инспектор компонентов”) среды разработки появляется список компонентов экранной формы. Навигатор позволяет просматривать деревья вложенности различных элементов проекта. Сама экранная форма является экземпляром класса JFrame (от Java Frame – “окно, кадр”, предоставляемое языком Java). В окне инспектора после схематического изображения компонента и имени соответствующей ему переменной в квадратных скобках указывается тип компонента. Развернём узел для нашей формы типа JFrame, а также вложенные узлы menuBar типа JMenuBar и fileMemu типа JMenu.
Окно инспектора компонентов Развёрнутое дерево вложенности Мы увидим имена переменных, соответствующих всем пунктам меню, вложенным в файловое меню: openMenuItem, saveMenuItem, saveAsMenuItem, exitMenuItem. Щелчок по имени openMenuItem в окне инспектора компонентов приведёт к тому, что в окне редактирования свойств появятся значения свойств данного пункта меню. В поле Text заменим слово “Open” на “Открыть”. Затем перейдём на пункт saveMenuItem, и так далее. В результате получим экранную форму с пунктами меню на русском языке. Рассмотрим теперь создание всплывающего меню, появляющегося при щелчке по какому-либо компоненту нашей формы. Качестве примера назначим всплывающее меню кнопке выхода. Для других компонентов процесс будет абсолютно аналогичным.
Новый узел jPopupMenu 1 Содержание узла В режиме дизайна (закладка Design) выберем мышью в палитре компонентов (окно Palette в правом верхнем окне) компонент JPopupMenu, и перетащим его на экранную форму. Он там не появится, но в окне инспектора компонентов в дереве Other Components возникнет новый узел jPopupMenu1[JPopupMenu]. Если щёлкнуть по узлу, окажется, что кроме самого компонента jPopupMenu1 в нём ничего нет. Щёлкнем правой кнопкой мыши по этому узлу, и в появившемся всплывающем меню выберем Add/JMenuItem. Создание нового пункта всплывающего меню После этого в дереве jPopupMenu1 возникнет узел jMenuItem1[JMenuItem], и в редакторе свойств компонентов можно задать значение свойству Text данного компонента. Введём текст “Выйти из программы”.
Узел jMenuItem 1 Свойства jMenuItem 1 Далее уже известным нам способом зададим обработчик нажатия на этот пункт меню – выберем во всплывающем меню, возникающем при щелчке правой кнопкой мыши по имени jMenuItem1 в окне Inspector, пункт Events/ Action/ ActionPerformed. А в обработчике напишем оператор выхода из программы System.exit(0); Мы пока только создали всплывающее меню, которое доступно в нашей форме, но ещё не назначили его никакому компоненту. Для того, чтобы назначить меню jPopupMenu1 кнопке JButton1, выделим её, и в редакторе свойств компонентов в пункте componentPopupMenu нажмём мышью стрелку вниз, разворачивающую выпадающий список. Кроме значения <none>, назначенного по умолчанию этому свойству для каждого компонента, там имеется имя jPopupMenu1. Его мы и выберем. Теперь всплывающее меню, состоящее из одного пункта “ Выйти из программы ”, появится при щелчке правой кнопкой мыши по кнопке. Добавление других пунктов меню и назначение им обработчиков событий проводится абсолютно так же, как для jMenuItem1. Пусть мы хотим создать в нашем проекте новый класс. Для этого щёлкнем правой кнопкой мыши по имени нашего пакета, и выберем в появившемся всплывающем окне пункт New/ Java Class… Создание нового класса. Шаг 1. Создание нового класса. Шаг 2. Появится диалоговое окно создания нового класса Java. В нём следует задать имя создаваемого класса, заменив имя по умолчанию. Зададим имя Figure. В качестве пакета, в котором расположен класс, будет автоматически задан пакет нашего приложения (если первоначальный щелчок правой клавишей был по имени этого пакета). Кроме описанной выше процедуры для создания нового класса можно воспользоваться мастером создания нового класса в главном меню среды NetBeans (File/New File…/Java Classes/Next>). В результате появится то же диалоговое окно, но в выпадающем списке придётся выбрать имя пакета . После нажатия на кнопку Finish (“закончить”) в редакторе исходного кода появляется заготовка класса. Заготовка нового класса. Если в класс требуется добавить метод, поле данных или конструктор, можно это делать вручную. Но удобнее добавлять методы с помощью среды разработки. Добавление в класс метода.Шаг 1. Щелчок правой клавиши мышки в области надписи Methods и выбор пункта Add Method… всплывающего окна приводит к появлению диалога, в котором можно путём установки галочек и выбора пунктов выпадающего меню задавать нужные параметры метода . Добавление в класс метода. Шаг 2. Аналогичным образом добавляются новые поля (Fields) и конструкторы (Constructors), но щелчок правой клавишей мыши должен делаться в области надписей Fields или Constructors. 2.13. Документирование исходного кода в Java Одной из важнейших частей написания программного обеспечения является документирование создаваемого кода. В Java для этих целей применяется средство, обеспечивающее поддержку на уровне синтаксиса языка программирования – специализированные комментарии. Они начинаются с комбинации символов /** и заканчиваются комбинацией символов */ Часть комментариев автоматически создаёт среда разработки. Пример: /** * Creates new form GUI_application */ Средством обработки внедрённых в исходный код комментариев и создания для класса справочных HTML-файлов является инструмент javadoc, входящий в состав JDK. Но в среде NetBeans удобнее пользоваться вызовом через главное меню: Build/Generate Javadoc for “…”. Документационные комментарии бывают для: · Пакетов (пока не функционируют). · Классов. · Интерфейсов. · Пользовательских типов-перечислений (на уровне пакетов пока не функционируют, но можно использовать для типов, заданных в классах). · Методов. · Переменных. Документационные комментарии пишутся непосредственно перед заданием соответствующей конструкции – пакета, класса, интерфейса, типа-перечисления, метода или переменной. Следует учитывать, что по умолчанию документация создаётся только для элементов, имеющих уровень видимости public или protected. Пример фрагмента кода с документационными комментариями: /** * Пример приложения Java с документационными комментариями <br> * В приложении заданы типы-перечисления Monthes и Spring и показано, * как с ними работать. * Кроме того, дан пример использования класса из другого пакета. * @see enumApplication.Monthes Информация о типе-перечислении Monthes * @see enumApplication.Spring * @see enumApplication#m1 * @version Версия 0.1 Первоначальная версия, проверено при компиляции * в среде NetBeans 5.5 * @author Вадим Монахов */ public class enumApplication extends javax.swing.JFrame { int i=1; /** * Spring - задаёт перечисление из 3 весенних месяцев года: march,apr,may. * <ul> * <li>march * <li>apr * <li>may * </ul> * Идентификатор для марта записан отличающимся от соответствующего * идентификатора в перечислении Monthes, а для апреля и мая записаны так * же - чтобы подчеркнуть, что их пространства имён независимы. */ public enum Spring {march,apr,may}; /** * Monthes - задаёт перечисление из 12 месяцев года: <br> * jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec <br> * (январь, февраль и т.д.) */ public enum Monthes {jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec}; Spring spr1= Spring.apr, spr2; /** *Переменная, иллюстрирующая работу с перечислениями */ public Monthes m1,m2=Monthes.mar,m3; Имеется два типа кода внутри блока документационного комментария – HTML-текст и метаданные (команды документации, начинающиеся с символа @ ). Если пишется обычный текст, он рассматривается как HTML-текст, поэтому все пробелы и переносы на новую строку при показе приводятся к одному пробелу. Для того, чтобы очередное предложение при показе начиналось с новой строки, следует вставить последовательность символов <br> , называющуюся тегом HTML. Возможно использование произвольных тегов HTML, а не только тега переноса на новую строку: теги неупорядоченного списка <ul> и <li>, теги гиперссылок, изображений и т.д. В то же время не рекомендуется использовать заголовки и фреймы, поскольку это может привести к проблемам – javadoc создаёт на основе документационного кода собственную систему заголовков и фреймов. Кроме того, при преобразовании в HTML-документ из документационного кода удаляются символы “*”, если они стоят на первом значимом месте в строке (символы пробелов не являются значимыми). Для более подробного изучения тегов HTML следует читать справочную или учебную литературу по этому языку разметки документов. Соответствующие ссылки и документы можно найти, например, на сайте автора http://barsic.spbu.ru/ www/comlan/html_r.html Команды документации (символы метаданных): · @see (“смотри”) – применяется для создания в документе гиперссылок на другие комментарии. Можно использовать для любых конструкций (классов, методов и т.д. ). Формат использования: @see ИмяКласса – для класса; @see ИмяКласса.ИмяПеречисления – для типа-перечисления, заданного в классе; @see ИмяКласса#ИмяЧлена – для метода или переменной; для интерфейса – аналогично классу. При этом имя класса или интерфейса может быть либо коротким, либо квалифицировано именем пакета. · @version (“версия”) – информация о версии. Используется для классов и интерфейсов. Формат использования: @version Информация о версии в произвольной форме. · @author (“автор”) - Информация об авторе. Используется для классов и интерфейсов. Формат использования: @author Информация об авторе в произвольной форме. Может включать не только имя, но и данные об авторских правах, а также об электронной почте автора, его сайте и т.д. · @since (“начиная с”) - Информация о версии JDK, начиная с которой введён или работоспособен класс или интерфейс. Формат использования: @since Информация в произвольной форме. · @param (сокращение от parameter -“параметр”) - информация о параметре метода. Комментарий /** @param … */ ставится в месте декларации метода в списке параметров перед соответствующим параметром. Формат использования: @param ИмяПараметра Описание. · @return (“возвращает”) - информация о возвращаемом методом значении и его типе. Формат использования: @return Информация в произвольной форме. · @throws (“возбуждает исключение”) - информация об исключительных ситуациях, которые могут возбуждаться методом. Формат использования: @throws ИмяКлассаИсключения Описание. · @deprecated (“устаревшее”) - информация о том, что данный метод устарел и в последующих версиях будет ликвидирован. При попытке использования таких методов компилятор выдаёт программисту предупреждение (warning) о том, что метод устарел, хотя и компилирует проект. Формат использования: @deprecated Информация в произвольной форме. Признаком окончания команды документации является начало новой команды или окончание комментария. Пример документации, созданной для пакета, из которого взят приведённый выше фрагмент кода: Головная страница файлов документации Страница описания элементов пакета java _ enum _ pkg Страница описания класса enumApplication Обратите внимание, что в краткой сводке (summary) приводятся только начальные строки соответствующей информации по элементам пакета или класса. Полную информацию можно прочитать после перехода по гиперссылке в описании соответствующего элемента. Поэтому важно, чтобы первые 2-3 строки информации содержали самые важные сведения. 2.14. Основные компоненты пакетов swing и awt Пока мы научились работать только с формами, кнопками и всплывающими меню. Перечислим ещё ряд полезных компонентов. Во-первых, следует остановиться на том, что в палитре компонентов NetBeans предлагается три категории компонентов: из библиотеки Swing (пакет swing), библиотеки AWT (пакет awt), и категория Beans. В Sun Java Studio Enterprise имеется ещё одна категория – Layouts, “менеджеры размещения”, - компоненты, отвечающие за способ расположения и выравнивания компонентов на форме. Библиотека Swing является основной для большинства современных графических приложений Java. В ней предлагаются следующие компоненты (перечислены в том порядке, в каком они возникают в палитре компонентов):
Очень часто в приложении требуется вывести служебную информацию. В старых версиях Java для этого служил вызов System.out.println(“Текст сообщения”). В учебных проектах и при выводе отладочной информации этот метод до сих пор удобен. Но предоставлять таким образом информацию конечному пользователю представляется анахронизмом. Для выдачи пользователю информационного сообщения лучше использовать вызов JOptionPane.showMessageDialog(null,"Привет!","Заголовок сообщения", JOptionPane.INFORMATION_MESSAGE); Если требуется вывести предупреждение об ошибке, последний параметр должен иметь значение JOptionPane.ERROR_MESSAGE, другое предупреждение - JOptionPane.WARNING_MESSAGE , вопрос - JOptionPane.QUESTION_MESSAGE. Наконец, если не требуется сопровождать вопрос иконкой на диалоговой панели, параметр должен быть JOptionPane.PLAIN_MESSAGE. Библиотека компонентов AWT (Abstract Window Toolkit - Абстрактный Инструментарий графического Окна) является устаревшей по сравнению с библиотекой Swing, хотя сам пакет awt до сих пор является основой графики Java. В библиотеке AWT имеются практически те же компоненты, что и в Swing, но в меньшем количестве и в более примитивном варианте - с худшим дизайном и меньшей функциональностью. Единственный компонент AWT, у которого нет прямого аналога в Swing – компонент типа Canvas – “холст для рисования”. Он обеспечивал вывод графических примитивов. Например, следующим образом: java.awt.Graphics g=canvas1.getGraphics(); g.drawLine(10,10,100,100); В Swing для этих целей можно рисовать по любому компоненту, например, по панели, или даже по кнопке: java.awt.Graphics g=jPanel1.getGraphics(); g.drawLine(10,10,100,100); g=jButton3.getGraphics(); g.drawLine(10,10,100,100); Ещё одна категория, на которой следует остановиться, это компоненты Layout – менеджеры размещения. Разработчики Java предложили оригинальную, но очень неоднозначную идею организации расположения компонентов на форме. Вместо того, чтобы явно указывать позиции компонентов на этапе проектирования или работы программы, и использовать якоря (anchors) для привязки краёв компонентов к краям группирующего компонента, как это делается в других языках программирования, предлагается использовать тот или иной менеджер размещения. При изменении размера формы взаимное расположение компонентов будет меняться в зависимости от типа менеджера. Например, “обычное” размещение с фиксированным положением достигается с помощью размещения на форме менеджера AbsoluteLayout. В NetBeans это делается через пункт Set Layout всплывающего меню, как показано на рисунке. По умолчанию действует режим Free Design - “свободный дизайн”. Если установить менеджер размещения AbsoluteLayout, в редакторе свойств компонентов оказываются доступны свойства x и y – координаты компонентов. Использовать якоря всё же можно, но с ограниченными возможностями и только в менеджере размещения Free Design – в других менеджерах они не работают. Для использования якоря следует щёлкнуть с помощью правой клавиши мыши по компоненту, расположенному на форме (например, кнопке), и в появившемся меню выбрать пункт Anchors. Якорь привязывает компонент к соответствующей стороне формы. Выбор менеджера размещения Установка привязки к краям формы – якорей Left – привязка к левому краю формы, Right – к правому, Top- к верхнему, Bottom – к нижнему. По умолчанию менеджер сам выбирает варианты привязки, показывая их пунктирными линиями. Язык Java был создан в 1995 году как платформо-независимый язык прикладного программирования. Он очень быстро приобрёл широкую популярность, и заметно потеснил языки C и C++ в области разработки прикладного программного обеспечения. В результате стали говорить о технологии Java и о платформе Java, подчёркивая, что это больше, чем просто язык программирования. В 1998 году появилась компонентная модель Java Beans, и ряд сред разработки приложений Java стал успешно конкурировать со средами, обеспечивающими визуальное проектирование пользовательского интерфейса – Microsoft Visual BASIC и Borland Delphi. Казалось, что язык Java завоевал лидирующие позиции в области прикладного программирования. Но в 2000 году Microsoft была предложена новая технология, названная .Net, в большой степени вобравшая в себя основные черты технологии Java: динамическую объектную модель, повышенную безопасность приложений (в том числе обеспечиваемую использованием ссылок и сборщика мусора), использование виртуальной машины и платформо-независимого байтового кода. Но технология .Net имела ряд новых черт. Во-первых, вместо одного языка программирования в .Net стало возможно использование произвольного числа языков программирования. От них требовалось только, чтобы они удовлетворяли спецификации, позволяющей скомпилированным классам работать под управлением виртуальной машины, называемой в .Net Common Language Environment или Common Language Runtime – общей исполняющей средой поддержки языков программирования. Базовым языком программирования стал созданный одновременно с .Net язык C# - фактически, явившийся усовершенствованным вариантом языка Java, но несовместимый с ним как по ряду синтаксических конструкций, так и по скомпилированному коду. Во-вторых, если Java рассматривался всеми в качестве языка программирования, то технология .Net фактически создавалась как платформо-независимая часть операционной системы MS Windows® . Поэтому важной частью .Net стал набор базовых классов .Net Framework, обеспечивающий поддержку прикладного программирования в большинстве практически важных областей. В .Net основой программирования, как и в Java, служат классы. Исходный код класса, написанный на любом из языков .Net (то есть удовлетворяющий спецификации Common Language Environment), компилируется в платформо-независимый код. Этот код уже не имеет специфики языка программирования, на котором был написан, работает под управлением исполняющей среды .Net и может использоваться любыми другими классами .Net. Причём скомпилированный код класса может использоваться не только для вызовов, но и для наследования и обеспечения полиморфизма. Такие классы называются компонентами . Важно то, что для использования каким-либо приложением необходимого класса .Net Framework как при разработке приложения, так и при его запуске на компьютере пользователя нет необходимости загружать класс через Интернет или устанавливать на компьютере каким-либо другим образом. Достаточно того, чтобы был установлен свободно распространяемый пакет компонентов .Net Framework, что делается в версиях MS Windows® начиная с Windows® XP непосредственно во время установки операционной системы. Причём набор компонентов в пакете стандартизован, что обеспечивает гарантированное нахождение нужного компонента и его работоспособность. Именно эти особенности обеспечивают преимущество оболочки операционной системы по сравнению с отдельными программами и пакетами. То, что технология .Net была сделана открытой и стандартизирована ISO (International Standard Organization – Международная Организация по Стандартам) в 2004 году, безусловно, сделало её привлекательной для многих разработчиков. Появление проекта Mono с реализацией .Net под Linux, а также ряда других проектов по реализации .Net под разными операционными системами позволило этой технологии выйти за пределы семейства операционных систем одной фирмы и сделало позиции Java неконкурентоспособными в данной области. Действительно, язык программирования – это одно, а оболочка операционной системы с набором большого числа компонентов – совсем другое. Тем более, что язык Java стал одним из возможных языков .Net, то есть вошёл в эту технологию как составная часть. Как это ни удивительно, в рамках технологии Java удалось найти достойный ответ на вызов со стороны .Net . Им стала платформа NetBeans и идеология Open Source (“Открытый исходный код”). NetBeans – это технология компонентного программирования, созданная на основе модели Java Beans, о которой речь пойдёт позже. Помимо набора компонентов в состав платформы NetBeans входит свободно распространяемая среда разработки NetBeans, позволяющая создавать различные типы программ на языке Java, в том числе – с использованием компонентов NetBeans, а также создавать такие компоненты. Движение Open Source, набирающее популярность в последние годы, стремится к всеобщей открытости программного кода. При этом следует отличать два варианта открытости. Первый из них (freeware – свободно распространяемое программное обеспечение) подразумевает свободное распространение и использование программ и их исходных кодов, как правило, с единственным ограничением – сохранением открытости и свободы распространения программ и исходных кодов программных продуктов, использующих этот исходный код. Второй требует открытости исходного кода для изучения и, при необходимости, исправления ошибок, но не означает передачи авторских прав на какое-либо другое использование этого исходного кода. Среда и компоненты NetBeans распространяются на основе соглашения Sun open licence (“Открытая лицензия Sun”). Эта лицензия позволяет свободно использовать среду и компоненты NetBeans для создания программного обеспечения, как свободно распространяемого, так и коммерческого, но требует, чтобы исходные коды создаваемых программ были открыты для просмотра. Мультиплатформенность и наличие большого количества свободно распространяемых компонентов NetBeans в сочетании с качественной бесплатной средой разработки и очень широким использованием языка Java даёт возможность надеяться, что NetBeans сможет стать унифицированной оболочкой различных операционных систем. Но для серьёзного соперничества с .Net потребуется наличие стандартизированных пакетов компонентов, одной библиотеки Swing мало. Более того, необходимо, чтобы все эти пакеты входили в поставку JDK. Наличие разрозненных нестандартизированных пакетов не даст преимуществ перед конкурирующей технологией .Net. Одним из решений этой проблемы стало расширение базового набора пакетов и классов в составе JDK. Даже самый старый пакет java в новых версиях JDK усовершенствуется, не говоря уж о более новом пакете javax. Кроме того, в поставке NetBeans Enterprise Pack имеется большое число дополнительных библиотечных пакетов. - Три базовых принципа объектно-ориентированного программирования: инкапсуляция , наследование , полиморфизм . - Класс – это описание того, как устроен объект . И поля данных , и методы задаются в классах. Но при выполнении программы поля данных хранятся в объектах, а методы – в классах. Методы задают поведение объекта, а поля данных - состояние объекта. - Переменные, описываемые в классах, называются глобальными . Они задают поля данных объектов. Переменные, описываемые в методах, называются локальными . Они являются вспомогательными и существуют только во время вызова метода. - Переменные ссылочного типа содержат адреса данных, а не сами данные. Поэтому присваивания для таких переменных меняют адреса, но не данные. Все объектные типы являются ссылочными. Потеря ссылки на объект приводит к сборке мусора. - Объект создаётся с помощью вызова конструктора - специальной подпрограммы-функции, задаваемой в классе. - Методы делятся на методы объектов и методы классов . Метод объекта можно вызывать только из объекта соответствующего типа. А метод класса может работать и при отсутствии объекта, и вызываться из класса. - При декларации класса можно указывать, что он общедоступен, с помощью модификатора доступа public. В этом случае возможен доступ к данному классу из других пакетов. В файле .java можно располагать только один общедоступный класс и произвольное число классов с другим уровнем видимости. Если модификатор public отсутствует, то доступ к классу разрешён только из классов, находящихся с ним в одном пакете. Про такие файлы говорят, что у них пакетный вариант доступа. - Важнейшими пакетами являются java и javax, а также вложенные в них пакеты. Информацию о содержащихся в них элементах можно получить в среде разработки, набрав java. или javax. И прочитав появившуюся подсказку. - Все классы и объекты приложения вызываются и управляются из метода main, который имеет сигнатуру public static void main(String[] args). Он является методом класса, и поэтому для его работы нет необходимости в создании объекта, являющегося экземпляром класса. - Визуальное проектирование приложения с графическим интерфейсом пользователя (GUI) происходит в режиме Design. Как правило, основой для построения такого интерфейса служат компоненты Swing. - Документирование исходного кода в Java осуществляется с помощью специальных документационных комментариев /** Текст комментария в формате HTML */ . Также имеется ряд команд документации, начинающихся с символа @ .Утилита javadoc позволяет по документационным комментариям создавать систему HTML-страниц с документацией о пакетах и классах. - Для выдачи пользователю информационного сообщения следует использовать вызов JOptionPane.showMessageDialog(null,"Привет!","Заголовок сообщения", JOptionPane.INFORMATION_MESSAGE). Типичные ошибки:
Глава 3. Примитивные типы данных и операторы для работы с ними 3.1.Булевский (логичес кий) тип Величины типа boolean принимают значения true или false. Объявление булевских переменных: boolean a; boolean b; Использование в выражениях при присваиваниях: a=true; b=a; Булевские величины обычно используются в логических операторах и в операциях отношения. Логические операторы
Значением логического выражения являются true или false. Например, если a=true, b=true, то a && b имеет значение true. А при a=false или b=false выражение a && b принимает значение false. Для работы с логическими выражениями часто применяют так называемые таблицы истинности. В них вместо логической единицы (true) пишут 1, а вместо логического нуля (false) пишут 0. В приведённой ниже таблице указаны значения логических выражений при всех возможных комбинациях значений a и b.
Выполнение булевских операторов происходит на аппаратном уровне, а значит, очень быстро. Реально процессор оперирует числами 0 и 1, хотя в Java это осуществляется скрытым от программиста образом. Логические выражения в Java вычисляются в соответствии с так называемым укороченным оцениванием: из приведённой выше таблицы видно, что если a имеет значение false, то значение оператора a&&b будет равно false независимо от значения b. Поэтому если b является булевским выражением, его можно не вычислять. Аналогично, если a имеет значение true, то значение оператора a||b будет равно true независимо от значения b. Это операторы сравнения и принадлежности. Они имеют результат типа boolean. Операторы сравнения применимы к любым величинам a и b одного типа, а также к произвольным числовым величинам a и b, не обязательно имеющим один тип.
Про оператор instanceof будет рассказано в разделе, посвящённом динамической проверке типов. 3.2.Целые типы, переменные, константы
Для задания в тексте программы численных литерных констант типа long, выходящих за пределы диапазона чисел типа int, после написания числа следует ставить постфикс – букву L. Например, 600000000000000L. Можно ставить и строчную l, но её хуже видно, особенно – на распечатках программы (можно перепутать с единицей). В остальных случаях для всех целочисленных типов значение указывается в обычном виде, и оно считается имеющим тип int – но при присваивании число типа int автоматически преобразуется в значение типа long. Как уже говорилось, объявление переменных может осуществляться либо в классе, либо в методе. В классе их можно объявлять как без явного присваивания начального значения, так и с указанием этого значения. Если начальное значение не указано, величина инициализируется нулевым значением. Если объявляется несколько переменных одного типа, после указания имени типа разрешается перечислять несколько переменных, в том числе – с присваиванием им начальных значений. В методе все переменные перед использованием обязательно надо инициализировать – автоматической инициализации для них не происходит. Это надо делать либо при объявлении, либо до попытки использования значения переменной в подпрограмме. Попытка использования в подпрограмме значения неинициализированной переменной приводит к ошибке во время компиляции. Рассмотрим примеры задания переменных в классе. int i,j,k; int j1; byte i1,i2=-5; short i3=-15600; long m1=1,m2,m3=-100; Заметим, что после указанных объявлений переменные i,j,k,j1,i1,m2 имеют значение 0. Для i1 это неочевидно – можно подумать, что обе переменные инициализируются значением -5. Поэтому лучше писать так: byte i1=0,i2=-5; Использование в выражениях: i=5; j=i*i + 1 m1=j m2=255; m1=m1 + m2*2; Тип char в Java, как и в C/C++, является числовым , хотя и предназначен для хранения отдельных символов. Переменной символьного типа можно присваивать один символ, заключённый в одинарные кавычки, либо кодирующую символ управляющую последовательность Unicode. Либо можно присваивать числовой код символа Unicode (номер символа в кодовой таблице): char c1='a'; char c2='\u0061'; char c3=97; Все три приведённые декларации переменных присваивают переменным десятичное значение 97, соответствующее латинской букве “a”. Существуют различные кодовые таблицы для сопоставления числам символов, которые могут быть отображены на экране или другом устройстве. В силу исторических причин самой распространённой является кодовая таблица ASCII для символов латиницы, цифр и стандартных специальных символов. Поэтому в таблице Unicode им были даны те же номера, и для них коды Unicode и ASCII совпадают: 'A' имеет код 65, 'B' – 66, 'Z' – 90, 'a' – 97, 'z' - 122, '0' - 48, '1' - 49, '9' – 57, ':' – 58, ';' – 59, '<' – 60, '=' – 61, '>' – 62, и так далее. К сожалению, с переменными и значениями символьного типа можно совершать все действия, которые разрешено совершать с целыми числами. Поэтому символьные значения можно складывать и вычитать, умножать и делить не только на “обычные” целые величины, но и друг на друга! То есть присваивание с1='a'*'a'+1000/'b' вполне допустимо, несмотря на явную логическую абсурдность. Константами называются именованные ячейки памяти с неизменяемым содержимым. Объявление констант осуществляется в классе, при этом перед именем типа константы ставится комбинация зарезервированных слов public и final: public final int MAX1=255; public final int MILLENIUM=1000; Константами можно пользоваться как переменными, доступными только по чтению. Попытка присвоить константе значение с помощью оператора присваивания “=” вызывает ошибку компиляции. Для того чтобы имена констант были хорошо видны в тексте программы, их обычно пишут в верхнем регистре (заглавными буквами). Имеется следующее правило хорошего тона: никогда не используйте одно и то же числовое литерное значение в разных местах программы, вместо этого следует задать константу и использовать её имя в этих местах. Например, мы пишем программу обработки текста, в которой во многих местах используется литерная константа 26 – число букв в английском алфавите. Если у нас возникнет задача модифицировать её для работы с алфавитом, в котором другое число букв (например, с русским), придётся вносить исправления в большое число мест программы. При этом нет никакой гарантии, что не будет забыто какое-либо из необходимых исправлений, или случайно не будет “исправлено” на новое значение литерное выражение 26, не имеющее никакого отношения к числу букв алфавита. Оптимистам, считающим, что проблема решается поиском числа 26 по файлам проекта (в некоторых средах разработки такое возможно), приведу пример, когда это не поможет: символ подчёркивания “_” иногда (но не всегда) считается буквой. Поэтому в некоторых местах программы будет фигурировать число 27, а в некоторых 26. И исправления надо будет вносить как в одни, так и в другие места. Поэтому работа перестаёт быть чисто технической – при изменениях требуется вникать в смысл участка программы, и всегда есть шанс ошибиться. Использование именованных констант полностью решают эту проблему. Если мы введём константу CHARS_COUNT=26, то вместо 27 будет записано CHARS_COUNT +1, и изменение значения CHARS_COUNT повлияет правильным образом и на это место программы. То есть достаточно внести одно изменение, и гарантированно получить правильный результат для всех мест программы, где необходимы исправления. 3.3.Основные операторы для работы с целочисленными величинами Все перечисленные ниже операторы действуют на две целочисленные величины, которые называются операндами. В результате действия оператора возвращается целочисленный результат. Во всех перечисленных далее примерах i и j обозначают целочисленные выражения, а v – целочисленную переменную.
Также имеются важные методы классов Integer и Long, обеспечивающие преобразование строкового представления числа в целое значение: Integer.parseInt(строка ) Long.parseLong(строка ) Например, если в экранной форме имеются текстовые пункты ввода jTextField1 и jTextField2, преобразование введённого в них текста в числа может проводиться таким образом: int n=Integer.parseInt(jTextField1.getText()); long n1=Long.parseLong(jTextField2.getText()); Функции Integer.signum(число ) и Long.signum(число ) возвращают знак числа – то есть 1 если число положительно, 0, если оно равно 0, и -1 если число отрицательно. Кроме того, в классах Integer и Long имеется ряд операторов для работы с числами на уровне их битового представления. Классы Integer и Long являются так называемыми оболочечными классами (wrappers), о них речь пойдёт чуть дальше. 3.4 .Вещественные типы и класс Math
Математические функции, а также константы “пи” (Math.PI) и “е” (Math.E ) заданы в классе Math, находящемся в пакете java.lang . Для того, чтобы их использовать, надо указывать имя функции или константы, квалифицированное впереди именем класса Math. Например, возможны вызовы Math.PI или Math.sin(x). При этом имя пакета java.lang указывать не надо – он импортируется автоматически. К сожалению, использовать математические функции без квалификатора Math, не получается, так как это методы класса. Константы в классе Math заданы так: public static final double E = 2.7182818284590452354; public static final double PI = 3.14159265358979323846; Модификатор static означает, что это переменная класса; final означает, что в классе-наследнике переопределять это значение нельзя. В приведённой ниже таблицы величины x , y , angdeg , angrad имеют тип double, величина a – тип float, величины m , n – целые типов long или int. Математические функции возвращают значения типа double, если примечании не указано иное.
Также имеются методы оболочечных классов Float и Double, обеспечивающие преобразование строкового представления числа в значение типа Float или Double: Float.parseFloat(строка ) Double.parseDouble(строка ) Например, если в экранной форме имеются текстовые пункты ввода jTextField1 и jTextField2, преобразование введённого в них текста в числа может проводиться таким образом: float f1= Float.parseFloat(jTextField1.getText()); double d1= Double.parseDouble(jTextField2.getText()); Иногда вместо класса Math ошибочно пытаются использовать пакет java.math, в котором содержатся классы BigDecimal, BigInteger, MathContext, RoundingMode. Класс BigDecimal обеспечивает работу с десятичными числами с произвольным числом значащих цифр – в том числе с произвольным числом цифр после десятичной точки. Класс BigInteger обеспечивает работу с целыми числами произвольной длины. Классы MathContext и RoundingMode являются вспомогательными, обеспечивающими настройки для работы с некоторыми методами классов BigDecimal и BigInteger. 3.5 .Правила явного и автоматического преобразования типа при работе с числовыми величинами Компьютер может проводить на аппаратном уровне целочисленные математические вычисления только с величинами типа int или long. При этом операнды в основных математических операциях ( + , - , * , / , % ) должны быть одного типа. Поэтому в тех случаях, когда операнды имеют разные типы, либо же типы byte, short или char, действуют правила автоматического преобразования типов: для величин типа byte, short или char сначала происходит преобразование в тип int, после чего производится их подстановка в качестве операндов. Если же один из операндов имеет тип long, действия производятся с числами типа long, поскольку второй операнд автоматически преобразуется к этому типу. Аналогично, при работе с вещественными величинами в Java возможна работа на аппаратном уровне только с операндами типов float и double. При этом операнды в основных математических операциях должны быть одного типа. Если один из операндов имеет тип double, а другой float, действия производятся с числами типа double, поскольку операнд типа float автоматически преобразуется к типу double. Если один из операндов целочисленный, а другой вещественный, сначала идёт преобразование целочисленного операнда к такому же вещественному типу, а потом выполняется оператор. Рассмотрим теперь правила совместимости типов по присваиванию. Они просты: диапазон значений типа левой части не должен быть уже, чем диапазон типа правой. Поэтому в присваиваниях, где тип в правой части не умещается в диапазон левой части, требуется указывать явное преобразование типа . Иначе компилятор выдаст сообщение об ошибке с не очень адекватной диагностикой “possible loss of precision” (“возможная потеря точности”). Для явного преобразования типа в круглых скобках перед преобразуемой величиной ставят имя того типа, к которому надо преобразовать. Например, пусть имеется double d=1.5; Величину в можно преобразовать к типу float таким образом: (float)d Аналогично, если имеется величина f типа float, её можно преобразовать в величину типа double таким образом: (double)f Во многих случаях к явному преобразованию типов прибегать не нужно, так как действует автоматическое преобразование, если переменной, имеющий тип с более широким диапазоном изменения, присваивается выражение, имеющее тип с более узким диапазоном изменения. Например, для вещественных типов разрешено присваивание только в том случае, когда слева стоит вещественная переменная более широкого диапазона, чем целочисленная или вещественная – справа. Например, переменной типа double можно присвоить значение типа float, но не наоборот. Но можно использовать преобразование типов для того, чтобы выполнить такое присваивание. Например: double d=1.5; float f=(float)d; Другие примеры допустимых присваиваний: byte byte0=1; //-128..127 short short0=1;//- 32768.. 32767 char char0=1;//0.. 65535 int int0=1; //- 2.147483648E9.. 2.147483647E9 long long0=1;//-9.223E18.. 9.223E18 float float0=1;// ±(1.4E-45..3.402E38) double double0=1;// ±(4.9E-324..1.797E308 ) short0=byte0; byte0=(byte )short0; char0=(char )short0; int0=short0; int0=char0; char0=(char )int0; short0=(short )int0; long0=byte0; byte0=(byte )long0; long0=char0; long0=int0; int0=(int )long0; float0=byte0; float0=int0; float0=(float )double0; double0=float0; 3.6. Оболочечные классы. Упаковка (boxing) и распаковка (unboxing) В ряде случаев вместо значения примитивного типа требуется объект. Например, для работы со списками объектов. Это связано с тем, что работа с объектами в Java может быть унифицирована, поскольку все классы Java являются наследниками класса Object, а для примитивных типов этого сделать нельзя. Для таких целей в Java каждому примитивному типу сопоставляется объектный тип, то есть класс. Такие классы называются оболочечными (class wrappers). В общем случае они имеют те же имена, что и примитивные типы, но начинающиеся не со строчной, а с заглавной буквы. Исключение почему-то составляют типы int и char, для которых имена оболочечных классов Integer и Character.
Внимание! Класс Character несколько отличается от остальных оболочечных числовых классов потому, что тип char “не вполне числовой”. Основное назначение оболочечных классов – создание объектов, являющихся оболочками над значениями примитивных типов. Процесс создание такого объекта (“коробки” - box) из значения примитивного типа называется упаковкой (boxing), а обратное преобразование из объекта в величину примитивного типа – распаковкой (unboxing). Оболочечные объекты (“обёртки” для значения примитивного типа) хранят это значение в поле соответствующего примитивного типа, доступном по чтению с помощью функции имяТипа Value(). Например, метода byteValue() для объекта типа Byte. Но во многих случаях можно вообще не обращать внимания на отличие переменных с типом оболочечных классов от переменных примитивных типов, так как упаковка и распаковка при подстановке такого объекта в выражение происходит автоматически, и объект оболочечного типа в этих случаях внешне ведёт себя как число. Таким образом, если нам необходимо хранить в объекте числовое значение, следует создать объект соответствующего оболочечного типа. Например, возможны такие фрагменты кода: Integer obj1=10; int i1= obj1*2; Byte b=1; obj1=i1/10; b=2; Но не следует забывать, что при операциях упаковки-распаковки происходят внешне невидимые дополнительные операции копирования значений в промежуточные буферные ячейки, поэтому соответствующие вычисления несколько медленнее операций с примитивными типами и требуют несколько больше памяти. Поэтому в критических к быстродействию и занимаемым ресурсам местам программы их использование нежелательно. С другой стороны, автоматическая упаковка-распаковка (она появилась в пятой версии JDK) во многих случаях заметно упрощает программирование и делает текст программы более читаемым. Так что для участков программы, некритичных к быстродействию, ею вполне можно пользоваться. Помимо создания объектов оболочечные классы имеют ряд других полезных применений. Например, в числовых оболочечных классах хранятся константы, с помощью которых можно получить максимальные и минимальные значения: Byte.MIN_VALUE, Byte.MAX_VALUE, Float.MIN_VALUE, Float.MAX_VALUE, Double.MIN_VALUE, Double.MAX_VALUE и т.п. В оболочечных классах также имеются методы классов, то есть такие, которые могут работать в отсутствии объекта соответствующего типа – для их вызова можно пользоваться именем типа. Например, как мы уже знаем, имеются методы Byte .parseByte(строка ) Short.parseShort(строка ) Integer.parseInt(строка ) Long.parseLong(строка ) Float.parseFloat(строка ) Double.parseDouble(строка ) Они преобразуют строку в число соответствующего типа. Вызовы Byte.valueOf(строка ) Short.valueOf(строка ) Integer.valueOf(строка ) Long.valueOf (строка ) Float.valueOf (строка ) Double.valueOf (строка ) аналогичны им, но возвращают не числовые значения, а объекты соответствующих оболочечных типов. Примеры использования оболочечных классов: int n1=Integer.MAX_VALUE; double d1= Double.MIN_VALUE; Отметим, что присваивание double d2= Double.parseDouble(jTextField1.getText()); будет работать совершенно так же, как double d2= Double.valueOf(jTextField1.getText()); несмотря на то, что во втором случае методом valueOf создаётся объект оболочечного типа Double. Поскольку в левой части присваивания стоит переменная типа double, происходит автоматическая распаковка, и переменной d2 присваивается распакованное значение. Сам объект при этом становится мусором – программная связь с ним теряется, и он через некоторое время удаляется из памяти системой сборки мусора. В данном случае ни быстродействие, ни объём памяти некритичны, поскольку операции взаимодействия с пользователем по компьютерным меркам очень медленные, а один объект оболочечного типа занимает пренебрежимо мало места в памяти (около сотни байт). Так что с потерями ресурсов в этом случае можно не считаться, обращая внимание только на читаемость текста программы. Поэтому автор предпочитает второй вариант присваивания: хотя он и “неоптимальный” по затрате ресурсов, но более читаем. При вычислении выражений важен приоритет операторов. Для операторов сложения, вычитания, умножения и деления он “естественный”: умножение и деление обладают одинаковым наиболее высоким приоритетом, а сложение и вычитание – одинаковым приоритетом, который ниже. Таким образом, например, a*b/c+d это то же, что ( (a*b)/c )+d Круглые скобки позволяют группировать элементы выражений, при этом выражение в скобках вычисляется до того, как участвует в вычислении остальной части выражения. То есть скобки обеспечивают больший приоритет, чем все остальные операторы. Поэтому (a+b)*c будет вычисляться так: сначала вычислится сумма a+b, после чего полученный результат будет умножен на значение c. Кроме перечисленных в Java имеется большое количество других правил, определяющих приоритеты различных операторов. Автор считает их изучение не только нецелесообразным, но даже вредным: программу следует писать так, чтобы все последовательности действий были очевидны и не могли вызвать сложностей в понимании текста программы и привести к логической ошибке. Поэтому следует расставлять скобки даже в тех случаях, когда они теоретически не нужны, но делают очевидной последовательность действий. Отметим, такие действия часто помогают заодно решить гораздо более сложные проблемы, связанные с арифметическим переполнением. Далее в справочных целях приведена таблица приоритета операторов. Ей имеет смысл пользоваться в случае анализа плохо написанных программ, когда из текста программы неясна последовательность операторов.
3. 8.Типы-перечисления (enum) Иногда требуется использовать элементы, которые не являются ни числами, ни строками, но ведут себя как имена элементов и одновременно обладают порядковыми номерами. Например, названия месяцев или дней недели. В этих случаях используют перечисления . Для задания типа какого-либо перечисления следует написать зарезервированное слово enum (сокращение от enumeration – “перечисление”), после которого имя задаваемого типа, а затем в фигурных скобках через запятую элементы перечисления. В качестве элементов можно использовать любые простые идентификаторы (не содержащие квалификаторов вида имя1.имя2 ). Тип-перечисление обязан быть глобальным – он может задаваться либо на уровне пакета, либо в каком-либо классе. Но его нельзя задавать внутри какого-либо метода. Элементы перечисления могут иметь любые имена, в том числе совпадающие в разных перечислениях или совпадающие с именами классов или их членов – каждое перечисление имеет своё собственное пространство имён . Доступ к элементу перечисления осуществляется с квалификацией именем типа-перечисления: ИмяТипа.имяЭлемента У каждого элемента перечисления имеется порядковый номер, соответствующий его положению в наборе - нумерация начинается с нуля. Поэтому первый элемент имеет номер 0, второй элемент – номер 1, и так далее. Имеется функция ordinal(), возвращающая порядковый номер элемета в перечислении. Также имеется функция compareTo, позволяющая сравнивать два элемента перечисления - она возвращает разницу в их порядковых номерах. Строковое представление значения можно получить с помощью функции name(). Преобразование из строки в значение типа “перечисление” осуществляется с помощью функции класса valueOf, в которую передаётся строковое представление значения. Если требуется рассматривать элементы перечисления как массив, можно воспользоваться функцией values() – она возвращает массив элементов, к которым можно обращаться по индексу. Формат вызова функции такой: ИмяТипа.values() Для примера зададим типы-перечисления Monthes (“месяцы”) и Spring (“весна”), соответствующие различным наборам месяцев: enum Monthes {jan,feb,mar,apr,may,jun,jul,aug,sept,oct,nov,dec}; enum Spring { march, apr, may }; Названия месяцев мы намеренно пишем со строчной буквы для того, чтобы было понятно, что это идентификаторы переменных, а не типы. А имя марта написано по-разному в типах Monthes и Spring для того, чтобы показать независимость их пространств имён. Объявление переменных типа “перечисление” делается так же, как для всех остальных типов, при этом переменные могут быть как неинициализированы, так и инициализированы при задании: public Monthes m1 ,m2=Monthes.mar, m3; - при задании в классе общедоступных полей m1, m2 и m3, Spring spr1=Spring.apr, spr2; - при задании в методе локальной переменной или задании в классе поля spr1 с пакетным уровнем доступа. После чего возможны следующие операторы: spr2=spr1; spr1=Spring.may; System.out.println("Результат сравнения="+spr2.compareTo(Spring.march)); После выполнения этих операторов в консольное окно будет выведен текст Результат сравнения=1 , поскольку в переменной spr2 окажется значение Spring.apr , порядковый номер которого на 1 больше, чем у значения Spring.march , с которым идёт сравнение. Пусть в переменной spr2 хранится значение Spring.may. Порядковый номер значения, хранящегося в переменной, можно получить с помощью вызова spr2.ordinal() . Он возвратит число 2, так как may – третий элемент перечисления (сдвиг на 1 получается из-за того, что нумерация начинается с нуля). Строковое представление значения, хранящегося в переменной spr2, можно получить с помощью вызова spr2.name() . Он возвратит строку “may” - имя типа в возвращаемое значение не входит. Если переменная типа “перечисление” не инициализирована, в ней хранится значение null. Поэтому вызов System.out.println("spr2="+spr2); осуществлённый до присваивания переменной spr2 значения возвратит строку spr2=null А вот попытки вызовов spr2.ordinal() или spr2.name() приведут к возникновению ошибки (исключительной ситуации) с диагностикой Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException Получение значения типа Spring по номеру, хранящемуся в переменной i, осуществляется так: spr1=Spring.values()[i]; Преобразование из строки в значение типа Spring будет выглядеть так: spr1=Spring.valueOf("march"); - Величины типа boolean принимают значения true или false. - Логические операторы && -“И”, || - “ИЛИ”, ^ - “Исключающее ИЛИ”, ! – “НЕ” применимы к величинам булевского типа. Логические выражения в Java вычисляются в соответствии с укороченным оцениванием. - Операторы сравнения применимы к любым величинам a и b одного типа, а также к произвольным числовым величинам a и b, не обязательно имеющим один тип. В качестве оператора сравнения на равенство используется составной символ, состоящий из двух подряд идущих символа равенства “==”. - В Java имеются встроенные примитивные целые типы byte, short, int, long и символьный тип char, в некотором смысле также являющийся целочисленным. При этом только тип char беззнаковый, все остальные – знаковые. - Для задания в тексте программы численных литерных констант типа long, выходящих за пределы диапазона чисел типа int, после написания числа следует ставить постфикс – букву L. - Константами называются именованные ячейки памяти с неизменяемым содержимым. Объявление констант осуществляется в классе, при этом перед именем типа константы ставится комбинация зарезервированных слов public и final. - В Java имеется два встроенных примитивных вещественных типа float и double (точнее, типы чисел в формате с плавающей точкой). - Математические функции, а также константы “пи” (Math.PI) и “е” (Math.E ) заданы в классе Math, находящемся в пакете java.lang . - Целочисленные математические вычисления проводятся на аппаратном уровне только с величинами типа int или long. Для величин типа byte, short или char сначала происходит преобразование в тип int, после чего производится их подстановка в качестве операндов. Если же один из операндов имеет тип long, действия производятся с числами типа long, поскольку второй операнд автоматически преобразуется к этому типу. - При работе с вещественными величинами в Java возможна работа на аппаратном уровне только с операндами типов float и double. Если один из операндов имеет тип double, а другой float, действия производятся с числами типа double, поскольку операнд типа float автоматически преобразуется к типу double. - Если один из операндов целочисленный, а другой вещественный, сначала идёт преобразование целочисленного операнда к такому же вещественному типу, а потом выполняется оператор. - В Java каждому примитивному типу сопоставляется объектный тип, то есть класс. Такие классы называются оболочечными (class wrappers). В общем случае они имеют те же имена, что и примитивные типы, но начинающиеся не со строчной, а с заглавной буквы. Исключение составляют типы int и char, для которых имена оболочечных классов Integer и Character. - Основное назначение оболочечных классов – создание объектов, являющихся оболочками над значениями примитивных типов. Процесс создание такого объекта (“коробки” - box) из значения примитивного типа называется упаковкой (boxing), а обратное преобразование из объекта в величину примитивного типа – распаковкой (unboxing). Упаковка и распаковка для числовых классов осуществляется автоматически. - В оболочечных классах имеется ряд полезных методов и констант. Например, минимальное по модулю не равное нулю и максимальное значение числового типа можно получить с помощью констант, вызываемых через имя оболочечного типа: Integer.MIN_VALUE, Integer.MAX_VALUE, Float.MIN_VALUE , Float.MAX_VALUE, Double.MIN_VALUE , Double.MAX_VALUE. и т.п. - В Java имеется 15 уровней приоритета операторов. В хорошо написанной программе ставятся скобки, повышающие читаемость программы, даже если они не нужны с точки зрения таблицы приоритетов. - Иногда требуется использовать элементы, которые не являются ни числами, ни строками, но ведут себя как имена элементов и одновременно обладают порядковыми номерами. Например, названия месяцев или дней недели. В этих случаях используют перечисления . Типичные ошибки:
Работа с выбором вариантов осуществляется следующим образом: if(jRadioButton1.isSelected() ) оператор1; if(jRadioButton2.isSelected()) оператор2; if(jRadioButton3.isSelected()) оператор3;
Глава 4. Работа с числами в языке Java Данная часть посвящена изучению работы с числами на более глубоком уровне. В ней рассматривается машинное представление целых и вещественных чисел, эффективное для аппаратной реализации, а также объясняются особенности и проблемы, к которым приводит такое представление. 4.1 Двоичное представление целых чисел Позиционные и непозиционные системы счисленияПозиционная система счисления - это такой способ записи числа, при котором вес цифры зависит от занимаемой позиции и пропорционален степени некоторого числа. Основание степени называется основанием системы счисления. Например, число 2006 в десятичной системе счисления представляется в виде суммы тысяч, сотен, десятков и единиц: 2006 = 2*103 + 0*102 + 0*101 + 6*100 , то есть слагаемых с различными степенями числа 10. По основанию степени - числу десять - система называется десятичной. Другие позиционные системы счисления отличаются только числом в основании степени. При написании программ чаще всего используют десятичную, шестнадцатеричную (основание шестнадцать), восьмеричную (основание восемь) и двоичную (основание два) системы. Число различных знаков - цифр, используемых для записи чисел - в каждой системе равно основанию данной системы счисления. 0,1 - цифры двоичной системы 0,1,2,3,4,5,6,7 - цифры восьмеричной системы 0,1,2,3,4,5,6,7,8,9 - цифры десятичной системы 0,1,2,3,4,5,6,7,8,9,A,B,B,C,D,E,F - цифры шестнадцатеричной системы В шестнадцатеричной системе "обычных" десятичных цифр недостаточно, и для обозначения цифр, больших девяти, используются заглавные латинские буквы A,B,C,D,E,F. В дальнейшем везде, где это необходимо, мы будем указывать основание системы счисления индексом рядом с числом: 9510 - в десятичной системе, 27F16 - в шестнадцатеричной системе, 67528 - в восьмеричной системе, 10001112 - в двоичной системе. Существует множество непозиционных систем счисления, в которых числа изображаются и называются по своим правилам. Для римской системы чисел характерны сопоставление отдельного знака каждому большому числу ( V - пять, X - десять, L - пятьдесят, C - сто, M - тысяча ), повторение знака столько раз, сколько таких чисел во всем числе ( III - три, XX - двадцать), отдельные правила для предшествующих и последующих чисел (IV - четыре, VI - шесть, IX - девять). Множество непозиционных систем счисления связано с традиционными способами измерения конкретных величин - времени ( секунда, минута, час, сутки, неделя, месяц, год), длины ( дюйм, фут, ярд, миля, аршин, сажень), массы ( унция, фунт), денежных единиц. Выполнение арифметических действий в таких системах представляет собой крайне сложную задачу. Приведём пример самой простой из возможных систем счисления – унарную. В ней имеется всего одна цифра 1. В унарной системе счисления число 1 изображается как 1, число 2 изображается как 11, число 3 как 111, число 4 как 1111, и так далее. Первоначально вместо единицы использовались палочки (помните детский сад?), поэтому такая система счисления иногда называется палочковой. Как ни странно, она является позиционной. Позиционные системы счисления с основанием 2 и более удобны для алгоритмизации математических операций с числами (вспомните способ сложения и умножения "столбиком"). Двоичная система является естественным способом кодирования информации в компьютере, когда сообщение представляется набором нулей ("0" - нет сигнала на линии) и единиц ("1" - есть сигнал на линии). Для обозначения двоичных цифр применяется термин "бит", являющийся сокращением английского словосочетания "двоичная цифра" (BInary digiT). Архитектура компьютера накладывает существенное ограничение на длину информации, обрабатываемой за одну операцию. Эта длина измеряется количеством двоичных разрядов и называется разрядностью. С помощью восьми двоичных разрядов можно представить 28 =256 целых чисел. Порция информации размером 8 бит (8-ми битовое число) служит основной единицей измерения компьютерной информации и называется байтом (byte). Как правило, передача информацией внутри компьютера и между компьютерами идет порциями, кратными целому числу байт. Машинным словом называют порцию данных, которую процессор компьютера может обработать за одну операцию (микрокоманду). Первые персональные компьютеры были 16-разрядными, т.е. работали с 16-битными (двухбайтными) словами. Поэтому операционные системы для этих компьютеров также были 16-разрядными. Например, MS DOS. Операционные системы для персональных компьютеров следующих поколений были 32-разрядны (Windows® ’95/’98/NT/ME/2000/XP, Linux, MacOS® ), так как предназначались для использования с 32-разрядными процессорами. Современные операционные системы либо 32-разрядны, либо даже 64-разрядны (версии для 64-разрядных процессоров). Представление чисел в двоичной и шестнадцатеричной системах счисления, а также преобразование из одной системы в другую часто необходимо при программировании аппаратуры для измерений, контроля и управления с помощью портов ввода-вывода, цифро-аналоговых и аналого-цифровых преобразователей. Двоичное представление положительных целых чиселЦелые числа в компьютере обычно кодируются в двоичном коде, то есть в двоичной системе счисления. Например, число 5 можно представить в виде . Показатель системы счисления принято записывать справа снизу около числа. Аналогично, , , , , , и так далее. Всё очень похоже на обозначение чисел в десятичной системе счисления: . Но только в качестве основания системы счисления используется число . У чисел, записанных в десятичной системе счисления, индекс 10 обычно не пишется, но его можно писать. Так что , , и так далее. В двоичной арифметике всего две цифры, 0 и 1. Двоичный код положительного целого числа – это коэффициенты разложения числа по степеням двойки. Умножение числа на двоичное десять, то есть на , приводит к дописыванию справа нуля в двоичном представлении числа. Умножение на двоичное сто, то есть на - дописыванию двух нулей. И так далее. Целочисленное деление на с отбрасыванием остатка производится путём отбрасывания последнего (младшего) бита, деление на - отбрасывания двух последних бит, и так далее. Обычно такие операции называют побитовыми сдвигами на n бит влево (умножение на ) или вправо (целочисленное деление на ). Сложение двоичных чисел можно производить “в столбик” по полной аналогии со сложением десятичных чисел. Единственное отличие – то, что в двоичной арифметике только две цифры, 0и1, а не десять цифр (от 0 до 9) как в десятичной. Поэтому если в десятичной арифметике единицу более старшего разряда даёт, к примеру, сложение 1 и 9, то в двоичной арифметике её даст сложение 1 и 1. То есть (в десятичной системе это равенство выглядит как 1+1=2). Аналогично, , и так далее. Примеры сложения “в столбик”:
Совершенно аналогично выполняется умножение: В машинной реализации целочисленного умножения используют побитовые сдвиги влево и сложения. Поскольку эти алгоритмы очень просты, они реализуются аппаратно. Двоичное представление отрицательных целых чисел. Дополнительный кодСтарший бит в целых без знака имеет обычный смысл, в целых со знаком – для положительных чисел всегда равен 0. В отрицательных числах старший бит всегда равен 1. В примерах для простоты мы будем рассматривать четырехбитную арифметику. Тогда в качестве примера целого положительного числа можно привести 01102 . Для хранения отрицательных чисел используется дополнительный код. Число (– n ), где n положительно, переводится в число n2=-n по следующему алгоритму: · этап 1: сначала число n преобразуется в число n 1 путём преобразования , во время которого все единицы числа n заменяются нулями, а нули единицами, то есть ; · этап 2: перевод , то есть к получившемуся числу n1 добавляется единица младшего разряда. Надо отметить, что дополнительный код отрицательных чисел зависит от разрядности. Например, код числа (–1) в четырёхразрядной арифметике будет , а в 8-разрядной арифметике будет . Коды числа (–2) будут и , и так далее. Для того, чтобы понять причину использования дополнительного кода, рассмотрим сложение чисел, представленных в дополнительном коде. Сложение положительного и отрицательного чисел Рассмотрим, чему равна сумма числа 1 и числа –1, представленного в дополнительном коде. Сначала переведём в дополнительный код число –1. При этом n=110 , n2 = –110. Этап1: n=110 =00012 n1=11102 ; Этап2: n2=11102 +1=11112 ; Таким образом, в четырёхбитном представлении –110 =11112 . Проверка: n2+n=100002 . Получившийся пятый разряд, выходящий за пределы четырехбитной ячейки, отбрасывается, поэтому в рамках четырехбитной арифметики получается n2+n=00002 =0. Аналогично n=210 =00102 n1=11012 ; n2=11102 ; n=310 =00112 n1=11002 ; n2=11012 ; n=410 =01002 n1=10112 ; n2=11002 ; Очевидно, во всех этих случаях n2+n=0. Что будет, если мы сложим 310 и –210 (равное 11102 , как мы уже знаем)?
После отбрасывания старшего бита, выходящего за пределы нашей четырёхбитовой ячейки, получаем 00112 + 11102 =00012 , то есть 310 + (–210 )=110 , как и должно быть. Сложение отрицательных чисел из-за отбрасывания лишнего старшего бита, выходящего за пределы ячейки. Поэтому . Вычитание положительных чисел осуществляется путём сложения положительного числа с отрицательным, получившимся из вычитаемого в результате его перевода в дополнительный код. Приведенные примеры иллюстрируют тот факт, что сложение положительного числа с отрицательным, хранящимся в дополнительном коде, или двух отрицательных, может происходить аппаратно (что значит очень эффективно) на основе крайне простых электронных устройств. Проблемы целочисленной машинной арифметикиНесмотря на достоинства в двоичной машинной (аппаратной) арифметике имеются очень неприятные особенности, возникающие из-за конечной разрядности машинной ячейки. Проблемы сложения положительных чисел Пусть a=310 =00112 ; b=210 =00102 ; a+b=01012 =510 , то есть все в порядке. Пусть теперь a=610 =01102 , b=510 =01012 . Тогда a+b =10112 = -32 . То есть сложение двух положительных чисел может дать отрицательное, если результат сложения превышает максимальное положительное число, выделяемое под целое со знаком для данной разрядности ячеек! В любом случае при выходе за разрешённый диапазон значений результат оказывается неверным. Если у нас беззнаковые целые, проблема остается в несколько измененном виде. Сложим 810 +810 в двоичном представлении. Поскольку 810 =10002 , тогда 810 +810 = 10002 + 10002 =100002 . Но лишний бит отбрасывается, и получаем 0. Аналогично в четырёхбитной арифметике, 810 + 910 =110 , и т.д. Как уже говорилось, умножение двоичных чисел осуществляется путем сложений и сдвигов по алгоритму, напоминающему умножение “в столбик”, но гораздо более простому, так как умножить надо только на 0 или 1. При целочисленном умножении выход за пределы разрядности ячейки происходит гораздо чаще, чем при сложении или вычитании. Например, 1102 1012 =1102 1002 +1102 12 =110002 +1102 =111002 . Если наша ячейка четырехразрядная, произойдет выход за ее пределы, и мы получим после отбрасывания лишнего бита 11102 = -210 <0. Таким образом, умножение целых чисел легко может дать неправильный результат. В том числе – даже отрицательное число. Поэтому при работе с целочисленными типами данных следует обращать особое внимание на то, чтобы в программе не возникало ситуаций арифметического переполнения. Повышение разрядности целочисленных переменных позволяет смягчить проблему, хотя полностью её не устраняет. Например, зададим переменные byte m=10,n=10,k=10; Тогда значения m*n, m*k и n*k будут лежать в разрешённом диапазоне -128..127. А вот m*n + m*k из него выйдет. Не говоря уж об m*n*k. Если мы зададим int m=10,n=10,k=10; переполнения не возникнет даже для m*n*k. Однако, при m=n=k=100 значение m*n*k будет равно 106 , что заметно выходит за пределы разрешённого диапазона –32768..32767. Хотя m*n, m*k и n*k не будут за него выходить (но уже 4*m*n за него выйдет). Использование типа long поможет и в этом случае. Однако уже значения m=n=k=2000 (не такие уж большие!) опять приведут к выходу m*n*k за пределы диапазона. Хотя для m*n выход произойдёт только при значениях около 50000. Вычисление факториала с помощью целочисленной арифметики даст удивительные результаты! В таких случаях лучше использовать числа с плавающей точкой. Пример: byte i=127, j=1, k; k=(byte)(i+j); System.out.println(k); В результате получим число (-128). Если бы мы попробовали написать byte i=127,j=1,k; System.out.println(i+j); то получили бы +128. Напомним, что значения величин типа byte перед проведением сложения преобразуются в значения типа int. Шестнадцатеричное представление целых чисел и перевод из одной системы счисления в другуюВо время программирования различного рода внешних устройств, регистров процессора, битовыми масками, кодировке цвета, и так далее, приходится работать с кодами беззнаковых целых чисел. При этом использование десятичных чисел крайне неудобно из-за невозможности лёгкого сопоставления числа в десятичном виде и его двоичных бит. А использование чисел в двоичной кодировке крайне громоздко – получаются слишком длинные последовательности нулей и единиц. Программисты используют компромиссное решение – шестнадцатеричную кодировку чисел, где в качестве основания системы счисления выступает число 16. Очевидно, . В десятичной системе счисления имеется только 10 цифр: 0,1,2,3,4,5,6,7,8,9. А в 16-ричной системе счисления должно быть 16 цифр. Принято обозначать недостающие цифры заглавными буквами латинского алфавита: A,B,C,D,E,F. То есть , , ,, , . Таким образом, к примеру, . В Java для того, чтобы отличать 16-ричные числа, как мы уже знаем, перед ними ставят префикс 0x: 0xFF обозначает , а 0x10 – это 1016 , то есть 16. Число N может быть записано с помощью разных систем счисления. Например, в десятичной: N = An 10n + ... + A2 102 + A1 101 + A0 100 ( An = 0 .. 9) или в двоичной: N = Bn 2n + ... + B2 22 + B1 21 + B0 20 ( Bn = 0 или 1 ) или в шестнадцатеричной: N = Cn 16n + ... + C2 162 + C1 161 + C0 160 ( Cn = 0 .. F ) Преобразование в другую систему счисления сводится к нахождению соответствующих коэффициентов. Например, Bn по известным коэффициентам An – при переводе из десятичной системы в двоичную, или коэффициентов An по коэффициентам Bn - из двоичной системы в десятичную. Преобразование чисел из системы с меньшим основанием в систему с большим основанием Рассмотрим преобразование из двоичной системы в десятичную. Запишем число N в виде N = Bn 2n + ... + B2 22 + B1 21 + B0 20 ( Bn = 0 или 1 ) и будем рассматривать как алгебраическое выражение в десятичной системе. Выполним арифметические действия по правилам десятичной системы. Полученный результат даст десятичное представление числа N. Пример: Преобразуем 010111102 к десятичному виду. Имеем: 010111102 = 0×27 +1×26 +0×25 +1×24 +1×23 +1×22 +1×21 +0×20 = = 0 + 64 + 0 + 16 + 8 + 4 + 2 + 0 = = 9410
Преобразование чисел из системы с большим основанием в систему с меньшим основанием Рассмотрим его на примере преобразования из десятичной системы в двоичную. Нужно для известного числа N10 найти коэффициенты в выражении N = Bn 2n + ... + B2 22 + B1 21 + B0 20 ( Bn = 0 или 1 ) Воспользуемся следующим алгоритмом: в десятичной системе разделим число N на 2 с остатком. Остаток деления (он не превосходит делителя) даст коэффициент B0 при младшей степени 20 . Далее делим на 2 частное, полученное от предыдущего деления. Остаток деления будет следующим коэффициентом B1 двоичной записи N. Повторяя эту процедуру до тех пор, пока частное не станет равным нулю, получим последовательность коэффициентов Bn . Например, преобразуем 34510 к двоичному виду. Имеем: частное остаток Bi 345 / 2 172 1 B0 172 / 2 86 0 B1 86 / 2 43 0 B2 43 / 2 21 1 B3 21 / 2 10 1 B4 10 / 2 5 0 B5 5 / 2 2 1 B6 2 / 2 1 0 B7 1 / 2 0 1 B8 34510 = 1010110012
Преобразование чисел в системах счисления с кратными основаниями Рассмотрим число N в двоичном и шестнадцатеричном представлениях. N = Bn 2n + ... + B2 22 + B1 21 + B0 20 ( Bi = 0 или 1 ) N = Hn 16n + ... + H2 162 + H1 161 + H0 160 ( Hi = 0 .. F16 , где F16 =1510 ) Заметим, что 16 = 24 . Объединим цифры в двоичной записи числа группами по четыре. Каждая группа из четырех двоичных цифр представляет число от 0 до F16 , то есть от 0 до1510 . От группы к группе вес цифры изменяется в 24 =16 раз (основание 16-ричной системы). Таким образом, перевод чисел из двоичного представления в шестнадцатеричное и обратно осуществляется простой заменой всех групп из четырех двоичных цифр на шестнадцатеричные (по одному на каждую группу) и обратно : 00002 = 016 00012 = 116 00102 = 216 00112 = 316 01002 = 416 01012 = 516 01102 = 616 01112 = 716 10002 = 816 10012 = 916 10102 = A16 10112 = B16 11002 = C16 11012 = D16 11102 = E16 11112 = F16 Например, преобразуем 10110101112 к шестнадцатеричному виду: 10110101112 = 0010 1101 01112 = 2D716 4.2. Побитовые маски и сдвиги
Побитовые операции – когда целые числа рассматриваются как наборы бит, где 0 и 1 играют роли логического нуля и логической единицы. При этом все логические операции для двух чисел осуществляются поразрядно – k-тый разряд первого числа с k-тым разрядом второго. Для простоты мы будем рассматривать четырёхбитовые ячейки, хотя реально самая малая по размеру ячейка восьмибитовая и соответствует типу byte. а) установка в числе a нужных бит в 1 с помощью маски m операцией a|m (арифметический, или, что то же, побитовый оператор OR). Пусть число a = a3 *23 + a2 *22 + a1 *21 + a0 *20 , где значения ai – содержание соответствующих бит числа (то есть либо нули , либо единицы).
Видно, что независимо от начального значения в числе a в результате нулевой и второй бит установились в единицу. Таким образом, операцию OR с маской можно использовать для установки нужных бит переменной в единицу, если нужные биты маски установлены в единицу, а остальные – нули. б) установка в числе a нужных бит в 0 с помощью маски m операцией a&m (арифметический, или, что то же, побитовый оператор AND):
Видно, что независимо от начального значения в числе a в результате первый и третий бит установились в нуль. Таким образом, операцию AND с маской можно использовать для установки нужных бит переменной в ноль, если нужные биты маски установлены в ноль, а остальные – единицы. в) инверсия (замена единиц на нули, а нулей на единицы) в битах числа a , стоящих на задаваемых маской m местах, операцией a^m (арифметический, или, что то же, побитовый оператор XOR ):
Видно, что если в бите, где маска m имеет единицу, у числа a происходит инверсия: если стоит 1, в результате будет 0, а если 0 – в результате будет 1. В остальных битах значение не меняется. Восстановление первоначального значения после операции XOR – повторное XOR с той же битовой маской:
Видно, что содержание ячейки приняло то же значение, что было первоначально в ячейке a. Очевидно, что всегда (a ^ m) ^ m = a, так как повторная инверсия возвращает первоначальные значения в битах числа. Операция XOR часто используется в программировании для инверсии цветов частей экрана с сохранением в памяти только информации о маске. Повторное XOR с той же маской восстанавливает первоначальное изображение. - Имеется команда перевода вывода графики в режим XOR при рисовании, для этого используется команда graphics.setXORMode(цвет). Ещё одна область, где часто используется эта операция – криптография. Инверсия всех битов числа осуществляется с помощью побитового отрицания ~ a . Побитовые сдвиги “<<”, “>>” и “>>>” приводят к перемещению всех бит ячейки, к которой применяется оператор, на указанное число бит влево или вправо. Сначала рассмотрим действие операторов на положительные целые числа. Побитовый сдвиг на n бит влево m<<n эквивалентен быстрому целочисленному умножению числа m на 2n . Младшие биты (находящиеся справа), освобождающиеся после сдвигов, заполняются нулями. Следует учитывать, что старшие биты (находящиеся слева), выходящие за пределы ячейки, теряются, как и при обычном целочисленном переполнении. Побитовые сдвиги на n бит вправо m>>n или m>>>n эквивалентны быстрому целочисленному делению числа m на 2n . При этом для положительных m разницы между операторами “>>” и “>>>” нет. Рассмотрим теперь операции побитовых сдвигов для отрицательных чисел m. Поскольку они хранятся в дополнительном коде, их действие нетривиально. Как и раньше, для простоты будем считать, что ячейки четырёхбитовые, хотя на деле побитовые операции проводятся только для ячеек типа int или long, то есть для 32-битных или 64-битных чисел. Пусть m равно -1. В этом случае m=11112 . Оператор m<<1 даст m=111102 , но из-за четырёхбитности ячейки старший бит теряется, и мы получаем m=11102 =-2. То есть также получается полная эквивалентность умножению m на 2n . Иная ситуация возникает при побитовых сдвигах вправо. Оператор правого сдвига “>>” для положительных чисел заполняет освободившиеся биты нулями, а для отрицательных – единицами. Легко заметить, что этот оператор эквивалентен быстрому целочисленному делению числа m на 2n как для положительных, так и для отрицательных чисел. Оператор m>>>n, заполняющий нулями освободившиеся после сдвигов биты, переводит отрицательные числа в положительные. Поэтому он не может быть эквивалентен быстрому делению числа на 2n . Но иногда такой оператор бывает нужен для манипуляции с наборами бит, хранящихся в числовой ячейке. Само значение числа в этом случае значения не имеет, а ячейка используется как буфер соответствующего размера. Например, можно преобразовать последовательность бит, образующее некое целое значение, в число типа float методом Float.intBitsToFloat(целое значение ) или типа double методом Double.intBitsToDouble (целое значение ). Так, Float.intBitsToFloat(0x7F7FFFFF) даст максимальное значение типа float. 4. 3. Двоичное представление вещественных чисел Двоичные дробиЦелое число 01012 можно представить в виде 01012 =0*23 + 1*22 + 0*21 + 1*20 Аналогично можно записать двоичную дробь: 11.01012 =1*21 + 1*20 + 0*2-1 + 1*2-2 + 0*2-3 + 1*2-4 Заметим, что сдвиг двоичной точки на n разрядов вправо (чаще говорят о сдвиге самого числа влево) эквивалентен умножению числа на (102 )n = 2n . Сдвиг точки влево (то есть сдвиг самого числа вправо) – делению на 2n . Мантисса и порядок числаРассмотрим сначала упрощенную схему хранения чисел в формате с плавающей точкой (f loating point), несколько отличающуюся от реальной. Число x с плавающей точкой может быть представлено в виде x=s*m*2p . Множитель s – знак числа. Второй множитель m называется мантиссой, а число p – порядком числа. Для простоты рассмотрим 10-битовую ячейку, состоящую из трёх независимых частей: знак порядок мантисса 1 бит 4 бита 5 бит Первым идёт знаковый бит. Если он равен 0, число положительно, если равен 1 – отрицательно. Набор бит, хранящийся в мантиссе, задает положительное число m, лежащее в пределах 1≤m<2. Оно получается из нашего двоичного числа путем переноса двоичной точки на место после первой значащей цифры числа. Например, числа 1.01012 , 10.1012 и 0.101012 и имеют одну и ту же мантиссу, равную 1.01012 . При этом следующая за ведущей единицей точка в ячейке, выделяемой под мантиссу, не хранится - она подразумевается. То есть мантиссы приведённых чисел будут храниться в виде 101012 . Число сдвигов двоичной точки (с учетом знака) хранится в части ячейки, выделяемой под порядок числа. В нашем примере числа 1.01012 , 10.1012 и 0.101012 будут иметь порядки 0, 1 и -1, соответственно. При перемножении чисел их мантиссы перемножаются, а порядки складываются. При делении – мантиссы делятся, а порядки вычитаются. И умножение, и деление мантисс происходит по тем же алгоритмам, что и для целых чисел. Но при выходе за размеры ячейки отбрасываются не старшие, а младшие байты. В результате каждая операция умножения или деления даёт результат, отличающийся от точного на несколько значений младшего бита мантиссы. Аналогичная ситуация с потерей младших бит возникает при умножениях и делениях. Ведь если в ячейках для чисел данного типа хранится k значащих цифр числа, то при умножении двух чисел точный результат будет иметь 2k значащих цифр, последние k из которых при записи результата в ячейку будут отброшены даже в том случае, если они сохранялись при вычислениях. А при делении в общем случае при точных вычислениях должна получаться бесконечная периодическая двоичная дробь, так что даже теоретически невозможно провести эти вычисления без округлений. С этим связана конечная точность вычислений на компьютерах при использовании формата с “плавающей точкой”. При этом чем больше двоичных разрядов выделяется под мантиссу числа, тем меньше погрешность в такого рода операциях. Замечание: системы символьных вычислений (или, что то же, – аналитических вычислений, или, что то же, системы компьютерной алгебры) позволяют проводить точные численные расчеты с получением результатов в виде формул. Однако они выполняют вычисления на много порядков медленнее, требуют намного больше ресурсов и не могут работать без громоздкой среды разработки. Поэтому для решения большинства практически важных задач они либо неприменимы, либо их использование нецелесообразно. При сложении или вычитании сначала происходит приведение чисел к одному порядку: мантисса числа с меньшим порядком делится на 2n , а порядок увеличивается на n, где n – разница в порядке чисел. При этом деление на 2n осуществляется путем сдвига мантиссы на n бит вправо, с заполнением освобождающихся слева бит нулями. Младшие биты мантиссы, выходящие за пределы отведенной под нее части ячейки, теряются. Пример: сложим числа 11.0112 и 0.110112 . Для первого числа мантисса 1.10112 , порядок 1, так как 11.0112 =1.10112 *(102 )1 . Для второго – мантисса 1.10112 , порядок -1, так как 0.110112 =1.10112 *(102 )-1 . Приводим порядок второго числа к значению 1, сдвигая мантиссу на 2 места вправо, так как разница порядков равна 2: 0.110112 = 0.0110112 * (102 )1 . Но при таком сдвиге теряется два последних значащих бита мантиссы (напомним, хранится 5 бит), поэтому получаем приближенное значение 0.01102 * (102 )1 . Из-за чего в машинной арифметике получается 1.10112 *(102 )1 + 0.0110112 *(102 )1 = (1.10112 + 0.0110112 )*(102 )1 ≈ (1.10112 + 0.01102 )*(102 )1 =10.00012 * (102 )1 ≈ 1.00002 *(102 )2 вместо точного значения 1.00001112 *(102 )2 . Таким образом, числа в описанном формате являются на деле рациональными, а не вещественными. При этом операции сложения, вычитания, умножения и деления выполняются с погрешностями, тем меньшими, чем больше разрядность мантиссы. Число двоичных разрядов, отводимых под порядок числа, влияет лишь на допустимый диапазон значений чисел, и не влияет на точность вычислений. Научная нотация записи вещественных чисел При записи программы в текстовом файле или выдачи результатов в виде “плоского текста” (plain text) невозможна запись выражений типа . В этом случае используется так называемая научная нотация, когда вместо основания 10 пишется латинская буква E (сокращение от Exponent – экспонента). Таким образом, запишется как 1.5E14, а как 0.31E-7. Первоначально буква E писалась заглавной, что не вызывало проблем. Однако с появлением возможности набора текста программы в нижнем регистре стали использовать строчную букву e, которая в математике используется для обозначения основания натуральных логарифмов. Запись вида 3e2 легко воспринять как , а не . Поэтому лучше использовать заглавную букву. Литерные константы для вещественных типов по умолчанию имеют тип double. Например, 1.5 , -17E2 , 0.0 . Если требуется ввести литерную константу типа float, после записи числа добавляют постфикс f (сокращение от “float”): 1.5f , -17E2f , 0.0f . Минимальное по модулю не равное нулю и максимальное значение типа float можно получить с помощью констант Float.MIN_VALUE - равна 2-149 Float.MAX_VALUE - равна (2-2-23 )∙2127 Аналогичные значения для типа double - с помощью констант Double.MIN_VALUE - равна 2-1074 Double.MAX_VALUE - равна (2-2-52 )∙21023 . Стандарт IEEE 754 представления чисел в формате с плавающей точкой**Этот параграф является необязательным и приводится в справочных целях В каком виде на самом деле хранятся числа в формате с плавающей точкой? Ответ даёт стандарт IEEE 754 (другой вариант названия IEC 60559:1989), разработанный для электронных счётных устройств. В этом стандарте предусмотрены три типа чисел в формате с плавающей точкой, с которыми могут работать процессоры: real*4, real*8 и real*10. Эти числа занимают 4, 8 и 10 байт, соответственно. В Java типу real*4 соответствует float, а типу real*8 соответствует double. Тип real*10 из распространённых языков программирования используется только в диалектах языка PASCAL, в Java он не применяется. Число r представляется в виде произведения знака s, мантиссы m и экспоненты 2p-d : r=s*m*2p-d . Число p называется порядком. Оно может меняться для разных чисел. Значение d, называемое сдвигом порядка, постоянное для всех чисел заданного типа. Оно примерно равно половине максимального числа pmax , которое можно закодировать битами порядка. Точнее, d= (pmax +1)/2-1. Для чисел real*4: pmax = 255, d= 127. Для чисел real*8: pmax = 2047, d= 1023. Для чисел real*10: pmax =32767, d=16383. Число называется нормализованным в случае, когда мантисса лежит в пределах 1≤m<2. В этом случае первый бит числа всегда равен единице. Максимальное значение мантиссы достигается в случае, когда все её биты равны 1. Оно меньше 2 на единицу младшего разряда мантиссы, то есть с практически важной точностью может считаться равным 2. Согласно стандарту IEEE 754 все числа формата с плавающей точкой при значениях порядка в диапазоне от 1 до pmax -1 хранятся в нормализованном виде. Такое представление чисел будем называть базовым . Когда порядок равен 0, применяется несколько другой формат хранения чисел. Будем называть его особым . Порядок pmax резервируется для кодировки нечисловых значений, соответствующее представление будем называть нечисловым . Об особом и нечисловом представлениях чисел будет сказано чуть позже. Размещение чисел в ячейках памяти такое:
Буква s обозначает знаковый бит; p – биты двоичного представления порядка, m – биты двоичного представления мантиссы. Если знаковый бит равен нулю, число положительное, если равен единице – отрицательное. В числах real*4 (float) и real*8 (double) при базовом представлении ведущая единица мантиссы подразумевается, но не хранится, поэтому реально можно считать, что у них под мантиссу отведено не 23 и 52 бита, которые реально хранятся, а 24 и 53 бита. В числах real*10 ведущая единица мантиссы реально хранятся, и мантисса занимает 64 бит. Под порядок в числах real*4 отведено 8 бит, в числах real*8 отведено 11 бит, а в числах real*10 отведено 15 бит.
Чему равны минимальное и максимальное по модулю числа при их базовом представлении? Минимальное значение достигается при минимальном порядке и всех нулевых битах мантиссы (за исключением ведущего), то есть при m=1 и p=1. Значит, минимальное значение равно 21-d . Максимальное значение достигается при максимальном порядке и всех единичных битах мантиссы, то есть при m≈2 и p= pmax -1. Значит, максимальное значение примерно равно При значениях порядка в диапазоне от 1 до pmax -1 базовое представление позволяет закодировать
В случае, когда порядок равен 0 или pmax , используется особое представление чисел, несколько отличающееся от базового. Если все биты порядка равны 0, но мантисса отлична от нуля, то порядок считается равным 1 (а не 0), а вместо единицы в качестве подразумеваемой ведущей цифры используется ноль. Это ненормализованное представление чисел. Максимальное значение мантиссы в этом случае на младший бит мантиссы меньше 1. Так что максимальное значение числа в особой форме представления равно (1- младший бит мантиссы)*21-d . То есть верхний предел диапазон изменения чисел в этом представлении смыкается с нижним диапазоном изменения чисел в базовом представлении. Минимальное ненулевое значение мантиссы в особом представлении равно 2-n , где n – число бит мантиссы после двоичной точки. Минимальное отличное от нуля положительное число для некоторого типа чисел с плавающей точкой равно 21-d-n . Таким образом, особое представление позволяет закодировать
Специальный случай особого представления – когда и порядок и мантисса равны нулю. Это значение обозначает машинный ноль. В соответствии со стандартом IEEE 754 имеются +0 и -0. Но во всех известных автору языках программирования +0 и -0 при вводе-выводе и сравнении чисел отождествляются.
Нечисловое представление соответствует случаю, когда p=pmax , то есть все биты порядка равны 1. Такое “число” в зависимости от значения мантиссы обозначает одно из трех специальных нечиселовых значений, которые обозначают как Inf (Infinity - “бесконечность”), NaN (Not a Number - “не число”), Ind (Indeterminate – “неопределённость”). Эти значения появляются при переполнениях и неопределённостях в вычислениях. Например, при делении 1 на 0 получается Inf, а при делении 0 на 0 получается Ind. Значение NaN может получаться при преобразовании строки в число, взятии логарифма от отрицательного числа, тригонометрической функции от бесконечности и т.п. Значение Inf соответствует нулевым битам мантиссы.Согласно IEEE 754 бесконечность имеет знак. Если знаковый бит 0 это + Inf, если знаковый бит 1 это –Inf. Значение Ind кодируется единицей в знаковом бите и битами мантиссы, равными 0 во всех разрядах кроме старшего (реального, а не подразумеваемого), где стоит 1. Все остальные сочетания знакового бита и мантиссы отведены под величины NaN. Значения NaN бывают двух типов – вызывающие возбуждение сигнала о переполнении (Signaling NaN) и не вызывающие (Quiet NaN). Значения обоих этих типов могут быть “положительными” (знаковый бит равен нулю) и “отрицательными” (знаковый бит равен единице). В современных языках программирования поддерживается только часть возможностей, реализованных в процессорах в соответствии со стандартом IEEE 754. Например, в Java значения бесконечности различаются как по знаку, так и по типу: имеются Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY. Но значение Ind вообще не употребляется и отождествляется с NaN, хотя Float.NaN и Double.NaN различаются. Числа в формате с плавающей точкой занимают следующие диапазоны значений:
Имеются методы оболочечных классов, позволяющие преобразовывать наборы бит, хранящихся в ячейках типа int, в значения float, и наоборот – значения типа float в их битовое представление типа int. При этом содержание ячеек не меняется – просто содержащиеся в ячейках наборы бит начинают по-другому трактоваться. Аналогичные операции существуют и для значений типа long и double: Float.intBitsToFloat(значение типа int ) Double.longBitsToDouble(значение типа long ) Float.floatToIntBits(значение типа float ) Double.doubleToLongBits(значение типа double ) Например, Float.intBitsToFloat(0x7F7FFFFF) даст значение, равное Float.MAX_VALUE, Float.intBitsToFloat(0x7F800000) – значение Float.POSITIVE_INFINITY, Float.intBitsToFloat(0xFF800000) – значение Float.NEGATIVE_INFINITY. Если аргумент метода Float.intBitsToFloat лежит в пределах от 0xF800001 до 0xF800001, результатом будет Float.NaN. Следует подчеркнуть, что данные операции принципиально отличаются от “обычных” преобразований типов, например, из int в float или из double в long. При “обычных” преобразованиях значение числа не меняется, просто меняется форма хранения этого значения и, соответственно, наборы битов, которыми кодируется это значение. Причём может измениться размер ячейки (скажем, при преобразовании значений int в значения double). А при рассматриваемых в данном разделе операциях сохраняется набор бит и размер ячейки, но меняется тип, который приписывается этому набору. - Отрицательные целые числа кодируются в дополнительном коде. - При сложении, вычитании и, особенно, умножении целых чисел может возникать выход за пределы допустимого диапазона. В результате у результата может получиться значение, имеющее противоположный по сравнению с правильным результатом знак. Или нуль при сложении чисел одного знака. - Побитовая маска AND (оператор “&”) служит для сбрасывания в 0 тех битов числа, где в маске стоит 0, остальные биты числа не меняются. Побитовая маска OR (оператор “|”) служит для установки в 1 тех битов числа, где в маске стоит 1, остальные биты числа не меняются. - Побитовая маска XOR (оператор “^”) служит для инверсии тех битов числа, где в маске стоит 1 (единицы переходят в нули, а нули – в единицы), остальные биты числа не меняются. Имеется команда перевода вывода графики в режим XOR при рисовании, для этого используется команда graphics.setXORMode(цвет ). - Инверсия всех битов числа осуществляется с помощью побитового отрицания ~ a . - Побитовые сдвиги “<<”, “>>” и “>>>” приводят к перемещению всех бит ячейки, к которой применяется оператор, на указанное число бит влево или вправо. Причём m<<n является очень быстрым вариантом операции m∙2n , а m>>n – целочисленному делению m на 2n . - Литерные константы для вещественных типов по умолчанию имеют тип double. Например, 1.5 , -17E2 , 0.0 . Если требуется ввести литерную константу типа float, после записи числа добавляют постфикс f (сокращение от “float”): 1.5f , -17E2f , 0.0f . - Число в формате с плавающей точкой состоит из знакового бита, мантиссы и порядка. Все операции с числами такого формата сопровождаются накоплением ошибки порядка нескольких младших битов мантиссы ненормализованного результата. - При проведении умножения и деления чисел в формате с плавающей точкой не происходит катастрофической потери точности результата. А при сложениях и вычитаниях такая потеря может происходить в случае, когда вычисляется разность двух значений, к которым прибавляется малая относительно этих значений добавка. Число порядков при потере точности равно числу порядков, на которые отличаются эти значения от малой добавки. Типичные ошибки:
Глава 5. Управляющие конструкции Составной операторСогласно синтаксису языка Java во многих конструкциях может стоять только один оператор, но часто встречается ситуация, когда надо использовать последовательность из нескольких операторов. Составной оператор — блок кода между фигурными скобками {}: Имеется два общепринятых способа форматирования текста с использованием фигурных скобок. В первом из них скобки пишут друг под другом, а текст, находящийся между ними, сдвигают на 1-2 символа вправо (изредка – больше). Пример: оператор { последовательность простых или составных операторов } Во втором, более распространённом, открывающую фигурную скобку пишут на той же строке, где должен начинаться составной оператор, без переноса его на следующую строку. А закрывающую скобку пишут под первым словом. Пример: оператор{ последовательность простых или составных операторов } Именно такой способ установлен по умолчанию в среде NetBeans, и именно он используется в приводимых в данной книге фрагментах программного кода. Тем не менее, автор предпочитает первый способ форматирования программного кода, так как он более читаемый. Для установки такого способа форматирования исходного кода следует перейти в меню Tools/Options, выбрать Editor/Indentation, после чего в появившейся диалоговой форме отметить галочкой пункт Add New Line Before Brace. После фигурных скобок по правилам Java, как и в /C++, ставить символ “;” не надо. Но его можно ставить после фигурных скобок в тех местах программы, где разрешается ставить ничего не делающий пустой оператор “;” . Условный оператор ifУ условного оператора if имеется две формы: if и if- else. По-английски if означает “если”, else - “в противном случае”. Таким образом, эти операторы могут быть переведены как “если…то…” и “если…то…в противном случае…”. Первая форма: if(условие) оператор1; Если условие равно true, выполняется оператор1. Если же условие==false, в операторе не выполняется никаких действий. Вторая форма: if(условие) оператор1; else оператор2; В этом варианте оператора if если условие==false, то выполняется оператор2. Обратите особое внимание на форматирование текста. Не располагайте все части оператора if на одной строке – это характерно для новичков! Пример: if(a<b) a=a+1; else if(a==b) a=a+1; else{ a=a+1; b=b+1; }; Из этого правила имеется исключение: если подряд идёт большое количество операторов if, умещающихся в одну строку, для повышения читаемости программ бывает целесообразно не переносить другие части операторов на отдельные строки. Надо отметить, что в операторе if в области выполнения, которая следует после условия, а также в области else, должен стоять только один оператор, а не последовательность операторов. Поэтому запись оператора в виде if(условие) оператор1; оператор2; else оператор3; недопустима. В таких случаях применяют составной оператор, ограниченный фигурными скобками. Между ними, как мы знаем, может стоять произвольное число операторов: if(условие){ оператор1; оператор2; } else оператор3; Если же мы напишем if(условие) оператор1; else оператор2; оператор3; - никакой диагностики ошибки компилятор не выдаст! Оператор3 в этом случае никакого отношения к условию else иметь не будет – подобное форматирование текста будет подталкивать к логической ошибке. При следующем форматировании текста программы, эквивалентному предыдущему при компиляции, уже более очевидно, что оператор3 не относится к части else: if(условие) оператор1; else оператор2; оператор3; Для того, чтобы оператор3 относился к части else, следует использовать составной оператор: if(условие) оператор1; else{ оператор2; оператор3; }; В случае последовательности операторов типа: if(условие1)if(условие2)оператор1 else оператор2; имеющийся else относится к последнему if, поэтому лучше отформатировать текст так: if(условие1) if(условие2) оператор1; else оператор2; Таким образом, если писать соответствующие if и else друг под другом, логика работы программы становится очевидной. Пример неправильного стиля оформления: if(условие1) if(условие2) оператор1; else оператор2; Этот стиль подталкивает к логической ошибке при чтении программы. Человек, читающий такую программу (как и тот, кто её писал), будет полагать, что оператор2 выполняется, если условие1==false, так как кажется, что он относится к первому if, а не ко второму. Надо отметить, что сама возможность такой ошибки связана с непродуманностью синтаксиса языка Java. Для правильной работы требуется переписать этот фрагмент в виде if(условие1){ if(условие2) оператор1; }; else оператор2; Чтобы избегать такого рода проблем, используйте опцию Reformat code (“переформатировать код”) из всплывающего меню, вызываемого в исходном коде щелчком правой кнопки мыши. Типичной является ситуация с забытым объединением последовательности операторов в составной с помощью фигурных скобок. Например, пишут if(условие) оператор1; оператор2; вместо if(условие) {оператор1; оператор2; }; Причём такую ошибку время от времени допускают даже опытные программисты. Reformat code не помогает – обычно эта команда вызывается после того, как программист далеко ушёл от проблемного места, и перенос оператора под начало “if” не замечает. Для того, чтобы гарантированно избежать такой ошибки, следует ВСЕГДА ставить фигурные скобки в тех местах операторов Java, где по синтаксису может стоять только один оператор. Конечно, было бы лучше, чтобы разработчики Java предусмотрели такую конструкцию как часть синтаксиса языка. Возможно, в среде NetBeans в дальнейшем будет сделана возможность в опциях редактора делать установку, позволяющую автоматически создавать эти скобки. Точно так же, как автоматически создаётся закрывающая круглая скобка в операторе if после того как вы набираете if( . Следует отметить разные правила использования точки с запятой при наличии части else в случае использования фигурных скобок и без них: if(i==0) i++; else i--; Во втором случае после скобок (перед else) точка с запятой не ставится: if(i==0){ i++; } else{ i--; }; Неприятной проблемой, унаследованной в Java от языка C, является возможность использования оператора присваивания “=” вместо оператора сравнения “==”. Например, если мы используем булевские переменные b1 и b2, и вместо if(b1==b2) i=1; else i=2; напишем if(b1=b2) i=1; else i=2; никакой диагностики ошибки выдано не будет. Дело в том, что по правилам C и Java любое присваивание рассматривается как функция, возвращающая в качестве результата присваиваемое значение. Поэтому присваивание b1=b2 возвратит значение, хранящееся в переменной b2. В результате оператор будет работать, но совсем не так, как ожидалось. Более того, будет испорчено значение, хранящееся в переменной b1. Проблемы с ошибочным написанием “=” вместо “==” в Java гораздо менее типичны, чем в C/C++, поскольку они возникают только при сравнении булевской переменной с булевским значением. Если же оператор “=” ставится вместо “==” при сравнении числовой переменной, происходит диагностика ошибки, так как такое присваивание должно возвращать число, а не булевское значение. Жёсткая типизация Java обеспечивает повышение надёжности программ. Оператор выбора switchЯвляется аналогом if для нескольких условий выбора. Синтаксис оператора следующий: switch(выражение){ case значение1: операторы1; …………………………… case значениеN: операторы N; default: операторы; } Правда, крайне неудобно, что нельзя ни указывать диапазон значений, ни перечислять через запятую значения, которым соответствуют одинаковые операторы. Тип выражения должен быть каким-нибудь из целых типов. В частности, недопустимы вещественные типы. Работает оператор следующим образом: сначала вычисляется выражение. Затем вычисленное значение сравнивается со значениями вариантов, которые должны быть определены ещё на этапе компиляции программы. Если найден вариант, которому удовлетворяет значение выражения, выполняется соответствующий этому варианту последовательность операторов, после чего НЕ ПРОИСХОДИТ выхода из оператора case, что было бы естественно. - Для такого выхода надо поставить оператор break.Эта неприятная особенность Java унаследована от языка C. Часть с default является необязательной и выполняется, если ни один вариант не найден. Пример: switch(i/j){ case 1: i=0; break; case 2: i=2; break; case 10: i=3; j=j/10; break; default: i=4; }; У оператора switch имеется две особенности: - Можно писать произвольное число операторов для каждого варианта case, что весьма удобно, но полностью выпадает из логики операторов языка Java. - Выход из выполнения последовательности операторов осуществляется с помощью оператора break. Если он отсутствует, происходит “проваливание” в блок операторов, соответствующих следующему варианту за тем, с которым совпало значение выражения. При этом никакой проверки соответствия очередному значению не производится. И так продолжается до тех пор, пока не встретится оператор break или не кончатся все операторы в вариантах выбора. Такие правила проверки порождают типичную ошибку, называемую “забытый break”. Условное выражение …?... : …Эта не очень удачная по синтаксису функция унаследована из языка C. Её синтаксис таков: условие?значение1:значение2 В случае, когда условие имеет значение true, функция возвращает значение1, в противном случае возвращается значение2. Например, мы хотим присвоить переменной j значение, равное i+1 при i<5, и i+2 в других случаях. Это можно сделать таким образом: j=i<5?i+1:i+2 Иногда при вычислении громоздких выражений этот оператор приходится использовать: без него программа оказывается ещё менее прозрачной, чем с ним. Приоритет разделителей “?” и “:” очень низкий – ниже только приоритет оператора присваивания (в любых его формах). Поэтому можно писать выражения без использования скобок. Но лучше всё-таки использовать скобки: j=(i<5)?(i+1):(i+2) Операторы инкремента ++ и декремента --Оператор “++” называется инкрементным (“увеличивающим”), а “--“ декрементным (“уменьшающим”). У этих операторов имеются две формы, постфиксная (наиболее распространённая, когда оператор ставится после операнда) и префиксная (используется очень редко, в ней оператор ставится перед операндом). Для любой числовой величины x выражение x++ или ++x означает увеличение x на 1, а выражение x-- или --x означает уменьшение x на 1. Различие двух форм связано с тем, когда происходит изменение величины – после вычисления выражения, в котором используется оператор, для постфиксной формы, или до этого вычисления – для префиксной. Например, присваивания j=i++ и j=++i дадут разные результаты. Если первоначально i=0, то первое присваивание даст 0, так как i увеличится на 1 после выполнения присваивания. А второе даст 1, так как сначала выполнится инкремент, и только потом будет вычисляться выражение и выполняться присваивание. При этом в обоих случаях после выполнения присваивания i станет равно 1. Оператор цикла forfor(блок инициализации; условие выполнения тела цикла; блок изменения счётчиков) оператор; В блоке инициализации через запятую перечисляются операторы задания локальных переменных, область существования которых ограничивается оператором for. Также могут быть присвоены значения переменным, заданным вне цикла. Но инициализация может происходить только для переменных одного типа. В блоке условия продолжения цикла проверяется выполнение условия, и если оно выполняется, идёт выполнение тела цикла, в качестве которого выступает оператор . Если же не выполняется – цикл прекращается, и идёт переход к оператору программы, следующему за оператором for. После каждого выполнения тела цикла (очередного шага цикла) выполняются операторы блока изменения счётчиков. Они должны разделяться запятыми. Пример: for(int i=1,j=5; i+j<100; i++,j=i+2*j){ ... }; Каждый из блоков оператора for является необязательным, но при этом разделительные “;” требуется писать. Наиболее употребительное использование оператора for – для перебора значений некоторой переменной, увеличивающихся или уменьшающихся на 1, и выполнения последовательности операторов, использующих эти значения. Переменная называется счетчиком цикла, а последовательности операторов – телом цикла. Пример1: вычисление суммы последовательно идущих чисел. Напишем цикл, в котором производится суммирование всех чисел от 1 до 100. Результат будем хранить в переменной result. int result=0; for(int i=1; i<=100; i++){ result=result+i; }; Цикл (повторное выполнение одних и тех же действий) выполняется следующим образом: - До начала цикла создаётся переменная result, в которой мы будем хранить результат. Одновременно выполняется инициализация – присваивается начальное значение 0. - Начинается цикл. Сначала выполняется блок инициализации - счётчику цикла i присваивается значение 1. Блок инициализации выполняется только один раз в самом начале цикла. - Начинается первый шаг цикла. Проверяется условие выполнения цикла. Значение i сравнивается со 100. - Поскольку сравнение 1<=100 возвращает true, выполняется тело цикла. В переменной result хранится 0, а значение i равно 1, поэтому присваивание result=result+i эквивалентно result=1. Таким образом, после первого шага цикла в переменной result будет храниться значение 1. - После выполнения тела цикла выполняется секция изменения счётчика цикла, то есть оператор i++, увеличивающий i на 1. Значение i становится равным 2. - Начинается второй шаг цикла. Проверяется условие выполнения тела цикла. Поскольку сравнение 2<=100 возвращает true, идёт очередное выполнение тела цикла, а затем – увеличение счётчика цикла. - Шаги цикла продолжаются до тех пор, пока счётчик цикла не станет равным 101. В этом случае условие выполнения тела цикла 101<=100 возвращает false, и происходит выход из цикла. Последнее присваивание result=result+i, проведённое в цикле, это result=result+100. Если бы нам надо было просуммировать числа от 55 до 1234, в блоке инициализации i надо присвоить 55, а в условии проверки поставить 1234 вместо 100. Пример 2: вычисление факториала. double x=1; for(i=1;i<=n;i++){ x=x*i; }; Заметим, что в приведённых примерах можно сделать некоторые усовершенствования -операторы присваивания записать следующим образом: result+=i; вместо result=result+i; для первого примера и x*=i; вместо x=x*i; для второго. На начальных стадиях обучения так лучше не делать, поскольку текст программы должен быть понятен программисту – все алгоритмы должны быть “прозрачны” для понимания. Наиболее распространённая ошибка при работе с циклами, в том числе – с циклом for - использование вещественного счётчика цикла. Разберём эту ошибку на примере. Пример 3. Вычисление площади под кривой. Пусть надо вычислить площадь S под кривой, задаваемой функцией f(x), на промежутке от a до b (провести интегрирование). Разобьём промежуток на n интервалов, при этом длина каждого интервала будет h=(b-a)/n. Мы предполагаем, что величины a,b,n и функция f(x) заданы. Площадь под кривой на интервале с номером j будем считать равной значению функции на левом конце интервала. Такой метод численного нахождения интеграла называется методом левых прямоугольников. На первый взгляд можно записать алгоритм вычисления этой площади в следующем виде: double S=0; double h=(b-a)/n; for(double x=a;x<b;x=x+h){ S=S+f(x)*h; }; И действительно, ИНОГДА такой алгоритм правильно работает. Но изменение числа интервалов n или границ a или b может привести к тому, что будет учтён лишний интервал, находящийся справа от точки b. Это связано с тем, что в циклах с вещественным счётчиком ВСЕГДА проявляется нестабильность, если последняя точка попадает на границу интервала. Что будет происходить на последних шагах нашего цикла? На предпоследнем шаге мы попадаем в точку x=b-h, при этом условие x<b всегда выполняется, и никаких проблем не возникает. Следующей точкой должна быть x=b. Проверяется условие x<b, и в идеальном случае должен происходить выход из цикла, поскольку x==b, и условие не должно выполняться. Но ведь все операции в компьютере для чисел в формате с плавающей точкой проводятся с конечной точностью. Поэтому практически всегда значение x для этого шага будет либо чуть меньше b, либо чуть больше. Отличие будет в последних битах мантиссы, но этого окажется достаточно для того, чтобы сравнение x<b иногда давало true. Хотя для заданных a, b и n результат будет прекрасным образом воспроизводиться, в том числе – на других компьютерах. Более того, при увеличении числа разбиений n погрешность вычисления площади даже для “неправильного” варианта будет убывать, хотя и гораздо медленнее, чем для “правильного”. Это один из самых неприятных типов ошибок, когда алгоритм вроде бы работает правильно, но для получения нужной точности требуется гораздо больше времени или ресурсов. Рассмотрим теперь правильную реализацию алгоритма. Для этого будем использовать ЦЕЛОЧИСЛЕННЫЙ счётчик цикла. Нам потребуется чуть больше рассуждений, и алгоритм окажется немного менее прозрачным, но зато гарантируется его устойчивость. Значение функции в начале интервала с номером j будет равна f(a+j*h). Первый интервал будет иметь номер 0, второй – номер 1, и так далее. Последний интервал будет иметь номер n-1. Правильно работающий алгоритм может выглядеть так: double S=0; double h=(b-a)/n; for(int j=0;j<=n-1;j++){ S=S+f(a+j*h)*h; }; Проверить неустойчивость первого алгоритма и устойчивость второго можно на примере f(x)=x2 , a=0, b=1.
В таблице жирным выделены первые значащие цифры неправильных значений, получающихся в результате неустойчивости алгоритма при n=10, n=13, n=102, n=103, n=1001. При отсутствии неустойчивости оба алгоритма при всех n должны были бы давать одинаковые результаты (с точностью до нескольких младших бит мантиссы). Очень характерной особенностью такого рода неустойчивостей является скачкообразное изменение результата при плавном изменении какого-либо параметра. В приведённой выше таблице меняется число n, но такая же ситуация будет наблюдаться и при плавном изменении чисел a или b. Например, при n=10 и a=0 получим следующие результаты в случае очень малых изменений b:
Вещественный счётчик цикла не всегда приводит к проблемам. Рассмотрим вариант численного нахождения интеграла методом средних прямоугольников. Для этого площадь под кривой на интервале с номером j будем считать равной значению функции в середине интервала. Алгоритм идентичен описанному выше для метода левых прямоугольников, за исключением выбора в качестве начальной точки a+h/2 вместо a. double S=0; h=(b-a)/n; for(double x=a+h/2;x<b;x=x+h){ S=S+f(x)*h; }; Для данного алгоритма проблем не возникает благодаря тому, что все точки x, в которых производится сравнение x<b, отстоят достаточно далеко от значения b – по крайней мере на h/2. Заметим, что метод средних прямоугольников гораздо точнее метода левых прямоугольников, и в реальных вычислениях лучше использовать либо его, либо метод Симпсона, который несколько сложнее, но обычно ещё более точен. Отметим ещё один момент, важный для эффективной организации циклов. Предложенные выше реализации циклов не самые эффективные по скорости, поскольку в них много раз повторяется достаточно медленная операция – “вещественное” умножение (умножение в формате с плавающей точкой). Лучше написать алгоритм метода средних прямоугольников так: double S=0; double h=(b-a)/n; for(double x=a+h/2;x<b;x=x+h){ S=S+f(x); }; S=S*h; При этом суммируются значения f(x), а не f(x)*h, а умножение делается только один раз – уже после выхода из цикла для всей получившейся суммы. Если время выполнения “вещественного”умножения tумнож , то экономия времени работы программы по сравнению с первоначальным вариантом будет (n-1)* tумнож . Для больших значений n экономия времени может быть существенной даже для мощных компьютеров. Такого рода действия очень характерны при написании программ с использованием циклов, и особенно важны при большом количестве шагов: следует выносить из циклов все операции, которые могут быть проделаны однократно вне цикла. Но при усовершенствованиях часто теряется прозрачность алгоритмов. Поэтому полезно сначала написать реализацию алгоритма “один к одному” по формулам, без всяких усовершенствований, и убедиться при не очень большом числе шагов, что всё работает правильно. А уже затем можно вносить исправления, повышающие скорость работы программы в наиболее критических местах. Не следует сразу пытаться написать программу, которая максимально эффективна по всем параметрам. Это обычно приводит к гораздо более длительному процессу поиска неочевидных ошибок в такой программе. Замечание: В Java отсутствует специальная форма оператора for для перебора в цикле элементов массивов и коллекций (или, что то же, наборов). Тем не менее оператор for позволяет последовательно обработать все элементы массива или набора. Пример поочерёдного вывода диалогов со значениями свойств компонентов, являющихся элементами массива компонентов главной формы приложения: java.util.List components= java.util.Arrays.asList(this.getComponents()); for (Iterator iter = components.iterator();iter.hasNext();) { Object elem = (Object) iter.next(); javax.swing.JOptionPane.showMessageDialog(null,"Компонент: "+ elem.toString()); } Оператор цикла while – цикл с предусловиемwhile(условие) оператор; Пока условие сохраняет значение true — в цикле выполняется оператор, иначе — действие цикла прекращается. Если условие с самого начала false, цикл сразу прекращается, и тело цикла не выполнится ни разу. Цикл while обычно применяют вместо цикла for в том случае, если условия продолжения достаточно сложные. В отличие от цикла for в этом случае нет формально заданного счётчика цикла, и не производится его автоматического изменения. За это отвечает программист. Хотя вполне возможно использование как цикла for вместо while , так и наоборот. Многие программисты предпочитают пользоваться только циклом for как наиболее универсальным. Пример: i=1; x=0; while(i<=n){ x+=i;//эквивалентно x=x+i; i*=2;//эквивалентно i=2*i; }; В операторе while очень часто совершают ошибки, приводящие к неустойчивости алгоритмов из-за сравнения чисел с плавающей точкой на неравенство. Как мы знаем, сравнивать их на равенство в подавляющем большинстве случаев некорректно из-за ошибок представления таких чисел в компьютере. Но большинство программистов почему-то считает, что при сравнении на неравенство проблем не возникает, хотя это не так. Например, если организовать с помощью оператора while цикл с вещественным счётчиком, аналогичный разобранному в разделе, посвящённому циклу for. Пример типичной ошибки в организации такого цикла приведён ниже: double a=…; double b=…; double dx=…; double x=a; while(x<=b){ … x=x+dx; }; Как мы уже знаем, данный цикл будет обладать неустойчивостью в случае, когда на интервале от a до b укладывается целое число шагов. Например, при a=0, b=10, dx=0.1 тело цикла будет выполняться при x=0, x=0.1, …, x=9.9. А вот при x=10 тело цикла может либо выполниться, либо не выполниться – как повезёт! Причина связана с конечной точностью выполнения операций с числами в формате с плавающей точкой. Величина шага dx в двоичном представлении чуть-чуть отличается от значения 0.1, и при каждом цикле систематическая погрешность в значении x накапливается. Поэтому точное значение x=10 достигнуто не будет, величина x будет либо чуть-чуть меньше, либо чуть-чуть больше. В первом случае тело цикла выполнится, во втором – нет. То есть пройдёт либо 100, либо 101 итерация (число выполнений тела цикла). Оператор цикла do... while – цикл с постусловиемdo оператор; while(условие); Если условие принимает значение false, цикл прекращается. Тело цикла выполняется до проверки условия, поэтому оно всегда выполнится хотя бы один раз. Пример: int i=0; double x=1; do{ i++; // i=i+1; x*=i; // x=x*i; } while(i<n); Если с помощью оператора do…while организуется цикл с вещественным счётчиком или другой проверкой на равенство или неравенство чисел типа float или double, у него возникают точно такие же проблемы, как описанные для циклов for и while. При необходимости организовать бесконечный цикл (с выходом изнутри тела цикла с помощью оператора прерывания) часто используют следующий вариант: do{ … } while(false); Операторы прерывания continue, break, return, System.exitДовольно часто требуется при выполнении какого-либо условия прервать цикл или подпрограмму и перейти к выполнению другого алгоритма или очередной итерации цикла. При неструктурном программировании для этих целей служил оператор goto. В Java имеются более гибкие и структурные средства для решения этих проблем - операторы continue, break, return, System.exit: continue; – прерывание выполнения тела цикла и переход к следующей итерации (проверке условия) текущего цикла; continue имя метки ; – прерывание выполнения тела цикла и переход к следующей итерации (проверке условия) цикла, помеченного меткой (label); break; – выход из текущего цикла; break имя метки ; – выход из цикла, помеченного меткой; return; – выход из текущей подпрограммы (в том числе из тела цикла) без возврата значения; return значение ; – выход из текущей подпрограммы (в том числе из тела цикла) с возвратом значения; System.exit(n) –выход из приложения с кодом завершения n. Целое число n произвольно задаётся программистом. Если n=0, выход считается нормальным, в других случаях - аварийным. Приложение перед завершением сообщает число n операционной системе для того, чтобы программист мог установить, по какой причине произошёл аварийный выход. Операторы continue и break используются в двух вариантах – без меток для выхода из текущего (самого внутреннего по вложенности) цикла, и с меткой - для выхода из помеченного ей цикла. Меткой является идентификатор, после которого стоит двоеточие. Метку можно ставить непосредственно перед ключевым словом, начинающим задание цикла (for, while, do). Пример использования continue без метки: for(int i=1;i<=10;i++){ if(i==(i/2)*2){ continue; }; System.out.println("i="+i); }; В данном цикле не будут печататься все значения i, для которых i==(i/2)*2. То есть выводиться в окно консоли будут только нечётные значения i. Ещё один пример использования continue без метки: for(int i=1;i<=20;i++){ for(int j=1;j<=20;j++){ if(i*j==(i*j/2)*2){ continue; }; System.out.println("i="+i+" j="+j+ " 1.0/(i*j-20)="+ (1.0/(i*j-20)) ); }; }; В этом случае будут выводиться значения i, j и 1.0/(i*j-20) для всех нечётных i и j от 1 до 19 . То есть будут пропущены значения для всех чётных i и j: i=1 j=1 1.0/(i*j-20)=-0.05263157894736842 i=1 j=3 1.0/(i*j-20)=-0.058823529411764705 i=1 j=5 1.0/(i*j-20)=-0.06666666666666667 i=1 j=7 1.0/(i*j-20)=-0.07692307692307693 i=1 j=9 1.0/(i*j-20)=-0.09090909090909091 i=1 j=11 1.0/(i*j-20)=-0.1111111111111111 i=1 j=13 1.0/(i*j-20)=-0.14285714285714285 i=1 j=15 1.0/(i*j-20)=-0.2 i=1 j=17 1.0/(i*j-20)=-0.3333333333333333 i=1 j=19 1.0/(i*j-20)=-1.0 i=3 j=1 1.0/(i*j-20)=-0.058823529411764705 i=3 j=3 1.0/(i*j-20)=-0.09090909090909091 i=3 j=5 1.0/(i*j-20)=-0.2 i=3 j=7 1.0/(i*j-20)=1.0 ... i=19 j=9 1.0/(i*j-20)=0.006622516556291391 i=19 j=11 1.0/(i*j-20)=0.005291005291005291 i=19 j=13 1.0/(i*j-20)=0.004405286343612335 i=19 j=15 1.0/(i*j-20)=0.0037735849056603774 i=19 j=17 1.0/(i*j-20)=0.0033003300330033004 i=19 j=19 1.0/(i*j-20)=0.002932551319648094 Пример использования continue с меткой: label_for1: for(int i=1;i<=20;i++){ for(int j=1;j<=20;j++){ if(i*j==(i*j/2)*2){ continue label_for1; }; System.out.println("i="+i+" j="+j+ " 1.0/(i*j-20)="+ (1.0/(i*j-20)) ); }; }; В отличие от предыдущего случая, после каждого достижения равенства i*j==(i*j/2)*2 будет производиться выход из внутреннего цикла (по j), и все последующие j для таких значений i будут пропущены. Поэтому будут выведены только значения i=1 j=1 1.0/(i*j-20)=-0.05263157894736842 i=3 j=1 1.0/(i*j-20)=-0.058823529411764705 i=5 j=1 1.0/(i*j-20)=-0.06666666666666667 i=7 j=1 1.0/(i*j-20)=-0.07692307692307693 i=9 j=1 1.0/(i*j-20)=-0.09090909090909091 i=11 j=1 1.0/(i*j-20)=-0.1111111111111111 i=13 j=1 1.0/(i*j-20)=-0.14285714285714285 i=15 j=1 1.0/(i*j-20)=-0.2 i=17 j=1 1.0/(i*j-20)=-0.3333333333333333 i=19 j=1 1.0/(i*j-20)=-1.0 Пример использования break без метки: for(int i=1;i<=10;i++){ if( i+6== i*i ){ break; }; System.out.println("i="+i); }; Данный цикл остановится при выполнении условия i+6== i*i . То есть вывод в окно консоли будет только для значений i, равных 1 и 2. Ещё один пример использования break без метки: for(int i=1;i<=20;i++){ for(int j=1;j<=20;j++){ if(i*j==(i*j/2)*2){ break; }; System.out.println("i="+i+" j="+j+ " 1.0/(i*j-20)="+ (1.0/(i*j-20)) ); }; }; В этом случае будут выводиться все значения i и j до тех пор, пока не найдётся пара i и j, для которых i*j==(i*j/2)*2. После чего внутренний цикл прекращается – значения i, j и 1.0/(i*j-20) для данного и последующих значений j при соответствующем i не будут выводиться в окно консоли. Но внешний цикл (по i) будет продолжаться, и вывод продолжится для новых i и j. Результат будет таким же, как для continue с меткой для внешнего цикла. Пример использования break с меткой: label_for1: for(int i=1;i<=20;i++){ for(int j=1;j<=20;j++){ if(i*j==(i*j/2)*2){ break label_for1; }; System.out.println("i="+i+" j="+j+ " 1.0/(i*j-20)="+ (1.0/(i*j-20)) ); }; }; В этом случае также будут выводиться все значения i и j до тех пор, пока не найдётся пара i и j, для которых i*j==(i*j/2)*2. После чего прекращается внешний цикл, а значит – и внутренний тоже. Так что вывод в окно консоли прекратится. Поэтому вывод будет только для i=1, j=1. - В Java имеются: условный оператор if, оператор выбора case, условное выражение …?... : … , операторы инкремента ++ и декремента -- , - В Java имеются операторы цикла: for , while – цикл с предусловием, do...while – цикл с постусловием. А также операторы прерывания циклов continue и break, подпрограмм -return, программы - System.exit. - Сравнение на равенство чисел в формате с плавающей точкой практически всегда некорректно. Но даже сравнение на неравенство таких чисел нельзя выполнять в случае, когда в точной арифметике эти числа должны быть равны. По этой причине нельзя использовать циклы с вещественным счётчиком в случаях, когда на интервале изменения счётчика укладывается целое число шагов счётчика.
Глава 6. Начальные сведения об объектном программировании В части 2 мы уже познакомились с первым принципом объектного программирования – инкапсуляцией . Затем научились пользоваться уже готовыми классами. Это – начальная стадия изучения объектного программирования. Для того чтобы овладеть его основными возможностями, требуется научиться создавать собственные классы, изменяющие и усложняющие поведение существующих классов. Важнейшими элементами такого умения является использование наследования и полиморфизма . Наследование и полиморфизм. UML-диаграммы Наследование опирается на инкапсуляцию. Оно позволяет строить на основе первоначального класса новые, добавляя в классы новые поля данных и методы. Первоначальный класс называется прародителем (ancestor), новые классы – его потомками (descendants). От потомков, в свою очередь, можно наследовать, получая очередных потомков. И так далее. Набор классов, связанных отношением наследования, называется иерархией классов . А класс, стоящий во главе иерархии, от которого унаследованы все остальные (прямо или опосредованно), называется базовым классом иерархии . В Java все классы являются потомками класса Object. То есть он является базовым для всех классов. Тем не менее, если рассматривается поведение, характерное для объектов какого-то класса и всех потомков этого класса, говорят об иерархии, начинающейся с этого класса, В этом случае именно он является базовым классом иерархии. Полиморфизм опирается как на инкапсуляцию, так и на наследование. Как показывает опыт преподавания, это наиболее сложный для понимания принцип. Слово “полиморфизм” в переводе с греческого означает “имеющий много форм”. В объектном программировании под полиморфизмом подразумевается наличие кода, написанного для объектов, имеющих тип базового класса иерархии. При этом такой код должен правильно работать для любого объекта, являющегося экземпляром класса из данной иерархии. Независимо от того, где этот класс расположен в иерархии. Такой код и называется полиморфным . При написании полиморфного кода заранее неизвестно, для объектов какого типа он будет работать – один и тот же метод будет исполняться по-разному в зависимости от типа объекта. Пусть, например, у нас имеется класс Figure-“фигура”, и в нём заданы методы show()– показать фигуру на экране, и и hide() - скрыть её. Тогда для переменной figure типа Figure вызовы figure.show() и figure.hide() будут показывать или скрывать объект, на который ссылается эта переменная. Причём сам объект “знает”, как себя показывать или скрывать, а код пишется на уровне абстракций этих действий. Основное преимущество объектного программирования по сравнению с процедурным как раз и заключается в возможности написания полиморфного кода. Именно для этого пишется иерархия классов. Полиморфизм позволяет резко увеличить коэффициент повторного использования программного кода и его модифицируемость по сравнению с процедурным программированием. В качестве примера того, как строится иерархия, рассмотрим иерархию фигур, отрисовываемых на экране – она показана на рисунке. В ней базовым классом является Figure, от которого наследуются Dot – “точка”, Triangle – “треугольник” и Square – “квадрат”. От Dot наследуется класс Circle – “окружность”, а от Circle унаследуем Ellipse – “эллипс”. И, наконец, от Square унаследуем Rectangle – “прямоугольник”. Отметим, что в иерархии принято рисовать стрелки в направлении от наследника к прародителю. Такое направление называется Generalization – “обобщение”, “генерализация”. Оно противоположно направлению наследования, которое принято называть Specialization – “специализация”. Стрелки символизируют направление в сторону упрощения. Иерархия фигур, отрисовываемых на экране Часто класс-прародитель называют суперклассом (superclass), а класс-наследник – субклассом (subclass). Но такая терминология подталкивает начинающих программистов к неверной логике: суперкласс пытаются сделать “суперсложным”. Так, чтобы его подклассы (это неверно воспринимается синонимом выражению “упрощённые разновидности”) обладали упрощённым по сравнению с ним поведением. На деле же потомки должны обладать более сложным устройством и поведением по сравнению прародителем . Поэтому в данном учебном пособии предпочтение отдаётся терминам “прародитель” и “наследник”. Чем ближе к основанию иерархии лежит класс, тем более общим и универсальным (general) он является. И одновременно – более простым. Класс, который лежит в основе иерархии, называется базовым классом этой иерархии. Базовый класс всегда называют именем, которое характеризует все объекты - экземпляры классов-наследников, и которое выражает наиболее общую абстракцию, применимую к таким объектам. В нашем случае это класс Figure. Любая фигура будет иметь поля данных x и y – координаты фигуры на экране. Класс Dot (“точка”) является наследником Figure, поэтому он будет иметь поля данных x и y, наследуемые от Figure. То есть в самом классе Dot задавать эти поля не надо. От Dot мы наследуем класс Circle (“окружность”), поэтому в нём также имеется поля x и y, наследуемые от Figure. Но появляется дополнительное поля данных. У Circle это поле, соответствующее радиусу. Мы назовём его r. Кроме того, для окружности возможна операция изменения радиуса, поэтому в ней может появиться новый метод, обеспечивающий это действие – назовём его setSize (“установить размер”). Класс Ellipse имеет те же поля данных и обеспечивает то же поведение, что и Circle, но в этом классе появляется дополнительное поле данных r2 – длина второй полуоси эллипса, и возможность регулировать значение этого поля. Возможен и другой подход, в некотором роде более логичный: считать эллипс сплюснутой или растянутой окружностью. В этом случае необходимо ввести коэффициент растяжения (aspect ratio). Назовём его k. Тогда эллипс будет характеризоваться радиусом r и коэффициентом растяжения k. Метод, обеспечивающий изменение k, назовём stretch (“растянуть”). Обратим внимание, что исходя из выбранной логики действий метод scale должен приводить к изменению поля r и не затрагивать поле k – поэтому эллипс будет масштабироваться без изменения формы. Каждый из классов этой ветви иерархии фигур можно считать описанием “усложнённой точки”. При этом важно, что любой объект такого типа можно считать “точкой, которую усложнили”. Грубо говоря, считать, что круг или эллипс – это такая “жирная точка”. Аналогичным образом Ellipse является “усложнённой окружностью” Аналогично, класс Square наследует поля x и y, но в нём добавляется поле, соответствующее стороне квадрата. Мы назовём его a. У Triangle в качестве новых, не унаследованных полей данных могут выступать координаты вершин треугольника; либо координаты одной из вершин, длины прилегающих к ней сторон и угол между ними, и так далее. Как располагать классы иерархии, базовый класс внизу а наследники вверху, образуя ветви дерева наследования, или наоборот, базовый класс вверху а наследники внизу, образуя “корни” дерева наследования – принципиального значения не имеет. По-видимому, на начальном этапе развития объектного программирования применялся первый вариант, почему базовый класс, лежащий в основе иерархии, и получил такое название. Такой вариант выбран в данном учебном пособии, поскольку именно он используется в NetBeans Enterprise Pack. Хотя настоящее время чаще используют второй вариант, когда базовый класс располагают сверху. В литературе по объектному программированию часто встречается следующий критерий: “если имеются классы A1 и A2, и можно сказать, что A2 является частным случаем A1, то A2 должен описываться как потомок A1”. Данный критерий не совсем корректен. Очень часто встречающийся вариант ошибочных рассуждений, основанный на нём, и приводящий к неправильному построению иерархии , выглядит так: “поскольку Circle является частным случаем Ellipse (при равных длинах полуосей), а Dot является частным случаем Circle (при нулевом радиусе), то класс Ellipse более общий, чем Circle, а Circle – более общий, чем Dot. Поэтому Ellipse должен являться прародителем для Circle, а Circle должен являться прародителем для Dot ”. Ошибка заключается в неправильном понимании идей “общности” и “специализации”, а также характерной путанице, когда объекты не отличают от классов. Каждый объект класса-потомка при любых значениях полей должен рассматриваться как экземпляр класса-прародителя, и с тем же поведением на уровне абстракции действий. Но только с некоторыми изменениями на уровне реализации этих действий. В концепции наследования основное внимание уделяется поведению объектов. Объекты с разным поведением имеют другой тип. А значения полей данных характеризуют состояние объекта, но не его тип. Мы говорим про абстракции поведения как на те характерные действия, которые могут быть описаны на уровне полиморфного кода, безотносительно к конкретной реализации в конкретном классе. По своему поведению любой объект-эллипс вполне может рассматриваться как экземпляр типа “Окружность” и даже вести себя в точности как окружность. Но не наоборот - объекты типа Окружность не обладает поведением Эллипса. Мы намеренно используем заглавные буквы для того, чтобы не путать классы с объектами. Если для эллипса можно изменить значение aspectRatio ( вызвать метод setAspectRatio (новое значение ) ), то для окружности такая операция не имеет смысла или запрещена. Аналогично, и для эллипса, и для окружности имеет смысл операция установки нового размера setSize(новое значение ), а для точки она не имеет смысла или запрещена. И даже если построить неправильную иерархию Ellipse-Circle-Dot и унаследовать от Ellipse эти методы в Circle и Dot, возникнет проблема с их переопределением. Если setAspectRatio будет менять отношение полуосей нашей “окружности” – она перестанет быть окружностью. Аналогично, если setSize изменит размер точки – та перестанет быть точкой. Если же сделать эти методы ничего не делающими “заглушками” – экземпляры таких потомков не смогут обладать поведением прародителя. Например, мы не сможем вписать окружность в прямоугольник, установив нужное значение aspectRatio – найдутся только две точки, общие для окружности и сторон прямоугольника, а не четыре, как для объекта типа Ellipse. То есть объект типа Circle на уровне абстракции поведения во многих случаях не сможет обладать всеми особенностями поведения объекта типа Ellipse. А значит, Circle не может быть потомком Ellipse. Можно привести нескончаемое число других примеров того, какие ситуации окажутся нереализуемыми для объектов таких неправильных иерархий. А отдельные хитрости, позволяющие выпутываться из некоторых из таких ситуаций, обычно бывают крайне искусственными, не позволяют решить проблему с очередной внезапно возникшей ситуацией, и только усложняют программу. Сформулируем критерий того, когда следует использовать наследование, более корректно: “если имеются классы A 1 и A 2, и можно считать, что A 2 является модифицированным (усложнённым или изменённым) вариантом A 1 с сохранением всех особенностей поведения A 1 , то A 2 должен описываться как потомок A 1. - На уровне абстракции, описывающей поведение, объект типа A 2 должен вести себя, как объект типа A 1 при любых значениях полей данных ”. Специализированный класс, вообще говоря, должен быть устроен более сложно (“расширенно” - extended) по сравнению с прародительским. У него должны иметься дополнительные поля данных и/или дополнительные методы. С этой точки зрения очевидно, что Окружность более специализирована, чем Точка, а Эллипс более специализирован, чем Окружность. Иногда встречаются ситуации, когда потомок отличается от прародителя только своим поведением. У него не добавляется новых полей или методов, а только переопределяется часть методов (возможно, только один). Отметим, что поля или методы, имеющиеся в прародителе, не могут отсутствовать в наследнике – они наследуются из прародителя. Даже если доступ к ним в классе-наследнике закрыт (так бывает в случае, когда поле или метод объявлены с модификатором видимости private – “закрытый”, “частный”). Когда про класс-потомок можно сказать, что он является специализированной разновидностью класса-прародителя (“B есть A”), всё очевидно. Но в объектном программировании иногда приходится использовать отношение “Класс B похож на A - имеет те же поля данных, плюс, возможно, дополнительные, но обладает несколько иным поведением”. Любой наш объект мы можем назвать фигурой. Поэтому то, что базовый класс нашей иерархии называется Figure, естественно и однозначно. И однозначно то, что все классы нашей иерархии должны быть его наследниками. А вот остальные элементы иерархии можно было бы устроить совсем по-другому. Например, так, как показано на следующем рисунке. Альтернативный вариант иерархии фигур Возможно и такое решение: все указанные классы сделать наследниками Figure и расположить на одном уровне наследования. Ещё один вариант иерархии фигур Возможны и другие варианты, ничуть не менее логичные. Какой вариант выбрать? Уже на этом простейшем примере мы убеждаемся, что проектирование иерархии – очень многовариантная задача. И требуется большой опыт, чтобы грамотно построить иерархию. В противном случае при написании кода классов не удаётся в полной мере обеспечить их функциональность, а код классов становится неуправляемым – внесение исправления в одном месте приводит к возникновению ошибок в совсем других местах. Причём возникает ошибок больше, чем исправляется. Один из важных принципов при построении таких иерархий – соответствие представлений из предметной области строящейся иерархии . В примере, приведённом на первом рисунке, мы имеем вполне логичную с точки зрения идеологии наследования иерархию, показанную на первом рисунке. С точки зрения общности/специализации такая иерархия безупречна. По этой причине она удобна для написания учебных программ, иллюстрирующих работу с классами-наследниками, совместимостью объектных типов, а также написанием полиморфного кода. Но в геометрии, из которой мы знаем о свойствах этих фигур, считается, что окружность является частным случаем эллипса, а точка – частным случаем окружности (а значит, и эллипса). Так как значения полей данных объекта задают его состояние, в некоторых случаях объекты, являющиеся Эллипсами по типу (внутреннему устройству), окажутся в состоянии, когда с точки предметной области они будут являться окружностями. Хотя по внутреннему устройству и будут отличаться от объектов-Окружностей. Поэтому данная иерархия может вызывать внутренний протест у многих людей. Особенно учитывая сложность различения классов и объектов в обычной речи и при не очень строгих рассуждениях (а можно ли всегда рассуждать абсолютно строго?). Поэтому такое решение может приводить к логическим ошибкам в рассуждениях. Вот почему последний из предложенных вариантов иерархий, когда все классы наследуются непосредственно от Figure, во многих случаях предпочтителен. Тем более, что никакого выигрыша при написании программного кода увеличение числа поколений наследования не даёт: код, написанный для класса Dot, вряд ли будет использоваться для объектов классов Circle и Ellipse. А ведь наследование само по себе не нужно – это инструмент для написания более экономного полиморфного кода. Более того, увеличение числа поколений приводит к снижению надёжности кода. Так что им не следует злоупотреблять. (Об этом подробнее говорится в одном из параграфов главы 8). На выбор варианта иерархии оказывают заметное влияние соображения повторного использования кода – если бы класс Ellipse активно использовал часть кода, написанного для класса Circle, а тот, в свою очередь, активно пользовался кодом класса Dot, выбор первого варианта мог бы стать предпочтительным по сравнению с третьим. Даже несмотря на некоторый конфликт с “обыденными” (не принципиальными!) представлениями предметной области. Но имеется одна возможность, которую можно реализовать, попытавшись совместить идеи, возникшие при попытках построить предыдущие варианты нашей иерархии. Мы пришли к выводу, что фигуры могут быть масштабируемы (без изменения формы, оставаясь подобными), а также растягиваемы. Поэтому можно ввести классы ScalableFigure (“масштабируемая фигура”) и StretchableFigure (“растягиваемая фигура”). Точка Dot не является ни масштабируемой, ни растягиваемой. Очевидно, что любая растягиваемая фигура должна быть масштабируемая. Окружность Circle и квадрат Square масштабируемы, но не растягиваемы. А прямоугольник Rectangle, эллипс Ellipse и треугольник Triangle как масштабируемы, так и растягиваемы. Поэтому наша иерархия будет выглядеть так: Итоговый вариант иерархии фигур Основное её преимущество по сравнению с предыдущими – возможность писать полиморфный код для наиболее общих разновидностей фигур. Введение промежуточных уровней наследования, отвечающих соответствующим абстракциям, является характерной чертой объектного программирования. При этом классы Figure, ScalableFigure и StretchableFigure будут абстрактными – экземпляров такого типа создавать не предполагается. Так как не бывает “фигуры”, “масштабируемой фигуры” или “растягиваемой фигуры” в общем виде, без указания её конкретной формы. Точно так же методы show и hide для этих классов также будут абстрактными. Ещё один важный принцип при построении иерархий на первый взгляд может показаться достаточно странным и противоречащим требованию повторного использования кода. Его можно сформулировать так: не использовать код неабстрактных классов для наследования . Можно заметить, что в приведённой иерархии несколько этапов наследования приходятся именно на абстрактные классы, и ни один из классов, имеющих экземпляры (объекты), не имеет наследников . Причина такого требования проста: изменение реализации одного класса, проводимое не на уровне абстракции, а относящееся только к одному конкретному классу, не должна влиять на поведение другого класса. Иначе возможны неотслеживаемые труднопонимаемые ошибки в работе иерархии классов. Например, если мы попробуем унаследовать класс Ellipse от Circle, после исправлений в реализации Circle, обеспечивающих правильную работу объектов этого типа, могут возникнуть проблемы при работе объектов типа Ellipse, которые до того работали правильно. Причём речь идёт об особенностях реализации конкретного класса, не относящихся к абстракциям поведения. Продумывание того, как устроены классы, то есть какие в них должны быть поля и методы (без уточнения об конкретной реализации этих методов), и описание того, какая должна быть иерархия наследования, называется проектированием . Это сложный процесс, и он обычно гораздо важнее написания конкретных операторов в реализации (кодирования ). В языке Java, к сожалению, отсутствуют адекватные средства для проектирования классов. Более того, в этом отношении он заметно уступает таким языкам как C++ или Object PASCAL, поскольку в Java отсутствует разделение декларации класса (описание полей и заголовков методов) и реализации методов. Но в Sun Java Studio и NetBeans Enterprise Pack имеется средство решения этой проблемы – создание UML-диаграмм. UML расшифровывается как Universal Modeling Language – Универсальный Язык Моделирования. Он предназначен для моделирования на уровне абстракций классов и связей их друг с другом – то есть для задач Объектно-Ориентированного Проектирования (OOA – Object-Oriented Architecture). Приведённые выше рисунки иерархий классов – это UML-диаграммы, сделанные с помощью NetBeans Enterprise Pack. Пока в этой среде пока нет возможности по UML-диаграммам создавать заготовки классов Java, как это делается в некоторых других средах UML-проектирования. Но если создать пустые заготовки классов, то далее можно разрабатывать соответствующие им UML-диаграммы, и внесённые изменения на диаграммах будут сразу отображаться в исходном коде. Как это делается будет подробно описано в последнем параграфе данной главы, где будет обсуждаться технология Reverse Engineering. Функции. Модификаторы. Передача примитивных типов в функции Основой создания новых классов является задание полей данных и методов. Но если поля отражают структуру данных, связанных с объектом или классом, то методы задают поведение объектов, а также работу с полями данных объектов и классов. Формат объявления функции следующий: Модификаторы Тип Имя (список параметров ){ Тело функции } Это формат “простой” функции, не содержащей операторов, возбуждающих исключительные ситуации. Про исключительные ситуации и формат объявления функции, которая может возбуждать исключительную ситуацию, речь пойдёт в одном из следующих параграфов. Комбинация элементов декларации метода Модификаторы Тип Имя (список параметров ) называется заголовком метода. Модификаторы – это зарезервированные слова, задающие - Правила доступа к методу (private , protected , public ). Если модификатор не задан, действует доступ по умолчанию – так называемый пакетный. - Принадлежность к методам класса (static ). Если модификатор не задан, считается, что это метод объекта. - Невозможность переопределения метода в потомках (final ). Если модификатор не задан, считается, что это метод можно переопределять в классах-потомках. - Способ реализации (native – заданный во внешней библиотеке DLL, написанной на другом языке программирования; abstract – абстрактный, не имеющий реализации). Если модификатор не задан, считается, что это обычный метод. - Синхронизацию при работе с потоками (synchronized ) . В качестве Типа следует указать тип результата, возвращаемого методом. В Java, как мы уже знаем, все методы являются функциями, возвращающими значение какого-либо типа. Если требуется метод, не возвращающий никакого значения (то есть процедура), он объявляется с типом void. Возврат значения осуществляется в теле функции с помощью зарезервированного слова return. Для выхода без возврата значения требуется написать return; Для выхода с возвратом значения требуется написать return выражение ; Выражение будет вычислено, после чего полученное значение возвратится как результат работы функции. Оператор return осуществляет прерывание выполнения подпрограммы, поэтому его обычно используют в ветвях операторов if-else или switch-case в случаях, когда необходимо возвращать тот или иной результат в зависимости от различных условий. Если в подпрограмме-функции в какой-либо из ветвей не использовать оператор return, будет выдана ошибка компиляции с диагностикой “missing return statement” – “ошибочное высказывание с return”. Список параметров – это объявление через запятую переменных, с помощью которых можно передавать значения и объекты в подпрограмму снаружи, “из внешнего мира”, и передавать объекты из подпрограмму наружу, “во внешний мир”. Объявление параметров имеет вид тип1 имя1, тип2 имя2,…, тип N имя N Если список параметров пуст, пишут круглые скобки без параметров. Тело функции представляет последовательность операторов, реализующую необходимый алгоритм. Эта последовательность может быть пустой, в этом случае говорят о заглушке – то есть заготовке метода, имеющей только имя и список параметров, но с отсутствующей реализацией. Иногда в такой заглушке вместо “правильной” реализации временно пишут операторы служебного вывода в консольное окно или в файл. Внутри тела функции в произвольном месте могут задаваться переменные. Они доступны только внутри данной подпрограммы и поэтому называются локальными . Переменные, заданные на уровне класса (поля данных класса или объекта), называются глобальными . Данные (значения или объекты) можно передавать в подпрограмму либо через список параметров, либо через глобальные переменные. Сначала рассмотрим передачу в подпрограмму через список параметров значений примитивного типа. Предположим, что мы написали в классе MyMath метод mult1 умножения двух чисел, каждое из которых перед умножением увеличивается на 1. Он может выглядеть так: double mult1(double x, double y){ x++; y++; return x*y; } Вызов данного метода может выглядеть так: double a,b,c; … MyMath obj1=new MyMath();//создали объект типа MyMath … c=obj1.mult1(a+0.5,b); Параметры, указанные в заголовке функции при её декларации, называются формальными . А те параметры, которые подставляются во время вызова функции, называются фактическими . Формальные параметры нужны для того, чтобы указать последовательность действий с фактическими параметрами после того, как те будут переданы в подпрограмму во время вызова. Это ни что иное, как особый вид локальных переменных, которые используются для обмена данными с внешним миром. В нашем случае x и y являются формальными параметрами, а выражения a+0.5 и b – фактическими параметрами. При вызове сначала проводится вычисление выражений, переданных в качестве фактического параметра, после чего получившийся результат копируется в локальную переменную, используемую в качестве формального параметра. То есть в локальную переменную x будет скопировано значение, получившееся в результате вычисления a+0.5, а в локальную переменную y – значение, хранящееся в переменной b. После чего с локальными переменными происходят все те действия, которые указаны в реализации метода. Соответствие фактических и формальных параметров идёт в порядке перечисления. То есть первый фактический параметр соответствует первому формальному, второй фактический – второму формальному, и так далее. Фактические параметры должны быть совместимы с формальными – при этом действуют все правила, относящиеся к совместимости примитивных типов по присваиванию, в том числе – к автоматическому преобразованию типов. Например, для mult1 можно вместо параметров типа double в качестве фактических использовать значения типа int или float. А если бы формальные параметры имели тип float, то использовать фактические параметры типа int было бы можно, а типа double – нельзя. Влияет ли как-нибудь увеличение переменной y на 1, происходящее благодаря оператору y++, на значение, хранящееся в переменной b? Конечно, нет. Ведь действия происходят с локальной переменной y, в которую при начале вызова было скопировано значение из переменной b. С самой переменной b в результате вызова ничего не происходит. А можно ли сделать так, чтобы подпрограмма изменяла значение в передаваемой в неё переменной? – Нет, нельзя. В Java значения примитивного типа наружу, к сожалению, передавать нельзя, в отличие от подавляющего большинства других языков программирования. Применяемый в Java способ передачи параметров называется передачей по значению . Иногда бывает нужно передать в подпрограмму неизменяемую константу. Конечно, можно проверить, нет ли где-нибудь оператора, изменяющего соответствующую переменную. Но надёжней проверка на уровне синтаксических конструкций. В этих целях используют модификатор final. Предположим, что увеличивать на 1 надо только первый параметр, а второй должен оставаться неизменным. В этом случае наш метод выглядел бы так: double mult1(double x, final double y){ x++; return x*y; } А вот при компиляции такого кода будет выдано сообщение об ошибке: double mult1(double x, final double y){ x++; y++; return x*y; } Локальные и глобальные переменные. Модификаторы доступа и правила видимости. Ссылка this Как уже говорилось, данные в подпрограмму могут передаваться через глобальные переменные. Это могут быть поля данных объекта, в методе которого осуществляется вызов, поля данных соответствующего класса, либо поля данных другого объекта или класса. Использование глобальных переменных не рекомендуется по двум причинам. - Во-первых, при вызове в списке параметров не видно, что идёт обращение к соответствующим переменным, и программа становится “непрозрачной” для программиста. Что делает её неструктурной. - Во-вторых, при изменении внутри подпрограммы-функции глобальной переменной возникает побочный эффект , связанный с тем, что функция не только возвращает вычисленное значение, но и меняет состояние окружения незаметным для программиста образом. Это может являться причиной плохо обнаружимых логических ошибок, не отслеживаемых компилятором. Конечно, бывают случаи, когда использование глобальных переменных не только желательно, а просто необходимо – иначе их не стали бы вводить как конструкцию языков программирования! Например, при написании метода в каком-либо классе обычно необходимо получать доступ к полям и методам этого класса. В Java такой доступ осуществляется напрямую, без указания имени объекта или класса. Правила доступа к методам и полям данных (переменным) из других пакетов, классов и объектов задаются с помощью модификаторов private , protected , public . Правила доступа часто называются также правилами видимости, это синонимы. Если переменная или подпрограмма невидимы в некой области программы, доступ к ним запрещён. private - элемент (поле данных или метод) доступен только в методах данного класса. Доступа из объектов нет! То есть если мы создали объект, у которого имеется поле или метод private, то получить доступ к этому полю или методу из объекта нельзя. Модификатор не задан - значит, действует доступ по умолчанию – так называемый пакетный, когда соответствующий элемент доступен только из классов своего пакета. Доступа из объектов нет, если они вызываются в операторах, расположенных в классах из других пакетов! Иногда, по аналогии с C++, этот тип доступа называют “дружественным”. protected - элемент доступен только в методах данного класса, данного пакета, а также классах-наследниках (они могут располагаться в других пакетах). public - элемент доступен из любых классов и объектов (с квалификацией именем пакета, если соответствующий класс не импортирован). Например, в классе class Vis1 { private int x=10,y=10; int p1=1; protected int p2=1; public int p3=1; } заданы переменные x,y,p1,p2,p3. Причём x и y обладают уровнем доступа private, p1 – пакетным, p2 – protected, p3 – public. Перечисление однотипных переменных через запятую позволяет использовать для нескольких переменных однократное задание имени типа и модификаторов, без повторений. Как уже говорилось, локальные переменные можно вводить в любом месте подпрограммы. Их можно использовать в данном методе только после места, где они заданы. Областью существования и видимости локальной переменной является часть программного кода от места объявления переменной до окончания блока, в котором она объявлена, обычно – до окончания метода. А вот переменные, заданные на уровне класса (глобальные переменные), создаются при создании объекта для методов объекта, и при первом вызове класса для переменных класса. И их можно использовать в методах данного класса как глобальные независимо от того, заданы переменные до метода или после. Ещё одной важной особенностью локальных переменных является время их существования: под них выделяется память в момент вызова, а высвобождается сразу после окончания вызова. Рассмотрим функцию, вычисляющую сумму чисел от 1 до n: double sum1(int n){ int i; double r=0; for(i=1;i<=n;i++){ r+=i; }; return r; } Вызов данного метода может выглядеть так: c=obj1.sum1(1000); При этом переменные i и r существуют только во время вызова obj1.sum1(1000). При следующем аналогичном вызове будет создан, а затем высвобожден из памяти следующий комплект i и r. Всё сказанное про локальные переменные также относится и к объектным переменным. Но не следует путать переменные и объекты: время жизни объектов гораздо больше. Даже если объект создаётся во время вызова подпрограммы, а после окончания этого вызова связь с ним кончается. Уничтожением неиспользуемых объектов занимается сборщик мусора (garbage collector). Если же объект создан в подпрограмме, и ссылка на него передана какой-либо глобальной переменной, он будет существовать после выхода из подпрограммы столько времени, сколько необходимо для работы с ним. Остановимся на области видимости локальной переменной. Имеются следующие уровни видимости: - На уровне метода. Переменная видна от места декларации до конца метода. - На уровне блока. Если переменная задана внутри блока {…}, она видна от места декларации до конца блока. Блоки могут быть вложены один в другой с произвольным уровнем вложенности. - На уровне цикла for. Переменная видна от места декларации в секции инициализации до конца тела цикла. Глобальные переменные видны во всей подпрограмме. Каждый объект имеет поле данных с именем this (“этот” – данное не слишком удачное обозначение унаследовано из C++), в котором хранится ссылка на сам этот объект. Поэтому доступ в методе объекта к полям и методам этого объекта может осуществляться либо напрямую, либо через ссылку this на этот объект. Например, если у объекта имеется поле x и метод show(), то this.x означает то же, что x, а this.show() – то же, show(). Но в случае перекрытия области видимости, о чём речь пойдёт чуть ниже, доступ по короткому имени оказывается невозможен, и приходится использовать доступ по ссылке this. Отметим, что ссылка this позволяет обойтись без использования имени объектной переменной, что делает код с её использованием более универсальным. Например, использовать в методах того класса, экземпляром которого является объект. Ссылка this не может быть использована в методах класса (то есть заданных с модификатором static), поскольку они могут вызываться без существующего объекта. Встаёт вопрос о том, что произойдёт, если на разных уровнях будет задано две переменных с одним именем. Имеется следующие варианты ситуаций: - В классе имеется поле с некоторым именем (глобальная переменная), и в списке параметров задаётся локальная переменная с тем же именем. Такая проблема часто возникает в конструкторах при инициализации полей данных и в методах установки значений полей данных setИмяПоля. Это разрешено, и доступ к параметру идёт по имени, как обычно. Но при этом видимость поля данных (доступ к полю данных по его имени) перекрывается, и приходится использовать ссылку на объект this. Например, если имя поля данных x, и имя параметра в методе тоже x, установка значения поля выглядит так: void setX(double x){ this.x=x } - В классе имеется поле с некоторым именем (глобальная переменная), и в методе задаётся локальная переменная с тем же именем. Ситуация разрешена и аналогична заданию локальной переменной в списке параметров. Доступ к полю идёт через ссылку this. - В классе имеется поле с некоторым именем (глобальная переменная), и в секции инициализации цикла for или внутри какого-нибудь блока, ограниченного фигурными скобками {…}, задаётся локальная переменная с тем же именем. В Java такая ситуация разрешена. При этом внутри цикла или блока доступна заданная в нём локальная переменная, а глобальная переменная видна через ссылку this. - Имеется локальная переменная (возможно, заданная как элемент списка параметров), и в секции инициализации цикла for или внутри какого-нибудь блока, ограниченного фигурными скобками {…}, задаётся локальная переменная с тем же именем. В Java такая ситуация запрещена. При этом выдаётся ошибка компиляции с информацией, что переменная с таким именем уже задана (“is already defined”). - Имеется метод, заданный в классе, и в другом методе задаётся локальная переменная с тем же именем. В Java такая ситуация разрешена и не вызывает проблем, так как компилятор отличает вызов метода от обращения к полю данных по наличию после имени метода круглых скобок. Передача ссылочных типов в функции. Проблема изменения ссылки внутри подпрограммы При передаче в подпрограмму ссылочной переменной возникает ряд отличий по сравнению со случаем примитивных типов, так как в локальную переменную, с которой идёт работа в подпрограмме, копируется не сам объект, а его адрес. Поэтому глобальная переменная, ссылающаяся на тот же объект, будет получать доступ к тем же самым полям данных, что и локальная. В результате чего изменение полей данных объекта внутри метода приведёт к тому, что мы увидим эти изменения после выхода из метода (причём неважно, будем мы менять поля непосредственно или с помощью вызова каких-либо методов). Для примера создадим в нашем пакете класс Location. Он будет служить для задания объекта соответствующего типа, который будет передаваться через список параметров в метод m1, вызываемый из нашего приложения. public class Location { public int x=0,y=0; public Location (int x, int y) { this.x=x; this.y=y; } } А в классе приложения напишем следующий код: Location locat1=new Location(10,20); public static void m1(Location obj){ obj.x++; obj.y++; } Мы задали переменную locat1 типа Location, инициализировав её поля x и y значениями 10 и 20. А в методе m1 происходит увеличение на 1 значения полей x и y объекта, связанного с формальным параметром obj. Создадим две кнопки с обработчиками событий. Нажатие на первую кнопку будет приводить к выводу информации о значениях полей x и y объекта, связанного с переменной locat1. А нажатие на вторую – к вызову метода m1. private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { System.out.println("locat1.x="+locat1.x); System.out.println("locat1.y="+locat1.y); } private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) { m1(locat1); System.out.println("Прошёл вызов m1(locat1)"; } Легко проверить, что вызов m1(locat1) приводит к увеличению значений полей locat1.x и locat1.y . При передаче в подпрограмму ссылочной переменной имеется особенность, которая часто приводит к ошибкам – потеря связи с первоначальным объектом при изменении ссылки. Модифицируем наш метод m1: public static void m1(Location obj){ obj.x++; obj.y++; obj=new Location(4,4); obj.x++; obj.y++; } После первых двух строк, которые приводили к инкременту полей передаваемого объекта, появилось создание нового объекта и перещёлкивание на него локальной переменной obj, а затем две точно такие же строчки, как в начале метода. Какие значения полей x и y объекта, связанного с переменной locat1 покажет нажатие на кнопку 1 после вызова модифицированного варианта метода? Первоначальный и модифицированный вариант метода дадут одинаковые результаты! Дело в том, что присваивание obj=new Location(4,4); приводит к тому, что переменная obj становится связанной с новым, только что созданным объектом. И изменение полей данных в операторах obj.x++ и obj.y++ происходит уже для этого объекта. А вовсе не для того объекта, ссылку на который передали через список параметров. Следует обратить внимание на то, какая терминология используется для описания программы. Говорится “ссылочная переменная” и “объект, связанный со ссылочной переменной”. Эти понятия не отождествляются, как часто делают программисты при описании программы. И именно строгая терминология позволяет разобраться в происходящем. Иначе трудно понять, почему оператор obj.x++ в одном месте метода даёт совсем не тот эффект, что в другом месте. Поскольку если бы мы сказали “изменение поля x объекта obj”, было бы невозможно понять, что объекты-то разные! А правильная фраза “изменение поля x объекта, связанного со ссылочной переменной obj” подталкивает к мысли, что эти объекты в разных местах программы могут быть разными. Способ передачи данных (ячейки памяти) в подпрограмму, позволяющий изменять содержимое внешней ячейки памяти благодаря использованию ссылки на эту ячейку, называется передачей по ссылке . И хотя в Java объект передаётся по ссылке, объектная переменная, в которой хранится адрес объекта, передаётся по значению. Ведь этот адрес копируется в другую ячейку, локальную переменную. А именно переменная является параметром, а не связанный с ней объект. То есть параметры в Java всегда передаются по значению . Передачи параметров по ссылке в языке Java нет. Рассмотрим теперь нетривиальные ситуации, которые часто возникают при передаче ссылочных переменных в качестве параметров. Мы уже упоминали о проблемах, возникающие при работе со строками. Рассмотрим подпрограмму, которая, по идее, должна бы возвращать с помощью переменной s3 сумму строк, хранящихся в переменных s1 и s2: void strAdd1(String s1,s2,s3){ s3=s1+s2; } Строки в Java являются объектами, и строковые переменные являются ссылочными. Поэтому можно было бы предполагать возврат изменённого состояния строкового объекта, с которым связана переменная s3. Но всё обстоит совсем не так: при вызове obj1.strAdd1(t1,t2,t3); значение строковой переменной t3 не изменится . Дело в том, что в Java строки типа String являются неизменяемыми объектами, и вместо изменения состояния прежнего объекта в результате вычисления выражения s1+s2 создаётся новый объект. Поэтому присваивание s3=s1+s2 приводит к перещёлкиванию ссылки s3 на этот новый объект. А мы уже знаем, что это ведёт к тому, что новый объект оказывается недоступен вне подпрограммы – “внешняя” переменная t3 будет ссылаться на прежний объект-строку. В данном случае, конечно, лучше сделать функцию strAdd1 строковой, и возвращать получившийся строковый объект как результат вычисления этой функции. Ещё пример: пусть нам необходимо внутри подпрограммы обработать некоторую строку и вернуть изменённое значение. Допустим, в качестве входного параметра передаётся имя, и мы хотим добавить в конец этого имени порядковый номер – примерно так, как это делает среда разработки при создании нового компонента. Следует отметить, что для этих целей имеет смысл создавать подпрограмму, хотя на первый взгляд достаточно выражения name+count. Ведь на следующем этапе мы можем захотеть проверить, является ли входное значение идентификатором (начинающимся с буквы и содержащее только буквы и цифры). Либо проверить, нет ли уже в списке имён такого имени. Напишем в классе нашего приложения такой код: String componentName="myComponent"; int count=0; public void calcName1(String name) { count++; name+=count; System.out.println("Новое значение="+name); } Создадим в нашем приложении кнопку, при нажатии на которую срабатывает следующий обработчик события: private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { calcName1(componentName); System.out.println("componentName="+componentName); } Многие начинающие программисты считают, что раз строки являются объектами, то при первом нажатии на кнопку значение componentName станет ”myComponent1”, при втором – ”myComponent2”, и так далее. Но значение myComponent остаётся неизменным, хотя в методе calcName1 новое значение выводится именно таким, как надо. В чём причина такого поведения программы, и каким образом добиться правильного результата? Если мы меняем в подпрограмме значение полей у объекта, а ссылка на объект не меняется, то изменение значения полей оказывается наблюдаемым с помощью доступа к тому же объекту через внешнюю переменную. А вот присваивание строковой переменной внутри подпрограммы нового значения приводит к созданию нового объекта-строки и перещёлкивания на него ссылки, хранящейся в локальной переменной name. Причём глобальная переменная componentName остаётся связанной с первоначальным объектом-строкой "myComponent". Как бороться с данной проблемой? Существует несколько вариантов решения. Во-первых, в данном случае наиболее разумно вместо подпрограммы-процедуры, не возвращающей никакого значения, написать подпрограмму-функцию, возвращающую значение типа String: public String calcName2(String name) { count++; name+=count; return name; } В этом случае не возникает никаких проблем с возвратом значения, и следующий обработчик нажатия на кнопку это демонстрирует: private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) { componentName=calcName2(componentName); System.out.println("componentName="+componentName); } К сожалению, если требуется возвращать более одного значения, данный способ решения проблемы не подходит. А ведь часто из подпрограммы требуется возвращать два или более изменённых или вычисленных значения. Во-вторых, можно воспользоваться глобальной строковой переменной – но это плохой стиль программирования. Даже использование глобальной переменной count в предыдущем примере не очень хорошо – но мы это сделали для того, чтобы не усложнять пример. В-третьих, возможно создание оболочечного объекта (wrapper), у которого имеется поле строкового типа. Такой объект передаётся по ссылке в подпрограмму, и у него внутри подпрограммы меняется значение строкового поля. При этом, конечно, это поле будет ссылаться на новый объект-строку. Но так как ссылка на оболочечный объект внутри подпрограммы не меняется, связь с новой строкой через оболочечный объект сохранится и снаружи. Такой подход, в отличие от использования подпрограммы-функции строкового типа, позволяет возвращать произвольное количество значений одновременно, причём произвольного типа, а не только строкового. Но у него имеется недостаток – требуется создавать специальные классы для формирования возвращаемых объектов. В-четвёртых, имеется возможность использовать классы StringBuffer или StringBuilder. Это наиболее адекватный способ при необходимости возврата более чем одного значения, поскольку в этой ситуации является и самым простым, и весьма эффективным по быстродействию и используемым ресурсам. Рассмотрим соответствующий код. public void calcName3(StringBuffer name) { count++; name.append(count); System.out.println("Новое значение="+name); } StringBuffer sbComponentName=new StringBuffer(); {sbComponentName.append("myComponent");} private void jButton8ActionPerformed(java.awt.event.ActionEvent evt){ calcName3(sbComponentName); System.out.println("sbComponentName="+sbComponentName); } Вместо строкового поля componentName мы теперь используем поле sbComponentName типа StringBuffer. Почему-то разработчики этого класса не догадались сделать в нём конструктор с параметром строкового типа, поэтому приходится использовать блок инициализации, в котором переменной sbComponentName присваивается нетривиальное начальное значение. В остальном код очевиден. Принципиальное отличие от использования переменной типа String – то, что изменение значения строки, хранящейся в переменной StringBuffer, не приводит к созданию нового объекта, связанного с этой переменной. Вообще говоря, с этой точки зрения для работы со строками переменные типа StringBuffer и StringBuilder подходят гораздо лучше, чем переменные типа String. Но метода toStringBuffer() в классах не предусмотрено. Поэтому при использовании переменных типа StringBuffer обычно приходится пользоваться конструкциями вида sb.append(выражение ). В методы append и insert можно передавать выражения произвольных примитивных или объектных типов. Правда, массивы преобразуются в строку весьма своеобразно, так что для их преобразования следует писать собственные подпрограммы. Например, при выполнении фрагмента int[] a=new int[]{10,11,12}; System.out.println("a="+a); был получен следующий результат: a=[I@15fea60 И выводимое значение не зависело ни от значений элементов массива, ни от их числа. Наличие автоматической упаковки-распаковки также приводит к проблемам. Пусть у нас имеется случай, когда в списке параметров указана объектная переменная: void m1(Double d){ d++; } Несмотря на то, что переменная в объектная, изменение значения в внутри подпрограммы не приведёт к изменению снаружи подпрограммы по той же причине, что и для переменных типа String. При инкременте сначала производится распаковка в тип double, для которого выполняется оператор “++”. После чего выполняется упаковка в новый объект типа Double, с которым становится связана переменная d. Приведём ещё один аналогичный пример: public void proc1(Double d1,Double d2,Double d3){ d3=d1+sin(d2); } Надежда на то, что в объект, передаваемый через параметр d3, возвратится вычисленное значение d3=d1+sin(d2), является ошибочной, так как при упаковке вычисленного результата создаётся новый объект. Таким образом, объекты стандартных оболочечных числовых классов не позволяют возвращать изменённое числовое значение из подпрограмм, что во многих случаях вызывает проблемы. Для этих целей приходится писать собственные оболочечные классы. Например: public class UsableDouble{ Double value=0; UsableDouble(Double value){ this.value=value; } } Объект UsableDouble в можно передавать в подпрограмму по ссылке и без проблем получать возвращённое изменённое значение. Аналогичного рода оболочные классы легко написать для всех примитивных типов. Если бы в стандартных оболочечных классах были методы, позволяющие изменить числовое значение, связанное с объектом, без изменения адреса объекта, в такого рода деятельности не было бы необходимости. Заканчивая разговор о проблемах передачи параметров в подпрограмму, автор хочет выразить надежду, что разработчики Java либо добавят в стандартные оболочечные классы такого рода методы, либо добавят возможность передачи переменных в подпрограммы по ссылке, как, к примеру, это было сделано в Java-образном языке C#. Наследование. Суперклассы и подклассы. Переопределение методов В объектном программировании принято использовать имеющиеся классы в качестве “заготовок” для создания новых классов, которые на них похожи, но обладают более сложной структурой и/или отличающимся поведением. Такие “заготовки” называются прародителями (ancestors), а основанные на них новые классы – потомками (descendants) или наследниками. Классы-потомки получают “в пользование” поля и методы, заданные в классах-прародителях, это называется наследованием (inheritance) полей и методов. В C++ и Java вместо терминов “прародители” и “потомки” чаще используют неудачные названия “суперклассы” (superclasses) и “подклассы” (subclasses). Как уже говорилось, суперклассы должны быть примитивнее подклассов, но приставка “супер” подталкивает программиста к прямо противоположным действиям. При задании класса-потомка сначала идут модификаторы, затем после ключевого слова class идёт имя декларируемого класса, затем идёт зарезервированное слово extends (“расширяет”), после чего требуется указать имя класса-родителя (непосредственного прародителя). Если не указывается, от какого класса идёт наследование, родителем считается класс Object. Сам класс-потомок называется наследником, или дочерним. В синтаксисе Java словом extends подчёркивается, что потомок расширяет то, что задано в прародителе – добавляет новые поля, методы, усложняет поведение. (Но всё это делает класс более специализированным, менее общим). Далее в фигурных скобках идёт реализация класса – описание его полей и методов. При этом поля данных и методы, имеющиеся в прародителе, в потомке описывать не надо – они наследуются. Однако в случае, если реализация прародительского метода нас не устраивает, в классе-потомке его можно реализовать по другому. В этом случае метод необходимо продекларировать и реализовать в классе-потомке. Кроме того, в потомке можно задавать новые поля данных и методы, отсутствующие в прародителях. Модификаторы, которые можно использовать: - public – модификатор, задающий публичный (общедоступный) уровень видимости. Если он отсутствует, действует пакетный уровень доступа - класс доступен только элементам того же пакета. - abstract – модификатор, указывающий, что класс является абстрактным, то есть у него не бывает экземпляров (объектов). Обязательно объявлять класс абстрактным в случае, если какой-либо метод объявлен как абстрактный. - final – модификатор, указывающий, что класс является окончательным (final) , то есть что у него не может быть потомков. Таким образом, задание класса-наследника имеет следующий формат: Модификаторы class ИмяКласса extends ИмяРодителя { Задание полей; Задание подпрограмм - методов класса, методов объекта, конструкторов } Данный формат относится к классам, не реализующим интерфейсы (interfaces). Работе с интерфейсами будет посвящён отдельный раздел. Рассмотрим в качестве примера наследование для классов описанной ранее иерархии фигур. Для простоты выберем вариант, в котором Figure - это класс-прародитель иерархии, Dot его потомок, а Circle - потомок Dot (то есть является “жирной точкой”). Напомним, что имена классов принято начинать с заглавной буквы. Класс Figure опишем как абстрактный – объектов такого типа создавать не предполагается, так как фигура без указания конкретного вида – это, действительно, чистая абстракция. По той же причине методы show (“показать”) и hide (“скрыть”) объявлены как абстрактные. Напомним также, что если в классе хоть один метод является абстрактным, это класс обязан быть объявлен как абстрактный. public abstract class Figure { //это абстрактный класс int x=0; int y=0; java.awt.Color color; java.awt.Graphics graphics; java.awt.Color bgColor; public abstract void show(); //это абстрактный метод public abstract void hide(); //это абстрактный метод public void moveTo(int x, int y){ hide(); this.x= x; this.y= y; show(); }; } Поля x и y задают координаты фигуры, а color – её цвет. Соответствующий тип задан в пакете java.awt. Поле graphics задаёт ссылку на графическую поверхность, по которой будет идти отрисовка фигуры. Соответствующий тип также задан в пакете java.awt. В отличии от полей x, y и color для этого поля при написании класса невозможно задать начальное значение, и оно будет присвоено при создании объекта. То же относится к полю bgColor (от “background color”) – в нём мы будем хранить ссылку на цвет фона графической поверхности. Цветом фона мы будем выводить фигуру в методе hide для того, чтобы она перестала показываться на экране. Это не самый лучший, но зато самый простой способ скрыть фигуру. В дальнейшем при желании реализацию метода можно изменить – это никак не коснётся остальных частей программы. В параграфе, посвящённом конструкторам, в классе FilledCircle мы применим более совершенный способ отрисовки и “скрывания” фигур, основанный на использовании режима рисования XOR (“исключающее или”). Установка этого режима производится методом setXORMode. Такой режим можно использовать для всех наших фигур. Метод moveTo имеет реализацию несмотря на то, что класс абстрактный, и в этой реализации используются имена абстрактных методов show и hide. Этот вопрос будет подробно обсуждаться в следующем параграфе, посвящённом полиморфизму. Рассмотрим теперь, как задаётся потомок класса Figure – класс Dot (“Точка”). Для Dot классы Object и Figure будут являться прародителями (суперклассами), причёт Figure будет непосредственным прародителем. Соответственно, для них класс Dot будет являться потомком (подклассом), причём для класса Figure – непосредственным потомком. Класс Dot расширяет (extends) функциональность класса Figure: хотя в нём и не появляется новых полей, зато пишется реализация для методов show и hide, которые в прародительском классе были абстрактными. В классе Figure мы использовали классы пакета java.awt без импорта этого пакета. В классе Dot используется импорт – обычно это удобнее, так как не надо много раз писать длинные имена. package java_gui_example; import java.awt.*; /** * @author В.В.Монахов */ public class Dot extends Figure{ /** Создаёт новый экземпляр типа Dot */ public Dot(Graphics graphics,Color bgColor) { this.graphics=graphics; this.bgColor=bgColor; } public void show(){ Color oldC=graphics.getColor(); graphics.setColor(Color.BLACK); graphics.drawLine(x,y,x,y); graphics.setColor(oldC); } public void hide(){ Color oldC=graphics.getColor(); graphics.setColor(bgColor); graphics.drawLine(x,y,x,y); graphics.setColor(oldC); ; } } Отметим, что в классе Dot не задаются поля x, y, graphics и метод moveTo – они наследуются из класса Figure. А методы show и hide переопределяются (override) – для них пишется реализация, соответствующая тому, каким именно образом точка появляется и скрывается на экране. Конструктор Dot(int x, int y, Graphics g) занимается созданием объекта типа Dot и инициализацией его полей. В методах show и hide используются методы объекта graphics. В методе show сначала во временной переменной oldC сохраняется информация о текущем цвете рисования. Затем в качестве текущего цвета устанавливается цёрный цвет (константа java.awt. Color.BLACK). Затем вызывается метод, рисующий точку, в качестве него используется рисование линии с совпадающими началом и концом. После чего восстанавливается первоначальный цвет рисования. Это необходимо для того, чтобы не повлиять на поведение других объектов, пользующихся для каких-либо целей текущим цветом. Такого рода действия являются очень характерными при пользовании разделяемыми (shared) ресурсами. Если вам при работе какого-либо метода требуется изменить состояние разделяемых внешних данных, сначала требуется сохранить информацию о текущем состоянии, а в конце вызова восстановить это состояние. Термин override (“переопределить”) на русский язык часто переводят как “перекрыть”. Это может вводить в заблуждение, так как имеется ещё одно понятие – перекрытие области видимости (hiding). Такое перекрытие возникает в случае, когда в классе-потомке задаётся поле с тем же именем, что и в прародителе (но, возможно, другого типа). Для методов совпадение имён разрешено, в том числе с именами глобальных и локальных переменных. Имя метода в сочетании с числом параметров и их типами называется его сигнатурой . А сигнатура метода в сочетании с типом возвращаемого значения называется контрактом метода. В контракт также входят типы возбуждаемых методом исключений, но о соответствующих правилах будет говориться в отдельном параграфе, посвящённом обработке исключительных ситуаций. Если контракт задаваемого метода совпадает с контрактом прародительского метода, говорят, что метод переопределён. Если у двух методов имена совпадают, но сигнатуры различны – говорят, что производится перегрузка (overloading) методов. Перегрузке методов далее будет посвящён отдельный параграф. Если же в одном классе два метода имеют одинаковые сигнатуры, то даже если их контракты отличаются, компилятор выдаёт сообщение об ошибке. В классе нашего приложения создадим на экранной форме панель, и будем вести отрисовку по ней. Зададим с помощью редактора свойств белый (или какой-нибудь другой) цвет панели – свойство background. Затем зададим переменную dot, которой назначим объект в обработчике нажатия на кнопку: Dot dot=new Dot(jPanel1.getGraphics(),jPanel1.getBackground()); После создания объекта-точки с помощью переменной dot можно вызывать методы show и hide: dot.show(); dot.hide(); Создадим на форме пункты ввода/редактирования текста jTextField1 и jTextField2. В этом случае становится можно вызывать метод moveTo, следующим образом задавая координаты, куда должна перемещаться точка: int newX=Integer.parseInt(jTextField1.getText()); int newY=Integer.parseInt(jTextField2.getText()); dot.moveTo(newX,newY); Наш пример оказывается достаточно функциональным для того, чтобы увидеть работу с простейшим объектом. Рассмотрим теперь класс ScalableFigure (“Масштабируемая фигура”), расширяющий класс Figure. Он очень прост. package java_gui_example; public abstract class ScalableFigure extends Figure{ int size; public void resize(int size) { hide(); this.size=size; show(); } } Класс ScalableFigure является абстрактным – объектов такого типа создавать не предполагается, так как масштабируемая фигура без указания конкретного вида – это абстракция. По этой же причине в классе не заданы реализации методов show и hide. Зато появилось поле size (“размер”), и метод resize (“изменить размер”), расширяющий этот класс по сравнению с прародителем. Для того, чтобы изменить размер фигуры, отрисовываемой на экране, надо не только присвоить полю size новое значение, но и правильно перерисовать фигуру. Сначала надо её скрыть, затем изменить значение size, после чего показать на экране – уже нового размера. Следует обратить внимание, что мы пишем данный код на уровне абстракций, для нас не имеет значения, какого типа будет фигура – главное, чтобы она была масштабируемая, то есть являлась экземпляром класса-потомка ScalableFigure. О механизме, позволяющем такому коду правильно работать, будет рассказано далее в параграфе, посвящённом полиморфизму. Опишем класс Circle (“Окружность”), расширяющий класс ScalableFigure. package java_gui_example; import java.awt.*; public class Circle extends ScalableFigure { Circle(Graphics g,Color bgColor, int r){ //это конструктор graphics=g; this.bgColor=bgColor; size=r; } public void show(){ Color oldC=graphics.getColor(); graphics.setColor(Color.BLACK); graphics.drawOval(x,y,size,size); graphics.setColor(oldC); } public void hide(){ Color oldC=graphics.getColor(); graphics.setColor(bgColor); graphics.drawOval(x,y,size,size); graphics.setColor(oldC); } }; В классе Circle не задаётся новых полей – в качестве радиуса окружности используется поле size, унаследованное от класса ScalableFigure. Зато введён конструктор, позволяющий задавать радиус при создании окружности. Кроме того, написаны новые реализации для методов show и hide, поскольку окружность показывается, скрывается и движется по экрану не так, как точка. Таким образом, усложнение структуры Circle по сравнением со ScalableFigure в основном связано с появлением реализации у методов, которые до этого были абстрактными. Очевидно, класс Circle является более специализированным по сравнению со ScalableFigure, не говоря уж о Figure. Поля x, y, color, bgColor, graphics и метод moveTo наследуется в Circle из класса Figure. А из ScalableFigure наследуются поле size и метод resize. Следует особо подчеркнуть, что наследование относится к классам, а не к объектам. Можно говорить, что один класс является наследником другого. Но категорически нельзя – что один объект является наследником другого объекта. Иногда говорят фразы вроде “объект circle является наследником Figure ”. Это не страшно, если подразумевается, что “объект circle является экземпляром класса-наследника Figure”. Слишком долго произносить правильную фразу. Но следует чётко понимать, что имеется в виду, и злоупотреблять такими оборотами не следует. Класс Circle является непосредственным (прямым) потомком ScalableFigure , а ScalableFigure – непосредственным (прямым) прародителем класса Circle . То есть для ScalableFigure класс Circle является подклассом, а для Circle класс ScalableFigure является суперклассом. Аналогично, для Figure подклассами являются и ScalableFigure, и Circle. А для Circle суперклассами являются и ScalableFigure, и Figure. Поскольку в Java все классы— потомки класса Object, то Object является прародителем и для Figure, и для ScalableFigure, и для Circle. Но непосредственным прародителем он будет только для Figure. Наследование и правила видимости. Зарезервированное слово super В данном параграфе рассматривается ряд нетривиальных ситуаций, связанных с правилами видимости при наследовании. Поля и методы, помеченные как private (“закрытый, частный”) наследуются, но в классах-наследниках недоступны. Это сделано в целях обеспечения безопасности. Пусть, например, некий класс Password1 обеспечивает проверку правильности пароля, и у него имеется строковое поле password (“пароль”), в котором держится пароль и с которым сравнивается введённый пользователем пароль. Если оно имеет тип public, такое поле общедоступно, и сохранить его в тайне мы не сможем. При отсутствии модификатора видимости или модификаторе protected на первый взгляд имеется необходимое ограничение доступа. Но если мы напишем класс Password2, являющийся наследником от Password1, в нём легко написать метод, “вскрывающий” пароль: public String getPass(){ return password; }; Если же поставить модификатор private, то в потомке до прародительского поля password не добраться! То, что private-поля наследуются, проверить достаточно просто: зададим класс public class TestPrivate1 { private String s=”Значение поля private”; public String get_s(){ return s; } } и его потомок, который просто имеет другое имя, но больше ничего не делает: public class TestPrivate2 extends TestPrivate1 { } Если из объекта, являющегося экземпляром TestPrivate2, вызвать метод get_s(), мы получим строку =”Значение поля private”: TestPrivate2 tst=new TestPrivate2(); System.out.println(tst.get_s()); Таким образом, поле s наследуется. Но если в классе, где оно задано, не предусмотрен доступ к нему с помощью каких-либо методов, доступных в наследнике, извлечь информацию из этого поля оказывается невозможным. Модификатор protected предназначен для использования соответствующих полей и методов разработчиками классов-наследников. Он даёт несколько большую открытость, чем пакетный вид доступа (по умолчанию, без модификатора), поскольку в дополнении к видимости из текущего пакета позволяет обеспечить доступ к таким членам в классах-наследниках, находящихся в других пакетах. Модификатором protected полезно помечать различного рода служебные методы, ненужные пользователям класса, но необходимые для функциональности этого класса. Существует “правило хорошего тона”: поля данных принято помечать модификатором private, а доступ к этим полям обеспечивать с помощью методов с тем же именем, но префиксом get (“получить” - доступ по чтению) и set (“установить” - доступ по записи). Эти методы называют “геттерами” и “сеттерами”. Такие правила основаны на том, что прямой доступ по записи к полям данных может разрушить целостность объекта. Рассмотрим следующий пример: пусть у нас имеется фигура, отрисовываемая на экране. Изменение её координат должно сопровождаться отрисовкой на новом месте. Но если мы напрямую изменили поле x или y, фигура останется на прежнем месте, хотя поля имеют новые значения! Если же доступ к полю осуществляется через методы setX и setY, кроме изменения значений полей будут вызваны необходимые методы, обеспечивающие перерисовку фигуры в новом месте. Также можно обеспечить проверку вводимых значений на допустимость. Возможен и гораздо худший случай доступа к полям напрямую: пусть у нас имеется объект-прямоугольник, у которого заданы поля x1,y1- координаты левого верхнего угла,x2,y2- координаты правого нижнего угла,w - ширина, h – высота, s- площадь прямоугольника. Они не являются независимыми: w=x2-x1, h=y2-y1, s=w*h. Поэтому изменение какого-либо из этих полей должно приводить к изменению других. Если же, скажем, изменить только x2, без изменения w и s, части объекта станут несогласованными. Предсказать, как поведёт себя в таких случаях программа, окажется невозможно! Ещё хуже обстоит дело при наличии наследования в тех случаях, когда в потомке задано поле с тем же именем, что и в прародителе, имеющее совместимый с прародительским полем тип. Так как для полей данных полиморфизм не работает, возможны очень неприятные ошибки. Указанные выше правила хорошего тона программирования нашли выражение в среде NetBeans при установленном пакете NetBeans Enterprise Pack. В ней при разработке UML-диаграмм добавление в класс поля автоматически приводит к установке ему модификатора private и созданию двух public-методов с тем же именем, но префиксами get и set. Эти типы видимости в дальнейшем, конечно, можно менять, как и удалять ненужные методы. Иногда возникает необходимость вызвать поле или метод из прародительского класса. Обычно это бывает в случаях, когда в классе-потомке задано поле с таким же именем (но, обычно, другим типом) или переопределён метод. В результате видимость прародительского поля данных или метода в классе-потомке утеряна. Иногда говорят, что поле или метод затеняются в потомке. В этих случаях используют вызов super.имяПоля или super.имяМетода (список параметров ). Слово super в этих случаях означает сокращение от superclass. Если метод или поле заданы не в непосредственном прародителе, а унаследованы от более далёкого прародителя, соответствующие вызовы всё равно будут работать. Но комбинации вида super.super.имя не разрешены. Использовать вызовы с помощью слова super разрешается только для методов и полей данных объектов. Для методов и переменных класса (то есть объявленных с модификатором static) вызовы с помощью ссылки super запрещены. Статическое и динамическое связывание методов. Полиморфизм Данный параграф, несмотря на краткость, является очень важным – практически всё профессиональное программирование в Java основано на использовании полиморфизма. В то же время эта тема является одной из наиболее сложной для понимания учащимися. Поэтому рекомендуется внимательно перечитать этот параграф несколько раз. Методы классов помечаются модификатором static не случайно – для них при компиляции программного кода действует статическое связывание . Это значит, что в контексте какого класса указано имя метода в исходном коде, на метод того класса в скомпилированном коде и ставится ссылка. То есть осуществляется связывание имени метода в месте вызова с исполняемым кодом этого метода. Иногда статическое связывание называют ранним связыванием , так как оно происходит на этапе компиляции программы. Статическое связывание в Java используется ещё в одном случае – когда класс объявлен с модификатором final (“финальный”, “окончательный”), Методы объектов в Java являются динамическими, то есть для них действует динамическое связывание . Оно происходит на этапе выполнения программы непосредственно во время вызова метода, причём на этапе написания данного метода заранее неизвестно, из какого класса будет проведён вызов. Это определяется типом объекта, для которого работает данный код - какому классу принадлежит объект, из того класса вызывается метод. Такое связывание происходит гораздо позже того, как был скомпилирован код метода. Поэтому такой тип связывания часто называют поздним связыванием . Программный код, основанный на вызове динамических методов, обладает свойством полиморфизма – один и тот же код работает по-разному в зависимости от того, объект какого типа его вызывает, но делает одни и те же вещи на уровне абстракции, относящейся к исходному коду метода. Для пояснения этих не очень понятных при первом чтении слов рассмотрим пример из предыдущего параграфа – работу метода moveTo. Неопытным программистам кажется, что этот метод следует переопределять в каждом классе-наследнике. Это действительно можно сделать, и всё будет правильно работать. Но такой код будет крайне избыточным – ведь реализация метода будет во всех классах-наследниках Figure совершенно одинаковой: public void moveTo(int x, int y){ hide(); x=this.x; y=this.y; show(); }; Кроме того, в этом случае не используются преимущества полиморфизма. Поэтому мы не будем так делать. Ещё часто вызывает недоумение, зачем в абстрактном классе Figure писать реализацию данного метода. Ведь используемые в нём вызовы методов hide и show, на первый взгляд, должны быть вызовами абстрактных методов – то есть, кажется, вообще не могут работать! Но методы hide и show являются динамическими, а это, как мы уже знаем, означает, что связывание имени метода и его исполняемого кода производится на этапе выполнения программы. Поэтому то, что данные методы указаны в контексте класса Figure, вовсе не означает, что они будут вызываться из класса Figure! Более того, можно гарантировать, что методы hide и show никогда не будут вызываться из этого класса. Пусть у нас имеются переменные dot1 типа Dot и circle1 типа Circle, и им назначены ссылки на объекты соответствующих типов. Рассмотрим, как поведут себя вызовы dot1.moveTo(x1,y1) и circle1.moveTo(x2,y2). При вызове dot1.moveTo(x1,y1) происходит вызов из класса Figure метода moveTo. Действительно, этот метод в классе Dot не переопределён, а значит, он наследуется из Figure. В методе moveTo первый оператор – вызов динамического метода hide. Реализация этого метода берётся из того класса, экземпляром которого является объект dot1, вызывающий данный метод. То есть из класса Dot. Таким образом, скрывается точка. Затем идет изменение координат объекта, после чего вызывается динамический метод show. Реализация этого метода берётся из того класса, экземпляром которого является объект dot1, вызывающий данный метод. То есть из класса Dot. Таким образом, на новом месте показывается точка. Для вызова circle1.moveTo(x2,y2) всё абсолютно аналогично – динамические методы hide и show вызываются из того класса, экземпляром которого является объект circle1, то есть из класса Circle. Таким образом, скрывается на старом месте и показывается на новом именно окружность. То есть если объект является точкой, перемещается точка. А если объект является окружностью - перемещается окружность. Более того, если когда-нибудь кто-нибудь напишет, например, класс Ellipse, являющийся наследником Circle, и создаст объект Ellipse ellipse=new Ellipse(…), то вызов ellipse.moveTo(…) приведёт к перемещению на новое место эллипса. И происходить это будет в соответствии с тем, каким образом в классе Ellipse реализуют методы hide и show. Заметим, что работать будет давным-давно скомпилированный полиморфный код класса Figure . Полиморфизм обеспечивается тем, что ссылки на эти методы в код метода moveTo в момент компиляции не ставятся – они настраиваются на методы с такими именами из класса вызывающего объекта непосредственно в момент вызова метода moveTo. В объектно-ориентированных языках программирования различают две разновидности динамических методов – собственно динамические и виртуальные . По принципу работы они совершенно аналогичны и отличаются только особенностями реализации. Вызов виртуальных методов быстрее. Вызов динамических медленнее, но служебная таблица динамических методов (DMT – Dynamic Methods Table) занимает чуть меньше памяти, чем таблица виртуальных методов (VMT – Virtual Methods Table). Может показаться, что вызовы динамических методов неэффективен с точки зрения затрат по времени из-за длительности поиска имён. На самом деле во время вызова поиска имён не делается, а используется гораздо более быстрый механизм, использующий упомянутую таблицу виртуальных (динамических) методов. Но мы на особенностях реализации этих таблиц останавливаться не будем, так как в Java нет различения этих видов методов. Класс Object является базовым для всех классов Java. Поэтому все его поля и методы наследуются и содержатся во всех классах. В классе Object содержатся следующие методы: public Boolean equals ( Object obj ) – возвращает true в случае, когда равны значения объекта, из которого вызывается метод, и объекта, передаваемого через ссылку obj в списке параметров. Если объекты не равны, возвращается false. В классе Object равенство рассматривается как равенство ссылок и эквивалентно оператору сравнения “==”. Но в потомках этот метод может быть переопределён, и может сравнивать объекты по их содержимому. Например, так происходит для объектов оболочечных числовых классов. Это легко проверить с помощью такого кода: Double d1=1.0,d2=1.0; System.out.println("d1==d2 ="+(d1==d2)); System.out.println("d1.equals(d2) ="+(d1.equals(d2))); Первая строка вывода даст d1==d2 =false, а вторая d1.equals(d2) =true public int hashCode() – выдаёт хэш-код объекта. Хэш-кодом называется условно уникальный числовой идентификатор, сопоставляемый какому-либо элементу. Из соображений безопасности выдавать адрес объекта прикладной программе нельзя. Поэтому в Java хэш-код заменяет адрес объекта в тех случаях, когда для каких-либо целей надо хранить таблицы адресов объектов. protected Object clone () throws CloneNotSupportedException – метод занимается копированием объекта и возвращает ссылку на созданный клон (дубликат) объекта. В наследниках класса Object его обязательно надо переопределить, а также указать, что класс реализует интерфейс Clonable. Попытка вызова метода из объекта, не поддерживающего клонирования, вызывает возбуждение исключительной ситуации CloneNotSupportedException (“Клонирование не поддерживается”). Про интерфейсы и исключительные ситуации будет рассказано в дальнейшем. Различают два вида клонирования: мелкое (shallow), когда в клон один к одному копируются значения полей оригинального объекта, и глубокое (deep), при котором для полей ссылочного типа создаются новые объекты, клонирующие объекты, на которые ссылаются поля оригинала. При мелком клонировании и оригинал, и клон будут ссылаться на одни и те же объекты. Если объект имеет поля только примитивных типов, различия между мелким и глубоким клонированием нет. Реализацией клонирования занимается программист, разрабатывающий класс, автоматического механизма клонирования нет. И именно на этапе разработки класса следует решить, какой вариант клонирования выбирать. В подавляющем большинстве случаев требуется глубокое клонирование. public final Class getClass () – возвращает ссылку на метаобъект типа класс. С его помощью можно получать информацию о классе, к которому принадлежит объект, и вызывать его методы класса и поля класса. protected void finalize () throws Throwable – вызывается перед уничтожением объекта. Должен быть переопределён в тех потомках Object, в которых требуется совершать какие-либо вспомогательные действия перед уничтожением объекта (закрыть файл, вывести сообщение, отрисовать что-либо на экране, и т.п.). Подробнее об этом методе говорится в соответствующем параграфе. public String toString () – возвращает строковое представление объекта (настолько адекватно, насколько это возможно). В классе Object этот метод реализует выдачу в строку полного имени объекта (с именем пакета), после которого следует символ ‘@’, а затем в шестнадцатеричном виде хэш-код объекта. В большинстве стандартных классов этот метод переопределён. Для числовых классов возвращается строковое представление числа, для строковых – содержимое строки, для символьного – сам символ (а не строковое представление его кода!). Например, следующий фрагмент кода Object obj=new Object(); System.out.println(" obj.toString() даёт "+obj.toString()); Double d=new Double(1.0); System.out.println(" d.toString()даёт "+d.toString()); Character c='A'; System.out.println("c.toString() даёт "+c.toString()); обеспечит вывод obj.toString() даёт java.lang.Object@fa9cf d.toString()даёт 1.0 c.toString()даёт A Также имеются методы notify() , notify All () , и несколько перегруженных вариантов метода wait , предназначенные для работы с потоками (threads). О них говорится в разделе, посвящённом потокам. Конструкторы. Зарезервированные слова super и this. Блоки инициализации Как уже говорилось, объекты в Java создаются с помощью зарезервированного слова new, после которого идёт конструктор – специальная подпрограмма, занимающаяся созданием объекта и инициализацией полей создаваемого объекта. Для него не указывается тип возвращаемого значения, и он не является ни методом объекта (вызывается через имя класса когда объекта ещё нет), ни методом класса (в конструкторе доступен объект и его поля через ссылку this). На самом деле конструктор в сочетании с оператором new возвращает ссылку на создаваемый объект и может считаться особым видом методов, соединяющим в себе черты методов класса и методов объекта. Если в объекте при создании не нужна никакая дополнительная инициализация, можно использовать конструктор, который по умолчанию присутствует для каждого класса. Это имя класса, после которого ставятся пустые круглые скобки – без списка параметров. Такой конструктор при разработке класса задавать не надо, он присутствует автоматически. Если требуется инициализация, обычно применяют конструкторы со списком параметров. Примеры таких конструкторов рассматривались нами для классов Dot и Circle. Классы Dot и Circle были унаследованы от абстрактных классов, в которых не было конструкторов. Если же идёт наследование от неабстрактного класса, то есть такого, в котором уже имеется конструктор (пусть даже и конструктор по умолчанию), возникает некоторая специфика. Первым оператором в конструкторе должен быть вызов конструктора из суперкласса. Но его делают не через имя этого класса, а с помощью зарезервированного слова super (от “superclass”), после которого идёт необходимый для прародительского конструктора список параметров. Этот конструктор инициализирует поля данных, которые наследуются от суперкласса (в том числе и от всех более ранних прародителей). Например, напишем класс FilledCircle -наследник от Circle, экземпляр которого будет отрисовываться как цветной круг. package java_gui_example; import java.awt.*; public class FilledCircle extends Circle{ /** Creates a new instance of FilledCircle */ public FilledCircle(Graphics g,Color bgColor, int r,Color color) { super(g,bgColor,r); this.color=color; } public void show(){ Color oldC=graphics.getColor(); graphics.setColor(color); graphics.setXORMode(bgColor); graphics.fillOval(x,y,size,size); graphics.setColor(oldC); graphics.setPaintMode(); } public void hide(){ Color oldC=graphics.getColor(); graphics.setColor(color); graphics.setXORMode(bgColor); graphics.fillOval(x,y,size,size); graphics.setColor(oldC); graphics.setPaintMode(); }} Вообще, логика создания сложно устроенных объектов: родительская часть объекта создаётся и инициализируется первой, начиная от части, доставшейся от класса Object, и далее по иерархии, заканчивая частью, относящейся к самому классу. Именно поэтому обычно первым оператором конструктора является вызов прародительского конструктора super(список параметров ), так как обращение к неинициализированной части объекта, относящейся к ведению прародительского класса, может привести к непредсказуемым последствиям. В данном классе мы применяем более совершенный способ отрисовки и “скрывания” фигур по сравнению с предыдущими классами. Он основан на использовании режима рисования XOR (“исключающее или”). Установка этого режима производится методом setXORMode. При этом повторный вывод фигуры на то же место приводит к восстановлению первоначального изображения в области вывода. Переход в обычный режим рисования осуществляется методом setPaintMode. В конструкторах очень часто используют зарезервированное слово this для доступа к полям объекта, видимость имён которых перекрыта переменными из списка параметров конструктора. Но в конструкторах оно имеет ещё одно применение - для обращения из одного варианта конструктора к другому, имеющему другой список параметров. Напомним, что наличие таких вариантов называется перегрузкой конструкторов. Например, пусть мы первоначально задали в классе Circle конструктор, в котором значение полей x, y и r задаётся случайным образом: Circle(Graphics g, Color bgColor){ graphics=g; this.bgColor=bgColor; size=(int)Math.round(Math.random()*40); } Тогда конструктор, в котором случайным образом задаются значения полей x и y, а значение size задаётся через список параметров конструктора, можно написать так: Circle(Graphics g, Color bgColor, int r){ this(g, bgColor); size=r; } При вызове конструктора с помощью слова this требуется, чтобы вызов this был первым оператором в реализации вызывающего конструктора. В отличие от языка C++ в Java не разрешается использование имени конструктора, отличающегося от имени класса. Порядок вызовов при создании объекта некого класса (будем называть его дочерним классом): - Создаётся объект, в котором все поля данных имеют значения по умолчанию (нули на двоичном уровне представления). - Вызывается конструктор дочернего класса. - Конструктор дочернего класса вызывает конструктор родителя (непосредственного прародителя), а также по цепочке все прародительские конструкторы и инициализации полей, заданных в этих классах, вплоть до класса Object. - Проводится инициализация полей родительской части объекта значениями, заданными в декларации родительского класса. - Выполняется тело конструктора родительского класса. - Проводится инициализация полей дочерней части объекта значениями, заданными в декларации дочернего класса. - Выполняется тело конструктора дочернего класса. Знание данного порядка важно в случаях, когда в конструкторе вызываются какие-либо методы объекта, и надо быть уверенным, что к моменту вызова этих методов объект получит правильные значения полей данных. Как правило, для инициализации полей сложно устроенных объектов используют конструкторы. Но кроме них в Java, в отличие от большинства других языков программирования, для этих целей могут также служить блоки инициализации класса и блоки инициализации объекта. Синтаксис задания классов с блоками инициализации следующий: Модификаторы class ИмяКласса extends ИмяРодителя { Задание полей; static { тело блока инициализации класса } { тело блока инициализации объекта } Задание подпрограмм - методов класса, методов объекта, конструкторов } Блоков инициализации класса и блоков инициализации объекта может быть несколько. Порядок выполнения операторов при наличии блоков инициализации главного класса приложения (содержащего метод main): - инициализация полей данных и выполнение блоков инициализации класса (в порядке записи в декларации класса); - метод main; - выполнение блоков инициализации объекта; - выполнение тела конструктора класса. Для других классов порядок аналогичен, но без вызова метода main: - инициализация полей данных и выполнение блоков инициализации класса (в порядке записи в декларации класса); - выполнение блоков инициализации объекта; - выполнение тела конструктора класса. Чем лучше пользоваться, блоками инициализации или конструкторами? Ответ, конечно, неоднозначен: в одних ситуациях – конструкторами, в других – блоками инициализации. Для придания начальных значений переменным класса в случаях, когда для этого требуются сложные алгоритмы, можно пользоваться только статическими блоками инициализации. Для инициализации полей объектов в общем случае лучше пользоваться конструкторами, но если необходимо выполнить какой-либо код инициализации до вызова унаследованного конструктора, можно воспользоваться блоком динамической инициализации. Как мы знаем, конструктор занимается созданием и рядом дополнительных действий, связанных с инициализацией объекта. Уничтожение объекта также может требовать дополнительных действий. В таких языках программирования как C++ или Object PASCAL для этих целей используют деструкторы – методы, которые уничтожают объект, и совершают все сопутствующие этому сопроводительные действия. Например, у нас имеется список фигур, отрисовываемых на экране, и мы хотим удалить из этого списка какую-нибудь фигуру. Перед уничтожением фигура должна исключить себя из списка, затем дать команду списку заново отрисовать содержащиеся в нём фигуры, и только после этого “умереть”. Именно такого рода действия характерны для деструкторов. Заметим, что возможна другая логика работы: дать списку команду исключить из него фигуру, после чего перерисовать фигуры, содержащиеся в списке. Но желательно, чтобы язык программирования поддерживал возможность реализации обоих подходов. В Java имеется метод finalize(). Если в классе , который производит завершающие действия перед уничтожением объекта сборщиком мусора, переопределить этот метод, он, как может показаться, может служить некой заменой деструктора. Но так как момент уничтожения объекта неопределёнен и может быть отнесён по времени очень далеко от момента потери ссылки на объект, метод finalize не может служить реальной заменой деструктору. Даже явный вызов сборщика мусора System.gk() сразу после вызова метода finalize() не слишком удачное решение, так как и в этом случае нет гарантии правильности порядка высвобождения ресурсов. Кроме того, сборщик мусора потребляет много ресурсов и в ряде случаев может приостановить работу программы на заметное время. Гораздо более простым и правильным решением будет написать в базовом классе разрабатываемой вами иерархии метод destroy() - “уничтожить, разрушить”, который будет заниматься выполнением всех необходимых вспомогательных действий (можно назвать метод dispose() – “избавиться, отделаться”, можно free() – “освободить”). Причём при необходимости надо будет переопределять этот метод в классах-наследниках. В случае, когда надо вызывать прародительский деструктор, следует делать вызов super.destroy(). При этом желательно, чтобы он был последним оператором в деструкторе класса – в противном случае может оказаться неправильной логика работы деструктора. Например, произойдёт попытка обращения к объекту, исключённому из списка, или попытка записи в уже закрытый файл. Логика разрушения объектов является обратной той, что используется при их создании: сначала разрушается часть, относящаяся к самому классу. Затем разрушается часть, относящаяся к непосредственному прародителю, и далее по иерархии, заканчивая частью, относящейся к базовому классу. Поэтому последним оператором деструктора бывает вызов прародительского деструктора super.destroy(). Напомним, что имя функции в сочетании с числом параметров и их типами называется сигнатурой функции. Тип возвращаемого значения и имена параметров в сигнатуру не входят. Понятие сигнатуры важно при задании подпрограмм с одинаковыми именами, но разными списками параметров – перегрузке (overloading) подпрограмм. Методы, имеющие одинаковое имя, но р азные сигнатуры, разрешается перегружать. Если же сигнатуры совпадают, перегрузка запрещена. Для задания перегруженных методов в Java не требуется никаких дополнительных действий по сравнению с заданием обычных методов. Если же перегрузка запрещена, компилятор выдаст сообщение об ошибке. Чаще всего перегружают конструкторы при желании иметь разные их варианты, так как имя конструктора определяется именем класса. Например, рассмотренные ранее конструкторы Circle(Graphics g, Color bgColor){ … } и Circle(Graphics g, Color bgColor, int r){ … } отличаются числом параметров, поэтому перегрузка разрешена. Вызов перегруженных методов синтаксически не отличается от вызова обычных методов, но всё-таки в ряде случаев возникает некоторая специфика из-за неочевидности того, какой вариант метода будет вызван. При разном числе параметров такой проблемы, очевидно, нет. Если же два варианта методов имеют одинаковое число параметров, и отличие только в типе одного или более параметров, возможны логические ошибки. Напишем класс Math1, в котором имеется подпрограмма-функция product , вычисляющая произведение двух чисел, у которой имеются варианты с разными целыми типами параметров. Пример полезен как для иллюстрации проблем, связанных с вызовом перегруженных методов, так и для исследования проблем арифметического переполнения. public class Math1 { public static byte product(byte x, byte y){ return x*y; } public static short product(short x, short y){ return x*y; } public static int product(int x, int y){ return x*y; } public static char product(char x, char y){ return x*y; } public static long product(long x, long y){ return x*y; } } Такое задание методов разрешено, так как сигнатуры перегружаемых вариантов различны. Обратим внимание на типы возвращаемых значений – они могут задаваться по желанию программиста. Подпрограммы заданы как методы класса (static) для того, чтобы при их использовании не пришлось создавать объект. Если бы мы попытались задать такие варианты методов: public static byte product(byte x, byte y){ return x*y; } public static int product(byte a, byte b){ return a*b; } то компилятор выдал бы сообщение об ошибке, так как у данных вариантов одинаковая сигнатура. - Ни тип возвращаемого значения, ни имена параметров на сигнатуру не влияют. Если при вызове метода product параметры имеют типы, совпадающие с заданными в одном из перегруженных вариантов, всё просто. Но что произойдёт в случае, когда в качестве параметра будут переданы значения типов byte и int? Какой вариант будет вызван? Проверка идёт при компиляции программы, при этом перебираются все допустимые варианты. В нашем случае это product(int x, int y) и product(long x, long y). Остальные варианты не подходят из-за типа второго параметра – тип подставляемого значенния должен иметь диапазон значений, “вписывающийся” в диапазон вызываемого метода. Из допустимых вариантов выбирается тот, который ближе по типу параметров, то есть в нашем случае product(int x, int y). Если среди перегруженных методов среди разрешённых вариантов не удаётся найти предпочтительный, при компиляции класса, где делается вызов, выдаётся диагностика ошибки. Так бы случилось, если бы мы имели следующую реализацию класса Math2 public class Math2 { public static int product(int x, byte y){ return x*y; } public static int product(byte x, int y){ return x*y; } } и в каком-нибудь другом классе имели переменные byte b1, b2 и сделали вызов Math1.product(b1,b2). Оба варианта перегруженного метода подходят, и выбрать более подходящий невозможно. Отметим, что класс Math2 при этом компилируется без проблем – в самом нём ошибок нет. Проблема в том классе, который его использует. Самая неприятная особенность перегрузки – вызов не того варианта метода, на который рассчитывал программист. Особо опасные ситуации при этом возникают в случае, когда перегруженные методы отличаются типом параметров, и в качестве таких параметров выступают объектные переменные. В этом случае близость совместимых типов определяется по близости в иерархии наследования – по числу этапов наследования. Отметим, что выбор перегруженного варианта проводится статически, на этапе компиляции. Поэтому тип, используемый для этого выбора, определяется типом объектной переменной, передаваемой в качестве параметра, а не типом объекта, который этой переменной назначен. Мы уже говорили, что полиморфный код обеспечивает основные преимущества объектного программирования. Но как им воспользоваться? Ведь тип объектных переменных задаётся на этапе компиляции. Решением проблемы является следующее правило: переменной некоторого объектного типа можно присваивать выражение, имеющее тот же тип или тип класса-наследника . Аналогичное правило действует при передаче фактического параметра в подпрограмму: В качестве фактического параметра вместо формального параметра некоторого объектного типа можно подставлять выражение, имеющее тот же тип или тип класса-наследника . В качестве выражения может выступать переменная объектного типа, оператор создания нового объекта (слово new, за которым следует конструктор), функция объектного типа (в том числе приведения объектного типа). Поэтому если мы создадим переменную базового типа, для которой можно писать полиморфный код, этой переменной можно назначить ссылку на объект, имеющий тип любого из классов-потомков. В том числе – ещё не написанных на момент компиляции базового класса. Пусть, например, мы хотим написать подпрограмму, позволяющую перемещать фигуры из нашей иерархии не в точку с новыми координатами, как метод moveTo, а на необходимую величину dx и dy по соответствующим осям. При этом у нас отсутствуют исходные коды базового класса нашей иерархии (либо их запрещено менять). Для этих целей создадим класс FiguresUtil (сокращение от Utilities – утилиты, служебные программы), а в нём зададим метод moveFigureBy (“переместить фигуру на”). public class FiguresUtil{ public static void moveFigureBy(Figure figure,int dx, int dy){ figure.moveTo(figure.x+dx, figure.y+dy); } } В качестве фактического параметра такой подпрограммы вместо figure можно подставлять выражение, имеющее тип любого класса из иерархии фигур. Пусть, например, новая фигура создаётся по нажатию на кнопку в зависимости от того, какой выбор сделал пользователь во время работы программы: если в радиогруппе отмечен пункт “Точка”, создаётся объект типа Dot. Если в радиогруппе отмечен пункт “Окружность”, создаётся объект типа Circle. Если же отмечен пункт “Круг”, создаётся объект типа FilledCircle. Отметим также, что класс FilledCircle был написан уже после компиляции классов Figure, Dot и Circle. Фрагмент кода для класса нашего приложения будет выглядеть так: Figure figure; java.awt.Graphics g=jPanel1.getGraphics(); //обработчик кнопки создания фигуры private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { if(jRadioButton1.isSelected() ) figure=new Dot(g,jPanel1.getBackground()); if(jRadioButton2.isSelected()) figure=new Circle(g,jPanel1.getBackground()); if(jRadioButton3.isSelected()) figure=new FilledCircle(g,jPanel1.getBackground(),20, java.awt.Color.BLUE); figure.show(); } //обработчик кнопки передвижения фигуры private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) { int dx= Integer.parseInt(jTextField1.getText()); int dy= Integer.parseInt(jTextField2.getText()); FiguresUtil.moveFigureBy(figure,dx,dy); } При написании программы неизвестно, ни какого типа будет передвигаемый объект, ни насколько его передвинут – всё зависит от решения пользователя во время работы программы. Именно возможность назначения ссылки на объект класса-потомка обеспечивает возможность использования полиморфного кода. Следует обратить внимание на ещё один момент – стиль написания вызова FiguresUtil.moveFigureBy(figure,dx,dy); Можно было бы написать его так: FiguresUtil.moveFigureBy( figure, Integer.parseInt(jTextField1.getText()), Integer.parseInt(jTextField2.getText()) ); При этом экономились бы две локальные переменные (аж целых 8 байт памяти! ), но читаемость, понимаемость и отлаживаемость кода стали бы гораздо меньше. Часто встречающаяся ошибка: пытаются присвоить переменной типа “наследник” выражение типа “прародитель”. Например, Figure figure; Circle circle; … figure =new Circle (); //так можно … circle= figure; - Так нельзя! Выдастся ошибка компиляции. Несмотря на то, что переменной figure назначен объект типа Circle – ведь проверка на допустимость присваивания делается на этапе компиляции, а не динамически. Если программист уверен, что объект имеет тип класса-потомка, в таких случаях надо использовать приведение типа . Для приведения типа перед выражением или именем переменной в круглых скобках ставят имя того типа, к которому надо осуществить приведение: Figure figure; Circle circle; Dot dot; … figure =new Circle (); //так можно … circle= (Circle)figure; //так можно! dot=(Dot) figure; //так тоже можно! Отметим, что приведение типа принципиально отличается от преобразования типа, хотя синтаксически записывается так же. Преобразование типа приводит к изменению содержимого ячейки памяти и может приводить к изменению её размера. А вот приведение типа не меняет ни размера, ни содержимого никаких ячеек памяти – оно меняет только тип, сопоставляемый ячейке памяти. В Java приведение типа применяется к ссылочным типам, а преобразование – к примитивным. Это связано с тем, что изменение типа ссылочной переменной не приводит к изменению той ячейки, на которую она ссылается. То есть в случае приведения тип объекта не меняется – меняется тип ссылки на объект. Приводить тип можно как в сторону генерализации, так и в сторону специализации. Приведение в сторону генерализации является безопасным, так как объект класса-потомка всегда является экземпляром прародителя, хоть и усложнённым А вот приведение в сторону специализации является опасным – вполне допустимо, что во время выполнения программы окажется, что объект, назначенный переменной, не является экземпляром нужного класса. Например, при приведении (Circle)figure может оказаться, что переменной figure назначен объект типа Dot, который не может быть приведён к типу Circle. В этом случае возникает исключительная ситуация приведения типа (typecast). Возможна программная проверка того, что объект является экземпляром заданного класса: if(figure instanceof Circle) System.out.println("figure instanceof Circle"); Иногда вместо работы с самими классами бывает удобно использовать ссылки на класс. Они получаются с помощью доступа к полю .class из любого класса. Возможно создание переменных типа “ссылка на класс”: Class c=Circle.class; Их можно использовать для обращения к переменным класса и методам класса. Кроме того, переменных типа “ссылка на класс” можно использовать для создания экземпляров этого класса с помощью метода newInstance(): Circle circle=(Circle)c.newInstance(); Возможна программная проверка соответствия объекта нужному типу с помощью ссылки на класс: if(figure.getClass()==Circle.class) circle= (Circle)figure; …; Но следует учитывать, что при такой проверке идёт сравнение на точное равенство классов, а не на допустимость приведения типов. А вот оператор isInstance позволяет проверять, является ли объект figure экземпляром класса, на который ссылается c : if(c.isInstance(figure)) System.out.println("figure isInstance of Circle"); Одним из важных элементов современного программирования является рефакторинг – изменение структуры существующего проекта без изменения его функциональности. Приведём три наиболее часто встречающихся примера рефакторинга. · Во-первых, это переименование элементов программы – классов, переменных, методов. · Во-вторых, перемещение элементов программы с одного места на другое. · В-третьих, инкапсуляция полей данных. В сложных проектах, конечно, возникают и другие варианты рефакторинга (например, выделение части кода в отдельный метод – “Extract method”), но с упомянутыми приходится встречаться постоянно. Поэтому рассмотрим эти три случая подробнее. Первый случай - переименование элементов программы . Для того, чтобы в среде NetBeans переименовать элемент, следует щёлкнуть по его имени правой кнопкой мыши. Это можно сделать в исходном коде программы, а можно и в окне Projects или Navigator. В появившемся всплывающем меню следует выбрать Refactor/Rename… После чего ввести новое имя и нажать кнопку “Next>”. Переименование класса. Шаг 1 Переименование класса. Шаг 2 Если галочка “Preview All Changes” (“Предварительный просмотр всех изменений”) не снята, в самом нижнем окне, Output (“Вывод”), появится дерево со списком мест, где будут проведены исправления. В случае необходимости галочки можно снять, и в этих местах переименование проводиться не будет. При нажатии на кнопку “Do Refactoring” (“Провести рефакторинг”) проводится операция переименования в выбранных местах программы. В отличие от обычных текстовых процессоров переименование происходит с учётом синтаксиса программы, так что элементы, не имеющие отношения к переименовываемому, но имеющие такие же имена, не затрагиваются. Что в выгодную сторону отличает NetBeans от многих других сред разработки, не говоря уж об обычных текстовых редакторах. Переименование класса. Шаг 3 Требуется быть внимательными: довольно часто начинающие программисты не замечают появления в окне Output списка изменений и кнопки “Do Refactoring”. Особенно если высота этого окна сделана очень малой. Если в диалоге переименования (шаг 2) флажок “Preview all Changes” снят, при нажатии на кнопку “Next>” сразу происходит рефакторинг. Следует также отметить, что после проведения рефакторинга возможен возврат к первоначальному состоянию (“откат”, операция undo). Обычно такая операция осуществляется с помощью главного меню проекта (кнопка Undo или пункт меню Edit/Undo), но в случае рефакторинга требуется правой клавишей мыши вызвать всплывающее окно и выбрать пункт Refactor/Undo. Откат может быть на несколько шагов назад путём повторения данного действия. Пре необходимости отказа от отката в меню рефакторинга следует выбрать пункт Redo. Второй случай - перемещение элементов программы с одного места на другое. Например, мы хотим переместить класс из одного пакета в другой. Для выполнения этого действия достаточно перетащить мышью в окне Projects узел, связанный с данным классом, в соответствующий пакет. При таком перемещении там, где это необходимо, автоматически добавляются операторы импорта. Если при перемещении возникают проблемы, о них выдаётся сообщение. Как правило, проблемы бывают связаны с неправильными уровнями видимости. Например, если указан пакетный уровень видимости метода, он доступен другим классам этого пакета. А при переносе класса в другой пакет в месте исходного кода, где осуществляется такой доступ, в новом варианте кода возникает ошибка доступа. Перенос класса в отдельный пакет, отличающийся от пакета приложения – хороший способ проверить правильности выбранных уровней доступа для членов класса. Аналогичным образом перемещаются пакеты. При этом все пакеты в дереве элементов показываются на одном уровне вложенности, но у вложенных пакетов имена квалифицируются именем родительского пакета.
Третий случай - инкапсуляция полей данных. Напрямую давать доступ к полю данных – дурной тон программирования. Поэтому рекомендуется давать полям уровень видимости private, а доступ к ним по чтению и записи осуществлять с помощью методов getИмяПоля и setИмяПоля - получить и установить значение этого поля. Такие методы в Java называют геттерами (getters) и сеттерами (setters). Но при введении в класс новых полей на первом этапе часто бывает удобнее задать поля с модификатором public и обеспечивать чтение значения полей напрямую, а изменение значения – путём присваивания полям новых значений. А затем можно исправить данный недостаток программы с помощью инкапсуляции полей данных. Это делается просто: в дереве элементов программы окна Projects в разделе Fields (“поля”) щёлкнем правой кнопкой мыши по имени поля и выберем в появившемся всплывающем меню Refactor/Encapsulate Fields… (“Провести рефакторинг”/ “Инкапсулировать поля…”).В появившемся диалоге нажмём на кнопку “Next>” и проведём рефакторинг. При этом каждое поле приобретёт модификатор видимости private, а во всех местах программы, где напрямую шёл доступ к этому полю, в коде будет проведена замена на вызовы геттеров и сеттеров. Более подробную информацию по идеологии и методах рефакторинга проектов, написанных на языке Java, можно найти в монографии [7]. Правда, эта книга уже несколько устарела – среда NetBeans позволяет делать в автоматическом режиме многие из описанных в [7] действий. Reverse engineering – построение UML-диаграмм по разработанным классам Среда NetBeans при установленном пакете NetBeans Enterprise Pack позволяет по имеющемуся исходному коду построить UML-диаграммы. Для этого следует открыть проект и нажать на главной панели среды разработки кнопку “Reverse Engineer…” Кнопка “ Reverse Engineering ” Появится диалоговая форма задания параметров создаваемого проекта, в которой следует изменить название проекта на осмысленное, по которому легко можно будет определить, к какому проекту Java он относится. Диалоговая форма задания параметров создаваемого UML -проекта В нашем случае UMLProject7 мы заменим на UML_Figure. После нажатия на кнопку Finish (“Закончить”) будет выдана форма с ненужной вспомогательной информацией, и в ней следует нажать кнопку Done (“Сделано”). В результате чего мы получим новый UML-проект, в котором можно просмотреть параметры, относящиеся к каждому классу: Параметры UML -проекта, относящиеся к классу Circle Для класса показываются конструкторы и обычные методы (узел Operations), а также отношения наследования и другие варианты отношений (узел Relationships). В UML-проекте можно сгенерировать UML-диаграммы, щёлкнув правой кнопкой мыши по имени соответствующего класса: Всплывающее меню действий с классом в UML -проекте Если выбрать пункт “Create Diagram From Selected Elements” (“Создать диаграмму из выбранных элементов”), и далее выбрать тип диаграммы “Class Diagram”, Выбор типа создаваемой диаграмы можно получить диаграмму такого вида: Диаграмма для класса Circle При этом лучше заменить имя создаваемой диаграммы, например, на Circle Diagram. Переименование можно сделать и позже, щёлкнув правой кнопкой мыши по имени диаграммы и выбрав в появившемся всплывающем меню пункт Rename… (“Переименовать…”). Если же выделить Circle,Dot,Figure, ScalableFigure, мы получим диаграмму наследования, которой можно дать имя Inheritance Diagram. Диаграмма для классов Circle,Dot,Figure, ScalableFigure Если для класса Circle во всплывающем меню выбрать пункт “Generate Dependency Diagram” (“Сгенерировать диаграмму зависимостей”), получим следующую диаграмму : Диаграмма зависимостей для класса Circle Пункт всплывающего меню Navigate to Source позволяет вместо диаграмм показывать редактор исходного кода. На диаграммах можно добавлять в классы или удалять из них поля и методы, проводить переименования, менять модификаторы. Причём изменения, сделанные на любой из диаграмм, автоматически отражаются как на других диаграммах UML-проекта, так и в исходном коде проекта Java (это проектирование – Forward Enineering). И наоборот - изменения, сделанные в исходном коде Java, автоматически применяются к диаграммам UML (это обратное проектирование – Reverse Enineering). В настоящее время работа с UML-проектами в NetBeans Enterprise Pack не до конца отлажена, иногда наблюдаются “баги” (мелкие ошибки). Но можно надеяться, что в ближайшее время недостатки будут исправлены. - Наследование опирается на инкапсуляцию. Оно позволяет строить на основе первоначального класса новые, добавляя в классы новые поля данных и методы. Первоначальный класс называется прародителем (ancestor), новые классы – его потомками (descendants). От потомков можно наследовать, получая очередных потомков. Набор классов, связанных отношением наследования, называется иерархией классов . Класс, стоящий во главе иерархии, от которого унаследованы все остальные (прямо или опосредованно), называется базовым классом иерархии . - Иерархия нужна для того, чтобы писать полиморфный код . Основные преимущества объектного программирования обеспечиваются наличием полиморфного кода. - Поля отражают состояние объекта, а методы - задают его поведение. - Чем ближе к основанию иерархии лежит класс, тем более общим и универсальным (general) он является. Чем дальше от базового класса иерархии стоит класс, тем более специализированным (specialized) он является. - Каждый объект класса-потомка при любых значениях его полей данных должен рассматриваться как экземпляр класса-прародителя на уровне абстракций поведения, но с некоторыми изменениями в реализации этого поведения. - Проектирование классов осуществляется с помощью UML-диаграмм. - В Java подпрограммы задаются только как методы в каком-либо классе и называются функциями . Объявление в классе функции состоит из задания заголовка и тела функции (её реализации ). - Параметры, указанные в заголовке функции при её декларации, называются формальными . А те параметры, которые подставляются во время вызова функции, называются фактическими . Формальные параметры нужны для того, чтобы указать последовательность действий с фактическими параметрами после того, как те будут переданы в подпрограмму во время вызова. Это ни что иное, как особый вид локальных переменных, которые используются для обмена данными с внешним миром. - Параметры в Java передаются в функцию всегда по значению . Передача по ссылке отсутствует. В частности, ссылки передаются в функции по значению. - В Java имеются уровни видимости private (“частный”, “закрытый”), пакетный, protected (“защищённый”) и public (Общедоступный). Пакетный – уровень видимости по умолчанию для полей и методов, для задания остальных уровней используются модификаторы private, protected и public. - Ссылка this обеспечивает ссылку на объект из метода объекта. Чаще всего она используется при перекрытии области видимости имени поля объекта формальным параметром функции. - В классе-наследнике методы можно переопределять . При этом у них должен сохраняться контракт – в который входит весь заголовок метода за исключением имён формальных параметров. - Можно задавать перегруженные (overloaded) варианты методов, отличающиеся сигнатурой . В сигнатуру входит только часть заголовка метода – имя функции, а также число, порядок и тип её параметров. - Переменной некоторого объектного типа можно присваивать выражение, имеющее тот же тип или тип класса-наследника. В качестве фактического параметра функции вместо формального параметра некоторого объектного типа можно подставлять выражение, имеющее тот же тип или тип класса-наследника. Именно это правило обеспечивает возможность использования полиморфного кода. - Для приведения типа используется имя типа, заключённое в круглые скобки – как и для преобразования типа. Но при преобразовании типа могут меняться содержимое и размер ячейки, к которой применяется данный оператор, а при приведении типа ячейка и её содержимое остаются теми же, просто начинают считать, что у ячейки другой тип. - Проверка на то, что объект является экземпляром заданного класса, осуществляется оператором instanceof: if(figure instanceof Circle)... - Возможна программная проверка точного соответствия объекта нужному типу с помощью ссылки на класс: if(figure.getClass()==Circle.class)... При этом для экземпляра класса-наследника Circle сравнение даст false, в отличие от экземпляра Circle. - Оператор isInstance позволяет проверять, является ли тип объекта совместимым с классом, на который задана ссылка c: if(c.isInstance(figure))... При если класс, экземпляром которого является объект figure, является наследником класса c или совпадает с ним, сравнение даст true. - Рефакторинг – изменение структуры существующего проекта без изменения его функциональности. Три наиболее часто встречающихся примера рефакторинга: 1) Переименование элементов программы – классов, переменных, методов. 2) Перемещение элементов программы с одного места на другое. 3) Инкапсуляция полей данных. - С помощью средств Reverse Engineering можно создавать UML-диаграммы классов и зависимостей классов. Причём после создания UML-проекта, сопровождающего Java-проект, изменения, сделанные в исходном коде Java, автоматически применяются к диаграммам UML, и наоборот. Типичные ошибки:
public static double factorial(int n) Модификатор static помечает подпрограмму как метод класса. То есть позволяет вызывать метод через имя класса без создания объекта. Напомним, что факториал натурального числа n – это произведение всех натуральных чисел от 1 до n : Кроме того, 0! считается равным 1. Обозначение факториала в виде n! математическое, в Java символ “!” зарезервирован для других целей. Также написать подпрограммы вычисления факториала с другими типами возвращаемых значений: public static long factorial_long(int n) и public static int factorial_int(int n) Сравнить работу подпрограмм при n=0,1,5,10,20,50,100. Объяснить результаты.
Глава 7. Важнейшие объектные типы Массив (array) – это упорядоченный набор одинаково устроенных ячеек, доступ к которым осуществляется по индексу. Например, если у массива имя a1, то a1[i] – имя ячейки этого массива, имеющей с индекс i. В Java массивы являются объектами , но особого рода – их объявление отличается от объявления других видов объектов. Переменная типа массив является ссылочной – в ней содержится адрес объекта, а не сам объект, как и для всех других объектных переменных в Java. В качестве элементов (ячеек) массива могут выступать значения как примитивных типов, так и ссылочных типов, в том числе – переменные типа массив. Тип ячейки массива называется базовым типом для массива. Для задания массива, в отличие от объектов других типов, не требуется предварительно задавать класс, и иметь специальное имя для данного объектного типа. Вместо имени класса при объявлении переменной используется имя базового типа, после которого идут пустые квадратные скобки. Например, объявление int[] a1; задаёт переменную a1 типа массив. При этом размер массива (число ячеек в нём) заранее не задаётся и не является частью типа. Для того, чтобы создать объект типа массив, следует воспользоваться зарезервированным словом new, после чего указать имя базового типа, а за ним в квадратных скобках число ячеек в создаваемом массиве: a1=new int[10]; Можно совместить объявление типа переменной и создание массива : int[] a1=new int[10]; После создания массивы Java всегда инициализированы – в ячейках содержатся нули. Поэтому если базовый тип массива примитивный, элементы массива будут нулями соответствующего типа. А если базовый тип ссылочный – в ячейках будут значения null. Ячейки в массиве имеют индексы, всегда начинающиеся с нуля. То есть первая ячейка имеет номер 0, вторая – номер 1, и так далее. Если число элементов в массиве равно n, то последняя ячейка имеет индекс n-1. Такая своеобразная нумерация принята в языках C и C++, и язык Java унаследовал эту не очень привлекательную особенность, часто приводящую к ошибкам при организации циклов. Длина массива хранится в поле length, которое доступно только по чтению – изменять его путём присваивания нового значения нельзя. Пример работы с массивом: int[] a=new int[100]; for(int i=0;i<a.length;i++){ a[i]=i+1; }; Если у нас имеется переменная типа массив, и ей сопоставлен массив заданной длины, в любой момент этой переменной можно сопоставить новый массив. Например, a1=new int[20]; При этом прежний объект-массив, находящийся в динамической области памяти, будет утерян и превратится в мусор. Переменные типа массив можно присваивать друг другу. Например, если мы задали переменную int[] a2; то сначала в ней хранится значение null (ссылка направлена “в никуда”): Массив с ячейками типа int
a1
a2
Присваивание a2=a1; приведёт к тому, что ссылочные переменные a1 и a2 будут ссылаться на один и тот же массив, расположенный в динамической области памяти. Массив с ячейками типа int
a1
a2
То есть присваивание переменных типа массив приводит к тому, что имена переменных становятся синонимами одного и того же массива – копируется адрес массива. А вовсе не приводит к копированию элементов из одного массива в другой, как это происходит в некоторых других языках программирования. В качестве элементов массивов могут выступать объекты. В этом случае доступ к полям и методам этих объектов производится через имя ячейки массива, после которого через точку указывается имя поля или метода. Например, если у нас имеется класс Circle (“окружность”), у которого имеются поля x, y и r, а также методы show() и hide(), то массив circles из 10 объектов такого типа может быть задан и инициализирован, например, так int n=10; Circle[] circles=new Circle[n]; for(int i=0;i<n;i++){ circles[i]=new Circle(); circles[i].x=40*i; circles[i].y= circles[i].x/2; circles[i].r=50; circles[i].show(); }; В такого рода программах для повышения читаемости часто применяется использование вспомогательной ссылки, позволяющей избежать многократного обращения по индексу. В нашем случае мы будем использовать в этих целях переменную circle. На скорости работы программы это почти не сказывается (хотя и может чуть повысить быстродействие), но делает код более читаемым: int n=10; Circle[] circles=new Circle[n]; Circle circle; for(int i=0;i<n;i++){ circle=new Circle(); circle.x=40*i; circle.y= circles[i].x/2; circle.r=50; circle.show(); circles[i]= circle; }; С помощью переменной circle мы инициализируем создаваемые объекты и показываем их на экране, после чего присваиваем ссылку на них ячейкам массива. Двумерный массив представляет собой массив ячеек, каждая из которых имеет тип “одномерный массив”. Соответствующим образом он и задаётся. Например, задание двумерного массива целых чисел будет выглядеть так: int[][] a=new int[10][20]; Будет задана ячейка типа “двумерный массив”, а также создан и назначен этой ссылочной переменной массив, имеющий по первому индексу 10 элементов, а по второму 20. То есть мы имеем 10 ячеек типа “одномерный массив”, каждая из которых ссылается на массив из 20 целых чисел. При этом базовым типом для ячеек по первому индексу является int[], а для ячеек по второму индексу int. Рассмотрим работу с двумерными массивами на примере заполнения двумерного массива случайными числами: int m=10;//10 строк int n=20;//20 столбцов int[][] a=new int[m][n]; for(int i=0;i<m;i++){ //цикл по строкам for(int j=0;j<n;j++){ //цикл по столбцам a[i][j]=(int)(100*Math.random()); System.out.print(a[i][j]+" "); }; System.out.println();//перевод на новую строку после вывода строки матрицы }; Многомерные массивы задаются аналогично двумерным – только указывается необходимое количество прямоугольных скобок. Следует отметить, что массивы размерности больше 3 используют крайне редко. Обычно в двумерных и многомерных массивах задают одинаковый размер всех массивов, связанных с ячейками по какому-либо индексу. Такие массивы называют регулярными. В Java, в отличие от большинства других языков программирования, можно задавать массивы с разным размером массивов, связанных с ячейками по какому-либо индексу. Такие “непрямоугольные” массивы называют иррегулярными. Обычно их используют для экономии памяти. При работе с иррегулярными массивами следует быть особенно аккуратными, так как разный размер “вложенных” массивов часто приводит к ошибкам при реализации алгоритмов. Пример задания иррегулярного двумерного массива треугольной формы: int n=9; int[][] a=new int[n][]; for(int i=0;i<a.length;i++){ //цикл по строкам a[i]=new int[i+1]; //число элементов в строке равно i+1 for(int j=0;j<a[i].length;j++){ //цикл по столбцам a[i][j]=100*i+j; System.out.print(a[i][j]+" "); }; System.out.println();//перевод на новую строку после вывода строки матрицы }; После создания массива требуется его инициализировать – записать нужные значения в ячейки. До сих пор мы делали это путём задания значений в цикле по некоторой формуле, однако часто требуется задать конкретные значения. Конечно, можно это сделать в виде int[] a=new int[4]; a[0]=2; a[1]=0; a[2]=0; a[3]=6; Но гораздо удобнее следующий вариант синтаксиса: int[] a=new int[] {2,0,0,6}; При этом приходится задавать массив без указания его размера непосредственно с помощью указания значений в фигурных скобках: В правой части оператора присваивания стоит так называемый анонимный массив – у него нет имени. Такие массивы обычно используют для инициализации, а также при написании кода для различного рода проверок. Если мы хотим присвоить новые значения, приходится либо присваивать поэлементно, либо создавать новый объект: a=new int[] {2,0,0,6}; При инициализации двумерных и многомерных массивов используют вложенные массивы, задаваемые с помощью фигурных скобок. Например, фрагмент кода int[][] b= new int[][] { {2,0,0,0}, //это b[0] {2,0,0,1}, //это b[1] {2,0,0,2}, //это b[2] {1,0,0,0}, //это b[3] {2,0,0,0}, //это b[4] {3,0,0,0}, //это b[5] }; приведёт к заданию целочисленного двумерного массива b, состоящего из 6 строк и 4 столбцов, т.е. int[6][4]. Таким образом можно задавать как регулярные, так и иррегулярные массивы. Но следует помнить, что в таком варианте синтаксиса проверки правильности размера массива по индексам не делается, что может привести к ошибкам. Например, следующий код при компиляции не выдаст ошибки, а будет создан иррегулярный массив: int[][] b= new int[][] { {2,0,0,0}, //это b[0] {2,0,0,1}, //это b[1] {2,0,0,2}, //это b[2] {1,0,0,0}, //это b[3] {2,0,0,0}, //это b[4] {3,0,0}, //это b[5] – массив из трёх элементов }; Из объектов-массивов можно вызывать метод clone(), позволяющий создавать копию (клон) массива: a=new int[] {2,0,0,6}; int[] a1=a.clone(); Напомним, что присваивание int[] b=a; не приведёт к копированию массива – просто переменная b станет ссылаться на тот же объект-массив. Копирование массивов можно осуществлять в цикле, но гораздо быстрее использовать метод System.arraycopy. int[] b=new int[a.length+10]; System.arraycopy(a,index1a,b, index1b,count); Из a в b копируется count элементов начиная с индекса index1a в массиве a. Они размещаются в массиве b начиная с индекса index1b. Содержимое остальных элементов b не меняется. Для использования метода требуется, чтобы массив b существовал и имел необходимую длину - при выходе за границы массивов возбуждается исключительная ситуация. Быстрое заполнение массива одинаковыми значениями может осуществляться методом Arrays.fill(массив , значение ). Класс Arrays расположен в пакете java.util. Поэлементное сравнение массива следует выполнять с помощью метода Arrays.equals(a,a1). Заметим, что у любого массива имеется метод equals, унаследованный от класса Object и позволяющий сравнивать массивы. Но, к сожалению, метод не переопределён, и сравнение идёт по адресам объектов, а не по содержимому. Поэтому a.equals(a1) это то же самое, что a==a1. Оба сравнения вернут false, так как адреса объектов, на которые ссылаются переменные a и a1, различаются. Напротив, сравнения a.equals(a3) и a==a3 вернут true, так как a и a3 ссылаются на один и тот же объект-массив. Сортировка (упорядочение по значениям) массива a производится методами Arrays.sort(a) и Arrays.sort(a,index1,index2). Первый из них упорядочивает в порядке возрастания весь массив, второй – часть элементов (от индекса index1 до индекса index2). Имеются и более сложные методы сортировки. Элементы массива должны быть сравниваемы (поддерживать операцию сравнения). Arrays.deepEquals(a1,a2) – сравнение на равенство содержимого массивов объектов a1 и a2 путём глубокого сравнения (на равенство содержимого, а не ссылок – на произвольном уровне вложенности). Также в классе Arrays содержится большое число других полезных методов. В Java получили широкое использование коллекции (Collections) – “умные” массивы с динамически изменяемой длиной, поддерживающие ряд важных дополнительных операций по сравнению с массивами. Базовым для иерархии коллекций является класс java.util.AbstractCollection. (В общем случае класс коллекции не обязан быть потомком AbstractCollection – он может является любым классом, реализующим интерфейс Collection). Основные классы коллекций:
Доступ к элементам коллекции в общем случае не может осуществляться по индексу, так как не все коллекции поддерживают индексацию элементов. Эту функцию осуществляют с помощью специального объекта – итератора (iterator). У каждой коллекции collection имеется свой итератор который умеет с ней работать, поэтому итератор вводят следующим образом: Iterator iter = collection.iterator() У итераторов имеются следующие три метода: boolean hasNext()- даёт информацию, имеется ли в коллекции следующий объект. Object next() – возвращает ссылку на следующий объект коллекции. void remove() – удаляет из коллекции текущий объект, то есть тот, ссылка на который была получена последним вызовом next(). Пример преобразования массива в коллекцию и цикл с доступом к элементам этой коллекции, осуществляемый с помощью итератора: java.util.List components= java.util.Arrays.asList(this.getComponents()); for (Iterator iter = components.iterator();iter.hasNext();) { Object elem = (Object) iter.next(); javax.swing.JOptionPane.showMessageDialog(null,"Компонент: "+ elem.toString()); } Основные методы коллекций:
Самыми распространёнными вариантами коллекций являются списки (Lists). Они во многом похожи на массивы, но отличаются от массивов тем, что в списках основными операциями являются добавление и удаление элементов. А не доступ к элементам по индексу, как в массивах. В классе List имеются методы коллекции, а также ряд дополнительных методов: list.get(i) – получение ссылки на элемент списка list по индексу i. list.indexOf(obj) - получение индекса элемента obj в списке list. Возвращает -1 если объект не найден. list.listIterator(i) – получение ссылки на итератор типа ListIterator, обладающего дополнительными методами по сравнению с итераторами типа Iterator. list.listIterator(i) – то же с позиционированием итератора на элемент с индексом i. list.remove(i) – удаление из списка элемента с индексом i. list.set(i,obj) – замена в списке элемента с индексом i на объект obj. list.subList(i1,i2) – возвращает ссылку на подсписок, состоящий из элементов списка с индексами от i1 до i2. Кроме них в классе List имеются и многие другие полезные методы. Ряд полезных методов для работы с коллекциями содержится в классе Collections: Collections.addAll(c,e1,e2,…,eN) - добавление в коллекцию c произвольного числа элементов e1,e2,…,eN. Collections.frequency(c,obj) – возвращает число вхождений элемента obj в коллекцию c. Collections.reverse(list) – обращает порядок следования элементов в списке list (первые становятся последними и наоборот). Collections.sort(list) – сортирует список в порядке возрастания элементов. Сравнение идёт вызовом метода e1.compareTo(e2) для очередных элементов списка e1 и e2. Кроме них в классе Collections имеются и многие другие полезные методы. В классе Arrays имеется метод Arrays.asList(a) – возвращает ссылку на список элементов типа T, являющийся оболочкой над массивом T[] a . При этом и массив, и список содержат одни и те же элементы, и изменение элемента списка приводит к изменению элемента массива, и наоборот. Работа со строками в Java. Строки как объекты. Классы String, StringBuffer и StringBuilder Класс String инкапсулирует действия со строками. Объект типа String – строка, состоящая из произвольного числа символов, от 0 до 2*109 . Литерные константы типа String представляют собой последовательности символов, заключённые в двойные кавычки: ”A”, ”abcd”, ”abcd”, ”Мама моет раму”, ” ”. Это так называемые “длинные” строки. Внутри литерной строковой константы не разрешается использовать ряд символов - вместо них применяются управляющие последовательности. Внутри строки разрешается использовать переносы на новую строку. Но литерные константы с такими переносами запрещены, и надо ставить управляющую последовательность “\n”. К сожалению, такой перенос строки не срабатывает в компонентах. Разрешены пустые строки, не содержащие ни одного символа. В языке Java строковый и символьный тип несовместимы. Поэтому ”A” – строка из одного символа, а 'A' – число с ASCII кодом символа ”A”. Это заметно усложняет работу со строками и символами. Строки можно складывать: если s1 и s2 строковые литерные константы или переменные, то результатом операции s1+s2 будет строка, являющаяся сцеплением (конкатенацией) строк, хранящихся в s1 и s2. Например, в результате операции String s=”Это ”+”моя строка”; в переменной s будет храниться строковое значение ”Это моя строка”. Для строк разрешён оператор ”+=”. Для строковых операндов s1 и s2 выражение s1+=s2 эквивалентно выражению s1=s1+s2. Любая строка (набор символов) является объектом – экземпляром класса String. Переменные типа String являются ссылками на объекты, что следует учитывать при передаче параметров строкового типа в подпрограммы, а также при многократных изменениях строк. При каждом изменении строки в динамической области памяти создаётся новый объект, а прежний превращается в “мусор”. Поэтому при многократных изменениях строк в цикле возникает много мусора, что нежелательно. Очень частой ошибкой является попытка сравнения строк с помощью оператора “==”. Например, результатом выполнения следующего фрагмента String s1="Строка типа String"; String s2="Строка"; s2+=" типа String"; if(s1==s2) System.out.println("s1 равно s2"); else System.out.println("s1 не равно s2"); будет вывод в консольное окно строки “s1 не равно s2”, так как объекты-строки имеют в памяти разные адреса. Сравнение по содержанию для строк выполняет оператор equals. Поэтому если бы вместо s1==s2 мы написали s1.equals(s2), то получили бы ответ “s1 равно s2”. Удивительным может показаться факт, что результатом выполнения следующего фрагмента String s1="Строка"; String s2="Строка"; if(s1==s2) System.out.println("s1 равно s2"); else System.out.println("s1 не равно s2"); будет вывод в консольное окно строки “s1 равно s2”. Дело в том, что оптимизирующий компилятор Java анализирует имеющиеся в коде программы литерные константы, и для одинаковых по содержанию констант использует одни и те же объекты-строки. В остальных случаях то, что строковые переменные ссылочные, обычно никак не влияет на работу со строковыми переменными, и с ними можно действовать так, как если бы они содержали сами строки. В классе String имеется ряд методов. Перечислим важнейшие из них. Пусть s1 и subS имеют тип String, charArray – массив символов char[], ch1 – переменная или значение типа char, а i, index1 и count (“счёт, количество”) – целочисленные переменные или значения. Тогда String.valueOf(параметр) – возвращает строку типа String, являющуюся результатом преобразования параметра в строку. Параметр может быть любого примитивного или объектного типа. String.valueOf(charArray, index1,count) – функция, аналогичная предыдущей для массива символов, но преобразуется count символов начиная с символа, имеющего индекс index1. У объектов типа String также имеется ряд методов. Перечислим важнейшие из них. s1.charAt(i) – символ в строке s1, имеющий индекс i (индексация начинается с нуля). s1.endsWith(subS) – возвращает true в случае, когда строка s1 заканчивается последовательностью символов, содержащихся в строке subS. s1.equals(subS) - возвращает true в случае, когда последовательностью символов, содержащихся в строке s1, совпадает с последовательностью символов, содержащихся в строке subS. s1.equalsIgnoreCase(subS) – то же, но при сравнении строк игнорируются различия в регистре символов (строчные и заглавные буквы не различаются). s1.getBytes() – возвращает массив типа byte[], полученный в результате платформо-зависимого преобразования символов строки в последовательность байт. s1.getBytes(charset) – то же, но с указанием кодировки (charset). В качестве строки charset могут быть использованы значения “ISO-8859-1” (стандартный латинский алфавит в 8-битовой кодировке), “UTF-8”, “UTF-16” (символы UNICODE) и другие. s1.indexOf(subS) – индекс позиции, где в строке s1 первый раз встретилась последовательность символов subS. s1.indexOf(subS,i) – индекс позиции начиная с i, где в строке s1 первый раз встретилась последовательность символов subS. s1. lastIndexOf (subS) – индекс позиции, где в строке s1 последний раз встретилась последовательность символов subS. s1. lastIndexOf (subS,i) – индекс позиции начиная с i, где в строке s1 последний раз встретилась последовательность символов subS. s1.length() – длина строки (число 16-битных символов UNICODE, содержащихся в строке). Длина пустой строки равна нулю. s1.replaceFirst(oldSubS,newSubS) – возвращает строку на основе строки s1, в которой произведена замена первого вхождения символов строки oldSubS на символы строки newSubS. s1.replaceAll(oldSubS,newSubS)– возвращает строку на основе строки s1, в которой произведена замена всех вхождений символов строки oldSubS на символы строки newSubS. s1.split(separator) – возвращает массив строк String[], полученный разделением строки s1 на независимые строки по местам вхождения сепаратора, задаваемого строкой separator. При этом символы, содержащиеся в строке separator, в получившиеся строки не входят. Пустые строки из конца получившегося массива удаляются. s1.split(separator, i) – то же, но положительное i задаёт максимальное допустимое число элементов массива. В этом случае последним элементом массива становится окончание строки s1, которое не было расщеплено на строки, вместе с входящими в это окончание символами сепараторов. При i равном 0 ограничений нет, но пустые строки из конца получившегося массива удаляются. При i <0 ограничений нет, а пустые строки из конца получившегося массива не удаляются. s1.startsWith(subS) – возвращает true в случае, когда строка s1 начинается с символов строки subs. s1.startsWith(subs, index1) – возвращает true в случае, когда символы строки s1 с позиции index1 начинаются с символов строки subs. s1.substring(index1) – возвращает строку с символами, скопированными из строки s1 начиная с позиции index1. s1.substring(index1,index2) – возвращает строку с символами, скопированными из строки s1 начиная с позиции index1 и кончая позицией index2. s1.toCharArray() – возвращает массив символов, скопированных из строки s1. s1.toLowerCase() – возвращает строку с символами, скопированными из строки s1, и преобразованными к нижнему регистру (строчным буквам). Имеется вариант метода, делающего такое преобразование с учётом конкретной кодировки (locale). s1.toUpperCase() - возвращает строку с символами, скопированными из строки s1, и преобразованными к верхнему регистру (заглавным буквам). Имеется вариант метода, делающего такое преобразование с учётом конкретной кодировки (locale). s1.trim() – возвращает копию строки s1, из которой убраны ведущие и завершающие пробелы. В классе Object имеется метод toString(), обеспечивающий строковое представление объекта. Конечно, оно не может отражать все особенности объекта, а является представлением “по мере возможностей”. В самом классе Object оно обеспечивает возврат методом полного имени класса (квалифицированное именем пакета), затем идёт символ “@”, после которого следует число – хэш-код объекта (число, однозначно характеризующее данный объект во время сеанса работы) в шестнадцатеричном представлении. Поэтому во всех классах-наследниках, где этот метод не переопределён, он возвращает такую же конструкцию. Во многих стандартных классах этот метод переопределён. Например, для числовых классов метод toString() обеспечивает вывод строкового представления соответствующего числового значения. Для строковых объектов - возвращает саму строку, а для символьных (тип Char) - символ. При использовании операций “+” и “+=” с операндами, один из которых является строковым, а другой нет, метод toString() вызывается автоматически для нестрокового операнда. В результате получается сложение (конкатенация) двух строк. При таких действиях следует быть очень внимательными, так как результат сложения более чем двух слагаемых может оказаться сильно отличающимся от ожидаемого. Например, String s=1+2+3; даст вполне ожидаемое значение s==”6”. А вот присваивание String s=”Сумма =”+1+2+3; даст не очень понятное начинающим программистам значение ” Сумма =123”. Дело в том, что в первом случае сначала выполняются арифметические сложения, а затем результат преобразуется в строку и присваивается левой части. А во втором сначала производится сложение ”Сумма =”+1. Первый операнд строковый, а второй – числовой. Поэтому для второго операнда вызывается метод toString(), и складываются две строки. Результатом будет строка ”Сумма =1”. Затем складывается строка ”Сумма =1” и число 2. Опять для второго операнда вызывается метод toString(), и складываются две строки. Результатом будет строка ”Сумма =12”. Совершенно так же выполняется сложение строки ”Сумма =12” и числа 3. Ещё более странный результат получится при присваивании String s=1+2+” не равно ”+1+2; Следуя изложенной выше логике мы получаем, что результатом будет строка “3 не равно 12”. Выше были приведены простейшие примеры, и для них всё достаточно очевидно. Если же в такого рода выражениях используются числовые и строковые функции, да ещё с оператором “ ? : ”, результат может оказаться совершенно непредсказуемым. Кроме указанных выше имеется ряд строковых операторов, заданных в оболочечных числовых классах. Например, мы уже хорошо знаем методы преобразования строковых представлений чисел в числовые значения Byte.parseByte(строка ) Short.parseShort(строка ) Integer.parseInt(строка ) Long.parseLong(строка ) Float.parseFloat(строка ) Double.parseDouble(строка ) и метод valueOf(строка ), преобразующий строковые представления чисел в числовые объекты – экземпляры оболочечных классов Byte, Short, Character, Integer, Long, Float, Double. Например, Byte.valueOf(строка ) , и т.п. Кроме того, имеются методы классов Integer и Long для преобразования чисел в двоичное и шестнадцатеричное строковое представление: Integer.toBinaryString(число ) Integer.toHexString(число ) Long.toBinaryString(число ) Long.toHexString(число ) Имеется возможность обратного преобразования – из строки в объект соответствующего класса (Byte, Short, Integer, Long) с помощью метода decode: Byte.decode(строка ) , и т.п. Также полезны методы для анализа отдельных символов: Character.isDigit(символ ) – булевская функция, проверяющая, является ли символ цифрой. Character.isLetter(символ ) – булевская функция, проверяющая, является ли символ буквой. Character.isLetterOrDigit(символ ) – булевская функция, проверяющая, является ли символ буквой или цифрой. Character.isLowerCase(символ ) – булевская функция, проверяющая, является ли символ символом в нижнем регистре. Character.isUpperCase(символ ) – булевская функция, проверяющая, является ли символ символом в верхнем регистре. Character.isWhitespace(символ ) – булевская функция, проверяющая, является ли символ “пробелом в широком смысле” – пробелом, символом табуляции, перехода на новую строку и т.д. Для того чтобы сделать работу с многочисленными присваиваниями более эффективной, используются классы StringBuffer и StringBuilder. Они особенно удобны в тех случаях, когда требуется проводить изменения внутри одной и той же строки (убирать или вставлять символы, менять их местами, заменять одни на другие). Изменение значений переменных этого класса не приводит к созданию мусора, но несколько медленнее, чем при работе с переменными типа String. Класс StringBuffer рекомендуется использовать в тех случаях, когда используются потоки (threads) – он, в отличие от классов String и StringBuilder, обеспечивает синхронизацию строк. Класс StringBuilder, введённый начиная с JDK 1.5, полностью ему подобен, но синхронизации не поддерживает. Зато обеспечивает большую скорость работы со строками (что обычно бывает важно только в лексических анализаторах). К сожалению, совместимости по присваиванию между переменными этих классов нет, как нет и возможности преобразования этих типов. Но в классах StringBuffer и StringBuilder имеется метод sb.append(s), позволяющий добавлять в конец “буферизуемой” строки sb обычную строку s. Также имеется метод sb.insert(index,s), позволяющий вставлять начиная с места символа, имеющего индекс index, строку s. Пример: StringBuffer sb=new StringBuffer(); sb.append("типа StringBuffer"); sb.insert(0,"Строка "); System.out.println(sb); Кроме строк в методы append и insert можно подставлять Буферизуемые и обычные строки можно сравнивать на совпадение содержания: s1.contentEquals(sb) – булевская функция, возвращающая true в случае, когда строка s1 содержит такую же последовательность символов, как и строка sb. Работа с графи кой Вывод графики осуществляется с помощью объектов типа java.awt.Graphics. Для них определён ряд методов, описанных в следующей далее таблице. Подразумевается, что w- ширина области или фигуры, h- высота; x,y- координаты левого верхнего угла области. Для фигуры x,y- координаты левого верхнего угла прямоугольника, в который вписана фигура.
Пример метода, работающего с графикой. java.awt.Graphics g,g1; private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) { java.awt.Graphics g,g1; g=jPanel1.getGraphics(); int x1=20,x2=120,y1=20,y2=120; int x3=20,y3=20,w3=60,h3=80; int x4=30,y4=60,w4=30,h4=40; int x0=10,y0=10,w0=10,h0=10; int w1=80,h1=120; g.setClip(0,0,60,80);//границы области вывода g.drawLine(x1,y1,x2,y2);//линия g.drawOval(x3,y3,w3,h3);//эллипс g.clipRect(x4,y4,20,20);//сужение области вывода g.clearRect(x4,y4,w4,h4);//очистка прямоугольника g.setClip(0,0,200,280); //новые границы области вывода g.copyArea(x1,y1,w1,h1,60,0); g.draw3DRect(10,20,w1,h1,false); g.drawPolygon(new java.awt.Polygon(new int[]{10,10,20,40}, new int[]{10,20,30,60},4) ); } В случае попытки такого использования возникает проблема: при перерисовке графического контекста всё выведенное изображение исчезает. А перерисовка вызывается автоматически при изменении размера окна приложения, а также его восстановлении после минимизации или перекрытия другим окном. Для того, чтобы результаты вывода не пропадали, в классе приложения требуется переопределить метод paint, вызываемый при отрисовке. Код этого метода может выглядеть так: public void paint(java.awt.Graphics g){ super.paint(g); g=jPanel1.getGraphics(); ... – команды графического вывода } Правда, при изменении размера окна приложения этот код не сработает, и для панели надо будет назначить обработчик private void jPanel1ComponentResized (java.awt.event.ComponentEvent evt) { ... – команды графического вывода } То, что для изменения размера компонента следует писать отдельный обработчик, вполне разумно – ведь при восстановлении окна требуется только воссоздать изображение прежнего размера. А при изменении размера может потребоваться масштабирование выводимых элементов. Поэтому алгоритмы вывода графики в этих случаях заметно отличаются. В случае отрисовки из обработчика какого-либо события изменения графического контекста не происходит до окончания обработчика. Это принципиальная особенность работы по идеологии обработчиков событий – пока не кончится один обработчик, следующий не начинается. Для досрочной отрисовки непосредственно во время выполнения обработчика события служит вызов метода update(Graphics g). Пример: for(int i=0;i<=100;i++){ FiguresUtil.moveFigureBy(figure,dx,dy); update(g); }; При работе со статическими изображениями изложенных алгоритмов вполне достаточно. Однако при использовании движущихся элементов во многих графических системах возникает мельтешение, связанное с постоянными перерисовками. В этих случаях обычно применяют идеологию двойной буферизации: отрисовку элементов по невидимому буферному изображению, а затем показ этого изображения в качестве видимого. А то изображение, которое было видимо, при этом становится невидимым буфером. Обработка исключительных ситуацийПри работе программы выполнение операторов обычно идёт в рамках “основного ствола” - в случае, когда всё идёт как надо. Но время от времени возникают исключительные ситуации (исключения - exceptions), приводящие к ответвлению от основного ствола: деление на 0, отсутствие места на диске или попытка писать на защищенную для записи дискету, ввод с клавиатуры ошибочного символа (например, буквы вместо цифры). В отличие от катастрофических ситуаций (ошибок) такие ситуации в большинстве случаев могут быть учтены в программе, и, в частности, они не должны приводить к аварийному завершению программы. В языках программирования предыдущих поколений для решения указанных проблем приходилось использовать огромное число проверок на допустимость присваиваний и математических операций. Мало того, что эти проверки резко замедляли работу программы - не было гарантии, что они достаточны, и что во время работы программы не возникнет "вылет" из-за возникновения непредусмотренной ситуации. В Java, как и в других современных языках программирования, для таких целей предусмотрено специальное средство — обработка исключительных ситуаций. При этом используется так называемый защищенный блок программного кода try (“попытаться”), после которого следует необязательные блоки перехвата исключений catch, за которыми идёт необязательный блок очистки ресурсов finally. Про наступившую исключительную ситуацию говорят, что она возникает, либо – что она возбуждается. В английском языке для этого используется слово throw – “бросить”. Поэтому иногда в переводной литературе используют дословный перевод “бросается исключительная ситуация”. Общий случай использования защищённого блока программного кода и перехвата исключительных ситуаций выглядит так: try{ операторы0; } catch (ТипИсключения1 переменная1){ операторы1; } catch (ТипИсключения2 переменная2){ операторы2; } catch (ТипИсключенияN переменнаяN){ операторыN; } finally{ операторы; } Отметим, что при задании блоков try-catch-finally после фигурных скобок точкой с запятой “;” можно не ставить, как и всегда в случае использования фигурных скобок. Но можно и ставить - по усмотрению программиста. Если исключительных ситуаций не было, операторы0 в блоке try выполняются в обычном порядке, после чего выполняются операторы в блоке finally. Если же возникла исключительная ситуация в блоке try, выполнение блока прерывается, и идёт перехват исключений в блоках catch (“перехватить”). В качестве параметра оператора catch задаётся ссылочная переменная, имеющая тип той исключительной ситуации, которую должен перехватить данный блок. Чаще всего эту переменную называют e (по первой букве от exception). Если тип исключения совместим с типом, указанном в качестве параметра, выполняется соответствующий оператор. После чего проверок в следующих блоках catch не делается. После проверок и, возможно, перехвата исключения в блоках catch выполняются операторы блока finally. Его обычно используют для высвобождения ресурсов, и поэтому часто называют блоком "очистки ресурсов". Специальных операторов или зарезервированных конструкций для обработки в блоке finally нет. Отличие кода внутри блока finally от кода, стоящего после оператора try…finally, возникает только при наличии внутри блоков try или catch операторов break, continue, return или System.exit, то есть операторов, прерывающих работу блока программного кода. В этом случае независимо от их срабатывания или несрабатывания сначала происходит выполнение операторов блока finally, и только потом происходит переход в другое место программы в соответствии с оператором прерывания. Пример обработки исключений: void myETest(String s,double y){ double x, z; try{ x=Double.parseDouble(s); z=Math.sqrt(x/y); } catch(ArithmeticException e){ System.out.println("Деление на ноль"); } catch(NumberFormatException e){ System.out.println("Корень из отрицательного числа!"); } }; Иерархия исключительных ситуацийИсключительные ситуации в Java являются объектами. Их типы являются классами-потомками объектного типа Throwable (от throw able – “способный возбудить исключительную ситуацию”). От Throwable наследуются классы Error (“Ошибка”) и Exception (“Исключение”). Экземплярами класса Error являются непроверяемые исключительные ситуации, которые невозможно перехватить в блоках catch. Такие исключительные ситуации представляют катастрофические ошибки, после которых невозможна нормальная работа приложения. Экземплярами класса Exception и его потомков являются проверяемые исключительные ситуации. Кроме одного потомка – класса RuntimeException (и его потомков). Имя этого класса переводится как “Исключительные ситуации времени выполнения”. Классы исключительных ситуаций либо предопределены в стандартных пакетах (существуют исключительные ситуации ArithmeticException для арифметических операций в пакете java.lang, IOException в пакете java.io, и так далее), либо описываются пользователем как потомки класса Exception или его потомков. В Java типы-исключения принято именовать, оканчивая имя класса на “Exception” (“Исключение”) для проверяемых исключений или на “Error” (“Ошибка”) для непроверяемых. По правилу совместимости типов исключительная ситуация типа-потомка всегда может быть обработана как исключение прародительского типа. Поэтому порядок следования блоков catch имеет большое значение: обработчик исключения более общего типа следует писать всегда после обработчика для его типа-потомка, иначе обработчик потомка никогда не будет вызван. В приведённом выше примере вместо NumberFormatException можно поставить Exception, так как других типов исключений кроме NumberFormatException сюда доходить не может. В этом случае метод выглядит так: void myETest(String s,double y){ double x, z; try{ x=Double.parseDouble(s); z=Math.sqrt(x/y); } catch(ArithmeticException e){ System.out.println("Деление на ноль"); } catch(Exception e){ System.out.println("Корень из отрицательного числа!"); } }; Но если бы мы попробовали таким же образом заменить тип исключения в первом блоке catch, то блок для исключений типа Exception всегда перехватывал бы управление, и обработчик для NumberFormatException никогда бы не срабатывал. Пример такого неправильно написанного кода: void myETest(String s,double y){ double x, z; try{ x=Double.parseDouble(s); z=Math.sqrt(x/y); } catch(Exception e){ System.out.println("Деление на ноль"); } catch(NumberFormatException e){ System.out.println("Корень из отрицательного числа!"); } }; В таких случаях среда разработки NetBeans выдаст сообщение вида “exception java.lang.NumberFormatException has already been caught” – “исключение java.lang.NumberFormatException уже было перехвачено”. Объявление типа исключительной ситуации и оператор throwДля того чтобы задать собственный тип исключительной ситуации, требуется задать соответствующий класс. Он должен быть наследником от какого-либо класса исключительной ситуации. Например, зададим класс исключения, возникающего при неправильном вводе пользователем пароля: class WrongPasswordException extends Exception { WrongPasswordException(){ // конструктор System.out.println(”Wrong password!”); } } Создание объекта-исключения может проводиться в произвольном месте программы обычным образом, как для всех объектов, при этом возбуждения исключения не происходит. Программное возбуждение исключительной ситуации производится с помощью оператора throw, после которого указывается оператор создания объекта-исключения: throw new ТипИсключения(); Например, throw new WrongPasswordException(); Если после частичной обработки требуется повторно возбудить исключительную ситуацию e, используется вызов throw e; Для проверяемых исключений всегда требуется явное возбуждение. При возбуждении исключения во время выполнения какого-либо метода прерывается основной ход программы, и идёт процесс обработки исключения. Его иногда называют “всплыванием” исключения по аналогии со всплыванием пузырька. Если в методе исключение данного типа не перехватывается, исполняется соответствующий блок finally, если он есть, и всплывание продолжается – происходит выход из текущего метода в метод более высокого уровня. Соответственно, исключение начинает обрабатываться на уровне этого метода. Если оно не перехватывается, происходит выход на ещё более высокий уровень, и так далее. Если в методе исключение данного типа перехватывается, всплывание прекращается. Если же ни на каком уровне исключение не перехвачено – оно обрабатывается исполняющей средой. При этом выполняются действия, предусмотренные в конструкторе исключения, а затем система выводит служебную информацию о том, где и какого типа исключение возникло. Исключительная ситуация – это особая, экстремальная ситуация, не планируемая заранее. Не следует использовать исключения в качестве конструкций, на которых основаны часто повторяющиеся в программе действия . Задание : усовершенствовать класс WrongPasswordException таким образом, чтобы сообщение об ошибке появлялось в виде диалогового окна. Объявление метода, который может возбуждать исключительную ситуацию . Зарезервированное слово throwsФормат объявления функции, которая может возбуждать проверяемые исключительные ситуации, следующий: Модификаторы Тип Имя (список параметров ) throws ТипИсключения1, ТипИсключения2,…, ТипИсключения N { Тело функции } Аналогичным образом объявляется конструктор, который может возбуждать проверяемые исключительные ситуации: Модификаторы ИмяКласса (список параметров ) throws ТипИсключения1, ТипИсключения2,…, ТипИсключения N { Тело конструктора } Слово throws означает “возбуждает исключительную ситуацию” (дословно – “бросает”). Непроверяемые исключения генерируются и обрабатываются системой автоматически – как правило, приводя к завершению приложения. При этом их типы нигде не указываются, и слово throws в заголовке метода указывать не надо. Если в теле реализуемого метода используется вызов метода, который может возбуждать исключительную ситуацию, и это исключение не перехватывается, в заголовке реализуемого метода требуется указывать соответствующий тип возбуждаемого исключения. Если же это исключение порождается внутри защищённого блока программного кода, и в каком-либо блоке catch перехватывается этот тип исключения или более общий (прародительский), то указывать в заголовке тип исключения не следует. Как мы уже знаем, в теле метода может быть использован оператор throw, возбуждающий исключительную ситуацию. В этом случае если объект-исключение не перехвачен, в заголовке метода требуется указывать соответствующий тип возбуждаемого исключения. В частности, оператор throw может быть использован для порождения исключения другого типа после обработки перехваченного исключения в блоке catch. Если в родительском классе задан метод, в заголовке которого указан тип какой-либо исключительной ситуации, а в классе-потомке этот метод переопределяется, в переопределяемом методе также требуется указывать совместимый тип исключительной ситуации. При этом может быть указан либо тот же тип , либо тип исключительной ситуации – потомка от данного типа. В противном случае на этапе компиляции выдаётся диагностика ошибки. Пример: проверка пароля, введённого пользователем. class CheckPasswordDemo{ private String password=””; public String getPassword(){ return password; }; public void setPassword()(){ ...//реализация метода }; public void checkPassword(String pass) throws WrongPasswordException { if(!pass.equals(password)) throw new WrongPasswordException(); }; } При вызове метода checkPassword в случае неправильного пароля, переданного в качестве параметра, возбуждается исключительная ситуация. Следует обратить внимание, что сравнение pass!=password всегда будет давать true, так как строки сравниваются как объекты. То есть при сравнении “==” проверяется идентичность адресов в памяти, а не содержание строк. Ещё один момент, на котором следует остановиться: не используйте возбуждение исключительных ситуаций для нормального режима функционирования программы! Не используйте его вместо блока else в операторе if! Возбуждение исключения выполняется намного дольше, потребляет много ресурсов и при неудачном использовании только ухудшает программу. Например, в нашем случае имело бы смысл при неправильном вводе пароля предусмотреть возможность ещё двух попыток ввода в обычном режиме – и только после третьей неудачной попытки возбуждать исключение. Концепция работы с файлами в Java включает две составляющие:
Обеспечивает работу с именами файлов (проверка существования файла или папки с заданным именем, нахождение абсолютного пути по относительному и наоборот, проверка и установка атрибутов файлов и папок).
Обеспечивает работу не только с файлами, но и с памятью, а также различными устройствами ввода-вывода. Работа с файлами и папками с помощью объектов типа File Объекты типа File могут рассматриваться как абстракции, инкапсулирующие работу с именами файлов и папок. При этом папка рассматривается как разновидность файла, обладающая особыми атрибутами. Создание объекта типа File осуществляется с помощью конструкторов, имеющих следующие варианты: File(”Имя папки ”) File(”Имя файла ”) File(”Имя папки ”,”Имя файла ”). При этом имена могут быть как короткими (локальными), без указания пути к файлу или папке, так и длинными (абсолютными), с указанием пути. В приведённой далее таблице файлы (папки) ищутся по имени в соответствии с правилами поиска файлов в операционной системе. Для платформы Windows® вместо символа ”\” в строках, соответствующих путям, должна использоваться последовательность ”\\”. Важнейшие файловые операции, инкапсулированные классом File:
Пример работы с файловыми объектами: File f1=new File(".."); // "." , "/" , "C:/../" System.out.println("getAbsolutePath(): "+f1.getAbsolutePath()); try{ System.out.println("getCanonicalPath(): "+f1.getCanonicalPath()); } catch(Exception e){ System.out.println("Исключение от getCanonicalPath() "); }; System.out.println("exists(): "+f1.exists()); System.out.println("canRead(): "+f1.canRead()); System.out.println("canWrite(): "+f1.canWrite()); Выбор файлов и папок с помощью файлового диалога При работе с файлами в подавляющем большинстве приложений требуется вызов файлового диалога. В нашем приложении из палитры компонентов (правое верхнее окно среды разработки) перетащим на экранную форму JLabel (“Метка”) – первый компонент в палитре Swing. А затем повторим эту операцию ещё раз. В первой метке мы будем показывать имя выбранного файла, а во второй – путь к этому файлу. Для того, чтобы вызвать файловый диалог, назначим обработчик события пункту файлового меню openMenuItem (“Открыть...”) – подузел [JFrame]/menuBar[JMenu]/fileMenu[JMenu]/openMenuItem[JMenuItem]. Двойной щелчок по узлу openMenuItem приводит к автоматическому созданию заготовки обработчика openMenuItemActionPerformed и открытию редактора исходного кода. Сначала мы создаём в приложении объект типа JFileChooser, соответствующий файловому диалогу. Если в начале записать import javax.swing.*, что желательно, то в соответствующих местах кода не потребуется квалификация javax.swing. Но в данном примере импорт не сделан намеренно для того, чтобы было видно, где используются классы данного пакета. javax.swing.JFileChooser fileChooser=new javax.swing.JFileChooser(); Добавим в обработчик необходимый код: private void openMenuItemActionPerformed(java.awt.event.ActionEvent evt) { if(fileChooser.showOpenDialog(null)!= fileChooser.APPROVE_OPTION){ System.out.println("Отказались от выбора"); return; }; System.out.println("Нажали Open"); jLabel1.setText(fileChooser.getSelectedFile().getName()); jLabel2.setText(fileChooser.getSelectedFile().getParent()); } В первой строке обработчика вызывается метод fileChooser.showOpenDialog(openMenuItem). Он показывает диалог на экране. В качестве параметра должен быть задан родительский компонент – в этом качестве мы используем пункт меню openMenuItem. Сравнение с переменной класса APPROVE_OPTION позволяет выяснить, была ли выбрана кнопка Open - “Открыть”. Следует обратить внимание на характерный приём – выход из подпрограммы с помощью оператора return в случае, когда не был осуществлён выбор файла. Неопытные программисты написали бы данный фрагмент кода таким образом: if(fileChooser.showOpenDialog(openMenuItem)== fileChooser.APPROVE_OPTION){ System.out.println("Нажали Open"); jLabel1.setText(fileChooser.getSelectedFile().getName()); jLabel2.setText(fileChooser.getSelectedFile().getParent()); } else System.out.println("Отказались от выбора"); На первый взгляд принципиальной разницы нет. Но при усовершенствовании программы код, соответствующий выбору файла с помощью диалога, заметно разрастётся, а код, соответствующий отказу от выбора, останется тем же. В результате практически весь код обработчика во втором варианте кода окажется вложенным в оператор if – а это может быть несколько страниц кода. В таком коде трудно разбираться. В первом варианте оператор if обладает небольшой областью действия, что позволяет легко разобраться с относящимся к нему кодом. Можно было бы перенести строку javax.swing.JFileChooser fileChooser=new javax.swing.JFileChooser() в обработчик события. В этом случае при каждом нажатии пункта меню “Файл/Открыть…” создавался бы новый объект-диалог. Такой код был бы работоспособен. Но создание диалога является относительно долгим процессом, требующим большого количества ресурсов операционной системы. Поэтому лучше создавать в приложении глобальную переменную, которой назначен диалог. Помимо прочего это позволяет в повторно открываемом диалоге оказываться в той же папке, где происходил последний выбор файла. Перед вызовом диалога можно программно установить папку, в которой он будет открываться: File folder=…; fileChooser.setCurrentDirectory(folder); Диалог сохранения файла открывается аналогичным образом – showSaveDialog. Практически всегда при использовании файловых диалогов требуется задавать фильтр, по которому просматриваются файлы. В подавляющем большинстве случаев требуется определённое расширение в имени файла. К сожалению, компонент JFileChooser до сих пор не поддерживает встроенной возможности настраивать фильтр – для этого требуется создание специального класса. Приведём пример простейшего такого класса: package java_gui_example; import java.io.*; public class SimpleFileFilter extends javax.swing.filechooser.FileFilter { String ext; SimpleFileFilter(String ext){ this.ext=ext; } public boolean accept(File f){ if(f==null) return false; if(f.isDirectory()){ return true ; } else return (f.getName().endsWith(ext)); } /** * Описание фильтра, возникающее в строке фильтра * @see FileView#getName */ public String getDescription(){ return "Text files (.txt)"; } } Использование приведённого класса следующее: задаётся глобальная переменная javax.swing.filechooser.FileFilter fileFilter=new SimpleFileFilter(".txt"); После чего она используется в обработчике события: fileChooser.addChoosableFileFilter(fileFilter); Данный оператор добавляет фильтр в выпадающий список возможных фильтров (масок) файлового диалога. Если добавляется уже существующий фильтр, операция добавления игнорируется. По умолчанию показывается последний из добавленных фильтров. Если требуется показать другой фильтр, который уже был добавлен в список фильтров диалога, требуется вызвать оператор fileChooser.setFileFilter(fileFilter); Он делает добавленный фильтр текущим, то есть видимым при открытии диалога. Кроме стандартных диалогов открытия и сохранения файлов имеется возможность вызова диалога с дополнительными программно задаваемыми элементами - с помощью метода showCustomDialog. Также имеется возможность выбирать несколько файлов. Для этого до вызова диалога требуется задать разрешение на такой выбор: fileChooser.setMultiSelectionEnabled(true); Получение массива выбранных файлов после вызова диалога идёт следующим образом: java.io.File[] files = fileChooser.getSelectedFiles(); if (files != null && files.length > 0) { String filenames = ""; for (int i=0; i<files.length; i++) { filenames = filenames + "\n" + files[i].getPath(); } } Имеется возможность выбора папки (директории), а не файла. В этом случае следует задать режим, когда позволяется выбирать только папки fileChooser.setFileSelectionMode(fileChooser.DIRECTORIES_ONLY); либо и файлы, и папки fileChooser.setFileSelectionMode(fileChooser.FILES_AND_DIRECTORIES); Возврат в обычный режим: fileChooser.setFileSelectionMode(fileChooser.FILES_ONLY); Для того, чтобы в выбранной папке просмотреть список файлов в папке, с которой связана переменная File folder, используется вызов вида String[] filenames= folder.list(filter); Получается массив строк с короткими именами файлов. Используется переменная filter, тип которой является классом, реализующим интерфейс java.io.FilenameFilter. О том, что такое интерфейсы, будет рассказано в отдельном разделе. Пример простейшего такого класса SimpleFilenameFilter: package java_gui_example; import java.io.*; public class SimpleFilenameFilter implements FilenameFilter{ String ext; public SimpleFilenameFilter(String ext) { this.ext=ext; } public boolean accept(File dir,String fileName){ return ext==""||fileName.endsWith(ext); } } Пример с показом файлов, содержащихся в выбранной папке, в компонент jTextArea1 (текстовая область) типа JTextArea, позволяющий показывать произвольное число строк: String[] filenamesArray; File folder=fileChooser.getSelectedFile(); SimpleFilenameFilter filter=new SimpleFilenameFilter(""); String filenames = ""; if(folder.isDirectory()){ filenamesArray=folder.list(filter); for (int i=0; i<filenamesArray.length; i++) { filenames = filenames + "\n" + filenamesArray[i]; }; jTextArea1.setText(filenames); } Аналогичный пример вывода выбранных в диалоге имён в режиме “мультиселект”, позволяющем отмечать несколько файлов и/или папок: java.io.File[] files = fileChooser.getSelectedFiles(); if (files != null && files.length > 0) { String filenames = ""; for (int i=0; i<files.length; i++) { filenames = filenames + "\n" + files[i].getPath(); }; jTextArea1.setText(filenames); }; Работа с потоками ввода-вывода Стандартные классы Java, инкапсулирующие работу с данными (оболочечные классы для числовых данных, классы String и StringBuffer, массивы и т.д.), не обеспечивают поддержку чтения этих данных из файлов или запись их в файл. Вместо этого используется весьма гибкая и современная, но не очень простая технология использования потоков (Streams). Поток представляет накапливающуюся последовательность данных, поступающих из какого-то источника. Порция данных может быть считана из потока, при этом она из потока изымается. В потоке действует принцип очереди – “первым вошёл, первым вышел”. В качестве источника данных потока может быть использован как стационарный источник данных (файл, массив, строка), так и динамический – другой поток. При этом в ряде случаев выход одного потока может служить входом другого. Поток можно представить себе как трубу, через которую перекачиваются данные, причём часто в таких “трубах” происходит обработка данных. Например, поток шифрования шифрует данные, полученные на входе, и при считывании из потока передаёт их в таком виде на выход. А поток архивации сжимает по соответствующему алгоритму входные данные и передаёт их на выход. “Трубы” потоков можно соединять друг с другом – выход одного со входом другого. Для этого в качестве параметра конструктора потока задаётся имя переменной, связанной с потоком - источником данных для создаваемого потока. Буферизуемые потоки имеют хранящийся в памяти промежуточный буфер, из которого считываются выходные данные потока. Наличие такого буфера позволяет повысить производительность операций ввода-вывода, а также осуществлять дополнительные операции – устанавливать метки (маркеры) для какого-либо элемента, находящегося в буфере потока и даже делать возврат считанных элементов в поток (в пределах буфера). Абстрактный класс InputStream (“входной поток”) инкапсулирует модель входных потоков, позволяющих считывать из них данные. Абстрактный класс OutputStream (“выходной поток”) - модель выходных потоков, позволяющих записывать в них данные. Абстрактные методы этих классов реализованы в классах-потомках. Методы класса InputStream:
Все методы класса InputStream, кроме markSupported и mark, возбуждают исключение IOException – оно возникает при ошибке чтения данных. Методы класса OutputStream:
Все методы этого класса возбуждают IOException в случае ошибки записи. Не все классы потоков являются потомками InputStream/OutputStream. Для чтения строк (в виде массива символов) используются потомки абстрактного класса java.io.Reader (“читатель”). В частности, для чтения из файла – класс FileReader. Аналогично, для записи строк используются классы- потомки абстрактного класса java.io.Writer (“писатель”). В частности, для записи массива символов в файл– класс FileWriter. Имеется ещё один важный класс для работы с файлами, не входящий в иерархии InputStream/OutputStream и Reader/ Writer. Это класс RandomAccessFile (“файл с произвольным доступом”), предназначенный для чтения и записи данных в произвольном месте файла. Такой файл с точки зрения класса RandomAccessFile представляет массив байт, сохранённых на внешнем носителе. Класс RandomAccessFile не является абстрактным, поэтому можно создавать его экземпляры. Все операции чтения и записи начинаются с позиции, задаваемой файловым указателем перед началом операции. После каждой такой операции файловый указатель сдвигается к концу файла на число позиций (байт), соответствующее типу считанных данных. В случае, когда во время записи файловый указатель выходит за конец файла, размер файла автоматически увеличивается. Важнейшие методы класса RandomAccessFile:
Все методы класса RandomAccessFile, кроме getChannel(), возбуждают исключение IOException – оно возникает при ошибке записи или ошибке доступа к данным. В конструкторе класса требуется указать два параметра. Первый – имя файла или файловую переменную, второй – строку с модой доступа к файлу. Имеются следующие варианты:
Например: java.io.RandomAccessFile rf1=new java.io.RandomAccessFile("q.txt","r"); java.io.RandomAccessFile rf2=new java.io.RandomAccessFile(file,"rw"); Рассмотрим примеры, иллюстрирующие работу с потоками. Пример чтения текста из файла: File file; javax.swing.JFileChooser fileChooser=new javax.swing.JFileChooser(); javax.swing.filechooser.FileFilter fileFilter=new SimpleFileFilter(".txt"); private void openMenuItemActionPerformed(java.awt.event.ActionEvent evt) { fileChooser.addChoosableFileFilter(fileFilter); if(fileChooser.showOpenDialog(null)!=fileChooser.APPROVE_OPTION){ return;//Нажали Cancel }; file = fileChooser.getSelectedFile(); try{ InputStream fileInpStream=new FileInputStream(file); int size=fileInpStream.available(); fileInpStream.close(); char[] buff=new char[size]; Reader fileReadStream=new FileReader(file); int count=fileReadStream.read(buff); jTextArea1.setText(String.copyValueOf(buff)); javax.swing.JOptionPane.showMessageDialog(null, "Прочитано "+ count+" байт"); fileReadStream.close(); } catch(Exception e){ javax.swing.JOptionPane.showMessageDialog(null, "Ошибка чтения из файла \n"+file.getAbsolutePath()); } } Переменной fileInpStream , которая будет использована для работы потока FileInputStream , мы задаём тип InputStream . Это очень характерный приём при работе с потоками, позволяющий при необходимости заменять в дальнейшем входной поток с FileInputStream на любой другой без изменения последующего кода. После выбора имени текстового файла с помощью файлового диалога мы создаём файловый поток: fileInpStream=new FileInputStream(file); Функция fileInpStream.available()возвращает число байт, которые можно считать из файла. После её использования поток fileInpStream нам больше не нужен, и мы его закрываем. Поток FileReader не поддерживает метод available() , поэтому нам пришлось использовать поток типа FileInputStream. Массив buff используется в качестве буфера, куда мы считываем весь файл. Если требуется читать файлы большого размера, используют буфер фиксированной длины и в цикле считывают данные из файла блоками, равными длине буфера, до тех пор, пока число считанных байт не окажется меньше размера буфера. Это означает, что мы дошли до конца файла. В таком случае выполнение цикла следует прекратить. При такой организации программы нет необходимости вызывать метод available() и использовать поток типа FileInputStream. В операторе Reader fileReadStream=new FileReader(file); используется тип Reader, а не FileReader, по той же причине, что до этого мы использовали InputStream , а не FileInputStream. строки правильно работает только для ANSI Построчное чтение из файла осуществляется аналогично, но содержимое блока try следует заменить на следующий код: FileReader filReadStream=new FileReader(file); BufferedReader bufferedIn=new BufferedReader(filReadStream); String s="",tmpS=""; while((tmpS=bufferedIn.readLine())!=null) s+=tmpS+"\n"; jTextArea1.setText(s); bufferedIn.close(); Пример записи текста в файл очень похож на пример чтения, но гораздо проще, так как не требуется вводить промежуточные буферы и потоки: private void saveAsMenuItemActionPerformed(java.awt.event.ActionEvent evt) { fileChooser.addChoosableFileFilter(fileFilter); if(fileChooser.showSaveDialog(null)!=fileChooser.APPROVE_OPTION){ return;//Нажали Cancel }; file = fileChooser.getSelectedFile(); try{ Writer filWriteStream=new FileWriter(file); filWriteStream.write(jTextArea1.getText() ); filWriteStream.close(); } catch(Exception e){ javax.swing.JOptionPane.showMessageDialog(null, "Ошибка записи в файл \n"+file.getAbsolutePath()); } } - В Java массивы являются объектами , но особого рода – их объявление отличается от объявления других видов объектов. Переменная типа массив является ссылочной. Массивы Java являются динамическими - в любой момент им можно задать новую длину. - Двумерный массив представляет собой массив ячеек, каждая из которых имеет тип “одномерный массив”. Трёхмерный – массив двумерных массивов. Соответствующим образом они и задаются. - Для копирования массивов лучше использовать метод System.arraycopy. Быстрое заполнение массива одинаковыми значениями может осуществляться методом Arrays.fill - класс Arrays расположен в пакете java.util Поэлементное сравнение массивов следует выполнять с помощью метода Arrays.equals (сравнение на равенство содержимого массивов) либо Arrays.deepEquals (глубокое сравнение, то есть на равенство содержимого, а не ссылок – на произвольном уровне вложенности). Сортировка (упорядочение по значениям) массива производится методом Arrays.sort. - Коллекции (Collections) – “умные” массивы с динамически изменяемой длиной, поддерживающие ряд важных дополнительных операций по сравнению с массивами. Доступ к элементам коллекции в общем случае не может осуществляться по индексу, так как не все коллекции поддерживают индексацию элементов. Эту функцию осуществляют с помощью специального объекта – итератора (iterator). У каждой коллекции имеется свой итератор который умеет с ней работать. - Класс String инкапсулирует действия со строками. Объект типа String – строка, состоящая из произвольного числа символов, от 0 до 2*109 . Литерные константы типа String представляют собой последовательности символов, заключённые в двойные кавычки. В классе Object имеется метод toString(), обеспечивающий строковое представление любого объекта. - Строки типа String являются неизменяемыми объектами – при каждом изменении содержимого строки создаётся новый объект-строка. Для того чтобы сделать работу с многочисленными присваиваниями более эффективной, используются классы StringBuffer и StringBuilder. - Вывод графики осуществляется с помощью методов объекта типа java.awt.Graphics. Для того, чтобы результаты вывода не пропадали, в классе приложения требуется переопределить метод paint, вызываемый при отрисовке. А также обработчик события ComponentResized. - Исключительные ситуации в Java являются объектами. Их типы являются классами-потомками объектного типа Throwable .От Throwable наследуются классы Error (“Ошибка”) и Exception (“Исключение”). Экземплярами класса Error являются непроверяемые исключительные ситуации, которые невозможно перехватить в блоках catch. Экземплярами класса Exception и его потомков являются проверяемые исключительные ситуации. Кроме одного потомка – класса RuntimeException (и его потомков). - Непроверяемые исключения генерируются и обрабатываются системой автоматически – как правило, приводя к завершению приложения. При этом их типы нигде не указываются, и слово throws в заголовке метода указывать не надо. Если же в теле реализуемого метода используется вызов метода, который может возбуждать исключительную ситуацию, и это исключение не перехватывается, в заголовке реализуемого метода требуется указывать соответствующий тип возбуждаемого исключения. - Объекты типа File обеспечивают работу с именами файлов и папок (проверка существования файла или папки с заданным именем, нахождение абсолютного пути по относительному и наоборот, проверка и установка атрибутов файлов и папок). - При работе с файлами в подавляющем большинстве приложений требуется вызов файлового диалога JFileChooser (пакет javax.swing). - Потоки ввода-вывода обеспечивают работу не только с файлами, но и с памятью, а также различными устройствами ввода-вывода. Соответствующие классы расположены в пакете java.io. Абстрактный класс InputStream (“входной поток”) инкапсулирует модель входных потоков, позволяющих считывать из них данные. Абстрактный класс OutputStream (“выходной поток”) - модель выходных потоков, позволяющих записывать в них данные. - Для чтения строк (в виде массива символов) используются потомки абстрактного класса Reader (“читатель”). В частности, для чтения из файла – класс FileReader. Аналогично, для записи строк используются классы- потомки абстрактного класса Writer (“писатель”). В частности, для записи массива символов в файл– класс FileWriter. - Имеется ещё один важный класс для работы с файлами - RandomAccessFile (“файл с произвольным доступом”), предназначенный для чтения и записи данных в произвольном месте файла. Такой файл с точки зрения класса RandomAccessFile представляет массив байт, сохранённых на внешнем носителе. Класс RandomAccessFile не является абстрактным, поэтому можно создавать его экземпляры. -
Глава 8. Наследование: проблемы и альтернативы. Интерфейсы. Композиция Проблемы множественного наследовани я классов. Интерфейсы Достаточно часто требуется совмещать в объекте поведение, характерное для двух или более независимых иерархий. Но ещё чаще требуется писать единый полиморфный код для объектов из таких иерархий в случае, когда эти объекты обладают схожим поведением. Как мы знаем, за поведение объектов отвечают методы. Это значит, что в полиморфном коде требуется для объектов из разных классов вызывать методы, имеющие одинаковую сигнатуру, но разную реализацию. Унарное наследование , которое мы изучали до сих пор, и при котором у класса может быть только один прародитель, не обеспечивает такой возможности. При унарном наследовании нельзя ввести переменную, которая бы могла ссылаться на экземпляры из разных иерархий, так как она должна иметь тип, совместимый с базовыми классами этих иерархий. В C++ для решения данных проблем используется множественное наследование . Оно означает, что у класса может быть не один непосредственный прародитель, а два или более. В этом случае проблема совместимости с классами из разных иерархий решается путём создания класса, наследующего от необходимого числа классов-прародителей. Но при множественном наследовании классов возникает ряд трудно разрешимых проблем, поэтому в Java оно не поддерживается. Основные причины, по которым произошёл отказ от использования множественного наследования классов: наследование ненужных полей и методов, конфликты совпадающих имён из разных ветвей наследования. В частности, так называемое ромбовидное наследование, когда у класса A наследники B и C, а от них наследуется класс D. Поэтому класс в получает поля и методы, имеющиеся в классе A, в удвоенном количестве – один комплект по линии родителя B, другой – по линии родителя C. Конечно, в C++ имеются средства решать указанные проблемы. Например, проблемы ромбовидного наследования снимаются использованием так называемых виртуальных классов, благодаря чему при ромбовидном наследовании потомкам достаётся только один комплект членов класса A, и различения имён из разных ветвей класса с помощью имён классов. Но в результате получается заметное усложнение логики работы с классами и объектами, вызывающее логические ошибки, не отслеживаемые компилятором. Поэтому в Java используется множественное наследование с помощью интерфейсов , лишённое практически всех указанных проблем. Интерфейсы являются важнейшими элементами языков программирования, применяемых как для написания полиморфного кода, так и для межпрограммного обмена. Концепция интерфейсов Java была первоначально заимствована из языка Objective-C, но в дальнейшем развивалась и дополнялась. В настоящее время она, фактически, стала основой объектного программирования в Java, заменив концепцию множественного наследования, используемую в C++. Интерфейсы являются специальной разновидностью полностью абстрактных классов. То есть таких классов, в которых вообще нет реализованных методов - все методы абстрактные. Полей данных в них также нет, но можно задавать константы (неизменяемые переменные класса). Класс в Java должен быть наследником одного класса-родителя, и может быть наследником произвольного числа интерфейсов. Сами интерфейсы также могут наследоваться от интерфейсов, причём также с разрешением на множественное наследование. Отсутствие в интерфейсах полей данных и реализованных методов снимает почти все проблемы множественного наследования и обеспечивает изящный инструмент для написания полиморфного кода. Например, то, что все коллекции обладают методами, перечисленными в разделе о коллекциях, обеспечивается тем, что их классы являются наследниками интерфейса Collection. Аналогично, все классы итераторов являются наследниками интерфейса Iterator, и т.д. Декларация интерфейса очень похожа на декларацию класса: МодификаторВидимости interface ИмяИнтерфейса extends ИмяИнтерфейса1 , ИмяИнтерфейса2 ,..., ИмяИнтерфейса N { декларация констант ; декларация заголовков методов ; } В качестве модификатора видимости может использоваться либо слово public – общая видимость, либо модификатор должен отсутствовать, в этом случае обеспечивается видимость по умолчанию - пакетная. В списке прародителей, расширением которых является данный интерфейс, указываются прародительские интерфейсы ИмяИнтерфейса1 , ИмяИнтерфейса2 и так далее. Если список прародителей пуст, в отличие от классов, интерфейс не имеет прародителя. Для имён интерфейсов в Java нет специальных правил, за исключением того, что для них, как и для других объектных типов, имя принято начинать с заглавной буквы. Мы будем использовать для имён интерфейсов префикс I (от слова Interface), чтобы их имена легко отличать от имён классов. Объявление константы осуществляется почти так же, как в классе: МодификаторВидимости Тип ИмяКонстанты = значение; В качестве необязательного модификатора видимости может использоваться слово public. Либо модификатор должен отсутствовать - но при этом видимость также считается public , а не пакетной . Ещё одним отличием от декларации в классе является то, что при задании в интерфейсе все поля автоматически считаются окончательными (модификатор final), т.е. без права изменения, и к тому же являющимися переменными класса (модификатор static). Сами модификаторы static и final при этом ставить не надо. Декларация метода в интерфейсе осуществляется очень похоже на декларацию абстрактного метода в классе – указывается только заголовок метода: МодификаторВидимости Тип ИмяМетода(списокПараметров) throws списокИсключений; В качестве модификатора видимости, как и в предыдущем случае, может использоваться либо слово public, либо модификатор должен отсутствовать. При этом видимость также считается public , а не пакетной . В списке исключений через запятую перечисляются типы проверяемых исключений (потомки Exception), которые может возбуждать метод. Часть throws списокИсключений является необязательной. При задании в интерфейсе все методы автоматически считаются общедоступными (public) абстрактными (abstract) методами объектов. Пример задания интерфейса: package figures_pkg; public interface IScalable { public int getSize(); public void setSize(int newSize); } Класс можно наследовать от одного родительского класса и от произвольного количества интерфейсов. Но вместо слова extends используется зарезервированное слово implements - реализует . Говорят, что класс-наследник интерфейса реализует соответствующий интерфейс, так как он обязан реализовать все его методы. Это гарантирует, что объекты для любых классов, наследующих некий интерфейс, могут вызывать методы этого интерфейса. Что позволяет писать полиморфный код для объектов из разных иерархий классов. При реализации возможно добавление новых полей и методов, как и при обычном наследовании. Поэтому можно считать, что это просто один из вариантов наследования, обладающий некоторыми особенностями. Уточнение : в абстрактных классах, реализующих интерфейс, реализации методов может не быть – наследуется декларация абстрактного метода из интерфейса. Замечание: Интерфейс также может реализовывать (implements) другой интерфейс, если в том при задании типа использовался шаблон (generics , template). Но эта тема выходит за пределы данного учебного пособия. В наследнике класса, реализующего интерфейс, можно переопределить методы этого интерфейса. Повторное указание в списке родителей интерфейса, который уже был унаследован кем-то из прародителей, запрещено. Реализации у сущности типа интерфейс не бывает, как и для абстрактных классов. То есть экземпляров интерфейсов не бывает. Но, как и для абстрактных классов, можно вводить переменные типа интерфейс. Эти переменные могут ссылаться на объекты, принадлежащие классам, реализующим соответствующий интерфейс. То есть классам-наследникам этого интерфейса. Правила совместимости таковы: переменной типа интерфейс можно присваивать ссылку на объект любого класса, реализующего этот интерфейс. С помощью переменной типа интерфейс разрешается вызывать только методы, декларированные в данном интерфейсе, а не любые методы данного объекта. Если, конечно, не использовать приведение типа. Основное назначение переменных интерфейсного типа – вызов с их помощью методов, продекларированных в соответствующем интерфейсе. Если такой переменной назначена ссылка на объект, можно гарантировать, что из этого объекта разрешено вызывать эти методы, независимо от того, какому классу принадлежит объект. Ситуация очень похожа с полиморфизмом на основе виртуальных и динамических методов объектов. Но гарантией работоспособности служит не одинаковость сигнатуры методов в одной иерархии, а одинаковость сигнатуры методов в разных иерархиях – благодаря совпадению с декларацией одного и того же интерфейса. Обязательно, чтобы методы были методами объектов – полиморфизм на основе методов класса невозможен, так как для вызовов этих методов используется статическое связывание (на этапе компиляции). Отличия интерфейсов от классов . Проблемы наследования интерфейсов · Не бывает экземпляров типа интерфейс, то есть экземпляров интерфейсов, реализующих тип интерфейс. · Список элементов интерфейса может включать только методы и константы. Поля данных использовать нельзя. · Элементы интерфейса всегда имеют тип видимости public (в том числе без явного указания). Не разрешено использовать модификаторы видимости кроме public. · В интерфейсах не бывает конструкторов и деструкторов. · Методы не могут иметь модификаторов abstract (хотя и являются абстрактными по умолчанию), static, native, synchronized, final, private, protected. · Интерфейс, как и класс, наследует все методы прародителя, однако только на уровне абстракций, без реализации методов. То есть интерфейс наследует только обязательность реализации этих методов в классе, поддерживающем этот интерфейс. · Наследование через интерфейсы может быть множественным. В декларации интерфейса можно указать, что интерфейс наследуется от одного или нескольких прародительских интерфейсов. · Реализация интерфейса может быть только в классе, при этом, если он не является абстрактным, то должен реализовать все методы интерфейса. · Наследование класса от интерфейсов также может быть множественным. Кроме указанных отличий имеется ещё одно, связанное с проблемой множественного наследования. В наследуемых классом интерфейсах могут содержаться методы, имеющие одинаковую сигнатуру (хотя, возможно, и отличающиеся контрактом). Как выбрать в классе или при вызове из объекта тот или иной из этих методов? Ведь, в отличие от перегруженных методов, компилятор не сможет их различить. Такая ситуация называется конфликтом имён. Аналогичная ситуация может возникнуть и с константами, хотя, в отличие от методов, реально этого почти никогда не происходит. Для использования констант из разных интерфейсов решением является квалификация имени константы именем соответствующего интерфейса – всё аналогично разрешению конфликта имён в случае пакетов. Например: public interface I1 { Double PI=3.14; } public interface I2 { Double PI=3.1415; } class C1 implements I1,I2 { void m1(){ System.out.println(”I1.PI=”+ I1.PI); System.out.println(”I2.PI=”+ I2.PI); }; } Но для методов такой способ различения имён запрещён. Поэтому наследовать от двух и более интерфейсов методы, имеющие совпадающие сигнатуры, но различающиеся контракты, нельзя. Если сигнатуры различаются, проблем нет, и используется перегрузка. Если контракты совпадают или совместимы, в классе можно реализовать один метод, который будет реализацией для всех интерфейсов, в которых продекларирован метод с таким контрактом. Методы, объявленные в интерфейсе, могут возбуждать проверяемые исключения. Контракты методов считаются совместимыми в случае, когда контракты отличаются только типом возбуждаемых исключительных ситуаций, причём классы этих исключений лежат в одной иерархии. При этом в классе, реализующем интерфейс, метод должен быть объявлен как возбуждающий совместимый с интерфейсом тип исключения – то есть либо такой же, как в прародительском интерфейсе, либо являющийся наследником этого типа. Подведём некоторый итог: В том случае, если класс A2 на уровне абстракций ведёт себя так же, как класс A1, но кроме того обладает дополнительными особенностями поведения, следует использовать наследование. То есть считать класс A2 наследником класса A1. Действует правило “A2 есть A1” (A2 is a A1). Если для нескольких классов A1,B1,… из разных иерархий можно на уровне абстракций выделить общность поведения, следует задать интерфейс I, который описывает эти абстракции поведения. А классы задать как наследующие этот интерфейс. Таким образом, действует правило “ A1, B1,… есть I” (A1, B1,… is a I). Множественное наследование в Java может быть двух видов: - Только от интерфейсов, без наследования реализации. - От класса и от интерфейсов, с наследованием реализации от прародительского класса. Если класс-прародитель унаследовал какой-либо интерфейс, все его потомки также будут наследовать этот интерфейс. Пример на использование интерфейсов Приведём пример абстрактного класса, являющегося наследником Figure, и реализующего указанный выше интерфейс IScalable: package figures_pkg; public abstract class ScalableFigure extends Figure implements IScalable { private int size; public int getSize() { return size; } public void setSize(int size) { this.size=size; } } В качестве наследника приведём код класса Circle: package figures_pkg; import java.awt.*; public class Circle extends ScalableFigure { public Circle(Graphics g,Color bgColor, int r){ setGraphics(g); setBgColor(bgColor); setSize(r); } public Circle(Graphics g,Color bgColor){ setGraphics(g); setBgColor(bgColor); setSize( (int)Math.round(Math.random()*40) ); } public void show(){ Color oldC=getGraphics().getColor(); getGraphics().setColor(Color.BLACK); getGraphics().drawOval(getX(),getY(),getSize(),getSize()); getGraphics().setColor(oldC); } public void hide(){ Color oldC=getGraphics().getColor(); getGraphics().setColor(getBgColor()); getGraphics().drawOval(getX(),getY(),getSize(),getSize()); getGraphics().setColor(oldC); } }; Приведём пример наследования интерфейсом от интерфейса: package figures_pkg; public interface IStretchable extends IScalable{ double getAspectRatio(); void setAspectRatio(double aspectRatio); int getWidth(); void setWidth(int width); int getHeight(); void setHeight(int height); } Интерфейс IScalable описывает методы объекта, способного менять свой размер (size). При этом отношение ширины к высоте (AspectRatio – отношение сторон) у фигуры не меняется. Интерфейс IStretchable описывает методы объекта, способного менять не только свой размер, но и “растягиваться” – изменять отношение ширины к высоте (AspectRatio). К интерфейсам применимы как оператор instanceof, так и приведение типов. Например, фрагмент кода для изменения случайным образом размера объекта с помощью интерфейса IScalable может выглядеть так: Object object; ... object= Circle(...);//конструктор создаёт окружность ... if(object instanceof IScalable){ ((IScalable) object).setSize( (int)(Math.random()*80) ); } Всё очень похоже на использование класса ScalableFigure: Figure figure; ... figure = Circle(...);//конструктор создаёт окружность ... if( figure instanceof IScalable){ figure.hide(); ((IScalable)figure).setSize((int)(Math.random()*80)); figure.show(); } Но если во втором случае переменная figure имеет тип Figure, то есть связанный с ней объект обязан быть фигурой, то на переменную object такое ограничение не распространяется. Зато фигуру можно скрывать и показывать, а для переменной типа Object это можно делать только после проверки, что object является экземпляром (то есть instanceof) класса Figure. Аналогичный код можно написать в случае использования переменной типа IScalable: IScalable scalableObj; ... scalableObj = Circle(...);//конструктор создаёт окружность ... scalableObj.setSize((int)(Math.random()*80)); Заметим, что присваивание Object object= Circle(...)разрешено, так как Circle – наследник Object. Аналогично, присваивание Figure figure = Circle(...) разрешено, так как Circle – наследник Figure. И, наконец, присваивание scalableObj =Circle(...) разрешено, так как Circle – наследник IScalable. При замене в коде Circle(...) на Dot(...) мы бы получили правильный код в первых двух случаях, а вот присваивание scalableObj = Dot (...);вызвало бы ошибку компиляции, так как класс Dot не реализует интерфейс IScalable, то есть не является его потомком. Композиция как альтернатива множественному наследованию Как уже говорилось ранее, наследование относится к одному из важных аспектов, присущих объектам – поведению. Причём оно относится не к самим объектам, а к классам. Но имеется и другой аспект, присущий объектам – внутреннее устройство. При наследовании этот аспект скорее скрывается, чем подчёркивается: наследники должны быть устроены так, чтобы отличие в их устройстве не сказывалось на абстракциях их поведения. Композиция – это описание объекта как состоящего из других объектов (отношение агрегации, или включения как составной части) или находящегося с ними в отношении ассоциации (объединения независимых объектов). Если наследование характеризуется отношением “is-a” (“это есть”, “является”), то композиция характеризуется отношением “has-a” (“имеет в своём составе”, “состоит из”) и “use-a” (“использует”). Важность использования композиции связана с тем, что она позволяет объединять отдельные части в единую более сложную систему . Причём описание и испытание работоспособности отдельных частей можно делать независимо от других частей, а тем более от всей сложной системы. Таким образом, композиция – это объединение частей в единую систему. В качестве примера агрегации можно привести классический пример – автомобиль. Он состоит из корпуса, колёс, двигателя, карбюратора, топливного бака и т.д. Каждая из этих частей, в свою очередь, состоит из более простых деталей. И так далее, до того уровня, когда деталь можно считать единым целым, не включающий в себя другие объекты. Шофёр также является неотъемлемой частью автомобиля, но вряд ли можно считать, что автомобиль состоит из шофёра и других частей. Но можно говорить, что у автомобиля обязательно должен быть шофёр. Либо говорить, что шофёр использует автомобиль. Отношение объекта “автомобиль” и объекта “шофёр” гораздо слабее, чем агрегация, но всё-таки весьма сильное – это композиция в узком смысле этого слова. И, наконец, отношение автомобиля с находящимися в нём сумками или другими посторонними предметами – это ассоциация. То есть отношение независимых предметов, которые на некоторое время образовали единую систему. В таких случаях говорят, что автомобиль используют для того, чтобы отвезти предметы по нужному адресу. С точки зрения программирования на Java композиция любого вида - это наличие в объекте поля ссылочного типа. Вид композиции определяется условиями создания связанного с этой ссылочной переменной объекта и изменения этой ссылки. Если такой вспомогательный объект создаётся одновременно с главным объектом и “умирает” вместе с ним – это агрегация. В противном случае это или композиция в узком смысле слова, или ассоциация. Композиция во многих случаях может служить альтернативой множественному наследованию, причём именно в тех ситуациях, когда наследование интерфейсов “не работает”. Это бывает в случаях, когда надо унаследовать от двух или более классов их поля и методы. Приведём пример. Пусть у нас имеются классы Car (“Автомобиль”) , класс Driver (“Шофёр”) и класс Speed (“Скорость”). И пусть это совершенно независимые классы. Зададим класс MovingCar (“движущийся автомобиль”) как public class MovingCar extends Car{ Driver driver; Speed speed; … } Особенностью объектов MovingCar будет то, что они включают в себя не только особенности поведения автомобиля , но и все особенности объектов типа Driver и Speed. Например, автомобиль “знает” своего водителя: если у нас имеется объект movingCar, то movingCar.driver обеспечит доступ к объекту “водитель” (если, конечно, ссылка не равна null). В результате чего можно будет пользоваться общедоступными (и только!) методами этого объекта. То же относится к полю speed. И нам не надо строить гибридный класс-монстр, в котором от родителей Car, Driver и Speed унаследовано по механизму множественного наследования нечто вроде машино-кентавра, где шофёра скрестили с автомобилем. Или заниматься реализацией в классе-наследнике интерфейсов, описывающих взаимодействие автомобиля с шофёром и измерение/задание скорости. Но у композиции имеется заметный недостаток: для получившегося класса имеется существенное ограничение при использовании полиморфизма. Ведь он не является наследником классов Driver и Speed. Поэтому полиморфный код, написанный для объектов типа Driver и Speed, для объектов типа MovingCar работать не будет. И хотя он будет работать для соответствующих полей movingCar.driver и movingCar.speed, это не всегда помогает. Например, если объект должен помещаться в список. Тем не менее часто использование композиции является гораздо более удачным решением, чем множественное наследование. Таким образом, сочетание множественного наследования интерфейсов и композиции в подавляющем большинстве случаев является полноценной альтернативой множественному наследованию классов. - Интерфейсы используются для написания полиморфного кода для классов, лежащих в различных, никак не связанных друг с другом иерархиях. - Интерфейсы описываются аналогично абстрактным классам. Так же, как абстрактные классы, они не могут иметь экземпляров. Но, в отличие от абстрактных классов, интерфейсы не могут иметь полей данных (за исключением констант), а также реализации никаких своих методов. - Интерфейс определяет методы, которые должны быть реализованы классом-наследником этого интерфейса. - Хотя экземпляров типа интерфейс не бывает, могут существовать переменные типа интерфейс. Такая переменная - это ссылка. Она дает возможность ссылаться на объект, чей класс реализует данный интерфейс. - С помощью переменной типа интерфейс разрешается вызывать только методы, декларированные в данном интерфейсе, а не любые методы данного объекта. - Композиция – это описание объекта как состоящего из других объектов (отношение агрегации, или включения как составной части) или находящегося с ними в отношении ассоциации (объединения независимых объектов). Композиция позволяет объединять отдельные части в единую более сложную систему. - Наследование характеризуется отношением “is-a” (“это есть”, “является”), а композиция - отношением “has-a” (“имеет в своём составе”, “состоит из”) и “use-a” (“использует”). - Сочетание множественного наследования интерфейсов и композиции в подавляющем большинстве случаев является полноценной альтернативой множественному наследованию классов. Типичные ошибки:
Глава 9. Дополнительные элементы объектного программирования на языке Java Потоки выполнения (threads) и синхронизация В многозадачных операционных системах (MS Windows, Linux и др.) программу, выполняющуюся под управлением операционной системы (ОС), принято называть приложением операционной системы (application), либо, что то же, процессом (process). Обычно в ОС паралельно (или псевдопаралельно, в режиме разделения процессорного времени) выполняется большое число процессов. Для выполнения процесса на аппаратном уровне поддерживается независимое от других процессов виртуальное адресное пространство. Попытки процесса выйти за пределы адресов этого пространства отслеживаются аппаратно. Такая модель удобна для разграничения независимых программ. Однако во многих случаях она не подходит, и приходится использовать подпроцессы (subprocesses), или, более употребительное название, threads . Дословный перевод слова threads - “нити”. Иногда их называют легковесными процессами (lightweight processes), так как при прочих равных условиях они потребляют гораздо меньше ресурсов, чем процессы. Мы будем употреблять термин “потоки выполнения”, поскольку термин multithreading – работу в условиях существования нескольких потоков,- на русский язык гораздо лучше переводится как многопоточность . Следует соблюдать аккуратность, чтобы не путать threads с потоками ввода-вывода (streams). Потоки выполнения отличаются от процессов тем, что находятся в адресном пространстве своего родительского процесса. Они выполняются параллельно (псевдопараллельно), но, в отличие от процессов, легко могут обмениваться данными в пределах общего виртуального адресного пространства. То есть у них могут иметься общие переменные, в том числе – массивы и объекты. В приложении всегда имеется главный (основной) поток выполнения. Если он закрывается – закрываются все остальные пользовательские потоки приложения. Кроме них возможно создание потоков-демонов (daemons), которые могут продолжать работу и после окончания работы главного потока выполнения. Любая программа Java неявно использует потоки выполнения. В главном потоке виртуальная Java-машина (JVM) запускает метод main приложения, а также все методы, вызываемые из него. Главному потоку автоматически даётся имя ”main”. Кроме главного потока в фоновом режиме (с малым приоритетом) запускается дочерний поток, занимающийся сборкой мусора. Виртуальная Java-машина автоматически стартует при запуске на компьютере хотя бы одного приложения Java, и завершает работу в случае, когда у неё на выполнении остаются только потоки-демоны. В Java каждый поток выполнения рассматривается как объект. Но интересно то, что в Java каждый объект, даже не имеющий никакого отношения к классу Thread, может работать в условиях многопоточности, поскольку в классе Object определены методы объектов, предназначенные для взаимодействия объектов в таких условиях. Это notify(), notifyAll(), wait(), wait(timeout) –“оповестить”, “оповестить всех”,“ждать”, “ждать до истечения таймаута”. Об этих методах будет рассказано далее. Преимущества и проблемы при работе с потоками выполненияПочему бывают нужны потоки выполнения? Представьте себе программу управления спектрометром, в которой одно табло должно показывать время, прошедшее с начала измерений, второе – число импульсов со счётчика установки, третье – длину волны, для которой в данный момент идут измерения. Кроме того, в фоновом режиме должен отрисовываться получающийся после обработки данных спектр. Возможны две идеологии работы программы – последовательная и параллельная. При последовательном подходе во время выполнения алгоритмов, связанных с показом информации на экране, следует время от времени проверять, не пришли ли новые импульсы со счётчиков, и не надо ли установить спектрометр на очередную длину волны. Кроме того, во время обработки данных и отрисовки спектра следует через определённые промежутки обновлять табло времени и счётчиков. В результате код программы перестаёт быть структурным. Нарушается принцип инкапсуляции – “независимые вещи должны быть независимы”. Независимые по логике решаемой проблемы алгоритмы оказываются перемешаны друг с другом. Такой код оказывается ненадёжным и плохо модифицируемым. Однако он относительно легко отлаживается, поскольку последовательность выполнения операторов программы однозначно определена. В параллельном варианте для каждого из независимых алгоритмов запускается свой поток выполнения, и ему задаётся необходимый приоритет. В нашем случае один (или более) поток занимается измерениями. Второй – показывает время, прошедшее с начала измерений, третий – число импульсов со счётчика, четвёртый – длину волны. Пятый поток рисует спектр. Каждый из них занят своим делом и не вмешивается в другой. Связь между потоками идёт только через данные, которые первый поток поставляет остальным. Несмотря на изящество параллельной модели, у неё имеются очень неприятные недостатки. Во-первых, негарантированное время отклика. Например, при использовании потоков для анимации графических объектов может наблюдался сбой равномерности движения объекта. Во-вторых, отладка программ, работающих с использованием параллелизма, с помощью традиционных средств практически невозможна. Если при каком-то сочетании условий по времени происходит ошибка, воспроизвести её обычно оказывается невозможно: воссоздать те же временные интервалы крайне сложно. Поэтому требуется применять специальные технологии с регистрацией всех потоков данных в файлы с отладочной информацией. Для этих целей, а также для нахождения участков кода, вызывающих наибольшую трату времени и ресурсов во время выполнения приложения, используются специальные программы – профилировщики (profilers). Синхронизация по ресурсам и событиямИспользование потоков выполнения порождает целый ряд проблем не только в плане отладки программ, но и при организации взаимодействия между разными потоками. Синхронизация по ресурсам Если разные потоки получают доступ к одним и тем же данным, причём один из них или они оба меняют эти данные, для них требуется обеспечить установить разграничение доступа. Пока один поток меняет данные, второй не должен иметь права их читать или менять. Он должен дожидаться окончания доступа к данным первого потока. Говорят, что осуществляется синхронизация потоков. В Java для этих целей служит оператор synchronize (“синхронизировать”). Такой тип синхронизации называется синхронизацией по ресурсам и обеспечивает блокировку данных на то время, которое необходимо потоку для выполнения тех или иных действий. В Java такого рода блокировка осуществляется на основе концепции мониторов и в применении к Java заключается в следующем. Под монитором понимается некая управляющая конструкция, обеспечивающая монопольный доступ к объекту. Если во время выполнения синхронизованного метода объекта другой поток попробует обратиться к методам или данным этого объекта, он будет блокирован до тех пор, пока не закончится выполнение синхронизованного метода. При запуске синхронизованного метода говорят, что объект входит в монитор , при завершении – что объект выходит из монитора . При этом поток, внутри которого вызван синхронизованный метод, считается владельцем данного монитора. Имеется два способа синхронизации по ресурсам: синхронизация объекта и синхронизация метода. Синхронизация объекта obj1 (его иногда называют объектом действия ) осуществляется следующим образом: synchronized(obj1) оператор ; Например: synchronized(obj1){ ... m1(obj1); ... obj1.m2(); ... } В данном случае в качестве синхронизованного оператора выступает участок кода в фигурных скобках. Во время выполнения этого участка доступ к объекту obj1 блокируется для всех других потоков. Это означает, что пока будет выполняться вызов оператора, выполнение вызова любого синхронизованного метода или синхронизованного оператора для этого объекта будет приостановлено до окончания работы оператора. Данный способ синхронизации обычно используется для экземпляров классов, разработанных без расчёта на работу в режиме многопоточности. Второй способ синхронизации по ресурсам используется при разработке класса, рассчитанного на взаимодействия в многопоточной среде. При этом методы, критичные к атомарности операций с данными (обычно - требующие согласованное изменение нескольких полей данных), объявляются как синхронизованные с помощью модификатора synchronized: public class ИмяКласса { ... public synchronized тип метод (...){ ... } } Вызов данного метода из объекта приведёт к вхождению данного объекта в монитор. Пример: public class C1{ public synchronized void m1(){ } } C1 obj1=new C1(); obj1.m1(); Пока будет выполняться вызов obj1.m1(), доступ из других потоков к объекту obj1 будет блокирован – выполнение вызова любого синхронизованного метода или синхронизованного оператора для этого объекта будет приостановлено до окончания работы метода m1(). Если синхронизованный метод является не методом объекта, а методом класса, при вызове метода в монитор входит класс, и приостановка до окончания работы метода будет относиться ко всем вызовам синхронизованных методов данного класса. Итак, возможна синхронизация как целого метода (но только при задании его реализации в классе), так и отдельных операторов. Иногда синхронизованную область кода (метод или оператор) называют критической секцией кода. Синхронизация по событиям Кроме синхронизации по данным имеется синхронизация по событиям , когда параллельно выполняющиеся потоки приостанавливаются вплоть до наступления некоторого события, о котором им сигнализирует другой поток. Основными операциями при таком типе синхронизации являются wait (“ждать”) и notify (“оповестить”). В Java синхронизацию по событиям обеспечивают следующие методы, заданные в классе Object и наследуемые всеми остальными классами: void wait() – поток, внутри которого какой-либо объект вызвал данный метод (владелец монитора), переводится в состояние ожидания. Поток приостанавливает работу своего метода run() вплоть до поступления объекту, вызвавшему приостановку (“засыпание”) потока уведомления notify() или notifyAll(). При неправильной попытке “разбудить” поток соответствующий код компилируется, но при запуске вызывает появление исключения llegalMonitorStateException. void wait(long millis) – то же, но ожидание длится не более millis миллисекунд. void wait(long millis, int nanos) – то же, но ожидание длится не более millis миллисекунд и nanos наносекунд. void notify() – оповещение, приводящее к возобновлению работы потока, ожидающего выхода данного объекта из монитора. Если таких потоков несколько, выбирается один из них. Какой – зависит от реализации системы. void notifyAll() - оповещение, приводящее к возобновлению работы всех потоков, ожидающих выхода данного объекта из монитора. Метод wait для любого объекта obj следует использовать следующим образом - необходимо организовать цикл while, в котором следует выполнять оператор wait: synchronized(obj){ while(not условие) obj.wait(); …//выполнение операторов после того, как условие стало true } При этом не следует беспокоиться, что цикл while постоянно крутится и занимает много ресурсов процессора. Этого не происходит: после вызова obj.wait() поток, в котором находится указанный код, “засыпает” и перестаёт занимать ресурсы процессора. При этом метод wait на время “сна” потока снимает блокировку с объекта obj, задаваемую оператором synchronized(obj). Что позволяет другим потокам обращаться к объекту с вызовом obj.notify() или obj.notifyAll(). Класс Thread и интерфейс Runnable. Создание и запуск потока выполненияИмеется два способа создать класс, экземплярами которого будут потоки выполнения: унаследовать класс от java.lang.Thread либо реализовать интерфейс java.lang.Runnable. Этот интерфейс имеет декларацию единственного метода public void run(), который обеспечивает последовательность действий при работе потока. При этом класс Thread уже реализует интерфейс Runnable, но с пустой реализацией метода run().Так что при создании экземпляра Thread создаётся поток, который ничего не делает. Поэтому в потомке надо переопределить метод run(). В нём следует написать реализацию алгоритмов, которые должны выполняться в данном потоке. Отметим, что после выполнения метода run() поток прекращает существование – “умирает”. Рассмотрим первый вариант, когда мы наследуем класс от класса Thread , переопределив метод run(). Объект-поток создаётся с помощью конструктора. Имеется несколько перегруженных вариантов конструкторов, самый простой из них – с пустым списком параметров. Например, в классе Thread их заголовки выглядят так: public Thread() – конструктор по умолчанию. Подпроцесс получает имя “system”. public Thread(String name) - поток получает имя, содержащееся в строке name. Также имеется возможность создавать потоки в группах. Но в связи с тем, что данная технология устарела и не нашла широкого распространения, о группах потоков выполнения в данном учебном пособии рассказываться не будет. В классе-потомке можно вызывать конструктор по умолчанию (без параметров), либо задать свои конструкторы, используя вызовы прародительских с помощью вызова super(список параметров ). Из-за отсутствие наследования конструкторов в Java приходится в наследнике заново задавать конструкторы с той же сигнатурой, что и в классе Thread. Это является простой, но утомительной работой. Именно поэтому обычно предпочитают способ задания класса с реализацией интерфейса Runnable, о чём будет рассказано несколькими строками позже. Создание и запуск потока осуществляется следующим образом: public class T1 extends Thread{ public void run(){ ... } ... } Thread thread1= new T1(); thread1.start(); Второй вариант – использование класса, в котором реализован интерфейс java.lang.Runnable. Этот интерфейс, как уже говорилось, имеет единственный метод public void run(). Реализовав его в классе, можно создать поток с помощью перегруженного варианта конструктора Thread: public class R1 implements Runnable{ public void run(){ ... } ... } Thread thread1= Thread( new R1() ); thread1.start(); Обычно таким способом пользуются гораздо чаще, так как в разрабатываемом классе не приходится заниматься дублированием конструкторов класса Thread. Кроме того, этот способ можно применять в случае, когда уже имеется класс, принадлежащий иерархии, в которой базовым классом не является Thread или его наследник, и мы хотим использовать этот класс для работы внутри потока. В результате от этого класса мы получаем метод run(), в котором реализован нужный алгоритм, и этот метод работает внутри потока типа Thread, обеспечивающего необходимое поведение в многопоточной среде. Однако в данном случае затрудняется доступ к методам из класса Thread – требуется приведение типа. Например, чтобы вывести информацию о приоритете потока, в первом способе создания потока в методе run() надо написать оператор System.out.println("Приоритет потока="+this.getPriority()); А во втором способе приходиться это делать в несколько этапов. Во-первых, при задании класса нам следует добавить в объекты типа R1 поле thread: public class R1 implements Runnable{ public Thread thread; public void run() { System.out.println("Приоритет потока="+thread.getPriority()); } } С помощью этого поля мы будем добираться до объекта-потока. Но теперь после создания потока необходимо не забыть установить для этого поля ссылку на созданный объект-поток. Так что создание и запуск потока будет выглядеть так: R1 r1=new R1(); Thread thread1=new Thread(r1, "thread1"); r1.thread=thread1; thread1.start();//либо, что то же, r1.thread.start() Через поле thread мы можем получать доступ к потоку и всем его полям и методам в алгоритме, написанном в методе run(). Указанные выше дополнительные действия – это всего три лишних строчки программы (первая - R1 r1=new R1(); вторая - r1.thread=thread1; третья - объявление в классе R1 - public Thread thread;) . Как уже говорилось ранее, напрямую давать доступ к полю данных – дурной тон программирования. Исправить этот недостаток нашей программы просто: в дереве элементов программы окна Projects в разделе Fields (“поля”) щёлкнем правой кнопкой мыши по имени thread и выберем в появившемся всплывающем меню Refactor/Encapsulate Fields… (“Провести рефакторинг”/ “Инкапсулировать поля…”). В появившемся диалоге нажмём на кнопку “Next>” и проведём рефакторинг, подтвердив выбор в нижнем окне. В классе Thread имеется несколько перегруженных вариантов конструктора с параметром типа Runnable: public Thread(Runnable target) – с именем “system” по умолчанию. public Thread(Runnable target, String name) – с заданием имени. Также имеются варианты с заданием группы потоков. Поля и методы, заданные в классе ThreadВ классе Thread имеется ряд полей данных и методов, про которые надо знать для работы с потоками. Важнейшие константы и методы класса Thread:
Важнейшие методы объектов типа Thread:
Следует отметить, что все ведущие разработчики процессоров перешли на многоядерную технологию. При этом в одном корпусе процессора расположено несколько процессорных ядер, способных независимо выполнять вычисления, но они имеют доступ к одной и той же общей памяти. В связи с этим программирование в многопоточной среде признано наиболее перспективной моделью параллелизации программ и становится одним из важнейших направлений развития программных технологий. Модель многопоточности Java позволяет весьма элегантно реализовать преимущества многоядерных процессорных систем. Во многих случаях программы Java, написанные с использованием многопоточности, эффективно распараллеливаются автоматически на уровне виртуальной машины - без изменения не только исходного, но даже скомпилированного байт-кода приложений. Но программирование в многопоточной среде является сложным и ответственным занятием, требующим очень высокой квалификации. Многие алгоритмы, кажущиеся простыми, естественными и надёжными, в многопоточной среде оказываются неработоспособными. Из-за чего способы решения даже самых простых задач становятся необычными и запутанными, не говоря уж о проблемах, возникающих при решении сложных задач. В связи с этим автор рекомендует на начальном этапе не увлекаться многопоточностью, а только ознакомиться с данной технологией. Тем, кто всё же хочет заняться таким программированием, рекомендуется сначала прочитать главу 9 в книге Джошуа Блоха [8]. Подключение внешних библиотек DLL.“Родные” (native) методы* *- данный параграф приводится в ознакомительных целях Для прикладного программирования средств Java в подавляющем большинстве случаев хватает. Однако иногда возникает необходимость подключить к программе ряд системных вызовов. Либо обеспечить доступ к библиотекам, написанным на других языках программирования. Для таких целей в Java используются методы, объявленные с модификатором native –“родной”. Это слово означает, что при выполнении метода производится вызов “родного” для конкретной платформы двоичного кода, а не платформо-независимого байт-кода как во всех других случаях. Заголовок “родного” метода описывается в классе Java, а его реализация осуществляется на каком-либо из языков программирования, позволяющих создавать динамически подключаемые библиотеки (DLL – Dynamic Link Library под Windows, Shared Objects под UNIX-образными операционными системами). Правило для объявления и реализации таких методов носит название JNI – Java Native Interface. Объявление “родного” метода в Java имеет вид Модификаторы native ВозвращаемыйТип имяМетода (список параметров ); Тело “родного” метода не задаётся – оно является внешним и загружается в память компьютера с помощью загрузки той библиотеки, из которой этот метод должен вызываться: System.loadLibrary(“ИмяБиблиотеки”); При этом имя библиотеки задаётся без пути и без расширения. Например, если под Windows библиотека имеет имя myLib.dll, или под UNIX или Linux имеет имя myLib.so , надо указывать System.loadLibrary(“myLib”); В случае, если файла не найдено, возбуждается непроверяемая исключительная ситуация UnsatisfiedLinkError. Если требуется указать имя библиотеки с путём, применяется вызов System.load (“ИмяБиблиотекиСПутём”); Который во всём остальном абсолютно аналогичен вызову loadLibrary. После того, как библиотека загружена, с точки зрения использования в программе вызов “родного” метода ничем не отличается от вызова любого другого метода. Для создания библиотеки с методами, предназначенными для работы в качестве “родных”, обычно используется язык С++. В JDK существует утилита javah.exe, предназначенная для создания заголовков C++ из скомпилированных классов Java. Покажем, как ей пользоваться, на примере класса ClassWithNativeMethod. Зададим его в пакете нашего приложения: package java_example_pkg; public class ClassWithNativeMethod { /** Creates a new instance of ClassWithNativeMethod */ public ClassWithNativeMethod() { } public native void myNativeMethod(); } Для того, чтобы воспользоваться утилитой javah, скомпилируем проект и перейдём в папку build\classes . В ней будут располагаться папка с пакетом нашего приложения java_example_pkg и папка META-INF. В режиме командной строки выполним команду javah.exe java_example_pkg.ClassWithNativeMethod - задавать имя класса необходимо с полной квалификацией, то есть с указанием перед ним имени пакета. В результате в папке появится файл java_example_pkg_ClassWithNativeMethod.h со следующим содержимым: /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class java_example_pkg_ClassWithNativeMethod */ #ifndef _Included_java_example_pkg_ClassWithNativeMethod #define _Included_java_example_pkg_ClassWithNativeMethod #ifdef __cplusplus extern "C" { #endif /* * Class: java_example_pkg_ClassWithNativeMethod * Method: myNativeMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif Функция Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod(JNIEnv *, jobject), написанная на C++, должна обеспечивать реализацию метода myNativeMethod() в классе Java. Имя функции C++ состоит из: префикса Java, разделителя “_”, модифицированного имени пакета (знаки подчёркивания “_” заменяются на “_1”), разделителя “_”, имени класса, разделителя “_”, имени “родного” метода. Первый параметр JNIEnv * в функции C++ обеспечивает доступ “родного” кода к параметрам и объектам, передающимся из функции C++ в Java. В частности, для доступа к стеку. Второй параметр, jobject , – ссылка на экземпляр класса, в котором задан “родной” метод, для методов объекта, и jclass – ссылка на сам класс – для методов класса. В языке C++ нет ссылок, но в Java все переменные объектного типа являются ссылками. Соответственно, второй параметр отождествляется с этой переменной. Если в “родном” методе имеются параметры, то список параметров функции C++ расширяется. Например, если мы зададим метод public native int myNativeMethod(int i); то список параметров функции C++ станет (JNIEnv *, jobject, jint) А тип функции станет jint вместо void. Соответствие типов Java и C++:
В реализации метода требуется объявить переменные. Например, если мы будем вычислять квадрат переданного в метод значения и возвращать в качестве результата значение параметра, возведённое в квадрат (пример чисто учебный), код реализации функции на C++ будет выглядеть так: #include ”java_example_pkg_ClassWithNativeMethod.h” JNIEXPORT jint JNICALL Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod (JNIEnv *env, jobject obj, jint i ){ return i*i }; Отметим, что при работе со строками и массивами для получения и передачи параметров требуется использовать переменную env. Например, получение длины целого массива, переданного в переменную jintArray intArr, будет выглядеть так: jsize length=(*env)->GetArrayLength(env, intArr); Выделение памяти под переданный массив: jint *intArrRef=(*env)->GetIntArrayElements(env, intArr,0); Далее с массивом intArr можно работать как с обычным массивом C++. Высвобождение памяти из-под массива: (*env)->ReleaseIntArrayElements(env, intArr, intArrRef ,0); Имеются аналогичные функции для доступа к элементам массивов всех примитивных типов: GetBooleanArrayElements, GetByteArrayElements,…, GetDoubleArrayElements. Эти функции копируют содержимое массивов Java в новую область памяти, с которой и идёт работа в C++. Для массивов объектов имеется не только функция GetObjectArrayElement, но и SetObjectArrayElement – для получения и изменения отдельных элементов таких массивов. Строка Java jstring s преобразуется в массив символов C++ так: const char *sRef=(*env)->GetStringUTFChars(env,s,0); Её длина находится как int s_len=strlen(sRef); Высвобождается из памяти как (*env)->ReleaseStringUTFChars(env,s,sRef); - Программу, выполняющуюся под управлением операционной системы, называют процессом (process), или, что то же, приложением. У каждого процесса своё адресное пространство. Потоки выполнения (threads) отличаются от процессов тем, что выполняются в адресном пространстве своего родительского процесса. Потоки выполняются параллельно (псевдопараллельно), но, в отличие от процессов, легко могут обмениваться данными в пределах общего виртуального адресного пространства. То есть у них могут иметься общие переменные, в том числе – массивы и объекты. - В приложении всегда имеется главный (основной) поток. Если он закрывается – закрываются все остальные пользовательские потоки приложения. Кроме них возможно создание потоков-демонов, которые могут продолжать работу и после окончания работы главного потока. - Любая программа Java неявно использует потоки. В главном потоке виртуальная Java-машина (JVM) запускает метод main приложения, а также все методы, вызываемые из него. Главному потоку автоматически даётся имя ”main”. - Если разные потоки получают доступ к одним и тем же данным, причём один из них или они оба меняют эти данные, для них требуется обеспечить установить разграничение доступа. Пока один поток меняет данные, второй не должен иметь права их читать или менять. Он должен дожидаться окончания доступа к данным первого потока. Говорят, что осуществляется синхронизация потоков. В Java для этих целей служит оператор synchronize (“синхронизировать”). Иногда синхронизованную область кода (метод или оператор) называют критической секцией кода. - При запуске синхронизованного метода говорят, что объект входит в монитор , при завершении – что объект выходит из монитора . При этом поток, внутри которого вызван синхронизованный метод, считается владельцем данного монитора. - Имеется два способа синхронизации по ресурсам: синхронизация объекта и синхронизация метода. Синхронизация объекта obj1 при вызове несинхронизованного метода: synchronized(obj1) оператор ; Синхронизация метода с помощью модификатора synchronized при задании класса: public synchronized тип метод (...){...} - Кроме синхронизации по данным имеется синхронизация по событиям , когда параллельно выполняющиеся потоки приостанавливаются вплоть до наступления некоторого события, о котором им сигнализирует другой поток. Основными операциями при таком типе синхронизации являются wait (“ждать”) и notify (“оповестить”). - Имеется два способа создать класс, экземплярами которого будут потоки: унаследовать класс от java.lang.Thread либо реализовать интерфейс java.lang.Runnable. - Интерфейс java.lang.Runnable имеет декларацию единственного метода public void run(), который обеспечивает последовательность действий при работе потока. Класс Thread уже реализует интерфейс Runnable, но с пустой реализацией метода run(). Поэтому в потомке Thread надо переопределить метод run(). - При работе с большим количеством потоков требуется их объединение в группы. Такая возможность инкапсулируется классом TreadGroup (“Группа потоков”). - Для получения доступа к библиотекам, написанным на других языках программирования, в Java используются методы, объявленные с модификатором native –“родной”. При выполнении такого метода производится вызов “родного” для конкретной платформы двоичного кода, а не платформо-независимого байт-кода как во всех других случаях. Заголовок “родного” метода описывается в классе Java, а его реализация осуществляется на каком-либо из языков программирования, позволяющих создавать динамически подключаемые библиотеки.
Глава 10. Введение в сетевое программирование World Wide Web, или как ее обычно называют, WWW - это распределенная компьютерная система, основанная на гипертексте. Информация в ней хранится на компьютерах с соответствующим программным обеспечением (серверах), объединеных в глобальную сеть. Она включает в себя не только текст, но и возможность выполнения определенных действий при выборе специально отмеченных участков текста (так называемый гипертекст), а также графику, видео, звук (т. н. средства мультимедиа). Эта информация содержится в виде HTML-документов, которые могут содержать ссылки на другие документы, хранящиеся как на том же самом, так и на другом сервере. На экране компьютера гиперссылки выглядят как выделенные другим цветом и/или подчеркиванием участки текста или рисунки (графические изображения). Используя гиперссылки, называемые также гипертекстовыми связями, пользователь может автоматически связаться с соответствующим источником информации в сети и получить на экране своего компьютера документ, на который была сделана ссылка. В большинстве случаев выбор гиперсвязей производится при помощи щелчка клавишей мыши на участке текста с гиперсвязью. При этом компьютер посылает через сеть по протоколу http запрос серверу, хранящему файл с необходимым документом. Сервер, получив запрос, посылает клиенту этот файл или сообщение об отказе, если требуемый документ по тем или иным причинам недоступен. Просмотр HTML-документов осуществляется с помощью браузеров - программ просмотра WWW-документов (WWW-browsers). В настоящее время получили распространение десятки таких программ, но наиболее известными и развитыми являются Microsoft Internet Explorer, Mozilla (в том числе один из его клонов, Fire Fox), Opera, а также уже сошедший со сцены Netscape Navigator. WWW-документ, как уже отмечалось, содержит форматированный текст, графику и гиперсвязи с использованием различных ресурсов Internet. Чтобы реализовать все эти возможности, был разработан специальный компьютерный язык - HyperText Markup Language (HTML) - язык разметки гипертекста . Гипертекст (“сверхтекст”) – это текст, содержащий дополнительные возможности, в частности – гиперссылки. Существует несколько версий языка HTML. Самая современная на данный момент версия - HTML 4.01, принятая в виде рекомендации консорциума W3C (World Wide Web Consortium), отвечающего за развитие языка HTML и других WWW-технологий. XML-версия языка HTML, называемая XHTML, пока не нашла широкого распространения. Наиболее употребляемая при создании простых WWW-документов версия - HTML 3.2. Существует большое количество сред, позволяющих интерактивно создавать HTML-документы. Тем не менее, даже в этом случае полезно знать основные принципы устройства HTML-документов и имеющиеся в этом языке средства разметки. Документ, написанный на языке HTML, представляет собой текстовый файл, содержащий собственно текст, несущий информацию пользователю, и теги разметки (murkup tags). Теги представляют собой определенные последовательности символов, заключенные между знаками '<' и '>'. Программа просмотра располагает текст на экране дисплея согласно задаваемой тегами разметке, а также включает в него рисунки, хранящиеся в отдельных графических файлах, и формирует гиперсвязи с другими документами или ресурсами Internet. После тега через пробел, вплоть до закрывающего символа '>', может следовать один или несколько параметров. Файл на языке HTML имеет расширения .html или .htm. Он приобретает облик WWW-документа только тогда, когда просматривается в специальной программе просмотра - браузере. Текст в HTML может включать любую последовательность символов, за исключением следующих: < > & "Вместо них должны присутствовать комбинации < > & "Символы табуляции и перехода на новую строку считаются эквивалентными пробелу, а несколько этих символов и пробелов подряд (в любой комбинации) одному пробелу. Для вставки в текст значимого пробела используется комбинация Теги предназначены для форматирования и разметки документа. Теги бывают парные ("контейнеры") и непарные. Действие парного тега начинается с открывающего тега и заканчивается при встрече соответствующего ему закрывающего, признаком которого является символ " / ". Например: <html>Это html документ </html>Непарный тег вызывает единичное действие в том месте, где он встречается. Например,тег <br> вызывает перевод текста на новую строку. Исключением из правила, гласящего об эквивалентности любого числа пробелов, табуляций и переходов на новую строку одному пробелу, является текст внутри контейнера <pre> </pre>. Этот текст показывается так же, как он был предварительно отформатирован в обычном текстовом редакторе с использованием моноширинного шрифта, и все пробелы и переносы на новую строку являются значимыми. Однако внутри данного контейнера могут действовать другие теги разметки. Внутри тега кроме его имени могут находиться атрибуты , задающие дополнительные параметры, необходимые для действия тега. Например, тег <img src="MyFile.gif" width=100 height=40> обеспечивает показ в данном месте текста изображения из файла с именем MyFile.gif, а ширина и высота изображения задаётся 100 на 40 точек (пикселей), соответственно. При этом атрибут src, задающий имя файла, является обязательным, а width и height - необязательные (могут отсутствовать). Типичный HTML-документ имеет заголовок и тело. Начало документа отмечается тегом <html> и заканчивается тегом </html> - русскоязычная кодировка ISO 1251. <meta http-equiv="content-type" content="text/html; charset=UTF-8">- кодировка UTF-8, и т.д. Тело документа определяет видимую часть документа: <body> Это html-документ, содержащий какой-то текст. </body>На дисплее: Часть текста или участок изображения в HTML-документах может ссылаться на другой текст внутри того же самого документа, или на другой документ в Internet. Такая связь называется гипертекстовой связью (hypertext link),гиперсвязью, гипертекстовой ссылкой или гиперссылкой. Она выделяется подчёркиванием и цветом. При ссылке на документ, находящийся на другом сервере, необходимо указать адрес (URL - 'Uniform Recourses Location') этого документа: сетевой адрес сервера и путь к этому документу на сервере. Если документ находится на том же сервере, но в другой папке, достаточно указать только путь к этой папке. В подавляющем большинстве случаев документ, на который делается ссылка, находится находится на том же сервере и в той же папке. В этом случае гипертекстовая ссылка имеет вид: <a href="имя_файла">текст_ссылки</a>Тут имя_файла - имя файла (с указанием расширения), содержащего документ, на который делается ссылка, а текст_ссылки - якорь, т. е. участок текста, который будет выделен как связанный с гиперсвязью. В общем случае перед именем файла ставится URL-адрес сервера и полный путь к файлу на сервере. Язык HTML позволяет ссылаться не только на документы целиком, но и на отдельные части конкретного документа. В этом случае та часть документа, на который делается ссылка, называется меткой. То место, куда осуществляется переход называется меткой. Якорь задается в виде <a href="имя_файла#имя_метки">текст</a> ,а метка - <a name="имя_метки">участок документа</a>Тут имя_метки - произвольное имя метки, на которую должен быть переход (уникальное для данного документа), а имя_файла - имя файла ( вместе с расширением и путём), содержащего документ, на какое-либо место которого осуществляется ссылка. Надо отметить, что переход производится в начало параграфа, в котором расположенна мишень. Поэтому делению текста на параграфы при наличии гиперсвязей внутри документа надо уделять особое внимание. Параграф - осуществляет показ одной пустой строки и "переводом каретки" перед началом заключенного в контейнере текста. Внутри параграфа возможно выравнивание: <p>без выравния текста</p> <p align=left>выравнивание текста по левому краю</p> <p align=right>выравнивание текста по правому краю</p> <p align=center>выравнивание текста по центру</p> Закрывающий тег </p> необязателен. Горизонтальная раздельная черта <hr> Заголовки - используются для выводов заголовков и подзаголовков (всего 6 уровней). Значение уровня заголовка может от 1 до 6. <h1>Заголовок первого уровня</h1> <h2>Заголовок второго уровня</h2> <h3>Заголовок третьего уровня</h3> <h4>Заголовок четвёртого уровня</h4> <h5>Заголовок пятого уровня</h5> <h6>Заголовок шестого уровня</h6>
Нумерованный список - задается в виде: <ol> <li>... <li>... <li>... ... </ol>В HTML-файле: Курсовые в срок сдали следующие студенты: <ol> <li>Иванов <li>Петров <li>Сидоров </ol>На дисплее: Курсовые в срок сдали следующие студенты: 1. Иванов 2. Петров 3. Сидоров
На дисплее: o Иванов o Петров o Сидоров В HTML существуют следующие основные стили текста: <b>Жирный текст (bold)</b> <i>Наклонный текст (italics)</i> <big>Большой размер шрифта</big> <small>Маленький размер шрифта</small> Нижние индексы<sub>(subscript)</sub> Верхние индексы<sup>(superscript)</sup> Кроме того, существует показ предварительно отформатированного текста. Текст внутри контейнера <pre>...</pre> показывается моноширинным фонтом, все символы имеют одинаковую ширину, все пробелы и переходы на новую строку показываются без игнорирования. Язык HTML позволяет вставлять в текст изображения, хранимые в отдельных графических файлах. Тег вывода изображения имеет следующий вид: <img src="имя_файла" width=ширина height=высота border=ширина_рамки hspase=отступ_вертикальный vspase=отступ_горизонтальный>где имя_файла - имя графического файла (с указанием расширениея), содержащего изображение, ширина - ширина изображения, высота - высота изображения, ширина_рамки - ширина рамки вокруг изображения. Все размеры задаются в пикселах (точках экрана). Если реальные размеры изображения не совпадают с заданными в атрибутах width и height, то при показе оно масштабируется до этих размеров. Атрибут border не обязателен, но желателен, если с картинкой асоциирована гиперсвязь. Атрибуты hspase и vspase задают отступы от картинки по вертикали и горизонтали для текста или других картинок. Рекомендуется всегда указывать ширину и высоту изображения, в противном случае программа просмотра будет вынуждена перед выводом изображения документа на экран загрузить как весь текстовой файл, так и все файлы с изображениями, что занимает много времени. Если же атрибуты width и height указаны, то текст покажется сразу, а изображения будут показываться по мере "подкачивания" по сети. Кроме того, объем текстовых файлов, как правило, намного меньше, чем у графических, и поэтому они получаются гораздо быстрее. В HTML-документах можно задавать таблицы . Каждая таблица должна начинаться тегом <table> , a eсли у таблицы требуется внешняя рамка, то с параметром border ( возможны варианты border или border=ширина_рамки): <table border> и заканчиваться тегом </table> Таблицы задаются построчно, каждая строка начинается тегом <tr> и заканчиваться тегом </tr> Каждая графа (т. е. "ячейка", "клетка") в строке с данными должна начинаться тегом <td> и заканчиваться тегом </td> При этом ширина столбцов подбирается автоматически по максимальной ширине одной из клеток столбца. В таблицы так же можно вставлять гипертекстовые ссылки, произвольным образом отформатированный текст, рисунки и т. п. Общий вид таблицы: <table border> <tr> <td>...</td> <td>...</td> <td>...</td> <td>...</td> </tr> <tr> <td>...</td> <td>...</td> <td>...</td> <td>...</td> </tr> ...</table>Эта таблица содержит две строки и четыре графы (столбца). Поскольку в HTML перевод на новую строку равнозначен пробелу, а лишние пробелы игнорируются, текст в HTML-документах обычно форматируют с помощью переводов на новую строку так, чтобы не было слишком длинных строк. Поэтому приведенная выше таблица может быть записана так: <table border> <tr> <td>...</td> <td>...</td> <td>...</td> <td>...</td> </tr> <tr> <td>...</td> <td>...</td> <td>...</td> <td>...</td> </tr> </table>Вид документа при просмотре файла HTML-броусером (browser), естественно, не изменится. Окна подключаемых модулей (plug-in) задаются контейнером <object>: <object атрибуты > <param name=имя 1 value=значение 1 > <param name=имя 2 value=значение 2 > ... <param name=имя N value=значение N > Альтернативный текст, который будет виден в браузерах, не поддерживающих работу с объектами данного типа </object> В качестве таких объектов могут служить апплеты Java, мультимедийные клипы и т.п. Напомним некоторые вещи, о которых рассказывалось в первой главе. Апплет – это специализированная программа Java с ограниченными возможностями, работающая в окне WWW-документа под управлением браузера. Как правило, апплеты встраивают в HTML-документы (наиболее распространённый вид WWW-документов). Между приложениями (applications) и апплетами (applets) Java имеется принципиальное различие: приложение запускается непосредственно с компьютера пользователя и имеет доступ ко всем ресурсам компьютера наравне с любыми другими программами. Апплет же загружается из WWW с постороннего сервера, причём из-за самой идеологии WWW сайт, с которого загружен апплет, в общем случае не может быть признан надёжным. А вот сам апплет имеет возможность передавать данные на любой сервер в WWW – всё зависит от алгоритма, заложенного создателем апплета. Поэтому для того, чтобы избежать риска утечки конфиденциальной информации с компьютера пользователя или совершения враждебных действий, у апплетов убраны многие возможности, имеющиеся у приложений. Поддержка работы с апплетами осуществляется стандартной библиотекой классов (core library), расположенной в пакете java.applet для обычных апплетов, а также классом javax.swing.JApplet для апплетов, использующих компоненты Swing и/или библиотеку Sun JFC (Java Foundation Classes). Для создания обычного апплета требуется задать класс, являющийся наследником класса java.applet.Applet, который сам является наследником класса java.awt.Panel. В классе апплета требуется переопределить ряд методов: public class Applet1 extends java.applet.Applet{ public void init(){ //Инициализация перед началом работы. //Вызывается один раз после загрузки апплета //перед первым выполнением метода start() } public void start(){ //Обеспечивает основную функциональность аплета. //В первый раз вызывается после загрузки апплета //и его инициализации методом init(). //Затем вызывается каждый раз при заходе пользователя //на HTML-страницу с апплетом. } public void update(java.awt.Graphics g){ //Форсирование перерисовки апплета с выполнением кода метода } public void paint(java.awt.Graphics g){ //Исполняется каждый раз при перерисовке апплета. //Обеспечивает всю визуализацию в апплете } public String getAppletInfo(){ return "Справочная информация об апплете"; } public void stop(){ //Приостанавливает выполнение апплета. //Исполняется каждый раз сразу после того, когда пользователь //покидает HTML-страницу с апплетом. } public void destroy(){ //Обычно не требует переопределения. //Предназначен для высвобождения ресурсов, захваченных апплетами. //Исполняется через негарантированный промежуток времени //каждый раз после вызова метода stop() //перед разрушением объекта апплета сборщиком мусора. } } Кроме методов, нуждающихся в переопределении, в классе Applet имеется некоторое количество методов, позволяющих проверять и задавать его состояние во время выполнения: getSize() – возвращает размер апплета. Ширину и высоту можно получить как getSize().width и getSize().height showStatus(String s) – показ в строке статуса браузера сообщения s. AppletContext getAppletContext() – получение апплетом информации об документе, из которого был вызван апплет, а также о других апплетах того же документа. add(Component comp) – добавление компонента в апплет. AudioClip getAudioClip(URL url) - получение апплетом аудиоклипа по заданному WWW-адресу url. Создаётся объект Java, ссылающийся на данный аудиоклип. URL getDocumentBase() - получение апплетом адреса WWW-документа, из которого был вызван апплет. Имеется большое количество других методов для работы с апплетами, большинство которых унаследовано от класса Panel. Ряд примеров апплетов с исходными кодами приведён в JDK в папке demo/applets. Пример апплета: import java.awt.*; public class Applet1 extends java.applet.Applet{ public void paint(java.awt.Graphics g){ g.setColor(Color.green); g.fillRect(0,0,getSize().width - 1, getSize().height - 1); g.setColor(Color.black); g.drawString("Вас приветствует апплет!",20,20); this.showStatus("Это пример апплета"); } } Пример HTML-документа, в который встроен апплет: <html> <body> Это пример апплета<p> <object codebase="." code="Applet1.class" width=200 height=150 > Альтернативный текст, который будет виден в браузерах, не поддерживающих работу с апплетами </object> Если данный HTML-документ имеет имя example.html, то для запуска апплета следует расположить файл Applet1.class в той же папке, что и example.html. После чего открыть в браузере файл example.html. Обычно проще всего это сделать двойным щелчком мыши по имени файла в окне навигации по файлам и папкам. Если в открывшемся окне браузера апплет не будет показан, его можно просмотреть в программе appletviewer. Для этого надо перейти в папку с файлами example.html и Applet1.class, и запустить appletviewer с параметром example.html. Например, для Windows® в командной строке надо набрать appletviewer.exe example.html Замечание: на домашнем компьютере с Windows XP SP1 и браузером MS Internet Explorer 6.0 автору не удалось запустить ни одного апплета, в том числе из примеров JDK. Хотя поддержка Java была включена, и автор пытался менять самые разные установки системы. Но в appletviewer всё работало. Это является хорошей иллюстрацией того, почему крупные фирмы предпочитают при работе в сетях использовать не апплеты, а заниматься обработкой со стороны сервера и отсылать результаты на компьютер клиента в готовом виде. Ведь у клиентов, работающих в WWW, могут быть компьютеры с различными версиями JDK, операционными системами и браузерами. И гарантировать работоспособность одного и того же апплета в таком разнообразном окружении практически невозможно. Если же обработка идёт со стороны сервера, всё упрощается. Со стороны клиента нужен только браузер, работающий со ставшими совершенно стандартными языками HTML и JavaScript (после версии HTML 4.01 и JavaScript 1.3 они перестали изменяться). Браузер передаёт запрос с клиентского компьютера на сервер, и там формируется новый HTML-документ для клиентского компьютера, при необходимости – со сформированными на сервере графическими файлами. Такая идеология позволяет при необходимости усовершенствовать систему незаметно для пользователей и с сохранением полной совместимости. Например, новая версия JVM устанавливается на сервере, и проводятся все необходимые настройки и тестирования. После чего вместо старой версии системы наружу показывается новая. Напомним что сервлеты – это приложения Java , запускаемые со стороны сервера. Они имеют возможности доступа к файловой системе и другим ресурсам сервера через набор управляющих конструкций, предопределённых в рамках пакета javax.servlet и технологии JSP. Технология JSP заключается в наличии дополнительных конструкций в HTML- или XML-документах, которые позволяют осуществлять вызовы сценариев (“скриптов”), написанных на языке Java. В результате удаётся очень просто и удобно осуществлять обработку данных или элементов документа, и внедрять в нужные места документа результаты обработки. Сценарии Java перед первым выполнением автоматически компилируются на стороне сервера, поэтому выполняемый код выполняется достаточно быстро. Но, конечно, это требует, чтобы была установлена соответствующая Java-машина. Для дальнейшей работы требуется, чтобы на компьютере кроме JDK был установлен NetBeans Enterprise Pack и входящие в состав дистрибутива пакет j2EE, а также Bundled Tomcat Server. Рассмотрим пример приложения, работающего с использованием сервлетов. Исходный код сервлета, выдающего на клиентском компьютере сообщение “Hello!”: import java.io.*;import javax.servlet.*;import javax.servlet.http.*; public class Hello extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello!</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>Hello!</h1>"); out.println("</body>"); out.println("</html>"); }}Пример файла, из которого вызывается данный сервлет: <html> <head> <title>Servlet example</title> </head> <body bgcolor="#FFFFFF"> <a href="servlet/Hello">Execute servlet</a> </body> </html> Работающие варианты сервлетов и их исходные коды можно посмотреть, открыв примеры: File/New Project…/ Samples/J2EE1.4/Web /Tomcat Servlet Example Пример разработан организацией The Apache Software Foundation (http://www.apache.org/), и его использование должно соответствовать лицензии, выложенной по адресу http://www.apache.org/licenses/LICENSE-2.0 . Открытие примера с сервлетами Элементы клиентского экрана запущенного приложения с сервлетами При нажатии на гиперссылку Execute (“Выполнить”) соответствующий сервлет выполняется, и на экране показывается сформированный им HTML-документ. Первым идёт пример Hello World. Примерное содержимое исходного кода сервлета можно увидеть, перейдя по гиперссылке Source в HTML-документ с изображением исходного кода. Но это не настоящий код, а лишь HTML-документ!- Его исходный код можно увидеть, сделав двойной щелчок в окне Projects… по узлу Web Pages/helloworld.html. А вот настоящий исходный код можно увидеть, сделав двойной щелчок в том же окне по узлу Source Packages/<default package>/HelloWorldExample.java То же относится к другим примерам.
Исходный код примера Hello World Технология JSP – Java Server Pages Одним из важнейших видов серверных программ являются приложения, использующие технологию JSP – Java Server Pages. Несмотря на то, что она опирается использование сервлетов, JSP является самостоятельной технологией. Идеология JSP основана на внедрение в HTML-документы специальных конструкций, позволяющих со стороны сервера выполнять программную обработку данных и выводить в соответствующее место документа результат. В качестве примеров в NetBeans Enterprise Pack приведены примеры Tomcat JSP Example и JSTL Example. Отметим, что Tomcat – название программного сервера, варианта сервера apache, который автоматически конфигурируется и запускается при выполнении примеров, а example означает “пример”. Большим достоинством среды NetBeans является то, что оригиналы всех примеров, открываемых в NetBeans, остаются в неприкосновенности – автоматически создаются копии примеров. Поэтому даже если вы внесёте исправления в исходный код проекта с примером, это не повлечёт изменений в новом проекте с таким же примером. Для правильной работы серверных примеров требуется, чтобы на компьютере была установлена работа с Интернет. Реально выходить в Интернет не надо, идёт соединение http://localhost:8084//. Но после запуска другого серверного приложения идёт соединение по тому же адресу, поэтому документ берётся из буфера – и показывается документ, созданный предыдущим приложением. Для показа правильного документа требуется нажать в браузере кнопку “обновить”, и в случае автономной работы в появившемся диалоге, предлагающем выбрать режим работы, выбрать “Подключиться”. Реального соединения с Интернет для адреса http://localhost:8084// не будет – все коммуникации станут проходить локально. Первый из примеров иллюстрирует базовые конструкции JSP, его можно просмотреть, создав проект File/New Project…/ Samples/J2EE1.4/Web /Tomcat JSP Example. Второй пример – надстройка над JSP, специальный набор тегов JSP, разработанный группой экспертов для облегчения разработки серверных приложений. Пример можно просмотреть, создав проект File/New Project…/ Samples/J2EE1.4/Web / JSTL Example. Оба этих примера также разработаны организацией The Apache Software Foundation (http://www.apache.org/), и их использование также должно соответствовать лицензии http://www.apache.org/licenses/LICENSE-2.0 . Порядок примеров в мастере создания приложений прямо противоположный рассматриваемому нами - сначала предлагается использовать JSTL как наиболее развитое средство, затем – JSP как средство более низкого уровня, и только затем Servlet – как средство ещё более низкого уровня. Мы используем при рассмотрении обратный порядок, так как JSP использует сервлеты, а JSTL является надстройкой над JSP. Рассмотрим подробнее первый пример. Первая страница запущенного примера JSP Как и в предыдущем случае, при нажатии на гиперссылку “Execute” выполняется соответствующий пример – в данном случае запускается страница JSP. А при нажатии гиперссылки Source показывается HTML-страница с примерным видом исходного кода. Страницы JSP представляют обычные HTML-документы, но имеющие расширение имени файла .jsp , а не .html или .htm . Это – заготовка HTML-документа, который будет показан пользователю-клиенту. При создании клиентского HTML-документа в этой заготовке выражения в фигурных скобках после знака доллара вычисляются, а в HTML-документ подставляется строковое представление результата. Например, выражение вида ${1 + 2} выдаст в соответствующее место документа символ 3. Последовательность \$ означает, что выражение ${…} не вычисляется, а рассматривается как строка. Первый пример JSP - вычисление выражений Имеется ряд встроенных в JSP тегов (объектов Java):
Данные объекты применяются в виде <%@ имяОбъекта параметр1=значение1 параметр2=значение2 ... %>Пример их использования: <%@ page session=true import=”java.util.*” %>Имеется возможность задания сценариев (интерпретируемого кода) с помощью специальных определений вида <%@ код %>Где код – сценарий Java для работы с документом или вычислений. Например, <% for(int i=0; i<table.getEntries().getRows(); i++) { cal.Entry entr = table.getEntries().getEntry(i); %> В частности, разрешается задавать собственные теги вида <%@ имяБиблиотеки prefix="имяОбъекта " uri="путь к библиотеке " %>После чего разрешается использовать теги вида < имяОбъекта :оператор > Так делается в примере использования оператора out в JSTL Example. Используется пользовательский объект c (сокращение от customer – “покупатель”), задаваемый как prefix="c" , и оператор out, заданный в библиотеке taglib по адресу uri="http://java.sun.com/jsp/jstl/core": <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>Отметим, что соответствующие библиотеки уже добавлены в проект. Для примера out это пакет org.apache.taglibs.standard.tag.el.core . После запуска данного приложения в появившемся документе в списке Examples это первый пример - General Purpose Tags. При переходе по данной гиперссылке мы получаем пример General-Purpose Tags Examples <out> При нажатии на “шестерёнки” получаем результат работы программы: Пример использования оператора c:out Соответствующий фрагмент исходного кода этого JSP-документа выглядит так: <table border="1"> <c:forEach var="customer" items="${customers}"> <tr> <td><c:out value="${customer.lastName}"/></td> <td><c:out value="${customer.phoneHome}" default="no home phone specified"/></td> <td> <c:out value="${customer.phoneCell}" escapeXml="false"> <font color="red">no cell phone specified</font> </c:out> </td> </tr> </c:forEach> </table> В этом примере используется объект customer – “покупатель”, заданный в файле Customer.java, расположенном в пакете org.apache.taglibs.standard.examples.beans . То есть самом первом в Source Packages пакете примера. А также объект customers – “покупатели”, заданный в файле Customers.java, расположенном в том же пакете. В классе customer заданы поля lastName, phoneHome, phoneCell и другие. А также ряд методов, которые также можно вызывать в сценарии. С помощью оператора forEach (заданного аналогично оператору out) осуществляется перебор всех объектов customer, агрегированных в объект customers - список покупателей. А с помощью тега c:out осуществляется вывод необходимой информации в документ. В JSP имеется огромное количество возможностей. Это тема для отдельной книги. В данном учебном пособии данная технология затронута совсем немного – просто в порядке информирования о её существовании и некоторых возможностях. Точно так же, для программирования в локальных и глобальных компьютерных сетях в пакете java.net имеется огромное количество средств разного уровня, описание которых требует отдельной книги. Это и Web-адресация (классы URL, HttpURLConnection, URI, JarURLConnection, URLClassLoader), и IP-адресация (классы InetAddress, InetAddress4, InetAddress6, NetworkInterface ), и управление соединениями через сокеты (классы Socket, SocketAddress, InetSocketAddress, ServerSocket, SocketPermission). Классы NetPermission, Authentificator и PasswordAuthentification обеспечивают поддержку авторизации (запрос и обработку имени и пароля). Кроме упомянутых возможностей пакета java.net имеется дистрибутив j2me, в котором поставляются средства разработки программного обеспечения для “тонких” аппаратных клиентов – то есть таких, которые обладают малыми по сравнению с персональными компьютерами ресурсами. В первую очередь - для сотовых телефонов и наладонных компьютеров. Это также тема для отдельной книги. Не менее важной и объёмной темой является сетевое программирование баз данных. Так что желающих освоить даже основы сетевых возможностей Java ждёт весьма длительная работа. - HTML - язык разметки гипертекста. Гипертекст (“сверхтекст”) – это текст, содержащий дополнительные возможности, в частности – гиперссылки. Документ, написанный на языке HTML, представляет собой текстовый файл, содержащий собственно текст, несущий информацию пользователю, и теги разметки (murkup tags). - Теги представляют собой определенные последовательности символов, заключенные между знаками '<' и '>'. Они предназначены для форматирования и разметки документа. Теги бывают парные ("контейнеры") и непарные. Действие парного тега начинается с открывающего тега и заканчивается при встрече соответствующего ему закрывающего, признаком которого является символ " / ". - При ссылке на документ, находящийся на другом сервере, необходимо указать адрес (URL - 'Uniform Recourses Location') этого документа: сетевой адрес сервера и путь к этому документу на сервере. Если документ находится на том же сервере, но в другой папке, достаточно указать только путь к этой папке. Гипертекстовая ссылка имеет вид <a href="имя_файла">текст_ссылки</a> - Для создания обычного апплета требуется задать класс, являющийся наследником класса java.applet.Applet, а для апплетов, использующих компоненты Swing и/или библиотеку Sun JFC (Java Foundation Classes) - наследником класса javax.swing.JApplet. - В классе апплета требуется переопределить ряд методов - init, start, update, paint, getAppletInfo, stop, destroy. - Сервлеты – это приложения Java , запускаемые со стороны сервера. Они имеют возможности доступа к файловой системе и другим ресурсам сервера через набор управляющих конструкций, предопределённых в рамках пакета javax.servlet и технологии JSP. - Технология JSP заключается в наличии дополнительных конструкций в HTML- или XML-документах, которые позволяют осуществлять вызовы сценариев (“скриптов”), написанных на языке Java. В результате удаётся очень просто и удобно осуществлять обработку данных или элементов документа, и внедрять в нужные места документа результаты обработки.
Глава 11. Встроенные классы Начиная с jdk 1.1 в язык Java были введены новые возможности для работы с классами, позволяющие реализовать дополнительные возможности инкапсуляции и композиции – так называемые “встроенные классы”. Они делятся на несколько категорий:
Вложенные (nested) классы и интерфейсы Вложенный класс задаётся во внешнем классе так: class ИмяВнешнегоКласса { тело внешнего класса static class ИмяВложенногоКласса { тело вложенного класса } продолжение тела внешнего класса } Экземпляры вложенного класса, а также методы класса и поля класса получают в имени квалификатор – имя класса верхнего уровня. Например, доступ к полю идёт как ИмяВнешнегоКласса.ИмяВложенногоКласса.имяПоля , а обращение к методу класса – как ИмяВнешнегоКласса.ИмяВложенногоКласса.имяМетода(список параметров) . Пусть у нас имя внешнего класса C1, а вложенного C_nested. Тогда создание экземпляра вложенного класса может идти, например так: C1.C_nested obj=new C1.C_nested(); Особенностью использования вложенных классов является то, что во внешнем классе могут быть поля, имеющие тип вложенного класса. При этом для данного случая квалификацию именем внешнего класса использовать не надо. Отметим, что в этом случае применяется то же правило, что и при доступе к обычным полям или методам, заданным в классе. Пример: class C1{ private C_nested obj1; static class C_nested { тело вложенного класса } C_nested getNested(){ return obj1; } } При компиляции для вложенных классов создаются самостоятельные классы .class, имеющие имя имяВнешнегоКласса $имяВложенногоКласса .class . Точно такое же имя выдаётся в методах объектВложенногоКласса .toString() или объектВложенногоКласса .getClass().getName(). А вот объектВложенногоКласса .getClass().getCanonicalName() возвращает имя вложенного класса через точку. Задание вложенного интерфейса аналогично заданию вложенного класса: class ИмяВнешнегоКласса { тело внешнего класса interface ИмяВложенногоИнтерфейса { объявление констант и заголовков методов } продолжение тела внешнего класса } Вложенные интерфейсы считаются имеющими модификатор static. Реализовывать вложенный интерфейс можно в постороннем классе – при этом имя интерфейса квалифицируется именем внешнего класса. Если же реализация идёт в самом внешнем классе, квалификация именем этого класса не требуется. Как правило, необходимость во вложенных классах возникает только в тех случаях, когда внешний класс служит заменой модуля процедурного языка программирования. В этом случае обычные классы приходится вкладывать во внешний класс, и они становятся вложенными. Внутренний класс задаётся так же, как вложенный, но только без модификатора static перед именем этого класса: class ИмяВнешнегоКласса { тело внешнего класса class ИмяВнутреннегоКласса { тело внутреннего класса } продолжение тела внешнего класса } Для внутренних классов экземпляры создаются через имя объекта внешнего класса , что принципиально отличает их от обычных и вложенных классов. Синтаксис таков: Сначала идёт создание экземпляра внешнего класса: ИмяВнешнегоКласса имяОбъекта = new ИмяВнешнегоКласса(параметры); Затем создаётся нужное число экземпляров внутреннего класса: ИмяВнешнегоКласса.ИмяВнутреннегоКласса имя1 = имяОбъекта .new ИмяВнутреннегоКласса(параметры); ИмяВнешнегоКласса.ИмяВнутреннегоКласса имя2 = имяОбъекта .new ИмяВнутреннегоКласса(параметры); и так далее. Достаточно часто из внутреннего класса необходимо обратиться к объекту внешнего класса. Такое обращение идёт через имя внешнего класса и ссылку this на текущий объект: ИмяВнешнегоКласса .this - это ссылка на внешний объект (его естественно назвать родительским объектом). А доступ к полю или методу внешнего объекта в этом случае, естественно, идёт так: ИмяВнешнегоКласса .this.имяПоля ИмяВнешнегоКласса .this.имяМетода(список параметров) . К сожалению, в Java, в отличие от языка JavaScript, нет зарезервированного слова parent для обращения к родительскому объекту. Будем надеяться, что в дальнейшем в java будет введён этот гораздо более читаемый и удобный способ обращения к родителю. Пример работы с внутренними классами: package java_gui_example; public class OuterClass { int a=5; public OuterClass() { } public class InnerClass{ int x=1,y=1; public class InnerClass2 { int z=0; InnerClass2(){ System.out.println("InnerClass2 object created"); }; void printParentClassNames(){ System.out.println("InnerClass.this.x="+InnerClass.this.x); System.out.println("OuterClass.this.a="+OuterClass.this.a); } } } InnerClass inner1; InnerClass.InnerClass2 inner2; public void createInner() { inner1=this.new InnerClass(); inner2=inner1.new InnerClass2(); System.out.println("inner1 name="+inner1.getClass().getName()); System.out.println("inner1 canonical name="+ inner1.getClass().getCanonicalName()); } } Если в приложении задать переменную типа OuterClass и создать соответствующий объект OuterClass outer1=new OuterClass(); то после этого можно создать объекты внутренних классов: outer1.createInner(); Доступ к внешним объектам иллюстрируется при вызове метода outer1.inner2.printParentClassNames(); Заметим, что при создании внутреннего класса в приложении, а не в реализации класса OuterClass, вместо InnerClass inner1=this.new InnerClass(); и InnerClass.InnerClass2 inner2= inner1.new InnerClass2(); придётся написать OuterClass.InnerClass inner3=outer1.new InnerClass(); OuterClass.InnerClass.InnerClass2 inner4=inner3.new InnerClass2(); Необходимость во внутренних классах обычно возникает в случаях, когда внешний класс описывает сложную систему, состоящую из частей, каждая из которых, в свою очередь, является системой, очень тесно связанной с внешней. Причём может существовать несколько экземпляров внешних систем. Для такого варианта агрегации идеология внутренних классов подходит очень хорошо. Никаких особенностей в применении локальных классов нет, за исключением того, что область существования их и их экземпляров ограничена тем блоком, в котором они заданы. Пример использования локального класса: class LocalClass1 { public LocalClass1(){ System.out.println("LocalClass1 object created"); } }; LocalClass1 local1=new LocalClass1(); Этот код можно вставить в любой метод. Например, в обработчик события нажатия на кнопку. Конечно, данный пример чисто иллюстративный. Анонимные ( anonimous) классы и обработчики событий Анонимный (безымянный) класс объявляется без задания имени класса и переменных данного безымянного типа – задаётся только конструктор класса вместе с его реализацией. У анонимного класса может быть только один экземпляр, причём он создаётся сразу при объявлении класса. Поэтому перед объявлением анонимного класса следует ставить оператор new. Анонимный класс должен быть наследником какого-либо класса или интерфейса, и соответствующий тип должен быть указан перед списком параметров конструктора. Синтаксис задания анонимного класса таков: new ИмяПрародителя (список параметров конструктора ) { тело конструктора } Как уже говорилось, анонимные классы обычно используют в обработчиках событий, причём сама необходимость в таких классах, по мнению автора, вызвана неудачной организацией в Java работы с обработчиками событий. Пример использования анонимного класса в “слушателе” события (о них речь пойдёт в следующем параграфе): addMouseMotionListener( new java.awt.event.MouseMotionAdapter(){ public void mouseDragged(java.awt.event.MouseEvent e){ System.out.println("Mouse dragged at: x="+ e.getX()+" y="+e.getY() ); } } ); Анонимные ( anonimous) классы и слушатели событий (listeners) Событие в Java (будем называть его программным событием, или, сокращённо, просто событием) – это объект, возникающий при наступлении какого-либо события в реальном мире при взаимодействии с ним компьютера (будем называть его физическим событием). Например, физическим событием может быть нажатие на клавишу клавиатуры. При наступлении некоторых физических событий возникают программные события – создаются объекты, имеющие тип, зависящий от того, какое событие наступило. Обработчики событий – подпрограммы, которые выполняют некоторый код при наступлении программного события. Например, код, который будет выполнен при нажатии пользователем на кнопку jButton1 во время работы приложения. В Java к каждому объекту, поддерживающему работу с неким событием, могут добавляться слушатели (listeners) событий этого типа – объекты-обработчики событий. Они являются экземплярами специальных классов Listeners, в которых заданы методы, реагирующие на соответствующие типы событий. Классы и интерфейсы для работы с событиями заданы в пакетах java.awt, java.awt.event и javax.swing.event. Важнейшие типы событий: В пакете java.awt: java.awt.AWTEvent – абстрактный класс, прародительский для всех классов событий. В пакете java.awt.event: ActionEvent – событие действия (как правило, нажатие). AdjustmentEvent – изменение значения в линии прокрутки (для компонентов с линией прокрутки). ComponentEvent – компонент переместился, изменил размер или видимость (visibility) -показался или был скрыт. ContainerEvent – содержимое компонента-контейнера изменилось – какой-либо компонент был в него добавлен или из него убран. FocusEvent – компонент получил или потерял фокус. HierarchyEvent – изменение положения компонента в физической иерархии (иерархии агрегации). Например, удаление родительского компонента, смена компонентом родителя (перетаскивание с одного компонента на другой), и т.п. InputEvent – произошло событие ввода. Базовый класс для классов событий ввода (KeyEvent, MouseEvent) InputMethodEvent – произошло событие ввода. Содержит информацию об обрабатываемом тексте. ItemEvent – событие, возникающее в случае, если пункт (item) был отмечен (selected) или с него была снята отметка (deselected). KeyEvent – событие нажатия на клавишу. MouseEvent – событие мыши. PaintEvent – событие отрисовки. Служит для управления очередью событий и не может быть использовано для управления отрисовкой вместо методов paint или update. TextEvent - событие, возникающее в случае, если текст в текстовом компоненте изменился. WindowEvent – окно изменило статус (открылось, закрылось, максимизировалось, минимизировалось, получило фокус, потеряло фокус). Также имеется большое количество событий в пакете javax.swing.event. Для того, чтобы программа могла обработать событие какого-то типа, в приложение требуется добавить объект event listener (“слушатель события”) соответствующего типа. Этот тип - класс, который должен реализовать интерфейс слушателя, являющийся наследником интерфейса java.util.EventListener. Имя интерфейса слушателя обычно складывается из имени события и слова Listener. Чтобы упростить реализацию интерфейсов, в Java для многих интерфейсов событий существуют так называемые адаптеры (adapters) – классы, в которых все необходимые методы интерфейсов слушателей уже реализованы в виде ничего не делающих заглушек. Так что в наследнике адаптера требуется только переопределение необходимых методов, не заботясь о реализации всех остальных. Перечислим важнейшие интерфейсы и адаптеры слушателей: ActionEvent – ActionListener. AdjustmentEvent – AdjustmentListener. ComponentEvent – ComponentListener - ComponentAdapter. ContainerEvent – ContainerListener - ContainerAdapter. FocusEvent – FocusListener - FocusAdapter. HierarchyEvent – HierarchyBoundsListener - HierarchyBoundsAdapter. InputEvent – нет интерфейсов и адаптеров. InputMethodEvent – InputMethodListener. ItemEvent – ItemListener. KeyEvent – KeyListener - KeyAdapter. MouseEvent - MouseListener - MouseAdapter. - MouseMotionListener - MouseMotionAdapter. По-английски motion – “движение”. Событие возникает при движении мыши. - MouseWheelListener-MouseWheelAdapter. По-английски wheel – “колесо”. Событие возникает при прокручивании колёсика мыши. PaintEvent – нет интерфейсов и адаптеров. TextEvent – TextListener. WindowEvent - WindowListener - WindowAdapter. - WindowFocusListener. Событие возникает при получении или потере окном фокуса. - WindowStateListener. Событие возникает при изменении состояния окна. Все компоненты Swing являются потомками javax.swing.JComponent. А в этом классе заданы методы добавления к компоненту многих из упомянутых слушателей: addComponentListener, addFocusListener и т.д. В классах компонентов, обладающих специфическими событиями, заданы методы добавления слушателей этих событий. Повторим теперь код, приведённый в предыдущем параграфе, с разъяснениями: addMouseMotionListener( new java.awt.event.MouseMotionAdapter(){ public void mouseDragged(java.awt.event.MouseEvent e){ System.out.println("Mouse dragged at: x="+ e.getX()+" y="+e.getY() ); } } ); В качестве параметра метода addMouseMotionListener выступает анонимный класс типа java.awt.event.MouseMotionAdapter, переопределяющий метод mouseDragged. В интерфейсе MouseMotionListener имеется два метода: mouseDragged(MouseEvent e); mouseMoved(MouseEvent e) Поскольку мы не переопределили заглушку “ mouseMoved ”, наш объект-обработчик событий типа MouseEvent (движение мыши порождает события именно такого типа) не будет ничего делать для обычного движения. А вот метод mouseMoved в нашем объекте-обработчике переопределён, поэтому при перетаскиваниях (когда идёт движение мыши с нажатой кнопкой) будет выводиться текст в консольное окно. Допустим, мы поместили код с добавлением слушателя в обработчик события нажатия на какую-либо кнопку (например, jButton1). После срабатывания обработчика наш компонент (главная форма приложения, если мы добавили слушателя ей), станет обрабатывать события типа MouseMotion. При этом на каждое такое событие в консольное окно будет осуществляться вывод позиции мыши, но только в том случае, если идёт перетаскивание – когда в пределах формы мы нажали кнопку мыши и не отпуская её перетаскиваем. Причём неважно, какая это кнопка (их можно при необходимости программно различить). Если мы расположим на форме панель jPanel1, и заменим вызов addMouseMotionListener (реально это вызов this.addMouseMotionListener) на jPanel1.addMouseMotionListener – слушатель расположится не в форме (объект this), а в панели. В этом случае события перетаскивания будут перехватываться только панелью. При этом важно, в какой области началось перетаскивание, но не важно, в какой области оно продолжается – события от перетаскивания, начавшегося внутри панели, будут возникать даже при перемещении курсора мыши за пределы окна приложения. Если нажать на кнопку добавления слушателя два раза – в списке слушателей станет два объекта-обработчика событий MouseEvent. Объекты из списка слушателей обрабатывают события по очереди, в порядке их добавления. Поэтому каждое событие будет обрабатываться дважды. Если добавить ещё объекты слушателей - будет обрабатываться соответствующее число раз. Конечно, в реальных ситуациях если добавляют более одного объекта-слушателя для события, они не повторяют одни и те же действия, а по-разному реагируют на это событие. Либо они являются экземплярами одного класса, но с разными значениями полей данных, либо, что чаще – экземплярами разных классов. Например, для события MouseEvent существует интерфейc MouseListener и реализующий его адаптер MouseAdapter, имеющий четыре метода: mouseClicked(MouseEvent e) mouseEntered(MouseEvent e) mouseExited(MouseEvent e) mouseReleased(MouseEvent e) Экземпляры классов-наследников MouseAdapter будут совсем по-другому обрабатывать события MouseEvent. Аналогично, можно создать экземпляр наследника MouseMotionListener, который будет реагировать не на перетаскивание, а на простое движение мыши. Обычно классы слушателей, наследующие от адаптеров, делают анонимными, совмещая декларацию, реализацию и вызов экземпляра класса. - В Java имеются встроенные классы, которые позволяют реализовать дополнительные возможности инкапсуляции и композиции. Они делятся на несколько категорий:
- Анонимный класс объявляется без задания имени класса и переменных данного безымянного типа – задаётся только конструктор класса вместе с его реализацией. У анонимного класса может быть только один экземпляр, причём он создаётся сразу при объявлении класса. Поэтому перед объявлением анонимного класса следует ставить оператор new. Безымянный класс должен быть наследником какого-либо класса или интерфейса, и соответствующий тип должен быть указан перед списком параметров конструктора. - Синтаксис задания безымянного класса таков: new ИмяПрародителя (список параметров конструктора ) { тело конструктора } - Программные события в Java – объекты, имеющие тип, зависящий от того, какое физическое событие наступило. Обработчики событий – подпрограммы, которые выполняют некоторый код при наступлении программного события. - В Java обработчики событий являются объектами – экземплярами специальных классов Listeners (“слушателей”). В этих классах заданы методы, реагирующие на соответствующий тип событий. Объекты обработчиков можно добавлять к компонентам, способным перехватывать соответствующие типы событий. - Классы и интерфейсы для работы с событиями заданы в пакетах java.awt, java.awt.event и javax.swing.event. - Классы, отвечающие за прослушивание событий, реализуют соответствующие интерфейсы - наследники интерфейса java.util.EventListener. Для того, чтобы упростить реализацию интерфейсов, в Java для многих интерфейсов событий существуют так называемые адаптеры (adapters) – классы, в которых все необходимые методы интерфейсов слушателей уже реализованы в виде ничего не делающих заглушек. Так что в наследнике адаптера требуется только переопределение необходимых методов, не заботясь о реализации всех остальных. Обычно эти классы делают анонимными, совмещая декларацию, реализацию и вызов.
Глава 1 2. Компонентное программирование Компонентная архитектура JavaBeans Компонент – это: · автономный элемент программного обеспечения, предназначенный для многократного использования, который может распространяться для использования в других программах в виде скомпилированного кода класса; · подключение к этим программам осуществляется с помощью интерфейсов; · взаимодействие с программной средой осуществляется по событиям, причём в программе, использующей компонент, можно назначать обработчики событий, на которые умеет реагировать компонент. Технология JavaBeans предоставляет возможность написания компонентного программного обеспечения на языке Java. Beans по-английски означает “зёрна” – обыгрывается происхождение названия “Java” от любимого создателями языка Java сорта кофе. Компоненты JavaBeans в литературе по языку Java часто упоминаются просто как Beans. Компонент JavaBeans может быть включен в состав более сложных (составных) компонентов, приложений, сервлетов, пакетов, модулей. Причём обычно это делается с помощью сред визуального проектирования. Компоненты JavaBeans предоставляют свои общедоступные методы и события для режима визуального проектирования. Доступ к ним возможен в том случае, когда их названия соответствуют особым шаблонам проектирования (bean design patterns). Для задания свойства требуется, чтобы существовали геттер и сеттер для этого свойства. Пример будет приведён в следующем параграфе. Компонент может быть установлен в среду разработки, в этом случае кнопки доступа к компонентам выносятся на палитру (palette) или панель инструментов (toolbox). Вы можете создать экземпляр компонента на проектируемой экранной форме в режиме Design (“дизайн”) путём выбора его кнопки на панели и перетаскивания на форму. Затем можно изменять его свойства , писать обработчики событий, включать в состав других компонентов и т. д. Компонент JavaBeans является классом Java и имеет три типа атрибутов:
При окончании работы со средой разработки состояние компонентов сохраняется в файле с помощью механизма сериализации - представления объектов Java в виде потока байтов. При последующей загрузке проекта сохранённое состояние компонентов считывается из файла. В NetBeans существует несколько способов создания компонента JavaBeans. Наиболее простым является использование мастера создания компонента. О нём будет сказано в следующем параграфе. Другой способ – создать для компонента класс BeanInfo, обеспечивающий поставку необходимой информации о компоненте. Этот класс должен реализовывать интерфейс BeanInfo, и его имя должно состоять из имени компонента и названия интерфейса. Например, для компонента MyComponent это будет MyComponentBeanInfo. Существует класс-адаптер SimpleBeanInfo для интерфейса BeanInfo. В его наследнике достаточно переписать методы, подлежащие модификации. Среда NetBeans позволяет с помощью мастера создать заготовку такого класса. Для редактирования некоторых свойств в среде визуального проектирования требуется специальный объект – редактор свойств (Property Editor). Среда NetBeans позволяет с помощью мастера создать заготовку и для такого класса. Мастер создания компонента в NetBeans Рассмотрим подробнее процесс создания собственного компонента. В NetBeans для этого необходимо выбрать в меню File/New File…/JavaBeans Objects/JavaBeans Component и нажать кнопку Next>. Создание компонента JavaBeans . Шаг 1 Далее в поле Class Name надо ввести имя компонента. В качестве примера мы введём MyBean. Затем обязательно следует выбрать пакет, в котором мы будем создавать компонент – мы выберем пакет нашего приложения. После чего следует нажать на кнопку Finish. Создание компонента JavaBean . Шаг 2 Приведём код получившейся заготовки: /* * MyBean.java * * Created on 30 Октябрь 2006 г., 23:16 */ package java_gui_example; import java.beans.*; import java.io.Serializable; /** * @author В.Монахов */ public class MyBean extends Object implements Serializable { public static final String PROP_SAMPLE_PROPERTY = "sampleProperty"; private String sampleProperty; private PropertyChangeSupport propertySupport; public MyBean() { propertySupport = new PropertyChangeSupport(this); } public String getSampleProperty() { return sampleProperty; } public void setSampleProperty(String value) { String oldValue = sampleProperty; sampleProperty = value; propertySupport.firePropertyChange(PROP_SAMPLE_PROPERTY, oldValue, sampleProperty); } public void addPropertyChangeListener(PropertyChangeListener listener) { propertySupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { propertySupport.removePropertyChangeListener(listener); } } В данном компоненте создана заготовка для строкового свойства sampleProperty. Геттер public String getSampleProperty() обеспечивает чтение значения свойства, а сеттер public void setSampleProperty(String value) обеспечивает установку нового значения. Служебный объект private PropertyChangeSupport propertySupport обеспечивает поддержку работы с обработчиком события PropertyChange. Отметим, что “property change” означает “изменение свойства”. Это событие должно возникать при каждом изменении свойств нашего компонента. Как уже говорилось, в каждом объекте, поддерживающем работу с неким событием (в нашем случае это событие PropertyChange), имеется список объектов-слушателей событий (listeners). Иногда их называют зарегистрированными слушателями. Методы с названием fireИмяСобытия (“fire” – “стрелять”, в данном случае – “выстрелить событием”) осуществляют поочерёдный вызов зарегистрированных слушателей из списка для данного события, передавая им событие на обработку. В нашем случае это метод propertySupport.firePropertyChange. Сначала он обеспечивает создание объекта-события, если значение свойства действительно изменилось, а потом поочерёдно вызывает слушателей этого события для его обработки. Методы public void addPropertyChangeListener(PropertyChangeListener listener) и public void removePropertyChangeListener(PropertyChangeListener listener) обеспечивают для компонента возможность добавления и удаления объекта слушателя - обработчика события Property Change. Если требуется создать другие свойства или обеспечить добавление и удаление обработчиков других событий, можно воспользоваться соответствующим мастером. В узле Bean Patterns (“Pattern” означает “образец” ) следует правой кнопкой мыши вызвать всплывающее меню, и выбрать Add. А затем в зависимости от того, что необходимо, выбрать один из видов свойств (Property) или событий (Event). Об этом более подробно будет говориться далее. Таким же образом удаляются свойства и события компонента. Пример создания компонента в NetBeans – панель с заголовком Задание новых свойств и событий В качестве простейшего примера визуального компонента создаем панель, у которой имеется заголовок (title). Унаследуем наш компонент от класса javax.swing.JPanel – для этого в импорте запишем import javax.swing.*; а в качестве родительского класса вместо Object напишем JPanel . С помощью рефакторинга заменим имя myBean на JTitledPanel, в узле полей Fields (а не в Bean Patterns!) поле sampleProperty на title, а константу PROP_SAMPLE_PROPERTY уберём, написав в явном виде имя свойства “title” в методе firePropertyChange. После чего в области Bean Patterns правой клавишей мыши вызовем всплывающее меню, и там вызовем пункт Rename… (“Переименовать”) для свойства sampleProperty – заменим имя на title. Это приведёт к тому, что методы getSampleProperty и setSampleProperty будут переименованы в getTitle и setTitle. Обязательно следует присвоить начальное значение полю title – в заготовке, полученной из Bean Pattern, это не делается. Мы установим private String title=”Заголовок”; Для показа заголовка необходимо импортировать классы java.awt.Graphics, java.awt.geom.Rectangle2D и переопределить в JTitledPanel.java метод paint: public void paint(Graphics g){ super.paint(g); FontMetrics fontMetrics=g.getFontMetrics(); Rectangle2D rect = fontMetrics.getStringBounds(title, g); g.drawString(title,(int)Math.round((this.getWidth()-rect.getWidth())/2), 10); } Для того, чтобы можно было пользоваться классами Graphics, FontMetrics и Rectangle2D, нам следует добавить импорт import java.awt.*; import java.awt.geom.Rectangle2D; Отметим, что можно было бы не вводить переменные fontMetrics и rect, а сразу писать в методе drawString соответствующие функции в следующем виде: g.drawString(title, (int)Math.round( ( this.getWidth() - g.getFontMetrics().getStringBounds(title,g).getWidth() )/2 ), 10); Но от этого текст программы стал бы гораздо менее читаемым. Даже несмотря на попытки отформатировать текст так, чтобы было хоть что-то понятно. Ещё одно необходимое изменение – добавление repaint() в операторе setTitle. Если этого не сделать, после изменения свойства компонент не перерисуется с вновь установленным заголовком. В результате получим следующий код компонента: /* * JTitledPanel.java * * Created on 30 Октябрь 2006 г., 23:16 */ package java_gui_example; import java.beans.*; import java.io.Serializable; import javax.swing.*; //добавлено вручную import java.awt.*; //добавлено вручную import java.awt.geom.Rectangle2D; //добавлено вручную /** * @author В.Монахов */ public class JTitledPanel extends JPanel implements Serializable { private String title="Заголовок"; //добавлено вручную private PropertyChangeSupport propertySupport; public JTitledPanel() { super(); propertySupport = new PropertyChangeSupport(this); } public String getTitle() { return title; } public void setTitle(String value) { String oldValue = title; title = value; propertySupport.firePropertyChange(”title”, oldValue, title); repaint(); //добавлено вручную } public void addPropertyChangeListener(PropertyChangeListener listener) { propertySupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener){ propertySupport.removePropertyChangeListener(listener); } public void paint(Graphics g){ //метод добавлен вручную super.paint(g); FontMetrics fontMetrics=g.getFontMetrics(); Rectangle2D rect = fontMetrics.getStringBounds(title, g); g.drawString(title,(int)Math.round((this.getWidth() - rect.getWidth())/2), 10); } } Для того, чтобы добавить наш компонент в палитру, следует открыть файл JTitledPanel.java в окне редактора исходного кода, и в меню Tools выбрать пункт Add to Palette. После чего в появившемся диалоге выбрать палитру, на которую будет добавлен компонент. Выбор палитры, на которую будет добавлен компонент Желательно выбрать Beans (чтобы не путать наши компоненты со стандартными) и нажать OK. Теперь компонент можно использовать наравне с другими. Использование созданного компонента Теперь мы можем менять текст заголовка как в редакторе свойств на этапе визуального проектирования, так и программно во время работы приложения. Мы также можем рисовать по нашей панели, и заголовок при этом будет виден, как и отрисовываемые примитивы. Например, мы можем вывести по нажатию на какую-нибудь кнопку строку “Тест”: Graphics g=jTitledPanel1.getGraphics(); FontMetrics fontMetrics=g.getFontMetrics(); Rectangle2D rect = fontMetrics.getStringBounds("Тест", g); g.drawString("Тест",10,30 ); Если мы будем усовершенствовать код нашего компонента, нет необходимости каждый раз удалять его из палитры компонентов и заново устанавливать – достаточно после внесения изменений заново скомпилировать проект (Build main project – F11). Добавление в компонент новых свойств В компонент можно добавить новое свойство. Пусть мы хотим задать свойство titleShift типа int – оно будет задавать высоту нашего заголовка., выбираем в окне Projects… для соответствующего компонента узел Bean Patterns и щелкаем по ним правой клавишей мыши. В появившемся всплывающем меню выбираем Add/Property, после чего в появившемся диалоге вводим имя и тип свойства.
Добавление в компонент свойства, слева – обычного, справа - массива Пункты Bound (“связанное свойство”) и Constrained (“стеснённое свойство”) позволяют использовать опцию “Generate Property Change Support” – без её выбора они ни на что не влияют. Свойства вида Bound – обычные свойства. При отмеченных опциях “Bound” и “Generate Property Change Support” автоматически добавляется код, генерирующий в компоненте событие PropertyChange при изменении свойства компонента. Именно таким образом была ранее создан средой NetBeans код для работы с событием PropertyChange. Например, если мы добавим целочисленное свойство titleShift (“shift”- сдвиг) вида Bound, задающее сдвиг заголовка по вертикали, в исходный код компонента добавится следующий текст: /** * Holds value of property titleShift. */ private int titleShift; /** * Getter for property titleShift. * @return Value of property titleShift. */ public int getTitleShift() { return this.titleShift; } /** * Setter for property titleShift. * @param titleShift New value of property titleShift. */ public void setTitleShift(int titleShift) { int oldTitleShift = this.titleShift; this.titleShift = titleShift; propertySupport.firePropertyChange ("titleShift", new Integer (oldTitleShift), new Integer (titleShift)); repaint();//добавлено вручную } Правда, как и в предыдущем случае, в автоматически сгенерированный код сеттера пришлось добавить оператор repaint(). Свойства вида Constrained требуют проверки задаваемого значения свойства на принадлежность к области допустимых значений. Если значение не удовлетворяет этому условию, возбуждается исключительная ситуация. При изменении таких свойств порождается событие VetoableChangeEvent. Слово Vetoable происходит от “Veto able” - способный на накладывание ограничения, накладывание вето. При задании свойств - массивов во всплывающем меню, вызываемом правой кнопкой мыши в узле Bean Patterns, следует пользоваться опцией Add/Indexed Property. Например, если мы выбрали параметры так, как указано на правом рисунке, приведённом выше, будет добавлен следующий код: /** * Holds value of property arr. */ private double[] arr; /** * Indexed getter for property arr. * @param index Index of the property. * @return Value of the property at <CODE>index</CODE>. */ public double getArr(int index) { return this.arr[index]; } /** * Getter for property arr. * @return Value of property arr. */ public double[] getArr() { return this.arr; } /** * Indexed setter for property arr. * @param index Index of the property. * @param arr New value of the property at <CODE>index</CODE>. */ public void setArr(int index, double arr) { this.arr[index] = arr; propertySupport.firePropertyChange ("arr", null, null ); } /** * Setter for property arr. * @param arr New value of property arr. */ public void setArr(double[] arr) { double[] oldArr = this.arr; this.arr = arr; propertySupport.firePropertyChange ("arr", oldArr, arr); } После добавления нового свойства следует заново скомпилировать проект (Build main project – F11). При этом, если при визуальном проектировании (Design) выделить компонент jTitledPanel1, его новые свойства появятся в окне jTitledPanel1[JTitledPanel]-Properties/ Properties сразу после компиляции проекта. Добавление в компонент новых событий Поскольку мы наследуем компонент от класса JPanel, большинство необходимых событий он уже умеет генерировать. Но в ряде случаев может потребоваться другой тип событий. В ряде случаев имеется возможность использовать готовые интерфейсы слушателей. Например, мы хотим, чтобы возникло событие java.awt.event.TextEvent, связанное с изменением текста заголовка. “Обычная” панель JPanel не имела свойств, связанных с текстом, и это событие в ней не поддерживалось. Интерфейс java.awt.event.TextListener имеет всего один метод textValueChanged(TextEvent e), так что в адаптере нет необходимости. Для создания такого события для нашего компонента требуется использовать добавление поддержки события через Bean Patterns. В Java имеется два типа источников событий: · Unicast Event Source – источник порождают целевые объекты событий, которые передаются одному слушателю-приёмнику. “Cast” – список исполнителей, “Unit”- единичный, “Unicast” – от Unit и Cast - один обработчик, “Source” - источник. В этом случае список слушателей не создаётся, а резервируется место только для одного. · Multicast Event Source - источник порождают целевые объекты событий, которые передаются нескольким слушателям-приёмникам. “Multi” – много, “Multicast” – много обработчиков. В этом случае для событий данного типа создаётся список слушателей. Очевидно, что для некоторых типов событий обязательно создавать список слушателей. Хотя в случае Unicast Event Source реализация оказывается проще. В нашем случае в списке нет необходимости, поэтому выберем первый вариант. В выпадающем списке диалога имеется возможность выбрать некоторые интерфейсы слушателей из пакетов java.awt.event и javax.swing.event. Однако нам нужен интерфейс, поддерживающий событие java.awt.event.TextEvent, который в нём отсутствует. Поэтому мы укажем имя интерфейса java.awt.event.TextListener вручную.
Задание в компоненте нового типа событий При выборе варианта Generate Empty (“Генерировать Пустое”) в коде компонента появятся пустые реализации методов добавления и удаления слушателей. Это достаточно экзотический случай, поэтому мы выберем вариант Generate Implementation (“Генерировать Реализацию”). Если выбрать опцию Generate Event Firing Methods (“Генерировать методы “выстреливания событиями” ”), происходит автоматическая генерация заготовок fire-методов fireИмяСобытия , предназначенных для оповещения зарегистрированных слушателей. В случае Unicast-источников обход списка слушателй не требуется, поэтому нам нет необходимости отмечать данный пункт. А вот в случае Multicast-источника это наиболее часто требующееся решение. При этом обычно бывает желательно передавать в методы событие как параметр – и для этого надо выбрать опцию Pass Event as Parameter (“Передавать событие как параметр”). Если пункт Generate Event Firing Methods отмечен, а опция Pass Event as Parameter не выбрана, событие не будет передаваться в fire-методы, а будет создано в самом fire-методе. Именно так происходит в примере для свойства sampleProperty, где вызов propertySupport.firePropertyChange(PROP_SAMPLE_PROPERTY, oldValue, sampleProperty) приводит к порождению внутри метода firePropertyChange события PropertyChange. Генерация кода, поддерживающего интерфейс java.awt.event.TextListener, приведёт для Unicast-источника без генерации fire-методов к появлению следующего кода: /** * Utility field holding the TextListener. */ private transient java.awt.event.TextListener textListener = null; /** * Registers TextListener to receive events. * @param listener The listener to register. */ public synchronized void addTextListener(java.awt.event.TextListener listener) throws java.util.TooManyListenersException { if (textListener != null) { throw new java.util.TooManyListenersException (); } textListener = listener; } /** * Removes TextListener from the list of listeners. * @param listener The listener to remove. */ public synchronized void removeTextListener(java.awt.event.TextListener listener) { textListener = null; } Но в этом случае добавлять код, обеспечивающий порождение события, должен программист. Если выбрана опция генерации fire-методов без передачи события как параметра, появится следующий дополнительный код по сравнению с предыдущим вариантом: /** * Notifies the registered listener about the event. * * @param object Parameter #1 of the <CODE>TextEvent<CODE> constructor. * @param i Parameter #2 of the <CODE>TextEvent<CODE> constructor. */ private void fireTextListenerTextValueChanged(java.lang.Object object,int i){ if (textListener == null) return; java.awt.event.TextEvent e = new java.awt.event.TextEvent (object, i); textListener.textValueChanged (e); } Этот код также не обеспечивает автоматической генерации события в нашем компоненте, но даёт возможность сделать это путём добавления одной строчки в код метода setTitle – перед вызовом метода repaint()мы напишем fireTextListenerTextValueChanged(this, java.awt.event.TextEvent.TEXT_VALUE_CHANGED); В качестве первого параметра fire-метода идёт ссылка на объект-источник события, в качестве второго – идентификатор типа события. Найти, где задаётся идентификатор, просто – достаточно перейти мышкой по гиперссылке java . awt . event . TextEvent , появляющейся в среде разработке при нажатии клавиши <CTRL>, и посмотреть исходный код конструктора. – Данную гиперссылку можно получить в строке java.awt.event.TextEvent e = new java.awt.event.TextEvent (object, i); в теле метода fireTextListenerTextValueChanged, в которой, собственно, и используется этот не очень понятный с первого взгляда параметр. Теперь после компиляции проекта мы можем назначать обработчики событий типа TextValueChanged нашему компоненту. К сожалению, для того, чтобы событие textValueChanged появилось в списке событий компонента в окне jTitledPanel1[JTitledPanel]-Properties/Events, требуется закрыть среду NetBeans и зайти в неё вновь. Для свойств этот баг отсутствует – они появляются в окне jTitledPanel1[JTitledPanel]-Properties/ Properties сразу после компиляции проекта. Теперь для нашего компонента можно назначать и удалять обработчик события textValueChanged как непосредственно на этапе визуального проектирования, так и программным путём. Покажем, каким образом это делается на этапе визуального проектирования. Выделим компонент jTitledPanel1 и выберем в окне jTitledPanel1[JTitledPanel]-Properties/Events событие textValueChanged. Нажмём кнопку с тремя точками, находящуюся рядом с полем – вызовется диалог добавления и удаления обработчиков событий. Вообще, имеется правило – если название на кнопке или пункте меню кончается на три точки, это означает, что при нажатии на кнопку или выборе пункта меню появится какой-нибудь диалог.
Создание обработчика события на этапе визуального проектирования Введём в качестве имени обработчика события (event handler) в качестве примера “myHandler” и нажмём “OK”. В списке обработчиков Handlers формы “Handlers for textValueChanged” появится имя myHandler. При закрытии этой формы по нажатию “OK” в исходном коде приложения (а не компонента!) появится код private void myHandler(java.awt.event.TextEvent evt) { // TODO add your handling code here: } Вместо комментария “// TODO add your handling code here:”, как обычно, следует написать свой код обработчика. Например, такой: javax.swing.JOptionPane.showMessageDialog( null,"Text="+ jTitledPanel1.getTitle() ); - Компонент – это:
- Компонент JavaBeans является классом Java и имеет три типа атрибутов:
- Наиболее простым способом создания компонента является использование мастера среды NetBeans. - Методы с названием fireИмяСобытия (“fire” – “стрелять”, в данном случае – “выстрелить событием”) осуществляют поочерёдный вызов зарегистрированных слушателей из списка для данного события, передавая им событие на обработку. - Методы addИмяСобытия Listener и removeИмяСобытия Listener обеспечивают для компонента возможность добавления и удаления объекта слушателя - обработчика события. - Если требуется задать в компоненте новые свойства или обеспечить генерацию компонентом новых типов событий событий, следует воспользоваться мастером, вызываемым через узел Bean Patterns. Таким же образом удаляются свойства и события компонента. - Для того, чтобы добавить компонент в палитру, следует открыть файл компонента в окне редактора исходного кода, и в меню Tools выбрать пункт Add to Palette. После чего в появившемся диалоге выбрать палитру, на которую будет добавлен компонент. Желательно выбирать Beans, чтобы не путать наши компоненты со стандартными. - Свойства вида Bound – обычные свойства. При изменении таких свойств порождается событие PropertyChange. Свойства вида Constrained требуют проверки задаваемого значения свойства на принадлежность к области допустимых значений. Если значение не удовлетворяет этому условию, возбуждается исключительная ситуация. При изменении таких свойств порождается событие VetoableChangeEvent. - В Java имеется два типа источников событий: · Unicast Event Source – источник порождают целевые объекты событий, которые передаются одному слушателю-приёмнику. В этом случае список слушателей не создаётся, а резервируется место только для одного обработчика. · Multicast Event Source - источник порождают целевые объекты событий, которые передаются нескольким слушателям-приёмникам. В этом случае для событий данного типа создаётся список слушателей. - Генерация события в компоненте обеспечивается вручную вызовом fire-метода или другим способом. · Создать собственный компонент JTitledPane, описанный в данной главе. · Усовершенствовать компонент, обеспечив добавление в него свойства titleColor. Подсказка: установка красного цвета рисования в качестве текущего цвета вывода графических примитивов для объекта Graphics g осуществляется вызовом метода g.setColor(Color.red). · Усовершенствовать компонент, обеспечив генерацию в нём событий типа TitleShiftEvent, предварительно создав соответствующий интерфейс. По желанию можно добавить и событие изменения цвета заголовка. · *По желанию учащегося: Усовершенствовать компонент, обеспечив добавление в него свойства titleFont и методов, обеспечивающих установку нужного размера и типа фонта.
(рекомендуется для изучения после чтения данного учебного пособия)
|