【问题标题】:Should java try blocks be scoped as tightly as possible?java try 块的范围是否应该尽可能紧密?
【发布时间】:2011-02-07 16:42:41
【问题描述】:

有人告诉我,使用 Java 的 try-catch 机制会产生一些开销。因此,虽然有必要将抛出已检查异常的方法放在 try 块中以处理可能的异常,但从性能角度来看,最好将 try 块的大小限制为仅包含可能抛出异常的那些操作。

我不太确定这是一个合理的结论。

考虑以下两个处理指定文本文件的函数的实现。

即使第一个确实会产生一些不必要的开销,我发现它更容易理解。仅通过查看语句就不太清楚异常究竟来自何处,但 cmets 清楚地显示了哪些语句是负责任的。

第二个比第一个更长更复杂。特别是,第一个很好的读行习惯必须被破坏以将readLine 调用放入 try 块中。

在定义中可能引发多个异常的函数中处理异常的最佳做法是什么?

这个包含try块内的所有处理代码:

void processFile(File f)
{
  try
  {
    // construction of FileReader can throw FileNotFoundException
    BufferedReader in = new BufferedReader(new FileReader(f));

    // call of readLine can throw IOException
    String line;
    while ((line = in.readLine()) != null)
    {
      process(line);
    }
  }
  catch (FileNotFoundException ex)
  {
    handle(ex);
  }
  catch (IOException ex)
  {
    handle(ex);
  }
}

这个只包含在 try 块中抛出异常的方法:

void processFile(File f)
{
  FileReader reader;
  try
  {
    reader = new FileReader(f);
  }
  catch (FileNotFoundException ex)
  {
    handle(ex);
    return;
  }

  BufferedReader in = new BufferedReader(reader);

  String line;
  while (true)
  {
    try
    {
      line = in.readLine();
    }
    catch (IOException ex)
    {
      handle(ex);
      break;
    }

    if (line == null)
    {
      break;
    }

    process(line);
  }
}

