Стоимость соединения, если / или по сравнению с try/catch в Java 6

В настоящее время у нас есть следующий составной оператор 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 только для того, чтобы проверить, что все промежуточные вызовы не являются нулевыми. Эта операция вызывается много раз для разных учетных записей.

7 ответов

Я должен не согласиться с аргументом Эдвина Бака.

Он говорит:

Как утверждают другие, исключения являются более дорогостоящими, чем заявления. Однако есть веская причина не использовать их в вашем случае. "Исключения для исключительных событий"

При распаковке сообщения, то, что не содержится в сообщении, является ожидаемой проверкой ошибок, а не исключительным событием.

По сути, это говорит о том, что если вы выполняете проверку ошибок, то ожидается ошибка (потому что вы искали ее), и поэтому она не исключительная.

Но это не то, что означает "исключительное событие". Исключительное событие означает необычное / необычное / маловероятное событие. Исключительным является вероятность события, а не то, ожидаете ли вы (и должны ли) его ожидать.

Итак, возвращаясь к первым принципам, основополагающим аргументом для исключения исключений является компромисс стоимости: стоимость явного тестирования события по сравнению со стоимостью генерации, улавливания и обработки исключения. Точнее.

Если вероятность события равна 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, предполагая, что оно пришло из вашего кода, когда, возможно, пришло от одного из получателей. Возможно, вы скрываете ошибку под кодом, и это может привести к огромным головным болям отладки (личный опыт). Аргументы производительности бесполезны, когда вы можете скрывать ошибки в своем (или чужом) коде, перехватывая непроверенное исключение, такое как NPE или IOOBE, для управления потоком.

Это действительно тот же аргумент, что и у Эдвина Бакса.

Проблема в том, что означает "управление потоком"?

  • С одной стороны, выбрасывание и отлов исключений - это форма управления потоком. Так что это означает, что вы никогда не должны бросать и ловить непроверенные исключения. Это явно не имеет смысла.

  • Итак, мы возвращаемся к рассуждению о различных видах управления потоком, который на самом деле является тем же рассуждением о том, что является "исключительным" по сравнению с тем, что "исключительным".

Я признаю, что вы должны быть осторожны, когда ловите NPE и тому подобное, чтобы быть уверенным, что вы не поймаете тот, который исходит из неожиданного источника (т.е. другой ошибки). Но в примере ОП существует минимальный риск этого. И вы можете и должны проверить, что те вещи, которые выглядят как простые получатели, действительно являются простыми получателями.

И вы также должны признать, что перехват NPE (в данном случае) приводит к более простому коду, который, вероятно, будет более надежным, чем длинная последовательность условий в 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(...);
}

Вы можете избежать как try/catch, так и составного ifs при использовании опций Java 8:

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 не ожидается, но есть исключение, и изменение сигнатуры метода невозможно, то могут использоваться исключения. Я не думаю, что разница во времени выполнения является проблемой, если вы не вызываете эту операцию сотни тысяч раз в секунду.

Вы можете смешать Groovy с вашим JVM-приложением - тогда строка станет значительно проще:

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

Как правило, обработка исключений большого пальца обходится дороже, чем ifs, но я согласен с TheZ в том, что наилучшим подходом будет сравнение / профилирование обеих версий при ожидаемой нагрузке. Разница может стать незначительной, если учесть затраты на ввод-вывод и сетевые подключения, которые обычно экстраполируют затраты процессора на порядки. Также обратите внимание, что !00.equals условие должно быть проверено во второй версии.

По существу, если / else против try catch - это не одно и то же. Производительность / стоимость этих вещей не имеют значения для вашего кода. После того, как вы правильно разработали и внедрили свои бизнес-правила, вы можете беспокоиться о производительности.

try / catch предназначен для обработки исключений для исключительных состояний программы. а если еще для условных состояний программы.

У них есть свое собственное использование. Посмотрите, что вам нужно, и код соответственно.

Более того, вы нарушаете закон Деметры. было бы лучше, если бы вы перешли к дизайну и коду, чтобы сделать его более простым для чтения, а также сделать его лучшим дизайном для удобства обслуживания и расширяемости.

Хорошо, ни один из ответов на самом деле не ответил на вопрос, хотя предложение Z было самым быстрым способом проверить это, учитывая мои текущие обстоятельства. Ни один из этих кодов не был разработан или написан мной, и приложение, частью которого он является, является массовым, что потребовало бы человеко-лет рефакторинга для обработки каждой подобной ситуации.

Итак, для всеобщего назидания:

Я сделал быстрый тест, который проверяет классы, необходимые для обоих методов. Мне все равно, как долго работает какой-либо из отдельных методов классов, так как это не имеет отношения к моему вопросу. Я также собрал / запустил JDK 1.6 и 1.7. Практически не было никакой разницы между двумя JDK.

Если все работает, то есть IE нигде не имеет значения null, среднее время:

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

Таким образом, использование исключений, когда объекты не равны нулю, в два раза быстрее, чем составной IF.

Все становится еще интереснее, если я намеренно форсирую исключение нулевого указателя в операторе get(0).

Средние значения здесь:

Method A: 36ms
Method B:  6ms

Итак, ясно, что в оригинальном документированном случае исключения - это путь, с точки зрения затрат.

Другие вопросы по тегам