JavaEE初阶(7)(认识文件、Java针对文件的操作、文件系统的操作【File】、文件内容的操作——流对象【Reader、Writer、InputStream、OutputStream】)

接上次博客:JavaEE进阶(6)多线程进阶——线程相关的面试题(常见的锁策略、CAS、Synchronized 原理、JUC的常见类、原子类、信号量、CountDownLatch、线程安全的集合类)_di-Dora的博客-CSDN博客

目录

文件操作 —— IO

认识文件

文件路径

文件类型

Java针对文件的操作

文件系统的操作

File 类概述

文件内容的操作——流对象

流对象(Steam)

究竟什么叫输入?什么叫输出?

Reader概述

Writer概述 

InputStream 概述

FileInputStream 概述

构造方法:

OutputStream 概述

方法

利用 OutputStreamWriter 进行字符写入 

方法一:利用 Scanner 进行字符读取

方法二:利用 PrintWriter 找到我们熟悉的方法 

小程序练习

1、删除指定文件

2、进行普通文件的复制

3、查找包含指定字符的文件


文件操作 —— IO

认识文件

操作系统里会把很多的硬件设备和软件设备都抽象成“文件”,统一进行管理。但是大部分情况下我们谈到的文件都是指硬盘的文件。文件就相当于是针对“硬盘”数据的一种抽象。

机械硬盘(HDD):机械硬盘适合顺序读写,不适合随机读写。

固态硬盘(SSD) :里面都是集成程度很高的芯片,类似于CPU和内存,当然读写速度还是比不上内存。所以固态硬盘要比机械硬盘效率高很多。

针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时,往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念, 就类似办公桌上的一份份真实的文件一般。

我们通常以“文件”的方式操作硬盘。

一台计算机上有很多文件,这些文件是通过“文件系统(操作系统提供的模块)”来进行组织的。

文件路径

操作系统使用“目录”这样的结构来组织文件。是一种树形结构。(N叉树)

所以我们就可以使用目录的层次结构来描述文件的所在位置——“路径”。

形如这样的字符串就体现出了当前文件在哪个目录中: 

1、绝对路径:是以C、D盘符开头的,这种叫做“绝对路径”;

2、相对路径:需要先制定一个目录作为基准目录。从基准目录出发看看沿着什么样的路线能够找到指定文件。基准不同,相对路径就不同。此时涉及的路径就是“相对路径”。它往往是 . (当前目录) 或  .. (当前目录的上一级目录) 开头的(. 这种情况可以省略)。

(1)C:\Users\ED\Pictures

         .\Camera Roll

(2)C:\Users\ED

         .\Pictures\Camera Roll

(3)C:\Users\ED\Pictures\Family Roll

         ..\Camera Roll

如果是命令行进行操作,基准目录就是你当前所处的目录: 

也可以切换: 

如果是图形化界面的程序,基准目录就不好说了。

对于IDEA来说,基准目录就是项目目录!

文件除了有数据内容之外,还有一部分信息,例如文件名、文件类型、文件大小等并不作为文件的数据而存在,我们把这部分信息可以视为文件的元信息。

文件类型

从编程的角度来看,主要分为两大类:(计算机本身存储的数据都是二进制的)

1、文本:文件中保存的数据都是字符串,保存的内容都是合法的字符;

2、二进制:文件中保存的数据仅仅是二进制数据,不要求保存的内容是合法的字符。

什么是合法的字符?

如果此时文件中的每个数据都是合法的 utf8编码的字符,就可以认为这个文件是文本文件。如果存在一些不是 utf8编码的合法字符,这个文件就是二进制文件。

如何判定一个文件是文本还是二进制文件?

使用记事本打开文件,如果打开后是乱码,就是二进制;否则是文本。因为记事本就是尝试按照字符的方式来展示内容,这个过程会自动查码表。 

生活中很多文件都是二进制的,比如docx,pptx……

区分文本和二进制是很重要的,因为我们写代码的时候二者的代码编写方式是不一样的!

Java针对文件的操作

文件系统的操作

File 类概述

属性:

  • static String (修饰符及类型)pathSeparator(属性):这是一个依赖于系统的路径分隔符,它以字符串形式表示。在Windows系统上通常是分号(;),而在类Unix系统上通常是冒号(:)。这个属性用于分隔多个文件路径。
  • static char(修饰符及类型) pathSeparatorChar(属性):这是一个依赖于系统的路径分隔符,以字符形式表示。它通常与pathSeparator属性具有相同的值,只是以字符的形式表示。

pathSeparator 就是“ \ ”,路径中用来分隔目录的符号。在windows上,“ \ ”和“ / ”都可以作为分隔符,但是 Linux/Mac 只可以用 “ / ”。我们一般用“ / ”,因为使用“ \ ”在代码中通常还要搭配通配字符来使用。

如果当前系统是Windows,pathSeparator 一般代表“ \ ”;但在 Linux/Mac ,pathSeparator 代表 “ / ”

构造方法:

  • File(File parent, String child):这个构造方法接受一个父目录(File类型)和一个孩子文件路径(String类型),并创建一个新的File实例,代表父目录和孩子文件的组合路径。
  • File(String pathname):这个构造方法接受一个文件路径(String类型),可以是绝对路径或相对路径,并创建一个新的File实例,代表该路径对应的文件或目录。
  • File(String parent, String child):这个构造方法接受一个父目录路径(String类型)和一个孩子文件路径(String类型),并创建一个新的File实例,父目录用路径表示。

一个File对象,表示了一个硬盘上的文件,在构造对象的时候就需要把这个文件的路径给指定进来(使用绝对路径或相对路径都可以)

方法:

File类还有许多方法用于执行文件和目录的操作,包括创建文件、删除文件、重命名文件、检查文件是否存在、获取文件信息等等。这些属性和构造方法使得File类成为Java中操作文件和目录的重要工具之一。

