Java编程思想13——Java I/O系统

创建一个好的输入/输出(I/O)系统是一项艰难的任务。挑战似乎来自于要涵盖所有的可能性。不仅存在各种I/O源端和想要与之通信的接收端(文件、 控制台、 网络链接等),而且还需要以多种不同 的方式与它们进行通信(顺序、 随机存取、 缓冲 、 二进制、 按字符、 按行、 按字等)。

如果缺乏历史的眼光, 很快我们就会对什么时候该使用哪些类,以及什么时候不该使用它们而感到迷惑。

1.File类

既能代表一个特定文件的名称, 又能代表一个目录下的一组文件的名称。如果它指的是一个文件集,我们就可以对此集合调用list()方法,这个方法会返回一个字符数组。

1.1 目录列表器

如果我们调用不带参数的 list()方法,便可以获得此File对象包含的全部列表。然而,如果我们想获得一个受限列表,例如,想得到所有扩展名为.java的文件,那么我们就要用到 “目录过滤器”。

1.2 目录实用工具

1.3 目录的检查及创建

File类不仅仅只代表存在的文件或目录。也可以用File对象来创建新的目录或尚不存在的整个目录路径。我们还可以查看文件的特性(如:大小,最后修改日期,读/写),检查某个File对象代表的是一个文件还是一个目录,并可以删除文件。

2.输入和输出

编程语言的I/O类库中常使用流这个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。“流”屏蔽了实际的I/O设备中处理数据的细节。

Java中“流”类库让人迷惑的主要原因就在于:创建单一的结果流,却序要创建多个对象。

2.1 lnputStream类型

InputStream的作用是用来表示那些从不同数据源产生输入的类。这些数据源包括:

  • 1)字节数组。
  • 2)String对象。
  • 3)文件。
  • 4)"管道”,工作方式与实际管道相似,即,从一端输入,从另一端输出。
  • 5)一个由其他种类的流组成的序列,以便我们可以将它们收集合并到一个流内。
  • 6)其他数据源,如Internet连接等


    Java编程思想13——Java I/O系统_第1张图片

2.2 OutputStream类型

该类别的类决定了输出所要去往的目标:字节数组(但不是String, 不过你当然可以用字节数组自己创建)、 文件或管道。


Java编程思想13——Java I/O系统_第2张图片

3.添加属性和有用的接口

Java I/O类库需要多种不同功能的组合, 这正是使用装饰器模式的理由所在。
Java I/O类库操作不便的原因在于:我们必须创建许多类——“核心"I/O类型加上所有的装饰器, 才能得到我们所希望的单个I/O对象。

Fil terlnputStream和FilterOutputStream是用来提供装饰器类接口以控制特定输入流(InputStream) 和输出流 (OutputStream) 的两个类。

3.1 通过FilterlnputStream从lnputStream读取数据

我们几乎每次都要对输入进行缓冲一一不管我们正在连接的是什么I/O设备,所以,I/O类库把无缓冲输入(而不是缓冲输入)作为特殊情况(或只是方法调用)就显得更加合理了。


Java编程思想13——Java I/O系统_第3张图片

3.2 通过FilterOutPutStream向OutputStream写入

Java编程思想13——Java I/O系统_第4张图片

4.Reade和Writer

InputStream和OutputStreamt在以面向字节形式的I/O中仍可以提供极有价值的功能,Reader和Writer则提供兼容Unicode与面向字符的I/O功能。另外:

  • 1)Java 1.1向InputStream和OutputStream继承层次结构中添加了一些新类,所以显然这两个类是不会被取代的。
  • 2)有时我们必须把来自于”字节”(byte)层次结构中的类和”字符”(character)层次结构中的类结合起来使用。为了实现这个目的,要用到“适配器"(adapter)类:lnputStreamReader可以把 InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writer。

设计Reader和Writer继承层次结构主要是为了国际化。老的I/O流继承层次结构仅支持8位字节流,并且不能很好地处理16位的Unicode字符。由于Unicode用于字符国际化(Java本身的 char也是16位的Unicode),所以添加Reader和Writer继承层次结构就是为了在所有的I/O操作中都支持Unicode。另外,新类库的设计使得它的操作比旧类库更快。

4.1 数据的来源和去处

java.util.zip类库就是面向字节的而不是面向字符的。因此,最明智的做法是尽量尝试使用Reader和Writer,一且程序代码无法成功编译,我们就会发现自己不得不使用面向字节的类库。


