Лабораторна робота 3

Робота з файлами

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

Спроектувати та реалізувати класи для представлення сутностей попередньої лабораторної роботи. Рішення повинне базуватися на раніше створеній ієрархії класів. Слід створити два похідних класи від класу, який представляє основну сутність. Один клас повинен бути доповненим можливостями читання даних з відповідно підготовленого текстового файлу та запису цих даних в інший файл після сортування. Другий клас повинен реалізовувати читання даних з XML-документу, зберігати дані в структурах, які автоматично створюються за допомогою технології зв'язування даних, та запис даних в інший XML-документ після сортування. Для уникнення дублювання даних у програмі слід також перевизначити клас, який представляє другу сутність. Похідні класи, які представляють основну сутність, повинні реалізовувати спільний інтерфейс, в якому оголошені функції читання з файлу і запису в файл.

Примітка: замість технології зв'язування даних, можна скористатися XML-серіалізацією, наприклад, засобами бібліотеки XStream.

Окрім роботи з файлами повинно бути реалізоване виведення результатів у консольне вікно.

1.2 Сортування цілих

Реалізувати програму читання з текстового файлу цілих додатних значень (числа розділені пробілами, слід читати до кінця файлу), занесення цих чисел у масив, сортування за зменшенням та за збільшенням суми цифр та зберігання обох результатів у двох нових текстових файлах. Перелічені дії реалізувати в окремій статичній функції. Для визначення порядку сортування створити класи, які реалізують інтерфейс Comparator.

1.3 Реалізація серіалізації й десеріалізації

Описати класи Студент і Академічна група (з полем – масивом студентів). Створити об'єкти, здійснити їх бінарну серіалізацію й десеріалізацію, а також серіалізацію й десеріалізацію в XML (стандартними засобами).

1.4 Робота з ZIP-архівом (додаткове завдання)

Описати класи Студент і Академічна група (з полем – масивом студентів). Створити об'єкти, здійснити запис даних про студентів академічної групи в архів. В іншій програмі здійснити читання з архіву.

1.5 Використання технологій 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() генерує виняток у випадку ділення на нуль.

