第十二章 异常

今天我们来学习一下异常,它的关键字是exception,异常本质上是程序上发生的错误,比如使用空的引用、数组下标越界、内存溢出等等。错误在我们编写程序的过程中会经常发生,包括编译期间和运行期间都可能会发生。如果是在编译期间出现的错误,比如说语法错误,编译器会给我们开发人员提示。但运行期间的错误的话,编译器就无能为力了,比如说数组越界。如果程序在运行期间出现了错误,程序可能会终止或直接导致系统崩溃。因此,如何对运行期间出现的错误进行处理呢?Java通过异常机制来处理程序运行期间出现的错误。

在Java中,异常类的结构层次图如下图所示:

在Java中异常被当做对象来处理,根类是java.lang.Throwable类,其下又分为两大类:Error和Exception。Error是无法处理的异常,比如内存溢出OutOfMemoryError,一般发生这种异常,JVM会选择终止程序。因此我们编写程序时不需要关心这类异常。Exception才是我们说的异常情况,比如空引用NullPointerException、数组越界IndexOutOfBoundsException等等。

Exception类的异常包括checked exception和unchecked exception。

checked exception(检查异常),java编译器强制程序员必须进行捕获处理,比如常见的IOExeption和SQLException。对于此类异常如果不进行捕获或者抛出声明处理,编译都不会通过。

unchecked exception(非检查异常),也称运行时异常(RuntimeException),比如常见的NullPointerException、IndexOutOfBoundsException。对于运行时异常,java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。

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

在Java中如果需要处理异常,必须先对异常进行捕获,然后再对异常情况进行处理。在我们日常的数学除法计算中,我们都知道除数是不能为零的,计算机中也遵守此原则。

// 除数不能是零
int x = 100 / 0;
System.out.println("100/0 = " + x);

如果运行上述代码,整个程序运行就会停止,然后Java虚拟机会抛出如下异常:

Exception in thread "main" java.lang.ArithmeticException: / by zero

ArithmeticException属于算术异常,也就是我们上面说的除数不能为零。

接下来,我们就通过异常捕获的方式来解决它,代码如下:

// 捕获除数不能是零的异常
try {
	
	int x = 100 / 0;
	System.out.println("100/0 = " + x);
	
} catch (ArithmeticException e) {
	
	// 打印异常的具体内容信息
	System.out.println(e.toString());
}

运行之后,程序并没有向之前一样由Java虚拟机抛出异常,而是执行了System.out.println(e.toString());语句,最终输出了:java.lang.ArithmeticException: / by zero

大家可能觉得这不都一样嘛,程序都无法正常运行,都抛出了异常信息。但是,大家忽略了两者的本质,前者是由JVM抛出的异常,程序直接停止运行了,不受开发开发人员控制了。后者是由开发人员主动输出的异常内容,其实程序运行的控制权仍然在开发人员手里,我们实际上仍然可以让程序继续执行下去,就看开发人员对“除数为零”的异常怎么处理啦。比如说,我们将除法表达式的结果数据进行矫正,让其进入到下一步的程序逻辑处理。

接下来,我们稍微讲解一下上面的代码,被try块包围的代码可能会发生异常,一旦发生异常,异常便会被catch捕获到,然后需要在catch块中进行异常处理。由于发生的异常可能是多个,因此我们可以使用多个catch来匹配不同类型的异常,这就如同Switch语句中的case匹配是一个道理。但是大家一定要注意的是,在有多个catch块的时候(多重捕获),是按照catch块的自上而下的先后顺序进行匹配的,一旦异常类型被一个catch块匹配,则不会与后面的catch块进行匹配。而且整个顺序最后是先进行子类异常的匹配,然后再是父类异常的匹配。如果你第一catch就匹配异常的父类exception的话,那下面的所有catch的代码都不会匹配,也就不会执行啦,这一点大家一定要切记!!!

在Java中还提供了另一种异常处理方式即抛出异常,顾名思义,也就是说一旦发生异常,我把这个异常抛出去,让调用者去进行处理,自己不进行具体的处理,此时需要用到throw和throws关键字。代码如下:

// 定义一个存在数组下标越界异常的方法
public static void test() throws Exception {
	
	int[] a = {1,2,3,4,5};
	System.out.println(a[20]);
}

首先我们定义了一个方法,在该方法中我们定义了一个大小为5的整型数组,但是我们访问了下标是20的数组元素,很明显,数组下标越界了,肯定会抛出ArrayIndexOutOfBoundsException异常的。但是,我们并不打算在该函数的内部使用try-catch语句来捕获这个异常,而是通过throws关键字将异常抛出给调用方来处理。紧接着,我们就可以在main入口方法中调用上面的test方法了。如果我们在main方法中直接调用test方法的话,我们发现Eclipe给了我们一个提示:Unhandled exception type Exception

