Макар и да има множество IO потокови класове в библиотеката, които могат да бъдат комбинирани по много начини, има само няколко, които ще се случи вероятно да използвате. Те изискват внимание при подбора на коректните комбинации, обаче. Следващия малко дълъг пример показва използването на типични IO конфигурации така че може да го ицползвате за справки като пишете собствен код. Забележете че всяка комбинация започва с изкоментиран номер и заглавие, което кореспондира на това от обясненията, които следват текста.
//: c10:IOStreamDemo.java
// Typical IO Stream Configurations
import java.io.*;
import com.bruceeckel.tools.*;
public class IOStreamDemo {
public static void main(String[] args) {
try {
// 1. Buffered input file
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream(args[0])));
String s, s2 = new String();
while((s = in.readLine())!= null)
s2 += s + "\n";
in.close();
// 2. Input from memory
StringBufferInputStream in2 =
new StringBufferInputStream(s2);
int c;
while((c = in2.read()) != -1)
System.out.print((char)c);
// 3. Formatted memory input
try {
DataInputStream in3 =
new DataInputStream(
new StringBufferInputStream(s2));
while(true)
System.out.print((char)in3.readByte());
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 4. Line numbering & file output
try {
LineNumberInputStream li =
new LineNumberInputStream(
new StringBufferInputStream(s2));
DataInputStream in4 =
new DataInputStream(li);
PrintStream out1 =
new PrintStream(
new BufferedOutputStream(
new FileOutputStream(
"IODemo.out")));
while((s = in4.readLine()) != null )
out1.println(
"Line " + li.getLineNumber() + s);
out1.close(); // finalize() not reliable!
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 5. Storing & recovering data
try {
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out2.writeBytes(
"Here's the value of pi: \n");
out2.writeDouble(3.14159);
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
System.out.println(in5.readLine());
System.out.println(in5.readDouble());
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 6. Reading/writing random access files
RandomAccessFile rf =
new RandomAccessFile("rtest.dat", "rw");
for(int i = 0; i < 10; i++)
rf.writeDouble(i*1.414);
rf.close();
rf =
new RandomAccessFile("rtest.dat", "rw");
rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();
rf =
new RandomAccessFile("rtest.dat", "r");
for(int i = 0; i < 10; i++)
System.out.println(
"Value " + i + ": " +
rf.readDouble());
rf.close();
// 7. File input shorthand
InFile in6 = new InFile(args[0]);
String s3 = new String();
System.out.println(
"First line in file: " +
in6.readLine());
in6.close();
// 8. Formatted file output shorthand
PrintFile out3 = new PrintFile("Data2.txt");
out3.print("Test of PrintFile");
out3.close();
// 9. Data file output shorthand
OutFile out4 = new OutFile("Data3.txt");
out4.writeBytes("Test of outDataFile\n\r");
out4.writeChars("Test of outDataFile\n\r");
out4.close();
} catch(FileNotFoundException e) {
System.out.println(
"File Not Found:" + args[0]);
} catch(IOException e) {
System.out.println("IO Exception");
}
}
} ///:~
Разбира се, едно нещо което много се използва е извеждането на форматирани данни на конзолата, но това беше вече разгледано в пакета com.bruceeckel.tools създаден в глава 5.
Части 1 до 4 демонстрират създаването и използването на входни потоци (макар и част 4 също да показва и прост начин на използване на изхозен поток като инструмент за тестване).
1. Буфериран вход от файл
За да отворите файл за вход използвате FileInputStream със String или File обект като име на файл. За скорост бихте желали входът да бъде буфериран така че давате получения манипулатор на BufferedInputStream. За да се чете входът форматиран, давате получения манипулатор на конструктора на DataInputStream, който е вашият краен обект и от чийто интерфейс четете.
В този пример само readLine( ) метода се използва, но разбира се налице са всичките методи на DataInputStream. Като достигнете края на файла, readLine( ) връща null и то се използва така, че се прекъсва while цикъла.
String-ът s2 се използва за събиране на цялото съдържание на файла (включително знаците за нов ред които трябва да се добавят, понеже readLine( ) ги маха). s2 после се използва в други части на програмата. Накрая се вика close( ) за да затвори файла. Технически, close( ) ще се извика когато се пусне finalize( ) и това се предполага да се случи (независимо дали ще има събиране на боклука) при завършването на програмата. Обаче Java 1.0 има един важен бъг, така че това няма да се случи. В Java 1.1 трябва явно да се извика System.runFinalizersOnExit(true) за да се гарантира, че finalize( ) ще се извика за всеки обект в системата. Най-безопасният подход е явно да се вика close( ) за файлове.
2. Вход от паметта
Това парче взема String s2 който сега съдържа целия файл и го използва за създаване на StringBufferInputStream. (String, не StringBuffer, се изисква за аргумент на конструктора.) Тогава read( ) се използва за четене на знаците един по един и изпращането им на конзолата. Забележете че read( ) връща следващия байт като int и трябва да се превърне в char за правилно извеждане.
3. Форматиран вход от паметта
Интерфейсът на StringBufferInputStream е огпаничен, така че обикновено го разширяваме чрез обгръщане в DataInputStream. Обаче ако сте избрали да прочитате байт по байт чрез readByte( ), всяка стойност е валидна и връщаната стойност не може да се използва за определяне края на въвеждането. Вместо това може да се използва метода available( ) за намиране колко знака остават. Ето пример който показва как да се чете файл байт по байт:
//: c10:TestEOF.java
// Testing for the end of file while reading
// a byte at a time.
import java.io.*;
public class TestEOF {
public static void main(String[] args) {
try {
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("TestEof.java")));
while(in.available() != 0)
System.out.print((char)in.readByte());
} catch (IOException e) {
System.err.println("IOException");
}
}
} ///:~
Забележете че available( ) работи различно според това какъв е видът на медията с която се работи – буквално “броят байтове който може да се прочете не блоково.” С файл това значи целия файл, но с други видове устройства това може да не е така, така че го използвайте внимателно.
Освен това бихте могли да хванете края на файла в подобни случаи чрез изключение. Обаче използването на изключения за управление хода на програмата се счита за неправилно тяхно използване.
4. Номериране на редове и файлов изход
Този пример показва използването на LineNumberInputStream за да се следят номерата на редовете. Тук не може просто да се съберат всичките конструктори заедно, понеже трябва да се пази манипулатор към LineNumberInputStream. (Забележете че това не е ситуация на наследяване, така че не може да се направи просто кастинг на in4 към LineNumberInputStream.) Така, li съдържа манипулатор към LineNumberInputStream, който после се използва за създаване на DataInputStream за лесно четене.
Този пример също показва писането на форматирани данни във файл. Първо се създава FileOutputStream за свързване с файла. За ефективност е направен BufferedOutputStream, което почти винаги ще е желания случай, но се налага да го правите явно. После за форматирането той е превърнат в PrintStream. Данновия файл създаден по този начин е четим като обикновен текстов файл.
Един от методите който показва че DataInputStream е изтощен е readLine( ), който връща null когато повече няма стрингове за четене. Всеки ред се записва във файла заедно с номера си, който се следи чрез li.
Ще видите явен close( ) за out1, което би имало смисъл ако програмата щеше да се върне и да чете файла отново. Тази програма обаче завършва без да се обръща към файла IODemo.out. Както се спомена преди, ако не извикате close( ) за всички ваши изходни файлове, бихте могли да откриете че буферът не е записан и файлът не е завършен.
Изходни потоци
Двата основни вида изходни потоци се различават по начина на записване на данните: при единия са предназначени за хората, а другият ги пише за да може да бъдат възстановени от DataInputStream. RandomAccessFile е отделно, макар и форматът на данните при него да е съвместим с DataInputStream и DataOutputStream.
5. Запазване и възстановяване на данни
PrintStream форматира данните така, че са разбираеми за човек. За извеждане на данни които да могат да бъдат възстановени (прочетени) от друг поток използваме DataOutputStream за запис на данните и DataInputStream за възстановяването им. Разбира се, тези потоци биха могли да бъдат всякакви, но тук се използва файл.
Забележете че знаковият низ е написан чрез writeBytes( ) а не чрез writeChars( ). Ако използвате последния бихте писали 16-битовите Unicode знаци. Тъй като няма съответен “readChars” метод в DataInputStream, ограничени сте да вземате знаците един по един чрез readChar( ). Така за ASCII, по-лесно е да пишете знаците като байтове следвани от знак за нов ред; после чрез readLine( ) да четете байтовете обратно като обикновен ASCII ред.
writeDouble( ) запомня double число в потока и съответно readDouble( ) го възстановява. Но за всеки от методите за четене, за да работи коректно, трябва точно да знаете разположението на данната в потока, тъй като е напълно възможно да четете double и като проста последователност от байтове, или char и т.н.. Така че трябва или да има фиксиран формат на файловете или в тях да се помни допълнително нужната информация за структурата им.
6. Четене и писане на файлове с произволен достъп
Както бе отбелязано преди, RandomAccessFile е почти напълно изолиран от останалата част на IO йерархията, освен че прилага интерфейсите на DataInput и DataOutput. Така че не може да го комбинирате в никакъв аспект на InputStream и OutputStream подкласовете. Макар и да има смисъл да се третира ByteArrayInputStream като елемент с произволен достъп, може да използвате RandomAccessFile само да отвори файл. Трябва да предполагате че RandomAccessFile е правилно буфериран, понеже не може да добавите тази черта.
Едничкият избор който имате е аргументът на вторият конструктор: може да отворите RandomAccessFile за четене (“r”) или четене и писане (“rw”).
Използването на RandomAccessFile е подобно на използването на комбинирани DataInputStream и DataOutputStream (понеже прилага еквивалентни интерфейси). Освен това може да видите, че seek( ) се използва за разходки по файла и промяна на една от стойностите.
Тъй като има няколко типични форми, които се използват често с фойлове, бихте могли да се чудите защо трябва да се пише толкова много код (всеки път-б.пр.) – това е един от недостатъците на декораторския подход. Тази част показва създаването и използването на съкратени процедури за четене и писане от файлове. Тези неща са сложени в пакета package com.bruceeckel.tools който беше започнат в глава 5 (Виж стр. 186). За да се добави всеки клас в библиотеката, просто го сложете в съответната директория и добавете оператор package.
7. Кратък начин за вход от файл
Създаването на обект чете файл от буфериран DataInputStream може да бъде капсулирано в клас наречен InFile:
//: com:bruceeckel:tools:InFile.java
// Shorthand class for opening an input file
package com.bruceeckel.tools;
import java.io.*;
public class InFile extends DataInputStream {
public InFile(String filename)
throws FileNotFoundException {
super(
new BufferedInputStream(
new FileInputStream(filename)));
}
public InFile(File file)
throws FileNotFoundException {
this(file.getPath());
}
} ///:~
И двете версии на String на конструктора и File версиите са включени, с цел да се прокара паралел със създаването на FileInputStream.
Сега може да намалите вероятността от повтарящ се стрес, както се вижда в примера.
Същият подход може да се приеме за създаване на PrintStream който пише в буфериран файл. Ето разширението за com.bruceeckel.tools:
//: com:bruceeckel:tools:PrintFile.java
// Shorthand class for opening an output file
// for human-readable output.
package com.bruceeckel.tools;
import java.io.*;
public class PrintFile extends PrintStream {
public PrintFile(String filename)
throws IOException {
super(
new BufferedOutputStream(
new FileOutputStream(filename)));
}
public PrintFile(File file)
throws IOException {
this(file.getPath());
}
} ///:~
Забележете че не е възможно конструктор да хване изключение, изхвърлено то конструктора на базовия клас.
9. Начин за писане в даннов файл
Накрая, същия вид кратък път може да се използва за запазване на данни (като противоположност на читаемите от човек):
//: com:bruceeckel:tools:OutFile.java
// Shorthand class for opening an output file
// for data storage.
package com.bruceeckel.tools;
import java.io.*;
public class OutFile extends DataOutputStream {
public OutFile(String filename)
throws IOException {
super(
new BufferedOutputStream(
new FileOutputStream(filename)));
}
public OutFile(File file)
throws IOException {
this(file.getPath());
}
} ///:~
Любопитно е (и е за нещастие) че проектантите на Java библиотеката не счетоха за необходимо да добавят тези удобства в техния стандарт.
Четене от стандартния вход
Следвайки подхода започнат от Unix за “стандартен вход,” “стандартен изход,” и “стандартен изход за грешки,” Java има System.in, System.out и System.err. По протежение на книгата сте виждали как се извежда на System.out, което е вече обгърнато от PrintStream обект. System.err е подобен на PrintStream, но System.in е "суров" InputStream, без обгръщането. Това значи, че докато можете да използвате System.out и System.err, System.in трябва да бъде обгърнат (с друг обект - б.пр.) преди да го използвате.
Типично ще искате да се чете по ред на един път с readLine( ), така че ще трябва да обгърнете System.in с DataInputStream. Този е “старие” начин на Java 1.0 за вход ред по ред. Малко по късно в главата ще видите решението в Java 1.1. Ето пример, който прави "ехо" на всеки въведен ред:
//: c10:Echo.java
// How to read from standard input
import java.io.*;
public class Echo {
public static void main(String[] args) {
DataInputStream in =
new DataInputStream(
new BufferedInputStream(System.in));
String s;
try {
while((s = in.readLine()).length() != 0)
System.out.println(s);
// An empty line terminates the program
} catch(IOException e) {
e.printStackTrace();
}
}
} ///:~
Причината да има try блок е че readLine( ) може да изхвърли IOException. Забележете че System.in трябва също да бъде буфериран, както с повечето потоци.
Не е много удобно че сте принудени да обгръщате System.in в DataInputStream във всяка програма, но може би е избран този начин заради максимална гъвкавост.
Скачени потоци
PipedInputStream и PipedOutputStream само накъсо се споменаха в тази глава. Това не е защото не са полезни, а защото ползата от тях не е явна докато не започнете да разбирате многонишковостта, понеже свързаните потоци се използват за връзка между нишките. Това е изяснено чрез пример в глава 14.
Сподели с приятели: |