异常
- 编译时异常Java程序必须显示处理,否则程序会发生错误,有可能对也有可能不对,由使用者觉得,譬如处理文件找不到的时候,所以这种异常必须立刻在程序中声明处理
- 一个异常要么程序员主动调用异常类处理,要么当jvm发现异常的时候,会自动new出一个相应异常的对象,并调用对象的相应方法。但是jvm遇到异常之后会令程序暂停,程序员捕获之后程序可以继续
1. 异常的特征以及引入原因
-
引入原因:
为了在 用户输入错误,设备错误如断电,物理限制如磁盘满了,代码错误 等情况下是程序返回到一种安全状态并保存用户记录,诞生了 异常类 用于处理上述异常。
在刚开始学习异常的时候,我就会想,为什么不直接用
if else
语句来处理,而非要用复杂的异常类去处理那些简单的譬如数组越界异常。自己直接写个if
语句对索引空间进行限制不就行了么?结果当然可以,但是我认为异常的存在仍然很有必要,原因如下:- 使用异常类会使得程序更加健壮,易于封装
- 事实上,当真正编写程序的时候,异常会变得异常复杂,但靠手动写
if
会很耗费精力,所以调用封装好了的异常类会更便捷 - 而且,有些异常,
if else
根本解决不了 UAU - 既然异常类可以解决所有的异常问题,那么是不是就不用if了?
当然不是!
使用异常类会产生较大的内存消耗,如果异常的频率较高的话就要用if来代替异常类了
2. 常见异常类
注意我们这个地方说的是
异常类
,也就是说,Java中把对异常的处理封住成了类,是类就一定有方法,同时我们也可以自己定义异常类,这个等喝口茶稍后再掰扯。
-
写个程序看一下常见的异常类
try { int a = a / 0; //java.lang.ArithmeticException("by zero") arr = null; //java.lang.ArithmeticException("by zero") arr[0] = arr[10]; //java.lang.ArrayIndexOutOfBoundsException }catch (ArithmeticException e){ //没有finally的情况下,catch之后此方法结束 System.out.println("ArithmeticException"); }catch (ArrayIndexOutOfBoundsException e){ System.out.println("ArrayIndexOutOfBoundsException"); }catch (NullPointerException e) { System.out.println("NullPointerException"); }catch (Exception e) { System.out.println("Exception"); }
-
这里有一个灰常有趣的解释,点击获取哦耶耶耶
-
常用异常方法:
System.out.println(e.getMessage()); //获取异常信息 System.out.println(e); //打印异常类名和异常信息 System.out.println(e.toString()); //和前者一样 e.printStackTrace(); //打印 异常位置, jvm 默认用这种方式处理
3. 异常框架和分类
-
Java中异常类的结构
图片来源:java中异常和处理详解从图中可以看出,Java内置的异常类全部继承于
java.lang
包中的Throwable
。其中我们把Error和RuntimeException及其派生类的 所有异常 叫做 非受查异常 ,其他的所有异常称为 受查异常。
非受查异常,即 编译器 不会去检查的异常,如栈溢出,内存溢出,数组访问越界等。换句话说,这些是完全可以由程序员来避免的异常,需要程序员自己去解决。
受查异常,编译器会在javac的编译过程中核查 是否为所有的受查异常提供了异常处理器 ,如输入,文件查找等有可能发生的异常。换句话说,这些异常程序员是无法处理的,是由程序的使用者而不是编写者导致的。
4. 异常中的关键字
-
try catch
-
try
用于检测异常 -
catch
用于捕获异常 -
语法
try { int a = a / 0; //检测到后,jvm抛出该异常 }catch (ArithmeticException e){ //ArithmeticException e = new ArithmeticException(); //catch捕获到一个相应异常的实例并作为参数传入进去 System.out.println("ArithmeticException"); }
-
特点:
- 如果try语句中抛出一个catch中说明的异常类,那么程序将跳过try语句所在方法的其余代码,执行catch中的处理器代码
- 如果try没有抛出一个catch中说明的异常类,那么不执行catch中的代码
- 如果try抛出了一个catch没有捕获的异常类,那么会交给基类(没有的话由jvm处理)
- catch之后就意味着处理
- catch捕获之后,异常就消失了
- catch中也可以重新抛出新的其他的异常
-
-
throw
用来抛出异常,throws
用来声明异常-
语法:
public void setAge(int age) throws Exception{ //抛出异常,调用此方法时,异常要么被调用者的catch捕获,要么由被调用者再次抛出 if (age > 0) this.age = age; else throw new Exception("滚回火星把"); //抛出了一个匿名对象
-
声明和抛出一般是同时用的
-
throws和throw的区别
- 前者用在方法声明后面,跟的是异常类名,可以跟多个,表示给方法的调用者抛出异常,如果后者是runtimeException就不用抛出
- 后者用在方法体内,跟的是异常对象名,只能抛出一个,它所抛出的异常由前者处理
-
再次抛出与异常链
-
再次抛出异常的例子
try{ access the database; }catch(SQLException e){ logger.log(level,message,e); //记录日志 throw new ServletException("databaseError:" + e.getMessage()); }
-
可以将上述例子包装
try{ access the database; }catch(SQLException e){ Throwable te = new ServletException("databaseError:"); te.initCause(e); //将原来错误作为原因 throw se; }
-
-
-
finally
-
上面说过,当
try
中有异常之后,try中异常之后的代码就会被跳过执行,但是此时异常之后的代码不得不执行,譬如方法中获得了本地资源,并且只有这个方法知道,此时就需要在检测到异常之后对代码进行处理(如数据库的链接)。不管异常是否被捕获,finally里面的代码都会被执行 -
代码例子:
InputStream in = new FileInputStream(); try{ //1 code that have exception //2 } catch(IOException e){ //3 show error message //4 } finally{ //5 in.close; } //6
-
上述例子分析:
- 如果try中没有IOException,那么执行1,2,5,6
- 如果try中抛出了一个IOException
- 如果catch中没有抛出异常,执行1,3,4,5,6
- 如果catch抛出异常,执行1,3,5
- 如果try抛出了一哥未被catch捕获的异常,执行1,5
-
finally常用于关闭资源
-
如果try和finally中都抛出异常,那么在普通的
try,catch,finally
中,finally中的异常将会覆盖try中的本来异常,怎么办?-
法一,嵌套try,代码:
InputStream in = ...; Exception ex = null; try{ try{ code } catch(Exception e){ ex = e; throw e; } } finally{ try{ in.close(); } catch (Exception e){ if(ex == null) //检测try的异常是否被抑制 throw e; } }
-
法二,采用带资源的try语句
try(Scanner in = new Scnner(new FIleInputStream("/words")),"UTF-8"){ while(in.hasNext()) System.out.printf(in.next()); }
前提是资源类中(也就是Scanner类)中实现了AutoCloseable,此接口中包含一个close方法,关闭资源。而且,带资源的try语句会抑制close抛出的异常,从而抛出原来try中的异常
-
-
既然我们说finally中的语句肯定会执行,请看这么一个栗子:
public static int demo1(){ int x = 10; try{ x = 20; int a = 10 / 0; return x; } //try和catch要写两个return catch (ArithmeticException e){ x = 30; System.out.println("OH shit o"); return x; } finally { x = 40; //通常是最后操作的步骤,如关闭IO流,释放资源,几乎不会用赋值操作 //return x; 如果在finally写上return,这个return覆盖刚才的return } } //a == 30 ,return是运行一部分,当30被返回以后,x会被赋值为40,所以x=40不会被返回
-
final, finally, finalize 区别
- final
- 修饰类,不能被继承
- 修饰方法,不能被重写
- 修饰变量,只能被赋值一次
- finally是try语句中的
- finalize是object的一个方法,由对象垃圾回收器调用此方法
- final
-
-
关于捕获,声明,抛出的注意事项
-
因为捕获异常需要消耗很长时间,所以一个方法需要
throws
所有可能的受查异常,而非受查异常要么不可控制,即ErrorException
,需要被throws
,要么应该避免发生,即RuntimeException
,换句话书,运行时异常,不应该被声明,而需要被程序员自己修正,如下:class a{ void b(int i) throws ArrayIndexOutBoundsException //bad style! {...} }
-
如果派生类覆盖了基类的一个方法,派生类方法中声明的受查异常不能比超类
throws
的受查异常更通用,话句话说,子类只能声明更特殊的异常,或者根本不声明抛出任何异常。如果基类没有抛出任何受查异常,子类也不能抛出任何受查异常。如果基类没有抛出非受查异常,派生类可以抛出非受查异常,但是我们刚才讲过,尽量不要声明非受查异常 -
可以捕获多个异常
-
try不一定非要搭配catch,建议try/catch,try/finally解耦合
-
如果方法抛出了异常,那么这个异常就不可能返回给调用者,如果这个异常一直没有被捕获,那么异常最后将由虚拟机来处理,怎样处理,请看第五点。先观察下第4部分中throw模块的整体可运行代码:
-
第一版,异常未调用者被捕获:
package Exception; public class Demo4_throw { public static void main(String [] args) throws Exception { //此处把异常抛给了虚拟机 person a = new person(); a.setAge(-1); } } class person{ int age; public int getAge() { return age; } public void setAge(int age) throws Exception{ //抛出异常,调用此方法时,异常要么捕获,要么抛出 if (age > 0) this.age = age; else throw new Exception("滚回火星把"); //抛出了一个匿名对象 // throw new RuntimeException("滚回火星把") /如果把此处改为RunTimeException,那么就不用抛出,因为他是运行时异常 } } //输出结果为: Exception in thread "main" java.lang.Exception: 滚回火星把 at Exception.person.setAge(Demo4_throw.java:24) at Exception.Demo4_throw.main(Demo4_throw.java:8)
- 第二版,异常被调用者捕获
package Exception; public class Demo4_throw { public static void main(String [] args) { person a = new person(); try { a.setAge(-1); }catch (Exception e){ System.out.println("程序可以正常运行"); } } } class person{ int age; public int getAge() { return age; } public void setAge(int age) throws Exception{ //抛出异常,调用此方法时,异常要么捕获,要么抛出 if (age > 0) this.age = age; else throw new Exception("滚回火星把"); //抛出了一个匿名对象 // throw new RuntimeException("滚回火星把") /如果把此处改为RunTimeException,那么就不用抛出,因为他是运行时异常 } } //输出结果为: 程序可以正常运行
-
5. JVM遇到异常如何工作
当异常未经过程序员处理时,程序发现异常之后,会先new一个相应异常的对象,先给main函数处理,不行的话会一直上抛给JVM,由JVM调用相应异常对象的方法
printStackTrace()
,然后在控制台打印出出错原因和地点,结束程序。如果程序中有catch关键字,那么在new异常对象之后,对象会传递到catch中,这就是所谓的捕获异常,之后异常就会消失,有catch内部处理,catch可以选择将异常重新抛出
- 为什么捕获异常会较大的性能消耗?
因为构造异常的实例比较耗性能。
- 在JVM的角度来看,构造异常实例时需要生成该异常的栈轨迹。
- 这个操作会逐一访问当前线程的栈帧,
- 记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。
6. 自定义异常
举一个不算恰当的比喻,Throwable在异常类中的地位就像是Object在类中的地位一样。Throwable一共有四个构造方法,正常情况下,所有派生的异常类在习惯上都需要支持一个默认的构造器和带有详细描述信息的构造器。
-
为什么要自定义异常?
- 主要通过自定义异常的名字,用来判断异常的类型
- 查看异常类源码我们可以发现,几乎所有内置的
ThrowableException
的派生类都是调用的基类方法,换句话说,派生类中没有自己实现的方法,全是继承基类的方法。从此我们可以看出,派生类异常的作用主要是通过异常类的名茶去排查异常的类型,我们完全可以自定义类去这样做
-
栗子
public class Demo6_ownException { public static void main(String [] args) throws Exception{ //此处把异常抛给了虚拟机 person1 a = new person1(); a.setAge(-1); } } class person1{ int age; public int getAge() { return age; } public void setAge(int age) throws AgeOutBoundsException{ //抛出异常,调用此方法时,异常要么捕获,要么抛出 if (age > 0) this.age = age; else throw new AgeOutBoundsException("滚回火星把"); //抛出了一个匿名对象 } } class AgeOutBoundsException extends Exception{ public AgeOutBoundsException() { } public AgeOutBoundsException(String message) { super(message); } //因为构造方法不能继承,所以要重写 }
7. Plus
异常捕获消耗的时间很长,能不用就不要用。但需要用的时候一定不能马虎省略。同时这些手段只看不练是起不到什么作用的,当真正在实践中遇到异常,才能体会到这些功能的强大。