2. Caros Colegas!
Нічого й думати освоїти Java технології за 16 тижнів, працюючи по дві години на тиждень.
Інша справа, познайомитися з цією технологією, знати її особливості і можливості і щось
навчитися в ній робити. А потім, коли обставини заставлять вас глибше вникнути в цю
технологію, ви вже не повинні її боятися і можете сміливо приступати до роботи.
Я пропоную вам курс програмування у Java з 19 уроків. Скільки уроків ви зможете засвоїти
залежить від нас з вами. В такому обсязі курс програмування у Java читається в нашому
університеті вперше. Я готував його майже рік. Але не все ще доведено до досконалості. Я
перечитую текст декілька разів і кожного разу знаходжу опечатки. Впевнений, що ви будете
знаходити їх і далі.
По ходу вивчення ви повинні виконати 14 або 15 лабораторних робіт. В кінці семестру залік
— можливо ви не знаєте, що в програмуванні це гірше за екзамен. Ви можете одержати його
лише у випадку, коли не пропустите жодного заняття і здасте всі лабораторні роботи.
За кожне пропущене заняття треба буде розрахуватися програмою, яка ілюструє всі методи
указаного мною класу, на зразок того, як це зроблено в одному з уроків для класу String.
Спішити ми не будемо, тому рекомендую сильнішим студентам, не чекаючи нас із
слабенькими та ледаченькими студентами, іти вперед і постаратися освоїти як можна більше.
А. Горбань
4.02.2008
2
3. ЗМІСТ
Урок 1. Вступ 4
Урок 2. Базовий курс Java 11
Урок 3. Обєктно-орієнтоване програмування в Java 42
Урок 4. Класи-оболонки 74
Урок 5. Робота з рядками 86
Урок 6. Класи-коллекції 99
Урок 7. Класи-утиліти 115
Урок 8. Принципи побудови графічного інтерфейса 121
Урок 9. Графічні примітиви 128
Урок 10. Основні компоненти 152
Урок 11. Розміщення компонентів 175
Урок 12.Обробка подій 183
Урок 13.Створення меню 202
Урок 14.Аплети 210
Урок 15.Зображення і звук 226
Урок 16.Обробка виключних ситуацій 252
Урок 17.Підпроцеси 262
Урок 18.Потоки введення/виведення 280
Урок 19 Мережеві засоби Java 301
3
4. Програмування в Java
Урок 1. Вступ
Поздоровляю вас зі вступом в ряди програмістів на Java — розроблювачів технології початку XXI
століття. Всі уроки ви повинні прочитувати вдома, а на лекції і лабораторних роботах ви повинні
звітуватися за виконані завдання. Перше серйозне завдання ви знайдете в кінці Уроку 2. Всі завдання
індивідуальні, щоб одержати залік в кінці семестру їх треба обовязково виконати. Отже, встановлюйте на
своїх компютерах JDK (що це таке дивись далі) - його можна скачати з Інтернет безкоштовно або
діставайте яке-небудь інтегроване середовище програмування (JBuilder, Eclipse... ). Останнє теж можна
безкоштовно скачати з Інтернет, але вам прийдеться самостійно навчитися ним користуватися. Більше
всього, що в лаборатрії ми будемо користуватися починаючи з третього уроку середовищем JBuilder.
1.1. Що таке Java
Це острів Ява в Малайському архіпелазі, територія Індонезії. Це сорт кофе, який полюбляють пити творці
Java. А якщо серйозно, то відповісти на це питання досить важко, тому що границі Java, і без того розмиті,
всь час розширюються. Спочатку Java (офіційний день нардження технології Java — 23 травня 1995 г.)
призначалась для програмування побутових електронних пристроїв, таких як мобільні телефони. Потім
Java стала застосовуватися для програмування браузерів — появились аплети. Потім виявилося, що на
Java можна створювати повноцінні аплікації. Їх графічні елементи стали оформлять у вигляді компонентів
— появились JavaBeans, з котрими Java ввійшла в світ розподілених систем і проміжного програмного
забезпечення, тісно повязаних з технологією CORBA. Остався один крок до програмування серверів —
цей крок був зроблений — зявилися сервлети і EJB (Enterprise JavaBeans). Сервери повинні взаємодіяти
з базами даних — появились драйвери JDBC (Java DataBase Connection). Взаємодія виявилася
ефективною, і багато систем управління базами даних і навіть операційні системи включили Java в своє
ядро, наприклад Oracle, Linux, MacOS X, AIX. Що ще не охоплено? Назвіть, і через півроку почуєте, что
Java уже застосовується і там. Із-за такої розмитості самого поняття його описують таким же розмитим
словом — технологія.
Прочитавши цей абзац, ви, без сумніву, відчуєте комплекс неповноцінності – скільки в області
інформатики, до якої ви і себе причисляєте, існує речей, про які ви не маєте жодного уявлення. Та замість
розчарування це повинно надати вам запалу до найскорішого освоєння хоча би програмування у Java.
Таке швидке і широке розповсюдження технології Java не в останню чергу повязано з тим, що вона
використовує нову, спеціально створену мову програмування, яка так і називається — мова Java. Ця мова
створена на базі мов Smalltalk, Pascal, C++ і ін., увібравши їх кращі, на думку творців, риси і відкинувши
гірші. На цей рахунок єсть різні думки, але безперечно, що мова виявилася зручною для вивчення,
написані на ній програми легко читаються і налаштовуються: першу програму можна написати уже через
годину після початку вивчення мови. Мова Java становиться мовою навчання обєктно-орієнтованому
програмуванню, так само, як мова Pascal була мовою навчання структурному програмуванню. Недарма
на Java уже написано величезна кількість програм, бібліотек класів, а власний аплет не написав тільки
вже зовсім лінивий.
Для повноти картини належить сказати, что створювати аплікації для технології Java можна не тільки в
мові Java, уже появились і інші мови, єсть навіть компілятори з мов Pascal і C++, але краще все-таки
використовувати мову Java; на ній всі аспекти технології викладаються простіше і зручніше.
Ясно, що всю технологію Java неможливо викласти в декількох лекціях, повне її описання складе цілу
бібліотеку. Ми торкнемося тільки мови Java. Після цього ви зможете створити Java аплікації будь-якої
складності, вільно розбиратися в літературі і лістингах програм, продовжувати вивчення аспектів
технології Java по спеціальній літературі. Мова Java теж дуже бурхливо розвивається, деякі її методи
оголошуються застарілими (deprecated), появляються нові конструкції, збільшується вбудована
бібліотека класів, але єсть стабільне ядро мови, зберігається її дух і стиль. Ось на ньому ми і
зконцентруємо нашу увагу.
4
5. 1.2. Виконання Java-програми
Як ви знаєте, програма, написана на одній із мов високого рівня, до котрих відноситься і мова Java, так
званий вихідний модуль ( "сирець" на жаргоні, від англійського "source"), не може бути зразу ж виконана. Її
спочатку треба скомпілювати, тобто перевести в послідовність машинних команд — обєктний модуль.
Але і він, як правило, не може бути зразу ж виконаним: обєктний модуль треба ще зкомпонувати із
бібліотеками використовуваних в модулі функцій і організувати перехресні посилки між секціями
обєктного модуля, одержавши в результі завантажувальний модуль — повністю готову до виконання
програму.
Вихідний модуль, написаний на Java, не може уникнути цих процедур, але тут проявляється головна
особливість технології Java — програма компілюється не зразу в машинні команди, не в команди якогось
конкретного процесора, а в команди так званої віртуальної машини Java (JVM, Java Virtual Machine).
Віртуальна машина Java — це сукупність команд разом з системою їх виконання. Для спеціалістів
скажемо, що віртуальна машина Java повністю стекова, так що не вимагається складна адресація комірок
памяті і велика кількість регістрів. Тому команди JVM короткі, більшість з них має довжину 1 байт, звідси
команди JVM називают байткодами (bytecodes), хоча єсть команди довжиною 2 і 3 байти. Згідно
статистичних дослідженнь середня довжина команди складає 1,8 байта. Повне описання команд і всієї
архітектури JVM міститься в специфікації віртуальної машини Java (VMS, Virtual Machine Specification).
Друга особливість Java — всі стандартніе функції, що викликаються в програмі, підключаються до неї
тільки на eтапі виконання, а не включаються в байт-коди. Як говорять спеціалісти, відбувається динамічне
компонування (dynamic binding). Це теж сильно зменшує обєм скомпільованої програми.
Отже, на першому етапі програма, написана на мові Java, переводиться компілятором в байт-коди. Ця
компіляція не залежить від типу якого-небудь конкретного процесора і архітектури деякого конкретного
компютера. Вона може бути виконана зразу ж після написання програми. Байт-коди записуються в
одному або декількох файлах, можуть зберігатися у зовнішній памяті або передаватися по мережі. Це
особливо зручно дякуючи невеликому розміру файлів з байт-кодами. Потім одержані в результаті
компіляции байт-коди можна виконувати на любому компютері, котрий має систему реалізації JVM. При
цьому не має значення ні тип процесора, ні архітектура компютера. Так реалізується принцип Java "Write
once, run anywhere" — "Написано раз, виконується де завгодно".
Інтерпретація байт-кодів і динамічне компонування значно сповільнюють виконання програм. Це не має
значення в тих ситуаціях, коли байт-коди передаються по мережі, мережа все рівно повільніша любої
інтерпретації, але в інших ситуаціях вимагається потужний і швидкий компютер. Тому постійно йде
вдосконалення інтерпретаторів в сторону збільшення швидкості інтерпретації. Разроблені JIT-
компілятори (Just-In-Time), запамятовуючі уже інтерпретовані частки кода в машинних командах
процесора і просто виконуючі ці участки при повторному зверненні, наприклад, в циклах. Це значно
збільшує швидкість обчислень, що повторяються. Фірма SUN розробила цілу технологію Hot-Spot і
включає її в свою віртуальну машину Java. Але, звичайно, найбільшу швидкість може дати тільки
спеціалізований процесор.
Фірма SUN Microsystems випустила мікропроцесори PicoJava, що працюють на системі команд JVM, і
збираеться випускати цілу серію все більш потужних Java-процесорів. Єсть уже і Java-процесори інших
фірм. Ці процесори безпосередньо виконують байт-коди. Але при виконанні програм Java на інших
процесорах вимагається ще інтерпретація команд JVM в команди конкретного процесора, а значить,
потрібна програма-інтерпретатор, причому для кожного типу процесорів, і для кожної архітектури
компютера треба написати свій інтерпретатор.
Це завдання уже виішено практично для всіх компютерних платформ. На них реалізовані віртуальні
машини Java, а для найбільш розповсюджених платформ існує декілька реалізацій JVM різних фірм. Все
більше операційнних систем і систем управління базами даних включають реалізацію JVM в своє ядро.
Створена і спеціальна операційна система JavaOS, яка застосовується в електронних пристроях. В
більшості браузерів вбудована віртуальна машина Java для виконання аплетів.
Уважний читч уже помітив, що крім реалізації JVM для виконання байт-кодів на компютері ще потрібно
5
6. мати набір функцій, які викликаються із байт-кодів і динамічно компонуються з байт-кодами. Цей набір
оформляється у вигляді бібліотеки класів Java, яка складається з одного або декількох пакетів. Кожна
функція може бути записана байт-кодами, але, оскільки вона буде зберігатися на конкретному комп’ютері,
її можна записати прямо в системі команд цього комп’ютера, уникнувши тим самим інтерпретації байт-
кодів. Такі функції називають "рідними" методами (native methods). Застосування "рідних" методів
прискорює виконання програми.
Фірма SUN Microsystems — творець технології Java — безкоштовно розповсюджує набір необхідних
програмних інструментів для повного циклу роботи с цією мовою програмування: компіляції, інтерпретації,
налаштовування, який включає і багату бібліотеку класів, під назвою JDK (Java Development Kit). Є набори
інструментальних програм і інших фірм. Наприклад, великою популярністю користується JDK фірми IBM.
1.3. Що таке JDK
Набір програм і класів JDK містить:
• компілятор javac із вихідного тексту в байт-коди;
• інтерпретатор java, який містить реалізацію JVM;
• полегшений інтерпретатор jre (в останніх версіях відсутній);
• програму перегляду аплетів appletviewer, що заміняє браузер;
• налаштовувач jdt>;
• дизассемблер javap;
• програму архівації і стиснення jar;
• програму збору документації javadoc;
• програму javah генерації заголовкових файлів мови С;
• програму javakey додавання електронного підпису;
• програму native2ascii, яка перетворює бінарні файли в текстові;
• програми rmic і rmiregistry для роботи з віддаленими об’єктами;
• програму serialver, яка визначає номер версії класу;
• бібліотеки і заголовкові файли "рідних" методів;
• бібліотеку класів Java API (Application Programming Interface).
В попередні версії JDK включались і налаштовувальні варіанти виконуваних програм: javac_g, java_g і т.д.
Компанія SUN Microsystems постійно розвиває і обновляє JDK, кожний рік появляються нові версії.
Відкрийте папку bin, яка входить у JDK і ви побачите, які програми містить ваша версія. Ось що
знаходиться на моєму комп’ютері.
6
7. Напряму я користувався лише трьома з цих програм (javac – компілятор, java – інтерпретатор,
appletviewer – перегляд аплетів). Цього досить, щоб написати і випробувати програми, пов’язані з
обчисленням і графікою, чим ми власне і займаємося. Дивлячись на інші програми цієї папки можете
уявити, що технологія Java не вичерпується обчисленнями і графікою.
У 1996 р. була випущена перша версія JDK 1.0, яка модифікувалась до версії с номером 1.0.2. У цій версії
бібліотека класів Java API містила 8 пакетів. Весь набір JDK 1.0.2 поставлявся в упакованому вигляді в
одному файлі розміром близько 5 Мбайтів, а після розпаковування займав близько 8 Мбайтів на диску.
В 1997 р. появилась версія JDK 1.1, остання її модифікація, 1.1.8, випущена в 1998 р. У цій версії було 23
пакети класів, займала вона 8,5 Мбайтів в упакованому вигляді і близько 30 Мбайтів на диску.
У перших версіях JDK всі пакети бібліотеки Java API були упаковані в один архівний файл classes.zip і
викликались безпосередньо із цього архіву, його не треба розпаковувати.
Потім набір інструментальних засобів був сильно перероблений. Версія JDK 1.2 вийшла в грудні 1998 р. і
містила уже 57 пакетів класів. В архівному вигляді цей файл розміром майже 20 Мбайт і ще окремий
файл розміром більше 17 Мбайт з упакованою документацією. Повна версія займає 130 Мбайт, із них
близько 80 Мбайт займає документація.
Починаючи з версії JDK 1.2, всі продукти технології Java власного виробництва компанія SUN стала
називати Java 2 Platform, Standard Edition, скорочено J2SE, a JDK перейменувала в Java 2 SDK, Standard
Edition (Software Development Kit), скорочено J2SDK, оскільки випускається ще Java 2 SDK Enterprise
Edition і Java 2 SDK Micro Edition. Між іншим, сама компанія SUN часто використовує і стару назву, а в
літературі закріпилась назва Java 2. Крім 57 пакетів класів, обовязкових на любій платформі і
одержавших назву Core API, в Java 2 SDK vl.2 входять ще додаткові пакети класів, названих Standard
Extension API. У версії 2 SDK SE, vl.3, яка вийшла в 2000 р., уже 76 пакетів класів, які складають Core API.
В упакованому вигляді це файл разміром близько 30 Мбайт, і ще файл з упакованою документацією
розміром 23 Мбайти. Все це розпаковується в 210 Мбайт дискового простору. Ця версія вимагає процесор
Pentium 166 і вище і не менше 32 Мбайтів оперативної памяті.
Зараз версія JDK 1.0.2 уже не використовується. Версія JDK 1.1.5 з графічною бібліотекою AWT
вбудована в популярні браузери Internet Explorer 5.0 і Netscape Communicator 4.7, тому вона
застосовується для створення аплетів. Технологія Java 2 широко використовується на серверах і в клієнт-
серверних системах.
Щоб уявити всю складність Java технології, відкрийте файл Index в папці Packages. Тут ви знайдете
близько сотні пакетів, і в деяких з них міститься по декілька десятків класів. Відкрити ці пакети можна з
папки JВuilder*jdk*srcsrcjava. (Замість зірочок треба поставити номери ваших версій програмного
продукту).
Крім JDK, компанія SUN окремо розповсюджує ще і набір JRE (Java Runtime Environment).
1.4. Що таке JRE
Набір програм і пакетів класів JRE містить все необхідне для виконання байт-кодів, в тому числі
інтерпретатор java (в попередніх версіях полегшений інтерпретатор jre) і бібліотеку класів. Це частина
JDK, яка не містить компілятори, налаштовувачі і інші засоби розробки. Якраз JRE або його аналог інших
фірм міститься в браузерах, що вміють виконувати програми на Java, в операційних системах і системах
управління базами даних.
Хоча JRE входить до складу JDK, фірма SUN розповсюджує цей набір і окремим файлом.
Версія JRE 1.3.0 — це архівний файл розміром близько 8 Мбайтів, що розвертається у 20 Мбайтів на
диску.
7
8. Після установки Java ви одержите каталог з назвою, наприклад, jdk1.3, а в ньому підкаталоги:
• bin, який містить виконувані файли;
• demo, який містить приклади програм;
• docs, котрий містить документацію, якщо ви її установили;
• include, котрий містить заголовкові файли "рідних" методів;
• jre, котрий містить набір JRE;
• old-include, для сумісності зі старими версіями;
• lib, котрий містить бібліотеки класів і файли властивостей;
• src, з вихідними текстами програм JDK. В нових версіях замість каталогу знаходиться упакований
файл src.jar.
Так-так! Набір JDK містить вихідні тексти більшості своїх програм, написані на Java. Це дуже зручно. Ви
завжди можете з точністю дізнатись, як працює той чи інший метод обробки інформації із JDK,
проглянувши вихідний код даного метода. Це дуже корисно і для вивчення Java на "живих" працюючих
прикладах.
Якщо ваш JDK являється частиною якогось інтегрованого середовища програмування, наприклад
JBuilder , то деякі з перечислених вище папок можуть знаходитися в іншому місці. Прогляньте вашу папку
JDK.
1.5. Як користуватися JDK
Найпопулярнішим у нас є середовище програмування JBuilder, дуже схоже на Delphi, так як створені
однією й тією ж фірмою. На ньому ми і зупинимося. Як користуватися JBuilder – розглянемо згодом. Але
існує велика кількість і інших можливостей, аж до написання програми в Блокнот.
1.6. Java в Internet
Розроблена для використання в мережах, Java просто не могла не знайти відображення на сайтах
Internet. Дійсно, маса сайтів повністю присвячена або містить інформацію про технологію Java. Одна
тільки фірма SUN має декілька сайтів з інформацією про Java:
• http://www.sun.com/ — тут всі посилки, звідси можна скопіювати JDK;
• http://java.sun.com/ — основний сайт Java, звідси теж можна скопіювати JDK;
• http://developer.java.sun.com/ — маса корисних речей для розроблювача;
• http://industry.java.sun.com/ — новини технології Java;
• http://www.javasoft.com/ — сайт фірми JavaSoft, підрозділу SUN;
• http://www.gamelan.com/.
На сайті фірми IBM є великий розділ http://www.ibm.com/developer . /Java/, де можна знайти дуже
багато корисного для програміста.
Компанія Microsoft містить інформацію про Java на своєму сайті: http:// www.microsoft.com/java/.
Великий внесок в розвиток технології Java робить корпорація Oracle: http://www.oracle.coin/.
Існує багато спеціалізованих сайтів:
• http://java.iba.com.by/ — Java team IBA (Білорусія);
• http://www.artima.com/;
• http://www.freewarejava.com/;
• http://www.jars.com/ — Java Review Service;
8
9. • http://www.javable.com — російськомовний сайт;
• http://www.javaboutique.com/;
• http://www.javalobby.com/;
• http://www.javalogy.com/;
• http://www.javaranch.com/;
• http://www.javareport.com/ — незалежне джерело інформації для розроблювачів;
• http://www.javaworld.com — електронний журнал;
• http://www.jfind.com/ — збірник програм і статей;
• http://www.jguru.com/ — поради спеціалістів;
• http://www.novocode.com/;
• http://www.sigs.com/jro/ — Java Report Online;
• http://www.sys-con.com/java/;
• http://theserverside.com/ — питання створення серверних Java-аплікацій;
• http://servlets.chat.ru/;
• http://javapower.da.ru/ — збірник FAQ російською мовою;
• http://www.purejava.ru/;
• http://java7.da.ru/;
• http://codeguru.earthweb.com/java/ — великий збірник аплетів і інших програм;
• http://securingjava.com/ — обговорюються питання безпеки;
• http://www.servlets.com/ — питання по написанню аплетів;
• http://www.servletsource.com/;
• http://coolservlets.com/;
• http://www.servletforum.com/;
• http://www.javacats.com/.
Персональні сайти:
• http://www.bruceeckel.com/ — сайт Bruce Eckel;
• http://www.davidreilly.com/java/;
• http://www.comita.spb.ru/users/sergeya/java/ — питання, що стосуються русифікації Java.
На жаль, адреси сайтів часто міняються. Можливо, ви вже не знайдете деяких із перечислених сайтів,
зате появляться багато інших.
1.7. Література по Java
Повне і строге описання мови викладено в книзі The Java Language Specification, Second Edition. James
Gosling, Bill Joy, Guy Steele, Gilad Bracha». Ця книга в електронному вигляді знаходиться за адресою
http://java.sun.com/docs /books/jls/second_edition/html/j.title.doc.html і займає в упакованому вигляді
близьо 400 Кбайт.
Таке ж повне і строге описання віртуальноїй машини Java викладено в книзі «The Java Virtual Machine
Specification, Second Edition. Tim Lindholm, Frank Yellin». В электронному вигляді вона знаходиться за
адресою http://java.sun.com /docs/books/vmspec/2nd-edition/html/VMSpecTOC.doc.htmI.
Тут же необхідно відзначити книгу "батька" технології Java Джеймса Гослінга, написану разом з Кеном
Арнольдом. Є російський переклад Гослинг Дж., Арнольд К. Язык программирования Java: Пер. с англ. —
СПб.: Питер, 1997. — 304 с.: ил.
Компанія SUN Microsystems має на своєму сайті постійно поновлюваний електронний підручник Java
Tutorial, розміром уже більше 14 Мбайт: http://java.sun.com/docs/books/tutorial/. Час від часу
9
10. зявляється його друковане видання The Java Tutorial, Second Edition: Object-Oriented Programming for the
Internet. Mary Campione, Kathy Walrath.
Повне описання Java API містится в документації, але є друковане видання The Java Application
Programming Interface. James Gosling, Frank Yellin and the Java Team, Volume 1: Core Packages; Volume 2:
Window Toolkit and Applets.
10
11. Програмування в Java
Урок 2. Базовий курс Java
Реквізити: Текст лекції і опис класу System
2.1. Вступ
Приступаючи до вивчення нової мови, корисно поцікавитися, які вихідні дані можуть опрацьовуватися
засобами цієї мови, в якому вигляді їх можна задавати і які стандартні методи опрацювання цих даних
закладені в мову. Це досить нудне заняття, тому що в кожній розвиненій мові програмування багато типів
даних і ще більше методів їх використання. Однак невиконання цих правил приводить до появи скритих
помилок, виявити які часто буває дуже важко. Але в кожному ремеслі спочатку приходиться "грати гами",
і ми не можемо цього уникнути.
Всі правила мови Java вичерпно викладені в її специфікації, яка скорочено називається JLS. Інколи, щоб
зрозуміти, як виконується та чи інша конструкція мови Java, приходиться звертатися до специфікації, але,
на щастя, це буває рідко, правила мови Java досить прості і той, хто має досвід програмування в інших
мовах, легко їх освоїть.
В цій главі приведені примітивні типи даних, операції над ними, оператори управління, показані "підводні
камені", яких треба уникати при їх використанні. Але почнемо, по традиції, з найпростішої програми.
2.2. Перша програма на Java
Java програма існує у вигляді класу. Починається службовим словом class, за яким слідує імя класу , а
далі, в фігурних дужках, тіло класу – поля змінних і методи. Ось наша перша Java програма:
class Zero {}
Програма може бути написана в будь-якому текстовому редакторі, наприклад Notepad. Потім її треба
зберегти у файлі, імя якого співпадає з іменем класу і з розширенням .java, в даному випадку
Zero.java. Потім програму треба скомпілювати, що в даному випадку означає переведення її в байт
коди. Для цього треба мати набір файлів JDK – Java Developer Kit, який можна безкоштовно зкачати з
Інтернет або зкопіювати з іншого комп’ютера. У мене ця папка знаходиться на диску С. Щоб спростити
компіляцію і запуск програми, я збережу цей файл також на диску С. На вашому компютері версія JDK
може бути відмінною від моєї (вони обновлюються майже щорічно). Те ж саме стосується і її місця
знаходження на диску, тому треба при потребі внести корективи у нижче наведену адресу папки bin.
Знаходимо в компютері програму Командная строка і відкриваємо її. Ось як було на моєму компютері.
Рис. 2.1. Програма Командная строка
Нам треба повернутися до корінної директорії С, де знаходиться JDK. Для цього набираємо в
командному рядку сd C: і натискаємо Enter (cd – change directory). Результат ви бачите нижче.
11
12. Рис. 2.2. Програма Командная строка готова працювати з файлами диску С
Тепер викликаємо компілятор javac, що знаходиться в папці bin і вказуємо йому файл для компіляції.
Тиснемо Enter і компілятор створить файл с байт-кодами, дасть йому імя Zero.class и запише цей файл в
поточний каталог – перевірте. Якщо все пройшло як треба, Командная строка повернеться до корінної
директорії С.
Рис. 2.3. Програма скомпільована
Залишилось викликати інтерпретатор java (JVM), передавши йому в якості аргумента імя класу (а не
файла). Після натиску Enter нас чекає перше розчарування - JVM не змогла запустити програму на
виконання і сповістила чому процес (thread) виключився (exception). В нашому класі відсутній стартер –
метод main().
Рис. 2.4. Невдала спроба запустити програму на виконання
Ситуація аналогічна з С++, але трохи складніша. Попробуємо її виправити.
class Zero{
public static void main(String[] args){}
}
Чому крім знайомого нам void перед main() зявилося ще й public static, а в аргументі String[] args я
поясню пізніше, а поки що візьміть це за правило. Перекомпілюйте файл і запустіть програму заново.
12
13. Рис. 2.5. Програма спрацювала
Цього разу все получилося, хоча програма і спрацювала вхолосту. Тепер заставимо її зробити щось
корисне.
2.3. Перша повноцінна програма на мові Java
По давній традиції, що започаткована в мові С, підручники по мовах програмування починаються з
програми, яка виводить на екран привітання "Hello, World!". He будемо порушувати цю традицію.
Лістинг 2.1. Перша повноцінна програма на мові Java
class Zero{
public static void main(String[] args){
System.out.println("Hello, World!");
}
}
Перекомпілюйте цю програму і запустіть на виконання. Цього разу все повинно вийти як треба.
Рис. 2.6. Перша результативна Java програма
На цьому простому прикладі можна помітити ряд суттєвих особливостей мови Java.
• Всяка програма являє собою один або декілька класів, в цьому найпростішому прикладі тільки
один клас (class).
• Початок класу позначачється службовим словом class, за яким іде імя класу, що вибирається
довільно, в даному випадку Zero. Все, що міститься в класі, записується в фігурних дужках і
складає тіло класу (class body).
13
14. • Всі дії виконуються за допомогою методів обробки інформації, коротко говорять просто метод
(method). Цей термін вживається в мові Java замість назви "функція", що використовується в
інших мовах.
• Методи розрізняються по іменах. Один із методів обовязково повинен називатися main, з нього
починається виконання програми. В нашій найпростішій програмі тільки один метод, а значить,
імя йому main .
• Як і положено функції, метод завжди видає в результат (частіше говорять, повертає (returns))
тільки одне значення, тип якого обовязково указується перед іменем метода. Метод може і не
повертати ніякого значення, виконуючи роль процедури, як у нашому випадку. Тоді замість типу
значення записується слово void, як це і зроблено у прикладі.
• Після імені метода в дужках, через кому, перечисляються аргументи (arguments) - або
параметри метода. Для кожного аргумента указується його тип і, через пробіл, імя. В прикладі
тільки один аргумент, його тип — массив, що складається з рядків символів. Рядок символів — це
вбудований в Java API тип String, а квадратні дужки — ознака масива. Імя масива може бути
довільним, в прикладі вибрано імя args.
• Перед типом значення, що повертається методом, можуть бути записані модифікатори
(modifiers). В прикладі їх два: слово public означає, що цей метод доступний звідусіль; слово static
забезпечує можливість виклику метода main () в самому початку виконання програми.
Модифікатори взагалі необовязкові, але для метода main () вони необхідні.
Зауваження.
В тексті після імені метода ставляться дужки, щоб підкреслити, що це є імя метода, а не простої змінної.
• Все, що містить метод, тіло метода (method body), записується в фігурних дужках.
Єдину дію, яку виконує метод main () в прикладі, заключається у виклику іншого метода зі складним
іменем System.out.println() і передачі йому на опрацювання одного аргумента, текстової константи "Hello,
World!". Текстові константи записуються в лапках, які являються тільки обмежувачами і не входять в
склад текста.
Складне імя System.out.println() означає, що в класі System, який входить в Java API, визначається змінна
з іменем out, котра містить екземпляр одного із класів Java API, класа PrintStream, В ньому є метод
println() . Все це стане ясно пізніше, а поки що просто будемо писати це довге імя.
Дія метода println () заключається у виведенні свого аргумента у вихідний потік, пов’язаний, як правило, з
виведенням на екран текстового термінала, у вікно MS-DOS Prompt або Command Prompt , в залежності
від вашої системи. Після виведення курсор переходить на початок наступного рядка екрана, на що указує
закінчення ln, слово println — скорочення слів print line. У складі обєкта out єсть і метод print (), що
залишає курсор в кінці виведеного рядка. Зрозуміло, це прямий вплив мови Pascal.
Відкрийте опис класу System в однойменному файлі папки Java2 і ознайомтеся з іншими методами класу
і його полів, в першу чергу поля out.
Зробимо зразу важливе зауваження. Мова Java розрізняє заглавні і прописні літери, імена main, Main,
MAIN різні з "точки зору" компілятора Java. В прикладі важливо писати String, System з заглавної літери, a
main з маленької. Але всередині текстової константи не має значення, писати World чи world, компілятор
взагалі не "дивиться" на неї, різниця буде помітна лише на екрані.
Зауваження
Мова Java розрізняє прописні і заглавні літери.
Свої імена можна записувати як завгодно, можна було б дати класу імя helloworld чи Helloworld, але між
Java-програмістами заключено догвір під іменем "Code Conventions for the Java Programming Language",
який можна знайти за адресою http://java.sun.com/docs/codeconv/index.html. Ось декілька пунктів
14
15. цього договора:
• імена класів починаються з заглавної літери; якщо ім’я містить декілька слів, то кожне слово
починається із заглавної літери;
• імена методів і змінних починаються з прописної літери; якщо ім’я містить декілька слів, то кожне
наступне слово теж починається з прописної літери;
• імена констант записуються повністю заглавними літерами; якщо ім’я містить декілька слів, то між
ними ставиться знак підкреслювання.
Звичайно, ці правила необов’язкові, хоча вони і входять в JLS, п. 6.8, але дуже полегшують розуміння
кода і надають програмі характерний для Java стиль.
Стиль визначають не лише імена, але і розташування тексту програми по рядках, наприклад,
розташування фігурних дужок: чи залишати відкриваючу дужку в кінці рядка з заголовком класа або
метода чи переносити на наступний рядок? Чомусь це дрібне питання викликає запеклі суперечки, деякі
засоби розробки, наприклад JBuilder, навіть пропонують вибрати певний стиль розташування фігурних
дужок. Деякі фірми встановлюють свій, внутріфірменний стиль. Ми постараємося слідувати стилю "Code
Conventions" і в тому, що стосується розбиття текста програми на рядки (компілятор же розглядає всю
програму як один довгий рядок, для нього програма — це просто послідовність символів), і в тому, що
стосується відступів (indent) у тексті.
Порада
Називайте файл з програмою іменем класа, що містить метод main (), дотримуйтесь регістру літер.
Зауваження:
Не указуйте розширення class при виклику інтерпретатора.
2.4. Коментарі:
В тексті програми можна вставить коментарі, котрі компілятор не буде враховувати. Вони дуже корисні
для пояснення по ходу програми. В період налаштування можна виключати із програми один або декілька
операторів, помітивши їх символами коментаря, як говорять програмісти, "закоментувавши" їх. Коментарі
вводяться таким чином:
• за двома похилими рисками підряд //, без пробілу між ними, починається коментар, що
продовжується до кінця рядка;
• за похилою рискою і зірочкою /* починається коментар, котрий може займати декілька рядків, до
зірочки і похилої риски */ (без пробілів між цими знаками).
Коментарі дуже зручні для читання і розуміння коду, вони перетворюють програму в документ, що описує
її дії. Програму з хорошими коментарями називають самодокументованою. Тому в Java введені
коментарі третього типу, а в склад JDK — програму javadoc, що поміщає ці коментарі в окремі файли
формату HTML і створює гіперпосилання між ними: за похилою рискою і двома зірочками підряд, без
пробілів, /** починається коментар, котрий може займати декілька рядків до зірочки з однією похилою
рискою */ і опрацьовується програмою javadoc. В такий коментар можна вставить вказівки програмі
javadoc, котрі починаються символом @. Якраз так створюється документація в JDK. Додамо коментарі
до нашого прикладу (лістинг 1.2).
Лістинг 2.2. Перша програма з коментарями
class HelloWorld{
/**
* Пояснення змісту і особливостей програми...
* @author Імя Прізвище (автора)
15
16. * @version 1.0 (це версія програми)
*/
// HelloWorld — це лише імя
// Наступний метод починає виконання програми
public static void main(String[] args){ // args не використовується
/* Наступний метод просто виводить свій аргумент на екран дисплея */
System.out.println("Hello, 21st Century World!");
// наступний виклик закоментований, метод не буде виконуватися
// System.out.println("Farewell, 20th Century!");
}
}
Зірочки на початку строчки ніякої ролі не відіграють, вони лише допомагають нам слідкувати за
коментарем. Приклад, звичайно, перевантажений поясненнями (це поганий стиль), тут просто показані
різні форми коментарів.
2.5. Константи
В мові Java можна записувати константи різних типів у різних виглядах. Перечислимо їх.
2.5.1. Цілі константи
Цілі константи можна записувати в трьох системах числення:
• в десятичній формі: +5, -7, 12345678;
• у восьмеричній формі, починаючи з нуля: 027, -0326, 0777; в запису таких констант недопустимі
цифри 8 і 9;
Зауваження
Число, що починається з нуля, записано в восьмеричній формі, а не в десятковій.
• в шістнадцятирічній формі, починаючи з нуля і латинської літери х або X: 0xff0a, 0xFC2D, 0x45a8,
0X77FF; тут заглавні і прописні літери не розрізняються.
Цілі константи зберігаються у форматі типу int (див. нижче).
В кінці цілої константи можна записати літеру заглавну L або прописну l, тоді константа буде зберігатися в
довгому форматі типу long (див. нижче): +25L, -0371, 0xffL, 0xDFDF1.
Порада
Не використовуйте при запису довгих цілих констант заглавну латинську літеру l, її легко переплутати з
одиницею.
2.5.2. Дійсні константи
Дійсні константи записуються лише в десятковій системі числення в двох формах:
• з фіксованою точкою: 37.25, -128.678967, +27.035;
• з плаваючою точкою: 2.5е34, -0.345е-25, 37.2Е+4; можна писати заглавну або прописну латинську
літеру Е; пробіли і дужки недопустимі.
В кінці дійсної константи можна поставити літеру F або f, тоді константа буде зберігатися в форматі типу
float (див. нижче): 3.5f, -45.67F, 4.7e-5f. Можна приписати і літеру D (або d): 0.045D, -456.77889d, що
16
17. означає тип double, але це вже зайве, оскільки дійсні константи і так зберігаються в форматі типа double.
2.5.3. Символьні константи
Для запису одиноких символів використовуються наступні форми.
• Друковані символи можна записувати в апострофах: 'а', 'N', '?'. Замініть у програмі HelloWorld
команду
System.out.println("Hello, World!"); на
System.out.println('H');
і випробуйте її. Буде надрукована літера Н. Якщо ж ви напишете
System.out.println('Hello, World!');
то компілятор відмовиться працювати з такою програмою.
Керуючі символи записуються в апострофах з оберненою похилою рискою:
• 'n' — символ переводу рядка newline з кодом ASCII 10;
• 'r' — символ повернення каретки CR з кодом 13;
• 'f' — символ переводу сторінки FF з кодом 12;
• 'b' — символ повернення на крок BS з кодом 8;
• 't' — символ горизонтальної табуляції НТ з кодом 9;
• '' — обернена похила риска;
• '"' — лапка;
• ''' — апостроф.
• Код будь-якого символу з десятичним кодуванням від 0 до 255 можна задати, записавши його не
більше ніж трьома цифрами у восьмеричній системі числення в апострофах після оберненої
похилої риски: '123' — буква S, '346' — буква ц . Не рекомендується використовувати цю форму
запису для друкованих і керуючих символів, перечислених у попередньому пункті, оскільки
компілятор зараз же переведе восьмеричний запис у вказану вище форму. Найбільший код '377'
— десяткове число 255.
• Код будь-якого символу в кодуванні Unicode набираеться в апострофах після оберненої похилої
риски і латинської літери u рівно чотирма шістнадцятирічними цифрами: 'u0053' — буква S,
'u0416' — знак питання ?.
Випробуйте програму з командами
System.out.println('123'); System.out.println('346'); System.out.println('u0053'); System.out.println('u0416');
Символи зберігаються в форматі типу char (див. нижче).
Примітка
Заглавні російські літери в кодуванні Unicode займають діапазон від 'u0410' — заглавна літера А, до
'u042F' — заглавна Я, прописні літери 'u0430' — а, до '044F' — я.
В якій би формі не записувалися символи, компілятор переводить їх в Unicode, включаючи і вихідний
текст програми.
Зауваження. Компілятор і виконуюча система Java працюють тільки з кодуванням Unicode.
Порада. Користуйтеся Unicode напряму лише у крайніх випадках
17
18. 2.5.4. Рядкові константи
Рядки символів поміщаються в лапки. Керуючі символи і коди записуються в рядках точно так же, з
оберненою похилою рискою, але без апострофів, і викликають ті ж самі дії. Рядки можуть
розташовуватися лише в одному рядку вихідного кода, не можна відкриваючі лапки поставити в одному
рядку, а закриваючі — в наступному.
Ось декілька прикладів:
"Цей рядокnз переносом"
""Динамо" — Чемпіон!"
Примітка
Рядок символів не можна починати в одному рядку вихідного кода, а закінчувати в іншому.
Для рядкових констант визначена операція зєднання, позначається плюсом.
" Зєднання " + "рядків" дає в результаті рядок "Зєднання рядків".
2.6. Імена
Імена (names) змінних, класів, методів і інших обєктів можуть бути простими (загальна назва —
ідентифікатори (idenifiers)) і складними (qualified names). Ідентифікатори в Java складаються з так
званих літер Java (Java letters) і арабських цифр 0 - 9, причому першим символом ідентифікатора не може
бути цифра. (Дійсно, як розуміти запис 2е3: як число 2000,0 чи як імя змінної?) В число літер Java
обов’язково входять прописні і заглавні латинські літери, знак долара $ і знак 'підкреслювання _, а також
символи національних алфавітів.
Зауваження
Не вживайте в іменах знак долара. Компілятор Java використовує його для запису імен вкладених
класів.
Ось приклади правильних ідентифікаторів:
a1 , my_var, var3_5, _var, veryLongVarName, aName, theName, a2Vh36kBnMt456dX
В іменах краще не вживати прописну літеру l, котру легко сплутати з одиницею, і літеру О, котру легко
прийняти за нуль.
Не забувайте про рекомендації "Code Conventions".
В класі Character, що входить до складу Java API, є два методи, що перевіряють, чи придатний даний
символ для використання в ідентифікаторі: isJavaIdentifierStart(), перевіряє, чи являється символ літерою
Java, придатною для першої літери ідентифікатора і isJavaldentifierPart(), що виясняє, чи можна взагалі
вживати цей символ в ідентифікаторі. Випробуйте наступні дві програми і ви побачите який із ASCII
символів на що придатний.
class Test {
public static void main(String[] args) {
for (int i = 0; i < 256; i++)
{ if (Character.isJavaIdentifierStart((char)i))
18
19. System.out.println(Integer.toString(i)+ " " + (char)i + " is Java
Identifier Start Symbol" );
else
System.out.println(Integer.toString(i)+ " " + (char)i + " isn't Java
Identifier Start Symbol" );
}
}
}
і
class Test {
public static void main(String[] args) {
for (int i = 0; i < 256; i++)
{ if (Character.isJavaIdentifierPart((char)i))
System.out.println(Integer.toString(i)+ " " + (char)i + " is Java Identifier
Part Symbol" );
else
System.out.println(Integer.toString(i)+ " " + (char)i + " isn't Java Identifier
Part Symbol" );
}
}
}
Службові слова Java, такі як class, void, static, зарезервовані, їх не можна використовувати в якості
ідентифікаторів .
Складне імя (qualified name) — це декілька ідентифікаторів, розділених точками, без пробілів, наприклад,
нам уже зустрічалоя імя System.out.println.
Порада. Ознайомтеся з іншими методами класу Character по одноіменному файлу.
2.7. Примітивні типи даних і операції
2.7.1. Загальна класифікація
Всі типи вихідних даних, вбудованих в мову Java, діляться на дві групи: примітивні типи (primitive types)
и посилочні типи (reference types).
Посилочні типи діляться на масиви (arrays), класи (classes) і інтерфейси (interfaces).
Примітивних типів всього вісім. Їх можна розділити на логічний (інколи говорять булів) тип boolean і
числові (numeric).
До числових типів відносяться цілі (integеr) і дійсні (floating-point) типи.
Цілих типів пять: byte, short, int, long, char.
Символи можна використовувати всюди, де вживається тип int, тому JLS причисляє їх до цілих типів.
Наприклад, їх можна використовувати в арифметичних обчисленнях, скажімо, можна написати 2 + 'b', до
двійки буде доданий ASCII код 98 літери 'b' і в результаті додавання одержимо 100.
Нагадаємо, що в записі 2 + "b" плюс розуміється як зєднання рядків, двійка буде перетворена в рядок, в
результаті одержимо рядок "2b".
19
20. Дійсних типів два: float і double. Оскільки по імені змінної неможливо визначити її тип, всі змінні
обовязково повинні бути описані перед їх використанням. Описання полягає в тому, що записується ім’я
типа, потім, через пробіл, список імен змінних, розділених комою. Для всіх або деяких змінних можна
вказати початкові значення після знака рівності, котрими можуть бути будь-які константні вирази того ж
типа. Описання кожного типу закінчується точкою з комою. В програмі може бути скільки завгодно
описань кожного типу.
Зауваження для спеціалістів
Java — мова зі строгою типізацією (strongly typed language).
Розберемо кожний тип детальніше.
2.7.2. Логічний тип
Значення логічого типа boolean виникають в результаті різних порівнянь, вроді 2 > 3, і використовуються,
головним чином, в умовних операторах і операторах циклів. Логічних значень всього два: true (істина) і
false (хиба). Це службові слова Java. Описання змінних цього типа виглядає так:
boolean b = true, bb = false, bool2;
Над логічними даними можна виконувати операції присвоювання, наприклад, bool2 = true, в тому числі й
сумісні з логічними операціями; порівняння на рівність b == bb і на нерівність b != bb, а також логічні
операції.
2.7.3. Логічні операції
Логічні операції:
• заперечення (NOT) ! (позначається знаком оклику);
• кон’юнкція (AND) & (амперсанд);
• диз’юнкція (OR) | (вертикальна риска);
• виключне АБО (XOR) ^ (каре).
Вони виконуються над логічними даними, їх результатом буде також логічне значення true або false.
Нагадаємо таблицю логічних операцій.
Таблиця 1.1. Логічні операції
b1 b2 !b1 b1&b2 b1|b2 b1^b2
true true false true true false
true false false false true true
false true true false true true
false false true false false false
Словами ці правила можна виразити так:
• заперечення змінює значення істинності;
20
21. • конюнкція істинна, тільки якщо обидва операнди істинні;
• дизюнкція хибна, тільки якщо обидва операнди хибні;
• виключне АБО істинно, тільки якщо значення операндів різні.
Зауваження
Якби Шекспір був програмістом, то фразу "То be or not to be" він написав би так: 2b | ! 2b.
Крім перечислених чотирьох логічних операцій єсть ще дві логічні операції скороченого обчислення:
• скорочена конюнкція (conditional-AND) &&;
• скорочена дизюнкція (conditional-OR) ||.
Здвоєні знаки амперсанда і вертикальної риски слід записувати без пробілів.
Правий операнд скорочених операцій обчислюється тільки в тому випадку, якщо від нього залежить
результат операції, тобто якщо лівий операнд конюнкції має значення true, або лівий операнд дизюнкції
має значення false.
Це правило дуже зручне і вправно застосовується , наприклад, можна записувати вирази (n != 0) && (m/n
> 0.001) або (n == 0) || (m/n > 0.001) не боячись ділення на нуль.
Зауваження
Практично завжди в Java використовуються якраз скорочені логічні операції.
2.7.4. Цілі типи
Специфікація мови Java, JLS, визначає розрядність (кількість байтів, що відводяться для зберігання
значень типа в оперативній памяті) і діапазон значень кожного типа. Для цілих типів вони наведені в табл.
1.2.
Таблиця 1.2. Цілі типи
Тип Розрядність
(байт)
Діапазон
byte 1 от -128 до 127
short 2 от -32768 до 32767
int 4 от -2147483648 до 2147483647
long 8 от -9223372036854775808 до 9223372036854775807
char 2 від 'u0000' до 'uFFFF' , в десятковій формі від 0 до 65535
Між іншим, для Java розрядність не стільки важлива, на деяких комп’ютерах вона може відрізнятися від
наведеної в таблиці, а от діапазон значень повинен витримуватись беззастережно.
21
22. Хоча тип char займає два байти, в арифметичних обчисленнях він працює як тип int, йому виділяється 4
байти, два старших байти заповнюються нулями.
Приклади виздначення змінних цілих типів:
byte b1 = 50, b2 = -99, bЗ;
short det = 0, ind = 1;
int i = -100, j = 100, k = 9999;
long big = 50, veryBig = 2147483648L;
char c1 = 'A', c2 = '?', newLine = 'n';
Цілі типи зберігаються в двійковому вигляді з додатковим кодом. Останнє означає, що для від’ємних
чисел зберігається не їх двійкове представлення, а доповняльний код цього двійкового представлення.
Доповняльний же код отримують так: в двійковому представленні всі нулі замінюються на одиниці, а
одиниці на нулі, після чого до результату прибавляється одиниця, розуміється, в двійковій арифметиці.
Наприклад, значення 50 змінної b1, визначеної вище, буде зберігатися в одному байті з вмістом
00110010, а значення -99 змінної b2 — в байті із вмістом, котрий обчислюємо так: число 99 переводимо в
двійкову форму, одержимо 01100011, міняємо одиниці з нулями, одержуємо 10011100, і прибавляємо
одиницю, одержавши нарешті байт зі вмістом 10011101.
Зміст всіх цих складностей в тому, що додавання числа з його доповняльним кодом в двійковій
арифметиці дасть в результаті нуль, старший біт просто втрачається. Це означає, що в такій дивній
арифметиці доповняльний код числа являється протилежним йому числом, числом с оберненим знаком.
А це, в свою чергу, означає, що замість того, щоб відняти від числа А число В, можна до А прибавить
доповняльний код числа В. Таким чином, операція віднімання виключається із набору машинних
операцій.
Над цілими типами можна виконувати масу операцій. Їх набір визначився в мові С, він виявився зручним і
кочує з мови в мову майже без змін. Особливості застосування цих операцій в мові Java показані на
прикладах.
2.7.5. Операції над цілими типами
Всі операції, котрі виконуються над цілими числами, можна розділити на наступні групи.
2.7.5.1. Арифметичні операції
До арифметичних операцій відносяться:
• додавання + (плюс);
• віднімання - (дефіс);
• множення * (зірочка);
• ділення / (похила риска — слеш);
• остача від ділення (ділення по модулю) % (процент);
• інкремент (збільшення на одиницю) ++;
• декремент (зменьшення на одиницю) - -
Між спареним плюсами і мінусами не можна залишати пробіли. Додавання, віднімання і множення цілих
значень виконується як звичайно, а от ділення цілих значень в результаті дає знову ціле (так зване "ціле
ділення"), наприклад, 5/2 дасть в результаті 2, а не 2.5, а 5/(-3) дасть -1. Дробова частина попросту
відкидається, відбувається урізання частки. Це спочатку дивує, а потім виявляється зручним для урізання
чисел.
Зауваження
22
23. В Java прийнято цілочисельне ділення.
Це дивне для математики правило природнє для програмування: якщо обидва операнди мають один і
той же тип, то і результат має той же тип. Досить написати 5/2.0 або 5.0/2 або 5.0/2.0 і одержимо 2.5 як
результат ділення дійсних чисел.
Операція ділення по модулю визначається так: а % b = а - (а / b) * b; наприклад, 5%2 дасть в результаті
1, а 5% (-3) дасть 2, тому що 5 = (-3) * (-1) + 2, але (-5)%3 дасть -2, оскільки -5 = 3 * (-1) - 2.
Операції інкремент і декремент означають збільшення або зменшення значення змінної на одиницю і
застосовуються тільки для змінних, а не для констант чи виразів, не можна написати 5++ або (а + b)++.
Наприклад, після приведених вище описань i++ дасть -99, a j—- дасть 99.
Цікаво, що ці операції можна записати і перед змінною: ++i, - - j. При першій формі запису (постфіксній) у
виразі приймає участь старе значення змінної і лише потім відбувається збільшення чи зменшення її
значення. При другій формі запису (префіксній) спочатку зміниться змінна і її нове значення буде
приймати участь у виразі.
Наприклад, після приведених вище описань, (k++) + 5 дасть в результаті 10004, а змінна k прийме
значення 10000. Але в тій же ситуації (++k) + 5 дасть 10005, а змінна k стане рівною 10000.
2.7.5.2. Приведення типів
Результат арифметичної операції має тип int, крім того випадку, коли один із операндів єсть типу long. В
цьому випадку результат буде типу long.
Перед виконанням арифметичної операції завжди відбувається підвищення (promotion) типів byte, short,
char. Вони перетворюються в тип int, а може бути, і в тип long, якщо інший операнд має тип long. Операнд
типу int підвищується до типу long, якщо інший операнд є типу long. Звичайно, числове значення
операнда при цьому не змінюється.
Це правило приводить інколи до неочікуваних результатів. Спроба скомпілювати просту програму,
представлену в лістинзі 1.3, приведе до повідомлення компілятора про помилку. Перевірте!
Лістинг 2.3. Невірне визначення змінної
class InvalidDef{
public static void main (String [] args) {
byte b1 = 50, b2 = -99;
short k = b1 + b2; // Невірно! '
System.out.println("k=" + k);
}
}
Це повідомлення означає, що в файлі InvalidDef.java, в рядку 4, виявлена можлива втрата точності
(possible loss of precision). Потім приводиться виявлений (found) і потрібний (required) типи, виводиться
рядок, в якому виявлена (а не зроблена) помилка, і відмічається символ, при аналізі якого знайдена
помилка. В кінці вказано загальне число виявлених (а не зроблених) помилок (1 error).
В таких випадках треба виконувати явне приведення типу. В даному випадку це буде звуження
(narrowing) типа int до типа short. Воно здійснюється операцією явного приведення, котра записується
перед потрібним значенням у вигляді імені типа в дужках. Визначення
short k = (short)(b1 + b2);
23
24. буде вірним.
Звуження відбувається просто відкиданням старших бітів, що необхідно враховувати для великих
значень. Наприклад, визначення
byte b = (byte) 300;
дасть змінній b значення 44. Дійсно, в двійковому представленні числа 300, рівному 100101100,
відкидається старший біт і получається 00101100. Таким же чином можна привести і явне розширення
(widening) типу, якщо в цьому єсть необхідність.
Якщо результат цілої операції виходить за діапазон свого типа int або long, то автоматично відбувається
приведення по модулю, рівному довжині цього діапазона, і обчислення продовжуються, переповнення
ніяк не відмічається.
Зауваження
В мові Java немає цілочисленого переповнення.
2.7.5.3. Операції порівняння
В мові Java єсть шість звичайних операцій порівняння цілих чисел по величині:
• більше >;
• менше <;
• більше або дорівнює >=;
• менше або дорівнює <=;
• рівно ==;
• не рівно !=.
Спарені символи записуються без пробілів, їх не можна переставлять місцями, запис => будет невірним.
Результат порівняння — логічне значення: true, в результаті, наприклад, порівняння 3 != 5; або false,
наприклад, в результаті порівняння 3 == 5.
Для запису складних порівнянь слід застосовувати логічні операції. Наприклад, в обчисленнях часто
приходиться робити перевірку типу а < х < b. Подібний запис в мові Java приведе до повідомлення про
помилку, оскільки перше порівняння, а < х, дасть true або false, a Java не знає, більше це, ніж b, чи
менше. В даному випадку слід написати вираз (а < х) && (х < b), причому тут дужки можна опустить,
написати просто а < х && х < b, але про це трохи пізніше.
2.7.5.4. Побітові операції
Інколи приходиться змінювати значення окремих бітів в цілих даних. Це виконується за допомогою
побітових (bitwise) операцій шляхом накладання маски. В мові Java єсть чотири побітові операції:
• доповнення (complement) ~ (тильда);
• побітова конюнкція (bitwise AND) &;
• побітова дизюнкція (bitwise OR) |;
• побітове виключне АБО (bitwise XOR) ^.
Вони виконуються порозрядно, після того як обидва операнди будуть приведені до одного типу int або
long, так же як і для арифметичних операцій, а значить, і до однієї розрядності. Операції над кожною
парою бітів виконуються згідно табл. 1.3.
24
25. В нашому прикладі b1 == 50, двійкове представлення 00110010, b2 == -99, двійкове представлення
10011101. Перед операцією відбувається підвищення до типу int. Одержимо представлення із 32-х
розрядів для b1 — 0...00110010, для b2 — 1...10011101. В результаті побітових операцій одержимо:
• ~b2 == 98, двійкове представлення 0...01100010;
• b1 & b2 == 16, двійкове представлення 0...00010000;
• b1 | b2 == -65, двійкове представлення 1...10111111;
• b1 ^ b2 == -81, двійкове представлення 1...10101111.
Двійкове представлення кожного результату займає 32 біти.
Зверніть увагу, що доповнення ~х завжди еквівалентно (-x) -1.
Таблиця 1.3. Побітові операції
nl n2 ~nl nl & n2 nl | n2 nl ^ n2
1
1
0
0
1
0
1
0
0
0
1
1
1
0
0
0
1
1
1
0
0
1
1
0
Те, що побітові операції маютиь ті ж самі позначення, що і логічні операції, не повинно викликати
непорозуміння. Перші виконуються над цілими числами в двійковій формі, а другі над булевими змінними.
2.7.5.5. Зсуви
В мові Java єсть три операції зсуву двійкових розрядів:
• зсув вліво <<;
• зсув вправо >>;
• беззнаковий зсув вправо >>>.
Ці операції своєрідні тим, що лівий і правий операнди в них мають різний зміст. Зліва стоїть значення
цілого типу, а права частина показує, на скільки двійкових розрядів зсувається значення, що стоїть в лівій
частині.
Наприклад, операція b1<< 2 зсуне вліво на 2 розряди попередньо підвищене значення 0...00110010
змінної b1, що дасть в результаті 0...011001000, десяткове 200. Звільнені справа розряди заповняються
нулями, ліві розряди, що знаходяться за 32-м бітом, втрачаються.
Операція b2 << 2 зсуне підвищене значення 1...10011101 на два розряди вліво. В результаті одержимо
1...1001110100, десяткове значення -396.
Зверніть увагу, що зсув вліво на n розрядів еквівалентний множенню числа на 2 в степені n.
Операція b1 >> 2 дасть в результаті 0...00001100, десяткове 12, а b2 >> 2 — результат 1..11100111,
десяткове -25, тобто зліва зсувається старший біт, праві біти втрачаються. Це так званий арифметичний
25
26. зсув.
Операція беззнакового зсуву у всіх випадках ставить зліва на звільнені місця нулі, здійснюючи логічний
зсув. Але внаслідок попереднього підвищення це має ефект лише для декількох старших розрядів
від’ємних чисел. Так, b2 >>> 2 має результатом 001...100111, десяткове число 1 073 741 799.
Якщо ж ми хочемо одержати логічний зсув початкового значення змінної b2, тобто, 0...00100111, треба
попередньо накласти на b2 маску, обнуливши старші біти: (b2 & 0XFF) >>> 2.
Зауваження
Будьте обережні при використанні зсувів вправо. Вам зараз важко уявити, в яких програмах може
знадобитися зсув. Але він досить вживаний в програмуванні. Якщо ви знайдете його у якійсь програмі, то
не полініться розібратися в його механізмі і доцільності застосування.
2.7.6. Дійсні типи
Дійсних типів у Java два: float і double. Вони характеризуються розрядністю, діапазоном значень і точністю
представлення, що відповідає стандарту IEEE 754-1985 з деякими змінами. До звичайних дійсних чисел
додаються ще три значення»
1. Додатня нескінченість, що виражається константою POSITIVE_INFINITY . Вона виникає при
переповненні додатнього значення, наприклад, в результаті операції множення 3.0*6е307.
2. Відємна нескінченість NEGATIVE_INFINITY.
3. "Не число", що записується константою NaN (Not a Number). Вона виникає при діленні дійсного числа
на нуль або при множенні нуля на нескінченість.
Крім того, стандарт розрізняє додатній і відємний нуль, що виникають при діленні на нескінченість
відповідного знака, хоча порівняння 0.0 == -0.0 дає true.
Операції з нескінченістю виконуються по звичайним математичним правилам.
У всьому останньому дійсні типи — це звичайні, дійсні значення, до яких можна застосувати всі
арифметичні операції і порівняння, перечислені для цілих типів. Характеристики дійсних типів приведені в
табл. 1.4.
Знавцям C/C++
В мові Java остача від ділення %, інкремент ++ і декремент — застосовується і для дійсних чисел.
Таблиця 1.4. Дійсні типи
Тип Розрядність Діапазон Точність
26
27. float 4 3,4е-38 < |х| < 3,4е38 7—8 цифр
double 8 1,7е-308<|х|<1,7е308 17 цифр
Приклади визначення дійсних типів:
float х = 0.001, у = -34.789;
double 21 = -16.2305, z2;
Оскільки до дійсних типів застосовуються всі арифметичні операції і порівняння, цілі і дійсні значення
можна змішувати в операціях. При цьому правило приведення типів доповнюється такими умовами:
• якщо в операції один операнд має тип double, то і другий приводиться до типу double;
• якщо один операнд має тип float, то і другий приводиться до типу float;
• в противному випадку діє правило приведення цілих значень.
2.7.7. Операції присвоювання
Просто операція присвоєння (simple assignment operator) записується знаком рівності =, зліва від котрого
стоїть змінна, а справа вираз, сумісний з типом змінної: х = 3.5, у = 2 * (х - 0.567) / (х + 2), b = х < у, bb = х
>= у && b.
Операція присвоювання діє так: вираз, що стоїть після знака рівності, обчислюється і приводиться до типу
змінної, що стоїть зліва від знака рівності. Результатом операції буде приведене значення правої частини.
Операція присвоювання має ще одну, побічну, дію: змінна, що стоїть зліва, одержує приведене значення
правої частини, старе її значення втрачається.
В операції присвоювання ліва і права частини нерівноправні, не можна написати 3.5 = х. Після операції х
= у зміниться змінна х, ставши рівною у, а після у = х зміниться у.
Крім простої операції присвоювання єсть ще 11 складних операцій присвоювання (compound assignment
operators): +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=; >>>=. Символи записуються без пробілів, не можна
переставляти їх місцями.
Всі складні операції присвоювання діють по одній схемі:
х ор= а еквівалентно х = (тип х), тобто (х ор а).
Нагадаємо, що змінна ind типу short визначена у нас із значенням 1. Присвоювання ind +=7.8 дасть в
результаті число 8, те ж саме значення одержить і змінна ind. Ця операція еквівалентна простій операції
присвоювання ind = (short)(ind + 7.8).
Перед присвоюванням, при необхідності, автоматично відбувається приведення типу. Тому:
byte b = 1;
b = b + 10; // Помилка!
27
28. b += 10; // Правильно!
Перед додаванням b + 10 відбувається підвищення b до типа int, результат додавання теж буде тип int і,
в першому випадку не може бути присвоєний змінній b без явного приведення типа. В другому випадку
перед присвоюванням відбудеться звуження результату додавання до типу byte.
2.7.8. Умовна операція
Ця своєрідна операція має три операнди. Спочатку записується довільний логічний вираз, тобто такий,
що має результатом true або false, потім ставиться знак питання, потім два довільних вирази, розділених
двокрапкою, наприклад,
х < 0 ? 0 : х
х > у ? х - у : х + у
Умовна операція виконується так. Спочатку обчислюється логічний вирз. Якщо одежано значення true, то
обчислюється перший вираз після знака ? і його значення буде результатом всієї операції. Останній
вираз при цьому не обчислюється. Якщо ж отримане значення false, то обчислюється тільки останній
вираз, його значення буде результатом операції.
Це дозволяє написати n == 0 ? : m / n не боячись ділення на нуль. Умовна операція спочатку здається
дивною, але вона дуже зручна для запису невеликих розгалужень.
2.8. Вирази
Із констант і змінних , операцій над ними, викликів методів і дужок складаються вирази (expressions).
Розуміється, всі елементи виразу повинні бути сумісними, не можна написати, наприклад, 2 + true. При
обчисленні виразу виконуються чотири правила:
1. Операції одного пріоритету обчислюються зліва направо: х + у + z обчислюється як (х + у) + z. Виняток:
операції присвоювання виконуються справа наліво: х = у = z обчислюється як х = (у = z).
2. Лівий операнд обчислюється раніше правого.
3. Операнди повністю обчислююються перед виконанням операції.
4. Перед виконанням складної операції присвоювання значення лівої частини зберігається для
використання в правій частині.
Наступні приклади показують особливості застосування перших трьох правил. Нехай
int а = 3, b = 5;
Тоді результатом виразу b + (b = 3) буде число 8; але результатом виразу (b = 3) + b будет число 6. Вираз
b += (b = 3) дасть в результаті 8, тому що обчислюється як перший із наведених вище виразів.
Знавцям C/C++
Більшість компіляторів мови C++ у всіх цих випадках вичислять значення 8.
Четверте правило можна продемонструвати так. При тих же визначеннях а і в результаті обчислення
виразу b+= а += b += 7 одержимо 20. Хоча операції присвоювання виконується справа наліво і після
першої, правої, операції значення b становиться рівним 12, але в останньому, лівому, присвоюванні
28
29. приймає участь старе значення b , рівне 5. А в результаті двох послідовних обчислень а += b += 7; b += а;
одержимо 27, оскільки в другому виразі приймає участь уже нове значення змінної b, рівне 12.
Знавцям C/C++
Більшість компіляторів C++ в обох випадках обчислять 27.
Вирази можуть мати складний і заплутаний вигляд. В таких випадках виникає питання про пріоритет
операцій, про те, які операції будуть виконані в першу чергу. Природно, множення і ділення виконуються
раніше додавання і віднімання.
Решта правил перечислені в наступному розділі.
Порядок обчислення виразу завжди можна відрегулювати дужками, їх можна ставити скільки завгодно.
Але тут важливо зберігати "золоту середину". При великій кількості дужок знижується наочність виразу і
легко помилитися в розстановці дужок. Якщо вираз з дужками корректний, то компілятор може
відслідкувати тільки парність дужок, але не правильність їх розстановки.
2.9. Пріоритет операцій
Операції перечислні в порядку спадання пріоритета. Операції в одному рядку мають однаковий пріоритет.
1. Постфіксні операції ++ і - -
2. Префіксні операції ++ і - -, доповнення ~ і заперечення !.
3. Приведення типу (тип).
4. Множення *, ділення / і знаходження решти %.
5. Додавання + і віднімання -.
6. Здвиги <<, >>, >>>.
7. Порівняння >, <, >=, <=.
8. Порівняння ==, !=.
9. Побітова конюнкція &.
10. Побітове виключне АБО ^.
11. Побітова дизюнкція | .
12. Конюнкція &&.
13. Дизюнкція | | .
14. Умовна операція ?: .
15. Присвоювання =, +=, -=, *=, /=, %=, &=, ^=, |=, <<, >>, >>>.
Тут перечислені не всі операції мови Java, список буде доповнюватися по мірі вивчення нових операцій.
29
30. 2.10. Оператори
Як ви знаєте, будь-який алгоритм призначений для виконання на комп’ютері, можна розробити,
використовуючи лише лінійні обчислення, розгалуження і цикли.
Записати його можна в різних формах: у вигляді блок-схеми, на псевдокоді, на звичайній мові, як ми
записуємо кулінарні рецепти, або як-небудь ще .
Всяка мова програмування повинна мати свої методи запису алгоритмів. Вони називаються операторами
(statements) мови. Мінімальній набір операторів повинен містити оператор для запису лінійних обчислень,
умовний оператор для запису розгалужень і оператор циклу.
Звичайно склад операторів мови програмування ширше: для зручності записи алгоритмів у мову
включаються декілька операторів циклу, оператор варіанта, оператори переходу, оператори описування
обєктів.
Набір операторів мови Java включає:
• оператори описування змінних і інших обєктів (вони були розглянуті вище);
• оператори-вирази;
• оператори присвоювання;
• умовний оператор if;
• три оператори циклу while, do-while, for;
• оператор варіанта switch;
• Оператори переходу break, continue і return;
• блок {};
• пустий оператор — просто крапка з комою.
Тут приведений не весь набір операторів Java, він буде доповнюватися по мірі вивчення мови.
Зауваження
В мові Java немає оператора goto.
Всякий оператор закінчується крапкою з комою.
Можна поставити крапку з комою в кінці любого виразу, і він стане оператором (expression statement). Але
це має зміст тільки для операцій присвоювання, інкременту , декременту і виклику методів. В решті
випадків це не має змісту, тому що обчислене значення виразу буде втрачено.
Знавцям Pascal
Крапка з комою в Java не розділяє оператори, а являється частиною оператора.
Лінійне виконання алгоритма забезпечується послідовним записом операторів. Перехід з рядка на рядок у
вихідному тексті не має ніякого значення для компілятора, він здійснюється тільки для наочності і
читабельності тексту.
2.10.1. Блок
Блок заключає в собі нуль або декілька операторів з метою використовувати їх як один оператор в тих
місцях, де по правилах мови можна записати тільки один оператор. Наприклад, {х = 5; у = ?;}. Можна
записати і пустий блок, просто пару фігурних дужок {}.
Блоки операторів часто використовуються для обмежння області дії змінних і просто для легшого читання
30
31. тексту програми.
2.10.2. Оператори присвоювання
Крапка з комою в кінці будь-якої операції присвоювання перетворює її в оператор присвоювання. Побічна
дія операції — присвоювання — становиться в операторі основним.
Різниця між операцією і оператором присвоювання носить лише теоретичний характер. Присвоювання
частіше застосовується як оператор, а не як операція.
2.10.3. Умовний оператор
Умовний оператор (if-then-else statement) у мові Java записується так:
if (логічний вираз) оператор1 else оператор2
і діє наступним чином. Спочатку обчислюється логічний вираз. Якщо результат true, то діє оператор1 і на
цьому дія умовного оператора завершуєтсья, оператор2 не діє, далі буде виконуватися наступний за if
оператор. Якщо результат false, то діє оператор2, при цьому оператор1 взагалі не виконується.
Умовний оператор може бути скороченим (if-then statement):
if (логвир) оператор1
і у випадку false не виконується нічого.
Синтаксис мови не дозволяє записувати декілька операторів ні у гілці then, ні у гілці else. При необхідності
створюється блок операторів у фігурных дужках. "Code Conventions" рекомендує завжди використовувати
фігурні дужки і розташовувати оператор в декількох рядках з відтступами, як в наступному прикладі:
if (а < х) {
х = а + b; } else {
х = а - b;
}
Це полегшує додавання оператороі в кожну гілку при зміні алгоритма. Ми не будемо строго слідувати
цьому правилу, щоб не збільшувати розмір тексту.
Дуже часто одним із операторів являється знову умовний оператор, наприклад:
if (n == 0) {
sign = 0;
} else if (n < 0) {
sign = -1;
} else {
sign = 1;
}
При цьому може виникнути така ситуація ("dangling else"):
int ind = 5, х = 100;
if (ind >= 10) if (ind <= 20) x = 0; else x = 1;
Збереже змінна х значення 100 чи стане рівною 1? Тут необхідно вольове рішення, і спільне для
31
32. більшості мов, в тому числі і Java. Правило таке: гілка else відноситься до найближчої зліва умови if, що
не має своєї гілки else. Тому в нашому прикладі змінна х залишиться рівною 100.
Змінити цей порядок можна за допомогою блоку:
if (ind > 10) {if (ind < 20) x = 0; else x = 1;}
Взагалі не варто використовувати складні вкладені умовні оператори. Перевірка умов займає багато часу.
По можливості краще скористатися логічними операціями, наприклад, в нашому прикладі можна
написати
if (ind >= 10 && ind <= 20) х = 0; else х = 1;
Користуйтеся тим, що ви узнали про пропозиції в Дискретній Математиці. Одну з них можна замінити на
еквівалентну, більш простішу для розуміння.
В лістинзі 1.4 обчислюються корені квадратного рівняння ах2 + bх + с = 0 для любих коэфіцієнтів, в тому
числі і нульових.
Лістинг 2.4. Обчислення коренів квадратного рівняння
class QuadraticEquation{
public static void main(String[] args){
double a = 0.5, b = -2.7, с = 3.5, d, eps=1e-8;
if (Math.abs(a) < eps)
if (Math.abs(b) < eps)
if (Math.abs(c) < eps) // Всі коефіцієнти рівні нулю
System.out.println("Розвязок —будь-яке число");
else
System.out.println("Розвязків немає");
else
System.out.println("xl = x2 = " +(-c / b) ) ;
else { // Коэфіцієнти не рівні нулю
if((d = b**b - 4*a*c)< 0.0){ // Комплексні корені
d = 0.5 * Math.sqrt(-d) / a;
a = -0.5 * b/ a;
System.out.println("xl = " +a+ " +i " +d+
",x2 = " +a+ " -i " +d);
} else {
// Дійсні корені
d =0.5 * Math.sqrt(d) / a;
a = -0.5 * b / a;
System.out.println("x1 = " + (a + d) + ", x2 = " +(a - d));
}
}
}
}
В цій програмі використані методи обчислення модуля abs() і квадратного кореня sqrt() з дійсного числа із
вбудованого в Java API класу Math. Оскільки всі обчислення з дійсними числами виконуються наближено,
ми вважаємо, що коефіцієнт рівняння дорівнює нулю, якщо його модуль менше 0,00000001. Зверніть
увагу на те, як в методе println() використовується зєднання рядків, і на те, як операція присвоювання при
обчисленні дискримінанта вкладена в логічний вираз. Прослідкуйте складну ієрархію операторів if ... else.
Якщо це вам здасться дуже заплутаним, переробіть програму використовуючи логічнні функції.
"Продвинутим" користувачам
32
33. Вам уже хочеться вводити коэфіцієнт а, b і с прямо з клавіатури? Будь ласка, користуйтеся методом
System. in. read (byte [ ] bt), але майте на увазі, що цей метод записуєт введені цифри в масив байтів bt в
кодуванні ASCII, в кожний байт по одній цифрі. Масив байтів потім треба перетворити в дійсне число,
наприклад, методом Double(new String(bt)).doubleValue() . Незрозуміло? Але це ще не все, треба
опрацювати виключні ситуації, які можуть виникнути при введенні. Пізніше, можливо, ми розглянемо це
питання більш детально.
2.10.4. Оператори циклу
2.10.4.1. Оператор while
Основний оператор циклу — оператор while — виглядає так:
while (логвир) оператор
Спочатку обчислюється логічний вираз логвир; якщо його значення true, то виконується оператор, що
утворює цикл. Потім знову обчислюється логвир і діє оператор, і так до тих пір, поки не получиться
значення false. Якщо логвир з самого початку рівний false, то оператор не буде виконуватися ні разу.
Попередня перевірка забезпечує безпеку виконанняя циклу, дозволяє уникнути переповнення, ділення на
нуль і інші неприємності. Тому оператор while являеться основним, а в деяких мовах і єдиним оператором
циклу.
Оператор в циклі може бути і пустим, наприклад, наступний фрагмент коду:
int i = 0;
double s = 0.0;
while ((s += 1.0 / ++i) < 10);
обчислює суму членів гармонічного ряду до тих пір, поки вона досягне значення 10. Такий стиль
характерний для мови С. Не варто ним захоплюватися, щоб не перетворити текст програми в шифровку,
на яку ви самі через пару тижнів будете дивитися зі здивуванням.
Можна організувати і нескінчений цикл:
while (true) оператор
Звичайно, із такого циклу треба передбачити якийсь вихід, наприклад, оператором break, як в лістинзі 1.5.
В противному випадку програма зациклиться, і вам прийдеться закінчувати її виконання "комбінацією з
трьох пальців" <Ctrl>+<Alt>+<Del> у MS Windows 95/98/ME, або через Task Manager у Windows NT/2000.
Якщо в цикл треба включить декілька операторів, то слід створити блок операторів {}.
2.10.4.2. Оператор do-while
Другий оператор цикла — оператор do-while — має вигляд do оператор while (логвир)
Тут спочатку виконується оператор, а потім відбувається обчислення логічного виразу логвир. Цикл
виконується, поки логвир залишається рівним true.
Знавцям Pascal
В циклі do-while перевіряються умови продовження, а не закінчення циклу.
Суттєва різниця між цими двома операторами циклу тільки в тому, що в циклі do-while оператор
обовязково виконується хоча б один раз.
33
34. Наприклад, нехай задана якась функція f(x), що має на відрізку [0; b] рівно один корінь. В лістинзі 1.5
приведена програма, що обчислює цей корінь наближено методом ділення пополам (бісекції, дихотомії).
Лістинг 2.5. Знаходження кореня нелінійного рівняння методом бісекції
class Bisection {
static double f(double x){
return x*x*x - 3*x*x +3; // Або щось інше
}
public static void main(String[] args){
double a = 0.0, b = 1.5, c, y, eps = 1e-8;
do{
c = 0.5 *(a + b); y = f(c);
if (Math.abs(y) < eps) break;
// Корінь знайдено. Виходимо із циклу
//Якщо на кінцях відрізка [а; c] функція має різні знаки:
if (f (a) * y < 0.0) b = c;
// Значить, корінь тут. Переносимо точку b в точку c
//В противному випадку:
else a = c;
// Переносимо точку а в точку с
// Продовжуємо, доки відрізок [а; b] не стане малим
} while (Math. abs (b -a) >= eps);
System.out.println("x = " +c+ ", f(" +c+ ") = " +y) ;
}
}
Клас Bisection складніший за попередні приклади: в ньому крім метода main () єсть іще метод обчисленя
функції f(x). Тут метод f() дуже простий: він обчислює значення многочлена і повертає його в якості
значення функції, причому все це виконується одним оператором:
return вираз
В методі main() появився ще один новий оператор break, який просто закінчує виконання циклe, якщо ми
по щасливій випадковості наткнулися на наближене значення кореня. Уважний читач помітив і появу
модифікатора static в оголошенні метода f(). Він необхідний тому, що метод f() викликється із статичнго
метода main().
2.10.4.3. Оператор for
Третій оператор циклe — оператор for — виглядає так:
for (списокВир1; логВир; списокВир2) оператор
Перед виконанням циклу обчислюється список виразів списокВир1. Це нуль або декілька виразів,
перечислених через кому. Вони обчислюються зліва направо, і в наступному виразі уже можна
використовувати результат попереднього виразу. Як правило, тут задаються початкові значення змінних
циклe.
Потім обчислюється логічний вираз логвир. Якщо він істинний, true, то діє оператор, потім обчислюється
зліва направо вирази із списку виразів списокВир2. Далі знову перевіряється логвир. Якщо він істинний,
то виконується оператор і списокВир2 і т. д. Як тільки логвир стане рівним false, виконання циклу
закінчується.
Коротше кажучи, виконується послідовність операторів
34
35. списокВир1; while (логВир){
оператор
списокВир2; }
з тим виключенням, що, коли оператором в циклі являється оператор continue, то списокВир2 все-таки
виконується.
Замість списокВир1 може стояти одне визначення змінних обовязково з початковим значенням. Такі
змінні діють лише в межах цього цикла.
Будь-яка частина оператора for може бути відсутня: цикл може бути пустим, вираз в заголовку теж, при
цьому крапки з комою зберігаються. Можна задати нескінчений цикл:
for (;;) оператор
В цьому випадку в тілі циклу слід передбачити який-небудь вихід.
Хоча в операторі for закладені великі можливості, використовується він, головним чином, для
перечислень, коли їх число відомо, наприклад, фрагмент коду ,
int s=0;
for (int k = 1; k <= N; k++) s += k * k;
// Тут змінна k вже невідома
обчислює суму квадратів перших N натуральних чисел.
2.10.5. Оператор continue і мітки
Оператор continue використовується тільки в операторах циклу. Він має дві форми. Перша форма
складається тільки із слова continue і здійснює негайний перехід до наступної ітерації циклу. В черговому
фрагменті коду оператор continue дозволяє обійти ділення на нуль:
for (int i = 0; i < N; i++){
if (i == j) continue;
s += 1.0 / (i - j);
}
Друга форма містить мітку:
continue мітка
мітка записується, як і всі ідентифікатори, із літер Java, цифр і знака підкреслювання, але не вимагає
ніякого описання. Мітка ставиться перед оператором або відкриваючій фігурній дужці і відокремлюється
від них двокрапкою. Так виходить помічений оператор або помічений блок.
Знавцям Pascal
Мітка не вимагає описання і не може починатися з цифри.
Друга форма використовується тільки у випадку декількох вкладених циклів для негайного переходу до
чергової ітерації одного із зовнішніх циклів, а саме, поміченого цикла.
35
36. 2.10.6. Оператор break
Оператор break використовується в операторах циклу і операторі варіанта для негайного виходу із цих
конструкцій.
Оператор break мітка
Наступна схема пояснює цю конструкцію.
Ml: { // Зовнішній блок
М2: { // Вкладений блок — другий рівень
М3: { // Третій рівень вкладеності...
if (щось трапилось) break M2;
// Якщо true, то тут нічого не виконується
}
// Тут теж нічого не виконується
}
// Сюди передається управління
}
Спочатку збиває в пантелику та обставина, що мітка ставиться перед блоком або оператором, а
управління передається за цей блок або оператор. Тому не варто захоплюватися оператором break з
міткою.
2.10.7. Оператор варіанта
Оператор варіанта switch організує розгалуження по декількох напрямках. Кожна гілка відмічається
константою або константним виразом якого-небудь цілого типа (крім long) і вибирається, якщо значення
певного виразу співпадає з цією константою. Вся конструкція виглядає так.
switch (цілВир){
case констВир1: оператор1
case констВир2: оператор2
. . . . .
case констВирN: операторN
default: операторDef
}
Вираз в дужках цілвир може бути типу byte, short, int, char, але не long. Цілі числа або цілочисельні
вирази, складені із констант, констВир теж не повинні мати тип long.
Оператор варіанта виконується так. Всі константні вирази обчислюються заздалегідь, на етапі компіляції, і
повинні мати відмінні один від одного значения. Спочатку обчислюються цілочисельний вираз цілвир.
Якщо він співпадає з однією із констант, то виконується оператор, помічений цією константою. Потім
виконуються ("fall through labels") всі наступні оператори, включаючи і операторDef, і робота оператора
варіант закінчується.
Якщо ж жодна константа не рівна значенню виразу, то виконується операторDef і всі наступні оператори.
Тому гілка default повинна записуватися останньою. Гілка default може бути відсутня, тоді в цій ситуації
оператор варіанта взагалі нічого не робить.
Таким чином, константи у варіантах case грають роль тільки міток, точок входу в оператор варіанта, а далі
виконуються всі інші оператори в порядку їх запису.
Знавцям Pascal
36
37. Після виконання одного варіанта оператор switch продовжує виконувати всі інші варіанти.
Частіше всього необхідно "пройти" тільки одну гілку операторів. В такому випадку використовується
оператор break, який зразу ж зупиняє виконання оператора switch. Може знадобитися виконати один і той
же оператор в різних гілках case. В цьому випадку ставимо декілька міток case підряд. Ось простий
приклад.
switch(dayOfWeek){
case 1: case 2: case 3: case 4:case 5:
System.out.println("Week-day");, break;
case 6: case 7:
System.out.println("Week-end"); break;
default:
System.out.printlnt("Unknown day");
}
Зауваження
He забувайте завершати варіанти оператором break.
2.11. Масиви
Як завжди в програмуванні массив — це сукупність змінних одного типу, що зберігаються в суміжних
комірках оперативної пам’яті.
Масиви в мові Java відносяться до посилкових типів і описуються своєрідно, але характерно для
посилкових типів. Описання проходить в три етапи.
Перший етап — оголошення (declaration). На цьому етапі визначається тільки змінна типу посилання
(reference) на масив, і містить тип масиву. Для цього записується ім’я типу елементів масиву,
квадратними дужками указується, що оголошується посилання на масив, а не проста змінна, і
перечислюються імена змінних типу посилання, наприклад,
double[] а, b;
Тут визначені дві змінні — посилання а і b на масиви типа double. Можна поставити квадратні дужки і
безпосередньо після імені. Це зручно робити серед визначень звичайних змінних:
int I = 0, ar[], k = -1;
Тут визначені дві змінні цілого типа i і k, і оголошено посилання на цілочисельний масив аг.
Другий етап — визначення (installation). На цьому етапі указується кількість елементів масиву, це
називається його довжиною, виділяється місце для масиву в оперативній пам’яті, змінна-посилання
одержує адресу масиву. Всі ці дії виконуються ще однією операцією мови Java — операцією new тип, що
виділяє місце в оперативній пам’яті для об’єкта указаного в операції типу і повертає в якості результату
адресу цього місця. Наприклад,
а = new double[5];
b = new double[100];
ar = new int[50];
Індекси масивів завжди починаються з 0. Масив а складається із пяти зміних а[0], а[1], , а[4]. Елемента
37
38. а[5] в масиві немає. Індекси можна задавати любими цілочисельними виразами, крім типа long,
наприклад, a[i+j], a[i%5], a[++i]. Виконуюча система Java слідкує за тим, щоб значення цих виразів не
виходили за межі довжини масива.
Третій етап — ініціалізація (initialization). На цьому етапі елементи масиву отримують початкові значення.
Наприклад,
а[0] = 0.01; а[1] = -3.4; а[2] = 2:.89; а[3] = 4.5; а[4] = -6.7;
for (int i = 0; i < 100; i++) b[i] = 1.0 /i;
for (int i = 0; i < 50; i++) ar[i] = 2 * i + 1;
Перші два етапи можна сумістити:
a = new double[5], b = new double[100];
int i = 0, ar[] = new int[50], k = -1;
Можна зразу задати і початкові значення, записавши їх в фігурних дужках через кому у вигляді констант
або константних виразів. При цьому навіть необов’язково указувати кількість елементів масиву, вона буде
рівною кількості початкових значень;
double[] а = {0.01, -3.4, 2.89, 4.5, -6.7};
Можна сумістити другий і третій етап:
а = new double[] {0.1, 0.2, -0.3, 0.45, -0.02};
Можна навіть створити безіменний масив, зразу ж використовуючи результат операції new, наприклад,
так:
System.out.println(new char[] {'H', 'e', '1', '1', 'o'});
Посилання на масив не являється частиною описаного масива, її можна перекинуть на другий масив того
ж типу операцією присвоювання. Наприклад, після присвоювання а = b обидві посилки а і b указують на
один і той же масив із 100 дійсних змінних типу double і містять одну і ту ж адресу.
Посилання може присвоїти "пусте" значення null, що не указує на жодну адресу оперативної памяті:
ar = null;
Після цього масив, на який указувало дане посилання, втрачаєтьсяся, якщо на нього не було других
посилань.
Крім простої операції присвоювання, з посиланнями можна виконувати ще тільки порівняння на рівність,
наприклад, а = b, і нерівність, а != b. При цьому співставляються адреси, що містяться в посиланнях, ми
можемо взнати, чи не посилаються вони на один и той же масив.
Зауваження для спеціалістів
Масиви в Java завжди визначаються динамічно, хоча посилання на них задаються статично.
Крім посилання на масив, для кожного масиву автоматично визначається ціла константа з одним і тим же
38
39. іменем length. Вона рівна довжині масива. Для кожного масиву імя цієї константи уточнюється іменем
масиву через точку. Так, після наших визначень, константа a.length рівна 5, константа b. length рівна 100,
a ar. length рівна 50.
Останній елемент масиву а можна записати так: a [a. length - 1], передостанній — a [a. length - 2] і т. д.
Елементи масиву звичайно перебираються в циклі виду:
double aMin = a[0], aMax = aMin;
for (int i = 1; i < a.length; i++){
if <a[i] < aMin) aMin = a[i];
if (a[i] > aMax) aMax = a[i];
}
double range = aMax - aMin;
Тут обчислюється діапазон значень масиву.
Елементи масиву — це звичайні змінні свого типу, з ними можна виконувати всі операції, допустимі для
цього типу:
(а[2] + а[4]) / а[0] і т. д.
Знавцям C/C++
Масив символів в Java не являється рядком, навіть якщо він закінчується нуль-символом ' u0000'.
2.12. Багатовимірні масиви
Елементами масиву у Java можуть бути знову масиви. Можна оголосити:
char[] [] с;
що еквівалентно
chart[] с[];
або
char с[][];
Потім визначаємо зовнішній масив:
с = new char[3][];
Становиться ясно, що с — масив, який складається з трьох элементів-масивів. Тепер визначимо його
елементи-масиви:
с[0] = new char[2];
с[1] = new char[4];
с[2] = new char[3];
Після цих визначень змінна с.length рівна 3, с[0] .length рівна 2,
39
40. c[l].length рівна 4 і с[2].length рівна 3.
Нарешті, задаємо початкові значення с [0] [0] = 'a', с[0][1] = 'r',
с[1][0] = 'г',с[1][1] = 'а',с[1][2] = 'у' і т.д.
Зауваження
Двомірний масив у Java не зобов’язаний бути прямокутним.
Описування можна скоротити:
int[] [] d = new int[3] [4];
А початкові значення задати так:
int[] [] d = {{I, 2, 3}, {4, 5, 6}};
В лістинзі 1.6 приведено приклад програми, яка обчислює перші 10 рядків трикутника Паскаля , заносить
їх в трикутний масив і виводить його елементи на екран.
Лістинг 2.6. Трикутник Паскаля
class PascalTriangle{
public static final int LINES = 10; // Так визначаються константи
public static void main(String[] args) {
int[][] p = new int [LINES] [];
p[0] = new int[1];
System. out. println (p [0] [0] = 1);
p[1] = new int[2];
p[1][0] = p[1][1] = 1;
System.out.println(p[1][0] + " " + p[1][1]);
for (int i = 2; i < LINES; i++){
p[i] = new int[i+1];
System.out.print((p[i][0] = 1) + " ");
for (int j = 1; j < i; j++)
System.out. print ( (p[i] [j] =p[i -1][j -1] + p[i-1][j]) + " ");
System. out. println (p [ i] [i] = 1);
}
}
}
Заключення
Уф-ф-ф!! Ось ви і подолали базові конструкції мови. Раз ви добралися до цього місця, значить, умієте уже
дуже багато. Ви можете написати програму на Java, налаштувати її, усунувши помилки, і виконати. Ви
здатні запрограмувати любий не занадто складний обчислювальний алгоритм, що опрацьовує числові
дані. Теперь можна перейти до питань створення складних виробничих програм. Такі програми вимагають
ретельного планування. Зробити це допомагає обєктно-орієнтоване програмування, до якого ми зараз і
переходимо.
Лабораторна робота 1. Базові поняття Java
1. Підготуйте простенькі програми для ілюстрації елементів мови Java розглянутих в пунктах
40
41. 2.5.1
2.5.3
2.5.4
2.6
2.7.2
2.7.3
2.7.4
2.7.5.1
2.7.5.2
2.7.5.3
2.7.5.4
2.7.5.5
2.7.6
2.7.7
2.7.8
2.8
2.9
2.10.1
2.10.2
2.10.3
2.10.4.1
2.10.4.2
2.10.4.3
2.10.5
2.10.6
2.10.7
2.11
2.12
Памятайте, завдання вважається виконаним, якщо програма спрацювала так, як ви того і хотіли. А те що
ви хотіли від програми, повинно бути записане в коментарі на початку програми. Тут же вкажете і своє
прізвище. Скопіюйте текст програми у doc файл і туди ж скопіюйте вікно Командная строка з результатом
виконання програми. Розподіліть самостійно програми між собою. Назвіть файл відповідним пунктом, за
яким повинно слідувати ваше прізвище, наприклад 21041_Popov. Ці файли здати старостам, які
обєднають їх у папку Java2Lab і передадуть викладачу.
Деякі з цих програм ви поясните всім на лекціях, деякі продемонструєте в лабораторії.
2. Ознайомтеся з методами класів System і Character по відповідним файлам. Звертайтесь до цих файлів
кожен раз, коли треба зрозуміти, як працює той чи інший метод .
41
42. Програмування в Java
Урок 3. Обєктно-орієнтоване програмування в Java
Реквізити:
• Текст лекції
• Файл Object.html з описом відповідного класу
• Файл System.txt з реалізацією відповідного класу
Зміст
• Парадигми програмування
• Принципи обєктно-орієнтованого програмування
• Абстракція
• Ієрархія
• Відповідальність
• Модульність
• Принцип KISS
• Як описати клас і підклас
• Абстрактні методи і класи
• Остаточні члени і класи
• Клас Object
• Конструктори і класи
• Операція new
• Статичні члени класу
• Клас Complex
• Метод main()
• Де видимі змінні
• Заключення
Дещо, з написаного далі, ви вже знаєте або принаймні чули. Але не спішіть пропускати текст у пошуках
чогось нового. Читайте все підряд і розмірковуйте. Пройде ще дуже багато часу доки ви все будете
розуміти.
3.1. Парадигми програмування
Вся піввікова історія програмування компютерів, а може бути, і історія всієї науки — це намагання
зівладати зі складністю навколишнього світу. Завдання, які постають перед програмістами, становляться
все більш громіздкими, інформація, яку треба опрацювати, росте як снігова куля. Ще недавно звичайними
одиницями виміру інформації були кілобайти і мегабайти, а зараз уже говорять тільки про гігабайти і
терабайти. Як тільки програмісти пропонують більш-менш задовільне розвязання поставлених задач, тут
же виникають нові, ще більш складні задачі. Програмісти придумують нові методи, створюють нові мови.
За піввіку з'явилось декілька сот мов, запропоновано багато методів і стилів. Деякі методи і стилі
становляться загальноприйнятими і утворюють на деякий час так звану парадигму програмування.
Перші, навіть самі простіші програми, написані в машинних кодах, складали сотні рядків зовсім
незрозумілого тексту. Для спрощення і прискорення програмуваня придумали мови високого рівня:
FORTRAN, Algol і сотні інших, поклавши рутинні операції по створенню машинного коду на компілятор. Ті
ж програми, переписані на мовах високого рівня, стали набагато зрозумілішими и коротшими. Але життя
вимагало розвязання більш складних задач, и програми знову збільшились у розмірах, стали
неоглядними.
Виникла ідея: оформити програму у вигляді декількох, по можливості простих, процедур або функцій,
кожна з яких вирішує свою конкретну задачу. Написати, скомпілювати і налаштувати невелику процедуру
можна легко і швидко. Потім залишається лише зібрати всі процедури в потрібному порядку в одну
програму. Крім того, один раз написані процедури можна потім використовувати в інших програмах як
42
43. будівельні цеглинки. Процедурне програмування швидко стало парадигмою. У всі мови всокого рівня
включили засоби написания процедур і функцій. Появилось багато бібліотек процедур і функцій на всі
випадки життя. Першою такою мовою програмування для вас була QBasic.
Постало питання про те, як виявити структуру програми, розбити програму на процедури, яку частину
коду виділити в окрему процедуру, як зробити алгоритм розвязку задачі простим і наочним, як зручніше
звязати процедури між собою. Досвідчені програмісти запропонували свої рекомендації, названі
структурним програмуванням. Структурне програмування виявилось зручним і стало парадигмою.
Появились мови програмування, наприклад Pascal, на яких зручно писати структурні програми. Більше
того, на них дуже важко написати неструктурні програми.
Складність поставших перед програмістами задач проявилась і тут: програми стали містить сотні
процедур, і знову виявились неосяжними. "Цеглинки" стали занадто маленькими. Потребувався новий
стиль програмування,
В той же час виявилося, що успішна або неуспішна структура вихідних даних може значно полегшити або
ускладнити їх опрацювання. Одні вихідні дані зручніше обєднати в масив, для інших більше підходить
структура дерева або стека. Ніклаус Вірт навіть назвав свою книгу "Алгоритми + структури данних =
програми".
Виникла ідея обєднати вихідні дані і всі процедури їх опрацювання в один модуль. Ця ідея модульного
програмування швидко завоювала уми і на деякий час стала парадигмою. Програми сскладалася із
окремих модулів, що містили десяток-другий процедур і функцій. Эфективність таких программ тим вище,
чим менше модулі залежать один від одного. Автономність модулів дозволяє створювати і бібліотеки
модулів, щоб потім використовувати їх в якості будівельних блоків для програми. Модульна парадигма
програмування дуже добре була видима в Delphi, коли до вашої програми автоматично приєднувався
десяток другий модулів.
Для того щоб забезпечити максимальну незалежність модулів один від одного, треба чітко відділити
процедури, котрі будуть викликатися іншими модулями,— відкриті (public) процедури, від допоміжних,
котрі опрацьовують дані, включені в цей модуль, — закритих (private) процедур – щось на зразок
глобальних і локальних змінних. Перші перечисляються в окремій частині модуля — інтерфейсі
(interface), другі приймають участь тільки в реалізації (implementation) модуля. Дані, занесені в модуль,
також діляться на відкриті, вказані в інтерфейсі і доступні для інших модулів, і закриті, доступні тільки для
процедур того ж модуля. В різних мовах програмування цей розподіл здійснюється по-різному. В мові
Turbo Pascal модуль спеціально ділиться на інтерфейс і реалізацію. В мові С інтерфейс виноситься в
окремі "заголовкові" (header) файли. В мові C++, крім того, для описання інтерфейса можна скористатися
абстрактнми класами. В мові Java єсть спеціальна конструкція для описання інтерфейсів, яка так і
називається — interface, але можна написати і абстрактні класи.
Так виникла ідея укриття, інкапсуляції (incapsulation) даних і методів їх обробки. Подібні ідеї періодично
виникають в дизайні побутової техніки. То телевізори щетиняться кнопками, ручками і движками на
радість допитливому телеглядачу, панує "приладний" стиль, то все кудись пропадає, а на панелі
залишаються тільки кнопка включеня і ручка регулювання звуку. Допитливий телеглядач береться за
відкрутку.
Інкапсуляція, звичайно, робиться не для того, щоб сховати від іншого модуля щось цікаве. Тут
преслідуються дві основні цілі. Перша — забезпечити безпеку використання модуля, винести в інтерфейс,
зробити загальнодоступними тільки ті методи обробки інформації, котрі не можуть зіпсувати або видалити
вихідні дані. Друга мета — зменшити складність, сховавши від зовнішнього світу непотрібні деталі
реалізації.
Знову виникає питання, яким чином розбити програму на модулі? Тут виявились корисними методи
розвязання старої задачі програмування — моделювання дій штучних и природних обєктів: роботів,
станків з програмним забезпеченням, безпілотних літаків, людей, тварин, рослин, систем забезпечення
життєдіяльності, систем управління технологічними процесами.
43
44. В самім ділі, кожний обєкт — робот, автомобіль, людина — має певні характеристики. Ними можуть
служити: вага, зріст, прізвище, максимальна швидкість, кут повороту, вантажопідємність. Обєкт може
виконувати якісь дії: зміщуватися в просторі, повертатися, підіймати, копати, рости або зменшуватися,
їсти, пити, народжуватися і помирати, змінюючи свої початкові характеристики. Зручно змоделювати
обєкт у вигляді модуля. Його характеристики будуть даними, постійними або змінннми, а дії —
процедурами.
Те ж саме з програмою, її розбивають на модулі так, щоб вона претворилась у сукупність взаємодіючих
обєктів. Так виникло обєктно-оріентоване програмування (object-oriented programming), скорочено ООП
(OOP) — сучасна парадигма програмування.
У вигляді обєктів можна представити зовсім неочікувані поняття. Наприклад, вікно на екрані дисплея — це
обєкт, що має ширину width і висоту height, розташоване на екрані, описується координатами (х, у) лівого
верхнього кута вікна, а також шрифт, яким у вікно виводится текст, скажімо, Times New Roman, колір фону
color, декілька кнопок, смуги прокрутки і інші характеристики. Вікно може зміщуватися по екрану методом
move(), збільшуватися або зменшуватися в размірах методом size(), звертатися в ярлик методом iconify(),
реагувати на дії миші і натискання клавіш. Це повноцінний обєкт! Кнопки, смуги прокрутки і інші елементи
вікна — це теж обєкти зі своїми розмірами, шрифтами, зміщеннями.
Здається, вважати, що вікно само "уміє" виконувати дії, а ми тільки даємо йому доручення: "Звернись,
розвернись, перемістись", — це дещо неочікуваний погляд на речі, але ж тепер можна подавати команди
не тільки мишкою і клавішами, але й голосом!
Ідея обєктно-орієнтованого програмування виявилась дуже плодотворною і стала активно розвиватися.
Вявилось, що зручно ставити задачу зразу у вигляді сукупності діючих обєктів — виник обєктно-
орієнтований аналіз, ООА. Вирішили проектувати складні системи у вигляді обєктів — зявилось обєктно-
орієнтоване проектування, ООП (OOD, object-oriented design).
Розглянемо детальніше принципи обєктно-орієнтованого програмування
3.2. Принципи обєктно-орієнтованого програмування
Обєктно-орієнтоване програмування розвивається уже більше двадцати років. Єсть декілька шкіл, кожна з
яких пропонує свій набір принципів роботи з обєктами і по-своєму викладає ці принципи. Але єсть і
декілька загальноприйнятих понять. Перечислимо їх.
3.2.1. Абстракція
Описуючи поведінку якого-небудь обєкта, наприклад автомобіля, ми будуємо його модель. Модель, як
правило, не може описати обєкт повністю, реальні обєкти досить складні. Приходится відбирати тільки ті
характеристики обєкта, котрі важливі для розвязання поставленої перед нами задачі. Для описання
вантажоперевезень важливою характеристикою буде вантажопідємність автомобіля, а для описання
автомобільних гонок вона не суттєва. Але для моделювання гонок обовязково треба описати метод
набору швидкості даним автомобілем, а для вантажоперевезення це не суть важливо.
Ми повинні абстрагуватися від деяких конкретних деталей обєкта. Дуже важливо вибрати правильну
степінь абстракції. Занадто висока степінь дасть тільки приблизне описанне обєкта, не дозволить
правильно моделювати його поведінку. Занадто низька степінь абстракції зробить модель дуже
складною, перевантажену деталями, і тому непридатною.
Наприклад, можна цілком точно предбачити погоду на завтра в певному місці, але розрахунки по такій
моделі триватимуть три доби навіть на самому потужному компютері. Для чого потрібна модель, що
запізнюється на два дні? Ну а точність моделі, якою користуються синоптики, ми всі знаємо самі. Зате
розрахунки по цій моделі займають всього декілька годин.
Описання кожної моделі робиться у вигляді одного або декількох класів (classes). Клас можна вважати
44
45. проектом, зліпком, кресленням, по якому потім будуть створюватися конкретні обєкти. Клас містить
описання змінних і констант, що характеризують обєкт. Вони називаються полями класу (class fields).
Процедури, які описують поведінку обєкта, називаються методами класу (class methods). Всередині
класу можна описати і вкладені класи (nested classes) і вкладені інтерфейси. Поля, методи і вкладені
класи першого рівня являються членами класу (class members). Різні школи обєктно-орієнтованного
програмування пропонують різні терміни, ми використовуємо термінологію, прийняту в технології Java.
Ось зразок описання автомобіля:
class Automobile{
int maxVelocity; // Поле, що містить найбільшу швидкість автомобіля
int speed; // Поле, що містить поточну швидкість автомобіля
int weight; // Поле, що містить вагу автомобіля
// Інші поля...
void moveTo(int x, int у){ // Метод, що моделює переміщення автомобіля. Параметри х
і у — не поля
int а = 1; // Локальна змінна — не поле
// Тіло метода. Тут описується закон переміщення автомобіля в точку (х, у)
}
// Інші методи. . .
}
Знавцям Pascal
В Java немає вкладених процедур і функцій, в тілі метода не можна описати інший метод.
Після того як описання класу закінчено, можна створювати конкретні обєкти, eкземпляри (instances)
описаного класу. Створення экземплярів відбувається в три етапи, подібно описанню масивів. Спочатку
оголошуютья посилання на обєкти: записується імя класу, і через пробіл перечисляються экземпляри
класу, точніше, посилання на них.
Automobile Iada2110, fordScorpio, oka;
Потім операцією new визначаються самі обєкти, під них виділяється оперативна память, посилання
отримує адресу цієї частини памяті в якості свого значення.
lada2110 = new Automobile();
fordScorpio = new Automobile();
oka = new Automobile();
На третьому этапі відбувається ініціалізація обєктів, задаються початкові значення. Цей етап, як правило,
суміщається з другим, якраз для цього в операції new повторяється імя класа з дужками Automobile (). Це
так званий конструктор (constructor) класу, але про нього поговоримо пізніше.
Оскільки імена полів, методів і вкладених класів у всіх обєктах одинакові, вони задані в описанні класу, їх
треба уточняти іменем посилання на обєкт:
lada2110.maxVelocity = 150;
fordScorpio.maxVelocity = 180;
oka.maxVelocity = 350;// Чому б і ні?
oka.moveTo(35, 120);
Нагадаємо, що текстовий рядок в лапках вважається в Java як обєкт класу String. Тому можна написати
int strlen = "Це обєкт класу String".length();
45
46. Обєкт "рядок" виконує метод length(), один із методів свого класу String, що підраховує число символів у
рядку. В результаті одержимо значення strlen, рівне 21. Подібний дивний запис зустрічається в Java
програмах на кожному кроці.
В багатьох ситуаціях створюють декілька моделів з різним степенем деталізації. Скажімо, для
конструювання пальто і шуби потрібна менш точна модель контурів людського тіла і його рухів, а для
конструювання фрака або вечірнього плаття — уже набагато точніша. При цьому більш точна модель, з
меншим степенем абстракції, буде використовувати уже наявні методи менш точної моделі.
Чи не здається вам, що клас Automobile сильно перевантажений? Дійсно, в світі випущені мільйони
автомобілів різних марок і видів. Що між ними спільного, крім чотирьох коліс? Та і коліс може бути більше
або менше. Чи не краще написати окремі класи для легкових і вантажних автомобілів, для гоночних
автомобілів и всюдиходів? Як організувати всю цю множину класів? На це питання обєктно-орієнтоване
програмування відповідає так: треба організувати ієрархію класів.
3.2.2. Ієрархія
Ієрархія обєктів давно використовується для їх класифікації. Особливо детально вона опрацьована в
біології. Всі знайомі з сімействами, родами і видами. Ми можемо зробити описання своїх домашніх тварин
(pets): кішок (cats), собак (dogs), корів (cows) і інших наступним чином:
class Pet{ // Тут описуємо спільні властивості всіх домашніх улюбленців
Master person; // Хазяїн тварини
int weight, age, eatTime; // Вага, вік, час годування
int eat(int food, int drink, int time){ // Процес годування
// Початкові дії...
if (time == eatTime) person.getFood(food, drink);
// Метод приймання їди
}
void voice(); // Звуки, що видають тварини
// Інше...
}
Потім створюємо класи, які описують більш конкретні обєкти, звязуючи іх із спільним класом:
class Cat extends Pet{ // Описуються властивості, властиві лише кішкам:
int mouseCatched; // число спійманих мишей
void toMouse(); // процес ловіння мишей
// інші властивості
}
class Dog extends Pet{ // Властивості собак:
void preserve(); // охороняти
}
Зверніть увагу, що ми не повторюємо спільні властивості, описані в класі Pet. Вони наслідуються
автоматично. Ми можемо визначити обєкт класа Dog і використовувати в ньому всі властивості класа Pet
так, як ніби то вони описані в класі Dog:
Dog Tuzik = new Dog(), Sharik = new Dog();
Після такого визначення можна буде написати
Tuzik.age = 3;
int p = Sharik.eat (30, 10, 12);
А класифікацію продовжити так:
46
47. class Pointer extends Dog{ ... } // Властивості породи Пойнтер
class Setter extends Dog{ ... } // Властивості сеттерів
Зверніть увагу, що на кожному наступному рівні ієрархії в клас додаються нові властивості, але жодна
властивість не пропадє. Тому і використовується слово extends — "розширює" і говорять, що клас Dog —
розширення (extension) класу Pet. З іншої сторони, кількість обєктів при цьому зменшується: собак менше,
ніж всіх домашніх тварин. Тому часто говорять, що клас Dog — підклас (subclass) класу Pet, а клас Pet —
суперклас (superclass) або надклас класу Dog.
Часто використовують генеалогічну термінологію: батьківський клас, дочірній клас, клас-нащадок, клас-
предок, виникають племінники і внуки, вся неспокійна сімейка вступає у відносини, гідні мексіканського
серіала.
В цій термінології говорять про наслідування (inheritance) класів, в нашому прикладі клас Dog наслідує
клас Pet.
Ми ще не визначили щасливого хазяїна нашого домашнього зоопарка. Опишемо його в класі Master.
Зробимо прикидку:
class Master{ // Хазяїн тварини
String name; // Прізвище, імя
// Інші дані
void getFood(int food, int drink); // Годівля
// Інше
}
Хазяїн і його домашні тварини постійно контактують. Їх взаємодія виражається дієсловами "гуляти",
"годувати", "охороняти", "чиститиь", "лащитися", "проситися" і іншими. Для описаня взаємодії обєктів
застосовується третій принцип обєктно-орієнтованого програмування — обовязок або відповідальність.
3.2.3. Відповідальність
В нашому прикладі розглядається тільки взаємодія в процесі годівлі, яка описується методом eat(). В
цьому методі тварина звертається до хазяїна, умоляючи його застосувати метод getFood().
В англомовній літературі подібне звернення описується словом message. Це поняття невдало
перекладено на українську мову ні до чого не зобовязуваним словом "повідомлення". Краще було б
використати слово "послання", "доручення" або навіть "розпорядження". Але термін "повідомлення"
устоявся і нам прийдеться його застосовувати. Чому ж не використовується словосполучення "виклик
методу", адже говорять: "Виклик процедури"? Тому що між цими поняттями єсть, по крайній мірі, три
відмінності.
• Повідомлення йде до конкретного обєкта, що знає метод розвязання задачі, в прикладі цей обєкт
— поточне значення змінної person. У кожного обєкта є свій поточний стан, свої значення полів
класу, і це може вплинути на виконання метода.
• Спосіб виконання доручення, що міститься в повідомленні, залежить від обєкта, якому воно
послане. Один хазяїн поставить миску з "Chappi", другий кине кістку, третій вижене собаку на
вулицю. Цю цікаву властивість називають поліморфізмом (polymorphism), її будемо обговорювати
далі.
• Звернення до методу відбудеться лише на етапі виконання програми, компілятор нічого не знає
про метод. Це називається "пізнім звязуванням" на противагу "ранньому звязуванню", при якому
процедура приєднується до програми на етапі компонування.
Отже, обєкт Sharik, виконуючи свій метод eat(), посилає повідомлення обєкту, посилання на який
міститься в змінній person, с просьбою видати йому певну кількість їди і питва. Повідомлення записане в
рядку person.getFood(food, drink).
47
48. Цим повідомленням заключається контракт (contract) між обєктами, суть якого в тому, що обєкт Sharik
бере на себя відповідальність (responsibility) задати правильні параметри в повідомленні, а обєкт —
поточне значення person — бере на себе відповідальністьсть застосувати метод годівлі getFood() , яким
би він не був.
Для того щоб правильно реалізувати принцип відповідальності, застосовується четвертий принцип
обєктно-орієнтованого програмування — модульність (modularity).
3.2.4. Модульність
Цей принцип стверджує — кожний клас повинен складати окремий модуль. Члени класу, до яких не
планується звертання зовні, повинні бути інкапсульовані. В мові Java інкапсуляция досягається
додаванням модифікатора private до описання члена класу. Наприклад:
private int mouseCatched;
private String name;
private void preserve();
Ці члени класів становляться закритими, ними можуть користуватися тільки екземпляри того ж самого
класу, наприкладр, Тuzik може дати доручення
Sharik.preserve().
А якщо в класі Master ми напишемо
private void getFood(int food, int drink);
то метод getFood() не буде знайдено, і нещасний Sharik не зможе отримати їжу.
В протилежність закритості ми можемо оголосити деякі члени класа відкритими, записавши замість
слова private модифікатор public, наприклад:
public void getFood(int food, int drink);
До таких членів може зверннутися любий обєкт любого класа.
Знавцям C++
В мові Java словами private, public і protected відмічаються окремо кожний член класу.
Принцип модульності предбачає відкривати члени класу тільки у випадку необхідності. Згадайте напис:
"Нормальне положення шлагбаума — закрите".
Якщо ж треба звернутися до поля класа, то рекомендується включить в клас спеціальні методи доступу
(access methods), окремоо для читання цього поля (get method) і для запису в це поле (set method). Імена
методів доступу рекомендується починати зі слів get і set, додаючи до цих слів імя поля. Для JavaBeans ці
рекомендації введені в ранг закону.
В нашому прикладі класу Master методи доступу до поля Name в самому простому випадку можуть
виглядіти так:
public String getName(){
return name;
}
public void setName(String newName)
48
49. {
name = newName;
}
В реальних ситуаціях доступ обмежується різними перевірками, особливо в set-методах, що змінюють
значення полів. Можна перевіряти тип значения, що вводиться, задавати діапазон значень, зрівнювати зі
списком допустимих значень.
Крім методів доступу рекомендується створювати провірочні is-методи, які повертають логічні значення
true або false. Наприклад, в клас Master можна включити метод, перевіряючий, чи задано імя хазяїна:
public boolean isEmpty(){
return name == null ? true : false;
}
і використовувати цей метод для перевірки при доступі до поля Name, наприклад:
if (masterOl.isEmpty()) masterOl.setName(“Іванов");
Таким чином, ми залишаємо відкритими лише методи, необхідні для взаємодії обєктів. При цьому зручно
спланувати класи так, щоб залежність між ними була найменшою, як прийнято говорит в теорії ООП, було
найменше зачеплення (low coupling) міжду класами. Тоді структура програми сильно спрощується. Крім
того, такі класи зручно використовувати як будівельні блоки для побудови інших програм.
Навпаки, члени класа повинні активно взаємодіяти один з одним, як говорять, мати тісну функціональну
звязність (high cohesion). Для цього в клас належить включати всі методи, що описують поведінку
моделюючого обєкта, і тільки такі методи, нічого лишнього. Одно із правил досягнення сильної
функціональної звязності, введенне Карлом Ліберхером (Karl J. Lieberherr), отримало назву закон
Деметра. Закон гласить: "в методі m() класу А належить використовувати тільки методи класу А, методи
класів, до яких належать аргументи метода m(), і методи класів, екземпляри яких створюються всередині
метода m().
Обєкти, побудовні по цим правилам, схожі на кораблі, споряджені всім необходіним. Вони виходять в
автономне плавання, готові виконати будь-яке доручення, на яке розрахована їх конструкція.
Чи будуть закриті члени класа доступні його наслідникам? Якщо в класі Pet написано
private Master person;
то чи можна використовувати Sharik.person? Розуміється, ні. Адже в противному випадку кожний, хто
цікавиться закритими полями класу А, може розширити його класом B, і продивитися закриті поля класу А
через екземпляри класу B.
Коли треба дозволити доступ наслідникам класу, але небажано віткривати його всьому світу, тоді в Java
використовується захищений (protected) доступ, позначений модифікатором protected, наприклад, обєкт
Sharik може звернутися до поля person батьківського класу Pet, якщо в класі Pet це поле описано так:
protected Master person;
Треба зразу сказати, що на доступ до члена класу впливає ще і пакет, в якому знаходиться клас, але про
це поговоримо в наступній главі.
Із цього загального схематичного описання принципів обєктно-орієнтованого програмування видно, що
мова Java дозволяє легко втілювати всі ці принципи. Ви уже зрозуміли, як записати клас, його поля і
методи, як інкапсулювати члени класу, як зробити росширення класу і якими принципами належить при
49
50. цьому користуватися. Розберемо тепер детальніше правила запису класів і розглянемо додаткові їх
можливості.
Але, говорячи про принципи ООП, я не можу утриматися від того, щоб не нагадати основний принцип
всякого програмування.
3.2.5. Принцип KISS
Самий основний, базовий і самий великий :принцип програмування — принцип KISS — не потребує
розяснень: "Keep It Simple, Stupid!"
3.3. Як описати клас і підклас
Отже, описання класу починаеться зі слова class, після якого записується імя класу. "Code Conventions"
рекомендує починать імя класу із заглавної літери.
Перед словом class можна записати модифікатори класа (class modifiers). Це одно із слів public, abstract,
final, strictfp. Перед іменем вкладеного класу можна поставити, крім того, модифікатори protected, private,
static. Модифікатори ми будем вводити по мірі вивчення мови.
Тіло класу, в якому в любому порядку перечисляються поля, методи, вкладені класи і інтерфейси,
заключається в фігурні дужки.
При описанні поля вказується його тип, потім, через пробіл, імя і, може бути, початкове значення після
знака рівності, яке можна записати константним виразом. Все це уже описано в уроці 2.
Описання поля може починатися з одного або декількох необовязкових модифікаторів public, protected,
private, static, final, transient, volatile. Якщо треба поставити декілька модифікаторів, то перечисляти їх JLS
рекомендує в указаному порядку, оскільки деякі компілятори вимагають певного порядку запису
модифікаторів.
При описанні метода указується тип значення, який він повертає або слово void, потім, через пробіл, імя
метода, потім, в дужках, список параметрів. Після цього в фігурних дужках розписується виконуваний
метод.
Описання метода може починатися з модифікаторів public, protected, private, abstract, static, final,
synchronized, native, strictfp.
В списку параметрів через кому перечисляються тип і імя кожного параметра. Перед типом якого-небудь
параметра може стояти модифікатор final. Такий параметр не можна змінювати всередині метода. Список
параметрів може бути відсутнім, але дужки зберігаються.
Перед початком роботи метода для кожного параметра виділяється ячейка оперативної памяті, в яку
копіюється значення параметра, задане при зверненні до метода. Такий спосіб називається передачею
параметрів по значенню.
В лістинзі 3.1 показано, як можна оформити метод ділення пополам для знаходження кореня нелінійного
рівняння із лістинга 1.5.
Лістинг 3.1. Знаходжение кореня нелінійного рівняння методом бісекцій
class Bisection2{
private static double EPS = 1e-8; // Константа
private double a = 0.0, b = 1.5, root; // Закриті поля
public double getRoot(){return root;}
50
51. // Метод доступа
public double f(double x)
{
return x*x*x - 3*x*x + 3; // Або щось інше
}
public void bisect(){ // Параметрів немає — метод працює з полями екземпляра
double y = 0.0; // Локальна змінна — не поле
do{
root = 0.5 *(a + b); y = f(root);
if (Math.abs(y) < EPS) break;
// Корінь знайдено. Виходимо з циклу.
// Якщо на кінцях відрізка [a; root] функція має різні знаки:
if (f(a) * y < 0.0) b = root;
// значить, корінь тут. Переносимо точку b в точку root
//В противному випадку:
else a = root;
// переносимо точку а в точку root
// Продовжуємо, доки [а; b] не стане малим
} while(Math.abs(b-a) >= EPS);
}
}
class Test{
public static void main(String[] args){
Bisection2 b2 = new Bisection2();
b2.bisect();
System.out.println("x = " +
b2.getRoot() + // Звертаємося до кореня через метод доступа
", f() = " +b2.f(b2.getRoot()));
}
}
В описанні метода f() збережений старий, процедурний стиль: метод одержує аргумент, опрацьовує його і
повертає результат. Описання метода bisect() виконано в дусі ООП: метод активний, він сам звертається
до полів экземпляра b2 і сам заносить результат в потрібне поле.
Імя метода, число і типи параметрів створюють сигнатуру (signature) метода. Компілятор розрізняє
методи не по їх іменах, а по сигнатурах. Це дозволяє записувати різні методи з однаковими іменами, що
відрізняються числом і/або типами параметрів.
Зауваження
Тип значення, що повертається, не входить в сигнатуру метода, значить, методи не можуть розрізнятися
тільки типом результe та їх роботою.
Наприклад, в класі Automobile ми записали метод moveTo(int x, int у), позначивши пункт призначення його
географічними координатами. Можна визначити ще метод moveTo (string destination) для вказівки
географічної назви пункту призначення і звертатися до нього так:
51
52. oka.moveTo("Полтава") ;
Таке дублювання методів називається перевантаженням (overloading). Перевантаження методів дуже
зручне у використанні. Згадайте, в уроці 2 ми виводили дані будь-якого типу на екран методом println() не
турбуючись про те, дані якого іменно типу ми виводимо. На самім ділі ми використовували різні методи з
одним и тим же іменем println, навіть не задумуючись про це. Звичайно, всі ці методи треба ретельно
спланувати і заздалегідь описати в класі. Це і зроблено в класі PrintStream, де представлено близько
двадцати методів print() і println(). Ось методи println().
void println() Terminate the current line by writing the line separator string.
void println(boolean x) Print a boolean and then terminate the line.
void println(char x) Print a character and then terminate the line.
void println(char[] x) Print an array of characters and then terminate the line.
void println(double x) Print a double and then terminate the line.
void println(float x) Print a float and then terminate the line.
void println(int x) Print an integer and then terminate the line.
void println(long x) Print a long and then terminate the line.
void println(Object x) Print an Object and then terminate the line.
void println(String x) Print a String and then terminate the line.
Якщо ж записати метод з тим же іменем в підкласі, наприклад:
class Truck extends Automobile{
void moveTo(int x, int y){
// Якісь дії
}
// Ще щось
}
то він перекриє метод суперкласа. Визначивши экземпляр класа Truck, наприклад:
Truck gazel = new Truck();
і записавши gazel.moveTo(25, 150), ми звернемося до метода класу Truck. Відбудеться перевизначення
(overriding) метода.
При перевизначенні права доступу до метода можна тільки розширити. Відкритий метод public повинен
залишатися відкритим, захищений protected може стати відткритим.
Чи можна всередині підкласу звернутися до метода суперкласу? Так, можна, якщо уточнити імя метода,
словом super, наприклад, super.moveTo(30, 40). Можна уточнити і імя метода, записаного в цьому ж класі,
словом this, наприклад, this.moveTo (50, 70), але в даному випадку це вже зайве. Таким же способом
можна уточняти і співпадаючі імена полів, а не тільки методів.
Дані уточнення подібні тому, як ми говорим про себя "я", а не "Іван Петрович", і говорим "батько", а не
"Петро Сидорович".
Перевизначення методів приводить до цікавих результатів. В класі Pet ми описали метод voice().
Перевизначимо його в підкласах і використаємо в класі chorus, як показано в лістинзі 3.2.
52
53. Лістинг 3.2. Приклад поліморфного метода
abstract class Pet{
abstract void voice();
}
class Dog extends Pet{
int k = 10;
void voice(){
System.out.println("Gav-gav!");
}
}
class Cat extends Pet{
void voice () {
System.out.println("Miaou!");
}
}
class Cow extends Pet{
void voice(){
System.out.println("Mu-u-u!");
}
}
public class Chorus{
public static void main(String[] args){
Pet[] singer = new Pet[3];
singer[0] = new Dog();
singer[1] = new Cat();
singer[2] = new Cow();
for (int i = 0; i < singer.length; i++)
singer[i].voice();
}
}
Вся справа тут у визначенні поля singer[]. Хоча масив singer [] має тип Pet, кожний його елемент
посилається на обєкт свого типу Dog, Cat, Сow. При виконанні програми викликається метод конкретного
обєкта, а не метод класу, яким визначалося імя посилання. Так в Java реалізується поліморфізм.
Знавцям C++
В мові Java всі методи являються віртуальними функціями.
Уважний читач помітив у описанні класу Pet нове слово abstract. Клас Pet і метод voice() являються
абстрактними.
3.4. Середовище програмування Jbuilder
53
54. Компіляція і запуск Java аплікації з командного рядка не такі вже й складні, якщо в програмі немає
помилок. Але таке може приснитися хіба-що уві сні. А на самім ділі, тільки виправиш одну помилку і
перекомпілюєш файл, як зявляється повідомлення про іншу помилку. Скоріше налаштування Java
програми робиться в інтегрованому середовищі програмування JBuilder. Правда, тут треба створювати
проект з багатьма файлами, але, на щастя, це робиться майже автоматично, вам треба лише заповняти
потрібні поля і натискати кнопки.
Отже, відкрийте середовище програмування JBuilder. В залежності від того, чи ви це робите перший раз,
чи вже працювали раніше, перед вами можуть зявитися різні вікна. Це також може залежати і від версії
JBuilder, але у вас уже достатньо досвіду, щоб внести в разі необхідності потрібні корективи в те, про що
йтиме мова далі. Я користуюся версією JВuilder4. Ось яке вікно відкрилося у мене.
Як би ви добре знали англійську мову, то з цього вікна дізналися б багато цікавого, як працювати в
середовищі програмування JBuilder. Але так як рівень вашої англійської мови не дозволяє це зробити, то
вам залишається тільки закрити це вікно і понадіятись на мене. Багато я не встигну пояснити, та для
початку і це буде добре. А далі все буде залежати лише від вас.
Перед цим я створив новий проект primeiro.jpx, але видалив з нього усі файли. Тому у мене зявилося таке
вікно. У вас же може зявитися шаблонний проект Wellcom.jpx із запрошенням дізнатися як створюються
Java проекти. Але знову вам на заваді стає англійська мова. Тому знову довірьтесь мені.
Закрийте всі файли, які ви знайдете у вікні проекту і не звертаючи уваги на те, що залишилось, тисніть в
меню File --> New Project. Запускається програма Project Wizard, яка за три кроки створить нам новий
54
55. проект. Назвемо його chor - я хочу запустити аплікацію лістингу 3.2 з уроку 3 про котячо-собачий хор, тому
й дав проекту таку назву, а збережемо там де захочемо – я в папці С:JavaProjects.
Ось який вигляд мало вікно Step1 до вибору імені проекту і папки,
а такий після.
Решту полів програма заповнює сама. Тиснемо Next і переходимо до наступного кроку (Step 2)
55
56. Залиште в цьому вікні все без зміни, воно в основному інформує вас про створені в проекті службові
файли. Тисніть Next
і впишіть у відповідні поля назву вашої аплікації, своє прізвище і установу, в якій працюєте. Опишіть
коротко призначення аплікації. Все це зявиться пізніше в одному із файлів проекту. Тиснемо Finish і
бачимо
Вікно інформує нас, що в проекті лише один файл chor.html. Давайте відкриємо його
56
57. Знайома картина. Але куди ж скопіювати нашу програму з лістингу 3.2. Створимо необхідні для цього
файли. Тиснемо File --> New і у вікні Object Galery
робимо double click на Application.
В поле Class я вписав імя класу chorus, який містить main() метод нашої аплікації. Тисну Next і бачу
57
58. Залишаю це вікно без уваги. Воно служить для створення файлу форми, подібної до Visual Basic і Delphi,
але ми на перших порах нею користуватися не будемо. Нам треба ще розібратися в конструкціях мови
програмування Java, а графічне середовище буде відволікати нашу увагу. Досить того, що ми в свій час
награлися у Visual Basic і Delphi. Одначе програма Application Wizard помимо нашої волі все ж таки
створить такий файл. Більше того, вона створить шаблонний файл для програмного коду і в нього впише
багато непотрібних для нас на перший випадок речей. Ось як ми будемо виходити із цієї ситуації.
Тиснемо Finish і бачимо
В правому
вікні
бачимо
файл
Frame1,
про що
свідчить
іконка
зверху. Цей
файл, як
уже
говорилося,
нам зараз
непотрібни
й. Ми могли
б на нього
просто не
звертати
уваги, але
краще його взагалі видалити з проекту, щоб не зававжав. У лівому верхньому вікні тиснемо правою
кнопкою миші на Frame1.java і в контексному меню вибираємо опцію Delete. І зразу на душі полегшало —
нічого тобі лишнього.
Навіть справа
58
59. залишилося всього одне вікно. Відкриваємо через double click файл chorus.java і знову бачимо там багато
незрозумілого, а головне непотрібного.
Видаляємо з цього файлу все, за виключенням першого рядка
і копіюємо в нього вміст лістингу 3.2. Зверніть увагу, в лівому нижньому вікні зявилася інформація про всі
класи, що входять в аплікацію. Ви можете запустити її на виконання натиском зеленого трикутничка у
ToolBar як в Delphi чи Visual Basic. Наші зусилля були варті того. Тепер ми можемо експериментувати з
будь якими програмами, варто лише видалити в chorus.java один програмний код і скопіювати на його
місце інший.
59
60. Ось що ми бачимо при запуску на виконання програми chorus.
Результат зявився в нижньому вікні, яке має точно таку ж структуру, як і Командная строка. Навчіться
користуватися середовищем програмування JBuilder і це полегшить вам роботу з наступним матеріалом.
3.5. Абстрактні методи і класи
При описаннні класу Pet ми не можемо задати в методі voice () ніякого корисного алгоритму, оскільки у
всіх тварин різні голоси. В таких випадках ми записуємо тільки заголовок метода і ставимо після
закриваючої список параметрів дужки крапку з комою. Цей метод буде абстрактним (abstract), що
необхідно вказати компілятору модифікатором abstract.
Якщо клас містить хоча один абстрактний метод, то створити його екземпляри, а тим більше
використовувати їх не удасться. Такий класс становиться абстрактним, що обовязково треба вказати
модифікатором abstract.
Як же використовувати абстрактні класи? Тільки породжуючи від них підкласи, в яких перевизначені
абстрактні методи.
Для чого ж потрібні абстрактні класи? Чи не краще зараз же написати потрібні класи з повністю
визначеними методами, а не наслідувати їх від абстрактного класа? Для відповіді знову звернемося до
лістингу 3.2.
Хоча елементи масива singer [] посилаються на підкласи Dog, Cat, Cow, але все-таки це змінні типу Pet і
посилатися вони можуть тільки на поля і методи, описані в суперкласі Pet. Додаткові поля підкласу для
них недоступні. Попробуйте звернутися, наприклад, до поля k класу Dog, написавши singer[0].k.
Компілятор "скаже", що він не може реалізувати таку посилку. Тому метод, який реалізується в декількох
підкласах, приходиться виносити в суперклас, а якщо там його неможливо реалізувати, то оголосити
абстрактним. Таким чином, абстрактні класи групуються на вершині ієрархії класів.
До речі, можна задати пусту реалізацію метода, просто поставивши пару фігурних дужок, нічого не
написавши між ними, наприклад:
void voice(){}
60
61. Получиться повноцінний метод. Але це штучне рішення, яке заплутає структуру класу. Замкнути ж
ієрархію можна остаточними класами.
3.6. Остаточні члени і класи
Помітивши метод модифікатором final, можна заборонити його перевизначення в підкласах. Це зручно в
цілях безпеки. Ви можете бути впевненими, що метод виконує ті дії, які ви задали. Якраз так визначені
математичні функції sin(), cos() інші в класі Math. Ми впенені, що метод Math.cos (x) обчислює якраз
косинус числа х. Розуміється, такий метод не може бути абстрактним.
Для повної безпеки, поля, що обробляються остаточними методами, належить зробити закритими
(private).
Якщо ж помітить модифікатором final увесь клас, то його взагалі не можна будет розширювати. Так
визначений, наприклад, класс Math:
public final class Math{ . . . }
Для змінних модифікатор final має зовсім інший зміст. Якщо помітити модифікатором final описання
змінної, то її значення (а воно повинно бути обовязково задано або тут же, або в блоці ініціалізації або в
конструкторі) не можна змінити ні в підкласах, ні в самому класі. Змінна перетворюється в константу.
Якраз так в мові Java визначаються константи:
public final int MIN_VALUE = -1, MAX_VALUE = 9999;
Згідно "Code Conventions" константи записуються прописними літерами, слова в них розділяються знаком
підкреслювання.
На самій вершині ієрархії класів Java стоїть класс Object.
3.7. Клас Object
Якщо при описанні класу ми не вказуємо ніяке розширення, тобто не пишемо слово extends і імя класу за
ним, як при описанні класу Pet, то Java вважає цей клас розширенням класу Оbject, і компілятор дописує
це за нас:
class Pet extends Object{ . . . }
Можна записати це розширення і явно.
Сам же класс Оbject не являється нічиїм наслідником, від нього починається ієрархія любих класів Java.
Зокрема, всі масиви — прямі наслідники класу Оbject.
Оскільки такий клас може містити тільки загальні властивості всіх класів, в нього включено лише декілька
самих загальних методів, наприклад, метод equals(), що порівнює даний обєкт на рівність з обєктом,
заданим в аргументі, і повертаючий логічне значення. Його можна використати так:
Object obj1 = new Dog(), obj 2 = new Cat();
if (obj1.equals(obj2)) ...
Оцініть обєктно-орієнтований дух цього запису: обєкт obj1 активний, він сам порівнює себя с другим
обєктом. Можна, звичайно, записати і obj2.equals(obj1), зробивши активним обєкт obj2, з тим же
результатом.
Як указувалось в главі 1, посилання можна порівнювати на рівність і нерівність:
61
62. obj1 == obj2; obj1 != obj 2;
В цьому випадку співставляються адреси обєктів, ми можемо узнати, чи не указують обидва посилання на
один і той же обєкт.
Метод equals() порівнює зміст обєктів в їх поточному стані, фактично він реалізований у класі Оbject як
тотожність: обєкт рівний тільки самому собі. Тому його часто перевизначають в підкласах, більше того,
правильно спроектовані, "добре виховані", класи повинні перевизначити методи класа Оbject, якщо їх не
влаштовує стандартна реалізація.
Другий метод класу Оbject, який належить перевизначати в підкласах, — метод toString (). Цей метод без
параметрів, який намагається зміст обєкта претворити в рядок символів і повертає обєкт класe String.
До цього методу виконуюча система Java звертається кожен раз, коли потрібно представити обєкт у
вигляді рядка, наприклад, в методі printing.
3.8. Конструктори класу
Ви уже звернули увагу на те, що в операції new, що визначає екземпляри класe, повторюється імя класe з
дужками. Це схоже на звертання до методу, але що за "метод", імя котрого повністю співпадає з іменем
класу?
Такий "метод" називається конструктором класу (class constructor). Його своєрідність заключається не
тільки в імені. Перечислимо особливості конструктора.
• Конструктор єсть в любому класі. Навіть якщо ви його не написали, компілятор Java сам створить
конструктор по замовчуванню (default constructor), котрий, між іншим, пустий, він не робить
нічого, кріме виклику конструктора суперкласу.
• Конструктор виконується автоматично при створенні экземпляра класу, після розподілу памяті і
обнулювання полів, але до початку використання створюваного обєкта.
• Конструктор не повертає ніякого значення. Тому в його описанні не пишеться навіть слово void,
але можна задати один із трьох модифікаторів public, protected або private.
• Конструктор не являється методом, він навіть не вважається членом класу. Тому його не можна
наслідувати або перевизначати в підкласі.
• Тіло конструктора може починатися:
• з виклику одного із конструкторів суперкласу, для цього записується слово super() з
параметрами в дужках, якщо вони потрібні;
• з виклику другого конструктора того ж класа, для цього записується слово this() з
параметрами в дужках, якщо вони потрібні.
• Якщо ж super() на початку конструктора не указаний, то спочатку виконується конструктор
суперкласа без аргументів, потім відбувається ініціалізація полів значеннями, указаними при їх
оголошенні, а вже потім те, що записано в конструкторі.
У всьому останньому конструктор можна вважати звичайним методом, в ньому дозволяється записувати
любі оператори, навіть оператор return, але тільки пустий, без всякого значення.
В класі може бути декілька конструкторів. Оскільки у них одне і те ж імя, що співпадає з іменем класу, то
вони повинні відрізнятися типом і/або кількістю параметрів.
В наших прикладах ми ні разу не розглядали конструктори класів, тому що при створенні екземплярів
наших класів викликався конструктор класу Оbject.
3.9. Операція new
Пора детальніше описати операцію з одним операндом, що позначається словом new. Вона
застосовується для виділення памяті масивам і обєктам.
62
63. В першому випадку в якості операнда указується тип елементів масиву і кількість його елементів у
квадратних дужках, наприклад:
double a[] = new double[100];
В другому випадку операндом служить конструктор класу. Якщо конструктора в класі немає, то
викликається конструктор по замовчуванню.
Числові поля класу одержують нульові значення, логічні поля — значення false, посилання — значення
null.
Результатом операції new буде посилання на створений обєкт. Це посилання може бути присвоєне
змінній типу посилання на даний тип:
Dog k9 = new Dog () ;
але може використовуватися і безпосередньо
new Dog().voice();
Тут після створення безіменного обєкта зразу виконується його метод voice(). Такий дивний запис
зустрічається в програмах, написаних на Java, на кожному кроці
3.10. Статичні члени класу
Різні екземпляри одного класу мають повністю незалежні один від одного поля, що приймають різні
значення. Зміна поля в одному екземплярі ніяк не впливає на те ж поле в другому екземплярі. В кожному
екземплярі для таких полів виділяється своя комірка памяті. Тому такі поля називаються змінними
екземпляра класа (instance variables) або змінними обєкта.
Інколи треба визначити поле, спільне для всього класу, зміна котрого в одному екземплярі потягне зміну
того ж поля у всіх екземплярах. Наприклад, ми хочемо в класі Automobile відзначити порядковий
заводський номер автомобіля. Такі поля називаються змінними класу (class variables). Для змінних класу
виділяється тільки одна комірка памяті, спільна для всіх екземплярів. Змінні класу створюються в Java
модифікатором static. В лістинзі 3.3 ми записуємо цей модифікатор при визначенні змінної number.
Лістинг 3.3. Статична змінна
class Automobile {
private static int number;
Automobile(){
number++;
System.out.println("From Automobile constructor:"+ " number = "+number);
}
}
public class AutomobileTest{
public static void main(String[] args){
Automobile lada2105 = new Automobile(),
fordScorpio = new Automobile(),
oka = new Automobile();
}
}
Цікаво, що до статичних змінних можна звертатися з іменем класа, Automobile.number, а не тільки з
іменем eкземпляра, lada2105.number, причому це можна робити, навіть якщо не створено жодного
екземпляра класу.
63
64. Для роботи з такими статичними змінними звичайно створюються статичні методи, помічені
модифікатором static. Для методів слово static має зовсім інший зміст. Виконуюча система Java завжди
створює в памяті тільки одну копію машинного коду метода, що розділяється всіма екземплярами,
незалежно від того, статичний цей метод чи ні.
Основна особливість статичних методів — вони виконуються зразу у всіх екземплярах класу. Більше того,
вони можуть виконуватися, навіть якщо не створений жоден екземпляр класа. Досить уточнити імя
метода іменем класа (а не іменем обєкта), щоб метод міг працювати. Якраз так ми користувалися
методами класу Math, не створюючи його екземпляри, а просто записуючи Math.abs(x), Math.sqrt(x). Точно
так же ми використовували метод System.out.println(). Та і методом main() ми користуємося, взагалі не
створюючи ніяких обєктів.
Тому статичні методи називаються методами класу (class methods), на відміну від нестатичних методів,
що називаються методами екземпляру (instance methods).
Звідси витікають інші особливості статичних методів:
• в статичному методі не можна використовувати посилання this і super;
• в статичному методі не можна прямо, не створюючи екземплярів, посилатися на нестатичні поля і
методи;
• статичні методи не можуть бути абстрактними;
• статичні методи перевизначаються в підкласах тільки як статичні.
Якраз тому в лістинзі 1.5 ми помітили метод f() модифікатором static. Але в лістинзі 3.1 ми працювали з
экземпляром b2 класу Bisection2, і нам не знадобилося оголошувати метод f() статичним.
Статичні змінні ініціалізуються ще до початку роботи конструктора, але при ініціалізації можна
використовувати тільки константні вирази. Якщо ж ініціалізація вимагає складних обчислень, наприклад,
циклів для задання значень элементам статичних масивів або звернення до методів, то ці обчислення
заключають в блок, помічений словом static, який теж буде виконаний до запуску конструктора:
static int[] a = new a[10];
static {
for(int k = 0; k < a.length; k++)
a[k] = k * k;
}
Оператори, заключені в такий блок, виконуються лише один раз, при першому завантаженні класу, а не
при створенні кожного екземпляра.
Тут уважний читач, напевно, піймав мене: "А говорив, що всі дії виконуються тільки за допомогою
методів!" Каюсь: блоки статичної ініціалізації, і блоки ініціалізації экземпляра записуються зовні всяких
методів і виконуються до початку виконання не то що метода, але навіть конструктора.
64
65. 3.11. Клас Complex
Комплексні числа широко використовуються не лише в математиці. Вони часто застосовуються в
графічних перетвореннях, в побудові фракталів, не говорячи уже про фізику і технічні дисципліни. Але
клас, що описує комплексні числа, чомусь не включений у стандартну бібліотеку Java. Заповнимо цей
пробіл.
Лістинг 2.4 довгий, але проглянути його треба уважно, при вивченні мови програмування дуже корисне
читання програм на цій мові. Більш того, тільки програми і варто читати, пояснення автора лише
заважають вникнути в зміст дій (жарт).
Лістинг 3.4. Клас Complex
class Complex {
private static final double EPS = 1e-12; // Точність обчислень
private double re, im; // Дійсна і уявна частини
// Чотири конструктори
Complex(double re, double im) { //Constructor 1
this.re = re; this.im = im;
}
/* після виконання цього методу створиться обєкт, який по формі нагадує масив або
двовимірний вектор, координати якого будуть введені нами параметри re і im. Самі ж
змінні класу re і im для нас недоступні – вони захищені директивою private.
Схожість же вказаного обєкта з масивом або вектором є тільки зовнішня, крім
координат обєкт має ще і методи, скриті в конструкції класу*/
Complex(double re){this(re, 0.0); } //Constructor 2
Complex(){this(0.0, 0.0); } //Constructor 3
Complex(Complex z){this(z.re, z.im) ; } //Constructor 4
// Методи доступу
public double getRe(){return re;}
public double getIm(){return im;}
public Complex getZ(){return new Complex(re, im);}
public void setRe(double re){this.re = re;}
public void setIm(double im){this.im = im;}
public void setZ(Complex z){re = z.re; im = z.im;}
// Модуль і аргумент комплексного числа
public double mod(){return Math.sqrt(re * re + im * im);}
public double arg(){return Math.atan2(re, im);}
// Перевірка: дійсне число?
public boolean isReal(){return Math.abs(im) < EPS;}
public void pr(){ // Виведення на екран
System.out.println(re + (im < 0.0 ? "" : "+") + im + "i");
}
// Перевизначення методів класа Object
public boolean equals(Complex z){
return Math.abs(re - z.re) < EPS &&
Math.abs(im - z.im) < EPS;
}
public String toString(){
return "Complex: " + re + " " + im;
}
// Методи, що реалізують операції +=, -=, *=, /=
public void add(Complex z){re += z.re; im += z.im;}
public void sub(Complex z){re -= z.re; im -= z.im;}
public void mul(Complex z){
double t = re * z.re - im * z.im;
im = re * z.im + im * z.re;
re = t;
65
66. }
public void div(Complex z){
double m = z.mod();
double t = re * z.re - im * z.im;
im = (im * z.re - re * z.im) / m;
re = t / m;
}
// Методи, що реалізують операції +, -, *, /
public Complex plus(Complex z){
return new Complex(re + z.re, im + z.im);
}
public Complex minus(Complex z){
return new Complex(re - z.re, im - z.im);
}
public Complex asterisk(Complex z){
return new Complex(
re * z.re - im * z.im, re * z.im + im * z.re);
}
public Complex slash(Complex z){
double m = z.mod();
return new Complex(
(re * z.re - im * z.im) / m, (im * z.re - re * z.im) / m);
}
}
// Перевіримо работу класу Complex
public class ComplexTest {
public static void main(String[] args){
Complex z1 = new Complex(), //Constructor 3, z1 = 0
z2 = new Complex(1.5), //Constructor 2, z2 = 1.5 – real number
z3 = new Complex(3.6, -2.2), //Constructor 1, z3 = 3.6 – 2.2i
z4 = new Complex(z3); //Constructor 4, z4 = z3
System.out.println(); // Залишаємо пустий рядок
System.out.print("z1 = "); z1.pr(); //z1 = 0.0 + 0.0i
System.out.print("z2 = "); z2.pr(); //z2 = 1.5 + 0.0i
System.out.print("z3 = "); z3.pr(); //z3 = 3.6 – 2.2i
System.out.print ("z4 = "); z4.pr(); //z4 = 3.6 – 2.2i
System.out.println(z4); // Працює метод toString() Complex: 3.6 -2.2
z2.add(z3); // z2 = z2 + z3
System.out.print("z2 + z3 = "); z2.pr(); //z2 + z3 = 5.1 – 2.2i
z2.div(z3); //z2 = z2/z3
System.out.print("z2 / z3 = "); z2.pr(); //z2/z3 = 1.303370787 + 0.185393258i
z2 = z2.plus(z2);
System.out.print("z2 + z2 = "); z2.pr(); // z2 + z2 = 2.606741574 + 0.370786516i
z3 = z2.slash(z1);
System.out.print("z2 / z1 = "); z3.pr(); /* на нуль не можна ділити. В будь якій
іншій мові програмування ця програма б не спрацювала і компілятор вивів би
відповідне попередження. Java ж виконала всі дозволені дії, а про не дозволену дію
сигналізувала символом NaN – not a number */
}
}
66
67. Якщо ви уважно все прочитали і звірили результати уазані в моїх коментарях з виведеними на екран, то
будете здивовані їх невідповідністю. Я запозичив цю програму з Інтернет і помітив, що два методи в класі
Сomplex реалізовані невірно. Справдилося моє передчуття, що програмісти не знають математики, а
беручись за математичні задачі вводять в оману і інших. Таким чином, треба дуже критично відноситися
до всього того, що знаходите в Інтернет. А тепер попробуйте знайти і виправити помилку в двох методах
класу Сomplex.
Клас комплекс можна реалізувати і по іншому, не вживаючи this. Одночасно я виправив помилки в
реалізації двох помилкових методів. Чи ви це помітили?
class Complex {
private static final double EPS = 1e-12; // Точність обчислень
private double x, y; // Дійсна і уявна частини
// Чотири конструктори
Complex(double re, double im) { //Constructor 1
x = re; y = im;
}
Complex(double re){x = re; y = 0.0; } //Constructor 2
Complex(){x = 0.0; y = 0.0; } //Constructor 3
Complex(Complex z){x =z.x; y = z.y ; } //Constructor 4
// Методи доступу
public double getRe(){return x;}
public double getIm(){return y;}
public Complex getZ(){return new Complex(x, y);}
public void setRe(double re){x = re;}
public void setIm(double im){y = im;}
public void setZ(Complex z){x = z.x; y = z.x;}
// Модуль і аргумент комплексного числа
public double mod(){return Math.sqrt(x * x + y * y);}
public double arg(){return Math.atan2(x, y);}
// Перевірка: дійсне число?
public boolean isReal(){return Math.abs(y) < EPS;}
public void pr(){ // Виведення на екран
System.out.println(x + (y < 0.0 ? "" : "+") + y + "i");
}
// Перевизначення методів класа Object
public boolean equals(Complex z){
return Math.abs(x - z.x) < EPS && Math.abs(y - z.y) < EPS;
}
public String toString(){
return "Complex: " + x + " " + y;
}
// Методи, що реалізують операції +=, -=, *=, /=
public void add(Complex z){x += z.x; y += z.y;}
public void sub(Complex z){x -= z.x; y -= z.y;}
67
68. public void mul(Complex z){
double t = x * z.x - y * z.y;
y = x * z.y + y * z.x;
x = t;
}
public void div(Complex z){
double m = z.mod()*z.mod();;
double t = x * z.x - y * z.y;
y = (y * z.x - x * z.y) / m;
x = t / m;
}
// Методи, що реалізують операції +, -, *, /
public Complex plus(Complex z){
return new Complex(x + z.x, y + z.y);
}
public Complex minus(Complex z){
return new Complex(x - z.x, y - z.y);
}
public Complex asterisk(Complex z){
return new Complex(
x * z.x - y * z.y, x * z.y + y * z.x);
}
public Complex slash(Complex z){
double m = z.mod()*z.mod();
return new Complex(
(x * z.x - y * z.y) / m, (y * z.x - x * z.y) / m);
}
}
Щоб добре зрозуміти, як користуватися this випишемо всі методи, в яких він використовується і рядом
альтернативний метод без this.
Complex(double re, double im) {
this.re =; this.im = im;
}
/*this допомагає відрізнити поля обєкта
re і im від переданих йому конструктором
параметрів re і im, позначених тими ж
символами*/
Complex(double re){this(re, 0.0); }
Complex(){this(0.0, 0.0); }
/*Фактично, this()(а не this) служить
для виклику попереднього конструктора, в
який передається нульова уявна частина.
Див. пункт 3.8*/
Complex(Complex z){this(z.re, z.im) ; }
/*Теж саме, змінюється лише спосіб
передачі рараметрів у конструктор*/
public void setRe(double re){this.re =
re;}
public void setIm(double im){this.im =
im;}
Complex(double re, double im) {
x = re; y = im;
}
/*Я позначив дійсну і уявну частину
комплексного числа як х і у, тому не
треба вживати this для створення
комплексного числа (обєкта) по переданим
в конструктор значенням re,im.*/
Complex(double re){x = re; y = 0.0; }
Complex(){x = 0.0; y = 0.0; }
/*А тут мені приходиться створювати
цілком нові конструктори*/
Complex(Complex z){x =z.x; y = z.y ; }
public void setRe(double re){x = re;}
public void setIm(double im){y = im;}
/*Тут же ми явно зекономили, завдяки
вдалим позначенням змінних класу і
параметрів конструкторів*/
68
69. Увага! this і this() - це різні речі.
3.12. Метод main()
Всяка програма, оформлена як додаток (application), повинна містити метод з іменем main. Він може бути
один на всі додатки або міститися в деяких класах цього додатку, а може знаходитися і в кожному класі.
Метод main() записується как звичайний метод, може містити будь-які описання і дії, але він обовязково
повинен бути відкритим (public), статичним (static), не повертати значення (void). Його аргументом
обовязково повинен бути масив рядків (string[]). По традиції цей массив називають args, хоча імя може
бути довільним.
Ці особливості виникають із-за того, що метод main() викликається автоматично виконуючою системою
Java в самому початку виконання додатку. При виклику інтерпретатора Java вказується клас, де
записаний метод main(), з якого треба почати виконання. Оскільки класів з методом main() може бути
декілька, можна побудувати додаток з додатковими точками входу, починаючи виконання додатку в
різних ситуаціях із різних класів.
Часто метод main() заносять в кожний клас з метою налагоджування. В цьому випадку в метод main()
включають тести для перевірки роботи всіх методів класа.
При виклику інтерпретатора Java можна передати в метод main() декілька параметрів, які інтерпретатор
заносить в масив рядків. Ці параметри перечисляються в рядку виклику Java через пробіл зразу після
імені класу. Якщо ж параметр містить пробіли, треба взяти його в лапки. Лапки не будуть включені в
параметр, це тільки обмежувачі.
Все це легко зрозуміти на прикладі лістинга 3.5, в якому записана програма, що виводить параметри,
котрі передаються в метод main() при запуску.
Лістинг 3.5. Передача параметрів в метод main()
class Echo {
public static void main(String[] args){
for (int i = 0; i < args.length; i++)
System.out.println("args[" + i +"]="+ args[i]);
}
}
Як бачимо, імя класа не входить в число параметрів. Воно і так відоме у методі main().
Знавцям C/C++
Оскільки в Java імя файла завжди співпадає з іменем класу, котрий містить метод main(), воно не
заноситься в args[0]. Доступ до змінних середовища дозволений не завжди і здійснюється іншим
способом. Деякі значення можна проглянути так: System.getProperties().list(System.out);.
Така властивість метода дозволяє передавати в програму значення, які потім можна присвоїти змінним
69
70. (подібно до сin>> в С++, input в Basic чи read в Pascal. Випробуйте наступні програми.
При запуску цієї програми введіть два числа, наприклад 10 і 15.
class Suma {
public static void main(String[] args){
Integer x = new Integer(args[0]);
Integer y = new Integer(args[1]);
int x1 = x.intValue();
int y1 = y.intValue();
System.out.println(String.valueOf(x1+y1));
}
}
Не лякайтеся складності цієї програми. Вивчіть методи класів Integer і String і не матимете проблем з
перетворюванням типів у Java.
3.13. Де змінні видимі
В мові Java нестатичні змінні можна оголошувати в будь-якому місці коду між операторами. Статичні
змінні можуть бути тільки полями класу, а значит,ь не можуть оголошуватися всередині методів і блоків.
Яка ж область видимості (scope) змінних? З яких методів ми можемо звернутися до тієї чи іншої змінної?
В яких операторах використовувати? Розглянемо на прикладі лістинга 3.6 різні випадки оголошення
змінних.
Лістинг 3.6. Видимість і ініціалізація змінних
class ManyVariables{
static int x = 9, у; // Статичні змінні — поля класа
// Вони видимі у всіх методах і блоках класа
// Змінна у отримує значення 0
static{ // Блок ініціалізації статичних змінних
// Виконується один раз при перому завантаженні класа після
// ініціалізацій в оголошеннях змінних
x = 99; // Оператор виконується зовні всякого метода!
}
int а = 1, р; // Нестатичні змінні — поля екземпляра
// Відомі у всіх методах і блоках класа, в котрих вони
//не перекриті другими змінними з тим же іменем
// Змінна р отримує значення 0
{ // Блок ініціалізації екземпляра
// Виконується при створенні кожного екземпляра після
// ініціалізацій при оголошеннях змінних
р = 999; // Оператор виконується зовні всякого метода!
}
static void f(int b){ // Параметр метода b — локальна
// змінна, відома тільки всередині метода
70
71. int a = 2; // Це друга змінна з тим же іменем "а"
// Вона відома тільки внутрі метода f() і
// тут перекривє першу "а"
int с; // Локальна змінна, відома тільки в методі f()
//Не отримує ніякого початкового значення
//і повинна бути визначена перед застосуванням
{ int c = 555; // Помилка! Спроба повторного оголошення
int х = 333; // Локальна змінна, відома тільки в цьому блоці
}
// Тут змінна х уже невідома
for (int d = 0; d < 10; d++){
// Змінна цикла d відома тількі в циклі
int а = 4; // Помилка!
int e = 5; // Локальна змінна, відома тільки в циклі for
e++; // Ініціалізується при кожному виконанні цикла
System.out.println("e = " + e) ; // Виводиться завжди "е = 6"
}
// Ту змінні d і е невідомі
}
public static void main(String[] args){
int a = 9999; // Локальна змінна, відома тільки всередині метода main()
f (a);
}
}
Зверніть увагу на те, що змінним класу і екземпляра неявно присвоюються нульові значення. Символи
неявно одержують значення 'u0000', логічні змінні — значення false, посилання одержують неявно
значення null.
Локальні ж змінні неявно не ініціюються. Їм повинні або явно присвоюватися значення, або вони повинні
визначатися до першого використання. На щастя, компілятор помічає невизначені локальні змінні і
повідомляє про них.
Увага
Поля класу при оголошенні обнуляються, локальні змінні автоматично не ініціалізуються.
В лістинзі 3.6 зявилася ще одна нова конструкція: блок ініціалізації екземпляра (instance initialization). Це
просто блок операторів в фігурних дужках, але записується він зовні всякого метода, прямо в тілі класу.
Цей блок виконується при сторенні кожного екземпляра, після ініціалізації при оголошенні змінних, але до
виконання конструктора. Він відіграє таку ж роль, як і static-блок для статичних змінних. Для чого ж він
потрібний, адже весь його зміст можна написати на початку конструктора? В тих випадках, коли
конструктор написати не можна, а саме, в безіменних внутрішніх класах.
71
72. 3.16. Заключення
Після прочитання цієї глави ви отримали уявлення про сучасну парадигму програмування — обєктно-
орієнтоване програмування і реалізації цієї парадигми в мові Java.
Не біда, якщо ви не усвоїли зразу принципы ООП. Для вироблення "обєктного" погляду на програмування
потрібні час і практика. Подальші уроки якраз і дадуть вам цю практику. Але спочатку необхідно
ознайомитися з важливими поняттями мови Java — пакетами і інтерфейсами.
Лабораторна робота 2. Створення класів
Куди б ви не звернулися з серйозним питанням, у вас попросять предявити паспорт, де зафіксоване ваше
прізвище, імя і по батькові, дата і місце народження, стать, місце проживання, номер паспорта та ким і
коли він виданий. Для компютерної обробки даних про людей в якійсь установі чи то в масштабі всієї
країни можна створити запис, полями якого будуть всі перечислені вище елементи.
Ось як би ми це зробили
Basic Pascal C++
Type Homo
Name(1 to 3) as String
BirthDate(1 to 3) as Integer
BirstPlace(1 to 4) as String
Sex as String
Address(1 to 4) as String
PassNumber as String
PassEmitter as String
PassEmissionDate(1 to 3) as
Integer
End Type
type
Homo = record
Name: array [1..3] of String;
BirthDate: array[1 .. 3] of Integer;
BirstPlace: array [1..4] of String;
Sex: String;
Address: array [1..4] of String;
PassNumber: String;
PassEmitter: String;
PassEmissionDate: array[1 .. 3] of
Integer;
End;
struct Homo
{
string[3] Name;
int[3] BirthDate;
string[4] BirstPlace;
string Sex;
string[4] Address;
string PassNumber;
string PassEmitter;
int[3] PassEmissionDate;
};
Примітка. У випадку С++ під string розуміємо не базовий тип, а вбудований клас.
А як це все робиться у Java. Автори цієї мови, навчені піввіковим досвідом програмування, дивилися далі.
Сучасні методи обробки даних не обмежуються наведеними вище даними про людину. Так, при сплаті
певних платежів або при відкритті рахунку в банку вас попросять предявити ідентифікаційний код
платника податків. На прийомі у лікаря його цікавить не ваша дата народження, а вік. Якщо ви будете
записуватися на прийом до лікаря через компютерну базу даних, ваш запис крім полів даних повинен
містити і метод, який по поточній даті і по даті народження автоматично визначає ваш вік. Але тоді це вже
не просто запис, а щось більше, ви вже здогадуєтесь – це клас. Поля класу повинні бути захищені від
постороннього доступу з програми, частиної якої стає ваш клас при взаємодії з інформаційною системою.
Тобто, треба заборонити цій програмі зчитувати дані з вашого класу, наприклад, так:
Homo.Address,
адже таким самим способом з програми хтось може і змінити вашу адресу. А це створить вам серйозні
проблеми – у вашому паспорті написано одне, а в інформаційні системі інше. Так можна і квартиру
втратити.
Але якщо зовсім заборонити зчитувати дані, то як же інформаційна система буде їх обробляти? Вихід
знайдено в доступі до даних через методи класу, написані таким чином, щоб дані можна було зчитувати,
але не змінювати зовні.
72
73. Перед вами ставиться наступне завдання.
1. Створити клас Homo з перечисленими вище полями і методами доступу до них та методом
обчислення віку по поточній даті і даті народження. Вік округляти до найближчого цілого.
2. Створити підклас Student з додатковими полями University, Faculty, Speciality, Year і методу
підрахунку середнього балу по пяти дисциплінам : Computer Science, Calculus, Discrete
Mathematic, Algebra, Geometry. Вважайте, що ваша стипендія пропорціональна цьому рейтингу –
це ваш спосіб існування, ваш доход, яким може зацікавитися податкова інспекція.
3. Піднімемося на ступеньку вище і створимо ще два підкласи: Doctor i Dealer. Доход першого
залежить від його досвіду і пропорціональний віку, а другого від кількості проданих автомобілів.
Подумайте, чи не варто створити клас Homo абстрактним.
Візьміть за зразок класи Pet, Dog, Cat i Cow з цієї лекції і напишіть програму, яка б виводила доход
кожного (студента, лікаря і торговця) з указанням професії і способу отримання доходу. Налаштуйте
програму і випробуйте її.
Працюйте самостійно. Ваші програми будете пояснювати на лекціях і демонструвати в лабораторії. У
файлі System.txt можете подивитися, як написаний клас System – дуже цікаво.
73
74. Програмування у Java
Урок 4. Класи-оболонки
• Числові класи
• Клас Boolean
• Клас Character
• Клас Biglnteger
• Клас BigDecimal
• Класс Class
4.1. Класова природа Java
Java — повністю об’єктно-орієнтована мова. Це означає, що все, що тільки можна, в Java представлено
обєктами.
Вісім примітивних типів порушують це правило. Вони залишені в Java із-за багаторічної звички до чисел і
символів. Та й арифметичні дії зручніше і скоріше виконувати зі звичайними числами, а не з об’єктами
класів.
Але і для цих типів у мові Java є відповідні класи — класи-оболонки (wrapper) примітивних типів.
Звичайно, вони призначені не для обчислень, а для дій, типових при роботі з класами — створення
об’єктів, перетворення об’єктів, одержання числових значень об’єктів у різних формах і передачі об’ємів у
методи по посиланню.
На рис. 4.1 показана одна із віток ієрархії класів Java. Для кожного примітивного типу є відповідний клас.
Числові класи мають спільного предка — абстрактний клас Number, в якому описані шість методів, котрі
повертають числове значення, що міститься в класі, приведене до відповідного примітивного типу:
byteValue (), doubleValue (), floatValue (), intValue(), longValue (), shortValue (). Прогляньте уважно файл
Number.html у папці java_lang і ще раз продумайте механізм використання абстрактних класів.
Указані вище методи перевизначені у кажному із шести числових класів-оболонок.
Рис. 4.1.
Помимо методу порівняння об’єктів equals(), перевизначеного із класу Оbject, всі описані в цій лекції
класи, крім Boolean і Сlass, мають метод compareTo (), котрий порівнює числове значення, що міститься в
даному об’єкті, з числовим значенням об’єкту — аргументу методу compareTo(). В результаті роботи
методу отримується ціле значення:
• 0, якщо значення рівні;
• від’ємне число (-1), якщо числове значення в даному об’єкті менше, ніж в об’єкті-аргументі;
• додатне число (+1), якщо числове значення в даному об’єкті більше числового значення, що
74
75. міститься в аргументі.
4.2. Числові класи
В кожному із шести числових класів-оболонок є статичні методи перетворення рядка символів типу String,
котрий представляє число, у відповідний примітивний тип: Byte.parseByte(), Double.parseDouble(),
Float.parseFloat(), Integer.parselnt(), Long.parseLong(), Short.parseShort(). Вихідний рядок типу string, як
завжди у статичних методах, задається як аргумент методу. Ці методи корисні при введенні даних в поля
введення, обробці параметрів командного рядка, т. е. тобто скрізь, де числа представляються рядками
цифр зі знаками плюс або мінус і десятковою точкою.
В кожному із цих класів є статичні константи MAX_VALUE і MIN_VALUE, котрі показують діапазон
числових значень відповідних примітивних типів. В класах Double і Float єсть іще константи
POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, про які йшла мова раніше, і логічні методи перевірки
isNan(), isInfinite().
Якщо ви добре знаєте двійкове представлення дійсних чисел, то можете скористатися статичними
методами floatToIntBits() і doubleToLongBits(), котрі перетворюють дійсне значення в ціле. Дійсне число
задається як аргумент методу. Потім ви можете змінити окремі біти побітними операціями і перетворити
змінене ціле число назад у дійсне значення методами intBitsToFloat() і longBitsToDouble().
Статичними методами toBinaryString(), toHexString() і toOctalString() класів Іnteger і Long можна
перетворити цілі значення типів int і long, задані як аргумент методу, в рядок символів, котрий показує
двійкове, шістнадцятеричне або восьмеричне представлення числа.
Передивіться файли Byte, Double, Float, Integer, Long, Short у папці java-langNumber і научіться
користуватися методами цих класів.
В лістинзі 4.1 показано застосування цих методів, а на рис. 4.2 реультат виконання програми
Лістинг 4.1. Методи числових класів
class NumberTest{
public static void main(String[] args){
int i = 0;
short sh = 0;
double d = 0;
Integer k1 = new Integer(55);
Integer k2 = new Integer(100);
Double d1 = new Double(3.14);
try{
i = Integer.parseInt(args[0]);
sh = Short.parseShort(args[0]);
d = Double.parseDouble(args[1]);
d1 = new Double(args[1]);
k1 = new Integer(args[0]);
}catch(Exception e){}
double x = 1.0/0.0;
System.out.println("i = " + i) ;
System.out.println("sh - " + sh) ;
System.out.println("d = " + d) ;
System.out.println("k1.intValue() = " + k1.intValue());
System.out.println("d1.intValue() = "+ d1.intValue());
System.out.println("k1 > k2? " + k1.compareTo(k2));
System.out.println ("x = " + x);
System.out.println("x isNaN? " + Double.isNaN(x));
System.out.println("x isInfinite? " + Double.isInfinite(x));
75
76. System.out.println("x == Infinity? " +
(x == Double.POSITIVE_INFINITY) );
System.out.println("d = " + Double.doubleToLongBits(d));
System.out.println("i = " + Integer.toBinaryString(i));
System.out.println("i = " + Integer.toHexString(i));
System.out.println("i = " + Integer.toOctalString(i));
}
}
Рис. 4.2
Методи parseІnt() і конструктори класів вимагають обробики виключень, тому в лістинг 4.1 вставлено блок
try{} catch(){}. Обробку виключних ситуацій ми розберемо в лекції 16.
Передивіться файли Byte, Double, Float, Integer, Long, Short у папці java.lang і научіться користуватися
методами цих класів.
4.3. Клас Boolean
Це дуже невеликий клас, призначений головним чином для того, щоб передавати логічні значення в
методи по посиланню.
Конструктор Boolean (String s) створює об’єкт, котрий містить значення true, якщо рядок s рівний "true" в
будь-якій комбінації регістрів літер, і значення false — для будь-якого іншого рядка.
Логічний метод booleanValue() повертає логічне значення, що зберігається в об’єкті.
Передивіться файл Boolean у папці java.lang і научіться користуватися методами цього класу.
76
77. 4.4. Класс Character
В цьому класі зібрані статичні константи і методи для роботи з окремими символами.
Статичний метод digit(char ch, int radix) переводить цифру ch системи числення з основою radix в її
числове значення типу int.
Статичний метод forDigit(int digit, int radix) робить обернене перетворення цілого числа digit у відповідну
цифру (тип char) у системі числення з основою radix.
Основа системи числення повинна знаходитися в діапазоні від Character.MIN_RADIX до
Character.MAX_RADIX.
Метод toString() переводить символ, котрий міститься в класі, в рядок з тим же символом.
Статичні методи toLowerCase(), toUpperCase(), toTitleCase() повертають символ, що міститься в класі, у
вказаному регістрі. Останній із цих методів служить для правильного переводу у верхній регістр чотирьох
кодів Unicode, котрі не виражаються одним символом.
Декілька статичних логічних методів перевіряють різні характеристики символу, переданого в якості
аргументу методу:
• isDefined() — виясняє, чи визначений символ в кодуванні Unicode;
• isDigit() — перевіряє, чи являється символ цифрою Unicode;
• isIdentifierIgnorable() — виясняє, чи можна використовувати символ в ідентифікаторах;
• isISOControl() — визначає, чи є символ управляючим;
• isJavaІdentifierPart() — виясняє, чи можна використовувати символ в ідентифікаторах;
• isJavaIdentifierStart() — виясняє, чи може символ бути на початку ідентифікатору;
• isLetter() — перевіряє, чи являється символ літерою Java;
• IsLetterOrDigit() — перевіряє, чи являється символ літерою або цифрою Unicode;
• isLowerCase() — визначає, чи символ записаний в нижньому регістрі;
• isSpaceChar() — виясняє, чи є символ пробілом в Unicode;
• isTitleCase() — перевіряє, чи являється символ титульним;
• isUnicodeldentifierPart() — виясняє, чи можна використовувати символ в іменах Unicode;
• isUnicodeIdentifierStart() — перевіряє, чи являється символ літерою Unicode;
• isUpperCase() — визначає, чи символ записаний у верхньому регістрі;
• isWhiteSpace() — виясняє, чи являється символ пробілом.
Точні діапазони управляючих символів, поняття верхнього і нижнього регістра, титульного символу,
пробільних символів, краще всього подивитися по документації Java API.
Лістинг 4.2 демонструє використання цих методів, а на рис. 4.3 показаний результат цієї програми.
Лістинг 4.2. Методи класу Character в програмі CharacterTest
class CharacterTest{
public static void main(String[] args){
char ch = '9';
Character c = new Character(ch);
System.out.println("ch = " + ch);
System.out.println("c.charValue() = " + c.charValue());
System.out.println("number of 'A' = " + Character.digit('A', 16));
System.out.println("digit for 12 = " + Character.forDigit(12, 16));
System.out.println("ch = " + c.toString());
System.out.println("ch isDefined? " + Character.isDefined(ch));
System.out.println("ch isDigit? " + Character.isDigit(ch));
77
78. System.out.println("ch isIdentifierIgnorable? " +
Character.isIdentifierIgnorable(ch));
System.out.println("ch isISOControl? " + Character.isISOControl(ch));
System.out.println("ch isJavaldentifierPart? " +
Character.isJavaIdentifierPart(ch));
System.out.println("ch isJavaldentifierStart? " +
Character.isJavaIdentifierStart(ch));
System.out.println("ch isLetter? " + Character.isLetter(ch));
System.out.println("ch isLetterOrDigit? " + Character.isLetterOrDigit(ch));
System.out.println("ch isLowerCase? " + Character.isLowerCase(ch));
System.out.println("ch isSpaceChar? " + Character.isSpaceChar(ch));
System.out.println("ch isTitleCase? " + Character.isTitleCase(ch));
System.out.println("ch isUnicodeIdentifierPart? " +
Character.isUnicodeIdentifierPart(ch));
System.out.println("ch isUnicodeIdentifierStart? " +
Character.isUnicodeIdentifierStart(ch));
System.out.println("ch isUpperCase? " + Character.isUpperCase(ch));
System.out.println("ch isWhitespace? " + Character.isWhitespace(ch)); }
}
Рис. 4.3
В клас Character вкладені класи Subset і UnicodeBlock, причому клас Unicode і ще один клас, inputSubset,
являється розширенням класу Subset, як це видно на рис. 4.1. Об’єкти цього класу містять підмножини
Unicode.
Передивіться файл Character у папці java.lang і визначте, чи всі методи цього класу були задіяні у
попередній програмі.
Разом з класами-оболонками зручно розглянути два класи для роботи з як завгодно великими числами.
4.5. Клас Biglnteger
Всі примітивні цілі тип мають обмежений діапазон значень. В цілочисельній арифметиці Java немає
78
79. переповнення, цілі числа приводяться по модулю, рівному діапазону значень. Для того щоб можна було
виконувати цілочисельні обчислення з будь-якою розрядністю, у склад Java API введений клас Biglnteger,
котрий зберігається в пакеті java.math. Цей клас розширює клас Number, значить, в ньому перевизначені
методи doubleValue(), floatValue(), intValue(), longValue(). Методи byteValue() і shortValue() не
перевизначені, а прямо наслідуються від класу Number.
Дії з об’єктами класу Biglnteger не приводять ні до переповнення, ні до приведення по модулю. Якщо
результат операції великий, то число розрядів просто збільшується. Числа зберігаються в двійковій формі
с додатковим кодом. Перед виконанням операції числа вирівнюються по довжині розповсюдженням
знакового розряду.
Шість конструкторів класу створюють об’єкт класу Biglnteger із рядка символів (знака числа і цифр) або із
масиву байтів. Дві константи — ZERO і ONE — моделюють нуль і одиницю в операціях з об’єктами класу
Biglnteger. Метод toByteArray() перетворює об’єкт в масив байтів.
Більшість методів класу Biglnteger моделюють цілочисельні операції і функції, повертаючи об’єкт класу
Biglnteger:
• abs() — повертає об’єкт, котрий містить абсолютну величину числа, яке зберігається в даному
об’єкті this;
• add(x) — операція this + х;
• and(x) — операція this & х;
• andNot(x) — операція this & (~х) ;
• divide (x) — операція this / х;
• divideAndRemainder(х) — повертає масив із двох об’єктів класу Biglnteger, який містить частку і
остачу від ділення this на х;
• gcd(x) — найбільший спільний дільник абсолютних значень об’єкту this і аргументу х;
• mах(х) — найбільше із значень об’єкту this і аргумента х;
• min(x) — найменше із значень об’єкту this і аргументу х;
• mod(x) — остача від ділення об’єкту this на аргумент методу х;
• modІnverse(x) — остача від ділення числа, оберненого об’єкту this, на аргумент х;
• modPow(n, m) — остача від ділення об’єкта this, піднесеного до степеня n, на m;
• multiply (х) —операція this * х;
• negate() — зміна знаку числа, що зберігається в об’єкті;
• not() — операція ~this;
• оr(х) — операція this | х;
• pow(n) — операція піднесення числа, яке зберігається в об’єкті, до степеня n;
• remainder(х) —операція this % х;
• shiftLeft (n) — операція this « n ;
• shiftRight (n) — операція this » n;
• signum() — функція sign (x);
• subtract (x) — операція this - x;
• xor(x) — операція this ^ x.
В лістинзі 4.3 приведені приклади використання даних методів
Лістинг 4.3. Методи класу Biglnteger в програмі BiglntegerTest
import java.math.BigInteger;
class BigIntegerTest{
public static void main(String[] args){
BigInteger a = new BigInteger("99999999999999999") ;
BigInteger b = new BigInteger("88888888888888888888");
System.out.println("bits in a = " + a.bitLength());
System.out.println("bits in b = " + b.bitLength());
System.out.println("a + b = " + a.add(b));
System.out.println("a & b = " + a.and(b));
79
80. System.out.println("a & ~b = " + a.andNot(b));
System.out.println("a / b = " + a.divide(b));
BigInteger[] r = a.divideAndRemainder(b);
System.out.println("a / b: q = " + r[0] + ", r = " + r[1]);
System.out.println("gcd(a, b) = " + a.gcd(b));
System.out.println("max(a, b) = " + a.max(b));
System.out.println("min(a, b) = " + a.min(b));
System.out.println("a mod b = " + a.mod(b));
System.out.println("I/a mod b = " + a.modInverse(b));
System.out.println("а^n mod b = " + a.modPow(a, b));
System.out.println("a * b = " + a.multiply(b));
System.out.println("-a = " + a.negate());
System.out.println ("~a = " + a.not());
System.out.println("a | b = " + a.or(b));
System.out.println("а ^ 3 = " + a.pow(3));
System.out.println("a % b = " + a.remainder(b));
System.out.println("a « 3 = " + a.shiftLeft(3));
System.out.println("a » 3 = " + a.shiftRight(3));
System.out.println("sign(a) = " + a.signum());
System.out.println("a - b = " + a.subtract(b));
System.out.println("а ^ b = " + a.xor(b));
}
}
Рис. 4.4
Зверніть увагу на те, що в програму лістингу 4.3 треба імпортувати пакет java.math. Передивіться файл
BigInteger у папці java.math і визначте, чи всі методи цього класу були задіяні у попередній програмі.
80
81. 4.6. Клас Big Decimal
Клас BigDecimal розташований в пакеті java.math.
Кожний об’єкт цього класу зберігає два цілочисельні значення: мантису дійсного числа у вигляді об’єкту
класу Biglnteger, і невід’ємний десятковий порядок числа типу int.
Наприклад, для числа 76.34862 буде зберігатися мантиса 7 634 862 в об’єкті класу Biglnteger, і порядок 5
як ціле число типу int. Таким чином, мантиса може містити будь-яку кількість цифр, а порядок обмежений
значенням константи integer.MAX_VALUE. Результат операції над об’ємами класу BigDecimal
округляється по одному із восьми правил, визначеними наступними статичними цілими константами:
• ROUND_CEILING — округлення в сторону більшого цілого;
• ROUND_DOWN — округлення до нуля, до меншого по модулю цілого значення;
• ROUND_FLOOR — округлення до меншого цілого;
• ROUND_HALF_DOWN — округлення до найближчого цілого, середнє значення округляється до
меншого цілого;
• ROUND_HALF_EVEN — округлення до найближчого цілого, середнє значення округляється до
парного числа;
• ROUND_HALF_UP — округлення до найближчого цілого, середнє значення округляється до
більшого цілого;
• ROUND_UNNECESSARY — вважається, що результат буде цілим, і округлення не знадобиться;
• ROUND_UP — округлення від нуля, до більшого по модулю цілому значенню.
В класі BigDecimal чотири конструктори:
• BigDecimal (Biglnteger bi) — об’єкт буде зберігати велике ціле b і порядок рівний нулю;
• BigDecimal (Biglnteger mantissa, int scale) — задається мантиса mantissa и невідємний порядок
scale обєкту; якщо порядок scale відємний, виникає виключна ситуація;
• BigDecimal (double d) — обєкт буде містити дійсне число подвійної точності d; якщо значення d
нескінчено або NaN, то виникає виключна ситуація;
• BigDecimal (String val) — число задається рядком символів val, який повинен містити запис числа
по правилах мови Java.
При використанні третього із перерахованих конструкторів виникає неприємна особливість, відмічена в
документації. Оскільки дійсне число при переводі в двійкову форму представляється, як правило,
нескінченим двійковим дробом, то при створенні обєкту, наприклад, BigDecimal(0.1), мантиса, що
зберігається в обєкті, буде дуже великою. Вона показана на рис. 4.5. Але при створенні такого ж обєкту
четвертим конструктором, BigDecimal ("0.1"), мантиса буде рівна просто 1.
В класі перевизначені методи doubleValue(), floatValue(), intValue(), longValue().
Більшість методів цього класу моделюють операції з дійсними числами. Вони повертаютьт обєкт класу
BigDecimal. Тут буква х означає обєкт класу BigDecimal, буква n - ціле значення типу int, буква r - спосіб
округлення, одну із восьми перечислених вище констант:
abs() — абсолютное значення обєкту this;
add(x) — операція this + х;
divide(х, r) — операція this / х з округленням по способу r;
divide(х, n, r) — операція this / х із зміною порядку і округленням по способу r;
mах(х) — найбільше із this і х;
min(x) — найменше із this і х;
movePointLeft(n) — зсув вліво на n розрядів;
movePointRight(n) — зсув вправо на n розрядів;
81
82. multiply(х) — операція this * х;
negate() — повертає обєкт з протилежним знаком;
scale() — повертає порядок числа;
setscale(n) — установлює новий порядок n ;
setscale(n, r) — установлює новий порядок n і округляє число при необхідності по способу r;
signum() — знак числа, котре зберігається в обєкті;
subtract(х) — операція this - х;
toBigInteger() — округлення числа, котре зберігається в обєкті;
unscaledValue() — повертає мантису числа.
Лістинг 4.4. Методи класу BigDecimal в програмі BigDecimalTest
import java.math.*;
class BigDecimalTest{
public static void main( String [] args) {
BigDecimal x = new BigDecimal("-12345.67890123456789");
BigDecimal y = new BigDecimal("345.7896e-4");
BigDecimal z = new BigDecimal(new BigInteger("123456789"),8);
System.out.println("|x| = " + x.abs());
System.out.println("x + у = " + x.add(y));
System.out.println("x / у = " + x.divide(y, BigDecimal.ROUND_DOWN));
System.out.println("х / у = " + x.divide(y, 6, BigDecimal.ROUND_HALF_EVEN));
System.out.println("max(x, y) = " + x.max(y));
System.out.println("min(x, y) = " + x.min(y));
System.out.println("x « 3 = " + x.movePointLeft(3));
System.out.println("x » 3 = " + x.movePointRight(3));
System.out.println("x * у = " + x.multiply(y));
System.out.println("-x = " + x.negate());
System.out.println("scale of x = " + x.scale());
System.out.println("increase scale of x to 20 = " + x.setScale(20));
System.out.println("decrease scale of x to 10 = " +
x.setScale (10, BigDecimal.ROUND_HALF_UP)) ;
System.out.println("sign(x) = " + x.signum());
System.out.println("x - у = " + x.subtract(y));
System.out.println("round x = " + x.toBigInteger());
System.out.println("mantissa of x = " + x.unscaledValue());
System.out.println("mantissa of 0.1 =n= " +
new BigDecimal(0.1).unscaledValue());
} }
Передивіться файл BigDecimal у папці java.math і визначте, чи всі методи цього класу були задіяні у
попередній програмі.
Рис. 4.5
Приведе
мо ще
один
приклад.
Напише
мо
простень
кий
калькуля
тор,
котрий
виконує
чотири арифметичні дії з числами довільної величинb. Він працює з командного рядка. Програма
82
83. представлена в лістинзі 4.5, а приклади використання калькулятора — на рис. 4.6.
Лістинг 4.5. Найпростіший калькулятор
import java.math.*;
class Calc{
public static void main(String[] args){
if (args.length < 3){
System.err.println("Usage: Java Calc operand operator operand");
return;
}
BigDecimal a = new BigDecimal(args[0]);
BigDecimal b = new BigDecimal(args[2]);
switch (args[1].charAt(0)){
case '+': System.out.println(a.add(b)); break;
case '-': System.out.println(a.subtract(b)); break;
case '*': System.out.println(a.multiply(b)); break;
case '/': System.out.println(a.divide(b,
BigDecimal.ROUND_HALF_EVEN)); break;
default : System.out.println("Invalid operator");
}
}
}
Рис. 4.6
Чому символ множення - зірочка — заключений на рис. 4.6 в лапки? "Юніксоїдам" це зрозуміло, а для
других дамо коротке пояснення. Це особливість операційної системи, а не мови Java. Введений з
клавіатури рядок спочатку проглядаює командна оболонка (shell) операційної системи, а зірочка для неї -
вказівка підставить на це місце всі імена файлів із поточного каталогу. Оболонка зробить це, і
інтерпретатор Java одержить від неї довгий рядок, в якому замість зірочки стоять імена файлів через
пробіл. Зірочка в лапках розуміється командною оболонкою як звичайний символ. Командна оболонка
знімає лапки і передає інтерпретатору Java те, що потрібно.
4.7. Клас Class
83
84. Клас Object, котрий стоїть на чолі ієрархії класів Java, представляє всі обєкти, що діють в системі,
являється їх спільною оболонкою. Всякий обєкт можна вважати екземпляром класу Object.
Клас з іменем Class представляє характеристики класу, екземпляром якого являється обєкт. Він зберігає
інформацію про те, чи не являється обєкт на самім ділі інтерфейсом, масивом або примітивним типом,
який суперклас обєкту, яке імя класу, які в ньому конструктори, поля, методи і вкладені класи.
В класі Сlass немає конструкторів, екземпляри цього класу створюються виконуючою системою Java під
час завантаження класу і представляється методом getClass() класу Оbject, наприклад:
String s = "Це рядок";
Class с = s.getClass();
Статичний метод forName(String class) повертає обєкт класу Class для класy, указаного в аргументі,
наприклад:
Class cl = Class.forName("Java.lang.String");
Але цей спосіб створення обєкту класу Class вважається застарілим (deprecated). В нових версіях JDK
для цієї мети використовується спеціальна конструкція — до імені класу через точку додається слово
class:
Class c2 = Java.lang.String.class;
Логічні методи isArray(), isIntetface(), isPrimitive() дозволяють уточнити, чи не являється обєкт масивом,
інтерфейсом або примітивним типом.
Якщо обєкт посилочного типу, то можна одержати дані про вкладені класи, конструктори, методи і поля
методами getDeclaredcCasses(), getDeclaredConstructors(), getDeclaredMethods(), getDeclaredFields(), у
вигляді масиву класів, відповідно Class, Constructor, Method, Field. Останні три класи розташованs в пакеті
java.lang.reflect і містять дані про конструктори, поля і методи аналогічно тому, як класс Сlass зберігає
дані про класи.
Методи getClasses(), getConstructors(), getlnterfaces(), getMethods(), getFields() повертають такі ж масиви,
але не всіх, а тільки відкритих членів класу.
Метод getSuperClass() повертає суперклас обєкту посилочного типу, getPackage() - пакет, getModifiers() -
модифікатори класу в бітовій формі. Модифікатори можна потім розшифрувати методами класу Modifier із
пакету java.lang.reflect.
Лістинг 4.6 показує застосування цих методів, а рис. 4.7 — виведення результатів
Лістинг 4.6. Методи класу Class в програмі ClassTest
import java.lang.reflect.*;
class ClassTest{
public static void main(String[] args){
Class c = null, c1 = null, c2 = null;
Field[] fld = null;
String s = "Some string";
c = s.getClass();
try{
c1 = Class.forName("java.lang.String"); // Старий стиль
c2 = java.lang.String.class; // Новий стиль
84
85. if (!c1.isPrimitive())
fld = c1.getDeclaredFields(); // Всі поля класу String
}catch(Exception e){}
System.out.println("Class c: " + c);
System.out.println("Class c1: " + c1);
System.out.println("Class c2: " + c2);
System.out.println("Superclass c: " + c.getSuperclass());
System.out.println("Package c: " + c.getPackage());
System.out.println("Modifiers c: " + c.getModifiers());
for(int i = 0; i < fld.length; i++)
System.out.println(fld[i]);
}
}
Рис. 4.7
Методи, котрі повертають властивості класів, спричиняють виключні ситуації, які вимагають обробки.
Тому в програму введено блок try{} catch() {}. Розгляд обробки виключних ситуацій ми відкладаємо до
уроку 16.
Передивіться файл Class у папці java.lang і визначте, чи всі методи цього класу були задіяні у попередній
програмі.
Лабораторна робота 3. Використання числових класів
Доповнити програму Calculator наступними функціями: обчислення тригонометричних функцій,
включаючи arc tg x, показникову і степеневу, включаючи квадратний і кубічний корені, логарифми
натуральний і десятковий.
85
86. Програмуванн в Java
УРОК 5. Робота з рядками
• Класс String
• Як створити рядок
• Зєднання рядків
• Маніпуляції з рядками
• Як узнати довжину рядка
• Як вибрати символи із рядка
• Як вибрати частину рядка (підрядок)
• Як порівняти рядки
• Як знайти символ в рядку
• Як знайти підрядок
• Як змінити регістр літер
• Як замінити окремий символ
• Як видалити пробіли на початку і в кінці рядка
• Як перетворити дані іншого типу в рядок
• Клас StringBuffer
• Конструктори
• Як додати підрядок
• Як вставити підрядок
• Як видалити підрядок
• Як видалити символ
• Як замінити підрядок
• Як перевернути рядок
• Синтаксичний разбір рядка
• Клас StringTokenizer
• Заключення
Дуже важливе місце в обробці інформації займає робота з текстами. Як і багато чого, текстові рядки в
мові Java являються обєктами. Вони представляються екземплярами класу String або класу StringBuffer .
Спочатку це незвично і здається занадто громіздким, але, призвичаївшись, ви оціните зручність роботи з
класами, а не с масивами символів. Звичайно, можливо занести текст в масив символів типу char або
навіть в масив байтів типу byte, але тоді ви не зможете використати готові методи роботи з текстовими
рядками.
Для чого в мову введені два класи для зберігання рядків? В обєктах класу String зберігаються рядки-
константи незмінної довжини і змісту, так би мовити, відлиті в бронзі. Це значно прискорює обробку рядків
і дозволяє економити память, розділяючи рядок між обєктами, які його використовують. Довжину рядків,
що зберігаються в обєктах класу StringBuffer, можна змінювати, вставляючи і додаючи рядки і символи,
видаляючи підрядки або зчіплючи декілька рядків у один рядок. В багатьох випадках, коли треба змінити
довжину рядка типу String , компілятор Java неявно перетворює його до типу StringBuffer, змінює довжину,
потім перетворює назад в тип String. Наприклад, наступна дія
String s = "Це" + " один " + "рядок";
компілятор виконує так:
String s = new StringBuffer().append("Це").append(" один ").append("рядок").toString();
Буде створений обєкт класу StringBuffer , в нього послідовно додані рядки "Це", " один ", "рядок", і
одержаний обєкт класу StringBuffer буде приведений до типу String методом toString (). Нагадаємо, що
символи в рядках зберігаються в кодировці Unicode, в якій кожен символ займає два байти. Тип кожного
символу char.
86
87. 5.1. Клас String
Перед роботою з рядком його треба створити. Це можна зробити різними способами.
5.1.1. Як створити рядок.
Найпростіший спосіб створити рядок — це організувати посилання типу String на рядок-константу:
String s1= "Це рядок.";
Якщо константа довга, можна записати її в декількох рядках текстового редактору, звязуюючи їх
операцією зчеплення:
String s2 = "Це довгий рядок, " + "записаний в двох рядках вихідного тексту";
Зауваження
Не забувайте різницю між пустим рядком String s = "", який не містить жодного символу, і пустою
посилкою String s = null, котра не вказує ні на один рядок і не являється обєктом.
Самий правильний спосіб створити обєкт з точки зору ООП — це викликати його конструктор в операції
new. Клас String надає у ваше розпорядження девять конструкторів:
• String() — створюється обєкт з пустим рядком;
• String (String str) — із одного обєкта створюється інший, тому цей конструктор використовується
рідко;
• String (StringBuffer str) — перетворена копія обєкту класу BufferString;
• String(byte[] byteArray) — обєкт створюється із масиву байтів byteArray;
• String (char [] charArray) — обєкт створюється із масиву charArray символів Unicode;
• String (byte [] byteArray, int offset, int count) — обєкт створюється із частини масиву байтів byteArray,
починається з індексу offset і містить count байтів;
• String (char [] charArray, int offset, int count) — те ж саме, але масив складається із символів
Unicode;
• String(byte[] byteArray, String encoding) — символи, записані в масиві байтів, задаються в Unicode-
рядку з врахування кодування encoding;
• String(byte[] byteArray, int offset, int count, String encoding) — те ж саме, але тільки для частини
масиву.
При неправильному заданні індексів offset, count або кодування encoding виникає виключна ситуація.
Конструктори, що використовують масив байтів byteArray, призначені для створення Unicode-рядка із
масива байтових ASCII-кодувань символів. Така ситуація виникає при читанні ASCII-файлів, одержанні
інформації із бази даних або при передачі інформації по мережі. В самому простому випадку компілятор
для отримння двобайтових символів Unicode додасть до кожного байту старший нульовий байт.
Получиться діапазон ' u0000 ' — ' u00ff ' кодування Unicode, відповідний кодам Latin 1. Тексти на кирилиці
будуть виведені неправильно.
Якщо ж на компютері зроблені місцеві установки, як говорять на жаргоні "установлена локаль" (locale) (в
MS Windows це виконується утилітою Regional Options у вікні Control Panel), то компілятор, прочитавши ці
установки, створить символи Unicode, що відповідають місцевому кодуванню сторінки. В русифікованому
варіанті MS Windows , яким ми в більшості випадків і користуємося, це звичайно кодова сторінка СР1251.
Якщо вихідний масив с кирилличним ASCII-текстом був у кодуванні СР1251, то рядок Java буде
87
88. створений правильно. Кирилиця попаде в свій діапазон 'u0400'—'u04FF' кодування Unicode. Але у
кирилиці єсть ще, по меншій мірі, чотири кодування.
• В MS-DOS застосовується кодування СР866.
• В UNIX звичайно застосовується кодування KOI8-R.
• На компютерах Apple Macintosh використовується кодування MacCyrillic.
• Єсть ще і міжнародне кодування кирилиці ISO8859-5;
Наприклад, байт 11100011 (0xЕ3 в шістнадцятеричній системі) в кодуванні СР1251 представляє
кириличну літеру Г, в кодуванні СР866 — літеру У, в кодуванні KOI8-R — літеру Ц, в ISO8859-5 — літеру
у, в MacCyrillic — літеру г.
Якщо вихідний кириличний ASCII-текст був в одному із цих кодувань, а місцеве кодування СР1251, то
Unicode-символи рядка Java не будуть відповідати кирилиці. В таких випадках використовуються останні
два конструктори, в яких параметром encoding указується, яку кодову таблицю використовувати
конструктору при створенні рядка.
Питання кирилізації ми ще будемо обговорювати в уроках 9 і 18, а поки що замітьте, що при створенні
рядка із масиву байтів краще вказати те ж саме кириличне кодування, в якому записаний масив. Тоді ви
одежите рядок Java з правильними символами Unicode.
При виведенні ж рядка на консоль, у вікно, у файл або при передачі по мережі краще перетворити рядок
Java з символами Unicode по правилам виведення в потрібне місце.
Ще один спосіб створити рядок — це використати два статичні методи
copyValueOf(chart] charArray) і copyValueOf(char[] charArray, int offset, int length).
Вони створюють рядок по заданому масиву символів і повертають його в якості результату своєї роботи.
Наприклад, післе виконання наступного фрагменту програми
char[] с = ('С', 'и', 'м', 'в', ' о', 'л', 'ь', 'н', 'и', 'й'};
String s1 = String.copyValueOf(с);
String s2 = String.copyValueOf(с, 3, 7);
одержимо в обєкті s1 рядок "Символьний", а в обєкті s2 - рядок "вольний".
5.1.2. Зчеплення рядків
З рядками можна проводити операцію зчеплення рядків (concatenation), що позначається знаком плюс +.
Ця операція створює новий рядок, просто складений із першого і другого рядка, як показано на початку
даного уроку. Її можна застосувати і до констант, і до змінних. Наприклад:
String attention = "Увага: ";
String s = attention + "невідомий символ";
Друга операція - присвоювання += застосовується до змінних в лівій частині:
attention += s;
Оскільки операція + перевантажена з додавання чисел на зчеплення рядків, постає питання про
88
89. пріоритет цих операцій. У зчепленні рядків пріоритет вище, ніж у додавання, тому, записавши "2" + 2 + 2,
одержимо рядок "222". Але, записавши 2 + 2 + "2", одержимо рядок "42", оскільки дії виконуються зліва
направо. Якщо ж запишемо "2" + (2 + 2), то одержимо "24".
5.1.3. Маніпуляції рядками
В класі String єсть багато методів для роботи з рядками. Подивимось, що вони дозволяють робити.
5.1.3.1. Як узнати довжину рядка
Для того щоб узнати довжину рядка, тобто кількість символів в ній, треба звернутися до методу length():
String s = "Write once, run anywhere.";
int len = s.length();
або ще простіше
int len = "Write once, run anywhere.".length();
оскільки рядок-константа — повноцінний обєкт класу String. Замітьте, що рядок — це не масив, у нього
немає поля length.
Уважний читач, що вивчив рис. 4.7, готовий зі мною не погодитися. Ну, що ж, дійсно, символи
зберігаються в масиві, але він закритий, як і всі поля класу String.
5.1.3.2. Як вибрати символи із рядка
Вибрати символ з індексом ind (індекс першого символу рівний нулю) можна методом charAt(int ind). Якщо
індекс ind відємний або не менший за довжину рядка, виникає виключна ситуація. Наприклад, після
изначення
char ch = s.charAt(3);
змінна ch буде мати значення 't'
Всі символи рядка у вигляді массиву символів можна отримати методом
toCharArray(), що повертє масив символів.
Якщо ж потрібно включити в масив символів dst, починаючи з індексу ind масиву підрядок від індексу
begin включно до індексу end виключно, то користуйтеся методом getChars(int begin, int end, char[] dst, int
ind) типу void. В масив буде записано end - begin символів, котрі займуть елементи масиву, починаючи з
індексу ind до індексу ind + (end - begin) - 1. Цей метод створює виключну ситуацію в наступних випадках:
• посилка dst = null;
• індекс begin відємний;
• індекс begin більше індексу end;
• індекс end більше довжини рядка;
• індекс ind відємний;
• ind + (end - begin) > dst.length.
Наприклад, після виконання
89
90. char[] ch = {'К', 'о', 'р', 'о', 'л', 'ь', ' ', 'л', 'е', 'т', 'а'};
"Пароль легко найти".getChars(2, 8, ch, 2);
результат буде такий:
ch = {'К', 'о', 'р', 'о', 'л', 'ь ', ' ', 'л', 'е', 'т', 'а'};
Якщо треба одержати масив байтів, що містить всеі символи рядка в байтовому кодуванні ASCII, то
використовуйте метод getBytes(). Цей метод при переводі символів із Unicode в ASCII використовує
локальну кодову таблицю. Якщо ж треба одержати масив байтів не в локальнному кодуванні, а в якомусь
іншому, використовуйте метод getBytes(String encoding).
5.1.3.4. Як вибрати підрядок
Метод substring(int begin, int end) виділяє підрядок від символу з індексом begin включно до символу з
індексом end виключно. Довжина підрядка буде рівною end - begin.
Метод substring (int begin) виділяє підрядок від індекса begin включно до кінця рядка.
Якщо індекси відємні, індекс end більше довжини рядка або begin більше ніж end, то виникає виключна
ситуація. Наприклад, післе виконання
String s = "Write onсe, run anywhere.";
String sub1 = s.substring(6, 10);
String sub2 = s.substring(16);
одержимо в рядку sub1 значенння "once", а в sub2 - значення "anywhere".
5.1.3.4. Як порівняти рядки
Операція порівняння == співставляє лише посилання на рядки. Вона виясняє, чи вказують посилання на
один і той же рядок. Наприклад, для рядків
String s1 = "Якийсь рядок";
String s2 = "Інший рядок";
порівняння s1 == s2 дає в результаті false.
Значення true одержимо лише у випадку , коли обидві посилки вказують на один і той же рядок,
наприклад, після присвоєння s1 = s2.
Логічний метод equals (object obj), перевизначений із класу Оbject, повертає true, якщо аргумент obj не
ріний null, являється обєктом класу String, і рядок, що міститься в ньому, повністю ідентичний даному
рядку аж до співпадання регістру літер. В решті випадків повертається значення false.
Логічний метод equalsIgnoreCase(object obj) працює так же, але однакові літери, записані в різних
регістрах, вважаються співпадаючими.
Наприклад, s2.equals("інший рядок") дасть в результаті false, а s2.equalsIgnoreCase("інший рядок")
поверне true.
90
91. Метод compareTo(string str) повертає ціле число типу int, обчислене за наступними правилами:
1. Порівнюються символи даного рядка this і рядка str з однаковими індексами, доки не зустрінуться
різні символи з індексом, допустимо k, або доки один з рядків не закінчиться.
2. В першому випадку повертається значення this.charAt(k) - str.charAt(k), тобто різниця кодів Unicode
перших неспівпадаючих символів.
3. В другому випадку повертається значення this.length() - str.length(), тобто різниця довжин рядків.
4. Якщо рядки співпадають, повертається 0.
Якщо значення str рівно null, виникає виключна ситуація. Нуль повертається в тій же ситуації, в якій метод
equals() повертає true.
Метод compareToІgnoreCase(string str) робить порівняння без врахування регістру літер, точніше кажучи,
виконується метод
this.toUpperCase().toLowerCase().compareTo(
str.toUpperCase().toLowerCase());
Ще один метод - compareTo(Object obj) створює виключну ситуацію, якщо obj не являється рядком. В
решті випадків він працює як метод compareTo(String str). Ці методи не враховують алфавітний порядок
символів в локальнму кодуванні.
Порівняти підрядок даного рядка this з підрядком тієї ж довжини len іншого рядка str можна логічним
методом
regionMatches(int indl, String str, int ind2, int len)
Тут ind1 — індекс початку підрядка даного рядка this, ind2 - індекс початку підрядка іншого рядка str.
Результат false отримаємо в наступних випадках:
• хоч би один із індексів ind1 або ind2 відємний;
• хоч би одно із ind1 + len або ind2 + len більше довжини відповідного рядка;
• хоч би одна пара символів не співпадає.
Цей метод розрізняє символи, записані в різних регістрах. Якщо треба порівняти підрядки без врахування
регістрів літер, то використовуйте логічний метод:
regionMatches(boolean flag, int indl, String str, int ind2, int len)
Якщо перший параметр flag рівний true, то регістр літер при порівнянні підрядків не враховується, якщо
false - враховується.
5.1.3.5. Як знайти символ в рядку
Пошук завжди ведеться з врахуванням регістру літер. Першу поява символу ch в даному рядку this можна
виявити методом indexOf(int ch), що повертає індекс цього символу в рядку або -1, якщо символу ch в
рядку this немає.
Наприклад, "Молоко". indexOf('о') видасть в результаті 1.
Другу і наступні появи символу ch в даному рядку this можна відслідкувати методом indexOf(int ch, int ind).
Цей метод починає пошук символу ch з індекса ind. Якщо ind < 0, то пошук іде з початку рядка, якщо ind
більше за довжину рядка, то символ не шукається, тобто повертається -1.
91
92. Наприклад, "молоко".indexof('о', indexof ('о') + 1) дасть в результаті 3.
Остання поява символу ch в даному рядку this відслідковується методом lastIndexОf (int ch). Він
проглядає рядок в зворотному порядку. Якщо символ ch не знайдено, повертається -1.
Наприклад, "Молоко".lastІndexОf('о') дасть в результаті 5.
Передостанню і попередню появу символу ch в даному рядку this можна відслідкувати методом
lastIndexОf(int ch, int ind), якийй проглядає рядок у зворотному порядку, починаючи з індексу ind. Якщо ind
більше за довжину рядка, то пошук іде від кінця рядка, якщо ind < 0, то повертається -1.
5.1.3.6. Як знайти підрядок
Пошук завжди ведеться з врахуванням регістру літер. Перше входження підрядка sub в даний рядок this
відшукується методом indexОf (String sub). Він повертає індекс першого символу першого входження
підрядка sub в рядок або -1, якщо підрядок sub не входить в рядок this. Наприклад, " Молоко".indexof ("ок")
дасть в результаті 3.
Якщо ви хочете почати пошук не з початку рядка, а з якогось індексу ind, використовуйте метод indexOf
(String sub, int ind). Якщо ind < 0, то пошук іде з початку рядка, якщо ж ind більше довжини рядка, то
символ не шукається, тобто повертається -1.
Останнє входження підрядка sub в даний рядок this можна відшукати методом lastІndexОf (string sub), що
повертає індекс першого символу останнього входження підрядка sub в рядок this або -1, якщо підрядок
sub не входить в рядок this.
Останнє вхоження підрядка sub не у весь рядок this, а тільки в його початок до індексу ind можна
відшукати методом lastIndexof(String stf, int ind). Якщо ind більше довжини рядка, то пошук іде від кінця
рядка, якщо ind < 0, то повертається -1.
Для того щоб перевірити, чи не починаеться даний рядок this з підрядка sub, використовуйте логічний
метод startsWith(string sub), який повертає true, якщо даний рядок this починається з підрядка sub, або
співпадає з ним, або підрядок sub пустий.
Можна перевірити і появу підрядка sub в даному рядку this, починаючи з деякого індексу ind логічним
методом startsWith(String sub),int ind). Якщо індекс ind відємний або більший за довжину рядка,
повертається false.
Для того щоб перевірити, чи не закінчується даний рядок this підрядком sub, використовуйте логічний
метод endsWitht(String sub). Врахуйтете, що він повертає true, якщо підрядок sub співпадає з усім рядком
або підрядок sub пустий.
Наприклад, if (fileName.endsWith(". Java")) відслідкує імена файлів з вихідними текстами Java.
Перечислені вище методи створюють виключну ситуацію, якщо
sub == null.
Якщо ви хочете здійснити пошук, який не враховує регістр літер, поміняйте перед цим регістр всіх
символів рядка.
5.1.3.7. Як змінити регістр літер
Метод toLowerCase () повертає новий рядок, в якому всі літери переведеі в нижній регістр, тобто зроблені
прописними.
92
93. Метод toUpperCase () повертає новий рядок, в якому всі літери переведеі у верхній регістр, тобто
зроблені заглавними. При цьому використовується локальна кодова таблиця по замовчуванню. Якщо
потрібна інша локаль, то застосовуються методи toLowerCase(Locale loc) і toUpperCase(Locale loc).
5.1.3.8. Як замінити окремий символ
Метод replace (int old, int new) повертає новий рядок, в якому всі входження символу old замінені
символом new. Якщо символа old в рядку немає, то повертається посилка на вихідний рядок. Наприклад,
післе виконання "Рука в руку сует хлеб", replace ('у', 'е') одержимо рядок "Река в реке сеет хлеб".
Регістр літер при заміні враховується.
5.1.3.9. Як видалити пробіли на початку і в кінці рядка
Метод trim() повертає новий рядок, в якому видалені початкові і кінцеві символи з кодами, що не
превищують 'u0020'.
5.1.3.10. Як перетворити дані іншого типу в рядок
В мові Java прийнято, що кожний клас відповідає за перетворення інших типів в тип цьоого класу і
повинен містить потрібні для цього методи. Клас String містить вісім статичних методів valueОf (type
elem) перетворення В рядок примітивних типів boolean, char, int, long, float, double, масиву char[], і просто
обєкту типу Оbject. Девятий метод valueОf(char[] ch, int offset, int len) перетворює в рядок підмасив масиву
ch, який починається з індексу offset і має len елементів.
Крім того, в кожному класі єсть метод toString (), перевизначений або просто унаслідуваний від класу
Object. Він перетворює обєкти класу в рядок. Фактично, метод valueOf() викликає метод toString ()
відповідного класу. Тому результат перетворення залежить від того, як реалізований метод toString ().
Ще один простий спосіб - зчепити значення elem якогось типу з пустим рядком: "" + elem. При цьому
неявно викликається метод elem. toString ().
5.2. Клас StringBuffer
Обєкти класу StringBuffer - це рядки змінної довжини. Тільки що створений обєкт має буфер ви значеної
ємності (capacity), по замовчуванню достатній для зберігання 16 символів. Ємність можна задати в
конструкторі обєкта. Як тілько буфер починає переповняться, його ємність автоматично збільшується,
щоб вмістити нові символи. Також ємність буферу можна збільшити, звернувшись до методу
ensureCapacity(int minCapacity). Цей метод змінить ємність тільки якщо minCapacity буде більшим за
довжину рядка, що зберігається в обєкті. Ємність буде збільшена по наступному правилу. Нехай ємність
буферу рівна N. Тоді нова ємність буде рівна
Мах(2 * N + 2, minCapacity)
Таким чином, ємність буферу не можна збільшити менше ніж вдвічі.
Методом setLength(int newLength) можна установити довільну довжину рядка. Якщо вона виявиться
більше поточної довжини, то додаткові символи будуть рівны ' u0000'. Якщо вона буде менше поточної
довжини, то рядок буде обрізаний, останні символи втратяться, точніше, будуть замінені символом
'u0000'. Ємність при цьому не зміниться. Якщо число newLength виявиться відємним, виникнк виключна
ситуація.
Порада
Будьте обережні, установлюючи нову довжину обєкту. Кількість символів у рядку можна узнать, як і для
93
94. обєкту класу String, методом length (), а ємність — методом capacity (). Створити обєкт класу StringBuffer
можна тільки конструкторами.
5.2.1. Конструктори
В класі StringBuffer три конструктори:
stringBuffer () - створює пустий обєкт з ємністю 16 символів;
stringBuffer.(int capacity) - створює пустий обєкт заданої ємності capacity;
StringBuffer (String str) - створює обєкт ємністю str.length() + 16, що містить рядок str.
5.2.2. Як додати підрядок
В класі StringBuffer єсть десять методів append (), що додають підрядок в кінець рядка. Вони не
створюють новий екземпляр рядка, а повертають посилання на той же самий, але змінений рядок.
Основний метод append (string str) приєднує рядок str в кінець даного рядка. Якщо посилка str == null, то
дододається рядок "null".
Шість методів append (type elem) додають примітивні типи boolean, char, int, long, float, double, перетворені
в рядок.
Два методи приєднують до рядка масив str і підмасив sub символів, перетворені в рядок: append (char []
str) і append (char [.] , sub, int offset, int len).
Десятий метод додає просто обєкт append (Object obj). Перед цим обєкт obj перетворюється в рядок
своїм методом toString ().
5.2.3. Як вставити підрядок
Десять методів insert () призначені для вставки рядка, вказаного параметром методу, в даний рядок.
Місце вставки задається першим параметром методу ind. Це індекс элементу рядка, перед яким буде
зроблена вставка. Він повинен бути невідємним і менше довжини рядка, інакше виникне виключна
ситуація. Рядок роздвигається, ємність буферу при необхідності збільшується. Методи повертають
посилку на той же перетворений рядок.
Основний метод insert (int ind, string str) вставляє рядок str в даний рядок перед його символом з індексом
іnd. Якщо посилка str == null вставляється рядок "null".
Наприклад, після виконання
String s = new StringBuffer("Це великий рядок"). insert(3, "не").toString();
одержимо s == "Це невеликий рядок".
Метод sb.insert(sb.length о, "xxx") буде працювати так же, як і метод
sb.append("xxx").
Шість методів insert (int ind, type elem) вставляють примітивні типи boolean, char, int, long, float, double,
перетворені в рядок. Два методи вставляють масив str і підмасив sub символів, перетворені в рядок:
94
95. insert(int ind, chart] str)
insert(int ind, char[] sub, int offset, int len)
Десятий метод вставляє просто обєкт:
insert(int ind, Object obj)
Обєкт obj перед додаванням перетворюється в рядок своїм методом toString().
5.2.4. Як видалити підрядок
Метод delete(int begin, int end) видаляє із рядка символи, починаючи з індексу begin включно до індексу
end виключно, якщо end більше за довжину рядка, то до кінця рядка.
Наприклад, після виконання
String s = new StringBuffer("Це невеликий рядок").
delete(3, 5).toString();
одержимо s == "Це великий рядок".
Якщо begin відємне, більше за довжину рядка або більше end, виникає виключна ситуація.
Якщо begin == end, видалення не відбувається.
5.2.5. Як видалити символ
Метод deІeteCharAt (int ind) видаляє символ з указаним індексом ind. Довжина рядка зменшуєтся на
одиницю. Якщо індекс ind відємний або більший за довжину рядка, виникає виключна ситуація.
5.2.6. Як замінити підрядок
Метод replace (int begin, int end, String str) видаляє символи із рядка, починаюси з індексу begin включно
до індексу end виключно, якщо end більше за довжину рядка, то до кінця рядка, і вставляє замість них
рядок str. Якщо begin відємне, більше за довжину рядка або більше end, виникає виключна ситуація.
5.2.7. Як перевернути рядок
Метод reverse() змінює порядок розташування символів у рядку на зворотний порядок.
Наприклад, після виконання
StringBuffer s = new StringBuffer("Це невеликий рядок"),
reverse().toString();
одержимо s == "кодяр йикилевен еЦ".
5.2.8. Синтаксичний розбір рядка
Задача розбору введеного тексту (parsing) — вічна задача програмування, наряду із сортуванням і
95
96. пошуком. Написана маса програм-парсерів (parser), що розбирають текст по різним ознакам. Але задача
залишається. І ось черговий програміст, зневірившись знайти що-небудь підходяще, береться за розробку
власної програми розбору.
В пакет java.util входить простий класс StringTokenizer, що полегшує розбір рядків. Класс StringTokenizer
невеликий, в ньому три конструктори і шість методів.
Перший конструктор StringTokenizer (String str) створює обєкт, готовий розбити рядок str на слова,
розділені так званими сепараторами - пробілами, символами табуляцій 't', переводу рядка 'n' і
повернення каретки 'r'. Сепаратори не включаються в число слів.
Другий конструктор StringTokenizer (String str. String delimeters) задає сепаратори другим параметром
deІimeters, наприклад:
StringTokenizer("Казнить,нельзя:пробелов-нет", " tnr,:-");
Тут перший сепаратор - пробіл. Потім ідуть символ табуляції, символ переводу рядка, символ повернення
каретки, кома, двокрапка, дефіс. Порядок розташування сепараторів у рядку deІimeters не має значення.
Сепаратори не включаються в число слів.
Третій конструктор дозволяє включити сепаратори в число слів:
StringTokenizer(String str, String deІimeters, boolean flag);
Якщо параметр flag рівний true, то сепаратори включаються в число слів, якщо false — ні. Наприклад:
StringTokenizer("а - (b + с) / b * с", " tnr+*-/(), true);
В разборі рядка на слова активно приймають участь два методи:
метод nextToken() повертає у вигляді рядка наступне слово;
логічний метод hasMoreTokens() повертає true, якщо в рядку ще єсть слова, і false, якщо слів більше
немає.
Третій метод countTokens() повертає число залишившихся слів.
Четвертий метод nextToken(string newDelimeters) дозволяє "на ходу" міняти сепаратори. Наступне слово
буде виділено по новим сепараторам newDeІimeters; нові сепаратори діють далі замість старих,
визначених в конструкторі або в попередньому методі nextToken().
Останні два методи nextEІement() і hasMoreEІements() реалізують інтерфейс Enumeration. Вони просто
звертаються до методів nextToken () і hasMoreTokens().
Схема дуже проста (лістинг 5.1).
Лістинг 5.1. Розбиття рядка на слова :
import java.util.*;
class Parse{
public static void main(String[] args){
String s = "String, which we want to decompose in words ";
StringTokenizer st = new StringTokenizer(s, " tnr,.");
while(st.hasMoreTokens()){
96
97. // Одержуємо слово і що-небудь робимо з ним, наприклад, просто виводимо на екран
System.out.println(st.nextToken()) ;
}
}
}
Одержані слова звичайно заносяться в який-небудь клас-коллекцію: Vector, Stack або інший, найбільш
зручний для подальшої обробки тексту контейнер. Класи-коллекції ми розглянемо в наступному уроці.
Передивіться файл StringTokenizer у папці java.util і визначте, чи всі методи цього класу були задіяні у
попередній програмі.
Заключення
Всі методи представлених в цьому уроці класів написані мовою Java. Їх вихідні тексти можна подивитися,
вони входять у склад JDK. Це дуже корисне заняття. Проглянувши вихідний текст, ви отримаєте повну
уяву про те, як працює метод. В останніх версіях JDK вихідні тексти зберігаються в упакованому
архіватором jar файлі src.jar, що знаходиться в кореневом каталозі JDK.
Лабораторна робота 4. Класи String, StringBuffer і StringTokenizer.
Над наведеними нижче завданнями ви повинні працювати дома, а на лабораторній роботі представите
налаштовані програми викладачу. При цьому програми 1 – 11 повинен знати кожний і на вибір викладача
запустити її на протязі 15 хвилин. Сподіваюся, що програми 12 – 15 виберуть і напишуть кращі студенти.
1. Напишіть програму, яка підраховує число слів у тексті. Розгляньте випадки, коли текст
визначається в самій програмі і вводиться з командного рядка при запуску програми на
виконання.
2. Напишіть програму, що записує текст у зворотному порядку. Розгляньте випадки, коли текст
визначається в самій програмі і вводиться з командного рядка при запуску програми на
виконання.
3. Напишіть програму, яка в рядку “This is a small string” замінить слово “small” на “very large”.
4. Напишіть програму, яка в рядку “This is a small string” видалить артикль “a” .
5. Напишіть програму, яка в рядку “This is a small string” видалить слово “small” .
6. Напишіть програму, яка в рядок “This is a small string” додасть слово “very” перед “small”.
97
98. 7. Напишіть програму, яка в кінець рядка “This is a small string” додасть рядок “, that we use to
ilustrate the methods of class StringBuffer” не створюючи нового рядка.
8. Напишіть програму, яка в рядку “This is a small string” замінить всі літери “i” на літеру “o”.
9. Напишіть програму, яка підраховує число символів у тексті. Розгляньте випадки, коли текст
визначається в самій програмі і вводиться з командного рядка при запуску програми на
виконання.
10. Напишіть програму, яка перевіряє, чи певне слово міститься в даному тексті. Розгляньте випадки,
коли текст визначається в самій програмі і вводиться з командного рядка при запуску програми на
виконання. Програма повинна розпізнавати слово незалежно від регістру, в якому воно записане.
11. Напишіть програму, яка перевіряє, чи певна літера міститься в даному тексті. Розгляньте
випадки, коли текст визначається в самій програмі і вводиться з командного рядка при запуску
програми на виконання. Програма повинна розпізнавати літеру незалежно від регістру, в якому
вона записана.
12. Напишіть програму, яка підрахує скільки разів певне слово входить в даний текст незалежно від
регістру, в якому воно написане.
13. Напишіть програму, яка підрахує скільки разів певна літера входить в даний текст незалежно від
регістру, в якому вона написана.
14. Напишіть програму, яка обчислює відносну частоту появи кожного символу в даному тексті,
включаючи знаки пунктуації і пробіли.
15. Напишіть програму, яка обчислює відносну частоту появи певного слова в даному тексті,
незалежно від регістру.
98
99. Програмування у Java
Урок 6. Класи-коллекції
• Клас Vector
• Як створити вектор
• Як додати елемент у вектор
• Як замінити елемент
• Як узнати розмір вектора
• Як звернутися до элементу вектора
• Як узнати, чи єсть елемент у векторі
• Як узнати індекс елементу
• Як видалити елементи
• Клас Stack
• Клас Hashtable
• Як створити таблицю
• Як заповнити таблицю
• Як одержати значення по ключу
• Як узнати наявність ключа або значення
• Як одержати всі елементи таблиці
• Як видалити елементи
• Клас Properties
• Інтерфейс Collection
• Інтерфейс List
• Інтерфейс Set
• Інтерфейс SortedSet
• Інтерфейс Map
• Інтерфейс Map.Entry
• Інтерфейс SortedMap
• Абстрактні класи-колекеції
• Інтерфейс Iterator
• Інтерфейс ListIterator
• Класи, що створюють списки
• Двонаправленний список
• Класи, що створюють відображення
• Упорядковані відображення
• Порівняння елементів колекцій
• Класи, що створюють множини
• Упорядковані множини
• Дії з колекціями
• Методи класу Collections
• Заключення
99
100. В лістинзі 5.1ми розібрали рядок на слова. Як їх зберегти для подальшої обробки? До сих пір ми
користувалися масивами. Вони зручні, якщо треба швидко обробити однотипні елементи, наприклад,
просумувати числа, знайти найбільше і найменше значення, посортувати елементи. Але вже для пошуку
потрібних даних у великому обємі інформації масиви незручні. Для цього краще використовувати бінарні
дерева пошуку. Крім того, масиви завжди мають постійну, наперед задану довжину, в масив незручно
додавати елементи. При видаленні елемента із масиву решту елементів треба перенумерувати. При
вирішенні задач, в яких кількість элементів заздалегідь невідома, елементи треба часто видаляти і
додавати, треба шукати інші способи зберігання. В мові Java з самих перших версій єсть клас Vector,
призначений для зберігання зміннного числа елементів самого загальногого типу Оbject.
6.1. Клас Vector
В класі Vector із пакету java.utiІ зберігаються елементи типу Оbject, а значить, довільного типу. Кількість
елементів може бути довільним і наперед не визначається. Елементи одержують індекси 0, 1, 2, .... До
кожного элемента вектора можна звернутися по індексу, як і до елемента массиву. Крім кількості
елементів, яку називають розміром (size) вектора, єсть ще розмір буферу — ємність (capacity) вектора.
Звичайно ємність співпадає з розміром вектора, але можна її збільшити методом ensureCapacity(int
minCapacity) або порівняти з розміром вектора методом trimToSize(). В Java 2 клас Vector перероблений,
щоб включити його в ієрархію класів-колекцій. Тому багато дій можна робити старими и новими
методами. Рекомендується використовувати нові методи, оскільки старі можуть бути виключені із
наступних версій Java.
6.1.1. Як створити вектор
В класі чотири конструктори:
• Vector () - створює пустий обєкт нульової довжини;
• Vector (int capacity) - створює пустий обєкт указаної ємності capacity;
• Vector (int capacity, int increment) - створює пустий обєкт указаної ємності capacity і задає число
increment, на яке збільшується ємність при необхідності;
• Vector (Collection с) - вектор створюється по указаній колекції. Якщо capacity відємне, створюється
виключна ситуація. Після створення вектору його можна заповняти елементами.
6.1.2. Як додати елемент у вектор
• Метод add (Object element) дозволяє додати елемент в кінець вектора (те ж саме робить старий
метод addElement (Object element).
• Методом add (int index, Object element) або старим методом insertElementAt (Object element, int
index) можна вставити елемент у вказане місце index. Елемент, що знаходиться на цьому місці, і
всі наступні элементи здвигаються, їх індекси збільшуються на одиницю.
• Метод addAІl (Collection coll) дозволяє додати в кінець вектора всі елементи коллекції coll.
• Методом addAІІ(int index, Collection coll) можна вставити в позицію index всі елементи коллекції
coll.
6.1.3. Як замінити елемент вектора
Метод set (int index, object element) заміняє елемент, що стоїть у векторі в позиції index, на елемент
element (те ж саме дозволяє виконувати старий метод setElementAt (Object element, int index))
6.1.4. Як узнати розмір вектора
Кількість елементів у векторі завжди можна взнати методом size(). Метод capacity() повертає ємність
вектора. Логічний метод isEmpty() повертає true, якщо у векторі немає жодного элемента.
6.1.5. Як звернутися до елемента вектора
Звернутися до першого элементу вектора можна методом firstEiement(), до останнього - методом
lastEІement(), до довільного элементу - методом get (int index) або старим методом elementAt (int index). Ці
100
101. методи повертають обєкт класу Оbject. Перед використанням його треба привести до потрібного типу.
Одержати всі елементи вектора у вигляді масиву типу object[] можна методами toArray() і toArray (Object []
а). Другий метод заносить всі елементи вектора в масив а, якщо в ньому достатньо місця.
6.1.6. Як узнати, чи єсть елемент у векторі
• Логічний метод contains (object element) повертає true, якщо елемент element знаходиться у
векторі.
• Логічний метод containsAІІ (Collection с) повертає true, якщо вектор містить всі элементи указаної
колекції.
6.1.7. Як узнати індекс елемента
Чотири методи дозволяють знайти позицію указаного елемента element:
• indexof (Object element) - повертає індекс першої появи элемента у векторі;
• indexOf (Object element, int begin) - веде пошук, починаючи з індекса begin включно;
• lastІndexOf (object element) - повертає індекс останньої появи елемента у векторі;
• lastІndexOf (Object element, int start) - веде пошук від індекса start включно до початку вектора.
Якщо елемент не знайдено, повертається -1.
6.1.8. Як видалити елементи вектора
• Логічний метод remove (Object element) видаляє із вектора перше входження указаного елемента
element. Метод повертає true, якщо елемент знайдено і видалення виконано.
• Метод remove (int index) видаляє елемент із позиції index і повертає його в якості свого результату
типу Оbject. Аналогічні дії дозволяють ввиконати старі методи типу void: removeElement (Object
element) і removeElementAt (int index), що не повертають результату.
• Видалити діапазон елментів можна методом removeRange(int begin, int end). Видаляються
елементи від позиції begin включно до позиції end виключно.
• Видалити із даного вектора всі елементи колекції coil можна логічним методом
removeAll(Collection coll). Видалити останні елементи можна, просто урізавши вектор методом
setSizefint newSize).
• Видалити всі елементи, крім тих, що входять у вказану колекцію coil, дозволяє логічний метод
retainAll(Collection coll). Видалити всі елементи вектора можна методом clear() або старим
методом removeAІІElements() або обнуливши розмір вектора методом setSize(0).
Лістинг 6.1 розширює лістинг 5.1, обробляючи виділені із рядка слова за допомогою вектора.
Лістинг 6.1. Робота з вектором
import java.util.*;
class Parse{
public static void main(String[] args){
Vector v = new Vector();
String s = "String, that we want to decompose in words.";
StringTokenizer st = new StringTokenizer(s, " tnr,.");
while (st.hasMoreTokens()){
// Одержуємо слово і заносимо у вектор
v.add(st.nextToken()); // Додаємо в кінець вектора
}
System.out.println(v.firstElement()); // Перший елемент
System.out.println(v.lastElement()); // Останній елемент
v.setSize(4); // Зменшуємо число елементів
v.add("compose"); // Додаємо в кінець вкороченого вектора
v.set(3, "again"); // Ставимо в позицію 3
101
102. for (int i = 0; i < v.size(); i++)// Перебираємо весь вектор
System.out.print(v.get(i) + " ");
System.out.println();
}
}
Клас Vector являється прикладом того, як можна обєкти класу Оbject, a значить, будь-які обєкти,
обєдувати в колекцію. Цей тип колекції упорядковує і навіть нумерує елементи. У векторі єсть перший
елемент, єсть останній елемент. До кожного элементу звертаються безпосередньо по індексу. При
додаванні і увидаленні елементів решта елементівы автоматично перенумеровуються.
Передивіться файл Vector у папці java.util і визначте, чи всі методи цього класу були задіяні у попередній
програмі.
Другий приклад колекції — клас Stack — розширяє клас Vector.
6.2. Клас Stack
Клас Stack із пакету java.utiІ обєднує елементи в стек. Стек (stack) реалізує порядок роботи з
елементами подібно магазину гвинтівки— першим вистрелить патрон, покладений в магазин останнім,—
або подібно залізничному тупику — першим із тупика вийде вагон, загнаний туди останнім. Такий порядок
обробки називається LIFO (Last In — First Out).
Перед работою створюється пустий стек конструктором Stack (). Потім на стек кладутся і знімаються
елементи, причому доступний тільки "верхній" елемент, той, що покладений на стек останнім. Додатково
до методів класу Vector клас Stack містить пять методів, що дозволяють працювати з колекцією як зі
стеком:
• push (Object item) —поміщає елемент item в стек;
• pop () - дістає верхній элемент зі стека;
• peek () - читає верхній елемент, не дістаючи його зі стека;
• empty () - перевіряє, чи не пустий стек;
• search (object item) - знаходить позицію элемента item в стеку. Верхній елемент має позицію 1, під
ним елемент 2 і т. д. Якщо елемент не знайдено, повертається - 1.
Лістинг 6.2 показує, як можна використовувати стек для перевірки парності символів.
Лістинг 6.2. Перевірка парності дужок
import java.util.*;
class StackTest{
static boolean checkParity(String expression,
String open, String close){
Stack stack = new Stack ();
StringTokenizer st = new StringTokenizer(expression, " tnr+*/-(){}", true);
while (st.hasMoreTokens ()) {
102
103. String tmp = st.nextToken();
if (tmp.equals(open)) stack.push(open);
if (tmp.equals(close)) stack.pop();
}
if (stack.isEmpty () ) return true;
else
return false;
}
public static void main(String[] args){
System.out.println(
checkParity("a - (b - (c - a) / (b + c) - 2" , "(", ")"));
}
}
Передивіться файл Stack у папці java.util і визначте, чи всі методи цього класу були задіяні у попередній
програмі.
Як бачимо, колекції значно полегшують обробку наборів даних. Ще один приклад колекції зовсім іншого
роду - таблиці — представляє клас Hashtable.
6.3. Класс Hashtable
Класс Hashtable розширяє абстрактний клас Dictionary. В обєктах цього класу зберігаються пари "ключ -
значення". Із таких пар "Прізвище І. Б. — номер" складається, наприклад, телефонний довідник. Ще один
приклад - анкета. Її можна представить як сукупність пар "Прізвище - Іванов", "Імя — Петро", "По батькові
- Сидорович", "Рік нарождення — 1975" і т. д. Подібних прикладів можна привести багато. Кожний обєкт
класу Hashtable крім розміру (size) - кількості пар, має ще дві характеристики: ємність (capacity) - розмір
буферу, і показник завантаженості (load factor) — процент заповнення буферу, по досягненні якого
збільшується його розмір.
6.3.1. Як створити таблицю
Для створення обєктів клас Hashtable має чотири конструктори:
• Hashtable () - створює пустий обєкт з початковою ємністю в 101 елемент і показником
завантаженості 0,75;
• Hashtable (int capacity) - створює пустий обєкт з початковою ємністю capacity і показником
завантаженості 0,75;
• Hashtable(int capacity, float loadFactor) - створює пустий обєкт з початковою ємністю capacity і
показником завантаженості loadFactor;
• Hashtable (Map f) - створює обєкт класу Hashtable, що містить всі елементи відображення f, з
ємністю, рівною подвоєному числу элементів відображеня f, але не менше 11, і показником
завантаженості 0,75.
6.3.2. Як заповнити таблицю
Для заповнення обєкту класу Hashtable використовуются два методи:
• Object put(Object key, Object value) — додає пару "key— value", якщо ключа key не було в таблиці, і
змінює значення value ключа key, якщо він уже єсть в таблиці. Повертає старе значення ключа
103
104. або null, якщо його не було. Якщо хоч би один параметр рівний null, виникає виключна ситуація;
• void putAІІ(Map f) — додає всі элементи відображення f. В обєктах-ключах key повинні бути
реалізовані методи hashCode() і equals ().
6.3.3. Як одержати значення по ключу
Метод get (Object key) повертає значення елемента з ключем key у вигляді обєкта класу object. Для
подальшої роботи його треба перетворити в конкретний тип.
6.3.4. Як узнати наявність ключа або значення
• Логічний метод containsKey(object key) повертає true, якщо в таблиці єсть ключ key.
• Логічний метод containsValue (Object value) або старий метод contains (Оbject value) повертає true,
якщо в таблиці єсть ключі із значенням value.
• Логічний метод isEmpty() повертає true, якщо в таблиці немає элементів.
6.3.5. Як одержати всі елементи таблиці
• Метод values() представляє всі значення value таблиці у вигляді інтерфейсу Collection. Всі
модифікації в обєкті Сollection змінюють таблицю, і навпаки.
• Метод keyset() представляє всі ключі key таблиці у вигляді інтерфейсу Set. Всі зміни в обєкті Set
коректують таблицю, і навпаки.
• Метод entrySet() представляє всі пари "key— value" таблиці у вигляді інтерфейсу Set. Всі зміни в
обєкті Set коректують таблицю, і навпаки.
• Метод toString() повертає рядок, що містить всі пари.
• Старі методи elements() і keys() повертають значення і ключі у вигляді інтерфейсу Enumeration.
6.3.6. Як видалити елементи
• Метод remove (Object key) видаляє пару с ключем key, повертаючи значення цього ключа, якщо
воно єсть, і null, якщо пара з ключем key не знайдена.
• Метод clear() видаляє всі елементи, очищаючи таблицю.
В лістинзі 6.3 показано, як можна використати клас HashtabІe для створення телефонного довідника, а на
рис. 6.1 — виведення цієї програми.
Лістинг 6.3. Телефонний довідник
import java.util.*;
class PhoneBook{
public static void main(String[] args){
Hashtable yp = new Hashtable();
String name = null;
yp.put("John", "123-45-67");
yp.put ("Lemon", "567-34-12");
yp.put("Bill", "342-65-87");
yp.put("Gates", "423-83-49");
yp.put("Batman", "532-25-08");
try{
name = args[0];
}
catch(Exception e){
System.out.println("Usage: Java PhoneBook Name");
return;
}
if (yp.containsKey(name))
System.out.println(name + "'s phone = " + yp.get(name));
else
System.out.println("Sorry, no such name");
104
105. }
}
Передивіться файл Hashtable у папці java.util і визначте, чи всі методи цього класу були задіяні у
попередній програмі.
Рис. 6.1. Робота з телефонною книгою
6.4. Класс Properties
Клас Properties розширяє клас HashtabІe. Він призначений в основному для введення і виведення пар
властивостей системи і їх значень. Пари зберігаються у вигляді рядків типу String. В класі Properties два
конструктори:
• Properties () - створює пустий обєкт;
• Properties (Properties default) - створює обєкт із заданими парами властивостей default.
Крім унаслідуваних від класу HashtabІe методів в класі Properties єсть ще наступні методи. Два методи,
що повертають значення ключа-рядка у вигляді рядка:
• String getProperty (string key) - повертає значення по ключу key;
• String getProperty(String.key, String defaultValue) - повертає значення по ключу key; якщо такого
ключа немає, повертається defaultValue.
• Метод setProperty(String key, String value) додає нову пару, якщо ключа key немає, і змінює
значення, якщо ключ key єсть.
• Метод load(InputStream in) завантажує властивості із вхідного потоку in.
• Методи list(PrintStream out) і list (PrintWriter out) виводять властивості у вихідний потік out.
• Метод store (OutputStream out, String header) виводить властивості у вихідний потік out із
заголовком header.
Дуже простий лістинг 6.4 і рис. 6.2 демонструють виведення всіх системних властивостей Java.
Лістинг 6.4. Виведення системних властивостей
class Prop{
public static void main(String[] args){
System.getProperties().list(System.out);
}
}
105
106. Передивіться файл Properties у папці java.util і визначте, як ще можна використати цей клас.
Рис. 6.2. Системні властивості
Приклади класів Vector, Stack, HashtabІe, Properties показують зручності класів-колекцій. Тому в Java 2
розроблена ціла ієрархія колекцій. Вона показана на рис. 6.3. Курсивом записані імена інтерфейсів.
Пунктирні лінії вказують класи, що реалізують ці інтерфейси. Всі колекції розбиті на три групи, описані в
інтерфейсах List, Set і Map.
Рис.
6.3.
Ієрар
хія
класі
в і
інтер
фейс
ів-
коле
кцій
Прик
ладо
м
реалі
зації
інтер
фейс
у List може служить клас
Vector, прикладом реалізації
інтерфейсу Мар — класс
HashtabІe. Колекції List і Set
мають багато спільного, тому
їх спільні методи обєднані і
106
107. винесені в суперінтерфейс Collection. Подивимось, що, на думку розробників Java API, повинно міститися
в цих колекціях.
6.5. Інтерфейс Collection
Інтерфейс Сollection із пакету java.util описує загальні властивості колекцій List і Set. Він містить методи
додавання і видалення элементів, перевірки и перетворення елементів:
• boolean add(Object obj) - додає елемент obj в кінець колекції; повертає false, якщо такий елемент в
колекції уже єсть, а колекція не допускає повторних элементів; повертає true, якщо додавання
пройшло успішно;
• boolean addAІІ(Collection coll) - додає всі елементи колекції coll в кінець даної колекції;
• void clear() - видаляє всі элементи колекції;
• boolean contains(Object obj) - перевіряє наявність элементу obj в колекції;
• boolean containsAll(Collection coll) - перевіряє наявність всіх элементів колекції coll в даній колекції;
• boolean isEmpty() - перевіряє, чи колекція пуста ;
• iterator iterator() - повертає ітератор даної колекції;
• boolean remove(object obj) — видаляє указаний елемент із колекції; повертає false, якщо елемент
не знайдено, true, якщо видалення пройшло успішно;
• boolean removeAІІ (Collection coll) — видаляє елементи указаної колекції, що належать даній
колекції;
• boolean retainAІІ(Collection coll) - видаляє всі елементи даної колекції, крім елементів колекції coll;
• int size() - повертає кількість елементів в колекції;
• Оbject [] toArray () - повертає всі елементи колекції у вигляді масиву;
• Object[] toArray<object[] a - записує всі елементи колекції в массив а, якщо в ньому достатньо
місця.
Передивіться файл java.utiІInterfacesCollection.
6.6. Інтерфейс List
Інтерфейс List із пакету java.utiІ, що розширює інтерфейс Collection, описує методи роботи з
упорядкованими колекціями. Інколи їх називають послідовностями (sequence). Елементи такої колекції
перенумеровані, починаючи з нуля, до них можна звернутися по індексу. На відміну від колекції Set
елементи колекції List можуть повторятися. Клас Vector — одна із реалізацій інтерфейсу List. Інтерфейс
List додає до методів інтерфейсу Collection методи, що використовують індекс index елемента:
• void add(int index, object obj) - вставляє елемент obj в позицію index; старі елементи, починаючи з
позиції index, зсуваються, їх індекси збільшуються на одиницю;
• boolean addAll(int index, Collection coll) - вставляє всі елементи колекції coІl;
• object get(int index) - повертає елемент, що знаходиться в позиції index;
• int indexOf(Object obj) - повертає індекс першої появи елемента obj в коллекції;
• int lastІndexOf (object obj) - повертає індекс останньої появи елемента obj в колекції;
• ListІterator listiterator () - повертає ітератор колекції;
• ListІterator listiterator (int index) — повертає ітератор кінця колекції від позиції index;
• object set (int index, object obj) — заміняє елемент, що знаходиться в позиції index, елементом obj;
• List subList(int from, int to) — повертає частину коллекції від позиції from включно до позиції to
виключно.
Передивіться файл java.utiІInterfacesList.
6.7. Інтерфейс Set
Інтерфейс Set із пакету java.utiІ розширює інтерфейс Collection, описуючи неупорядковану колекцію, яка
не містить повторюваних елементіов. Це відповідає математичному поняттю множини (set). Такі колекції
зручні для перевірки наявності або відсутності у елемента властивості, що визначає множину. Нові
методи в інтерфейс Set не додані, просто метод add () не стане додавати ще одну копію елемента, якщо
такий елемент уже єсть в множині. Цей інтерфейс расширюється інтерфейсом SortedSet.
107
108. Передивіться файл java.utiІInterfacesSet.
6.8. Інтерфейс SortedSet
Інтерфейс SortedSet із пакету java.utiІ, розширює інтерфейс Set і описує упорядковану множину,
відсортовану по природному порядку зростання його елементів або по порядку, заданому реалізацією
інтерфейсу comparator. Елементи не нумеруються, але єсть поняття першого, останнього, більшого і
меншого элемента. Додаткові методи інтерфейсу відображають ці поняття:
• comparator comparator() - повертає спосіб упорядкування колекції; Object first () - повертає перший,
менший елемент колекції;
• SortedSet headset(Object toElement) — повертає початкові, менші елементи до елементу toEІement
виключно;
• object last() - повертає останній, більший елемент колекції;
• SortedSet subset(Object fromElement, Object toEІement) - повертає підмножину колекції від
елемента fromElement включно до елемента toEІement виключно;
• SortedSet tailSet(Object fromElement) - повертає останні, більші елементи колекції від елемента
fromElement включно.
Передивіться файл java.utiІInterfacesSortedSet.
6.9. Інтерфейс Map
Інтерфейс Map із пакету java.utiІ описує колекцію, що складається із пар "ключ — значення". У кожного
ключа тільки одне значення, що відповідає математичному поняттю однозначної функції або
відображення (mар). Таку колекцію часто називають ще словником (dictionary) або асоціативним масивом
(associative array). Звичайний масив — найпростіший приклад словника із заздалегідь заданим числом
елементів. Це відображення множини перших невідємних цілих чисел на множину элементів масиву,
множина пар "індекс масиву - елемент масиву". Клас HashTable - одна із реалізацій інтерфейсу Мар.
Інтерфейс Map містить методи, що працюють з ключами і значеннями:
• boolean containsKey(Object key) - перевіряє наявність ключа key;
• boolean containsValue(Object value) - перевіряє наявність значення value;
• Set entryset() — представляє колекцію у вигляді множини, кожний елемент якого — пара із даного
відображення, з якою можна працювати методами вкладеного інтерфейсу Map. Entry;
• Оbject get(Object key) - повертає значення, відповідне ключу key;
• Set keyset() - представляє ключі колекції у вигляді множини;
• Object put(Object key, Object value) — додає пару "key— value", якщо такої пари не було, і заміняє
значення ключа key, якщо такий ключ уже єсть в колекції;
• void putAІІ(Map m) - додає до колекції всі пари із відображення m;
• Соllection values() - представляє всі значення у вигляді колекції.
В інтерфейс Мар вкладений інтерфейс Map.Entry, який містить методи роботи з окремою парою.
Передивіться файл java.utiІInterfacesMap.
6.10. Вкладений інтерфейс Map.Entry
Цей інтерфейс описує методи роботи з парами, отриманими методом entrySet():
• методи getKey() і getVaІue() дозволяють отримати ключ і значення пари;
• метод setVaІue(Оbject value) змінює значення в даній парі.
Передивіться файл java.utiІInterfacesMap.Entry
6.11. Інтерфейс SortedMap
108
109. Інтерфейс SortedMap, розширює інтерфейс Map і описує впорядковану по ключах колекцію Мар.
Сортування відбувається або в природному порядку зростання ключів, або, в порядку, описаному в
інтерфейсі Comparator. Елементи не нумеруються, але єсть поняття більшого і меншого із двох
елементов, першого, самого маленького, і останнього, самого більшого елемента коллекції. Ці поняття
описуються наступними методами:
• Сomparator comparator () - повертає спосіб упорядочення коллекції;
• Оbject firstKey() - повертає перший, найменший елемент колекції;
• SortedMap headMap(Object toKey) - повертає початок колекції до елемента з ключем toKey
виключно;
• Оbject lastKey() - повертає останній, найбільший ключ колекції;
• SоrtedMap subMap (Object fromKey, Object toKey) - повертає частину колекції від елемента з
ключем fromKey включно до елемента з ключем toKey виключно;
• SortedMap taІІMap(Оbject fromKey) - повертаєт остачу колекції від элементу fromKey включно.
Ви можете створювати свої коллекції, реалізувавши розглянуті інтерфейсы. Це справа важка, оскільки в
інтерфейсах багато методів. Щоб полегшити цю задачу, в Java API введені частинні реалізації
інтерфейсов — абстрактні класи-колекції.
Передивіться файл java.utiІInterfacesSortedMap.
6.12. Абстрактні класи-колекції
Ці класи лежать в пакеті java.util.
• Абстрактний клас AbstractGollection реалізує інтерфейс Collection, але залишає нереалізованими
методи iterator(), size().
• Абстрактний клас AbstractList реалізует інтерфейс List, але залишає нереалізованным метод get()
і наслідуваний метод size(). Цей клас дозволяє реалізувати колекцію з прямим доступом до
элементів, подібно масиву
• Абстрактний клас AbstractSequеntіаІList реалізує інтерфейс List, але залишає нереалізованим
метод listІterator(іnt index) і наслідуваний метод size(). Даний клас дозволяє реалізувати колекцію з
послідовним доступом до елементів за допомогою ітератора ListІterator.
• Абстрактний клас AbstractSet реалізує інтерфейс Set, але залишає нереалізованими методи,
наслідувані від AbstractCollection.
• Абстрактний клас AbstractMap реалізує інтерфейс Map, але залишає нереалізованим метод
entrySet().
Нарешті, в складі Java API єсть повністю реалізовані класи-колекції помимо уже розглянутих класів
Vectоr, Stack, Hashtable і Properties. Це класи ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap,
WeakHashMap. Для роботи з цими класами розроблені інтерфейси Іterator, ListІterator, Comparator і класи
Arrays та Collections. Перед тим як розглянути використання даних класів, обговоримо поняття ітератора.
6.13. Інтерфейс Iterator
В 70—80-х роках минулого століття, після того як була усвідомлена важливість правильної організації
даних у певну структуру, велика увага приділялась вивченню і побудові різних структур даних: звязаних
списків, черг, деків, стеків, дерев, мереж. Разом з розвитком структур даних розвивались і алгоритми
роботи з ними: сортування, пошук, обхід, хешування. Цим питанням присвячена обширна література. В
90-х роках було вирішено заносити дані в певну колекцію, заховавши її внутрішню структуру, а для роботи
з даними використовувати методи цієї колекції. Зокрема, завдання обходу поклали на саму колекцію. В
Java API введений інтерфейс Іterator, який описує спосіб обходу всіх елементів колекції. В кожній колекції
єсть метод Іterator(), який повертає реалізацію інтерфейса Іterator для вказаної колекції. Отримавши цю
реалізацію, можна обходити колекцію в деякому порядку, визначеним даним ітератором, за допомогою
методів, описаних в інтерфейсі Іterator і реалізованих в цьому ітераторі. Подібна техніка використана в
класі StringTokenizer. В інтерфейсі Іterator описані всього три методи:
• логічний метод hasNext() повертає true, якщо обхід ще не завершено;
• метод next() робить поточним наступний елемент коллкції і повертає його у вигляді обєкта класу
109
110. Оbject;
• метод remove() видаляє поточний елемент колекції.
Можна представити собі справу так, що ітератор — це покажчик на елемент колекції. При створенні
ітератора покажчик установливаетюється перед першим елементом, метод next() переміщує показчик на
перший елемент і показує його. Наступне застосування методу next() переміщає показчик на другий
елемент колекції і показує його. Останнє застосування методу next() виводить показчик за останній
елемент колекції. Метод remove(), здається, лишній, він уже не відноситься до завдання обходу колекції,
але дозволяє при перегляді коллекцї видалити із нього непотрібні елементи.
В лістинзі 6.5 до тексту лістинга 6.1 додана робота з ітератором.
Лістинг 6.5. Використання ітератора вектора
import java.util.*;
class Vect {
public static void main(String[] args){
Vector v = new Vector();
String s = "String, that we want to decompose in words.";
StringTokenizer st = new StringTokenizer(s, " tnr,.");
while (st.hasMoreTokens()){
// Одержуємо слово і заносимо у вектор.
v.add(st.nextToken()); // Додаємо в кінець вектора }
System.out.println(v.firstElement());// Перший елемент
System.out.println(v.lastElement()); // Останній елемент
v.setSize(4); // Зменшуємо число елементів
v.add("compose."); // Додаємо в кінці скороченого вектора
v.set(3, "again"); // Ставимо в позицію 3
for (int i = 0; i < v.size(); i++) // Перебираємо увесь вектор
System.out.print(v.get(i) + ".");
System.out.println();
Iterator it = v.iterator (); // Одержуємо ітератор вектора
try{
while(it.hasNext())// Доки у векторі єсть елементи,
System.out.println(it.next());// виводимо поточний элемент
}catch(Exception e){}
}
}
}
Запустіть програму на виконання і ретельно проаналізуйте результат.
6.14. Інтерфейс Listlterator
Інтерфейс ListІterator розширює інтерфейс Іterator, забезпечуючи переміщення по колекції як в прямому,
так і в зворотному напрямі. Він може бути реалізований лише в тих колекціях, в яких єсть поняття
наступного і попереднього елемента і де елементи пронумеровані. В інтерфейс ListІterator додані наступні
методи:
• void add(Object element) - додає eлемент element перед поточним елементом;
• boolean hasPrevious() - повертає true, якщо в колекції єсть елементи, що стоять перед поточним
елементом;
• int nextІndex() - повертає індекс поточного елемента; якщо поточним являється останій елемент
колекції, повертає розмір колекції;
• Object previous() - повертає попередній елемент і робить його поточним;
• int previousІndex() - повертає індекс попереднього елемента;
• void set(Object element) - замінює поточний елемент елементом element - виконується зразу після
next() або previous().
110
111. Як бачимо, ітератори можуть змінювати колекцію, в якій вони працюють, додаючи, видаляючи і заміняючи
елементи. Щоб це не приводило до конфліктів, передбачена виключная ситуація, яка виникає при
намаганні використовувати ітератори паралельно "рідним" методам колекції. Якраз тому в лістинзі 6.5 дії
з ітератором заключені в блок try{}catch(){}.
Змінимо закінчення лістингу 6.5 з використанням ітератора ListІterator.
// Текст лістинга 6.1...
// ...
ListIterator lit = v.listIterator(); // Одржуємо ітератор вектора
// Показчик зараз знаходиться перед початком вектора
try{
while(lit.hasNext()) // Доки у вектора єсть елементи
System.out.println(lit.next());//Переходимо до наступного элемента і виводимо його
// Тепер показчик за кінцем вектора. Перейдемо в початок
while (lit. hasPrevious ())
System, out. println(lit. previous()); :
}catch (Exception e) {}
Цікаво, що повторне використання методів next() і previous() один за другим буде видавати один і той же
поточний елемент. Подивимось тепер, які можливості представляють класи-коллекції Java 2.
6.15. Класи, що створюють списки
Клас ArrayList повністю реалізує інтерфейс List і ітератор типу iterator. Клас ArrayList дуже схожий на клас
Vector, має той же набір методів і може використовуватися в тих же ситуаціях. В класі ArrayList три
конструктори:
• ArrayList () - створює пустий объект;
• ArrayList (Collection coll) - створює обєкт, який містить всі елементи колекції coll;
• ArrayList (int inіtCapacity) - створює пустий обєкт ємності initCapacity.
Єдина відмінність класу ArrayList від класу Vector заключається в тому, що класс ArrayList не
синхронізований. Це означає, що одночаснa зміна экземпляру цього класу декількома підпроцесами
приведе до непередбачуваних результатів. Ці питання розглянемо в уроці 17.
6.16. Двонапрвлений список
Клас LinkedList повністю реалізує інтерфейс List і містить додаткові методи, що перетворюють його в
двонаправлений список. Він реалізує ітератори типу iterator i bistiterator. Цей клас можна використовувати
для обpобки елементів в стеку, деку або двонаправленому списку. В класі LinkedList два конструктори:.
• LinkedList - створює пустий обєкт
• LinkedList (Collection coll) — створює обєкт, що містить всі елементи колекції coll.
6.17. Класи, що створюють відображення
Клас HashMap повністю реалізує інтерфейс Map, а також ітератор типу iterator. Клас HashMap дуже
схожий на клас HashtabІe і може використовуватися в тих же ситуаціях. Він має той же набір функцій і такі
ж конструктори:
• HashMap() - створює пустий обєкт з показником завантаженості 0,75;
• НаshМар(int capacity) - створює пустий обєкт з початковою ємністю capacity і показником
завнтаженості 0,75;
• HashMap(int capacity, float loadFactor) - створює пустий обєкт з початковою ємністю capacity і
показником завaнтаженості loadFactor;
• HashMap(Map f) - створює обєкт класу HashMap, що містить всі елементи відображення f, з
ємністю, рівною подвоєному числу елементів відображення f, але не менше 11, і показником
завантаженості 0,75.
111
112. • Клас WeakHashMap відрізняється від класу HashMap тільки тим, що в його обєктах не
використовувані елементи, на які ніхто не посилається, автоматично виключаються із обєкта.
6.18. Упорядковані відображення
Клас ТгееМар повністю реалізує інтерфейс sortedMap. Він реалізований як бінарне дерево пошуку,
значить його елементи зберігаються в упорядкованому вигляді. Це значно прискорює пошук потрібного
елемента. Порядок задається або природним слідуванням елементів, або обєктом, реалізуючим
інтерфейс няння Comparator. В цьому класі чотири конструктори:
• ТгееМар () — створює пустий обєкт з природним порядком елементів;
• TreeМар (Comparator с) — створює пустий обєкт, в якому порядком елементів задається обєктом
порівняння с;
• ТгееМар (Map f) — створює обєкт, що містить всі елементи відображення f, з природним порядком
його елементів;
• ТгееМар (SortedMap sf) — створює обєкт, що містить всі елементи відображення sf в тому ж
порядку.
Тут треба пояснити, яким способом можна задати упорядкованість елементів колекції
6.19. Порвіняння елементів колекцій
Інтерфейс Comparator описує два методи порівняння:
• int compare(Object obj1, Оbject obj2) - повертає відємне число, якщо obj1 в певному змісті менше
obj2; нуль, якщо вони вважаються рівними; додатне число, якщо obj1 більше obj2. Для студентів,
знайомих з теорією множин, скажемо, що цей метод порівняння має властивості рефлексивності,
антисиметричності і транзитивності;
• boolean equals(Object obj) - порівнює даний обєкт з обєктом obj, повертає true, якщо обєкти
співпадають в певному змісті, заданому цим методом.
Для кожної колекції можна реалізувати ці два методи, задавши конкретний спосіб порівняння елементів, і
визначити обєкт класу SortedMap другим конструктором. Елементи колекції будуть автоматично
відсортовані в заданому порядку. Лістинг 6.6 показує один из можливих способів упорядкованості
комплексних чисел — обєктів класу Сomplex із лістингу 2.4. Тут описується клас ComplexCompare,
реалізуючий інтерфейс Comparator. В лістинзі 6.7 він застосовується для упорядкованого зберігання
множини комплексних чисел.
Лістинг 6.6. Порівняння комплексних чисел
import java.util.*;
class ComplexCompare implements Comparator{
public int compare(Object objl, Object obj2){
Complex z1 = (Complex)objl, z2 = (Complex)obj2;
double re1 = z1.getRe(), iml = zl.getlm();
double re2 = z2.getRe(), im2 = z2.getlm();
if (rel != re2) return (int)(re1 - re2);
else if (im1 != im2) return (int)(im1 — im2);
else return 0;
}
public boolean equals(Object z) {
return compare(this, z) == 0;
}
}
6.20. Класи, що створюють множини
Класс HashSet повністю реалізує інтерфейс set і ітератор типу iterator. Класс HashSet використовується в
тих випадках, коли треба зберігати тільки одну копію кожного елемента. В класі HashSet чотири
112
113. конструктори:
• HashSet () - створює пустий обєкт з показником завантаженості 0,75;
• HashSet (int capacity) - створює пустий обєкт з початковою ємністю capacity і показником
завнтаженості 0,75;
• HashSet (int capacity, float loadFactor) — створює пустий обєкт з початковою ємністю capacity і
показником завaнтаженості loadFactor;
• HashSet (Collection coll) — створює обєкт класу HashMap, що містить всі елементи відображення
coll, з ємністю, рівною подвоєному числу елементів відображення f, але не менше 11, і показником
завантаженості 0,75.
6.21. Упорядковані множини
Класс TreeSet повністю реалізує інтерфейс sortedSet і ітератор типу iterator. Клас TreeSet реалізований як
бінарне дерево пошуку, значить, його елементи зберігаються в упорядкованому вигляді. Це значно
прискорює пошук потрібного елемента. Порядок задається або природним слідуванням елементів, або
обєктом, реалізуючим інтерфейс порівняння Comparator. Цей клас зручний при пошуку елемента в
множині, наприклад, для перевірки, чи володіє якийсь елемент властивістю, що визначає множину. В
класі TreeSet чотири конструктори:
• TreeSet() - створює пустий обєкт з при родним порядком елементів;
• TreeSet (Comparator с) - створює пустий обєкт, в якому порядок задається обєктом порівняння с;
• TreeSet (Collection coll) - створює обєкт, що містить всі елементи колекції coll, з природним
порядком її елементів;
• TreeSet (SortedMap sf) - створює обєкт, що містить всі елементи відображення sf, в тому ж
порядку.
В лістинзі 6.7 показано, як можна зберігати комплексні числа в упорядкованому вигляді. Порядок
задається обєктом класу ComplexCompare, визначеного в лістинзі 6.6.
Лістинг 6.7. Зберігання комплексних чисел в упорядкованому вигляді
TreeSet ts = new TreeSet (new ComplexCompare());
ts.add(new Complex(1.2, 3.4));
ts. add (new Complex (-1.25, 33.4»;
ts.add(new Complex(1.23, -3.45));
ts.add(new Complex(16.2, 23.4));
Iterator it = ts.iterator();
while(it.hasNext()) , ((Complex)it.next()).pr();
6.22. Дії з колекціями
Колекції призначені для зберігання елементів у зручному для подальшої обробки вигляді. Дуже часто
обробка заключається в сортуванні елементів і пошуку потрібного елемента. Ці і інші методи обробки
зібрані в класс Collections.
6.23. Методи класу Collections
Всі методи класу Сollections статичні, ними можна користуватися, не створюючи екземпляри класу
Collections. Як звичайно в статичих методах, колекція, з якою працює метод, задається його аргументом.
Сортування може бути зроблена лише в упорядкованій колекції, реалізуючій інтерфейс List. Для
сортуванні в класі Сollections єсть два методи:
• static void sort (List coll) - сортує в природному порядку зростання колекцію coll, реалізуючу
інтерфейс List;
• static void sort (List coll, Comparator c) - сортує колекцію coll в порядку, заданому обєктом с. Після
сортування можна здійснювати бінарний пошук в колекції.
113
114. • static int binarySearch(List coll, Object element) - відшукує елемент element у відсортованій в
природному порядку зростання колекції coll і повертає індекс елемента або відємне число, якщо
елемент не знайдено; відємне число показує індекс, з яким елемент element був би вставлений в
колекцію, з протилежним знаком;
• static int binarySearch(List coll, Object element, Comparator c) - те ж, але колекція відсортована в
порядку, визначеному обєктом с.
Чотири методи знаходять найбільший і найменший елементи в упорядкованій колекції:
• static object max (Collection coll) - повертає найбільший в природному порядку елемент колекції
coll;
• static Object max (Collection coll, Comparator c) - те ж в порядку, заданому обєктом с;
• static object min (Collection coll) - повертає найменший в природному порядку елемент колекції coll;
• static Object min(Collection coll, Comparator c) - те ж в порядку, заданому обєктом с.
Два методи "перемішують" елементи колекції випадковим способом:
• static void shuffle (List coll) - випадкові числа задаються по замовчуванню;
• static void shuffle (List coll, Random r) - випадкові числа визначаються обєктом r.
• Метод reverse (List coll) змінює порядок розташування елементів на зворотний.
• Метод copy (List from, List to) копіює колекцію from в колекцію to.
• Метод fill (List coll, Оbject element) замінює всі елементи даної колекції coll елементом element.
З рештою методів познайомимося по мірі необхідності.
6.24. Заключення
Таким чином, в даному уроці ми вияснили, що мова Java представляє багато засобів для роботи з
великими обємами інформації. В більшості випадків достатньо додати в програму три-пять операторів,
щоб можна було зробити нетривіальну обробку інформації.
В наступному уроці ми розглянемо аналогічні засоби для роботи з масивами, датами, для одержання
випадкових чисел і інших необхідних засобів програмування.
Лабораторна робота 5. Використання класів Vector, Stack і HashTable для створення баз даних.
1. Користуючись класом Vector створіть базу даних, елементами якої є обєкти класу Student,
створені на попередній роботі. Передбачити необхідні методи управління базою даних і
проілюструвати їх на працюючій програмі.
2. Переробіть попередню програму з використанням класу Stack. Проілюструйте в програмі дію
методів, відсутніх у класі Vector.
3. Переробіть програму пункту 1 з використанням класу HashTable використовуючи пари (Прізвище
студента, обєкт класу Student).
114
115. Урок 7
Класи-утиліти
• Робота с масивами
• Локальні установки
• Робота з датами і часом
• Часовий пояс і літній час
• Клас Сalendar
• Підклас GregorianCalendar
• Представлення дати і часу
• Одержання випадкових чисел
• Копіювання масивів
• Взаємодія з системою
В цьому уроці описані засоби, корисні для створення програм: робота с масивами, датами, випадковими
числами.
7.1. Робота з масивами
В класі Arrays із пакету java.utiІ зібрано багато методів для роботи з масивами. Їх можна розділити на
чотири групи.
Вісімнадцять статичних методів сортують масиви з різними типами числових елементів у порядку
зростання чисел або просто обєкти в їх природному порядку. Вісім із них мають простий вигляд
static void sort(type[] a)
де type може бути один із семи примітивних типів byte, short, int, long, char, float, double або тип Object.
Вісім методів з тими ж типами сортують частину масиву від індексу from включно до індексу to виключно:
static void sort(type[] a, int from, int to)
Останні два методи сортування упорядковують масив або його частину з елементами типу Оbject по
правилу, заданому обєктом с, реалізуючим інтерфейс Comparator:
static void sort(Object[] a, Comparator c)
static void sort(Object[] a, int from, int to, Comparator c)
Після сортування можна організувати бінарний пошук в масиві одним із девяти статичних методів пошуку.
Вісім методів мають вигляд
static int binarySearch(type[] a, type element)
де type - один із тих же восьми типів. Девятий метод пошуку має вигляд
static int binarySearch(Object[] a, Object element, Comparator c).
Він відшукує елемент element в масиві, відсортованому в порядку, заданому обєктом с. Методи пошуку
повертають індекс знайденого елемента масиву. Якщо елемент не знайдено, то повертається відємне
число, що означає індекс, з яким елемент був би вставлений в масив у заданому порядку, з протилежним
знаком.
Вісімнадцять статичних методів заповнюють масив або частину масиву указаним значенням value:
static void fill(type[], type value)
static void fill(type[], int from, int to, type value)
де type - один із восьми примітивних типів або тип Оbject. Нарешті, девять статичних логічних методів
115
116. порівнюють масиви:
static boolean equals(type[] al, type[] a2)
де type - один із восьми примітивних типів або тип Object.
Масиви вважаються рівними, і повертається true, якщо вони мають однакову довжину і рівні елементи
масивів з однаковими індексами. В лістинзі 7.1 приведений простий приклад роботи з цими методами.
Лістинг 7.1. Застосування методів класу Arrays
import java.util.*;
class ArraysTest{
public static void main(String[] args){
int[] a = {34, -45, 12, 67, -24, 45, 36, -56};
Arrays.sort(a) ;
for (int i = 0; i < a.length; i++)
System.out.print (a[i] + " ");
System.out.println();
Arrays.fill(a, Arrays.binarySearch(a, 12), a.length, 0);
for (int i = 6; i < a.length; i++)
System.out.print(a[i] + " ");
System.out.println();
}
}
7.2. Локальні установки
Деякі дані — дати, час — традиційно представляються в різних місцевостях по-різному. Наприклад, дата
в Росії виводится в форматі число, місяць, рік через точку: 27.06.01. В США прийнято запис
місяць/число/рік через похилу риску: 06/27/01. Сукупність таких форматів для даної місцевості, як
говорять на жаргоні "локаль", зберігається в обєкті класу Locale із пакету java.utiІ. Для створення такого
обєкта достатньо знать мову language і місцевість country. Інколи потрібна третя характеристика —
варіант variant, що визначає програмний продукт, наприклад, "WIN", "MAC", "POSIX". По замовчуванню
місцеві установки визначаються операційною системою і читаються із системних властивостей.
Подивіться на рядки :
user.language = ua// Мова — українська
user.region = UA // Місцевість — Україна
file.encoding = Cp1251 // Байтове кодування — CP1251
Вони визначають українську локаль і локальне кодування байтових символів. Локаль, установлену по
замовчуванні на тій машині, де виконується програма, можна вияснити статичним методом
Locale.getDefault(). Щоб працювати з другою локаллю, її треба спочатку створити. Для цього в класі
Locale єсть два конструктори:
Locale(String language, String country)
Locale(String language, String country. String variant)
116
117. Параметр language — це рядок із двух літер, визначений стандартом ISO639, наприклад, "ru", "fr", "en".
Параметр country — рядок із двох заглавних літер, визначений стандартом ISO3166, наприклад, "RU",
"US", "EN". Параметр variant не визначається стандартом, це може бути, наприклад, рядок "Traditional".
Локаль часто указують одним рядком "ru_RU", "en_GB", "en_US", "en_CA" і т. д. Після створення локалі
можна зробити її локаллю по замовчуванню статичним методом:
Locale.setDefault(Locale newLocale);
Декілька статичних методів класу Locale дозволяють одержати параметри локалі по замовчуванні або
локалі, заданої параметром locale:
String getСountry() - стандартний код країни із двох літер;
String getDispІayCountry() - країна записується словом, що може бути виведене на екран;
String getDisplayCountry (Locale locale) - те ж для указаної локалі.
Такі ж методи єсть для мови і варіанта. Можна продивитись список всіх локалів, визначених для даної
JVM, і їх параметрів, що виводиться в стандартному вигляді:
Locale[] getAvailableLocales()
String[] getlSOCountries()
String[] getlSOLanguages()
Установленна локаль надалі використовується при виведенні даних в місцевому форматі.
7.3. Робота з датами і часом
Методи роботи з датами часом зібрані в два класи: Calendar і Date із пакету java.utiІ. Обєкт класу Date
зберігає число мілісекунд, що пройшло з 1 січня 1970 г. 00:00:00 по Грінвічу. Це "день народження" UNIX,
він називається "Epoch". Клас Date зручно використовувати для відрахунку проміжків часу в мілісекундах.
Одержати поточне число мілісекунд, що пройшли з моменту Epoch на тій машині, де виконується
програма, можна статичним методом
System.currentTimeMillis()
В класі Date два конструктори. Конструктор Date() заносить в створюваний обєкт поточний час машини, на
якій виконується програма, по системному годиннику, а конструктор Date(long miІІisec) — указане число.
Одержати значення, що зберігається в обєкті, можна методом long getTime(), установити нове значення -
методом setTime(long newTime). Три логічних методи порівнюють час:
• boolean after (long when) - повертає true, якщо час when більше даного;
• boolean before (long when) - повертає true, якщо when менше даного;
• boolean after (Object when) - повертає true, якщо when - обєкт класу Date і часи співпадають.
Ще два методи, порівнюючи час, повертають відємне число типу int, якщо даний час менше аргумента
when; нуль, якщо часи співпадають; додатне число, якщо даний час більше аргумента when:
• int compareTo(Date when);
• int compareTo(Оbject when) — якщо when не відноситься до обєктів класу Date, створюється
виключна ситуація.
Перетворення мілісекунд, що зберігаються в обєктах класу Date, в поточні час і дату виконується
методами класу calendar.
7.3.1. Часовий пояс і літній час
Методи установки і зміни часового поясу (time zone), а також літнього часу DST (Daylight Savings Time),
зібрані в абстрактному класі Timezone із пакету java.utiІ. В цьому ж пакеті єсть його реалізація — підкласс
SimpleTimeZone. В класі SimpleTimeZone три конструктори, але частіше всього обєкт створюється
статичним методом getDefault(), що повертає часовий пояс, установлений на машині, яка виконує
117
118. програму. В цих класах багато методів роботи з часовими поясами, але в більшоті випадків вимагається
тільки узнати часовий пояс на машині, що виконує программу, статичним методом getDefault(),
перевірити, чи здійснюється перехід на літній час, логічним методом useDaylightTime(), і установити
часовий пояс методом setDefault (TimeZone zone).
7.3.2. Клас Calendar
Класс Calendar — абстрактний, в ньому зібрані загальні властивості календарів: юліанського,
григоріанського, місячного. В Java API поки що єсть тільки одна його реалізація — підклас
GregorianCalendar. Оскільки Сalendar — абстрактний клас, його екземпляри створюються чотирма
статичними методами по заданій локалі і/або часовому поясі:
• Calendar getlnstance()
• Calendar getlnstance(Locale loc)
• Calendar getlnstance(TimeZone tz)
• Calendar getlnstance(TimeZone tz, Locale loc)
Для роботи з місяцями визначені цілочисельні константи від JANUARY до DECEMBER, а для роботи з
днями тижня — константи від MONDAY до SUNDAY. Перший день тижня можна узнати методом int
getFirstDayOfWeek(), a установити — методом setFirstDayOfWeek(int day), наприклад:
setFirstDayOfWeek(Calendar.MONDAY)
Решта методів дозволяють проглянути часі часовий пояс або установити їх.
7.3.3. Підклас GregorianCalendar
В григоріанському календарі дві цілочисельні константи визначають ери: вс (before Christ) і AD (Anno
Domini). Сім конструкторів визначають календар по часу, часовому поясу і/або локалі:
• GregorianCalendar()
• GregorianCalendar(int year, int month, int date)
• GregorianCalendar(int year, int month, int date, int hour, int minute)
• GregorianCalendar(int year, int month, int date, int hour, int minute, int second)
• GregorianCalendar(Locale loc)
• GregorianCalendar(TimeZone tz)
• GregorianCalendar(TimeZone tz, Locale loc)
Після створення обєкта треба визначити дату переходу з юліанського календаря на григоріанський
календар методом setGregorianChange(Date date). По замовчуванню це 15 жовтня 1582 г. На території
Росії перехід був здійснений 14 лютого 1918 р., значить, створення обєкта greg треба виконати так:
GregorianCalendar greg = new GregorianCalendar();
greg.setGregorianChange(new GregorianCalendar(1918, Calendar.FEBRUARY, 14) .getTime ()) ;
Узнати, чи являється рік високосним в григоріанському календарі, можна логічним методом isLeapYear().
Метод get(int field) повертає елемент календаря, заданий аргументом field. Для цього аргумента в класі
Calendar визначені наступні статичні цілочисельні константи: ERA, WEEK_OF_YEAR, DAY_OF_WEEK,
SECOND, YEAR, WEEK_OF_MONTH, DAY_OF_WEEK_IN_MONTH, MILLISECOND, MONTH,
DAY_OF_YEAR, HOUR_OF_DAY, ZONE_OFFSET, DATE, DAY_OF_MONTH, MINUTE, DST_OFFSET..
Декілька методів set(), що використовують ці константи, устанавлюють відповідні значення.
7.3.4. Представлення дати і часу
Різноманітні способи представлення дат і часу можназдійснювати методами, зібраними в абстрактний
клас DateFormat і його підклас SimpleDateFormat із пакету Java.text. Клас DateFormat пропонує чотири
стилі представлення дати і часу:
• стиль SHORT представляє дату і час в короткому числовому вигляді: 27.04.01 17:32; в локалі
118
119. США: 4/27/01 5:32 РМ;
• стиль MEDIUM задає рік чотирма цифрами і показує секунди: 27.04.2001 17:32:45; в локалі США
місяць представляється трьома буквами;
• стиль LONG представляє місяць словом і додає часовий пояс: 27 квітень 2001 р. 17:32:45
GMT+03.-00;
• стиль FULL в російській локалі такий же, як і стиль LONG; в локалі США додається ще день тижня.
Єсть ще стиль DEFAULT, співпадаючий зі стилем MEDIUM.
При створенні обєкта классу SimpІeDateFormat можна задати в конструкторі шаблон, що визначає якийсь
інший формат, наприклад:
SimpІeDateFormat sdf = new SimpІeDateFormat("dd-MM-yyyy hh.iran");
System.out.println(sdf.format(new Date()));
Одержимо виведення в такому вигляді: 27-04-2001 17.32.
В шаблоні літера d означає цифру дня місяця, м — цифру місяця, у — цифру року, h — цифру години, m
— цифру хвилин. Решта позначень для шаблону указані в документації по классу SimpІeDateFormat. Ці
буквенні позначення можна змінити за допомогою класу DateFormatSymbols. Не у всіх локалях можна
створити обєкт класу SimpІeDateFormat. В таких випадках використовуються статичні методи getІnstance()
класу DateFormat, що повертає обєкт класу DateFormat. Параметрами цих методів служать стиль
представлення дати і часу і, може бути, локаль. Після створення обєкта метод format() класу DateFormat
повертає рядок з датою і часом, згідно заданому стилю. В якості аргумента задається обєкт класу Date.
Наприклад:
System.out.println("LONG: " + DateFormat.getDateTimelnstance(
DateFormat. LONG, DateFormat. LONG) . format (new Date ()));
або
System.out.println("FULL: " + DateFormat.getDateTimelnstance(
DateFormat.FULL,DateFormat.FULL, Locale.US).format(new Date()));
7.4. Одержання випадкових чисел
Одержати випадкове невідємне число, строго менше одиниці, типу double можна статичним методом
random() із класу java.lang.Math. При першому зверненні до цього методу створюється генератор
псевдовипадкових чисел, якийй використовується потім при одержанні наступних випадкових чисел.
Більш серйозні дії з випадковими числами можна організувати за допомогою методів класу Random із
пакету java.utiІ. В класі два конструктори:
• Random (long seed) — створює генератор псевдовипадкових чисел, що використовує для початку
роботи число seed;
• Random() —вибирає в якості початкового значення поточний час.
Створивши генератор, можна одержувати випадкові числа відповідного типу методами nextBoolean(),
nextDouble(), nextFloat(), nextGaussian(), next int(), nextLong(), nextІnt(int max) або записати зразу
послідовність випадкових чисел в заздалегідь визначений масив байтів bytes методом nextBytes(byte[]
bytes). Дійсні випадкові числа рівномірно розподіляються в діапазоні від 0,0 включно до 1,0 виключно. Цілі
випадкові числа рівномірно розподіляються по всьому діапазону відповідного типу за одним
виключенням: якщо в аргументі указано цле число max, то діапазон випадкових чисел буде від нуля
включно до max виключно.
7.5. Копіювання масивів
В класі System із пакету java.lang єсть статичний метод копіювання масивів, який використовує сама
виконуюча система Java. Цей метод діє швидкоо і надійно, його зручно застосовувати в програмах.
Синтаксис:
119
120. static void arraycopy(Object src, int src_ind, Object dest, int dest_ind, int count)
Із масиву, на який указує посилка src, копіюється count елементів, починаючи з елемента з індексом
src_ind, в масив, на якийй указує посилка dest, починаючи з його елемента з індексом dest_ind. Всі індекси
повинні бутиь задані так, щоб елементи лежали в масивах, типи масивів повинні бути сумісними, а
примітивні типи зобовязані повністю співпадати. Посилки на масив не повинні бутиь рівні null. Посилки src
і dest можуть співпадати, при цьому для копіювання створюється проміжний буфер. Метод можна
використовувати, наприклад, для зсуву елементів в масиві. Після виконання
int[] arr = {5, 6, 1, 8, 9, 1, 2, 3, 4, 5, -3, -7};
System.arraycopy(arr, 2, arr, 1, arr.length - 2);
одержимо (5, 7, 8, 9, 1, 2, 3, 4, 5, -3, -7, -7}.
7.6. Взаємодія з системою
Клас System дозволяє здійснювати і деяку взаємодію з системою під час виконання програми (run time).
Але крім нього для цього єсть спеціальний клас Runtime. Клас Runtime містить деякі методи взаємодії з
JVM під час виконання програми. Кожний додаток може одержати тільки один екземпляр даного класу
статичним методом getRuntime (). Всі виклики цього методу повертають посилку на один і той же обєкт.
• Методи fгееМемогу() і totaІMemory() повертають обєм вільної і всієї памяті, що знаходиться в
розпорядженні JVM для розміщення обєктів, в байтах, у вигляді числа типу long. He варто твердо
спиратися на ці числа, оскільки обєм памяті змінюється динамічно.
• Метод exit(int status) запускає процес останова JVM і передає операційній системі статус
завершення status. По домовленості, ненульовий статус означає ненормальне завершення.
Зручніше використовувати аналогічний метод класу system, який являється статичним.
• Метод halt(int status) здійснює негайний останов JVM. Він не завершує запущені процеси
нормально і повинен використовуватися лише в аварійних ситуаціях.
• Метод loadІibrary(string libName) дозволє підвантажити динамічну бібліотеку під час виконання по
її імені libName.
• Метод load (string fileName) підвантажити динамічну бібліотеку по імені файла fileName, в якому
вона зберігається. Взагалі ж, замість цих методів зручніше використовувати статичні методи класу
system з тими ж іменами і аргументами.
• Метод gc() запускає процес звільнення непотрібної оперативної памяті (garbage collection). Цей
процес періодично запускається самою віртуальною машиною Java і виконується на фоні с
невеликим пріоритетом, але можна його запустити і із програми. Знову-таки зручніше
використовувати статичний Метод System.gc().
Нарешті, декілька методів ехес() запускають в окремих процесах виконувані файли. Аргументом цих
методів служить командний рядок виконуваного файла. Наприклад, Runtime.getRuntime().exec ("notepad")
запускає програму Блокнот на платформі MS Windows.
• Методи exec() повертають екземпляр класу process, що дозволяють керувати запущеним
процесом. Методом destroy() можна зупинити процес, методом exitValue() одержати його код
завершення.
• Метод waitFor() призупиняє основний підпроцес до тих пір, поки не закінчиться запущений процес.
• Три методи getlnputStream(), getOutputStream() і getErrorStream() повертають вхідний, вихідний
потік и потік помилок запущеного процесу (дивись урок 18).
120
121. Програмування у Java
Урок 8. Принципи побудови графічного інтерфейса
• Компонент і контейнер
• Ієрархія класів AWT
• Заключення
Класи використовувані в цьому уроці:
Button
Canvas
Checkbox
CheckboxMenuItem
Choice
Color
Component
Container
Frame
Graphics
Label
List
Menu
Menubar
Menultem
PopupMenu
Scrollbar
TextArea
TextField
Для вашої зручності описання цих класів поміщено в папку Java 8
8.1. Важкі і легкі компоненти
В попередніх уроках ми писали програми, звязані з текстовим терміналом і запускали їх із командного
рядка. Такі програми називаються консольними додатками. Вони розробляються для виконання на
серверах, там, де не потрібний інтерактивний звязок з користувачем. Програми, тісно взаємодіючі з
користувачем, сприймаючі сигнали від клавіатури і миші, працюють в графічному середовищі. Кожний
додаток, призначений для роботи в графічному середовищі, повинен створити хоча б одне вікно, в якому
буде виконуватися його робота, і зареєструвати його в графічній оболонці операційної системи, щоб вікно
могло взаємодіяти з операційною системою і іншими вікнами: перекриватися, переміщатися, змінювати
розміри, звертатися в ярлик.
Єсть багато різних графічних систем: MS Windows, X Window System, Macintosh. В кожній з них свої
правила побудови вікон і їх компонентів: меню, полів введення, кнопок, списків, смуг прокрутки. Ці
правила складні і заплутані. Графічні API містять сотні функцій. Для полегшення створення вікон і їх
компонентів написані бібліотеки класів: MFC, Motif, OpenLook, Qt, Tk, Xview, OpenWindows і багато інших.
Кожний клас такої бібліотеки описує зразу цілий графічний компонент, що управляється методами цього і
інших класів.
В технології Java справа ускладюється тим, що додатки Java повинні працювати в будь-якому або хоча б
в багатьох графічних середовищах. Потрібна бібліотека класів, незалежна від конкретної графічної
системи. В першій версії JDK задачу вирішили наступним способом: були розроблені інтерфейси, що
містили методи роботи з графічними обєктами. Класи бібліотеки AWT реалізують ці інтерфейси для
створення додатків. Додатки Java використовують дані методи для розміщення і переміщення графічних
обєктів, зміни їх розмірів, взаємодії обєктів.
З другого боку, для роботи з екраном в конкретному графічному середовищі ці інтерфейси реалізуються в
кожному такому середовищі окремо. В кожній графічній оболонці це робиться по-своєму, засобами цієї
121
122. оболонки за допомогою графічних бібліотек даної операційної системи. Такі інтерфейси були названі
peer-інтерфейсами. Бібліотека класів Java, основаних на peer-інтерфейсах, отримала назву AWT
(Abstract Window Toolkit). При виведенні обєкта, створеного в додатку Java і основаного на peer-
інтерфейсі, на екран створюється парний йому (peer-to-peer) обєкт графічної підсистеми операційної
системи, котрий і відображається на екрані. Ці обєкти тісно взаємодіють під час роботи додатку. Тому
графічні обєкти AWT в кожному графічному середовищі мають вигляд, характерний для цього
середовища: в MS Windows, Motif, OpenLook, OpenWindows, скрізь вікна, створені в AWT, виглядають як
"рідні" вікна. Якраз із-за такої реалізації peer-інтерфейсів і інших "рідних" методів, написаних, головним
чином, на мові C++, приходиться для кожної платформи випускати свій варіант JDK.
У версії JDK 1.1 бібліотека AWT була перероблена. В неї додана можливість створення компонентів,
повністю написаних на Java і не залежних від peer-інтерфейсів. Такі компоненти стали називати "легкими"
(lightweight) на відміну від компонентів, реалізованих через peer-інтерфейси, названих "важкими" (heavy).
"Легкі" компоненти скрізь виглядають однаково, зберігаючи заданий при створенні вигляд (look and feel).
Білше того, додаток можна розробити таким способом, щоб після його запуску можна було вибрати
якийсь певний вигляд: Motif, Metal, Windows 95 або якийсь інший, і змінити цей вигляд в будь-який момент
роботи. Ця цікава особливість "легких" компонентів отримала назву PL&F (Pluggable Look and Feel) або
plaf. Була створена обширна бібліотека "легких" компонентів Java, названа Swing. В ній були переписані
всі компоненти бібліотеки AWT, так що бібліотека Swing може використовуватися самостійно, незважаючи
на те, що всі класи з неї розширяють класи бібліотеки AWT. Бібліотека класів Swing поставлялась як
доповнення до JDK 1.1.
Інтерфейс
(набір оголошених, але не реалізованих
методів)
peer-interfaces
Реалізація методами Реалізація методами
конкретної платформи Java
Heavy Componetnts (classes) Light Components (classes)
AWT Swing
В склад Java 2 SDK вона включена як основна графічна бібліотека класів, реалізуюча ідею "100% Pure
Java", поряд з AWT. В Java 2 бібліотека AWT значно розширена доданням нових засобів рисування,
виведення текстів і зображень, одержавших назву Java 2D, і засобів, реалізуючих переміщення тексту
методом DnD (Drag and Drop). Крім того, в Java 2 включені нові методи введення/виведення Input Method
Framework і засоби звязку з додатковими пристроями введення/виведення, такими як світлове перо або
клавіатура Бройля, названі Accessibility. Всі ці засоби Java 2: AWT, Swing, Java 2D, DnD, Input Method
Framework і Accessibility склали бібліотеку графічних засобів Java, названу JFC (Java Foundation Classes).
Опис кожного з цих засобів займе цілу книгу, тому ми обмежимося представленням тільки основних
засобів бібліотеки AWT.
8.2. Компонент і контейнер
Основне поняття графічного інтерфейса користувача (ГІК) — компонент (component) графічної системи.
В українській мові це слово підрозуміває просто складову частину, елемент чого-небудь, але в
графічному інтерфейсі це поняття більш конкретне. Воно означає окремий, повністю визначений елемент,
котрий можна використовувати в графічному інтерфейсі незалежно від інших елементів. Наприклад, це
поле введення, кнопка, рядок меню, смуга прокрутки, радіокнопка. Саме вікно додатку — теж є компонент.
Компоненти можуть бути і невидимими, наприклад, панель, обєднуюча компоненти, також являється
компонентом.
Ви не здивуєтесь, узнавши, що в AWT компонентом вважається обєкт класу Component або обєкт всякого
122
123. класу, розширяючого клас Сomponent. В класі Сomponent зібрані загальні методи роботи з будь-яким
компонентом графічного інтерфейса користувача. Цей клас — центр бібліотеки AWT. Ознайомтеся з цим
класом. Кожний компонент перед виведенням на екран поміщаєтся в контейнер (container). Контейнер
"знає", як розмістити компоненти на екрані. Розуміється, в мові Java контейнер — це обєкт класу Container
або всякого його розширення. Ознайомтеся з цим класом. Прямий нащадок цього класу — клас
jcomponent — вершина ієрархії багатьох класів бібліотеки Swing.
Увага! Ви постійно будете зустрічати імена компонентів двох типів, наприклад Button і jButton. Знайте, що
перший компонент важкий і для його використання треба підключати бібліотеку AWT(import java.awt.*).
Другий компонент легкий і для його використання треба підключати бібліотеку Swing(import java.swing.*).
При створенні додатків для програміста немає значення, яку бібліотеку використовувати. А от при
створенні аплетів, які запускаються браузером, використовується виключно AWT, хоча всього можна
очікувати від нових версій Java.
Створивши компонент — обєкт класу Component або його розширення, належить додати його до
попередньо створеного обєкту класу container або його розширення одним із методів add(). Клас Container
сам являється невидимим компонентом, він розширює клас Component. Таким чином, в контейнер поряд
з компонентами можна поміщати контейнери, в яких знаходяться інші компоненти, досягаючи тим самим
більшої гнучкості розміщення компонентів. Основне вікно додатку, активно взаємодіюче з операційною
системою, необхідно побудувати по правилаx графічної системи. Воно повинне переміщатися по екрану,
змінювати розміри, реагувати на дії миші і клавіатури. У вікні повинні бути, як мінімум, наступні стандартні
компоненти.
• Рядок заголовку (title bar), з лівої сторони якого необхідо розмістит кнопку контекстного меню, а з
правої — кнопки звертання і розвертання вікна і кнопку закриття додатку.
• Необовязковий рядок меню (menu bar) з випадаючими пунктами меню.
• Горизонтальна і вертикальна смуги прокрутки (scrollbars).
• Вікно повинне бути обмежено рамкою (border), реагуючою на дії миші.
Вікно з цими компонентами в готовому вигляді описане в класі Frame. Щоб створити вікно, досить зробити
свій клас розширенням класу Frame, як показано в лістинзі 8.1. Всього вісім рядків тексту і вікно готове.
Лістинг 8.1. Найпростіше вікно додатку
import java.awt.*;
class TooSimpleFrame extends Frame{
public static void main(String[] args){
Frame fr = new TooSimpleFrame(); //Конструктор вікна
fr.setSize(400, 150); //Методами класу установлюємо властивості обєкту
fr.setVisible(true);
}
}
Класс TooSimpleFrame володіє всіма властивостями класу Frame, являючись його розширенням. В ньому
створюється экземпляр вікна fr, і установлюються розміри вікна на екрані - 400x150 пікселів - методом
setSize(). Якщо не задати розмір вікна, то на екрані появиться вікно мінімального розміру - тільки рядок
заголовку. Звичайно, потім його можна розтянути за допомогою миші до будь-якого розміру.
123
124. Потім вікно виводиться на екран методом setVisible(true). Справа в тому, що, з точки зору бібліотеки AWT,
створити вікно значить виділити область оперативної памяті, заповнену потрібними пікселями, а вивести
вміст цієї області на екран - уже інша задача, яку і вирішує метод setVisible(true). Звичайно, таке вікно
непридатне для роботи. Не кажучи уже про те, що у нього немає заголовка і тому вікно не можна закрити.
Хоча його можна переміщати по екрану, міняти розміри, звертати на панель задач і розкривати, але
команду завершення додатку ми не запрограмували. Вікно не можна закрити ані клацанням кнопкою миші
по кнопці з хрестиком в правому верхньому куті вікна, ані комбінацією клавіш <Alt>+<F4>. Приходиться за-
вершати роботу додатку способами операційної системи, наприклад, комбінацією клавіш <Ctrl>+<C>, або
закриттям вікна Командний рядок. В лістинзі 8.2 до програми лістингу 8.1 додані заголовок вікна і
звернення до методу, що дозволяє завершити додаток.
Лістинг 8.2. Просте вікно додатку
import java.awt.*;
import java.awt.event.*;
class SimpleFrame extends Frame{
SimpleFrame(String s){
super (s);
setSize(400, 150);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit (0);
}
});
}
public static void main(String[] args){
new SimpleFrame(" Мy program");
}
}
Якщо ви запустите програму в середовищі JBuilder, то позбавитесь від надокучливого вікна Командная
строка.
В програму додано конструктор класу SimpleFrame, що звертається до конструктора свого суперкласу
Frame, який записує свій аргумент s в рядок заголовка вікна. В конструктор перенесена установка розмірів
вікна, виведення його на екран і додано звернення до методу addWindowListener(), реагуючому на дії з
вікном. В якості аргумента цьому методу передається екземпляр безіменного внутрішнього класу,
розширяючого клас WindowAdapter. Цей безіменний клас реалізує метод windowСІosing(), що обробляє
закриття вікна.
124
125. Дана реалізація дуже проста — додаток завершається статичним методом exit() класу System. Вікно при
цьому закривається автоматично. Все це ми детально розберемо в уроці 12, а поки що просто додавайте
ці рядки у всі ваші програми для закрття вікна і завершення роботи додатку. Отже, вікно готове. Але воно
поки що пусте. Виведемо в нього, по традиції, привітання "Hello, World!", правда, злегка змінене. В лістинзі
3.3 приведена повна програма цього виведення, а рис. 8.1 демонструє вікно.
Лістинг 8.3. Графічна програма з привітанням
import java.awt.*;
import java.awt.event.*;
class Hello extends Frame{
Hello(String s){
super(s);
}
public void paint(Graphics g){
g.setFont(new Font("Serif", Font.ITALIC | Font.BOLD, 30));
g.drawString("Hello, XXI century World!", 20, 100);
}
public static void main(String[] args){
Frame f = new Hello("Вітаю тебе, XXI століття!");
f.setSize(400, 150);
f.setVisible(true);
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 8.1. Вікно програми-привітання
Для виведення тексту ми перевизначаєм метод paint() класу Сomponent. Клас Frame наслідує цей метод з
пустою реалізацією. Метод paint() одержує в якості аргумента екземпляр g класу Graphics, що вміє,
зокрема, виводити текст методом drawString(). В цьому методі крім тексту ми указуємо положення початку
рядка у вікні - 20 пікселів від лівого краю і 100 пікселів зверху. Ця точка - ліва нижня точка першої літери
тексту Н. Крім того, ми установили новий шрифт "Serif" більшого розміру - 30 пунктів, напівжирний, курсив.
Всякий шрифт - обєкт класу Font, а задається він методом setFont() класу Graphics. Роботу з шрифтами
ми розглянемо в наступному уроці. В лістинзі 8.3 ми винесли виклики методів установки розмірів вікна,
виведення його на екран і завершення програми в метод main().
Як бачимо із цього простого прикладу, бібліотека AWT велика і розгалужена, в ній багато класів,
взаємодіючих один з одним. Розглянемо ієрархію деяких найчастіше використовуваних класів AWT.
8.3. Ієрархія класів AWT
На рис. 8.2 показана ієрархія основних класів AWT. Основу її складають готові компоненти: Button,
125
126. Canvas, Checkbox, Choice, Container, Label, List, Scrollbar, TextArea, TextField, Menubar, Menu, PopupMenu,
Menultem, CheckboxMenuItem. Якщо цього набору замало, то від класу Canvas можна породити власні
"важкі" компоненти, а від класу Component — "легкі" компоненти.
Основні контейнери — це класи Panel, ScrollPane, Window, Frame, Dialog, FileDialog. Свої "важкі"
контейнери можна породити від класу Panel, а "легкі" — від класу Сontainer. Цілий набір класів допомагає
розміщувати компоненти, задавати колір, шрифт, рисунки і зображення, реагувати на сигнали від миші і
клавіатури.
На рис. 8.2 показані і початкові класи ієрархії бібліотеки Swing — класи JComponent, JWindow, JFrame,
JDialog, JApplet.
Заключення
Як бачимо, бібліотека графічних класів AWT дуже велика і детально опрацьована. Ця різноманітність
класів тільки відображає різноманітність задач побудови графічного інтерфейса. Бажання покращити
інтерфейс безмежне. Воно приводит до створення все нових бібліотек класів і розширенню існуючих.
Незалежними виробниками створено уже багато графічних бібліотек Java: KL Group, JBCL, і зявляються
все нові і нові бібліотеки. Інформацію про них можна одержати на сайтах, указаних в уроці 1. В наступних
уроках ми детально розглянемо, як можна використовувати бібліотеку AWT для створення власних
додатків з графічним інтерфейсом користувача, зображеннями, анімацією і звуком.
Рис. 8.2. Ієрархія основних класів AWT
Лабораторна робота 7. Робота з графічними компонентами
1. Створіть вікно більшого розміру на зразок лістингу 8.3.
2. Видаліть з нього текстовий рядок.
3. Попробуйте додати в нього послідовно компоненти Button, Canvas, Checkbox, Choice, Container,
Label, List, Scrollbar, TextArea, TextField, Menubar, Menu, PopupMenu, Menultem,
CheckboxMenuItem, і кожного разу запускайте програму, щоб пересвідчитися який вигляд мають ці
компоненти. При одночасному виведенні вони можуть накластися один на одний, так що ви
деяких із них можете і не побачити. Давайте компонентам те ж саме імя, але з індексом,
126
127. наприклад Button button1. Майте на увазі, що ви працюєте з контейнером і створивши компонент
він автоматично в ньому не зявиться. Перечитайте уважно початок параграфу 8.2, щоб зрозуміти,
яким методом компонент додається до контейнера. Дивись зразок нижче.
class Hello extends Frame{
Hello(String s){
super(s);
Button button1 = new Button("Button1");
add(button1);
}
4. Ви помічаєте, що картина дещо інша, ніж у VB, Delphi, MFC. Границі компонентів не чіткі. Щоб їх
краще розрізняти, закрашуйте в різні кольори. Колір, як і все у Java, це клас. Установити колір
значить створити обєкт цього класу. Ось приклад:
Color cl = new Color(255,0,0);
button1.setBackground(cl);
Про інші конструктори класу Color почитайте у відповідному файлі.
5. Тепер ви бачите, що by default y Java компонент займає все вікно. Щоб покінчити з цим
неподобством зразу ж після команди super(s); вставте setLayout(null); Тепер розтягніть
ваші компоненти по формі методом setBounds() визначеним у класі Component і, таким чином,
діючим для всіх компонентів. Зробіть так, щоб усі вказані вище компоненти були видимі
одночасно. Відцентруйте написи на них. Учіться знаходити необхідні методи у відповідних класах.
6. Поміняйте колір фону вікна з білого на якийсь інший двома методами: у методі main() і зовні цього
методу.
127
128. Урок 9
Графічні примітиви
• Методи класу Graphics
• Як задати колір
• Як нарисувати фігуру
• Клас Polygon
• Як вивести текст
• Як установити шрифт
• Як задати шрифт
• Клас FontMetrics
• Можливості Java 2D
• Перетворення координат
• Клас AffineTransform
• Рисування фігур засобами Java 2D
• Клас BasicStroke
• Клас GeneralPath
• Класи GradientPaint і TexturePaint
• Виведення тексту засобами Java 2D
• Методи покращення візуалізації
• Заключення
Потрібні в цьому уроці нові класи ви можете знайти в папці Java9.
При створенні компонента, тобто обєкта класу Component, автоматично формується його графічний
контекст (graphics context). В контексті розміщується область рисування і виведення тексту та
зображень. Контекст містить поточний і альтернативний колір рисування і колір фону — обєкти класу
Сolor, поточний шрифт для виведення тексту — обєкт класу Font. В контексті визначена система
координат, початок якої з координатами (0, 0) розташований у верхньому лівому куті області рисування,
вісь Ох направлена вправо, вісь Оу — вниз. Точки координат знаходяться між пікселями.
Керує контекстом класс Graphics або новий клас Graphics2D, введений в Java 2. Оскільки графічний
контекст сильно залежить від конкретної графічної платформи, ці класи зроблені абстрактними. Тому не
можна безпосередньо створити екземпляри класу Graphics або Graphics2D. Одначе кожна віртуальна
машина Java реалізує методи цих класів, створює їх екземпляри для компонента і представляє обєкт
класу Graphics методом getGraphics() класу Component або як аргумент методів paint() і update().
Подивимось спочатку, які методи роботи з графікою і текстом представляє нам класс Graphics.
9.1. Методи класу Graphics
При створенні контексту в ньому задається поточний колір для рисування, зазвичай чорний, і колір фону
області рисування — білий або сірий. Змінити поточний колір можна методом setСoІor (Color newCoІor),
аргумент newСoІor котрого — обєкт класу Color. Узнати поточний колір можна методом getСolor(),
повертаючим обєкт класу Сolor.
9.1.1. Як задати колір
Колір, як і все в Java, — обєкт певного класу, а саме, класу Сolor. Основу класу складають сім
конструкторів кольору. Самий простий конструктор:
Color(int red, int green, int blue)
створює колір, одержаний змішуванням червоного red, зеленого green і синього blue . Ця кольорова
модель називається RGB. Кожна складова змінюється від 0 (відсутність складової) до 255 (повна
інтенсивність цієї складової). Наприклад:
Color pureRed = new Color(255, 0, 0);
Color pureGreen = new Color(0, 255, 0);
128
129. визначають чистий яскраво-червоний pureRed і чистий яскраво-зелений pureGreen кольори.
В другому конструкторі інтенсивність складової можна змінювати більш гладко дійсними числами від 0.0
(відсутність складової) до 1.0 (повна інтенсивність складової):
Color(float red, float green, float blue)
Наприклад:
Color someColor = new Color(0.05f, 0.4f, 0.95f);
Третій конструктор
Color(int rgb)
задає всі три складові в одному цілому числі. В бітах 16—23 записується червона складова, в бітах 8—15
— зелена, а в бітах 0—7 — синя складова кольору. Наприклад:
Color с = new Color(0хFF8F48FF);
Тут червона складова задана з інтенсивністю 0x8F, зелена — 0x48, синя — 0xFF.
Наступні три конструктори
Color(int red, int green, int blue, int alpha)
Color(float red, float green, float blue, float alpha)
Color(int rgb, boolean hasAlpha)
вводять четверту складову кольору, так звану "альфу", що визначає прозорість кольору. Ця складова
проявляє себе при накладанні одного кольору на другий. Якщо альфа рівна 255 або 1,0, то колір повністю
непрозорий, попередній колір не просвічується крізь нього. Якщо альфа рівна 0 або 0,0, то колір
абсолютно прозорий, для кожного пікселя видно тільки попередній колір. Останній із цих конструкторів
враховує складову альфа, що знаходиться в бітах 24—31, якщо параметр hasAІpha рівний true. Якщо ж
hasAІpha рівне false, то складова альфа вважається рівною 255, незалежно від того, що записано в
старших бітах параметра rgb. Перші три конструктори створюють непрозорий колір з альфою, рівною 255
або 1,0.
Сьомий конструктор
Color(ColorSpace cspace, float[] components, float alpha)
дозволяє створювати колір не тільки в кольоровій моделі (color model) RGB, але і в інших моделях: CMYK,
HSB, CIEXYZ, визначених обєктом класу ColorSpace. Для створення кольору в моделі HSB можна
скористатися статичним методом
getHSBColor(float hue, float saturation, float brightness).
Якщо немає необхідності ретельно підбирати кольори, то можна просто скористатися однією із
тринадцати статичних констант типу color класу Сolor. Наперекір стандарту "Code Conventions" вони
записуються рядковими літерами: black, blue, cyan, darkGray, gray, green, lightGray, magenta, orange, pink,
red, white, yellow.
Методи класу Color дозволяють одержати складові поточного кольору: getRed(), getGreen(), getBlue(),
getAlpha(), getRGB(), getColorSpace (), getComponents (). Два методи створюють більш яскравий brighter() і
більш темний darker() кольори в порівнянні з поточним кольором. Вони корисні, якщо треба виділити
активний компонент або, навпаки, показати неактивний компонент блідніше решти компонентів. Два
статичних методи повертають колір, перетворений із кольорової моделі RGB в HSB і навпаки:
float[] RGBtoHSB(int red, int green, int blue, float[] hsb)
129
130. int HSBtoRGB(int hue, int saturation, int brightness)
Створивши колір, можна рисувати ним в графічному контексті.
9.1.2. Як нарисувати фігуру
Основний метод рисування
drawLine(int x1, int y1, int х2, int y2)
рисує поточним кольором відрізок прямої між точками з координатами (x1, y1) і (х2, у2). Одного
цього методу достатньо, щоб нарисувати будь-яку картину по точках, рисуюючи кожну точку з
координатами (х, у) методом drawLine (x, у, х, у) і змінюючи кольори від точки до точки. Але ніхто,
зрозуміло, не стане цього робити.
Інші графічні примітиви:
• drawRect(int x, int у, int width, int height) — рисує прямокутник зі сторонами, параллльними краям
екрана, і задається координатами верхнього лівого кута (х, у), шириною width пікселів і висотою
height пікселів;
• draw3DRect(int x, int у, int width, int height, boolean raised) — рисує прямокутник, що ніби-то
виділяється із площини рисування, якщо аргумент raised рівний true, або ніби-то вдавлений в
площину, якщо аргумент raised рівний false;
• drawOval(int x, int у, int width, int height) — рисує овал, вписаний в прямокутник, заданий
аргументами метода. Якщо width == height, то одержимо коло;
• drawArc(int x, int у, int width, int height, int startAngle, int arc) — рисує дугу овала, вписаного в
прямокутник, заданий першми чотирма аргументами. Дуга має величину arc градусів і
відраховується від кута startAngle. Кут відраховується в градусах від вісі Ох. Додатній кут
відраховується проти годиникової стрілки, відємний — по годинниковій стрілці;
• drawRoundRect (int x, int у, int width, int height, int arcWidth, int arcHeight) — рисує прямокутник із
закругленими краями. Закруглення викреслюються четвертинками овалів, вписаних в
прямокутники шириною arcWidth і висотою arcHeight, побудовані в кутах основного прямокутника;
• drawPolyline(int[] xPoints, int[] yPoints, int nPoints) — рисує ламану з вершинами в точках xPoints[i],
yРoints[i]) і числом вершин nPoints;
• drawPolygon(int[] xPoints, int[] yPoints, int nPoints) — рисує замкнуту ламану, проводячи замикаючий
відрізок прямої між першою і останньої точками;
• drawРoІygon(Polygon p) — рисує замкнуту ламану, вершини якої задані объектом р класу Polygon.
9.2. Клас Polygon
Цей клас призначений для роботи з многокутником, зокрема, з трикутниками і довільними
чотирикутниками. Обєкти цього класу можна створити двома конструкторами:
• Polygon () — створює пустий обєкт;
• Polygon(int[] xPoints, int[] yPoints, int nPoints) — задаються вершини многокутника (xPoints[i],
yPoints[i]) і їх число nPoints
Після створення обєкта в нього можна додавати вершини методом
addPoint(int x, int у)
Логічні методи contains() дозволяють перевірити, чи не лежить в многокутнику задана аргументами
метода точка, відрізок прямої або цілий прямокутник зі сторонами, паралельними сторонам екрана.
Логічні методи intersects() дозволяють перевірити, чи не перетинаються з даним многокутником відрізок
прямої, заданий аргументами метода, або прямокутник зі сторонами, паралельними сторонам экрана.
Методи getBounds() і getBounds2D() повертають прямокутник, цілком вміщаючий в себе даний
многокутник.
Повернемося до методів класу Graphics. Декілька методів викреслюють фігури, залиті поточним
130
131. кольором: fillRect(), fill3DRect(), filІArco, fiІІОval(), filІPoІiygon(), filІRoundRect(). У них такі ж аргументи, як і у
відповідних методів, викреслюючих незаповнені фігури. Наприклад, якщо ви хочете змінити колір фону
області рисування, то установіть новий поточний колір і накресліть ним заповнений прямокутник
величиною у всю область:
public void paint(Graphics g){
Color initColor = g.getColor(); // Зберігаємо початковий колір
g.setColor(new Color(0, 0, 255)); // Устанавлюємо колір фону
// Заливаємо область рисування
g.fillRect(0, 0, getSizeO.width - 1, getSize().height - 1);
g.setColor(initColor); // Відновлюємо початковий колір
// Подальші дії
}
Як бачимо, в класі Graphics зібрані тільки самі необхідні засоби рисування. Немає навіть методу, що
задає колір фону (хоча можна задати колір фону компонента методом setBackground() класу Сomponent).
Засоби рисування, виведення тексту в область рисування і виведення зображень значно доповнені і
розширені в підкласі Graphics2D, що входить в систему Java 2D. Наприклад, в ньому єсть метод задання
кольору фону setBackground(Color с). Перед тим як звернутися до класу Graphics2D, розглянемо засоби
класу Graphics для виведення тексту.
9.3. Як вивести текст
Для виведення тексту в область рисування поточним ольором і шрифтом, починаючи з точки (х, у), в
класі Graphics єсть декілька методів:
• drawString (String s, int x, int y) – виводить рядок s;
• drawBytes(byte[] b, int offset, int length, int x, int у) — виводить length елементів масиву байтів b,
починаючи з індексу offset;
• drawChars(chart] ch, int offset, int length, int x, int у) — виводить length елементів масиву символів
ch, починаючи з індексу offset.
Четвертий метод виводить текст, занесений в обєкт класу, реалізуючого інтерфейс
AttributedCharacterІterator. Це дозволяє задавати свій шрифт для кожного символу:
drawString(AttributedCharacterІterator iter, int x, int y)
Точка (х, у) — це ліва нижня точка першої літери тексту на базовій лінії (baseline) виведення шрифта.
9.3.1. Як установити шрифт
Метод setFont(Font newFont) класу Graphics установлює поточний шрифт для виведення тексту. Метод
getFont() повертає поточний шрифт. Як і все в мові Java, шрифт — це обєкт класу Font. Подивимося, які
можливості представляє цей клас.
9.3.2. Як задати шрифт
Обєкти класу Font зберігають накреслення (glyphs) символів, що утворюють шрифт. Їх можна створити
двома конструкторами:
• Font (Map attributes) — задає шрифт із заданими аргументом attributes. Ключі атрибутів i деякі їх
значенния задаються константами класу TextAttribute із пакету java.awt.font. Цей конструктор
характерний для Java 2D і буде розглянутий далі в цьому уроці.
• Font (String name, int style, int size) — задає шрифт по імені name, із стилем style і розміром size
типографських пунктів. Цей конструктор характерний для JDK 1.1, але широко використовується і
в Java 2D в силу своєї простоти.
Типографський пункт в Россії і деяких європейських країнах рівний 0,376 мм, Точніше, 1/72 частини
французького дюйма. В англо-американській системі мір пункт рівний 1/72 частини англійського дюйма,
131
132. 0,351 мм. Останній пункт і використовується в компютерній графіці. Імя шрифту name може бути рядком із
фізичним іменем шрифту, наприклад, "Courier New", або один із рядків "Dialog", "Dialoglnput",'
"Monospaced", "Serif", "SansSerif", "Symbol". Це так звані логічні імена шрифтів (logical font names). Якщо
name == null, то задається шрифт по замовчувнню. Стиль шрифту style — це одна із констант класу Font:
• BOLD — напівжирный;
• ITALIC — курсив;
• PLAIN — звичайний.
Напівжирний курсив (bolditalic) можна задати операцією побітового додавання, Font. BOLD | Font. ITALIC,
як це зроблено в лістинзі 8.3. При виведенні тексту логічним іменам шрифтів і стилям співставляються
фізичні імена шрифтів (font face name) або імена сімейств шрифтів (font name). Ці імена реальних
шрифтів, наявних у графічній підсистемі операційної системи. Наприклад, логічному іменi "Serif" може
бути співставлено імя сімейства (family) шрифтів Times New Roman, а в комбінації зі стилями — конкретні
фізичні імена Times New Roman Bold, Times New Roman Italic. Ці шрифти повинні знаходитися в складі
шрифтів графічної системи тієї машини, на якій виконується додаток. Список імен доступних шрифтів
можна прородивитися наступними операторами:
Font[] fnt = Toolkit.getGraphicsEnvironment.getAHFonts();
for (int i = 0; i< fnt.length; i++)
System.out.println(fnt[i].getFontName());
В склад SUN J2SDK входить сімейство шрифтів Lucida. Установивши SDK, ви можете бути впевнені, що
ці шрифти єсть у вашій системі. Таблиці співставлення логічених і фізичних імен шрифтів знаходяться у
файлах з іменами
• font.properties;
• font.properties.ar;
• font.properties.ja;
• font.properties.ru.
і т. д. Ці файли повинні бути розташовані в JDK в каталозі jdkl.3jrelib або в якому-небудь іншому
підкаталозі lib кореневого каталога JDK тієї машини, на якій виконується додаток. Потрібний файл
вибирається віртуальною машиною Java по закінченні імені файла. Це закінчення співпадає з
міжнародним кодом мови, установленого в локалі або в системній властивості user.language (див. рис.
6.2). Якщо у вас установлена російська локаль з міжнародним кодом мови "ru", то для співставлення буде
вибраний файл font.properties.ru. Якщо такий файл не знайдено, то використовується файл font.properties,
що не відповідає ніякій конкретній локалі. Тому можна залишити в системі тільки один файл
font.properties, переписавши в нього зміст потрібного файла або створивши файл заново. Для будь-якої
локалі буде використовуватися цей файл. В лістинзі 9.1 показано скорочений зміст файла
font.properties.ru із JDK 1.3 для платформи MS Windows.
Лістинг 9.1. Примірний файл font.properties.ru :
# %W% %E%
# Це просто коментарі
# AWT Font default Properties for Russian Windows
#
# Три співставлення логічному імені "Dialog":
dialog.0=Arial,RUSSIAN_CHARSET
dialog.l=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
dialog.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
# По три співставлення стилям ITALIC, BOLD, ITALIC+BOLD:
dialog.italic.0=Arial Italic,RUSSIAN_CHARSET
dialog.italic.l=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
dialog.italic.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
dialog.bold.0=Arial Bold,RUSSIAN_CHARSET
dialog.bold.l=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
dialog.bold.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
132
133. dialog.bolditalic.0=Arial Bold Italic,RUSSIAN_CHARSET
dialog.bolditalic.l=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
dialog.bolditalic.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
# По три співставлення імені "Dialoglnput" і стилям:
dialoginput.0=Courier New,RUSSIAN_CHARSET
dialoginput.l=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
dialoginput.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
dialoginput.italic.0=Courier New Italic,RUSSIAN_CHARSET
# і так далі
#
# По три співставлення імені "Serif" і стилям:
serif.0=Times New Roman,RUSSIAN_CHARSET
serif.l=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
serif.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
serif.italic.0=Times New Roman Italic,RUSSIAN_CHARSET
# і так далі
# Інші логічні імена
sansserif. CMArial,RUSSIAN_CHARSET
sansserif.l=WingDings,SVMBOL_CHARSET,NEED_CONVERTED
sansserif.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
sansserif.italic. 0=Arial Italic,ROSSIAN_CHARSET
# і так далі
#
monospaced.0=Courier New,RUSSIAN_CHARSET
monospaced.l=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
monospaced.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
monospaced.italic.0=Courier New Italic,RUSSIAN_CHARSET
# і так далі
# Default font definition
#
default.char=2751
# for backword compatibility
# Старі логічні імена версії JDK 1.0
timesroman.0=Times New Roman,RUSSIAN_CHARSET
helvetica.0=Arial,RUSSIAN_CHARSET
courier.0=Courier New,RUSSIAN_CHARSET
zapfdingbats.0=WingDings,SYMBOL_CHARSET
# font filenames for reduced initialization time
# Файли зі шрифтами
filename.Arial=ARIAL.TTF
filename.Arial_Bold=ARIALBD.TTF
filename.Arial_Italic=ARIALI.TTF
filename.Arial_Bold_Italic=ARIALBI.TTF
filename.Courier_New=COUR.TTF
filename.Courier_New_Bold=COURBD.TTF
filename.Courier_New_Italic=COURI.TTF
filename.Courier_New_Bold_Italic=COURBI.TTF
filename.Times_New_Roman=TIMES.TTF
filename.Times_New_Roman_Bold=TlMESBD.TTF
filename.Times_New_Roman_Italic=TIMES3.TTF
filename.Times_New_Roman_Bold Italic=TIMESBI.TTF
filename.WingDings=WINGDING.TTF
filename.Symbol=SYMBOl.TTF
# name aliases
# Псевдоіими логічних імен закоментовані
# alias.timesroman=serif
# alias.helvetica=sansserif
# alias.courier=monospaced
# Static FontCharset info
133
134. #
# Класи перетворення символів у байти
fontcharset.dialog.0=sun.io.CharToByteCP1251
fontcharset.dialog.l=sun.awt.windows.CharToByteWingDings
fontcharset.dialog.2=sun.awt.CharToByteSymbol
fontcharset.dialoginput.0=sun.io.CharToByteCP1251
fontcharset.dialoginput.l=sun.awt.windows.CharToByteWingDings
fontcharset.dialoginput.2=sun.awt.CharToByteSymbol
fontcharset.serif.0=sun.io.CharToByteCP1251
fontcharset.serif.l=sun.awt.windows.CharToByteWingDings
fontcharset.serif.2=sun.awt.CharToByteSymbol
fontcharset.sansserif.0=sun.io.CharToByteCP1251
fontcharset.sansserif.l=sun.awt.windows.CharToByteWingDings
fontcharset.sansserif.2=sun.awt.CharToByteSymbol
fontcharset.monospaced.0=sun.io.CharToByteCP1251
fontcharset.monospaced.l=sun.awt.windows.CharToByteWingDings
fontcharset.monospaced.2=sun.awt.CharToByteSymbol
# Exclusion Range info
#
# He проглядати вцьому шрифті вказані діапазони
exclusion.dialog.0=0100-0400,0460-ffff
exclusion.dialoginput.0=0100-0400, 0460-ffff
exclusion.serif.0=0100-0400,04 60-ffff
exclusion.sansserif.0=0100-0400, 0460-ffff
exclusion.monospaced.0=0100-0400,0460-ffff
# charset for text input
#
# Введені байтові символи кодуються в кириличний діапазон кодуванняи Unicode
inputtextcharset=RUSSIAN_CHARSET
Більша частина цього файла зайнята співставленнями логічних і фізичних імен. Ви бачите, що під
номером 0:
• логічномлу імені "Dialog" співставлено імя сімейства Arial;
• логічному імені "Dialoginput" співставлено імя сімейства Courier New;
• логічному імені "Serif" співставлено імя сімейства Times New Roman;
• логічному імені "Sansserif" співставлено імя сімейства Arial;
• логічному імені "Monospaced" співставлено імя сімейства Courier New.
Там, де указаний стиль: dialog.italic, dialog.bold і т.д., підставлений відповідний фізичний шрифт. В рядках
лістинга 9.1, що починаються зі слова filename, указані файли з відповідними фізичними шрифтами,
наприклад:
filename.Arial=ARIAL.TTF
Ці рядки необобовязкові, але вони прискорюють пошук файлів зі шрифтами. Теперь подивимося на
останні рядки лістинга 9.1. Рядок
exclusion.dialog.0=0100-0400, 0460-ffff
означає, що в шрифті, співставленому логічному імені "Dialog" під номером 0, а саме, Arial, не стануть
відшукуватися накреслення (glyphs) символів з кодами в діапазонах 'u0100' —'u0400' і 'u0460' —'uFFFF'.
Вони будуть взяті із шрифту, співставленого цьому імени під номером 1, а саме, WingDings. Те ж саме
буде відбуватися, якщо потрібні накреслення не знайдені в шрифті, співставленому логічному імені під
номером 0. Не всі файли зі шрифтами Unicode містять накреслення (glyphs) всіх символів. Якщо потрібні
накреслення не знайдені і в співставленні 1 (в даному прикладі в шрифті WingDings), вони будуть
відшукуватися у співставленні 2 (тобто в шрифті Symbol) і т. д. Подібних співставлень можна написати
скільки завгодно. Таким чином, кожному логічому імені шрифта можна співставити різні діапазони різних
реальних шрифтів, а також застрахуватися від відсутності накреслень деяких символів в шрифтах
134
135. Unicode.
Всі співставлення під номерами 0, 1, 2, 3, 4 належить повторити для всіх стилів: bold, italic, bolditalic. Якщо
в графічній системі використовуються шрифти Unicode, як, наприклад, в MS Windows NT/2000, то більше
ні про що не треба турбуватися. Якщо ж графічна система використовує байтові ASCII-шрифти як,
наприклад, MS Windows 95/98/ME, то треба потурбуватися про їх правильне перекодування в Unicode і
навпаки. Для цього на платформі MS Windows використовуються константи Win32 API
RUSSIAN_CHARSET, SYMBOL_CHARSET, ANSI_CHARSET, OEM_CHARSET і ін., показуючі, яку кодову
таблицю використовувати при перекодуванні, так же, як це відмічалося в уроці 5 при створенні рядка із
масиву байтів. Якщо логічним іменам співставлені байтові ASCII-шрифти (в прикладі це шрифти
WingDings і Symbol), то необхідність перекодування задається константою NEED_CONVERTED.
Перекодуванням займаються методи спеціальних класів charToByteCP1251, TiarToByteWingDings,
CharToByteSymbol. Вони указуються для кожного співставлення імен в рядках, що починаються зі слова
fontcharset. Ці рядки обовязкові для всіх шрифтів, помічених константою NEED_CONVERTED. В
останньому рядку файла указана кодова сторінка для перекодування в Unicode символів, що вводяться в
поля введення:
inputtextcharset = RUSSIAN_CHARSET
Цей запис задає кодову таблицю СР1251. Отже, збираючись виводити рядок str в графічний контекст
методом drawstring(), ми створюємо поточний шрифт конструктором класуа Font, указуючи в ньому
логічне імя шрифту, наприклад, "Serif". Виконуюча система Java відшукує у файле font.properties,
відповідному локальній мові, співставлений цьому логічному імені фізичний шрифт операційної системи,
наприклад, Times New Roman. Якщо це Unicode-шрифт, то із нього добуваються накреслення символів
рядка str по їх кодуванню Unicode і відображаються в графічний контекст. Якщо це байтовий ASCII-
шрифт, то рядок str попередньо перекодовується в масив байтів методами класу, указаного в одному із
рядків fontcharset, наприклад, CharToByteCP1251. Хороші приклади файлів font.properties.ru зібрані на
сторінці Сергія Астахова, указаній в уроці 0. Обговорення цих питань і прикладів файлів font.properties для
X Window System дані в документації SUN J2SDK в файлі docs/guide/intl /fontprop.html.
Завершуючи обговорення логічних і фізичних імен шрифтів, належить сказати, що в JDK 1.0
використовувались логічні імена "Helvetica", "TimesRoman", "Courier", замінені в JDK 1.1.З "SansSerif",
"Serif", "Monospaced", відповідно, із ліцензійних міркувань. Старі імена залишились у файлах
font.properties для сумісності.
При виведенні рядка у вікно додатку дуже часто виникає необхідність розташувати її певним способом
відносно інших елементів зображення: центрувати, вивести над або під іншим графічним обєктом. Для
цього треба знати метрику рядка: ії висоту і ширину. Для вимірювання розмірів окремих символів і рядка в
цілому розроблений клас FontMetrics. В Java 2D клас FontMetrics замінений класом TextLayout. Його ми
розглянемо в кінці цього уроку , а зараз вияснимо, яку користь можна отримати із методів класу
FontMetrics.
9.4. Класс FontMetrics
Клас FontMetrics являється абстрактним, тому не можна скористатися його конструктором. Для
одержання обєкта класу FontMetrics, що містить набір метричних характеристик шрифту f, треба
звернутися до методу getFontMetrics (f) класу Graphics або класу Component. Клас FontMetrics дозволяє
узнати ширину окремого символу ch в пікселях методом charwidth(ch), загальну ширину всіх символів
масиву або під-масиву символів або байтів методами getСhars() і getBytes(), ширину цілого рядка str в
пікселях методом stringwidth(str).
Декілька методів повертають в пікселях вертикальні розміри шрифту. Інтерліньяж (leading) — відстань між
нижньою точкою звисаючих елементів такихлітер, как р, у і верхньою точкою виступаючих елементів таких
літер, як б і в наступного — повертає метод getLeading(). Середня відстань від базової лінії шрифту до
верхньої точки прописных літер і виступаючих елементів того ж рядка (ascent) повертаєт метод
getAscent(), а максимальне — метод getMaxAscent(). Середню відстань звисаючих елементів від базової
лінії того ж рядка (descent) повертає метод getDescent(), а максимальну — метод getMaxDescent().
Нарешті, висоту шрифту (height) — суму ascent + descent + leading — повертає метод getHeight(). Висота
шрифту рівна відстані між базовими лініями сусідніх рядків. Ці елементи показані на рис. 9.1.
135
136. Рис. 9.1. Елементи шрифту
Додаткові характеристики шрифту можно визначити методами класу LineMetrics із пакету java.awt.font.
Обєкт цього класу можна одержати кількома методами getLineMetrics() класу FontMetrics. Лістинг 9.2
показує застосування графічних примітивів і шрифтів, а рис. 9.2 — результат виконання програми із цього
лістингу.
Лістинг 9.2. Використання графічиих примітивів і шрифтів
import java.awt.*;
import java.awt.event.*;
class GraphTest extends Frame{
GraphTest(String s){
super(s);
}
public void paint(Graphics g){
Dimension d = getSize();
int dx = d.width / 20, dy = d.height / 20;
g.drawRect(dx, dy + 20, d.width - 2*dx, d.height - 2*dy - 20);
g.drawRoundRect(2 * dx, 2*dy + 20, d.width - 4*dx, d.height - 4*dy - 20, dx, dy);
g.fillArc(d.width / 2 - dx, d.height - 2*dy + 1, 2*dx, dy - 1, 0, 360);
g.drawArc(d.width / 2 - 3*dx, d.height - 3*dy / 2 - 5, dx, dy / 2, 0, 360);
g.drawArc(d.width / 2 + 2*dx, d.height - 3*dy / 2 - 5, dx, dy / 2, 0, 360);
Font f1 = new Font("Serif", Font.BOLD|Font.ITALIC, 2 * dy);
Font f2 = new Font ("Serif", Font.BOLD, 5 * dy / 2);
FontMetrics fm1 = getFontMetrics(f1);
FontMetrics fm2 = getFontMetrics(f2);
String s1 = "Всяка останя помилка";
String s2 = "являється передостанньою.";
String s3 = "Закон налаштування програми";
int firstLine = d.height / 3;
int nextLine = fm1.getHeight();
int secondLine = firstLine + nextLine / 2;
g.setFont(f2);
g.drawString(s3, (d.width-fm2.stringWidth(s3)) / 2, firstLine);
g.drawLine(d.width / 4, secondLine - 2,3 * d.width / 4, secondLine - 2);
g.drawLine(d.width / 4, secondLine - 1, 3 * d.width / 4, secondLine - 1);
g.drawLine(d.width / 4, secondLine, 3 * d.width / 4, secondLine);
g.setFont(f1);
g.drawString(s1, (d.width - fm1.stringWidth(s1)) /2, firstLine + 2 * nextLine);
g.drawString(s2, (d.width - fm1.stringWidth(s2)) / 2, firstLine + 3 * nextLine);
}
public static void main(String[] args){
GraphTest f = new GraphTest(" Приклад рисування");
f.setBounds(0, 0, 500, 300);
f.setVisible(true);
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
136
137. }
});
}
}
В лістинзі 9.2 використаний простий клас Dimension, головне призначення якого — зберігати ширину і
висоту прямокутного обєкта в своїх полях width і height. Метод getsize() класу Сomponent повертає
розміри компонента у вигляді обєкта класу Dimension. В лістинзі 9.2 розміри компонента f типу GrapMest
установлені в конструкторі методом setBounds() рівнbми 500x300 пікселів. Ще одна особливість лістингу
9.2 — для викреслювання товстої лінії, відділяючої заголовок від тексту, прийшлось провести три
паралельні прямі на відстані один піксель одна від одної.
Рис. 9.2.
Приклад
викорис
тання
класу
Graphics
Як
бачимо
із огляду
класу
Graphics
і
супутніх
йому
класів,
засоби
рисуван
ня и виведення тексту в цьому класі досить обмежені. Лінії можна проводити тільки суцільні і тільки
товщиною в один піксель, текст виводиться тільки горизонтально і зліва направо, не враховуються
особливості пристроїв виведення, наприклад, розрішимість екрана. Ці обмеження можна обійти різними
хитрощами: креслити декілька паралельних ліній, притиснутих одна до одної, як в лістинзі 9.2, або
вузький заповнений прямокутник, виводити текст по одній літері, одердати розрішимість екрана методом
getScreenSize() класу Java.awt.Toolkit і використовувати його надалі. Але все це утруднює програмування,
лишає його стрункості і природності, порушує принцип KISS. В Java 2 клас Graphics, в рамках системи
Java 2D, значно розширений класом Graphics2D.
9.5. Можливості Java 2D
В систему пакетів і класів Java 2D, основа якої— клас Graphics2D пакета java.awt, внесено декілька
принципово нових положень.
• Крім координатної системи, прийнятої в класі Graphics і названої координатним простором
користувача (User Space), введена ще система координат пристрою виведення (Device Space):
екрана монітора, принтера. Методи класу Graphics2D автоматично переводять (transform) систему
координат користувача в систему координат пристрою при виведенні графіки.
• Перетворення координат користувача в координати пристроюа можна задати "вручну", причому
перетворенням може служити будь-яке афіне перетворення площини, зокрема, поворот на
довільний кут і/або стиснення/розтягнення. Воно визначається як обєкт класу AffineTransform. Його
можна установити як перетворення по замовчуванню методом setTransform(). Можливо виконати
перетворення "на льоту" методами transform() і translate() і робити композицію перетворень
методом concatenate().
• Оскільки аффінне перетворення дійсне, координаты задаються дійсними, а не цілими числами.
• Графічні примітивb: прямокутник, овал, дуга і ін., реалізують тепер новий інтерфейс shape пакету
137
138. java.awt. Для їх викреслювання можна використовувати новий єдиний для всіх фігур метод draw(),
аргументом кякого може бути будь-який обєкт, реалізувавший інтерфейс shape. Введено метод
fill(), заповнюючий фігури— обєкти класу, реалізувавшого інтерфейс shape.
• Для викреслювання (stroke) ліній введено поняття пера (реn). Властивості пера описує інтерфейс
stroke. Клас Basicstroke реалізує цей інтерфейс. Перо володіє чотирма характеристиками:
• воно має толщину (width) в один (по замовчуванню) або декілька пікселів;
• воно може закінчити лінію (end cap) закругленням — статична константа CAP_ROUND, прямим
обрізом — CAP_SQUARE (по замовчуванню), або не фиксувати певний спосіб закінчення —
CAP_BUTT;
• воно може спрягати лінії (line joins) закругленням — статична константа JOIN_ROUND, відрізком
прямої — JOIN_BEVEL, або просто зістикувати — JOIN_MITER (по замовчуванню);
• воно може креслити лінію різними пунктирами (dash) і штрих-пунктирами, довжини штрихів і
проміжків задаються в масиві, елементи масиву з парними індексами задають довжину штриха, з
непарними індексами — довжину проміжку між штрихами.
• Методи заповнення фігур описані в інтерфейсі Paint. Три класи реалізують цей інтерфейс. Клас
color реалізує його суцільною (solid) заливкою, класс GradientPaint — градієнтним (gradient)
заповненням, при якому колір плавно змінюєтья від однієї заданої точки до другої заданої точки,
класс Texturepaint — заповненням по попередньо заданному зразку (pattern fill).
• Літери тексту розуміються як фігуры, тобто обєкти, реалізуючі інтерфейс shape, і можуть
викреслюватися методом draw() з використанням всіх можливостей цього методу. При їх
викреслюванні використовується перо, всі методи заповнення і перетворення.
• Крім імені, стилю і розміру, шрифт отримав багато додаткових атрибутів, наприклад,
перетворення координат, підкреслювання або закреслювання тексту, виведення тексту справа
наліво. Колір тексту і його фону являються тепер атрибутами самого тексту, а не графічного
контексту. Можна задати різну ширину символів шрифту, надрядкові і підрядкові індекси. Атрибути
установлюються константами класу TextAttribute.
• Процес візуалізації (rendering) регулюється правилами (hints), визначеними константами класу
RenderingHints.
З такими можливостями Java 2D стала повноцінною системою рисування, виведення тексту і зображень.
Подивимося, як реалізовані ці можливості, і як ними можна скористатися.
9.6. Перетворення координат
Правило перетворення координат користувача в координати графічного пристрою (transform) задається
автоматично при створенні графічного контексту так же, як колір і шрифт. Надалі його можна змінити
методом setTransform() так же, як змінюються колір або шрифт. Аргументом цього методу служить обєкт
класу AffineTransform із пакету java.awt.geom, подібно обєктам класу Сolor або Font при заданні кольору
або шрифту. Ррозглянемо детальніше клас AffineTransform.
Афінне перетворення координат задається двома основними конструкторами класу AffineTransform:
AffineTransform(double a, double b, double с, double d, double e, double f)
AffineTransform (float a, float b, float c, float d, float e, float f)
При цьому точка з координатами (х, у) в просторі користувача перейде в точку з координатами (а*х+с*у+е,
b*x+d*y+f) в просторі графічного пристрою. Таке перетворення не викривляє площину — прямі лінії
переходять в прямі, кути мж лініями зберігаються. Прикладами афінних перетворень служать повороти
навколо будь-якої точки на будь-який кут, паралельні зcуви, відображення від осей, стиснення і
розтягнення по вісям.
Наступні два конструктори використовують в якості аргумента масив {а, b, с, d, e, f} або {а, b, с, d}, якщо
e = f = 0, складений із таких же коефіцієнтів в тому ж порядку:
AffineTransform(double[] arr)
AffineTransform(float[] arr)
Пятий конструктор створює новий обєкт по іншому, уже наявному, обєкту:
138
139. AffineTransform(AffineTransform at)
Шостий конструктор — конструктор по замовчуванню — створює тотожне перетворення:
AffineTransform ()
Ці конструктори математично точні, але незручні при заданні конкретних перетворень. Попробуйте
розрахувати коефіцієнти повороту на 57° навколо точки з координатами (20, 40) або догадатися, як буде
перетворено простір користувача після виконання методів:
AffineTransform at = new AffineTransform(-1.5, 4.45, -0.56, 34.7, 2.68, 0.01);
g.setTransform(at);
В багатьох випадках зручніше створювати перетворення статичними методами, повертаючими обєкт
класу AffineTransform.
getRotatelnstance (double angle) — повертає поворот на кут angle, заданий в радіанах, навколо початку
координат. Додатній напрям повороту такий, що точки вісі Ох повертаються в напряму до вісі Оу. Якщо
вісі координат користувача не змінювалися перетворенням відображення, то додатнє значення angle
задає поворот по годинниковій стрілці.
• getRotatelnstance(double angle, double x, double у) — такий же поворот навколо точки з
координатами (х, у).
• getScalelnstance (double sx, double sy) — змінює маштаб по вісі Ох в sx раз, по вісі Оу — в sy раз.
• getSharelnstance (double shx, double shy) — перетворює кожну точку (x, у) в точку (x + shx*y, y +
shy*x ).
• getTranslatelnstance (double tx, double ty) — здвигає кожну точку (х, у) в точку (x + tx, y + ty).
Метод createІnverse() повертає перетворення, обернене поточному перетворенню. Після створення
перетворення його можна змінювати методами:
• setTransform(AffineTransform at)
• setTransform(double a, double b, double c, double d, double e, double f)
• setToIdentity()
• setToRotation(double angle)
• setToRotation(double angle, double x, double y)
• setToScale(double sx, double sy)
• setToShare(double shx, double shy)
• setToTranslate(double tx, double ty)
зробивши поточним перетворення, задане одним із цих методів. Перетворення, задані методами:
• concatenate(AffineTransform at)
• rotate(double angle)
• rotate(double angle, double x, double y)
• scale(double sx, double sy)
• shear(double shx, double shy)
• translate(double tx, double ty)
виконується перед поточним перетворенням, створюючи композицію перетворень.
Перетворення, задане методом preconcatenate{AffineTransform at), навпаки, здійснюються післе поточного
перетворення. Інші методи класу AffineTransform виконують перетворення різних фігур в просторі
користувача. Пора привести приклад. Додамо в початок методу paint() в лістинзі 9.2 чотири оператори, як
записано в лістинзі 9.3.
Лiстинг 9.3. Перетворення простору користувача
import java.awt.*;
139
140. import java.awt.event.*;
import java.awt.geom.*;
class GraphTest extends Frame{
GraphTest(String s){
super(s);
}
public void paint(Graphics gr){
Graphics2D g = (Graphics2D)gr;
AffineTransform at = AffineTransform.getRotateInstance(-Math.PI/4.0, 250.0,150.0);
at.concatenate(new AffineTransform(0.5, 0.0, 0.0, 0.5, 100.0, 60.0));
g.setTransform(at);
Dimension d = getSize();
int dx = d.width / 20, dy = d.height / 20;
g.drawRect(dx, dy + 20, d.width - 2*dx, d.height - 2*dy - 20);
g.drawRoundRect(2 * dx, 2*dy + 20, d.width - 4*dx, d.height - 4*dy - 20, dx, dy);
g.fillArc(d.width / 2 - dx, d.height - 2*dy + 1, 2*dx, dy - 1, 0, 360);
g.drawArc(d.width / 2 - 3*dx, d.height - 3*dy / 2 - 5, dx, dy / 2, 0, 360);
g.drawArc(d.width / 2 + 2*dx, d.height - 3*dy / 2 - 5, dx, dy / 2, 0, 360);
Font f1 = new Font("Serif", Font.BOLD|Font.ITALIC, 2 * dy);
Font f2 = new Font ("Serif", Font.BOLD, 5 * dy / 2);
FontMetrics fm1 = getFontMetrics(f1);
FontMetrics fm2 = getFontMetrics(f2);
String s1 = "Всяка останя помилка";
String s2 = "являється передостанньою.";
String s3 = "Закон налаштування програми";
int firstLine = d.height / 3;
int nextLine = fm1.getHeight();
int secondLine = firstLine + nextLine / 2;
g.setFont(f2);
g.drawString(s3, (d.width-fm2.stringWidth(s3)) / 2, firstLine);
g.drawLine(d.width / 4, secondLine - 2,3 * d.width / 4, secondLine - 2);
g.drawLine(d.width / 4, secondLine - 1, 3 * d.width / 4, secondLine - 1);
g.drawLine(d.width / 4, secondLine, 3 * d.width / 4, secondLine);
g.setFont(f1);
g.drawString(s1, (d.width - fm1.stringWidth(s1)) /2, firstLine + 2 * nextLine);
g.drawString(s2, (d.width - fm1.stringWidth(s2)) / 2, firstLine + 3 * nextLine);
}
public static void main(String[] args){
GraphTest f = new GraphTest(" Приклад рисування");
f.setBounds(0, 0, 500, 300);
f.setVisible(true);
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Метод paint () починається з отримання екземпляру g класу Graphics2D простим приведенням аргументу
gr до типу Graphics2D. Потім, методом getRotatelnstance() визначаються поворот на 45° проти
годинникової стрілки навколо точки (250.0, 150.0). Це перетворення — экземпляр at класу AffineTransform.
Метод concatenate(), виконуваний обєктом at, додає до цього перетворення стиснення в два рази по обом
вісям координат і перенос початку координат в точку (100.0, 60.0). Нарешті, композиція цих перетворень
установлюється як поточне перетворення обєкта g методом setTransform(). Перетворення виконується в
наступному порядку. Спочатку простір користувача стискується в два рази вподовж обох вісей, потім
початок координат користувача — лівий верхній кут — переноситься в точку (100.0, 60.0) простору
графічного пристрою. Потім картинка повертається на кут 45° проти часової стрілки навколо точки (250.0,
150.0). Результат цих перетворень показаний на рис. 9.3.
140
141. Рис. 9.3. Перетворення координат
9.7. Рисування фігур засобами Java2D
Характеристики пера для рисування фігур описані в інтерфейсі stroke. В Java 2D єсть поки що тільки один
клас, реалізуючий цей інтерфейс — клас BasicStroke.
9.7.1. Класс BasicStroke
Конструктори класу BasicStroke визначають характеристики пера. Основний конструктор
BasicStroke(float width, int cap, int join, float miter, float[] dash, float dashBegin)
задає:
• товщину пера width в пікселях;
• оформлення кінця лінії cap; це одна із констант:
• CAP_ROUND — закруглений кінець лінії;
• CAP_SQUARE — квадратний кінець лінії;
• CAP_BUTT — оформлення відсутнє;
• спосіб спряження ліній join; це одна із констант:
• JOIN_ROUND — лінії спрягаються дугою кола;
• JOIN_BEVEL — лінії спрягаються відрізком прямої, перпендикулярним биісектрисі кута між
лініями;
• JOIN_MITER — лінії просто стикуються;
• відстань між лініями miter, починаючи з якої застосовується спряження JOIN_MITER;
• довжину штрихів і проміжків міжу штрихами — масив dash; елементи масиву з парними індексами
задають довжину штриха в пікселях, елементи з непарними індексами — довжину проміжка; масив
перебирається циклічно;
• індекс dashBegin, починаючи з якого перебираютсья елементи масиву dash.
Решта конструкторів задають деякі характеристики по замовчуванню:
• BasicStroke (float width, int cap, int join, float miter) — суцільна лінія;
• BasicStroke (float width, int cap, int join) — суцільна лінія із спряженням JOIN_ROUND або
JOIN_BEVEL; для спряження JOIN_MITER задається значення miter = 10.0f;
141
142. • BasicStroke (float width) — прямий обріз CAP_SQUARE і спряження JOIN_MITER із значенням miter
= 10.0f;
• BasicStroke () — ширина1. 0f.
Краще один раз побачити, ніж сто раз прочитати. В лістинзі 9.4 визначено пять пер з різними
характеристиками, рис. 9.4 показує, як вони рисують.
Лістинг 9.4. Визначення пер
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
class StrokeTest extends Frame{
StrokeTest(String s) {
super (s) ;
setSize(500, 400);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
public void paint(Graphics gr){
Graphics2D g = (Graphics2D)gr;
g.setFont(new Font("Serif", Font.PLAIN, 15));
BasicStroke pen1 = new BasicStroke(20,
BasicStroke.CAP_BUTT,BasicStroke.JOIN_MITER,30);
BasicStroke pen2 = new BasicStroke(20, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
BasicStroke рen3 = new BasicStroke(20, BasicStroke.CAP_SQUARE,
BasicStroke.JOIN_BEVEL);
float[] dash1 = {5, 20};
BasicStroke pen4 = new BasicStroke(10,
BasicStroke.CAP_ROUND,BasicStroke.JOIN_BEVEL, 10, dash1, 0);
float[] dash2 = {10, 5, 5, 5};
BasicStroke pen5 = new BasicStroke(10, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_BEVEL, 10, dash2, 0);
g.setStroke(pen1);
g.draw(new Rectangle2D.Double(50, 50, 50, 50));
g.draw(new Line2D.Double(50, 180, 150, 180));
g.setStroke(pen2);
g.draw(new Rectangle2D.Double(200, 50, 50, 50));
g.draw(new Line2D.Double(50, 230, 150, 230));
g.setStroke(рen3);
g.draw(new Rectangle2D.Double(350, 50, 50, 50));
g.draw(new Line2D.Double(50, 280, 150, 280));
g.drawString("JOIN_MITER", 40, 130);
g.drawString("JOIN_ROUND", 180, 130);
g.drawString("JOINJBEVEL", 330, 130);
g.drawString("CAP_BUTT", 170, 190);
g.drawString("CAP_ROUND", 170, 240);
g.drawString("CAP_SQUARE", 170, 290);
g.setStroke(pen5);
g.draw(new Line2D.Double(50, 330, 250, 330));
g.setStroke(pen4);
g.draw(new Line2D.Double(50, 360, 250, 360));
g.drawString("{10, 5, 5, 5,...}", 260, 335);
g.drawString("(5, 10,...}", 260, 365);
142
143. }
public static void main(String[] args){
new StrokeTest("Мої пера");
}
}
Після створення пера одним із конструкторов і установки пера методом setStroke() можна рисувати різні
фігури методами draw() і fill(). Загальні вастивості фігур, які можна нарисувати методом draw() класу
Graphics2D, описані в інтерфейсі shape. Цей інтерфейс реалізований для сстворення звичайного набору
фігур —прямокутников, прямих, эліпсів, дуг, точок — класами Rectangle2D, RoundRectangle2D, Line2D,
Ellipse2D, Arc2D, Point2D пакету java.awt.geom. В цьому пакеті єсть ще класи CubicСurve2D і
QuadCurve2D для створення кривих третього і другого порядку. Всі ці класи абстрактні, але існують їх
реалізації — вкладені класи Double і Float для задання координат числами відповідного типа. В Лістинзі
9.4 використані класи Rectangle2D.Double і Line2d.Double для викреслювання прямокутників і відрізків.
9.7.2. Клас GeneralPath
В пакеті java.awt.geom єсть ще один цікавий клас — GeneralPath. Обєкти цього класу можуть містити
складні конструкції, складені із відрізків прямых або кривих ліній і інших фігур, або не зєднаних між
собою. Більше того, оскільки цей клас реалізує інтерфейс shape, його екземпляри самі являються
фігурами і можуть бути елементами інших обєктів класу GeneralPath.
Спочатку створюється пустий обєкт класуа GeneralPath конструктором по замовчуванню GeneralPath()
або обєкт, що містить одну фігуру, конструктором GeneralPath(Shape sh). Потім до цього объекта
додаються фігуры методом append(Shape sh, boolean connect). Якщо параметр connect рівний true, то
нова фігура зєднується з попередніми фігурами за допомогою поточного пера. В обєкті єсть поточна
точка. Спочатку її координати (0, 0), потім її можна перемістити в точку (х, у) методом moveTo(float x,float
у).
Від поточної точки до точки (х, у) можна провести:
• відрізок прямої методом lineTo(float x, float у);
• відрізок квадратичної кривої методом quadTot (float x1, float y1, float x, float y),
• криву Безье методом curveTo(float x1, float y1, float x2, float y2, float x, float y).
Поточною точкою після цього становитсья точка (х, у). Початкову і кінцеву точки можна зєднати методом
cІosePath(). Ось як можна створити трикутник із заданими вершинами:
GeneralPath p = new GeneralPath();
p.moveTo(xl, yl); // Переносимо поточну точку в першу вершину,
p.lineTo(x2, y2); // проводимо сторону трикутника до другої вершини,
p.lineTo(x3, уЗ); // проводимо другу сторону,
p.closePathf); // проводимо третю сторону до першої вершини
Способи заповнення фігур визначені в інтерфейсі Paint. Зараз Java 2D містить три реалізації цього
інтерфейса — класи Сolor, GradientPaint і TexturePaint. Клас Color нам відомий, подивимось, які способи
заливки пропонують класи GradientPaint і TexturePaint.
143
144. Рис. 9.4. Пера з різними характеристиками
9.7.3. Класи GradientPaint і TexturePaint
Клас GradientPaint пропонує зробити заливку наступним чином. В двох точках М і N устанавлюються різні
кольори. В точці M(x1, y1) задається колір c1, в точці N(х2, у2) — колір с2. Колір заливки гладко
змінюється від с1 до с2 вподовж прямої, що зєднує точки М і N, залишаючись постійним вподовж кожної
прямої, перпендикулярної прямій МN. Таку заливку створює конструктор
GradientPaint(float x1, float y1, Color c1, float x2, float y2, Color c2)
При цьому зовні відрізка МN колір залишається постійним: за точкою М - колір c1, за точкою N - колір с2.
Другий конструктор
GradientPaint(float xl, float yl, Color cl, float x2, float y2, Color c2, boolean cyclic)
при заданні параметра cyclic == true повторює заливку смуги МN у всій фігурі. Ще два конструктори
задають точки як обєкти класу Point2D.
Класс TexturePaint поступє складніше. Спочатку створюється буфер — обєкт класу Bufferedlmage із
пакету java.awt.image. Це великий складний клас. Ми з ним ще зустрінемося в уроці 15, а поки що нам
знадобиться тільки його графічний контекст, що управляється екземпляром класу Graphics2D. Цей
екземпляр можна отримати методом createGraphics() класу Bufferedlmage. Графічний контекст буферу
заповнюється фигурою, яка буде служить зразком заповнення. Потім по буферу створюється обєкт класу
TexturePaint. При цьому ще задається прямокутник, розміри якого будуть розмірами зразка заповнення.
Конструктор виглядає так:
TexturePaint(Bufferedlmage buffer, Rectangle2D anchor)
Після створення заливки — обєкта класу Сolor, GradientPaint або TexturePaint — вона установлюється в
144
145. графічному контексті методом setPaint (Paint p) і використовується надалі методом fill (Shape sh). Все це
демонструє лістинг 9.5 і рис. 9.5.
Лістинг 9.5. Способи заливки
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.awt.event.*;
class PaintTest extends Frame{ PaintTest(String s){ super(s) ;
setSize(300, 300);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){ System.exit(0); } });
}
public void paint(Graphics gr){
Graphics2D g = (Graphics2D)gr;
BufferedImage bi = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
Graphics2D big = bi.createGraphics();
big.draw(new Line2D.Double(0.0, 0.0, 10.0, 10.0));
big.draw(new Line2D.Double(0.0, 10.0, 10.0, 0.0));
TexturePaint tp = new TexturePaint(bi,
new Rectangle2D.Double(0.0, 0.0, 10.0, 10.0));
g.setPaint(tp);
g.fill(new Rectangle2D. Double (50, 50, 200, 200));
GradientPaint gp = new GradientPaint(100, 100, Color.white,
150, 150, Color.black, true); g.setPaint(gp);
g.fill(new Ellipse2D.Double (100, 100, 200, 200));
}
public static void main(String[] args){
new PaintTest(" Способи заливки");
}
}
Рис. 9.5. Способи заливки
145
146. 9.8. Виведення тексту засобами Java 2D
Шрифт — обєкт класу Font — крім імені, стилю і розміру має ще півтора десятка атрибутів:
підкреслювання, закреслювання, нахили, колір шрифту і колір фону, ширину і товщину символів, аффінне
перетворення, розташування зліва направо або справа наліво. Атрибути шрифту задаються як статичні
константи класу TextAttribute. Найбільш використовувані атрибути перечислені в табл. 9.1.
Таблиця 9.1. Атрибути шрифту
Атрибут Значення
BACKGROUND Колір фону. Обєкт, реалізуючий інтерфейс Paint
FOREGROUND Колір тексту. Обєкт, реалізуючий інтерфейс Paint
BIDI EMBEDDED Рівень вкладеності перегляду тексту. Ціле від 1 до 1 5
CHAR_REPLACEMENT Фігура, що заміняє символ. Обєкт GraphicAttribute
FAMILY Сімейство шрифта. Рядок типу string
FONT Шрифт. Обєкт класу Font
JUSTIFICATION Допуск при вирівнюванні абзаца. Обєкт класу Float із значеннями від 0,0 до 1,0.
Єсть дві константи: JUSTIFICATION FULL і JUSTIFICATION NONE
POSTURE Нахил шрифту. Обєкт класу Float. Єсть дві константи:
POSTURE_OBLIQUE і POSTURE_REGULAR
RUNJHRECTION Перегляд тексту: RUN DIRECTION LTR — зліва направо, RUN DIRECTION RTL
— справа наліво
SIZE Розмір шрифта в пунктах. Обєкт класу Float
STRIKETHROUGH Перекреслювання шрифта. Задається константою STRIKETHROUGH ON, по
замовчуванню перекреслювання немає
SUPERSCRIPT Нижні або верхні індекси. Константи: SUPERSCRIPT_NONE,
SUPERSCRIPT_SUB, SUPERSCRIPT_SUPER
SWAP COLORS Переміна місцями кольору тексті і кольору фону. Константа SWAP COLORS
ON, по замовчуванню заміни немає
TRANSFORM Перетворення шрифту. Обєкт класу AffineTransform
UNDERLINE Підкреслювання шрифту. Константи: UNDERLINE_ON,
UNDERLINE_LOW_DASHED, UNDERLINE_LOW_DOTTED, UNDERLINE LOW
GRAY, UNDERLINE LOW ONE PIXEL, UNDERLINE_LOW_TWO_PIXEL
WEIGHT Товщина шрифту. Константи: WEIGHT ULTRA LIGHT, WEIGHT _ EXTRA_LIGHT,
WEIGHT _ LIGHT, WEIGHT _ DEMILIGHT, WEIGHT _ REGULAR, WEIGHT _
SEMIBOLD, WEIGHT MEDIUM, WEIGHT DEMIBOLD, WEIGHT _ BOLD, WEIGHT
HEAVY, WEIGHT _ EXTRABOLD, WEIGHT _ ULTRABOLD
146
147. WIDTH Ширина шрифту. Константи: WIDTH CONDENSED,WIDTH SEMI CONDENSED,
WIDTH REGULAR, WIDTH_SEMI_EXTENDED, WIDTH_EXTENDED
На жаль, не всі шрифти дозволяют задавати всі атрибути. Побачити список допустимих атрибутів для
даного шрифта можна методом getAvailableAttributes() класу Font. В класі Font єсть конструктор Font(Map
attributes), яким можна зразу задати потрібні атрибути створюваному шрифту. Це вимагає попереднього
запису атрибутів у спеціально створений для цього обєкт класу, реалізуючий інтерфейс Map: класу
HashMap, WeakHashMap або Hashtable (див. урок 7). Наприклад:
HashMap hm = new HashMap ();
hm.put(TextAttribute.SIZE, new Float(60.Of));
hm.put(TextAttribute.POSTURE, TextAttribute.POSTUREJDBLIQUE);
Font f = new Font(hm);
Можна створити шрифт і другим конструктором, яким ми користувалися в лістинзі 9.2, а потім додавати і
змінювати атрибути методами deriveFont() класу Font. Текст в Java 2D має свій власний контекст — обєкт
класу FontRenderContext, що зберігає всю інформацію, необхідну для виведення тексту. Отримати його
можна методом getFontRendercontext() класу Graphics2D. Вся інформація про текст, в тому числі і про
його контекст, збирається в обєкті класу TextLayout. Цей класс в Java 2D заміняє клас FontMetrics. В
конструкторі класу TextLayout задається текст, шрифт і контекст. Початок методу paint() з усіма цими
визначеннями може виглядати так:
public void paint(Graphics gr){
Graphics2D g = (Graphics2D)gr;
FontRenderContext frc = g.getFontRenderContext();
Font f = new Font("Serif", Font.BOLD, 15);
String s = "Якийсь текст";
TextLayout tl = new TextLayout(s, f, frc) ; // Продовження методу }
В класі TextLayout єсть не тільки більше двадцати методів getxxxo, що дозволяють узнати різні дані про
текст, його шрифте і контекст, але і метод
draw(Graphics2D g, float x, float у)
що викреслює зміст обєкта класу TextLayout в графічній області g, починаючи з точки (х, у). Ще один
цікавий метод
getOutline(AffineTransform at)
повертаєт контур шрифту у вигляді обєкта shape. Цей контур можна потім заповнити по якому-небудь
зразку або вивести тільки контур, як показано в лістинзі 9.6.
Лістинг 9.6. Виведення тексту засобами Java 2D
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.event.*;
class StillText extends Frame{
StillText(String s) {
super(s);
setSize(400, 200);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0) ;
}
});
147
148. }
public void paint(Graphics gr){
Graphics2D g = (Graphics2D)gr;
int w = getSize().width, h = getSize().height;
FontRenderContext frc = g.getFontRenderContext();
String s = "Тінь";
Font f = new Font("Serif", Font.BOLD, h/3);
TextLayout tl = new TextLayout(s, f, frc);
AffineTransform at = new AffineTransform();
at.setToTranslation(w/2-tl.getBounds().getWidth()/2, h/2);
Shape sh = tl.getOutline(at);
g.draw(sh);
AffineTransform atsh = new AffineTransform(1, 0.0, 1.5, -1, 0.0, 0.0);
g.transform(at);
g.transform(atsh);
Font df = f.deriveFont(atsh);
TextLayout dtl = new TextLayout(s, df, frc);
Shape sh2 = dtl.getOutline(atsh);
g.fill(sh2); }
public static void main(String[] args) {
new StillText(" Ефект тіні");
}
}
На рис. 9.6 показано виведення цієї програми.
Рис. 9.6. Виведення тексту засобами Java 2D
Ще одна можливість створити текст з атрибутами — визначити обєкт класуа AttributedString із пакета java.
text. Конструктор цього класу
AttributedString(String text, Map attributes)
задає зразу і текст, і його атрибути. Потім можна додати або змінити характеристики тексту одним із трьох
методів addAttibute().
Якщо текст займає декілька рядків, то постає питання його форматування. Для цього замість класу
TextLayout використовується клас LineBreakMeasurer, методи якого дозволяють відформатувати абзац.
Для кожного сегмента тексту можна одержати екземпляр класу TextLayout і вивести текст,
використовуючи його атрибути. Для редагування тексту необхідно відслідкувати курсором (caret) поточну
позицію в тексті. Це здійснюється методами класу TextHitinfo, а методи класу TextLayout дозволяють
одержати позицію курсора, виділити блок тексту і підсвітити його. Нарешті, можна задати окремі правила
для виведення кожного символу текста. Для цього треба одержати екземпляр класу Glyphvector методом
148
149. createGІyphvector() класу Font, змінити позицію символу методом setСІiyphPosition(), задати перетворення
символу, якщо це допустимо для даного шрифта, методом setСІyphTransform(), і вивести змінений текст
методом drawGІyphVector() класу Graphics2D. Все це показано в лістинзі 9.7 і на рис. 9.7 — виведення
програми лістинга 9.7.
Лістинг 9.7. Виведення окремих символів
import j ava.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.event.*;
class GlyphTest extends Frame{ GlyphTest(String s){ super(s) ;
setSize(400, 150);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
public void paint(Graphics gr){
int h = 5;
Graphics2D g = (Graphics2D)gr;
FontRenderContext frc = g.getFontRenderContext();
Font f = new Font("Serif", Font.BOLD, 30);
GlyphVector gv = f.createGiyphvector(frc, "Танцюючий текст");
int len = gv.getNumGlyphs();
for (int i = 0; i < len; i++){
Point2D.Double p = new Point2D.Double(25 * i, h = -h);
gv.setGlyphPosition(i, p) ;
}
g.drawGiyphVector(gv, 10, 100); }
public static void main(String[] args)(
new GlyphTest(" Виведення окремих символів");
}
}
Рис. 9.7. Виведення окремих символів
9.9. Методи покращення візуалізації
Візуалізацію (rendering) створеної графіки можна удосконалити, установивши один із методів (hint)
покращення одним із методів класу Graphics2D:
setRenderingHints(RenderingHints.Key key, Object value)
setRenderingHints(Map hints)
Ключі — методи покращення — і їх значення задаються константами класу RenderingHints,
149
150. перечисленными в табл. 9.2.
Таблиця 9.2. Методи візуалізації і їх значення
Метод Значення
KEY_ANTIALIASING Розмивання крайніх пікселів ліній для гладкості зображення; три значення,
що задаються константами VALUE ANTIALIAS DEFAULT, VALUE
ANTIALIAS ON, VALUE~ANTIALIAS OFF
KEY_TEXT_ANTTALIAS ING Tе ж для тексту. Константи: VALUE TEXT ANTIALIASING_DEFAULT, VALUE
TEXT ANTIALIASING ON, VALUE_TEXT_ANTIALIASING_OFF
KEY RENDERING Три типи візуалізації. Константи: VALUE RENDER SPEED, VALUE RENDER
QUALITY, VALUE RENDER DEFAULT
KEY COLOR RENDERING Tе ж для кольору. Константи: VALUE COLOR RENDER_SPEED, VALUE
COLOR RENDER QUALITY, VALUE COLOR RENDER DEFAULT
KEY ALPHA
INTERPOLATION
Плавне спряження ліній. Константи: VALUE ALPHA
INTERPOLATION_SPEED, VALUE ALPHA INTERPOLATION QUALITY,
VALUE_ALPHA_INTERPOLATION_DEFAULT
KEY_INTERPOLATION Способи спряження. Константи: VALUE_INTERPOLATION_BILINEAR,
VALUE INTERPOLATION BICUBIC, VALUE
INTERPOLATION_NEAREST_NEIGHBOR
KEY_DITHERING Заміна близьких кольорів. Константи: VALUE DITHER ENABLE, VALUE
DITHER DISABLE, VALUE DITHER DEFAULT
He всі графічні системи забезпечують виконання цих методів, тому задання указаних атрибутив не
означає, що визначені ними методи будуть застосовуватися на самім ділі. Ось як може виглядати початок
метода paint() з указанням методів покращення візуалізації:
public void paint(Graphics gr){
Graphics2D g = (Graphics2D)gr;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
// Продовження методу
}
9.10. Заключення
В цьому уроці ми, розуміється, не змогли детально разібрати всі можливості Java 2D. Ми не торкнулись
моделів задання кольору і змішування кольорів, друку графіки і тексту, динамічного завантаження
шрифтів, зміни області рисування. В уроці 15 ми розглянемо засоби Java 2D для роботи із зображеннями,
в уроці 18 — засоби друку. В документації SUN J2SDK, в каталозі docsguide2dspec, єсть керівництво
Java 2 API Guide з оглядом всіх можливостей Java 2D. Там єсть посилання на посібники по Java 2D. В
каталозі demojfcJava2Dsrc приведени вихідні тексти програм, що використовують Java 2D.
Лабораторна робота 8. Робота з класом Graphics і Graphics2D
150
151. 1. Ви вже, без сумніву, випробували програму лістинга 9.2. Візьміть її за зразок і виведіть у вікно всі
фігури, перечислені в параграфі 9.1.2. Нижче кожної фігури виведіть її назву. Вибирайте для
кожної фігури свій колір. Для кожної назви виберіть свій шрифт, розмір, колір і стиль. Особливу
увагу зверніть на способи задання вершин полігону і ламаної. Ви повинні проілюструвати всі
можливі випадки.
2. Розберіться з програмою 9.5 і попробуйте залити пару фігур узорами відмінними від використаних
у цій програмі, в тому числі розгляньте і заливку суцільним кольором.
151
152. Програмування у Java
Урок 10. Основні компоненти
• Клас Component
• Клас Cursor
• Як створити свій курсор
• Події
• Клас Container
• Компонент Label
• Компонент Button
• Компонент Checkbox
• Клас CheckboxGroup
• Як створити групу радіокнопок
• Компонент Choice
• Компонент List
• Компоненти для введення тексту
• Клас TextComponent
• Компонент TextField
• Компонент TextArea
• Компонент ScrollBar
• Контейнер Panel
• Контейнер ScrollPane
• Контейнер Window
• Контейнер Frame
• Контейнер Dialog
• Контейнер FileDialog
• Створення власних компонентів
• Компонент Canvas
• Створення “легкого” компонента
Графічна бібліотека AWT пропонує більше двадцати готових компонентів. Вони показані на рис. 8.2.
Найбільш часто використовуються підкласи класу Component: класи Button, Canvas, Checkbox, Choice,
Container, Label, List, Scrollbar, TextArea, TextField, Panel, ScrollPane, Window, Dialog, FileDialog, Frame. Ще
одна група компонентів — це компоненти меню — класи Menuitem, MenuBar, Menu, PopupMenu,
CheckboxMenuItem. Ми розглянемо їх в уроці 13. Забігаючи наперед, для кожного компонента
перечислимо події, які в ньому відбуваються. Обробку подій ми розберемо в уроці 12. Почнемо вивчати ці
компоненти від простих компонентів до складних і від найбільш часто використовуваних до
використовуваних рідше. Але спочатку подивимося на те спільне, що єсть у всіх цих компонентах, а
саме клас Сomponent.
10.1. Класс Component
Клас Сomponent — центр бібліотеки AWT — дуже великий і володіє багатьма можливостями. В ньому
пять статичних констант, визначаючих розміщення компонента всередині простору, виділеного для
компонента у вміщаючому його контейнері: BOTTOM_ALIGNMENT, CENTER_ALIGNMENT,
LEFT_ALIGNMENT, RIGHT_ALIGNMENT, TOP_ALIGNMENT, і близько сотні методів. Більшість методів—
це методи доступу getxxx(), isxxx(), setxxx(). Вивчати їх немає рації, треба просто подивитися, як вони
використовуються в підкласах.
Конструктор класу недоступний — він захищений (protected), тому, що клас Сomponent абстрактний, він
не може використовуватися сам по собі, використовуються лише його підкласи. Компонент завжди займає
прямокутну область зі сторонами, паралельними сторонам екрана і в кожний момент часу має певні
розміри, вимірювані в пікселях, які можна узнати методом getSize(), повертаючим обєкт класу Dimension,
або цілочисельними методами getHeight() і getWidth(), повертаючими висоту і ширину прямокутника.
Новий розмір компонента можна установити із програми методами setSize(Dimension d) або setSize(int
width, int height), якщо це дозволяє менеджер розміщення контейнера, що містить компонент. У
152
153. компонента єсть оптимальний розмір, при якому він виглядає найбільш пропорціонально. Його можна
одержати методом getPreferredSize() у вигляді обєкта Dimension.
Компонент має мінимальний і максимальний розміри. Їх повертають методи getMinimumSize() і
getMaximumSize() у вигляді обєкта Dimension. В компоненті єсть система координат. Її початок - точка з
координатами (0, 0) - знаходиться в лівому верхньому куті компонента, вісь Ох іде вправо, вісь Оу - вниз,
координатні точки розташовані між пікселями. В компоненті зберігаються координати його лівого
верхнього кута в системі координат вміщаючого його контейнера. Їх можна узнати методами getLocation(),
а зменити — методами setLocation(), перемістивши компонент в контейнері, якщо це дозволить менеджер
розміщення компонентів.
Можна вияснити зразу і положення, і розмір прямокутної області компонента методом getBounds(),
повертаючим обєкт класу Rectangle, і змінити разом і положення, і розмір компонента методами
setBounds(), якщо це дозволить зробити менеджер розміщення. Компонент може бути недоступним для
дій користувача, тоді він виділяється на екрані світло-сірим кольором. Доступність компонента можна
перевірити логічним методом isEnabІed(), а змінити— методом setEnabled(boolean enable). Для багатьох
компонентів визначається графічний контекст — обєкт класу Graphics, — який керується методом paint(),
описаним в попередньому уроці, і який можна одержати методом getGraphics().
В контексті єсть поточний колір і колір фону — обєкти класу Сolor. Колір фону можна одержати методом
getBackground(), а змінити— методом setBackground(Color color). Поточний колір можна одержати
методом getForeground(), а змінити - методом setForeground(Color color). В контексті єсть шрифт — обєкт
класу Font, що повертається методом getFont() і змінюється методом setFont(Font font). В компоненті
визначається локаль — обєкт класу Locale. Його можна одержати методом getLocale(), змінити - методом
setLocale(Locale locale).
В компоненті існує курсор, що показує положення миші, — обєкт класу Cursor. Його можна одержати
методом getСursor(), змінюється форма курсора у "важких" компонентах за допомогою метода
setСursor(Cursor cursor). Зупинимося на цих класах детальніше.
10.2. Клас Cursor
Основа класу — статичні константи, що визначають форму курсора:
• CROSSHAIR_CURSOR — курсор у вигляді хреста, появляється при пошуку позиції для
розміщення якогось елемента;
• DEFAULT_CURSOR — звичайна форма курсора — стрілка вліво вверх;
• HAND_CURSOR — "указуючий перст", появляється при виборі якогось елемента списку;
• MOVE_CURSOR — хрест зі стрілками, появлється при переміщенні елемента;
• TEXT_CURSOR — вертикальна риска, появляється в текстових полях;
• WAIT_CURSOR — зображення годинника, появляється при очікуванні.
Наступні курсори появляються при наближенні до краю або кута компонента:
• E_RESIZE_CURSOR — стрілка вправо з упором;
• N_RESIZE_CURSOR — стрілка вверх с упором;
• NE_RESIZE_CURSOR — стрілка вправо вверх, упираюча в кут;
• NW_RESIZE_CURSOR — стрілка вліво вверх, упираюча в кут;
• S_RESIZE_CURSOR — стрілка вниз з упором;
• SE_RESIZE_CURSOR — стрілка вправо вниз, упираюча в кут;
• SW_RESIZE_CURSOR — стрілка вліво вниз, упираюча в кут;
• W_RESIZE_CURSOR — стрілка вліво з упором.
Перечислені константи являються аргументом type в конструкторі класу Cursor(int type). Замість
конструктора можна звернутися до статичного методу getPredefinedCursor(int type), створюючому обєкт
класу Cursor і повертаючому посилання на нього. Одержати курсор по замовчуванню можна статичним
методом getDefauІtcursor(). Потім створений курсор треба установити в компонент. Наприклад, після
виконання:
153
154. Cursor curs = new Cursor(Cursor.WAIT_CURSOR);
SomeComp.setCursor(curs);
при появі показчика миші в компоненті Somecomp показчик прийме вигляд годинника.
10.3. Як створити свій курсор
Крім цих наперед визначених курсорів можна задати свою власну форму курсора. Її тип носить назву
CUSTOM_CURSOR. Сформувати свій курсор можна методом
createCustomCursor(Image cursor, Point hotspot, String name)
створюючим обєкт класу Сursor і повертаючим посилання на нього. Перед цим належить створити
зображення курсора cursor — обєкт класу image. Як це зробити, розповідається в уроці 15. Аргумент name
задає імя курсору, можна написати просто null. Аргумент hotspot задає точку фокуса курсора. Ця точка
повинна бути в межах зображення курсора, точніше, в межах, показуваних методом
getBestCursorSize(int desiredWidth, int desiredHeight)
повертаючим посилання на обєкт класу Dimension. Аргументи методу означають бажаний розмір курсора.
Якщо графічна система не допускає створення курсорів, повертається (0,0). Цей метод показує
приблизно розмір того курсора, який створить графічна система, наприклад, (32, 32). Зображення cursor
буде підігнано під цей розмір, при цьому можливі спотворення.
Третій метод— getMaximumCursorColors() — повертає найбільшу кількісь кольорів, наприклад, 256, яку
можна використовувати в зображенні курсора. Це методи класу java.awt.Toolkit, з яким ми ще не
працювали. Клас Toolkit містить деякі методи, звязуючі додатки Java із засобами платформи, на якій
виконується додаток. Тому не можна створити екземпляр класу Toolkit конструктором, для його
одержання належить виконати статичний метод Toolkit.getDefaultToolkit(). Якщо додаток працює у вікні
Window або його розширеннях, наприклад, Frame, то можна одержати екземпляр Toolkit методом
getToolkit() класу Window.
Зберемо все це разом:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
class SimpleFrame extends Frame{
SimpleFrame(String s){
super (s);
setSize(400, 150);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit (0);
}
});
Toolkit tk = Toolkit.getDefaultToolkit();
int colorMax = tk.getMaximumCursorColors(); // Найбільше число кольорів
Dimension d = tk.getBestCursorSize(50, 50); // d — розмір зображення
int w = d.width, h = d.height, k = 0;
Point p = new Point(0, 0); // Фокус курсора буде в його верхньому лівому куті
int[] pix = new int[w * h]; // Тут будуть пікселі зображення
for(int i = 0; i < w; i++) {
for(int j = 0; j < h; j++)
if (j < i) pix[k++] = 0xFFFF0000; // Лівий нижній кут - червоний
else pix[k++] = 0; // Правий верхній кут — прозорий
}
// Створюється прямокутне зображення розміром (w, h),
154
155. // заповнене масивом пікселів pix, з довжиною рядка w
Image im = createImage(new MemoryImageSource(w, h, pix, 0, w));
Cursor curs = tk.createCustomCursor(im, p, null);
setCursor(curs);
}
public static void main(String[] args){
new SimpleFrame(" Мy program");
}
}
В цьому прикладі створюється курсор у вигляді червоного прямокутного трикутника з катетами розміром
32 пікселі і установлюється в якомусь компоненті someComp.
10.4. Події
• Подія ComponentEvent відбувається при переміщенні компонента, зміні його розміру, видаленні з
екрана і появі на єкрані.
• Подія FocusEvent відбувається при одержанні або втраті фокуса.
• Подія KeyEvent проявляється при кожному нтисканні і звільнення клавіші, якщо компонент має
фокус введення.
• Подія MouseEvent відбувається при маніпуляцяях миші на компоненті.
Кожний компонент перед виведенням на екран поміщається в контейнер — підклас класу Сontainer.
Познайомимося з цим класом.
10.5. Клас Container
Клас Сontainer — прямий підклас класу Сomponent, і наслідує всі його методи. Крім них основу класу
складають методи додавання компонентів у контейнер:
• add (Component comp) — компонент comp додається в кінець контейнера;
• add (Component comp, int index) — компонент comp додається в позицію index в контейнері, якщо
index == -1, то компонент додається в кінець контейнера;
• add (Component comp, object constraints) — менеджеру розміщення контейнера даються вказівки
обєктом Сonstraints;
• add (String name. Component comp) —компонент отримує імя name.
Два методи видаляють компоненти із контейнера:
• remove (Component comp) — видаляє компонент з іменем comp;
• remove (int index) — видаляє компонент з індексом index в контейнері.
Один із компонентів в контейнері отримує фокус вводу (input focus), на нього направляється введення з
клавіатури. Фокус можна переносити з одного компонента на інший клавішами <Tab> і <Shift>+<Tab>.
Компонент може запросити фокус методом requestFocus() і передати фокус наступному компоненту
методом transferFocus(). Компонент може перевірити, чи має він фокус, своїм логічним методом
hasFocus(f). Це методи класу Component.
Для полегшення розміщення компонентів в контейнері визначається менеджер розміщення (layout
155
156. manager) — обєкт, реалізуючий інтерфейс LayoutManager або його підінтерфейс LayoutManager2. Кожний
менеджер розміщує компоненти в якомусь своєму порядку: один менеджер розставляє компоненти в
таблицю, другий норовить розтягти компоненти по сторонах, третій просто розміщує їх один за другим, як
слова в тексті. Менеджер визначає зміст слів "додати в кінець контейнера" і "додати в позицію index". В
контейнері в будь-який момент часу може бути установлений тільки один менеджер розміщення. В
кожному контейнері єсть свій менеджер по замовчуванню, установка іншого менеджера виконується
методом setLayout(LayoutManager manager). Менеджери розміщення ми розглянемо детальніше в
наступному уроці. В даному уроці ми будемо розміщувати компоненти вручну, відключивши менеджер по
замовчуванню методом setLayout (null).
Події
Крім подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent при додаванні і
видаленні компонентів в контейнері відбувається подія ContainerEvent. Перейдемо до розгляду
конкретних компонентив. Самий простий компонент описує клас Label.
10.6. Компонент Label
Компонент Label — це просто рядок тексту, оформлений як графічний компонент для розміщення в
контейнері. Текст можна поміняти тільки методом доступу setText(String text), але не введенням
користувача з клавіатури або за допомогою миші. Створюється обєкт цього класу одним із трьох
конструкторів:
• Label () — пустий обєкт без тексту;
• Label (String text) — обєкт з текстом text, який притискується до лівого краю компонента;
• Label (String text, int alignment) — обєкт з текстом text і визначеним розміщенням в компоненті
тексту, задаваного однією із трьох констант: CENTER, LEFT, RIGHT.
Розміщення можна змінити методом доступу setAlignment(int alignment). Решта методів, крім методів,
унаслідуваних від класу Сomponent, дозволяють одержати текст getText() і розміщення getAlignment().
Події
В класі Label відбуваються події класів Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent.
Ненабагато складніший клас Button.
10.7. Компонент Button
Компонент Button — це кнопка стандартного для даної графічної системи вигляду з написом, здатна
реагувати на клік кнопки миші — при натискуванні вона "вдавлюється" в площину контейнера, при
відпусканні — становиться "випуклою". Два конструктори Button() і Button (String label) створюють кнопку
без напису і з написом label відповідно. Методи доступа getLabel() і setLabel (String label) дозволяють
одержати і змінити напис на кнопці. Головна функція кнопки — реагувати на клік миші, і інші методи класу
обробляють ці дії. Ми розглянемо їх в уроці 12.
Події
Крім подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при кліку на кнопку
відбувається подія ActionEvent. Трохи складніший за клас Label клас Сheckbox, створюючий кнопки
вибору.
10.8. Компонент Checkbox
Компонент Checkbox — це напис справа від невеликого квадратика, в якому в деяких графічних системах
появляється галочка після кліку кнопкою миші — компонент переходить в стан (state) on. Після наступного
кліку галочка пропадє — це стан off. В інших графічних системах стан on відмічається "вдавлюванням"
квадратика. В компоненті Сheckbox стани on/off відмічаються логічними значеннями true/false відповідно.
Три конструктори Checkbox (), Checkbox(String label), Checkbox(String label, boolean state) створюють
156
157. компонент без напису, з написом label в стані off, і в заданому стані state. Методи доступу getLabel(),
setLabel (String label), getState(), setState (boolean state) повертають і змінюють ці параметри компонента.
Компоненти Сheckbox зручні для швидкого і наочного вибору із списка, цілком розташованого на екрані,
як показано на рис. 10.1. Там же продемонстрована ситуація, в якій потрібно вибрати тільки один пункт із
декількох. В таких ситуаціях створюється група так званих радіокнопок (radio buttons). Вони помічаються
кружком або ромбиком, а не квадратиком, выбір позначається жирною точкою в кружку або
"вдавллюванням" ромбика.
Події
В класі Checkbox відбуваються події класу Component: ComponentEvent, FocusEvent, KeyEvent,
MouseEvent, а при зміні стану кнопки виникає подія ItemEvent. В бібліотеці AWT радіокнопки не створюють
окремий компонент. Замість цього декілька компонентів Сheckbox обєднуються в группу за допомогою
обєкту класу СheckboxGroup.
10.9. Клас CheckboxGroup
Клас CheckboxGroup дуже малий, оскільки його завдання — просто дати спільне імя всім обєктам
Сheckbox, утворюючим одну групу. В нього входить один конструктор по замовчуванню CheckboxGroup() і
два методи доступу:
• getSelectedCheckbox(), повертаючий вибраний обєкт Checkbox;
• setSelectedCheckbox (Checkbox box), задаючий вибір.
10.10. Як створити групу радіокнопок
Щоб організувати групу радіокнопок, треба спочатку сформувати обєкт класу CheckboxGroup, а потім
створити кнопки конструкторами
• Checkbox(String label, CheckboxGroup group, boolean state)
• Checkbox(String label, boolean state, CheckboxGroup group)
Ці конструктори ідентичні, просто при запису конструктора можна не думати про порядок слідування його
аргументів. Тільки одна радіокнопка в групі може мати стан state = true. Пора привести приклад. В лістинзі
10.1 приведена програма, поміщаюча в контейнер Frame дві мітки Label зверху, під ними зліва три обєкти
Сheckbox, справа — групу радіокнопок. Внизу — три кнопки Button. Результат виконанняя програми
показаний на рис. 10.1.
Лістинг 10.1. Розміщення компонентів
import java.awt.*;
import java.awt.event.*;
class SimpleComp extends Frame{
SimpleComp(String s){ super(s);
setLayout(null);
Font f = new Font("Serif", Font.BOLD, 15);
setFont(f);
Label L1 = new Label("Choose things:", Label.CENTER);
L1.setBounds(0, 50, 120, 30); add(L1);
Label L2 = new Label("Choose paying:");
L2.setBounds(160, 50, 200, 30); add(L2);
Checkbox ch1 = new Checkbox("Books");
ch1.setBounds(20, 90, 100, 30); add(ch1);
Checkbox ch2 = new Checkbox("Discs");
ch2.setBounds(20, 120, 100, 30); add(ch2);
Checkbox ch3 = new Checkbox("Tois");
ch3.setBounds(20, 150, 100, 30); add(ch3);
CheckboxGroup grp = new CheckboxGroup();
Checkbox chg1 = new Checkbox("Mail", grp,true);
157
158. chg1.setBounds(170, 90, 200, 30); add(chg1);
Checkbox chg2 = new Checkbox("Credit Card", grp, false);
chg2.setBounds(170, 120, 200, 30); add(chg2);
Button b1 = new Button("Continue");
b1.setBounds( 30, 220, 100, 30); add(b1);
Button b2 = new Button("Cancel");
b2.setBounds(140, 220, 100, 30); add(b2);
Button b3 = new Button("Exit");
b3.setBounds(250, 220, 100, 30); add(b3);
setSize(400, 300);
setVisible(true);
}
public static void main(String[] args){
Frame f = new SimpleComp (" Прості компоненти");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 10.1. Прості компоненти
Ми не програмували реакції на натискання кнопок, тому такі натискання не мають жодного ефекту.
Відмітьте, що кожний створюваний компонент належить заносити в контейнер, в даному випадку Frame,
методом add(). Лівий верхній кут компонента поміщається в точку контейнера з координатами, указаними
першими двома аргументами методу setBounds(). Розмір компонента задається останніми двома
параметрами цього методу. Якщо немає необхідності відображати весь список на екрані, то замість групи
радіокнопок можна створити розкриваючийся список — обєкт класу Choice.
10.11. Компонент Choice
Компонент Сhoice — це розкриваючийся список, один, вибраний, пункт (item) якого видимий в полі, а інші
появляються при кліку кнопкою миші на невелику кнопку справа від поля компонента. Спочатку
конструктором Choice() створюється пустий список. Потім, методом add (String text), в список додаються
нові пункти з текстом text. Вони розміщуються в порядку написання методов add() і нумеруються від нуля.
Вставить новий пункт в потрібне місце можна методом insert (String text, int position). Вибір пункту можна
158
159. зробити із програми методом select (String text) або select(int position). Видалити один пункт із списку
можна методом remove(String text) або remove (int position), а всі пункти зразу — методом remove(). Число
пунктів у списку можна узнати методом getІtemCount(). Вияснити, який пункт знаходиться в позиції pos
можна методом getІtem(int pos), повертаючим рядок. Нарешті, визначення вибраного пункту виконується
методом getSelectedIndex(), повертаючим позицію цього пункту, або методом getSelectedItem(),
повертаючим виділений рядок.
Події
В класі Choice відбуваються події класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent,
а при виборі пункта відбувається подія ItemEvent. Якщо треба показати на екрані декілька пунктів списку,
то створіть обєкт класу List.
10.12. Компонент List
Компонент List — це список із смугою прокрутки, в якому можна виділить один або декілька пунктів.
Кількість видимих на екрані пунктів визначається конструктором списку і розміром компонента. В класі три
конструктори:
• List() — створює пустий список з чотирма видимими пунктами;
• List (int rows) — створює пустий список з rows видимими пунктами;
• List (int rows, boolean multiple) — створює пустий список в якому можна відмітити декілька пунктів,
якщо multiple == true.
•
Після створення обєкта в список додаються пункти з текстом item:
• метод add (String item) — додає новий пункт в кінець списку;
• метод add (String item, int position) — додає новий пункт в позицію position.
Позиції нумеруются по порядку, починаючи з нуля. Видалити пункт можна методами
remove (String item), remove (int position), removeAll ().
Метод repІaceІtem(String newitem, int pos) дозволяє замінити текст пункта в позиції pos. Кількість пунктів у
списку повертає метод getІtemСount(). Виділений пункт можна отримати методом getSelectedItem(), а його
позицію — методом getSelectedlndex(). Якщо список дозволяє здійснювати множинний вибір, то виділені
пункти у вигляді масиву типу String[] можнa одержати методом getSelectedІtems(), позиції виділених
пунктів у вигляді масиву типу int[] — методом getSelectedlndexes(). Крім цих необхідних методів клас List
містить багато інших, дозволяючих маніпулювати пунктами списку і отримувати його характеристики.
Події
Крім подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при подвійному кліку
кнопкою миші на вибраному пункті відбувається подія ActionEvent. В лістинзі 10.2 задаютсья компоненти,
аналогічні компонентам лістинга 10.1, за допомогою класів Сhoice і List, а рис. 10.2 показує, як зміниться
при цьому інтерфейс.
Лістинг 10.2. Використання списків
import java.awt.*;
import java.awt.event.*;
class ListTest extends Frame{
ListTest(String s){ super(s);
setLayout(null);
setFont(new Font("Serif", Font.BOLD, 15));
Label l1 = new Label("Choice things:", Label.CENTER);
l1.setBounds(0, 50, 120, 30); add (l1);
Label l2 = new Label("Choice paying mode:");
l2.setBounds(170, 50, 200, 30); add(l2);
159
160. List l = new List(2, true);
l.add("Books");
l.add("Discs");
l.add("Tois");
l.setBounds(20, 90, 100, 40); add(l);
Choice ch = new Choice();
ch.add("By mail transfer");
ch.add("By credit card");
ch.setBounds(170, 90, 200,30); add(ch);
Button b1 = new Button("Continue");
b1.setBounds( 30, 150, 100, 30); add(b1);
Button b2 = new Button("Cancel");
b2.setBounds(140, 150, 100, 30); add(b2);
Button b3 = new Button("Exit");
b3.setBounds(250, 150, 100, 30); add(b3);
setSize(400, 200); setVisible(true);
}
public static void main(String[] args){
Frame f = new ListTest("Прості компоненти");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
G
Рис. 10.2. Використання списків
10.13. Компоненти для введення тексту
В бібліотеці AWT єсть два компоненти для введення тексту з клавіатури: TextField, дозволяючий ввести
тількb один рядок, і TextArea, в якому можна ввести декілька рядків. Обидва класи росширюють клас
TextСomponent, в якому зібрані їх спільні методи, такі як виділення тексту, позиціювання курсора,
одержання тексту.
10.13.1. Клас TextComponent
В класі TextComponent немає конструктора, цей клас не використовується самостійно. Основний метод
класу — метод getText() — повертає текст, що знаходиться в полі введення, у вигляді рядка. Поле
введення може бути нередагованим, в цьому стані текст в полі не можна змінити з клавіатури або
мишкою. Узнати стан поля можна логічним методом isEditabІe(), змінити значення в ньому — методом
setEditable(boolean editable). Текст, що знаходиться в полі, зберігається як обєкт класу String, тому у
кожного символу єсть індекс (у першого — індекс 0). Індекс використовується для визначення позиції
160
161. курсора (caret) методом getCaretPosition(), для установки позиції курсора методом setСaretРosition(int ind)
і для виділення тексту. Текст виділяється мишею або клавішами зі стрілками при натисканні клавіші
<Shift>, але можна виділити його із програми методом select(int begin, int end). При цьому помічається
текст від символа з індексом begin включно, до символа з індексом end виключно. Весь текст виділяє
метод selectAlІ(). Можна відмітити початок виділення методом setSelectionStart(int ind) і кінець виділення
методом setSelectionEnd(int ind). Важливіше все-таки не задати, а отримати виделений текст. Його
повертає метод getSeІectedText(), а початковий і кінцевий індекс виділення повертають методи
getSelectionStart() і getSelectionEnd().
Події
Крім подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні тексту
користувачем відбувається подія TextEvent.
10.13.2. Компонент TextField
Компонент TextField — це поле для введення одного рядка тексту. Ширина поля вимірюється в колонках
(column). Ширина колонки — це середня ширина символу в шрифті, яким вводиться текст. Натискування
клавіші <Enter> закінчує введення і служить сигналом до початку обробки введеного тексту, тобто при
цьому відбувається подія ActionEvent. В класі чотири конструктори:
• TextField() — створює пусте поле шириною в одну колонку;
• TextField(int columns) — створює пусте поле з числом колонок columns;
• TextField( String text) — створює поле з текстом text;
• TextField(String text, int columns) — створює поле з текстом text i числом колонок columns.
До методів, унаслідуваних від класу TextComponent, додаються ще методи getColumns() і setColumns(int
col). Цікава різновидність поля введення — поле для введення пароля. В такому полі замість введених
символів появляється який-небудь особливий eхо-символ, частіше всього зірочка, щоб пароль ніхто не
підглянув через плече. Дане поле введення одержується виконанням методу setEcnoCnar(char echo).
Аргумент echo — це символ, який буде появлятися в полі. Перевірити, чи установлений ехо-символ,
можна логічним методом echoCharisSet(), одержати ехо-символ — методом getEchoChar(). Щоб
првернути поле введення в звичайний стан, достатньо виконати метод setEchoChar(0).
Події
Крім подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні тексту
користувачем відбувається подія TextEvent, а при натисканні на клавішу <Enter> — подія ActionEvent.
10.13.3. Компонент TextArea
Компонент TextArea — це область введення з довільним числом рядків. Натискання клавіші <Enter>
просто переводить курсор в початок наступного рядка. В області введення можуть бути установлені смуги
прокрутки, одна або обидві. Основний конструктор класу
TextArea(String text, int rows, int columns, int scrollbars)
створює область введення з текстом text, числом видимих рядків rows, числом колонок columns, і
заданням смуг прокрутки scrollbars однієї із чотирьох констант: SCROLLBARS_NONE,
SCROLLBARS_HORIZONTAL_ONLY, SCROLLBARS_VERTICAL_ONLY, SCROLLBARS_BOTH. Решта
конструкторів задають деякі параметри по замовчуванню:
• TextArea (String text, int rows, int columns) — присутні обидві смуги прокрутки;
• TextArea (int rows, int columns) — в полі пустий рядок;
• TextArea (string text) — розміри устанавлює контейнер;
• TextArea () — конструктор по замовчуванню.
•
Серед методів класу TextArea найбільш важливі методи:
161
162. • append ( String text), додаючий текст text в кінець уже введеного тексту;
• insert ( String text, int pos), вставляючий текст в указану позицію pos;
• replaceRange (String text, int begin, int end), видаляючий текст починаючи з індекса begin включно
по end виключно, і поміщаючий замість нього текст text.
Інші методи дозволяють змінити і отримати кількість видимих рядків.
Події
Крім подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні тексту
користувачем відбувається подія TextEvent.
В лістинзі 10.3 створюються три поля: tf1, tf2, tf3 для введення імені користувача, його пароля і
замовлення, і не редагована область введення, в якій накопичується замовлення. В полі введення пароля
tf2 появляється eхо-символ *. Результат показаний на рис. 10.3.
Лістинг 10.3. Поля введення
import java.awt.*;
import java.awt.event.*;
class TextTest extends Frame{
TextTest(String s){
super(s);
setLayout(null);
setFont(new Font("Serif", Font.PLAIN, 14));
Label l1 = new Label("Your Name:", Label.RIGHT);
l1.setBounds(20, 30, 70, 25); add(l1);
Label l2 = new Label("Password:", Label.RIGHT);
l2.setBounds(20, 60, 70, 25); add(l2);
TextField tf1 = new TextField(30) ;
tf1.setBounds(100, 30, 160, 25); add(tf1);
TextField tf2 = new TextField(30);
tf2.setBounds(100, 60, 160, 25);
add(tf2); tf2.setEchoChar('*');
TextField tf3 = new TextField("Input here your request", 30);
tf3.setBounds(10, 100, 250, 30); add(tf3);
TextArea ta = new TextArea("Your request:", 5, 50,
TextArea.SCROLLBARS_NONE);
ta.setEditable(false);
ta.setBounds(10, 150, 250, 140); add(ta);
Button b1 = new Button("Apply");
b1.setBounds(280, 180, 100, 30); add(b1);
Button b2 = new Button("Cancel");
b2.setBounds(280, 220, 100, 30); add(b2);
Button b3 = new Button("Exit");
b3.setBounds(280, 260, 100, 30); add(b3);
setSize(400, 300); setVisible(true);
}
public static void main(String[] args){
Frame f = new TextTest(" Поля введення");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
162
163. Рис. 10.3. Поля
введення
10.14. Компонент
Scrollbar
Компонент Scrollbar
— це смуга
прокрутки, але в
бібліотеці AWT клас
Scrollbar
використовується
ще й для організації
повзунка (slider). Обєкт може розміщатися горизонтально або вертикально, зазвичай смуги прокрутки
розміщуються внизу і справа. Кожна смуга прокрутки охвачує деякий діапазон значень і зберігає поточне
значення із цього діапазону. В лінійці прокрутки єсть пять елементів управління для переміщення по
діапазону. Дві стрілки на кінцях лінійки викликають переміщення на одну одиницю (unit) у відповідному
напрямі при кліку на стрілку кнопкою миші. Положення движка або бігунка (bubble, thumb) показує поточне
значення із діапазону і може його змінювати при переміщенні бігунка за допомогою миші. Два проміжки
між движком і стрілками дозволяють переміститься на один блок (block) кліком кнопки миші. Зміст понять
"одиниця" і "блок" залежить від обєкта, з якимм працює смуга прокрутки. Наприклад, для вертикальної
смуги прокрутки при перегляді тексту це може бути рядок і сторінка або рядок і абзац. Методи роботи з
даним компонентом описані в інтерфейсе Adjustable, котрий реалізований классом ScroІІbar.
В класі Scrollbar три конструктори:
• Scrollbar() — створює вертикальну смугу прокрутки з діапазоном 0—100, поточним значенням 0 і
блоком 10 одиниць;
• Scrollbar (int orientation) — оріентация orientation задається однією із двох констант HORIZONTAL
або VERTICAL;
• Scrollbar(int orientation, int value, int visible, int min, int max) — задає, крім орієнтації, ще початкове
значення value, розмір блоку visible, діапазон значень min—max.
Аргумент visible визначає ще і довжину движка — вона устанавлюється пропорціонально діапазону
значень і довжині смуги прокрутки. Наприклад, конструктор по замовчуванню задасть довжину движка
рівною 0,1 довжини смуги прокрутки. Основний метод класу — getValue() — повертає значення поточного
положення движка на смузі прокрутки. Останні методи доступу дозволяють узнати і змінити
характеристики обєкта, приклади їх використання показані в лістинзі 12.6.
163
164. Події
Крім подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні значення
користувачем відбувається подія AdjustmentEvent.
В лістинзі 10.4 створюються три вертикальні смуги прокрутки — червона, зелена і синя, дозволяючі
вибрати якесь значення відповідного кольору в діапазоні 0—255, з початковим значенням 127. Крім них
створюється область, заповнена отриманим кольором, і дві кнопки. Смуги прокрутки, їх заголовок і
маштабні мітки поміщені в окремий контейнер р типу Panel. Про це трохи пізніше в даному уроці. Як все
це виглядує, показано на рис. 10.4. В лістинзі 12.6 ми "оживимо" цю програму.
Лістинг 10.4. Лінійки прокрутки для вибору кольору
import java.awt.*;
import java.awt.event.*;
class ScrollTest extends Frame{
Scrollbar sbRed = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255);
Scrollbar sbGreen = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255);
Scrollbar sbBlue = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255);
Color mixedColor = new Color(127, 127, 127);
Label lm = new Label();
Button b1 = new Button("Apply");
Button b2 = new Button("Cancel");
ScrollTest(String s){ super(s);
setLayout(null);
setFont(new Font("Serif", Font.BOLD, 15));
Panel p = new Panel();
p.setLayout(null);
p.setBounds(10,50, 150, 260); add(p);
Label lc = new Label("Choice color");
lc.setBounds(20, 0, 120, 30); p.add(lc);
Label lmin = new Label("0", Label.RIGHT);
lmin.setBounds(0, 30, 30, 30); p.add(lmin);
Label lmiddle = new Label("127", Label.RIGHT);
lmiddle.setBounds(0, 120, 30, 30); p.add(lmiddle);
Label lmax = new Label("255", Label.RIGHT);
lmax.setBounds(0, 200, 30, 30); p.add(lmax);
sbRed.setBackground(Color.red);
sbRed.setBounds(40, 30, 20, 200); p.add(sbRed);
sbGreen.setBackground(Color.green);
sbGreen.setBounds(70, 30, 20, 200); p.add(sbGreen);
sbBlue.setBackground(Color.blue);
sbBlue.setBounds(100, 30, 20, 200); p.add(sbBlue);
Label lp = new Label("Pattern:");
lp.setBounds(250, 50, 120, 30); add (lp);
lm.setBackground(new Color(127, 127, 127));
lm.setBounds(220, 80, 120, 80); add(lm);
b1.setBounds(240, 200, 100, 30); add(b1);
b2.setBounds(240, 240, 100, 30); add(b2);
setSize(400, 300); setVisible(true);
}
public static void main(String[] args){
Frame f = new ScrollTest("Color choice");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
164
165. }
}
В лістинзі 10.4 використаний контейнер Panel. Розглянемо можливості цього класу.
10.15. Контейнер Panel
Контейнер Panel — це невидимий компонент графічного інтерфейса, призначений для обєднання
декількох компонентів в один обєкт типу Panel. Класс Panel дуже простий, але важливий. В ньому всього
два конструктори:
• Panel() — створює контейнер з менеджером розміщення по замовчуванню FlowLayoutJ
• Panel(LayoutManager layout) — створює контейнер з указанням менеджером розміщення
компонентів layout.
Після створення контейнера в нього додаються компоненти унаслідовані методом add():
Panel p = new Panel()
p.add(compl);
p.add(comp2);
і т. д. Розміщує компоненти в контейнері його менеджер розміщення, про що ми поговоримо в наступному
уроці.
Контейнер Panel використовуєть дуже часто. Він зручний для створення групи компонентів. В лістинзі 10.4
три смугм прокрутки разом із заголовком "Підберіть колір" і маштабними мітками 0, 127 і 255 утворюють
природну групу. Якщо ми захочемо перемістить її в іншее місце вікна, нам прийдеться переносити кожен
із семи компонентів, що входять в указану группу. При цьому прийдеться слідкувати за тим, щоб їх
взаємне положення не змінилося. Замість цього ми створили панель р і розмістили на ній всі сім
елементів. Метод setBounds() кожного із розглядуваних компонентів указує в даному випадку положення і
розмір компонента в системі координат панелі р, а не вікна Frame. У вікно ми помістили зразу цілу
панель, а не її окремі компоненти. Тепер для переміщення всієї групи компонентів досить перемістить
панель, і розташовані на ній обєкти автоматично перемістяться разом з нею, не змінивши свого
взаємного
положення.
Рис. 10.4. Смуги прокрутки для вибору кольору
165
166. 10.16. Контейнер ScrollPane
Контейнер ScrollPane може містить тільки один компонент, зате такий, який не поміщається цілком у вікні.
Контейнер забезпечує засоби прокрутки для перегляду великого компонента. В контейнері можна
установити смуги прокрутки або постійно, константою SCROLLBARS_ALWAYS, або так, шоб вони
появлялись тілько при необхідності (якщо компонент дійсно не поміщається у вікно) константою
SCROLLBARS_AS_NEEDED. Якщо смуги прокрутки не установлені, це задає константа
SCROLLBARS_NEVER, то переміщення компонента для перегляду потрібно забезпечити із програми
одним із методов setScrollPosition(). В класі два конструктори:
• ScrollPane() — створює контейнер, в якому смуги прокрутки появляються по необхідності;
• ScrollPane(int scrollbars) — створює контейнер, в якому поява лінійок прокрутки задається однією
із трьох указанних вище констант.
Крнструктори створюють контейнер розміром 100x100 пікселів, надалі можна змінити розмір
унаслідуваним методом setSize( int width, int height). Обмеження, що ScroІІPane може містить тільки один
компонент, легко обходиться. Завжди можна зробити цим єдиним компонентом обєкт класу Panel,
розмістивши на панелі що завгодно. Серед методів класу цікаві ті, что дозволяють прокручивати
компонент в ScroІІPane:
• метод getHAdjustable() і getVAdjustabІe() повертають положення смуг прокрутки у вигляді
інтерфейса Adjustable;
• метод getScroІІPosition() показує координати (х,у) точки компонента, що знаходиться в лівому
верхньому куті панелі ScroІІPane, у вигляді обєкта класу Point;
• метод setScrollPosition(Point р) або setScrollPosition(int х, int у) прокручує компонент в позицію (х,
у).
10.17. Контейнер Window
Контейнер Window — це пусте вікно, без внутрішніх елементів: рамки, рядка заголовка, рядка меню, смуг
прокрутки. Це просто прямокутна область на екрані. Вікно типу Window самостійно, не міститься ні в
якому контейнері, його не треба заносити в контейнер методом add(). Одначе воно не звязано з віконним
менеджером графічної системи. Тому не можна змінити його розміри, перемістить в інше місце екрана.
Воно може бути створено тільки яким-небудь уже існуючим вікном, власником (owner) або батьком
(parent) вікна Window . Коли вікно-власник прибирається з екрана, разом з ним прибирається і породжене
вікно. Власник вікна указується в конструкторі:
• window (Frame f) — створює вікно, власник якого — фрейм f;
• Window (Window owner) — створює вікно, власник якого - уже наявне вікно або підклас класу
Window.
Створене конструктором вікно не виводиться на екран автоматично. Його належить відобразити методом
show(). Прибрати вікно з екрана можна методом hide(), а перевірити, чи видно вікно на екрані - логічним
методом isShowing(). Вікно типу Window можна використовувати для створення спливаючих вікон
попередження, повідомлення, підказки. Для створення діалогових вікон єсть підклас Dialog, спливаючих
меню — клас popupMenu (див. урок 13). Видиме на екране вікно виводиться на передній план методом
toFront() або, навпаки, поміщається на задній план методом toBack(). Знищити вікно, звільнивши зайняті
ним ресурси, можна методом dispose(). Менеджер розміщення компонентів у вікні по замовчуванню -
BorderLayout. Вікно створює свій екземпляр класу Toolkit, який можна отримати методом getToolkit().
Події
Кріме подійй класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні розмірів
вікна, його переміщенні або видаленні з екрана, а також показу на екрані відбувається подія windowEvent.
10.18. Контейнер Framе
Контейнер Frame — це повноцінне готове вікно з рядком заголовку, в який поміщені кнопки контекстного
166
167. меню, звертання вікна в ярлик і розвертання на весь екран і кнопка закрития додатку. Заголовок вікна
записується в конструкторі або методом setTitІe(String title). Вікно обмежено рамкою. В нього можна
установити рядок меню методом setMenuBar (MenuBar mb). Це ми обговоримо в уроці 13. На кнопці
контекстного меню в лівій частині рядка заголовка зображена димляча чашечка кофе — логотип Java. Ви
можете установити там друге зображення методом setІconІmage(image icon), створивши попередньо
зображення icon у вигляді обєкта класу image. Як це зробити, пояснюється в уроці 15. Всі элементи вікна
Frame викреслюються графічною оболонкою операційної системи по правилах цієї оболонки. Вікно Frame
автоматично реєструється у віконному менеджері графічної оболонки і може переміщаться, змінювати
розміри, звертаться в панель завдань (task bar) за допомогою миші або клавіатури, як "рідне" вікно
операційної системи. Створити вікно типу Frame можна наступними конструкторами:
• Frame() — створює вікно з пустим рядком заголовка;
• Frame(ІString title) — записує аргумент title в рядок заголовка.
Методи класу Frame здійснюють доступ до елементів вікна, але не забувайте, що класс Frame наслідує
близько двохсот методів класів Component, Container і Window. Зокрема, наслідується менеджер
розміщення по замовчуванню — BorderLayout.
Події
Кріме подій класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні розмірів
вікна, його переміщенні або видаленні з екрана, а також показу на екрані відбувається подія windowEvent.
Програма лістингу 10.5 створює два вікна типу Frame, в які поміщаються рядки — мітки Label. При закртті
основного вікна кліком по відповіднійй кнопці в рядку заголовка або комбинацією клавіш <Alt>+<F4>
виконання програми завершується зверненням до методу system.exit (0), і закрываються обидва вікна.
При закритті другого вікна відбувається звернення до методу dispose(), і закрывається тільки це вікно.
Лістинг 10.5. Створення двох вікон
import java.awt.* ;
import java.awt.event.*;
class TwoFrames{
public static void main(String[] args){
Fr1 f1 = new Fr1(" Головне вікно");
Fr2 f2 = new Fr2(" Друге вікно");
}
}
class Fr1 extends Frame{
Fr1(String s){
super(s);
setLayout(null);
Font f = new Font("Serif", Font.BOLD, 15);
setFont(f);
Label l = new Label("This is the main window", Label.CENTER);
l.setBounds(10, 30, 180, 30);
add(l);
setSize(200, 100);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit (0);
}
});
}
}
class Fr2 extends Frame{ Fr2(String s){
super(s);
setLayout(null) ;
167
168. Font f = new Font("Serif", Font.BOLD, 15);
setFont(f);
Label l = new Label("This is the second window", Label.CENTER);
l.setBounds(10, 30, 180, 30);
add(l);
setBounds(50, 50, 200, 100);
setVisible(true);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev) {
dispose ();
}
});
}
}
На рис. 10.5 показано виведення цієї програми. Взаємне положення вікон визначається віконним
менеджером операційної системи і може бути не таким, яке показане на рисунку.
Рис. 10.5. Програма з двома вікнами
10.19. Контейнер Dialog
Контейнер Dialog — це вікно фіксованого розміру, призначене для відповіді на повідомлення додатку.
Воно автоматичнои реєструється у віконному менеджері графічної оболонки, тому його можна
переміщати по екрані, змінювати його розміри. Але вікно типу Dialog, як і його суперклас — вікно типу
Window, - обовязково має власника owner, який указується в конструкторі. Вікно типу Dialog може бути
модальним (modal), в якому треба обовязково виконати всі передбачені дії, інакше із вікна не можна буде
вийти. В класі сім конструкторів. Із них:
• Dialog (Dialog owner) — створює немодальне діалогове вікно з пустим рядком заголовка;
• Dialog (Dialog owner, string title) — створює немодальне діалогове вікно з рядком заголовка title;
• Dialog(Dialog owner, String title, boolean modal) — створює діалогове вікно, яке буде модальним,
якщо modal == true.
Чотири інших конструктори аналогiчні, але створюють діалогові вікна, нaлежні вікну типу Frame:
• Dialog(Frame owner)
• Dialog(Frame owner, String title)
• Dialog(Frame owner, boolean modal)
• Dialog(Frame owner, String title, Boolean modal)
Серед методів класу цікаві методи: isModaІ(), перевіряючий стан модальності, і setModal(boolean modal),
змінюючий цей стан.
Події
168
169. Крім подійй класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні розмірів
вікна, його переміщенні або видаленні з екрана, а також показу на екрані відбувається подія windowEvent.
В лістинзі 10.6. створюється модальне вікно доступу, в яке вводиться імя і пароль. Поки не буде зроблено
правильне введення, інші дії неможливі. На рис. 10.6 показано вигляд цього вікна.
Лістинг 10.6. Модальне вікно доступу
import java.awt.*;
import java.awt.event.*;
class LoginWin extends Dialog{
LoginWin(Frame f, String s){
super(f, s, true);
setLayout(null);
setFont(new Font("Serif", Font.PLAIN, 14));
Label l1 = new Label("Your Name:", Label.RIGHT);
l1.setBounds(20, 30, 70, 25); add(l1);
Label l2 = new Label("Password:", Label.RIGHT);
l2.setBounds(20, 60, 70, 25); add(l2);
TextField tf1 = new TextField(30);
tf1.setBounds(100, 30, 160, 25); add(tf1);
TextField tf2 = new TextField(30);
tf2.setBounds(100, 60, 160, 25); add(tf2);
tf2.setEchoChar('*');
Button b1 = new Button("Hello");
b1.setBounds(50, 100, 100, 30); add(b1);
Button b2 = new Button("Cancel");
b2.setBounds(160, 100, 100, 30); add(b2);
setBounds(50, 50, 300, 150); } }
class DialogTest extends Frame{ DialogTest(String s){ super(s);
setLayout(null); setSize(200, 100);
setVisible(true);
Dialog d = new LoginWin(this, " Вікно входу"); d.setVisible(true);
}
public static void main(String[] args){
Frame f = new DialogTest("Owner window");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 10.6. Модальне вікно доступу
10.20. Контейнер FileDialog
169
170. Контейнер FileDialog — це модальне вікно з власником типу Frame, що містить стандартне вікно вибору
файла операційної системы для відкриття (константа LOAD) або збереження (константа SAVE). Вікна
операційної системи створюються і поміщаються в обєкт класу FileDialog автоматично. В класі три
конструктори:
• FileDialog (Frame owner) — створює вікно з пустим заголовком для відткриття файлів;
• FileDialog (Frame owner, String title) — створює вікно для відткриття файлів із заголовком title;
• FileDialog(Frame owner, String title, int mode) — створює вікно для відткриття або збереження
документа, аргумент mode має два значения: FileDialog.LOAD і FileDialog.SAVE.
Методи класу getDirectory() і getFiІe() повертають тільки вибраний каталог і імя файла у вигляді рядка
String. Завантаження або збереження файлу потім треба виконувати методами класів
введення/виведення, як розповідається в уроці 18, там же приведені приклади використання класу
FileDialog. Можна установити початковий каталог для пошуку файла і імя файла методами
setDirectory(String dir) і setFile(String fileName). Замість конкретного імени файла fileName можна написати
шаблон, наприклад, *.java (перші символи — зірочка і точка), тоді у вікні будуть видимі тільки імена
файлів, що закінчуються точкою і словом java. Метод setFilenameFilter(FilenameFilter filter) устанoвлює
шаблон filter для імени вибраного файла. У вікні будуть видимі тілько імена файлів, що підходять під
шаблон. Цей метод не реалізований в SUN JDK на платформі MS Windows.
Події
Крім подійй класу Component: ComponentEvent, FocusEvent, KeyEvent, MouseEvent, при зміні розмірів
вікна, його переміщенні або видаленні з екрана, а також показу на екрані відбувається подія windowEvent.
10.21. Створення власних компонентів
Створити свій компонент, доповнюючий властивості і методи уже існуючих компонентів AWT, дуже просто
— треба лише створити свій клас як розширення існуючого класу Button, TextFieІd або іншого класу-
компонента. Якщо треба скомбiнувати декілька компонентів в один, новий, компонент, то достить
розширити клас Panel, розташувавши компоненти на панелі. Якщо ж треба створити цілком новий
компонент, то AWT пропонує дві можливості: створити "важкий" або "легкий" компонент. Для створення
власних "важких" компонентів у бібліотеці AWT єсть клас Сanvas — пустий компонент, для якого
створюється свій peer-обєкт графічної системи.
10.22. Компонент Canvas
Компонент Canvas — це пустий компонент. Клас Сanvas дуже простий — в ньому тільки конструктор по
замовчуванню Canvas() і пуста реалізація методу paint(Graphics g). Щоб створити свій "важкий"
компонент, необхідно розширити клас Сanvas, доповнивши його потрібними полями і методами, і при
необхідності перевизначити метод paint(). Наприклад, як ви помітили, на стандартній кнопці Button можна
написати тілько один текстовий рядок. Не можна написати декілька рядків або відобразити на кнопці
рисунок. Створимо свій "важкий" компонент — кнопку з рисунком.
В лістинзі 10.7 кнопка з рисунком — клас FІowerButton. Рисунок задається методом drawFІower(), а
рисується методом paint(). Метод paint(), крім того, креслить по краях кнопки внизу і справа відрізки
прямих, зображуючих тінь, відкинутою "випуклою" кнопкою. При натисканні кнопки миші на компоненті такі
ж відрізки кресляться зверху і зліва — кнопка "вдавилась". При цьому рисунок здвигається на два пікселя
вправо вниз — він "вдавлюється" в площину вікна. Крім цього, в класі FІowerButton задана реакція на
натискання і відпускання кнопки миші. Це ми обговоримо в уроці 12, а поки що скажемо, что при кожному
натисканні і відпусканні кнопки змінюється значення поля isDown і кнопка перекреслюється методом
repaint (). Це досягається виконанням методів mousePressed() і mouseReleased(). Для порівняння рядом
розміщена стандартна кнопка типу Button того ж розміру. Рис. 10.7 демонструє вигляд цих кнопок.
Лістинг 10.7. Кнопка з рисунком
import java.awt.*;
import java.awt.event.*;
class FlowerButton extends Canvas implements MouseListener{
170
171. private boolean isDown=false;
public FlowerButton(){
super();
setBackground(Color.lightGray);
addMouseListener(this);
}
public void drawFlower(Graphics g, int x, int y, int w, int h){
g.drawOval(x + 2*w/5 - 6, y, w/5, w/5);
g.drawLine(x + w/2 - 6, y + w/5, x + w/2 - 6, y + h - 4);
g.drawOval(x + 3*w/10 -6, y + h/3 - 4, w/5, w/5) ;
g.drawOval(x + w/2 - 6, y + h/3 - 4, w/5, w/5); }
public void paint(Graphics g){
int w = getSize().width, h = getSize().height;
if (isDown){
g.drawLine(0, 0, w - 1, 0) ;
g.drawLine(0, 1, w - 1, 1);
g.drawLine(0, 0, 0, h - 1);
g.drawLine (1, 1, 1, h - 1);
drawFlower(g, 8, 10, w, h);
}
else
{
g.drawLine(0, h - 2, w - 2, h - 2);
g.drawLine(0, h - 1, w - 1, h - 1);
g.drawLine(w - 2, h - 2, w - 2, 0);
g.drawLine(w - 1, h - 1, w - 1, 1);
drawFlower (g, 6, 8, w, h) ; } }
public void mousePressed(MouseEvent e){
isDown=true; repaint(); }
public void mouseReleased(MouseEvent e){
isDown=false; repaint(); }
public void mouseEntered(MouseEvent e){}
public void mouseExited(MouseEvent e) {}
public void mouseClicked(MouseEvent e){}
}
class DrawButton extends Frame{
DrawButton(String s) {
super (s) ;
setLayout(null);
Button b = new Button("OK");
b.setBounds(200, 50, 100, 60); add(b);
FlowerButton d = new FlowerButton();
d.setBounds(50, 50, 100, 60); add(d);
setSize(400, 150);
setVisible(true);
}
public static void main(String[] args){
Frame f= new DrawButton(" Кнопка з рисунком");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
171
172. Рис. 10.7. Кнопка з рисунком
10.23. Створення "легкого" компонента
"Легкий" компонент, який не має свого peer-обєкта в графічій системі, створюється як пряме розширення
класу Сomponent або Container. При цьому необхідо задати ті дії, які у "важких" компонентах виконує peer-
обект. Наприклад, замінивши в лістинзі 10.7 заголовок класу FlowerButton рядком
class FlowerButton extends Component implements MouseListener
а потім перекомпілювавши і виконавши программу, ви одержите "легку" кнопку, але побачите, що її фон
став білим, тому що метод
setBackground(Color.lightGray)
не спрацював. Це пояснюється тим, что тепер всю чорну роботу по зображенню кнопки на екрані виконує
не peer-двійник кнопки, а "важкий" контейнер, в якому розташована кнопка, в нашому випадку клас Frame.
Контейнер же нічого не знає про те, що треба звернутися до методу setBackground(), він рисує тільки те,
щто записано в методі paint(). Прийдеться прибрати метод setBackground() із конструктора і заливати фон
сірим кольором вручну в методі paint(), як показано в лістинзі 10.8. "Легкий" контейнер не уміє рисувати
свої "легкі" компоненти, тому в кінці методу paint() "легкого" контейнера треба звернутися до методу
paint() суперкласу: super.paint(g). Тоді рисуванням займеться "важкий" суперклас-контейнер. Він нарисує і
свій "легкий" контейнер, і розміщені в контейнері "легкі" компоненти.
Порада
Завершуйте метод paint() "легкого" контейнера зверненням до методу paint() суперкласу. Оптимальний
розмір "важкого" компонента установлюється peer-обєктом, а для "легких" компонентів його треба задати
явно, перевизначивши метод getPreferredSize(), інакше деякі менеджеры розміщення, наприклад
FІowLayout(), установлять нульовий розмір, і компонент не буде видно на екрані.
Порада
Перевизначайте метод getPreferredSize().
Цікава особливість "легких" компонентів — вони ізначально рисуються прозорими, не зафарбована
частина прямокутного обєкта не буде видима. Це дозволяє створити компонент будь-якої видимої форми.
Лістинг 10.8 показує, як можна змінити метод paint() лістинга 10.7 для створення круглої кнопки і задати
додаткові методи, а рис. 10.8 демонструє її вигляд.
Лістинг 10.8. Створення круглої кнопки ;
import java.awt.*;
import java.awt.event.*;
class FlowerButton extends Canvas implements MouseListener{
private boolean isDown=false;
public FlowerButton(){
super();
addMouseListener(this);
}
public void drawFlower(Graphics g, int x, int y, int w, int h){
g.drawOval(x + 2*w/5 - 6, y, w/5, w/5);
172
173. g.drawLine(x + w/2 - 6, y + w/5, x + w/2 - 6, y + h - 4);
g.drawOval(x + 3*w/10 -6, y + h/3 - 4, w/5, w/5) ;
g.drawOval(x + w/2 - 6, y + h/3 - 4, w/5, w/5); }
public void paint(Graphics g){
int w = getSize().width, h = getSize().height;
int d = Math.min(w, h); // Діаметр круга
Color с = g.getColor(); // Зберігаємо поточний колір
g.setColor(Color.lightGray); // Установлюємо сірий колів
g.fillArc(0, 0, d, d, 0, 360); // Заливаємо круг сірим кольором
g.setColor(с); // Відновлюємо поточний колір
if (isDown){
g.drawArc(0, 0, d, d, 43, 180);
g.drawArc(0, 1, d - 2, d - 2, 43, 180);
drawFlower(g, 8, 10, d, d);
}else{
g.drawArc(0, 0, d, -d, 229, 162);
g.drawArc(0, 1, d - 2, d - 2, 225, 170);
drawFlower(g, 6, 8, d, d);
}
}
public Dimension getPreferredSize(){
return new Dimension(30,30);
}
public Dimension getMinimumSize()
{
return getPreferredSize(); }
public Dimension getMaximumSize(){
return getPreferredSize();
}
public void mousePressed(MouseEvent e){
isDown=true; repaint(); }
public void mouseReleased(MouseEvent e){
isDown=false; repaint(); }
public void mouseEntered(MouseEvent e){}
public void mouseExited(MouseEvent e) {}
public void mouseClicked(MouseEvent e){}
}
class DrawButton extends Frame{
DrawButton(String s) {
super (s) ;
setLayout(null);
Button b = new Button("OK");
b.setBounds(200, 50, 100, 60); add(b);
FlowerButton d = new FlowerButton();
d.setBounds(50, 50, 100, 60); add(d);
setSize(400, 150);
setVisible(true);
}
public static void main(String[] args){
Frame f= new DrawButton(" Кнопка з рисунком");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
173
174. Рис. 10.8. Кругла кнопка
Зразу ж треба дати ще одну рекомендацію. "Легкі" контейнери не займаються обробкою подій без
спеціальної вказівки. Тому в конструктор "легкого" компонента належить включити звернення до методу
enabІeEvents() для кожного типу подій. В нашому прикладі в конструктор класу FІowerButton корисно
додати рядок
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
на випадок, якщои кнопка виявиться в "легкому" контейнері. Детальніше про це ми поговоримо в уроці 12.
В документації єсть хороші прилади створення "легких" компонентів, подивіться сторінку
docsguideawtdemoslightweightindex.html.
Лабораторна робота 9. Властивості компонентів
1. Ви вже розібрали програми лістингів 10.7 і 10.8 і випробували їх. Це перші програми, які реагують на
кліки мишкою по кнопках. Заставте цю програму робити щось інше, при натисканні на кнопки. Наприклад,
змінювати колір компонентів, виводити текст у вікно чи в компонент, додавати нові компоненти і т. п.
Переробіть круглу кнопку в овальну. Будьте готові при здачі лабораторної роботи до того, що викладач
заставить вас замість овальної кнопки зробити трикутну, пятикутну, шестикутну. А також скаже, що
повинна зробити програма при натисканні на кнопку.
174
175. Програмування у Java
Урок 11. Розміщення компонентів
• Менеджер FlowLayout
• Менеджер BorderLayout
• Менеджер GridLayout
• Менеджер CardLayout
• Менеджер GridBagLayout
• Заключення
В попередньому уроці ми розміщували компоненти "вручну", задаючи їх розміри і положення в контейнері
абсолютними координатами в координатній системі контейнера. Для цього ми застосовували метод
setBounds(). Такий спосіб розміщує компоненти з точністю до пікселя, але не дозволяеє переміщати їх.
При зміні розмірів вікна за допомогою миші компоненти залишаться на своїх місцях, привязаними до
лівого верхнього кута контейнера. Крім того, немає гарантії, що всі монsтори відобразять компоненти так,
як ви задумали. Щоб врахувати зміну розмірів вікна, треба задати розміри і положення компонента
відносно розмірів контейнера, наприклад, так:
int w = getSize().width; // Одержимо ширину
int h = getSize().height; //і висоту контейнера
Button b = new Button("OK"); // Створюємо кнопку
b.setBounds(9*w/20, 4*h/5, w/10, h/10);
і при всякій зміні розмірів вікна задавати розташувння компонента заново. Щоб позбавити програміста від
цієї кропіткої роботи, в бібліотеку AWT внесені два интерфейси: LayoutManager і породжений від нтого
інтерфейс LayoutManager2, а також пять реалізацій цих інтерфейсів: класи BorderLayout, CardLayout,
FlowLayout, GridLayout і GridBagLayout. Ці класс названі менеджерами розміщення (layout manager)
компонентів. Кожний програміст може створити свої менеджери розміщення, реалізувавши інтерфейси
LayoutManager або LayoutManager2. Подивимося, як розміщують компоненти ці класи.
11.1. Менеджер FlowLayout
Найбільш просто поступає менеджер розміщення FlowLayout. Він укладає в контейнер один компонент за
другим зліва направо як цеглини, переходячи від верхніх рядів до нижніх. При зміні розміру контейнера
"цеглини" перестроюються, як показано на рис. 11.1. Компоненти поступають в тому порядку, в якому
вони задані в методах add(). В кожному ряді компоненти могжть притискуватися до лівого краю, якщои в
конструкторі аргумент align рівний FlowLayout. LEFT, до правого краю, якщо цей аргумент FlowLayout.
RIGHT, або збиратися в середині ряду, якщо FlowLayout.CENTER. Між компонентами можна залишити
проміжки (gap) по горизонталі hgap і вертикалі vgap. Це задається в конструкторі:
FlowLayout(int align, int hgap, int vgap)
Другий конструктор задає проміжки розміром 5 пікселів: FlowLayout(int align)
Третій конструктор визначає вирівнювання по центру і проміжки 5 пікселів: FlowLayout()
Після формуванняя обєкта ці параметри можна змінити методами: setHgapfint hgap), setVgap(int vgap),
setAlignment(int align)
В лістинзі 11.1 створюється кнопка Button, мітка Label, кнопка вибора Сheckbox, розкриваючийся список
Сhoice, поле ввелдення TextField і все це розміщується в контейнері Frame. Рис. 11.1 містить вигляд цих
компонентів при різних розмірах контейнера.
Лістинг 11.1. Менеджер розміщення FlowLayout
import java.awt.*;
import java.awt.event.*;
class FlowTest extends Frame{
175
176. FlowTest(String s) {
super(s);
setLayout (new FlowLayout (FlowLayout.LEFT, 10, 10));
add(new Button("Button"));
add(new Label("Label"));
add(new Checkbox("Choice"));
add(new Choice());
add(new TextField("Help", 6));
setSize(300, 100); setVisible(true);
}
public static void main(String[] args){
Frame f= new FlowTest(" Менеджер FlowLayout");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 11.1. Розміщення компонентів за допомогою FlowLayout
11.2. Менеджер BorderLayout
Менеджер разміщення BorderLayout ділить контейнер на пять нерівних областей, повністю заповнюючи
кожну область одним компонентом, як показано на рис. 11.2. Области отримали географічні назви
NORTH, SOUTH, WEST, EAST і CENTER. Метод add() у випадку застосування BorderLayout має два
аргументи: посилку на компонент comp і область region, в яку поміщається компонент — одну із
перечислених вище констант: add(Component comp, String region). Метод add (Component comp) з одним
аргументом поміщає компонент в область CENTER. В класi два конструктори:
• BorderLayout() — між областями немає проміжків;
• BorderLayout(int hgap int vgap) — між областями залишається горизонтальні hgap і вертикальні
vgap проміжки, задані в пікселях.
Якщо в контейнер поміщається менше пяти компонентів, то деякі області не використовуються і не
займають місце в контейнері, як можна помітити на рис. 11.3. Якщо не зайнята область CENTER, то
компоненти притискуються до границь контейнера.
В лістинзі 11.2 створюються пять кнопок, розміщених в контейнері. Замітьте відсутність установки
менеджера в контейнері setLayout() — менеджер BorderLayout установлений в контейнер Frame по
замовчуванню. Результат розміщення показаний на рис. 11.2.
Лістинг 11.2. Менеджер розміщення BorderLayout
import java.awt.*;
import java.awt.event.* ;
class BorderTest extends Frame{
BorderTest(String s){ super(s);
add(new Button("North"), BorderLayout.NORTH);
add(new Button("South"), BorderLayout.SOUTH);
add(new Button("West"), BorderLayout.WEST);
176
177. add(new Button("East"), BorderLayout.EAST);
add(new Button("Center"));
setSize(300, 200);
setVisible(true);
}
public static void main(String[] args){
Frame f= new BorderTest(" Менеджер BorderLayout");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 11.2. Області розміщення BorderLayout
Менеджер розміщення BorderLayout здається незручним: він має не більше пяти компонентів, останні
розтікаються по всій области, області мають дивний вигляд. Але справа в тому, що в кожну область
можна помістити не компонент, а панель, і розміщувати компоненти на ній, як зроблено в лістинзі 11.3 і
показано на рис. 11.3. Нагадаємо, що на панелі Panel менеджер розміщення по замовчуванню
FІowLayout.
Лістинг 11.3. Складне компонування
import java.awt.*;
import java.awt.event.*;
class BorderPanelTest extends Frame{
BorderPanelTest(String s){
super(s);
// Створюємо панель р2 з трьома кнопками
Panel p2 = new Panel();
p2.add(new Button("Execute"));
p2.add(new Button("Cancel"));
p2.add(new Button("Exit"));
Panel p1 = new Panel();
p1.setLayout(new BorderLayout());
// Поміщаємо панель р2 з кнопками на "півдні" панелі р1
p1.add(p2, BorderLayout.SOUTH);
// Поле введення поміщаємо на "півночі"
p1.add(new TextField("Input Field", 20), BorderLayout.NORTH);
// Область введення поміщається в центрі
p1.add(new TextArea("Input Area", 5, 20, TextArea.SCROLLBARS_NONE),
BorderLayout.CENTER);
177
178. add(new Scrollbar(Scrollbar.HORIZONTAL), BorderLayout.SOUTH);
add(new Scrollbar(Scrollbar.VERTICAL), BorderLayout.EAST);
// Панель p1 поміщаємо в "центрі" контейнера
add(p1, BorderLayout.CENTER);
setSize(200, 200);
setVisible(true) ;
}
public static void main(String[] args){
Frame f= new BorderPanelTest(" Складне компонування");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 11.3. Компонування за допомогою FІowLayout і BorderLayout
11.3. Менеджер GridLayout
Менеджер розміщення GridLayout розставляє компоненти в таблицю із заданим в конструкторі числом
рядків rows і стовпців columns:
GridLayout(int rows, int columns).
Всі компоненти отримують одинаковий розмір. Проміжків між компонентами немає. Другий конструктор
дозволяє задати проміжки між компонентами в пікселях по горизонталі hgap і вертикалі vgap:
GridLayout(int rows, int columns, int hgap, int vgap)
Конструктор по заовчуванню GridLayout() задає таблицю розміром 0x0 без проміжків між компонентами.
Компоненти будуть розташовуватися в одному рядку. Компоненти розміщуються менеджером GridLayout
злiва направо по рядкам створеної таблиці в тому порядку, в якому вони задані в методах add(). Нульова
кількість рядків або стовпців означає, що менеджер сам створить потрібне їх число. В лістинзі 11.4
розміщуються кнопки для калькулятора, а рис. 11.4 показує, як виглядає це розміщення.
Лістинг 11.4. Менеджер GridLayout
import java.awt.*;
import java.awt.event.*;
import java.util.*;
class GridTest extends Frame{
GridTest(String s){ super(s);
setLayout(new GridLayout(4, 4, 5, 5));
StringTokenizer st =
new StringTokenizer("7 8 9 / 4 5 6 * 1 2 3 - 0 . = + ");
178
179. while(st.hasMoreTokens())
add(new Button(st.nextToken()));
setSize(200, 200); setVisible(true);
}
public static void main(String[] args){
Frame f= new GridTest(" Менеджер GridLayout");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 11.4. Розміщення кнопок менеджером GridLayout
11.4. Менеджер Card Layout
Менеджер розміщення СardLayout своєрідний - він показує в контейнері тілько один, перший (first),
компонент. Решта компонентів лежить під першим у певному порядку як гральні карти в колоді. Їх
розташування визначається порядком, в якому написані методи add(). Наступний компонент можна
показати методом next (Container с), попередній - методом previous (Container с), останній - методом last
(Container с), перший - методом first (Container с). Аргумент цих методов - посилка на контейнер, в який
поміщені компоненти, звичайно this. В класі два конструктори:
• СardLayout() — не відділяє компонент відт границь контейнера;
• CardLayout(int hgap, int vgap) — задає горизонталне hgap і вертикальне vgap поля.
Менеджер CardLayout дозволяє організувати і довільний доступ до компонентів. Метод add() для
менеджера CardLayout має своєрідний вигляд:
add(Component comp, Object constraints)
Тут аргумент constraints повинен мати тип String і містити імя компонента. Потрібний компонент з іменем
name можна показати методом:
show(Container parent, String name)
В лістинзі 11.5 менеджер розміщення c1 працює з панеллю р, поміщеній в "центр" контейнера Frame.
Панель р указується як аргумент parent в методах next() і show(). На "північ" контейнера Frame
відправлена панель р2 з міткою в розкриваючому списку ch. Рис. 11.5 демонструє результат роботи
програми.
Лістинг 11.5. Менеджер CardLayout
import java.awt.*;
import java.awt.event.*;
class CardTest extends Frame{ CardTest(String s){
179
180. super(s);
Panel p = new Panel();
CardLayout cl = new CardLayout();
p.setLayout(cl);
p.add(new Button("Ukrainian page"),"page1");
p.add(new Button("English page"), "page2");
p.add(new Button("German page"), "pageЗ");
add(p);
cl.next(p);
cl.show(p, "pagel");
Panel p2 = new Panel();
p2.add(new Label("Choice language:"));
Choice ch = new Choice();
ch.add("Ukrainian");
ch.add("English");
ch.add("German");
p2.add(ch);
add(p2, BorderLayout.NORTH);
setSize(400, 300);
setVisible(true); }
public static void main(String[] args){
Frame f= new CardTest(" Менеджер CardLayout");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 11.5. Менеджер розміщення CardLayout
11.5. Менеджер GridBagLayout
Менеджер розміщення GridBagLayout розставляє компоненти найбільш гнучко, дозволяючи задавати
розміри і положення кожного компонента. Але він виявився досить складним і застосовуються рідко. В
класі GridBagLayout єсть тільки один конструктор по замовчуванню, без аргументів. Менеджер класу
GridBagLayout, на відміну від інших менеджерів розміщення, не містить правил розміщення. Він відіграє
тільки організуючу роль. Йому передаються посилання на компонент і правила розташування цього
компонента, а сам він поміщає даний компонент по вказаним правилам у контейнер. Всі правила
розміщення компонентів задаються в обєкті іншого класу, GridBagConstraints. Менеджер розміщує
компоненти в таблиці з невизначеним задалегідь числом рядків і стовпців. Один компонент може займати
декілька клітинок цієї таблиі, заповнювати клітинку цілком, розташовується в її центрі, куті або
притискується до краю клітинки. Класс GridBagConstraints містить одинадцять полів, визначаючих розміри
компонентів, їх положення в контейнері і взаємне положення, і декілька констане - значень деяких полів.
Вони перечислені в табл. 11.1. Ці параметры визначаються конструктором, що має одинадцять
аргументів. Другий конструктор — конструктор по замовчуванню — присвоює параметрам значення,
180
181. задані по замовчуванню.
Таблиця 11.1. Поля класу GridBagConstraints
Поле Значення
anchor Напрям розміщення компонента в контейнері. Константи: CENTER, NORTH, EAST,
NORTHEAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, і NORTHWEST; пo
замовчуванню CENTER
fill Розтягування компонента для заповнення клітинки. Константи: NONE, HORIZONTAL,
VERTICAL, BOTH; ПО умолчанию NONE
gridheight Кількість клітинок в колонці, зайнятих компонентом. Ціле типу int, по замовчуванню 1.
Константа REMAINDER означає, що компонент займе решту колонки, RELATIVE —
буде наступним по порядку в колонкці
gridwidth Кількість клітинок і рядку, зайнятих компонентом. Ціле типу int, по замовчуванню 1.
Константа REMAINDER означає, що компонент займе решту рядка, RELATIVE — буде
наступним в рядку по порядку
gridx Номер клітинки в рядку. Сама ліва клітинка має номер 0. По замовчуванню константа
RELATIVE, що означає: наступна по порядку
gridy Номер клітинки в стовпці. Сама верхня клітинка має номер 0. По замовчуванню
константа RELATIVE, що означає: наступна по порядку
insets Поля в контейнері. Обєкт класу insets; по замовчуванню обєект з нулями
ipadx, ipady Горизонтальные и вертикальные поля вокруг компонентов; по замовчуванню 0
weightx,
weighty
Пропорційний розтяг компонентів при зміні розміру контейнера; по замовчуванню 0,0
Як правило, обєкт класу GridBagConstraints створюється конструктором по замовчуванню, потім значення
потрібних полів змінюються простим присвоєнням нових значень, наприклад:
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1.0;
gbc.gridwidth = GridBagConstraints.REMAINDER
gbc.gridheight =2;
Післяя створення обєкта gbc класу GridBagConstraints менеджеру розміщення указується, що при
поміщенні компонента comp в контейнер належить застосовувати правила, занесені в обєкт gbc. Для
цього застосовується метод
add(Component comp, GridBagConstraints gbc)
Отже, схема застосування менеджера GridBagLayout така:
Лістинг 11.6. Менеджер GridBagLayout
import java.awt.*;
import java.awt.event.*;
class GridBagTest extends Frame{ GridBagTest(String s){
super(s);
GridBagLayout gbl = new GridBagLayout();
181
182. setLayout(gbl);
GridBagConstraints c = new GridBagConstraints();
Button b1 = new Button("Button1");
c.gridwidth = 2;
add(b1,c);
Button b2 = new Button("Button2");
c.gridwidth = 1;
add(b2,c);
setSize(400, 300);
setVisible(true); }
public static void main(String[] args){
Frame f= new GridBagTest(" Менеджер GridBagLayout");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Рис. 11.6. Менеджер GridBagLayout
В документації до класу GridBagLayout приведений хороший приклад використання цього менеджера
розміщення.
Заключення
Всі менеджери розміщення написані повністю мовою Java, в склад SUN J2SDK входять їх вихідні тексти.
Якщо ви вирішили написати свій менеджер розміщення, реалізувавши інтерфейс LayoutManager або
LayoutManager2, то подивіться ці вихідні тексти.
Лабораторна робота 10. Створення повноцінної Java програми з графічним інтерфейсом.
1. Візьміть за основу програму лістингу 11.4. Додайте в форму текстове вікно і запрограмуйте
повноцінний калькулятор.
2. Додайте у форму ще декілька кнопок і запрограмуйте обчислення тригонометричних, обернених
тригонометричних і інших елементарних функцій. Будьте готові до того, що викладач при прийомі
роботи запропонує додати ще якусь кнопку - функцію.
182
183. Програмування у Java
Урок 12. Обробка подій
• Подія ActionEvent
• Обробка дій миші
• Класи-адаптери
• Обробка дій клавіатури
• Подія TextEvent
• Обробка дій з вікном
• Подія ComponentEvent
• Подія ConainerEvent
• Подія FocusEvent
• Подія ItemEvent
• Подія AdjustmentEvent
• Декілька слухачів одного джерела
• Диспетчеризація подій
• Створення власної події
В двох попередніх главах ми написали багато програм, створюючих інтерфейси, але, власне, інтерфейса,
тобто взаємодії з користувачем, ці програми не забезпечують. Можна клікнути по кнопці на екрані, вона
буде "вдавлюватися" в площину екрана, але більше нічого не буде відбуватися. Можна ввести текст в
поле введення, але він не стане сприйматися і оброблятися програмою. Все це відбувається із-за того,
що ми не задали обробку дій користувача, обробку подій.
12.1. Подія
Подія (event) в бібліотеці AWT виникає при дії на компонент якими-небудь маніпуляціями мишею, при
введенні з клавіатури, при переміщенні вікна, зміні його розмірів. Обєкт, в якому відбулася подія,
називається джерелом (source) події. Всі події в AWT класифіковані. При виникнкенні події виконуюча
система Java автоматично створює обєкт відповідного події класу. Цей обєкт не виконує ніяких дій, він
тільки зберігає всі дані про події.
На чолі ієрархії класів-подій стоїть клас Eventobject із пакета java.util — безпосереднє розширення класу
Object. Його розширяє абстрактний клас AWTEvent із пакета java.awt — глава класів, описуючих події
бібліотеки AWT. Подальша ієрархія класів-подій показана на рис. 12.1. Всі класи, відображені на рисунку,
крім класу AWTEvent, зібрані в пакет java.awt.event. Події типу ComponentEvent, FосusEvent, KeyEvent,
MouseEvent виникають у всіх компонентах. А події типу ContainerEvent — тільки в контейнерах: Container,
Dialog, FileDialog, Frame, Panel, ScrollPane, Window.
Рис. 12.1. Ієрархія класів, описуючих події AWT
Події типу WindowEvent виникають тільки у вікнах: Frame, Dialog, FileDialog, Window.
Події типу TextEvent генеруються тільки в контейнерах Textcomponent, TextArea, TextField.
Події типу ActionEvent проявляються тільки в контейнерах Button, List, TextField.
Події типу ItemEvent виникають тільки в контейнерах Checkbox, Choice, List.
Нарешті, події типу AdjustmentEvent виникають тільки в контейнері Scrollbar.
Узнати, в якому обєкті відбулася подія , можна методом getSource() класу Eventobject. Цей метод
183
184. повертає тип Оbject. В кожному з цих класів-подійй визначений метод paramstring(), повертаючий вміст
обєкта даного класу у вигляді рядка String. Крім того, в кожному класі єсть свої методи, посталяючі ті чи
інші відомості про події. Зокрема, метод getio() повертає ідентификатор (identifier) події — ціле число, що
означає тип події. Ідентифікатори події визначені в кожному класі-події як константи.
Методи обробки подій описані в інтерфейсах-слухачах (listener). Для кожного показаного на рис. 12.1 типу
подій, крім inputEvent (ця подія рідко використовується самостійно), єсть свій інтерфейс. Імена
інтерфейсів складаються із імені події і слова Listener, наприклад, ActionListener, MouseListener. Методи
інтерфейса "слухають", що відбувається в потенційному джерелі події. При виникненні події ці методи
автоматично виконуються, одержуючи в якості аргумента обєкт-подію і використовуючи при обробці
відомості про події, що містяться в цьому обєкті.
Щоб задати обробку події певного типу, треба реалізувати відповідний інтерфейс. Класи, реалізуючі такий
інтерфейс, класи-оброблювачі (handlers) події, називаются слухачами (listeners): вони "слухають", що
відбувається в обєкті, щоб відслідкувати виникнення події і опрацювати її. Щоб звязатия з обробчиком
події, класи-джерела події повинні отримати посилку на екземпляр eventHandІer класу-обробчика події
одним із методів addXxxListener(XxxEvent eventHandier), де Ххх — імя події. Такий спосіб реєстрації, при
якому слухач залишає "візитну карточку" джерелу для свого виклику при виникненні події, називається
зворотний виклик (callback). Ним часто користуються студенти, які, звонячи батькам і не бажаючи платити
за телефонну розмову, говорять: "Передзвони мені по такому-то номеру". Зворотна дія — відмова від
обробчика, переривання прослуховування — виконується методом removeXxxListener(). Таким чином,
компонент-джерело, в якому відбулася подія, не займається його обробкою. Він звертається до
экземпляра класу-слухача, уміючого обробляти події, делегує (delegate) йому повноваження по обробці.
Така схема отримала назву схеми делегування (delegation). Вона зручна тим, що ми можемо легко
змінити клас-обробчик і опрацювати подію по-другому або призначити декілька оброблювачів однієї й тієї
ж події. З іншого боку, ми можемо один оброблювач призначити на прослуховування декількох обєктів-
джерел подій. Ця схема здається занадто складною, але ми нею часто користуємося в житті. Допустимо,
ми вирішили облаштувати квартиру. Ми поміщаємо в неї, як в контейнер, різні компоненти: меблі,
сантехніку, электроніку, антикваріат. Ми вважаємо, що може відбутися неприємна подія — квартиру
відвідають грабіжники, — і хочемо її опрацювати. Ми знаємо, что класи-оброблювачі цієї події — охоронні
агентства, — і звертаємося до деякого экземпляра такого класу. Компоненти-джерела події, тобто речі,
які можуть бути украдені, приєднують до себе датчики методом addXxxListener(). Потім экземпляр-
оброблювач "слухає", що відбувається в обєктах, до яких він підключений. Він реагує на виникнення
тільки однієї події — викрадення прослуховуваного обєкта, — інші події, наприклад, коротке замикання
або прорив водопровідної труби, його не цікавлять. При виникненні "своєї" події він діє по контракту,
записаному в методі опрацювання.
Зауваження
В JDK 1.0 була прийнята інша модель обробки подійй. Не дивуйтесь, читаючи старі книги і проглядаючи
вихідні тексти старих програм, але і не користуйтеся старою моделлю. Наведемо приклад. Нехай в
контейнері типу Frame поміщено поле введення tf типу TextField, не редагована область введення ta типу
TextArea і кнопка b типу Button. В поле tf вводиться рядок, після натискання клавіші <Enter> або кліку
кнопкою миші по кнопці b рядок переноситься в область ta. Після цього можна знову вводити рядок в
поле tf і т. д.
Тут і при натисканні клавіші <Enter> і при кліку кнопкою миші виникає подія класу ActionEvent, причому
вона може виникнути у двох компонентах-джерелах: полі tf або кнопці b. Обробка події в обох випадках
заключається в отриманні рядка тексту із поля tf (наприклад, методом tf.getText() і розміщенні його в
область ta (скажімо, методом ta.append()). Значить, можна написати один оброблювач події ActionEvent,
реалізувавши відповідний інтерфейс, який називається ActionListener. В цьому інтерфейсі всього один
метод actionPerformed(). Отже, пишемо:
class TextMove implements ActionListener{
private TextField tf;
private TextArea ta;
TextMove(TextField tf, TextArea ta){
this.tf = tf; this.ta = ta;
184
185. }
public void actionPerformed(ActionEvent ae){
ta.append(tf.getText()+"n");
}
}
Оброблювач події готовий. При виникненні події типу ActionEvent буде створений экземпляр класу-
оброблювача TextMove, конструктор отримає посилання на конкретні поля обєкта-джерела, метод
actionPerformed(), автоматично включившись в роботу, перенесе текст із одного поля в друге.
Теперь напишемо класс-контейнер, в якому знаходяться джерела tf і b події ActionEvent, і підключим до
них слухача цієї події TextMove, передавши їм посилки на нього методом addActionListener(), як показано
в лістинзі 12.1.
Лістинг 12.1. Обробка події ActionEvent
import java.awt.*;
import java.awt.event.*;
class MyNotebook extends Frame{
MyNotebook(String title) {
super(title);
TextField tf = new TextField("Input a text", 50);
add(tf, BorderLayout.NORTH);
TextArea ta = new TextArea();
ta.setEditable(false);
add(ta);
Panel p = new Panel();
add(p, BorderLayout.SOUTH);
Button b = new Button("Transfer");
p.add(b);
tf.addActionListener(new TextMove(tf, ta));
b.addActionListener(new TextMove(tf, ta));
setSize(300, 200); setVisible(true);
}
public static void main(String[] args){
Frame f = new MyNotebook(" Обробка ActionEvent");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
class TextMove implements ActionListener{
private TextField tf;
private TextArea ta;
TextMove(TextField tf, TextArea ta){
this.tf = tf; this.ta = ta;
}
public void actionPerformed(ActionEvent ae){
ta.append(tf.getText()+"n");
}
}
На рис. 12.2 показано результат роботи з цією програмою. В лістинзі 12.1 в методах addActionListener()
створюються два экземпляри класу TextMove — для прослуховування поля tf і для прослуховування
кнопки b. Можна створити один экземпляр класу TextMove, він буде прослуховувать обидва компоненти:
TextMove tml = new TextMove(tf, ta);
185
186. tf.addActionListener(tml);
b.addActionListener(tml);
Але в першому випадку екземпляри створюються після виникнення події у відповідному компоненті, а в
другому — незалежно від того, наступила подія чи ні, що приводить до витрат памяті, навіть якщо подія
не відбулася. Вирішуйте самі, що краще.
Рис. 12.2. Обробка події ActionEvent
Клас, що містить джерело події, может сам її опрацьовувати. Ви можете самостійно прослухувати
компоненти в своїй квартирі, установивши пульт сигналізації біля ліжка. Для цього достатньо реалізувати
відповідний інтерфейс прямо в класі-контейнері, як показано в лістинзі 12.2.
Лістинг 12.2. Самообробка події ActionEvent
import java.awt.*;
import java.awt.event.*;
class MyNotebook extends Frame implements ActionListener{
private TextField tf;
private TextArea ta;
MyNotebook(String title){
super(title) ;
tf = new TextField ("Input Text **", 50) ;
add(tf, BorderLayout.NORTH);
ta = new TextArea();
ta.setEditable(false);
add(ta);
Panel p = new Panel();
add(p, BorderLayout.SOUTH);
Button b = new Button("Transfer");
p.add(b);
tf.addActionListener(this) ;
b.addActionListener(this) ;
setSize(300, 200); setVisible(true) ; }
public void actionPerformed(ActionEvent ae){
ta.append(tf.getText()+"n"); }
public static void main(String[] args){
Frame f = new MyNotebook(" Обробка ActionEvent");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
186
187. }
}
Тут tf і ta уже не локальні змінні, а змінні экземпляру, оскільки вони використовуються і в конструкторі, і в
методі actionPerformed(). Цей метод тепер - один із методів класу MyNotebook. Класс MyNotebook став
класом-оброблювачем події ActionEvent - він реаліує інтерфейс ActionListener. В методі addActionListener()
указується аргумент this - клас сам слухає свої компоненти.
Розглянута схема, здається, простіша і зручніша, але вона представляє менше можливостей. Якщо ви
захочете змінити обробку, наприклад заносити записи в поле ta по алфавіту або по часу виконання
завданьй, то прийдеться переписати і перекомпілювати клас MyNotebook. Ще один варіант - зробити
оброблювач вкладеним класом. Це дозволяє обійтися без змінних екземпляра і конструктора в класі-
обробчика TextMove, як показано в лістинзі 12.3.
Лістинг 12.3. Обробка вкладеним класом
import java.awt.*;
import java.awt.event.*;
class MyNotebook extends Frame{
private TextField tf;
private TextArea ta;
MyNotebook(String title){
super(title);
tf = new TextField("Input Text", 50);
add(tf, BorderLayout.NORTH);
ta = new TextArea();
ta.setEditable(false);
add (ta);
Panel p = new Panel();
add(p, BorderLayout.SOUTH);
Button b = new Button("Transfer");
p.add(b);
tf.addActionListener(new TextMove());
b.addActionListener(new TextMove());
setSize(200, 200);
setVisible(true);
}
public static void main(String[] args){
Frame f = new MyNotebook(" Обробка ActionEvent");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit (0);
}
});
}
// Вкладений клас
class TextMove implements ActionListener{
public void actionPerformed(ActionEvent ae){
ta.append(tf.getText()+"n");
}
}
}
Нарешті, можна створити безіменний вкладений клас, що ми і робили в цьому і попередньому уроках,
обробробляючи натискування комбінації клавиш <Alt>+<F4> або клік кнопкою миші по кнопці закриття
вікна. При цьому виникає подія типу windowEvent, для її обробки ми зверталися до методу
windowСІosing(), реалізуючи його звернення до методу завершення додатку System.exit(0). Але для цього
треба мати суперклас визначеного безіменного класу, такий як windowAdapter. Такими суперкласами
можуть бути класи-адаптери, про них мова піде чуть далі. Перейдемо до детального розгляду різних типів
187
188. подій.
12.2. Подія ActionEvent
Ця проста подія означає, що треба виконати якусь дію. При цьому не має значення, що викликало подію:
клік миші, натискання клавіші чи щось інше. В класі ActionEvent єсть два корисні методи:
• метод getActionCommand() повертає у вигляді рядка String напис на кнопці Button, точніше, те, що
установлено методом setActionCoramand(String s) класу Button, выбраний пункт списку List, або
щось інше, залежно від компонента;
• метод getModifiers() повертає код клавіш <Alt>, <Ctrl>, <Meta> або <Shift>, якщо якась одна або
декілька з них були натиснені, у вигляд числа типу int; узнать, які саме клавіші були натиснуті,
можна порівнянням зі статичними константами цього класу ALT_MASK, CTRL_MASK,
META_MASK, SHIFT_MASK.
Примітка
Клавіші <Meta> на PC-клавіатурі неє, її дія часто призначається на клавішу <Esc> або ліву клавішу <Alt>.
Наприклад:
public void actionPerformed(ActionEvent ae){
if (ae.getActionCommand() == "Open" &&
(ae.getModifiers() | ActionEvent.ALT_MASK) != 0){
// Якісь дії
}
}
12.3. Обробка дій миші
Подія MouseEvent виникає в компоненті по одній із семи причин:
• натискання кнопки миші — ідентифiкатор MOUSE_PRESSED;
• відпускання кнопки миши — ідентифiкатор MOUSE_RELEASED;
• клік кнопкою миші — ідентифiкатор MOUSE_CLICKED (натискання і відпускання не відрізняються);
• переміщення миші — ідентифiкатор MOUSE_MOVED;
• переміщення миші с натисненою кнопкою — ідентифiкатор MOUSE_DRAGGED;
• поява курсора миші в компоненті — ідентифiкатор MOUSE_ENTERED;
• вихід курсора миші із компонента — ідентiфiкатор MOUSE_EXITED.
Для їх обробки єсть сім методів у двох інтерфейсах:
public interface MouseListener extends EventListener{
public void mouseClicked(MouseEvent e);
public void mousePressed(MouseEvent e) ;
public void mouseReleased(MouseEvent e);
public void mouseEntered(MouseEvent e);
public void mouseExited(MouseEvent e);
}
public interface MouseMotionListener extends EventListener{
public void mouseDragged(MouseEvent e);
public void mouseMoved(MouseEvent e);
}
Ці методи можуть одержати від аргумента е координати курсора миші в системі координат компонента
методами e.getx(), e.getv(), або одним методом e.getPoint(), повертаючим екземпляр класу Point.
Подвійний клік кнопкою миші можна відслідкувати методом e.getСІickСount(), повертаючим кількість
клікіов. При переміщенні миші повертається 0. Узнати, яка кнопка була натиснута, можна за допомогою
методу e.getModifiers() класу inputEvent порівнянням з наступими статичними константами класу
188
189. inputEvent:
• BUTTON1_MASK — натиснута перша кнопка, звичайно ліва;
• BUTTON2_MASK — натиснута друга кнопка, звичайно середня, або одночаснно обидві кнопки на
двохкнопковій миші;
• BUTTON3_MASK — натиснута третья кнопка, звичайно права.
Приведемo приклад, уже ставший класичним. В лістинзі 12.4 представлений найпростіший варіант
"рисувалки" — клас scribble. При натисканні першої кнопки миші методом mousePressed()
запамятовуються координати курсора миші. При протягуванні миші викреслюються відрізки прямих між
поточним і попереднім положенням курсора миші методом mouseDragged(). На рис. 12.3 показано
приклад роботи з цією програмою.
Лістинг 12.4. Найпростіша программа рисування
import java.awt.*;
import java.awt.event.*;
public class ScribbleTest extends Frame{
public ScribbleTest(String s){
super(s);
ScrollPane pane = new ScrollPane();
pane.setSize(300, 300);
add(pane, BorderLayout.CENTER);
Scribble scr = new Scribble(this, 500, 500);
pane.add(scr);
Panel p = new Panel();
add(p, BorderLayout.SOUTH);
Button b1 = new Button("Red");
p.add(b1);
b1.addActionListener(scr);
Button b2 = new Button("Green");
p.add(b2);
b2.addActionListener(scr) ;
Button b3 = new Button("Blue");
p.add(b3);
b3.addActionListener(scr) ;
Button b4 = new Button("Black");
p.add(b4);
b4.addActionListener(scr);
Button b5 = new Button("Clean");
p.add(b5);
b5.addActionListener(scr);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
pack();
setVisible(true);
}
public static void main(String[] args){
new ScribbleTest(" "Рисувалка"");
}
}
class Scribble extends Component implements ActionListener, MouseListener,
MouseMotionListener{
protected int lastX, lastY, w, h;
protected Color currColor = Color.black;
protected Frame f;
189
190. public Scribble(Frame frame, int width, int height){
f = frame;
w = width;
h = height;
enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
addMouseListener(this);
addMouseMotionListener(this); }
public Dimension getPreferredSize(){
return new Dimension(w, h); }
public void actionPerformed(ActionEvent event){
String s = event.getActionCommand();
if (s.equals ("Clean")) repaint();
else if (s.equals ("Red")) currColor = Color.red;
else if (s.equals("Green")) currColor = Color.green;
else if (s.equals("Blue")) currColor = Color.blue;
else if (s.equals("Black")) currColor = Color.black; }
public void mousePressed(MouseEvent e){
if ( (e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0) return;
lastX = e.getX(); lastY = e.getY(); }
public void mouseDragged(MouseEvent e){
if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0) return;
Graphics g = getGraphics();
g.setColor(currColor);
g.drawLine(lastX, lastY, e.getX(), e.getY());
lastX = e.getX(); lastY = e.getY(); }
public void mouseReleased(MouseEvent e){}
public void mouseClicked(MouseEvent e){}
public void mouseEntered(MouseEvent e){}
public void mouseExited(MouseEvent e){}
public void mouseMoved(MouseEvent e){}
}
Рис. 12.3. Приклад роботи з програмою рисування
190
191. При створенні класу-слухача scribble і реалізації интерфейсів MouseListener і MouseMotionListener
прийшлось реалізувати всі їх сім методів, хоча ми відслідкували тільки натискання і переміщення миші, і
нам потрібні були тільки методи mousePressed() і mouseDragged(). Для решти методів ми задали пусті
реалізації. Щоб полегшити задачу реалізації інтерфейсів, що мають більше одного метода, створені
класи-адаптери.
12.4. Класи-адаптери
Класи-адаптери представляють собою пусту реалізацію інтерфейсів-слухачів, що мають більше одного
методу. Їх імена складаються із імені події і слова Adapter. Наприкладр, для дії з мишею єсть два класи-
адаптери. Виглядають вони дуже просто:
public abstract class MouseAdapter implements MouseListener{
public void mouseClicked(MouseEvent e){}
public void mousePressed(MouseEvent e){}
public void mouseReleased(MouseEvent e){}
public void mouseEntered(MouseEvent e){}
public void mouseExited(MouseEvent e){}
}
public abstract class MouseMotionAdapter implements MouseMotionListener{
public void mouseDragged(MouseEvent e){}
public void mouseMoved(MouseEvent e){}
}
Замість того щоб реалізувати інтерфейс, можна розширять ці класи. Не бог знає що, але корисно для
створеннія безіменного вкладеного класу, як у нас і робилолося для закриття вікна. Там ми
використовували класс-адаптер WindowAdapter. Класів-адаптерів всього сім. Крім уже згадуваних трьох
класів, це класи ComponentAdapter, ContainerAdapter, FocusAdapter і KeyAdapter.
12.5. Обробка дій клавіатури
Подія KeyEvent відбуваєься в компоненті по любій із трьох причин:
• натиснута клавіша — ідентифікатор KEY_PRESSED;
• відпущена клавиша — ідентифікатор KEY_RELEASED;
• введений символ — ідентифікатор KEY_TYPED.
Остання подія виникає із-за того, що деякі символы вводятся натисканням декількох клавіш, наприклад,
заглавні літери вводяться комбінацією клавіш <Shift>+<літера>. Згадайте ще <Аlt>-введення в MS
Windows. Натискання функціональних клавіш, наприклад <F1>, не викликає подію KEY_TYPED.
Обробляються ці події трьома методами, описаними в інтерфейсі:
public interface KeyListener extends EventListener{
public void keyTyped(KeyEvent e);
public void keyPressed(KeyEvent e);
public void keyReleased(KeyEvent e);
}
• Метод e.getKeyChar() повертає символ Unicode типу char, звязаний з клавішею. Якщо з клавішею
не звязаний ніякий символ, то повертається константа CHAR_UNDEFINED.
• Метод e. getKeyCode () повертає код клавіши у вигляді цілого числа типу int.
В класі KeyEvent визначені коди всіх клавіш у вигляді констант, названих віртуальними кодами клавіш
(virtual key codes), наприклад, VK_FI, VK_SHIFT, VK_A, VK_B, VK_PLUS. Вони перечислені в документації
до класу KeyEvent. Фактичне значення віртуального коду залежить від мови і розкладки клавіатури. Щоб
узнати, яка клавіша була натиснута, треба порівнятиь результат виконання методу getKeyCode() з цими
константами. Якщо коду клавіші немає, як відбувається при настанні події KEY_TYPED, то повертається
значення VK_UNDEFINED. Щоб узнати, чи не натиснута одна або декілька клавіш-модифікаторів <Alt>,
191
192. <Ctrl>, <Meta>, <Shift>, треба скористатися унаслідуваним від класу inputEvent методом getModifiers() і
порівняти його результат з константами ALT_MASK, CTRL_MASK, META_MASK, SHIFT_MASK. Другий
спосіб — застосувати логічні методи isAltDown(), isControlDown(), isMetaDown(), isShiftDown(). Додамо в
лістинг 12.3 можливіст очистки поля введення tf після натискання клавіші <Esc>. Для цього перепишемо
вкладений клас-слухач TextMove:
class TextMove implements ActionListener, KeyListener{
public void actionPerformed(ActionEvent ae){
ta.append{tf .getText 0+"n");
}
public void keyPressed(KeyEvent ke) {
if (ke.getKeyCodeO == KeyEvent.VK_ESCAPE) tf.setText("");
}
public void keyReleased(KeyEvent ke){)}
public void keyTyped(KeyEvent ke){}
}
12.6. Подія TextEvent
Подія TextEvent настає тільки по одній причині — зміні тексту — і позначається ідентифікатором
TEXT_VALUE_CHANGED. Відповідний інтерфейс має тільки один метод:
public interface TextListener extends EventListener{
public void textValueChanged(TextEvent e) ;
}
Від аргументу е цього методу можна одержати посилку на обєкт-джерело події методом getSource(),
унаслідуваним від класу Eventobject, наприклад, так:
TextComponent tc = (TextComponent)e.getSpurce();
String s = tc.getText() ;
// Подальша обробка
12.7. Обробка дій з вікном
Подія windowEvent може настати по семи причинах:
• вікно відкрилось — ідентифікатор WINDOW_OPENED;
• вікно закрилось — ідентифікатор WINDOW_CLOSED;
• спроба закриття вікна — ідентифікатор WINDOW_CLOSING;
• вікно отримало фокус — ідентифікатор WINDOW_ACTIVATED;
• вікно втратило фокус — ідентифікатор WINDOW_DEACTIVATED;
• вікно звернулося в ярлик — ідентіфикатор WINDOW_ICONIFIED;
• вікно розвернулося — ідентифікатор WINDOW_DEICONIFIED.
Відповідний інтерфейс містить сім методів:
public interface WindowListener extends EventListener {
public void windowOpened(WindowEvent e);
public void windowClosing(WindowEvent e);
public void windowClosed(WindowEvent e);
public void windowlconified(WindowEvent e);
public void windowDeiconified(WindowEvent e);
public void windowActivated(WindowEvent e);
public void windowDeactivated(WindowEvent e); }
Аргумент е цих методів дає посилку типу Window на вікно-джерело методом e.getWindow(). Частіше
всього ці події використовуються для перерисовування вікна методом repaint() при зміні його розмірів і
для зупинки додатку при закртті вікна.
192
193. 12.8. Подія ComponentEvent
Дана подія настає в компоненті по четирьох причинах:
• компонент переміщається — ідентифiкатор COMPONENT_MOVED;
• компонент змінює розмір — ідентифiкатор COMPONENT_RESIZED;
• компонент видалений з екрана — ідентiфикатор COMPONENT_HIDDEN;
• компонент появився на екране — ідентiфикатор COMPONENT_SHOWN.
•
Відповідний інтерфейс містить опис четирьох методів:
public interface ComponentListener extends EventListener{
public void componentResized(ComponentEvent e);
public void componentMoved(CompоnentEvent e);
public void componentShown(ComponentEvent e);
public void componentHidden(ComponentEvent e);
}
Аргумент е методів цього інтерфейса представляє посилання на компонент-джерело події методом
e.getComponent().
12.9. Подія ContainerEvent
Ця подія настає по двох причинах:
• в контейнер додано компонент — ідентифікатор COMPONENT_ADDED;
• із контейнера видалено компонент — ідентифікатор COMPONENT_REMOVED.
Цим причинам відповідають методи интерфейса:
public interface ContainerListener extends EventListener{
public void componentAdded(ContainerEvent e) ;
public void componentRemoved(ContainerEvent e);
}
Аргумент е представляє посилку на компонент, чиє додавання або видалення із контейнера викликало
подію, методом e.getСhild(), і посилку на контейнер - джерело події методом e.getСontainer(). Звичайно
при настанні даної події контейнер переміщає свої компоненти.
12.10. Подія FocusEvent
Подія настає в компоненті, коли він отримує фокус введення — ідентифікатор FOCUS_GAINED, або
втрачає фокус - ідентифікатор FOCUS_LOST. Відповідний інтерфейс:
public interface FocusListener extends EventListener{
public void focusGainedtFocusEvent e) ;
public void focusLost(FocusEvent e) ;
}
Звичайно при втраті фокусу компонент перерисовується блідим кольором, для цього застосовується
метод brighter() класу Color, при отриманні фокусу становиться яскравіше, що досягається застосуванням
методу darker(). Це приходиться робити самостійно при створенні свого компонента.
12.11. Подія ItemEvent
Ця подія настаєт при виборі або відмові від вибору элемента в списку List, Сhoice або прапорця Сheckbox
і позначається ідентификатором ITEM_STATE_CHANGED. Відповідний інтерфейс дуже простий:
193
194. public interface ItemListener extends EventListener{
void itemStateChanged(ItemEvent e);
}
Аргумент е представляє посилку на джерело методом e.getІtemSelectable(), посилку на вибраний пункт
методом e.getІtem() у вигляді Оbject. Метод e.getStateChange() дозволяє уточнити, що відбулося:
значення SELECTED указує на те, що елемент був вибраний, значення DESELECTED — відбулася
відмова від вибору. В наступному уроці ми розглянемо приклади використання цієї події.
12.12. Подія AdjustmentEvent
Ця подія настає для смуги прокрутки ScroІІbar при всякій зміні її бігунка і позначається ідентифікатором
ADJUSTMENT_VALUE_CHANGED. Відповідний інтерфейс описує один метод:
public interface AdjustmentListener extends EventListener{
public void adjustmentValueChanged(AdjustmentEvent e);
}
Аргумент е цього метода представляє посилку на джерело події методом e.getAdjustable(), поточне
значення положення бігунка смуги прокрутки методом е.getvalue(), і спосіб зміни його значення методом
e.getAdjustmentType(), повертаючим наступні значення:
• UNIT__INCREMENT — збільшення на одну одиницю;
• UNIT_DECREMENT — зменшення на одну одиницю;
• BLOCK_INCREMENT — збільшення на один блок;
• BLOCK_DECREMENT — зменшення на один блок;
• TRACK — процес переміщення бігунка смуги прокрутки.
"Оживимо" программу створення кольору, приведену в лістинзі 10.4, додавши необхідні дії. Результат
цього приведений в лістинзі 12.5.
Лістинг 12.5. Програма створення кольору
import java.awt.*;
import java.awt.event.*;
class ScrollTest extends Frame{
Scrollbar sbRed = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255);
Scrollbar sbGreen = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255);
Scrollbar sbBlue = new Scrollbar(Scrollbar.VERTICAL, 127, 10, 0, 255);
Color c = new Color(127, 127, 127);
Label lm = new Label();
Button b1 = new Button("Apply");
Button b2 = new Button("Cancel");
ScrollTest(String s){ super(s);
setLayout(null);
setFont(new Font("Serif", Font.BOLD, 15));
Panel p = new Panel();
p.setLayout(null);
p.setBounds(10,50, 150, 240); add(p);
Label lc = new Label("Choice color");
lc.setBounds(20, 0, 120, 30); p.add(lc);
Label lmin = new Label("0", Label.RIGHT);
lmin.setBounds(0, 30, 30, 30); p.add(lmin);
Label lmiddle = new Label("127", Label.RIGHT);
lmiddle.setBounds(0, 120, 30, 30); p.add(lmiddle);
Label lmax = new Label("255", Label.RIGHT);
lmax.setBounds(0, 200, 30, 30); p.add(lmax);
sbRed.setBackground(Color.red);
sbRed.setBounds(40, 30, 20, 200); p.add(sbRed);
194
195. sbRed.addAdjustmentListener(new ChColor());
sbGreen.setBackground(Color.green);
sbGreen.setBounds(70, 30, 20, 200); p.add(sbGreen);
sbGreen.addAdjustmentListener(new ChColor());
sbBlue.setBackground(Color.blue);
sbBlue.setBounds(100, 30, 20, 200); p.add(sbBlue);
sbBlue.addAdjustmentListener(new ChColor());
Label lp = new Label("Pattern:", Label.CENTER);
lp.setBounds(240, 50, 100, 30); add (lp);
lm.setBackground(new Color(127, 127, 127));
lm.setBounds(220, 80, 120, 80); add(lm);
b1.setBounds(240, 200, 100, 30); add(b1);
b1.addActionListener(new ApplyColor());
b2.setBounds(240, 240, 100, 30); add(b2);
b2.addActionListener(new CancelColor());
setSize(400, 320); setVisible(true);
}
class ChColor implements AdjustmentListener{
public void adjustmentValueChanged(AdjustmentEvent e){
int red = c.getRed(), green = c.getGreen(), blue = c.getBlue();
if (e.getAdjustable() == sbRed) red = e.getValue();
else if (e.getAdjustable() == sbGreen) green = e.getValue();
else if (e.getAdjustable() == sbBlue) blue = e.getValue();
c = new Color(red, green, blue);
lm.setBackground(c);
}
}
class ApplyColor implements ActionListener {
public void actionPerformed(ActionEvent ae){
setBackground(c);
}
}
class CancelColor implements ActionListener {
public void actionPerformed(ActionEvent ae){
c = new Color(127, 127, 127);
sbRed.setValue(127);
sbGreen.setValue(127);
sbBlue.setValue(127);
lm.setBackground(c);
setBackground(Color.white);
}
}
public static void main(String[] args){
Frame f = new ScrollTest("Створення кольору ");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
195
196. 12.13. Декілька
слухачів одного
джерела
На початку цього
уроку, в лістингах
12.1—12.3, ми
привели приклад
класу TextMove,
слухаючого зразу
два компоненти:
поле введення tf
типу TextField у
кнопку b типу Button.
Частіше зустрічається протилежна ситуація — декілька слухачів слідкують за одним компонентом. В тому
ж прикладі кнопка b у відповідь на клікк по ній кнопки миші здійснювала ще і власні дії - вона
"вдавлювалась", а при відпусканні кнопки миші становилась "випуклою". В класі Button ці дії виконує peer-
объект. В класі FІowerButton лістинга 10.6 такі ж дії виконує метод paint() цоьго класу. В даній моделі
реалізований design pattern під назвою observer. До кожного компонента можна приєднати скільки
завгодно слухачів однієї й тієї ж події або різних типів подій. Одначе при цьому не гарантується якийсь
певний порядок їх виклику, хоча частіше всього слухачі викликаються в порядку написання методів
addXxxListener(). Якщо потрібно задати визначений порядок виклику слухачів для обробки події, то
прийдеться звертатися до них одинза одним або створювати обєкт, викликаючий слухачів у потрібному
порядку.
Посилки на приєднані методами addxxxbistener() слухачі можна було б зберігати в будь-якому класі-
коллекції, наприклад, Vector, але в пакет java. awt спеціально для цього введений клас
AWTEventMuІticaster. Він реалізує всі одинадцять інтерфейсів xxxListener, значить, сам являється
слухачем будь-якої події. Основу класу складають своєрідні статичні методи add(), написані для кожного
типу подій, наприклад:
add(ActionListener a, ActionListener b)
Своєрідність цих методів двояка: вони повертають посилку на той же інтерфейс, в даному випадку,
ActionListener, і приєднують обєкт а до обєкту b, створюючи сукупність слухачів одного і того ж типу. Це
дозволяє застосовувати їх подібно операціям а += . Заглянувши у вихідний текст класу Button, ви
196
197. побачите, що метод addActionListener() дуже простий:
public synchronized void addActionListener(ActionListener 1){
if (1 = null){ return; }
actionListener = AWTEventMuiticaster.add(actionListener, 1);
newEventsOnly = true;
}
Він додає до сукупності слухачів actionListener нового слухача 1. Для подій типу inputEvent, а саме,
KeyEvent і MouseEvent, єсть можливість зупинити подальшу обробку події методом consume(). Якщо
записати виклик цього метода в класс-слухач, то ні peer-обєкти, ні наступні слухачі не будуть обробляти
подію. Цим способом звичайно коистуються, щоб відмінить стандартні дії компонента, наприклад,
"вдавлювання" кнопки.
12.14. Диспетчеризація подій
Якщо вам знадобиться обробити просто дію миші, не важливо, натискання це, переміщення або ще що-
небудь, то прийдеться включати цю обробку у всі сім методів двох класів-слухачів подій миші. Цю роботу
можна полегшити, виконавши обробку не в слухачі, а на більш ранній стадії. Справа в тому, що перш ніж
подія дійде до слухача, вона обробляється кількома методами. Щоб в компоненті настала подія AWT,
повинна бути виконана хоча б одна із двох умов: до компонента приєднаний слухач або в конструкторі
компонента визначена мрожливість появи події методом enableEvents (). В аргументі цього метода через
операцію побітового додавання перечисляються константи класу AWTEvent, задаючі події, які можуть
настати в компоненті, наприклад:
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK |
AWTEvent.MOUSE_EVENT_MASK I AWTEvent.KEY_EVENT_MASK)
При настанні події створюється обєкт ідповідного класу xxxEvent. Метод dispatchEvent() визначає, де
появилась подія — в компоненті чи одному із його підкомпонентів, — і передає обєкт-подію методу
processEvent() компонента-джерела. Метод processEvent() визначає тип події і передає його
спеціалізованому методу processxxxEvent(). Ось початок цього методу:
protected void processEvent(AWTEvent e){
if (e instanceof FocusEvent){
processFocusEvent((FocusEvent)e);
}else if (e instanceof MouseEvent){
switch (e.getlDO ) {
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_ENTERED:
case MouseEvent.MOUSE_EXITED:
processMouseEvent((MouseEvent)e);
break/case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
processMouseMotionEvent((MouseEvent)e);
break; } }else if (e instanceof KeyEvent){
processKeyEvent((KeyEvent)e); }
// ...
Потім включається спеціалізований метод, наприклад, processKeyEvent(). Він-то і передає объект-подію
слухачу. Ось вихідний текст цього методу:
protected void processKeyEvent(KeyEvent e){
KeyListener listener = keyListener;
if (listener != null){ int id = e.getlDf);
switch(id){
case KeyEvent.KEYJTYPED: listener.keyTyped(e);
197
198. break;
case KeyEvent.KEY_PRESSED: listener.keyPressed(e);
break;
case KeyEvent.KEY_RELEASED: listener.keyReleased(e);
break;
}
}
}
Із цього опису видно, що коли ви хочете обробити будь-яку подію типу AWTEvent, то вам потрібно
перевизначити метод processEvent(), а якщо більш конкретну подію, наприклад, подію клавіатури, -
перевизначити більш конкретний метод processKeyEvent(). Коли ви не перевизначаєте весь метод цілком,
то не забудьте в кінці звернутися до методу суперкласу, наприклад, super.processKeyEvent(e);
Зауваження
He забувайте звертатися до методу processXxxEvent() суперкласу.
В наступному уроці ми застосуємо таке перевизначення в лістинзі 13.2 для виклику спливаючого меню.
12.15. Створення власної події
Ви можете створити власну подію і визначити джерело та умови її настання. В лістинзі 12.6 приведений
приклад створення події MyEvent. Подія MyEvent говорить про початок роботи програми (START) і
закінченні її роботи (STOP).
Лістинг 12.6, Створення власної події
// 1. Створюємо свій класс події:
public class MyEvent extends java.util.EventObjectf protected int id;
public static final int START = 0, STOP = 1;
public MyEvent(Object source, int id){
super(source);
this.id = id;
}
public int getID(){ return id; }
}
// 2. Описуємо Listener:
public interface MyListener extends java.util.EventListener{
public void start{MyEvent e);
public void stop(MyEvent e); }
// 3. В тілі потрібного класу створюємо метод fireEvent():
protected Vector listeners = new Vector();
public void fireEvent( MyEvent e){
Vector list = (Vector) listeners.clone();
for (int i = 0; i < list.sizeO; i++) {
MyListener listener = (MyListener)list.elementAt(i);
switch(e.getID() ) {
case MyEvent.START: listener.start(e); break;
case MyEvent.STOP: listener.stop(e); break;
}
}
}
Все, тепер при запуску програми робимо
fireEvent(this, MyEvent.START);
а по закінченню
198
199. fireEvent(this, MyEvent.STOP);
При цьому всі зареєстровані слухачі одержать екземпляри подій.
Лабораторна робота 11. Програмування реакції на натискання клавіш і маніпуляції з мишкою.
1. Побудова кривих другого порядку по означенню.
Події типу WindowEvent виникають тільки у вікнах: Frame, Dialog, FileDialog, Window.
Події типу TextEvent генеруються тільки в контейнерах Textcomponent, TextArea, TextField.
Події типу ActionEvent проявляються тільки в контейнерах Button, List, TextField.
Події типу ItemEvent виникають тільки в контейнерах Checkbox, Choice, List.
Нарешті, події типу AdjustmentEvent виникають тільки в контейнері Scrollbar.
Лістинг 12.1. Обробка події ActionEvent
import java.awt.*;
import java.awt.event.*;
class MyNotebook extends Frame{
MyNotebook(String title) {
super(title);
TextField tf = new TextField("Input a text", 50);
add(tf, BorderLayout.NORTH);
TextArea ta = new TextArea();
ta.setEditable(false);
add(ta);
Panel p = new Panel();
add(p, BorderLayout.SOUTH);
Button b = new Button("Transfer");
p.add(b);
tf.addActionListener(new TextMove(tf, ta));
b.addActionListener(new TextMove(tf, ta));
setSize(300, 200); setVisible(true);
}
public static void main(String[] args){
Frame f = new MyNotebook(" Обробка ActionEvent");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
class TextMove implements ActionListener{
private TextField tf;
private TextArea ta;
TextMove(TextField tf, TextArea ta){
this.tf = tf; this.ta = ta;
}
public void actionPerformed(ActionEvent ae){
ta.append(tf.getText()+"n");
}
}
. В лістинзі 12.1 в методах addActionListener() створюються два экземпляри класу TextMove — для
199
200. прослуховування поля tf і для прослуховування кнопки b. Можна створити один экземпляр класу
TextMove, він буде прослуховувать обидва компоненти:
TextMove tml = new TextMove(tf, ta);
tf.addActionListener(tml);
b.addActionListener(tml);
Лістинг 12.2. Самообробка події ActionEvent
import java.awt.*;
import java.awt.event.*;
class MyNotebook extends Frame implements ActionListener{
private TextField tf;
private TextArea ta;
MyNotebook(String title){
super(title) ;
tf = new TextField ("Input Text **", 50) ;
add(tf, BorderLayout.NORTH);
ta = new TextArea();
ta.setEditable(false);
add(ta);
Panel p = new Panel();
add(p, BorderLayout.SOUTH);
Button b = new Button("Transfer");
p.add(b);
tf.addActionListener(this) ;
b.addActionListener(this) ;
setSize(300, 200); setVisible(true) ; }
public void actionPerformed(ActionEvent ae){
ta.append(tf.getText()+"n"); }
public static void main(String[] args){
Frame f = new MyNotebook(" Обробка ActionEvent");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Лістинг 12.3. Обробка вкладеним класом
import java.awt.*;
import java.awt.event.*;
class MyNotebook extends Frame{
private TextField tf;
private TextArea ta;
MyNotebook(String title){
super(title);
tf = new TextField("Input Text", 50);
add(tf, BorderLayout.NORTH);
ta = new TextArea();
ta.setEditable(false);
add (ta);
Panel p = new Panel();
add(p, BorderLayout.SOUTH);
Button b = new Button("Transfer");
p.add(b);
200
201. tf.addActionListener(new TextMove());
b.addActionListener(new TextMove());
setSize(200, 200);
setVisible(true);
}
public static void main(String[] args){
Frame f = new MyNotebook(" Обробка ActionEvent");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit (0);
}
});
}
// Вкладений клас
class TextMove implements ActionListener{
public void actionPerformed(ActionEvent ae){
ta.append(tf.getText()+"n");
}
}
}
201
202. Програмування у Java
Урок 13. Створення меню
13.1. Меню
В контейнер типу Frame закладена можливість установки стандартного рядка меню (menu bar),
розташованого нижче рядка заголовка, як показано на рис. 13.1. Цей рядок - обєкт класу MenuBar. Все,
що потрібно зробити для установки рядка меню в контейнері Frame - це створити обєкт класу MenuBar і
звернутися до методу setMenuBar():
Frame f = new Frame("Приклад меню");
MenuBar mb = new MenuBar();
f.setMenuBar(mb);
Якщо імя mb не знадобиться, можна сумістить два останніх звернення до методів:
f.setMenuBar(new MenuBar());
Розуміється, рядок меню ще пустий й пункти меню не створені. Кожний элемент рядка меню - випадаюче
меню (drop-down menu ) - це обєкт класу Menu. Створити ці обєкти і занести їх в рядок меню не
складніше, ніж створити рядок меню:
Menu mFile = new Menu("Файл");
mb.add(mFile);
Menu mEdit = new Menu("Правка");
mb.add(mEdit);
Menu mView = new Menu("Вид");
mb.add(mView);
Menu mHelp = new Menu("Справка");
mb.setHelpMenu(mHelp);
і т. д. Елементи розташовуються зліва направо в порядку звернень до методів add(), як показано на рис.
13.1. В багатьох графічних системах прийнято меню Справка (Help) притискати до правого краю рядка
меню. Це досягається зверненням до методу setHeІpMenu(), але фактичнее положення меню Справка
визначається графічною оболонкою.
Рис. 13.1. Система меню
Потім визначаємо кожне випадаюче меню, створюючи його пункти. Кожний пункт меню — це обєкт класу
MenuІtem. Схема його створення і додавання до меню точно така ж, як і самого меню:
202
203. MenuІtem create = new Menuitem("Створити");
mFile.add(create);
MenuІtem open = new Menuitem("Відкрити...");
mFile.add(open);
і т. д.
Пункти меню будуть озташовані зверху вниз в порядку звернення до методів add(). Часто пункти меню
обєднуються в групи. Одна група від іншої відокремлюється горизонтальною рискою. На рис. 13.1 риска
проведена між командами Відкрити і Відправити. Ця риска створюється методом addSeparator() класу
Menu або визначається як пункт меню з написом спеціального виду — дефісом:
mFile.addfnew Menuitem("-"));
Цікаво, що класс Menu розширює класс MenuІtem, а не навпаки. Це означає, що меню само являється
пунктом меню, і дозволяє задавати меню в якості пункта іншого меню, тим самим організючи вкладені
підменю:
Menu send = new Menu("Відправити");
mFile.add(send);
Тут меню send додається в меню mFile як один із його пунктів. Підменю send заповняється пунктами
меню як звичайне меню. Часто команди меню створюються для вибору із них певних можливостей,
подібно компонентам Сheckbox. Такі пункти можна виділити кліком кнопки миші або відмінити виділення
повторним кліком. Ці команди — обєкти класу CheckboxMenuItem:
CheckboxMenuItem disk = new CheckboxMenuItem("Диск A:", true);
send.add(disk);
send.add(new CheckboxMenuItem("Архів")) ;
і т. д.
Все, що получилось в результаті перечислених дій, показано на рис. 13.1. Деякі графічні оболонки, але не
MS Windows, дозволяють створювати відокремлювані (tear-off) меню, які можна переміщати по екрану. Це
указується в конструкторі
Menu(String label, boolean tearOff)
Якщо tearoff == true і графічна оболочка уміє сстворювати відокремлюване меню, то воно буде створено.
В противному випадку цей аргумент просто ігнорується.
Нарешті, треба призначити дії командам меню. Команди меню типу MenuІtem породжують події типу
ActionEvent, тому потрібно приєднати до них обєкт класу-слухача як до звичайних компонентів, записавши
щось подібне до
create.addActionListener(new SomeActionEventHandler())
open.addActionListener(new AnotherActionEventHandler())
Пункти типу CheckboxMenuItem породжують події типу ItemEvent, тому треба звертатися до обєкту-
слухача цієї події:
disk.addltemListener(new SomeltemEventHandler())
Дуже часто дії, записані в командах меню, викликаються не тільки кліком кнопки миші, але і "гарячими"
клавішами-акселераторами (shortcut), діючими частіше всього при натисканні клавіші <Ctrl>. На екрані в
пунктах меню, яким призначені "гарячі" клавіші, появляються підказки виду Ctrl+N, Ctrl+O, як на рис. 13.1.
"Гаряча" клавіша визначається обєктом класу MenuShortcut і вказується в його конструкторі константою
класу KeyEvent, наприклад:
MenuShortcut keyCreate = new MenuShortcut(KeyEvent.VK_N);
203
204. Після цього "гарячою" буде комбінація клавіш <Ctrl>+<N>. Потім отриманий обєкт указується в
конструкторі класу MenuІtem:
Menuitem create = new Menuitem("Створити", keyCreate);
Натискання <Ctrl>+<N> буде викликати вікно створення. Ці дії, зрозуіло, можна сумістити, наприклад,
Menuitem open = new Menultern("Відкрити...",
new -MenuShortcut(KeyEvent.VK_O));
Можна додати ще натискання клавіші <Shift>. Дія пункта меню буде викликатися натисканням комбінації
клавіш <Shift>+<Ctrl>+<X>, якщо скористуватися другим конструктором:
MenuShortcut(int key, boolean useShift) з аргументом useShift == true.
Програма рисування, створена в лістинзі 12.4 і показана на рис. 12.3, явно перевантажена кнопками.
Перенесемо їх дії в пункти меню. Додамо можливість маніпуляції файлами і команду завершення роботи.
Це зроблено в лістинзі 13.1. Клас scribble не змінився і в лістинзі не наведений. Результат показаний на
рис. 13.2.
Лістинг 13.1. Програма рисування з меню
import java.awt.*;
import java.awt.event.*;
public class MenuScribble extends Frame{
public MenuScribble(String s) { super(s);
setSize(400,150);
setVisible(true);
ScrollPane pane = new ScrollPane();
pane.setSize(300, 300);
add(pane, BorderLayout.CENTER);
Scribble scr = new Scribble(this, 500, 500);
pane.add(scr);
MenuBar mb = new MenuBar();
setMenuBar(mb);
Menu f = new Menu("File");
Menu v = new Menu("Draw");
mb.add(f); mb.add(v);
MenuItem open = new MenuItem("Open...", new MenuShortcut(KeyEvent.VK_0));
MenuItem save = new MenuItem("Save", new MenuShortcut(KeyEvent.VK_S));
MenuItem saveAs = new MenuItem("Save As...");
MenuItem exit = new MenuItem("Exit", new MenuShortcut(KeyEvent.VK_Q));
f.add(open); f.add(save); f.add(saveAs);
f.addSeparator(); f.add(exit);
open.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
FileDialog fd = new FileDialog(new Frame(),
" Open File", FileDialog.LOAD);
fd.setVisible(true);
}
});
204
205. saveAs.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
FileDialog fd = new FileDialog(new Frame(),
" Save File As", FileDialog.SAVE);
fd.setVisible(true);
}
});
exit.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
System.exit(0);
}
});
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit (0);
}
});
Menu c = new Menu("Color");
v.add(c);
MenuItem clear = new MenuItem("Clear",new MenuShortcut(KeyEvent.VK_D));
v.add(clear);
MenuItem red = new MenuItem("Red");
MenuItem green = new MenuItem("Green");
MenuItem blue = new MenuItem("Blue");
MenuItem black = new MenuItem("Black");
c.add(red); c.add(green); c.add(blue); c.add(black);
red.addActionListener(scr);
green.addActionListener(scr);
blue.addActionListener(scr) ;
black.addActionListener(scr) ;
clear.addActionListener(scr) ;
}
public static void main(String[] args){
Frame fr = new MenuScribble (" "Рисувалка" з меню");
}
}
class Scribble extends Component implements ActionListener, MouseListener,
MouseMotionListener{
protected int lastX, lastY, w, h;
protected Color currColor = Color.black;
protected Frame f;
public Scribble(Frame frame, int width, int height){
f = frame;
w = width;
h = height;
enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
addMouseListener(this);
addMouseMotionListener(this); }
public Dimension getPreferredSize(){
return new Dimension(w, h); }
205
206. public void actionPerformed(ActionEvent event){
String s = event.getActionCommand();
if (s.equals ("Clear")) repaint();
else if (s.equals ("Red")) currColor = Color.red;
else if (s.equals("Green")) currColor = Color.green;
else if (s.equals("Blue")) currColor = Color.blue;
else if (s.equals("Black")) currColor = Color.black; }
public void mousePressed(MouseEvent e){
if ( (e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0) return;
lastX = e.getX(); lastY = e.getY(); }
public void mouseDragged(MouseEvent e){
if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0) return;
Graphics g = getGraphics();
g.setColor(currColor);
g.drawLine(lastX, lastY, e.getX(), e.getY());
lastX = e.getX(); lastY = e.getY(); }
public void mouseReleased(MouseEvent e){}
public void mouseClicked(MouseEvent e){}
public void mouseEntered(MouseEvent e){}
public void mouseExited(MouseEvent e){}
public void mouseMoved(MouseEvent e){}
}
Рис. 13.2. Програма рисування з меню
13.2. Спливаюче меню
Спливаюче меню (popup menu) появляється звичайно при натисканні або відпусканні правої або средньої
кнопки миші і являється контекстним (context) меню. Його команди залежать від компонента, на якому
була натиснута кнопка миші. В мові Java спливаюче меню — обєкт класу Рорupmenu. Цей клас росширяє
клас Menu, а значить, наслідує всі властивості меню і пункту меню Menultem. Спливаюче меню
приєднується не до рядка меню типу MenuBar або до меню типу Menu в якості підменю, а до певного
компонента. Для цього в класі Сomponent єсть метод add(PopupMenu menu).
У деяких компонентів, наприклад TextFieІd і TextArea, уже існує спливаюче меню. Подібні меню не можна
перевизначати.
Приєднати спливаюче меню можна тільки до одного компонента. Якщо треба використовувати спливаюче
меню з кількома компонентами в контейнері, то його приєднують до контейнера, а потрібний компонент
визначають за допомогою метода getСomponent() класу MouseEvent, як показано в лістинзі 13.2.
206
207. Крім унаслідуваних властивостей і методів, в класі PopupMenu єсть метод show (Component comp, int x, int
у), показуючий спливаюче меню на экрані так, що його лівий верхній кут роташовується в точці (х, у) в
системі координат компонента соmр. Частіше всього це компонент, на якому натиснута кнопка миші, і
повертається методом getСomponent(). Компонент comp повинен бути всередині контейнера, до якого
приєднують меню, інакше виникне виключна ситуація.
Спливающее меню появляється у MS Windows при відпусканні правої кнопки миші, а в інших графічних
системах можуть бути іші правила. Щоб врахувати цю різницю, в клас MouseEvent ведено логічний метод
isPopupTrigger(), показуючий, що наступивша подія миші викликає появу спливаючого меню. Його
потрібно викликати при настанні всякої події миші, щоб перевіряти, чи не являється вона сигналом до
появи спливаючого меню, тобто звернення до методу show(). Було б занадто незручно включати таку
перевірку в усі сім методів класів-слухачів подій миші. Тому метод IsPopupTrigger() краще викликати в
методі processMouseEvent().
Пропонуємо ще раз програму рисування із лістинга 12.4, ввівши в класс scribble спливаюче меню для
вибора кольору рисування і очистки вікна і змінивши обробку подійй миші. Для простоти видалимо рядок
меню, хоча його можна було залишити. Результат показаний в лістинзі 13.2, а на рис. 13.3 — вигляд
спливаючого меню в MS Windows.
Лістинг 13.2. Програма рисування з спливаючим меню
import java.awt.*;
import java.awt.event.*;
public class PopupMenuScribble extends Frame{
public PopupMenuScribble(String s){ super (s) ;
ScrollPane pane = new ScrollPane();
pane.setSize(300, 300);
add(pane, BorderLayout.CENTER);
Scribble scr = new Scribble(this, 500, 500);
pane.add(scr);
addWindowListener(new WinClose());
pack ();
setVisible(true);
}
class WinClose extends WindowAdapter{
public void windowClosing(WindowEvent e){
System.exit(0);
}
}
public static void main(String[] args){
new PopupMenuScribble(" "Рисувалка" з спливаючим меню");
}
}
class Scribble extends Component implements ActionListener{
protected int lastX, lastY, w, h;
protected Color currColor = Color.black;
protected Frame f;
protected PopupMenu c;
public Scribble(Frame frame, int width, int height){
f = frame; w = width; h = height;
enableEvents(AWTEvent.MOUSE_EVENT_MASK|AWTEvent.MOUSE_MOTION_EVENT_MASK);
c = new PopupMenu ("Color") ;
add(c);
MenuItem clear = new MenuItem("Clear", new MenuShortcut(KeyEvent.VK_C));
MenuItem red = new MenuItem("Red");
MenuItem green = new MenuItem("Green");
MenuItem blue = new MenuItem("Blue");
MenuItem black = new MenuItem("Black");
c.add(red); c.add(green); c.add(blue);
207
208. c.add(black); c.addSeparator(); c.add(clear);
red.addActionListener(this);
green.addActionListener(this);
blue.addActionListener(this);
black.addActionListener(this);
clear.addActionListener(this);
}
public Dimension getPreferredSize()
{
return new Dimension(w, h);
}
public void actionPerformed(ActionEvent event){
String s = event.getActionCommand();
if (s.equals("Clear")) repaint();
else if (s.equals("Red")) currColor = Color.red;
else if (s.equals("Green")) currColor = Color.green;
else if (s.equals("Blue")) currColor = Color.blue;
else if (s.equals("Black")) currColor = Color.black;
}
public void processMouseEvent(MouseEvent e){
if (e.isPopupTrigger())
c.show(e.getComponent (), e.getX(), e.getY());
else if (e.getID() == MouseEvent.MOUSE_PRESSED){
lastX = e.getX(); lastY = e.getY(); }
else super.processMouseEvent(e); }
public void processMouseMotionEvent(MouseEvent e){
if (e.getID() == MouseEvent.MOUSE_DRAGGED){
Graphics g = getGraphics();
g.setColor(currColor) ;
g.drawLine(lastX, lastY, e.getX(), e.getY());
lastX = e.getX(); lastY = e.getY();
}
else super.processMouseMotionEvent(e);
}
}
Рис. 13.3. Програма рисування з спливаючим меню
208
209. Лабораторна робота 12. Аплікація з використанням меню
1. Оюєднайте програми побудови кривих другого порядку в одну з викликом процедури побудови кривої з
меню.
209
210. 1
Урок 14
Аплети
• Передача параметрів
• Параметри тега <applet>
• Відомості про аплет
• Зображення і звук
• Слідкування за процесом завантаження
• Клас MediaTracker
• Захист від аплету
• Заключення
14.1. Аплет
До сих пір ми створювали додатки (applications), працюючі самостійно (standalone) в JVM під управлінням
графічної оболонки операційної системи. Ці додатки мали власне вікно верхнього рівня типу Frame,
зареєстроване у віконному менеджері (window manager) графічної оболонки. Крім додатків, мова Java
дозволяє створювати аплети (applets). Це програми, що працюють в середовищі іншої програми -
браузера. Аплету не потрібне вікно верхнього рівня - їм служить вікно браузера. Вони не запускаються
JVM — їх завантажує браузер, котрий сам запускає JVM для виконання аплету. Ці особливості
відбиваються на написанні програми аплета.
З точки зору мови Java, аплет — це всяке розширення класу Applet, котрий, в свою чергу, розширяє клас
Panel. Таким чином, аплет - це панель спеціального виду, контейнер для розміщення компонентів з
додатковими властивостями і методами. Менеджером розміщення компонентів по замовчуванню, як і в
класі Panel, служить FІowLayout. Класс Applet знаходиться в пакеті java.applet, в якому крім нього єсть
тільки три інтерфейси, реалізовані в браузері. Треба відмітити, що не всі браузери реалізують ці
інтерфейси повністю.
Оскільки JVM не запускає аплет, відпадає необхідність в методi main(), його немає в аплетах. В аплетах
рідко зустрічається конструктор. Справа в тому, що при запуску першого створюється його контекст. Під
час виконання конструктора контекст ще не сформований, тому не всі початкові значення вдається
визначити в конструкторі. Початкові дії, зазвичай виконувані в конструкторі і методі main(), в аплеті
записуються в метод init() класу Applet. Цей метод автоматачно запускається виконуючою системою Java
браузера зразу ж післе завантаження аплета. Ось як він виглядає у вихідному коді класу Applet:
public void init(){}
Не густо! Метод init() не має аргументів, не повертає значення і повинен перевизначатися в кожному
аплеті — підкласі класу Applet. Зворотні дії — завершення роботи, звільнення ресурсів — записуються
при необхідності в метод destroy(), також виконуваний автоматично при вивантаженні аплету. В класі
Applet єсть пуста реалізація цього методу.
Крім методів init() і destroy() в класі Applet присутні ще два пустих методи, виконувані автоматично.
Браузер повинен звертатися до методу start() при кожній появі аплету на екрані і звертатися до методу
stop(), коли аплет зникаєт з eкрана. В методі stop() можна визначити дії, зупиняючі роботу аплета, в
методі start() — відновлюючі її. Треба зразу ж відмітити, що не всі браузери звертаються до цих методів
як повинно. Так, перший із розглянутих нижче аплетів HelloWorld.html мені не вдалося запустити
браузером Internet Explorer, прийшлося скористатися власним Java браузером appletviewer, як пояснено
далі.
Роботу указаних методів можна пояснити простим житійським прикладом. Приїхавши весною на дачу, ви
прокладаєте водопровідні труби, прикручуєте крани, протягуєте шланги - виконуєте метод init() для своєї
зрошувальної системи. Після цього, приходячи на ділянку, включаєте крани — запускаєте метод start(), а
виходячи, виключаєте їх — виконуєте метод stop(). Нарешті, восени ви розбираєте зрошувальну систему,
відкручуєте крани, просушуєте і укладуєте водопровідні труби - виконуєте метод destroy().
210
211. 2
14.2. Найпростіший аплет
Перераховані вище методи init(), start(), stop(), destroy() не є обовязковими при написанні простих аплетів,
які не займають багато памяті, як свідчить наступний приклад. В лістинзі 14.1 записаний простенький
аплет, виконуючий вічну програму HelloWorІd.
Лістинг 14.1. Аплет HelloWorld
import java.awt.*;
import java.applet.*;
public class HelloWorld extends Applet{
public void paint(Graphics g){
g.drawstring("Hello, XXI century World!", 10, 30);
}
}
Ця програма записується в файл HelloWorld.java і компілюється зазвичай: javac HelloWorld.java.
14.3. Виконання аплета
Компілятор створює файл HelloWorld.class, але скористатися для його виконання інтерпретатором java
теперь не можна немає методу main(). Замість інтерпретації треба дать вказівку браузеру для запуску
аплета. Всі вказівки браузеру даються помітками, тегами (tags), на мові HTML (HyperText Markup
Language). Зокрема, вказівка на запуск аплета даєтсья в тезі <applet>. В ньому обовязково задається імя
файла з класом аплета параметром code, ширина width і висота height панелі аплета в пікселях. Повністю
текст HTML для нашого аплета приведений в лістинзі 14.2.
Лістинг 14.2. Файл HTML для завантаження аплета HelloWorІd
<html>
<head><title> Applet</title></head> <body>
<br>
<applet code = "HeІІoWorld.class" width = "200" height = "100">
</applet>
</body>
</html>
Цей текст заноситься в файл з розсширенням html або htm, наприклад HelloWorld.html. Імя файла
довільне, ніяк не звязано з аплетом або класом апплета. Обидва файли — HelloWorld.html і
HelloWorld.class - поміщаються в один каталог на сервері, і файл HelloWorld.html завантажується в
браузер, який може знаходитися в будь-якому місці Internet. Браузер, проглядаючи HTML-файл, виконає
тег <appІet> і завантажить аплет. Після завантаження аплет зявиться у вікні браузера, як показано на рис.
14.1.
В цьому простому прикладі можна помітити ще дві особливості аплетів. По-перше, розмір аплета
задається не в ньому, а в тезі <applet>. Це дуже зручно, можна змінювати розмір аплета, не компілюючи
його заново. Можна організувати аплет невидимим, зробивши його розміром в один піксель. Крім того,
розмір аплета дозволяється задати в процентах по відношенню до розміру вікна браузера, наприклад,
<applet code = "HelloWorld.class" width = "100%" height = "100%"> .
По-друге, як видно на рис. 14.1, у аплета сірий фон. Такий фон був у перших браузерів, і аплет не
виділявся із тексту у вікні браузера. Тепер у браузерах прийнято білий фон, його можна установити
звичайним для компонентів методом setBackground(Color.white), звернувшись до нього в методs init(). В
склад JDK будь-якої версії входить програма appІetviewer. Це найпростіший браузер, призначений для
запуску аплетів з метою налаштування. Якщо під рукою немає Internet-браузера, можна скористуватися
ним. AppІetviewer запускається з командного рядка:
appІetviewer HelloWorld.html
211
212. 3
Рис. 14.2. appІetviewer показує аплет HelloWorld.
Ви звернули увагу, що внизу вікна знаходиться напис Applet started , якого ми ніде не програмували. Якби
ми змінили колір вікна аплета, а це ми зробимо згодом, то побачили б, що напис Applet started
знаходиться на окремій смужці, так званій status bar. В ній програма запуску аплета appletviewer і
інформує користувача, про стан аплета, в даному випадку він стартував.
14.4. Приклад більш складного аплета
Як тільки що було сказано, у нижньому рядку браузера — рядку стану (status bar) — відображаються дані
про завантаження файлів. Аплет може записати в нього будь-який рядок str методом showStatus(String
str). В лістинзі 14.3 приведено аплет, записуючий в рядок стан браузера "біжучий рядок ", а в лістинзі 14.4
— відповідний HTML-файл. Перш ніж аналізувати наступну програму, ознайомтеся з методами substring()
і CharAt() класу String.
Лістинг 14.3. Біжучий рядок в рядку стану браузера
// Файл RunningString.Java
import java.awt.*;
import java.applet.*;
public class RunningString extends Applet{
private boolean go; //розширюємо клас Applet полем go
public void start(){// реалізуємо пустий метод start() класу Applet
go = true;
sendMessage("This string is printed automatically by applet "); /* реалізацію
метода sendMessage(String s) дивись нижче*/
}
public void sendMessage(String s){// реалізуємо власний метод sendMessage(String s)
String s1 = s+" "; // До String s додається пробіл
while(go){ //Якщо аплет стартував, то String s висвічується у status bar
showStatus(s);
try{ /*блок try .. catch на випадок збою, при використанні класу Thread є
обовязковим, бажаючі можуть прочитати про це в уроці 17, який ми, за браком часу,
цього 2008 року розглянути не зможемо*/
Thread.sleep(200); /*так робиться пауза – метод sleep()статичний, тому обєкт класу
Thread не створюємо*/
}
212
213. 4
catch(Exception e){}
s = s1.substring(1)+s.charAt(0); /*Перший символ рядка переноситься в його кінець –
починається формуватися копія рядка позаду оригіналу – ефект рухомого рядка.*/
s1 =s; // з одержаним рядком операція повторюється в циклі while
}
} //Закінчується процедура формування копії рядка
public void stop(){ /* метод stop() зараз не рекомендують застосовувати, так як при
роботі в мережі він може вплинути на стан і інших моніторів. Аплет спрацює і без
цього методу - перевірте*/
go = false;
}
}
Лістинг 14.4. Файл RunningString.html
<html>
<head> <title> Applet</title></head>
<body>
<br>
<applet code = "RunningString.class" width = "1" height = "1">
</applet>
</body>
</html>
На жаль , немає строгого стандарту на виконаня аплетів, і браузери можуть запускати їх по-різному.
Програма appІetviewer здатна показати аплет не так, як браузери. Приходиться перевіряти аплети на всіх
наявних браузерах, добиваяючись однакового виконання.
14.5. Аплет, створюючий вікно
Приведемо більш складний приклад. Аплет showWindow створює вікно someWindow типу Frame, в якому
розташоване поле введення типу TextFieІd. В нього вводиться текст, і після натискання клавіші <Enter>
переноситься в поле введення аплета. В аплеті присутня кнопка. Після кліку кнопкою миші по ній вікно
someWindow то зникає з екрана, то знову зявляється на ньому. Програма приведена в лістингах 14.5 і
14.6, результат — на рис. 14.3.
Лістинг 14.5. Аплет, створюючий вікно
// Файл ShowWindow.java
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
public class ShowWindow extends Applet{
private SomeWindow sw = new SomeWindow();
private TextField tf = new TextField(30);
private Button b = new Button("Hide");
public void init(){
add(tf); add(b); sw.pack();
b.addActionListener(new ActShow());
sw.tf.addActionListener(new ActShow());
}
public void start(){ sw.setVisible(true);}
public void stop(){ sw.setVisible(false);}
public void destroy(){
sw.dispose() ; sw = null; tf = null; b = null;
}
213
214. 5
public class ActShow implements ActionListener{
public void actionPerformed(ActionEvent ae){
if (ae.getSource() = sw.tf)
tf.setText(sw.tf .getText()) ;
else if (b.getActionCommand() == "Show"){
sw.setVisible(true);
b.setLabel("Hide") ; }
else{
sw.setVisible(false);
b.setLabel("Show");
}
}
}
}
class SomeWindow extends Frame{
public TextField tf = new TextField(50);
SomeWindow(){
super(" Вікно введення");
add(new Label("Input, please, your name"), "North");
add(tf, "Center");
}
}
Лістинг 14.6. Файл ShowWindow.html
<html>
<head><title> ShowWindow Applet</title></head>
<body>
<br>
<applet code = "ShowWindow.class" width = "400" height = "50">
</applet>
</body>
</html>
Рис. 14.3. Аплет, створюючий вікно
Зауваження по налаштуванню
Браузери поміщають завантажені аплети в свій кэш, тому після кліку кнопкою миші по кнопці Refresh або
214
215. 6
Reload запускається стара копія апплета із кэша. Для завантаження нової копії треба при кліку по кнопці
Refresh в IE (Internet Explorer) тримати натиснутою клавішу <Ctrl>, а при кліку по кнопці Reload в NC
(Netscape Communicator) — клавішу <Shift>. Інколи і це не допомагає. Не врятовує навіть перезапуск
браузера. Тоді треба очистити обидва кэші - і дисковий, і кэш в памяті. В IE це виконується кнопкою Delete
Files у вікнs, викликаному набором команди Tools | Internet Options. B NC необхідно відкрити вікно Cache
командою Edit | Preferences | Advanced.
При запуску додатку інтерпретатором java із командного рядка в нього можна передати параметри у
вигляді аргумента метода main(String [] args). В аплети також передаються параметри, але іншим
шляхом.
14.3. Передача параметрів
Передача параметрів в аплет відбувається за допомогою тегів <param>, розташованих між відкриваючим
тегом <appІet> і закриваючим тегом </appІet> в HTML-файлі. В тегах <param> указується назва параметра
name і його значення value. Передамо, наприклад, в наш аплет HeІІoWorІd параметри шрифту. В лістинзі
14.7 показано змінений файл HelloWorld.html.
Лістинг 14.7. Параметри для передачі в аплет
<html>
<head><title> Applet</title></head>
<body>
<br>
<applet code = "HelloWorld.class" width = "400" height = "50"> /*Далі йде те, що
буде передано в аплет програмою appletviewer*/
<param name = "fontName" value = "Serif">
<param name = "fontStyle" value = "2">
<param name = "fontSize" value = "30">
</applet>
</body>
</html>
В аплеті для прийому кожного параметра треба скористатися методом getParameter (String name) класу
Applet, повертаючому рядок типу String. В якості аргумента цього методу задається значення параметра
name у вигляді рядка, причому тут не розрізняється регістр літер, а метод повертає значення параметра
value теж у вигляді рядка.
Зауваження по налаштуванню
Оператори System.out.println(), зазвичай записувані в аплет для налаштування, виводять указані в них
аргументи в спеціальне вікно браузера Java Console. Спочатку треба установити можливість показу цього
вікна. В Internet Explorer це робиться установкою прапорця Java Console enabled набором команд Tools |
Internet Options | Advanced. Після перезапуску IE в меню View зявляється команда Java Console.
В лістинзі 14.8 показано перероблений аплет HelloWorld. В ньому призначений білий фон, а шрифт
установлюється з параметрами, добутими із HTML-файла.
Лiстинг 14.8. Апплет, приймаючий параметри
import java.awt.*;
import java.applet.*;
public class HelloWorld extends Applet{ public void init(){
setBackground(Color.white);
String font = "Serif";
int style = Font.PLAIN, size = 10; /*наступні дані аплет отримає від програми
appletviewer з файлу HelloWorld.html*/
font = getParameter("fontName");
style = Integer.parseInt(getParameter("fontStyle"));
215
216. 7
size = Integer.parseInt(getParameter("fontSize"));
setFont(new Font(font, style, size));
}
public void paint(Graphics g){
g.drawString("Hello, XXI century World!", 10, 30);
}
}
Порада
Сподіваючись на те, що параметри будуть задані в HTML-файлі, все-таки присвойте початкові значення
змінним в аплеті, як це зроблено в лістинзі 14.8. На рис. 14.4 показано працюючий аплет.
Рис. 14.4. Аплет із зміненим шрифтом
Правила хорошого тону рекомендують описати параметри, передавані апплету, у вигляді масиву, кожний
елемент якого — масив із трьох рядків, відповідаючий одному параметру. Дана структура
представляється у вигляді "імя", "тип", "опис". Для нашого прикладу можна написати:
String[][] pinfo = {
{"fontName", "String", "font name"},
{"fontStyle", "int", "font style"},
{"fontSize", "int", "font size"}
};
Потім перевизначається метод getParameterІnfo(), повертаючий указаний масив. Це пустий метод класу
Applet. Будь-який обєкт, бажаючий узнати, що передать аплету, может викликати цей метод. Для нашого
прикладу перевизначення вигляає так:
public String[][] getParameterInfо(){
return pinfo;
}
Крім того, правила хорошого тону приписують перевизначити метод getAppletІnfо(), повертаючий рядок, в
який записано імя автора, версія апплета і інші дані про аплет, котрі ви хотіли б представити всім
бажаючим. Наприклад:
public String getAppletInfo(){
return "MyApplet v.1.5 P.S.Ivanov";
}
Подивимось тепер, які ще параметри можна задати в тезі <appІet>.
14.4. Параметри тега <applet>
Перечислимо всі параметри тега <applet>.
Обовязкові параметри:
216
217. 8
• code — URL-адреса файла з класом аплета або архівного файла;
• width і height — ширина і висота аплета в пікселях.
Необовязкові параметри:
• codebase — URL-адреса каталога, в якому розташований файл класу аплета. Яущо цей параметр
відсутній, браузер буде шукати файл в тому ж каталозі, де розміщений відповіднийий HTML-файл;
• archive — файли всіх класів, складаючих аплет, можуть бути упаковані архіватором ZIP або
спеціальним архіватором JAR в один або декілька архівних файлів. Параметр задає URL-адреси
цих файлів через кому;
• align — вирівнювання апплета у вікні браузера. Цей параметр має одне із наступних значень:
ABSBOTTOM, ABSMIDDLE, BASELINE, BOTTOM, CENTER, LEFT, MIDDLE, RIGHT, TEXTTOP,
TOP;
• hspace і vspace — горизонтальні і вертикальні поля, відокремлюючі аплет від інших обєктів у вікні
браузера в пікселях;
• download — задає порядок завантаження зображень аплетом. Імена зображень перечисляються
через кому в порядку завантаження;
• name - імя аплета. Параметр потрібний, якщо завантажуються декілька аплетів з однаковими
значеннями code і codebase;
• style — інформація про стиль CSS (Cascading Style Sheet); title — текст, відображуваний в процесі
виконання аплета;
• alt — текст, що виводиться замість аплета, якщо браузер не може завантажити його;
• mayscript — не має значення. Це слово указує на те, що аплет буде звертатися до тексту
JavaScript.
Між тегами <applet> і </applet> можна написати текст, який буде виведений, якщо браузер не зможе
зрозуміти тег <applet>. Ось повний приклад:
<applet name = "AnApplet" code = "AnApplet.class"
archive = "anapplet.zip, myclasses.zip"
codebase = "http://www.some.com/public/applets"
width = "300" height = "200" align = "TOP"
vspace = "5" hspace = "5" mayscript
alt = "If you have a Java-enabled browser, you would see an applet here.">
<hr>If your browser recognized the applet tag, you would see an applet here.<hr>
</applet>
Порада
Обовязково упаковуйте всі класи аплета в zip- і rаr-архіви і указуйте їх в параметрі archive в HTML-файлі.
Це значно прискорить завантаження аплета. Слід ще сказати, що, починаючи з версії HTML 4.0, єсть тег
<object>, призначений для завантаження і аплетів, і інших обєктів, наприклад, ActiveX. Крім того, деякі
браузери можуть використовувати для завантаження аплетів тег <embed>. Ми уже згадували, що при
завантаженні аплета браузер створює контекст, в якому збирає всі дані, необхідні для виконання аплета.
Деякі дані із контексту можна передати в аплет.
14.5. Дані про оточення аплета
Метод getCodeBase() повертає URL-адресу каталога, в кякому лежить файл класу аплета.
Метод getDocumentBase() повертає URL-адресу каталога, в якому лежить HTML-файл, викликавший
аплет. Браузер реалізує інтерфейс AppletСontext, що знаходиться в пакеті java.applet. Аплет може
отримати посилку на цей інтерфейс методом getAppletContext(). За допомогою методів getApplet (String
name) і getApplets() інтерфейса AppletСontext можна отримати посилку на указаний аргументом name
аплет або на всі аплети, завантажені в браузер.
Метод showDocument(URL address) завантажує в браузер HTML-файл з адресою address.
217
218. 9
Метод showDocument (URL address, String target) завантажує файл у фрейм, указаний другим аргументом
target. Цей аргумент може приймати наступні значення:
• _seif — те ж вікно і той же фрейм, в якому працює аплет;
• _parent — батьківський фрейм аплета;
• _top — фрейм верхнього рівня вікна аплета;
• _blank — нове вікно верхнього рівня;
• name — фрейм або вікно з іменем name, якщо воно не існує, то буде створено.
14.6. Зображення і звук
Ззображення в Java — це обєкт класу image, представляючий прямокутний масив пікселів. Його можуть
показати на екрані логічні методи drawІmage() класу Graphics. Ми розглянемо їх детально в наступному
уроці, а поки що нам знадобляться два логічні методи:
• drawlmage(Image img, int x, int y, ImageObserver obs)
• drawImage(Image img, int x, int y, int width, int height, ImageObserver obs)
Методи починають рисувать зображення, не чекаючи закінчення завантаження зображення img. Більше
того, завантаження не почнеться, поки не викликаний метод drawlmage(). Методи повертають false, поки
завантаження не закінчиться. Аргументи (х, у) задають координати лівого верхнього кута зображення img;
width і height — ширину і висоту зображення на екрані; obs — посилання на обєкт, реалізуючий інтерфейс
ImageObserver, слідкуючий за процесом завантаження зображення. Останньому аргументу можна дати
значення this.
Перший метод задає на екрані такі ж розміри зображення, як і у обєкта класу image, без змін. Одержати ці
розміри можна методами getWidth(), getHeight() класу Image. Інтерфейс ImageObserver, реалізований
класом Component, а значить, і класом Applet, описує тільки один логічний метод imageUpdate(),
виконуваний при кожній зміні зображення. Саме цей метод побуджує перерисовувати компонент на екрані
при кожній його зміні. Подивимося, як його можна використовувати в процесі завантаження файлів із
Internet.
14.7. Відслідковування процесу завантаження
Якщо ви хоч раз бачили, як зображення завантажується із Internet, то помітили, що воно появляється на
екрані по частинах по мері завантаження. Це відбувається в тому випадку, коли системна властивість
awt.image. incrementalDraw має значення true. При поступанні кожної порції зображення браузер викликає
логічний метод imageUpdate() інтерфейса ImageObserver. Аргументи цього методу містить інформацію
про процес завантаження зображення img. Розглянемо їх:
imageUpdate(Image img, int status, int x, int y, int width, int height);
Аргумент status містить інформацію про завантаження у вигляді одного цілого числа, яке можна порівняти
з наступними константами інтерфейса ImageObserver:
• WIDTH — ширина уже завантаженої частини зображеняя відома, і може бути одержана із
аргументу width;
• HEIGHT — висота уже завантаженої частини зображеняя відома, і може бути одержана із
аргументу height;
• PROPERTIES — властивості зображення уже відомі, їх можна одержати методом getProperties()
класу Image;
• SOMEBITS — одержані пікселі, достатні для рисування маштабованої версії зображення;
аргументи x, y, width, height визначені;
• FRAMEBITS — одержаний наступний кадр зображення, що містить декілька кадрів; аргументи x, y,
width, height не визначені;
• ALLBITS — всезображення одержано, аргументи x, y, width, height не містять інформації;
• ERROR — завантаження зупинено, рисування перервано, визначений біт ABORT;
• ABORT — завантаження перервано, рисування призупинено до приходу наступної порції
зображення.
218
219. 10
Ви можете перевизначити цей метод в своєму аплеті і використовувати його аргументи для слідкування
за процесом завантаження і визначення моменту повного завантаження.
Інший спосіб відслідкувати закінчення завантаження - скористатися методами класу MediaTracker. Вони
дозволяють перевірити, чи не закінчено завантаження, або призупинити роботу аплета до закінчення
звантаження. Один екземпляр класу MediaTracker може слідкувати за завантаженням декількох
зареєстрованих в ньому зображень.
14.8. Клас MediaTracker
Спочатку конструктором MediaTracker (Component comp) створюється обєкт класу для указаного
аргументом компонента. Аргумент конструктора частіше всього this. Потім методом addlmage(Image img,
int id) реєструється зображення img під порядковим номером id. Декілька зображень можна зареєструвати
під одним номером. Після цього логічними методами checkID(int id), checkID(int id, boolean load) іcheckAll()
перeвіряється, чи завантажено зображення з порядковим номером id або всі зареєстровані зображення.
Методи повертають true, якщо зображення уже завантажено, false — в противному випадку. Якщо
аргумент load рівний true, то відбувається завантаження всіх ще не завантажених зображень. Методи
statusID(int id), statusID(int id, boolean load) і statusALL повертають ціле число, яке можна порівняти із
статичними константами COMPLETE, ABORTED, ERRORED. Нарешті, методи waitForID(int id) і waitForAll()
чекають закінчення завантаження зображення. В наступному уроці в лістинзі 15.5 ми застосуємо ці
методи для очікування завнтаження зображення.
Зображення, що знаходиться в обєкті класу image можна створити безпосередньо по пікселям, а можна
одержати із графічного файла, типу GIF або JPEG, одним із двох методів класу Applet:
• getІmage(URL address) — задається URL-адреса графічного файла;
• getImage(URL address, String fileName) — задається адреса каталога address і імя графічного
файла filename.
Аналогічно, звуковий файл в аплетах представляється у вигляді обєкта, реалізуючого інтерфейс
AudioСІip, і може бути отримний із файла типу AU, AIFF, WAVE або MIDI одним із трьох методів класу
Applet з такими ж аргументами:
getAudioClip(URL address)
getAudioClip(URL address, String fileName)
newAudioClip(URL address)
Останній метод статичний, його можна використовувать не тільки в аплетах, але і в додатках. Інтерфейс
AudioСlip із пакета java.applet дуже простий. В ньому всього три методи без аргументів. Метод play()
програє мелодію один раз. Метод loop() нескінчено повторяє мелодію. Метод stop() зупиняє програвання.
Цей інтерфейс реалізується браузером. Звичайно, перед програвнням звукових файлів браузер повинен
бути звязаний із звуковою системою компьютера.
В лістинзі 14.9 приведено простий приклад завантаження зображення і звуку із файлів, що знаходяться в
тому ж каталозі, що і HTML-файл. На рис. 14.5 показано, як виглядає зображення, збільшене в два рази.
Лістинг 14.9, Звук і зображення в аплеті
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
public class SimpleAudioImage extends Applet{
private Image img;
private Audioclip ac;
public void init(){
img = getImage(getDocumentBase(), "gifIcon.gif");
ac = getAudioClip(getDocumentBase(), "spacemusic.au"); }
public void start (){ ac.loop();
219
220. 11
}
public void paint(Graphics g){
int w = img.getWidth(this), h = img.getHeight(this);
g.drawImage(img, 0, 0, 2 * w, 2 * h, this); }
public void stop() { ac.stop(); }
}
Рис. 14.5. Виведення зображення у звуковому супроводі
Жаль, що ви не можете з тексту лекції насолоджуватися звуками космічної музики.
Перед виведенням на eкран зображення можна перетворити, але про це поговоримо в наступному уроці.
Як бачите, аплету в браузері дозволено дуже мало. Це не випадково. Аплет, що зявився в браузері
звідкисьо із Internet, може натворити немало бід. Він може бути викликаний із файла з цікавим текстом,
невидимо обшукати файловую систему і викрасти секретні дані, або, навпаки, відкрити вікно, таке ж, в яке
ви вводите пароль, і перехватить його. Тому браузер повідомляє при завнтаженні аплета: "Applet started",
а в рядку стану вікна, відкритого аплетом, зявляється напись: "Warning: Applet Window". Але це не єдиний
захист від аплета. Розглянемо цю проблему детальніше.
14.9. Захист від аплета
Браузер може взагалі відмовитися від завантаження аплетів. В Netscape Communicator це робиться за
допомогою прапорця Enable Java у вікні, викликаному командою Edit | Preferences | Advanced, в Internet
Explorer — у вікні після вибору команди Tools | Internet Options | Security. В такому випадку говорити
більше немає про що. Якщо ж браузер завантажує аплет, то створює йому обмеження, так звану
"пісочницю" (sandbox), в яку попадає аплет, але вийти з якої не може. Кожний браузер створює свої
обмеження, але як правило вони заключаються в тому, що аплет:
• не може звертатися до файлової системи машини, на якій він виконується, навіть для читання
файлів або перегляду каталогів;
• може звязатися па мережі тільки з тим сайтом, з якого він був завантажений;
• не може прочитати системні властивості, як це робить, наприклад, додаток в лістинзі 6.4;
• не може друкувати на принтері, підключеном до того компютер, на якому він виконується;
• не може користуватися буфером обміну (clipboard);
• не може запустить додаток методом ехес();
• не може використовувати "рідні" методи або завантажити бібліотеку методом load();
• не може зупинити JVM методом exit();
• не может створювати класи в пакетах java.*, а класи пакетів sun.* не може навіть завантажувати.
Браузери могжуть посилити або ослабити ці обмеження, наприклад, дозволити локальним аплетам,
завантаженим з тієї ж машини, де вони виконуються, доступ до файлової системи. Найменшим
обмеженням підлягають довірені (trusted) аплети, що мають электронний підпис за допомогою класів із
пакетів java.security.*.
При створені додатку, що завантажує аплети, необхідо забезпечити засоби перовірки аплета і задати
обмеження. Їх дає в розпорядження клас securityManager. Екземпляр цього класу або його потомка
220
221. 12
установлюється в JVM при запуску віртуальної машини статичним методом
setSecurityManager(SecurityManager sm) класу System. Звичайні додатки не можуть використовувати
даний метод. Кожний браузер розширяє клас SecurityManager по-своєму, установлюючи ті чи інші
обмеження. Єдиний екземпляр цього класу створюється при запуску JVM в браузері і не може бути
змінений.
Заключення
Аплети були спершу практичним застосуванням Java. За перші два роки існування Java були написані
тисячі дуже цікавих і красивих аплетів, ожививших WWW. Маса аплетів розкидана по Internet, хороші
приклади аплетів зібрані в JDK в каталозі demoapplets. В JDK увійшов цілий пакет java.applet, в який
фірма SUN збиралась заносити класи, що розвивають і покращують аплети. Із збільшенням швидкості і
покращенням якості компютерних мереж значення апплетів сильно упало. Тепер вся обробка даних,
раніше виконувана аплетами, переноситься на сервер, браузер тільки завантажує і показує результати
цієї обробки, становиться "тонким клієнтом".
З іншого боку, зявилось багато спеціалізованих програм, в тому числі написаних на Java, завантажуючих
інформацію із Internet. Така можливість єсть зараз у всіх музичних і відеопрогравачах. Фірма SUN більше
не розвивє пакет java.applet. В ньому так і залишився один клас і три інтерфейси. В бібліотеку Swing
увійшов класс JApplet, розширяючий клас Applet. В ньому єсть додаткові можливості, наприклад, можна
установити систему меню. Він здатний використовувати всі класи бібліотеки Swing. Але більшість
браузерів ще не мають Swing в своєму складі, тому приходиться завантажувати класи Swing із сервера
або включати їх в jar-архів разом з класами аплета.
Лабораторна робота 13. Створення аплетів.
1. Переробити програму побудови кривих другого порядку в аплет.
221
222. 13
Лістинг 14.1. Аплет HelloWorld
import java.awt.*;
import java.applet.*;
public class HelloWorld extends Applet{
public void paint(Graphics g){
g.drawstring("Hello, XXI century World!", 10, 30);
} }
Лістинг 14.3. Біжучий рядок в рядку стану браузера
// Файл RunningString.Java
import java.awt.*;
import java.applet.*;
public class RunningString extends Applet{
private boolean go; //розширюємо клас Applet полем go
public void start(){// реалізуємо пустий метод start() класу Applet
go = true;
sendMessage("This string is printed automatically by applet "); /* реалізацію метода
sendMessage(String s) дивись нижче*/
}
public void sendMessage(String s){// реалізуємо власний метод sendMessage(String s)
String s1 = s+" "; // До String s додається пробіл
while(go){ //Якщо аплет стартував, то String s висвічується у status bar
showStatus(s);
try{ /*блок try .. catch на випадок збою, при використанні класу Thread є
обовязковим, бажаючі можуть прочитати про це в уроці 17, який ми, за браком
часу, цього 2008 року розглянути не зможемо*/
Thread.sleep(200); /*так робиться пауза – метод sleep()статичний, тому обєкт класу
Thread не створюємо*/
}
catch(Exception e){}
s = s1.substring(1)+s.charAt(0); /*Перший символ рядка переноситься в його кінець
– починається формуватися копія рядка позаду оригіналу – ефект рухомого
рядка.*/
s1 =s; // з одержаним рядком операція повторюється в циклі while
}
} //Закінчується процедура формування копії рядка
public void stop(){ /* метод stop() зараз не рекомендують застосовувати, так як при
роботі в мережі він може вплинути на стан і інших моніторів. Аплет спрацює і без
цього методу - перевірте*/
222
223. 14
go = false;
}}
Лістинг 14.5. Аплет, створюючий вікно
// Файл ShowWindow.java
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
public class ShowWindow extends Applet{
private SomeWindow sw = new SomeWindow();
private TextField tf = new TextField(30);
private Button b = new Button("Hide");
public void init(){
add(tf); add(b); sw.pack();
b.addActionListener(new ActShow());
sw.tf.addActionListener(new ActShow());
}
public void start(){ sw.setVisible(true);}
public void stop(){ sw.setVisible(false);}
public void destroy(){
sw.dispose() ; sw = null; tf = null; b = null;
}
public class ActShow implements ActionListener{
public void actionPerformed(ActionEvent ae){
if (ae.getSource() = sw.tf)
tf.setText(sw.tf .getText()) ;
else if (b.getActionCommand() == "Show"){
sw.setVisible(true);
b.setLabel("Hide") ; }
else{
sw.setVisible(false);
b.setLabel("Show");
} }}}
class SomeWindow extends Frame{
public TextField tf = new TextField(50);
SomeWindow(){
super(" Вікно введення");
add(new Label("Input, please, your name"), "North");
add(tf, "Center");
} }
223
224. 15
Лiстинг 14.8. Апплет, приймаючий параметри
import java.awt.*;
import java.applet.*;
public class HelloWorld extends Applet{ public void init(){
setBackground(Color.white);
String font = "Serif";
int style = Font.PLAIN, size = 10; /*наступні дані аплет отримає від програми
appletviewer з файлу HelloWorld.html*/
font = getParameter("fontName");
style = Integer.parseInt(getParameter("fontStyle"));
size = Integer.parseInt(getParameter("fontSize"));
setFont(new Font(font, style, size));
}
public void paint(Graphics g){
g.drawString("Hello, XXI century World!", 10, 30);
}
}
Лістинг 14.9, Звук і зображення в аплеті
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
public class SimpleAudioImage extends Applet{
private Image img;
private Audioclip ac;
public void init(){
img = getImage(getDocumentBase(), "gifIcon.gif");
ac = getAudioClip(getDocumentBase(), "spacemusic.au"); }
public void start (){ ac.loop();
}
public void paint(Graphics g){
int w = img.getWidth(this), h = img.getHeight(this);
g.drawImage(img, 0, 0, 2 * w, 2 * h, this); }
public void stop() { ac.stop(); }
}
224
225. 16
Лістинг 14.2. Файл HTML для завантаження аплета HelloWorІd
<html>
<head><title> Applet</title></head> <body>
<br>
<applet code = "HeІІoWorld.class" width = "200" height = "100">
</applet> </body> </html>
Лістинг 14.4. Файл RunningString.html
<html>
<head> <title> Applet</title></head>
<body>
<br>
<applet code = "RunningString.class" width = "1" height = "1">
</applet> </body> </html>
Лістинг 14.6. Файл ShowWindow.html
<html>
<head><title> ShowWindow Applet</title></head>
<body>
<br>
<applet code = "ShowWindow.class" width = "400" height = "50">
</applet> </body> </html>
Лістинг 14.7. Параметри для передачі в аплет
<html>
<head><title> Applet</title></head>
<body>
<br>
<applet code = "HelloWorld.class" width = "400" height = "50"> /*Далі йде те, що
буде передано в аплет програмою appletviewer*/
<param name = "fontName" value = "Serif">
<param name = "fontStyle" value = "2">
<param name = "fontSize" value = "30">
</applet> </body> </html>
225
226. Урок 15
Зображення і звук
• Модель обробки "поставщик-споживач"
• Класи-фільтри
• Як віділити фрагмент зображення
• Як змінить колір зображення
• Як переставити пікселі зображення
• Модель обробки прямим доступом
• Перетворення зображення в Java 2D
• Афінне перетворення зображення
• Зміна інтенсивності зображення
• Зміна складових кольору
• Створення різноманітних ефектів
• Анімація
• Покращення зображення подвійною буферизацією
• Звук
• Програвання звуку в Java 2
• Синтез і запис звуку Java 2
Як уже згадувалось в попередньому уроці, зображення в Java — це обєкт класу Image. Там же показано,
як в аплетах застосовуються методи getlmage() для створення цих обєктів із графічних файлів. Додатки
теж можуть застосовувати аналогічні методи getІmage() класу Toolkit із пакета java.awt з одним
аргументом типу String або URL. Звернення до цих методів із компонента виконується через метод
getToolkit () класу Component і виглядає так:
Image img = getToolkit().getlmage("С:imageslvanov.gif");
В загальному випадку звернення можна зробити через статичний метод getDefaultToolkit() класу Toolkit:
Image img = Toolkit.getDefaultToolkit().getlmage("С:imageslvanov.gif");
Але, крім цих методів, клас Toolkit містить пять методів createlmage(), повертаючих посилання на обєкт
типу Іmage:
• createlmage (String filsName) — створює зображення із вмісту графічного файлу filename;
• createlmage (URL address) — створює зображення із вмісту графічного файлу по адресу address;
• createlmage (byte [] imageData) — створює зображення із масиву байтів imageData, дані в якому
повинні мати формат GIF або JPEG;
• createlmage (byte [] imageData, int offset, int length) — створює зображення із частини масива
imageData, що починається з індекса offset довжиною length байтів;
• createlmage (ImageProducer producer) — створює зображення, одержане від поставщика producer.
Останній метод єсть і в класі Сomponent. Він використовує модель "поставщик-споживач" і вимагає
детального пояснення.
15.1. Модель обробки "поставщик-споживач"
Дуже часто зображення перед виведенням на екран підлягає обробці: змінюються кольори окремих
пікселів або цілих частин зображення, виділяються і перетворюються якісь фрагменти зображення. В
бібліотеці AWT застосовуються дві моделі обробки зображення. Одна модель реалізує давно відому в
програмуванні спільну модель "поставщик-споживач" (Producer-Consumer). Згідно цієї моделі один обєкт,
"поставщик", генерує сам або перетворює отриману із іншого місця продукцію, в даному випадку, набір
пікселів, і передає іншим обєктам. Ці обєкти, "споживачі", приймають продукцію і теж перетворюють її при
необхідності. Тільки після цього створюється обєкт класу Іmage і зображення виводиться на екран. У
одного поставщика може бути декілька споживачів, котрі повинні бути зареєстровані поставщиком.
Поставщик і споживач активно взаємодіють, звертаючись до методів один одного.
226
227. В AWT ця модель описана в двох інтерфейсах: ImageProducer і ImageConsumer пакета java. awt. image.
Інтерфейс ImageProducer описує пять методів:
• addConsumer(ImageConsumer ic) - реєструє споживача ic; removeConsumer (ImageConsumer ic) -
скасовує реєстрацію;
• isConsumer( ImageConsumer ic) — логічний метод, перевіряє, чи зареєстрований споживач ic;
• startProduction (ImageConsumer ic) — реєструє споживача ic і починає поставку зображення всім
зареєстрованим споживачам;
• requestTopDownLeftRightResend (ImageConsumer ic) — використовується споживаем для того,
щоб затребувати зображення ще раз в порядку "зверху-вниз, зліва-направо" для методів обробки,
застосовуючи саме такий порядок.
З кожним екземпляром класу Іmage звязаний обєкт, реалізуючий інтерфейс ImageProducer. Його можна
отримати методом getSource() класу Image. Найпростіша реалізація интерфейса ImageProducer - клас
MemoryІmageSource — створює пікселі в оперативній памяті по масиву байтів або цілих чисел. Спочатку
створюється масив pix, що містить колір кожної точки. Потім одним із шести конструкторів створюється
обєкт класу MemoryІmageSource. Він може бути оброблений споживачем або прямо перетворений у тип
Image методом createlmage ().
В лістинзі 15.1 наведена проста программа, що виводить на екран квадрат розміром 100x100 пікселів.
Лівий верхній кут квадрата синій, лівий нижній — червоний, правий верхній — зелений, а до центру
квадрата кольори змішуються.
Лістинг 15.1. Зображення, побудоване по точках
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
class InMemory extends Frame {
private int w = 100, h = 100;
private int[] pix = new int[w * h];
private Image img;
InMemory(String s)
{ super(s);
int i = 0;
for (int y = 0; y < h; y++){
int red = 255 * y / (h - 1);
for (int x = 0; x < w; x++){
int green = 255 * x / (w - 1) ;
pix[i++] = (255 << 24)|(red << 16)|(green << 8)| 128; } }
setSize(250, 200);
setVisible(true);
}
public void paint(Graphics gr){
if (img == null)
img = createImage(new MemoryImageSource(w, h, pix, 0, w));
gr.drawImage(img, 50, 50, this);
}
public static void main(String[] args){
Frame f= new InMemory(" Зображення в памяті");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){ System.exit (0);}
});
227
228. }
}
В лістинзі 15.1 в конструктор класу-постачальника MemoryІmageSource (w, h, pix, о, w) заноситься ширина
w і висота h зображення, масив pix, зміщення в цьому масиві о і довжина рядка w. Споживачем служить
зображення img, яке створюється методом createlmage () і виводится на екран методом drawlmage(img,
50, 50, this). Лівий верхній кут зображення img розташовується в точці (50, 50) контейнера, а останній
аргумент this показує, що роль imageObserver відіграє сам класс InMemory. Це заставляє включить в
метод paint() перевірку if (img == null), інакше зображення буде постійно перерисовуваться. Другий спосіб
уникнути цього — перевизначити метод imageupdate(), про що говорилось в уроці 14, просто написавши в
ньому return true. Рис. 15.1 демонструє виведення цієї програми.
Рис. 15.1. Зображення, створене по точках
Інтерфейс imageConsumer описує сім методів, найважливішими із яких являються два методи setPixeІs().
Перший:
setPixels(int x, int y, int width, int height, ColorModel model, byte[] pix, int offset, int scansize);
Другий метод відрізняється тільки тим, що масив pix містить елементи типу int.
Рис. 15.2. Класи, реалізуючі модель "постачальник-споживач"
До цих методів звертається постачальник для передачі пікселів споживачу. Передається прямокутник
шириною width і висотою height із заданим верхнім лівим кутом (х, у), заповнений пікселями із масива pix,
починаючи з індекса offset. Кожен рядок займає scansize елементів масиву pix. Колір пікселів
визначається в кольоровій моделі model (звичайно це модель RGB). На рис. 15.2 показана ієрархія класів,
228
229. реалізуючих модель "постачальник-споживач".
15.2. Класи-фільтри
Інтерфейс imageConsumer не має рації реалізовувать, використовується його готова реалізація - клас
imageFilter. Незважаючи на назву, цей клас не робить ніякої фільтрації, він передає зображення без зміни.
Для перетворення зображення даний клас належить розширити, перевизначивши метод setPixeІs().
Результат перетворення належить передати споживачу, роль якого відіграє поле consumer цього класу. В
пакеті java. awt. image єсть чотири розширення класу ImageFilter:
• CropImageFilter (int x, int у, int w, int h) — виділяє фрагмент зображення, указаний в приведеному
конструкторі;
• RGBimageFilter - дозволяеє змінювати окремі пікселі; це абстрактний клас, він вимагає
розширення і перевизначення свого методу filterRGB();
• RepІicateScaІeFilter (int w, int h) — змінює розміри зображення на указані в приведеному
конструкторі, дублюючи рядки і/або стовпці при збільшенні розмірів або прибираючи деякі із них
при зменшенні;
• AreaAveragingScaleFilter (int w, int h) — розширення попереднього класу; використовує більш
складний алгоритм зміни розмірів зображення, усереднюючий значення сусідніх пікселів.
Застосовуються ці класи разом із іншим класом-постачальником, реалізуючим інтерфейс ImageProducer -
класом FilteredlmageSource. Цей класс перетворює уже готову продукцію, отриману від другого
постачальника producer, використовуючи для перетворення обєкт filter класу-фільтра imageFilter або його
підкласу. Обидва обєкта задаються в конструкторі
FilteredlmageSource(ImageProducer producer, ImageFilter filter)
Все це здається дуже заплутаним, але схема застосування фільтрів завжди одна і та ж. Вона показана в
лістингах 15.2—15.4.
15.3. Як виділити фрагмент зображення
В лістинзі 15.2 виділяється фрагмент зображення і виводится на екран у збільшеному вигляді. Крім того,
нижче виводяться зображення, збільшені зі допомогною класів RepІicateScaІeFiІter і
AreaAveragingScaleFilter.
Лістинг 15.2. Приклади маштабування зображення
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
class CropTest extends Frame{
private Image img, cropimg, replimg, averimg;
CropTest(String s){ super (s) ;
// 1. Створюємо зображення — обєкт класу Image
img = getToolkit().getImage("javalogo52x88.gif");
// 2. Створюємо обєкти-фільтри:
// а) виділяємо лівий верхній кут розміром 30x30
CropImageFilter crp = new CropImageFilter(0, 0, 30, 30);
// б) збільшуємо зображення в два рази простим методом
ReplicateScaleFilter rsf = new ReplicateScaleFilter(104, 176);
// в) збільшуємо зображення в два рази з усредненням
AreaAveragingScaleFilter asf = new AreaAveragingScaleFilter(104, 176);
// 3. Створюємо зміну зображення
cropimg = createImage(new FilteredImageSource(img.getSource(), crp));
replimg = createImage(new FilteredImageSource(img.getSource(), rsf));
averimg = createImage(new FilteredImageSource(img.getSource(), asf));
setSize(400, 350); setVisible(true); }
public void paint(Graphics g){ g.drawImage(img, 10, 40, this);
229
230. g.drawImage(cropimg, 150, 40, 100, 100, this);
g.drawImage(replimg, 10, 150, this);
g.drawImage(averimg, 150, 150, this);
}
public static void main(String[] args){
Frame f= new CropTest(" Маштабування");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
На рис. 15.3 зліва зверху показано початкове зображення, справа — збільшений фрагмент, внизу —
зображення, збільшене двома способами.
Рис. 15.3. Маштабування зображення
15.4. Як змінити колір зображення
В лістинзі 15.3 змінюються кольори кожного пікселя зображення. Це досягається просто зсувом rgb » 1
вмісту пікселя на один біт вправо в методі fiІterRGB(). При цьому підсилюється червона складова кольору.
Метод fiІterRGB() перевизначений в розширенні coІorFilter класу RGBImageFilter.
Лістинг 15.3. Зміна кольору всіх пікселів
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
class RGBTest extends Frame{
private Image img, newimg;
RGBTest(String s){
super(s);
img = getToolkit().getImage("javalogo52x88.gif");
230
231. RGBImageFilter rgb = new ColorFilter();
newimg = createImage(new FilteredImageSource(img.getSource(), rgb));
setSize(400, 350);
setVisible(true); }
public void paint(Graphics g){
g.drawImage(img, 10, 40, this);
g.drawImage(newimg, 150, 40, this); }
public static void main(String[] args){
Frame f= new RGBTest(" Зміна кольору");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
class ColorFilter extends RGBImageFilter{ ColorFilter(){
canFilterIndexColorModel = true; }
public int filterRGB(int x, int y, int rgb){
return rgb >> 1;
}
}
15.5. Як переставити пікселі зображення
В лістинзі 15.4 визначається перетворення пікселів зображення. Створюється новий фільтр - розширення
shiftFilter класу imageFilter, здвигаючий зображення циклічно вправо на указане в конструкторі число
пікселiв. Все, що для цього потрібно, - це перевизначити метод setPixels().
Лістинг 15.4. Циклічний зсув зображення
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
class ShiftImage extends Frame{ private Image img, newimg;
ShiftImage(String s){ super(s);
// 1. Одержуємо зображення із файла
img = getToolkit().getImage("javalogo52x88.gif");
// 2. Створюємо екземпляр фільтра
ImageFilter imf = new ShiftFilter(26);
// Зсув на 26 пікселів
// 3. Одержуємо нові пікселі за допомогою фільтра
ImageProducer ip = new FilteredImageSource(img.getSource(), imf);
// 4. Створюємо нове зображення
newimg = createImage(ip);
setSize(300, 200);
231
232. setVisible(true) ; }
public void paint(Graphics gr){
gr.drawImage(img, 20, 40, this);
gr.drawImage(newimg, 100, 40, this); }
public static void main(String[] args){
Frame f= new ShiftImage(" Циклічний зсув зображення");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0); }
});
}
}
// Клас-фільтр
class ShiftFilter extends ImageFilter{
private int sh;
// Зсув на sh пікселів вправо.
public ShiftFilter(int shift){ sh = shift; }
public void setPixels(int x, int y, int w, int h,
ColorModel m, byte[] pix, int off, int size){
for (int k = x; k < x+w; k++){
if (k+sh <= w)
consumer.setPixels(k, y, 1, h, m, pix, off+sh+k, size);
else
consumer.setPixels(k, y, 1, h, m, pix, off+sh+k-w, size);
}
}
}
Як видно із листинга 15.4, перевизначення методу setPixels() заключається в тому, щоб змінити аргументи
цього методу, переставивши, тим самим, пікселі зображення, і передати їх споживау consumer — полю
класу imageFiІter методом setPixels() споживача. На рис. 15.4 показано результат виконання цієї
програми. Інша модель обробки зображеня введена в Java 2D. Вона названа моделлю прямого доступу
(immediate mode model).
Риc. 15.4. Перестановка пікселів зображення
15.6. Модель обробки прямим доступом
Подібно до того, як замість класу Graphics система Java 2D використовує його розширення Graphics2D,
описане в уроці 9, замість класу Іmage в Java 2D використовується його розширення — клас
BufferedІmage. В конструкторі цього классу
Bufferedlmage(int width, int height, int imageType)
232
233. задаються розміри зображення і спосіб зберігання точок — одна із констант:
TYPE_INT_RGB
TYPE_INT_ARGB
TYPE_INT_ARGB_PRE
TYPE_INT_BRG
TYPE_BYTE_GRAY
TYPE_BYTE_BINARY
TYPE_BYTE_INDEXED
TYPE_3BYTE_BRG
TYPE_4BYTE_ABRG
TYPE_4BYTE_ABRG_PRE
TYPE_USHORT_GRAY
TYPE_USHORT_565_RGB
TYPE_USHORT_555_RGB
Як бачите, кожний піксель може займати 4 байти — INT, 4BYTE, або 2 байта — USHORT, або 1 байт —
BYTE. Може використовуватися кольорова модель RGB, або додана альфа-складова — ARGB, або
задани інший порядок розташування кольорових складових — BRG, або задані градації сірого кольору —
GRAY. Кожна складова кольору може займати один байт, 5 бітів або 6 бітів. Екземпляри класу
BufferedІmage рідко створюються конструкторами. Для їх створення частіше звертаються до методів
createІmage () класу Сomponent з простим приведенням типу:
BufferedІmage bi = (Bufferedlmage)createІmage(width, height)
При цьому екземпляр bi отримує характеристики компонента: колір фону і колір рисування, спосіб
зберігання точок. Розташування точок в зображенні регулюється класом Raster або його підкласом
WritabІeRaster. Ці класи задають систему координат зображення, представляють доступ до окремих
пікселів методами getPixeІ(), дозволяють виділяти фрагменти зображення методами getPixeІs(). Клас
WritabІeRaster додатково дозволяє змінювати окремі пікселі методами getPixeІ() або цілі фрагменти
зображення методами setPixels() і setRect(). Початок системи координат зображення — лівий верхній кут
— має координати (minХ, minY), не обовязково рівні нулю.
При створенні екземпляра класу BufferedІmage автоматично формується звязанный з ним екземпляр
класу WritabІeRaster. Точки зображення зберігаються в скритому буфері, вміщаючим одновимірний або
двовимірний масив точок. Вся робота з буфером здійснюється методами одного із класів DataBufferByte,
DataBufferlnt, DataBufferShort, DataBufferushort в залежності від довжини даних. Загальні властивості цих
класів зібрані в їх абстрактному суперкласі DataBuffer. В ньому визначені типи даних, що зберігаються в
буфері: TYPE_BYTE, TYPE_SHORT, TYPE_INT, TYPE_ІNDEFINED.
Методи класу DataBuffer дозволяють прямий доступ до даних буфера, але зручніше і безпечніше
звертатися до них методами класів Raster і WritableRaster. При створенні екземпляра класу Raster або
класу WritableRaster створюється екземпляр відповідного підкласу класу DataBuffer. Щоб не враховувати
спосіб зберігання точок зображення, Raster може звертатися не до буфера DataBuffer, а до підкласів
абстрактного класу SampІeModeІ, що розглядає не окремі байти буфера, а складові (samples) кольору. В
моделі RGB — це червона, зелена і синя складові. В пакеті java.awt. image єсть пять підкласів класу
SampІeModeІ:
• СomponentSampІeModel — кожна складова кольору зберігається в окремому елементі масива
DataBuffer;
• BandedSampleModel — дані зберігаються по складовим, складов одного кольору зберігаються в
одному масиві, a DataBuffer містить двовимірний масив: по масиву для кожної складової; даний
клас розширяє класс ComponentSampІeModel;
• PixelInterleavedSampІeModel — всі складові кольору одного пікселя зберігаються в сусідніх
елементах єдиного масива DataBuffer; даний клас розширяє клас ComponentSampІeModel;
• MultiPixeІPackedSampІeModel — колір кожного пікселя містить тільки одну складову, яка може бути
упакована в один елемент масива DataBuffer;
• SingІePixelPackedSampleModel — всі складові кольору кожного пікселя зберігаються в одному
елементі масиву DataBuffer.
На рис. 15.5 представлена ієрархія класів Java 2D, реалізуюча модель прямого доступу. Отже, Java 2D
створює складну і розгалуджену трьохшарову систему DataBuffer — SampІeModeІ — Raster управління
даними зображення BufferedІmage. Ви можете маніпулювати точками зображення, використовуючи їх
координати в методах класів Raster або спуститься на рівень нижче і звертатися до складових кольору
піксела методами класів SampІeModeІ. Якщо ж вам треба працювати з окремими байтами, скористуйтесь
класами DataBuffer. Застосовувати цю систему приходиться рідко, тільки при створенні свого способу
перетворення зображення. Стандартні ж перетворення виконуються дуже просто.
233
234. 15.7. Перетворення зображення в Java 2D
Перетворення зображення source, що зберігається в обєкті класу Buffеredlmage, в нове зображення
destination виконується методом filter(Buffredlmage source, Buffredlmage destination) описаним в інтерфейсі
BuffredІmageOp. Указаний метод повертає посилку на новий, змінений обєкт destination класу
Buffredlmage, що дозволяє задати ланцюжок послідовних перетворень. Можна перетворювати тільки
координатну систему зображення методом filter(Raster source, WritableRaster destination) повертаючим
посилку на змінений обєкт класу WritableRaster. Даний метод описано в інтерфейсі RasterOp. Спосіб
перетворення визначається класом, реалізуючим ці інтерфейси, а параметри перетворення задаються в
конструкторі класу. В пакеті java.awt.image єсть шість класів, реалізуючих інтерфейси BuffredІmageOp і
RasterOp:
• AffineTransformOp — виконує афінне перетворення зображення: зcув, поворот, відображення,
стискання або розтягування по вісям;
• RescaІeOp — змінює інтенсивність зображення;
• LookupOp — змінює окремі складові кольору зображення;
• BandCombineOp — мінює складові кольору в Raster;
• СolorConvertdp — змінює кольорову модель зображення;
• ConvolveOp — виконує згортання, що дозволяє змінити контраст і/або яскравість зображення,
створити ефект "розмитості" і інші ефекти.
Рис. 15.5. Класи, реалізуючі модель прямого доступу
Розглянемо, як можна застосувати ці класи для перетворення зображення.
15.8. Афінне перетворення зображення
Клас AffineTransform і його використання детально розібрані в уроці 9, тут ми тільки застосуємо його для
перетворення зображення. В конструкторі класу AffineTransformOp указується попередньо створене
афінне перетворення at і спосіб інтерполяції interp і/або правила візуалізації hints:
AffineTransformOp(AffineTransform at, int interp);
AffineTransformOp(AffineTransform at, RenderingHints hints);
Спосіб інтерполяції — це одна із двох констант: TYPE_NEAREST_NEIGHBOR (по замовчуванню в
234
235. другому конструкторі) або TYPE_BILINEAR. Після створення обєкту класу AffineTransformOp
застосовується метод filter(). При цьому зображення перетворюється всередині нової області типу
BufferedІmage, як показано на рис. 15.6, справа. Сама область виділена чорним кольором. Другий спосіб
афінного перетворення зображення — застосувати метод drawlmage(Bufferedlmage img, BufferedlmageOp
op, int x, int y) класу Graphics2D. При цьому перетворюється вся область img, як продемонстровано на
рис. 15.6, посередині. В лістинзі 15.5 показано, як задаються перетворення, представлені на рис. 15.6.
Зверніть увагу на особливості роботи з BufferedІmage. Треба створити графічний контекст зображення і
вивести в нього зображення. Ці дії здаються зайвими, але зручні для подвійної буферизації, яка зараз
стала стандартом перерисовування зображень, а в біблиотеці Swing виконується автоматично.
Лістинг 15.5. Афінне перетворення зображення
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.awt.event.*;
public class AffOp extends Frame{
private BufferedImage bi;
public AffOp(String s){ super (s) ;
// Завантажуємо зображення
Image img = getToolkit().getImage("javalogo52x88.gif");
// В цьому блоці організовано очікування завантаження
try{
MediaTracker mt = new MediaTracker(this);
mt.addImage(img, 0);
mt.waitForID(0);
// Чекаємо закінчення завантаження
}
catch(Exception e){}
// Розміри створюваної області bi співпадають з розмірами зображення img
bi = new BufferedImage(img.getWidth(this), img.getHeight(this),
BufferedImage.TYPE_INT_RGB);
// Створюємо графічний контекст big зображення bi
Graphics2D big = bi.createGraphics();
// Виводимо зображення img в графічний контекст
big.drawImage(img, 0, 0, this);
}
public void paint(Graphics g){
Graphics2D g2 = (Graphics2D)g;
int w = getSize().width;
int h = getSize().height;
int bw = bi.getWidth(this);
int bh = bi.getHeight(this);
// Створюємо афінне перетворення
AffineTransform at = new AffineTransform();
at.rotate(Math.PI/4); // Задаємо поворот на 45 градусів
//по годинниковій стрілці навколо лівого верхнього кута.
//Потім зсуваємо зображення вправо на величину bw
at.preConcatenate(new AffineTransform(1, 0, 0, 1, bw, 0));
// Визначаємо область зберігання bimg перетвореного
// зображення. Її розмір вдвічі больший попереднього
BufferedImage bimg = new BufferedImage(2*bw, 2*bw, BufferedImage.TYPE_INT_RGB);
// Створюємо обєкт biop, що містить перетворення at
BufferedImageOp biop = new AffineTransformOp(at,
AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
// Перетворюємо зображення, результат заносимо в bimg
biop.filter(bi, bimg);
// Виводимо початкове зображення.
g2.drawImage(bi, null, 10, 30);
235
236. // Виводимо змінене перетворенням biop область bi
g2.drawImage(bi, biop, w/4+3, 30);
// Виводимо перетворене всередині області bimg зображення
g2.drawImage(bimg, null, w/2+3, 30); }
public static void main(String[] args){
Frame f = new AffOp(" Афінне перетворення");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
f.setSize(400, 200);
f.setVisible(true) ;
}
}
На рис. 15.6 показано початкове зображення, перетворена область і перетворене всередині області
зображення.
Рис. 15.6. Афінне перетворення зображення
15.9. Зміна інтенсивности зображення
Зміна інтенсивності зображення виражається математично множеням кожної складової кольору на число
factor і додаванням до результату множення числа offset. Результат приводиться до діапазону значень
складової. Після цього інтенсивність кожної складової кольору лінійно змінюється в одному і тому ж
масштабі. Числа factor і offset сталі для кожного пікселя і задаються в конструкторі класу разом з
правилами візуалізації hints:
RescaleOp(float factor, float^offset, RenderingHints hints)
Після цього залишається застосувати метод filter().
На рис. 15.7 інтенсивність кожного кольору зменшена вдвічі, в результаті білий фон став сірим, а кольори
- темнішими. Потім інтенсивність збільшена на 70 одиниць. В лістинзі 15.6 приведена программа, що
виконує це перетворення.
Лістинг 15.6. Зміна інтенсивності зображення
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
public class Rescale extends Frame{
private BufferedImage bi;
public Rescale(String s){
236
237. super (s) ;
Image img = getToolkit().getImage("javalogo52x88.gif");
try{
MediaTracker mt = new MediaTracker(this);
mt.addImage(img, 0);
mt.waitForID(0); }
catch(Exception e){}
bi = new BufferedImage(img.getWidth(this), img.getHeight(this),
BufferedImage.TYPE_INT_RGB);
Graphics2D big = bi.createGraphics();
big.drawImage(img, 0, 0, this);
}
public void paint(Graphics g){
Graphics2D g2 = (Graphics2D)g;
int w = getSize().width;
int bw = bi.getWidth(this);
int bh = bi.getHeight(this);
BufferedImage bimg = new BufferedImage(bw, bh, BufferedImage.TYPE_INT_RGB);
//——————— Початок визначення перетворення --——-———
RescaleOp rop = new RescaleOp(0.5f, 70.0f, null);
rop.filter(bi, bimg);
//——————— Кінець визначення перетворення ———————
g2.drawImage(bi, null, 10, 30);
g2.drawImage(bimg, null, w/2+3, 30);
}
public static void main(String[] args){
Frame f = new Rescale(" Зміна інтенсивності");
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
f.setSize(300, 200);
f.setVisible(true);
}
}
Рис. 15.7. Зміна інтенсивності зображення
15.10. Зміна складових кольору
Щоб змінити окремі складові кольору, треба перш за все продивитися тип зберігання елементів в
BufferedІmage, по замовчуванню це TYPE_INT_RGB. Тут три складові — червона, зелена і синя. Кожна
складова кольору займає один байт, всі вони зберігаються в одному числі типу int. Потім треба скласти
237
238. таблицю нових значень складових. В лістинзі 15.7 це двовимірний масив samples. Потім заповнюємо
даний масив потрібними значеннями складових кожного кольору. В лістинзі 15.7 задається яскраво-
червоний колір рисування і білий колір фону. По отриманій таблиці створюємо екземпляр класу
ByteLookupTabІe, який звяже цю таблицю з буфером даних. Цей екземпляр використовуємо для
створення обєкта класу LookupOp. Нарешті, застосовуємо метод filter() цього класу.
В лістинзі 15.7 приведений тільки фрагмент програми. Для одержання повної програми його треба
вставити в лістинг 15.6 замість виділеного в ньому фрагмента. Логотип Java буде нарисований яскраво-
червоним кольором.
Лістинг 15.7. Зміна складових кольору
//————————————— Вставити в лістинг 15.6 ————————
byte samples[][] = new byte[3][256];
for (int j = 0; j < 255; j++){
samples[0][j] = (byte)(255); // Червона складова
samples[1][j] = (byte)(0); // Зелена складова
samples[2][j] = (byte)(0); // Синя складова
}
samples[0][255] = (byte) (255); // Колір фону — білий
samples[1][255] = (byte) (255) ;
samples [2] [255] = (byte) (255) ;
ByteLookupTable blut=new ByteLookupTable(0, samples);
LookupOp lop = new LookupOp(blut, null);
lop.filter(bi, bimg);
//————————————— Кінець вставки ———————————————-
15.11. Створення різних эфектів
Операція згортання (convolution) задає значення кольору точки в залежності від кольорів сусідніх точок
наступним способом. Нехай точка з координатами (х, у) має колір, вираженим числом А(х, у). Складаємо
масив із девяти дійсних чисел w(0), w(i), ... w(8). Тоді нове значенне кольору точки з координатами (х, у)
буде рівним:
w(0)*A(x-l, y-l)+w(l)*A(x, y-l)+w(2)*A(x+l, y-l)+ w(3)*A(x-l, y)+w(4)*A(x, y)+w(5)*A(x+l, у)+
w(6)*A(x-l, y+l)+w(7)*A(x, y+l)+w(8)*A(x+l, y+1)
Задаючи різні значення ваговим коефіцієнтам w(i), будемо отримувати різні эфекти, підсилюючи чи
зменшуючи вплив сусідніх точок. Якщо сума всіх девяти чисел w(i) рівна 1.0f, то інтенсивність кольору
залишиться попередня. Якщо при цьомц всі ваги рівні між собою, тобто рівні 0.1111111f, то одержимо
ефект размитості, тумана, димки. Якщо вага w(4) значно більше решти при загальній сумі їх l.0f, то
зростає контрастність, виникає эфект графіки, штрихового рисунка.
238
239. Можно згорнути не тільки сусідні точки, але і наступні ряди точок, взявши масив вагових коэфіцієнтів із 15
елементов (3x5, 5x3), 25 элементів (5x5) і більше. В Java 2D згортання робиться так. Спочатку
визначаємо масив ваг, наприклад:
float[] w = {0, -1, 0, -1, 5, -1, 0, -1, 0};
Потім створюємо екземпляр класу Kernel — ядра згортання:
Kernel kern = new Kernel(3, 3, w);
Потім обєкт класу ConvoІveOp з цим ядром:
ConvolveOp conv = new ConvoiveOp(kern);
Все готово, застосовуємо метод filter(): conv.filter(bi, bimg);
В лістинзі 15.8 записані дії, необхідні для створення eфекта "размитості".
Лістинг 15.8. Створення різних эфектів
//—————————— Вставити в лістинг 15.6 ——————————————
float[] wl = { 0.11111111f, 0.11111111f, 0.11111111f, 0.11111111f, 0.11111111f,
0.11111111f, 0.11111111f, 0.11111111f, 0.11111111f };
Kernel kern = new Kernel(3, 3, wl);
ConvolveOp cop = new ConvolveOp(kern, ConvolveOp.EDGE_NO_OP, null);
cop.filter(bi, bimg) ;
//—————————— Кінець вставки ———————————————————————
На рис 15.8 представлені зліва направо початкове зображення і зображення, перетворені ваговими
матрицями w1, w2 i w3, де матриця w1 показана в лістинзі 15.8, а матриці w2 i w3 виглядають так:
float[] w2 = { 0, -1, 0,-1, 4, -1, 0, -1, 0 } ;
239
240. float[] w3 = { -1, -1, -1,-1, 9, -1, -1, -1, -1 };
Рис. 15.8. Створення эфектів
15.12. Анімація
Єсть декілька способів створити анімацію. Самий простий із них — записати заздалегідь всі необхідні
кадри в графічні файли, завантажити їх в оперативну память у вигляді обєктів класу Image або
Bufferedlmage і виводити по черзі на экран. Це зроблено в лістинзі 15.9. Заготовлено десять кадрів у
файлах run1.gif, run2.gif, , run10.gif. Вони завантажуються в масив img[] і виводяться на екран циклічно
100 раз, із затримкою в 0,1 сек.
Лiстинг 15.9. Проста анімація
import java.awt.*;
import java.awt.event.*;
class SimpleAnim extends Frame{
private Image[] img = new Image[10];
private int count;
SimpleAnim(String s){ super(s);
MediaTracker tr = new MediaTracker(this);
for (int k = 0; k < 10; k++){
img[k] = getToolkit().getImage("run"+ String.valueOf(k) +".gif");
tr.addImage(img[k], 0);
}
try{
tr.waitForAll(); // Чекаємо завантаження всіх зображень
}
catch(InterruptedException e){}
240
241. setSize(400, 300);
setVisible(true);
}
public void paint(Graphics g){
g.drawImage(img[count % 10], 0, 0, this);
}
// public void update(Graphics g){ paint(g); }
public void go(){ while(count < 100){
repaint(); // Виводимо наступний кадр
try{ // Затримка в 0.1 сек
Thread.sleep(100);
}catch(InterruptedException e){count++;}
}
}
public static void main(String[] args){
SimpleAnim f = new SimpleAnim(" Проста анімація");
f.go();
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0); }
});
}
}
Якщо кадри анімації повністю перерисовують вікно, то його очистка методом clearRect() не потрібна.
Більше того, вона часто викликає неприємне мерехтіння із-за появи на мить білого фону. В такому
випадку треба зробити наступне перевизначення:
public void update(Graphics g) {
paint(g);
}
В лістинзі 15.9 це перевизначення зроблено як коментар.
Для "легких" компонентів справа складніша. Метод repaint() послідовно звертається до методів repaint()
обмежуючих "легких" контейнерів, поки не зустрінеться "важкий" контейнер, частіше всього це екземпляр
класу Container. В ньому викликається метод update(), очищаючий і перерисовуючий контейнер. Після
цього йде звернення до методів update() обмежуючих всі "легкі" компоненти в контейнері. Звідси слідує,
що для усунення мерехтіння "легких" компонентів необхідно перевизначити метод update() першого
обмежуючого "важкого" контейнера, звертаючись в ньому до методів super.update (g) або super.paint(g).
Якщо кадри покривають тільки частину вікна, причому кожний раз нову, то очистка вікна необхідна, інакше
старі кадри залишаться у вікні, зявится "хвіст". Щоб усунути мерехтіння, використовують прийом,
одержавший назвуе "подвійна буферизація " (double buffering).
15.13. Покращення зображення подвійною буферизацією
Суть подвійної буферизації в тому, що в оперативній памяті створюється буфер — обєкт класу Image або
BufferedІmage, і викликається його графічний контекст, в якому формується зображення. Там же
відбувається очистка буфера, яка теж не відображається на екрані. Тільки після виконання всіх дій готове
зображення виводиться на екран. Все це відбувається в методі update(), а метод paint() тільки
звертається до update(). Лістинги 15.10—15.11 пояснюють даний прийом.
Лістинг 15.10. Подвійна буферизація за допомогою класу image
public void update(Graphics g){
int w = getSize().width, h = getSize().height;
// Створюємо зображення-буфер в оперативній памяті
Image offImg = createImage(w, h);
// Одержуємо його графічний контекст
241
242. Graphics offGr = offImg.getGraphics();
// Змінюємо поточний колір буфера на колір фону
offGr.setColor(getBackground());
//и заповнюємо ним вікно компонента, очищуючи буфер
offGr.fillRect(0, 0, w, h);
// Повертаємо поточний колір буфера
offGr.setColor(getForeground());
// Для лістингу 15.9 виводимо в контекст зображення
offGr.drawImage(img[count % 10], 0, 0, this);
// Рисуємо в графічному контексті буфера
// (необовязкова дія)
paint(offGr);
// Виводимо зображення-буфер на экран
// (можно перенести в метод paint())
g.drawImage(offImg, 0, 0, this); }
// Метод paint() необовязковий
public void paint(Graphics g).update(g); }
Лістинг 15.11. Подвійна буферизація за допомогою класу BufferedІmage
public void update(Graphics g){
Graphics2D g2 = (Graphics2D},g;
int w = getSize().width, h = getSize().height;
// Створюємо зображення-буфер в оперативній памяті
BufferedImage bi = (BufferedImage)createImage(w, h);
// Створюємо графічний контекст буфера
Graphics2D big = bi.createGraphics();
// Установлюємо колір фону
big.setColor(getBackground());
// Очищаємо буфер кольором фону
big.clearRect(0, 0, w, h);
// Відновлюємо поточний колір
big.setColor(getForeground());
// Виводимо що-небудь в графічний контекст big
// ...
// Виводимо буфер на экран
g2.drawImage(bi, 0, 0, this);
}
Метод подвійної буферизації став фактичнo стандартом виведення змінних зображень, а в бібліотеці
Swing він застосовується автоматично. Даний метод зручний і при перерисовуванні окремих частин
зображення. В цьому випадку в зображенні-буфері рисується незмінна частина зображення, а в методі
paint() — те, що змінюється при кожному перерисовуванні.
В лістинзі 15.12 показано другий спосіб анімації — кадри зображення рисуютсья безпосередньо в
програмі, в методі update(), по заданому закону зміни зображення. В результаті червоний мячик плигає на
фоні зображення.
Лiстинг 15.12. Анімація рисуванням
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
class DrawAnim extends Frame{
private Image img;
private int count;
DrawAnim(String s) {
super(s);
242
243. MediaTracker tr = new MediaTracker(this);
img = getToolkit().getImage("IMG_0106.jpg");
tr.addImage(img, 0);
try{
tr.waitForID(0) ;
}catch(InterruptedException e) {}
setSize(1200, 800);
setVisible(true);
}
public void update(Graphics g){
Graphics2D g2 = (Graphics2D)g;
int w = getSize().width, h = getSize().height;
BufferedImage bi = (BufferedImage)createImage(w, h) ;
Graphics2D big = bi.createGraphics();
// Заповнюємо фон зображенням img
big.drawImage(img, 0, 0, this);
// Установлюємо колір рисования
big.setColor(Color.red);
// Рисуємо в графічному контексті буфера круг,
// перемещающийся по синусоиде
big.fill(new Arc2D.Double(4*count, 50+30*Math.sin(count),
50, 50, 0, 360, Arc2D.OPEN));
// Змінюємо колір рисування
big.setColor(getForeground());
// Рисуємо горизонтальну пряму
big.draw(new Line2D.Double(0, 125, w, 125));
// Виводимо зображення-буфер на екран
g2.drawImage(bi, 0, 0, this); }
public void go(){
while(count < 1000){
repaint();
try{
Thread.sleep(10);
}catch(InterruptedException e){}
count++;
}
}
public static void main(String[] args){
DrawAnim f = new DrawAnim(" Анімація");
f.go();
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
Eфект мерехтіння, переливи кольору, затемнення і інші эфект, отримані заміною окремих пікселів
зображення, зручно створювати за допомогою класу MemoryІmageSource. Метод newPixels() цього класу
викликають негайне перерисовування зображення навыть без звернення до методу repaint(), якщо перед
цим виконаний метод setAnimated(true). Частіше всього застосовуються два методи:
• newPixels(int x, int y, int width, int height) — отримувачу посилається указаний аргументами
прямокутний фрагмент зображення;
• newPixels() — отримувачу посилається все зображення.
В лістинзі 15.13 показано застосування цього способу. Квадрат, виведений на екран, переливається
різними кольорами.
243
244. Лістинг 15.13. Анімація за допомогою MemorylmageSource
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
class InMemory extends Frame{
private int w = 100, h = 100, count;
private int[] pix = new int[w * h];
private Image img;
MemoryImageSource mis;
InMemory(String s){ super(s);
int i = 0;
for(int y = 0; y < h; y++){
int red = 255 * y / (h - 1);
for(int x = 0; x < w; x++){
int green = 255 * x / (w - 1);
pix[i++] = (255 << 24)|(red << 16)|(green << 8)| 128;
}
}
mis = new MemoryImageSource(w, h, pix, 0, w);
// Задаємо можливість анімації
mis.setAnimated(true);
img = createImage(mis);
setSize(350, 300);
setVisible(true);
}
public void paint(Graphics gr){
gr.drawImage(img, 10, 30, this);
}
public void update(Graphics g){ paint(g); }
public void go(){
while (count < 100){
int i = 0;
// Змінюємо масив пікселів по деякому закону
for(int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
pix[i++] = (255 << 24)|(255 + 8 * count << 16)| (8*count <<8)| 255 + 8 * count;
// Повідомляємо споживача про зміну
mis.newPixels();
try{
Thread.sleep(100);
}catch(InterruptedException e){}
count++;
}
}
public static void main(String[] args){
InMemory f= new InMemory(" Зображення в памяті");
f.go();
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent ev){ System.exit(0); }
});
}
}
Ось і всі засоби для анімації, решта — їх вміле застосування. Комбінуючи розглянуті способи, можна
добитися цікавих эфектів. В документації SUN J2SDK, в каталогах demoapplets і demojfcJava2D src,
наведено багато прикладів аплетів і додатків з анімацією.
244
245. 15.14. Звук
Як було указано в попередньому уроці, в аплетах реалізується інтерфейс AudioСlip. Екземпляр обєкта,
реалізуючого цей інтерфейс можна отримати методом getAudioClip(), котрий, крім того, завантажує
звуковий файл, а потім користуватися методами play(), loop() і stop() цього інтерфейса для програвання
музики. Для застосування даного ж прийому в додатках в класс Applet введено статичний метод
newAudioclіp(URL address), завантажуючий звуковий файл, що знаходиться по адресу address, і
повертаючий обєкт, реалізуючий інтерфейс AudioСlip. Його можна використовувать для програвання
звуку в додатку, якщо звичайно звукова система компютера уже настроєна.
В лістинзі 15.14 приведено найпростіший консольний додаток, нескінчено програючий звуковий файл
doom.mid, що знаходиться в поточному каталозі. Для завершення додатку треба застосувати засоби
операційної системи, наприклад, комбінацію клавіш <Ctrl>+<C>.
Лістинг 15.14. Найпростіший аудіододаток
import java.applet.* ;
import java.net.*;
class SimpleAudio{
SimpleAudio () {
try{
AudioClip ac = Applet.newAudioClip(new URL("file:doom.mid"));
ac.loop();
}catch(Exception e){}
}
public static void main(String[] args){
new SimpleAudio();
}
}
Таким способом можна пргравати звукові файли типів AU, WAVE, AIFF, MIDI без стискання. В склад
віртуальної машины Java, що входить в SUN J2SDK починаючи з версії 1.3, включено пристрій,
програваючий звук, записаний в одному із форматів AU, WAVE, AIFF, MIDI, перетворюючий, мікшуючий і
записуючий звук в тих же форматах. Для роботи з цим пристроєм створені класи, зібрані в пакети
javax.sound.sampled, javax.sound.midi, javax.sound.sampled.spi і javax.sound.midi.spi. Перечислений набір
класів для роботи із звуком отримав назву Java Sound API.
15.15. Програвання звуку в Java 2
Програвач звуку, вбудований в JVM, розрахований на два способи запису звуку: моно і стерео оцифровку
(digital audio) з частотою дискретизації (sample rate) від 8 000 до 48 000 Гц і апроксимацією (quantization) 8
і 16 бітів, і MIDI-послідовності (sequences) типу 0 і 1. Оцифрований звук повинен зберігатися в файлах
типу AU, WAVE і AIFF. Його можна програвати двома способами. Перший спосіб описаний в інтерфейсі
clip. Він розрахований на відтворення невеликих файлів або неодноразового програвання файла і
заключається в тому, що весь файл цілком завантажується в оперативну память, а потім проигррається.
Другий спосіб описаний в інтерфейсі SourceDataLine. Згідно цьому способу файл завантажується в
оперативну память по частинах в буфер, розмір якого можна задати довільно. Перед завантаженням
файла треба задати формат запису звуку в обєкті класу AudioFormat. Конструктор цього класу:
AudioFormat(float sampleRate, int sampleSize, int channels, boolean signed, boolean bigEndian)
вимагає знання частоти дискретизації sampleRate (по замовчуванню 44 100 Гц), аппроксимації
sampleSize, заданої в бітах (по замовчуванню 16), числа каналів channels (1 — моно, по замовчуванню 2
— стерео), запис чисел із знаком, signed == true, або без знака, і порядка розташування байтів в числі
bigEndian. Такі дані звичайно невідомі, тому їх отримують із самого файла. Це відбувається в два кроки.
На першому кроці отримуємо формат файла статичним методом getAudioFiІeFormat() класу AudioSystem,
на другому — формат запису звуку методом getFormat() класу AudioFiІeFormat. Це описано в лістинзі
15.15. Після того як формат запису визначено і занесено в обєкт класу AudioFormat, в обєкті класу
DataLine.infо збирається інформація про вхідну лінію (line) і способі програвання clip або SourceDataLine.
245
246. Далі треба перевірити, чи зможе проигравач обслуговувати лінію з таким форматом. Потім треба звязати
лінію з програвачем статичним методом getLine() класу AudioSystem. Потім створюємо потік даних із
файла — обєкт класу AudioІnputstream. Із цього потоку теже можна отримати обєкт класу AudioFormat
методом getFormat(). Даний варіант вибраний в лістинзі 15.16. Відкриваємо створений потік методом
ореn(). У-фф! Все готово, тепер можна почати програвання методом start(), завершити методом stop(),
"перемотать" в початок методом setFramePosition(0) або setMillisecondPosition(0).
Можна задати пргравання n раз підряд методом loop(n) або нескінчене число раз методом loop
(Clip.LOOP_CONTINUOUSLY) . Перед цим необхідно установити початкову n і кінцеву m позиції
повторення методом setLoopPoints(n, m). По закінченню програвання треба закрити лінію методом close
(). Вся ця послідовність дій показана в лістинзі 15.15.
Лістинг 15.15. Програвання аудіокліпа
import javax.sound.sampled.*;
import java.io.*;
class PlayAudio{
PlayAudio(String s){
play(s);
}
public void play(String file){
Clip line = null;
try{
// Створюємо обєкт, представляючий файл
File f = new File (file);
// Отримуємо інформацию про спосіб запису файла
AudioFileFormat aff = AudioSystem.getAudioFileFormat(f);
// Отримуємо інформацию про спосіб запису звуку
AudioFormat af = aff.getFormat();
// Збираємо всю інформацію вмісті, додаючи дані про клас
DataLine.Info info = new DataLine.Info(Clip.class, af) ;
// Перевіряємо, чи можна програвати такий формат
if (!AudioSystem.isLineSupported(info)){
System.err.println("Line is not supported");
System.exit(0);
}
// Отримуємо лінію звязку з файлом
line = (Clip)AudioSystem.getLine(info);
// Створюємо потік байтів із файла
AudioInputStream ais = AudioSystem.getAudioInputStream(f);
// Відкриваємо лінію
line.open(ais);
}catch(Exception e){
System.err.println(e);
}
// Начинаємо програвання
line.start();
// Тут треба зробити затримку до закінчення програвання або зупинити його наступним
//методом:
try{ // Затримка в 0.1 сек
Thread.sleep(60000);
}catch(InterruptedException e){ }
line.stop();
//По закінченні програвання закриваємо лінію
line.close();
}
public static void main(String[] args){
if (args.length != 1)
System.out.println("Usage: Java PlayAudio filename");
246
247. new PlayAudio(args[0]);
}
}
Як бачите, методи Java Sound API виконують елементарні дії, які треба повторяти із програми в
программу. Як говорять, це методи "низького рівня" (low level). Другий спосіб, використовуючий методи
інтерфейса SourceDataLine, вимагає попереднього створення буфера довільного розміру.
Лістинг 15.16. Програвання аудіофайла
import javax.sound.sampled.*;
import java.io.*;
class PlayAudioLine{
PlayAudioLine(String s){
play(s);
}
public void play(String file){
SourceDataLine line = null;
AudioInputStream ais = null;
byte[] b = new byte[2048]; // Буфер даних
try{
File f = new File(file);
// Створюємо вхідний потік байтів із файла f
ais = AudioSystem.getAudioInputStream(f);
// Дістаємо із потоку інформацію про спосіб запису звуку
AudioFormat af = ais.getFormat () ;
// Заносимо цю інформацію в обєкт info
DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);
// Перевіряємо, чи підходить такий спосіб запису звуку
if (!AudioSystem.isLineSupported(info)){
System.err.println("Line is not supported");
System.exit(0);
}
// Отримуємо вхідну лінію
line = (SourceDataLine)AudioSystem.getLine(info);
// Відкриваємо лінію
line.open(af);
try{
Thread.sleep(1000);
}catch(InterruptedException e){}
// Починаємо програвання
int num = 0 ;
line.start(); // Чекаємо появи даних в буфері
// Раз за разом заповняємо буфер
while(( num = ais.read(b)) != -1)
line.write(b, 0, num);
// "Зливаємо" буфер, програючи залишок файла
line.drain();
// Закриваємо потік
ais.close();
} catch (Exception e) {
System.err.println (e);
}
// Зупиняємо програвання
line.stop();
// Закриваємо лінію
line.close();
}
public static void main(String[] args){
247
248. String s = "bark.aif";
if (args.length > 0) s = args[0];
new PlayAudioLine(s) ;
}
}
Управляти програванням файла можна за допомогою подій. Подія класу LineEvent настає при відкриті,
OPEN, і закритті, CLOSE, потоку, при початку, START, і закінченні, STOP, програвання. Характер події
відмічається указаними константами. Відповідний інтерфейс LineListener описує тільки один метод
update().
В MIDI-файлах зберігається послідовність (sequence) команд для секвенсора (sequencer) – пристрою для
запису, програвання і редагування MlDI-послідовності, яким може бути фізичний пристрій або программа.
Послідовність складається із декількох дорожок (tracks), на яких записані MIDI-події (events). Кожна
дорожка завантажується в своєму каналі (channel). Звичайно дорожка містить звучання одного
музичнного інструмента або запис голосу одного виконавця або запис декількох виконавців, мікшовану
синтезатором (synthesizer). Для програвання MIDI-послідовності в найпростішому випадку треба
створити екземпляр секвенсора, відкрити його і направити в нього послідовність, отриману із файла, як
показано в лістинзі 15.17. Після цього треба почати програвання методом start(). Закінчити програвання
можна методом stop(), "перемотать" послідовність на початок запису або на указаний час програвання -
методами setMicrosecondPositionflong mcs) або setTickPosition(long tick).
Лістинг 15.17. Програвання MIDI-послідовності
import javax.sound.midi.*;
import java.io.*;
class PlayMIDK{
PlayMIDK(String s) {
play(s);
}
public void play(String file){
try{
File f = new File(file);
// Отримуємо секвенсор по замовчуванню
Sequencer sequencer = MidiSystem.getSequencer();
// Перевіряємо, чи отриманий секвенсор
if (sequencer == null) {
System.err.println("Sequencer is not supported");
System.exit(0);
}
// Відкриваємо секвенсор
sequencer.open();
// Отримуємо MIDI-послідовність із файла
Sequence seq = MidiSystem.getSequence(f);
// Направляємо послідовність в секвенсор
sequencer.setSequence(seq);
// Починаємо програвання
sequencer.start();
// Тут треба зробити затримку на час програвання, а потім зупинити:
try{
Thread.sleep(67000);
}catch(InterruptedException e){}
sequencer.stop();
}catch(Exception e){
System.err.println(e);
}
}
public static void main(String[] args){
String s = "doom.mid";
248
249. if (args.length > 0) s = args[0];
new PlayMIDK(s);
}
}
15.16. Синтез і запис звуку в Java 2
Синтез звуку заключається в створенні MIDI-послідовності - обєкта класу sequence — яким-небудь
способом: з мікрофона, лінійного входа, синтезатора, із файла, або просто створити в програмі, як це
робиться в лістинзі 15.18. Спочатку створюється пуста послідовність одним із двох конструкторів:
Sequence(float divisionType, int resolution)
Sequence(float divisionType, int resolution, int numTracks)
Перший аргумент divisionType визначає спосіб відрахунків моментів (ticks) MIDI-подій — це одна із
констант:
• PPQ (Pulses Per Quarter note) — відрахунки заміряються в долях від тривалості звуку в чверть;
• SMPTE_24, SMPTE_25, SMPTE_so, SMPTE_30DROP (Society of Motion Picture and Television
Engineers) — відрахунки в долях одного кадра, при указаному числі кадрів в секунду.
Другий аргумент resolution задає ккількість відрахунків у вказану одиницю, наприклад,
Sequence seq = new Sequence)Sequence.PPQ, 10);
задає 10 відрахунків у звуці тривалістю в чверть. Третій аргумент numTracks визначає конструктор
дорожок в MIDI-послідовності. Потім, якщо застосовувався перший конструктор, в послідовності
створюється одна або декілька дорожок:
Track tr = seq.createTrack();
Якщо застосовувався другий конструктор, то треба отримати уже створені конструктором дорожки:
Track[] trs = seq.getTracks();
Потім дорожки заповнюються MIDI-подіями за допомогою MIDl-подій. Єсть декілька типів повідомлень для
різних типів подій. Найчастіше зустрічаються події типу shortMessage, які створюються конструктором по
замовчуванню і потім заповнюються методом setMessage():
ShortMessage msg = new ShortMessage();
rasg.setMessage(ShortMessage.NOTEJDN, 60, 93);
Перший аргумент указує тип повідомлення: NOTE_ON - почати звучання, NOTE_OFF - зупинити звучання
і т. д. Другий аргумент для типу NOTE_ОN показує высоту звуку, в стандарті MIDI це числа від 0 до 127,
60 - нота "до" першої октави. Третій аргумент означає "швидкість" натискання клавіші MIDI-інструмента і
по-різному розуміється різними пристроями. Далі створюється MIDI-подія:
MidiEvent me = new MidiEvent{msg, ticks);
Перший аргумент конструктора msg — це повідомлення, другий аргумент ticks - час наступу події (в
нашому прикладі програвання ноти "до") в одиницях послідовності seq (в нашому прикладі в десятих
долях чверті). Час відраховується від початку програвання послідовності. Нарешті, подія заноситься на
дорожку:
tr.add(me);
Указані дії продовжуються, поки всі дорожки не будуть заповнені всіма подіями. В лістинзі 15.18 це
робиться в циклі, але звичайно MIDI-події створюються в методах обробки натискання клавіш на
звичайній абои спеціальній MIDI-клавіатурі. Ще один спосіб — вивести на екран зображення клавіатури і
249
250. створювати MIDI-події в методах обробки натискання кнопки миші на цій клавіатурі. Після створення
послідовності її можна програвати, як в лістинзі 15.17, або записати у файл або вихідний потік. Для цього
замість методу start() треба застосовувати метод startRecording(), який одночасно і програє послідовність,
і готує її до запису, яку здійснюють статичні методи:
write(Sequence in, int type, File out)
write(Sequence in, int type, OutputStream out)
Другий аргумент type задає тип MIDI-файла, який краще всього визначити для заданної послідовності seq
статичним методом getMidiFiІeTypes(seq). Даний метод повертає масив можливих типів. Треба
скористатися нульовим елементом масиву. Все це показано в лістинзі 15.18.
Лістинг 15.18. Створення MIDI-послідовності нот звукоряду
import javax.sound.midi.*;
import java.io.*;
class SynMIDI {
SynMIDI() {
play(synth());
}
public Sequence synth(){
Sequence seq = null;
try{
// Послідовність буде відраховувати по 10 MIDI-подій на звук довжиною в чверть
seq = new Sequence(Sequence.PPQ, 10);
// Створюємо в послідовністю одну дорожку
Track tr = seq.createTrack();
for (int k = 0; k < 100; k++){
ShortMessage msg = new ShortMessage();
// Пробігаємо MIDI-ноти від номера 10 до 109
msg.setMessage(ShortMessage.NOTE_ON, 10+k, 93);
// Будемо програвати ноти через кожні 5 відрахунків
tr.add(new MidiEvent(msg, 5*k));
msg = null;
}
} catch (Exception e) {
System. err.println("From synth(): "+e);
System.exit (0);
}
return seq;
}
public void play (Sequence seq) {
try{
Sequencer sequencer = MidiSystem.getSequencer();
if (sequencer == null){
System.err.println("Sequencer is not supported");
System.exit(0);
}
sequencer.open();
sequencer.setSequence(seq);
sequencer.startRecording();
int[] type = MidiSystem.getMidiFileTypes(seq);
MidiSystem.write(seq, type[0], new File("gammas.mid"));
}catch(Exception e) {
System.err.println("From play(): " + e);
}
}
public static void main(String[] args){
new SynMIDI();
250
251. }
}
На жаль, із-за обмалі часу ми не можемо торкнутися теми про роботу з синтезатором (synthesizer),
мікшування звуку, роботи з декільками інструментами і інших можливостей Java Sound API. В
документації SUN J2SDK, в каталозі docsguidesoundprog_guide, єсть детальне керівництво програміста,
а в каталозі demosoundsrc лежать вихідні тексти синтезатора, використовуваного в Java Sound API.
Лабораторна робота 14. Робота з зображенням і анімація.
251
252. Програмування у Java
Урок 16. Обробка виключних ситуацій
• Блоки перехвату виключення
• Частина заголовку метода throws
• Оператор throw
• Ієрархія класів-виключень
• Порядок обробки виключень
• Створення власних виключень
• Заключення
16.1. Виключні ситуації
Виключні ситуації (exceptions) можуть виникнути під час виконання (runtime) програми, перервавши її
звичайний хід. До них відносится ділення на нуль, відсутність завантажуваного файла, відємний або
вийшовший за верхню межу індекс масива, переповнення виділеної памяті і маса інших неприємностей,
які можуть трапитися в самий непідходячий момент. Звичайно, можна передбачити такі ситуації і
застрахуватися від них як-небудь так:
if (something == wrong){
// Робимо аварійні дії
}else{
// Звичайний хід дій
}
Але при цьому багато часу йде на перевірки, і програма перетворюється в набір цих перевірок.
Подивіться будь-яку солідну программу, написану мовою С або Pascal, і побачите, що вона на 2/3
складається із таких перевірок. В обєктно-орієнтованих мовах програмування прийнято інший підхід. При
виникненні виключної ситуації виконуюча система створює обєкт певного класу, відповідний виникнувшій
ситуації, який містить дані про те, що, де і коли трапилося. Цей обєкт передається на обробку програмі, в
якій виникло виключення. Якщо програма не обробляє виключення, то обєкт повертається обробчику по
замовчуванню виконуючої системи. Обробчик поступає дуже просто: виводить на консоль повідомлення
про виникнувше виключення і зупиняє виконання програми.
Наведемо приклад. В програмі лістинга 16.1 може виникнути ділення на нуль, якщо запустити її з
аргументом 0. В програмі немає ніяких засобів обробки такої виключної ситуації. Подивіться на рис. 16.1,
які повідомлення виводить виконуюча система Java.
Лістинг 16.1. Програма без обробки виключень
class SimpleExt {
public static void main(String[] args){
int n = Integer.parseInt(args[0]);
System.out.println("10 / n = " + (10 / n));
System.out.println("After all actions");
}
}
Програма SimpleExt запущена три рази. Перший раз аргумент args[0] рівний 5 і програма виводить
результат: "ю / n = 2". Після цього появляється друге повідомлення: "After all actions". Другий раз аргумент
рівний 0, і замість результата ми отримуємо повідомлення про те, що в підпроцесі "main" відбулося
виключення класу ArithmeticException внаслідок ділення на нуль: "/ by zero". Далі уточнюється, що
виключення виникло при виконанні метода main класу SimpleExt, а в дужках указано, що дія, в результаті
якої виникла виключна ситуація, записана в четвертому рядку файла SimpleExt.java. Виконання програми
зупиняється, заключне повідомлення не появляється. Третій раз программа запущена взагалі без
аргумента. В масиві args немає елементів, його довжина рівна нулю, а ми намагаємося звернутися до
элементу args[0]. Виникає виключна ситуація класу ArrayIndexOutOfBoundsException внаслідок дії,
записаної в третьому рядку файла SimpleExt.java. Виконання програми зупиняється, звернення до методу
252
253. println() не відбувається.
Рис. 16.1. Повідомлення про виключні ситуації
16.2. Блоки перехвату виключення
Ми можемо перехватить і обробити виключення в програмі. При опису обробки застосовується
бейсбольна термінологія. Говорять, що виконуюча система або програма "викидає" (throws) обєкт-
виключення. Цей обєкт "пролітає" через всю програму, зявившись спочатку в тому методі, де відбулося
виключення, а програма в одному або декількох місцях намагається (try) його "перехопити" (catch) і
обробити. Обробку можна зробити повністю в одному місці, а можна обробити виключення в одному місці,
викинути знову, перехопити в другому місці і обробляти далі.
Ми уже багато раз в цій книзі стикались з необхідністю обробляти різні виключні ситуації, але не робили
цього, тому що не хотіли відволікатися від основних конструкцій мови. Не вводьте це в звичку! Добре
написані обєктно-орієнтовані програми обовязково повинні обробляти всі виникаючі в них виключні
ситуації. Для того щоб спробувати (try) перехватить (catch) обєкт-виключення, треба весь код програми, в
якому може виникнути виключна ситуація, охопити оператором try {} catch() {}. Кожнийй блок catch(){}
перехоплює виключення тільки одного типу, того, котрий указано в його аргументі. Але можна написати
декілька блоків catch(){} для перехоплення декількох типів виключень. Наприклад, ми знаємо, що в
програмі лістинга 16.1 можуть виникнути виключення двох типів. Напишемо блоки їх обробки, як це
зроблено в лістинзі 16.2.
Лістинг 16.2. Програма з блоками обробки виключень
class SimpleExt1{
public static void main(String[] args){
try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()");
System.out.println(" 10 / n = " + (10 / n) ) ;
System. out. println ("After results output");
}catch(ArithmeticException ae){
System.out.println("From Arithm.Exc. catch: "+ae);
}catch(ArrayIndexOutOfBoundsException arre){
253
254. System.out.println("From Array.Exc.catch: "+arre);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
В програму лістинга 16.1 вставлено блок try{} і два блоки перехоплення catch(){} для кожного типу
виключення. Обробка виключення тут заключається просто у виведенні повідомлення і вмісту обєкта-
виключення, як воно представлено методом toString() відповідного класу-виключення. Після блоків
перехоплення вставлено ще один, необовязковий блок finally(). Він призначений для виконання дій, які
треба виконати обовязково, щоб там не трапилось. Все, що написано в цьому блоці, буде виконано і при
виникненні виключення, і при звичайній ході програми, і навіть якщо вихід із блоку try{} здійснюється
оператором return.
Якщо в операторі обробки виключень єсть блок finally{}, то блок catch() {} може бути відсутнім, тобто
можна не перехоплювати виключення, але при його появі все-таки зробити якісь обовязкові дії. Крім
блоків перехоплення в лістинзі 16.2 після кожної дії робиться трасуючий друк, щоб можна було прослідити
за порядком виконання програми. Програма запущена три рази: з аргументом 5, з аргументом 0 і взагалі
без аргумента. Результат показано на рис. 16.2.
Рис. 16.2. Повідомлення обробки виключень
Після першого запуску, при звичайній ході програми, виводяться всі дії. Після другого запуску, що веде до
ділення на нуль, управління зараз же передається у відповідний блок catch(ArithmeticException ае) {},
потім виконується те, що написано в блоці finally{}. Після третього запуску управління після виконання
методу parseInt() передається в другий блок catch(ArraylndexOutOfBoundsException arre){}, потім в блок
finally{}.
Зверніть увагу, що у всіх випадках — і при звичайній ході програми, і після цих обробок — виводиться
повідомлення "After all actions". Це свідчить про те, що виконання програми не зупиняється при
виникненні виключної ситуації, як це було в програмі лістингу 16.1, а продовжується після обробки і
виконання блоку finally {}. При запису блоків обробки виключень треба чітко уявляти собі, як буде
передаватися управління у всіх випадках. Тому вивчіть уважно рис. 16.2. Цікаво, що пустий блок catch (){},
254
255. в якому між фігурними дужками нічого немає, навіть пробілу, теж вважається обробкою виключення і
приводить до того, що виконання програми не зупиниться. Саме так і "обробляли" виключення в
попередніх уроках. Трохи вище було сказано, що викинуте виключення "пролітає" через всю програму. Що
це означає? Змінимо програму лістингу 16.2, винісши ділення в окремий метод f(). Одержимо лістинг 16.3.
Лістинг 16.3. Видалення виключень із методу
class SimpleExt2{
private static void f(int n){
System.out.println(" 10 / n = " + (10 / n));
}
public static void main(String[] args){
try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()");
f (n);
System.out.println("After results output");
}catch(ArithmeticException ae){
System.out.println("From Arithm.Exc. catch: "+ae);
}catch(ArrayIndexOutOfBoundsException arre){
System.out.println("From Array.Exc. catch: "+arre);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
Відкомпілювавши і запустивши програму лістингу 16.3, впевнимося, що виведення програми не
змінилося, воно таке ж, як на рис. 16.2. Виключення, що виникло при діленні на нуль в методі f(),
"пролетіло" через цей метод, "вилетіло" в метод main(), там перехоплено і оброблено.
16.3. Частина заголовка методу throws
Та обставина, що метод не обробляє виникаючого в ньому виключення, а викидає (throws) його, належить
відмічати в заголовку методу службовим словом throws і указанням класу виключення:
private static void f(int n) throws ArithmeticException{
System.out.println(" 10 / n = " + (10 / n)) ;
}
Чому ж ми не зробили цього в лістинзі 16.3? Справа в тому, що специфікація JLS ділить всі виключення
на перевіряючі (checked), тобто, котрі перевіряє компілятор, і неперевіряючі (unchecked). При перевірці
компілятор помічає необроблені в методах і конструкторах виключення і вважає за помилку відсутність в
заголовку таких методів і конструкторів з поміткою throws. Саме для уникнення подібних помилок ми в
попередніх уроках вставляли в лістинги блоки обробки виключень. Так от, виключення класу
RuntimeException і його підкласів, одним із яких являється ArithmeticException, неперевіряючі, для них
помітка throws необовязкова. Ще одно велике сімейство неперевіряючих виключень складає клас Error і
його розширення. Чому компілятор не перевіряє ці типи виключень? Причина в тому, що виключення
класу RuntimeException свідчать про помилки в програмі, і 'єдино розумний метод їх обробки — виправити
вихідний текст програми і перекомпілювати її. Що стосується класу Error, то ці виключення дуже важко
локалізувати і на стадії компіляції неможливо визначити місце їх появи.
Напроти, виникнення перевіряючого виключення показує, що програма недостатньо продумана, не всі
можливі ситуації описані. Така програма повинна бути допрацьована, про що і нагадує компілятор. Якщо
метод або конструктор викидає декілька виключень, то їх треба перечислити через кому після слова
throws. Заголовок метода main() лістингу 16.1, якби виключення, які він викидає, не були б обєктами
підкласів класу RuntimeException, належало б написати так:
255
256. public static void main(String[] args)
throws ArithmeticException, ArrayIndexOutOfBoundsException{
// Зміст методу
}
Перенесемо тепер обробку ділення на нуль в метод f() і додамо трасуючий друк, як це зроблено в лістинзі
16.4. Результат — на рис. 16.3.
Лістинг 16.4. Обобка виключення в методі
class SimpleExt3{
private static void f(int n){ // throws ArithmeticException{
try{
System.out.println(" 10 / n = " + (10 / n) ) ;
System.out.println("From f() after results output");
}catch(ArithmeticException ae){
System.out.println("From f() catch: " + ae) ;
// throw ae;
}finally{ System.out.println("From f() finally"); }
}
public static void main(String[] args){
try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()");
f (n);
System.out.println("After results output"); }
catch(ArithmeticException ae){
System.out.println("From Arithm.Exc. catch: "+ae);
}catch(ArrayIndexOutOfBoundsException arre){
System.out.println("From Array.Exc. catch: "+arre);
}finally{ System.out.println("From finally");}
System.out.println("After all actions");
}
}
Рис. 16.3. Обробка виключення в методі
Уважно прослідкуйте за передачею управління і замітьте, що виключенні класу ArithmeticException уже не
256
257. викидається в метод main(). Оператор try {} catch() {} в методі f() можна розглядати як вкладений в
оператор обробки виключень в методі main(). При необхідності виключення можна викинути оператором
throw(). В лістинзі 16.4 цей оператор показаний як коментар. Приберіть символ коментаря //,
перекомпілюйте програму і подивіться, як зміниться її виведення.
16.4. Оператор throw
Цей оператор дуже простий: післе слова throw через пробіл записується объект класу-виключення.
Досить часто він створюється прямо в операторі throw, наприклад:
throw new ArithmeticException();
Оператор можна записати в будь-якому місці програми. Він негайо викидає записаний в ньому обєкт-
виключення і далі обробка цього виключення іде як звичайно, ніби-то тут відбулося ділення на нуль або
інша дія, що викликала виключення класу ArithmeticException. Отже, кожний блок catch() перехоплює
один певний тип виключень. Якщо треба однаково обробити декілька типів виключень, то можна
скористатися тим, що класи-виключенн утворюють ієрархію. Змінимо ще раз лістинг 16.2, одержавши
лістинг 16.5.
Лістинг 16.5. Обробка декількох типів виключень
class SimpleExt4{
public static void main(String[] args){
try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()");
System.out.println(" 10 / n = " + (10 / n) ) ;
System.out.println("After results output");
}catch(RuntimeException ae){
System.out.println("From Run.Exc. catch: "+ae);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
257
258. }
В лістинзі 16.5 два блоки catch() {} замінені одним блоком, перехоплюючим виключення класу
RuntimeException. Як видно на рис. 16.4, цей блок перехоплює обидва виключення. Чому? Тому що це
виключення підкласів классу RuntimeException.
Рис. 16.4. Перехват декількох типів виключень
Таким чином, переміщаючись по ієрархії класів-виключень, ми можемо обробляти зразу більш-менш
крупні сукупності виключень. Розглянемо детальніше ієрархію класів-виключень.
16.5. Ієрархія класів-виключень
Всі класи-виключення розширюють клас ThrowabІe — безпосереднє розширення класу Оbject. У класі
ThrowabІe і у всіх його розширеннях два конструктори:
• ThrowabІe() — конструктор по замовчуванню;
• ThrowabІe (String message) — створюваний объект буде містити довільне повідомлення message.
Записане в конструкторі повідомлення можна отримати потім методом getMessage(). Якщо обєкт
створювася конструктором по замовчуваннюю, то даний метод поверне null. Метод toString() повертає
короткий опис подї, саме він працював у попередніх лістингах. Три методи виводять повідомлення про всі
методи, що зустрілися на шляху "польоту" виключення:
• printstackTrace() — виводить повідомлення в стандартне виведення, як правило, це консоль;
• printStackTrace(PrintStream stream) — виводить повідомлення в байтовий потік stream;
• printStackTrace(PrintWriter stream) — виводить повідомлення в символьний потік stream.
У класі ThrowabІe два безпосередніх наслідники — класи Error і Exception. Вони не додають нових
методів, а служать для розподілу класів-виключень на два великиих сімейства - сімейство класів-помилок
(error) і сімейство власне класів-виключень (exception). Класи-помилки, розширяючі клас Error, свідчать
про виникнення складних ситуацій у віртуальній машині Java. Їх обробка вимагає глибокого розуміння всіх
тонкощів роботи JVM. Її не рекомендується виконувати в звичайній програмі. Не радять навіть викидати
помилки оператором throw. He треба робити свої класи-виключення розширеннями класу Error або
якогось його підкласу. Імена класів-помилок закінчуються словом Error. Класи-виключення, розширяючі
класс Exception, відмічають виникнення звичайної нештатної ситуації, яку можна і навіть потрібно
258
259. обробити. Такі виключення належить викинути оператором throw. Класів-виключень дуже багато, більше
двохсот. Вони розкидані буквально по всіх пакетах J2SDK. В більшості випадків ви здатні підібрати
готовий клас-виключення для обробки виключних ситуацій в своїй програмі. При бажанні можна створити і
свій клас-виключення, розширивши клас Exception або будь-який його підклас.
Серед класів-виключень виділяється клас RuntimeException — пряме розширення класу Exception. В
ньому і його підкласах відмічаютьсч виключення, що виникли при роботі JVM, але не такі серйозні, як
помилки. Їх можна обробляти і викидати, розширяти своїми класами, але краще довірити це JVM, оскільки
частіше всього це просто помилка в програмі, яку треба виправити. Особливість виключенб даного класу
в тому, що їх не треба відмічати в заголовку метода поміткою throws. Імена класів-виключень
закінчуються словом Exception.
16.6. Порядок обробки виключень
Блоки catch () {} перехоплюють виключення в порядку написання цих блокцв. Це правило приводить до
цікавих результатів. В лістинзі 16.2 ми записали два блоки перехоплення catch() і обидва блоки
виконувались при виникненні відповідного виключення. Це відбувалося тому, що класи-виключення
ArithmeticException і ArrayindexOutofBoundsException знаходяться на різних гілках ієрархії виключень .
Інакше буде, якщо блоки catch() {} перехоплюють виключення, розташовані на одній гілці. Якщо в лістинзі
16.4 після блоку, перехоплюючого RuntimeException, помістити блок, обробляючий вихід індекса за межі:
try{
// Оператори, викликаючі виключення
}catch(RuntimeException re){
// Якась обробка
}catch(ArrayIndexOutOfBoundsException ae){
// Ніколи не буде виконано!
}
то він не буде виконуватися, оскільки виключення цього типу являється, до того ж, виключенням
загального типу RuntimeException і буде перехоплюватися попереднім блоком catch () {}.
16.7. Створення власних виключень
Перш за все, треба чітко визначити ситуації, в яких буде виникати ваші власні виключення, і подумати, не
чи не стане його перехоплення заразом перехоплювати також і інші, не враховані вами виключення.
Потім треба вибрати суперклас створюваного класу-виключення. Ним може бути клас Exception або один
із його чисельних підкласів. Після цього можна написати клас-виключення. Його імя повинне
завершатися словом Exception. Як правило, цей клас складається тільки із двох конструкторів і
перевизначення методів toString() і getMessage().
Розглянемо простий приклад. Нехай метод handle(int cipher) обробляє арабські цифри 0—9, які
передаються йому в аргументі cipher типу int. Ми хочемо викинути виключення, якщо аргумент cipher
виходить за діапазон 0—9. Перш за все, упевнимося, що такого виключення немає в ієрархіїи класів
Exception. До всього іншого, не відслідковується і більш загальна ситуація попадання цілого числа в
якийсь діапазон. Тому будемо розширяти наш клас. Назвемо его cipherException, прямо від класу
Exception. Визначимо класс CipherException, як показано в лістинзі 16.6, і використаємо його в класі
ExceptDemo. На рис. 16.5 продемонстровано виведення цієї програми.
Лістинг 16.6. Створення класу-виключення
class CipherException extends Exception{
private String msg;
CipherException(){ msg = null;}
CipherException(String s){ msg = s;}
public String toString(){
return "CipherException (" + msg + ")" ;
}
}
259
260. class ExceptDemo{
static public void handle(int cipher) throws CipherException{
System.out.println("handle()'s beginning");
if (cipher < 0 || cipher > 9)
throw new CipherException("" + cipher);
System.out.println("handle()'s ending");
}
public static void main(String[] args){
try{
handle(1) ;
handle(10); }
catch(CipherException ce){
System.out.println("caught " + ce) ;
ce.printStackTrace(); }
}
}
Рис. 16.5. Обробка власного виключення
16.8. Заключення
Обробка виключних ситуацій стала тепер обовязковою частиною обєктно-орієнтованих програм.
Застосовуючи методи класів J2SDK і інших пакетів, звертайте увагу на те, які виключення вони
викидають, і обробляйте їх. Виключення різко змінюютьт хід виконання програми, роблять його
заплутаним. Не займайтеся складною обробкою, памятайте про принцип KISS. Наприклад, із блоку
finally{} можна викинути виключення і обробити його в іншому місці. Подумайте, що відбудеться в цьому
випадку з виключенням, що виникло в блоці try{}? Воно ніде не буде перехоплено і оброблено.
260
262. Урок 17
Підпроцеси
• Клас Thread
• Синхронізація підпроцесів
• Узгодження роботи декількох підпроцесів
• Пріоритети підпроцесів
• Підпроцеси-демони
• Групи підпроцесів
• Заключення
17.1. Процес
Основне поняття сучасних операційних систем — процесс (process). Як і всі загальні поняття, процес
важко визначити. Можна розуміти під процесом виконувану (runnable) програму, але треба памятати про
те, що у процесу єсть декілька станів. Процес може в будь-який момент перейти до виконання машинного
коду іншої програми, а також "заснути" (sleep) на деякий час, призупинивши виконання програми. Він
може бути вивантажений на диск. Кількість станів процесу і їх особливості залежать від операційної
системи.
Всі сучасні операційні системи багатозадачні (multitasking), вони запускають і виконують зразу декілька
процесів. Одночасно може працювати браузер, текстовий редактор, музичний програвач. На екрані
дисплея відкриваються декілька вікон, кожне з яких звязано із своїм працюючим процесом. Якщо на
компютері тільки один процессор, то він переключається з одного процесу на другий, створюючи
видимість одночасної роботи. Переключення відбувається по закінченню одного або декількох "тиків"
(ticks). Розмір тику залежить від тактової частоти процесора і звичайно має порядок 0,01 секунди.
Процесам назначаються різні пріоритети (priority). Процеси з низьким пріоритетом не можуть перервати
виконання процесу з більш високим пріоритетом, вони менше займають процесор, тому виконуються
повільно, як говорять, "на фоні". Самий високий пріоритет у системних процесів, наприклад, у диспетчера
(scheduler), який як раз і займається переключенням процесора з процесу на процесс. Такі процеси не
можна переривати, поки вони не закінчать работу, інакше компютер швидко прийде в хаотичний стан.
Кожному процесу виділяється певна область оперативної памяті для розміщення коду програми і її даних
— його адресний простір. В цю ж область записується частина даних про процес, складаюча його
контекст (context). Дуже важливо розділить адресний простір різних процесів, щоб вони не могли змінити
код і дані один одного. Операційні системи по-різному відносяться до забезпечення захисту адресних
просторів процесів. MS Windows NT/2000 ретельно розділяють адресні простори, витрачаючи на це
багато ресурсів і часу. Це підвищує надійність виконання програми, але утруднює створення процесу.
Такі операційні системи погано справляються з управлінням великого числа процесів.
Операційні системи сімейства UNIX менше турбуються про захист памяті, але легше створюють процеси і
здатні управляти сотнею одночасно працюючих процесів. Крім управління работою процесів операційна
система повинна забезпечити засоби їх взаємодії: обмін сигналами і повідомленнями, створення спільних
декільком процесам областей памяті і виконуваного коду програми. Ці засоби також вимагають ресурсів і
уповільнюють роботу компютера.
Роботу багатозадачної системи можна спроститс і прискорити, якщо дозволити взаємодіючим процесам
працювати в одному адресному просторі. Такі процесси називаються threads. Буквальний переклад -
"нитка", але ми зупинимося на слові "підпроцес". Підпроцеси створюють нові труднощі для операційної
системи — треба дуже уважно слідкувати за тим, щоб вони не заважали один одному при запису в спільні
ділянки памяті, — але зате полегшують взаємодію підпроцесів. Створення підпроцесів і управління ними
— це справа операційної системи, але в Java введені засоби для виконання цих дій. Оскільки програми,
написані на Java, повинні працювати у всіх операційних системах, ці засоби дозволяють виконувати
тільки самі загальні дії.
Коли операційна система запускає віртуальну машину Java для виконання додатку, вона створює один
процес з декількома підпроцесами. Головний (main) підпроцес виконує байт-коди програми, а саме, він
зразу ж звертається до методу main() додатку. Цей підпроцес може породити нові підпроцеси, які, в свою
262
263. чергу, здатні породить підпроцеси і т. д. Головним підпроцесом аплета являеться один із підпроцесів
браузера, в якому аплет виконується. Головний підпроцес не грає ніякої особливої ролі, просто він
створюється першим.
Підпроцес в Java створюється і управляється методами класу Thread. Після створення обєкта цього класу
одним із його конструкторів новий підпроцес запускається методом start (). Отримати посилку на поточний
підпроцес можна статичним методом Thread.currentThread() ;
Клас Thread реалізує інтерфейс RunnabІe. Цей інтерфейс описує тільки один метод run(). Новий
підпроцес буде виконувати те, що записано в цьому методі. Між іншим, клас Thread містить тільки пусту
реалізацію методу run(), тому клас Thread не використовується сам по собі, він завжди розширюється.
При його розширенні метод run() перевизначається. Метод run() не містить аргументів, так як нікому
передавати їх значення в метод. Він не повертає значення, його нікуди передавати. До методу run() не
можна звернутися із програми, це завжди робиться автоматично виконуючою системою Java при запуску
нового підпроцесу методом start ().
Отже, задати дії створюваного підпроцесу можна двома способами: розширити клас Thread або
реалізувати інтерфейс RunnabІe. Перший спосіб дозволяє використовувати методи класу Thread для
управління підпроцесом. Другий спосіб застосовується в тих випадках, коли треба тільки реалізувати
метод run(), або клас, створюючий підпроцес, уже розширяє якийсь інший клас. Подивимося, які
конструктори і методи містить клас Thread.
17.2. Клас Thread
В класі Thread сім конструкторів:
• Thread(ThreadGroup group, Runnable target, String name) — створює підпроцес з іменем name,
належний групі group і виконуючий метод run() обєкта target. Це основной конструктор, всі інші
звертаються до нього з тим чи іншим параметром, рівним null;
• Thread() — створюваний підпроцесс буде виконувати свій метод run();
• Thread(Runnable target);
• Thread(Runnable target, String name);
• Thread(String name);
• Thread(ThreadGroup group, Runnable target);
• Thread(ThreadGroup group, String name).
Імя підпроцесу name не має ніякого значення, воно не використовується віртуальною машиною Java і
застосовується тільки для відрізняння підпроцесів в програмі. Після створення підпроцесу його треба
запустити методом start(). Віртуальна машина Java почне виконувати метод run() цього обєкта-
підпроцеса. Підпроцес завершить роботу після виконання метода run(). Для знищення обєкта-підпроцеса
вслід за цим він повинен присвоїти значення null.
Виконуваний підпроцес можна призупинити статичним методом sleep (long ms) на ms мілісекунд. Цей
метод ми уже використовували в попередніх уроках. Якщо обчислювальна система може відраховувати
наносекунди, то можна призупинити підпроцес з точністю до наносекунд методом sleep(long ms, int
nanosec). В лістинзі 17.1 приведено найпростіший приклад. Головний підпроцесс створює два підпроцеси
з іменами Thread1 і Thread 2, виконуючих один і той же метод run(). Цей метод просто виводить 20 раз
текст на екран, а потім повідомляє про своє завершення.
Лістинг 17.1. Два підпроцеси, запущені із головного підпроцесу
class OutThread extends Thread{
private String msg;
OutThread(String s, String name){
super(name); msg = s;
}
public void run()
{
for(int i = 0; i < 20; i++){
263
264. // try{
// Thread.sleep(100);
// }catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of " + getName());
}
} class TwoThreads{
public static void main(String[] args){
new OutThread("HIP", "Thread 1").start();
new OutThread("hop", "Thread 2").start();
System.out.println();
}
}
На рис. 17.1 показано результат двох запусків програми лістингу 17.1. Як бачите, в першому випадку
підпроцес Thread1 встиг відпрацювати повністю до переключення процесора на виконання другого
підпроцесу. В другому випадку робота підпроцеса Thread1 була перервана, процесор переключився на
виконання підпроцеса Thread 2, встиг виконати його повністю, а потім переключився знову на виконання
підпроцеса Thread1 і завершив його.
Рис. 17.1. Два підпроцеси працюють без затримки
Це дуже повчальний приклад, але якщо у вас сучасний компютер з більшою швидкістю дії, то запустивши
на ньому програму лістингу 17.1 ви можете побачити зовсім іншу картину. Підпроцеси можуть спрацювати
так швидко, що переключення не здійсниться.
Приберемо в лістинзі 17.1 коментарі, затримавши тим самим виконання кожної ітерації циклу на 0,1
секунди. Пуста обробка виключення InterruptedException означає, що ми ігноруємо спробу переривання
роботи підпроцеса. На рис. 17.2 показано результат двох запусків програми. Як бачите, процесор
переключається з одного підпроцеса на інший, але в одному місці регулярність переключення
порушується і раніше запущений підпроцес завершується пізніше.
Як же досягти узгодженості, як говорять, синхронізації (synchronization) підпроцесів? Обговоримо це
нижче, а поки що покажемо ще два варіанти створення тієї ж самої програми. В лістинзі 17.2 приведено
другий варіант тієї ж програми: сам клас TwoThreads2 являється розширенням класу Thread, а метод run()
реалізується прямо в ньому.
Лістинг 17.2. Клас розширює Thread
class TwoThreads2 extends Thread{
private String msg;
TwoThreads2(String s, String name){
264
265. super(name); msg = s;
}
public void run(){
for(int i = 0; i < 20; i++){
try{
Thread.sleep(100);
}catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of " + getName());
}
public static void main(String[] args)(
new TwoThreads2("HIP", "Thread 1").start();
new TwoThreads2("hop", "Thread 2").start();
System.out.println();
}
}
Третій варіант: клас TwoThreads3 реалізує інтерфейс RunnabІe. Цей варіант записаний в лістинзі 17.3. Тут
не можна використовувати методи класу Thread, але зате клас TwoThreads3 може бутиь розширенням
іншого класу. Наприклад, можна зробити його аплетом, розширивши клас Applet або JAppІet.
Лістинг 17.3. Реалізація інтерфейса Runnabie
class TwoThreadsS implements RunnabІe{
private String msg;
TwoThreads3(String s){ msg = s; }
public void run(){
forfint i = 0; i < 20; i++){
try{
Thread.sleep(100);
}catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of thread.");
}
public static void main (String[] args){
new Thread(new TwoThreads3("HIP"), "Thread 1").start ();
new Thread(new TwoThreads3("hop"), "Thread 2").start ();
System.out.println();
}
}
Рис. 17.2. Підпроцеси працюють із затримкою
265
266. Частіше всього в новому підпроцесі задаються нескінчені дії, виконувані на фоні основних дій:
програється музика, на екрані крутиться анімований логотип фірми, біжить рекламний рядок. Для
реалізації такого підпроцеса в методі run() задається нескінчений цикл, зупинюваний після того, як обєкт-
підпроцес отримає значення null. В лістинзі 17.4 показано четвертий варіант тієї ж самої програми, в якій
метод run() виконується до тих пір, доки поточний обєкт-підпроцес th співпадає з обєктом gо, запустившим
поточний підпроцес. Для переривання його виконання передбачений метод stop(), до якого звертається
головний підпроцес. Ця стандартна конструкція, рекомендована документацією J2SDK. Головний
підпроцес в даному прикладі тільки створює обєкти-підпроцеси, чекає одну секунду і зупиняє їх.
Лістинг 17.4. Зупинка роботи підпроцесів
class TwoThreadsS implements Runnabie{
private String msg;
private Thread go;
TwoThreadsS(String s){
msg = s;
go = new Thread(this);
go.start();
}
public void run(){
Thread th = Thread.currentThread();
while(go == th){
try{
Thread.sleep(100);
}catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of thread.");
}
public void stop(){ go = null; }
public static void main(String[] args){
TwoThreadsS thl = new TwoThreadsS("HIP");
TwoThreadsS th2 = new TwoThreadsS("hop");
try{
Thread.sleep(1000);
}catch(InterruptedException ie){}
thl.stop(); th2.stop();
System.out.printlnf);
}
}
17.3. Синхронізація підпроцесів
Основна складність при написанні програм, в яких працюють декілька підпроцесів — це узгодити сумісну
роботу підпроцесів із загальними комірками памяті. Класичний приклад — банківська трансакція, в якій
змінюється залишок на рахунку клієнта з номером numDep. Припустимо, що для її виконання
запрограмовані такі дії:
Deposit myDep = getDeposit(numDep); // Отримуємо рахунок з номером numDep
int rest = myDep.getRest();// Отримуємо залишок на рахунку myDep
Deposit newDep = myDep.operate(rest, sum); // Змінюємо залишок на величину sum
myDep.setDeposit(newDep); // Заносимо новий залишок на рахунок myDep
Нехай на рахунку лежить 1000 гривнів. Ми вирішили зняти з рахунку 500 гривнів, а в той же час поступив
поштовий переказ на 1500 гривнів. Ці дії виконують різні підпроцеси, але змінюють вони один і той же
рахунок myDep з номером numDep. Подивившись ще раз на рис. 17.1 і 17.2, ви повірите, що послідовність
дій може скластися так. Перший підпроцес проробить віднімання 1000 -500, в цей час другий підпроцес
виконає всі три дії і запише на рахунок 1000+1500 = 2500 гривнів, після чого перший підпроцес виконає
свою останню дію і у нас на рахунку виявиться 500 гривнів. Навряд ии вам сподобається таке виконання
266
267. двох трансакцій.
В мові Java прийнятий виход із цього положення, названий в теорії операційних систем монітором
(monitor). Він заключається в тому, що підпроцес блокує обєкт, з яким працює, щоб другі підпроцеси не
могли звернутися до даного обєкту, поки блокування не буде знято. В нашому прикладі перший підпроцес
повинен спочатку заблокувати рахунок myDep, потім повністю виконати всю трансакцію і зняти
блокування. Другий підпроцес призупиниться і стане чекати, поки блокування не буде знято, після чого
почне працювати з обєктом myDep. Все це робиться одним оператором synchronized() {}, як показано
нижче:
Deposit myDep = getDeposit(numDep);
synchronized(myDep){
int rest = myDep.getRest();
Deposit newDep = myDep.operate(rest, sum);
myDep.setDeposit(newDep);
}
В заголовку оператора synchronized в дужках указується посилання на обєкт, який буде заблокований
перед виконанням блоку. Обєкт буде недоступний для інших підпроцесів, поки виконується блок. Після
виконання блоку блокування знімається. Якщо при написанні якогось методу виявилось, що в блок
synchronized входять всі оператори цього методу, то можна просто помітить метод словом synchronized,
зробивши його синхронізованим (synchronized):
synchronized int getRest()(
// Тіло методу
}
synchronized Deposit operate(int rest, int sum) {
// Тіло методу
}
synchronized void setDeposit(Deposit dep){
// Тіло методу
}
В цьому випадку блокується объект, виконуючий метод, тобто this. Якщо всі методи, до яких не повинні
одночасно звертатися декілька підпроцесів, помічені synchronized, то оператор synchronized() {} уже не
потрібний. Тепер, якщо один підпроцес виконує синхронізований метод обєкта, то інші підпроцеси уже не
можуть звернутися до жодного синхронізованого методу того ж самого обєкта.
Приведемо простий приклад. Метод run() в лістинзі 17.5 виводить рядок "Hello, World!" із затримкою в 1
секунду між словами. Цей метод виконується двома підпроцесами, працюючими з одним обєктом th.
Програма виконується два рази. Перший раз метод run() не синхронзований, другий раз синхронізований,
його заголовок показано в лістинзі 17.4 як коментар. Результат виконання програми представлений на
рис. 17.3.
Лiстинг 17.5. Синхронізація методу
class TwoThreads4 implements Runnable{
public void run(){
// synchronized public void run(){
System.out.print("Hello, ");
try{
Thread.sleep(1000);
}catch(InterruptedException ie){}
System.out.println("World!");
}
public static void main(String[] args){
TwoThreads4 th = new TwoThreads4();
new Thread(th).start();
new Thread(th).start();
267
268. }
}
Рис. 17.3. Синхронізація методу
Дії, що входять в синхронізований блок або метод створюють критичну ділянку (critical section) програми.
Декілька підпроцесів, що збираються виконувати критичну ділянку, стають в чергу. Це сповільнює роботу
програми, тому для швидкого її виконання критичних ділянок повинно бути як можна менше, і вони
повинні бути як можна коротші. Багато методів Java 2 SDK синхронізовані. Зверніть увагу, що на рис.
17.1 слова виводяться впереміжку, але кожне слово виводиться повністю. Це відюувається тому, що
метод print() класу Printstream синхронізований, при його виконанні вихідний потік system.out блокується
до тих пір, доки метод print () не закінчить свою роботу.
Отже, ми можемо легко організувати послідовний доступ декількох підпроцесів до полів одного обєкта за
допомогою оператора synchronized() {}. Синхронізація забезпечує взаємно виключаюче (mutually
exclusive) виконання підпроцесів. Але що робити, якщо потрібний сумісний доступ декількох підпроцесів
до спільних обєктів? Для цього в Java існує механізм очікування і сповіщення (wait-notify).
17.4. Узгодження роботи декількох підпроцесів
Можливість створення багатопотокових програм закладена в Java з самого її створення. В кожному
обєкті єсть три методи wait() і один метод notify(), дозволяючі призупиняти роботу підпроцесу з цим
обєктом, дозволити іншому підпроцесу попрацювати з обєктом, а потім сповістити (notify) перший
підпроцес про можливість продовження роботи. Ці методи визначені прямо в класі object і наслідуються
всіма класами. З кожним обєктом звязано багато підпроцесів, очікуючих доступу до обєкту (wait set).
Спочатку цей "зал очікування" порожній.
Основний метод wait (long miІІisec) призупиняє поточний підпроцес this, працюючий з обєктом, на miІІisec
мілісекунд і переводить його в "зал очікування", в множину очікуючих підпроцесів. Звернення до цього
методу допускається тільки в синхронізованому блоці або методі, щоб бути впевненими в тому, що з
обєктом працює тільки один підпроцес. Через miІІisec або після того, як обєкт отримає сповіщення
методом notify(), підпроцес готовий відновити роботу. Якщо аргумент miІІisec рівний 0, то час очікування
не визначено і відновлення роботи підпроцесу можна тільки після того, як обєкт отримає сповіщення
методом notify(). Відміна даного методу від методу sleep() в тому, що метод wait() знімають блокування з
обєкта. З обєктом може працювати один із підпроцесів із "зала очікування", звичайно той, який чекав
довше всіх, хоч це не гарантуеться специфікацією JLS.
Другий метод wait () еквівалентний wait(0). Третій метод wait (long millisec, int nanosec) уточнює затримку
на nanosec наносекунд, якщо їх зуміє відрахувати операційна система. Метод notify() виводить із "зали
очікування" тільки один, довільно вибраний підпроцес. Метод notifyAll() виводить із стану очікування всі
підпроцеси. Ці методи теж повинні виконуватися в синхронізованому блоці або методі. Як же застосувати
все це для узгодженого доступу до обєкта? Як завжди, краще всього пояснити це на прикладі.
268
269. Звернемося знову до схеми "постачальик-споживач", уже використану в уроці 15. Один підпроцес,
постачальник, робить обчислення, другий, споживач, очікує результати цих обчислень і використовує їх в
міру поступання. Підпроцеси передають інформацію через спільний екземпляр st класу store. Робота цих
підпроцесів повинна бути узгоджена. Споживач зобовязаний чекати, доки постачальник не занесе
результат обчислення в обєкт st, а постачальник повинен чекати, доки споживач не візьме цей результат.
Для простоти постачальник просто заносить в спільний обєкт класу store цілі числа, а споживач лише
забирає їх. В лістинзі 17.6 клас store не забезпечує узгодженості отримання і видачі інформацію.
Результат роботи показаний на рис. 17.4.
Лістинг 17.6. Неузгоджені підпроцеси
class Store{
private inf inform;
synchronized public int getlnform(){ return inform; }
synchronized public void setlnform(int n){ inform = n; }
}
class Producer implements Runnable{
private Store st;
private Thread go;
Producer(Store st){
this.st = st;
go = new Thread(this);
go.start();
}
public void run(){
int n = 0;
Thread th = Thread.currentThread();
while(go == th){
st.setlnform(n);
System.out.print("Put: " + n + " ");
n++;
}
}
public void stop(){ go = null;
}
}
class Consumer implements Runnable{
private Store st;
private Thread go;
Consumer(Store st){
this.st = st;
go =-new Thread(this);
go.start () ;
}
public void run(){
Thread th = Thread.currentThread();
while(go == th) System.out.println("Got: " + st.getlnformf));
}
public void stop(){ go = null; }
}
class ProdCons{
public static void main(String[] args){
Store st = new Store();
Producer p = new Producer(st);
Consumer с = new Consumer(st);
try{
Thread.sleep(30);
}catch(InterruptedException ie){}
269
270. p.stop(); c.stop();
}
}
Рис. 17.4. Неузгоджена робота двох підпроцесів
В лістинзі 17.7 в клас store внесено логічне поле ready, що відмічає процес отримання і видачі інформації.
Коли нова порція информації отримана від постачальника Producer, в полі ready заноситься значення
true, отримувач consumer може забирати цю порцію інформації. Після видачі інформації змінна ready
становиться рівною false. Але цього мало. Те, що отримувач може забрати продукт, не означає, що він
дійсно забере його. Тому в кінці методу setinform() отримувач сповіщається про поступанні продукту
методом notify(). Поки поле ready не прийме потрібне значення, підпроцес переводится в "залу
очікування" методом wait(). Результат роботи програми з обновленим класом store показаний на рис. 17.5.
Лістинг 17.7. Узгодження отримання і видачі інформації
class Store{
private int inform = -1;
private boolean ready;
synchronized public int getlnform(){
try{
if (! ready) wait();
ready = false;
return inform;
}catch(InterruptedException ie){
}finally!
notify();
}
return -1;
}
synchronized public void setlnform(int n)(
if (ready)
try{
wait ();
}catch(InterruptedException ie){}
inform = n;
ready = true;
notify();
}
}
Оскільки сповіщення поставщика в методі getinform() повинно відбуватися уже після відправки інформації
оператором return inform, воно включено в блок finally{}
Зверніть увагу: повідомлення "Got: 0" відстає на один крок від дійсного отримання інформації.
270
271. Рис. 17.5. Узгоджена робота підпроцесів
17.5. Пріоритети підпроцесів
Планувальник підпроцесів віртуальної машини Java призначає кожному підпроцесу однаковий час
виконання процесором, переключаючись з підпроцеса на підпроцес по закінченню цього часу. Інколи
необхідно виділити якомусь підпроцесу більше або менше часу в порівнянні з іншим підпроцесом. В
такому випадку можна задати підпроцесу більший або менший пріоритет. В класі Thread єсть три цілі
статичні константи, що задають пріоритети:
• NORM_PRIORITY — звичайний пріоритет, який одержує кожний підпроцес при запуску, його
числове значення 5;
• MIN_PRIORITY — найменший пріоритет, його значення 1;
• MAX_PRIORITY — найвищий пріоритет, його значення 10.
Крім цих значень можна задать будь-яке проміжне значення від 1 до 10, але треба памятати про те, що
процесор буде переключатися між підпроцесами з однаковим вищим пріоритетом, а підпроцеси з меншим
пріоритетом не стануть виконуватися, якщо тільки не призупинені всі підпроцеси з вищим пріоритетом.
Тому для підвищення загальної продуктивності належить призупиняти час від часу методом sleep()
підпроцеси з високим пріоритетом.
Установити той чи інший пріоритет можна в будь-який час методом setPriorityfint newPriority), якщо
підпроцес має право змінювати свій пріоритет. Перевірити наявність такого права можна методом
checkAtcess(). Цей метод викидає виключення класу SecurityЕxception, якщо підпроцес не може змінити
свій пріоритет. Породжені підпроцеси будуть мати той же пріоритет, що і підпроцес-батько. Отже,
підпроцеси, як правило, повинні працювати з пріоритетом NORM_PRIORITY. Підпроцеси більшу частину
часу очікуючі настання якоїсь події, наприклад, натискання користувачем кнопки Вихід, можуть отримати
більш високий пріоритет MAX_PRIORITY. Підпроцеси, виконуючі тривалу роботу, наприклад, установку
мережевого зєднання або рисування зображення в памяті при подвійній буферизації, могжть працювати з
нижчим пріоритетом MIN_PRIORITY.
17.6. Підпроцеси-демони
Робота програми починається з виконання метоуа main() головним підпроцесом. Цей підпроцес може
породити інші підпроцеси, вони, в свою чергу, здатні породити свої підпроцеси. Після цього головний
підпроцес нічим не буде відрізнятися від решти підпроцесів. Він не слідкує за породженими ним
підпроцесами, не чекає від них ніяких сигналів. Головний підпроцес може завершитися, а програма буде
продовжувати роботу, доки не закінчить роботу останній підпроцес. Це правило не завжди зручне.
Наприклад, якийсь із підпроцесів може призупинитися, очікуючи мережевого зєднання, яке ніяк не може
наступити. Користувач, не дочекавшись зєднання, зупиняє роботу головного підпроцесу, але програма
продовжує працювати.
271
272. Такі випадки можна врахувати, оголосивши деякі підпроцеси демонами (daemons). Це поняття не
співпадає з поняттям демона в UNIX. Просто програма завершується по закінченні роботи останнього
користувальського (user) підпроцесу, не чекаючи закінчення роботи демонів. Демони будуть примусово
завершені виконуючою системою Java. Оголосити підпроцес демоном можна зразу після його створення,
перед запуском. Це робиться методом setDaemon(true). Даний метод звертаэться до методу
checkAccess() і може викинути SecurityException. Змінити статус демона після запуску процесу уже
неможна. Всі підпроцеси, породжені демоном, теж будуть демонами. Для зміни їх статусу необхідно
звернутися до методу setDaemon(false).
17.7. Групи підпроцесів
Підпроцеси обєднуються в групи. На початку роботи програми виконуюча система Java створює групу
підпроцесів з іменем main. Всі підпроцеси по замовчуванню попадають в цю групу. В будь-який час
програма може створити новіе групи підпроцесів і підпроцеси, що входять в ці групи. Спочатку
створюється група — екземпляр класуа ThreadGroup, конструктором
ThreadGroup(String name)
При цьому група отримує імя, задане аргументом name. Потім цей екземпляр указується при створенні
підпроцесіов в конструкторах класу Thread. Всі підпроцеси попадуть в групу з іменем, заданим при
створенні групи. Групи підпроцесів можуть утворювати ієрархію. Одна група породжується від другої
конструктором
ThreadGroup(ThreadGroup parent, String name)
Групи підпроцесіов використовуються головним чином для задання пріоритетів підпроцесам всередині
групи. Зміна пріоритетів всередині групи не буде впливати на пріоритети підпроцесів зовні ієрархії цієї
групиы. Кожна група маєт максимальний пріоритет, устанавлюваний методом setMaxPriority(int maxPri)
класу ThreadGroup. Ні один підпроцес із цієї групи не може перевищити значення maxPri, але пріоритети
підпроцесів, задані до установки maxPri, не змінюються.
17.8. Заключення
Технологія Java по своїй суті — багатозадачна технологія, основана на threads. Це одна із причин, по яких
технологія Java так і не може розумним способом реалізовуватися в MS-DOS і Windows 3.1, незважаючи
на багато спроб. Тому, конструюючи програму для Java, належить весь час памятати, що вона буде
виконуватися в багатозадачному середовищі. Треба ясно представлять собі, що буде, якщо програма
почне виконуватися одночасно кількома підпроцесами, виділяти критичні ділянки і синхронізовувати їх. З
другого боку, якщои програма виконує декілька дій, треба подумати, чи не зробити їх виконання
одночасним, створивши додаткові підпроцеси і розподіливши їх пріоритети.
272
273. В лістинзі 17.1 приведено найпростіший приклад. Головний підпроцесс створює
два підпроцеси з іменами Thread1 і Thread 2, виконуючих один і той же метод
run(). Цей метод просто виводить 20 раз текст на екран, а потім повідомляє про
своє завершення.
Лістинг 17.1. Два підпроцеси, запущені із головного підпроцесу
class OutThread extends Thread{
private String msg;
OutThread(String s, String name){
super(name); msg = s;
}
public void run()
{
for(int i = 0; i < 20; i++){
// try{
// Thread.sleep(100);
// }catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of " + getName());
}
} class TwoThreads{
public static void main(String[] args){
new OutThread("HIP", "Thread 1").start();
new OutThread("hop", "Thread 2").start();
System.out.println();
}
}
В лістинзі 17.2 приведено другий варіант тієї ж програми: сам клас TwoThreads2
являється розширенням класу Thread, а метод run() реалізується прямо в ньому.
Лістинг 17.2. Клас розширює Thread
class TwoThreads2 extends Thread{
private String msg;
TwoThreads2(String s, String name){
super(name); msg = s;
}
public void run(){
for(int i = 0; i < 20; i++){
273
274. try{
Thread.sleep(100);
}catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of " + getName());
}
public static void main(String[] args)(
new TwoThreads2("HIP", "Thread 1").start();
new TwoThreads2("hop", "Thread 2").start();
System.out.println();
}
}
Третій варіант: клас TwoThreads3 реалізує інтерфейс RunnabІe. Цей варіант
записаний в лістинзі 17.3. Тут не можна використовувати методи класу Thread, але
зате клас TwoThreads3 може бутиь розширенням іншого класу. Наприклад, можна
зробити його аплетом, розширивши клас Applet або JAppІet.
Лістинг 17.3. Реалізація інтерфейса Runnabie
class TwoThreadsS implements RunnabІe{
private String msg;
TwoThreads3(String s){ msg = s; }
public void run(){
forfint i = 0; i < 20; i++){
try{
Thread.sleep(100);
}catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of thread.");
}
public static void main (String[] args){
new Thread(new TwoThreads3("HIP"), "Thread 1").start ();
new Thread(new TwoThreads3("hop"), "Thread 2").start ();
System.out.println();
}
}
Частіше всього в новому підпроцесі задаються нескінчені дії, виконувані на фоні
основних дій: програється музика, на екрані крутиться анімований логотип фірми,
274
275. біжить рекламний рядок. Для реалізації такого підпроцеса в методі run() задається
нескінчений цикл, зупинюваний після того, як обєкт-підпроцес отримає значення
null. В лістинзі 17.4 показано четвертий варіант тієї ж самої програми, в якій метод
run() виконується до тих пір, доки поточний обєкт-підпроцес th співпадає з
обєктом gо, запустившим поточний підпроцес. Для переривання його виконання
передбачений метод stop(), до якого звертається головний підпроцес. Ця
стандартна конструкція, рекомендована документацією J2SDK. Головний
підпроцес в даному прикладі тільки створює обєкти-підпроцеси, чекає одну
секунду і зупиняє їх.
Лістинг 17.4. Зупинка роботи підпроцесів
class TwoThreadsS implements Runnabie{
private String msg;
private Thread go;
TwoThreadsS(String s){
msg = s;
go = new Thread(this);
go.start();
}
public void run(){
Thread th = Thread.currentThread();
while(go == th){
try{
Thread.sleep(100);
}catch(InterruptedException ie){}
System.out.print(msg + " ");
}
System.out.println("End of thread.");
}
public void stop(){ go = null; }
public static void main(String[] args){
TwoThreadsS thl = new TwoThreadsS("HIP");
TwoThreadsS th2 = new TwoThreadsS("hop");
try{
Thread.sleep(1000);
}catch(InterruptedException ie){}
thl.stop(); th2.stop();
System.out.printlnf);
}
}
275
276. Приведемо простий приклад. Метод run() в лістинзі 17.5 виводить рядок "Hello,
World!" із затримкою в 1 секунду між словами. Цей метод виконується двома
підпроцесами, працюючими з одним обєктом th. Програма виконується два рази.
Перший раз метод run() не синхронзований, другий раз синхронізований, його
заголовок показано в лістинзі 17.4 як коментар.
Лiстинг 17.5. Синхронізація методу
class TwoThreads4 implements Runnable{
public void run(){
// synchronized public void run(){
System.out.print("Hello, ");
try{
Thread.sleep(1000);
}catch(InterruptedException ie){}
System.out.println("World!");
}
public static void main(String[] args){
TwoThreads4 th = new TwoThreads4();
new Thread(th).start();
new Thread(th).start();
}
}
Звернемося знову до схеми "постачальик-споживач", уже використану в уроці 15.
Один підпроцес, постачальник, робить обчислення, другий, споживач, очікує
результати цих обчислень і використовує їх в міру поступання. Підпроцеси
передають інформацію через спільний екземпляр st класу store. Робота цих
підпроцесів повинна бути узгоджена. Споживач зобовязаний чекати, доки
постачальник не занесе результат обчислення в обєкт st, а постачальник повинен
чекати, доки споживач не візьме цей результат. Для простоти постачальник просто
заносить в спільний обєкт класу store цілі числа, а споживач лише забирає їх. В
лістинзі 17.6 клас store не забезпечує узгодженості отримання і видачі
інформацію. Результат роботи показаний на рис. 17.4.
Лістинг 17.6. Неузгоджені підпроцеси
class Store{
private inf inform;
synchronized public int getlnform(){ return inform; }
synchronized public void setlnform(int n){ inform = n; }
}
276
277. class Producer implements Runnable{
private Store st;
private Thread go;
Producer(Store st){
this.st = st;
go = new Thread(this);
go.start();
}
public void run(){
int n = 0;
Thread th = Thread.currentThread();
while(go == th){
st.setlnform(n);
System.out.print("Put: " + n + " ");
n++;
}
}
public void stop(){ go = null;
}
}
class Consumer implements Runnable{
private Store st;
private Thread go;
Consumer(Store st){
this.st = st;
go =-new Thread(this);
go.start () ;
}
public void run(){
Thread th = Thread.currentThread();
while(go == th) System.out.println("Got: " + st.getlnformf));
}
public void stop(){ go = null; }
}
class ProdCons{
public static void main(String[] args){
Store st = new Store();
Producer p = new Producer(st);
Consumer с = new Consumer(st);
try{
Thread.sleep(30);
}catch(InterruptedException ie){}
277
278. p.stop(); c.stop();
}
}
В лістинзі 17.7 в клас store внесено логічне поле ready, що відмічає процес
отримання і видачі інформації. Коли нова порція информації отримана від
постачальника Producer, в полі ready заноситься значення true, отримувач consumer
може забирати цю порцію інформації. Після видачі інформації змінна ready
становиться рівною false. Але цього мало. Те, що отримувач може забрати
продукт, не означає, що він дійсно забере його. Тому в кінці методу setinform()
отримувач сповіщається про поступанні продукту методом notify(). Поки поле
ready не прийме потрібне значення, підпроцес переводится в "залу очікування"
методом wait(). Результат роботи програми з обновленим класом store показаний
на рис. 17.5.
Лістинг 17.7. Узгодження отримання і видачі інформації
class Store{
private int inform = -1;
private boolean ready;
synchronized public int getlnform(){
try{
if (! ready) wait();
ready = false;
return inform;
}catch(InterruptedException ie){
}finally!
notify();
}
return -1;
}
synchronized public void setlnform(int n)(
if (ready)
try{
wait ();
}catch(InterruptedException ie){}
inform = n;
ready = true;
notify();
}
}
278
280. Урок 18
Потоки введення/виведення
• Консольне введення/виведення
• Файлове введення/виведення
• Отримання властиостей файла
• Буферизоване введення/виведення
• Потік простих типів Java
• Кодування UTF-8
• Прямий доступ до файлу
• Канали обміну інформацією
• Серіалізація обєктів
• Друк в Java 2
• Друк засобами Java 2D
• Друк файлу
• Друк сторінок з різними параметрами
18.1. Введення/Виведення в Java
Програми, написані нами в попередніх уроках, сприймали інформацію тільки із параметрів командного
рядка і графічних компонентів, а результати виводили на консоль або в графічні компоненти. Одначе в
багатьох випадках треба виводити результати на принтер, у файл, базу даних або передавати по мережі.
Вихідні дані теж часто приходиться завантажувати із файла, бази даних або із мережі. Для того щоб
абстрагуватись від особливостей конкретних пристроїв введення/виведення, в Java використовується
поняття потоку (stream). Вважається, що в програму іде вхідний поток (input stream) символів Unicode
або просто байтів, що сприймається в програмі методами read(). Із програми методами write() або print (),
println() виводиться вихідний потік (output stream) символів або байтів. При цьому не має значення , куди
направлений потік: на консоль, на принтер, у файл або в мережу, методи write() і print() нічого про це не
знають.
Можна уявити собі потік як трубу, по якій в одному напряму послідовно "течуть" символи або байти, один
за другим. Методи read() , write() , print(), println() взаємодіють з одним кінцем труби, другий кінець
зєднується з джерелом або приймачем даних конструкторами класів, в яких реалізовані ці методи.
Звичайно, повне ігнорування особливостей пристроїв введення/виведення сильно сповільнює передачу
інформації. Тому в Java все-таки виділяється файлове введення/виведення, виведення на друк,
мережевий потік.
Три потоки визначені в класі System статичними полями in, out і err. Їх можна використовувати без всяких
додаткових визначень, що ми весь час і робили. Вони називаються відповідноо стандартним введенням
(stdin), стандартным виведенням (stdout) і стандартним виведенням повідомлень (stderr). Ці
стандартні потоки можуть бути зєднані з різними конкретними присторями введення/виведення. Потоки
out і err — це екземпляри класу Printstream, організуючого вихідний потік байтів. Ці екземпляри виводять
інформацію на консоль методами print(), println() i write(), яких в класі Printstream мається близько
двадцати для різних типів аргументів.
Потік err призначений для виведення системних повідомлень програми: трасування, повідомлень про
помилки абои, просто, про виконання якихось этапів програми. Такі дані звичайно заносяться в спеціальні
журнали, log-файли, а не виводяться на консоль. В Java єсть засоби перепризначення потоку, наприклад,
з консолі у файл.
Поток in — це экземпляр класу inputstream. Він призначений на клавіатурне виедення з консолі методами
read(). Клас inputstream абстрактний, тому реально використовується якийсь із його підкласів.
Поняття потоку виявилось настільки зручним і полегшуючим програмування введення/виведення, що в
Java передбачена можливістьсть створення потоків, направляючих символи або байти не на зовнішнній
пристрій, а в масив або із масиву, обто звязуючих програму з областю оперативної памяті. Більше того,
можна створити потік, звязаний з рядком типу string, що знаходиться, знову-таки, в оперативній памяті.
280
281. Кріме того, можна створити канал (pipe) обміну інформацією між підпроцесами.
Ще один вид потоку — потік байтів, складаючих обєкт Java. Його можна направити у файл або передати
по мережі, потім відновити в оперативній памяті. Ця операція називається серіалізацією (serialization)
обєктів.
Методи організації потоків зібрані в класи пакета java.io. Крім класів, організуючих потік, в пакет java.io
входять класи з методами перетворення потоку, наприклад, можна перетворити потік байтів, утворюючих
цілі числа, в потік цих чисел. Ще одна можливість, представлена класами пакета java.io, — злити декілька
потоків в один потік.
Отже, в Java єсть цілих чотири ієрархії класів для створення, перетворення і злиття потоків. На чолі
ієрархії чотири класи, безпосередньо розширюючих клас object:
• Reader — абстрактний клас, в якому зібрані самі загальні методи символьного введення;
• Writer — абстрактний клас, в якому зібрані самі загальні методи символьного виведення;
• Inputstream — абстрактний клас з загальними методами байтового введення;
• Outputstream — абстрактний клас з загальними методами байтового виведення.
Класи вхідних потоків Reader і Inputstream визначають по три методи введення:
• read () — повертає один символ або байт, взятий із вхідного потоку, в вигляді цілого значення типу
int; якщо потік уже закінчився, повертає -1;
• read (char[] buf) — заповняє заздалегідь визначений масив buf символами із вхідного потоку; в
класі inputstream массив типу byte[] і заповняється він байтами; метод повертає фактичне число
взятих із потоку елементів або -1, якщо потік уже закінчився;
• read (char[] buf, int offset, int len) — заповнює частину символьного або байтового масиву buf,
починаючи з індекса offset, число взятих із потоку елементів рівне len; метод повертає фактичне
число взятих із потока елементів або -1.
Ці методи викидають IOException, якщо відбулася помилка введення/виведення. Четвертий метод skip
(long n) "промотує" потік з поточної позиції на n символів або байтів вперед. Ці елементи потоку не
вводяться методами read(). Метод повертає реальне число пропущених елементів, яке може відрізнятися
від n, наприклад потік може закінчиться. Поточний елемент потоку можна помітить методом mark (int n), а
потім повернутися до поміченого елементу методом reset(), але не більше ніж через n елементів. Не всі
підкласи реалізують ці методи, тому перед розстановкою поміток належить звернутися до логічного
методу marksupported(), який повертає true, якщо реалізовані методи розстановки і повернення до
поміток.
Класи вихідних потоків writer і outputstream визначають по три майже однакові методи введення:
• write (char[] buf) — виводить масив у вихідний потік, в класі Outputstream масив має тип byte[];
• write (char[] buf, int offset, int len) — виводить len елементів масиву buf, починаючи з злемента із
індексом offset;
• write (int elem) в класі Writer - виводить 16, а в класі Outputstream 8 молодших бітів аргумента elem
у вихідний потік.
В класі Writer єсть ще два методи:
• write (string s) — виводить рядок s у вихідний потік;
• write (String s, int offset, int len) — виводить len символів рядка s, починаючи із символа з номером
offset.
Багато підкласів класів Writer і Outputstream здійснюють буферизовае виведення. При цьому елементи
спочатку накопичуються в буфері, в оперативній памяті, і виводяться у вихідний потік тільки після того, як
буфер заповниться. Це зручно для вирівнювання швидкостей виведення із програми і виведення потоку,
але часто треба вивести інформацію в потік ще до заповнення буферу. Для цього передбачений метод
flush(). Даний метод зразу ж виводить весь вміст буферу в потік. Нарешті, по закінченню роботи з потоком
його необхідно закрити методом closed. Класи, що входять в ієрархії потоків введення/виведення,
281
282. показані на рис. 18.1 и 18.2.
Рис. 18.1. Ієрархія символьних потоків
Рис. 18.2. Класи байтових потоків
Всі класи пакета java.io можна розділити на дві групи: класи, що створюють потік (data sink), і класи, що
управляють потоком (data processing). Класи, що створюють потоки, в свою чергу, можна розділити на
пять груп:
• класи, що створюють потоки, звязані з файлами:
FileReader, FilelnputStream, FileWriterFile, Outputstream, RandomAccessFile
• класи, що створюють потоки, звязані з масивами:
CharArrayReader, ByteArraylnputStream, CharArrayWriter, ByteArrayOutputStream
• клас, що створюють канали обміну інформацією між підпроцесами:
PipedReader, PipedlnputStream, PipedWriter, PipedOutputStream
• класи, створюючі символьні потоки, звязані з рядком:
StringReader, StringWriter
• класи, створюючі байтові потоки із обєктів Java:
ObjectlnputStream, ObjectOutputStream
Зліва перечислені класи символьних потоків, справа — класи байтових потокив. Класи, управляючі
потоком, отримують в своїх конструкторах уже наявний потік і створюють новий, перетворений потік.
Можна уявляти їх собі як "перехідне кільце", після якого йде труба іншого діаметру. Чотири класи створені
спеціально для перетворення потоків:
FilterReader, FilterlnputStream, FilterWriter, FilterOutputStream
Самі по собі ці класи не мають ніякої користі — вони виконують тотожне перетворення. Їх належить
розширювати, перевизначаючи методи введення/виведення. Але для байтових фільтрів єсть корисні
розширення, яким відповідають деякі символьні класи. Перечислимо їх.
• Чотири класи виконують буферизоване введення/виведення: BufferedReader, BufferedlnputStream,
282
283. BufferedWriter, BufferedOutputStream
• Два класи перетворюють потік байтів, утворюючих вісім простих типів Java, в ці самі типи:
DatalnputStream, DataOutputStream
• Два класи містять методи, дозволяючі повернути декілька символів або байтів у вхідний потік:
PushbackReader, PushbacklnputStream
• Два класи звязані з виведенням на рядкові пристрої — екран дисплея, принтер: PrintWriter,
PrintStream
• Два класса звязують байтовий і символьний потоки:
InputstreamReader — перетворює вхідний байтовий потік у символьний потік;
Outputstreamwriter — перетворює вихідний символьний потік в байтовий потік.
• Клас streamTokenizer дозволяє розібрати вхідний символьний потік на окремі елементи (tokens)
подібно до того, як клас stringTokenizer, розглянутий нами в уроці 5, розбирав рядок.
• Із управляючих класів виділяється клас sequenceinputstream, зливаючий декілька потоків, заданих
в конструкторі, в один потік, і клас LineNumberReader, "уміючий" читати вихідний символьний потік
порядково. Рядки в потоці розділяються символами 'n' і/або 'г'.
Цей огляд класів введення/виведення трохи проясняє ситуацію, але не пояснює, як їх використовувати.
Перейдемо до розгляду реальних ситуацій.
18.2. Консольне введення/виведення
Для виведення на консоль ми завжди використовували метод printІn() класу Printstream, ніколи не
визначаючи екземпляри цього класу. Ми просто використовували статичне поле out класу System, яке
являється обєктом класу PrintStream. Виконуюча система Java звязує це поле з консоллю. Між іншим,
якщо вам набридло писати system.out.printІn(), то ви можете визначити нове посилання на System.out,
наприклад:
PrintStream pr = System.out;
і писати просто pr.printІn(). Консоль являється байтовим пристроєм, і символи Unicode перед виведенням
на консоль повинні бути перетворені в байти. Для символів Latin 1 з кодами 'u0000' — 'u00FF' при цьому
просто відкидається нульoвий старший байт і виводяться байти '0х00' —'0xFF'. Для кодів кирилиці, які
лежать в діапазоні 'u0400 —'u04FF кодування Unicode, і інших національних алфавітів відбувається
перетворення по кодовій таблиці, відповідній установленій на компютері локалі. Ми обговорювали це в
уроці 5.
Труднощі з відображенням кирилиці виникають, якщо виведення на консоль відбувається в кодуванні,
відмінному від локалі. Саме так відбувається в русифікованих версіях MS Windows NT/2000. Звичайно в
них установлюється локаль з кодовою сторінкою СР1251, а виведення на консоль відбувається в
кодуванні СР866. В цьому випадку треба замінити Printstream, який не може працювати з символьним
потоком, на Printwriter і "вставити перехідне кільце" між потоком символів Unicode і потоком байтів
System. out, що виводяться на консоль, у вигляді обєкта класу OutputstreamWriter. В конструкторі цього
обєкта належить указати потрібне кодування, в даному випадку, СР866. Все це можна зробити одним
оператором:
PrintWriter pw = new PrintWriter(new OutputstreamWriter(System.out, "Cp866"), true);
Класс Printstream буферизує вихідний потік. Другий аргумент true його конструктора викликає примусове
зкидання вмісту буфера у вихідний потік після кожного виконання методу printІn(). Але після print() буфер
не спорожнюється! Для зкидання буферу після кожного print() треба писать flush(), як це зроблено в
лістинзі 18.2.
Зауваження
Методи класу PrintWriter по замовчуванню не очищають буфер, а метод print() не очищає його в будь-
якому випадку. Для очистки буфера використовуй метод flush(). Після цього можна виводить будь-який
текст методами класса PrintWriter, які просто дублюють методи класу Printstream, і писати, наприклад,
pw.println("Це український текст"); як показано в лістинзі 18.1 і на рис. 18.3. Слід відмітити, що конструктор
класу PrintWriter, в якому заданий байтовий потік, завжди неявно створює обєкт класу OutputstreamWriter
283
284. з локальним кодуванням для перетворення байтового потоку в символьный потік.
Введення з консолі відбувається методами read() класу Inputstream за допомогою статичного поля in
класу System. З консолі йде потік байтів, отриманих із scan-кодів клавіатури. Ці байти повинні бути
перетворені в символи Unicode такими ж кодовими таблицями, як і при виведенні на консоль.
Перетворення йде по тій же схемі — для правильного введення кирилиці зручнішее всього визначити
екземпляр класу BufferedReader, використовуючи в якості "перехідного кільця" обєкт класу
InputstreamReader:
BufferedReader br = new BufferedReader( new InputstreamReader(System.an, "Cp866"));
Клас BufferedReader перевизначає три методи read() свого суперкласу Reader. Крім того, він містить
метод readLine(). Метод readLine() пoвертає рядок типу String, що містить символи вхідного потоку,
починаючи з поточного, і закінчуючи символом 'n' і/або 'r'. Ці символи-розділювачі не входять в
повернений рядок. Якшо у вхідному потоці немає символів, то повертається null. В лістинзі 18.1
приведена програма, ілюструюча перечислені методи консольного введення/виведення. На рис. 18.3
показано виведення цієї програми.
Лістинг 18.1. Консольне введення/виведення
import j ava.io.*;
class PrWr{
public static void main(String[] args){
try{
BufferedReader br = new BufferedReader(new InputstreamReader(System.in, "Cp866"));
PrintWriter pw = new PrintWriter(
new OutputstreamWriter(System.out, "Cp866"), true);
String s = "Це рядок з українським текстом";
System.out.println("System.out puts: " + s);
pw.println("PrintWriter puts: " + s) ;
int с = 0;
pw.println("Посимвольне введення:");
while((с = br.read()) != -1)
pw.println((char)c);
pw.println("Порядкове введення:");
do{
s = br.readLine();
pw.println(s);
}while(!s.equals("q"));
}catch(Exception e){
System.out.println(e);
}
}
}
Пояснимо рис. 18.3. Перший рядок виводиться потоком System.out. Як бачимо, кирилиця виводиться
неправильно. Наступний рядок попередньо перетворений в потік байтів, записаних в кодуванні СР866.
Потім, після тексту "Посимвольне введення:" з консолі вводяться символи "Україна" і натискується
клавіша <Enter>. Кожний введений символ відображається на екрані — операційна система працює в
режимі так званого "еха". Фактичне введення з консолі починається тільки після натискання клавіші
<Enter>, тому шо клавіатурне введення буферизується операційною системою. Символи зразу ж після
введення відображаються по одному в рядку. Зверніть увагу на два поржніх рядки після літери а. Це
виведені символы 'n' і 'r', які попали у вхідний потік при натисканні клавіші <Enter>. У них немає ніякого
графічного накреслення (glyph). Потім натиснута комбінація клавіш <Ctrl>+<Z>. Вона відображається на
консоль як "^Z" і означає закінчення клавіатурного введення, завершуючи цикл введення символів. Коди
цих клавіш уже не попадають у вхідний потік. Далі, після тексту "Порядкове введення:" з клавиатури
набирається рядок "Це рядок" і, вслід за натисканням клавіші <Enter>, заноситься в рядок s. Потім рядок s
виводиться знову на консоль. Для закінчення роботи набираємо q і натискуємо клавішу <Enter>.
284
285. Рис. 18.3. Консольне введення/виведення
18.3. Файлове введення/виведення
Оскільки файли в більшості сучасних операційних систем розуміються як послідовність байтів, для
файлового введення/виведення створюються байтові потоки за допомогою класів FiІeІnputstream і
FileOutputstream. Це особливо зручно для бінарних файлів, що зберігають байт-кодb, архіви, зображення,
звук. Але дуже багато файлів містять тексти, складені із символів. Незважаючи на те, що символи можуть
зберігатися в кодуванні Unicode, ці тексти частіше всього записанів байтових кодуваннях. Тому і для
текстових файлів можна використовувати байтові потоки. В такому випадку з боку програми прийдеться
організовувать перетворення байтів у символи і навпаки.
Щоб полегшити це перетворення, в пакет java.io введені класи FiІeReader в FileWriter. Вони організовують
перетворення потоку: із сторони програми потоки символьні, із сторони файла — байтові. Це
відбувається тому, що дані класи розсширюють класи InputStreamReader і OutputstreamWriter, відповідно,
значить, містять "перехідне кільце" всередині себе. Незважаючи на відмінність потоків, використання
класів файлового введення/виведення дуже схоже. В конструкторах всіх чотирьох файлових потоків
задається імя файла у вигляді рядка типу string або посилка на обєкт класу File. Конструктори не тільки
створюють обєкт, але і відшукують файл і відкривають його. Наприклад:
Fileinputstream fis = new FilelnputStreamC'PrWr.Java");
FileReader fr = new FileReader("D:jdkl.3srcPrWr.Java");
При невдачі викидається виключення класу FileNotFoundException, але конструктор класу FileWriter
викидає більш загальне виключення IOException. Після відкриттия вихідного потоку типу FileWriter або
FileQutputStream вміст файлу, якщо він не був порожнім, зтирається. Для того щоб можна було робити
запис в кінець файла, і в тому і в іншому класі передбачений конструктор з двома аргументами. Якщо
другий аргумент рівний true, то відбувається дозапис в кінець файлу, якщо false, то файл заповнюється
новою інформацією. Наприклад:
FileWriter fw = new FileWriter("ch!8.txt", true);
FiieOutputstream fos = new FileOutputstream("D:samplesnewfile.txt");
Увага
285
286. Вміст файлу, відкритого для запису конструктором з одним аргументом, стирається. Зразу після
виконання конструктора можна читати файл:
fis.read(); fr.read(); або записувати в нього: fos.write((char)с); fw.write((char)с);
По закінченню роботи з файлом потік належить закрити методом close(). Перетворення потоків у класах
FileReader і FileWriter виконується по кодових таблицях установленої на компютері локалі. Для
правильного введення кирилиці треба застосувати FileReader, a нe FileInputStream. Якщо файл містить
текст в кодуванні, відмінний від локального кодування, то прийдеться вставляти "перехідне кільце"
вручну, як це робилось для консолі, наприклад:
InputStreamReader isr = new InputStreamReader(fis, "KOI8_R"));
Байтовий потік fis визначений вище.
18.4. Отримання властивостей файла
В конструкторах класів файлового введення/виведення, описаних в попередньому розділі, указувалось
імя файла у вигляді рядка. При цьому залишалось невідомим, чи існує файл, чи дозволений до нього
доступ, яка довжина файла. Отриати такі дані можна від попередньо створеного екземпляру класу File,
що містить дані про файл. В конструкторі цього класу
File(String filename)
указується шлях до файлу або каталогу, записаний по правилах операційної системи. В UNIX імена
каталогів розділяються похилою рискою /, в MS Windows — зворотною похилою рискою , в Apple
Macintosh — двокрапкою :. Цей символ міститься в системній властивості file.separator (див. рис. 6.2).
Шляху до файлу передує префікс. В UNIX це похила риска, в MS Windows — літера розділу диска,
двокрапка і зворотна похила риска. Якщо префікса немає, то шлях вважається відносним і до нього
додається шлях до поточного каталогу, який зберігається в системній властивості user.dir. Конструктор не
перевіряє, чи існує файл з таким іменем, тому післе створення обєкта слід це перевірити логічним
методом exists().
Класс File містить близько сорока методів, що дозволяють узнати різні властивості файла або каталога.
Перш за все, логічними методами isFile(), isDirectory() можна вияснить, чи являється шлях, указаний в
конструкторі, шляхом до файла або каталога. Для каталога можна отримати його зміст — список імен
файлів і підкаталогів— методом list(), повертаючим масив рядків string[]. Можна отримати такий же список
у вигляді масиву обєктів класу File[] методом listFiles(). Можна выбрати із списку тільки деякі файли,
реалізувавши інтерфейс FileNameFiiter і звернувшись до методу
list(FileNameFilter filter).
Якщо каталог з указаним в конструкторі шляхом не існує, його можна створити логічним методом mkdir().
Цей метод повертає true, якщои каталог удалось створити. Логічний метод mkdir() створює ще і всі
неіснуючі каталоги, указані в шляху. Порожній каталог видаляється методом delete(). Для файла можна
отримати його довжину в байтах методом length(), час останньої модифікації в секундах з 1 січня 1970 р.
методом lastModified(). Якщо файл не існує, ці методи повертають нуль. Логічні методи canRead(),
canWrite() показують права доступу до файла. Файл можна переіменувати логічним методом
renameTo(File newMame) або видалити логічним методом delete(). Ці методи повертають true, якщо
операція пройшла успішно. Якщо файл з указаним в конструкторі шляхом не існує, його можно створити
логічним методом createNewFile(), що повертає true, якщо файл не існував, і його удалось створити, і
false, якщо файл уже існував.
Статичними методами
createTempFile(String prefix, String suffix, File tmpDir)
createTempFile(String prefix, String suffix)
286
287. можна створити тимчасовий файл з іменем prefix і розширенням suffix в каталозі tmpDir або каталозі,
указаному в системній властивості java.io.tmpdir (см. рис. 6.2). Імя prefix повинно містити не менше трьох
символів. Якщо suffix = null, то файл одержить суфікс .tmp. Перечислені методи повертають посилку типу
File на створений файл. Якщо звернутися до методу deІeteOnExit(), то по завершенні роботи JVM
тимчасовий файл буде знищений.
Декілька методів getxxxo повертають імя файла, імя каталога і інші дані про шлях до файлу. Ці методи
корисні в тих випадках, коли посилка на обєкт класу File повертається іншими методами і потрібні дані
про файл. Нарешті, метод toURL() повертає шлях до файлу у формі URL. В лістинзі 18.2 показано
приклад використання класу File, а на рис. 18.4 — початок виведення цієї програми.
Лістинг 18.2. Визначення властивостей файла і каталога
import java.io.*;
class FileTest{
public static void main(String[] args) throws IOException{
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(System.out, "Cp866"), true);
File f = new File("FileTest.Java");
pw.println();
pw.println("Файл "" + f.getName() + "" " +
(f.exists()?"":"не ") + "существует");
pw.println("Вы " + (f.canRead()?"":"не ") + "можете читать файл");
pw.println("Вы " + (f.canWrite()?"":"нe ") +
"можете записывать в файл");
pw.println("Длина файла " + f.length() + " б");
pw.println() ;
File d = new File("D:jdkl.3MyProgs");
pw.println("Содержимое каталога:");
if (d.exists() && d.isDirectory()) {
String[] s = d.list();
for (int i = 0; i < s.length; i++)
pw.println(s[i]);
}
}
}
Рис. 18.4. Властивості файла і початок виведення каталога
18.5. Буферизоване введення/виведення
Операції введення/виведення в порівнянні з операціями в оперативній памяті виконуються дуже повільно.
Для компенсації в оперативній памяті виділяється деяка проміжна область — буфер, в який поступово
287
288. накопичується інформація. Коли буфер заповнений, його вміст швидко переноситься процесором, буфер
очищається і знову заповнюється інформацією. Життєвий приклад буфера — поштова скринька, в якій
накопичуються листи. Ми кидаємо в нього листа і йдемо в своїх справах, не очікуючи приїзду поштової
машини. Поштова машина періодично очищає поштову скриньку, переносячи зразу велику кількість
листів. Уявіть собі місто, в якому немає поштових скриньок, і гатовп людей з листами в руках очікує
приїзду поштової машини.
Класи файлового введення/виведення не займаються буферизацією. Для цієї мети єсть чотири спеціальні
класи BufferedXxx, перечислені вище. Вони приєднуються до потоків введення/виведення як "перехідне
кільце", наприклад:
BufferedReader br = new BufferedReader(isr);
BufferedWriter bw = new BufferedWriter(fw);
Потоки isr і fw визначені вище. Програма лістинга 18.3 читає текстовый файл, написаний в кодуванні
СР866, і записує його вміст у файл в кодуванні KOI8_R. При читанні і записі застосовується буферизація.
Імя вихідного файла задається в командному рядку параметром args[0], імя копії — параметром args[].
Лістинг 18.3. Буферизоване файлове введення/виведення
import java.io.*;
class DOStoUNIX{
public static void main(String[] args) throws IOException{
if (args.length != 2){
System.err.println("Usage: DOStoUNIX Cp866file KOI8_Rfile");
System.exit(0);
}
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(args[0]), "Cp866"));
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStreamtargs[1]), "KOI8_R"));
int с = 0;
while ((c = br.readO) != -1)
bw.write((char)c);
br.closeO; bw.close();
System.out.println("The job's finished.");
}
}
18.6. Потік простих типів Java
Клас DataOutputstream дозволяє записати дані простих типів Java у вихідний потік байтів методами
writeBoolean (boolean b), writeBytefint (b), writeShort(int h), writeChar(int c), writeln(int n), writeLong(long І),
writeFloat(float f), writeDouble(double d). Крім того, метод writeBytes(String s) записує кожний символ рядка
s в один байт, відкидаючи старший байт кодування кожного символу Unicode, а метод writeСhar(String s)
записує кожний символ рядка s в два байти, перший байт — старший байт кодування Unicode, так же, як
це робить метод writeChar().
Ще один метод writeUTF(Sring s) записує рядок s у вихідний потік в кодуванні UTF-8. Треба пояснити це
кодування.
18.7. Кодування UTF-8
Запис потоку в байтовму кодуванні викликає труднощі з використанням національних символів, запис
потоку в Unicode збільшує довжину потоку в два рази. Кодування UTF-8 (Universal Transfer Format)
являється компромісом. Символ в цьому кодуванні записується одним, двома або трьома байтами.
Символи Unicode із діапазону 'u0000' —'u007F', в якому лежить англійський алфавіт, записуються одним
288
289. байтом, старший байт просто відкидається. Символи Unicode із діапазону 'u0080' —'u07FF', в якому
лежать найбільш розповсюджені символи національних алфавітів, записуються двома байтами
наступним чином: символ Unicode з кодуванням 00000хххххуууууу записується як 110ххххх10уууууу.
Решта символів Unicode із діапазону 'u0800' —'UFFFF' записуються трьома байтами по наступному
правилу: символ Unicode з кодуванням xxxxyyyyyyzzzzzz записується як 1110xxxx10yyyyyy10zzzzzz. Такий
дивний спосіб розподілу бітів дозволяеє по першим бітам коду узнати, скільки байтів складає код
символа, і правильно відрахувати символи в потоці.
Так ось, метод writeUTF(String s) спочатку записує в потік в перші два байти потоку довжину рядка s в
кодуванні UTF-8, а потім символи рядка в цьому кодуванні. Читати цей запис потім треба парним методом
readUTF() класу DatalnputStream. Клас DatalnputStream перетворює вхідний потік байтів типу InputStream,
що містить дані простих типів Java, в дані того ж типу. Такий потік, як правило, створюється методами
класу DataOutputstream. Дані із цього потоку можна прочитати методами readBoolean(), readByte(),
readShort(), readChar(), readlnt(), readLong(), readFloat(), readDouble(), повертаючими дані відповідного
типу. Крім того, методи readUnsignedByte() і readUnsignedShort () повертають ціле типу int, в якому старші
три или два байти нульові, а молодші один або два байти заповнені байтами із вхідного потоку.
Метод readUTF(), двоїстий методу writeUTF(), повертає рядок типу string, отриманий із потоку, записаного
методом writeUTF(). Ще один, статичний, метод readUTF(Datainput in) робить те ж саме із вхідним потоком
in, записаним в кодуванні UTF-8. Цей метод можна застосовувати, не створюючи обєкт класу
DatalnputStream. Програма в лістинзі 18.4 записує у файл fib.txt числа Фібоначчі, а потім читає цей файл і
виводить його зміст на консоль. Для контролю записувані у файл числа теж виводяться на консоль. На
рис. 18.5 показано виведення цієї програми.
Лiстинг 18.4. Введення/виведення даних
import j ava.io.*;
class DataPrWr{
public static void main(String[] args) throws IOException{
DataOutputstream dos = new DataOutputstream (
new FileOutputStream("fib.txt"));
int a = 1, b = 1, с = 1;
for (int k = 0; k < 40; k++){
System.out.print(b + " ");
dos.writelnt(b);
a = b; b = с; с = a + b;
}
dos.closet);
System.out.println("n");
DatalnputStream dis = new DatalnputStream (
new FilelnputStream("fib.txt")) ;
while(true)
try{
a = dis.readlnt();
System.out.print(a + " ">;
}catch(lOException e){
dis.close();
System.out.println("End of file");
System.exit (0);
}
}
}
Зверніть увагу на те, що спроба читання за кінцем файла викидає виключення класу IOException, його
обробка заключається в закритті файла і закінченні програми.
18.8. Прямий доступ до файла
Якщо необхідно інтенсивно працювати з файлом, записуючи в нього дані різних типів Java, змінюючи їх,
289
290. відшукючи і читаючи потрібні інформацію, то краще всього скористатися методами класу
RandomAccessFile. В конструкторах цього классу
RandomAccessFile(File file, String mode)
RandomAccessFile(String fileName, String mode)
другим аргументом mode задається режим відкриття файла. Це може бути рядок "r" — відкриття файла
тільки для читання, абои "rw" — відкриття файла для читання і запису. Цей клас зібрав всі корисні методи
роботи з файлом. Він містить всі методи класів DataІnputstream і DataOutputstream, крім того, дозволяє
прочитати зразу цілий рядок методом readln() і відшукати потрібні дані у файлі. Байти файла
нумеруються, починаючи з 0, подібно елементам масиву. Файл має неявний показчик (file pointer)
поточної позиції. Читання і запис відбувається, починаючи з поточної позиції файла. При відкритті файла
конструктором показчик стоїтьт на початку файла, в позиції 0. Поточну позицію можна узнати методом
getFiiePointer(). Кожне читання або запис переміщає показчик на довжину прочитаного або записаного
даного. Завжди можна перемістити показчик в нову позицію pos методом seek (long pos). Метод seek(0)
переміщає показчик на початок файла. В класі немає методів перетворення символів в байти і назад по
кодовим таблицям, тому він не пристосований для роботи з кирилицею.
Рис. 18.5. Введення/виведення даних
18.9. Канали обміну інформацією
В попередньому уроці ми бачили, яких зусиль коштує організувати правильний обмін інформацією між
підпроцесами. В пакеті java.io єсть чотири класи pipedxxx, полегшуючі це завдання. В одному підпроцесі
— джерелі інформації — створюється обєкт класу PipedWriter або PipedOutputstream, в який записується
інформація методами write() цих класів. В другому подпроцесі — приймачу інформації — формується
обєкт класу PipedReader або PipedІnputstream. Він звязується з обєктом-джерелом за допомогою
конструктора або спеціальним методом connect(), і читає інформацію методами read(). Джерело і приймач
можна створити і звязати в зворотному порядку.
Так створюється однонаправлений канал (pipe) інформації. На самім ділі це деяка область оперативної
памяті, до якої організований сумісний доступ двох або більше підпроцесів. Доступ синхронізується,
записуючі процеси не можуть завадити читанню. Якщо треба організувати двосторонній обмін
інформацією, то створюються два канали. В лістинзі 18.5 метод run() класу Source генерує інформацію,
для простоти просто цілі числа k, і передає їх в канал методом pw. write (k). Метод run() класу Target
читає інформацію із канала методом pr.read(). Кінці каналу звязуються за допомогою конструктора класу
Target. На рис. 18.6 видно послідовність запису і читання інформації.
Лістинг 18.5. Канал обміну інформацією
import java.io.*;
class Target extends Thread{
private PipedReader pr;
Target(PipedWriter pw){
try{
pr = new PipedReader(pw);
}catch(lOException e){
290
291. System.err.println("From Target(): " + e);
}
}
PipedReader getStream(){ return pr;}
public void run(){
while(true)
try{
System.out.println("Reading: " + pr.read());
}catch(IOException e){
System.out.println("The job's finished.");
System.exit(0);
}
}
}
class Source extends Thread{
private PipedWriter pw;
Source (){
pw = new PipedWriter();
}
PipedWriter getStream(){ return pw;}
public void run(){
for (int k = 0; k < 10; k++)
try{
pw.write(k);
System.out.println("Writing: " + k);
}catch(Exception e){
System.err.printlnf"From Source.run(): " + e) ;
}
}
}
class PipedPrWr{
public static void main(String[] args){
Source s = new Source();
Target t = new Target(s.getStream());
s.start();
t.start();
}
Рис. 18.6. Дані, передавані між підпроцесами
18.10. Серіалізація обєктів
291
292. Методи класів ObjectlnputStream і ObjectOutputStream дозволяють прочитати із вхідного байтового потоку
або записати у вихідний байтовий потік дані складних типів — обєкти, масиви, рядки — подібно тому, як
методи класів Datainputstream і DataOutputstream читають і записують дані простих типів. Схожість
підсилюється тим, що класи Objectxxx містить методи як для читання, так і запису простих типів. Між
іншим, ці методи призначені не для використання в програмах, а для запису/читання полів обєктів і
елементів масивів.
Процес запису обєкта у вихідний потік отримав назву серіалізації (serialization), а читання обєкта із
вхідного потоку і відновлення його в оперативній памяті — десеріалізації (deserialization). Серіалізація
обєкта порушує його безпеку, оскільки зловредний процес може серіалізувати обєкт в масив, переписати
деякі элементи масиву, представляючі private-поля обєкта, забезпечивши собі, наприклад, доступ до
секретного файлу, а потім десеріалізувати обєкт із зміненими полями і здійснювати з ним недопустимі дії.
Тому серіалізувати можна не кожний обєкт, а тільки той, котрий реалізує інтерфейс seriaІizabІe. Цей
інтерфейс не містить ні полів, ні методів. Реалізувати в ньому нема чого. По суті справи запис
class A implements SeriaiizabІe{...}
це тільки помітка, дозволяюча серіалізацію класу А. Як завжди в Java, процес серіалізації максимально
автоматизований. Досить створити обєкт класу ObjectOutputStream, звязавши його з вихідним потоком, і
виводить в цей потік обєкти методом writeObject():
MyClass me = new MyClass("abc", -12, 5.67e-5);
int[] arr = {10, 20, 30};
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("myobjects.ser")) ;
oos.writeObject(me);
oos.writeObject(arr);
oos.writeObject("Some string");
oos.writeObject (new Date());
oos.flush();
У вихідний потік виводяться всі нестатичні поля обєкта, незалежно від прав доступу до них, а також дані
про клас цього обєкта, необхідні для його правильного відновлення при десеріалізації. Байт-коди методів
класу не серіалізуються. Якщо в обєкті присутні посилки на інші обєкти, то вони теж серіалізуються, а в
них можуть бути посилки на інші обєкти, котрі знову-таки серіалізуються, и отримується ціла множина
чудернацьки звязаних між собою серіалізованих обєктів. Метод writeObject() роспізнає дві посилки на
один обєкт і виводить його у вихідний потік тільки один раз. До того ж, він роспізнає посилки, замкнуті в
кільце, і уникає зациклювання.
Всі класи обєктів, що входять в таку серіалізовану множину, а також всі їх внутрішні класи, повинні
реалізувати інтерфейс seriaІizabІe, в противному випадку буде викинуте виключення класу
NotseriaІizabІeException і процес серіалізації перерветься. Багато класів J2SDK реалізують цей інтерфейс.
Врахуйте також, що всі потомки таких класів наслідують реалізацію. Наприклад, клас java.awt.Component
реалізує інтерфейс Serializable, значить, всі графічні компоненти можна серіалізувати. Не реалізують цей
інтерфейс звичайно класи, тісно звязані з виконанням програм, наприклад, java.awt.Toolkit. Стан
екземплярів таких класів немає рації зберігати або передавати по мережі. Не реалізують інтерфейс
Serializable і класи, що містять внутрішні дані Java "для службового користування". Десеріалізація
відбувається так же просто, як і серіалізація:
ObjectlnputStream ois = new ObjectInputStream(
new FilelnputStream("myobjects.ser"));
MyClass mcl = (MyClass)ois.readObject();
int[] a = (int[])ois.readObject();
String s = (String)ois.readObject();
Date d = (Date)ois.readObject() ;
Треба тільки додержуватися порядок читання елементів потоку. В лістинзі 18.6 ми створюємо обєкт класу
GregorianCaІendar з поточною датою і часом, серіалізуємо його у файл date.ser, через три секунди
292
293. десеріалізуємо і зрівнюємо з поточним часом. Результат показано на рис. 18.7.
Лiстинг 18.6. Серіалізація обєкта
import java.io.*;
import java.util.*;
class SerDatef
public static void main(String[] args) throws Exception{
GregorianCaiendar d - new GregorianCaiendar();
QbjectOutputStream oos = new ObjectOutputStream{
new FileOutputStream("date.ser"));
oos.writeObject(d);
oos.flush();
oos.close();
Thread.sleep(3000);
ObjectlnputStream ois = new ObjectlnputStream(
new FileInputStream("date.ser"));
GregorianCaiendar oldDate = (GregorianCaiendar)ois.readObject();
ois.close();
GregorianCaiendar newDate = new GregorianCaiendar();
System.out.println("Old time = " +
oldDate.get(Calendar.HOUR) + ":" +
oldDate.get(Calendar.MINUTE) +":" +
oldDate.get(Calendar.SECOND) +"nNew time = " +
newDate.get(Calendar.HOUR) +":" +
newDate.get(Calendar.MINUTE) +":" +
newDate.get(Calendar.SECOND));
}
}
Рис.18.7. Серіалізація обєкта
Якщо не потрібно серіалізувати якесь поле, то достатньо помітить його службовим словом transient,
наприклад:
transient MyClass me = new MyClass("abc", -12, 5.67e-5);
Метод writeObject() не записує у вихідний потік поля, помічені static і transient. Взагалі ж, цю ситуацію
можно змінити, перевизначивши метод writeObject() або задавши список серіалізованих полів. Процес
серіалізації навіть можна повністю налагодити під свої потреби, перевизначивши методи
введення/виведення і скористувавшись допоміжними класами. Можна також взяти весь процес на себе,
реалізувавши не інтерфейс Serializable, а інтерфейс ExternaІizabІe, але тоді прийдеться реалізувать
методи readExternaІ() і writeExternaІ() , виконуючі введення/виведення.
Ми не будемо займатися тут цими питаннями. Якщо вам необхідно повністю освоїти процес серіалізації,
то зверніться до специфікації Java Object Serialization Specification, розташований серед документації
293
294. J2SDK в каталозі docsguideserializationspec. Там же єсть і приклади програм, реалізуючих цю
специфікацію.
18.11. Друк в Java
Оскільки принтер — пристрій графічний, виведення на друк дуже схоже на виведення графічених обєктів
на екран. Тому в Java засоби друку входять в графічну бібліотеку AWT і в систему Java 2D. В графічному
компоненті крім графічного контекста — обєкта класу Graphics, створюється ще "друкарський контекст".
Це теж обєкт класу Graphics, але реалізуючий інтерфейс printGraphics і отриманий із іншого джерела —
обєкта класу printjob, що входить в пакет java.awt. Сам же цей обєкт створюється за допомогою класу
Toolkit пакета java.awt. На практиці це виглядає так:
PrintJob pj = getToolkitO .get,Print Job (this, "Job Title", null);
Graphics pg = pj.getGraphics();
Метод getPrintJob() спочатку виводить на екран стандартне вікно Print операційної системи. Коли
користувач вибере в цьому вікні параметри друку і почне друк кнопкою ОК, створюється обєкт pj. Якщо
користувач відмовляється від друку кнопкою Cancel, то метод повертає null. В класі Toolkit два методи
getPrint Job ():
• getPrintJob(Frame frame, String jobTitle, JobAttributes jobAttr, PageAttributes pageAttr)
• getPrintJob(Frame frame, String jobTitle, Properties prop)
Коротко охарактеризуємо аргументи методів.
• Аргумент frame указує на вікно верхнього рівня, управляюче друком. Цей аргумент не може бути
null. Рядок jobTitle задає заголовок завдання, який не друкується, і може бути рівним null.
• Аргумент prop залежить від реалізації системи друку, часто це просто null, в даному випадку
задаються стандартні параметри друку.
• Аргумент jobAttr задає параметри друку. Клас JobAttributes, екземпляром якого являється цей
аргумент, має складну будоіу. В ньому пять підкласів, які містять статичні константи — параметри
друку, котрі використовуються в конструкторі класу. Єсть також конструктор по замовчуванню,
задаючий стандартні параметри друку.
• Аргумент pageAttr задає параметри сторінки. Клас pageProperties теж містить пять підкласів із
статичними константами, котрі і задають параметри сторінки і використовуються в конструкторі
класу. Якщо для друку досить стандартних параметрів, то можна скористатися конструктором по
замовчуванню.
Після того як "друкарський контекст" — обєкт pg класу Graphics — визначений, можна викликати метод
print(pg) або printAll(pg) класу Component. Цей метод установлює звязок з принтером по замовчуванню і
викликає метод paint(pg). На друк виводиться все те, що задано цим методом. Наприклад, щоб
роздрукувати текстовий файл, треба в процесі введення розбити його текст на рядки і в методі paint(pg)
вивести рядки методом pg.drawstring() так же, як ми виводили їх на екран в уроці 9. При цьому слід
врахувати, що в "друкарському контексті" немає шрифту по замовчуванню, завжди треба установлювати
шрифт методом pg.setFont().
Після виконання всіх методів print() застосовується метод pg.dispose(), викликаючиий прогін сторінки, і
метод pj.end(), закінчуючий друк.
В лістинзі 18.7 приведено простий приклад друку тексту і кола, заданих в методі paint(). Цейт метод
працює два рази: перший раз креслячи текст і коло на екрані, другий раз, точно так же, на листі паперу,
вставленому в принтер. Всі методи друку зібрані в один метод simplePrint().
Лістинг 18.7. Друк засобами AWT
import java.awt.*;
import j ava.awt.event.*;
class PrintTest extends Frame{
PrintTest(String s){
294
295. super(s);
setSize(400, 400);
setVisible(true);
}
public void simplePrint{){
PrintJob pj =
getToolkitO.getPrintJob(this, "JobTitle", null);
if (pj != null){
Graphics pg = pj.getGraphics();
if (pg != null){
print(pg);
pg.dispose();
}else System.err.println("Graphics's null");
pj.end();
}else System.err.println("Job's null");
}
public void paint(Graphics g){
g.setFonttnew Font("Serif", Font.ITALIC, 30));
g.setColor(Color.black);
g.drawArcdOO, 100, 200, 200, 0, 360);
g.drawstring("Страница 1", 100, 100);
}
public static void main(String[] args){
PrintTest pt = new PrintTest(" Простий приклад друку");
pt.simplePrint();
pt.addWindowListener(new WindowAdаpter(){
public void windowClosing(WindowEvent ev){
System.exit(0);
}
});
}
}
18.12. Друк засобами Java 2D
Розсширена графічна система Java 2D пропонує нові інтерфейси і класи для друку, зібрані в пакет
java.awt.print. Ці класи повністю перекривають всі стандартні можливості друку бібліотеки AWT. Більше
того, вони зручніші в роботі і пропонують додаткові можливості. Якщо ви маєте цей пакет, то,
безсумнівно, треба використовувати його, а не стандартні засоби друку AWT. Як і стандартні засоби AWT,
методи класів Java 2D виводять на друк вміст графічного контексту, заповненого методами класу
Graphics або класу Graphics2D.
Всякий клас Java 2D, що збирається друкувати хоча б одну сторінку тексту, графіки або зображення
називається класом, рисуючим сторінки (page painter). Такий клас повинен реалізувати інтерфейс
Printable. В цьому інтерфейс описані дві константи і тільки один метод print(). Клас, рисуючий сторінки,
повинен реалізовувати цей метод. Метод print() повертає ціле типу int і має три аргументи:
print(Graphics g, PageFormat pf, int ind);
Перший аргумент g — це графічний контекст, що виводиться на лист паперу, другий аргумент pf —
екземпляр класу PageFormat, визначаючий розмір і орієнтацію сторінки, третій аргумент ind —
порядковий номер сторінки, що відраховується з нуля. Метод print () класу, рисуючого сторінки, заміняє
собою метод paint(), що використовується стандартними засобами друку AWT. Клас, рисуючий сторінки,
не зобовязаний розширяти клас Frame і перевизначити метод paint(). Все заповнення графічного
контексту методами класу Graphics або Graphics2D тепер виконується в методі print(). Коли друк сторінки
буде закінчено, метод print() повинен повернути ціле значення, задане константою PAGE_EXISTS. Буде
зроблено повторне звернення до методу print() для друку наступної сторінки. Аргумент ind при цьому
зростає на 1. Коли ind перевищить кількість сторінок, метод print() повинен повернути значення
NO_SUCH_PAGE, що служить сигналом закінчення друку.
295
296. Слід памятати, що система друку може декілька раз звернутися до методу paint() для друку однієї і тієї ж
сторінки. При цьому аргумент ind не змінюється, а метод print() повинен створити той же графічний
контекст.
Клас PageFormat визначає параметри сторінки. На сторінці вводиться система координат з одиницею
довжини 1/72 дюйма, початок якої і напрям вісей визначається однією із трьох констант:
• PORTRAIT — початок координат розташовано в лiвому верхньому куті сторінки, вісь Ох
направлена вправо, вісь Оу — вниз;
• LANDSCAPE — початок координат в лівому нижньому куті, вісь Ох йде вверх, вісь Оу — вправо;
• REVERSE_LANDSCAPE — початок координат в правому верхньому куті, вісь Ох йде вниз, вісь Оу
— вліво.
Більшість принтерів не може друкувати без полів, на всій сторінці, а здійснює виведення тільки в деяку
область друку (imageable area), координати лівого верхнього кута якої повертаються методами
getІmageabІeХ() і getlmageableY(), а ширина і висота — методами getlmageableWidth() і
getlmageableHeight(). Ці значення треба враховувати при розташуванні елементів в графічному контексті,
наприклад, при розміщенні рядківк тексту методом drawstring(), як це зроблено в лістинзі 18.9. В класі
тільки один конструктор по замовчуванню PageFormat(), задаючий стандартні параметри сторінки,
визначені для принтера по замовчуванню обчислювальною системою.
Виникає питання: "Як же тоді задати параметри сторінки?" Відповідь проста: "За допомогою стандартного
вікна операційної системи". Метод pageDiaІog(PageDiaiog pd) відкриває на екрані стандартне вікно
Параметри сторінки (Page Setup) операційної системи, в якому уже задані параметри, визначені в обєкті
pd. Якщо користувач вибрав у цьому вікні кнопку Відміна, то повертається посилання на обєкт pd, якщо
кнопку ОК, то створюється і повертається посилка на новий обєкт. Обєкт pd в будь-якому випадку не
змінюється. Він звичайно створюється конструктором. Можна задати параметри сторінки і із програми,
але тоді слід спочатку визначити обєкт класуа Paper конструктором по замовчуванню:
Paper р = new Paper()
Потім методами
p.setSize(double width, double height)
p.setlmageableArea(double x, double y, double width, double height)
задати розмір сторінки і області друку. Потім визначити обєкт класу pageFormat з параметрами по
замовчуванню:
PageFormat pf = new PageFormat()
і задати нові параметри методом
pf.setPaper(p)
Тепер викликати на екран вікно Параметри сторінки методом pageDiaІog() уже не обовязково, і ми
отримаємо мовчазний (silent) процес друку. Так робиться в тих випадках, коли друк виконується на фоні
окремим підпроцесом. Отже, параметри сторінки визначені, метод print() — теж. Теперь треба дати
завдання на друк (print job) — указати кількість сторінок, їх номери, порядок друку сторінок, кількість копій.
Всі ці дані збираються в класі PrinterJob.
Система друку Java 2D розрізняє два види звадань. В більш простих зваданнях — Printable Job — єсть
тільки один клас, рисуючий сторінки, тому у всіх сторінок одні и ті ж параметри, сторінки друкуються
послідовно з першої по останню або з останньої сторінки по першу, це залежить від системы друку.
Другий, більш складний вид завдань — Pageable Job — визначає для друку кожної сторінки свій клас,
рисуючий сторінки, тому у кожної сторінки можуть бути власні параметри. Крім того, можна друкувати не
все, а тільки вибрані сторінки, виводити їх в зворотному порядку, друкувати на обох сторонах листа. Для
здійснення цих можливостей визначається екземпляр класу Book або створюється клас, реалізуючий
296
297. інтерфейс Pageable. В класі Book, знову-таки, один конструктор, створюючий порожній обєкт:
Book b = new Book()
Після створення в даний обєкт додаються класи, рисуючі сторінки. Для цього в класі Book єсть два
методи:
• append (Printable p, PageFormat pf) —додає обєкт р в кінець;
• append(Printable p, PageFormat pf, int numPages) — додає numPages екземплярів р в кінець; якщо
число сторінок невідомо, то задається константа UNKNOWN_NUMBER_OF_PAGES.
При складанні завдання на друк, тобто після створення екземпляра класу PrinterJob, треба указати вид
завдання одним і тілько одним із трьох методів цього класу setPrintable(Printable pr), setPrintable(Printable
pr, PageFormat pf) або setPageble (Pageable pg). Заодно задаються один або декілька класів рг, рисуючих
сторінки в цьому завданні. Решта параметрів завдання можна задати в стандартному діалоговому вікні
Print операційної системи, яке відкривається на екрані при виконанні логічного методу printDialog().
Указаний метод не має аргументів. Він поверне true, коли користувач клікне по кнопці ОК, і false після
натискання кнопки Відміна.
Залишається задати число копій, якщо воно більше 1, методом setСopies(int n) і завдання сформовано.
Ще один корисний метод defaultPage() класу PrinterJob повертає обєкт класу PageFormat по
замовчуванню. Цей метод можна використовувати замість конструктора класу PageFormat.
Залишилось сказати, як створюється екземпляр класу PrinterJob. Оскільки цей клас тісно звязаний з
системою друку компютера, його обєкти створюються не конструктором, а статичним методом
getPrinterJob(), що знаходиться в тому ж самому класі Printer Job. Початок друку задається методом print()
класу PrinterJob. Цей метод не має аргументів. Він послідовно викликає методи print(g, pf, ind) класів,
рисуючих сторінки, для кажної сторінки.
Зберемо все це вмісті в лістинзі 18.8. В ньому засобами Java 2D друкується те ж, що і в лістинзі 18.7.
Зверніть увагу на п. 6. Після закінчення друку програма не закінчується автоматичнои, для її завершення
ми звертаємося до методу System.exit (0).
Лістинг 18.8. Простий друк методами Java 2D
import java.awt.*;
import java.awt.geom.*;
import java.awt.print.*;
class Print2Test implements Printable{
public int print(Graphics g, PageFormat pf, int ind)
throws PrinterException{ // Друкуємо не більше 5 сторінок
if (ind > 4) return Printable.NO_SUCH_PAGE;
Graphics2D g2 = (Graphics2D)g;
g2.setFont(new Font("Serif", Font.ITALIC, 30));
g2.setColor (Color.black);
g2.drawstring("Page " + (ind + I), 100, 100);
g2.draw(new Ellipse2D.Double(100, 100, 200, 200));
return Printable.PAGE_EXISTS;
}
public static void main(String[] args){
// 1. Створюємо екземпляр завдання
PrinterJob pj = Printer Job.getPrinter Job();
// 2, Відкриваємо діалогове вікно Параметри сторінки
PageFormat pf = pj.pageDialog (pj.defaultPaige() );
// 3. Задаємо вид завдання, обєкт класу, рисуючого сторінку і вибрані параметри
сторінки
pj.setPrintable(new Print2Test(), pf};
// 4. Якщо потрібно надрукувати декілька копій, то:
pj.setCopies(2); // По замовчуванню друкується одна копія
297
298. // 5. Відкриваємо діалогове вікно Друк (необовязково)
if (pj.printDialog())( // Якщо OK... try{
pj.print(); // Звертається до print(g, pf, ind)
}catch(Exception e){
System.err.println(e);
}
}
// 6. Завершуємо завдання
System.exit(0);
}
}
18.13. Друк файла
Друк текстового файла заключається в розміщенні його рядків в графічному контексті методом
drawstring(). При цьому необхідо прослідкувати за правильним розміщенням рядків в області друку і
розбиттям файла на сторінці. В лістинзі 18.9 приведений спрощений приклад друку текстового файла, імя
якого задається в командному рядку. Із файла читаються готові рядки, програма не порівнює їх довжину із
шириною області друку, не виділяє абзаци. Виведення відбувається в локальному кодуванні.
Лістинг 18.9. Друк текстового файла
import java.awt.*;
import java.awt.print.*;
import java.io.* ;
public class Print2File{
public static void main(String[] args){
if (args.length < 1){
System.err.println("Usage: Print2File path");
System, exit(0);
}
PrinterJob pj = PrinterJob.getPrinterJob();
PageFormat pf = pj.pageDialog(pj.defaultPage());
pj.setPrintable(new FilePagePainter(args[0]), pf);
if (pj.printDialog()){
try{
pj.print();
}catch(PrinterException e){}
)
System, exit(0);
}
}
class FilePagePainter implements Printable{
private BufferedReader br;
private String file;
private int page = -1;
private boolean eof;
private String[] line;
private int numLines;
public FilePagePainter(String file){
this.file = file;
try{
br = new BufferedReader(new FileReader(file));
}catch(IOException e){ eof = true; }
}
public int print(Graphics g, PageFormat pf, int ind)
throws PrinterException(
g.setColor(Color.black);
g.setFont(new Font("Serif", Font.PLAIN, 10));
298
299. int h = (int)pf.getlmageableHeight();
int x = (int)pf.getlmageableX() + 10;
int у = (int)pf.getlmageableY() + 12;
try{
// Якщо система друку запросила цю сторінку перший раз
if (ind != page){
if (eof) return Printable.NO_SUCH_PAGE;
page = ind;
line = new String[h/12]; // Масив рядків на сторінці
numLines =0; // Число рядків на сторінці
// Читаємо рядки із файла і формуємо масив рядків
while (у + 48 < pf.getlmageableY() + h){
line[numLines] = br.readLine();
if (line[numLines] == null){
eof = true; break; }
numLines++;
У += 12;
}
}
// Розміщуємо колонтитул
у = (int)pf.getImageableY() + 12;
g.drawstring("Файл: " + file + ", сторінка " +
(ind + 1), x, у);
// Залишаємо два порожні рядки
у += 36;
// Разміщуємо рядки тексту поточної сторінки
for (int i = 0; i < numLines; i++){
g.drawString(line[i], x, y) ;
у += 12;
}
return Printable.PAGE_EXISTS;
}catch(lOException e){
return Printable.NO_SUCH_PAGE;
}
}
}
18.14. Друк сторінок з різними параметрами
Друк виду Printable Job не зовсім зручний — у всіх сторінок повинні бути однакові параметри, неможна
задати число сторінок і порядок їх друку, у вікні Параметри сторінки не видно число сторінок, що
виводиться на друк. Всі ці можливості представляє друк виду Pageable Job за допомогою класу Book. Як
уже говорилось вище, спочатку створюється порожній обєкт класу Book, потім до нього додаються різні
або однакові класи, рисуючі сторінки. При цьому визначаються обєкти класу pageFormat, задаючі
параметри цих сторінок, і число сторінок. Якщо число сторінок невідомо, то замість нього указується
константа UNKNOWN_NUMBER_OF_PAGES. В такому випадку сторінки будуть друкуватися в порядку
зростання їх номерів до тих пір, поки метод print() не поверне NO_SUCH_PAGE. Метод setPage(int
pagelndex, Printable p, PageFormat pf) заміняє обєкт в позиції pagelndex на новий обєкт р.
В програмі лістинга 18.10 створюються два класи, рисуючі сторінки: Cover і Сontent. Ці класи дуже прості
— в них тільки реалізований метод print(). Клас Сover рисує титульный лист крупним напівжирним
шрифтом. Текст друкується знизу вверх вподовж довгої сторони аркуша на його правій половині. Клас
Сontent виводить звичайний текст звичайним способом. Параметри титульного аркуша визначаються в
класі pf1, параметри других сторінок задаються в діалоговому вікні Параметри сторінки і містяться в класі
рf2. В обєкт bk класу Book занесені три сторінки: перша сторінка — титульний аркуш, на двох інших
друкується один і той же текст, записаний в методі print() класу Content.
Лістинг 18.10. Друк сторінок з різними параметрами
299
300. import j ava.awt.*;
import j ava.awt.print.*;
public class Print2Book{
public static void main(String[]
args){
PrinterJob pj = PrinterJob.getPrinterJob();
// Для титульного аркуша вибирається альбомна орієнтація
PageFormat pfl = pj.defaultPage();
pfl.setOrientation(PageFormat.LANDSCAPE);
// Параметри інших сторінок задаються в діалоговому вікні
PageFormat pf2 = pj.pageDialog (new PageFormat());
Book bk = new Book();
// Перша сторінка — титульний аркуш
bk.append(new Cover(), pfl);
// Дві інших сторінки
bk.append(new Content(), pf2, 2);
// Визначається вид друку — Pageable Job
pj.setPageable(bk);
if (pj.printDialog()){
try{
pj.print() ;
}catch (Exception e){}
}
System.exit(0);
}
}
class Cover implements Printable{
public int print(Graphics g, PageFormat pf, int ind)
throws PrinterException{
g.setFont (new Font ("Helvetica-Bold", Font.PIiAIsN, 40)) ;
g.setColor(Color.black) ;
int у = (int) (pf.getlmageableY() + pf.getlmageableHeigbt() /2);
g.drawstring("Це заголовок,", 72, у);
g.drawstring("Він друкується вподовж довгої", 72, у+60);
g.drawstring("сторони аркуша паперу.", 72, у+120);
return Printable.PAGE_EXISTS;
}
}
class Content implements Printable{
public int print(Graphics g, PageFormat pf, int ind)
throws PrinterException{
Graphics2D g2 = (Graphics2D)g;
g2.setFont(new Font("Serif", Font.PLAIN, 12));
g2.setColor(Color.black);
int x = (int)pf .getlmageableX() + 30;
int у = (int)pf.getlmageableY();
g2.drawstring("Це рядки звичайного тексту.", х, у += 16);
g2.drawstring("Вони друкуються з параметрами,", х, у += 16);
g2.drawstring("вибраними в діалоговому вікні.", х, у += 16);
return Printable.PAGE_EXISTS;
}
}
300
301. Урок 19
Мережеві засоби Java
• Робота в WWW
• Робота по протоколу TCP
• Робота по протоколу UDP
Коли число компютерів в установі перевалює за десяток і співробітникам набридає бігати з дискетами
один до одного для обміна файлами, тоді в компютери вставляються мережеві карти, протягуються
кабелі і компьютери обєднуються вмережу. Спочатку всі компютери в мережі рівноправні, вони роблять
одне і те ж — це однорангова (peer-to-peer) мережа. Потім купують компютер з великими і швидкими
жорсткими дисками, і всі файли уустанови починають зберігатися на даних дисках — цей компютер
становиться файл-сервером, що надає послуги зберігання, пошуку, архівації файлів. Потім купують
дорогий і швидкий принтер. Компютер, звязаний з ним, становиться принт-сервером, надаючим послуги
друку. Потім появляються графічний сервер, обчислювальний сервер, сервер бази даних. Решта
компютерів становляться кліентами цих серверів. Така архітектура мережі називається архітектурою
кліент-сервер (client-server).
Сервер постійно знаходиться в стані очікування, він прослуховує (listen) мережу, чекаючи запиту від
клієнтів. Клієнт звязується з сервером і посилає йому запит (request) з описанням послуги, наприклад,
імя потрібного файла. Сервер обробляє запит і відправляє відповідь (response), в нашому прикладі,
файл, або повідомлення про неможливість надати послугу. Після цього звязок може бути розірваним або
продовжитися, організуючи сеанс звязку, названий сесією (session).
Запити клієнта і відповіді сервера формуються по строгим правилам, сукупність яких утворює протокол
(protocol) звязку. Всякий протокол повинен, перш за все, містить правила зєднання компютерів. Клієнт
перед посиланням запиту повинен впевнитися, що сервер в робочому стані, прослуховує мережу, і почув
клієнта. Пославши запит, клієнт повинен бути упевненим, що запит дійшов до сервера, сервер зрозумів
запит і готовий відповісти на нього. Сервер повинен упевнитися, що відповідь дійшла до кліента.
Закінчення сесії повинно бути чітко зафіксовано, щоб сервер міг звільнити ресурси, заняті обрабкою
запитів клієнта.
Всі правила, утворюючі протокол, повинні бути зрозумілими, однозначними і короткими, щоб не
загромождати мережу. Тому повідомлення, що пересилаються по мережі, нагадують шифровки, в них має
значення кожний біт. Отже, всі мережеві зєднання основані на трьох основних поняттях: клієнт, сервер і
протокол. Клієнт і сервер — поняття відносні. В одній сесії компютер може бути сервером, а в другій —
клієнтом. Наприклад, файл-сервер може послати принт-серверу файл на друк, становлячись його
клієнтом.
Для обслуговування протоколу: формування запитів і відповідей, перевірок їх відповідності протоколу,
розшифровки повідомлень, звязку з мережевими пристроями створюється программа, що складається із
двох частин. Одна частина програми працює на сервері, друга — на клієнті. Ці частини так і називаються
серверною частиною програми і клієнтською частиною програми, або, коротше, сервером і клієнтом. Дуже
часто клієнтська і серверна частини програми пишуться окремо, різними фірмами, оскільки від цих
програм вимагається тільки, щоб вони дотримувалися протоколу. Більш того, по кожному протоколу
працюють десятки клієнтів і серверів, що відрізняються різними зручностями.
Звичайно на одному компютері-сервері працюють декілька програм-серверів. Одна програма займається
електронною поштою, друга — пересилкою файлів, третя надає Web-сторінки. Для того щоб їх відрізняти,
кожній програмі-серверу надається номер порта (port). Це просто ціле додатне число, яке указує клієнт,
що звертається до певної програми-серверу. Число, взагалі кажучи, може бути будь-яким, але найбільш
розповсюдженим протоколам даються стандартні номери, щоб клієнти були твердо упевнені, що
звертаються до потрібного сервера. Так, стандартний номер порта электронної пошти 25, пересилки
файлів — 21, Web-сервера — 80. Стандартні номери простягаються від 0 до 1023. Числа, починаючи з
1024 до 65 535, можна використовувати для своїх власних номерів портів.
Все це схоже на телевізійні канали. Клієнт-телевізор звертається за допомогою антени до сервера-
телецентру і вибирає номер каналу. Він уупевнений, що на першому каналі НТН, на ругому — 1+1 і т. д.
301
302. Щоб рівномірно розподілити навантаження на сервер, часто декілька портів прослуховуються
програмами-серверами одного типу. Web-сервер, крім порту з номером 80, может прослуховувать порт
8080, 8001 і ще який-небудь.
В процесі передачі повідомлення використовується декілька протоколів. Навіть коли ми відправляємо
листа, ми спочатку пишемо повідомлення, починаючи його: "Вельмишановний Іване Петровичу!" і
закінчуючи: "Щиро Ваш". Це один протокол. Можна почати листа словами: "Вася, привіт!" і закінчити: "Ну,
пока". Це інший протокол. Потім ми кладемо листа в конверт і пишемо на ньому адресу по протоколу,
запропонованому Міністерством звязку. Потім лист попадає на пошту, упаковується в мішок, на якому
пишеться адреса по протоколу поштового звязку. Мішок завантажується в літак чи потяг, який
переміщається по своєму протоколу. Зверніть увагу, що кожний протокол тільки додає до повідомлення
свою інформацію, не змінюючи його, нічого не знаяючи про те, що зроблено по попередньому протоколу і
що буде зроблено по правилах наступного протокола. Це дуже зручно — можна програмувати один
протокол, нічого не знаяючи про інші протоколи.
Перш ніж дійти до адресата, лист проходить зворотний шлях: виймається із літака чи потяга, потім із
мішка, потім із конверта. Тому говорять про стек (stack) протоколів: "Першим прийшов, останнім вийшов".
В сучасних глобальних мережах прийнятий стек із чотирьох протоколів, названий стеком протоколів
TCP/IP. Спочатку ми пишемо повідомлення, користуючись програмою, реалізуючу прикладний
(application) протокол: HTTP (80), SMTP (25), TELNET (23), FTP (21), РОРЗ (100) або інший протокол. В
дужках записано стандартний номер порта. Потім повідомлення обробляється по транспортному
(transport) протоколу. До нього додають номера портів відправника і отримувача, контрольна сума і
довжина повідомлення. Найбільш розповсюджені транспортні протоколи TCP (Transmission Control
Protocol) і UDP (User Datagram Protocol). В результаті работи протокола TCP получається TCP-пакет
(packet), а протокола UDP — дейтаграма (datagram).
Дейтаграма невелика — всього біля кілобайта, тому повідомлення ділиться на частини, із яких
створюються окремі дейтаграми. Дейтаграми посилаються одна за другою. Вни можуть іти до отримувача
різними маршрутами, прийти зовсім в іншому порядку, деякі дейтаграми можуть загубитися. Прикладна
програм отримувача повинна сама потурбуватися про те, щоб зібрати із отриманих дейтаграм вихідне
повідомлення. Для цього звичайно перед посилкою частини повідомлення нумеруються, як сторінки в
книзі. Таким чином, протокол UDP працює як поштова служба. Посилаяючи книгу, ми розрізаємо її на
сторінки, кожну сторінку відправляємо в своєму конверті, і ніколи не впевнені, що всі листи дойдуть до
адресата.
ТСР-пакет тоже невеликий, і пересилання також іде оремими пакетами, але протокол TCP забезпечує
надійний звязок. Спочатку установлюється зєднання з отримувачем. Тільки після цього посилаються
пакеты. Отримання кожного пакета підтверджується отриму