【Java】I/O 流篇 —— 字符 I/O 流
目录
- 字符集
- ASCII(美国信息交换标准代码)
- GBK(汉字内码扩展规范)
- Unicode(UTF-8 作为常用实现方式)
- 字符 I/O 流
- 字符流概述
- FileReader 类
- 构造方法
- 基本操作
- 读数据的 2 种方法
- FileWriter 类
- 构造方法
- 写数据的 5 种方法
- 基本操作
- 原理
- FileReader 原理
- FileWriter 原理
- 基本概念
- 工作原理
- 使用场景
- 代码示例
- 注意事项
字符集
ASCII(美国信息交换标准代码)
- 诞生背景:20 世纪 60 年代,计算机技术开始兴起,当时计算机主要在美国使用。为了实现不同计算机系统之间信息的交换和处理,美国制定了 ASCII 编码标准。它是基于拉丁字母的一套电脑编码系统,也是计算机领域中最早且应用广泛的字符集。
- 编码方式:ASCII 采用 7 位二进制数进行编码,由于计算机存储的基本单位是字节(8 位),所以在实际存储时,最高位补 0,这样就占用 1 个字节。7 位二进制数可以表示的字符数量为 个。
- 字符范围:
- 控制字符:包括一些不可显示的字符,用于控制设备的操作,如换行符(LF,ASCII 码值为 10)、回车符(CR,ASCII 码值为 13)、制表符(TAB,ASCII 码值为 9)等,它们的 ASCII 码值范围是 0 - 31 和 127。
- 可显示字符:包含英文字母(大写 A - Z,ASCII 码值为 65 - 90;小写 a - z,ASCII 码值为 97 - 122)、数字 0 - 9(ASCII 码值为 48 - 57)以及常见的标点符号和特殊字符(如!、@、# 等),其 ASCII 码值范围是 32 - 126。
- 应用场景:ASCII 字符集在处理纯英文文本时非常高效,几乎所有的计算机系统和软件都支持 ASCII 编码。例如,在早期的英文程序、简单的文本文件、网络通信协议(如 HTTP 协议中部分头部信息)等场景中广泛应用。
- 局限性:ASCII 只能表示英文字符和一些基本的控制字符,无法满足世界上其他众多语言文字(如中文、日文、阿拉伯文等)的编码需求。随着计算机在全球的普及,这种局限性越来越明显。
GBK(汉字内码扩展规范)
- 诞生背景:随着计算机在中国的普及,ASCII 字符集无法满足中文信息处理的需求。我国首先制定了 GB2312 字符集,收录了 6000 多个常用汉字和一些符号。但 GB2312 不能涵盖所有的中文汉字,为了更全面地满足中文信息处理的需要,1995 年发布了 GBK 字符集,它是对 GB2312 的扩展和增强。
- 编码方式:GBK 采用双字节编码方式。对于 ASCII 字符,GBK 字符集保持了与 ASCII 编码的一致性,使用单字节表示(第一个字节的最高位为 0),这保证了对英文等 ASCII 字符的兼容性。而对于汉字和其他非 ASCII 字符,则使用两个字节表示。一般来说,第一个字节(高字节)的取值范围是 0x81 - 0xFE,第二个字节(低字节)的取值范围是 0x40 - 0xFE(除去 0x7F)。
- 字符范围:GBK 收录了 21003 个汉字,涵盖了大部分常用字、次常用字以及一些生僻字,同时还包括 ASCII 字符和大量的图形符号,如数学运算符号、货币符号、制表符等。这使得 GBK 能够满足中文文本处理中对各种字符的需求。
- 应用场景:GBK 字符集在中文操作系统(如早期的中文 Windows 系统)、中文软件(如 WPS、早期的办公软件等)、中文数据库存储(如早期的 MySQL 数据库在处理中文数据时,GBK 是一种常见的字符集配置选项)等领域得到了广泛应用。
- 局限性:GBK 字符集主要是为了满足中文信息处理的需求而设计的,对于其他语言文字的支持有限。在国际通用性方面存在不足,不同国家和地区有各自的字符集标准,在进行跨国数据交换和多语言系统开发时,GBK 字符集可能会遇到兼容性问题,需要进行字符集转换等额外操作。
Unicode(UTF-8 作为常用实现方式)
- 诞生背景:为了解决全球不同语言文字的编码问题,实现所有字符的统一编码,Unicode 应运而生。Unicode 旨在为世界上每一种语言的每一个字符都提供一个唯一的数字编码,无论该字符在何种平台、何种程序以及何种语言中使用,其编码都是一致的。UTF-8 是 Unicode 的一种可变长字符编码实现方式,因其良好的兼容性和空间效率,成为了 Unicode 应用中最常用的编码方式。
- 编码方式(UTF-8):UTF-8 是一种可变长的编码方式,它使用 1 - 4 个字节来表示一个字符:
- 对于 ASCII 字符(码点范围 U+0000 - U+007F),UTF - 8 使用 1 个字节表示,且编码与 ASCII 码完全相同,这保证了对 ASCII 字符集的兼容性。
- 对于常用的汉字等字符(一般码点范围在 U+4E00 - U+9FFF 等),UTF - 8 使用 3 个字节表示。
- 对于一些生僻字符或特殊符号(码点范围在辅助平面等),UTF - 8 可能会用 4 个字节表示。
- 字符范围:Unicode 涵盖了世界上几乎所有已知的语言文字、符号、象形文字以及表情符号等。包括但不限于中文、英文、日文、阿拉伯文、古文字等各种语言的字符,以及数学符号、音乐符号、旗帜符号等特殊符号。
- 应用场景:Unicode(UTF-8)在互联网、操作系统、编程语言、数据库等领域都得到了广泛的支持。在互联网上,网页通常使用 UTF - 8 编码来确保不同语言的内容都能正确显示;在操作系统中,如现代的 Windows、Linux、macOS 等都支持 Unicode 字符集;在编程语言中,像 Python 3 默认使用 Unicode 字符串,Java 也对 Unicode 有良好的支持;在数据库中,如 MySQL、Oracle 等也都支持 Unicode 编码,方便存储和处理多语言数据。
- 优势:Unicode 的优势在于其广泛的字符覆盖范围和良好的兼容性,能够满足全球各种语言文字的编码需求,使得不同语言之间的信息交流和处理更加便捷。UTF - 8 作为其实现方式,在存储和传输方面具有较高的效率,尤其是对于以 ASCII 字符为主的文本,能够节省存储空间。
字符 I/O 流
字符流概述
- 字符流的底层其实就是字节流:字符流 = 字节流 + 字符集
- 特点:
- 输入流:一次读一个字节,遇到中文时,一次读多个字节
- GBK 一次读两个字节
- UTF - 8 一个读三个字节
- 输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中
- 输入流:一次读一个字节,遇到中文时,一次读多个字节
- 使用场景:对于纯文本文件进行读写操作
FileReader 类
构造方法
-
FileReader(File file)
- 功能描述:创建一个新的
FileReader
对象,该对象会从指定的File
对象所代表的文件中读取字符数据。同样,如果文件不存在、是目录或无法打开,会抛出FileNotFoundException
异常。
- 功能描述:创建一个新的
-
FileReader(String fileName)
- 功能描述:创建一个新的
FileReader
对象,该对象会根据指定的文件名称来读取文件。文件名称可以是相对路径或绝对路径。如果指定的文件不存在、是一个目录而不是文件,或者由于其他原因无法打开该文件,会抛出FileNotFoundException
异常。
- 功能描述:创建一个新的
-
FileReader(FileDescriptor fd)
- 功能描述:创建一个新的
FileReader
对象,该对象会通过指定的文件描述符fd
来读取字符数据。文件描述符是一个底层系统用于标识打开文件的整数,这种构造方法通常用于与底层系统进行交互或处理已经打开的文件。
- 功能描述:创建一个新的
基本操作
操作步骤:
-
创建字符输入流对象
FileReader fr = new FileReader("aaa.txt");
-
读取数据
fr.read();
-
释放资源
fr.close();
代码示例:
import java.io.FileReader;
import java.io.IOException;
public class Demo {
public static void main(String[] args) throws IOException {
// 创建对象
FileReader fr = new FileReader("aaa.txt");
// 读取数据
int ch;
while((ch = fr.read()) != -1) {
System.out.println(ch);
}
// 释放资源
fr.close();
}
}
注意事项:
- read() 默认一个字节一个字节的读取,如果遇到中文就会一次读取多个
- 在读取之后,read() 的底层还会进行解码并转成十进制,并将其作为返回值,对应字符集字符
读数据的 2 种方法
int read()
读取数据,末尾返回 -1int read(char[] buffer)
读取多个数据,末尾返回 -1
代码示例:
import java.io.FileReader;
import java.io.IOException;
public class Demo {
public static void main(String[] args) throws IOException {
// 创建对象
FileReader fr = new FileReader("aaa.txt");
/*以下是文件中的内容
你好
今天天气不错
*/
// 读取数据
char[] chars = new char[2];
int len;
while((len = fr.read(chars)) != -1) {
System.out.print(new String(chars,0,len));
}
// 释放资源
fr.close();
}
}
程序运行结果如下:
你好
今天天气不错
如果把代码中的输出语句改为以下情形:
System.out.println(new String(chars,0,len));
则程序运行结果变为:
你好
今天
天气
不错
造成这样的原因是在 “你好” 的后面其实是有 “\r\n”,由于数组长度为 2,每次只能读取两个字符,读取到 “\r\n” 就会回车换行,然后 println() 又会换行,所以导致输出结果变为这样,究其原因就是数组长度不够。
注意事项:
int read()
返回的是十进制整数,如果要想得到字符,可以进行强制类型转换int read(char[] buffer)
是将空参 read() 与强制类型转换结合起来,所以得到的直接是字符
FileWriter 类
构造方法
-
FileWriter(String fileName)
- 功能描述:创建一个
FileWriter
对象,用于向指定名称的文件写入字符数据。如果指定的文件不存在,会尝试创建该文件;如果文件已经存在,会覆盖原有内容。若在创建或打开文件过程中出现问题,比如没有权限创建文件、指定的路径无效等,会抛出IOException
异常。
- 功能描述:创建一个
-
FileWriter(String fileName, boolean append)
- 功能描述:创建一个
FileWriter
对象,用于向指定名称的文件写入字符数据。append
参数决定了写入操作的模式,如果append
为true
,则会以追加模式打开文件,新写入的数据会添加到文件末尾;如果append
为false
,则会覆盖原有内容。同样,若在操作文件时出现问题,会抛出IOException
异常。
- 功能描述:创建一个
-
FileWriter(File file)
- 功能描述:创建一个
FileWriter
对象,用于向指定File
对象所代表的文件写入字符数据。若文件不存在,会尝试创建;若存在,会覆盖原有内容。若文件操作出现问题,会抛出IOException
异常。
- 功能描述:创建一个
-
FileWriter(File file, boolean append)
- 功能描述:创建一个
FileWriter
对象,用于向指定File
对象所代表的文件写入字符数据。append
参数决定写入模式,true
为追加模式,false
为覆盖模式。若操作文件时出现问题,会抛出IOException
异常。
- 功能描述:创建一个
写数据的 5 种方法
void write(int c)
写出一个字符void write(String str)
写出一个字符串void write(String str,int off,int len)
写出一个字符串的一部分void write(char[] cbuf)
写出一个字符数组void write(char[] cbuf,int off,int len)
写出字符数组的一部分
基本操作
操作步骤:
-
创建字符输出流对象
FileWriter fw = new FileWriter("aaa.txt");
-
写入数据
fw.write();
-
释放资源
fw.close();
代码示例:
import java.io.FileWriter;
import java.io.IOException;
public class Demo {
public static void main(String[] args) throws IOException {
// 创建对象
FileWriter fw = new FileWriter("aaa.txt");
// 写入数据
fw.write(25105); // 写入'我'
//释放资源
fw.close();
}
}
注意事项:
- 创建字符输出流对象
- 参数是字符串表示的路径或者 File 对象都是可以的
- 如果文件不存在会创建一个新的文件,但是要保证父级路径是存在的
- 如果文件已经存在,则会清空文件,如果不想清空可以打开续写开关
- 写数据
- 如果 write 方法的参数是整数,但是实际上写到本地文件中的是整数在字符集上对应的字符
- 释放资源
- 每次使用完流之后都要释放资源
原理
FileReader 原理
- 创建字符输入流对象
- 关联文件,并创建缓冲区(长度为 8192 的字节数组)
- 读取数据
- 判断缓冲区中是否有数据可以读取
- 缓冲区没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区
- 如果文件中也没有数据了,返回 - 1
- 缓冲区有数据:就从缓冲区中读取。
- 空参的 read 方法:一次读取一个字节,遇到中文一次读多个字节,把字节解码并转成十进制返回
- 有参的 read 方法:把读取字节,解码,强转三步合并了,强转之后的字符放到数组中
来看下面这段代码:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class Demo {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("aaa.txt");
fr.read();
FileWriter fw = new FileWriter("aaa.txt");
int ch;
while((ch = fr.read()) != -1) {
System.out.println((char)ch);
}
fw.close();
fr.close();
}
}
我们知道创建字符输出流对象是会清空文件中原本的内容,那么问题来了,在这段代码中,创建了字符输出流对象后还会输出文件中的内容吗?
答案是会的,因为在创建字符输出流对象之前,调用了一次字符输入流的 read 方法,这会使文件中的内容被读取进入到缓冲区,即使创建字符输出流对象是会清空文件中原本的内容,但是只要缓冲区中有数据,就会被读取
FileWriter 原理
FileWriter
是 Java 中用于将字符数据写入文件的字符输出流类,其 flush
方法在处理文件写入操作时发挥着重要作用,下面将从基本概念、工作原理、使用场景、代码示例、注意事项等方面详细介绍:
基本概念
FileWriter
继承自 OutputStreamWriter
,而 OutputStreamWriter
实现了 Flushable
接口,因此 FileWriter
具备 flush
方法。该方法的主要作用是将缓冲区中暂存的数据强制写入到目标文件中,从而清空缓冲区。
工作原理
FileWriter
在进行字符写入操作时,并非直接将字符写入文件,而是先将字符数据存储在内部的缓冲区中。这是为了减少与文件系统的频繁交互,提高写入效率。当缓冲区被填满或者调用 flush
方法时,FileWriter
会将缓冲区中的字符数据按照指定的字符编码转换为字节序列,然后将这些字节数据写入到目标文件中。
使用场景
- 实时数据写入:当需要确保某些重要数据及时写入文件,而不是等待缓冲区填满时,可以调用
flush
方法。例如,在记录日志的场景中,每记录一条重要日志信息后,就调用flush
方法,保证日志信息能立即写入文件,避免因程序崩溃或异常导致数据丢失。 - 多线程写入:在多线程环境下,多个线程可能同时向同一个文件写入数据。为了保证数据的完整性和顺序性,每个线程在完成一部分数据写入后,可以调用
flush
方法,确保数据及时写入文件,避免不同线程的数据相互干扰。
代码示例
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterFlushExample {
public static void main(String[] args) {
try (FileWriter fw = new FileWriter("test.txt")) {
// 写入一部分数据
fw.write("这是第一段写入的数据。");
// 调用 flush 方法,将缓冲区数据写入文件
fw.flush();
System.out.println("第一段数据已写入文件。");
// 继续写入另一部分数据
fw.write("这是第二段写入的数据。");
// 再次调用 flush 方法
fw.flush();
System.out.println("第二段数据已写入文件。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述代码中,每次调用 write
方法写入数据后,都调用了 flush
方法,确保数据及时写入文件。
注意事项
- 与
close
方法的关系:当调用FileWriter
的close
方法时,会自动调用flush
方法,将缓冲区中的数据刷新到文件中,然后关闭流。但在某些情况下,可能需要在关闭流之前多次刷新缓冲区,此时就需要手动调用flush
方法。 - 性能影响:虽然
flush
方法可以确保数据及时写入文件,但频繁调用flush
方法会增加与文件系统的交互次数,从而降低写入性能。因此,需要根据实际需求合理使用flush
方法,在保证数据安全的前提下,尽量减少不必要的刷新操作。