当前位置: 首页 > news >正文

文件操作和 IO(二):文件内容操作 => 流对象

目录

1. 流

1.1 什么是流

1.2 字节流/字符流

2. InputStream(字节流 - 读/输入)

2.1 打开文件

2.2 关闭文件

2.2.1 try with resources

2.3 读文件

2.3.1 int read()

2.3.2 int read(byte[] b)

2.3.3 read(byte[] b, int off, int len)

3. OutputStream(字节流 - 写/输出)

3.1 打开/关闭文件

3.2 写文件

3.2.1 追加写

4. Reader(字符流 - 读/输入)

4.1 打开/关闭文件

4.2 读文件

4.2.1 转码

5. Writer(字符流 - 写/输出)

6. 拓展 - 缓冲区 

 7. 练习一 (扫描文件名称并删除)

7.1 代码

8. 练习二 (文件复制)

8.1 代码

9. 练习三 (根据名称和内容搜索文件)

9.1 代码


引言, 上篇博客所讲的 File 类, 是针对文件进行系统操作.

而本篇博客所讲的"流", 就是针对文件进行内容操作 --- 读写文件.

1. 流

针对文件的内容操作(读写文件), 主要是通过 "流对象" 来实现的.

为了方便大家更好的理解什么是 "流", 这里举个例子.

1.1 什么是流

说起流, 想必大家第一时间就会想到 水流. 

如果我们要接100ml的水, 通过水流可以有很多种接法:

  • 可以一次接100ml, 一次性接完.
  • 可以一次接50ml, 分两次接完.
  • 可以一次接1ml, 分一百次接完.
  • ......

计算机中的 "流" 和水流也是十分相似的:

如果我们要从文件中读取100字节的数据, 通过 "流" 可以有很多种读数据的方法:

  • 可以一次读100字节数据, 一次读完.
  • 可以一次读50字节数据, 分两次读完.
  • 可以一次读1字节数据, 分一百次读完.
  • ......

所以, 在计算机中, 读写数据也是通过 "流(stream)" 来实现的.

流, 是操作系统层面的术语, 各种编程语言操作文件, 都叫做流.

1.2 字节流/字符流

Java 中, 也提供了几十个类来表示 流.

针对那么多的流, 大体上分为以下两个大的类别:

  1. 字节流. 在读写文件时, 以字节为单位, 是针对二进制文件使用的.
  2. 字符流. 在读写文件时, 以字符为单位, 是针对文本文件使用的

注意:

字节 != 字符

一个字节对应多少个字符呢?? 答案是不确定, 这取决于字符集(编码方式)

举个例子,

一个汉字占多少个字节, 这取决于字符集(汉字是怎样编码的).

  • gbk 字符集中, 一个汉字占2个字节(Windows 10/11 简体中文版默认是 gbk 编码)
  • utf8 字符集中, 一个汉字占3个字节. (utf8 本身是变长编码, 1~4 个字节)
  • Unicode 字符集中, 一个汉字占2个字节.(Java 的 char 使用的就是 Unicode)

针对 字节流, 有两个代表性的类(其他流对象, 都是直接或间接的继承自这两个类):

  1. InputStream --- 输入(从文件中读数据)
  2. OutputStream --- 输出(往文件中写数据)

针对 字符流, 有两个代表性的类(其他流对象, 都是直接或间接的继承自这两个类):

  1. Reader --- 输入(从文件中读数据)
  2. Writer --- 输出(往文件中写数据)

啥叫输入, 啥叫输出呢??

输入, 输入, 是在 cpu 的视角下, 数据的流向.

  • 输入: 数据从硬盘(文件) => cpu/内存
  • 输出: 数据从cpu => 硬盘/内存

可以把自己想象成 cpu, 如果数据迎面向你走来, 就是输入; 如果数据离你而去, 就是输出.


2. InputStream(字节流 - 读/输入)

上文说到, InputStream 可以针对文件进行读(输入)操作.

注意: 像 InputStream 这样的流对象体系, 不仅是针对文件进行操作, 像后续的网络编程也需要 流对象.

2.1 打开文件

InputStream 本身也是一个抽象类, 是不能进行实例化的. 

通过 new FileInputStream 来完成向上转型, FileInputStream 的构造方法, 填写要操作文件的路径(绝对/相对).

