【Java】I/O 操作详解

 📃个人主页: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 程序将内容中的数据写入文件,输入输出操作在实际开发中比较广泛。

  1. IO:输入/输出(Input/Output)
  2. 流:是一种抽象概念,是对数据传输的总称.也就是说数据在设备间的传输称为流,流的本质是数据传输
  3. IO流就是用来处理设备间数据传输问题的.常见的应用: 文件复制; 文件上传; 文件下载

IO流的分类:

(1)按照数据的流向

  • 输入流:读数据
  • 输出流:写数据

(2)按照数据类型来分:

  1. 字节流
    • 字节输入流
    • 字节输出流
  2. 字符流
    • 字符输入流
    • 字符输出流

IO流的使用场景

  1. 如果操作的是纯文本文件,优先使用字符流
  2. 如果操作的是图片、视频、音频等二进制文件,优先使用字节流
  3. 如果不确定文件类型,优先使用字节流,字节流是万能的流

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

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

演示:

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 虚拟机退出时 自动删除文件 fileSystem.out.println("file 是否为文件:" + file.isFile());System.out.println("file 的相对路径:" + file.getPath());
}

2.3 遍历目录下的文件

File 类中提供了 list()方法,可以获取目录下所有文件和目录的名称。获取目录下所有文件和目录名称后,可以通过这些名称遍历目录下的文件,按照调用方法的不同,对目录下的文件遍历可分为以下3种方式。

  1. 遍历指定目录下的所有文件
  2. 遍历指定目录下指定扩展名的文件
  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 方法@Overridepublic boolean accept(File dir, String name) {File currFile = new File(dir, name);// 如果文件以 .java 结尾返回trueif(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 

  1. 字节流抽象基类
    • InputStream:这个抽象类是表示字节输入流的所有类的超类
    • OutputStream:这个抽象类是表示字节输出流的所有类的超类
    • 子类名特点:子类名称都是以其父类名作为子类名的后缀
  2. 字节输出流
    • 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() 方法都是用来读数据的。其中:

  1. 第一个 read() 方法是从输入流中逐个读入字节;
  2. 而第二个和第三个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()方法,都用于向输出流写入字节。

  1. 其中,第一个write()方法逐个写入字节;
  2. 后两个write()方法将若干字节以字节数组的形式一次性写人,从而提高写数据的效率。
  3. flush()方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。
  4. close()方法用来关闭1/0流并释放与当前1/0流相关的系统资源。

InputStream OutputStream 这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能, InputStream 类 OutputStream 类提供了不同的子类,形成了体系结构,如下图:

InputStream 体系结构图:

OutputStream 体系结构图:

3.2 字节流读文件

  • 🧩InputStream 就是JDK提供的基本输入流,它是所有输入流的父类,FileInputStream InputStream 的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。因为从文件读取数据是重复的操作,所以需要通过循环语句实现数据的持续读取。

下面通过一个案例实现字节流对文件数据的读取。在实现案例之前,先做以下操作:

  1. 首先在 Java项目的根目录下创建文本文件test.txt
  2. 在文件中输入内容“itcast” 并保存
  3. 然后使用字节输入流对象读取 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类似.

  1. OutputStream是所有输出流的父类。
  2. OutputStream 是一个抽象类,如果使用此类,则必须先通过子类实例化对象。
  3. 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 又是什么意思呢 》 解释如下:

对于字节流写数据,应该如何实现换行

  1. windows:\r\n
  2. linux:\n
  3. mac:\r


需要注意的是:I/O流 在进行数据读写操作时会出现异常。为了保持代码的简洁,在InputStream 读文件和OutputStream写文件的程序中都使用了throws关键字将异常抛出。然而一旦遇到 I/O异常I/O流 close()方法无法得到执行I/O流 对象占用的系统资源将得不到释放

因此,为了保证I/O流close()方法 必须执行,通常将关闭 I/O流 的操作写在 finally代码块中。

3.4 字节流复制文件

在应用程序中,I/O 流通常都是成对出现的,即输入流和输出流一起使用。例如:文件的复制就需要通过输入流读取一个文件中的数据,再通过输出流将数据写入另一个文件

下面通过一个案例演示文件内容的复制:

  1. 首先在 src 项目的根目录下创建 source目录和 target 目录,
  2. 然后在 source 目录中存放 a.png文件,
  3. 最后将 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目录中

注意事项:

上述实现的文件复制过程是逐字节读写,需要频繁地操作文件,效率非常低

打个比方:

  • 从北京运送烤鸭到上海,如果有一万只烤鸭,每次运送一只,就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批烤鸭装在车厢中,这样就可以成批地运送烤鸭,这时的车厢就相当于一个缓冲区

因此在通过流的方式复制文件时,为了提高效率,也可以定义一个字节数组作为缓冲区

  1. 在复制文件时,可以一次性读取多个字节的数据,并保存在字节数组中,然后将字节数组中的数据一次性写入文件。
  2. 程序中的缓冲区就是一块内存,它主要用于暂时存放输入输出的数据,由于使用缓冲区减少了对文件的操作次数,所以可以提高数据的读写效率

利用缓冲区复制文件,修改代码如下:

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类

  1. Reader 类是字符输入流,用于从某个源设备读取字符;
  2. Writer类是字符输出流。用于向某个目标设备写入字符。
  3. 在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 类的很多子类都是成对出现。例如:

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

4.2 字符流读文件

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

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

  1. 首先新建文本文件 test.txt 并在其中输入字符 “itcast”
  2. 然后创建字符输入流 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();
}

