十:异常处理与文件读写——让程序更健壮、数据能持久化

一、回顾与本篇目标

前面九篇,你从零开始学会了 Java 的语法基础、面向对象编程、字符串处理和 ArrayList。你现在已经能写出结构清晰、功能完整的程序了。

但到目前为止,我们写的程序都是“理想环境”下的——假设用户总是输入正确的数据,文件总是存在,数组索引永远不会越界。真实世界不是这样的:用户可能输入字母而不是数字,文件可能被误删,网络可能断开。如果程序遇到这些问题就直接崩溃,那用户体验会非常糟糕。

另外,到目前为止我们程序处理的数据都只在内存中——程序一关,数据全丢。一个真正的程序需要把数据存到硬盘上,下次运行时还能读出来。

这两个问题——处理意外情况持久化数据——就是本篇要解决的核心内容。

本篇的目标:

  1. 理解什么是异常——程序运行时的意外情况
  2. 学会用 try/catch/finally 捕获和处理异常
  3. 了解 Java 中常见的异常类型
  4. 学会用 FileWriterBufferedWriter 写文件
  5. 学会用 FileReaderBufferedReader 读文件

二、异常是什么——程序运行时的意外

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-resourcestry (资源声明) { ... } 这种语法中,圆括号里声明的资源会在 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 中的代码无论如何都会执行,适合做清理工作。
  • 常见异常NullPointerExceptionArrayIndexOutOfBoundsExceptionArithmeticExceptionNumberFormatExceptionIOException
  • 文件写入FileWriter + BufferedWriter。用 write() 写内容,newLine() 换行。第二个参数 true 表示追加模式。用 try-with-resources 自动关闭文件。
  • 文件读取FileReader 逐字符读(read() 返回 -1 表示结束),BufferedReader 逐行读(readLine() 返回 null 表示结束)。

有了异常处理,你的程序可以应对各种意外情况,不会轻易崩溃。有了文件读写,你的程序可以把数据持久化到硬盘上,下次运行时还能读出来。这两项能力让程序从“玩具”变成了“工具”。

下一篇预告

下一篇是《Java 零基础入门》的终篇——《综合实战——学生管理系统》。我们将综合运用本系列学过的所有知识:类与对象、封装、继承、多态、ArrayList、文件读写、异常处理——从零开始搭建一个完整的学生管理系统。这是对你学习成果的一次全面检验。

Java 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容