跳到主要内容Java I/O 操作详解 | 极客日志Javajava
Java I/O 操作详解
Java I/O 操作详解涵盖 File 类使用、字节流与字符流区别、转换流与缓冲流应用及序列化机制。通过代码示例演示文件创建、遍历、删除、复制等操作,对比不同流在文本与二进制数据处理的适用场景。重点讲解 InputStream、OutputStream、Reader、Writer 体系,强调资源关闭重要性及编码问题处理。
1. 引言
I/O 操作主要是指使用 Java 程序完成输入 (Input)、输出 (Output) 操作。输入是指将文件内容以数据流的形式读入内存,输出是指通过 Java 程序将内容中的数据写入文件,输入输出操作在实际开发中比较广泛。
- IO:输入/输出 (Input/Output)
- 流:是一种抽象概念,是对数据传输的总称。也就是说数据在设备间的传输称为流,流的本质是数据传输
- IO 流就是用来处理设备间数据传输问题的。常见的应用:文件复制;文件上传;文件下载
IO 流的分类:
(1)按照数据的流向
(2)按照数据类型来分:
- 字节流
- 字符流
IO 流的使用场景
- 如果操作的是纯文本文件,优先使用字符流
- 如果操作的是图片、视频、音频等二进制文件,优先使用字节流
- 如果不确定文件类型,优先使用字节流,字节流是万能的流
2. File 类
java.io 包中的 File 类 是唯一一个可以代表磁盘文件的对象,它定义了一些用于操作文件的方法。通过调用 File 类 提供的各种方法,可以创建、删除或者重命名文件,判断硬盘上某个文件是否存在,查询文件最后修改时间,等等。本节将针对 File 类 进行详细讲解。
2.1 创建 File 对象
File 类 提供了多个构造方法用于创建 File 对象。File 类 的常用构造方法如下所示:
| 方法声明 | 功能描述 |
|---|
| File(String pathname) | 通过指定的一个字符串类型的文件路径创建一个 File 对象 |
| File(String parent, String child) | 根据指定的一个字符串类型的父路径和一个字符串类型的子路径(包括文件名称)创建一个 File 对象 |
| File(File parent, String child) | 根据指定的一个 File 类的父路径和一个字符串类型的子路径(包括文件名称)创建一个 File 对象 |
所有的构造方法都需要传入文件路径,那么我们应该如何去用呢?
- 如果程序只处理一个目录和文件,并且知道该目录或文件的路径,就建议使用构造 1
- 如果程序处理的是一个公共目录中的若干子目录或文件,就建议使用构造 2 和构造 3
案例:
public static void main(String[] args) {
();
();
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
File
f1
=
new
File
"D:\\file\\a.txt"
File
f2
=
new
File
"src\\Hello.txt"
**注:**目录符号(\)用 \ 表示,因为 \ 在 Java 中是特殊字符,具有转义作用,因此用 \ 表示,此外我们也可以用 / 来作目录符号。
2.2 File 类的常用方法
| 方法声明 | 功能描述 |
|---|
| boolean exists() | 判定 File 对象对应的文件或目录是否存在 |
| boolean delete() | 删除 File 对象对应的文件或目录 |
| boolean createNewFile() | 当 File 对象对应文件不存在时则创建新文件,并且将新建的 File 对象指向新文件 |
| String getName() | 返回 File 对象表示的文件或目录的名称 |
| String getPath() | 返回 File 对象表示的文件或目录的路径 |
| String getAbsolutePath() | 返回 File 对象表示的文件或目录的绝对路径 |
| String getParentFile() | 返回 File 对象对应目录的父目录(注:返回的目录不包含最后一级子目录) |
| boolean canRead() | 判定 File 对象对应的文件或目录是否可读 |
| boolean canWrite() | 判定 File 对象对应的文件或目录是否可写 |
| boolean isFile() | 判断 File 对象对应的是否是文件 |
| boolean isDirectory() | 判断 File 对象对应的是否是目录 |
| boolean isAbsolute() | 判断 File 对象对应的是否是绝对路径 |
| long lastModified() | 返回 1970 年 1 月 1 日 0 时 0 分 0 秒到文件最后修改时间的毫秒值 |
| long length() | 返回文件内容的长度(注:单位为字节) |
| String[] list() | 列出指定目录的全部内容(包括子目录和文件),只列出名称 |
| File[] listFiles() | 返回一个包含 File 对象所有子文件和子目录的 File 数组 |
public static void main(String[] args) {
File file = new File("src/test.txt");
System.out.println("文件是否存在:" + file.exists());
System.out.println("文件名:" + file.getName());
}
- 补充学习:createTempFile() 方法 和 deleteOnExit() 方法
在一些特定情况下,程序需要读写一些临时文件,为此,File 类提供了 createTempFile() 方法 和 deleteOnExit() 方法,用于操作临时文件。createTempFile() 方法用于创建一个临时文件,deleteOnExit() 方法在 Java 虚拟机退出时自动删除临时文件。
public static void main(String[] args) throws IOException {
File file = File.createTempFile("itcast-", ".txt");
file.deleteOnExit();
System.out.println("file 是否为文件:" + file.isFile());
System.out.println("file 的相对路径:" + file.getPath());
}
2.3 遍历目录下的文件
File 类中提供了 list() 方法,可以获取目录下所有文件和目录的名称。获取目录下所有文件和目录名称后,可以通过这些名称遍历目录下的文件,按照调用方法的不同,对目录下的文件遍历可分为以下 3 种方式。
- 遍历指定目录下的所有文件
- 遍历指定目录下指定扩展名的文件
- 遍历包括子目录中的文件在内的所有文件
File 类的 list() 方法可以遍历指定目录下的所有文件。下面通过一个案例演示如何使用 list() 方法遍历目录下的所有文件,如下:
public static void main(String[] args) {
File file = new File("src/IO");
if(file.isDirectory()) {
String[] names = file.list();
for(String name:names) {
System.out.println(name);
}
}
}
上述代码实现了遍历一个目录下所有文件的功能,然而有时程序只需要获取指定类型的文件,如获取指定目录下所有扩展名为'.java'的文件。
针对这种需求,File 类提供了一个重载的 list() 方法,该方法接收一个 FilenameFilter 类型的参数。FilenameFilter 是一个接口,被称作文件过滤器,其中定义了抽象方法 accept() 用于依次对指定 File 的所有子目录或文件进行迭代。在调用 list() 方法时,需要实现 FilenameFilter,并在 accept() 方法中进行筛选,从而获得指定类型的文件。
下面通过一个案例演示如何遍历指定目录下所有扩展名为'.java'的文件,如下:
public static void main(String[] args) {
File file = new File("src/IO");
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
File currFile = new File(dir, name);
if(currFile.isFile() && name.endsWith(".java")){
return true;
} else return false;
}
};
if(file.exists()) {
String[] lists = file.list();
for(String name:lists) {
System.out.println(name);
}
}
}
前面的两个例子演示的都是遍历当前目录下的文件。有时候在一个目录下,除了文件,还有子目录,如果想获取所有子目录下的文件,list() 方法显然不能满足要求,这时可以使用 File 类提供的另一个方法—— listFiles()。
该方法返回一个File 对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则可以递归遍历子目录。下面通过一个案例演示包括子目录文件的所有文件的遍历,如下:
public static void main(String[] args) {
File file = new File("src");
fileDir(file);
}
public static void fileDir(File dir) {
File[] files = dir.listFiles();
for(File file :files)
{
if(file.isDirectory()){
fileDir(file);
}
System.out.println(file.getAbsolutePath());
}
}
2.4 删除文件及目录
在操作文件时,可能会遇到需要删除一个目录下某个文件或删除整个目录的操作,这时就可以调用 File 类中的 delete() 方法。
public static void main(String[] args) {
File file = new File("src/IO");
if(file.exists()){
System.out.println(file.delete());
}
}
因为文件删除失败了,File 类中的 delete() 方法只能删除一个指定的文件,假如 File 对象代表一个目录,而且这个目录下包含子目录或文件,则 File 类中的 delete() 方法 时不允许删除整个目录的。此时就需要采用递归的方法来全部删除
public static void main(String[] args) {
File file = new File("src/IO");
deleteDir(file);
System.out.println("删除成功!!");
}
public static void deleteDir(File dir) {
if(dir.exists()) {
File[] files = dir.listFiles();
for(File file :files)
{
if(file.isDirectory()){
deleteDir(file);
} else file.delete();
}
dir.delete();
}
}
- 删除目录是从 Java 虚拟机直接删除而不放入到回收站,文件一旦被删除就无法恢复,因此在进行文件删除操作的时候需要格外小心!
3. 字节流
3.1 基本概念
在程序的开发中,经常需要处理设备之间的数据传输,而在计算机中,无论是文本,图片、音频还是视频,所有文件都是以二进制(字节)形式存在的。对于字节的输入输出,I/O 系统提供了一系列流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。
JDK 提供了两个抽象类—— InputStream 和 OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承 InputStream,所有的字节输出流都继承 OutputStream。
- InputStream:这个抽象类是表示字节输入流的所有类的超类
- OutputStream:这个抽象类是表示字节输出流的所有类的超类
- 子类名特点:子类名称都是以其父类名作为子类名的后缀
| 方法声明 | 功能描述 |
|---|
| int read() | 从输入流读取一字节(8 位), 把它转化位 0 - 255 的整数,并返回这个整数 |
| int read(byte[] b) | 从输入流读取若干字节,把它们保存到参数 b 指定的字节数组中,返回的整数表示读取的字节数 |
| int read(byte[] b, int off, int len) | 从输入流读取若干字节,把它们保存到参数 b 指定的字节数组中,off 指定字节数组保存数据的起始索引,len 表示读取的字节数 |
| void close() | 关闭输入流并且释放与其相关的所有系统资源 |
上表中的 3 个 read() 方法都是用来读数据的。其中:
- 第一个 read() 方法是从输入流中逐个读入字节;
- 而第二个和第三个 read() 方法则可以将若干字节以字节数组的形式一次性读入,从而提高读数据的效率。在进行 I/O 操作时,当前 I/O 流会占用一定的内存,由于系统资源非常宝贵,因此,在 I/O 操作结束后,应该调用 close() 方法关闭 I/O 流,从而释放当前 I/O 流 所占的系统资源。
| 方法声明 | 功能描述 |
|---|
| void write(int b) | 将指定的字节写入此文件输出流,一次写一个字节数据 |
| void write(byte[] b) | 将参数 b 指定的字节数组的所有字节写入到此文件输出流,一次写一个字节数组数据 |
| void write(byte[] b, int off, int len) | 将指定 byte 数组从偏移量 off(起始索引)开始的 len 字节写入此文件输出流 |
| void flush() | 刷新输出流并且强制写出所有缓冲的输出字节 |
| void close() | 关闭输出流并且释放与其关联的所有系统资源 |
**注:**上表前 3 个是重载的 write() 方法,都用于向输出流写入字节。
- 其中,第一个 write() 方法逐个写入字节;
- 后两个 write() 方法将若干字节以字节数组的形式一次性写人,从而提高写数据的效率。
- flush() 方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。
- close() 方法用来关闭 I/O 流并释放与当前 I/O 流相关的系统资源。
InputStream 和 OutputStream 这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能, InputStream 类 和 OutputStream 类提供了不同的子类,形成了体系结构。
3.2 字节流读文件
- InputStream 就是 JDK 提供的基本输入流,它是所有输入流的父类,FileInputStream 是 InputStream 的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。因为从文件读取数据是重复的操作,所以需要通过循环语句实现数据的持续读取。
下面通过一个案例实现字节流对文件数据的读取。在实现案例之前,先做以下操作:
- 首先在 Java 项目的根目录下创建文本文件 test.txt
- 在文件中输入内容**'itcast'** 并保存
- 然后使用字节输入流对象读取 test.txt 文本文件
public static void main(String[] args) throws IOException {
FileInputStream in = new FileInputStream("src/IO/test.txt");
int b = 0;
while(true) {
b = in.read();
if(b == -1){
break;
}
System.out.println(b + " ");
}
in.close();
}
由于计算机中的数据都是以字节的形式存在的。在 test.txt 文件中,字符 i、t、c、a、s、t 各占一字节,所以最终结果显示的就是文件 test.txt 中的 6 字节对应的十进制数**(即这 6 个字母的 ASCII 码值)**。
- 有时,在文件读取的过程中可能会发生错误。例如,由于文件不存在而导致无法读取。
- 或者用户没有读取权限等等。这些错误都由 Java 虚拟机自动封装成 IOException 异常并抛出。例如,当读取一个不存在的文件时,控制台会报告异常信息,
当读取一个不存在的文件时,程序就会有一个潜在的问题。如果文件读取过程中发生了 I/O 错误,InputStream 就无法正常关闭,系统资源也无法及时释放,这样会造成系统资源浪费。
对此,可以使用 try... finally 语句保证 InputStream 在任何情况下都能够正确关闭。修改上述代码,将读取文件的代码放入try 语句块中,将关闭输入流的代码放入finally 语句块中,具体代码如下:
public static void main(String[] args) throws Exception {
InputStream input = null;
try {
FileInputStream in = new FileInputStream("src/IO/test.txt");
int b = 0;
while (true) {
b = in.read();
if (b == -1) {
break;
}
System.out.print(b + " ");
}
} finally {
if (input != null) {
input.close();
}
}
}
3.3 字节流写文件
OutputStream 是 JDK 提供的基本输出流,与 InputStream 类似.
- OutputStream是所有输出流的父类。
- OutputStream 是一个抽象类,如果使用此类,则必须先通过子类实例化对象。
- OutputStream 类有多个子类,其中FileOutputStream 子类是操作文件的字节输出流,专门用于把数据写入文件。
public static void main(String[] args) throws Exception {
OutputStream out = new FileOutputStream("src/IO/example.txt");
String str = "Island1314";
byte[] b = str.getBytes();
for(int i = 0; i < b.length; i++){
out.write(b[i]);
}
out.close();
}
由上可知,使用 FileOutputStream 写数据时,程序自动创建了文件 example.txt,并将数据写入example.txt 文件。需要注意的是,如果通过 FileOutputStream 向一个已经存在的文件中写入数据,那么该文件中的数据会被覆盖。
若希望在已存在的文件内容之后追加新内容,我们应该怎么做:
- 可使用 FileOutputStream 的构造函数 public FileOutputStream(String name,boolean append)
- 创建文件输出流以指定的名称写入文件,并把 append 参数的值设置为 true。如果第二个参数为 true,则字节将写入文件的末尾而不是开头
public static void main(String[] args) throws Exception {
OutputStream out = new FileOutputStream("src/IO/example.txt",true);
String str = "\r\n201314";
byte[] b = str.getBytes();
for(int i = 0; i < b.length; i++){
out.write(b[i]);
}
out.close();
}
上面的 \r \n 又是什么意思呢 》 解释如下:
- windows:\r\n
- linux:\n
- mac:\r
需要注意的是:I/O 流 在进行数据读写操作时会出现异常。为了保持代码的简洁,在InputStream 读文件和OutputStream写文件的程序中都使用了throws 关键字将异常抛出。然而一旦遇到 I/O 异常,I/O 流 的 close() 方法 将无法得到执行,I/O 流 对象占用的系统资源将得不到释放。
因此,为了保证I/O 流 的 close() 方法 必须执行,通常将关闭 I/O 流 的操作写在 finally 代码块中。
3.4 字节流复制文件
在应用程序中,I/O 流通常都是成对出现的,即输入流和输出流一起使用。例如:文件的复制就需要通过输入流读取一个文件中的数据,再通过输出流将数据写入另一个文件。
- 首先在 src 项目的根目录下创建 source 目录和 target 目录,
- 然后在 source 目录中存放 a.png 文件,
- 最后将 source 目录下的 a.png 复制到 target 目录下并重新命名为 b.png。
public static void main(String[] args) throws Exception{
InputStream in = new FileInputStream("src/source/a.png");
OutputStream out = new FileOutputStream("src/target/b.png");
int len;
long begintime = System.currentTimeMillis();
while ((len = in.read())!= -1){
out.write(len);
}
long endtime = System.currentTimeMillis();
System.out.println("复制文件所消耗时间:" + (endtime - begintime) + "ms");
in.close();
out.close();
}
- 通过 while 循环将 a.png 的所有字节逐个进行复制。
- 每循环一次,就通过调用 FileInputStream 的 read() 方法读取一字节,
- 并通过调用 FileOutputStream 的 write() 方法将该字节写入指定文件,直到 len 的值为 -1,表示读到了文件末尾,结束循环,完成文件的复制。
- 程序运行结束后,会在命令行窗口打印复制文件所消耗的时间。
- 由上可知,程序复制文件共消耗了 6038ms。在复制文件时,由于计算机性能等各方面原因,会导致复制文件所消耗的时间不确定,因此每次运行程序的结果未必相同。
在程序运行结束后,打开 target 目录,发现 source 目录中的 a.png 文件被成功复制到 target 目录中
上述实现的文件复制过程是逐字节读写,需要频繁地操作文件,效率非常低
- 从北京运送烤鸭到上海,如果有一万只烤鸭,每次运送一只,就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批烤鸭装在车厢中,这样就可以成批地运送烤鸭,这时的车厢就相当于一个缓冲区
因此在通过流的方式复制文件时,为了提高效率,也可以定义一个字节数组作为缓冲区。
- 在复制文件时,可以一次性读取多个字节的数据,并保存在字节数组中,然后将字节数组中的数据一次性写入文件。
- 程序中的缓冲区就是一块内存,它主要用于暂时存放输入输出的数据,由于使用缓冲区减少了对文件的操作次数,所以可以提高数据的读写效率。
public static void main(String[] args) throws Exception{
InputStream in = new FileInputStream("src/source/a.png");
OutputStream out = new FileOutputStream("src/target/b.png");
byte[] buff = new byte[1024];
int len;
long begintime = System.currentTimeMillis();
while ((len = in.read(buff))!= -1){
out.write(buff, 0, len);
}
long endtime = System.currentTimeMillis();
System.out.println("复制文件所消耗时间:" + (endtime - begintime) + "ms");
in.close();
out.close();
}
可以看出复制文件消耗时间明显减少,说明使用缓冲区读写文件可以有效地提高程序读写效率
4. 字符流
4.1 字符流定义及基本用法
前面讲解的内容都是通过字节流直接对文件进行读写。如果读写的文件内容是字符,考虑到使用字节流读写字符可能存在传输效率以及数据编码问题、此时建议使用字符流。
同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader 类 和 Writer 类。
- Reader 类是字符输入流,用于从某个源设备读取字符;
- Writer 类是字符输出流。用于向某个目标设备写入字符。
- 在 JDK 中,Reader 类和 Writer 类提供了一系列与读写数据相关的方法。
| 方法声明 | 功能描述 |
|---|
| int read() | 以字符为单位读数据 |
| int read(char[] cbuf) | 将数据读入 char 类型的数组,并返回数组长度 |
| int read(char[] cbuf, int off, int len) | 将数据读入 char 类型的数组的指定区间,并返回数组长度 |
| void close() | 关闭数据流 |
| long transferTo(Writer out) | 将数据之间读入字符输出流 |
| 方法声明 | 功能描述 |
|---|
| void write(int c) | 以字符为单位写数据 |
| void write(char[] cbuf) | 将 char 类型的数组中的数据写出 |
| void write(char[] cbuf, int off, int len) | 将 char 类型的数组中指定区间的数据写出 |
| void write(String str) | 将 String 类型的数据写出 |
| void write(String str, , int off, int len) | 将 String 类型中指定区间的数据写出 |
| void flush() | 强制将缓冲区的数据同步到输出流 (刷新流),之后还可以继续写数据 |
| void close() | 关闭数据流 |
Reader 类 和 Writer 类作为字符流的顶级父类,也有许多子类,形成了体系结构,分别如下:
在上面我们可以看到字符流的继承关系和字节流的继承关系类似,Reader 类 和 Writer 类的很多子类都是成对出现。例如:
- FileReader 和 FileWriter 用于读写文件
- BufferedReader 和 BufferedWriter 是具有缓冲功能的字符流,使用他们可以提高读写效率
4.2 字符流读文件
在程序开发中,经常需要对文本文件的内容进行读取。如果想从文件中直接读取字符,便可以使用字符输入流 FileReader,通过它可以从关联的文件中读取一个或一组字符。
下面通过一个案例演示如何使用 FileReader 读取文件中的字符:
- 首先新建文本文件 test.txt 并在其中输入字符 'itcast'
- 然后创建字符输入流 FileReader 对象以读取 reader.txt 文件中的内容
public static void main(String[] args) throws Exception {
FileReader reader = new FileReader("src/IO/test.txt");
int ch;
while((ch = reader.read()) != -1){
System.out.print((char) ch);
}
reader.close();
}
注:FileReader 对象的 read() 方法返回的是 int 类型的值,如果想获得字符,就必须进行强制类型转换。
4.3 字符流写文件
上面讲解了字符流对文本文件内容的读取。现在讲解通过字符流向文本文件中写入内容,此时需要使用 FileWriter 类,该类可以一次向文件中写人一个或一组字符。
下面通过一个案例演示如何使用 FileWriter 将字符写入文件
public static void main(String[] args) throws Exception {
FileWriter writer = new FileWriter("src/IO/example.txt");
String str = "IsLand1314";
writer.write(str);
writer.write("\r\n");
writer.close();
}
FileWriter 同 FileOutputStream 一样,如果指定的文件不存在,就会先创建文件,再写入数据;如果文件存在,则原文件内容会被覆盖。如果想在文件末尾追加数据,同样需要调用重载的构造方法,将上面第三行代码修改为:
FileWriter writer = new FileWriter("src/IO/example.txt", true);
4.4 数据编码解码问题
由于字节流操作中文不是特别的方便,所以 Java 就提供字符流
- 用字节流复制文本文件时,文本文件也会有中文,但是没有问题,原因是最终底层操作会自动进行字节拼接成中文,如何识别是中文的呢?
- 汉字在存储的时候,无论选择哪种编码存储,第一个字节都是负数
| 函数声明 | 功能描述 |
|---|
| byte[] getBytes() | 使用平台的默认字符集将该 String 编码为一系列字节 |
| byte[] getBytes(String charsetName) | 使用指定的字符集将该 String 编码为一系列字节 |
| String(byte[] bytes) | 使用平台的默认字符集解码指定的字节数组来创建字符串 |
| String(byte[] bytes, String charsetName) | 通过指定的字符集解码指定的字节数组来创建字符串 |
public static void main(String[] args) throws UnsupportedEncodingException {
String s = "中国";
byte[] bys = s.getBytes("GBK");
System.out.println(Arrays.toString(bys));
String ss = new String(bys,"GBK");
System.out.println(ss);
}
5. 转换流
前面提到 I/O 流分为字节流和字符流,字节流和字符流之间可以进行转换。JDK 提供了两个类用于将字节流转换为字符流,分别是 InputStreamReader 和 OutputStreamReader。
InputStreamReader:是从字节流到字符流的桥梁,父类是 Reader
- 它读取字节,并使用指定的编码将其解码为字符
- 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集
OutputStreamReader:是从字符流到字节流的桥梁,父类是 Writer
- 是从字符流到字节流的桥梁,使用指定的编码将写入的字符编码为字节
- 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集
通过 InputStreamReader 和 OutputStreamReader 将字节流转换为字符流,可以提高文件的读写效率
| 方法声明 | 功能描述 |
|---|
| InputStreamReader(InputStream in) | 使用默认字符编码创建 InputStreamReader 对象 |
| InputStreamReader(InputStream in,String chatset) | 使用指定的字符编码创建 InputStreamReader 对象 |
| OutputStreamWriter(OutputStream out) | 使用默认字符编码创建 OutputStreamWriter 对象 |
| OutputStreamWriter(OutputStream out,String charset) | 使用指定的字符编码创建 OutputStreamWriter 对象 |
- 首先。在 src 项目的根目录下新建文本文件 test.txt
- 并在文件中输入**'Island1314'**
- 其次,在 sre 文件夹中创建一个类,在类中创建字节输入流 FileInputStream 对象读取 src.txt 文件中的内容,并将字节输入流转换成字符输入流。
- 再次,创建一个字节输出流对象,并指定目标文件为des.txt
- 最后,将字节输出流转换成字符输出流将字符输出到文件中
public static void main(String[] args) throws Exception {
FileInputStream in = new FileInputStream("src/IO/test.txt");
InputStreamReader isr = new InputStreamReader(in);
FileOutputStream out = new FileOutputStream("src/IO/des.txt");
OutputStreamWriter osw = new OutputStreamWriter(out);
int ch;
while((ch = isr.read()) != -1)
{
osw.write(ch);
}
isr.close();
osw.close();
}
6. 缓冲流
6.1 字节缓冲流
- **BufferedOutputStream:**该类实现缓冲输出流。通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用
- **BufferedInputStream:**创建 BufferedInputStream 将创建一个内部缓冲区数组。当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节
- 字节流缓冲区的核心优势就是一次读取多个字节数据,从而减少硬盘操作子树
| 方法声明 | 功能描述 |
|---|
| BufferedOutputStream(OutputStream out) | 创建字节缓冲输出流对象 |
| BufferedInputStream(InputStream in) | 创建字节缓冲输入流对象 |
public static void main(String[] args) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("src/IO/test.txt"));
bos.write("hello\r\n".getBytes());
bos.write("world\r\n".getBytes());
bos.close();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src/IO/test.txt"));
byte[] bys = new byte[1024];
int len;
while ((len=bis.read(bys))!=-1) {
System.out.print(new String(bys,0,len));
}
bis.close();
}
6.2 字符缓冲流
- **BufferedWriter:**将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小。默认值足够大,可用于大多数用途
- **BufferedReader:**从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以使用默认大小。默认值足够大,可用于大多数用途
| 方法声明 | 功能描述 |
|---|
| BufferedWriter(Writer out) | 创建字符缓冲输出流对象 |
| BufferedReader(Reader in) | 创建字符缓冲输入流对象 |
public static void main(String[] args) throws IOException {
BufferedWriter bw = new BufferedWriter(new FileWriter("src/IO/test.txt"));
for (int i = 0; i < 10; i++) {
bw.write("hello" + i);
bw.newLine();
bw.flush();
}
bw.close();
BufferedReader br = new BufferedReader(new FileReader("src/IO/test.txt"));
String line;
while ((line=br.readLine())!=null) {
System.out.println(line);
}
br.close();
}
7. 序列化反序列化
程序在运行过程中,数据都保存在 Java 对象(内存) 中,但很多情况下还需要将一些数据永久保存到磁盘上。为此,Java 提供了对象序列化机制,可以将对象中的数据保存到磁盘。
对象序列化(serialize)是指将一个 Java 对象转换成一个 I/O 流的字节序列的过程。
- 对象序列化机制可以使内存中的 Java 对象转换成与平台无关的二进制流,
- 通过编写程序,既可以将这种二进制流持久地保存在磁盘上,
- 又可以通过网络将其传输到另一个网络节点。
其他程序在获得了二进制流后,还可以将二进制流恢复成原来的 Java 对象,这种将 I/O 流 中的字节序列恢复为 Java 对象的过程称为 反序列化(deserialize)。
如果想让某个对象支持序列化机制,那么这个对象所属的类必须是可序列化的。在 Java 中,可序列化的类必须实现 Serializable 或 Externalizable 两个接口之一。
Serializable 接口或 Externalizable 接口实现序列化机制的主要区别
| 方法声明 | 功能描述 |
|---|
| Serializable 接口 | Externalizable 接口 |
| 系统自动存储必要的信息 | 由程序员自己决定要存储的信息 |
| Java 内部支持,易于实现,只需实现该接口即可,不需要其他代码支持 | 该接口只提供了两个抽象方法,实现该接口时必须重写这两个抽象方法 |
| 性能较差 | 性能较好 |
与实现 Serializable 接口相比,虽然实现 Externalizable 接口可以带来性能上的一定提升,但由于后者需要实现两个抽象方法,所以将导致编程的复杂度提高。
- 在实际开发时,大部分情况下使用Serializable 接口的方式实现对象序列化。
使用Serializable 接口实现对象序列化非常简单,只需要让目标类实现 Serializable 接口即可,无须实现任何方法。例如,自定义 Person 类,让 Person 类实现 Serializable 接口,如下:
public class Person implements Serializable{
private static final long serialVersionUID = 1L;
private int id;
private String name;
private int age;
}
在上述代码中,Person 类实现了 Serializable 接口,并指定了 serialVersionUID 变量值,该属性的值的作用是标识 Java 类的序列化版本。如果不显式定义 serialVersionUID 变量值,那么serialVersionUID 属性的值将由 Java 虚拟机 根据类的相关信息计算得出
serialVersionUID适用于 Java 的对象序列化机制。简单来说,Java 的对象序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,Java 虚拟机会把字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较。如果相同,就认为是一致的,可以进行反序列化;否则就会抛出序列化版本不一致的异常。
- 因此,为了在反序列化时确保序列化版本的兼容性,最好在每一个要序列化的类中加入 private static final long serialVersionUID 的变量值,具体数值可自定义,默认是 1L。
- 如果不显式指定 serialVersionUID 的值,系统可以根据类名、接口名、成员方法及属性等生成一个 64 位的哈希值,将这个哈希值作为 serialVersionUID 的值。
- 定义了 serialVersionUID 的值,如果 serialVersionUID 所属类的某个对象被序列化,即使该对象对应的类被修改了,该对象也依然可以被正确地反序列化。
8. 小结
包括 File 类,包括创建 File 对象、File 类的常用方法、遍历目录下的文件和删除文件及目录;字节流,包括字节流的概念、字节流读文件、字节流写文件和文件的复制;字符流,包括字符流的定义及基本用法、字符流读文件和字符流写文件;转换流的使用;序列化和反序列化。通过本章的学习,读者应该了解 I/O 流,并且熟练掌握了 I/O 流的相关知识。
字节流是 IO 中最基础的形式,它以字节(8 位)为单位进行数据传输,适用于处理所有类型的数据,包括文本、图片、音频和视频等二进制数据。在 Java 中,字节流的基类是InputStream和OutputStream。字节流在操作时通常不会使用缓冲区,直接与文件本身进行操作,这意味着每次调用read方法都可能伴随着一次磁盘 IO,因此效率相对较低。为了提高效率,可以使用如BufferedInputStream和BufferedOutputStream这样的缓冲字节流。
字符流则是以 Unicode 码元(16 位)为单位进行数据传输,主要用于处理文本数据。字符流在处理数据时会涉及字符编码的转换,如 UTF-8 或 GBK 等。在 Java 中,字符流的基类是Reader和Writer。字符流在输出前会完成 Unicode 码元序列到相应编码方式的字节序列的转换,并使用内存缓冲区来存放转换后的字节序列,等待都转换完毕再一同写入磁盘文件中。
- 字节流操作的基本单元为字节,而字符流操作的基本单元为 Unicode 码元。
- 字节流不使用缓冲区,字符流使用缓冲区。
- 字节流可以处理任何类型的数据,字符流主要处理文本数据。
- 字节流与文件直接操作,字符流在操作时使用缓冲区。