以下是File类的常见方法以及它们的修饰符和返回值类型:

  1. String getParent()

    • 修饰符:public
    • 返回值类型:String
    • 方法签名:public String getParent()
    • 说明:返回File对象的父目录文件路径。
  2. String getName()

    • 修饰符:public
    • 返回值类型:String
    • 方法签名:public String getName()
    • 说明:返回File对象的纯文件名称。
  3. String getPath()

    • 修饰符:public
    • 返回值类型:String
    • 方法签名:public String getPath()
    • 说明:返回File对象的文件路径。
  4. String getAbsolutePath()

    • 修饰符:public
    • 返回值类型:String
    • 方法签名:public String getAbsolutePath()
    • 说明:返回File对象的绝对路径。
  5. String getCanonicalPath()

    • 修饰符:public
    • 返回值类型:String
    • 方法签名:public String getCanonicalPath()
    • 说明:返回File对象的规范化的绝对路径。
  6. boolean exists()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean exists()
    • 说明:判断File对象描述的文件是否真实存在。
  7. boolean isDirectory()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean isDirectory()
    • 说明:判断File对象代表的文件是否是一个目录。
  8. boolean isFile()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean isFile()
    • 说明:判断File对象代表的文件是否是一个普通文件。
  9. boolean createNewFile()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean createNewFile()
    • 说明:根据File对象,自动创建一个空文件。成功创建后返回true。
  10. boolean delete()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean delete()
    • 说明:根据File对象,删除该文件。成功删除后返回true。
  11. void deleteOnExit()

    • 修饰符:public
    • 返回值类型:void
    • 方法签名:public void deleteOnExit()
    • 说明:根据File对象,标注文件将被删除,删除动作会在JVM运行结束时才会进行。
  12. String[] list()

    • 修饰符:public
    • 返回值类型:String[]
    • 方法签名:public String[] list()
    • 说明:返回File对象代表的目录下的所有文件名。
  13. File[] listFiles()

    • 修饰符:public
    • 返回值类型:File[]
    • 方法签名:public File[] listFiles()
    • 说明:返回File对象代表的目录下的所有文件,以File对象表示。
  14. boolean mkdir()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean mkdir()
    • 说明:创建File对象代表的目录。
    • 如果该目录已经存在,mkdir() 方法将返回 false,表示目录创建失败。如果目录成功创建,它将返回 true。
  15. boolean mkdirs()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean mkdirs()
    • 说明:创建File对象代表的目录,如果必要,会创建中间目录。
  16. boolean renameTo(File dest)

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean renameTo(File dest)
    • 说明:进行文件改名,也可以视为我们平时的剪切、粘贴操作。
  17. boolean canRead()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean canRead()
    • 说明:判断用户是否对文件有可读权限。
  18. boolean canWrite()

    • 修饰符:public
    • 返回值类型:boolean
    • 方法签名:public boolean canWrite()
    • 说明:判断用户是否对文件有可写权限。

注意:

  • 文件名=前缀+扩展名;使用路径构造File对象,一定要把前缀和扩展名都带上!
  • 一般文件系统上都会对文件有权限的限制,约定了这个文件哪些用户可以读,哪些用户可以写。当然,在Windows里面可能不太明显,因为Windows系统里的用户都是管理员,管理员的权限最大,可以无视文件本身的权限。

1、 

package io;

import java.io.File;
import java.io.IOException;

public class Demo1 {
    public static void main(String[] args) throws IOException {
        File file=new File("d:/test.txt");
        System.out.println(file.getParent());
        System.out.println(file.getName());
        System.out.println(file.getPath());
        System.out.println(file.getAbsolutePath());
        System.out.println(file.getCanonicalPath());
    }
}

这里并不要求这个文件是真实存在的。

你可能会奇怪,为啥最后面由 d 变成了D?——Windows 上的盘符是不区分大小写的。

这里我们还需要稍微改一下例子来讲一下:getAbsolutePath() 和 getCanonicalPath() 之间的差别:

public class Demo1 {
    public static void main(String[] args) throws IOException {
        File file=new File("./test.txt");//相对路径
        System.out.println(file.getParent());
        System.out.println(file.getName());
        System.out.println(file.getPath());
        System.out.println(file.getAbsolutePath());
        System.out.println(file.getCanonicalPath());
    }
}

“D:\JavaJava\IO\test.txt”,这个结果就是把当前的相对路径拼接到基准目录上,从而得以由相对路径转换为绝对路径;

前面的“D:\JavaJava\IO\”就是基准路径,对于IDEA来说,基准目录就是项目目录的所在目录。

而 getCanonicalPath() 就是针对绝对路径的简化,把“ . ”去掉了。

2、

package io;

import java.io.File;
import java.io.IOException;

public class Demo2 {
        public static void main(String[] args) throws IOException {
            File file = new File("hello-world.txt"); 
            // 要求该文件不存在,才能看到相同的现象
            System.out.println(file.exists());
            System.out.println(file.isDirectory());
            System.out.println(file.isFile());
            //创建文件
            System.out.println(file.createNewFile());

            System.out.println(file.exists());
            System.out.println(file.isDirectory());
            System.out.println(file.isFile());
            System.out.println(file.createNewFile());
        }
}

3、

    public static void main(String[] args) throws InterruptedException {
        File f = new File("d:/test.txt");
        boolean ret=f.delete();
        System.out.println("ret="+ret);

    }

等到进程结束之后再删除: 

package io;

import java.io.File;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        File f = new File("d:/test.txt");
        boolean ret=f.delete();
        System.out.println("ret="+ret);

        //不是立即删除,而是进程结束之后才真正删除
        f.deleteOnExit();
        Thread.sleep(5000);
        System.out.println("进程结束!");
    }
}

deleteOnExit()在平时有什么应用?

deleteOnExit() 方法通常用于在程序退出时自动删除临时文件或进行一些清理操作。它的应用场景包括:

  • 临时文件的管理:当程序,比如Office,需要创建一些临时文件,但在程序运行结束后不再需要这些文件时,程序会在退出时自动删除它们,以节省磁盘空间和确保不留下不必要的文件。这样做是为了解决“电脑突然断电,数据来不及保存,但是通过这样的文件,就可以告诉系统上次是正常关闭还是异常关闭。通过临时文件就可以恢复之前的正在编辑却还没来得及保存的内容了”。
  • 确保资源释放:在某些情况下,我们可能会使用 deleteOnExit() 来确保程序退出时释放某些资源,例如关闭数据库连接、释放网络连接等。这可以帮助避免资源泄漏问题。
  • 清理临时数据:如果我们的程序在运行过程中生成了一些临时数据或缓存,而这些数据在程序退出时不再需要,就可以使用 deleteOnExit() 来自动清理这些数据,以避免它们占用过多的内存或磁盘空间。
  • 文件锁的释放:在多线程或多进程环境中,可能会使用文件锁来协调对某个文件的访问。在程序退出时,也可以使用 deleteOnExit() 来释放文件锁,以确保其他进程或线程可以访问该文件。

总之,deleteOnExit() 方法在需要确保一些资源或临时文件在程序退出时被清理时非常有用。但需要注意的是,这个方法是在程序退出时才会执行删除操作,因此不适用于需要立即删除文件的情况。如果需要立即删除文件,应该使用 delete() 方法。

4、注意给的应该是目录,而不是文件!

    public static void main(String[] args) {
        File f = new File("d:/");
        String[] files=f.list();
        System.out.println(files);
    }

 这样写能否打印出数组内容?如果不能,打印的是啥?

 

这里打印的是哈希值:这是一个数组,里面的元素是String,这个数组对应的哈希值是后面那一串。 

这是通过调用hashCode方法得到的。

在JVM上层,Java代码中是没有任何方法获取到“内存地址”的。要想拿到内存地址。要想拿到内存地址,我们只能靠native方法,进入JVM内部,通过C++代码获取到。

package io;

import java.io.File;
import java.util.Arrays;

public class Demo4 {
    public static void main(String[] args) {
        File f = new File("d:/");
        String[] files=f.list();
        System.out.println(Arrays.toString(files));
    }
}

 这样,D盘里面的一些目录就会被我们获取到,里面还有一些系统自带的特殊目录:

5、 

public class Demo5 {
    public static void main(String[] args) {
        File f = new File("d:/java");
        //创建目录
        boolean ret = f.mkdir();
        System.out.println("ret="+ret);
    }
}
 

 但是一旦我们把目录写的复杂一点:

public class Demo5 {
    public static void main(String[] args) {
        File f = new File("d:/java/aaa/ddd/sss");
        boolean ret = f.mkdir();
        System.out.println("ret="+ret);
    }
}

