Java异常真的看这一篇就够了

前言:本篇博客结合培训班视频编写,希望以最通俗易懂的文字,讲明白异常机制。大部分在介绍概念的时候,我都会用自问自答的方式来描述,原因是这样子更加印象深刻。
动力节点:https://www.bilibili.com/video/BV1Rx411876f?p=629

目录

一、异常概述
二、异常继承结构
三、编译时异常和运行时异常
四、异常的处理
    - 1)处理方式一:捕获
    - 2)处理方式二:抛出
五、异常类的常用方法
    - 1)printStackTrace()方法
    - 2)getMessage()方法
六、finally的使用
七、自定义异常
八、注意点总结


一、异常概述

回到目录

1)什么是异常?
通俗地说,就是程序编译或者执行过程中发生了不正常的情况。

2)java提供异常机制有什么用?
当程序执行过程中出现了不正常情况(异常)的时候,java会把异常的信息打印到控制台上,供程序员参考,程序员就能根据异常信息,对程序进行修改,让程序更加健壮。假如没有异常机制,我们的代码错了也不知道,那显然是不行的。

我们先看看以下的代码:

public class ExceptionTest02 {
     
    public static void main(String[] args) {
     
        int a = 10;
        int b = 0;
        int c = a/b;
        System.out.println(a + "/" + b + "=" + c);
    }
}

显然,我们的代码是不正确的,因为数学中,作为分母的 b 是不能为 0 的,我们执行这段代码,发现控制台输出了以下信息。

有了这些信息,我们就得知,原来我们的代码在第5行出错了,且原因是除数为 0,然后我们就能对代码进行修改,让程序更加健壮。

public class ExceptionTest02 {
     
    public static void main(String[] args) {
     
        int a = 10;
        int b = 0;
        if(b == 0){
      //如果b为0,直接return
			System.out.println("除数不能为0");
			return;
		}
        int c = a/b; //b不为0就会执行到这里进行运算
        System.out.println(a + "/" + b + "=" + c);
    }
}

3)为什么我们代码写错的时候,会有异常出现呢?
在java中,万物皆对象,异常也不例外,各种异常都被封装成了类(也就是异常在java中以类的形式存在),当代码写错时,java虚拟机(JVM)会根据异常类创建一个异常对象,将这个对象“抛出”,并且终止我们程序的执行。
在异常类中,都有一个参数类型为字符串的构造方法,参数是字符串类型,传入的是异常的原因
在这里插入图片描述
比如我们上面的代码中,假如b为 0 时

int c = a/b; //JVM执行到这里会new异常对象:new ArithmeticException("/ by zero");

然后JVM将异常信息打印在控制台上(先不用关注其底层如何实现,知道是JVM在抛异常对象即可)。



二、异常继承结构

回到目录

下图中,红线均表示继承。

Java异常真的看这一篇就够了_第1张图片
                                                                                图(1)

对图(1)的解释
1)Throwable是一个类,该单词的中文翻译是“可抛出的”,也就是继承了该类的Error(错误)和Exception(异常)都
是可以抛出的。这里的抛出是指throw和throws关键字,后面讲。
2)Error是“错误”,Exception是“异常”,错误和异常都会结束程序的执行,但是错误不能被处理,异常可以被处理
。这里的处理是指捕获或者抛出,后面讲。
3)Error子类中,不仅仅包括上图中的两个,但这个不是我们学习的重点,没必要了解太多。Exception子类中,有
一个子类是RuntimeException,RuntimeException下的所有子类都称为运行时异常;除RuntimeException的以外
其他子类,都称为编译时异常。这两种异常同样后面讲。
4)同样的,运行时异常的子类不仅仅只有这些,这里只是列举一些常见的异常。这些我们需要认识,当我们遇到异常
时,就能比较快地知道哪里错误了。


三、编译时异常和运行时异常

回到目录

在前面我们已经提到,异常在java以类的形式存在,当异常出现的时候,是由JVM去创建一个异常对象并且抛出,我们还知道,创建对象这个过程发生在运行阶段。也就是说,编译时异常和运行时异常都是发生在运行阶段,那么编译时异常这个名称是怎么来的呢?

