Java使用一种叫做“异常处理”的错误捕获机制处理程序可能出现的异常。这种处理机制的其实就是你抛我接,即当程序执行某个可能抛出异常的语句块时正好触发了异常,此时程序会把捕获到的异常“抛”出去并终止当前程序的执行,这个时候就会有专门的语句块“接”住这个异常,然后进行相应的处理。
首先,先介绍一下异常到底是什么。通常来说,一个程序需要关注的异常可能包括以下几个方面:
1)用户输入错误
2)设备错误
3)物理限制
4)代码错误(例如数组索引不合法、试图在散列表中查找一个不存在的值以及试图使用一个没有被赋值的对象等等)
java中的异常分为内置的异常类和用户自定义异常类两大类,所有的异常类都必须继承自Throwable类,其中,内置异常类的大体继承层次如下图所示
注:图片引用自忘情雨博主的博客。
从以上层次图中可以看出:
Throwable的下一层立即被分解为Error和Exception类
Error类层次结构描述了Java运行时系统内部错误和资源耗尽错误。应用程序无法抛出此种类型的对象
Exception类是程序设计时应该考虑的,这个类又被分解为RuntimeException和IOException。
RuntimeException是由程序错误导致的异常。有以下几种情况:
1)错误的类型转换2)数组访问越界3)访问null指针
IOException是程序本身没有问题,但是是由像IO这种错误导致的。
1)试图在文件尾部后面读取数据2)试图打开一个不存在的文件3)试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
非受查异常和受查异常:
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查异常,所有其他的异常称为受查异常。编译器将核查是否为所有的的受查异常类型提供了异常处理器。
然后,就是如何声明受查异常
声明形式很简单,就是
作用域修饰符 返回类型 函数名 (参数列表) throws 异常类型。
例如在标准类库中有一个提供的FileInputStream类的一个构造器:
public FileInputStream(String name) throws FileNotFoundException
如果发生异常,则代码块立即停止执行,并抛出此异常,并搜索异常处理器,以便处理此抛出的对象
并不是所有的方法都需要我们用这样的方式来声明抛出的异常,一般来说什么异常需要使用throws子句声明,需要依赖于以下四种情况:
1)调用一个受查异常的方法,例如,FileInputStream构造器。
2)程序运行过程中发现错误,并且利用throw语句抛出一个受查异常。
3)程序出现错误。
4)Java虚拟机和运行时库出现的内部错误。
另外,那些可能被他人使用的Java方法,应该根据异常规范,在方法的首部声明这个方法可能抛出的异常。
如果一个方法要抛出多个异常,则多个异常之间需要使用逗号分隔开。
需要注意的是不需要声明Java的内部错误以及一些非受查异常。
如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常共通用。
再就是如何抛出一个受查异常
抛出一个受查异常只需要throw 受查异常对象即可。
下面看具体一个小例子:
代码:
package Exception;
import java.io.EOFException;
import java.util.Scanner;
public class FileReadExceptionTest {
public static String readData(int n) throws EOFException{
if(n<1024) {
String info=n+"不足1024";
throw new EOFException(info);
}
return "正确";
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
try {
System.out.println(readData(n));
}catch(EOFException e) {
System.out.println(e);
}finally {
sc.close();
}
}
}
以下是两个测试结果:
从例子中可以看出,要抛出一个一个已经存在的异常,可以按以下步骤来进行操作:
1)找到一个合适的异常类
2)创建这个类的对象
3)将对象抛出
当然,类库中的异常不会面面俱到,用户也可以根据代码的实际情况来编写特定的异常类,但是必须都要继承最初的Throwable或其子类:下面是一个小例子:
代码:
package Exception;
import java.util.Scanner;
public class EnoughTo500Test {
public static void test(int num) throws NotEnoughTo500Exception{
if(num<500) {
String info=num+"不足500";
throw new NotEnoughTo500Exception(info);
}else {
System.out.println(num+"比500大"+(num-500));
}
}
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int n=in.nextInt();
try {
test(n);
}catch(NotEnoughTo500Exception e) {
System.out.println(e);
}finally {
in.close();
}
}
}
class NotEnoughTo500Exception extends Exception{
/**
*
*/
private static final long serialVersionUID = -241336180743859044L;
public NotEnoughTo500Exception() {}
public NotEnoughTo500Exception(String info) {
super(info);
}
}
运行结果:
然后是如何捕获异常
捕获异常也很简单,就是使用catch子句,这样一来,就得到了异常处理的一般形式:
try{
代码
}catch(异常类型 e){
处理异常
}
接下来说明一下异常的处理过程:
1)首先,如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
2)如果try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么程序将首先跳过try语句块的其余代码,然后执行catch子句中的处理代码。
3)如果try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
然后是一些特殊情况:
1)如果需要捕获多个异常,可以在catch子句后再次增加catch语句,如果多个异常的处理方式是一样的,可以在一个catch子句中声明多个可捕获异常,并在中间使用"|"分隔开。但是只有当捕获类型之间不存在继承关系的时候才可以这样做。
2)在catch中还可以再次抛出异常,这样做的目的是改变抛出异常的类型
下面是一个例子用于输出接收的int型整数:
package Exception;
import java.util.InputMismatchException;
import java.util.Scanner;
public class StringToInteger {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
try {
double x=in.nextDouble();
if(Double.compare((long)x, x)!=0) {
throw new DoubleRatherBeIntegerException("这是一个小数,并非整数");
}
if((long)x>Integer.MAX_VALUE || (long)x<Integer.MIN_VALUE) {
String info="输入的整数超出范围:范围("+Integer.MIN_VALUE+"<x<"+Integer.MAX_VALUE+")";
throw new IntegerRangeException(info);
}
System.out.println("这个整数是"+(int)x);
}catch(InputMismatchException e1) {
System.out.println(e1+":输入的不是一个数字");
}catch(IntegerRangeException e2) {
System.out.println(e2);
}catch(DoubleRatherBeIntegerException e3) {
System.out.println(e3);
}finally {
in.close();
}
}
}
class IntegerRangeException extends Exception{
/**
*
*/
private static final long serialVersionUID = 1L;
public IntegerRangeException(String info) {
super(info);
}
}
class DoubleRatherBeIntegerException extends Exception{
/**
*
*/
private static final long serialVersionUID = 1L;
public DoubleRatherBeIntegerException(String info) {
super(info);
}
}
运行结果:
我们发现,在对整数范围异常的处理和输入是浮点数的处理是一样的,所以也可以修改为如下等价形式:
package Exception;
import java.util.InputMismatchException;
import java.util.Scanner;
public class StringToInteger {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
try {
double x=in.nextDouble();
if(Double.compare((long)x, x)!=0) {
throw new DoubleRatherBeIntegerException("这是一个小数,并非整数");
}
if((long)x>Integer.MAX_VALUE || (long)x<Integer.MIN_VALUE) {
String info="输入的整数超出范围:范围("+Integer.MIN_VALUE+"<x<"+Integer.MAX_VALUE+")";
throw new IntegerRangeException(info);
}
System.out.println("这个整数是"+(int)x);
}catch(InputMismatchException e1) {
System.out.println(e1+":输入的不是一个数字");
}catch(IntegerRangeException|DoubleRatherBeIntegerException e2) {
System.out.println(e2);
}finally {
in.close();
}
}
}
class IntegerRangeException extends Exception{
/**
*
*/
private static final long serialVersionUID = 1L;
public IntegerRangeException(String info) {
super(info);
}
}
class DoubleRatherBeIntegerException extends Exception{
/**
*
*/
private static final long serialVersionUID = 1L;
public DoubleRatherBeIntegerException(String info) {
super(info);
}
}
运行结果同上一代码。
下面是一个再次抛出异常的例子:(测试x是否大于500)
package Exception;
import java.util.Scanner;
public class repeatThrowException {
//定义一个可能会抛出异常的方法
public static void test(int a) throws Exception{
try {
if(a<500) {
throw new Exception("数字不足500");
}else {
System.out.println("这个数字是"+a);
}
}catch(Exception e){
throw new NotEnough500("数字大小异常:"+e.getMessage());
}
}
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int x=in.nextInt();
//测试x是否大于500
try {
test(x);
} catch (Exception e) {
System.out.println(e.getMessage());
}
in.close();
}
}
@SuppressWarnings("serial")
class NotEnough500 extends Exception{
NotEnough500(String info){
super(info);
}
}
运行结果:
更加通用的是使用initCause()与getCause():
package Exception;
import java.util.Scanner;
public class repeatThrowException {
//定义一个可能会抛出异常的方法
public static void test(int a) throws Exception{
try {
if(a<500) {
throw new Exception("数字不足500");
}else {
System.out.println("这个数字是"+a);
}
}catch(Exception e){
Exception se=new numSizeException("数字大小异常");
se.initCause(e);
throw se;
}
}
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int x=in.nextInt();
//测试x是否大于500
try {
test(x);
} catch (Exception e) {
System.out.println(e.getCause());
}
in.close();
}
}
@SuppressWarnings("serial")
class numSizeException extends Exception{
numSizeException(String info){
super(info);
}
}
有两点需要注意的是:System.out.println(e)相当于System.out.println(e.getClass().getName()+":"+e.getMessage()),而getMessage()得到的就是与异常对象相关的详细描述。再就是如果要使用"|"将两个异常作为一个进行处理,其前提是这多个异常之间不存在子类的关系。
在前面的代码中我们看到,在我们接收异常的代码中最后还有一个finally的块,而在我们的finally块中我们只加了一句in.close();于是看到这里我们就能猜出它有什么作用了,那就是释放资源,当然这只是它功能中的一个,下面,我们就具体来看看到底finally子句有什么用:
当加入finally子句后,我们的异常处理语句块的形式变为:
try{
代码
}catch(异常类型 e){
处理异常
}
.../*其他catch语句块*/
finally{
各种操作
}
首先我们来看看加入finally子句后的执行过程:
当加入finally子句后,如果语句块没有抛出异常,那么在执行完正常程序之后,最后会执行finally子句中的内容。如果在try块中出现了异常,程序会中断try语句块中语句的执行,并且在执行完对应异常的catch语句后,又去执行finally子句中的内容。
需要注意的是,finally子句中的内容基本上是必须执行的,除非在前面的语句中执行了System.exit(num)。,神奇的是,如果在前面的语句中使用了return语句,这时照样会执行finally中的语句,如果此时finally中也有return语句,则此函数最终结果是finally语句中返回的值。
下面是一个典型的finally中的返回值覆盖其他块中返回值的例子:
package Exception;
import java.util.Scanner;
public class FinallyTest {
public static int calcAAddB() throws Exception{
Scanner in=new Scanner(System.in);
try {
int a=in.nextInt();
int b=in.nextInt();
return a+b;
}catch(Exception e) {
throw e;
}finally {
in.close();
return 10;
}
}
public static void main(String[] args) {
try {
System.out.println(calcAAddB());
} catch (Exception e) {
System.out.println(e);
}
}
}
运行结果:
对于输入的任意值返回的都是4,这种代码在ecilipse编辑环境下会给出警告。
在finally语句中也可能会抛出异常,如果此时try语句中抛出了异常,正好finally语句中的代码也抛出了异常,这个异常就会覆盖前面抛出异常,这显然是我们不想看到的,因此这个时候要转而抛出原来try语句中的异常,这样,代码就会变得异常繁琐,好在下面我们还会介绍一种带资源的try语句完美的解决了这个问题
下面的使用不带资源的try语句以及带资源的try语句实现同样的功能的两个示例:
代码1:
package Exception;
import java.util.Scanner;
public class reThrowException {
public static void main(String[] args) throws Exception {
Demo();
}
public static void Demo() throws Exception{
Scanner in=new Scanner(System.in);
Exception ex=null;
try {
try {
int i=in.nextInt();
System.out.println(i);
}catch(Exception e) {
ex=e;
throw e;
}
}finally {
try {
in.close();
}catch(Exception e) {
if(ex==null) throw e;
}
}
}
}
运行结果:
代码2:
package Exception;
import java.util.Scanner;
public class useTryWithResource {
public static void main(String[] args) throws Exception {
demo();
}
public static void demo() throws Exception{
try(Scanner in=new Scanner(System.in);Scanner in2=new Scanner(System.in)){
int i=in.nextInt();
int j=in2.nextInt();
System.out.println(i+j);
}catch(Exception e) {
throw e;
}
}
}
运行结果:
这种带资源的try语句可以在执行完毕try语句后自动执行close操作,当然也可以加catch以及finally语句,不过这些语句都是在执行close操作后再执行,不过不推荐在这种情况下加太多这种语句。
从代码2可以看出:这种代码的语法格式是:
try(资源1;资源2;资源3;...){
}catch{
}
//其他catch语句
finally{
}
接下来是最后的部分了,就是分析堆栈轨迹元素:
堆栈轨迹其实就是一个方法调用过程的列表,可以使用Throw类中提供的一些方法来显示这些列表,下面请看一个例子
代码:
package Exception;
import java.util.Scanner;
public class StackTraceTest {
public static int factorial(int n) {
System.out.println("factorial("+n+"):");
Throwable t=new Throwable();
StackTraceElement[] frames=t.getStackTrace();
for(StackTraceElement f:frames) {
System.out.println(f);
}
int r;
if(n==1) r=1;
else r=n*factorial(n-1);
System.out.println("return "+r);
return r;
}
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
System.out.println("Enter n:");
int n=in.nextInt();
factorial(n);
in.close();
}
}
运行结果:
我相信这个结果不少人乍看一眼是一脸懵逼,博主我刚看也是,不过根据堆栈的知识以及前面的描述(是一个方法调用过程的列表)稍加分析后便可看懂:在main方法中调用第9行的factorial,注意此时此方法调用并未结束,因此第一次输出便是factorial在栈顶,main在栈底的结果,当程序运行到25行,此时第一个factorial方法结束,进行出栈操作,但是这个结束的factorial方法又调用了另一个新的factorial方法,所以16行调用压入栈,然后第9行方法压入栈,此时就形成上面第二部分的结果,以此类推,形成上面第三部分结果,最后程序结束,输出结果。
最后是关于异常机制的几点使用技巧:
1:异常处理不能代替简单的测试。
2:不要过分细化异常。
3:利用异常层次结构。
4:不要压制异常。
5:在检测错误时,“苛刻”要比放任好。
6:不要羞于传递异常。