Java — это универсальный язык программирования, имеющий много альтернативных решений для ваших определённых задач. Тем не менее, существуют хорошие подходы, которым следует следовать, и также существуют некоторые неудачные подходы, которые мы до сих пор в большинстве своём используем.

Один из наиболее распространённых неудачных подходов — это использование исключений для контроля потока выполнения. Этого следует избегать по двум причинам:

  1. Это снижает производительность и быстродействие вашего кода
  2. Это делает ваш код менее читаемым

Давайте начнём с рассмотрения примера. Здесь исключение используется для управления потоком выполнения:

   public static int findAge(String name) {
       try {
           String ageAsString = findUser(name);
           return ageAsString.length();
       } catch (NameNotFoundException e) {
           return 0;
       }
   }
   private static String findUser(String name) {
       if(name==null) {
           throw new NameNotFoundException();
       }
       return name;
   }

Если пользователь предоставит не нулевое значение имени, то метод findAge вернёт длину этого имени, но если имя пользователя будет null, то тогда метод findUser выбросит исключение NameNotFoundException и в этом случае метод findAge вернёт 0.

Как мы можем преобразовать этот код без использования исключений? Вообще, существует множество вариантов, рассмотрим один из них:

public static int findAgeNoEx(String name) {
       String ageAsString = findUserNoEx(name);
       return ageAsString.length();
   }
   private static String findUserNoEx(String name) {
       if(name==null) {
           return "";
       }
       return name;
   }

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

public class ControlFlowWithExceptionOrNot {
   public static class NameNotFoundException extends RuntimeException {
       private static final long serialVersionUID = 3L;
   }
   private static final int TRIAL = 10000000;
   public static void main(String[] args) throws InterruptedException {
       long start = System.currentTimeMillis();
       for (int i = 0; i < TRIAL; i++) {
           findAgeNoEx(null);
       }
       System.out.println("Duration :" + (System.currentTimeMillis() - start));
       long start2 = System.currentTimeMillis();
       for (int i = 0; i < TRIAL; i++) {
           findAge(null);
       }
       System.out.println("Duration :" + (System.currentTimeMillis() - start2));
   };
   public static int findAge(String name) {
       try {
           String ageAsString = findUser(name);
           return ageAsString.length();
       } catch (NameNotFoundException e) {
           return 0;
       }
   }
   private static String findUser(String name) {
       if (name == null) {
           throw new NameNotFoundException();
       }
       return name;
   }
   public static int findAgeNoEx(String name) {
       String ageAsString = findUserNoEx(name);
       return ageAsString.length();
   }
   private static String findUserNoEx(String name) {
       if (name == null) {
           return "";
       }
       return name;
   }
}

Вывод:

Duration :16
Duration :6212

Как видно, использование исключения обошлось нам в тысячи миллисекунд на моём Intel Core i7-3630QM.

Если мы сравним два наших findAge метода с точки зрения читабельности, то метод без исключения абсолютно понятен: во-первых мы может быть абсолютно уверены, что метод findUser возвращает строку; а во-вторых независимо от того, какая именно строка будет возвращена, мы получим её длину. В то же время, метод с исключением несколько запутанный: не до конца ясно что именно возвращает метод findUser. Он может вернуть строку, а может и выбросить исключение и из сигнатуры метода этого не видно. По этой причине парадигма функционального программирования не приветствует использование исключений.

В конце концов, будет лучше, если вы будете использовать исключения там, где действительно возникает исключительная ситуация и это необходимо. Если же вы будете использовать исключения для управления потоком выполнения, это вызовет падение производительности программы и сделает ваш код менее читабельным.

Надеюсь, данная статья была вам интересна, а возможно и полезна.

