文件 IO

文件IO

什么是文件?

文件,是在硬盘上存储数据的方式。

操作系统,帮我们把硬盘上的一些细节都封装起来了,程序员只需要了解 文件 相关的接口即可。

硬盘又是什么呢?

硬盘是用来存储数据的,和内存相比,硬盘的存储空间更大,访问速度更慢,成本更低,能够持久化存储

操作系统通过 “文件系统” 这样的模块来管理硬盘。

比如我们经常说的 C盘,D盘

image-20240725140714631

实际上我的电脑只有一个硬盘,操作系统可以通过 文件系统 把这个硬盘抽象成多个硬盘一样

image-20240725140900985

文件系统,就相当于一个管理员,他有自己的格式来组织硬盘上的数据。

如图,Windows 上的文件系统是 NTFS,而Linux 则多是 EXT4。

文件结构

不同的文件系统,管理文件的方式都是类似的。

一般都是通过 “目录 (也就是 文件夹) — 文件” 构成了 “N叉数” 的树形结构。

image-20240725141455972

如图,上述就是一个 树形结构,通过这个树形结构,来管理我们的文件。

路径

D盘 => tmp => cat.jpg;通过这个路线就能找到电脑上的一个文件,这个路线,就被成为 “路径”。

Windows 上使用 / 或者 \ 来分割不同的目录。

d:/tmp/cat.jpg 和 d:\tmp\cat.jpg,这两种方式的效果是一样的

绝对路径

以盘符开头的路径,也叫做 “绝对路径”。

绝对路径相当于是从 “此电脑” 这里出发,找文件的过程。比如刚刚的 d:/tmp/cat.jpg 就是一个绝对路径

相对路径

. 或者 .. 开头的路径,叫做 “相对路径”。

相对路径,需要有一个 “基准目录” / “工作目录”,表示从这个基准目录出发,怎么走才能找到这个文件。

对于 d:/tmp/cat.jpg

. 当前所在目录

如果以 d: 为基准目录,./tmp/cat.jpg 就是要找的路径。

如果以 d:/tmp 为基准目录,./cat.jpg 就是要找的路径。

.. 表示当前所在目录的 上一层 目录

还是以 d:/tmp/cat.jpg 为例,找 cat.jpg 所在

如果以 d:/tmp/111(111也是一个目录)为基准, => …/cat.jpg

如果以 d:/tmp/111/aaa 为基准, => …/…/cat.jpg

同样是一个 cat.jpg 的文件,站在不同的基准目录上,查找的路径是不相同的!!!

文件分类

文件系统上存储的文件,具体来说又可以分成两大类

1)文本文件;2)二进制文件

文本文件,存储的是字符,比如我们常听的 字符集 utf8,他就相当于是一个大表,这个表上的数据的集合,就可以称为是 字符~~

一个最简单的方式,来判断文件是二进制文件还是文本,直接用记事本打开这个文件。如果能看懂,就说明是 文本,打开之后看不懂(也就是我们经常说的乱码),就是二进制文件。

记事本打开文件,就是尝试把当前的数据在码表中进行查询~~

word 文档这种形式,里面其实包含很多信息的,它这种形式,被称为 “富文本”

文件系统操作

后续针对文件的操作,文件和二进制,操作方式是完全不同的~~

文件系统操作:创建文件,删除一个文件,创建目录…

C 语言标准库,不支持文件系统操作,使用 C 删除一个文件,是非常 费劲的~~

即使是 C++,也是从 2017 年才开始支持文件系统操作的。

对于Java,引入了 io这个包, io 就是 input 和 output,就是对文件的 输入和输出。

File 概述

Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。。File只能对文件操作,不能操作文件的内容。

构造方法:

image-20240725152031563

我们一般多使用 第二种,第二种参数的字符串,就表示一个路径。可以是绝对路径,也可以是相对路径。

【注意】在Java中,创建一个 File 对象并不会创建或确认文件的实际存在性,它仅仅是一个文件路径的抽象表示。换句话说,File 对象仅仅是一个抽象的文件路径表示,它存储了文件的路径信息,但并不进行实际的文件操作。具体的文件操作(如创建、读取、写入、删除等)需要通过文件操作的方法来实现。

方法:

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

