异常处理
- 异常机制可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,提高程序的健壮性。
- java中的异常机制主要依赖于try,catch,finally,throw和throws五个关键字。
- java将异常分为两种:Checked异常和Runtime异常
一. 概述
异常出现的背景:
- 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,所以程序总是不够健壮。
- 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。
二 .异常处理机制
使用try…catch捕获异常
java提出一种假设:如果程序可以顺利完成,那就“一切正常",把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理。
try{
//业务实现代码
...
}
catch(Exception e)
{
alert 输入不合法
goto retry
}
如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给java运行时环境,这个过程被称为抛出异常.
当java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块。如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程称为捕获(catch)异常.如果java运行时,环境找不到捕获异常的catch块,则运行时环境终止,java程序也将退出。
异常类的继承体系
问题:当java运行环境接收到异常对象时,如何为该异常对象寻找catch块?
答:当java运行时环境将调用该catch块来处理该异常;否则再次拿该异常对象和下一个catch块中的异常类进行比较。
java常见的异常类之间的继承关系;
java把所有的非正常情况分为两种:异常和错误
Error错误:一般是指与虚拟机相关的问题,如系统
崩溃,虚拟机错误,动态链接失败等,这种错误无法恢复或不可捕获,将导致应用程序中断。
注意:通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象,在定义该方法时,也无须在其trhows子句中声明该方法可能抛出Error及其任何子类。
注意: 异常捕获时,先捕获小异常,再捕获大异常。
例子
public class DivTest
{
public static void main(String[] args)
{
try
{
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
System.out.println("你输入的两个数相除的结果是:"+c);
}
catch(IndexOutOfBoundsException ie)
{
System.out.println("数组越界:运行程序时输入的参数个数不够");
}
catch(NumberFormatException ne)
{
System.out.println("数字格式异常,程序只能接收整数参数");
}
catch(ArithmeticException ae)
{
System.out.println("算术异常");
}
catch(Exception e)
{
System.out.println("未知异常");
}
}
}
java7提供的多异常捕获
使用一个catch块捕获多种异常时需要注意如下两个地方:
- 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。
- 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
多异常捕获:
public class MultiExceptionTest
{
public static void main(String[] args)
{
try
{
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
System.out.println("你输入的两个数相除的结果是: "+c);
}
catch(IndexOutOfBoundsException|NumberFormatException|ArithmeticException ie)
{
System.out.println("程序发生了数组越界,数字格式异常,算术异常之一");
//捕获多异常时,异常变量默认为final修饰
//所以下面代码有错
ie=new ArithmeticException("test");
}
catch(Exception e)
{
System.out.println("未知异常");
//捕获一种类型的异常时,异常变量没有final修饰
//所以下面代码正确
e=new RuntimeException("test");
}
}
}
访问异常信息
如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。当java运行时决定调用某个catch块来处理该异常对象时,会将该异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有的异常对象都包含如下几个常用的方法:
- getMessage():返回该异常的详细描述字符串
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
- getStackTrace():返回该异常的跟踪栈信息。
public class AccessExceptionMsg
{
public static void main(String[] args)
{
try
{
FileInputStream fis=new FileInputStream("a.txt");
}
catch(IOException ioe)
{
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
}
}
使用finally回收资源
提示:
java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。
为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。
try
{
//业务实现代码
...
}
catch(SubException e)
{
//异常处理块1
...
}
catch(SubExecption2 e)
{
//异常处理块2
...
}
...
finally
{
//资源回收块
...
}
当java程序执行try块,catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally语句块,如果没有finally语句块,程序立即执行return语句或者throw语句,方法终止;如果有finally语句,系统立即开始执行finally块。—只有当finally块执行完成后,系统才会再次跳回来执行try块,catch块里的return或throw语句;如果finally块里也使用了return或者throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块,catch块中的如何代码。
三. Checked异常和Runtime异常体系
java的异常类被分为两大类:Checked异常和Runtime异常(运行时异常).
对于Checked异常的处理方式有如下两种:
- 当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修复该异常。
- 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。
Runtime异常则更加灵活,Runtime异常无须显示声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。
使用throws声明抛出异常
使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行。
throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开。
throws ExceptionClass1,ExceptionClass2...
如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显示捕获该异常,要么放在另一个带throws声明抛出的方法中。
public class ThrowsTest
{
public static void main(String[] args)
throws Exception
{
//因为test()方法声明抛出IOException异常
//所以调用该方法的代码要么处于try...catch块中
//要么处于另一个带throws声明抛出的方法中
test();
}
public static void test() throws IOException
{
//因为FileInputStream的构造器声明抛出IOException异常
//所以调用FileInputStream的代码要么处于try...catch块中
//要么处于另一个带throws声明抛出的方法中
FileInputStream fis=new FileInputStream(" a.txt");
}
}
使用throws声明抛出异常时有一个限制,就是方法重写时“两小”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
public class OverrideThrows
{
public void test() throws IOException
{
FileInputStream fis=new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows
{
//子类方法声明抛出了比父类方法更大的异常
//所以下面方法出错
public void test() throws Exception
{
}
由此可见,使用Checked异常至少存在如下两大不便:
- 对于程序中的Checked异常,java要求必须显示捕获并处理该异常,或者显示声明抛出该异常。这样就增加了编程复杂度.
- 如果在方法中显示声明抛出Checked异常,将会导致方法签名和异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。
大部分时候推荐使用Runtime异常,而不是使用Checked异常。尤其是程序需要自行抛出异常时。
使用throw抛出异常
当程序出现错误时,系统会自动抛出异常;除此之外,java也允许程序自行抛出异常,自行抛出异常使用throw语句完成。
抛出异常
很多时候,系统是否要抛出异常,可能需要根据应用的业务需求来决定,如果程序中的数据,执行与既定的业务需求不符,这就是一种异常。由于与业务需求不符而产生的异常,必须由程序员来决定抛出,系统无法抛出这种异常。
如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。
throw Exception;
如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块中,显示捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块中,也无须放在带throws声明抛出的方法中;程序既可以显示使用try…catch来捕获并处理该异常,也可以完全不理会该异常,把异常交给该方法调用者处理。
public class ThrowTest
{
public static void main(String[] args)
{
try
{
//调用声明抛出Checked异常的方法,要么显示捕获该异常
//要么在main方法中再次声明抛出
throwChecked(-3);
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
//调用声明抛出Runtime异常的方法既可以显示捕获该异常
//也可以不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception
{
if(a>0)
{
//自行抛出Exception异常
//该代码必须处于try块中,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if(a>0)
{
//自行抛出RuntimeException异常,既可以显示捕获该异常
//也可以完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}
自定义异常类
通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常也包含了该异常的有用信息。所以在选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情况下,应用程序常常需要抛出自定义异常。
定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。
public class AuctionException extends Exception
{
//无参数的构造器
public AuctionException(){
}
//带一个字符串参数的构造器
public AuctionException(String msg)
{
super(msg);
}
catch和throw同时使用
在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。
public class AuctionTest
{
private duoble 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());
}
}
}
企业级应用对异常的处理通常分为两个部分:1.应用后台需要通过日志来记录异常发生的详细情况; 2.应用还需要根据异常向应用使用者传达某种提示。在这种情况下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。