Java - где и как следует использовать исключения?

Я читал некоторые вещи об обработке исключений в Java, чтобы иметь возможность писать лучший код. ОК, я признаю, я виновен; Я использовал слишком много блоков try-catch{}, я использовал ex.printStackTrace() в улове, даже не используя надлежащий регистратор (на самом деле System.out а также System.err были перенаправлены на PrintWriter, так что журнал был создан). Однако после нескольких часов чтения я нахожусь в странном месте: неизвестном. Если исключения предназначены для передачи информации о ненормальных состояниях потока, как узнать, ГДЕ это правильный уровень, чтобы что-то делать с этой информацией?

Например, когда возникает ошибка базы данных, следует ли возвращать нулевое значение или код ошибки или выдавать исключение? Если выброшено, ГДЕ должно обрабатываться это исключение? Я понимаю, что бесполезно даже регистрировать исключение, если вы ничего не можете с этим поделать. Тем не менее, в приложениях с графическим интерфейсом это может легко убить ваш графический интерфейс (я использую SWT, и я видел это слишком часто), даже для случая menuShown() метод (ArrayIndexOutOfBounds Исключение закроет приложение, если не обработано). Пример может продолжаться вечно, но вот краткое изложение вопросов:

  1. Влияет ли чрезмерное использование try-catch() на производительность?
  2. Лучше использовать определенные типы исключений? Что если я пропустил ловушку одного из возможных X типов исключений, которые могут возникнуть?
    Честно говоря, я слышал и использую всего 10%, я думаю о стандартных исключениях Java, через 2-3 года. Да, кто-то сказал, что если вызывающий не знает, как обращаться с выданными исключениями, он не должен иметь права вызывать метод броска. Это правильно?
  3. Я прочитал эту статью Андерса Хейлсберга, в которой говорится, что проверенные исключения - это плохо. Должно ли это указывать на то, что в некоторых случаях рекомендуется удобное проглатывание исключений?
  4. Картинка стоит 1000 слов; Я думаю, что некоторые примеры очень помогут здесь.

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

9 ответов

Решение

Исключение есть, поэтому программисту Задачи не приходится иметь дело с проблемой самостоятельно. (1): В случае, если проблема НЕ является ЛОГИЧЕСКОЙ для него, чтобы справиться в Задаче. Задача чтения строки из потока не должна обрабатывать ошибку диска, не так ли? Но это должно быть очень логично, если данные не содержат String.

(2): Он не может справиться с этим сам (недостаточно информации). Задача для чтения строки из файла, а файл не найден, может попросить пользователя выбрать другой файл, но как теперь задача, из какой папки может быть файл? расширение файла может быть. Не зная этого, как задача может создать графический интерфейс для повторного запроса этого.

(3): не существует логического (или управляемого) способа различать разные доходы. Если задача не может прочитать файл и вернуть ноль. А что, если файл в неправильном формате, тоже вернуть null? Как эти два могут отличаться? Исключения могут использоваться, чтобы отличать это. Вот почему это называется Исключением:-D.

(4): Есть много подобных задач, которые требуют одинаковой обработки и написания, которые во всех задачах трудно поддерживать. Написание кода дескриптора для любого доступа может привести к путанице, так как вам может потребоваться много дубликатов.


interface DBAccess {
    public Result accessDB();
}

class DBOperation {
    static public void DoOperation(DBAccess pAccess) {
        try { return DBAccess.accessDB(); }
        catch(InvalidDBPasswordException IPE) {
             // Do anything about invalid password
        }
        catch(DBConnectionLostException DBCLE) {
             // Do anything about database connection lost
        }
        // Catch all possible DB problem
    }
}

...

private User[] ShowUserList_and_ReturnUsers() {
    // Find the used.

    // Show user list

    if (Users.count() == 0)
         return null;
    else return Users;

    // No need to handle DB connection problem here
}
private User[] GetUserProfile() {
    // Find the used and return
    // No need to handle DB connection problem here
}
...

