MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java常见异常及其处理方法

2024-03-227.0k 阅读

Java 常见异常及其处理方法

Java 异常体系概述

在 Java 编程中,异常是指在程序执行过程中出现的、打断正常指令流程的事件。Java 异常机制为开发者提供了一种统一的方式来处理程序运行时发生的错误情况,使得程序能够更加健壮和可靠。Java 的异常体系结构是一个树形结构,以 Throwable 类作为所有异常和错误的基类。

Throwable 类有两个主要的子类:ErrorExceptionError 类通常用于表示严重的系统错误,如 OutOfMemoryError(内存溢出错误)和 StackOverflowError(栈溢出错误)等,这类错误一般无法通过程序代码进行处理,因为它们往往代表着 JVM 本身的问题或者系统资源耗尽等严重情况。

Exception 类及其子类则用于表示程序运行时可能出现的各种可恢复的错误情况。Exception 又进一步分为 Checked Exception(受检异常)和 Unchecked Exception(非受检异常)。

Checked Exception(受检异常)

Checked Exception 是在编译期就需要处理的异常。如果一个方法可能抛出某个 Checked Exception,要么在方法内部使用 try-catch 块捕获并处理该异常,要么在方法声明中使用 throws 关键字声明抛出该异常,让调用该方法的上层代码来处理。例如,IOException 及其子类(如 FileNotFoundException)就是典型的 Checked Exception。当我们尝试读取一个不存在的文件时,就会抛出 FileNotFoundException

Unchecked Exception(非受检异常)

Unchecked Exception 包括 RuntimeException 及其子类,它们在编译期不需要强制处理。这类异常通常是由于程序逻辑错误导致的,如 NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组越界异常)等。虽然在编译期不会强制要求处理,但在运行时一旦发生,同样会导致程序中断。

常见的 Checked Exception 及其处理

IOException

IOException 是所有输入输出操作相关异常的基类,用于处理与流、文件、网络等 I/O 操作相关的错误。

示例:读取文件时可能抛出 FileNotFoundException

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class FileReadingExample {
    public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader("nonexistentfile.txt"));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            reader.close();
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到:" + e.getMessage());
        } catch (IOException e) {
            System.out.println("读取文件时发生错误:" + e.getMessage());
        }
    }
}

在上述代码中,FileReader 的构造函数如果传入一个不存在的文件名,就会抛出 FileNotFoundException,这是 IOException 的子类。我们使用 try-catch 块来捕获并处理这个异常,打印出错误信息。同时,readLine()close() 方法也可能抛出 IOException,所以我们也捕获了这个更宽泛的异常类型。

SQLException

SQLException 用于处理与数据库操作相关的异常。当执行 SQL 语句时,可能会出现各种错误,如数据库连接失败、SQL 语法错误、违反数据库约束等,都会抛出 SQLException

示例:查询数据库时可能抛出 SQLException

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class DatabaseQueryExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String user = "root";
        String password = "password";
        try (Connection conn = DriverManager.getConnection(url, user, password);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT * FROM nonexistent_table")) {
            while (rs.next()) {
                System.out.println(rs.getString(1));
            }
        } catch (SQLException e) {
            System.out.println("数据库操作错误:" + e.getMessage());
        }
    }
}

在这个例子中,如果尝试查询一个不存在的表 nonexistent_table,就会抛出 SQLException。我们通过 try-catch 块捕获并打印错误信息。注意,这里使用了 Java 7 引入的 try-with-resources 语句,它会自动关闭实现了 AutoCloseable 接口的资源(如 ConnectionStatementResultSet),从而避免了手动关闭资源可能导致的 IOException

常见的 Unchecked Exception 及其处理

NullPointerException

NullPointerException 是 Java 程序中最常见的异常之一,当程序试图在一个 null 对象上调用方法、访问属性或者进行数组操作时就会抛出该异常。

示例:空指针异常示例

public class NullPointerExceptionExample {
    public static void main(String[] args) {
        String str = null;
        // 试图调用 null 对象的方法
        int length = str.length(); 
    }
}

上述代码会抛出 NullPointerException,因为 strnull,无法调用 length() 方法。为了避免这种异常,在调用对象的方法或访问属性之前,应该先检查对象是否为 null

改进后的代码

public class NullPointerExceptionExample {
    public static void main(String[] args) {
        String str = null;
        if (str != null) {
            int length = str.length();
            System.out.println("字符串长度:" + length);
        } else {
            System.out.println("字符串为 null");
        }
    }
}

ArrayIndexOutOfBoundsException

ArrayIndexOutOfBoundsException 当程序试图访问数组中不存在的索引位置时抛出。数组的索引范围是从 0 到数组长度减 1,如果超出这个范围就会引发该异常。

示例:数组越界异常示例

public class ArrayIndexOutOfBoundsExceptionExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        // 试图访问超出数组范围的索引
        int value = numbers[3]; 
    }
}

在上述代码中,numbers 数组的有效索引是 0、1、2,访问索引 3 会导致 ArrayIndexOutOfBoundsException。为了避免这种异常,在访问数组元素之前,需要确保索引在有效范围内。

改进后的代码

public class ArrayIndexOutOfBoundsExceptionExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        int index = 3;
        if (index >= 0 && index < numbers.length) {
            int value = numbers[index];
            System.out.println("数组元素:" + value);
        } else {
            System.out.println("索引超出范围");
        }
    }
}

