《JAVA编程思想》学习笔记:第18章(Java的I/O系统)

目录
Java编程思想(一)第1~4章:概述
Java编程思想(二)第5章:初始化和清理
Java编程思想(三)第6章:访问权限
Java编程思想(四)第7章:复用类
Java编程思想(五)第8章:多态
Java编程思想(六)第9章:接口
Java编程思想(七)第10章:内部类
Java编程思想(八)第11章:持有对象
Java编程思想(九)第12章:异常
Java编程思想(十)第13章:字符串
Java编程思想(十一)第14章:类型信息
Java编程思想(十二)第15章:泛型
Java编程思想(十三)第16章:数组
Java编程思想(十四)第17章:深入研究容器

第十八章、Java的I/O系统

完整的容器分类法

I/O源端与之通信的接收端:文件、控制台、网络链接等。

通信方式:顺序、随机存取、缓冲、二进制、按字符、按行、按字等。

1. File类

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

目录的检查:

  • f.canRead() 判断File是否可读
  • f.canWrite() 判断File是否可写
  • f.getName() 获取文件的名字
  • f.getParent() 获取父目录文件路径
  • f.getPath() 获取File路径
  • f.length() 获取File长度
  • f.lastModified() 获取文件上次被修改时间
  • f.exists() 判断文件是否存在

文件的创建

  • File file=new File() 创建文件对象
  • file.createNewFile() 创建文件
  • file.mkdir() 创建目录
  • file.delete() 删除目录

2. 输入和输出

编程语言的I/O库经常使用流,它代表任何有能力产生数据的数据源对象或者有能力接受数据的接收端对象。

Java类库中的I/O类分成输入和输出两部分:

  • 通过继承,任何自Inputstream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或者字节数组。
  • 同样的OutputStream或者Writer派生出来的类都含有名为write()的基本方法,用于写单个字节或者字节数组。
  • 但是不会用到(上述方法read()和write()),它们之所以存在是因为别的类可以使用它们以便提供更有用的接口。因此,很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能。

2.1 InputStream类型

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

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

每一种数据源都有相应的InputStream子类。另外,FilterInputStream也属于一种InputStream,为装饰器类提供的基类,其中,装饰器类可以把属性或有用的接口与输入流连接在一起。

2.2 OutputStream类型

该类别的类决定了输出所要去往的目标:字节数组、文件或管道

另外,FilterOutputStream为装饰器类提供了一个基类,装饰器类把属性或者有用的接口与输出流连接了起来:

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

  • Java I/O类库里存在filter(过滤器)类的原因所在抽象类filter是所有装饰器类的基类。
  • FilterInputStream和FilterOutputStream:是用来提供装饰类器接口以及控制特定输入流和输出流的两个类。FilterInputStream和FilterOutputStream分别自I/O类库中的基类InputStream和OutputStream派生而来,这两个类是装饰器的必要条件。

装饰者(GoF23之一):动态的将功能附加到对象上,在对象扩展方面,它比继承更加有弹性。

3.1 通过FilterInputStream从InputStream读取数据

FilterInputStream类能够完成完全不同的事情,其中,DateInputStream允许读取不同的基本类型数据以及String对象。

其他FilterInputStream类则在内部修改InputStream的行为方式:是否缓冲,是否保留它所读过的行(允许查询行数或设置行数),以及是否把单一字符推回输入流。

3.2 通过FilterOutputStream向OutputStream导入

4. Reader和Writer

InputStream和OutputStream在以面向字节形式的IO中可以提供极有价值的功能,Reader和Writer(Java 1.1对基础IO流类库进行了重大修改,可能会以为是用来替换InputStream和OutputStream的)则提供兼容Unicode和面向字符的IO功能。

  • Java 1.1向InputStream和OutputStream继承层次中添加了一些新类,所以这两个类不会被取代。
  • 有时必须把来自于字节层次结构中的类和字符层次中的类结合起来。为了实现这个目的,要用到适配器类:InputStreamReader可以吧InputStream转换为Reader,而OutputStreamWriter可以吧OutputStream转换为Writer。

适配器(GoF23之一):将一个类的接口转换成客户端希望的另一个接口。

