【Java异常】概念+体系+分类+练习

一、 异常

1.1 异常概念

通俗点,异常就是不正常的意思,生活中:医生说得了感冒,发烧,某个部位异常,则该部位的功能就会受到影响。在程序中的意思就是:

  • 异常:指的是程序在执行过程中,出现非正常情况,最终就会导致JVM非正常停止
    在Java面向对象编程语言中,异常本身就是一个类,产生异常就是创建异常对象并抛出一个异常对象。Java处理异常的方式就是中断处理。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行

1.2 异常体系

异常机制其实就是帮助我们找到程序中的问题,异常的根类是:java.lang.Throwable,其下两个子类:java.lang.Errorjava.lang.Exception平时所说的异常都是指:java.lang.Exception
在这里插入图片描述
Throwable体系:

  • Error: 严重错误Error,无法通过处理的错误信息,只能事先避免,好比绝症
  • Exception:表示异常,异常产生后程序员可以通过代码的方式取解决纠正,使得程序继续运行,是必须要处理好的。好比感冒,发烧。

Throwable中常用方法:

  • public void PrintStackTrace():打印异常详细信息
  • public String getMessage():获取异常发生原因
1.3 异常分类

我们平常说的异常就是Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。

异常(Exception)的分类: 根据在编译时期还是运行时期去检查异常?

  • 编译期异常:checked异常。 在编译时期就会检查,如果没有处理异常就会编译失败(如:日期格式化)。
  • 运行期异常:runtime异常。在运行时期,检查异常,在编译时期,运行时异常不会报错(如:数学异常)
    在这里插入图片描述
    我们来举个栗子看看三种异常:
/*
 java.lang.Throwable
      Exception:编译期异常,进行编译java代码出现的错误
           RuntimeException:运行期异常,java程序运行时出现的问题
      Error:错误
           错误相当于程序得了一个无法治愈的毛病(非典,癌症晚期),必须对源代码修改,程序才能执行
 */
public class Demo01Exception {
    public static void main(String[] args) {
        //1、Exception:编译期异常
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");//格式化日期
        Date date = null;
        try {
           date =  sdf.parse("2020-12-02");//把字符串格式日期,解析为Date格式日期
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date);

        //2、RuntimeException:运行时异常,在Java程序执行过程中出现的问题
        int[]arr = {1,2,3};
        try{
            //可能会出现异常的代码
            System.out.println(arr[3]);
        }catch (Exception e){
            e.printStackTrace();
        }

        /*3、Error:错误
        OutOfMemoryError:Java heap space //内存溢出
        创建数组太大了,超过JVM分配的内存
        处理:修改代码
         */
        int []brr = new int[1024*1024*1024];
    }
}
1.4 异常的产生过程解析

先运行以下程序,程序会产生一个:ArrayIndexOutOfBoundsException数组下标异常,我们通过图解来解析下异常产生过程。

工具类:

  //获取数组指定下标元素
    public static int getElement(int []arr,int index){
        int ele = arr[index];
        return ele;
    }

测试类:

 public static void main(String[] args) {
        //创建数组
        int[]arr = {1,2,3};
        int e = getElement(arr,3);
        System.out.println(e);
    }

执行结果:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3

我们来图解分析:
在这里插入图片描述

二、异常处理

Java异常处理的五个关键字:try、catch、finally、throw、throws

2.1 throws函数声明
  • throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过

  • throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。

  • 采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。

栗子:

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{ 
     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}
2.2 try…catch…finally语句块
try{
     //try块中放可能发生异常的代码。
     //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
     //如果发生异常,则尝试去匹配catch块。
 
}catch(SQLException SQLexception){
    //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
    //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
    //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
    //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
    //如果try中没有发生异常,则所有的catch块将被忽略。
 
}catch(Exception exception){
    //...
}finally{
 
    //finally块通常是可选的。
   //无论异常是否发生,异常是否匹配被处理,finally都会执行。
   //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。
  //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 
}

