Appendix-IO-Streams
附录: 流式IO
Java 7 引入了一种简单明了的方式来读写文件和操作目录。大多情况下,文件这一章所介绍的那些库和技术就足够你用了。但是,如果你必须面对一些特殊的需求和比较底层的操作,或者处理一些老版本的代码,那么你就必须了解本附录中的内容。
对于编程语言的设计者来说,实现良好的输入
nio
类(全称是 “new I/O”,
因此,要想充分理解
编程语言的
注意:
Java 8 函数式编程中的Stream
类和这里的I/O stream 没有任何关系。这又是另一个例子,如果再给设计者一次重来的机会,他们将使用不同的术语。
- 字节流对应原生的二进制数据;
- 字符流对应字符数据,它会自动处理与本地字符集之间的转换;
- 缓冲流可以提高性能,通过减少底层
API 的调用次数来优化I/O 。
从InputStream
,所有与输出有关系的类都继承自 OutputStream
。所有从 InputStream
或 Reader
派生而来的类都含有名为 read()
的基本方法,用于读取单个字节或者字节数组。同样,所有从 OutputStream
或 Writer
派生而来的类都含有名为 write()
的基本方法,用于写单个字节或者字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。
我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是装饰器设计模式
这里我只会提供这些类的概述,并假定你会使用
输入流类型
InputStream
表示那些从不同数据源产生输入的类,如表
- 字节数组;
String
对象;- 文件;
- “管道”,工作方式与实际生活中的管道类似:从一端输入,从另一端输出;
- 一个由其它种类的流组成的序列,然后我们可以把它们汇聚成一个流;
- 其它数据源,如
Internet 连接。
每种数据源都有相应的 InputStream
子类。另外,FilterInputStream
也属于一种 InputStream
,它的作用是为“装饰器”类提供基类。其中
表InputStream
类 | 功能 | 构造器参数 | 如何使用 |
---|---|---|---|
ByteArrayInputStream |
允许将内存的缓冲区当做 InputStream 使用 |
缓冲区,字节将从中取出 | 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
StringBufferInputStream |
将 String 转换成 InputStream |
字符串。底层实现实际使用 StringBuffer |
作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
FileInputStream |
用于从文件中读取信息 | 字符串,表示文件名、文件或 FileDescriptor 对象 |
作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
PipedInputStream |
产生用于写入相关 PipedOutputStream 的数据。实现“管道化”概念 |
PipedOutputSteam |
作为多线程中的数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
SequenceInputStream |
将两个或多个 InputStream 对象转换成一个 InputStream |
两个 InputStream 对象或一个容纳 InputStream 对象的容器 Enumeration |
作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
FilterInputStream |
抽象类,作为“装饰器”的接口。其中InputStream 类提供有用的功能。见表 |
见表 |
见表 |
输出流类型
如表String
,当然,你也可以用字节数组自己创建
另外,FilterOutputStream
为“装饰器”类提供了一个基类
表OutputStream
类型
类 | 功能 | 构造器参数 | 如何使用 |
---|---|---|---|
ByteArrayOutputStream |
在内存中创建缓冲区。所有送往“流”的数据都要放置在此缓冲区 | 缓冲区初始大小(可选) | 用于指定数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
FileOutputStream |
用于将信息写入文件 | 字符串,表示文件名、文件或 FileDescriptor 对象 |
用于指定数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
PipedOutputStream |
任何写入其中的信息都会自动作为相关 PipedInputStream 的输出。实现“管道化”概念 |
PipedInputStream |
指定用于多线程的数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
FilterOutputStream |
抽象类,作为“装饰器”的接口。其中OutputStream 提供有用功能。见表 |
见表 |
见表 |
添加属性和有用的接口
装饰器在泛型这一章引入。
但是,装饰器模式也有一个缺点:在编写程序的时候,它给我们带来了相当多的灵活性(因为我们可以很容易地对属性进行混搭
FilterInputStream
和 FilterOutputStream
是用来提供装饰器类接口以控制特定输入流 InputStream
和 输出流 OutputStream
的两个类,但它们的名字并不是很直观。FilterInputStream
和 FilterOutputStream
分别从InputStream
和 OutputStream
派生而来,这两个类是创建装饰器的必要条件(这样它们才能为所有被装饰的对象提供统一接口
通过 FilterInputStream
从 InputStream
读取
FilterInputStream
类能够完成两件截然不同的事情。其中,DataInputStream
允许我们读取不同的基本数据类型和 String
类型的对象(所有方法都以 “read” 开头,例如 readByte()
、readFloat()
等等DataOutputStream
,我们就可以通过数据“流”将基本数据类型的数据从一个地方迁移到另一个地方。具体是那些“地方”是由表
其它 FilterInputStream
类则在内部修改 InputStream
的行为方式:是否缓冲,是否保留它所读过的行(允许我们查询行数或设置行数
在实际应用中,不管连接的是什么
表FilterInputStream
类型
类 | 功能 | 构造器参数 | 如何使用 |
---|---|---|---|
DataInputStream |
与 DataOutputStream 搭配使用,按照移植方式从流读取基本数据类型(int 、char 、long 等) |
InputStream |
包含用于读取基本数据类型的全部接口 |
BufferedInputStream |
使用它可以防止每次读取时都得进行实际写操作。代表“使用缓冲区” | InputStream ,可以指定缓冲区大小(可选) |
本质上不提供接口,只是向进程添加缓冲功能。与接口对象搭配 |
LineNumberInputStream |
跟踪输入流中的行号,可调用 getLineNumber() 和 setLineNumber(int) |
InputStream |
仅增加了行号,因此可能要与接口对象搭配使用 |
PushbackInputStream |
具有能弹出一个字节的缓冲区,因此可以将读到的最后一个字符回退 | InputStream |
通常作为编译器的扫描器,我们可能永远也不会用到 |
通过 FilterOutputStream
向 OutputStream
写入
与 DataInputStream
对应的是 DataOutputStream
,它可以将各种基本数据类型和 String
类型的对象格式化输出到“流”中,。这样一来,任何机器上的任何 DataInputStream
都可以读出它们。所有方法都以 “write” 开头,例如 writeByte()
、writeFloat()
等等。
PrintStream
最初的目的就是为了以可视化格式打印所有基本数据类型和 String
类型的对象。这和 DataOutputStream
不同,后者的目的是将数据元素置入“流”中,使 DataInputStream
能够可移植地重构它们。
PrintStream
内有两个重要方法:print()
和 println()
。它们都被重载了,可以打印各种各种数据类型。print()
和 println()
之间的差异是,后者在操作完毕后会添加一个换行符。
PrintStream
可能会造成一些问题,因为它捕获了所有 IOException
(因此,我们必须使用 checkError()
自行测试错误状态,如果出现错误它会返回 true
PrintStream
没有处理好国际化问题。这些问题都在 PrintWriter
中得到了解决,这在后面会讲到。
BufferedOutputStream
是一个修饰符,表明这个“流”使用了缓冲技术,因此每次向流写入的时候,不是每次都会执行物理写操作。我们在进行输出操作的时候可能会经常用到它。
表FilterOutputStream
类型
类 | 功能 | 构造器参数 | 如何使用 |
---|---|---|---|
DataOutputStream |
与 DataInputStream 搭配使用,因此可以按照移植方式向流中写入基本数据类型(int 、char 、long 等) |
OutputStream |
包含用于写入基本数据类型的全部接口 |
PrintStream |
用于产生格式化输出。其中 DataOutputStream 处理数据的存储,PrintStream 处理显示 |
OutputStream ,可以用 boolean 值指示是否每次换行时清空缓冲区(可选) |
应该是对 OutputStream 对象的 final 封装。可能会经常用到它 |
BufferedOutputStream |
使用它以避免每次发送数据时都进行实际的写操作。代表“使用缓冲区”。可以调用 flush() 清空缓冲区 |
OutputStream ,可以指定缓冲区大小(可选) |
本质上并不提供接口,只是向进程添加缓冲功能。与接口对象搭配 |
Reader 和Writer
Reader
和 Writer
时,可能会以为这两个类是用来替代 InputStream
和 OutputStream
的,但实际上并不是这样。尽管一些原始的“流”类库已经过时了(如果使用它们,编译器会发出警告InputStream
和 OutputStream
在面向字节Reader
和 Writer
则提供兼容
-
Java 1.1 往InputStream
和OutputStream
的继承体系中又添加了一些新类,所以这两个类显然是不会被取代的; -
有时我们必须把来自“字节”层级结构中的类和来自“字符”层次结构中的类结合起来使用。为了达到这个目的,需要用到“适配器(adapter)类”:
InputStreamReader
可以把InputStream
转换为Reader
,而OutputStreamWriter
可以把OutputStream
转换为Writer
。
设计 Reader
和 Writer
继承体系主要是为了国际化。老的char
也是Reader
和 Writer
继承体系就是为了让所有的
数据的来源和去处
几乎所有原始的Reader
和 Writer
类来提供原生的InputStream
和 OutputStream
才是正确的解决方案。特别是 java.util.zip
类库就是面向字节而不是面向字符的。因此,最明智的做法是尽量尝试使用 Reader
和 Writer
,一旦代码没法成功编译,你就会发现此时应该使用面向字节的类库了。
下表展示了在两个继承体系中,信息的来源和去处(即数据物理上来自哪里又去向哪里)之间的对应关系:
来源与去处: |
相应的 |
---|---|
InputStream |
Reader 适配器: InputStreamReader |
OutputStream |
Writer 适配器: OutputStreamWriter |
FileInputStream |
FileReader |
FileOutputStream |
FileWriter |
StringBufferInputStream (已弃用) |
StringReader |
(无相应的类) | StringWriter |
ByteArrayInputStream |
CharArrayReader |
ByteArrayOutputStream |
CharArrayWriter |
PipedInputStream |
PipedReader |
PipedOutputStream |
PipedWriter |
总的来说,这两个不同的继承体系中的接口即便不能说完全相同,但也是非常相似的。
更改流的行为
对于 InputStream
和 OutputStream
来说,我们会使用 FilterInputStream
和 FilterOutputStream
的装饰器子类来修改“流”以满足特殊需要。Reader
和 Writer
的类继承体系沿用了相同的思想——但是并不完全相同。
在下表中,左右之间对应关系的近似程度现比上一个表格更加粗略一些。造成这种差别的原因是类的组织形式不同,BufferedOutputStream
是 FilterOutputStream
的子类,但 BufferedWriter
却不是 FilterWriter
的子类(尽管 FilterWriter
是抽象类,但却没有任何子类,把它放在表格里只是占个位置,不然你可能奇怪 FilterWriter
上哪去了
过滤器: |
相应 |
---|---|
FilterInputStream |
FilterReader |
FilterOutputStream |
FilterWriter |
BufferedInputStream |
BufferedReader (也有readLine() |
BufferedOutputStream |
BufferedWriter |
DataInputStream |
使用 DataInputStream ( 如果必须用到 readLine() ,那你就得使用 BufferedReader 。否则,一般情况下就用 DataInputStream |
PrintStream |
PrintWriter |
LineNumberInputStream |
LineNumberReader |
StreamTokenizer |
StreamTokenizer (使用具有 Reader 参数的构造器) |
PushbackInputStream |
PushbackReader |
有一条限制需要明确:一旦要使用 readLine()
,我们就不应该用 DataInputStream
(否则,编译时会得到使用了过时方法的警告BufferedReader
。除了这种情况之外的情形中,DataInputStream
仍是
为了使用时更容易过渡到 PrintWriter
,它提供了一个既能接受 Writer
对象又能接受任何 OutputStream
对象的构造器。PrintWriter
的格式化接口实际上与 PrintStream
相同。
PrintWriter
构造器,以便在将输出写入时简化文件的创建过程,你马上就会见到它们。
其中一种 PrintWriter
构造器还有一个执行自动println()
调用之后,自动执行
未发生改变的类
有一些类在
以下这些 |
---|
DataOutputStream |
File |
RandomAccessFile |
SequenceInputStream |
特别是 DataOutputStream
,在使用时没有任何变化;因此如果想以可传输的格式存储和检索数据,请用 InputStream
和 OutputStream
继承体系。
RandomAccessFile 类
RandomAccessFile
适用于由大小已知的记录组成的文件,所以我们可以使用 seek()
将文件指针从一条记录移动到另一条记录,然后对记录进行读取和修改。文件中记录的大小不一定都相同,只要我们能确定那些记录有多大以及它们在文件中的位置即可。
最初,我们可能难以相信 RandomAccessFile
不是 InputStream
或者 OutputStream
继承体系中的一部分。除了实现了 DataInput
和 DataOutput
接口(DataInputStream
和 DataOutputStream
也实现了这两个接口)之外,它和这两个继承体系没有任何关系。它甚至都不使用 InputStream
和 OutputStream
类中已有的任何功能。它是一个完全独立的类,其所有的方法(大多数都是 native
方法)都是从头开始编写的。这么做是因为 RandomAccessFile
拥有和别的Object
。
从本质上来讲,RandomAccessFile
的工作方式类似于把 DataIunputStream
和 DataOutputStream
组合起来使用。另外它还有一些额外的方法,比如使用 getFilePointer()
可以得到当前文件指针在文件中的位置,使用 seek()
可以移动文件指针,使用 length()
可以得到文件的长度。另外,其构造器还需要传入第二个参数(和fopen()
相同)用来表示我们是准备对文件进行 “随机读”(r)还是“读写”(rwRandomAccessFile
能设计成继承自 DataInputStream
,可能也是个不错的实现方式。
在RandomAccessFile
的大多数功能(但不是全部)都被
IO 流典型用途
尽管我们可以用不同的方式来组合
在这些示例中,异常处理都被简化为将异常传递给控制台,但是这样做只适用于小型的示例和工具。在你自己的代码中,你需要考虑更加复杂的错误处理方式。
缓冲输入文件
如果想要打开一个文件进行字符输入,我们可以使用一个 FileInputReader
对象,然后传入一个 String
或者 File
对象作为文件名。为了提高速度,我们希望对那个文件进行缓冲,那么我们可以将所产生的引用传递给一个 BufferedReader
构造器。BufferedReader
提供了 line()
方法,它会产生一个 Stream<String>
对象:
// iostreams/BufferedInputFile.java
// {VisuallyInspectOutput}
import java.io.*;
import java.util.stream.*;
public class BufferedInputFile {
public static String read(String filename) {
try (BufferedReader in = new BufferedReader(
new FileReader(filename))) {
return in.lines()
.collect(Collectors.joining("\n"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
System.out.print(
read("BufferedInputFile.java"));
}
}
Collectors.joining()
在其内部使用了一个 StringBuilder
来累加其运行结果。该文件会通过 try-with-resources
子句自动关闭。
从内存输入
下面示例中,从 BufferedInputFile.read()
读入的 String
被用来创建一个 StringReader
对象。然后调用其 read()
方法,每次读取一个字符,并把它显示在控制台上:
// iostreams/MemoryInput.java
// {VisuallyInspectOutput}
import java.io.*;
public class MemoryInput {
public static void
main(String[] args) throws IOException {
StringReader in = new StringReader(
BufferedInputFile.read("MemoryInput.java"));
int c;
while ((c = in.read()) != -1)
System.out.print((char) c);
}
}
注意 read()
是以 int
形式返回下一个字节,所以必须类型转换为 char
才能正确打印。
格式化内存输入
要读取格式化数据,我们可以使用 DataInputStream
,它是一个面向字节的InputStream
类而不是 Reader
类。我们可以使用 InputStream
以字节形式读取任何数据(比如一个文件
// iostreams/FormattedMemoryInput.java
// {VisuallyInspectOutput}
import java.io.*;
public class FormattedMemoryInput {
public static void main(String[] args) {
try (
DataInputStream in = new DataInputStream(
new ByteArrayInputStream(
BufferedInputFile.read(
"FormattedMemoryInput.java")
.getBytes()))
) {
while (true)
System.out.write((char) in.readByte());
} catch (EOFException e) {
System.out.println("\nEnd of stream");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
ByteArrayInputStream
必须接收一个字节数组,所以这里我们调用了 String.getBytes()
方法。所产生的的 ByteArrayInputStream
是一个适合传递给 DataInputStream
的 InputStream
。
如果我们用 readByte()
从 DataInputStream
一次一个字节地读取字符,那么任何字节的值都是合法结果,因此返回值不能用来检测输入是否结束。取而代之的是,我们可以使用 available()
方法得到剩余可用字符的数量。下面例子演示了怎么一次一个字节地读取文件:
// iostreams/TestEOF.java
// Testing for end of file
// {VisuallyInspectOutput}
import java.io.*;
public class TestEOF {
public static void main(String[] args) {
try (
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("TestEOF.java")))
) {
while (in.available() != 0)
System.out.write(in.readByte());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
注意,available()
的工作方式会随着所读取媒介类型的不同而有所差异,它的字面意思就是“在没有阻塞的情况下所能读取的字节数”。对于文件,能够读取的是整个文件;但是对于其它类型的“流”,可能就不是这样,所以要谨慎使用。
我们也可以通过捕获异常来检测输入的末尾。但是,用异常作为控制流是对异常的一种错误使用方式。
基本文件的输出
FileWriter
对象用于向文件写入数据。实际使用时,我们通常会用 BufferedWriter
将其包装起来以增加缓冲的功能(可以试试移除此包装来感受一下它对性能的影响——缓冲往往能显著地增加PrintWriter
。按照这种方式创建的数据文件可作为普通文本文件来读取。
// iostreams/BasicFileOutput.java
// {VisuallyInspectOutput}
import java.io.*;
public class BasicFileOutput {
static String file = "BasicFileOutput.dat";
public static void main(String[] args) {
try (
BufferedReader in = new BufferedReader(
new StringReader(
BufferedInputFile.read(
"BasicFileOutput.java")));
PrintWriter out = new PrintWriter(
new BufferedWriter(new FileWriter(file)))
) {
in.lines().forEach(out::println);
} catch (IOException e) {
throw new RuntimeException(e);
}
// Show the stored file:
System.out.println(BufferedInputFile.read(file));
}
}
try-with-resources
语句会自动
文本文件输出快捷方式
PrintWriter
中添加了一个辅助构造器,有了它,你在创建并写入文件时,就不必每次都手动执行一些装饰的工作。下面的代码使用这种快捷方式重写了 BasicFileOutput.java
:
// iostreams/FileOutputShortcut.java
// {VisuallyInspectOutput}
import java.io.*;
public class FileOutputShortcut {
static String file = "FileOutputShortcut.dat";
public static void main(String[] args) {
try (
BufferedReader in = new BufferedReader(
new StringReader(BufferedInputFile.read(
"FileOutputShortcut.java")));
// Here's the shortcut:
PrintWriter out = new PrintWriter(file)
) {
in.lines().forEach(out::println);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println(BufferedInputFile.read(file));
}
}
使用这种方式仍具备了缓冲的功能,只是现在不必自己手动添加缓冲了。但遗憾的是,其它常见的写入任务都没有快捷方式,因此典型的
存储和恢复数据
PrintWriter
是用来对可读的数据进行格式化。但如果要输出可供另一个“流”恢复的数据,我们可以用 DataOutputStream
写入数据,然后用 DataInputStream
恢复数据。当然,这些流可能是任何形式,在下面的示例中使用的是一个文件,并且对读写都进行了缓冲。注意 DataOutputStream
和 DataInputStream
是面向字节的,因此要使用 InputStream
和 OutputStream
体系的类。
// iostreams/StoringAndRecoveringData.java
import java.io.*;
public class StoringAndRecoveringData {
public static void main(String[] args) {
try (
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")))
) {
out.writeDouble(3.14159);
out.writeUTF("That was pi");
out.writeDouble(1.41413);
out.writeUTF("Square root of 2");
} catch (IOException e) {
throw new RuntimeException(e);
}
try (
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")))
) {
System.out.println(in.readDouble());
// Only readUTF() will recover the
// Java-UTF String properly:
System.out.println(in.readUTF());
System.out.println(in.readDouble());
System.out.println(in.readUTF());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出结果:
3.14159
That was pi
1.41413
Square root of 2
如果我们使用 DataOutputStream
进行数据写入,那么DataInputStream
准确地读取数据。这一点很有价值,众所周知,人们曾把大量精力耗费在数据的平台相关性问题上。但现在,只要两个平台上都有
当我们使用 DastaOutputStream
时,写字符串并且让 DataInputStream
能够恢复它的唯一可靠方式就是使用writeUTF()
和 readUTF()
来实现的。writeUTF()
和 readUTF()
使用的是一种适用于writeUTF()
所写的字符串时,必须编写一些特殊的代码才能正确读取。
有了 writeUTF()
和 readUTF()
,我们就可以在 DataOutputStream
中把字符串和其它数据类型混合使用。因为字符串完全可以作为DataInputStream
来恢复它。
writeDouble()
将 double
类型的数字存储在流中,并用相应的 readDouble()
恢复它(对于其它的书类型,也有类似的方法用于读写double
数据作为一个简单的字节序列、char
或其它类型读入。因此,我们必须:要么为文件中的数据采用固定的格式;要么将额外的信息保存到文件中,通过解析额外信息来确定数据的存放位置。注意,对象序列化和
读写随机访问文件
使用 RandomAccessFile
就像是使用了一个 DataInputStream
和 DataOutputStream
的结合体(因为它实现了相同的接口:DataInput
和 DataOutput
seek()
方法移动文件指针并修改对应位置的值。
在使用 RandomAccessFile
时,你必须清楚文件的结构,否则没法正确使用它。RandomAccessFile
有一套专门的方法来读写基本数据类型的数据和
// iostreams/UsingRandomAccessFile.java
import java.io.*;
public class UsingRandomAccessFile {
static String file = "rtest.dat";
public static void display() {
try (
RandomAccessFile rf =
new RandomAccessFile(file, "r")
) {
for (int i = 0; i < 7; i++)
System.out.println(
"Value " + i + ": " + rf.readDouble());
System.out.println(rf.readUTF());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
try (
RandomAccessFile rf =
new RandomAccessFile(file, "rw")
) {
for (int i = 0; i < 7; i++)
rf.writeDouble(i * 1.414);
rf.writeUTF("The end of the file");
rf.close();
display();
} catch (IOException e) {
throw new RuntimeException(e);
}
try (
RandomAccessFile rf =
new RandomAccessFile(file, "rw")
) {
rf.seek(5 * 8);
rf.writeDouble(47.0001);
rf.close();
display();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出结果:
Value 0: 0.0
Value 1: 1.414
Value 2: 2.828
Value 3: 4.242
Value 4: 5.656
Value 5: 7.069999999999999
Value 6: 8.484
The end of the file
Value 0: 0.0
Value 1: 1.414
Value 2: 2.828
Value 3: 4.242
Value 4: 5.656
Value 5: 47.0001
Value 6: 8.484
The end of the file
display()
方法打开了一个文件,并以 double
值的形式显示了其中的七个元素。在 main()
中,首先创建了文件,然后打开并修改了它。因为 double
总是seek()
定位到第double
值,则要传入的地址值应该为 5*8
。
正如前面所诉,虽然 RandomAccess
实现了 DataInput
和 DataOutput
接口,但实际上它和InputStream
及 OutputStream
子类中的任何一个组合起来,所以我们也没法给它添加缓冲的功能。
该类的构造器还有第二个必选参数:我们可以指定让 RandomAccessFile
以“只读”(r)方式或“读写”
(rw)方式打开文件。
除此之外,还可以使用 nio
中的“内存映射文件”代替 RandomAccessFile
,这在附录:新
本章小结
toString()
方法,进行简单的扩展。当我们向一个期望收到字符串的方法传送一个非字符串对象时,会自动调用对象的 toString()
方法(这是
在File
对象来判断文件是否存在,因为如果我们用 FileOutputStream
或者 FileWriter
打开,那么这个文件肯定会被覆盖。
一旦你理解了装饰器模式,并且开始在某些需要这种灵活性的场景中使用该类库,那么你就开始能从这种设计中受益了。到那时候,为此额外多写几行代码的开销应该不至于让人觉得太麻烦。但还是请务必检查一下,确保使用文件一章中的库和技术没法解决问题后,再考虑使用本章的