Дело против проверенных исключений - PullRequest
418 голосов
/ 05 марта 2009

В течение ряда лет я не мог получить достойный ответ на следующий вопрос: почему некоторые разработчики так против проверенных исключений? У меня было много разговоров, читал что-то в блогах, читал то, что говорил Брюс Экель (первый человек, которого я видел, выступал против них).

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

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

В целом (из того, как был разработан Java),

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

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

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

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

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

У меня нет проблем с переносом Main с catch (Exception) (или, в некоторых случаях, catch (Throwable), чтобы программа могла корректно завершиться - но я всегда ловлю конкретные исключения, которые мне нужны. Это позволяет мне, по крайней мере, отобразить соответствующее сообщение об ошибке.

Вопрос, на который люди никогда не отвечают, таков:

Если вы выбросите RuntimeException подклассы вместо исключения подклассы, то как вы знаете, что ты должен ловить?

Если ответом является catch Exception, то вы также имеете дело с ошибками программиста так же, как системные исключения. Это кажется мне неправильным.

Если вы перехватываете Throwable, то вы обрабатываете системные исключения и ошибки ВМ (и тому подобное) одинаково. Это кажется мне неправильным.

Если ответ таков, что вы ловите только те исключения, которые, как вы знаете, выбрасываются, то как вы узнаете, какие из них выбрасываются? Что происходит, когда программист X выдает новое исключение и забывает его поймать? Это кажется мне очень опасным.

Я бы сказал, что программа, отображающая трассировку стека, неверна. Неужели люди, которым не нравятся проверенные исключения, так не чувствуют?

Итак, если вам не нравятся отмеченные исключения, можете ли вы объяснить, почему нет, И, пожалуйста, ответьте на вопрос, на который нет ответа?

Редактировать: я не ищу совета о том, когда использовать какую-либо модель, я ищу , почему люди выходят из RuntimeException, потому что им не нравится выходить из Exception и / или почему они ловят исключение, а затем перебрасывать RuntimeException вместо добавления бросков к их методу. Я хочу понять мотивацию неприязни к проверенным исключениям.

Ответы [ 32 ]

259 голосов
/ 05 марта 2009

Я думаю, что прочитал то же интервью с Брюсом Экелом, которое вы дали - и оно всегда меня раздражало. Фактически, аргумент был сделан интервьюируемым (если это действительно тот пост, о котором вы говорите) Андерсом Хейлсбергом, гением MS, стоящим за .NET и C #.

http://www.artima.com/intv/handcuffs.html

Несмотря на то, что я из Хейлсберга и его работ, этот аргумент всегда казался мне фальшивым. Это в основном сводится к:

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

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

Сводка сводки аргумента такова: «Программисты не будут использовать их должным образом, и их неправильное использование хуже, чем отсутствие их» * .

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

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

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

Теперь программист API должен быть осторожен, чтобы не создавать повсеместно проверенные исключения, иначе они просто будут раздражать клиентского программиста. Очень ленивый клиентский программист прибегнет к ловле (Exception) {}, как предупреждает Хейлсберг, и вся выгода будет потеряна, и ад последует. Но в некоторых обстоятельствах просто нет замены хорошему проверенному исключению.

Для меня классическим примером является API открытия файлов. Каждый язык программирования в истории языков (по крайней мере, в файловых системах) имеет где-то API, который позволяет открывать файл. И каждый клиентский программист, использующий этот API, знает, что им приходится иметь дело с тем случаем, что файл, который они пытаются открыть, не существует. Позвольте мне перефразировать это: каждый клиентский программист, использующий этот API , должен знать , что он должен иметь дело с этим делом. И тут есть одна загвоздка: могут ли программисты API помочь им узнать, что им следует справиться с этим, оставив только комментарии, или они действительно настаивают , что клиент справится с этим.

В Си идиома выглядит примерно так:

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

где fopen указывает на ошибку, возвращая 0, а C (по глупости) позволяет вам рассматривать 0 как логическое значение и ... В основном, вы изучаете эту идиому и все в порядке. Но что, если ты новичок и ты не выучил идиому. Затем, конечно, вы начинаете с

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

и учиться трудному пути.

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

Этот четко определенный протокол обычно определяется сигнатурой метода. Здесь fopen требует, чтобы вы передали ему строку (или символ * в случае C). Если вы дадите ему что-то еще, вы получите ошибку во время компиляции. Вы не следовали протоколу - вы не используете API должным образом.

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

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

(В динамически типизированном языке, таком как Ruby, вы можете передать что угодно, скажем, float, в качестве имени файла - и он скомпилируется. Зачем беспокоить пользователя проверенными исключениями, если вы даже не собираетесь контролировать метод аргументы. Аргументы, приведенные здесь, применимы только к языкам со статической типизацией.)

Итак, что насчет проверенных исключений?

Вот один из API-интерфейсов Java, который вы можете использовать для открытия файла.

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

Видишь этот улов? Вот подпись для этого метода API:

public FileInputStream(String name)
                throws FileNotFoundException

Обратите внимание, что FileNotFoundException является проверено исключение.

Программист API говорит вам следующее: «Вы можете использовать этот конструктор для создания нового FileInputStream, но вы

a) должен передать имя файла как Строка
б) должен принять вероятность того, что файл не может быть найденным во время выполнения "