/** An onClick event to show user list */ {
    DBOperation.DoOperation(new DBAccess() {
        public Result accessDB() {
            return ShowUserList_and_ReturnUsers();
        }
    });
}
/** An onClick event to show a user profile */ {
    DBOperation.DoOperation(new DBAccess() {
        public Result accessDB() {
            return GetUserProfile();
        }
    });
}
... Many more DB access

(5): Запись всех проверок на ошибки усложнит или замедлит задачу. Вышеуказанная проблема должна показать, как она может помочь уменьшить осложнение. Вот как это помогает не тормозить.



for(int i = 0; i < Users.length; i++) {
    User aUser = Users[i];
    // Do something with user
}

Replaced with

try {
  for(int i = 0; ; i++) {
      User aUser = Users[i];
      // Do something with user
  }
}
catch(ArrayOutOfBoundException AOBE) {}

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


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

(1). Использование чрезмерного try-catch() отрицательно влияет на производительность? Ответ: Согласно "Эффективной Java", насколько я помню, он имеет очень крошечный эффект (только не очень хороший в цикле) (у меня сейчас нет книги со мной).

(2). Использование определенных типов исключений лучше? Ответ: Для конкретного пользователя лучше избегать решения неправильной проблемы.

Что если я пропустил один из возможных X типов исключений, которые могут возникнуть? Честно говоря, я слышал и использую всего 10%, я думаю о стандартных исключениях Java, через 2-3 года. Ответ: Точно так же, как если вы обрабатываете ошибку без исключения, вы тоже можете ее пропустить. Вы просто добавляете это, когда узнаете это.

Да, кто-то сказал, что если вызывающий не знает, как обращаться с отклоненными исключениями, он не должен иметь права вызывать метод броска. Это правильно? Ответ: Нет, если я не знаю, что делать с каким-то исключением, перебросьте его.

(3). Я прочитал эту статью Андерса Хейлсберга, в которой говорится, что проверенные исключения - это плохо. Должно ли это указывать на то, что в некоторых случаях рекомендуется удобное проглатывание исключений? Ответ: Я думаю, что он говорит о "проверке исключения" как о функции для компилятора, чтобы гарантировать, что какое-то исключение должно быть обработано. Идея иметь исключение.

(4). Картинка стоит 1000 слов... я думаю, что некоторые примеры очень помогут здесь. Ответ: код выше.

Я получил пробежку сейчас.... Извините...:-p (Будь там через минуту, дорогая!!)

Основное эмпирическое правило для исключения: если вы можете что-то с этим сделать, поймайте это и обработайте, если не можете, перебросьте это к следующему методу. Чтобы разобраться в некоторых ваших особенностях:

  1. Нет, чрезмерное использование try / catch не повлияет на производительность
  2. Используя наиболее конкретный тип исключения, вы можете. Например, вы не должны вообще генерировать исключение, если можете его избежать. Выбрасывая определенный тип, вы даете пользователю знать, что может пойти не так. Тем не менее, вы можете перебросить его как нечто более общее, поэтому вызывающим, которые не имеют отношения к конкретному исключению, не нужно знать об этом (например, GUI не будет заботиться, если это IOException против ArrayIndexOutOFBoundsException).
  3. Вы найдете людей, которым больше нравятся проверенные исключения, и вы найдете людей, которым больше нравится непроверенные. В общем, я стараюсь использовать непроверенные исключения, потому что в большинстве случаев вы не можете ничего сделать с большинством проверенных исключений, и вы по-прежнему можете обрабатывать непроверенные исключения, просто не нужно. Я часто перебрасываю проверенные исключения, так как не могу с ними ничего поделать (другая стратегия - перехватить проверенное исключение и перебросить его как непроверенное, поэтому классам, находящимся выше в цепочке, не нужно его перехватывать, если они не хотят).

