写点什么

详解 Java 中的异常和处理时间

作者:Java-fenn
  • 2022 年 9 月 14 日
    湖南
  • 本文字数:8053 字

    阅读完需:约 26 分钟

主题 Java 简介程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用 C 语言风格:用函数返回值作为执行状态?。


Java 提供了更加优秀的解决办法:异常处理机制。


异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。


Java 中的异常可以是函数中的语句执行时引发的,也可以是程序员通过 throw 语句手动抛出的,只要在 Java 程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE 就会试图寻找异常处理程序来处理异常。


Throwable 类是 Java 异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK 中内建了一些常用的异常类,我们也可以自定义异常。


Java 异常的分类和类结构图 Java 标准裤内建了一些通用的异常,这些类以 Throwable 为顶层父类。


Throwable 又派生出 Error 类和 Exception 类。


错误:Error 类以及他的子类的实例,代表了 JVM 本身的错误。错误不能被程序员通过代码处理,Error 很少出现。因此,程序员应该关注 Exception 为父类的分支下的各种异常类。


异常:Exception 以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被 Java 异常处理机制使用,是异常处理的核心。


总体上我们根据 Javac 对异常的处理要求,将异常类分为 2 类。


非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac 在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用 try...catch...finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除 0 错误 ArithmeticException,错误的强制类型转换错误 ClassCastException,数组索引越界 ArrayIndexOutOfBoundsException,使用了空对象 NullPointerException 等等。


检查异常(checked exception):除了 Error 和 RuntimeException 的其它异常。javac 强制要求程序员为这样的异常做预备处理工作(使用 try...catch...finally 或者 throws)。在方法中要么用 try-catch 语句捕获它并处理,要么用 throws 子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如 SQLException , IOException,ClassNotFoundException 等。


需要明确的是:检查和非检查是对于 javac 来说的,这样就很好理解和区分了。


初识异常下面的代码会演示 2 个异常类型:ArithmeticException 和 InputMismatchException。前者由于整数除 0 引发,后者是输入的数据不能被转换为 int 类型引发。


package com.example; import java. util .Scanner ; public class AllDemo{ public static void main (String [] args ){System . out. println( "----欢迎使用命令行除法计算器----" ) ;CMDCalculate ();} public static void CMDCalculate (){Scanner scan = new Scanner ( System. in ); int num1 = scan .nextInt () ; int num2 = scan .nextInt () ; int result = devide (num1 , num2 ) ;System . out. println( "result:" + result) ;scan .close () ;} public static int devide (int num1, int num2 ){ return num1 / num2 ;}} /*****************************************


----欢迎使用命令行除法计算器----20Exception in thread "main" java.lang.ArithmeticException : / by zeroat com.example.AllDemo.devide( AllDemo.java:30 )at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )at com.example.AllDemo.main( AllDemo.java:12 )


----欢迎使用命令行除法计算器----1rException in thread "main" java.util.InputMismatchExceptionat java.util.Scanner.throwFor( Scanner.java:864 )at java.util.Scanner.next( Scanner.java:1485 )at java.util.Scanner.nextInt( Scanner.java:2117 )at java.util.Scanner.nextInt( Scanner.java:2076 )at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )at com.example.AllDemo.main( AllDemo.java:12 )*****************************************/异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的 caller 都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了 异常追踪栈 。


异常最先发生的地方,叫做 异常抛出点 。


从上面的例子可以看出,当 devide 函数发生除 0 异常时,devide 函数将抛出 ArithmeticException 异常,因此调用他的 CMDCalculate 函数也无法正常完成,因此也发送异常,而 CMDCalculate 的 caller——main 因为 CMDCalculate 抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的 caller 中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由 main 函数抛给 JRE,导致程序终止。


上面的代码不使用异常处理机制,也可以顺利编译,因为 2 个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。


代码中我选择使用 throws 声明异常,让函数的调用者去处理可能发生的异常。但是为什么只 throws 了 IOException 呢?因为 FileNotFoundException 是 IOException 的子类,在处理范围内。