设计Reader和Writer继承层次结构只要是为了国际化。老的IO流继承层次结构仅支持8位字节流,并且不能很好地处理16位的Unicode字符。所以Reader和Writer继承层次结构就是为了在所有IO操作中都支持Unicode。

4.1 数据的来源和去处

4.2 更改流的行为

对于InputStream和OutputStream来说,有装饰器子类来修改流以满足需要。Reader和Writer的类继承层次结构继续沿用相同的思想——但不完全相同。无论何时使用readLine(),都不应该使用DataInputStream,而应该使用BufferedReader。

为了更容易地过渡到使用PrintWriter,它提供了一个既接受Writer对象又能接受任何OutputStream对象的构造器。PrintWriter的格式化接口实际上与PrintStream相同。

5. 自我独立的类:RandomAccessFile

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

RandomAccessFile实现了DataInput和DataOutput接口,它是一个完全独立的类,从头开始编写其所有的方法(大多数都是本地的)。这么做是因为RandomccessFile拥有和别的I/O类型本质不同的行为,因为可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接从Object派生而来。

方法getFilePointer()用于查找当前所处的文件位置,seek()用于在文件内移至新的位置,length()用于判断文件的最大尺寸。另外,其构造器还需要第二个参数(和C中的fopen()相同)用来指示我们只是“随机读”®还是“既读又写”(rw)。

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

6.1 缓冲输入文件

使用以String或File对象作为文件名的FileInputReader。为了提高速度,对文件进行缓冲,将所产生的引用传给一个BufferedReader构造器。

6.2 从内存输入

从BufferedInputFile.read()读入的String结果被用来创建一个StringReader。然后调用read()每次读取一个字符,并发送到控制台。

6.3 格式化的内存输入

要读取格式化数据,可以使用DataInputStream,它是面向字节的IO类,因此必须用InputStream而不是Reader。

DataInputStream in = new DataInputStream(new ByteArrayInputStream);

DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“TestEOF.java”)));

6.4 基本文件输出

FileWriter对象可以向文件写入数据。通常会用BufferedWriter将其包装起来用以缓冲输出。

6.5 存储和恢复数据

PrintWriter可以对数据进行格式化,以便阅读。但是为了输出可供另一个流恢复的数据,需要用DataOutputStream写入数据,并用DataInputStream恢复数据。当然,这些流可以使用任何形式,下面示例使用的是一个文件,并且对于读和写进行了缓冲处理。注意DataOutputStream和DataInputStream是面向字节的,因此要使用InputStream和OutputStream。

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

DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“Data.txt”)));

6.6 读写随机访问文件

RandomAccessFile rf = new RandomAccessFile(file, “rw”);

7. 文件读写的使用工具

读取文件:

BufferedReader in = new BufferedReader(new FileReader((new File(fileName)).getAbsoluteFile()));

写入文件:

PrintWriter out = new PrintWriter((new File(fileName)).getAbsoluteFile());

8. 标准IO

标准IO源自于Unix的“程序所使用的单一信息流”这一概念。程序的所有输入都可以来自于标准输入,所有输出都可以发送到标准输出。

8.1 从标准输入中读取

标准输入:System.in未加工的InputStream

标准输出:System.out PrintStream对象

标准错误:System.err PrintStream对象

通常会用readLine()一次一行读取输入,将System.in包装城BufferedReader来使用,这要求必须用InputStreamReader把Sytem.in转换成Reader。System,in通常应该对它进行缓冲。

8.2 将System.out转换成PrintWriter

public class ChangeSystemOut {

public static void main(String[] args) {

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

out.println("Hello, world");

}

}

8.3 标准IO重定向

Java的System类提供了静态方法嗲用,以允许对标准输入输出和错误IO流进行重定向:

setIn(InputStream)

setOut(PrintStream)

setErr(PrintStream)

9.进程控制

Java内部执行其他操作系统的程序,并且控制这些程序输入输出,Java类库提供了执行这些操作的类。

想运行一个程序,向OSException.command()传递一个command字符串,它与在控制台上运行该程序所键入的命令相同。这个命令被传递给java,lang.ProcessBuilder构造器,然后产生的ProcessBuilder对象被启动:

10. 新I/O

JDK 1.4的java.nio.*包中引入了新的IO类库,其目的在于提高速度。实际上,旧的IO包已经使用nio重新实现过,以便充分利用这种速度提高。

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

