Java 异常处理

Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字,

  • try关键字后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码

  • catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块

  • 多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行

  • throws关键字主要在方法签名中使用,抛出一个具体的异常对象

  • throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象

我们希望所有的错误都可以在编译阶段被发现,就是在试图运行程序之前排除所有错误,但这是不现实的,余下的问题必须在运行期间得到解决。JAVA将异常分为两种,Checked异常和Runtime异常,JAVA认为Checked异常都是可以在编译阶段被处理的异常,所以他强制程序处理所有多的Checked异常;而Runtime异常则无需处理

异常机制可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性

Java异常机制

Java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性

使用try...catch捕获异常

Java提出了一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理。下面是Java异常处理机制的语法结构

try
{
    // 业务实现代码
    ...
}
catch (Exception)
{
    alert 输入不合法
    goto retry
}

如果执行try块里业务逻辑代码时出现异常,系统自动生成一个异常对象,该对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。当Java运行时环境收到异常对象时,会寻找能处理该异常的catch块,如果找到合适的catch块,则把该异常对象交给catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出

String inputStr = null;
// br.readLine():每当在键盘上输入一行内容按回车,
// 用户刚刚输入的内容将被br读取到。
while ((inputStr = br.readLine()) != null)
{
    try
    {
        // 将用户输入的字符串以逗号作为分隔符,分解成2个字符串
        String[] posStrArr = inputStr.split(",");
        // 将2个字符串转换成用户下棋的坐标
        int xPos = Integer.parseInt(posStrArr[0]);
        int yPos = Integer.parseInt(posStrArr[1]);
        // 把对应的数组元素赋为"●"。
        if (!gb.board[xPos - 1][yPos - 1].equals("╋"))
        {
            System.out.println("您输入的坐标点已有棋子了,"
            + "请重新输入");
            continue;
        }
        gb.board[xPos - 1][yPos - 1] = "●";
    }
    catch (Exception e)
    {
        System.out.println("您输入的坐标不合法,请重新输入,"
        + "下棋坐标应以x,y的格式");
        continue;
    }
    ...
}

异常类的继承体系

当Java运行时环境接收到异常对象时,catch关键字形式(Exception e)的每一个catch块都会处理该异常类及其实例

当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常;否则再次判断该异常对象和下一个catch块里的异常类进行比较

Java异常捕获流程示意图
Java 异常处理_第1张图片

当程序进入负责异常处理的catch块时,系统生成的异常对象ex将会传给catch块后的异常形参,从而允许catch块通过该对象来获得异常的详细信息

try块后可以有多个catch块,try块后使用多个catch块是为了针对不同异常类提供不同的异常处理方式。当系统发生不同的意外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常

通过在try块后提供多个catch块可以无须在异常处理块中使用if、switch判断异常类型,但依然可以针对不同异常类型提供相应的处理逻辑,从而提供更细致,更有调理的异常处理逻辑

从上图可以看出,通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行,除非在循环中使用了continue开始下一次循环,下一次循环又重新运行了try块,这才可能导致多个catch块被执行

try块与if语句不一样,try块后的花括号({...})不可以省略,即使try块里只有一行代码,也不可以省略这个花括号。与之类似的,catch块后的花括号({...})也不可以省略。还有一点需要指出:try块里声明的变量是代码块内局部变量,它只在try块内有效,catch块中不能访问该变量。

Java常见的异常类之间的继承关系图
Java 异常处理_第2张图片

Java把所有非正常情况分成两种:异常(Exception)和错误(Error),它们都是继承Throwable父类

Error错误,一般是指与虚拟机(JVM)相关的问题,如系统崩溃、虚拟机出错误、动态链接失败等,这种错误是java程序的根本运行环境出现了问题,这样错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句声明该方法能抛出Error及其任何子类

