第九章 Java异常及程序调试技术

前言:

程序编码人员难免会遇到编程错误,但应采取必要措施避免错误发生,或能在出现错误时快速定位并修正。本章系统介绍Java程序错误的分类、异常处理机制、自定义异常、程序调试技巧,并通过综合案例演示如何优雅地处理异常。


一、程序错误分类

程序错误大致分为三类:

  1. 编译错误(静态错误):拼写错误或未正确导入类导致,IDEA通常能即时提示并快速修正。
  2. 程序异常(运行时异常) :程序运行过程中因考虑不周导致的异常终止,Java提供try-catch-finally机制处理。
  3. 程序运行逻辑错误:运行结果与预期不符,多为算法设计错误,需重新审视算法并修正。

二、Java异常及其处理入门

2.1 程序异常现象举例

以下代码从键盘输入两个整数并相除,当第二个数为零时会抛出ArithmeticException

java代码演示:

java 复制代码
package org.example;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个整数:");
        int n1 = sc.nextInt();
        System.out.println("请再输入一个整数:");
        int n2 = sc.nextInt();
        int n3 = n1 / n2;  // 若n2为0,此处抛出异常
        System.out.println(n1 + "/" + n2 + "=" + n3);
    }
}

输出结果:

解决方法:在除法前检查除数是否为0,或使用异常处理机制。

Java代码演示:

java 复制代码
package org.example;
 
import java.util.Scanner;
 
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
 
        System.out.println("请输入一个整数:");
        int n1 = sc.nextInt();
        System.out.println("请再输入一个整数:");
        int n2 = sc.nextInt();
 
if (n2 == 0) System.exit(1);  //如果除数为0,则退出程序从而避免异常发生
 
        int n3 = n1 / n2;
        System.out.println(n1 + " / " + n2 + " = " + n3);
    }
}

2.2 Java异常处理入门

Java提供try...catch...finally机制处理运行时异常,基本语法如下:

java代码演示:

java 复制代码
try {
    // 可能出现异常的代码
} catch (异常类型1 变量) {
    // 对异常类型1的处理代码
} catch (异常类型2 变量) {
    // 对异常类型2的处理代码
} ...
finally {
    // 无论是否发生异常都会执行的代码
}

改进后的除法示例:

java 复制代码
package org.example;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个整数:");
        int n1 = sc.nextInt();
        System.out.println("请再输入一个整数:");
        int n2 = sc.nextInt();

        try {
            int n3 = n1 / n2;
            System.out.println(n1 + "/" + n2 + "=" + n3);
        } catch (ArithmeticException e) {
            System.out.println("发生了程序异常,异常原因如下:" + e.getMessage());
        } finally {
            System.out.println("程序运行结束!");
        }
    }
}

三、深入Java异常及异常处理

Java异常类构成了一个庞大的体系。异常类的顶端是Throwable类,典型异常类及其继承关系如图:

Java异常类体系以Throwable 为顶层,主要分支为ErrorException

3.1 Error类

  • JDK文档对Error类是这样解释的:Error类是Trowable的子类,它表示程序运行过程中出现了严重错误,并且应用程序不应该捕获它。
  • 从这段描述可知:Error是程序无法处理的错误,表示Java虚拟机在运行应用程序时发生了严重问题。大多数Error表示代码运行时Java 虚拟机出现的问题,一般来说与代码编写者执行的操作无关,但是与程序对计算机资源的使用或占用有关。
  • 解决Java程序运行时出现Error异常的有效途径就是仔细检查程序代码,减小程序对计算机资源的占用,或者补充缺失的资源。

总结:Error 类表示程序运行过程中的严重错误,应用程序不应捕获它。通常与JVM资源占用有关,如OutOfMemoryErrorStackOverflowError。解决方法是优化程序算法,减少资源消耗。

3.2 Exception类

Exception 类是程序可以捕获并需要适当处理的所有异常的祖先。常见子类包括RuntimeExceptionIOExceptionArithmeticExceptionNullPointerExceptionIndexOutOfBoundsException等。

根据程序中一个异常是否被Java编译器强制要求捕获并处理,可以进一步将Exception类及其子类划分为非检查性异常和检查性异常,习惯上称为unchecked exception和checked exception。

3.3 非检查性异常(Unchecked Exception)

又称运行时异常 ,编译器不强制捕获。主要包括Error类及其子类、RuntimeException类及其子类。常见的有:

  • NullPointerException
  • IndexOutOfBoundsException
  • ArithmeticException
  • ClassCastException

根本原因:编码疏漏。最好的解决办法是检查并修正代码缺陷。

