【Java】I/O 操作详解

IsLand1314~ 2024-10-16 13:05:01 阅读 81

 📃个人主页: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

案例:

<code>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 对象 表示的文件或目录的相对路径

在 UNIX / Linux 等系统上,如果路径以 斜线 / 开始,则这个路径为绝对路径在 Widows 等系统上,如果路径以 盘符 开始,则这个路径为绝对路径

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 数组

演示:

<code>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文本文件

案例代码:

<code>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\nlinux:\nmac:\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 类的很多子类都是成对出现。例如:

FileReaderFileWriter 用于读写文件BufferedReaderBufferedWriter 是具有缓冲功能的字符流,使用他们可以提高读写效率

4.2 字符流读文件

🥬在程序开发中,经常需要对文本文件的内容进行读取。如果想从文件中直接读取字符,便可以使用字符输入流 FileReader,通过它可以从关联的文件中读取一个或一组字符。

下面通过一个案例演示如何使用 FileReader 读取文件中的字符:

首先新建文本文件 test.txt 并在其中输入字符 “itcast”然后创建字符输入流 FileReader对象以读取 reader.txt文件中的内容

<code>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();

}

注意:

 FileWriterFileOutputStream 一样,如果指定的文件不存在,就会先创建文件,再写入数据;如果文件存在,则原文件内容会被覆盖。如果想在文件末尾追加数据,同样需要调用重载的构造方法,将上面第三行代码修改为:

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

是从字符流到字节流的桥梁,使用指定的编码将写入的字符编码为字节它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集

通过 InputStreamReaderOutputStreamReader 将字节流转换为字符流,可以提高文件的读写效率

方法声明 功能描述
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中,可序列化的类必须实现 SerializableExternalizable 两个接口之一。

Serializable 接口或 Externalizable 接口实现序列化机制的主要区别

Serializable 接口 Externalizable 接口
系统自动存储必要的信息 由程序员自己决定要存储的信息
Java 内部支持,易于实现,只需实现该接口即可,不需要其他代码支持 该接口只提供了两个抽象方法,实现该接口时必须重写这两个抽象方法
性能较差 性能较好

👻 与实现 Serializable  接口相比,虽然实现 Externalizable 接口可以带来性能上的一定提升,但由于后者需要实现两个抽象方法,所以将导致编程的复杂度提高。

在实际开发时,大部分情况下使用Serializable 接口的方式实现对象序列化。

🎉使用Serializable 接口实现对象序列化非常简单,只需要让目标类实现 Serializable 接口即可,无须实现任何方法。例如,自定义Person类,让Person类实现 Serializable接口,如下:

<code>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中,字节流的基类是InputStreamOutputStream。字节流在操作时通常不会使用缓冲区,直接与文件本身进行操作,这意味着每次调用read方法都可能伴随着一次磁盘IO,因此效率相对较低。为了提高效率,可以使用如BufferedInputStreamBufferedOutputStream这样的缓冲字节流。

字符流则是以Unicode码元(16位)为单位进行数据传输,主要用于处理文本数据。字符流在处理数据时会涉及字符编码的转换,如UTF-8或GBK等。在Java中,字符流的基类是ReaderWriter。字符流在输出前会完成Unicode码元序列到相应编码方式的字节序列的转换,并使用内存缓冲区来存放转换后的字节序列,等待都转换完毕再一同写入磁盘文件中。

主要区别在于:

字节流操作的基本单元为字节,而字符流操作的基本单元为Unicode码元。

字节流不使用缓冲区,字符流使用缓冲区。

字节流可以处理任何类型的数据,字符流主要处理文本数据。

字节流与文件直接操作,字符流在操作时使用缓冲区。

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!💞❤️‍🔥💞

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1uyo4puciczbw



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。