Java IO|File 文件操作 + 字节流 / 字符流完整笔记 + 递归删除文件实战

Java IO 文件与流全套详解

前言

IO(Input/Output,输入输出)是 Java 后端、文件操作、网络编程的基础,分为文件系统 IO(本地文件读写)与网络 IO。本文完整梳理两大核心模块:

  1. java.io.File 类:本地文件、目录的增删改查、属性判断、递归遍历目录;
  2. 四大 IO 流:字符流 Reader/Writer、字节流 InputStream/OutputStream,包含读写模板、资源关闭、缓冲区、try-with-resources 语法;
  3. 综合实战:递归遍历目录,根据关键词匹配文件名,交互式确认删除文件。

基本概念:硬件和文件概述

文件定义

操作系统里,会把很多硬件设备和软件资源都抽象成"文件",统一进行管理。

但大部分情况下,谈到的文件指的是硬盘里的文件 ⇒ 文件就相当于是对硬盘数据的一种抽象。

那硬盘都有哪些呢?接下来介绍一下硬盘的类别。

硬盘

硬盘有机械硬盘和固态硬盘两种。

机械硬盘(HDD)

  1. 适合顺序读写,不适合随机读写。
  2. 磁头的移动需要时间。

固态硬盘(SSD)

  1. 固态硬盘里是集成程度很高的芯片(类似于CPU、内存)。
  2. 固态硬盘比机械硬盘效率高,无论是顺序读写还是随机读写。
  3. 一般系统盘都要用固态硬盘,若想存储更多的资料,可以额外加机械硬盘。

服务器开发中,两种硬盘都会涉及到,尤其是一些用来存储大规模数据的机器,仍然是机械硬盘为主。

实际的操作是通过文件的方式来操作硬盘。

即使是固态硬盘,读写速度还是要比内存低很多。

内存 VS 外存

内存:

速度快,空间小,贵,数据是断电就消失。

外存:

速度慢,空间大,便宜,数据是断电后还在。

文件路径

  1. 一台计算机上有很多文件,这些文件是通过"文件系统"来进行组织的。
  2. 操作系统,使用"目录"这样的结构来组织文件,使用目录的层次结构来描述文件所在的位置。
  3. 文件所在的位置也就是路径。
  4. 路径又有两种表示:绝对路径和相对路径。

绝对路径

以C:(或者是D:)盘符开头的路径。

相对路径

需要先指定一个目录作为基准目录,从基准目录出发,看沿着哪条路线能找到指定文件(往往以 . 或者 ... 开头的; . 表示当前目录;... 表示当前目录的上一级目录)。

若是命令行进行操作,基准目录就是当前所处的目录;若是图形化界面的程序,基准目录就不好说了。

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

文件类型

从编程的角度,文件有两种类型:文本文件和二进制文件,它们的代码编写方式不同。

文本文件

文件中保存的数据都是字符串(本质上也是二进制数据,它的每一段二进制都严格对应某一套字符编码里的合法字符),保存的内容都是合法的字符。

二进制文件

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

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

直接使用记事本打开这个文件,若打开之后乱码,那么该文件就是二进制文件,否则,就是文本文件。

对文件的操作

对文件的操作分成两种:文件系统的操作+文件内容的操作。

  1. 文件系统的操作:File类,创建文件、删除文件、判定文件是否存在、判定文件类型、重命名...
  2. 文件内容的操作:流对象,读文件、写文件。

文件系统的操作

File类的常见属性

修饰符及类型 属性 说明
static String pathSeparator 依赖于系统的路径分隔符,String类型的表示
static char separator 依赖于系统的目录分隔符,char类型的表示

File类的构造方法

  1. File(File parent,String child):父目录+孩子路径
  2. File(String pathname):文件路径
  3. File(String parent,String child):父目录(用路径表示)+孩子路径

一个File对象,就表示了一个硬盘上的文件,在构造对象时,就需要把这个文件的路径指定进来。

文件名=前缀+扩展名。⇒ 使用路径构造File对象,一定要把前缀和扩展名都带上。

获取文件信息代码示例

java 复制代码
import java.io.File;import java.io.IOException;
public class Main {
    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()); //获取当前文件的修饰过的文件路径
    }
}

运行结果:

.

test.txt

