什么是异常
异常是指程序运行可能出现的不能正常继续的情况,也可以理解为程序出现了不在预期范围内的一些情况,都可以称之为异常。
异常的分类
所有的异常类是从java.lang.Exception类继承的子类。Exception类是Throwable类的子类。除了Exception类外,Throwable还有一个子类Error 。Java程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。Error用来指示运行时环境发生的错误。例如,JVM内存溢出。一般地,程序不会从错误中恢复。层次关系图如下
Throwable
Error Exception
other Exception RuntimeException
所以从上图可以看出,异常分为两类:
- 运行时异常:运行的时候才出现的,程序编译期间无法预知,这种情况一般不做捕获。出现运行期异常一般是代码不严谨,需要修改代码。例如下面的例子
/** * Created by lili on 15/11/25. */ public class ExceptionTest { public static void main(String[] args) { int a = 10; int b = 0; System.out.println(a/b); System.out.println("---------"); } }
上述代码就是代码不严谨所致,分母不能为0,所以程序运行至a/b会抛出异常Exception in thread "main" java.lang.ArithmeticException: / by zero
java.lang.Object 继承者 java.lang.Throwable 继承者 java.lang.Exception 继承者 java.lang.RuntimeException 继承者 java.lang.ArithmeticException
查看该异常,发现是运行期异常,所以不能捕获,需要给出处理,例如加一个if判断。
- 非运行时异常:在编译期间就能发现的异常,必须要明确捕获或者抛出,不然程序编译无法通过。例如下面的代码:
/** * Created by lili on 15/11/25. */ public class ExceptionTest { public static void main(String[] args) { String dateStr = "2015-11-22"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date d = sdf.parse(dateStr); } }
上述代码无法通过编译,因为sdf.parse(dateStr)可能出现问题(尽管没问题),所以要做异常处理。
public class ExceptionTest { public static void main(String[] args) { String dateStr = "2015-11-22"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); try { Date d = sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } finally { System.out.println("异常处理后"); } System.out.println("结束在这里"); } }
处理异常的几种方法
- try{语句;}catch(异常名 e){异常处理}catch(异常名 e){异常处理}...finally{结束处理}
上述是异常正面处理的方式,对可能出现异常的语句进行try catch,try包含的语句的原则是尽量要少,try catch可以有多层,例如上面的对日期处理的异常处理可以写成下面这样的多层异常处理,最下面一层处理所有可能出现的异常。
try { Date d = sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); System.out.println("异常处理1"); } catch(Exception e){ e.printStackTrace(); System.out.println("异常处理2"); }finally { System.out.println("异常处理后"); }
也就是说,如果抛出的是异常A,我们处理异常A的父亲异常是可以的。但是,一般建议catch准确的异常。上述多层异常处理的前后关系必须是父亲异常必须在后,即异常范围越大越靠后放,即最后才可能去比较,这也符合精确处理的原则,因为如果将Exception写在最前面,后面的处理都不会执行。最后不管是否有异常需要处理,finally都会执行。
JDK7提供了一种新的异常捕获方式,该方法用户捕获同等级的异常,并且这些异常的处理方法可以有相同的处理方式。try{ }catch (ArithmeticException | ArrayIndexOutOfBoundsException e){ System.out.println("同类异常处理"); }finally { }
try catch执行的逻辑:
在try里面发现问题后,jvm会帮我们生成一个异常对象,然后把这个对象抛出,和catch里面的类进行匹配。
一旦有匹配的,就执行catch里面的处理,然后结束了try...catch 继续执行后面的语句。 - throws:当出现的异常我们没有能力或者权限去处理时,可以在改方法体的()后面去抛出。
public class ExceptionTest { public static void main(String[] args) { String dateStr = "2015-11-22"; try { dateFormat(dateStr); } catch (ParseException e) { e.printStackTrace(); }
System.out.println("这行代码可以执行");
} public static void dateFormat(String str) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse(str);
}
}上述代码展示了如何使用throws,但是调用该方法的地方还是要面临该异常。面临的时候可以选择处理或者继续抛出,但是在main方法里最好不要再抛出,不然就抛给JVM。抛给JVM的话,一旦出现问题,那后面的代码就无法继续执行了。
public class ExceptionTest { public static void main(String[] args) throws ParseException { String dateStr = "2015-11-22"; dateFormat(dateStr); System.out.println("这行代码执行不了"); } public static void dateFormat(String str) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date d = sdf.parse(str); } }
- throw:如果出现了异常情况,我们可以把该异常抛出,这个时候的抛出的应该是异常的对象。
public class ExceptionTest { public static void main(String[] args) { int a = 10; int b = 0; division(a,b); } public static void division(int a,int b){ if(b==0){ throw new ArithmeticException("被除数为0"); } System.out.println(a/b); System.out.println("---------"); } }
上面之所以不会提示处理division,是因为ArithmeticException是运行时异常的子类,所以这是程序有问题,要改善程序。
throws和throw的区别
- throws
用在方法声明后面,跟的是异常类名
可以跟多个异常类名,用逗号隔开
表示抛出异常,由该方法的调用者来处理
throws表示出现异常的一种可能性,并不一定会发生这些异常
2. throw
用在方法体内,跟的是异常对象名
只能抛出一个异常对象名
表示抛出异常,由方法体内的语句处理
throw则是抛出了异常,执行throw则一定抛出了某种异常
finally关键字:如果执行finally之前JVM意外退出,finally也有可能执行不了
finally一般用在try catch中用来做最后的一些收尾工作,如释放资源,在IO操作和数据库操作中用的比较多。但是如果在catch中有return语句,finally是否会执行呢?
public class ExceptionTest { public static void main(String[] args) { int a = 10; System.out.println("return a:"+ test(a)); } public static int test(int a) { try{ a = 10; int b = a / 0; } catch (Exception e){ a = 20; return a; } finally { a = 30; System.out.println("finally a:" + a); } a = 40; return a; } }
上面这段代码返回的是
finally a:30
return a:20
这说明,就算有返回语句,finally还是执行了,但是此时返回的a应该是30才对啊,观察debug发现,是执行到return把return语句的a替换为20后发现还有finally才去执行finally的,此时返回值已经确定。所以,finally中的语句执行在return中间;但是如果将代码改为下面这样,则返回的a也为30;
public class ExceptionTest { public static void main(String[] args) { Student s = new Student(0); System.out.println("return a:" + test(s).age); } public static Student test(Student s) { try { s.age = 10; int b = s.age / 0; } catch (Exception e) { s.age = 20; return s; } finally { s.age = 30; System.out.println("finally a:" + s.age); } s.age = 40; return s; } }
如何自定义异常
上面就提到了,异常分为两类,一类是运行期异常,一类是非运行期异常,所以自定义异常类一般是继承自这两类异常。以Exception为例进行分析。
Exception类中出了5个构造方法,啥方法都没有。
public Exception(){} public Exception(String message) { super(message); } public Exception(String message, Throwable cause) { super(message, cause); } public Exception(Throwable cause) { super(cause); } protected Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); }
这里就要去了解出现异常是打印的信息从何而来?前面讲到throw的时候就可以看出,创建一个Exception对象并throw。try catch中是自动创建了一个异常对象去对比,类型一致(或者可以向上转型)则执行catch。所以打印信息应该是new对象的时候传入的,也有默认的打印(空构造时)。
所以我们自定义异常时,出了要继承两类异常外,还需要至少有两个构造的方法,一个空构造,一个非空构造,这样可以自定义异常输出。
下面是一个自定义的异常类和测试用例:
public class ExceptionTest { public static void main(String[] args){ int[] ageArr = new int[]{1,2,3,-1,130}; for(int a : ageArr){ try { ageCheck(a); } catch (AgeException e) { e.printStackTrace(); } } } public static void ageCheck(int age) throws AgeException { if(age > 120){ throw new AgeException("年龄超过120岁."); } if(age < 0){ throw new AgeException("年龄小于0岁."); } } } class AgeException extends Exception { public AgeException() { } public AgeException(String message) { super(message); } }
异常注意事项
1. 子类重写父类方法时,子类的方法必须抛出相同的异常或父类异常的子类。(父亲坏了,儿子不能比父亲更坏)
2. 如果父类抛出了多个异常,子类重写父类时,只能抛出相同的异常或者是他的子集,子类不能抛出父类没有的异常
3. 如果被重写的方法没有异常抛出,那么子类的方法绝对不可以抛出异常,如果子类方法内有异常发生,那么子类只能try,不能throws