3.4 检查性异常(Checked Exception)

又称非运行时异常 ,编译器强制要求处理,否则无法通过编译。主要包括Exception子类中除RuntimeException及其子类以外的所有异常,如:

  • IOException
  • SQLException
  • ClassNotFoundException

示例:使用Class.forName()时必须处理ClassNotFoundException

java代码演示:

java 复制代码
package org.example;

public class Main {
    public static void main(String[] args) {
        try {
            Class.forName("com.ttt.exception.Abc");
        } catch (ClassNotFoundException e) {
            System.out.println("不能加载需要的类");
        }
    }
}

3.5 Java异常处理规则

处理Java异常的方式是采用try...catch...finally机制:将可能出现异常的代码放置在try代码块中,并使用catch捕获所有可能出现的异常并进行处理,最后,有选择的,可以使用finally代码块进行清理工作。

注意事项:

  1. try块:可放置任何语句,包括可能产生异常的语句。
  2. catch顺序 :捕获的异常必须从小到大排列,即子类异常在前,父类异常在后。
  3. 异常传播 :在catch块中可通过throw将异常抛给方法的直接调用者。

3.6 自定义异常

当内置异常类无法满足需求时,可继承Exception或其子类创建自定义异常。

java代码演示:

java 复制代码
package com.ttt.exception;

public class MyException extends Exception {
    private final String message;
    private final ExceptionType et;

    public MyException(String message, ExceptionType et) {
        this.message = message;
        this.et = et;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public ExceptionType getEt() {
        return et;
    }

    @Override
    public String toString() {
        return "MyException{" +
                "message='" + message + '\'' +
                ", et=" + et +
                '}';
    }
}

配套枚举类:

java代码演示:

java 复制代码
package com.ttt.exception;

public enum ExceptionType {
    Age,
    Salary
}

四、案例:优雅地处理程序异常

4.1 案例任务

定义Worker类,包含属性:nameagesalarycompany。该类只有private 构造方法,通过public static工厂方法创建对象。构造方法中对参数进行检查:

  • 年龄必须在18~60 之间,否则抛出MyException,类型为ExceptionType.Age
  • 薪金必须在0~100000 之间,否则抛出MyException,类型为ExceptionType.Salary

4.2 案例实施

Worker类代码

java 复制代码
package com.ttt.exception;

import java.util.Scanner;

/**
 * Worker 类:表示一个员工对象,包含姓名、年龄、薪金和公司信息。
 * 通过静态工厂方法 getWorkerFromInput() 从控制台获取用户输入来创建实例。
 */
public class Worker {
    private String name;      // 员工姓名
    private int age;          // 员工年龄
    private float salary;     // 每月薪金
    private String company;   // 所属公司名称

    // 全局共享的 Scanner 对象,用于从控制台读取用户输入
    private static Scanner sc = new Scanner(System.in);

    /**
     * 私有构造方法,用于创建 Worker 对象。
     * 在创建时会对年龄和薪金进行合法性校验,不合法则抛出自定义异常。
     *
     * @param name    员工姓名
     * @param age     员工年龄(合法范围:18 ~ 60)
     * @param salary  每月薪金(合法范围:0 ~ 1000000)
     * @param company 所属公司名称
     * @throws MyException 当年龄或薪金不在合法范围内时抛出,携带对应的异常类型
     */
    private Worker(String name, int age, float salary, String company) throws MyException {
        // 校验年龄是否在 18~60 的合法范围内
        if (age < 18 || age > 60) {
            throw new MyException("年龄错误", ExceptionType.Age);
        }
        // 校验薪金是否在 0~1000000 的合法范围内
        if (salary < 0 || salary > 1000000) {
            throw new MyException("薪金错误", ExceptionType.Salary);
        }
        this.name = name;
        this.age = age;
        this.salary = salary;
        this.company = company;
    }

