Skip to content

Commit 9a3fe12

Browse files
committed
✓ 附录:IO 流
1 parent 83a3bb8 commit 9a3fe12

File tree

1 file changed

+307
-8
lines changed

1 file changed

+307
-8
lines changed

docs/book/Appendix-IO-Streams.md

Lines changed: 307 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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("\nEnd 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

Comments
 (0)