И в этом весь смысл.

Ключ в основном состоит в том, что в вопросе говорится как «Вещи, которые находятся вне контроля программиста». Моей первой мыслью было, что он / она имеет в виду то, что находится вне контроля программистов API . Но на самом деле, проверенные исключения при правильном использовании должны действительно относиться к вещам, которые находятся вне контроля как программиста клиента, так и программиста API. Я думаю, что это ключ к тому, чтобы не злоупотреблять проверенными исключениями.

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

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

Это было бы яснее с контр-кейсом. Представьте, что я пишу таблицу API. У меня есть модель таблицы где-то с API, включая этот метод:

public RowData getRowData(int row) 

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

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(я бы на самом деле не назвал это "Проверено".)

Это неправильное использование проверенных исключений. Код клиента будет полон вызовов для извлечения данных строки, каждый из которых должен будет использовать try / catch, и для чего? Собираются ли они сообщить пользователю, что был найден неправильный ряд? Вероятно, нет - потому что, каким бы ни был пользовательский интерфейс, окружающий мое табличное представление, он не должен позволять пользователю переходить в состояние, когда запрашивается недопустимая строка. Так что это ошибка со стороны клиентского программиста.

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

С проверенным исключением в getRowData, это явно тот случай, который приведет к тому, что ленивый программист Хейлсберга просто добавит пустые уловы. Когда это происходит, недопустимые значения строк не будут очевидны даже для отладчика тестировщика или разработчика клиента, скорее, они приведут к ошибкам, которые трудно определить источник. Ракеты Арианны взорвутся после запуска.

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

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

  1. Вне контроля клиента или Закрыто или Открыто:

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

  2. Ubiquity:

    Проверенные исключения не должны использоваться при вызове API, который часто выполняется клиентом. Под частым я имею в виду множество мест в клиентском коде - не часто во времени. Поэтому клиентский код не склонен многократно открывать один и тот же файл, но мое табличное представление получает RowData повсеместно из разных методов. В частности, я собираюсь написать много кода вроде

    if (model.getRowData().getCell(0).isEmpty())
    

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

  3. Информирование пользователя:

    Проверенные исключения следует использовать в тех случаях, когда вы можете представить полезное сообщение об ошибке, которое будет представлено конечному пользователю. Это «и что вы будете делать, когда это произойдет?» вопрос, который я поднял выше. Это также относится к пункту 1. Поскольку вы можете предсказать, что что-то за пределами вашей системы клиент-API может привести к тому, что файл не будет там, вы можете разумно сообщить об этом пользователю:

    "Error: could not find the file 'goodluckfindingthisfile'"
    

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

    "Internal error occured: IllegalArgumentException in ...."
    

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

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

Извините, я не хотел делать это так долго и вафельно. Позвольте мне закончить с двумя предложениями:

A: Программисты API: экономно используйте проверенные исключения, чтобы сохранить их полезность. В случае сомнений используйте непроверенное исключение.

B: Клиентские программисты: привычка создавать упакованное исключение (Google google) на раннем этапе разработки. JDK 1.4 и более поздние версии предоставляют конструктор в RuntimeException для этого, но вы также можете легко создать и свой собственный. Вот конструктор:

public RuntimeException(Throwable cause)

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

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

Поместите это в один из маленьких шаблонов кода вашей IDE и используйте его, когда вам лень. Таким образом, если вам действительно нужно обработать проверенное исключение, вы будете вынуждены вернуться и разобраться с ним, увидев проблему во время выполнения. Потому что, поверьте мне (и Андерсу Хейлсбергу), вы никогда не вернетесь к этому TODO в своем

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}
176 голосов
/ 05 марта 2009

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

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

