异常处理
异常:
对于计算机程序来说,我们不会保证我们写的程序不会出任何问题,就算程序设计没有逻辑错误,那么你能够保证用户会合法按照自己的意愿去输入数据吗?就算用户相当的配合你,你能够保证你在运行程序时不会出现其他问题吗?比如说网络突然中断,或者是电源中断等。
那么我们就要在设计程序时就要考虑到这些问题,保证当程序运行出错时,有相对应的解决方案,而不是直接将异常直接抛到虚拟机导致程序终止运行。
常出现的异常
- IndexOutOfException:一般是数组下标越界异常
int[] arr=new int[10]; System.out.println(arr[20]);
- NullPointException:空指针
String string=null; string.charAt(0);
- NumberFormatException:数值类型转换错误
String string="abc"; Integer a=Integer.parseInt(string);
- ArithmeticException:算术异常
int a=2; int b=0; a=a/b;
- ClassCastException:引用类型转换异常
public class Main { public static void main(String[] args) { String string=""; Animal dog=new Dog(); Cat cat=(Cat)dog; } } class Animal{ } class Dog extends Animal{ } class Cat extends Animal{ }
异常处理机制
方式一:try…catch
try { //执行代码 }catch (Exception e){ //解决方案 }
例如当我们计算1/0时,这是会有算术错误,我们可以捕捉在运算时产生的算术异常
try { int a=2/0; }catch (ArithmeticException e){ e.printStackTrace(); }
这样在遇到算术异常时,我们的catch语句就会捕捉这个异常,进行解决
- 可以采用多个catch块去捕捉多个异常
- 但是要注意,小异常放在上面,大异常放在下面
- 因为大异常是他们的父类,一但它放在上面,其余的小异常就不会被执行
- 所以优先处理小异常,这样才会使结构清晰,处理更加明确
当我们在try语句块中执行某条语句产生异常时,在它之后的代码就不会执行,此时会转到执行捕捉到该异常的catch块中执行相应语句
在try语句块中定义的变量为局部变量,在catch语句块中是无法访问的
方式二:多异常捕获
从Java7开始,一个catch块可以捕获多种多样的异常
- 捕获多种异常时,多种异常类型之间用竖线|隔开
- 捕获多种异常时,异常变量由隐式的final修饰,不能够对该异常变量重新赋值
try { int a=1/0; }catch (ArithmeticException |IndexOutOfBoundsException e){ System.out.println("程序产生了算术异常或数组越界异常"); //e=new Exception();改段代码会报错,因为catch括号里面的变量e被final修饰,不可重新赋值 }catch (Exception e){ e=new Exception(); }
由于多异常捕捉模式的变量是被final修饰的,所以在catch语句块中是无法给变量重新赋值的
方式三:try…catch…finally
当我们在某些时候,在try语句块中会打开一些文件,或者连接数据库等操作,那么一但我们在try语句块中产生了异常,那么我们之后的代码就不会执行,导致文件和数据库等不能及时正确关闭。
- Java的垃圾回收机制是不会回收任何物理资源,只会回收堆内存中对象所占用的内存
为了保证可以及时回收try里面打开的物理资源,Java异常处理机制提供了finally语句块,这个语句块是在执行完try和catch语句块后,必须要执行的一个代码块,如果在try或者catch中return语句,也不会影响finally的执行,执行完finally后,再去执行return,如果finally中有return语句,那么try和catch语句块中的return就不会被执行,但是有一点如果在try中的是System.exit(1),就不会去执行finally语句块了,因为这样会导致直接退出虚拟机。
FileWriter file=null; try{ file=new FileWriter("text.txt"); }catch (IOException e){ e.printStackTrace(); }finally { if(file!=null){ try { file.close(); } catch (IOException e) { e.printStackTrace(); } } }
这样一但try块出现异常,该代码依然会执行finally中的close方法将文件关闭
Java7、9增强的自动关闭资源的try语句
当我们在finally中执行关闭资源的代码时,程序显得很臃肿
Java7:
- Java7增强了try语句的功能,它允许我们在try后面紧跟一对括号,括号里面可以声明定义一些资源
- 当我们执行完try语句后,程序会自动帮我们关闭资源
- 这些资源必须实现AutoCloseable接口或者是Closeable接口的close方法
- Close接口是AutoCloseable的子接口
try (FileWriter file=new FileWriter("text.txt")) { file.write(""); }catch(Exception e){ e.printStackTrace(); }
Java9:
- Java9进一步增强该操作,不需要再括号内声明定义了
- 可以在try语句外面定义
- 但是该资源变量必须是final或者是有效final就是始终没有被赋过其他值
FileWriter file1=new FileWriter("text.txt"); FileWriter file2=new FileWriter("text.txt"); try(file1;file2){ file.write(""); }catch(Exception e){ e.printStackTrace(); }
Checked异常和Runtime异常
对于Java中的异常可分为两大类:
一类是Check异常(编译时异常):
一类是Runtime异常(运行时异常):
所有的RuntimeException类及其子类的异常都是运行时异常,其余是编译时异常
运行时异常是指我们程序在运行后才会出现的异常,这类异常不影响编译,只是在运行过程会发生错误
但是编译时异常,我们必须去显示的处理该异常,否则我们不能通过编译,例如当我们在调用多线程中的sleep方法时,
我们需要用try…catch包住才可以使用,否则就会报错,因为该方法在定义时,抛出了编译异常,需要我们显式处理
使用throws抛出异常
- 当我们有时不知道如何去处理该异常时,我们就需要将产生的异常对象向上抛
- 方法会抛给main方法,如果在main方法中没有得到解决,就会继续向上抛
- 抛给Java虚拟机,打印异常跟踪栈信息,并终止程序运行
- 这也就是为什么之前我们运行的程序在运行失败后程序会终止,而且在控制台打印一些红色的错误
public static void main(String[] args) throws IOException { test(); } public static void test() throws IOException { int a=2/0; }
public static void main(String[] args){ try { test(); } catch (IOException e) { e.printStackTrace(); } } public static void test() throws IOException { int a=2/0; } }
注意:
- 但我们调用抛出异常的方法时,这个方法要么在try…catch语句块中
- 要么在另一个带throws的方法中
- 因为既然方法抛出了异常就要去捕捉进行解决
方法重写时抛异常注意事项
当我们的子类继承父类时,如果父类的方法抛出了异常
子类重写该方法的时候抛出的异常不得大过父类抛出的异常
class A{ public void test() throws ArithmeticException{ } } class B extends A{ @Override public void test() throws Exception { } }
上述代码会报错,因为子类B抛出的是Exception异常大过父类抛出的异常
使用throw抛出异常
在某些情景下,不一定是真正的异常,只要是不符合我们预期的想法就是异常
当不符合预期时,我们可以手动去抛出异常
public static void main(String[] args){ int a=-1; try { if(a<0){ throw new Exception("a为负数"); } }catch (Exception e){ e.printStackTrace(); } }
对于Check异常我们要么显式捕捉处理,要么继续抛
而Runnable异常,我们既可以显式捕捉,也可以不予理会
自定义异常类
因为Java定义的异常有限,有时并不会满足我们的需求,这时我们就需要自己去定义一个异常类,实现我们所需要的功能
- 首先要继承Exception类或者是RuntimeException等基类
- 然后定义两个构造器
- 有参构造器通常传入的是错误信息
class MyException extends Exception{ public MyException(){ } public MyException(String msg){ super(msg); } }
总结
- 异常只是应用于处理非正常的情况,不要过度使用异常来代替正常的流程控制
- 对于一些可预知的错误,程序员完全有能力提供相应代码去处理,而不是去抛异常
例如:
int a[]=new int[10]; for(int i=0;i<100;i++){ try { System.out.println(a[i]); }catch (IndexOutOfBoundsException e){ e.printStackTrace(); } }
- 这种情况程序员是可以预料到的,完全有能力利用代码去避免这种错误的发生
而不是一味的去抛异常再去解决 - 这种抛异常虽然简单,但是Java运行时捕捉到异常后,还要进入catch块
这样会降低运行效率 - 异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,
绝不要使用异常处理来代替正常的业务逻辑判断