概述
本篇文章将总结Java中的异常处理机制,通过具体的代码示例来演示try-catch,finally,throw,throws等关键字的用法,旨在简单了解Java中异常处理机制,后续将总结如何在项目中通过Spring实现系统的统一异常处理。异常定义了程序中可能遇到的非致命性错误,而不是编译时的语法错误,例如程序要打开一个不存在的文件,装载一个不存在的类等。
一、try-catch语句块
Java中提供try-catch语句来进行异常的捕获和处理
代码演示:
package com.jwang.exception;
import org.junit.Test;
public class TestException
{
// 该方法演示不进行异常处理会发生什么:
@Test
public void testException1()
{
int result1 = devide(3, 1);
System.out.println("the result1 is " + result1);
System.out.println("============后面即将发生异常了,但没有捕获处理==============");
// 下面语句会发成异常,但不处理
int result2 = devide(3, 0);
System.out.println("the result2 is " + result2);
System.out.println("============前面发生异常了,但没有捕获处理==============");
}
// 该方法演示异常发生时try-catch是一个怎样的执行顺序:
@Test
public void testException2()
{
// 监视有可能发生异常的代码段,如果发生异常,会将异常的信息封装到一个对象当中,传递给catch
try
{
// 不会发生异常,所以后面的打印语句正常被执行
int result1 = devide(3, 1);
System.out.println("the result1 is " + result1);
// 发生除数为0的异常,所以后面的打印语句不会被执行,跳转到catch语句
System.out.println("============后面即将发生异常了,捕获处理了==============");
int result2 = devide(3, 0);
System.out.println("the result2 is " + result2);
System.out.println("============前面发生异常了,但没有捕获处理==============");
}
catch (Exception e)
{
// 接收来自try语句的异常对象
System.out.println(e.getMessage() + "异常发生了,现在正再catch中");// 对异常进行处理
}
// try代码块之后的语句:
System.out.println("现在正在执try代码快后的程序。。。。");// 这条语句会被正常执行
}
// 编写一个求商的函数
public int devide(int x, int y) throws ArithmeticException
{
int result = x / y;
return result;
}
}
第一个方法的运行结果如下所示:
我们看到程序在调用被0除的方法之前运行正常,控制台打印出了预期的结果,接着调用被0除的方法时发生异常,由于异常没有被处理,所以程序直接报错,终止运行。
第二个方法的运行结果如下所示:
通过上面的运行结果我们发现将可能发生异常的代码放到try语句块中,当try中的代码发生异常时程序会跳转到catch语句块中继续执行,执行完catch中的代码后,程序会接着继续执行catch语句后面的代码,但是try中发生异常的代码以后的代码就不会再执行。可见java中的异常处理是结构化的,不会因为一个异常影响整个程序的执行。
当try中代码发生异常时,系统会将这个异常的代码行号,类别等信息封装到一个异常对象中,并将这个对象传递给catch代码块。
二、throws关键字
针对上面的例子,假设devide()方法和testException2方法()分别位于不同的类中,那么在testException2中调用devide方法时怎么知道devide方法可能出现异常呢?他又怎么能够想到用try-catch语句来进行处理呢?问题可以这样解决,只要在定义devide方法时,在devide方法参数列表后用throws关键字声明一下,该函数有可能发生异常及异常的类别。这样调用者在调用时就必须用try-catch语句进行处理了,否则编译无法通过。通过throws关键字可以一直往上一层调用者抛,这样源头的调用这就必须处理。
代码演示:
package com.jwang.exception;
import org.junit.Test;
/**
* 描述:throws关键字的使用
* @author jwang
*
*/
public class TestException2
{
/**
* 该方法中调用了通过throws关键字抛出异常的方法,通过try-catch进行异常处理
*/
@Test
public void testException1()
{
// 监视有可能发生异常的代码段,如果发生异常,会将异常的信息封装到一个对象当中,传递给catch
try
{
// 虽然不会发生异常,但是调用的方法可能抛出异常,所以要放在try中
int result1 = devide(3, 1);
System.out.println("the result1 is " + result1);
// 发生除数为0的异常,所以后面的打印语句不会被执行,跳转到catch语句
System.out.println("============后面即将发生异常了,捕获处理了==============");
int result2 = devide(3, 0);
System.out.println("the result2 is " + result2);
System.out.println("============前面发生异常了,已经被捕获处理==============");
}
catch (Exception e)
{
// 接收来自try语句的异常对象
System.out.println(e.getMessage() + "异常发生了,现在正再catch中");// 对异常进行处理
}
// try代码块之后的语句:
System.out.println("现在正在执try代码快后的程序。。。。");// 这条语句会被正常执行
}
/**
* 编写一个求商的函数,并通过throws关键字抛出异常
* @param x
* @param y
* @return
* @throws ArithmeticException
*/
public int result(int x, int y) throws ArithmeticException
{
int result = x / y;
return result;
}
/**
* 该方法调用通过throws关键字抛出异常的方法,不处理继续抛出
* @param x
* @param y
* @return
* @throws ArithmeticException
*/
public int devide(int x, int y) throws ArithmeticException
{
return result(x, y);
}
}
运行的结果如下所示:
注意:
(1)某个方法中用throws声明有可能发生异常时,调用方法的语段中必须对异常进行处理,即使没有异常。
(2)如果一个方法中的语段执行时可能发生某种异常,但是并不能确定该如何处理,则此方法应该声明抛出异常,表明该方法将不对这些异常进行处理,而是由该方法的调用者来负责处理。也就是程序中异常没有用try-catch捕获处理,我们可以在程序代码所在的函数(方法)声明后用throws声明该函数要抛出异常,将该异常抛出到该函数的调用函数中,一直到mian方法,jvm肯定是要处理的,这样编译就能通过了。虽然编译能通过,但是异常一旦发生,没有处理,程序就会非正常终止。所以在正常的开发中我们必须对可能发生的异常进行捕获处理。
三、系统异常和自定义异常
系统异常类:
1、throwable有两个直接子类,一个是Error类,通常为内部错误,正常情况下并不期望用户程序捕获他们。另一个是Exception类,绝大部分用户程序应当捕获的异常类的根类。一些常用的异常类都直接或间接派生自Exception类,因此我们可以认为绝大多数的异常都属于Exception。
2、ArithmeticException(表示在算数运算中发生异常)NullPointerException(空指针异常)等都是Exception类的子类。
3、异常类中有两个常用的方法:
(1)String getMessage()在Exception类中定义的方法,被继承到所有的异常类中,用于获得与异常相关的描述信息。
(2)void printStackTrace()在Exception类中定义的方法,用于显示异常的堆栈信息,不但有异常的原因,还涉及产生异常的代码行。
自定义异常类与throw关键字:
1、除了系统提供的异常,我们也可以自己定义自己的异常类,自定义的异常类必须继承Exception类。
2、在一个方法内部使用throw抛出异常对象,如果该方法的内部没有用try-catch语句对这个抛出的异常进行处理,则此方法因该声明抛出异常而由该方法的调用者负责处理。
程序示例:
package com.jwang.exception;
/**
* @author jwang
* 自定义一个异常类,必须继承Exception类
*/
@SuppressWarnings("serial")
public class MyFirstException extends Exception
{
// 异常类中定义一个变量
int devisor;
// 自定义异常类的一个构造函数,供throw抛出该类的异常对象时调用
public MyFirstException(String msg, int devisor)
{
// 调用父类的带一个参数的构造方法
super(msg);
this.devisor = devisor;
}
// 自定义异常类中的一个方法,返回除数的值
public int getdevisor()
{
return devisor;
}
}
package com.jwang.exception;
import org.junit.Test;
/**
* @author jwang 自定义异常应用
*/
public class TestException3
{
@Test
public void testException()
{
try
{
//int result = devide(3,0);当这一句的devide方法被调用的时候,会自动调用下面与此异常相匹配的catch异常处理语段,即调用第二个catch语段
int result = devide(3, -1);// 当这一句的devide方法被调用的时候,会调用devidebyminusexception异常处理语段
// int result = devide(3,1);//当这一句的devide方法被调用的时候,会自动调用第三个catch语段进行处理
System.out.println("the result is " + result);
}
catch (MyFirstException e)// 当除数为负数的时候自动调用这个catch语段进行处理
{
System.out.println("program is running into " + "devidebyminusexception");
System.out.println(e.getMessage());// 调用父类的方法,输出异常信息
System.out.println("the devisor is " + e.getdevisor());// 调用自定义异常类中独有的方法,输出除数
}
catch (ArithmeticException e)// 当除数为0的时候会自动调用这个catch语段进行处理
{
System.out.println("program is running into " + "ArithmeticException");
System.out.println(e.getMessage());
}
catch (Exception e)// 当异常与其他异常都不匹配的时候就会调用这个catch处理异常,要是有多个异常处理语段的时候,这个通常放在末尾。
{
System.out.println("program is running into " + "other unknow exception");
System.out.println(e.getMessage());
}
finally // 无论是否发生异常都会被执行
{
System.out.println("program is running into " + "finally");
}
System.out.println("program is running here ,that is normal");
}
/**
* 这个方法接收两个参数并抛出两个可能的异常
* @param x
* @param y
* @return
* @throws ArithmeticException
* @throws MyFirstException
*/
public int devide(int x, int y) throws ArithmeticException, MyFirstException
{
if (y < 0)
{
// 当除数小于0时,在方法的内部抛出一个自定义类型的异常对象
throw new MyFirstException("被除数为负", y);
}
int result = x / y;
return result;
}
}
运行结果:
分析:
1、我们可以看到上面的程序中,devide方法声明时抛出了两个异常,java中一个方法可以声明抛出多个异常。
2、上面的程序使用一个try后面跟着多个catch语句来捕捉异常,每一个catch可以处理一个不同种类的异常。如果我们调用devide(3,0),将会发生ArithmeticException 异常,程序就将跳转到catch (ArithmeticException e)代码块来执行。如果我们调用devide(3,-1),将会发生devidebyminusexception类型的异常,程序将跳转至catch (devidebyminusexception e)代码块来执行。如果devide方法中发生了除了上面两种异常之外的异常时,程序将跳转到catch(Exception e)代码块进行处理,这个代码块能够处理所有类型的异常。所以编写代码时应该放在最后。否则其他处理异常的代码不会执行。
四、finally关键字(代码见上面程序示例)
1、finally块必须和try-catch块一起使用,不能单独使用。每一个try语句必须有一个或多个catch语句相对应,try代码块与catch代码块,以及finally代码块之间不能有其他的语句,必须连着。
2、无论try-catch中发生什么,finally块都会被执行。即使try代码块和catch代码块中使用了return语句退出当前方法,或者使用了break语句跳出了某个循环,相应的finally块都要被执行。当发生异常时,程序可能会意外的中断,有些被占用的资源就得不到清理。finally块可以确保执行清理工作,或者进行资源的释放工作。
3.Finally块不能被执行的唯一情况是:在被保护代码块中执行了System.exit(0).
五、注意:
1、一个方法被覆盖时,覆盖他的方法必须抛出相同的异常或者异常的子类。
2、如果父类抛出了多个异常,那么重写(覆盖)方法必须抛出那些异常的一个子集,也就是说,不能抛出新的异常。