Java编程思想13——Java I/O系统_第5张图片

4.2 更改流的行为

对千InputStream和OuputStream来说,我们会使用FilterlnputStream和FilterOutputStream 的装饰器子类来修改“流”以满足特殊需要。Reader和Writer的类继承层次结构继续沿用相同的思想,是并不完全相同。

造成这种差别的原因是因为类的组织形式不同,尽管BufferedOutputStream是FilterOutputStream的子 类,但是BufferedWriter并不是FilterWriter的子类。


Java编程思想13——Java I/O系统_第6张图片

有一点很清楚:无论我们何时使用readLine(),都不应该使用DatalnputStream(这会遭到编译器的强烈反对),而应该使用BufferedReader。除了这一点,DataInputStream仍是l/0类库
的首选成员。

4.3 未发生变化的类

Java编程思想13——Java I/O系统_第7张图片

5.自我独立的类:RandomAccessFile

RandomAccessFile适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从 一处转移到另一处,然后读取或者修改记录。文件中记录的大小不一定都相同,只要我们能够确定那些记录有多大以及它们在文件中的位置即可。

RandomAccessFlle不是InputStream或者OutputStrei皿继承层次结构中的一部分。除了实现了Datalnput和DataOutput接口(DatalnputStream和DataOutputStream也实现了这两个接口)之外,它和这两个继承层次结构没有任何关联。它甚至不使用 lnputStream和OutputStream类中已有的任何功能。它是一个完全独立的类,从头开始编写其所有的方法(大多数都是本地的)。这么做是因为RandomAccessFile拥有和别的I/O类型本质不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接从Obj派生而来。

从本质上来说,RandomAccessFile的工作方式类似千把DatalnputStream和DataOutStream 组合起来使用,还添加了一些方法。其中方法getFilePointer()用于查找当前所处的文件位置, seek()用于在文件内移至新的位置,length()用干判断文件的最大尺寸。另外,其构造器还斋要第二个参数(和C中的fopen()相同)用来指示我们只是“随机读"(r)还是“既读又写"(rw)。 它并不支持只写文件,这表明RandomAccessFile若是从DatalnputStream继承而来也可能会运行得很好。

只有RandonAccessFile支持搜寻方法,并且只适用于文件。

RandomAccessFile的大多数功能(但不是全部)由nio存储映射文件所取代。

6.I/O流的典型使用方式

6.1 缓冲输入文件

BuffferedReader in = new BufferedReader ( 
  new FileReader(filename)); 

6.2 从内存输入

StringReader in= new StringReader(
  BufferedinputFile.read("Memoryinput.java"));

6.3 格式化的内存输入

要读取格式化数据, 可以使用DatalnputStream, 它是一个面向字节的I/O类(不是面向字 符的)。 因此我们必须使用InputStream类而不是Reader类。当然, 我们可以用InputStream以字 节的形式读取任何数据(例如一个文件), 不过,在这里使用的是字符串。

DatalnputStream in= new DatalnputStream( 
  new ByteArrayInputStream( 
    BufferedlnputFile.read(
      "FormattedHemorylnput. java"). getBytes())) ; 

6.4 基本的文件输出

FileWriter对象可以向文件写人数据。首先,创建一个与指定文件连接的FileWriter。实际上,我们通常会用BufferedWriter将其包装起来用以缓冲输出(尝试移除此包装来感受对性能的影响一一缓冲往往能显著地增加I/O操作的性能)。为了提供格式化机制,它被装饰成了PrintWriter。

PrintWriter out= new PrintWriter( 
  new BufferedWriter(new FileWriter(file))); 

文本文件输出的快捷方式
Java SE5在PrintWriter中添加了一个辅助构造器,使得你不必在每次希望创建文本文件并向其中写入时,都去执行所有的装饰工作。

PrintWriter out = new PrintWriter(fHe); 

你仍旧是在进行缓存,只是不必自己去实现。遗憾的是,其他常见的写入任务都没有快捷方式,因此典型的I/O仍旧包含大量的冗余文本。

6.5 存储和恢复数据

Print Writer可以对数据进行格式化,以便人们的阅读。但是为了输出可供另一个“流”恢 复的数据,我们需要用DataOutputStream写入数据,并用DatalnputStream恢复数据。

DataOutputStream out = new DataOutputStream( 
  new BufferedOutputStream( 
    new FileOutputStream("Data.txt")));