编译时异常:如下图。显然,在我们写代码的时候,我们的编译器就能识别出某个地方有异常需要先处理。也就是在编译前就能知道异常的存在,如果不处理,编译将不能通过,所以称为“编译时异常”。

Java异常真的看这一篇就够了_第2张图片
                                                                                图(2)

运行时异常:看我们前面那个除以 0 的代码,显然,我们在编写代码的时候,编译器并没有发现 int c = a/b; 这一行代码存在异常(也就是这行代码下并没有红色波浪线)。需要在我们执行程序的时候,它才能确定这行代码存在异常,因此称为“运行时异常”。

有些小伙伴可能前面的基础不够扎实,还不知道什么是编译,这里你先简单理解成:
我们在编写java代码的时候,其实是在一个 “类名,java” 的文件上编写,编译能根据这个文件生成另外一个文件,称为字节码文件,文件名是 “类名.class”,只有字节码文件,我们的java虚拟机才能运行,“.java”是运行不了的。
那么也就是说,如果是编译时异常,我们将不能得到 .class 文件,而运行时异常可以,我们来看下图。
Java异常真的看这一篇就够了_第3张图片
                                                                                图(3)

怎么样?对比其他资料上那些经过多次复制粘贴文字表述,这里是不是清晰了很多。那些学过异常,然后知识不牢固的,相信你看到这里,对编译时异常和运行时异常会有更清晰的了解了。



四、异常的处理

回到目录

在写异常的处理之前,请大家先记住一件事:编译时异常可以捕获或者抛出(需要处理),运行时异常不需要处理

现在,先讲前半句,也就是编译时异常的处理方式:
假设有这么个案例,我是做销售的,某次业务中不小心使得公司亏损了1000块钱,我们把这件事当做是java中的异常,我应该怎么做?


1)处理方式一:捕获

第一个方式是我自己掏钱把1000块补上,我自己处理这件事。
在java中,假设一个方法出现了编译时异常,同样的,它也可以通过try...catch...来解决这个异常,这种方式称为
捕获。
针对图(2)中的异常,我们捕获处理的代码如下:(注意看注释,且在执行以下代码之前,请在F盘中创建一个a.txt
文件)
public class ExceptionTest01 {
     
    public static void main(String[] args)  {
     
        //try是"尝试"的意思,也就是尝试一下try后面 { } 中的内容。如果有异常,则进行捕捉,即catch环节
        //;如果没有异常,则不需要catch,跳过catch之后,继续从System.out.println("skr");开始,往下
        //执行
        try {
      
            FileInputStream fis = new FileInputStream("F:\\a.txt");
        } catch (FileNotFoundException e) {
     
        //catch是"捕捉"的意思,也就是在try中判断出现异常之后,catch将异常抓住并进行处理。e.printStac
        //kTrace()就是将异常信息打印在控制台上,其中printStackTrace()所有Throwable子类都具有的方法
            e.printStackTrace();
        }
        System.out.println("skr");
    }
}

可能执行上面的代码,你还是不太能理解,执行结果是直接在控制台上打印“skr”,这是因为,我们的 F:\a.txt 文件是确确实实存在于我们的硬盘上的,也就是能找到文件,也就不会出现异常。
我们修改一下代码,改成 F:\b.txt ,此时,文件已经不存在,我们运行一下,结果如下:
Java异常真的看这一篇就够了_第4张图片
                                                                                图(4)

可以看到,它帮我们将异常信息,包括原因、位置等都打印出来了。还有一个细节,我们注意到,"skr"也被打印出来了!也就是说,异常发生之后,程序还在执行!这和我们前面的说法不同,我们前面是说,发生异常后,程序终止执行,这是为什么呢?带着这个问题,我们来看编译时异常的另一种处理方法。


2)处理方式二:抛出

