Потоки введення-виведення

ЛЕКЦІЯ 10

Тема: Потоки введення-виведення

План

1. Загальна характеристика потоку в мові Java.
2. Консольне введення-виведення.

3. Файлове введення-виведення.

4. Буферизоване введення-виведення.

5. Прямий доступ до файлу та канали обміну інформацією.

6. Серіалізація об’єктів.

7. Друк в Java 2.

І. Загальна характеристика потоку в мові Java

Для того щоб абстрагуватись від особливостей конкретних пристроїв введення-виведення, в Java використовується поняття потоку (stream). Вважається, що в програму іде вхідний потік символів Unicode або просто байтів, що сприймається в програмі методом read(). Із програми методами write() або print (), println() виводиться вихідний потік символів або байтів. При цьому не має значення, куди направлений потік: на консоль, на принтер, у файл або в мережу, методи write() і print() нічого про це не знають. Звичайно, повне ігнорування особливостей пристроїв введення-виведення сильно сповільнює передачу інформації. Тому в 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.

Класи вихідних потоків 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.

Всі класи пакету java.io можна розділити на дві групи: класи, що створюють потік (data sink), і класи, що управляють потоком (data processing). Класи, що створюють потоки, в свою чергу, можна розділити на п’ять груп:

  1. класи, що створюють потоки, зв’язані з файлами: FileReader, FilelnputStream, FileWriterFile, Outputstream, RandomAccessFile;
  2. класи, що створюють потоки, зв’язані з масивами: CharArrayReader, ByteArraylnputStream, CharArrayWriter, ByteArrayOutputStream;
  3. класи, що створюють канали обміну інформацією між підпроцесами: PipedReader, Pipedlnput-Stream, PipedWriter, PipedOutputStream;
  4. класи, утворюючі символьні потоки, зв’язані з рядком: StringReader, StringWriter;
  5. класи, утворюючі байтові потоки із об’єктів Java: ObjectlnputStream, ObjectOutputStream.

Класи, які здійснюють управління потоком, отримують в своїх конструкторах уже наявний потік і створюють новий, перетворений потік. Їх можна уявити собі як «перехідне кільце», після якого йде труба іншого діаметру. Чотири класи створені спеціально для перетворення потоків: FilterReader, FilterlnputStream, FilterWriter, FilterOutputStream. Самі по собі ці класи не мають ніякої користі – вони виконують тотожне перетворення. Їх потрібно розширювати, перевизначаючи методи введення-виведення.

II. Консольне введення-виведення

Для виведення на консоль ми використовували метод println() класу Printstream, ніколи не визначаючи екземпляри цього класу. Ми просто використовували статичне поле out класу System, яке являється об’єктом класу PrintStream. Виконуюча система Java зв’язує це поле з консоллю. Якщо вам набридло писати system.out.println(), то можна визначити нове посилання на System.out, наприклад:

PrintStream pr = System.out;

і писати просто pr.println().

Консоль являється байтовим пристроєм, і символи Unicode перед виведенням на консоль повинні бути перетворені в байти. Для символів Latin 1 з кодами '\u0000' - '\u00FF' при цьому просто відкидається нульовий старший байт і виводяться байти '0х00' - '0xFF'. Для кодів кирилиці, які лежать в діапазоні '\u0400' – '\u04FF' кодування Unicode, і інших національних алфавітів відбувається перетворення по кодовій таблиці, відповідній установленій на комп’ютері локалі.

Труднощі з відображенням кирилиці виникають, якщо виведення на консоль відбувається в кодуванні, відмінному від локалі. Саме так відбувається в русифікованих версіях MS Windows NT/2000. Звичайно в них встановлюється локаль з кодовою сторінкою СР1251, а виведення на консоль відбувається в кодуванні СР866. В цьому випадку потрібно замінити Printstream, який не може працювати з символьним потоком, на Printwriter і «вставити перехідне кільце» між потоком символів Unicode і потоком байт System.out, що виводяться на консоль, у вигляді об’єкта класу OutputstreamWriter. В конструкторі цього об’єкта належить вказати потрібне кодування, в даному випадку СР866. Все це можна зробити одним оператором:

PrintWriter pw = new PrintWriter(new OutputstreamWriter(System.out, "Cp866",true);

Клас Printstream буферизує вихідний потік. Другий аргумент true його конструктора викликає примусове cкидання вмісту буфера у вихідний потік після кожного виконання методу println(). Для cкидання буферу після кожного print() треба писати flush().

Введення з консолі відбувається методами read() класу Inputstream за допомогою статичного поля in класу System. З консолі йде потік байт, отриманих із scan-кодів клавіатури. Ці байти повинні бути перетворені в символи Unicode такими ж кодовими таблицями, як і при виведенні на консоль. Перетворення йде по тій же схемі – для правильного введення кирилиці зручніше всього визначити екземпляр класу BufferedReader, використовуючи в якості «перехідного кільця» об’єкт класу InputstreamReader:

BufferedReader br = new BufferedReader( new InputstreamReader(System.an,"Cp866"));

Клас BufferedReader перевизначає три методи read() свого суперкласу Reader. Крім того, він містить метод readLine(), який повертає рядок типу String, що містить символи вхідного потоку, починаючи з поточного, і закінчуючи символом '\n' або '\r'. Ці символи-розділювачі не входять в повернений рядок. Якщо у вхідному потоці немає символів, то повертається null. В наступному лістинзі приведена програма, яка ілюструє перечисленні методи консольного введення-виведення:

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);

} } }

Пояснимо дію програми. Перший рядок виводиться потоком System.out. Наступний рядок попередньо перетворений в потік байт, записаних в кодуванні СР866. Потім, після тексту «Посимвольне введення:» з консолі вводяться символи «Україна» і натискається клавіша <Enter>. Кожен введений символ відображається на екрані. Фактичне введення з консолі починається тільки після натискання клавіші <Enter>, тому шо клавіатурне введення буферизується операційною системою. Символи відразу ж після введення відображаються по одному в рядку. Слід також звернути увагу на два порожніх рядки після літери а. Це виведені символи '\n' і '\r', які попали у вхідний потік при натисканні клавіші <Enter>. У них немає ніякого графічного накреслення. Потім натиснута комбінація клавіш <Ctrl>+<Z>. Вона відображається на консолі як «^Z» і означає закінчення клавіатурного введення, завершуючи цикл введення символів. Коди цих клавіш уже не попадають у вхідний потік. Далі, після тексту «Порядкове введення:» з клавіатури набирається рядок «Це рядок» і, вслід за натисканням клавіші <Enter>, заноситься в рядок s. Потім рядок s виводиться знову на консоль. Для закінчення роботи набираємо q і натискуємо клавішу <Enter>.

III. Файлове введення-виведення

Оскільки файли в більшості сучасних операційних систем розуміються як послідовність байт, то для файлового введення-виведення створюються байтові потоки за допомогою класів 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"));

Клас File містить близько сорока методів, що дозволяють взнати різні властивості файлу або каталогу. Перш за все, логічними методами isFile(), isDirectory() можна вияснити, чи являється шлях, вказаний в конструкторі, шляхом до файлу або каталогу. Для каталогу можна отримати його зміст — список імен файлів і підкаталогів — методом list(), повертаючим масив рядків string[]. Можна отримати такий же список у вигляді масиву об’єктів класу File[] методом listFiles(). Можна вибрати із списку тільки деякі файли, реалізувавши інтерфейс FileNameFiiter і звернувшись до методу list(FileNameFilter filter).

Якщо каталог з вказаним в конструкторі шляхом не існує, його можна створити логічним методом mkdir(). Цей метод повертає true, якщо каталог вдалось створити. Логічний метод mkdir() створює ще і всі неіснуючі каталоги, вказані в шляху. Порожній каталог видаляється методом delete(). Для файлу можна отримати його довжину в байтах методом length(), час останньої модифікації в секундах методом 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 на створений файл. Якщо звернутися до методу deleteOnExit(), то по завершенні роботи JVM тимчасовий файл буде знищений.

Декілька методів getxxxo повертають ім’я файлу, ім’я каталогу і інші дані про шлях до файлу. Ці методи корисні в тих випадках, коли посилання на об’єкт класу File повертається іншими методами і потрібні дані про файл. Нарешті, метод toURL() повертає шлях до файлу у формі URL.

IV. Буферизоване введення-виведення