这个时候我们就可以使用 mkdirs ( ) : 它能够创建出多级目录。

public class Demo5 {
    public static void main(String[] args) {
        File f = new File("d:/java/aaa/ddd/sss");
        boolean ret = f.mkdirs();
        System.out.println("ret="+ret);
    }
}

6、

package io;

import java.io.File;

public class Demo6 {
    public static void main(String[] args) {
        //src就是“源”,dest就是“目标”
        File srcFile = new File("d:/test.txt");
        File destFile = new File("d:/test2/txt");
        //文件必须已经存在,才能够重命名成功
        boolean ret = srcFile.renameTo(destFile);
        System.out.println("ret = "+ret);
    }
}

文件内容的操作——流对象

流对象(Steam)

"流对象"通常是指在编程中用于处理输入和输出的数据流的对象。数据流是一种连续的数据序列,可以是字节流或字符流,用于将数据从一个地方传输到另一个地方。流对象提供了在程序中读取和写入数据的方式。

在标准库中,提供的读写文件的流对象,不是一两个类,而是很多类。但是这些类都可以归结到两个大的类别中:

字节流(Byte Streams): 字节流用于处理二进制数据,它们操作字节的形式,适用于处理图像、音频、视频、文件等二进制数据。在Java中,InputStream和OutputStream是处理字节流的抽象类。

每次读/写的最小单位,都是“字节”(8bit)

  • InputStream 用于从输入源(例如文件、网络连接、内存缓冲区)读取字节数据。
  • OutputStream 用于将字节数据写入输出目标(例如文件、网络连接、内存缓冲区)。

字符流(Character Streams): 字符流用于处理文本数据,它们以字符的形式读取和写入数据,适用于处理文本文件、配置文件等。在Java中,Reader和Writer是处理字符流的抽象类。

每次读/写的最小单位,“字符”(一个字符对应几个字节看当前的字符集是那种,GBK一个中文字符对应两个字节,UTF8一个中文字符对应三个字节)

字符流本质上是针对字节流进行了又一层封装。字符流能自动帮我们把文件中几个相邻的字节转换成一个字符(帮我们完成了一个自动查字符集表)

  • Reader 用于从输入源读取字符数据。
  • Writer 用于将字符数据写入输出目标。

流对象提供了一种抽象的方式来处理输入和输出,它们通常是面向字节或字符的,可以轻松地与各种数据源和数据目标进行交互。流对象允许数据逐个或批量地读取和写入,通常使用缓冲区来提高性能。

在编程中,流对象是处理文件操作、网络通信、数据传输等关键任务的基本工具之一。通过适当选择和使用字节流或字符流,可以有效地处理各种数据处理需求。

究竟什么叫输入?什么叫输出?

我们把一个数据保存到硬盘,这个过程算输入还是输出?

其实简单来看:

站在硬盘的视角,输入;

站在cpu视角,输出 。

"输入"和"输出"是相对于数据流动的角度来定义的。

  • 输入(Input): 输入是数据从外部传输到计算机系统或程序中的过程。这包括从外部设备、文件、网络或其他源接收数据到计算机内存或寄存器中的过程。输入可以是用户输入的键盘输入、鼠标移动、传感器数据、文件读取等。对于计算机系统来说,输入数据是外部数据,需要通过输入操作进行获取和处理。

  • 输出(Output): 输出是数据从计算机系统或程序传输到外部世界的过程。这包括将数据从计算机内存、寄存器或其他存储位置发送到外部设备、文件、网络或其他目标的过程。输出可以是显示在屏幕上的图像、打印到打印机的文档、保存到硬盘的文件、通过网络发送的数据等。对于计算机系统来说,输出数据是要传输到外部的数据。

在将数据保存到硬盘的情况下:

  • 从计算机的角度来看(站在CPU视角),将数据写入硬盘是一个输出操作。CPU或程序将数据从内存或其他位置发送到硬盘上的文件,这是将数据传送到外部存储的输出过程。

  • 从硬盘的角度来看(站在硬盘的视角),接收到数据是一个输入操作。数据从计算机系统传输到硬盘,这是硬盘接收输入数据的过程。

我们应该站在CPU的视角来看,因为CPU是最重要的!

Reader概述

Reader 类是 Java 中用于读取字符数据的抽象类,它是字符输入流的基类。以下是 Reader 类的主要方法及其说明:

方法:

1、int read()

  • 修饰符:public abstract
  • 返回值类型:int
  • 方法签名:public abstract int read() throws IOException
  • 说明:从输入流中读取一个字符的数据并返回其Unicode值。如果已经读到流的末尾,则返回 -1。这是最常用的读取字符的方法之一。

2、int read(char[] cbuf)

  • 修饰符:public
  • 返回值类型:int
  • 方法签名:public int read(char[] cbuf) throws IOException
  • 说明:最多读取 cbuf.length 个字符(若干个)的数据到字符数组 cbuf 中,然后返回实际读取的字符数。如果已经读到流的末尾,则返回 -1。它会把参数指定的cbuf数组给填充满

3、int read(char[] cbuf, int off, int len)

  • 修饰符:public
  • 返回值类型:int
  • 方法签名:public int read(char[] cbuf, int off, int len) throws IOException
  • 说明:最多读取 len 个字符的数据到字符数组 cbuf 中,放在从 off 开始的位置,然后返回实际读取的字符数。如果已经读到流的末尾,则返回 -1。

4、void close()

  • 修饰符:public
  • 返回值类型:void
  • 方法签名:public abstract void close() throws IOException
  • 说明:关闭字符输入流,释放与之关联的资源。在完成文件读取后,应该调用 close() 方法来释放资源,以防止资源泄漏。

Reader 类的子类通常用于从文件、网络连接、字符串等输入源中读取字符数据。这些方法提供了一种逐字符或批量读取字符数据的方式,并且通常使用字符缓冲来提高性能。读取完成后,应该及时关闭字符输入流以释放资源,使用 close() 方法来关闭。

接下来我们用代码演示一遍这些方法:

Reader是一个抽象类,它不可以new实例,只可以new其子类,并实现其抽象方法。

Reader的一些除了FileReader的子类:

  • BufferedReader:用于缓冲读取字符数据,通常与其他 Reader 结合使用以提高性能。它允许批量读取字符数据,并减少对底层输入源的直接读取操作。常用于读取文本文件和网络数据。
  • InputStreamReader:将字节数据从输入流转换为字符数据,通常用于从字节输入流中读取字符数据。可以指定字符集来进行字符编码和解码。
  • StringReader:用于从字符串中读取字符数据,而不是从文件或网络中读取。通常用于将字符串转换为字符流。
  • CharArrayReader:用于从字符数组中读取字符数据,而不是从文件或网络中读取。通常用于从字符数组中构建字符流。
  • PipedReader:用于线程之间的字符数据通信。它通常与 PipedWriter 一起使用,一个线程将数据写入 PipedWriter,另一个线程从 PipedReader 读取数据。

标准库已经为我们提供了现成的类了,所以我们可以直接使用:

这里抛出了一个异常:文件如果不存在,就会打开失败。 

你会发现:当我们throws这个异常,FileNotFoundException不见了!

