Обработка ошибок и исключения
2. Коды возврата. Основная идея — в случае ошибки возвращать специальное значение, которое не может быть корректным. Например, если в методе есть операция деления, то придется проверять делитель на равенство нулю. Также проверим корректность аргументов a и b :
Double f(Double a, Double b) < if ((a == null) || (b == null)) < return null; > //. if (Math.abs(b) < EPS) < return null; > else < return a / b; > >
При вызове метода необходимо проверить возвращаемое значение:
Double d = f(a, b); if (d != null) < //. > else < //. >
Минусом такого подхода является необходимость проверки возвращаемого значения каждый раз при вызове метода. Кроме того, не всегда возможно определить тип ошибки.
3.Использовать флаг ошибки: при возникновении ошибки устанавливать флаг в соответствующее значение:
boolean error; Double f(Double a, Double b) < if ((a == null) || (b == null)) < error = true; return null; > //. if (Math.abs(b) < EPS) < error = true; return b; > else < return a / b; > >
error = false; Double d = f(a, b); if (error) < //. > else < //. >
Минусы такого подхода аналогичны минусам использования кодов возврата.
4.Можно вызвать метод обработки ошибки и возвращать то, что вернет этот метод.
Double f(Double a, Double b) < if ((a == null) || (b == null)) < return nullPointer(); > //. if (Math.abs(b) < EPS) < return divisionByZero(); > else < return a / b; > >
Но в таком случае не всегда возможно проверить корректность результата вызова основного метода.
5.В случае ошибки просто закрыть программу.
if (Math.abs(b) < EPS) < System.exit(0); return this; >
Это приведет к потере данных, также невозможно понять, в каком месте возникла ошибка.
Исключения
В Java возможна обработка ошибок с помощью исключений:
Double f(Double a, Double b) < if ((a == null) || (b == null)) < throw new IllegalArgumentException("arguments of f() are null"); > //. return a / b; >
Проверять b на равенство нулю уже нет необходимости, так как при делении на ноль метод бросит непроверяемое исключение ArithmeticException .
- разделить обработку ошибок и сам алгоритм;
- не загромождать код проверками возвращаемых значений;
- обрабатывать ошибки на верхних уровнях, если на текущем уровне не хватает данных для обработки. Например, при написании универсального метода чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей метод программы;
- классифицировать типы ошибок, обрабатывать похожие исключения одинаково, сопоставлять специфичным исключениям определенные обработчики.
Каждый раз, когда при выполнении программы происходит ошибка, создается объект-исключение, содержащий информацию об ошибке, включая её тип и состояние программы на момент возникновения ошибки. После создания исключения среда выполнения пытается найти в стеке вызовов метод, который содержит код, обрабатывающий это исключение. Поиск начинается с метода, в котором произошла ошибка, и проходит через стек в обратном порядке вызова методов. Если не было найдено ни одного подходящего обработчика, выполнение программы завершается.
Таким образом, механизм обработки исключений содержит следующие операции:
- Создание объекта-исключения.
- Заполнение stack trace’а этого исключения.
- Stack unwinding (раскрутка стека) в поисках нужного обработчика.
Классификация исключений
Класс Java Throwable описывает все, что может быть брошено как исключение. Наследеники Throwable — Exception и Error — основные типы исключений. Также RuntimeException , унаследованный от Exception , является существенным классом.
Иерархия стандартных исключений
Проверяемые исключения
Наследники класса Exception (кроме наслеников RuntimeException ) являются проверяемыми исключениями(checked exception). Как правило, это ошибки, возникшие по вине внешних обстоятельств или пользователя приложения – неправильно указали имя файла, например. Эти исключения должны обрабатываться в ходе работы программы, поэтому компилятор проверяет наличие обработчика или явного описания тех типов исключений, которые могут быть сгенерированы некоторым методом.
Все исключения, кроме классов Error и RuntimeException и их наследников, являются проверяемыми.
Error
Класс Error и его подклассы предназначены для системных ошибок. Свои собственные классы-наследники для Error писать (за очень редкими исключениями) не нужно. Как правило, это действительно фатальные ошибки, пытаться обработать которые довольно бессмысленно (например OutOfMemoryError ).
RuntimeException
Эти исключения обычно возникают в результате ошибок программирования, такие как ошибки разработчика или неверное использование интерфейса приложения. Например, в случае выхода за границы массива метод бросит OutOfBoundsException . Такие ошибки могут быть в любом месте программы, поэтому компилятор не требует указывать runtime исключения в объявлении метода. Теоретически приложение может поймать это исключение, но разумнее исправить ошибку.
Обработка исключений
Чтобы сгенерировать исключение используется ключевое слово throw . Как и любой объект в Java, исключения создаются с помощью new .
if (t == null) < throw new NullPointerException("t = null"); >
Есть два стандартных конструктора для всех исключений: первый — конструктор по умолчанию, второй принимает строковый аргумент, поэтому можно поместить подходящую информацию в исключение.
Возможна ситуация, когда одно исключение становится причиной другого. Для этого существует механизм exception chaining. Практически у каждого класса исключения есть конструктор, принимающий в качестве параметра Throwable – причину исключительной ситуации. Если же такого конструктора нет, то у Throwable есть метод initCause(Throwable) , который можно вызвать один раз, и передать ему исключение-причину.
Как и было сказано раньше, определение метода должно содержать список всех проверяемых исключений, которые метод может бросить. Также можно написать более общий класс, среди наследников которого есть эти исключения.
void f() throws InterruptedException, IOException < //.
try-catch-finally
Код, который может бросить исключения оборачивается в try -блок, после которого идут блоки catch и finally (Один из них может быть опущен).
try < // Код, который может сгенерировать исключение >
Сразу после блока проверки следуют обработчики исключений, которые объявляются ключевым словом catch.
try < // Код, который может сгенерировать исключение > catch(Type1 id1) < // Обработка исключения Type1 > catch(Type2 id2) < // Обработка исключения Type2 >
Сatch -блоки обрабатывают исключения, указанные в качестве аргумента. Тип аргумента должен быть классом, унаследованного от Throwable , или самим Throwable . Блок catch выполняется, если тип брошенного исключения является наследником типа аргумента и если это исключение не было обработано предыдущими блоками.
Код из блока finally выполнится в любом случае: при нормальном выходе из try , после обработки исключения или при выходе по команде return .
NB: Если JVM выйдет во время выполнения кода из try или catch , то finally -блок может не выполниться. Также, например, если поток выполняющий try или catch код остановлен, то блок finally может не выполниться, даже если приложение продолжает работать.
Блок finally удобен для закрытия файлов и освобождения любых других ресурсов. Код в блоке finally должен быть максимально простым. Если внутри блока finally будет брошено какое-либо исключение или просто встретится оператор return , брошенное в блоке try исключение (если таковое было брошено) будет забыто.
import java.io.IOException; public class ExceptionTest < public static void main(String[] args) < try < try < throw new Exception("a"); > finally < throw new IOException("b"); > > catch (IOException ex) < System.err.println(ex.getMessage()); > catch (Exception ex) < System.err.println(ex.getMessage()); > > >
После того, как было брошено первое исключение — new Exception(«a») — будет выполнен блок finally , в котором будет брошено исключение new IOException(«b») , именно оно будет поймано и обработано. Результатом его выполнения будет вывод в консоль b . Исходное исключение теряется.
Обработка исключений, вызвавших завершение потока
При использовании нескольких потоков бывают ситуации, когда поток завершается из-за исключения. Для того, чтобы определить с каким именно, начиная с версии Java 5 существует интерфейс Thread.UncaughtExceptionHandler . Его реализацию можно установить нужному потоку с помощью метода setUncaughtExceptionHandler . Можно также установить обработчик по умолчанию с помощью статического метода Thread.setDefaultUncaughtExceptionHandler .
Интерфейс Thread.UncaughtExceptionHandler имеет единственный метод uncaughtException(Thread t, Throwable e) , в который передается экземпляр потока, завершившегося исключением, и экземпляр самого исключения. Когда поток завершается из-за непойманного исключения, JVM запрашивает у потока UncaughtExceptionHandler , используя метод Thread.getUncaughtExceptionHandler() , и вызвает метод обработчика – uncaughtException(Thread t, Throwable e) . Все исключения, брошенные этим методом, игнорируются JVM.
Информация об исключениях
- getMessage() . Этот метод возвращает строку, которая была первым параметром при создании исключения;
- getCause() возвращает исключение, которое стало причиной текущего исключения;
- printStackTrace() печатает stack trace, который содержит информацию, с помощью которой можно определить причину исключения и место, где оно было брошено.
Exception in thread "main" java.lang.IllegalStateException: A book has a null property at com.example.myproject.Author.getBookIds(Author.java:38) at com.example.myproject.Bootstrap.main(Bootstrap.java:14) Caused by: java.lang.NullPointerException at com.example.myproject.Book.getId(Book.java:22) at com.example.myproject.Author.getBookIds(Author.java:35)
Все методы выводятся в обратном порядке вызовов. В примере исключение IllegalStateException было брошено в методе getBookIds , который был вызван в main . «Caused by» означает, что исключение NullPointerException является причиной IllegalStateException .
Разработка исключений
Чтобы определить собственное проверяемое исключение, необходимо создать наследника класса java.lang.Exception . Желательно, чтобы у исключения был конструкор, которому можно передать сообщение:
public class FooException extends Exception < public FooException() < super(); > public FooException(String message) < super(message); > public FooException(String message, Throwable cause) < super(message, cause); > public FooException(Throwable cause) < super(cause); > >
Исключения в Java7
- обработка нескольких типов исключений в одном catch -блоке:
catch
(IOException | SQLException ex)
В таких случаях параметры неявно являются final , поэтому нельзя присвоить им другое значение в блоке catch .
Байт-код, сгенерированный компиляцией такого catch -блока будет короче, чем код нескольких catch -блоков.
- Try с ресурсами позволяет прямо в try -блоке объявлять необходимые ресурсы, которые по завершению блока будут корректно закрыты (с помощью метода close() ). Любой объект реализующий java.lang.AutoCloseable может быть использован как ресурс.
static String readFirstLineFromFile(String path) throws IOException < try (BufferedReader br = new BufferedReader(new FileReader(path))) < return br.readLine(); > >
В приведенном примере в качестве ресурса использутся объект класса BufferedReader , который будет закрыт вне зависимосити от того, как выполнится try -блок.
Можно объявлять несколько ресурсов, разделяя их точкой с запятой:
public static void viewTable(Connection con) throws SQLException < String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES"; try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query)) < //Work with Statement and ResultSet > catch (SQLException e) < e.printStackTrace; >>
Во время закрытия ресурсов тоже может быть брошено исключение. В try-with-resources добавленна возможность хранения «подавленных» исключений, и брошенное try -блоком исключение имеет больший приоритет, чем исключения получившиеся во время закрытия. Получить последние можно вызовом метода getSuppressed() от исключения брошенного try -блоком.
- Перебрасывание исключений с улучшенной проверкой соответствия типов.
Компилятор Java SE 7 тщательнее анализирует перебрасываемые исключения. Рассмотрим следующий пример:
static class FirstException extends Exception < >static class SecondException extends Exception < >public void rethrowException(String exceptionName) throws Exception < try < if ("First".equals(exceptionName)) < throw new FirstException(); > else < throw new SecondException(); > > catch (Exception ex) < throw e; > >
В примере try -блок может бросить либо FirstException , либо SecondException . В версиях до Java SE 7 невозможно указать эти исключения в декларации метода, потому что catch -блок перебрасывает исключение ex , тип которого — Exception .
В Java SE 7 вы можете указать, что метод rethrowException бросает только FirstException и SecondException . Компилятор определит, что исключение Exception ex могло возникнуть только в try -блоке, в котором может быть брошено FirstException или SecondException . Даже если тип параметра catch — Exception , компилятор определит, что это экземпляр либо FirstException , либо SecondException :
public void rethrowException(String exceptionName) throws FirstException, SecondException < try < // . > catch (Exception e) < throw e; > >
Если FirstException и SecondException не являются наследниками Exception , то необходимо указать и Exception в объявлении метода.
Примеры исключений
- любая операция может бросить VirtualMachineError . Как правило это происходит в результате системных сбоев.
- OutOfMemoryError . Приложение может бросить это исключение, если, например, не хватает места в куче, или не хватает памяти для того, чтобы создать стек нового потока.
- IllegalArgumentException используется для того, чтобы избежать передачи некорректных значений аргументов. Например:
public void f(Object a) < if (a == null) < throw new IllegalArgumentException("a must not be null"); > >
- IllegalStateException возникает в результате некорректного состояния объекта. Например, использование объекта перед тем как он будет инициализирован.
Гарантии безопасности
При возникновении исключительной ситуации, состояния объектов и программы могут удовлетворять некоторым условиям, которые определяются различными типами гарантий безопасности:
- Отсутствие гарантий (no exceptional safety). Если было брошено исключение, то не гарантируется, что все ресурсы будут корректно закрыты и что объекты, методы которых бросили исключения, могут в дальнейшем использоваться. Пользователю придется пересоздавать все необходимые объекты и он не может быть уверен в том, что может переиспозовать те же самые ресурсы.
- Отсутствие утечек (no-leak guarantee). Объект, даже если какой-нибудь его метод бросает исключение, освобождает все ресурсы или предоставляет способ сделать это.
- Слабые гарантии (weak exceptional safety). Если объект бросил исключение, то он находится в корректном состоянии, и все инварианты сохранены. Рассмотрим пример:
class Interval < //invariant: left double left; double right; //. >
Если будет брошено исключение в этом классе, то тогда гарантируется, что ивариант «левая граница интервала меньше правой» сохранится, но значения left и right могли измениться.
- Сильные гарантии (strong exceptional safety). Если при выполнении операции возникает исключение, то это не должно оказать какого-либо влияния на состояние приложения. Состояние объектов должно быть таким же как и до вызовов методов.
- Гарантия отсутствия исключений (no throw guarantee). Ни при каких обстоятельствах метод не должен генерировать исключения. В Java это невозможно, например, из-за того, что VirtualMachineError может произойти в любом месте, и это никак не зависит от кода. Кроме того, эту гарантию практически невозможно обеспечить в общем случае.
Источники
- Обработка ошибок и исключения — Сайт Георгия Корнеева
- Лекция Георгия Корнеева — Лекториум
- The Java Tutorials. Lesson: Exceptions
- Обработка исключений — Википедия
- Throwable (Java Platform SE 7 ) — Oracle Documentation
- try/catch/finally и исключения — www.skipy.ru
10: Обработка ошибок с помощью исключений
Основная философия Java в том, что “плохо сформированный код не будет работать”.
Идеальное время для поимки ошибки — это время компиляции, прежде чем вы попробуете даже запустить программу. Однако не все ошибки могут быть определены во время компиляции. Оставшиеся проблемы должны быть обработаны во время выполнения, с помощью некоторого правила, которая позволяет источнику ошибки передавать соответствующую информацию приемщику, который будет знать, как правильно обрабатывать затруднение.
В C и других ранних языках могло быть несколько таких правил, и они обычно устанавливались соглашениями, а не являлись частью языка программирования. Обычно вы возвращали специальное значение или устанавливали флаг, а приемщику предлагалось взглянуть на это значение или на флаг и определить, было ли что-нибудь неправильно. Однако, по прошествии лет, было обнаружено, что программисты, использующие библиотеки, имеют тенденцию думать о себе, как о непогрешимых, например: “Да, ошибки могут случаться с другими, но не в моем коде”. Так что, не удивительно, что они не проверяют состояние ошибки (а иногда состояние ошибки бывает слишком глупым, чтобы проверять [51] ). Если вы всякий раз проверяли состояние ошибки при вызове метода, ваш код мог превратиться нечитаемый ночной кошмар. Поскольку программисты все еще могли уговорить систему в этих языках, они были стойки к принятию правды: Этот подход обработки ошибок имел большие ограничения при создании больших, устойчивых, легких в уходе программ.
Решением является упор на причинную натуру обработки ошибок и усиление правил. Это действительно имеет долгую историю, так как реализация обработки исключений возвращает нас к операционным системам 1960-х и даже к бейсиковому “ on error goto ” (переход по ошибке). Но исключения C++ основывались на Ada, а Java напрямую базируется на C++ (хотя он больше похож на Object Pascal).
Слово “исключение” используется в смысле “Я беру исключение из этого”. В том месте, где возникает проблема, вы можете не знать, что делать с ней, но вы знаете, что вы не можете просто весело продолжать; вы должны остановиться и кто-то, где-то должен определить, что делать. Но у вас нет достаточно информации в текущем контексте для устранения проблемы. Так что вы передаете проблему в более высокий контекст, где кто-то будет достаточно квалифицированным, чтобы принять правильное решение (как в цепочке команд).
Другая, более значимая выгода исключений в том, что они очищают код обработки ошибок. Вместо проверки всех возможных ошибок и выполнения этого в различных местах вашей программы, вам более нет необходимости проверять место вызова метода (так как исключение гарантирует, что кто-то поймает его). И вам необходимо обработать проблему только в одном месте, называемом обработчик исключения. Это сохранит ваш код и разделит код, описывающий то, что вы хотите сделать, от кода, который выполняется, если что-то случается не так. В общем, чтение, запись и отладка кода становится яснее при использовании исключений, чем при использовании старого способа обработки ошибок.
Так как обработка исключений навязывается компилятором Java, то есть так много примеров, которые могут быть написаны в этой книге без изучения обработки исключений. Эта глава вводит вас в код, который вам необходим для правильной обработки исключений, и способы, которыми вы можете генерировать свои собственные исключения, если ваш метод испытывает затруднения.
Основные исключения
Исключительное состояние — это проблема, которая мешает последовательное исполнение метода или ограниченного участка, в котором вы находитесь. Важно различать исключительные состояния и обычные проблемы, в которых вы имеете достаточно информации в текущем контексте, чтобы как-то справиться с трудностью. В исключительном состоянии вы не можете продолжать обработку, потому что вы не имете необходимой информации, чтобы разобраться с проблемой в текущем контексте. Все, что вы можете сделать — это выйти из текущего контекста и отослать эту проблему к высшему контексту. Это то, что случается, когда вы выбрасываете исключение.
Простой пример — деление. Если вы делите на ноль, стоит проверить, чтобы убедиться, что вы пройдете вперед и выполните деление. Но что это значит, что делитель равен нулю? Может быть, вы знаете, в контексте проблемы вы пробуете решить это в определенном методе, как поступать с делителем, равным нулю. Но если это не ожидаемое значение, вы не можете это определить внутри и раньше должны выбросить исключение, чем продолжать свой путь.
Когда вы выбрасываете исключение, случается несколько вещей. Во-первых, создается объект исключения тем же способом, что и любой Java объект: в куче, с помощью new . Затем текущий путь выполнения (который вы не можете продолжать) останавливается, и ссылка на объект исключения выталкивается из текущего контекста. В этот момент вступает механизм обработки исключений и начинает искать подходящее место для продолжения выполнения программы. Это подходящее место — обработчик исключения, чья работа — извлечь проблему, чтобы программа могла попробовать другой способ, либо просто продолжиться.
Простым примером выбрасывания исключения является рассмотрение ссылки на объект, называемой t . Возможно, что вы можете передать ссылку, которая не была инициализирована, так что вы можете пожелать проверить ее перед вызовом метода, использующего эту ссылку на объект. Вы можете послать информацию об ошибке в больший контекст с помощью создания объекта, представляющего вашу информацию и “выбросить” его из вашего контекста. Это называется выбрасыванием исключения. Это выглядит так:
if(t == null) throw new NullPointerException();
Здесь выбрасывается исключение, которое позволяет вам — в текущем контексте — отказаться от ответственности, думая о будущем решении. Оно магически обработается где-то в другом месте. Где именно будет скоро показано.
Аргументы исключения
Как и многие объекты в Java, вы всегда создаете исключения в куче, используя new , который резервирует хранилище и вызывает конструктор. Есть два конструктора для всех стандартных исключений: первый — конструктор по умолчанию, и второй принимает строковый аргумент, так что вы можете поместить подходящую информацию в исключение:
if(t == null) throw new NullPointerException("t = null");
Эта строка позже может быть разложена при использовании различных методов, как скоро будет показано.
Ключевое слово throw является причиной несколько относительно магических вещей. Обычно, вы сначала используете new для создания объекта, который соответствует ошибочному состоянию. Вы передаете результирующую ссылку в throw . Объект, в результате, “возвращается” из метода, даже если метод обычно не возвращает этот тип объекта. Простой способ представлять себе обработку исключений, как альтернативный механизм возврата, хотя вы будете иметь трудности, если будете использовать эту аналогию и далее. Вы можете также выйти из обычного блока, выбросив исключение. Но значение будет возвращено, и произойдет выход из метода или блока.
Любое подобие обычному возврату из метода здесь заканчивается, потому что куда вы возвращаетесь, полностью отличается от того места, куда вы вернетесь при нормальном вызове метода. (Вы закончите в соответствующем обработчике исключения, который может быть очень далеко — на много уровней ниже по стеку вызова — от того места, где выброшено исключение.)
В дополнение, вы можете выбросить любой тип Выбрасываемого(Throwable) объекта, который вы хотите. Обычно вы будете выбрасывать различные классы исключений для каждого различного типа ошибок. Информация об ошибке представлена и внутри объекта исключения, и выбранным типом исключения, так что кто-то в большем контексте может определить, что делать с вашим исключением. (Часто используется только информация о типе объекта исключения и ничего значащего не хранится в объекте исключения.)
Ловля исключения
Если метод выбросил исключение, он должен предполагать, что исключение будет “поймано” и устранено. Один из преимуществ обработки исключений Java в том, что это позволяет вам концентрироваться на проблеме, которую вы пробуете решить в одном месте, а затем принимать меры по ошибкам из этого кода в другом месте.
Чтобы увидеть, как ловятся исключения, вы должны сначала понять концепцию критического блока . Он является секцией кода, которая может произвести исключение и за которым следует код, обрабатывающий это исключение.
Блок try
Если вы находитесь внутри метода, и вы выбросили исключение (или другой метод, вызванный вами внутри этого метода, выбросил исключение), такой метод перейдет в процесс бросания. Если вы не хотите быть выброшенными из метода, вы можете установить специальный блок внутри такого метода для поимки исключения. Он называется блок проверки, потому что вы “проверяете” ваши различные методы, вызываемые здесь. Блок проверки — это обычный блок, которому предшествует ключевое слово try :
try < // Код, который может сгенерировать исключение >
Если вы внимательно проверяли ошибки в языке программирования, который не поддерживает исключений, вы окружали каждый вызов метода кодом установки и проверки ошибки, даже если вы вызывали один и тот же метод несколько раз. С обработкой исключений вы помещаете все в блок проверки и ловите все исключения в одном месте. Это означает, что ваш код становится намного легче для написания и легче для чтения, поскольку цель кода — не смешиваться с проверкой ошибок.
Обработчики исключений
Конечно, выбрасывание исключения должно где-то заканчиваться. Это “место” — обработчик исключения , и есть один обработчик для каждого типа исключения, которые вы хотите поймать. Обработчики исключений следуют сразу за блоком проверки и объявляются ключевым словом catch :
try < // Код, который может сгенерировать исключение > catch(Type1 id1) < // Обработка исключения Type1 > catch(Type2 id2) < // Обработка исключения Type2 > catch(Type3 id3) < // Обработка исключения Type3 > // и так далее.
Каждое catch предложение (обработчик исключения) как меленький метод, который принимает один и только один аргумент определенного типа. Идентификаторы ( id1 , id2 и так далее) могут быть использованы внутри обработчика, как аргумент метода. Иногда вы нигде не используете идентификатор, потому что тип исключения дает вам достаточно информации, чтобы разобраться с исключением, но идентификатор все равно должен быть.
Обработчики должны располагаться прямо после блока проверки. Если выброшено исключение, механизм обработки исключений идет охотится за первым обработчиком с таким аргументом, тип которого совпадает с типом исключения. Затем происходит вход в предложение catch, и рассматривается обработка исключения. Поиск обработчика, после остановки на предложении catch, заканчивается. Выполняется только совпавшее предложение catch; это не как инструкция switch , в которой вам необходим break после каждого case , чтобы предотвратить выполнение оставшейся части.
Обратите внимание, что внутри блока проверки несколько вызовов различных методов может генерировать одно и тоже исключение, но вам необходим только один обработчик.
Прерывание против возобновления
Есть две основные модели в теории обработки исключений. При прерывании (которое поддерживает Java и C++), вы предполагаете, что ошибка критична и нет способа вернуться туда, где возникло исключение. Кто бы ни выбросил исключение, он решил, что нет способа спасти ситуацию, и он не хочет возвращаться обратно.
Альтернатива называется возобновлением — это означает, что обработчик исключения может что-то сделать для исправления ситуации, а затем повторно вызовет придирчивый метод, предполагая, что вторая попытка будет удачной. Если вы хотите возобновления, это означает, что вы все еще надеетесь продолжить выполнение после обработки исключения. В этом случае ваше исключение больше похоже на вызов метода, в котором вы должны произвести настройку ситуации в Java, после чего возможно возобновление. (То есть, не выбрасывать исключение; вызвать метод, который исправит проблему.) Альтернатива — поместить ваш блок try внутри цикла while , который производит повторный вход в блок try , пока не будет получен удовлетворительный результат.
Исторически программисты используют операционные системы, которые поддерживают обработку ошибок с возобновлением, в конечном счете, заканчивающуюся использованием прерывающего кода и пропуском возобновления. Так что, хотя возобновление на первый взгляд кажется привлекательнее, оно не так полезно на практике. Вероятно, главная причина — это с оединение таких результатов: ваш обработчик часто должен знать, где брошено исключение и содержать не характерный специфический код для места выброса. Это делает код трудным для написания и ухода, особенно для больших систем, где исключения могут быть сгенерированы во многих местах.
Создание ваших собственных исключений
Вы не ограничены в использовании существующих Java исключений. Это очень важно, потому что часто вам будет нужно создавать свои собственные исключения, чтобы объявить специальную ошибку, которую способна создавать ваша библиотека, но это не могли предвидеть, когда создавалась иерархия исключений Java.
Для создания вашего собственного класса исключения вы обязаны наследовать его от исключения существующего типа, предпочтительно от того, которое наиболее близко подходит для вашего нового исключения (однако, часто это невозможно). Наиболее простой способ создать новый тип исключения — это просто создать конструктор по умолчанию для вас, так чтобы он совсем не требовал кода:
//: c10:SimpleExceptionDemo.java // Наследование вашего собственного исключения. class SimpleException extends Exception <> public class SimpleExceptionDemo < public void f() throws SimpleException < System.out.println( "Throwing SimpleException from f()"); throw new SimpleException (); > public static void main(String[] args) < SimpleExceptionDemo sed = new SimpleExceptionDemo(); try < sed.f(); >catch(SimpleException e) < System.err.println("Caught it!"); > > > ///:~
Когда компилятор создает конструктор по умолчанию, он автоматически (и невидимо) вызывает конструктор по умолчанию базового класса. Конечно, в этом случае у вас нет конструктора SimpleException(String) , но на практике он не используется часто. Как вы увидите, наиболее важная вещь в использовании исключений — это имя класса, так что чаще всего подходят такие исключения, как показаны выше.
Вот результат, который печатается на консоль стандартной ошибки — поток для записи в System.err . Чаще всего это лучшее место для направления информации об ошибках, чем System.out , который может быть перенаправлен. Если вы посылаете вывод в System.err , он не может быть перенаправлен, в отличие от System.out , так что пользователю легче заметить его.
Создание класса исключения, который также имеет конструктор, принимающий String , также достаточно просто:
//: c10:FullConstructors.java // Наследование вашего собственного исключения. class MyException extends Exception < public MyException() <> public MyException(String msg) < super(msg); > > public class FullConstructors < public static void f() throws MyException < System.out.println( "Throwing MyException from f()"); throw new MyException(); > public static void g() throws MyException < System.out.println( "Throwing MyException from g()"); throw new MyException("Originated in g()"); > public static void main(String[] args) < try < f(); >catch(MyException e) < e.printStackTrace(System.err); >try < g(); >catch(MyException e) < e.printStackTrace(System.err); >> > ///:~
Дополнительный код достаточно мал — добавлено два конструктора, которые определяют способы создания MyException . Во втором конструкторе явно вызывается конструктор базового класса с аргументом String с помощью использования ключевого слова super .
Информация трассировки направляется в System.err , так как это лучше, поскольку она будет выводиться, даже если System.out будет перенаправлен.
Программа выводит следующее:
Throwing MyException from f() MyException at FullConstructors.f(FullConstructors.java:16) at FullConstructors.main(FullConstructors.java:24) Throwing MyException from g() MyException: Originated in g() at FullConstructors.g(FullConstructors.java:20) at FullConstructors.main(FullConstructors.java:29)
Вы можете увидеть недостаток деталей в этих сообщениях MyException , выбрасываемых из f( ) .
Процесс создания вашего собственного исключения может быть развит больше. Вы можете добавить дополнительные конструкторы и члены:
//: c10:ExtraFeatures.java // Дальнейшее украшение класса исключения. class MyException2 extends Exception < public MyException2() <> public MyException2(String msg) < super(msg); > public MyException2(String msg, int x) < super(msg); i = x; > public int val() < return i; > private int i; > public class ExtraFeatures < public static void f() throws MyException2 < System.out.println( "Throwing MyException2 from f()"); throw new MyException2(); > public static void g() throws MyException2 < System.out.println( "Throwing MyException2 from g()"); throw new MyException2("Originated in g()"); > public static void h() throws MyException2 < System.out.println( "Throwing MyException2 from h()"); throw new MyException2( "Originated in h()", 47); > public static void main(String[] args) < try < f(); >catch(MyException2 e) < e.printStackTrace(System.err); >try < g(); >catch(MyException2 e) < e.printStackTrace(System.err); >try < h(); >catch(MyException2 e) < e.printStackTrace(System.err); System.err.println("e.val() Georgia">Бал добавлен член - данные i, вместе с методами, которые читают его значение и дополнительные конструкторы, которые устанавливают его. Вод результат работы: Throwing MyException2 from f() MyException2 at ExtraFeatures.f(ExtraFeatures.java:22) at ExtraFeatures.main(ExtraFeatures.java:34) Throwing MyException2 from g() MyException2: Originated in g() at ExtraFeatures.g(ExtraFeatures.java:26) at ExtraFeatures.main(ExtraFeatures.java:39) Throwing MyException2 from h() MyException2: Originated in h() at ExtraFeatures.h(ExtraFeatures.java:30) at ExtraFeatures.main(ExtraFeatures.java:44) e.val() = 47
Так как исключение является просто еще одним видом объекта, вы можете продолжать этот процесс наращивания мощность ваших классов исключений. Однако запомните, что все это украшение может быть потеряно для клиентского программиста, использующего ваш пакет, так как он может просто взглянуть на выбрасываемое исключение и ничего более. (Это способ чаще всего используется в библиотеке исключений Java.)
Спецификация исключения
В Java, вам необходимо проинформировать клиентских программистов, которые вызывают ваши методы, что метод может выбросить исключение. Это достаточно цивилизованный метод, поскольку тот, кто производит вызов, может точно знать какой код писать для поимки всех потенциальных исключений. Конечно, если доступен исходный код, клиентский программист может открыть программу и посмотреть на инструкцию throw , но часто библиотеки не поставляются с исходными текстами. Для предотвращения возникновения этой проблемы Java обеспечивает синтаксис (и навязывает вам этот синтаксис), позволяющий вам правильно сказать клиентскому программисту, какое исключение выбрасывает этот метод, так что клиентский программист может обработать его. Это спецификация исключения и это часть объявления метода, добавляемая после списка аргументов.
Спецификация исключения использует дополнительное ключевое слово throws , за которым следует за список потенциальных типов исключений. Так что определение вашего метода может выглядеть так:
void f() throws TooBig, TooSmall, DivZero < //.
Если вы скажете
void f() < // .
это будет означать, что исключения не выбрасываются из этого метода. (Кроме исключения, типа RuntimeException , которое может быть выброшено в любом месте — это будет описано позже.)
Вы не можете обмануть спецификацию исключения — если ваш метод является причиной исключения и не обрабатывает его, компилятор обнаружит это и скажет вам что вы должны либо обработать исключение, либо указать с помощью спецификации исключения, что оно может быть выброшено из вашего метода. При введении ограничений на спецификацию исключений с верху вниз, Java гарантирует, что исключение будет корректно обнаружено во время компиляции[52].
Есть одно место, в котором вы можете обмануть: вы можете заявить о выбрасывании исключения, которого на самом деле нет. Компилятор получит ваши слова об этом и заставит пользователя вашего метода думать, что это исключение на самом деле выбрасывается. Это имеет благотворный эффект на обработчика этого исключения, так как вы на самом деле позже можете начать выбрасывать это исключение и это не потребует изменения существующего кода. Также важно создание абстрактного базового класса и интерфейсов , наследующих классам или реализующим многие требования по выбрасыванию исключений.
Перехват любого исключения
Можно создать обработчик, ловящий любой тип исключения. Вы сделаете это, перехватив исключение базового типа Exception (есть другие типы базовых исключений, но Exception — это базовый тип, которому принадлежит фактически вся программная активность):
catch(Exception e) < System.err.println("Caught an exception"); >
Это поймает любое исключение, так что, если вы используете его, вы будете помещать его в конце вашего списка обработчиков для предотвращения перехвата любого обработчика исключения, который мог управлять течением.
Так как класс Exception — это базовый класс для всех исключений, которые важны для программиста, вы не получите достаточно специфической информации об исключении, но вы можете вызвать метод, который пришел из его базового типа Throwable :
String getMessage( )
String getLocalizedMessage ( )
Получает подробное сообщение или сообщение, отрегулированное по его месту действия.
String toString( )
Возвращает короткое описание Throwable, включая подробности сообщения, если они есть.
void printStackTrace( )
void printStackTrace(PrintStream)
void printStackTrace ( PrintWriter )
Печатает Throwable и трассировку вызовов Throwable. Вызов стека показывает последовательность вызовов методов, которые подвели вас к точке, в которой было выброшено исключение. Первая версия печатает в поток стандартный поток ошибки, второй и третий печатают в выбранный вами поток (в Главе 11, вы поймете, почему есть два типа потоков).
Throwable fillInStackTrace ( )
Запись информации в этот Throwable объекте о текущем состоянии кадра стека. Это полезно, когда приложение вновь выбрасывает ошибки или исключение (дальше об этом будет подробнее).
Кроме этого вы имеете некоторые другие метода, наследуемые от базового типа Throwable Object (базовый тип для всего). Один из них, который может быть удобен для исключений, это getClass( ) , который возвращает объектное представление класса этого объекта. Вы можете опросить у объекта этого Класса его имя с помощью getName( ) или toString( ) . Вы также можете делать более изощренные вещи с объектом Класса, которые не нужны в обработке ошибок. Объект Class будет изучен позже в этой книге.
Вот пример, показывающий использование основных методов Exception :
//: c10:ExceptionMethods.java // Демонстрация методов Exception. public class ExceptionMethods < public static void main(String[] args) < try < throw new Exception("Here's my Exception"); > catch(Exception e) < System.err.println("Caught Exception"); System.err.println( "e.getMessage(): " + e.getMessage()); System.err.println( "e.getLocalizedMessage(): " + e.getLocalizedMessage()); System.err.println("e.toString(): " + e); System.err.println("e.printStackTrace():"); e.printStackTrace(System.err); > > > ///:~
Вывод этой программы:
Caught Exception e.getMessage(): Here's my Exception e.getLocalizedMessage(): Here's my Exception e.toString(): java.lang.Exception: Here's my Exception e.printStackTrace(): java.lang.Exception: Here's my Exception at ExceptionMethods.main(ExceptionMethods.java:7) java.lang.Exception: Here's my Exception at ExceptionMethods.main(ExceptionMethods.java:7)
Вы можете заметить, что методы обеспечивают больше информации — каждый из них дополняет предыдущий.
Повторное выбрасывание исключений
Иногда вам будет нужно вновь выбросить исключение, которое вы только что поймали, обычно это происходит, когда вы используете Exception , чтобы поймать любое исключение. Так как вы уже имеете ссылку на текущее исключение, вы можете просто вновь бросить эту ссылку:
catch(Exception e) < System.err.println("An exception was thrown"); throw e; >
Повторное выбрасывание исключения является причиной того, что исключение переходит в обработчик следующего, более старшего контекста. Все остальные предложения catch для того же самого блока try игнорируются. Кроме того, все, что касается объекта исключения, сохраняется, так что обработчик старшего контекста, который поймает исключение этого специфического типа, может получить всю информацию из этого объекта.
Если вы просто заново выбросите текущее исключение, то информация, которую вы печатаете об этом исключении, в printStackTrace( ) будет принадлежать источнику исключения, а не тому месту, откуда вы его вновь выбросили. Если вы хотите установить новый стек информации трассировки, вы можете сделать это, вызвав функцию fillInStackTrace( ) , которая возвращает объект исключения, для которого текущий стек наполняется информацией для старого объекта исключения. Вот как это выглядит:
//: c10:Rethrowing.java // Демонстрация fillInStackTrace() public class Rethrowing < public static void f() throws Exception < System.out.println( "originating the exception in f()"); throw new Exception("thrown from f()"); > public static void g() throws Throwable < try < f(); >catch(Exception e) < System.err.println( "Inside g(), e.printStackTrace()"); e.printStackTrace(System.err); throw e; // 17 // throw e.fillInStackTrace(); // 18 > > public static void main(String[] args) throws Throwable < try < g(); >catch(Exception e) < System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); > > > ///:~
Важные строки помечены комментарием с числами. При раскомментированной строке 17 (как показано), на выходе получаем:
originating the exception in f() Inside g(), e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24) Caught in main, e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24)
Так что стек трассировки исключения всегда помнит исходное место, не имеет значения, сколько прошло времени перед повторным выбрасыванием.
Если закомментировать строку 17, а строку 18 раскомментировать, будет использоваться функция fillInStackTrace( ) , и получим результат:
originating the exception in f() Inside g(), e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24) Caught in main, e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.g(Rethrowing.java:18) at Rethrowing.main(Rethrowing.java:24)
Поскольку fillInStackTrace( ) в строке 18 становится новой исходной точкой исключения.
Класс Throwable должен появиться в спецификации исключения для g( ) и main( ) , потому что fillInStackTrace( ) производит ссылку на объект Throwable . Так как Throwable — это базовый класс для Exception , можно получить объект, который является Throwable , но не Exception , так что обработчик для Exception в main( ) может промахнуться. Чтобы убедится, что все в порядке, компилятор навязывает спецификацию исключения для Throwable . Например, исключение в следующем примере не перехватывается в main( ) :
//: c10:ThrowOut.java public class ThrowOut < public static void main(String[] args) throws Throwable < try < throw new Throwable(); > catch(Exception e) < System.err.println("Caught in main()"); > > > ///:~
Также возможно вновь выбросить исключение, отличающееся от того, которое вы поймали. Если вы делаете это, вы получаете сходный эффект, как если бы вы использовали fillInStackTrace( ) — информация об оригинальном состоянии исключения теряется, а то, с чем вы остаетесь — это информация, относящаяся к новому throw :
//: c10:RethrowNew.java // Повторное выбрасывание объекта, // отличающегося от пойманного. class OneException extends Exception < public OneException(String s) < super(s); > > class TwoException extends Exception < public TwoException(String s) < super(s); > > public class RethrowNew < public static void f() throws OneException < System.out.println( "originating the exception in f()"); throw new OneException("thrown from f()"); > public static void main(String[] args) throws TwoException < try < f(); >catch(OneException e) < System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); throw new TwoException("from main()"); > > > ///:~
Вот что напечатается:
originating the exception in f() Caught in main, e.printStackTrace() OneException: thrown from f() at RethrowNew.f(RethrowNew.java:17) at RethrowNew.main(RethrowNew.java:22) Exception in thread "main" TwoException: from main() at RethrowNew.main(RethrowNew.java:27)
Конечное исключение знает только то, что оно произошло в main( ) , а не в f( ) .
Вам никогда не нужно заботится об очистке предыдущего исключения или что другое исключение будет иметь значение. Они являются объектами, базирующимися в куче и создающимися с помощью new , так что сборщик мусора автоматически очистит их все.
Стандартные исключения Java
Класс Java Throwable описывает все, что может быть выброшено как исключение. Есть два основных типа объектов Throwable (“тип” = “наследуется от”). Error представляет ошибки времени компиляции и системные ошибки, о поимке которых вам не нужно беспокоиться (за исключением особых случаев). Exception — основной тип, который может быть выброшен из любого стандартного метода библиотеки классов Java и из вашего метода, что случается во время работы. Так что основной тип, интересующий программистов Java — это Exception .
Лучший способ получить обзор исключений — просмотреть HTML документацию Java, которую можно загрузить с java.sun.com. Это стоит сделать один раз, чтобы почувствовать разнообразие исключений, но вы скоро увидите, что нет никакого специального отличия одного исключения от другого кроме его имени. Кроме того, число исключений в Java увеличивается, поэтому бессмысленно перечислять их в книге. Каждая новая библиотека, получаемая от третьих производителей, вероятно, имеет свои собственные исключения. Важно понимать концепцию и то, что вы должны делать с исключением.
Основная идея в том, что имя исключения представляет возникшую проблему, и имя исключения предназначено для самообъяснения. Не все исключения определены в java.lang , некоторые создаются для поддержки других библиотек, таких как util , net и io , как вы можете видеть по полому имени класса или по их наследованию. Например, все исключения I/O наследуются от java.io.IOException .
Особый случай RuntimeException
Первый пример в этой главе был:
if(t == null) throw new NullPointerException();
Это может быть немного пугающим: думать, что вы должны проверять на null каждую ссылку, передаваемую в метод (так как вы не можете знать, что при вызове была передана правильная ссылка). К счастью вам не нужно это, поскольку Java выполняет стандартную проверку во время выполнения за вас и, если вы вызываете метод для null ссылки, Java автоматически выбросит NullPointerException . Так что приведенную выше часть кода всегда излишняя.
Есть целая группа типов исключений, которые относятся к такой категории. Они всегда выбрасываются Java автоматически и вам не нужно включать их в вашу спецификацию исключений. Что достаточно удобно, что они все сгруппированы вместе и относятся к одному базовому классу, называемому RuntimeException , который является великолепным примером наследования: он основывает род типов, которые имеют одинаковые характеристики и одинаковы в поведении. Также вам никогда не нужно писать спецификацию исключения, объявляя, что метод может выбросить RuntimeException , так как это просто предполагается. Так как они указывают на ошибки, вы, фактически, никогда не выбрасываете RuntimeException — это делается автоматически. Если вы заставляете ваш код выполнять проверку на RuntimeException s, он может стать грязным. Хотя вы обычно не ловите RuntimeExceptions , в ваших собственных пакетах вы можете по выбору выбрасывать некоторые из RuntimeException .
Что случится, если вы не выбросите это исключение? Так как компилятор не заставляет включать спецификацию исключений для этого случая, достаточно правдоподобно, что RuntimeException могут принизывать насквозь ваш метод main( ) и не ловится. Чтобы увидеть, что случится в этом случае, попробуйте следующий пример:
//: c10:NeverCaught.java // Игнорирование RuntimeExceptions. public class NeverCaught < static void f() < throw new RuntimeException("From f()"); > static void g() < f(); >public static void main(String[] args) < g(); >> ///:~
Вы уже видели, что RuntimeException (или любое, унаследованное от него) — это особый случай, так как компилятор не требует спецификации этих типов.
Вот что получится при выводе:
Exception in thread "main" java.lang.RuntimeException: From f() at NeverCaught.f(NeverCaught.java:9) at NeverCaught.g(NeverCaught.java:12) at NeverCaught.main(NeverCaught.java:15)
Так что получим такой ответ: Если получаем RuntimeException , все пути ведут к выходу из main( ) без поимки, для такого исключения вызывается printStackTrace( ) , и происходит выход из программы.
Не упускайте из виду, что вы можете только игнорировать RuntimeException в вашем коде, так как вся другая обработка внимательно ограничивается компилятором. Причина в том, что RuntimeException представляют ошибки программы:
- Ошибка, которую вы не можете поймать (получение null ссылки, передаваемой в ваш метод клиентским программистом, например).
- Ошибки, которые вы, как программист, должны проверять в вашем коде (такие как ArrayIndexOutOfBoundsException , где вы должны обращать внимание на размер массива).
Вы можете увидеть какая огромная выгода от этих исключений, так как они помогают процессу отладки.
Интересно заметить, что вы не можете классифицировать обработку исключений Java, как инструмент с одним предназначением. Да, он предназначен для обработки этих надоедливых ошибок времени выполнения, которые будут случаться, потому что ограничения накладываются вне кода управления, но он также важен для определенных типов ошибок программирования, которые компилятор не может отследить.
Выполнение очистки с помощью finally
Часто есть такие места кода, которые вы хотите выполнить независимо от того, было ли выброшено исключение в блоке try , или нет. Это обычно относится к некоторым операциям, отличным от утилизации памяти (так как об этом заботится сборщик мусора). Для достижения этого эффекта вы используете предложение finally [53] в конце списка всех обработчиков исключений. Полная картина секции обработки исключений выглядит так:
try < // Критическая область: Опасная активность, // при которой могут быть выброшены A, B или C > catch(A a1) < // Обработчик ситуации A > catch(B b1) < // Обработчик ситуации B > catch(C c1) < // Обработчик ситуации C > finally < // Действия, совершаемые всякий раз >
Для демонстрации, что предложение finally всегда отрабатывает, попробуйте эту программу:
//: c10:FinallyWorks.java // Предложение finally выполняется всегда. class ThreeException extends Exception <> public class FinallyWorks < static int count = 0; public static void main(String[] args) < while(true) < try < // Пост-инкремент, вначале равен нулю: if(count++ == 0) throw new ThreeException(); System.out.println("No exception"); > catch(ThreeException e) < System.err.println("ThreeException"); > finally < System.err.println("In finally clause"); if(count == 2) break; // выйти из "while" > > > > ///:~
Эта программа также дает подсказку, как вы можете поступить с фактом, что исключения в Java (как и исключения в C++) не позволяют вам возвратится обратно в то место, откуда оно выброшено, как обсуждалось ранее. Если вы поместите ваш блок try в цикл, вы сможете создать состояние, которое должно будет встретиться, прежде чем вы продолжите программу. Вы также можете добавить статический счетчик или какое-то другое устройство, позволяющее циклу опробовать различные подходы, прежде чем сдаться. Этим способом вы можете построить лучший уровень живучести вашей программы.
Вот что получается на выводе:
ThreeException In finally clause No exception In finally clause
Независимо от того, было выброшено исключение или не, предложение finally выполняется всегда.
Для чего нужно finally?
В языках без сборщика мусора и без автоматического вызова деструктора [54] , finally очень важно, потому что оно позволяет программисту гарантировать освобождение памяти независимо от того, что случилось в блоке try . Но Java имеет сборщик мусора, так что освобождение памяти, фактически, не является проблемой. Также, язык не имеет деструкторов для вызова. Так что, когда вам нужно использовать finally в Java?
finally необходимо, когда вам нужно что-то установить, отличное от блока памяти, в его оригинальное состояние. Это очистка определенного вида, такое как открытие файла или сетевого соединения, рисование на экране или даже переключение во внешний мир, как смоделировано в следующем примере:
//: c10:OnOffSwitch.java // Почему используется finally? class Switch < boolean state = false; boolean read() < return state; > void on() < state = true; > void off() < state = false; > > class OnOffException1 extends Exception <> class OnOffException2 extends Exception <> public class OnOffSwitch < static Switch sw = new Switch(); static void f() throws OnOffException1, OnOffException2 <> public static void main(String[] args) < try < sw.on(); // Код, который может выбросить исключение. f(); sw.off(); > catch(OnOffException1 e) < System.err.println("OnOffException1"); sw.off(); > catch(OnOffException2 e) < System.err.println("OnOffException2"); sw.off(); > > > ///:~
Цель этого примера — убедится, что переключатель выключен, когда main( ) будет завершена, так что sw.off( ) помешена в конце блока проверки и в каждом обработчике исключения. Но возможно, что будет выброшено исключение, которое не будет поймано здесь, так что sw.off( ) будет пропущено. Однако с помощью finally вы можете поместить очищающий код для блока проверки только в одном месте:
//: c10:WithFinally.java // Finally гарантирует очистку. public class WithFinally < static Switch sw = new Switch(); public static void main(String[] args) < try < sw.on(); // Код, который может выбросить исключение. OnOffSwitch.f(); > catch(OnOffException1 e) < System.err.println("OnOffException1"); > catch(OnOffException2 e) < System.err.println("OnOffException2"); > finally < sw.off(); >> > ///:~
Здесь sw.off( ) была перемещена только в одно место, где она гарантировано отработает не зависимо от того, что случится.
Даже в случае исключения, не пойманного в этом случае набором предложений catch , finally будет выполнено прежде, чем механизм обработки исключений продолжит поиск обработчика на более высоком уровне:
//: c10:AlwaysFinally.java // Finally выполняется всегда. class FourException extends Exception <> public class AlwaysFinally < public static void main(String[] args) < System.out.println( "Entering first try block"); try < System.out.println( "Entering second try block"); try < throw new FourException(); > finally < System.out.println( "finally in 2nd try block"); > > catch(FourException e) < System.err.println( "Caught FourException in 1st try block"); > finally < System.err.println( "finally in 1st try block"); > > > ///:~
Вывод этой программы показывает что происходит:
Entering first try block Entering second try block finally in 2nd try block Caught FourException in 1st try block finally in 1st try block
Инструкция finally также будет исполнена в ситуации, когда используются инструкции break и continue . Обратите внимание, что наряду с помеченным break и помеченным continue , finally подавляет необходимость в использовании инструкции goto в Java.
Ловушка: потерянное исключение
Вообще, реализация исключений Java достаточно выдающееся, но, к сожалению, есть недостаток. Хотя исключения являются индикаторами кризиса в вашей программе и не должны игнорироваться, возможна ситуация, при которой исключение просто потеряется. Это случается при определенной конфигурации использования предложения finally :
//: c10:LostMessage.java // Как может быть потеряно исключение. class VeryImportantException extends Exception < public String toString() < return "A very important exception!"; > > class HoHumException extends Exception < public String toString() < return "A trivial exception"; > > public class LostMessage < void f() throws VeryImportantException < throw new VeryImportantException(); > void dispose() throws HoHumException < throw new HoHumException(); > public static void main(String[] args) throws Exception < LostMessage lm = new LostMessage(); try < lm.f(); >finally < lm.dispose(); >> > ///:~
Вот что получаем на выходе:
Exception in thread "main" A trivial exception at LostMessage.dispose(LostMessage.java:21) at LostMessage.main(LostMessage.java:29)
Вы можете видеть, что нет свидетельств о VeryImportantException , которое просто заменилось HoHumException в предложении finally . Это достаточно серьезная ловушка, так как это означает, что исключения могут быть просто потеряны и далее в более узких и трудно определимых ситуациях, чем показано выше. В отличие от Java, C++ трактует ситуации, в которых второе исключение выбрасывается раньше, чем обработано первое, как ошибку программирования. Надеюсь, что будущие версии Java решат эту проблему (с другой стороны, вы всегда окружаете метод, который выбрасывает исключение, такой как dispose( ) , предложением try-catch ).
Ограничения исключений
Когда вы перегружаете метод, вы можете выбросить только те исключения, которые указаны в версии базового класса этого метода. Это полезное ограничение, так как это означает, что код, работающий с базовым классом, будет автоматически работать с любым другим объектом, наследованным от базового класса (конечно, это фундаментальная концепция ООП), включая исключения.
Этот пример демонстрирует виды налагаемых ограничений (времени компиляции) на исключения:
//: c10:StormyInning.java // Перегруженные методы могут выбрасывать только те // исключения, которые указаны в версии // базового класса, или унаследованное от // исключения базового класса. class BaseballException extends Exception <> class Foul extends BaseballException <> class Strike extends BaseballException <> abstract class Inning < Inning() throws BaseballException <> void event () throws BaseballException < // На самом деле ничего не выбрасывает > abstract void atBat() throws Strike, Foul; void walk() <> // Ничего не выбрасывает > class StormException extends Exception <> class RainedOut extends StormException <> class PopFoul extends Foul <> interface Storm < void event() throws RainedOut; void rainHard() throws RainedOut; > public class StormyInning extends Inning implements Storm < // можно добавить новое исключение для // конструкторов, но вы должны работать // с базовым исключеним конструктора: StormyInning() throws RainedOut, BaseballException <> StormyInning(String s) throws Foul, BaseballException <> // Обычный метод должен соответствовать базовому классу: //! void walk() throws PopFoul <> //Ошибка компиляции // Интерфейс НЕ МОДЕТ добавлять исключения к существующим // методам базового класса: //! public void event() throws RainedOut <> // Если метод еще не существует в базовом классе // исключение допустимо: public void rainHard() throws RainedOut <> // Вы можете решить не выбрасывать исключений вообще, // даже если версия базового класса делает это: public void event() <> // Перегруженные методы могут выбрасывать // унаследованные исключения: void atBat() throws PopFoul <> public static void main(String[] args) < try < StormyInning si = new StormyInning(); si.atBat(); > catch(PopFoul e) < System.err.println("Pop foul"); > catch(RainedOut e) < System.err.println("Rained out"); > catch(BaseballException e) < System.err.println("Generic error"); > // Strike не выбрасывается в унаследованной версии. try < // Что случится при обратном приведении? Inning i = new StormyInning(); i.atBat(); // Вы должны ловить исключения от метода // версии базового класса: > catch(Strike e) < System.err.println("Strike"); > catch(Foul e) < System.err.println("Foul"); > catch(RainedOut e) < System.err.println("Rained out"); > catch(BaseballException e) < System.err.println( "Generic baseball exception"); > > > ///:~
В Inning вы можете увидеть, что и конструктор, и метод event( ) говорят о том, что они будут выбрасывать исключение, но они не делают этого. Это допустимо, потому что это позволяет вам заставить пользователя ловить любое исключение, которое может быть добавлено и перегруженной версии метода event( ) . Эта же идея применена к абстрактным методам, как видно в atBat( ) .
Интересен interface Storm , потому что он содержит один метод ( event( ) ), который определен в Inning , и один метод, которого там нет. Оба метода выбрасывают новый тип исключения: RainedOut . Когда StormyInning расширяет Inning и реализует Storm , вы увидите, что метод event( ) в Storm не может изменить исключение интерфейса event( ) в Inning . Кроме того, в этом есть здравый смысл, потому что, в противном случае, вы никогда не узнаете, что поймали правильную вещь, работая с базовым классом. Конечно, если метод, описанный как интерфейс, не существует в базовом классе, такой как rainHard( ) , то нет проблем, если он выбросит исключения.
Ограничения для исключений не распространяются на конструкторы. Вы можете видеть в StormyInning , что конструктор может выбросить все, что хочет, не зависимо от того, что выбрасывает конструктор базового класса. Но, так как конструктор базового класса всегда, так или иначе, должен вызываться (здесь автоматически вызывается конструктор по умолчанию), конструктор наследованного класса должен объявить все исключения конструктора базового класса в своей спецификации исключений. Заметьте, что конструктор наследованного класса не может ловить исключения, выброшенные конструктором базового класса.
Причина того, что StormyInning.walk( ) не будет компилироваться в том, что она выбрасывает исключение, которое Inning.walk( ) не выбрасывает. Если бы это допускалось, то вы могли написать код, вызывающий Inning.walk( ) , и не иметь обработчика для любого исключения, а затем, когда вы заменили объектом класса, унаследованного от Inning , могло начать выбрасываться исключение и ваш код сломался бы. При ограничивании методов наследуемого класса в соответствии со спецификацией исключений методов базового класса замена объектов допустима.
Перегрузка метода event( ) показывает, что версия метода наследованного класса может не выбрасывать исключение, даже если версия базового класса делает это. Опять таки это хорошо, так как это не нарушит ни какой код, который написан с учетом версии базового класса с выбрасыванием исключения. Сходная логика применима и к atBat( ) , которая выбрасывает PopFoul — исключение, унаследованное от Foul , выбрасываемое версией базового класса в методе atBat( ) . Таким образом, если кто-то напишет код, который работает с классом Inning и вызывает atBat( ) , он должен ловить исключение Foul . Так как PopFoul наследуется от Foul , обработчик исключения также поймает PopFoul .
Последнее, что нас интересует — это main( ) . Здесь вы можете видеть, что если вы имеете дело с объектом StormyInning , компилятор заставит вас ловить только те исключения, которые объявлены для этого класса, но если вы выполните приведение к базовому типу, то компилятор (что совершенно верно) заставит вас ловить исключения базового типа. Все эти ограничения производят более устойчивый код обработки исключений [55] .
Полезно понимать, что хотя спецификация исключений навязываются компилятором во время наследования, спецификация исключений не является частью метода типа, который включает только имя метода и типы аргументов. Поэтому вы не можете перегрузить метод, основываясь на спецификации исключений. Кроме того, только потому, что спецификация исключений существует в версии метода базового класса, это не означает, что она должна существовать в версии метода наследованного класса. Это немного отличается от правил наследования, по которым метод базового класса должен также существовать в наследуемом классе. Есть другая возможность: “спецификации исключения интерфейса” для определенного метода может сузиться во время наследования и перегрузки, но он не может расшириться — это точно противоречит правилам для интерфейса класса при наследовании.
Конструкторы
Когда пишете код с исключениями, обычно важно, чтобы вы всегда спрашивали: “Если случится исключение, будет ли оно правильно очищено?” Большую часть времени вы этим сохраните, но в конструкторе есть проблемы. Конструктор переводит объект в безопасное начальное состояние, но он может выполнить некоторые операции — такие как открытие файла — которые не будут очищены, пока пользователь не закончит работать с объектом и не вызовет специальный очищающий метод. Если вы выбросили исключение из конструктора, это очищающее поведение может не сработать правильно. Это означает, что вы должны быть особенно осторожными при написании конструктора.
Так как вы только изучили о finally , вы можете подумать, что это корректное решение. Но это не так просто, потому что finally выполняет очищающий код каждый раз, даже в ситуации, в которой вы не хотите, чтобы выполнялся очищающий код до тех пор, пока не будет вызван очищающий метод. Таким образом, если вы выполняете очистку в finally , вы должны установить некоторый флаг, когда конструктор завершается нормально, так что вам не нужно ничего делать в блоке finally , если флаг установлен. Потому что это обычно не элегантное решение (вы соединяете ваш код в одном месте с кодом в другом месте), так что лучше попробовать предотвратить выполнение такого рода очистки в finally , если вы не вынуждены это делать.
В приведенном ниже примере класс, называемый InputFile , при создании открывает файл и позволяет вам читать его по одной строке (конвертируя в String ). Он использует классы FileReader и BufferedReader из стандартной библиотеки Java I/O, которая будет обсуждаться в Главе 11, но которая достаточно проста, что вы, вероятно, не будете иметь трудностей в понимании основ ее использования:
//: c10:Cleanup.java // Уделение внимание на исключение // в конструкторе. import java.io.*; class InputFile < private BufferedReader in; InputFile(String fname) throws Exception < try < in = new BufferedReader( new FileReader(fname)); // Другой код, который может выбросить исключение > catch(FileNotFoundException e) < System.err.println( "Could not open " + fname); // Что не открыто, то не закроется throw e; > catch(Exception e) < // Все другие исключения должны быть перекрыты try < in.close(); >catch(IOException e2) < System.err.println( "in.close() unsuccessful"); > throw e; // Повторное выбрасывание > finally < // Не закрывайте их здесь. > > String getLine() < String s; try < s = in.readLine(); >catch(IOException e) < System.err.println( "readLine() unsuccessful"); s = "failed"; > return s; > void cleanup() < try < in.close(); >catch(IOException e2) < System.err.println( "in.close() unsuccessful"); > > > public class Cleanup < public static void main(String[] args) < try < InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) System.out.println(""+ i++ + ": " + s); in.cleanup(); > catch(Exception e) < System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); > > > ///:~
Конструктор для InputFile получает аргумент String , который является именем файла, который вы открываете. Внутри блока try создается FileReader с использование имени файла. FileReader не очень полезен до тех пор, пока вы не используете его для создания BufferedReader , с которым вы фактически можете общаться — обратите внимание, что в этом одна из выгод InputFile , который комбинирует эти два действия.
Если конструктор FileReader завершится неудачно, он выбросит FileNotFoundException , которое должно быть поймано отдельно, потому что это тот случай, когда вам не надо закрывать файл, так как его открытие закончилось неудачно. Любое другое предложение catch должно закрыть файл, потому что он был открыт до того, как произошел вход в предложение catch. (Конечно это ненадежно, если более одного метода могут выбросить FileNotFoundException . В этом случае вы можете захотеть разбить это на несколько блоков try .) Метод close( ) может выбросить исключение, так что он проверяется и ловится, хотя он в блоке другого предложения catch — это просто другая пара фигурных скобок для компилятора Java. После выполнения локальных операций исключение выбрасывается дальше, потому что конструктор завершился неудачей, и вы не захотите объявить, что объект правильно создан и имеет силу.
В этом примере, который не использует вышеупомянутую технику флагов, предложение finally определенно это не то место для закрытия файла, так как он будет закрываться всякий раз по завершению конструктора. Так как вы хотим, чтобы файл был открыт для использования все время жизни объекта InputFile , этот метод не подходит.
Метод getLine( ) возвращает String , содержащую следующую строку файла. Он вызывает readLine( ) , который может выбросить исключение, но это исключение ловится, так что getLine( ) не выбрасывает никаких исключений. Одна из проблем разработки исключений заключается в том, обрабатывать ли исключение полностью на этом уровне, обрабатывать ли его частично и передавать то же исключение (или какое-то другое) или просто передавать его дальше. Дальнейшая передача его, в подходящих случаях, может сильно упростить код. Метод getLine( ) превратится в:
String getLine() throws IOException < return in.readLine(); >
Но, конечно, теперь вызывающий код несет ответственность за обработку любого исключения IOException , которое может возникнуть .
Метод cleanup( ) должен быть вызван пользователем, когда закончится использование объекта InputFile . Это освободит ресурсы системы (такие как указатель файла), которые используются объектами BufferedReader и/или FileReader [56] . Вам не нужно делать этого до тех пор, пока вы не закончите работать с объектом InputFile . Вы можете подумать о перенесении такой функциональности в метод finalize( ) , но как показано в Главе 4, вы не можете всегда быть уверены, что будет вызвана finalize( ) (даже если вы можете быть уверены, что она будет вызвана, вы не будете знать когда). Это обратная сторона Java: вся очистка — отличающаяся от очистки памяти — не происходит автоматически, так что вы должны информировать клиентского программиста, что он отвечает за это и, возможно, гарантировать возникновение такой очистки с помощью finalize( ) .
В Cleanup.java InputFile создается для открытия того же исходного файла, который создает программа, файл читается по строкам, а строки нумеруются. Все исключения ловятся в основном в main( ) , хотя вы можете выбрать лучшее решение.
Польза от этого примера в том, что он показывает вам, почему исключения введены именно в этом месте книги — вы не можете работать с основами ввода/вывода, не используя исключения. Исключения настолько интегрированы в программирование на Java, особенно потому, что компилятор навязывает их, что вы можете выполнить ровно столько, не зная их, сколько может сделать, работая с ними.
Совпадение исключений
Когда выброшено исключение, система обработки исключений просматривает “ближайшие” обработчики в порядке их записи. Когда он находит совпадение, исключение считается обработанным и дальнейшего поиска не производится.
Для совпадения исключения не требуется точного соответствия между исключением и его обработчиком. Объект наследованного класса будет совпадать обработчику базового класса, как показано в этом примере:
//: c10:Human.java // Ловля иерархических исключений. class Annoyance extends Exception <> class Sneeze extends Annoyance <> public class Human < public static void main(String[] args) < try < throw new Sneeze(); > catch(Sneeze s) < System.err.println("Caught Sneeze"); > catch(Annoyance a) < System.err.println("Caught Annoyance"); > > > ///:~
Исключение Sneeze будет поймано первым предложением catch , с которым оно совпадает — конечно, это первое предложение. Конечно, если вы удалите первое предложение catch, оставив только:
try < throw new Sneeze(); > catch(Annoyance a) < System.err.println("Caught Annoyance"); >
Код все равно будет работать, потому что он ловит базовый класс Sneeze . Другими словами, catch(Annoyance e) будет ловить Annoyance или любой другой класс, наследованный от него. Это полезно, потому что, если вы решите добавить еще унаследованных исключений в метод, то код клиентского программиста не будет требовать изменений до тех пор, пока клиент ловит исключения базового класса.
Если вы пробуете “маскировать” исключения наследованного класса, помещая первым предложение catch для базового класса, как здесь:
try < throw new Sneeze(); > catch(Annoyance a) < System.err.println("Caught Annoyance"); > catch(Sneeze s) < System.err.println("Caught Sneeze"); >
компилятор выдаст вам сообщение об ошибке, так как catch-предложение Sneeze никогда не будет достигнуто.
Руководство по исключениям
Используйте исключения для:
- Исправления проблем и нового вызова метода, который явился причиной исключения.
- Исправления вещей и продолжения без повторной попытки метода.
- Подсчета какого-то альтернативного результата вместо того, который должен был вычислить метод.
- Выполнения того, что вы можете в текущем контексте и повторного выброса того же исключения в более старший контекст.
- Выполнения того, что вы можете в текущем контексте и повторного выброса другого исключения в более старший контекст.
- Прекращения программы .
- Упрощения. (Если ваша схема исключений делает вещи более сложными, то это приводит к тягостному и мучительному использованию.)
- Создать более безопасные библиотеки и программы. (Для краткосрочной инвестиции — для отладки — и для долгосрочной инвестиции (Для устойчивости приложения).)
Резюме
Улучшение перекрытия ошибок является мощнейшим способом, который увеличивает устойчивость вашего кода. Перекрытие ошибок является фундаментальной концепцией для каждой написанной вами программы, но это особенно важно в Java, где одна из главнейших целей — это создание компонент программ для других. Для создание помехоустойчивой системы каждый компонент должен быть помехоустойчивым.
Цель обработки исключений в Java состоит в упрощении создания больших, надежных программ при использовании меньшего кода, насколько это возможно, и с большей уверенностью, что ваше приложение не имеет не отлавливаемых ошибок.
Исключения не ужасно сложны для изучения и это одна из тех особенностей, которая обеспечивает немедленную и значительную выгоду для вашего проекта. К счастью, Java ограничивает все аспекты исключений, так что это гарантирует, что они будут использоваться совместно и разработчиком библиотеки, и клиентским программистом.
Упражнения
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
- Создайте класс с main( ) , который выбрасывает объект, класса Exception внутри блока try . Передайте конструктору Exception аргумент String . Поймайте исключение внутри предложение catch и напечатайте аргумент String . Добавьте предложение finally и напечатайте сообщение, чтобы убедится, что вы были там.
- Создайте ваш собственный класс исключений, используя ключевое слово extends . Напишите конструктор для этого класса, который принимает аргумент String , и хранит его внутри объекта в ссылке String . Напишите метод, который печатает хранящийся String . Создайте предложение try-catch для наблюдения своего собственного исключения.
- Напишите класс с методом, который выбрасывает исключение типа, созданного в Упражнении 2. Попробуйте откомпилировать его без спецификации исключения, чтобы посмотреть, что скажет компилятор. Добавьте соответствующую спецификацию исключения. Испытайте ваш класс и его исключение в блоке try-catch.
- Определите ссылку на объект и инициализируйте ее значением null . Попробуйте вызвать метод по этой ссылке. Не окружайте код блоком try-catch , чтобы поймать исключение.
- Создайте класс с двумя методами f( ) и g( ) . В g( ) выбросите исключение нового типа, который вы определили. В f( ) вызовите g( ) , поймайте его исключение и, в предложении catch , выбросите другое исключение (второго определенного вами типа). Проверьте ваш код в main( ) .
- Создайте три новых типа исключений. Напишите класс с методом, который выбрасывает все три исключения. В main( ) вызовите метод, но используйте только единственное предложение catch , которое будет ловить все три вида исключений.
- Напишите код для генерации и поимки ArrayIndexOutOfBoundsException .
- Создайте свое собственное поведение по типу возобновления, используя цикл while , который будет повторяться, пока исключение больше не будет выбрасываться.
- Создайте трехуровневую иерархию исключений. Теперь создайте базовый класс A , с методом, который выбрасывает исключение базового класса вашей иерархии. Наследуйте B от A и перегрузите метод так, чтобы он выбрасывал исключение второго уровня в вашей иерархии. Повторите то же самое, унаследовав класс C от B . В main( ) создайте C и приведите его к A , затем вызовите метод.
- Покажите, что конструктор наследуемого класса не может ловить исключения, брошенные конструктором базового класса .
- Покажите, что OnOffSwitch.java может завершиться неудачей при выбрасывании RuntimeException внутри блока try .
- Покажите, что WithFinally.java не завершится неудачей при выбрасывании RuntimeException в блоке try .
- Измените Упражнение 6, добавив предложение finally . Проверьте, что предложение finally выполняется даже, если выбрасывается NullPointerException .
- Создайте пример, в котором вы используете флаг для управления вызовом кода очистки, как описано во втором параграфе под заголовком “Конструкторы”.
- Измените StormyInning.java , добавив тип исключения UmpireArgument и метод, который его выбрасывает. Проверьте измененную иерархию.
- Удалите первый catch в Human.java и проверьте, что код все равно компилируется и правильно работает.
- Добавьте второй уровень потерь исключения в LostMessage.java , так чтобы HoHumException заменялось третьим исключением.
- В Главе 5 найдите две программы, называемые Assert.java и измените их, чтобы они выбрасывали свои собственные исключения вместо печать в System.err . Это исключение должно быть внутренним классом, расширяющим RuntimeException .
- Добавьте подходящий набор исключений в c08:GreenhouseControls.java .
[51] C программист может посмотреть на возвращаемое значение printf( ) , как пример этого.
[52] Это значительное улучшение, по сравнению с обработкой исключений в C++, которая не ловит нарушения спецификации исключений до времени выполнения, хотя это не очень полезно.
[53] Обработка исключений в C++ не имеет предложения finally , поэтому в C++ освобождение происходит в деструкторах, чтобы завершить такой род очистки.
[54] Деструктор — это функция, которая всегда вызывается, когда объект более не используется. Вы всегда знаете точно, где совершен вызов деструктора. C++ имеет автоматический вызов деструктора, но Object Pascal из Delphi версии 1 и 2 не делает этого (что изменяет значение и использование концепции деструкторов в этом языке).
[55] ISO C++ добавил сходное ограничение, которое требует, чтобы исключение наследуемого метода были теми же или наследовались от тех же, что и выбрасываемые методом базового класса. Это первый случай, в котором C++ реально способен проверить спецификацию исключений во время компиляции.
[56] В C++ деструктор должен это обрабатывать за вас.
Глава 7 ИСКЛЮЧЕНИЯ
Плохо подогнанное снаряжение может заставить ваш гранатомет M203 выстрелить в самый неожиданный момент . Подобное происшествие плохо скажется на вашей репутации среди тех , кто останется в живых . Журнал PS армии США , август 1993 года Во время своей работы приложение иногда сталкивается с разного рода нештатными ситуациями . При вызове метода некоторого объекта он может обнаружить у себя внутренние проблемы ( неверные значения переменных ), найти ошибки в других объектах или данных ( например , в файле или сетевом адресе ), определить факт нарушения своего базового контракта ( чтение данных из закрытого потока ) и так далее . Многие программисты не проверяют все возможные источники ошибок , и на то есть веская причина : если при каждом вызове метода анализировать все мыслимые ошибки , текст программы становится совершенно невразумительным . Таким образом достигается компромисс между правильностью ( проверка всех ошибок ) и ясностью ( отказ от загромождения основной логики программы множеством проверок ). Исключения предоставляют удобную возможность проверки ошибок без загромождения текста программы . Кроме того , исключения непосредственно сигнализируют об ошибках , а не меняют значения флагов или каких — либо полей , которые потом нужно проверять . Исключения превращают ошибки , о которых может сигнализировать метод , в явную часть контракта этого метода . Список исключений виден программисту , проверяется компилятором и сохраняется в расширенных классах , переопределяющих данный метод . Исключение во збуждается , когда возникает неожиданное ошибочное состояние . Затем исключение перехватывается соответствующим условием в стеке вызова методов . Если исключение не перехвачено , срабатывает обработчик исключения по умолчанию , который обычно выводит полезную информацию об исключении ( скажем , содержимое стека вызовов ). 7.1. Создание новых типов исключений Исключения в Java представляют собой объекты . Все типы исключений ( то есть все классы , объекты которых возбуждаются в качестве исключений ) должны расширять класс языка Java, который называется Throwable, или один из его подклассов . Класс Throwable содержит строку , которая может использоваться для описания исключения . По соглашению , новые типы исключений расширяют класс Exception, а не Throwable. Исключения Java, главным образом , являются проверяемыми — это означает , что компилятор следит за тем , чтобы ваши методы возбуждали лишь те исключения , о которых объявлено в заголовке метода . Стандартные исключения времени выполнения и ошибки расширяют классы RuntimeException и Error, тем самым создавая непроверяемые исключения . Все исключения , определяемые программистом , должны расширять класс Exception, и , таким образом , они являются проверяемыми . Иногда хочется иметь больше данных , описывающих состояние исключения , — одной строки , предоставляемой классом Exception, оказывается недостаточно . В таких случаях можно расширить класс Exception и создать на его основе новый класс с дополнительными данными ( значения которых обычно задаются в конструкторе ).
converted to PDF by BoJIoc Например , предположим , что в интерфейс Attributed, рассмотренный в главе 4 , добавился метод replaceValue, который заменяет текущее значение именованного атрибута новым . Если атрибут с указанным именем не существует , возбуждается исключение — вполне резонно предположить , что заменить несуществующий атрибут не удастся . Исключение должно содержать имя атрибута и новое значение , которое пытались ему присвоить . Для работы с таким исключением создается класс NoSuchAttribiteException: public class NoSuchAttributeException extends Exception < public String attrName; public Object newValue; NoSuchAttributeException(String name, Object value) < super("No attribute named \"" + name + "\" found"); attrName = name; newValue = value; >> NoSuchAttribiteException расширяет Exception и включает конструктор , которому передается имя атрибута и присваиваемое значение ; кроме того , добавляются открытые поля для хранения данных . Внутри конструктора вызывается конструктор суперкласса со строкой , описывающей происходящее . Исключения такого рода могут использоваться во фрагменте программы , перехватывающем исключения , поскольку они выводят понятное человеку описание ошибки и данные , вызвавшие ее . Добавление полезной информации — одна из причин , по которым создаются новые исключения . Другая причина для появления новых типов исключений заключается в том , что тип является важной частью данных исключения , поскольку исключения перехватываются по их типу . Из этих соображений исключение NoSuch AttribiteException стоит создать даже в том случае , если вы не собираетесь включать в него новые данные ; в этом случае программист , для которого представляет интерес только это исключение , сможет перехватить его отдельно от всех прочих исключений , запускаемых методами интерфейса Attributed или иными методами , применяемыми к другим объектам в том же фрагменте программы . В общем случае исключения новых типов следует создавать тогда , когда программист хочет обрабатывать ошибки одного типа и пропускать ошибки другого типа . В этом случае он может воспользоваться новыми исключениями для выполнения нужного фрагмента программы , вместо того чтобы изучать содержимое объекта — исключения и решать , интересует ли его данное исключение или же оно не относится к делу и перехвачено случайно . 7.2. Оператор throw Исключения возбуждаются оператором throw, которому в качестве параметра передается объект . Например , вот как выглядит реализация replaceValue в классе AttributedImpl из главы 4: public void replaceValue(String name, Object newValue) throws NoSuchAttributeException <
Attr attr = | find(name); | // Искать attr |
if (attr == | null) | // Если атрибут не найден |
throw new NoSuchAttributeException(name, this); attr.valueOf(newValue); >
Метод replaceValue сначала ищет имя атрибута в текущем объекте Attr. Если атрибут не найден , то возбуждается объект — исключение типа NoSuch AttribiteException и его конструктору предоставляются содержательные данные . Исключения являются
converted to PDF by BoJIoc объектами , поэтому перед использованием их необходимо создать . Если атрибут не существует , то его значение заменяется новым . Разумеется , исключение может быть порождено вызовом метода , внутри которого оно возбуждается . 7.3. Условие throws Первое , что бросается в глаза в приведенном выше методе replace Value, — это список проверяемых исключений , которые в нем возбуждаются . В Java необходимо перечислить проверяемые исключения , возбуждаемые методом , поскольку программист при вызове метода должен знать их в такой же степени , в какой он представляет себе нормальное поведение метода . Проверяемые исключения , возбуждаемые методом , не уступают по своей важности типу возвращаемого значения — и то и другое необходимо объявить . Проверяемые исключения объявляются в условии throws, которое может содержать список значений , отделяемых друг от друга запятыми . Внутри метода разрешается возбуждать исключения , являющиеся расширениями типа Exception в условии throws, поскольку всегда допускается полиморфно использовать класс вместо его суперкласса . Метод может возбуждать несколько различных исключений , являющихся расширениями одного конкретного класса , и при этом объявить в условии throws всего один суперкласс . Тем не менее , поступая таким образом , вы скрываете от работающих с методом программистов полезную информацию , потому что они не будут знать , какие из возможных расширенных типов исключений возбуждаются методом . В целях надлежащего документирования условие throws должно быть как можно более полным и подробным . Контракт , определяемый условием throws, обязан неукоснительно соблюдаться — можно возбуждать лишь те исключения , которые указаны в данном условии . Возбуждение любого другого исключения ( прямое , с помощью throw, или косвенное , через вызов другого метода ) является недопустимым . Отсутствие условия throws не означает , что метод может возбуждать любые исключения ; наоборот , оно говорит о том , что он не возбуждает никаких исключений . Все стандартные исключения времени выполнения ( такие , как ClassCast Exception и ArithmeticException) представляют собой расширения класса RuntimeException. О более серьезных ошибках сигнализируют исключения , которые являются расширениями класса Error и могут возникнуть в произвольный момент в произвольной точке программы . RuntimeException и Error — единственные исключения , которые не нужно перечислять в условии throws; они являются общепринятыми и могут возбуждаться в любом методе , поэтому компилятор не проверяет их . Полный список классов стандартных непроверяемых исключений приведен в Приложении Б . Инициализаторы и блоки статической инициализации не могут возбуждать проверяемые исключения , как прямо , так и посредством вызова метода , возбуждающего исключение . Во время конструирования объекта нет никакого способа перехватить и обработать исключение . При инициализации полей выход заключается в том , чтобы инициализировать их внутри конструктора , который может возбуждать исключения . Для статических инициализаторов можно поместить инициализацию в статический блок , который бы перехватывал и обрабатывал исключение . Статические блоки не возбуждают исключений , но могут перехватывать их .
Java довольно строго подходит к обработке проверяемых исключений , поскольку это помогает избежать программных сбоев , вызванных невниманием к ошибкам . Опыт показывает , что программисты забывают про обработку ошибок или откладывают ее на будущее , которое так никогда и не наступает . Условие throws ясно показывает , какие исключения возбуждаются методом , и обеспечивает их обработку .
converted to PDF by BoJIoc При вызове метода , у которого в условии throws приведено проверяемое исключение , имеются три варианта : ∙ Перехватить исключение и обработать его . ∙ Перехватить исключение и перенаправить его в обработчик одного из ваших исключений , для чего возбудить исключение типа , объявленного в вашем условии throws. ∙ Объявить данное исключение в условии throws и отказаться от его обработки в вашем методе ( хотя в нем может присутствовать условие finally, которое сначала выполнит некоторые завершающие действия ; подробности приводятся ниже ). При любом из этих вариантов вам необходимо перехватить исключение , возбужденное другим методом ; это станет темой следующего раздела . Упражнение 7.1 Создайте класс — исключение ObjectNotFoundException для класса Linked List, построенного нами в предыдущих упражнениях . Включите в него метод find, предназначенный для поиска объектов в списке , который либо возвращает нужный объект LinkedList, либо возбуждает исключение , если объект отсутствует в списке . Почему такой вариант оказывается более предпочтительным , нежели возврат значения null для ненайденного объекта ? Какие данные должны входить в ObjectNotFoundException? 7.4. Операторы try, catch и finally Чтобы перехватить исключение , необходимо поместить фрагмент программы в оператор try. Базовый синтаксис оператора try выглядит следующим образом : try блок catch (тип-исключения идентификатор) блок catch (тип-исключения идентификатор) блок . finally блок Тело оператора try выполняется вплоть до возбуждения исключения или до успешного завершения . Если возникает исключение , то по порядку просматриваются все условия catch, пока не будет найдено исключение нужного класса или одного из его суперклассов . Если подходящее условие catch так и не найдено , то исключение выходит из текущего оператора try во внешний , который может обработать его . В операторе try может присутствовать любое количество условий catch, в том числе и ни одного . Если ни одно из условий catch внутри метода не перехватывает исключение , то оно передается в тот фрагмент программы , который вызвал данный метод . Если в try присутствует условие finally, то составляющие его операторы выполняются после того , как вся обработка внутри try будет завершена . Выполнение finally происходит независимо от того , как завершился оператор — нормально , в результате исключения или при выполнении управляющего оператора типа return или break. В приводимом ниже примере осуществляется подготовка к обработке одного из исключений , возбуждаемых в replaceValue: try < attributedObj.replaceValue("Age", new Integer(8)); >catch (NoSuchAttributeException e)
converted to PDF by BoJIoc // так не должно быть, но если уж случилось — восстановить Attr attr = new Attr(e.attrName, e.newValue); attrbuteObj.add(attr); > try содержит оператор ( представляющий собой блок ), который выполняет некоторые действия , в обычных условиях заканчивающиеся успешно . Если все идет нормально , то работа блока на этом завершается . Если же во время выполнения программы в try- блоке возбудилось какое — либо исключение ( прямо , посредством throw, либо косвенно , через внутренний вызов метода ), то выполнение кода внутри try прекращается , и просматриваются связанные с ним условия catch, чтобы определить , нужно ли перехватывать исключение . Условие catch чем — то напоминает внедренный метод с одним параметром — типом перехватываемого исключения . Внутри условия catch вы можете пытаться восстановить работу программы после произошедшего исключения или же выполнить некоторые действия и повторно возбудить исключение , чтобы вызывающий фрагмент также имел возможность перехватить его . Кроме того , catch может сделать то , что сочтет нужным , и прекратить свою работу — в этом случае управление передается оператору , следующему за оператором try ( после выполнения условия finally, если оно имеется ). Универсальное условие catch ( например , перехватывающее исключения типа Exception) обычно говорит о плохо продуманной реализации , поскольку оно будет перехватывать все исключения , а не только то , которое нас интересует . Если воспользоваться подобным условием в своей программе , то в результате при возникновении проблем с атрибутами будет обрабатываться , скажем , исключение ClassCastException. Условия catch в операторе try просматриваются поочередно , от первого к последнему , чтобы определить , может ли тип объекта — исключения присваиваться типу , объявленному в catch. Когда будет найдено условие catch с подходящим типом , происходит выполнение его блока , причем идентификатору в заголовке catch присваивается ссылка на объект — исключение . Другие условия catch при этом не выполняются . С оператором try может быть связано произвольное число условий catch, если каждое из них перехватывает новый тип исключения . Поскольку условия catch просматриваются поочередно , перехват исключения некоторого типа перед перехватом исключения расширенного типа является ошибкой . Первое условие всегда будет перехватывать исключение , а второе — никогда . По этой причине размещение условия catch для исключения — суперкласса перед условием для одного из его подклассов вызывает ошибку во время компиляции : class SuperException extends Exception < >class SubException extends SuperException < >class BadCatch < public void goodTry() < /* НЕДОПУСТИМЫЙ порядок перехвата исключений */ try < throw new SubException(); >catch (SuperException superRef) < // Перехватывает и SuperException, и SubException >catch (SubException subRef) < // Никогда не выполняется >> >
В каждом операторе try обрабатывается только один исключительный случай . Если catch или finally возбуждают новое исключение , то условия catch данного try не рассматриваются повторно . Код в условиях catch и finally находится за пределами защиты
Какие исключения объявляются в заголовке метода
RuntimeException и классы, унаследованные от него. Их перехватывать не обязательно. Это unchecked исключения. Здесь явно неточность. Дальше задача на перехват ArithmeticException — подкласс RuntimeException. Не перехватываются только ошибки — Error и его подклассы.
Эмиль Уровень 15
31 января 2023
Мне кажется цвета лучше поменять, труднопронозируемые сделать зелеными, а обязательные — красными на картинке. И еще не понятно из объяснения почему надо методу main пробрасывать, почему он «умеет» их обрабатывать без необходимости обёртывания, а метод 1 хоть и имеет те же исключения в сигнатуре, но в нем они не обрабатываются.
Екатерина Екатериновна Уровень 17
4 ноября 2022
А вот если исключение не обязательное к прописыванию в сигнатуре, но обрабатывать его в текущем методе я не хочу, то можно его писать в блоке throws? Пойду проверю..
Grock Уровень 44
14 сентября 2022
Друзья, поясните, пожалуйста: 1) try-catch — на первый взгляд, интуитивно понятно для чего — узнать какое исключение выдает (если выдает) конкретная строка кода. То бишь try-catch полезная штука для диагностики; 2) А в вот throws в сигнатуру метода зачем указывать? Чтобы просто пропустить ошибку и обеспечить работу программы дальше? С ошибкой? Не устраняя ее? Но как так, зачем создавать условие работы программы с ошибкой? Разве ошибки не нужно устранять? Подскажите, пожалуйста, знатоки ответ на п. 2.. да и на п. 1, если понимание неверное / неполное?
Ihor Уровень 13
5 августа 2022
А зачем указывать в сигнатуре метода исключения?
Aleksei Reinsalu Уровень 19
20 ноября 2021
Картинку сохраните. Пригодится.
Aleksei Reinsalu Уровень 19
20 ноября 2021
То есть если нам дали дописывать программу, где main бросает какие-то исключения валидатору, то делать проверку на них в своем коде нужно только когда этого прямо требуют условия задачи. Ну или когда еще какие-то причины есть. А по умолчанию программа с ними продолжит работать.
Игорь Уровень 17
15 августа 2021
Правильно ли я понял? Программист должен предугадать , что может наворотить пользователь , или он сам и прописать все возможные ошибки в try, catch. Если же талантливый пользователь умудриться сделать что то не прописанное в try, catch программа свалиться с выбросом stack trace. и придется в логах искать с каким исключением свалилась программа и дописывать это исключение в try, catch.
Komarov Anton Уровень 12
14 июля 2021
У меня возник вопрос касательно throw и я задал его моему другу программисту: — Объясни, я понимаю про try, catch и для чего они нужны, один проверяет, второй работает с исключениями. Но! А для чего нам «выбрасывать» через throw в сигнатуре метода исключения выше по программе? Почему нельзя сразу прописать в методе, где появляется исключение, блок try/catch и всё? — Давай представим что мы с тобой работаем в фирме по изготовлению книг. Ты мой босс, а я рабочий. (Босс — это родительский класс, рабочий — унаследованный). Ты как босс говоришь (вызываешь метод) рабочему изготовить 10 книг. Я начинаю выполнять задачу (метод makeBook), и на 7 книге у меня кончаются скрепки для сшивания листов. В реальной жизни рабочий не будет решать сам эту проблему, а доложит о ней начальству, мол так и так, скрепок нет, выполнить задачу не могу. Вот и в программах бывает так, что некоторые исключения нужно вернуть выше по коду, пускай босс сам с ней разбирается =) Надеюсь кому-то для понимания будет полезно. =)