异常处理
程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?
Java提供了更加优秀的解决办法:异常处理机制。
异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。
Throwable类是Java异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。
2.Java异常的分类
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出Error类和Exception类。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
根据编译时对异常处理的要求,又可以将异常Exception分为两类:受检异常与非受检异常。
受检异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try...catch...finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。
非受检异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try...catch...finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。
在IDEA等Java集成开发环境(Integrated Development Environment,IDE)中,受检异常会在编写代码时以红色波浪线标注出来,程序员不处理异常则无法通过编译。
3.异常处理的例子
3.1非受检异常
在数学运算中,除数为0是不被允许的操作,在Java程序中也是一样,一旦运算时除数为0,就会发生算数异常(ArithmeticException),算数异常是非受检异常,属于RuntimeException的一个子类。以下代码会发生除0异常。
public static void main(String[] args) {
int a = 1/0;
}
//运行结果:
//Exception in thread "main" java.lang.ArithmeticException: / by zero
// at com.javaEELesson.day04.Demo7.main(Demo7.java:5)
3.2受检异常
受检异常要求程序员必须通过try...catch...finally语句捕获并处理,或者抛出给方法的调用者,受检异常并不是说程序运行一定会发生错误,而是可能会发生未知原因的错误,所以需要提前捕获可能发生的异常。
3.2.1(捕获异常):
public static void main(String[] args) {
//FileInputStream的构造函数会抛出FileNotFoundException
try {
FileInputStream fileIn = new FileInputStream("C:\\a.txt");
} catch (FileNotFoundException e) {
//通过catch语句捕获可能发生的异常
System.out.println("我处理了异常");
}
}
//运行结果:
//我处理了异常
3.2.2(抛出给调用者):
public static void main(String[] args) throws FileNotFoundException {
//FileInputStream的构造函数会抛出FileNotFoundException
FileInputStream fileIn = new FileInputStream("C:\\a.txt");
}
//运行结果:
//Exception in thread "main" java.io.FileNotFoundException: C:\a.txt (系统找不到指定的文件。)
// at java.io.FileInputStream.open0(Native Method)
// at java.io.FileInputStream.open(FileInputStream.java:195)
// at java.io.FileInputStream.<init>(FileInputStream.java:138)
// at java.io.FileInputStream.<init>(FileInputStream.java:93)
// at com.javaEELesson.day04.Demo7.main(Demo7.java:9)
在上面例子中,在运行结果的异常信息的第一句语句中描述了异常的类型,第二条语句就是程序运行时第一次发生异常的函数位置,第一次发生异常的位置叫做异常抛出点,从异常抛出点开始一直向调用栈的栈顶向栈底回溯,这种行为叫异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的调用者中找到最近的异常处理程序(try...catch...finally语句)。 在写法一中可以看到,我们在catch语句中打印了一条语句“我处理了异常”,这时,程序就没有因为异常而终止运行,而是正常运行并终止了。因为此时JVM默认我们已经处理了异常,所以它就不会因为异常未处理而终止程序。 在写法二中,虽然我们将异常抛出给了方法的调用者,但是事实上我们并没有处理异常,此时JVM一直将异常往上回溯都没有发现处理该异常的程序,于是就终止了程序的运行,并且将异常的堆栈信息打印出来提醒程序员进行异常处理。
3.2.3 非受检异常也可以用try···catch···finally语句捕获并且进行处理,处理后程序就可以正常运行而不是异常终止。不过此时对于IDE来说,不会提醒程序员要进行异常处理,需要程序员手动将可能发生的异常捕获。
public static void main(String[] args) {
try{
int a = 1/0;
} catch (ArithmeticException e) {
System.out.println("我处理了异常");
}
}
//运行结果:
//我处理了异常
4.try...catch...finally语句块
4.1 try,catch,finally这三种语句块中定义的普通变量都是局部变量,有效范围只在该语句块内部。异常处理流程必须至少匹配一个catch或finally。
4.2 编写代码时将可能发生异常的语句放在try语句块中。发生异常时,发生异常异常之后的代码都不会执行,而是直接开始匹配异常。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )。 而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)。
4.3 一旦在try语句块中发生异常,JVM会自动产生一个异常类的实例化对象,并且按顺序在每个catch语句块的参数列表中寻找匹配的异常,一旦匹配到了异常,就会执行该catch语句块中的代码。否则还是会将异常抛出。
4.4 在catch语句块中的异常匹配机制是从上到下顺序匹配,支持父类匹配,一旦匹配到第一个符合条件的catch语句块,其余catch语句块就不会再进行匹配。所以在进行异常处理时一定要将匹配子类异常的catch语句块写在匹配父类异常的catch语句块前面。这样才能保证每个catch语句块都有意义。
4.5 在一个catch语句块中的参数列表中可以匹配一个或多个异常。
4.6 finally语句块,finally语句块比较特殊,在finally语句块中进行的一些操作比较不符合正常人的直觉,下面列举一些finally语句块的特殊之处。
4.7 无论在try语句块中的异常是否发生,无论是否匹配到了异常,无论是否成功处理了异常,finally语句块中的语句都会执行,只有当程序终止时,finally语句块中语句才不会执行。也就是说,除了程序终止这种情况外,finally语句块都必定会执行。
//未发生异常的情况。
public static void main(String[] args) {
try{
int a = 1/1;
} catch (NullPointerException e) {
System.out.println("我处理了异常");
} finally {
System.out.println("我是finally语句块,我成功执行了");
}
}
//运行结果:
//我是finally语句块,我成功执行了
//异常成功处理的情况。
public static void main(String[] args) {
try{
int a = 1/0;
} catch (ArithmeticException e) {
System.out.println("我处理了异常");
} finally {
System.out.println("我是finally语句块,我成功执行了");
}
}
//运行结果:
//我处理了异常
//我是finally语句块,我成功执行了
//异常未成功处理的情况。
public static void main(String[] args) {
try{
int a = 1/0;
} catch (NullPointerException e) {
System.out.println("我处理了异常");
} finally {
System.out.println("我是finally语句块,我成功执行了");
}
}
//异常未匹配的情况。
public static void main(String[] args) {
try{
int a = 1/0;
} finally {
System.out.println("我是finally语句块,我成功执行了");
}
}
//运行结果:
//我是finally语句块,我成功执行了
//Exception in thread "main" java.lang.ArithmeticException: / by zero
// at com.javaEELesson.day04.Demo7.main(Demo7.java:10)
4.8
//关闭数据库连接。
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection(url,userName,password);
stmt = conn.createStatement;
String sql = "sql";//需要执行的sql
stmt.executeUpdate(sql);
stmt.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if(stmt! = NULL){
stmt.close();
}
if(conn! = NULL){
conn.close();
}
}
}
4.9 在try或catch语句块中即便有return,break,continue等改变执行流的语句,finally也会执行。并且finally中如果有return语句,会覆盖try或catch语句块中的返回值。
//正常运行且没有finally时的情况。
public static void main(String[] args) {
System.out.println(divide(2, 1));
}
private static double divide(int a, int b) {
try {
double c = a/b;
return c;
} catch (ArithmeticException e) {
System.out.println("我捕获了异常");
return -1;
}
}
//运行结果:
//2.0
//发生异常且没有finally时的情况。
public static void main(String[] args) {
System.out.println(divide(2, 0));
}
private static double divide(int a, int b) {
try {
double c = a/b;
return c;
} catch (ArithmeticException e) {
System.out.println("我捕获了异常");
return -1;
}
}
//运行结果:
//我捕获了异常
//-1.0
//正常运行且有finally时的情况。
public static void main(String[] args) {
System.out.println(divide(2, 1));
}
private static double divide(int a, int b) {
try {
double c = a/b;
return c;
} catch (ArithmeticException e) {
System.out.println("我捕获了异常");
return -1;
} finally {
System.out.println("我是finally语句块,我成功执行了");
return -2;
}
}
//运行结果:
//我是finally语句块,我成功执行了
//-2.0
//发生异常且有finally时的情况。
public static void main(String[] args) {
System.out.println(divide(2, 0));
}
private static double divide(int a, int b) {
try {
double c = a/b;
return c;
} catch (ArithmeticException e) {
System.out.println("我捕获了异常");
return -1;
} finally {
System.out.println("我是finally语句块,我成功执行了");
return -2;
}
}
//运行结果:
//我捕获了异常
//我是finally语句块,我成功执行了
//-2.0
4.10 原因:在程序运行到return语句时,会向一个内存地址写入返回值,此时如果还有finally语句块的话就继续执行finally语句块,finally语句块中的return语句也是向相同的内存地址写入返回值,所以就会覆盖原先写入的返回值。对于引用数据类型就是写入堆中的地址。
4.11 finally中的return会抑制(消灭)try或catch中的异常,finally中的异常会覆盖try或catch中的异常。
public static void main(String[] args) {
System.out.println(divide(2, 0));
}
private static double divide(int a, int b) {
try {
double c = a/b;
return c;
} finally {
System.out.println("我是finally语句块,我成功执行了");
return -2;
}
}
//运行结果:
//我是finally语句块,我成功执行了
//-2.0
4.12 因此在finally中不要使用return。不要在finally中抛出异常。减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。将尽量将所有的return写在函数的最后面,而不是try ... catch ... finally中。
5.何时使用try...catch...finally,何时使用throws?
当异常发生时,不应立即捕获,而是应该考虑当前作用域是否有能力处理这一异常,如果没有,则应将该异常继续向上抛出,交由更上层的作用域来处理。就如在上面的代码例子中,除数为0导致的异常,在函数中是无法处理的,因为函数并不知道调用者到底要输入什么数,只有将异常抛出给调用者,调用者捕获异常后再进行处理。
6.throw 异常抛出语句
程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。不过因为抛出的异常还要进行异常处理,所以一般不手动抛出。
7.自定义异常
如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。
按照国际惯例,自定义的异常应该总是包含如下的构造函数:
一个无参构造函数 一个带有String参数的构造函数,并传递给父类的构造函数。 一个带有String参数和Throwable参数,并都传递给父类构造函数 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。 下面是IOException类的完整源代码,可以借鉴。
public class IOException extends Exception
{
static final long serialVersionUID = 7818375828146090155L;
public IOException()
{
super();
}
public IOException(String message)
{
super(message);
}
public IOException(String message, Throwable cause)
{
super(message, cause);
}
public IOException(Throwable cause)
{
super(cause);
}
}
8.异常的注意事项
1、当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。
例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。
2、Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。
也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。
9.异常的链化
在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。
异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。
查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。
public class Throwable implements Serializable {
private Throwable cause = this;
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}
public Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
//........
}
下面是一个例子,演示了异常的链化:从命令行输入2个int,将他们相加,输出。输入的数不是int,则导致getInputNumbers异常,从而导致add函数异常,则可以在add函数中抛出
一个链化的异常。
public static void main(String[] args)
{
System.out.println("请输入2个加数");
int result;
try
{
result = add();
System.out.println("结果:"+result);
} catch (Exception e){
e.printStackTrace();
}
}
//获取输入的2个整数返回
private static List<Integer> getInputNumbers()
{
List<Integer> nums = new ArrayList<>();
Scanner scan = new Scanner(System.in);
try {
int num1 = scan.nextInt();
int num2 = scan.nextInt();
nums.add(new Integer(num1));
nums.add(new Integer(num2));
}catch(InputMismatchException immExp){
throw immExp;
}finally {
scan.close();
}
return nums;
}
//执行加法计算
private static int add() throws Exception
{
int result;
try {
List<Integer> nums =getInputNumbers();
result = nums.get(0) + nums.get(1);
}catch(InputMismatchException immExp){
//链化:以一个异常对象为参数构造新的异常对象。
throw new Exception("计算失败",immExp);
}
return result;
}
//请输入2个加数
//r 1
//java.lang.Exception: 计算失败
// at practise.ExceptionTest.add(ExceptionTest.java:53)
// at practise.ExceptionTest.main(ExceptionTest.java:18)
//Caused by: java.util.InputMismatchException
// at java.util.Scanner.throwFor(Scanner.java:864)
// at java.util.Scanner.next(Scanner.java:1485)
// at java.util.Scanner.nextInt(Scanner.java:2117)
// at java.util.Scanner.nextInt(Scanner.java:2076)
// at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
// at practise.ExceptionTest.add(ExceptionTest.java:48)
// ... 1 more
部分转载:https://www.cnblogs.com/lulipro/p/7504267.html#finally_return
浙公网安备 33010602011771号