Как правило, мне нравится регистрировать исключения в том месте, где они были обнаружены - даже если я ничего не могу с этим поделать, это помогает диагностировать проблему. Если вы не знакомы с ним, также посмотрите на метод Thread.setDefaultUncaughtExceptionHandler. Это позволяет обрабатывать исключения, которые никем не перехвачены, и что-то с ними делать. Это особенно полезно в приложении с графическим интерфейсом, так как в противном случае исключение может не быть замечено.

Чтобы попасть в некоторые примеры:

try {
   // some database operation
}
catch (IOException ex) {
   // retry the database operation. then if an IO exception occurs rethrow it. this shows an example doing something other than just catch, logging and/or rethrowing.       
}

Я буду рад углубиться в любые части этого, если хотите.

Много хороших ответов, позвольте мне добавить пару моментов, которые не были упомянуты.

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

Для многих, возможно, для большинства исключений, которые я создаю, единственное, что программа может реально сделать, это отобразить сообщение об ошибке и дать пользователю возможность изменить свои входные данные и повторить попытку. Большинство ошибок проверки - неверный формат даты, нецифровые числа в числовом поле и т. Д. - попадают в эту категорию. Для них я создаю один тип исключения, который я обычно называю "BadInputException" или "ValidationException", и использую тот же класс исключений во всей системе. Когда возникает ошибка, я "выдаю новое исключение BadInputException(" Сумма должна содержать только цифры ")" или что-то подобное, а затем вызывающий абонент отображает его и позволяет пользователю повторить попытку.

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

Простое правило: если у вас есть два или более исключений, которые ВСЕГДА обрабатываются с идентичным дублирующимся кодом, объедините их в одно исключение. Если ваш блок catch выполняет дополнительную проверку, чтобы выяснить, что это за ошибка, это должны быть два (или более) класса исключений. Я видел код, который выполняет исключение.getMessage, а затем ищет ключевые слова в сообщении, чтобы выяснить, в чем проблема. Это безобразно Сделайте несколько исключений и сделайте это чисто.

  1. Есть три веские причины использовать исключения, а не другие способы обработки ошибок.

(а) Это позволяет избежать проблемы "магических" возвращаемых значений, например, ненулевая строка - это реальный ответ, но нулевой означает, что произошла ошибка. Или, что еще хуже, "NF" означает, что файл не найден, "NV" означает недопустимый формат, а все остальное - реальный ответ. За исключением, исключение является исключением, а возвращаемое значение является возвращаемым значением.

(б) Исключения аккуратно пропускают основную строку кода. Обычно, когда есть ошибка, вы хотите пропустить целую кучу обработки, которая не имеет смысла без правильных данных, и перейти прямо к отображению сообщения об ошибке и выходу, или повторению операции, или к тому, что подходит. В старых дурных штампах мы писали: "GOTO panic-abort". GOTO опасны по всем причинам, о которых много говорили. Исключения исключают то, что, возможно, было последней из оставшихся веских причин использовать GOTO.

(c) Возможно, следствием (b), вы можете решить проблему на соответствующем уровне. Иногда, когда происходит ошибка, вы хотите повторить ту же самую функцию - например, ошибка ввода-вывода может представлять кратковременный сбой связи. С другой стороны, вы можете быть на десять уровней в подпрограммах, когда получите ошибку, которая не может быть обработана каким-либо образом, кроме взрыва всей программы и отображения сообщения "извините, армагеддон произошел, все здесь мертвы". За исключением, не только легко выбрать правильный уровень, но вы можете сделать разные варианты в разных модулях.

Возвращаемое значение или исключение

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

Влияние на производительность.

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

Конкретные исключения

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

проверено

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

пример

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

for (int retries = 0;; retries++) {
    try {
        commandService.execute(command);
        return;
    } catch (Exception e}
        Log.error(e);
        if (retries < 3) {
            continue;
        } else {
            saveForAnalysis(command, e);
            alertOperator();
            return;
        }
    }
}

