Обработка ошибок джава

Время на прочтение
8 мин

Количество просмотров 18K

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

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

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

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

1. Освободите ресурсы в блоке finally или используйте инструкцию «Try-With-Resource»

Довольно часто вы используете ресурс в своем блоке try, например InputStream, который вам нужно закрыть позже. Распространенной ошибкой в ​​таких ситуациях является закрытие ресурса в конце блока try.

public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);

      // используем inputStream для чтения файла 
            
      // не делайте этого
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

Проблема в том, что этот подход работает отлично до тех пор, пока не генерируется исключение. Все операторы в блоке try будут выполнены, и ресурс будет закрыт.

Но вы не зря добавили блок try. Вы вызываете один или несколько методов, которые могут вызвать исключение, или, может быть, вы сами вызываете исключение. Это означает, что вы можете не дойти до конца блока try. И как следствие, вы не закроете ресурсы.

Поэтому вам следует поместить весь код очистки в блок finally или использовать оператор try-with-resource.

Используйте блок Finally

В отличие от последних нескольких строк вашего блока try, блок finally всегда выполняется. Это происходит либо после успешного выполнения блока try, либо после обработки исключения в блоке catch. Благодаря этому вы можете быть уверены, что освободите все захваченные ресурсы.

public void closeResourceInFinally() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);

        // используем inputStream для чтения файла

    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
}

Оператор Java 7 «Try-With-Resource»

Другой вариант — это оператор try-with-resource, который я объяснил более подробно во введении в обработку исключений Java.

Вы можете использовать его, если ваш ресурс реализует интерфейс AutoCloseable. Это то, что делает большинство стандартных ресурсов Java. Когда вы открываете ресурс в предложении try, он автоматически закрывается после выполнения блока try или обработки исключения.

public void automaticallyCloseResource() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
        // используем inputStream для чтения файла

    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

2. Конкретные исключения предпочтительнее

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

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

Поэтому всегда старайтесь найти класс, который лучше всего подходит для вашего исключительного события, например, генерируйте NumberFormatException вместо IllegalArgumentException. И избегайте создания неспецифического исключения.

public void doNotDoThis() throws Exception {
    ...
}

public void doThis() throws NumberFormatException {
    ...
}

3. Документируйте определенные вами исключения

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

Итак, не забудьте добавить объявление @throws в свой Javadoc и описать ситуации, которые могут вызвать исключение.

/**
 * Этот метод делает что-то чрезвычайно полезное ...
 *
 * @param input
 * @throws MyBusinessException, если ... происходит
 */
public void doSomething(String input) throws MyBusinessException {
    ...
}

4. Генерирование исключений с описательными сообщениями

Идея, лежащая в основе этой передовой практики, аналогична двум предыдущим. Но на этот раз вы не предоставляете информацию вызывающей стороне вашего метода. Сообщение об исключении читают все, кто должен понимать, что произошло, когда исключение было зарегистрировано в файле журнала или в вашем инструменте мониторинга.

Следовательно, он должен как можно точнее описать проблему и предоставить наиболее актуальную информацию для понимания исключительного события.

Не поймите меня неправильно; вы не должны писать абзац текста. Но вам следует объяснить причину исключения в 1-2 коротких предложениях. Это помогает вашей группе эксплуатации понять серьезность проблемы, а также упрощает анализ любых инцидентов, связанных с обслуживанием.

Если вы выберете конкретное исключение, его имя класса, скорее всего, уже будет описывать тип ошибки. Таким образом, вам не нужно предоставлять много дополнительной информации. Хорошим примером этого является NumberFormatException. Оно вызывается конструктором класса java.lang.Long, когда вы предоставляете String в неправильном формате.

try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
}

Название класса NumberFormatException уже говорит вам о типе проблемы. Его сообщение должно содержать только строку ввода, которая вызвала проблему. Если имя класса исключения не так выразительно, вам необходимо предоставить необходимую информацию в сообщении.

17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"

5. Сначала перехватите наиболее конкретное исключение

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

Проблема в том, что выполняется только первый блок catch, соответствующий исключению. Итак, если вы сначала поймаете IllegalArgumentException, вы никогда не достигнете блока catch, который должен обрабатывать более конкретное NumberFormatException, потому что это подкласс IllegalArgumentException.

Всегда сначала перехватывайте наиболее конкретный класс исключения и добавляйте менее конкретные блоки перехвата в конец вашего списка.

Пример такого оператора try-catch представлен в следующем фрагменте кода. Первый блок catch обрабатывает все NumberFormatException, а второй — все IllegalArgumentException, которые не являются NumberFormatException.

public void catchMostSpecificExceptionFirst() {
    try {
        doSomething("Сообщение");
    } catch (NumberFormatException e) {
        log.error(e);
    } catch (IllegalArgumentException e) {
        log.error(e)
    }
}

6. Не перехватывайте Throwable

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

Если вы используете Throwable в предложении catch, он не только перехватит все исключения; он также перехватит все ошибки. JVM выдает ошибки, чтобы указать на серьезные проблемы, которые не предназначены для обработки приложением. Типичными примерами этого являются OutOfMemoryError или StackOverflowError. И то, и другое вызвано ситуациями, которые находятся вне контроля приложения и не могут быть обработаны.

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

public void doNotCatchThrowable() {
    try {
        // делает что-нибудь
    } catch (Throwable t) {
        // не делает этого!
    }
}

7. Не игнорируйте исключения

Вы когда-нибудь анализировали отчет об ошибке, в котором выполнялась только первая часть вашего сценария использования?

Часто это вызвано игнорируемым исключением. Разработчик, вероятно, был уверен, что оно никогда не будет вызвано, и добавил блок catch, который не обрабатывает и не регистрирует его. И когда вы найдете этот блок, вы, скорее всего, даже найдете один из известных комментариев «Этого никогда не будет».

public void doNotIgnoreExceptions() {
    try {
        // делает что-нибудь
    } catch (NumberFormatException e) {
        // это никогда не выполнится
    }
}

Что ж, возможно, вы анализируете проблему, в которой произошло невозможное.

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

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

public void logAnException() {
    try {
        // делает что-нибудь
    } catch (NumberFormatException e) {
        log.error("Это никогда не должно происходить: " + e);
    }
}

8. Не пишите в лог сгенерированные исключения

Это, вероятно, наиболее часто игнорируемая передовая практика в списке. Вы можете найти множество фрагментов кода и даже библиотек, в которых исключение перехватывается, регистрируется и повторно генерируется.

try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}

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

17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)

Повторные сообщения также не добавляют никакой информации. Как объясняется в лучшей практике №4, сообщение об исключении должно описывать исключительное событие. А трассировка стека сообщает вам, в каком классе, методе и строке было сгенерировано исключение.

Если вам нужно добавить дополнительную информацию, вы должны перехватить исключение и обернуть его в пользовательское. Но обязательно следуйте передовой практике номер 9.

public void wrapException(String input) throws MyBusinessException {
    try {
        // делает что-нибудь
    } catch (NumberFormatException e) {
        throw new MyBusinessException("Сообщение с описанием ошибки.", e);
    }
}

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

9. Оберните исключение, не обрабатывая его

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

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

public void wrapException(String input) throws MyBusinessException {
    try {
        // делает что-нибудь
    } catch (NumberFormatException e) {
        throw new MyBusinessException("Сообщение с описанием ошибки.", e);
    }
}

Резюме

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

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

Глава 9

Обработка исключений

Основные навыки и понятия

  • Представление об иерархии исключений
  • Использование ключевых слов try и catch
  • Последствия неперехвата исключений
  • Применение нескольких операторов catch
  • Перехват исключений, генерируемых подклассами
  • Вложенные блоки try
  • Генерирование исключений
  • Представление о членах класса Throwable
  • Использование ключевого слова finally
  • Использование ключевого слова throws
  • Представление о исключениях, встроенные в Java
  • Создание специальных классов исключений

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

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

В Java определены стандартные исключения для наиболее часто встречающихся программных ошибок, в том числе деления на нуль или попытки открыть несуществующий файл. Для того чтобы обеспечить требуемую реакцию на конкретную ошибку, в программу следует включить соответствующий обработчик событий. Исключения широко применяются в библиотеке Java API.

Иерархия исключений

В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable имеются два непосредственных подкласса: Exception и Error. Исключения типа Error относятся к ошибкам, возникающим в виртуальной машине Java, а не в прикладной программе. Контролировать такие исключения невозможно, поэтому реакция на них в прикладной программе, как правило, не предусматривается. В связи с этим исключения данного типа не будут описываться в этой книге.

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

Общее представление об обработке исключений

Для обработки исключений в Java предусмотрены пять ключевых слов: try, catch, throw, throws и finally. Они образуют единую подсистему, в которой использование одного ключевого слова почти всегда автоматически влечет за собой употребление другого. Каждое из упомянутых выше ключевых слов будет подробно рассмотрено далее в этой главе. Но прежде следует дать общее представление об их роли в процессе обработки исключений. Поэтому ниже поясняется вкратце, каким образом они действуют.

Операторы, в которых требуется отслеживать появление исключений, помещаются в блок try. Если в блоке try будет сгенерировано исключение, его можно перехватить и обработать нужным образом. Системные исключения генерируются автоматически. А для того чтобы сгенерировать исключение вручную, следует воспользоваться ключевым словом throw. Иногда возникает потребность обрабатывать исключения за пределами метода, в котором они возникают, и для этой цели служит ключевое слово throws. Если же некоторый фрагмент кода должен быть выполнен обязательно и независимо от того, возникнет исключение или нет, его следует поместить в блок finally.

Использование ключевых слов try и catch

Основными языковыми средствами обработки исключений являются ключевые слова try и catch. Они используются совместно. Это означает, что нельзя указать ключевое слово catch в коде, не указав ключевое слово try. Ниже приведена общая форма записи блоков try/catch, предназначенных для обработки исключений.

try {
    // Блок кода, в котором должны отслеживаться ошибки
}
catch (тип_исключения_1 объект_исключения) {
    // Обработчик исключения тип_исключения_1
}
catch (тип_исключения_2 объект_исключения) {
    // Обработчик исключения тип_исключения_2
}

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

Следует иметь в виду, что если исключение не генерируется, блок try завершается обычным образом и ни один из его операторов catch не выполняется. Выполнение программы продолжается с первого оператора, следующего за последним оператором catch. Таким образом, операторы catch выполняются только при появлении исключения.

На заметку.
В версии JDK 7 внедрена новая форма оператора try, поддерживающая автоматическое управления ресурсами и называемая оператором try с ресурсами. Более подробно она описывается в главе 10 при рассмотрении потоков ввода-вывода, в том числе и тех, что связаны с файлами, поскольку потоки ввода-вывода относятся к числу ресурсов, наиболее употребительных в прикладных программах.

Простой пример обработки исключений

Рассмотрим простой пример, демонстрирующий перехват и обработку исключения. Как известно, попытка обратиться за границы массива приводит к ошибке, и виртуальная машина Java генерирует соответствующее исключение ArraylndexOutOf BoundsException. Ниже приведен код программы, в которой намеренно создаются условия для появления данного исключения, которое затем перехватывается.

// Демонстрация обработки исключений,
class ExcDemol {
    public static void main (String args[]) {
        int nums[] = new int[4];

        // Создание блока try.
        try {
            System.out.println("Before exception is generated.");

            // Попытка обратиться за границы массива.
            nums[7] = 10;
            System.out.println("this won't be displayed");
        }
        // Перехват исключения в связи с обращением за границы массива.
        catch (ArraylndexOutOfBoundsException exc) {
            System.out.println("Index out-of-bounds!");
        }
        System.out.println("After catch statement.");
    }
}

Результат выполнения данной программы выглядит следующим образом:

Before exception is generated.
Index out-of-bounds!
After catch statement.