public [int or IOException] writeToStream(OutputStream stream) {
    [void or IOException] a= stream.write(mybytes);
    if (a instanceof IOException)
        return a;
    return mybytes.length;
}

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

Проблема в том, что большая часть кода, в том числе большие наборы стандартной библиотеки, неправильно использует проверенные исключения для реальных исключительных условий, которые вы вполне можете захотеть поднять на несколько уровней. Почему IOException не является RuntimeException? На любом другом языке я могу допустить исключение ввода-вывода, и если я ничего не сделаю для его обработки, мое приложение остановится, и я получу удобную трассировку стека для просмотра. Это лучшее, что может случиться.

Возможно, двумя способами из примера вы хотите перехватить все IOException от всего процесса записи в поток, прервать процесс и перейти к коду сообщения об ошибках; в Java вы не можете сделать это, не добавляя «throws IOException» на каждом уровне вызова, даже на уровнях, которые сами не выполняют IO. Такие методы не должны знать об обработке исключений; добавление исключений в свои подписи:

  1. излишне увеличивает сцепление;
  2. делает подписи интерфейса очень хрупкими для изменения;
  3. делает код менее читабельным;
  4. настолько раздражает, что обычная реакция программиста состоит в том, чтобы победить систему, выполнив что-то ужасное, например, «throws Exception», «catch (Exception e) {}», или завернуть все в RuntimeException (что делает отладку более сложной).

А еще есть множество смешных библиотечных исключений, таких как:

try {
    httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
    throw new CanNeverHappenException("oh dear!");
}

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

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

Проверенные исключения, реализованные в Java и его стандартной библиотеке, означают шаблон, шаблон, шаблон. На и без того многословном языке это не победа.

73 голосов
/ 05 марта 2009

Вместо того, чтобы перефразировать все (многие) причины против проверенных исключений, я выберу только одну. Я потерял счетчик количества написаний этого блока кода:

try {
  // do stuff
} catch (AnnoyingcheckedException e) {
  throw new RuntimeException(e);
}

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

Я также потерял счет, сколько раз видел это:

try {
  // do stuff
} catch (AnnoyingCheckedException e) {
  // do nothing
}

Почему? Потому что кому-то приходилось иметь с этим дело и было лень. Было ли это неправильно? Конечно. Это происходит? Абсолютно. Что если бы это было неконтролируемое исключение? Приложение только что умерло (что предпочтительнее, чем проглатывание исключения).

И затем у нас есть сводящий с ума код, который использует исключения в качестве формы управления потоком, как java.text.Format . Bzzzt. Неправильно. Пользователь, помещающий «abc» в числовое поле в форме, не является исключением.

Хорошо, я думаю, что было три причины.

44 голосов
/ 05 марта 2009

Ну, речь не идет об отображении трассировки стека или тихом сбое. Это возможность сообщать об ошибках между слоями.

Проблема с проверенными исключениями заключается в том, что они побуждают людей проглотить важные детали (а именно, класс исключений). Если вы решите не проглатывать эту деталь, то вам придется добавлять объявления бросков по всему вашему приложению. Это означает, 1) что новый тип исключения будет влиять на множество сигнатур функций, и 2) вы можете пропустить конкретный экземпляр исключения, которое вы действительно хотите поймать (скажем, вы открываете дополнительный файл для функции, которая записывает данные в файл. Вторичный файл является необязательным, поэтому вы можете игнорировать его ошибки, но поскольку подпись throws IOException, это легко не заметить).

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

Конечно, теперь нам нужно специализировать обработку ошибок на более высоких уровнях, реализуя логику повторных попыток и тому подобное. Все это AppSpecificException, поэтому мы не можем сказать «Если IOException выброшен, повторите попытку» или «Если ClassNotFound выброшен, прервать полностью». У нас нет надежного способа получить исключение real , потому что вещи переупаковываются снова и снова, когда они проходят между нашим кодом и сторонним кодом.

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

Я снова и снова обнаруживал, что во всем проекте, о котором я упоминал, обработка исключений делится на 3 категории:

  1. Поймать и обработать специфическое исключение. Это для реализации логики повторения, например.
  2. Поймать и отбросить другие исключения. Все, что здесь происходит, это, как правило, ведение журнала, и обычно это банальное сообщение типа «Невозможно открыть $ filename». Это ошибки, с которыми ничего не поделаешь; только более высокие уровни знают достаточно, чтобы справиться с этим.
  3. Поймай все и выведи сообщение об ошибке. Обычно это находится в самом корне диспетчера, и все, что он делает, это гарантирует, что он может сообщить об ошибке вызывающей стороне через механизм не-исключения (всплывающий диалог, маршалинг объекта ошибки RPC и т. Д.).
43 голосов
/ 27 ноября 2013

Я знаю, что это старый вопрос, но я провел некоторое время в борьбе с проверенными исключениями, и мне есть что добавить. Пожалуйста, прости меня за это!

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

Возьмите хороший старый интерфейс Java List. У нас есть общие реализации в памяти, такие как ArrayList и LinkedList. У нас также есть скелетный класс AbstractList, который позволяет легко создавать новые типы списков. Для списка только для чтения нам нужно реализовать только два метода: size() и get(int index).

Этот пример WidgetList класс читает некоторые объекты фиксированного размера типа Widget (не показаны) из файла:

class WidgetList extends AbstractList<Widget> {
    private static final int SIZE_OF_WIDGET = 100;
    private final RandomAccessFile file;

    public WidgetList(RandomAccessFile file) {
        this.file = file;
    }

    @Override
    public int size() {
        return (int)(file.length() / SIZE_OF_WIDGET);
    }

    @Override
    public Widget get(int index) {
        file.seek((long)index * SIZE_OF_WIDGET);
        byte[] data = new byte[SIZE_OF_WIDGET];
        file.read(data);
        return new Widget(data);
    }
}

Предоставляя виджеты с помощью привычного интерфейса List, вы можете извлекать элементы (list.get(123)) или просматривать список (for (Widget w : list) ...), не зная о самом WidgetList. Можно передать этот список любым стандартным методам, которые используют общие списки, или заключить его в Collections.synchronizedList. Коду, который его использует, не нужно ни знать, ни заботиться о том, создаются ли «виджеты» на месте, поступают ли они из массива, читаются ли они из файла, базы данных, по всей сети или из будущего ретранслятора подпространства. Он все равно будет работать правильно, потому что интерфейс List реализован правильно.

Только это не так. Вышеупомянутый класс не компилируется, потому что методы доступа к файлу могут выдавать IOException, проверенное исключение, которое вы должны «поймать или указать». Вы не можете указать его как выброшенный - компилятор не позволит вам этого сделать, потому что это нарушит контракт интерфейса List. И нет никакого полезного способа, которым WidgetList сам может обработать исключение (как я объясню позже).

Очевидно, единственное, что нужно сделать, это перехватить и перебросить проверенные исключения как неконтролируемое исключение:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw new WidgetListException(e);
    }
}

public static class WidgetListException extends RuntimeException {
    public WidgetListException(Throwable cause) {
        super(cause);
    }
}

((Редактировать: Java 8 добавила класс UncheckedIOException именно для этого случая: для перехвата и перебрасывания IOException s через границы полиморфного метода. Вид доказывает мою точку зрения!))

Так что проверенные исключения просто не работают в подобных случаях. Вы не можете бросить их. То же самое для умного Map, поддерживаемого базой данных, или реализации java.util.Random, подключенной к источнику квантовой энтропии через COM-порт. Как только вы пытаетесь сделать что-то новое с использованием полиморфного интерфейса, концепция проверенных исключений не срабатывает. Но проверенные исключения настолько коварны, что они все равно не оставят вас в покое, потому что вам все равно придется перехватывать и отбрасывать любые из методов более низкого уровня, загромождать код и загромождать трассировку стека.

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

На самом деле, вы можете генерировать необъявленные проверенные исключения, если вы прибегаете к взлому. Во время выполнения JVM не заботится о проверенных правилах исключений, поэтому нам нужно обмануть только компилятор. Самый простой способ сделать это - использовать дженерики. Это мой метод для него (имя класса показано, потому что (до Java 8) это требуется в синтаксисе вызова для универсального метода):

class Util {
    /**
     * Throws any {@link Throwable} without needing to declare it in the
     * method's {@code throws} clause.
     * 
     * <p>When calling, it is suggested to prepend this method by the
     * {@code throw} keyword. This tells the compiler about the control flow,
     * about reachable and unreachable code. (For example, you don't need to
     * specify a method return value when throwing an exception.) To support
     * this, this method has a return type of {@link RuntimeException},
     * although it never returns anything.
     * 
     * @param t the {@code Throwable} to throw
     * @return nothing; this method never returns normally
     * @throws Throwable that was provided to the method
     * @throws NullPointerException if {@code t} is {@code null}
     */
    public static RuntimeException sneakyThrow(Throwable t) {
        return Util.<RuntimeException>sneakyThrow1(t);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> RuntimeException sneakyThrow1(
            Throwable t) throws T {
        throw (T)t;
    }
}

Ура! Используя это, мы можем выбросить проверенное исключение на любую глубину стека, не объявляя его, не заключая его в RuntimeException и не загромождая трассировку стека! Снова используем пример «WidgetList»:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw sneakyThrow(e);
    }
}

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

try {
    ...
} catch (Throwable t) { // catch everything
    if (t instanceof IOException) {
        // handle it
        ...
    } else {
        // didn't want to catch this one; let it go
        throw t;
    }
}

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

К счастью, оператор throw t; здесь допустим, несмотря на то, что проверен тип t, благодаря добавленному в Java 7 правилу о перебрасывании перехваченных исключений.


Когда отмеченные исключения встречаются с полиморфизмом, возникает и обратный случай: когда метод определен как потенциально вызывающий проверяемое исключение, а переопределенная реализация - нет. Например, методы абстрактного класса OutputStream * write все задают throws IOException. ByteArrayOutputStream - это подкласс, который записывает данные в массив в памяти вместо истинного источника ввода-вывода. Его переопределенные методы write не могут вызывать IOException s, поэтому у них нет предложения throws, и вы можете вызывать их, не беспокоясь о требовании перехватить или указать.

За исключением не всегда. Предположим, что Widget имеет метод для сохранения его в поток:

public void writeTo(OutputStream out) throws IOException;

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

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
    someWidget.writeTo(out);
} catch (IOException e) {
    // can't happen (although we shouldn't ignore it if it does)
    throw new RuntimeException(e);
}

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

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

Это правило вызывает некоторые абсурдные проблемы. Например, один из трех write методов OutputStream является , а не переопределенным ByteArrayOutputStream. В частности, write(byte[] data) - это удобный метод, который записывает полный массив, вызывая write(byte[] data, int offset, int length) со смещением 0 и длиной массива. ByteArrayOutputStream переопределяет метод с тремя аргументами, но наследует метод удобства с одним аргументом как есть. Унаследованный метод делает все правильно, но он включает в себя нежелательное предложение throws. Возможно, это было упущением при проектировании ByteArrayOutputStream, но они никогда не смогут это исправить, потому что это нарушит совместимость исходного кода с любым кодом, который перехватывает исключение - исключение, которое никогда, никогда не будет и никогда не будет выдано!

Это правило раздражает и во время редактирования и отладки. Например, иногда я временно закомментирую вызов метода, и если бы он мог вызвать проверенное исключение, компилятор теперь будет жаловаться на существование локальных блоков try и catch. Поэтому я тоже должен это закомментировать, и теперь при редактировании кода внутри IDE будет отступать на неправильный уровень, потому что { и } закомментированы. Г! Это небольшая жалоба, но, похоже, единственное, что проверенные исключения когда-либо делают, это вызывает проблемы.


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

Вместо этого мы получаем следующую идиому, которая широко используется как способ закрыть компилятор:

try {
    ...
} catch (SomeStupidExceptionOmgWhoCares e) {
    e.printStackTrace();
}

В графическом интерфейсе или автоматизированной программе напечатанное сообщение не будет видно. Хуже того, он запускается вместе с остальной частью кода после исключения. Разве исключение не является ошибкой? Тогда не печатайте это. Иначе что-то еще может взорваться через мгновение, и к этому времени исходный объект исключения исчезнет. Эта идиома не лучше, чем BASIC On Error Resume Next или PHP error_reporting(0);.

Вызов какого-то класса логгеров не намного лучше:

try {
    ...
} catch (SomethingWeird e) {
    logger.log(e);
}

Это так же лениво, как и e.printStackTrace();, и все еще работает с кодом в неопределенном состоянии. Кроме того, выбор конкретной системы ведения журнала или другого обработчика зависит от приложения, поэтому это мешает повторному использованию кода.

Но подожди! Существует простой и универсальный способ найти обработчик для конкретного приложения. Он выше стека вызовов (или он установлен как обработчик необработанных исключений Thread * ). Так что в большинстве мест, все, что вам нужно сделать, это выбросить исключение выше в стек . Например, throw e;. Проверенные исключения только мешают.

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

23 голосов
/ 01 апреля 2009

SNR

Во-первых, проверенные исключения уменьшают «отношение сигнал / шум» для кода. Андерс Хейлсберг также говорит об императивном и декларативном программировании, которое является аналогичной концепцией. В любом случае рассмотрим следующие фрагменты кода:

Обновление пользовательского интерфейса из не-пользовательского потока в Java:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

Обновление пользовательского интерфейса из не-пользовательского потока в C #:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

Что мне кажется намного понятнее. Когда вы начинаете все больше и больше работать с пользовательским интерфейсом в Swing, проверенные исключения начинают становиться действительно раздражающими и бесполезными.

Побег из тюрьмы

Для реализации даже самых базовых реализаций, таких как интерфейс List в Java, проверенные исключения как инструмент для проектирования по контракту не работают. Рассмотрим список, который поддерживается базой данных, файловой системой или любой другой реализацией, которая выдает проверенное исключение. Единственная возможная реализация - перехватить проверенное исключение и повторно выбросить его как исключение без проверки:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

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

Заключение

  • Отлов исключений отличается от их обработки.
  • Проверенные исключения добавляют шум к коду.
  • Обработка исключений хорошо работает в C # без них.

Я уже писал об этом ранее .

22 голосов
/ 05 марта 2009

Artima опубликовал интервью с одним из архитекторов .NET Андерсом Хейлсбергом, в котором подробно освещены аргументы против проверенных исключений. Короткий дегустатор:

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

20 голосов
/ 05 марта 2009

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

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

И причина в том, что, если это так, я должен это исправить, и я должен это исправить сразу же. Я хочу сразу же узнать, что есть проблема.

Сколько раз вы фактически обрабатываете исключения? Я не говорю о ловле исключений - я говорю об обработке их? Слишком легко написать следующее:

try {
  thirdPartyMethod();
} catch(TPException e) {
  // this should never happen
}

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

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

20 голосов
/ 05 марта 2009

Статья Эффективные исключения Java хорошо объясняет, когда использовать непроверенные и когда использовать отмеченные исключения. Вот некоторые цитаты из этой статьи, чтобы выделить основные моменты:

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

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

(ТАК не допускает таблицы, поэтому вы можете прочитать следующее с исходной страницы ...)

Непредвиденные

  • Считается, что: часть дизайна
  • Ожидается, что произойдет: регулярно, но редко
  • Кого это волнует: вышестоящий код, который вызывает метод
  • Примеры: альтернативные режимы возврата
  • Best Mapping: проверенное исключение

Fault

  • Считается: неприятным сюрпризом
  • Ожидается, что произойдет: Никогда
  • Кого это волнует: людей, которым нужно решить проблему
  • Примеры: ошибки программирования, аппаратные сбои, ошибки конфигурации, отсутствующие файлы, недоступные серверы
  • Best Mapping: непроверенное исключение
19 голосов
/ 05 марта 2009

Короче говоря:

Исключениями являются вопросы разработки API. - Не больше, не меньше.

Аргумент для проверенных исключений:

Чтобы понять, почему проверенные исключения могут быть не очень хорошими, давайте перевернем вопрос и спросим: когда или почему проверенные исключения привлекательны, то есть, почему вы хотите, чтобы компилятор принудительно ввел объявление исключений?

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

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

В действительности, слишком часто пакет com.acme выбрасывает AcmeException, а не конкретные подклассы. Затем вызывающим абонентам необходимо обрабатывать, объявлять или повторно сигнализировать AcmeExceptions, но все еще не может быть уверенным, произошло ли AcmeFileNotFoundError или AcmePermissionDeniedError.

Так что, если вас интересует только AcmeFileNotFoundError, решение состоит в том, чтобы подать запрос функции программистам ACME и сказать им, чтобы они реализовали, объявили и документировали этот подкласс AcmeException.

Так зачем?

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

В результате языки без проверенных исключений обычно не намного хуже. Программисты могут испытывать искушение выбросить неопределенные экземпляры общего Error класса, а не AcmeException, но если они вообще заботятся о качестве своего API, они все равно научатся вводить AcmeFileNotFoundError.

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

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

Стоит также отметить, что Java VM не проверила исключения - их проверяет только компилятор Java, а файлы классов с измененными объявлениями исключений совместимы во время выполнения. Безопасность Java VM не улучшена проверенными исключениями, только стиль кодирования.

...