由于该文件可能是不存在的, 所以可能会抛出 FileNotFoundException 异常(继承自IOException): 

(文件没有找到的异常)

并且, 这里的创建对象操作一旦成功, 就相当于 "打开文件" 的操作(类似于 C语言的 fopen)

只有先打开文件, 才能进行后续的读写操作, 而打开操作, 就是根据文件路径, 定位到对应的磁盘空间.


2.2 关闭文件

由于文件也是一种资源, 所以谈到打开文件, 那就不得不提关闭文件~

在对文件完成相关的操作后, 一定一定要关闭文件资源. 如果没有关闭文件的话, 就会造成"文件资源泄露", 类似于 C语言 的"内存资源泄露".

对于我们Java 程序员, 由于内存的释放 JVM 的 GC 已经帮助我们自动完成了, 我们只需申请内存就好, 释放我们不用去管.

但是 文件资源 不等同于 内存资源, 虽然 GC 能够自动管理内存, 但是不能自动管理文件, 文件资源的释放需要我们手动来完成.

 而为什么非要释放内存资源呢??

还记得在进程中的 PCB 吗?? 进程的一个关键属性就是: 文件描述符表.

进程每打开一个文件, 就会在文件描述符表中申请一个表项(相当于占了个位置), 而文件描述符表的长度是固定的(无法扩容), 所以, 如果光打开文件而不释放文件, 就会使文件描述符表中的表项消耗殆尽, 后续再打开文件, 就会打开失败, bug 就出现了~

通俗来说, 如果只打开文件而不释放文件, 就是 "占着茅坑不拉屎"~

而 close 方法, 就是用来关闭文件资源的, 并且该方法可能抛出IOException 异常 :

上文说到, 为了避免 文件资源泄露, close 方法是一定要执行的,

为了避免程序中途 return 或者抛异常使程序提前结束, 而导致 close 没有执行到, 我们可以将 close 放到 finally 块中, 从而保证 close 100%被执行.

虽然这样保证了 close 的执行, 但是又出现了一个新的问题 => 丑!!

2.2.1 try with resources

try-finally 虽然保证了 close 的执行, 但是代码变得非常的冗余不美观, 要知道这是一个看脸的世界~

而 try with resources 就可以在保证 close 的执行(文件资源关闭)的前提下, 又能保证代码的简洁美观.

try with resources 是 Java 1.5 引入的语法, 可以保证代码出了 try 的代码块, 就会执行 close.

并且, try 内可以打开多个资源, 中间使用 ; 隔开即可:

 到这里, 有同学就想到了之前的 ReentrantLock : "既然 try with resources 这么香, 是不是它也能保证 ReentrantLock 的 unlock 的执行??"

很遗憾, 答案是不能~

因为只有实现了 Closable 接口的类才能够使用 try with resources, Closable 接口约定了类必须有 close 方法, 这样才能让 JVM 自动调用~~

2.3 读文件

打开文件后, 通过 read 方法来读取文件中的内容, read 的使用(传参)有以下三种形式:

2.3.1 int read()

read 不带参数的版本, 调用一次, 读取一个字节的数据, 返回读取到的内容. 

当数据全部读取完毕后, 返回 -1.

这里有一个问题, 明明是读取一个字节的内容, 为什么 read 的返回值是一个 int 类型的数据呢?? 为啥不用 byte 类型呢??

  1. 因为一个字节(8 bit)数据的取值范围为 0 ~ 255 .
  2. 而一个 byte 的只能表示 -128 ~ 127
  3. 并且 int 还可以表示 -1, 代表文件读取完毕.

我们将 test.txt 中的内容换成汉字, 继续观察:

