【Java基础】Java异常体系和异常处理机制

一.异常机制的概述

概述
异常机制是指当程序出现错误后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器

程序错误分为三种:编译错误;2.运行时错误;3.逻辑错误。

  1. 编译错误是因为程序没有遵循语法规则,编译程序能够自己发现并且提示我们错误的原因和位置,这个也是大家在刚接触编程语言最常遇到的问题。
  2. 运行时错误是因为程序在执行时,运行环境发现了不能执行的操作。
  3. 逻辑错误是因为程序没有按照预期的逻辑顺序执行。异常也就是指程序运行时发生错误,而异常处理就是对这些错误进行处理和控制。

二.Java异常的分类

  • 所有的异常都是从Throwable继承而来的,是所有异常的共同祖先。
    在这里插入图片描述
Throwable

Throwable: 有两个子类:Exception(异常)Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理错误是无法处理。

Trowable类中常用方法如下:

//返回异常发生时的详细信息
public string getMessage();

//返回异常发生时的简要描述
public string toString();
 
//返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
public string getLocalizedMessage();
 
//在控制台上打印Throwable对象封装的异常信息
public void printStackTrace();
Error(错误)
  • Error是程序无法处理的错误,它是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
  • 这些错误是不可查的,大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况
Exception(异常)
  • Exception是程序本身可以处理的异常,这种异常分两大类
    运行时异常(Unchecked Exception)非运行时异常(Checked Exception)。程序中应当尽可能去处理这些异常。

    • 运行时异常 RuntimeException(Unchecked Exception 不检查异常):即可以编译通过,一般由程序的逻辑错误引起,开发过程中应尽量避免。例如:NullPointerException,IndexOutOfBoundsException等。自定义异常一般继承此类。该异常包括运行时异常(RuntimeException及其子类)和错误(Error)

    • 非运行时异常:RuntimeException以外的异常(checked Exception 检查异常)编译器编译阶段进行处理,程序必须处理此类异常否则无法通过编译,如IOException、SQLException等。程序要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。

三.异常处理流程

在java应用中,异常的处理机制分为抛出/声明(向上抛)异常捕获异常

抛出异常: 当前方法可能会抛出异常,但是不知道如何处理该异常,就将该异常交由调用这个方法的的上一级使用者处理,如果main方法也不知道如何处理这个异常的时候,就会交由JVM来处理这个异常,JVM的做法是:打印异常的跟踪栈消息,并终止程序。这就相当于计算机硬件发生损坏,但是计算机本身无法处理,就将该异常交给维修人员来处理。

捕获异常: 一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常。所谓合适类型的异常处理器指的是异常对象类型和异常处理器类型一致。

对于不同的异常,java采用不同的异常处理方式:

  1. 运行异常将由系统自动抛出,应用本身可以选择处理或者忽略该异常。

  2. 对于方法中产生的Error,该异常一旦发生JVM将自行处理该异常,因此java允许应用不抛出此类异常。

  3. 对于所有的(checked Exception 检查异常),必须进行捕获或者抛出该方法之外交给上层处理。也就是当一个方法存在异常时,要么使用try-catch捕获,要么使用该方法使用throws将该异常抛调用该方法的上层调用者。