注意:

 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对象

下面通过一个案例演示如何将字节流转为字符流

  1. 首先.在src项目的根目录下新建文本文件 test.txt
  2. 并在文件中输入“Island1314”
  3. 其次,在sre文件夹中创建一个类,在类中创建字节输入流 FileInputStream对象读取src.txt文件中的内容,并将字节输入流转换成字符输入流。
  4. 再次,创建一个字节输出流对象,并指定目标文件为des.txt
  5. 最后,将字节输出流转换成字符输出流将字符输出到文件中
public static void main(String[] args) throws Exception {// 创建字节输入流 in ,并且指定源文件 test.txtFileInputStream in = new FileInputStream("src/IO/test.txt");// 将字节输入流 in 转化为 字符输入流 isrInputStreamReader isr = new InputStreamReader(in);// 创建字节输出流 out ,并且指定源文件 des.txtFileOutputStream out = new FileOutputStream("src/IO/des.txt");// 将字节输出流 out 转化为 字符输出流 oswOutputStreamWriter osw = new  OutputStreamWriter(out);int ch; // 定义一个变量用于记录读取的字符while((ch = isr.read()) != -1) // 循环判断是否读到文件末尾{osw.write(ch);}isr.close(); // 关闭字符输入流,节省资源osw.close(); // 关闭字符输出流,节省资源
}


 

6. 缓冲流 🔍

6.1 字节缓冲流

  1. BufferedOutputStream:该类实现缓冲输出流.通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用
  2. BufferedInputStream:创建BufferedInputStream将创建一个内部缓冲区数组.当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节
  3. 字节流缓冲区的核心优势就是一次读取多个字节数据,从而减少硬盘操作子树

构造方法:

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 字符缓冲流

  1. BufferedWriter:将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小。默认值足够大,可用于大多数用途
  2. 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流 的字节序列的过程

  1. 对象序列化机制可以使内存中的Java对象转换成与平台无关的二进制流,
  2. 通过编写程序,既可以将这种二进制流持久地保存在磁盘上,
  3. 又可以通过网络将其传输到另一个网络节点。

🥁 其他程序在获得了二进制流后,还可以将二进制流恢复成原来的Java对象,这种将 I/O流 中的字节序列恢复为Java对象的过程称为 反序列化(deserialize)

🍇 如果想让某个对象支持序列化机制,那么这个对象所属的类必须是可序列化的。在Java中,可序列化的类必须实现 SerializableExternalizable 两个接口之一。

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

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

主要区别在于:

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/446604.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Golang | Leetcode Golang题解之第472题连接词