理由很简单,因为它是IOException的子类。它也可以去代表FileNotFoundException。

还有一个点,如果你之前看方法看的比较仔细,你会发现 read 方法的返回值是一个 int ?而不是一个char?

所以这里我们返回 int 其实也是为了能够表示出 -1这样的情况,同时char是有符号的,而我们希望得到的是一个无符号的char。

还有一个问题,它返回的0~6535是一个两个字节的范围,但是如果是utf8编码,一个中文字符应该是3个字节啊?不够用啊?

其实在Java标准库内部,对于字符编码是进行了很多的处理工作:

如果只使用 char,此时使用的字符集固定就是Unicode;

如果使用的是String,此时就会自动的把每个字符的Unicode转化成UTF8。

char[] c = >包含的每个字符都是Unicode编码;

一旦使用这个字符数组构造成String:String s = new String(c);

这就会在内部把每个字符都转换成UTF8编码。

s.charAt(i)=>char:就会把对应的 UTF8 的数据转换为Unicode。

为什么会有这种差异呢?
很简单,因为我们把多个Unicode连续放到放到一起是很难区分出来从哪里到哪里是一个完整的字符的。
UTF8是可以去奋斗,它可以认为是针对连续多个字符进行传输的时候的一种改进方案(也是从Unicode演化而来) 

第一种 read 就大功告成:

package io;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class Demo7 {
    //文件如果不存在,就会打开失败
    public static void main(String[] args) throws IOException {
        //创建Reader对象的过程就是“打开文件”的过程
        Reader reader = new FileReader("d:/test.txt");
        while(true){
            int read =reader.read();
            if(read == -1){
                //读取完毕
                break;
            }
            char ch =(char)read;
            System.out.println(ch);
        }

    }
}

应该往这个read里面传入一个是空的字符数组(不是NULL,而是一个没有实际意义数据的数组),然后又read方法内部,对这个数组内容进行填充。此时,cbuf这个参数称为“输出型参数”。

在Java中,通过将一个空的字符数组传递给 Reader 的 read 方法,然后在 read  方法内部对这个字符数组的内容进行填充,实际上是在使用字符数组作为输出参数来接收读取的字符数据。这种用法将字符数组 cbuf 视为输出型参数,因为它在方法调用过程中被修改以返回读取的数据。

具体来说:

  1. cbuf 参数: cbuf 是一个字符数组,通常用于存储从 Reader  读取的字符数据。在调用 read 方法时,您将一个已经初始化的字符数组传递给它。这个字符数组用于接收读取的字符数据。

  2. 输出型参数: 在 read 方法内部,会将读取的字符数据填充到 cbuf 数组中。这意味着 cbuf在方法调用结束后将包含从输入流中读取的字符。

例如,考虑以下代码片段:

char[] cbuf = new char[1024]; // 初始化一个字符数组
int bytesRead = reader.read(cbuf); // 调用 read 方法,将读取的字符存储到 cbuf 中

在上面的代码中,cbuf 数组被传递给read  方法,并在方法执行后包含了从输入流中读取的字符。bytesRead 变量用于记录实际读取的字符数。

通过这种方式,cbuf 在方法调用之前是一个空的字符数组,但在方法调用之后被填充并包含读取的字符数据。这种使用方式将字符数组 cbuf 视为输出型参数,用于返回方法的结果。这种模式在处理字符数据时非常常见,特别是在读取文本文件或处理文本数据时。

"输出型参数" 是一种函数或方法参数传递的方式,通过这种方式可以将计算结果返回给调用者。

通常,在函数中,参数可以分为三种类型:

  • 输入参数(Input Parameters): 这些参数用于传递函数所需的输入数据,函数在执行过程中使用这些数据进行计算或操作。
  • 输出参数(Output Parameters): 这些参数用于将函数的计算结果或其他输出信息传递给调用者。输出参数在函数执行后被赋予新值,以便调用者获取结果。
  • 输入输出参数(Input/Output Parameters): 这些参数同时用于传递输入数据和接收输出结果。调用者提供了初始值作为输入,函数在执行过程中可能修改这些参数的值,最终将结果传递给调用者。

"输出型参数" 主要指的是输出参数,它允许函数将计算结果或其他输出信息传递给调用者。在许多编程语言中,例如Java、C++,输出参数通常通过引用或指针传递给函数。在Java中,可以使用对象来模拟输出参数的效果,因为Java中没有显式的指针或引用参数。

举个食堂打饭的例子方便你理解:
 

这里的空数组其实就相当于是你的空餐盘,
食堂柜口就是方法内部,
你把空餐盘交给打饭阿姨,
她会根据你想要的菜给你打饭,
然后最后把打好菜的餐盘返回给你。 

第二个这种read 会尽可能的把 cbuf 这个数组给填满。

当然,如果实际上文件的内容填不满也没关系,直接把所有数据都读出来就好了。

但是,如果文件内容特别长,超出数组的范围,就可以用while循环执行多次,保证最终把文件读完。

如果文件为空,就直接返回-1。

package io;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class Demo7 {
    //文件如果不存在,就会打开失败
    public static void main(String[] args) throws IOException {
        //创建Reader对象的过程就是“打开文件”的过程
        Reader reader = new FileReader("d:/test.txt");

        //2、一次read多个字符
        while (true){
            char[] cbuf = new char[1024];
            //n 表示当前读到的字符个数
            int n = reader.read(cbuf);
            if(n==-1){
                //读取完毕
                break;
            }
            System.out.println("n = "+n);
            for(int i =0;i<n;i++){
                System.out.println(cbuf[i]);
            }

        }
    }
}

 第三种read也差不多:

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class Demo7 {
    public static void main(String[] args) throws IOException {
        // 创建Reader对象的过程就是“打开文件”的过程
        Reader reader = new FileReader("d:/test.txt");

        // 创建一个字符数组用于存储读取的字符数据
        char[] cbuf = new char[1024];
        int off = 0; // 起始位置
        int len = 1024; // 最大读取字符数

        while (true) {
            // n 表示当前读到的字符个数
            int n = reader.read(cbuf, off, len);
            
            if (n == -1) {
                // 读取完毕
                break;
            }

            System.out.println("n = " + n);
            for (int i = 0; i < n; i++) {
                System.out.println(cbuf[i]);
            }

            // 更新起始位置和最大读取字符数
            off += n;
            len -= n;
        }
        
        // 关闭字符输入流
        reader.close();
    }
}

使用close方法,最主要的目的是为了释放文件描述符。

还记得我们之前讲的PCB吗?这里会包含很多属性,比如pid,内存指针,文件描述符表……

文件描述符表通常是一个数组或列表,用于跟踪一个进程所打开的文件或其他I/O资源。每当进程打开一个文件或创建一个文件描述符时,操作系统会为其分配一个文件描述符表中的一个元素。这个表的长度是有限的,因为系统资源是有限的。

如果你的代码一直打开文件,而不去关闭文件,就会使这个表里的元素越来越多,一直到把这个数组占满了,后续再尝试打开文件,就会出错了。这就是文件资源泄露。 

文件资源泄漏(File Resource Leak)是指在一个程序中持续打开文件,但不正确地关闭它们,导致文件描述符表中的元素不断增加,最终达到上限,从而导致后续的文件打开操作失败。这类似于内存泄漏——malloc了之后却没有free之类的,但不同之处在于它涉及到操作系统资源而不仅仅是内存。

