Java 文件 & 文件操作


一、文件重要概念

1、文件概述

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

2、文件元信息

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

3、文件的系统管理

随着文件越来越多,对文件的系统管理也被提上了日程,如何进行文件的组织呢,一种合乎自然的想法出现了,就是按照层级结构进行组织 —— 也就是我们学习过的树形结构。这样,一种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的概念。这里提到的的管理信息一般指文件的元信息。通过一个个的目录/文件夹,我们就可以将文件组织起来,从而更方便的使用了。

4、文件路径

文件路径是用来定位文件系统中文件的,主要分为绝对路径和相对路径。实际表示路径是通过一个字符串来表示,每个目录之间用斜杠-/或反斜杠-\来分割(-反斜杠只是在Windows中适用,代码中要写成\,所以一般建议适用斜杠-/进行分割)

绝对路径:指的是文件系统上一个文件的具体位置,从盘符开始,一层一层往下找,这个过程中得到的路径就是绝对路径。

相对路径:从给定的某个基准目录出发,一层一层往下找,这个过程中得到的路径就是相对路径。在表示相对路径中,需要明确的是基准目录是什么。

相比之下,绝对路径,可以理解成是以“此电脑”为工作路径的相对路径。

📝举个例子:

上面我们提到,计算机的目录是有层级结构的,是以 N 叉树的结构来组织的,Windows一般都是从“此电脑”开始,所以表示绝对路径的时候可以把“此电脑”省略,直接从盘幅(A、B、C……)开始表示。例如上图绝对路径C:\Program Files\Java 就表示从 C 盘出发 -->ProgramFiles -->Java 的绝对路径。如果从C:\Program Files\Java目录出发到 jdk1.8.0_192,相对路径可以表示为:./jdk1.8.0_192

注意.在相对路径中,是一个特殊符号,表示当前目录。..在相对路径中表示当前目录的上机目录。

5、文本文件和二进制文件

文本文件:是基于字符编码的文件,一般采用定长编码方式,比如 ASCII 编码、UNICODE 编码。其中 ASCII编码采用 8 个特定的比特来表示每一个字符,而 UNICODE 编码采用 16 比特。文件里存储的数据需要遵守编码标准。

二进制文件:直接由二进制数字0和1组成,不存在统一的字符编码,也就是说由你来决定多少个比特表示一个什么值,你可以根据具体的应用来指定这些比特的含义,类似于自定义编码,而且是不定长度的编码。文件内容没有字符集的限制,可以存任何数据。

Tips: 如何判定一个文件是文本文件还是二进制文件?
简单粗暴的方式:用记事本打开某个文件,如果内容能看得懂,就是文本文件,反之是二进制文件。

6、快捷方式

Windows 操作系统上,还有一类文件比较特殊,就是平时我们看到的快捷方式(shortcut),这种文件只是对真实文件的一种引用而已。其他操作系统上也有类似的概念,例如,软链接(soft link)等。

二、文件操作

文件操作主要分为以下两部分:

  1. 文件系统操作:创建文件、删除文件、重命名文件、创建目录等操作。
  2. 文件内容操作:针对文件内容进行IO操作(读和写)。下面将分别进行展开。

1、文件系统操作

文件是存储在硬盘上的,如果想要操作文件,需要通过在内存中创建一个对应的对象,通过操作这个对象可以间接影响到硬盘上的文件。Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。

注意:有 File 对象,并不代表真实存在该文件。

File类下为我们提供一组API用来对文件进行相关操作,这些 API 都很简单,这里就直接列出不在赘述了:

构造方法:

方法名称说明
File(File parent, String child)根据父目录 + 孩子文件路径,创建一个新的 File 实例
File(String pathname)根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径
File(String parent, String child)根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示

方法:

返回值类型方法签名说明
StringgetParent()返回 File 对象的父目录文件路径
StringgetName()返回 FIle 对象的纯文件名称
StringgetPath()返回 File 对象的文件路径
StringgetAbsolutePath()返回 File 对象的绝对路径
StringgetCanonicalPath()返回 File 对象的修饰过的绝对路径
booleanexists()判断 File 对象描述的文件是否真实存在
booleanisDirectory()判断 File 对象代表的文件是否是一个目录
booleanisFile()判断 File 对象代表的文件是否是一个普通文件
booleancreateNewFile()根据 File 对象,自动创建一个空文件。成功创建后返回 true
booleandelete()根据 File 对象,删除该文件。成功删除后返回 true
voiddeleteOnExit()根据 File 对象,标注文件将被删除,删除动作会到 JVM 运行结束时才会进行
String[]list()返回 File 对象代表的目录下的所有文件名
File[]listFiles()返回 File 对象代表的目录下的所有文件,以 File 对象表示
booleanmkdir()创建 File 对象代表的目录
booleanmkdirs()创建 File 对象代表的目录,如果必要,会创建中间目录
booleanrenameTo(File dest)进行文件改名,也可以视为我们平时的剪切、粘贴操作
booleancanRead()判断用户是否对文件有可读权限
booleancanWrite()判断用户是否对文件有可写权限