站在操作系统的角度来看待:目录也是文件

操作系统的文件是一个更广义的概念,具体来说有很多不同的类型

1、普通文件(通常见到的文件)

2、目录文件(通常见到的文件夹)

**windows 上,目录之间的分隔符,可以使用 / 也可以使用 \ **

Linux 和 mac 上面,就只支持 /

所以即使在 windows 上,也尽量使用 / ,使用 \ 在代码中需要搭配转义字符

下面通过代码,来看具体的用法:

分组看一下,第一组,获取文件路径相关

image-20240725162919164

第二组,判断 文件/目录 属性

image-20240725164520794

第三组,删除文件

image-20240725165237632

第四组,创建目录

image-20240725170423317

第五组,文件重命名:文件重命名,也可以起到移动文件的效果

image-20240725171146912

文件内容操作

上述我们可以发现,File只是操纵的文件的名字、路径等,但是并不能对文件的内容进行修改,下面就来看一下,如何 读/写文件。

文件这里的内容本质是来自于硬盘,硬盘又是操作系统管理的,使用某个编程语言操作文件,本质上都是需要调用系统的 api

虽然不同的编程语言操作文件的 api 有所差别,但是基本步骤都是一样的

文件内容操作的核心步骤有四个:

1、打开文件 2、关闭文件 3、读文件 4、写文件

同时,操作系统里有个 **流(stream)**的概念,对于文件,便有 文件流。文件流类似水流,例如:有一个1000ml的水桶,可以分为10次接(每次100ml)、也可以分为5次接(每次200ml)等等。而操作文件时,也是一样的,可以分多次读取。

Java IO可以分为输入流和输出流,分别对应于读取和写入数据

image-20240725174204361

Java l0 流是一个比较庞大的体系,涉及到非常多的类,这些不同类, 都有各自不同的特性但是总的来说,使用方法都是类似的.

  1. 构造方法,打开文件.

  2. close 方法,关闭文件

  3. 如果衍生自 InputStream 或者 Read, 就可以使用 read 方法来读数据

  4. 如果衍生自 OutputStream 或者 Writer 就可以使用 write 方法来写数据了

Reader 使用

Java 中,Reader 是用于字符流的输入 的一个 抽象类,不能直接 new Reader,因此他提供了一组方法,让我们来创建。

Reader 的创建和使用

我们经常使用此方式来创建:

创建:Reader 对象名 = new FileReader( );

【注】:

FileReader 构造方法,可以填写一个文件路径(绝对路径/相对路径都行),也可以填写一个构造好的 File 对象

示例:

public static void main(String[] args) throws IOException {
    // FileReader 构造方法,可以填写一个文件路径(绝对路径/相对路径都行),也可以填写一个构造好的 File 对象
    Reader reader = new FileReader("d:/test.txt");
    // 关闭资源
    reader.close();
}

image-20240725184119698

reader.close(),这个操作是非常重要的,它的作用是释放必要的资源

如果不释放,就会出现 “文件资源泄露” 的问题,这是一个非常严重的问题!

一个进程打开一个文件,是要从系统这里申请一定的资源的,它会占用进程的 pcb 里的文件描述符表中一个内容。这个 文件描述符表,是一个顺序表,长度有限,并且不会自动扩容,一旦一直打开文件,而不去关闭不用的文件,文件描述符表就会被占满,后续就无法继续打开新的文件了!!!

上面的代码中,如果在 close之前,出现错误,就会导致 close 没有执行,因此可以捕获一下异常,让 close,始终能够执行

public static void main(String[] args) throws IOException {
    // FileReader 构造方法,可以填写一个文件路径(绝对路径/相对路径都行),也可以填写一个构造好的 File 对象
    Reader reader = new FileReader("d:/test.txt");
    try {
        // 中间代码无论出现啥情况, close 都能保证执行到
    } finally {
        // 关闭资源
        reader.close();
    }
}

上面这种方式,虽然可以做到,但是会感觉很啰嗦,很麻烦,因此还有一种更优雅的方法

try with resources,就是 try ()

try (Reader reader = new FileReader("d:/test.txt")) {
    // 中间代码无论出现啥情况, close 都能保证执行到
}