Несмотря на всю простоту данного примера программы, он наглядно демонстрирует несколько важных особенностей обработки исключений. Во-первых, код, подлежащий проверке на наличие ошибок, помещается в блок try. И во-вторых, когда возникает исключение (в данном случае это происходит при попытке обратиться за границы массива), выполнение блока try прерывается и управление получает блок catch. Следовательно, явного обращения к блоку catch не происходит, но переход к нему осуществляется лишь при определенном условии, возникающем в ходе выполнения программы. Так, оператор вызова метода println(), следующий за выражением, в котором происходит обращение к несуществующему элементу массива, вообще не выполняется. По завершении блока catch выполнение программы продолжается с оператора, следующего за этим блоком. Таким образом, обработчик исключений предназначен для устранения программных ошибок, приводящих к исключительным ситуациям, а также для обеспечения нормального продолжения исполняемой программы.

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

на следующую строку кода:

Теперь исключение не возникнет и блок catch не выполнится.

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

/* Исключение может быть сгенерировано одним методом,
   а перехвачено другим. */

class ExcTest {
    // сгенерировать исключение
    static void genException()  {
        int nums[] = new int[4];

        System.out.println("Before exception is generated.");

        // Здесь генерируется исключение в связи с
        // обращением за границы массива.
        nums[7] = 10;
        System.out.println("this won't be displayed");
    }
}

class ExcDemo2 {
    public static void main(String args[])  {
        try {
            ExcTest.genException() ;
        }
        //А здесь исключение перехватывается.
        catch (ArraylndexOutOfBoundsException exc) {
            System.out.println("Index out-of-bounds!");
        }
        System.out.println("After catch statement.");
    }
}

Выполнение этой версии программы дает такой же результат, как и при выполнении ее предыдущей версии. Этот результат приведен ниже.

Before exception is generated.
Index out-of-bounds!
After catch statement.

Метод genException() вызывается из блока try, и поэтому генерируемое, но не перехватываемое в нем исключение перехватывается далее в блоке catch в методе main(). Если бы метод genException() сам перехватывал исключение, оно вообще не дошло бы до метода main().

Последствия неперехвата исключений

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

// Обработка ошибки средствами виртуальной машины Java,
class NotHandled {
    public static void main(String args[]) {
        int nums[] = new int[4];

        System.out.println("Before exception is generated.");

        // Попытка обращения за границы массива,
        nums[7] = 10;
    }
}

При появлении ошибки, связанной с обращением за границы массива, выполнение программы прекращается и выводится следующее сообщение:

Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: 7 at NotHandled.main(NotHandled.java:9)

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

Как упоминалось выше, тип исключения должен соответствовать типу, указанному в операторе catch. В противном случае исключение не будет перехвачено. Так, в приведенном ниже примере программы делается попытка перехватить исключение, связанное с обращением за границы массива, с помощью оператора catch, в котором указан тип ArithmeticException еще одного встроенного в Java исключения. При неправильном обращении к массиву будет сгенерировано исключение ArraylndexOutOfBoundsException, не соответствующее типу, указанному в операторе catch. В результате программа будет завершена аварийно.

// Эта программа не будет работать нормально!
class ExcTypeMismatch {
    public static void main(String args[]) {
        int nums[] = new int[4];
        try {
        System.out.println("Before exception is generated.");
        // При выполнении следующего оператора возникает
        // исключение ArraylndexOutOfBoundsException
        nums[7] = 10;
        System.out.println("this won’t be displayed");
        }
        /* Исключение, связанное с обращением за границы массива,
        нельзя обработать с помощью оператора catch, в котором
        указан тип исключения ArithmeticException. */
        catch (ArithmeticException exc) {
        System.out.println("Index out-of-bounds!");
        }
        System.out.println("After catch statement.");
    }
}

Ниже приведен результат выполнения данной программы.

Before exception is generated.
Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: 7
    at ExcTypeMismatch.main(ExcTypeMismatch.java:10)

Нетрудно заметить, что оператор catch, в котором указан тип исключения ArithmeticException, не может перехватить исключение ArraylndexOutOfBoundsException.

Обработка исключений — изящный способ устранения программных ошибок

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

// Изящная обработка исключения и продолжение выполнения программы,
class ExcDemo3 {
    public static void main(String args[])  {
        int numer[] = { 4, 8, 16, 32, 64, 128 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.length; i++)   {
            try {
                System.out.println(numer[i] + " / " +
                                   denom[i] + " is " +
                                   numer[i]/denom[i]);
            }
            catch (ArithmeticException exc) {
                // перехватить исключение
                System.out.println("Can't divide by Zero!");
            }
        }
    }
}

Результат выполнения данной программы выглядит следующим образом:

4 / 2 is 2
Can't divide by Zero!
16/ 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16

Данный пример демонстрирует еще одну важную особенность: обработанное исключение удаляется из системы. Иными словами, на каждом шаге цикла блок try выполняется в программе сызнова, а все возникшие ранее исключения считаются обработанными. Благодаря этому в программе могут обрабатываться повторяющиеся ошибки.

Применение нескольких операторов catch

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

// Применение нескольких операторов catch.  '
class ExcDemo4 {
    public static void main(String args[]) {
        // Здесь массив numer длиннее массива denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.length; i++)   {
            try {
                System.out.println(numer[i] + " / " +
                                   denom[i] + " is " +
                                   numer[i]/denom[i]);
            }
            // За блоком try следует несколько блоков catch подряд,
            catch (ArithmeticException exc) {
                // перехватить исключение
                System.out.println("Can't divide by Zero!");
            }
            catch (ArraylndexOutOfBoundsException exc) {
                // перехватить исключение
                System.out.println("No matching element found.");
            }
        }
    }
}

Выполнение этой программы дает следующий результат:

4 / 2 is 2
Can't divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
No matching element found.
No matching element found.

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

Вообще говоря, выражения с операторами catch проверяются в том порядке, в котором они встречаются в программе. И выполняется лишь тот из них, который соответствует типу возникшего исключения. А остальные блоки операторов catch просто игнорируются.

Перехват исключений, генерируемых подклассами

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

Рассмотрим в качестве примера следующую программу

//В операторах catch исключения типа подкласса должны
// предшествовать исключениям типа суперкласса,
class ExcDemo5 {
    public static void main(String args[]) {
        // Здесь массив numer длиннее массива denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.length; i++)   {
            try {
                System.out.println(numer[i] + " / " +
                                   denom[i] + " is " +
                                   numer[i]/denom[i]);
            }
            // Перехват исключения от подкласса.
            catch (ArraylndexOutOfBoundsException exc) {
                System.out.println("No matching element found.");
            }
            // Перехват исключения от суперкласса.
            catch (Throwable exc) {
                System.out.println("Some exception occurred.");
            }
        }
    }
}

Ниже приведен результат выполнения данной программы.

4 / 2 is 2
Some exception occurred.
16 / 4 is 4
32 / 4 is 8
Some exception occurred.
128 / 8 is 16
No matching element found.
No matching element found.

В данном случае оператор catch (Throwable) перехватывает все исключения, кроме ArraylndexOutOfBoundsException. Соблюдение правильного порядка следования операторов catch приобретает особое значение в том случае, когда исключения генерируются в самой программе.

Вложенные блоки try

Блоки try могут быть вложенными друг в друга. Исключение, возникшее во внутреннем блоке try и не перехваченное связанным с ним блоком catch, распростра¬няется далее во внешний блок try и обрабатывается связанным с ним блоком catch. Такой порядок обработки исключений демонстрируется в приведенном ниже примере программы, где исключение ArraylndexOutOfBoundsException не перехватывается во внутреннем блоке catch, но обрабатывается во внешнем.

// Применение вложенных блоков try.
class NestTrys {
    public static void main(String args[]) {
        // Массив numer длиннее, чем массив denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };

        // Вложенные блоки try.
        try { // Внешний блок try.
            for(int i=0; i<numer.length; i++)   {
                try { // Внутренний блок try.
                    System.out.println(numer[i] + " / " +
                                       denom[i] + " is " +
                                       numer[i]/denom[i]) ;
                }
                catch (ArithmeticException exc) {
                    // перехватить исключение
                    System.out.println("Can't divide by Zero!");
                }
            }
        }
        catch (ArraylndexOutOfBoundsException exc) {
            // перехватить исключение
            System.out.println("No matching element found.");
            System.out.println("Fatal error - program terminated.");
        }
    }
}

Выполнение этой программы может дать, например, следующий результат:

4 / 2 is 2
Can't divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
No matching element found.
Fatal error - program terminated.

В данном примере исключение, которое может быть обработано во внутреннем блоке try (в данном случае ошибка деления на нуль), не мешает дальнейшему выполнению программы. А вот ошибка превышения границ массива перехватывается во внешнем блоке try, что приводит к аварийному завершению программы.

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

Генерирование исключений

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

где объект_исключения должен быть объектом класса, производного от класса Throwable.

Ниже приведен пример программы, демонстрирующий применение оператора throw. В этой программе исключение ArithmeticException генерируется вручную.

// Генерирование исключения вручную,
class ThrowDemo {
    public static void main(String args[])  {
        try {
            System.out.println("Before throw.");
                // Генерирование исключения.
                throw new ArithmeticException() ;
        }
        catch (ArithmeticException exc) {
            // перехватить исключение
            System.out.println("Exception caught.");
        }

        System.out.println("After try/catch block.");
    }
}

Выполнение этой программы дает следующий результат:

Before throw.
Exception caught.
After try/catch block.

Обратите внимание на то, что исключение ArithmeticException генерируется с помощью ключевого слова new в операторе throw. Дело в том, что оператор throw генерирует исключение в виде объекта. Поэтому после ключевого слова throw недостаточно указать только тип исключения, нужно еще создать объект для этой цели.

Повторное генерирование исключений

Исключение, перехваченное блоком catch, может быть повторно сгенерировано для обработки другим аналогичным блоком. Чаще всего повторное генерирование исключений применяется с целью предоставить разным обработчикам доступ к исключению. Так, например, повторное генерирование имеет смысл в том случае, если один обработчик оперирует одним свойством исключения, а другой обработчик ориентирован на другое его свойство. Повторно сгенерированное исключение не может быть перехвачено тем же самым блоком catch. Оно распространяется в другие блоки catch.

Ниже приведен пример программы, демонстрирующий повторное генерирование исключений.

//•Повторное генерирование исключений,
class Rethrow {
    public static void genException()   {
        // Массив numer длиннее маесивв denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.length; i++)   {
            try {
                System.out.println(numer[i] + " / " +
                                   denom[i] + " is " +
                                   numer[i]/denom[i]);
            }
            catch (ArithmeticException exc) {
                // перехватить исключение
                System.out.println("Can11 divide by Zero!");
            }
            catch (ArraylndexOutOfBoundsException exc) {
                // перехватить исключение
                System.out.println("No matching element found.");
                throw exc; // Повторное генерирование исключения.
            }
        }
    }
}

class RethrowDemo {
    public static void main(String args[])  {
        try {
            Rethrow.genException();
        }
        catch(ArraylndexOutOfBoundsException exc) {
            // Перехват повторно сгенерированного включения.
            System.out.println("Fatal error - " +
                               "program terminated.");
        }
    }
}

В данной программе ошибка деления на нуль обрабатывается локально в методе genException(), а при попытке обращения за границы массива исключение генерируется повторно. На этот раз оно перехватывается в методе main().

Подробнее о классе Throwable

В приведенных до сих примерах программ только перехватывались исключения, но не выполнялось никаких действий над представляющими их объектами. В выражении оператора catch указываются тип исключения и параметр, принимающий объект исключения. А поскольку все исключения представлены подклассами, производными от класса Throwable, то они поддерживают методы, определенные в этом классе. Некоторые наиболее употребительные методы из класса Throwable приведены в табл. 9.1.

Таблица 9.1. Наиболее употребительные методы из класса Throwable

