10.5 IO流的典型应用

尽管库内存在大量IO流类,可通过多种不同的方式组合到一起,但实际上只有几种方式才会经常用到。然而,必须小心在意才能得到正确的组合。下面这个相当长的例子展示了典型IO配置的创建与使用,可在写自己的代码时将其作为一个参考使用。注意每个配置都以一个注释形式的编号起头,并提供了适当的解释信息。
//: 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");
    }
  }
} ///:~

10.5.1 输入流

当然,我们经常想做的一件事情是将格式化的输出打印到控制台,但那已在第5章创建的com.bruceeckel.tools中得到了简化。 第1到第4部分演示了输入流的创建与使用(尽管第4部分展示了将输出流作为一个测试工具的简单应用)。

1、缓冲的输入文件

为打开一个文件以便输入,需要使用一个FileInputStream,同时将一个String或File对象作为文件名使用。为提高速度,最好先对文件进行缓冲处理,从而获得用于一个BufferedInputStream的构建器的结果句柄。为了以格式化的形式读取输入数据,我们将那个结果句柄赋给用于一个DataInputStream的构建器。DataInputStream是我们的最终(final)对象,并是我们进行读取操作的接口。

在这个例子中,只用到了readLine()方法,但理所当然任何DataInputStream方法都可以采用。一旦抵达文件末尾,readLine()就会返回一个null(空),以便中止并退出while循环。

“String s2”用于聚集完整的文件内容(包括必须添加的新行,因为readLine()去除了那些行)。随后,在本程序的后面部分中使用s2。最后,我们调用close(),用它关闭文件。从技术上说,会在运行finalize()时调用close()。而且我们希望一旦程序退出,就发生这种情况(无论是否进行垃圾收集)。然而,Java 1.0有一个非常突出的错误(Bug),造成这种情况不会发生。在Java 1.1中,必须明确调用System.runFinalizersOnExit(true),用它保证会为系统中的每个对象调用finalize()。然而,最安全的方法还是为文件明确调用close()。

2、从内存输入

这一部分采用已经包含了完整文件内容的String s2,并用它创建一个StringBufferInputStream(字串缓冲输入流)——作为构建器的参数,要求使用一个String,而非一个StringBuffer)。随后,我们用read()依次读取每个字符,并将其发送至控制台。注意read()将下一个字节返回为int,所以必须将其造型为一个char,以便正确地打印。

3、格式化内存输入

StringBufferInputStream的接口是有限的,所以通常需要将其封装到一个DataInputStream内,从而增强它的能力。然而,若选择用readByte()每次读出一个字符,那么所有值都是有效的,所以不可再用返回值来侦测何时结束输入。相反,可用available()方法判断有多少字符可用。下面这个例子展示了如何从文件中一次读出一个字符:

//: 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");
    }
  }
} ///:~

注意取决于当前从什么媒体读入,avaiable()的工作方式也是有所区别的。它在字面上意味着“可以不受阻塞读取的字节数量”。对一个文件来说,它意味着整个文件。但对一个不同种类的数据流来说,它却可能有不同的含义。因此在使用时应考虑周全。

为了在这样的情况下侦测输入的结束,也可以通过捕获一个违例来实现。然而,若真的用违例来控制数据流,却显得有些大材小用。

4、行的编号与文件输出

这个例子展示了如何LineNumberInputStream来跟踪输入行的编号。在这里,不可简单地将所有构建器都组合起来,因为必须保持LineNumberInputStream的一个句柄(注意这并非一种继承环境,所以不能简单地将in4造型到一个LineNumberInputStream)。因此,li容纳了指向LineNumberInputStream的句柄,然后在它的基础上创建一个DataInputStream,以便读入数据。

这个例子也展示了如何将格式化数据写入一个文件。首先创建了一个FileOutputStream,用它同一个文件连接。考虑到效率方面的原因,它生成了一个BufferedOutputStream。这几乎肯定是我们一般的做法,但却必须明确地这样做。随后为了进行格式化,它转换成一个PrintStream。用这种方式创建的数据文件可作为一个原始的文本文件读取。 标志DataInputStream何时结束的一个方法是readLine()。一旦没有更多的字串可以读取,它就会返回null。每个行都会伴随自己的行号打印到文件里。该行号可通过li查询。