题目&#xff1a; 题解&#xff1a; type trie struct {children [26]*trieisEnd bool }func (root *trie) insert(word string) {node : rootfor _, ch : range word {ch - aif node.children[ch] nil {node.children[ch] &trie{}}node node.children[ch]}node.isE…

Qt-系统网络TCP回显客户端与服务端(65)

目录 描述 函数 使用 服务器 准备工作 声明相关函数与对象 绑定并监听 定义槽函数与连接 瑕疵 释放 客户端 准备工作 声明相关函数与对象 初始化并连接服务器 给发送添加槽函数 连接信号槽处理响应函数 测试运行 补充 代码 客户端 服务器 描述 有UDP&…

MySQL 删除数据库

1.使用命令行删除一个数据库 1.1 首先登陆进入 MySQL 操作界面&#xff0c;命令如下&#xff1a; 命令 : mysql -utest -p;1.2 登陆成功之后可以使用如下命令查看当前已有数据库&#xff1a; 命令 : SHOW DATABASES; 执行结果如下图: 如图所示当前已包含 MySQL 系统数据库和…

TensorRT-LLM七日谈 Day3

今天主要是结合理论进一步熟悉TensorRT-LLM的内容 从下面的分享可以看出&#xff0c;TensorRT-LLM是在TensorRT的基础上进行了进一步封装&#xff0c;提供拼batch&#xff0c;量化等推理加速实现方式。 下面的图片更好的展示了TensorRT-LLM的流程&#xff0c;包含权重转换&…

iPhone 16 Pro 拆解揭秘:设计改进与维修便利性

苹果最新推出的iPhone 16系列在许多方面都进行了更新和改进&#xff0c;而这次我们要聚焦的是其中的高端型号——iPhone 16 Pro。 这款手机不仅在性能上有所提升&#xff0c;在内部构造上也带来了不少变化&#xff0c;让我们一起来看看这些细节吧。 更容易进入的内部结构 对于…

一、Java基础

韩顺平Java基础 浮点型使用细节基本数据类型转换自动类型转换强制类型转换 浮点型使用细节 double d 8.1 / 3 的结果是一个非常接近2.7的小数&#xff0c;比如2.69999997&#xff0c;这是计算机的运算规则造成的 基本数据类型转换 自动类型转换 对于第四点&#xff0c;如下…

电脑知识:适用于 Windows 10 的 PDF 编辑器列表

PDF 是一种流行的、多功能且安全的文件格式&#xff0c;用于在线共享文档。但是&#xff0c;如果没有合适的应用程序&#xff0c;查看和编辑 PDF 文件可能会变得复杂。 幸运的是&#xff0c;有很多 PDF 编辑器可以帮助您更正重要文档上的错误、填写表格、为合同添加签名、更改…

Unity3d折叠Inspector中的变量

InspectorFoldoutGroup插件 [Pixeye.Unity.Foldout("【曲线图】")] public BrokenLineUpDownGraph aimStabilityGraph;[Pixeye.Unity.Foldout("【曲线图】")] public BrokenLineUpGraph aimDensityGraph;[Pixeye.Unity.Foldout("【曲线图】")] p…

MedMamba代码解释及用于糖尿病视网膜病变分类

MedMamba原理和用于糖尿病视网膜病变检测尝试 1.MedMamba原理 MedMamba发表于2024.9.28&#xff0c;是构建在Vision Mamba基础之上&#xff0c;融合了卷积神经网的架构&#xff0c;结构如下图&#xff1a; 原理简述就是图片输入后按通道输入后切分为两部分&#xff0c;一部分走…

Spring Boot 应用开发:入门与实战

Spring Boot 应用开发&#xff1a;入门与实战 引言 Spring Boot 是 Spring 框架的一个子项目&#xff0c;旨在简化 Spring 应用的配置和开发。它通过自动配置和嵌入式服务器&#xff0c;极大地简化了 Java 企业级应用的开发。本文将详细介绍 Spring Boot 的核心概念&#xff…

JVM进阶调优系列(1)类加载器原理一文讲透

