Java异常小总结

Java异常小总结


异常的介绍

Java语言中,将程序执行中发生的不正常情况称为“异常”(开发过程中的语法错误和逻辑错误不是异常)。Java程序在执行过程中所发生的异常事件可分为两类(都是属于Throwable下的):

  • Error: Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽、虚拟机错误(VirtualMacheError)及其子类内存溢出错误(OutOfMemoryError)和栈溢出错误(StackOverflowError)。一般不编写针对性的代码进行处理;
  • Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:
    空指针访问、试图读取不存在的文件、网络连接中断; 异常

一个简单的异常演示:

public class TestExceptionOne {
    public static void main(String[] args) {
        String str = null;
        str.indexOf("zx"); // NullPointerException
        System.out.println("--------");
    }
}

程序会发生NullPointerException具体过程如下:

当执行str.indexOf("zx")的时候,Java发现str的值为null,无法继续执行,于是启用异常处理机制,首先创建一个异常对象,这里是类NullPointerException的对象,然后查找看谁能处理这个异常,在上面的代码中,没有代码能处理这个异常,于是Java就启用默认处理机制(异常发生点后的代码都不会执行),那就是打印异常栈信息到屏幕,并退出程序。异常栈信息包括了从异常发生点到最上层调用者的轨迹、行号等。

再看一个例子:

import java.util.Scanner;

public class TestExceptionOne {
    public static void main(String[] args) {
        System.out.println("请输入一串数字: ");
        Scanner cin = new Scanner(System.in);
        String str = cin.next();
        int num = Integer.parseInt(str);
        System.out.println(num);
    }
}

从这个例子来看,为什么需要处理异常呢?

如果用户输入的不是数字,最后就会抛出NumberFormatException异常,对于屏幕输出中的异常栈信息,使用的用户无法理解,也不知道该怎么办,我们需要给用户一个较为友好的信息,告诉用户,他应该输入的是数字,要做到这一点,我们需要自己"捕获"异常。

查看NumberFormatExceptionInteger的源码可以发现:
在这里插入图片描述

分析:

  • 就是创建了一个类的对象,只是这个类是一个异常类。throw就是抛出异常,会触发Java的异常处理机制。之前那个NullPointerException可以认为throw是由Java虚拟机自己实现的。

  • throw关键字可以与return关键字进行对比,return代表正常退出,throw代表异常退出,return的返回位置是确定的,就是上一级调用者,而throw后执行哪行代码则是不确定的,由异常处理机制动态确定。

  • 异常处理机制会从当前函数开始查找看谁"捕获"了这个异常,当前函数没有就查看上一层,直到主函数,如果主函数也没有,就使用默认机制,即输出异常栈信息并退出,这正是我们在屏幕输出中看到的。

上面的程序改进加上异常处理 try - catch:

import java.util.Scanner;

public class TestExceptionOne {
    public static void main(String[] args) {
        System.out.println("请输入一串数字: ");
        Scanner cin = new Scanner(System.in);
        try {
            String str = cin.next();
            int num = Integer.parseInt(str);
            System.out.println(num);
        }catch (NumberFormatException e){
            System.out.println("输入的不是一串数字..");
        }
    }
}

捕获异常后,程序就不会异常退出了,但try语句内异常点之后的其他代码就不会执行了,执行完catch内的语句后,程序会继续执行catch大括号外的代码。


异常的分类

这里写图片描述

整体的架构图:

这里写图片描述

对于这些错误,一般有两种解决方法:

  • 一是遇到错误就终止程序的运行;
  • 另一种方法是由程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理;

捕获错误最理想的是在编译期间,但有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等

关于Throwable类:

有四个构造方法:

public Throwable()
public Throwable(String message)
public Throwable(String message, Throwable cause)
public Throwable(Throwable cause)

有两个主要参数,一个是message,表示异常消息,另一个是cause,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发,cause表示底层异常。

Throwable还有一个public方法用于设置cause

Throwable initCause(Throwable cause)

Throwable的某些子类没有带cause参数的构造方法,就可以通过这个方法来设置,这个方法最多只能被调用一次。

所有构造方法中都有一句重要的函数调用:它会将异常栈信息保存下来,这是我们能看到异常栈的关键。

fillInStackTrace();

Throwable有一些常用方法用于获取异常信息:

void printStackTrace()  // 打印异常栈信息到标准错误输出流,
//它还有两个重载的方法:打印栈信息到指定的流。
void printStackTrace(PrintStream s)  
void printStackTrace(PrintWriter s)
String getMessage()  // 获取设置的异常message和cause
Throwable getCause()
StackTraceElement[] getStackTrace() //获取异常栈每一层的信息,每个StackTraceElement包括文件名、类名、函数名、行号等信息。

分类:编译时异常和运行时异常
这里写图片描述
运行时异常:

  • 是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。
  • 对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。

