第十八章、Java的I/O系统
对程序语言的设计者来说,创建一个好的I/O系统是一个艰难的任务:
需要涵盖不同的I/O的来源端与想要与之通信的接收端:文件、控制台、网络链接等。
需要涵盖不同的通信方式:顺序、随机存取、缓冲、二进制、按字符、按行、按字等。
1. File类
File(文件)既能代表一个特定文件名称,又能代表一个目录下的一组文件的名称。如果是文件集,可以对此集合调用list()方法,返回一个字符数组。
1.1 目录列表
查看一个文件目录的方法(使用File类):
f.isFile() 判断File是否为文件
f.isDirectory() 判断File是否为文件夹
f.list() 返回某个File下的所有文件和目录的文件名,返回的是String数组
f.listFiles() 返回某个File下所有文件和目录的绝对路径,返回的是File数组
1.2 目录实用工具
在文件集上执行操作:
Directory.local()
Directory.walk()
1.3 目录的检查:
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() 删除目录
file.renameTo(newFile) 重命名(或移动)文件到新的目录&文件名位置
2. 输入和输出
“流”:编程语言的I/O库经常使用“流”,它代表任何有能力产生数据的数据源对象或者有能力接受数据的接收端对象。
“流”:屏蔽了实际的I/O设备处理数据的细节(封装具体底层&设备实现?)。
Java类库中的I/O类分成输入和输出两部分:
输入:通过继承,任何自Inputstream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或者字节数组。
输出:同样的OutputStream或者Writer派生出来的类都含有名为write()的基本方法,用于写单个字节或者字节数组。
总结:但是不会用到(上述方法read()和write()),它们之所以存在是因为别的类可以使用它们以便提供更有用的接口。因此,很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能。
2.1 InputStream类型
InputStream的作用是用来表示从不同数据源产生输出的类,这些数据源(均为InputStream的子类)包括:
字节数组 ByteArrayInputStream
String对象 StringBufferInputStream
文件 FileInputStream
管道 PipedInputStream:工作方式与实际管道相似,即从一段输入,从另一端输出
序列 SequenceInputStream 一个由其他种类的流组成的序列,以便可以将它们收集合并到一个流内
其他数据源,如Internet链接等
特殊派生类:FilterInputStream也属于一种InputStream,为装饰器类提供的基类,其中,装饰器类可以把属性或有用的接口与输入流连接在一起。
2.2 OutputStream类型
该类别的类决定了输出所要去往的目标,包含:
字节数组 ByteArrayOutputStream
文件 FileOutputStream
管道 PipedOutputStream
特殊派生类: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 I/O流的典型使用方式(Typical uses of I/O streams)
尽管可以通过不同的方式组合I/O流类,但我们可能也就只用到其中的几种组合。下面的例子可以作为
6.1 缓冲输入文件
使用以String或File对象作为文件名的FileInputReader。为了提高速度,对文件进行缓冲,将所产生的引用传给一个BufferedReader构造器。
BufferedReader in = new BufferedReader(new FileReader(filename));
6.2 从内存输入
从BufferedInputFile.read()读入的String结果被用来创建一个StringReader。然后调用read()每次读取一个字符,并发送到控制台。
StringReader in = new StringReader(BufferedInputFile.read("MemoryInput.java"));
6.3 格式化的内存输入
要读取格式化数据,可以使用DataInputStream,它是面向字节的IO类,因此必须用InputStream而不是Reader。
DataInputStream in = new DataInputStream(
new ByteArrayInputStream(BufferedInputFile.read("FormattedMemoryInput.java").getBytes()));
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“TestEOF.java”)));
6.4 基本文件输出
FileWriter对象可以向文件写入数据。通常会用BufferedWriter将其包装起来用以缓冲输出。
BufferedReader in = new BufferedReader(new StringReader(
BufferedInputFile.read("BasicFileOutput.java")));
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
6.5 存储和恢复数据
PrintWriter可以对数据进行格式化,以便阅读。但是为了输出可供另一个流恢复的数据,需要用DataOutputStream写入数据:
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(“Data.txt”)));
DataInputStream恢复数据:
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
PrintWriter out = new PrintWriter(System.out, true);
8.3 标准IO重定向
Java的System类提供了静态方法嗲用,以允许对标准输入输出和错误IO流进行重定向:
setIn(InputStream)
setOut(PrintStream)
setErr(PrintStream)
9. 进程控制
Java内部执行其他操作系统的程序,并且控制这些程序输入输出,Java类库提供了执行这些操作的类: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)
内存映射文件允许创建和修改因为太大而不能放入内存的文件。可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组访问
MappedByteBuffer out = new RandomAccessFile("test.dat", "rw").getChannel()
.map(FileChannel.MapMode.READ_WRITE, 0, length);
10.6.1 性能
nio实现后性能有所提高,但是映射文件访问往往可以更加显著地加快速度。
10.7 文件加锁
JDK 1.4引入了文件加锁机制,允许同步访问某个做为共享资源的文件。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到本地操作系统的加锁工具。
FileOutputStream fos= new FileOutputStream("file.txt");
FileLock fl = fos.getChannel().tryLock();
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
11.3 Java档案文件(JAR)
JAR(Java ARchive,Java档案文件)文件格式:
将一组文件压缩到单个压缩文件中。同Java中任何其他东西一样,JAR也是跨平台的。由于采用压缩技术,可以使传输时间更短,只需向服务器发送一次请求即可。
12 对象序列化(Android ->Parcelable)
对象序列化的相关概念:
Java的对象序列化&反序列化:将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。
跨平台特性:这一过程甚至可通过网络进行;这意味着序列化机制能自动弥补不同操作系统之间的差异。也就是说,可以在运行Windows系统的计算机上创建一个对象,将其序列化,通过网络将它发送给一台运行Unix系统的计算机,然后在那里谁确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心字节的顺序或者其他任何细节。
轻量级持久性(lightweight persistence):利用序列化可以实现轻量级持久性(lightweight persistence)。“持久性”意味着一个对象的生存周期并不取决于程序是否正在执行;它可以生存于程序的调用之间。
对象序列化的概念加入到语言中是为了支持两种主要特性。
RMI场景: 一是Java的远程方法调用(Remote Method Invocation, RMI),它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。
对Java Beans来说,对象的序列化也是必需的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并在程序启动时进行后期恢复,这种具体工作就是由对象序列化完成的。
对象序列化的实现:
应用侧的实现:只要对象实现了Serializable接口(该接口仅是一个标记接口,不包括任何方法),对象的序列化处理就会非常简单。class Data implements Serializable
平台侧的实现:当序列化的概念被加入到语言中时,许多标准库类都发生了改变,以便具备序列化特性一其中包括所有基 本数据类型的封装器、所有容器类以及许多其他的东西。甚至Class对象也可以被序列化。
底层实现步骤:
序列化一个对象:首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内。这时,只需调用writeObject0即可将对象序列化,并将其发送给OutputStream(对象化序列是基于字节的,因要使用InputStream和OutputStream继承层次结构)。
反序列化一个对象:要反向进行该过程(即将-一个序列还原为一个对象),需要将个InputStream封装在ObjectmputStream内,然后调用readObject()。和往常一样,我们最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。
12.1 寻找类
将一个对象从它的序列化状态中恢复出来,哪些工作是必须的?
举例:网络侧传输过来的序列化数据,本地如何反序列化?
前置条件:必须保证JVM找得到对应对象的***.class文件,否则反序列化时,会throw ClassNotFoundException。
12.2 序列化的控制
12.1. 1 Serializable接口
应用场景:默认序列化机制。
12.1. 2 Exterbalizable(implements Serializable)接口
应用场景:默认序列化机制(Serializable)并不难操纵。如果希望部分序列化或子对象不必序列化。可通过Exterbalizable接口代替Serializable。
新增方法:Exterbalizable接口继承了Serializable,同时增添了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用。
12.1. 3 Serializable对象与Externalizable对象的区别:
Serializable对象:对于Serializable对象来说,对象完全以它存储的二进制位为基础进行构造,而不调用构造器。
Externalizable对象:对于Externalizable对象,所有的普通的默认构造器都会被调用(包括字段定义时的初始化),然后调用readeExternal()。必须注意:所有的默认构造器都被调用之后,才能使Externalizable对象产生正确的行为。
12.1. 4 transient(瞬时)关键字
应用场景:特定子对象不想让java的序列化自动保存与恢复,可以使用transient逐个字段地关闭序列化。
场景举例:如对象中包含了敏感信息的字段(比如用户密码)时
实现结果:当对象被恢复时,transient的password域就会变成null。虽然toString()是用重载后的+运算符来连接String对象,但是null引用会被自动转换成字符串null。
12.1. 5 Externalizable的替代方法
Externalizable的替代方案1:
方案原理:可以实现Serializable接口,并添加(非覆盖或实现)名为writeObject()和readObject()方法。这样一旦对象被序列化或者被反序列化还原,就会自动地分别调用这个方法。也就是说,只要提供这两个方法,就会使用它们而不是默认的序列化机制。
具体实现:实际上并不是从这个类的其他方法中调用它们,而是ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法调用了对象的writeObject()和readObject()方法(类型信息章节展示了如何在类的外部访问private方法)。
private void writeObject(ObjectOutputStream stream)throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
Externalizable的替代方案2:
还有另一个技巧,可在自己的writeObject()内部,调用defaultWriteObject()来选择执行默认的writeObject().
12.1. 6 深度拷贝&序列化
深度拷贝:deep copy,表示复制的是整个对象网,而不仅仅是基本类型对象和对象引用;
实现方法:通过对字节数组使用对象序列化,可实现深度拷贝。
13 XML(android *.xml 即使用此机制)
对象序列化的一个重要限制是它只是Java解决方案:只有Java程序才能反序列化这种对象。
Xml格式:一种更具有互操作性的解决方案是将数据转换为XML格式,这样可以使其被各种各样的平台和语言使用。
14 Preferences (Android ->SharedPreference)
Preferences API与对象序列化相比,前者与对象持久性更密切。它可以自动存储和读取信息。
不过,它只能用于小的受限的数据集合——只能存储基本数据和字符串,并且每个字符串的存储长度不能超过8K。顾名思义,Preferences API用于存储和读取用户的偏好preferences以及程序配置项的设置。
Preferences是一个键-值集合,存储在一个节点层次结构中。
15 总结:
Java I/O流类库:确能满足我们的基本需求:可以通过控制台、文件、内存块、甚至因特网进行读写。
装饰器模式:在此章节的大量运用,提供了应用的巨大灵活性。