天天看点

Java编程思想 Java I/O系统总结

File类

目录列表器

假设我们想查看一个目录列表 可以用两种方法来使用File对象 如果我们调用不带参数的list()方法 便可以获得此File对象包含的全部列表 然而 如果我们想获得一个受限列表 例如 想得到所有扩展名为.java的文件 那么我们就要用到 目录过滤器 这个类会告诉我们怎样显示符合条件的File对象

下面是一个示例 注意 通过使用java.utils.Arrays.sort()和String.CASE_INSENSITIVE.ORDERComparator 可以很容易地对结果进行排序(按字母顺序)

Java编程思想 Java I/O系统总结

匿名内部类

这个例子很适合用一个匿名内部类进行改写 首先创建一个filter()方法 它会返回一个指向FilenameFilter的引用

Java编程思想 Java I/O系统总结

这个设计有所改进 因为现在FilenameFilter类紧密地和DirList2绑定在一起 然而 我们可以进一步修改该方法 定义一个作为list()参数的匿名内部类 这样一来程序会变得更小

Java编程思想 Java I/O系统总结

目录实用工具

程序设计中一项常见的任务就是在文件集上执行操作 这些文件要么在本地目录中 要么遍布于整个目录树中 如果有一种工具能够为你产生这个文件集 那么它会非常有用 下面的使用工具类就可以通过使用local()方法产生由本地目录中的文件构成的File对象数组 或者通过使用walk()方法产生给定目录下的由整个目录树中所有文件构成的List(File对象比文件名更有用 因为File对象包含更多的信息) 这些文件是基于你提供的正则表达式而被选中的

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

TreeInfo.toString()方法使用了一个 灵巧打印机 类 以使输出更容易浏览 容器默认的toString()方法会在单个行中打印容器中的所有元素 对于大型集合来说 这会变得难以阅读 因此你可能希望使用可替换的格式化机制 下面是一个可以添加新行并缩排所有元素的工具

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

Directory实用工具放在了net.mindview.util包中 以使其可以更容易地被获得 下面的例子说明了你可以如何使用它的样本

Java编程思想 Java I/O系统总结

我们可以更进一步 创建一个工具 它可以在目录中穿行 并且根据Strategy对象来处理这些目录中的文件(这是策略设计模式的另一个示例)

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

目录的检查及创建

