异常处理

Java中把所有非正常情况分为两种,Error错误和Exception异常,它们都继承自Throwable异常。

Error错误一般指与虚拟机相关的问题,如系统崩溃,虚拟机错误,动态链接失败等。这种错误是无法恢或不可捕获的,将导致应用程序中断。其子类有:IOError、ThreadDeath、LinkageError、AWTError

Exception异常指可以使用catch块来捕获,子类有IOException,RuntimeException,SQLException。其中RuntimeException运行时异常中常见的有IndexOutOfBoundsException数组越界异常,NullPointerException空指针异常,ClassCastException类转换异常。

异常捕获时,需遵循先捕获小异常,再捕获大异常。

Java7提供的多异常捕获

在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块的后异常形参来获得。

所有异常对象都包含了如下几个常用的相关信息。

  • getMessage():返回该异常的详细描述字符串
  • printStackTrace():将该异常的跟踪栈信息输出到标准输出
  • printStackTrace(PrintStream):将该异常的跟踪栈信息输出到指定输出流
  • getStacktTrace():返回该异常的跟踪栈信息

使用finally回收资源

有时,程序在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块中包含完整的异常处理流程被称为异常处理的嵌套。

java7的自动关闭资源的try语句

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块。

 

Checked异常和Runtime异常体系

只有Java支持Checked异常,其它语言都没有提供Checked异常。Java认为所以Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。如果没有处理Checked异常,该程序在编译时就发生错误,无需通过编译。

Checked异常的处理方式有两种:

当知道如何处理异常时,使用try...catch进行捕获异常

当不知道何种异常,需要在定义该方法时声明抛出异常

Runtime异常更加灵活,Runtime异常无须显示的声明抛出,如果程序需要捕获异常Runtime异常,也可以使用try...catch块来实现

使用throws声明抛出异常

使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常抛给上一级,如果main方法也不知道怎样处理,则交给JVM处理,JVM处理异常的方法是,打印异常的跟踪栈信息,并中止程序允许,这就是程序在遇到异常后自动结束的原因。“C#,Pthon,Rubby等语言没有Checked异常,所有的异常都是Runtime异常。

使用throw抛出异常

抛出异常

如果需要在程序中自行抛出异常,则应使用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基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器,另一个是带字符串参数的构造器,这个字符串将作为该异常对象的描述信息。

catch和throw同时使用

前面提到的异常处理方式有两种:

在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次抛出异常。

该方法签名中声明抛出该异常,将异常完全交给方法调用者处理。

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结合使用。

Java 7 增强的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);
    }
}

Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。

异常处理规则

成功的异常处理应遵循以下4个目标 

使程序代码混乱最小化

捕获保留诊断信息

通知合适的人员

采用合适的方式结束异常活动

总结

java的异常处理主要依赖于try、catch、finally、throw和throws 5个关键字。应知悉Checked异常和Runtime异常之间的区别,知道实际开发中最常用的异常链和异常转译。

你可能感兴趣的:(异常处理)