@Test public void testException() throws IOException{ //FileInputStream 的构造函数会抛出 FileNotFoundExceptionFileInputStream fileIn = new FileInputStream("E:\a.txt"); int word; //read 方法会抛出 IOExceptionwhile((word = fileIn.read())!=-1){System.out.print((char)word);} //close 方法会抛出 IOExceptionfileIn.clos}异常处理的基本语法在编写代码处理异常时,对于检查异常,有 2 种不同的处理方式:使用 try...catch...finally 语句块处理它。或者,在函数签名中使用 throws 声明交给函数调用者 caller 去解决。


try...catch...finally 语句块 try{ //try 块中放可能发生异常的代码。//如果执行完 try 且不发生异常,则接着去执行 finally 块和 finally 后面的代码(如果有的话)。//如果发生异常,则尝试去匹配 catch 块。}catch(SQLException SQLexception){ //每一个 catch 块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7 中可以将多个异常声明在一个 catch 中。 //catch 后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个 catch 块来处理异常。 //在 catch 块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个 catch 块中的局部变量,其它块不能访问。 //如果当前 try 块中发生的异常在后续的所有 catch 中都没捕获到,则先去执行 finally,然后到这个函数的外部 caller 中去匹配异常处理器。//如果 try 中没有发生异常,则所有的 catch 块将被忽略。}catch(Exception exception){//...}finally{


//finally块通常是可选的。
复制代码


//无论异常是否发生,异常是否匹配被处理,finally 都会执行。 //一个 try 至少要有一个 catch 块,否则, 至少要有 1 个 finally 块。但是 finally 不是用来处理异常的,finally 不会捕获异常。 //finally 主要做一些清理工作,如流的关闭,数据库连接的关闭等。}需要注意的地方 1、try 块中的局部变量和 catch 块中的局部变量(包括异常变量),以及 finally 中的局部变量,他们之间不可共享使用。


2、每一个 catch 块用于处理一个异常。异常匹配是按照 catch 块的顺序从上往下寻找的,只有第一个匹配的 catch 会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个 catch 块都有存在的意义。


3、java 中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理 catch 代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的 catch 代码块”后面接着执行。


有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做: resumption model of exception handling (恢复式异常处理模式 )


而 Java 则是让执行流恢复到处理了异常的 catch 块后接着执行,这种策略叫做: termination model of exception handling (终结式异常处理模式)


public static void main(String[] args){ try {foo();}catch(ArithmeticException ae) {System.out.println("处理异常");}} public static void foo(){ int a = 5/0; //异常抛出点 System.out.println("为什么还不给我涨工资!!!"); //////////////////////不会执行}throws 函数声明 throws 声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则 javac 保证你必须在方法的签名上使用 throws 关键字声明这些可能抛出的异常,否则编译不通过。


throws 是另一种处理异常的方式,它不同于 try...catch...finally,throws 仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。


采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。


public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN{//foo 内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。}finally 块 finally 块不管异常是否发生,只要对应的 try 执行了,则它一定也执行。只有一种方法让 finally 块不执行:System.exit()。因此 finally 块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。


良好的编程习惯是:在 try 块中打开资源,在 finally 块中清理释放这些资源。


需要注意的地方:1、finally 块没有处理异常的能力。处理异常的只能是 catch 块。


2、在同一 try...catch...finally 块中 ,如果 try 中抛出异常,且有匹配的 catch 块,则先执行 catch 块,再执行 finally 块。如果没有 catch 块匹配,则先执行 finally,然后去外面的调用者中寻找合适的 catch 块。


3、在同一 try...catch...finally 块中 ,try 发生异常,且匹配的 catch 块中处理异常时也抛出异常,那么后面的 finally 也会执行:首先执行 finally 块,然后去外围调用者中寻找合适的 catch 块。


这是正常的情况,但是也有特例。关于 finally 有很多恶心,偏、怪、难的问题,我在本文最后统一介绍了,电梯速达->:​ ​finally 块和 return​ ​


throw 异常抛出语句 throw exceptionObject


程序员也可以通过 throw 语句手动显式的抛出一个异常。throw 语句的后面必须是一个异常对象。


throw 语句必须写在函数中,执行 throw 语句的地方就是一个异常抛出点,它和由 JRE 自动形成的异常抛出点没有任何差别。


public void save(User user){ if(user == null) throw new IllegalArgumentException("User 对象为空"); //......}异常的链化在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设 B 模块完成自己的逻辑需要调用 A 模块的方法,如果 A 模块发生异常,则 B 也将不能完成而发生异常,但是 B 在抛出异常时,会将 A 的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。


异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带 Throwable 参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。


查看 Throwable 类源码,可以发现里面有一个 Throwable 字段 cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。


public class Throwable implements Serializable { private Throwable cause = this; public Throwable(String message, Throwable cause){fillInStackTrace();detailMessage = message; this.cause = cause;} public Throwable(Throwable cause){fillInStackTrace();detailMessage = (cause==null ? null : cause.toString()); this.cause = cause;} //........}下面是一个例子,演示了异常的链化:从命令行输入 2 个 int,将他们相加,输出。输入的数不是 int,则导致 getInputNumbers 异常,从而导致 add 函数异常,则可以在 add 函数中抛出


一个链化的异常。


public static void main(String[] args){


System.out.println("请输入2个加数"); int result; try {    result = add();    System.out.println("结果:"+result);} catch (Exception e){    e.printStackTrace();}
复制代码


} //获取输入的 2 个整数返回 private static List<Integer> getInputNumbers(){List<Integer> nums = new ArrayList<>();Scanner scan = new Scanner(System.in); try { int num1 = scan.nextInt(); int num2 = scan.nextInt();nums.add(new Integer(num1));nums.add(new Integer(num2));}catch(InputMismatchException immExp){ throw immExp;}finally {scan.close();} return nums;} //执行加法计算 private static int add() throws Exception{ int result; try {List<Integer> nums =getInputNumbers();result = nums.get(0) + nums.get(1);}catch(InputMismatchException immExp){ throw new Exception("计算失败",immExp); /////////////////////////////链化:以一个异常对象为参数构造新的异常对象。} return result;}


/*请输入 2 个加数 r 1java.lang.Exception: 计算失败 at practise.ExceptionTest.add(ExceptionTest.java:53)at practise.ExceptionTest.main(ExceptionTest.java:18)Caused by: java.util.InputMismatchExceptionat java.util.Scanner.throwFor(Scanner.java:864)at java.util.Scanner.next(Scanner.java:1485)at java.util.Scanner.nextInt(Scanner.java:2117)at java.util.Scanner.nextInt(Scanner.java:2076)at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)at practise.ExceptionTest.add(ExceptionTest.java:48)... 1 more


*/


自定义异常如果要自定义异常类,则扩展 Exception 类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自 RuntimeException。


按照国际惯例,自定义的异常应该总是包含如下的构造函数:


一个无参构造函数一个带有 String 参数的构造函数,并传递给父类的构造函数。一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数一个带有 Throwable 参数的构造函数,并传递给父类的构造函数。下面是 IOException 类的完整源代码,可以借鉴。


public class IOException extends Exception{ static final long serialVersionUID = 7818375828146090155L; public IOException(){ super();} public IOException(String message){ super(message);} public IOException(String message, Throwable cause){ super(message, cause);} public IOException(Throwable cause){ super(cause);}}异常的注意事项 1、当子类重写父类的带有 throws 声明的函数时,其 throws 声明的异常必须在父类异常的可控范围内——用于处理父类的 throws 方法的异常处理器,必须也适用于子类的这个带 throws 方法 。这是为了支持多态。


例如,父类方法 throws 的是 2 个异常,子类就不能 throws 3 个及以上的异常。父类 throws IOException,子类就必须 throws IOException 或者 IOException 的子类。


至于为什么?我想,也许下面的例子可以说明。


2、Java 程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。


也就是说,Java 中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。


finally 块和 return 首先一个不容易理解的事实:在 try 块中即便有 return,break,continue 等改变执行流的语句,finally 也会执行。


public static void main(String[] args){int re = bar();System.out.println(re);}private static int bar(){try{return 5;} finally{System.out.println("finally");}}/*输出:finally5*/很多人面对这个问题时,总是在归纳执行的顺序和规律,不过我觉得还是很难理解。我自己总结了一个方法。用如下 GIF 图说明。


也就是说:try...catch...finally 中的 return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是 0x80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。


finally 中的 return 会覆盖 try 或者 catch 中的返回值。public static void main(String[] args){ int result;


    result = foo();    System.out.println(result); /////////2
复制代码


result = bar();System.out.println(result); /////////2}


@SuppressWarnings("finally") public static int foo(){    trz{ int a = 5 / 0;    } catch (Exception e){ return 1;    } finally{ return 2;    }
}
@SuppressWarnings("finally") public static int bar(){ try { return 1; }finally { return 2; }}
复制代码


finally 中的 return 会抑制(消灭)前面 try 或者 catch 块中的异常 class TestException{ public static void main(String[] args){ int result; try{result = foo();System.out.println(result); //输出 100} catch (Exception e){System.out.println(e.getMessage()); //没有捕获到异常} try{result = bar();System.out.println(result); //输出 100} catch (Exception e){System.out.println(e.getMessage()); //没有捕获到异常}} //catch 中的异常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception{ try { int a = 5/0; return 1;}catch(ArithmeticException amExp) { throw new Exception("我将被忽略,因为下面的 finally 中使用了 return");}finally { return 100;}} //try 中的异常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception{ try { int a = 5/0; return 1;}finally { return 100;}}}finally 中的异常会覆盖(消灭)前面 try 或者 catch 中的异常 class TestException{ public static void main(String[] args){ int result; try{result = foo();} catch (Exception e){System.out.println(e.getMessage()); //输出:我是 finaly 中的 Exception} try{result = bar();} catch (Exception e){System.out.println(e.getMessage()); //输出:我是 finaly 中的 Exception}} //catch 中的异常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception{ try { int a = 5/0; return 1;}catch(ArithmeticException amExp) { throw new Exception("我将被忽略,因为下面的 finally 中抛出了新的异常");}finally { throw new Exception("我是 finaly 中的 Exception");}} //try 中的异常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception{ try { int a = 5/0; return 1;}finally { throw new Exception("我是 finaly 中的 Exception");}


}
复制代码


}上面的 3 个例子都异于常人的编码思维,因此我建议:


不要在 fianlly 中使用 return。不要在 finally 中抛出异常。减轻 finally 的任务,不要在 finally 中做一些其它的事情,finally 块仅仅用来释放资源是最合适的。将尽量将所有的 return 写在函数的最后面,而不是 try ... catch ... finally 中。

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
详解Java中的异常和处理时间_Java_Java-fenn_InfoQ写作社区