• 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 语法糖

  • 相关阅读:
    C#编程(七十三)----------浅析C#中内存管理
    C#高级编程小结
    C#编程(七十二)----------DynamicObject和ExpandoObject
    C#编程(七十一)----------DLR ScriptRuntime
    C#编程(七十)----------dynamic类型
    C#编程(六十九)----------DLR简介
    C#编程(六十八)----------LINQ小结
    C#编程(六十七)----------LINQ提供程序
    C#编程(六十六)----------表达式树总结
    python 显示上午下午
  • 原文地址:https://www.cnblogs.com/GuixinChan/p/13553933.html
Copyright © 2020-2023  润新知