输入与输出
这一章我们讨论Java程序的输入与输出。Java在I/O方面提供了众多支持,使我们的工作得到大大的简化。我们将学习利用这些支持以完成各种复杂的输入、输出。
1.理解java.io的类继承关系
首先,让我们考察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
图1.1 java.io包中常用类层次图(不含字符流类)
图1.1中包含了许多的输入和输出类(这还不包括我们欢天喜地上要讲到的字符流输入输出类)。为了能正确运用它们,我们必须对它们的功能和关系有个大根式的认识。
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)
图1.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编写的。
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类与其子类,并在以后的示例中加以验证。
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
3 文件I/O
这一节中我们将结合实例讨论File,FileInputStream,FileOutputStream,FileDescriptor和RandomAccessFile类的方法与使用。
3.1 一个文件I/O实例
让我们用一个例子来演示对文件的输入输出(例7.1)。图7.3中列出了这个例子的运行结果。
例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:}
例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文件,也不会有任何信息显示。这无疑是一种不良的编程风格。因此我们提倡对各个异常分别处理,这样对出错情况可以很地掌握。
3.2 文件输入输出的类库支持
下面我们逐一介绍例1中用到的各个类。
1.File类
File类的构造函数有三个。分别根据文件名、文件路径与文件名、文件对象(目录)与文件名创建实例。即:
■public File(String path)
■public File(String path,String name)
■public File(File dir,String name)
除了例子中用到的以外,还有许多方法,下面仅列出较常用的:
■public boolean exists()判断文件是否存在
■public boolean canRead()判断文件是否可读
■public long length()返回文件长度
■public boolean mkdir()创建目录
■public boolean renameTo(File dest)文件改名
其中,后三个方法可能抛出I/O异常。
2.FileInputStream类
它是文件输入流类
构造函数有三个:
■public FileInputStream(String fileName) throws FileNotFoundException
■public FileInputStream(File file) throws FileNotFoundException
■public FileInputStream(int fd) throws FileNotFoundException
三个构造函数分别根据文件名、文件对象、文件描述符创建一个文件输入流。例子中用的是第一种。
方法:
read()、skip()、available() 、close()分别重写了抽象类InputStream的同名方法,功能如前所述。此外还有:
■public final int getFD()
返回相应的文件描述符。
■protedted void finalize() throws IOException
关闭输入流,并收集无用内存空间。
现在我们必须介绍一下文件描述符类FileDescriptor。这个类用于访问操作系统维护的文件描述符(也称句柄)。但这个类产不能访问很多信息。它只提供了两个方法,即valid(),以判断文件描述符是否有效;sync(),用以同步系统缓冲区。
3.FileOutputStream类
文件输出流。三个构造函数,其参数、返回值及异常均与FileInputStream的相对应。write()、close()方法重写了OutputStream的同名方法。getFD()与finalize()功能与InputStream的类似。
4.ReadomAccessFile类
该类用于随机访问文件。
构造函数有三种:
■public RandomAccessFile(String Filename,String mode) throws IOException
■public RandomAccessFile(int FD) throws IOException
■public RandomAccessFile(File file,String mode)throws IOException
由上可见,我们可以用文件名加读写方式、文件描述符、File对象加读写方式来创建其对象。其中读写方式用“r”表示只读,“rw”表示可读写,等等。用过C语言的读者对此应当不会陌生。
此类的成员方法很多。除了重写InputStream的read()方法之外,还可以读、写一个布尔值、一个字节、一个整数......等对象。这些方法都不可重写,并且抛出I/O异常(IOException)。讯方法名为“read”加类型名(类型名的第一字母大写),写方法名为“write”加类型名。如
readInt()读一个整型数
writeDouble()写一个双精度浮点数
等。另外还有文件指针的操作,如skipBytes(int n)等。
有了以上这些类的支持,处理文件输入输出和维护文件就容易多了。
4 内存缓冲区
内存缓冲区I/O,对字节流来说指的是ByteArrayInputStream和ByteArrayOutputStream类的运用。此外,StringBufferInputStream与ByteArrayInputStream用法相似将一并介绍。对字符流不另举例,它们使用与字节流类类似。
4.1 程序示例
同上一节一样,我们还是先看一个例子(例2)
例2 ByteArrayIODemo.java
1:import java.io.*;
2:
3: public class ByteArrayIODemo{
4: public static void main(String args[]) throws IOException{
String s ="This a test";
5: byte buffer[]=s.getBytes();
6: ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
7: for(int i=0;i<buffer.length;++i)
8: byteArrayOut.write(buffer[i]);
//由字节数组创建字节输入流
9: ByteArrayInputStream inStream = new
ByteArrayInputStream(byteArrayOut.toByteArray());
//读写
10: boolean eof=false;
11: while(!eof){
12: int c=inStream.read();
13: if(c==-1) eof=true;
14: else System.out.print((char)c);
15: }
16: System.out.println("/nThe'writeTo' method can produce same results.");
//用ByteArrayOutputStream的writeTo()方法写
17: byteArrayOut.writeTo(System.out);
18: byteArrayOut.close();
//available()与reset()的使用
19: System.out.println("/nThe buf of inStream has the length(before
seset):"+inStream.available());
20: inStream.reset();
21: System.out.println("/nThe buf of inStream has the length:"+inStream.available());
22: inStream.close();
23: }
24:}
该程序的运行结果:(略)
这个例子看来相对简单些。我们先把字节数组的内容写进一个字节数组输出流对象。然后,用一个字节数组输入流对象读数据,再用System.out输出。程序显示了writeTo()方法的作用。另外,我们还在reset()前、后用了两疚available()方法,请注意两方法先后产生的不同结果。这个例子主要用来演示字节数组I/O的部分方法。
4.2缓冲区I/O的类库支持
看过了例子,我们接下来介绍有关的类。
1.ByteArrayInputStream类
这个类用于从一个
字节数组取得输入数据。
它有两个构造函数:
■public ByteArrayInputStream(byte Buf[])
由字节数组创建相应的输入流。
■public ByteArrayInputStream(byte buf[],int offset,int length)
由字节数组中起点为offset长为length的一段创建输入流。
成员变量:
protected byte buf[]数据缓冲区
protected int pos 缓冲区中当前位置
protected int count缓冲区中字节数目
该类的成员方法都是同步(synchronized)的。
■public synchronized int read()
读一个字节。
■public synchronized int read(byte b[],int offset,intrlength)
读取多个字节,返值一般为读到的字节数。但读到末尾时返回-1。
■public synchronized long skip(long n)
跳过n个字节。若返回值不等于n,可能是遇到末尾。
■public synchronized int available()
求得缓冲区内字节数目。
■public synchronized void reset()
该指针重新设置到输入流的开始处。注意,这个reset()与InputStream中的功能不同。它并不作用于标记。
2.ByteArrayOutputStream类
这个类用于把数据写进字节数组(缓冲区)。
构造函数:
■public ByteArrayOutputStream()
■public ByteArrayOntput Stream(int size)
其中size指定缓冲区的初始大小(它是可以动态增长的)。
成员变量:
protected byte buf[]缓冲区
protected int count缓冲区大小
方法:
■public synchronized void write(int b)
写一个字节。
■public synchronized void write(byte b[],int offset,int length)
把数组b中由offset开始长为length的一部分写入缓冲区。
■public synchronized void writeTo(OutputStream out)throws IOException
把缓冲区内容写到另一输出流out。
■public synchronized void reset()
指针定到缓冲区开始。当然,以后再写入也就是从缓冲区的开始位置重写了,原有的内容就都清掉了。
■public syschronized byte[] toByteArray()
将缓冲区内容作为一个字节数组返回。
■public int size()
当前缓冲区大小。
■public string toString()
■public string toString(int hibyte)
把缓冲区内容转化为字符串。其中hibyte指把字符(通常是8位的ASCII字符)转为16位的Unicode值时,高八的值。
3.StringBufferInputStream类
它的构造函数以一个字符串为参数,原型为:
■public StringBufferInputStream(String s)
其余成员变量及方法均与ByteArrayInputStream的同名且基本功能相同,此不赘述。
这三个类的共性是内存中开辟了一段空间来做I/O缓冲区,故称缓冲区I/O类。
5 过滤器I/O
这一节涉及的类较多,但我们可以结合几个例子逐一介绍。
在第一节中,我们已经谈了一些过滤器类的特性。过滤器是可以“连接”的,即一个数据流经过过滤后,其结果可以再次过滤。我们可以使用这样一串过滤器中的任一个方法来完成某种特殊的操作。关于这一点在第二个例子中有更明白的阐述。
5.1 例1:各类数据的I/O
第一个例子(例3)演示了对各类数据的输入输出。
例3FilterIODemo1.java。
1: import java.io.*;
2: public class FilterIODemo1{
3: public static void main(String args[]) throws IOException{
//串接过滤器
4: BufferedOutputStream bufOut=
5: new BufferedOutputStream(new FileOutputStream("text.txt"));
6: DataOutputStream dataOut = new DataOutputStream(bufOut);
//用DataOutputStream类写各种数据
7: dataOut.writeBoolean(true);
8: dataOut.writeChar('a');
9: dataOut.writeInt(1);
10: dataOut.writeDouble(3.3);
11: bufOut.close();
12: dataOut.close();
13: BufferedInputStream bufIn=
14: new BufferedInputStream(new FileInputStream("text.txt"));
15: DataInputStream dataIn= new DataInputStream(bufIn);
//用DataInputStream类读各种数据
16: System.out.println(dataIn.readBoolean());
17: System.out.println(dataIn.readChar());
18: System.out.println(dataIn.readInt());
19: System.out.println(dataIn.readDouble());
20: bufIn.close();
21: dataIn.close();
22: }
23:}
例3的运行结果如下:(略)
上述例子演示了DataInputStream、DataOutpurStream、BufferedInputStream和BufferedOutputStream的使用。该程序中只有一个方法main()。
在方法的开头,我们实例化了BufferedOutputStream类,得到对象bufOut。注意,我们的数据输出的最终目的地是文件“Text.txt”。为了能够利用BufferedOutputStream的缓输出(把输出内容先存入缓冲,然后大块输出)功能,我们在文件输出应对上加一个过滤器,形成:
数据→过滤器对象bufOut→文件输出流
这样,我们用dataOut来写数据,就可以直接把各种类型的数据写入文件text.txt。
程序的后半部分几乎是第一个程序的翻版。我们在输入流上也加了过滤器,就可以用过滤器的方法来操作输入流了。
由于BufferedOutputStream和BufferedInputStream没有提供新的方法,这个例子也许会使读者产生一种错觉,好像只有最外层(最接近“数据”)的过滤器才能操纵输入。事实并非如此,我们将在下一个例子中说明这一点。
我们要解释的问题是,如果我们读数据时,选用的读方法与写时不一致会怎么样呢?读者可以自行实验一下。如果我们把
dataIn.readBoolean()换作dataIn.readChar()
读出的结果 就不正确了(注意读到的并不是字符‘t’),各种类型的数据存储的格式是不同的。虽然我们得到了图7.4所示的结果,但如果你用type命令看一下text.txt,将会看到不同的输出。因此,不要把程序的输出和数据的内部在存储混 为一谈。当使用dataI/O时,应当对你要读的数据的类型心中有数。DataInputStream并不能从一堆数据中析取你所需要的那个整数。
5.2 过滤器类家庭
下面我们介绍例1中出现的过滤器类。首先介绍一下它们的父类FilterInputStream和FilterOutputStream。
1.FilterInputStream类
这是一个抽象类。它是所有过滤器输入类的父类,提供了从一个输入流创建另一个输入流的方法。
构造函数:
■public FilterInputStream(InputStream in)
人一个输入流构造过滤器输入流。
方法:
重写了InputStream的同名方法,未提供新的方法。
2.FilterOutputStream类
与FilterOutputStream相对应,提供从一个输出流创建另一个输出流的方法。
构造函数:
■public Filer OutputStream(OutputStream out)
由输出流创始创建一个过滤器输出流。
方法:
重写了OutputStream的同名方法。
3.BufferedInputStream类
从这个类开始,我们来介绍例7.3中用到的过滤器子类。BufferedInputStream类提供了一种“预输入”功能,它把输入数据在其缓冲区内暂存,在适当的时候把较大块的数据提交出去。
构造函数:
■public BufferedInputStream(InputStream in)
■public BufferedInputStream(InputSteam in,int size)
其中size指缓冲区大小。
方法:
重写了父类的方法。其中skip()、available()、mark()、reset()均为同步(synchonized)方法。
4. BufferedOutputStream类
提供“缓输出”功能,把输出数据暂存后,在适当时候大批送出。
构造函数:
■public BufferedOutputStream(OutputStream out)
■public BufferedOutputStream(OutputStream out,int size)
方法:
■public synchronized void write(int b) throws IOException
■public synchronized void write(byte b[],int offset,int length) throws IOException
■public synchronized void flush() throws IOException
以上方法重写了父类的同名方法。
在BufferedI/O类中,还有一些protect型的成员变量,是关于缓冲区和标记的,这里就不一一列出了。
5. DataInput接口和DataOutput接口
要介绍Data I/O类,就必须介绍Data I/O接口。
Data I/O类的目的是从流中析取或向流中写入指定的数据对象。一个流可以是纯字符流,也可以包含许多类型的数据。DataInput接口和DataOutput接口就提供了从流中析取和写入数据的方法。用于读的方法除了个别之外都是无参的,写的方法则往往以被 写的数据类型为参数。方法的名字也很好记,即为“read”或“write”后接类型名,如readInt(),readUnsignedByte(),writeInt(),writeUnsignedByte()等。这引起方法均可能抛出I/O异常。一般说来,读时遇文件尾时抛出EOFException(是IOException的子类),读写时发生其它错误抛出IOException。
除了上面所说的名字很有规律的方法外,Data I/O接口中还有几个方法:
■public abstract void readFully(byte buffer[])
读全部数据到buffer[]数组。读时系统处于阻塞状态。
■public abstract void readFully(byte buffer[],int offset,int length)
把数据读到数组buffer[]中从Offset开始长为length的地方。
■public abstract int skipBytes(int n)
跳过规定字节数。返值为实际跳过的字节数。
■public abstract String readLine()
读取一行数据。
此外,还有我们早已熟悉的write()方法的三种重载形式。
6. DataInputStream类
介绍过两个数据I/O的接口后,介绍数据I/O流类就简单多了。DataInputStream类实现了DataInput接口,因面也就实现了这个接口的所有成员方法。此外,还有两个read()方法:
■public final int read(byte b[])
■public final int read(byte b[],int offset,int length)
重写了FilterInputStream的同名方法。
DataInputStream只有一个构造函数。像所有过滤器输入流类一样,这个构造函数的参数是InputStream的一个对象。
7.DataOutputStream类
这个类的成员方法我们都很熟悉了。除了实现DataOutput接口的方法之外,再就是一个flush()方法。write()与flush()重写了FilterOutputStream类的同名方法。
现在我们可以回过头来再看一下例7.3,印证一下刚才讲过的内容。这个例子的重点之一是演示过滤器的“连接”,另一个是介绍相应的类。
5.3 例2:行号与“可推回”的流
在下面的例子(例4)中,我们将进一步理解过滤器的连接问题。前面例子基本上用的都是字节流类,这个例子使用字符流类。
例4 FilterIODemo2.java。
1:import java.io.*;
3:public class FilterIODemo2{
4: public static void main(String args[]) throws IOException{
5: String s="this is a multi-line string./n It is /nused to demo filterIO./n";
6: char array[]=new char[s.length()];
7: for(int i=0;i<s.length();++i)
8: array[i]=s.charAt(i);
//创建字符流,串接过滤器
9: CharArrayReader charReader = new CharArrayReader(array);
10: PushbackReader pushReader = new PushbackReader(charReader);
11: LineNumberReader lineReader = new LineNumberReader(pushReader);
12: String line;
//读字符流,加行号输出
13: while((line = lineReader.readLine())!=null){
14: System.out.println(lineReader.getLineNumber()+":"+line);
15: }
//指针置到开头
16: try{ pushReader.reset();}catch(IOException e){}
//读字符流,每读到一个'/n'就把它推回
17: boolean eof = false;
18: boolean met = false;
19: while(!eof){
20: int c=pushReader.read();
21: if(c==-1) eof=true;
22: else if(((char)c=='/n')&&!met){met =true;pushReader.unread(c);}
23: else met =false;
24: if(c!=-1) System.out.print((char)c);
25: }
26: System.out.flush();
27: pushReader.close();
28: charReader.close();
29: lineReader.close();
30: }
31:}
该程序的运行结果如下:(略)
这个例子的功能是:给一个字符串加上行号后输出;把每个换行符都“重复”一次,即每次换行时加一个空行。该例子使用的是字符流I/O,演示了几个类的使用:CharArrayReader,PushbackReader,LineNumberReader。此外,我们还可以复习一下前面提到的几个流类。
PushbackReader,顾名思义是是可以把数据“推回”输入流的流类。我们用它来实现对换行符的重复——只要读完后把“推回去”,下次就可再读一遍了。LineNumberReader可以追踪输入的行数,用它来实现加行号输出。
现在来讲解一下程序。第5行中,在main()方法的开始,定义了一个字符串s。其中,有三个换行符‘/n’。然后创建一个字节数组Array[],并在接下来的for循环(第7、8行)中为它赋值。以此为参数创建了一个内存缓冲区输入流的对象。这就是我们一串过滤器的源点。注意array并不是一个输入流,相应的CaarArrayReader也不是一个过滤器。
现在考虑选用过滤器。可根据我们想要的功能来选择。既然我们要行号,那么显然最好是一行一行读数据。BufferedReader的readLine()方法正是我们需要。(readLine()方法本来是DataInputStream类的方法,但在1.1版中过时了。详细情况在第一节中已有说明。这里用DataInputStream也是可以的但编译时会警告信息。)加行号我们可以一行一行地读,也可以自己高于个变量来累计行数。当然也可以利用一个现成的类和现在的方法——选择LineNumbdrReader类及其getLineNumber()方法。由于LineNumbdrReader本身是BuffredReader类的子类,可以直接用它来逐行读数据,不必再引入BufferedReader类。为了重复写回画换行符可选用PushbackInputStream类和它的unread()方法。
下面的任务是把它们串起来,如例子所示,可将它些过滤器一个“输出”作为下一个的“输入”。第一个while循环(第13到15行)中做的事很简单;读一行信息,取得其行号,然后一些输出。
第二个while循环(第19行到25行)的工作是重写操作符。我们用pushReader来读数据。布尔量eof来标识输入是否结束,met用来标识当瓣换行符是否被推回过。当输入没有结束时,每读到一个‘/n’ 时,就不会再“推回”了,保证换行符只被重复一次。
正如前面所提到过的,一串过滤器中的任一个都可以操作数据,无论该过滤器是最先的或最末的或是中间的任何一个。
由于我们是用print()方法来输出字符的,程序结束时可能还有一部分数据在缓冲区中,没被写到屏幕上。因此我们加了一个flush()方法强制显示到屏幕上。
将用过的流都关闭(第27到29行)是一种好的编辑风格。虽然Java的“垃圾收集”系统可以回收废弃不用的资源,仍应自觉地打扫“战场”,把能回收的资源主动回收。
5.4 类库支持
下面详细介绍一下例7.4中新出现的类。有一点需要解释,就是字符流I/O类与字节流I/O类的继承关系并不是一一对应的。比如,字节流I/O类中的PrintStream是FilterOutputStream的子类,而对应的字符流类PrintWriter却是Writer类的子类。因此,严格地说PrintWriter并非过滤器类。但是,为了能够分六别类地研究这些类,我们不苛求这个差别,而是按照字节流I/O类的继承关系,对应地把相应字符流I/O类也看作过滤器类。
1.PushbackReader类
构造函数两个:
■public PushbackReader(Reader in,int size)
创建缓冲区大小为size的一个PushbackReader对象。
■public PushbackReader(Reader in)
创建缓冲区大小为一个字符的一个PushbackReader对象。
方法:
■public int read()
■public int read(char cbuf[],int offset,int length)
读数据。
■public void unread(int ch)
回退一个字符。当缓冲区满或发生其它输入输出的异常情况时,抛出I/O异常。
■public int avaliable()
返回缓冲区内字节个数。
■public boolean markSupported()
确认输入流是否支持标记功能。
read()、unread()、available()均可能抛出IOException。
2.LineNumberReader类
构造函数两个,与PushbackReader类似。
下面列出方法的原型,其中我们已经熟悉的,在此就不给出解释了。
■public int read() throws IOException
■public int read(char cbuf[],int offset,int length) throws IOException
■public void setLineNumber(int lineNumber)
设置行号。
■public int getLineNumber()
读行号。
■public long skip(long n) throws IOException
■public int available()throws IOException
■public void mark(int readAheadLimit)throws IOException
在当前位置作标记。从此读取readAheadLimit个字符后标记变为无效。
■public void reset()throws IOException
返回到上一标记。
3.PrintStream类和PrintWriter类
PrintStream类是过滤器类中一个不可忽视的成员,最基本的标准输出就要借助于它——我们常用的System.out变量就是PrintStream实例。与之对应的字符流类是PrintWriter类。
PrintStream有两个构造函数(在新版API中已标记为过时):
■public PrintStream(OutputStream out)
■public PrintStream(OutputStream out,boolean autoFlush)
其中,autoFlush置为true时,每当输出遇到换行符,缓冲区的内容就被强制全部输出,如同调用了一次flush()。但要注意,如果没遇到换行符,还是会有数据“憋”在缓冲区里。
方法(已熟悉的就不解释):
■public void write(int b)
■public void write(byte b,int offset,int length)
■public void flush()
■public void close()
■public void print(Object obj)
这个方法功能是非常强大的,它可以输出任何对象,而不必另加说明。此外print()方法有许多重载形式,即有多种参数。它们是字符串(String)、字符数组(char[])、字符(char)、整数(int)、长整数(long)、浮点数(float)、双精度浮点数(double)、布尔值(boolean)。其中,输出多个数单位的print()方法(也就是指参数为String和char[]的)是同步(synchronized)方法。
■public void println()输出一个换行符。
■public synchronized void println(Object obj)
println()方法有9个重载形式,几乎就是print()方法的翻版。唯一的区别在于println()方法都是同步的。
■public boolean checkError()
检查输出过程中有什么错误,如有,返回true值。只要输出流中出现一次错误,则出错后的任意对checkError()的调用均会返回真值。
下面介绍PrintWriter类。
如同第二节中所说,PrintWriter是JDK1.1版增加了与字节流I/O相对应的字符流I/O。但是,为了保持兼容性,原先的类几乎没有改动。再加之调试的需要,PrintStream类被保留,并且System类中的成员变量out、err仍作为它的对象。然而,PrintWriter用于大多数输出比PrintStream更为合适。因此1.1版的API中建议新开发的代码使用PrintWriter类,并将 PrintStream类的两个构造函数标记为过时。这样,虽然使用System.out输出不会产生问题,在程序中创建新的PrintStream对象时却会产生编译时的警告。
PrintWriter类与PrintStream类的方法是对应的。有一个不同之外需提请读者注意,就是当前者的自动清空缓冲区的功能被使能时(构造函数中autoFlush置为true),仅当println()方法被调用时才自动清缓冲区,而不是像PrintStream一样遇到一个换行符就清缓冲。
到此为止,我们已介绍了各种类型的过滤器I/O类。适用于字节流和字符的各种对应过滤器类,其方法也是对应的。因此,对没有介绍的类读者可以从其对应类推理其功能。
6 管道I/O
管道I/O是专门用于线程通信的。对于字节流Java提供了两个类,PipedInputStream类被线程用来写字节数据。两个管道I/O流对象可以连接起来,这样一个线程写的数据就可以被另一个线程来读。对于字符流也有两个类,分别叫做PipedReader和PipedWriter。我们只详细介绍字节流的管道I/O类。
6.1 PipedInputStream类
这个类有两个构造函数。一个无参,用它建立起输入流后,需将它与一个管道输出流相连接。另一个以管道输出流(PipedOutputStream)对象为参数,创建一个与该输出流对象相连接的输入流。
PipedInputStream类的所有方法均可能抛出IOException。
■public void connect (PipedOutputStream src)
将输入流连接到某管道输出流。
■public synchronized int read()
■public synchronized int read(byte b[],int offset,int length)
读数据。
■public void close()
关闭流。
6.2 PipedOutputStream类
与PipedInputStream类完全对应,它有两个构造函数,其中一个以PipedInputStream对象为参数,另一个无参。成员方法也是包括connect(),close(),另外还有两种形式的write()方法,这里就不细述了。
6.3 程序示例
下面用一个示例(例5)具体演示管道I/O的使用。
例5 PipeIODemo.java
1: import java.lang.*;
2: import java.io.PipedInputStream;
3: import java.io.PipedOutputStream;
4: import java.io.IOException;
5:
6: public class PipeIODemo{
7: public static void main(String args[]){
//这里的Reader和Writer不是字符流输入输出的基本类,而是下文自定义的
8: Reader thread1=new Reader("1");
9: Writer thread2=new Writer("2");
//联接管道
10: try{
11: thread2.pipeOut.connect(thread1.pipeIn);
12: }catch(IOException ex){
13: System.out.println("IOException occured when connecting two stream");
14: }
//启动线程
15: thread1.start();
16: thread2.start();
//循环,等线程均结束后程序中止
17: do{
18: }while(thread1.isAlive()||thread2.isAlive());
19: System.out.println("All over!");
20: }
21:}
//自定义读者类
22:class Reader extends Thread{
23: public PipedInputStream pipeIn;
24: String threadName;
25: public Reader(String name){
26: super();
27: threadName = name;
28: pipeIn = new PipedInputStream();
29: }
30: public void run(){
31: try{
32: boolean over = false;
33: while(!over){
34: int ch=pipeIn.read();
35: try{
36: Thread.sleep(200);
37: }catch(InterruptedException ex){
38: System.out.println("Sleep is interrupted!");
39: }
40: if(ch=='.') over = true;
41: else System.out.println("Thread "+threadName+" read "+(char)ch);
42: }
43:
44: }catch(IOException ex){
45: System.out.println("IOException occured when try to read data");
46: }
47: }
48:}
//自定义写者类
49:class Writer extends Thread{
50: public PipedOutputStream pipeOut;
51: String threadName;
//待写内容
52: String content = "orange apple";
53: public Writer(String name){
54: super();
55: threadName=name;
56: pipeOut = new PipedOutputStream();
57: }
58: public void run(){
59: try{
//将字符串内容逐字输出
60: for(int i=0;i<content.length();++i){
61: pipeOut.write(content.charAt(i));
62: try{
63: Thread.sleep(200);
64: }catch(InterruptedException ex){
65: System.out.println("Sleep is interrupted!");
66: }
67: System.out.println("Thread "+threadName+" wrote "+content.charAt(i));
68: }
69: pipeOut.write('.');
70: }catch(IOException ex){
71: System.out.println("IOException occured when try to write data");
72: }
73: }
74:}
该程序的运行结果如下:(略)
这个例子功能很简单。两个线程,一个是读者,一个是写者,读者取写者所写的内容。双方约定以‘.’为结束符。
这个例子演示了管道I/O一般过程,首先是创建管理I/O流类对象。这个工作是在Reader和Writer类的构造函数中做的(第28、56行)。因此当我们创建了thread1和thread2两个线程时,pipeIn和pipeOut就被创建了。然后我们把它们连接起来,再启动两个线程工作,最后打印“All Over!” 表示运行结束。
可以看出,读线程与写线程实际上是不必关心对方的情况的。它们的工作就是读或写,每处理一个字符输出一条信息表明自己做过的工作。我们在pipeIn的输出信息中加了一大段空格,这样的目的是使两个线程的输出能容易分辨。另外,让两个线程处理一个字符就睡眠(sleep) 一会儿并不是必须的,这样只是为了增加线程交替执行的机会。如果去年这一段,可能执行数次者不出现thread1、thread2交替输出信息的现象,容易被误解为两个线程必须一个死亡才执行另一个。另外,作为结束符的“.” 并没有显示出来。
这个例子实现的是单向通信。实际上,为每个线程都分别创建输入流对象和输出流对象,再分别连接起来,就可以实现双向通信。读者有兴趣不妨一试。
7 java.io包中的其它类
7.1 SequenceInputStream类
这个类的功能是合并多个输入流。其构造函数有两个,一个以枚举(Enumeration)对象为参数, 一个以两个InputStream对象为参数。方法则有两个read()方法,分别读一个字符、读数据入字节数组中的一段。再就是一个close()方法。例7.6利用它来实现了两上文件的并接。其中还使用了ByteArrayOutputStream,用意是将两个文件并接的结果先在内存缓冲区中暂存一下。这个例子允许目的的文件是两个源文件之一。
例6 FileCat.java
import java.lang.System;
import java.io.*;
public class FileCat{
public static void main(String args[]){
SequenceInputStream seqIn;
if(args.length!=3){System.out.println("Usage:java FileCat filesrc filesrc filedst");}
else{
try{
FileInputStream f1=new FileInputStream(args[0]);
FileInputStream f2=new FileInputStream(args[1]);
seqIn=new SequenceInputStream(f1,f2);
ByteArrayOutputStream byteArrayOut=new ByteArrayOutputStream();
boolean eof=false;
int byteCount=0;
while(!eof){
int c=seqIn.read();
if(c==-1)eof=true;
else{
//将读到的数据写入字节数组输出流
byteArrayOut.write((char)c);
++byteCount;
}
}
FileOutputStream outStream=new FileOutputStream(args[2]);
//将数据写入文件
byteArrayOut.writeTo(outStream);
System.out.println(byteCount+" bytes were read.");
seqIn.close();
outStream.close();
byteArrayOut.close();
f1.close();
f2.close();
}catch(FileNotFoundException ex){
System.out.println("Cannot open source files.Please check if they"+
"exists and allows freading.");
}catch(IOException ex){
System.out.println("IOexception occured!");
}
}
}
}
7.2 Streamtokenizer类
这个类是用来构造词法分析器的。缺省情况下,它可以识别数值、字母以及字符串。它的构造函数只有一个,以输入流(inputStream)对象为参数。本节我们给出一个例子(例7.7),并介绍例子中出现的该类的部分方法。
例7.7 TokenIODemo.java。
1:import java.io.IOException ;
2:import java.lang.System;
3:import java.io.InputStreamReader ;
4:import java.io.StreamTokenizer;
5:import java.io.FileInputStream ;
6:
7:public class TokenIODemo{
8: public static void main(String args[]) throws IOException{
//从文件创建输入流
9: FileInputStream fileIn = new FileInputStream ("hello.c");
//从字节流创建字符流
10: InputStreamReader inReader = new InputStreamReader (fileIn);
11: StreamTokenizer tokenStream = new StreamTokenizer (inReader);
//设置注释风格
12: tokenStream.slashStarComments(true);
13: tokenStream.slashSlashComments (true);
//识别行结束符;如果参数为假,将行结束符视作空白符
14: tokenStream.eolIsSignificant (true);
//设置引号的符号表示
15: tokenStream.quoteChar ('"');
//将ASCII码为0-32的字符设为空白符
16: tokenStream.whitespaceChars (0,32);
17: boolean eof = false;
18: do{
19: int token = tokenStream.nextToken ();
20: switch(token){
//文件结束符
21: case tokenStream.TT_EOF :
22: System.out.print(" EOF ");
23: eof=true;
24: break;
//行结束符
25: case tokenStream.TT_EOL :
26: System.out.print (" EOL ");
27: break;
//单词
28: case tokenStream.TT_WORD :
29: System.out.print (" Word "+tokenStream.sval );
30: break;
//数字
31: case tokenStream.TT_NUMBER :
32: System.out.print(" Number "+tokenStream.nval );
33: break;
34: default:
35: System.out.print(" "+(char)token);
36: }
37: }while(!eof);
38: System.out.flush();
39: }
40:}
下面是该例的运行结果:
E:/>java TokenIODemo
# Word include < Word stdio.h > EOL EOL Word main ( ) { EOL Word print ( " ,
Number 1234.0 ) ; EOL EOL } EOL EOF
E:/>
其中,hello.c程序的源代码如下:
#include <stdio.h>
//To say "hello world"
main(){
print("hello world %d/n",1234);
/* It is a test for TokenIODemo*/
}
例子中我们用到了这样一些方法:
■public void whitespaceChars(int low,int hi)
把给定范围的字符设为空格(不可见)字符。类似的方法还有wordChars()(设为单词字符),ordinaryChars()(设置为除了单词字符、数据字符等有实际含义字符之外的其它字符)。
■public void slachStarComments(boolean flag)
■public void slachSlashComments(boolean flag)
flag为真,则可训别相应风格的注释。前者(slashStar)指C风格的注释(/*...*/)。后者指C++风格的注释“//”。
■public int nextToken()
从输入流取得下一个词法分析单位。
■public void eolIsSingnificant(boolean flag)
如果参数为真,识别行结束符;否则,将行结束符视作空白符。
例子中还用到了一些常量和变量。TT_EOF、TT_EOL、TT_NUMBER、TT_WORD分别表示文件结束符、行结束符、数值和单词。public String sval是指字符串值;public double nval指双精度值。这些常量、变量的使用在例子中已有明确的演示,这里就不多说了。
7.3 FilenameFilter接口
这个接口不太常用,只提供了一个方法:
■public abstract boolean accept(File dir,String fileName)
功能是确定某一文件列表是否包含了指定的文件。
7.4 Serializable接口
实现这一接口的类可以被“串行化”,即它们的对象可以被转化为某种形式,该形式可以被输入输出,而保存对象的结构。也就是说,只有实现了这一接口,类的对象才能被完整地输入输出和存储。
该接口不含任何方法和变量,它只充当一个标记。编程时只要在类定义时中上:
... implements Serializable
即可使该类的对象具有“串行性” 。
小结
在这一章中,我们比较全面地介绍了java.io包中的类和接口,并给出了示例。读者通过这一章学习,应掌握java的输入输出类,并将种I/O手段灵活运用于自编的程序之中。