JavaSE(十四)——文件操作和IO

news2024/11/20 10:08:59

文章目录

  • 文件操作和IO
    • 文件相关概念
    • Java操作文件
      • 文件系统操作
      • 文件内容操作
        • 字节流
          • FileOutputStream
          • FileInputStream
          • 代码演示
        • 字符流
          • FileWriter
          • FileReader
          • 代码演示
        • 缓冲流
        • 转换流
    • 案例练习

文件操作和IO

文件相关概念

文件 通常指的是包含用户数据的文件,如文本文件、图像文件、音频文件等。这些文件有具体的扩展名,存储实际的数据内容。

如下图:
在这里插入图片描述

上面描述的其实是狭义的文件,更广义的文件可以指代任何可以通过文件系统接口进行操作的资源(“一切皆文件”),在此描述下例如设备文件、管道、套接字等都被视为文件。

但是我们这里涉及的讨论主要还是针对狭义的文件。接下来,我们会介绍两个后续不断会提到的概念:目录路径

  1. 目录

    • 概念:目录是文件系统的组织结构,用于存储文件和其他目录。目录其实就是文件夹。
    • 目录是文件吗? 目录被视为文件,只不过它的内容是目录项而不是用户数据,目录实际上是一个包含目录项的特殊文件。
  2. 路径

    • 概念:用于描述文件系统中从根目录或当前目录到某个特定文件或目录的地址。路径可以用来区分或识别文件,例如某个路径:C:\Program Files\Java\jdk-17

    • 分类:路径分为绝对路径相对路径 两种类型。

      • 绝对路径:从根目录开始到目标文件或目录的完整路径。

        一个文件的绝对路径是唯一的,在大多数操作系统中,绝对路径总是以根目录(如Windows中的驱动器字母)开始。例如:C:\Users\dell\text.txt就是text.txt文件的绝对路径

      • 相对路径:相对路径是相对于当前工作目录的路径,它不从根目录开始。

        准确来说,相对路径需要一个基准路径,只不过这个基准路径通常是当前工作目录的路径,正因为如此,相对路径是非唯一的。


举个例子,详细解释一下相对路径:C:\Users\dell\text.txttext.txt的相对路径是什么,这要取决于基准路径的选择:

.表示当前所在的目录位置;..表示上一级目录

  1. 基准路径是 C:\Users\dell
    • 相对路径:text.txt 或者 .\text.txt
    • 解释:从C:\Users\dell开始,text.txt就在当前目录下
  2. 基准路径是 C:\Users
    • 相对路径:dell\text.txt 或者 .\dell\text.txt
    • 解释:从 C:\Users 开始,需要进入 dell 子目录才能找到 text.txt
  3. 基准路径是 C:\
    • 相对路径:Users\dell\text.txt 或者 .\Users\dell\text.txt
    • 解释:从C:\开始,需要依次进入Usersdell才能找到text.txt
  4. 基准路径是 C:\Users\dell\subdir
    • 相对路径:..\text.txt
    • 解释:从 C:\Users\dell\subdir 开始,需要返回上一级目录 C:\Users\dell 才能找到 text.txt

有关路径分隔符使用/(斜杠)还是\(反斜杠)的问题

  • /是通用的,大多数操作系统都将它作为路径分隔符

  • \是Windows操作系统特有的,并且通常是默认的(作者笔记本就是Windows操作系统,所以上面的介绍都采用了\)。Windows。

    同时,Windows也是支持/


为什么Windows会默认将\作为默认路径分隔符呢?

这其实是一个历史遗留问题,Windows的前身是DOS,DOS采用\作为路径分隔符,所以这个习惯就被保留下来了。


我们平时描述路径时尽量还是使用/(斜杠)作为路径分隔符,因为这是通用的,同时在许多现代编程语言中\作为转义字符,\\才能表示反斜杠,这是不方便的。


从开发的角度看,文件可以简单分为 文本文件二进制文件

所有的文件最终都是以二进制形式存储的。在这些文件中,有些文件的二进制数据可以按照特定的字符编码标准(如ASCII、UTF-8、UTF-16等)被解释为字符。当二进制数据恰好符合这些字符编码标准时,这些文件被称为文本文件;剩下的文件中的二进制数据不能直接被解释为字符,或者即使能被解释为字符,这些字符也没有意义,这些文件就是所谓的二进制文件

两者具体的对比:

文本文件二进制文件
可读性人类可读,可以直接用记事本查看和编辑人类不可读,需要特定的程序或工具来解析
文件大小通常较大,因为字符编码占用空间通常较小,因为直接存储原始数据
效率较低,需要更多的字节表示相同的信息较高,因为减少了编码解码的步骤
用途源代码文件(如.c.java)、日志文件(如.log)、配置文件(如.ini)等编译后的文件(如.class.exe)、库文件(如.so)、数据文件(如.bin)等

Java操作文件

在介绍文件操作前,我们必须先简单理解几个概念及其它们的关系,这些概念包括:IO

IO 是一个比较广泛的概念,指计算机的输入和输出操作。 是处理数据传输的一种抽象机制,分为字节流字符流。流实际上是对IO的进一步抽象。

  1. 字节流是用于处理二进制数据的流。字节流每次读取一个字节,适用于处理图片、音频等二进制文件。字符集不涉及字符编码转换,因此可以处理任何形式的二进制数据。
  2. 字符流是用于处理字符数据的流。每次读写一个字符(通常是Unicode字符),适用于处理文本文件。字符流自动处理字符编码的转换,使得处理文本数据十分方便。

字节流和字符流并不严格对应二进制文件和文本文件,而是根据数据的处理方式区分的。

例如,要复制一个文本文件,在这个场景下,我们并不关心文本文件的内容,因此直接使用字节流完成复制任务即可。

文件只是IO的一个应用领域,Java中有关文件的操作涉及到两个包java.iojava.nio,它们适合不同的应用场景,我们要介绍的是java.io中的一些常用组件。


文件系统操作