DatalnputStream in = new DatalnputStream(
  new BufferedinputStream( 
    new FilelnputStream("Data. txt")));

如果我们使用DataOutputStream写入数据,Java保证我们可以使用DatalnputStream准确地读取数据一一无论读和写数据的平台多么不同。这一点很有价值,因为我们都知道,人们曾经花费了大址时间去处理特定千平台的数据问题。 只要两个平台上都有Java,这种问题就不会再发生。

对象序列化和XML可能是更容易的存储和读取复杂数据结构的方式。

6.6 读写随机访问文件

使用RandomAccessFile, 类似于组合使用了DatalnputStream.和DataOutputStream (因为它实现了相同的接口: Datalnput和DataOutput)。
在使用RandomAccessFile时,你必须知道文件排版, 这样才能正确地操作它。RandomAccessFile拥有读取基本类型和UTF-8字符串的各种具体方法。

你可能会考虑使用“内存映射文件”来代替RandomAccessFile。

6.7 管道流

它们的价值只有在我们开始理解多线程之后才会显现,因为管道流用于任务之间的通信。

7.文件读写的实用工具

一个很常见的程序化任务就是读取文件到内存,修改,然后再写出。JavaI/0类库的问题之一就是:它需要编写相当多的代码去执行这些常用操作。更糟糕的是,装饰器会使得要记住如何打开文件变成一件相当困难的事。因此,在我们的类库中添加帮助类就显得相当有意义,这样就可以很容易地为我们完成这些基本任务。

7.1 读取二进制文件

8.标准I/O

程序的所有输入都可以来自于标准轮入,它的所有输出也都可以发送到标准轮出,以及所有的错误信息都可以发送到标准错误。标准I/O的意义在干:我们可以很容易地把程序串联起来,一个程序的标准输出可以成为另一程序的标准输人。 这真是一个强大的工具。

8.1 从标准输入中读取

按照标准I/O模型, Java提供了System.in、System.out和System.err。

System.out已经亭先被包装成了 PrintStream对象。System.err同样也是PtintStream, 但System.in却是一个没有被包装过的未经加工的InputStream。这意味尽管我们可以立即使用System.out和System.err, 但是在读取 System.in之前必须对其进行包装。

System.in和大多数流一样,通常应该对它进行缓冲。

8.2 将System.out转换成PrintWriter

PrintWriter out = new PrintWriter(System.out, true);

第二个参数设为true,以便开启自动清空功能;否则,你可能看不到输出。

8.3 标准I/O重定向

setln(lnputStream) 
setOut(PrintStream) 
setErr(PrintStream) 

I/O重定向操纵的是字节流,而不是字符流,因此我们使用的是lnputStream和Output­Stream, 而不是Reader和Writer。

9.进程控制

要想运行一个程序,你需要向OSExecute.command()传递一个command字符串,它与你在控制台上运行该程序所键入的命令相同。

为了捕获程序执行时产生的标准输出流,你需要调用getlnputStream(),这是因为 lnputStream是我们可以从中读取信息的流。该程序的错误被发送到了标准错误流,并且通过调用getErrotStream()得以捕获。 如果存在任何错误,它们都会被打印并且会抛出OSExecuteException, 因此调用程序需要处理这个问题。

10.新I/O(New I/O)

旧的I/O包已经使用nio重新实现过,以便充分利用这种速度提高。速度的提高在文件I/O和网络I/O中都有可能发生。

速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。

唯一直接与通道交互的缓冲器是ByteBuffer——也就是说,可以存储未加工字节的缓冲器 。这种处理虽然很低级,但却正好,因为这是大多数操作系统中更有效的映射方式。

旧 I/O类库中有三个类被修改了,用以产生FileChannel 。这三个被修改的类是FilelnputStream、FileOutputStream以及用于既读又写的RandomAccessFile。注意这些是字节操纵流,与低层的nio性质一致。Reader和Writer这种宇符模式类不能用于产生通道;但是 java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer。

通道是一种相当基础的东西:可以向它传送用于读写的ByteBuffer, 并且可以锁定文件的某些区域用于独占式访问。

nio的目标就是快速移动大量数据, 因此ByteBuffer的大小就显得尤为重要。

甚至达到更高的速度也有可能,方法就是使用allocateDfrect() 而不是al!ocate() , 以产生一个与操作系统有更高耦合性的 “直接” 缓冲器。 但是,这种分配的开支会更大, 并且具体实现也随操作系统的不同而不同,因此必须再次实际运行应用程序来查看直接缓冲是否可以使我们获得速度上的优势。

