Выбрать главу

Секция 3 показывает, как поступать с объектами, при конструировании ко¬торых возможны сбои и которые нуждаются в завершении. Здесь программа усложняется, потому что каждое конструирование должно заключаться в от¬дельную копию try-catch и за ним должна следовать конструкция try-finally, обеспечивающая завершение.

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

Идентификация исключений

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

Идентификация исключений не требует обязательного соответствия между исключением и обработчиком. Объект порожденного класса подойдет и для об¬работчика, изначально написанного для базового класса:

//: exceptions/Human.java // Перехват иерархии исключений.

class Annoyance extends Exception {} class Sneeze extends Annoyance {}

public class Human {

public static void main(String[] args) { // Перехват точного типа try {

throw new SneezeO; } catch(Sneeze s) {

System out println("Перехвачено Sneeze"). } catch(Annoyance a) {

System 0ut.println("nepexBa4eH0 Annoyance"),

}

// Перехват базового типа try {

throw new SneezeO. } catch(Annoyance a) {

System out рпп^пС'Перехвачено Annoyance").

}

}

Перехвачено Sneeze Перехвачено Annoyance *///•-

Исключение Sneeze будет перехвачено в первом блоке catch, который ему со¬ответствует — конечно, это будет первый блок. Но, если удалить первый блок catch, оставив только проверку Annoyance, программа все равно работает, пото¬му что она перехватывает базовый класс Sneeze. Другими словами, блок catch (Annoyance а) поймает Annoyance или любой другой класс, унаследованный от не¬го. Если вы добавите новые производные исключения в свой метод, программа пользователя этого метода не потребует изменений, так как клиент перехваты¬вает исключения базового класса.

Если вы попытаетесь «замаскировать» исключения производного класса, по¬местив сначала блок catch базового класса:

try {

throw new SneezeO;

} catch(Annoyance a) { // ..

} catch(Sneeze s) { II...

}

компилятор выдаст сообщение об ошибке, так как он видит, что блок catch для исключения Sneeze никогда не выполнится.

Альтернативные решения

Система обработки исключений представляет собой «черный ход», позволяю¬щий программе нарушить нормальную последовательность выполнения ко¬манд. «Черный ход» открывается при возникновении «исключительных ситуа¬ций», когда обычная работа далее невозможна или нежелательна. Исключения представляют собой условия, с которыми текущий метод справиться не в со¬стоянии. Причина, по которой возникают системы обработки исключений, кро¬ется в том, что программисты не желали иметь дела с громоздкой проверкой всех возможных условий возникновения ошибок каждой функции. В результа¬те ошибки ими просто игнорировались. Стоит отметить, что вопрос удобства программиста при обработке ошибок стоял на первом месте для разработчиков Java.

Основное правило при использовании исключений гласит: «Не обрабаты¬вайте исключение, если вы не знаете, что с ним делать». По сути, отделение кода, ответственного за обработку ошибок, от места, где ошибка возникает, яв¬ляется одной из главных целей обработки исключений. Это позволяет вам скон¬центрироваться на том, что вы хотите сделать в одном фрагменте кода, и на том, как вы собираетесь поступить с ошибками в совершенно другом месте програм¬мы. В результате основной код не перемежается с логикой обработки ошибок, что упрощает его сопровождение и понимание. Исключения также сокращают объем кода, так как один обработчик может обслуживать несколько потенци¬альных источников ошибок.

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

try {

// ^ делает что-то полезное

} са^И(6бязывающееИсключение е) {} // Проглотили!

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

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

Несмотря на кажущуюся простоту, проблема не только очень сложна, но и к тому же неоднозначна. Существуют твердые приверженцы обеих точек зрения, которые считают, что верный ответ (их) очевиден и просто бросается в глаза. Вероятно, одна из точек зрения основана на несомненных преимущест¬вах перехода от слабо типизированного языка (например, С до выхода стандар¬та ANSI) к языку с строгой статической проверкой типов (то есть с проверкой во время компиляции), подобному С++ или Java. Преимущества такого перехо¬да настолько очевидны, что строгая статическая проверка типов кажется пана¬цеей от всех бед. Я надеюсь поставить под вопрос ту небольшую часть моей эво¬люции, отличающуюся абсолютной верой в строгую статическую проверку типов: без сомнения, большую часть времени она приносит пользу, но сущест¬вует неформальная граница, за которой такая проверка становится препятстви¬ем на вашем пути (одна из моих любимых цитат такова: «Все модели неверны, но некоторые полезны»).

Предыстория

Обработка исключений зародилась в таких системах, как PL/1 и Mesa, а затем мигрировала в CLU, Smalltalk, Modula-3, Ada, Eiffel, С++, Python, Java и в поя¬вившиеся после Java языки Ruby и С#. Конструкции Java сходны с конст¬рукциями С++, кроме тех аспектов, в которых решения С++ приводили к проблемам.

Обработка исключений была добавлена в С++ на довольно позднем этапе стандартизации. Модель исключений в С++ в основном была заимствована из CLU. Впрочем, в то время существовали и другие языки с поддержкой обра¬ботки исключений: Ada, Smalltalk (в обоих были исключения, но отсутствовали их спецификации) и Modula-З (в котором существовали и исключения, и их спецификации).

Следуя подходу CLU при разработке исключений С++, Страуструп считал, что основной целью является сокращение объема кода восстановления после ошибки. Вероятно, он видел немало программистов, которые не писали код об¬работки ошибок на С, поскольку объем этого кода был устрашающим, а разме¬щение выглядело нелогично. В результате все происходило в стиле С: ошибки в коде игнорировались, а с проблемами справлялись при помощи отладчиков. Чтобы исключения реально заработали, С-программисты должны были писать «лишний» код, без которого они обычно обходились. Таким образом, объем но¬вого кода не должен быть чрезмерным. Важно помнить об этих целях, говоря об эффективности контролируемых исключений в Java.

С++ добавил к идее CLU дополнительную возможность: спецификации ис¬ключений, то есть включение в сигнатуру метода информации об исключениях, возникающих при вызове. В действительности спецификация исключения не¬сет двойной смысл. Она означает: «Я возбуждаю это исключение в коде, а вы его обрабатываете». Но она также может означать: «Я игнорирую исключение, которое может возникнуть в моем коде; обеспечьте его обработку». При осве¬щении механизмов исключений мы концентрировались на «обеспечении обра¬ботки», но здесь мне хотелось бы поближе рассмотреть тот факт, что зачастую исключения игнорируются, и именно этот факт может быть отражен в специ¬фикации исключения.