回到案例,我除了自己掏钱补上,其实我还有另一种处理方式,我可以上报我的组长,让他想办法。
同样的,在java中,如果一个方法出现了异常,那它可以不进行捕捉处理,它可以抛给它的调用者(调用它的另外一
个方法),实现的方式是使用throws关键字。
我们继续来看下面的代码,仍然是注意看注释。
public class ExceptionTest01 {
     
	//3)经理不乐意了,那让董事长(Java虚拟机)去解决吧!继续throws
    public static void main(String[] args) throws FileNotFoundException {
     
        lisi();
        System.out.println("skr");
    }
    //2)组长李四犯愁了,他也不想掏腰包,于是继续throws,又抛给了经理main方法
    public static void lisi() throws FileNotFoundException {
     
        zhangsan();
    }
    //1)定义方法,假设张三犯错导致公司亏损,那他赶紧告诉了组长李四
    public static void zhangsan() throws FileNotFoundException{
     
        FileInputStream fis = new FileInputStream("F:\\a.txt");
    }
}

由于我们的 F:\a.txt 文件是存在的,因此不会出现异常,因此继续执行打印 “skr”;
我们再将 F:\a.txt 修改成 F:\b.txt ,发现报错了,且没有打印 skr 。这是因为,各个方法都不对异常进行处理,Java虚拟机只能将异常信息打印在控制台上,并直接终止程序的运行。
Java异常真的看这一篇就够了_第5张图片
                                                                                图(5)

假如我们一开始就捕捉处理,就不需要一层层地向上抛了,如下代码

public class ExceptionTest01 {
     
    public static void main(String[] args) {
     
        lisi();
        System.out.println("skr");
    }
    public static void lisi() {
     
        zhangsan();
    }
    public static void zhangsan(){
     
        try {
     
            FileInputStream fis = new FileInputStream("F:\\a.txt");
        } catch (FileNotFoundException e) {
     
            e.printStackTrace();
        }
    }
}

【小总结】
编译时异常有两种处理方式,一种方式是在方法内部自行捕捉处理,这种方式如果出现了异常,会打印异常信息,但不会导致程序的终止,因为异常不是被Java虚拟机发现的;另一种方式是在声明方法的时候,加上throws,将可能出现的异常抛给调用者,如果调用者一直没有处理,直到抛给Java虚拟机,Java虚拟机如果发现有异常,那么就会打印异常信息,并终止程序。

我们用了大篇幅来介绍编译时异常的处理,那么运行时异常呢?其实运行时异常我们不用处理,原因很简单。因为运行时异常即使不处理,也能通过编译

学到这里,相信你有以下的困惑:
1)为什么异常要分成编译时异常和运行时异常,直接全部作为编译时异常,那我们的程序不就绝对安全了吗?

想法是正确的,确实,这样能保证我们的程序绝对安全,但是这样的话,我们的程序将导到处都是处理异常的代码,
可读性很差。

2)编译时异常有两种处理方式,那我应该选择哪一种呢?

当异常有必要上报,让调用者知道的时候,就需要用throws,你会觉得这是废话,我来解释一下。

我们来看下图。前面我们已经说到,异常在java中是类的形式存在,异常出现后有异常信息的打印是因为创建了异常
对象,然后调用异常对象的方法进行打印的,这里再强调一遍,我们看下面的代码就好理解了。
显然,我们创建完FileNotFoundException对象,就是想要上抛,让我们程序员在创建FileInputStream对象的时候
,能够清楚地知道该构造方法可能会因为找不到文件而出现异常,所以我们程序员才能使用try catch进行处理。
这里为什么使用throws而不使用try catch也就很明显了,假如我们在这里使用try catch的话,那不是搬石砸脚吗?
我们创建了异常对象,却又要捕捉处理它,那不是没事找事做吗?

具体的话,其实等待了实际的开发,我们才能够真正领会到,所以这里不理解也没关系。

Java异常真的看这一篇就够了_第6张图片
                                                                                图(6)

五、异常类的常用方法

回到目录

在前面的学习中,我们捕捉到异常对象之后,打印异常信息都是使用printStackTrace()方法,其实还有其他常用的方法,现在我们来了解一下。

1)printStackTrace()方法

