关于性能:哪个更好/更高效:检查错误值或捕获Java中的异常

Which is better/more efficient: check for bad values or catch Exceptions in Java

在Java中更有效:检查坏值以防止异常或让异常发生并捕获它们吗?

下面是两个示例代码块来说明这一区别:

1
2
3
4
5
6
7
8
9
10
11
12
void doSomething(type value1) {
    ResultType result = genericError;

     if (value1 == badvalue || value1 == badvalue2 || ...) {
          result = specificError;
     } else {
          DoSomeActionThatFailsIfValue1IsBad(value1);
          // ...
          result = success;
     }
     callback(result);
}

对战

1
2
3
4
5
6
7
8
9
10
11
12
void doSomething(type value1) {
     ResultType result = genericError;
     try {
          DoSomeActionThatFailsIfValue1IsBad(value1);
          // ...
          result = success;
     } catch (ExceptionType e) {
          result = specificError;
     } finally {
          callback(result);
     }
}

一方面,你总是在做比较。另一方面,我真的不知道系统内部如何生成异常、抛出异常,然后触发catch子句。它听起来效率较低,但是如果它在非错误情况下不增加开销,那么平均来说效率更高。这是什么?它是否添加了类似的检查?在为异常处理而添加的隐式代码中是否存在这种检查,即使附加了显式检查层?也许它总是取决于异常的类型?我不考虑什么?

我们还假设所有"坏值"都是已知的——这是一个显而易见的问题。如果您不知道所有的坏值——或者列表太长而且不规则——那么无论如何,异常处理可能是唯一的方法。

那么,每种方法的优缺点是什么?为什么?

要考虑的附带问题:

  • 如果大多数情况下值为"坏"(会引发异常),您的答案会如何更改?
  • 这在多大程度上取决于正在使用的虚拟机的具体情况?
  • 如果对language-x提出同样的问题,答案会不同吗?(更普遍的情况是,它询问是否可以假定检查值总是比依赖异常处理更有效,因为它增加了当前编译器/解释器的开销。)
  • (新)抛出异常的动作很慢。输入一个try块是否有开销,即使没有引发异常?

相似之处如下:

  • 这类似于此答案中的代码示例,但声明它们仅在概念上相似,而不是编译后的实际情况。
  • 前提类似于这个问题,但在我的例子中,任务的请求者(例如"某物")不是方法的调用者(例如"dosomething")(因此没有返回)。
  • 这个很相似,但我没有找到我问题的答案。

  • 和其他太多的问题相似,除了:

    我不是在问理论上的最佳实践。我在问更多关于运行时性能和效率的问题(这意味着,对于特定情况,会有非意见的答案),尤其是在资源有限的平台上。例如,如果唯一错误的值只是一个空对象,那么检查它或只是尝试使用它并捕获异常会更好/更有效吗?


"如果大多数情况下值为"bad"(将引发异常),您的答案将如何更改?"我想这就是钥匙。与比较相比,异常是昂贵的,因此您确实希望在异常情况下使用异常。

同样,您关于这个答案可能如何变化的问题取决于语言/环境的联系:在不同的环境中,异常的开销是不同的。例如,当第一次抛出异常时,.NET 1.1和2.0的速度非常慢。


纯粹从效率的角度出发,结合您的代码示例,我认为这取决于您期望看到坏值的频率。如果不好的值不太常见,则比较更快,因为异常代价很高。但是,如果坏值非常罕见,则使用异常可能更快。

不过,归根结底,如果您在寻找性能,请分析您的代码。这段代码甚至可能不是一个问题。如果是的话,那么尝试两种方法,看看哪个更快。同样,这取决于你期望看到坏值的频率。


我几乎找不到有关抛出异常的成本的最新信息。很明显,一定有一些,您正在创建一个对象,并且可能正在获取堆栈跟踪信息。

在具体的示例中,您将讨论:

1
2
3
4
5
6
7
if (value1 == badvalue || value1 == badvalue2 || ...) {
      result = specificError;
 } else {
      DoSomeActionThatFailsIfValue1IsBad(value1);
      // ...
      result = success;
 }

我这里的问题是,如果(可能不完全)复制调用方中的逻辑,而该逻辑应该由您所调用的方法所拥有,那么您就处于危险之中。

因此我不会进行这些检查。您的代码没有执行实验,它"知道"应该发送的数据,我想?因此,抛出异常的可能性应该很低。因此,保持简单,让被叫方进行检查。


嗯,例外情况更昂贵,是的,但对我来说,它是关于如何权衡效率成本和糟糕设计。除非您的用例需要它,否则始终坚持最佳设计。

真正的问题是,你什么时候抛出一个异常?在特殊情况下。

