什么是异常,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); } }使用 throw 是开发者主动抛出异常,即读到 throw 代码就一定会抛出异常,基本语法:“throw new Exception();”,这是一种基于代码的逻辑判断从而主动抛出异常的方式,例如:
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); } }test() 方法在定义时通过 throws 关键字声明了该方法可能会抛出 NumberFormatException 异常,所以我们在调用该方法时,需要手动使用 try-catch 进行捕获。同时 catch 代码块中可以捕获 NumberFormatException,也可以捕获 Exception,这两种方式都是没问题的,例如:
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 都可以呢?因为 Java 的多态特性。
我们在前面的章节中介绍过面向对象的三大特征之一的多态,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); } }但是在这种情况下,main 方法在调用 test 方法时就必须手动进行捕获。这里需要注意,如果方法抛出 RunntimeException 异常或者其子类异常,外部调用该方法时可以不进行 try-catch 捕获。如果方法抛出的是 Exception 异常或者其子类异常,则外部调用时必须进行 try-catch 捕获,否则会报错,如下图所示。
图 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); } }test() 方法声明时会抛出 Exception,主方法中的代码在调用 test() 方法时就需要对异常进行处理,这里选择将异常抛出,那么抛出的异常是交给谁来处理的呢?你一定知道答案的!没错,就是交给 Java 虚拟机来处理了。
自定义异常类
在实际开发中,我们除了使用 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++; } } }add() 方法定义时声明了可能会抛出 MyNumberException 异常,是 Exception 的子类,所以在 main 方法中调用 add() 方法时需要手动进行处理,上述代码我们是通过 try-catch 的方式处理的,同时也可以直接让 main 方法抛出异常,代码如下:
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。