唯一直接与通道交互的缓冲器是ByteBuffer,可以存储未加工字节的缓冲器。java.nio.ByteBuffer是相当基础的类:通过稿子分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据。但是,没办法输出或读取对象,即使是字符串对象也不行。这种处理虽然很低级,但却正好,因为这是大多数草走系统中更有效的映射方式。

旧IO类库有三个类被修改了,用以产生FileChannel。这三个被修改类是FileInputStream、FileOutputStream以及用于既读又写的RandomAccessFile。这些都是字节操作流,与底层nio性质一致。Reader和Writer这些字符模式类不能用于产生通道;但是java.nio.channels.Channels类提供了使用方法,用于在通道中产生Reader和Writer。

getChannel()将会产生一个FileChannel。通道是一种相当基础的:可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。

使用warp()方法将已存在的字节数组包装到ByteBuffer中。

data.txt文件用RandomAccessFile被再次打开。注意可以在文件内随处移动FileChanel;这里,把它移动到最后,以便附加其他写操作。

对于只读访问,必须显式地使用静态的allocate()方法来分配ByteBuffer。nio的目的就是快速移动大量数据,因此ByteBuffer的大小显得尤为重要——实际上,使用1K可能比通常使用的小一点(必须通过实际运行应用程序来找到最佳尺寸)。甚至叨叨更高速度,使用allocateDirect(),以产生一个与操作系统有更高耦合性的直接缓冲器(但分配的开支会更大)。

一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的flip(),让它做好让别人读取字节的准备。如果打算使用缓冲器执行进一步read()操作,也必须得使用clear()来为每个read()做好准备。

10.1 转换数据

在GetChannel.java中,必须每次只读取一个字节的数据,然后将每个byte类型强制转换成char类型。而java.nio.CharBuffer有一个toString方法:返回一个包含缓冲器中所有字符的字符串。

10.2 获取基本类型

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

bb.asCharBuffer();

bb.asShortBuffer();

bb.asIntBuffer();

bb.asLongBuffer();

bb.asFloatBuffer();

bb.asDoubleBuffer();

10.3 视图缓冲器

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

先用重载后的put()方法存储一个整数数组。接着get()和put()方法调用直接访问底层ByteBuffer中的某个整数位置。注意,这些通过直接与ByteBuffer对话访问绝对位置的方式也同样适用于基本类型。

一旦底层的ByteBuffer通过视图缓冲器填满了整数或其他基本类型时,就可以直接被写到通道中。正像从通道中读取那样容易,然后使用视图缓冲器可以把任何数据都转化为某一特定的基本类型。

10.4 用缓冲器操纵数据

此图阐明了nio类之间的关系,便于理解怎么移动和转换数据。如果想把一个字节数组写到文件中去,那么就应该使用ByteBuffer.wrap()方法把字节数组包装起来,然后用getChannel()方法在FileOutputStream上打开一个通道,接着将来自于ByteBuffer的数据写到FileChannel。

注意:BytBuffer是将数据移进移出通道的唯一方式,并且只能创建一个独立的基本类型缓冲器,或者使用as方法从ByteBuffer中获得。也就是说,不能把基本类型的缓冲器转换成ByteBuffer。

10.5 缓冲器的细节

Buffer有数据和可以高效地访问及操作这些数据的四个索引组成,mark(标记)、position(位置)、limit(界限)和capacity(容量)。

10.6 内存映射文件(MemoryMappedFile)

内存映射文件允许创建和修改因为太大而不能放入内存的文件。可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组访问

使用map()产生MappedByteBuffer,一个特殊类型的直接缓冲器,必须指定映射文件初始位置和映射区域长度。

MappedByteBuffer由ByteBuffer继承而来,因此它具有ByteBuffer的所有方法。

创建了128MB,这可能比操作系统所允许一次载入内存的空间大。但似乎可以一次访问整个文件。因为只有一部分文件放入了内存,其他被交换了出去。

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

10.6.1 性能

nio实现后性能有所提高,但是映射文件访问往往可以更加显著地加快速度。

10.7 文件加锁

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

