Java 文件 IO 全面解析:从路径到字节流与字符流

文件 IO 是 Java 开发中最基础也最常用的能力之一。无论是读写配置文件、处理文件上传下载,还是解析日志,都离不开对文件的操控。本文将从文件路径、文件类型出发,系统讲解 File 类与字节流/字符流的核心用法,帮你写出健壮、规范的 IO 代码。

目录

    1. 什么是文件
    1. 路径
    1. 文件的类型(文本 / 二进制)
    1. File 类
    1. 读写(流)
    • 5.1 字节流 / 5.2 字符流
    • 5.3 FileInputStream / 5.4 FileOutputStream
    • 5.5 Reader / 5.6 Writer

1. 什么是文件

这里所说的"文件",指的是硬盘上存储的文件。我们只需调用操作系统提供的 API 来操作文件即可。

2. 路径

在 Windows 上,路径从盘符开始;在 Linux 上,路径从根目录(/)开始。

image-20250918153715333绝对路径

image-20250918153740286

下图展示的是相对路径。

. 表示当前目录,是一个相对路径。

File file =new File("./test.txt");

.. 表示上一级目录。

/ 为正斜杠,\ 为反斜杠。

3. 文件的类型

文本文件

存储的是字符。

二进制文件

没有限制,可以存储任意数据。

4. File 类

image-20251001155550019

以上图为例。

        File file1 =new File("D:/MSIc4909.txt");
        System.out.println(file1.getName());
        System.out.println(file1.getParent());
        System.out.println(file1.getPath());
        System.out.println(file1.getAbsoluteFile());
        System.out.println(file1.getCanonicalPath());

image-20251001160741871

执行结果如上,最后一项为空(null)是因为当前进程的工作目录下找不到该相对路径所指向的文件。

        File file1 =new File("c:/MSIc4909.txt");
        System.out.println(file1.exists());
        System.out.println(file1.isFile());
        System.out.println(file1.isDirectory());

该文件是我虚构的,所以返回值都为 false

image-20251001161605061

delete() 有两种:第一种是直接删除;第二种(deleteOnExit)是先"登记"删除,待进程结束后再真正删除。

        File file1 =new File("E:/MSIc4909.txt");
        System.out.println(file1.exists());
        file1.createNewFile();
        System.out.println(file1.exists());
        file1.delete();
        System.out.println(file1.exists());

image-20251001162409879

image-20251001163045838

此外,renameTo 可用于重命名和移动文件。

file.renameTo(FILE);

image-20251001164158923

5. 读写(流)

文件的核心操作其实就是读和写,为此 Java 提供了流(Stream)对象。

5.1 字节流

以字节为基本单位进行读写,常用来操作二进制文件。

输入:

InputStream

输出:

OutputStream

5.2 字符流

以字符为基本单位进行读写,常用来操作文本文件。

输入:

Reader

输出:

Writer

上述四个类都是抽象类,因此我们重点学习它们各自的子类,一共八个类。

5.3 FileInputStream

1. 打开文件
InputStream inputStream = new FileInputStream("./s.txt");

括号中的参数是文件目录,既可以是相对路径,也可以是绝对路径。

2. read

image-20251001170831390

read 有三个重载方法:

  1. 一次读一个字节,返回 -1 表示已读到文件末尾(由于计算机按字节寻址,这是最基础的读取方式)。
  2. 一次读若干字节,参数是一个数组(输出型参数)——方法会把读取到的内容填充到该数组中。之所以这样设计,是因为方法通常只能返回一个值;它会尽可能地把数组 b 填满。
  3. 与第二种类似,但多了两个参数:off 表示从 b[off] 开始写入,len 表示最多写入 len 个字节;若剩余数据不足,则读到的就少。
3. 关闭文件
inputStream.close();
        inputStream.close();
        System.out.println(inputStream.read());

之所以要强调关闭,是因为:读写文件时会在操作系统的"文件描述符表"中占用一项;如果打开的文件过多(描述符耗尽),就会引发问题。

如果你不及时释放,就会出现文件资源泄露(这是一类非常严重的 Bug)