Метод Описание
Throwable filllnStackTrace() Возвращает объект типа Throwable, содержащий полную трассировку стека исключений. Этот объект пригоден для повторного генерирования исключений
String getLocalizedMessage() Возвращает описание исключения, локализованное по региональным стандартам
String getMessage() Возвращает описание исключения
void printStackTrace() Выводит трассировку стека исключений
void printStackTrace(PrintStream stream) Выводит трассировку стека исключений в указанный поток
void printStackTrace(PrintWriter stream) Направляет трассировку стека исключений в указанный поток
String toString() Возвращает объект типа String, содержащий полное описание исключения. Этот метод вызывается из метода println() при выводе объекта типа Throwable

Среди методов, определенных в классе Throwable, наибольший интерес представляют методы pr intStackTrace() и toString(). С помощью метода printStackTrace() можно вывести стандартное сообщение об ошибке и запись последовательности вызовов методов, которые привели к возникновению исключения, А метод toString() позволяет получить стандартное сообщение об ошибке. Этот метод также вызывается в том случае, когда объект исключения передается в качестве параметра методу println(). Применение этих методов демонстрируется в следующем примере программы:

// Применение методов из класса Throwable.
class ExcTest {
    static void genException()  {
        int nums[] = new int[4];

        System.out.println("Before exception is generated.");

        // сгенерировать исключение в связи с попыткой
        // обращения за границы массива
        nums[7] = 10;
        System.out.println("this won't be displayed");
    }
}

class UseThrowableMethods {
    public static void main(String args[])  {

        try {
            ExcTest.genException() ;
        }
        catch (ArraylndexOutOfBoundsException exc) {
            // перехватить исключение
            System.out.println("Standard message is: ");
            System.out.println(exc) ;
            System.out.println("nStack trace: ");
            exc.printStackTrace();
        }

        System.out.println("After catch statement.");
    }
}

Результат выполнения данной программы выглядит следующим образом:

Before exception is generated.
Standard message is:
java.lang.ArraylndexOutOfBoundsException: 7

Stack trace:
java.lang.ArraylndexOutOfBoundsException: 7
    at ExcTest.genException(UseThrowableMethods.java:10)
    at UseThrowableMethods.main(UseThrowableMethods.java:19)
After catch statement.

Использование ключевого слова finally

Иногда требуется определить кодовый блок, который должен выполняться по завершении блока try/catch. Допустим, в процессе работы программы возникло исключение, требующее ее преждевременного завершения. Но в программе открыт файл или установлено сетевое соединение, а следовательно, файл нужно закрыть, а соединение разорвать. Для выполнения подобных операций нормального завершения программы удобно воспользоваться ключевым словом finally.

Для того чтобы определить код, который должен выполняться по завершении блока try/catch, нужно указать блок finally в конце последовательности операторов try/catch. Ниже приведена общая форма записи блока try/catch вместе с блоком finally.

try {
    // Блок кода, в котором отслеживаются ошибки.
}
catch (тип_исключения_1 объект_исключения) {
    // Обработчик исключения тип_исключения_1
}
catch (тип_исключения_2 объект_исключения) {
    // Обработчик исключения тип_исключения_2
}
//. . .
finally {
// Код блока finally
}

Блок finally выполняется всегда по завершении блока try/catch независимо от того, какое именно условие к этому привело. Следовательно, блок finally получит управление как при нормальной работе программы, так и при возникновении ошибки. Более того, он будет вызван даже в том случае, если в блоке try или в одном из блоков catch будет присутствовать оператор return для немедленного возврата из метода.

Ниже приведен краткий пример программы, демонстрирующий применение блока finally.

// Применение блока finally,
class UseFinally {
    public static void genException(int what) {
        int t;
        int nums[] = new int[2];

        System.out.println("Receiving " + what);
        try {
            switch(what) {
            case 0:
                t = 10 / what; // сгенерировать ошибку деления на нуль
                break;
            case 1:
                nums[4] = 4; // сгенерировать ошибку обращения к массиву
                break;
            case 2:
                return; // возвратиться из блока try
            }
        }
        catch (ArithmeticException exc) {
            // перехватить исключение
            System.out.println("Can1t divide by Zero!");
            return; // возвратиться из блока catch
        }
        catch (ArraylndexOutOfBoundsException exc) {
            // перехватить исключение
            System.out.println("No matching element found.");
        }
        // Этот блок выполняется независимо от того, каким
        // образом завершается блок try/catch.
        finally {
            System.out.println("Leaving try.");
        }
    }
}

class FinallyDemo {
    public static void main(String args[]) {

        for(int i=0; i < 3; i++) {
            UseFinally.genException(i);
            System.out.println() ;
        }
    }
}

В результате выполнения данной программы получается следующий результат:

Receiving О
Can't divide by Zero!
Leaving try.

Receiving 1
No matching element found.
Leaving try.

Receiving 2
Leaving try.

Нетрудно заметить, что блок finally выполняется независимо от того, каким об¬
разом завершается блок try/catch.

Использование ключевого слова throws

Иногда исключения нецелесообразно обрабатывать в том методе, в котором они возникают. В таком случае их следует указывать с помощью ключевого слова throws. Ниже приведена общая форма объявления метода, в котором присутствует ключевое слово throws.

возвращаемый_тип имя_метода(список_параметров) throws список_исключений {
    // Тело метода
}

В списке исключений через запятую указываются исключения, которые может генерировать метод.

Возможно, вам покажется странным, что в ряде предыдущих примеров ключевое слово throws не указывалось при генерировании исключений за пределами методов. Дело в том, что исключения, генерируемые подклассом Error или RuntimeException, можно и не указывать в списке оператора throws. Исполняющая система Java по умолчанию предполагает, что метод может их генерировать. А исключения всех остальных типов следует непременно объявить с помощью ключевого слова throws. Если этого не сделать, возникнет ошибка при компиляции.

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

throws java.io.IOException

Теперь вы знаете, зачем это было нужно. При вводе данных может возникнуть исключение IOException, а на тот момент вы еще не знали, как оно обрабатывается. Поэтому мы и указали, что исключение должно обрабатываться за пределами метода main(). Теперь, ознакомившись с исключениями, вы сможете без труда обработать исключение IOException самостоятельно.

Рассмотрим пример, в котором осуществляется обработка исключения IOException. В методе prompt() отображается сообщение, а затем выполняется ввод символов с клавиатуры. Такой ввод данных может привести к возникновению исключения IOException. Но это исключение не обрабатывается в методе prompt(). Вместо этого в объявлении метода указан оператор throws, т.е. обязанности по обработке данного исключению поручаются вызывающему методу. В данном случае вызывающим является метод main(), в котором и перехватывается исключение.

// Применение ключевого слова throws,
class ThrowsDemo {
    // Обратите внимание на оператор throws в объявлении метода.
    public static char prompt(String str)
        throws java.io.IOException {

        System.out.print(str + ": ");
        return (char) System.in.read() ;
    }

    public static void main(String args[]) {
        char ch;

        try {
            // В методе prompt() может быть сгенерировано исключение,
            // поэтому данный метод следует вызывать в блоке try.
            ch = prompt("Enter a letter");
        }
        catch(java.io.IOException exc) {
            System.out.println("I/O exception occurred.");
            ch = 'X';
        }
        System.out.println("You pressed " + ch);
    }
}

Обратите внимание на одну особенность приведенного выше примера. Класс IOException относится к пакету java. io. Как будет разъяснено в главе 10, в этом пакете содержатся многие языковые средства Java для организации ввода-вывода. Следовательно, пакет java.io можно импортировать, а в программе указать только имя класса IOException.

Новые средства обработки исключений, внедренные в версии JDK 7

С появлением версии JDK 7 механизм обработки исключений в Java был значительно усовершенствован благодаря внедрению трех новых средств. Первое из них поддерживает автоматическое управление ресурсами, позволяющее автоматизировать процесс освобождения таких ресурсов, как файлы, когда они больше не нужны. В основу этого средства положена расширенная форма оператора try, называемая оператором try с ресурсами и описываемая в главе 10 при рассмотрении файлов. Второе новое средство называется многократным перехватом, а третье — окончательным или более точным повторным генерированием исключений. Два последних средства рассматриваются ниже.

Многократный перехват позволяет перехватывать два или более исключения одним оператором catch. Как пояснялось ранее, после оператора try можно (и даже принято) указывать два или более оператора catch. И хотя каждый блок оператора catch, как правило, содержит свою особую кодовую последовательность, нередко в двух или более блоках оператора catch выполняется одна и та же кодовая последовательность, несмотря на то, что в них перехватываются разные исключения. Вместо того чтобы перехватывать каждый тип исключения в отдельности, теперь можно воспользоваться единым блоком оператора catch для обработки исключений, не дублируя код.

Для организации многократного перехвата следует указать список исключений в одном операторе catch, разделив их типы оператором поразрядного ИЛИ. Каждый параметр многократного перехвата неявно указывается как final. (По желанию модификатор доступа final можно указать и явным образом, но это совсем не обязательно.) А поскольку каждый параметр многократного перехвата неявно указывается как final, то ему нельзя присвоить новое значение.

В приведенной ниже строке кода показывается, каким образом многократный перехват исключений ArithmeticException и ArraylndexOutOfBoundsException указывается в одном операторе catch.

