📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
目录
1. 引言 🚀
2. File 类 📕
2.1 创建 File 对象
2.2 File 类的常用方法
2.3 遍历目录下的文件
2.4 删除文件及目录
3. 字节流 💦
3.1 基本概念
3.2 字节流读文件
3.3 字节流写文件
3.4 字节流复制文件
4. 字符流 💧
4.1 字符流定义及基本用法
4.2 字符流读文件
4.3 字符流写文件
4.4 数据编码解码问题
5. 转换流 🖊
6. 缓冲流 🔍
6.1 字节缓冲流
6.2 字符缓冲流
7. 序列化反序列化 🔖
8. 小结 📖
1. 引言 🚀
💥I/O 操作主要是指 使用 Java 程序完成输入(Input)、输出(Output) 操作。输入是指将文件内容以数据流的形式读入内存,输出是指通过 Java 程序将内容中的数据写入文件,输入输出操作在实际开发中比较广泛。
- IO:输入/输出(Input/Output)
- 流:是一种抽象概念,是对数据传输的总称.也就是说数据在设备间的传输称为流,流的本质是数据传输
- IO流就是用来处理设备间数据传输问题的.常见的应用: 文件复制; 文件上传; 文件下载
IO流的分类:
(1)按照数据的流向
- 输入流:读数据
- 输出流:写数据
(2)按照数据类型来分:
- 字节流
- 字节输入流
- 字节输出流
- 字符流
- 字符输入流
- 字符输出流
IO流的使用场景
- 如果操作的是纯文本文件,优先使用字符流
- 如果操作的是图片、视频、音频等二进制文件,优先使用字节流
- 如果不确定文件类型,优先使用字节流,字节流是万能的流
2. File 类 📕
java.io 包中的 File 类 是唯一一个可以代表磁盘文件的对象,它定义了一些用于操作文件的方法。通过调用 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) {
// 方法一:通过绝对路径创建
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(); // java 虚拟机退出时 自动删除文件 file
System.out.println("file 是否为文件:" + file.isFile());
System.out.println("file 的相对路径:" + file.getPath());
}
2.3 遍历目录下的文件
File 类中提供了 list()方法,可以获取目录下所有文件和目录的名称。获取目录下所有文件和目录名称后,可以通过这些名称遍历目录下的文件,按照调用方法的不同,对目录下的文件遍历可分为以下3种方式。
- 遍历指定目录下的所有文件
- 遍历指定目录下指定扩展名的文件
- 遍历包括子目录中的文件在内的所有文件
下面分别对这3种遍历方式进行详细讲解。
(1)遍历指定目录下的所有文件
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); //输出文件名
}
}
}
(2)遍历指定目录下指定扩展名的文件
🍎 上述代码实现了遍历一个目录下所有文件的功能,然而有时程序只需要获取指定类型的文件,如获取指定目录下所有扩展名为“.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() {
// 实现 accept 方法
@Override
public boolean accept(File dir, String name) {
File currFile = new File(dir, name);
// 如果文件以 .java 结尾返回true
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); //输出文件名
}
}
}
(3)遍历包括子目录下的文件在内的所有文件
🍉 前面的两个例子演示的都是遍历当前目录下的文件。有时候在一个目录下,除了文件,还有子目录,如果想获取所有子目录下的文件,list()方法显然不能满足要求,这时可以使用File 类提供的另一个方法—— listFiles()。
🍈 该方法返回一个File对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则可以递归遍历子目录。下面通过一个案例演示包括子目录文件的所有文件的遍历,如下:
public static void main(String[] args) {
// 创建一个代表目录的 File 对象
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());
}
}
// 输出
false
为啥会输出 false 呢?
🌸因为文件删除失败了,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/0 系统提供了一系列流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。
🔥 JDK 提供了两个抽象类—— InputStream 和 OutputStream ,它们是字节流的顶级父类,所有的字节输入流都继承 InputStream ,所有的字节输出流都继承 OutputStream 。
- 字节流抽象基类
- InputStream:这个抽象类是表示字节输入流的所有类的超类
- OutputStream:这个抽象类是表示字节输出流的所有类的超类
- 子类名特点:子类名称都是以其父类名作为子类名的后缀
- 字节输出流
- FileOutputStream(String name):创建文件输出流以指定的名称写入文件
🌸注: I/O 流的输入输出是相对于 程序而言的
- InputStream 类常用方法
方法声明 | 功能描述 |
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/0操作结束后,应该调用close()方法关闭 I/O 流,从而释放当前 I/O 流 所占的系统资源。
- OutputStream 类常用方法
方法声明 | 功能描述 |
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()方法用来关闭1/0流并释放与当前1/0流相关的系统资源。
InputStream 和 OutputStream 这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能, 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; // 定义 int 类型的变量 b,用于 记住每次读取的 1 字节
while(true) {
b = in.read(); // 变量 b 记住读取的每一字节
if(b == -1){ // 如果读取的字节 位 -1,则跳出循环
break;
}
System.out.println(b + " "); // 否则输出b
}
in.close();
}
// 输出
105 116 99 97 115 116
由于计算机中的数据都是以字节的形式存在的。在test.txt文件中,字符i、 t、c、a、s、t 各占一字节,所以最终结果显示的就是文件test.txt中的6字节对应的十进制数(即这6个字母的ASCII码值)。
🔥 注意:
- 有时,在文件读取的过程中可能会发生错误。例如,由于文件不存在而导致无法读取。
- 或者用户没有读取权限等等。这些错误都由Java虚拟机自动封装成 IOException 异常并抛出。例如,当读取一个不存在的文件时,控制台会报告异常信息,
当读取一个不存在的文件时,程序就会有一个潜在的问题。如果文件读取过程中发生了 I/O 错误,InputStream 就无法正常关闭,系统资源也无法及时释放,这样会造成系统资源浪费。
对此,可以使用 try…· finally 语句保证 InputStream 在任何情况下都能够正确关闭。修改上述代码,将读取文件的代码放入try语句块中,将关闭输入流的代码放入finaly语句块中,具体代码如下:
public static void main(String[] args) throws Exception {
InputStream input = null;
try {
// 创建一个文件字节输入流
FileInputStream in = new FileInputStream("src/IO/test.txt");
int b = 0; // 定义 int 类型的变量 b,用于 记住每次读取的 1 字节
while (true) {
b = in.read(); // 变量 b 记住读取的每一字节
if (b == -1) { // 如果读取的字节 位 -1,则跳出循环
break;
}
System.out.print(b + " "); // 否则输出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();
}
// 在 example.txt 查看
Island1314
201314
// 解释:程序通过字节输出流对象out向文件example.txt写入后,并没有将文件原来的数据清空,而是将新写入的数据追加到了文件的末尾。
上面的 \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{
// 创建一个文件输入流,用于读取 sorce 目录的 a.png 文件
InputStream in = new FileInputStream("src/source/a.png");
// 创建一个文件输出流,用于将读取数据写入到 target 目录的 b.png 文件
OutputStream out = new FileOutputStream("src/target/b.png");
int len; //用于记住每次读取的 1 字节
// 获取复制文件前的系统时间
long begintime = System.currentTimeMillis();
while ((len = in.read())!= -1){ // 读取 1 字节并且判断是否读到文件末尾
out.write(len); // 将读取的 1 字节写入文件
}
// 获取文件复制结束后的时间
long endtime = System.currentTimeMillis();
System.out.println("复制文件所消耗时间:" + (endtime - begintime) + "ms");
in.close();
out.close();
}
// 输出:
复制文件所消耗时间:6038ms
上述代码实现了文件的复制:
- 通过while循环将a.png的所有字节逐个进行复制。
- 每循环一次,就通过调用FileInputStream的read()方法读取一字节,
- 并通过调用FileOutputStream 的write()方法将该字节写入指定文件,直到 len 的值为 -1,表示读到了文件末尾,结束循环,完成文件的复制。
- 程序运行结束后,会在命令行窗口打印复制文件所消耗的时间。
- 由上可知,程序复制文件共消耗了6038ms。在复制文件时,由于计算机性能等各方面原因,会导致复制文件所消耗的时间不确定,因此每次运行程序的结果未必相同。
在程序运行结束后,打开target目录,发现source目录中的 a.png 文件被成功复制到 target目录中
注意事项:
上述实现的文件复制过程是逐字节读写,需要频繁地操作文件,效率非常低
打个比方:
- 从北京运送烤鸭到上海,如果有一万只烤鸭,每次运送一只,就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批烤鸭装在车厢中,这样就可以成批地运送烤鸭,这时的车厢就相当于一个缓冲区
因此在通过流的方式复制文件时,为了提高效率,也可以定义一个字节数组作为缓冲区。
- 在复制文件时,可以一次性读取多个字节的数据,并保存在字节数组中,然后将字节数组中的数据一次性写入文件。
- 程序中的缓冲区就是一块内存,它主要用于暂时存放输入输出的数据,由于使用缓冲区减少了对文件的操作次数,所以可以提高数据的读写效率。
利用缓冲区复制文件,修改代码如下:
public static void main(String[] args) throws Exception{
// 创建一个文件输入流,用于读取 sorce 目录的 a.png 文件
InputStream in = new FileInputStream("src/source/a.png");
// 创建一个文件输出流,用于将读取数据写入到 target 目录的 b.png 文件
OutputStream out = new FileOutputStream("src/target/b.png");
// 以下是用 缓冲区 读写文件
byte[] buff = new byte[1024]; // 定义一个字节数组作缓冲区
int len; //用于记住每次读取的 1 字节
// 获取复制文件前的系统时间
long begintime = System.currentTimeMillis();
while ((len = in.read(buff))!= -1){ // 读取 1 字节并且判断是否读到文件末尾
out.write(buff, 0, len); // 将读取的 1 字节写入文件
}
// 获取文件复制结束后的时间
long endtime = System.currentTimeMillis();
System.out.println("复制文件所消耗时间:" + (endtime - begintime) + "ms");
in.close();
out.close();
}
// 输出:
复制文件所消耗时间:8ms
可以看出复制文件消耗时间明显减少,说明使用缓冲区读写文件可以有效地提高程序读写效率
4. 字符流 💧
4.1 字符流定义及基本用法
🔥 前面讲解的内容都是通过字节流直接对文件进行读写。如果读写的文件内容是字符,考虑到使用字节流读写字符可能存在传输效率以及数据编码问题、此时建议使用字符流。
同字节流一样,字符流也有两个抽象的顶级父类,分别是 Reader类 和 Writer类。
- Reader 类是字符输入流,用于从某个源设备读取字符;
- Writer类是字符输出流。用于向某个目标设备写入字符。
- 在JDK中,Reader 类和Writer 类提供了一系列与读写数据相关的方法。
注:字符流 = 字节流 + 编码表
Reader 类的常用方法
方法声明 | 功能描述 |
int read() | 以字符为单位读数据 |
int read(char[] cbuf) | 将数据读入 char 类型的数组,并返回数组长度 |
int read(char[] cbuf, int off, int len) | 将数据读入 char 类型的数组的指定区间,并返回数组长度 |
void close() | 关闭数据流 |
long transferTo(Writer out) | 将数据之间读入字符输出流 |
Writer 类的常用方法
方法声明 | 功能描述 |
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 体系结构图:
🌈 在上面我们可以看到字符流的继承关系和字节流的继承关系类似,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 对象,用来读取文件字符
FileReader reader = new FileReader("src/IO/test.txt");
int ch; // 用于记录读取的字符
while((ch = reader.read()) != -1){ // 循环判断是否读到文件末尾
System.out.print((char) ch); // 不是文件末尾就打印字符
}
reader.close(); // 关闭字符输入流,释放资源
}
// 输出
itcast
注:FileReader对象的 read() 方法返回的是 int 类型的值,如果想获得字符,就必须进行强制类型转换。
4.3 字符流写文件
🍋🟩上面讲解了字符流对文本文件内容的读取。现在讲解通过字符流向文本文件中写入内容,此时需要使用FileWriter类,该类可以一次向文件中写人一个或一组字符。
下面通过一个案例演示如何使用 FileWriter 将字符写入文件
public static void main(String[] args) throws Exception {
// 创建一个 FileWriter 对象,用于向文件写入数据
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(); //[-28, -72, -83, -27, -101, -67]
//byte[] bys = s.getBytes("UTF-8"); //[-28, -72, -83, -27, -101, -67]
byte[] bys = s.getBytes("GBK"); //[-42, -48, -71, -6]
System.out.println(Arrays.toString(bys));
//String ss = new String(bys);
//String ss = new String(bys,"UTF-8");
String ss = new String(bys,"GBK");
System.out.println(ss);
}
5. 转换流 🖊
🍊 前面提到I/0流分为字节流和字符流,字节流和字符流之间可以进行转换。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 {
// 创建字节输入流 in ,并且指定源文件 test.txt
FileInputStream in = new FileInputStream("src/IO/test.txt");
// 将字节输入流 in 转化为 字符输入流 isr
InputStreamReader isr = new InputStreamReader(in);
// 创建字节输出流 out ,并且指定源文件 des.txt
FileOutputStream out = new FileOutputStream("src/IO/des.txt");
// 将字节输出流 out 转化为 字符输出流 osw
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(OutputStream out)
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(InputStream in)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src/IO/test.txt"));
//一次读取一个字节数据
// int by;
// while ((by=bis.read())!=-1) {
// System.out.print((char)by);
// }
//一次读取一个字节数组数据
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.write("\r\n");
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{
// 为该类指定 serialVersionUID 变量值
private static final long serialVersionUID = 1L;
// 声明变量
private int id;
private String name;
private int age;
//... 此处省略各属性的 gettter 和 setter 方法
}
💞 在上述代码中,Person类实现了 Serializable接口,并指定了 serialVersionUID变量值,该属性的值的作用是标识Java类的序列化版本。如果不显式定义 serialVersionUID变量值,那么serialVersionUID属性的值将由 Java 虚拟机 根据类的相关信息计算得出
补充知识:serialVersionUID
🍒 serialVersionUID适用于Java的对象序列化机制。简单来说,Java的对象序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,Java虚拟机会把字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较。如果相同,就认为是一致的,可以进行反序列化;否则就会抛出序列化版本不一致的异常。
- 因此,为了在反序列化时确保序列化版本的兼容性,最好在每一个要序列化的类中加入 private static final long serialVersionUID 的变量值,具体数值可自定义,默认是1L。
- 如果不显式指定 serialVersionUID 的值,系统可以根据类名、接口名、成员方法及属性等生成一个64位的哈希值,将这个哈希值作为serialVersionUID的值。
- 定义了serialVersionUID的值,如果serialVersionUID所属类的某个对象被序列化,即使该对象对应的类被修改了,该对象也依然可以被正确地反序列化。
8. 小结 📖
本章主要介绍了 I/O流 的相关知识。
💌 包括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码元。
-
字节流不使用缓冲区,字符流使用缓冲区。
-
字节流可以处理任何类型的数据,字符流主要处理文本数据。
-
字节流与文件直接操作,字符流在操作时使用缓冲区。
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!💞❤️🔥💞