首先,让我们考察Java提供的常用输出输出流类(图7.1)。由于类的数目较多,没有列出1.1版本中新增的字符流类。在图7.2中,我们把字符流类与字节流类作了对比,在该图中可以看到字符流类的继承关系。接口和异常类也被省略了。
┌BufferedInputStream
├DataInputStream
┌FilterInputStream┼LineNumberInputStream
├FileInputStream └PushbackInputStream
├ByteArrayInputStream
┌InputStream──┼PipedInputStream
│ ├SequenceInputStream
│ ├StringBufferInputStream
│ └ObjectInputStream ┌BufferedOutputStream
│ ┌FilterOutputStream┼DataOutputStream
Object┤ ├FileOutputStream └PrintStream
├OutputStream──┼ByteArrayOutputStream
├File ├PipedOutputStream
├FileDescriptor └ObjectOutputStream
├ObjdecStreamClass
├RandomAccessFile
└StreamTokenizer
图7.1 java.io包中常用类层次图(不含字符流类)
图7.1中包含了许多的输入和输出类(这还不包括我们欢天喜地上要讲到的字符流输入输出类)。为了能正确运用它们,我们必须对它们的功能和关系有个大根式的认识。
7.1.1 字节流与字符流
第二章中提到了Unicode字符集和ASCII字符集。前者用16位来表示一个字符,而者用8位来表示一个字符。Unicode字符集可表示的符号显然比ASCII字符集多得多,它可以表示世界上大多数语言的符号。
在JDK1.0x版本中,只提供了字节流输入输出类。也就是说,输入输出的数据以字节为读写单位。这就给操作一些双字节字符带来了困难。比如汉字,用一个字节是不能表示,这就使Java程序的汉化成了问题。例如,用1.0x版的JDK开发一个文本编辑器,就可能出现这样的情况:用剪贴板可以把汉字贴进文本域却无法用键盘向文本域输入汉字字符。这就是标准输入流每次只接收了一个汉字的第一字节引起的。
JDK1.1版对输入输出作了改进,为字节流输入输出类增加了对应的字符流输入输出类这样,程序员就可以根据实际情况选用合适的类。
字符流I/O有其显示而易见的好处。首先它可以适用于世界上大部分语言,从而为Java程序的本地化带来方便。其次,一次读一个字符(16位)比读一个字节来得快,一般情况下可以弥补将数据按当前语言标准编码、解码的时间开销。
字节流I/O类和字符流I/O类的命名有其对应关系。字节输入流类的名字以“InputStream”结尾。而字符输入流类的名字以“Reader” 结尾。字节输出流类的名字后缀为“OutputStream”,而字符输出流类的名字后缀为“Writer”。
为了在适当的时候能把这两种流类联系起来,API中设置了两个类,充当二者的桥梁。InputStreamReader根据特定的编码规则从字节流创建相应的字符流,而Output。StreamWriter则根据编码规则从字符流读取字符,把它们转化为字节,写入字节流中。
下面列出两种流类的对应关系(图7.2)。其中,左边一栏是按继承关系排列的字符流类,右边是对应的字节流类。
Reader InputStream
├BufferedReader BufferedInputStream
│ └LineNumberReader LineNumberReader
├CharArrayReader ByteArrayInputStream
├InputStreamReader (none)
│ └FileReader FileInputStream
├FilterReader FilterInputStream
│ └PushbackReader PushbackInputStream
├PipedReader PipedInputStream
└StringReader StringBufferInputStream
Write OutputStream
├BufferedWriter BufferedOutputStream
├CharArrayWriter ByteArrayOutputStream
├OutputStreamWriter (none)
│ └FileWriter FileOutputStream
├FilterWriter FilterOutputStream
├PrintWriter PrintStream
├PipedWriter PipedOutputStream
└StringWriter (none)
图7.2字符流类与字节流类的对应关系
另外,1.1版的API中,对一些1.0x版本中已存在的类也进行了微小的修改,这主要是因为有类对字节和字符的转换可能产生错误。如以下构造函数和方法标记为过时:
Sting DataInputStream.readLine()
InputStream Runtime.getLocalizedInputStream(InputStream)
OutputStream Runtime.getLocalizedOutputStream(OutputStream)
StreamTokenizer(InputStream)
String(byte ascii[],int hibyte,int offset,int count)
String(byte ascii[],int hibyte)
void String.getBytes(int srcBegin,int srcEnd,byte dst[],int dstBegin)
另外,添加了如下构造函数和方法:
StreamTokenizer(Reader)
byte[] String.getBytes()
void Throwable.printStackTrace(PrintWriter)
当程序员使用旧的API编程时,可以用
javac -deprecation(文件名)
来进行编译,这样编译器会给出较为详细的警告信息。编程人员可根据这些信息查找新文档,以获知新版本中的替代方法。
本章的例子都是依据1.1版本的API编写的。
7.1.2 输入输出类的分类
java.io包中的类各有各的分工,粗略说来可以分为以下几类:
文件I/O:有三类。对字节流类来说,包括把文件作为源进行流式输入的FileInputStream类;把文件作为目的进行流式输出的 FileOutputStream类;若你想随机存取文件,即在文件的任意位置读、数据,那么可以使用RandomAccessFile类。字符类则有 FileReader和FileWriter类。它们的功能对应于前两个字节流类。
除此之外,还有两个类是与文件访问有关的,确切地说其功能更近于文件管理。它们是File类,用以访问文件或目录;FileDescriptor则封装了操作系统用以追踪被访问文件的信息。
内存缓冲区I/O:字节流类有ByteArrayInputStream类,将字节数组转化为输入流,是从一个字符串创建输入流,与 ByteArrayInputStream异曲同工,帮也归入此类别。相应地,字符流类有CharArrayReader, CharArrayWriter,StringReader,此外还多一个StringWriter用来写字符串。
余下一些类可以不同方式存取流中的数据。字节流类中,DataInputStream和DataOutputStream因其能对流中的不同类的对象分别操作而显得与众不同;ObjectInputStream和ObjectOutputStream能把若干完整的对象按选定的格式进行读写,但要求被操作对象实现Serializable接口;BufferedInputStream和BufferedOutputStream可以对流数据进行缓冲,实现类似“预输入”、“缓输出”的功能;LineNumberInputStream跟踪输入流中的行数;PusthbackInputStream提供了一个“可推回”的流,从这个流中读了数据后,还可以将它放回流中;PrintStream类提供了许多重载的方法以简化输出。对应的字符流类可以从 7.1.1节的对应关系中查出。
除了上述类以外,Java还有种特殊的I/O类——管道I/O类。它们是专门为线程通讯预备的。管道提供了自动同步机制,可以防止线程通讯中的数据混乱。
至引相信读者已对各个I/O类的功能有所了解。这里再解释一下过滤器I/O 推广java.io包中有不少类是过滤器类,它们都是从FilterInputStream或FilterOutputStream之中派生而来(参见图 7.1)。在字符流中,也有类似的类,但并不像字节流类一样必然从某个公共的过滤器父类派生而来。
过滤器(Filter)形成的类对象从一个流中读入数据,写入另一个,就像一个流经过过滤产生另一个流一样。过滤器可以联合使用,也就是说“过滤”过的流可以再经其它过滤器“过滤”,过滤器型类的共性是:
(1)用和种流为参数的构造,且输入型过滤器用输入流,输出型过滤器用输出流;
(2)无明显的源/目的限制;
(3)流中数据的内容“多少”并未改变,只可能性质略有变化。
读者不妨以这几条标准去理解过滤器I/O类与其子类,并在以后的示例中加以验证。
7.2 输入流与输出流
字节输入流InputStream与字节输出流OUtputStream是两个抽象类。它们为java.io包中名目繁多的字节输入和输出流打下了基础。由于是抽象类,它们不能被实例化(也就是说,不能得到其对象),但它们的方法可以被派生类所继承或重写。
对于字符流,相应的流类是Reader和Writer。由于它们的方法与InputStream和OutputStream对应,只是把对字节的操作改为对字符的操作,这里不再重复介绍。但为了读者能够对它们的对应关系有个基本认识,在本节末尾附上Reader类的方法列表,请读者参照。
InputStream的方示如下:
■public abstract int read() throws IOException
■public int read(byte b[]) throws IOException
■public int read(byte b[],int offset,int length) throws IOException
功能为从输入流中读数据。这一方法有几种重载形式,可以读一个字节或一组字节。当遇到文件尾时,返回-1。最后一种形式中的offset是指把结果放在b[]中从第offset个字节开始的空间,length为长度。
■public int available() throws IOException
输入流共有多少字节可读。注意此方法对InputStream的各派生类不一定都有效,有时会有返回零字节的错误结果。
■public void close() throws IOException
关闭输入流并释放资源。
■public boolean markSupperted()
返回布尔值,说明此流能否做标记。
■public synchronized void mark(int readlimit)
为当前流做标记。其参数说明在标记失效前可以读多少字节,这个值通常也就设定了流的缓冲区大小。
■public synchronized void reset() throws IOException
返回到上一次做标记处。
■public long skip (long n) throws IOEnception
从输入流跳过几个字节。返回值为实际跳过的字节数。
对于“mark”我们还需解释一下。输入流提供“标记”这一机制,使人们可以记录流中某些特定的位置,并能重复读部分内容。支持“mark”就必须要求当前流有一定大小的缓冲区,存放部分数据,即从标记点到当前位置的数据。当这一缓冲区装满溢出,我们就无法追踪到上一个标记处的数据了,这就称之为“标记失效”。若想用reset()返回到一个失效的标记处,将会发生输入输出异常(IOException)。
OutputStream的方法如下。各方法均可能抛出输入输出异常(throws IOException)。
■public abstract void write(int b)
■public void write(byte b[])
■public void write(byte b[],int offset,int length)
这三个重载形式都是用来向输出流写数据的。具体每个不甘落后 作用,读者可根据前文read()方法对照之。
■public void flush()
清除缓冲区,将缓冲区内尚未写出的数据全部输出。若要继承OutputStream类,这个方法必须重写,因为OutputStream中的方法未做任何实物性工作。
■public void close()
关闭输出流,释放资源。
以上提到的这些方法,在下面的章节中将有不少被运用,读者可根据实例领会它们。
附Reader类的方法列表。
构造函数:
■protected Reader()
■protected Reader(object lock)
方法:
■public int read() throws IOException
■public int read(char cbuf[]) throws IOException
■public abstract int read(char cbuf[],int off,int len)throws IOException
■public long skip(long n) throws IOException
■public boolean ready() throws IOException //判断流是不可以读
■public boolean mark(int readAheadLimit)throws IOException
■public void reset() throws IOException
■public abstract void close() throws IOException
7.3 文件I/O
这一节中我们将结合实例讨论File,FileInputStream,FileOutputStream,FileDescriptor和RandomAccessFile类的方法与使用。
7.3.1 一个文件I/O实例
让我们用一个例子来演示对文件的输入输出(例7.1)。图7.3中列出了这个例子的运行结果。
例7.1 fileIODemo.java。
1:import java.io.*;
2:import java.lang.*;
3:
4: public class fileIODemo{
5: public static void main(String args[]){
6: try{
//创建输入输出流
7: FileInputStream inStream = new FileInputStream("text.src");
8: FileOutputStream outStream = new FileOutputStream("text.des");
//读文并写入输出流
9: boolean eof = false;
10: while(!eof){
11: int c = inStream.read();
12: if(c==-1) eof = true;
13: outStream.write((char)c);
14: }
15: inStream.close();
16: outStream.close();
17: }catch(FileNotFoundException ex){
18: System.out.println("Error finding the files");
19: }catch(IOException ex){
20: System.out.println("IOException occured.");
21: }
//获取文件管理信息
22: File file = new File("text.des");
23: System.out.println("Parent Directory:"+file.getParent());
24: System.out.println("Path:"+file.getPath());
25: System.out.println("File Name:"+file.getName());
26: try{
//创建RandomAccessFile对象,以便随机读写。"rw"代表可读可写
27: RandomAccessFile rafile = new RandomAccessFile("text.des","rw");
//指针置到文件头
28: rafile.seek(0);
29: boolean eof=false;
30: System.out.println("The content from very head:");
//读文件
31: while(!eof){
32: int c = rafile.read();
33: if(c==-1) eof = true;
34: else System.out.print((char)c);
35: }
//下两行把读指针置到第三字节
36: rafile.seek(0);
37: rafile.skipBytes(3);
38: System.out.println("\nThe pointer's position:"+rafile.getFilePointer());
39: System.out.println("The content from current position:");
40: eof=false;
41: while(!eof){
42: int c=rafile.read();
43: if(c==-1) eof=true;
44: else System.out.print((char)c);
45: }
//强制输出缓冲区中所有内容
46: System.out.flush();
47: rafile.close();
48: }catch(IOException ex){
49: System.out.println("RandomAccessFile cause IOException!");
50: }
51: }
52:}
例7.1的运行结果如下:
(略)
为了充分展示与文件I/O相关的类的作用,我们的例子中有一些冗余的东西。我们的这个程序位于C:\BookDemo\ch07路径下(见例7.1行 7),此路径又有一个子上当text,其中有文件text.src。运行此程序,将在C:\bookDemo\ch07下创建一个新文件 text.des,text.src的内容被写信此文件。下面的段对File类的演示说明了文件的部分管理信息。然后我们又使用了 RandomAccessFile,试验了文件在指定位置的读写。
第46行的Sytem.out.flush()语句不可以被省略,读者不妨去掉它试一试。你会发现,有一部分输出信息不知道到哪儿去了。实际上,flush()的作用就是把缓冲区中的数据全部输出,我们棣输出流输出以后,某些输出流(有缓冲区的流)只是把数据写进了缓冲区而已,不会马上写到我们要求的目的地。如果不像例子中一样强制输出,部分数据可以就来不及在程序结束前输出了。
细心的读者或许要问:为什么第一次用ReadomAccessFile读文件时,输出语句后面没有flush()呢?岂非自相矛盾吗?原来, System.out是PrintStream类的对象(关于PrintStream后有缓冲区中的内容清除出去。因此许多地方就不必加flush() 了。PrintStream的这个特点,在创建其对象时是可以去掉(disable)的。
这个程序中用到了IOException和FileNotFoundException两个异常。后者是从前者派生出来的,因此,如果去年程序中的所有try、catch,而在main()方法开头加上throws IOException,哪样可以。但这样不好区分各种不同的异常情况,即使找不到我们需要的text.src文件,也不会有任何信息显示。这无疑是一种不良的编程风格。因此我们提倡对各个异常分别处理,这样对出错情况可以很地掌握。