Операції введення-виведення в порівнянні з операціями в оперативній пам’яті виконуються дуже повільно. Для компенсації в оперативній пам’яті виділяється деяка проміжна область — буфер, в якому поступово накопичується інформація. Коли буфер заповнений, його вміст швидко переноситься процесором, буфер очищується і знову заповнюється інформацією. Життєвий приклад буфера — поштова скринька, в якій накопичуються листи. Ми кидаємо в нього листа і йдемо в своїх справах, не очікуючи приїзду поштової машини. Поштова машина періодично очищує поштову скриньку, переносячи відразу велику кількість листів.

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

BufferedReader br = new BufferedReader(isr);

BufferedWriter bw = new BufferedWriter(fw);

Програма лістингу, поданого нижче, читає текстовий файл, написаний в кодуванні СР866, і записує його вміст у файл в кодуванні KOI8_R. При читанні і записі застосовується буферизація. Ім’я вихідного файлу задається в командному рядку параметром args[0], ім’я копії — параметром args[]:

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.");

} }

V. Прямий доступ до файлу та канали обміну інформацією

Якщо необхідно інтенсивно працювати з файлом, записуючи в нього дані різних типів Java, змінюючи їх, відшукуючи і читаючи потрібну інформацію, то краще всього скористатися методами класу RandomAccessFile. В конструкторах цього класу:

RandomAccessFile(File file, String mode)

RandomAccessFile(String fileName, String mode)

другим аргументом mode задається режим відкриття файлу. Це може бути рядок "r" — відкриття файлу тільки для читання, або "rw" — відкриття файлу для читання і запису. Цей клас зібрав всі корисні методи роботи з файлом. Він містить всі методи класів DataІnputstream і DataOutputstream, крім того, дозволяє прочитати відразу цілий рядок методом readln() і відшукати потрібні дані у файлі. Байти файлу нумеруються, починаючи з 0, подібно елементам масиву. Файл має неявний покажчик поточної позиції. Читання і запис відбувається, починаючи з поточної позиції файла. При відкритті файлу конструктором, покажчик стоїть на початку файлу, в позиції 0. Поточну позицію можна взнати методом getFiiePointer(). Кожне читання або запис переміщає покажчик на довжину прочитаного або записаного даного. Завжди можна перемістити покажчик в нову позицію pos методом seek (long pos). Метод seek(0) переміщає покажчик на початок файлу. В класі немає методів перетворення символів в байти і назад по кодовим таблицях, тому він не пристосований для роботи з кирилицею.

Значних зусиль вартує організовувати правильний обмін інформацією між підпроцесами. В пакеті java.io є чотири класи pipedxxx, полегшуючі це завдання. В одному підпроцесі — джерелі інформації — створюється об’єкт класу PipedWriter або PipedOutputstream, в який записується інформація методами write() цих класів. В другому підпроцесі — приймачу інформації — формується об’єкт класу PipedReader або PipedІnputstream. Він зв’язується з об’єктом-джерелом за допомогою конструктора або спеціальним методом connect(), і читає інформацію методами read(). Джерело і приймач можна створити і зв’язати в зворотному порядку.

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

В лістинзі метод run() класу Source генерує інформацію (цілі числа k) і передає їх в канал методом pw. write (k); метод run() класу Target читає інформацію із каналу методом pr.read(); кінці каналу зв’язуються за допомогою конструктора класу Target:

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(); }

VI. Серіалізація об’єктів

Методи класів ObjectlnputStream і ObjectOutputStream дозволяють прочитати із вхідного байтового потоку або записати у вихідний байтовий потік дані складних типів — об’єкти, масиви, рядки — подібно тому, як методи класів Datainputstream і DataOutputstream, що читають і записують дані простих типів. Схожість підсилюється тим, що клас Objectxxx містить методи як для читання, так і запису простих типів. Між іншим, ці методи призначені не для використання в програмах, а для запису-читання полів об’єктів і елементів масивів.

Процес запису об’єкта у вихідний потік отримав назву серіалізації (serialization), а читання об’єкта із вхідного потоку і відновлення його в оперативній пам’яті — десеріалізації (deserialization).

Серіалізація об’єкта порушує його безпеку, оскільки будь-який процес може серіалізувати об’єкт в масив, переписати деякі елементи масиву, представляючі private-поля об’єкта, забезпечивши собі, наприклад, доступ до секретного файлу, а потім десеріалізувати об’єкт із зміненими полями і здійснювати з ним недопустимі дії.

Тому серіалізувати можна не кожний об’єкт, а тільки той, котрий реалізує інтерфейс serializable. Цей інтерфейс не містить ні полів, ні методів. Реалізувати в ньому нема чого. По суті запис:

class A implements SeriaiizabІe{...}

це тільки мітка, дозволяюча серіалізацію класу А. Як завжди в Java, процес серіалізації максимально автоматизований. Досить створити об’єкт класу 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();

У вихідний потік виводяться всі нестатичні поля об’єкта, незалежно від прав доступу до них, а також дані про клас цього об’єкта, необхідні для його правильного відновлення при десеріалізації. Байт-коди методів класу не серіалізуються. Якщо в об’єкті присутні посилки на інші об’єкти, то вони теж серіалізуються, а в них можуть бути посилання на інші об’єкти, котрі знову-таки серіалізуються, і отримується ціла множина зв’язаних між собою серіалізованих об’єктів. Метод writeObject() розпізнає два посилання на один об’єкт і виводить його у вихідний потік тільки один раз. До того ж, він розпізнає посилання, замкнуті в кільце, і уникає зациклювання.

Всі класи об’єктів, що входять в таку серіалізовану множину, а також всі їх внутрішні класи, повинні реалізувати інтерфейс serializable, в протилежному випадку буде відкинуте виключення класу NotseriaІizabІeException і процес серіалізації перерветься.

Клас java.awt.Component реалізує інтерфейс Serializable, а це означає, що всі графічні компоненти можна серіалізувати. Не реалізують цей інтерфейс звичайно класи, тісно пов’язані з виконанням програм, наприклад, java.awt.Toolkit. Стан екземплярів таких класів немає рації зберігати або передавати по мережі. Не реалізують інтерфейс Serializable і класи, що містять внутрішні дані Java «для службового користування». Десеріалізація відбувається так же просто, як і серіалізація:

ObjectlnputStream ois = new ObjectInputStream

(new FilelnputStream("myobjects.ser"));

MyClass mcl = (MyClass)ois.readObject();

int[] a = (int[])ois.readObject();

String s = (String)ois.readObject();

Date d = (Date)ois.readObject();

Потрібно тільки додержуватися порядку читання елементів потоку. В лістинзі, поданому нижче, створюється об’єкт класу GregorianCaІendar з поточною датою і часом, серіалізується його у файл date.ser, через три секунди десеріалізується і зрівнюється з поточним часом:

import java.io.*;

import java.util.*;

class SerDatef

public static void main(String[] args) throws Exception{

GregorianCaiendar d - new GregorianCaiendar();

QbjectOutputStream oos = new ObjectOutputStream{

new FileOutputStream("date.ser"));

oos.writeObject(d);

oos.flush();

oos.close();

Thread.sleep(3000);

ObjectlnputStream ois = new ObjectlnputStream(

new FileInputStream("date.ser"));

GregorianCaiendar oldDate = (GregorianCaiendar)ois.readObject();

ois.close();

GregorianCaiendar newDate = new GregorianCaiendar();

System.out.println("Old time = " +

oldDate.get(Calendar.HOUR) + ":" +

oldDate.get(Calendar.MINUTE) +":" +

oldDate.get(Calendar.SECOND) +"\nNew time = " +

newDate.get(Calendar.HOUR) +":" +

newDate.get(Calendar.MINUTE) +":" +

newDate.get(Calendar.SECOND)); }}

Якщо не потрібно серіалізувати якесь поле, то достатньо помітити його службовим словом transient, наприклад:

transient MyClass me = new MyClass("abc", -12, 5.67e-5);

Метод writeObject() не записує у вихідний потік поля, помічені static і transient. Взагалі ж, цю ситуацію можна змінити, перевизначивши метод writeObject() або задавши список серіалізованих полів. Процес серіалізації навіть можна повністю налагодити під свої потреби, перевизначивши методи введення-виведення і скористувавшись допоміжними класами. Можна також взяти весь процес на себе, реалізувавши не інтерфейс Serializable, а інтерфейс Externalizable, але тоді прийдеться реалізувати методи readExternal() і writeExternal(), виконуючі введення-виведення.

VII. Друк в Java 2

Оскільки принтер — пристрій графічний, то виведення на друк дуже схоже на виведення графічних об’єктів на екран. Тому в Java засоби друку входять в графічну бібліотеку AWT і в систему Java 2D. В графічному компоненті крім графічного контексту — об’єкта класу Graphics, створюється ще «друкарський контекст». Це теж об’єкт класу Graphics, реалізуючий інтерфейс printGraphics і отриманий із іншого джерела — об’єкта класу printjob, що входить в пакет java.awt. Сам же цей об’єкт створюється за допомогою класу Toolkit пакету java.awt. На практиці це виглядає так:

PrintJob pj = getToolkitO.get,Print Job (this, "Job Title", null);

Graphics pg = pj.getGraphics();

Метод getPrintJob() спочатку виводить на екран стандартне вікно Print операційної системи. Коли користувач вибере в цьому вікні параметри друку і почне друк кнопкою ОК, створюється об’єкт pj. Якщо користувач відмовляється від друку кнопкою Cancel, то метод повертає null. В класі Toolkit два методи getPrint Job ():

getPrintJob(Frame frame,String jobTitle,JobAttributes jobAttr, PageAttributes pageAttr)

getPrintJob(Frame frame, String jobTitle, Properties prop)

Коротко охарактеризуємо аргументи методів:

  • аргумент frame вказує на вікно верхнього рівня, керуюче друком. Цей аргумент не може бути null. Рядок jobTitle задає заголовок завдання, який не друкується, і може бути рівним null;
  • аргумент prop залежить від реалізації системи друку (часто це просто null). В даному випадку задаються стандартні параметри друку;
  • аргумент jobAttr задає параметри друку. Клас JobAttributes, екземпляром якого являється цей аргумент, має складну будову. В ньому п’ять підкласів, які містять статичні константи — параметри друку, котрі використовуються в конструкторі класу. Є також конструктор по замовчуванню, задаючий стандартні параметри друку;
  • аргумент pageAttr задає параметри сторінки. Клас pageProperties теж містить п’ять підкласів із статичними константами, котрі і задають параметри сторінки і використовуються в конструкторі класу. Якщо для друку досить стандартних параметрів, то можна скористатися конструктором по замовчуванню.

Після того як «друкарський контекст» — об’єкт pg класу Graphics — визначений, можна викликати метод print(pg) або printAll(pg) класу Component. Цей метод встановлює зв’язок з принтером по замовчуванню і викликає метод paint(pg). На друк виводиться все те, що задано цим методом. Наприклад, щоб роздрукувати текстовий файл, потрібно в процесі введення розбити його текст на рядки і в методі paint(pg) вивести рядки методом pg.drawstring(). При цьому слід врахувати, що в «друкарському контексті» немає шрифту по замовчуванню, завжди потрібно встановлювати шрифт методом pg.setFont().

Після виконання всіх методів print() застосовується метод pg.dispose(), викликаючиий перебіг сторінки, і метод pj.end(), закінчуючий друк.

Розширена графічна система Java 2D пропонує нові інтерфейси і класи для друку, зібрані в пакет java.awt.print. Ці класи повністю перекривають всі стандартні можливості друку бібліотеки AWT. Більше того, вони зручніші в роботі і пропонують додаткові можливості. Як і стандартні засоби AWT, методи класів Java 2D виводять на друк вміст графічного контексту, заповненого методами класу Graphics або класу Graphics2D.

Будь-який клас Java 2D, що збирається друкувати хоча б одну сторінку тексту, графіки або зображення називається класом, малюючим сторінки. Такий клас повинен реалізувати інтерфейс Printable. В цьому інтерфейсі описані дві константи і тільки один метод print(). Клас, малюючий сторінки, повинен реалізовувати цей метод. Метод print() повертає ціле типу int і має три аргументи:

print(Graphics g, PageFormat pf, int ind);

Перший аргумент g — це графічний контекст, що виводиться на лист паперу. Другий аргумент pf — екземпляр класу PageFormat, визначаючий розмір і орієнтацію сторінки. Третій аргумент ind — порядковий номер сторінки, що відраховується від нуля. Метод print() класу, малюючого сторінки, заміняє собою метод paint(), що використовується стандартними засобами друку AWT. Клас, малюючий сторінки, не зобов’язаний розширювати клас Frame і перевизначати метод paint(). Все заповнення графічного контексту методами класу Graphics або Graphics2D тепер виконується в методі print(). Коли друк сторінки буде закінчено, метод print() повинен повернути ціле значення, задане константою PAGE_EXISTS. Буде зроблено повторне звернення до методу print() для друку наступної сторінки. Аргумент ind при цьому зростає на 1. Коли ind перевищить кількість сторінок, метод print() повинен повернути значення NO_SUCH_PAGE, що служить сигналом закінчення друку.