异常的产生过程的解析
int[] arr = {1,2,3};
int num = arr[3]; //数组下标越界
  • 获取arr中下标为3的元素,arr的下标最大为2,这时候,JVM就会检测程序出现异常
    JVM会做两件事:

    • JVM会根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的(内容,原因,位置new ArrayIndexOutOfBoundsException("3");
    • 在当前代码所在方法中,没有异常的处理逻辑(try…catch),那么JVM就会把异常对象抛出方法的调用者main方法来处理这个异常。
  • main方法接收到这个异常对象,main方法也没有异常的处理逻辑,继续把异常对象抛出main方法的调用者JVM处理 new ArrayIndexOutOfBoundsException("3");

  • JVM接收到了这个异常对象,做了两件事情 new ArrayIndexOutOfBoundsException("3");

    • 把异常对象(内容,原因,位置)以红色的字体打印在控制台
    • JVM会终止当前在执行的Java程序–>终端处理

四.捕获异常

try-catch
		try {
        	//可能产生的异常的代码区,也成为监控区
        }catch (ExceptionType1 e) {
        	//捕获并处理try抛出异常类型为ExceptionType1的异常
        }catch(ExceptionType2 e) {
        	//捕获并处理try抛出异常类型为ExceptionType2的异常
        }
  • 监控区一旦发生异常,则会根据当前运行时的信息创建异常对象,并将该异常对象抛出监控区,同时
  • 系统根据该异常对象依次匹配catch代码块,如果匹配成功,就运行当前匹配成功的catch代码块中的异常处理代码,一旦处理结束,那就意味着整个try-catch结束。
try-catch-finally
   try {
   		//可能产生的异常的代码区
   }catch (ExceptionType1 e) {
   		//捕获并处理try抛出异常类型为ExceptionType1的异常
   }catch (ExceptionType2 e){
   		//捕获并处理try抛出异常类型为ExceptionType2的异常
   }finally{
   		//无论是出现异常,finally块中的代码都将被执行
   }

try-catch-finally代码块的执行顺序:

  1. try代码块没有捕获异常时,try代码块中的代码依次执行,跳过catch。如果存在finally代码块则执行finally代码块,否则执行try-catch块后面的代码
  2. try捕获到异常时,如果catch块中没有匹配的异常类型,则该异常交给JVM处理。如果存在finally代码块则执行finally代码块,但是 try-catch-finally代码块后面的代码不会被执行了
  3. try捕获到异常时,如果catch块中有匹配的异常类型,则跳到对应catch块执行处理。如果存在finally则执行finally代码块执行完finally代码块之后继续执行后续代码;否则直接执行后续代码
  4. try代码块出现异常之后的代码不会被执行
finally与方法返回值问题
public static void main(String[] args) {
	System.out.println(test());
}
	
public static int test(){
	try {
		System.out.println("try block");
	
		int i = 1 / 0; 
		return 0;
	} catch (Exception e) {
		System.out.println("catch block");
		return 1;
	} finally {
		System.out.println("finally block");
		return 2;
	}
}
//运行代码输出
// try block
// catch block
// finally block
// 2

打印结果是 2,就算把 int i = 1 / 0这一行注释掉,打印结果也是 2。

结论 : finally里的 return语句会把 try/catch块里的 return语句效果给覆盖掉

假如我们不在 finally中 return,转而在finally中赋值,结果会怎样?

public static void main(String[] args) {
	System.out.println(test());
}
	
public static int test(){
	int i = 999;
	try {
		System.out.println("try block");
		i = 1 / 0;
		return i;
	} catch (Exception e) {
		System.out.println("catch block");
		i = 100;
		return i;
	} finally {
		System.out.println("finally block");
		i = 200;
	}
}
//打印结果
//try block
//catch block
//finally block
//100

在 finllay改变了i的值,但是最后输出还是 100,为什么呢?

我的理解: 在 return的时候会把返回值压入栈,并把返回值赋值给栈中的局部变量, 最后把栈顶的变量值作为函数返回值。所以在 finally中的返回值就会覆盖 try/catch中的返回值,如果 finally中不执行 return语句,在 finally中修改返回变量的值,不会影响返回结果。下图为字节码文件中的部分内容:
在这里插入图片描述

try-catch-resource

当我们使用try语句时,使用finally也许会造成一个异常被覆盖的问题,即try语句块中会抛出某个异常,执行finally语句块中跑出了同样的异常,这样的话就会导致原有的异常会丢失,转而抛出的finally语句中的异常。这时我们可以使用带资源的try语句来处理(前提是这个资源实现了AutoCloseable接口的类,否则程序会报错)

格式如下:括号中可以写多行语句,try括号内的资源会在try语句结束后自动释放,前提是这些可关闭的资源必须实现 java.lang.AutoCloseable 接口。

try(Resource res = ...) {
    //TODO:res run
}

实例:文件拷贝

    public static void copy(String srcPath, String destPath) {
        //1、创建源
        File src = new File(srcPath); //源头
        File dest = new File(destPath);//目的地
        //2、选择流
        try (
             InputStream is = new FileInputStream(src);
             OutputStream os = new FileOutputStream(dest);
             ) {

            //3、操作 (分段读取)
            byte[] flush = new byte[1024]; //缓冲容器
            int len = -1; //接收长度
            while ((len = is.read(flush)) != -1) {
                os.write(flush, 0, len); //分段写出
            }
            os.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
总结
  • try代码块:用于捕获异常。其后可以接零个或者多个catch块。如果没有catch块,后必须跟finally块,来完成资源释放等操作,另外建议不要在finally中使用return不要通过catch来控制代码流程

  • catch代码块:用于捕获异常,并在其中处理异常。

  • finally代码块:无论是否捕获异常,finally代码总会被执行。如果try代码块或者catch代码块中有return语句时,finally代码块将在方法返回前被执行。

以下几种情况,finally代码块不会被执行:

  1. 在前边的代码中使用System.exit()退出应用。

  2. 程序所在的线程死亡或者cpu关闭

  3. 如果在finally代码块中的操作又产生异常,则该finally代码块不能完全执行结束,同时该异常会覆盖前边抛出的异常。

五.抛出异常

throws声明异常(向上抛出)
  • 如果一个方法可能抛出异常,但是没有能力处理该异常或者需要通过该异常向上层汇报处理结果,可以方法声明时使用throws来抛出异常。这就相当于计算机硬件发生损坏,但是计算机本身无法处理,就将该异常交给维修人员来处理。
//又发
public void methodName throws Exception1,Exception2….(params){}

其中Exception1,Exception2…为异常列表 一旦该方法中某行代码抛出异常,则该异常将由调用该方法的上层方法处理。如果上层方法无法处理,可以继续将该异常向上层抛。

throw抛出异常

方法内,用throw来抛出一个Throwable类型的异常。 一旦遇到到throw语句,后面的代码将不被执行。然后,便是进行异常处理——包含该异常的try-catch最终处理,也可以向上层抛出。注意我们只能抛出Throwable类和其子类的对象。

public void methodName(params){
	throw newExceptionType;
}
异常关系链
  • 在实际开发过程中经常在捕获一个异常之后抛出另外一个异常,并且我们希望在新的异常对象中保存原始异常对象的信息,实际上就是异常传递,即把底层的异常对象传给上层,一级一级,逐层抛出。
  • 当程序捕获了一个底层的异常,而在catch处理异常的时候选择将该异常抛给上层…这样异常的原因就会逐层传递,形成一个由低到高的异常链。但是异常链在实际应用中一般不建议使用,同时异常链每次都需要就将原始的异常对象封装为新的异常对象,消耗大量资源。
  • 现在(jdk 1.4之后)所有的Throwable的子类构造中都可以接受一个cause对象,这个cause也就是原始的异常对象。
/*
 *高层异常
 */
class HighLevelExceptionextends Exception{
   public HighLevelException(Throwable cause) {
      super(cause);
   }
}

/*
 *中层异常
 */
class MiddleLevelExceptionextends Exception{
   public MiddleLevelException(Throwable cause) {
      super(cause);
   }
}

/*
 *底层异常
 */
class LowLevelExceptionextends Exception{}

public class TestException {
   public void highLevelAccess()throws HighLevelException{
      try {
          middleLevelAccess();
      }catch (Exception e) {
          throw new HighLevelException(e);
      }
}

  
public void middleLevelAccess()throws MiddleLevelException{
      try {
          lowLevelAccess();
      }catch (Exception e) {
          throw new MiddleLevelException(e);
      }
}
 
public void lowLevelAccess()throws LowLevelException {
      throw new LowLevelException();
}

  public static void main(String[] args) {
      /*
       * lowlevelAccess()将异常对象抛给middleLevelAccess(),而
       * middleLevelAccess()又将异常对象抛给highLevelAccess(),
       *也就是底层的异常对象一层层传递给高层。最终在在高层可以获得底层的异常对象。
       */
      try {
          new TestException().highLevelAccess();
      }catch (HighLevelException e) {
          e.printStackTrace();
          System.out.println(e.getCause());
      }
  }
}
异常转义
  • 异常转义,也可以称之“异常类型转换”,就是将一种类型的异常转成另一种类型的异常,然后再抛出异常。
  • 之所以要进行转译,是为了更准确的描述异常,在实际应用中,为了构建自己的日志系统,经常需要把系统的一些异常信息描述成我们想要的异常信息,就可以使用异常转译。异常转译针对所有Throwable类的子类而言,其子类型都可以相互转换。

通常而言,更为合理的转换方式是:

  1. Error——>Exception
  2. Error——>RuntimeException
  3. Exception——>RuntimeException,

在下面的代码中,我们自定义了MyException异常类,然后我们将IOException类型的异常转为MyException类型的异常最后抛出

class MyException extends Exception {
   public MyException(String msg, Throwable e) {
      super(msg, e);
   }
}

public class Demo {
   public static void main(String[] args)throws MyException {
      Filefile =new File("H:/test.txt");
      if (file.exists())
          try {
             file.createNewFile();
          }catch (IOException e) {
             throw new MyException("文件创建失败!", e);
          }
   }
}

相关链接 https://blog.csdn.net/qq_29229567/article/details/89397648

发布了62 篇原创文章 · 获赞 109 · 访问量 5287

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/103156803