一、输入和输出
编程语言的I/O类库中常使用流这个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。“流”屏蔽了实际的I/O设备中处理数据的细节。
Java类库中的I/O类分成输入和输出两部分。通过继承,任何自Inputstream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或者字节数组。同样,任何自OutputStream或Writer派生而来的类都含有名为write()的基本方法,用于写单个字节或字节数组。但是,我们通常不会用到这些方法,他们之所以存在是因为别的类可以使用他们,以便提供更有用的接口。因此,我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是装饰器设计模式)。实际上,Java中“流”类库让人迷惑的主要原因在于:创建单一的结果流,却需要创建多个对象。
1、InputStream类型
InputStream的作用是用来表示那些从不同数据源产生输入的类。这些数据源包括:
- 字节数组
- String对象
- 文件
- “管道”,工作方式yu与实际管道相似,即从一端输入,从另一端输出。
- 一个由其他种类的流组成的序列,以便我们可以将他们手机合并到一个流内。
- 其他数据源,如Internet连接等。
每一种数据源都有相应的InputStream子类。另外,FilterInputStream也属于一种InputStream,为“装饰器(decorator)”类提供积累。其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起。
类 | 功能 | 构造器参数和如何使用 |
---|---|---|
ByteArrayInputStream | 允许将内存的缓冲区当做inputStream使用 | 缓冲区,字节将从中取出;作为一种数据源:将其与FilterInputStream对象相连以提供有用接口 |
StringBufferInputStream | 将String转换成InputStream | 字符串。底层实现实际使用StringBuffer;作为一种数据源:将其与FilterInputStream对象相连以提供有用接口 |
FileInputStream | 用于从文件中读取信息 | 字符串,表示文件名、文件或FileDescriptor对象;作为一种数据源:将其与FilterInputStream对象相连以提供有用接口 |
PipedInputStream | 产生用于写入相关PipedOutputStream的数据。实现管道化概念 | PipedOutputStream;作为多线程中的数据源:将其与FilterInputStream对象相连以提供有用接口 |
SequenceInputStream | 将两个或多个InputStream对象转换成单一InputStream | 两个InputStream对象或一个容纳InputStream对象的容器Enumeration;作为一种数据源:将其与FilterInputStream对象相连以提供有用接口 |
FilterInputStream | 抽象类,作为“装饰器”的接口。其中装饰器为其他的InputStream类提供有用功能。 | 见下表 |
2、OutputStream类型
该类别的类决定了输入要去往的目标:字节数组(但不是String,不过可以用字节数组自己创建)、文件或管道。
另外,FilterOutputStream为“装饰器”类提供了一个基类,“装饰器”类把属性或者有用的接口与输出流连接了起来。
类 | 功能 | 构造器参数 | 如何使用 |
---|---|---|---|
ByteArrayOutputStream | 在内存中创建缓冲区。所有送往“流”的数据都要放置在此缓冲区 | 缓存区初始化尺寸(可选的) | 用于指定数据的目的地:将其与FilterOutputStream对象相连以提供有用的接口 |
FileOutputStream | 用于将信息写至文件 | 字符串,表示文件名、文件或FileDescriptor对象 | 指定数据的目的地:将其与FilterOutputStream对象相连以提供有用的接口 |
PipedOutputStream | 任何写入其中的信息dou’h都会自动作为相关PipedInputStream的输出。实现管道化概念 | PipedInputStream | 指定用于多线程的数据的目的地:将其与FilterOutputStream对象相连以提供有用接口 |
FilterOutputStream | 抽象类,作为装饰器的接口。其中装饰器为其他OutputStream提供有用功能。 | 见下表 | 见下表 |
二、Reader和Writer
尽管一些原始的“流”类库不再被使用,但是InputStream和OutputStream在以面向字节形式的I/O中仍可以提供极有价值的功能,Reader和Writer则提供兼容Unicode与面向字符的I/O功能。另外:
- Java1.1向InputStream和OutputStream继承层次结构中添加了一些新类,所以显然这两个类是不会被取代的。
- 有时我们必须把来自于“字节”层次结构中的类和“字符”层次结构中的类结合起来使用。为了实现这个目的,要用到“适配器”类:InputStreamReader可以把InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writer。
设计Reader和Writer继承层次结构主要是为了国际化。老的I/O流继承层次结构仅支持8位字节流,并且不能很好地处理16位的Unicode字符。由于Unicode用于字符国际化(java本身的char也是16位的Unicode),所以添加Reader和Writer继承层次结构就是为了在所有的I/O操作中都支持unicode。另外,新类库的设计使得它的操作比旧类库更快。
三、新I/O:NIO
速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。我们可以把它想象成一个煤矿,通常是一个包含煤层(数据)的矿藏,而缓冲器则是派送到矿藏的卡车。卡车满载煤炭而归,我们从卡车上获得煤炭。也就是我们没有和通道直接交互,我们只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
唯一直接与通道交互的缓冲器是ByteBuffer–也就是说,可以存储未加工字节的缓冲器。当我们查询JDK文档中的java.nio.ByteBuffer时,会发现他是相当基础的类:通过告知分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据。但是,没办法输出或读取对象,即使是字符串对象也不行。这种处理虽然很低级,但却正好,因为这是大多数操作系统中更有效的映射方式。
旧I/O类库中有三个类被修改了,用以产生FileChannel。这三个被修改的类是FileInputStream、FileOutputStream以及用于既读又写的RandomAccessFile。注意这些是字节操纵流,与低层的NIO性质一致。Reader和Writer这种字符模式类不能用于产生通道,但是java.nio.channels.Channels类提供了实用方法,用以在tong’dao’zho通道中产生Reader和Writer。
四、对象序列化
Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可以通过网络进行;这意味着序列化机制能够自动弥补不同操作系统之间的差异。也就是说,可以在运行windows系统的计算机上创建一个对象,将其序列化,通过网络将它发送给一台运行Unix系统的计算机,然后在那里准确的重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心字节的顺序或者其他任何细节。
就其本身来说,对象的序列化是非常有趣的,因为利用它可以实现轻量级持久性(lightweight persistence)。持久性一位置一个对象的生存周期并不取决于程序是否正在执行;他可以生存与程序的调用之间。通过j一个序列化对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久性的效果。之所以称其为“轻量级”,是因为不能用某种“persistent”关键字来简单的定义一个对象,并让系统自动维护其他细节。相反,对象必须在程序中显式的序列化(serialize)和反序列化(deserialize)还原。如果需要一个更严格的持久性机制,可以考虑像Hibernate之类的工具。
对象序列化的概念加入到语言中是为了支持两种主要特性。一是java的远程方法调用RMI,它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值。
再者,对Java Beans来说,对象的序列化也是必须的。使用一个Bean时,一般情况下是在设计阶段对他的状态信息进行配置。这种状态信息必须保存下来,并在程序启动时进行后期恢复;这种具体工作就是由对象序列化完成的。
只要对象实现了Serializable接口(该接口仅是一个标机接口,不包含任何方法),对象的序列化处理就会非常简单。当序列化的概念被加入到语言中时,许多标准类库发生了改变,以便具备序列化特性-其中包括所有基本数据类型的封装器,所有容器类以及许多其他的东西。甚至Class对象也可以被序列化。
要序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内。这时,只需调用writeObject()即可将对象序列化,并将其发送给outputStream(对象序列化是基于字节的,因此要使用InputStream和OutputStream继承层次结构)。要反向进行该过程(即将一个序列还原为一个对象),需要将一个InputStream封装在ObjectInputStream内,然后调用readObject()。和往常一样,我们最后获得的是一个引用,它只想一个向上转型的Object,所以必须向下转型才能直接设置他们。
对象序列化特别聪明的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有引用,并保存那些对象;接着又能对对象内包含的每个这样的引用进行追踪;以此类推。这种情况有时被称为“对象网”,单个对象可与之建立连接,而且它还包含了对象的引用数组以及成员对象。如果必须保持一套自己的对象序列化机制,那么维护那些可追踪到所有链接的代码可能会显得非常麻烦。然后,由于java的对象序列化似乎找不出什么缺点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。
五、总结
Java I/O流类库的确能满足我们的基本需求:我们可以通过控制台、文件、内存块,甚至因特网进行读写。通过继承,我们可以创建新类型的输入和输出对象。并且通过重新定义toString()方法,我们甚至可以对流接受的对象类型进行简单扩充。当我们向一个期望收到字符串的方法传送一个对象时,会自动调用toString()方法(这是java有限的自动类型转换功能)。
在I/O流类库的文档和设计中,仍留有一些没有解决的问题。I/O流类库使我们喜忧参半。它确实能做许多事情,而且具有可移植性。但是如果我们没有理解装饰器模式,那么这种设计就不是很直觉,因此在学习和传授它的过程中,需要额外的开销。而且它并不完善。
一旦我们理解了装饰器模式,并开始在某些情况下使用该类库以利用其提供的灵活性,那么你就开始从这个设计中受益了。到那个时候,为此额外多写几行代码的开销应该不至于使人觉得太麻烦。