Java 6中复合if /或与try / catch的成本

如果声明,我们目前有以下化合物……

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语句,只是为了validation所有插入的调用都不是null。 对于不同的帐户,此操作被多次调用。

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

他说:

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

解压缩消息时,消息中不存在错误检查,而不是exception事件。

这基本上是说如果你进行错误检查,那么预计会出现错误(因为你正在寻找它),因此也不例外。

但这不是“特殊事件”的意思。 特殊事件意味着exception/exception/不太可能发生的事件。 特殊情况是关于事件发生的可能性,而不是关于您是否(或应该)期待和/或寻找事件。

因此,回到第一原则,避免exception的根本原因是成本权衡:明确测试事件的成本与抛出,捕获和处理exception的成本。 确切地说。

如果事件的概率是P.

  • 使用例外的平均成本是:

    P *创建/抛出/捕获/处理exception的成本+(1 – P)*没有明确测试的成本

  • 不使用例外的平均成本是:

    P *成本测试时发生的情况并进行error handling+(1 – P)*在没有发生条件时进行测试的成本。

当然,这是“特殊”==“不太可能”的地方。因为,如果P接近0,使用exception的开销变得越来越不重要。 如果P足够小(取决于问题),exception将更有效。


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

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

  • 在第一个版本中,可能会重复计算子表达式,并重复幕后空值检查。 JIT编译器可能能够优化其中的一部分,但这取决于是否存在副作用。 如果不能,则测试顺序可能相当昂贵。

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


(这是一个有用的例子,可以清楚地表明我的非正式方程式意味着什么:

  // Version 1 if (someTest()) { doIt(); } else { recover(); } // Version 2 try { doIt(); } catch (SomeException ex) { recover(); } 

和以前一样,让P成为引起exception的概率

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

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

版本#2 – 如果我们假设doIt()的成本是否相同,无论是否抛出exception,那么一次执行版本#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()成功的成本将被取消。 我们留下了一个积极的成分(为避免exception而进行测试的成本)和一个与失败概率成正比的负面成分。 该等式告诉我们, 无论抛出/捕获开销有多昂贵 ,如果概率P足够接近零,则差异将为负


回应此评论:

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

这与Edwin Bucks的论点非常相似。

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

  • 一方面,抛出和捕获exception是流量控制的一种forms 。 所以这意味着你永远不应该抛出并捕获未经检查的exception。 这显然毫无意义。

  • 因此,我们回过头来争论不同类型的流量控制,这实际上是关于什么是“特殊”与“非特殊”的争论。

我认识到你在捕捉NPE时需要小心谨慎,以确保你没有抓住一个来自意外来源的东西(即一个不同的bug)。 但在OP的例子中,风险很小。 你可以而且应该检查看起来像简单吸气剂的那些东西真的是简单的吸气剂。

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

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

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

例外情况适用于特殊事件

解压缩消息时,消息中存在错误检查,而不是exception事件。

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

 -- for billingRemoteService -- public boolean hasResponse(); public BillingRemoteResponse getResponse(); -- for BillingRemoteResponse -- public List getCustomerList(); -- for Customer -- public Customer(Long ecpdId, ...) { if (ecpdId == null) throw new IllegalArgumentException(...); } 

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

 def result = billingRemoteService?. serviceBody?. serviceResponse?. customersList?. customersList[0]; if ('00' != billingRemoteService?.serviceHeader?.statusCode || result?. billAccountInfo?. getEcpdId == null) throw new WebServicesException ... 

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

 import java.util.List; import java.util.Optional; public class Optionals { interface BillingRemoteService {Optional getServiceBody();} interface ServiceBody {Optional getServiceResponse();} interface ServiceResponse {Optional> getCustomersList();} interface Customer {Optional getBillAccountInfo();} interface BillAccountInfo {Optional getEcpdId();} interface EcpId {Optional 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但是exception,并且无法更改方法签名,则可以使用exception。 我不认为运行时差异是一个问题,除非你每秒调用这个操作数十万次。

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

基本上如果/ else vs try catch不是同一个东西。 这些东西的性能/成本与您的代码无关。 一旦您正确地设计和实施业务规则,您就可以担心性能。

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

他们有自己的用途。 看看你需要什么,并相应地编码。

而且,你违反了德米特定律。 如果你仔细阅读设计和代码以使其更易于阅读以及使其成为更好的可维护性和可扩展性设计,那会更好。

好的,没有一个答案真的回答了这个问题,尽管根据我目前的情况,Z的建议是检查这个问题的最快方法。 这些代码都不是由我设计或编写的,它所属的应用程序是庞大的,这意味着需要多年的重构来处理这样的每一种情况。

所以,对每个人的启发:

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

如果事情有效 – IE在任何地方都没有空,平均时间是:

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

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

如果我故意在get(0)语句中强制执行空指针exception,事情会变得更有趣。

这里的平均值是:

 Method A: 36ms Method B: 6ms 

因此,很明显,在最初记录的案例中,例外是成本方式。