ClassCastException

ClassCastException 当试图将一个对象强制转换为不兼容的类型时抛出。例如,将一个 String 对象强制转换为 Integer 对象,就会引发该异常。

示例:类型转换异常示例

public class ClassCastExceptionExample {
    public static void main(String[] args) {
        Object obj = "Hello";
        // 试图将 String 对象强制转换为 Integer 对象
        Integer num = (Integer) obj; 
    }
}

上述代码会抛出 ClassCastException,因为 obj 实际上是一个 String 类型,无法直接转换为 Integer 类型。为了避免这种异常,可以使用 instanceof 关键字在类型转换之前进行类型检查。

改进后的代码

public class ClassCastExceptionExample {
    public static void main(String[] args) {
        Object obj = "Hello";
        if (obj instanceof Integer) {
            Integer num = (Integer) obj;
            System.out.println("转换后的整数:" + num);
        } else {
            System.out.println("对象不是 Integer 类型,无法转换");
        }
    }
}

IllegalArgumentException

IllegalArgumentException 当方法接收到一个不合法或不合适的参数时抛出。这种异常通常用于方法内部对参数进行有效性检查。

示例:非法参数异常示例

public class IllegalArgumentExceptionExample {
    public static void calculateSquareRoot(double number) {
        if (number < 0) {
            throw new IllegalArgumentException("不能计算负数的平方根");
        }
        double result = Math.sqrt(number);
        System.out.println("平方根:" + result);
    }

    public static void main(String[] args) {
        try {
            calculateSquareRoot(-10);
        } catch (IllegalArgumentException e) {
            System.out.println("错误:" + e.getMessage());
        }
    }
}

在上述代码中,calculateSquareRoot 方法检查传入的参数是否为负数,如果是,则抛出 IllegalArgumentException。调用该方法时,可以使用 try-catch 块捕获并处理这个异常。

自定义异常

除了 Java 内置的异常类型,开发者还可以根据业务需求自定义异常。自定义异常通常继承自 Exception 类(如果是 Checked Exception)或 RuntimeException 类(如果是非受检异常)。

示例:自定义 Checked Exception

class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}

public class CustomCheckedExceptionExample {
    public static void processData(int value) throws MyCheckedException {
        if (value < 0) {
            throw new MyCheckedException("数据不能为负数");
        }
        System.out.println("处理数据:" + value);
    }

    public static void main(String[] args) {
        try {
            processData(-5);
        } catch (MyCheckedException e) {
            System.out.println("捕获到自定义异常:" + e.getMessage());
        }
    }
}

在这个例子中,MyCheckedException 继承自 Exception,因此是一个 Checked Exception。processData 方法在传入负数时抛出该异常,调用该方法的 main 方法必须使用 try-catch 块捕获或者在方法声明中使用 throws 关键字声明抛出该异常。

示例:自定义 Unchecked Exception

class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}

public class CustomUncheckedExceptionExample {
    public static void validateUser(String username) {
        if (username == null || username.isEmpty()) {
            throw new MyUncheckedException("用户名不能为空");
        }
        System.out.println("用户名有效:" + username);
    }

    public static void main(String[] args) {
        try {
            validateUser("");
        } catch (MyUncheckedException e) {
            System.out.println("捕获到自定义非受检异常:" + e.getMessage());
        }
    }
}

这里 MyUncheckedException 继承自 RuntimeException,是一个非受检异常。validateUser 方法在用户名不符合要求时抛出该异常,调用该方法时可以选择捕获也可以不捕获,因为非受检异常在编译期不强制要求处理。

异常处理的最佳实践

  1. 尽量具体地捕获异常:在 try-catch 块中,尽量捕获具体的异常类型,而不是宽泛的 Exception 类型。这样可以更准确地处理不同类型的异常,并且避免掩盖真正的错误。例如,在处理文件读取操作时,先捕获 FileNotFoundException,然后再捕获更宽泛的 IOException
  2. 避免空的 catch:空的 catch 块会忽略异常,使得程序难以调试和维护。应该在 catch 块中记录异常信息或者采取适当的恢复措施。
  3. 合理使用 finallyfinally 块中的代码无论 try 块是否抛出异常,都会被执行。通常用于关闭资源(如文件、数据库连接等)。Java 7 引入的 try-with-resources 语句在大多数情况下可以更方便地管理资源,减少手动关闭资源的代码。
  4. 不要在构造函数中抛出 Checked Exception:如果在构造函数中抛出 Checked Exception,会使得创建对象变得复杂,因为调用者必须处理这个异常。可以考虑抛出 Unchecked Exception 或者在构造函数中进行必要的初始化检查,确保对象处于可用状态。
  5. 异常处理与性能:虽然异常机制为程序提供了强大的错误处理能力,但频繁地抛出和捕获异常会对性能产生一定的影响。因此,应该避免在正常的程序流程中使用异常来控制逻辑,而是将异常用于处理真正的错误情况。

通过深入了解 Java 常见异常及其处理方法,开发者可以编写出更加健壮、可靠和易于维护的程序。无论是处理 Checked Exception 还是 Unchecked Exception,都需要根据具体的业务需求和场景选择合适的处理方式,同时遵循异常处理的最佳实践,以提高程序的质量和稳定性。在实际开发中,不断积累处理异常的经验,能够更好地应对各种复杂的情况,确保 Java 应用程序的高效运行。