尽管Java具有自动内存管理(垃圾回收)机制,可以自动释放不再使用的内存,但是对于文件和其他非内存资源,通常需要手动释放这些资源。这包括文件、网络连接、数据库连接、输入/输出流等。

在使用完文件后显式地调用 close() 方法来释放资源。这可以帮助确保及时释放文件资源,避免文件描述符表被耗尽。如果不关闭文件,尤其是在循环中频繁打开文件的情况下,可能会导致资源泄漏和程序性能下降。

但是!如果你的代码这么写,仍然有文件资源泄露的风险!因为close可能执行不到!

一旦上面的逻辑抛出异常,就会导致close执行不到。

那这样呢?

        try {
            //2、一次read多个字符
            while (true){
                char[] cbuf = new char[3];
                //n 表示当前读到的字符个数
                int n = reader.read(cbuf);
                if(n==-1){
                    //读取完毕
                    break;
                }
                System.out.println("n = "+n);
                for(int i =0;i<n;i++){
                    System.out.println(cbuf[i]);
                }

            }
            
        }finally {
            reader.close();
        }

这样写,无论 try 中的代码是正常执行完毕还是出现异常,都能保证 finally 执行完毕。  

话又说回来,虽然能这么写,但是不够elegant!

还有一种方法:

        try (Reader reader = new FileReader("d:/test.text")){
            while (true){
                char[] cbuf = new char[3];
                //n 表示当前读到的字符个数
                int n = reader.read(cbuf);
                if(n==-1){
                    //读取完毕
                    break;
                }
                System.out.println("n = "+n);
                for(int i =0;i<n;i++){
                    System.out.println(cbuf[i]);
                }
            }
        }

这里的“try-catch”代码块叫 “ try with reasourses ” 

 这个语法的目的就是“()”定义的变量会在try代码块结束的时候自动调用其中的close 方法,不论是正常结束还是抛出异常。

要求写到“ ( ) ”里面的必须实现Closeable接口。比如:

Writer概述 

Writer 类是 Java 中用于写入字符数据的抽象类,它是字符输出流的基类

Writer 类的主要方法及其说明:

void write(int c)

  • 修饰符:public abstract
  • 参数:int c - 要写入的字符的 Unicode 值。
  • 说明:将指定的字符写入输出流。这是用于写入单个字符的方法。

void write(char[] cbuf)

  • 修饰符:public
  • 参数:char[] cbuf - 要写入的字符数组。
  • 说明:将字符数组中的所有字符写入输出流。

void write(char[] cbuf, int off, int len)

  • 修饰符:public
  • 参数:char[] cbuf - 要写入的字符数组,int off - 起始偏移量,int len - 要写入的字符数。
  • 说明:从字符数组的指定偏移量开始,写入指定数量的字符到输出流。

void write(String str)

  • 修饰符:public
  • 参数:String str - 要写入的字符串。
  • 说明:将字符串中的字符写入输出流。

void write(String str, int off, int len)

  • 修饰符:public
  • 参数:String str - 要写入的字符串,int off - 起始偏移量,int len - 要写入的字符数。
  • 说明:从字符串的指定偏移量开始,写入指定数量的字符到输出流。

void flush()

  • 修饰符:public
  • 说明:将输出流的缓冲区中的数据刷新到目标位置。通常在需要确保数据被写入但不关闭输出流时使用。

void close()

  • 修饰符:public abstract
  • 说明:关闭字符输出流,释放与之关联的资源。在完成字符写入后,应该调用 close() 方法来释放资源,以防止资源泄漏。

Writer 类的子类通常用于将字符数据写入文件、网络连接、字符数组等输出源。这些方法提供了多种方式来写入字符数据,包括逐字符写入和批量写入,以及支持字符串的写入。最后,应该使用 close() 方法来关闭字符输出流,以确保资源被正确释放。

 从上到下:

一次写一个字符;

一次写多个字符(字符数组);

一次写一个字符串;

带有 offset 和 len,offset 就指的是从数组/字符串中的第几个字符开始写的。

注意:Writer写入文件,默认情况下就会把原有文件的内容给清空掉,如果不想被清空,就需要在构造方法中多加个参数:true

在 Java 中,FileWriter允许我们指定一个布尔参数,通常称为 "append",用来控制是否追加内容到文件。当我们将这个参数设置为 true 时,新的数据将被追加到文件的末尾,而不会清空文件。

package io;

import java.io.*;

