1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-thinking-in-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
9.2 异常的捕获.md 27 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 11.03.2025 09:15 d56454c

9.2 Поймание исключений

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

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

9.2.1 Блок try

Если метод находится внутри другого метода и "бросает" исключение (или вызывает другой метод, который порождает исключение), этот метод выйдет из своего выполнения при возникновении исключения. Чтобы не позволить throw покинуть метод, можно использовать специальный блок кода для поймания исключения. Такой блок называется "блоком try", потому что здесь мы "пытаемся" различные вызовы методов. Блок try представляет собой обычную область видимости, начинающуюся со слова try:

try {
    # Код, который может порождать исключения
}
```Если используется язык программирования, не поддерживающий управление исключениями, то каждому методу придётся окружить его проверками ошибок и кодом для их обнаружения — даже если метод вызывается несколько раз. В случае использования технологии управления исключениями всё можно поместить внутрь одного блока `try`, где будут пойманы все исключения. Это значительно упрощает код и делает его более читаемым, так как цель кода больше не будет смешиваться со сложной проверкой ошибок.## 9.2.2 Управители исключений

Конечно, сгенерированное исключение должно быть перехвачено где-то. Это "где-то" известно как управитель исключений или модуль управления исключениями. Для каждого типа исключений, который требуется поймать, должен существовать свой управитель исключений. Управители исключений следуют сразу после блока `try` и отмечаются ключевым словом `catch`. Пример представлен ниже:

```markdown
try {
    # Код, который может порождать исключения
} catch(Тип1 id1) {
    # Обработка исключений типа Тип1
} catch(Тип2 id2) {
    # Обработка исключений типа Тип2
} catch(Тип3 id3) {
    # Обработка исключений типа Тип3
}

