第10章JavaIO系统“对语言设计人员来说,创建好的输入/输出系统是一项特别困难的任务。”由于存在大量不同的设计方案,所以该任务的困难性是很容易证明的。其中最大的挑战似乎是如何覆盖所有可能的因素。不仅有三种不同的种类的I/O需要考虑(文件、控制台、网络连接),而且需要通过大量不同的方式与它们通信(顺序、随机访问、二进制、字符、按行、按字等等)。Java库的设计者通过创建大量类来攻克这个难题。事实上,Java的I/O系统采用了如此多的类,以致刚开始会产生不知从何处入手的感觉(具有讽刺意味的是,Java的I/O设计初衷实际要求避免过多的类)。从Java1.0升级到Java1.1后,I/O库的设计也发生了显著的变化。此时并非简单地用新库替换旧库,Sun的设计人员对原来的库进行了大手笔的扩展,添加了大量新的内容。因此,我们有时不得不混合使用新库与旧库,产生令人无奈的复杂代码。本章将帮助大家理解标准Java库内的各种I/O类,并学习如何使用它们。本章的第一部分将介绍“旧”的Java1.0I/O流库,因为现在有大量代码仍在使用那个库。本章剩下的部分将为大家引入Java1.1I/O库的一些新特性。注意若用Java1.1编译器来编译本章第一部分介绍的部分代码,可能会得到一条“不建议使用该特性”(Deprecatedfeature)警告消息。代码仍然能够使用;编译器只是建议我们换用本章后面要讲述的一些新特性。但我们这样做是有价值的,因为可以更清楚地认识老方法与新方法之间的一些差异,从而加深我们的理解(并可顺利阅读为Java1.0写的代码)。10.1输入和输出可将Java库的I/O类分割为输入与输出两个部分,这一点在用Web浏览器阅读联机Java类文档时便可知道。通过继承,从InputStream(输入流)衍生的所有类都拥有名为read()的基本方法,用于读取单个字节或者字节数组。类似地,从OutputStream衍生的所有类都拥有基本方法write(),用于写入单个字节或者字节数组。然而,我们通常不会用到这些方法;它们之所以存在,是因为更复杂的类可以利用它们,以便提供一个更有用的接口。因此,我们很少用单个类创建自己的系统对象。一般情况下,我们都是将多个对象重叠在一起,提供自己期望的功能。我们之所以感到Java的流库(StreamLibrary)异常复杂,正是由于为了创建单独一个结果流,却需要创建多个对象的缘故。很有必要按照功能对类进行分类。库的设计者首先决定与输入有关的所有类都从InputStream继承,而与输出有关的所有类都从OutputStream继承。10.1.1InputStream的类型InputStream的作用是标志那些从不同起源地产生输入的类。这些起源地包括(每个都有一个相关的InputStream子类):(1)字节数组(2)String对象(3)文件(4)“管道”,它的工作原理与现实生活中的管道类似:将一些东西置入一端,它们在另一端出来。(5)一系列其他流,以便我们将其统一收集到单独一个流内。(6)其他起源地,如Internet连接等(将在本书后面的部分讲述)。除此以外,FilterInputStream也属于InputStream的一种类型,用它可为“破坏器”类提供一个基础类,以便将属性或者有用的接口同输入流连接到一起。这将在以后讨论。表10.1InputStream的类型类功能构建器参数/如何使用ByteArrayInputStream允许内存中的一个缓冲区作为InputStream使用从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口StringBufferInputStream将一个String转换成InputStream一个String(字串)。基础的实施方案实际采用一个StringBuffer(字串缓冲)/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口FileInputStream用于从文件读取信息代表文件名的一个String,或者一个File或FileDescriptor对象/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口PipedInputString产生为相关的PipedOutputStream写的数据。实现了“管道化”的概念PipedOutputStream/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口SequenceInputStream将两个或更多的InputStream对象转换成单个InputStream使用两个InputStream对象或者一个Enumeration,用于InputStream对象的一个容器/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口FilterInputStream对作为破坏器接口使用的类进行抽象;那个破坏器为其他InputStream类提供了有用的功能。参见表10.3参见表10.3/参见表10.310.1.2OutputStream的类型这一类别包括的类决定了我们的输入往何处去:一个字节数组(但没有String;假定我们可用字节数组创建一个);一个文件;或者一个“管道”。除此以外,FilterOutputStream为“破坏器”类提供了一个基础类,它将属性或者有用的接口同输出流连接起来。这将在以后讨论。表10.2OutputStream的类型类功能构建器参数/如何使用ByteArrayOutputStream在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区。可选缓冲区的初始大小/用于指出数据的目的地。若将其同FilterOutputStream对象连接到一起,可提供一个有用的接口FileOutputStream将信息发给一个文件用一个String代表文件名,或选用一个File或FileDescriptor对象/用于指出数据的目的地。若将其同FilterOutputStream对象连接到一起,可提供一个有用的接口PipedOutputStream我们写给它的任何信息都会自动成为相关的PipedInputStream的输出。实现了“管道化”的概念PipedInputStream/为多线程处理指出自己数据的目的地/将其同FilterOutputStream对象连接到一起,便可提供一个有用的接口FilterOutputStream对作为破坏器接口使用的类进行抽象处理;那个破坏器为其他OutputStream类提供了有用的功能。参见表10.4参见表10.4/参见表10.410.2增添属性和有用的接口利用层次化对象动态和透明地添加单个对象的能力的做法叫作“装饰器”(Decorator)方案——“方案”属于本书第16章的主题(注释①)。装饰器方案规定封装于初始化对象中的所有对象都拥有相同的接口,以便利用装饰器的“透明”性质——我们将相同的消息发给一个对象,无论它是否已被“装饰”。这正是在JavaI/O库里存在“过滤器”(Filter)类的原因:抽象的“过滤器”类是所有装饰器的基础类(装饰器必须拥有与它装饰的那个对象相同的接口,但装饰器亦可对接口作出扩展,这种情况见诸于几个特殊的“过滤器”类中)。子类处理要求大量子类对每种可能的组合提供支持时,便经常会用到装饰器——由于组合形式太多,造成子类处理变得不切实际。JavaI/O库要求许多不同的特性组合方案,这正是装饰器方案显得特别有用的原因。但是,装饰器方案也有自己的一个缺点。在我们写一个程序的时候,装饰器为我们提供了大得多的灵活性(因为可以方便地混合与匹配属性),但它们也使自己的代码变得更加复杂。原因在于JavaI/O库操作不便,我们必须创建许多类——“核心”I/O类型加上所有装饰器——才能得到自己希望的单个I/O对象。FilterInputStream和FilterOutputStream(这两个名字不十分直观)提供了相应的装饰器接口,用于控制一个特定的输入流(InputStream)或者输出流(OutputStream)。它们分别是从InputStream和OutputStream衍生出来的。此外,它们都属于抽象类,在理论上为我们与一个流的不同通信手段都提供了一个通用的接口。事实上,FilterInputStream和FilterOutputStream只是简单地模仿了自己的基础类,它们是一个装饰器的基本要求。10.2.1通过FilterInputStream从InputStream里读入数据FilterInputStream类要完成两件全然不同的事情。其中,DataInputStream允许我们读取不同的基本类型数据以及String对象(所有方法都以“read”开头,比如readByte(),readFloat()等等)。伴随对应的DataOutputStream,我们可通过数据“流”将基本类型的数据从一个地方搬到另一个地方。这些“地方”是由表10.1总结的那些类决定的。若读取块内的数据,并自己进行解析,就不需要用到DataInputStream。但在其他许多情况下,我们一般都想用它对自己读入的数据进行自动格式化。剩下的类用于修改InputStream的内部行为方式:是否进行缓冲,是否跟踪自己读入的数据行,以及是否能够推回一个字符等等。后两种类看起来特别象提供对构建一个编译器的支持(换言之,添加它们为了支持Java编译器的构建),所以在常规编程中一般都用不着它们。也许几乎每次都要缓冲自己的输入,无论连接的是哪个IO设备。所以IO库最明智的做法就是将未缓冲输入作为一种特殊情况处理,同时将缓冲输入接纳为标准做法。表10.3FilterInputStream的类型类功能构建器参数/如何使用DataInputStream与DataOutputStream联合使用,使自己能以机动方式读取一个流中的基本数据类型(int,char,long等等)InputStream/包含了一个完整的接口,以便读取基本数据类型BufferedInputStream避免每次想要更多数据时都进行物理性的读取,告诉它“请先在缓冲区里找”InputStream,没有可选的缓冲区大小/本身并不能提供一个接口,只是发出使用缓冲区的要求。要求同一个接口对象连接到一起LineNumberInputStream跟踪输入流中的行号;可调用getLineNumber()以及setLineNumber(int)只是添加对数据行编号的能力,所以可能需要同一个真正的接口对象连接PushbackInputStream有一个字节的后推缓冲区,以便后推读入的上一个字符InputStream/通常由编译器在扫描器中使用,因为Java编译器需要它。一般不在自己的代码中使用10.2.2通过FilterOutputStream向OutputStream里写入数据与DataInputStream对应的是DataOutputStream,后者对各个基本数据类型以及String对象进行格式化,并将其置入一个数据“流”中,以便任何机器上的DataInputStream都能正常地读取它们。所有方法都以“wirte”开头,例如writeByte(),writeFloat()等等。若想进行一些真正的格式化输出,比如输出到控制台,请使用PrintStream。利用它可以打印出所有基本数据类型以及String对象,并可采用一种易于查看的格式。这与DataOutputStream正好相反,后者的目标是将那些数据置入一个数据流中,以便DataInputStream能够方便地重新构造它们。System.out静态对象是一个PrintStream。PrintStream内两个重要的方法是print()和println()。它们已进行了覆盖处理,可打印出所有数据类型。print()和println()之间的差异是后者在操作完