美文网首页
Java基础:异常

Java基础:异常

作者: 红发_SHANKS | 来源:发表于2018-02-27 17:59 被阅读33次

    做一个小的笔记,有疑问时方便回顾。

    如同大多数现代编程语言一样,Java语言有着健壮的错误处理机制,将控制权从出错点转移给强壮的错误处理器。

    Java 支持异常处理,而不是让错误代码冒泡似的从方法调用链上一层一层向上返回。在异常处理中,一个方法可以通过 “抛出异常” 来发出一个严重问题的信号。调用链的某一个方法,不一定是直接调用者,负责“捕获”异常。

    异常处理的根本优点是:它将错误检测和错误处理解耦了

    异常处理要点:

    1、当你抛出一个异常的时候,控制权转到最近的异常处理器
    2、在 Java 中,由编译器跟踪已检查异常( checked exception )
    3、使用 try / catch 结构处理异常
    4、在正常执行完以后或者是异常发生时,try-with-resources 语句会自动关闭资源
    5、使用 try/finally 结构处理那些无论执行是否正常地进行都要发生的行为
    6、你可以捕获和重新抛出一个异常,或者将它和另外一个异常链起来
    

    异常抛出

    在某种情况下,一个方法,可能发现自己无法执行手头上的任务。可能是缺少必要的资源,可能是提供的参数不一致,这种情况下,最好抛出异常。

    示例场景

    假设一个方法返回一个给定的上下限之间的随机数。而调用者可能由于某种原因将最大范围和最小范围写反了。这种时候,我们应该抛出一个异常,提示方法调用者问题的所在。

        public static int randInt(int low, int high) {
            if (low > high) throw new IllegalArgumentException(String.
                    format("low should be <= high,but low is %d and high is %d", low, high));
            return low + (int) (new Random().nextDouble() * (high - low + 1));
        }
    

    在这里,throw 语句抛出了一个带有错误信息指示的异常对象,方法体内的返回语句不再得到执行,控制权从错误发生点转移到了异常处理器(用户捕获异常的地方)

    异常继承的层次

    异常结构图(图片来自 diycode).png
    所有的异常类都派生自 Throwable
    Error
    Error用来表示编译时和系统错误,一般我们不用关心
    CheckedException
    我们在编码的时候,要么捕获检查异常,要么在方法头声明他们,编译器会检查这些异常是否被妥善的处理。这样的异常通常是可能被预知的情况,比如:输入输出,文件损坏,网络连接失败。在编码的时候,必须处理或者是抛出合理的异常信息来报告这样的错误。
    UnCheckedException
    我们程序的大部分问题其实都来自于 UnCheckedException , 因为编译器不强制要求我们对这样的异常进行处理,而且这样的异常都发生在运行时,在编译期间并不会被编译器检查出来。未检查异常表明是程序员造成的错误,不是不可避免的外部风险导致的,这样的异常往往是细心一点可以避免的。

    已检查异常的声明

    如果一个方法没有捕获一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。

    也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。

    下面方法的声明抛出一个 RemoteException 异常:

    import java.io.*;
    public class className
    {
      public void deposit(double amount) throws RemoteException
      {
        // Method implementation
        throw new RemoteException();
      }
      //Remainder of class definition
    }
    

    一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。在throws语句中,可以将异常合并到一个共同的父类。这样做好不好,取决于异常自身。例如,一个方法抛出多个 IOException,那么将他们都在 throws IOException 中抛出是有意义的。反之,如果异常之间没有联系,则不能合并抛出,这样违背了异常检查的母的。

    一些人认为承认方法抛出异常是很丢脸的一件事,相反,在方法内部处理异常不是更好吗?
    实际上,我们应该允许每种异常找到她自己的处理器。异常处理的黄金法则是”早抛出,后捕获“。
    

    当我们在覆盖方法的时候,不能够抛出比父类方法更多的已检查异常,或者是无关的已检查异常。如果父类方法没有 throws 关键字语句,则覆盖后的方法不能抛出已检查异常。

    抛出了异常的方法,应该在文档中有所体现,比如:@throws:FileNotFoundException if there is no file with the filename

    异常捕获

    为了捕获异常,需要准备一个 try 代码块儿,最简单的形式如下:

       try{
          statements
        }catch(ExceptionClass exceptionObject){
            handler
        }
    

    当执行 try 语句块的时候,如果发生异常,控制转移到异常处理器。这种简单形式有两个变种:
    变种一:

    try {
        // Code that might generate exceptions
    } catch(Type1 t1){
        // Handle exceptions of type1 
    } catch(Type2 t2){
        // Handle exceptions of type2 
    } catch(Type3 t3){
        // Handle exceptions of type3 
    }
    

    catch语句按照从上到下的顺序执行,所以最具体的异常必须放在最前面。匹配具有唯一性。

    变种二

            try{
                statements
            }catch (IllegalArgumentException|IllegalStateException e){
                handler
            }
    
    

    这种情况下,处理器只能在异常变量上调用所有异常共有的方法。这个变种是JDK1.7以后出现的,不过现在基本都是基于1.7/1.8开发的,倒是不用担心了。

    try-with-resources

    正常情况下我们释放资源的需通过finally,但在JDK1.7后,可以使用try-with-resources方式,它可以实现自动释放功能,而不需要加上finally子句。需要做的就是将需要释放的资源对象放在try语句:

    public String read(String filename) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
            StringBuilder builder = new StringBuilder();
            String line = null;
            while((line=reader.readLine())!=null){
                builder.append(line);
                builder.append(String.format("%n"));
            }
            return builder.toString();
        }
    }
    

    并且可以放入多个需要释放的资源:当有多个资源需要释放的时候,先初始化的资源会被后释放,类似于栈的原理。

    被try块包裹的资源必须属于一个实现了 AutoCloseable 接口的类,或者是 AutoCloseable的子接口,比如 Closeable,但是它抛出的是 IOException。

    public void copyFile(String fromPath, String toPath) throws IOException {
        try (InputStream input = new FileInputStream(fromPath);
             OutputStream output = new FileOutputStream(toPath)) {
            byte[] buffer = new byte[8192];
            int len = -1;
            while ((len = input.read(buffer)) != -1) {
                output.write(buffer, 0, len);
            }
        }
    }
    

    finally子句

    在上面,我们释放资源是通过 try-with-resources, 但是不是所有的资源都实现了 AutoCloseable 接口。这种情况下,我们需要使用 finally 子句来实现资源释放。

    Java中主要通过finally把资源恢复到它们的初始状态,如:已打开的文件或网络链接等,总言之,就是与外界“世界”的某个开关。

    这样做是因为无论程序是发生了异常还是正常结束,都会执行 finally 子句。

            try{
                do work
            }finally {
                clean up
            }
    

    我们应该避免在 finally 中抛出异常,因为如果 try 块儿中发生异常,这个异常会被 finally 中的异常所掩盖。参考案例:

            BufferedReader in = null;
            try{
                in = Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8);
            }catch (IOException e){
                System.err.println("caught IOException:"+e.getMessage());
            }finally {
                if (in!=null){
                    //这里也会抛出 IOException , 这时候,要么嵌套 try{}catch{}.要么是改用 try-with-resources
                    in.close();
                }
            }
    

    重新抛出异常和异常链

    这一小节的笔记是直接摘抄自 < https://www.diycode.cc/topics/208/>
    有时我们在捕获到异常后,可能在捕获的地方不适合处理该异常,我们需要将它重新抛出:

        catch(Exception e){
            throw e; 
        } 
    

    这样有一个好处,我们可以将异常交给上一级环境处理,但是这样就会存在一个问题,抛出的的异常携带的信息,也就是printStackTrace()方法显示的是原来异常抛出点的调用栈信息,而非重新抛出点的信息,这样重新抛出点的调用信息就被掩盖了。如果想更新重新抛出点信息到这个异常调用栈中,就可以使用fillInStackTrace()方法:

    catch(Exception e){
        throw e.fillInStackTrace(); 
    }
    

    那么当前调用栈的信息就更新到了这个异常对象中了,还有一种情况,也会存在类似的丢失现象:

    catch(Exception e){
        throw new Exception();
    } 
    

    这样我们上一级的抛出的异常信息就丢了,接收异常的地方就是只能得到new Exception()这个异常的信息。在JDK1.4以前如果你希望保存丢失的那个异常信息,只能通过编码的方式自己实现,而在JDK1.4后,Throwable类有一个构造方法接收一个Throwable类型的参数(文章上方方法汇总可以查看该构造方法)。那么这个传入的参数称为cause,它用来表示原始异常,那么就可以通过异常链从新的异常追踪到异常最初发生的位置。除了构造方法,我们还可以通过initCause(Throwable cause)方法传入一个Throwable对象,它的作用和构造函数传入一个Throwable对象是一样的。大家还记得之前介绍过finally字句吗?,它其实也会造成异常丢失:

    class VeryImportantException extends Exception {
        @Override
        public String toString() {
            return "A very important exception!";
        }
    }
    
    class OtherException extends Exception {
        @Override
        public String toString() {
            return "Other exception";
        }
    }
    
    public class Test {
        void f() throws VeryImportantException {
            throw new VeryImportantException();
        }
    
        void dispose() throws OtherException {
            throw new OtherException();
        }
    
        public static void main(String[] args) {
            try {
                Test test = new Test();
                try {
                    test.f();
                } finally {
                    test.dispose();
                }
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }/* Output:
    Other exception
    *///:~
    

    我们把最外一层try看着是上一级程序的处理,在这个try里面发生了两次异常,但是我们只能获得从finally中抛出的异常信息,而在f()方法中的异常信息丢失,这种情况我们称上一个异常被抑制了。这在JDK1.7之前同样需要我们自己编码去解决这个问题,在JDK1.7之后,新加入了两个方法帮助我们能够很好的去解决这个问题了,那就是addSuppressed(Throwable exception)和getSuppressed(),对于上述问题的解决:

    class VeryImportantException extends Exception {
        @Override
        public String toString() {
            return "A very important exception!";
        }
    }
    
    class OtherException extends Exception {
        @Override
        public String toString() {
            return "Other exception";
        }
    }
    
    public class Test {
        void f() throws VeryImportantException {
            throw new VeryImportantException();
        }
    
        void dispose() throws OtherException {
            throw new OtherException();
        }
    
        public static void main(String[] args) {
            try {
                Test test = new Test();
                Exception exception = null;
                try {
                    test.f();
                } catch (VeryImportantException e) {
                    exception = e;
                } finally {
                    try {
                        test.dispose();
                    } catch (OtherException e) {
                        if (exception != null) {
                            exception.addSuppressed(e);
                        } else {
                            exception = e;
                        }
                    }
                    if (exception != null) {
                        throw exception;
                    }
                }
            } catch (Exception e) {
                System.out.println(e);
                for (Throwable throwable : e.getSuppressed()) {
                    System.out.println(throwable);
                }
            }
        }
    }/* Output:
    A very important exception!
    Other exception
    *///:~
    

    堆栈踪迹

    如果没有在任何地方捕获异常,就会显示堆栈踪迹(stack trace)——列出异常抛出时,所有未决方法的调用信息。堆栈踪迹信息会被推送到错误消息的流 System.err。

    Android中常用的一种捕获未捕获异常的方式就是利用:Thread.getDefaultUncaughtExceptionHandler()。值得注意的是,未捕获异常会终止其所在的线程。

    有时,我们捕获了异常,但是不知道怎么处理的时候,至少要打印出堆栈踪迹信息,而不是简单的忽略她。e.printStackTrace()

    Objects.requireNonNull

     public static void printMessage(String message){
            String realMessage = Objects.requireNonNull(message,"message can not be null !");
            System.out.println(realMessage);
        }
    

    上面的代码看起来好像没有什么实质性的作用,但是在开发截断,可以明显的帮助我们发现 NPE的位置。

    未检查异常概览

    未检查异常.png

    已检查异常概览

    已检查异常.png

    异常方法

    帮助我们更好的找到crash的原因


    image.png

    参考
    [1]: https://www.diycode.cc/topics/208 "Java 异常详解"
    [2]: http://wiki.jikexueyuan.com/project/java/exceptions.html "Java 异常处理"
    [3]: http://www.runoob.com/java/java-exceptions.html "Java 异常处理"
    [4]: 《 Java for the impatient 》

    相关文章

      网友评论

          本文标题:Java基础:异常

          本文链接:https://www.haomeiwen.com/subject/cqrmxftx.html