这接这样写就可以了,只要 try 代码块执行完毕了,就会自动调用到 close 方法~~

这种设定,就类似于 synchronized,出了代码化,自动解锁

另外,try with resources 也支持打开多个文件

try (Reader reader = new FileReader("d:/test.txt");
     Reader reader2 = new FileReader("d:/test.txt")) {
	// 中间代码无论出现啥情况, close 都能保证执行到
}

这样写,也是可以的

读文件

我们可以使用 reader.read(),来读取文件,这个 read 方法有四种形式

image-20240725231950708

【注】读的时候,其内部维护着 “位置指针” 或者称为“当前位置”的概念,每次调用 read() 方法时,会从当前位置读取字符,并将位置指针移动到下一个要读取的位置。(这四个方法都会记录)

下面具体来看一下:

第一种 read():

  • 含义:一次读取一个 char 类型的字符
  • 返回值:int,若成功读取到一个字符,则返回他的 ASCII 码;如果读到末尾,则返回 -1

这里,也有会有这样一个疑问?

我的文件是 utf8 格式,一个字符应该是 3 个字节;这里读出来的一个字符是 2 个字节 ???

java 的 char 类型是使用 unicode 编码的(一个字符两个字节),使用这个方法读一个字符,java 标准库内部会帮我们自动完成转换!!程序猿感知不到。一个个字符, unicode 编码和 utf8 是不同的!!

栗子:

image-20240725233244535


第二种 read(char[] cbuf):

  • 含义:会把读到的内容,填充到参数的这个 cbuf 数组中,此处的参数,相当于是一个 “输出型参数”
  • 返回值:int,返回读取到的字符个数;若读到末尾,返回 -1

reader.read(buf) 方法的作用是从 reader 中读取数据,并将读取的内容存储到 buf 数组中。这里 buf 数组在方法调用前是传入的,但在方法调用后会被方法修改,存储了读取的数据,因此可以说 buf 在这个过程中扮演了一个输出型参数的角色。

尽管在 Java 中常见的做法是使用方法的返回值来表示输出型参数(例如 read() 方法返回读取的字符数),但在这种情况下,buf 数组本身也可以看作是一个输出型参数,因为它在方法调用后存储了方法执行的结果。

栗子:

image-20240725234352144


第三种read(CharBuffer Target)

第三种和第二种是一样的,只不过这里是一个 buffer数组,用的很少,这里就不说这个了。


第四种 read(char[] buff, int off, int len)

  • 含义:也是把内容读取到 buff 数组里,不过是从 0 号位置开始,读取 len 个
  • 返回值:int,返回读取到的字符个数;若读到末尾,返回 -1

有时候,我们在读取文件内容时,可能会涉及到有很多个小文件,都需要读取,并且频道一起,就可以使用上述方法了

image-20240725235423452


一个应用场景题,怎么读取一个较大的文件? 答:循环读取

eg:

try(Reader reader = new FileReader("d:/test.txt")) {
    while (true) {
        char buf[] = new char[1024];
        int n = reader.read(buf);
        if(n == -1) {
            // 读到文件末尾了
            break;
        }
        for (int i = 0; i < n; i++) {
            System.out.print(buf[i] + ",");
        }
    }
}

假设,test.txt 较大,就可以每次读取 1024 个字符


InputStream

InputStream 是字节流,也就是它的操纵单位是 字节,而不是一个字符。它的用法和 Reader 相似。

读的时候,内部会记录上次读取的位置,新一轮循环的时候,会从上次读取结束的位置开始读,而不是每次都重头开始

InputStream 的创建和使用

和 Reader 一样,创建的时候也不能直接new,需要借助它提供的方法,同时也需要 close

try (InputStream inputStream = new FileInputStream("d:/test.txt")) {
            
}

上面,就是它在代码中经常出现的形式了。

也许有人会问,test.txt 不是文本文件吗?怎么也可以用 字节流的InputStream 打开?

事实上,文本文件,也可以使用 字节流 打开。只不过此时,你读到的每个字节,就不是完整的字符了。

读文件

和 reader 类似,inputStream 也提供了3个方法(比 reader 少了那个不常用的),并且用法也是一样的

image-20240726115356477

也会记录上次读的位置,下次读的时候,就从新位置开始