可看到用于out1的、一个明确指定的close()。若程序准备掉转头来,并再次读取相同的文件,这种做法就显得相当有用。然而,该程序直到结束也没有检查文件IODemo.txt。正如以前指出的那样,如果不为自己的所有输出文件调用close(),就可能发现缓冲区不会得到刷新,造成它们不完整。。

10.5.2 输出流

两类主要的输出流是按它们写入数据的方式划分的:一种按人的习惯写入,另一种为了以后由一个DataInputStream而写入。RandomAccessFile是独立的,尽管它的数据格式兼容于DataInputStream和DataOutputStream。

1、保存与恢复数据

PrintStream能格式化数据,使其能按我们的习惯阅读。但为了输出数据,以便由另一个数据流恢复,则需用一个DataOutputStream写入数据,并用一个DataInputStream恢复(获取)数据。当然,这些数据流可以是任何东西,但这里采用的是一个文件,并进行了缓冲处理,以加快读写速度。

注意字串是用writeBytes()写入的,而非writeChars()。若使用后者,写入的就是16位Unicode字符。由于DataInputStream中没有补充的“readChars”方法,所以不得不用readChar()每次取出一个字符。所以对ASCII来说,更方便的做法是将字符作为字节写入,在后面跟随一个新行;然后再用readLine()将字符当作普通的ASCII行读回。

writeDouble()将double数字保存到数据流中,并用补充的readDouble()恢复它。但为了保证任何读方法能够正常工作,必须知道数据项在流中的准确位置,因为既有可能将保存的double数据作为一个简单的字节序列读入,也有可能作为char或其他格式读入。所以必须要么为文件中的数据采用固定的格式,要么将额外的信息保存到文件中,以便正确判断数据的存放位置。

2、读写随机访问文件

正如早先指出的那样,RandomAccessFile与IO层次结构的剩余部分几乎是完全隔离的,尽管它也实现了DataInput和DataOutput接口。所以不可将其与InputStream及OutputStream子类的任何部分关联起来。尽管也许能将一个ByteArrayInputStream当作一个随机访问元素对待,但只能用RandomAccessFile打开一个文件。必须假定RandomAccessFile已得到了正确的缓冲,因为我们不能自行选择。

可以自行选择的是第二个构建器参数:可决定以“只读”(r)方式或“读写”(rw)方式打开一个RandomAccessFile文件。

使用RandomAccessFile的时候,类似于组合使用DataInputStream和DataOutputStream(因为它实现了等同的接口)。除此以外,还可看到程序中使用了seek(),以便在文件中到处移动,对某个值作出修改。

10.5.3 快捷文件处理

由于以前采用的一些典型形式都涉及到文件处理,所以大家也许会怀疑为什么要进行那么多的代码输入——这正是装饰器方案一个缺点。本部分将向大家展示如何创建和使用典型文件读取和写入配置的快捷版本。这些快捷版本均置入packagecom.bruceeckel.tools中(自第5章开始创建)。为了将每个类都添加到库内,只需将其置入适当的目录,并添加对应的package语句即可。

1、快速文件输入

若想创建一个对象,用它从一个缓冲的DataInputStream中读取一个文件,可将这个过程封装到一个名为InFile的类内。如下所示:

//: 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。就象这个例子展示的那样,现在可以有效减少创建文件时由于重复强调造成的问题。

2、快速输出格式化文件

亦可用同类型的方法创建一个PrintStream,令其写入一个缓冲文件。下面是对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());
  }
} ///:~

注意构建器不可能捕获一个由基础类构建器“掷”出的违例。

3、快速输出数据文件

最后,利用类似的快捷方式可创建一个缓冲输出文件,用它保存数据(与由人观看的数据格式相反):

//: 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库的设计者居然没想到将这些便利措施直接作为他们的一部分标准提供。

10.5.4 从标准输入中读取数据

以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的解决方案。下面是个简单的例子,作用是回应我们键入的每一行内容:

//: 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内,所以显得有点不方便。但采用这种设计方案,可以获得最大的灵活性。

10.5.5 管道数据流

本章已简要介绍了PipedInputStream(管道输入流)和PipedOutputStream(管道输出流)。尽管描述不十分详细,但并不是说它们作用不大。然而,只有在掌握了多线程处理的概念后,才可真正体会它们的价值所在。原因很简单,因为管道化的数据流就是用于线程之间的通信。这方面的问题将在第14章用一个示例说明。

下一节:尽管StreamTokenizer并不是从InputStream或OutputStream衍生的,但它只随同InputStream工作,所以十分恰当地包括在库的IO部分中。