# и т.д...
```Каждый `catch` — это управитель исключений, который похож на небольшой метод, требующий одного конкретного типа параметра. Внутри контроллера можно использовать идентификаторы (`id1`, `id2` и т.д.), как обычные параметры метода. Иногда идентификаторы вообще не используются, поскольку тип исключения предоставляет достаточно информации для эффективной обработки исключения. Однако, даже если они не используются, идентификаторы должны находиться на своих местах. Контроллер должен находиться "немедленно" после блока `try`. Если будет "выброшено" исключение, механизм управления исключениями будет искать первый контроллер с совпадающими параметрами и типом исключения. Затем он войдет в этот блок `catch`, считая, что исключение было обработано (поиск контроллера прекращается сразу же после завершения блока `catch`). Только соответствующий блок `catch` будет выполнен; это отличается от конструкции `switch`, где требуется команда `break` после каждого `case`, чтобы предотвратить случайное выполнение других операторов.Внутри блока `try` обратите внимание на то, что множество различных вызовов методов могут генерировать одно и то же исключение, но потребуется всего один обработчик.

### 9.2.2 Прерывание и восстановление

В теории управления исключениями существует два основных подхода. В подходе "прерывание" (который поддерживается Java и C++) мы предполагаем, что ошибка слишком серьезна, чтобы можно было вернуться к месту её возникновения. Кто бы ни "бросил" исключение, это указывает на невозможность исправления ошибки и желания не возвращаться назад.

Другой подход называется "восстановлением". Это значит, что обработчик исключений обязан исправить текущее состояние и затем получить метод, который вызвал ошибку, предполагая, что следующая попытка выполнения будет успешной. Если используется восстановление, это означает, что вы хотите продолжить выполнение даже после того, как исключение было обработано. В этом случае наша ошибка больше похожа на вызов метода — мы используем его для установки специальных условий в Java, которые приводят к поведению, похожему на "восстановление" (то есть, вместо бросания исключения, мы вызываем метод для решения проблемы). Кроме того, можно поместить свой блок `try` внутрь цикла `while`, чтобы постоянно входить в блок `try`, пока не будет достигнут желаемый результат.Из исторической перспективы, если программист использует операционную систему, поддерживающую управление исключениями с возможностью восстановления, в конечном итоге всё равно придётся использовать код, аналогичный прерыванию, и пропустить процесс восстановления. Поэтому, хотя "восстановление" кажется идеальным решением, на практике оно оказывается сложным. Основной причиной может быть необходимость контроля за тем, были ли созданы исключения, а также наличием специализированного кода для места их создания. Это делает код трудным для написания и поддержки, особенно в больших системах, где исключения могут создаваться в нескольких местах.## 9.2.3 Нормы исключений

В Java, чтобы сообщить программистам-клиентам, которые будут вызывать методы, что те могут "вызвать" исключения, используется вежливый способ. Только так клиентские программисты смогут точно знать, какой код писать для перехвата всех потенциальных исключений. Конечно, если вы предоставляете исходный код, клиентские программисты смогут полностью проверить код и найти соответствующие операторы `throw`. Однако обычно библиотеки не сопровождаются исходным кодом. Для решения этой проблемы Java предоставляет специальную синтаксическую конструкцию (которую она насильно заставляет использовать), чтобы вежливо сообщить клиентским программистам, какие исключения может "вызвать" данный метод, позволяя им удобно контролировать это. Это то, что мы называем "нормой исключений", которая является частью объявления метода и располагается после списка аргументов.

Аннотация `throws` использует дополнительное ключевое слово: `throws`; за которым следует полный список потенциальных типов исключений. Таким образом, наш метод теперь выглядит следующим образом:```markdown
void f() throws tooBig, tooSmall, divZero { //...

Если используется следующий код:

void f() [ // ...

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

Однако полностью полагаться на аннотацию throws нельзя — если метод вызывает исключение, но не контролирует его, компилятор заметит это и сообщит вам, что вам нужно либо контролировать исключение, либо указывать, что метод может "выбросить" исключение. При использовании правильной последовательности аннотаций throws Java гарантирует корректность исключений на этапе компиляции (примечание 2).

2: Это значительное улучшение по сравнению с управлением исключениями в C++, где ошибки, нарушающие аннотацию throws, могут быть обнаружены только во время выполнения программы. Это делает механизм управления исключениями в C++ менее эффективным.


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

```markdown
catch (Exception e) {
    System.out.println("caught an exception");
}

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

Для всех основных типов исключений, поскольку Exception является их базовым классом, мы получаем ограниченную информацию об исключении, но можем использовать методы из базового класса Throwable:

getMessage()

Получение детального сообщения.

toString()

Возврат краткого описания объекта Throwable, включающего детальное сообщение (если таковое имеется).

printStackTrace()
printStackTrace(PrintStream)

Выводит путь вызова стека для Throwable. Вызовы стека показывают последовательность методов, которые привели нас к месту возникновения исключения.Первая версия выводит стандартный поток ошибок, вторая — выбранный нами поток. Если вы работаете в Windows, нельзя переопределять стандартный поток ошибок. Поэтому обычно предпочитаются второй вариант, который отправляет результат в System.out; таким образом, выход можно переопределить для любого пути, как вам будет удобно.Кроме того, мы можем получить некоторые другие методы от базового класса Object (Throwable), являющегося базовым типом для всех объектов. Один из полезных методов для управления исключениями может быть getClass(), который возвращает объект, представляющий класс этого объекта. Мы можем использовать getName() или toString() для получения имени этого класса Class. Также можно выполнять более сложные операции с объектом Class, хотя эти операции не являются необходимыми при управлении исключениями. Подробнее о Class объектах будет рассказано позже в этой главе.

Ниже представлен пример специального использования методов Exception (если возникают трудности при выполнении программы, обратитесь к разделу 3.1.2 "Присваивание" главы 3):

//: ExceptionMethods.java
// Демонстрация методов исключений
package c09;

public class ExceptionMethods {
  public static void main(String[] args) {
    try {
      throw new Exception("Здесь мое исключение");
    } catch(Exception e) {
      System.out.println("Поймано исключение");
      System.out.println(
        "e.getMessage(): " + e.getMessage());
      System.out.println(
        "e.toString(): " + e.toString());
      System.out.println("e.printStackTrace():");
      e.printStackTrace();
    }
  }
} ///:~

Эта программа выводит следующее:

Поймано исключение
e.getMessage(): Здесь мое исключение
e.toString(): java.lang.Exception: Здесь мое исключение
e.printStackTrace():
java.lang.Exception: Здесь мое исключение
        at ExceptionMethods.main
```Как видно, этот метод предоставляет множество информации — каждая последующая категория информации является подмножеством предыдущей категории.

## Переброска Исключений

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

```catch(Exception e) {
    System.out.println("Произошла ошибка");
    throw e;
}

Переброс исключения приводит его в более высокий контекст управления ошибками. Любой последующий блок catch внутри того же блока try будет проигнорирован. Кроме того, все данные, связанные с объектом исключения, сохранятся, поэтому более высокий уровень контроля ошибок сможет извлечь всю информацию из этого объекта.

Если просто перебрасывать текущее исключение, информация, связанная с исключением, которая выводится методом printStackTrace(), будет указывать на место возникновения исключения, а не на место переброса. Для установки новой трассировки стека можно вызвать метод fillInStackTrace(). Этот метод вернет специальный объект исключения, создание которого происходит следующим образом: информация о текущем стеке заполняется в оригинальный объект исключения. Вот пример:

//: Перебрасывание.java
// Демонстрация fillInStackTrace()
``````java
public class Перебрасывание {
  public static void f() throws Исключение {
    System.out.println("генерация исключения в f()");
    throw new Исключение("выброшено из f()");
  }
  public static void g() throws Бросаемое {
    try {
      f();
    } catch (Исключение e) {
      System.out.println("внутри g(), e.printStackTrace()");
      e.printStackTrace();
      throw e; // 17
      // throw e.fillInStackTrace(); // 18
    }
  }
  public static void main(String[] args) throws Бросаемое {
    try {
      g();
    } catch (Исключение e) {
      System.out.println("поймано в main, e.printStackTrace()");
      e.printStackTrace();
    }
  }
}
///:~

Самые важные строки помечены комментариями. Обратите внимание, что строка 17 не является закомментированной. Её вывод будет следующим:

генерация исключения в f()
внутри g(), e.printStackTrace()
java.lang.Exception: выброшено из f()
        at Перебрасывание.f(Перебрасывание.java:8)
        at Перебрасывание.g(Перебрасывание.java:12)
        at Перебрасывание.main(Перебрасывание.java:24)
поймано в main, e.printStackTrace()
java.lang.Exception: выброшено из f()
        at Перебрасывание.f(Перебрасывание.java:8)
        at Перебрасывание.g(Перебрасывание.java:12)
        at Перебрасывание.main(Перебрасывание.java:24)

Поэтому трассировка стека всегда будет указывать истинное начало исключения, независимо от количества перебросов.Если закомментировать строку 17 и раскомментировать строку 18, то будет использоваться fillInStackTrace(), и результат будет таким:

генерация исключения в f()
внутри g(), e.printStackTrace()
java.lang.Exception: выброшено из f()
        at Rethrowing.f(Rethrowing.java:8)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:24)
поймано в main, e.printStackTrace()
java.lang.Exception: выброшено из f()
        at Rethrowing.g(Rethrowing.java:18)
        at Rethrowing.main(Rethrowing.java:24)
```Использование `fillInStackTrace()` делает 18-ю строку новым источником исключения.

Для методов `g()` и `main()` класс `Throwable` должен присутствовать в спецификации выбрасываемых исключений, так как `fillInStackTrace()` создаёт ссылку на объект типа `Throwable`. Поскольку `Throwable` является базовым классом для `Exception`, это может привести к ситуации, когда ссылка на объект типа `Throwable` будет указывать на что-то, что не является экземпляром `Exception`. В результате, ссылка на `Exception` внутри `main()` может потерять свою целостность. Чтобы гарантировать корректность всех операций, компилятор требует использования спецификации исключений для `Throwable`.

Например, исключение в следующей программе не будет поймано в `main()`:

```java
//: ThrowOut.java
public class ThrowOut {
  public static void
  main(String[] args) throws Throwable {
    try {
      throw new Throwable();
    } catch(Exception e) {
      System.out.println("Поймано в main()");
    }
  }
} ///:~

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

//: RethrowNew.java
// Перебросить другой объект вместо того, который был пойман
``````java
public class RethrowNew {
  public static void f() throws Exception {
    System.out.println(
      "генерация исключения в f()");
    throw new Exception("выброшено из f()");
  }

  public static void main(String[] args) {
    try {
      f();
    } catch (Exception e) {
      System.out.println(
        "Поймано в main, e.printStackTrace()");
      e.printStackTrace();
      throw new NullPointerException("из main");
    }
  }
}
///:~

генерация исключения в f() Поймано в main, e.printStackTrace() java.lang.Exception: выброшено из f() at RethrowNew.f(RethrowNew.java:8) at RethrowNew.main(RethrowNew.java:13) java.lang.NullPointerException: из main at RethrowNew.main(RethrowNew.java:18)


Последний бросаемый **исключение** знает только то, что он происходит из `main()`, а не из `f()`. Обратите внимание, что `Throwable` не является обязательным в любом спецификаторе исключений.

Никогда не следует заботиться о том, как очистить предыдущее исключение или какие-либо другие исключения, связанные с ним. Они являются объектами, созданными с помощью `new`, основанными на куче памяти, поэтому сборщик мусора автоматически освобождает их.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-thinking-in-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-thinking-in-java-zh.git
oschina-mirror
wizardforcel-thinking-in-java-zh
wizardforcel-thinking-in-java-zh
master