本章概要
- Java I/O
- 输入流和输出流
- 字节流和字符流
- 节点流和处理流
- 内存映射文件技术
1.8 Java I/O
流是一个抽象的概念,代表了数据的无结构化传递。流的本质是数据在不同设备之间的传输。在 Java 中,数据的读取和写入都是以流的方式进行的。
在 Java 中,根据数据流向的不同,可以将流分为输入(Input)流和输出(Output)流;根据单位的不同,可以将流分为字节流和字符流;根据等级不同,可以将流分为节点流和处理流。
1.8.1 输入流和输出流
- 输入流:输入流用于将数据从控制台、文件、网络等外部设备输入到应用程序进程中,如下:
- 输出流:输出流用于将应用程序进程中的数据输出到控制台、文件、显示器等中,如下:
1.8.2 字节流和字符流
- 字节流:字节流是以字节(1byte = 8bit)为单位对数据进行读写操作,也就是说,字节流进行一次读取或者写入都是以 8bit 为单位进行的,因此主要用于处理二进制数据。在 Java 中使用 InputStream 、OutputStream 处理字节数据,其中 InputStream 用于字节流输入,OutputStream 用于字节流输出。
- 字符流:字符流以字符为单位对数据进行读写操作,一次读取或写入都是以 16bit 为单位进行的。Java 中的字符采用 Unicode 编码,一个字符占两个字节。字符流主要用于处理文本数据的读写,在处理过程中需要进行字符集的转化。在 Java 中使用 Reader 、Writer 处理字符数据,其中 Reader 用于字符流输入,Writer 用于字符流输出。字节流和字符流的输入流、输出流对比如下:
类型 | 字节流 | 字符流 |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
- InputStream(字节输入流):InputStream 是一个抽象类,其子类包括 FileInputStream(文件输入流)、ObjectInputStream(对象输入流)、ByteInputStream(字节数组输入流)、PipedInputStream(管道输入流)、FilterInputStream(过滤器输入流)、SequenceInputStream(顺序输入流)、StringBufferedInputStream(缓冲字符串输入流),如下:
InputStream 类的所有方法在遇到错误时都会抛出 IOException 。InputStream 用于以字节形式将数据读入应用程序中,常用的方法及其作用如下:
方法 | 作用 |
---|---|
int read() | 从输入流读取 8 字节数据并将其转换成一个 0~255 的整数,返回值为读取的总字节数,遇到数据流的末尾则返回 -1 |
int read(byte[] b) | 从输入流中读取最大长度为 len 字节的数据并保存到 b 字节数组中,遇到数据流的末尾则返回 -1 |
int read(byte[] b,int off,int len) | 以输入流中的 off 位置为开始位置读取最大长度为 len 字节的数据,并将其保存到 b 字节数组中 |
void close() | 关闭数据流 |
int available() | 返回可以从输入流中读取的位数 |
skip(long n) | 从输入流跳过 n 字节 |
一段基于 FileInputStream 读取文件的代码如下:
public static void main(String[] args) throws IOException {
String path = "F:\\";
String fileName = "test.txt";
//1.定义待读取的文件
File file = new File(path,fileName);
//2.从文件中读取数据到 fileInputStream
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[fileInputStream.available()];
int n = 0;
//3.从 fileInputStream 中不断循环读取字节数据并写入 bytes,
//直到遇到数据流结尾时,read 方法返回 -1,则退出循环
while ((n = fileInputStream.read(bytes)) != -1){
//将 byte[] 转化为字符串
String s = new String(bytes);
System.out.println(s);
}
//关闭输入文件流
fileInputStream.close();
}
打印内容如下:
你好
小明
是的
很好啊
- OutputStream(字节输出流):OutputStream 是一个抽象类,其子类包括 FileOutputStream(文件输出流)、ByteOutputStream(字节数组输出流)、FilterOutputStream(过滤器输出流)、ObjectOutputStream(对象输出流)、PipedOutputStream(管道输出流),如下:
OutputStream 类的所有方法在遇到错误时都会抛出 IOException 异常。OutputStream 用于以字节形式将数据输出到目标设备,常用的方法及其作用如下:
方法 | 作用 |
---|---|
int write(b) | 将指定字节的数据写入输出流 |
int write(byte[] b) | 将指定字节数组的数据写入输出流 |
int write(byte[] b,int off,int len) | 将指定的字节数组从 off 位置开始的 len 字节的内容写入输出流 |
close() | 关闭数据流 |
flush() | 刷新数据流,强行将缓冲区的内容写入输出流 |
基于 FileOutputStream 读取文件的一段代码如下:
public static void main(String[] args) throws IOException {
String path = "F:\\";
String fileName = "test.txt";
//1.定义待写入的文件
File file = new File(path,fileName);
//2.定义 fileOutputStream
FileOutputStream fileOutputStream = new FileOutputStream(file);
//3.将数据写入 fileOutputStream
fileOutputStream.write("biubiubiu".getBytes());
//4.关闭 fileOutputStream
fileOutputStream.close();
}
- Reader:Reader 类是所有字符流输入类的父类,用于以字符形式将数据读取到应用程序中,具体的子类的作用说明如下:
- CharArrayReader:将字符数组转换为字符输入流并从中读取字符
- StringReader:将字符串转换为字符输入流并从中读取字符
- BufferedReader:为其它字符输入流提供读缓冲区
- PipedReader:连接到一个 PipedWriter
- FilterReader:Reader 类的子类,用于丰富 Reader 类的功能
- InputStreamReader:将字节输入流转换为字符输入流,可以指定字符编码
Reader 类中的常用方法有 close、mark、skip、reset 等,以下表主要介绍 Reader 类中最常用的 read 方法:
方法名及返回值类型 | 方法说明 |
---|---|
int read() | 从输入流中读取一个字符并转化为 0~65535 的整数,当读取到流的末尾时返回 -1 |
int read(char[] cbuf) | 从输入流中读取若干个字符并保存到参数 cbuf 指定的字符数组中,当读取到流的末尾时,返回 -1 |
int read(char[] cbuf,int off,int len) | 以输入流中的 off 位置为开始位置读取最大长度为 len 直接的数据并将其保存到 cbuf 字符数组中,当读取到流的末尾时返回 -1 |
基于 BufferedReader 读取文件的一段代码如下:
public static void main(String[] args) throws IOException {
String path = "F:\\test.txt";
//1.创建 fileReader
FileReader fileReader = new FileReader(path);
//2.基于 fileReader 创建 bufferedReader
BufferedReader bufferedReader = new BufferedReader(fileReader);
//3.定义一个 strLine ,表示 bufferedReader 读取的结果
String strLine = "";
//4.调用 readLine 方法将缓冲区中的数据读取为字符串,
//当 readLine 返回 -1 时,表示已经读取到文件末尾了
while ((strLine = bufferedReader.readLine()) != null){
System.out.println(strLine);
}
//5.关闭 bufferedReader
bufferedReader.close();
}
- Writer:Writer 类是所有字符流输出类的父类,用于以字符形式将数据写出到外部设备,具体子类图如下:
、
子类介绍如下:
- CharArrayWriter:用于向内存缓冲区的字符数组写数据
- StringWriter:用于向内存缓冲区的字符串(StringBuffer)写数据
- BufferedWriter:用于为其它字符串输出流提供写缓冲区
- PipedWriter:用于连接到一个 PipedReader
- OutputStreamWriter:用于将字节输出流转换为字符输出流,可以指定字符编码
- FilterWriter:过滤器字符输出流
Writer 类的所有方法在执行出错时都会引发 IOException 异常。Writer 类也包含 close、flush 等方法,这些方法的功能可以参考 OutputStream 类的方法。下面主要介绍 Writer 类的 write 方法和 append 相关的方法,如下:
方法名及返回值类型 | 方法说明 |
---|---|
void write(int c) | 向输出流写入一个字符 |
void write(char[] cbuf) | 将字符数组 cbuf 中的字符写入输出流中 |
void write(char[] cbuf,int off,int len) | 将字符数组 cbuf 中从 off 位置开始获取长度为 len 的字符并写入输出流中 |
void write(String str) | 将字符串写入输出流中 |
void write(String str,int off,int len) | 将字符串中的部分字符串写入输出流中 |
Writer append(char c) | 将字符追加到输出流中 |
Writer append(charSequence esq) | 将参数 esq 指定的字符序列追加到输出流中 |
Writer append(charSequence esq,int start,int end) | 将参数 esq 指定字符序列的子序列追加到输出流中 |
基于 BufferedWriter 将字符串写入文件中的一段代码如下:
public static void main(String[] args) throws IOException {
//1.定义一个 fileWriter
String path = "F:\\test.txt";
FileWriter fileWriter = new FileWriter(path);
//2.基于 fileWriter 定义一个 bufferedWriter
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
//3.调用 bufferedWriter 的 write 方法将字符串写入 bufferedWriter
bufferedWriter.write("小明开始输入啦,biubiubiu");
//4.关闭 bufferedWriter
bufferedWriter.close();
//5.关闭 fileWriter
fileWriter.close();
}
1.8.3 节点流和处理流
节点流是低级流,直接与数据源相连,对数据源上的流进行读写。
处理流是高级流,采用修饰器模式对节点流进行了封装,不直接与数据源相连,主要用于消除不同节点流的实现差异,提供更方便的方法来完成数据的输入和输出。
例如,FileInputStream、FileOutputStream、FileReader、FileWriter 属于节点流;BufferInputStream、BufferOutputStream、BufferReader、BufferWriter 属于处理流。
相对于节点流,处理流有如下特性:
- 性能高:处理流通过增加缓存的方式提高数据的输入和输出效率
- 操作方便:处理流封装了一系列高级方法来完成一次性大批量数据的输入和输出
1.8.4 内存映射文件技术
操作系统可以利用虚拟内存实现将一个文件或文件的一部分“映射”到内存中。然后,这个文件就可以被当作内存数据来访问,比传统的文件要快得多,这种技术就是内存映射文件技术。
内存映射文件技术的一个关键优势就是操作系统负责真正的文件读写,应用程序只需处理内存数据,就可以实现非常快速的 I/O 操作。在写入过程中,即使应用程序在将数据写入内存后进程出错退出,操作系统仍然会将内存映射文件中的数据写入(写出)文件系统。
另一个更突出的优势是共享内存,即内存映射文件可被多个进程同时访问,起到低时延共享内存的作用。
Java 中 的 java.nio 包支持的内存映射文件,具体使用方式是通过 MappedByteBuffer 读写内存,而且内存映射文件技术涉及的内存在 Java 的堆空间之外,这也是其效率高的一个原因。
在 Java 中将一个文件映射到内存并操作共分为如下 3 步:
- 从文件中获得一个通道(channel)
String path = "F:\\test.txt";
RandomAccessFile randomAccessFile = new RandomAccessFile(path,"rw");
FileChannel fileChannel = randomAccessFile.getChannel();
以上代码定义了一个可读写的 RandomAccessFile,然后调用 RandomAccessFile 的 getChannel 方法获取一个 FileChannel。
- 调用 FileChannel 的 map 方法将文件映射到虚拟内存
fileChannel.map(mode,0,length);
以上代码中的 mode 参数用于指定映射模式,支持的模式有如下 3 种:
- FileChannel.MapMode.READ_ONLY:所产生的缓冲区是只读的
- FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中。注意,其它映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的最终行为是依赖于操作系统。
- FileChannel.MapMode.PRIVATE:所产生的的缓冲区是可写的,但任何修改对该缓冲区来说都是私有的,不会传播到文件中。
- 调用 MappedByteBuffer 的 put(byte[] src) 向内存映射文件写入数据,调用 get(int index) 获取文件中对应索引的数据,以字节形式返回。
一段完整的 Java 内存映射文件操作代码如下:
public static void main(String[] args) throws IOException {
//1.定义文件流
String path = "F:\\test.txt";
RandomAccessFile randomAccessFile = new RandomAccessFile(path, "rw");
//2.获取 fileChannel
FileChannel fileChannel = randomAccessFile.getChannel();
//3.定义 mappedByteBuffer
int start = 0;
int len = 1024;
//调用 map 函数的过程其实就是磁盘文件到内存数据的映射过程,
//对 fileChannel 调用 map 函数后,应用程序可以像使用内存一样使用该文件
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.PRIVATE, start, len);
//4.进行 mappedByteBuffer 数据的输入,分别在内存映射文件中写入如下字符串
mappedByteBuffer.put("12345".getBytes());
mappedByteBuffer.put("6789".getBytes());
mappedByteBuffer.put("xiaoming".getBytes());
System.out.println((char) mappedByteBuffer.get(2));
//5.mappedByteBuffer 数据的读取:读取所有数据
for (int i = 0; i < mappedByteBuffer.position(); i++) {
System.out.println((char) mappedByteBuffer.get(i));
}
}
以上代码首先通过 “RandomAccessFile randomAccessFile = new RandomAccessFile(path, “rw”);”定义 RandomAccessFile 文件流实例 randomAccessFile,然后通过 “FileChannel fileChannel = randomAccessFile.getChannel();”获取 FileChannel,接着通过 “fileChannel.map(FileChannel.MapMode.PRIVATE, start, len);”将磁盘文件映射到内存数据,这样程序可以像使用内存一样使用该文件。
在内存文件映射好后,通过 “mappedByteBuffer.put(“xiaoming”.getBytes());” 向 mappedByteBuffer 写入数据,通过 “(char) mappedByteBuffer.get(2)”读取第 3 个字符 “a”,然后通过 for 循环对内存映射文件中的数据进行遍历。
内存映射文件到底有多快呢?
对于一个 30MB 的文件计算 CRC,使用 InputStream 耗时 21553ms,使用 RandomAccessFile 耗时 26668 ms,使用 BufferedInputStream 耗时 317ms,使用 MappedByteBuffer 耗时 75ms。可以看出, MappedByteBuffer 的效率分别是 InputStream 和 RandomAccessFile 的287、355倍;约是 BufferedInputStream 的 4.2 倍,这种优势还会随着文件大小的增加而增加,在达到 1G 以上时更加明显。
相关面试题:
- Java 内存映射文件技术是什么?你在项目中有使用过该技术吗?★★★☆☆
- 在 Java 中有几种类型的流?★★★☆☆
- InputStream 的实现类有哪些?★★★☆☆
- OutputStream 的实现类有哪些?★★★☆☆
- 在 Java 中如何进行文件读写?★★★☆☆
- 字节流和字符流有什么区别?★★☆☆☆
- 什么是处理流?常见的处理流有哪些?★★☆☆☆
- 字符流和字节流有什么区别?★★☆☆☆