您当前的位置:首页 > 计算机 > 编程开发 > Java

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

时间:07-05来源:作者:点击数:

一、异常概述

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在抛异常对象即可)。


二、异常继承结构

既然异常以类的形式存在,那么类与类之间肯定存在继承或者实现关系,异常也不例外。而且,有趣的是,所有的异常都是类,没有接口,也没有抽象类。从异常的祖宗类Throwable开始,就是普通类。

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

在这里插入图片描述

图(1)

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

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

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

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

在这里插入图片描述

图(2)

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

有些小伙伴可能前面的基础不够扎实,还不知道什么是编译,这里你先简单理解成:

我们在编写java代码的时候,其实是在一个 “类名,java” 的文件上编写,编译能根据这个文件生成另外一个文件,称为字节码文件,文件名是 “类名.class”,只有字节码文件,我们的java虚拟机才能运行,“.java”是运行不了的。

那么也就是说,如果是编译时异常,我们将不能得到 .class 文件,而运行时异常可以,我们来看下图。

在这里插入图片描述

图(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 ,此时,文件已经不存在,我们运行一下,结果如下:

在这里插入图片描述

图(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 。这是因为,各个方法都不对异常进行处理,包括main()方法,它也不处理,但是它已经不能再把异常抛给其他的方法了,所以它把异常交给了Java虚拟机。Java虚拟机很无奈,它只能将异常信息打印在控制台上,并直接终止程序的运行。

在这里插入图片描述

图(5)

假如我们一开始就捕捉处理,就不需要一层层地向上抛,也就不会抛到Java虚拟机手里,也就不会终止程序,main()方法中的 “skr” 也能打印了。如下代码

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的话,那不是搬石砸脚吗?
我们创建了异常对象,却又要捕捉处理它,那不是没事找事做吗?

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

图(6)

五、异常类的常用方法

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

我们这里要了解的三个方法,分别是printStackTrace()、toString()、getMessage(),它们之间的区别,我先列出来:

1)printStackTrace():打印异常类型、错误信息、出错位置等。最常用。

2)toString():返回异常类型+错误信息。

3)getMessage():返回错误信息。最不常用。

注意一下我这里的用词,打印和返回,也就是后两个需要使用System.out.println()才能打印输出,第一个则是直接调用方法就行。

1)printStackTrace()方法

这个方法最为常用,也是IDEA自动生成try…catch…的时候调用的方法,也是我们上面一直在用的方法。看看代码:

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

public class ExceptionTest01 {
    public static void main(String[] args) {
    //m1()和m2()都将异常往上抛,我们在main()方法中进行捕捉
        try { 
            m2();
        } catch (FileNotFoundException e) {
            e.printStackTrace(); //这里先试试调用printStackTrace()方法的效果
        }
    }
    public static void m2() throws FileNotFoundException {
        m1();
    }
    public static void m1() throws FileNotFoundException {
        FileInputStream fis = new FileInputStream("F:\\b.txt");
    }
}
在这里插入图片描述

图(7)

由于我们的 F:\b.txt 文件是不存在的,因此出现异常了。现在,我们是要学习如何看异常信息。

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

所以,这就是printStackTrace()方法能带给我们的信息,非常详细,因此最为常用。

2)toString()方法

我们稍微改一下上面的代码就行,将方法换成toString()方法,如下

        try { 
            m2();
        } catch (FileNotFoundException e) {
            System.out.println(e.toString());
        }

和printStackTrace()相比,这个方法能拿到的信息更少了

在这里插入图片描述

3)getMessage()方法

我们再来试试getMessage()方法

        try { 
            m2();
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        }

只能说,更low了

在这里插入图片描述

当然,不常用不代表不用,毕竟存在就有它自己的原因。(废话)


六、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还是能执行,我们看以下图片

在这里插入图片描述

图(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以上的钱时,抛出异常,效果如下。说明我们自定义的异常类成功啦!

在这里插入图片描述

图(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 抛过来的异常对象,抛给该方法的调用者。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门