何时捕获异常,何时抛出异常?

84

我一直在编写Java代码。但有时候,我不知道应该何时抛出异常、何时捕获异常。我正在处理一个有很多方法的项目。层次结构如下:

Method A will call Method B and Method B will call some Method C and Method C will call Method D and Method E.

目前我正在做的是-在所有方法中抛出异常,然后在方法A中捕获并记录为错误日志。但我不确定这是否是正确的方式?还是应该开始在所有方法中捕获异常?因此,在何时捕获异常与何时抛出异常之间存在困惑。我知道这是一个愚蠢的问题,但不知何故,我仍然难以理解这个重要概念。

能否有人给我一个详细的示例来说明何时捕获异常与何时抛出异常,以便我的概念得到清晰?在我的情况下,我应该继续抛出异常,然后在主调用的A方法中捕获它吗?


3
在可能处理异常情况的情况下,始终应该在合适的层次上捕获它。 - ppeterka
这不是一个二选一的问题。你可以捕获异常并进行一些处理,然后重新抛出它们。你应该想出一些具体的例子来说明你的想法。按照这里推荐的方式发布一个示例:http://www.sscce.org/ - dcaswell
8个回答

104

当你在知道该如何处理异常的方法中时,应该捕获异常。

例如,暂且不考虑实际工作方式,假设你正在编写一个用于打开和读取文件的库。

因此,你有一个类,比如:

public class FileInputStream extends InputStream {
    public FileInputStream(String filename) { }
}

现在,假设文件不存在。你应该怎么办?如果你很难想出答案,那是因为没有一个... FileInputStream 不知道该如何处理这个问题。所以它将问题上抛,即:

public class FileInputStream extends InputStream {
    public FileInputStream(String filename) throws FileNotFoundException { }
}

现在,假设有人正在使用你的库。他们可能有这样的代码:

public class Main {
    public static void main(String... args) {
        String filename = "foo.txt";
        try {
            FileInputStream fs = new FileInputStream(filename);

            // The rest of the code
        } catch (FileNotFoundException e) {
            System.err.println("Unable to find input file: " + filename);
            System.err.println("Terminating...");
            System.exit(3);
        }
    }
}

在这里,程序员知道该做什么,因此他们捕获异常并处理异常。


1
将一条甚至没有提及文件名的消息打印到stderr,然后强制退出JVM,这几乎不能算是“处理”异常。请改进您的代码。(我知道这只是一个简单的例子,但您可以做得更好...) - user949300
25
@user949300 这是一个 sscce。它旨在提供一个简单示例,而不是用其他内容混淆它。你提到了 filename 的问题,我已经进行了修复。 - durron597
2
@user949300 还要注意的是,它在 main 方法中,那里真的没有太多事情可做。显然,在代码深处强制退出 JVM 是不明智的。 - durron597
@durron597 - 我们认为什么是“好”的异常处理实践?打印堆栈跟踪是否可以被视为“好”或“有用”的处理方式?“好”的处理是否意味着我们应该捕获异常并打印异常原因,用户可以用来调试(例如,由于非英文字符无法解析您的文件)?还是catch块应该做一些事情来解决错误(例如,重试连接到互联网)/优雅地退出(例如,不给用户蓝屏)? - MasterJoe
1
@durron597 - 谢谢。我有点担心因为这样的问题而被踩,但我相信这对人们来说实际上会非常有用。 - MasterJoe
显示剩余2条评论

34

在两种情况下应该捕获异常。

1. 在最低级别处理

这是你与第三方代码集成的级别,例如ORM工具或执行IO操作(访问HTTP资源、读取文件、保存到数据库等)的任何库。也就是说,离开应用程序原生代码与其他组件交互的级别

在这个级别上,不受你控制的意外问题可能会发生,例如连接失败和文件被锁定。

你可能想通过捕获TimeoutException来处理数据库连接故障,以便几秒钟后重试。同样,当访问文件时出现异常,文件可能正在被进程锁定,但在下一时刻可用。

