Java 从入门到精通(十二):File 与 IO 流基础,为什么程序“读写文件”时总是容易出问题?

Java 从入门到精通(十二):File 与 IO 流基础,为什么程序"读写文件"时总是容易出问题?

前一篇我们把异常处理与自定义异常讲清楚了。

但很多人学完异常之后,真正第一次强烈感受到"异常不是语法题,而是工程题"的场景,往往不是算术运算,也不是集合操作,而是:

文件读写。

比如你很快就会遇到这些问题:

  • 为什么同样一段代码,昨天能读文件,今天就报错?
  • 为什么文件明明存在,程序却说找不到?
  • 为什么写进去的中文打开后变成乱码?
  • 为什么复制文件时,一不小心就把内容写坏了?
  • 为什么代码里已经 close() 了,资源问题还是层出不穷?

这些问题表面看起来零散,背后其实都指向同一件事:

程序和外部世界打交道时,事情就不再像内存里的变量那样"理所当然"。

内存里的数据是瞬时的,程序一停就没了;但文件是落在磁盘上的,是持久化的,是要跨时间、跨程序、甚至跨机器被读取的。

所以学 File 与 IO,不是为了背几个类名,而是为了建立一套很重要的理解:

  1. Java 里"文件"到底怎么表示

  2. 程序是如何把数据写出去、再读回来的

  3. 为什么字符、字节、路径、缓冲区这些概念必须分清

  4. 怎样写出不容易出错的基础读写代码

这一篇不追求一次把所有 IO API 背完,而是先把最核心的骨架搭起来。


一、先搞清楚:什么是 File?什么是 IO?

很多初学者会把这两个词混在一起。

其实它们不是一回事。

1)File:表示"文件或目录"这个对象

在 Java 里,File 更像是:

对磁盘路径的一种抽象表示。

比如:

java 复制代码
File file = new File("demo.txt");

这并不等于"文件内容已经读进来了"。

它只是告诉 Java:

  • 这里有个路径
  • 这个路径可能指向一个文件
  • 也可能指向一个目录
  • 你可以基于它去判断是否存在、是否可读、是否可写、是否创建

所以你要先记住:

File 主要负责"描述文件",不负责真正"读写内容"。

2)IO:Input / Output

IO 就是输入输出。

站在程序角度:

  • Input:把外部数据读进程序
  • Output:把程序中的数据写到外部

例如:

  • 从磁盘读取文本文件
  • 把日志写入文件
  • 从网络读取数据
  • 向控制台输出内容

它们本质上都属于 IO。

所以这两个概念的关系可以简单理解成:

  • File:告诉你"目标是谁"
  • IO:告诉你"数据怎么进出"

二、为什么文件操作总比普通变量更容易出问题?

因为变量大多活在内存里,环境相对可控。

例如:

java 复制代码
int age = 18;
String name = "Tom";

这类代码如果报错,通常原因很直接。

但文件操作会额外受很多外部因素影响:

  • 文件是否真的存在
  • 路径写得对不对
  • 程序有没有权限访问
  • 当前工作目录是不是你以为的那个目录
  • 读写时编码是否一致
  • 资源有没有及时关闭

所以 IO 之所以让初学者头疼,不是因为 API 难,而是因为它天然带着不确定性

这也是为什么文件读写和异常处理经常是连在一起学的。


三、File 类最常用的能力有哪些?

先看一个最基础的例子:

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

public class Demo {
    public static void main(String[] args) {
        File file = new File("test.txt");

        System.out.println("文件是否存在:" + file.exists());
        System.out.println("是不是文件:" + file.isFile());
        System.out.println("是不是目录:" + file.isDirectory());
        System.out.println("文件名:" + file.getName());
        System.out.println("绝对路径:" + file.getAbsolutePath());
    }
}

这些方法非常适合做"前置判断"。

比如你要读一个文件,最好不要上来就直接读,而是先确认:

  • 它存在吗?
  • 它真的是文件吗?
  • 路径是不是你想要的那个?

很多"找不到文件"的问题,本质上不是文件丢了,而是:

你以为程序在 A 目录运行,实际上它在 B 目录运行。

所以 getAbsolutePath() 这个方法,调试时非常有用。


四、创建文件和目录时要注意什么?

