📄 chapter10.htm
字号:
与DataInputStream对应的是DataOutputStream,后者对各个基本数据类型以及String对象进行格式化,并将其置入一个数据“流”中,以便任何机器上的DataInputStream都能正常地读取它们。所有方法都以“wirte”开头,例如writeByte(),writeFloat()等等。<br>
若想进行一些真正的格式化输出,比如输出到控制台,请使用PrintStream。利用它可以打印出所有基本数据类型以及String对象,并可采用一种易于查看的格式。这与DataOutputStream正好相反,后者的目标是将那些数据置入一个数据流中,以便DataInputStream能够方便地重新构造它们。System.out静态对象是一个PrintStream。<br>
PrintStream内两个重要的方法是print()和println()。它们已进行了覆盖处理,可打印出所有数据类型。print()和println()之间的差异是后者在操作完毕后会自动添加一个新行。<br>
BufferedOutputStream属于一种“修改器”,用于指示数据流使用缓冲技术,使自己不必每次都向流内物理性地写入数据。通常都应将它应用于文件处理和控制器IO。<br>
<br>
表10.4 FilterOutputStream的类型<br>
<br>
类 功能 构建器参数/如何使用<br>
<br>
DataOutputStream 与DataInputStream配合使用,以便采用方便的形式将基本数据类型(int,char,long等)写入一个数据流
OutputStream/包含了完整接口,以便我们写入基本数据类型<br>
PrintStream 用于产生格式化输出。DataOutputStream控制的是数据的“存储”,而PrintStream控制的是“显示”
OutputStream,可选一个布尔参数,指示缓冲区是否与每个新行一同刷新/对于自己的OutputStream对象,应该用“final”将其封闭在内。可能经常都要用到它<br>
BufferedOutputStream
用它避免每次发出数据的时候都要进行物理性的写入,要求它“请先在缓冲区里找”。可调用flush(),对缓冲区进行刷新
OutputStream,可选缓冲区大小/本身并不能提供一个接口,只是发出使用缓冲区的要求。需要同一个接口对象连接到一起<br>
<br>
10.3 本身的缺陷:RandomAccessFile<br>
RandomAccessFile用于包含了已知长度记录的文件,以便我们能用seek()从一条记录移至另一条;然后读取或修改那些记录。各记录的长度并不一定相同;只要知道它们有多大以及置于文件何处即可。<br>
首先,我们有点难以相信RandomAccessFile不属于InputStream或者OutputStream分层结构的一部分。除了恰巧实现了DataInput以及DataOutput(这两者亦由DataInputStream和DataOutputStream实现)接口之外,它们与那些分层结构并无什么关系。它甚至没有用到现有InputStream或OutputStream类的功能——采用的是一个完全不相干的类。该类属于全新的设计,含有自己的全部(大多数为固有)方法。之所以要这样做,是因为RandomAccessFile拥有与其他IO类型完全不同的行为,因为我们可在一个文件里向前或向后移动。不管在哪种情况下,它都是独立运作的,作为Object的一个“直接继承人”使用。<br>
从根本上说,RandomAccessFile类似DataInputStream和DataOutputStream的联合使用。其中,getFilePointer()用于了解当前在文件的什么地方,seek()用于移至文件内的一个新地点,而length()用于判断文件的最大长度。此外,构建器要求使用另一个自变量(与C的fopen()完全一样),指出自己只是随机读("r"),还是读写兼施("rw")。这里没有提供对“只写文件”的支持。也就是说,假如是从DataInputStream继承的,那么RandomAccessFile也有可能能很好地工作。<br>
还有更难对付的。很容易想象我们有时要在其他类型的数据流中搜索,比如一个ByteArrayInputStream,但搜索方法只有RandomAccessFile才会提供。而后者只能针对文件才能操作,不能针对数据流操作。此时,BufferedInputStream确实允许我们标记一个位置(使用mark(),它的值容纳于单个内部变量中),并用reset()重设那个位置。但这些做法都存在限制,并不是特别有用。<br>
<br>
10.4 File类<br>
File类有一个欺骗性的名字——通常会认为它对付的是一个文件,但实情并非如此。它既代表一个特定文件的名字,也代表目录内一系列文件的名字。若代表一个文件集,便可用list()方法查询这个集,返回的是一个字串数组。之所以要返回一个数组,而非某个灵活的集合类,是因为元素的数量是固定的。而且若想得到一个不同的目录列表,只需创建一个不同的File对象即可。事实上,“FilePath”(文件路径)似乎是一个更好的名字。本节将向大家完整地例示如何使用这个类,其中包括相关的FilenameFilter(文件名过滤器)接口。<br>
<br>
10.4.1 目录列表器<br>
现在假设我们想观看一个目录列表。可用两种方式列出File对象。若在不含自变量(参数)的情况下调用list(),会获得File对象包含的一个完整列表。然而,若想对这个列表进行某些限制,就需要使用一个“目录过滤器”,该类的作用是指出应如何选择File对象来完成显示。<br>
下面是用于这个例子的代码(或在执行该程序时遇到困难,请参考第3章3.1.2小节“赋值”):<br>
<br>
449-450页程序<br>
<br>
DirFilter类“实现”了interface FilenameFilter(关于接口的问题,已在第7章进行了详述)。下面让我们看看FilenameFilter接口有多么简单:<br>
<br>
public interface FilenameFilter {<br>
boolean accept(文件目录, 字串名);<br>
}<br>
<br>
它指出这种类型的所有对象都提供了一个名为accept()的方法。之所以要创建这样的一个类,背后的全部原因就是把accept()方法提供给list()方法,使list()能够“回调”accept(),从而判断应将哪些文件名包括到列表中。因此,通常将这种技术称为“回调”,有时也称为“算子”(也就是说,DirFilter是一个算子,因为它唯一的作用就是容纳一个方法)。由于list()采用一个FilenameFilter对象作为自己的自变量使用,所以我们能传递实现了FilenameFilter的任何类的一个对象,用它决定(甚至在运行期)list()方法的行为方式。回调的目的是在代码的行为上提供更大的灵活性。<br>
通过DirFilter,我们看出尽管一个“接口”只包含了一系列方法,但并不局限于只能写那些方法(但是,至少必须提供一个接口内所有方法的定义。在这种情况下,DirFilter构建器也会创建)。<br>
accept()方法必须接纳一个File对象,用它指示用于寻找一个特定文件的目录;并接纳一个String,其中包含了要寻找之文件的名字。可决定使用或忽略这两个参数之一,但有时至少要使用文件名。记住list()方法准备为目录对象中的每个文件名调用accept(),核实哪个应包含在内——具体由accept()返回的“布尔”结果决定。<br>
为确定我们操作的只是文件名,其中没有包含路径信息,必须采用String对象,并在它的外部创建一个File对象。然后调用getName(),它的作用是去除所有路径信息(采用与平台无关的方式)。随后,accept()用String类的indexOf()方法检查文件名内部是否存在搜索字串"afn"。若在字串内找到afn,那么返回值就是afn的起点索引;但假如没有找到,返回值就是-1。注意这只是一个简单的字串搜索例子,未使用常见的表达式“通配符”方案,比如"fo?.b?r*";这种方案更难实现。<br>
list()方法返回的是一个数组。可查询这个数组的长度,然后在其中遍历,选定数组元素。与C和C++的类似行为相比,这种于方法内外方便游历数组的行为无疑是一个显著的进步。<br>
<br>
1. 匿名内部类<br>
下例用一个匿名内部类(已在第7章讲述)来重写显得非常理想。首先创建了一个filter()方法,它返回指向FilenameFilter的一个句柄:<br>
<br>
451-452页程序<br>
<br>
注意filter()的自变量必须是final。这一点是匿名内部类要求的,使其能使用来自本身作用域以外的一个对象。<br>
之所以认为这样做更好,是由于FilenameFilter类现在同DirList2紧密地结合在一起。然而,我们可采取进一步的操作,将匿名内部类定义成list()的一个参数,使其显得更加精简。如下所示:<br>
<br>
452页下程序<br>
<br>
main()现在的自变量是final,因为匿名内部类直接使用args[0]。<br>
这展示了如何利用匿名内部类快速创建精简的类,以便解决一些复杂的问题。由于Java中的所有东西都与类有关,所以它无疑是一种相当有用的编码技术。它的一个好处是将特定的问题隔离在一个地方统一解决。但在另一方面,这样生成的代码不是十分容易阅读,所以使用时必须慎重。<br>
<br>
2. 顺序目录列表<br>
经常都需要文件名以排好序的方式提供。由于Java 1.0和Java 1.1都没有提供对排序的支持(从Java
1.2开始提供),所以必须用第8章创建的SortVector将这一能力直接加入自己的程序。就象下面这样:<br>
<br>
453-454页程序<br>
<br>
这里进行了另外少许改进。不再是将path(路径)和list(列表)创建为main()的本地变量,它们变成了类的成员,使它们的值能在对象“生存”期间方便地访问。事实上,main()现在只是对类进行测试的一种方式。大家可以看到,一旦列表创建完毕,类的构建器就会自动开始对列表进行排序。<br>
这种排序不要求区分大小写,所以最终不会得到一组全部单词都以大写字母开头的列表,跟着是全部以小写字母开头的列表。然而,我们注意到在以相同字母开头的一组文件名中,大写字母是排在前面的——这对标准的排序来说仍是一种不合格的行为。Java
1.2已成功解决了这个问题。<br>
<br>
10.4.2 检查与创建目录<br>
File类并不仅仅是对现有目录路径、文件或者文件组的一个表示。亦可用一个File对象新建一个目录,甚至创建一个完整的目录路径——假如它尚不存在的话。亦可用它了解文件的属性(长度、上一次修改日期、读/写属性等),检查一个File对象到底代表一个文件还是一个目录,以及删除一个文件等等。下列程序完整展示了如何运用File类剩下的这些方法:<br>
<br>
454-456页程序<br>
<br>
在fileData()中,可看到应用了各种文件调查方法来显示与文件或目录路径有关的信息。<br>
main()应用的第一个方法是renameTo(),利用它可以重命名(或移动)一个文件至一个全新的路径(该路径由参数决定),它属于另一个File对象。这也适用于任何长度的目录。<br>
若试验上述程序,就可发现自己能制作任意复杂程度的一个目录路径,因为mkdirs()会帮我们完成所有工作。在Java
1.0中,-d标志报告目录虽然已被删除,但它依然存在;但在Java 1.1中,目录会被实际删除。<br>
<br>
10.5 IO流的典型应用<br>
尽管库内存在大量IO流类,可通过多种不同的方式组合到一起,但实际上只有几种方式才会经常用到。然而,必须小心在意才能得到正确的组合。下面这个相当长的例子展示了典型IO配置的创建与使用,可在写自己的代码时将其作为一个参考使用。注意每个配置都以一个注释形式的编号起头,并提供了适当的解释信息。<br>
<br>
457-459页程序<br>
<br>
10.5.1 输入流<br>
当然,我们经常想做的一件事情是将格式化的输出打印到控制台,但那已在第5章创建的com.bruceeckel.tools中得到了简化。<br>
第1到第4部分演示了输入流的创建与使用(尽管第4部分展示了将输出流作为一个测试工具的简单应用)。<br>
<br>
1. 缓冲的输入文件<br>
为打开一个文件以便输入,需要使用一个FileInputStream,同时将一个String或File对象作为文件名使用。为提高速度,最好先对文件进行缓冲处理,从而获得用于一个BufferedInputStream的构建器的结果句柄。为了以格式化的形式读取输入数据,我们将那个结果句柄赋给用于一个DataInputStream的构建器。DataInputStream是我们的最终(final)对象,并是我们进行读取操作的接口。<br>
在这个例子中,只用到了readLine()方法,但理所当然任何DataInputStream方法都可以采用。一旦抵达文件末尾,readLine()就会返回一个null(空),以便中止并退出while循环。<br>
“String s2”用于聚集完整的文件内容(包括必须添加的新行,因为readLine()去除了那些行)。随后,在本程序的后面部分中使用s2。最后,我们调用close(),用它关闭文件。从技术上说,会在运行finalize()时调用close()。而且我们希望一旦程序退出,就发生这种情况(无论是否进行垃圾收集)。然而,Java
1.0有一个非常突出的错误(Bug),造成这种情况不会发生。在Java 1.1中,必须明确调用System.runFinalizersOnExit(true),用它保证会为系统中的每个对象调用finalize()。然而,最安全的方法还是为文件明确调用close()。<br>
<br>
2. 从内存输入<br>
这一部分采用已经包含了完整文件内容的String s2,并用它创建一个StringBufferInputStream(字串缓冲输入流)——作为构建器的参数,要求使用一个String,而非一个StringBuffer)。随后,我们用read()依次读取每个字符,并将其发送至控制台。注意read()将下一个字节返回为int,所以必须将其造型为一个char,以便正确地打印。<br>
<br>
3. 格式化内存输入<br>
StringBufferInputStream的接口是有限的,所以通常需要将其封装到一个DataInputStream内,从而增强它的能力。然而,若选择用readByte()每次读出一个字符,那么所有值都是有效的,所以不可再用返回值来侦测何时结束输入。相反,可用available()方法判断有多少字符可用。下面这个例子展示了如何从文件中一次读出一个字符:<br>
<br>
461页程序<br>
<br>
注意取决于当前从什么媒体读入,avaiable()的工作方式也是有所区别的。它在字面上意味着“可以不受阻塞读取的字节数量”。对一个文件来说,它意味着整个文件。但对一个不同种类的数据流来说,它却可能有不同的含义。因此在使用时应考虑周全。<br>
为了在这样的情况下侦测输入的结束,也可以通过捕获一个违例来实现。然而,若真的用违例来控制数据流,却显得有些大材小用。<br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -