Никто не умеет обрабатывать ошибки
является плохой практикой. Возврат кодов – также плохая практика. Но становится ли нам, программистам, жить легче с этими знаниями и так уж ли они неоспоримы? И самый забавный вопрос – кто-нибудь в мире умеет грамотно обрабатывать ошибки, возникающие по ходу работы приложения? (под этим я понимаю обработку только тех ошибок, которые имеет смысл обрабатывать и вывод сообщений об ошибках, которые соответствуют действительно произошедшей, которые не вводят пользователя в замешательство, а в идеале и предлагают решение возникшей проблемы).
Целью данной статьи не является «разнос» существующих концепций, за незнанием автором лучшего подхода. Целью статьи является заострение проблемы обработки исключений и признание того факта, что недостаточно просто делать отписки в стиле «не делайте catch(Exception) » или «бросайте всегда исключения наверх – авось, там, наверху разберутся». Это ничем не помогает. На русском языке на эту тему так вообще мало чего написано (да и кто доверяет русскоязычным авторам? Шутка, но в каждой шутке есть доля шутки). Статья предназначена также для не умудрённых опытом разработчиков, чтобы читатель осознал то, что мучаются обработкой ошибок абсолютно все, включая «сильнейших мира сего», а также начал понимать проблему на немного более глубоком уровне.
Хотелось бы также упомянуть, что в целях краткости я не переводил абсолютно всё из этих статей. Это вырезки, которые являются наиболее важными с точки зрения заданной темы. Текст от себя я буду обрамлять следующим образом: Spock speech текст end of Spock speech
Проблемы с проверяемыми исключениями.
Bruce Eckel: C# не имеет механизма проверяемых исключений. Каким образом принималось решение о том включать или нет механизм проверяемых исключений в C#?
Anders Hejlsberg: Я вижу две проблемы с проверяемыми исключениями: масштабируемость и версионируемость.
Bruce Eckel: Раньше я думал, что проверяемые исключения — это круто.
Anders Hejlsberg: Именно. Честно говоря, при поверхностном рассмотрении, они действительно выглядят хорошо и кажется, что с этой идеей всё нормально. Я абсолютно согласен с тем, что механизм исключений это отличная фича. Просто конкретная реализация может быть проблематичной. Если реализовать этот механизм так, как он был реализован в Java, то, я думаю, вы просто размениваете один набор проблем на другой. В результате для меня неясно становится ли жизнь хоть сколько-нибудь легче. Мы просто делаем жизнь другой.
Bruce Eckel: Были ли в команде разработчиков C# разногласия относительно проверяемых исключений?
Anders Hejlsberg: Нет, я думаю, что в нашей команде проектировщиков языка в большей степени царило согласие. C# молчалив по отношению к проверяемым исключениям. Если однажды лучшее решение будет найдено – и поверьте мне, мы продолжаем думать о данной проблеме – мы сможем вернуться и прикрутить, что нужно. Я убеждённый сторонник того, что если вам нечего сказать такого, что продвинет искусство вперёд, то лучше промолчать и придерживаться нейтральной позиции, а не пытаться создать свой фреймворк.
Bruce Eckel: Разработчики, практикующие экстремальное программирование говорят «делай наиболее просто, чтобы работало».
Anders Hejlsberg: Да, Эйнштейн сказал «делай наиболее просто, но не проще простого». Полагаю, что проверяемые исключения это наручники для программистов. Можно наблюдать программистов, которые берутся использовать какой-нибудь новый API, декларирующий множество потенциально пробрасываемых исключений и можно увидеть насколько засорённым становится их код. В результате вы осознаёте, что проверяемые исключения им ничем не помогают.
Bill Venners: Вы упоминали масштабируемость и версионируемость относительно проверяемых исключений. Не могли бы вы пояснить, что вы имели ввиду под двумя проблемами, связанными с этими понятиями?
Anders Hejlsberg: Начнём с версионирования, потому что тут проблемы легко увидеть. Скажем я создаю метод foo , который декларирует проброс исключений A, B и C. Во второй версии метода я хочу добавить пару фишек и теперь метод foo может выкинуть также исключение D. Добавление нового исключения является несовместимым изменением, потому что существующие пользователи этого метода почти 100% не будут ловить это исключение.
Добавление нового исключения ломает клиентский код. Это как добавление метода в интерфейс.
Bill Venners: Но не ломаете ли вы клиентский код в любом случае, даже если в языке отсутствуют проверяемые исключения?
Anders Hejlsberg: Нет, потому что в большом количестве случаев людям «по-барабану». Они не собираются ловить какие-либо из этих исключений. На нижнем уровне, вокруг цикла сообщений находится обработчик исключений. Этот обработчик просто выводит окошко о том, что что-то пошло не так. Программисты защищают свой код, покрывая его повсюду конструкциями try\finally , так что они просто уклоняются от обработки исключения, именно исключение они обрабатывать и не собирались.
Bill Venners: В общем, вы считаете, что в наиболее распространённом случае пользователи предпочитают обработчик на вершине стека, явной обработке исключений?
Anders Hejlsberg: Забавно, что люди думают, что наиболее важной частью взаимоотношений с исключениями является их обработка. Это как раз не так важно. В хорошо написанном приложении отношение конструкций try\finally к try\catch примерно 10 к 1.
Bill Venners: Так что же в результате?
Anders Hejlsberg: В результате, вы защищаете себя от исключений, а не обрабатываете их. Обработку исключений вы реализуете где-то в другом месте. Естественно, что в любом типе событийно-ориентированного приложения, как в случае с любым современным UI-приложением, вы реализуете обработку исключений вокруг цикла сообщений и просто здесь их и обрабатываете. Но по ходу программы вы защищаете себя, освобождая выделенные ресурсы, которые были захвачены и так далее. Вы подчищаете за собой так, что вы всегда находитесь в непротиворечивом состоянии. Вы не хотите писать программу, которая в сотне разных мест обрабатывает исключения и выбрасывает окна с сообщениями об ошибках.
Обработка исключений должна быть централизованной и вы должны просто защищать себя в то время как исключения распространяются до обработчика.
Spock speech
Проблема с масштабируемостью не меняет смысла обработки исключений так, как было предложено Андерсом Хейлсбергом, поэтому перевод выкладывать с точки зрения данной темы не целесообразно (кому интересно – могут пройти и посмотреть, но в целом всё сводится к тому, что при росте программы, количество выбрасываемых исключений растёт и никто не сможет их все обрабатывать, особенно методы, которые могут пробросить десяток-другой различных типов исключений, ваши обработчики превратятся в адские портянки try\catch и код станет невозможно поддерживать).
Совсем недавно, 3 марта 2014 года Эрик Липперт в своём блоге также поднял тему обработки исключений в C#.
Обсуждение он разбил на две части: в первой части он задал несколько вопросов, а во второй части агрегировал ответы и сделал резюме.
Итак, вопросы, которые задал читателям своего блога Эрик:
end of Spock speech
Жизнь с непроверяемыми исключениями
- Имели ли ваши программы баги, которые вы исправили, добавив обработку исключения, о возможности выброса которого вы даже не подозревали?
- Когда вы пишете новый код, который вызывает методы, которые вы не разрабатывали, то каким образом вы догадываетесь о том, какие исключения могут быть выброшены? Существуют ли типы исключений, которые, по вашему мнению, должны быть всегда обработаны в независимости от того, какой метод их выбрасывает? Например, верно ли то, что любой метод, выбрасывающий IOException должен быть всегда завёрнут в try-catch ?
Spock speech
Был ещё задан 3-й вопрос, но вопрос не очень интересный и ответы на него также особого интереса не представляют, поэтому соответствующие части будут опущены.
end of Spock speech
Основной вывод по комментариям читателей: исключения привносят небольшой беспорядок в C#. Семантика языка и организация (или недостаток организации) иерархии исключений делает сложным узнать то, какие исключения нужно ловить и какие следует пропустить. Множество читателей оставили множество отличных комментариев, но один из них произвёл на меня наиболее сильное впечатление:
Я думаю, что вся концепция «обработки» исключений слегка напоминает игру для дураков. Я, наверное, могу посчитать на пальцах одной руки количество случаев, когда я был действительно в состоянии обработать специфический тип исключения и сделать в обработчике что-то интеллектуальное. В 99% случаев ты должен ловить или всё или ничего. Когда выбрасывается исключение любого типа, восстановите стабильное состояние и затем либо продолжайте, либо прерывайте исполнение программы.
Это грубо, но, я думаю, справедливо.
Этот комментарий предполагает Покемон-обработку – поймай их всех! (gotta catch ‘em all) – вот решение проблемы. Я был удивлён тем, что почти треть комментаторов выразили поддержку использования catch(Exception) , ведь исторически это описывалось как плохая практика компанией Микрософт. C#, как я обычно говорю, спроектирован быть «кабинкой успеха» («pit of success»), где то, что является наиболее простым, является и наиболее правильным. Эта замечательная цель, похоже, в данном случае не была достигнута. Если catch(Exception) это и наименее верный путь и наиболее простой, то это потому что правильный путь слишком тяжёл.
Подавляющее большинство комментаторов написали, что правили баги, причиной которых было отсутствие catch для конкретных типов исключений, хотя такие случаи у разных людей случались с разной периодичность: «от 1 до 2 раз», «изредка», до «часто».
Треть сказали, что они использовали MSDN и другие формы документации (включая XML-комментарии) в целях определения типов исключений, которые следовало бы ловить. MSDN и хвалили и критиковали; какие-то части MSDN написаны отлично, другие написаны так, что ничего не понятно. Третью часть документации всесторонне изкритиковали, такой документации никто не верит.
Четверть сказали, что использовали что-то вроде метода проб и ошибок – отладка, тестирование, чтение логов, получения краш-дампов для того, чтобы выяснить какие исключения следует ловить.
Опять же, разочарование было резюмировано следующим комментарием:
Каждый try/catch блок это упражнение в разочаровании, потому что ты думаешь, что ловишь нужный тип исключения до тех пор, пока всё не сломается в эксплуатации.
Достаточно много комментаторов заметили, что система обработки исключений предполагает наиболее важной информацией сам тип исключения, однако одного типа недостаточно для того, чтобы произвести правильную обработку исключения.
Spock speech
Внезапно! Почти всё, что сказано либо прямо, либо косвенно противоречит The Best Practice!
Подкину ещё проблему: что, если вам нужно в цикле вызывать код, который всё время выбрасывает исключения в случае, если что-то не так? Старая непроизводительная машина (а их есть у нас в России великое множество) «сдохнет». Тут придут на помощь (если вы владелец вызываемого метода) коды возвратов или экземпляры классов, которые содержат в себе нужную информацию. Ой, оказывается коды возвратов не мертвы, как ожидалось.
А от catch(Exception) вам никуда не деться: вам придётся повсюду, а не только на уровне цикла сообщений делать catch(Exception) , если только вы не считаете, что в вашей конкретной ситуации пусть себе исключения летают через всю систему (чем дальше летят, тем больше жрут ресурсов).
Остаётся одна загвоздка с catch(Exception) : мы можем поймать StackOverflow или OutOfMemory и даже глазом не повести, что приведёт к печальным последствиям, за которые можно заплатить миллионами рублей (или долларов), если вы не пишете Hello, World!
Для «решения» (намеренно взял в кавычки, поскольку едва ли существующие решения проблем с обработкой ошибок кого-либо полностью удовлетворяют, или приближаются хотя бы близко к полному удовлетворению) означенной проблемы подойдёт фильтрование. Кстати говоря, несмотря на то, что в MSDN фильтрация исключений признаётся не лучшей практикой, сама часть Enterprise Framework ответственная за обработку исключений базируется на фильтровании, которое настраивается через соответствующие политики, сюрприз!
Вот простой статический класс, упрощающий обработку исключений:
public static class Exceptions < private static readonly ListfatalExceptions = new List < typeof (OutOfMemoryException), typeof (StackOverflowException), //Ещё типы исключений, который по вашему мнению всегда являются фатальными >; public static string FullMessage(this Exception ex) < var builder = new StringBuilder(); while (ex != null) < builder.AppendFormat("", ex, Environment.NewLine); ex = ex.InnerException; > return builder.ToString(); > public static void TryFilterCatch(Action tryAction, Func isRecoverPossible, Action handlerAction) < try < tryAction(); >catch (Exception ex) < if (!isRecoverPossible(ex)) throw; handlerAction(); >> public static void TryFilterCatch(Action tryAction, Func isRecoverPossible, Action handlerAction) < try < tryAction(); >catch (Exception ex) < if (!isRecoverPossible(ex)) throw; handlerAction(ex); >> public static bool NotFatal(this Exception ex) < return fatalExceptions.All(curFatal =>ex.GetType() != curFatal); > public static bool IsFatal(this Exception ex) < return !NotFatal(ex); >>
Примеры использования:
Exceptions.TryFilterCatch(host.Close, Exceptions.NotFatal, ex => logger.Error("Исключение при закрытии хоста сервиса.", ex)); private bool TryGetTpmProcess(out Process process) < process = null; try < process = Process.GetProcessById(App.TpmProcessId.GetValueOrDefault()); return true; >catch (Exception ex) < if (ex.IsFatal()) throw; return false; >>
Метод TryFilterCatch позволяет «портянки» преобразовывать в краткую запись. Методы расширения также оказываются удобными в использовании. Способ с TryFilterCatch подсмотрел здесь.
Кратким резюме всех моих изысканий по данном вопросу является следующий вывод: все мы пользуемся меньшим из зол, но зло выбираем в любом случае, поскольку никто не знает как избавиться ото зла или сократить его до минимума. Под необходимым злом я имею ввиду концепцию непроверяемых исключений, которые являются дефолтовым способом уведомления всех и вся о том, что что-то пошло не так в C# (да и в Java тоже с того момента, как все поняли, что проверяемые исключения ничего не дают).
Так что, не верьте в «утверждения вылитые в граните», смотрите в оба и не позволяйте исключениям убивать ваше приложение.
end of Spock speech
Как обрабатывать необработанные исключения в ASP.NET Core Web API
Чтобы сделать обработку исключений простой и последовательной, старайтесь не бросать исключения в тех случаях, когда вы можете определить ошибку самостоятельно. Я рекомендую возвращать подходящую ошибку, как в коде ниже.
[HttpGet("")] public async Task> Get(int id, CancellationToken cancellationToken) < var order = await _ordersService.Get(id, cancellationToken); if (order == null) < return NotFound(); >return Ok(order); >
Не используйте ошибки для управления потоком приложения. Использование исключений снижает производительность, негативно влияет на читабельность кода, прерывает поток и требует дополнительных действий по корректной обработке исключений.
Также избегайте такого состояния API, когда бросить исключение и отправить ошибку 500 — это единственный способ ответить на запрос. Такие ситуации должны стать поводом для рефакторинга дизайна вашего API и use cases. Отправляйте ошибку 500 только в исключительных необработанных случаях, таких как проблемы с базой данных, системные ошибки и т. п.
Существует несколько способов добавить обработку исключений в ASP.NET Core. Это Exception Filters, Exception handler lambda и Middleware. Я рекомендую последний. Middleware отлавливает ошибки из конструкторов контроллеров, фильтров и обработчиков, ошибки маршрутизации и т. п.
Реализуйте интерфейс IMiddleware и зарегистрируйте этот класс в Startup.cs, как в коде ниже. Обработчик ошибок должен быть первым в конвейере, чтобы ловить любые исключения при обработке запроса.
public class Startup < public void ConfigureServices(IServiceCollection services) < // Middlewares services.AddTransient(); services.AddTransient(); services.AddControllers(); > public void Configure(IApplicationBuilder app) < app.UseMiddleware(); // Should be always in the first place app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.UseMiddleware(); app.UseEndpoints(endpoints => < endpoints.MapControllers(); >); > >
Если исключение не обработано, клиенты API получат Unknown Error. Простейший обработчик ошибок должен поймать исключение, залогировать его и отправить статус Internal Server Error. Код ниже добавляет C#-класс, который делает все перечисленное.
public class ErrorHandlerMiddleware : IMiddleware < private readonly ILogger_logger; public ErrorHandlerMiddleware(ILogger logger) < _logger = logger; >public async Task InvokeAsync(HttpContext context, RequestDelegate next) < try < await next(context); >catch (Exception exception) < const string message = "An unhandled exception has occurred while executing the request."; _logger.LogError(exception, message); context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status500InternalServerError; >> >
Я уверен, что нет необходимости проверять свойство context.Response.HasStarted. Его достаточно хорошо обрабатывает сам .NET, бросая InvalidOperationException с подробным сообщением. Как это выглядит в консоли в нашем случае:
Достаточно ли этого? У меня в проектах обычно немного больше требований к обработке ошибок. Вот они:
- Логируйте больше деталей об исключении. Не добавляйте эти сведения в сообщение исключения. (О том, как использовать свойство Exception.Data, чтобы логировать дополнительные сведения об исключениях, вы можете прочитать в моей предыдущей статье.)
- Не отправляйте секретную внутреннюю информацию клиентам Web API, такую как stack trace, exception data и т. п.
- Не обрабатывайте TaskCanceledException как внутреннюю ошибку сервера, когда причина исключения в отмене запроса клиентом, так что наиболее подходящий HTTP-ответ в этом случае — это 499.
- Используйте JSON как наиболее подходящий веб-формат для обработки ошибок на стороне клиента.
- Текст ошибок переводится на другие языки, поэтому лучше не показывать пользователю сообщение исключения. Это должно быть что-то, что можно легко перевести, например: «Ой! Что-то пошло не так». Также в сообщении должен быть какой-нибудь уникальный код, с которым пользователь может обратиться в службу поддержки вашего приложения.
- Используйте систему мониторинга для хранения, анализа логов, поиска и агрегирования проблем по данным об ошибках, включая код ошибки. Это создаст возможности для дальнейшей автоматизации поддержки вашего приложения.
Ниже пример более сложного обработчика ошибок, который соответствует этим требованиям.
public class ErrorHandlerMiddleware : IMiddleware < private static readonly JsonSerializerOptions SerializerOptions = new() < PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = < new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) >, WriteIndented = true >; private readonly IWebHostEnvironment _env; private readonly ILogger _logger; public ErrorHandlerMiddleware(IWebHostEnvironment env, ILogger logger) < _env = env; _logger = logger; >public async Task InvokeAsync(HttpContext context, RequestDelegate next) < try < await next(context); >catch (Exception exception) when (context.RequestAborted.IsCancellationRequested) < const string message = "Request was cancelled"; _logger.LogInformation(message); _logger.LogDebug(exception, message); context.Response.Clear(); context.Response.StatusCode = 499; //Client Closed Request >catch (Exception exception) < exception.AddErrorCode(); const string message = "An unhandled exception has occurred while executing the request."; _logger.LogError(exception, exception is YourAppException ? exception.Message : message); const string contentType = "application/json"; context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = contentType; var json = ToJson(exception); await context.Response.WriteAsync(json); >> private string ToJson(in Exception exception) < var message = exception.Message; var code = exception.GetErrorCode(); if (!_env.IsDevelopmentOrQA()) < return JsonSerializer.Serialize(new < message, code >, SerializerOptions); > try < var info = exception.ToString(); var data = exception.Data; var error = new < message, code, info, data >; return JsonSerializer.Serialize(error, SerializerOptions); > catch (Exception ex) < const string mes = "An exception has occurred while serializing error to JSON"; _logger.LogError(ex, mes); >return string.Empty; > >
Я предлагаю использовать хэш-код исключения как код ошибки, чтобы отправлять пользователям один и тот же код на схожие проблемы. Чтобы создать короткий код, подойдет любой хэш-алгоритм. Я применяю наиболее доступный SHA-1, затем обрезаю результат до длины, достаточной для того, чтобы сохранить уникальность кода ошибки. Расширение для класса Exception, создающее короткий код ошибки, добавляется с помощью кода ниже.
private const string ErrorCodeKey = "errorCode"; public static Exception AddErrorCode(this Exception exception) < using var sha1 = SHA1.Create(); var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(exception.ToString())); var errorCode = string.Concat(hash[..5].Select(b =>b.ToString("x"))); exception.Data[ErrorCodeKey] = errorCode; return exception; > public static string GetErrorCode(this Exception exception)
Простой пример всплывающего окна об ошибке на стороне клиента.
Я надеюсь, этот подход поможет вам в поддержке приложений. Буду благодарен за вопросы и комментарии к статье 🙂
Собеседование по Java — исключения (exceptions) (вопросы и ответы)
Список вопросов и ответов по теме «Исключения в Java».
К списку вопросов по всем темам
Вопросы
1. Дайте определение понятию “исключение”
2. Какова иерархия исключений.
3. Можно/нужно ли обрабатывать ошибки jvm?
4. Какие существуют способы обработки исключений?
5. О чем говорит ключевое слово throws?
6. В чем особенность блока finally? Всегда ли он исполняется?
7. Может ли не быть ни одного блока catch при отлавливании исключений?
8. Могли бы вы придумать ситуацию, когда блок finally не будет выполнен?
9. Может ли один блок catch отлавливать несколько исключений (с одной и разных веток наследований)?
10. Что вы знаете об обрабатываемых и не обрабатываемых (checked/unchecked) исключениях?
11. В чем особенность RuntimeException?
12. Как написать собственное (“пользовательское”) исключение? Какими мотивами вы будете руководствоваться при выборе типа исключения: checked/unchecked?
13. Какой оператор позволяет принудительно выбросить исключение?
14. Есть ли дополнительные условия к методу, который потенциально может выбросить исключение?
15. Может ли метод main выбросить исключение во вне и если да, то где будет происходить обработка данного исключения?
16. Если оператор return содержится и в блоке catch и в finally, какой из них “главнее”?
17. Что вы знаете о OutOfMemoryError?
18. Что вы знаете о SQLException? К какому типу checked или unchecked оно относится, почему?
19. Что такое Error? В каком случае используется Error. Приведите пример Error’а.
20. Какая конструкция используется в Java для обработки исключений?
21. Предположим, есть блок try-finally. В блоке try возникло исключение и выполнение переместилось в блок finally. В блоке finally тоже возникло исключение. Какое из двух исключений “выпадет” из блока try-finally? Что случится со вторым исключением?
22. Предположим, есть метод, который может выбросить IOException и FileNotFoundException в какой последовательности должны идти блоки catch? Сколько блоков catch будет выполнено?
Ответы
1. Дайте определение понятию “исключение”
Исключение — это проблема(ошибка) возникающая во время выполнения программы. Исключения могут возникать во многих случаях, например:
- Пользователь ввел некорректные данные.
- Файл, к которому обращается программа, не найден.
- Сетевое соединение с сервером было утеряно во время передачи данных. И т.д.
Все исключения в Java являются объектами. Поэтому они могут порождаться не только автоматически при возникновении исключительной ситуации, но и создаваться самим разработчиком.
2. Какова иерархия исключений.
Исключения делятся на несколько классов, но все они имеют общего предка — класс Throwable. Его потомками являются подклассы Exception и Error.
Исключения (Exceptions) являются результатом проблем в программе, которые в принципе решаемы и предсказуемы. Например, произошло деление на ноль в целых числах.
Ошибки (Errors) представляют собой более серьёзные проблемы, которые, согласно спецификации Java, не следует пытаться обрабатывать в собственной программе, поскольку они связаны с проблемами уровня JVM. Например, исключения такого рода возникают, если закончилась память, доступная виртуальной машине. Программа дополнительную память всё равно не сможет обеспечить для JVM.
В Java все исключения делятся на два типа: контролируемые исключения (checked) и неконтролируемые исключения (unchecked), к которым относятся ошибки (Errors) и исключения времени выполнения (RuntimeExceptions, потомок класса Exception).
Контролируемые исключения представляют собой ошибки, которые можно и нужно обрабатывать в программе, к этому типу относятся все потомки класса Exception (но не RuntimeException).
3. Можно/нужно ли обрабатывать ошибки jvm?
Обрабатывать можно, но делать этого не стоит. Разработчику не предоставлены инструменты для обработки ошибок системы и виртуальной машины.
4. Какие существуют способы обработки исключений?
В Java есть пять ключевых слов для работы с исключениями:
- try — данное ключевое слово используется для отметки начала блока кода, который потенциально может привести к ошибке.
- catch — ключевое слово для отметки начала блока кода, предназначенного для перехвата и обработки исключений.
- finally — ключевое слово для отметки начала блока кода, которой является дополнительным. Этот блок помещается после последнего блока ‘catch’. Управление обычно передаётся в блок ‘finally’ в любом случае.
- throw — служит для генерации исключений.
- throws — ключевое слово, которое прописывается в сигнатуре метода, и обозначающее что метод потенциально может выбросить исключение с указанным типом.
Общий вид конструкции для «поимки» исключительной ситуации выглядит следующим образом:
Использование механизма исключений в Java
Чтобы разработать очевидную и последовательную стратегию обработки исключений, ответьте на вопросы, непрерывно досаждающие Java разработчикам:
- Какие исключения я должен использовать?
- Когда я должен использовать исключения?
- Как я могу наилучшим образом использовать исключения?
- Каково влияние на производительность?
При разработке мультиплатформенных API и приложений или при разработке их третьими лицами, ответы на эти вопросы становятся наиболее значимыми.
Какие исключения я должен использовать?
Исключения бывают двух типов:
- Исключения, вызванные компилятором, или проверенные исключения
- Исключения времени выполнения или непроверенные исключения
В языке Java исключения (Exceptions) и ошибки (Errors) являются объектами. Когда метод вызывает, еще говорят «бросает» от слова «throws», исключительную ситуацию, он на самом деле работает с объектом. Но такое происходит не с любыми объектами, а только с теми, которые наследуются от Throwable. На рисунке ниже показана диаграмма классов стандартных исключений Java:
Исключения, вызванные компилятором (проверенные, checked exception) являются экземплярами класса Exception или одного из его подклассов — исключая ветвь RuntimeException. RuntimeException, Error и их наследников еще называют непроверенными (unchecked exception).
Компилятор ожидает, что все проверенные исключения будут соответствующим образом обработаны. Проверенные исключения должны быть объявлены в throws разделе метода, который может их вызвать — конечно, если они не будут обработаны в этом же методе. Вызывающий метод должен позаботится об этих исключениях путем их обработки или объявлении их в секции throws . Таким образом, проверенные исключения дают программисту возможность их обработки.
Checked Exception обязывает пользователя обработать ее (использую конструкцию try-catch) или же отдать на откуп обрамляющим методам, в таком случае к декларации метода, который бросает проверяемое (checked) исключение, дописывают конструкцию throws, например:
public Date parse(String source) throws ParseException
Например проверенным исключением является java.io.IOException . Исходя из его названия, оно вызывается при неправильном завершении операции ввода/вывода. Посмотрите на следующий код:
try < BufferedReader br = new BufferedReader(new FileReader("MyFile.txt")); String line = br.readLine(); >catch(FileNotFoundException fnfe) < System.out.println( "Файл MyFile.txt не найден."); >catch(IOException ioe)
Конструктор класса FileReader вызывает исключения FileNotFoundException — подкласс класса IOException — если файл не найден. Иначе, если файл существует, но по каким-то причинам метод readLine() не может из него читать, FileReader вызывает IOException .
Исключения времени выполнения (непроверенные) — это экземпляры класса RuntimeException или одного из его подклассов. Вы не должны объявлять такие исключения в секции throws вызываемого метода. Также, вызывающий метод не должен их обрабатывать, хотя и может. Непроверенные исключения обычно вызываются только при возникновении проблем в окружении виртуальной машины Java (JVM). Также, программисты должны воздерживаться от их обработки, так как JVM удобнее самой обработать эту ситуацию.
К unchecked исключениям относятся, например, NullPointerException, ArrayIndexOutOfBoundsException, ClassCastExcpetion и так далее. Это те ошибки, которые могут возникнут практически в любом методе. Несомненно, описывать каждый метод как тот, который бросает все эти исключения, было бы глупо.
java.lang.ArithmeticException — это пример непроверенного исключения, вызываемого при возникновении арифметических ошибок. Например, деление на ноль вызывает экземпляр этого класса. Следующий код иллюстрирует использование непроверенного исключения :
public static float fussyDivide(float dividend, float divisor) throws FussyDivideException < float q; try < q = dividend/divisor; >catch(ArithmeticException ae) < throw new FussyDivideException( "Can't divide by zero."); >> public class FussyDivideException extends Exception < public FussyDivideException(String s) < super(s); >>
fussyDivide() гарантирует вызывающему методу, что вызываемый не допустит деления на ноль. Он делает это путем перехвата ArithmeticException — непроверенного исключения — и за этим вызывает FussyDivideException — проверенное исключение.
Для понимания того, делать ли исключение проверенным или непроверенным, следуйте правилу: Если исключение должен обработать вызывающий метод, тогда исключение должно быть проверенным, иначе оно может быть непроверенным.
Когда я должен использовать исключения?
Спецификация Java гласит, что «исключение должно быть вызвано при нарущении семантических ограничений,» что в основном подразумевает вызов исключения в невозможных в обычных условиях ситуациях или при грубом нарушении допустимого поведения. Иными словами: если в методе возможна ситуация, которую метод не в состоянии обработать самостоятельно, он должен «бросать» ошибку. Но ни в коем случае нельзя использовать исключительные ситуации для управления ходом выполнения программы.
Чаще всего Exceptions бросаются при нарушении контракта метода. Контракт (contract) — это негласное соглашение между создателем метода (метод сделает и/или вернет именно то, что надо) и пользователем метода (на вход метода будут передаваться значения из множества допустимых).
Нарушение контракта со стороны создателя метода — это, например, что-нибудь на подобии MethodNotImplementedYetException :).
Пользователь метода может нарушить контракт, например, таким способом: на вход Integer.parseInt(String) подать строку с буквами и по заслугам получить NumberFormatException .
Чтобы более ясно понять виды поведения, которые могут классифицироваться как «нормальное» или исключительное, смотрите на примеры кода
Passenger getPassenger() < try < Passenger flier = object.searchPassengerFlightRecord("Jane Doe"); catch(NoPassengerFoundException npfe) < //какая-то работа >>
Passenger getPassenger() < Passenger flier = object.searchPassengerFlightRecord("Jane Doe"); if(flier == null) //какая-то работа >
В случае 1, при неудачном поиске пассажира, вызывается NoPassengerFoundException ; а в случае 2, простая проверка на null проверяет успешность. Разработчики часто встречаются с подобными ситуациями в повседневной работе; фокус в проектировании эффективной стратегии.
Итак, после общей философии об исключениях, что вы должны делать при возможности того, что поиск ничего не отыщет? Если результат поиска пуст, это не более чем случай нормальной работы? Поэтому, для логичного использования исключений, предпочтите подход в случае 2, а не в случае 1. (Да — мы учитываем влияние на производительность. Если бы этот код был бы в компактном цикле, тогда бы многократные вычисления if могли бы ухудшить производительность. Однако, действительное влияние выражения if на критичном участке на производительность можно узнать только после профайлинга и обширного анализа производительности. Эмпирические результаты показывают, что пытась писать максимально производительный код при игнорировании принципов дизайна чаще приносит больше вреда чем пользы. Итак, начните сначала и проектируйте вашу систему правильно, и затем измените позже при необходимости.)
Хороший пример исключительной ситуации: — Если экземпляр объекта, являющийся результатом вызова метода поиска был null , это приводит к фундаментальному нарушению семантики методов класса getPassenger . Для понимания влияния исключений на производительность прочтите параграф о производительности.
Как я могу наилучшим образом использовать исключения?
Все Java разработчики должны решать задачу перехвата различных видов исключений и должны знать, что с ними делать. Это усложняется, когда код должен обрабатывать нетривиальные ошибки системного уровня, и упрощается на прикладном уровне. Это особенно проявляется при создании своего API, используемого в других приложениях, и вы не имеете графического интерфейса.
Обычно используется один из трех подходов обработки исключений:
- Перехват и обработка всех исключений.
- Объявление исключений в секции throws метода и передача исключений вызывающему методу.
- Перехват исключений и преобразование его к другому классу и повторный вызов исключения.
Давайте рассмотрим эти случаи и попытаемcя разработать реальное решение.
Passenger getPassenger() < try < Passenger flier = object.searchPassengerFlightRecord("John Doe"); >catch(MalformedURLException ume) < //какая-то обработка >catch(SQLException sqle) < //какая-то обработка >>
В одном крайнем случае вы могли бы перехватывать все исключения и найти способ уведомления вызывающего метода о возникших ошибках. Этот подход, как показано в первом примере, должен вернуть null значение или другим способом уведомить вызывающий метод о возникшей ошибке.
Как стратегия дизайна, этот подход имеет значительные недостатки. Вы теряете всю поддержку времени компиляции, и вызываемый метод должен заботится о проверке всех возможных возвращаемых значений. Также здесь смешивается рабочий код и код обработки ошибок, что приводит к беспорядку.
Passenger getPassenger() throws MalformedURLException, SQLException
Случай 2 является другой крайностью. Метод getPassenger() объявляет все исключения, вызываемые данным методом в throws секции. Таким образом, getPassenger() знает о всех возможных исключительных ситуацияхt, но предпочитает не обрабатывать их а передать их вызывающему методу. То есть происходит передача исключения вызываемому методу. Однако это не является жизнеспособным решением, так как вся ответственность за обработку ошибок «всплывает наверх» — или перемещается вверх по иерархии — что может вызвать значительные проблемы, особенно при переносе с однойсплатформы на другую. Представьте, например, что вы разрабатываете Sabre (система бронирования авиабилетов), и метод searchPassengerFlightRecord() — часть вашего API для пользователя вашей системы. Приложение Travelocity , которое включает в себя getPassenger() , как часть системы, должно работать с каждым исключением, генерируемым вашей системой. Также, приложение может не интересовать то, какое исключение вызвано: MalformedURLException или SQLException , его только заботит что-то вроде «Поиск неудачен». Рассмотрим это в примере 3.
Passenger getPassenger() throws TravelException < try < Passenger flier = object.searchPassengerFlightRecord("Gary Smith"); >catch(MalformedURLException ume) < //какая-то обработка throw new TravelException( "Search Failed", ume); >catch(SQLException sqle) < //какая-то обработка throw new TravelException( "Search Failed", sqle); >>
Случай 3 находится посередине между двумя крайностями случаев 1 и 2, используя свой класс-исключение TravelException . Этот класс понимает действительно произошедшие исключения, передаваемые ему в качестве аргумента и преобразует их из сообщений системного уровня в сообщение прикладного уровня. Так вы сохраняете гибкость знания о действительно произошедшем исключении путем сохранения оригинального исключения как части экземпляра нового объекта-исключения, что упрощает отладку. Этот подход предоставляет элегантный метод проектирования API и кросс-платформенных приложений.
Следующий код демонстрирует класс TravelException , который мы использовали в качестве собственного исключения в примере 3. Он принимает два аргумента. Один из них — сообщение, которое может быть выведено в поток ошибок; другой — реальное исключение, которое привело к вызову нашего исключения. Этот код показывает как можно сохранить другую информациб внутри пользовательского исключения. Преимущество этого сохранения состоит в том, что если вызываемый метод захочет узнать реальнуб причину вызова TravelException , он всего лишь должен вызвать метод getHiddenException() . Это позволяет вызывающемому методу решить, нужно ли работать со специфичным исключением или достаточно обработки TravelException .
public class TravelException extends Exception < private Exception hiddenException_; public TravelException(String error, Exception excp) < super(error); hiddenException_ = excp; >public Exception getHiddenException() < return(hiddenException_); >>
Каково влияние на производительность?
Исключения имеют цену, и для понимания получаемых результатов, рассмотрим механизм обработки исключений. JVM поддерживает стек вызовов, содержащий список методов, вызванных данной нитью, начиная с первого вызванного нитью метода и заканчивая с текущим методом. Стек вызовов показывает путь вызова методов, произведенных нитью для достижения текущего метода.
Стек вызовов Java показывает вызванные методы. Вершиной является текущий выполняемый метод.
Рисунок показывает графическое представление стека вызовов нашего кода. Внутри виртуальной машины Java, методы сохраняют свое состояние в стеке Java. Каждый метод получает кадр стека, вталкиваемый в стек при вызове метода и выталкиваемый при завершении метода. (Кадр стека хранит локальные переменные метода, параметры, возвращаемое значение, и другую важную информацию необходимую JVM). При нормальном завершении метода его кадр выталкивается из стека и фрейм стека, находящегося под ним, становится текущим, то есть с выполняемым в настоящий момент методом.
Так, с целью нашего сравнения, что требуется в случае 1? Если NoPassengerFoundException вызывается при выполнении метода searchPassengerFlightRecord() (вершина стека), дальнейшее выполнение кода прерывается и JVM получает управление для осуществления механизма обработки ошибок Java. Затем виртуальная машина Java ищет в текущем методе секцию catch для исключения NoPassengerFoundException . Если секция не найдена, тогда JVM выталкивает кадр текущего метода из стека, и вызывающий метод становится текущим. JVM снова ищет в текущем методе, который ранее был вызывающим, подходящую catch секцию. Этот цикл повторяется до нахождения подходящего обработчика исключения. Или, если нить является последней нефоновой нитью, тогда приложение прерывается с распечаткой стека вызовов на консоли. Вкратце, применение JVM для обработки вызванного исключения требует больше ресурсов, т.е. принудительное завершение метода является значительно более дорогим в плане производительности, чем нормальное завершение метода.
Finally
В Java поддерживается конструкция try-catch-finally. В С++ я ряде других языков слова finally нет. Пользуются им примерно так:
InputStream is = null; try < is = new FileInputStream("c:/file.txt"); is.read(…) >finally
Независимо от того, нормально ли отработал блок try, или там возникло исключение, блок finally вызовется всегда, и там можно будет освободить занятые ресурсы.
Такой подход является альтернативой использования метода finalize() , который во многом похож на деструкторы С++ и тоже используется для освобождения ресурсов, однако момент его вызова не определен. Но есть один большой минус данного метода — Java-машина может отложить уничтожение объекта, как и вызов метода finalize() на сколько угодно. Эти методы вызываются сборщиком мусора, то есть точно не будут выполнены до того, как кончится память. Даже если память кончится — вызов может не произойти, финализируемые объекты JVM удаляет в последнюю очередь.
Напротив, момент вызова finally вполне определен, то есть с его помощью мы можем освободить ресурсы сразу после того, как они стали не нужны.
В Java 7 появилась новая форма записи такой конструкции:
try(InputStream is = new FileInputStream(«c:/file.txt»))
Это специальная конструкция try , называемая try-with-resources . После try следуют круглые скобки, где объявляются переменные и создаются объекты. Эти объекты можно использовать внутри блока try, обозначенного скобками <>. Когда выполнение команд блока try закончится, независимо от того – нормально оно закончилось или было исключение, для объекта, созданного внутри круглых скобок (), будет вызван метод close() . Но для того чтобы иметь возможность создать объект внутри круглых скобок блока try необходимо имплементировать свой объект от специального интерфейса AutoCloseable .
public interface AutoCloseable
Данный интерфейс имеет всего лишь один метод close() , который необходимо переопределить, и именно данный метод будет вызываться. Только объекты такого типа можно использовать внутри круглых скобок try-with-resources для «автоматического закрытия».
При необходимости внутри круглых скобок можно создавать несколько объектов, разделив их точкой с запятой. После последнего объекта точка с запятой не указывается.
try( InputStream is = new FileInputStream(«c:/file.txt»); OutputStream os = new FileOutputStream(«c:/output.txt») )
Методы класса Throwable
Класс Throwable определяет ряд интересных методов:
- String getMessage() — возвращает детализированную информацию о возбуждаемом исключении.
- String getLocalizedMessage() — возвращает дополнительную информацию о возбуждаемом исключении, специфичную для данного контекста.
- void printStackTrace() — выполняет вывод самого исключения и последовательность вызовов, которые привели к этому исключению.
- StackTraceElement[] getStackTrace() — возвращает ту же самую последовательность вызовов, что и в printStackTrace() , но в виде отдельных кадров стека.
Заключение
Важно иметь продуманную и последовательную стратегию обработки исключений ради эффективности и хорошей практике программирования. Обработка исключительных ситуаций должна рассматриваться не как добавочная, а как неотъемлемая часть процесса разработки. Сила исключений позволяет разрабатывать приложения, которые являются устойчивыми и надежными в соответствии с проектом, а не по случайности.