    /**
     * 静态工厂方法:从控制台读取用户输入,创建并返回一个 Worker 对象。
     * 依次读取姓名、年龄、薪金、公司名称,若姓名或公司名称为空则返回 null。
     * 若构造过程中抛出 MyException,则打印失败信息并返回 null。
     *
     * @return 创建成功的 Worker 对象,若输入无效或校验失败则返回 null
     * @throws MyException 声明可能抛出 MyException(实际在内部已被 catch 捕获)
     */
    public static Worker getWorkerFromInput() throws MyException {
        // 读取员工姓名
        System.out.print("请输入姓名:");
        String n = sc.nextLine();

        // 读取员工年龄
        System.out.print("请输入年龄:");
        int a = sc.nextInt();

        // 读取每月薪金
        System.out.print("请输入每月薪金:");
        float sa = sc.nextFloat();

        // 清除 nextFloat() 后缓冲区中残留的换行符,避免影响后续 nextLine() 读取
        sc.nextLine();

        // 读取公司名称
        System.out.print("请输入公司名称:");
        String co = sc.nextLine();

        // 若姓名或公司名称为空白字符串,视为无效输入,直接返回 null
        if (n.isBlank() || co.isBlank()) {
            return null;
        }

        try {
            // 调用私有构造方法创建 Worker 对象
            return new Worker(n, a, sa, co);
        } catch (MyException e) {
            // 捕获年龄或薪金校验失败抛出的自定义异常,打印失败原因
            System.out.println("创建Worker对象失败!");
            System.out.print("失败原因:" + e.getMessage());
        }
        return null;
    }

    /**
     * 重写 toString 方法,返回 Worker 对象的字符串描述。
     *
     * @return 包含姓名、年龄、薪金、公司信息的格式化字符串
     */
    @Override
    public String toString() {
        return "Worker{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                ", company='" + company + '\'' +
                '}';
    }
}

Main测试类

java 复制代码
package org.example;
import com.ttt.exception.Worker;

public class Main {
    public static void main(String[] args) {
        Worker w = Worker.getWorkerFromInput();
        if (w == null) {
            System.out.println("不能创建Worker对象");
            return;
        }
        System.out.println(w.toString());
    }
}

运行示例

java 复制代码
请输入姓名:Gates
请输入年龄:34
请输入每月薪金:6000
请输入公司名称:TTT
Worker{name='Gates', age=34, salary=6000.0, company='TTT'}

当输入公司名称为空时:

java 复制代码
请输入姓名:Gates
请输入年龄:34
请输入每月薪金:6000
请输入公司名称:
不能创建Worker对象

五、在IDEA中调试Java程序

调试程序分为四步:

  1. 设置断点:在代码行左侧单击,出现红色圆点。
  2. 以Debug模式运行 :点击IDE右上角的Debug按钮。
  3. 逐步执行并观察变量 :使用Step Over(F8)逐行执行,查看变量值变化。
  4. 发现并修正缺陷:根据观察结果修改代码,重复上述步骤直至程序正确。

六、Java异常及程序调试最佳实践

  • Error类异常:虽由JVM引起,但本质是程序算法缺陷(如内存泄漏)。应重新审视算法,减少资源占用。
  • 检查性异常 :编译器强制处理,相对容易应对。务必在catch中给出有意义的处理逻辑,而非仅打印堆栈。
  • 非检查性异常:需仔细审视每行代码可能引发的运行时异常,逐一处理。鲁棒程序中,业务代码仅占30%~40%,异常处理代码占60%~70%。
  • 程序调试:是发现并修正程序错误的重要技术手段,尤其适用于逻辑错误和隐蔽的运行时异常。

七、练习:将从键盘输入的字符串转换为浮点数

编写Java程序,接收键盘输入的任意字符串,将其转换为浮点数并输出。要求处理各种异常,例如:

  • 输入"1234.5" → 输出1234.5
  • 输入"X1234.5hello" → 捕获异常并给出友好提示

提示 :可使用Float.parseFloat()Double.parseDouble(),并捕获NumberFormatException

相关推荐
葡萄城技术团队2 小时前
InfluxDB 2\.x 深度解析:核心架构、Flux 函数与制造业落地指南(三)
java·开发语言·架构
Super 含2 小时前
Android 启动优化(五):线程、GC 与 IO 为什么会拖慢启动?
java·服务器·数据库
counting money2 小时前
Java IO流详解:从InputStream到文件操作实战
java·开发语言·python
wuyk5552 小时前
98.C语言易混难点:字符数组与字符串指针的底层差异
c语言·开发语言·c++·stm32·嵌入式硬件·算法
峥无3 小时前
从0到1手撕红黑树:封装实现 my_map 与 my_set(SGI-STL 源码级深度解析)
开发语言·c++·笔记·算法·stl
波特率1152003 小时前
C++新特性---属性说明符与标准属性
开发语言·c++
程序员小八7773 小时前
上海百度B端java后端日常实习一面
java·开发语言
wp123_13 小时前
IPX8 防水 Type‑C连接器:安费诺 124018792112A 与 TONEVEE TY48087‑24A 技术梳理
c语言·开发语言
不会代码的小猴3 小时前
3. 控件学习1
开发语言·c++·笔记·qt·算法
fangzhanpeng1683 小时前
(前端)2.js变量作用域样例
开发语言·前端·javascript