# 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`。
(5) 保存与恢复数据
`PrintStream`能格式化数据,使其能按我们的习惯阅读。但为了输出数据,以便由另一个数据流恢复,则需用一个`DataOutputStream`写入数据,并用一个`DataInputStream`恢复(获取)数据。当然,这些数据流可以是任何东西,但这里采用的是一个文件,并进行了缓冲处理,以加快读写速度。
注意字符串是用`writeBytes()`写入的,而非`writeChars()`。若使用后者,写入的就是16位Unicode字符。由于`DataInputStream`中没有补充的`readChars`方法,所以不得不用`readChar()`每次取出一个字符。所以对ASCII来说,更方便的做法是将字符作为字节写入,在后面跟随一个新行;然后再用`readLine()`将字符当作普通的ASCII行读回。
`writeDouble()`将`double`数字保存到数据流中,并用补充的`readDouble()`恢复它。但为了保证任何读方法能够正常工作,必须知道数据项在流中的准确位置,因为既有可能将保存的`double`数据作为一个简单的字节序列读入,也有可能作为`char`或其他格式读入。所以必须要么为文件中的数据采用固定的格式,要么将额外的信息保存到文件中,以便正确判断数据的存放位置。
(6) 读写随机访问文件
正如早先指出的那样,`RandomAccessFile`与IO层次结构的剩余部分几乎是完全隔离的,尽管它也实现了DataInput和`DataOutput`接口。所以不可将其与`InputStream`及`OutputStream`子类的任何部分关联起来。尽管也许能将一个`ByteArrayInputStream`当作一个随机访问元素对待,但只能用`RandomAccessFile`打开一个文件。必须假定`RandomAccessFile`已得到了正确的缓冲,因为我们不能自行选择。
可以自行选择的是第二个构造器参数:可决定以“只读”(`r`)方式或“读写”(`rw`)方式打开一个`RandomAccessFile`文件。
使用`RandomAccessFile`的时候,类似于组合使用`DataInputStream`和`DataOutputStream`(因为它实现了等同的接口)。除此以外,还可看到程序中使用了`seek()`,以便在文件中到处移动,对某个值作出修改。
## 10.5.3 快捷文件处理
由于以前采用的一些典型形式都涉及到文件处理,所以大家也许会怀疑为什么要进行那么多的代码输入——这正是装饰器方案一个缺点。本部分将向大家展示如何创建和使用典型文件读取和写入配置的快捷版本。这些快捷版本均置入`packagecom.bruceeckel.tools`中(自第5章开始创建)。为了将每个类都添加到库内,只需将其置入适当的目录,并添加对应的`package`语句即可。
(7) 快速文件输入
若想创建一个对象,用它从一个缓冲的`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`。
就象这个例子展示的那样,现在可以有效减少创建文件时由于重复强调造成的问题。
(8) 快速输出格式化文件
亦可用同类型的方法创建一个`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());
}
} ///:~
```
注意构造器不可能捕获一个由基类构造器“抛”出的异常。
(9) 快速输出数据文件
最后,利用类似的快捷方式可创建一个缓冲输出文件,用它保存数据(与由人观看的数据格式相反):
```
//: 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章用一个示例说明。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物