public class DivTest
{
    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 ie)
        {
            System.out.println("数组越界:运行程序时输入的参数个数不够");
        }
        catch (NumberFormatException ne)
        {
            System.out.println("数字格式异常:程序只能接受整数参数");
        }
        catch (ArithmeticException ae)
        {
            System.out.println("算术异常");
        }
        catch (Exception e)
        {
            System.out.println("未知异常");
        }
    }
}
  • 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常

  • 如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常,Java运行时将调用NumberFormatException对应的catch块处理该异常

  • 如果运行该程序时输入的第二个参数是0,将发生除0异常,Java运行时将调用ArithmeticException对应的catch块处理该异常

  • 如果运行该程序时出现其他异常,该异常对象总是Exception类或其子类的实例,Java运行时将调用Exception对应的catch块处理该异常

public class NullTest
{
    public static void main(String[] args)
    {
        Date d = null;
        try
        {
            System.out.println(d.after(new Date()));
        }
        catch (NullPointerException ne)
        {
            System.out.println("空指针异常");
        }
        catch(Exception e)
        {
            System.out.println("未知异常");
        }
    }
}
  • 如果运行该程序时试图调用一个null对象的实例方法或实例变量时,Java运行时将调用NullPointerException对应的catch块处理该异常

实际上,进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面(先处理小异常,再处理大异常),否则将出现编译错误

Java7提供的多异常捕获

一个catch块可以捕获多钟类型的异常,使用一个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 fileInputStream =new FileInputStream("NBA.txt");
        } 
        catch (Exception ioe) 
        {
            System.out.println(ioe.getMessage());
            ioe.printStackTrace();
        }
    }
}

使用finally回收资源

有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收

Java垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存

如果try块的某条语句引起了异常,该语句后面的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。如果在catch块里进行资源回收,但catch块完全有可能得不到执行,这将导致不能及时回收这些物理资源

为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行

try
{
    // 业务实现代码
    ...
}
catch (SubException1 e)
{
    // 异常处理块1
    ...
}
catch (SubException2 e)
{
    // 异常处理块2
    ...
}
...
finally
{
    // 资源回收块
    ...
}

异常处理语法结构中,只有try块是必须的,也就是说,如果没有try块,则不能有后面的catch块和finally块;catch块和finally块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;但不能只有try块,既没有catch块,也没有finally块;多个catch块必须位于try块之后,finally块必须位于所有的catch块之后

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());
            // return语句强制方法返回
            return ;       // ①
            // 使用exit来退出虚拟机
            // System.exit(1);     // ②
        }
        finally
        {
            // 关闭磁盘文件,回收资源
            if (fis != null)
            {
                try
                {
                    fis.close();
                }
                catch (IOException ioe)
                {
                    ioe.printStackTrace();
                }
            }
            System.out.println("执行finally块里的资源回收!");
        }
    }
}

除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行

在通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效

当Java程序执行try块,catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块——只有当finally块执行完后,系统才会再跳回来执行try块,catch块里的return或throw语句。如果finally块也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会再跳回去执行try块、catch块里的任何代码

尽量避免在finally里使用return或throw等导致方法终止的语句,否则可能出现一些很奇怪的情况

异常处理的嵌套

在try块、catch块或finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套

异常处理流程代码可以放在任何可执行性代码的地方,因此完整的异常处理流程既可以放在try块里,也可以放在catch块里,还可以放在finally块里

异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低

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

Java7增强了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。为了保证try语句可以正常关闭资源,这些资源实现类必须实现Closeable或AutoCloseable接口,实现这些类就必须实现close()方法

Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。

  • Closeable接口里的close()方法声明抛出了IOException,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类

  • AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明抛出任何异常

下面程序示范如何使用自动关闭资源的try语句

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("庄生晓梦迷蝴蝶");
        }
    }
}

上面程序圆括号里代码分别声明、初始化了两个IO流,由于BufferedReader、PrintStream都实现了Closeable接口,而且它们放在try语句中声明、初始化,所以try语句会自动关闭它们。因此程序是安全的。自动关闭资源的try语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块

