【Java】I/O 流篇 —— 字节 I/O 流
目录
- I/O 流
- 概述
- I/O 流的分类
- I/O 流的体系
- 字节 I/O 流
- FileOutputStream 类
- 基本操作
- 写数据的 3 种方式
- 换行和续写
- 原理
- 基本工作流程
- 原理详细剖析
- FileInputStream 类
- 基本操作
- 循环读取
- 文件拷贝
- 小文件拷贝
- 大文件拷贝
I/O 流
概述
定义:I/O流(Input/Output Stream) 是计算机中数据传输的一种抽象概念,用于描述数据在程序和外部设备(如文件、网络、内存等)之间的流动方式。可以将它想象成一条“数据管道”,数据像水流一样在这条管道中连续传输。
作用:I/O 流用于读写文件中的数据(可以是读写文件,或网络中的数据)。
I/O 流中,以程序为参照物进行读写:
- 读(Read) → Input(输入)
- 当程序从外部(如文件、键盘、网络等)获取数据时,称为“读”操作。
- 例如:读取文件内容、接收用户键盘输入、获取网络数据等。
- 这属于输入(Input),因为数据是从外部流向程序的。
- 写(Write) → Output(输出)
- 当程序向外部(如文件、屏幕、打印机等)发送数据时,称为“写”操作。
- 例如:将结果保存到文件、在屏幕上显示信息、发送数据到网络等。
- 这属于输出(Output),因为数据是从程序流向外部。
I/O 流的分类
流的方向:输入流(读取)和输出流(写出)
操作文件类型:
- 字节流:能操作所有类型的文件
- 字符流:只能操作纯文本文件(用 Windows 自带的记事本打开能读懂的文件)
I/O 流的体系
这四个类为 I/O 流中的超类,但由于是抽象类,无法进行实例化,下面将学习这四个类的子类。
字节 I/O 流
FileOutputStream 类
操作本地文件的字节输出流,可以把程序中的数据写到本地文件中
基本操作
操作步骤:
-
创建字节输出流对象
FileOutputStream fos = new FileOutputStream("aaa.txt");
-
写数据
fos.write();
-
释放资源
fos.close();
代码示例:
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建字节输出流对象,可按需修改路径
FileOutputStream fos = new FileOutputStream("aaa.txt");
// 写数据
fos.write(97); // 文件中输出 a
// 释放资源
fos.close();
}
}
注意事项:
-
操作字节输出流需要抛出异常,可以抛出父类异常 IOException
-
创建字节输出流对象
-
参数可以是字符串表示的路径,也可以是 File 对象
-
如果文件不存在则创建一个新的文件,但要保证父路径是存在的
-
如果文件已经存在,则会清空文件原有的内容
-
-
写入数据
- write() 的参数是整数,但是输出到文件中的是该整数对应的 ASCII 字符
-
释放资源
- 每次使用完流之后都要释放资源,否则资源会被一直占用
写数据的 3 种方式
void write(int b)
一次写一个字节数据void write(byte[] b)
一次写一个字节数组数据void write(byte[] b int off,int len)
一次写一个字节数组的部分数据
代码示例:
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("aaa.txt");
// 一次写一个字节数据
fos.write(97); // a
// 一次写一个字节数组数据
byte[] bytes = {97,98,99,100,101};
fos.write(bytes); // abcde
// 一次写一个字节数组的部分数据
fos.write(bytes, 1, 3); // bcd
fos.close();
}
}
换行和续写
- 换行写:写出一个换行符即可
- Windows —— \r\n
- Linux —— \n
- Mac —— \r
代码示例:
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("aaa.txt");
// 写入第一行数据
String str1 = "hello";
byte[] bytes1 = str1.getBytes();
fos.write(bytes1);
// 写入换行符
String str2 = "\r\n";
byte[] bytes2 = str2.getBytes();
fos.write(bytes2);
// 写入第二行数据
String str3 = "world";
byte[] bytes3 = str3.getBytes();
fos.write(bytes3);
fos.close();
}
}
-
续写
FileOutputStream
有 2 个可以接收布尔类型参数的构造函数,该布尔参数用于指定是否以追加模式打开文件。当这个参数设为true
时,新写入的数据就会被添加到文件的末尾,实现续写功能。构造函数的具体形式如下:// 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处 public FileOutputStream(File file, boolean append) throws FileNotFoundException; // 创建一个向具有指定 name 的文件中写入数据的输出文件流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处 public FileOutputStream(String name, boolean append) throws FileNotFoundException;
代码示例:
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("aaa.txt",true);
// 写入前部分数据
String str1 = "hello";
byte[] bytes1 = str1.getBytes();
fos.write(bytes1);
// 写入后部分数据
String str2 = " world";
byte[] bytes2 = str2.getBytes();
fos.write(bytes2);
// 输出 "hello world"
fos.close();
}
}
原理
基本工作流程
FileOutputStream
的主要功能是将字节数据从程序写入到文件中,其基本工作流程如下:
- 创建流对象:通过构造函数创建
FileOutputStream
对象,该对象会关联一个文件,后续的写入操作将针对这个文件进行。 - 写入数据:调用
FileOutputStream
的write()
方法将字节数据写入到关联的文件中。 - 关闭流:调用
close()
方法关闭流,释放系统资源。
原理详细剖析
-
构造函数
FileOutputStream
提供了多个构造函数,用于创建不同方式关联文件的流对象,常见的构造函数如下:// 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果文件存在,则会覆盖原有内容 public FileOutputStream(File file) throws FileNotFoundException; // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处 public FileOutputStream(File file, boolean append) throws FileNotFoundException; // 创建一个向具有指定名称的文件中写入数据的输出文件流。如果文件存在,则会覆盖原有内容 public FileOutputStream(String name) throws FileNotFoundException; // 创建一个向具有指定 name 的文件中写入数据的输出文件流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处 public FileOutputStream(String name, boolean append) throws FileNotFoundException;
在创建
FileOutputStream
对象时,Java 会在操作系统层面打开一个文件描述符,该描述符用于标识和操作文件。 -
写入数据
FileOutputStream
提供了多种write()
方法用于写入数据,常见的方法如下:// 将指定的字节写入此文件输出流 public void write(int b) throws IOException; // 将 b.length 个字节从指定 byte 数组写入此文件输出流中 public void write(byte[] b) throws IOException; // 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流 public void write(byte[] b, int off, int len) throws IOException;
当调用
write()
方法时,Java 会将数据从用户空间(Java 程序所在的内存空间)复制到内核空间的缓冲区中。内核空间是操作系统管理的内存空间,用于与硬件设备进行交互。 -
关闭流
在使用完
FileOutputStream
后,必须调用close()
方法关闭流,以释放系统资源。关闭流时,操作系统会将缓冲区中的数据全部写入磁盘,并关闭文件描述符。// 关闭此文件输出流并释放与此流有关的所有系统资源 public void close() throws IOException;
FileInputStream 类
操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来
基本操作
操作步骤:
-
创建字节输入流对象
FileInputStream fis = new FileInputStream("aaa.txt");
-
读取数据
fis.read();
-
释放资源
fis.close();
代码示例:
import java.io.FileInputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建字节输入流对象
FileInputStream fis = new FileInputStream("aaa.txt");
// 读取数据
int b = fis.read(); // 文件中的数据为 a
System.out.println(b); // 输出 97
// 释放资源
fis.close();
}
}
注意事项:
-
创建字节输入流对象
- 如果文件不存在,程序直接报错
-
读取数据
-
一次只读一个字节,读出来的是数据对应的 ASCII 数字
-
读到文件末尾,read() 返回 -1
-
-
释放资源
- 每次使用完流之后都要释放资源,否则资源会被一直占用
循环读取
import java.io.FileInputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建字节输入流对象
FileInputStream fis = new FileInputStream("aaa.txt");
// 循环读取
int b;
while((b = fis.read()) != -1) {
System.out.println((char)b);
}
// 释放资源
fis.close();
}
}
注意事项:
循环体不能写成以下形式:
while(fis.read() != -1) {
System.out.println(fis.read());
}
while
循环条件 fis.read() != -1
中调用 fis.read()
方法会读取一个字节,当这个字节不是文件结束标志(即返回值不为 -1)时,循环继续执行。
而在循环体中的 System.out.println(fis.read());
又再次调用了 fis.read()
方法,这会导致每次循环读取了两个字节:一个用于判断循环条件,一个用于输出。
这就会出现跳过字节读取的情况,比如文件内容是 “abc”,按这个逻辑,循环条件读取了 “a” 后进入循环,循环体又读取 “b” 并输出,下一次循环条件读取 “c” 后进入循环,又试图读取下一个字节(但文件已结束),就可能出现意料之外的结果或错误。
文件拷贝
小文件拷贝
核心思想:边读边写
代码示例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建对象
FileInputStream fis = new FileInputStream("1.mp4");
FileOutputStream fos = new FileOutputStream("2.mp4");
// 循环读取
int b;
while((b = fis.read()) != -1) {
fos.write(b);
}
// 释放资源
fos.close();
fis.close();
}
}
注意事项:
- 创建对象时,文件的后缀名要一致
- 释放资源时,先使用的资源要最后释放
大文件拷贝
由于 read() 方法一次只读一个字节数据,如果遇到很大的文件,那么拷贝速度就会很慢,所以我们可以使用 read() 方法的重载形式来拷贝:
public int read(byte[] buffer)
一次读一个字节数组数据(每次读取会尽可能把数组装满)
注意事项:数组本身也占用空间,如果创建过大的长度,内存可能奔溃,所以创建数组的时候,长度最好选择 1024B 的整数倍,例如 1024 * 1024 * 5,也就是 5MB
代码示例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建对象
FileInputStream fis = new FileInputStream("aaa.txt"); // 数据为 abc
FileOutputStream fos = new FileOutputStream("bbb.txt")
// 读取数据
byte[] bytes = new byte[3];
int len = fis.read(bytes);
fos.write(bytes);
System.out.println("本次读取到" + len + "字节的数据"); // 3
String str = new String(bytes);
System.out.println("本次读取的内容为:" + str); // abc
// 释放资源
fos.close();
fis.close();
}
}
接下来我们看另一段代码,假设 aaa.txt 中的数据是 abcde,字节数组的长度为 2:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建对象
FileInputStream fis = new FileInputStream("aaa.txt");
// 读取数据
byte[] bytes = new byte[2];
int len1 = fis.read(bytes);
System.out.println("本次读取到" + len1 + "字节的数据");
String str1 = new String(bytes);
System.out.println("本次读取的内容为:" + str1);
int len2 = fis.read(bytes);
System.out.println("本次读取到" + len2 + "字节的数据");
String str2 = new String(bytes);
System.out.println("本次读取的内容为:" + str2);
int len3 = fis.read(bytes);
System.out.println("本次读取到" + len3 + "字节的数据");
String str3 = new String(bytes);
System.out.println("本次读取的内容为:" + str3);
int len4 = fis.read(bytes);
System.out.println("本次读取到" + len4 + "字节的数据");
String str4 = new String(bytes);
System.out.println("本次读取的内容为:" + str4);
// 释放资源
fos.close();
fis.close();
}
}
运行结果如下:
本次读取到2字节的数据
本次读取的内容为:ab
本次读取到2字节的数据
本次读取的内容为:cd
本次读取到1字节的数据
本次读取的内容为:ed
本次读取到-1字节的数据
本次读取的内容为:ed
问题:
- 为什么第三次读取到 1 字节的数据,却显示读取到的内容为 ed?
- 为什么第四次数据已经读取完了,却还是显示读取到的内容为 ed?
原因:
在第一次读取前,指针指向 a,字节数组内容为空
第一次读取,字节数组长度为 2,a 和 b 进入数组,所以指针向后移两位到 c
第二次读取,c 和 d 进入数组,把 a 和 b 覆盖,指针向后移两位到 e
第三次读取,由于只剩下 e,进入数组后只把 c 覆盖了,但是 d 还在数组里,所以读取到的内容为 ed
第四次读取,由于在上一次读取时,已经将数据读取完了,不会有新的数据把数组中的内容覆盖,所以输出的内容仍为 ed
我们可以通过使用字符串的 String(byte[] b int off,int len)
方法来解决残留的数据:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建对象
FileInputStream fis = new FileInputStream("aaa.txt");
FileOutputStream fos = new FileOutputStream("bbb.txt");
// 读取数据
byte[] bytes = new byte[2];
int len1 = fis.read(bytes);
System.out.println("本次读取到" + len1 + "字节的数据");
String str1 = new String(bytes,0,len1);
System.out.println("本次读取的内容为:" + str1);
int len2 = fis.read(bytes);
System.out.println("本次读取到" + len2 + "字节的数据");
String str2 = new String(bytes,0,len2);
System.out.println("本次读取的内容为:" + str2);
int len3 = fis.read(bytes);
System.out.println("本次读取到" + len3 + "字节的数据");
String str3 = new String(bytes,0,len3);
System.out.println("本次读取的内容为:" + str3);
// 释放资源
fos.close();
fis.close();
}
}
程序运行结果如下:
本次读取到2字节的数据
本次读取的内容为:ab
本次读取到2字节的数据
本次读取的内容为:cd
本次读取到1字节的数据
本次读取的内容为:e
那么同理我们可以得到通过使用write(byte[] b int off,int len)
方法来解决读取数据时的数据残留,以下是大文件拷贝的代码示例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 创建对象
FileInputStream fis = new FileInputStream("aaa.txt");
FileOutputStream fos = new FileOutputStream("bbb.txt");
// 读取数据
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
// 释放资源
fos.close();
fis.close();
}
}