今天开始写JVM调优系列&#xff0c;并发编程系列也会继续穿插连载&#xff0c;让各位同学闲暇之余有更多阅读选择。 起笔写第一篇&#xff0c;并不好写。首先要构思整个系列的大概框架&#xff0c;一个好的框架一定是深度上由浅入深、逻辑上有严格顺序&#xff0c;读者订阅跟踪…

《OpenCV计算机视觉》—— 人脸检测

文章目录 一、人脸检测流程介绍二、用于人脸检测的关键方法1.加载分类器&#xff08;cv2.CascadeClassifier()&#xff09;2.检测图像中的人脸&#xff08;cv2.CascadeClassifier.detectMultiscale()&#xff09; 三、代码实现 一、人脸检测流程介绍 下面是一张含有多个人脸的…

使用camunda的DMN实现班级决策案例

班级决策 Camunda 支持DMN1.3版本&#xff0c;在BPMN业务活动流程中&#xff0c;可通过业务规则任务调用DMN决策。DMN决策目的是想把业务代码和决策进行解耦&#xff0c;使决策分析人员只需关心决策即可。 需求描述 通过幼儿园学生年龄age和身高height分配不同的班级&#xff0…

10.13论文阅读

通过联合学习检测和描述关键点增强可变形局部特征 摘要 局部特征提取是计算机视觉中处理图像匹配和检索等关键任务的常用方法。大多数方法的核心理念是图像经历仿射变换&#xff0c;忽略了诸如非刚性形变等更复杂的效果。此外&#xff0c;针对非刚性对应的新兴工作仍然依赖于…

个性化图像生成新王炸!无需微调,Meta重磅发布Imagine yourself:三大核心全面SOTA!

论文链接&#xff1a;https://arxiv.org/pdf/2409.13346 亮点直击 本文提出了“Imagine Yourself”&#xff0c;这是一种用于个性化图像生成的创新型最先进模型。该模型可以将任意参考图像作为输入进行定制化图像生成&#xff0c;并且不需要针对每个对象进行调整。“Imagine Yo…

springboot汽车售票系统演-毕业设计源码07891

基于springboot的汽车售票系统 摘 要 汽车售票系统主要功能模块包括系统用户管理、车次车票信息、车票预定、退票信息、改签信息等&#xff0c;采取面对对象的开发模式进行软件的开发和硬体的架设&#xff0c;能很好的满足实际使用的需求&#xff0c;完善了对应的软体架设以及…

【C】C语言常见概念~

C语言常见概念 转义字符 转义字符&#xff0c;顾名思义&#xff0c;转变原来意思的字符 比如 #include <stdio.h> int main() {printf("abcndef");return 0; }输出的结果为&#xff1a; 将代码修改一下&#xff1a; #include <stdio.h> int main(…

萱仔求职复习系列——2 Linux的常用方法(包含基础进阶高级操作)

由于最近接了一个笔试&#xff0c;发现笔试可能涉及到Linux&#xff0c;我准备临时抱佛脚一下赶紧复习一下Linux的用法哈哈。Linux 的基础用法包含文件系统操作、权限管理、网络配置、进程管理等基本命令&#xff1b;进阶操作包括网络调试、包管理、服务管理和用户管理等&#…

UE5学习笔记24-添加武器弹药

一、给角色的武器添加弹药 1.创建界面&#xff0c;根据笔记23的界面中添加 2.绑定界面控件 UPROPERTY(meta (Bindwidget))UTextBlock* WeaponAmmoAmount;UPROPERTY(meta (Bindwidget))UTextBlock* CarriedAmmoAmount; 3.添加武器类型枚举 3.1创建武器类型枚举头文件 3.2创建文…

【论文解读系列】EdgeNAT: 高效边缘检测的 Transformer

代码&#xff1a; https://github.com/jhjie/edgenat 论文&#xff1a; https://arxiv.org/abs/2408.10527v1 论文 EdgeNAT: Transformer for Efficient Edge Detection 介绍了一种名为EdgeNAT的基于Transformer的边缘检测方法。 1. 背景与动机 EdgeNAT预测结果示例。(a, b)…