文件系统操作指的是创建文件、删除文件、移动文件、对文件重命名等不涉及文件内容的操作。File类是java.io包中唯一代表磁盘文件本身的组件,它定义了一系列文件系统操作方法。因此这一部分围绕 File 类展开。

构造方法

常见构造方法:

构造方法说明
File(String pathname)根据文件路径创建一个File实例,路径可以是绝对路径,也可以是相对路径
File(String parent, String child)将父路径字符串和子路径字符串组合起来,创建一个File实例
File(File parent, String child)将抽象的父路径和子路径字符串组合,创建一个File实例
  • 通过上述构造方法只是实例化了一个代表某个文件的File实例,并不会创建文件。 实例化File对象时,这个文件可以存在也可以不存在
//绝对路径创建File实例
File file1 = new File("C:/Demo/test1.txt");
//相对路径创建File实例
File file2 = new File("./test2.txt");
  • 根据相对路径创建的File实例默认选中当前工作目录,当前工作目录是由启动程序的方式决定的。 例如,如果采用IDEA启动,后续通过代码创建的文件会在项目目录中。

常用方法

方法说明
String getParent()返回 File 对象的父目录文件路径
String getName()返回 File 对象的纯文件名词
String getPath()返回 File 对象的文件路径
String getAbsolutePath()返回 File 对象的绝对路径
String getCanonicalPath()返回 File 对象的修饰过的路径
——————————(分隔线)
boolean exists()判断 File 对象描述的文件是否真实存在
boolean isDirectory()判断 File 对象代表的文件是否是一个已存在目录
boolean isFile()判断 File 对象代表的文件是否是一个已存在的普通文件
boolean createNewFile()根据 File 对象,创建一个文件。成功创建后返回true,如果文件已经存在,返回false
boolean delete()根据 File 对象,删除文件或空目录。如果目录中包含文件或其他子目录,delete() 方法将返回 false,表示删除操作失败。
void deleteOnExit()根据 File 对象,标注文件将被删除,删除行为会在JVM运行结束时进行
File createTempFile(String prefix, String suffix)根据prefix前缀字符串和suffix后缀字符串创建一个新的空文件,该文件在默认临时文件目录中。需要手动删除
——————————(分隔线)
String[] list()返回 File 对象代表的目录下的所有文件名,如果代表的不是目录,返回null
String[] list(FilenameFilter filter)返回指定目录中符合 FilenameFilter 过滤条件的所有文件名。
File[] listFiles()返回 File 对象代表的目录下的所有文件的File对象,如果代表的不是目录,返回null
File[] listFiles(FilenameFilter filter)返回指定目录中符合 FilenameFilter 过滤条件的所有文件的File对象。
boolean mkdir()创建 File 对象代表的目录,如果代表的不是目录,返回false,代表创建失败
boolean mkdirs()创建 File 对象代表的目录,如果必要,会创建中间目录
boolean renameTo(File dest)对 File 对象代表的文件重命名,dest代表新的文件或目录的 File 对象,类似于剪切、粘贴操作
boolean canRead()判断用户是否对文件有可读权限
boolean canWrite()判断用户是否对文件有可写权限
  • 如表格演示,将常用方法分成三部分,演示时也会分成三部分。

【第一部分演示】

public class Demo5 {
    public static void main(String[] args) throws IOException {
        File file1 = new File("C:/Demo/test1.txt");
        File file2 = new File("./test2.txt");

        System.out.println("fil1的打印演示:");
        System.out.println(file1.getParent());
        System.out.println(file1.getName());
        System.out.println(file1.getPath());
        System.out.println(file1.getAbsolutePath());
        System.out.println(file1.getCanonicalPath());
        
        System.out.println();
        
        System.out.println("file2的打印演示:");
        System.out.println(file2.getParent());
        System.out.println(file2.getName());
        System.out.println(file2.getPath());
        System.out.println(file2.getAbsolutePath());
        System.out.println(file2.getCanonicalPath());
    }
}

在这里插入图片描述

  • 通过打印结果可以看出:
    1. getParent()返回的父路径形式与构造File对象时的参数有关,如file2的返回值是.
    2. getName()返回的就是文件名
    3. getPath()返回的结果也与构造File对象的参数有关
    4. getAbsolutePath()返回绝对路径,但仍可能存在符号链接、相对路径以及...
    5. getCanonicalPath()返回规范路径,消除了符号链接、相对路径、...等,同时如果文件不存在或路径无效,可能会抛出 IOException异常。

【第二部分演示】

public class Demo6 {
    public static void main(String[] args) throws IOException {
        File file = new File("C:/Demo/test.txt");
        //判断是否真实存在
        System.out.println(file.exists());

        //创建出来
        file.createNewFile();

        //判断类型
        System.out.println(file.isFile());
        System.out.println(file.isDirectory());

        //删除并判断是否成功
        file.delete();
        System.out.println(file.exists());

    }
}

在这里插入图片描述


delete()方法只能删除普通文件和空目录,如果不是空目录,将无法删除。 如果想要删除一个非空目录,必须递归逐个删除。

    private static void deleteDir(File dir) {
        if(!dir.isDirectory()) {
            return;
        }
        File[] files = dir.listFiles();
        //空目录
        if(files == null) {
            dir.delete();
        }else {
            for(int i = 0; i < files.length; i++) {
                if(files[i].isFile()) {
                    files[i].delete();
                }else {
                    deleteDir(files[i]);
                }
            }
        }
        //删除自己
        dir.delete();
    }

deleteOnExit()方法 和 createTempFile(String prefix, String suffix)通常配合使用,以确保临时文件在程序退出时自动删除。

public class Demo8 {
    public static void main(String[] args) throws IOException {
        //创建临时文件
        File tempFile = File.createTempFile("test", ".txt");
        //查看是否创建成功
        System.out.println(tempFile.exists());
        //设置JVM运行结束后删除
        tempFile.deleteOnExit();
        //判断JVM结束前,deleteOnExit()后,是否存在
        System.out.println(tempFile.exists());
    }
}

