首页 > 其他分享 >文件操作和 IO(二):文件内容操作 => 流对象

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

时间:2024-11-06 14:15:24浏览次数:3  
标签:文件 读取 字符 read System 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

标签:文件,读取,字符,read,System,IO,操作,字节
From: https://blog.csdn.net/2401_83595513/article/details/143327227

相关文章

  • 在 Windows Server 2025 中,C:\Users\Administrator\AppData 文件夹是与用户配置、
    在WindowsServer2025中,C:\Users\Administrator\AppData文件夹是与用户配置、应用程序数据存储相关的一个系统文件夹,主要用于存储与特定用户账户(在此示例中是Administrator)相关的应用程序设置和数据。这个文件夹通常对用户来说是“隐藏”的,因此在默认情况下你可能无法直接看......
  • h5端IOS滑动不流畅问题怎么解决
    1.CSS动画和过渡避免重排:频繁修改DOM结构、样式或使用会引发重排的属性(如width、height、top等)会导致性能下降。尽量使用transform和opacity进行动画处理。css.smooth{ transition:transform0.3sease,opacity0.3sease;}Note:transition是一种css样......
  • 如何通过Python SDK更新Collection中已存在的Doc
    本文介绍如何通过PythonSDK更新Collection中已存在的Doc。说明若更新Doc时指定id不存在,则本次更新Doc操作无效如只更新部分属性fields,其他未更新属性fields默认被置为NonePythonSDK1.0.11版本后,更新Doc时vector变为非必填项前提条件已创建Cluster:创建Cluster。......
  • 将doc文件转换为docx文件
    将.doc文件转换为.docx文件通常不会导致兼容性变差,反而可能提升兼容性。以下是一些关键点:文件格式更新:.docx是Microsoft在2007年引入的新版文件格式,基于开放XML标准,具有更好的跨平台兼容性和开放性。与旧的.doc格式相比,.docx文件通常更小,支持更多的格式和功......
  • Meta AR 眼镜团队前负责人加入 OpenAI;visionOS 2.2 Beta 引入超宽屏投屏模式丨 RTE 开
       开发者朋友们大家好: 这里是「RTE开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享RTE(Real-TimeEngagement)领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「有看点的会议」,但内容仅代表编辑......
  • win11系统双击执行.bat文件不能执行,亲测有效
    起源:双击xxx.bat文件直接让我选择打开方式我记得是双击,就会直接运行才对,于是网上查询了半天都没解决,最终参考了这个博主的解决方法【点击跳转】,亲测解决了!!!还看到一个博主总结了好几种方法【点击跳转】1、win键+R键,在出来的框中输入regedit,打开注册表编辑器2、找到这个路径:计......
  • 使用platformio平台Arduino开发ESP32-C2
    使用platformio平台Arduino开发ESP32-C2有两种方法,推荐方法二。方法一:安装vscode后安装platformio插件(参考:YourGatewaytoEmbeddedSoftwareDevelopmentExcellence·PlatformIO安装时,需要可靠的网络链接。使用platformio创建一个esp32-c3项目(platformio平台默认......
  • INT2067 Introduction to Programming and Problem Solving
    Assignment1INT2067IntroductiontoProgrammingandProblemSolving2024-2025Semester1DueDate:October30,2024(Wednesday)1IntroductionInthisassignment,youneedtoimplementatext-basedgamebasedontheriddleaboutafarmerwhoneedstocros......
  • YUM源服务器搭建之详解(Detailed Explanation of Building a YUM Source Server)
      ......
  • Linux之Chronyd 时间服务器配置(Chronod Time Server Configuration in Linux)
      ......