通过对FileChannel调用tryLock()或lock(),就可以获得整个文件的FileLock。SocketChannel、DatagramChannel和ServerSocketChannel不需要加锁,因为它们是从单进程实体继承而来,通常不在两个进程之间共享socket。tryLock()是非阻塞式的,它设法获取锁,如果不能得到(当其他一些进程已经持有相同的锁,并且不共享时),它将直接从方法调用返回。lock()是阻塞式的,它要阻塞进程直至锁可以获得,或调用lock()的线程中断,或调用lock()的通道关闭。使用FileLock.release()可以释放锁。

也可以对文件部分上锁:

tryLock(long position, long size, boolean shared)或者lock(long position, long size, boolean shared)

其中加锁区域由size-position决定,第三个参数指定是否共享锁。

尽管无参的加锁方法将根据文件尺寸变化而变化,但是具有固定尺寸的锁不随文件变化而变化。

对于独占锁或者共享锁的支持必须有底层的操作系统提供。如操作系统不支持共享锁并未每一个请求都创建锁,那么它就会使用独占锁。锁的类型可以通过FileLock.isShared()进行查询。

11. 压缩

Java IO类库中的类支持读写压缩格式的数据流。 Java IO类库中的类支持读写压缩格式的数据流。

11.1 用GZIP进行简单压缩

如果相对单一数据流进行压缩,GZIP接口是比较合适的选择:

压缩:BufferedOutputStream <- GZIPOutputStream <- FileOutputStream

解压:BufferedReader <- InputStreamReader <- GZIPInputStream <- FileInputStream

11.2 用Zip进行多文件保存

支持Zip格式的Java库更加全面,它显示了用Checksum类来计算和校验文件的校验和的方法。一共有两种Checksum类型:Adler32(快)和CRC32(慢,准确)。

压缩:BufferedOutputStream <- ZIPOutputStream <- CheckedOutputStream <- FileOutputStream

解压: BufferedReader <- InputStreamReader <- ZIPInputStream <- CheckedInputStream <- FileInputStream

对于每一个要加入压缩档案的文件,都必须调用putNextEntry(),并将其传递给一个ZipEntry对象。ZipEntry包含一个功能很广泛的接口,允许获取和设置Zip文件内该特定项上所有可利用的数据:名字、压缩的和未压缩的文件大小、日期、CRC检验和、额外字段数据、注释、压缩方法以及他是否是一个目录入口等等。

11.3 Java档案文件

ip格式也被应用于JAR(Java ARchive,Java档案文件)文件格式中。将一组文件压缩到单个压缩文件中。同Java中任何其他东西一样,JAR也是跨平台的。由于采用压缩技术,可以使传输时间更短,只需向服务器发送一次请求即可。

Sun的JDK自带jar程序,可根据选择自动压缩文件:

jar [options] destination [manifest] inputfile(s)

其中options只是一个字母几个:

创建一个名为myJarFile.jar的JAR文件,包含当前目录下所有类文件,以及自动产生的清单文件:

jar cf myJarFile.jar *.class

下面的命令与前例类似,但添加了一个名为myManifestFile.mf的用户自建清单文件:

jar cmf myJarFile.jar myHanifestFile.nf *.class

下面的命令会产生myJarFile.jar内所有文件的一个目录表:

jar tf myJarFile.jar

下面的命令添加“v”(详尽)标志,可以提供有关myJarFle.jar中的文件的更详细的信息:

jar. tvf myJarfile.jar

假定audio、classes和image是子目录, 下面的命令将所有子目录合并到文件myApp.jar中,其中也包括了“v” 标志。当jar程序运行时,该标志可以提供更详细的信息:

jar cvf myApp.jar audio classes image

如果用0 (零)选项创建一个JAR文件,那么该文件就可放入类路径变量(CLASSPATH) 中:

CLASSPATH=”lib1.jar;lib2.jar:”

然后Java就可以在1lib1.jar和lib2.jar中搜索目标类文件了。

12 对象序列化(Android ->Pacelable)

Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行;这意味着序列化机制能自动弥补不同操作系统之间的差异。也就是说,可以在运行Windows系统的计算机上创建一个对象,将其序列化,通过网络将它发送给一台运行Unix系统的计算机,然后在那里谁确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心字节的顺序或者其他任何细节。

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

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