因为我们在定义test方法的时候,使用throws抛出了异常的父类Exception,因此调用者必须处理这个异常,否则无法编译程序,也就不可能运行了。所以,我们得使用try-catch包裹test方法才行,代码如下:

// 调用数组下标越界异常的方法
try {
	test();
} catch (Exception e) {
	e.printStackTrace();
}

这次我们就可以正常的编译运行该程序了,因为我们程序中确实存在异常,因此运行的结果也是输出异常的详细内容,如下图所示:

在上述代码中,大家可能注意到了,在catch代码块中,我们调用了异常类的printStackTrace的方法,从字面意思来看,就是打印异常的栈轨迹。异常是在执行某个函数时引发的,而函数又是一层一层的调用,形成调用栈结构。注意,只要一个函数发生了异常,那么他的所有的调用者都会被异常影响。异常最先发生的地方,叫做异常抛出点,然后依次向上(调用者)传递这个异常,直到有调用者能够处理这个异常。如果没有任何一个调用者来处理这个异常的话,那么这个异常最终由入口main方法抛给Java虚拟机,输出异常的信息,同时还包括函数的调用栈信息,当然这会导致程序运行停止。

从上面的例子可以看出,当test方法发生数组越界异常时,test方法将抛出ArrayIndexOutOfBoundsException异常,因此调用他的main方法进行了try-catch异常捕获,所以就会进入到catch的处理代码块中,因为我们在处理代码块中打印了异常的栈轨迹,因此就会输出异常的信息和函数的调用信息,我们可以看到依次出现了test方法和main方法。这些信息可以帮助开发人员精确定位异常发生的地方,然后去处理它。

接下来,我们使用throw关键字手动来抛出异常对象。下面看一个例子:

// 重载test方法,手动抛出异常
public static void test(int index)  {
	
	int[] a = {1,2,3,4,5};
	if(index<0 || index>=a.length)
		throw new ArrayIndexOutOfBoundsException("数组下标越界");
	else
		System.out.println(a[index]);
}
// 调用重载方法test
try {
	test(20);
} catch (Exception e) {
	e.printStackTrace();
}

运行之后输出异常信息,如下所示:

java.lang.ArrayIndexOutOfBoundsException: 数组下标越界

       at Hello.test(Hello.java:53)

       at Hello.main(Hello.java:31)

大家注意throws和throw是两个不同的关键字。throws出现在方法的声明中,表示该方法可能会抛出的异常,然后交给上层调用它的方法程序处理,并且允许throws后面跟着多个异常类型;throws表示出现异常的一种可能性,并不一定会发生这些异常;而throw则一定抛出了某种异常对象。

最后我们来介绍一下“finally”关键字。try关键字单独无法使用,必须配合catch或者finally使用。Java编译器允许的组合使用形式只有以下三种形式:

try...catch...;       try....finally......;    try....catch...finally...

当然catch块可以有多个,但try块只能有一个,finally块是可选的(但是最多只能有一个finally块)。三个块执行的顺序为try—>catch—>finally。当然如果没有发生异常,则catch块不会执行。但是finally块无论在什么情况下都是会执行的。

// finally的使用
try {
	
	String str = "abc";
	//String str = "123";
	int num = Integer.parseInt(str);
	
} catch (NumberFormatException e) {
	e.printStackTrace();
} finally {
	System.out.println("肯定会执行的代码");
}

我们通过运行代码发现,不管是否发生异常,finally代码块都会执行。

除了Java定义好的异常类外,在开发过程中根据业务的异常情况自定义异常类。如果要自定义异常类,则继承Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则继承自RuntimeException。两者的区别在于,前者必须使用try-catch进行捕获,后者可以不做处理。关于如何自定义异常类,我们就不再详细叙述了,因为我们很少这样做。为什么?因为异常的捕获仅仅是防患于未然,我们真正关心的是,发生异常后如何让程序继续运行,且能够纠正因异常而造成的数据错误。比如说,除数不能为零的异常,我们正确的做法应该是优先对除数进行判零的操作,而不是通过发生异常后再去处理。在比如说,数组下标越界的异常,我们通常也是优先对下标进行数组长度的判断,而不是发生异常后再去处理。异常属于错误,处理错误的方式很多种,异常并不是最好的方式,尤其是从程序健壮性角度出发考虑的话。也就是说,只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程。而且通常情况下,发生异常后,我们都会记录日志,然后通过日志去分析异常的发生场景,进而再研究出解决方案,最后给程序打补丁。

最后我们来介绍java程序中几种常见的异常:

1、java.lang.NullpointerException(空引用异常)

这个异常经常遇到,异常的原因是程序中的对象值为null,也就是说它并没有分配到内存空间。该异常经常出现在调用对象方法,或者访问对象属性的时候,但本质是没有成功创建对象,因此就不可能成功调用其方法或访问其属性了。

// NullpointerException异常
Random rom = null;
//Random rom = new Random();
System.out.println("随机数:" + rom.nextInt(100));

执行上述代码的话,就会抛出NullPointerException异常,因为rom对象是空对象。

2、 java.lang. NumberFormatException(数字格式异常)

当试图将一个String类型数据转换为指定的数字类型,但该字符串不满足数值型数据的要求时,就抛出这个异常。在上面的代码示例中,我们已经说明了此问题啦。

3、java.lang.IndexOutOfBoundsException(数组下标越界异常)

调用的数组或者字符串的下标值超出了数组范围的时候,就会出现这个异常。在上面的代码示例中,我们已经说明了此问题啦。解决的办法就是对下标数值进行范围判断。

4、java.lang.ArithmeticException(数学运算异常)

当数学运算中出现了除以零这样的运算就会出这样的异常,解决办法还是对数值进行判断。

5、java.lang. ClassNotFoundException(未找到类异常)

当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误。例如调用Class.forName(类路径)去加载一个不存在的类的时候,就会出现该异常。因此我们需要去看看这个类是否存在,或者是不是类路径写错了。

6、java.lang. ClassCastException(数据类型转换异常)

当试图将一个类A的实例对象强制转换为另一个类B类型的时候,如果两个类不相同,那么就会出现这种异常。因此,解决方法就是查看该实例对象new的时候是不是类B。

7、java.lang.NoSuchMethodException(方法不存在异常)

当程序试图通过反射来创建对象,访问某个方法,但是该方法不存在就会引发异常。

8、java.lang.IllegalArgumentException(非法参数异常)

对象调用方法传递的参数类型不对的时候,就会发生该异常。因此我们要去类中检查该函数的参数是什么类型等等。

9、java.lang.IllegalAccessException(没有访问权限)

当程序要调用一个类,但当前的方法即没有对该类的访问权限便会出现这个异常。因此我们要去检查检查该方法是不是public访问控制符修饰的。

10、 java.lang.FileNotFoundException(文件未找到异常)

当程序打开一个不存在的文件来进行读写时将会引发该异常,因此要检查文件是否存在。

11、 java.lang.EOFException(文件已结束异常)

当从文件读取数据到达文件末尾时,就会引发此异常,因此我们要检查文件的内容是不是完整的,我们想要读取的数据是否真在在文件中存在。

12、java.lang.CloneNotSupportedException (不支持克隆异常)

当没有实现Cloneable接口就调用clone()方法则抛出该异常。

13、java.lang.OutOfMemoryException (内存不足错误)

当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。在解决java内存溢出问题之前,需要对java虚拟机的内存管理有一定的认识。java虚拟机的内存包括三种不同的区域:静态区域、堆区域、栈区域。静态区域主要存储类和方法的信息。堆区域用来存放Class的实例(即对象)。每次用new创建一个对象实例后,对象实例存储在堆区域中。栈区域主要基本类型变量以及方法的输入输出参数。Java堆区域是Java虚拟机所管理的内存最大的一块,也是垃圾收集器管理的主要区域,容易发生内存溢出也是该区域。我们可以通过参数Xms(初始堆大小)和Xmx(最大堆大小)来指定Java虚拟机的堆区域大小,这两个参数是在我们使用Java命令运行字节码文件的时候传递的,如下所示:

Java  -Xmx128m  -Xms64m  Test

如果是在Eclipse中,我们可以右键单击工程(Hello),然后选择properties->Run/Debug Settings,然后在右边点击我们的工程Hello,然后点击Edit按钮,然后在新弹框中选择第二项(Arguments),然后我们就可以看到“VM arguments”输入框啦,在输入框中输入我们的参数设置“-Xmx128m  -Xms64m”,最后点击“OK”就搞定了。截图如下:

另外JVM最大内存首先取决于实际的物理内存和操作系统,具体不再细述了。

本课程涉及的代码可以免费下载:
https://download.csdn.net/download/richieandndsc/85645935

今天的内容就讲的这里,我们来总结一下。今天我们主要讲了Java的异常类。这里只需要大家了解两种异常,一种是检查性的,我们必须进行异常捕获处理,另一种是非检查性的,我们可以不用处理。但是,不管那种,我们都需要保证程序的健壮性。好的,谢谢大家的收看,欢迎大家在下方留言,我也会及时回复大家的留言的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值