第一种 read(): 基本不用 (谁没事 一个字节 一个字节 的读)

  • 含义:一次读取一个字节
  • 返回值:
    • 如果读取到了有效的字节数据,则返回其字节值(0 到 255 之间的整数),表示成功读取一个字节。
    • 如果到达流的末尾(即没有更多的数据可读),则返回 -1,表示已经读取完毕。

这里虽然返回值是 int,但是他只用了一个字节。


第二种 read(byte[] b):

含义

  • read(byte[] b) 方法从输入流中读取多个字节,并将它们存储到提供的字节数组 b 中。
  • 返回值是实际读取的字节数,如果到达流的末尾,则返回 -1

第三种 read(byte[] b, int off, int len)

含义

  • read(byte[] b, int off, int len) 方法从输入流中读取最多 len 个字节,并将它们存储到提供的字节数组 b 中,从数组的偏移量 off 开始。
  • 返回值是实际读取的字节数,如果到达流的末尾,则返回 -1

字节流读取的栗子:

image-20240726123834817

image-20240726124329957

输出结果是正确的。

但是,Java中 直接使用 字符/字节 的情况很少,都是把它们转化为字符串(String)。

Java中,对于字符串的操作,只有你想不到,没有它做不到。因此,开发中,还是 字符串用的多。

字节流转化为字符串

那么如何转化为字符串呢?

首先想到便是 String 类的构造方法 (String 内部,会自动完成编码的转化)

image-20240726124852009

如图所示,使用这种方式便可以转化为字符串,但是这种方式 既不美观,也不方便,因此就有了下面的方式。

借助Scanner

我们平时都是这样用 Scanner

Scanner scanner = new Scanner(System.in);

这里,System.in 相当于是一个特殊的文件,对应到 “标准“输入流。

在操作系统中,“文件” 是一个广义的概念。普通的硬盘上的文件,是文件,网络编程中的网卡(Socket) 也是文件。

这是 Scanner 的构造方法

image-20240726143631781

它的参数是一个 InPutStream 流,对于 Scanner 来说,它对于参数都是一视同仁的。只是把当前读到的字节数据进行转换~~ 不关心这个数据究竟是来自于标准输入,还是来自于文件,还是来自于网卡…

因此,我们就可以借助 Scanner 来进行字节流的转化

image-20240726144015574

这样,我们就可以正常读出文件的内容,并且,scanner的所有方法,都是可以正常使用的

但是,要注意, Scanner 只是用来读文本文件的。不适合读取二进制文件。在标准库中,还提供了一些工具类, 辅助更方便的读写二进制格式的文件,比如:BufferedInputStreamBufferedOutputStreamDataInputStreamDataOutputStream 等,这里就不详细展开了


Writer

写 (删输出) 操作。它的使用方法 和 输入非常相似,Write 之前要打开文件,用完了也需要关闭文件。

经常这样来构造:Writer writer = new FileWriter()

writer 的 方法:

image-20240726145558478

write 的方法是很多的,尤其是 writer 是可以一次直接写一个字符串,非常方便

栗子:

try (Writer writer = new FileWriter("d:/test.txt")) {
    // writer 是可以一次直接写一个字符串。这个是非常方便的
    writer.write("hello java");
}

这样,就会在 test.txt 这个文件里,写入 hello java

但是,默认情况下,输出流对象 (无论 字符流 还是 字节流) 打开一个文件,是会直接清空里面的内容的,但是可以使用追加的方式来写,此时就不会清空内容了。

try (Writer writer = new FileWriter("d:/test.txt", true)) {// 追加,默认是 false
    // writer 是可以一次直接写一个字符串。这个是非常方便的
    writer.write("hello java");
}

这样,在打开文件的时候,加上 true,就会保留之前的内容。

【注意】新打开的时候,才会清空。打开之后,后续多次写入,是类似于追加的效果,其内部也会记录上次写入的最后一个位置。

OutputStream

OutputStream outputStream = new FileOutputStream()

image-20240726150458517

它的使用 和 Writer 是一样的,只不过他是按照字节的方式来写入的。同时,也是可以追加写的。

对于第一个方法,参数是一个 int,该方法只会写 该int 的低8位,因为int 是 4个字节,32位;他只写一个字节,8位。