编译时异常

  • 是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求java程序必须捕获或声明所有编译时异常。
  • 对于这类异常,如果程序不处理,可能会带来意想不到的结果。

区别

  • RuntimeException(运行时异常)比较特殊,含义是unchecked exception(未受检异常),相对而言,Exception的其他子类和Exception自身则是checked exception(受检异常),Error及其子类也是unchecked exception
  • checked还是unchecked,区别在于Java如何处理这两种异常,对于checked异常,Java会强制要求程序员进行处理,否则会有编译错误,而对于unchecked异常则没有这个要求;

常见的运行时异常RuntimeException:
在这里插入图片描述


异常的处理以及自定义异常

Java提供的异常处理机制是抓抛模型。

  • Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。

  • 异常对象的生成:

    • 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个对应异常类的实例对象并抛出 --> 自动抛出
    • 由程序员手动创建:Exception exception = new ClassCastException(); --> 创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样;
  • 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。(就是上面说的异常链)

异常处理的5个关键字
在这里插入图片描述

Java提供的"抓抛模型"

  • "抛",当我们执行代码时,一旦出现异常,就会在异常的代码出生成一个对应的异常类型的对象,并将此对象抛出(自动抛出/手动抛出)。

    • ① 一旦抛出此异常类的对象,那么程序就终止执行;
    • ② 此异常类的对象抛给方法的调用者;
    • ③ 异常类既可以是现成的异常类,也可以是自己创建的异常类;
  • "抓",抓住上一步抛出来的异常类的对象,如何抓? 即为异常的处理方式。

"抓"的两种处理方式

  • 第一种处理的方式: try catch

    • ① 注意try内声明的变量,类似于局部变量,出了try{}语句,就不能被调用;
    • catch语句内部是对异常对象的处理 ,例如 getMessage()printStackTrace()
    • ③ 可以有多个catch语句,try中抛出的异常类对象从上往下去匹配catch中的异常类的类型,一旦满足就执行catch中的代码,执行完,就跳出其后的多条catch语句
    • ④ 对于运行时异常,可以不显示的处理。对于编译时异常,必须要显示的进行处理;
    • ⑤ 若catch中有多个异常类型是"并列关系",熟上熟下都可以。若是"包含"关系,必须将子类放在父类的上面,否则报错;
    • finally中存放的是一定会执行的代码,不管try中,catch中是否仍有异常未处理,以及是否有return语句;
    • try - catch是可以嵌套的;
    • catch块内处理完后,可以重新抛出异常,异常可以是原来的,也可以是新建的(为什么要重新抛出呢?因为当前代码不能够完全处理该异常,需要调用者进一步处理);
  • 第二种处理的方式: throws(在方法的声明处,显示的抛出改异常对象的类型)

    • 当异常在一个方法内部出现的时候,会抛一个异常类的对象,抛给方法的调用者;
    • 异常的对象可以逐层向上抛,直至main()中,当然在向上抛的过程中,可以再通过try-catch进行处理;

注意: 子类重写父类的方法,其抛出的异常类型只能是被重写的方法的异常类的子类或和异常类一样;

这里补充一下异常使用原则:

异常应该且仅用于异常情况,也就是说异常不能代替正常的条件判断。比如说,循环处理数组元素的时候,你应该先检查索引是否有效再进行处理,而不是等着抛出索引异常再结束循环。对于一个引用变量,如果正常情况下它的值也可能为null,那就应该先检查是不是null,不为null的情况下再进行调用。
另一方面,真正出现异常的时候,应该抛出异常,而不是返回特殊值,比如Stringsubstring方法,它返回一个子字符串,代码如下:

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

代码会检查beginIndex的有效性,如果无效,会抛出StringIndexOutOfBoundsException。纯技术上一种可能的替代方法是不抛异常而返回特殊值null,但beginIndex无效是异常情况,异常不能假装当正常处理。


finallythrows使用

finally内的代码不管有无异常发生,都会执行。具体来说:

  • 如果没有异常发生,在try内的代码执行结束执行;
  • 如果有异常发生且被catch捕获,在catch内的代码执行结束执行;
  • 如果有异常发生但没被捕获,则在异常被抛给上层 之前执行

由于finally的这个特点,它一般用于释放资源,如数据库连接、文件流等。

注意: try/catch/finally语法中,catch不是必需的,也就是可以只有try/finally,表示不捕获异常,异常自动向上传递,但finally中的代码在异常发生后也执行。

finally有一个容易出错的地方,就是配合return的使用:

  • 如果在try或者catch语句内有return语句,则return语句在finally语句执行结束后才执行,但finally并不能改变返回值;

看下面例子:

public class TestExceptionOne {
    public static void main(String[] args) {
        System.out.println(method()); // 10
    }
    public static int method(){
        int ret = 10;
        try{
            return ret;
        }finally{
            ret = 20;
        }
    }
}

