JavaSE之异常处理

定义

  在程序中,如果不处理异常,正常运行的程序会完全停止运行。异常是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。 

 

 Exception:Exception又分为可检查(checked)异常和不检查(unchecked)异常。

  • 可检查异常(非运行时异常),是RuntimeException以外的异常,类型上都属于Exception类和其子类,如IOException、SQLException等以及用户自定义的Exception异常。对于这种异常,java编译器强制要求我们必须对出现的这些异常进行处理,否则程序就不能编译通过。
  • 不检查异常(运行时异常),其异常都是RuntimeException类及其子类,如 NullPointerException、ArrayIndexOutOfBoundsException之类,这些异常可以选择在程序里捕获处理,也可以不处理,一般是由程序逻辑错误引起的,从逻辑的角度去避免这种异常的发生,如果不处理该异常,程序会把异常一直往上层抛,一直到最上层,导致线程退出或者程序退出。

Java 异常的处理机制

  Java的异常处理本质上是抛出异常和捕获异常。

  • 抛出:在当前环境下无法获得必要的信息来解决问题,从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常
  • 捕获异常:使用try-catch块,在抛出异常的时候,将其转为寻找合适的异常处理器,来处理它,或者忽略异常,让程序继续执行。

 异常处理的基础语法

  Java异常处理涉及到五个关键字,分别是:trycatchfinallythrowthrows

try-catch

try{
    //监控区域
}catch(Exception e){
    //the code of handling exception1
}catch(Exception e){
    //the code of handling exception2
}

  try-catch所描述的即是监控区域,关键词try后的一对大括号将一块可能发生异常的代码包起来,即为监控区域。Java方法在运行过程中发生了异常,则创建异常对象。将异常抛出监控区域之外,由Java运行时系统负责寻找匹配的catch子句来捕获异常。若有一个catch语句匹配到了,则执行该catch块中的异常处理代码,就不再尝试匹配别的catch块了。

  使用多重的catch语句:很多情况下,由单个的代码段可能引起多个异常。处理这种情况,我们需要定义两个或者更多的catch子句,每个子句捕获一种类型的异常,当异常被引发时,每个catch子句被依次检查,第一个匹配异常类型的子句执行,当一个catch子句执行以后,其他的子句将被旁路,异常类型从大到小,从上到下依次处理。

反例:Exception异常应该是放在ArrayIndexOutOfBoundsException后面,下面的写法会让Exception后面的异常处理被屏蔽掉。

反例:
try{
    //code that might generate exceptions    
}catch(Exception e){
    //the code of handling exception1
}catch(ArrayIndexOutOfBoundsException e){
    //the code of handling exception2
}
正确处理:
try{
    //code that might generate exceptions    
}catch(ArrayIndexOutOfBoundsException e){ 
  //the code of handling exception1 
}catch(Exception e){ 
  //the code of handling exception2 
}

  嵌套try语句try语句可以被嵌套。也就是说,一个try语句可以在另一个try块的内部。每次进入try语句,异常的前后关系都会被推入堆栈。如果一个内部的try语句不含特殊异常的catch处理程序,堆栈将弹出,下一个try语句的catch处理程序将检查是否与之匹配。这个过程将继续直到一个catch语句被匹配成功,或者是直到所有的嵌套try语句被检查完毕。如果没有catch语句匹配,Java运行时系统将处理这个异常。

try{
    try{
    //code that might generate exceptions    
    }catch(IOException e){
        //the code of handling exception1
    }  
}catch(Exception e){
    //the code of handling exception1
}

throw

  throw 用于抛出明确的异常。程序执行完throw语句之后立即停止;throw后面的任何语句不被执行,最邻近的try块用来检查它是否含有一个与异常类型匹配的catch语句。如果发现了匹配的块,控制转向该语句;如果没有发现,次包围的try块来检查,以此类推。如果没有发现匹配的catch块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。

throw new NullPointerException("demo");

throws

  如果一个方法会出现异常但却不处理它,那么这个方法必须明确这个异常,以使方法的调用者调用时不发生异常。要做到这点,我们可以在方法声明中包含一个throws子句

public void info() throws Exception
{
   //body of method
   throw new IllegalAccessException(); }

  info()方法里面,明确地会出现异常IllegalAccessException,但是却不想捕获处理而是抛出去,那么它必须在方法上明确这个存在着异常。

Throws抛出异常的规则

  • 如果是不受检查异常(unchecked exception),即ErrorRuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
  • 如果一个方法可能出现受检查异常(checked exception),要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误
  • 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
  • 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。

finally

  无论是否发生异常,我们都有需要执行的代码,这就是  final关键字的所在。例如我们在读取文件文件的时候,如果是否发生了异常,都希望最后能关闭这个文件的流。finally子句是可选项,可以有也可以无,但是每个try语句至少需要一个catch或者finally子句。

public int getPlayerScore(String playerFile) {
    Scanner contents;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile ) {
        logger.warn("File not found, resetting score.");
        return 0; 
    } finally {
        try {
            if (contents != null) {
                contents.close();
            }
        } catch (IOException io) {
            logger.error("Couldn't close the reader!", io);
        }
    }
}

