Java学习笔记-文件IO
八、文件 IO
基础介绍
程序运行时的数据都在内存里,程序一关数据就消失。文件 IO(Input/Output)就是把数据写到硬盘文件里持久保存,或者从硬盘文件读回内存。
两大流体系:
| 体系 | 处理单位 | 适合场景 | 类名 |
|---|---|---|---|
| 字节流 | byte(字节) | 二进制数据(图片、视频、对象序列化) | InputStream / OutputStream |
| 字符流 | char(字符) | 文本数据(txt、csv、json) | Reader / Writer |
报价系统用字符流:报价单数据是文本,用
FileReader/FileWriter或BufferedReader/BufferedWriter。
带缓冲 vs 不带缓冲:
| 类型 | 特点 | 比喻 |
|---|---|---|
| 不带缓冲(FileReader/FileWriter) | 读一个字节写一个字节,频繁访问硬盘 | 用勺子一勺勺舀水 |
| 带缓冲(BufferedReader/BufferedWriter) | 先装满缓冲区,再一次性读写 | 用桶打一桶水再倒 |
always 用缓冲流,效率差几十倍。
为什么构造器一层包一层?(装饰器模式)
▼java复制代码new BufferedWriter( // 第3层:加缓冲(效率) new OutputStreamWriter( // 第2层:指定编码(字符 → 字节) new FileOutputStream(file), // 第1层:连接文件(字节 → 硬盘) "UTF-8" ) )
| 层级 | 类 | 作用 |
|---|---|---|
| 最底层 | FileOutputStream | 直接连硬盘,处理原始字节 |
| 中间层 | OutputStreamWriter | 把字符按指定编码转成字节 |
| 最外层 | BufferedWriter | 加缓冲区,减少硬盘访问次数 |
每一层都是给上一层"加功能",像俄罗斯套娃。可以只包一层,也可以包多层。
核心方法表
File 类(文件/目录操作)
| 方法 | 作用 |
|---|---|
File(String path) | 创建文件对象(不创建真实文件) |
exists() | 判断文件/目录是否存在 |
isFile() | 判断是否是文件 |
isDirectory() | 判断是否是目录 |
createNewFile() | 创建新文件 |
mkdir() | 创建目录 |
mkdirs() | 创建多级目录 |
delete() | 删除文件/空目录 |
getAbsolutePath() | 获取绝对路径 |
listFiles() | 列出目录下所有子文件 |
BufferedWriter(字符输出流,写文件)
| 方法 | 作用 |
|---|---|
BufferedWriter(Writer out) | 构造器,包装一个 Writer |
write(String str) | 写入字符串 |
newLine() | 写入换行符(跨平台) |
flush() | 刷新缓冲区,把数据刷到硬盘 |
close() | 关闭流 |
BufferedReader(字符输入流,读文件)
| 方法 | 作用 |
|---|---|
BufferedReader(Reader in) | 构造器,包装一个 Reader |
readLine() | 读取一行,返回 String(读到末尾返回 null) |
close() | 关闭流 |
ObjectOutputStream(对象序列化输出)
| 方法 | 作用 |
|---|---|
ObjectOutputStream(OutputStream out) | 构造器 |
writeObject(Object obj) | 写入对象 |
close() | 关闭流 |
ObjectInputStream(对象序列化输入)
| 方法 | 作用 |
|---|---|
ObjectInputStream(InputStream in) | 构造器 |
readObject() | 读取对象(返回 Object,需强转) |
close() | 关闭流 |
经典代码
1. 写文本文件(BufferedWriter)
▼java复制代码// 目标:把报价单列表写入 data/quotations.txt File file = new File("data/quotations.txt"); file.getParentFile().mkdirs(); // 如果 data 目录不存在,自动创建 // try-with-resources:自动关闭流,不用手动 close() try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { for (SmartHomeQuotation q : quotations) { // 把一条报价单写成一行:编号,项目名称,金额 String line = q.getQuotationNo() + "," + q.getProjectName() + "," + q.calcOriginalTotalAmount(); writer.write(line); writer.newLine(); // 换行 } writer.flush(); // 刷新缓冲区 System.out.println("写入完成,共 " + quotations.size() + " 条"); } catch (IOException e) { e.printStackTrace(); }
double 类型写入 CSV 的陷阱:
calcOriginalTotalAmount()返回1739723.0,但 Excel 打开时可能隐藏.0(Excel 把纯数字识别为数值型,自动省略末尾的 .0)。解决方法:用
String.format强制保留两位小数(财务规范)▼java复制代码// 保留两位小数:1739723.00 String amountStr = String.format("%.2f", q.calcOriginalTotalAmount()); // 保留两位小数 + 千分位:1,739,723.00 String amountStr = String.format("%,.2f", q.calcOriginalTotalAmount()); // 写入 writer.write(q.getQuotationNo() + "," + q.getProjectName() + "," + amountStr);
2. 读文本文件(BufferedReader)
▼java复制代码File file = new File("data/quotations.txt"); if (!file.exists()) { System.out.println("文件不存在"); return; } try (BufferedReader reader = new BufferedReader(new FileReader(file))) { String line; while ((line = reader.readLine()) != null) { // 逐行读取,null 表示文件末尾 // 解析一行:Q-SM-001,赵女士全屋智能,54300.0 String[] parts = line.split(","); String quotationNo = parts[0]; String projectName = parts[1]; double amount = Double.parseDouble(parts[2]); System.out.println("编号:" + quotationNo + ",项目:" + projectName + ",金额:" + amount); } } catch (IOException e) { e.printStackTrace(); }
3. 对象序列化(ObjectOutputStream)
▼java复制代码// 把报价单对象完整保存到文件(包括对象里的所有字段) try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("data/quotation.obj"))) { oos.writeObject(quotation); // 写入对象 System.out.println("对象已序列化保存"); } catch (IOException e) { e.printStackTrace(); }
4. 对象反序列化(ObjectInputStream)
▼java复制代码try (ObjectInputStream ois = new ObjectInputStream( new FileInputStream("data/quotation.obj"))) { SmartHomeQuotation q = (SmartHomeQuotation) ois.readObject(); // 强转 System.out.println(q.getSummaryLine()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }
5. 追加模式写文件(不覆盖原有内容)
▼java复制代码// FileWriter 第二个参数 true = 追加模式 try (BufferedWriter writer = new BufferedWriter( new FileWriter("data/log.txt", true))) { writer.write("新的日志记录"); writer.newLine(); } catch (IOException e) { e.printStackTrace(); }
对象序列化详解
什么是序列化?
程序运行时,对象在内存里。程序一关,内存清空,对象就消失了。序列化就是把内存中的对象变成一串字节,可以存到硬盘文件,也可以通过网络发送给别人。
▼java复制代码// 序列化:对象 → 文件(冻住) ObjectOutputStream.writeObject(q); // 反序列化:文件 → 对象(解冻) SmartHomeQuotation q = (SmartHomeQuotation) ObjectInputStream.readObject();
为什么要实现 Serializable 接口?
Serializable 是一个标记接口(里面没有任何方法)。它的作用就是告诉 JVM:
"这个类里的数据可以安全地转成字节流,你放心序列化。"
如果没有实现:
▼java复制代码public class SmartHomeQuotation { } // ❌ 没实现 Serializable oos.writeObject(q); // ❌ 抛异常:NotSerializableException
实现后:
▼java复制代码public class SmartHomeQuotation implements Serializable { // ✅ private static final long serialVersionUID = 1L; }
哪些类已经实现了 Serializable?
JDK 自带的常用类基本都实现了:
| 类 | 是否已实现 |
|---|---|
String | ✅ |
Integer、Double、Long 等包装类 | ✅ |
ArrayList | ✅ |
HashMap | ✅ |
Date | ✅ |
你的自定义类必须自己加上:
▼java复制代码public class SmartHomeQuotation implements Serializable { private static final long serialVersionUID = 1L; } public class Worker implements Serializable { private static final long serialVersionUID = 1L; }
重要:如果
SmartHomeQuotation里引用了Person(客户),那么Person及其所有子类(Customer、Company)也必须实现Serializable,否则序列化时会报错。
serialVersionUID 的作用:
想象这是对象的"版本号":
- 你今天存了一个
SmartHomeQuotation(版本1) - 明天你给类加了
remark字段(变成版本2) - 再用版本2去读版本1的文件 → 类结构变了,JVM 不知道怎么办,抛异常
加了 serialVersionUID = 1L,告诉 JVM:"版本兼容,能读就读,不能读的字段忽略。"
完整示例:序列化 + 反序列化
▼java复制代码// 第1步:所有相关实体类必须实现 Serializable public class SmartHomeQuotation implements Serializable { private static final long serialVersionUID = 1L; // ... 字段 ... } public abstract class Person implements Serializable { private static final long serialVersionUID = 1L; } public class Customer extends Person implements Serializable { private static final long serialVersionUID = 1L; } public class Company extends Person implements Serializable { private static final long serialVersionUID = 1L; } // 第2步:序列化写入 public static void serialize(SmartHomeQuotation q, File file) { try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream(file))) { oos.writeObject(q); System.out.println("序列化完成:" + file.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); } } // 第3步:反序列化读取 public static SmartHomeQuotation deserialize(File file) { try (ObjectInputStream ois = new ObjectInputStream( new FileInputStream(file))) { SmartHomeQuotation q = (SmartHomeQuotation) ois.readObject(); System.out.println("反序列化成功:" + q.getQuotationNo()); return q; } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); return null; } }
关键:
SmartHomeQuotation引用了Person(客户/商家),Person及其子类(Customer、Company)都必须实现Serializable,否则writeObject()抛NotSerializableException。
文本文件 vs 对象序列化:怎么选?
两种都能存数据,但适用场景完全不同:
| 对比 | 文本文件(CSV/txt) | 对象序列化(.obj) |
|---|---|---|
| 存什么 | 纯文本字符串 | 完整的对象(字段+类型+对象关系) |
| 人能看懂吗 | ✅ 记事本/Excel 能打开 | ❌ 二进制乱码 |
| 别的程序能读吗 | ✅ Excel、Python 都能读 | ❌ 只有 Java 能读 |
| 存对象方不方便 | ❌ 手动拼字符串,字段多了很麻烦 | ✅ 一行 writeObject(q) 搞定 |
| 嵌套对象能存吗 | ❌ 自己处理引用关系 | ✅ 自动处理对象树 |
| 版本兼容性 | ✅ 加字段不影响旧文件 | ❌ 类结构变了可能读不了 |
| 文件大小 | 小(纯文本) | 大(包含类信息) |
什么时候用文本文件?
▼java复制代码// 1. 数据要给别的程序用(Excel、Python、别的系统) // 2. 人需要直接看内容(日志、配置) // 3. 数据格式简单,就几个字段 BufferedWriter.write("Q-SM-001,赵女士,50000"); // Excel 能打开
什么时候用序列化?
▼java复制代码// 1. 临时缓存对象,程序重启后还原 // 2. 对象关系复杂(报价单→客户→地址,嵌套多层) // 3. 网络传输对象(A电脑的对象传给B电脑) oos.writeObject(quotation); // 整个对象树一键保存
CSV 的陷阱:字段中包含逗号怎么办?
▼java复制代码// 如果 projectName = "赵女士,全屋智能" // 直接拼接:Q-SM-001,赵女士,全屋智能,50000 // split(",") 后变成 4 段,解析错乱! // 正确做法:字段含逗号时用双引号包裹 // "Q-SM-001","赵女士,全屋智能","50000" // 实际项目不要自己拼 CSV,用第三方库: // Apache Commons CSV、OpenCSV、FastCSV
项目实际选择:
| 需求 | 推荐方式 | 理由 |
|---|---|---|
| 报价单导出给财务 | 文本文件 CSV | 财务用 Excel 打开 |
| 程序缓存,重启还原 | 序列化 | 一键存取,不用拼字符串 |
| 配置文件 | 文本文件 properties | 人能改 |
| 日志记录 | 文本文件 txt | 人能看 |
一句话: 要给人看或给别的程序用 → 文本文件;要快速存取 Java 对象 → 序列化。
注意点
- try-with-resources 自动关闭流
▼java复制代码try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { // 写操作... } // ← 大括号结束自动调用 writer.close()
实现了
AutoCloseable接口的类都可以这样用。
多个资源时的关闭顺序:
▼java复制代码// ✅ fos 和 writer 都必须放进 try(),否则 fos 没人关 // 关闭顺序:先开后关(writer 先关,再关 fos) try (FileOutputStream fos = new FileOutputStream(file); // 第1个打开 BufferedWriter writer = new BufferedWriter( // 第2个打开 new OutputStreamWriter(fos, "UTF-8"))) { fos.write(bom); // 写 BOM writer.write("内容"); // 写内容 } // ← 先关 writer,再关 fos(栈顺序:先开后关)
fos 不放 try() 的后果:
▼java复制代码// ❌ 错误:fos 在 try 外面 FileOutputStream fos = new FileOutputStream(file); // 打开文件 fos.write(bom); // 写 BOM try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(fos, "UTF-8"))) { writer.write("内容"); } // ← writer 关了,但 fos 还开着! // ❌ fos 没人关 → 内存泄漏、文件被锁、数据丢失
- flush 与 close 的关系
| 操作 | 作用 | 什么时候需要 |
|---|---|---|
flush() | 把缓冲区数据刷到硬盘 | 需要立即看到文件内容时(如日志实时写入) |
close() | 先 flush,再关闭流 | 用完流必须调,try-with-resources 自动处理 |
▼java复制代码// try-with-resources 的 close() 会自动 flush // 所以大部分情况下不需要手动 flush try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { writer.write("内容"); // 不需要手动 flush,close 时会自动刷 } // 只有需要立即落盘时才手动 flush(如实时日志) writer.write("异常日志"); writer.flush(); // 立即写到硬盘,防止崩溃丢失
- 流是单向的:要么读,要么写,不能同时读写。
文件编码三种写法对比
核心问题:Java 写的文本文件,Excel 打开可能乱码。
原因:Java 默认 UTF-8,Excel 默认 GBK,两边编码不一致。
三种解决方案对比:
| 方案 | 代码 | Excel 双击打开 | 跨平台 | 实际场景 |
|---|---|---|---|---|
| UTF-8(不加 BOM) | new OutputStreamWriter(new FileOutputStream(file), "UTF-8") | ❌ 中文乱码 | ✅ Linux/Mac/Windows 都正常 | 纯后端系统,人不看文件 |
| GBK | new OutputStreamWriter(new FileOutputStream(file), "GBK") 或 new FileWriter(file) | ✅ 正常显示 | ❌ Linux/Mac 打开乱码 | 国内内部系统,只给 Windows Excel 用 |
| UTF-8 with BOM ✅ | 先写 BOM 头,再用 UTF-8 写内容 | ✅ 正常显示 | ✅ 全平台正常 | 生产环境标准做法 |
写法1:UTF-8(不加 BOM)
▼java复制代码try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) { writer.write("编号,项目名称,原始金额,客户姓名"); writer.newLine(); }
- Excel 双击打开:❌ 乱码(Excel 不知道这是 UTF-8,用 GBK 解码)
- 需要手动操作:Excel → 数据 → 从文本/CSV → 选择文件 → 编码选 UTF-8
写法2:GBK
▼java复制代码try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), "GBK"))) { // 或直接用 new FileWriter(file),Windows 默认 GBK writer.write("编号,项目名称,原始金额,客户姓名"); writer.newLine(); }
- Excel 双击打开:✅ 正常
- 缺点:Linux/Mac 打开乱码;不支持多语言混合(中英文+日文+韩文)
写法3:UTF-8 with BOM(生产标准)
▼java复制代码FileOutputStream fos = new FileOutputStream(file); // 先写 UTF-8 BOM 头(3个字节),告诉 Excel "这是 UTF-8" fos.write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF}); try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(fos, "UTF-8"))) { writer.write("编号,项目名称,原始金额,客户姓名"); writer.newLine(); // ... 遍历写数据 }
- Excel 双击打开:✅ 正常(看到 BOM 自动用 UTF-8 解码)
- Linux/Mac/Windows:✅ 全平台正常
- 缺点:文件开头多 3 个字节(BOM),对程序读取无影响
BOM 是什么?
BOM(Byte Order Mark)是文件开头的特殊字节序列,像"编码身份证":
- UTF-8 BOM:
EF BB BF(3 个字节) - Excel 看到 BOM 就知道"这是 UTF-8 文件,我用 UTF-8 解码"
- 没有 BOM,Excel 默认用系统编码(Windows = GBK),所以乱码
FileWriter/FileReader 的编码真相:
▼java复制代码// FileWriter 内部其实是这样(用系统默认编码) new FileWriter(file) // ≈ new OutputStreamWriter(new FileOutputStream(file), "系统默认编码") // FileReader 内部其实是这样(用系统默认编码) new FileReader(file) // ≈ new InputStreamReader(new FileInputStream(file), "系统默认编码")
所以 FileWriter 和 FileReader 确实是配对的——都用系统默认编码。
但问题是:系统默认编码会变。
| 环境 | 系统默认编码 |
|---|---|
| Windows 中文 | GBK |
| Mac | UTF-8 |
| Linux | UTF-8 |
你今天用 FileWriter(GBK)写的文件,明天在 Mac 上用 FileReader(UTF-8)读 → 乱码。
实际项目中的正确做法:
始终显式指定编码,不用 FileWriter/FileReader。
▼java复制代码// 写入 GBK new OutputStreamWriter(new FileOutputStream(file), "GBK") // 读取 GBK new InputStreamReader(new FileInputStream(file), "GBK") // 写入 UTF-8 new OutputStreamWriter(new FileOutputStream(file), "UTF-8") // 读取 UTF-8 new InputStreamReader(new FileInputStream(file), "UTF-8")
核心原则:写入用什么编码,读取就必须用什么编码。
读取时的编码处理:
▼java复制代码// 读取 GBK 文件 BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(file), "GBK")); // 读取 UTF-8 文件(带不带 BOM 都可以) BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(file), "UTF-8")); // 注意:UTF-8 with BOM 的文件,读到的第一行开头会有 \uFEFF 字符 // 如果用 readLine() 读表头,需要去掉它 String header = reader.readLine(); if (header.startsWith("\uFEFF")) { header = header.substring(1); // 去掉 BOM 字符 }
实验验证结果:
| 写法 | 编码 | Excel 双击打开 | 原因 |
|---|---|---|---|
| UTF-8 无 BOM | UTF-8 | ❌ 需要先新建空白文件再导入 | Excel 不知道编码,默认用 GBK 解码,乱码 |
| GBK | GBK | ✅ 直接打开 | Windows 系统默认编码 |
| UTF-8 with BOM | UTF-8 + BOM 头 | ✅ 直接打开 | Excel 看到 BOM 头,自动用 UTF-8 解码 |
一句话结论:
实际项目一律用 UTF-8 with BOM,兼顾跨平台和 Excel 兼容性。
自动检测文件编码(生产环境做法)
用户不懂编码,好的程序应该自动检测。 不能让用户手动选择编码。
检测流程:
▼text复制代码用户上传文件 ↓ 检测前3个字节(BOM) ↓ 有 BOM? ──→ 直接知道编码(UTF-8 / UTF-16) ↓ 没有 用第三方库分析内容 ↓ 返回猜测的编码(GBK / UTF-8 / Big5 等) ↓ 用检测到的编码读取文件
第一步:检测 BOM(不需要第三方库)
▼java复制代码public static String detectByBOM(File file) throws IOException { try (FileInputStream fis = new FileInputStream(file)) { byte[] bom = new byte[3]; int read = fis.read(bom); // UTF-8 BOM:EF BB BF if (read >= 3 && bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF) { return "UTF-8"; } // UTF-16 BE:FE FF if (read >= 2 && bom[0] == (byte) 0xFE && bom[1] == (byte) 0xFF) { return "UTF-16BE"; } // UTF-16 LE:FF FE if (read >= 2 && bom[0] == (byte) 0xFF && bom[1] == (byte) 0xFE) { return "UTF-16LE"; } } return null; // 没有 BOM }
第二步:没有 BOM 时,用 juniversalchardet 猜测
▼java复制代码// Maven 依赖:com.googlecode.juniversalchardet:juniversalchardet:1.0.3 public static String detectByContent(File file) throws IOException { byte[] bytes = Files.readAllBytes(file.toPath()); UniversalDetector detector = new UniversalDetector(null); detector.handleData(bytes, 0, bytes.length); detector.dataEnd(); String encoding = detector.getDetectedCharset(); // "UTF-8" / "GBK" / "GB2312" detector.reset(); return encoding != null ? encoding : "UTF-8"; // 猜不出默认 UTF-8 }
完整检测 + 读取:
▼java复制代码public static void readWithAutoDetect(File file) throws IOException { // 1. 先检测 BOM String encoding = detectByBOM(file); // 2. 没有 BOM,用内容猜测 if (encoding == null) { encoding = detectByContent(file); } System.out.println("检测到编码:" + encoding); // 3. 用检测到的编码读取 try (BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(file), encoding))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } }
实际项目中的三种做法对比:
| 做法 | 场景 | 是否专业 |
|---|---|---|
| 让用户选择编码 | 简单内部工具 | ❌ 用户不懂编码 |
| 约定固定编码 | 项目内部系统,统一 UTF-8 | ✅ 但无法处理外部文件 |
| 自动检测编码 | 用户上传文件(导入 Excel/CSV) | ✅ 生产标准 |
一句话:好的程序不应该问用户"这是什么编码",而应该自己检测出来。
常见坑
- 忘记 flush/close,数据没写进硬盘
▼java复制代码BufferedWriter writer = new BufferedWriter(new FileWriter(file)); writer.write("内容"); // ❌ 没 flush 也没 close,数据还在缓冲区,文件可能是空的! // ✅ try-with-resources 自动解决 try (BufferedWriter writer = ...) { ... }
- readLine() 返回 null 表示文件结束,不是空字符串
▼java复制代码while ((line = reader.readLine()) != null) { ... } // ✅ while ((line = reader.readLine()).isEmpty()) { ... } // ❌ 末尾抛 NullPointerException
- 相对路径 vs 绝对路径
▼java复制代码// 相对路径:相对于程序运行的"工作目录" File file = new File("data/quotations.txt"); // 工作目录通常是项目根目录,所以文件在 项目根目录/data/quotations.txt // 绝对路径:完整路径 File file = new File("C:/JavaProject/zx-plat-practice/data/quotations.txt");
怎么知道工作目录在哪?
▼java复制代码System.out.println(System.getProperty("user.dir")); // 输出:C:\JavaProject\zx-plat-practice
- 文件路径分隔符:Windows 用
\,Linux/Mac 用/。建议用File.separator或直接用/(Java 会自动转)。
▼java复制代码File file = new File("data/quotations.txt"); // ✅ 推荐,跨平台 File file = new File("data\\quotations.txt"); // ❌ Windows 专用
- 对象序列化必须实现
Serializable接口
▼java复制代码public class SmartHomeQuotation implements Serializable { // ✅ 必须! private static final long serialVersionUID = 1L; // 建议加上 }
-
反序列化时类结构变了会抛异常:给类加了字段,之前序列化的文件就读不了了。
serialVersionUID可以缓解。 -
反序列化时类找不到会抛
ClassNotFoundException:- 序列化文件存的是类的完整包名(
cn.com.d99714.smart.entity.SmartHomeQuotation) - 如果反序列化时类路径变了(比如包名改了、类删了),JVM 找不到类,抛异常
- 或者 jar 包缺失(比如把文件从 A 项目拷到 B 项目,B 项目没有这个类)
- 序列化文件存的是类的完整包名(
▼java复制代码try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { SmartHomeQuotation q = (SmartHomeQuotation) ois.readObject(); } catch (ClassNotFoundException e) { // 类路径变了或类缺失 System.out.println("找不到对应的类:" + e.getMessage()); }
受检异常(Checked Exception)与运行时异常
为什么 write()、readLine() 会强制要求处理异常?
Java 把异常分两类:
| 类型 | 例子 | 编译器要求 | 为什么这样设计 |
|---|---|---|---|
| 受检异常 | IOException、SQLException、FileNotFoundException | 强制处理! 不处理编译报错 | 这些风险可以预见,写代码时就必须想好出错怎么办 |
| 运行时异常 | NullPointerException、IndexOutOfBoundsException、ArithmeticException | 不强制,运行时才可能报错 | 通常是代码逻辑错误,理论上可以避免 |
IOException 为什么是受检异常?
因为读写文件很容易出错:
- 磁盘满了
- 文件被别的程序占用
- 权限不够
- 硬盘坏了
Java 设计者认为:"这些风险可以预见,你必须在写代码时就考虑好出错了怎么办。"
处理受检异常的两种方式:
方式1:try-catch 自己捕获
▼java复制代码static void writeCSV(BufferedWriter writer) { try { writer.write("内容"); writer.newLine(); } catch (IOException e) { e.printStackTrace(); // 自己处理 } }
方式2:throws 抛给调用方处理
▼java复制代码static void writeCSV(BufferedWriter writer) throws IOException { writer.write("内容"); writer.newLine(); // 如果出错,抛给调用我的人处理 } // 调用方统一捕获 public static void main(String[] args) { try { writeCSV(writer); } catch (IOException e) { e.printStackTrace(); } }
什么时候用哪种?
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 工具类方法(被多处调用) | throws | 调用方根据自身场景决定怎么处理 |
| 最上层方法(main、Controller) | try-catch | 最后一道防线,必须处理 |
| 需要回滚资源(数据库事务) | try-catch-finally | 出错也要释放资源 |
一句话:受检异常是 Java 强制你"面对风险"。底层方法用
throws往上抛,顶层方法用try-catch兜底。
IDEA 查 API 文档
常用快捷键:
| 操作 | IDEA 快捷键 | 效果 |
|---|---|---|
| 查看文档 | 鼠标放类/方法上,按 Ctrl + Q | 弹出中英文文档说明 |
| 跳转到源码 | Ctrl + B 或 Ctrl + 点击 | 跳转到类的源码实现 |
| 查看继承结构 | Ctrl + H | 弹出类的继承树 |
| 查看方法列表 | Ctrl + F12 | 弹出当前类的所有方法 |
| 全局搜索类 | 双击 Shift | 搜索任意类名 |
| 查看参数提示 | Ctrl + P | 方法调用时看参数类型 |
查文档的标准流程(以 Serializable 为例):
▼text复制代码第1步:光标放在类名上 public class SmartHomeQuotation implements Serializable { ↑ 光标放这里 第2步:按 Ctrl + B 跳转源码 → 跳转到 java.io.Serializable → 看到内容:public interface Serializable { } → 结论:标记接口,无方法,告诉 JVM "这个类可以序列化" 第3步:按 Ctrl + Q 查看文档 → 弹出英文文档 → 看到核心说明: "Serializability of a class is enabled by the class implementing the java.io.Serializable interface." → 结论:实现接口即可启用序列化能力
查 ObjectOutputStream.writeObject 示例:
▼text复制代码第1步:确定类名 → ObjectOutputStream 第2步:Ctrl + B 跳转到类 → 看类描述: "An ObjectOutputStream writes primitive data types and graphs of Java objects to an OutputStream" 第3步:Ctrl + F12 看方法列表 → 找到 writeObject(Object obj) 第4步:点击方法看详细说明 → 参数、返回值、抛出的异常 第5步:回到自己的代码,知道怎么用了
一句话:IDEA 里
Ctrl + B看源码,Ctrl + Q看文档。源码是最好的文档。
号外
-
NIO(New IO):JDK 1.4 引入的"新 IO",
Path、Files、Channel等,性能更高,但 API 更复杂。初学先掌握传统 IO。 -
Properties 类:专门读写
.properties配置文件(键值对格式)。
▼java复制代码// 读取配置文件 Properties props = new Properties(); try (FileInputStream fis = new FileInputStream("config.properties")) { props.load(fis); } String dbUrl = props.getProperty("database.url"); String dbUser = props.getProperty("database.username"); // config.properties 内容: // database.url=jdbc:mysql://localhost:3306/smarthome // database.username=root
-
CSV 格式:逗号分隔值,Excel 可以直接打开,是数据交换的通用格式。
-
File.separator跨平台写法:
▼java复制代码// 拼接路径时用这个常量,自动适配操作系统 String path = "data" + File.separator + "quotations.txt"; // Windows → "data\\quotations.txt" // Mac/Linux → "data/quotations.txt"