自动关闭资源的try语句后也可以带多个catch块和一个finally块

Checked异常和Runtime异常体系

Java的异常被分为两大类:Checked异常和Runtime异常(运行时异常)。所有RuntimeException类及其子类实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则称为Checked异常

只有Java语言提供了Checked异常,其他语言都没有提供Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常

对于Checked异常的处理方式有两种

  • 当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常,然后在对应的catch块中修补该异常

  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常

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

使用throws声明抛出异常

使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交给JVM处理。JVM对异常处理的方法是打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因

throws声明抛出只能在方法签名中进行使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开

throws Exception1, Exception2...

下面使用了throws来声明抛出IOException异常,一旦使用throws语句声明抛出该异常,程序就无须使用try...catch块来捕获该异常了。程序声明不处理IOException异常,将该异常交给JVM处理,所以程序一旦遇到该异常,JVM就会打印该异常的跟踪栈信息,并结束程序

public class ThrowsTest
{
    public static void main(String[] args)
        throws IOException
    {
        FileInputStream fis = new FileInputStream("a.txt");
    }
}

如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中

public class ThrowsTest2
{
    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");
    }
}

子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多

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异常。尤其当程序需要自行抛出异常时,使用Runtime异常更加简洁。当使用Runtime异常时,程序无须在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可

使用throw抛出异常

当程序出现错误时,系统会自动抛出异常;Java允许程序自行抛出异常,自行抛出异常使用throw语句来完成。

抛出异常

throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的语法格式如下:

throw ExceptionInsantance;

当Java运行时接收到开发者自行抛出的异常时,同样会中止当前的执行流,跳到该异常对应的catch块,由该catch块来处理该异常。也就是说,不管是系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有任何差别

如果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,不符合要求");
        }
    }
}

自行抛出Runtime异常比自行抛出Checked异常的灵活性更好。抛出Checked异常则可以让编译器提醒程序员必须处理该异常

自定义异常类

在通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常也包含了该异常的有用信息。所以在选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常

自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)

public class AuctionException extends Exception
{
    // 无参构造器
    public AuctionException(){}
    
    // 带一个字符串参数的构造器
    public AuctionException(String msg)
    {
        super(msg);
    }
}

在大部分情况下,创建自定义异常都可采用与AuctionException.java相似的代码完成,只需改变AuctionException异常的类名即可,让该异常类的类名可以准确描述该异常

catch和throw同时使用

两种异常处理方式

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

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

当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可以完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常

为了实现这种通过多个方法协作处理同一异常的情形,可以在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结合使用的情况在大型企业级应用中非常常用。企业级应用对异常的处理通常分成两个部

  • 应用后台需要通过日志来记录异常发生的详细情况

  • 应用还需要根据异常向应用传达某种提示

在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用

异常链

对于真实的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会跨层访问。下图显式了这种具有分层结构应用的大致示意图
Java 异常处理_第3张图片

程序先捕获原始的异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译

public calSal throws SalException
{
    try
    {
        //实现结算工资的业务逻辑
        ....
    }
    catch(SQLException sqle)
    {
        //把原始异常记录下来,留个管理员
        ...
        //下面异常中的message就是向用户的提示
        throw new SalException("访问底层数据库出现异常");
    }
    catch(Exception e)
    {
        //把原始异常记录下来,留个管理员
        ....
        //下面异常中的message就是向用户的提示
        throw new SalException("系统出现未知异常");
    }
}

这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理(职责链模式),也称为“异常链”

所有的Throwable子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。如果我们希望上面SalException可以追踪到最原始的异常信息。则可以将该方法改写如下

public calSal throws SalException
{
    try
    {
        //实现结算工资的业务逻辑
        ....
    }
    catch(SQLException sqle)
    {
        //把原始异常记录下来,留个管理员
        ...
        //下面异常中的message就是向用户的提示
        throw new SalException(sqle);
    }
    catch(Exception e)
    {
        //把原始异常记录下来,留个管理员
        ....
        //下面异常中的message就是向用户的提示
        throw new SalException(e);
    }
}