程序输出10。实际执行过程是,在执行到try内的return ret;语句前,会先将返回值ret保存在一个临时变量中,然后才执行finally语句,最后try再返回那个临时变量,finally中对ret的修改不会被返回。

如果在finally中也有return语句呢? trycatch内的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖trycatch内的返回值,而且还会掩盖trycatch内的异常,就像异常没有发生一样,例如:

public class TestExceptionOne {
    public static void main(String[] args) {
        System.out.println(method()); // 20
    }
    public static int method(){
        int ret = 10;
        try{
            int a = 5/0; // Exception will not happened
            return ret;
        }finally{
            return 20;
        }
    }
}

5/0会触发ArithmeticException,但是finally中有return语句,这个方法就会返回20,而不再向上传递异常了。

finally中不仅return语句会掩盖异常,如果finally中抛出了异常,则原异常就会被掩盖:

public class TestExceptionOne {
    public static void main(String[] args) {
        method();
    }

    public static void method(){
        try{
            int a = 5/0; // replaced
        }finally{
            throw new RuntimeException("hello");
        }
    }
}

finally中抛出了RuntimeException,则原异常ArithmeticException就丢失了。
在这里插入图片描述

所以为避免混淆,应该避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。

再看一个finally中使用return的例子:

public class ReturnExceptionDemo {

    public static void methodA(){
        try {
            System.out.println("进入方法A!");
            throw new RuntimeException("在方法A中制造异常!");

        }finally {
            System.out.println("执行方法A中的finally!");
        }
    }

    public static int methodB(){
        try {
            System.out.println("进入方法B!");
            return 1;
        } finally {
            System.out.println("执行方法B中的finally!");
            return 2;
        }
    }

    public static void main(String[] args) {
        try {
            methodA();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println(methodB());
    }
}

输出:
这里写图片描述

throws关键字

throws跟在方法的括号后面,可以声明多个异常,以逗号分隔。
这个声明的含义是说:

  • 我这个方法内可能抛出这些异常,我没有进行处理,至少没有处理完,调用者必须进行处理。
  • 这个声明没有说明,具体什么情况会抛出什么异常,作为一个良好的实践,应该将这些信息用注释的方式进行说明,这样调用者才能更好的处理异常。

对于RuntimeExceptionchecked exception区别:

  • 对于RuntimeException(unchecked exception),是不要求使用throws进行声明的,但对于checked exception,则必须进行声明,换句话说,如果没有声明,则不能抛出。

  • 对于checked exception不可以抛出而不声明,但可以声明抛出但实际不抛出,不抛出声明它干嘛?主要用于在父类方法中声明,父类方法内可能没有抛出,但子类重写方法后可能就抛出了,子类不能抛出父类方法中没有声明的checked exception,所以就将所有可能抛出的异常都写到父类上了。

如果一个方法内调用了另一个声明抛出checked exception的方法,则必须处理这些checked exception,不过,处理的方式既可以是catch,也可以是继续使用throws,如下代码所示:

public void tester() throws AppException {
    try {
        test();
    }  catch (SQLException e) {
        e.printStackTrace();
    }
}

对于test抛出的SQLException,这里使用了catch,而对于AppException,则将其添加到了自己方法的throws语句中,表示当前方法也处理不了,还是由上层处理吧。


自定义异常

除了Java API中定义的异常类,也可以自己定义异常类,一般通过继承Exception或者它的某个子类,如果父类是RuntimeException或它的某个子类,则自定义异常也是unchecked exception,如果是ExceptionException的其他子类,则自定义异常是checked exception

如何自定义一个异常类:

  • 自定义的异常类继承现有的异常类;
  • 提供一个序列化ID,提供几个重载的构造器;
/**
 * 自己创建的异常类对象
 */
public class MyException extends Exception{

    //序列化的机制  可以唯一的确定一个异常类的对象
    static final long serialVersionUID = -338751699319948L;

    public MyException() {

    }
    public MyException(String message) {
        super(message);
    }
}

再看自定义异常的一个案例:

public class EcmDefDemo {
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        try {
//            int a = cin.nextInt();
//            int b = cin.nextInt();
            int a = Integer.parseInt(args[0]);
            int b = Integer.parseInt(args[1]);
            ecm(a,b); //<font color = red id = "1">执行除法
        }catch (NumberFormatException e) {
            System.out.println("输入的数据类型不一致....");
        }
        catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("缺少了输入数据.....");
        }
        catch (ArithmeticException e) {
            System.out.println("分母为0了.....");
        }
        catch (DefException e) {
            System.out.println(e.getMessage());
        }
    }

    //判断是否输入负数,如果输入了,就抛出一个异常
    public static void ecm (int a,int b) throws DefException {
        if(a < 0 || b < 0){
            throw new DefException("你输入的数值存在负数.....");
        }
        System.out.println(a / b);
    }
}

class DefException extends Exception{
    static final long serialVersionUID = -33873124229948L;
    public DefException() {
    }
    public DefException(String message) {
        super(message);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值