Java Language
Java Pitfalls - использование исключений
Поиск…
Вступление
Некоторые нарушения языка программирования Java могут выполнять программу для получения неправильных результатов, несмотря на то, что они правильно составлены. Основная цель этого раздела - перечислить общие ошибки, связанные с обработкой исключений , и предложить правильный способ избежать таких ошибок.
Pitfall - Игнорирование или сбой исключений
В этом примере речь идет об умышленном игнорировании или «раздавливании» исключений. Или, точнее, речь идет о том, как поймать и обработать исключение таким образом, чтобы его игнорировать. Тем не менее, прежде чем мы опишем, как это сделать, мы должны сначала указать, что исключение от раздавливания, как правило, не является правильным способом борьбы с ними.
Исключения обычно забрасываются (кем-то), чтобы уведомить другие части программы о том, что произошло какое-то значительное (то есть «исключительное») событие. Вообще (хотя и не всегда) исключение означает, что что-то пошло не так. Если вы закодируете свою программу для выдачи исключения, есть вероятность, что проблема снова появится в другой форме. Чтобы ухудшить ситуацию, когда вы выкалываете исключение, вы выбрасываете информацию в объекте исключения и связанной с ним трассировке стека. Скорее всего, это затруднит выяснение того, каков был исходный источник проблемы.
На практике, при использовании функции автоматической коррекции IDE, «исправление» ошибки компиляции, вызванной необработанным исключением, часто происходит сбой при сбоях. Например, вы можете увидеть такой код:
try {
inputStream = new FileInputStream("someFile");
} catch (IOException e) {
/* add exception handling code here */
}
Ясно, что программист принял предложение IDE, чтобы ошибка компиляции исчезла, но предложение было неуместным. (Если файл открылся не сработал, программа, скорее всего, что-то с этим inputStream
. С приведенной выше «коррекцией» программа может потерпеть неудачу позже, например, с помощью NullPointerException
поскольку inputStream
теперь имеет значение null
.)
Сказав это, вот пример умышленного подавления исключения. (В целях аргумента предположим, что мы определили, что прерывание при показе самообороны является безвредным.) Комментарий говорит читателю, что мы сознательно раздавили исключение и почему мы это сделали.
try {
selfie.show();
} catch (InterruptedException e) {
// It doesn't matter if showing the selfie is interrupted.
}
Другой общепринятый способ подчеркнуть, что мы намеренно подавляем исключение, не говоря о том, почему нужно указывать это с именем переменной исключения, например:
try {
selfie.show();
} catch (InterruptedException ignored) { }
Некоторые IDE (например, IntelliJ IDEA) не будут отображать предупреждение о пустом блоке catch, если имя переменной установлено на ignored
.
Pitfall - Catching Throwable, Exception, Error или RuntimeException
Общим шаблоном мыслей для неопытных программистов на Java является то, что исключения - это «проблема» или «бремя», и лучший способ справиться с этим - как можно скорее поймать их всех 1 . Это приводит к следующему коду:
....
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (Exception ex) {
System.out.println("Could not open file " + fileName);
}
Вышеприведенный код имеет значительный недостаток. catch
на самом деле поймает больше исключений, чем ожидает программист. Предположим, что значение fileName
равно null
из-за ошибки в другом месте приложения. Это заставит конструктор FileInputStream
выкинуть FileInputStream
NullPointerException
. Обработчик поймает это и сообщит пользователю:
Could not open file null
что бесполезно и запутанно. Хуже того, предположим, что это был код «процесс ввода», который выдал неожиданное исключение (отмечено или не отмечено!). Теперь пользователь получит сообщение об ошибке для проблемы, которая не возникла при открытии файла, и вообще не может быть связана с I / O.
Корень проблемы заключается в том, что программист закодировал обработчик для Exception
. Это почти всегда ошибка:
- Catching
Exception
поймает все проверенные исключения и большинство непроверенных исключений. - Catching
RuntimeException
большинство непроверенных исключений. -
Error
CatchingError
будет проверять неконтролируемые исключения, которые сигнализируют внутренние ошибки JVM. Эти ошибки, как правило, не подлежат восстановлению и не должны быть пойманы. - Catching
Throwable
поймает все возможные исключения.
Проблема с улавливанием слишком широкого набора исключений заключается в том, что обработчик обычно не может обрабатывать все из них соответствующим образом. В случае Exception
и т. Д. Программисту сложно предсказать, что можно поймать; т.е. чего ожидать.
В общем, правильное решение , чтобы иметь дело с исключениями , которые выбрасываются. Например, вы можете поймать их и обработать их на месте:
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (FileNotFoundException ex) {
System.out.println("Could not open file " + fileName);
}
или вы можете объявить их как thrown
приложенным методом.
Очень мало ситуаций, когда ловушка Exception
подходит. Единственное, что возникает обычно, это что-то вроде этого:
public static void main(String[] args) {
try {
// do stuff
} catch (Exception ex) {
System.err.println("Unfortunately an error has occurred. " +
"Please report this to X Y Z");
// Write stacktrace to a log file.
System.exit(1);
}
}
Здесь мы действительно хотим иметь дело со всеми исключениями, поэтому Throwable
Exception
(или даже Throwable
) верна.
1 - Также известен как Покемон Исключение обработки .
Pitfall - Бросание Throwable, Exception, Error или RuntimeException
Хотя Throwable
Exception
Throwable
, Exception
, Error
и RuntimeException
является плохим, бросать их еще хуже.
Основная проблема заключается в том, что когда ваше приложение должно обрабатывать исключения, наличие исключений верхнего уровня затрудняет различение различных условий ошибки. Например
try {
InputStream is = new FileInputStream(someFile); // could throw IOException
...
if (somethingBad) {
throw new Exception(); // WRONG
}
} catch (IOException ex) {
System.err.println("cannot open ...");
} catch (Exception ex) {
System.err.println("something bad happened"); // WRONG
}
Проблема в том, что, поскольку мы Exception
экземпляр Exception
, мы вынуждены его поймать. Однако, как описано в другом примере, catching Exception
является плохим. В этой ситуации становится трудно различать «ожидаемый» случай Exception
который генерируется, если somethingBad
true
, и непредвиденный случай, когда мы фактически поймаем неконтролируемое исключение, такое как NullPointerException
.
Если исключение верхнего уровня разрешено распространять, мы сталкиваемся с другими проблемами:
- Теперь мы должны помнить все разные причины, по которым мы выбрали верхний уровень и дискриминируем / обрабатываем их.
- В случае
Exception
иThrowable
нам также необходимо добавить эти исключения в предложениеthrows
методов, если мы хотим, чтобы исключение распространялось. Это проблематично, как описано ниже.
Короче говоря, не бросайте эти исключения. Бросьте более конкретное исключение, которое более подробно описывает «исключительное событие», которое произошло. Если вам нужно, определите и используйте специальный класс исключений.
Объявление Throwable или Exception в «бросках» метода проблематично.
Заманчиво заменить длинный список исключенных исключений в предложение throws
метода с Exception
или даже Throwable. Это плохая идея:
- Это заставляет вызывающего абонента обрабатывать (или распространять)
Exception
. - Мы больше не можем полагаться на компилятор, чтобы рассказать нам о конкретных проверенных исключениях, которые необходимо обработать.
- Обработка
Exception
должным образом затруднено. Трудно понять, какие фактические исключения могут быть пойманы, и если вы не знаете, что можно поймать, трудно понять, какая стратегия восстановления подходит. - Обработка
Throwable
еще сложнее, так как теперь вы также должны справляться с потенциальными сбоями, которые никогда не должны восстанавливаться.
Этот совет означает, что некоторые другие шаблоны следует избегать. Например:
try {
doSomething();
} catch (Exception ex) {
report(ex);
throw ex;
}
Вышеупомянутые попытки регистрировать все исключения по мере их прохождения, без окончательного обращения с ними. К сожалению, до Java 7, throw ex;
заявление заставило компилятор думать, что любое Exception
может быть выброшено. Это может заставить вас объявить вложенный метод как throws Exception
. Начиная с Java 7 компилятор знает, что набор исключений, которые могут быть (переброшены), меньше.
Pitfall - Catching InterruptedException
Как уже указывалось в других ловушках, ловя все исключения, используя
try {
// Some code
} catch (Exception) {
// Some error handling
}
Поставляется с множеством разных проблем. Но одна из проблем заключается в том, что это может привести к взаимоблокировкам, поскольку он разбивает систему прерываний при написании многопоточных приложений.
Если вы начинаете нить, вы также должны быть в состоянии остановить ее внезапно по разным причинам.
Thread t = new Thread(new Runnable() {
public void run() {
while (true) {
//Do something indefinetely
}
}
}
t.start();
//Do something else
// The thread should be canceld if it is still active.
// A Better way to solve this is with a shared variable that is tested
// regularily by the thread for a clean exit, but for this example we try to
// forcibly interrupt this thread.
if (t.isAlive()) {
t.interrupt();
t.join();
}
//Continue with program
t.interrupt()
приведет к t.interrupt()
InterruptedException в этом потоке, чем предназначен для отключения потока. Но что, если Thread должен очистить некоторые ресурсы до того, как он полностью остановится? Для этого он может поймать InterruptedException и выполнить некоторую очистку.
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
//Do something indefinetely
}
} catch (InterruptedException ex) {
//Do some quick cleanup
// In this case a simple return would do.
// But if you are not 100% sure that the thread ends after
// catching the InterruptedException you will need to raise another
// one for the layers surrounding this code.
Thread.currentThread().interrupt();
}
}
}
Но если у вас есть выражение catch-all в вашем коде, InterruptedException также будет поймано им, и прерывание не продолжится. Который в этом случае может привести к тупиковой ситуации, поскольку родительский поток ждет бесконечно, чтобы этот ада остановился на t.join()
.
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
try {
//Do something indefinetely
}
catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (InterruptedException ex) {
// Dead code as the interrupt exception was already caught in
// the inner try-catch
Thread.currentThread().interrupt();
}
}
}
Так что лучше поймать Исключения отдельно, но если вы настаиваете на использовании catch-all, по крайней мере, поймите InterruptedException индивидуально заранее.
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
try {
//Do something indefinetely
} catch (InterruptedException ex) {
throw ex; //Send it up in the chain
} catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (InterruptedException ex) {
// Some quick cleanup code
Thread.currentThread().interrupt();
}
}
}
Pitfall - Использование исключений для нормального управления потоком
Существует мантра, которую некоторые специалисты Java обычно читают:
«Исключения должны использоваться только в исключительных случаях».
(Например: http://programmers.stackexchange.com/questions/184654 )
Суть этого в том, что это плохая идея (в Java) использовать исключения и обработку исключений для реализации нормального управления потоком. Например, сравните эти два способа обращения с параметром, который может быть нулевым.
public String truncateWordOrNull(String word, int maxLength) {
if (word == null) {
return "";
} else {
return word.substring(0, Math.min(word.length(), maxLength));
}
}
public String truncateWordOrNull(String word, int maxLength) {
try {
return word.substring(0, Math.min(word.length(), maxLength));
} catch (NullPointerException ex) {
return "";
}
}
В этом примере мы (по дизайну) рассматриваем случай, когда word
равно null
как будто это пустое слово. Две версии имеют значение null
либо с использованием обычных if ... else, либо try ... catch . Как нам решить, какая версия лучше?
Первый критерий - читаемость. Хотя читаемость трудно объективно определить количественно, большинство программистов согласятся с тем, что существенный смысл первой версии легче распознать. Действительно, чтобы действительно понять вторую форму, вам нужно понять, что Math.min
NullPointerException
не может быть String.substring
методами Math.min
или String.substring
.
Второй критерий - эффективность. В версиях Java до Java 8 вторая версия значительно (на порядки) медленнее первой версии. В частности, построение объекта исключения влечет за собой захват и запись стековых кадров, на всякий случай, если требуется стек.
С другой стороны, существует множество ситуаций, когда использование исключений является более читаемым, более эффективным и (иногда) более правильным, чем использование условного кода для обработки «исключительных» событий. Действительно, есть редкие ситуации, когда необходимо использовать их для «не исключительных» событий; т.е. события, которые происходят относительно часто. Для последнего стоит взглянуть на способы сокращения накладных расходов на создание объектов исключений.
Pitfall - чрезмерные или неуместные стеки
Одним из наиболее неприятных вещей, которые могут сделать программисты, является разброс вызовов printStackTrace()
во всем их коде.
Проблема заключается в том, что printStackTrace()
будет писать stacktrace для стандартного вывода.
Для приложения, предназначенного для конечных пользователей, которые не являются Java-программистами, stacktrace в лучшем случае неинформативна и в худшем случае вызывает тревогу.
Вероятно, для серверного приложения никто не будет смотреть на стандартный вывод.
Лучше всего не вызывать printStackTrace
напрямую, или если вы вызываете его, сделайте это так, чтобы трассировка стека была записана в файл журнала или файл ошибки, а не на консоль конечного пользователя.
Один из способов сделать это - использовать структуру ведения журнала и передать объект исключения в качестве параметра события журнала. Однако даже регистрация исключений может быть вредной, если это было сделано вредно. Рассмотрим следующее:
public void method1() throws SomeException {
try {
method2();
// Do something
} catch (SomeException ex) {
Logger.getLogger().warn("Something bad in method1", ex);
throw ex;
}
}
public void method2() throws SomeException {
try {
// Do something else
} catch (SomeException ex) {
Logger.getLogger().warn("Something bad in method2", ex);
throw ex;
}
}
Если исключение method2
в method2
, вы, вероятно, увидите два экземпляра одной и той же stacktrace в файле журнала, что соответствует тому же отказу.
Короче говоря, либо регистрируйте исключение, либо повторно бросайте его (возможно, завернутое с другим исключением). Не делай того и другого.
Pitfall - прямое подклассирование «Throwable»
Throwable
имеет два прямых подкласса: Exception
и Error
. Хотя можно создать новый класс, который напрямую расширяет Throwable
, это нецелесообразно, так как многие приложения предполагают, что существуют только Exception
и Error
.
Более того, нет практической выгоды для прямого подкласса Throwable
, поскольку полученный класс, по сути, просто проверенное исключение. В противном случае Exception
подкласса приведет к такому же поведению, но будет более четко передавать ваши намерения.