IO потоци на Java 1.1
В тази точка може да започнете да си блъскате главата, чудейки се дали има друг възможен дизайн на IO потоци който би изисквал повече писане. Би ли могъл някой да направи по-неподходящ дизайн?” Пригответе се: Java 1.1 прави някои значителни промени в IO потоковата библиотека. Като видите Reader и Writer класовете първо мислите (както и аз) че може би те са предназначени да заменят класовете InputStream и OutputStream. Но не е така. Макар и някои аспекти на старата библиотека да се смятат за остарели (ако ги използвате ще получите предупреждение от компилатора), старите потоци са оставени заради обратната съвместимост и понеже:
-
Нови класове са били сложени в старата йерархия, та очевидно Sun не изоставя старите потоци.
-
По някой път се предполага да се използват класовете от старата йерархия в комбинация с класовете от новата йерархия и за да се направи това има “мостови” класове: InputStreamReader преобразува InputStream в Reader и OutputStreamWriter преобразува OutputStream в Writer.
Като резултат има ситуации където са налице повече нива на обгръщане с новата IO потокова библиотека отколкото старата. Отново, това е недостатък на подхода с декораторите – цената, която се плаща заради повишената гъвкавост.
Най-важният мотив за добавяне на Reader и Writer йерархиите в Java 1.1 е интернационализацията. Старата IO потокова йерархия поддържа само 8-битови знакови потоци и не работи с 16-битови Unicode знаци добре. Понеже Unicode се използва за интернационализация (и естественият за Java char е 16-битов Unicode), Reader и Writer йерархиите бяха добавени за поддръжка на Unicode във всичките IO операции. Освен това новите библиотеки са проектирани по-бързи от старите.
Каквато е практиката в тази книга, аз ще се опитам да дам общ поглед върху класовете, като за пълните подробности и изчерпателен списък на методите ще използвате онлайн документацията.
Източници и стоци на данни
Почти всичките IO потокови класове на Java 1.0 имат съответни Java 1.1 класове за да осигурят естествена Unicode манипулация. Би било най-лесно да кажем “Използвайте винаги новите класове и никога старите,” но нещата не са толкова прости. Понякога се налага да се използват Java 1.0 IO потоковите класове поради устройството на библиотеката; в частност, java.util.zip библиотеките са нови неща към старата библиотека и те разчитат на компоненти от старите потоци. Така че най-смисления подход е да се опитаме да използваме Reader и Writer където е възможно, а ще се случи и да се върнем къъм старите библиотеки понеже иначе кодът няма да се компилира.
Ето таблица която показва съответствието между източниците и стоците на информация (тоест, местата откъдето физически идва и където физически отива информацията) в старите и новите библиотеки.
-
Източници и стоци:
Java 1.0 клас
|
Съответен Java 1.1 клас
|
InputStream
|
Reader
преобразувател: InputStreamReader
|
OutputStream
|
Writer
преобразувател: OutputStreamWriter
|
FileInputStream
|
FileReader
|
FileOutputStream
|
FileWriter
|
StringBufferInputStream
|
StringReader
|
(няма съответен клас)
|
StringWriter
|
ByteArrayInputStream
|
CharArrayReader
|
ByteArrayOutputStream
|
CharArrayWriter
|
PipedInputStream
|
PipedReader
|
PipedOutputStream
|
PipedWriter
|
Изобщо, ще откриете че интерфейсите в новите и старите библиотеки много си приличат, ако не са еднакви.
Променяне на поведението на потоците
В Java 1.0 потоците бяха приспособявани към конкретни нужди чрез използване на "декораторните" класове FilterInputStream и FilterOutputStream. С IO потоците в Java 1.1 тази идея продължава да се прилага, но не се следва извличането на всички декоратори от единствен базов "филтърен" клас. Това може да доведе до смущения ако се опитвате да разберете библиотеката гледайки йерархията на класовете.
В следващата таблица съответствието е по-грубо отколкото в предишната. Разликата се дължи на организацията на класовете: докато BufferedOutputStream е подклас на FilterOutputStream, BufferedWriter не е подклас на FilterWriter (който, макар че е abstract, няма подкласове и затова изглежда да е поставен за запазване на място или просто да не се чудите къде се е дянал). Обаче интерфейсите са достатъчно близки и е очевидно, че се очаква да използвате новите версии навсякъде, където е възможно (тоест освен в класовете, където се налага да се направи Stream вместо Reader или Writer).
-
Филтри:
Java 1.0 клас
|
Съответен Java 1.1 клас
|
FilterInputStream
|
FilterReader
|
FilterOutputStream
|
FilterWriter (abstract клас без подкласове)
|
BufferedInputStream
|
BufferedReader
(също има readLine( ))
|
BufferedOutputStream
|
BufferedWriter
|
DataInputStream
|
използвай DataInputStream
(Освен когато искате да използвате readLine( ), та ще трябва BufferedReader)
|
PrintStream
|
PrintWriter
|
LineNumberInputStream
|
LineNumberReader
|
StreamTokenizer
|
StreamTokenizer
(използвайте конструктор който взема Reader)
|
PushBackInputStream
|
PushBackReader
|
Има една насока която е доста ясна: Навсякъде където искате да използвате readLine( ), няма да го правите с DataInputStream повече (това се отбелязва със съобщение от компилатора), а с BufferedReader. Извън това DataInputStream е все още “предпочитания” член на Java 1.1 IO библиотеката.
За да се направи преходът PrintWriter по-лесен, той има конструктори които приемат OutputStream обект. Обаче PrintWriter повече не поддържа форматирането което прави PrintStream; интерфейсите са практически същите.
Непроменени класове
Види се проектантите на Java библиотеката са чувствали, че някои класове са направени както трябва от самото начало и те са оставени без промяна:
-
Java 1.0 без съответни Java 1.1 класове
|
DataOutputStream
|
File
|
RandomAccessFile
|
SequenceInputStream
|
DataOutputStream, в частност, се използва без промяна, токо че за запомняне и четене на данни в транспортируем формат сте принудени да стоите в InputStream и OutputStream йерархиите.
Пример
За да видим ефекта от новите класове, нека да погледнем подходяща част от IOStreamDemo.java примера променен да използва класовете Reader и Writer:
//: c10:NewIODemo.java
// Java 1.1 IO typical usage
import java.io.*;
public class NewIODemo {
public static void main(String[] args) {
try {
// 1. Reading input by lines:
BufferedReader in =
new BufferedReader(
new FileReader(args[0]));
String s, s2 = new String();
while((s = in.readLine())!= null)
s2 += s + "\n";
in.close();
// 1b. Reading standard input:
BufferedReader stdin =
new BufferedReader(
new InputStreamReader(System.in));
System.out.print("Enter a line:");
System.out.println(stdin.readLine());
// 2. Input from memory
StringReader in2 = new StringReader(s2);
int c;
while((c = in2.read()) != -1)
System.out.print((char)c);
// 3. Formatted memory input
try {
DataInputStream in3 =
new DataInputStream(
// Oops: must use deprecated class:
new StringBufferInputStream(s2));
while(true)
System.out.print((char)in3.readByte());
} catch(EOFException e) {
System.out.println("End of stream");
}
// 4. Line numbering & file output
try {
LineNumberReader li =
new LineNumberReader(
new StringReader(s2));
BufferedReader in4 =
new BufferedReader(li);
PrintWriter out1 =
new PrintWriter(
new BufferedWriter(
new FileWriter("IODemo.out")));
while((s = in4.readLine()) != null )
out1.println(
"Line " + li.getLineNumber() + s);
out1.close();
} catch(EOFException e) {
System.out.println("End of stream");
}
// 5. Storing & recovering data
try {
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out2.writeDouble(3.14159);
out2.writeBytes("That was pi");
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
BufferedReader in5br =
new BufferedReader(
new InputStreamReader(in5));
// Must use DataInputStream for data:
System.out.println(in5.readDouble());
// Can now use the "proper" readLine():
System.out.println(in5br.readLine());
} catch(EOFException e) {
System.out.println("End of stream");
}
// 6. Reading and writing random access
// files is the same as before.
// (not repeated here)
} catch(FileNotFoundException e) {
System.out.println(
"File Not Found:" + args[1]);
} catch(IOException e) {
System.out.println("IO Exception");
}
}
} ///:~
Изобщо ще видите, че преправянето е доста праволинейно и кодовете доста си приличат. Все пак има важни разлики, обаче. Преди всичко, понеже файловете с произволен достъп не са променени,секция 6 не е повторена.
Секция 1 се свива малко, понеже ако искате само да четете трябва само да обгърнете FileReader с BufferedReader. Секция 1b показва новия начин да се направи System.in за четене от конзолата, а той се разширява защото System.in е DataInputStream и BufferedReader иска Reader аргумент, та InputStreamReader се въвлича за изпълнение на транслацията.
В секция 2 може да се види че ако имате String и искате да четете от него просто използвате StringReader вместо StringBufferInputStream и останалото е същото.
Секция 3 показва бъг в новата IO потокова библиотека. Ако имате String и искате да четете от него, не се очаква да използвате StringBufferInputStream повече. Когато компилирате код с StringBufferInputStream конструктор получавате съобщение, което казва да не го използвате повече. Вместо това се очаква да използвате StringReader. Обаче ако искате да правите форматилан вход от паметта както в секция 3, налага се да използвате DataInputStream – няма “DataReader” да го замести – и DataInputStream конструкторът изисква InputStream аргумент. Така че нямате избор освен използването на остарелия StringBufferInputStream клас. Компилаторът предупреждава че използвате остарял клас, но няма какво друго да се направи.2
Секция 4 е достатъчно праволинейно преработена от старата библиотека за новата, без изненади. В секция 5 се налага да използвате всичките стари потокови класове понеже DataOutputStream и DataInputStream ги изискват и няма алтернативи. Не получавате никакво съобщение по време на компилация. Ако потокът е остарял (ще се изоставя - б.пр.) неговият конструктор обикновено предизвиква предупреждение по време на компилация, но в DataInputStream само методът readLine( ) е остарял, понеже се очаква да използвате BufferedReader за readLine( ) (но DataInputStream за всичкия останал форматиран вход).
Ако сравните секция 5 със съответната от IOStreamDemo.java, ще забележите че в нея версия данните са написани преди текста. Това е поради бъг допуснат в Java 1.1, който се показва със следващата програма:
//: c10:IOBug.java
// Java 1.1 (and higher?) IO Bug
import java.io.*;
public class IOBug {
public static void main(String[] args)
throws Exception {
DataOutputStream out =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out.writeDouble(3.14159);
out.writeBytes("That was the value of pi\n");
out.writeBytes("This is pi/2:\n");
out.writeDouble(3.14159/2);
out.close();
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
BufferedReader inbr =
new BufferedReader(
new InputStreamReader(in));
// The doubles written BEFORE the line of text
// read back correctly:
System.out.println(in.readDouble());
// Read the lines of text:
System.out.println(inbr.readLine());
System.out.println(inbr.readLine());
// Trying to read the doubles after the line
// produces an end-of-file exception:
System.out.println(in.readDouble());
}
} ///:~
Видимо нищо след написано след извикването на writeBytes( ) не може да се извлече обратно. Това е доста ограничаваща грешка и може да се надяваме че ще бъде отстранена по времето, когато четете това. Ще пуснете горната програма за да я изпробвате; ако не получите изключение и стойностите се извеждат правилно, работата е наред.
Генератор на справки
//: c10:CrossReference.java
// Generates cross-reference listing of tokens
import java.util.*;
import java.io.*;
class CrossReference {
// Comparator to ignore case:
static class NoCase implements Comparator {
public int compare(Object o1, Object o2) {
String s1 = (String) o1;
String s2 = (String) o2;
return s1.compareToIgnoreCase(s2);
}
}
static void process(BufferedReader r)
throws IOException {
TreeMap map = new TreeMap(new NoCase());
String line;
int lineno = 0;
// Build map, reading a line at a time:
while ((line = r.readLine()) != null) {
++lineno;
// Read each token:
String delim =
" `~!@#$%^&*()-_=+\\|[{]};:'\",<.>/?"
+ "0123456789";
StringTokenizer tokens =
new StringTokenizer(line, delim);
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken();
if (!map.containsKey(token)) {
// Add token and empty list to map:
map.put(token, new LinkedList());
}
// See if this line is in there already:
LinkedList lines =
(LinkedList) map.get(token);
if (lines.isEmpty() ||
((Integer)lines.getLast()).intValue() !=
lineno) {
// Add line number to list:
lines.addLast(new Integer(lineno));
}
}
}
// Output:
Iterator p = map.entrySet().iterator();
while (p.hasNext()) {
Map.Entry e = (Map.Entry) p.next();
System.out.print(e.getKey() + ": ");
LinkedList lines = (LinkedList)e.getValue();
for (int i = 0; i < lines.size(); ++i) {
if (i > 0)
System.out.print(", ");
System.out.print(lines.get(i));
}
System.out.println();
}
}
public static void
main(String[] args) throws IOException {
// Process each file:
for (int i = 0; i < args.length; ++i) {
System.out.println("File: " + args[i]);
process(new BufferedReader(
new FileReader(args[i])));
System.out.println("====================");
}
}
} ///:~
Пренасочване на стандартния IO
Java 1.1 е добавил методи в класа System, които позволяват да се пренасочват стандартния входен, изходен и за грешки потоци чрез извикване на статични методи:
setIn(InputStream)
setOut(PrintStream)
setErr(PrintStream)
Пренасочването на изхода е особено полезно, когато много информация пробягва по вашия екран без да може да я проследите. Пренасочването на входа е ценно когато имате да пускате програма с повтарящ се потребителски вход при всяко пускане. Ето прост пример за използване на тези методи:
//: c10:Redirecting.java
// Demonstrates the use of redirection for
// standard IO in Java 1.1
import java.io.*;
class Redirecting {
public static void main(String[] args) {
try {
BufferedInputStream in =
new BufferedInputStream(
new FileInputStream(
"Redirecting.java"));
// Produces deprecation message:
PrintStream out =
new PrintStream(
new BufferedOutputStream(
new FileOutputStream("test.out")));
System.setIn(in);
System.setOut(out);
System.setErr(out);
BufferedReader br =
new BufferedReader(
new InputStreamReader(System.in));
String s;
while((s = br.readLine()) != null)
System.out.println(s);
out.close(); // Remember this!
} catch(IOException e) {
e.printStackTrace();
}
}
} ///:~
Тази програма насочва стандартния вход да е от файл, а стандартния изход истандартната грешка към друг файл.
Това е друг пример където предупреждението за остарялост е неизбежно. Съобщението което ще получите с помощта на флага -deprecation е:
Note: The constructor java.io.PrintStream(java.io.OutputStream)
has been deprecated.
Обаче и System.setOut( ) и System.setErr( ) изискват PrintStream обект за аргумент, така че се принуждавате да използвате PrintStream конструктор. Може да се чудите дали Java 1.1 смята за остарял целия PrintStream щом е остарял конструктора, докато проектантите на библиотеката, по същото време когато са решили това, са добавили и нови методи към System изискващи PrintStream наместо PrintWriter, който е новият и предпочитан заместител. Това е мистерия.
Сподели с приятели: |