解释java程序中的异常机制_Java异常机制

d2c911e8b3777a85386ea48a9e01ff3a.png

异常机制可以使得程序中的异常处理代码和正常业务打代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。

一、概述

在程序运行过程中出现错误,导致程序出现非预期场景。异常处理可以保证出现错误后,控制接下来的程序流程,是选择定位错误信息,还是抛出异常或捕获异常、还是避免程序非正常退出,都取决于我们。异常机制已经成为判断一门编程语言是否成熟的标准,异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。

异常:程序在运行过程中发生由于外部问题导致的程序异常事件,发生的异常会中断程序的运行。(在Java等面向对象的编程语言中)异常本身是一个对象,产生异常就是产生了一个异常对象。注意在Java中异常不是错误,在下文的异常的分类中有解释。

Java异常机制主要依赖于trycatchfinallythrowthrows五个关键词。

  • 1.try:后紧跟一个花括号括起来的代码块(花括号不可以省略),简称try块,里面放置可能引发异常的代码;
  • 2.catch:后面对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块,可以有多个catch块;
  • 3.finally:主要用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件),异常机制总是保证finally块总是被执行。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止;
  • 4.throw:用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象;
  • 5.throws:用在方法签名中,用于声明该方法可能抛出的异常。

Java的异常分为两种,checked异常(编译时异常)和Runtime异常(运行时异常)

Java异常分为两种,checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无需处理,Checked异常可以提醒程序员需要处理所以可能发生的异常,但是Checked异常也给编程带来一些繁琐之处。

二、异常与错误

Java把所有非正常情况分成两种:异常(Exception)和错误(Error),都是继承自Throwable父类(可抛出),如图所示:

2dd5819d0dae6664c15b47e5a02e902e.png

异常和错误最大的区别:异常能被程序本身可以处理,错误是无法处理。

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。一般指的是与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这些错误无法恢复或者不可能捕获,将导致应用程序中断,通常应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws字句中声明该方法可能抛出Error及其任何子类。

例如,Java虚拟机运行错误(Virtual Machine Error),当JVM 不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通过Error的子类描述。

Exception(异常):是程序本身可以处理的异常Exception类有一个重要的子类RuntimeExceptionRuntimeException类及其子类表示“JVM常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException

三、检查的异常和非检查的异常

通常,Java的异常(包括ExceptionError)分为检查异常(checked

exceptions)和非检查的异常(unchecked exceptions)。

Java异常(只包括Exception,不包括Error)分为两大类:Checked异常和RuntimeException异常(运行时异常)。

1、检查异常(编译器要求必须处置的异常)

定义:正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

对检查异常(checked exception)的几种处理方式:

① 继续抛出,消极的方法,一直可以抛到Java虚拟机来处理,就是通过throws exception抛出。

使用throws声明抛出异常的思路是:当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常是将交给JVM处理,JVM对于异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。

throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开,throws声明抛出的语法格式如下:

throws ExceptionClass1,ExceptionClass2……

上面throws声明抛出的语法格式仅跟在方法签名之后,如下例子程序使用了throws来声明抛出IOException异常,一旦使用throws语句声明抛出该异常,程序就无须使用try……catch块来捕获该异常了:

public class demo1 {
    public static void main(String[]args)throws IOException{
        FileInputStream fis=new FileInputStream("a.txt");
    }
}

上面程序声明不处理IOException异常,将该异常交给JVM处理,所以程序一旦遇到该异常,JVM就会打印该异常的跟踪栈信息,并结束程序了,运行上面的程序,会看到如下结果:

Exception in thread "main" java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
    at java.io.FileInputStream.open0(Native Method)
    at java.io.FileInputStream.open(FileInputStream.java:195)
    at java.io.FileInputStream.<init>(FileInputStream.java:138)
    at java.io.FileInputStream.<init>(FileInputStream.java:93)
    at demo.demo1.main(demo1.java:10)

② 用try...catch捕获

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

