先想想,什么是正常?一个程序从头跑到尾,所有的步骤都是按照我们的预期运行的,这就是正常。
异常,自然就是和正常不同了啊。简单来说,异常就是程序运行过程中出现了问题,或者出现了和我们预期不同的情况。
因此,我们可以认为,异常就是程序运行过程中出现的错误。
这种情况很多,例如我们设计了一个计算器,用户用它来计算“10/0”;或者我们设计了一个读取并分析文件的程序,用户却输错了文件名,这就要求程序打开一个不存在的文件;或者我们需要通过网络发送文件此时网络却断开了,我们需要写入日志信息磁盘却没有可用空间了……
可以说,异常情况非常多,要写出一个错误少、运行稳定的程序,就需要考虑到各种异常情况并加以处理。在以前,这是非常考验开发人员的经验和细心程度的。
Java为了提高开发效率,设计了异常处理机制。在Java的异常处理机制中,所有的异常情况都被封装到对象中。当异常发生时,JVM就会生成对应的异常对象——一般称之为“抛出异常对象”,或者“抛出异常”,同时暂停程序的执行。如果我们为这种情况编写了合适的代码——一般称为“异常处理”——这些异常对象就会被捕获并进行相应的处理,处理完毕后,继续执行程序。
这个机制有点像应急预案:我们先针对某些问题设计一个处理方案,异常对象类似于一种信号,当这个信号出现时,就意味着某个问题发生了,然后就可以启动相应的预案,待处理完毕后继续后面的操作。
看到这里,可能有人会问:如果我们没有预料到某个问题,也就没有制定对应的处理方案,怎么办?这个时候,JVM会把这个异常向上级代码(比如某段代码发生了异常,这段代码位于方法A中,方法A被方法B调用,方法A就是这段代码的上级,方法B则是方法A的上级)逐级上报,如果上级代码中有相应的处理方案,就进行处理;如果没有任何处理方案,那么程序到此暂停执行,并把异常信息交给用户,由用户进行下一步处理。
Java中,表示异常的类都实现了Throwable
接口,或者说,所有的异常类都是Throwable
接口的子类。Throwable
接口有两个直接子类,一是Error
类,二是Exception
类。Error
,翻译为“错误”,代表那些程序自身无法处理的问题,例如资源耗尽、JVM崩溃等;Exception
,翻译为“异常”,代表那些不太严重、程序自身可以处理的问题,例如除0操作、文件名错误等。Java中的异常处理机制针对的就是Exception
对象代表的各类问题。
Exception
类的子类分为两大类,一是RuntimException
类及其子类,这类异常只有在运行时才能确定是否会发生,即使不处理编译也能通过,称为运行时异常;二是其它子类,这些异常如果不被处理,编译时会报错,称为编译时异常。
常见的运行时异常包括:ArithmeticException
(算数异常),IndexOutOfBoundsException
(下标越界异常),ClassCastException
(类型转换异常),NullPointException
(空指针异常),NumberFormatException
(数字格式化异常)等等。
在Java中处理异常也很简单,就是一个套路:
try{
可能产生异常的代码;
}catch(异常对象){
发生异常时的处理代码;
}finally{
无论是否发生异常都会执行的代码;
}
try语句块中包含可能会发生异常的代码,当异常发生时,JVM会暂停程序执行,将异常封装为对象,该异常对象会被catch语句所捕获,catch语句会将捕获到的异常对象和括号中的异常对象的类型进行比对,如果实际发生的异常属于catch括号中的异常类型或是其子类型,就执行catch语句块中的处理代码;如果写了多个catch语句,就会从上到下依次比对,然后执行第一个匹配的类型,如果没有相匹配的类型,则将异常抛出。
finally部分是可选的,无论异常是否发生,其中的代码都会执行。
为了便于理解,来看一个例子:
import java.util.Scanner;
public class ExceptionDemo01 {
public static void main(String[] args) {
int a,b;
Scanner sc=new Scanner(System.in);
System.out.println("请输入被除数:");
a=sc.nextInt();
System.out.println("请输入除数:");
b=sc.nextInt();
try{
System.out.println("a/b="+(a/b));
}catch (ArithmeticException e){
System.out.println("发生了除以0的情况,除数不能为0");
//输出异常信息
//e.printStackTrace();
}
}
}
在这个程序中,用户先输入被除数和除数,然后让两者相除,如果发生异常,JVM会将其封装成一个ArithmeticException
类型的对象,当catch语句捕捉到这个异常对象时,会和括号中的异常对象比对,如果两者属于同一个类,或抛出的异常对象属于括号中异常类的子类,那么就可以通过对象e
来引用这个抛出的异常对象了(回忆一下多态的内容)。之后就可以在catch语句块中进行相对应的处理了。
这个程序中提供了两种处理方式,一是提醒输入错误,二是注释部分,显示异常的相关信息。
但这两种方法都没有什么用处,因为不管是提示输入错误,还是显示异常信息,程序都结束了,用户也没有机会改正——从结果的角度看,这种方式和不进行异常处理区别不大。
所以,我们应该让用户改错的机会,从而保证程序的功能最终可以实现。
我们来看看改进版的程序:
import java.util.Scanner;
public class ExceptionDemo02 {
public static void main(String[] args) {
int a, b;
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入被除数:");
a = sc.nextInt();
System.out.println("请输入除数:");
b = sc.nextInt();
try {
System.out.println("a/b=" + (a / b));
//如果不发生异常,就会中止循环
break;
} catch (ArithmeticException e) {
System.out.println("发生了除以0的情况,请重新输入");
} finally {
System.out.println("----------END----------");
}
}
}
}
在这个程序中,我们把相关代码放到while循环中,而且循环条件是true
,这就意味着程序会一直执行——除非没有发生异常,会执行break;
语句,中止循环。
而且,我们增加了finally
语句部分。在执行时我们会发现,无论异常是否会发生,finally语句中的代码都会执行。
这里留给大家一个问题:如果用户输入错误,程序也会输出----------END----------
,然后让用户重新输入数据,这和我们的使用习惯不相符,请大家思考一下,如何才能保证发生异常时不会输出----------END----------
,不发生异常时才会输出呢?
可能已经有人发现了,刚才的例子其实还有一个缺陷,如果用户输入的除数不是0
而是零
或者zero
怎么办?
在这个例子中,用户输入的数据其实是字符串,然后转换为了数字。字符串"0"转换成数字0
倒也自然,“零”怎么转换呢?除非我们不用标准库,而是另外写程序……太麻烦了,而且要考虑的问题太多了,最重要的是,只要用户稍微注意一点,就可以避免这些问题。所以我们还是让用户输入阿拉伯数字吧,这时只要防止用户有意无意输入非阿拉伯数字形式的信息即可。
如果用户输入信息的形式错了,就会产生InputMismatchException
类型的异常,我们需要对它进行捕捉和处理。刚才例子可以修改为:
import java.util.InputMismatchException;
import java.util.Scanner;
public class ExceptionDemo03 {
public static void main(String[] args) {
int a, b;
Scanner sc = new Scanner(System.in);
while (true) {
try {
System.out.println("请输入被除数:");
a = sc.nextInt();
System.out.println("请输入除数:");
b = sc.nextInt();
System.out.println("a/b=" + (a / b));
//如果不发生异常,就会中止循环
break;
} catch (ArithmeticException e) {
System.out.println("发生了除以0的情况,请重新输入");
}catch (InputMismatchException e){
//将错误形式的信息清理出缓冲区
sc.nextLine();
System.out.println("您输入的数字格式有误,请重新输入");
}finally {
System.out.println("----------END----------");
}
}
}
}
在这个程序中,我们把负责用户输入信息的代码也放倒了try语句块中,这样就可以处理输入时产生的异常了。此时try语句块中的代码可能会产生两种异常,ArithmeticException
和InputMismatchException
类型,我们就在try的后面针对两种异常写两个catch语句即可。在处理InputMismatchException
类型异常时,我们加了一行sc.nextLine();
,这是由于:用户输入的数据会先保存在缓冲区中,然后由程序取出数据并进行处理;如果用户输错了数据,由于程序无法处理,那么原来的错误数据就会留在缓冲区中,到下一次循环时,程序读取到的还是错误的数据,这就阻止我们继续输入,所以要使用这一行代码把错误数据读出缓冲区。
还记得我们之前做过一个猜数字的游戏吗?当时的程序没有考虑用户会输入非数字的数据,但在现实中这种问题时有发生,大家可以思考一下, 如何修改当时的程序,使我们的猜数字游戏能够避免用户输入错误格式的数据呢?
需要注意,发生异常时,JVM会从上至下依次检查,看看哪个catch语句能处理当前的异常,如果找到了,那就直接使用,而不再考虑后面有没有更合适的。假设现在有个程序,从上到下写了三个catch语句,分别处理A,B和C三种异常,其中A是B的父类,B是C的父类。如果发生了A类异常,毫无疑问是由第一个catch语句来处理,那如果发生了B类或者C类异常呢?也是第一个catch语句来处理,因为A是B和C的父类啊。
就好像,你是一个马戏团老板,你跟你的驯兽师说:如果动物逃跑了,一枪崩了就是;如果是猫跑了,你拿出小鱼干引诱一下就回来了;如果是狗跑了,你扔块肉出去它就回来了。之后狗跑了,驯兽师拿出你的“应急预案”,看到第一句:“如果动物逃跑了,一枪崩了就是”,然后掏出枪把狗打死了……
这就是Java的异常处理机制,只要能找到一个处理方案,绝对不会去看后面有没有更好的处理方案。所以,在进行异常处理时,越是具体的异常处理,越是要放到前面,而那些具体异常类的父类,尽量放到后面,辈分越高越往后面放。
catch(Exception e)
这种异常处理代码,不然此后所有的catch语句都会失效,除非这是最后一个catch语句。04
这两个词看起来很像,含义也差不多:都是“抛出”的意思。不过应用场合不同。
throws
用于方法,允许方法不处理那些必须处理的编译时异常,而将这些异常交给调用该方法的代码处理。使用方法是:方法返回值 方法名(参数列表) throws 异常类1,异常类2...{方法体}
。
throw
用于手工抛出一个指定类型的异常——即使此时并没有真的异常发生也可以抛出。使用方法是:throw new 异常类构造方法;
例如:
import java.util.InputMismatchException;
import java.util.Scanner;
public class ExceptionDemo04 {
public static double divide(double a,double b){
if(b==0){
//如果除数为0,抛出ArithmeticException类型的异常
throw new ArithmeticException();
}
return a/b;
}
//本方法在运行时,可能会出现ArithmeticException, InputMismatchException两种异常,本方法不予处理,抛出
public static double inputDevide() throws ArithmeticException, InputMismatchException {
double a,b;
Scanner sc=new Scanner(System.in);
System.out.println("请输入被除数:");
a = sc.nextDouble();
System.out.println("请输入除数:");
b = sc.nextDouble();
return divide(a,b);
}
public static void main(String[] args) {
double result=inputDevide();
System.out.println(result);
}
}
我们在divide()
方法中,抛出了一个ArithmeticException
异常——这是强制抛出的,因为实数相除时,除数为0,结果为Infinity,无限大,所以这里时强制抛出的。在inputDevide()
方法中,可能会产生InputMismatchException
异常,但不想在这个方法中处理,于是使用throws
关键字将其抛出,由于这个方法中也调用了divide()
方法,可能产生ArithmeticException
异常,也可以一同抛出。抛出多个异常时,这些异常类用逗号分隔开。如果这些异常类具有相同的父类,也可以只抛出其父类,例如本例中inputDevide()
方法可以这样抛出异常:throws Exception
。
在主方法中,调用了inputDevide()
方法,就需要在主方法中处理ArithmeticException
和InputMismatchException
异常。不过由于这两个异常都是运行时异常,所以Java编译器并没有强制要求处理,如果这些异常中包含编译时异常,编译器就会强制要求处理异常。
Java的标准库中提供了大量异常类,可以满足我们的大部分需要,但有时也需要我们自定义异常类。自定义异常的一般方法是:
super
关键字调用父类构造方法;throw
关键字抛出异常。我们来看一个例子:
//自定义异常类
class DivideByZeroException extends Exception{
public DivideByZeroException(){
super();
}
//如果希望提供异常信息,可以设计带有异常信息的构造方法
public DivideByZeroException(String message) {
super(message);
}
}
public class ExceptionDemo05 {
public static double divide(double a,double b) throws DivideByZeroException{
if(b==0){
//DivideByZeroException是编译时异常,必须进行处理,这里通过throws抛出
throw new DivideByZeroException("除数为0,不能正常计算!");
}
return a/b;
}
public static void main(String[] args) {
try {
System.out.println(divide(25.6,0));
} catch (DivideByZeroException e) {
//输出异常信息
System.out.println("异常信息是:"+e.getMessage());
}
}
}
由于DivideByZeroException
类继承自Exception
类,它是一个编译时异常,所以在出现这个异常时,要么使用try…catch语句进行异常处理,要么在所属方法中使用throws
抛出。但必须要在某段代码中对其进行处理,否则最终会抛给用户——也就是程序会在异常发生时退出。
04