一是Java的远程方法调用(Remote Method Invocation, RMI),它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。

对Java Beans来说,对象的序列化也是必需的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并在程序启动时进行后期恢复,这种具体工作就是由对象序列化完成的。

只要对象实现了Serializable接口(该接口仅是一个标记接口,不包括任何方法),对象的序列化处理就会非常简单。当序列化的概念被加入到语言中时,许多标准库类都发生了改变,以便具备序列化特性一其中包括所有基 本数据类型的封装器、所有容器类以及许多其他的东西。甚至Class对象也可以被序列化。

序列化一个对象的步骤:

首先要创建某些OutputStream对象

然后将其封装在一个ObjectOutputStream对象内。

这时,只需调用writeObject0即可将对象序列化,并将其发送给OutputStream(对象化序列是基于字节的,因要使用InputStream和OutputStream继承层次结构)。

要反向进行该过程(即将-一个序列还原为一个对象),需要将个InputStream封装在ObjectmputStream内,然后调用readObject()。和往常一样,我们最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。

下面我们来看一个例子:

class Data implements Serializable

从输出上看,被还原后的对象确实包含了原对象中的所有链接。 在对一个Serializable对象进行还原的过程中,没有调用任何构造器,包含默认的构造器。整个对象都是通过InputStream中取得数据恢复过来的。

12.1 寻找类

将一个对象从它的序列化状态中恢复出来,哪些工作是必须的?

打开和读取mystery对象中的内容都需要Alien的Class对象;Java虚拟机找不到Alien.class(除非它正好在类路径Classpath内,而本例不在类路径之内)。这样就会得到一个名叫ClassNotFoundException的异常(同样,除非能够验证Alien存在,否则它等于消失)。必须保证Java虚拟机能够找到相关的.class文件。

默认序列化机制并不难操纵。如果希望部分序列化或子对象不必序列化。可通过Exterbalizable接口代替Serializable。Exterbalizable接口继承了Serializable,同时增添了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用。

如果从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()来为基类组件提供恰当的存储和恢复功能。为正常运行,不仅需要在writeExternal()方法(没有任何默认行为来为Externalizable 对象写入任何成员对象)中将来自对象的重要信息写入,还必须在readExternal()方法中恢复数据。因为Externalizable对象的默认构造器行为使其看起来似乎像某种自动发生的存储与恢复操作。但实际上并非如此。

Serializable对象与Externalizable对象的区别:

对于Serializable对象来说,对象完全以它存储的二进制位为基础进行构造,而不调用构造器。

对于Externalizable对象,所有的普通的默认构造器都会被调用(包括字段定义时的初始化),然后调用readeExternal()。必须注意:所有的默认构造器都被调用之后,才能使Externalizable对象产生正确的行为。

transient(瞬时)关键字

特定子对象不想让java的序列化自动保存与恢复,可以使用transient逐个字段地关闭序列化。

当对象被恢复时,transient的password域就会变成null。虽然toString()是用重载后的+运算符来连接String对象,但是null引用会被自动转换成字符串null。

Externalizable的替代方法

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

方法特征签名:

private void writeObject(ObjectOutputStream stream)throws IOException;

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;

实际上并不是从这个类的其他方法中调用它们,而是ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法调用了对象的writeObject()和readObject()方法(类型信息章节展示了如何在类的外部访问private方法)。

还有另一个技巧,可在自己的writeObject()内部,调用defaultWriteObject()来选择执行默认的writeObject().

13 XML

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

开源XOM类库,Person类有一个getXML()方法,使用XOM来产生被转换过的XML的Element对象的Person数据。还有一个构造器,接受Element并从中抽取恰当的Person数据:

14 Preferences (Android ->SharedPreference)

Preferences API与对象序列化相比,前者与对象持久性更密切。因为它可以自动存储和读取信息。不过,它只能用于小的受限的数据集合——只能存储基本数据和字符串,并且每个字符串的存储长度不能超过8K。顾名思义,Preferences API用于存储和读取用户的偏好preferences以及程序配置项的设置。

Preferences是一个键-值集合,存储在一个节点层次结构中。

发布了220 篇原创文章 · 获赞 73 · 访问量 96万+

猜你喜欢

转载自blog.csdn.net/cbk861110/article/details/104111708