有时我们需要处理异常,下面介绍几种常见的写法。

写法一:try-catch 环绕

        try{
            InputStream inputStream = new FileInputStream("./s.txt");
            int n=0;
            while(n!=-1){
                byte[] bytes =new byte[1024];
                n=inputStream.read(bytes);
                for (int i = 0; i < n; i++) {
                    System.out.println(bytes[i]);
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            inputStream.close();
        }

这种写法肯定是错的,因为 try 中声明的变量作用域无法延伸到 finally

写法二:改进版(变量提到 try 外)

但这样变量有可能直接初始化失败而为 null

        InputStream inputStream =null;
        try{
            inputStream = new FileInputStream("./s.txt");
            int n=0;
            while(n!=-1){
                byte[] bytes =new byte[1024];
                n=inputStream.read(bytes);
                for (int i = 0; i < n; i++) {
                    System.out.println(bytes[i]);
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            inputStream.close();
        }

写法三:最终正确版(增加判空)

为此我们还需要加一个判空判断,最终形式如下:

        InputStream inputStream =null;
        try{
            inputStream = new FileInputStream("./s.txt");
            int n=0;
            while(n!=-1){
                byte[] bytes =new byte[1024];
                n=inputStream.read(bytes);
                for (int i = 0; i < n; i++) {
                    System.out.println(bytes[i]);
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            if(inputStream!=null) {
                inputStream.close();
            }
        }

上面的写法显然比较繁琐,有没有更简洁的方案?有的。

使用 try-with-resources,会在 try 结束后自动调用 close,并妥善处理 close 自身可能抛出的异常:

        try(InputStream inputStream = new FileInputStream("./s.txt")){
            int n=0;
            while(n!=-1){
                byte[] bytes =new byte[1024];
                n=inputStream.read(bytes);
                for (int i = 0; i < n; i++) {
                    System.out.println(bytes[i]);
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }

注意:如果你需要把流对象返回出去,或在关闭前后做额外处理(例如加锁控制),try-with-resources 就不太合适了,因为它会自动关闭流。

当然,如果调用完文件后进程马上退出,那不显式调用 close 也没问题。

5.4 FileOutputStream

1. 创建与打开文件
OutputStream outputStream =null;
outputStream = new FileOutputStream("./s.txt");

注意:以这种方式打开文件会清空原有内容。如果需要追加而非覆盖,可以传入第二个参数来解决。

2. 写入

image-20251004114851765

写入方法与读取类似:

  1. 一次写一个字节(类似 ASCII 码值)。
  2. 写入整个字节数组。
  3. 写入字节数组的一部分。
            outputStream = new FileOutputStream("./s.txt");
            outputStream.write(105);

清空内容后,写入了一个 i(ASCII 105)。

            outputStream = new FileOutputStream("./s.txt",true);
            outputStream.write(105);

这次没有清空原有内容(追加模式)。

image-20251004115709670

3. 关闭文件
outputStream.close();

5.5 Reader

1. 打开文件
Reader reader = new FileReader("./s.txt")
2. read

image-20251004120203969

  1. 一次读一个字符。
  2. 尽可能把整个数组读满。
  3. CharBuffer 是对 char 数组的封装。
  4. 读入后以 off 为起始下标、写入长度为 len 的内容。
char[] bytes =new char[1024];
reader.read(bytes);
for (int i = 0; i < n; i++) {
    System.out.println(bytes[i]);
}

5.6 Writer

1. 打开
Writer writer= new FileWriter("./s.txt")
2. 写入

image-20251004122350497

写入方法比较直观,此处不再赘述。

3. 关闭
    writer.close();

小结

本文系统梳理了 Java 文件 IO 的核心知识:文件路径(绝对路径与相对路径)以及文本文件、二进制文件的区别;File 类对文件路径与元信息的查询、创建、删除、重命名等操作;以及以 InputStream/OutputStream 为代表的字节流、以 Reader/Writer 为代表的字符流。其中尤其要牢记及时释放资源(手动 close 或使用 try-with-resources),避免文件描述符耗尽导致资源泄露。掌握这些基础,你就能从容应对绝大多数文件读写场景。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