Java的异常被分为两大类:Checked异常和Runtime异常(运行时异常)。所有RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。
Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时将会发生错误,无法通过编译。
对于Checked异常的处理方式有如下两种:
1、当前方法明确知道如何处理该异常,程序应该使用try..catch块来捕获该异常,然后在对应的catch块中修复该异常。
2、当前方法不知道如何处理该异常,应该在定义方法时声明抛出该异常。
Runtime异常则更加灵活,
Runtime异常无需显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try...catch块来实现。
注:只有Java提供了Checked异常,Checked异常体现了Java的严谨性,它要求程序员要么显式声明抛出,要么显式捕获并处理它。
一、使用throws声明抛出异常
使用throws声明抛出异常的思路,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理:如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交由JVM处理。JVM对异常的处理方法是,打印跟踪栈信息,并终止程序的运行,这就是前面程序在遇到异常后自动结束的原因。
throws声明抛出只能在方法签名中使用,使用throws可以声明抛出多个异常类,多个异常类之间使用逗号隔开,throws声明抛出的语法格式如下:
throws ExceptionClass1,ExceptionClass2...
如下例子程序使用了throws来声明抛出IOException异常,一旦程序使用了throws语句声明抛出该异常,程序无须使用try...catch块来捕获该异常。
1 import java.io.*; 2 public class ThrowsTest 3 { 4 public static void main(String[] args) 5 throws IOException 6 { 7 var fis=new FileInputStream("a.txt"); 8 } 9 }
上面程序声明不处理IOException异常,将该异常交给JVM处理,所以一旦遇到了该异常,JVM会打印该异常的跟踪栈信息,并结束程序。运行上面程序,将看到如下的运行结果:
1 ---------- 运行Java捕获输出窗 ---------- 2 Exception in thread "main" java.io.FileNotFoundException: a.txt (系统找不到指定的文件。) 3 at java.base/java.io.FileInputStream.open0(Native Method) 4 at java.base/java.io.FileInputStream.open(FileInputStream.java:213) 5 at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155) 6 at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110) 7 at ThrowsTest.main(ThrowsTest.java:7) 8 9 输出完成 (耗时 0 秒) - 正常终止
注:不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行改代码块时出现了异常,系统总会生成一个异常对象。如果程序没有为这段代码定义任何catch块,则Java运行时环境无法找到处理该catch块,程序就会退出。如果声明抛出了该异常,系统就会打印异常的跟踪栈信息。
如果某段代码调用了一个带throws声明的方法,该方法抛出Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显式捕获该异常,要么方法在另一个带throws方法声明抛出的方法中。下面程序示范了这种方法
1 import java.io.*; 2 public class ThrowsTest2 3 { 4 public static void main(String[] args) 5 throws Exception 6 { 7 //因为test()方法声明抛出IOException异常 8 //所以调用该方法的代码要么处于try...catch块中, 9 //要么在另一个带有throws声明抛出的方法中 10 test(); 11 } 12 public static void test() 13 throws IOException 14 { 15 //因为FileInputStream的构造器声明抛出IOException 16 //所以调用FileInputStream的代码要么处在try...catch块中, 17 //要么在另一个带有throws声明抛出的方法中 18 var fis=new FileInputStream("a.txt"); 19 } 20 }
二、方法重写时声明抛出异常的限制
使用throws声明抛出异常时有一个限制,就是方法重写的“两小”中的一条规则:子类方法声明抛出的异常应该是父类方法声明抛出的异常的子类或者相同,子类方法声明抛出的异常不允许比父类声明抛出的异常多。
1 import java.io.*; 2 public class OverrideThrows 3 { 4 public void test() throws IOException 5 { 6 var fis=new FileInputStream("a.txt"); 7 } 8 } 9 class Sub extends OverrideThrows 10 { 11 //子类方法声明抛出了比父类更大的异常 12 public void test() throws Exception 13 { 14 var fis=new FileInputStream("a.txt"); 15 } 16 } 17 ---------- 编译Java ---------- 18 OverrideThrows.java:12: 错误: Sub中的test()无法覆盖OverrideThrows中的test() 19 public void test() throws Exception 20 ^ 21 被覆盖的方法未抛出Exception 22 1 个错误 23 24 输出完成 (耗时 1 秒) - 正常终止
上面程序中Sub子类中的test()方法声明抛出Exception异常,该Exception异常是IOException类的父类,这将导致程序无法编译通过。
使用Checked异常至少存在两个不便的地方:
1、对于程序中的Checked异常,Java要求必须显式捕获并处理该异常,或者显式抛出该异常,这就增加了变成的复杂度。
2、如果在方法中显式声明抛出了Checked异常,将会导致方法签名与异常耦合,如果该方法重写父类的方法,则该方法抛出的异常还会被重写方法所抛出的异常限制。
当使用Runtime异常时,程序无须在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。使用Runtime异常是比较省事的方式,使用这种方式既可以享受“正常代码和错误处理代码分离”,“保证程序健壮性”的优势,又可以避免因为使用Checked异常带来的编程繁琐性。
Checked异常能在编译时提醒程序员代码可能存在的问题,提醒程序员必须处理该异常,或者该异常由方法调用者来处理,从而避免程序员因为粗心而忘记处理该异常的错误。