@@ -195,47 +195,346 @@ Java 5 添加了几种 `PrintWriter` 构造器,以便在将输出写入时简
195195
196196最初,我们可能难以相信 ` RandomAccessFile ` 不是 ` InputStream ` 或者 ` OutputStream ` 继承体系中的一部分。除了实现了 ` DataInput ` 和 ` DataOutput ` 接口(` DataInputStream ` 和 ` DataOutputStream ` 也实现了这两个接口)之外,它和这两个继承体系没有任何关系。它甚至都不使用 ` InputStream ` 和 ` OutputStream ` 类中已有的任何功能。它是一个完全独立的类,其所有的方法(大多数都是 ` native ` 方法)都是从头开始编写的。这么做是因为 ` RandomAccessFile ` 拥有和别的 I/O 类型本质上不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接继承自 ` Object ` 。
197197
198- 从本质上来讲,` RandomAccessFile ` 的工作方式类似于把 ` DataIunputStream ` 和 ` DataOutputStream ` 组合起来使用。另外它还有一些额外的方法,比如使用 ` getFilePointer() ` 可以得到当前文件指针在文件中的位置,使用 ` seek() ` 可以移动文件指针,使用 ` length() ` 可以得到文件的长度, 另外,其构造器还需要传入第二个参数(和 C 语言中的 ` fopen() ` 相同)用来表示我们是准备对文件进行 “随机读”(r)还是“读写”(rw)。它并不支持只写文件,从这点来看,如果当初 ` RandomAccessFile ` 能设计成继承自 ` DataInputStream ` ,可能也是个不错的实现方式。
198+ 从本质上来讲,` RandomAccessFile ` 的工作方式类似于把 ` DataIunputStream ` 和 ` DataOutputStream ` 组合起来使用。另外它还有一些额外的方法,比如使用 ` getFilePointer() ` 可以得到当前文件指针在文件中的位置,使用 ` seek() ` 可以移动文件指针,使用 ` length() ` 可以得到文件的长度。 另外,其构造器还需要传入第二个参数(和 C 语言中的 ` fopen() ` 相同)用来表示我们是准备对文件进行 “随机读”(r)还是“读写”(rw)。它并不支持只写文件,从这点来看,如果当初 ` RandomAccessFile ` 能设计成继承自 ` DataInputStream ` ,可能也是个不错的实现方式。
199199
200- 在 Java 1.4 中,` RandomAccessFile ` 的大多数功能(但不是全部)都被 nio 中的** 内存映射文件(mmap)** 取代,详见[ 附录:新 I/O] ( ./Appendix-New-IO.md ) 。
200+ 在 Java 1.4 中,` RandomAccessFile ` 的大多数功能(但不是全部)都被 nio 中的** 内存映射文件** (mmap)取代,详见[ 附录:新 I/O] ( ./Appendix-New-IO.md ) 。
201201
202202<!-- Typical Uses of I/O Streams -->
203+
203204## IO流典型用途
204205
206+ 尽管我们可以用不同的方式来组合 I/O 流类,但常用的也就其中几种。你可以下面的例子可以作为 I/O 典型用法的基本参照(在你确定无法使用[ 文件] ( ./17-Files.md ) 这一章所述的库之后)。
205207
208+ 在这些示例中,异常处理都被简化为将异常传递给控制台,但是这样做只适用于小型的示例和工具。在你自己的代码中,你需要考虑更加复杂的错误处理方式。
206209
207210### 缓冲输入文件
208211
209-
212+ 如果想要打开一个文件进行字符输入,我们可以使用一个 ` FileInputReader ` 对象,然后传入一个 ` String ` 或者 ` File ` 对象作为文件名。为了提高速度,我们希望对那个文件进行缓冲,那么我们可以将所产生的引用传递给一个 ` BufferedReader ` 构造器。` BufferedReader ` 提供了 ` line() ` 方法,它会产生一个 ` Stream<String> ` 对象:
213+
214+ ``` java
215+ // iostreams/BufferedInputFile.java
216+ // {VisuallyInspectOutput}
217+ import java.io.* ;
218+ import java.util.stream.* ;
219+
220+ public class BufferedInputFile {
221+ public static String read (String filename ) {
222+ try (BufferedReader in = new BufferedReader (
223+ new FileReader (filename))) {
224+ return in. lines()
225+ .collect(Collectors . joining(" \n " ));
226+ } catch (IOException e) {
227+ throw new RuntimeException (e);
228+ }
229+ }
230+
231+ public static void main (String [] args ) {
232+ System . out. print(
233+ read(" BufferedInputFile.java" ));
234+ }
235+ }
236+ ```
237+
238+ ` Collectors.joining() ` 在其内部使用了一个 ` StringBuilder ` 来累加其运行结果。该文件会通过 ` try-with-resources ` 子句自动关闭。
210239
211240### 从内存输入
212241
242+ 下面示例中,从 ` BufferedInputFile.read() ` 读入的 ` String ` 被用来创建一个 ` StringReader ` 对象。然后调用其 ` read() ` 方法,每次读取一个字符,并把它显示在控制台上:
213243
244+ ``` java
245+ // iostreams/MemoryInput.java
246+ // {VisuallyInspectOutput}
247+ import java.io.* ;
214248
215- ### 格式化内存输入
249+ public class MemoryInput {
250+ public static void
251+ main (String [] args ) throws IOException {
252+ StringReader in = new StringReader (
253+ BufferedInputFile . read(" MemoryInput.java" ));
254+ int c;
255+ while ((c = in. read()) != - 1 )
256+ System . out. print((char ) c);
257+ }
258+ }
259+ ```
216260
261+ 注意 ` read() ` 是以 ` int ` 形式返回下一个字节,所以必须类型转换为 ` char ` 才能正确打印。
217262
263+ ### 格式化内存输入
218264
219- ### 基本文件的输出
265+ 要读取格式化数据,我们可以使用 ` DataInputStream ` ,它是一个面向字节的 I/O 类(不是面向字符的)。这样我们就必须使用 ` InputStream ` 类而不是 ` Reader ` 类。我们可以使用 ` InputStream ` 以字节形式读取任何数据(比如一个文件),但这里使用的是字符串。
266+
267+ ``` java
268+ // iostreams/FormattedMemoryInput.java
269+ // {VisuallyInspectOutput}
270+ import java.io.* ;
271+
272+ public class FormattedMemoryInput {
273+ public static void main (String [] args ) {
274+ try (
275+ DataInputStream in = new DataInputStream (
276+ new ByteArrayInputStream (
277+ BufferedInputFile . read(
278+ " FormattedMemoryInput.java" )
279+ .getBytes()))
280+ ) {
281+ while (true )
282+ System . out. write((char ) in. readByte());
283+ } catch (EOFException e) {
284+ System . out. println(" \n End of stream" );
285+ } catch (IOException e) {
286+ throw new RuntimeException (e);
287+ }
288+ }
289+ }
290+ ```
291+
292+ ` ByteArrayInputStream ` 必须接收一个字节数组,所以这里我们调用了 ` String.getBytes() ` 方法。所产生的的 ` ByteArrayInputStream ` 是一个适合传递给 ` DataInputStream ` 的 ` InputStream ` 。
293+
294+ 如果我们用 ` readByte() ` 从 ` DataInputStream ` 一次一个字节地读取字符,那么任何字节的值都是合法结果,因此返回值不能用来检测输入是否结束。取而代之的是,我们可以使用 ` available() ` 方法得到剩余可用字符的数量。下面例子演示了怎么一次一个字节地读取文件:
295+
296+ ``` java
297+ // iostreams/TestEOF.java
298+ // Testing for end of file
299+ // {VisuallyInspectOutput}
300+ import java.io.* ;
301+
302+ public class TestEOF {
303+ public static void main (String [] args ) {
304+ try (
305+ DataInputStream in = new DataInputStream (
306+ new BufferedInputStream (
307+ new FileInputStream (" TestEOF.java" )))
308+ ) {
309+ while (in. available() != 0 )
310+ System . out. write(in. readByte());
311+ } catch (IOException e) {
312+ throw new RuntimeException (e);
313+ }
314+ }
315+ }
316+ ```
317+
318+ 注意,` available() ` 的工作方式会随着所读取媒介类型的不同而有所差异,它的字面意思就是“在没有阻塞的情况下所能读取的字节数”。对于文件,能够读取的是整个文件;但是对于其它类型的“流”,可能就不是这样,所以要谨慎使用。
319+
320+ 我们也可以通过捕获异常来检测输入的末尾。但是,用异常作为控制流是对异常的一种错误使用方式。
220321
322+ ### 基本文件的输出
221323
324+ ` FileWriter ` 对象用于向文件写入数据。实际使用时,我们通常会用 ` BufferedWriter ` 将其包装起来以增加缓冲的功能(可以试试移除此包装来感受一下它对性能的影响——缓冲往往能显著地增加 I/O 操作的性能)。在本例中,为了提供格式化功能,它又被装饰成了 ` PrintWriter ` 。按照这种方式创建的数据文件可作为普通文本文件来读取。
325+
326+ ``` java
327+ // iostreams/BasicFileOutput.java
328+ // {VisuallyInspectOutput}
329+ import java.io.* ;
330+
331+ public class BasicFileOutput {
332+ static String file = " BasicFileOutput.dat" ;
333+
334+ public static void main (String [] args ) {
335+ try (
336+ BufferedReader in = new BufferedReader (
337+ new StringReader (
338+ BufferedInputFile . read(
339+ " BasicFileOutput.java" )));
340+ PrintWriter out = new PrintWriter (
341+ new BufferedWriter (new FileWriter (file)))
342+ ) {
343+ in. lines(). forEach(out:: println);
344+ } catch (IOException e) {
345+ throw new RuntimeException (e);
346+ }
347+ // Show the stored file:
348+ System . out. println(BufferedInputFile . read(file));
349+ }
350+ }
351+ ```
352+
353+ ` try-with-resources ` 语句会自动 flush 并关闭文件。
222354
223355### 文本文件输出快捷方式
224356
225-
357+ Java 5 在 ` PrintWriter ` 中添加了一个辅助构造器,有了它,你在创建并写入文件时,就不必每次都手动执行一些装饰的工作。下面的代码使用这种快捷方式重写了 ` BasicFileOutput.java ` :
358+
359+ ``` java
360+ // iostreams/FileOutputShortcut.java
361+ // {VisuallyInspectOutput}
362+ import java.io.* ;
363+
364+ public class FileOutputShortcut {
365+ static String file = " FileOutputShortcut.dat" ;
366+
367+ public static void main (String [] args ) {
368+ try (
369+ BufferedReader in = new BufferedReader (
370+ new StringReader (BufferedInputFile . read(
371+ " FileOutputShortcut.java" )));
372+ // Here's the shortcut:
373+ PrintWriter out = new PrintWriter (file)
374+ ) {
375+ in. lines(). forEach(out:: println);
376+ } catch (IOException e) {
377+ throw new RuntimeException (e);
378+ }
379+ System . out. println(BufferedInputFile . read(file));
380+ }
381+ }
382+ ```
383+
384+ 使用这种方式仍具备了缓冲的功能,只是现在不必自己手动添加缓冲了。但遗憾的是,其它常见的写入任务都没有快捷方式,因此典型的 I/O 流依旧涉及大量冗余的代码。本书[ 文件] ( ./17-Files.md ) 一章中介绍的另一种方式,对此类任务进行了极大的简化。
226385
227386### 存储和恢复数据
228387
229-
388+ ` PrintWriter ` 是用来对可读的数据进行格式化。但如果要输出可供另一个“流”恢复的数据,我们可以用 ` DataOutputStream ` 写入数据,然后用 ` DataInputStream ` 恢复数据。当然,这些流可能是任何形式,在下面的示例中使用的是一个文件,并且对读写都进行了缓冲。注意 ` DataOutputStream ` 和 ` DataInputStream ` 是面向字节的,因此要使用 ` InputStream ` 和 ` OutputStream ` 体系的类。
389+
390+ ``` java
391+ // iostreams/StoringAndRecoveringData.java
392+ import java.io.* ;
393+
394+ public class StoringAndRecoveringData {
395+ public static void main (String [] args ) {
396+ try (
397+ DataOutputStream out = new DataOutputStream (
398+ new BufferedOutputStream (
399+ new FileOutputStream (" Data.txt" )))
400+ ) {
401+ out. writeDouble(3.14159 );
402+ out. writeUTF(" That was pi" );
403+ out. writeDouble(1.41413 );
404+ out. writeUTF(" Square root of 2" );
405+ } catch (IOException e) {
406+ throw new RuntimeException (e);
407+ }
408+ try (
409+ DataInputStream in = new DataInputStream (
410+ new BufferedInputStream (
411+ new FileInputStream (" Data.txt" )))
412+ ) {
413+ System . out. println(in. readDouble());
414+ // Only readUTF() will recover the
415+ // Java-UTF String properly:
416+ System . out. println(in. readUTF());
417+ System . out. println(in. readDouble());
418+ System . out. println(in. readUTF());
419+ } catch (IOException e) {
420+ throw new RuntimeException (e);
421+ }
422+ }
423+ }
424+ ```
425+
426+ 输出结果:
427+
428+ ```
429+ 3.14159
430+ That was pi
431+ 1.41413
432+ Square root of 2
433+ ```
434+
435+ 如果我们使用 ` DataOutputStream ` 进行数据写入,那么 Java 就保证了即便读和写数据的平台多么不同,我们仍可以使用 ` DataInputStream ` 准确地读取数据。这一点很有价值,众所周知,人们曾把大量精力耗费在数据的平台相关性问题上。但现在,只要两个平台上都有 Java,就不会存在这样的问题[ ^ 3 ] 。
436+
437+ 当我们使用 ` DastaOutputStream ` 时,写字符串并且让 ` DataInputStream ` 能够恢复它的唯一可靠方式就是使用 UTF-8 编码,在这个示例中是用 ` writeUTF() ` 和 ` readUTF() ` 来实现的。UTF-8 是一种多字节格式,其编码长度根据实际使用的字符集会有所变化。如果我们使用的只是 ASCII 或者几乎都是 ASCII 字符(只占 7 比特),那么就显得及其浪费空间和带宽,所以 UTF-8 将 ASCII 字符编码成一个字节的形式,而非 ASCII 字符则编码成两到三个字节的形式。另外,字符串的长度保存在 UTF-8 字符串的前两个字节中。但是,` writeUTF() ` 和 ` readUTF() ` 使用的是一种适用于 Java 的 UTF-8 变体(JDK 文档中有这些方法的详尽描述),因此如果我们用一个非 Java 程序读取用 ` writeUTF() ` 所写的字符串时,必须编写一些特殊的代码才能正确读取。
438+
439+ 有了 ` writeUTF() ` 和 ` readUTF() ` ,我们就可以在 ` DataOutputStream ` 中把字符串和其它数据类型混合使用。因为字符串完全可以作为 Unicode 格式存储,并且可以很容易地使用 ` DataInputStream ` 来恢复它。
440+
441+ ` writeDouble() ` 将 ` double ` 类型的数字存储在流中,并用相应的 ` readDouble() ` 恢复它(对于其它的书类型,也有类似的方法用于读写)。但是为了保证所有的读方法都能够正常工作,我们必须知道流中数据项所在的确切位置,因为极有可能将保存的 ` double ` 数据作为一个简单的字节序列、` char ` 或其它类型读入。因此,我们必须:要么为文件中的数据采用固定的格式;要么将额外的信息保存到文件中,通过解析额外信息来确定数据的存放位置。注意,对象序列化和 XML (二者都在[ 附录:对象序列化] ( Appendix-Object-Serialization.md ) 中介绍)是存储和读取复杂数据结构的更简单的方式。
230442
231443### 读写随机访问文件
232444
233-
445+ 使用 ` RandomAccessFile ` 就像是使用了一个 ` DataInputStream ` 和 ` DataOutputStream ` 的结合体(因为它实现了相同的接口:` DataInput ` 和 ` DataOutput ` )。另外,我们还可以使用 ` seek() ` 方法移动文件指针并修改对应位置的值。
446+
447+ 在使用 ` RandomAccessFile ` 时,你必须清楚文件的结构,否则没法正确使用它。` RandomAccessFile ` 有一套专门的方法来读写基本数据类型的数据和 UTF-8 编码的字符串:
448+
449+ ``` java
450+ // iostreams/UsingRandomAccessFile.java
451+ import java.io.* ;
452+
453+ public class UsingRandomAccessFile {
454+ static String file = " rtest.dat" ;
455+
456+ public static void display () {
457+ try (
458+ RandomAccessFile rf =
459+ new RandomAccessFile (file, " r" )
460+ ) {
461+ for (int i = 0 ; i < 7 ; i++ )
462+ System . out. println(
463+ " Value " + i + " : " + rf. readDouble());
464+ System . out. println(rf. readUTF());
465+ } catch (IOException e) {
466+ throw new RuntimeException (e);
467+ }
468+ }
469+
470+ public static void main (String [] args ) {
471+ try (
472+ RandomAccessFile rf =
473+ new RandomAccessFile (file, " rw" )
474+ ) {
475+ for (int i = 0 ; i < 7 ; i++ )
476+ rf. writeDouble(i * 1.414 );
477+ rf. writeUTF(" The end of the file" );
478+ rf. close();
479+ display();
480+ } catch (IOException e) {
481+ throw new RuntimeException (e);
482+ }
483+ try (
484+ RandomAccessFile rf =
485+ new RandomAccessFile (file, " rw" )
486+ ) {
487+ rf. seek(5 * 8 );
488+ rf. writeDouble(47.0001 );
489+ rf. close();
490+ display();
491+ } catch (IOException e) {
492+ throw new RuntimeException (e);
493+ }
494+ }
495+ }
496+ ```
497+
498+ 输出结果:
499+
500+ ```
501+ Value 0: 0.0
502+ Value 1: 1.414
503+ Value 2: 2.828
504+ Value 3: 4.242
505+ Value 4: 5.656
506+ Value 5: 7.069999999999999
507+ Value 6: 8.484
508+ The end of the file
509+ Value 0: 0.0
510+ Value 1: 1.414
511+ Value 2: 2.828
512+ Value 3: 4.242
513+ Value 4: 5.656
514+ Value 5: 47.0001
515+ Value 6: 8.484
516+ The end of the file
517+ ```
518+
519+ ` display() ` 方法打开了一个文件,并以 ` double ` 值的形式显示了其中的七个元素。在 ` main() ` 中,首先创建了文件,然后打开并修改了它。因为 ` double ` 总是 8 字节长,所以如果要用 ` seek() ` 定位到第 5 个(从 0 开始计数) ` double ` 值,则要传入的地址值应该为 ` 5*8 ` 。
520+
521+ 正如前面所诉,虽然 ` RandomAccess ` 实现了 ` DataInput ` 和 ` DataOutput ` 接口,但实际上它和 I/O 继承体系中的其它部分是分离的。它不支持装饰,故而不能将其与 ` InputStream ` 及 ` OutputStream ` 子类中的任何一个组合起来,所以我们也没法给它添加缓冲的功能。
522+
523+ 该类的构造器还有第二个必选参数:我们可以指定让 ` RandomAccessFile ` 以“只读”(r)方式或“读写”
524+ (rw)方式打开文件。
525+
526+ 除此之外,还可以使用 ` nio ` 中的“内存映射文件”代替 ` RandomAccessFile ` ,这在[ 附录:新 I/O] ( Appendix-New-IO.md ) 中有介绍。
234527
235528<!-- Summary -->
236529## 本章小结
237530
531+ Java 的 I/O 流类库的确能够满足我们的基本需求:我们可以通过控制台、文件、内存块,甚至因特网进行读写。通过继承,我们可以创建新类型的输入和输出对象。并且我们甚至可以通过重新定义“流”所接受对象类型的 ` toString() ` 方法,进行简单的扩展。当我们向一个期望收到字符串的方法传送一个非字符串对象时,会自动调用对象的 ` toString() ` 方法(这是 Java 中有限的“自动类型转换”功能之一)。
532+
533+ 在 I/O 流类库的文档和设计中,仍留有一些没有解决的问题。例如,我们打开一个文件用于输出,如果在我们试图覆盖这个文件时能抛出一个异常,这样会比较好(有的编程系统只有当该文件不存在时,才允许你将其作为输出文件打开)。在 Java 中,我们应该使用一个 ` File ` 对象来判断文件是否存在,因为如果我们用 ` FileOutputStream ` 或者 ` FileWriter ` 打开,那么这个文件肯定会被覆盖。
534+
535+ I/O 流类库让我们喜忧参半。它确实挺有用的,而且还具有可移植性。但是如果我们没有理解“装饰器”模式,那么这种设计就会显得不是很直观。所以,它的学习成本相对较高。而且它并不完善,比如说在过去,我不得不编写相当数量的代码去实现一个读取文本文件的工具——所幸的是,Java 7 中的 nio 消除了此类需求。
238536
537+ 一旦你理解了装饰器模式,并且开始在某些需要这种灵活性的场景中使用该类库,那么你就开始能从这种设计中受益了。到那时候,为此额外多写几行代码的开销应该不至于让人觉得太麻烦。但还是请务必检查一下,确保使用[ 文件] ( ./17-Files.md ) 一章中的库和技术没法解决问题后,再考虑使用本章的 I/O 流库。
239538
240539[ ^ 1 ] : 很难说这就是一个很好的设计选择,尤其是与其它编程语言中简单的 I/O 类库相比较。但它确实是如此选择的一个正当理由。
241540
0 commit comments