Exception 异常
- 异常也是对象,也有自己的体系,在这个体系中,所有异常对象的根类是 throwable 接口。
- 异常和 error 错误是不同的概念。
错误是严重的 JVM 系统问题,一般不期待程序员去捕获、处理这些错误,同时,也不应期待程序不会从错误中恢复。
当发生错误时,通常意味着应用程序已经无法正常执行,最好的做法是记录错误信息然后终止程序。
- Java 提供了以下关键字和类来支持异常处理:
- try:用于包裹可能会抛出异常的代码块。
- catch:用于捕获异常并处理异常的代码块。
- finally:用于包含无论是否发生异常都需要执行的代码块。
- throw:用于手动抛出异常。
- throws:用于在方法声明中指定方法可能抛出的异常。
- Exception 类:是所有异常类的父类,它提供了一些方法来获取异常信息,如 getMessage()、printStackTrace() 等。
- 在 Java 中,异常分为两种:
- Checked Exceptions 检查(强制)性异常:必须提前在代码中处理的异常,如果不处理编译器报错。
- Unchecked / RunTime Exceptions 非检查性(运行时)异常:可以在代码中选择性处理的异常,即使不处理也能不报错编译通过
1. Checked Exceptions 检查性/强制性/编译时异常
- 强制性异常是由编译器检查到的异常
- 强制性异常必须提前在程序中写好代码处理。要么通过 try-catch语句捕获,要么通过方法签名声明抛出。
- 强制性异常通常是由
外部
因素引起的,例如文件不存在、网络连接中断等。 - 强制性异常必须在代码中显式处理,要么通过 try-catch 块捕获并处理,要么通过在方法签名中使用 throws 关键字声明该异常,以通知调用者处理。
try {
// 可能会抛出 IOException 的代码
} catch (IOException e) {
// 处理 IOException
}
public void readFile() throws IOException {
// 可能会抛出IOException的代码
}
2. Unchecked / RunTime Exceptions 非检查性(运行时)异常
- 运行时异常是在运行时抛出的异常
- 运行时异常无需提前在程序中设置捕获语句进行处理。
- 运行时异常通常是由程序
内部
错误引起的,例如空指针引用、数组越界等。 - 运行时异常通常是由程序员在编写代码时可以避免的,因此也称为“非强制性”异常。
3. 处理异常
- 异常的处理包括两种方式:
- 声明异常:类似于推卸责任的处理方式
在方法定义时使用 throws 关键字声明异常,告知调用者,调用这个方法可能会出现异常,但自身并不处理。这种处理方式的态度是:如果出现了异常则会抛给调用者来处理。 - 捕获异常:真正的处理
在可能出现异常的代码上使用 try-catch 进行捕获处理。这种处理方式的态度是:把异常抓住。
- 声明异常:类似于推卸责任的处理方式
4. 手动抛出异常 throw
- 程序在运行中检测到错误后,可以创建一个合适类型的异常实例并抛出它
- 在同一方法内先用 throw 抛出异常,再用 try-catch 捕获没有任何作用。正确的做法是调用该方法的方法内,用try-catch 捕获
- 手动抛出的异常有两种, 分别为运行时异常和编译时异常.、
- 抛出运行时异常时,可以不用处理抛出的这个异常.
- 抛出编译时异常时,必须要处理抛出的这个异常,否则编译不能通过
/** divide 方法内部检查了除数是否为 0,如果是,则使用 throw 关键字抛出一个 ArithmeticException。
- 调用这个方法的代码块通过 try-catch 捕获了这个异常,并在 catch 块中处理了这个错误情况
*/
public static double divide(double chushu, double beichushu) throws ArithmeticException {
// 事实上,即使不手动抛出,JVM 也会检测到该运行时异常,自动抛出
// 因此下面三行代码有没有,输出结果都一样
if (beichushu == 0) {
throw new ArithmeticException("除数不能为零");
}
return beichushu / chushu;
}
public static void main(String[] args) {
try {
double result = divide(10, 0);
System.out.println("结果是: " + result);
} catch (ArithmeticException e) {
System.err.println("发生错误: " + e.getMessage());
}
}
public class ThrowEx {
// 手动抛出运行时异常,可以不处理
public void setAge(int age) {
if (age < 0) {
throw new NullPointerException("输入的年龄小于0");
}
}
// 手动抛出编译时异常,调用该方法者必须要处理这个异常
public void setAge2(int age) throws FileNotFoundException {
if (age < 0) {
throw new FileNotFoundException("输入的年龄小于0");
}
}
public static void main(String[] args) {
ThrowEx throwEx = new ThrowEx();
// 首先明确,尽管方法 1, 2 都抛出了异常,但在调用方法种,并没有处理抛出的异常
//方法 1 指定的异常是运行时异常,调用者不是必须处理这个异常,编译也能通过
//方法 2 指定的异常是编译时异常,由于调用者没有处理,因此无法编译通过
throwEx.setAge(-5);
//throwEx.setAge2(-5);
}
调用方法 1 时,正常编译构建,并送出错误提示
调用方法 2 时,无法通过编译构建
同时调用方法1,2时,依然无法通过编译构建,这验证了之前说过的话:抛出编译时异常时,必须要处理抛出的这个异常,否则编译不能通过。
5. 自动捕获异常 try-catch-finally
- 使用 try-catch-finally 处理编译时异常,是让程序在编译时就不再报错,但是运行时仍然有可能报错。相当于我们使用 try-catch 将一个编译时可能出现的异常,延迟到运行时出现。
- 在开发中,运行时异常比较常见,此时一般不用 try-catch 去处理,因为处理和不处理都是一个报错,最好办法是去修改代码。
- try 和 catch 必须配对使用,finally 则没有强制要求
- try 语句块中:放的是可能出现问题的代码,尽量不要把不相关的代码放入到里面,否则会出现截断的问题。
- catch 括号中,声明想要捕获的异常类型;语句块中,放置捕获指定异常后处理问题的代码,如果问题在 try 语句块中没有出现,则 catch 不会运行。
- finally 语句块中:放的是不管问题异常是否产生,都要执行的代码。
/**Integer.parseInt(str)方法将 str 转换为整数。但由于str的内容不是有效的整数(它包含字母而非数字),
- 这个操作将会抛出一个 NumberFormatException。
- catch 括号中声明本次捕获目标是 NumberFormatException,如果发生此异常,将执行该块中的代码,即 e.printStackTrace()。
- 第二个 catch 括号中声明本次捕获目标是 Exception,由于 Exception 本就是所有异常的根类
- 因此第二个 catch 可以捕获除 NumberFormatException 之外的所有异常类型(前提是这些异常没有在第一个 catch 块中被捕获)。
- 这里,如果捕获到任何其他异常,程序将输出e.getMessage(),即异常的描述信息。
- finally块:无论是否发生异常,finally块中的代码都会被执行。
*/
public static void main(String[] args) {
String str = "avocado";
try {
int i = Integer.parseInt(str);
} catch (NumberFormatException e) {
e.printStackTrace();
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
System.out.println("运行完毕");
}
}
6. 声明异常 throws
- 在 Java 中,当前执行的语句必属于某个方法。
- 每个方法都必须声明它可能抛出的强制性异常的类型。
- 声明的方式是:在方法头使用 throws 关键字
public void myMethod throws Exceptionl, Exception2,… ,ExceptionN
- throws 一般用于方法中可能存在异常时,需要将异常情况向方法之上的层级抛出,由抛出方法的上一级来解决这个问题,如果方法的上一级无法解决的会就会再将异常向上抛出,最终会抛给 main 方法,这样一来 main 方法中调用了这个方法的时候,就需要解决这个可能出现的异常.
当然 main 方法也可以不解决异常,将异常往上抛给 JVM ,如果 JVM 也无法解决的话,那么 JVM 就 over 了 - try-catch-fianlly 真正将异常给处理掉了。throws 只是将异常抛给了方法的调用者,并没有真正将异常处理掉。
- 之前所述,子类重写父类方法时,不可以抛出新的强制性异常,或者比父类方法声明的更广泛的强制性异常。
7. 如何选择 try-catch-finally 和 throws
什么时候捕捉?什么时候声明?
如果异常发生后需要调用者来处理的,需要调用者知道的,则采用声明方式。否则采用捕捉。
- 同一方法内, try-catch 和 throws 不要同时使用。因为只要 try-catch 就已经将异常处理掉了,再 throws 没有任何意义
- 放眼整个程序,try-catch 和 throws 应当联合使用。
例如,在方法标签通过 throws 抛出异常,让调用者决定如何处理。而调用者使用 try-catch 捕获并处理这些异常。
- 如果父类的方法没有声明抛出任何检查型异常(Checked Exception),则子类在重写这个方法时也不能声明抛出检查型异常。因此,如果子类重写的方法中需要处理异常,必须在方法内部使用 try-catch-finally 进行处理。
如果父类的方法没有使用 throws 声明任何检查型异常,意味着该方法不抛出任何需要强制处理的检查型异常。
- 在一个复杂的调用链中,建议在较底层的方法中使用 throws 抛出异常,而在较高层的方法中使用 try-catch-finally 来处理这些异常。
假设我们有以下方法调用链:
方法 a 调用 方法 b
方法 b 调用 方法 c
在较底层的方法 b、c ,使用 throws 声明这些方法可能抛出的异常,而不在这些方法内部进行异常捕获和处理。这种做法有以下优点:
异常传播:将异常传递给调用者,让调用者决定如何处理。
简化代码:底层方法专注于核心逻辑,不必处理异常,从而简化代码。
public class Example {
public static void methodC() throws Exception {
if (someCondition) {
throw new Exception("Method C exception");
}
// 方法 C 的其他逻辑
}
public static void methodB() throws Exception {
methodC();
// 方法 B 的其他逻辑
}
public static void methodA() {
try {
methodB();
} catch (Exception e) {
System.err.println("发生错误: " + e.getMessage());
} finally {
System.out.println("清理操作");
}
}
public static void main(String[] args) {
methodA();
}
}
高层方法使用 try-catch-finally 处理异常
在较高层的方法 a ,使用 try-catch-finally 捕获和处理所有底层方法抛出的异常。这种做法有以下优点:
集中处理:所有异常都在一个地方处理,便于管理和维护。
保证执行:即使发生异常,也可以确保 finally 块中的清理操作能够执行。
为什么要这样做
数据返回问题:如果在方法 b 中使用 try-catch 捕获并处理异常,方法 a 可能无法获得所需的数据。这是因为异常处理可能会中断正常的数据流。
异常聚合:将异常传递到高层方法,允许高层方法集中处理所有可能的异常,提高了代码的可读性和可维护性。
8. 自定义异常类
Java 中允许自定义异常(类)
- 所有异常都必须是 Throwable 的子类。
- 如果目标是写一个强制性异常类,则需要继承 Exception 类。
- 如果目标是写一个运行时异常类,则需要继承 RuntimeException 类。
- 一个异常类和其它任何类一样,包含有变量和方法。
8.1 自定义异常处理-- 支付余额不足
// 自定义异常类:余额不足
// 继承 Exception 类,该自定义异常是强制性异常,必须在编译之前,由 try-catch 捕获
public class InsufficientFundsException extends Exception
{
//balanceShortage 储存当出现异常(取出钱多于余额时)所缺乏的钱
private double balanceShortage;
public InsufficientFundsException(double balanceShortage)
{
this.balanceShortage= balanceShortage;
}
public double getBalanceShortage()
{
return balanceShortage;
}
}
//此类模拟银行账户
public class Account {
// 区分异常类,此处的 balance 就是单纯的余额
private double balance;
private int cardNum;
public Account (int cardNum) {
this.cardNum = cardNum;
}
//方法:存钱
public void deposit(double amount) {
balance += amount;
}
//方法:取钱
public void withdraw(double amount) throws
InsufficientFundsException {
if (amount <= balance) {
balance -= amount;
} else {
double needs = amount - balance;
throw new InsufficientFundsException(needs);
}
}
//方法:返回余额
public double getBalance() {
return balance;
}
//方法:返回卡号
public int getCardNum() {
return cardNum;
}
}
public class BankDemo
{
public static void main(String [] args)
{
Account account = new Account(101);
System.out.println("Depositing $500...");
account.deposit(500.00);
try
{
System.out.println("\nWithdrawing $100...");
account.withdraw(100.00);
System.out.println("\nWithdrawing $600...");
account.withdraw(600.00);
}catch(InsufficientFundsException e)
{
System.out.println("Sorry, but you are short $"
+ e.getBalanceShortage());
e.printStackTrace();
}
}
}
输出为 :
Depositing $500...
Withdrawing $100...
Withdrawing $600...
Sorry, but you are short $200.0
InsufficientFundsException
at CheckingAccount.withdraw(CheckingAccount.java:25)
at BankDemo.main(BankDemo.java:13)