1)创建文件

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

public class Demo {
    public static void main(String[] args) throws IOException {
        File file = new File("note.txt");
        if (!file.exists()) {
            file.createNewFile();
        }
    }
}

这里的 createNewFile() 会抛 IOException,因为创建文件这件事并不是百分之百能成功。

可能失败的原因包括:

  • 路径不合法
  • 没有权限
  • 磁盘或目录状态异常

2)创建目录

java 复制代码
File dir = new File("data");
if (!dir.exists()) {
    dir.mkdir();
}

如果要创建多级目录,更常用的是:

java 复制代码
dir.mkdirs();

区别是:

  • mkdir():只能创建单级目录
  • mkdirs():可以连父目录一起创建

这个细节很常见,也很容易写错。


五、为什么 IO 要分"字节流"和"字符流"?

这是 File 与 IO 入门里最重要的一个分界线。

1)字节流:适合处理一切二进制数据

字节流按 byte 来读写。

典型类:

  • InputStream
  • OutputStream
  • FileInputStream
  • FileOutputStream

它适合处理:

  • 图片
  • 视频
  • 音频
  • 压缩包
  • 以及任何"你不想擅自按字符解释"的数据

2)字符流:适合处理文本

字符流按 char 来读写。

典型类:

  • Reader
  • Writer
  • FileReader
  • FileWriter

它更适合处理:

  • 普通文本
  • 配置文件
  • 日志内容
  • 用户可读字符串

为什么一定要区分?

因为"文本"最终在底层也是字节。

字符流做的事情,本质上是:

帮你处理"字节 ↔ 字符"之间的转换。

如果你处理的是纯文本,用字符流通常更自然。

如果你处理的是图片、压缩包这类二进制内容,用字符流就可能直接把数据搞坏。

所以不要死记,先记原则:

  • 文本优先考虑字符流
  • 二进制优先考虑字节流

六、先看最基础的字节输出:把内容写进文件

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

public class Demo {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("a.txt")) {
            fos.write(65);
            fos.write(66);
            fos.write(67);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行后,文件里会出现:

text 复制代码
ABC

因为:

  • 65 对应字符 A
  • 66 对应字符 B
  • 67 对应字符 C

但你也会发现,这种写法并不直观。

实际开发里更常见的是写字节数组:

java 复制代码
try (FileOutputStream fos = new FileOutputStream("a.txt")) {
    String str = "hello";
    fos.write(str.getBytes());
} catch (IOException e) {
    e.printStackTrace();
}

这里的 getBytes() 本质上是把字符串转成字节数组再写出去。


七、读取文件时在做什么?

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

public class Demo {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("a.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里要理解两个关键点。

1)read() 每次读一个字节

它返回的是一个 int,不是 byte

原因是 Java 需要用 -1 表示"读到文件末尾了"。

2)为什么还能强转成 char

因为这个例子里文件内容刚好是普通英文字符。

但如果内容是中文,或者编码不一致,这种写法就容易出现乱码。

所以它更适合作为"理解 IO 原理"的示例,而不是直接照搬到所有文本读取场景。


八、字符流为什么更适合读文本?

如果你读取的是文本,字符流代码通常更顺手。

例如:

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

public class Demo {
    public static void main(String[] args) {
        try (FileReader fr = new FileReader("a.txt")) {
            int ch;
            while ((ch = fr.read()) != -1) {
                System.out.print((char) ch);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

它和字节流看起来很像,但思路上已经更偏向"文本字符"而不是"原始字节"。

对应地,写文本也可以用:

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

public class Demo {
    public static void main(String[] args) {
        try (FileWriter fw = new FileWriter("b.txt")) {
            fw.write("Java IO 入门");
            fw.write("\n学会读写文件很重要");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这类写法更适合初学者理解"写文本文件"这件事。


九、为什么现在更推荐 try-with-resources?

很多旧教程会这样写:

java 复制代码
FileInputStream fis = null;
try {
    fis = new FileInputStream("a.txt");
    // 读取逻辑
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这当然没错,但太啰嗦,而且很容易漏。

现在更推荐:

java 复制代码
try (FileInputStream fis = new FileInputStream("a.txt")) {
    // 读取逻辑
} catch (IOException e) {
    e.printStackTrace();
}

因为它会自动帮你关闭资源。

这非常重要。

文件流、网络流、数据库连接,本质上都属于"资源"。

如果你总忘记关,轻则浪费资源,重则导致句柄耗尽、文件被占用、程序异常。

所以从工程习惯上说:

能用 try-with-resources,就尽量别手写 finally 关闭。


十、初学 IO 最容易踩的 5 个坑

1)把相对路径想当然

比如你写:

java 复制代码
new File("data.txt")

你以为它在项目根目录,实际上它可能相对于当前运行目录。

所以一旦出问题,先打印:

java 复制代码
System.out.println(file.getAbsolutePath());

2)文本和二进制不分

  • 文本:优先字符流
  • 图片、压缩包、音视频:优先字节流

别混着来。

3)读写完不关流

这会带来各种隐蔽问题。

现在直接养成 try-with-resources 的习惯。

4)忽略编码问题

最典型的表现就是:

  • 写进去是中文
  • 打开之后变乱码

这通常不是"Java 坏了",而是编码没处理一致。

5)一次只读一个字符/字节还觉得没问题

教学示例可以这样写,但真实开发里,通常会结合缓冲数组、缓冲流来提升效率。

这也是为什么下一篇继续讲 IO 时,通常就会进入:

  • 缓冲流
  • 高效复制
  • 编码转换
  • 更现代的 NIO

十一、你现在应该建立的不是 API 记忆,而是 IO 思维

学到这里,真正重要的不是你能不能一口气背出所有流类名,而是你有没有形成下面这套判断:

当你要操作文件时,先问自己 4 个问题:

  1. 目标是什么?

    • 文件还是目录?
    • 路径是否正确?
  2. 处理的是什么数据?

    • 文本还是二进制?
  3. 应该用什么流?

    • 字节流还是字符流?
  4. 资源怎么安全关闭?

    • 是否使用 try-with-resources?

如果你形成了这套思路,后面学缓冲流、转换流、NIO、序列化时就不会乱。


十二、最后总结

这一篇你要真正带走的,不是"File 和 IO 有哪些类",而是这几个核心认识:

1)File 主要负责描述路径和文件信息

它不负责真正的内容读写。

2)IO 的本质是数据在程序和外部世界之间流动

输入是读进来,输出是写出去。

3)字节流和字符流必须分清

  • 文本:优先字符流
  • 二进制:优先字节流

4)文件操作天然带着不确定性

所以异常处理和资源关闭非常重要。

5)try-with-resources 是现代 Java 基础习惯

能自动关流,就别手动把代码写复杂。


如果前面的面向对象、集合、泛型是在帮你把"程序内部的数据结构"搭起来,那么 File 与 IO 做的事情,就是把程序真正接到外部世界上。

从这一篇开始,你写的代码就不再只是"在控制台跑一下",而是会开始:

  • 读配置
  • 写日志
  • 保存数据
  • 处理文件

这一步很关键。

因为很多真正像"软件"的程序,都是从这里开始有了现实感。

相关推荐
汽车搬砖家2 小时前
vSOMEIP系列 -6: vsomeip python版部署,双机跨域通信(vsomeip - davinci AP someip)
python·汽车
小陈工2 小时前
Python Web开发入门(十六):前后端分离架构设计——从“各自为政”到“高效协同”
开发语言·前端·数据库·人工智能·python
gogogo出发喽3 小时前
使用Pear Admin Flask
后端·python·flask
橘子编程3 小时前
操作系统原理:从入门到精通全解析
java·linux·开发语言·windows·计算机网络·面试
与虾牵手3 小时前
Python asyncio 踩了一周坑,我把能犯的错全犯了一遍
python
飞Link3 小时前
LangGraph 核心架构解析:节点 (Nodes) 与边 (Edges) 的工作机制及实战指南
java·开发语言·python·算法·架构
xuhaoyu_cpp_java3 小时前
Boyer-Moore 投票算法
java·经验分享·笔记·学习·算法
资深设备全生命周期管理3 小时前
EXE Ver 适用于 未安装Python 以及包的Windows OS
python
JavaEdge.3 小时前
Chrome加载已解压的扩展程序-清单文件缺失或不可读取 无法加载清单
java