在这里插入图片描述


【第三部分演示】

list()listFiles()以及它们的重载版本都不会递归地返回子目录的内容,只返回当前目录下的文件和子目录的列表。

list()listFiles()的带参数的重载版本list(FilenameFilter filter)listFiles(FilenameFilter filter)中的FilenameFilter是一个函数式接口,称为文件过滤器。只有一个抽象方法boolean accept(File dir, String name),用来指定文件名的过滤规则,符合条件的(规则下返回true)文件名将被保留,不符合过滤条件的(规则下返回false)的文件将被过滤掉,带FilenameFilter文件过滤器类型参数的list()listFiles()方法最终会返回符合过滤条件的所有文件名或者文件的File对象。

public class Demo9 {
    public static void main(String[] args) {
        File file = new File("C:/Demo");

        //使用没有文件过滤器的list方法返回C:/Demo目录下的所有文件名
        String[] files1 = file.list();

        //使用带有文件过滤器的list方法过滤出名字包含"test"的文件名
        String[] files2 = file.list((dir, name) -> name.contains("test"));

        //打印查看效果
        System.out.println(Arrays.toString(files1));
        System.out.println(Arrays.toString(files2));
    }
}

在这里插入图片描述

前面的删除非空目录的代码中其实已经包含了遍历目录中所有文件的操作,就是利用listFiles()拿到所有的文件对象数组,然后遍历对象数组,如果是目录,就递归遍历这个目录,整体就是一个递归方法,这里不再演示。


public class Demo10 {
    public static void main(String[] args) {
        File dir = new File("C:/Demo/newSubDemo1");//这个File实例代表的文件的中间目录Demo已经存在
        File dirs = new File("C:/Demo/newSubDemo2/newSubDemo3");//这个File实例代表的文件的中间目录newSubDemo2不存在

        System.out.println("Demo目录是否存在:" + dir.getParentFile().exists());
        System.out.println("newSubDemo2目录是否存在:" + dirs.getParentFile().exists());
        System.out.println("newSubDemo1目录是否存在:" + dir.exists());
        System.out.println("newSubDemo3目录是否存在:" + dirs.exists());
        System.out.println("使用mkdir创建中间目录均存在的目录,是否成功:" + dir.mkdir());
        System.out.println("使用mkdir创建中间目录不存在的目录,是否成功:" + dirs.mkdir());
        System.out.println("使用mkdirs创建中间目录不存在的目录,是否成功:" + dirs.mkdirs());
    }
}

在这里插入图片描述

  • 上述代码验证了:mkdir()mkdirs()方法均能创建目录,但mkdir()方法不能创建中间目录不存在的目录,而mkdirs()方法在必要时能够创建中间目录。

public class Demo11 {
    public static void main(String[] args) {
        File srcFile = new File("C:/Demo/demo1.txt");
        File dstFile = new File("C:/Demo/newDemo1.txt");
        System.out.println(srcFile.exists());
        System.out.println(dstFile.exists());
        System.out.println(srcFile.renameTo(dstFile));
    }
}

在这里插入图片描述

  • renameTo(File dest)方法的调用者代表的文件 和 参数代表的文件的关系:

    调用者:表示要被重命名的源文件或目录。

    参数:表示新的文件名或者路径。

  • 如果调用者代表的文件不存在,则方法直接返回false

  • 如果参数代表的文件已经存在,在Windows系统下不会覆盖已有文件,即方法返回false

  • 明确renameTo()方法不只是重命名,它有类似于剪切粘贴的功能


文件内容操作

文件内容操作即对文件的内容进行操作,包括读写文件。对文件内容的操作是通过流实现的,可以通过字节流字符流操作文件内容,同时使用缓冲流提高读写效率,还可以使用转换流实现字节流和字符流的转换。因此我们将讨论java.io中与这四种流相关的组件。

在开始介绍前,我们得先理解什么是输入什么是输出,输入和输出是站在CPU的角度考虑的,而不是站在我们的角度,因此:

  • 读文件是输入
  • 写文件是输出

字节流

InputStreamOutputStream 类是 java.io 包中与字节流相关的所有组件的基类。这两个类是抽象类,提供了处理字节流的基本方法和框架。与文件相关的字节流类主要是 FileInputStreamFileOutputStream。这两个类分别用于从文件中读取字节和将字节写入文件。

为了方便演示,我们会在介绍完FileInputStreamFileOutputStream后一起演示。


FileOutputStream

常用构造方法

构造方法说明
FileOutputStream(String name)根据字符串构造,默认的覆盖写入模式
FileOutputStream(String name, boolean append)根据字符串构造,可以指定模式:true(追加模式);false(覆盖写入模式)
FileOutputStream(File file)根据File对象构造,默认的覆盖写入模式
FileOutputStream(File file, boolean append)根据File对象构造,可以指定模式:true(追加模式);false(覆盖写入模式)
  • 如果用于构造FileOutputStream的字符串或File对象所指向的文件不存在,那么在第一次尝试写入时,Java 会自动创建这个文件。
  • 如果文件已经存在,并且是覆盖写入模式,那么文件中的原有内容将被删除,新的内容将从头开始写入;如果是追加模式,那么新写入的数据将会被添加到文件的末尾,原有的内容将被保留。
  • 如果试图使用 FileOutputStream 写入一个目录而不是一个文件,Java 将抛出一个 FileNotFoundException 异常。

常用方法