Мы намеренно не улавливали и не перебрасывали исключения в логике обработки, поскольку чувствовали, что это не принесет никакой пользы.

Одна вещь, которую мы сделали в нашей команде - это наличие пользовательских исключений для наших ошибок. Мы используем фреймворк Hibernate Validator, но вы можете сделать это с любыми фреймворками или стандартными исключениями.

Например, у нас есть ValidationException для обработки ошибок валидации. У нас есть ApplicationException для обработки системных ошибок.

Вы хотите свести к минимуму ваши попытки поймать. В нашем случае у нас будет валидатор, который соберет ВСЕ проверки в объектах "InvalidValue", а затем сгенерирует единственное исключение ValidationException с информацией о недопустимом значении, включенном в него. Затем вы можете сообщить пользователю, какие поля были ошибочными и т. Д.

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

Наконец, иногда вы не можете не использовать много блоков try/catch из-за внешних API, на которые вы полагаетесь. Это отлично. Как упоминалось ранее, перехватите внешнее исключение и отформатируйте его в то, которое имеет больше смысла для вашего приложения. Затем бросьте пользовательское исключение.

Пожалуйста, не возвращайте ноль в случае нефатальных ошибок. Вместо этого верните NullObject.

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

  • Использование чрезмерного try-catch() отрицательно влияет на производительность?

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

  • Использование определенных типов исключений лучше? Что если я пропустил один из возможных X типов исключений, которые могут возникнуть? Честно говоря, я слышал и использую всего 10%, я думаю о стандартных исключениях Java, через 2-3 года. Да, кто-то сказал, что если вызывающий не знает, как обращаться с отклоненными исключениями, он не должен иметь права вызывать метод броска. Это правильно?

Я не уверен, что понял вопрос, но скажу: "Если вы не знаете, что делать с исключением, повторите его".

  • Я прочитал эту статью Андерса Хейлсберга, в которой говорится, что проверенные исключения - это плохо. Должно ли это указывать на то, что в некоторых случаях рекомендуется удобное проглатывание исключений?

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

  • Картинка стоит 1000 слов... я думаю, что некоторые примеры очень помогут здесь.

Спринга DataAccessException это очень хороший пример. Проверьте главу 10. Поддержка DAO.

  1. Хотя у меня нет никаких цифр, я не верю, что try-catch оказывает существенное влияние на производительность (не то, что я видел). Я думаю, что если вы не столкнетесь со многими исключениями, влияние на производительность будет практически нулевым. Но в любом случае лучше всего сначала правильно реализовать код и, во-вторых, добиться хорошей производительности - гораздо проще выполнить второй после завершения первого.

  2. Я думаю, что класс исключения должен быть конкретным относительно того, что на самом деле является исключением. Проблема с SQLExceptions в Java заключается в том, что они не дают вам никакой информации о том, что действительно пошло не так. Spring использует гораздо больше описательных исключений базы данных (исключения взаимоблокировок, исключения целостности данных и т. Д.). Таким образом, вы можете определить, в чем на самом деле была проблема.

  3. Проверенные исключения могут раздражать, но я не думаю, что они всегда плохие. Например, Spring использует непроверенные исключения для ошибок базы данных, но я все еще проверяю их и либо 1) обрабатываю их прямо там, если это возможно, либо 2) заключаю в более общее исключение, которое показывает, что компонент вышел из строя.

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

se-radio сделал эпизод подкаста на эту тему обработки ошибок, который объясняет некоторую философию о том, как использовать исключения, которые можно переформулировать как "Где их поглотить".

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

В некотором смысле это приводит к некой иерархии исключений: по одной для каждого уровня кода.

Как я думаю, они сказали, что не имеет смысла объяснять пользователю, что такой кластер БД вышел из строя из-за недоступности DNS или из-за переполнения диска. На этом уровне произошло нечто, что не позволило завершить транзакцию, это все, что должен знать пользователь.

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

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