【问题讨论】:

  • “过早优化”是一个短语,用于描述程序员让性能考虑影响一段代码的设计的情况。这可能会导致设计不如原本的干净或代码不正确,因为优化会使代码复杂化,而程序员会因优化而分心。 - 维基百科 [en.wikipedia.org/wiki/Program_optimization#When_to_optimize]
  • “我们应该忘记小的效率,比如说大约 97% 的时间:过早的优化是万恶之源。但我们不应该放弃那关键的 3% 的机会。” - 克努斯 [stackoverflow.com/questions/211414/…
  • 过早的优化是一切科学的根源。

标签: java exception-handling readability


【解决方案1】:

在我看来,将 try 块放在可能引发异常的特定代码周围,使其更易于阅读。您可能希望为每个错误显示不同的消息并向用户提供说明,具体取决于错误发生的位置。

但是,大多数人提到的性能问题与引发异常有关,而不是与 try 块本身有关。

换句话说,只要您从未引发错误,try 块就不会显着影响性能。您不应将 try 块视为另一个流控制结构并引发错误以分支代码。这就是你想要避免的。

【讨论】:

  • 只是对最后一段的附加说明。如果你有一个循环的 try/catch inside,你可以捕获一个异常并继续循环。如果你的 try/catch 在循环之外,你的循环将被中止。任何一个都可能是您想要的,并且会影响您放置 try/catch 的位置。
【解决方案2】:

这是最糟糕的过早优化。不要这样做。

“我们应该忘记小的效率,比如大约 97% 的时间:过早的优化是万恶之源”- Knuth。

【讨论】:

    【解决方案3】:

    没有。您应该考虑的唯一一件事是您可以在哪里合理处理异常以及需要回收哪些资源(使用 finally)。

    【讨论】:

    • +1 用于提及清理资源。在 OP 中,只有第一个实现适合添加单个 finally 子句来关闭文件。
    • +1 用于提及清理。到目前为止,我从未在我使用的任何 Reader 对象上调用过 close。真丢人!
    • Java 7 try-with-resources 在多大程度上改变了您的偏好?
    • 我大力提倡将 try-catch 与 try-finally 分开,它们有两个不同的目的,应该这样对待。因此,我发现 try-with-resources 虽然更易于阅读,但根本不会影响代码的结构。
    【解决方案4】:

    这里的基本前提是false:try 块的大小对性能没有影响。性能会受到运行时实际引发异常的影响,这与 try 块的大小无关。

    但是,保持较小的 try 块可以产生更好的程序。

    您可能会捕获异常以恢复和继续,或者您可能会捕获它们只是为了将它们报告给调用者(或通过某些 UI 向人类报告)。

    在第一种情况下,您可以从中恢复的故障通常非常具体,这会导致较小的 try 块。

    在第二种情况下,异常被捕获以便可以被另一个异常包装并重新抛出,或显示给用户,小的try块意味着您可以更准确地知道哪个操作失败了,并且进行该调用的更高级别的上下文。这允许您创建更具体的错误报告。

    当然,这些指南有……例外(抱歉!)。例如,在某些情况下,非常具体的错误报告可能是一个安全问题。


    了解try 块对编译代码的影响可能很有用。它根本不会改变编译的指令! (当然,对应的catch 块可以,因为它和其他任何代码一样。)

    try 块在与该方法关联的异常表中创建一个条目。该表具有一系列源指令计数器、异常类型和目标指令。当引发异常时,将检查此表以查看是否存在具有匹配类型的条目,以及包含引发异常的指令的范围。如果是,则执行分支到相应的目标编号。

    要意识到的重要一点是,除非需要,否则不会查询此表(并且对运行性能没有影响)。 (忽略加载类的一点开销。)

    【讨论】:

    • 没错。同样值得注意的是,与 try-catch 相关的开销仅在抛出异常或块具有 finally 子句时才会发生,如 VM 规范 java.sun.com/docs/books/jvms/second_edition/html/… 中所述。
    • @ig0774 那么,如果我添加一个像finally { in.close(); } 这样的finally 块,那么相关的开销会生效吗?根据我对规范的理解,finally 子句只是在编译的 try 块中添加了一条额外的指令,以在正常返回之前将 finally 块作为子例程调用。
    • @isme:那条指令就是我所说的 finally 块的“开销”;它与 try {} 块不同,它不添加任何指令或自行进行特殊处理。
    • 因此 try-catch-finally 机制产生的开销与 finally 块的数量成正比。并且最多可以有一个 finally 块。因此,在最坏的情况下,JVM 会执行一条额外的指令,即“执行 finally 块”。特别是,开销的数量与 try 块的大小不成正比!感谢 erickson 和 ig0774 让我明白了这一点!
    【解决方案5】:

    有人告诉我,使用 Java 的 try-catch 机制有一些开销。

    当然。方法调用也有开销。但是您不应该将所有代码放在一个方法中。

    不要吹嘘过早的优化号角,但重点应该是易于阅读、组织等。语言结构很少像系统组织和算法选择那样影响性能。

    对我来说,第一个最容易阅读。

    【讨论】:

      【解决方案6】:

      第二种方法会产生一个编译器错误,reader 可能没有被初始化。您可以通过将其初始化为 null 来解决此问题,但这仅意味着您可以获得 NPE,并且没有任何优势。

      【讨论】:

      • 不会产生这样的编译错误。 catch 块末尾的 return 语句确保读取器将被初始化或永远不会读取!当然,我只是在编译器发出您描述的错误之后才添加此语句。 :)
      • 确实如此——我错过了。谢天谢地,编译器在这里为我们解决这些问题。 :)
      【解决方案7】:

      第二种方法几乎没有好处。毕竟,如果您可以成功打开文件但无法从中读取文件,那么您的计算机就出了问题。因此知道 io 异常来自 readLine() 方法很少有用。同样如您所知,无论如何都会针对不同的问题引发不同的异常(FileNotFoundException等)

      只要您使用“逻辑”块确定它的范围,即一次性打开、读取和关闭文件,我会使用第一种方法。它更易于阅读,尤其是在处理 IO 时,try-catch 开销使用的处理器周期将是最小的。

      【讨论】:

        猜你喜欢
        • 2011-11-16
        • 2012-05-13
        • 1970-01-01
        • 2022-08-19
        • 2011-02-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多