2、文件IO

数据流:在计算机科学中,流(Stream)是指在输入和输出设备间建立的一条数据通道,可用于读取或写入数据。流可以将连续的数据序列视为由最初的比特位流,在内存中流过字节、块等单元。流可被看做是一个定长队列(FIFO),数据经过该队列后会被自动删除,这使得流具有异步处理效果,即数据流一旦进入队列,就可以不停地传输和处理,不需要在传输完所有数据之后才进行处理。而且,流还具有灵活性和高效性等优点。

例如:我们可以将读、写(输入,输出)视为向水库抽水和放水:

  1. 在Java中,有 InputStreamOutputStream 两个 抽象类,分别表示字节流输入和输出。
  2. ReaderWriter 两个 抽象类,分别表示字符流输入和输出。

通过上面这些类及它们的子类可以实现对文件、网络等输入输出设备的读写操作。

(1)InputStream

常用方法:

返回值类型方法签名说明
intread()读取一个字节的数据,返回 -1 代表已经完全读完了
intread(byte[] b)最多读取 b.length 字节的数据到 b 中,返回实际读到的数量;-1 代表已经读完了
intread(byte[] b, int off, int len)最多读取 len - off 字节的数据到 b 中,放在从 off 开始,返回实际读到的数量;-1 代表以及读完了
voidclose()关闭字节流

注意点1InputStream 是一个抽象类

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

FileInputStream构造方法:

方法名称说明
FileInputStream(File file)利用 File 构造文件输入流
FileInputStream(String name)利用文件路径构造文件输入流

注意点2close 操作非常重要

  1. 打开的IO流需要占用系统资源。如果不及时关闭,会造成系统资源的浪费。例如,如果不及时关闭文件输入流,会导致文件描述符被长时间占用,对其他需要访问该文件的程序造成影响,甚至导致文件被锁定。这样会严重影响系统的稳定性和性能。
  2. 一些网络连接需要通过close操作才能正确释放资源。例如,在使用Socket进行网络通信时,使用完成后必须手动关闭Socket,否则会导致网络资源得不到释放,进而影响系统的正常运行。
  3. close操作还可以防止数据写入丢失。如果在数据写入完成后未执行close操作,那么数据有可能只是写入了缓存而没有写入物理设备,如果在数据写入之前关闭程序,那么数据将可能丢失。

在使用完 IO 流之后,一定要及时调用 close() 方法来关闭流,避免资源泄漏和占用问题的发生。此外,由于InputStream实现了一个特定的interface:Clonable,通常使用try-with-resources 语法可以自动调用 close() 方法,使得代码更加简洁、易读,同时也规避了忘记关闭流的问题。

InputStream操作演示

📝例1.读取英文字符文件(单字节读取)

public static void main(String[] args) throws IOException {
        // 使用try-with-resources语法
        try (InputStream is = new FileInputStream("test01.txt")) {
            while (true) {
                int b = is.read();
                if (b == -1) {
                    // 代表文件已经全部读完
                    break;
                }

                System.out.printf("%c", b);
            }
        }
    }

📝例2.读取英文字符文件(byte[] b方式)

    public static void main(String[] args) throws IOException {
        // 使用try-with-resources语法
        try (InputStream is = new FileInputStream("test02.txt")) {
            // 前提:测试文件不超过1024字节
            byte[] buf = new byte[1024];
            int len;

            while (true) {
                len = is.read(buf);
                if (len == -1) {
                    // 代表文件已经全部读完
                    break;
                }
                // 以字符打印读取到内容
                for (int i = 0; i < len; i++) {
                    System.out.printf("%c", buf[i]);
                }
            }
        }
    }

(2)InputStream 搭配 Scanner 使用

假设此时我们将测试用例test02.txt中内容改为hello你好!+−×÷,再次使用上述读取方式,读取结果如下:

由于使用 InputStream 进行读取时,直接读取到的是字节,而对于一些特殊的字符,由于编码方式的原因,想要用这种方式读取出数据是非常困难的,此时我们可以使用 Scanner 类,搭配 InputStream 进行字符的读取。

构造方法说明
Scanner(InputStream is, String charset)使用 charset 字符集进行 is 的扫描读取

测试:

public static void main(String[] args) throws IOException {
        // 使用try-with-resources语法
        try (InputStream is = new FileInputStream("test02.txt")) {
            try (Scanner scanner = new Scanner(is,"utf-8")) {
                while (scanner.hasNextLine()) {
                    System.out.println(scanner.nextLine());
                }
            }
        }
    }

(3)OutputStream

返回值类型方法签名说明
voidwrite(int b)将指定的字节写入
voidwrite(byte[] b)将 b 这个字符数组中的数据全部写入 os 中
voidwrite(byte[] b, int off, int len)将 b 这个字符数组中从 off 开始的数据写入 os 中,一共写 len 个
voidclose()关闭字节流
voidflush()刷新操作,将数据刷到设备中

