Java学习笔记:异常

这篇文章是对自己学习的一个总结,学习资料是疯狂Java讲义第三版,李刚编,电子工业出版社出版。


异常概述

每个函数正常请款下回返回一些值或者不返回,但是在实际业务中,程序总是会出现各种各样的错误。比如一个方法中使用到了数学的运算,然后人眼看上去逻辑没问题,运行一段时间后也没出什么问题。但是突然有一天参数输入错了,出现出除零的错误。很明显,这样程序就不能继续运行下去,又不能正常的返回值,这时候就需要使用到异常,让我们知道程序发生了错误,甚至知道发生了具体什么错误。

异常不仅仅是让开发者了解到错误的发生,开发者还可以预先写好发生异常时程序要怎么处理的对策。比如上面除零的错误,如果预先预料到会出现这样的错误,那我们可以不用眼睁睁看着程序崩溃报错,还可以处理这个错误,让程序不报错,改为输“除数不能为零”,这样就友好很多。


异常处理机制

  • 使用try...catch捕获异常

try...catch是非常强大的语法,它专门用于捕获异常并处理,代码结构如下。

try
{
    //正常业务代码
}
catch (异常类型1 e)
{
    //发生异常1后执行的代码
}
catch (异常类型2 e)
{
    //发生异常2后执行的代码
}
finally
{
    //一些操作
}

这段代码的意思就是,如果try代码块中的代码发生了异常,那系统会生成一个记录着异常信息的异常类,并立刻try中代码的执行,然后开始寻找合适的catch块,之后就讲异常对象交个合适的代码块处理。如果找不到合适的catch,那就直接终止运行环境。

catch块是可以有多个的,每个都可以表示捕获不同类型的异常。所谓合适的catch块是指代码出现的异常类型和catch块中的异常类型一样,这部分具体细节会文章下文详细讲。

顺便一提,try块中定义的变量是局部变量,只在try中有效,其它地方是访问不到该变量的。

finally是无论如何都会执行的代码,无论有没有发生异常,最终都会执行,这个可以省略。

  • 异常类的继承架构和多异常捕获

异常的种类有很多,有表示IO出错的异常IOException,有表示数据库出错的SQLExctption等等。下面就讲讲各个常见异常之间的关系。

Java把所有的非正常情况分为两种,异常(Exception)和错误(error),他们都继承于Throwable父类。Java关于异常和错误的继承架构如下图所示。

Error错误一般是指和虚拟机相关的问题,比如系统崩溃,虚拟机错误,动态链接失效等。这一类错误是无法被修复和捕获的,会这直接导致应用程序中断。应用程序无法处理这些错误,所以应用程序不应该使用catch块来捕获Error对象。

Exception是这篇文章讲解的重点。上面提到过一个try可以有多个catch块来处理。使用多个catch块的时候要注意,catch异常的顺序要遵循“从小到大的原则”。千万不要写成下面的代码

try{
    //业务代码
}
catch (Exception e)
{
    //一些处理
}
catch (IOException e)
{
    //一些处理
}
catch (SQLException e)
{
    //一些处理
}

代码是从上到下执行的,当try发生异常时,一找到合适的catch就会执行catch块中的内容,然后有finally就执行finally中的内容,没有的话就结束,根本不会管其他的catch块的内容。

所有的异常都是继承Exception,所以上面的代码发生异常时,总是会执行第一个catch,而后面的catch永远没有执行的机会。所以catch放置的顺序应该是子类在前,父类在后的从小到大的顺序。

Java中catch还允许一次性catch多个异常,异常之间以 ' | '隔开,示例如下。

try{
    //业务代码
}
catch (IndexOutOfBoundException | NumberFormatException e)
{
    //一些操作
}

这个就可以一次性捕获两个异常。

顺便提一下,一次性捕获多个异常时,异常类都隐性的加了final修饰;只捕获一个异常的话不会给异常类加final修饰。所以下面的代码中第一个catch代码会出错,而第二个不会。

try{
    //业务代码
}
catch (IndexOutOfBoundException | NumberFormatException e)
{
    e = new ArithmeticException("test");
}
catch (Exception e)
{
    e = new AritmeticException("test");
}
  • 访问异常信息

catch捕获到异常类对象以后,异常类有以下几个常用方法来访问异常的相关信息。

getMessage():返回异常的详细描述字符串;

printStackTrace():将该异常的跟踪栈信息输出到标准错误输出流;

printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。

getStackTrace:返回异常的跟踪栈信息。

  • try...catch...finally的执行顺序

try代码发生异常,立刻终止try中代码的执行。程序开始逐个检测catch中捕获的异常是否与本次出现的异常想符合,如果匹配到相同类型的异常,就进入对应的catch执行代码。最后执行finally。

无论try中有没有发生异常,无论是匹配到哪个catch块,finally中的代码在最后一定都会执行。及时在try或catch中执行了return,finally也会执行,除非try或catch中使用了System.exit(1)语句来推出虚拟机,那finally将失去执行的机会。