方法说明
void write(int b)写入最低的8位(即最低的一个字节)
void write(byte b[])将 b 字节数组中的数据全部写入
void write(byte b[], int off, intlen)将 b 字节数组中从 off 开始的 len 个数据写入
void close()关闭字节流
void flush()立即刷新输出流,将缓冲区中的数据立即写入
  • 完成文件操作后一定要调用close()方法释放资源,否则可能会出现文件资源泄露的问题。

  • 实际情况中很少使用flush()方法,flush()方法一般在close()时自动调用。

  • FileOutputStream类(包括后面的FileInputStream等类)代表字节流,但为什么write(int b)需要一个int类型的参数?

    1. 表示范围和返回值

      采用int类型可以确保方法能够处理更大的范围,尽管write(int b)只会写入最低位的一个字节。同时,某些情况下write方法可能需要返回值来表示写入操作的结果,尽管实际上write方法没有返回值,但这种设计为未来的扩展留下了空间

    2. 历史遗留

      Java的I/O流设计借鉴了C语言的I/O库。C语言习惯使用int类型。

  • write(byte b[])write(int b)更常用,因为前者的字节数组一次可以写入多个字节,减少了系统调用次数,提高了写入效率;字节数组作为缓存,批量写入数据,减少了文件I/O次数,提高了I/O操作的性能;处理更复杂的数据,例如从网络接收的数据通常都是以字节数组的方式存在的。

浅谈文件资源泄露问题

每个进程都会有一个PCB来描述进程中的某些属性,其中就包含文件描述符表。每当打开一个文件,都会申请一个表项,如果我们打开了大量的文件但是不关闭释放资源时,文件描述符表就会爆满进而发生错误,即出现了严重的文件资源泄露问题。


tip: 文件描述符表不会自动扩容。这是因为:

  1. 操作系统有资源限制,不允许单个进程申请过多的资源。
  2. 文件描述符表是操作系统内核的一部分,内核空间的内存管理十分严格,不允许随意扩展。
  3. 另外,如果允许一个文件描述符表过大,那么整个表的查找和管理性能就会大打折扣。

FileInputStream

常用构造方法

构造方法说明
FileInputStream(String name)根据文件路径构造文件输入流
FileInputStream(File file)根据 File 对象构造文件输入流
  • 如果所指向的文件不存在 或者 文件存在但是一个目录,Java将会抛出FileNotFoundException异常

常用方法

方法说明
int read()从输入流中读取一个字节的数据,返回 -1 表示读完
int read(byte b[])从输入流中最多读取 b.length 个字节的数据到字节数组b中,返回实际读取的数量, -1 代表读完
int read(byte b[], int off, int len)从输入流中读取最多 len 个字节的数据,读到的数据存放到字符数组b中,从off位置开始存,方法返回实际读取的数量,-1 代表读完
void close()关闭字节流
  • 有参数的read方法读到的数据都存放在参数byte b[]字符数组中,即b数组既作为参数,又用于“返回值”,这种参数我们称之为 输出型参数。之所以可以这么做,是因为数组类型实际上是一个引用类型。

    可以这么理解输出型参数:字符数组想象成一个饭盒,read方法想象成餐厅,我们将饭盒给餐厅(打饭阿姨),餐厅就会还给我们一个盛满饭的饭盒。

  • 一次读取多个字节的read方法会更常用,原因是字节数组可以作为缓存数组,同上介绍。

  • 读操作执行完毕后及时close()


代码演示

Java 7引入了 try-with-resources 语法,旨在简化资源管理,特别是对于那些必须显式关闭以防止资源泄漏的对象,如我们接下来要讲的文件输入输出流。

try-with-resources 语法特性:

  • 使用方法try后的括号内实例化资源对象,仍可以使用catch捕获异常以及finally语句。同时,try括号内可以声明多个资源,每个资源之间用分号隔开。

    try (ResourceType resource = new ResourceType()) {
        // 使用资源的代码
    } catch (ExceptionType1 e1) {
        // 处理异常
    } finally {
        // 可选的finally块
    }
    
  • 使用条件

    1. 资源对象必须在try括号内初始化(创建)
    2. 必须实现 AutoCloseable 接口(意味着包括所有实现了 Closeable 接口的类)
  • 使用优势:自动关闭资源而不需要手动调用close方法,资源(对象)会在try代码块结束时自动关闭,不论是否发生异常。这减少了由于关闭资源而造成的潜在错误或内存泄漏风险,使得程序员可以更专注于业务逻辑。