这个方法较为常用,也就是我们上面一直在用的方法。原因是该方法能够打印异常原因,还能打印异常的跟踪路径,如何理解呢?看看这代码和图你就明白了。

import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class ExceptionTest01 {
     
    public static void main(String[] args) throws FileNotFoundException {
     
        m2();
    }
    public static void m2() throws FileNotFoundException {
     
        m1();
    }
    public static void m1() throws FileNotFoundException {
     
        FileInputStream fis = new FileInputStream("F:\\b.txt");
    }
}

Java异常真的看这一篇就够了_第7张图片
                                                                                图(7)

还是找不到文件这个异常,只不过现在我们是要学习如何看异常信息。
上图写的很清楚了,在红框中,是我们需要关注的,且应该直接找最上面那一行。因为最上面那一行是异常出现的根源,它告诉我们,出现异常的根源是在 ExceptionTest01类的 m1() 方法出错,在类的第12行代码。
于是我们去检查第12行,结合异常原因,发现原来是 new FileInputStream() 时,传入的路径 F:\b.txt 有问题,文件不存在。


2)getMessage()方法

getMessage()方法比较少用,但是在某些时候,只能使用该方法,这里不再扩展,我们看一下怎么用就行。

getMessage()方法以字符串形式返回异常原因,所以我们还需要加上打印的代码。

public class ExceptionTest01 {
     
    public static void main(String[] args)  {
     
        try {
      
            FileInputStream fis = new FileInputStream("F:\\b.txt");
        } catch (FileNotFoundException e) {
     
            System.out.println(e.getMessage());
        } 
    }
}

这个方法不难,大家直接复制粘贴看看效果就行了。



六、finally的使用

我们已经知道,try中的代码,如果出现异常,就会到catch步骤;如果没有出现异常,就跳过catch步骤。finally呢?finally就是无论try中的代码有没有出现异常,finally{ }中的代码都会执行。finally一般用于关闭资源。如下代码

public class ExceptionTest01 {
     
    public static void main(String[] args)  {
     
        try {
      
            FileInputStream fis = new FileInputStream("F:\\a.txt");
        } catch (FileNotFoundException e) {
     
            e.printStackTrace();
        } finally{
     
        	//这就是关闭资源,如果你已经学过了IO流,这个不难看懂;没学过也不要紧,
        	//你只要知道,无论有没有异常发生,finally中的代码都会执行即可
			fis.close(); 
		}
        System.out.println("skr");
    }
}

另外,更离谱的是,即使return结束方法了,finally还是能执行,我们看以下图片
Java异常真的看这一篇就够了_第8张图片
                                                                                图(8)

finally在学习IO流的时候,就会经常用到。



七、自定义异常

SUN公司提供的JDK内置异常类在实际的开发中是不够用的,因为实际开发中有很多的业务,这些业务出现异常之后,JDK中没有。所以我们就需要自定义异常。

怎么自定义异常呢?不会就模仿!
我们来看看 ArithmeticException 类的代码是怎么写的。(不需要记代码!稍微瞄一眼就行!我们模仿就好了,以下是SUN公司写的源代码。)

public class ArithmeticException extends RuntimeException {
     
    private static final long serialVersionUID = 2256477558314496007L;

    public ArithmeticException() {
     
        super();
    }

    public ArithmeticException(String s) {
     
        super(s);
    }
}

我们发现,ArithmeticException类只有两个构造方法,那么会不会是模仿的关键呢?我们试一下:
自定义一个异常,当银行卡中没有钱,你还要取钱的时候,就会报错。

public class NoMoneyException extends RuntimeException {
     
	public NoMoneyException () {
     
        super();
    }

    public NoMoneyException (String s) {
     
        super(s);
    }
}

我们来测试一下这个自定义的异常类管不管用,代码如下

public class ExceptionTest03 {
     
    private int balance = 100; //余额

    public static void main(String[] args) {
     
        ExceptionTest03 test03 = new ExceptionTest03();
        test03.getMoney(200);
    }