10.1 转换数据

缓冲器容纳的是普通的字节,为了把它们转换成字符,我们要么在轮入它们的时候对其进行偏码,要么在将其从缓冲器输出时对它们进行解码。可以使用java.nio.charset.Charset类实现这些功能,该类提供了把数据编码成多种不同类型的字符集的工具。

另一个选择是在读文件时,使用能够产生可打印的输出的字符集进行encode()。

10.2 获取基本类型

尽管ByteBuffer只能保存字节类型的数据,但是它具有可以从其所容纳的字节中产生出各种不同基本类型值的方法。

10.3 视图缓冲器

视图缓冲器 (view buffer) 可以让我们通过某个特定的基本数据类型的视窗查看其底层的 ByteBuffer。
对视图的任何修改都会映射成为对ByteBuffer中数据的修改。 这使我们可以很方便地向ByteBuffer插入数据。

字节存放次序
不同的机器可能会使用不同的字节排序方法来存储数据。"big endian" (高位优先)将最重要的字节存放在地址最低的存储器单元。而"little endian" (低位优先)则是将最重要的字节放 在地址最高的存储器单元。当存储扯大于一个字节时,像int、float等,就要考虑字节的顺序问题了。ByteBuffer是以高位优先的形式存储数据的,井且数据在网上传送时也常常使用高位优先的形式。我们可以使用带有参数ByteOrder.BIG_ENDIAN或ByteOrder.LITTLE_ENDIAN的 order()方法改变ByteBuffer的字节排序方式。

10.4 用缓冲器操纵数据

下面的图阐明了nio类之间的关系,便千我们理解怎么移动和转换数据。


Java编程思想13——Java I/O系统_第8张图片

例如, 如果想把一个字节数组写到文件中去, 那么就应该使用ByteBuffer.wrap()方法把字节数组包装起来, 然后用getChanneI()方法在FileOutputStream上打开一个通道, 接着将来自千ByteBuffer的数据写到FileChannel中。

注意: ByteBuffer是将数据移进移出通道的唯一方式, 并且我们只能创建一个独立的基本类型缓冲器, 或者使用 "as" 方法从ByteBuffer中获得。 也就是说, 我们不能把基本类型的缓冲器转换成ByteBuffer。然而,由于我们可以经由视图缓冲器将基本类型数据移进移出ByteBuffer, 所以这也就不是什么真正的限制了。

10.5 缓冲器的细节

Buffer由数据和可以高效地访问及操纵这些数据的四个索引组成, 这四个索引是: mark (标记),position (位置), limit (界限)和capacity (容扯)。 下面是用千设置和复位索引以及查询它们的值的方法。


Java编程思想13——Java I/O系统_第9张图片

在缓冲器中插入和提取数据的方法会更新这些索引,用于反映所发生的变化。

10.6 内存映射文件

内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。 有了内存映射文 件,我们就可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组来访问。 这种方法极大地简化了用于修改文件的代码。

为了既能写又能读,我们先由RandomAccessFile开始, 获得该文件上的通道, 然后调用 map()产生MappedByteBuffer, 这是一种特殊类型的直接缓冲器。 注意我们必须指定映射文件的初始位置和映射区域的长度,这意味若我们可以映射某个大文件的较小的部分。

MappedByteBuffer由ByteBuffer继承而来,因此它具有ByteBuffer的所有方法。这里,我们仅仅展示了非常简单的put()和get(),但是我们同样可以使用像asCharBuffer()等这样的用法。

注意底层操作系统的文件映射工具是用来最大化地提高性能。

性能
尽管“旧”的I/O流在用nio实现后性能有所提高,但是“映射文件访问”往往可以更加显著地加快速度。即使建立映射文件的花费很大, 但是 整体受益比起I/0流来说还是很显著的。

10.7 文件加锁

文件加锁机制,它允许我们同步访问某个作为共享资源的文件。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具。

对独占锁或者共享锁的支持必须由底层的操作系统提供。

文件映射通常应用于极大的文件。我们可能需要对这种巨大的文件进行部分加锁,以便其他进程可以修改文件中未被加锁的部分。

11.压缩