注意:

  • try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用
  • 每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义
  • java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行

有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )
而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)
栗子:

public static void main(String[] args){
        try {
            foo();
        }catch(ArithmeticException ae) {
            System.out.println("处理异常");
        }
}
public static void foo(){
        int a = 5/0;  //异常抛出点
        System.out.println("为什么还不给我涨工资!!!");  //不会执行
}
2.3 finally块
  • finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有一种方法让finally块不执行:System.exit()。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等

需要注意的地方:

  • finally块没有处理异常的能力。处理异常的只能是catch块。
  • 在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。
  • 在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。
2.4throw 异常抛出语句
  • throw exceptionObject
    程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象

  • throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别

栗子:

throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别

三、自定义异常

  • 如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException

  • 按照国际惯例,自定义的异常应该总是包含如下的构造函数:

1、一个无参构造函数
2、一个带有String参数的构造函数,并传递给父类的构造函数。
3、一个带有String参数和Throwable参数,并都传递给父类构造函数
4、一个带有Throwable 参数的构造函数,并传递给父类的构造函数。

下面是IOException类的完整源代码,可以借鉴:

public class IOException extends Exception
{
    static final long serialVersionUID = 7818375828146090155L;
 
    public IOException()
    {
        super();
    }
 
    public IOException(String message)
    {
        super(message);
    }
 
    public IOException(String message, Throwable cause)
    {
        super(message, cause);
    }
 
    public IOException(Throwable cause)
    {
        super(cause);
    }
}

栗子:
1、写一个异常类: IllegalAgeException继承Exception

/**
 * 非法年龄异常
 自定义异常,通常用来声明满足语法但是不满足业务的错误情况,java没有现成的异常来说明这种情况
 * 自定义异常需要完成以下工作:
 * 1、定义类名 见名知意
 * 2、需要直接/间接的继承Exception
 * 3、提供Exception中定义的所有种类构造器
 */
public class IllegalAgeException extends Exception{
    public IllegalAgeException() {
    }
    public IllegalAgeException(String message) {
        super(message);
    }

    public IllegalAgeException(String message, Throwable cause) {
        super(message, cause);
    }

    public IllegalAgeException(Throwable cause) {
        super(cause);
    }

    public IllegalAgeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

2、写一个person类,里面有一个age(年龄属性),那当setAge(int age)方法中age不符合业务逻辑时候,我们就throw 上面创建的自定义异常

public class Person {
    private int age;

    public int getAge() {
        return age;
    }

