写程序错误在所难免,对错误的控制一直是编程人员要解决的一大问题,如果有一种机制能够帮助我们在写程序的时候就规避可能发生的错误,这样就可能提高编程的效率啦。
由于很难设计出一套完美的错误控制方案,许多语言干脆将问题简单地忽略掉,将其转嫁给库设计人员。对大多数错误控制方案来说,最主要的一个问题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。《java编程思想》
java中的异常控制是java语言的强大之处的体现之一。
在很多编程语言中经常将“异常控制”模块内置到程序设计语言本身,有时甚至内建到操作系统内,但是有时并不会强制用户(程序员)使用,即不会在一些可能发生异常的情况下强制我们处理它。但是java与大多数编程语言不同的是它有时候将强制我们处理它们(异常),处理的方式是抛出和捕获都行。而且就算我们不处理发生的异常,最后JVM也会帮我们处理(即中断程序的运行,将异常发生的路径的打印给我们看)。
java是“一切皆对象”,同样的java中的异常(Exception)体系是由各种各样的类构成的。通常的异常的根类是java.lang.Throwable, 在其下面有两个直接的子类:java.lang.Error与java.lang.Exception, 而我们平常所说的异常指的就是Exception及其子类。至于Error是属于系统内部出现的问题,例如栈溢出,我们一般也处理不了,不过多讲述。要注意的是异常类并不是只在java.lang包中,还可能出现在你自定义的包中,引人的jar文件中。
在编程过程中我们可以不用处理的,交给JVM处理即可,当然JVM的处理方式是中断程序。这类异常一般都是运行时期的异常(RuntimeException),即在程序运行时才可能出现,而且一般这种情况是我们自己造成的,比如说数组越界啦,空指针的引用啦。
在编程过程中必须要处理的,处理的方式可以是“抛出”或者是“捕获”。如果是“抛出”的话最后可以抛出给JVM去处理(仍然是中断处理),“捕获”的话就是我们自己来手动处理。这类异常也称为编译异常。即在编译时必须处理,如果不处理,编译不能通过,就跟我们的语法错误一样,编译器会给我们报错。
对于需要处理的异常,在IDEA中会给我们两种选择处理的方式(对于这两种方式更进一步的还会在下面讲到):
上面使用橙色标出来的指的是必检异常,绿色指的是免检异常。
由上图可知,异常体系的基类是Exception,而Exception又是继承Throwable,其实查看各种异常类的源码可以发现,各种异常类中基本就只有构造方法和一个序列号ID,而构造方法里面又只调用父类的构造方法,这样沿着继承链反向查看回去,会发现最终调用的还是Throwable中的构造方法。
里面的构造方法:(IDEA里面鼠标移上按住Crtl+单击就能进去看了)
//NullPointerException类
public NullPointerException() {
super();
}
public NullPointerException(String s) {
super(s);
}
//最终的Throwable类的构造方法
public Throwable() {
fillInStackTrace(); //此方法记录此Throwable对象信息,了解当前线程的堆栈帧的当前状态。
}
public Throwable(String message) {
fillInStackTrace();
detailMessage = message;
}
下面举一个数组越界异常(ArrayIndexOfBoundsException)来看看异常产生的大概过程。
public static int getElement(int[] nums, int index) {
return nums[index];
}
public static void main(String[] args){
int[] nums = {1, 2, 3, 4, 5};
System.out.println(getElement(nums, 5));
}
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at com.smrobot.exception.demo1.getElement(demo1.java:47)
at com.smrobot.exception.demo1.main(demo1.java:13)
由上面也可以看出ArrayIndexOfBoundsException是一个在代码运行期间才可能会发生的异常(运行期异常),而让这个异常产生的原因往往是因为我们自己的原因,例如这个例子中传入了越界的下标。
System.out.println(getElement(nums, 5));
public static int getElement(int[] nums, int index) {
return nums[index]; // JVM 会在这里做这么一个操作:throw new ArrayIndexOfBoundsException(5);
}
当我们写一个方法时,为了确保调用者传递的参数符合我们的想要的规范,以致于能让程序健壮运行,这个时候我们就可以使用抛出异常的方式来对传递的参数进行提前判断,如果不符合要求,即可抛出一个异常让调用者知道。而java中抛出异常的关键字就是throw。
使用格式如下:
throw new 异常类(参数);
举例:Objects类中的非空判断
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
类似的java的源码中经常可以看到,提前对传递的参数进行判断,增强代码的健壮性。
当程序执行到了throw语句时,就会停止继续执行下去,而将对应的异常“返回”给该方法的调用者。(可以将之看成是类似return的效果,即"return"一个异常对象)
那么抛出的异常可以怎么样处理呢?一种是用throws声明抛出的异常,让调用者处理;一种是自己用try…catch处理,而不进行抛出了。
由于抛出的异常一般是将该异常从一个方法中抛出去(main()方法也是一样的),所以声明抛出的异常也是写在方法的定义中,就跟声明函数的返回值一样。
使用格式如下:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
举例:
public class ThrowsDemo2 {
public static void main(String[] args) throws IOException {
read("a.txt");
}
public static void read(String path) throws FileNotFoundException, IOException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 如果文件名不是a.txt就认为文件不存在
throw new FileNotFoundException("文件不存在");
}
if (!path.equals("b.txt")) {
//文件不为b.txt就抛出IOException,只是举例用
throw new IOException();
}
}
}
对于方法中抛出的必检异常(编译异常),如果方法内部本身没有处理,则必须使用throws声明,提示调用者去处理,不处理在IDEA中就会标红,并且不能编译通过。而对于运行时期的异常,不进行声明也是可以的,最后JVM会帮助我们进行中断处理。
前面的throw和throws并不会真正让我们自己处理异常发生时要进行的操作,如果我们不处理,最后就是交给JVM中断程序运行的方式来处理了。try…catch就是可以让我们捕获处理异常,不至于让程序中断。
使用语法:
try {
//可能出现异常的代码
} catch(异常类型1 e) {
//处理异常的代码
//常用的有记录日志,打印异常信息,继续抛出异常给调用者处理
//详细方法的调用可以看上面的:Throwable中常用的方法
}
捕获多个异常时的注意事项
try {
} catch(异常类型1 e) {
} catch(异常类型2 e) {
}...
注意,在这种处理方式中,如果存在子父类异常,那么需要将子类异常声明在上面,父类异常声明在下面。例如,ArrayIndexOutOfBoundsException就是IndexOutOfBoundsException的子类(具体可查看API或源码),因此使用上面的方式分别捕获这两种异常的时候需要类似于下面的书写方式
try {
} catch(ArrayIndexOutOfBoundsException e) {
//子类异常放在上面
} catch(IndexOutOfBoundsException e) {
//父类异常放在下面
}
也可以直接使用一个最大的异常对象Exception进行捕获,这样就不用写太多的catch代码块了
try {
} (Exception e) {
//使用异常的基类来捕获,这样所有可能发生的异常都会被捕获到,当然捕获到是最新发生的异常
}
finally代码块是和try…catch一起使用,并在放到最后面来使用,常用来关闭各种资源,例如IO接口的关闭啦,数据库连接的关闭啦,同步锁的关闭啦…
finally可以保证不管程序是否出现异常了,相关的资源都可以被关闭掉,因为finally代码块是一定会执行的。
使用语法如下:
try {
//编写可能出现异常的代码
} catch (Exception e) {
//处理异常
} finally {
//关闭已打开的资源
}
注意:只要当在try或者catch代码块中调用了退出JVM的方法(System.exit(0))时,finally代码块才不会执行,不然都会执行的;同时,即使在try或者catch中return语句,finally中的代码也是会被执行的;如果finally有return语句,那么程序最终返回的是finally代码块中return语句的内容。
我们不仅可以用java中已经帮我们定义好的异常,也可以自定义属于自己的异常提示,来满足自己的业务需求,例如考试成绩是负数的异常啦,年龄是负数的异常啦。
自定义异常的方式如下:
举例如下:
// 自己自定义的业务异常 -- 登录异常
public class LoginException extends Exception {
//给个序列化ID
private static final long serialVersionUID = -5116101128118950844L;
/**
* 空参构造,调用父类空参构造
*/
public LoginException() {
super();
}
/**
* 带参构造,可传入自定义的提示信息
*/
public LoginException(String message) {
super(message);
}
} //可以仿照JDK中各种异常类的定义方法
自定义的异常的使用方法和使用JDK中的异常方式是一样的,只不过自定义的异常更加符合自己想要达到的效果罢辽~