Слід пам’ятати, що система друку може декілька раз звернутися до методу paint() для друку однієї і тієї ж сторінки. При цьому аргумент ind не змінюється, а метод print() повинен створити той же графічний контекст.

Клас PageFormat визначає параметри сторінки. На сторінці вводиться система координат з одиницею довжини 1/72 дюйма, початок якої і напрям осей визначається однією із трьох констант:

  • PORTRAIT – початок координат розташовано в лiвому верхньому куті сторінки, вісь Ох направлена вправо, вісь Оу – вниз;
  • LANDSCAPE – початок координат в лівому нижньому куті, вісь Ох йде вверх, вісь Оу – вправо;
  • REVERSE_LANDSCAPE – початок координат в правому верхньому куті, вісь Ох йде вниз, вісь Оу – вліво.

Більшість принтерів не може друкувати без полів, на всій сторінці, а здійснює виведення тільки в деяку область друку, координати лівого верхнього кута якої повертаються методами getІmageabІeХ() і getlmageableY(), а ширина і висота – методами getlmageableWidth() і getlmageableHeight(). Ці значення треба враховувати при розташуванні елементів в графічному контексті, наприклад, при розміщенні рядків тексту методом drawstring(). В класі тільки один конструктор по замовчуванню PageFormat(), задаючий стандартні параметри сторінки, визначені для принтера по замовчуванню обчислювальною системою.

Метод pageDiaІog(PageDiaiog pd) відкриває на екрані стандартне вікно Параметри сторінки операційної системи, в якому уже задані параметри, визначені в об’єкті pd. Якщо користувач вибрав у цьому вікні кнопку Відміна, то повертається посилання на об’єкт pd, якщо кнопку ОК, то створюється і повертається посилання на новий об’єкт. Об’єкт pd в будь-якому випадку не змінюється. Він звичайно створюється конструктором. Можна задати параметри сторінки і із програми, але тоді слід спочатку визначити об’єкт класу Paper конструктором по замовчуванню:

Paper р = new Paper()

Потім наступними методами задається розмір сторінки і області друку:

p.setSize(double width, double height)

p.setlmageableArea(double x, double y, double width, double height)

Далі визначається об’єкт класу pageFormat з параметрами по замовчуванню:

PageFormat pf = new PageFormat()

і задаються нові параметри методом: pf.setPaper(p).

Після цього викликати на екран вікно Параметри сторінки методом pageDiaІog() не обов’язково, оскільки ми отримаємо мовчазний процес друку. Так робиться в тих випадках, коли друк виконується в фоновому режимі окремим підпроцесом. Далі треба дати завдання на друк (print job) — вказати кількість сторінок, їх номери, порядок друку сторінок, кількість копій. Всі ці дані збираються в класі PrinterJob.

Система друку Java 2D розрізняє два види завдань. В більшості простих завдань — Printable Job — є тільки один клас, малюючий сторінки, тому у всіх сторінок одні й ті ж параметри, сторінки друкуються послідовно з першої по останню або з останньої сторінки по першу, це залежить від системи друку. Другий, більш складний вид завдань — Pageable Job — визначає для друку кожної сторінки свій клас, малюючий сторінки. Тому у кожної сторінки можуть бути власні параметри. Крім того, можна друкувати не все, а тільки вибрані сторінки, виводити їх в зворотному порядку, друкувати на обох сторонах листа. Для здійснення цих можливостей визначається екземпляр класу Book або створюється клас, реалізуючий інтерфейс Pageable. В класі Book є також один конструктор, створюючий порожній об’єкт:

Book b = new Book()

Після створення в даний об’єкт додаються класи, малюючі сторінки. Для цього в класі Book є два методи:

  • append (Printable p, PageFormat pf) —додає об’єкт р в кінець;
  • append(Printable p, PageFormat pf, int numPages) — додає numPages екземплярів р в кінець.

Якщо кількість сторінок невідома, то задається константа UNKNOWN_ NUMBER_OF_PAGES.

Залишається задати число копій (якщо воно більше 1), методом setСopies(int n) і завдання сформовано. Ще один корисний метод defaultPage() класу PrinterJob повертає об’єкт класу PageFormat по замовчуванню. Цей метод можна використовувати замість конструктора класу PageFormat.

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

Потоки введення-виведення