学习笔记:Java异常机制

主要从这几个方面来说说Java中的异常:

图1.知识体系

1.  异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

     异常的继承体系如下:

图1.1 异常的继承体系

    Throwable类是 Java 语言中所有错误或异常的父类。所以,如果要实现自定义异常,那么就要继承这个类或其子类。

    Error:不做过多陈述,出现错误是非常严重的,因为Error是无法处理(虚拟机相关的问题:系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢)必须找到导致错误的根源,修改代码。

    Exception:异常,也就是平时所说的“报错”,具体表现就是IDE控制台出现红色的信息,异常类以......Exception结尾,他有非常多的子类。

如图,我们都知道除数不能为0,当除数为0时,就会有一个算术异常(java.lang.ArithmeticException: / by zero):

图2 异常示例
 

    异常的分类:如图2所示,在我们编写代码时,并没有出现任何错误提示,而当启动程序运行时,出现了这个异常。

我们把这个叫做:运行时异常(runtimeException) ;当写代码的过程中出现错误提示(红色波浪线),称为:编译错误。

常见的运行异常有1. NullPointerExceptin 空指针     2. IndexOutOfBoundsException 索引越界        3. ClassCastException 类转换      4. IllegalArgumentException 非法参数

    问题:异常是怎么产生的(异常产生的过程)?

简单说一下,异常产生的过程。我们都知道,java程序都是经过编译后,运行在JVM(Java虚拟机)上,如图2,代码经过编译为.class文件,JVM加载.class文件开始运行:①JVM检测到程序做1除以0运算,但除数不能为0,于是,JVM创建了异常对象:exception = new ArithmeticException();   ②抛出异常对象(throw exception); ③把异常传递给方法调用者(这里为main),但main并没有处理这个异常,又传给了JVM;④JVM处理异常简单粗暴:打印异常信息,然后终止程序运行。

    问题:以图2为例,JVM处理异常简单粗暴,那么我们能否在JVM处理前,自己把这个异常抛出?

当然可以,我们可以在做运算前检查一下参数,这里主要检查参数b。如果有问题自己在方法内抛出异常。代码如下:

public static void main(String[] args) {
        int a = 1,b = 0;
        getResult(a, b);
    }
    //计算a除以b的结果的方法
    private static void getResult(int a, int b) {
        //检查参数
        if (b == 0){
            //仿照JVM内,抛出(throw)一个(声明好的)算数异常
            throw new ArithmeticException("b 不能为0");
        }
        int  i = a/b;
        System.out.println(i);
    }

2. 那么,该如何在出现异常时优雅的处理异常,让程序不停止运行或者奔溃?

处理异常,离不开五个关键字:try、catch、finally、throw、throws。

    ①先说第一个关键字,throw(抛出):这个关键字,在上述示例中已经出现。有关throw关键字的使用:throw是使用在方法内,将这个异常传递(动态的抛出)到对象调用者处,并停止当前方法的执行(后面的方法也不会执行)。       

格式:throw new 异常类名(参数)。   如:throw new NullPointerException("要访问的值不存在");

作用: 警告方法调用者,非法调用当前方法。

源码解读_Objects.requireNonNull()方法: 这是java.util.Objects工具类的一个查看指定引用对象不为空的静态方法,该方法主要用于验参数验证和构造函数。

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

如果这个方法用于图2的参数验证和我们自己抛出异常效果是一样的,只不过直接使用,更方便。

