Потоки введення-виведення
ЛЕКЦІЯ 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). Класи, що створюють потоки, в свою чергу, можна розділити на пять груп:
- класи, що створюють потоки, звязані з файлами: FileReader, FilelnputStream, FileWriterFile, Outputstream, RandomAccessFile;
- класи, що створюють потоки, звязані з масивами: CharArrayReader, ByteArraylnputStream, CharArrayWriter, ByteArrayOutputStream;
- класи, що створюють канали обміну інформацією між підпроцесами: PipedReader, Pipedlnput-Stream, PipedWriter, PipedOutputStream;
- класи, утворюючі символьні потоки, звязані з рядком: StringReader, StringWriter;
- класи, утворюючі байтові потоки із обєктів 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(). При цьому необхідо прослідкувати за правильним розміщенням рядків в області друку і розбиттям файлу на сторінці.
Потоки введення-виведення