try-with-resources

  JDK1.7之后有了try-with-resource处理机制。首先被自动关闭的资源需要实现Closeable或者AutoCloseable接口,因为只有实现了这两个接口才可以自动调用close()方法去自动关闭资源。 

接口实现AutoCloseable接口,重写close方法:

public class Connection implements AutoCloseable {
    public void sendData() {
        System.out.println("正在发送数据");
    }
    @Override
    public void close() throws Exception {
        System.out.println("正在关闭连接");
    }
}

调用:

public class TryWithResource {
    public static void main(String[] args) {
        try (Connection conn = new Connection()) {
            conn.sendData();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

正在发送数据
正在关闭连接

原理

  反编译刚才例子的class文件:

public class TryWithResource {
    public TryWithResource() {
    }
    public static void main(String[] args) {
        try {
            Connection e = new Connection();
            Throwable var2 = null;
            try {
                e.sendData();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if(e != null) {
                    if(var2 != null) {
                        try {
                            e.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        e.close();
                    }
                }
            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }
    }
}

  编译器自动帮我们生成了finally块,并且在里面调用了资源的close方法,所以例子中的close方法会在运行的时候被执行。不过在catch(){}代码块中有一个addSuppressed()方法,即异常抑制方法。如果业务处理和关闭连接都出现了异常,业务处理的异常会抑制关闭连接的异常,只抛出处理中的异常,仍然可以通过getSuppressed()方法获得关闭连接的异常。

  稍微修改一下刚才的例子:我们将刚才的代码改回远古时代手动关闭异常的方式,并且在sendDataclose方法中抛出异常: 

public class Connection implements AutoCloseable {
    public void sendData() throws Exception {
        throw new Exception("send data");
    }
    @Override
    public void close() throws Exception {
        throw new MyException("close");
    }
}
public class TryWithResource { public static void main(String[] args) { try { test(); } catch (Exception e) { e.printStackTrace(); } } private static void test() throws Exception { Connection conn = null; try { conn = new Connection(); conn.sendData(); } finally { if (conn != null) { conn.close(); } } } }

运行结果:由于我们一次只能抛出一个异常,所以在最上层看到的是最后一个抛出的异常——也就是close方法抛出的MyException,而sendData抛出的Exception被忽略了。这就是所谓的异常屏蔽。

basic.exception.MyException: close
    at basic.exception.Connection.close(Connection.java:10)
    at basic.exception.TryWithResource.test(TryWithResource.java:82)
    at basic.exception.TryWithResource.main(TryWithResource.java:7)
    ......

用try-with-resource的方式再次运行

public class TryWithResource {
    public static void main(String[] args) {
        try (Connection conn = new Connection()) {
            conn.sendData();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:异常信息中多了一个Suppressed的提示,告诉我们这个异常其实由两个异常组成,MyException是被Suppressed的异常。

java.lang.Exception: send data
    at basic.exception.Connection.sendData(Connection.java:5)
    at basic.exception.TryWithResource.main(TryWithResource.java:14)
    ......
    Suppressed: basic.exception.MyException: close
        at basic.exception.Connection.close(Connection.java:10)
        at basic.exception.TryWithResource.main(TryWithResource.java:15)
        ... 5 more

在使用try-with-resource的过程中,一定需要了解资源的close方法内部的实现逻辑。否则还是可能会导致资源泄露。

举个例子,在Java BIO中采用了大量的装饰器模式。当调用装饰器的close方法时,本质上是调用了装饰器内部包裹的流的close方法。

public class TryWithResource {
    public static void main(String[] args) {
        try (FileInputStream fin = new FileInputStream(new File("input.txt"));
                GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,我们从FileInputStream中读取字节,并且写入到GZIPOutputStream中。GZIPOutputStream实际上是FileOutputStream的装饰器。由于try-with-resource的特性,实际编译之后的代码会在后面带上finally代码块,并且在里面调用fin.close()方法和out.close()方法。我们再来看GZIPOutputStream类的close方法:

public void close() throws IOException {
    if (!closed) {
        finish();
        if (usesDefaultDeflater)
            def.end();
        out.close();
        closed = true;
    }
}

我们可以看到,out变量实际上代表的是被装饰的FileOutputStream类。在调用out变量的close方法之前,GZIPOutputStream还做了finish操作,该操作还会继续往FileOutputStream中写压缩信息,此时如果出现异常,则会out.close()方法被略过,然而这个才是最底层的资源关闭方法。正确的做法是应该在try-with-resource中单独声明最底层的资源,保证对应的close方法一定能够被调用。在刚才的例子中,我们需要单独声明每个FileInputStream以及FileOutputStream

public class TryWithResource {
    public static void main(String[] args) {
        try (FileInputStream fin = new FileInputStream(new File("input.txt"));
                FileOutputStream fout = new FileOutputStream(new File("out.txt"));
                GZIPOutputStream out = new GZIPOutputStream(fout)) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

由于编译器会自动生成fout.close()的代码,这样肯定能够保证真正的流被关闭。

 

参考:

 Java入门之异常处理 

深入理解 Java try-with-resource 语法糖

原文地址:https://www.cnblogs.com/GuixinChan/p/13553933.html