public class Demo14 {
    public static void main(String[] args) throws FileNotFoundException {
        //写文件
        try(FileOutputStream outputStream = new FileOutputStream("D:/DemoFile/INNER/demo.txt")) {
            outputStream.write(97);
            outputStream.write(98);
            outputStream.write('B');
            outputStream.write(new byte[]{'a', 'b', 'c'});
            outputStream.write('难');
        } catch (IOException e) {
            e.printStackTrace();
        }

        //读文件
        try(FileInputStream inputStream = new FileInputStream("D:/DemoFile/INNER/demo.txt")) {
            while(true) {
                byte[] buf = new byte[1024];
                int r = inputStream.read(buf);
                if(r == -1) {
                    break;
                }
                for(int i = 0; i < r; i++) {
                    System.out.println(buf[i]);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

  • 这段代码使用 try-with-resources 语法同时演示了FileOutputStreamFileInputStream,以覆盖写入的方式尝试写入:97、98、‘B’、‘a’、‘b’、‘c’、‘难’,读文件并打印结果如图。

    前6行打印分别对应97、98、‘B’、‘a’、‘b’、‘c’,而最后一行打印的结果是-66(对应尝试写入’难’),具体原因是:

    汉字字符通常占2~4个字节,而write(int b)方法实际只会写入最低的8位(一个字节),这就导致汉字字符的数据写入不完整,只写入了最低的一个字节,因此打印时只打印出一个字节的内容,-66就是汉字的最低8位的内容,这块内容是没有任何实际意义的。

  • 列举一种正确写入汉字字符的方法:

    public class Demo15 {
        public static void main(String[] args) {
            //向文件写入汉字字符
            try(FileOutputStream fileOutputStream = new FileOutputStream("D:/DemoFile/INNER/demo.txt")) {
                String test = "你好";
                byte[] data = test.getBytes();
                fileOutputStream.write(data);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            //读文件并以十六进制打印
            try(FileInputStream fileInputStream = new FileInputStream("D:/DemoFile/INNER/demo.txt")) {
                while(true) {
                    byte[] buf = new byte[1024];
                    int r = fileInputStream.read(buf);
                    if(r == -1) {
                        break;
                    }
                    for(int i = 0; i < r; i++) {
                        System.out.printf("%x\n", buf[i]);
                    }
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    在这里插入图片描述

    针对汉字(或包含汉字)字符串,使用getBytes()得到对应的字节数组,再利用write(byte b[])方法写入文件,此时就不会发生数据写入不完整的问题了;打印时采用十六进制的方式打印结果如上图,e4、bd、a0 代表 ‘你’,e5、a5、bd 代表 ‘好’,这里采用了UTF-8的编码方式,我们可以从编码转换网站中验证一下:

    在这里插入图片描述


字符流

前面提到,采用字节流的方式省去了编码解码的过程,性能相对较快,但是我们发现如果以字节流的方式读取文件,得到的结果是不可读的,需要手动查询,如果想要程序返回可读结果,我们还需要手动编写解码程序,解码工作是相对复杂且难以理解的 并降低了开发效率。因此,实际情况下我们更常用字符流的方式读文件,解码和编码的转换工作会由底层封装的逻辑自动进行。

接下来我们开始介绍字符流相关API:WriterReader 类是 java.io 包中与字符流相关的所有组件的基类。这两个类是抽象类,提供了处理字符流的基本方法和框架。与文件相关的字符流类主要是 FileReaderFileWriter。这两个类用于以字符为单位进行文件读写操作。


FileWriter

常用构造方法

构造方法说明
FileWriter(String fileName)根据字符串构造,默认的覆盖写入模式
FileWriter(String fileName, boolean append)根据字符串构造,可以指定模式:true(追加模式);false(覆盖写入模式)
FileWriter(File file)根据File对象构造,默认的覆盖写入模式
FileWriter(File file, boolean append)根据File对象构造,可以指定模式:true(追加模式);false(覆盖写入模式)
  • 注意问题与FileOutputStream类相同(FileWriter 内部使用了 FileOutputStream):自动创建、追加模式和覆盖写入模式、异常的抛出。

常用方法

方法说明
void write(int c)接受一个 int 类型,将其转换为字符后写入文件
void write(String str)将整个字符串写入文件
void write(char[] cbuf)将字符数组中的所有元素写入文件
void write(String str, int off, int len)从 off 位置开始,将字符串 str 写入文件,共写入 len 个字符
void write(char[] cbuf, int off, int len)从 off 位置开始,将字符数组写入文件,共写入 len 个元素(字符)
void close()关闭字符流
void flush()立即刷新输出流,将缓冲区中的数据立即写入
  • 同样的注意问题:
    1. 及时close释放资源,避免文件资源泄露
    2. 通常使用一次性写入多个字符的write方法,原因就在于减少系统调用次数并有缓存作用

FileReader

常用构造方法

构造方法说明
FileReader(String fileName)根据文件路径构造文件输入流
FileReader(File file)根据 File 对象构造文件输入流

常用方法

方法说明
int read()从输入流中读取一个字符并返回其Unicode编码形式的 int 值,如果到达文件末尾,则返回 -1
int read(char[] cbuf)从输入流中读取字符并尽量填满 cbuf 数组,方法返回实际读取的数量,到达文件末尾返回 -1
int read(CharBuffer target)从输入流中读取字符放进 target 缓冲区中,返回实际读取的字符数量,到达文件末尾时返回 -1
int read(char[] cbuf, int off, int len)从输入流中最多读入 len 个字符,读取到的字符从 cbuf 的 off 位置开始放,返回实际读取的字符数量,到达文件末尾时返回 -1
void close()关闭字符流
  • CharBuffer类相当于对char[]进行了封装,可以保存字符数据,实际上是一个缓冲区。
  • 及时close释放资源

代码演示
public class Demo15 {
    public static void main(String[] args) throws IOException {
        //写文件
        try(FileWriter writer = new FileWriter("D:/DemoFile/INNER/newDemo.txt")) {
            writer.write("你好,字符流");
            writer.write(new char[]{'h', 'e', 'l', 'l', 'o'});
        }

        //读文件
        try(FileReader reader = new FileReader("D:/DemoFile/INNER/newDemo.txt")) {
            while(true) {
                char[] cBuf = new char[100];
                int r = reader.read(cBuf);
                if(r == -1) {
                    break;
                }
                for(int i = 0; i < r; i++) {
                    System.out.print(cBuf[i]);
                }
            }
        }
    }
}

在这里插入图片描述

  • 前面按照字节流读取一个汉字是3个字节,而现在却“变成”2个字节,其实这两种方式都是正确的。

    字节流读取的是原始数据即3个字节;而字符流在读取的时候会根据文件的内容编码格式进行解析,返回时针对3个字节进行了转码(用这三个字节查询到其指代的汉字,又将汉字的unicode编码值查询出来),最终将编码值返回到char变量(2个字节)中。


缓冲流

缓冲流 是一种用于提高输入输出操作效率的流。缓冲流通过在内存中维护一个缓冲区,减少了对底层系统调用的次数,从而提高了 I/O 操作的性能。Java 提供了多种缓冲流,包括字节缓冲流(BufferedInputStreamBufferedOutputStream)和字符缓冲流(BufferedReaderBufferedWriter)。

缓冲流可以显著提高 Java 程序在处理大量数据或频繁进行 I/O 操作时的性能,但是对于小文件就没有必要使用缓冲流了,因为性能提升效果不明显,甚至可能引入额外的开销。

四个缓冲流相关的类上面已经提到,分别对应到上面介绍的四个类,那具体如何使用呢?我们先介绍一下它们的构造方法(四个类的构造方法十分相似,我们放在一个表格里):

构造方法说明
BufferedInputStream(InputStream in)创建一个新的字节缓冲输入流
BufferedInputStream(InputStream in, int size)创建一个新的字节缓冲输入流,并指定缓冲区大小
BufferedOutputStream(OutputStream out)创建一个新的字节缓冲输出流
BufferedOutputStream(OutputStream out, int size)创建一个新的字节缓冲输出流,并指定缓冲区大小
BufferedReader(Reader in)创建一个新的字符缓冲输入流
BufferedReader(Reader in, int sz)创建一个新的字符缓冲输入流,并指定缓冲区大小
BufferedWriter(Writer out)创建一个新的字符缓冲输出流
BufferedWriter(Writer out, int sz)创建一个新的字符缓冲输出流,并指定缓冲区大小

若想要使用缓冲流对文件执行读写操作,读写方法不变,只是需要套个壳而已, 即:将FileInputStreamFileOutputStreamFileReaderFileWriter这四个类的对象传入对应的缓冲流构造方法,构造出一个缓冲流对象,用这个对象调用之前的读或写方法。

不过需要注意的是,缓冲流也需要及时关闭释放资源,即:需要将构造对象放到try括号里。


【具体演示】

public class Demo16 {
    public static void main(String[] args) {
        //写文件
        try(FileOutputStream outputStream = new FileOutputStream("D:/DemoFile/INNER/IN/demo1.txt");
            BufferedOutputStream bufferedOutput = new BufferedOutputStream(outputStream)) {
            for(int i = 97; i <= 122; i++) {
                bufferedOutput.write(i);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        //读文件
        try(FileReader reader = new FileReader("D:/DemoFile/INNER/IN/demo1.txt");
            BufferedReader bufferedReader = new BufferedReader(reader)) {
            while(true) {
                char[] cBuf = new char[100];
                int r = bufferedReader.read(cBuf);
                if(r == -1) {
                    break;
                }
                for(int i = 0; i < r; i++) {
                    System.out.print(cBuf[i]);
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

  • 代码中使用了缓冲流的方式读写文件,但是后续的案例练习不会使用缓冲流,这是因为我们不涉及大文件,如果使用也就直接套个壳即可。

转换流

转换流InputStreamReaderOutputStreamWriter)在 Java 中用于在字节流和字符流之间进行转换。它们特别适用于处理涉及字符编码的输入输出操作。

转换流的使用场景

  • 文件操作:在处理文本文件时,特别是需要指定字符编码的场景下,使用转换流是常见的做法。

    举例解释:当文件编码与平台默认编码不一致,此时采用FileReader读取文件,会出现乱码。

  • 网络通信:在网络通信中,为了确保数据的正确性和一致性,使用转换流是必要的。

  • 多语言和国际化:在处理多语言和国际化数据时,使用转换流是必不可少的。

如果平台默认编码与文件编码不一致,FileOutputStreamFileWriterFileInputStreamFileReader都会出现乱码情况。

  • FileWriter:使用平台默认的字符编码来写入文件。如果文件需要使用特定的字符编码(例如 UTF-8),而平台默认编码不同(例如 GBK),则会导致乱码。
  • FileOutputStream:写入字节数据。如果你直接写入字符串而不指定字符编码,也会导致乱码。String.getBytes() 方法在没有指定字符编码时,会使用平台默认编码。
  • FileReader:使用平台默认的字符编码来读取文件。如果文件的实际编码与平台默认编码不同,读取时会出现乱码。
  • FileInputStream:读取的是字节数据,不会直接处理字符编码问题。但如果你直接将字节数据转换为字符串而不指定字符编码,也会导致乱码。

基于这样的场景,我们就需要转换流来指定需要的编码方式。


根据使用场景不难理解,对于我们初学者/学生来说很少用到转换流,关于两个类:

  • InputStreamReader将字节输入流转换为字符输入流
  • OutputStreamWriter将字符输出流转换为字节输出流

【构造方法】

构造方法说明
InputStreamReader(InputStream in)
OutputStreamWriter(OutputStream out)
使用平台默认字符编码
InputStreamReader(InputStream in, Charset cs)
OutputStreamWriter(OutputStream out, Charset cs)
使用指定的字符编码
InputStreamReader(InputStream in, String charsetName)
OutputStreamWriter(OutputStream out, String charsetName)
使用指定的字符编码名称
  • 当采用只有一个参数构造方法时,效果和直接使用FileReader等类一致,都使用平台默认编码。

这里只演示了构造方法,具体如何使用,与缓冲流类似,也是套个壳,然后指定一下字符编码即可,具体参照接下来的演示代码理解。


【具体演示】

文件读写场景:

  • 读取文本文件:特别是当文件的编码不是平台默认编码时,使用 InputStreamReader 可以确保正确读取文件内容。
  • 写入文本文件:同样,当需要写入特定编码的文件时,使用 OutputStreamWriter 可以确保数据的正确性。
// 写文件,指定 UTF-8 编码
try (FileOutputStream fos = new FileOutputStream("example.txt");
     OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
     BufferedWriter bw = new BufferedWriter(osw)) {
    String content = "Hello, World!";
    bw.write(content);
} catch (IOException e) {
    e.printStackTrace();
}

// 读文件,指定 UTF-8 编码
try (FileInputStream fis = new FileInputStream("example.txt");
     InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
     BufferedReader br = new BufferedReader(isr)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
  • 代码举例演示了以UTF-8的方式读写文件。

总之,当平台默认编码与文件编码不一致时,为了避免乱码问题,你需要确保在读取和写入文件时使用与文件实际编码相同的编码。(使用转换流)


案例练习

结束了文件系统操作和文件内容操作后,我们实现几个小案例巩固一下。

【案例一】

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

public class Demo2 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入您要扫描的目录:");
        String scanDir = scanner.next();
        File dir = new File(scanDir);
        if(!dir.isDirectory()) {
            System.out.println("您输入的不是目录");
            return;
        }
        System.out.println("请输入您要查找的关键字:");
        String keyWord = scanner.next();
        scanFiles(dir, keyWord);
    }

    private static void scanFiles(File scanDir, String keyWord) {
        File[] files = scanDir.listFiles();
        for(int i = 0; i < files.length; i++) {
            if(files[i].isFile()) {
                if(files[i].getName().contains(keyWord)) {
                    dealFile(files[i]);
                }
            }else {
                scanFiles(files[i], keyWord);
            }
        }
    }

    private static void dealFile(File file) {
        System.out.println("找到文件" + file.getAbsolutePath() + "是否删除?(y/n)");
        Scanner scanner = new Scanner(System.in);
        while(true) {
            String input = scanner.next();
            if(input.equals("y")) {
                file.delete();
                return;
            }else if(input.equals("n")) {
                return;
            }else {
                System.out.println("输入非法!请输入y/n");
            }
        }
    }
}
  • 与之前递归删除非空目录的思想差不多:列出目录下的所有文件,遍历,如果是普通文件判断是否包含指定字符串;如果是目录,则递归。

【案例二】

进行普通文件的复制(注意是复制而不是剪切粘贴)

public class Demo3 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入源文件路径:");
        String srcPath = scanner.next();
        File src = new File(srcPath);
        if(!src.isFile()) {
            System.out.println("路径错误!非文件!");
            return;
        }
        System.out.println("请输入目标文件路径:");
        String dstPath = scanner.next();
        File dst = new File(dstPath);
        if(!new File(dst.getParent()).isDirectory()) {
            System.out.println("目标路径错误!");
            return;
        }
        try(InputStream inputStream = new FileInputStream(src); OutputStream outputStream = new FileOutputStream(dst)) {
            byte[] buf = new byte[1024];
            while(true) {
                int n = inputStream.read(buf);
                if(n == -1) {
                    break;
                }
                outputStream.write(buf, 0, n);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 判断输入合法,文件内容操作,边读边写

【案例三】

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

public class Demo4 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要搜索的路径: ");
        String rootPath = scanner.next();
        File rootFile = new File(rootPath);
        if (!rootFile.isDirectory()) {
            System.out.println("输入的路径不是目录!");
            return;
        }
        System.out.println("请输入要搜索的关键字: ");
        String keyword = scanner.next();

        scanDir(rootFile, keyword);
    }

    private static void scanDir(File rootFile, String keyword) {
        // 1. 列出当前目录下所有的内容
        File[] files = rootFile.listFiles();
        if (files == null) {
            // 当前目录为空, 直接返回
            return;
        }
        // 2. 遍历当前目录下所有的文件
        for (File file : files) {
            if (file.isFile()) {
                // 是普通文件
                dealFile(file, keyword);
            } else {
                // 是目录, 递归调用
                scanDir(file, keyword);
            }
        }
    }

    private static void dealFile(File file, String keyword) {
        // 1. 判定文件名是否包含关键字
        if (file.getName().contains(keyword)) {
            // 包含关键字, 打印文件名
            System.out.println("文件名包含关键字:" + file.getAbsolutePath());
            return;
        }
        // 2. 判定文件内容是否包含. 由于 keyword 是字符串. 就按照字符流的方式来处理.
        StringBuilder stringBuilder = new StringBuilder();
        try (Reader reader = new FileReader(file)) {
            while (true) {
                char[] chars = new char[1024];
                int n = reader.read(chars);
                if (n == -1) {
                    break;
                }
                stringBuilder.append(chars, 0, n);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 3. 判定 stringBuilder 是否包含关键字
        if (stringBuilder.indexOf(keyword) >= 0) {
            // 包含关键字
            System.out.println("文件内容包含关键字: " + file.getAbsolutePath());
        }
        return;
    }
}
  • 注意:现在的方案性能较差,尽量不要在太复杂的目录下或者大文件下实现

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt桌面应用开发 第五天(常用控件)

目录 1.QPushButton和ToolButton 1.1QPushButton 1.2ToolButton 2.RadioButton和CheckBox 2.1RadioButton单选按钮 2.2CheckBox多选按钮 3.ListWidget 4.TreeWidget控件 5.TableWidget控件 6.Containers控件 6.1QScrollArea 6.2QToolBox 6.3QTabWidget 6.4QStacke…

Vue.js 插槽 Slots 实际应用 最近重构项目的时候遇到的...

前端开发中 插槽 Slots 是一个重要的概念 我们可以查看一下vue.js的官方文档 https://cn.vuejs.org/guide/components/slots 类似于连接通道一样 可以把核心代码逻辑搬到另外的地方 做一个引用 而原先的地方可能并不能这样书写 对于这个概念我在vue的官方文档里面找到了…

ubuntu pytorch容器内安装gpu版本的ffmpeg

一、基础镜像和生成容器 pytorch/pytorch &#xff1a;1.13.1-cuda11.6-cudnn8-devel 生成容器&#xff0c;一定要加NVIDIA_DRIVER_CAPABILITIEScompute,utility,video,graphics&#xff0c;否则侯建无法推流&#xff0c;报错缺少编码之类的。 docker run -it --gpus all -e …

VSCode+ESP-IDF开发ESP32-S3-DevKitC-1(2)第一个工程 LED心跳灯

VSCodeESP-IDF开发ESP32-S3-DevKitC-1&#xff08;2&#xff09;第一个工程 LED心跳灯 前言1.新建工程2.编写控制LED代码3.LED控制独立成.c和.h文件 前言 实际开发中很多时候我们需要有一个类似心跳灯或运行指示灯的灯以不同的状态闪烁以表示程序的运行状态&#xff0c;所以第…

在ubunto18.04安装node 14.16.0

这里演示安装node 14.16.0&#xff0c;其他版本也一样的安装步骤&#xff0c;需要注意1 1.检查本机服务器类型&#xff0c;有的是x64&#xff0c;有的是ARM&#xff0c;先查清楚是什么类型再进行下载&#xff0c;否则会存在编译出错的问题 bash: /opt/node-v14.16.0-linux-x6…

基于YOLOv8深度学习的智慧农业猪行为检测系统研究与实现(PyQt5界面+数据集+训练代码)

随着智慧农业的快速发展&#xff0c;畜牧业的智能化管理已逐渐成为提高生产效率、提升动物福利、降低运营成本的关键手段之一。在此背景下&#xff0c;畜牧场对动物行为的自动化监测需求日益增长&#xff0c;尤其是在大型养猪场&#xff0c;猪群的日常行为检测对于疾病预防、饲…

独立资源池与共享资源池在云计算中各自的优势

在云计算领域&#xff0c;独立资源池和共享资源池是两种关键的资源管理策略&#xff0c;它们各自具有独特的优势&#xff0c;以适应不同的业务需求和场景。 独立资源池的优势 资源独占性&#xff1a;独立资源池为特定应用或用户提供专属的资源&#xff0c;这意味着资源不会被其…

异或和之和

//暴力做法 枚举每个子区间 O(n^3) //优化1 利用前缀异或和快速求出区间异或和 O(n^2) //优化2 处理位运算的常用方法&#xff1a;拆位法 常用的思想&#xff1a;贡献法思想 下面详见优化2&#xff1a; 1.拆位贡献法 2.实战真题1 题目链接&#xff1a;1.异或和之和 - 蓝桥…

【金融风控项目-07】:业务规则挖掘案例

文章目录 1.规则挖掘简介2 规则挖掘案例2.1 案例背景2.2 规则挖掘流程2.3 特征衍生2.4 训练决策树模型2.5 利用结果划分分组 1.规则挖掘简介 两种常见的风险规避手段&#xff1a; AI模型规则 如何使用规则进行风控 **使用一系列逻辑判断(以往从职人员的经验)**对客户群体进行区…

第8章硬件维护-8.2 可维护性和可靠性验收

8.2 可维护性和可靠性验收 可维护性和可靠性验收非常重要&#xff0c;硬件维护工程师在后端发现问题后&#xff0c;总结成可维护性和可靠性需求&#xff0c;在产品立项的时候与新特性一起进行需求分析&#xff0c;然后经过设计、开发和测试环节&#xff0c;在产品中落地。这些需…

在k8s上部署minio

一、 环境 已部署k8s&#xff0c;支持helm部署 二、添加Minio Helm Chart仓库 helm repo add bitnami https://charts.bitnami.com/bitnami -n your_namespace helm repo update -n your_namespace部署带tls的minio helm install minio-s3 bitnami/minio -n your_namespace…

gtest 框架

基本了解 google提供的一个C测试框架&#xff0c;主要就是简化测试单元的书写&#xff0c;具有高效、灵活可拓展的特点 主要特点 简单易用&#xff1a;gtest 提供了清晰且易于使用的 API&#xff0c;便于开发者快速编写单元测试。丰富的断言支持&#xff1a;gtest 提供了多种断…

机器学习—误差分析

帮助运行诊断的最重要的方法是选择下一步要尝试的内容&#xff0c;提高你的学习算法性能&#xff0c;偏差和方差可能是最重要的想法&#xff0c;然后是错误分析。 假设Mcv500&#xff0c;即有500个交叉验证示例&#xff0c;你的算法错误的分类了100个&#xff0c;错误分析过程…

微知-如何查看BlueField DPU上的内存信息,包括内存主频和位宽?(dmidecode -t memory)

背景 在定位DPU上网卡性能的时候&#xff0c;可能涉及到查看内存的主频、位宽、电压等信息&#xff0c;如何快速查看&#xff1f; 命令 dmidecode -t memory实操 可以看到主频是 3200MT/s&#xff0c;另外还能看到位宽&#xff0c;大小&#xff0c;电压等信息。

【AI系统】AI系统架构的组成

AI 系统组成 如图所示&#xff0c;大致可以将 AI 系统分为以下几个具体的方向&#xff1a; AI 训练与推理框架 AI 框架不仅仅是指如 PyTorch 等训练框架&#xff0c;还包括推理框架。其负责提供用户前端的 AI 编程语言&#xff0c;接口和工具链。负责静态程序分析与计算图构建…

竞赛思享会 | 2024年第十届数维杯国际数学建模挑战赛D题【代码+演示】

Hello&#xff0c;这里是Easy数模&#xff01;以下idea仅供参考&#xff0c;无偿分享&#xff01; 题目背景 本题旨在通过对中国特定城市的房产、人口、经济、服务设施等数据进行分析&#xff0c;评估其在应对人口老龄化、负增长趋势和极端气候事件中的韧性与可持续发展能力。…

机器学习基础07

目录 1.逻辑回归 1.1原理 1.2API 2.K-Means 2.1算法过程 2.2API 3.SVM&#xff08;支持向量机&#xff09; 3.1算法原理​ 3.2API 1.逻辑回归 逻辑回归(Logistic Regression)是机器学习中的一种分类模型&#xff0c;逻辑回归是一种分类算法。 1.1原理 逻辑回归的输…

基于python的在线投票系统小程序u9t2g.

目录 项目介绍开发技术具体实现截图微信开发者工具介绍技术路线开发语言以及框架介绍python-flask核心代码部分展示python-django核心代码部分展示详细视频演示源码获取 项目介绍 投票系统用户端是基于微信小程序&#xff0c;管理员端是基于web网页端&#xff0c; &#xff0c…

android 使用MediaPlayer实现音乐播放--权限请求

在Android应用中&#xff0c;获取本地音乐文件的权限是实现音乐扫描功能的关键步骤之一。随着Android版本的不断更新&#xff0c;从Android 6.0&#xff08;API级别23&#xff09;开始&#xff0c;应用需要动态请求权限&#xff0c;而到了android 13以上需要的权限又做了进一步…

向量数据库FAISS之五:原理(LSH、PQ、HNSW、IVF)

1.Locality Sensitive Hashing (LSH) 使用 Shingling MinHashing 进行查找 左侧是字典&#xff0c;右侧是 LSH。目的是把足够相似的索引放在同一个桶内。 LSH 有很多的版本&#xff0c;很灵活&#xff0c;这里先介绍第一个版本&#xff0c;也是原始版本 Shingling one-hot …