МІНІСТЕРСТВО ОСВІТИ ТА НАУКИ УКРАЇНИ
НАЦІОНАЛЬНИЙ УНІВЕРСИТЕТ “ЛЬВІВСЬКА ПОЛІТЕХНІКА”
Потоки та робота із файлами у Java
Методичні вказівки
до виконання лабораторної роботи №6
з курсу “Об’єктно-орієнтоване програмування”
для студентів базового напрямку
6.0804 “Комп’ютерні науки”
ЗАТВЕРДЖЕНО
на засіданні кафедри “Системи автоматизованого проектування” Протокол № 1від 30.08.2010
ЛЬВІВ 2010 Мова програмування Java. Методичні вказівки до виконання лабораторної роботи №06 “Потоки та робота із файлами у Java” з курсу: “Об’єктно-орієнтоване програмування” для студентів базового напрямку 6.0804 “Комп’ютерні науки”.
Укладачі: Каркульовський В.І., доцент, к.т.н.
Керницький А.Б., ст.викл., др.інж.
Відповідальний за випуск:
Рецензенти:
1. МЕТА РОБОТИ
Одержати навички .
2.ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ
2.1. ВВЕДЕННЯ ТА ВИВЕДЕННЯ В JAVA
У багатьох випадках потрібно виводити результати на принтер, у файл, базу даних або передавати по мережі. Вхідні дані теж часто приходиться завантажувати із файла, бази даних або із мережі. Для того щоб абстрагуватись від особливостей конкретних пристроїв введення/виведення, в Java використовується поняття потоку (stream). Вважається, що у програму іде вхідний поток (input stream) символів Unicode або просто байтів, що сприймається в програмі методами read(). Із програми методами write() або print (), println() виводиться вихідний потік (output stream) символів або байтів. При цьому не має значення куди направлений потік: на консоль, на принтер, у файл або в мережу, методи write() і print() нічого про це не знають.
Можна уявити собі потік як трубу, по якій в одному напрямку послідовно "течуть" символи або байти, один за одним. Методи read() , write() , print(), println() взаємодіють з одним кінцем труби, другий кінець з’єднується з джерелом або приймачем даних - конструкторами класів, в яких реалізовані ці методи. Звичайно, повне ігнорування особливостей пристроїв введення/виведення сильно сповільнює передачу інформації. Тому в Java виділяється файлове введення/виведення, виведення на друк, виведення у мережевий потік.
Три потоки визначені у класі System статичними полями in, out і err. Їх можна використовувати без будь-яких додаткових визначень. Вони називаються відповідно стандартним введенням (stdin), стандартним виведенням (stdout) і стандартним виведенням повідомлень (stderr). Ці стандартні потоки можуть бути зєднані з різними конкретними присторями введення/виведення. Потоки out і err — це екземпляри класу Printstream, який організовує вихідний потік байтів. Ці екземпляри виводять інформацію на консоль методами print(), println() i write(), яких в класі Printstream є близько двадцати для різних типів аргументів.
Потік err призначений для виведення системних повідомлень програми: трасування, повідомлень про помилки або про виконання певних етапів програми. Такі дані звичайно заносяться в спеціальні журнали, log-файли, а не виводяться на консоль. В Java є засоби перепризначення потоку, наприклад, з консолі у файл.
Потік in — це екземпляр класу inputstream. Він призначений для клавіатурного введення з консолі методами read(). Клас inputstream є абстрактним, тому реально використовується хтось із його підкласів.
Поняття потоку виявилось настільки зручним у програмуванні введення/виведення, що в Java передбачена можливістьсть створення потоків, які направляють символи або байти не на зовнішній пристрій, а в масив або із масиву, тобто які зв’язують програму з областю оперативної пам’яті. Більше того, можна створити потік, зв’язаний з рядком типу string, що знаходиться в оперативній памяті. Крім того, можна створити канал (pipe) обміну інформацією між підпроцесами.
Ще один вид потоку — потік байтів, який складає об’єкт Java. Його можна направити у файл або передати по мережі, потім відновити в оперативній пам’яті. Ця операція називається серіалізацією (serialization) об’єктів.
Методи організації потоків зібрані у класи пакета java.io. Крім класів, які організують потік, в пакет java.io входять класи з методами перетворення потоку, наприклад, можна перетворити потік байтів, які утворюють цілі числа, в потік цих чисел. Ще одна можливість, представлена класами пакета java.io, — злити декілька потоків в один потік.
Отже, в Java присутні чотири ієрархії класів для створення, перетворення і злиття потоків, які безпосередньо розширюють клас object:
Reader — абстрактний клас, в якому зібрані найзагальніші методи символьного введення;
Writer — абстрактний клас, в якому зібрані найзагальніші методи символьного виведення;
Inputstream — абстрактний клас з загальними методами байтового введення;
Outputstream — абстрактний клас з загальними методами байтового виведення.
Класи вхідних потоків Reader і Inputstream визначають по три методи введення:
read () — повертає один символ або байт, взятий із вхідного потоку, у вигляді цілого значення типу int; якщо потік уже закінчився, повертає -1;
read (char[] buf) — заповнює заздалегідь визначений масив buf символами із вхідного потоку; в класі inputstream масив типу byte[] і він заповнюється байтами; метод повертає фактичну кількість взятих із потоку елементів або -1, якщо потік уже закінчився;
read (char[] buf, int offset, int len) — заповнює частину символьного або байтового масиву buf, починаючи з індекса offset, кількість взятих із потоку елементів дорівнює len; метод повертає фактичну кількість взятих із потока елементів або -1.
Ці методи видають IOException, якщо відбулася помилка введення/виведення. Четвертий метод skip (long n) "промотує" потік з поточної позиції на n символів або байтів вперед. Ці елементи потоку не вводяться методами read(). Метод повертає реальну кількість пропущених елементів, яка може відрізнятися від n, наприклад потік може закінчиться. Поточний елемент потоку можна помітити методом mark (int n), а потім повернутися до поміченого елементу методом reset(), але не більше ніж через n елементів. Не всі підкласи реалізують ці методи, тому перед розстановкою поміток треба звернутися до логічного методу marksupported(), який повертає true, якщо реалізовані методи розстановки і повернення до поміток.
Класи вихідних потоків writer і outputstream визначають по три майже однакових методи введення:
write (char[] buf) — виводить масив у вихідний потік, в класі Outputstream масив має тип byte[];
write (char[] buf, int offset, int len) — виводить len елементів масиву buf, починаючи з злемента із індексом offset;
write (int elem) в класі Writer - виводить 16, а в класі Outputstream 8 молодших бітів аргумента elem у вихідний потік.
У класі Writer присутні ще два методи:
write (string s) — виводить рядок s у вихідний потік;
write (String s, int offset, int len) — виводить len символів рядка s, починаючи із символа з номером offset.
Багато підкласів класів Writer і Outputstream здійснюють буферизовае виведення. При цьому елементи спочатку нагромаджуються у буфері, в оперативній пам’яті, і виводяться у вихідний потік тільки після того, як буфер заповниться. Це зручно для вирівнювання швидкостей виведення із програми і виведення потоку, але часто потрібно вивести інформацію у потік ще до заповнення буферу. Для цього передбачений метод flush(). Даний метод зразу ж виводить весь вміст буфера у потік. По завершені роботи з потоком його необхідно закрити методом closed. Класи, що входять в ієрархії потоків введення/виведення, показані на рис. 18.1 и 18.2.
Рис. 1.1. Ієрархія символьних потоків
Рис. 1.2. Класи байтових потоків
Всі класи пакета java.io можна розділити на дві групи: класи, що створюють потік (data sink), і класи, що керують потоком (data processing). Класи, що створюють потоки, у свою чергу, можна розділити на пять груп:
класи, що створюють потоки, зв’язані з файлами:
FileReader, FilelnputStream, FileWriterFile, Outputstream, RandomAccessFile
класи, що створюють потоки, зв’язані з масивами:
CharArrayReader, ByteArraylnputStream, CharArrayWriter, ByteArrayOutputStream
класи, що створюють канали обміну інформацією між підпроцесами:
PipedReader, PipedlnputStream, PipedWriter, PipedOutputStream
класи, що створюють символьні потоки, звя’зані з рядком:
StringReader, StringWriter
класи, що створюють байтові потоки із об’єктів Java:
ObjectlnputStream, ObjectOutputStream
Зліва перераховані класи символьних потоків, справа — класи байтових потокив. Класи, які керують потоком, отримують у своїх конструкторах уже наявний потік і створюють новий, перетворений потік. Можна собі їх уявляти як "перехідне кільце", після якого іде труба іншого діаметру. Чотири класи створені спеціально для перетворення потоків:
FilterReader, FilterlnputStream, FilterWriter, FilterOutputStream
Самі по собі ці класи не мають жодної користі — вони виконують тотожне перетворення. Їх портрібно розширювати, перевизначаючи методи введення/виведення. Але для байтових фільтрів є корисні розширення, яким відповідають деякі символьні класи. Перерахуємо їх.
Чотири класи виконують буферизоване введення/виведення: BufferedReader, BufferedlnputStream, BufferedWriter, BufferedOutputStream
Два класи перетворюють потік байтів, які утворюють вісім простих типів Java, у ці самі типи: DatalnputStream, DataOutputStream
Два класи містять методи, які дозволяють повернути декілька символів або байтів у вхідний потік: PushbackReader, PushbacklnputStream
Два класи пов’язані з виведенням на рядкові пристрої — екран дисплея, принтер: PrintWriter, PrintStream
Два класса зв’язують байтовий і символьний потоки:
InputstreamReader — перетворює вхідний байтовий потік у символьний потік;
Outputstreamwriter — перетворює вихідний символьний потік у байтовий потік.
Клас streamTokenizer дозволяє розібрати вхідний символьний потік на окремі елементи (tokens).
Із керуючих класів виділяється клас sequenceinputstream, який зливає декілька потоків, заданих у конструкторі, в один потік, і клас LineNumberReader, який "вміє" читати вихідний символьний потік порядково. Рядки у потоці розділяються символами '\n' і/або '\г'.
Тепер перейдемо до розгляду реальних ситуацій.
2.2. КОНСОЛЬНЕ ВВЕДЕННЯ/ВИВЕДЕННЯ
Для виведення на консоль у попередніх лабораторних роботах ми використовували метод printІn() класу Printstream, ніколи не визначаючи екземпляри цього класу. Ми просто використовували статичне поле out класу System, яке є об’єктом класу PrintStream. Виконуюча система Java зв’язує це поле з консоллю. Можна скоротити написання system.out.printІn(). Для цього потрібно визначити нове посилання на System.out, наприклад:
PrintStream pr = System.out;
і писати просто pr.printІn(). Консоль є байтовим пристроєм, і символи Unicode перед виведенням на консоль повинні бути перетворені у байти. Для символів Latin 1 з кодами '\u0000' — '\u00FF' при цьому просто відкидається старший нульoвий байт і виводяться байти '0х00' —'0xFF'. Для кодів кирилиці, які лежать у діапазоні '\u0400 —'\u04FF кодування Unicode, і інших національних алфавітів відбувається перетворення по кодовій таблиці, яка встановлена на комп’ютері.
Труднощі з відображенням кирилиці виникають, якщо виведення на консоль відбувається в кодуванні, відмінному від локального. Саме так відбувається в кирилихованих версіях MS Windows XP. Звичайно у них встановлюється локальна кодова сторінка СР1251, а виведення на консоль відбувається в кодуванні СР866. У цьому випадку треба замінити Printstream, який не може працювати з символьним потоком, на Printwriter і "вставити перехідне кільце" між потоком символів Unicode і потоком байтів System. out, що виводяться на консоль, у вигляді об’єкта класу OutputstreamWriter. В конструкторі цього об’єкта потрібно вказати потрібне кодування, у даному випадку, СР866. Все це можна зробити одним оператором:
PrintWriter pw = new PrintWriter(new OutputstreamWriter(System.out, "Cp866"), true);
Клас Printstream буферизує вихідний потік. Другий аргумент true його конструктора викликає примусове зкидання вмісту буфера у вихідний потік після кожного виконання методу printІn(). Але після print() буфер не звільняється. Для зкидання буфера після кожного print() потрібно писати flush(), як це зроблено у лістингу 2.2.
Введення з консолі відбувається методами read() класу Inputstream за допомогою статичного поля in класу System. З консолі йде потік байтів, отриманих із scan-кодів клавіатури. Ці байти повинні бути перетворені у символи Unicode такими ж кодовими таблицями, як і при виведенні на консоль. Перетворення йде по тій же схемі — для правильного введення кирилиці зручніше визначити екземпляр класу BufferedReader, використовуючи в якості "перехідного кільця" об’єкт класу InputstreamReader:
BufferedReader br = new BufferedReader( new InputstreamReader(System.an, "Cp866"));
Клас BufferedReader перевизначає три методи read() свого суперкласу Reader. Крім того, він містить метод readLine(). Метод readLine() пoвертає рядок типу String, що містить символи вхідного потоку, починаючи з біжучого, і закінчуючи символом '\n' і/або '\r'. Ці символи-розділювачі не входять в повернений рядок. Якшо у вхідному потоці немає символів, то повертається null. У лістингу 2.1 наведена програма, яка ілюструє перераховані методи консольного введення/виведення.
Лістинг 2.1. Консольне введення/виведення
import j ava.io.*;
class PrWr{
public static void main(String[] args){
try{
BufferedReader br = new BufferedReader(new InputstreamReader(System.in, "Cp866"));
PrintWriter pw = new PrintWriter(
new OutputstreamWriter(System.out, "Cp866"), true);
String s = "Це рядок з українським текстом";
System.out.println("System.out puts: " + s);
pw.println("PrintWriter puts: " + s) ;
int с = 0;
pw.println("Посимвольне введення:");
while((с = br.read()) != -1)
pw.println((char)c);
pw.println("Порядкове введення:");
do{
s = br.readLine();
pw.println(s);
}while(!s.equals("q"));
}catch(Exception e){
System.out.println(e);
}
}
}
2.3. ФАЙЛОВЕ ВВЕДЕННЯ/ВИВЕДЕННЯ
Оскільки файли у більшості сучасних операційних систем розуміються як послідовність байтів, для файлового введення/виведення створюються байтові потоки за допомогою класів FiІeІnputstream і FileOutputstream. Це особливо зручно для бінарних файлів, що зберігають байт-коди, архіви, зображення, звук. Але дуже багато файлів містять тексти, складені із символів. Незважаючи на те, що символи можуть зберігатися у кодуванні Unicode, ці тексти частіше за все записані у байтових кодуваннях. Тому і для текстових файлів можна використовувати байтові потоки. У такому випадку з боку програми прийдеться організовувати перетворення байтів у символи і навпаки.
Щоб спрстити це перетворення, в пакет java.io введені класи FiІeReader у FileWriter. Вони організовують перетворення потоку: із сторони програми потоки символьні, зі сторони файла — байтові. Це відбувається тому, що дані класи розсширюють відповідно класи InputStreamReader і OutputstreamWriter, і містять "перехідне кільце" всередині себе. Незважаючи на відмінність потоків, використання класів файлового введення/виведення дуже схоже. В конструкторах всіх чотирьох файлових потоків задається ім’я файла у вигляді рядка типу string або посилання на об’єкт класу File. Конструктори не тільки створюють об’єкт, але і шукають файл і відкривають його. Наприклад:
Fileinputstream fis = new FilelnputStreamC'PrWr.Java");
FileReader fr = new FileReader("D:\\jdkl.3\\src\\PrWr.Java");
При невдачі видається виключення класу FileNotFoundException, але конструктор класу FileWriter видає більш загальне виключення IOException. Після відкриттия вихідного потоку типу FileWriter або FileQutputStream вміст файлу, якщо він не був порожнім, стирається. Для того щоб можна було робити запис у кінець файла, і в тому і в іншому класі передбачений конструктор з двома аргументами. Якщо другий аргумент дорівнює true, то відбувається дозапис у кінець файлу, якщо false, то файл заповнюється новою інформацією. Наприклад:
FileWriter fw = new FileWriter("ch!8.txt", true);
FiieOutputstream fos = new FileOutputstream("D:\\samples\\newfile.txt");
Вміст файлу, відкритого для запису конструктором з одним аргументом, стирається. Зразу після виконання конструктора можна читати файл:
fis.read(); fr.read(); або записувати в нього: fos.write((char)с); fw.write((char)с);
По закінченню роботи з файлом потік потрібно закрити методом close(). Перетворення потоків у класах FileReader і FileWriter виконується по кодових таблицях встановленої на компютері локалізації. Для правильного введення кирилиці треба застосувати FileReader, a нe FileInputStream. Якщо файл містить текст у кодуванні, відмінному від локального кодування, то прийдеться вставляти "перехідне кільце" вручну, як це робилось для консолі, наприклад:
InputStreamReader isr = new InputStreamReader(fis, "KOI8_R"));
Байтовий потік fis визначений вище.
2.4. ОТРИМАННЯ ВСЛАСТИВОСТЕЙ ФАЙЛА
У конструкторах класів файлового введення/виведення, описаних у попередньому розділі, вказувалось ім’я файла у вигляді рядка. При цьому залишалось невідомим, чи існує файл, чи дозволений до нього доступ, і яка довжина файла. Отримати такі дані можна від попередньо створеного екземпляру класу File, що містить дані про файл. У конструкторі цього класу
File(String filename)
вказується шлях до файлу або каталогу, записаний за правилами операційної системи. У MS Windows — зворотною похилою рискою \. Цей символ міститься в системній властивості file.separator. Шляху до файлу передує префікс. В MS Windows — літера розділу диска, двокрапка і зворотна похила риска. Якщо префікса немає, то шлях вважається відносним і до нього додається шлях до біжучого каталогу, який зберігається в системній властивості user.dir. Конструктор не перевіряє, чи існує файл з таким іменем, тому після створення об’єкта потрібно це перевірити логічним методом exists().
Клас File містить близько сорока методів, що дозволяють взнати різні властивості файла або каталога. Перш за все, логічними методами isFile(), isDirectory() можна вияснити, чи є шлях, вказаний у конструкторі, шляхом до файла або каталога. Для каталога можна отримати його зміст — список імен файлів і підкаталогів— методом list(), який повертає масив рядків string[]. Можна отримати такий же список у вигляді масиву об’єктів класу File[] методом listFiles(). Можна выбрати зі списку тільки деякі файли, реалізувавши інтерфейс FileNameFiiter і звернувшись до методу
list(FileNameFilter filter).
Якщо каталог з вказаним у конструкторі шляхом не існує, його можна створити логічним методом mkdir(). Цей метод повертає true, якщо каталог увалося створити. Логічний метод mkdir() створює ще і всі неіснуючі каталоги, вказані у шляху. Порожній каталог видаляється методом delete(). Для файла можна отримати його довжину в байтах методом length(), час останньої модифікації в секундах з 1 січня 1970 р. методом lastModified(). Якщо файл не існує, ці методи повертають нуль. Логічні методи canRead(), canWrite() показують права доступу до файла. Файл можна переіменувати логічним методом renameTo(File newMame) або видалити логічним методом delete(). Ці методи повертають true, якщо операція пройшла успішно. Якщо файл з вказаним у конструкторі шляхом не існує, його можна створити логічним методом createNewFile(), що повертає true, якщо файл не існував, і його вдалось створити, і false, якщо файл уже існував.
Статичними методами
createTempFile(String prefix, String suffix, File tmpDir)
createTempFile(String prefix, String suffix)
можна створити тимчасовий файл з іменем prefix і розширенням suffix в каталозі tmpDir або каталозі, вказаному у системній властивості java.io.tmpdir. Ім’я prefix повинно містити не менше трьох символів. Якщо suffix = null, то файл одержить суфікс .tmp. Перечислені методи повертають посилання типу File на створений файл. Якщо звернутися до методу deІeteOnExit(), то по завершенні роботи JVM тимчасовий файл буде знищено.
Декілька методів getxxxo повертають ім’я файла, ім’я каталога та інші дані про шлях до файлу. Ці методи корисні у тих випадках, коли посилання на об’єкт класу File повертається іншими методами і потрібні дані про файл. Метод toURL() повертає шлях до файлу у формі URL. У лістингу 2.2 показано приклад використання класу File.
Лістинг 2.2. Визначення властивостей файла і каталога
import java.io.*;
class FileTest{
public static void main(String[] args) throws IOException{
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(System.out, "Cp866"), true);
File f = new File("FileTest.Java");
pw.println();
pw.println("Файл \"" + f.getName() + "\" " +
(f.exists()?"":"не ") + "існує");
pw.println("Ви " + (f.canRead()?"":"не ") + "можете читати файл");
pw.println("Ви " + (f.canWrite()?"":"нe ") +
"можете записувати у файл");
pw.println("Довжина файла " + f.length() + " б");
pw.println() ;
File d = new File("D:\\jdkl.3\\MyProgs");
pw.println("Вміст каталога:");
if (d.exists() && d.isDirectory()) {
String[] s = d.list();
for (int i = 0; i < s.length; i++)
pw.println(s[i]);
}
}
}
2.5. БУФЕРИЗОВАНЕ ВВЕДЕННЯ/ВИВЕДЕННЯ
Операції введення/виведення у порівнянні з операціями в оперативній пам’яті виконуються дуже повільно. Для компенсації в оперативній памяті виділяється деяка проміжна область — буфер, в який поступово нагромаджується інформація. Коли буфер заповнений, його вміст швидко переноситься процесором, буфер очищається і знову заповнюється інформацією.
Класи файлового введення/виведення не займаються буферизацією. Для цієї мети існує чотири спеціальні класи BufferedXxx, перераховані вище. Вони приєднуються до потоків введення/виведення як "перехідне кільце", наприклад:
BufferedReader br = new BufferedReader(isr);
BufferedWriter bw = new BufferedWriter(fw);
Потоки isr і fw визначені вище. Програма лістинга 2.3 читає текстовий файл, написаний у кодуванні СР866, і записує його вміст у файл у кодуванні KOI8_R. При читанні і записі застосовується буферизація. Ім’я вихідного файла задається у командному рядку параметром args[0], ім’я копії — параметром args[].
Лістинг 2.3. Буферизоване файлове введення/виведення
import java.io.*;
class DOStoUNIX{
public static void main(String[] args) throws IOException{
if (args.length != 2){
System.err.println("Usage: DOStoUNIX Cp866file KOI8_Rfile");
System.exit(0);
}
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(args[0]), "Cp866"));
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStreamtargs[1]), "KOI8_R"));
int с = 0;
while ((c = br.readO) != -1)
bw.write((char)c);
br.closeO; bw.close();
System.out.println("The job's finished.");
}
}
2.6. ПОТІК ПРОСТИХ ТИПІВ JAVA ТА КОДУВАННЯ UTF-8
Клас DataOutputstream дозволяє записати дані простих типів Java у вихідний потік байтів методами writeBoolean (boolean b), writeBytefint (b), writeShort(int h), writeChar(int c), writeln(int n), writeLong(long І), writeFloat(float f), writeDouble(double d). Крім того, метод writeBytes(String s) записує кожний символ рядка s в один байт, відкидаючи старший байт кодування кожного символу Unicode, а метод writeСhar(String s) записує кожний символ рядка s у два байти, перший байт — старший байт кодування Unicode, так само, як це робить метод writeChar().
Запис потоку у байтовму кодуванні викликає труднощі з використанням національних символів, запис потоку в Unicode збільшує довжину потоку у два рази. Кодування UTF-8 (Universal Transfer Format) є компромісом. Символ у цьому кодуванні записується одним, двома або трьома байтами. Символи Unicode із діапазону '\u0000' —'\u007F', в якому лежить англійський алфавіт, записуються одним байтом, старший байт просто відкидається. Символи Unicode із діапазону '\u0080' —'\u07FF', в якому лежать найбільш розповсюджені символи національних алфавітів, записуються двома байтами наступним чином: символ Unicode з кодуванням 00000хххххуууууу записується як 110ххххх10уууууу. Решта символів Unicode із діапазону '\u0800' —'\UFFFF' записуються трьома байтами за наступним правилом: символ Unicode з кодуванням xxxxyyyyyyzzzzzz записується як 1110xxxx10yyyyyy10zzzzzz. Такий спосіб розподілу бітів дозволяє по першим бітам коду взнати, скільки байтів складає код символа, і правильно відрахувати символи в потоці.
Метод writeUTF(String s) спочатку записує у потік у перші два байти довжини рядка s в кодуванні UTF-8, а потім символи рядка у цьому кодуванні. Читати цей запис потрібно парним методом readUTF() класу DatalnputStream. Клас DatalnputStream перетворює вхідний потік байтів типу InputStream, що містить дані простих типів Java, у дані того ж типу. Такий потік, як правило, створюється методами класу DataOutputstream. Дані із цього потоку можна прочитати методами readBoolean(), readByte(), readShort(), readChar(), readlnt(), readLong(), readFloat(), readDouble(), які повертають дані відповідного типу. Крім того, методи readUnsignedByte() і readUnsignedShort () повертають ціле значення типу int, у якому старші три або два байти нульові, а молодші один або два байти заповнені байтами із вхідного потоку.
Метод readUTF(), подібний до методу writeUTF(), повертає рядок типу string, отриманий із потоку, записаного методом writeUTF(). Ще один, статичний, метод readUTF(Datainput in) робить те ж саме із вхідним потоком in, записаним у кодуванні UTF-8. Цей метод можна застосовувати, не створюючи об’єкт класу DatalnputStream. Програма у лістингу 2.4 записує у файл fib.txt числа Фібоначчі, а потім читає цей файл і виводить його вміст на консоль. Для контролю записувані у файл числа теж виводяться на консоль. На рис. 2.1 показано виведення цієї програми.
Лiстинг 2.4. Введення/виведення даних
import j ava.io.*;
class DataPrWr{
public static void main(String[] args) throws IOException{
DataOutputstream dos = new DataOutputstream (
new FileOutputStream("fib.txt"));
int a = 1, b = 1, с = 1;
for (int k = 0; k < 40; k++){
System.out.print(b + " ");
dos.writelnt(b);
a = b; b = с; с = a + b;
}
dos.closet);
System.out.println("\n");
DatalnputStream dis = new DatalnputStream (
new FilelnputStream("fib.txt")) ;
while(true)
try{
a = dis.readlnt();
System.out.print(a + " ">;
}catch(lOException e){
dis.close();
System.out.println("End of file");
System.exit (0);
}
}
}
Зверніть увагу на те, що спроба читання з кінця файла видає виключення класу IOException, його обробка полягає у закритті файла і закінченні роботи програми.
2.7. ПРЯМИЙ ДОСТУП ДО ФАЙЛА
Якщо необхідно інтенсивно працювати з файлом, записуючи в нього дані різних типів Java, змінюючи їх, шукаючи і читаючи потрібні інформацію, то краще за все скористатися методами класу RandomAccessFile. У конструкторах цього класу
RandomAccessFile(File file, String mode)
RandomAccessFile(String fileName, String mode)
другим аргументом mode задається режим відкриття файла. Це може бути рядок "r" — відкриття файла тільки для читання, або "rw" — відкриття файла для читання і запису. Цей клас зібрав всі потрібні методи роботи з файлом. Він містить всі методи класів DataІnputstream і DataOutputstream, крім того, дозволяє прочитати зразу цілий рядок методом readln() і відшукати потрібні дані у файлі. Байти файла нумеруються, починаючи з 0, подібно елементам масиву. Файл має неявний вказівник на (file pointer) поточну позицію. Читання і запис відбувається, починаючи з поточної позиції файла. При відкритті файла конструктором вказівник стоїть на початку файла, у позиції 0. Біжучу позицію можна взнати методом getFilePointer(). Кожне читання або запис переміщає вказівник на довжину прочитаного або записаного даного. Завжди можна перемістити вказівник в нову позицію pos методом seek (long pos). Метод seek(0) переміщає вказівник на початок файла. У класі немає методів перетворення символів в байти і назад по кодовим таблицям, тому він не пристосований для роботи з кирилицею.
Рис. 2.1. Введення/виведення даних
2.8. КАНАЛИ ОБМІНУ ІНФОРМАЦІЄЮ
У пакеті java.io є чотири класи pipedxxx, які полегшують завдання обміну між каналами. В одному підпроцесі — джерелі інформації — створюється об’єкт класу PipedWriter або PipedOutputstream, в який записується інформація методами write() цих класів. У другому підпроцесі — приймачі інформації — формується об’єкт класу PipedReader або PipedІnputstream. Він зв’язується з об’єктом-джерелом за допомогою конструктора або спеціальним методом connect(), і читає інформацію методами read(). Джерело і приймач можна створити і зв’язати у зворотному порядку.
Так створюється однонаправлений канал (pipe) інформації. На справді це деяка область оперативної пам’яті, до якої організований спільний доступ двох або більше підпроцесів. Доступ синхронізується, і записуючі процеси не можуть завадити читанню. Якщо потрібно організувати двосторонній обмін інформацією, то створюються два канали. У лістингу 2.5 метод run() класу Source генерує інформацію (цілі числа k), і передає їх в канал методом pw. write (k). Метод run() класу Target читає інформацію із канала методом pr.read(). Кінці каналу зв’язуються за допомогою конструктора класу Target. На рис. 2.2 видно послідовність запису і читання інформації.
Лістинг 2.5. Канал обміну інформацією
import java.io.*;
class Target extends Thread{
private PipedReader pr;
Target(PipedWriter pw){
try{
pr = new PipedReader(pw);
}catch(lOException e){
System.err.println("From Target(): " + e);
}
}
PipedReader getStream(){ return pr;}
public void run(){
while(true)
try{
System.out.println("Reading: " + pr.read());
}catch(IOException e){
System.out.println("The job's finished.");
System.exit(0);
}
}
}
class Source extends Thread{
private PipedWriter pw;
Source (){
pw = new PipedWriter();
}
PipedWriter getStream(){ return pw;}
public void run(){
for (int k = 0; k < 10; k++)
try{
pw.write(k);
System.out.println("Writing: " + k);
}catch(Exception e){
System.err.printlnf"From Source.run(): " + e) ;
}
}
}
class PipedPrWr{
public static void main(String[] args){
Source s = new Source();
Target t = new Target(s.getStream());
s.start();
t.start();
}
Рис. 2.3. Дані, які передаються між підпроцесами
2.9. СЕРІАЛІЗАЦІЯ ОБ’ЄКТІВ
Методи класів ObjectlnputStream і ObjectOutputStream дозволяють прочитати із вхідного байтового потоку або записати у вихідний байтовий потік дані складних типів — об’єкти, масиви, рядки — подібно до того, як методи класів Datainputstream і DataOutputstream читають і записують дані простих типів. Схожість підсилюється тим, що класи Objectxxx містить методи як для читання, так і запису простих типів. Ці методи призначені не для використання у програмах, а для запису/читання полів об’єктів і елементів масивів.
Процес запису об’єкта у вихідний потік отримав назву серіалізації (serialization), а читання об’єкта із вхідного потоку і відновлення його в оперативній пам’яті — десеріалізації (deserialization). Серіалізація об’єкта порушує його безпеку, оскільки шкідливий процес може серіалізувати об’єкт в масив, переписати деякі елементи масиву, представляючі private-поля об’єкта, забезпечивши собі, наприклад, доступ до секретного файлу, а потім десеріалізувати об’єкт із зміненими полями і здійснювати з ним неприпустимі дії.
Тому серіалізувати можна не кожний об’єкт, а лише той, який реалізує інтерфейс seriaІizabІe. Цей інтерфейс не містить ні полів, ні методів.
class A implements SeriaiizabІe{...}
це тільки помітка, яка дозволяє серіалізацію класу А. Процес серіалізації є максимально автоматизованим. Досить створити об’єкт класу ObjectOutputStream, зв’язавши його з вихідним потоком, і виводити в цей потік об’єкти методом writeObject():
MyClass me = new MyClass("abc", -12, 5.67e-5);
int[] arr = {10, 20, 30};
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("myobjects.ser")) ;
oos.writeObject(me);
oos.writeObject(arr);
oos.writeObject("Some string");
oos.writeObject (new Date());
oos.flush();
У вихідний потік виводяться всі нестатичні поля