    //这里要throw异常
    public void setAge(int age) throws IllegalAgeException {
        if (age<0 || age>100){
            //throw new RuntimeException("年龄不合法");//运行时异常
            throw new IllegalAgeException();//通常使用throw主动对外抛出一个异常
            //就要在当前的方法上使用throws声明该异常的抛出,否则编译不通过
            //TODO: 只有RuntimeException是个例外
        }
        this.age = age;
    }
}

3、测试

public class ThrowDemo {
    public static void main(String[] args) {
        Person p = new Person();
        //TODO: 语法满足,但是业务不满足(要符合人的年龄)
        try {
            /*
            当我们调用一个含有throws声明异常的方法,编译器要求我们必须处理异常
            ,方式两种:
            1、try catch 扑捉
            2、在当前方法上继续使用throws声明异常抛出
            但是永远不要在main方法上写throws
             */
            p.setAge(1000);
        } catch (IllegalAgeException e) {
            e.printStackTrace();
        }
        System.out.println("今年"+p.getAge());
    }
}

异常的注意事项

1、当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态

例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类

至于为什么?我想,也许下面的例子可以说明:

class Father
{
    public void start() throws IOException
    {
        throw new IOException();
    }
}
 
class Son extends Father
{
    public void start() throws Exception
    {
        throw new SQLException();
    }
}
/**********************假设上面的代码是允许的(实质是错误的)***********************/
class Test
{
    public static void main(String[] args)
    {
        Father[] objs = new Father[2];
        objs[0] = new Father();
        objs[1] = new Son();
 
        for(Father obj:objs)
        {
        //因为Son类抛出的实质是SQLException,而IOException无法处理它。
        //那么这里的try。。catch就不能处理Son中的异常。
        //多态就不能实现了。
            try {
                 obj.start();
            }catch(IOException)
            {
                 //处理IOException
            }
         }
   }
}

2、Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束

也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行

四、finally块和return

在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行

1、finally块中使用return会覆盖method的返回值

以下代码的返回值为:1

public static int div(){
        try {
            return 3;
        }catch (ArithmeticException e){
            System.out.println("catch in div");
            return 2;
        }
        finally {
            System.out.println("finally in div");
            return 1;
        }
    }

以下代码的返回值同样是:1

public static int div(){
        try {
            return 3/0;
        }catch (ArithmeticException e){
            System.out.println("catch in div");
            return 2;
        }
        finally {
            System.out.println("finally in div");
            return 1;
        }
    }

2、finally块中使用return会抑制异常的冒泡传输

即:只要finally中使用了return语句,调用者便认为该方法正常返回

以下代码的输出为:
catch in div
finally in div
catch in adapter
finally in adapter


public class Test {

    public static void main(String[] args) {
        adapter();
    }

    public static void adapter() {
        try {
            div();
        } catch (ArithmeticException e) {
            System.out.println("catch in adapter");
        } finally {
            System.out.println("finally in adapter");
        }
    }

    public static int div() {
        try {
            int a = 5 / 0;
            return a;
        } catch (ArithmeticException e) {
            System.out.println("catch in div");
            throw e;  // 重新将异常抛出给调用者
        } finally {
            System.out.println("finally in div");
        }
    }
}

但如果在 div 的finally块中添加了return语句

public static int div(){
        try {
            int a =  5/0;
            return a;
        }catch (ArithmeticException e){
            System.out.println("catch in div");
            throw  e; // 重新将异常抛出给调用者,但是抛出会被忽略
        }
        finally {
            System.out.println("finally in div");
            return 1;
        }
    }

则代码的输出为:
catch in div
finally in div
finally in adapter

即:finally块中的return语句会阻止异常的栈调用传输,使caller认为该方法已经正常返回

3、finally块中的throw语句会覆盖try和catch语句中的异常


public class Test {

    public static void main(String[] args) {
        adapter();
    }

    public static void adapter() {
        try {
            div();
        } catch (Exception e) {
            System.out.println(String.format("catch in adapter: %s",e.getMessage()));
        } finally {
            System.out.println("finally in adapter");
        }
    }

    public static int div() throws Exception{
        try {
            int a = 5 / 0;
            return a;
        } catch (ArithmeticException e) {
            System.out.println("catch in div");
            throw new Exception("Exception in div"); // 抛出新的异常
        } finally {
            System.out.println("finally in div");
            throw new Exception("Exception in Finally");  // 抛出新的异常
        }
    }
}

输出是:
catch in div
finally in div
catch in adapter: Exception in Finally
finally in adapter

即,catch块中抛出的异常北finally块抛出的异常替换了

修改div方法为:

public static int div() throws Exception{
        try {
            int a = 5 / 0;
            return a;
        }  finally {
            System.out.println("finally in div");
            throw new Exception("Exception in Finally");  // 抛出新的异常
        }
    }

输出为:
finally in div
catch in adapter: Exception in Finally
finally in adapter

即,try块中捕获的异常北finally块抛出的异常替换

finally块和普通代码块一样,无法同时使用return语句和throw语句,因为无法通过编译

谢谢观看

while(true){
    
    
System.out.print("谢谢观看");
}

猜你喜欢

转载自blog.csdn.net/qq_44682003/article/details/110494961