    public void getMoney(int money){
      //参数是要取得钱
        if(money > balance){
     
            throw new NoMoneyException("取的钱比余额多,取钱失败");
        }
        balance = balance - money;
        System.out.println("取了" + money + "钱,卡内剩下" + balance + "钱。");
    }
}

我们调用 getMoney() 方法,传入100时,正常打印;传入100以上的钱时,抛出异常,效果如下。说明我们自定义的异常类成功啦!
Java异常真的看这一篇就够了_第9张图片
                                                                                图(9)

细心的小伙伴应该能够注意到,我们这里继承的是 RuntimeException ,也就是我们自定义的异常类是运行时异常类,那么如何自定义编译时异常类呢?
答案是【继承Exception类】即可,这个小伙伴们自己试一下哈,继承 RuntimeException 改成继承 Exception,我们就能看到 throw new NoMoneyException(“取的钱比余额多,取钱失败”); 这行代码下划红色波浪线了。



八、注意点总结

在学完以上的知识点之后,我们最后再来看看几个注意点。

1) throws 关键字后面可以接多个异常类

//如下,getMoney()方法同时抛出了两个异常
public void getMoney(int money) throws NoMoneyException,IOException{
     
}

2) 任何一个调用者都可以 try catch 对异常进行捕捉

在前面张三、组长李四、经理main的案例代码中,其实在zhangsan()、lisi()、main()这三个方法中,都能对异常
进行try catch。不是在 zhangsan() 中才能处理。

3) main() 方法最好不要使用throws

main()方法如果还用throws方法往上抛的话,一旦出现异常,那么程序就会停止执行了。这种在main()方法上加
throws,在实际开发中基本不存在。
因为异常往上抛的目的,是为了提醒程序员在某处可能会出现异常,在可能出现时,及时处理。

4) try中某行出现异常,该行以下的代码都不会再执行
复制执行以下代码,“张三”和“王五”会被打印,“李四”不会。

public class ExceptionTest01 {
     
    public static void main(String[] args){
     
        try {
     
            System.out.println("张三");
            FileInputStream fis = new FileInputStream("F:\\b.txt");
            System.out.println("李四");
        } catch (FileNotFoundException e) {
     
            e.printStackTrace();
        }
        System.out.println("王五");
    }
}

5) catch中的类可以是父类,但是一般不写父类,因为子类会更加精确

其实,我们在 catch( ) 的小括号的是,是可以写成 Exception e 的(这里应用了java的多态特性,Exception 
e = new FileNotFoundException("")),而且,无论是写 catch (FileNotFoundException e),还是写
catch (Exception e),其最终打印的异常信息是一样的。
你会觉得,那我就写 Exception e 就好了,多省事。
这样写的可读性很差,程序员看代码的时候,不能精确地知道该异常是什么类型,理解起来就需要更多时间。

6) catch可以多行,可以捕捉多个异常

既然throws后可以多个异常,那try catch相应的,也需要有多个catch来处理这些异常,如下。
public static void main(String[] args) {
     
    ExceptionTest03 test03 = new ExceptionTest03();
    try {
     
        test03.getMoney(200);
    }catch (NoMoneyException e) {
     
        e.printStackTrace();
    }catch (IOException e) {
     
        e.printStackTrace();
    }
}

7) 父类已经捕捉,子类就没有必要在写了

在上述的代码中,NoMoneyException 和 IOException 都是继承自 Exception 类,那么我们可以直接写父类 
Exception。代码如下。与问题(5)同理,为了提高代码可读性,我们一般不这么做。
另外,如果我们捕捉了 Exception 之后,再捕捉 IOException ,就会报错,大家自己试一下。
public static void main(String[] args) {
     
    ExceptionTest03 test03 = new ExceptionTest03();
    try {
     
        test03.getMoney(200);
    }catch (Exception e) {
     
        e.printStackTrace();
    }
}

8) throw和throws的区别
这个问题,其实 图(6) 已经很清楚了,throw是用在方法内,用来创建异常对象之后,将对象抛出到方法体外;throws是用在方法声明语句上,是将 throw 抛过来的异常对象,抛给该方法的调用者。

你可能感兴趣的:(javase,java)