《Java 程序设计》第 12 章 - 异常处理

发布于:2025-07-31 ⋅ 阅读:(15) ⋅ 点赞:(0)

        大家好!今天我们来学习《Java 程序设计》中的第 12 章 —— 异常处理。在编程过程中,错误和异常是不可避免的。一个健壮的程序必须能够妥善处理各种异常情况。本章将详细介绍 Java 中的异常处理机制,帮助大家编写出更稳定、更可靠的 Java 程序。

思维导图

12.1 异常与异常类

12.1.1 异常的概念

        在 Java 中,异常(Exception) 是指程序在运行过程中发生的非正常事件,它会中断程序的正常执行流程。

        想象一下现实生活中的场景:当你开车去上班时,可能会遇到轮胎漏气、发动机故障等意外情况,这些情况会阻止你按计划到达公司。在程序中也是如此,比如:

  • 试图打开一个不存在的文件
  • 网络连接中断
  • 除以零的运算
  • 数组下标越界

        这些情况都可以称为异常。Java 的异常处理机制提供了一种优雅的方式来处理这些意外情况,使程序能够继续运行或友好地终止。

12.1.2 异常类

        Java 中的所有异常都是通过来表示的,这些类统称为异常类。Java 提供了一个完善的异常类体系,所有异常类都直接或间接继承自Throwable

Throwable类有两个重要的子类:

  • Error:表示严重的错误,通常是虚拟机相关的问题,如内存溢出(OutOfMemoryError),程序一般无法处理这类错误。
  • Exception:表示程序可以处理的异常,是我们在编程中主要关注的类。

Exception类又可以分为:

  • Checked Exception(受检异常):在编译时就需要处理的异常,如果不处理,编译器会报错。如IOExceptionSQLException等。
  • Unchecked Exception(非受检异常):也称为运行时异常(RuntimeException),在编译时不需要强制处理,通常是由程序逻辑错误引起的。如NullPointerExceptionArrayIndexOutOfBoundsException等。

下面是异常类的继承关系图:

@startuml
title Java异常类体系
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- RuntimeException
Exception <|-- IOException
Exception <|-- SQLException
RuntimeException <|-- NullPointerException
RuntimeException <|-- ArrayIndexOutOfBoundsException
RuntimeException <|-- ArithmeticException
@enduml

12.2 异常处理

        Java 提供了一套完整的异常处理机制,主要通过trycatchfinallythrowthrows关键字来实现。

12.2.1 异常的抛出与捕获

异常处理的核心思想是抛出异常捕获异常

  1. 抛出异常:当程序执行过程中遇到异常情况时,会创建一个异常对象并将其抛出。
  2. 捕获异常:异常被抛出后,程序可以捕获这个异常并进行处理,而不是让程序直接崩溃。

        形象地说,这就像生活中 "上报问题" 和 "解决问题" 的过程:员工遇到无法解决的问题(抛出异常),上报给经理(捕获异常),经理来处理这个问题。

12.2.2 try-catch-finally 语句

try-catch-finally是 Java 中处理异常的基本结构,语法如下:

try {
    // 可能会发生异常的代码
} catch (异常类型1 异常对象名) {
    // 处理异常类型1的代码
} catch (异常类型2 异常对象名) {
    // 处理异常类型2的代码
} finally {
    // 无论是否发生异常,都会执行的代码
}

  • try 块:包含可能会抛出异常的代码。
  • catch 块:用于捕获并处理 try 块中抛出的异常。可以有多个 catch 块,分别处理不同类型的异常。
  • finally 块:无论是否发生异常,都会执行的代码,通常用于释放资源

执行流程示意图

示例代码

public class TryCatchFinallyDemo {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        int[] arr = {1, 2, 3};
        
        try {
            // 可能发生异常的代码
            int result = a / b;  // 会抛出ArithmeticException
            System.out.println("数组的第4个元素是:" + arr[3]);  // 会抛出ArrayIndexOutOfBoundsException
            System.out.println("计算结果:" + result);
        } catch (ArithmeticException e) {
            // 处理算术异常
            System.out.println("发生算术异常:" + e.getMessage());
            e.printStackTrace();  // 打印异常堆栈信息
        } catch (ArrayIndexOutOfBoundsException e) {
            // 处理数组下标越界异常
            System.out.println("发生数组下标越界异常:" + e.getMessage());
        } finally {
            // 无论是否发生异常,都会执行
            System.out.println("finally块执行了,通常用于释放资源");
        }
        
        System.out.println("程序继续执行...");
    }
}

运行上述代码,输出结果:

12.2.3 用 catch 捕获多个异常

Java 7 及以上版本允许在一个catch块中捕获多种类型的异常,使用|分隔不同的异常类型。

示例代码

import java.io.FileNotFoundException;
import java.io.IOException;

public class MultiCatchDemo {
    public static void main(String[] args) {
        try {
            // 模拟可能抛出不同异常的操作
            int choice = Integer.parseInt(args[0]);
            if (choice == 1) {
                throw new FileNotFoundException("文件未找到");
            } else if (choice == 2) {
                throw new IOException("I/O操作失败");
            } else if (choice == 3) {
                throw new ArithmeticException("算术错误");
            }
        } catch (FileNotFoundException e) {
            // 先捕获子类异常
            System.out.println("处理文件未找到异常:" + e.getMessage());
        } catch (IOException e) {
            // 再捕获父类异常
            System.out.println("处理其他I/O异常:" + e.getMessage());
        } catch (ArithmeticException e) {
            // 捕获ArithmeticException
            System.out.println("处理算术异常:" + e.getMessage());
        } catch (ArrayIndexOutOfBoundsException e) {
            // 捕获数组下标越界异常(当没有传入命令行参数时)
            System.out.println("请传入一个整数参数(1-3)");
        }

        System.out.println("程序结束");
    }
}

说明

  1. 当一个catch捕获多种异常类型时,这些异常类型不能有继承关系
  2. 这种写法比多个catch块更简洁,尤其是当多种异常的处理逻辑相同时。
  3. 可以通过命令行参数来测试不同的异常情况:
    • java MultiCatchDemo 1:测试 FileNotFoundException
    • java MultiCatchDemo 2:测试 IOException
    • java MultiCatchDemo 3:测试 ArithmeticException
    • java MultiCatchDemo:测试 ArrayIndexOutOfBoundsException

12.2.4 声明方法抛出异常

        如果一个方法可能会抛出异常,但不想在方法内部处理,而是让调用者来处理,可以使用throws关键字在方法声明处声明该方法可能抛出的异常。

语法

修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2, ... {
    // 方法体
}

示例代码

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class ThrowsDemo {
    // 声明方法可能抛出FileNotFoundException和IOException
    public static void readFile(String fileName) throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream(fileName);
        int data = fis.read();
        while (data != -1) {
            System.out.print((char) data);
            data = fis.read();
        }
        fis.close();
    }
    
    public static void main(String[] args) {
        try {
            // 调用声明了抛出异常的方法,必须处理这些异常
            readFile("test.txt");
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到:" + e.getMessage());
        } catch (IOException e) {
            System.out.println("文件读取错误:" + e.getMessage());
        }
        
        System.out.println("\n程序执行完毕");
    }
}

说明

  1. 对于 checked exception,如果方法不处理,就必须在方法声明中用throws声明。
  2. 对于 unchecked exception(RuntimeException 及其子类),可以不用throws声明,编译器不会强制要求。
  3. 调用声明了异常的方法时,要么用try-catch处理这些异常,要么在当前方法中也用throws声明继续向上抛出。

12.2.5 用 throw 语句抛出异常

  throw语句用于手动抛出一个具体的异常对象。通常在满足特定条件时,我们认为这是一个异常情况,就可以手动抛出异常。

语法

throw 异常对象;

示例代码

public class ThrowDemo {
    // 计算年龄的方法,如果年龄不合法则抛出异常
    public static void printAge(int birthYear) {
        int currentYear = 2023;
        int age = currentYear - birthYear;
        
        if (birthYear < 1900 || birthYear > currentYear) {
            // 手动抛出异常
            throw new IllegalArgumentException("出生年份不合法:" + birthYear);
        }
        
        System.out.println("今年" + age + "岁");
    }
    
    public static void main(String[] args) {
        try {
            printAge(2000);  // 合法的出生年份
            printAge(2050);  // 不合法的出生年份,会抛出异常
            printAge(1850);  // 这行代码不会执行
        } catch (IllegalArgumentException e) {
            System.out.println("捕获到异常:" + e.getMessage());
        }
        
        System.out.println("程序继续执行");
    }
}

运行结果:

throwthrows的区别

  • throw用于方法内部,抛出的是一个具体的异常对象。
  • throws用于方法声明处,声明的是方法可能抛出的异常类型,可以是多个。

12.2.6 try-with-resources 语句

        Java 7 引入了try-with-resources语句,用于自动管理资源(如文件流、数据库连接等)。它确保在资源使用完毕后自动关闭资源,无需在finally块中手动关闭。