public class Demo8 {
    public static void main(String[] args) {
        try(Writer writer = new FileWriter("d:/test.text",true)) {
            //直接使用write方法就可以写入数据
            writer.write("我在学校文件操作");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

其他的一些常见子类:

  • FileWriter:用于将字符数据写入文件。可以设置是否追加数据到文件末尾。
  • BufferedWriter:用于缓冲写入,通常与其他 Writer 结合使用以提高性能。它允许批量写入字符数据,并减少对底层文件的直接写入操作。
  • OutputStreamWriter:将字符数据转换为字节数据,然后写入字节输出流。通常用于将字符数据写入网络连接或其他字节流目标。
  • PrintWriter:用于将格式化的文本输出到字符流,通常用于写入日志文件或生成报告。
  • StringWriter:用于将字符数据写入字符串缓冲区,而不是文件。通常用于构建字符串。
  • CharArrayWriter:用于将字符数据写入字符数组,而不是文件。通常用于构建字符串。

InputStream 概述

在Java中,InputStream 是用于从输入源读取字节数据的抽象类,它是字节流的基类。InputStream 主要用于处理二进制数据,每次读取和写入的最小单位是字节。

虽然 InputStream 专注于字节数据,但在某些情况下,我们需要从输入流中读取字符数据,例如从文件中读取文本文件的内容。为了在字节流和字符流之间建立连接,Java 提供了 InputStreamReader 类,它是 Reader 的子类,并将字节流转换为字符流。

InputStreamReader 在构造时接受一个 InputStream 对象作为参数,它将字节从 InputStream 中读取,并将其转换为字符数据,然后提供给程序。这种机制允许我们将字节流包装在字符流之上,以便能够以字符的方式读取和处理字节数据。

因此,InputStreamReader 的存在是为了在需要时将字节流与字符流整合起来,以便在处理文本文件等情况下能够方便地读取和操作字符数据。这种字符编码转换通常是透明的,使得我们可以在处理字符数据时无需关心底层的字节表示。

InputStream 是Java中用于从输入源读取字节数据的抽象类,所以它不能直接new,我们需要借助一个FileInputStream类来new 。InputStream​​​​​​​是所有输入流的父类,用于处理字节流。以下是InputStream 的主要方法及其说明:

方法:

  1. int read()

    • 修饰符:public abstract
    • 返回值类型:int
    • 方法签名:public abstract int read() throws IOException
    • 说明:从输入流中读取一个字节的数据并返回。如果已经读到流的末尾,则返回-1。该方法是阻塞的,如果没有可用的字节数据,它会等待直到有数据可读。
  2. int read(byte[] b)

    • 修饰符:public
    • 返回值类型:int
    • 方法签名:public int read(byte[] b) throws IOException
    • 说明:最多读取b.length个字节的数据到字节数组b中,然后返回实际读取的字节数。如果已经读到流的末尾,则返回-1。该方法会尽量将b填满,但不一定要填满整个数组。
  3. int read(byte[] b, int off, int len)

    • 修饰符:public
    • 返回值类型:int
    • 方法签名:public int read(byte[] b, int off, int len) throws IOException
    • 说明:最多读取len - off个字节的数据到字节数组b中,放在从off 位置开始,然后返回实际读取的字节数。如果已经读到流的末尾,则返回-1。这个方法允许你从指定偏移位置开始填充数据。
  4. void close()

    • 修饰符:public
    • 返回值类型:void
    • 方法签名:public void close() throws IOException
    • 说明:关闭输入流,释放与之关联的资源。在完成文件读取后,应该调用 close() 方法来释放资源,以防止资源泄漏。

InputStream 的这些方法允许逐字节或批量读取字节数据,并且在读取完数据后关闭流,以确保资源被正确释放。它是Java输入流的基本抽象类,通常用于读取文件、网络数据、内存缓冲区等字节数据源。不过,它是字节流,如果需要处理字符数据,通常会使用Reader类的子类。

读出来的就是文件中对应字母或中文的ASCLL码值 

说明

InputStream 只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream

FileInputStream 概述

FileInputStream 是 Java 中用于从文件中读取字节数据的类。它提供了多种构造方法来创建文件输入流,用于读取文件中的数据。

构造方法:
  1. FileInputStream(File file):这个构造方法接受一个 File 对象作为参数,用于创建一个文件输入流,以读取指定文件的内容。

    • 签名:public FileInputStream(File file) throws FileNotFoundException
    • 说明:利用 File 构造文件输入流。
  2. FileInputStream(String name):这个构造方法接受一个文件路径的字符串作为参数,用于创建一个文件输入流,以读取指定路径的文件的内容。

    • 签名:public FileInputStream(String name) throws FileNotFoundException
    • 说明:利用文件路径构造文件输入流。

这两个构造方法用于创建 FileInputStream 对象,使得你可以从指定的文件中读取数据。需要注意的是,这些构造方法可能会抛出  FileNotFoundException  异常,如果指定的文件不存在或无法访问。

FileInputStream 可以用于读取各种类型的文件,包括文本文件、二进制文件、图像文件等。它通常与 InputStream 的读取方法一起使用,例如 read()、read(byte[]) 等,来从文件中逐字节或批量读取数据。读取完成后,应该及时关闭文件输入流以释放资源,使用 close() 方法来关闭。

OutputStream 概述

方法
  1. void write(int b): 这个方法用于写入一个字节的数据到输出流。它接受一个整数参数 b,该整数应该在 0 到 255 之间,表示要写入的字节。如果写入成功,它会将字节数据写入输出流。
  2. void write(byte[] b): 这个方法用于将一个字节数组 b 中的所有数据写入输出流。它将整个字节数组的内容写入输出流。
  3. int write(byte[] b, int off, int len): 这个方法用于将字节数组 b 中从偏移量 off 开始的 len 个字节写入输出流。通常,这个方法用于部分写入字节数组的内容。
  4. void close(): 关闭输出流(字节流)。这个方法用于释放与输出流相关的资源,并确保所有数据都被写入。在关闭输出流后,不能再向其写入数据。
  5. void flush(): 刷新输出流。输出流通常会使用缓冲区,数据在写入设备之前可能会存储在缓冲区中。flush 方法用于强制将缓冲区中的数据写入设备,以确保数据被及时发送。通常,在数据写入完成后或在需要确保数据即时到达设备时调用 flush 方法。重要:我们知道 I/O 的速度是很慢的,所以,大多的 OutputStream 为 了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的 一个指定区域里,直到该区域满了或者其他指定条件时才真正将数据写 入设备中,这个区域一般称为缓冲区。但造成一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置, 调用 flush(刷新)操作,将数据刷到设备中。

这些方法在处理文件写入、网络通信等情况下非常有用,可以有效地管理数据的写入和流的关闭。在使用 OutputStream 时,要特别注意在适当的时候调用 flush 方法,以确保数据被及时发送到目标位置。

说明:

OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中, 所以使用 FileOutputStream

package IO;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;

public class Demo7 {
    public static void main(String[] args) {
        try(InputStream inputStream =  new FileInputStream("d:/test.test")) {
            byte[] buffer = new byte[1024];
            int n = inputStream.read(buffer);
            System.out.println("n="+n);
            for(int i =0;i<n;i++){
                System.out.printf("%x\n",buffer[i]);//按照16进制进行打印
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

UTF8 刚好就是67个字节,通过这种方式就可以把每个字节都获取到: 

package IO;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

public class Demo8 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("D:\\test\\test.txt")) {
            String s = "你好!高山你好!大海!";
            //字节流没有 write版本,我们需要先将其转换为字节数组,获取到每个字段UTF8编码
            outputStream.write(s.getBytes());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

和 Writer 类似,OutputStream 打开一个文件就会默认清空文件的原有内容,写入的数据就会成为文件中新的数据。如果不想清空,就可以使用追加写的方式,即在构造方法中第二个参数传入 true。 

package IO;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

public class Demo8 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("D:\\test\\test.txt",true)) {
            String s = "你好!高山!  你好!大海!";
            //字节流没有 write版本,我们需要先将其转换为字节数组,获取到每个字段UTF8编码
            outputStream.write(s.getBytes());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 

   1、将文件完全读完的两种方式。相比较而言,后一种的 IO 次数更少,性能更好:

// 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "Hello" 的内容
public class Main {
    public static void main(String[] args) throws IOException {
        try (InputStream is = new FileInputStream("hello.txt")) {
            while (true) {
                int b = is.read();
                if (b == -1) {
                    // 代表文件已经全部读完
                    break;
               }
                
                System.out.printf("%c", b);
           }
       }
   }
}
package IO;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Demo3 {
    // 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容
        public static void main(String[] args) throws IOException {
            try (InputStream is = new FileInputStream("hello.txt")) {
                byte[] buf = new byte[1024];
                int len;
                while (true) {
                    len = is.read(buf);
                    if (len == -1) {
                        // 代表文件已经全部读完
                        break;
                    }
                    // 每次使用 3 字节进行 utf-8 解码,得到中文字符
                    // 利用 String 中的构造方法完成
                    // 这个方法了解下即可,不是通用的解决办法
                    for (int i = 0; i < len; i += 3) {
                        String s = new String(buf, i, 3, "UTF-8");
                        System.out.printf("%s", s);
                    }
                }
            }
        }
}

2、这里我们把文件内容中填充中文看看,注意,写中文的时候使用 UTF-8 编码。hello.txt 中填         写"你好中国"

     注意:这里我利用了这几个中文的 UTF-8 编码后长度刚好是 3 个字节和长度不超过 1024 字节       的现状, 但这种方式并不是通用的。

// 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容
public class Main {
    public static void main(String[] args) throws IOException {
        try (InputStream is = new FileInputStream("hello.txt")) {
            byte[] buf = new byte[1024];
            int len;
            while (true) {
                len = is.read(buf);
                if (len == -1) {
                    // 代表文件已经全部读完
                    break;
               }
                // 每次使用 3 字节进行 utf-8 解码,得到中文字符
                // 利用 String 中的构造方法完成
                // 这个方法了解下即可,不是通用的解决办法
                for (int i = 0; i < len; i += 3) {
                    String s = new String(buf, i, 3, "UTF-8");
                    System.out.printf("%s", s);
               }
           }
       }
   }
}

其实像刚才上面讲的那个字节流的代码,是可以转成字符流的。比如别人给我们提供的是字节流对象,但是我们知道实际的数据内容是文本数据,就可以把上述字节流转换成字符流,一共有两种方法:

利用 OutputStreamWriter 进行字符写入 
方法一:利用 Scanner 进行字符读取

很多Scanner 类是 Java 中用于处理输入数据的便捷工具类,它可以用于从不同来源(包括 InputStream)读取数据。Scanner 的构造方法有多种,可以用于不同的输入源和字符集设置。

构造方法:使用 charset 字符集进行 is 的扫描读取

 Scanner(InputStream is, String charset) 

在构造方法中,Scanner(InputStream is, String charset) 可以用于从 InputStream 中读取数据,并指定字符集 charset 进行解码。这个构造方法可以帮助我们在读取字符数据时指定正确的字符编码,以确保数据被正确解析。 

​​​​​​​ 

两者本质是一样的,所以使用System.in构造Scanner可以,使用 InputStream也行!

package IO;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class Demo4 {
    public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("D:\\test\\test.txt")){
            //此时scanner就是从文件中读取了!
            Scanner scanner = new Scanner(inputStream);
            //就可以使用 scanner 读取后续的数据
            String str = scanner.next();
            System.out.println(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

方法二:利用 PrintWriter 找到我们熟悉的方法 
package IO;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class Demo {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("D:\\test\\test.txt",true)) {
            //这就相当于把字节流转成字符流了
            PrintWriter writer = new PrintWriter(outputStream);
            writer.printf("Hello!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

现在运行一下,应该会写入“Hello!” 

明明我们已经写入字符串,但是文件里可能还是空着的,为什么?

所以这里头还涉及到一个关键点:flush() ——“缓冲区”

PrintWriter这样的类,在写入的时候不一定是直接而写硬盘,而是先把数据存到一个内存构成的“缓冲区”(buffer)中

许多输出流(包括 PrintWriter)在将数据写入硬盘之前会将数据先存储在内存中的缓冲区中,这是出于性能和效率的考虑。缓冲区的引入是为了减少频繁的磁盘写入操作,因为将数据写入磁盘通常比写入内存要慢得多(比内存慢个几千几万倍)。所以为了提高效率,应该想办法减少写硬盘的次数。通过将数据暂时存储在内存中,可以在一定程度上提高写入操作的效率。

但这也带来了一个问题,即在数据写入缓冲区后,如果还没来得及把缓冲区的数据写入硬盘,进程就结束了,程序突然崩溃或关闭,数据可能会丢失,因为尚未实际写入硬盘。

为了确保数据被写入硬盘,我们可以使用以下方法之一:

1、手动刷新缓冲区:你可以调用 flush() 方法来手动刷新缓冲区,这将强制将缓冲区中的数据写入硬盘。例如:

writer.flush();

2、关闭流时自动刷新:某些流(如 PrintWriter)可以在关闭时自动刷新缓冲区。你可以在创建流时通过构造函数或设置选项来启用这种自动刷新行为。例如:

PrintWriter writer = new PrintWriter(outputStream, true); // 启用自动刷新

通过上述方式,你可以确保数据在程序关闭或刷新缓冲区时写入硬盘,而不会丢失。这是一种权衡,可以根据你的需求来选择是否手动刷新缓冲区或启用自动刷新。

小程序练习

我们学会了文件的基本操作 + 文件内容读写操作,接下来,我们就利用所学知识来实现一些小工具程序:

1、删除指定文件

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

我们先来简单分析一下:

这道题要我们完成一个文件操作系统,我们需要做到:

  1. list列出目录内容;
  2. 判定文件类型
  3. 删除文件

值得注意的是这道题的深层要求:我们要能够找到目录中的所有文件,以及子目录中的所有文件,只要遇到子目录都要能够往里面找。

所以我们可以采用“递归”的方式,把所有的子目录都扫描一遍。

方法一:

package IO;

import java.io.File;
import java.util.Scanner;

public class Demo13 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 1. 先让用户输入一个要扫描的目录
        System.out.println("请输入要扫描的路径: ");
        String path = scanner.next();
        File rootPath = new File(path);
        if (!rootPath.isDirectory()) {
            System.out.println("您输入的扫描的路径有误!! ");
            return;
        }
        // 2. 再让用户输入一个要查询的关键词.
        System.out.println("请输入要删除文件的关键词: ");
        String word = scanner.next();
        // 3. 可以进行递归的扫描了.
        //    通过这个方法进行递归.
        scanDir(rootPath, word);
    }

    private static void scanDir(File rootPath, String word) {
        // 1. 先列出 rootPath 中所有的文件和目录.
        File[] files = rootPath.listFiles();
        if (files == null) {
            // 当前目录为 null, 就可以直接返回了.
            return;
        }
        // 2. 遍历这里的每个元素, 针对不同类型做出不同的处理.
        for (File f : files) {
            // 加个日志, 方便观察当前递归的执行过程.
            System.out.println("当前扫描的文件: " + f.getAbsolutePath());
            if (f.isFile()) {
                // 普通文件. 检查文件是否要删除. 并执行删除动作.
                checkDelete(f, word);
            } else {
                // 目录. 递归的再去判定子目录里包含的内容
                scanDir(f, word);
            }
        }
    }

    private static void checkDelete(File f, String word) {
        if (!f.getName().contains(word)) {
            // 不必删除, 直接方法结束
            return;
        }
        // 需要删除
        System.out.println("当前文件为: " + f.getAbsolutePath() + ", 请确认是否要删除(Y/n): ");
        Scanner scanner = new Scanner(System.in);
        String choice = scanner.next();
        if (choice.equals("Y") || choice.equals("y")) {
            // 真正执行删除操作
            f.delete();
            System.out.println("删除完毕!");
        } else {
            // 如果输入其他值, 不一定非得是 n, 都会取消删除操作.
            System.out.println("取消删除!");
        }
    }
}

方法二:

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): ");
        String rootDirPath = scanner.next();
        File rootDir = new File(rootDirPath);
        if (!rootDir.isDirectory()) {
            System.out.println("您输入的根目录不存在或者不是目录,退出");
            return;
        }
        System.out.print("请输入要找出的文件名中的字符: ");
        String token = scanner.next();
        List<File> result = new ArrayList<>();
        // 因为文件系统是树形结构,所以我们使用深度优先遍历(递归)完成遍历
        scanDir(rootDir, token, result);
        System.out.println("共找到了符合条件的文件 " + result.size() + " 个,它们分别是:");
        for (File file : result) {
            System.out.println(file.getCanonicalPath() + "   请问您是否要删除该文件?y/n");
            String in = scanner.next();
            if (in.toLowerCase().equals("y")) {
                if (file.delete()) {
                    System.out.println("文件已删除:" + file.getCanonicalPath());
                } else {
                    System.out.println("无法删除文件:" + file.getCanonicalPath());
                }
            }
        }
    }

    private static void scanDir(File rootDir, String token, List<File> result) {
        File[] files = rootDir.listFiles();
        if (files == null || files.length == 0) {
            return;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                scanDir(file, token, result);
            } else {
                if (file.getName().contains(token)) {
                    result.add(file.getAbsoluteFile());
                }
            }
        }
    }
}

2、进行普通文件的复制

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);

        System.out.print("请输入要复制的文件(绝对路径 OR 相对路径): ");
        String sourcePath = scanner.next();
        File sourceFile = new File(sourcePath);
        if (!sourceFile.exists()) {
            System.out.println("文件不存在,请确认路径是否正确");
            return;
        }

        if (!sourceFile.isFile()) {
            System.out.println("文件不是普通文件,请确认路径是否正确");
            return;
        }

        System.out.print("请输入要复制到的目标路径(绝对路径 OR 相对路径): ");
        String destPath = scanner.next();
        File destFile = new File(destPath);
        if (destFile.exists()) {
            if (destFile.isDirectory()) {
                System.out.println("目标路径已经存在,并且是一个目录,请确认路径是否正确");
                return;
            }

            if (destFile.isFile()) {
                System.out.print("目标文件已经存在,是否要进行覆盖?y/n: ");
                String ans = scanner.next();
                if (!ans.toLowerCase().equals("y")) {
                    System.out.println("停止复制");
                    return;
                }
            }
        }

        try (InputStream is = new FileInputStream(sourceFile);
             OutputStream os = new FileOutputStream(destFile)) {

            byte[] buf = new byte[1024];
            int len;

            while ((len = is.read(buf)) != -1) {
                os.write(buf, 0, len);
            }

            System.out.println("复制已完成");
        } catch (IOException e) {
            System.out.println("复制过程中发生异常: " + e.getMessage());
        }
    }
}

3、查找包含指定字符的文件

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

注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验 。

先来看看只在文件内容中寻找指定字符串的(不包括文件目录):

import java.io.*;
import java.util.*;
public class Main3 {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): ");
        String rootDirPath = scanner.next();
        File rootDir = new File(rootDirPath);
        if (!rootDir.isDirectory()) {
            System.out.println("您输入的根目录不存在或者不是目录,退出");
            return;
        }
        System.out.print("请输入要找出的文件名中的字符: ");
        String token = scanner.next();
        List<File> result = new ArrayList<>();
        // 因为文件系统是树形结构,所以我们使用深度优先遍历(递归)完成遍历
        scanDirWithContent(rootDir, token, result);
        System.out.println("共找到了符合条件的文件 " + result.size() + " 个,它们分别是");
        for (File file : result) {
            System.out.println(file.getCanonicalPath());
        }
    }
    private static void scanDirWithContent(File rootDir, String token,
                                           List<File> result) throws IOException {
        File[] files = rootDir.listFiles();
        if (files == null || files.length == 0) {
            return;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                scanDirWithContent(file, token, result);
            } else {
                if (isContentContains(file, token)) {
                    result.add(file.getAbsoluteFile());
                }
            }
        }
    }
    // 我们全部按照utf-8的字符文件来处理
    private static boolean isContentContains(File file, String token) throws
            IOException {
        StringBuilder sb = new StringBuilder();
        try (InputStream is = new FileInputStream(file)) {
            try (Scanner scanner = new Scanner(is, "UTF-8")) {
                while (scanner.hasNextLine()) {
                    sb.append(scanner.nextLine());
                    sb.append("\r\n");
                }
            }
        }

        return sb.indexOf(token) != -1;
    }
}

 

  1. 性能改进:在处理大文件时,一次性读取整个文件并搜索可能会导致内存消耗较大。我们可以考虑使用缓冲读取并搜索,逐块读取文件内容。

  2. 忽略文件编码:在检查文件内容时,假定文件使用UTF-8编码。这可能会导致对非UTF-8编码文件的问题。我们可以使用字节流来处理文件内容,而不是字符流,这样可以忽略文件编码。

  3. 递归改进:使用递归扫描整个目录结构是有效的,但对于较深的目录结构,可能会导致栈溢出。我们可以考虑使用迭代方式来遍历目录,以减少递归深度。

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): ");
        String rootDirPath = scanner.next();
        File rootDir = new File(rootDirPath);
        if (!rootDir.isDirectory()) {
            System.out.println("您输入的根目录不存在或者不是目录,退出");
            return;
        }
        System.out.print("请输入要找出的文件名中的字符: ");
        String token = scanner.next();
        List<File> result = new ArrayList<>();
        scanDirWithContent(rootDir, token, result);
        System.out.println("共找到了符合条件的文件 " + result.size() + " 个,它们分别是");
        for (File file : result) {
            System.out.println(file.getCanonicalPath());
        }
    }

    private static void scanDirWithContent(File rootDir, String token, List<File> result) {
        Stack<File> stack = new Stack<>();
        stack.push(rootDir);

        while (!stack.isEmpty()) {
            File currentDir = stack.pop();
            File[] files = currentDir.listFiles();

            if (files == null || files.length == 0) {
                continue;
            }

            for (File file : files) {
                if (file.isDirectory()) {
                    stack.push(file);
                } else {
                    if (isContentContains(file, token)) {
                        result.add(file.getAbsoluteFile());
                    }
                }
            }
        }
    }

    private static boolean isContentContains(File file, String token) {
        try (InputStream is = new FileInputStream(file)) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                String content = new String(buffer, 0, bytesRead, "UTF-8");
                if (content.contains(token)) {
                    return true;
                }
            }
        } catch (IOException e) {
            // 处理异常
        }
        return false;
    }
}