在这种情况下,以下是指南:

  • 只处理特定的异常,例如SqlTimeoutExceptionIOException。永远不要处理通用异常(类型为Exception的异常)
  • 只有当有实际意义的处理方式时才处理异常,例如重试、触发补偿操作或向异常添加更多数据(例如上下文变量),然后重新抛出异常
  • 不要在此处执行日志记录
  • 让其他异常向上传播,因为它们将由第二个情况处理。
  • 2. 在最高级别上

    这将是在异常直接抛出到用户之前可以处理异常的最后一个位置。

    您的目标是记录错误并将详细信息发送给程序员,以便他们可以识别和纠正错误。添加尽可能多的信息,记录下来,然后向用户显示道歉消息,特别是如果这是软件中的错误,他们可能无法做任何事情。

    在这种第二种情况下的指导方针是:

    • 处理泛型Exception类
    • 从当前执行上下文中添加更多信息
    • 记录错误并通知程序员
    • 向用户表示歉意
    • 尽快解决问题

    这些指导方针背后的原因

    首先,异常代表不可逆转的错误。它们代表系统中的错误、程序员犯的错误或应用程序无法控制的情况。

    在这些情况下,通常用户无法做什么。因此,你唯一能做的就是记录错误,采取必要的补救措施,并向用户道歉。如果是程序员犯的错误,最好让他们知道并修复它,努力实现更稳定的版本。
    其次,try catch块根据使用方式可能会掩盖应用程序执行流程。try catch块的功能类似于标签及其goto伴侣,会导致应用程序执行流从一个点跳转到另一个点。

    何时抛出异常

    在开发库的背景下更容易解释。当你遇到错误并且除了让API的使用者知道这个错误并让他们决定怎么做之外,你应该抛出异常。
    想象一下,你是某个数据访问库的开发人员。当你遇到网络错误时,除了抛出异常外,你无法做任何事情。从数据访问库的角度来看,这是一种不可逆转的错误。
    但是,在开发网站时情况就不同了。你可能会捕获这样的异常以进行重试,但如果从外部层面接收到无效参数,则会抛出异常,因为这些参数应该在那里进行验证。

    在表示层中,情况又有所不同,您希望用户提供无效参数。在这种情况下,只需显示友好的消息而不是抛出异常。


    https://roaddd.com/the-only-two-cases-when-you-should-handle-exceptions/中所述。


    4
    我认为这是所有答案中最好、最完整、最详细的回答。 - Fer B.
    我不同意不在最低层执行日志记录的规则。在某些情况下,您可能需要自定义日志记录,以提供超出堆栈跟踪所能提供的附加详细信息。例如,哪些参数值被提供给导致抛出异常的SQL查询。您可以在发生事件的DAL层直接记录该信息,然后重新抛出自定义异常类型(例如LoggedException),以在更高的层次上捕获它,该异常类型表示“此异常已经被记录,因此不要再次记录它,但您仍然需要生成向用户的道歉消息”。 - Jacob Stamm
    简而言之,在需要专门的、高度详细的日志记录的情况下,应该在最低级别捕获并记录它们,然后重新抛出一个自定义异常,高级别异常处理程序将知道不要重新记录。所有其他情况都应遵循您的规则。 - Jacob Stamm

    8

    当函数遭遇到故障或错误时,应该抛出异常。

    函数是一个工作单元,故障应该根据对功能的影响视为错误或其他类型。在函数f内部,如果一个故障阻止了f满足任何调用方的前置条件、实现任何f自身的后置条件或重新确立任何f负责维护的不变量,则该故障被视为错误。

    有三种不同的错误:

    • 一个条件阻止函数满足必须调用的另一个函数的前置条件(例如参数限制);
    • 一个条件阻止函数建立其自身的某个后置条件(例如产生有效返回值是一个后置条件);
    • 一个条件阻止函数重新建立它负责维护的不变量。这是一种特殊类型的后置条件,特别适用于成员函数。每个非私有成员函数的基本后置条件是它必须重新确立其类的不变量。
    任何其他情况都不是错误,不应报告为错误。
    在函数检测到自己无法处理的错误并且阻止其继续进行任何形式的正常或预期操作时报告错误。
    在具有足够知识来处理错误、翻译错误或执行在错误策略中定义的边界(例如在主线程或线程主干上)的位置处理错误。

    来源:C++编码标准:101条规则、指南和最佳实践



    7
    一般来说,应该在能够有所作为的层次上捕获异常。例如,用户试图连接到某个数据库,但在D方法中失败了。
    你想如何处理呢?也许可以弹出一个对话框,显示“抱歉,无法连接到SERVER/DB”或其他信息。是A、B还是C方法创建了这个SERVER/DB信息(比如通过读取设置文件或询问用户输入)并尝试连接呢?这可能是应该处理异常的方法。或者至少是离应该处理它的方法最近的方法。
    这真的因应用程序而异,因此这只能是非常一般性的建议。我的大部分经验都是关于Swing/桌面应用程序的,通常可以根据哪些类正在执行程序逻辑(例如“控制器”)以及谁正在弹出对话框(例如“视图”)来感受。通常,“控制器”应该捕获异常并尝试做些什么。
    在Web应用程序中,情况可能会有所不同。
    以下是一些非常基本的代码,其中大多数类不存在,我也不确定DB的URL是否有意义,但你可以理解一下。模糊的Swing风格...
    /*  gets called by an actionListener when user clicks a menu etc... */
    public URL openTheDB() {
      URL urlForTheDB = MyCoolDialogUtils.getMeAURL(URL somePreviousOneToFillInTheStart);
      try {
         verifyDBExists(urlForTheDB);
         // this may call a bunch of deep nested calls that all can throw exceptions
         // let them trickle up to here
    
         // if it succeeded, return the URL
         return urlForTheDB;
      }
      catch (NoDBExeption ndbe) {
        String message = "Sorry, the DB does not exist at " + URL;
        boolean tryAgain = MyCoolDialogUtils.error(message);
        if (tryAgain)
          return openTheDB();
        else
          return null;  // user said cancel...
      }
      catch (IOException joe) {
        // maybe the network is down, aliens have landed
        // create a reasonable message and show a dialog
      }
    
    }
    

    7
    我将分享一个模式,它在生产环境中拯救过我一两次。
    动机
    我的目标是确保那个可怜的家伙(可能是我),在午夜时刻尝试解决严重支持票证问题时,获得一组嵌套“由...引起”的错误,包括数据如ID,而不会使代码过于混乱。 方法
    为了实现这一点,我捕获所有的已检查异常并将其重新抛出为未经检查的异常。然后在每个架构层的边界处使用全局catch(通常是抽象或注入的,因此只需编写一次)。在这些点上,我可以向错误堆栈添加额外的上下文信息,或者决定是否记录并忽略,或提出自定义已检查异常以保存任何额外的上下文变量。顺便说一下,我仅在顶层记录错误,以防止“双重记录”发生(例如cron作业、spring控制器用于ajax)。
    throw new RuntimeException(checked,"Could not retrieve contact " + id);
    

    采用这种方法,您的GUI或业务层的方法签名不会因为必须声明与数据库相关的异常而变得混乱。

    下面举个实际例子:

    假设我的代码工作是自动续订许多保险单。该架构支持GUI手动触发一个保单的续订。同时,假设某个保单的评级区域的邮政编码在数据库中损坏了。

    我想要实现的错误日志示例如下所示。

    日志消息:由于错误,将策略1234标记为需要手动干预:

    来自堆栈跟踪:续订策略1234时出错。正在回滚事务...此catch还将覆盖保存错误或生成信函等错误。

    来自堆栈跟踪:原因:评级策略1234错误...此catch将捕获检索许多其他对象和算法错误(例如NPE)等错误。

    来自堆栈跟踪:原因:检索评级区域73932错误...

    来自堆栈跟踪:原因:JPA:字段“postcode”中出现意外的null


    6

    在可能的最低级别处理异常。如果方法无法正确处理异常,则应将其抛出。

    • 如果您有连接到资源(例如打开文件/网络)的方法,请使用catch
    • 如果类层次结构中较高的类需要错误信息,请使用throw

    1
    通常情况下,当您想要通知方法的调用者发生了某些失败时,会抛出异常。
    例如:无效的用户输入、数据库问题、网络中断、文件缺失。

    1
    正如其他人所说,一般来说,只有在能够实际处理异常时才应该捕获它,否则就抛出它。
    例如,如果您正在编写读取连接玩家信息的代码以及其中一个 I/O 方法抛出了 IOException 异常,那么您将希望抛出该异常,并且调用 load 方法的代码将希望捕获该异常并相应地处理它(例如断开玩家连接或向客户端发送响应等)。之所以不希望在 load 方法中处理异常,是因为在该方法中,您无法有意义地处理异常,因此您将异常委托给调用者,希望他们能够处理它。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接