②第二个关键字,throws(抛出):这里准确说为声明异常,即,将可能会出现的问题标识出来,报告给方法调用者,由调用者处理。【如果说异常是一个“流氓”,那么throws的处理方式为,我打不过你,我去叫人处理你;而稍后要说的try...catch,则一点“不怂”,直接踹你,把你KO了】 

   用法:运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

   格式:方法名(参数)throws  异常类名1,异常类名2......{//方法体}

    示例稍后展示;

③try...catch...(catch...)finally:

如果发生异常,不处理的话,程序就会立刻停止,这样带来的后果是很严重的,所以,我们要处理异常,即,用try..catch...

try中写的是可能会出现异常的代码,catch来捕获异常,当try中的代码出现问题时,catch就会立刻做出响应,捕获并处理异常;反之,try中的代码正常运行,也就没catch什么事了。

语法 (try和catch都不能单独使用,必须连用):

try{
    //编写可能会出现异常的代码
}catch(异常类型 e){
    //处理异常的代码
    //记录日志/打印异常信息/继续抛出异常
}

 public static void main(String[] args) {
        method();
        System.out.println("程序执行结束了");

    }
    private static void method() {
        int[] array = {1,2,3};
        try{
            int e = array[3]; //JVM : throw new ArrayIndexOutOfBoundsException()
            System.out.println(e);
        }catch (Exception e){ // e = new ArrayIndexOutOfBoundsException()
            // 发生异常并捕获了,执行这里
            System.out.println("呵呵,异常发生了");
        }
    }

这样虽然捕获了异常,并且不影响后面的代码执行,但是,如果业务逻辑复杂,要想快速定位到发生异常的原因,位置,就要调用ThrowableprintStackTrace()方法,这个方法的作用是打印栈中追溯,简单说,就是打印异常信息(包含了异常的类型,原因和出现的位置)。只需要在catch中加入:

// 打印栈中追溯 (打印异常信息)
e.printStackTrace();
图3 

那么上面说的throws,将异常声明,由方法调用者处理,那么,方法调用者就需要try...catch来处理。

例:

public static void main(String[] args) {
        String date = "2018-09-09";     //定义个时间,这里格式是正确的,可以试试错的
        //调用方法,需要处理这个方法抛出的异常
        try {
            method(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }

    }
    //将字符串类型时间转换成Date类型
    private static void method(String date) throws ParseException {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        //parse 方法可能有一个格式转换的问题,也就是参数格式个定义的转换格式不一致
        //这里不处理,而是抛出
        Date date1 = dateFormat.parse(date);
        System.out.println(date1);
    }

那么当,try中的代码可能发生多种异常时,那么可能就需要“分类讨论”,也就是分别列出可能发生异常情况;

语法:

try{
    //编写可能会出现异常的代码
}catch(异常类型 e){
    //处理异常的代码
    //记录日志/打印异常信息/继续抛出异常
} catch (异常类型  e){

}

//以下代码只是示例,不具有任何实际意义 
public static void main(String[] args) {
        try {
            method(1);
        } catch (NullPointerException e) {
            // 如果出现空指针异常
            e.printStackTrace();
            System.out.println(0);
        }catch (IndexOutOfBoundsException e) {
            //如果出现越界异常
            e.printStackTrace();
            System.out.println(1);
        }
        System.out.println("程序继续运行");
    }
    private static void method(int i) {
        if(i == 0){
            throw new NullPointerException("空指针异常");
        }else{
            throw new ArrayIndexOutOfBoundsException("越界异常了");
        }
    }

    我们都知道Exception是所有异常的父类,可不可以直接用一个catch(Exception e)处理呢?当然是可以的。那么,这样“分类讨论”有什么意义呢?

    当Java运行时,出现异常,就需要处理,而异常种类又是多种多样的,不同的异常,根据实际业务需求处理的方式可能不同,所以,需要不同的catch块来处理专门的异常。【多个catch块处理时,需要,注意异常等级需要从小到大,也就是第一个catch处理的异常一定不能是他的父类,因为父类异常一定能处理子类的异常,要是父类异常都处理完了,要子类异常来干嘛!】

    

最后来说说,finally代码块,finally意为最后的,在这里的意思就是,无论这个这个代码try了,还是catch了,finally都要执行。

  public static void main(String[] args) {

        try{
            //可能出现异常的代码
            int i = 1/0;
            System.out.println(i);
        }catch(NullPointerException e){
            // 抓到异常,会执行这里
            System.out.println("抓住了");

        }finally {
            // 无论如何都执行
            System.out.println("无论如何都执行");
        }

        System.out.println("程序继续执行");
    }

finally一般用在程序结束后释放资源之类的。

除了上面所说的,printStackTrace()方法输出异常信息之外,还有一些其他方法,访问异常信息:getMessage()方法返回该异常的详细描述字符串;   printStackTrace(PrintStream s)方法将该异常的跟踪栈信息输出到指定输出流;    getStackTrace()方法返回跟踪栈信息。

一般来说,使用printStackTrace()足够使用。

4. 最后是关于异常的注意点

    ①finally 与 return 之间的矛盾。

我们都知道return 标志着一个一个方法的结束,理论上return之后,任何代码不会执行。

但请看示例:

 public static void main(String[] args) {
        //调用定义的方法接收返回值
        int result = method();
        System.out.println("result:" + result);
    }

    private static int method() {
        int i = 1;
        try{
            return i;
        }catch (Exception e){
            i = 2;
        }finally {
            i = 3;
            System.out.println("finally:" + i);
            return i;   
        }
    }

在运行代码前?控制台会输出什么?按道理应该会是1.

事实却不是这样的:控制台会输出

finally:3
result:3

原因就在于:return 执行权 被finally 抢过去了,先记录了 i 的值 也就是数字1,而不是变量,等到finally 执行完, return接着执行 return i    而此时 i 被赋值为3了。

     ②方法重写和异常声明

问题:父类不抛出异常,子类可以抛出(throws)异常吗 ?

现有个父类Person,Person有个方法show,子类Student继承父类Person,并且重写了父类方法,那么,父类的方法没有抛出异常的情况下,子类重写的方法能抛出异常吗?

答案是:

父类方法如果没有抛出编译异常,那么子类重写的方法也不能抛出编译异常!!!!!!!
图4 
这里的IO异常属于编译异常。
图5

这是为什么?

//父类引用指向子类对象
Person p = new Student();
p.show(); // 执行子类重写的方法

原因:

    首先看上述代码示例。我们都知道异常分为两类,一个是编译时异常,一个是运行时异常。编译时异常在写代码的时候就要处理,而运行时期才会出现运行时异常。也就是说,这是一对矛盾,因为java中编译器编译时是只看 “=” 号左边,程序运行是看 “=” 号右边,而上述代码中“=”左边是父类引用,而父类方法没有抛出编译异常,所以不用处理,但是“=”右边new的是子类,子类重写的方法却去抛出编译异常。    矛盾点就在于编译异常的概念和运行看“=”右边出现矛盾,为了避免这种情况,就禁止这样去写。

5. 最后说一下自定义异常

     什么是自定义异常?

在开发中根据自己业务的异常情况来定义异常类

     为什么需要自定义异常?

Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是没有定义好的,此时我们根据自己业务的异常情况来定义异常类。,例如年龄负数问题,考试成绩负数问题。

     如何自定义异常类?

自定义一个编译期异常: 自定义类 并继承于 java.lang.Exception 。
自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException 。

// 业务逻辑异常
public class LoginException extends Exception {
/**
* 空参构造
*/
public LoginException() {
} /
**
* *
@param message 表示异常提示
*/
public LoginException(String message) {
super(message);
}
}
public class Demo {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};
public static void main(String[] args) {
//调用方法
try{
// 可能出现异常的代码
checkUsername("nill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(LoginException e){
//处理异常
e.printStackTrace();
}
} /
/判断当前注册账号是否存在
//因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
throw new LoginException("亲"+name+"已经被注册了!");
}
} r
eturn true;
}
}

最后,推进一篇文章【Java 中的 try catch 影响性能吗

猜你喜欢

转载自blog.csdn.net/weixin_38816084/article/details/82717444