Лабораторна робота 1
Робота з потоками введення-виведення
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Спроектувати та реалізувати класи для представлення сутностей лабораторної роботи № 5 курсу "Основи програмування" попереднього семестру. Рішення повинне базуватися ієрархії класів, створеній під час реалізації індивідуального завдання лабораторної роботи № 1 курсу "Об'єктно-орієнтоване програмування (перша частина)". Слід створити два похідних класи від класу, який представляє основну сутність. Один клас повинен бути доповненим можливостями читання даних з відповідно підготовленого текстового файлу та запису цих даних в інший файл після сортування. Другий клас повинен реалізовувати двійкову серіалізацію створених раніше об'єктів, десеріалізацію, а також сортування і серіалізацію в інший файл. Похідні класи повинні реалізовувати спільний інтерфейс, в якому оголошені функції читання з файлу і запису в файл.
Окрім роботи з файлами повинно бути реалізоване виведення результатів у консольне вікно.
1.2 Сортування цілих
Реалізувати програму читання з текстового файлу цілих додатних значень (числа розділені пробілами, слід читати до кінця файлу), занесення цих чисел у масив, сортування за зменшенням та за збільшенням суми цифр та зберігання обох результатів у двох нових текстових файлах. Перелічені дії реалізувати в окремій статичній функції. Для визначення порядку сортування створити класи, які реалізують інтерфейс Comparator
.
1.3 Реалізація серіалізації й десеріалізації
Описати класи Студент і Академічна група (з полем - масивом студентів). Створити об'єкти, здійснити їх бінарну серіалізацію й десеріалізацію.
1.4 Робота з ZIP-архівом
Описати класи Студент і Академічна група (з полем - масивом студентів). Створити об'єкти, здійснити запис даних про студентів академічної групи в архів. В іншій програмі здійснити читання з архіву.
1.5 Список файлів усіх підкаталогів
Увести з клавіатури ім'я певної теки. Вивести на екран імена усіх файлів цієї теки, а також усіх файлів підкаталогів, їхніх підкаталогів тощо. Реалізувати виведення через рекурсивну функцію. Якщо тека не існує, вивести повідомлення про помилку.
1.6 Реалізація власного архіватора (завдання підвищеної складності, за вибором студента)
Використовуючи код Хафмана (Huffman code), створити власний архіватор і деархіватор файлів вказаного (поточного) каталогу та підкаталогів.
2 Методичні вказівки
2.1 Загальна характеристика платформи Java SE
Java Platform, Standard Edition, (Java SE) -
стандартна версія платформи Java, призначена для створення і виконання аплетів і застосунків, розрахованих на індивідуальне користування або на використання в масштабах малого підприємства. Java SE визначається специфікацією пакетів і класів, які забезпечують вирішення завдань за такими напрямами:
- робота з математичними функціями
- робота з контейнерними класами
- робота з таймером і календарем
- робота з текстом
- інтернаціоналізація та локалізація
- робота з регулярними виразами
- робота з потоками введення-виведення і файловою системою
- робота з XML
- cеріалізація і десеріалізація
- створення програм графічного інтерфейсу користувача
- використання графічних засобів
- підтримка друку
- підтримка роботи зі звуком
- використання RTTI, рефлексії та завантажувачів класів
- використання потоків управління
- робота з базами даних
- Java Native Interface
- засоби виконання сценаріїв (скриптів)
- підтримка мережевої взаємодії
- взаємодія з програмним середовищем
- забезпечення безпеки застосунків
- підтримка ведення системного журналу
- розгортання Java-застосунків
Низка технологій Java SE буде розглянута в курсі "Основи Java".
2.2 Потоки введення та виведення
2.2.1 Загальні концепції
Класи, які здійснюють файлове введення та виведення, а також інші дії з потоками, розташовані у пакеті java.io
. Класи цього пакету пропонують низку методів для створення таких потоків, читання, запису, тощо. Безпосередню роботу з текстовими файлами здійснюють об'єкти класів FileReader
та FileWriter
. Існує дві підмножини класів - відповідно для роботи з текстовими та бінарними (двійковими) файлами.
Потоки, призначені для роботи з текстовою інформацією, мають назву потоків символів. Імена класів таких потоків закінчуються відповідно словами "...Reader"
і "...Writer"
.
Важливий елемент роботи з файловими потоками - це буферизація. Буферизація передбачає створення в оперативній пам'яті спеціальної області (буферу), у яку дані завантажуються з файлу для подальшого поелементного читання або поелементно записуються дані з подальшим переписуванням на диск. Об'єкти класу BufferedReader
здійснюють таке буферизоване читання. Для буферизованого виведення застосовують об'єкти класу BufferedWriter
.
Безпосереднє форматоване виведення здійснюється методами print()
та println()
об'єкту класу PrintWriter
.
Уся робота з потоками, окрім стандартних потоків System.in
і System.out
, повинна передбачати перехоплення винятків, пов'язаних з введенням-виведенням. Це IOException
та його нащадки - FileNotFoundException
, ObjectStreamException
та інші.
Дуже важливо закрити всі файли, взаємодія з якими мала місце. Під час закриття файлів здійснюється запис у файл даних, що залишилися в буфері, звільнення буфера та інших ресурсів, пов'язаних з файлом. Закрити файл можна за допомогою методу close()
. Наприклад, для потоку in
:
in.close();
Якщо програма, яка потребує файлового введення, завантажується у середовищі Eclipse (або IntelliJ IDEA), необхідні для читання файли слід розмістити у теці проекту (не у теці пакету). Саме у теці проекту можна знайти результуючі файли, які з'являються після завершення виконання програми, що включає файлове виведення.
У програмі можна одночасно відкрити декілька потоків введення і декілька потоків виведення.
2.2.2 Робота з потоками символів
У наступному прикладі з файлу з ім'ям data.txt здійснюється читання одного цілого і одного дійсного значення, їхня сума записується у файл results.txt.
package ua.in.iwanoff.java.first; 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.in.iwanoff.java.first; import java.io.*; import java.util.Scanner; public class FileTest { void readWrite() { try (Scanner scanner = new Scanner(new FileReader("data.txt"))) { try (PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { new FileTest().readWrite(); } }
Перевагою такого підходу є можливість довільного розташування вихідних даних (не обов'язково в одному рядку). Як видно з наведеного прикладу, кілька блоків try ()
можуть використовувати один блок catch ()
. Альтернативою є розміщення декількох тверджень всередині дужок:
try (Scanner scanner = new Scanner(new FileReader("data.txt")); PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } catch (IOException ex) { ex.printStackTrace(); }
Під час роботи з класом Scanner
можна визначити додаткові параметри, наприклад, встановити символ-роздільник (або послідовність символів). Наприклад, можна перед читанням даних додати наступний рядок:
scanner.useDelimiter(",");
Тепер об'єкт-сканер буде сприймати коми як роздільники (замість пробілів).
2.2.3 Робота з бінарними потоками (потоками байтів)
Для роботи з нетекстовими (бінарними) файлами використовують потоки, імена яких замість "Reader"
або "Writer"
містять "Stream
", наприклад InputStream
, FileInputStream
, OutputStream
, FileOutputStream
тощо. Такі потоки мають назву потоків байтів. У наступному прикладі здійснюється копіювання двійкового файлу FileCopy.class у теку проекту з новим ім'ям:
package ua.in.iwanoff.java.first; 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/in/iwanoff/java/first/FileCopy.class", "FileCopy.copy"); } }
Як видно з наведеного прикладу, Java дозволяє використовувати звичайну риску (/
) замість зворотної. Це - більш універсальний підхід, прийнятний для різних операційних систем. Крім того, зворотну риску необхідно було б записати двічі (\\
).
Для роботи з бінарними файлами існують додаткові можливості - використання потоків даних і потоків об'єктів. Так звані потоки даних (data streams) підтримують бінарне введення / виведення значень примітивних типів даних (boolean
, char
, byte
, short
, int
, long
, float
і double
), а також значень типу String
. Усі потоки даних реалізують інтерфейси DataInput
або DataOutput
. Для більшості задач достатньо стандартних реалізацій цих інтерфейсів - DataInputStream
і DataOutputStream
. Дані у файлі зберігаються в такому вигляді, в якому вони представлені в оперативній пам'яті. Для запису рядків використовують метод writeUTF()
. У наступному прикладі здійснюється запис даних:
package ua.in.iwanoff.java.first; 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.in.iwanoff.java.first; import java.io.*; import java.util.*; public class DataReadDemo { public static void main(String[] args) { try (DataInputStream in = new DataInputStream( new FileInputStream("data.dat"))) { double x = in.readDouble(); String s = in.readUTF(); List<Integer> list = new ArrayList<>(); try { while (true) { int k = in.readInt(); list.add(k); } } catch (Exception e) { } System.out.println(x); System.out.println(s); System.out.println(list); } catch (Exception e) { e.printStackTrace(); } } }
Примітка. У наведеній вище програмі вихід з циклу здійснюється через збудження винятку. Такий підхід не є рекомендованим, оскільки механізм винятків знижує ефективність роботи програми. У нашому випадку доцільно було б окремо зберігати у файлі довжину масиву перед його елементами, а потім використовувати цю довжину для організації циклу for
під час читання.
Для читання і запису даних може бути також використаний клас java.io.RandomAccessFile
. Об'єкт цього класу дозволяє вільно пересуватися всередині файлу в прямому і зворотному напрямку. Основною перевагою класу RandomAccessFile
є можливість читати і записувати дані в довільне місце файлу.
Для того щоб створити об'єкт класу RandomAccessFile
, необхідно викликати його конструктор з двома параметрами: ім'я файлу для введення / виведення і режимом доступу до файлу. Для визначення режиму можна використовувати спеціальні рядки, такі як "r"
(для читання), "rw"
(для читання й запису) тощо. Наприклад, таким може бути відкриття файлу даних:
RandomAccessFile file1 = new RandomAccessFile("file1.dat", "r"); // для читання RandomAccessFile file2 = new RandomAccessFile("file2.dat", "rw"); // для читання й запису
Після того як файл відкритий, можна використовувати методи на кшталт readDouble()
, readInt()
, readUTF()
тощо для читання або writeDouble()
, writeInt()
, writeUTF()
тощо для виведення.
В основі керування файлом лежить вказівник на поточну позицію, де відбувається читання або запис даних. На момент створення об'єкта класу RandomAccessFile
вказівник встановлюється на початок файлу і має значення 0. Виклики методів read...()
и write...()
зсувають позицію поточного вказівника на кількість прочитаних або записаних байтів. Для довільного зсуву покажчика на деяку кількість байтів можна використовувати метод skipBytes()
, або ж встановити покажчик у певне місце файлу викликом методу seek()
. Для того щоб дізнатися поточну позицію, в якій знаходиться вказівник, потрібно викликати метод getFilePointer()
. Наприклад, в одній програмі ми записуємо дані в новий файл:
RandomAccessFile fileOut = new RandomAccessFile("new.dat", "rw"); int a = 1, b = 2; fileOut.writeInt(a); fileOut.writeInt(b); fileOut.close();
В іншій програмі ми читаємо друге ціле число:
RandomAccessFile fileIn = new RandomAccessFile("new.dat", "rw"); fileIn.skipBytes(4); // переміщаємо файловий указівник до другого числа int c = fileIn.readInt(); System.out.println(c); fileIn.close();
Дізнатися довжину файлу в байтах можна за допомогою функції length()
.
2.3 Бінарна серіалізація об'єктів
Механізм серіалізації (serialization, розміщення у послідовному порядку) передбачає запис об'єктів у потік бітів для зберігання у файлі або для передачі через комп'ютерні мережі. Десеріалізація передбачає читання потоку бітів, створення збережених об'єктів та відтворення їхнього стану на момент збереження. Для того, щоб об'єкти певного класу можна було серіалізувати, клас повинен реалізовувати інтерфейс java.io.Serializable
. Цей інтерфейс не визначає жодного метода, його наявність лише вказує, що об'єкти цього класу можна серіалізувати. Однак гарантована серіалізація і десеріалізація вимагає наявності в таких класах спеціального статичного поля serialVersionUID
, яке забезпечує унікальність класу. Середовище Eclipse дозволяє згенерувати необхідне значення автоматично (Quick Fix | Adds a generated serial version ID з контекстного меню).
Класи ObjectOutputStream
та ObjectInputStream
дозволяють здійснювати серіалізацію та десеріалізацію. Вони реалізують інтерфейси ObjectOutput
та ObjectInput
відповідно. Механізми серіалізації та десеріалізації розглянемо на наступному прикладі. Припустимо, описано клас Point
:
package ua.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; import java.io.*; public class ArrayDeserialization { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("temp.dat"))) { ArrayOfPairs arrayOfPairs = (ArrayOfPairs) in.readObject(); for (Pair p : arrayOfPairs.pairs) { System.out.println(p.x + " " + p.y); } } catch (Exception e) { e.printStackTrace(); } } }
Деякі поля класу, значення яких не впливають на стан об'єкта, можна описати з модифікатором transient
. Наприклад:
class SomeClass implements Serializable { transient int someUnnecessaryField; }
Такі поля не будуть збережені у потоці під час серіалізації та не будуть відтворені під час десеріалізації.
Серіалізація та десеріалізація можуть бути використані замість файлового введення та виведення. Головним недоліком бінарної серіалізації є необхідність роботи з двійковими (нетекстовими) файлами.
2.4 Робота з архівами
Пакет java.util.zip
надає можливості роботи зі стандартними файлами ZIP і GZIP форматів.
Для запису в архів застосовують клас ZipOutputStream
. За допомогою функції setMethod()
цього класу можна визначити метод архівації - ZipOutputStream.DEFLATED
(з компресією) або ZipOutputStream.STORED
(без компресії). Метод setLevel()
визначає рівень компресії (вд 0 до 9, за умовчанням Deflater.DEFAULT_COMPRESSION
, зазвичай, максимальна компресія). Метод setComment()
дозволяє додати коментар до архіву.
Для кожного запису, який треба помістити в zip-файл, створюється об'єкт ZipEntry
. Бажане ім'я для файлу передається конструктору ZipEntry
. В ньому можна окремо встановити аналогічні параметри. Далі за допомогою метода putNextEntry()
класу ZipOutputStream
"розкривається" відповідна точка входу до архіву. За допомогою засобів роботи з файловими потоками здійснюється запис даних в архів, потім слід закрити об'єкт ZipEntry
за допомогою виклику closeEntry()
.
У наведеному нижче прикладі створюється архів Source.zip
, до якого додається вміст вихідного файлу ZipCreator.java
:
package ua.in.iwanoff.java.first; 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/in/iwanoff/java/first/ZipCreator.java"); zOut.putNextEntry(zipEntry); try (FileInputStream in = new FileInputStream("src/ua/in/iwanoff/java/first/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.in.iwanoff.java.first; 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 Робота з файловою системою
Пакет java.io
надає можливість роботи як із вмістом файлів, так і з файловою системою в цілому. Цю можливість реалізує клас File
. Для створення об'єкта цього класу як параметр конструктора слід визначити повний або відносний шлях до файлу. Наприклад:
File dir = new File("C:\\Users"); File currentDir = new File("."); // Тека проекту (поточна)
Клас File
містить методи для отримання списку файлів визначеної теки (list()
, listFiles()
), отримання та модифікації атрибутів файлів (setLastModified()
, setReadOnly()
, isHidden()
, isDirectory()
тощо), створення нового файлу (createNewFile()
, createTempFile()
), створення тек (mkdir()
), видалення файлів та тек (delete()
) та багато інших. Роботу деяких з цих методів можна продемонструвати на наступному прикладі:
package ua.in.iwanoff.java.first; import java.io.*; import java.util.*; public class FileTest { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки, яку ви хочете створити:"); String dirName = scanner.next(); File dir = new File(dirName); // Створюємо нову теку: if (!dir.mkdir()) { System.out.println("Не можна створити теку!"); return; } // Створюємо новий файл всередині нової теки: File file = new File(dir + "\\temp.txt"); file.createNewFile(); // Показуємо список файлів теки: System.out.println(Arrays.asList(dir.list())); file.delete(); // Видаляємо файл dir.delete(); // Видаляємо теку } }
Функція list()
без параметрів дозволяє отримати масив рядків - усіх файлів та підкаталогів теки, визначеної під час створення об'єкта типу File
. Виводяться відносні імена файлів (без шляхів). У наступному прикладі ми отримуємо список файлів та підкаталогів теки, ім'я якої вводиться з клавіатури:
package ua.in.iwanoff.java.first; import java.io.File; import java.io.FilenameFilter; import java.util.Scanner; public class ListOfFiles { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки:"); String dirName = scanner.next(); File dir = new File(dirName); if (!dir.isDirectory()) { System.out.println("Хибне ім\'я теки!"); return; } String[] list = dir.list(); for(String name : list) { System.out.println(name); } } }
На відміну від list()
, функція listFiles()
повертає масив об'єктів типу File
. Це надає додаткові можливості - отримання імен файлів з повним шляхом, перевірки значень атрибутів файлів, окрему роботу з теками тощо. Ці додаткові можливості покажемо на наступному прикладі:
File[] list = dir.listFiles(); // Виводяться дані про файли в усталеній формі: for(File file : list) { System.out.println(file); } // Виводиться повний шлях: for(File file : list) { System.out.println(file.getCanonicalPath()); } // Виводяться тільки підкаталоги: for(File file : list) { if (file.isDirectory()) System.out.println(file.getCanonicalPath()); }
Для визначення маски-фільтру необхідно створювати об'єкт класу, який реалізує інтерфейс FilenameFilter
. У наступному прикладі ми отримуємо список файлів та підкаталогів, імена яких починаються з літери 's'
:
String[] list = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().charAt(0) == 's'; } }); for(String name : list) { System.out.println(name); }
Аналогічний параметр типу FilenameFilter
можна застосувати до функції listFiles()
.
3 Приклади програм
3.1 Порядкове копіювання текстових файлів
Припустимо, необхідно створити програму, яка здійснює копіювання текстових файлів рядок за рядком. Імена файлів задаються аргументами командного рядка. Текст програми буде таким:
package ua.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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 Класи "Країна" та "Перепис населення"
Припустимо, необхідно доповнити попередньо створену програму класами, які виконують завдання прикладу лабораторної роботи № 5 курсу "Основи програмування" попереднього семестру, але дані про переписи населення читають із файлів, а також записують результати сортування за кількістю населення в інший файл. При чому перший з класів працюватиме зі звичайними текстовими файлами, а другий - через двійкову серіалізацію і десеріалізацію.
Для роботи з переписами нам потрібні раніше створені класи AbstractCensus
і AbstractCountry
. Вони розташовані в пакеті ua.in.iwanoff.oop.first
(див. приклад 3.4 лабораторної роботи № 1 курсу "Об'єктно-орієнтоване програмування"). Найкращим варіантом реалізації буде не створення нового проекту, а додавання до раніше створеного проекту нового пакету, що дозволить посилатися на раніше створені класи.
Для уніфікації роботи програми нам доцільно створити інтерфейс, який декларує відповідні операції читання і запису. Цей інтерфейс, наприклад, може бути таким:
package ua.in.iwanoff.java.first; public interface FileIO { void readFromFile(String fileName) throws Exception; void writeToFile(String fileName) throws Exception; }
Клас TextFileCountry
для роботи з текстовими файлами буде походити від класу CountryWithArray
(реалізований у прикладі лабораторної роботи № 1 курсу "Об'єктно-орієнтоване програмування") і реалізовувати інтерфейс FileIO
:
package ua.in.iwanoff.java.first; import ua.in.iwanoff.oop.first.AbstractCensus; import ua.in.iwanoff.oop.first.CensusWithData; import ua.in.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
, у якому дані про переписи будуть відсортовані за алфавітом коментарів.
Тепер можна реалізувати варіант з двійковою серіалізацією і десеріалізацією. Створюємо клас SerializedCensus
, який реалізує інтерфейс Serializable
:
package ua.in.iwanoff.java.first; import ua.in.iwanoff.oop.first.AbstractCensus; import java.io.Serializable; public class SerializedCensus extends AbstractCensus implements Serializable { private static final long serialVersionUID = -4998473980228618354L; private int year; private int population; private String comments; public SerializedCensus() { } public SerializedCensus(int year, int population, String comments) { this.year = year; this.population = population; this.comments = comments; } @Override public int getYear() { return year; } @Override public void setYear(int year) { this.year = year; } @Override public int getPopulation() { return population; } @Override public void setPopulation(int population) { this.population = population; } @Override public String getComments() { return comments; } @Override public void setComments(String comments) { this.comments = comments; } }
Клас SerializedCountry
також реалізовує інтерфейс Serializable
, і, крім того, інтерфейс FileIO
. Оскільки об'єкт не може реалізувати сам себе, функція readFromFile()
інтерфейсу не може бути реалізована. В тілі цієї функції розташовуємо генерацію відповідного стандартного винятку. Десеріалізацію будемо виконувати в окремій статичній функції deserialize()
. Код буде таким:
package ua.in.iwanoff.java.first; import ua.in.iwanoff.oop.first.AbstractCensus; import ua.in.iwanoff.oop.first.AbstractCountry; import ua.in.iwanoff.oop.first.CompareByComments; import ua.in.iwanoff.oop.first.CountryWithArray; import java.io.*; import java.util.Arrays; public class SerializedCountry extends AbstractCountry implements Serializable, FileIO { private static final long serialVersionUID = 5884026597888745689L; private String name; private double area; private SerializedCensus[] censuses; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public double getArea() { return area; } @Override public void setArea(double area) { this.area = area; } @Override public AbstractCensus getCensus(int i) { return censuses[i]; } @Override public void setCensus(int i, AbstractCensus census) { censuses[i] = (SerializedCensus) census; } @Override public boolean addCensus(AbstractCensus census) { if (getCensuses() != null) { for (AbstractCensus c : getCensuses()) { if (c.equals(census)) { return false; } } } setCensuses(addToArray(getCensuses(), census)); return true; } @Override public boolean addCensus(int year, int population, String comments) { AbstractCensus census = new SerializedCensus(year, population, comments); return addCensus(census); } @Override public int censusesCount() { return censuses.length; } @Override public void clearCensuses() { censuses = null; } @Override public AbstractCensus[] getCensuses() { return censuses; } @Override public void setCensuses(AbstractCensus[] censuses) { this.censuses = new SerializedCensus[censuses.length]; for (int i = 0; i < censuses.length; i++) { this.censuses[i] = (SerializedCensus) censuses[i]; } } @Override public void sortByPopulation() { Arrays.sort(censuses); } @Override public void sortByComments() { Arrays.sort(censuses, new CompareByComments()); } @Override public void readFromFile(String fileName) throws Exception { throw new UnsupportedOperationException(); } public static SerializedCountry deserialize(String fileName) throws ClassNotFoundException, IOException { ObjectInputStream in = new ObjectInputStream(new FileInputStream(fileName)); SerializedCountry country = (SerializedCountry) in.readObject(); in.close(); return country; } @Override public void writeToFile(String fileName) throws IOException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName)); out.writeObject(this); out.close(); } }
Окремо створюємо клас CountrySerializer
, у функції main()
якого здійснюємо створення країни і її серіалізацію. Код будет таким:
package ua.in.iwanoff.java.first; import java.io.IOException; public class CountrySerializer { public static void main(String[] args) { SerializedCountry country = new SerializedCountry(); country.createCountry(); try { country.writeToFile("Ukraine.dat"); } catch (IOException e) { System.err.println("Write failed"); e.printStackTrace(); } } }
Після виконання програми в кореневій теці проекту створюється файл Ukraine.dat
.
Для десеріалізації і тестування створюємо окремий клас CountryDeserializer
:
package ua.in.iwanoff.java.first; import java.io.FileNotFoundException; import java.io.IOException; public class CountryDeserializer { public static void main(String[] args) { SerializedCountry country = null; try { country = SerializedCountry.deserialize("Ukraine.dat"); country.testCountry(); country.writeToFile("ByComments.dat"); } catch (FileNotFoundException e) { System.err.println("Read failed"); e.printStackTrace(); } catch (IOException e) { System.err.println("Write failed"); e.printStackTrace(); } catch (ClassNotFoundException e) { System.err.println("Deserialization error"); e.printStackTrace(); } } }
Після виконання цієї програми в кореневій теці проекту можна буде знайти файл ByComments.dat
.
4 Вправи для контролю
- Прочитати з текстового файлу дійсні значення (до кінця файлу), знайти їх суму та вивести в інший текстовий файл.
- Прочитати з текстового файлу дійсні значення (до кінця файлу), знайти добуток модулів ненульових елементів та вивести в інший текстовий файл.
- Прочитати з текстового файлу цілі значення (до кінця файлу), знайти добуток парних елементів та вивести в інший текстовий файл.
- Прочитати з текстового файлу цілі значення (до кінця файлу), замінити від'ємні значення модулями, додатні нулями та вивести отримані значення в інший текстовий файл.
- Прочитати з текстового файлу цілі значення (до кінця файлу), замінити розділити парні елементи на 2, непарні - збільшити у 2 рази та вивести отримані значення в інший текстовий файл.
- Описати класи Факультет та Інститут (з полем - масивом факультетів). Створити об'єкти, здійснити їх бінарну серіалізацію й десеріалізацію.
5 Контрольні запитання
- Назвіть основні складові платформи Java SE.
- Чим відрізняються потоки байтів від потоків символів за областю застосування?
- Які класи забезпечують роботу з текстовими файлами і бінарними файлами?
- У чому сенс явного закриття файлів
?
- Чи можна одночасно відкрити кілька потоків введення / виведення?
- Яким чином можна забезпечити автоматичне закриття потоків?
- У чому переваги використання класу
RandomAccessFile
? - Для чого використовують файли даних
DataOutputStream
іDataInputStream
? Які у них переваги і недоліки? - Що таке серіалізація і для чого вона використовується?
- У чому є переваги й недоліки серіалізації?
- Які функції слід визначити для реалізації інтерфейсу
java.io.Serializable
? - Для чого використовують модифікатор
transient
? - Як в Java здійснюється робота з архівами?
- Чи можна створити архів з кількома файлами всередині?
- Як визначити поняття "файлова система"?
- Які можна назвати типові функції для роботи з файловою системою?
- Які засоби надає Java для роботи з файловою системою?
- Як отримати атрибути файлу за допомогою засобів класу
java.io.File
? - Чим відрізняються функції
list()
іlistFiles()
? - Як здійснити копіювання файлів?