异常处理机制
java中的异常处理机制使得即使程序出现异常,代码也能够继续执行下去而不是直接退出程序。下面我们先来简单的了解一下异常处理是怎么使用。
在引用异常处理之前,代码运行中存在异常会导致JVM直接中断该程序并输出异常信息,如下:
public class ExceptionTry { public static void main(String[] args) { int i = 0; System.out.println(9 / i); System.out.println("程序继续执行"); } }
而在使用异常处理机制之后,我们可以对异常进行简单的处理,应用场景有很多,假如淘宝商品的筛选中价格筛选区间实现是使用如下的代码(目前学习还不够深入,淘宝具体功能的实现可能不是这么简单,只是我是这么理解的):
Scanner scanner = new Scanner(System.in);
double a = scanner.nextDouble();
假如用户玩心大起,或者输入时有误触,输入字符串,这会产生InputMismatchException异常,也就是输入类型无法转换为double型,数据输入类型不匹配,从而导致程序直接中断退出,用户再次使用就只能重新启动,极度影响软件的使用体验。同时,在debug代码中也会带来许多不必要的麻烦。这个时候使用异常处理可以很好的优化使用体验,同时极大的提高了代码的健壮性。还是使用第一个例子:
public class ExceptionTry { public static void main(String[] args) { try { int i = 0; System.out.println(9 / i); } catch (Exception e) { System.out.println("代码出现异常:" + e.getMessage()); } System.out.println("程序继续执行"); } }
这个时候代码是正常运行结束的:
异常和错误的区分
异常(Exception)和错误(Error)是两个完全不同的概念,在我的理解中前者是代码在偶然性或者编程错误等因素导致的一般性问题,可以使用针对性的代码来进行处理的,比如空指针使用异常,算数错误异常(除零,开负数根等等),类型转换错误,数组越界等等;而错误则是更加严重的JVM系统内部错误或者资源耗尽导致程序完全不能执行,比如栈溢出,内存不足等等。通俗来讲就是前者是肚子疼,后者是癌症。
异常中又可以细分为运行时异常和编译时异常,前者是编译器无法检查出的逻辑错误,如上面那个例子中的by zero的除零错误就是运行时异常,而后者是编译器要求必须处理的异常,不然编译过不了,比如在开文件流的时候使用空文件出现的FileNotFundException。
try-catch-finally的使用
try-catch-finally语法了解
了解了异常处理机制的作用接下来我们可以来先看看这个语句是怎么使用的
整体上是很好理解的,try后面的代码是我们认为的可能出现异常的代码,在这个代码块中如果出现异常,JVM就会把出现的异常捕获到catch模块中,然后我们可以在其中进行针对性的操作,比如还是第一个例子,我们可以在catch中输出报错然后要求进行重新输入:
public class ExceptionTry { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int i = 0; try { System.out.println(9 / i); } catch (Exception e) { System.out.println("代码出现异常:" + e.getMessage()); System.out.println("请重新输入i"); i = scanner.nextInt(); } System.out.println("程序继续执行"); } }
我们还可以在外层套循环来保证输入的数据不会出现异常,出现异常则会一直在循环中重复进行输入,甚至之后我们可以自定义异常来使得输入的数据符合我们的要求(异常除了系统自己规定的类以外还可以自定义,毕竟异常本质上也还是一种类)。
在出现异常的情况下,代码是按照try-catch-finally的执行顺序来的,finally可以省略,finally是语句必须执行的内容,一般是在执行一段代码中不管是否发生异常都必须执行其中的业务逻辑的应用场景中,比如不管是否发生异常都需要关闭一些文件流,关闭一些资源等等。catch其实也可以省略,不过这样的话程序异常会和之前一样由JVM来处理:中断程序,输出异常信息。
try-catch-finally的具体使用实例
import java.util.Scanner; public class Try { public static void main(String[] args) { while(true) { try { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个整数"); int a = scanner.nextInt(); System.out.println("a的值为:" + a); break; } catch (Exception e) { System.out.println("输入错误,请重新输入"); } } } }
以上就实现了数据输入时限定只能得到整数,输入异常时还不会退出程序的数据输入。我们还可以在其中添加我们想要具体限定的条件,比如大小,位长等等。
多说一句,如果尝试在循环外部创建Scanner对象,然后内部依旧使用nextInt方法,并在调试时第一次输入异常的话,那么之后就不会再有输入的机会了,会陷入一个死循环,无法实现我们想要的功能。有兴趣的朋友可以看一下。
我们在出现异常的时候是直接跳出代码的执行的,通过断点追踪进去Scanner具体的原码后发现,本来退出被调用方法时为了不影响下一次操作需要对一些参数进行重置的操作,现在因为异常直接跳出导致重置操作无法进行,这样就会对下一次数据的读取造成影响。关于上面那个问题,我发现的是Scanner里面的skipped属性(用来判断分隔符是否被跳过了,应该就是字段是否已经被读取完毕)没有被改回false,所以导致之后的循环中scanner对象的每一次使用skipped属性都无法被修改。(因为skipped每次都是true,就代表字段已经被读取完毕了;被读取的字段token是空,无法进行重置参数的操作;needInput初始就是false,而且只有第一次正常的读取中readInput后也被修改为false,不需要更多的输入,跳过了输入阶段;所以又抛出异常throwFor()退出方法的调用到nextInt(),但是退出时又没有修改skipped属性,所以下一次还是true,就这样进入了一个死循环)具体有问题的源码内容如下:
我们可以通过在循环体内创建对象,这样scanner会在每次调用的时候重新创建从而来进行更新;或者我们可以通过使用Integer.paseInt(scanner.next())来改变数据类型,这样就不会让scanner的属性没有重新初始化遭受异常强制中断了,因为这个时候异常时发生在外部的paseInt中的。
当然严谨来说我其实应该还要具体进去看看getCompleteTokenInBuffer方法的,并且我的解释在自己看来都不是很好,但是这对于我这个小垃圾来说源码看着属实是有点头晕了,看着getCompleteTokenInBuffer的注释大概应该可能是这么回事吧,但是又想具体搞清楚,属于是又菜又爱玩了。之后要是实力有所精进会更加具体详实的搞清楚这个问题。诚心欢迎各位大佬朋友、兄弟姐妹指正我说法里面的错误和不足,拜谢!
异常处理的整体机制
知道try-catch-finally的用法之后我们可以来看看异常处理的整体的处理机制了。我们知道,在使用try-catch-finally之前,异常都是由我们的JVM系统来处理的,JVM在异常处理中有着我们Object类一样的顶级地位,Object是所有类的最终父类,JVM也是所有类的异常处理的最终处理单位。具体是像下图中的结构:
我们的main调用了f1方法,f1方法又调用了f2方法。这里用到了我们的throws了,throws就像是摆烂摸鱼,如果f2中没有显式地处理这个异常,那么f2就会把这个异常抛给调用他的f1,这个异常我不想管,不管了。所以在没有使用try处理前,异常会被一层一层的被抛给最高级的JVM系统,并最终由他来处理这个异常。同时我们可以修改异常类型来选择性的抛弃指定的异常。(将Exception修改为具体的异常类型)
try-catch-finally的使用细节
public class Try { public static void main(String[] args) { System.out.println(a.method()); } } class a{ public static int method() { int i = 0; try { return 9 / i; } catch (Exception e) { System.out.println("catch"); return ++i; } finally { System.out.println("finally"); return ++i; } } }
关于这一段代码的执行顺序,我们只需要明确一个概念就可以迎刃而解了——finally中的代码是必须执行的业务逻辑。在这个语句中try中发生了异常被捕获到了catch中,执行完之后到return ++i;当然是先执行完 ++i 之后发现finally没有被执行,所以就在return之前先去执行finally 的语句,并最终在finally中return。所以最后的结果是2;
如果将catch中的 ++i 换成 i - 8,我们的过程其实没有变化,只不过 i - 8 和 ++i有很大的区别,后者是可以作为一个独立的语句来执行的,而前者没有接收者,所以return i - 8;这里其实在底层先用一个临时变量temp储存了 i - 8 的值,然后再去执行finally的代码。如果finally中的代码没有出口return,就算finally中修改了 i ,最终还是会返回temp的值;如果有,那么会在finally中直接return。
public class Try { public static void main(String[] args) { System.out.println(a.method()); } } class a{ public static int method() { int i = 0; try { return 9 / i; } catch (Exception e) { System.out.println("catch"); return i - 8; } finally { System.out.println("finally"); i++; // return ++i; //返回2 } } }
结果如下:
catch
finally
-8