Java基础:异常机制
异常概述
在Java程序运行的过程中,所有发生不正常的情况我们统称为异常。(开发过程中的语法错误和逻辑错误不叫异常)异常的出现会打断我们程序的执行过程,一旦异常出现,异常代码处的后续代码将不再执行(未处理的情况下),因此在程序的设计过程中我们需要尽量避免并且妥善的处理异常发生的情况。Java异常处理机制也秉承着Java面向对象的设计思想,所有的异常都是以类的形式存在的(可以参考下文的异常体系结构),除了内置的异常外我们还可以手动的自定义异常。
异常的分类
在Java语言体系中,异常可以分为两大类
1、java.lang.Error: Java虚拟机无法解决的严重异常,例如jvm系统内部错误,资源耗尽等情况。一般我们不会设计相应的代码去捕捉这种异常,原因在于即使捕捉到了也无法给出相应的处理。
public class ErrorTest { public static void main(String[] args) { main(args);//java.lang.StaticOverflowError(栈溢出异常) } }
2、java.lang.Exception:因编程错误或者偶然因素导致的一般性问题,比如空指针异常,数组角标越界异常。对于这些异常我们可以轻易的捕捉并且进行处理
public class ExceptionTest { public static void main(String[] args) { int[] a = new int[5]; a[5] = 10; System.out.println("a[5] = " + a[5]);//java.lang.ArrayIndexOutOfBoundsException(数组角标越界) } }
无论是Error异常还是Exception异常都继承了java.lang.Throwable类
编译时异常和运行时异常(都属于Exception异常)
Java程序在执行的过程中分为两个过程,一是先将源文件编译成.class字节码文件,二是在内存中加载,运行字节码文件。在编译的过程中发生的异常我们称之为编译时异常,在运行时发生的异常我们称之为运行时异常。对于编译时异常我们也可以称之为受检(checked)异常,对于运行时异常我们称之为非受检(unckecked)异常。无论是编译时异常还是运行时异常都是属于Exception异常。看下面的异常体系结构图,对于蓝色的就是运行时异常,红色的是编译时异常。
异常类的继承体系
常见的运行时异常
1、数组角标越界
public class ExceptionTest { public static void main(String[] args) { int[] a = new int[5]; a[5] = 10; System.out.println("a[5] = " + a[5]);//java.lang.ArrayIndexOutOfBoundsException(数组角标越界) } }
2、类型转换异常
public class ExceptionTest { public static void main(String[] args) { Object object = new Date(); //Exception in thread "main" java.lang.ClassCastException: java.util.Date cannot be cast to java.lang.String String str = (String) object; } }
3、空指针异常
public class ExceptionTest { public static void main(String[] args) { int[] a = null; //Exception in thread "main" java.lang.NullPointerException System.out.println("a[2] = " + a[2]); } }
其他的还有很多,比如NumberFormatException异常、InputMisMacthException异常等....
常见的编译时异常
FileNotFoundException
ClassNotFoundException
SQLException
NoSuchFieldException
NoSuchMethodException
ParseException
异常的处理方式一:try...catch...finally
try...catch..finally是在开发中经常使用的处理异常的方式之一,使用try将可能出现的代码块包裹,一旦出现了异常就会生成一个相应的异常类,该异常类与多个catch中对应的异常类参数进行匹配,如果匹配成功就执行相应的catch代码块中的代码,执行完成就跳出该try..catch结构(不执行其余catch),如果有finally代码块会继续执行finally里面的代码。该种写法的好处是可以比较灵活的运用,同时在使用catch捕获到异常之后我们还可以进行一些其他操作。
try...catch...finally模板
1 try{ 2 //可能出现异常的代码 3 }catch (Exception e){ 4 //捕获到异常之后进行的操作 5 }finally { 6 //一定会执行的代码 7 }
try...catch...finally注意事项
1、try代码块是不能省略的(catch和finally如果想要使用需要搭配try,反之亦然)
2、try代码块不能单独出现,要么与catch代码块搭配(此时可以捕获到出现的异常在catch代码块中进行处理),要么与finally代码块搭配(此时不能捕获异常,但是finally代码块里的内容可以执行)
1 //try...catch搭配 2 try{ 3 int a = 10 / 0; 4 System.out.println("a = " + a); 5 }catch (Exception e){ 6 System.out.println("代码出现异常"); 7 } 8 输出结果: 9 代码出现异常 10 11 /**************************************/ 12 13 //try...finally搭配 14 try { 15 int a = 10 / 0; 16 }finally { 17 System.out.println("一定会执行的代码"); 18 } 19 输出结果: 20 一定会执行的代码 21 Exception in thread "main" java.lang.ArithmeticException: / by zero 22 at com.cangfengwork.exercise.throwable.ExceptionTest.main(ExceptionTest.java:21)
3、catch代码块可以出现多次,但是捕获的异常大小需要从小到大,简单来说就是前面的catch捕获的异常类需要是后面catch捕获的异常类的子类(如果前后没有子父类关系则不要求顺序)
1 //try搭配多个catch 2 try{ 3 int a = 10 / 0; 4 System.out.println("a = " + a); 5 }catch (ArithmeticException e){ 6 System.out.println("出现了ArithmeticException异常"); 7 }catch (Exception e){ 8 System.out.println("出现了异常"); 9 } 10 输出结果: 11 出现了ArithmeticException异常 12 13 /*****************************************************/ 14 //try搭配多个catch 15 try{ 16 int a = 10 / 0; 17 System.out.println("a = " + a); 18 }catch (Exception e){ 19 System.out.println("出现了ArithmeticException异常"); 20 }catch (ArithmeticException e){ 21 /*此处编译报错,因为ArithmeticException异常是Exception异常的子类,应放在前面*/ 22 System.out.println("出现了异常"); 23 }
4、在try代码块中声明的变量一旦出了该代码块则不能被调用
1 try{ 2 int a = 10 / 0; 3 System.out.println("a = " + a); 4 }catch (Exception e){ 5 System.out.println("出现了ArithmeticException异常"); 6 } 7 System.out.println("a = " + a);//编译报错:Cannot resolve symbol 'a'
5、finally代码块中的内容一定会被执行,如果一个方法需要返回值且finally中写了return,则返回结果一定是finally中的结果
1 public static void main(String[] args) { 2 int a = test(); 3 System.out.println("a = " + a); 4 } 5 6 public static int test(){ 7 try{ 8 System.out.println("try..........."); 9 int a = 10 / 0; 10 return 1; 11 }catch (Exception e){ 12 System.out.println("catch..........."); 13 return 2; 14 }finally { 15 System.out.println("finally..........."); 16 return 3; 17 } 18 } 19 20 输出结果: 21 try........... 22 catch........... 23 finally........... 24 a = 3
6、try...catch...finally是可以嵌套使用的(可能我们在catch中也可能会出现异常)
1 public static void main(String[] args) { 2 int a = test(); 3 System.out.println("a = " + a); 4 } 5 6 public static int test(){ 7 try{ 8 System.out.println("try..........."); 9 int a = 10 / 0; 10 return 1; 11 }catch (Exception e){ 12 System.out.println("catch..........."); 13 14 try{ 15 System.out.println("内部try............"); 16 int[] a = new int[10]; 17 System.out.println("a[10] = " + a[10]); 18 return 2; 19 }catch (ArrayIndexOutOfBoundsException e1){ 20 System.out.println("内部catch......."); 21 // e1.printStackTrace();//打印异常 22 return 4; 23 }finally { 24 System.out.println("内部finally......."); 25 return 5; 26 } 27 }finally { 28 System.out.println("finally..........."); 29 return 3; 30 } 31 } 32 33 输出结果: 34 try........... 35 catch........... 36 内部try............ 37 内部catch....... 38 内部finally....... 39 finally........... 40 a = 3
7、在日常开发中,我们通常使用try...catch...finally来处理编译时异常,但是无法解决运行时异常(通常运行时异常需要我们修改代码),换句话说,我们可以使用try...catch...finally将编译时异常延时到运行时出现
异常处理方式二:throws + 异常类型
该方法是异常处理的第二种方法,也是Java程序默认的异常处理机制。通常写在方法的声明处,指出该方法在执行时可能会出现的异常类型。同样的,当方法体内出现了异常代码,会在异常代码处生成一个对应的异常类对象,一旦满足方法声明处的异常类型就会被抛出。而异常代码处后续的代码不再执行。
1 public static int test() throws FileNotFoundException { 2 File file = new File("hello.txt"); 3 FileInputStream fileInputStream = new FileInputStream(file);//一旦此处出现异常,下面的输出语句将不再执行 4 System.out.println("后续代码.........."); 5 return 1; 6 }
与try...catch...finally处理异常的方式对比,throws并没有真正的将异常处理掉,仅仅是将异常抛给方法的调用者,由方法调用者使用try...catch解决异常或者继续抛给上方的方法调用者
当子类重写父类的方法时,子类所抛出的异常范围应当不大于比父类抛出异常范围
在子类重写父类的方法时,该方法可能会抛出异常,这个时候子类要么使用try...catch将异常进行处理,要么将异常抛给方法调用者,此时子类重写的方法抛出的异常范围应当不大于比父类抛出的异常范围
1 public class ExceptionTest { 2 public static void main(String[] args) { 3 ExceptionTest exceptionTest = new ExceptionTest(); 4 exceptionTest.test(new Sub()); 5 } 6 7 public void test(Super s){ 8 try { 9 s.superMethod(); 10 } catch (IOException e) { 11 e.printStackTrace(); 12 } 13 } 14 15 } 16 17 class Super{ 18 public void superMethod() throws IOException { 19 System.out.println("superMethod......"); 20 } 21 } 22 23 class Sub extends Super{ 24 @Override 25 public void superMethod(){ 26 27 System.out.println("subMethod......"); 28 } 29 30 }
原因:看上述代码我们可以知道,在test方法中我们对父类中的方法出现的异常进行了处理,在main方法中我们使用了多态传递了一个子类的对象,如果此时子类中重写的方法所抛出的异常类型为Exception(也就是说比父类方法抛出的异常范围大)那么很显然,在test方法中无法匹配到catch代码块,异常也就无法进行处理
当然,在开发工具中,我们如果不遵守这个规则,编译的时候会不通过
1 class Sub extends Super{ 2 @Override 3 public void superMethod() throws Exception{//此时编译不通过:'superMethod()' in 'com.cangfengwork.exercise.throwable.Sub' clashes with 'superMethod()' in 'com.cangfengwork.exercise.throwable.Super'; overridden method does not throw 'java.lang.Exception' 4 5 System.out.println("subMethod......"); 6 } 7 8 }
在开发中,如果父类中的方法没有使用throws抛出异常,那么子类重写的方法也不能使用throws,只能使用try...catch来处理异常
手动抛出异常(throw的用法)
在上文的介绍中,无论是1try...catch还是throws都是处理异常的方法,而throw是手动的生成异常的方法。throw和throws是完全的两个概念,一定不要被他们相似的外表迷惑!!!
1 public class ThrowTest { 2 public static void main(String[] args) { 3 List<Integer> list = new ArrayList<>(); 4 test(list); 5 } 6 7 public static void test(List<Integer> flourList) { 8 /** 9 * 输入十个数字,范围在0~100之间,并将他们存到集合中,如果输入错误将抛出异常!! 10 */ 11 Scanner scanner = new Scanner(System.in); 12 for (int i = 0; i < 10; i++) { 13 int a = scanner.nextInt(); 14 if (a < 10 && a > 0){ 15 flourList.add(a); 16 }else { 17 throw new RuntimeException("第【"+(i+1)+"】个数背错了,准备挨打!!!"); 18 } 19 } 20 } 21 }
输出结果:
1 1 2 1 -1 2 3 4 -1 -4 -10 2 Exception in thread "main" java.lang.RuntimeException: 第【4】个数背错了,准备挨打!!! 3 at com.cangfengwork.exercise.throwable.ThrowTest.test(ThrowTest.java:28) 4 at com.cangfengwork.exercise.throwable.ThrowTest.main(ThrowTest.java:15)
throw和throws的区别
throws是处理异常的方法,它写在方法的声明处,用来将方法体内部可能出现的异常抛出,而throw是手动创建异常对象的方法,它写在方法体内部的任意位置。一般来说,throw后面跟的是运行时异常类的对象(不做要求,可以跟其他异常类对象),而throws后面跟的是Exception异常类的对象。
自定义异常类
在开发的过程中,我们可能会遇到各种各样的异常(此处的异常泛指不符合我们要求的数据,比如要求输入正整数但是用户输入了负数),而Java并没有提供相应的异常类供我们使用,这个时候我们就可以自定义一个异常类来处理类似的情况。
定义自定义异常类:
1 public class MyException extends RuntimeException{ 2 //提供序列号,一般来说,继承的类有两种,一种是Exception,一种是RunTimeException。继承Exception需要我们使用异常处理的两种方式进行处理,而RunTimeException则不需要 3 static final long serialVersionUID = -7034897190745766939L; 4 5 //提供无参构造器 6 public MyException(){ 7 8 } 9 //提供带参数的构造器 10 public MyException(String message){ 11 super(message);//使用父类的带参构造器 12 } 13 }
使用:
1 public static void test(List<Integer> flourList) { 2 /** 3 * 输入十个数字,范围在0~100之间,并将他们存到集合中,如果输入错误将抛出异常!! 4 */ 5 Scanner scanner = new Scanner(System.in); 6 int a = 0; 7 for (int i = 0; i < 10; i++) { 8 a = scanner.nextInt(); 9 if (a < 10 && a > 0){ 10 flourList.add(a); 11 }else { 12 throw new MyException("第【"+(i+1)+"】个数背错了,准备挨打!!!"); 13 } 14 } 15 System.out.println("a = " + a); 16 }