什么是异常,Java异常详解
我们知道,Java 程序运行首先需要进行编译,将 Java 文件编译成计算机能够识别的字节码文件,这类错误在程序编译时就会暴露出来,会导致程序编译失败。IDE 集成开发环境都会对这种错误进行提示,即我们在编写代码时能看到的语法错误,就叫编译时错误。因为可以即时看到,所以这种错误一般都能避免。
运行时错误在我们编写代码的过程中以及程序编译期间都难以发现,甚至可以正常编译通过,但一旦运行就会报错,这类错误一般不容易发现。编写代码的过程中往往会因为疏忽导致运行时错误的出现,例如数组下标越界,把 0 当作除数等。
Java 是一门面向对象的编程语言,世间万物都可以看作对象,那么同理错误也可以看作一个对象。Java 中有一组类专门来描述各种不同的运行时错误,叫作异常类。
Java 结合异常类提供了处理错误的机制,具体步骤就是当程序出现错误时,会创建一个包含错误信息的异常类的实例化对象,并将该对象提交给系统,由系统转交给能处理该异常的代码进行处理。
Java 将异常分为两类,包括 Error 和 Exception。Error 指系统错误,由 Java 虚拟机生成,我们编写的程序无法处理。Exception 指程序运行期间出现的错误,我们编写的程序可以对其进行处理。
举个生活中的例子来类比 Error 和 Exception。当你骑自行车出去玩时,如果半路自行车链条掉了,我们把链条重新安装就可以继续骑行,这属于自己可以处理的问题,就是 Exception。如果前方的路塌陷了,所有车都无法通过了,这属于我们无法处理的问题,就是 Error。
异常的使用
异常的使用需要用到两个关键字 try 和 catch,并且这两个关键字需要结合起来使用,用 try 来监听可能会抛出异常的代码,一旦捕获到异常,生成异常对象并交给 catch 来处理,基本语法如下:try{ //可能抛出异常 }catch(异常对象){ //处理异常 }举个简单的例子:
- public class Test {
- public static void main(String[] args) {
- try {
- int num = 10/0;
- }catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- }
java.lang.ArithmeticException: / by zero
at Test.main(test.java:4)
如果我们将代码修改为“int num = 10/10;”再次运行,就不会看到异常信息了。因为此时没有发生错误,就不会产生 Exception 对象,catch 代码块不执行。
以上代码是异常最基本的使用,通常除了使用 try 和 catch 关键字,我们还需要用到 finally 关键字,这个关键字有什么作用呢?无论程序是否抛出异常,finally 代码块中的程序都会执行。finally 一般跟在 catch 代码块后面,基本语法如下:
try{ //可能抛出异常 }catch(异常对象){ //处理异常 }finally{ //必须执行的代码 }例如:
- public class Test {
- public static void main(String[] args) {
- try {
- int num = 10/0;
- }catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }finally {
- System.out.println("finally...");
- }
- }
- }
java.lang.ArithmeticException: / by zero
finally...
at Test.main(Test.java:4)
现在我们对代码进行修改,不使用 finally,而在 catch 代码块后面直接执行“System.out.println("finally...");”,代码如下:
- public class Test {
- public static void main(String[] args) {
- try {
- int num = 10/0;
- }catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- System.out.println("finally...");
- }
- }
java.lang.ArithmeticException: / by zero
finally...
at Test.main(Test.java:4)
定义一个带有返回值的方法 test,在该方法中加入 try-catch,例如:
- public class Test {
- public static void main(String[] args) {
- System.out.println(test());
- }
- public static int test() {
- try {
- System.out.println("try...");
- return 10;
- }catch (Exception e) {
- // TODO: handle exception
- }
- System.out.println("finally...");
- return 20;
- }
- }
try...
10
现在对代码进行修改,代码如下:
- public class Test {
- public static void main(String[] args) {
- System.out.println(test());
- }
- public static int test() {
- try {
- System.out.println("try...");
- return 10;
- }catch (Exception e) {
- // TODO: handle exception
- }finally {
- System.out.println("finally...");
- return 20;
- }
- }
- }
try...
finally...
20
异常类
Java 将运行时出现的错误全部封装成类,并且不是一个类,而是一组类。同时这些类之间是有层级关系的,由树状结构一层层向下分级,处在最顶端的类是 Throwable,是所有异常类的根结点。Throwable 有两个直接子类:Error 和 Exception,这两个类前面已经提到了。Error 表示系统错误,程序无法解决;Exception 指程序运行时出现的错误,程序可以处理。Throwable、Error 和 Exception 都存放在 java.lang 包中。Error 常见的子类有 VirtualMachineError、AWTError、IOError。VirtualMachineError 的常见的子类有 StackOverflowError 和 OutOfMemoryError,用来描述内存溢出等系统问题。VirtualMachineError、StackOverflowError 和 OutOfMemoryError 都存放在 java.lang 包中,AWTError 存放在 java.awt 包中,IOError 存放在 java.io 包中。
Exception 常见的子类主要有 IOException 和 RuntimeException,IOException 存放在 java.io 包中,RuntimeException 存放在 java.lang 包中。Exception 类要重点关注,因为这部分异常是需要我们在编写代码的过程中手动进行处理的。
IOException 的常用子类有 FileLockInterruptionException、FileNotFoundException 和 FilerException,这些异常通常都是处理通过 IO 流进行文件传输时发生的错误,在后面 IO 流的章节我们会详细讲解。FileLockInterruptionException 存放在 java.nio.channels 包中,FileNotFoundException 存放在 java.io 包中,FilerException 存放在 javax.annotation.processing 包中。
RuntimeException的常用子类如下。
- ArithmeticException:表示数学运算异常。
- ClassNotFoundException:表示类未定义异常。
- IllegalArgumentException:表示参数格式错误异常。
- ArrayIndexOutOfBoundsException:表示数组下标越界异常。
- NullPointerException:表示空指针异常。
- NoSuchMethodError:表示方法未定义异常。
- NumberFormatException:表示将其他数据类型转为数值类型时的不匹配异常。
它们全部存放在 java.lang 包中。异常类的体系结构如下图所示。

图 1 Java异常类的体系结构
以上我们列举出了实际开发中经常使用到的异常类,还有很多没有列举出来,感兴趣的读者可以自己去查找 API 文档。除了使用 Java 官方提供的异常类,我们也可以根据需求自定义异常类。
throw和throws
throw 和 throws 是 Java 在处理异常时使用的两个关键字,都用来抛出异常,但是使用的方式以及表示的含义完全不同,接下来就带领大家一起来学习二者的区别。Java 中抛出异常有 3 种方式,第 1 种是我们之前介绍过的使用 try-catch 代码块捕获异常。这种方式其实是一种防范机制,即代码中有可能会抛出异常。如果抛出,则捕获并进行处理;如果不抛出,则程序继续向后执行。例如:
- public class Test {
- public static void main(String[] args) {
- String str = "Java";
- try {
- Integer num = Integer.parseInt(str);
- }catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- }
java.lang.NumberFormatException: For input string: "Java"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Integer.parseInt(Integer.java:786)
at Test.main(Test.java:5)
- public class Test {
- public static void main(String[] args) {
- String str = "Java";
- Integer num = Integer.parseInt(str);
- }
- }
Exception in thread "main" java.lang.NumberFormatException: For input string: "Java"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Integer.parseInt(Integer.java:786)
at Test.main(Test.java:4)
答案是 Java 有非常完善的错误处理机制,即使开发者不主动在程序中进行异常捕获,Java 虚拟机也会自动完成异常处理。这相当于员工因为疏忽没有把工作做好,老板会在汇总工作时发现问题并帮员工解决,保证最终的工作成果没有问题。但是很显然这种方式不是很好,不能总让老板去处理错误,员工在做本职工作时应该把所有问题都处理好。
回到代码中也是一样的道理,我们在编写程序时应尽量将异常进行处理,这个工作不要交给 Java 虚拟机去处理。如果我们进行如下修改,程序就不会抛出异常:
- public class Test {
- public static void main(String[] args) {
- String str = "10";
- Integer num = Integer.parseInt(str);
- }
- }
- public class Test {
- public static void main(String[] args) {
- String str = "Java";
- if(str.equals("Java")) {
- throw new NumberFormatException();
- }else {
- int num = Integer.parseInt(str);
- }
- }
- }
Exception in thread "main" java.lang.NumberFormatException
at Test.main(Test.java:5)
那么 throws 是如何抛出异常的呢?try-catch 和 throw 都是作用于具体的逻辑代码,throws 则是作用于方法,用来描述该方法可能会抛出的异常,具体实现如下:
- public class Test {
- public static void main(String[] args) {
- try {
- test();
- } catch (NumberFormatException e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- public static void test() throws NumberFormatException{
- String str = "Java";
- int num = Integer.parseInt(str);
- }
- }
- public class Test {
- public static void main(String[] args) {
- try {
- test();
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- public static void test() throws NumberFormatException{
- String str = "Java";
- int num = Integer.parseInt(str);
- }
- }
我们在前面的章节中介绍过面向对象的三大特征之一的多态,NumberFormatException 是具体的数值类型转换异常,Exception 是所有异常的父类。NumberFormatException 也可以理解成 Exception 的另外一种表现形式,这里我们使用这两类异常都是可以的。同时在调用 test 方法时,可以使用 try-catch 主动捕获,也可以不添加 try-catch,直接交给 Java 虚拟机来处理异常。所以这种情况下,加不加 try-catch 都是可以的,但是建议添加。
既然我们在 catch 中可以使用多态来捕获异常,那么在方法定义时也可以使用多态来描述可能发生的异常,即代码可以做如下修改:
- public class Test {
- public static void main(String[] args) {
- try {
- test();
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- public static void test() throws Exception{
- String str = "Java";
- int num = Integer.parseInt(str);
- }
- }

图 2 报错
如果不添加 try-cath,也可以通过让 main 方法抛出该异常的方式来解决这个错误,代码如下:
- public class Test {
- public static void main(String[] args) throws Exception {
- test();
- }
- public static void test() throws Exception{
- String str = "Java";
- int num = Integer.parseInt(str);
- }
- }
自定义异常类
在实际开发中,我们除了使用 Java 提供的异常类之外,也可以根据需求来自定义异常类,比如定义一个方法,对传入的参数进行 ++ 操作并返回,同时要求参数必须是整数类型,如果传入的参数不是整数类型则抛出自定义异常,具体实现如下:
- public class MyNumberException extends Exception {
- public MyNumberException(String error) {
- super(error);
- }
- }
- public class Test {
- public static void main(String[] args) {
- Test test = new Test();
- try {
- int num = test.add("hello");
- } catch (MyNumberException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- public int add(Object object) throws MyNumberException {
- if(!(object instanceof Integer)) {
- String error = "传入的参数不是整数类型";
- throw new MyNumberException(error);
- }else {
- int num = (int) object;
- return num++;
- }
- }
- }
- public class Test {
- public static void main(String[] args) throws MyNumberException {
- Test test = new Test();
- int num = test.add("hello");
- }
- public int add(Object object) throws MyNumberException {
- if(!(object instanceof Integer)) {
- String error = "传入的参数不是整数类型";
- throw new MyNumberException(error);
- }else {
- int num = (int) object;
- return num++;
- }
- }
- }
推荐使用手动 try-catch 的方式,谁调用谁处理,不要把所有的任务都交给 Java 虚拟机来处理。
这里我们需要注意,Java 中有些异常在 throw 之后,还需要在方法定义处添加 throws 声明,有些异常则不需要,直接 throw 即可。这是因为 Exception 的异常分 checked exception 和 runtime exception,checked exception 表示需要强制去处理的异常,即 throw 异常之后需要立即处理,要么自己 try-catch,要么抛给上一层去处理,否则会报错,例如“Unhandled exception type Exception”。而 runtime exception 没有这个限制,throw 之后可以不处理。
直接继承自 Exception 的类就是 checked exception,继承自 RuntimeException 的类就是 runtime exception。我们自定义的 MyNumberExcpetion 是直接继承 Exception的,所以需要在 add() 方法定义处声明 throws MyNumberExcpetion。