只有Java语言提供了Checked异常,Checked异常体现了Java的严谨性,它要求程序员必须注意该异常——要么显式声明抛出,要么显式捕获并处理它,总之不允许对于Checked异常不闻不问,这是一种严谨的设计哲学,可以增加程序的健壮性,但是问题是大部分的方法总是不能明确地知道如何处理异常,因此只能声明抛出该异常,而这种情况又是非常普遍的,所以Checked异常降低了程序开发的生产率和代码的执行效率。关于Checked异常的优劣,在Java领域是一个备受争议的问题。

2、不可查异常(编译器不要求强制处置的异常)

定义:编译器不要求强制处置的异常,不会在编译的时候检查,一个个去检查会使得工作变得更加繁琐,只能在运行时才能检查出来,包括运行时异常(RuntimeException与其子类)和错误(Error)。

对未检查的异常(unchecked exception )的几种处理方式:

  1. 捕获;
  2. 继续抛出;
  3. 不处理。

3、对于Exception的异常总结

Exception 这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

四、异常处理机制

Java异常机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

4.1、使用try……catch捕获异常

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。

当Java运行时环境受到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

try {
   ...  //监视代码执行过程,一旦返现异常则直接跳转至catch,
        // 如果没有异常则直接跳转至finally
} catch (SomeException e) {
    ... //可选执行的代码块,如果没有任何异常发生则不会执行;
        //如果发现异常则进行处理或向上抛出。
} finally {
    ... //必选执行的代码块,不管是否有异常发生,
        // 即使发生内存溢出异常也会执行,通常用于处理善后清理工作。
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
 
public class DemoTryCatch {
 
    public static void main(String[] args) {
        //捕获异常
        try {
            //可能产生异常的代码
            readFile();
        } catch (FileNotFoundException e) {
            //异常的处理逻辑,将异常记录日志,异常封装后显示
          System.out.println("系统找不到指定的路径");
        }
        System.out.println("后续代码");
 
    }
    public static  void readFile() throws FileNotFoundException {
        InputStream is = new FileInputStream("E:/iodemo/ch01.txt");
    }
}
​
运行结果:
    系统找不到指定的路径
    后续代码

上面的程序处理读文件时文件路径名称找不到,系统都将抛出一个异常对象,并把这个异常对象交给对应的catch块处理,catch块的处理方式是提醒用户系统找不到指定的路径,通常将异常记录在日志中,异常封装后显示。

4.2、异常类的继承体系

Java运行时环境接收到的异常对象,每个catch块都是专门用于处理该异常类及其子类的异常实例。当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常或者子类的实例,如果是,Java运行时环境将调用和下一个catch块里的异常类进行比较,Java异常捕获流程示意图如下:

a0d40be014f2d2e0f98abfb10d282e4f.png

注意:

①如果try块被执行一次,则try块后只有一个catch块会被执行,绝对不可以有多个catch块被执行,除非在循环中国使用了continue开始了下一次循环,下一次循环又重新运行了try块,这才是导致对个catch块被执行的原因;

try块后面的花括号不可以省略,即使try块只有一行代码,也不可以省略这个花括号,与之类似的是,catch块后的花括号也是不可以省略的;

try块里声明的变量时代码块内局部变量,它只在try块内有效,在catch块中不能访问该对象。

4.3、访问异常信息(异常对象包含的常用方法)

如果需要访问异常对象的详细信息,可以在catch代码块中调用对应的方法来访问。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。

  • getMessage();返回该异常的详细描述字符串
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
  • printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定的输出流
  • getStackTrace():返回该异常的跟踪栈信息
import java.io.FileInputStream;
import java.io.IOError;
import java.io.IOException;
 
public class AccessException {
 
    public static void main(String args[]){
        try{
            FileInputStream fis = new FileInputStream("a.txt");
        }catch (IOException ioe){
            System.out.println(ioe.getMessage());
            ioe.printStackTrace();
        }
    }
}
输出打印信息为:
    a.txt (系统找不到指定的文件。)

上述的代码看到异常的详细描述信息:a.txt(系统找不到指定的文件),这就是调用异常的getMessage()方法返回的字符串。下面更加详细的信息是该异常的跟踪栈信息:

java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
    at java.io.FileInputStream.open0(Native Method)
    at java.io.FileInputStream.open(FileInputStream.java:195)
    at java.io.FileInputStream.<init>(FileInputStream.java:138)
    at java.io.FileInputStream.<init>(FileInputStream.java:93)
    at demo.AccessException.main(AccessException.java:11)

4.4、使用finally回收资源

有些时候,程序杂在try块里打开了一些物理资源(例如数据库连接、网络连接和自盘文件等),这些物理资源都是必须显式回收。Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。

为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块,不管try块中的代码是否出现,也不管哪一个catch块被执行,甚至在try块或者catch块中执行了return语句,finally块总会被执行。完整的Java异常处理语法结构如下:

try{
    可能发生错误的代码
}catch(err){
    只有发生错误时才执行的代码
}finally{
    无论是否出错,肯定都要执行的代码
}

异常处理结构中只有try块是必须的,catchfinally是可选的,但是catch块和finally块至少出现其中之一,也可以同时出现。

try catch finally块中return的执行顺序:

①没有异常发生时,优先级别依次为finally->try

②有异常发生时,优先级依次为finally->catch->捕获异常块最外面的return

参考:https://blog.csdn.net/chenmingxu438521/article/details/102536086

4.5、Java7Java9对于异常的增加功能

Java7之前的是每个catch只能捕获一种类型的异常,从Java7开始,一个catch块可以捕获多种类型的异常,当捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开;捕获多种类型的异常时,当异常变量有隐式的final修饰,因此程序不能对于异常变量重新赋值。

public class MultiExceptionTest {
    public static void main(String[]args){
        try{
            int a=Integer.parseInt(args[0]);
            int b=Integer.parseInt(args[1]);
            int c=a/b;
            System.out.println("您输入的两个数相除的结果是:"+c);
        }catch(IndexOutOfBoundsException|NumberFormatException|ArithmeticException ie){
            System.out.println("程序发生了数组越界,数字格式异常,算术异常之一");
            //对于捕获多异常时,异常变量默认有final修饰,所以不能直接赋值,程序有错
            ie=new ArithmeticException("test");
        }catch (Exception e){
            System.out.println("未知异常");
            //捕获一种类型的异常时,异常变量没有final修饰
            e=new RuntimeException("test");
        }
    }
}

对于捕获多异常时,异常变量默认有final修饰,所以不能直接赋值,程序有错;捕获一种类型的异常时,异常变量没有final修饰,所以对于异常变量的赋值没有错。

Java7增强的自动关闭资源的try语句,为了保证可以正常关闭资源,这些资源实现类必须实现AutoCloseable或者Closeable接口,实现这两个接口就必须实现close()方法。CloseableAutoCloseable的子接口,可以被自动关闭的资源类要么实现了AutoCloseable接口,要么实现了Closeable接口,Closeable接口里的close()方法时只能声明抛出IOException或其子类;AotoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明抛出任何异常。

import java.io.*;
 
public class AutoCloseTest
{
    public static void main(String[] args)
        throws IOException
    {
        try (
            // 声明、初始化两个可关闭的资源
            // try语句会自动关闭这两个资源。
            BufferedReader br = new BufferedReader(
                new FileReader("AutoCloseTest.java"));
            PrintStream ps = new PrintStream(new
                FileOutputStream("a.txt")))
        {
            // 使用两个资源
            System.out.println(br.readLine());
            ps.println("庄生晓梦迷蝴蝶");
        }
    }
}

上面的程序分别声明、初始化了两个IO流,由于BufferedReader、PrintStream都实现了Closeable接口,而且他们放在try语句中声明、初始化,所以try语句会自动关闭它们,因此上面的程序是安全的,自动关闭资源的try语句相当于包含隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。

Java7增强的throw语句,即时Java编译器会执行更细致的检查,会检查throw语句抛出异常的实际类型,如Java编译器知道代码处实际上只可能抛出FileNotFoundException异常,因此在方法签名中只声明抛出FileNotFoundException异常即可。

五、使用throw抛出异常

很多时候,系统是否需要抛出异常和实际的业务有关,如果程序中的数据、执行与既定的业务需求不符合,这就是一种异常,由于与业务需求不符合而产生的异常,必须由程序员来决定抛出,系统无法抛出这种异常,对于这种程序中自行抛出的异常,则应该使用throw语句,throw语句可以单独使用,抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例,

六、异常处理规则

6.1、异常处理原则:

异常信息是提醒“为什么”会抛出——即问题出现的原因,针对于异常的处理利用以下原则,有助于在调试过程中最大限度的使用好异常。

①具体明确

  具体明确指的是在抛出异常时需要针对具体问题来抛出异常,抛出的异常要足够具体详细;在捕获异常时需要对捕获的异常进行细分,这时会有多个catch语句块,这几个catch块中间泛化程度越低的异常需要越放在前面捕获,泛化程度高的异常捕获放在后面,这样的好处是如果出现异常可以近可能得明确异常的具体类型是什么。

例如 FileInputStream 的一个构造方法如下, 对file对象做检查后判断file是否有效,如果无效直接抛出FileNotFoundException,而不是IOException或者其他更宽泛的Exception

②提早抛出

提早抛出的基本目的还是为了防止问题扩散,这样出现异常的话排查起来会比较耗时,比较典型的一种情况是 NPE(NullPointerException),当某个参数对象为null时,如果不提早判断并抛出异常的话,这个null可能会藏的比较深,等到出现NPE时就需要往回追溯代码了。这样就给排查问题增加了难度。所以我们的处理原则是出现问题就及早抛出异常。

例如 上面FileInputStream 的构造方法,在使用前就对Filepath做了判断,如果为null 就及早的抛出NullPointerException,防止在后面open方法中传入一个null,从而简化了出现异常的情况,方便定位问题。

③延迟捕获

延迟捕获说的是对异常的捕获和处理需要根据当前代码的能力来做,如果当前方法内无法对异常做处理,即使出现了检查异常也应该考虑将异常抛出给调用者做处理,如果调用者也无法处理理论上他也应该继续上抛,这样异常最终会在一个适当的位置被catch下来,而比起异常出现的位置,异常的捕获和处理是延迟了很多。但是也避免了不恰当的处理。

6.2、处理技巧

对于异常的处理,能避免的异常,尽量在事先做判断来避免异常的发生,当判断时发现逻辑上已经不能往下走了,需要停止流程,这时候将异常抛出并准确的提示使用者问题所在。对于事先无法预判的异常需要对其进行处理。异常分运行时异常RuntimeException 和 检查异常Checked Exception, RuntimeException 一般用在由于接口方法使用不当的时候,如: 使用了null获取属性方法, 数组下标越界,除法运算除以0等,

  • 如果你调用服务方法的方式不正确,你应该马上修改代码,避免发生RuntimeException
  • 如果是用户方法调用你的方法的方式不正确,你应该立刻抛出RuntimeException,强制让使用者修正代码或改变使用方式,防止问题蔓延;
  • 一般情况下,不要捕获或声明RuntimeException,需要做的是完善程序代码。因为问题在于你的程序本身有问题,如果你用异常流程处理了,反而让正常流程问题一直存在,对于检查异常,一般先看能不能处理,能处理的异常使用try-catch 语句块捕获处理,不能处理使用throws分类型抛出给上一级处理。

  使用try-catch语句块处理时一般需要注意以下几方面:try语句块内要分清稳定代码和非稳定代码,对于稳定的不会出现异常的代码不要放到try语句块中,则有以下几点:

  • catch捕获的异常一定要处理;
  • 若使用了finally语句块,在语句块内一定要对资源对象,流对象进行关闭(jdk1.7之后 可以使用try-with-resources替代);
  • finally中不要使用return语句,因为finally语句块最后一定会执行,这里的return语句会覆盖之前的return语句。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值