要使用try-with-resources,资源类必须实现AutoCloseable接口(或其子类Closeable接口)。

语法

try (资源声明) {
    // 使用资源的代码
} catch (异常类型 异常对象名) {
    // 处理异常的代码
}

示例代码

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class TryWithResourcesDemo {
    public static void main(String[] args) {
        // try-with-resources语句,资源会自动关闭
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            int data = fis.read();
            while (data != -1) {
                System.out.print((char) data);
                data = fis.read();
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到:" + e.getMessage());
        } catch (IOException e) {
            System.out.println("文件读取错误:" + e.getMessage());
        }
        
        System.out.println("\n程序执行完毕");
    }
}

传统方式与 try-with-resources 对比

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

// 传统方式需要在finally中手动关闭资源
public class TraditionalResourceHandling {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("test.txt");
            // 使用资源,这里添加一些实际的读取操作示例
            int data = fis.read();
            while (data != -1) {
                System.out.print((char) data);
                data = fis.read();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();  // 关闭资源可能也会抛出异常
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

说明

  1. try-with-resources语句可以声明多个资源,用分号分隔
  2. 资源的关闭顺序与声明顺序相反。
  3. try-with-resources语句也可以有catchfinally块,用于处理异常或执行必要的清理工作。

12.3 自定义异常类

        Java 提供的异常类可能无法满足所有业务需求,这时我们可以自定义异常类。自定义异常类通常继承自Exception(checked exception)或RuntimeException(unchecked exception)。

自定义异常类的步骤

  1. 创建一个类,继承ExceptionRuntimeException
  2. 提供构造方法,通常至少提供两个构造方法:一个无参构造方法,一个带有详细信息的构造方法。

示例代码

// 自定义异常类:用户年龄不合法异常
class InvalidAgeException extends Exception {
    // 无参构造方法
    public InvalidAgeException() {
        super();
    }
    
    // 带有详细信息的构造方法
    public InvalidAgeException(String message) {
        super(message);
    }
}

// 自定义异常类:用户姓名为空异常(继承自RuntimeException)
class EmptyNameException extends RuntimeException {
    public EmptyNameException() {
        super();
    }
    
    public EmptyNameException(String message) {
        super(message);
    }
}

// 使用自定义异常的示例
public class CustomExceptionDemo {
    // 注册用户的方法
    public static void registerUser(String name, int age) throws InvalidAgeException {
        if (name == null || name.trim().isEmpty()) {
            // 抛出unchecked异常,不需要在方法声明中throws
            throw new EmptyNameException("用户名不能为空");
        }
        
        if (age < 0 || age > 150) {
            // 抛出checked异常,需要在方法声明中throws
            throw new InvalidAgeException("年龄不合法:" + age + ",年龄必须在0-150之间");
        }
        
        System.out.println("用户注册成功:" + name + "," + age + "岁");
    }
    
    public static void main(String[] args) {
        try {
            registerUser("张三", 25);    // 正常情况
            registerUser("", 30);       // 姓名为空,会抛出EmptyNameException
            registerUser("李四", 200);   // 年龄不合法,会抛出InvalidAgeException
        } catch (InvalidAgeException e) {
            System.out.println("注册失败:" + e.getMessage());
        } catch (EmptyNameException e) {
            System.out.println("注册失败:" + e.getMessage());
        }
        
        System.out.println("程序结束");
    }
}

运行结果:

何时需要自定义异常

  • 当 Java 内置异常不能准确描述业务中的异常情况时。
  • 希望通过异常类型来区分不同的错误场景,便于异常处理。
  • 需要在异常中包含特定的业务信息时。

12.4 断言

        断言(Assertion)是 Java 1.4 引入的特性,用于在程序开发和测试阶段检查某些条件是否满足。如果断言失败,会抛出AssertionError

12.4.1 使用断言

断言的语法有两种形式:

  1. 简单形式
assert 布尔表达式;

如果布尔表达式的值为false,则抛出AssertionError

    2.带消息的形式:

assert 布尔表达式 : 消息表达式;

如果布尔表达式的值为false,则抛出AssertionError,并将消息表达式的值作为错误消息。

12.4.2 开启和关闭断言

        默认情况下,Java 虚拟机(JVM)是关闭断言功能的。要开启断言,需要使用-ea(或-enableassertions)参数。

开启断言的方式

  • 对所有类开启断言:java -ea 类名
  • 对特定包开启断言:java -ea:包名... 类名
  • 对特定类开启断言:java -ea:类名 类名

关闭断言的方式

  • 使用-da(或-disableassertions)参数,用法与-ea类似。

在 IDE(如 Eclipse、IntelliJ IDEA)中,可以在运行配置中设置 VM 参数来开启或关闭断言。

12.4.3 何时使用断言

断言主要用于:

  1. 检查程序内部的 invariants(不变量),即那些在程序正常执行时必须为真的条件。
  2. 检查方法的前置条件和后置条件。
  3. 检查私有方法的参数有效性(对于公共方法,应使用异常来处理无效参数)。

注意

  • 断言不应该用于检查程序运行时可能出现的预期错误,如用户输入错误。
  • 断言可能会被关闭,因此不能依赖断言来处理程序的关键功能。
  • 不要在断言表达式中包含有副作用的操作(如修改变量值),因为当断言关闭时,这些操作不会执行。

12.4.4 断言示例

public class AssertionDemo {
    // 计算三角形面积的方法(海伦公式)
    public static double calculateTriangleArea(double a, double b, double c) {
        // 检查前置条件:三角形的三条边必须为正数
        assert a > 0 && b > 0 && c > 0 : "三角形的边长必须为正数";
        
        // 检查前置条件:三角形任意两边之和大于第三边
        assert a + b > c && a + c > b && b + c > a : "不满足三角形两边之和大于第三边";
        
        double s = (a + b + c) / 2;
        double area = Math.sqrt(s * (s - a) * (s - b) * (s - c));
        
        // 检查后置条件:面积必须为正数
        assert area > 0 : "计算出的面积必须为正数";
        
        return area;
    }
    
    public static void main(String[] args) {
        try {
            double area1 = calculateTriangleArea(3, 4, 5);
            System.out.println("直角三角形的面积:" + area1);
            
            // 测试无效的三角形(两边之和不大于第三边)
            double area2 = calculateTriangleArea(1, 1, 3);
            System.out.println("面积:" + area2);
        } catch (AssertionError e) {
            System.out.println("断言失败:" + e.getMessage());
        }
    }
}

运行说明

  1. 当关闭断言运行时(默认情况),程序不会检查断言条件,可能会计算出不合理的结果。
  2. 当开启断言运行时(java -ea AssertionDemo),第二个计算会触发断言失败,输出:
直角三角形的面积:6.0
断言失败:不满足三角形两边之和大于第三边

12.5 小结

本章我们学习了 Java 中的异常处理机制,主要内容包括:

  1. 异常的概念:异常是程序运行时发生的非正常事件,会中断程序的正常执行。
  2. 异常类体系:所有异常类都继承自Throwable,主要分为ErrorException两大类。Exception又分为 checked exception 和 unchecked exception。
  3. 异常处理机制
    • 使用try-catch-finally语句捕获和处理异常。
    • 使用throws声明方法可能抛出的异常。
    • 使用throw手动抛出异常。
    • 使用try-with-resources自动管理资源。
  4. 自定义异常:当 Java 内置异常不能满足需求时,可以自定义异常类。
  5. 断言:用于开发和测试阶段检查某些条件是否满足,默认是关闭的。

        掌握异常处理是编写健壮 Java 程序的关键。合理地使用异常处理机制,可以使程序在遇到错误时能够优雅地处理,而不是直接崩溃,同时也便于调试和维护。

编程练习

  1. 练习 1:除法计算器
    编写一个程序,实现两个整数的除法运算。要求:

    • 处理除数为 0 的情况(ArithmeticException)。
    • 处理输入非整数的情况(InputMismatchException)。
    • 使用 try-catch-finally 结构,确保程序在任何情况下都能友好地提示用户。

练习 1 参考答案

import java.util.InputMismatchException;
import java.util.Scanner;

public class DivisionCalculator {
    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(System.in);
            
            System.out.print("请输入被除数:");
            int dividend = scanner.nextInt();
            
            System.out.print("请输入除数:");
            int divisor = scanner.nextInt();
            
            int result = dividend / divisor;
            System.out.println(dividend + " / " + divisor + " = " + result);
        } catch (ArithmeticException e) {
            System.out.println("错误:除数不能为0");
        } catch (InputMismatchException e) {
            System.out.println("错误:请输入有效的整数");
        } finally {
            if (scanner != null) {
                scanner.close();
                System.out.println("资源已释放");
            }
        }
        
        System.out.println("程序结束");
    }
}

        

        希望本章的内容能帮助你理解和掌握 Java 异常处理的相关知识。如果有任何疑问或建议,欢迎在评论区留言讨论!


网站公告

今日签到

点亮在社区的每一天
去签到