美文网首页
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基础之异常

    Java基础之异常 目录 异常简单介绍 ThrowableErrorException 异常分类 如何处理异常异常...

  • 在Alibaba广受喜爱的“Java突击宝典”简直太牛了

    0-1年:夯实基础 1.Java基础(Java异常分类及处理+Java反射+Java注解+Java内部类+Java...

  • JAVA高级(2)—— 异常机制

    一、基础 1、JAVA异常 1.1、Runtime异常 1)非Checked异常,所有RuntimeExcepti...

  • Java基础之异常处理

    Java基础之异常处理 在Java程序中,异常分为编译异常以及运行时异常 编译异常:程序在编译期间就出现的异常,必...

  • java基础知识精华总结(二)

    1.异常类 JAVA中除了RunTimeException 类,其他异常均须捕获或抛出。 以上就是 java 基础...

  • Java 基础:异常

    目录:一、 异常继承体系二、 发生异常到时候,程序的执行特征:三、 异常与错误的区别四、 抛出异常 throw五、...

  • Java基础——异常

    声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权;凯哥学堂旨在促进VIP学员互...

  • java基础--异常

    1.处理异常 try-catch以及try-catch-finally捕获异常 try{ //一些会抛出异常的...

  • java基础——异常

    父类java.lang.Throwable(implements java.io.Serializable) ...

  • Java基础:异常

    做一个小的笔记,有疑问时方便回顾。 如同大多数现代编程语言一样,Java语言有着健壮的错误处理机制,将控制权从出错...

网友评论

      本文标题:Java基础:异常

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