class 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 = new ClassThatImplementsAutoCloseable()) {
    // дії, які можуть призвести до винятку
}
catch (Exception 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.

Середовище IntelliJ IDEA дозволяє автоматизувати процес створення блоків перехоплення та обробки винятків. Якщо в тексті функції помітити блок та застосувати функцію Code | Surround With... | try / catch, помічений блок буде розташовано у блоці перехоплення винятків (try { }), а далі будуть додані catch-блоки, які міститимуть стандартну обробку всіх можливих винятків.

Примітка. Середовище Eclipse також дозволяє автоматизувати процес створення блоків перехоплення та обробки винятків. Це можна зробити за допомогою функції Source | Surround With | Try/catch Block головного меню.

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.third;

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.third;

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.3 Робота з бінарними потоками (потоками байтів)

Для роботи з нетекстовими (бінарними) файлами використовують потоки, імена яких замість "Reader" або "Writer" містять "Stream", наприклад InputStream, FileInputStream, OutputStream, FileOutputStream тощо. Такі потоки мають назву потоків байтів. У наведеному нижче прикладі здійснюється копіювання двійкового файлу FileCopy.class у теку проекту з новим ім'ям:

package ua.inf.iwanoff.oop.third;

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/third/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.third;

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.third;

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.4 Бінарна серіалізація об'єктів

Механізм серіалізації (serialization, розміщення у послідовному порядку) передбачає запис об'єктів у потік бітів для зберігання у файлі або для передачі через комп'ютерні мережі. Десеріалізація передбачає читання потоку бітів, створення збережених об'єктів та відтворення їхнього стану на момент збереження. Для того, щоб об'єкти певного класу можна було серіалізувати, клас повинен реалізовувати інтерфейс java.io.Serializable. Цей інтерфейс не визначає жодного метода, його наявність лише вказує, що об'єкти цього класу можна серіалізувати. Однак гарантована серіалізація і десеріалізація вимагає наявності в таких класах спеціального статичного поля serialVersionUID, яке забезпечує унікальність класу. Середовище Eclipse дозволяє згенерувати необхідне значення автоматично (Quick Fix | Adds a generated serial version ID з контекстного меню).

Класи ObjectOutputStream та ObjectInputStream дозволяють здійснювати серіалізацію та десеріалізацію. Вони реалізують інтерфейси ObjectOutput та ObjectInput відповідно. Механізми серіалізації та десеріалізації розглянемо на наведеному нижче прикладі. Припустимо, описано клас Point:

package ua.inf.iwanoff.oop.third;

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.third;

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.third;

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.third;

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.third;

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.third;

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.5 Робота з архівами

2.5.1 Стандартні засоби для роботи з архівами

Пакет 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.third;

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/third/ZipCreator.java");
            zOut.putNextEntry(zipEntry);
            try (FileInputStream in = new FileInputStream("src/ua/inf/iwanoff/oop/third/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.third;

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.2 Створення і використання JAR-файлів

Для розгортання Java-застосунку на комп'ютері клієнта достатньо скопіювати всі необхідні файли .class в необхідних теках відповідних пакетів. Ідеологія Java не дозволяє створювати файли, що безпосередньо виконуються (.exe). Однак файли, необхідні для виконання програми, можна упакувати в архів спеціального типу – так званий JAR. За своїм форматом це фактично zip-архів, але він містить файл MANIFEST.MF в каталозі META-INF, в якому повинен бути зазначений головний клас програми (такий клас повинен містити метод main()). Цей клас задається параметром Main-Class. Номер версії JAR задається параметром Manifest-Version.

Програму, зібрану в JAR-файл можна запустити на виконання такою командою:

java -jar ім'я_файлу.jar

Застосунки графічного інтерфейсу користувача слід запускати командою:

javaw -jar ім'я_файлу.jar

У цьому випадку не буде створюватися консольне вікно.

Якщо засоби JDK були встановлені на комп'ютері і правильно визначені всі шляхи, запустити застосунок можна подібно до того, як в операційній системі запускаються виконуються файли (дворазовим натисканням миші на JAR-файлі).

Для того, щоб створити JAR-файл у середовищі IntelliJ IDEA, спочатку слід виконати певні налаштування. Зокрема, у головному меню вибираємо File | Project Structure | Project Settings | Artifacts, далі слід натиснути кнопку "+" і додати Jar | From modules with dependencies.... Далі в діалоговому вікні вказуємо модуль (Module, зазвичай заповнюється автоматично) і головний клас (Main Class, клас з функцією main(), яка цікавить нас як точка старту застосунку). Найкращий варіант вибору головного класу – скористатися піктограмою вибору файлів (Browse...). Наступним кроком є безпосереднє створення архіву. У головному меню вибираємо Build | Build Artifacts..., далі Action | Build. У дереві тек проекту в піддереві out з'являється гілка artifacts, яка містить створений JAR-файл.

2.6 Використання мови XML

2.6.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.6.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.6.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.third;

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.6.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.third;

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.6.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.6.6 Технологія зв'язування даних

Використання мови Java передбачає зручний спосіб роботи з XML-файлами – механізм зв'язування даних. Цей механізм передбачає генерацію набору класів, які описують елементи файлу, та створення відповідної структури об'єктів у пам'яті.

Засіб зв'язування даних XML містить компілятор схеми, що транслює схему в набір специфічних для схеми класів з відповідними методами доступу і зміни (тобто get і set). Він також містить механізм маршалізації (запису структурованих даних в XML-документ), підтримує демаршалізацію XML документів у відповідну структуру взаємозалежних екземплярів. Автоматично створеною структурою даних можна користуватися без ручного розміщення даних у списках або масивах.

Традиційно першою технологією зв'язування даних була технологія Castor. Пізніше був стандартизований API JAXB (Java Architecture for XML Binding). Версія 2 специфікації JAXB припускає як генерацію класів за схемою, так і генерацію схеми за існуючою структурою класів.

Технологія зв'язування даних найбільш часто застосовується для генерації класів за наявною схемою.

Для підтримки стандартних технологій 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$" 

Примітка. Починаючи з JDK 9 утиліту xjc.exe виключено з набору засобів JDK. Найкращий спосіб подолати цю проблему – використання плагіну Maven cxf XJC.

Для того, щоб створена команда працювала коректно, файл схеми слід розташувати у новому пакеті, в якому потім з'являться згенеровані файли.

Примітка: паралельні налаштування середовища Eclipse тут і далі не розглядатимуться.

Розглянемо такий приклад. Створюємо новий пакет, або визначаємо існуючий пакет для розташування класів, код яких буде пізніше згенеровано.

В теці проекту створюємо 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>

Здійснюємо генерацію класів. У дереві проекту (у відповідному пакеті) з'являються файли 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.third;

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.third"); // пакет з необхідними класами
            // Зчитуємо дані з файлу й завантажуємо в об'єкт згенерованого класу:
            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-документа.

2.6.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.third;

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.third;

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.third;

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.oop.third.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.third;

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-серіалізації. Одна з найбільш поширених бібліотек – XStream. Ця бібліотека, яка вільно розповсюджується, дозволяє дуже легко серіалізувати та десеріалізувати файли XML і JSON. Для роботи з цією бібліотекою достатньо завантажити необхідні JAR-файли. Але більш зручний і сучасний підхід забезпечує використання Maven для підключення бібліотеки. Відповідний приклад буде розглянуто нижче.

2.7 Використання засобів автоматизації складання проектів

2.7.1 Системи автоматизації складання

Під автоматизацією складання (build automation, автоматизация сборки) розуміють використання спеціальних засобів для автоматичного відстеження взаємозалежностей між файлами в рамках проекту. Автоматизація складання також передбачає виконання типових дій, таких як, наприклад,

  • компіляція сирцевого коду;
  • збирання програми з окремих частин;
  • підготовка супровідної документації;
  • створення jar-архіву;
  • розгортання програми.

Інтегровані середовища розробки (IDE) найчастіше беруть на себе управління складанням проекту. Однак ці засоби, як правило, обмежені і не сумісні в різних IDE. Разом з тим, потреба в перенесенні проекту в іншу IDE виникає досить часто. Крім того, зручно було б описати і зафіксувати послідовність деяких дій над артефактами проекту під час виконання типового набору завдань процесу розробки.

Альтернативу пропонують незалежні засоби автоматизації збирання. Зараз серед найбільш популярних засобів автоматизації збирання можна назвати Apache Ant, Gradle і Apache Maven.

Apache Ant (ant – мураха) – заснований на Java сумісний із різними платформами набір засобів для автоматизації процесу складання програмного продукту (проект організації Apache Software Foundation). Управління процесом складання відбувається за допомогою XML-сценарію – так званого build-файлу (усталено називається build.xml), який відповідає певним правилам. Дії, які можна виконувати за допомогою Ant, описуються цілями (targets). Цілі можуть залежати одна від одної. Якщо до виконання певної мети повинна бути виконана інша мета, то можна визачити залежність однієї мети від іншої. Цілі містять виклики команд-завдань (завдань) Tasks. Кожне завдання є командою, що виконує деяку елементарну дію. Існує декілька наперед визначених завдань, які призначені для опису типових дій: компіляція з допомогою javac, запуск програми, створення jar, розгортання тощо. Існує можливість самостійного розширення множини завдань Ant. Завдання Ant включають роботу з файловою системою (створення каталогів, копіювання і видалення файлів), виклик компілятора, створення jar-архіву, виконання Java-застосунку, генерацію документації тощо.

На сьогодні засоби Ant стали менш популярні, порівняно з Gradle і Maven через їх обмеженість. Крім того, в порівнянні з Maven, Ant пропонує імперативний (командний) підхід до складання проекту: розробник повинен описати послідовність дій, які виконуються під час збирання, а не очікуваний результат.

Засіб автоматизації збирання Gradle вперше створено в 2007 році під ліцензією Apache License 2.0. У вересні 2012 року вийшла стабільна реалізація 2.7. Gradle використовує концепції Apache Ant and Apache Maven, але замість XML використовує мову, побудовану на синтаксисі мови Groovy. Засоби Gradle використовуються переважно в Android-розробці.

2.7.2 Apache Maven

Apache Maven – це набір управління складанням проектів, який використовує синтаксис XML для специфікації опцій складання, але у порівнянні з Ant забезпечує більш високий рівень атоматизації. Maven створюється і публікується Apache Software Foundation починаючи з 2004 р. Для визначення опцій складання використовують побудовану на XML мову POM (Project Object Model). На відміну від Apache Ant, Maven, забезпечує декларативне, а не імперативне складання проекту в файлах проекту pom.xml міститься його декларативний опис (що ми хочемо отримати), а не окремі команди.

Як і Ant, Maven дозволяє здійснити запуск процесів компіляції, створення jar-файлів, створення дистрибутива програми, генерації документації тощо.

Найважливішою функцією Maven є управління залежностями, які присутні в проектах, що використовують сторонні бібліотеки (які, в свою чергу, використовують інші сторонні бібліотеки). Також Maven дозволяє вирішувати конфлікти версій бібліотек.

Maven базується на Plugin-архітектурі, яка дозволяє застосовувати плагіни для різних завдань (compile, test, build, deploy, checkstyle, pmd, scp-transfer) без необхідності інсталювати їх в конкретний проект. Існує велика кількість готових плагінів.

Інформація для проекту, що підтримується Maven, міститься у файлі pom.xml, в якому задаються залежності пакета, керованого Maven, від інших пакетів і бібліотек.

Середовище IntelliJ IDEA усталено містить у собі підтримку роботи з Maven-проектами. Для створення нового проекту з підтримкою Maven у вікні New Project слід у лівій частині вибрати Maven. Для проекту визначається версія JDK (Project SDK). Припустимо, це JDK 11. Крім того, проект можна створити, базуючись на архетипі (archetype) – готовму шаблоні проекту. Для першого проекту Maven можна обійтись без архетипів.

На наступній сторінці майстра вибираємо ім'я (Name, наприклад, HelloMaven), місце розташування (Location) і координати артефакту (Artifact Coordinates), в які входить так звана "трійка" GAV (groupId, artifactId, version).

  • groupId – посилання на автора або організацію (підрозділ), де створено проект; відповідний ідентифікатор будують за правилами побудови імен пакетів – інвертоване доменне ім'я;
  • artifactId – назва проекту; вона не обов'язково повинна збігатися з ім'ям проекту IntelliJ IDEA, але використання однакових імен в цьому контексті є бажаним; під час створення проекту це поле автоматично заповнюється ім'ям проекту;
  • version – версія проекту; усталено визанчається 1.0-SNAPSHOT, тобто це перша версія проекту, який знаходиться у стані розробки; для нового проекту – це 1.0-SNAPSHOT; це означає що проект знаходиться у стадії розробки.

Для нашого першого проекту IntelliJ IDEA автоматично створює файл pom.xml з таким вмістом:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ua.inf.iwanoff.oop.third</groupId>
    <artifactId>HelloMaven</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

</project>

У розділі properties вказується версія JDK (JDK 11).

Слід також звернути увагу на структуру проекту. Це типова структура проектів Maven. На рівні проекту створюється тека src з такою структурою:

src
    main
        java
        resources
    test
        java

Каталог src – це кореневий каталог сирцевого коду і коду тестових класів. Каталог main – це кореневий каталог для сирцевого коду, пов'язаного безпосередньо з програмою (без тестів). Каталог test містить сирцевий код тесту. Безпосередньо пакети сирцевого коду рзташовують у підкаталогах java. Каталог resources містить інші ресурси, необхідні для проекту. Це можуть бути файли властивостей (properties), які використовують для інтернаціоналізації програми, файли розмічення вікон графічного інтерфейсу користувача, стилів, або щось інше.

Після компіляції проекту до структури тек на рівні проекту буде додано каталог target зі скомпільованими класами.

Слід звернути увагу на інструментальне вікно Maven, ярлик якого зазвичай розташований праворуч. Якщо розкрити це вікно, можна побачити список команд Maven, які забезпечують життєвий цикл проекту:

  • clean – очищення проекту і видалення всіх файлів, які були створені попереднім складанням;
  • validate – перевірка коректності метаінформації про проект;
  • compile – компіляція проекту;
  • test – тестування за допомогою JUnit;
  • package – створення архіву jar, war або ear;
  • verify – перевірка коректності пакета і відповідності вимогам якості;
  • install – інсталяція (копіювання) файлів .jar, war або ear в локальний репозиторій;
  • site – генерація сайту;
  • deploy – публікація файлів в віддалений репозиторій.

Примітка: якщо використовувати Maven поза IntelliJ IDEA, ці команди вводяться у командному рядку, наприклад: mvn clean; для використання Maven без IntelliJ IDEA, його слід завантажити і встановити окремо.

Деякі команди для свого успішного виконання вимагають виконання попередніх команд життєвого циклу. Наприклад, package передбачає виконання compile. Виконання попередніх команд здійснюється автоматично. Виконання команд передбачає виведення в консольному вікні характерних для Maven повідомлень.

У каталозі java створюємо пакет і клас, на якому здійснюватиметься демонстрація роботи з Maven. Наприклад, клас має такий сирцевий код:

package ua.inf.iwanoff.oop.third;

public class MavenDemo {
    public static int multiply(int i, int k) {
        return i * k;
    }

    public static void main(String[] args) {
        System.out.println("Hello, Maven!");
        System.out.println("2 * 2 = " + multiply(2, 2));
    }
}

Серед команд Maven немає безпосереднього виконання програми. Для того, щоб завантажити програму на виконання, слід скористатися засобами IntelliJ IDEA (через меню Run). Але при цьому автоматично виконуються необхідні команди Maven, які покривають певні фази життєвого циклу.

Примітка: набір стандартних команд Maven може бути розширений за допомогою механізма плагінів.

Дуже важлива функція Maven – управління залежностями. Зазвичай реальний проект окрім API, яке надає JDK, використовує численні бібліотеки, для підключення яких необхідно завантажувати JAR-файли. Ці бібліотеки базуються на використанні інших бібліотек, які теж треба завантажувати тощо. Окрема проблема виникає з версіями бібліотек та їх сумісністю.

Maven надає простий декларативний підхід до управління залежностями. Достатньо додати відомості про необхідну бібліотеку в розділі <dependencies>. Наприклад, для тестування коду нашого проекту доцільно додати можливість використовувати JUnit 5. Можна, звичайно додати необхідну залежність вручну, але краще скористатися інтерактивними можливостями IntelliJ IDEA. Вибравши у вікні редактору файл pom.xml, слід натиснути Alt+Insert, далі у списку Generate вибрати Dependency, потім у діалоговому вікні у рядку Search For Artifact набрати junit і вибрати org.junit.jupiter:junit-jupiter-api:5.8.1. До тексту файлу буде додано групу <dependencies>:

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.8.1</version>
        </dependency>
    </dependencies>

У цьому коді додані дані помічені як помилка. Необхідно перезавантажити проект Maven. В інструментальному вікні Maven знаходимо першу кнопку (Reload All Maven Projects). Помилки в pom.xml повинні зникнути.

Тепер можна додати тест. Можна використати функцію контекстного меню Generate... | Test.... Паралельну ієрархію пакетів, а також необхідний клас буде додано до гілки test проекту.

2.8 Робота з системним журналом

2.8.1 Загальні відомості про логування

Робота з системним журналом (логування) використовується для реєстрації в спціальном файлі (як правило, текстововому) протоколу подій, які відбуваються під час роботи програми. Це, наприклад, трасування виконання конструкторів, методів, блоків обробки критичних ситуацій та інші повідомлення, пов'язані зі звевадженням.

Логгер (logger) являє собою точку входу в систему логування. Кожен логгер можна уявити як іменований канал для повідомлень, в який вони відправляються для обробки.

Важливим поняттям логування є рівень логування (log level), що визначає відносну важливість протоколюються повідомлень. Коли повідомлення передається в логгер, рівень логування повідомлення порівнюється з рівнем логування логгеру. Якщо рівень логування повідомлення вище або дорівнює рівню логування логгеру, повідомлення буде оброблено, в іншому випадку – проігноровано.

2.8.2 Стандартні засоби Java для логування

Стандартні засоби пакету java.util.logging надають можливість протоколювання подій. Є такі рівні логгірованія за зростанням: FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, а також ALL і OFF, що вмикає та вимикає всі рівні відповідно. Для створення логеру слід використовувати статичні методи класу java.util.logging.Logger. Наприклад:

Logger log = Logger.getLogger("MyLog");
log.setLevel(Level.ALL);

Ім'я журналу визначають довільно. Тепер у створений таким чином журнал можна писати дані, зокрема, повідомлення:

log.log(Level.INFO, "Все OK"); // виведення на консоль

Якщо ми хочемо, щоб повідомлення також заносилися в файл, слід скористатися класом java.util.logging.FileHandler:

FileHandler fileHandler = new FileHandler("C:/MyFile.log");
log.addHandler(fileHandler);
log.log(Level.INFO, "Все OK"); // виведення на консоль і у файл

Примітка: використання запису журналу в файл передбачає перехоплення винятку java.io.IOException.

У наведеному нижче прикладі створюється об'єкт-журнал, в який заноситимуться повідомлення всіх рівнів. Одночасно з виведенням на консоль здійснюється запис у визначений файл:

package ua.inf.iwanoff.oop.third;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LogDemo {
    public static void main(String[] args) throws IOException {
        Logger log = Logger.getLogger("MyLog");
        log.setLevel(Level.ALL);
        FileHandler fileHandler = new FileHandler("C:/MyFile.log");
        log.addHandler(fileHandler);
        log.log(Level.INFO, "Все OK"); // виведення на консоль і у файл
    }
}

Для конфігурації стандартних засобів логування використовують спеціальний файл властивостей (файл з розширенням .properties). Зокрема, можна окремо задавати параметри логування для виведення на консоль і в файл.

2.8.3 Використання бібліотеки Log4j

Є істотні недоліки стандартних інструментів логування (java.util.logging). Це труднощі налаштування, низька ефективність, обмежені можливості журналювання, недостатньо інтуїтивна конфігурація. Ці недоліки стимулювали незалежний розвиток альтернативних бібліотек логування.

Apache Log4j 2 - бібліотека логування (протоколювання) програм Java, яка фактично стала промисловим стандартом. Вона забезпечує значні покращення у порівнянні зі своїм попередником Log4j 1. З 2015 р версія Log4j 1 не рекомендується до використання.

Зараз актуальною є версія 2.14. API Log4j можно загрузить по адресу: https://logging.apache.org/log4j/2.x/.

Для того, щоб скористатися можливостями бібліотеки Log4j 2, можна в середовищі IntelliJ IDEA створити новий проект з підтримкою Maven, наприклад, log4j-test. До файлу pom.xml додаємо залежності:

<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.14.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.1</version>
    </dependency>
</dependencies>

Після перезавантаження проекту (кнопка Reload All Maven Projects) у проекті можна користатися Log4j 2.

Створюємо клас з функцією main(). Для здійснення логування у програмі слід створити об'єкт класу org.apache.logging.log4j.Logger. Цей об'єкт дозволяє здійснювати запис в журнал повідомлення відповідно до встановленого рівня.

package ua.in.iwanoff.oop.third;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class HelloLog4J {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(HelloLog4J.class);
        logger.fatal("Hello, Log4j!");
    }
}

Повідомленню "Hello, Log4j!" передує інформація про дату та час, функцію і клас.

Параметри логування зберігаються в спеціальному файлі конфігурації. Оскільки конфігурацію логування поки не визначено (немає відповідного файлу), працює усталена (default) конфігурація, згідно з якою виводяться тільки повідомлення рівня error та fatal. Рівень логування fatal, який використано для виведення повідомлення, має найвищий приоритет. Усталено всі повідомлення виводяться на консоль.

Для того, щоб змінити політику логування, слід створити файл конфігурації. Його ім'я – log4j2.xml. Такий файл слід створити у теці java\src\resources. Його вміст в найпростішому випадку буде таким:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT"/>
        <File name="FileAppender" fileName="hello-app-${date:yyyyMMdd}.log" immediateFlush="false" append="true"/>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="ConsoleAppender" />
            <AppenderRef ref="FileAppender"/>
        </Root>
    </Loggers>
</Configuration>

У файлі присутня група <Appenders>, в якій вказано, що виведення здійснюється на консоль і в файл, ім'я якого будується з "hello-app" і поточної дати. У групі <Loggers> вказані рівні виведення з напрямками виведення. В нашому випадку це "debug".

Log4J підтримує такі рівні виведення, в порядку зростання пріоритету:

trace – трасування всіх повідомлень в зазначений апендер
debug - детальна інформация для зневадження
info  - інформация
warn  - попередження
error - помилка
fatal - фатальна помилка - у цього повідомлення найвищий пріоритет.

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

Оскільки усталена конфігурація більше не працює, з повідомлень зникла інформація про дату та час, функцію і клас. Її можна відтворити змінивши вміст файлу log4j2.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %logger{36} - %msg%n" />
        </Console>
        <File name="FileAppender" fileName="hello-app-${date:yyyyMMdd}.log" immediateFlush="false" append="true">
            <PatternLayout pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %logger{36} - %msg%n"/>
        </File>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="ConsoleAppender" />
            <AppenderRef ref="FileAppender"/>
        </Root>
    </Loggers>
</Configuration>

Окрім формату XML, конфігураційний файл можна створювати у форматах JSON, YAML, або PROPERTIES.

3 Приклади програм

3.1 Порядкове копіювання текстових файлів

Припустимо, необхідно створити програму, яка здійснює копіювання текстових файлів рядок за рядком. Імена файлів задаються аргументами командного рядка. Текст програми буде таким:

package ua.inf.iwanoff.oop.third;

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.third;

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.third;

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.third;

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.third;

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.third;

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.third;

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.third;

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.third;

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-серіалізація за допомогою бібліотеки XStream

Припустимо, необхідно здійснити серіалізацію і десеріалізацію даних про лінію, яка описується двома точками. Створюємо новий Maven-проект XStreamTest. До файлу pom.xml додаємо залежність від бібліотеки xstream. Отримаємо такий файл pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ua.inf.iwanoff.oop.third</groupId>
    <artifactId>XStreamTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.18</version>
        </dependency>
    </dependencies>

</project>

Створюємо класи Line і Point. Ці класи не мають конструкторів без параметрів і публічних влистивостей, тому вони не можуть бути серіалізовані за допомогою java.beans.XMLEncoder і java.beans.XMLDecoder. Але XStream дозволяє їх серіалізувати, оскільки ця бібліотека орієнтується на поля, а не на властивості. Клас Point:

package ua.inf.iwanoff.oop.third;

public class Point {
    private double x, y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return x + " " + y;
    }
}

Клас Line:

package ua.inf.iwanoff.oop.third;

public class Line {
    private Point first, second;

    public Line(double firstX, double firstY, double secondX, double secondY) {
        first = new Point(firstX, firstY);
        second = new Point(secondX, secondY);
    }

    @Override
    public String toString() {
        return first + " " + second;
    }
}

Для запису в файл можна створити такий клас:

package ua.inf.iwanoff.oop.third;

import com.thoughtworks.xstream.XStream;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class XStreamSerialization {
    public static void main(String[] args) {
        XStream xStream = new XStream();
        Line line = new Line(1, 2, 3, 4);
        xStream.alias("line", Line.class);
        String xml = xStream.toXML(line);
        try (FileWriter fw = new FileWriter("Line.xml"); PrintWriter out = new PrintWriter(fw)) {
            out.println(xml);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Отримуємо XML-файл:

<line>
  <first>
    <x>1.0</x>
    <y>2.0</y>
  </first>
  <second>
    <x>3.0</x>
    <y>4.0</y>
  </second>
</line>

Примітка: якщо не використовувати аліас, кореневий тег буде таким: <ua.inf.iwanoff.oop.third.Line>

В іншій програмі здійснюємо читання:

package ua.inf.iwanoff.oop.third;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.security.AnyTypePermission;

import java.io.File;

public class XStreamDeserialization {
    public static void main(String[] args) {
        XStream xStream = new XStream();
        xStream.addPermission(AnyTypePermission.ANY);
        xStream.alias("line", Line.class);
        Line newLine = (Line) xStream.fromXML(new File("Line.xml"));
        System.out.println(newLine);
    }
}

Бібліотека надає також інші способи роботи з файлами.

3.7 Класи "Країна" та "Перепис населення"

Припустимо, необхідно доповнити програму роботи з країнами та переписами населення, створену в попередній лабораторній роботі засобами читання і запису даних у текстові файли та XML-документи. Від раніше створених класів можна створити нові похідні класи, один з яких здійснюватиме читання даних з текстового файлу і запис змінених (відсортованих) даних в текстовий файл, а другий забезпечує ці функції працюючи з XML-документами. Найкращим варіантом реалізацї буде не створення нового проекту, а додавання до раніше створеного проекту нового пакету, що дозволить посилатися на раніше створені класи.

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

package ua.inf.iwanoff.oop.third;

public interface FileIO {
    void readFromFile(String fileName) throws Exception;
    void writeToFile(String fileName) throws Exception;
}

Клас TextFileCountry для роботи з текстовими файлами буде походити від класу CountryWithArray (реалізований у прикладі попередньої лабораторної роботи) і реалізовувати інтерфейс FileIO:

package ua.inf.iwanoff.oop.third;

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.third. У цьому пакеті розташовуємо файл схеми документу (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.third;

import ua.inf.iwanoff.oop.first.AbstractCensus;
import ua.inf.iwanoff.oop.third.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.third;

import ua.inf.iwanoff.oop.first.AbstractCensus;
import ua.inf.iwanoff.oop.first.AbstractCountry;
import ua.inf.iwanoff.oop.third.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.third.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.third.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 Вправи для контролю

  1. Прочитати з текстового файлу дійсні значення (до кінця файлу), знайти їх суму та вивести в інший текстовий файл.
  2. Прочитати з текстового файлу дійсні значення (до кінця файлу), знайти добуток модулів ненульових елементів та вивести в інший текстовий файл.
  3. Прочитати з текстового файлу цілі значення (до кінця файлу), знайти добуток парних елементів та вивести в інший текстовий файл.
  4. Прочитати з текстового файлу цілі значення (до кінця файлу), замінити від'ємні значення модулями, додатні нулями та вивести отримані значення в інший текстовий файл.
  5. Прочитати з текстового файлу цілі значення (до кінця файлу), замінити розділити парні елементи на 2, непарні – збільшити у 2 рази та вивести отримані значення в інший текстовий файл.
  6. Описати класи Факультет та Інститут (з полем – масивом факультетів). Створити об'єкти, здійснити їх бінарну серіалізацію й десеріалізацію.
  7. Створити схему документу та XML-документ, який описує дані про користувача. Згенерувати класи за допомогою технології JAXB.
  8. Створити схему документу та XML-документ, який описує дані про книгу. Згенерувати класи за допомогою технології JAXB.
  9. Створити схему документу та XML-документ, який описує дані про місто. Згенерувати класи за допомогою технології JAXB.
  10. Створити схему документу та XML-документ, який описує дані про кінофільм. Згенерувати класи за допомогою технології JAXB.
  11. Описати класи Факультет та Інститут (з полем – масивом факультетів). Створити об'єкти, здійснити їх серіалізацію й десеріалізацію в XML.

5 Контрольні запитання

  1. Чи можна використовувати основний результат функції, якщо відбулася генерація винятку?
  2. Чи можна розмістити виклик функції, що генерує виняток, поза блоком try?
  3. У чому призначення функції printStackTrace()?
  4. Які додаткові можливості синтаксису перехоплення винятків з'явилися у версії Java 7?
  5. Чим відрізняються потоки байтів від потоків символів за областю застосування?
  6. Які класи забезпечують роботу з текстовими файлами і бінарними файлами?
  7. У чому сенс явного закриття файлів?
  8. Чи можна одночасно відкрити кілька потоків введення / виведення?
  9. Яким чином можна забезпечити автоматичне закриття потоків?
  10. У чому переваги використання класу RandomAccessFile?
  11. Для чого використовують файли даних DataOutputStream і DataInputStream? Які у них переваги і недоліки?
  12. Що таке серіалізація і для чого вона використовується?
  13. У чому є переваги й недоліки серіалізації?
  14. Які функції слід визначити для реалізації інтерфейсу java.io.Serializable?
  15. Для чого використовують модифікатор transient?
  16. Як в Java здійснюється робота з архівами?
  17. Для яких цілей використовуються XML-документи?
  18. Які обмеження накладаються на структуру XML-документу, синтаксис і розташування тегів?
  19. Чим відрізняються технології SAX і DOM?
  20. Яким чином здійснюється читання і запис XML-документів?
  21. Що таке XSLT?
  22. Чим відрізняється валідний (valid) та правильно оформлений (well-formed) XML-документ?
  23. Чим відрізняються шаблони документу і схеми документу?
  24. Чи є шаблон документу XML-документом?
  25. Чи є схема документу XML-документом?
  26. Для чого в XML-документах необхідні простори імен?
  27. Що таке маршалізація і демаршалізація?
  28. У чому переваги технології зв'язування даних?
  29. Які є стандартні й нестандартні технології зв'язування даних?
  30. Які класи відповідають специфікації Java Beans?
  31. Які є недоліки й переваги XML-серіалізації?

 

up