catch(final ArithmeticException | ArraylndexOutOfBoundsException e) {

Ниже приведен краткий пример программы, демонстрирующий применение многократного перехвата исключений.

// Применение средства многократного перехвата исключений.
// Примечание: для компиляции этого кода требуется JDK 7
// или более поздняя версия данного комплекта,
class MultiCatch {
    public static void main(String args[]) {
        int a=88, b=0;
        int result;
        char chrs[] = { 'А', 'В', 'C' };

        for(int i=0; i < 2; i++)    {
            try {
                if (i == 0)
                    // сгенерировать исключение ArithmeticException
                    result = а / b;
                else
                    // сгенерировать исключение ArraylndexOutOfBoundsException
                    chrs[5] = 'X';
            }
            // В этом операторе catch организуется перехват обоих исключений,
            catch(ArithmeticException | ArraylndexOutOfBoundsException е) {
                System.out.println("Exception caught: " + e);
            }
        }

        System.out.println("After multi-catch.");
    }
}

В данном примере программы исключение ArithmeticException генерируется при попытке деления на нуль, а исключение ArraylndexOutOfBoundsException — при попытке обращения за границы массива chrs. Оба исключения перехватываются одним оператором catch.

Средство более точного повторного генерирования исключений ограничивает этот процесс лишь теми проверяемыми типами исключений, которые генерируются в соответствующем блоке try и не обрабатываются в предыдущем блоке оператора catch, а также относятся к подтипу или супертипу указываемого параметра. И хотя такая возможность требуется нечасто, ничто не мешает теперь воспользоваться ею в полной мере. А для организации окончательного повторного генерирования исключений параметр оператора catch должен быть, по существу, указан как final. Это означает, что ему нельзя присвоить новое значение в блоке catch. Он может быть указан как final явным образом, хотя это и не обязательно.

Встроенные в Java исключения

В стандартном пакете java. lang определены некоторые классы, представляющие стандартные исключения Java. Часть из них использовалась в предыдущих примерах программ. Наиболее часто встречаются исключения из подклассов стандартного класса RuntimeException. А поскольку пакет java. lang импортируется по умолчанию во все программы на Java, то исключения, производные от класса RuntimeException, становятся доступными автоматически. Их даже обязательно включать в список оператора throws. В терминологии языка Java такие исключения называют непроверяемыми, поскольку компилятор не проверяет, обрабатываются или генерируются подобные исключения в методе. Непроверяемые исключения, определенные в пакете java.lang, приведены в табл. 9.2, тогда как в табл. 9.3 — те исключения из пакета j ava. lang, которые следует непременно включать в список оператора throws при объявлении метода, если, конечно, в методе содержатся операторы, способные генерировать эти исключения, а их обработка не предусмотрена в теле метода. Такие исключения принято называть проверяемыми. В Java предусмотрен также ряд других исключений, определения которых содержатся в различных библиотеках классов. К их числу можно отнести упоминавшееся ранее исключение IOException.

Таблица 9.2. Непроверяемые исключения, определенные в пакете java.lang

Исключение Описание
ArithmeticException Арифметическая ошибка, например попытка деления на нуль
ArraylndexOutOfBoundsException Попытка обращения за границы массива
ArrayStoreException Попытка ввести в массив элемент, несовместимый с ним по типу
ClassCastException Недопустимое приведение типов
EnumConstNotPresentException Попытка использования нумерованного значения, которое не было определено ранее
IllegalArgumentException Недопустимый параметр при вызове метода
IllegalMonitorStateException Недопустимая операция контроля, например, ожидание разблокировки потока
IllegalStateException Недопустимое состояние среды выполнения или приложения
IllegalThreadStateException Запрашиваемая операция несовместима с текущим состоянием потока
IndexOutOfBoundsException Недопустимое значение индекса
NegativeArraySizeException Создание массива отрицательного размера
NullPointerException Недопустимое использование пустой ссылки
NumberFormatException Неверное преобразование символьной строки в число
SecurityException Попытка нарушить систему защиты
StringlndexOutOfBounds Попытка обращения к символьной строке за ее границами
TypeNotPresentException Неизвестный тип
UnsupportedOperationException Неподдерживаемая операция

Таблица 9.3. Проверяемые исключения, определенные в пакете java.lang

Исключение Описание
ClassNotFoundException Класс не найден
CloneNotSupportedException Попытка клонирования объекта, не реализующего интерфейс Cloneable
IllegalAccessException Доступ к классу запрещен
InstantiationException Попытка создания объекта абстрактного класса или интер¬фейса
InterruptedException Прерывание одного потока другим
NoSuchFieldException Требуемое поле не существует
NoSuchMethodException Требуемый метод не существует
ReflectiveOperationException Суперкласс исключений, связанных с рефлексией (добавлен в версии JDK 7)

Создание подклассов, производных от класса Exception

Несмотря на то что встроенные в Java исключения позволяют обрабатывать большинство ошибок, механизм обработки исключений не ограничивается только этими ошибками. В частности, можно создавать исключения для обработки потенциальных ошибок в прикладной программе. Создать исключение несложно. Для этого достаточно определить подкласс, производный от класса Exception, который, в свою очередь, является подклассом, порожденным классом Throwable. В создаваемый подкласс не обязательно включать реализацию каких-то методов. Сам факт существования такого подкласса позволяет использовать его в качестве исключения.

В классе Exception не определены новые методы. Он лишь наследует методы, предоставляемые классом Throwable. Таким образом, все исключения, включая и создаваемые вами, содержат методы класса Throwable. Конечно же, вы вольны переопределить в создаваемом вами классе один или несколько методов.

Ниже приведен пример, в котором создается исключение NonlntResultException. Оно генерируется в том случае, если результатом деления двух целых чисел является дробное число. В классе NonlntResultException содержатся два поля, предназначенные для хранения целых чисел, а также конструктор. В нем также переопределен метод toString(), что дает возможность выводить описание исключения с помощью метода println().

// Применение специально создаваемого исключения.
// создать исключение
class NonlntResultException extends Exception {
    int n;
    int d;

    NonlntResultException(int i, int j) {
        n = i;
        d = j;
    }

    public String toString()    {
        return "Result of " + n + " / " + d +
                " is non-integer.";
    }
}

class CustomExceptDemo {
    public static void main(String args[]) {

        // В массиве numer содержатся нечетные числа,
        int numer[] = { 4, 8, 15, 32, 64, 127, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.length; i++)   {
            try {
                if((numer[i]%2) != 0)
                    throw new
                        NonlntResultException(numer[i], denom[i]);
                System.out.println(numer[i] + " / " +
                                   denom[i] + 11 is " +
                                   numer[i]/denom[i]);
            }
            catch (ArithmeticException exc) {
                // перехватить исключение
                System.out.println("Can11 divide by Zero!");
            }
            catch (ArraylndexOutOfBoundsException exc) {
                // перехватить исключение
                System.out.println("No matching element found.");
            }
            catch (NonlntResultException exc) {
                System.out.println(exc) ;
            }
        }
    }
}

Результат выполнения данной программы выглядит следующим образом:

4 / 2 is 2
Can't divide by Zero!
Result of 15 / 4 is non-integer.
32 / 4 is 8
Can't divide by Zero!
Result of 127 / 8 is non-integer.
No matching element found.
No matching element found.

Пример для опробования 9.1.
Добавление исключений в класс очереди

В этом проекте предстоит создать два класса исключении, которые будут использоваться классом очереди, разработанным в примере для опробования 8.1. Эти исключения должны указывать на переполнение и опустошение очереди, а генерировать их будут методы put() и get() соответственно. Ради простоты эти исключения добавляются в класс FixedQueue, но вы можете без труда внедрить их в любые другие классы очереди, разработанные в примере для опробования 8.1.

Последовательность действий

  1. Создайте файл QExcDemo.java.
  2. Определите следующие исключения в файле QExcDemo.java:
    /*
    Пример для опробования 9.1.
    Добавление обработчиков исключений в класс очереди.
    */
    // Исключение, указывающее на переполнение очереди,
    class QueueFullException extends Exception {
        int size;
    
        QueueFullException(int s) { size = s; }
    
        public String toString()    {
            return "nQueue is full. Maximum size is " + size;
        }
    }
    
    // Исключение, указывающее на опустошение очереди,
    class QueueEmptyException extends Exception {
        public String toString()    {
            return "nQueue is empty.";
        }
    }
    

    Исключение QueueFullException генерируется при попытке поместить элемент в уже заполненную очередь, а исключение QueueEmptyException — в ответ на попытку извлечь элемент из пустой очереди.

  3. Измените класс FixedQueue таким образом, чтобы при возникновении ошибки он генерировал исключение. Соответствующий код приведен ниже. Введите этот код в файл QExcDemo.java.
    // Класс, реализующий очередь фиксированного размера
    // для хранения символов.
    class FixedQueue implements ICharQ {
        private char q[]; // Массив для хранения элементов очереди,
        private int putloc, getloc; // Индексы размещения и извлечения
    
        // элементов очереди.
        // создать пустую очередь заданного размера
        public FixedQueue(int size) {
            q = new char[size+1]; // выделить память для очереди
            putloc = getloc = 0;
        }
    
        // поместить символ в очередь
        public void put(char ch)
        throws QueueFullException {
    
            if(putloc==q.length-1)
                throw new QueueFullException(q.length-1);
    
            putloc++;
            q[putloc] = ch;
        }
    
        // извлечь символ из очереди
        public char get()
        throws QueueEmptyException {
    
            if(getloc == putloc)
                throw new QueueEmptyException();
    
            getloc++;
            return q[getloc];
        }
    }
    

    Добавление исключений в класс FixedQueue выполняется в два этапа. Сначала в определении методов get() и put() указывается оператор throws с типом генерируемого исключения. А затем в этих методах организуется генерирование исключений при возникновении ошибок. Используя исключения, можно организовать обработку ошибок в вызывающей части программы наиболее рациональным способом. Как вы помните, в предыдущих версиях рассматриваемой здесь программы выводились только сообщения об ошибках. А генерирование исключений является более профессиональным подходом к разработке данной программы.

  4. Для опробования усовершенствованного класса FixedQueue введите в файл QExcDemo.java приведенный ниже исходный код класса QExcDemo.
    // Демонстрация исключений при обращении с очередью,
    class QExcDemo {
        public static void main(String args[])  {
            FixedQueue q = new FixedQueue(10);
            char ch;
            int i;
    
            try {
                // Переполнение очереди.
                for(i=0; i < 11; i++)   {
                    System.out.print("Attempting to store : " +
                                     (char) ('A' + i));
                    q.put((char) (fA' + i));
                    System.out.println(" - OK");
                }
                System.out.println();
            }
            catch (QueueFullException exc) {
                System.out.println(exc);
            }
            System.out.println();
    
            try {
                // Попытка извлечь символ из пустой очереди.
                for(i=0; i < 11; i++) {
                System.out.print("Getting next char: ");
                ch = q.get();
                System.out.println(ch);
                }
            }
            catch (QueueEmptyException exc) {
                System.out.println(exc);
            }
        }
    }
    
  5. Класс FixedQueue реализует интерфейс ICharQ, в котором определены методы get() и put(), и поэтому интерфейс ICharQ необходимо изменить таким образом, чтобы в нем отражалось наличие операторов throws. Ниже приведен видоизмененный соответственно код интерфейса ICharQ. Не забывайте о том, что он должен храниться в файле ICharQjava.
    // Интерфейс очереди для хранения символов с генерированием исключений,
    public interface ICharQ {
        // поместить символ в очередь
        void put(char ch) throws QueueFullException;
        // извлечь символ из очереди
        char get() throws QueueEmptyException;
    }
    
  6. Скомпилируйте сначала новую версию исходного файла IQChar. j ava, а затем исходный файл QExcDemo. java и запустите программу QExcDemo на выполнение. В итоге вы получите следующий результат ее выполнения:
    Attempting to store A - OK
    Attempting to store В - OK
    Attempting to store С - OK
    Attempting to store D - OK
    Attempting to store E - OK
    Attempting to store F - OK
    Attempting to store G - OK
    Attempting to store H - OK
    Attempting to store I - OK
    Attempting to store J - OK
    Attempting to store К
    Queue is full. Maximum size is 10
    
    Getting next char: A
    Getting next char: В
    Getting next char: С
    Getting next char: D
    Getting next char: E
    Getting next char: F
    Getting next char: G
    Getting next char: H
    Getting next char: I
    Getting next char: J
    Getting next char:
    Queue is empty.
    

Упражнение для самопроверки по материалу главы 9

  1. Какой класс находится на вершине иерархии исключений?
  2. Объясните вкратце, как пользоваться ключевыми словами try и catch?
  3. Какая ошибка допущена в приведенном ниже фрагменте кода?
    // ...
    vals[18] = 10;
    catch (ArraylndexOutOfBoundsException exc) {
        // обработать ошибку
    }
    
  4. Что произойдет, если исключение не будет перехвачено?
  5. Какая ошибка допущена в приведенном ниже фрагменте кода?
    class A extends Exception { ...
    class В extends А { ...
        // ...
    try {
        // ...
    }
    catch (A exc) { ... }
    catch (В exc) { ... }
    
  6. Может ли внутренний блок catch повторно генерировать исключение, которое будет обработано во внешнем блоке catch?
  7. Блок finally — последний фрагмент кода, выполняемый перед завершением программы. Верно или неверно? Обоснуйте свой ответ.
  8. Исключения какого типа необходимо явно объявлять с помощью оператора throws, включаемого в объявление метода?
  9. Какая ошибка допущена в приведенном ниже фрагменте кода?
    class MyClass { // ... }
    // ...
    throw new MyClass();
    
  10. Отвечая на вопрос 3 упражнения для самопроверки по материалу главы 6, вы создали класс Stack. Добавьте в него специальные исключения для реагирования на попытку поместить элемент в переполненный стек и извлечь элемент из пустого стека.
  11. Какими тремя способами можно сгенерировать исключение?
  12. Назовите два подкласса, производных непосредственно от класса Throwable.
  13. Что такое многократный перехват?
  14. Следует ли перехватывать в программе исключения типа Error?

Исключение (exception) — это ненормальная ситуация (термин «исключение» здесь следует понимать как «исключительная ситуация»), возникающая во время выполнения программного кода. Иными словами, исключение — это ошибка, возникающая во время выполнения программы (в runtime).

Исключение — это способ системы Java (в частности, JVM — виртуальной машины Java) сообщить вашей программе, что в коде произошла ошибка. К примеру, это может быть деление на ноль, попытка обратиться к массиву по несуществующему индексу, очень распространенная ошибка нулевого указателя (NullPointerException) — когда вы обращаетесь к ссылочной переменной, у которой значение равно null и так далее.

В любом случае, с формальной точки зрения, Java не может продолжать выполнение программы.

Обработка исключений (exception handling) — название объектно-ориентированной техники, которая пытается разрешить эти ошибки.

Программа в Java может сгенерировать различные исключения, например:

  • программа может пытаться прочитать файл из диска, но файл не существует;

  • программа может попытаться записать файл на диск, но диск заполнен или не отформатирован;

  • программа может попросить пользователя ввести данные, но пользователь ввел данные неверного типа;

  • программа может попытаться осуществить деление на ноль;

  • программа может попытаться обратиться к массиву по несуществующему индексу.

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

Преимущество обработки исключений заключается в том, что она предусматривает автоматическую реакцию на многие ошибки, избавляя от необходимости писать вручную соответствующий код.

В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable имеются два непосредственных подкласса: Exception и Error.

Исключения типа Error относятся к ошибкам, возникающим в виртуальной машине Java, а не в прикладной программе. Контролировать такие исключения невозможно, поэтому реакция на них в приложении, как правило, не предусматривается. В связи с этим исключения данного типа не будут рассматриваться в книге.

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

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


Так как в Java
ВСЁ ЯВЛЯЕТСЯ ОБЪЕКТОМ, то исключение тоже является объектом некоторого класса, который описывает исключительную ситуацию, возникающую в определенной части программного кода.

«Обработка исключений» работает следующим образом:

  • когда возникает исключительная ситуация, JVM генерирует (говорят, что JVM ВЫБРАСЫВАЕТ исключение, для описания этого процесса используется ключевое слово throw) объект исключения и передает его в метод, в котором произошло исключение;

  • вы можете перехватить исключение (используется ключевое слово catch), чтобы его каким-то образом обработать. Для этого, необходимо определить специальный блок кода, который называется обработчиком исключений, этот блок будет выполнен при возникновении исключения, код должен содержать реакцию на исключительную ситуацию;

  • таким образом, если возникнет ошибка, все необходимые действия по ее обработке выполнит обработчик исключений.

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

Рассмотрим пример исключения и реакцию стандартного обработчика Java.

public static void main(String[] args) {

System.out.println(5 / 0);

Мы видим, что стандартный обработчик вывел в консоль сообщение об ошибке. Давайте разберемся с содержимым этого сообщения:

«C:Program FilesJavajdk1.8.0_60binjava»

Exception in thread «main» java.lang.ArithmeticException: / by zero

at ua.opu.Main.main(Main.java:6)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:497)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Process finished with exit code 1

Exception in thread «main» java.lang.ArithmeticException: / by zero

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

at ua.opu.Main.main(Main.java:6)

в каком классе, методе и строке произошло исключение. Используя эту информацию, мы можем найти ту строчку кода, которая привела к исключительной ситуации, и предпринять какие-то действия. Строки

at ua.opu.Main.main(Main.java:6)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:497)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

называются «трассировкой стека» (stack tracing). О каком стеке идет речь? Речь идет о стеке вызовов (call stack). Соответственно, эти строки означают последовательность вызванных методов, начиная от метода, в котором произошло исключение, заканчивая самым первым вызванным методом.

Для вызова методов в программе используется инструкция «call». Когда вы вызываете метод в программе, важно сохранить адрес следующей инструкции, чтобы, когда вызванный метод отработал, программа продолжила работу со следующей инструкции. Этот адрес нужно где-то хранить в памяти. Также перед вызовом необходимо сохранить аргументы функции, которые тоже необходимо где-то хранить.

Вся эта информация хранится в специальной структуре – стеке вызовов. Каждая запись в стеке вызовов называется кадром или фреймом (stack frame).

Таким образом, зная, какая строка привела к возникновению исключения, вы можете изменить код либо предусмотреть обработчик событий.

Как уже было сказано выше, исключение это объект некоторого класса. В Java существует разветвленная иерархия классов исключений.

В Java, класс исключения служит для описания типа исключения. Например, класс NullPointerException описывает исключение нулевого указателя, а FileNotFoundException означает исключение, когда файл, с которым пытается работать приложение, не найден. Рассмотрим иерархию классов исключений:

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

От класса Throwable наследуются классы Error и Exception. Среди подклассов Exception отдельно выделен класс RuntimeException, который играет важную роль в иерархии исключений.

В Java существует некоторая неопределенность насчет того – существует ли два или три вида исключений.

Если делить исключения на два вида, то это:

  1. 1.

    контролируемые исключения (checked exceptions) – подклассы класса Exception, кроме подкласса RuntimeException и его производных;

  2. 2.

    неконтролируемые исключения (unchecked exceptions) – класс Error с подклассами, а также класс RuntimeException и его производные;

В некоторых источниках класс Error и его подклассы выделяют в отдельный вид исключений — ошибки (errors).

Далее мы видим класс Error. Классы этой ветки составляют вид исключений, который можно обозначить как «ошибки» (errors). Ошибки представляют собой серьезные проблемы, которые не следует пытаться обработать в собственной программе, поскольку они связаны с проблемами уровня JVM.

На самом деле, вы конечно можете предпринять некоторые действия при возникновении ошибок, например, вывести сообщение для пользователя в удобном формате, выслать трассировку стека себе на почту, чтобы понять – что вообще произошло.

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

В качестве примеров «ошибок» можно привести: переполнение стека вызова (класс StackOverflowError); нехватка памяти в куче (класс OutOfMemoryError), вследствие чего JVM не может выделить память под новый объект и сборщик мусора не помогает; ошибка виртуальной машины, вследствие которой она не может работать дальше (класс VirtualMachineError) и так далее.

Несмотря на то, что в нашей программе мы никак не можем помочь этой проблеме, и приложение не может работать дальше (ну как может работать приложение, если стек вызовов переполнен или JVM не может дальше выполнять код?!); знание природы этих ошибок поможет вам предпринять некоторые действия, чтобы избежать этих ошибок в дальнейшем. Например, ошибки типа StackOverflowError и OutOfMemoryError могут быть следствием вашего некорректного кода.

Например, попробуем спровоцировать ошибку StackOverflowError

public static void main(String[] args) {

public static void methodA() {

private static void methodB() {

Получим такое сообщение об ошибке

Exception in thread «main» java.lang.StackOverflowError

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

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

Exception in thread «main» java.lang.OutOfMemoryError: Java heap space

at java.base/java.util.Arrays.copyOf(Arrays.java:3511)

at java.base/java.util.Arrays.copyOf(Arrays.java:3480)

at java.base/java.util.ArrayList.grow(ArrayList.java:237)

at java.base/java.util.ArrayList.grow(ArrayList.java:244)

at java.base/java.util.ArrayList.add(ArrayList.java:454)

at java.base/java.util.ArrayList.add(ArrayList.java:467)

at com.company.Main.main(Main.java:13)

Process finished with exit code 1

Ошибка VirtualMachineError может означать, что следует переустановить библиотеки Java.

В любом случае, следует относиться к типу Error не как к неизбежному злу и «воле богов», а просто как к сигналу к тому, что в вашем приложении что-то не так, или что-то не так с программным или аппаратным обеспечением, которое вы используете.

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

В классе Exception описаны исключения двух видов: контролируемые исключения (checked exceptions) и неконтролируемые исключения (unchecked exceptions).

Неконтролируемые исключения содержатся в подклассе RuntimeException и его наследниках. Контролируемые исключения содержатся в остальных подклассах Exception.

В чем разница между контролируемыми и неконтролируемыми исключениями, мы узнаем позже, а теперь рассмотрим вопрос – а как же именно нам обрабатывать исключения?

Обработка исключений в методе может выполняться двумя способами:

  1. 1.

    с помощью связки try-catch;

  2. 2.

    с помощью ключевого слова throws в сигнатуре метода.

Рассмотрим оба метода поподробнее:

Способ 1. Связка try-catch

Этот способ кратко можно описать следующим образом.

Код, который теоретически может вызвать исключение, записывается в блоке try{}. Сразу за блоком try идет блок код catch{}, в котором содержится код, который будет выполнен в случае генерации исключения. В блоке finally{} содержится код, который будет выполнен в любом случае – произошло ли исключение или нет.

Теперь разберемся с этим способом более подробно. Рассмотрим следующий пример – программу, которая складывает два числа, введенные пользователем из консоли

public static void main(String[] args) {

Scanner scanner = new Scanner(System.in);

System.out.println(«Введите первое число: «);

String firstNumber = scanner.nextLine();

System.out.println(«Введите второе число: «);

String secondNumber = scanner.nextLine();

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

System.out.println(«Результат: « + (a + b));

Первое, что нам нужно определить – и что является главным при работе с исключениями, КАКАЯ ИНСТРУКЦИЯ МОЖЕТ ПРИВЕСТИ К ВОЗНИКНОВЕНИЮ ИСКЛЮЧЕНИЯ?

То есть, мы должны понять – где потенциально у нас может возникнуть исключение? Понятно, что речь идет не об операции сложения и не об операции чтения данных из консоли. Потенциально опасными строчками кода здесь являются строчки

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

в которых происходит преобразование ввода пользователя в целое число (метод parseInt() преобразует цифры в строке в число).

Почему здесь может возникнуть исключение? Потому что пользователь может ввести не число, а просто какой-то текст и тогда непонятно – что записывать в переменную a или b. И да, действительно, если пользователь введет некорректное значение, возникнет исключение в методе Integer.parseInt().

Итак, что мы можем сделать. «Опасный код» нужно поместить в блок try{}

Обратите внимание на синтаксис блока try. В самом простом случае это просто ключевое слово try, после которого идут парные фигурные скобки. Внутри этих скобок и заключается «опасный» код, который может вызвать исключение. Сразу после блока try должен идти блок catch().

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

} catch (NumberFormatException e) {

// сохранить текст ошибки в лог

System.out.println(«Одно или оба значения некорректны!»);

System.out.println(«Результат: « + (a + b));

Обратите внимание на синтаксис блока catch. После ключевого слова, в скобках описывается аргумент с именем e типа NumberFormatException.

Когда произойдет исключение, то система Java прервет выполнение инструкций в блоке try и передаст управление блоку catch и запишет в этот аргумент объект исключения, который сгенерировала Java-машина.

То есть, как только в блоке try возникнет исключение, то дальше инструкции в блоке try выполняться не будут! А сразу же начнут выполняться действия в блоке catch.

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

Блок catch сработает только в том случае, если указанный в скобках тип объекта исключения будет суперклассом или будет того же типа, что и объект исключения, который сгенерировала Java.

Например, если в нашем примере мы напишем код, который потенциально может выбросить исключение типа IOException, но не изменим блок catch

} catch (NumberFormatException e) {

// сохранить текст ошибки в лог

System.out.println(«Одно или оба значения некорректны!»);

тогда обработчик не будет вызван и исключение будет обработано стандартным обработчиком Java.

Способ 2. Использование ключевого слова throws

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

Изменим наш пример и выделим в отдельный метод код, который будет запрашивать у пользователя число и возвращать его как результат работы метода

public static void main(String[] args) {

int a = getNumberFromConsole(«Введите первое число»);

int b = getNumberFromConsole(«Введите второе число»);

System.out.println(«Результат: « + (a + b));

public static int getNumberFromConsole(String message) {

Scanner scanner = new Scanner(System.in);

System.out.print(message + «: «);

String s = scanner.nextLine();

return Integer.parseInt(s);

Мы понимаем, что в данном методе может произойти исключение, но мы не хотим или не можем его обработать. Причины могут быть разными, например:

  1. 1.

    обработка исключений может происходить централизованно однотипным способом (например, показ окошка с сообщением и с определенным текстом);

  2. 2.

    это не входит в нашу компетенцию как программиста – обработкой исключений занимается другой программист;

  3. 3.

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

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

public static int getNumberFromConsole(String message) throws NumberFormatException {

Scanner scanner = new Scanner(System.in);

System.out.print(message + «: «);

String s = scanner.nextLine();

return Integer.parseInt(s);

Обратите внимание на расположение сигнатуру метода. Мы привыкли, что при объявлении метода сразу после скобок входных аргументов мы открываем фигурную скобку и записываем тело метода. Здесь же, после входных аргументов, мы пишем ключевое слово throws и потом указываем тип исключения, которое может быть сгенерировано в нашем методе. Если метод может выбрасывать несколько типов исключений, они записываются через запятую

public static void foo() throws NumberFormatException, ArithmeticException, IOException {

Тогда, в методе main мы должны написать примерно следующее

public static void main(String[] args) {

a = getNumberFromConsole(«Введите первое число»);

b = getNumberFromConsole(«Введите второе число»);

} catch (NumberFormatException e) {

// сохранить текст ошибки в лог

System.out.println(«Одно или оба значения некорректны!»);

System.out.println(«Результат: « + (a + b));

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

Отличия между контролируемыми и неконтролируемыми исключениями

Если вы вызываете метод, который выбрасывает checked исключение, то вы ОБЯЗАНЫ предусмотреть обработку возможного исключения, то есть связку try-catch.

Яркий пример checked исключения – класс IOException и его подклассы.

Рассмотрим пример – попробуем прочитать файл и построчно вывести его содержимое на экран консоли:

public static void main(String[] args) {

Path p = Paths.get(«c:\temp\file.txt»);

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

Как мы видим, компилятор не хочет компилировать наш код. Чем же он недоволен? У нас в коде происходит вызов двух методов – статического метода Files.newBufferedReader() и обычного метода BufferedReader.readLine().

Если посмотреть на сигнатуры этих методов то можно увидеть, что оба этих метода выбрасывают исключения типа IOException. Этот тип исключения относится к checked-исключению и поэтому, если вы вызываете эти методы, компилятор ТРЕБУЕТ от вас предусмотреть блок catch, либо в самом вашем методе указать throws IOException и, таким образом, передать обязанность обрабатывать исключение другому методу, который будет вызывать ваш.

Таким образом, «оборачиваем» наш код в блок try и пишем блок catch.

public static void main(String[] args) {

Path p = Paths.get(«c:\temp\file.txt»);

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

} catch (IOException e) {

System.out.println(«Ошибка при чтении файла!»);

Еще один способ — указать в сигнатуре метода, что он выбрасывает исключение типа IOException и переложить обязанность обработать ошибку в вызывающем коде

public static void main(String[] args) {

Path p = Paths.get(«c:\temp\file.txt»);

} catch (IOException e) {

System.out.println(«Ошибка при чтении файла!»);

public static void printFile(Path p) throws IOException {

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

Eсли метод выбрасывает checked-исключение, то проверка на наличие catch-блока происходит на этапе компиляции. И вы обязаны предусмотреть обработку исключения для checked-исключения.

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

Зачем необходимо наличие двух видов исключений?

В большинстве языков существует всего лишь один тип исключений – unchecked. Некоторые языки, например, C#, в свое время отказались от checked-исключений.

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

С другой стороны, зачем нужно делать некоторые типы исключений checked? Почему просто не сделать все исключения unchecked и оставить решения об обработке исключений целиком на совести программиста?

В официальной документации написано, что unchecked-исключения – это те исключения, от которых программа «не может восстановиться», тогда как checked-исключения позволяют откатить некоторую операцию и повторить ее снова.

На самом деле, если вы посмотрите на различные типы unchecked-исключений, то вы увидите, что большинство их связаны с ошибками самого программиста. Выход за пределы массива, исключение нулевого указателя, деление на ноль – большинство из подобного рода исключений целиком лежат на совести программистов. Тогда мы можем сказать, что лучше программист пишет более хороший код, чем везде вставляет проверки на исключения.

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

Вы можете написать идеальный код, но потом вы отдадите приложение пользователю, а он введет название файла, которого нет или напишет неправильный IP для сокет-соединения. Таким образом, мы заранее должны быть готовыми к неверным действиям пользователя или к программным или аппаратным проблемам на его стороне и в обязательном порядке предусмотреть обработку возможных исключений.

Дополнительно об исключениях

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

Java позволяет вам для одного блока try предусмотреть несколько блоков catch, каждый из которых должен обрабатывать свой тип исключения

public static void foo() {

} catch (ArithmeticException e) {

// обработка арифметического исключения

} catch (IndexOutOfBoundsException e) {

// обработка выхода за пределы коллекции

} catch (IllegalArgumentException e) {

// обработка некорректного аргумента

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

Так как вы можете указать как точный класс, так и суперкласс, то если первым блоком будет блок для суперкласса – выполнится он. Например, исключение FileNotFoundException является подклассом IOException. И поэтому если вы первым поставите блок с IOException – он будет вызываться для всех подтипов исключений, в том числе и для FileNotFoundException и блок c FileNotFoundException никогда не выполнится.

public static void main(String[] args) {

Path p = Paths.get(«c:\temp\file.txt»);

} catch (IOException e) {

System.out.println(«Ошибка при чтении файла!»);

} catch (FileNotFoundException e) {

// данный блок никогда не будет вызван

public static void printFile(Path p) throws IOException {

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

Один блок для обработки нескольких типов исключений

Начиная с версии Java 7, вы можете использовать один блок catch для обработки исключений нескольких, не связанных друг с другом типов. Приведем пример

public static void foo() {

} catch (ArithmeticException | IllegalArgumentException | IndexOutOfBoundsException e) {

// три типа исключений обрабатываются одинаково

Как мы видим, один блок catch используется для обработки и типа IOException и NullPointerException и NumberFormaException.

Вы можете использовать вложенные блоки try, которые могут помещаться в других блоках try. После вложенного блока try обязательно идет блок catch

public static void foo() {

} catch (IllegalArgumentException e) {

// обработка вложенного блока try

} catch (ArithmeticException e) {

Выбрасывание исключения с помощью ключевого слова throw

С помощью ключевого слова throw вы можете преднамеренно «выбросить» определенный тип исключения.

public static void foo(int a) {

throw new IllegalArgumentException(«Аргумент не может быть отрицательным!»);

Кроме блока try и catch существует специальный блок finally. Его отличительная особенность – он гарантированно отработает, вне зависимости от того, будет выброшено исключение в блоке try или нет. Как правило, блок finally используется для того, чтобы выполнить некоторые «завершающие» операции, которые могли быть инициированы в блоке try.

public static void foo(int a) {

FileOutputStream fout = null;

File file = new File(«file.txt»);

fout = new FileOutputStream(file);

} catch (IOException e) {

// обработка исключения при записи в файл

} catch (IOException e) {

При любом развитии события в блоке try, код в блоке finally отработает в любом случае.

Блок finally отработает, даже если в try-catch присутствует оператор return.

Как правило, блок finally используется, когда мы в блоке try работаем с ресурсами (файлы, базы данных, сокеты и т.д.), когда по окончании блока try-catch мы освобождаем ресурсы. Например, допустим, в процессе работы программы возникло исключение, требующее ее преждевременного закрытия. Но в программе открыт файл или установлено сетевое соединение, а, следовательно, файл нужно закрыть, а соединение – разорвать. Для этого удобно использовать блок finally.

Блок try-with-resources является модификацией блока try. Данный блок позволяет автоматически закрывать ресурс после окончания работы блока try и является удобной альтернативой блоку finally.

public static void foo() {

Path p = Paths.get(«c:\temp\file.txt»);

try (BufferedReader reader = Files.newBufferedReader(p)) {

while ((line = reader.readLine()) != null)

System.out.println(line);

} catch (IOException e) {

Внутри скобок блока try объявляется один или несколько ресурсов, которые после отработки блока try-catch будут автоматически освобождены. Для этого объект ресурса должен реализовывать интерфейс java.lang.AutoCloseable.

Создание собственных подклассов исключений

Встроенные в Java исключения позволяют обрабатывать большинство распространенных ошибок. Тем не менее, вы можете создавать и обрабатывать собственные типы исключений. Для того, чтобы создать класс собственного исключения, достаточно определить как его произвольный от Exception или от RuntimeException (в зависимости от того, хотите ли вы использовать checked или unchecked – исключения).

Насчет создания рекомендуется придерживаться двух правил:

  1. 1.

    определитесь, исключения какого типа вы хотите использовать для собственных исключений (checked или unchecked) и старайтесь создавать исключения только этого типа;

  2. 2.

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

Плохие практики при обработке исключений

Ниже представлены действия по обработке ошибок, которые характерны для плохого программиста. Ни в коем случае не рекомендуется их повторять!

  1. 1.

    Указание в блоке catch объекта исключения типа Exception. Существует очень большой соблазн при создании блока catch указать тип исключения Exception и, таким образом, перехватывать все исключения, которые относятся к этому классу (а это все исключения, кроме системных ошибок). Делать так крайне не рекомендуется, т.к. вместо того чтобы решать проблему с исключениями, мы фактически игнорируем ее и просто реализуем некоторую «заглушку», чтобы приложение продолжило работу дальше. Кроме того, каждый тип исключения должен быть обработан своим определенным образом.

  2. 2.

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

  3. 3.

    Игнорирование исключения. Следующий плохой прием состоит в том, что мы просто игнорируем исключение и оставляем блок catch пустым. Программа должна реагировать на исключения и должна информировать пользователя и разработчика о том, что что-то пошло не так. Безусловно, исключение это не повод тут же закрывать приложение, а попытаться повторить то действие, которое привело к исключению (например, повторно указать название файла, попытаться открыть базу данных через время и т.д.). В любом случае, когда приложение в ответ на ошибку никак не реагирует – не выдает сообщение, но и не делает того, чего от нее ожидали – это самый плохой вариант.

Catching and Handling Exceptions

Catching and Handling Exceptions

This section describes how to use the three exception handler components — the try, catch, and finally blocks — to write an exception handler. Then, the try-with-resources statement, introduced in Java SE 7, is explained. The try-with-resources statement is particularly suited to situations that use Closeable resources, such as streams.

The last part of this section walks through an example and analyzes what occurs during various scenarios.

The following example defines and implements a class named ListOfNumbers. When constructed, ListOfNumbers creates an ArrayList that contains 10 Integer elements with sequential values 0 through 9. The ListOfNumbers class also defines a method named writeList(), which writes the list of numbers into a text file called OutFile.txt. This example uses output classes defined in java.io, which are covered in the Basic I/O section.

// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class ListOfNumbers {

    private List<Integer> list;
    private static final int SIZE = 10;

    public ListOfNumbers () {
        list = new ArrayList<>(SIZE);
        for (int i = 0; i < SIZE; i++) {
            list.add(i);
        }
    }

    public void writeList() {
    // The FileWriter constructor throws IOException, which must be caught.
        PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));

        for (int i = 0; i < SIZE; i++) {
            // The get(int) method throws IndexOutOfBoundsException, which must be caught.
            out.println("Value at: " + i + " = " + list.get(i));
        }
        out.close();
    }
}

The first line in boldface is a call to a constructor. The constructor initializes an output stream on a file. If the file cannot be opened, the constructor throws an IOException. The second boldface line is a call to the ArrayList class’s get method, which throws an IndexOutOfBoundsException if the value of its argument is too small (less than 0) or too large (more than the number of elements currently contained by the ArrayList.

If you try to compile the ListOfNumbers class, the compiler prints an error message about the exception thrown by the FileWriter constructor. However, it does not display an error message about the exception thrown by get(). The reason is that the exception thrown by the constructor, IOException, is a checked exception, and the one thrown by the get() method, IndexOutOfBoundsException, is an unchecked exception.

Now that you’re familiar with the ListOfNumbers class and where the exceptions can be thrown within it, you’re ready to write exception handlers to catch and handle those exceptions.

The Try Block

The first step in constructing an exception handler is to enclose the code that might throw an exception within a try block. In general, a try block looks like the following:

try {
    code
}
catch and finally blocks . . .

The segment in the example labeled code contains one or more legal lines of code that could throw an exception. (The catch and finally blocks are explained in the next two subsections.)

To construct an exception handler for the writeList() method from the ListOfNumbers class, enclose the exception-throwing statements of the writeList() method within a try block. There is more than one way to do this. You can put each line of code that might throw an exception within its own try block and provide separate exception handlers for each. Or, you can put all the writeList() code within a single try block and associate multiple handlers with it. The following listing uses one try block for the entire method because the code in question is very short.

private List<Integer> list;
private static final int SIZE = 10;

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entered try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    }
    catch and finally blocks  . . .
}

If an exception occurs within the try block, that exception is handled by an exception handler associated with it. To associate an exception handler with a try block, you must put a catch block after it; the next section, The catch Blocks, shows you how.

The Catch Blocks

You associate exception handlers with a try block by providing one or more catch blocks directly after the try block. No code can be between the end of the try block and the beginning of the first catch block.

try {

} catch (ExceptionType name) {

} catch (ExceptionType name) {

}

Each catch block is an exception handler that handles the type of exception indicated by its argument. The argument type, ExceptionType, declares the type of exception that the handler can handle and must be the name of a class that inherits from the Throwable class. The handler can refer to the exception with name.

The catch block contains code that is executed if and when the exception handler is invoked. The runtime system invokes the exception handler when the handler is the first one in the call stack whose ExceptionType matches the type of the exception thrown. The system considers it a match if the thrown object can legally be assigned to the exception handler’s argument.

The following are two exception handlers for the writeList() method:

try {

} catch (IndexOutOfBoundsException e) {
    System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
    System.err.println("Caught IOException: " + e.getMessage());
}

Exception handlers can do more than just print error messages or halt the program. They can do error recovery, prompt the user to make a decision, or propagate the error up to a higher-level handler using chained exceptions, as described in the Chained Exceptions section.

Multi-Catching Exceptions

You can catch more than one type of exception with one exception handler, with the multi-catch pattern.

In Java SE 7 and later, a single catch block can handle more than one type of exception. This feature can reduce code duplication and lessen the temptation to catch an overly broad exception.

In the catch clause, specify the types of exceptions that block can handle, and separate each exception type with a vertical bar (|):

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Note: If a catch block handles more than one exception type, then the catch parameter is implicitly final. In this example, the catch parameter ex is final and therefore you cannot assign any values to it within the catch block.

The Finally Block

The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break. Putting cleanup code in a finally block is always a good practice, even when no exceptions are anticipated.

Note: If the JVM exits while the try or catch code is being executed, then the finally block may not execute.

The try block of the writeList() method that you’ve been working with here opens a PrintWriter. The program should close that stream before exiting the writeList() method. This poses a somewhat complicated problem because writeList()‘s try block can exit in one of three ways.

  1. The new FileWriter statement fails and throws an IOException.
  2. The list.get(i) statement fails and throws an IndexOutOfBoundsException.
  3. Everything succeeds and the try block exits normally.

The runtime system always executes the statements within the finally block regardless of what happens within the try block. So it’s the perfect place to perform cleanup.

The following finally block for the writeList() method cleans up and then closes the PrintWriter.

finally {
    if (out != null) {
        System.out.println("Closing PrintWriter");
        out.close();
    } else {
        System.out.println("PrintWriter not open");
    }
}

Important: The finally block is a key tool for preventing resource leaks. When closing a file or otherwise recovering resources, place the code in a finally block to ensure that resource is always recovered.

Consider using the try-with-resources statement in these situations, which automatically releases system resources when no longer needed. The try-with-resources Statement section has more information.

The Try-with-resources Statement

The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.

The following example reads the first line from a file. It uses an instance of BufferedReader to read data from the file. BufferedReader is a resource that must be closed after the program is finished with it:

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
                   new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

In this example, the resource declared in the try-with-resources statement is a BufferedReader. The declaration statement appears within parentheses immediately after the try keyword. The class BufferedReader, in Java SE 7 and later, implements the interface java.lang.AutoCloseable. Because the BufferedReader instance is declared in a try-with-resource statement, it will be closed regardless of whether the try statement completes normally or abruptly (as a result of the method BufferedReader.readLine() throwing an IOException.

Prior to Java SE 7, you can use a finally block to ensure that a resource is closed regardless of whether the try statement completes normally or abruptly. The following example uses a finally block instead of a try-with-resources statement:

static String readFirstLineFromFileWithFinallyBlock(String path)
                                                     throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}

However, in this example, if the methods readLine() and close both throw exceptions, then the method readFirstLineFromFileWithFinallyBlock() throws the exception thrown from the finally block; the exception thrown from the try block is suppressed. In contrast, in the example readFirstLineFromFile(), if exceptions are thrown from both the try block and the try-with-resources statement, then the method readFirstLineFromFile() throws the exception thrown from the try block; the exception thrown from the try-with-resources block is suppressed. In Java SE 7 and later, you can retrieve suppressed exceptions; see the section Suppressed Exceptions for more information.

You may declare one or more resources in a try-with-resources statement. The following example retrieves the names of the files packaged in the zip file zipFileName and creates a text file that contains the names of these files:

public static void writeToFileZipFileContents(String zipFileName,
                                           String outputFileName)
                                           throws java.io.IOException {

    java.nio.charset.Charset charset =
         java.nio.charset.StandardCharsets.US_ASCII;
    java.nio.file.Path outputFilePath =
         java.nio.file.Paths.get(outputFileName);

    // Open zip file and create output file with
    // try-with-resources statement

    try (
        java.util.zip.ZipFile zf =
             new java.util.zip.ZipFile(zipFileName);
        java.io.BufferedWriter writer =
            java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
    ) {
        // Enumerate each entry
        for (java.util.Enumeration entries =
                                zf.entries(); entries.hasMoreElements();) {
            // Get the entry name and write it to the output file
            String newLine = System.getProperty("line.separator");
            String zipEntryName =
                 ((java.util.zip.ZipEntry)entries.nextElement()).getName() +
                 newLine;
            writer.write(zipEntryName, 0, zipEntryName.length());
        }
    }
}

In this example, the try-with-resources statement contains two declarations that are separated by a semicolon: ZipFile and BufferedWriter. When the block of code that directly follows it terminates, either normally or because of an exception, the close() methods of the BufferedWriter and ZipFile objects are automatically called in this order. Note that the close methods of resources are called in the opposite order of their creation.

The following example uses a try-with-resources statement to automatically close a java.sql.Statement object:

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);

        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");

            System.out.println(coffeeName + ", " + supplierID + ", " +
                               price + ", " + sales + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

The resource java.sql.Statement used in this example is part of the JDBC 4.1 and later API.

Note: A try-with-resources statement can have catch and finally blocks just like an ordinary try statement. In a try-with-resources statement, any catch or finally block is run after the resources declared have been closed.

Suppressed Exceptions

An exception can be thrown from the block of code associated with the try-with-resources statement. In the example writeToFileZipFileContents(), an exception can be thrown from the try block, and up to two exceptions can be thrown from the try-with-resources statement when it tries to close the ZipFile and BufferedWriter objects. If an exception is thrown from the try block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception thrown by the block is the one that is thrown by the writeToFileZipFileContents() method. You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed() method from the exception thrown by the try block.

Classes That Implement the AutoCloseable or Closeable Interface

See the Javadoc of the AutoCloseable and Closeable interfaces for a list of classes that implement either of these interfaces. The Closeable interface extends the AutoCloseable interface. The close() method of the Closeable interface throws exceptions of type IOException while the close() method of the AutoCloseable interface throws exceptions of type Exception. Consequently, subclasses of the AutoCloseable interface can override this behavior of the close() method to throw specialized exceptions, such as IOException, or no exception at all.

Putting It All Together

The previous sections described how to construct the try, catch, and finally code blocks for the writeList() method in the ListOfNumbers class. Now, let’s walk through the code and investigate what can happen.

When all the components are put together, the writeList() method looks like the following.

public void writeList() {
    PrintWriter out = null;

    try {
        System.out.println("Entering" + " try statement");

        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           +  e.getMessage());

    } catch (IOException e) {
        System.err.println("Caught IOException: " +  e.getMessage());

    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        }
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

As mentioned previously, this method’s try block has three different exit possibilities; here are two of them.

  1. Code in the try statement fails and throws an exception. This could be an IOException caused by the new FileWriter statement or an IndexOutOfBoundsException caused by a wrong index value in the for loop.
  2. Everything succeeds and the try statement exits normally.

Let’s look at what happens in the writeList() method during these two exit possibilities.

Scenario 1: An Exception Occurs

The statement that creates a FileWriter can fail for a number of reasons. For example, the constructor for the FileWriter throws an IOException if the program cannot create or write to the file indicated.

When FileWriter throws an IOException, the runtime system immediately stops executing the try block; method calls being executed are not completed. The runtime system then starts searching at the top of the method call stack for an appropriate exception handler. In this example, when the IOException occurs, the FileWriter constructor is at the top of the call stack. However, the FileWriter constructor doesn’t have an appropriate exception handler, so the runtime system checks the next method — the writeList() method — in the method call stack. The writeList() method has two exception handlers: one for IOException and one for IndexOutOfBoundsException.

The runtime system checks writeList()‘s handlers in the order in which they appear after the try statement. The argument to the first exception handler is IndexOutOfBoundsException. This does not match the type of exception thrown, so the runtime system checks the next exception handler — IOException. This matches the type of exception that was thrown, so the runtime system ends its search for an appropriate exception handler. Now that the runtime has found an appropriate handler, the code in that catch block is executed.

After the exception handler executes, the runtime system passes control to the finally block. Code in the finally block executes regardless of the exception caught above it. In this scenario, the FileWriter was never opened and doesn’t need to be closed. After the finally block finishes executing, the program continues with the first statement after the finally block.

Here’s the complete output from the ListOfNumbers program that appears when an IOException is thrown.

Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open

Scenario 2: The try Block Exits Normally

In this scenario, all the statements within the scope of the try block execute successfully and throw no exceptions. Execution falls off the end of the try block, and the runtime system passes control to the finally block. Because everything was successful, the PrintWriter is open when control reaches the finally block, which closes the PrintWriter. Again, after the finally block finishes executing, the program continues with the first statement after the finally block.

Here is the output from the ListOfNumbers program when no exceptions are thrown.

Entering try statement
Closing PrintWriter

Catching and Handling Exceptions

Исключение — ошибка, которая нарушает нормальную работу программы. Java обеспечивает надежный объектно-ориентированный способ обработки исключений. Именно его мы и будем изучать в этом руководстве. 

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

Java — объектно-ориентированный язык программирования, поэтому всякий раз, когда происходит ошибка при выполнении инструкции, создается объект-исключение, а затем нормальный ход выполнения программы останавливается и JRE пытается найти кого-то, кто может справиться (обработать) это исключение. Объект-исключение содержит много информации об отладке, а именно номер строки, где произошло исключение, тип исключения и т.д.

Что и как происходит, когда появляется ошибка

Когда в методе происходит исключение, то процесс создания объекта-исключения и передачи его в Runtime Environment называется «бросать исключение».

После создания исключения, Java Runtime Environment пытается найти обработчик исключения.

Обработчик исключения — блок кода, который может обрабатывать объект-исключение.

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

Пример

У нас есть 3 метода, каждый из которых вызывает друг-друга: А -> В -> С (А вызывает В, а В вызывает С). Если исключение появляется в методе C, то поиск соответствующего обработчика будет происходить в обратном порядке: С -> В -> А (сначала там, где было исключение — в С, если там нет обработчика, то идем в метод В — если тут тоже нет, то идем в А).

Если соответствующий обработчик исключений будет найден, то объект-исключение передаётся обработчику.

Обработать исключение — значит «поймать исключение».

Если обработчик исключений не был найден, то программа завершает работу и печатает информации об исключении.

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

Основные элементы обработки исключений в Java

Мы используем определенные ключевые слова в для создания блока обработки исключений. Давайте рассмотрим их на примере. Также мы напишем простую программу для обработки исключений.

  • Бросить исключение (throw) — ключевое слово, которое используется для того, чтобы бросить исключение во время выполнения. Мы знаем, что Java Runtime начинает поиск обработчика исключений как только оно будет брошено, но часто нам самим нужно генерировать исключение в нашем коде, например, в программе авторизации, если какое-то поле null. Именно для таких случаем и существует возможность бросить исключение.
  • throws — когда мы бросаем исключение в методе и не обрабатываем его, то мы должны использовать ключевое слово throws в сигнатуре метода для того, чтобы пробросить исключение для обработки в другом методе. Вызывающий метод может обработать это исключение или пробросить его еще дальше с помощью throws в сигнатуре метода. Следует отметить, что пробрасывать можно сразу несколько исключений.
  • Блок try-catch используется для обработки исключений в коде. Слово try — это начало блока обработки, catch — конец блока для обработки исключений. Мы можем использовать сразу несколько блоков catch при одном try. catch в качестве параметра принимает тип исключения для обработки.
  • finally — необязательная завершающая конструкция блока try-catch. Как только исключение остановило процесс исполнения программы, в finally мы можем безопасно освободить какие-то открытые ресурсы. Следует отметить, что finally блок выполняется всегда — не смотря на появление исключительной ситуации.

Давайте посмотрим простую программу обработки исключений в Java.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

package ua.com.prologistic;

import java.io.FileNotFoundException;

import java.io.IOException;

public class ExceptionHandling {

    // в методе main() пробрасывается сразу несколько исключений

    public static void main(String[] args) throws FileNotFoundException, IOException {

        // в блоке try-catch перехватываются сразу несколько исключений вызовом дополнительного catch(…)

        try{

            testException(5);

            testException(10);

        }catch(FileNotFoundException e){

            e.printStackTrace();

        }catch(IOException e){

            e.printStackTrace();

        }finally{

            System.out.println(«Необязательный блок, но раз уже написан, то выполнятся будет не зависимо от того было исключение или нет»);          

        }

        testException(15);

    }

    // тестовый метод создания, обработки и пробрасывания исключения

    public static void testException(int i) throws FileNotFoundException, IOException{

        if(i < 0){

            FileNotFoundException myException = new FileNotFoundException(«число меньше 0: « + i);

            throw myException;

        }else if(i > 10){

            throw new IOException(«Число должно быть в пределах от 0 до 10»);

        }

    }

}

А в консоле эта программа напишет такое:

java.io.FileNotFoundException: число меньше 0: 5

    at ua.com.prologistic.ExceptionHandling.testException(ExceptionHandling.java:24)

    at ua.com.prologistic.ExceptionHandling.main(ExceptionHandling.java:10)

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

Exception in thread «main» java.io.IOException: Число должно быть в пределах от 0 до 10

    at ua.com.prologistic.ExceptionHandling.testException(ExceptionHandling.java:27)

    at ua.com.prologistic.ExceptionHandling.main(ExceptionHandling.java:19)

Обратите внимание, что метод testException() бросает исключение, используя ключевое слово throw, а в сигнатуре метода используется ключевое слово throws, чтобы дать понять вызывающему методу тип исключений, которые может бросить testException().

Важные моменты в обработке исключений:

  • Нельзя использовать блоки catch или finally без блока try.
  • Блок try также может быть использован только с catch блоком, или только с finally блоком, или с тем и другим блоком.
  • Мы можем использовать несколько блоков catch только с одним try.
  • try-catch блоки могут быть вложенными — этим они очень похожи на if-else конструкции.
  • Мы можем использовать только один, блок finally в одном try-catch.

Иерархия исключений в Java

Java исключения являются иерархическими, а наследование используется для категоризации различных типов исключений. Throwable — родительский класс в иерархии Java исключений. Он имеет два дочерних объекта — Error и Exception. Исключения далее разделены на проверяемые исключения и исключения времени выполнения.

  1. Error — это тип ошибок, которые выходят за рамки вашей программы, их невозможно предвидеть или обработать. Это может быть аппаратный сбой, «поломка» JVM или ошибка памяти. Именно для таких необычных ситуаций есть отдельная иерархия ошибок. Мы должны просто знать, что такие ошибки есть и не можем справиться с такими ситуациями. Примеры Error: OutOfMemoryError и StackOverflowError.
  2. Проверяемые исключения (Checked Exceptions) — тип исключений, которые мы можем предвидеть в программе и попытаться обработать, например, FileNotFoundException. Мы должны поймать это исключение и написать внятное и полезное сообщение пользователю о том, что произошло (также желательно логировать ошибки). Exception — родительский класс всех проверяемых исключений (Checked Exceptions). Если мы бросили проверяемое исключение, то должны поймать его в том же методе или должны пробросить его с помощью ключевого слова throws.
  3. Runtime Exception — это ошибки программиста. Например, пытаясь получить элемент из массива, мы должны проверить длину массива, прежде чем пытаться получить элемент — в противном случае это может быть брошен ArrayIndexOutOfBoundException. RuntimeException — родительский класс для всех Runtime исключений. Если мы сами бросаем Runtime Exception в методе, то не обязательно указывать в сигнатуре метода ключевое слово throws.

На рисунке 1 представлена иерархия исключений в Java:

иерархия исключений в Java

Рисунок 1 — Иерархия исключений в Java

 Полезные методы в обработке исключений

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

Полезные методы класса Throwable:

  1. public String getMessage() — этот метод возвращает сообщение, которое было создано при создании исключения через конструктор.
  2. public String getLocalizedMessage() — метод, который переопределяют подклассы для локализации конкретное сообщение об исключении. В реализации Throwable класса этот метод просто использует метод getMessage(), чтобы вернуть сообщение об исключении (Throwable на вершине иерархии — ему нечего локализировать, поэтому он вызывает getMessage()).
  3. public synchronized Throwable getCause() — этот метод возвращает причину исключения или идентификатор в виде null, если причина неизвестна.
  4. public String toString() — этот метод возвращает информацию о Throwable в формате String.
  5. public void printStackTrace() — этот метод выводит информацию трассировки стека в стандартный поток ошибок, этот метод перегружен и мы можем передать PrintStream или PrintWriter в качестве аргумента, чтобы написать информацию трассировки стека в файл или поток.

Автоматическое управление ресурсами и улучшения блока перехвата ошибок в Java 7

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

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

catch(IOException | SQLException | Exception ex){

     //что-то сделать с перехваченной ошибкой…

}

Как видим, здесь блок catch перехватывает сразу несколько исключений — это очень красиво, компактно и удобно.

В большинстве случаев мы используем блок finally для того, чтобы закрыть открытые потоки, подключения или освободить другие ресурсы. Очень часто мы забываем закрыть и получаем runtime исключения. Такие исключения трудно отлаживать. Поэтому в Java 7 был введен try с ресурсами, где мы можем открыть ресурс в самом try и использовать его внутри блока try-catch. Когда программа заканчивает выполнение блока try-catch, то среда выполнения автоматически закрывает эти ресурсы. Вот пример try-catch блока с ресурсами:

// try c ресурсами

try (MyResource mr = new MyResource()) {

            System.out.println(«Красивый и компактный код в try c ресурсами»);

        } catch (Exception e) {

            e.printStackTrace();

        }

Создание своих классов исключений

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

Вот пример своего класса исключений и его использование:

package ua.com.prologistic;

// наследуемся от класс Exception

public class MyException extends Exception {

    private String errorCode = «Unknown_Exception»;

    public MyException(String message, String errorCode){

        super(message);

        this.errorCode = errorCode;

    }

    public String getErrorCode(){

        return this.errorCode;

    }

}

А теперь проверим в работе наш класс MyException:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

package ua.com.prologistic;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.io.InputStream;

public class CustomExceptionExample {

    public static void main(String[] args) throws MyException {

        try {

            processFile(«file.txt»);

        } catch (MyException e) {

            processErrorCodes(e);

        }

    }

    // метод для обработки ошибок

    private static void processErrorCodes(MyException e) throws MyException {

        // здесь мы ищем указанный при выбросе исключения код ошибки и сообщаем пользователю что произошло

        switch(e.getErrorCode()){

        case «BAD_FILE_TYPE»:

            System.out.println(«Неподходящий тип файла»);

            throw e;

        case «FILE_NOT_FOUND_EXCEPTION»:

            System.out.println(«Файл не найден»);

            throw e;

        case «FILE_CLOSE_EXCEPTION»:

            System.out.println(«Ошибка при закрытии файла»);

            break;

        default:

            System.out.println(«Произошла неизвестная ошибка « + e.getMessage());

            e.printStackTrace();

        }

    }

    // метод для работы с файлом, который пробрасывает наш тип исключений

    private static void processFile(String file) throws MyException {      

        InputStream fis = null;

        try {

            fis = new FileInputStream(file);

        } catch (FileNotFoundException e) {

            // здесь мы бросаем исключение с указанием кода ошибки

            throw new MyException(e.getMessage(),«FILE_NOT_FOUND_EXCEPTION»);

        }finally{

            try {

                if(fis !=null)fis.close();

            } catch (IOException e) {

                // здесь мы бросаем исключение с указанием кода ошибки

                throw new MyException(e.getMessage(),«FILE_CLOSE_EXCEPTION»);

            }

        }

    }

}

Полезные советы по обработке исключений в Java

  1. Не используйте для перехвата исключений класс Exception. В иерархии исключений есть множество классов на все случаи жизни вашей программы, которые не только эффективно обработают конкретную ошибку, но и предоставят полезную для пользователя и отладки информацию.
  2. Бросайте исключение как можно раньше. Это является хорошей практикой программирования на Java.
  3. Ловите исключения только тогда, когда сможете эффективно для пользователя и отладки их обработать.
  4. Освобождайте ресурсы. Перехватывая исключение всегда закрывайте открытые ресурсы. Еще проще и эффективнее это делать с Java 7. Используйте try с ресурсами для лаконичного и красивого кода.
  5. Логируйте исключения. Логируйте сообщения, которые предоставляет исключение. В большинстве случаев это даст вам четкое понимание причин и поможет в отладке. Не оставляйте пустым блок catch, иначе он будет просто поглощать исключение без каких-либо значимых деталей для отладки.
  6. Один catch для нескольких исключений. Используйте преимущества Java 7 для удобства и красоты вашего кода.
  7. Используйте свои исключения. Это позволит вам лучше чувствовать свою программу и эффективнее с ней работать.
  8. Соглашения об именовании. Когда вы создать свои классы исключений, следите за тем, что из самого названия класса будет ясно, что это исключение.
  9. Используйте исключения с умом. Бросить исключение — достаточно дорогостоящая в Java операция. Возможно, в некоторых случаях будем уместно не бросать исключений, а вернуть, например, логическую переменную, которая обозначала успешное или не успешное выполнение метода.
  10. Документируйте исключения. Желательно писать javadoc @throws для ваших исключений. Это будет особенно полезно в тех случаях, когда ваша программа предоставляет интерфейс для работы с другими приложениями.

Вот и все, что нужно знать об обработке исключений в Java.

Понравилась статья? Поделить с друзьями:
  • Обработка ошибок wpf
  • Обработка ошибок node js express
  • Обработка ошибок windows forms
  • Обработка ошибок mysql php
  • Обработка ошибок visual basic