File类不仅仅只代表存在的文件或目录 也可以用File对象来创建新的目录或尚不存在的整个目录路径 我们还可以查看文件的特性(如:大小 最后修改日期 读/写) 检查某个File对象代表的是一个文件还是一个目录 并可以删除文件 下面的示例展示了File类的一些其他方法(请参考http://java.sun.com上的HTML文档以全面了解它们)

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

输入和输出

InputStream类型

InputStream的作用是用来表示那些从不同数据源产生输入的类 如表18-1所示 这些数据源包括

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

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

    Java编程思想 Java I/O系统总结

OutputStream类型

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

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

Java编程思想 Java I/O系统总结

添加属性和有用的接口

通过FilterInputStream从InputStream读取数据

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

Java编程思想 Java I/O系统总结

通过FilterOutPutStream向OutputStream写入

FilterOutPutStream的类型及功能如表18-4所示

Java编程思想 Java I/O系统总结

Reader和Writer

数据的来源和去处

几乎所有原始的Java I/O流类都有相应的Reader和Writer类来提供天然的Unicode操作 然而在某些场合 面向字节的InputStream和OutputStream才是正确的解决方案 特别是 java.util.zip类库就是面向字节的而不是面向字符的 因此 最明智的做法是尽量尝试使用Reader和Writer 一旦程序代码无法成功编译 我们就会发现自己不得不使用面向字节的类库

下面的表展示了在两个继承层次结构中 信息的来源和去处(即数据物理上来自哪里及去向哪里)之间的对应关系

Java编程思想 Java I/O系统总结

更改流的行为

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

在下表中 相对于前一表格来说 左右之间的对应关系的近似程度更加粗略一些 造成这种差别的原因是因为类的组织形式不同 尽管BufferedOutputStream是FilterOutputStream的子类 但是BufferedWriter并不是FilterWriter的子类(尽管FilterWriter是抽象类 没有任何子类 把它放在那里也只是把它作为一个占位符 或仅仅让我们不会对它所在的地方产生疑惑) 然而 这些类的接口却十分相似

Java编程思想 Java I/O系统总结

未发生变化的类

有一些类在Java 1.0和Java 1.1之间则未做改变

Java编程思想 Java I/O系统总结

自我独立的类:RandomAccessFile

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

I/O流的典型使用方式

缓冲输入文件

如果想要打开一个文件用于字符输入 可以使用以String或File对象作为文件名的FileInputReader 为了提高速度 我们希望对那个文件进行缓冲 那么我们将所产生的引用传给一个BufferedReader构造器 由于BufferedReader也提供readLine()方法 所以这是我们的最终对象和进行读取的接口 当readLine()将返回null时 你就达到了文件的末尾

Java编程思想 Java I/O系统总结

从内存输入

在下面的示例中 从BufferedInputFile.read()读入的String结果被用来创建一个StringReader 然后调用read()每次读取一个字符 并把它发送到控制台

Java编程思想 Java I/O系统总结

格式化的内存输入

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

Java编程思想 Java I/O系统总结

如果我们从DataInputStream用readByte()一次一个字节地读取字符 那么任何字节的值都是合法的结果 因此返回值不能用来检测输入是否结束 相反 我们可以使用available()方法查看还有多少可供存取的字符 下面这个例子演示了怎样一次一个字节地读取文件

Java编程思想 Java I/O系统总结

基本的文件输出

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

Java编程思想 Java I/O系统总结

文本文件输出的快捷方式

Java SE5在PrintWriter中添加了一个辅助构造器 使得你不必在每次希望创建文本文件并向其中写入时 都去执行所有的装饰工作 下面是用这种快捷方式重写的BasicFileOutput.java

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

存储和恢复数据

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

Java编程思想 Java I/O系统总结

读写随机访问文件

在使用RandomAccessFile时 你必须知道文件排版 这样才能正确地操作它 RandomAccessFile拥有读取基本类型和UTF-8字符串的各种具体方法 下面是示例

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

管道流

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

文件读写的实用工具

下面的TextFile类被用来简化对文件的读写操作 它包含的static方法可以像简单字符串那样读写文本文件 并且我们可以创建一个TextFile对象 它用一个ArrayList来保存文件的若干行(如此 当我们操纵文件内容时 就可以使用ArrayList的所有功能)

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

读取二进制文件

这个工具与TextFile类似 因为它简化了读取二进制文件的过程

Java编程思想 Java I/O系统总结

标准I/O

从标准输入中读取

通常我们会用readLine()一次一行地读取输入 为此 我们将System.in包装成BufferedReader来使用这要求我们必须用InputStreamReader把System.in转换成Reader 下面这个例子将直接回显你所输入的每一行

Java编程思想 Java I/O系统总结

将System.out转换成PrintWriter

System.out是一个PrintStream 而PrintStream是一个OutputStream PrintWriter有一个可以接受OutputStream作为参数的构造器 因此 只要需要 就可以使用那个构造器把System.out转换成PrintWriter

Java编程思想 Java I/O系统总结

标准I/O重定向

如果我们突然开始在显示器上创建大量输出 而这些输出滚动得太快以至于无法阅读时 重定向输出就显得极为有用 对于我们想重复测试某个特定用户的输入序列的命令行程序来说 重定向输入就很有价值 下例简单演示了这些方法的使用

Java编程思想 Java I/O系统总结

进程控制

你经常会需要在Java内部执行其他操作系统的程序 并且要控制这些程序的输入和输出 Java类库提供了执行这些操作的类

一项常见的任务是运行程序 并将产生的输出发送到控制台 本节包含了一个可以简化这项任务的实用工具 在使用这个实用工具时 可能会产生两种类型的错误 普通的导致异常的错误 对这些错误我们只需重新抛出一个运行时异常 以及从进程自身的执行过程中产生的错误 我们希望用单独的异常来报告这些错误

Java编程思想 Java I/O系统总结

要想运行一个程序 你需要向OSExecute.command()传递一个command字符串 它与你在控制台上运行该程序所键入的命令相同 这个命令被传递给java.lang.ProcessBuilder构造器(它要求这个命令作为一个String对象序列而被传递) 然后所产生的ProcessBuilder对象被启动

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

为了捕获程序执行时产生的标准输出流 你需要调用getInputStream() 这是因为InputStream是我们可以从中读取信息的流 从程序中产生的结果每次输出一行 因此要使用readLine()来读取 这里这些行只是直接被打印了出来 但是你还可能希望从command()中捕获和返回它们 该程序的错误被发送到了标准错误流 并且通过调用getErrotStream()得以捕获 如果存在任何错误 它们都会被打印并且会抛出OSExecuteException 因此调用程序需要处理这个问题

下面是展示如何使用OSExecute的示例

Java编程思想 Java I/O系统总结

这里使用了javap反编译器(随JDK发布)来反编译该程序

新I/O

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

下面的简单实例演示了上面三种类型的流 用以产生可写的 可读可写的及可读的通道

Java编程思想 Java I/O系统总结

一旦调用read()来告知FileChannel向ByteBuffer存储字节 就必须调用缓冲器上的flip() 让它做好让别人读取字节的准备(是的 这似乎有一点拙劣 但是请记住 它是很拙劣的 但却适用于获取最大速度) 如果我们打算使用缓冲器执行进一步的read()操作 我们也必须得调用clear()来为每个read()做好准备 这在下面这个简单文件复制程序中可以看到

Java编程思想 Java I/O系统总结

然而 上面那个程序并不是处理此类操作的理想方式 特殊方法transferTo()和transferFrom()则允许我们将一个通道和另一个通道直接相连

Java编程思想 Java I/O系统总结

转换数据

回过头看GetChannel.java这个程序就会发现 为了输出文件中的信息 我们必须每次只读取一个字节的数据 然后将每个byte类型强制转换成char类型 这种方法似乎有点原始 如果我们查看一下java.nio.CharBuffer这个类 将会发现它有一个toString()方法是这样定义的 返回一个包含缓冲器中所有字符的字符串 既然ByteBuffer可以看作是具有asCharBuffer()方法的CharBuffer 那么为什么不用它呢 正如下面的输出语句中第一行所见 这种方法并不能解决问题

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

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

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

获取基本类型

尽管ByteBuffer只能保存字节类型的数据 但是它具有可以从其所容纳的字节中产生出各种不同基本类型值的方法 下面这个例子展示了怎样使用这些方法来插入和抽取各种数值

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

视图缓冲器

视图缓冲器(view buffer)可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer ByteBuffer依然是实际存储数据的地方 支持 着前面的视图 因此 对视图的任何修改都会映射成为对ByteBuffer中数据的修改 正如我们在上一示例看到的那样 这使我们可以很方便地向ByteBuffer插入数据 视图还允许我们从ByteBuffer一次一个地(与ByteBuffer所支持的方式相同)或者成批地(放入数组中)读取基本类型值 在下面这个例子中 通过IntBuffer操纵ByteBuffer中的int型数据

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

一旦底层的ByteBuffer通过视图缓冲器填满了整数或其他基本类型时 就可以直接被写到通道中了 正像从通道中读取那样容易 然后使用视图缓冲器可以把任何数据都转化成某一特定的基本类型 在下面的例子中 通过在同一个ByteBuffer上建立不同的视图缓冲器 将同一字节序列翻译成了short int float long和double类型的数据

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

ByteBuffer通过一个被 包装 过的8字节数组产生 然后通过各种不同的基本类型的视图缓冲器显式了出来 我们可以在下图中看到 当从不同类型的缓冲器读取时 数据显式的方式也不同 这与上面程序的输出相对应

Java编程思想 Java I/O系统总结

字节存放次序

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

考虑包含下面两个字节的ByteBuffer

Java编程思想 Java I/O系统总结

如果我们以short(ByteBuffer.asShortBuffer())形式读取数据 得到的数字是97(二进制形式为00000000 01100001) 但是如果将ByteBuffer更改成低位优先形式 仍以short形式读取数据 得到的数字却是24832(二进制形式为01100001 00000000)

这个例子展示了怎样通过字节存放模式设置来改变字符中的字节次序

Java编程思想 Java I/O系统总结

用缓冲器操纵数据

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

Java编程思想 Java I/O系统总结

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

缓冲器的细节

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

Java编程思想 Java I/O系统总结

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

下面的示例用到一个很简单的算法(交换相邻字符) 以对CharBuffer中的字符进行编码(scramble)和译码(unscramble)

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

尽管可以通过对某个char数组调用wrap()方法来直接产生一个CharBuffer 但是在本例中取而代之的是分配一个底层的ByteBuffer 产生的CharBuffer只是ByteBuffer上的一个视图而已 这里要强调的是 我们总是以操纵ByteBuffer为目标 因为它可以和通道进行交互

下面是进入symmetricScramble()方法时缓冲器的样子

Java编程思想 Java I/O系统总结

position指针指向缓冲器中的第一个元素 capacity和limit则指向最后一个元素

在程序的symmetricScramble()方法中 迭代执行while循环直到position等于limit 一旦调用缓冲器上相对的get()或put()函数 position指针就会随之相应改变 我们也可以调用绝对的 包含一个索引参数的get()和put()方法(参数指明get()或put()的发生位置) 不过 这些方法不会改变缓冲器的position指针

当操纵到while循环时 使用mark()调用来设置mark的值 此时 缓冲器状态如下

Java编程思想 Java I/O系统总结

两个相对的get()调用把前两个字符保存到变量c1和c2中 调用完这两个方法后 缓冲器如下

Java编程思想 Java I/O系统总结

为了实现交换 我们要在position=0时写入c2 position=1时写入c1 我们也可以使用绝对的put()方法来实现 或者使用reset()把position的值设为mark的值

Java编程思想 Java I/O系统总结

这两个put()方法先写c2 接着写c1

Java编程思想 Java I/O系统总结

在下一次循环迭代期间 将mark设置成position的当前值

Java编程思想 Java I/O系统总结

这个过程将会持续到遍历完整个缓冲器 在while循环的最后 position指向缓冲器的末尾 如果要打印缓冲器 只能打印出position和limit之间的字符 因此 如果想显示缓冲器的全部内容 必须使用rewind()把position设置到缓冲器的开始位置 下面是调用rewind()之后缓冲器的状态(mark的值则变得不明确)

Java编程思想 Java I/O系统总结

当再次调用symmetricScramble()功能时 会对CharBuffer进行同样的处理 并将其恢复到初始状态

内存映射文件

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

Java编程思想 Java I/O系统总结

性能

尽管 旧 的I/O流在用nio实现后性能有所提高 但是 映射文件访问 往往可以更加显著地加快速度 下面的程序进行了简单的性能比较

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

文件加锁

JDK 1.4引入了文件加锁机制 它允许我们同步访问某个作为共享资源的文件 不过 竞争同一文件的两个线程可能在不同的Java虚拟机上 或者一个是Java线程 另一个是操作系统中其他的某个本地线程 文件锁对其他的操作系统进程是可见的 因为Java的文件加锁直接映射到了本地操作系统的加锁工具

下面是一个关于文件加锁的简单例子

Java编程思想 Java I/O系统总结

也可以使用如下方法对文件的一部分上锁

Java编程思想 Java I/O系统总结

或者

Java编程思想 Java I/O系统总结

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

对映射文件的部分加锁

如前所述 文件映射通常应用于极大的文件 我们可能需要对这种巨大的文件进行部分加锁 以便其他进程可以修改文件中未被加锁的部分 例如 数据库就是这样 因此多个用户可以同时访问到它

下面例子中有两个线程 分别加锁文件的不同部分

Java编程思想 Java I/O系统总结

压缩

Java I/O类库中的类支持读写压缩格式的数据流 你可以用它们对其他的I/O类进行封装 以提供压缩功能

这些类不是从Reader和Writer类派生而来的 而是属于InputStream和OutputStream继承层次结构的一部分 这样做是因为压缩类库是按字节方式而不是字符方式处理的 不过有时我们可能会被迫要混合使用两种类型的数据流(注意我们可以使用InputStreamReader和OutputStreamWriter在两种类型间方便地进行转换)

Java编程思想 Java I/O系统总结

尽管存在许多种压缩算法 但是Zip和GZIP可能是最常用的 因此我们可以很容易地使用多种可读写这些格式的工具来操纵我们的压缩数据

用GZIP进行简单压缩

GZIP接口非常简单 因此如果我们只想对单个数据流(而不是一系列互异数据)进行压缩 那么它可能是比较适合的选择 下面是对单个文件进行压缩的例子

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

用Zip进行多文件保存

支持Zip格式的Java库更加全面 利用该库可以方便地保存多个文件 它甚至有一个独立的类 使得读取Zip文件更加方便 这个类库使用的是标准Zip格式 所以能与当前那些可通过因特网下载的压缩工具很好地协作 下面这个例子具有与前例相同的形式 但它能根据需要来处理任意多个命令行参数 另外 它显示了用Checksum类来计算和校验文件的校验和的方法 一共有两种Checksum类型 Adler32(它快一些)和CRC32(慢一些 但更准确)

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

Java档案文件

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

Sun的JDK自带的jar程序可根据我们的选择自动压缩文件 可以用命令行的形式调用它 如下所示

Java编程思想 Java I/O系统总结

其中options只是一个字母集合(不必输入任何 - 或其他任何标识符) 以下这些选项字符在Unix和Linux系统中的tar文件中也具有相同的意义 具体意义如下所示

Java编程思想 Java I/O系统总结

如果压缩到JAR文件的众多文件中包含某个子目录 那么该子目录会被自动添加到JAR文件中 且包括该子目录的所有子目录 路径信息也会被保留

以下是一些调用jar的典型方法 下面的命令创建了一个名为myJarFile.jar的JAR文件 该文件包含了当前目录中的所有类文件 以及自动产生的清单文件

Java编程思想 Java I/O系统总结

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

Java编程思想 Java I/O系统总结

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

Java编程思想 Java I/O系统总结

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

Java编程思想 Java I/O系统总结

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

Java编程思想 Java I/O系统总结

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

Java编程思想 Java I/O系统总结

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

jar工具的功能没有zip工具那么强大 例如 不能够对已有的JAR文件进行添加或更新文件的操作 只能从头创建一个JAR文件 同时 也不能将文件移动至一个JAR文件 并在移动后将它们删除 然而 在一种平台上创建的JAR文件可以被在其他任何平台上的jar工具透明地阅读(这个问题有时会困扰zip工具)

对象序列化

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

对象序列化特别 聪明 的一个地方是它不仅保存了对象的 全景图 而且能追踪对象内所包含的所有引用 并保存那些对象 接着又能对对象内包含的每个这样的引用进行追踪 依此类推 这种情况有时被称为 对象网 单个对象可与之建立连接 而且它还包含了对象的引用数组以及成员对象 如果必须保持一套自己的对象序列化机制 那么维护那些可追踪到所有链接的代码可能会显得非常麻烦 然而 由于Java的对象序列化似乎找不出什么缺点 所以请尽量不要自己动手 让它用优化的算法自动维护整个对象网 下面这个例子通过对链接的对象生成一个worm(蠕虫)对序列化机制进行了测试 每个对象都与worm中的下一段链接 同时又与属于不同类(Data)的对象引用数组链接

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

寻找类

将一个对象从它的序列化状态中恢复出来 有哪些工作是必须的呢 举个例子来说 假如我们将一个对象序列化 并通过网络将其作为文件传送给另一台计算机 那么 另一台计算机上的程序可以只利用该文件内容来还原这个对象吗

回答这个问题的最好方法就是做一个实验 下面这个文件位于本节的子目录下

Java编程思想 Java I/O系统总结

而用于创建和序列化一个Alien对象的文件也位于相同的目录下

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

这个程序不但能捕获和处理异常 而且将异常抛出到main()方法之外 以便通过控制台产生报告 一旦该程序被编译和运行 它就会在c12目录下产生一个名为X.file的文件 以下代码位于一个名为xfiles的子目录下

Java编程思想 Java I/O系统总结

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

序列化的控制

默认的序列化机制并不难操纵 然而 如果有特殊的需要那又该怎么办呢 例如 也许要考虑特殊的安全问题 而且你不希望对象的某一部分被序列化 或者一个对象被还原以后 某子对象需要重新创建 从而不必将该子对象序列化

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

下面这个例子展示了Externalizable接口方法的简单实现 注意Blip1和Blip2除了细微的差别之外 几乎完全一致

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

下面这个例子示范了如何完整保存和恢复一个Externalizable对象

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

transient(瞬时)关键字

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

例如 假设某个Login对象保存某个特定的登录会话信息 登录的合法性通过校验之后 我们想把数据保存下来 但不包括密码 为做到这一点 最简单的办法是实现Serializable 并将password字段标志为transient 下面是具体的代码

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

Externalizable的替代方法

如果不是特别坚持实现Externalizable接口 那么还有另一种方法 我们可以实现Serializable接口 并添加(注意是 添加 而非 覆盖 或者 实现)名为writeObject()和readObject()的方法 这样一旦对象被序列化或者被反序列化还原 就会自动地分别调用这两个方法 也就是说 只要我们提供了这两个方法 就会使用它们而不是默认的序列化机制

这些方法必须具有准确的方法特征签名

Java编程思想 Java I/O系统总结

还有另外一个技巧 在你的writeObject()内部 可以调用defaultWriteObject()来选择执行默认的writeObject() 类似地 在readObject()内部 我们可以调用defaultReadObject() 下面这个简单的例子演示了如何对一个Serializable对象的存储与恢复进行控制

Java编程思想 Java I/O系统总结

版本控制

有时可能想要改变可序列化类的版本(比如源类的对象可能保存在数据库中) 虽然Java支持这种做法 但是你可能只在特殊的情况下才这样做 此外 还需要对它有相当深程度的了解 从http://java.sun.com处下载的JDK文档中对这一主题进行了非常彻底的论述

我们会发现在JDK文档中有许多注解是从下面的文字开始的

警告 该类的序列化对象和未来的Swing版本不兼容 当前对序列化的支持只适用于短期存储或应用之间的RMI

这是因为Java的版本控制机制过于简单 因而不能在任何场合都可靠运转 尤其是对JavaBeans更是如此 有关人员正在设法修正这一设计 也就是警告中的相关部分

使用 持久性

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

下面这个例子说明了上述问题

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

如果我们想保存系统状态 最安全的做法是将其作为 原子 操作进行序列化 如果我们序列化了某些东西 再去做其他一些工作 再来序列化更多的东西 如此等等 那么将无法安全地保存系统状态 取而代之的是 将构成系统状态的所有对象都置入单一容器内 并在一个操作中将该容器直接写出 然后同样只需一次方法调用 即可以将其恢复

下面这个例子是一个想象的计算机辅助设计(CAD)系统 该例演示了这一方法 此外 它还引入了static字段的问题 如果我们查看JDK文档 就会发现Class是Serializable的 因此只需直接对Class对象序列化 就可以很容易地保存static字段 在任何情况下 这都是一种明智的做法

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

恢复对象相当直观

Java编程思想 Java I/O系统总结

可以看到 xPos yPos以及dim的值都被成功地保存和恢复了 但是对static信息的读取却出现了问题 所有读回的颜色应该都是 3 但是真实情况却并非如此 Circle的值为1(定义为RED) 而Sequare的值为0(记住 它们是在构造器中初始化的) 看上去似乎static数据根本没有被序列化 确实如此 尽管Class类是Serializable的 但它却不能按我们所期望的方式运行 所以假如想序列化static值 必须自己动手去实现

这正是Line中的serializeStaticState()和deserializeStaticState()两个static方法的用途 可以看到 它们是作为存储和读取过程的一部分被显式地调用的(注意必须维护写入序列化文件和从该文件中读回的顺序) 因此 为了使CADState.java正确运转起来 我们必须

  1. 为几何形状添加serializeStaticState()和deserializeStaticState()
  2. 移除ArrayList shapeTypes以及与之有关的所有代码
  3. 在几何形状内添加对新的序列化和反序列化还原静态方法的调用

    另一个要注意的问题是安全 因为序列化也会将private数据保存下来 如果你关心安全问题 那么应将其标记成transient 但是这之后 还必须设计一种安全的保存信息的方法 以便在执行恢复时可以复位那些private变量

XML

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

作为一个示例 假设有一个Person对象 它包含姓和名 你想将它们序列化到XML中 下面的Person类有一个getXML()方法 它使用XOM来产生被转换为XML的Element对象的Person数据 还有一个构造器 接受Element并从中抽取恰当的Person数据

Java编程思想 Java I/O系统总结
Java编程思想 Java I/O系统总结

从XML文件中反序列化Person对象也很简单

Java编程思想 Java I/O系统总结

Preferences

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

Preferences是一个键 值集合(类似映射) 存储在一个节点层次结构中 尽管节点层次结构可用来创建更为复杂的结构 但通常是创建以你的类名命名的单一节点 然后将信息存储于其中 下面是一个简单的例子

Java编程思想 Java I/O系统总结

这里用的是userNodeForPackage() 但我们也可以选择用systemNodeForPackage() 虽然可以任意选择 但最好将 user 用于个别用户的偏好 将 system 用于通用的安装配置 因为main()是静态的 因此PreferencesDemo.class可以用来标识节点 但是在非静态方法内部 我们通常使用getClass() 尽管我们不一定非要把当前的类作为节点标识符 但这仍不失为一种很有用的方法

一旦我们创建了节点 就可以用它来加载或者读取数据了 在这个例子中 向节点载入了各种不同类型的数据项 然后获取其keys() 它们是以String[]的形式返回的 如果你习惯于keys()属于集合类库 那么这个返回结果可能并不是你所期望的 注意get()的第二个参数 如果某个关键字下没有任何条目 那么这个参数就是所产生的默认值 当在一个关键字集合内迭代时 我们总要确信条目是存在的 因此用null作为默认值是安全的 但是通常我们会获得一个具名的关键字 就像下面这条语句

Java编程思想 Java I/O系统总结

在通常情况下 我们希望提供一个合理的默认值 实际上 典型的习惯用法可见下面几行

Java编程思想 Java I/O系统总结

这样 在我们第一次运行程序时 UsageCount的值是0 但在随后引用中 它将会是非零值

在我们运行PreferencesDemo.java时 会发现每次运行程序时 UsageCount的值都会增加1 但是数据存储到哪里了呢 在程序第一次运行之后 并没有出现任何本地文件 Preferences API利用合适的系统资源完成了这个任务 并且这些资源会随操作系统不同而不同 例如在Windows里 就使用注册表(因为它已经有 键值对 这样的节点对层次结构了) 但是最重要的一点是 它已经神奇般地为我们存储了信息 所以我们不必担心不同的操作系统是怎么运作的

还有更多的Preferences API 参阅JDK文档可很容易地理解更深的细节