发现, 一个汉字是3个字节, 推断出是 utf8 编码. 查看码表, 发现确实是文件中的内容, 故, 读取文件成功.

    while (true) {// 一次读取一个字节的内容int data = inputStream.read();if (data == -1) {// 文件读完break;}System.out.println(data);}

2.3.2 int read(byte[] b)

  • read 带一个参数的版本, 调用一次, 读取多个字节. 
  • 其中字节数组作为 "输出型参数" 传入, 会将读取到的内容尽可能的填满到字节数组中(也就是说, 每次最多能读取的字节数是字节数组的长度), 如果填不满, 能填几个是几个. 返回值是读取到的字节数, 当文件全部读取完毕时, 返回 -1
  • 如果定义的字节数组足够大(能够装满文件内容), 则会一次读完所有的数据

    while (true) {// 自定义数组长度byte[] data = new byte[1024];// 一次读取多个字节// data 作为"输出型"参数, 将读取到的内容尽可能填满字节数组// 如果填不满, 能填几个是几个// 返回值是读取到的字节数int n = inputStream.read(data);if (n == -1) {// 文件读完break;}for (int i = 0; i < n; i++) {System.out.println(data[i]);}}

2.3.3 read(byte[] b, int off, int len)

  • 字节数组 byte[] b 仍然作为 "输出型参数" 保存读取到的内容. 
  • off => offset, 代表偏移量(数组下标), 将读取到的内容放到字节数组的 off 下标处.(带一个参数的版本, 默认将数据放到 0 下标处)
  • len 代表, 一次最多读取 len 个字节的内容.
  • 返回值仍然为读取到的字节数, 当文件全部读取完毕返回 -1

这个版本的作用就是, 可以将每次读操作读到的数据, 放到一个数组的某一部分.


3. OutputStream(字节流 - 写/输出)

3.1 打开/关闭文件

从 cpu 往文件中写(输出)数据, 同样需要先打开文件(搭配 try with resources 使用, 出代码块自动调用 close 关闭文件资源): 

注意 OutputStream 的以下注意事项:

  1. 当文件不存在时, OutputStream 会自动创建这个文件!!(当文件不存在时, 不会出现报错)
  2. 每次使用 OutputStream 打开一个文件时, 会清除上次文件中的内容!! (在打开文件的一瞬间, 上次文件中的内容就清空了)
  3. 若采用 追加写 的方式, 会避免上次文件中的内容被清空.

3.2 写文件

OutputStream 通过 write 方法来向文件中写数据.

write 方法同样提供了三个版本:

  • 第一个版本 write(int b), 传入要写数据的内容(ASCII 值):
  • 第二个版本 write(byte[] b), 将字节数组中的内容全部写入文件中:
  • 第三个版本 write(byte[] b, int off, int len) : 从字符数组的 off 下标处的数据开始, 写入文件中, 一共写入 len 个字节的数据.

3.2.1 追加写

为避免文件内容被清空, 可以采用 追加写 形式进行写操作.

上文的写操作是不带追加写模式的, 即每次进行写操作时, 都会把上次文件中的内容清空.

而追加写模式, 可以不清空上次文件中内容, 而继续往后写本次要写入的数据.

使用追加写, 直接在构造 FileOutputStream 对象时, 第二个参数传入 true 即可(开启追加写模式):


4. Reader(字符流 - 读/输入)

4.1 打开/关闭文件

Reader 的操作和 InputStream 的操作步骤相同, 想要读取一个文件的内容, 首先需要打开文件.

当 Reader 对象被创建好时, 文件就被打开了, 搭配 try with resources 使用, 出了代码块就会自动关闭文件资源:

同样, 当要读取的文件不存在时, 程序会抛出异常.

4.2 读文件

使用 read 进行读操作时, 是一个字符一个字符的读取, read 同样也提供了多种版本, 当读取完毕时返回 -1:

  • 第一个版本, read(), 一次读取一个字符, 读取完毕返回 -1:
  • 第二个版本(字符数组版本), read(char[] cbuf), 一次读多个字符放到字符数组中, 尽可能将数组填满, 读取完毕返回 -1:
  • 第三个版本, read(char[] cbuf, int off, int len), 将读到的字符从字符数组的 off 位置开始放, 最多读 len 个

4.2.1 转码

到这里可以发现, 字符流的 read 和 字节流的 read 使用上是差不多的.

但是, 我们在使用字节流读取汉字时, 一汉字占的是 3 个字节(utf8 编码), 但是在使用字符流读取时, 一次读取的是两个字节的内容, 一个字符(包括汉字)占的是 2 个字节.

为什么两种方式读取的汉字都是相同的, 字符所占的字节数却不同呢?? 这两个哪个是对的, 哪个是错的呢??

答: 两个方式的读取结果, 都是正确的.

  1. 字节流读取的数据, 是文件中原始的数据, 在硬盘上保存文件的时候, 采用的是 utf8 编码, 一个汉字占的是 3 个字节. 
  2. 而字符流在读取文件的时候, 就会根据文件的内容编码格式, 进行解析. 调用一次 read(), 确实读取到的是 3 个字节的内容('你' 这个汉字)(按照 utf8 进行的编码),  但是在返回的时候, 就对这 3 个字节('你' 这个汉字)进行转码了.
  3. 是这样进行转码的: 把 3 个字节的内容在 utf8 中查了一下, 发现是 '你' 这个汉字. read 又把 '你' 这个汉字在 unicode 表中查了一下, 得到 unicode 的编码值, 最终把 unicode 的编码值进行了返回, 最终返回的就是两个字节了.

综上, Java 使用字符流进行读取时, 会自动进行转码操作, 从而提升了我们的开发效率(代码写得快~~)

效率的影响因素有两个:

  1. 程序执行快 => 运行效率
  2. 代码写得快 => 开发效率(更为重要)

虽然转码有性能开销, 但是这些性能的开销却大大提高了我们的开发效率~~


5. Writer(字符流 - 写/输出)

Writer 是以字符流中进行写操作的类, 以字符的形式向文件中写数据.

其用法也是分为以下几步:

  1. 打开文件(Writer 对象一旦创建好, 就打开了文件. 文件可以不存在, Writer 会自动创建)
  2. 写文件(write() 方法, 以字符形式写数据)
  3. 关闭文件(出了 try 自动调用 close 方法)

可以观察到, Writer 的 write 方法, 不仅可以一次写一个字符, 可以一次写一个字符数组, 还可以一次写一个字符串.

此外, 若想使用 追加写 的形式写数据, 依旧在构造对象时, 第二个参数传入 true 即可:


6. 拓展 - 缓冲区 

缓冲区, 通常就是一段内存空间.(可以提高程序的效率)

大家都知道, 硬盘的读写速度是比较慢的, 一次一次的读取是很低效的, 因此我们就希望减少文件的读写次数来提高效率. 

因此在进行 IO 操作的时候, 就希望能够使用缓冲区, 把要读/写的内容放到缓冲区中先存一波, 攒一攒, 最后将这些数据一起读/写.

举个例子, 大家嗑瓜子时, 都喜欢把磕完的瓜子皮放到手中, 等瓜子皮在手中满后, 再去扔到垃圾桶中, 这样我们就避免每磕一个瓜子就去扔一个瓜子片, 而手, 就可以认为是一个缓冲区.

在上文中, 流对象的 read/write 方法中的字节数组/字符数组, 其实就是一个缓冲区, 可以提高文件读写时的效率.


综上, 8 个流对象(InputStream, FileInputStream, OutputStream, FileOutputStream, Writer, FileWriter, Reader, FileReader)的介绍已经完毕了, 总结如下:

  • 流对象的使用流程: 打开文件 => 读/写文件 => 关闭文件
  • 文本文件的读写使用字符流
  • 二进制文件的读写使用字节流

 7. 练习一 (扫描文件名称并删除)

要求: 扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件

7.1 代码

/*** 扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件*/
public class Demo11 {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);System.out.println("请输入要搜索文件的路径:");String rootDir = scanner.next();File sourceFile = new File(rootDir);if(!sourceFile.isDirectory()) {System.out.println("输入的不是目录!!");return;}System.out.println("请输入要删除文件的关键字:");String key = scanner.next();searchFile(key, sourceFile);}private static void searchFile(String key, File sourceFile) {File[] files = sourceFile.listFiles();if (files == null) {// 目录为空return;}for (File file : files) {if (file.isFile()) {// 普通文件 => 判断isDestFile(key, file);}else {// 目录 => 递归深搜searchFile(key, file);}}}private static void isDestFile(String key, File file) {String fileName = file.getName();if (fileName.contains(key)) {System.out.println("找到含关键字的文件名, 该文件路径为:" + file.getAbsoluteFile());System.out.println("是否删除(y/s):");Scanner scanner = new Scanner(System.in);String input = scanner.next();if (input.equalsIgnoreCase("y")) {file.delete();System.out.println("文件删除成功!!");}}}
}

8. 练习二 (文件复制)

进行普通文件的复制

8.1 代码

/*** 进行普通文件的复制*/
public class Demo12 {public static void main(String[] args) throws IOException {Scanner scanner = new Scanner(System.in);System.out.println("请输入源文件路径:");String sourcePath = scanner.next();File sourceFile = new File(sourcePath);if (!sourceFile.isFile()) {System.out.println("源文件不是文件或不存在!!");return;}System.out.println("请输入目标文件路径:");String destPath = scanner.next();File destFile = new File(destPath);// 目标文件可以不存在, 但是目标文件下的目录必须存在if (!destFile.getParentFile().isDirectory()) {System.out.println("目标文件所在目录不存在!!");return;}copyFile(sourceFile, destFile);}private static void copyFile(File sourceFile, File destFile) throws IOException {// 进行的是复制操作, 要求两个文件中的内容完全相同// 所以不能使用 追加写try (InputStream inputStream = new FileInputStream(sourceFile);OutputStream outputStream = new FileOutputStream(destFile)) {while (true) {byte[] bytes = new byte[1024];int n = inputStream.read(bytes);if (n == -1) {break;}outputStream.write(bytes, 0, n);}}}
}

9. 练习三 (根据名称和内容搜索文件)

扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)

9.1 代码

注意:由于代码是通过深搜来进行查找的, 所以目前方案性能较差,尽量不要在太复杂的目录下或者大文件下实验

/*** 扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)*/
public class Demo13 {public static void main(String[] args) throws FileNotFoundException {Scanner scanner = new Scanner(System.in);System.out.println("请输入要搜索的目录:");String rootPath = scanner.next();File rootDir = new File(rootPath);if (!rootDir.isDirectory()) {System.out.println("输入的路径不是目录或不存在!!");return;}System.out.println("请输入要搜索的文件的名称的关键字或文件内容中的关键字:");String key = scanner.next();searchFile(key, rootDir);}private static void searchFile(String key, File rootDir) throws FileNotFoundException {File[] files = rootDir.listFiles();if (files == null) {return;}for (File file : files) {if (file.isFile()) {isDestFile(key, file);} else {searchFile(key, file);}}}private static void isDestFile(String key, File file) throws FileNotFoundException {if (file.getName().contains(key)) {System.out.println("文件名称中包含关键字: " + file.getAbsoluteFile());return;}StringBuilder stringBuilder = new StringBuilder();try (Reader reader = new FileReader(file)) {while (true) {char[] chars = new char[1024];int read = reader.read(chars);if (read == -1) {break;}stringBuilder.append(chars, 0, read);}} catch (IOException e) {throw new RuntimeException(e);}if (stringBuilder.indexOf(key) >= 0) {// 包含关键字System.out.println("文件内容中包含关键字: " + file.getAbsoluteFile());}}
}

END


http://www.mrgr.cn/news/65841.html

相关文章:

  • Spring Bean的作用域和生命周期
  • centos7 安装python3.9.4,解决import ssl异常
  • 河北冠益荣信科技公司洞庭变电站工程低压备自投装置的应用
  • 慢sql优化和Explain解析
  • 项目一:使用 Spring + SpringMVC + Mybatis + lombok 实现网络五子棋
  • 高级SQL技巧详解与实例
  • 小北的字节跳动青训营与LangChain实战课:深入解析模型I/O与提示模板(持续更新中~~~)
  • Java 入门
  • DFS求解迷宫最长移动路线
  • 助力风力发电风机设备智能化巡检,基于YOLOv8全系列【n/s/m/l/x】参数模型开发构建无人机巡检场景下风机叶片缺陷问题智能化检测预警模型
  • Java基础06(代码运行时的内存图)
  • 基于matlab的图像形状与分类的方法比较
  • Windows基础2(病毒编写)
  • WordPress站点网站名称、logo设置
  • C语言 | Leetcode C语言题解之第538题把二叉搜索树转换为累加树
  • 科研绘图系列:R语言圆堆积图(circle stacked plot)
  • Nginx线程模型
  • 【AIGC】如何通过ChatGPT轻松打造个性化GPTs应用
  • 【数据结构- 合法括号字符串】力扣1190. 反转每对括号间的子串
  • 代码训练营 day55|卡码网98
  • Linux:网络协议socket
  • 高频面试题基本总结(含笔试高频算法整理)回顾44
  • 从最小作用量原理推导牛顿三大定律
  • 简单题:环状 DNA 序列的最小表示法| 豆包MarsCode AI刷题
  • PGMP练-DAY16
  • Android沙箱