Лабораторна робота 2
Робота з винятками і файлами в Java
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Спроектувати та реалізувати класи для представлення сутностей попередньої лабораторної роботи. Рішення повинне базуватися на раніше створеній ієрархії класів. Слід створити два похідних класи від класу, який представляє основну сутність. Один клас повинен бути доповненим можливостями читання даних з відповідно підготовленого текстового файлу та запису цих даних в інший файл після сортування. Другий клас повинен реалізовувати читання даних з XML-документу, зберігати дані в структурах, які автоматично створюються за допомогою технології зв'язування даних, та запис даних в інший XML-документ після сортування. Для уникнення дублювання даних у програмі слід також перевизначити клас, який представляє другу сутність. Похідні класи, які представляють основну сутність, повинні реалізовувати спільний інтерфейс, в якому оголошені функції читання з файлу і запису в файл.
Окрім роботи з файлами повинно бути реалізоване виведення результатів у консольне вікно.
1.2 Сортування цілих
Реалізувати програму читання з текстового файлу цілих додатних значень (числа розділені пробілами, слід читати до кінця файлу), занесення цих чисел у масив, сортування за зменшенням та за збільшенням суми цифр та зберігання обох результатів у двох нових текстових файлах. Перелічені дії реалізувати в окремій статичній функції. Для визначення порядку сортування створити класи, які реалізують інтерфейс Comparator
.
1.3 Реалізація серіалізації й десеріалізації
Описати класи Студент і Академічна група (з полем – масивом студентів). Створити об'єкти, здійснити їх бінарну серіалізацію й десеріалізацію, а також серіалізацію й десеріалізацію в XML.
1.4 Список файлів усіх підкаталогів
Увести з клавіатури ім'я певної теки. Вивести на екран імена усіх файлів цієї теки, а також усіх файлів підкаталогів, їхніх підкаталогів тощо. Реалізувати виведення через рекурсивну функцію. Якщо тека не існує, вивести повідомлення про помилку.
1.5 Робота з ZIP-архівом (додаткове завдання)
Описати класи Студент і Академічна група (з полем – масивом студентів). Створити об'єкти, здійснити запис даних про студентів академічної групи в архів. В іншій програмі здійснити читання з архіву.
1.6 Використання технологій SAX і DOM (додаткове завдання)
Підготувати XML-документ з даними про студентів академічної групи. За допомогою технології SAX здійснити читання даних з XML-документу і виведення даних на консоль. За допомогою технології DOM здійснити читання даних з того ж XML-документу, модифікацію даних і запис їх в новий документ.
2 Методичні вказівки
2.1 Обробка винятків
2.1.1 Основні концепції
Використання механізму обробки винятків є дуже важливою складовою частиною практики програмування на Java. Майже кожна програма на Java містить певні частини цього механізму. Об'єкти-винятки дозволяють програмісту відокремити точки виникнення помилок часу виконання від коду, який ці помилки повинен обробити. Це дозволяє створювати більш надійно працюючі універсальні класи і бібліотеки.
Виняток – це подія, що виникає під час виконання програми і порушує нормальне виконання інструкцій коду. Механізм генерації та обробки винятків дозволяє передати інформацію про помилку з місця виникнення у місце, де ця помилка може бути оброблена. Винятки в Java поділяють на синхронні (помилка часу виконання, ситуація, згенерована за допомогою throw
) і асинхронні (системні, збої віртуальної машини Java). Місце виникнення другої групи винятків виявити досить складно.
Механізм винятків присутній в усіх сучасних мовах об'єктно-орієнтованого програмування. У порівнянні з C++, Java реалізує більш строгий механізм роботи з винятками.
2.1.2 Синтаксис генерації винятків
Для генерації винятку використовується оператор throw
. Після ключового слова throw
міститься об'єкт класу java.lang.Throwable
, або класів, похідних від нього. Для програмних винятків найчастіше використовується клас java.lang.Exception
(похідний від Throwable
). Використання Exception
замість Throwable
дозволяє відокремити власний виняток від системних помилок. Найкраща практика керування винятками – створювати класи, похідні від Exception
. Такі похідні класи зазвичай відбивають специфіку конкретної програми.
class SpecificException extends Exception { }
Є також базовий клас для генерації системних помилок – клас Error
. Класи Exception
і Error
мають загальний базовий клас – Throwable
.
Виняток генерується шляхом використання ключового слова throw
, за яким розташовують об'єкт-виняток. У більшості випадків об'єкт-виняток створюється в точці генерації винятку за допомогою оператора new
. Наприклад, типове твердження throw
може виглядати так:
void f() . . . . . . if (/* помилка */) { throw new SpecificException(); }
У заголовку функції необхідно перелічити усі типи винятків, які генерує ця функція. Це слід зробити за допомогою ключового слова throws
:
void f() throws SpecificException, AnotherException { . . . if (/* помилка */) { throw new SpecificException(); } if (/* інша помилка */) { throw new AnotherException(); } . . . }
У наведеному нижче прикладі функція reciprocal()
генерує виняток у випадку ділення на нуль.
сlass DivisionByZero extends Exception { } class Test { double reciprocal(double x) throws DivisionByZero { if (x == 0) { throw new DivisionByZero(); } return 1 / x; } }
На відміну від C++, Java не допускає створення винятків примітивних типів. Дозволені тільки об'єкти, похідні від Throwable
або Exception
.
Під час успадкування для перевизначених функцій список винятків повинен зберігатися.
2.1.3 Синтаксис обробки винятків
Виняток, який був згенерований у певній частині коду, повинен бути перехоплений в іншій частині. Наприклад, якщо ми хочемо звернутися до функції, яка потенційно може згенерувати виняток, виклик цієї функції поміщають у блок try { }
:
double x, y; . . . try { y = reciprocal(x); }
Після блоку try
повинен міститись один чи декілька оброблювачів (блоків catch
). Кожен такий оброблювач відповідає визначеному типу винятку:
catch (DivisionByZero d) { // обробка винятку } catch (Exception ex) { // обробка винятку }
Класи винятків утворюють ієрархію. Під час порівняння типів винятків оброблювач базового типу сприймає також винятки всіх створених від нього типів. Звідси випливає, що оброблювачі похідних типів варто розміщати до оброблювачів базових типів. Припустимо, є така ієрархія класів винятків:
class BaseException extends Exception { } class FileException extends BaseException { } class FileNotFoundException extends FileException { } class WrongFormatException extends FileException { } class MathException extends BaseException { } class DivisionByZero extends MathException { } class WrongArgument extends MathException { }
Припустимо, є деяка функція, яка може згенерувати всі типи винятків:
public class Exceptions { public static void badFunc() throws BaseException { // можуть виникнути різні винятки } }
Залежно від логіки програми різні типи винятків можна обробляти більш детально:
try { Exceptions.badFunc(); } catch (FileNotFoundException ex) { // файл не знайдено } catch (WrongFormatException ex) { // хибний формат } catch (FileException ex) { // інші помилки, пов'язані з файлами } catch (MathException ex) { // усі математичні помилки обробляємо разом } catch (BaseException ex) { // підбираємо всі інші винятки функції badFunc() } catch (Exception ex) { // про всяк випадок }
Після останнього блоку catch
можна розмістити блок finally
. Цей код завжди виконується незалежно від того, виник чи не виник виняток, навіть якщо в якомусь з блоків був здійснений вихід з функції.
try { openFile(); // інші дії } catch (FileError f) { // обробка винятку } catch (Exception ex) { // обробка винятку } finally { closeFile(); }
У версії Java 7 до синтаксису винятків додані нові конструкції, які роблять роботу з винятками більш зручною. Наприклад, можна створити обробник подій різних типів з використанням побітової операції "АБО":
public void newMultiCatch() { try { methodThatThrowsThreeExceptions(); } catch (ExceptionOne | ExceptionTwo | ExceptionThree e) { // обробка всіх винятків } }
Інші додаткові можливості пов'язані з так званим блоком управління ресурсами ("try-with-resources"). Для об'єкта класу, який реалізує інтерфейс java.lang.AutoCloseable
можна розмістити створення об'єкта безпосередньо після try
. Для такого об'єкта автоматично буде викликано метод close()
після завершення блоку try {}
(аналогічно виконанню коду в finally
):
try (ClassThatImplementsAutoCloseable sc) { // дії, які можуть призвести до винятку } catch (Excrption f) { // обробка винятку } // автоматичний виклик sc.close()
На відміну від C++, не можна використовувати catch
(...)
для перехоплення будь-якого винятку. Замість цього можна використовувати перехоплення винятків базових класів:
catch (Exception ex) { // обробка винятку }
або
catch (Throwable ex) { // обробка винятку }
Типова реалізація оброблювача винятку – виклик методу printStackTrace()
.
catch (Throwable ex) { ex.printStackTrace(); }
Цей метод здійснює виведення інформації про трасування стеку в стандартний потік повідомлень про помилки System.err
. Нижче наведений приклад роботи функції printStackTrace()
:
java.lang.NullPointerException at SomeClass.g(SomeClass.java:9) at SomeClass.f(SomeClass.java:6) at SomeClass.main(SomeClass.java:3)
Якщо в межах блоку catch
() { }
не можна повністю обробити виняток, його можна передати далі:
catch (SomeException ex) { // локальна обробка винятку throw ex; }
Іноді для адекватної обробки інформації про виняток необхідно володіти певною додатковою інформацією. Наприклад, ми створюємо функцію, всередині тіла якої необхідно знайти квадратний корінь. Якщо аргумент від'ємний, необхідно генерувати виняток. Для зневадження програми корисно знати, яке саме від'ємне значення було отримане. Можна створити клас-виняток, об'єкт якого зберігатиме це значення. У конструкторі воно встановлюється, а в точці обробки винятку його можна отримати за допомогою геттера. Цей підхід можна продемонструвати на такому прикладі. Створюємо клас-виняток:
public class WrongArgumentException extends Exception { private double arg; public WrongArgumentException(double arg) { this.arg = arg; } public double getArg() { return arg; } }
Виняток може бути згенерований у якійсь функції, якщо неможливо використати аргумент:
public class SomeLib { public static void doSomeUseful(double x) throws WrongArgumentException { // перевірка x if (x < 0) throw new WrongArgumentException(x); double y = Math.sqrt(x); // подальша робота } }
Тепер перехоплений об'єкт-виняток може бути застосований для отримання більш детальної інформації.
public class ExceptionTest { public static void main(String[] args) { double x = new java.util.Scanner(System.in).nextDouble(); try { // . . . SomeLib.doSomeUseful(x); // . . . } catch (WrongArgumentException e) { System.err.println(e.getClass().getName() + e.getArg()); } } }
Як видно з наведеного прикладу, за допомогою викликів методу getClass().getName()
можна отримати ім'я класу. Це можна зробити для будь-якого об'єкта (не тільки винятку).
Виклик функції, що може згенерувати виняток, поза блоком try
приводить до помилки компіляції. Перевірка повинна обов'язково виконуватися:
double f(double x) { double y; try { y = reciprocal(x); } catch (DivisionByZero ex) { ex.printStackTrace(); y = 0; } return y; }
Неперехоплений виняток може бути передано зовнішньому оброблювачу з використанням ключового слова throws
:
double g(double x) throws DivisionByZero { double y; y = reciprocal(x); return y; }
Це правило обов'язкове для усіх винятків Java крім об'єктів класу RuntimeException
або його нащадків. Про генерацію таких винятків не треба вказувати в заголовку функції. Програміст може обробляти чи ігнорувати такі винятки на свій розсуд. Функції, які генерують такі викнятки, не декларують їх у своєму заголовку. Типовий клас винятків такого виду – NullPointerException
.
Середовище Eclipse дозволяє автоматизувати процес створення блоків перехоплення та обробки винятків. Якщо в тексті функції помітити блок та застосувати функцію Source | Surround With | Try/catch Block, помічений блок буде розташовано у блоці перехоплення винятків (try { }
), а далі будуть додані catch
-блоки, які міститимуть стандартну обробку всіх можливих винятків.
2.2 Потоки введення та виведення
2.2.1 Загальні концепції
Як більшість сучасних мов і платформ, Java узагальнює поняття потоків (streams), розповсюджуючи спільні підходи на файлові, консольні, мережеві та інші процесси введення-виведення.
Класи, які здійснюють файлове введення та виведення, а також інші дії з потоками, розташовані у пакеті java.io
. Класи цього пакету пропонують низку методів для створення таких потоків, читання, запису, тощо. Існує дві підмножини класів – відповідно для роботи з текстовими та бінарними (двійковими) файлами.
Уся робота з потоками, окрім стандартних потоків System.in
і System.out
, повинна передбачати перехоплення винятків, пов'язаних з введенням-виведенням. Це IOException
та його нащадки – FileNotFoundException
, ObjectStreamException
та інші.
Дуже важливо закрити всі файли, взаємодія з якими мала місце. Під час закриття файлів здійснюється запис у файл даних, що залишилися в буфері, звільнення буфера та інших ресурсів, пов'язаних з файлом. Закрити файл можна за допомогою методу close()
. Наприклад, для потоку in
:
in.close();
Якщо програма, яка потребує файлового введення, завантажується у середовищі Eclipse (або IntelliJ IDEA), необхідні для читання файли слід розмістити у теці проекту (не у теці пакету). Саме у теці проекту можна знайти результуючі файли, які з'являються після завершення виконання програми, що включає файлове виведення.
У програмі можна одночасно відкрити декілька потоків введення і декілька потоків виведення.
2.2.2 Робота з потоками символів
Потоки, призначені для роботи з текстовою інформацією, мають назву потоків символів. Імена класів таких потоків закінчуються відповідно словами "...Reader"
і "...Writer"
. Безпосередню роботу з текстовими файлами здійснюють об'єкти класів FileReader
та FileWriter
.
Важливий елемент роботи з файловими потоками – це буферизація. Буферизація передбачає створення в оперативній пам'яті спеціальної області (буферу), у яку дані завантажуються з файлу для подальшого поелементного читання або поелементно записуються дані з подальшим переписуванням на диск. Об'єкти класу BufferedReader
здійснюють таке буферизоване читання.
Для буферизованого виведення застосовують об'єкти класу BufferedWriter
. Безпосереднє форматоване виведення здійснюється методами print()
та println()
об'єкту класу PrintWriter
.
У наведеному нижче прикладі з файлу з ім'ям data.txt здійснюється читання одного цілого і одного дійсного значення, їхня сума записується у файл results.txt.
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.StringTokenizer; public class FileTest { void readWrite() { try { FileReader fr = new FileReader("data.txt"); BufferedReader br = new BufferedReader(fr); String s = br.readLine(); int x; double y; try { StringTokenizer st = new StringTokenizer(s); x = Integer.parseInt(st.nextToken()); y = Double.parseDouble(st.nextToken()); } finally { br.close(); } double z = x + y; FileWriter fw = new FileWriter("results.txt"); PrintWriter pw = new PrintWriter(fw); pw.println(z); pw.close(); } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { new FileTest().readWrite(); } }
Для відкриття файлу створюється об'єкт класу FileReader
, у конструкторі якого вказується рядок – ім'я файлу. Посилання на створений об'єкт передається у конструктор класу BufferedReader
. Читання з файлу здійснюється за допомогою методу readLine()
, який повертає посилання на рядок символів, або null
, якщо досягнуто кінець файлу.
Змінна s
типу String
посилається на рядок, який містить два числа. Для виділення з цього рядку окремих лексем використовують об'єкт класу StringTokenizer
, у конструктор якого передається рядок. Посилання на окремі частини рядку поступово отримують за допомогою методу nextToken()
. Ці посилання можуть бути використані безпосередньо, або використовуються для перетворення даних у числові значення (статичні методи parseDouble()
та parseInt()
класів Double
та Integer
відповідно).
Для читання з файлу можна використовувати вже знайомий клас Scanner
. Фактичним параметром конструктора може бути файловий потік. Попередній приклад можна реалізувати за допомогою класу Scanner
. Можна також скоротити код шляхом виключення непотрібних змінних. Крім того, доцільно скористатися конструкцією try () { }
Java 7 для автоматичного закриття потоку:
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.Scanner; public class FileTest { void readWrite() { try (Scanner scanner = new Scanner(new FileReader("data.txt"))) { try (PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { new FileTest().readWrite(); } }
Перевагою такого підходу є можливість довільного розташування вихідних даних (не обов'язково в одному рядку). Як видно з наведеного прикладу, кілька блоків try { }
можуть використовувати один блок catch { }
. Альтернативою є розміщення декількох тверджень всередині дужок:
try (Scanner scanner = new Scanner(new FileReader("data.txt")); PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } catch (IOException ex) { ex.printStackTrace(); }
Під час роботи з класом Scanner
можна визначити додаткові параметри, наприклад, встановити символ-роздільник (або послідовність символів). Наприклад, можна перед читанням даних додати такий рядок:
scanner.useDelimiter(",");
Тепер об'єкт-сканер буде сприймати коми як роздільники (замість пропусків).
2.2.3 Робота з бінарними потоками (потоками байтів)
Для роботи з нетекстовими (бінарними) файлами використовують потоки, імена яких замість "Reader"
або "Writer"
містять "Stream
", наприклад InputStream
, FileInputStream
, OutputStream
, FileOutputStream
тощо. Такі потоки мають назву потоків байтів. У наведеному нижче прикладі здійснюється копіювання двійкового файлу FileCopy.class
у теку проекту з новим ім'ям:
package ua.inf.iwanoff.oop.second; import java.io.*; public class FileCopy { public static void copy(String inFile, String outFile) { byte[] buffer = new byte[1024]; // Буфер байтів try (InputStream input = new FileInputStream(inFile); OutputStream output = new FileOutputStream(outFile)) { int bytesRead; while ((bytesRead = input.read(buffer)) >= 0) { output.write(buffer, 0, bytesRead); } } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { copy("bin/ua/inf/iwanoff/oop/second/FileCopy.class", "FileCopy.copy"); } }
Як видно з наведеного прикладу, Java дозволяє використовувати звичайну риску (/
) замість зворотної. Це – більш універсальний підхід, прийнятний для різних операційних систем. Крім того, зворотну риску необхідно було б записати двічі (\\
).
Для роботи з бінарними файлами існують додаткові можливості – використання потоків даних і потоків об'єктів. Так звані потоки даних (data streams) підтримують бінарне введення / виведення значень примітивних типів даних (boolean
, char
, byte
, short
, int
, long
, float
і double
), а також значень типу String
. Усі потоки даних реалізують інтерфейси DataInput
або DataOutput
. Для більшості задач достатньо стандартних реалізацій цих інтерфейсів – DataInputStream
і DataOutputStream
. Дані у файлі зберігаються в такому вигляді, в якому вони представлені в оперативній пам'яті. Для запису рядків використовують метод writeUTF()
. У наведеному нижче прикладі здійснюється запис даних:
package ua.inf.iwanoff.oop.second; import java.io.*; public class DataStreamDemo { public static void main(String[] args) { double x = 4.5; String s = "all"; int[] a = { 1, 2, 3 }; try (DataOutputStream out = new DataOutputStream( new FileOutputStream("data.dat"))) { out.writeDouble(x); out.writeUTF(s); for (int k : a) out.writeInt(k); } catch (IOException e) { e.printStackTrace(); } } }
Тепер дані можна прочитати в іншій програмі:
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.*; public class DataReadDemo { public static void main(String[] args) { try (DataInputStream in = new DataInputStream( new FileInputStream("data.dat"))) { double x = in.readDouble(); String s = in.readUTF(); List<Integer> list = new ArrayList<>(); try { while (true) { int k = in.readInt(); list.add(k); } } catch (Exception e) { } System.out.println(x); System.out.println(s); System.out.println(list); } catch (Exception e) { e.printStackTrace(); } } }
Примітка. У наведеній вище програмі вихід з циклу здійснюється через збудження винятку. Такий підхід не є рекомендованим, оскільки генерація винятків знижує ефективність роботи програми. У нашому випадку доцільно було б окремо зберігати у файлі довжину масиву перед його елементами, а потім використовувати цю довжину для організації циклу for
під час читання.
Для читання і запису даних може бути також використаний клас java.io.RandomAccessFile
. Об'єкт цього класу дозволяє вільно пересуватися всередині файлу в прямому і зворотному напрямку. Основною перевагою класу RandomAccessFile
є можливість читати і записувати дані в довільне місце файлу.
Для того щоб створити об'єкт класу RandomAccessFile
, необхідно викликати його конструктор з двома параметрами: ім'я файлу для введення / виведення і режимом доступу до файлу. Для визначення режиму можна використовувати спеціальні рядки, такі як "r"
(для читання), "rw"
(для читання й запису) тощо. Наприклад, таким може бути відкриття файлу даних:
RandomAccessFile file1 = new RandomAccessFile("file1.dat", "r"); // для читання RandomAccessFile file2 = new RandomAccessFile("file2.dat", "rw"); // для читання й запису
Після того як файл відкритий, можна використовувати методи на кшталт readDouble()
, readInt()
, readUTF()
тощо для читання або writeDouble()
, writeInt()
, writeUTF()
тощо для виведення.
В основі керування файлом лежить вказівник на поточну позицію, де відбувається читання або запис даних. На момент створення об'єкта класу RandomAccessFile
вказівник встановлюється на початок файлу і має значення 0. Виклики методів read...()
и write...()
зсувають позицію поточного вказівника на кількість прочитаних або записаних байтів. Для довільного зсуву вказівника на деяку кількість байтів можна використовувати метод skipBytes()
, або ж встановити вказівник у певне місце файлу викликом методу seek()
. Для того щоб дізнатися поточну позицію, в якій знаходиться вказівник, потрібно викликати метод getFilePointer()
. Наприклад, в одній програмі ми записуємо дані в новий файл:
RandomAccessFile fileOut = new RandomAccessFile("new.dat", "rw"); int a = 1, b = 2; fileOut.writeInt(a); fileOut.writeInt(b); fileOut.close();
В іншій програмі ми читаємо друге ціле число:
RandomAccessFile fileIn = new RandomAccessFile("new.dat", "rw"); fileIn.skipBytes(4); // переміщаємо файловий указівник до другого числа int c = fileIn.readInt(); System.out.println(c); fileIn.close();
Дізнатися довжину файлу в байтах можна за допомогою функції length()
.
2.3 Бінарна серіалізація об'єктів
Механізм серіалізації (serialization, розміщення у послідовному порядку) передбачає запис об'єктів у потік бітів для зберігання у файлі або для передачі через комп'ютерні мережі. Десеріалізація передбачає читання потоку бітів, створення збережених об'єктів та відтворення їхнього стану на момент збереження. Для того, щоб об'єкти певного класу можна було серіалізувати, клас повинен реалізовувати інтерфейс java.io.Serializable
. Цей інтерфейс не визначає жодного метода, його наявність лише вказує, що об'єкти цього класу можна серіалізувати. Однак гарантована серіалізація і десеріалізація вимагає наявності в таких класах спеціального статичного поля serialVersionUID
, яке забезпечує унікальність класу. Середовище Eclipse дозволяє згенерувати необхідне значення автоматично (Quick Fix | Adds a generated serial version ID з контекстного меню).
Класи ObjectOutputStream
та ObjectInputStream
дозволяють здійснювати серіалізацію та десеріалізацію. Вони реалізують інтерфейси ObjectOutput
та ObjectInput
відповідно. Механізми серіалізації та десеріалізації розглянемо на наведеному нижче прикладі. Припустимо, описано клас Point
:
package ua.inf.iwanoff.oop.second; import java.io.Serializable; public class Point implements Serializable { private static final long serialVersionUID = -3861862668546826739L; private double x, y; public void setX(double x) { this.x = x; } public void setY(double y) { this.y = y; } public double getX() { return x; } public double getY() { return y; } }
Також створено клас Line
:
package ua.inf.iwanoff.oop.second; import java.io.Serializable; public class Line implements Serializable { private static final long serialVersionUID = -4909779210010719389L; private Point first = new Point(), second = new Point(); public void setFirst(Point first) { this.first = first; } public Point getFirst() { return first; } public Point getSecond() { return second; } public void setSecond(Point second) { this.second = second; } }
У наведеній нижче програмі (у тому ж пакеті) здійснюється створення об'єктів з подальшою серіалізацією:
package ua.inf.iwanoff.oop.second; import java.io.*; public class SerializationTest { public static void main(String[] args) { Line line = new Line(); line.getFirst().setX(1); line.getFirst().setY(2); line.getSecond().setX(3); line.getSecond().setY(4); try (ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("temp.dat"))) { out.writeObject(line); } catch (IOException e) { e.printStackTrace(); } } }
В іншій програмі можна здійснити десеріалізацію:
package ua.inf.iwanoff.oop.second; import java.io.*; public class DeserializationTest { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream( new FileInputStream("temp.dat"))) { Line line = (Line) in.readObject(); System.out.println(line.getFirst().getX() + " " + line.getFirst().getY() + " " + line.getSecond().getX() + " " + line.getSecond().getY()); } catch (IOException e) { e.printStackTrace(); } ; } }
Можна також серіалізовувати об'єкти, які містять масиви інших об'єктів. Наприклад:
package ua.inf.iwanoff.oop.second; import java.io.*; class Pair implements Serializable { private static final long serialVersionUID = 6802552080830378203L; double x, y; public Pair(double x, double y) { this.x = x; this.y = y; } } class ArrayOfPairs implements Serializable { private static final long serialVersionUID = 5308689750632711432L; Pair[] pairs; public ArrayOfPairs(Pair[] pairs) { this.pairs = pairs; } } public class ArraySerialization { public static void main(String[] args) { Pair[] points = { new Pair(1, 2), new Pair(3, 4), new Pair(5, 6) }; ArrayOfPairs arrayOfPoints = new ArrayOfPairs(points); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "temp.dat"))) { out.writeObject(arrayOfPoints); } catch (IOException e) { e.printStackTrace(); } } }
Тепер можна здійснити десеріалізацію:
package ua.inf.iwanoff.oop.second; import java.io.*; public class ArrayDeserialization { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("temp.dat"))) { ArrayOfPairs arrayOfPairs = (ArrayOfPairs) in.readObject(); for (Pair p : arrayOfPairs.pairs) { System.out.println(p.x + " " + p.y); } } catch (Exception e) { e.printStackTrace(); } } }
Деякі поля класу, значення яких не впливають на стан об'єкта, можна описати з модифікатором transient
. Наприклад:
class SomeClass implements Serializable { transient int someUnnecessaryField; }
Такі поля не будуть збережені у потоці під час серіалізації та не будуть відтворені під час десеріалізації.
Серіалізація та десеріалізація можуть бути використані замість файлового введення та виведення. Головним недоліком бінарної серіалізації є необхідність роботи з двійковими (нетекстовими) файлами.
2.4 Робота з архівами
Пакет java.util.zip
надає можливості роботи зі стандартними файлами ZIP і GZIP форматів.
Для запису в архів застосовують клас ZipOutputStream
. За допомогою функції setMethod()
цього класу можна визначити метод архівації – ZipOutputStream.DEFLATED
(з компресією) або ZipOutputStream.STORED
(без компресії). Метод setLevel()
визначає рівень компресії (вд 0 до 9, за умовчанням Deflater.DEFAULT_COMPRESSION
, зазвичай, максимальна компресія). Метод setComment()
дозволяє додати коментар до архіву.
Для кожного запису, який треба помістити в zip-файл, створюється об'єкт ZipEntry
. Бажане ім'я для файлу передається конструктору ZipEntry
. В ньому можна окремо встановити аналогічні параметри. Далі за допомогою методу putNextEntry()
класу ZipOutputStream
"розкривається" відповідна точка входу до архіву. За допомогою засобів роботи з файловими потоками здійснюється запис даних в архів, потім слід закрити об'єкт ZipEntry
за допомогою виклику closeEntry()
.
У наведеному нижче прикладі створюється архів Source.zip
, до якого додається вміст вихідного файлу ZipCreator.java
:
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.zip.*; public class ZipCreator { public static void main(String[] args) { try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Source.zip"))) { ZipEntry zipEntry = new ZipEntry("src/ua/inf/iwanoff/oop/second/ZipCreator.java"); zOut.putNextEntry(zipEntry); try (FileInputStream in = new FileInputStream("src/ua/inf/iwanoff/oop/second/ZipCreator.java")) { byte[] bytes = new byte[1024]; int length; while ((length = in.read(bytes)) >= 0) { zOut.write(bytes, 0, length); } } zOut.closeEntry(); } catch (IOException e) { e.printStackTrace(); } } }
Новостворений архів містить відносний шлях до файлу. Якщо це не потрібно, під час створення об'єкту ZipEntry
слід вказати тільки ім'я без шляху:
ZipEntry zipEntry = new ZipEntry("ZipCreator.java");
Для того, щоб прочитати дані з архіву, необхідно скористатися класом ZipInputStream
. У кожному такому архіві завжди потрібно переглядати окремі записи (entries). Метод getNextEntry()
повертає об'єкт типу ZipEntry
. Метод read()
класу ZipInputStream
повертає -1 наприкінці поточного запису (а не тільки в кінці Zip-файлу). Далі викликається метод closeEntry()
для отримання можливості переходу до зчитування наступного запису. В наведеному нижче прикладі здійснюється читання запсу ZipCreator.java
з раніше створеного архіву та виведення його вмісту в консольне вікно:
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.zip.*; public class ZipExtractor { public static void main(String[] args) { try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Source.zip"))) { ZipEntry entry; byte[] buffer = new byte[1024]; while ((entry = zIn.getNextEntry()) != null) { int bytesRead; System.out.println("------------" + entry.getName() + "------------"); while ((bytesRead = zIn.read(buffer)) >= 0) { System.out.write(buffer, 0, bytesRead); } zIn.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
Аналогічно здійснюється робота з архівами формату GZIP. Відповідні потоки читання та запису – GZIPInputStream
і GZIPOutputStream
.
2.5 Використання мови XML
2.5.1 Загальні концепції
Розширювана мова розмічування XML (eXtensible Markup Language) – це незалежний від платформи метод структурування інформації. Оскільки XML відокремлює зміст документу від його структури, його успішно використовують для обміну інформацією. Наприклад, XML можна використовувати для передачі даних між програмою та базами даних, або між базами даних, що мають різні формати.
Файли формату XML – це завжди текстові файли. Синтаксис мови XML багато в чому схожий на синтаксис мови HTML, яка застосовується для розмічування текстів, що публікуються в Internet. Мова XML також може бути безпосередньо застосована для розмітки текстів.
Найчастіше документ у форматі XML починається з так званого префіксу. Префікс для документа в загальному випадку має такий вигляд:
<?xml version="1.0" [інші-атрибути] ?>
Серед можливих атрибутів найбільш корисним є атрибут encoding="кодування"
. Він задає кодування для тексту. Якщо необхідно використовувати однобайтові (не UNICODE) символи кирилиці, це можна визначити, наприклад, в такий спосіб:
<?xml version="1.0" encoding="Windows-1251"?>
Після заголовку може міститись інформація про тип документу. Решта XML-документу складається з набору XML-елементів. Елементи розділені тегами. Початкові теги починаються з символу <
з розміщеним далі ім'ям елементу. Кінцеві теги починаються з символів </
, за якими слід ім'я елементу. Початковий і кінцевий теги закінчуються символом >
. Все, що знаходиться між двома тегами, – це вміст елементу. Всі стартові теги повинні бути співставлені кінцевими тегами. Всі значення атрибутів повинні бути вказані в лапках. Документ обов'язково повинен мати кореневий тег, у який вкладають усі інші теги.
На відміну від HTML, XML дозволяє використовувати необмежений набір пар тегів, кожна з яких представляє не те, як включені в неї дані повинні виглядати, а те, що вони означають. XML дозволяє створювати свій набір тегів для кожного класу документів. Таким чином, точніше назвати його не мовою, а метамовою.
Маючи формально описану структуру документа, можна перевірити його коректність. Наявність тегів розмітки дозволяє аналізувати документ як людині, так і програмі. XML-документи, в першу чергу, призначені для програмного аналізу їхнього вмісту.
У наведеному нижче прикладі в XML-файлі зберігаються прості числа.
<?xml version="1.0" encoding="UTF-8"?> <Numbers> <Number>1</Number> <Number>2</Number> <Number>3</Number> <Number>5</Number> <Number>7</Number> <Number>11</Number> </Numbers>
Теги Numbers
та Number
вигадав автор документу. Відступи в тексті файлу використані для поліпшення його сприйняття людиною.
Теги можуть містити в собі атрибути – додаткову інформацію про елементи, яка міститься всередині кутових дужок. Значення атрибутів обов'язково брати у лапки. Наприклад, можна запропонувати тег Message
з атрибутами to
та from
:
<Message to="you" from="me"> <Text> Для чого потрібен XML? </Text> </Message>
Важливим правилом формування XML є обов'язковість вживання кінцевих тегів. Крім того не можна плутати порядок кінцевих тегів. Цей текст містить помилку:
<A> <B> text </A> </B>
Необхідно писати так:
<A> <B> text </B> </A>
Допускається використання порожніх тегів. Такі теги закінчуються символом /. Наприклад, можна вживати <Nothing/>
замість пари <Nothing></Nothing>
.
На відміну від HTML-тегів, XML-теги залежать від регістру, тому <cat>
та <CAT>
– це різні теги.
У XML-документі можна вживати коментарі, синтаксис яких збігається з синтаксисом коментарів HTML-документів:
<!-- Це коментар -->
Програми розпізнавання XML-документів – так звані XML-парсери – здійснюють розбір документа до знаходження першої помилки, на відміну від HTML-парсерів, вбудованих в браузер. Браузери намагаються відобразити HTML-документ, навіть, якщо код містить помилки.
XML-документ, який відповідає всім синтаксичним правилам XML, вважається правильно оформленим документом (коректним документом, well-formed document).
2.5.2 Стандартні підходи до роботи з XML-документами. Засоби JAXP
Існує два стандартних підходи до роботи з XML-документами в програмах:
- подіє-орієнтована модель документу (Simple API for XML, SAX), що працює на потоці даних, обробляючи його, коли виникають пов'язані з різними тегами події;
- об'єктна модель документу (Document Object Model, DOM), що дозволяє створити в пам'яті колекцію пов'язаних з тегами об'єктів, організованих в ієрархію.
Базований на подіях підхід не дозволяє розробнику змінювати дані в вихідному документі. В разі необхідності коригування частини даних документ треба повністю оновити. На відміну від нього DOM забезпечує API, який дозволяє розробникові додавати або видаляти вузли в будь-якій точці дерева в застосунку.
Обидва підходи використовують поняття парсеру. Парсер (parser) – це програмний застосунок, призначений для того, щоб аналізувати документ шляхом розділення його на лексеми (tokens). Парсер може ініціювати події (як у SAX), або будувати в пам'яті дерево даних.
Для реалізації стандартних підходів до роботи з XML в Java SE використовують засоби Java API for XML Processing (JAXP, інтерфейс програмування застосунків Java для роботи з XML). JAXP надає засоби валідації і розбору XML-документів. Для реалізації об'єктної моделі документа JAXP включає програмний інтерфейс DOM, SAX реалізований однойменною програмним інтерфейсом. На додаток до них надано програмний інтерфейс Streaming API for XML (StAX, потоковий API для XML), а також засоби XSLT (XML Stylesheet Language Transformations, мова перетворення XML-документів).
2.5.3 Використання Simple API for XML і StAX
Simple API for XML (SAX, простий програмний інтерфейс для роботи з XML) надає послідовний механізм аналізу XML-документу. Аналізатор, який реалізує інтерфейс SAX (SAX Parser), обробляє інформацію з XML документу як єдиний потік даних. Цей потік даних доступний тільки в одному напрямку, тобто, раніше оброблені дані неможливо повторно прочитати без повторного аналізу. Більшість програмістів збігається в думці, що обробка XML документів з використанням SAX, в цілому, швидше, ніж під час використання DOM. Це пояснюється тим, що потік SAX вимагає набагато меншого обсягу пам'яті в порівнянні з побудовою повного дерева DOM.
SAX аналізатори реалізують з використанням підходу, керованого подіями (event-driven approach) коли програмісту необхідно описати оброблювачі подій, які викликаються аналізаторами під час обробки XML-документу.
Засоби Java SE для роботи з SAX реалізовані в пакетах javax.xml.parsers
і org.xml.sax
, а також у вкладених в них пакетах. Для створення об'єкта класу javax.xml.parsers.SAXParser
слід скористатися класом javax.xml.parsers.SAXParserFactory
, що надає відповідні фабричні методи. SAX парсер не створює в пам'яті представлення документу XML. Замість цього, SAX парсер інформує клієнтів про структуру документу XML, використовуючи механізм зворотного виклику. Можна самостійно створити клас, який реалізує низку необхідних інтерфейсів, зокрема org.xml.sax.ContentHandler
. Однак більш простий і рекомендований спосіб - використовувати клас org.xml.sax.helpers.DefaultHandler
, створивши похідний клас і перекривши його методи, які повинні викликатися під час виникнення різних подій в процесі аналізу документу. Найбільш часто перекриваються такі методи:
-
startDocument()
іendDocument()
- методи, які викликаються на початку і наприкінці аналізу XML-документу startElement()
іendElement()
- методи, які викликаються на початку і наприкінці аналізу елемента документу-
characters()
- метод, що викликається під час отримання текстового вмісту елемента XML-документу.
Наведений нижче приклад ілюструє використання SAX для читання документу. Припустимо, в каталозі проекту створено файл Hello.xml з таким вмістом:
<?xml version="1.0" encoding="UTF-8" ?> <Greetings> <Hello Text="Привіт, це атрибут!"> Привіт, це текст! </Hello> </Greetings>
Примітка. Під час збереження файлу слід вказати кодування UTF-8.
Код програми, яка читає дані з XML, буде таким:
package ua.inf.iwanoff.oop.second; import java.io.IOException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class HelloSAX extends DefaultHandler { @Override public void startDocument() { System.out.println("Opening document"); } @Override public void endDocument() { System.out.println("Done"); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { System.out.println("Opening tag: " + qName); if (attributes.getLength() > 0) { System.out.println("Атрибути: "); for (int i = 0; i < attributes.getLength(); i++) { System.out.println(" " + attributes.getQName(i) + ": " + attributes.getValue(i)); } } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { System.out.println("Closin tag: " + qName); } @Override public void characters(char[] ch, int start, int length) throws SAXException { String s = new String(ch).substring(start, start + length).trim(); if (s.length() > 0) { System.out.println(s); } } public static void main(String[] args) { SAXParser parser = null; try { parser = SAXParserFactory.newInstance().newSAXParser(); } catch (ParserConfigurationException | SAXException e) { e.printStackTrace(); } if (parser != null) { InputSource input = new InputSource("Hello.xml"); try { parser.parse(input, new HelloSAX()); } catch (SAXException | IOException e) { e.printStackTrace(); } } } }
Оскільки метод characters()
викликається для кожного тегу, вміст є сенс виводити, якщо рядок не порожній.
StAX був розроблений як щось середнє між інтерфейсами DOM і SAX. У цьому програмному інтерфейсі використана метафора курсору, що представляє точку входу в межах документу. Застосунок переміщує курсор вперед, читаючи інформацію та отримуючи інформацію від синтаксичного аналізатора за необхідності.
2.5.4 Використання Об'єктної моделі документу (DOM)
DOM є серією Рекомендацій, що виробляються Консорціумом World Wide Web (W3C). DOM починалася як спосіб ідентифікації і маніпулювання елементами на HTML-сторінці (DOM Level 0).
Діюча Рекомендація DOM (DOM Level 3) є API, який визначає об'єкти, представлені в XML-документі, а також методи і властивості, які використовуються для доступу до них і маніпулювання ними.
Починаючи з DOM Рівня 1, DOM API містить інтерфейси, які представляють різні типи інформації, що можуть бути знайдені в XML-документі. Він також включає в себе методи, необхідні для роботи з цими об'єктами. Можна навести деякі найбільш вживані методи стандартних інтерфейсів DOM.
Інтерфейс Node
є основним типом даних DOM. Він визначає низку корисних методів для отримання даних про вузли та навігації вузлами:
getFirstChild()
іgetLastChild()
повертають перший або останній дочірній елемент цього вузла;getNextSibling()
іgetPreviousSibling()
повертають наступного або попереднього "брата" цього вузла;getChildNodes()
повертає посилання на список типуNodeList
дочірніх елементів цього вузла; за допомогою методів інтерфейсуNodeList
можна отримати i-й вузол (методitem(i)
) і загальну кількість таких вузлів (методgetLength()
);getParentNode()
повертає "батьківський" вузол;getAttributes()
повертає асоціативний масив типуNamedNodeMap
атрибутів цього вузла;hasChildNodes()
повертаєtrue
, якщо вузол має дочірні елементи.
Існує низка методів, що забезпечують модифікацію XML-документу – insertBefore()
, replaceChild()
, removeChild()
, appendChild()
тощо.
Крім Node
, DOM також визначає кілька підінтерфейсів інтерфейсу Node
:
-
Element
– представляє елемент XML в вихідному документі; в елемент входить пара тегів (що відкривається і що закривається) і весь текст між ними; Attr
- представляє атрибут елемента;Text
– вміст елемента;Document
– представляє весь XML-документ; тільки один об'єкт типуDocument
існує для кожного XML-документу; маючи об'єктDocument
, можна знайти корінь дерева DOM за допомогою методуgetDocumentElement()
; від кореня можна маніпулювати всім деревом.
Додатковими типами вузлів є:
Comment
– представляє коментар в XML-файлі;ProcessingInstruction
– представляє інструкцію обробки;CDATASection
– представляє розділCDATA
.
XML-парсери вимагають створення екземпляра певного класу. Недоліком цього є те, що під час зміни парсерів необхідно змінювати вихідний код. Для деяких парсерів іноді можна використовувати так звані фабричні класи. За допомогою статичного методу newInstance()
створюється екземпляр "фабричного" об'єкта, за допомогою якого створюється об'єкт класу, що реалізує інтерфейс DocumentBuilder
. Такий об'єкт безпосередньо є необхідним парсером: реалізує методи DOM, які потрібні для аналізу й обробки XML-файлу. Під час створення об'єкта-парсера можуть генеруватися винятки, які необхідно перехоплювати. Далі можна створювати об'єкт типу Document
, завантажувати дані з файлу з ім'ям, наприклад, fileName
і здійснювати його аналіз:
try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(fileName); . . .
Після обходу й модифікації дерева його можна зберегти в іншому файлі.
Використання DOM розглянемо на прикладі попереднього файлу (Hello.xml
). Наведена нижче програма виводить на консоль текст атрибуту і тексту, змінює їх і зберігає зміни в новому XML-документі:
package ua.inf.iwanoff.oop.second; import java.io.*; import org.w3c.dom.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; public class HelloDOM { public static void main(String[] args) throws Exception { Document doc; // посилання на об'єкт "документ" // Створюємо "будівник документів" за допомогою "фабричного методу": DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); doc = db.parse(new File("Hello.xml")); // Знаходимо кореневий тег: Node rootNode = doc.getDocumentElement(); // Переглядаємо всі "дочірні" теги: for (int i = 0; i < rootNode.getChildNodes().getLength(); i++) { Node currentNode = rootNode.getChildNodes().item(i); if (currentNode.getNodeName().equals("Hello")) { // Переглядаємо всі атрибути: for (int j = 0; j < currentNode.getAttributes().getLength(); j++) { if (currentNode.getAttributes().item(j).getNodeName().equals("Text")) { // Знайшли потрібний атрибут. Виводимо текст атрибуту - вітання: System.out.println(currentNode.getAttributes().item(j).getNodeValue()); // Змінюємо вміст атрибуту: currentNode.getAttributes().item(j).setNodeValue("Привіт, тут був DOM!"); // Подальший пошук є недоцільним: break; } } // Змінюємо текст: System.out.println(currentNode.getTextContent()); currentNode.setTextContent("\n Привіт, тут теж був DOM!\n"); break; } } // Створення об'єкта-перетворювача (в цьому випадку - для запису в файл). // Використовуємо фабричний метод: Transformer transformer = TransformerFactory.newInstance().newTransformer(); // Запис у файл: transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(new File("HelloDOM.xml")))); } }
Після виконання програми у теці проекту можна буде знайти такий файл (HelloDOM.xml
):
<?xml version="1.0" encoding="UTF-8" standalone="no"?><Greetings> <Hello Text="Привіт, тут був DOM!"> Привіт, тут теж був DOM! </Hello> </Greetings>
У наведеному прикладі для збереження зміненого документу в файлі використано клас javax.xml.transform.Transformer
. У загальному випадку цей клас використовують для реалізації так званого XSLT-перетворення. XSLT (eXtensible Stylesheet Language Transformations) - мова перетворень XML-документів у інші XML-документи або інші об'єкти, такі як HTML, звичайний текст тощо. XSLT-процесор приймає один або кілька XML-документів джерела, а також один або кілька модулів перетворення, і обробляє їх для отримання вихідного документу. Перетворення містить набір правил шаблону: інструкції та інші директиви, якими керується XSLT-процесор під час генерації вихідного документу.
2.5.5 Використання шаблону і схеми документу
Структуровані дані, які можуть бути представленими у формі XML-файлу, потребують додаткової інформації. Найбільш розповсюдженими є два основних формати представлення такої інформації – Визначення шаблону документу (DTD) та Схема документу (XSD).
DTD (Document Template Definition) – набір правил, що дозволяють однозначно визначити структуру певного класу XML-документів. DTD не є XML-документом. DTD дуже простий, але не описує типів елементів. Директиви DTD можуть бути присутніми як у заголовку самого XML-документу (internal DTD), так і в іншому файлі (external DTD). Наявність DTD не є обов'язковою.
Наприклад, ми маємо такий XML-файл:
<?xml version="1.0" encoding="UTF-8"?> <Pairs> <Pair> <x>1</x> <y>4</y> </Pair> <Pair> <x>2</x> <y>2</y> </Pair> . . . </Pairs>
DTD-файл, який описує структуру цього документа, матиме такий вигляд:
<?xml version="1.0" encoding="UTF-8"?> <!ELEMENT Pair (x, y)> <!ELEMENT x (#PCDATA)> <!ELEMENT y (#PCDATA)> <!ELEMENT Pairs (Pair+)>
Знак плюс в останньому рядку означає що елементів Pair
всередині тегу Pairs
може бути один або багато. Крім того, можна також використовувати * (0 або багато), знак питання (0 або 1). Відсутність знака означає, що може бути присутнім саме один елемент.
XML Schema – це альтернативний DTD спосіб завдання структури документу. На відміну від визначення шаблону, схема є XML-документом. Крім того, XML схема своїми можливостями істотно перевершує DTD. Наприклад, у схемі можна вказувати типи тегів та атрибутів, визначати обмеження тощо.
XML-документ, який є правильно оформленим, посилається на граматичні правила та повністю їм відповідає, має назву валідного (valid) документу.
Для того щоб запобігти конфліктам імен тегів, у XML можна створювати так звані простори імен. Простір імен визначає префікс, пов'язаний з певною схемою документу та додається до тегів. Власний простір імен визначається за допомогою конструкції на кшталт такої:
<root xmlns:pref="http://www.someaddress.org/">
У цьому прикладі root
– кореневий тег XML-документу, pref
– префікс, який визначає простір імен, "http://www.someaddress.org/
" – якась адреса, наприклад, доменне ім'я автора схеми. Програми, які обробляють XML-документи, ніколи не перевіряють цю адресу. Вона необхідна лише для забезпечення унікальності простору імен.
Безпосередньо схема використовує простір імен xs
.
Використання схеми документу можна продемонструвати на такому прикладі. Припустимо, ми маємо такий XML-файл:
<?xml version="1.0" encoding="Windows-1251" ?> <Student Name="Джон" Surname="Сміт"> <Marks> <Mark Subject="Математика" Value="B"/> <Mark Subject="Фізика" Value="A"/> <Mark Subject="Програмування" Value="C"/> </Marks> <Comments> Не наш студент </Comments> </Student>
Створення файлу схеми слід починати зі стандартної конструкції:
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> </xs:schema>
Проміж тегами <xs:schema>
та </xs:schema>
буде розташована інформація про схему документу. Для того, щоб описати теги документу, всередині його можна додавати стандартні теги. Для складних тегів, у які вкладаються інші, або які мають параметри:
<xs:element name="ім'я тегу"> <xs:complexType> . . . </xs:complexType> </xs:element>
Всередині тегу можна розмістити список елементів:
<xs:sequence> . . . </xs:sequence>
Можна розмістити посилання на інший тег:
<xs:element ref="ім'я іншого тегу"/>
Для елементів, які безпосередньо містять дані, використовують такий тег
<xs:element name="ім'я тегу" type="ім'я типу"/>
Наведена нижче таблиця містить деякі стандартні типи даних, що використовуються в схемі:
Ім'я |
Опис |
xs:string |
Рядок символів як послідовність 10646 символів Unicode або ISO/IEC, включаючи пропуск, символ табуляції, повернення каретки і переведення рядка |
xs:integer |
ціле значення |
xs:boolean |
Бінарні логічні значення: true або false ,1 або 0. |
xs:float |
32-бітне число з плаваючою комою |
xs:double |
64-бітне число з плаваючою комою |
xs:anyURI |
Універсальний ідентифікатор ресурсу (Uniform Resource Identifier) |
Тег
<xs:attribute name="ім'я атрибуту" type="ім'я типу" />
дозволяє описувати атрибути.
Існує також велика кількість додаткових параметрів тегів. Параметр maxOccurs
задає максимальну кількість входжень елемента, minOccurs
задає мінімальну кількість входжень елемента, unbounded
визначає необмежену кількість входжень, required
визначає обов'язкове входження, mixed
визначає елемент, який має змішаний тип та ін.
Для нашого студента можна запропонувати такий файл схеми (Student.xsd
):
<?xml version="1.0"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Student"> <xs:complexType> <xs:sequence> <xs:element name="Comments" type="xs:string"/> <xs:element name="Marks"> <xs:complexType> <xs:sequence> <xs:element ref="Mark" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="Name" type="xs:string" /> <xs:attribute name="Surname" type="xs:string" /> </xs:complexType> </xs:element> <xs:element name="Mark"> <xs:complexType> <xs:attribute name="Subject" type="xs:string" /> <xs:attribute name="Value" type="xs:string" /> </xs:complexType> </xs:element> </xs:schema>
2.5.6 Технологія зв'язування даних
Використання мови Java передбачає зручний спосіб роботи з XML-файлами – механізм зв'язування даних. Цей механізм передбачає генерацію набору класів, які описують елементи файлу, та створення відповідної структури об'єктів у пам'яті.
Засіб зв'язування даних XML містить компілятор схеми, що транслює схему в набір специфічних для схеми класів з відповідними методами доступу і зміни (тобто get
і set
). Він також містить механізм маршалізації (запису структурованих даних в XML-документ), підтримує демаршалізацію XML документів у відповідну структуру взаємозалежних екземплярів. Автоматично створеною структурою даних можна користуватися без ручного розміщення даних у списках або масивах.
Традиційно першою технологією зв'язування даних була технологія Castor. Пізніше був стандартизований API JAXB (Java Architecture for XML Binding). Версія 2 специфікації JAXB припускає як генерацію класів за схемою, так і генерацію схеми за існуючою структурою класів.
Для підтримки стандарту технологій API JAXB в середовищі Eclipse повинні бути встановлені засоби Dali Java Persistence Tools (JAXB Support). Якщо в Eclipse не встановлені необхідні програмні засоби, їх можна додати за допомогою головного меню Eclipse Help | Install New Software, далі в рядку Work with: обираємо рядок, який містить адресу http://download.eclipse.org/releases/
для відповідної версії. Далі знаходимо в списку потрібні програмні засоби і натискаємо Next, на сторінці Install Details знову натискаємо Next, далі слід погодитися з умовами ліцензії й натиснути Finish. Після завантаження нового програмного забезпечення слід перезавантажити Eclipse.
Примітка: відповідні плагіни можна також завантажити у попередніх версіях Eclipse, які будуть вказані у рядку Work with:.
Технологія зв'язування даних найбільш часто застосовується для генерації класів за наявною схемою. Спочатку в теці проекту треба створити файл схеми (*.xsd
).
Для роботи генератора класів необхідно використовувати JDK, а не JRE. Якщо засоби JDK не встановлені на комп'ютері, їх можна завантажити зі сторінки http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html#javasejdk. Після встановлення (копіювання) JDK в опціях Eclipse (Window | Preferences) обираємо Java | Installed JREs, далі Add... | Next | Directory... Необхідно вибрати встановлену раніше JDK і натиснути Finish. Після цього в списку встановлених JRE обираємо JDK, яку слід встановити за умовчанням і натиснути OK.
Якщо проект було створено раніше, йому слід встановити JDK як JRE за умовчанням в опціях проекту У головному меню викликаємо функцію Project | Properties | Java Build Path далі на закладці Libraries обираємо JRE System Library, натискуємо кнопку Edit... , переключаємо System Library в Alternate JRE, у відповідному рядку вибираємо встановлену раніше JDK.
Для нормальної роботи з XML-документами, які містять кириличні символи, в опціях проекту слід встановити кодову таблицю UTF-8 (Project | Properties | Resource | Text file encoding | Other та вибрати UTF-8).
Далі необхідно обрати файл xsd
в дереві Package Explorer. У контекстному меню вибираємо Generate | JAXB Classes. Далі у вікні майстра генерації класів вказуємо проект, пакет та інші додаткові відомості, якщо необхідно. У разі успішного завершення генерації в зазначеному пакеті з'являться згенеровані класи.
Розглянемо такий приклад. В теці проекту створюємо, XML-документ Hello.xml (New | File контекстного меню проекту). Цей файл бажано відкрити за допомогою текстового редактора (Open With | Text Editor) і додати такий текст:
<?xml version="1.0" encoding="UTF-8" ?> <Greetings> <Hello Text="Привіт, XML!" /> </Greetings>
Цьому документу відповідає файл схеми Hello.xsd, який ми також створюємо в теці проекту. Цей файл також бажано відкрити за допомогою текстового редактора і додати такий текст:
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Greetings"> <xs:complexType> <xs:sequence> <xs:element name="Hello"> <xs:complexType> <xs:attribute name="Text" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
Засобами Dali Java Persistence Tools здійснюємо генерацію класів. У дереві проекту (у відповідному пакеті) з'являються файли Greetings.java
і ObjectFactory.java
.
Клас Greetings
представляє кореневий тег XML-документу й містить всередині вкладений клас Hello
. Взагалі всім вкладеним тегам відповідають вкладені класи, розташовані всередині класу, який відповідає за кореневий тег. Відповідно клас Greetings
містить поле типу Greetings.Hello
і надає методи getHello()
і setHello()
. Щодо класу Greetings.Hello
, то відповідно до схеми XML-документу цей клас містить поле text
типу String для представлення відповідного атрибуту, а також функції getText()
і setText()
. Анотації в коді керують поданням даних в XML-документі.
Клас ObjectFactory
надає так звані фабричні методи для створення об'єктів згенерованих класів: createGreetings()
і createGreetingsHello()
. Оскільки під час генерації класів завжди створюється клас з ім'ям ObjectFactory
, класи, які відповідають різним схемам, слід розташовувати в різних пакетах.
Тепер у функції main()
можна здійснити дії щодо завантаження документу, читання і зміни значення атрибута і запису в новий файл:
package ua.inf.iwanoff.oop.second; import java.io.*; import javax.xml.bind.*; public class HelloJAXB { public static void main(String[] args) { try { // Через об'єкт класу JAXBContext забезпечується доступ до JAXB API: JAXBContext jaxbContext = JAXBContext. newInstance("ua.inf.iwanoff.oop.second"); // пакет з необхідними класами // Зчитуємо дані з файлу й завантажуємо в об'єкт згенерованого класу: Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); Greetings greetings = (Greetings)unmarshaller.unmarshal(new FileInputStream("Hello.xml")); // Виводимо старе значення атрибута: System.out.println(greetings.getHello().getText()); // Змінюємо значення атрибута: greetings.getHello().setText("Привіт, JAXB!"); // Створюємо об'єкт-Marshaller для виведення в файл: Marshaller marshaller = jaxbContext.createMarshaller(); // "Включаємо" форматування: marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // Зберігаємо об'єкт у новому файлі: marshaller.marshal(greetings, new FileWriter("HelloJAXB.xml")); } catch (JAXBException | IOException e) { e.printStackTrace(); } } }
Новий файл HelloJAXB.xml
буде таким:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Greetings> <Hello Text="Привіт, JAXB!"/> </Greetings>
Як видно з прикладу, технологія зв'язування даних забезпечує більш якісне форматування XML-документа.
Примітка: Для підтримки стандартних технологій API JAXB в середовищі IntelliJ IDEA Community Edition необхідно виконати деякі налаштування. Один із шляхів реалізації технології JAXB – підключення утиліти xjc.exe
, що входить в набір засобів JDK. Цю утиліту можна запускати в командному рядку, проте доцільніше налаштувати контекстне меню. У вікні Settings обираємо Tools | External Tools і натискаємо кнопку "+". У діалоговому вікні Edit Tool вводимо ім'я (Name:) нової команди Generate JAXB Classes
, шлях до утиліти xjc.exe
(Program:), який на конкретному комп'ютері слід вибрати в діалоговому вікні вибору файлів (кнопка "...") і параметри (Parameters:), які в нашому випадку будуть такими:
-p $FileFQPackage$ -d "$SourcepathEntry$" "$FilePath$"
Для того, щоб створена команда працювала коректно, файл схеми слід розташувати у новому пакеті, в якому потім з'являться згенеровані файли.
2.5.7 Серіалізація в XML-файли
Головним недоліком описаної раніше бінарної серіалізації є необхідність роботи з двійковими (нетекстовими) файлами. Зазвичай такі файли використовують не для довгострокового зберігання даних, а для одноразового зберігання і відновлення об'єктів. Безумовно, більш зручною й керованою є серіалізація в текстовий файл, зокрема, в XML-документ. Існує декілька підходів до серіалізації й десеріалізації, побудованої на XML. Найбільш простим є підхід, заснований на використанні класів java.beans.XMLEncoder
і java.beans.XMLDecoder
. Найбільш природне застосування цих класів – зберігання та відтворення елементів графічного інтерфейсу. Але можна також зберігати об'єкти інших класів, які відповідають специфікації Java Beans.
Java Bean -
це клас, що задовольняє таким вимогам:
- клас відкритий (
public
) - відсутні відкриті дані (відкритими можуть бути тільки методи)
- клас повинен містити усталений конструктор (конструктор без аргументів)
- клас повинен реалізовувати інтерфейс
java.io.Serializable
- пара методів з іменами
setNnn()
іgetNnn()
утворюють властивість з іменемnnn
і відповідним типом. Для властивостей типуboolean
використовують "is
" замість "get
" (isNnn()
).
Раніше були реалізовані класи Line
і Point
. XML-серіалізація не вимагає реалізації інтерфейсу Serializable
. Однак класи повинні бути відкритими, мати відкриті функції доступу (геттери і сеттери) до закритих полів. Клас Point
:
package ua.inf.iwanoff.oop.second; public class Point { private double x, y; public void setX(double x) { this.x = x; } public void setY(double y) { this.y = y; } public double getX() { return x; } public double getY() { return y; } }
Клас Line
:
package ua.inf.iwanoff.oop.second; public class Line { private Point first = new Point(), second = new Point(); public void setFirst(Point first) { this.first = first; } public Point getFirst() { return first; } public Point getSecond() { return second; } public void setSecond(Point second) { this.second = second; } }
Можна запропонувати такий код, який забезпечує XML-серіалізацію:
package ua.inf.iwanoff.oop.second; import java.beans.XMLEncoder; import java.io.*; public class XMLSerialization { public static void main(String[] args) { Line line = new Line(); line.getFirst().setX(1); line.getFirst().setY(2); line.getSecond().setX(3); line.getSecond().setY(4); try (XMLEncoder xmlEncoder = new XMLEncoder(new FileOutputStream("Line.xml"))) { xmlEncoder.writeObject(line); xmlEncoder.flush(); } catch (IOException e) { e.printStackTrace(); } } }
Після виконання програми ми отримаємо такий XML-файл:
<?xml version="1.0" encoding="UTF-8"?> <java version="1.8.0_66" class="java.beans.XMLDecoder"> <object class="ua.inf.iwanoff.labs.fifth.Line"> <void property="first"> <void property="x"> <double>1.0</double> </void> <void property="y"> <double>2.0</double> </void> </void> <void property="second"> <void property="x"> <double>3.0</double> </void> <void property="y"> <double>4.0</double> </void> </void> </object> </java>
Тепер можна здійснити десеріалізацію за допомогою такого коду:
package ua.inf.iwanoff.oop.second; import java.beans.XMLDecoder; import java.io.*; public class XMLDeserialization { public static void main(String[] args) { try (XMLDecoder xmlDecoder = new XMLDecoder(new FileInputStream("Line.xml"))) { Line line = (Line)xmlDecoder.readObject(); System.out.println(line.getFirst().getX() + " " + line.getFirst().getY() + " " + line.getSecond().getX() + " " + line.getSecond().getY()); } catch (IOException e) { e.printStackTrace(); } } }
Існують також інші (нестандартні) реалізації XML-серіалізації.
2.6 Робота з файловою системою
Пакет java.io
надає можливість роботи як із вмістом файлів, так і з файловою системою в цілому. Цю можливість реалізує клас File
. Для створення об'єкта цього класу як параметр конструктора слід визначити повний або відносний шлях до файлу. Наприклад:
File dir = new File("C:\\Users"); File currentDir = new File("."); // Тека проекту (поточна)
Клас File
містить методи для отримання списку файлів визначеної теки (list()
, listFiles()
), отримання та модифікації атрибутів файлів (setLastModified()
, setReadOnly()
, isHidden()
, isDirectory()
тощо), створення нового файлу (createNewFile()
, createTempFile()
), створення тек (mkdir()
), видалення файлів та тек (delete()
) та багато інших. Роботу деяких з цих методів можна продемонструвати на наведеному нижче прикладі:
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.*; public class FileTest { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки, яку ви хочете створити:"); String dirName = scanner.next(); File dir = new File(dirName); // Створюємо нову теку: if (!dir.mkdir()) { System.out.println("Не можна створити теку!"); return; } // Створюємо новий файл всередині нової теки: File file = new File(dir + "\\temp.txt"); file.createNewFile(); // Показуємо список файлів теки: System.out.println(Arrays.asList(dir.list())); file.delete(); // Видаляємо файл dir.delete(); // Видаляємо теку } }
Функція list()
без параметрів дозволяє отримати масив рядків – усіх файлів та підкаталогів теки, визначеної під час створення об'єкта типу File
. Виводяться відносні імена файлів (без шляхів). У наведеному нижче прикладі ми отримуємо список файлів та підкаталогів теки, ім'я якої вводиться з клавіатури:
package ua.inf.iwanoff.oop.second; import java.io.File; import java.io.FilenameFilter; import java.util.Scanner; public class ListOfFiles { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки:"); String dirName = scanner.next(); File dir = new File(dirName); if (!dir.isDirectory()) { System.out.println("Хибне ім\'я теки!"); return; } String[] list = dir.list(); for(String name : list) { System.out.println(name); } } }
На відміну від list()
, функція listFiles()
повертає масив об'єктів типу File
. Це надає додаткові можливості – отримання імен файлів з повним шляхом, перевірки значень атрибутів файлів, окрему роботу з теками тощо. Ці додаткові можливості продемонструємо на такому прикладі:
File[] list = dir.listFiles(); // Виводяться дані про файли в усталеній формі: for(File file : list) { System.out.println(file); } // Виводиться повний шлях: for(File file : list) { System.out.println(file.getCanonicalPath()); } // Виводяться тільки підкаталоги: for(File file : list) { if (file.isDirectory()) System.out.println(file.getCanonicalPath()); }
Для визначення маски-фільтру необхідно створювати об'єкт класу, який реалізує інтерфейс FilenameFilter
. У наведеному нижче прикладі ми отримуємо список файлів та підкаталогів, імена яких починаються з літери 's'
:
String[] list = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().charAt(0) == 's'; } }); for(String name : list) { System.out.println(name); }
Аналогічний параметр типу FilenameFilter
можна застосувати до функції listFiles()
.
2.7 Клонування об'єктів
Іноді виникає необхідність в створенні копії деякого об'єкта, наприклад, для виконання з копією дій, що не порушують даних про оригінал. Просте присвоювання призводить тільки до копіювання посилань. Якщо нам необхідно поелементно скопіювати деякий об'єкт, необхідно використовувати механізм так званого клонування.
У базовому класі java.lang.Object
є функція clone()
, усталене використання якої дозволяє скопіювати об'єкт поелементно. Ця функція також визначена для масивів, рядків і інших стандартних класів. Наприклад, так можна отримати копію існуючого масиву і працювати з цією копією:
package ua.inf.iwanoff.oop.second; import java.util.Arrays; public class ArrayClone { public static void main(String[] args) { int[] a1 = { 1, 2, 3, 4 }; int[] a2 = a1.clone(); // Копія елементів System.out.println(Arrays.toString(a2)); // [1, 2, 3, 4] a1[0] = 10; // змінюємо перший масив System.out.println(Arrays.toString(a1)); // [10, 2, 3, 4] System.out.println(Arrays.toString(a2)); // [1, 2, 3, 4] } }
Для того, щоб можна було клонувати об'єкти користувальницьких класів, ці класи повинні реалізовувати інтерфейс Cloneable
. Цей інтерфейс не оголошує жодного методу. Він всього лише вказує, що об'єкти даного класу можна клонувати. В іншому випадку виклик функції clone()
призведе до генерації винятку типу CloneNotSupportedException
.
Припустимо, нам потрібно клонувати об'єкти класу Human
, що включає два поля типу String
– name
і surname
. Додаємо до опису класу реалізацію інтерфейсу Cloneable
, генеруємо конструктор з двома параметрами, для зручності виведення вмісту полів перекриваємо функцію toString()
. У функції main()
здійснюємо тестування клонування об'єкта:
package ua.inf.iwanoff.oop.second; public class Human implements Cloneable { private String name; private String surname; public Human(String name, String surname) { super(); this.name = name; this.surname = surname; } @Override public String toString() { return name + " " + surname; } public static void main(String[] args) throws CloneNotSupportedException { Human human1 = new Human("John", "Smith"); Human human2 = (Human) human1.clone(); System.out.println(human2); // John Smith human1.name = "Mary"; System.out.println(human1); // Mary Smith System.out.println(human2); // John Smith } }
Як видно з прикладу, після клонування у вихідний об'єкт можна вносити зміни. При цьому копія не зміниться.
Для зручності використання функції clone()
її можна перекрити, змінивши її тип результату і зробивши відкритою. Завдяки наявності цієї функції спроститься клонування (не потрібно буде кожен раз приводити тип):
@Override public Human clone() throws CloneNotSupportedException { return (Human) super.clone(); } . . . Human human2 = human1.clone();
Стандартне клонування, реалізоване в класі java.lang.Object
, дозволяє створювати копії об'єктів, поля яких – типи значення і тип String
(а також класи-обгортки). Якщо поля об'єкта – посилання на масиви або інші типи, необхідно застосовувати так зване "глибоке" клонування. Припустимо, певний клас SomeCloneableClass
містить два поля типу double
масив цілих. "Глибоке" клонування забезпечить створення окремих масивів для різних об'єктів.
package ua.inf.iwanoff.oop.second; import java.util.Arrays; public class SomeCloneableClass implements Cloneable { private double x, y; private int[] a; public SomeCloneableClass(double x, double y, int[] a) { super(); this.x = x; this.y = y; this.a = a; } @Override protected SomeCloneableClass clone() throws CloneNotSupportedException { SomeCloneableClass scc = (SomeCloneableClass) super.clone(); // копіюємо х і y scc.a = a.clone(); // тепер два об'єкти працюють з різними масивами return scc; } @Override public String toString() { return " x=" + x + " y=" + y + " a=" + Arrays.toString(a); } public static void main(String[] args) throws CloneNotSupportedException { SomeCloneableClass scc1 = new SomeCloneableClass(0.1, 0.2, new int[] { 1, 2, 3 }); SomeCloneableClass scc2 = scc1.clone(); scc2.a[2] = 4; System.out.println("scc1:" + scc1); System.out.println("scc2:" + scc2); } }
3 Приклади програм
3.1 Порядкове копіювання текстових файлів
Припустимо, необхідно створити програму, яка здійснює копіювання текстових файлів рядок за рядком. Імена файлів задаються аргументами командного рядка. Текст програми буде таким:
package ua.inf.iwanoff.oop.second; import java.io.*; public class TextFileCopy { public static void main(String[] args) { if (args.length < 2) { System.out.println("Необхідні аргументи!"); return; } try (BufferedReader in = new BufferedReader(new FileReader(args[0])); PrintWriter out = new PrintWriter(new FileWriter(args[1]))) { String line; while ((line = in.readLine()) != null) { out.println(line); } } catch (IOException e) { e.printStackTrace(); } } }
3.2 Сортування дійсних чисел
Припустимо, необхідно реалізувати програму читання з текстового файлу дійсних значень у діапазоні від -1000 до 1000, сортування за збільшенням і за зменшенням модулів та зберігання обох результатів у двох нових текстових файлах. Числа у вихідному файлі розділені пробілами, їх слід читати до кінця файлу.
У класі DoubleNumbers
, який ми проектуємо, створюємо вкладений статичний клас для опису винятку, пов'язаного з хибним дійсним значенням (менше, ніж -1000 або більше, ніж 1000). Крім того, під час роботи функції sortDoubles()
, яка виконує основне завдання, можуть виникати винятки типу IOException
(файл не знайдено, файл не можна створити тощо) та InputMismatchException
(об'єкт типу Scanner
намагається отримати Double
з лексеми, яка не може бути переведена у число). Для сортування за зменшенням модулів створюємо окрему статичну функцію comareByAbsValues()
, у якій створюється локальний клас та повертається його об'єкт. Вихідний код матиме такий вигляд:
package ua.inf.iwanoff.oop.second;
import java.io.*;
import java.util.*;
import static java.lang.Math.*;
public class DoubleNumbers {
/**
* Внутрішній клас-виняток, який дозволяє зберігати хибне дійсне
* значення, прочитане з файлу (менше, ніж -1000 або більше, ніж 1000)
*
*/
public static class DoubleValueException extends Exception {
private double wrongValue;
public DoubleValueException(double wrongValue) {
this.wrongValue = wrongValue;
}
public double getWrongValue() {
return wrongValue;
}
}
/**
* Статична функція, яка визначає метод порівняння дійсних
* чисел під час сортування за зменшенням абсолютної величини
*
* @return об'єкт, який реалізує інтерфейс Comparator
*
*/
public static Comparator<Double> comareByAbsValues() {
// Локальний клас:
class LocalComparator implements Comparator<Double> {
@Override
public int compare(Double d1, Double d2) {
return -Double.compare(abs(d1), abs(d2));
}
}
return new LocalComparator();
}
/**
* Функція здійснює читання дійсних чисел у діапазоні від -1000 до 1000, сортування
* за двома ознаками та занесення у два результуючі файли
*
* @param inFileName - ім'я вихідного файлу
* @param firstOutFileName - ім'я файлу, який міститиме числа, відсортовані
* за зростанням
* @param secondOutFileName - ім'я файлу, який міститиме числа, відсортовані
* за зменшенням абсолютних величин
* @throws DoubleValueException
* @throws IOException
* @throws InputMismatchException
*/
public static void sortDoubles(String inFileName, String firstOutFileName,
String secondOutFileName) throws DoubleValueException, IOException,
InputMismatchException {
Double[] arr = {};
try (BufferedReader reader = new BufferedReader(new FileReader(inFileName));
Scanner scanner = new Scanner(reader)) {
while (scanner.hasNext()) {
double d = scanner.nextDouble();
if (abs(d) > 1000) {
throw new DoubleValueException(d);
}
Double[] arr1 = new Double[arr.length + 1];
System.arraycopy(arr, 0, arr1, 0, arr.length);
arr1[arr.length] = d;
arr = arr1;
}
}
PrintWriter firstWriter = new PrintWriter(new FileWriter(firstOutFileName));
PrintWriter secondWriter = new PrintWriter(new FileWriter(secondOutFileName));
try {
Arrays.sort(arr);
for (Double x : arr)
firstWriter.print(x + " ");
Arrays.sort(arr, comareByAbsValues());
for (Double x : arr)
secondWriter.print(x + " ");
}
// Результуючі файли доцільно закрити у блоці finally:
finally {
firstWriter.close();
secondWriter.close();
}
}
public static void main(String[] args) {
try {
sortDoubles("in.txt", "out1.txt", "out2.txt");
}
// Неправильне дійсне значення:
catch (DoubleValueException e) {
e.printStackTrace();
System.err.println("Wrong value: " + e.getWrongValue());
}
// Помилка, пов'язана з файлами:
catch (IOException e) {
e.printStackTrace();
}
// Файл містить щось, що не є дійсним числом:
catch (InputMismatchException e) {
e.printStackTrace();
}
}
}
Функція hasNext()
повертає true
, якщо за допомогою об'єкта типу Scanner
можна прочитати наступне значення.
3.3 Двійкова серіалізация і десеріалізація даних
Припустимо, необхідно створити класи "Країна" (Country
) і "Континент" (Continent
), створити об'єкт типу Continent
, здійснити його серіалізацию і десеріалізацію. Клас Country
буде таким:
package ua.inf.iwanoff.oop.second; import java.io.Serializable; public class Country implements Serializable { private static final long serialVersionUID = -6755942443306500892L; private String name; private double area; private int population; public Country(String name, double area, int population) { this.name = name; this.area = area; this.population = population; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getArea() { return area; } public void setArea(double area) { this.area = area; } public int getPopulation() { return population; } public void setPopulation(int population) { this.population = population; } }
Клас Continent
може бути таким:
package ua.inf.iwanoff.oop.second; import java.io.Serializable; public class Continent implements Serializable { private static final long serialVersionUID = 8433147861334322335L; private String name; private Country[] countries; public Continent(String name, Country... countries) { this.name = name; this.countries = countries; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Country[] getCountries() { return countries; } public void setCountries(Country[] countries) { this.countries = countries; } }
Наведена нижче програма здійснює створення та серіалізацію об'єкта Continent:
package ua.inf.iwanoff.oop.second; import java.io.*; public class DataSerialization { public static void main(String[] args) { Continent c = new Continent("Європа", new Country("Україна", 603700, 46314736), new Country("Франція", 547030, 61875822), new Country("Німеччина", 357022, 82310000) ); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Countries.dat"))) { out.writeObject(c); } catch (IOException e) { e.printStackTrace(); }; } }
Так можна здійснити десеріалізацію:
package ua.inf.iwanoff.oop.second; import java.io.*; public class DataDeserialization { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("Countries.dat"))) { Continent continent = (Continent) in.readObject(); for (Country c : continent.getCountries()) { System.out.println(c.getName() + " " + c.getArea() + " " + c.getPopulation()); } } catch (IOException e) { e.printStackTrace(); }; } }
3.4 Робота з архівом
Дані про об'єкти з прикладу 3.3 можна зберегти в архіві. Наведена нижче програма здійснює створення об'єкта Continent
і збереження даних в архіві. Кожній країні відповідає своя точка входу ZipEntry
:
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.zip.*; public class StoreToZip { public static void main(String[] args) { Continent continent = new Continent("Європа", new Country("Україна", 603700, 46314736), new Country("Франція", 547030, 61875822), new Country("Німеччина", 357022, 82310000) ); try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Continent.zip")); DataOutputStream out = new DataOutputStream(zOut)) { for (Country country : continent.getCountries()) { ZipEntry zipEntry = new ZipEntry(country.getName()); zOut.putNextEntry(zipEntry); out.writeDouble(country.getArea()); out.writeInt(country.getPopulation()); zOut.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
Так можна здійснити читання з архіву:
package ua.inf.iwanoff.oop.second; import java.io.*; import java.util.zip.*; public class ReadFromZip { public static void main(String[] args) { try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Continent.zip")); DataInputStream in = new DataInputStream(zIn)) { ZipEntry entry; while ((entry = zIn.getNextEntry()) != null) { System.out.println("Країна: " + entry.getName()); System.out.println("Територія: " + in.readDouble()); System.out.println("Населення: " + in.readInt()); System.out.println(); zIn.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
3.5 Використання технології DOM
Припустимо, підготовлений XML-документ з даними про континент (Continent.xml):
<?xml version="1.0" encoding="UTF-8"?> <ContinentData Name="Європа"> <CountriesData> <CountryData Name="Україна" Area="603700" Population="46314736" > <CapitalData Name="Київ" /> </CountryData> <CountryData Name="Франція" Area="547030" Population="61875822" > <CapitalData Name="Москва" /> </CountryData> <CountryData Name="Німеччина" Area="357022" Population="82310000" > <CapitalData Name="Берлін" /> </CountryData> </CountriesData> </ContinentData>
Примітка: помилка зі столицею Франції зроблена навмисне.
Необхідно засобами DOM прочитати дані, виправити помилку і зберегти в новому файлі. Програма матиме такий вигляд:
package ua.inf.iwanoff.oop.second; import java.io.*; import org.w3c.dom.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; public class ContinentWithDOM { public static void main(String[] args) { try { Document doc; DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); doc = db.parse(new File("Continent.xml")); Node rootNode = doc.getDocumentElement(); mainLoop: for (int i = 0; i < rootNode.getChildNodes().getLength(); i++) { Node countriesNode = rootNode.getChildNodes().item(i); if (countriesNode.getNodeName().equals("CountriesData")) { for (int j = 0; j < countriesNode.getChildNodes().getLength(); j++) { Node countryNode = countriesNode.getChildNodes().item(j); if (countryNode.getNodeName().equals("CountryData")) { // Знаходимо атрибут за іменем: if (countryNode.getAttributes().getNamedItem("Name").getNodeValue().equals("Франція")) { for (int k = 0; k < countryNode.getChildNodes().getLength(); k++) { Node capitalNode = countryNode.getChildNodes().item(k); if (capitalNode.getNodeName().equals("CapitalData")) { capitalNode.getAttributes().getNamedItem("Name").setNodeValue("Париж"); break mainLoop; } } } } } } } Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(new File("CorrectedConinent.xml")))); } catch (Exception e) { e.printStackTrace(); } } }
3.6 Класи "Країна" та "Перепис населення"
Припустимо, необхідно доповнити програму роботи з країнами та переписами населення, створену в попередній лабораторній роботі засобами читання і запису даних у текстові файли та XML-документи. Від раніше створених класів можна створити нові похідні класи, один з яких здійснюватиме читання даних з текстового файлу і запис змінених (відсортованих) даних в текстовий файл, а другий забезпечує ці функції працюючи з XML-документами. Найкращим варіантом реалізацї буде не створення нового проекту, а додавання до раніше створеного проекту нового пакету, що дозволить посилатися на раніше створені класи.
Для уніфікації роботи програми нам доцільно створити інтерфейс, який декларує відповідні операції читання і запису. Цей інтерфейс, наприклад, може бути таким:
package ua.inf.iwanoff.oop.second; public interface FileIO { void readFromFile(String fileName) throws Exception; void writeToFile(String fileName) throws Exception; }
Клас TextFileCountry
для роботи з текстовими файлами буде походити від класу CountryWithArray
(реалізований у прикладі попередньої лабораторної роботи) і реалізовувати інтерфейс FileIO
:
package ua.inf.iwanoff.oop.second; import ua.inf.iwanoff.oop.first.AbstractCensus; import ua.inf.iwanoff.oop.first.CensusWithData; import ua.inf.iwanoff.oop.first.CountryWithArray; import java.io.*; import java.util.InputMismatchException; import java.util.Scanner; public class TextFileCountry extends CountryWithArray implements FileIO { @Override public void readFromFile(String fileName) throws FileNotFoundException, InputMismatchException { try (Scanner scanner = new Scanner(new FileReader(fileName))) { setName(scanner.next()); setArea(scanner.nextDouble()); while (scanner.hasNext()) { int year = scanner.nextInt(); int population = scanner.nextInt(); String comments = scanner.nextLine(); addCensus(new CensusWithData(year, population, comments)); } } } @Override public void writeToFile(String fileName) throws IOException { try (PrintWriter out = new PrintWriter(new FileWriter(fileName))) { out.println(getName() + " " + getArea()); for (AbstractCensus census : getCensuses()) { out.print(census.getYear() + " " + census.getPopulation()); out.println(census.getComments()); } } } public static void main(String[] args) { TextFileCountry country = new TextFileCountry(); try { country.readFromFile("Ukraine.txt"); country.testCountry(); country.writeToFile("ByComments.txt"); } catch (FileNotFoundException e) { System.err.println("Read failed"); e.printStackTrace(); } catch (IOException e) { System.err.println("Write failed"); e.printStackTrace(); } catch (InputMismatchException e) { e.printStackTrace(); System.err.println("Wrong format"); } } }
Перед запуском програми необхідно створити файл з вихідними даними. Формат вихідного файлу передбачає, що в першому рядку мстяться дані про назву країни та її територію, а далі в окремих рядках – дані про переписи (рік, кількість населення, коментар). Наприклад можна запропонувати такий вихідний файл (Ukraine.txt
):
Україна 603628 1959 41869000 Перший післявоєнний перепис 1970 47126500 Нас побільшало 1979 49754600 Просто перепис 1989 51706700 Останній радянський перепис 2001 48475100 Перший перепис у незалежній Україні
Цей файл можна створити різними текстовими редакторами і розташувати у кореневій теці проекту. Важливо вказати кодову сторінку UTF-8.
Після виконання функції main()
класу TextFileCountry
у кореневій теці проекту з'явиться файл ByComments.txt
, у якому дані про переписи будуть відсортовані за алфавітом коментарів.
Для реалізації версії програми, яка працює з XML-документами спочатку доцільно розробити документ і його схему. Можна запропонувати, наприклад, такій XML-документ (Ukraine.xml
). Його слід розташувати у кореневій теці проекту:
<?xml version="1.0" encoding="UTF-8"?> <CountryData Name="Україна" Area="603628" > <CensusData Year="1959" Population="41869000" Comments="Перший післявоєнний перепис" /> <CensusData Year="1970" Population="47126500" Comments="Нас побільшало" /> <CensusData Year="1979" Population="49754600" Comments="Просто перепис" /> <CensusData Year="1989" Population="51706700" Comments="Останній радянський перепис" /> <CensusData Year="2001" Population="48475100" Comments="Перший перепис у незалежній Україні" /> </CountryData>
Для генерації класів через технологію JAXB створюємо новий підпакет xml
всередині пакету ua.inf.iwanoff.oop.second
. У цьому пакеті розташовуємо файл схеми документу (Country.xsd
):
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="CountryData"> <xs:complexType> <xs:sequence> <xs:element maxOccurs="unbounded" name="CensusData"> <xs:complexType> <xs:attribute name="Year" type="xs:int" use="required" /> <xs:attribute name="Population" type="xs:int" use="required" /> <xs:attribute name="Comments" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="Name" type="xs:string" use="required" /> <xs:attribute name="Area" type="xs:double" use="required" /> </xs:complexType> </xs:element> </xs:schema>
Примітка: Для проекту слід встановити кодову таблицю UTF-8 через властивості проекту. На жаль, перевизначення кодової таблиці для раніше створеного проекту може призвести до втрати кириличних даних у попередньо написаному програмному коді. В цьому випадку більш коректним є створення нового проекту зі встановленою кодовою таблицею UTF-8 і перенесення необхідного коду через буфер обміну.
Далі генеруємо необхідні класи засобами JAXB. Будуть створені класи ObjectFactory
і CountryData
. Останній клас описує дані про країну відповідно до описаної схеми. Всередині цього класу можна знайти вкладений статичний клас CensusData
. Посилання на цей клас можна використати у новому класі XMLCensus
, який представлятиме окремий перепис для випадку читання даних з XML-файл. Цей клас фактично адаптує CensusData
до вимог ієрархії раніше створених класів. Код класу XMLCensus
буде таким:
package ua.inf.iwanoff.oop.second; import ua.inf.iwanoff.oop.first.AbstractCensus; import ua.inf.iwanoff.oop.second.xml.CountryData; public class XMLCensus extends AbstractCensus { CountryData.CensusData censusData; public XMLCensus(CountryData.CensusData censusData) { this.censusData = censusData; } @Override public int getYear() { return censusData.getYear(); } @Override public void setYear(int year) { censusData.setYear(year); } @Override public int getPopulation() { return censusData.getPopulation(); } @Override public void setPopulation(int population) { censusData.setPopulation(population); } @Override public String getComments() { return censusData.getComments(); } @Override public void setComments(String comments) { censusData.setComments(comments); } }
Починаємо створення класу XMLCountry
. Найбільш цікавим з автоматично згенерованих класів є клас CountryData
. Доцільно описати поле класу XMLCountry
для роботи з даними XML-файлу -
посилання на кореневий елемент:
private CountryData countryData = new CountryData();
Для зберігання даних буде застосована структура об'єктів автоматично згенерованих класів. Ця структура з'являється у пам'яті після читання даних з XML-документу. Доступ до окремих даних здійснюватиметься через методи автоматично згенерованих класів. Весь вихідний текст файлу XMLCountry.java
матиме такий вигляд:
package ua.inf.iwanoff.oop.second; import ua.inf.iwanoff.oop.first.AbstractCensus; import ua.inf.iwanoff.oop.first.AbstractCountry; import ua.inf.iwanoff.oop.second.xml.CountryData; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.util.Collections; import java.util.Comparator; public class XMLCountry extends AbstractCountry implements FileIO { private CountryData countryData = new CountryData(); @Override public String getName() { return countryData.getName(); } @Override public void setName(String name) { countryData.setName(name); } @Override public double getArea() { return countryData.getArea(); } @Override public void setArea(double area) { countryData.setArea(area); } @Override public AbstractCensus getCensus(int i) { return new XMLCensus(countryData.getCensusData().get(i)); } @Override public void setCensus(int i, AbstractCensus census) { countryData.getCensusData().get(i).setYear(census.getYear()); countryData.getCensusData().get(i).setPopulation(census.getPopulation()); countryData.getCensusData().get(i).setComments(census.getComments()); } @Override public boolean addCensus(AbstractCensus census) { CountryData.CensusData censusData = new CountryData.CensusData(); boolean result = countryData.getCensusData().add(censusData); setCensus(censusesCount() - 1, census); return result; } @Override public boolean addCensus(int year, int population, String comments) { CountryData.CensusData censusData = new CountryData.CensusData(); censusData.setYear(year); censusData.setPopulation(population); censusData.setComments(comments); return countryData.getCensusData().add(censusData); } @Override public int censusesCount() { return countryData.getCensusData().size(); } @Override public void clearCensuses() { countryData.getCensusData().clear(); } @Override public void sortByPopulation() { Collections.sort(countryData.getCensusData(), Comparator.comparing(CountryData.CensusData::getPopulation)); } @Override public void sortByComments() { Collections.sort(countryData.getCensusData(), Comparator.comparing(CountryData.CensusData::getComments)); } @Override public AbstractCensus[] getCensuses() { AbstractCensus[] censuses = new AbstractCensus[censusesCount()]; for (int i = 0; i < censusesCount(); i++) { censuses[i] = new XMLCensus(countryData.getCensusData().get(i)); } return censuses; } @Override public void setCensuses(AbstractCensus[] censuses) { clearCensuses(); for (AbstractCensus census : censuses) { addCensus(census); } } @Override public void readFromFile(String fileName) throws JAXBException, FileNotFoundException { JAXBContext jaxbContext = JAXBContext.newInstance("ua.inf.iwanoff.oop.second.xml"); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); countryData = (CountryData) unmarshaller.unmarshal(new FileInputStream(fileName)); } @Override public void writeToFile(String fileName) throws JAXBException, IOException { JAXBContext jaxbContext = JAXBContext.newInstance("ua.inf.iwanoff.oop.second.xml"); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.marshal(countryData, new FileWriter(fileName)); } public CountryData.CensusData getCensusData(int i) { return countryData.getCensusData().get(i); } public static void main(String[] args) { XMLCountry country = new XMLCountry(); try { country.readFromFile("Ukraine.xml"); country.testCountry(); country.writeToFile("ByComments.xml"); } catch (FileNotFoundException e) { System.out.println("Read failed"); e.printStackTrace(); } catch (IOException e) { System.out.println("Write failed"); e.printStackTrace(); } catch (JAXBException e) { e.printStackTrace(); System.out.println("Wrong format"); } } }
Як видно з наведеного вище тексту, для сортування переписів слід здійснити сортування списку об'єктів типу CensusData
у структурі об'єктів, яка була створена під час десеріалізації. Метод Comparator.comparing()
, як і в попередній лабораторній роботі, використовує посилання на методи доступу до даних як параметри.
Після виконання програми в кореневій теці проекту автоматично створюється файл ByComments.xml
, в якому дані про переписи розташовані за зростанням кількості населення.
4 Вправи для контролю
- Прочитати з текстового файлу дійсні значення (до кінця файлу), знайти їх суму та вивести в інший текстовий файл.
- Прочитати з текстового файлу дійсні значення (до кінця файлу), знайти добуток модулів ненульових елементів та вивести в інший текстовий файл.
- Прочитати з текстового файлу цілі значення (до кінця файлу), знайти добуток парних елементів та вивести в інший текстовий файл.
- Прочитати з текстового файлу цілі значення (до кінця файлу), замінити від'ємні значення модулями, додатні нулями та вивести отримані значення в інший текстовий файл.
- Прочитати з текстового файлу цілі значення (до кінця файлу), замінити розділити парні елементи на 2, непарні – збільшити у 2 рази та вивести отримані значення в інший текстовий файл.
- Описати класи Факультет та Інститут (з полем – масивом факультетів). Створити об'єкти, здійснити їх бінарну серіалізацію й десеріалізацію.
- Створити схему документу та XML-документ, який описує дані про користувача. Згенерувати класи за допомогою технології JAXB.
- Створити схему документу та XML-документ, який описує дані про книгу. Згенерувати класи за допомогою технології JAXB.
- Створити схему документу та XML-документ, який описує дані про місто. Згенерувати класи за допомогою технології JAXB.
- Створити схему документу та XML-документ, який описує дані про кінофільм. Згенерувати класи за допомогою технології JAXB.
- Описати класи Факультет та Інститут (з полем – масивом факультетів). Створити об'єкти, здійснити їх серіалізацію й десеріалізацію в XML.
5 Контрольні запитання
- Чи можна використовувати основний результат функції, якщо відбулася генерація винятку?
- Чи можна розмістити виклик функції, що генерує виняток, поза блоком
try
? - У чому призначення функції
printStackTrace()
? - Які додаткові можливості синтаксису перехоплення винятків з'явилися у версії Java 7?
- Чим відрізняються потоки байтів від потоків символів за областю застосування?
- Які класи забезпечують роботу з текстовими файлами і бінарними файлами?
- У чому сенс явного закриття файлів
?
- Чи можна одночасно відкрити кілька потоків введення / виведення?
- Яким чином можна забезпечити автоматичне закриття потоків?
- У чому переваги використання класу
RandomAccessFile
? - Для чого використовують файли даних
DataOutputStream
іDataInputStream
? Які у них переваги і недоліки? - Що таке серіалізація і для чого вона використовується?
- У чому є переваги й недоліки серіалізації?
- Які функції слід визначити для реалізації інтерфейсу
java.io.Serializable
? - Для чого використовують модифікатор
transient
? - Як в Java здійснюється робота з архівами?
- Чи можна створити архів з кількома файлами всередині?
- Для яких цілей використовуються XML-документи?
- Які обмеження накладаються на структуру XML-документу, синтаксис і розташування тегів?
- Чим відрізняються технології SAX і DOM?
- Яким чином здійснюється читання і запис XML-документів?
- Що таке XSLT?
- Чим відрізняється валідний (valid) та правильно оформлений (well-formed) XML-документ?
- Чим відрізняються шаблони документу і схеми документу?
- Чи є шаблон документу XML-документом?
- Чи є схема документу XML-документом?
- Для чого в XML-документах необхідні простори імен?
- Що таке маршалізація і демаршалізація?
- У чому переваги технології зв'язування даних?
- Які є стандартні й нестандартні технології зв'язування даних?
- Які налаштування проекту слід встановити для використання засобів Dali Java Persistence Tools?
- Які класи відповідають специфікації Java Beans?
- Які є недоліки й переваги XML-серіалізації?
- Як визначити поняття "файлова система"?
- Які можна назвати типові функції для роботи з файловою системою?
- Які засоби надає Java для роботи з файловою системою?
- Як отримати атрибути файлу за допомогою засобів класу java.io.File?
- Чим відрізняються функції
list()
іlistFiles()
? - Як здійснити копіювання файлів?
- У чому полягає процес клонування об'єктів?