Java I/O类库中的类支持读写压缩格式的数据流。
属千InputStream和OutputStream继承层次结构的一部分。这样做是因为压缩类库是按字节方式而不是字符方式处理的。不过有时我们可能会被迫要混合使用两种类型的数据流(注意我们可以使用InputStreamReader和OutputStreamWriter 在两种类型间方便地进行转换)。


Java编程思想13——Java I/O系统_第10张图片

11.1 用GZIP进行简单压缩

11.2 用Zip进行多文件保存

11.3 Java档案文件

Zip格式也被应用千JAR (Java ARchive, Java档案文件)文件格式中。这种文件格式就像Zip 一样,可以将一组文件压缩到单个压缩文件中。 同Java中其他任何东西一样,JAR文件也是跨平台的,所以不必担心跨平台的问题。 声音和图像文件可以像类文件一样被包含在其中。

12.对象序列化

Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。

利用它可以实现轻量级持久性 (lightweight persistence)。“持久性”意味若一个对象的生存周期井不取决千程序是否正在执行, 它可以生存于程序的调用之间。

对象序列化的概念加入到语言中是为了支持两种主要特性。

  • 1)是Java的远程方法调用 (Remote Method Invocation, RMI), 它使存活于其他计算机上的对象使用起来就像是存活千本机 上一样。当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值。
  • 2)对JavaBeans来说,对象的序列化也是必需的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,井在程序启动时进行后期恢复,这种具体工作就是由对象序列化完成的。

对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内所包含的所有引用,并保存那些对象,接着又能对对象内包含的每个这样的引用进行追踪,依此类推。这种情况有时被称为“对象网”,单个对象可与之建立连接,而且它还包含了对象的引 用数组以及成员对象。

12.1 寻找类

12.2 序列化的控制

可通过实现Externalizable接口-一4代替实现Serializable接口一一来对序列化过程进行控制。这个Extemalizable接口继承了Serializable接口,同时增添了两个方法:writeExtemal()和readExtemal()从这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊操作。

transient (瞬时)关键字
当我们对序列化进行控制时,可能某个特定子对象不想让Java的序列化机制自动保存与恢复。如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),通常就会面临这种情况。即使对象中的这些信息是private(私有)属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访间到它。

有一种办法可防止对象的敏感部分被序列化,就是将类实现为Externalizable。

然而,如果我们正在操作的是一个Serializable对象,那么所有序列化操作都会自动进行。为了能够予以控制,可以用transient(瞬时)关键字逐个字段地关闭序列化,它的意思是“不用麻烦你保存或恢复数据我自己会处理的"。

由千Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和 Serializable对象一起使用。

Externalizable的替代方法
可以实现Serializa­ble接口,并添加(注意我说的是“添加“,而非“覆盖”或者“实现,,)名为writeObject() 和readObject()的方法。这样一旦对象被序列化或者被反序列化还原,就会自动地分别调用这两个方法。也就是说,只要我们提供了这两个方法,就会使用它们而不是默认的序列化机制。

版本控制

12.3 使用 “持久性”

一个比较诱人的使用序列化技术的想法是:存储程序的一些状态, 以便我们随后可以很容 易地将程序恢复到当前状态。 但是在我们能够这样做之前,必须回答几个问题。如果我们将两个对象-它们都具有指向第三个对象的引用一进行序列化,会发生什么情况?当我们从它们的序列化状态恢复这两个对象时,第三个对象会只出现一次吗?如果将这两个对象序列化成独立的文件, 然后在代码的不同部分对它们进行反序列化还原, 又会怎样呢?

只要将任何对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,并且没有任何意外重复复制出的对象。

如果我们想保存系统状态,最安全的做法是将其作为“原子”操作进行序列化。

13.XML

对象序列化的一个重要限制是它只是Java的解决方案:只有Jay哇且序才能反序列化这种对象。 一种更具互操作性的解决方案是将数据转换为XML格式,这可以使其被各种各样的平台和语言使用。

14.Preferences

Preferences API与对象序列化相比,前者与对象持久性更密切,因为它可以自动存储和读取信息。不过,它只能用于小的受限的数据集合一我们只能存储基本类型和字符串,并且每个字符串的存储长度不能超过8K(不是很小,但我们也并不想用它来创建任何重要的东西)。顾名思义,PreferencesAPI用于存储和读取用户的偏好(preferences)以及程序配置项的设置。

Preferences是一个键-值集合(类似映射),存储在一个节点层次结构中。

你可能感兴趣的:(Java编程思想13——Java I/O系统)