一、回顾与本篇目标
前面九篇,你从零开始学会了 Java 的语法基础、面向对象编程、字符串处理和 ArrayList。你现在已经能写出结构清晰、功能完整的程序了。
但到目前为止,我们写的程序都是“理想环境”下的——假设用户总是输入正确的数据,文件总是存在,数组索引永远不会越界。真实世界不是这样的:用户可能输入字母而不是数字,文件可能被误删,网络可能断开。如果程序遇到这些问题就直接崩溃,那用户体验会非常糟糕。
另外,到目前为止我们程序处理的数据都只在内存中——程序一关,数据全丢。一个真正的程序需要把数据存到硬盘上,下次运行时还能读出来。
这两个问题——处理意外情况和持久化数据——就是本篇要解决的核心内容。
本篇的目标:
- 理解什么是异常——程序运行时的意外情况
- 学会用
try/catch/finally捕获和处理异常 - 了解 Java 中常见的异常类型
- 学会用
FileWriter和BufferedWriter写文件 - 学会用
FileReader和BufferedReader读文件
二、异常是什么——程序运行时的意外
2.1 生活中的类比
假设你早上出门上班。正常情况下,你按计划走——走到地铁站,坐地铁,到公司。但可能出现意外:
- 走到一半发现下雨了,你没带伞——你需要应对方案(比如买把伞、叫个车)。
- 地铁突然停运了——你需要备用路线(比如换乘公交)。
- 路上出了点小意外,但不管怎样你还是要到公司(比如先不管迟到,确保人到)。
在程序中,异常就是这些“意外情况”——程序运行时发生了意料之外的事情,如果不处理,程序就会崩溃退出。Java 提供了异常处理机制,让你能够优雅地应对这些意外,给用户友好的提示,而不是直接闪退。
2.2 你其实已经见过异常了
在第六篇数组那一章,我提到过 ArrayIndexOutOfBoundsException——数组下标越界异常。如果你访问了不存在的索引,Java 就会抛出这个异常:
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // 抛出 ArrayIndexOutOfBoundsException
程序会报错并停止。这就是一个没有被处理的异常。下面我们来学习如何主动捕获和处理异常。
三、try/catch——捕获和处理异常
3.1 基本语法
把可能出错的代码放在 try 块中,把出错后的处理代码放在 catch 块中:
try {
// 可能出错的代码
} catch (异常类型 变量名) {
// 出错后的处理代码
}
3.2 最简单的例子
public class ExceptionDemo1 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
// 尝试访问一个可能越界的索引
System.out.println(arr[5]);
} catch (ArrayIndexOutOfBoundsException e) {
// 如果越界了,执行这里的代码
System.out.println("出错了:数组下标越界!");
System.out.println("错误详情:" + e.getMessage());
}
System.out.println("程序继续执行……");
}
}
输出:
出错了:数组下标越界!
错误详情:Index 5 out of bounds for length 3
程序继续执行……
关键理解:
try块中的代码出错了,Java 跳过了try块中剩余的代码,直接跳到catch块。catch块执行完毕后,程序继续往后执行,不会崩溃退出。e是一个变量,代表被捕获到的异常对象。你可以通过它获取错误信息。
3.3 捕获多种不同类型的异常
可以在一个 try 后面跟多个 catch,分别处理不同类型的异常:
public class ExceptionDemo2 {
public static void main(String[] args) {
try {
String str = null;
// 这行会抛出 NullPointerException(空指针异常)
System.out.println(str.length());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组下标越界了!");
} catch (NullPointerException e) {
System.out.println("空指针异常:对一个 null 对象调用了方法!");
} catch (Exception e) {
// Exception 是所有异常的"父类型",能捕获任何异常
System.out.println("发生了其他异常:" + e.getMessage());
}
System.out.println("程序继续执行……");
}
}
catch 的顺序很重要:子类异常必须写在父类异常前面。如果把 catch (Exception e) 写在最前面,后面的 catch 永远不会被执行。
3.4 finally——无论如何都会执行的代码
finally 块中的代码无论是否发生异常,都会被执行。它通常用来做清理工作——比如关闭文件、释放资源。
public class FinallyDemo {
public static void main(String[] args) {
try {
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // 会越界
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("捕获到异常:" + e.getMessage());
} finally {
System.out.println("这行无论如何都会打印——即使没有异常也会执行");
}
}
}
输出:
捕获到异常:Index 5 out of bounds for length 3
这行无论如何都会打印——即使没有异常也会执行
如果 try 块中没有异常,catch 会被跳过,但 finally 仍然会执行。
3.5 常见异常类型
| 异常类型 | 什么时候发生 | 示例 |
|---|---|---|
NullPointerException |
对一个 null 对象调用方法或访问属性 | String s = null; s.length(); |
ArrayIndexOutOfBoundsException |
数组下标越界 | arr[10](数组长度只有 5) |
ArithmeticException |
算术运算错误 | int x = 10 / 0;(除数为 0) |
NumberFormatException |
字符串转数字时格式不对 | Integer.parseInt("abc"); |
FileNotFoundException |
文件不存在 | 试图读取一个不存在的文件 |
四、文件写入——把数据存到硬盘
到目前为止,程序处理的数据都在内存中——变量、数组、ArrayList——程序退出后就全没了。文件写入让数据能持久保存到硬盘上。
4.1 最简单的文件写入——FileWriter
import java.io.FileWriter;
import java.io.IOException;
public class FileWriteDemo {
public static void main(String[] args) {
// 用 try-with-resources 语法(自动关闭文件)
try (FileWriter writer = new FileWriter("output.txt")) {
writer.write("你好,这是写入文件的第一行!\n");
writer.write("这是第二行。\n");
writer.write("这是第三行。\n");
System.out.println("文件写入成功!");
} catch (IOException e) {
System.out.println("文件写入失败:" + e.getMessage());
}
}
}
逐行解释:
FileWriter:Java 中用来写文件的类。创建一个FileWriter对象并指定文件名,如果文件不存在会自动创建。writer.write("内容"):把字符串写入文件。不会自动换行——需要换行的地方要手动加\n。try-with-resources:try (资源声明) { ... }这种语法中,圆括号里声明的资源会在 try 代码块结束后自动关闭。不需要手动写writer.close()。这是 Java 7 引入的特性,强烈推荐使用。IOException:输入输出异常的父类。文件操作可能出错(磁盘满了、没有权限等),必须捕获或声明这个异常。
运行后,项目文件夹下会出现一个 output.txt 文件,内容就是你写入的三行文字。
4.2 更高效的文件写入——BufferedWriter
如果写的内容比较多,FileWriter 每次调用 write() 都会直接操作硬盘,效率较低。BufferedWriter 在内存中设了一个缓冲区,攒够一批数据再一次性写入硬盘,效率更高。
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriteDemo {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output2.txt"))) {
writer.write("第一行内容");
writer.newLine(); // 写入一个换行符(跨平台,比手动写 \n 更好)
writer.write("第二行内容");
writer.newLine();
writer.write("第三行内容");
System.out.println("文件写入成功!");
} catch (IOException e) {
System.out.println("文件写入失败:" + e.getMessage());
}
}
}
BufferedWriter 的优势:
newLine():写入一个跨平台的换行符。在 Windows 上会写入\r\n,在 Mac/Linux 上会写入\n。这比手动写\n更好,因为不同操作系统的换行符不同。- 缓冲区让频繁写入操作更高效。
4.3 追加模式——在文件末尾添加内容
默认情况下,FileWriter 会覆盖原有文件的内容。如果你想在文件末尾追加内容而不覆盖,用追加模式:
// 第二个参数 true 表示追加模式
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output2.txt", true))) {
writer.write("这是追加的一行");
writer.newLine();
System.out.println("追加成功!");
} catch (IOException e) {
System.out.println("追加失败:" + e.getMessage());
}
五、文件读取——把硬盘的数据读出来
5.1 用 FileReader 读取文件
import java.io.FileReader;
import java.io.IOException;
public class FileReadDemo {
public static void main(String[] args) {
try (FileReader reader = new FileReader("output.txt")) {
int character;
System.out.println("===== 文件内容 =====");
// read() 返回一个字符的 ASCII 码,读到末尾返回 -1
while ((character = reader.read()) != -1) {
System.out.print((char) character); // 把 ASCII 码转成字符
}
System.out.println("\n===== 读取完毕 =====");
} catch (IOException e) {
System.out.println("文件读取失败:" + e.getMessage());
}
}
}
逐行解释:
reader.read():每次读取一个字符,返回它的 ASCII 码值(整数)。如果读到了文件末尾,返回 -1。(char) character:把整数的 ASCII 码转回字符再打印。while ((character = reader.read()) != -1):这是一个经典的读文件模式——先读一个字符赋给character,然后判断是否等于 -1。只要没到文件末尾,就继续循环。
5.2 更高效的逐行读取——BufferedReader
和写文件类似,BufferedReader 提供了缓冲区,并且提供了非常方便的 readLine() 方法——一次读取一整行:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReadDemo {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("output.txt"))) {
String line;
int lineNumber = 1;
System.out.println("===== 逐行读取 =====");
// readLine() 读取一行,读到末尾返回 null
while ((line = reader.readLine()) != null) {
System.out.println("第 " + lineNumber + " 行:" + line);
lineNumber++;
}
System.out.println("===== 读取完毕 =====");
} catch (IOException e) {
System.out.println("文件读取失败:" + e.getMessage());
}
}
}
readLine() 的特点:
- 一次读取一整行(不包括换行符)。
- 读到文件末尾时返回
null(注意:不是 -1,和read()不同)。 - 循环条件是
(line = reader.readLine()) != null——和刚才的!= -1不同,这里比较的是null。
六、综合演示——学生信息管理(带文件存储)
下面这个示例把之前学的面向对象、ArrayList、文件读写全部整合起来,做一个能保存到文件的学生信息管理程序:
import java.io.*;
import java.util.ArrayList;
// Student 类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() { return this.name; }
public int getScore() { return this.score; }
// 把学生信息转成一行字符串(用于写入文件)
public String toFileString() {
return this.name + "," + this.score;
}
// 从一行字符串创建学生对象(用于从文件读取)
public static Student fromFileString(String line) {
String[] parts = line.split(","); // 按逗号分割
String name = parts[0];
int score = Integer.parseInt(parts[1]); // 字符串转整数
return new Student(name, score);
}
@Override
public String toString() {
return "姓名:" + this.name + ",成绩:" + this.score + " 分";
}
}
// 主程序
public class StudentFileManager {
public static void main(String[] args) {
String filename = "students.txt";
ArrayList students = new ArrayList<>();
// 1. 先尝试从文件加载已有数据
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) {
students.add(Student.fromFileString(line));
}
System.out.println("从文件加载了 " + students.size() + " 条学生记录");
} catch (FileNotFoundException e) {
System.out.println("未找到已有数据文件,开始录入新数据");
} catch (IOException e) {
System.out.println("读取文件出错:" + e.getMessage());
} catch (NumberFormatException e) {
System.out.println("文件数据格式错误:" + e.getMessage());
}
// 2. 添加新学生(模拟——实际项目中可能是用户输入)
students.add(new Student("张三", 85));
students.add(new Student("李四", 92));
students.add(new Student("王五", 78));
// 3. 显示所有学生
System.out.println("\n===== 学生名单 =====");
for (Student s : students) {
System.out.println(s);
}
// 4. 统计信息
int total = 0;
int maxScore = students.get(0).getScore();
String topStudent = students.get(0).getName();
for (Student s : students) {
total += s.getScore();
if (s.getScore() > maxScore) {
maxScore = s.getScore();
topStudent = s.getName();
}
}
double average = (double) total / students.size();
System.out.println("\n===== 统计信息 =====");
System.out.println("总人数:" + students.size());
System.out.println("平均分:" + average);
System.out.println("最高分:" + maxScore + "(" + topStudent + ")");
// 5. 保存到文件
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
for (Student s : students) {
writer.write(s.toFileString());
writer.newLine();
}
System.out.println("\n数据已保存到 " + filename);
} catch (IOException e) {
System.out.println("保存文件失败:" + e.getMessage());
}
}
}
第一次运行(文件不存在):
未找到已有数据文件,开始录入新数据
===== 学生名单 =====
姓名:张三,成绩:85 分
姓名:李四,成绩:92 分
姓名:王五,成绩:78 分
===== 统计信息 =====
总人数:3
平均分:85.0
最高分:92(李四)
数据已保存到 students.txt
第二次运行(文件已存在,会先加载):
从文件加载了 3 条学生记录
===== 学生名单 =====
姓名:张三,成绩:85 分
姓名:李四,成绩:92 分
姓名:王五,成绩:78 分
姓名:张三,成绩:85 分
姓名:李四,成绩:92 分
姓名:王五,成绩:78 分
...
第二次运行时会发现学生翻倍了——因为先加载了文件中的 3 个学生,又添加了 3 个。这暴露了一个逻辑问题(应该先去重或让用户选择是否添加),但作为演示已经完整展示了文件读写的全部流程。
代码设计要点:
toFileString():把学生对象转成"张三,85"这样的字符串,方便写入文件。fromFileString():从"张三,85"这样的字符串解析出姓名和成绩,创建学生对象。用split(",")按逗号分割字符串。- 异常处理分层:分别捕获
FileNotFoundException(文件不存在)、IOException(读写错误)、NumberFormatException(文件内容格式不对)。 - try-with-resources:读写文件都用这个语法,自动关闭文件,避免资源泄漏。
七、本篇动手练习
练习 1:异常处理练习
新建 Practice10_1.java。让用户输入两个整数(写在代码里模拟即可),计算它们的商。用 try/catch 捕获 ArithmeticException(除数为 0)和 NumberFormatException(输入不是数字),分别给出友好的错误提示。
练习 2:日记本
新建 Practice10_2.java。写一个程序,往 diary.txt 文件中追加一行文字(模拟写日记)。每次运行程序,往文件末尾加一行当前日期和一段文字。提示:获取当前日期可以用 java.time.LocalDate.now().toString()。
练习 3:读取并统计文件
新建 Practice10_3.java。读取 students.txt 文件(使用综合演示中生成的文件),统计:文件中有多少行、所有成绩的总和、平均分、最高分的学生姓名。注意处理文件可能不存在的情况。
练习 4:简单日志系统
新建 Practice10_4.java。写一个 Logger 类,有一个 log(String message) 方法。每次调用这个方法,把当前时间戳和消息追加写入 app.log 文件。在主程序中模拟记录几条日志,然后读取整个日志文件并打印出来。
八、本篇小结
这一篇你学会了两项重要的能力——处理异常和读写文件:
- 异常:程序运行时的意外情况。用
try/catch捕获异常,防止程序崩溃。finally中的代码无论如何都会执行,适合做清理工作。 - 常见异常:
NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException、NumberFormatException、IOException。 - 文件写入:
FileWriter+BufferedWriter。用write()写内容,newLine()换行。第二个参数true表示追加模式。用try-with-resources自动关闭文件。 - 文件读取:
FileReader逐字符读(read()返回 -1 表示结束),BufferedReader逐行读(readLine()返回null表示结束)。
有了异常处理,你的程序可以应对各种意外情况,不会轻易崩溃。有了文件读写,你的程序可以把数据持久化到硬盘上,下次运行时还能读出来。这两项能力让程序从“玩具”变成了“工具”。
下一篇预告
下一篇是《Java 零基础入门》的终篇——《综合实战——学生管理系统》。我们将综合运用本系列学过的所有知识:类与对象、封装、继承、多态、ArrayList、文件读写、异常处理——从零开始搭建一个完整的学生管理系统。这是对你学习成果的一次全面检验。
Java 零基础入门,每周更新。












暂无评论内容