.\test.txt

D:\Java\FileIO.\test.txt

D:\Java\FileIO\test.txt

文件的创建代码示例

java 复制代码
import java.io.File;import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        File file = new File("./test.txt");
        System.out.println(file.exists());//文件是否存在
        System.out.println(file.isDirectory());//是否为目录
        System.out.println(file.isFile());//是否为普通文件

        //创建文件
        boolean ret=file.createNewFile();//返回值为boolean
        System.out.println("ret = "+ret);
        System.out.println(file.exists());//文件是否存在
        System.out.println(file.isDirectory());//是否为目录
        System.out.println(file.isFile());//是否为普通文件
    }
}

运行结果:

true

false

true

ret = false

true

false

true

文件的删除代码示例

java 复制代码
import java.io.File;import java.io.IOException;
public class Main {
    public static void main(String[] args) throws IOException {
        File file = new File("./test.txt");
        file.createNewFile();
        boolean ret= file.delete();
        System.out.println("ret = "+ret);

        File file1 = new File("./test.txt");
        file1.createNewFile();
        file1.deleteOnExit();//JVM运行结束时才会进行删除
        System.out.println(file1.exists());
    }
}

运行结果:

ret = true

true

打印目录下所有文件名的代码示例

java 复制代码
import java.io.File;import java.io.IOException;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws IOException {
        File f=new File("d:/");//若f为文件,则打印结果为null
        String[] files=f.list();//返回File对象代表的目录下的所有文件名
        System.out.println(Arrays.toString(files));
    }
}

创建单层文件夹代码示例

java 复制代码
import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        File f=new File("./java123");
        boolean ret=f.mkdir();//创建File对象代表的目录,单层目录
        if(ret){
            System.out.println("文件夹创建成功");
        }else{
            System.out.println("创建失败,文件夹已存在或路径错误");
        }
    }
}

运行结果:

文件夹创建成功

创建多层文件夹的代码示例

java 复制代码
import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        File f=new File("./java234/aaa/bbb/ccc");
        boolean ret=f.mkdirs();//创建File对象代表的目录,多层目录
        if(ret){
            System.out.println("文件夹创建成功");
        }else{
            System.out.println("创建失败,文件夹已存在或路径错误");
        }
    }
}

运行结果:

文件夹创建成功

文件重命名的代码示例

java 复制代码
import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        File srcFile=new File("./java123");//这个文件必须存在,对它重命名
        File destFile=new File("./java456");//这个文件可以不存在
        boolean ret=srcFile.renameTo(destFile);
        if(ret) System.out.println("重命名成功");
        else System.out.println("重命名失败");
    }
}

运行结果:

重命名成功

文件内容的读写

对文件内容的操作有三个步骤:

  1. 打开文件(构造方法);
  2. 读(输入)/写(输出);
  3. 关闭文件。
    关闭文件这个步骤很重要,close() 是为了释放文件描述符。
    文件描述符表:一个顺序表(数组),一个进程每次打开一个文件,就需要在这个表里面分配一个元素,但这个数组的长度存在上限。
    若代码一直打开文件,而不关闭,就会使文件描述符表里的元素越来越多,一直到把这个数组占满了,后续再尝试打开文件,就会出错。

对文件内容的操作是通过流对象来实现的。

在标准库中,将流对象归并为两大类别:字节流、字符流。

字节流 VS 字符流

字节流:

  1. 对应着二进制文件。
  2. 每次读/写的最小单位是字节。
  3. 提供了两个父类:输入InputStream、输出OutputStream。
    注意:判断是输入还是输出,要站在CPU的角度,比如说把一个数据保存在硬盘中,这个过程就是输出(写)。

字符流:

  1. 对应着文本文件。
  2. 每次读/写的最小单位是字符。(一个字符对应几个字节,要看当前的字符集是哪种)
  3. 提供了两个父类:输入Reader、输出Writer。
  4. 字符流本质上是对字节流进行了又一层封装。字符流能自动把文件中几个相邻的字节,转换成一个字符,帮助我们完成了一个自动查字符集表。

Reader 的使用