对于InputStream ,可以搭配 Scanner,简化代码。同样,OutputStream 也有搭档,他就是 PrintWriter,他就相当于 System.out , 不过,System.out 是输出到控制台,而 PrintWriter 则是输出到文件中。


相关练习

我们学会了文件的基本操作 + 文件内容读写操作,接下来,我们实现一些小工具程序,来锻炼我们的能 力。

示例一

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

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

// 扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
public class Demo10 {
    private static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        // 1. 让用户输入一个目录,后续的查找都是针对这个目录来进行的
        System.out.println("请输入要搜索的根目录:");
        File rootPath = new File(scanner.next());
        // 2. 再让用户输入要搜索/删除的关键词
        System.out.println("请输入要删除的关键词:");
        String world = scanner.next();
        // 3. 判断一下当前输入的目录是否有效
        if (!rootPath.isDirectory()) {
            System.out.println("根目录不存在!");
            return;
        }
        // 4. 遍历目录,从根目录出发,按照 深度优先(递归) 的方式,进行遍历
        scanDir(rootPath, world);
    }

    public static void scanDir(File currenDir, String word) {
        // 1. 先列出当前目录中都包含哪些内容
        File[] files = currenDir.listFiles();
        if (files == null || files.length == 0) {
            // 空的目录或非法的目录
            return;
        }
        // 2. 遍历列出的文件,分两个情况分别讨论
        for (File f : files) {
            // 加个日志,方便看程序执行的过程
            System.out.println(f.getAbsolutePath());
            if (f.isFile()) {
                // 3. 如果当前文件是普通文件,看看文件名是否包含了 world,来决定是否要删除
                dealFile(f, word);
                System.out.println("删除成功!");
            } else {
                // 4. 如果当前文件是目录文件,就递归执行 scanDir
                scanDir(f, word);
            }
        }
    }

    private static void dealFile(File f, String word) {
        // 1. 先判定当前文件名是否包含 word
        if (!f.getName().contains(word)) {
            // 此时这个文件不包含 word 关键词。直接跳过
        }
        // 2. 包含 word 就需要询问用户是否要删除该文件
        System.out.println("该文件是:" + f.getAbsolutePath() + ",是否要确定删除(Y/N)");
        String choice = scanner.next();
        if (choice.equals("Y") || choice.equals("y")) {
            f.delete();
        }
        // 如果是其他值,反正不是确定删除,忽略
    }
}

示例二

进行普通文件的复制

相当于 Ctrl+c => Ctrl+v; 打开 A,依次读出每个字节,在写到 文件B 就行了

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

//进行普通文件的复制
public class Demo11 {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        // 1. 输入路径并且判断合法性
        System.out.println("请输入要复制的源文件路径:");
        String src = scanner.next();
        File srcFile = new File(src);
        if (!srcFile.isFile()) {
            System.out.println("您输入的源文件路径非法!");
            return;
        }
        System.out.println("请输入要复制到的目标路径:");
        String dest = scanner.next();
        File destFile = new File(dest);
        // 不要求目标文件本身存在,但是要保证目标文件所在的目录,是存在的
        // 假设目标文件写作 d:/tmp/test.png,就需要保证 d:/tmp 目录是存在的
        if (!destFile.getParentFile().isDirectory()) {
            System.out.println("您输入的目标文件路径非法!");
            return;
        }
        // 2. 进行复制操作的过程。按照字节流打开
        try (InputStream inputStream = new FileInputStream(srcFile);
             OutputStream outputStream = new FileOutputStream(destFile)) {
            while (true) {
                byte[] buffer = new byte[1024];
                int n = inputStream.read(buffer);
                System.out.println("n = " + n);
                if (n == -1) {
                    System.out.println("读取到 eof,循环结束。");
                    break;
                }
                outputStream.write(buffer,0,n);
            }
        }
    }
}

byte[] buffer = new byte[1024];

这里 buffer的大小,是可以更改的,buffer越大,循环的次数越少,就能降低访问硬盘的次数,提高效率;但是,buffer大的前提,是内存要够。因此,设计的时候,不能盲目设置buffer的大小。过大过小,都会造成问题。

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如果Null没有null

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值