Философия Java3
Шрифт:
Секция 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.