7

我们目前有以下复合 if 语句......

if ((billingRemoteService == null)
    || billingRemoteService.getServiceHeader() == null
    || !"00".equals(billingRemoteService.getServiceHeader().getStatusCode())
    || (billingRemoteService.getServiceBody() == null) 
    || (billingRemoteService.getServiceBody().getServiceResponse() == null) 
    || (billingRemoteService.getServiceBody().getServiceResponse().getCustomersList() == null) 
    || (billingRemoteService.getServiceBody().getServiceResponse().getCustomersList().getCustomersList() == null) 
    || (billingRemoteService.getServiceBody().getServiceResponse().getCustomersList().getCustomersList().get(0) == null) 
    || (billingRemoteService.getServiceBody().getServiceResponse().getCustomersList().getCustomersList().get(0).getBillAccountInfo() == null)
    || (billingRemoteService.getServiceBody().getServiceResponse().getCustomersList().getCustomersList().get(0).getBillAccountInfo().getEcpdId() == null)) {
        throw new WebservicesException("Failed to get information for Account Number " + accountNo);
}

return billingRemoteService.getServiceBody().getServiceResponse().getCustomersList().getCustomersList().get(0);

这不能简化为...

try {
    //Check to be sure there is an EpcdId.
    (billingRemoteService.getServiceBody().getServiceResponse().getCustomersList().getCustomersList().get(0).getBillAccountInfo().getEcpdId();
    return billingRemoteService.getServiceBody().getServiceResponse().getCustomersList().getCustomersList().get(0);
} catch (NullPointerException npe) {
    throw new WebservicesException("Failed to get information for Account Number " + accountNo);
}

如果是这样,Java 6 下两种方法之间的“成本”差异是什么?这似乎是一个非常复杂的 if 语句,只是为了验证所有干预调用都不为空。不同账户多次调用此操作。

4

7 回答 7

7

我必须不同意埃德温·巴克的论点。

他说:

正如其他人所说,异常比 if 语句更昂贵。但是,有一个很好的理由不在您的情况下使用它们。“例外是针对特殊事件”

解包消息时,消息中没有的内容是错误检查,而不是异常事件。

这本质上是说,如果您进行错误检查,则预期会出现错误(因为您正在寻找它),因此不会例外。

但这不是“异常事件”的意思。异常事件是指不寻常/不寻常/不太可能发生的事件。例外是关于事件发生的可能性,而不是关于你是否(或应该)期待和/或寻找它。

所以回到第一原则,避免异常的根本原因是成本权衡:显式测试事件的成本与抛出、捕获和处理异常的成本。准确地说。

如果事件的概率是 P

  • 使用异常的平均成本是:

    P * 创建/抛出/捕获/处理异常的成本 + (1 - P) * 没有显式测试的成本

  • 不使用异常的平均成本是:

    P * 条件发生时的测试成本并进行错误处理 + (1 - P) * 条件不发生时的测试成本。

当然,这就是“异常”==“不太可能”出现的地方。因为,如果 P 越来越接近 0,使用异常的开销就会变得越来越小。如果 P 足够小(取决于问题),异常将更有效率。


因此,在回答最初的问题时,这不仅仅是 if / else 与异常的成本。您还需要考虑您正在测试的事件(错误)的可能性

另一件需要注意的是,JIT 编译器有很大的空间来优化这两个版本。

  • 在第一个版本中,可能有很多重复的子表达式计算,以及重复的幕后空值检查。JIT 编译器可能能够优化其中的一些,尽管这取决于是否可能存在副作用。如果不能,那么测试序列可能会相当昂贵。

  • 在第二个版本中,JIT 编译器可以在不使用异常对象的情况下注意到在同一方法中抛出和捕获异常。由于异常对象不会“逃脱”,它可以(理论上)被优化掉。如果发生这种情况,使用异常的开销几乎会消失。


(这是一个有效的示例,可以清楚地说明我的非正式方程式的含义:

  // Version 1
  if (someTest()) {
      doIt();
  } else {
      recover();
  }

  // Version 2
  try {
      doIt();
  } catch (SomeException ex) {
      recover();
  }

和之前一样,设 P 为引发异常的概率。

版本#1 - 如果我们假设someTest()测试成功与失败的代价相同,并使用“doIt-success”来表示没有抛出异常时doIt的代价,那么版本执行一次的平均代价# 1是:

  V1 = cost("someTest") + P * cost("recover") + (1 - P) * cost("doIt-success")

版本 #2 - 如果我们假设doIt()无论是否抛出异常,成本都是相同的,那么版本 #2 的一次执行的平均成本是:

  v2 = P * ( cost("doit-fail") + cost("throw/catch") + cost("recover") ) +
       (1 - P) * cost("doIt-success")

我们从另一个中减去一个来得出平均成本的差异。

  V1 - V2 = cost("someTest") + P * cost("recover") + 
            (1 - P) * cost("doIt-success") -
            P * cost("doit-fail") - P * cost("throw/catch") -
            P * cost("recover") - (1 - P) * cost("doIt-success")

          = cost("someTest") - P * ( cost("doit-fail") + cost("throw/catch") )

请注意,成功的成本recover()doIt()成功的成本抵消了。我们剩下一个积极的部分(进行测试以避免异常的成本)和一个与失败概率成正比的消极部分。该等式告诉我们,无论投掷/接球开销多么昂贵,如果概率P足够接近于零,则差值将是负数


针对此评论:

您不应该为流控制捕获未经检查的异常的真正原因是:如果您调用的方法之一抛出 NPE,会发生什么?您捕获 NPE 假定它来自您的代码,而它可能来自其中一个 getter。您可能在代码下面隐藏了一个错误,这可能会导致大量的调试问题(个人经验)。当您可能通过捕获 NPE 或 IOOBE 等未经检查的异常来进行流控制来隐藏您(或其他人)代码中的错误时,性能参数是无用的。

这与埃德温雄鹿的论点确实相同。

问题是“流量控制”是什么意思?

  • 一方面,抛出和捕获异常是一种流控制形式。所以这意味着你永远不应该抛出和捕获未经检查的异常。这显然没有任何意义。

  • 因此,我们回到争论不同类型的流量控制,这实际上与争论什么是“异常”与什么是“非异常”是一样的。

我认识到在捕获 NPE 和类似情况时需要小心,以确保您不会捕获来自意外来源(即不同的错误)的 NPE。但在 OP 的示例中,这种风险很小。你可以而且应该检查那些看起来像简单吸气剂的东西真的是简单吸气剂。

而且您还必须认识到,捕获 NPE(在这种情况下)会导致代码更简单,这可能比if语句中的长序列条件更可靠。请记住,这种“模式”可以在很多地方复制。

底线是异常和测试之间的选择可能很复杂。一个简单的口头禅告诉你总是使用测试在某些情况下会给你错误的解决方案。并且“错误”可能不太可靠和/或可读性较差和/或代码较慢。

于 2012-09-11T03:13:32.623 回答
2

正如其他人所说,异常比 if 语句更昂贵。但是,有一个很好的理由不在您的情况下使用它们。

例外是针对特殊事件

解包消息时,消息中没有的内容是错误检查,而不是异常事件。

这段代码对其他实例中的数据感兴趣了。向那些其他实例添加一些行为。现在所有的行为都在不在类中的代码中,这是不好的面向对象。

-- for billingRemoteService --
public boolean hasResponse();
public BillingRemoteResponse getResponse();

-- for BillingRemoteResponse --
public List<Customer> getCustomerList();

-- for Customer --
public Customer(Long ecpdId, ...) {
  if (ecpdId == null) throw new IllegalArgumentException(...);
}
于 2012-09-10T20:45:37.503 回答
1

您可以将 Groovy 混合到您的基于 JVM 的应用程序中——这样行会变得相当简单:

def result = billingRemoteService?.
  serviceBody?.
  serviceResponse?.
  customersList?.
  customersList[0];
if ('00' != billingRemoteService?.serviceHeader?.statusCode ||
   result?.
   billAccountInfo?.
   getEcpdId == null)
  throw new WebServicesException
...
于 2012-09-10T20:50:59.140 回答
1

使用 Java 8 选项时,您可以避免 try/catch 和复合 if:

import java.util.List;
import java.util.Optional;

public class Optionals
{
    interface BillingRemoteService
    {Optional<ServiceBody> getServiceBody();}

    interface ServiceBody
    {Optional<ServiceResponse> getServiceResponse();}

    interface ServiceResponse
    {Optional<List<Customer>> getCustomersList();}

    interface Customer
    {Optional<BillAccountInfo> getBillAccountInfo();}

    interface BillAccountInfo
    {Optional<EcpId> getEcpdId();}

    interface EcpId
    {Optional<BillingRemoteService> getEcpdId();}

    Object test(BillingRemoteService billingRemoteService) throws Exception
    {
        return
        billingRemoteService.getServiceBody()
        .flatMap(ServiceBody::getServiceResponse)
        .flatMap(ServiceResponse::getCustomersList)
        .map(l->l.get(0))
        .flatMap(Optional::ofNullable)
        .flatMap(Customer::getBillAccountInfo)
        .flatMap(BillAccountInfo::getEcpdId).orElseThrow(()->new Exception("Failed to get information for Account Number "));
    }
}

这需要您更改这些方法的签名,但如果它们经常返回 null,我认为无论如何都应该这样做。如果不期望 null 而是异常,并且无法更改方法签名,则可以使用异常。我认为运行时差异不是问题,除非您每秒调用此操作数十万次。

于 2016-03-04T12:44:41.777 回答
0

好的,没有一个答案真正回答了这个问题,尽管考虑到我目前的情况,theZ 的建议是检查这个问题的最快方法。这些代码都不是我设计或编写的,它所包含的应用程序非常庞大,这意味着需要数年的重构才能处理这样的每一种情况。

所以,为了大家的熏陶:

我做了一个快速测试来模拟这两种方法所需的类。我不在乎类的任何单个方法运行多长时间,因为它与我的问题无关。我还使用 JDK 1.6 和 1.7 构建/运行。两个 JDK 之间几乎没有区别。

如果一切正常 --- IE 任何地方都没有空值,平均时间是:

Method A (compound IF):  4ms
Method B (exceptions):   2ms

因此,当对象不为空时使用异常的速度是复合 IF 的两倍。

如果我故意在 get(0) 语句中强制一个空指针异常,事情会变得更加有趣。

这里的平均值是:

Method A: 36ms
Method B:  6ms

因此,很明显,在记录的原始案例中,例外是要走的路,成本明智。

于 2012-09-11T02:35:17.490 回答
0

本质上 if/else 与 try catch 不是一回事。这些东西的性能/成本与您的代码无关。一旦您正确设计和实施业务规则,您就可以担心性能。

try/catch 用于程序异常状态的异常处理。而 if else 用于程序的条件状态。

他们有自己的用途。查看您需要的内容并进行相应的编码。

此外,你违反了德米特法则。如果您仔细阅读设计和代码以使其更易于阅读并使其成为可维护性和可扩展性的更好设计,那会更好。

于 2012-09-10T20:31:39.140 回答
0

根据经验,异常处理比 ifs 更昂贵,但我同意 TheZ 的观点,即最好的方法是在预期负载下对两个版本进行基准测试/分析。当您考虑 IO 和网络成本时,差异可能会变得可以忽略不计,这通常会按数量级推断 CPU 成本。另外,请注意,!00.equals可能应该在第二个版本中检查条件。

于 2012-09-10T20:25:47.740 回答