【Java I/O 流】4 - 4 字符流的文件输入输出流
§4-4 字符流的文件输入输出流
4-4.1 字符流
在上一节我们讲到,字节流在读取文件时,会逐个字节读取,这种读取方式会使得在读取多字节所表示的字符时发生乱码的现象。此外,即使能够调用重载的 read
方法读取多字节,但是由于方法实现的特性,想要将其中的字节数据转换为对应字符集的内容,往往会遇到许多困难(可能发生数组内容覆盖导致旧数据可能未完全读取的问题)。这是我们不愿意看到的,因此,我们往往会使用字符流来读取纯文本文件。
字符流实际上就是字节流 + 字符集。它会根据指定的字符集,在读写字符时适当地选取读写长度,适用于纯文本文件的读写操作。
字符流主要有两个抽象类 Reader
和 Writer
,二者的子类有很多,但它们的命名都体现了它们的作用和继承结构,如下图所示:
4-4.2 FileReader
文件阅读器
FileReader
位于 java.io
,是 Reader
的一个子类,用于读取纯文本文件。FileReader
使用平台默认的字符集或指定字符集将字节解码为字符。
FileReader
目的是读取字符流,若要读取原生字节流,考虑使用 FileInputStream
。
书写步骤:
- 创建文件阅读器对象;
- 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的
File
对象; - 若文件不存在,则会抛出异常
FileNotFoundException
;
- 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的
- 读取文件内容;
- 使用
read
方法读取文件中的数据,方法返回一个整型(int
)数据; - 方法会自动根据对象所指定的字符集正确读取一个或多个字节;
read
方法具有多种重载;- 可以多次调用该方法,不断读取文件中剩余的数据,若无更多内容可读,则返回
-1
;
- 使用
- 释放资源;
- 使用
close
方法关闭流,解除 Java 程序对文件的占用; - 应当在每次使用完流后释放资源,解除占用;
- 使用
三个方法都会抛出编译时异常,应当及时处理。
构造方法:
构造方法 | 描述 |
---|---|
FileReader(File file) |
使用平台默认字符集,创建一个关联指定文件的文件阅读器对象 |
FileReader(File file, Charset charset) |
使用指定字符集,创建一个关联指定文件的文件阅读器对象 |
FileReader(String fileName) |
适用平台指定字符集,创建一个关联指定文件的文件阅读器对象 |
FileReader(String fileName, Charset charset) |
使用指定字符集,创建一个关联指定文件的的文件阅读器对象 |
其中,Charset
是一个位于 java.nio.charset
的抽象类,在传入参数时,可以传入位于相同位置的 StandardCharsets
中所定义的 Charset
实现类对象。
StandardCharsets
中所定义的枚举字符集常量有:
字段 | 描述 |
---|---|
ISO_8859_1 |
ISO 拉丁字母表 No.1,又称 ISO-LATIN-1 |
US_ASCII |
七位 ASCII,又称 ISO646-US、Unicode 字符集的基本拉丁块 |
UTF_16 |
十六位 UCS 转换格式,其字节顺序由一个可选的字节顺序标记来标识 |
UTF_16BE |
十六位 UCS 转换格式,大端字节顺序 |
UTF_16LE |
十六位 UCS 转换格式,小端字节顺序 |
UTF-8 |
八位 UCS 转换格式 |
读取文件:
方法 | 描述 |
---|---|
int read() |
读取一个字符 |
int read(char[] cbuf) |
将字符读取到一个数组中,返回读取的字符数 |
int read(char[] cbuf, int off, int len) |
将字符读取到一个数组的一部分中,返回读取的字符数 |
- 方法会尝试尽可能多地读取字符;
- 空参方法会返回该字符的十进制编码,可强制转型为
char
获取对应字符; - 若已经来到了流的末端,无更多数据可读时,方法返回
-1
;
4-4.3 FileWriter
文件写入器
FileWriter
位于 java.io
,是 Writer
的一个子类,用于写出一个纯文本文件。FileWriter
使用平台默认字符集或指定字符集写出文件。
FileWriter
目的是写出字符流,若要写出原生字节流,考虑使用 FileOutputStream
。
书写步骤:
- 创建文件写入器对象;
- 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的
File
对象; - 必须保证父级路径存在,否则抛出异常
FileNotFoundException
; - 若文件不存在(父级路径必须存在),则会新建该文件;
- 方法的最后一个参数
boolean append
用于控制续写(追加)行为,默认为false
,则会在创建对象时清空文件; - 若
boolean append
为true
,则启动追加,在文件末尾写入新数据;
- 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的
- 写出数据;
- 使用
write
方法向文件写出数据; write
方法具有多种重载;- 可多次调用该方法,不断地向文件写出数据;
- 使用
- 释放资源;
- 使用
close
方法关闭流,解除 Java 程序对文件的占用; - 应当在每次使用完流后释放资源,解除占用;
- 使用
三个方法都会抛出编译时异常,应当及时处理。
写出数据的方式:
方法 | 描述 |
---|---|
void write(int c) |
写出单字符 |
void write(char[] cbuf) |
写出一个字符数组 |
void write(String str) |
写出一个字符串 |
void write(char[] cbuf, int off, int len) |
写出一个字符数组的一部分 |
void write(String str, int off, int len) |
写出一个字符串的一部分 |
- 若传入参数为整数,则实际输出的为其对应编码的字符,
- 不同于字节输出流,受到字节的限制,部分字符编码可能会因为超出字节上限发生溢出,进而导致乱码;
4-4.4 底层原理
FileReader
:对象创建成功后,对象内部会创建一个长度为 8192 的字节型数组,用作缓冲区。每次调用 read
方法,会先判断缓冲区中是否有数据,若没有,则读取文件中一部分数据,并放入缓冲区中;若有,则读取缓冲区中的数据。
若缓冲区还存在尚未读取完的数据,则继续读取缓冲区中的内容,否则,则继续从文件中读取下一部分数据,并放入缓冲区中读取。读到文件末尾,则返回 -1
。
空参 read
一次读取一个字符,遇到多字节字符则读取多个字节,将字符解码为十进制编码后返回。
有参 read
一次读取多个字符,尽可能多地读取数据,并将解码所的字符存放到数组中,并返回读取的字符个数。
设置缓冲区的目的在于,不同于字节流的逐个字节读取,字符流每次读取是逐个字符读取,而不同语言的字符可能由多个字节表示,逐个字节读取并不合适。为了提高效率,就设置了长度适中的缓冲区(在内存中),尽可能地减少上下文切换,提高效率。
FileWriter
:对象创建成功后,对象内部也会创建一个长度为 8192 的字节型数组,用作缓冲区。每次调用 write
方法,都是往缓冲区中写入数据,而并非立刻写进文件中。当且仅当缓冲区已满,或调用 flush
方法手动刷新,或关闭流时,缓冲区中的数据才会被写进文件中。
值得注意的是,调用了 flush
后,仍能够继续调用 write
方法,因为此时流尚未关闭。但一旦关闭了流,就不能够再调用 write
写数据了,否则会抛出异常 IOException
。
缓冲区:缓冲区是字符流的一个特点,这一特点在字节流中并不存在。
4-4.5 案例演示
需求一:复制文件夹(含子目录)
public static void copy(File src, File dest) throws IOException {
Objects.requireNonNull(src);
Objects.requireNonNull(dest);
//判断源文件
if (!src.exists()) {
System.err.println("错误:源文件不存在");
return;
}
//判断目标路径
if (dest.isFile()) {
//目标路径是文件,舍弃
System.err.println("错误:目标路径是文件,应当为目录。");
return;
} else {
//无论是否存在,都先创建目录
dest.mkdirs();
}
//开始复制:深度优先
File[] files = src.listFiles(); //获取源文件夹下的子文件(夹)
//若为空:不存在(排除)、为文件、需要权限(暂不考虑)
if (files == null) {
//复制单文件
copyFile(src, dest);
} else {
//复制文件夹
//创建文件夹(自己)
File self = new File(dest, src.getName());
self.mkdirs();
//遍历文件夹,将内容复制到目标路径的 self
for (File file : files) {
if (file.isFile()) {
//判断为文件
copyFile(file, self);
} else {
//判断为文件夹
//深度优先
copy(file, self);
}
}
}
}
private static void copyFile(File src, File dest) throws IOException {
Objects.requireNonNull(src);
Objects.requireNonNull(dest);
//判断存在性
if (!src.exists() || !dest.exists()) {
System.err.println("错误:源文件或目标路径不存在");
return;
}
//判断源和目标属性
if (src.isDirectory() || dest.isFile()) {
System.err.println("错误:源为目录或目标为文件");
return;
}
//链接文件
FileInputStream fis = new FileInputStream(src); //链接源文件
FileOutputStream fos = new FileOutputStream(new File(dest, src.getName())); //链接输出文件
//边读边写
byte[] bytes = new byte[1 << 22]; //4 MB
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
//关闭流
fos.close();
fis.close();
}
需求二:文件加密与解密
public static void encrypt(File src) throws IOException {
Objects.requireNonNull(src);
if (!src.exists()) {
System.err.println("错误:源文件不存在");
return;
}
if (src.isDirectory()) {
System.err.println("错误:不支持目录加密解密");
return;
}
//开始加密
FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(new File(src.getParent(), "Processed.txt"));
//读写
byte[] bytes = new byte[1 << 22]; //4 MB
int len;
while ((len = fis.read(bytes)) != -1) {
//使用异或运算,再次进行异或可解密
for (int i = 0; i < len; i++) {
bytes[i] ^= 100;
}
fos.write(bytes, 0, len);
}
//关闭流
fos.close();
fis.close();
}
需求三:对文件中的数据进行排序:2-1-9-4-7-8
,要求格式不变
public static void sortData(File src) throws IOException {
Objects.requireNonNull(src);
if (!src.exists()) {
System.err.println("错误:文件不存在");
return;
}
if (src.isDirectory()) {
System.err.println("错误:不支持文件夹");
return;
}
//读取
ArrayList<Integer> data = null;
FileReader fr = new FileReader(src);
StringBuilder sb = new StringBuilder();
int ch;
while ((ch = fr.read()) != -1) {
sb.append((char) ch);
}
fr.close();
//使用流解决问题
data = (ArrayList<Integer>) Arrays.stream(sb.toString().split("-"))
.map(Integer::parseInt)
.sorted(Integer::compare)
.collect(Collectors.toList());
//写入
//使用String类成员方法
FileWriter fw = new FileWriter(src);
String res = data.toString().replace(", ", "-");
res = res.substring(1, res.length() - 1);
fw.write(res);
fw.close();
}