Комментарии (26)


  1. zolotyh
    17.11.2019 00:19
    +1

    Иногда может потребоваться вернуть пустую строку. Как тогда отличать этот случай от случая, когда что-то идёт не так? И ещё один вопрос: что делать с другими, более развесистыми типами и более сложными случаями? Длинна строки обычно считается по месту, IMHO метод ей не нужен.


  1. atamur
    17.11.2019 00:21
    +6

    Рекомендация избегать исключений так как они ухудшают производительность это преждевременная оптимизация — вполне вероятно в вашем проекте полный отказ от них не повысит производительность и на 2%.


    С другой стороны приведённый пример с возвратом пустой строки противоречит второму совету — повышать понятность кода: пустая строка неотличима от реального имени и такой метод может привести к трудноуловимым ошибкам.


    Подобный подход имеет место и даже имя — https://ru.m.wikipedia.org/wiki/Null_object_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F), но использовать его нужно явно и осторожно.


    В данном случае функциональный подход рекомендует не просто отказаться от исключения, а явно отобразить исключение в возвращаемом значении, например используя Optional.


  1. tuxi
    17.11.2019 00:55

    Крайности всегда плохо. НУ и делать проверку входных параметров на null — это хорошая практика. В этой проверке можно вернуть как значение, так и бросить исключение. Все зависит от контекста. Например:

    public int getDiscountValue(Client client) { 
      if (client == null) { 
        return 0; 
      }
      return ...
    }
    

    а иногда стоит и исключение бросить, чтобы последующая логика стала более прозрачной
    public int getDiscountValue(ClientType clientType) throws NotFoundException { 
      if (clientType == null) { 
        throw new NotFoundException("client type is null ...");
      }
      return ...
    }
    

    ИМХО надо уметь совмещать оба подхода.
    И отделять очевидное от не очевидного.


  1. ukt
    17.11.2019 01:41
    +1

    Есть уже optional с java 8, может его пользовать?
    Действительно стоит использовать эксепшены для логики?
    То что вы длину в ексепшене возвращаете 0 — это точно не айс, ибо одно из полей фио действительно может быть 0 и это валидное значение, но по логике оно не валидное. Поэтому либо null, либо Integer val = null, аналогично и с именем.


  1. BugM
    17.11.2019 02:08

    Уже много лет недоумеваю. Где tryParse или что угодно похожее чтобы ловить ошибки без исключений?

    https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html#parseInt(java.lang.String)

    PS: Я в курсе про все библиотеки. Но почему этого нет в SDK? Так ведь и до плюсов докатимcя где уже лет 15 не могут set.contains сделать.


    1. sshikov
      17.11.2019 10:37

      А зачем вам в JDK то, что можно сделать библиотекой? Я еще понимаю, когда нельзя — тут желание расширить JDK вполне естественно. Ведь все равно то что вдруг появится в JDK, появится лишь в свежих версиях — а использовать скажем Vavr вы можете уже сегодня (а точнее позавчера), начиная с Java 8.


      1. BugM
        17.11.2019 13:18

        Потому что это удобно и логично.
        Потому что это будут массово использовать. Неудача при парсинге int это далеко не всегда ошибка когда надо кидать исключение.
        Контакт там не страшный, испортить что-то через много лет сложно.

        Если вводить одновременно с inline типами, то перейдут все. И достаточно быстро. Киллер фича же. Исключение это самый махровый энтерпрайз.

        Вместо var проще использовать Идею. Она сама великолепно пишет типы. А читать код с явными типами в среднем проще чем код с var.
        И остаются только места где область видимости пяток строк и тип очень длинный и очевидный.

        for(var item : items) {}


        1. sshikov
          17.11.2019 13:43

          >Потому что это удобно и логично.
          С вашей точки зрения? Ну не вопрос, хотя с моей удобно и логично просто заменить библиотеку, там где это можно. Вот я прямо сейчас пишу код вида Try.of(()->...), и совершенно не страдаю от того, что это не часть JDK. И кстати да, возможность для написания таких библиотек дала Java 8, с появлением лямбд. Сделать лямбду при помощи библиотеки либо нельзя, либо слишком сложно. И в этом я вижу разницу между тем, что нужно в JDK, а что можно библиотекой.

          >Исключение это самый махровый энтерпрайз.
          Отличие энтерпрайза от всех остальных в первую очередь в том, что там начали все автоматизировать много лет назад. Поэтому там работает множество легаси систем, переписывать которые на новые технологии слишком дорого — и потому глупо.

          Ну и вообще, когда говорят «все», подразумевая «кроме энтерпрайза», для меня лично это звучит как «никто». Потому что объемы и масштабы этих остальных «всех» зачастую просто несравнимы.

          Например, у нас уже довольно много лет функционируют кластеры Hadoop на версии 2.6, которая не поддерживает Java новее чем 1.8. И никогда не будет. В том числе потому, оно еще и е реализовано.

          И никто не будет переходить ни на какие inline типы — потому что остановить кластер из сотен машин для обновления хадупа практически не реально, а профит от такого перехода вообще не очевиден. То что для вас киллер фича, для проекта который крутится который год в таком режиме, который дорого останавливать, и нельзя ломать совместимость — это может быть наоборот, шоу стоппер.

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


          1. BugM
            18.11.2019 10:26
            -2

            Я и говорю. Зачем в set функция contains? Она же есть в любой библиотеке, да и написать совсем несложно. В плюсы так и превратимся.


            Как же хорошо что время монстров которым "переходить на новые технологии глупо" проходит. Сейчас кто не бежит со всех ног тот исчезает. Самый яркий пример это карточка от Эппла. По сравнению с российскими она простенькая и обычная, а по сравнению с "множество легаси систем, переписывать которые на новые технологии слишком дорого — и потому глупо" очень продвинутая и удобная.


            Попробуйте вместо модных девопсов нанять обычных админов и выделить им ресурсы разработки. Жить с наследием предков которое все тронуть бояться страшно. План обновления кластера нужен обязательно. Да и резервирование пригодится. Чтобы без боязни выключать можно было.


            1. uncle_doc
              18.11.2019 11:00

              Есть в этом деле и «обратная сторона медали». В таком замечательном языке как Swift от Apple функция substring by index — помечена как deprecated (:


            1. sshikov
              18.11.2019 11:40
              +1

              Можно поинтересоваться, у вас большой опыт в энтерпрайзе?


  1. igormich88
    17.11.2019 02:52

    У вас очевидно что слишком синтетический пример, исключения подразумевают что они случаются достаточно редко (собственно потому они и называются исключения).
    Если в показанном выше примере не каждый раз передавать null, а один раз из 100, то производительность варианта с try… catch будет в двое хуже. А если один раз из 1000, то разница в производительности будет почти незаметна (проверил у себя на компе).
    Главный плюс исключений это прозраная передача потока управления наверх (разумеется когда это необходимо).

    PS и да наверное лучше возвращать не 0, а -1. А еще лучше Optional, как уже написали.


  1. yarick123
    17.11.2019 03:29
    +1

    Вот мои «десять копеек». Основная мысль статьи — правильная. Пример — неплохой. Объяснение и обоснование — так себе. Управление потоком без исключений — это когда всё идёт по плану, когда данные не битые, когда файлы там, где мы их хотим найти, когда сеть не сбоит,… Как только случается то, что для нас выходит за рамки запланированного нормального хода работы, тогда появляются исключения. При чём эти рамки определяем мы сами. Чаще всего, на исключения надо реагировать существенно выше по иерархии кода, прерывая его выполнение. Недалеко от тех мест, где исключения возникают, обычно вообще не понятно, что с ними делать, кроме как передать или обернуть-и-передать вызывающему. В языках программирования без исключений приходится изворачиваться и тянуть информацию об ошибке назад по стеку вызовов, организуя альтернативное управление ходом программы с кучей дополнительных проверок. Потом иногда оказывается, что «совсем уж исключительные ситуации» всё-равно нужно было обрабатывать иначе. Например, деление на 0 или ctrl-C в консольном приложении на C.

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

    Конечно, за удобство элегантно выпрыгнуть из стека вызовов, надо платить. Поэтому там, где важна производительность в исключительных ситуациях, возможно, от Exception придётся отказаться. Но лично мне такое встречалось нечасто.


    1. vektory79
      17.11.2019 10:32

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


    1. sshikov
      17.11.2019 10:41

      >Чаще всего, на исключения надо реагировать существенно выше по иерархии кода
      Это слишком сильное обобщение, вероятно верное для вас, но не факт что верное всегда. Я бы скорее написал «можно реагировать сильно выше».


  1. merhalak
    17.11.2019 03:31

    Большое количество времени при работе с исключениями забирает на себя операция записи стектрейса. Попробуй слегка поменять исключение и переснять показания.


    public static class NameNotFoundException extends RuntimeException {
        private static final long serialVersionUID = 3L;
    
        NameNotFoundException() {
            super("name not found", null, true, false);
        }
    }


  1. dopusteam
    17.11.2019 08:45
    +1

    А есть нормальные примеры?


  1. funca
    17.11.2019 09:53

    Если null это не валидное значение для name, лучше сообщить об этом явно:
    private String findUser(@NotNull String name) { Object.requireNotNull(name, "name must not be null"); ... }
    Разработчик, вызвавший findUser(null) получит NPE, исправит ошибку в коде и в продакшене такая исключительная ситуация не возникнет ни разу.


  1. sshikov
    17.11.2019 10:34

    Все это далеко не ново. Были уже давно нормальные посты, даже с разбором влияния наличия стектрейса на производительность. Хотя я верю, что найти их на хабре не так просто.


    1. merhalak
      17.11.2019 12:32

      https://m.habr.com/ru/company/jugru/blog/324932/


      Например. Достаточно просто, если знать, что искать.



  1. Wyrd
    17.11.2019 14:42

    findAge которая возвращает «типа» длиннюу имени пользователя, а на самом деде длину переданной ей строки — вы это серьезно?


  1. oxff
    17.11.2019 16:29
    -1

    Мерять производительность способом, описанным в статье совершенно неверно.
    Автор, погугли "JMH", эта библиотека является частью JDK. В сети куча примеров и видео, в том числе на русском от её автора — Алексея Шипилёва.


    Кроме того, присоединяюсь к предыдущим комментаторам. В целом, посыл статьи правильный, описывает известный антипаттерн. Но приведённый код из раздела "как надо" не прошёл бы ревью.


  1. resetme
    17.11.2019 18:47

    Исключения не просотак названы исключениями. Это что-то исключительное в потоке выполнения вашей программы. Они случаются не часто, и обычно когда случаются, то пишутся в логи, а это замедляет работу, но это не критично, так как исключения это должны быть редкостью, а не как правило в потоке выполнения программы. Надеюсь автор это понимает.


  1. nApoBo3
    17.11.2019 22:33

    Сама идя, не используй исключение для управления поведением приложения разумна, но вот иллюстрация…
    Тест не корректен, да и код как раз показывает почему убирать исключение бездумно не следует.
    Убрав из этого кода исключение вы реализовали не корректное null поведение, которое рано или поздно больно ударит по голове ручкой граблей. У null объекта длинна не 0, она не определена. И множество тех кто пойдет за вами будут не мало удивлены наличием длинны у null объекта. И тем объемом костылей которые нужно будет забить, чтобы не исправляя ваш код реализовать корректный обработчик null для данной ситуации.
    Сам тест показывает лишь то, что обработка исключения дольше чем отсутствие обработки исключения. Это имхо очевидно, что-то делать всегда дольше чем не делать.
    Только вот создавать ПО в котором 99% процентов входных данных будут попадать под исключение это как-то странно, исключение оно на то и исключение, что не является обычным поведением. Если вы скорректируйте входные данные на 99% процентов корректных строк и 1% null, результаты теста сильно измениться, и даже в таком случае будет не слишком показательным, поскольку 1% исключений это ОЧЕНЬ много.


    1. uncle_doc
      18.11.2019 09:02

      Всецело поддерживаю ваше замечание, но немного дополню для автора. Чтобы полностью «избавиться» от исключений обычно в систему вводят объект типа

      Result<T>
      и например, проверяют у него свойство success = true|false перед тем как прочитать свойство data с данными. Но, как говорится — в программировании чудес не бывает, если мы не пишем код в одном месте то мы пишем его в другом месте, т.е. в вашем случае вместо того чтобы писать код на исключениях мы напишем другой код, для того чтобы «обойти» этот подход (потому что лучший код это тот которого нет). Ну и конечно стоит добавить что не всем типам ПО такой подход подойдет, например в большинстве мобильных приложений такой подход оправдан.