引言:通过前面章节的学习,我们对面向对象相关内容已经有了深刻的理解,本章节我们来学习一个在实际开发中使用频繁的内容-异常处理。
一、异常:
1.什么是异常?
异常 是指在程序的运行过程中所发生的不正常的事件,它会中断正在运行的程序。
程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?
Java提供了更加优秀的解决办法:异常处理机制。异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。
Throwable类是Java异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。
2.异常体系:
Java标准裤内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出Error类和Exception类。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
总体上我们根据Javac对异常的处理要求,将异常类分为2类。
非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。
检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。
需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了,异常体系如下:
3.为什么使用异常处理机制?
我们来看生活的一个例子,正常情况下,小王每日开车上班,一路畅通,耗时30分钟,但是如果路上遇上堵车,撞车等突发事件时,小王就不能到达公司,堵车及撞车这种突发事件不是一定发生的,但是是有可能发生,并且一旦发生,小王就不能及时到达公司。在java程序中,也会有这种情况,有些问题可能发生,可能不发生,但是一旦发生就会导致程序无法运行,如下列代码:
package cn.demo1;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d", num1,
num2, num1/ num2));
System.out.println("感谢使用本程序!");
}
}
当输入除数和被除数合理正确时,程序执行无问题。但是一旦我们输入有误,大家都知道数学上除数不能为0,如果我们在输入除数时输入为0,则上述代码会出现错误结果如下:
到这大家可能已经对异常有一个明确的认识了,那么我们如何避免异常对程序的影响呢?当然我们可以通过基本的if…else进行相应判定,代码如下:
package cn.demo1;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = 0;
if (in.hasNextInt()) { // 如果输入的除数是整数
num2 = in.nextInt();
if (0 == num2) { // 如果输入的除数是0
System.err.println("输入的除数是0,程序退出。");
System.exit(1);
}
} else { // 如果输入的除数不是整数
System.err.println("输入的除数不是整数,程序退出。");
System.exit(1);
}
}
}
但是大家通过代码可以看出这种解决方式弊端很多:
- 代码臃肿
- 程序员要花很大精力“堵漏洞”
- 程序员很难堵住所有“漏洞”
如何解决这个问题呢,java提供了一套优秀的异常处理机制,这样程序员可以将大部分精力放在业务逻辑及核心的处理上,而不用花费过多精力在一些琐碎上。
二、异常处理机制:
Java的异常处理是通过5个关键字来实现的:try、catch、 finally、throw、throws。
- try:执行可能产生异常的代码
- catch:捕获异常
- finally:无论是否发生异常,代码执行内容
- throw:手动抛出异常
- throws:声明异常
那么接下来我们就继承异常处理进行具体讲解:
1.try-catch:
首先我们来看第一种,将可能发生异常的相关代码放入try块中,通过catch块进行捕获相应异常,并作出相应处理,例如上述例子使用try-catch处理代码如下:
package cn.demo1;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
try {
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d",
num1, num2, num1/ num2));
System.out.println("感谢使用本程序!");
} catch (Exception e) {
System.err.println("出现错误:被除数和除数必须是整数," +
"除数不能为零。");
e.printStackTrace();
}
}
}
该段代码可能有两种异常,除数为0 时异常,输入不为整数时异常,所以上诉代码通过异常处理后有三种情况:
第一种:try执行代码无问题,则代码直接正常执行完成。
第二种: try执行代码有问题,则程序进入catch块,catch块执行完相应的异常处理以后,程序继续执行。
第三种: try执行代码有问题,捕获异常,异常类型不匹配,程序中断。
很明显我们的第三种情况仍然没有解决问题,如何进一步处理呢,可以使用多重catch块。
2.多重catch块:
多重catch块主要用于一段代码中含有多种异常,需要分别对不同异常进行相应处理,如上述代码还有两种异常,我们想分别处理,代码如下:
package cn.demo1;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
try {
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d",
num1, num2, num1/ num2));
} catch (InputMismatchException e) {
System.err.println("被除数和除数必须是整数。");
} catch (ArithmeticException e) {
System.err.println("除数不能为零。");
} catch (Exception e) {
System.err.println("其他未知异常。");
}
}
}
这样我们可以将代码中所有可能出现的异常彻底进行处理,实际开发中很多时候也会进行多重catch处理,但是注意在多重catch块处理时,一定注意捕获的异常类型从上到下可以是不同类型,也可以是后面类型包含起前面类型,但是不允许前面捕获异常类型范围比后面捕获异常类型大。
3.try-catch-finally:
通过try-catch我们可以实现基本的异常处理,实际中还有一种情况我们需要考虑,如果try中有异常会导致程序中断,但是有些场合不管我们程序是否有异常我们都要执行后续操作,如在对数据库操作过程中,不管我们程序执行是否有问题,最终我们都应该将数据库操作对象进行相应关闭,那么如何解决这个问题呢,java中提供了另一个关键字finally最终执行来解决这个问题,代码如下:
package cn.demo1;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
try {
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d",
num1, num2, num1/ num2));
} catch (InputMismatchException e) {
System.err.println("被除数和除数必须是整数。");
} catch (ArithmeticException e) {
System.err.println("除数不能为零。");
} catch (Exception e) {
System.err.println("其他未知异常。");
}finally{
System.out.println("最终执行代码");
}
}
}
注意:程序中含有return代码仍然会执行finally块,当且仅当System.exit(1)不执行fianlly块。
如下代码,程序仍然执行fianlly,不会结束:
package cn.demo1;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
try {
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d",
num1, num2, num1/ num2));
return; //finally语句块仍旧会执行
} catch (Exception e) {
System.err.println("出现错误:被除数和除数必须是整数," +
"除数不能为零");
return; //finally语句块仍旧会执行
} finally {
System.out.println("最终执行代码!");
}
}
}
当且仅当以下情况fianlly不执行,代码如下:
package cn.demo1;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
try {
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d",
num1, num2, num1/ num2));
return; //finally语句块仍旧会执行
} catch (Exception e) {
System.err.println("出现错误:被除数和除数必须是整数," +
"除数不能为零");
System.exit(1); //程序结束,不再执行finally
} finally {
System.out.println("最终执行代码!");
}
}
}
4.throws:
上述我们具体讲解了异常处理机制中的几种情况,每一种情况都是在捕获到异常时直接进行处理,但是在实际开发中可能会有这样的情况,捕获到异常以后不想现在处理,想后期方法调用处理(分层以后,各自做各自的事),或知道该处有异常,但是无法进行具体处理,如编写接口,我们知道该处有异常,但是该异常只能在后续的实现类中进行具体处理,这种情况如何处理呢?java提供了关键字throws声明异常,后续进行处理代码如下:
package cn.demo1;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
/**
* 输入被除数和除数,计算商并输出。
* @throws Exception 声明异常
*/
public static void divide() throws Exception {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d",
num1, num2, num1/ num2));
}
/**
* 通过try-catch捕获并处理异常。
* @param args
*/
public static void main(String[] args) {
try {
divide();
} catch (Exception e) {
System.err.println("出现错误:被除数和除数必须是整数,"
+ "除数不能为零");
e.printStackTrace();
}
}
}
当然后续调用者如果仍然不想进行异常处理,同样可以继续进行省略,由更后续者进行处理,如果最后都没有进行处理,则由java虚拟机进行处理。代码如下:
package cn.demo1;
import java.util.Scanner;
/**
* @author hz
* @version 1.0
*/
public class Demo1 {
/**
* 输入被除数和除数,计算商并输出。
* @throws Exception 声明异常
*/
public static void divide() throws Exception {
Scanner in = new Scanner(System.in);
System.out.print("请输入被除数:");
int num1 = in.nextInt();
System.out.print("请输入除数:");
int num2 = in.nextInt();
System.out.println(String.format("%d / %d = %d",
num1, num2, num1/ num2));
}
/**
* 通过try-catch捕获并处理异常。
* @param args
*/
public static void main(String[] args) throws Exception {
divide();
}
}
注意:不管是后续执行异常处理还是继续执行异常声明,后续操作异常类型可以和前面异常类型相似或比前面异常类型范围大。
5.throw:
之前不管是异常的处理还是异常的声明,所有的异常均是由系统抛出,如果现在程序在执行过程中,除了系统自动抛出异常外,有些问题需要程序员自行抛出异常,在这个时候就需要我们使用throw进行抛出.如我们人的性别正常只有"男"和"女"两种,当输入的内容不是这两种时,则程序抛出异常,如何实现呢?代码如下:
package cn.demo1;
/**
* 使用throw在方法内抛出异常。
* @author hz
*/
public class Person {
private String name = "";// 姓名
private int age = 0;// 年龄
private String sex = "男";// 性别
/**
* 设置性别。
* @param sex 性别
* @throws Exception
*/
public void setSex(String sex) throws Exception {
if ("男".equals(sex) || "女".equals(sex))
this.sex = sex;
else {
throw new Exception("性别必须是“男”或者“女”!");
}
}
/**
* 打印基本信息。
*/
public void print() {
System.out.println(this.name + "(" + this.sex
+ "," + this.age + "岁)");
}
}
package cn.demo1;
/**
* 捕获throw抛出的异常。
* @author hz
*/
public class Demo1 {
public static void main(String[] args) {
Person person = new Person();
try {
person.setSex("Male");
person.print();
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、常见异常分析:
1. java.lang.NullpointerException(空指针异常):
原因:这个异常经常遇到,异常的原因是程序中有空指针,即程序中调用了未经初始化的对象或者是不存在的对象。经常出现在创建对象,调用数组这些代码中,比如对象未经初始化,或者图片创建时的路径错误等等。对数组代码中出现空指针,是把数组的初始化和数组元素的初始化搞混淆了。数组的初始化是对数组分配空间,而数组元素的初始化,是给数组中的元素赋初始值。
2. java.lang.ClassNotFoundException(指定的类不存在)
原因:当试图将一个String类型数据转换为指定的数字类型,但该字符串不满足数值型数据的要求时,就抛出这个异常。例如将String类型的数据"123456"转换为数值型数据时,是可以转换的的。但是如果String类型的数据中包含了非数字型的字符,如123*56,此时转换为数值型时就会出现异常。系统就会捕捉到这个异常,并进行处理
3. java.lang.ClassNotFoundExceptio(指定的类不存在)
原因:是因为类的名称和路径不正确,通常都是程序试图通过字符串来加载某个类的时候可能会引发异常。例如:
调用Class.forName()、或者调用ClassLoad的finaSystemClass()、或者是LoadClass()时出现异常
4. java.lang.IndexOutOfBoundsException(数组下标越界异常)
原因:查看程序中调用的数组或者字符串的下标值是不是超出了数组的范围,一般来说,显示调用数组不太容易出这样的错,但隐式调用就有可能出错了,还有一种情况,是程序中定义的数组的长度是通过某些特定方法决定的,不是事先声明的,这个时候可以先查看一下数组的length,以免出现这个异常。
5. java.lang.IllegalArgumentException(方法的参数错误)
例如g.setColor(int red,int green,int blue)这个方法中的三个值,如果有超过255的会出现这个异常,如果程
序中存在这个异常,就要去检查一下方法调用中的参数传递或参数值是不是有错
6. java.lang.IllegalAccessException(没有访问权限)
当程序要调用一个类,但当前的方法即没有对该类的访问权限便会出现这个异常。如果程序中用了Package的情况下有可能出现这个异常
7. ava.lang.ArithmeticException(数学运算异常)
当数学运算中出现了除以零这样的运算就会出这样的异常。
8. java.lang.ClassCastException(数据类型转换异常)
当试图将对某个对象强制执行向下转换,但该对象又不可转换或又不可转换为其子类的实例时将出现该异常
9. java.lang.FileNotFoundException(文件未找到异常)
当程序打开一个不存在的文件来进行读写时将会引发该异常。该异常由FileInputStream,FileOutputStream,
RandomAccessFile的构造器声明抛出,即使被操作的文件存在,但是由于某些原因不可访问,比如打开一个
只有只读权限的文件并向其中写入数据,以上构造方法依然会引发异常
10. java.lang.ArrayStoreException(数组存储异常)
当试图将类型为不兼容类型的对象存入一个Object[]数组时将引发异常
11. java.lang.NoSuchMethodException(方法不存在异常)
当程序试图通过反射来创建对象,访问(修改或读取)某个方法,但是该方法不存在就会引发异常。
12. java.lang.EOFException(文件已结束异常)
当程序在输入的过程中遇到文件或流的结尾时,引发异常。因此该异常用于检查是否达到文件或流的结尾
13. java.lang.InstantiationException(实例化异常)
当试图通过Class的newInstance()方法创建某个类的实例,但程序无法通过该构造器来创建该对象时引发。
Class对象表示一个抽象类,接口,数组类,基本类型 。该Class表示的类没有对应的构造器。
14. java.lang.InterruptedException(被中止异常)
当某个线程处于长时间的等待、休眠或其他暂停状态,而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常。
15. java.lang.CloneNotSupportedException (不支持克隆异常)
当没有实现Cloneable接口或者不支持克隆方法时,调用其clone()方法则抛出该异常
16. java.lang.OutOfMemoryException (内存不足错误)
当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误
17. java.lang.NoClassDefFoundException (未找到类定义错误)
当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误
总结:
- Java 异常处理模型与 C++ 中异常处理模型的最大不同之处,就是在 Java 异常处理模型中引入了 try-finally 语法,它主要用于清理非内存性质的一些资源(垃圾回收机制无法处理的资源),例如,数据库连接、 Socket 关闭、文件流的关闭等。
- 所有的异常都必须从 Throwable 继承而来,不像 C++ 中那样,可以抛出任何类型的异常。因此,在 Java 的异常编程处理中,没有 C++ 中的 catch(…) 语法,而它的 catch(Throwable e) 完全可以替代 C++ 中的 catch(…) 的功能。
- 在 Java 的异常处理模型中,要求所有被抛出的异常都必须要有对应的“异常处理模块”。也即是说,如果你在程序中 throw 出一个异常,那么在你的程序中(函数中)就必须要 catch 这个异常(处理这个异常)。但是,对于 RuntimeException 和 Error 这两种类型的异常(以及它们的子类异常),却是例外的。其中, Error 表示 Java 系统中出现了一个非常严重的异常错误;而 RuntimeException 虽然是 Exception 的子类,但是它却代表了运行时异常(这是 C++ 异常处理模型中最不足的,虽然 VC 实现的异常处理模型很好)
- 如果一个函数中,它运行时可能会向上层调用者函数抛出一个异常,那么,它就必须在该函数的声明中显式的注明(采用 throws 关键字,语法与 C++ 类似)。