如果您的参数不在您要查找的范围内,我建议您返回错误代码或布尔值。

例如,一个方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int IsAuthenticated(String username, String password)
{

     if(!Validated(username,password)
     {
          // just an error
          // log it
          return -2;  
     }

     // contacting the Database here
     if cannot connect to db
     {
          // woww this is HUUGE
          throw new DBException('cannot connect'); // or something like that
     }

     // validate against db here
     if validated, return 0;

    // etc etc

}

那是我的2美分


在我看来,如果只是为了拥有一个安全运行的系统,那么您应该在任何可能引发异常的地方使用try/catch块。如果首先检查可能的数据错误,则可以更好地控制错误响应。所以我建议两者都做。


像这样的问题就像问,

"用所有抽象函数编写接口或基类是否更有效?"

哪一种效率更高重要?其中只有一个是解决特定情况的正确方法


为了安全起见,假设异常是昂贵的。它们通常是,如果不是,至少会促使您明智地使用异常。(进入一个try块通常是非常便宜的,因为实现者尽其所能做到这一点,即使以使异常变得更昂贵为代价。毕竟,如果异常被正确使用,代码进入try块的频率将比它抛出的频率高很多倍。)

更重要的是,例外是一个样式问题。异常情况的例外使代码更简单,因为错误检查代码更少,所以实际的功能更清晰、更紧凑。

然而,如果在更正常的情况下抛出异常,那么读者必须记住无形的控制流,这与Intercal的COME FROM...UNLESS...声明相当。(Intercal是早期的笑话语言之一。)这非常混乱,很容易导致误读和误解代码。

我的建议适用于我所知道的每种语言和环境:

别担心这里的效率。除了效率之外,还有很强的理由可以证明以有效的方式使用异常。

自由使用试块。

在特殊情况下使用例外。如果可能出现异常,则测试异常并以另一种方式进行处理。


通常,人们会假设try-catch更昂贵,因为它在代码中看起来更重,但这完全取决于jit。我的猜测是,如果没有一个真实的案例和一些性能度量,就无法判断。比较可能会更昂贵,尤其是当您有许多值时,例如,或者因为在许多情况下==不起作用,您必须调用equals()

至于应该选择哪一个(如"代码样式"),我的答案是:确保用户在失败时收到有用的错误消息。其他的都是品味问题,我不能给你规定。


我个人的观点是,异常表明某些东西被破坏了——这很可能是一个使用非法参数或除数为零或找不到文件等调用的API。这意味着可以通过检查值来抛出异常。

对于你的代码的读者——也是我个人的观点——如果你能确定它不是被各种奇怪的抛出(如果被用作程序流的一部分,它本质上是伪装的goto),那么遵循这个流程就容易多了。你根本就没什么可想的了。

我认为这是件好事。"聪明的"代码很难让人信服。

另一方面,JVM变得更聪明了,为提高效率而编码通常是没有回报的。


最理想的情况是,我想你会发现这可能是一次洗漱。他们都会表现得很好,我认为抛出异常永远不会成为你的瓶颈。您可能更关注Java的设计(以及其他Java程序员会期待什么),这是抛出的异常。Java是非常围绕抛出/捕获异常而设计的,您可以打赌设计者会尽可能高效地实现该过程。

我认为这主要是一种哲学和语言文化之类的东西。在Java中,一般公认的做法是方法签名是方法和调用它的代码之间的一种契约。因此,如果收到不正确的值,通常会抛出未检查的异常,并让它在更高的级别上处理:

1
2
3
4
5
6
7
8
9
public void setAge(int age)
{
    if(age < 0)
    {
        throw new IllegalArgumentException("Array can't be negative");
    }

    this.age = age;
}

在这种情况下,调用者破坏了他们的契约结束,所以您向他们吐出了他们的输入,并有一个异常。"throws"条款是在你因某种原因不能履行合同时使用的。

1
2
3
4
5
6
7
8
9
10
11
public void readFile(String filename) throws IOException
{
   File myfile = new File(filename);
   FileInputStream fis = new FileInputStream(myfile);

   //do stuff

   fis.read();

   //do more stuff
}

在这种情况下,作为方法编写者,您已经破坏了合同的结尾,因为用户给了您有效的输入,但是由于IOException,您无法完成他们的请求。

希望能让你走上正轨。祝你好运!


注意,如果您的代码不抛出异常,那么它并不总是意味着输入在界限内。依赖于标准JAVA(API+JVM)抛出异常,例如EDCOX1、1或EDCOX1×2是验证输入的一种非常不健康的方式。垃圾进入有时会产生垃圾,但没有例外。

是的,例外是相当昂贵的。它们不应该在正常的处理流程中被抛出。