简要介绍
  1. Reader 的修饰符及类型是 public abstract,抽象类,不能new出实例,只能new子类。
  2. 创建Reader对象的过程就是"打开文件"过程。
  3. 主要方法:
    (1)read():无参,一次读取一个字符,返回值为int(0-65535),当返回值为-1时表示文件已读完;
    (2)read(char\[\] cbuf):一次读取若干个字符,会把参数指定的cbuf数组给填充满。
    (3)read(char\[\] cbuf,int off,int len):一次读取若干个字符,会把参数指定的cbuf数组中从off位置开始,到len这么长的范围尽量填满。
  4. 为什么字符流Reader能精准一次读一个完整字符,不会读到半个汉字乱码呢?
    把多个Unicode连续放到一起,很难区分从哪里到哪里是一个完整字符,但utf8可以区分。
read()的使用代码示例
java 复制代码
import java.io.Reader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        Reader reader=new FileReader("./test.txt");//该文件中的内容为:你好
        while(true){
            int c= reader.read();
            if(c==-1){//文件中的数据已经读完了
                break;
            }
            char ch=(char)c;
            System.out.println(ch);
        }
        reader.close();//一个文件使用完了,要记得close
    }
}

运行结果:

read(char\[\] cbuf)的代码示例
java 复制代码
import java.io.Reader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        Reader reader=new FileReader("./test.txt");//该文件中的内容为:你好,java
        while(true){
            char[] cubf=new char[4];
            int c= reader.read(cubf);//返回的是读取元素的个数
            if(c==-1){//文件中的数据已经读完了
                break;
            }
            System.out.println("c = "+c);
            for(int i=0;i<c;i++){
                System.out.println(cubf[i]);
            }
        }
        reader.close();
    }
}

运行结果:

c = 4

j

c = 3

a

v

a

注意:一个文件使用完毕之后一定要close()。

以上的书写方式仍然可能会出现文件资源泄露的风险。

可以采用try-finally方式或try with resource方式(要求必须实现Closeable接口)。

修改后的代码:

java 复制代码
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class Test {
    public static void main(String[] args) throws IOException {
        // try() 括号内声明实现AutoCloseable接口的流,代码块结束自动close
        try (Reader reader = new FileReader("./test.txt")) {
            while (true) {
                char[] cubf = new char[4];
                int c = reader.read(cubf); // 返回本次实际读取到的字符数量
                if (c == -1) { // 读到文件末尾
                    break;
                }
                System.out.println("c = " + c);
                for (int i = 0; i < c; i++) {
                    System.out.println(cubf[i]);
                }
            }
        }
        // 不需要写 reader.close()!try-with-resources 无论正常结束/异常都会自动关闭流
    }
}

Writer 的使用

简要介绍
  1. Writer写入文件,默认情况下会把原有文件的内容清空掉再写入,若不想清空,需要在构造方法中加个参数true。
  2. 主要方法:
    (1)write(int c):一次写一个字符。
    (2)write(String str):一次写一个字符串。
    (3)write(char\[\] cbuf):一次写多个字符。
    (4)write(String str,int off,int len)。
    (5)write(char\[\] cbuf,int off,int len)。
代码示例
java 复制代码
public class Main {
    public static void main(String[] args) {
        try(Writer writer=new FileWriter("./test.txt",true)){
            writer.write("我在学习");
        }catch(IOException e){
            throw new RuntimeException(e);
        }
    }
}

InputStream 的使用

简要介绍

主要方法:

(1)read():方法返回值为int(0-255),值为-1时代表读到文件末尾。

(2)read(byte\[\] b).

(3)read(byte\[\] b,int off,int len)。

