Java中把所有非正常情况分为两种,Error错误和Exception异常,它们都继承自Throwable异常。
Error错误一般指与虚拟机相关的问题,如系统崩溃,虚拟机错误,动态链接失败等。这种错误是无法恢或不可捕获的,将导致应用程序中断。其子类有:IOError、ThreadDeath、LinkageError、AWTError
Exception异常指可以使用catch块来捕获,子类有IOException,RuntimeException,SQLException。其中RuntimeException运行时异常中常见的有IndexOutOfBoundsException数组越界异常,NullPointerException空指针异常,ClassCastException类转换异常。
异常捕获时,需遵循先捕获小异常,再捕获大异常。
在java7以前,每个catch块只能捕获一种类型的异常。但从Java7开始,一个catch块可以捕获多种类型异常。
使用一个catch块捕获多种类型异常时,多种异常类型之间用竖线 | 隔开
捕获多种类型的异常时,异常变量有隐式的final修饰。因此程序不能对异常变量重新赋值。
public class MultiException{
public static void main(String[] args){
try{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(argsp[1]);
int c = a/b;
System.out.println("你输入的两个数相除的结果是:" + c);
}
catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie){
System.out.println("程序发生了越界、数字格式异常、算术异常之一:" + c);
//捕获多种异常时,异常变量默认为final修饰,下面代码有错
ie = new ArithmeticException("test");
}
catch(Exception e)
{
System.out.println("未知异常:" );
//捕获一种异常时,异常变量没有final修饰,下面代码正确
e = new RuntimeException("test");
}
}
}
如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。
所有异常对象都包含了如下几个常用的相关信息。
有时,程序在try块里打开了一些物理资源(例如数据库连接、网络连接、磁盘文件),这些物理资源都必须显式回收。不管try块中是否出现异常,也不管哪一个catch块被执行,甚至try块或catch块中执行了return语句,finally块总会被执行。
Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存
public class FinallyTest{
public static void main(String[] args)
{
FileInputStream fis = null;
try
{
fis = new FileInputStream("a.txt");
}
catch(IOException ioe)
{
System.out.println(ioe.getMessage());
//rerurn语句强制方法返回
return;
//使用exit退出虚拟机
//System.exit(1);
}
finally
{
//关闭磁盘文件
if(fis != null)
{
try
{
fis.close();
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
}
System.out.println("执行finally块里的资源回收!");
}
}
}
使用在catch代码块中使用return语句,finally块依然会执行。然而使用System.exit(!)语句来退出虚拟机,则finally块将会失去执行的机会。除非在try块、catch块调用了退出虚拟机的方法,否则不管try块、catch块执行了怎样的代码,异常处理的finally块总会被执行。
通常,不要在finally块中调用return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,将会导致rerurn、throw语句失效。
正如以上代码中,finally块中也包含了一个完整的异常处理流程,这种在try块,catch块或finally块中包含完整的异常处理流程被称为异常处理的嵌套。
FileInputStream fis = null;
try
{
fis = new FileInputStream("a.txt");
}
finally
{
//关闭磁盘文件,回收资源
if(fis != null)
{
fis.close();
}
}
Java7以前,不得不写的臃肿代码在Java7中改变了这种局面,Java7中增强了try语句的功能,它允许在try关键字后跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束后显式关闭资源,比如数据库连接、网络连接等物理资源。需要注意的是,为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法
Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。Closeable接口中的方法声明抛出了IOException,因此它的实现类在实现close()方法时,只能声明抛出IOException或其子类,AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明抛出任何异常。
public class AutoCloseTest{
public static void main(String[] args){
throws IOException
{
try(
//声明、初始化两个可关闭的资源
//try语句会自动关闭这些资源
BufferedReader br = new BufferedReader(
new FileReader("AutoCloseTest.java"));
PrintStream ps = new PrintStream(new FileOutputStream("a.txt")))
{
System.out.println(br.readline());
ps.println("222");
}
}
}
由于BufferedReader、PrintStream都实现了CLoseable接口,而且它们放在try语句中声明、初始化,所以try语句会自动关闭它们。因此上面的程序是安全的。
自动关闭资源的try语句相当于隐式的finally块。因此这个try语句可以既没有catch块,也没有finally块。
只有Java支持Checked异常,其它语言都没有提供Checked异常。Java认为所以Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。如果没有处理Checked异常,该程序在编译时就发生错误,无需通过编译。
Checked异常的处理方式有两种:
当知道如何处理异常时,使用try...catch进行捕获异常
当不知道何种异常,需要在定义该方法时声明抛出异常
Runtime异常更加灵活,Runtime异常无须显示的声明抛出,如果程序需要捕获异常Runtime异常,也可以使用try...catch块来实现
使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常抛给上一级,如果main方法也不知道怎样处理,则交给JVM处理,JVM处理异常的方法是,打印异常的跟踪栈信息,并中止程序允许,这就是程序在遇到异常后自动结束的原因。“C#,Pthon,Rubby等语言没有Checked异常,所有的异常都是Runtime异常。
抛出异常
如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。
throw ExceptionInstance;
如果throw语句抛出的异常是Checked异常,则该throw语句要么在try块中,显示捕获异常。要么放在一个带throw声明抛出的方法中,即把该异常交给该方法的调用者处理。如果throw语句抛出的是Runtime异常,则该语句应放在try块中,也无须在带throws声明抛出的方法中。
public class ThrowTest{
public static void main(String[] args)
{
throwChecked(-3);
throwRuntime(3);
public static void throwChecked(int n) throws Exception
{
if(a>0)
{
throw new Exception("n为正数”);
System.out.println(n);
}
}
public static void throwRuntime(int n)
{
if(a>0)
{
throw new RuntimeException("n为正数”);
System.out.println(n);
}
}
自行抛出Runtime异常比自行抛出Checked异常的灵活性更好。同样抛出Checked异常则可以让编译器提醒程序员必须处理该异常。
通常程序很少会自行抛出系统异常,因为异常的类名也包含了该异常的有用信息。所有在选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情况,应用程序常常需要抛出自定义异常。
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器,另一个是带字符串参数的构造器,这个字符串将作为该异常对象的描述信息。
前面提到的异常处理方式有两种:
在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次抛出异常。
该方法签名中声明抛出该异常,将异常完全交给方法调用者处理。
public class AuctionTest{
private double initPrice = 30.0;
//因为该方法显示抛出了AuctionException异常
//所以此处需要声明抛出了AuctionException
public void bid(String bidPrice){
throws AuctionException
{
double d = 0.0;
try
{
d = Double.parseDouble(bidPrice);
}
catch(Exception e)
{
//此处完成本方法中可以对异常执行的修复处理
//此处仅仅是在控制台打印异常的跟踪栈信息
e.printStackTrace();
//再次抛出自定义异常
throw new AuctionException(
“竞拍价必须是数值,"+”不能包含其它字符“);
}
if(initPrice > d){
throw new AuctionException("竞拍价比起拍价低,"+"不允许竞拍");
}
initPrice =d;
}
public static void main(String[] args){
{
AuctionTest at = new AuctionTest();
try
{
at.bid("df");
}
catch (AuctionException ae)
{
//再次捕获到bid()方法的异常,并对该异常进行处理
System.err.println(ae.getMessage());
}
}
}
这种catch和throw结合使用的情况在大型企业级应用中非常常用,企业级应用对异常的处理通常分为两个部分:1应用后台需要通过日志来记录异常发生的详细情况;2应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所以异常需要两个方法共同完成,也就必须将catch和throw结合使用。
对以下代码:
try
{
new FileOutputStream("a.txt");
}
catch(Exception ex)
{
ex.printStackTrace();
throw ex;
}
从Java7开始,Java编译器会执行更细致的检查,Java编译器会检查throw语句异常的实际类型,这样编译器知道1代码实际上只可能排除FileNotFoundException异常。因此在方法签名中只要声明抛出FileNotFoundException异常即可。
public class ThrowTest2{
public static void main(String[] args)
//Java7会检查1号代码处可能抛出异常的实际类型
//因此此处只需声明抛出FileNotFoundException异常即可
throw FileNotFoundException
{
try
{
new FileOutputStream("a.txt");
}
catch(Exception ex)
{
ex.printStackTrace();
throw ex;
}
}
把底层的原始异常直接传给用户是不负责任的表现。通常需要先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户是提示信息,这种处理方式被称为异常转移。假设程序需要实现工资计算的方法,则程序应该采用如下结构的代码实现该方法。
public class AutoCloseTest{
public static void main(String[] args){
throws IOException
{
try(
//实现工资结算的业务逻辑
catch(SQLException sqle)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的message就是对用户的提示
throw new SalExcetion("访问底层数据库出现异常”);
}
catch(Exception e)
{
//把原始异常记录下来,留个管理员
...
throw new SalException("系统出现未知异常");
}
}
这种把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式。可以保证底层异常不会扩散到表现层。可以避免向上暴露太多的实现细节。这完全符合面向对象的封装原则。这种把捕获异常然后接着抛出另一个异常,并把原始异常信息保存下路是一种典型的链式处理(23种设计模式之一:职责链之一),也被称为异常链。
在Jdk1.4之前,程序员必须把自己编写的代码来保持原始异常信息。从1.4开始,所以的Throwable的子类在构造器中都可以结束一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常。使得即时在当前位置创建并抛出象新的异常,你可可能通过这个异常链追踪到异常最初发生的位置。
public class AutoCloseTest{
public static void main(String[] args){
throws IOException
{
try(
//实现工资结算的业务逻辑
catch(SQLException sqle)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的message就是对用户的提示
throw new SalExcetion(sqle);
}
catch(Exception e)
{
//把原始异常记录下来,留个管理员
...
throw new SalException(e);
}
}
上面程序中粗体字代码创建SalException对象时,传入了一个Exception对象,而不是传入一个String对象,这就需要SalException类相应的构造器。从1.4以后,Throwable基类已有了一个可以接收Exception参数的方法,所以可以采用SqlException类。
public class SalException extends Exception{
public SalException(){}
public SalException(String msg){
super(msg);
}
//创建一个可以接收Throwable参数的构造器
public SalException(Throwable t)
{
super(t);
}
}
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
成功的异常处理应遵循以下4个目标
使程序代码混乱最小化
捕获保留诊断信息
通知合适的人员
采用合适的方式结束异常活动
java的异常处理主要依赖于try、catch、finally、throw和throws 5个关键字。应知悉Checked异常和Runtime异常之间的区别,知道实际开发中最常用的异常链和异常转译。