包含文件名称和内容的:

import java.io.*;
import java.util.*;

public class Main5 {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): ");
        String rootDirPath = scanner.next();
        File rootDir = new File(rootDirPath);
        if (!rootDir.isDirectory()) {
            System.out.println("您输入的根目录不存在或者不是目录,退出");
            return;
        }
        System.out.print("请输入要找出的字符: ");
        String token = scanner.next();
        List<File> result = new ArrayList<>();
        // 因为文件系统是树形结构,所以我们使用递归(深度优先遍历)完成遍历
        scanDir(rootDir, token, result);
        System.out.println("共找到了符合条件的文件 " + result.size() + " 个,它们分别是:");
        for (File file : result) {
            System.out.println(file.getCanonicalPath() + "   请问您是否要删除该文件?y/n");
            String in = scanner.next();
            if (in.toLowerCase().equals("y")) {
                if (file.delete()) {
                    System.out.println("文件已删除:" + file.getCanonicalPath());
                } else {
                    System.out.println("无法删除文件:" + file.getCanonicalPath());
                }
            }
        }
    }

    private static void scanDir(File rootDir, String token, List<File> result) {
        File[] files = rootDir.listFiles();
        if (files == null || files.length == 0) {
            return;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                scanDir(file, token, result);
            } else {
                if (file.getName().contains(token) || fileContainsToken(file, token)) {
                    result.add(file.getAbsoluteFile());
                }
            }
        }
    }

    private static boolean fileContainsToken(File file, String token) {
        try (Scanner scanner = new Scanner(file, "UTF-8")) {
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                if (line.contains(token)) {
                    return true;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值