代码示例
java 复制代码
public class Main {
    public static void main(String[] args) {
        try(InputStream inputStream=new FileInputStream("./test.txt")){
            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]);//字节一般用十六进制来表示,ASCII码
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:

n = 25

e4

bd

a0

e5

a5

bd

ef

bc

8c

6a

61

76

61

e6

88

91

e5

9c

a8

e5

ad

a6

e4

b9

a0

OutputStream 的使用

简要介绍
  1. 和Writer类似,写入文件,默认情况下会把原有文件的内容清空掉再写入,若不想清空,需要在构造方法中加个参数true。
  2. 主要方法:
    (1)write(int b)。
    (2)write(byte\[\] b)。
    (3)write(byte\[\] b,int off,int len)。
代码示例
java 复制代码
public class Main {
    public static void main(String[] args) {
        try(OutputStream outputStream=new FileOutputStream("./test.txt")){
            String s="HelloWorld";
            outputStream.write(s.getBytes());
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

字节流的代码可以转成字符流

代码示例1
java 复制代码
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        try(InputStream inputStream=new FileInputStream("./test.txt");){
            Scanner scanner=new Scanner(inputStream);
            String s=scanner.next();
            System.out.println(s);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:

HelloWorld

代码示例1
java 复制代码
public class Main {
    public static void main(String[] args) {
        try(OutputStream outputStream=new FileOutputStream("./test.txt");
            PrintWriter writer=new PrintWriter(outputStream)){
            writer.printf("hi");
            writer.flush();//PrintWriter这样的类,在写入时,不一定是直接写硬盘,而是先把数据写入到一个内存构成的缓冲区中,此时需要刷新缓冲区,flush
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

实战训练

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

分析:文件系统操作:

  1. list列出目录内容;
  2. 判定文件类型;
  3. 删除文件。
java 复制代码
public class Main {
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        //1.让用户输入指定目录
        System.out.println("请输入要扫描的指定目录:");
        String s=scanner.next();
        File file=new File(s);
        if(!file.isDirectory()){//用户输入的不是目录
            System.out.println("您输入的路径有误");
            return;
        }
        //2.让用户输入要删除文件的关键词
        System.out.println("请输入要删除文件的关键词:");
        String key=scanner.next();
        //3.进行递归扫描
        scanDir(file,key);
    }
    public static void scanDir(File root,String key){
        //1.先列出该目录下的所有文件和目录
        File[] list=root.listFiles();
        if(list==null){
            //当前目录为空,直接返回
            return;
        }
        //2.遍历每个元素并处理
        for(File f:list){
            if(f.isDirectory()){
                //是目录,就递归判定子目录中的内容
                scanDir(f,key);
            }else{
                //普通文件,检查该文件是否要删除
                checkDelete(f,key);
            }
        }
    }
    public static void checkDelete(File f,String key){
        if(!f.getName().contains(key)){
            //无需删除,直接返回
            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{
            //不删除
            System.out.println("取消删除");
        }
    }
}

全文总结

  1. 区分createNewFile()/mkdir()/mkdirs()、delete()/deleteOnExit()、list()/listFiles()常用 API;
  2. IO 流分为字节流(任意文件)、字符流(仅限文本);牢记 CPU 视角区分输入 / 输出;
  3. 资源释放重中之重,优先使用try-with-resources自动关闭流,杜绝文件描述符泄露;
    PrintWriter、Writer 存在缓冲区,必要时调用flush()防止数据滞留内存丢失;
  4. 递归遍历目录是 IO 经典编程题,核心思路:listFiles()遍历 + 子目录递归 + 文件业务判断。

本文全部示例代码均可直接复制运行,,欢迎点赞收藏,评论区交流学习问题!

🔗 系列文章导航

本篇是「Java并发编程系列」的连载内容,点击链接查看完整系列:

🔹 上一篇:Java 并发编程(七)|JUC 工具类与线程安全集合考点

🔹 下一篇:网络初识|基础入门:局域网广域网、IP 端口、TCP/IP 五层模型与封装解封装全过程

👉 点击直达「Java并发编程」专栏合集

相关推荐
伊玛目的门徒1 小时前
试用leetcode之典中典 二数之和问题
java·算法·leetcode
GLDbalala3 小时前
GPU PRO 5 - 4.6 Adaptive Scalable Texture Compression 笔记
笔记
xian_wwq3 小时前
【学习笔记】模型权重管理:Safetensors与私有化Hub(23/35)
笔记·学习
2601_963771374 小时前
How to Scale Your WordPress Store Traffic in 2026
前端·php·plugin
ctrl_v助手4 小时前
Halcon学习笔记2
人工智能·笔记·学习
懒鸟一枚4 小时前
深入理解 Linux 内存、Swap 交换分区与分页机制的关系
java·linux·数据库
YUS云生5 小时前
Python学习笔记·第31天:FastAPI入门——路由、路径参数、查询参数与请求体
笔记·python·学习
我命由我123456 小时前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
考虑考虑6 小时前
Sentinel安装
java·后端·微服务