上面程序中创建SalException对象时,传入了一个Exception对象,而不是传入了一个String对象,这就需要SalException类有相应的构造器。从JDK1.4以后,Throwable基类有一个可以接收Exception参数的方法,所以可以采用如下代码来定义SalException类

public class SalException extends Exception
{
    public SalException(){}
    public SalException(String msg)
    {
        super(msg);
    }
    public SalException(Throwable t)
    {
        super(t);
    }
}

Java的异常跟踪栈

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

class SelfException extends RuntimeException
{
    SelfException(){}
    SelfException(String msg)
    {
        super(msg);
    }
}
public class PrintStackTraceTest
{
    public static void main(String[] args)
    {
        firstMethod();
    }
    public static void firstMethod()
    {
        secondMethod();
    }
    public static void secondMethod()
    {
        thirdMethod();
    }
    public static void thirdMethod()
    {
        throw new SelfException("自定义异常信息");
    }
}

从结果可知,异常从thirdMethod方法开始触发,传到secondMethod方法,再传到firstMethod方法,最后传到main方法,在main方法终止,这个过程就是Java的异常跟踪栈

Exception in thread "main" SelfException: 自定义异常信息
at PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:25)
at PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:21)
at PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:17)
at PrintStackTraceTest.main(PrintStackTraceTest.java:13)

只要异常没有被完全捕获(包括异常没有捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者...直至最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息

跟踪栈记录程序中所有的异常发生点,各行显式被调用方法中执行的停止位置,并标明类、类中的方法名、与故障点对应的文件的行。一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口main方法或Thread类的run方法(多线程)

public class ThreadExceptionTest implements Runnable
{
    public void run()
    {
        firstMethod();
    }
    public void firstMethod()
    {
        secondMethod();
    }
    public void secondMethod()
    {
        int a = 5;
        int b = 0;
        int c = a / b;
    }
    public static void main(String[] args)
    {
        new Thread(new ThreadExceptionTest()).start();
    }
}

程序在Thread的run方法中出现了ArithmeticException异常,这个异常的源头是ThreadExceptionTest的secondMethod方法。这个异常传播到Thread类的run方法就会结束(如果该异常没有得到处理,将会导致该线程中止运行)

Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at ThreadExceptionTest.secondMethod(ThreadExceptionTest.java:15)
at ThreadExceptionTest.firstMethod(ThreadExceptionTest.java:9)
at ThreadExceptionTest.run(ThreadExceptionTest.java:5)
at java.lang.Thread.run(Unknown Source)

异常处理规则

  • 使程序代码混乱最小化

  • 捕获并保留诊断信息

  • 通知合适的人员

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

不要过度使用异常

过度使用异常主要有两个方面:

  • 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理

  • 使用异常处理来代替流程控制

异常处理机制的初衷是将不可预期的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制

异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制。对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常

不要使用过于庞大的try块

正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常

避免使用Catch All语句

所谓Catch All语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常

try
{
       // code here with checked exceptions
}
catch (Throwable t) 
{
       //exception handler
       t.printStackTrace();
}

这种处理方式有如下两点不足之处:

  • 所有异常都采用相同的处理方式,这将导致无法对不同异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制,这是得不偿失的做法。

  • 这种捕获方式可能将程序中的错误、Runtime异常等可能导致程序终止的情况全部捕获到,从而 “压制”了异常。如果出现了一些“关键”异常,那个异常也会被“静悄悄”地忽略

不要忽略捕获到的异常

通常建议对异常进行适当措施:

  • 处理异常。对异常采用合适的修补,然后绕过异常发生的地方继续执行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作......总之,对于Checked异常,程序应该尽量采用修复

  • 重新抛出新异常。把当前运行环境下能做的事情尽量作完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者

  • 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者来负责处理该异常

你可能感兴趣的:(后端,异常处理,java)