Глава 11. Исключения
Оглавление
Когда программа нарушает семантические ограничения языка программирования Java, виртуальная машина Java сигнализирует об этой ошибке программе как об исключении.
Пример такого нарушения — попытка индексировать массив за пределами его границ. Некоторые языки программирования и их реализации реагируют на такие ошибки немедленным завершением программы; другие языки программирования позволяют реализации реагировать произвольным или непредсказуемым способом. Ни один из этих подходов не совместим с целями разработки платформы Java SE: обеспечивать переносимость и надёжность.
Вместо этого язык программирования Java предусматривает, что исключение будет сгенерировано при нарушении семантических ограничений и вызовет нелокальное изменение управления от точки возникновения исключения к точке, которую может указать программист.
Исключение считается сгенерированным из точки его возникновения и обработанным в точке, в которую происходит передача управления.
Программы также могут явно генерировать исключения, используя throw операторы (§14.18).
Явное использование throw операторов предоставляет альтернативу устаревшему стилю обработки ошибок путём возвращения необычных значений, таких как целое значение -1, где отрицательное значение обычно не ожидается. Опыт показывает, что такие необычные значения часто игнорируются или не проверяются вызывающими процедурами, что приводит к программам, которые не являются надёжными, проявляют нежелательное поведение или то и другое.
Каждое исключение представляется экземпляром класса Throwable или одного из его подклассов (§11.1). Такой объект может использоваться для переноса информации от точки возникновения исключения к обработчику, который его перехватывает. Обработчики устанавливаются с помощью catch блоков try операторов (§14.20).
В процессе генерации исключения виртуальная машина Java прерывает, последовательно, любое выражение, оператор, вызов метода или конструктора, инициализаторы и выражения инициализации поля, которые начаты, но не завершены в текущей потоке. Этот процесс продолжается до тех пор, пока не будет найден обработчик, который обрабатывает это конкретное исключение, указав класс исключения или его суперкласс (§11.2). Если такой обработчик не найден, то исключение может быть обработано одним из иерархии обработчиков необработанных исключений (§11.3) — таким образом, предпринимаются все усилия, чтобы исключение не осталось необработанным.
Механизм исключений платформы Java SE интегрирован с её моделью синхронизации (§17.1), так что мониторы разблокируются при завершении synchronized операторов (§14.19) и вызовах synchronized методов (§8.4.3.6, §15.12).
Исключение представляется экземпляром класса Throwable (прямого подкласса Object) или одного из его подклассов.
Throwable и все его подклассы вместе образуют классы исключений.
Классы Exception и Error являются прямыми подклассами Throwable:
-
Exceptionявляется суперклассом всех исключений, из которых обычные программы могут захотеть восстановиться.Класс
RuntimeExceptionявляется прямым подклассомException.RuntimeExceptionявляется суперклассом всех исключений, которые могут быть выброшены по многим причинам во время вычисления выражений, но из которых всё ещё возможно восстановление.RuntimeExceptionи все его подклассы вместе образуют классы исключений времени выполнения. -
Errorявляется суперклассом всех исключений, из которых обычные программы обычно не ожидают восстановиться.Errorи все его подклассы вместе образуют классы ошибок.
Классы исключений времени выполнения — это классы исключений времени выполнения и классы ошибок.
Классы проверяемых исключений — это все классы исключений, кроме классов исключений времени выполнения. То есть, классы проверяемых исключений — это Throwable и все его подклассы, кроме RuntimeException и его подклассов, и Error и его подклассов.
Программы могут использовать существующие классы исключений API платформы Java SE в операторах throw или определять дополнительные классы исключений как подклассы Throwable или любого из его подклассов, как необходимо. Чтобы воспользоваться проверкой на время компиляции обработчиков исключений (§11.2), обычно большинство новых классов исключений определяются как классы проверяемых исключений, то есть, как подклассы Exception, которые не являются подклассами RuntimeException.
Класс Error является отдельным подклассом Throwable, отличным от Exception в иерархии классов, чтобы позволить программам использовать выражение "} catch (Exception e)
{" (§11.2.3) для перехвата всех исключений, из которых возможно восстановление, не перехватывая ошибки, из которых обычно восстановление невозможно.
Обратите внимание, что подкласс Throwable не может быть обобщенным (§8.1.2).
Исключение выбрасывается по одной из трёх причин:
-
был выполнен оператор
throw(§14.18). -
синхронно была обнаружена аномальная ситуация выполнения виртуальной машиной Java, а именно:
-
вычисление выражения нарушает стандартную семантику языка программирования Java (§15.6), например, целочисленное деление на ноль.
-
произошла ошибка при загрузке, линковке или инициализации части программы (§12.2, §12.3, §12.4); в этом случае выбрасывается экземпляр подкласса
LinkageError. -
внутренняя ошибка или ограничение ресурсов не позволяют виртуальной машине Java реализовать семантику языка программирования Java; в этом случае выбрасывается экземпляр подкласса
VirtualMachineError.
Эти исключения не выбрасываются в произвольной точке программы, а скорее в точке, где они указаны как возможный результат вычисления выражения или выполнения оператора.
-
-
произошло асинхронное исключение (§11.1.3).
Большинство исключений возникают синхронно в результате действия потока, в котором они возникают, и в точке программы, которая может привести к такому исключению. Асинхронное исключение, напротив, — это исключение, которое потенциально может возникнуть в любой точке выполнения программы.
Асинхронные исключения возникают только в результате:
-
Вызова (устаревшего) метода
stopклассаThreadилиThreadGroup.(Устаревшие) методы
stopмогут вызываться одним потоком для воздействия на другой поток или на все потоки в указанной группе потоков. Они являются асинхронными, потому что могут возникнуть в любой точке выполнения другого потока или потоков. -
Внутренняя ошибка или ограничение ресурсов в виртуальной машине Java, которые препятствуют её реализации семантики языка программирования Java. В этом случае асинхронное исключение, которое выбрасывается, является экземпляром подкласса
VirtualMachineError.Обратите внимание, что
StackOverflowError, подклассVirtualMachineError, может быть выброшен синхронно вызовом метода (§15.12.4.5), а также асинхронно из-за выполнения методаnativeили ограничений ресурсов виртуальной машины Java. Аналогично,OutOfMemoryError, другой подклассVirtualMachineError, может быть выброшен синхронно во время создания экземпляра класса (§15.9.4, §12.5), создания массива (§15.10.2, §10.6), инициализации класса (§12.4.2) и преобразования к ящику (§5.1.7), а также асинхронно.
Платформа Java SE допускает небольшое, но ограниченное выполнение кода перед выбросом асинхронного исключения.
Асинхронные исключения редки, но правильное понимание их семантики необходимо, если требуется генерировать высококачественный машинный код.
Указанная выше задержка разрешена, чтобы оптимизированный код мог обнаружить и выбросить эти исключения в точках, где их можно практически обработать, соблюдая семантику языка программирования Java. Простая реализация может проверять наличие асинхронных исключений в точке каждого перехода управления. Поскольку программа имеет конечный размер, это устанавливает ограничение на общую задержку обнаружения асинхронного исключения. Поскольку асинхронное исключение не произойдёт между переходами управления, генератор кода имеет некоторую гибкость для переупорядочения вычислений между переходами управления для повышения производительности. Статья Polling Efficiently on Stock Hardware Марка Феле, Proc. 1993 Conference on Functional Programming and Computer Architecture, Копенгаген, Дания, стр. 179-187, рекомендуются для дальнейшего чтения.
Язык программирования Java требует, чтобы программа содержала обработчики исключений, требующих обработки, которые могут возникнуть в результате выполнения метода или конструктора (§8.4.6, §8.8.5). Эта проверка на этапе компиляции наличия обработчиков исключений предназначена для сокращения количества исключений, которые не обрабатываются должным образом. Для каждого исключения, требующего обработки, которое является возможным результатом, в части throws метода или конструктора необходимо указать класс этого исключения или один из его суперклассов (§11.2.3).
Классы исключений, требующих обработки (§11.1.1), указанные в части throws, являются частью соглашения между разработчиком и пользователем метода или конструктора. Часть throws переопределяемого метода не может указывать, что этот метод приведет к выбрасыванию любого исключения, требующего обработки, которое переопределяемый метод не разрешено, согласно своей части throws, выбрасывать (§8.4.8.3). Когда используются интерфейсы, несколько объявлений методов могут быть переопределены одним объявлением переопределения. В этом случае объявление переопределения должно иметь часть throws, совместимую со всеми переопределяемыми объявлениями (§9.4.1).
Классы исключений, не требующих обработки (§11.1.1), исключаются из проверки на этапе компиляции.
Классы ошибок исключаются, потому что они могут возникать во многих местах программы, и восстановление после них затруднительно или невозможно. Программа, объявляющая такие исключения, была бы перегруженной и бесполезной. Тем не менее, сложные программы могут захотеть перехватить и попытаться восстановиться от некоторых из этих условий.
Классы исключений времени выполнения исключаются, потому что, по мнению разработчиков языка программирования Java, объявление таких исключений не будет существенно способствовать установлению корректности программ. Многие операции и конструкции языка программирования Java могут привести к исключениям во время выполнения. Информация, доступная компилятору Java, и уровень анализа, который выполняет компилятор, обычно недостаточны для установления того, что такие исключения во время выполнения не могут возникнуть, даже если это очевидно для программиста. Требование объявлять такие классы исключений просто затруднит работу программистов.
Например, некоторый код может реализовывать циклическую структуру данных, которая по построению никогда не включает ссылки null; программист может быть уверен, что NullPointerException не может возникнуть, но компилятору Java будет сложно это доказать. Технология теоретического доказательства, необходимая для установления таких глобальных свойств структур данных, выходит за рамки данного спецификации.
Мы говорим, что оператор или выражение может сгенерировать класс исключения E, если, согласно правилам в §11.2.1 и §11.2.2, выполнение оператора или выражения может привести к генерации исключения класса E.
Мы говорим, что часть catch может обработать свой(и) обрабатываемый(ые) класс(ы) исключения(й):
-
Обрабатываемый класс исключения одноэлементной части
catch— это объявленный тип его параметра исключения (§14.20). -
Обрабатываемые классы исключений многоэлементной части
catch— это альтернативы в объединении, обозначающем тип его параметра исключения.
Выражение создания экземпляра класса (§15.9) может сгенерировать класс исключения E, если выполняется одно из следующих условий:
-
Выражение является квалифицированным выражением создания экземпляра класса, и квалифицирующее выражение может сгенерировать E; или
-
Некоторые выражения из списка аргументов могут сгенерировать E; или
-
E — один из типов исключений типа вызова выбранного конструктора (§15.12.2.6); или
-
Выражение создания экземпляра класса включает ClassBody, и некоторые инициализаторы экземпляра или инициализаторы переменных экземпляра в ClassBody могут сгенерировать E.
Выражение вызова метода (§15.12) может сгенерировать класс исключения E, если выполняется одно из следующих условий:
-
Выражение вызова метода имеет вид Primary
.[TypeArguments] Identifier, и выражение Primary может сгенерировать E; или -
Некоторые выражения из списка аргументов могут сгенерировать E; или
-
E — один из типов исключений типа вызова выбранного метода (§15.12.2.6).
Выражение лямбда-выражения (§15.27) не может сгенерировать классы исключений.
Для всех других типов выражений, выражение может сгенерировать класс исключения E, если это может сделать одно из его непосредственных подвыражений.
Обратите внимание, что выражение ссылки на метод (§15.13) вида Primary :: [TypeArguments] Identifier может сгенерировать класс исключения, если подвыражение Primary может сгенерировать класс исключения. В отличие от этого, лямбда-выражение ничего не может сгенерировать, и не имеет непосредственных подвыражений для анализа исключений. Именно тело лямбда-выражения, содержащее выражения и операторы, может генерировать классы исключений.
Оператор throw (§14.18), выражение которого имеет статический тип E и не является окончательным или фактически окончательным параметром исключения, может сгенерировать исключение E или любой класс исключения, который может быть сгенерирован выражением.
Например, оператор throw new
java.io.FileNotFoundException(); может сгенерировать только java.io.FileNotFoundException. Формально, он не может сгенерировать подкласс или надкласс java.io.FileNotFoundException.
Оператор throw, выражение которого является окончательным или фактически окончательным параметром исключения в блоке обработки исключений C, может сгенерировать исключение класса E, если:
-
E — это класс исключения, который может быть сгенерирован блоком
tryоператораtry, который объявляет C; и -
E совместим по присваиванию с любым из классов исключений, которые можно поймать в C; и
-
E не совместим по присваиванию ни с одним из классов исключений, которые можно поймать в блоках обработки исключений, объявленных слева от C в том же операторе
try.
Оператор try (§14.20) может сгенерировать исключение класса E, если выполняется хотя бы одно из следующих условий:
-
Блок
tryможет сгенерировать E, или выражение, используемое для инициализации ресурса (в операторе с ресурсами), может сгенерировать E, или автоматическое вызов методаclose()ресурса (в операторе с ресурсами) может сгенерировать E, и E не совместим по присваиванию ни с одним из классов исключений, которые можно поймать в любом блоке обработки исключений оператораtry, и либо блокfinallyотсутствует, либо блокfinallyможет завершиться нормально; или -
Некоторые блоки
catchоператораtryмогут сгенерировать E, и либо блокfinallyотсутствует, либо блокfinallyможет завершиться нормально; или -
Наличествует блок
finallyи он может сгенерировать E.
Оператор явного вызова конструктора (§8.8.7.1) может сгенерировать исключение класса E, если выполняется хотя бы одно из следующих условий:
-
Некоторые выражения в списке параметров вызова конструктора могут сгенерировать E; или
-
E определён как класс исключения блока обработки исключений вызываемого конструктора (§15.12.2.6).
Любой другой оператор S может сгенерировать исключение класса E, если выражение или оператор, непосредственно содержащийся в S, может сгенерировать E.
Ошибка компиляции возникает, если тело метода или конструктора может сгенерировать некоторый класс исключения E, когда E — это проверяемое исключение, и E не является подклассом какого-либо класса, объявленного в блоке обработки исключений метода или конструктора.
Ошибка компиляции возникает, если тело лямбда-выражения может сгенерировать некоторый класс исключения E, когда E — это проверяемое исключение, и E не является подклассом какого-либо класса, объявленного в блоке обработки исключений типа функции, на который указывает лямбда-выражение.
Ошибка компиляции возникает, если инициализатор переменной класса (§8.3.2) или статический инициализатор (§8.7) именованного класса или интерфейса может сгенерировать проверяемое исключение.
Ошибка компиляции возникает, если инициализатор переменной экземпляра (§8.3.2) или инициализатор экземпляра (§8.6) именованного класса может сгенерировать проверяемое исключение, если этот именованный класс не имеет хотя бы одного явно объявленного конструктора, и класс исключения или один из его предков не объявлен явно в блоке обработки исключений каждого конструктора.
Обратите внимание, что ошибка компиляции не возникает, если инициализатор переменной экземпляра или инициализатор экземпляра анонимного класса (§15.9.5) может сгенерировать исключение. В именованном классе программист несёт ответственность за распространение информации о том, какие классы исключений могут быть сгенерированы инициализаторами, объявляя соответствующий блок обработки исключений в любом явном объявлении конструктора. Это соотношение между классами исключений, которые могут генерироваться инициализаторами класса, и классами исключений, объявленными в конструкторах класса, гарантируется для объявления анонимного класса, так как явные объявления конструкторов невозможны, и компилятор Java всегда генерирует конструктор со соответствующим блоком обработки исключений для объявления анонимного класса, основанный на классах исключений, которые могут быть сгенерированы его инициализаторами.
Ошибка компиляции возникает, если блок обработки исключений может поймать проверяемый класс исключения E1, и при этом блок кода, соответствующий этому блоку обработки исключений, не может сгенерировать проверяемый класс исключения, который является подклассом или надклассом E1, за исключением случаев, когда E1 является Exception или надклассом Exception.
Ошибка компиляции возникает, если блок обработки исключений может поймать класс исключения E1, и предыдущий блок обработки исключений в непосредственно содержащем операторе try может поймать E1 или его суперкласс.
Компилятор Java рекомендуется выводить предупреждение, если блок обработки исключений может поймать проверяемый класс исключения E1, а соответствующий блок кода может сгенерировать проверяемый класс исключения E2, где E2 <: E1, и предыдущий блок обработки исключений в непосредственно содержащем операторе try может поймать проверяемый класс исключения E3, где E2 <: E3 <: E1.
Пример 11.2.3-1. Обработка проверяемых исключений
import java.io.*;
class StaticallyThrownExceptionsIncludeSubtypes {
public static void main(String[] args) {
try {
throw new FileNotFoundException();
} catch (IOException ioe) {
// "catch IOException" catches IOException
// and any subtype.
}
try {
throw new FileNotFoundException();
// Statement "can throw" FileNotFoundException.
// It is not the case that statement "can throw"
// a subtype or supertype of FileNotFoundException.
} catch (FileNotFoundException fnfe) {
// ... Handle exception ...
} catch (IOException ioe) {
// Legal, but compilers are encouraged to give
// warnings as of Java SE 7, because all subtypes of
// IOException that the try block "can throw" have
// already been caught by the prior catch clause.
}
try {
m();
// m's declaration says "throws IOException", so
// m "can throw" IOException. It is not the case
// that m "can throw" a subtype or supertype of
// IOException (e.g. Exception).
} catch (FileNotFoundException fnfe) {
// Legal, because the dynamic type of the exception
// might be FileNotFoundException.
} catch (IOException ioe) {
// Legal, because the dynamic type of the exception
// might be a different subtype of IOException.
} catch (Throwable t) {
// Can always catch Throwable.
}
}
static void m() throws IOException {
throw new FileNotFoundException();
}
}
Согласно правилам выше, каждый вариант в операторе с несколькими catch (§14.20) должен уметь поймать какое-то исключение, сгенерированное блоком try и не пойманное предыдущими блоками обработки исключений. Например, второй блок обработки исключений ниже вызовет ошибку компиляции, потому что анализ исключений определит, что SubclassOfFoo уже пойман первым блоком обработки исключений:
try { ... }
catch (Foo f) { ... }
catch (Bar | SubclassOfFoo e) { ... }
Когда выбрасывается исключение (§14.18), управление передаётся из кода, вызвавшего исключение, к ближайшему динамически охватывающему catch блоку, если таковой имеется, оператора try (§14.20), который может обработать исключение.
Оператор или выражение динамически охватываются catch блоком, если он находится внутри try блока оператора try, частью которого является catch блок, или если вызывающий оператор или выражение динамически охватывается catch блоком.
Вызывающий оператор или выражение зависит от места его расположения:
-
Если внутри метода, то вызывающим выражением является выражение вызова метода (§15.12), которое было выполнено для вызова метода.
-
Если внутри конструктора, инициализатора экземпляра или инициализатора переменной экземпляра, то вызывающим выражением является выражение создания экземпляра класса (§15.9) или вызов метода
newInstance, который был выполнен для создания объекта. -
Если внутри статического инициализатора или инициализатора переменной
static, то вызывающим выражением является выражение, использующее класс или интерфейс для его инициализации (§12.4).
Возможность обработки исключения конкретным catch блоком определяется по сравнением класса брошенного объекта с классами перехватываемых исключений в catch блоке. catch блок может обработать исключение, если один из его классов перехватываемых исключений является классом исключения или суперклассом класса исключения.
Эквивалентно, catch блок перехватит любой объект исключения, который является instanceof (§15.20.2) одним из его классов перехватываемых исключений.
Передача управления, происходящая при выбрасывании исключения, вызывает прерывистое завершение выражений (§15.6) и операторов (§14.1), пока не встретится catch блок, который может обработать исключение; выполнение затем продолжается выполнением блока этого catch блока. Код, вызвавший исключение, никогда не возобновляется.
Все исключения (синхронные и асинхронные) являются точными: когда происходит передача управления, все эффекты выполненных операторов и выражений до точки выбрасывания исключения должны быть учтены. Никакие выражения, операторы или части их, которые встречаются после точки выбрасывания исключения, не должны быть вычислены.
Если оптимизированный код упреждающе выполнил некоторые выражения или операторы, которые следуют за точкой возникновения исключения, такой код должен быть готов скрыть это упреждающее выполнение от видимого пользователю состояния программы.
Если не удаётся найти catch блок, способный обработать исключение, то текущий поток (поток, встретивший исключение) завершается. Перед завершением выполняются все finally блоки, и необработанное исключение обрабатывается в соответствии со следующими правилами:
-
Если у текущего потока установлен обработчик необработанных исключений, то этот обработчик выполняется.
-
В противном случае, вызывается метод
uncaughtExceptionдляThreadGroup, являющегося родителем текущего потока. ЕслиThreadGroupи его родительскиеThreadGroupне переопределяютuncaughtException, то вызывается методuncaughtExceptionобработчика по умолчанию.
В ситуациях, когда желательно гарантировать, что один блок кода всегда будет выполнен после другого, даже если этот другой блок завершается прерывисто, можно использовать оператор try с finally блоком (§14.20.2).
Если try или catch блок в операторе try-finally или try-catch-finally завершается прерывисто, то finally блок выполняется во время распространения исключения, даже если в конечном итоге не будет найдено соответствующего catch блока.
Если finally блок выполняется из-за прерывистого завершения try блока и сам finally блок завершается прерывисто, то причина прерывистого завершения try блока отбрасывается, и новая причина прерывистого завершения передаётся оттуда.
Точные правила для прерывистого завершения и перехвата исключений подробно описаны в спецификации каждого оператора в §14 (Блоки и операторы) и для выражений в §15 (Выражения) (особенно §15.6).
Пример 11.3-1. Выбрасывание и перехват исключений
Следующая программа объявляет класс исключений TestException. Метод main класса Test вызывает метод thrower четыре раза, вызывая исключения три из четырёх раз. Оператор try в методе main перехватывает каждое исключение, которое выбрасывает метод. Независимо от того, завершается ли вызов thrower нормально или прерывисто, печатается сообщение, описывающее произошедшее.
class TestException extends Exception {
TestException() { super(); }
TestException(String s) { super(s); }
}
class Test {
public static void main(String[] args) {
for (String arg : args) {
try {
thrower(arg);
System.out.println("Test \"" + arg +
"\" didn't throw an exception");
} catch (Exception e) {
System.out.println("Test \"" + arg +
"\" threw a " + e.getClass() +
"\n with message: " +
e.getMessage());
}
}
}
static int thrower(String s) throws TestException {
try {
if (s.equals("divide")) {
int i = 0;
return i/i;
}
if (s.equals("null")) {
s = null;
return s.length();
}
if (s.equals("test")) {
throw new TestException("Test message");
}
return 0;
} finally {
System.out.println("[thrower(\"" + s + "\") done]");
}
}
}
Если мы выполним программу, передав ей аргументы:
divide null not test
она выведет результат:
[thrower("divide") done]
Test "divide" threw a class java.lang.ArithmeticException
with message: / by zero
[thrower("null") done]
Test "null" threw a class java.lang.NullPointerException
with message: null
[thrower("not") done]
Test "not" didn't throw an exception
[thrower("test") done]
Test "test" threw a class TestException
with message: Test message
Объявление метода thrower должно содержать throws блок, потому что он может выбрасывать экземпляры TestException, который является классом проверяемых исключений (§11.1.1). Если throws блок будет опущен, возникнет ошибка компиляции.
Обратите внимание, что finally блок выполняется при каждом вызове thrower, независимо от того, происходит ли исключение, как показано выводом «[thrower(...)
done]», который появляется для каждого вызова.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.