Лабораторна робота 5

Робота з винятками та файлами в Java

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

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

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

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

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

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

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

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

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

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-блоки, які міститимуть стандартну обробку всіх можливих винятків.

2.2 Потоки введення та виведення. Потоки символів

2.2.1 Загальні концепції

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

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

Уся робота з потоками, окрім стандартних потоків System.in і System.out, повинна передбачати перехоплення винятків, пов'язаних з введенням-виведенням. Це IOException та його нащадки – FileNotFoundException, ObjectStreamException та інші.

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

in.close();

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

У програмі можна одночасно відкрити декілька потоків введення і декілька потоків виведення.

2.2.2 Робота з потоками символів

Потоки, призначені для роботи з текстовою інформацією, мають назву потоків символів. Імена класів таких потоків закінчуються відповідно словами "...Reader" і "...Writer". Безпосередню роботу з текстовими файлами здійснюють об'єкти класів FileReader та FileWriter.

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

Для буферизованого виведення застосовують об'єкти класу BufferedWriter. Безпосереднє форматоване виведення здійснюється методами print() та println() об'єкту класу PrintWriter.

У наведеному нижче прикладі з файлу з ім'ям data.txt здійснюється читання одного цілого й одного дійсного значення, їхня сума записується у файл results.txt.

package ua.inf.iwanoff.java.fifth;

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.java.fifth;

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.java.fifth;

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/java/fifth/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.java.fifth;

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.java.fifth;

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, яке забезпечує унікальність класу.

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

package ua.inf.iwanoff.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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

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-файл.

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

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

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

package ua.inf.iwanoff.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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.java.fifth;

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 Класи "Країна" та "Перепис населення"

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

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

package ua.inf.iwanoff.java.fifth;

import ua.inf.iwanoff.java.second.AbstractCountry;
import ua.inf.iwanoff.java.third.CountryWithList;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.InputMismatchException;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Клас розширений можливостями файлового читання з запису даних
 */
public abstract class CountryWithFile extends CountryWithList {
    /**
     * Здійснює читання даних про країну та переписи зі вказаного файлу
     *
     * @param fileName ім'я файлу
     * @throws Exception
     */
    public abstract void readFromFile(String fileName) throws Exception;

    /**
     * Здійснює запис даних про країну та переписи в указаний файл
     *
     * @param fileName ім'я файлу
     * @throws Exception
     */
    public abstract void writeToFile(String fileName) throws Exception;

    /**
     * Здійснює тестування методів класу
     */
    public void testCountry(String from, String toByPopulation, String toByComments) {
        createCountry();
        try {
            writeToFile(from);
        }
        catch (IOException e) {
            System.err.println("Write failed");
            e.printStackTrace();
        }
        catch (Exception e) {
            e.printStackTrace();
            System.err.println("Wrong format");
        }
        testCountry();
        clearCensuses();
        try {
            readFromFile(from);
            sortByPopulation();
            writeToFile(toByPopulation);
            sortByComments();
            writeToFile(toByComments);
        }
        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");
        }
        catch (Exception e) {
            e.printStackTrace();
            System.err.println("Wrong format");
        }
        testCountry();
    }
}

Клас CountryWithTextFile для роботи з текстовими файлами буде таким:

package ua.inf.iwanoff.java.fifth;

import ua.inf.iwanoff.java.second.Census;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.Scanner;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Клас розширений можливостями роботи з текстовими файлами
 */
public class CountryWithTextFile extends CountryWithFile {
    /**
     * Здійснює читання даних про країну та переписи зі вказаного файлу
     *
     * @param fileName ім'я файлу
     * @throws Exception
     */
    @Override
    public void readFromFile(String fileName) throws Exception {
        clearCensuses();
        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();
                comments = comments.trim();
                addCensus(new Census(year, population, comments));
            }
        }
    }

    /**
     * Здійснює запис даних про країну та переписи в указаний файл
     *
     * @param fileName ім'я файлу
     * @throws Exception
     */
    @Override
    public void writeToFile(String fileName) throws Exception {
        try (PrintWriter out = new PrintWriter(new FileWriter(fileName))) {
            out.println(getName() + " " + getArea());
            for (Census census : getCensuses()) {
                out.print(census.getYear() + " " + census.getPopulation() + " ");
                out.println(census.getComments());
            }
        }
    }

    /**
     * Демонстрація роботи програми
     * @param args аргументи командного рядка (не використовуються)
     */
    public static void main(String[] args) {
        new CountryWithTextFile().testCountry("Ukraine.txt", "ByPopulation.txt", "ByComments.txt");
    }
}

Після виконання функції main() класу CountryWithTextFile у кореневій теці проєкту з'являються текстові файли, які містять результати сортувань.

Клас, який працює з файлами даних, буде таким:

package ua.inf.iwanoff.java.fifth;

import ua.inf.iwanoff.java.second.Census;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Клас розширений можливостями роботи з бінарними файлами даних
 */
public class CountryWithDataFile extends CountryWithFile {
    /**
     * Здійснює читання даних про країну та переписи зі вказаного файлу
     *
     * @param fileName ім'я файлу
     * @throws Exception
     */
    @Override
    public void readFromFile(String fileName) throws Exception {
        clearCensuses();
        try (DataInputStream in = new DataInputStream(
                new FileInputStream(fileName))) {
            setName(in.readUTF());
            setArea(in.readDouble());
            int count = in.readInt();
            for (int i = 0; i < count; i++) {
                int year = in.readInt();
                int population = in.readInt();
                String comments = in.readUTF();
                addCensus(new Census(year, population, comments));
            }
        }
    }

    /**
     * Здійснює запис даних про країну та переписи в указаний файл
     *
     * @param fileName ім'я файлу
     * @throws Exception
     */
    @Override
    public void writeToFile(String fileName) throws Exception {
        try (DataOutputStream out = new DataOutputStream(
                new FileOutputStream(fileName))) {
            out.writeUTF(getName());
            out.writeDouble(getArea());
            out.writeInt(censusesCount());
            for (int i = 0; i < censusesCount(); i++) {
                out.writeInt(getCensus(i).getYear());
                out.writeInt(getCensus(i).getPopulation());
                out.writeUTF(getCensus(i).getComments());
            }
        }
    }

    /**
     * Демонстрація роботи програми
     *
     * @param args аргументи командного рядка (не використовуються)
     */
    public static void main(String[] args) {
        new CountryWithDataFile().testCountry("Ukraine.dat", "ByPopulation.dat", "ByComments.dat");
    }
}

Після виконання програми в кореневій теці проєкту автоматично створюються бінарні файли даних.

4 Вправи для контролю

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

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

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

 

up