@@ -195,47 +195,346 @@ Java 5 添加了几种 `PrintWriter` 构造器,以便在将输出写入时简
195
195
196
196
最初,我们可能难以相信 ` RandomAccessFile ` 不是 ` InputStream ` 或者 ` OutputStream ` 继承体系中的一部分。除了实现了 ` DataInput ` 和 ` DataOutput ` 接口(` DataInputStream ` 和 ` DataOutputStream ` 也实现了这两个接口)之外,它和这两个继承体系没有任何关系。它甚至都不使用 ` InputStream ` 和 ` OutputStream ` 类中已有的任何功能。它是一个完全独立的类,其所有的方法(大多数都是 ` native ` 方法)都是从头开始编写的。这么做是因为 ` RandomAccessFile ` 拥有和别的 I/O 类型本质上不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接继承自 ` Object ` 。
197
197
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 ` ,可能也是个不错的实现方式。
199
199
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 ) 。
201
201
202
202
<!-- Typical Uses of I/O Streams -->
203
+
203
204
## IO流典型用途
204
205
206
+ 尽管我们可以用不同的方式来组合 I/O 流类,但常用的也就其中几种。你可以下面的例子可以作为 I/O 典型用法的基本参照(在你确定无法使用[ 文件] ( ./17-Files.md ) 这一章所述的库之后)。
205
207
208
+ 在这些示例中,异常处理都被简化为将异常传递给控制台,但是这样做只适用于小型的示例和工具。在你自己的代码中,你需要考虑更加复杂的错误处理方式。
206
209
207
210
### 缓冲输入文件
208
211
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 ` 子句自动关闭。
210
239
211
240
### 从内存输入
212
241
242
+ 下面示例中,从 ` BufferedInputFile.read() ` 读入的 ` String ` 被用来创建一个 ` StringReader ` 对象。然后调用其 ` read() ` 方法,每次读取一个字符,并把它显示在控制台上:
213
243
244
+ ``` java
245
+ // iostreams/MemoryInput.java
246
+ // {VisuallyInspectOutput}
247
+ import java.io.* ;
214
248
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
+ ```
216
260
261
+ 注意 ` read() ` 是以 ` int ` 形式返回下一个字节,所以必须类型转换为 ` char ` 才能正确打印。
217
262
263
+ ### 格式化内存输入
218
264
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
+ 我们也可以通过捕获异常来检测输入的末尾。但是,用异常作为控制流是对异常的一种错误使用方式。
220
321
322
+ ### 基本文件的输出
221
323
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 并关闭文件。
222
354
223
355
### 文本文件输出快捷方式
224
356
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 ) 一章中介绍的另一种方式,对此类任务进行了极大的简化。
226
385
227
386
### 存储和恢复数据
228
387
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 ) 中介绍)是存储和读取复杂数据结构的更简单的方式。
230
442
231
443
### 读写随机访问文件
232
444
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 ) 中有介绍。
234
527
235
528
<!-- Summary -->
236
529
## 本章小结
237
530
531
+ Java 的 I/O 流类库的确能够满足我们的基本需求:我们可以通过控制台、文件、内存块,甚至因特网进行读写。通过继承,我们可以创建新类型的输入和输出对象。并且我们甚至可以通过重新定义“流”所接受对象类型的 ` toString() ` 方法,进行简单的扩展。当我们向一个期望收到字符串的方法传送一个非字符串对象时,会自动调用对象的 ` toString() ` 方法(这是 Java 中有限的“自动类型转换”功能之一)。
532
+
533
+ 在 I/O 流类库的文档和设计中,仍留有一些没有解决的问题。例如,我们打开一个文件用于输出,如果在我们试图覆盖这个文件时能抛出一个异常,这样会比较好(有的编程系统只有当该文件不存在时,才允许你将其作为输出文件打开)。在 Java 中,我们应该使用一个 ` File ` 对象来判断文件是否存在,因为如果我们用 ` FileOutputStream ` 或者 ` FileWriter ` 打开,那么这个文件肯定会被覆盖。
534
+
535
+ I/O 流类库让我们喜忧参半。它确实挺有用的,而且还具有可移植性。但是如果我们没有理解“装饰器”模式,那么这种设计就会显得不是很直观。所以,它的学习成本相对较高。而且它并不完善,比如说在过去,我不得不编写相当数量的代码去实现一个读取文本文件的工具——所幸的是,Java 7 中的 nio 消除了此类需求。
238
536
537
+ 一旦你理解了装饰器模式,并且开始在某些需要这种灵活性的场景中使用该类库,那么你就开始能从这种设计中受益了。到那时候,为此额外多写几行代码的开销应该不至于让人觉得太麻烦。但还是请务必检查一下,确保使用[ 文件] ( ./17-Files.md ) 一章中的库和技术没法解决问题后,再考虑使用本章的 I/O 流库。
239
538
240
539
[ ^ 1 ] : 很难说这就是一个很好的设计选择,尤其是与其它编程语言中简单的 I/O 类库相比较。但它确实是如此选择的一个正当理由。
241
540
0 commit comments