这里有个有意思的地方,Java程序中使用了try之后,程序执行到try,catch中的throw和return后,并不会如往常一样立刻终止该方法,而是会去寻找有没有finally,然后执行finally中的代码。这就可能导致try和catch中的return和throw失效,比如下面的代码会返回false而不是true。

try
{
    return true;
}
finally
{
    return false;
}

基于finally的特性,finally很适合用来关闭资源。而Java 7以后,都可以使用try来关闭资源。比如下面的代码

try (
    BufferedReader br = new BufferedReader(new FileReader("test.txt"));
    PrintStream ps = new PrintStream(new FileOutputStream("a.txt")))
{
    //一些操作
}

上面的代码在try的后面用加上括号,并在括号中声明了两个IO资源。这样的话就不用再finally中显示关闭资源,系统会自动关闭这两个资源。


Checked异常和Runtime异常体系

Java的异常类分为两大类,Checked异常和Runtime异常(运行时异常)。文章开头给了异常类的继承体系,RuntimeException继承Excepiton,所有RuntimeException类及其子类的实例都被称为Runtime异常,而其他的异常就被称为Checked异常。

只有Java才会区分Checked异常和Runtime异常。

Checked异常就是我们必须显示处理的异常。因为Java设计的严密性,Java认为程序员在编程时必须考虑到出现异常的情况以及相应的处理对策,也就是说对于那些可预见的异常(Checked异常),程序员必须显示处理(使用try....catch处理,或者直接throws给调用者),否则程序连编译期都通不过,会直接报错。比如下面的代码是涉及到常见的一种Checked异常,IOException。两个例子在运行之前都不会报错,但如果去掉throws或者try..catch,那就会直接报错。

//throws IOException,系统不会报错
public static void main(String[] args) throws IOException{
    //IO操作
}

//catch了IOException,系统也不会报错
public static void main(String[] args){
    try
    {
        FileInputStream fis = new FileInputStream("test.txt);
    }
    catch (IOException e)
    {
        //一些处理
    }
}

而Runtime异常,比如常见的越界异常IndexOutOfBoundsException,这类异常不需要我们预先使用throws或者try...catch处理,它至少可以在编译期之后报错。


throw和throws

throw和throws都可以抛出异常,只是两者使用的地方不同

先解释什么是抛出异常。当异常发生时,无非就两种解决方式,在发生异常的这个方法内处理这个异常,将异常交给该方法的调用者处理。而抛出异常就是后者。

比如下面的代码就是抛出异常

public static void main(String[] args) throws IOException{
    //IO操作
}

如果在主函数中发生了IO异常,我们会看到系统会马上在控制台打印异常堆栈信息。这是因为调用主函数的对象是Java虚拟机,而虚拟机处理异常的方式就是直接打印异常堆栈信息。所以抛出异常就是指将异常抛给调用者处理。

throws是放置在方法名字的最后面,括号之前比如上面那段代码。顺便说一下,throws也可以声明抛出若干个异常中的一个,异常之间用' ,' 隔开。比如下面的代码

public static void main(String[] args) throws IOException, SQLException{
    //IO操作
}

而throw是放置在方法体之内,在方法体之内我们可以根据需求随时抛出一个异常。比如下面的代码,只要a变成了零就抛出一个异常。

public static void main(String[] args){
    int a = 0;
    //a经过一些计算后
    if(0 == a){
        throw new Exception("a变成了0的异常");
    }
}

当然throw也可以用在try...catch之中,比如下面的代码

public static void main(String[] args){
    try
    {
        //业务代码
    }
    catch (Exception e)
    {
        throw e;            
    }
}

所以,throw和throws的主要区别只是两者放置的位置不同而已。

同时用了throw就必须用throws,不然会报错。

这里想多说几句。几年前刚接触到异常的时候很迷惑,不知道拿来干嘛,只是有时候涉及到IO,SQL等Checked异常不得不敷衍应付写上throws Exception。但现在工作了写了真正的业务代码后发现,好的异常机制可以让系统走得更远。

比如系统业务代码中有一个方法int test(),该方法有一个int型变量a。从代码的角度说a可以是任何int值,但是从业务逻辑上来说,一旦a变成了负数,就意味着流程错误。可能是遭到攻击,或者用户违规操作。那么一旦发现a变成了负数,即系统出现错误,我们就要马上处理这个错误以免发生损失,同时也要记录错误发生的地点。

处理错误可以是立刻终止该方法,但是记录错误信息呢?或许可以通过打印日志来记录,但是普通用户使用系统时发生错误的话,系统会直接崩溃。用户看着一堆不知道干嘛的502信息会很迷惑,对用户体验不好。用异常机制处理错误的话就可以用很人性化的方式告诉用户系统出错了,这样比较人性化。


异常链

真是的企业级应用中,层与层之间又非常明确的责任划分如下图所示。

对于这样的一个系统,当数据库发生SQLException时,程序就不应该底层的SQLException传到表现层。一是对于正常用户而言,他们不关系也不明白SQLException是怎么回事,二是把这种错误暴露给恶意攻击者是件很不安全的事。

通常的做法是抛出一个业务异常,异常中包含比较友好的提示信息

`````````

猜你喜欢

转载自blog.csdn.net/sinat_38393872/article/details/103014084
今日推荐