重要提醒: 我们知道 I/O 的速度是很慢的,所以,大多的 OutputStream 为了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的一个指定区域里,直到该区域满了或者其他指定条件时才真正将数据写入设备中,这个区域一般称为缓冲区。但很可能会造成这样一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置,调用 flush(刷新)操作,将数据刷到设备中。

注意:同InputStream,OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream。最后不要忘记closeflush

📝例:测试写入(byte[] b 方式)

    public static void main(String[] args) throws IOException {
        try (OutputStream os = new FileOutputStream("test03.txt")) {
            String s = "hello world,你好世界!";
            byte[] b = s.getBytes("utf-8"); //返回一个 byte 数组
            os.write(b);

            // 不要忘记 flush
            os.flush();
        }
    }

(4)字符流 Reader & Writer

由于 ReaderWriter 字符流抽象类和InputStream和OutputStream字节流抽象类使用方法上类似,这里直接给出常用API,就不再展开论述了,大家可以类比上面对字节流的介绍进行理解。

Reader 抽象类常用 API

返回值类型方法签名说明
intread()读一个字符 。-1 代表已经读完了
intread(char[] cbuf)将字符读入数组。返回实际读到的数量;-1 代表已经读完了
intread(char[] cbuf, int off, int len)最多读取 len - off 字节的数据到 b 中,放在从 off 开始,返回实际读到的数量;-1 代表以及读完了
voidclose()关闭流并释放与之相关联的任何系统资源。

Writer 抽象了常用 API

返回值类型方法签名说明
voidwrite(int c)写一个字符
voidwrite(char[] cbuf)写入一个字符数组。
voidwrite(char[] cbuf, int off, int len)写入字符数组的一部分。
voidwrite(String str)写一个字符串
voidwrite(String str, int off, int len)写一个字符串的一部分。
voidflush()刷新流。
voidclose()关闭流,先刷新。

三、文件操作小项目:查找指定文件

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

思路:此题的核心是文件目录的扫描,因为文件系统是树形结构,所以我们使用深度优先遍历(递归)完成遍历,然后进行文件名称和内容的比对即可。

代码展示:

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class SearchDestfile {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 1、用户指定根目录
        System.out.println("请输入要扫描的根目录(绝对路径 OR 相对路径):");
        String rootDirPath = scanner.nextLine();
        File rootDir = new File(rootDirPath);

        // 判断输入目录是否有效
        if (!rootDir.isDirectory()) {
            // 如果输入为无效目录,直接返回
            System.out.println("您输入的根目录不存在或者为非法目录,程序退出!");
            return;
        }

        // 2、用户输入关键词
        System.out.println("请输入需要查找的关键词:");
        String keyWord = scanner.nextLine();

        // 用来存储满足条件的 file 对象
        List<File> result = new ArrayList<>();

        // 3、递归的进行目录/文件的遍历
        searchKey(rootDir,keyWord,result);

        // 4、打印结果集
        System.out.println();
        System.out.println("文件名或文件内容包含关键字“"+keyWord+"”的文件路径如下:");
        for (File file:result) {
            System.out.println(file.getAbsoluteFile());
        }
    }

    private static void searchKey(File directory, String keyWord,List<File> result) {
        // 返回当前目录下的所有文件
        File[] files = directory.listFiles();
        if (files==null || files.length==0) {
            // 如果目录不存在、或者被拒绝访问、或者为空,没必要继续递归,直接返回
            return;
        }

        // 目录里有内容, 就遍历目录中的每个元素
        for (File file:files) {
            if (file.isFile()) {
                // 打印扫描日志
                System.out.println("当前查找到:"+file.getAbsoluteFile());

                // 如果条目是普通文件,查看是否包含关键词
                // (1)先判断文件名是否含有关键词
                if (file.getName().contains(keyWord)) {
                    result.add(file);
                } else {
                    // (2)如果文件名不含关键词,再判断文件内容是否包含关键词
                    // 将内容以字符串形式读取出来
                    String content = readFile(file);
                    if (content.contains(keyWord)) {
                        result.add(file);
                    }
                }

            } else if (file.isDirectory()) {
                // 如果当前file是一个目录
                // 继续递归搜索
                searchKey(file,keyWord,result);
            } else {
                // 如果既不是普通文件,又不是目录,直接跳过
                continue;
            }
        }

    }

    private static String readFile(File file) {
        // 读取文件的整个内容, 返回出来.
        // 使用字符流来读取. 由于咱们匹配的是字符串, 此处只能按照字符流处理, 才是有意义的.
        StringBuilder stringBuilder = new StringBuilder();
        try (Reader reader = new FileReader(file)) {
            while (true) {
                int x = reader.read();
                if (x==-1) {
                    break;
                } else {
                    // 将读取到的字符添加到stringBuilder中,最后返回字符串
                    stringBuilder.append((char)x);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }

}